Files
shaders/build_plasma.py
emil28092005 92e8b287a3 Render the ice as long videos and play them as wallpapers
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.
2026-10-31 01:22:16 +03:00

131 lines
6.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Embed two GLSL passes into a standalone page.
build_plasma.py [scene.frag] [ice.frag] [out.html]
Defaults to the plasma wall: 6_ice_plasma.frag + 7_ice_over.frag -> ice_plasma.html."""
import json
import sys
from pathlib import Path
here = Path(__file__).resolve().parent
args = (sys.argv[1:] + ['6_ice_plasma.frag', '7_ice_over.frag', 'ice_plasma.html'])[:3]
scene = (here / args[0]).read_text()
ice = (here / args[1]).read_text()
html = r'''<!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 = __SCENE__;
const iceSource = __ICE__;
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>
'''
(here / args[2]).write_text(html.replace('__SCENE__', json.dumps(scene)).replace('__ICE__', json.dumps(ice)))
print(args[2], 'written')