Files
shaders/ice.html
T
emil28092005 27d238cbe6 Ice slab with layered plasma, two-pass refraction
Pass 1 renders eight plasma sheets into an atlas with linear depth in alpha;
pass 2 marches the refracted ray to each sheet and absorbs it by real path
length, so near and far layers separate under pointer parallax. Plasma cycle
repeats exactly every 30 s; - / = change speed.
2026-10-30 19:56:16 +03:00

93 lines
12 KiB
HTML

<!doctype html><html><head><meta charset=utf-8><title>ice</title>
<style>html,body{margin:0;height:100%;background:#000;overflow:hidden}
canvas{display:block;width:100vw;height:100vh}
#osd{position:fixed;left:14px;bottom:12px;font:13px ui-monospace,monospace;color:#9cf;opacity:.7;pointer-events:none}</style>
</head><body><canvas id=c></canvas><div id=osd></div><script>
const FRAG_A = atob('Ly8gUGFzcyBBOiBpY3kgc2NlbmUuIFJHQiA9IGNvbG91ciwgQSA9IGxpbmVhciBkZXB0aCAvIDguMCAgKG91ciB6LWJ1ZmZlcikKcHJlY2lzaW9uIGhpZ2hwIGZsb2F0Owp1bmlmb3JtIHZlYzIgdV9yZXM7IHVuaWZvcm0gZmxvYXQgdV90aW1lOyB1bmlmb3JtIHZlYzIgdV9tb3VzZTsKCmZsb2F0IHNkU3BoZXJlKHZlYzMgcCwgZmxvYXQgcil7IHJldHVybiBsZW5ndGgocCkgLSByOyB9CgpmbG9hdCBtYXAodmVjMyBwKXsKICBmbG9hdCBkID0gc2RTcGhlcmUocCwgMS4wKTsKICBmb3IoaW50IGk9MDtpPDM7aSsrKXsKICAgIGZsb2F0IGZpID0gZmxvYXQoaSk7CiAgICB2ZWMzIHEgPSBwOwogICAgZmxvYXQgYSA9IHVfdGltZSowLjUgKyBmaSoyLjE7CiAgICBxLnh6ID0gbWF0Mihjb3MoYSksLXNpbihhKSxzaW4oYSksY29zKGEpKSAqIHEueHo7CiAgICBkID0gbWluKGQsIHNkU3BoZXJlKHEgLSB2ZWMzKDAuNzUsMC4wLDAuMCksIDAuMzQpKTsKICB9CiAgcmV0dXJuIGQ7Cn0KCnZvaWQgbWFpbigpewogIHZlYzIgdXYgPSAoZ2xfRnJhZ0Nvb3JkLnh5IC0gMC41KnVfcmVzKS91X3Jlcy55OwogIHZlYzMgcm8gPSB2ZWMzKDAuMCwwLjAsOC41KTsKICB2ZWMzIHJkID0gbm9ybWFsaXplKHZlYzModXYsIC0xLjYpKTsKCiAgZmxvYXQgdCA9IDAuMDsgaW50IGhpdCA9IDA7CiAgZm9yKGludCBpPTA7aTw5MDtpKyspewogICAgZmxvYXQgZCA9IG1hcChybyArIHJkKnQpOwogICAgaWYoZCA8IDAuMDAxKXsgaGl0ID0gMTsgYnJlYWs7IH0KICAgIHQgKz0gZDsKICAgIGlmKHQgPiAxMi4wKSBicmVhazsKICB9CgogIHZlYzMgY29sID0gdmVjMygwLjAwOCwwLjAyLDAuMDQ1KTsKICBjb2wgKz0gMC4wNypwb3coMS4wIC0gYWJzKHV2LnkpKjAuOSwgNC4wKSp2ZWMzKDAuMTUsMC4zNSwwLjYpOyAgIC8vIGNvbGQgaGF6ZQogIGZsb2F0IGRlcHRoID0gMTIuMDsKCiAgaWYoaGl0ID09IDEpewogICAgZGVwdGggPSB0OwogICAgdmVjMyBwb3MgPSBybyArIHJkKnQ7CiAgICB2ZWMzIG4gPSBub3JtYWxpemUodmVjMyhtYXAocG9zK3ZlYzMoMC4wMDEsMCwwKSktbWFwKHBvcy12ZWMzKDAuMDAxLDAsMCkpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgbWFwKHBvcyt2ZWMzKDAsMC4wMDEsMCkpLW1hcChwb3MtdmVjMygwLDAuMDAxLDApKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1hcChwb3MrdmVjMygwLDAsMC4wMDEpKS1tYXAocG9zLXZlYzMoMCwwLDAuMDAxKSkpKTsKICAgIHZlYzMgbCA9IG5vcm1hbGl6ZSh2ZWMzKDAuNDUsMC44NSwwLjU1KSk7CiAgICBmbG9hdCBkaWYgPSBtYXgoZG90KG4sbCksMC4wKTsKICAgIGZsb2F0IGZyZSA9IHBvdygxLjAgLSBtYXgoZG90KG4sLXJkKSwwLjApLCAzLjApOwoKICAgIC8vIGZyb3plbiBsYXllcnMgaW5zdGVhZCBvZiByYWluYm93CiAgICBmbG9hdCBmID0gMC41ICsgMC41KnNpbihwb3MueSoyMC4wICsgdV90aW1lKjIuMCArIHBvcy54KjUuMCArIG4ueiozLjApOwogICAgdmVjMyBhbGIgPSBtaXgodmVjMygwLjkwLDAuOTcsMS4wMCksIHZlYzMoMC4zNCwwLjY2LDAuOTIpLCBmKTsKICAgIGNvbCA9IGFsYiooMC4wNiArIDEuMDUqZGlmKSArIHZlYzMoMC42MCwwLjkwLDEuMCkqZnJlKjEuNjsKICAgIGNvbCAqPSAwLjg2ICsgMC4xNCpzaW4ocG9zLnkqMzguMCArIHVfdGltZSo0LjApOwogIH0KCiAgLy8gZGVwdGggZm9nOiBmYXIgdGhpbmdzIGZhZGUgaW50byB0aGUgY29sZCB2b2lkCiAgY29sID0gbWl4KGNvbCwgdmVjMygwLjAzLDAuMTAsMC4xOSksIHNtb290aHN0ZXAoMy4wLCAxMi4wLCBkZXB0aCkqMC44NSk7CiAgZ2xfRnJhZ0NvbG9yID0gdmVjNChjb2wsIGNsYW1wKGRlcHRoLzEyLjAsIDAuMCwgMS4wKSk7Cn0K');
const FRAG_B = atob('Ly8gUGFzcyBCOiB0aGUgaWNlIHdhbGwuIFJlZnJhY3RzIHBhc3MgQSB0aHJvdWdoIGEgZGlzcGxhY2VkIHN1cmZhY2UgdXNpbmcgaXRzIGRlcHRoIGJ1ZmZlci4KcHJlY2lzaW9uIGhpZ2hwIGZsb2F0Owp1bmlmb3JtIHZlYzIgdV9yZXM7IHVuaWZvcm0gZmxvYXQgdV90aW1lOyB1bmlmb3JtIHZlYzIgdV9tb3VzZTsKdW5pZm9ybSBzYW1wbGVyMkQgdV9zY2VuZTsKCmNvbnN0IGZsb2F0IElDRV9aID0gMS4yMDsgICAgICAvLyBmcm9udCBmYWNlIG9mIHRoZSBpY2UKY29uc3QgdmVjMyAgU0lHX0EgPSB2ZWMzKDAuMTMsMC4wNDIsMC4wMjApOyAgLy8gYWJzb3JwdGlvbiBpbiBpY2UgKHJlZCBkaWVzIGZpcnN0IC0+IGJsdWUpCmNvbnN0IHZlYzMgIFNJR19CID0gdmVjMygwLjE2LDAuMDU1LDAuMDI2KTsgIC8vIGFic29ycHRpb24gaW4gdGhlICJkZXB0aCBmb2ciIGJlaGluZAoKZmxvYXQgaGFzaCh2ZWMyIHApeyByZXR1cm4gZnJhY3Qoc2luKGRvdChwLHZlYzIoMTI3LjEsMzExLjcpKSkqNDM3NTguNTQ1Myk7IH0KZmxvYXQgbm9pc2UodmVjMiBwKXsKICB2ZWMyIGk9Zmxvb3IocCksIGY9ZnJhY3QocCk7IGY9ZipmKigzLjAtMi4wKmYpOwogIHJldHVybiBtaXgobWl4KGhhc2goaSksaGFzaChpK3ZlYzIoMSwwKSksZi54KSwgbWl4KGhhc2goaSt2ZWMyKDAsMSkpLGhhc2goaSt2ZWMyKDEsMSkpLGYueCksIGYueSk7Cn0KZmxvYXQgZmJtKHZlYzIgcCl7CiAgZmxvYXQgYT0wLjUsIHM9MC4wOwogIGZvcihpbnQgaT0wO2k8NTtpKyspeyBzKz1hKm5vaXNlKHApOyBwKj0yLjAyOyBhKj0wLjU7IH0KICByZXR1cm4gczsKfQoKLy8gaGVpZ2h0IG9mIHRoZSBpY2Ugc3VyZmFjZSBhdCAoeCx5KSDigJQgYWxtb3N0IGZyb3plbiwgZHJpZnRzIHZlcnkgc2xvd2x5CmZsb2F0IHN1cmYodmVjMiBwKXsKICBmbG9hdCB0ID0gdV90aW1lKjAuMDQ7CiAgcmV0dXJuIDAuMDU1KmZibShwKjEuNyArIHZlYzIodCwtdCkpICsgMC4wMjIqZmJtKHAqNC4zIC0gdmVjMih0KjEuNyx0KSk7Cn0KCnZlYzMgc3VyZmFjZU5vcm1hbCh2ZWMyIHApewogIGZsb2F0IGUgPSAwLjAwNDsKICBmbG9hdCBoeCA9IHN1cmYocCt2ZWMyKGUsMC4wKSkgLSBzdXJmKHAtdmVjMihlLDAuMCkpOwogIGZsb2F0IGh5ID0gc3VyZihwK3ZlYzIoMC4wLGUpKSAtIHN1cmYocC12ZWMyKDAuMCxlKSk7CiAgcmV0dXJuIG5vcm1hbGl6ZSh2ZWMzKC1oeC8oMi4wKmUpLCAtaHkvKDIuMCplKSwgMS4wKSk7Cn0KCi8vIHByb2plY3QgYSB3b3JsZCBwb2ludCB3aXRoIHRoZSBzY2VuZSBjYW1lcmEgKHJvPSgwLDAsMy42KSwgZm92IHZpYSAxLjYpCnZlYzIgcHJvamVjdCh2ZWMzIHApewogIHZlYzMgZCA9IHAgLSB2ZWMzKDAuMCwwLjAsOC41KTsKICByZXR1cm4gZC54eS8oLWQueikqMS42Owp9Cgp2b2lkIG1haW4oKXsKICB2ZWMyIHV2ID0gKGdsX0ZyYWdDb29yZC54eSAtIDAuNSp1X3JlcykvdV9yZXMueTsKICB2ZWMzIHJvID0gdmVjMygwLjAsMC4wLDMuNik7CiAgdmVjMyByZCA9IG5vcm1hbGl6ZSh2ZWMzKHV2LCAtMS42KSk7CgogIC8vIC0tLSBmcm9udCBzdXJmYWNlOiBwbGFuZSB6PUlDRV9aIGRpc3BsYWNlZCBieSBzdXJmKCksIHR3byBmaXhlZC1wb2ludCBzdGVwcwogIGZsb2F0IHQgPSAoSUNFX1ogLSByby56KS9yZC56OwogIGZvcihpbnQgaT0wO2k8MjtpKyspewogICAgdmVjMyBwMCA9IHJvICsgcmQqdDsKICAgIHQgPSAoSUNFX1ogKyBzdXJmKHAwLnh5KSAtIHJvLnopL3JkLno7CiAgfQogIHZlYzMgcHMgPSBybyArIHJkKnQ7CiAgdmVjMyBuICA9IHN1cmZhY2VOb3JtYWwocHMueHkpOwogIGlmKGRvdChuLC1yZCkgPCAwLjApIG4gPSAtbjsKCiAgZmxvYXQgY29zaSA9IGNsYW1wKGRvdChuLC1yZCksIDAuMCwgMS4wKTsKICBmbG9hdCBmcmVzID0gMC4wMjUgKyAwLjk3NSpwb3coMS4wIC0gY29zaSwgNS4wKTsKICB2ZWMzIHNreSA9IG1peCh2ZWMzKDAuMDE1LDAuMDUsMC4xMCksIHZlYzMoMC40MiwwLjY4LDAuOTgpLCBzbW9vdGhzdGVwKC0wLjM1LDAuOTUsbi55KSk7CiAgc2t5ICs9IHZlYzMoMC45LDAuOTcsMS4wKSpwb3coY29zaSwgNjAuMCk7ICAgICAgICAgICAgICAgICAgICAgIC8vIHNwZWN1bGFyIGdsaW50CgogIC8vIC0tLSByZWZyYWN0aW9uIHdpdGggY2hyb21hdGljIGRpc3BlcnNpb24gZG93biB0byB0aGUgYmFjayBmYWNlCiAgZmxvYXQgYmFja1ogPSBJQ0VfWiAtICgwLjU1ICsgMC4xOCpmYm0ocHMueHkqMC45KSk7CiAgdmVjMyBzY2VuZUNvbCA9IHZlYzMoMC4wKTsKICBmbG9hdCBwYXRoSWNlID0gMC4wOwogIGZvcihpbnQgYz0wO2M8MztjKyspewogICAgZmxvYXQgaW9yID0gMS4zMSArIGZsb2F0KGMpKjAuMDIyOyAgICAgICAgICAgICAgICAgICAgICAgICAgICAvLyAxLjMxIC4uIDEuMzU0CiAgICB2ZWMzIGQyID0gcmVmcmFjdChyZCwgbiwgMS4wL2lvcik7CiAgICBpZihsZW5ndGgoZDIpIDwgMC41KSBkMiA9IHJkOyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAvLyB0b3RhbCBpbnRlcm5hbCByZWZsZWN0aW9uIGd1YXJkCiAgICBmbG9hdCB0MiA9IChiYWNrWiAtIHBzLnopL2QyLno7CiAgICB2ZWMzIHBlID0gcHMgKyBkMip0MjsKICAgIHBhdGhJY2UgPSB0MjsKICAgIHZlYzIgc3V2ID0gcHJvamVjdChwZSk7CiAgICB2ZWMyIHRleFVWID0gKHN1dip1X3Jlcy55ICsgMC41KnVfcmVzKS91X3JlczsKICAgIHZlYzQgcyA9IHRleHR1cmUyRCh1X3NjZW5lLCBjbGFtcCh0ZXhVViwgdmVjMigwLjAwMSksIHZlYzIoMC45OTkpKSk7CiAgICBzY2VuZUNvbFtjXSA9IHNbY107CiAgfQoKICAvLyAtLS0gei1idWZmZXI6IGhvdyBtdWNoIGljZS9mb2cgdGhlIHZpZXcgcmF5IGhhZCB0byBjcm9zcyB0byB0aGUgb2JqZWN0CiAgdmVjMiAgYmFzZVVWID0gZ2xfRnJhZ0Nvb3JkLnh5L3VfcmVzOwogIGZsb2F0IGRTY2VuZSA9IHRleHR1cmUyRCh1X3NjZW5lLCBiYXNlVVYpLmEgKiAxMi4wOyAgICAgICAgICAgICAgIC8vIGxpbmVhciBkZXB0aCBmcm9tIHBhc3MgQQogIGZsb2F0IGNlbGwgICA9IG1heCgwLjAsIGRTY2VuZSAtICh0ICsgcGF0aEljZSkpOyAgICAgICAgICAgICAgICAgLy8gaWNlLXRvLW9iamVjdCBnYXAKICB2ZWMzICBhYnNvcmJBID0gZXhwKC1TSUdfQSpwYXRoSWNlKTsgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC8vIGluc2lkZSB0aGUgc2xhYgogIHZlYzMgIGFic29yYkIgPSBleHAoLVNJR19CKmNlbGwpOyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLy8gZGVwdGggaGF6ZSBiZWhpbmQgaXQKCiAgdmVjMyBpY2VUaW50ID0gdmVjMygwLjgwLDAuOTQsMS4wKTsgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAvLyBwdXJlLCBjbGVhbiBibHVlIGljZQogIHZlYzMgY29sID0gc2NlbmVDb2wgKiAxLjEwICogYWJzb3JiQSAqIGFic29yYkIgKiBpY2VUaW50OwoKICAvLyAtLS0gYm9keSBkZXRhaWw6IHNtYWxsIHRyYXBwZWQgYnViYmxlcywgaGFpciBjcmFja3MsIGZyb3plbiBzaGVldHMgKHN0YXRpYywgdGhleSBsaXZlIGluIHRoZSBzbGFiKQogIHZlYzIgcSA9IHBzLnh5KjIuMjsKICBmbG9hdCB6b25lID0gc21vb3Roc3RlcCgwLjQyLCAwLjYyLCBmYm0ocSowLjcgKyAyMS4wKSk7ICAgICAgICAgIC8vIGNsZWFyIGljZSB2cyBidWJibHkgaWNlCiAgZmxvYXQgYnViID0gbm9pc2UocSoxOS4wICsgMy4xKSAqIG5vaXNlKHEqMTcuMCAtIDEuNyk7CiAgZmxvYXQgZG90cyA9IHNtb290aHN0ZXAoMC4wNDUsIDAuMDc1LCBidWIpICogbWl4KDAuMTAsIDEuMCwgem9uZSkgKiAwLjM0OwogIGZsb2F0IHJpZCAgPSBhYnMoZmJtKHEqMS45ICsgMTEuMCkqMi4wIC0gMS4wKTsKICBmbG9hdCBjcmFjayA9IHNtb290aHN0ZXAoMC4wMjIsIDAuMCwgcmlkKSAqIDAuNDU7CiAgZmxvYXQgc2hlZXQgPSBzbW9vdGhzdGVwKDAuNjIsIDAuNzgsIGZibSh2ZWMyKHEueSoxLjMsIHEueCowLjM1KSkgKTsKCiAgZmxvYXQgc3RyZWFrID0gc21vb3Roc3RlcCgwLjU1LDAuOTUsIGZibSh2ZWMyKHEueCo3LjAsIHEueSowLjM1KSArIDUuMCkpOwogIGZsb2F0IHNoZWVuICA9IHBvdyhtYXgoMC4wLCBkb3Qobiwgbm9ybWFsaXplKHZlYzMoMC4zNSwwLjc1LDAuNTUpKSkpLCA0MC4wKTsKICBjb2wgKz0gaWNlVGludCAqIChkb3RzICsgY3JhY2sqMC4yMiArIHN0cmVhayowLjA1ICsgc2hlZW4qMC41KSAqICgwLjI1ICsgMC43NSpjb3NpKTsKICBjb2wgKz0gaWNlVGludCAqIHNoZWV0ICogMC4wNiAqIHNtb290aHN0ZXAoMC4wLCAxLjIsIGNlbGwpOwoKICAvLyAtLS0gcmVmbGVjdGlvbiBvbiB0b3AsIHRoaWNrZXIgcmltIHdoZXJlIHRoZSBzbGFiIHR1cm5zIGF3YXkKICBjb2wgPSBtaXgoY29sLCBza3ksIGZyZXMqMC44NSk7CiAgY29sICs9IGljZVRpbnQgKiBwb3coMS4wIC0gY29zaSwgMi4wKSAqIDAuMTA7ICAgICAgICAgICAgICAgICAgICAvLyBmcm9zdGVkIGdyYXppbmcgZWRnZQoKICBjb2wgPSBwb3coY2xhbXAoY29sLCAwLjAsIDEuMCksIHZlYzMoMC40NTQ1KSk7CiAgZ2xfRnJhZ0NvbG9yID0gdmVjNChjb2wsIDEuMCk7Cn0K');
const VS = 'attribute vec2 p;void main(){gl_Position=vec4(p,0,1);}';
const SHOT = location.hash.indexOf('shot') >= 0;
const T = parseFloat((location.hash.match(/t=([\d.]+)/)||[0,0])[1]) || 0;
const cv = document.getElementById('c'), osd = document.getElementById('osd');
const gl = cv.getContext('webgl', {antialias:false, preserveDrawingBuffer:SHOT});
function prog(vs, fs){
const sh=(t,s)=>{const o=gl.createShader(t);gl.shaderSource(o,s);gl.compileShader(o);
if(!gl.getShaderParameter(o,gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(o)); return o;};
const p=gl.createProgram();
gl.attachShader(p,sh(gl.VERTEX_SHADER,vs)); gl.attachShader(p,sh(gl.FRAGMENT_SHADER,fs));
gl.linkProgram(p); if(!gl.getProgramParameter(p,gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p));
return p;
}
let PA, PB;
try { PA = prog(VS, FRAG_A); PB = prog(VS, FRAG_B); }
catch(e){ osd.textContent = 'shader error: ' + e.message; document.title = 'ERR'; }
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1,1,-1,-1,1,1,1]), gl.STATIC_DRAW);
// offscreen target: colour + depth packed into alpha
let fbo, tex;
function resize(){
const d = SHOT ? 1 : (devicePixelRatio||1);
const W = SHOT ? parseInt(location.hash.match(/w=(\d+)/)?.[1]||700) : Math.round(innerWidth*d);
const H = SHOT ? W : Math.round(innerHeight*d);
cv.width=W; cv.height=H; cv.style.width = SHOT ? W+'px' : '100vw'; cv.style.height = SHOT ? H+'px' : '100vh';
tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,W,H,0,gl.RGBA,gl.UNSIGNED_BYTE,null);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE);
fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER,gl.COLOR_ATTACHMENT0,gl.TEXTURE_2D,tex,0);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}
resize();
let mx=0.5, my=0.5;
if(!SHOT){
addEventListener('resize', resize);
addEventListener('mousemove', e=>{ mx=e.clientX/innerWidth; my=1-e.clientY/innerHeight; });
}
function draw(p, time){
gl.useProgram(p);
const l = gl.getAttribLocation(p,'p');
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.enableVertexAttribArray(l); gl.vertexAttribPointer(l,2,gl.FLOAT,false,0,0);
const u = n => gl.getUniformLocation(p,n);
if(u('u_res')) gl.uniform2f(u('u_res'), cv.width, cv.height);
if(u('u_time')) gl.uniform1f(u('u_time'), time);
if(u('u_mouse'))gl.uniform2f(u('u_mouse'), mx, my);
if(u('u_scene')){ gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, tex); gl.uniform1i(u('u_scene'), 0); }
gl.drawArrays(gl.TRIANGLE_STRIP,0,4);
}
function frame(time){
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.viewport(0,0,cv.width,cv.height);
draw(PA, time);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0,0,cv.width,cv.height);
draw(PB, time);
osd.textContent = 'ice · move mouse · ' + cv.width + 'x' + cv.height;
if(SHOT) blit(); else requestAnimationFrame(frame);
}
function blit(){ // headless firefox won't composite a WebGL canvas, so hand pixels to a 2D canvas
const W=cv.width, H=cv.height, px=new Uint8Array(W*H*4);
gl.readPixels(0,0,W,H,gl.RGBA,gl.UNSIGNED_BYTE,px);
const out=document.createElement('canvas'); out.width=W; out.height=H; out.style.display='block';
const x=out.getContext('2d'), img=x.createImageData(W,H);
for(let y=0;y<H;y++) img.data.set(px.subarray((H-1-y)*W*4,(H-y)*W*4), y*W*4);
x.putImageData(img,0,0);
cv.remove(); document.body.appendChild(out);
}
if(PA && PB) frame(SHOT ? T : 0);
</script></body></html>