music-video-gen/flow-state/src/scenes/shader/halftone-misprint.js
Dejvino e2e5104646 Take the effects off the visualizers and leave them to the grade
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>
2026-08-18 16:21:34 +02:00

103 lines
4.4 KiB
JavaScript

// Glitch family: a four-colour press run with the plates out of register. Each
// ink is screened at its own angle, and each one lands a little off, so the
// image is fringed and the dots moiré against each other.
//
// Every other glitch scene here corrupts a signal — tearing it, freezing it,
// blocking it up. This one is a printing fault rather than an electronic one:
// the image is never damaged, it is simply separated and reassembled wrong.
// Nothing else in the library is made of dots, and the dots are what carry it.
export const halftoneMisprint = {
name: 'Halftone Misprint',
family: 'glitch',
kind: 'fragment',
// The paper's tooth is the point; take the track's grain in full.
texture: 1.2,
consumes: ['ink'],
traits: ['camera', 'style'],
params: {
screen: { type: 'float', range: [18, 90], default: 42, uniform: 'u_screen', bias: 'density' },
slip: { type: 'float', range: [0, 0.06], default: 0.02, uniform: 'u_slip' },
angle: { type: 'float', range: [0, 1.6], default: 0.5, uniform: 'u_angle' },
ink: { type: 'float', range: [0.3, 1.5], default: 0.9, uniform: 'u_ink', slowAxis: true },
art: { type: 'float', range: [0.5, 4], default: 1.6, uniform: 'u_art', bias: 'density' },
press: { type: 'float', range: [0.02, 0.4], default: 0.09, uniform: 'u_press', bias: 'motion', rate: true },
palette: { type: 'palette', count: 5 },
},
reactive: {
slip: { feature: 'flux', amount: 0.3, response: 'spike' },
ink: { feature: 'bandLow', amount: 0.25, response: 'smooth' },
},
shader: `
// The artwork being printed: a soft, slow field, deliberately simple. What the
// scene is about is the separation, not the picture.
float artwork(vec2 q, float t) {
float a = fbm(q * u_art + vec2(t * 0.5, -t * 0.3), 4);
float b = 0.5 + 0.5 * sin(q.x * u_art * 2.0 + a * 4.0 + t);
return sat(a * 0.7 + b * 0.5);
}
// One screened separation: sample the artwork where THIS plate landed, then
// threshold it against a rotated dot grid. The dot grows with ink coverage,
// which is what a halftone is.
float plate(vec2 q, float ang, vec2 slip, float t, float gain) {
float density = sat(artwork(q + slip, t) * gain);
vec2 r = rot(ang) * (q + slip);
// Ruling is a count of dots ACROSS THE FRAME, not a size in pixels — which
// is what keeps a 720p preview and a 4K export the same print rather than
// the same dot pitch on a bigger sheet.
vec2 cell = fract(r * u_screen * 0.5) - 0.5;
// Dot edge hardness is the track's: a sharp track prints a crisp dot on
// coated stock, a soft one lets the ink spread into the fibre.
float soft = 0.04 + u_sigSoft * 0.45;
float radius = sqrt(max(density, 0.0)) * 0.62;
return smoothstep(radius, radius - soft, length(cell));
}
vec4 scene(vec2 uv, vec2 p) {
float t = u_time * u_press + u_seed;
p = sigCamera(p);
// Registration error. It steps on the bar rather than drifting: a press
// slips, settles, and slips again, and a continuously crawling offset reads
// as a wobble rather than as a fault.
float era = floor(u_barPhase * 4.0) + floor(t * 2.0) * 4.0;
vec2 slipC = (hash22(vec2(era, 1.0)) - 0.5) * u_slip;
vec2 slipM = (hash22(vec2(era, 2.0)) - 0.5) * u_slip;
vec2 slipY = (hash22(vec2(era, 3.0)) - 0.5) * u_slip;
// Classic screen angles: 15°, 75°, 0°, spread by the angle parameter so a
// track can have them in near-register or wildly rosetted.
float a1 = 0.26 * u_angle, a2 = 1.31 * u_angle, a3 = 0.0;
float c = plate(p, a1, slipC, t, u_ink);
float m = plate(p, a2, slipM, t, u_ink);
float y = plate(p, a3, slipY, t, u_ink);
// Subtractive-ish: each ink takes light out of the paper, in its own hue.
vec3 col = pal(1) * 0.85;
col -= pal(2) * c * 0.5;
col -= pal(3) * m * 0.5;
col -= pal(4) * y * 0.45;
// The black plate, printed last and in register, which is what stops the
// whole thing turning to mud.
float k = plate(p, 0.65 * u_angle, vec2(0.0), t, u_ink * 0.7);
col = mix(col, pal(0) * 0.12, k * 0.55);
// Ink edges take the track's line weight — a hard press or a soft one.
col += pal(0) * sigEdge(0.5 - c) * (0.05 + u_sigLine * 0.35);
col = max(col, vec3(0.0));
return vec4(inkValue(col), 1.0);
}
`,
};
export default halftoneMisprint;