music-video-gen/flow-state/src/scenes/shader/moire-grid.js
Dejvino c950f8f3b1 Give a full-frame field a subject the eye can track, and let the song choose
what it does

"One pattern across the screen" scenes have nothing to watch. Warping the field
around a focal point was the previous attempt and it did not fix that — measured
on Voronoi it raised orientation variety while layout stayed at 0.004, which is
another way of saying the cells changed and the frame still had no subject.

So a subject is now a form that does something TO the field, and which thing is
the song's decision rather than the scene's: shift, warp, punch, morph or
overlay. The same form punching a hole, bending the pattern or running it at
another rate are three different videos, and across six songs the bank picks
overlay, morph, shift, morph, warp and punch — so it is a real axis rather than
a constant with five names.

subjectSDF, subjectMask, subjectEdge and subjectWarp are in the contract, so any
field scene can take a subject without knowing where the focal points are.

Moiré Grid is the pilot and implements all five impacts. 0.031 to 0.037 — up a
fifth, and the layout block moved from essentially nothing to 0.018, which is
the first time anything has moved layout on a full-frame field. That is the
number that speaks to the complaint: the frame's energy is no longer spread
evenly, so there is somewhere to look.

It is still under the 0.04 bar, and I have not seen it. The score says there is
now a subject; whether it is worth watching is the question the gallery cannot
answer.

One bug worth recording: the subject helpers were placed above the ink in the
preamble, and subjectEdge draws with inkStroke. GLSL has no forward
declarations, so the whole preamble failed to compile and every scene using it
rendered pure black — which the gallery reported as a variety of exactly 0.000
across all six blocks rather than as an error. An all-zero row means a dead
shader, not a boring scene.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:52:57 +02:00

112 lines
5.0 KiB
JavaScript

// Geometric family: two rotating line grids interfering.
//
// Moiré is a spatial-aliasing effect by nature, so this scene is the most likely
// in the library to alias badly. Line width is held above a floor and scaled by
// u_pixelScale, which is what keeps a 720p preview and a 4K export looking the
// same rather than the preview shimmering.
export const moireGrid = {
name: 'Moiré Grid',
family: 'geometric',
kind: 'fragment',
// Personality: see look/Personality.js.
// Crisp line work: the track's surface grain would only fur the edges.
texture: 0,
consumes: ['ink'],
traits: ['camera', 'style'],
params: {
// The subject: the song's protagonist standing in the field, with the
// interference running at a different phase inside it. A window rather
// than a shape laid on top — the pattern is still the picture, it just
// has somewhere to be about, and the edge gives the eye a line to track.
subject: { type: 'float', range: [0, 1], default: 0.7, uniform: 'u_subject' },
subjSize: { type: 'float', range: [0.2, 0.75], default: 0.42, uniform: 'u_subjSize' },
density: { type: 'float', range: [6, 60], default: 22, uniform: 'u_density', bias: 'density' },
offset: { type: 'float', range: [0.0, 0.5], default: 0.08, uniform: 'u_offset' },
rotate: { type: 'float', range: [0, 0.25], default: 0.04, uniform: 'u_rotate', bias: 'motion', rate: true },
// Named u_lineWidth, not u_width: the shader contract already declares
// `uniform float u_width` for stereo width, and a colliding name is a
// redefinition error that renders the scene as a black frame.
width: { type: 'float', range: [0.06, 0.5], default: 0.2, uniform: 'u_lineWidth' , slowAxis: true },
warp: { type: 'float', range: [0, 1], default: 0.25, uniform: 'u_warp' },
palette: { type: 'palette', count: 4 },
},
reactive: {
offset: { feature: 'bandLow', amount: 0.35 },
warp: { feature: 'flux', amount: 0.2, response: 'smooth' },
},
shader: `
// Anti-aliased line grid: the smoothstep edge is widened by the screen-space
// derivative, so lines stay a consistent visual weight at any resolution.
float grid(vec2 q, float density, float width) {
vec2 g = q * density;
vec2 f = abs(fract(g) - 0.5);
float d = min(f.x, f.y);
float aa = max(fwidth(d), 0.001);
return smoothstep(width * 0.5 + aa, width * 0.5 - aa, d);
}
vec4 scene(vec2 uv, vec2 p) {
float t = u_time * u_rotate + u_seed;
p = sigFolded(sigCamera(p));
vec2 warp = vec2(fbm(p * 1.5 + t, 3), fbm(p * 1.5 - t, 3)) - 0.5;
vec2 q = p + warp * u_warp;
// Drawn in the track's hand: its line weight scales this scene's.
float weight = u_lineWidth * (0.5 + u_sigLine);
float a = grid(rot(t * 6.28318530718) * q, u_density, weight);
float b = grid(rot(-t * 6.28318530718 + u_offset * 3.14159) * (q + u_offset), u_density, weight);
// The subject does something TO the field, and which thing is the song's
// decision rather than this scene's — the same form punching a hole,
// bending the grid or running it at another rate are three different
// videos. See Identity.IMPACTS.
float d = subjectSDF(p, u_subjSize);
float inside = smoothstep(0.01, -0.01, d) * u_subject;
// WARP bends the pattern around the form instead of replacing it inside,
// so it is applied to the coordinate everything below is drawn from.
if (impactIs(1.0)) q = subjectWarp(q, u_subjSize, u_subject * 1.2);
vec2 qi = q + vec2(u_offset * 0.7, -u_offset * 0.4);
float ai = grid(rot(t * 6.28318530718 * 1.35) * qi, u_density * 1.18, weight);
float bi = grid(rot(-t * 6.28318530718 * 0.7 + u_offset * 3.14159) * (qi + u_offset), u_density * 0.86, weight);
// The interference term is the point: where both grids land, it peaks.
float interference = a * b;
float either = max(a, b);
if (impactIs(0.0)) { // shift: a different beat inside
interference = mix(interference, ai * bi, inside);
either = mix(either, max(ai, bi), inside);
} else if (impactIs(2.0)) { // punch: the field stops at the form
interference *= 1.0 - inside;
either *= 1.0 - inside;
} else if (impactIs(3.0)) { // morph: one grid survives inside
interference = mix(interference, a * 0.35, inside);
either = mix(either, a, inside);
} else if (impactIs(4.0)) { // overlay: the form reads as solid
either = max(either, inside);
interference = max(interference, inside * 0.6);
}
vec3 col = pal(0) * 0.05;
col += pal(1) * either * 0.35;
col += pal(2) * interference * (0.8 + 0.35);
// The rim, so the form has an edge to follow rather than only a change of
// texture. Drawn in the song's hand like everything else.
col = mix(col, pal(3), inkStroke(d) * u_subject);
col *= 0.6 + 0.4 * exp(-dot(p, p) * 0.3);
return vec4(inkValue(col), 1.0);
}
`,
};
export default moireGrid;