screentinker/tizen/js/transitions.js
ScreenTinker 95b8d1b293 Export shared modules to the browser even when Node is in the page
Transitions have never run on BrightSign, and it was never a GPU problem.

`transitionRuntimeReady()` is a presence check on three globals and touches no
WebGL at all. A BrightSign roHtmlWidget is created with `nodejs_enabled: true`,
which puts Node's `module` into classic-script scope — so every shared module
that exported with an `else` took the CommonJS branch and never assigned its
browser global. The runtime was absent before WebGL was ever asked a question.

This is deducible from the fleet without touching the hardware: the player
pushes system.reboot / display.power / display.resolution / system.self_update
only behind BS.hasHost(), which needs require('@brightsign/messageport') to
resolve. Our XT245's stored capability row carries all four, so Node
integration was live in that page, so the CommonJS branch was taken.

Transitions are the least of it. schedule-eval.js had the same shape, and the
player falls back to "always active" when ScheduleEval is missing — so per-item
DAYPARTING silently stopped applying on that platform and scheduled content
played outside its window with nothing in any log. player-media-health.js the
same. Four files, all fixed by exporting to BOTH targets rather than either/or.

media-mute.js, orientation-style.js and wall-geometry.js already assigned their
globals in a separate unconditional block and were never affected; the audit
that reached me claimed all seven, and reading them is what separated the four
from the three.

THE GUARD, WITHOUT WHICH THE ABOVE IS A REGRESSION.

Restore the globals alone and BrightSign starts attempting video wipes it
cannot supply. On a hardware video plane drawImage(video) succeeds, throws
nothing, and paints a fully TRANSPARENT frame — so the wipe fades from nothing,
behind a video plane that is still lit. Worse than the hard cut it replaces.

The discriminator already existed: videoFrameIsCapturable() probes ALPHA, so a
genuine fade-to-black still reads as captured. It was wired into the screenshot
path and not this one, which asked isMediaReadable() — a CORS question, "am I
allowed to read this", not "did any pixels arrive". Both the outgoing frame and
the incoming warm-play snapshot now consult it, cached per platform, defaulting
to available while undetermined so a cold start is not crippled.

Net effect on BrightSign: image-to-image transitions light up, anything
involving video hard-cuts honestly, and dayparting starts working.

Full video transitions are reachable later — BrightSign documents that video
"captured as a canvas for WebGL processing must be routed to the GPU" via a
per-element hwz="off", which keeps hardware decode at an 8-bit/1080p ceiling.
That needs the hardware to validate and is not in this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014skWYXJUWhF73EvNPgB2AS
2026-08-07 10:36:50 -05:00

219 lines
34 KiB
JavaScript
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.

