// Minimal family: one hairline drawing a harmonograph figure — two pendulums // per axis, slightly out of tune with each other — over its own slowly fading // ghost. Nothing else is in the frame. // // Silk Ribbon is the other minimal with a single moving element, and it is a // wide band: an area with two edges. This is a line with no width to speak of, // and it is the only scene in the library where what you are looking at is the // HISTORY of a moving point rather than the point. The figure never repeats // exactly, because the two frequencies are deliberately not a whole ratio, so // the drawing precesses instead of closing. export const pendulumTrace = { name: 'Pendulum Trace', family: 'minimal', kind: 'fragment', // Paints 0% of the frame — see checks/phase12 coverage. surface: 'composable', // Fine line work; grain only furs it up. texture: 0.25, consumes: ['ink'], traits: ['camera', 'style'], params: { ratio: { type: 'float', range: [1.5, 5.5], default: 3.01, uniform: 'u_ratio' }, detune: { type: 'float', range: [0, 0.08], default: 0.03, uniform: 'u_detune' }, size: { type: 'float', range: [0.5, 1.5], default: 1.05, uniform: 'u_size' }, decay: { type: 'float', range: [0, 0.5], default: 0.15, uniform: 'u_decay' }, weight: { type: 'float', range: [0.002, 0.02], default: 0.006, uniform: 'u_weight' }, arc: { type: 'float', range: [1.0, 8], default: 3.5, uniform: 'u_arc', bias: 'density' }, persist: { type: 'float', range: [0.9, 0.996], default: 0.992, uniform: 'u_persist', slowAxis: true }, speed: { type: 'float', range: [0.05, 0.9], default: 0.3, uniform: 'u_speed', bias: 'motion', rate: true }, palette: { type: 'palette', count: 5 }, }, reactive: { weight: { feature: 'beat', amount: 0.15, response: 'spike' }, arc: { feature: 'bandMid', amount: 0.2, response: 'smooth' }, }, shader: ` // Where the pen is at time s. Two pendulums per axis, the second slightly // detuned from a whole-number ratio — which is the entire reason a harmonograph // figure precesses instead of retracing one closed loop forever. vec2 pen(float s) { // The swing loses energy and is pushed again on the phrase. A real // exponential decay in absolute time would be correct and useless: five // minutes in, the figure would have shrunk to a dot. float damp = 1.0 - u_decay * (1.0 - cos(u_phrasePhase * 6.28318530718)) * 0.5; float x = sin(s) + 0.6 * sin(s * u_ratio + 1.7); float y = cos(s * (1.0 + u_detune)) + 0.6 * cos(s * u_ratio * (1.0 - u_detune) + 0.4); return vec2(x, y) * 0.5 * u_size * damp; } vec4 scene(vec2 uv, vec2 p) { float t = u_time * u_speed + u_seed; p = sigCamera(p); vec3 col = pal(0) * 0.07; // Vignette wash, so the empty frame is a lit space rather than black. col += pal(1) * 0.13 * exp(-dot(p, p) * 0.6); // The live stroke: the last u_arc radians of travel, sampled at a fixed // resolution and reduced to the nearest point on the curve. float best = 1e9; float head = 0.0; // lint: fixed-cost — 48 samples along a fixed arc, not a search for (int i = 0; i < 48; i++) { float f = float(i) / 47.0; float s = t * 20.0 - u_arc * (1.0 - f); float d = length(p - pen(s)); if (d < best) { best = d; head = f; } } float w = u_weight * (0.4 + u_sigLine * 2.5); float line = smoothstep(w, w * 0.2, best); // The tip is brightest and the tail falls away, so the line has a direction // of travel even in a still frame. vec3 ink = palRamp(0.2 + head * 0.35); col += ink * line * (0.5 + head * 0.9) * (0.6 + 0.6 * 0.8); // The ghost: everything the pen has already drawn, fading. Sampled straight // rather than smeared, so old strokes stay in place and thin out instead of // sliding around the frame. col = max(col, prev(uv) * u_persist); return vec4(inkValue(col), 1.0); } `, }; export default pendulumTrace;