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.
131 lines
6.8 KiB
Python
131 lines
6.8 KiB
Python
#!/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')
|