// GL Transitions v1 param parser — ByteTinker, MIT
//
// Single source of truth for shader parameters. The `.glsl` files declare their own
// params using the GL Transitions comment convention, extended with an optional range:
//
// uniform float bounce; // = 0.5 [0.0..1.5]
//
// This parser is consumed by:
// - the web player renderer (uniform defaults + values from snapshot config)
// - the Tizen player renderer (same)
// - the dashboard picker (slider min/max/default/step)
//
// There is deliberately no separate param schema. If you find yourself adding one,
// the shader and the UI have already drifted.
const PARAM_RE =
/uniform\s+float\s+(\w+)\s*;\s*\/\/\s*=\s*(-?[\d.]+)\s*(?:\[\s*(-?[\d.]+)\s*\.\.\s*(-?[\d.]+)\s*\])?/g;
/**
* Parse param declarations out of a GLSL source string.
* @param {string} src
* @returns {Array<{name:string, default:number, min:number, max:number, step:number}>}
*/
function parseParams(src) {
const out = [];
let m;
PARAM_RE.lastIndex = 0;
while ((m = PARAM_RE.exec(src))) {
const def = parseFloat(m[2]);
const min = m[3] !== undefined ? parseFloat(m[3]) : Math.min(0, def);
const max = m[4] !== undefined ? parseFloat(m[4]) : (def === 0 ? 1 : def * 2);
out.push({ name: m[1], default: def, min, max, step: (max - min) / 200 });
}
return out;
}
/**
* Merge stored values over defaults, dropping unknown keys and clamping to range.
* Never trust the snapshot blob — a shader may have been edited since the playlist
* was configured, and an out-of-range uniform can produce a black frame.
* @param {Array} params result of parseParams
* @param {Object} stored values from snapshot.transition.params
*/
function resolveParams(params, stored) {
const out = {};
for (const p of params) {
const v = stored && typeof stored[p.name] === 'number' ? stored[p.name] : p.default;
out[p.name] = Math.min(p.max, Math.max(p.min, v));
}
return out;
}
// The renderer wraps each .glsl with this. Keep it identical across web, Tizen, and
// Android — the shader sources assume exactly these names and nothing else.
const PREAMBLE = `precision highp float;
varying vec2 vUv;
uniform sampler2D uFrom;
uniform sampler2D uTo;
uniform float progress;
uniform float ratio;
vec4 getFromColor(vec2 uv){ return texture2D(uFrom, uv); }
vec4 getToColor(vec2 uv){ return texture2D(uTo, uv); }
`;
const EPILOGUE = `
void main(){ gl_FragColor = transition(vUv); }`;
const VERTEX = `attribute vec2 aPos;
varying vec2 vUv;
void main(){ vUv = aPos * 0.5 + 0.5; gl_Position = vec4(aPos, 0.0, 1.0); }`;
// Hand-committed copy of shared/Transitions — keep the both-targets export in step with it.
if (typeof module !== 'undefined' && module.exports) {
module.exports = { parseParams, resolveParams, PREAMBLE, EPILOGUE, VERTEX };
}
if (typeof self !== 'undefined') {
self.TransitionParams = { parseParams, resolveParams, PREAMBLE, EPILOGUE, VERTEX }; // browser (player/demo)
}
;
// Portable WebGL transition compositor (GL Transitions v1).
//
// ONE implementation, consumed by both the web player (inlined at build) and the Tizen player.
// No DOM dependency beyond the <canvas> handed in. The .glsl shaders + params.js are the single
// source of truth; this module only wraps (PREAMBLE + shader + EPILOGUE, shared VERTEX) and runs
// them. It composites TWO textures (from -> to) across `progress` 0..1; the outgoing frame stays
// live in `uFrom` for the whole transition, so there is never a blank seam.
//
// Never-blank teeth live here too: on `webglcontextlost` the renderer flips `lost` and calls
// opts.onContextLost so the player can hard-cut to a plain <img> instead of showing black.
(function (root, factory) {
var api = factory();
if (typeof module !== 'undefined' && module.exports) module.exports = api;
if (root) root.TransitionRenderer = api;
})(typeof self !== 'undefined' ? self : this, function () {
'use strict';
function compile(gl, type, src) {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
const log = gl.getShaderInfoLog(s);
gl.deleteShader(s);
throw new Error('shader compile failed: ' + log);
}
return s;
}
// wrap = { PREAMBLE, EPILOGUE, VERTEX } from params.js
function createRenderer(canvas, wrap, opts) {
opts = opts || {};
const attrs = {
alpha: false, antialias: false, depth: false, stencil: false,
premultipliedAlpha: false, preserveDrawingBuffer: !!opts.preserveDrawingBuffer,
};
const gl = canvas.getContext('webgl', attrs) || canvas.getContext('experimental-webgl', attrs);
if (!gl) throw new Error('no-webgl');
let lost = false;
const onLost = (e) => { e.preventDefault(); lost = true; if (opts.onContextLost) opts.onContextLost(); };
const onRestored = () => { lost = false; programs = {}; buildQuad(); if (opts.onContextRestored) opts.onContextRestored(); };
canvas.addEventListener('webglcontextlost', onLost, false);
canvas.addEventListener('webglcontextrestored', onRestored, false);
let quadBuf, vShader;
function buildQuad() {
quadBuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW);
vShader = compile(gl, gl.VERTEX_SHADER, wrap.VERTEX);
}
buildQuad();
let programs = {}; // shaderSrc -> { program, uni:{} }
function programFor(src) {
if (programs[src]) return programs[src];
const f = compile(gl, gl.FRAGMENT_SHADER, wrap.PREAMBLE + '\n' + src + '\n' + wrap.EPILOGUE);
const p = gl.createProgram();
gl.attachShader(p, vShader);
gl.attachShader(p, f);
gl.bindAttribLocation(p, 0, 'aPos');
gl.linkProgram(p);
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
const log = gl.getProgramInfoLog(p);
gl.deleteProgram(p); gl.deleteShader(f);
throw new Error('program link failed: ' + log);
}
gl.deleteShader(f);
programs[src] = { program: p, uni: {} };
return programs[src];
}
function uniLoc(rec, name) {
if (!(name in rec.uni)) rec.uni[name] = gl.getUniformLocation(rec.program, name);
return rec.uni[name];
}
function makeTex() {
const t = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, t);
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);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
return t;
}
let texFrom = makeTex(), texTo = makeTex();
let curShader = null;
function upload(tex, source) {
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); // match getFromColor/getToColor uv convention
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
}
return {
get lost() { return lost; },
gl,
// Compile eagerly so a bad shader throws HERE (caller hard-cuts) rather than mid-transition.
setShader(src) { curShader = src; programFor(src); },
setFrom(img) { upload(texFrom, img); },
setTo(img) { upload(texTo, img); },
resize(w, h) { canvas.width = w; canvas.height = h; gl.viewport(0, 0, w, h); },
render(progress, params) {
if (lost || !curShader) return false;
const rec = programFor(curShader);
gl.useProgram(rec.program);
gl.viewport(0, 0, canvas.width, canvas.height);
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texFrom); gl.uniform1i(uniLoc(rec, 'uFrom'), 0);
gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, texTo); gl.uniform1i(uniLoc(rec, 'uTo'), 1);
gl.uniform1f(uniLoc(rec, 'progress'), Math.max(0, Math.min(1, progress)));
gl.uniform1f(uniLoc(rec, 'ratio'), canvas.width / Math.max(1, canvas.height));
if (params) for (const k in params) { const loc = uniLoc(rec, k); if (loc) gl.uniform1f(loc, params[k]); }
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
return true;
},
destroy() {
canvas.removeEventListener('webglcontextlost', onLost);
canvas.removeEventListener('webglcontextrestored', onRestored);
try {
gl.deleteTexture(texFrom); gl.deleteTexture(texTo); gl.deleteBuffer(quadBuf);
for (const k in programs) gl.deleteProgram(programs[k].program);
} catch (e) { /* context already gone */ }
programs = {};
},
};
}
return { createRenderer, compile };
});
;
window.__TRANSITION_SHADERS={"CRTCollapse":"// CRT Collapse\n// blurb: Frame crushes to a line, then a dot, then the next image blooms back out. Power-cycle drama.\n// Author: Dan (ByteTinker)\n// License: MIT\n// GL Transitions v1 — GLSL ES 1.0, runs unmodified in WebGL1 and Android GLES2.\nuniform float lineHold; // = 0.16 [0.0..0.45]\nuniform float flashGain;// = 1.6 [0.0..4.0]\nuniform float bloom; // = 14.0 [4.0..40.0]\n\nvec4 transition(vec2 uv){\n vec2 c = uv - 0.5;\n float half_ = 0.5 * lineHold + 0.5; // unused shaping guard\n float pa = clamp(progress / 0.45, 0.0, 1.0);\n float pb = clamp((progress - 0.55) / 0.45, 0.0, 1.0);\n\n float vs = 1.0 - smoothstep(0.0, 0.70, pa);\n float hs = 1.0 - smoothstep(0.62, 1.0, pa);\n float vs2 = smoothstep(0.0, 0.38, pb);\n float hs2 = smoothstep(0.30, 1.0, pb);\n\n vec3 col = vec3(0.0);\n if(progress < 0.5){\n if(abs(c.y) < vs * 0.5 + 0.0016 && abs(c.x) < hs * 0.5 + 0.0016){\n vec2 s = vec2(c.x / max(hs, 0.003), c.y / max(vs, 0.003)) + 0.5;\n col = getFromColor(clamp(s, 0.0, 1.0)).rgb;\n }\n } else {\n if(abs(c.y) < vs2 * 0.5 + 0.0016 && abs(c.x) < hs2 * 0.5 + 0.0016){\n vec2 s = vec2(c.x / max(hs2, 0.003), c.y / max(vs2, 0.003)) + 0.5;\n col = getToColor(clamp(s, 0.0, 1.0)).rgb;\n }\n }\n\n float d = length(vec2(c.x * ratio, c.y));\n float flash = exp(-pow(abs(progress - 0.5) * 8.0, 2.0));\n col += vec3(1.0, 0.96, 0.88) * flash * exp(-d * bloom) * flashGain;\n return vec4(col, 1.0);\n}\n","Datamosh":"// Datamosh\n// blurb: P-frame corruption — the old frames gradients smear the new one until the blocks give up.\n// Author: Dan (ByteTinker)\n// License: MIT\n// GL Transitions v1 — GLSL ES 1.0, runs unmodified in WebGL1 and Android GLES2.\nuniform float blockSize; // = 30.0 [6.0..90.0]\nuniform float bleed; // = 0.45 [0.0..1.5]\nuniform float chroma; // = 0.5 [0.0..2.0]\n\nfloat bt_hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }\n\nvec4 transition(vec2 uv){\n float env = sin(3.14159265 * progress);\n vec2 bs = vec2(blockSize * ratio, blockSize);\n vec2 blk = floor(uv * bs);\n\n vec3 c0 = getFromColor((blk + 0.5) / bs).rgb;\n vec3 c1 = getFromColor((blk + vec2(1.5, 0.5)) / bs).rgb;\n vec3 c2 = getFromColor((blk + vec2(0.5, 1.5)) / bs).rgb;\n vec2 mv = vec2(dot(c1 - c0, vec3(0.333)), dot(c2 - c0, vec3(0.333)));\n mv *= bleed * env * 5.0;\n\n float keep = step(progress, bt_hash(blk) * 0.85 + 0.10);\n\n vec3 A = getFromColor(fract(uv + mv)).rgb;\n vec2 ub = fract(uv + mv * 0.5);\n vec3 B;\n float ch = chroma * env * 0.01;\n B.r = getToColor(fract(ub + vec2(ch, 0.0))).r;\n B.g = getToColor(ub).g;\n B.b = getToColor(fract(ub - vec2(ch, 0.0))).b;\n\n vec3 res = mix(B, A, keep);\n float q = mix(255.0, 10.0, env);\n res = floor(res * q) / q;\n return vec4(res, 1.0);\n}\n","Etch":"// Etch\n// blurb: Photomask reveal — the frame develops in on a stepper field, with a hot exposure edge.\n// Author: Dan (ByteTinker)\n// License: MIT\n// GL Transitions v1 — GLSL ES 1.0, runs unmodified in WebGL1 and Android GLES2.\nuniform float cellSize; // = 26.0 [6.0..90.0]\nuniform float edgeGlow; // = 1.0 [0.0..3.0]\nuniform float randomness;// = 0.55 [0.0..1.0]\nuniform float softness; // = 0.09 [0.005..0.3]\n\nfloat bt_hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }\n\nvec4 transition(vec2 uv){\n vec2 g = floor(uv * vec2(cellSize * ratio, cellSize));\n float m = bt_hash(g);\n float sweep = (uv.x + uv.y) * 0.5;\n float mask = clamp(mix(sweep, m, randomness), 0.0, 1.0);\n\n float e = softness;\n float pp = progress * (1.0 + e);\n float t = smoothstep(mask, mask + e, pp);\n\n vec3 a = getFromColor(uv).rgb;\n vec3 b = getToColor(uv).rgb;\n vec3 res = mix(a, b, t);\n\n float edge = exp(-abs(pp - mask) / max(e, 0.005) * 1.6);\n edge *= (1.0 - smoothstep(0.92, 1.0, progress)) * smoothstep(0.0, 0.05, progress);\n res += vec3(1.0, 0.70, 0.22) * edge * edgeGlow * 0.7;\n\n return vec4(res, 1.0);\n}\n","FiberSplice":"// Fiber Splice\n// blurb: Two ends draw apart, the arc fires, and the new frame fuses in from the seam.\n// Author: Dan (ByteTinker)\n// License: MIT\n// GL Transitions v1 — GLSL ES 1.0, runs unmodified in WebGL1 and Android GLES2.\nuniform float separation; // = 0.30 [0.0..0.5]\nuniform float arcGain; // = 1.8 [0.0..4.0]\nuniform float arcTight; // = 26.0 [4.0..80.0]\nuniform float flicker; // = 0.5 [0.0..1.0]\n\nfloat bt_hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }\n\nvec4 transition(vec2 uv){\n float x = uv.x;\n float ph = clamp(progress / 0.46, 0.0, 1.0);\n float pb = clamp((progress - 0.54) / 0.46, 0.0, 1.0);\n\n vec3 col = vec3(0.0);\n if(progress < 0.5){\n float off = ph * separation;\n if(x < 0.5 - off) col = getFromColor(vec2(x + off, uv.y)).rgb;\n else if(x >= 0.5 + off) col = getFromColor(vec2(x - off, uv.y)).rgb;\n } else {\n float off = (1.0 - pb) * separation;\n if(x < 0.5 - off) col = getToColor(vec2(x + off, uv.y)).rgb;\n else if(x >= 0.5 + off) col = getToColor(vec2(x - off, uv.y)).rgb;\n }\n\n float fl = 1.0 + flicker * (bt_hash(vec2(floor(uv.y * 90.0), floor(progress * 120.0))) - 0.5);\n float arc = exp(-abs(x - 0.5) * arcTight) * exp(-pow((progress - 0.5) * 8.5, 2.0)) * fl;\n col += vec3(0.72, 0.88, 1.0) * arc * arcGain;\n col += vec3(1.0) * arc * arc * 0.6;\n\n return vec4(col, 1.0);\n}\n","FilmAdvance":"// Film Advance\n// blurb: The strip pulls through the gate — frame bar and sprockets sweep past, shutter flickers, next frame registers with a bounce.\n// Author: Dan (ByteTinker)\n// License: MIT\n// GL Transitions v1 — GLSL ES 1.0, runs unmodified in WebGL1 and Android GLES2.\nuniform float frameBar; // = 0.11 [0.02..0.35]\nuniform float bounce; // = 0.5 [0.0..1.5]\nuniform float shutter; // = 0.55 [0.0..1.0]\nuniform float sprockets; // = 1.0 [0.0..1.0]\nuniform float grainAmt; // = 0.5 [0.0..1.0]\n\nfloat bt_hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }\n\nvec4 transition(vec2 uv){\n float e = progress;\n float env = sin(3.14159265 * e);\n\n // pull with a little registration overshoot at the gate\n float ease = smoothstep(0.0, 1.0, e);\n ease += bounce * 0.045 * sin(e * 18.85) * e * (1.0 - e) * 4.0;\n ease = clamp(ease, 0.0, 1.0);\n\n float o = ease * (1.0 + frameBar);\n float t = uv.y - o;\n\n vec3 col;\n if(t >= 0.0){\n col = getFromColor(vec2(uv.x, min(t, 1.0))).rgb;\n } else if(t <= -frameBar){\n col = getToColor(vec2(uv.x, clamp(t + 1.0 + frameBar, 0.0, 1.0))).rgb;\n } else {\n float scr = bt_hash(vec2(floor(uv.x * 400.0), floor(e * 12.0)));\n col = vec3(0.035, 0.028, 0.022) + vec3(0.10, 0.08, 0.06) * step(0.985, scr);\n }\n\n // edge perforations, present only while the strip is moving\n float edge = min(uv.x, 1.0 - uv.x);\n float band = 1.0 - smoothstep(0.030, 0.038, edge);\n float sy = fract((uv.y - o) * (1.0 / (1.0 + frameBar)) * 4.0);\n float holeY = 1.0 - smoothstep(0.26, 0.34, abs(sy - 0.5));\n float holeX = 1.0 - smoothstep(0.008, 0.014, abs(edge - 0.019));\n float perf = holeY * step(0.006, edge) * (1.0 - holeX * 0.0);\n vec3 strip = mix(vec3(0.02), vec3(0.86, 0.83, 0.75), perf);\n col = mix(col, strip, band * env * sprockets);\n\n // shutter blade\n float blade = 1.0 - shutter * env * (0.75 + 0.25 * sin(e * 62.8));\n col *= blade;\n\n // gate grain\n float g = bt_hash(uv * vec2(720.0, 405.0) + floor(e * 48.0)) - 0.5;\n col += vec3(g) * grainAmt * env * 0.20;\n\n return vec4(col, 1.0);\n}\n","PacketLoss":"// Packet Loss\n// blurb: Blocks drop out of the stream, rows tear, and the new frame retransmits block by block.\n// Author: Dan (ByteTinker)\n// License: MIT\n// GL Transitions v1 — GLSL ES 1.0, runs unmodified in WebGL1 and Android GLES2.\nuniform float cols; // = 26.0 [4.0..80.0]\nuniform float rows; // = 15.0 [3.0..48.0]\nuniform float rowTear; // = 0.06 [0.0..0.3]\nuniform float garbage; // = 0.5 [0.0..1.0]\n\nfloat bt_hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }\n\nvec4 transition(vec2 uv){\n float env = sin(3.14159265 * progress);\n vec2 bs = vec2(cols, rows);\n\n float rowId = floor(uv.y * rows);\n float tear = (bt_hash(vec2(rowId, floor(progress * 24.0))) - 0.5) * rowTear * env;\n vec2 uvt = vec2(fract(uv.x + tear), uv.y);\n\n vec2 blk = floor(uvt * bs);\n float dropT = 0.05 + bt_hash(blk) * 0.40;\n float backT = 0.55 + bt_hash(blk + 11.3) * 0.40;\n\n float gone = step(dropT, progress);\n float back = step(backT, progress);\n\n vec3 a = getFromColor(uvt).rgb;\n vec3 b = getToColor(uvt).rgb;\n\n vec3 junk = getFromColor(fract(uvt + vec2(bt_hash(blk + 3.1) * 0.5, bt_hash(blk + 5.7) * 0.5))).rgb;\n junk = junk.gbr * (0.4 + bt_hash(blk + 9.0) * 0.6);\n vec3 hole = mix(vec3(0.0), junk, garbage * step(0.55, bt_hash(blk + 2.2)));\n\n vec3 col = mix(a, hole, gone * (1.0 - back));\n col = mix(col, b, back);\n return vec4(col, 1.0);\n}\n","PixelSort":"// Pixel Sort\n// blurb: Columns tear and quantize like glitch art, then resolve on a staggered per-column threshold.\n// Author: Dan (ByteTinker)\n// License: MIT\n// GL Transitions v1 — GLSL ES 1.0, runs unmodified in WebGL1 and Android GLES2.\nuniform float columns; // = 200.0 [20.0..600.0]\nuniform float strength; // = 0.45 [0.0..1.0]\nuniform float split; // = 0.012 [0.0..0.06]\nuniform float density; // = 0.35 [0.0..0.9]\n\nfloat bt_hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }\n\nvec4 transition(vec2 uv){\n float env = sin(3.14159265 * progress);\n float col = floor(uv.x * columns);\n float seed = bt_hash(vec2(col, 3.0));\n float active = step(density, seed);\n\n float amt = env * strength * (seed * 2.0 - 1.0) * active;\n vec3 a = getFromColor(vec2(uv.x, fract(uv.y + amt))).rgb;\n\n float s = split * env * active;\n vec2 ub = vec2(uv.x, fract(uv.y - amt));\n vec3 b;\n b.r = getToColor(vec2(fract(ub.x + s), ub.y)).r;\n b.g = getToColor(ub).g;\n b.b = getToColor(vec2(fract(ub.x - s), ub.y)).b;\n\n float q = mix(255.0, 6.0, env);\n a = floor(a * q) / q;\n b = floor(b * q) / q;\n\n float t = smoothstep(seed * 0.5, seed * 0.5 + 0.5, progress);\n return vec4(mix(a, b, t), 1.0);\n}\n","QuantumDither":"// Quantum Dither\n// blurb: Pixels stay undecided, shimmering between both frames, then collapse on an ordered threshold.\n// Author: Dan (ByteTinker)\n// License: MIT\n// GL Transitions v1 — GLSL ES 1.0, runs unmodified in WebGL1 and Android GLES2.\nuniform float grain; // = 180.0 [20.0..600.0]\nuniform float coherence; // = 0.6 [0.0..1.0]\nuniform float shimmer; // = 0.5 [0.0..1.0]\nuniform float edgeSoft; // = 0.10 [0.01..0.4]\n\nfloat bt_hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }\nfloat bt_bayer2(vec2 a){ a = floor(a); return fract(a.x * 0.5 + a.y * a.y * 0.75); }\nfloat bt_bayer4(vec2 a){ return (bt_bayer2(a * 0.5) * 0.25 + bt_bayer2(a)) / 0.9375; }\n\nvec4 transition(vec2 uv){\n vec2 g = floor(uv * vec2(grain * ratio, grain));\n float n = bt_hash(g);\n float bo = bt_bayer4(g);\n float thr = mix(n, bo, coherence);\n\n float w = edgeSoft;\n float front = abs(progress - thr);\n float undecided = exp(-front / max(w, 0.01) * 1.4);\n thr += (bt_hash(g + floor(progress * 45.0) * 17.0) - 0.5) * shimmer * undecided * 0.5;\n\n float t = smoothstep(thr - w, thr + w, progress * (1.0 + 2.0 * w) - w);\n\n vec3 a = getFromColor(uv).rgb;\n vec3 b = getToColor(uv).rgb;\n vec3 col = mix(a, b, t);\n col += vec3(0.18, 0.55, 0.62) * undecided * shimmer * 0.5;\n return vec4(col, 1.0);\n}\n","ReelChange":"// Reel Change\n// blurb: Cue mark burns in the corner, the splice jumps the frame, dust settles on the new reel.\n// Author: Dan (ByteTinker)\n// License: MIT\n// GL Transitions v1 — GLSL ES 1.0, runs unmodified in WebGL1 and Android GLES2.\nuniform float cueSize; // = 0.045 [0.0..0.12]\nuniform float jumpAmt; // = 0.16 [0.0..0.5]\nuniform float dust; // = 0.6 [0.0..1.0]\nuniform float scratch; // = 0.5 [0.0..1.0]\n\nfloat bt_hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }\n\nvec4 transition(vec2 uv){\n float e = progress;\n float splice = clamp((e - 0.46) / 0.16, 0.0, 1.0);\n float active = step(0.46, e) * step(e, 0.62);\n\n // stepped misregistration through the splice\n float k = floor(splice * 4.0);\n float off = (bt_hash(vec2(k, 2.0)) - 0.5) * jumpAmt * (1.0 - splice) * active;\n\n vec2 su = vec2(uv.x, fract(uv.y + off));\n vec3 a = getFromColor(su).rgb;\n vec3 b = getToColor(su).rgb;\n vec3 col = mix(a, b, step(0.54, e));\n\n // black frame bar riding through during the splice\n float barY = fract(uv.y + off * 3.0 + k * 0.37);\n float bar = (1.0 - smoothstep(0.0, 0.045, abs(barY - 0.5))) * active;\n col = mix(col, vec3(0.02), bar);\n\n // cue mark, upper right, four-frame flicker\n vec2 cp = (uv - vec2(0.865, 0.855)) * vec2(ratio, 1.0);\n float ring = 1.0 - smoothstep(cueSize * 0.72, cueSize, length(cp));\n float cueWin = step(0.20, e) * step(e, 0.47);\n float flick = 0.55 + 0.45 * step(0.5, fract(e * 26.0));\n col += vec3(1.0, 0.94, 0.80) * ring * cueWin * flick * 0.85;\n\n // dust and scratches, decaying after the change\n float decay = exp(-max(e - 0.54, 0.0) * 9.0) * step(0.46, e);\n float sp = bt_hash(uv * vec2(300.0, 170.0) + floor(e * 40.0));\n col += vec3(0.9) * step(0.9975, sp) * dust * decay;\n float sc = bt_hash(vec2(floor(uv.x * 260.0), floor(e * 20.0)));\n col += vec3(0.85, 0.82, 0.74) * step(0.995, sc) * scratch * decay * 0.7;\n\n return vec4(col, 1.0);\n}\n","SignalLock":"// Signal Lock\n// blurb: Static, rolling sync bars, then the picture snaps in like a tuner acquiring.\n// Author: Dan (ByteTinker)\n// License: MIT\n// GL Transitions v1 — GLSL ES 1.0, runs unmodified in WebGL1 and Android GLES2.\nuniform float noiseAmount; // = 0.85 [0.0..1.0]\nuniform float rollSpeed; // = 2.4 [0.0..8.0]\nuniform float barCount; // = 3.0 [0.0..8.0]\nuniform float tearAmount; // = 0.12 [0.0..0.5]\n\nfloat bt_hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }\n\nvec4 transition(vec2 uv){\n float env = sin(3.14159265 * progress); // 0 at both ends\n float settle = 1.0 - smoothstep(0.55, 1.0, progress);\n\n vec2 uvr = uv;\n uvr.y = fract(uv.y + settle * progress * rollSpeed);\n float line = floor(uv.y * 240.0);\n float tear = (bt_hash(vec2(line, floor(progress * 30.0))) - 0.5) * tearAmount * env;\n uvr.x = fract(uv.x + tear);\n\n vec4 a = getFromColor(uv);\n vec4 b = getToColor(uvr);\n float sig = smoothstep(0.42, 0.58, progress);\n vec4 img = mix(a, b, sig);\n\n float bp = fract(uv.y + progress * barCount);\n float bar = smoothstep(0.0, 0.05, bp) * (1.0 - smoothstep(0.05, 0.11, bp));\n img.rgb += bar * 0.40 * env;\n\n float st = bt_hash(uv * vec2(640.0, 360.0) + floor(progress * 60.0));\n img.rgb = mix(img.rgb, vec3(st), pow(env, 0.55) * noiseAmount * 0.7);\n\n return vec4(img.rgb, 1.0);\n}\n","SpectrumSweep":"// Spectrum Sweep\n// blurb: An analyzer band crosses the frame, drawing the incoming image as bars before it resolves.\n// Author: Dan (ByteTinker)\n// License: MIT\n// GL Transitions v1 — GLSL ES 1.0, runs unmodified in WebGL1 and Android GLES2.\nuniform float bandWidth; // = 0.22 [0.05..0.6]\nuniform float barCount; // = 64.0 [8.0..200.0]\nuniform float glow; // = 1.2 [0.0..3.0]\nuniform float floorLift; // = 0.12 [0.0..0.5]\n\nvec4 transition(vec2 uv){\n float front = progress * (1.0 + bandWidth) - bandWidth;\n vec3 a = getFromColor(uv).rgb;\n vec3 b = getToColor(uv).rgb;\n\n if(uv.x < front) return vec4(b, 1.0);\n if(uv.x > front + bandWidth) return vec4(a, 1.0);\n\n float k = (uv.x - front) / bandWidth; // 0 at trailing edge, 1 at leading\n float bx = (floor(uv.x * barCount) + 0.5) / barCount;\n\n vec3 s = b;\n float lum = dot(getToColor(vec2(bx, uv.y)).rgb, vec3(0.299, 0.587, 0.114));\n float h = clamp(lum + floorLift, 0.0, 1.0);\n\n float bar = step(uv.y, h) * (0.35 + 0.65 * step(h - 0.02, uv.y));\n float gap = smoothstep(0.0, 0.06, fract(uv.x * barCount)) * (1.0 - smoothstep(0.94, 1.0, fract(uv.x * barCount)));\n vec3 analyzer = vec3(0.25, 1.0, 0.72) * bar * gap;\n\n vec3 col = mix(s, analyzer, smoothstep(0.15, 0.85, k));\n col = mix(col, a, smoothstep(0.75, 1.0, k));\n col += vec3(0.4, 1.0, 0.8) * exp(-abs(k - 1.0) * 22.0) * glow;\n return vec4(col, 1.0);\n}\n","ThermalBloom":"// Thermal Bloom\n// blurb: The frame falls into false colour, blooms hot, and the next image cools back out of it.\n// Author: Dan (ByteTinker)\n// License: MIT\n// GL Transitions v1 — GLSL ES 1.0, runs unmodified in WebGL1 and Android GLES2.\nuniform float heat; // = 1.0 [0.0..1.0]\nuniform float blur; // = 0.010 [0.0..0.05]\nuniform float gain; // = 0.35 [0.0..1.2]\n\nvec3 bt_iron(float t){\n t = clamp(t, 0.0, 1.0);\n return clamp(vec3(\n smoothstep(0.05, 0.55, t) * 1.15,\n smoothstep(0.40, 0.98, t),\n smoothstep(0.0, 0.22, t) * 0.62 - smoothstep(0.22, 0.62, t) * 0.60 + smoothstep(0.78, 1.0, t)\n ), 0.0, 1.0);\n}\n\nvec4 transition(vec2 uv){\n float env = sin(3.14159265 * progress);\n float t = smoothstep(0.34, 0.66, progress);\n\n float r = blur * env;\n vec3 a = vec3(0.0), b = vec3(0.0);\n for(int i = 0; i < 5; i++){\n float f = (float(i) - 2.0) * 0.5;\n a += getFromColor(clamp(uv + vec2(f * r * ratio, f * r), 0.0, 1.0)).rgb;\n b += getToColor(clamp(uv + vec2(f * r * ratio, -f * r), 0.0, 1.0)).rgb;\n }\n a /= 5.0; b /= 5.0;\n\n vec3 real = mix(a, b, t);\n float lum = dot(real, vec3(0.299, 0.587, 0.114));\n vec3 thermal = bt_iron(lum + env * gain);\n\n vec3 col = mix(real, thermal, env * heat);\n return vec4(col, 1.0);\n}\n","TraceRoute":"// Trace Route\n// blurb: Copper traces route across the board and the new frame fills in behind them.\n// Author: Dan (ByteTinker)\n// License: MIT\n// GL Transitions v1 — GLSL ES 1.0, runs unmodified in WebGL1 and Android GLES2.\nuniform float pitch; // = 30.0 [6.0..90.0]\nuniform float wander; // = 0.45 [0.0..1.0]\nuniform float traceGlow;// = 1.4 [0.0..3.0]\nuniform float traceW; // = 0.10 [0.02..0.35]\n\nfloat bt_hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }\n\nvec4 transition(vec2 uv){\n vec2 gv = vec2(pitch * ratio, pitch);\n vec2 cell = floor(uv * gv);\n vec2 f = fract(uv * gv);\n\n float march = (cell.x / gv.x) * 0.65 + abs(cell.y / gv.y - 0.5) * 0.35;\n float d = clamp(mix(march, bt_hash(cell), wander), 0.0, 0.92);\n\n float e = 0.07;\n float pp = progress * (1.0 + e);\n float fill = smoothstep(d, d + e, pp);\n\n vec3 a = getFromColor(uv).rgb;\n vec3 b = getToColor(uv).rgb;\n vec3 col = mix(a, b, fill);\n\n float horiz = step(0.5, bt_hash(cell + 4.7));\n float line = mix(\n 1.0 - smoothstep(0.0, traceW, abs(f.x - 0.5)),\n 1.0 - smoothstep(0.0, traceW, abs(f.y - 0.5)),\n horiz);\n float pad = 1.0 - smoothstep(0.0, traceW * 1.6, length(f - 0.5));\n float front = exp(-abs(pp - d) / max(e, 0.005) * 1.5);\n front *= smoothstep(0.0, 0.05, progress) * (1.0 - smoothstep(0.92, 1.0, progress));\n\n col += vec3(1.0, 0.58, 0.20) * max(line, pad) * front * traceGlow;\n return vec4(col, 1.0);\n}\n","VanEck":"// Van Eck\n// blurb: The next frame reconstructs from raster noise, scanline by scanline, behind an acquisition beam.\n// Author: Dan (ByteTinker)\n// License: MIT\n// GL Transitions v1 — GLSL ES 1.0, runs unmodified in WebGL1 and Android GLES2.\nuniform float lineCount; // = 300.0 [60.0..720.0]\nuniform float smear; // = 0.07 [0.0..0.3]\nuniform float jitter; // = 0.35 [0.0..1.0]\nuniform float phosphor; // = 1.0 [0.0..1.0]\n\nfloat bt_hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }\n\nvec4 transition(vec2 uv){\n float gate = smoothstep(0.0, 0.04, progress);\n float line = floor(uv.y * lineCount);\n float ly = line / lineCount;\n\n float j = bt_hash(vec2(line, 7.0)) * jitter;\n float thresh = clamp(ly * (1.0 - jitter) + j, 0.0, 0.8);\n float acq = smoothstep(thresh, thresh + 0.18, progress);\n\n vec3 b = vec3(0.0);\n float sm = smear * (1.0 - acq);\n for(int i = 0; i < 6; i++){\n float f = float(i) / 5.0;\n b += getToColor(vec2(fract(uv.x + f * sm), uv.y)).rgb;\n }\n b /= 6.0;\n\n float lum = dot(b, vec3(0.299, 0.587, 0.114));\n vec3 phos = vec3(lum) * mix(vec3(1.0), vec3(1.0, 0.72, 0.25), phosphor);\n b = mix(phos, b, acq);\n\n float n = bt_hash(uv * vec2(900.0, 500.0) + floor(progress * 90.0));\n b = mix(b, vec3(n) * mix(vec3(1.0), vec3(1.0, 0.72, 0.25), phosphor), (1.0 - acq) * 0.55);\n\n vec3 a = getFromColor(uv).rgb;\n float wipe = smoothstep(thresh - 0.25, thresh, progress) * gate;\n vec3 res = mix(a, b, wipe);\n\n float beam = exp(-abs(progress - thresh) * 55.0) * gate;\n res += beam * vec3(1.0, 0.78, 0.34) * 0.55;\n\n res = mix(res, getToColor(uv).rgb, smoothstep(0.96, 1.0, progress));\n return vec4(res, 1.0);\n}\n"};
window.__TRANSITION_MANIFEST=[{"id":"CRTCollapse","name":"CRT Collapse","blurb":"Frame crushes to a line, then a dot, then the next image blooms back out. Power-cycle drama.","file":"CRTCollapse.glsl","params":[{"name":"lineHold","default":0.16,"min":0,"max":0.45},{"name":"flashGain","default":1.6,"min":0,"max":4},{"name":"bloom","default":14,"min":4,"max":40}]},{"id":"Datamosh","name":"Datamosh","blurb":"P-frame corruption — the old frames gradients smear the new one until the blocks give up.","file":"Datamosh.glsl","params":[{"name":"blockSize","default":30,"min":6,"max":90},{"name":"bleed","default":0.45,"min":0,"max":1.5},{"name":"chroma","default":0.5,"min":0,"max":2}]},{"id":"Etch","name":"Etch","blurb":"Photomask reveal — the frame develops in on a stepper field, with a hot exposure edge.","file":"Etch.glsl","params":[{"name":"cellSize","default":26,"min":6,"max":90},{"name":"edgeGlow","default":1,"min":0,"max":3},{"name":"randomness","default":0.55,"min":0,"max":1},{"name":"softness","default":0.09,"min":0.005,"max":0.3}]},{"id":"FiberSplice","name":"Fiber Splice","blurb":"Two ends draw apart, the arc fires, and the new frame fuses in from the seam.","file":"FiberSplice.glsl","params":[{"name":"separation","default":0.3,"min":0,"max":0.5},{"name":"arcGain","default":1.8,"min":0,"max":4},{"name":"arcTight","default":26,"min":4,"max":80},{"name":"flicker","default":0.5,"min":0,"max":1}]},{"id":"FilmAdvance","name":"Film Advance","blurb":"The strip pulls through the gate — frame bar and sprockets sweep past, shutter flickers, next frame registers with a bounce.","file":"FilmAdvance.glsl","params":[{"name":"frameBar","default":0.11,"min":0.02,"max":0.35},{"name":"bounce","default":0.5,"min":0,"max":1.5},{"name":"shutter","default":0.55,"min":0,"max":1},{"name":"sprockets","default":1,"min":0,"max":1},{"name":"grainAmt","default":0.5,"min":0,"max":1}]},{"id":"PacketLoss","name":"Packet Loss","blurb":"Blocks drop out of the stream, rows tear, and the new frame retransmits block by block.","file":"PacketLoss.glsl","params":[{"name":"cols","default":26,"min":4,"max":80},{"name":"rows","default":15,"min":3,"max":48},{"name":"rowTear","default":0.06,"min":0,"max":0.3},{"name":"garbage","default":0.5,"min":0,"max":1}]},{"id":"PixelSort","name":"Pixel Sort","blurb":"Columns tear and quantize like glitch art, then resolve on a staggered per-column threshold.","file":"PixelSort.glsl","params":[{"name":"columns","default":200,"min":20,"max":600},{"name":"strength","default":0.45,"min":0,"max":1},{"name":"split","default":0.012,"min":0,"max":0.06},{"name":"density","default":0.35,"min":0,"max":0.9}]},{"id":"QuantumDither","name":"Quantum Dither","blurb":"Pixels stay undecided, shimmering between both frames, then collapse on an ordered threshold.","file":"QuantumDither.glsl","params":[{"name":"grain","default":180,"min":20,"max":600},{"name":"coherence","default":0.6,"min":0,"max":1},{"name":"shimmer","default":0.5,"min":0,"max":1},{"name":"edgeSoft","default":0.1,"min":0.01,"max":0.4}]},{"id":"ReelChange","name":"Reel Change","blurb":"Cue mark burns in the corner, the splice jumps the frame, dust settles on the new reel.","file":"ReelChange.glsl","params":[{"name":"cueSize","default":0.045,"min":0,"max":0.12},{"name":"jumpAmt","default":0.16,"min":0,"max":0.5},{"name":"dust","default":0.6,"min":0,"max":1},{"name":"scratch","default":0.5,"min":0,"max":1}]},{"id":"SignalLock","name":"Signal Lock","blurb":"Static, rolling sync bars, then the picture snaps in like a tuner acquiring.","file":"SignalLock.glsl","params":[{"name":"noiseAmount","default":0.85,"min":0,"max":1},{"name":"rollSpeed","default":2.4,"min":0,"max":8},{"name":"barCount","default":3,"min":0,"max":8},{"name":"tearAmount","default":0.12,"min":0,"max":0.5}]},{"id":"SpectrumSweep","name":"Spectrum Sweep","blurb":"An analyzer band crosses the frame, drawing the incoming image as bars before it resolves.","file":"SpectrumSweep.glsl","params":[{"name":"bandWidth","default":0.22,"min":0.05,"max":0.6},{"name":"barCount","default":64,"min":8,"max":200},{"name":"glow","default":1.2,"min":0,"max":3},{"name":"floorLift","default":0.12,"min":0,"max":0.5}]},{"id":"ThermalBloom","name":"Thermal Bloom","blurb":"The frame falls into false colour, blooms hot, and the next image cools back out of it.","file":"ThermalBloom.glsl","params":[{"name":"heat","default":1,"min":0,"max":1},{"name":"blur","default":0.01,"min":0,"max":0.05},{"name":"gain","default":0.35,"min":0,"max":1.2}]},{"id":"TraceRoute","name":"Trace Route","blurb":"Copper traces route across the board and the new frame fills in behind them.","file":"TraceRoute.glsl","params":[{"name":"pitch","default":30,"min":6,"max":90},{"name":"wander","default":0.45,"min":0,"max":1},{"name":"traceGlow","default":1.4,"min":0,"max":3},{"name":"traceW","default":0.1,"min":0.02,"max":0.35}]},{"id":"VanEck","name":"Van Eck","blurb":"The next frame reconstructs from raster noise, scanline by scanline, behind an acquisition beam.","file":"VanEck.glsl","params":[{"name":"lineCount","default":300,"min":60,"max":720},{"name":"smear","default":0.07,"min":0,"max":0.3},{"name":"jitter","default":0.35,"min":0,"max":1},{"name":"phosphor","default":1,"min":0,"max":1}]}];