render_wallpaper.py drives headless Firefox frame by frame (four at a time, resuming where it stopped), then ffmpeg. Thirty seconds at 30 fps is exactly one plasma cycle, so frame 900 meets frame 0 and the loop has no seam. plasma-plugin/dev.emil.video is a minimal Plasma 6 video wallpaper: one file per screen, filling the screen, looping. H.264 is encoded Baseline because Qt here decodes through openh264, which rejects High profile and leaves a frozen frame; a new source also has to be played explicitly. The #shot hash learned info=0 to hide the on-screen hint for renders.
114 lines
21 KiB
HTML
114 lines
21 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Сквозь лёд</title>
|
||
<style>
|
||
html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#03111d}
|
||
canvas{display:block;width:100%;height:100%;touch-action:none}
|
||
#info{position:fixed;bottom:20px;left:24px;color:#d8f4ff;font:13px/1.6 system-ui,sans-serif;text-shadow:0 1px 5px #001;pointer-events:none}
|
||
#info strong{font-weight:500} #info span{opacity:.75}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<canvas id="view" aria-label="Плазма под прозрачным льдом. Двигайте указатель для параллакса."></canvas>
|
||
<div id="info" role="status"><strong>Сквозь лёд</strong><br><span id="help">Двигайте мышь · скорость − / =</span></div>
|
||
<script>
|
||
'use strict';
|
||
const sceneSource = "// Pass 1: eight plasma sheets, one atlas tile each. Alpha stores linear depth,\n// except on the three sheets that carry the fish: there it stores how much of the\n// pixel the fish covers, so the ice pass can draw them as solid dark shapes.\n// Density field lit as a height surface: billowing volumes with a real terminator.\nprecision highp float;\nuniform vec2 u_res; // size of one tile\nuniform float u_time;\n\nfloat hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }\nfloat noise(vec2 p) {\n vec2 i = floor(p), f = fract(p);\n f = f*f*(3.0-2.0*f);\n return mix(mix(hash(i), hash(i+vec2(1.0,0.0)), f.x),\n mix(hash(i+vec2(0.0,1.0)), hash(i+vec2(1.0,1.0)), f.x), f.y);\n}\nfloat fbm(vec2 p) {\n float sum = 0.0, amp = 0.5;\n for (int i = 0; i < 4; i++) {\n sum += amp * noise(p);\n p = mat2(1.62, 1.17, -1.17, 1.62) * p;\n amp *= 0.5;\n }\n return sum;\n}\nfloat fbm2(vec2 p) {\n return 0.5 * noise(p) + 0.25 * noise(mat2(1.62, 1.17, -1.17, 1.62) * p);\n}\n\n// Silhouette of a fish: pointed nose, deepest just behind the head, a narrow\n// peduncle, then a tail fan with a fork cut out of it. One width function, so the\n// whole animal is guaranteed to be a single connected shape.\nfloat smin(float a, float b, float k) {\n float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);\n return mix(b, a, h) - k * h * (1.0 - h);\n}\nfloat sdTriangle(vec2 p, vec2 p0, vec2 p1, vec2 p2) {\n vec2 e0 = p1 - p0, e1 = p2 - p1, e2 = p0 - p2;\n vec2 v0 = p - p0, v1 = p - p1, v2 = p - p2;\n vec2 q0 = v0 - e0 * clamp(dot(v0, e0) / dot(e0, e0), 0.0, 1.0);\n vec2 q1 = v1 - e1 * clamp(dot(v1, e1) / dot(e1, e1), 0.0, 1.0);\n vec2 q2 = v2 - e2 * clamp(dot(v2, e2) / dot(e2, e2), 0.0, 1.0);\n float s = sign(e0.x * e2.y - e0.y * e2.x);\n vec2 d = min(min(vec2(dot(q0, q0), s * (v0.x * e0.y - v0.y * e0.x)),\n vec2(dot(q1, q1), s * (v1.x * e1.y - v1.y * e1.x))),\n vec2(dot(q2, q2), s * (v2.x * e2.y - v2.y * e2.x)));\n return -sqrt(d.x) * sign(d.y);\n}\n// Half-height of the body at x: pointed nose, deepest a quarter of the way back,\n// then down to a narrow peduncle. Sweeping a circle along it keeps the body one piece.\nfloat fishWidth(float x) {\n float t = clamp((1.15 - x) / 2.30, 0.0, 1.0); // 0 at the nose, 1 at the peduncle\n return 0.34 * pow(sin(3.14159 * pow(t, 0.55)), 0.8) + 0.05 * smoothstep(0.70, 1.0, t);\n}\nfloat sdFish(vec2 p) {\n float d = length(vec2(max(abs(p.x) - 1.15, 0.0), p.y)) - fishWidth(p.x); // body\n // tail: two lobes fanning back from the peduncle, with a notch between them\n d = smin(d, sdTriangle(p, vec2(-1.02, 0.03), vec2(-1.62, 0.09), vec2(-1.46, 0.52)), 0.08);\n d = smin(d, sdTriangle(p, vec2(-1.02, -0.03), vec2(-1.62, -0.09), vec2(-1.46, -0.52)), 0.08);\n // dorsal and pectoral fins\n d = smin(d, sdTriangle(p, vec2(0.22, 0.20), vec2(-0.06, 0.46), vec2(-0.40, 0.16)), 0.05);\n d = smin(d, sdTriangle(p, vec2(0.46, -0.16), vec2(0.26, -0.08), vec2(0.10, -0.28)), 0.04);\n return d;\n}\n\nconst float EXTENT = 3.8; // half-width of a sheet in world units; must match the ice pass\n\nvoid main() {\n vec2 tile = floor(gl_FragCoord.xy / u_res);\n float layer = tile.x + tile.y * 4.0;\n vec2 uv = fract(gl_FragCoord.xy / u_res);\n vec2 p = (uv - 0.5) * 2.0 * vec2(EXTENT * u_res.x / u_res.y, EXTENT);\n p *= 0.85 + 0.10 * layer; // far sheets carry finer detail, which reads as distance\n float angle = hash(vec2(layer, 3.1)) * 6.2831853;\n p = mat2(cos(angle), sin(angle), -sin(angle), cos(angle)) * p;\n\n // Periodic time: every field returns exactly after 30 seconds.\n float phase = u_time * 6.2831853 / 30.0;\n vec2 o = vec2(cos(phase), sin(phase));\n vec2 seed = vec2(layer * 17.3, layer * 9.1);\n\n // Two nested warp stages, the marbled-plasma motion.\n vec2 q = vec2(fbm(p * 0.50 + seed + o * 0.55),\n fbm(p * 0.50 + seed + 23.1 - o * 0.55));\n vec2 r = vec2(fbm(p * 0.85 + 2.0 * q + seed + vec2(o.y, -o.x) * 0.40),\n fbm(p * 0.85 + 2.0 * q + seed + 31.7 - vec2(o.y, -o.x) * 0.40));\n vec2 w = p * 1.10 + 2.6 * r + seed; // warped coords of the density field\n\n // The silhouette is shared by all sheets (slid a little per depth), so the gaps\n // stay open right through the stack instead of filling in with overlap.\n float mass = smoothstep(0.520, 0.548, fbm((p + o * 0.30 + vec2(0.10, -0.07) * layer) * 0.62 + r * 1.1 + 13.7));\n\n // Light the density as a height surface: $cheap 2-octave gradient, strong terminator.\n float h = 0.05;\n vec2 slope = vec2(fbm2(w + vec2(h, 0.0)) - fbm2(w - vec2(h, 0.0)),\n fbm2(w + vec2(0.0, h)) - fbm2(w - vec2(0.0, h))) * (10.0 / (2.0 * h));\n vec3 nrm = normalize(vec3(-slope, 1.0));\n float lit = clamp(dot(nrm, normalize(vec3(-0.38, 0.52, 0.76))), 0.0, 1.0);\n\n // Folded planes across the volume, feathered so they never read as a grid.\n vec2 g = (p + r * 2.0) * 1.4;\n vec2 cell = floor(g), f = fract(g);\n float feather = smoothstep(0.0, 0.10, min(min(f.x, 1.0 - f.x), min(f.y, 1.0 - f.y)));\n float facet = mix(1.0, mix(0.70, 1.25, hash(cell + seed + step(1.0, f.x + f.y) * 37.0)), feather);\n\n float shade = 0.05 + 1.10 * lit * lit; // lit tops, near-black undersides\n float e = mass * facet * shade * 0.085\n + mass * (1.0 - mass) * 4.0 * 0.040 // thin torn silhouette\n + mass * pow(lit, 14.0) * 0.110; // glint where a fold faces the light\n vec3 col = e * mix(vec3(0.02, 0.42, 0.60), vec3(0.08, 0.64, 0.88), facet * 0.5);\n // Faint dye in the open gaps so darkness reads as depth, not as a hole.\n col += smoothstep(0.40, 0.85, fbm(p * 0.30 + seed * 0.4 + o * 0.3)) * vec3(0.0003, 0.0011, 0.0015);\n\n\n // Two sheets carry fish. Each one cuts straight across the slab once per cycle,\n // with long gaps and a wide spread of sizes: mostly small, now and then something\n // far too big. They are almost pure black, so all you ever see is the silhouette\n // eating into the glow behind them, bent by the cracks in the ice.\n float cover = (0.42 + layer * 0.26) / 2.6; // the sheet's own opacity, except where a fish is\n if (layer == 0.0 || layer == 1.0) {\n vec2 sp = (uv - 0.5) * 2.0 * vec2(EXTENT * u_res.x / u_res.y, EXTENT);\n float t01 = u_time / 30.0;\n for (int k = 0; k < 2; k++) {\n float id = float(k);\n float lt = fract(t01 + hash(vec2(layer, id)));\n if (lt < 0.02 || lt > 0.98) continue;\n float dir = hash(vec2(id, layer + 5.0)) > 0.5 ? 1.0 : -1.0;\n float y0 = (hash(vec2(id, layer + 13.0)) - 0.5) * 4.6;\n float y1 = y0 + (hash(vec2(id, layer + 17.0)) - 0.5) * 1.8;\n vec2 pos = mix(vec2(-dir * 8.6, y0), vec2(dir * 8.6, y1), lt);\n float tilt = atan((y1 - y0) / 17.2);\n float size = 0.30 + 1.30 * pow(hash(vec2(id, layer + 3.0)), 2.2);\n vec2 q = (mat2(cos(tilt), -sin(tilt), sin(tilt), cos(tilt)) * (sp - pos) * vec2(dir, 1.0)) / size;\n float d = sdFish(q);\n float px = 0.019 / size;\n float cov = clamp(0.5 - d / px, 0.0, 1.0);\n if (cov >= 0.0) {\n // A dark body, a faint halo where the ice is lit around it, and two small\n // eyes that burn cold blue: that is the only thing that says \"alive\".\n float halo = max(clamp(0.5 - (d - 0.16) / (px * 6.0), 0.0, 1.0) - cov, 0.0);\n col += vec3(0.006, 0.020, 0.032) * halo;\n if (cov > 0.0) {\n col = mix(col, vec3(0.0010, 0.0014, 0.0018), cov);\n float eye = smoothstep(0.045, 0.012, length(q - vec2(0.80, 0.095)));\n eye += 0.22 * smoothstep(0.11, 0.0, length(q - vec2(0.80, 0.095)));\n eye += 0.07 * smoothstep(0.26, 0.0, length(q - vec2(0.76, 0.05))); // the head catches it\n col += vec3(0.06, 0.44, 0.80) * eye * cov;\n cover = 1.0;\n }\n }\n }\n }\n float alpha = (layer == 0.0 || layer == 1.0) ? cover : (0.42 + layer * 0.26) / 2.6;\n gl_FragColor = vec4(col, alpha);\n}\n";
|
||
const iceSource = "// Pass 2: clean ice. Refraction, chromatic shear, absorption by true path length.\n// Nothing is added to empty space, so the gaps between sheets stay black.\nprecision highp float;\nuniform vec2 u_res;\nuniform float u_time;\nuniform vec2 u_mouse; // normalized, bottom-left origin\nuniform sampler2D u_scene;\nuniform vec2 u_tile_res;\n\nconst float EXTENT = 3.8; // must match the plasma pass\nconst float FRONT = -2.0; // ice surface, camera at the origin looking down -z\nconst vec3 SIGMA = vec3(0.66, 0.26, 0.11); // absorption per unit: red dies first, so depth turns blue\nconst float IOR = 1.31;\n// The fish sheets store coverage in alpha, so their depth comes from the index.\n\nfloat hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }\nfloat noise(vec2 p) {\n vec2 i = floor(p), f = fract(p);\n f = f*f*(3.0-2.0*f);\n return mix(mix(hash(i), hash(i+vec2(1.0,0.0)), f.x),\n mix(hash(i+vec2(0.0,1.0)), hash(i+vec2(1.0,1.0)), f.x), f.y);\n}\nfloat fbm(vec2 p) {\n float sum = 0.0, amp = 0.5;\n for (int i = 0; i < 3; i++) { sum += amp * noise(p); p = mat2(1.62, 1.17, -1.17, 1.62) * p; amp *= 0.5; }\n return sum;\n}\nfloat fbm2(vec2 p) {\n return 0.5 * noise(p) + 0.25 * noise(mat2(1.62, 1.17, -1.17, 1.62) * p);\n}\nvec2 atlasUV(vec2 world, float layer) {\n vec2 uv = world / (2.0 * EXTENT) + 0.5;\n uv = clamp(uv, 0.5 / u_tile_res, 1.0 - 0.5 / u_tile_res); // keep bilinear taps inside the tile\n return (vec2(mod(layer, 4.0), floor(layer / 4.0)) + uv) / vec2(4.0, 2.0);\n}\n// The slab's fracture lines: two families of long, slightly wandering cracks. Shared\n// by the height field and the sheen, so each crack is both a bright seam and a step.\nvec2 fracture(vec2 x) {\n float d = fbm(x * 0.62 + 3.0) - 0.5; // zero set: long winding cracks\n float e = fbm(x * 1.35 - 7.0) - 0.5; // a sparser second family\n return vec2(d, e);\n}\n// Frozen surface: fine ridged grain over the plates the cracks have lifted apart.\n// Static in world space; only the plasma behind it moves.\nfloat heightSmooth(vec2 x) {\n return 0.090 * (1.0 - abs(2.0 * fbm2(x * 2.2 + 2.0) - 1.0))\n + 0.055 * (1.0 - abs(2.0 * fbm2(x * 6.4 - 5.0) - 1.0));\n}\nfloat height(vec2 x) {\n vec2 fr = fracture(x);\n return heightSmooth(x)\n + 0.014 * clamp(fr.x * 110.0, -1.0, 1.0) // the slab sits at a different level\n + 0.009 * clamp(fr.y * 110.0, -1.0, 1.0); // on each side of a crack\n}\nvoid main() {\n vec2 screen = gl_FragCoord.xy / u_res;\n vec2 p = (gl_FragCoord.xy - 0.5 * u_res) / u_res.y;\n vec2 look = clamp(u_mouse - 0.5, -0.35, 0.35); // orbiting the slab centre\n vec3 ray = normalize(vec3(p - look * 2.2, -1.6));\n\n // Front surface along the view ray; two fixed-point steps settle on the swell.\n float t = FRONT / ray.z;\n for (int i = 0; i < 2; i++) t = (FRONT + height(ray.xy * t)) / ray.z;\n vec3 hit = ray * t;\n\n float e = 0.008;\n vec2 slope = vec2(height(hit.xy + vec2(e, 0.0)) - height(hit.xy - vec2(e, 0.0)),\n height(hit.xy + vec2(0.0, e)) - height(hit.xy - vec2(0.0, e))) / (2.0 * e);\n float bevel = smoothstep(0.14, 0.0, min(min(screen.x, 1.0 - screen.x), min(screen.y, 1.0 - screen.y)));\n slope += bevel * (screen - 0.5) * 0.55; // the slab turns away at the frame edge\n // The cracks are steps in the surface, so their slope flips sign within a pixel.\n // That is what tears the plasma beautifully, and what would slice a fish to bits,\n // so the fish are bent by the smooth swell alone.\n vec2 slopeSmooth = vec2(heightSmooth(hit.xy + vec2(e, 0.0)) - heightSmooth(hit.xy - vec2(e, 0.0)),\n heightSmooth(hit.xy + vec2(0.0, e)) - heightSmooth(hit.xy - vec2(0.0, e))) / (2.0 * e);\n vec3 normal = normalize(vec3(-slope, 1.0));\n vec3 normalSmooth = normalize(vec3(-slopeSmooth, 1.0));\n float facing = max(dot(-ray, normal), 0.0);\n\n // Refracted ray; red and blue bend a little differently, which is what makes ice ring.\n vec3 inside = refract(ray, normal, 1.0 / IOR);\n vec2 shearR = (refract(ray, normal, 1.0 / (IOR - 0.012)) - inside).xy;\n vec2 shearB = (refract(ray, normal, 1.0 / (IOR + 0.019)) - inside).xy;\n inside = normalize(inside);\n inside.z = min(inside.z, -0.40); // bound grazing rays at the crack steps\n\n // Recentre the parallax on the middle of the sheet stack: near and far sheets\n // then slide past each other at different rates as the pointer moves.\n vec2 par = look * 4.54;\n\n vec3 acc = vec3(0.0);\n float trans = 1.0;\n for (int i = 0; i < 8; i++) {\n float layer = float(i);\n bool solid = layer == 0.0 || layer == 1.0; // the two sheets that hold fish\n float depth = solid ? 0.42 + layer * 0.26\n : texture2D(u_scene, atlasUV(hit.xy + par, layer)).a * 2.6;\n float path = (hit.z - (FRONT - depth)) / -inside.z; // ice crossed to reach this sheet\n // The slab is seen at a wide angle: the sheets themselves stretch up to four times\n // towards the frame edges, which a fish silhouette cannot hide. So the fish ride\n // on screen space and the ice bends them through its own slope instead.\n vec2 at = solid ? (screen - 0.5) * 2.0 * EXTENT + normalSmooth.xy * path * 1.8 + par * 0.6\n : (hit + inside * path).xy + par;\n vec3 plasma;\n plasma.r = texture2D(u_scene, atlasUV(at + shearR * path, layer)).r;\n plasma.g = texture2D(u_scene, atlasUV(at, layer)).g;\n plasma.b = texture2D(u_scene, atlasUV(at + shearB * path, layer)).b;\n vec3 c = plasma * exp(-SIGMA * path);\n // Near plumes are nearly opaque, so the sheets behind them show through the gaps\n // instead of averaging into a wash. That is what makes the stack read as depth.\n // The shard is a solid, so its coverage comes straight from pass 1.\n float alpha = solid ? texture2D(u_scene, atlasUV(at, layer)).a * 0.92\n : clamp(max(max(c.r, c.g), c.b) * 1.8, 0.0, 0.80);\n acc += trans * c;\n trans *= 1.0 - alpha;\n }\n\n // Gentle shoulder: overlapping glow stays coloured instead of clipping to white.\n vec3 col = vec3(1.0) - exp(-acc * 1.5);\n\n // Glass: reflection and a rim only where the surface turns away. Empty space stays dark.\n float fresnel = pow(1.0 - facing, 5.0);\n vec3 sky = mix(vec3(0.004, 0.016, 0.030), vec3(0.075, 0.220, 0.320), normal.y * 0.5 + 0.5);\n col += sky * (fresnel * 1.2 + bevel * 0.03);\n // Steep faces of the crests catch a thin sheen: the edge of the frozen surface,\n // painted only where the ice actually tilts, so the open water stays black.\n // Only the crack walls are this steep, so this sheen outlines the seams alone\n // instead of washing the whole grain.\n float tilt = smoothstep(1.00, 2.60, length(slope));\n col += vec3(0.022, 0.062, 0.078) * tilt;\n vec2 fr = fracture(hit.xy);\n float crack = (1.0 - smoothstep(0.0, 0.004, abs(fr.x)))\n + (1.0 - smoothstep(0.0, 0.004, abs(fr.y)));\n col += vec3(0.020, 0.055, 0.068) * crack * smoothstep(0.58, 0.80, fbm2(hit.xy * 0.30 + 21.0));\n\n gl_FragColor = vec4(pow(clamp(col, 0.0, 1.0), vec3(0.4545)), 1.0);\n}\n";
|
||
const vertexSource = 'attribute vec2 p; void main(){gl_Position=vec4(p,0.0,1.0);}';
|
||
const canvas = document.getElementById('view'), help = document.getElementById('help');
|
||
const gl = canvas.getContext('webgl', {antialias:false, alpha:false, preserveDrawingBuffer:true});
|
||
function fail(message) { help.textContent = message; throw new Error(message); }
|
||
if (!gl) fail('WebGL недоступен в этом браузере.');
|
||
function program(source) {
|
||
function shader(type, code) {
|
||
const s = gl.createShader(type);
|
||
gl.shaderSource(s, code); gl.compileShader(s);
|
||
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) fail('Ошибка шейдера: ' + gl.getShaderInfoLog(s));
|
||
return s;
|
||
}
|
||
const p = gl.createProgram(), vs = shader(gl.VERTEX_SHADER, vertexSource), fs = shader(gl.FRAGMENT_SHADER, source);
|
||
gl.attachShader(p, vs); gl.attachShader(p, fs); gl.linkProgram(p);
|
||
gl.deleteShader(vs); gl.deleteShader(fs);
|
||
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) fail('Ошибка программы: ' + gl.getProgramInfoLog(p));
|
||
return p;
|
||
}
|
||
const scene = program(sceneSource), ice = program(iceSource);
|
||
const buffer = gl.createBuffer();
|
||
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1,1,-1,-1,1,1,1]), gl.STATIC_DRAW);
|
||
let texture, framebuffer, tileWidth, tileHeight;
|
||
function resize() {
|
||
const max = gl.getParameter(gl.MAX_TEXTURE_SIZE);
|
||
const dpr = Math.min(devicePixelRatio || 1, 1.25, max / (innerWidth * 3), max / (innerHeight * 1.5));
|
||
canvas.width = Math.max(4, Math.floor(innerWidth * dpr / 4) * 4);
|
||
canvas.height = Math.max(4, Math.floor(innerHeight * dpr / 4) * 4);
|
||
tileWidth = canvas.width * 3 / 4; tileHeight = canvas.height * 3 / 4;
|
||
if (texture) gl.deleteTexture(texture);
|
||
if (framebuffer) gl.deleteFramebuffer(framebuffer);
|
||
texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture);
|
||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, tileWidth * 4, tileHeight * 2, 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);
|
||
framebuffer = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
||
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) fail('Не удалось создать текстуру сцены.');
|
||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||
}
|
||
addEventListener('resize', resize); resize();
|
||
const shot = location.hash.startsWith('#shot');
|
||
const shotParams = new URLSearchParams(location.hash.slice(1));
|
||
let mouseX = Number(shotParams.get('mx') ?? 0.5), mouseY = Number(shotParams.get('my') ?? 0.5);
|
||
let speed = 1, time = Number(shotParams.get('t') ?? 0), previous;
|
||
if (shotParams.get('info') === '0') document.getElementById('info').style.display = 'none';
|
||
addEventListener('pointermove', e => { mouseX = e.clientX / innerWidth; mouseY = 1 - e.clientY / innerHeight; });
|
||
addEventListener('keydown', e => {
|
||
if (e.key === '-' || e.key === '_') speed = Math.max(0.125, speed / 2);
|
||
else if (e.key === '=' || e.key === '+') speed = Math.min(4, speed * 2);
|
||
else return;
|
||
help.textContent = 'Двигайте мышь · скорость ' + speed + '× (− / =)';
|
||
});
|
||
function draw(p, width, height) {
|
||
gl.useProgram(p);
|
||
const position = gl.getAttribLocation(p, 'p');
|
||
gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.enableVertexAttribArray(position);
|
||
gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
|
||
gl.uniform2f(gl.getUniformLocation(p, 'u_res'), width, height);
|
||
gl.uniform1f(gl.getUniformLocation(p, 'u_time'), time);
|
||
gl.uniform2f(gl.getUniformLocation(p, 'u_mouse'), mouseX, mouseY);
|
||
gl.uniform2f(gl.getUniformLocation(p, 'u_tile_res'), tileWidth, tileHeight);
|
||
gl.uniform1i(gl.getUniformLocation(p, 'u_scene'), 0);
|
||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
||
}
|
||
function frame(now) {
|
||
if (previous !== undefined) time += (now - previous) * 0.001 * speed;
|
||
previous = now;
|
||
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
||
gl.viewport(0, 0, tileWidth * 4, tileHeight * 2);
|
||
draw(scene, tileWidth, tileHeight);
|
||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||
gl.viewport(0, 0, canvas.width, canvas.height);
|
||
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture);
|
||
draw(ice, canvas.width, canvas.height);
|
||
if (shot) {
|
||
// Headless Firefox does not composite WebGL; expose the actual rendered pixels.
|
||
const pixels = new Uint8Array(canvas.width * canvas.height * 4);
|
||
gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
||
const output = document.createElement('canvas');
|
||
output.width = canvas.width; output.height = canvas.height;
|
||
const ctx = output.getContext('2d'), image = ctx.createImageData(output.width, output.height);
|
||
for (let y = 0; y < output.height; y++)
|
||
image.data.set(pixels.subarray((output.height - y - 1) * output.width * 4, (output.height - y) * output.width * 4), y * output.width * 4);
|
||
ctx.putImageData(image, 0, 0); canvas.replaceWith(output);
|
||
} else requestAnimationFrame(frame);
|
||
}
|
||
if (shot) frame(0); else requestAnimationFrame(frame);
|
||
</script>
|
||
</body>
|
||
</html>
|