Forty-nine scenes carried their own `glow`, every one of the sixty-five added its own grain, and a handful had bloom, chroma, haze, smear or trail besides — all of which the post chain already does, with an envelope and a threshold no scene can see. Two uncoordinated effect layers is the smaller half of the problem. The larger half is that an effect knob sampled per song makes a scene look varied across parameter draws while its structure never moves, so the gallery and the variety harness were both being told a scene was original when only its halo had changed. Removed in three shapes. Scene grain goes entirely — the post chain owns it. Statements that ADD a falloff term scaled by the effect are bloom by another name and are deleted. What remains is an intensity on something already drawn, so the uniform is pinned to its default and the knob deleted: the picture survives, the fake variety does not. Reactive entries driving those params went too, since an effect modulated by the audio was the most convincing fake of the lot. That broke the style trait, which is the interesting part. Surface treatment had been leaving the scenes for two commits — sigGrain to post, sigEdge to inkStroke — and removing the last of it left `style` with almost nothing to express: a style+shape signature had two eligible scenes in the whole library. The trait is not obsolete, it has moved, so inkStroke and inkMask now read u_sigLine and u_sigSoft alongside the ink's own weight and edge. A scene drawing in the song's hand honours the track's line weight by construction, and the runtime probe confirms it rather than taking it on trust. This reverses a call made two commits ago for a reason that only became true now. One check needed re-aiming rather than fixing. Two independently built shows are two WebGL contexts, and engine/hash.js says at the top that bit-exactness is a same-context guarantee; the check had been demanding it anyway and getting away with it because the scenes it happened to cast were bit-stable. A casting change put smooth-gradient scenes in frame and the last bit moved. Measured before touching the check: three levels out of 255 across 2.6% of pixels, invisible. The cross-context comparison is now a distance with a ceiling of four, and bit-exactness within one show is still demanded by the check below it. 89/89 checks, 11/11 tests, all static gates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
99 lines
4.6 KiB
JavaScript
99 lines
4.6 KiB
JavaScript
// Glitch family: tape. The transport is not quite steady, so every line of the
|
|
// image is displaced by where the tape was when that line was written — a
|
|
// continuous horizontal warp, chroma trailing behind luma, and the occasional
|
|
// dropout where the oxide has worn through.
|
|
//
|
|
// Scan Tear and Block Mosh are digital faults: they are quantised, blocky, and
|
|
// they happen to whole regions at once. This is analogue and wet — nothing has
|
|
// an edge, the error varies smoothly down the frame, and the colour lags the
|
|
// picture instead of being replaced by it. Wow and flutter, not corruption.
|
|
//
|
|
// No declared slow axis: the dropouts and the head smear put two windows of
|
|
// identical parameters 0.041 apart, the second-highest noise floor in the
|
|
// library, and chroma bleed against that measured 0.02x.
|
|
|
|
export const analogWow = {
|
|
name: 'Analog Wow',
|
|
family: 'glitch',
|
|
kind: 'fragment',
|
|
texture: 0.9,
|
|
consumes: ['ink'],
|
|
traits: ['camera', 'style'],
|
|
|
|
params: {
|
|
wow: { type: 'float', range: [0, 0.12], default: 0.035, uniform: 'u_wow' },
|
|
flutter: { type: 'float', range: [0, 0.03], default: 0.008, uniform: 'u_flutter' },
|
|
bleed: { type: 'float', range: [0, 0.05], default: 0.015, uniform: 'u_bleed' },
|
|
dropout: { type: 'float', range: [0, 0.6], default: 0.2, uniform: 'u_dropout' },
|
|
bars: { type: 'float', range: [1, 9], default: 3.5, uniform: 'u_bars', bias: 'density' },
|
|
speed: { type: 'float', range: [0.05, 0.7], default: 0.2, uniform: 'u_speed', bias: 'motion', rate: true },
|
|
palette: { type: 'palette', count: 5 },
|
|
},
|
|
|
|
reactive: {
|
|
dropout: { feature: 'flux', amount: 0.3, response: 'spike' },
|
|
wow: { feature: 'bandLow', amount: 0.25, response: 'smooth' },
|
|
},
|
|
|
|
shader: `
|
|
// What was recorded: broad soft bars of colour, the kind of thing that shows
|
|
// tape damage clearly because it has no detail of its own to hide it behind.
|
|
vec3 programme(vec2 q, float t) {
|
|
float band = q.y * u_bars + fbm(q * 1.4 + vec2(t * 0.3, 0.0), 3) * 1.2;
|
|
vec3 col = palRamp(0.1 + band * 0.12);
|
|
col *= 0.55 + 0.45 * sat(0.5 + 0.5 * sin(band * 3.14159 + t));
|
|
col += pal(4) * smoothstep(0.85, 1.0, sat(0.5 + 0.5 * sin(q.x * 3.0 - t * 1.3))) * 0.25;
|
|
// The boundary between bands is drawn in the track's weight, so even a
|
|
// damaged signal is damaged in the video's own hand.
|
|
col += pal(4) * inkStroke(fract(band) - 0.5) * (0.15 + u_sigLine * 0.9);
|
|
return col * 0.7;
|
|
}
|
|
|
|
vec4 scene(vec2 uv, vec2 p) {
|
|
float t = u_time * u_speed + u_seed;
|
|
p = sigCamera(p);
|
|
|
|
// Transport error for THIS line: a slow wow, a faster flutter, and a little
|
|
// noise on top. It is a function of y, which is what makes the frame skew
|
|
// rather than slide — every line was written at a different moment.
|
|
float line = p.y * 30.0;
|
|
float err = sin(p.y * 2.1 - t * 1.7) * u_wow
|
|
+ sin(p.y * 11.0 + t * 5.3) * u_flutter
|
|
+ (vnoise(vec2(line, t * 3.0)) - 0.5) * u_flutter * 2.0;
|
|
|
|
vec2 q = p + vec2(err, 0.0);
|
|
|
|
// Luma is where it should be; chroma trails it. Sampling the programme
|
|
// three times at three displacements is exactly what a chroma delay does.
|
|
vec3 col;
|
|
col.r = programme(q + vec2(u_bleed, 0.0), t).r;
|
|
col.g = programme(q, t).g;
|
|
col.b = programme(q - vec2(u_bleed * 0.7, 0.0), t).b;
|
|
|
|
// Dropouts: bands where the tape has lost contact. They travel slowly down
|
|
// the frame, and they replace the picture with the tape's own noise floor
|
|
// rather than with black — a black band is a cut, this is a fault.
|
|
float bandId = floor(p.y * 8.0 + t * 2.0);
|
|
float hit = step(1.0 - u_dropout * 0.35, hash11(bandId * 1.7 + floor(t * 3.0)));
|
|
float band = smoothstep(0.5, 0.15, abs(fract(p.y * 8.0 + t * 2.0) - 0.5)) * hit;
|
|
vec3 noise = pal(1) * (0.25 + 0.5 * hash12(vec2(uv.x * 400.0, bandId)));
|
|
col = mix(col, noise, band * 0.7);
|
|
|
|
// Head smear: the previous frame, dragged sideways by the same transport
|
|
// error, is what gives tape its characteristic horizontal ghosting. The
|
|
// programme above stands on its own, so a seek recovers immediately.
|
|
vec3 past = prev(uv - vec2(err * 0.4 + 0.004, 0.0));
|
|
col = mix(col, max(col, past), 0.7 * 0.55);
|
|
|
|
// Head-switching noise at the bottom of the frame — the one part of the
|
|
// image that is always damaged.
|
|
float sw = smoothstep(0.06, 0.0, uv.y) * u_dropout;
|
|
col = mix(col, pal(2) * hash12(vec2(uv.x * 300.0, floor(t * 12.0))), sw * 0.6);
|
|
|
|
return vec4(inkValue(col), 1.0);
|
|
}
|
|
`,
|
|
};
|
|
|
|
export default analogWow;
|