Three glitch scenes: a press, a loop and a tape
Halftone Misprint is a printing fault rather than an electronic one — the image is never damaged, only separated and reassembled out of register, and nothing else in the library is made of dots. Droste Feedback scales the previous frame where Time Smear translates it, so the image never clears. Analog Wow is the continuous, wet counterpart to Scan Tear and Block Mosh: the error varies smoothly down the frame because every line was written at a different moment. Three things that had to be got right. The halftone ruling is a count of dots across the frame, not a pixel pitch, or a 4K export is the same dot on a bigger sheet. The Droste loop has to CONTRACT — expanding pushes every copy off the edge and leaves a plume instead of a corridor. And all three expressed style only through grain at first, which is the Side Quest 1 complaint: they now put the track's line weight and edge softness into the dot, the ring and the band boundary, taking the measured style response from 24/11/18 to 86/255/88. Neither feedback scene declares a slow axis, with the numbers recorded in the files: their own history moves the ten-second average further than any parameter does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,9 @@ import { dataAisle } from './shader/data-aisle.js';
|
||||
import { voronoiShatter } from './shader/voronoi-shatter.js';
|
||||
import { apollonianGasket } from './shader/apollonian-gasket.js';
|
||||
import { isometricBlocks } from './shader/isometric-blocks.js';
|
||||
import { halftoneMisprint } from './shader/halftone-misprint.js';
|
||||
import { drosteFeedback } from './shader/droste-feedback.js';
|
||||
import { analogWow } from './shader/analog-wow.js';
|
||||
|
||||
/**
|
||||
* The scene library. Families exist so the arc driver can choose by section
|
||||
@@ -143,6 +146,9 @@ const MODULES = [
|
||||
voronoiShatter,
|
||||
apollonianGasket,
|
||||
isometricBlocks,
|
||||
halftoneMisprint,
|
||||
drosteFeedback,
|
||||
analogWow,
|
||||
];
|
||||
|
||||
const errors = [];
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
// 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,
|
||||
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' },
|
||||
smear: { type: 'float', range: [0.4, 0.92], default: 0.7, uniform: 'u_smear' },
|
||||
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' },
|
||||
smear: { feature: 'loudness', amount: 0.15, 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) * sigEdge(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), u_smear * 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);
|
||||
|
||||
col += sigGrain(uv);
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default analogWow;
|
||||
@@ -0,0 +1,85 @@
|
||||
// Glitch family: video feedback. A camera pointed at its own monitor — the
|
||||
// frame is redrawn inside itself slightly smaller and slightly turned, so a
|
||||
// small motif at the centre becomes an endless corridor of copies of itself.
|
||||
//
|
||||
// Time Smear is the other feedback glitch and it TRANSLATES the previous frame,
|
||||
// which leaves a comet trail behind a moving subject. This scales it. The
|
||||
// difference is the whole scene: a translated feedback drifts off the edge and
|
||||
// clears, a scaled one never clears, because everything the frame has ever
|
||||
// contained is still in there, one ring further in.
|
||||
//
|
||||
// A live motif is drawn every frame, so the image exists before the loop has
|
||||
// anything in it and comes back after a seek.
|
||||
//
|
||||
// No declared slow axis. What the corridor holds depends on where the loop has
|
||||
// been, so two ten-second windows of the same parameters are 0.020 apart on
|
||||
// their own and the persistence axis moved 0.004 — the same measurement Turing
|
||||
// Bloom and Voronoi Shatter record. A feedback scene's history is its structure,
|
||||
// and Phase 11 cannot separate a parameter from it.
|
||||
|
||||
export const drosteFeedback = {
|
||||
name: 'Droste Feedback',
|
||||
family: 'glitch',
|
||||
kind: 'fragment',
|
||||
texture: 0.5,
|
||||
traits: ['camera', 'style'],
|
||||
|
||||
params: {
|
||||
zoom: { type: 'float', range: [0.93, 1.02], default: 0.975, uniform: 'u_zoomStep' },
|
||||
turn: { type: 'float', range: [-0.12, 0.12], default: 0.04, uniform: 'u_turn' },
|
||||
persist: { type: 'float', range: [0.9, 0.995], default: 0.985, uniform: 'u_persist' },
|
||||
motif: { type: 'float', range: [0.04, 0.35], default: 0.14, uniform: 'u_motif' },
|
||||
bloom: { type: 'float', range: [0, 1.4], default: 0.6, uniform: 'u_bloom', bias: 'energy' },
|
||||
tint: { type: 'float', range: [0, 0.5], default: 0.15, uniform: 'u_tint' },
|
||||
pace: { type: 'float', range: [0.05, 0.8], default: 0.25, uniform: 'u_pace', bias: 'motion', rate: true },
|
||||
palette: { type: 'palette', count: 5 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
bloom: { feature: 'bandMid', amount: 0.3, response: 'smooth' },
|
||||
motif: { feature: 'beat', amount: 0.2, response: 'spike' },
|
||||
},
|
||||
|
||||
shader: `
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float t = u_time * u_pace + u_seed;
|
||||
// Folded as well as filmed: a feedback loop through a mirror is still a
|
||||
// feedback loop, and the fold is the track's, not this scene's.
|
||||
p = sigFolded(sigCamera(p));
|
||||
|
||||
// The motif. Small, live, and always drawn: this is the thing the loop eats.
|
||||
vec2 at = vec2(sin(t * 0.6), cos(t * 0.47)) * 0.42;
|
||||
float d = length(p - at) - u_motif;
|
||||
float ring = abs(d) - u_motif * 0.35;
|
||||
|
||||
vec3 col = pal(0) * 0.05;
|
||||
col += palRamp(0.2 + fbm(p * 2.0 + t, 3) * 0.2) * smoothstep(0.02, -0.02, ring) * (0.5 + u_bloom * 0.8);
|
||||
col += pal(4) * sigEdge(ring) * (0.3 + u_bloom * 0.9);
|
||||
col += pal(3) * exp(-abs(d) * 9.0) * u_bloom * 0.35;
|
||||
|
||||
// The loop: read the previous frame from a slightly LARGER, slightly turned
|
||||
// sample of itself, so last frame's image comes back shrunk toward the
|
||||
// middle. Contracting is what nests — expanding pushes every copy off the
|
||||
// edge and leaves a plume instead of a corridor. Scaling about the centre in
|
||||
// screen space is what keeps the copies concentric rather than sliding.
|
||||
vec2 c = uv - 0.5;
|
||||
c.x *= u_aspect;
|
||||
c = rot(u_turn) * c / max(u_zoomStep, 0.5);
|
||||
c.x /= u_aspect;
|
||||
vec3 past = prev(c + 0.5);
|
||||
|
||||
// Each generation is tinted a step further round the palette, so depth into
|
||||
// the corridor is legible as colour rather than only as size.
|
||||
past = mix(past, palRamp(0.6) * (past.r + past.g + past.b) * 0.5, u_tint);
|
||||
|
||||
col = max(col, past * u_persist);
|
||||
|
||||
// Vignette, which also stops the corner content being recycled forever.
|
||||
col *= 0.75 + 0.25 * exp(-dot(p, p) * 0.35);
|
||||
col += sigGrain(uv);
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default drosteFeedback;
|
||||
@@ -0,0 +1,102 @@
|
||||
// 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,
|
||||
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));
|
||||
col += sigGrain(uv);
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default halftoneMisprint;
|
||||
Reference in New Issue
Block a user