Two helpers made the bulk of it mechanical. `inkStroke` is a drop-in for
sigEdge — the same line at the identity's weight rather than the track's — and
`castForm` is a drop-in for sigForm, same signature so call sites do not change
shape. With those in place the substitution table is one-to-one:
sigForm( -> castForm( sigShape( -> castMain( sigEdge( -> inkStroke(
Forty-seven scenes went through that pass in one run: twenty-six take the cast
and the ink, twenty-one take the ink alone. Then the gate ran over all sixty-five
and found three the pass had broken, which is the entire reason it exists.
Spectrum Sculpture rendered pure black. It had been using sigShape as a RADIAL
METRIC rather than drawing it, and the cast carries notches and a hollow — an
annulus used as a radius turns a sculpture inside out. Reverted to sigShape and
dropped to ink only. The lesson generalises: a scene that reads a form as
geometry is not a scene that draws it, and the classifier cannot tell those
apart from the source.
Eclipse Field stopped honouring its `style` trait. It opts out of surface grain,
so sigEdge was its only style evidence, and the ink replaced it. The trait claim
is now dropped — and so is the lint change that had let inkMask count as style
evidence, which was wrong and was hiding exactly this. A trait is a property of
the track a scene may honour; an artifact is content it draws. Taking the ink
says nothing about whether a scene responds to u_sigLine.
Circuit Bloom went empty at the bottom of its `grown` range, where the pads were
carried by a hairline and the ink's stroke is thinner than the edge it replaced.
Now filled as well as stroked.
Also: the backtick check now covers every shader literal rather than only the
preamble, because a mechanical pass over sixty files reintroduced one
immediately. Three rounds lost to that typo is enough.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
85 lines
3.9 KiB
JavaScript
85 lines
3.9 KiB
JavaScript
// Glitch family: horizontal bands, each holding the image at a different age.
|
|
//
|
|
// Distinct from every other glitch scene in the library by what it corrupts.
|
|
// Scan Tear displaces rows sideways, Pitch Shatter shifts bands vertically,
|
|
// Block Mosh drags blocks along a stroke, Signal Decay attacks the signal
|
|
// itself. Here NOTHING is displaced: every band shows the same image, just at a
|
|
// different moment. The frame is spatially intact and temporally shredded,
|
|
// which is a slit-scan — and it is built from the one feedback buffer by giving
|
|
// each band its own persistence rather than by keeping a history.
|
|
//
|
|
// Scaffolded by tools/new-scene.js. See HOWTO-visualizers.md.
|
|
|
|
export const timeSmear = {
|
|
name: 'Time Smear',
|
|
family: 'glitch',
|
|
kind: 'fragment',
|
|
consumes: ['ink'],
|
|
traits: ['camera', 'style'],
|
|
|
|
params: {
|
|
bands: { type: 'int', range: [3, 22], default: 9, uniform: 'u_bands', bias: 'density' },
|
|
lag: { type: 'float', range: [0.3, 0.97], default: 0.82, uniform: 'u_lag' },
|
|
stagger: { type: 'float', range: [0, 1], default: 0.7, uniform: 'u_stagger' },
|
|
scale: { type: 'float', range: [1, 12], default: 4.2, uniform: 'u_scale', bias: 'density' },
|
|
speed: { type: 'float', range: [0.05, 1.2], default: 0.4, uniform: 'u_speed', bias: 'motion', rate: true },
|
|
glow: { type: 'float', range: [0, 1.5], default: 0.5, uniform: 'u_glow', bias: 'energy' },
|
|
reshuffle:{ type: 'float', range: [0, 1], default: 0.45, uniform: 'u_reshuffle' },
|
|
palette: { type: 'palette', count: 5 },
|
|
},
|
|
|
|
reactive: {
|
|
glow: { feature: 'beat', amount: 0.3, response: 'spike' },
|
|
scale: { feature: 'bandHigh', amount: 0.2, response: 'smooth' },
|
|
},
|
|
|
|
shader: `
|
|
vec4 scene(vec2 uv, vec2 p) {
|
|
float t = u_time * u_speed + u_seed;
|
|
vec2 cp = sigCamera(p);
|
|
|
|
// The subject. It must stand alone: a scene that only reads prev() is black
|
|
// until feedback converges and is not the same after a seek as after
|
|
// playback. This is a moving figure with enough structure that lagging it
|
|
// is legible — a flat field would smear into nothing.
|
|
float swirl = fbm(cp * u_scale * 0.4 + vec2(sin(t * 0.7), cos(t * 0.5)), 4);
|
|
float arm = sin(atan(cp.y, cp.x) * 3.0 + length(cp) * u_scale - t * 2.5 + swirl * 3.0);
|
|
float figure = sat(0.5 + arm * 0.5) * exp(-dot(cp, cp) * 0.55);
|
|
|
|
vec3 fresh = palRamp(0.1 + swirl * 0.4 + figure * 0.3) * (0.25 + figure * 1.1);
|
|
fresh += pal(4) * pow(figure, 5.0) * u_glow;
|
|
|
|
// Which band this pixel is in, and how old that band's image is. The
|
|
// assignment re-rolls on the eighth-bar grid, so the shredding steps with
|
|
// the music rather than crawling.
|
|
float era = floor(u_barPhase * 8.0) + floor(t * 2.0) * 8.0;
|
|
float band = floor(uv.y * float(u_bands));
|
|
float rnd = hash12(vec2(band, era * u_reshuffle));
|
|
|
|
// Age: 0 means live, 1 means as stale as this scene allows. Staggering by
|
|
// the hash is what makes neighbouring bands disagree about the present.
|
|
float age = mix(band / float(u_bands), rnd, u_stagger);
|
|
float persistence = u_lag * (0.35 + age * 0.65);
|
|
|
|
// The band's own history, held by weighting the previous frame against the
|
|
// fresh one. A band at high persistence updates so slowly that it is
|
|
// effectively showing several frames ago.
|
|
vec3 held = prev(uv);
|
|
vec3 col = mix(fresh, held, persistence);
|
|
|
|
// Seam between bands, drawn in the track's line weight so the shredding
|
|
// reads as deliberate rather than as a broken renderer.
|
|
float seam = abs(fract(uv.y * float(u_bands)) - 0.5) * 2.0;
|
|
col += pal(3) * smoothstep(0.88, 1.0, seam) * (0.05 + u_sigLine * 0.12);
|
|
|
|
// Stale bands lose colour, the way a held frame loses its highlights.
|
|
col = mix(col, vec3(dot(col, vec3(0.299, 0.587, 0.114))), age * 0.35);
|
|
|
|
col += sigGrain(uv);
|
|
return vec4(inkValue(col), 1.0);
|
|
}
|
|
`,
|
|
};
|
|
|
|
export default timeSmear;
|