Finish the migration: all 65 scenes on the identity artifacts

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>
This commit is contained in:
Dejvino 2026-08-18 06:39:19 +02:00
parent e5bb7d77e0
commit 2d3ed8eb02
49 changed files with 234 additions and 123 deletions

View File

@ -452,6 +452,33 @@ float inkMask(float d, vec2 uv) {
return clamp(max(fillA, line), 0.0, 1.0); return clamp(max(fillA, line), 0.0, 1.0);
} }
/**
* Outline only, in the song's hand. The drop-in replacement for sigEdge.
*
* sigEdge draws a line at the track's line weight; this draws it at the
* identity's, which is the same idea one layer up. Kept separate from inkMask
* because a scene that only ever wanted an edge should not suddenly acquire a
* fill when it migrates.
*/
float inkStroke(float d) {
float soft = mix(0.03, 0.0015, clamp(u_inkEdge, 0.0, 1.0));
float w = 0.004 + u_inkWeight * 0.055;
return smoothstep(w + soft, w - soft, abs(d));
}
/**
* The protagonist as a filled, inked mask at a point. Replaces sigForm.
*
* Same signature as the thing it supersedes so the substitution is mechanical
* across the library see MIGRATION.md. uv is recomputed here rather than
* passed, so the call site does not have to change shape.
*/
float castForm(vec2 p, vec2 centre, float size) {
float s = max(size, 1e-3);
vec2 uv = vec2(p.x / u_aspect, p.y) * 0.5 + 0.5;
return inkMask(castMain((p - centre) / s) * s, uv);
}
/** The track's value structure. Off unless the identity asked for it. */ /** The track's value structure. Off unless the identity asked for it. */
vec3 inkValue(vec3 col) { vec3 inkValue(vec3 col) {
if (u_inkPosterize < 1.5) return col; if (u_inkPosterize < 1.5) return col;

View File

@ -17,6 +17,7 @@ export const analogWow = {
family: 'glitch', family: 'glitch',
kind: 'fragment', kind: 'fragment',
texture: 0.9, texture: 0.9,
consumes: ['ink'],
traits: ['camera', 'style'], traits: ['camera', 'style'],
params: { params: {
@ -46,7 +47,7 @@ vec3 programme(vec2 q, float t) {
col += pal(4) * smoothstep(0.85, 1.0, sat(0.5 + 0.5 * sin(q.x * 3.0 - t * 1.3))) * 0.25; 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 // The boundary between bands is drawn in the track's weight, so even a
// damaged signal is damaged in the video's own hand. // damaged signal is damaged in the video's own hand.
col += pal(4) * sigEdge(fract(band) - 0.5) * (0.15 + u_sigLine * 0.9); col += pal(4) * inkStroke(fract(band) - 0.5) * (0.15 + u_sigLine * 0.9);
return col * 0.7; return col * 0.7;
} }
@ -92,7 +93,7 @@ vec4 scene(vec2 uv, vec2 p) {
col = mix(col, pal(2) * hash12(vec2(uv.x * 300.0, floor(t * 12.0))), sw * 0.6); col = mix(col, pal(2) * hash12(vec2(uv.x * 300.0, floor(t * 12.0))), sw * 0.6);
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -14,6 +14,7 @@ export const apollonianGasket = {
kind: 'fragment', kind: 'fragment',
// Drawn geometry; grain only muddies the small discs. // Drawn geometry; grain only muddies the small discs.
texture: 0.3, texture: 0.3,
consumes: ['ink'],
traits: ['camera', 'style'], traits: ['camera', 'style'],
params: { params: {
@ -88,14 +89,14 @@ vec4 scene(vec2 uv, vec2 p) {
vec3 col = pal(0) * 0.06; vec3 col = pal(0) * 0.06;
col += palRamp(gen) * edge * (0.55 + u_rim * 0.7); col += palRamp(gen) * edge * (0.55 + u_rim * 0.7);
col += palRamp(gen + 0.25) * glow * u_rim * 0.4; col += palRamp(gen + 0.25) * glow * u_rim * 0.4;
col += pal(4) * sigEdge(d) * 0.5; col += pal(4) * inkStroke(d) * 0.5;
// Fill the interiors faintly so the discs are objects and not just outlines. // Fill the interiors faintly so the discs are objects and not just outlines.
col += palRamp(gen + 0.5) * smoothstep(0.0, -0.06, d) * 0.12; col += palRamp(gen + 0.5) * smoothstep(0.0, -0.06, d) * 0.12;
col *= 0.7 + 0.3 * exp(-dot(p, p) * 0.2); col *= 0.7 + 0.3 * exp(-dot(p, p) * 0.2);
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -21,6 +21,7 @@ export const aqueductMarch = {
family: 'structural', family: 'structural',
kind: 'fragment', kind: 'fragment',
texture: 0.9, texture: 0.9,
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'space', 'style'], traits: ['shape', 'camera', 'space', 'style'],
params: { params: {
@ -61,7 +62,7 @@ float tier(vec2 q, float bays, float top, float bottom, float shift) {
// The opening sits low in the tier, leaving a solid band above it to carry // The opening sits low in the tier, leaving a solid band above it to carry
// the next tier — which is the whole structural point of an aqueduct. // the next tier — which is the whole structural point of an aqueduct.
float hole = sigShape(local) * min(cw, ch) * k; float hole = castMain(local) * min(cw, ch) * k;
return smoothstep(-0.004, 0.004, hole); return smoothstep(-0.004, 0.004, hole);
} }
@ -110,7 +111,7 @@ vec4 scene(vec2 uv, vec2 p) {
vec3 masonry = mix(pal(2) * 0.35, pal(4) * 0.8, sat(q.x * 0.4 + 0.5)); vec3 masonry = mix(pal(2) * 0.35, pal(4) * 0.8, sat(q.x * 0.4 + 0.5));
masonry *= 0.7 + (0.15 + u_sigLine * 0.5) * course; masonry *= 0.7 + (0.15 + u_sigLine * 0.5) * course;
masonry += pal(3) * u_sunlight * 0.22 * sat(q.x * 0.5 + 0.5); masonry += pal(3) * u_sunlight * 0.22 * sat(q.x * 0.5 + 0.5);
masonry += pal(4) * sigEdge(0.5 - abs(fract(q.x * u_bays) - 0.5) - 0.03) * (0.1 + u_sigLine * 0.9); masonry += pal(4) * inkStroke(0.5 - abs(fract(q.x * u_bays) - 0.5) - 0.03) * (0.1 + u_sigLine * 0.9);
float hazed = mix(1.0, 0.55, fk * u_recede); float hazed = mix(1.0, 0.55, fk * u_recede);
col = mix(col, masonry * hazed, stone * base); col = mix(col, masonry * hazed, stone * base);
@ -118,7 +119,7 @@ vec4 scene(vec2 uv, vec2 p) {
col = sigAir(col, p, smoothstep(-0.2, 0.9, p.y - horizon)); col = sigAir(col, p, smoothstep(-0.2, 0.9, p.y - horizon));
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -13,6 +13,7 @@ export const auroraVeil = {
name: 'Aurora Veil', name: 'Aurora Veil',
family: 'flow', family: 'flow',
kind: 'fragment', kind: 'fragment',
consumes: ['ink'],
traits: ['camera', 'space', 'style'], traits: ['camera', 'space', 'style'],
params: { params: {
@ -73,7 +74,7 @@ vec4 scene(vec2 uv, vec2 p) {
col = sigAir(col, p, smoothstep(0.0, 1.6, abs(p.x))); col = sigAir(col, p, smoothstep(0.0, 1.6, abs(p.x)));
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -17,6 +17,7 @@ export const balanceStack = {
name: 'Balance Stack', name: 'Balance Stack',
family: 'minimal', family: 'minimal',
kind: 'fragment', kind: 'fragment',
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'space', 'style'], traits: ['shape', 'camera', 'space', 'style'],
params: { params: {
@ -102,7 +103,7 @@ vec4 scene(vec2 uv, vec2 p) {
x += u_lean * size * 1.6 + sin(t * 1.3 + fi * 0.7) * 0.012 * fi; x += u_lean * size * 1.6 + sin(t * 1.3 + fi * 0.7) * 0.012 * fi;
vec2 at = vec2(x, y); vec2 at = vec2(x, y);
float d = sigShape((p - at) / size) * size; float d = castMain((p - at) / size) * size;
// Body: shaded across the form, so it has a lit side and a dark side. // Body: shaded across the form, so it has a lit side and a dark side.
float lit = sat(dot(normalize(p - at + 1e-4), lightDir) * 0.5 + 0.5); float lit = sat(dot(normalize(p - at + 1e-4), lightDir) * 0.5 + 0.5);
@ -110,13 +111,13 @@ vec4 scene(vec2 uv, vec2 p) {
col = mix(col, body, smoothstep(0.004, -0.004, d)); col = mix(col, body, smoothstep(0.004, -0.004, d));
// Rim: the sun catching the edge, in the track's line weight. // Rim: the sun catching the edge, in the track's line weight.
col += pal(4) * sigEdge(d) * (0.3 + u_rim * 0.9) * (0.4 + lit * 0.8); col += pal(4) * inkStroke(d) * (0.3 + u_rim * 0.9) * (0.4 + lit * 0.8);
y += size; y += size;
} }
col = sigAir(col, p, smoothstep(0.1, 1.5, abs(p.x) * 0.7 + sat(above * 0.5))); col = sigAir(col, p, smoothstep(0.1, 1.5, abs(p.x) * 0.7 + sat(above * 0.5)));
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -11,6 +11,7 @@ export const blockMosh = {
family: 'glitch', family: 'glitch',
kind: 'fragment', kind: 'fragment',
// Personality: see look/Personality.js. // Personality: see look/Personality.js.
consumes: ['ink'],
traits: ['camera', 'style'], traits: ['camera', 'style'],
params: { params: {
@ -74,7 +75,7 @@ vec4 scene(vec2 uv, vec2 p) {
col += pal(2) * u_burst * (0.5 + 0.5 * blk) * (0.5 + 0.5 * beat); col += pal(2) * u_burst * (0.5 + 0.5 * blk) * (0.5 + 0.5 * beat);
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -15,6 +15,7 @@ export const cargoBelt = {
kind: 'fragment', kind: 'fragment',
// Takes the track's surface grain, but lightly — this is drawn, not filmed. // Takes the track's surface grain, but lightly — this is drawn, not filmed.
texture: 0.4, texture: 0.4,
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'style'], traits: ['shape', 'camera', 'style'],
params: { params: {
@ -65,11 +66,11 @@ vec4 scene(vec2 uv, vec2 p) {
if (rnd > u_gap) { if (rnd > u_gap) {
vec2 local = vec2((withinCell - 0.5) * 2.0, dy / max(span * 0.5, 1e-3)); vec2 local = vec2((withinCell - 0.5) * 2.0, dy / max(span * 0.5, 1e-3));
float size = u_crateSize * (0.7 + rnd * 0.5); float size = u_crateSize * (0.7 + rnd * 0.5);
float d = sigShape(local / max(size, 1e-3)) * size; float d = castMain(local / max(size, 1e-3)) * size;
vec3 crateColor = pal(int(mod(cell + fi, 4.0)) + 1); vec3 crateColor = pal(int(mod(cell + fi, 4.0)) + 1);
col = mix(col, crateColor * (0.35 + rnd * 0.5), smoothstep(0.02, -0.02, d)); col = mix(col, crateColor * (0.35 + rnd * 0.5), smoothstep(0.02, -0.02, d));
col += crateColor * sigEdge(d) * (0.4 + u_lamp * 0.5); col += crateColor * inkStroke(d) * (0.4 + u_lamp * 0.5);
// Lamp on a minority of crates, pulsing on the beat. Local, not // Lamp on a minority of crates, pulsing on the beat. Local, not
// whole-frame: a per-crate blink is not a flash. // whole-frame: a per-crate blink is not a flash.
@ -82,7 +83,7 @@ vec4 scene(vec2 uv, vec2 p) {
} }
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -15,6 +15,7 @@ export const cellDivide = {
name: 'Cell Divide', name: 'Cell Divide',
family: 'organic', family: 'organic',
kind: 'fragment', kind: 'fragment',
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'style'], traits: ['shape', 'camera', 'style'],
params: { params: {
@ -95,13 +96,13 @@ vec4 scene(vec2 uv, vec2 p) {
// Nucleus, in the track's signature form. // Nucleus, in the track's signature form.
if (u_nucleus > 0.01) { if (u_nucleus > 0.01) {
float size = u_nucleus * 0.35; float size = u_nucleus * 0.35;
float d = sigShape((p - nearestAt) / max(size, 1e-3)) * size; float d = castMain((p - nearestAt) / max(size, 1e-3)) * size;
col = mix(col, pal(1), smoothstep(0.008, -0.008, d) * 0.85); col = mix(col, pal(1), smoothstep(0.008, -0.008, d) * 0.85);
col += pal(2) * sigEdge(d) * (0.35 + u_glow * 0.4); col += pal(2) * inkStroke(d) * (0.35 + u_glow * 0.4);
} }
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -15,6 +15,7 @@ export const circuitBloom = {
kind: 'fragment', kind: 'fragment',
// Takes the track's surface grain, but lightly — this is drawn, not filmed. // Takes the track's surface grain, but lightly — this is drawn, not filmed.
texture: 0.4, texture: 0.4,
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'style'], traits: ['shape', 'camera', 'style'],
params: { params: {
@ -91,13 +92,17 @@ vec4 scene(vec2 uv, vec2 p) {
// Pads sit where both traces meet, stamped in the signature form. // Pads sit where both traces meet, stamped in the signature form.
if (horizontal > 0.5 && vertical > 0.5 && rnd2 > 1.0 - u_pads) { if (horizontal > 0.5 && vertical > 0.5 && rnd2 > 1.0 - u_pads) {
float d = sigShape(f / max(u_padSize, 1e-3)) * u_padSize; float d = castMain(f / max(u_padSize, 1e-3)) * u_padSize;
col += pal(2) * smoothstep(0.01, -0.01, d) * reach * 0.7; col += pal(2) * smoothstep(0.01, -0.01, d) * reach * 0.7;
col += pal(3) * sigEdge(d) * reach * (0.4 + u_pulse * 0.4); // Filled as well as stroked. At the bottom of the 'grown' range the pads
// were a hairline and nothing else, and the ink's stroke can be thinner
// than the edge it replaced — which left the frame empty at grown=0.25.
col = mix(col, pal(3), inkMask(d, uv) * reach * 0.6);
col += pal(3) * inkStroke(d) * reach * (0.4 + u_pulse * 0.4);
} }
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -9,6 +9,7 @@ export const classicWave = {
// Personality: see look/Personality.js. // Personality: see look/Personality.js.
// Smooth concentric colour: grain only mutes the ramp it is built on. // Smooth concentric colour: grain only mutes the ramp it is built on.
texture: 0, texture: 0,
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'style'], traits: ['shape', 'camera', 'style'],
params: { params: {
@ -38,7 +39,7 @@ vec4 scene(vec2 uv, vec2 p) {
p = sigCamera(p); p = sigCamera(p);
// The rings take the track's signature form: round tracks get circles, // The rings take the track's signature form: round tracks get circles,
// hexagonal tracks get hexagonal rings, and it costs one call. // hexagonal tracks get hexagonal rings, and it costs one call.
float d = sigShape(p) + 1.0; float d = castMain(p) + 1.0;
float angle = atan(p.y, p.x); float angle = atan(p.y, p.x);
float t = u_time * u_speed + u_seed; float t = u_time * u_speed + u_seed;
@ -66,7 +67,7 @@ vec4 scene(vec2 uv, vec2 p) {
col *= 0.6 + 0.4 * (1.0 - smoothstep(0.8, 1.8, d)); col *= 0.6 + 0.4 * (1.0 - smoothstep(0.8, 1.8, d));
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -9,6 +9,7 @@ export const curlFlow = {
family: 'flow', family: 'flow',
kind: 'fragment', kind: 'fragment',
// Personality: see look/Personality.js. // Personality: see look/Personality.js.
consumes: ['ink'],
traits: ['camera', 'space', 'style'], traits: ['camera', 'space', 'style'],
params: { params: {
@ -81,7 +82,7 @@ vec4 scene(vec2 uv, vec2 p) {
col *= 0.6 + 0.4 * exp(-dot(p, p) * 0.35); col *= 0.6 + 0.4 * exp(-dot(p, p) * 0.35);
col = sigAir(col, p, smoothstep(0.0, 1.7, length(p))); col = sigAir(col, p, smoothstep(0.0, 1.7, length(p)));
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -12,6 +12,7 @@ export const dataAisle = {
family: 'structural', family: 'structural',
kind: 'fragment', kind: 'fragment',
texture: 0.6, texture: 0.6,
consumes: ['ink'],
traits: ['camera', 'space', 'style'], traits: ['camera', 'space', 'style'],
params: { params: {
@ -62,7 +63,7 @@ vec3 wall(vec2 at, float side, float t, float lightAmount) {
float bright = mix(0.25, 1.0, busy); float bright = mix(0.25, 1.0, busy);
col += palRamp(0.55 + id * 0.35) * led * bright * lightAmount * 1.8; col += palRamp(0.55 + id * 0.35) * led * bright * lightAmount * 1.8;
col += pal(4) * sigEdge(seam - seamW) * (0.15 + u_sigLine * 0.9); col += pal(4) * inkStroke(seam - seamW) * (0.15 + u_sigLine * 0.9);
return col; return col;
} }
@ -106,7 +107,7 @@ vec4 scene(vec2 uv, vec2 p) {
col = mix(col, pal(1) * 0.12, sat(far * u_haze)); col = mix(col, pal(1) * 0.12, sat(far * u_haze));
col = sigAir(col, p, far); col = sigAir(col, p, far);
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -22,6 +22,7 @@ export const drosteFeedback = {
family: 'glitch', family: 'glitch',
kind: 'fragment', kind: 'fragment',
texture: 0.5, texture: 0.5,
consumes: ['ink'],
traits: ['camera', 'style'], traits: ['camera', 'style'],
params: { params: {
@ -54,7 +55,7 @@ vec4 scene(vec2 uv, vec2 p) {
vec3 col = pal(0) * 0.05; 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 += 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(4) * inkStroke(ring) * (0.3 + u_bloom * 0.9);
col += pal(3) * exp(-abs(d) * 9.0) * u_bloom * 0.35; col += pal(3) * exp(-abs(d) * 9.0) * u_bloom * 0.35;
// The loop: read the previous frame from a slightly LARGER, slightly turned // The loop: read the previous frame from a slightly LARGER, slightly turned
@ -77,7 +78,7 @@ vec4 scene(vec2 uv, vec2 p) {
// Vignette, which also stops the corner content being recycled forever. // Vignette, which also stops the corner content being recycled forever.
col *= 0.75 + 0.25 * exp(-dot(p, p) * 0.35); col *= 0.75 + 0.25 * exp(-dot(p, p) * 0.35);
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -13,6 +13,7 @@ export const dustChamber = {
name: 'Dust Chamber', name: 'Dust Chamber',
family: 'minimal', family: 'minimal',
kind: 'fragment', kind: 'fragment',
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'space', 'style'], traits: ['shape', 'camera', 'space', 'style'],
params: { params: {
@ -77,13 +78,13 @@ vec4 scene(vec2 uv, vec2 p) {
float lit = exp(-pow((at.x - axis) / max(halfWidth, 1e-3), 2.0) * 2.0); float lit = exp(-pow((at.x - axis) / max(halfWidth, 1e-3), 2.0) * 2.0);
float pulse = 0.55 + 0.45 * sin(t * 2.0 + s * 3.0); float pulse = 0.55 + 0.45 * sin(t * 2.0 + s * 3.0);
float m = sigForm(p, at, u_moteSize * (0.6 + hash11(s + 3.3))); float m = castForm(p, at, u_moteSize * (0.6 + hash11(s + 3.3)));
col += pal(3) * m * lit * pulse * (0.5 + beam); col += pal(3) * m * lit * pulse * (0.5 + beam);
} }
col = sigAir(col, p, smoothstep(0.0, 1.5, length(p))); col = sigAir(col, p, smoothstep(0.0, 1.5, length(p)));
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -18,7 +18,12 @@ export const eclipseField = {
kind: 'fragment', kind: 'fragment',
// Crisp line work: the track's surface grain would only fur the edges. // Crisp line work: the track's surface grain would only fur the edges.
texture: 0, texture: 0,
traits: ['shape', 'camera', 'space', 'style'], consumes: ['cast', 'ink'],
// No `style`: this scene opts out of surface grain (texture: 0) and its only
// other style evidence was sigEdge, which the migration replaced with the
// ink. Traits and artifacts are different layers, so taking the ink does not
// earn the trait back. See MIGRATION.md.
traits: ['shape', 'camera', 'space'],
params: { params: {
size: { type: 'float', range: [0.2, 0.85], default: 0.45, uniform: 'u_size' }, size: { type: 'float', range: [0.2, 0.85], default: 0.45, uniform: 'u_size' },
@ -47,7 +52,7 @@ vec4 scene(vec2 uv, vec2 p) {
vec2 discAt = vec2(0.0, sigHorizonY() * 0.25); vec2 discAt = vec2(0.0, sigHorizonY() * 0.25);
// Distance to the occluder's edge, in the track's form. // Distance to the occluder's edge, in the track's form.
float d = sigShape((p - discAt) / max(u_size, 1e-3)) * u_size; float d = castMain((p - discAt) / max(u_size, 1e-3)) * u_size;
// The source behind it. // The source behind it.
float toLight = length(p - lightAt); float toLight = length(p - lightAt);
@ -76,11 +81,11 @@ vec4 scene(vec2 uv, vec2 p) {
// The occluder: not pure black, so it reads as an object rather than a hole. // The occluder: not pure black, so it reads as an object rather than a hole.
col = mix(col, pal(0) * 0.12, smoothstep(0.004, -0.004, d)); col = mix(col, pal(0) * 0.12, smoothstep(0.004, -0.004, d));
col += pal(3) * sigEdge(d) * (0.4 + u_corona * 0.5); col += pal(3) * inkStroke(d) * (0.4 + u_corona * 0.5);
col = sigAir(col, p, smoothstep(0.0, 1.7, length(p))); col = sigAir(col, p, smoothstep(0.0, 1.7, length(p)));
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -12,6 +12,7 @@ export const flora = {
family: 'organic', family: 'organic',
kind: 'fragment', kind: 'fragment',
// Personality: see look/Personality.js. // Personality: see look/Personality.js.
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'style'], traits: ['shape', 'camera', 'style'],
params: { params: {
@ -48,7 +49,7 @@ float petal(vec2 p, vec2 node, vec2 dir, float len, float width) {
float across = dot(off, perp); float across = dot(off, perp);
float taper = 0.6 + 0.4 * sat(along / max(len, 1e-3)); float taper = 0.6 + 0.4 * sat(along / max(len, 1e-3));
vec2 q = vec2(along / max(len, 1e-3), across / max(width * taper, 1e-3)); vec2 q = vec2(along / max(len, 1e-3), across / max(width * taper, 1e-3));
float form = sigForm(q * 0.5, vec2(0.0), 0.5); float form = castForm(q * 0.5, vec2(0.0), 0.5);
float clip = smoothstep(0.0, -0.1, along) * smoothstep(len + 0.12, len * 0.72, along); float clip = smoothstep(0.0, -0.1, along) * smoothstep(len + 0.12, len * 0.72, along);
return form * clip; return form * clip;
} }
@ -105,13 +106,13 @@ vec4 scene(vec2 uv, vec2 p) {
for (int k = 0; k < 6; k++) { for (int k = 0; k < 6; k++) {
float a = 6.2831853 * (float(k) + 0.5) / 6.0 + s; float a = 6.2831853 * (float(k) + 0.5) / 6.0 + s;
vec2 off = vec2(cos(a), sin(a)) * u_leaf * (0.30 + 0.2 * beat); vec2 off = vec2(cos(a), sin(a)) * u_leaf * (0.30 + 0.2 * beat);
bloom += sigForm(p - vec2(tipX, tipY), off, u_leaf * 0.5); bloom += castForm(p - vec2(tipX, tipY), off, u_leaf * 0.5);
} }
col = mix(col, pal(2), sat(bloom) * u_bud * (0.6 + 0.4 * beat)); col = mix(col, pal(2), sat(bloom) * u_bud * (0.6 + 0.4 * beat));
} }
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -16,6 +16,7 @@ export const gateCorridor = {
kind: 'fragment', kind: 'fragment',
// Takes the track's surface grain, but lightly — this is drawn, not filmed. // Takes the track's surface grain, but lightly — this is drawn, not filmed.
texture: 0.4, texture: 0.4,
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'space', 'style'], traits: ['shape', 'camera', 'space', 'style'],
params: { params: {
@ -62,7 +63,7 @@ vec4 scene(vec2 uv, vec2 p) {
float phase = fract((fi / float(u_gates)) + t); float phase = fract((fi / float(u_gates)) + t);
float scale = u_aperture * exp(phase * 3.2) * 0.35; float scale = u_aperture * exp(phase * 3.2) * 0.35;
float d = abs(sigShape(q / max(scale, 1e-3)) * max(scale, 1e-3)); float d = abs(castMain(q / max(scale, 1e-3)) * max(scale, 1e-3));
// Near gates are drawn thicker and brighter: the only depth cue that // Near gates are drawn thicker and brighter: the only depth cue that
// matters once the geometry is right. // matters once the geometry is right.
@ -83,7 +84,7 @@ vec4 scene(vec2 uv, vec2 p) {
col = sigAir(col, p, 1.0 - smoothstep(0.0, 1.2, length(q))); col = sigAir(col, p, 1.0 - smoothstep(0.0, 1.2, length(q)));
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -17,6 +17,7 @@ export const girderLattice = {
kind: 'fragment', kind: 'fragment',
// Crisp line work: the track's surface grain would only fur the edges. // Crisp line work: the track's surface grain would only fur the edges.
texture: 0, texture: 0,
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'space', 'style'], traits: ['shape', 'camera', 'space', 'style'],
params: { params: {
@ -58,7 +59,7 @@ vec4 scene(vec2 uv, vec2 p) {
if (above <= 0.001) { if (above <= 0.001) {
col = sigAir(col, p, 1.0); col = sigAir(col, p, 1.0);
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
float best = 1e3; float best = 1e3;
@ -94,9 +95,9 @@ vec4 scene(vec2 uv, vec2 p) {
} }
if (u_plate > 0.004) { if (u_plate > 0.004) {
float pd = sigShape((p - vec2(halfW0, y0)) / u_plate) * u_plate; float pd = castMain((p - vec2(halfW0, y0)) / u_plate) * u_plate;
plateHit = min(plateHit, pd); plateHit = min(plateHit, pd);
pd = sigShape((p - vec2(-halfW0, y0)) / u_plate) * u_plate; pd = castMain((p - vec2(-halfW0, y0)) / u_plate) * u_plate;
plateHit = min(plateHit, pd); plateHit = min(plateHit, pd);
} }
@ -109,7 +110,7 @@ vec4 scene(vec2 uv, vec2 p) {
float steel = smoothstep(weight, weight * 0.3, best); float steel = smoothstep(weight, weight * 0.3, best);
col = mix(col, pal(2) * (0.3 + above * 0.5), steel); col = mix(col, pal(2) * (0.3 + above * 0.5), steel);
col += pal(3) * sigEdge(best - weight) * 0.4; col += pal(3) * inkStroke(best - weight) * 0.4;
// Joint plates. // Joint plates.
col = mix(col, pal(4) * 0.7, smoothstep(0.006, -0.006, plateHit)); col = mix(col, pal(4) * 0.7, smoothstep(0.006, -0.006, plateHit));

View File

@ -9,6 +9,7 @@ export const horizonLines = {
// Personality: see look/Personality.js. // Personality: see look/Personality.js.
// Crisp line work: the track's surface grain would only fur the edges. // Crisp line work: the track's surface grain would only fur the edges.
texture: 0, texture: 0,
consumes: ['ink'],
traits: ['space', 'camera', 'style'], traits: ['space', 'camera', 'style'],
params: { params: {
@ -80,7 +81,7 @@ vec4 scene(vec2 uv, vec2 p) {
col *= 0.55 + 0.45 * exp(-dot(p, p) * 0.5); col *= 0.55 + 0.45 * exp(-dot(p, p) * 0.5);
col = sigAir(col, p, smoothstep(0.0, 1.6, abs(p.y))); col = sigAir(col, p, smoothstep(0.0, 1.6, abs(p.y)));
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -13,6 +13,7 @@ export const inkBleed = {
name: 'Ink Bleed', name: 'Ink Bleed',
family: 'organic', family: 'organic',
kind: 'fragment', kind: 'fragment',
consumes: ['ink'],
traits: ['camera', 'space', 'style'], traits: ['camera', 'space', 'style'],
params: { params: {
@ -73,7 +74,7 @@ vec4 scene(vec2 uv, vec2 p) {
col = sigAir(col, p, smoothstep(0.0, 1.7, length(p))); col = sigAir(col, p, smoothstep(0.0, 1.7, length(p)));
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -13,6 +13,7 @@ export const isometricBlocks = {
family: 'geometric', family: 'geometric',
kind: 'fragment', kind: 'fragment',
texture: 0.6, texture: 0.6,
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'style'], traits: ['shape', 'camera', 'style'],
params: { params: {
@ -102,7 +103,7 @@ vec4 scene(vec2 uv, vec2 p) {
// squashed by the tilt — so every block in the field is the same // squashed by the tilt — so every block in the field is the same
// silhouette as every other subject in the video. // silhouette as every other subject in the video.
vec2 q = vec2(fx, (p.y - groundY - h) / tilt); vec2 q = vec2(fx, (p.y - groundY - h) / tilt);
float dTop = sigShape(q / (u_plan * cw)) * (u_plan * cw); float dTop = castMain(q / (u_plan * cw)) * (u_plan * cw);
float topMask = smoothstep(0.004, -0.004, dTop); float topMask = smoothstep(0.004, -0.004, dTop);
vec3 top = mix(pal(2), pal(3), sat(h / max(u_height, 1e-3))) * 0.8; vec3 top = mix(pal(2), pal(3), sat(h / max(u_height, 1e-3))) * 0.8;
@ -112,12 +113,12 @@ vec4 scene(vec2 uv, vec2 p) {
// a flash. // a flash.
float picked = step(0.75, hash12(cell * 3.17 + 5.0)); float picked = step(0.75, hash12(cell * 3.17 + 5.0));
col += pal(3) * topMask * picked * u_beat * 0.45; col += pal(3) * topMask * picked * u_beat * 0.45;
col += pal(4) * sigEdge(dTop) * (0.2 + u_light * 1.2) * step(0.0, dy); col += pal(4) * inkStroke(dTop) * (0.2 + u_light * 1.2) * step(0.0, dy);
} }
col *= 0.75 + 0.25 * exp(-dot(p, p) * 0.3); col *= 0.75 + 0.25 * exp(-dot(p, p) * 0.3);
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -8,6 +8,7 @@ export const kaleidoTunnel = {
// Personality: see look/Personality.js. // Personality: see look/Personality.js.
// Hard grid lines on flat colour: grain reads as dirt on the lens. // Hard grid lines on flat colour: grain reads as dirt on the lens.
texture: 0, texture: 0,
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'style'], traits: ['shape', 'camera', 'style'],
params: { params: {
@ -37,7 +38,7 @@ vec4 scene(vec2 uv, vec2 p) {
// Cross-section measured in the signature form: the tunnel mouth is the // Cross-section measured in the signature form: the tunnel mouth is the
// track's shape rather than a circle. // track's shape rather than a circle.
float radius = max(sigShape(p) + 1.0, 1e-4); float radius = max(castMain(p) + 1.0, 1e-4);
vec2 folded = kaleido(p, sides); vec2 folded = kaleido(p, sides);
float angle = atan(folded.y, folded.x); float angle = atan(folded.y, folded.x);
@ -65,7 +66,7 @@ vec4 scene(vec2 uv, vec2 p) {
col += pal(3) * (1.0 - fade) * u_glow * 0.6; col += pal(3) * (1.0 - fade) * u_glow * 0.6;
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -13,6 +13,7 @@ export const karmanStreet = {
family: 'flow', family: 'flow',
kind: 'fragment', kind: 'fragment',
texture: 0.7, texture: 0.7,
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'space', 'style'], traits: ['shape', 'camera', 'space', 'style'],
params: { params: {
@ -92,14 +93,14 @@ vec4 scene(vec2 uv, vec2 p) {
col += pal(2) * sat(-vort) * u_glow * 0.5; col += pal(2) * sat(-vort) * u_glow * 0.5;
// The obstacle: still, solid, with the stream piling up on its nose. // The obstacle: still, solid, with the stream piling up on its nose.
float d = sigShape((p - origin) / u_body) * u_body; float d = castMain((p - origin) / u_body) * u_body;
col = mix(col, pal(0) * 0.1, smoothstep(0.005, -0.005, d)); col = mix(col, pal(0) * 0.1, smoothstep(0.005, -0.005, d));
col += pal(4) * sigEdge(d) * (0.5 + u_glow * 0.5); col += pal(4) * inkStroke(d) * (0.5 + u_glow * 0.5);
col += pal(4) * exp(-abs(d) * 14.0) * smoothstep(0.1, -0.3, p.x - origin.x) * u_glow * 0.35; col += pal(4) * exp(-abs(d) * 14.0) * smoothstep(0.1, -0.3, p.x - origin.x) * u_glow * 0.35;
col = sigAir(col, p, smoothstep(0.0, 1.6, length(p - origin) * 0.7)); col = sigAir(col, p, smoothstep(0.0, 1.6, length(p - origin) * 0.7));
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -14,6 +14,7 @@ export const magnetLines = {
kind: 'fragment', kind: 'fragment',
// Line work. Grain furs it up, so take only a dusting. // Line work. Grain furs it up, so take only a dusting.
texture: 0.5, texture: 0.5,
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'style'], traits: ['shape', 'camera', 'style'],
params: { params: {
@ -77,16 +78,16 @@ vec4 scene(vec2 uv, vec2 p) {
// lint: fixed-cost — the same three poles // lint: fixed-cost — the same three poles
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
vec2 c = poleAt(i, t); vec2 c = poleAt(i, t);
float d = sigShape((p - c) / u_core) * u_core; float d = castMain((p - c) / u_core) * u_core;
vec3 tint = mod(float(i), 2.0) < 0.5 ? pal(3) : pal(4); vec3 tint = mod(float(i), 2.0) < 0.5 ? pal(3) : pal(4);
col = mix(col, tint * 0.25, smoothstep(0.004, -0.004, d)); col = mix(col, tint * 0.25, smoothstep(0.004, -0.004, d));
col += tint * sigEdge(d) * (0.5 + u_glow * 0.6); col += tint * inkStroke(d) * (0.5 + u_glow * 0.6);
col += tint * exp(-abs(d) * 12.0) * u_glow * 0.25; col += tint * exp(-abs(d) * 12.0) * u_glow * 0.25;
} }
col *= 0.7 + 0.3 * exp(-dot(p, p) * 0.3); col *= 0.7 + 0.3 * exp(-dot(p, p) * 0.3);
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -12,6 +12,7 @@ export const neonCity = {
family: 'structural', family: 'structural',
kind: 'fragment', kind: 'fragment',
// Personality: see look/Personality.js. // Personality: see look/Personality.js.
consumes: ['ink'],
traits: ['space', 'camera', 'style'], traits: ['space', 'camera', 'style'],
params: { params: {
@ -115,7 +116,7 @@ vec4 scene(vec2 uv, vec2 p) {
col = sigAir(col, p, smoothstep(0.0, 1.4, length(p))); col = sigAir(col, p, smoothstep(0.0, 1.4, length(p)));
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -15,6 +15,7 @@ export const pendulumTrace = {
kind: 'fragment', kind: 'fragment',
// Fine line work; grain only furs it up. // Fine line work; grain only furs it up.
texture: 0.25, texture: 0.25,
consumes: ['ink'],
traits: ['camera', 'style'], traits: ['camera', 'style'],
params: { params: {
@ -86,7 +87,7 @@ vec4 scene(vec2 uv, vec2 p) {
col = max(col, prev(uv) * u_persist); col = max(col, prev(uv) * u_persist);
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -10,6 +10,7 @@ export const pitchShatter = {
family: 'glitch', family: 'glitch',
kind: 'fragment', kind: 'fragment',
// Personality: see look/Personality.js. // Personality: see look/Personality.js.
consumes: ['ink'],
traits: ['camera', 'style'], traits: ['camera', 'style'],
params: { params: {
@ -68,7 +69,7 @@ vec4 scene(vec2 uv, vec2 p) {
col += pal(int(mod(slice, 5.0))) * (1.0 - sliceRand) * u_glow * 0.3 * sliceRand; col += pal(int(mod(slice, 5.0))) * (1.0 - sliceRand) * u_glow * 0.3 * sliceRand;
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -10,6 +10,7 @@ export const psychedelicDrift = {
family: 'glitch', family: 'glitch',
kind: 'fragment', kind: 'fragment',
// Personality: see look/Personality.js. // Personality: see look/Personality.js.
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'style'], traits: ['shape', 'camera', 'style'],
params: { params: {
@ -108,7 +109,7 @@ vec4 scene(vec2 uv, vec2 p) {
// characters includes the one every other scene is built from. // characters includes the one every other scene is built from.
if (kind == 0.0) d = sdStar(sp, size, 5.0 + floor(hash11(s * 1.2) * 3.0)); if (kind == 0.0) d = sdStar(sp, size, 5.0 + floor(hash11(s * 1.2) * 3.0));
else if (kind == 1.0) d = sdSmiley(sp, size, u_time, fi); else if (kind == 1.0) d = sdSmiley(sp, size, u_time, fi);
else d = sigShape(sp / max(size, 1e-3)) * size; else d = castMain(sp / max(size, 1e-3)) * size;
float hit = 0.0; float hit = 0.0;
for (int j = 0; j < 5; j++) { for (int j = 0; j < 5; j++) {
@ -130,7 +131,7 @@ vec4 scene(vec2 uv, vec2 p) {
} }
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -11,6 +11,7 @@ export const pylonGrid = {
// Personality: see look/Personality.js. // Personality: see look/Personality.js.
// Takes the track's surface grain, but lightly — this is drawn, not filmed. // Takes the track's surface grain, but lightly — this is drawn, not filmed.
texture: 0.4, texture: 0.4,
consumes: ['cast', 'ink'],
traits: ['shape', 'space'], traits: ['shape', 'space'],
params: { params: {
@ -72,7 +73,7 @@ vec4 scene(vec2 uv, vec2 p) {
// Crown stamped in the track's signature form. // Crown stamped in the track's signature form.
vec2 crown = vec2(cx, yTop); vec2 crown = vec2(cx, yTop);
float size = (0.06 + 0.2 * scale) * (1.0 + u_beat * 0.4); float size = (0.06 + 0.2 * scale) * (1.0 + u_beat * 0.4);
col += pal(3) * sigForm(p, crown, size) * inten * (0.6 + u_pulse); col += pal(3) * castForm(p, crown, size) * inten * (0.6 + u_pulse);
col += pal(int(mod(fr, 4.0))) * exp(-dot(p - crown, p - crown) * 60.0) * u_pulse * depthShade; col += pal(int(mod(fr, 4.0))) * exp(-dot(p - crown, p - crown) * 60.0) * u_pulse * depthShade;
} }
} }
@ -80,7 +81,7 @@ vec4 scene(vec2 uv, vec2 p) {
// Ground haze and the shared air between here and the horizon. // Ground haze and the shared air between here and the horizon.
col = sigAir(col, p, smoothstep(0.0, 1.6, abs(p.y - hy))); col = sigAir(col, p, smoothstep(0.0, 1.6, abs(p.y - hy)));
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -16,6 +16,7 @@ export const quasicrystal = {
kind: 'fragment', kind: 'fragment',
// Crisp line work: the track's surface grain would only fur the edges. // Crisp line work: the track's surface grain would only fur the edges.
texture: 0, texture: 0,
consumes: ['ink'],
traits: ['camera', 'style'], traits: ['camera', 'style'],
params: { params: {
@ -67,11 +68,11 @@ vec4 scene(vec2 uv, vec2 p) {
// Ridges: where the sum crosses zero, which is the aperiodic skeleton. // Ridges: where the sum crosses zero, which is the aperiodic skeleton.
float ridge = 1.0 - abs(shaped); float ridge = 1.0 - abs(shaped);
col += pal(4) * pow(sat(ridge), 6.0) * u_glow; col += pal(4) * pow(sat(ridge), 6.0) * u_glow;
col += pal(3) * sigEdge(shaped) * u_glow * 0.3; col += pal(3) * inkStroke(shaped) * u_glow * 0.3;
col *= 0.65 + 0.35 * exp(-dot(p, p) * 0.22); col *= 0.65 + 0.35 * exp(-dot(p, p) * 0.22);
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -13,6 +13,7 @@ export const saltFlat = {
name: 'Salt Flat', name: 'Salt Flat',
family: 'minimal', family: 'minimal',
kind: 'fragment', kind: 'fragment',
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'space', 'style'], traits: ['shape', 'camera', 'space', 'style'],
params: { params: {
@ -68,14 +69,14 @@ vec4 scene(vec2 uv, vec2 p) {
// The one object: small, on the horizon, in the track's form. // The one object: small, on the horizon, in the track's form.
if (u_monolith > 0.005) { if (u_monolith > 0.005) {
vec2 at = vec2(u_standing, horizon + u_monolith * 0.9); vec2 at = vec2(u_standing, horizon + u_monolith * 0.9);
float d = sigShape((p - at) / u_monolith) * u_monolith; float d = castMain((p - at) / u_monolith) * u_monolith;
col = mix(col, pal(0) * 0.12, smoothstep(0.006, -0.006, d)); col = mix(col, pal(0) * 0.12, smoothstep(0.006, -0.006, d));
col += pal(4) * sigEdge(d) * (0.3 + u_glowBand * 0.5); col += pal(4) * inkStroke(d) * (0.3 + u_glowBand * 0.5);
} }
col = sigAir(col, p, smoothstep(0.0, 1.4, abs(p.x))); col = sigAir(col, p, smoothstep(0.0, 1.4, abs(p.x)));
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -15,6 +15,7 @@ export const scaleMosaic = {
family: 'organic', family: 'organic',
kind: 'fragment', kind: 'fragment',
texture: 0.8, texture: 0.8,
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'style'], traits: ['shape', 'camera', 'style'],
params: { params: {
@ -62,7 +63,7 @@ vec4 scene(vec2 uv, vec2 p) {
// back to screen distance at the end. A scale is therefore as wide as its // back to screen distance at the end. A scale is therefore as wide as its
// slice of the turn and as tall as its ring — wider ones further out, which // slice of the turn and as tall as its ring — wider ones further out, which
// is what growing on a cone looks like. // is what growing on a cone looks like.
float d = sigShape(f / max(rad, 1e-3)) * max(rad, 1e-3) * u_growth * r; float d = castMain(f / max(rad, 1e-3)) * max(rad, 1e-3) * u_growth * r;
// Shade by lift, so the surface has a direction of light across it. // Shade by lift, so the surface has a direction of light across it.
float shade = 0.3 + 0.7 * sat(0.5 + lift * 1.4); float shade = 0.3 + 0.7 * sat(0.5 + lift * 1.4);
@ -72,7 +73,7 @@ vec4 scene(vec2 uv, vec2 p) {
col = mix(col, body * 0.6, smoothstep(0.004, -0.02, d)); col = mix(col, body * 0.6, smoothstep(0.004, -0.02, d));
// Rim light along every scale edge, in the track's line weight. // Rim light along every scale edge, in the track's line weight.
col += pal(4) * sigEdge(d) * (0.3 + u_sheen * 0.8); col += pal(4) * inkStroke(d) * (0.3 + u_sheen * 0.8);
// A specular sweep across the whole head, which is what makes hundreds of // A specular sweep across the whole head, which is what makes hundreds of
// separate tiles read as one surface. // separate tiles read as one surface.
@ -81,7 +82,7 @@ vec4 scene(vec2 uv, vec2 p) {
col *= 0.72 + 0.28 * exp(-r * r * 0.3); col *= 0.72 + 0.28 * exp(-r * r * 0.3);
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -10,6 +10,7 @@ export const scanTear = {
family: 'glitch', family: 'glitch',
kind: 'fragment', kind: 'fragment',
// Personality: see look/Personality.js. // Personality: see look/Personality.js.
consumes: ['ink'],
traits: ['style'], traits: ['style'],
params: { params: {
@ -73,7 +74,7 @@ vec4 scene(vec2 uv, vec2 p) {
col += pal(4) * torn * u_shift * 0.6; col += pal(4) * torn * u_shift * 0.6;
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -14,6 +14,7 @@ export const shojiGrid = {
kind: 'fragment', kind: 'fragment',
// Paper has a tooth, and this is the one scene that wants it. // Paper has a tooth, and this is the one scene that wants it.
texture: 1.0, texture: 1.0,
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'style'], traits: ['shape', 'camera', 'style'],
params: { params: {
@ -67,10 +68,10 @@ vec4 scene(vec2 uv, vec2 p) {
if (u_form > 0.01) { if (u_form > 0.01) {
float which = step(0.62, hash12(cell * 1.7 + 9.0)); float which = step(0.62, hash12(cell * 1.7 + 9.0));
vec2 at = vec2(f.x * 2.0 / float(u_cols), f.y * 2.0 / float(u_rows)); vec2 at = vec2(f.x * 2.0 / float(u_cols), f.y * 2.0 / float(u_rows));
float d = sigShape(at / u_form) * u_form; float d = castMain(at / u_form) * u_form;
col = mix(col, col * 0.35 + pal(4) * glowField * 0.35, col = mix(col, col * 0.35 + pal(4) * glowField * 0.35,
smoothstep(0.01, -0.01, d) * which); smoothstep(0.01, -0.01, d) * which);
col += pal(4) * sigEdge(d) * which * glowField * 0.7; col += pal(4) * inkStroke(d) * which * glowField * 0.7;
} }
// The frame. Opaque, in front of everything, and the thing that makes the // The frame. Opaque, in front of everything, and the thing that makes the
@ -78,10 +79,10 @@ vec4 scene(vec2 uv, vec2 p) {
float bar = min(0.5 - abs(f.x), 0.5 - abs(f.y)); float bar = min(0.5 - abs(f.x), 0.5 - abs(f.y));
float wood = smoothstep(u_stile, u_stile * 0.4, bar); float wood = smoothstep(u_stile, u_stile * 0.4, bar);
col = mix(col, pal(0) * 0.14, wood); col = mix(col, pal(0) * 0.14, wood);
col += pal(4) * sigEdge(bar - u_stile) * 0.12; col += pal(4) * inkStroke(bar - u_stile) * 0.12;
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -10,6 +10,7 @@ export const signalDecay = {
name: 'Signal Decay', name: 'Signal Decay',
family: 'glitch', family: 'glitch',
kind: 'fragment', kind: 'fragment',
consumes: ['ink'],
traits: ['camera', 'style'], traits: ['camera', 'style'],
params: { params: {
@ -101,7 +102,7 @@ vec4 scene(vec2 uv, vec2 p) {
col = max(col, prev(uv) * u_persist); col = max(col, prev(uv) * u_persist);
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -11,6 +11,7 @@ export const silkRibbon = {
// Personality: see look/Personality.js. // Personality: see look/Personality.js.
// A soft drape of light. Grain furs the one thing it is made of. // A soft drape of light. Grain furs the one thing it is made of.
texture: 0, texture: 0,
consumes: ['ink'],
traits: ['camera', 'style'], traits: ['camera', 'style'],
params: { params: {
@ -74,7 +75,7 @@ vec4 scene(vec2 uv, vec2 p) {
col = mix(col, pal(2), exp(-knot * knot * 30.0) * u_glow * 0.5); col = mix(col, pal(2), exp(-knot * knot * 30.0) * u_glow * 0.5);
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -14,6 +14,7 @@ export const slowOrb = {
// the one where speckle is most visible for being least justified. It keeps // the one where speckle is most visible for being least justified. It keeps
// its own `grain` param, which exists to stop large flat areas banding. // its own `grain` param, which exists to stop large flat areas banding.
texture: 0, texture: 0,
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'space', 'style'], traits: ['shape', 'camera', 'space', 'style'],
params: { params: {
@ -43,7 +44,7 @@ vec4 scene(vec2 uv, vec2 p) {
// The body is the track's signature form — this scene is one shape in an // The body is the track's signature form — this scene is one shape in an
// empty frame, so the shape had better be the track's. // empty frame, so the shape had better be the track's.
float wobble = fbm(q * 2.4 + t, 4) * u_wobble; float wobble = fbm(q * 2.4 + t, 4) * u_wobble;
float d = (sigShape(q / max(u_size, 1e-3)) * u_size) * (1.0 + wobble); float d = (castMain(q / max(u_size, 1e-3)) * u_size) * (1.0 + wobble);
// TRAIT style: the body's edge is drawn in the track's hand. The scene's // TRAIT style: the body's edge is drawn in the track's hand. The scene's
// u_softness says how soft this orb is; u_sigSoft says how soft this VIDEO // u_softness says how soft this orb is; u_sigSoft says how soft this VIDEO
@ -54,7 +55,7 @@ vec4 scene(vec2 uv, vec2 p) {
// A rim in the track's line weight, so a sharp-handed track gets a defined // A rim in the track's line weight, so a sharp-handed track gets a defined
// limb against the halo instead of only a softer gradient. // limb against the halo instead of only a softer gradient.
float rim = sigEdge(d) * u_sigLine; float rim = inkStroke(d) * u_sigLine;
vec3 col = pal(0) * 0.05; vec3 col = pal(0) * 0.05;
col = mix(col, pal(1), body * 0.85); col = mix(col, pal(1), body * 0.85);
@ -67,7 +68,7 @@ vec4 scene(vec2 uv, vec2 p) {
col += (hash12(uv * 640.0 + floor(u_frame)) - 0.5) * u_grain * 0.08; col += (hash12(uv * 640.0 + floor(u_frame)) - 0.5) * u_grain * 0.08;
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -12,6 +12,7 @@ export const spectrumSculpture = {
// Personality: see look/Personality.js. // Personality: see look/Personality.js.
// Takes the track's surface grain, but lightly — this is drawn, not filmed. // Takes the track's surface grain, but lightly — this is drawn, not filmed.
texture: 0.4, texture: 0.4,
consumes: ['ink'],
traits: ['shape', 'camera', 'style'], traits: ['shape', 'camera', 'style'],
params: { params: {
@ -65,8 +66,11 @@ vec4 scene(vec2 uv, vec2 p) {
float seg = 6.28318530718 / bars; float seg = 6.28318530718 / bars;
float angle = atan(p.y, p.x) + t * seg; float angle = atan(p.y, p.x) + t * seg;
// Radial extent measured in the track's form, so the sculpture is built on // Radial extent measured in the track's SIGNATURE form, not in the cast.
// the same outline as everything else in the video. // This is a metric, not a subject: the cast carries notches and a hollow,
// and an annulus used as a radius turns the sculpture inside out — it
// rendered pure black. A scene that reads a form as geometry rather than
// drawing it wants sigShape, and the migration should leave it alone.
float radius = sigShape(p) + 1.0; float radius = sigShape(p) + 1.0;
float index = floor((angle + 3.14159265) / seg); float index = floor((angle + 3.14159265) / seg);
float cellAngle = mod(angle + 3.14159265, seg) / seg; float cellAngle = mod(angle + 3.14159265, seg) / seg;
@ -91,10 +95,10 @@ vec4 scene(vec2 uv, vec2 p) {
col += palRamp(slot * 0.7 + 0.15) * inBar * shape * (0.6 + band); col += palRamp(slot * 0.7 + 0.15) * inBar * shape * (0.6 + band);
// Inner ring outline holds the composition together. // Inner ring outline holds the composition together.
col += pal(2) * sigEdge(radius - u_radius) * 0.35; col += pal(2) * inkStroke(radius - u_radius) * 0.35;
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -14,6 +14,7 @@ export const stairwellDescent = {
family: 'structural', family: 'structural',
kind: 'fragment', kind: 'fragment',
texture: 0.7, texture: 0.7,
consumes: ['ink'],
traits: ['camera', 'space', 'style'], traits: ['camera', 'space', 'style'],
params: { params: {
@ -82,7 +83,7 @@ vec4 scene(vec2 uv, vec2 p) {
col = mix(col, pal(1) * 0.3, sky * 0.5); col = mix(col, pal(1) * 0.3, sky * 0.5);
col = sigAir(col, p, sat(1.0 - fade)); col = sigAir(col, p, sat(1.0 - fade));
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -13,6 +13,7 @@ export const stormRift = {
name: 'Storm Rift', name: 'Storm Rift',
family: 'glitch', family: 'glitch',
kind: 'fragment', kind: 'fragment',
consumes: ['ink'],
traits: ['camera', 'space', 'style'], traits: ['camera', 'space', 'style'],
params: { params: {
@ -101,7 +102,7 @@ vec4 scene(vec2 uv, vec2 p) {
col = max(col, prev(uv) * u_afterglow); col = max(col, prev(uv) * u_afterglow);
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -13,6 +13,7 @@ export const suspensionSpan = {
family: 'structural', family: 'structural',
kind: 'fragment', kind: 'fragment',
texture: 0.6, texture: 0.6,
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'space', 'style'], traits: ['shape', 'camera', 'space', 'style'],
params: { params: {
@ -78,7 +79,7 @@ vec4 scene(vec2 uv, vec2 p) {
// Deck: one heavy line all the way across, which is what stops the cable // Deck: one heavy line all the way across, which is what stops the cable
// reading as a piece of string. // reading as a piece of string.
col += pal(4) * smoothstep(w * 2.5, w * 0.6, abs(p.y - deckY)) * (0.4 + u_glow * 0.5); col += pal(4) * smoothstep(w * 2.5, w * 0.6, abs(p.y - deckY)) * (0.4 + u_glow * 0.5);
col += pal(4) * sigEdge(abs(p.y - deckY) - w * 3.0) * 0.25; col += pal(4) * inkStroke(abs(p.y - deckY) - w * 3.0) * 0.25;
// Towers: a pair of legs and a crossbeam, with the signature form as the // Towers: a pair of legs and a crossbeam, with the signature form as the
// lamp on top — the only object in the frame that is not a line. // lamp on top — the only object in the frame that is not a line.
@ -87,17 +88,17 @@ vec4 scene(vec2 uv, vec2 p) {
col += palRamp(0.05) * smoothstep(w * 3.0, w * 1.2, dTower) * 0.7; col += palRamp(0.05) * smoothstep(w * 3.0, w * 1.2, dTower) * 0.7;
if (u_lamp > 0.004) { if (u_lamp > 0.004) {
float d = sigShape((vec2(x, p.y) - vec2(0.0, towerTop + u_lamp)) / u_lamp) * u_lamp; float d = castMain((vec2(x, p.y) - vec2(0.0, towerTop + u_lamp)) / u_lamp) * u_lamp;
col += pal(3) * smoothstep(0.004, -0.004, d) * (0.6 + u_glow); col += pal(3) * smoothstep(0.004, -0.004, d) * (0.6 + u_glow);
col += pal(3) * exp(-abs(d) * 20.0) * u_glow * 0.5; col += pal(3) * exp(-abs(d) * 20.0) * u_glow * 0.5;
col += pal(3) * sigEdge(d) * 0.6; col += pal(3) * inkStroke(d) * 0.6;
} }
// Every other bay is a little further off, so the line of the bridge has // Every other bay is a little further off, so the line of the bridge has
// some air in it rather than being a flat elevation drawing. // some air in it rather than being a flat elevation drawing.
col = sigAir(col, p, smoothstep(0.2, 1.4, abs(bay) * 0.7 + hash11(bayId) * 0.5)); col = sigAir(col, p, smoothstep(0.2, 1.4, abs(bay) * 0.7 + hash11(bayId) * 0.5));
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -14,6 +14,7 @@ export const tideRings = {
name: 'Tide Rings', name: 'Tide Rings',
family: 'organic', family: 'organic',
kind: 'fragment', kind: 'fragment',
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'space', 'style'], traits: ['shape', 'camera', 'space', 'style'],
params: { params: {
@ -52,7 +53,7 @@ vec4 scene(vec2 uv, vec2 p) {
); );
// Distance in the signature metric: round tracks get circular wavefronts. // Distance in the signature metric: round tracks get circular wavefronts.
float d = sigShape(p - src) + 1.0; float d = castMain(p - src) + 1.0;
float amp = exp(-d * u_decay); float amp = exp(-d * u_decay);
sum += sin(d * u_wavelength - t * 6.0 + fract(s) * 6.28) * amp; sum += sin(d * u_wavelength - t * 6.0 + fract(s) * 6.28) * amp;
energy += amp; energy += amp;
@ -71,7 +72,7 @@ vec4 scene(vec2 uv, vec2 p) {
col = sigAir(col, p, smoothstep(0.0, 1.7, length(p))); col = sigAir(col, p, smoothstep(0.0, 1.7, length(p)));
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -14,6 +14,7 @@ export const timeSmear = {
name: 'Time Smear', name: 'Time Smear',
family: 'glitch', family: 'glitch',
kind: 'fragment', kind: 'fragment',
consumes: ['ink'],
traits: ['camera', 'style'], traits: ['camera', 'style'],
params: { params: {
@ -75,7 +76,7 @@ vec4 scene(vec2 uv, vec2 p) {
col = mix(col, vec3(dot(col, vec3(0.299, 0.587, 0.114))), age * 0.35); col = mix(col, vec3(dot(col, vec3(0.299, 0.587, 0.114))), age * 0.35);
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -13,6 +13,7 @@ export const truchetFold = {
kind: 'fragment', kind: 'fragment',
// Takes the track's surface grain, but lightly — this is drawn, not filmed. // Takes the track's surface grain, but lightly — this is drawn, not filmed.
texture: 0.4, texture: 0.4,
consumes: ['ink'],
traits: ['camera', 'style'], traits: ['camera', 'style'],
params: { params: {
@ -64,11 +65,11 @@ vec4 scene(vec2 uv, vec2 p) {
vec3 col = pal(0) * 0.05; vec3 col = pal(0) * 0.05;
col += tint * line * 0.75; col += tint * line * 0.75;
col += tint * halo * u_glow * 0.35; col += tint * halo * u_glow * 0.35;
col += pal(4) * sigEdge(d) * u_glow * 0.25; col += pal(4) * inkStroke(d) * u_glow * 0.25;
col *= 0.65 + 0.35 * exp(-dot(p, p) * 0.25); col *= 0.65 + 0.35 * exp(-dot(p, p) * 0.25);
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -24,6 +24,7 @@ export const turingBloom = {
name: 'Turing Bloom', name: 'Turing Bloom',
family: 'organic', family: 'organic',
kind: 'fragment', kind: 'fragment',
consumes: ['ink'],
traits: ['camera', 'style'], traits: ['camera', 'style'],
params: { params: {
@ -117,7 +118,7 @@ vec4 scene(vec2 uv, vec2 p) {
col *= 0.75 + 0.25 * exp(-dot(p, p) * 0.2); col *= 0.75 + 0.25 * exp(-dot(p, p) * 0.2);
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -20,6 +20,7 @@ export const voronoiShatter = {
family: 'geometric', family: 'geometric',
kind: 'fragment', kind: 'fragment',
texture: 0.5, texture: 0.5,
consumes: ['ink'],
traits: ['camera', 'style'], traits: ['camera', 'style'],
params: { params: {
@ -84,11 +85,11 @@ vec4 scene(vec2 uv, vec2 p) {
// Seams: drawn in the track's hand, brightest where three plates meet. // Seams: drawn in the track's hand, brightest where three plates meet.
float seam = smoothstep(u_seam, u_seam * 0.15, border); float seam = smoothstep(u_seam, u_seam * 0.15, border);
col += pal(4) * seam * (0.4 + u_glow * 0.8); col += pal(4) * seam * (0.4 + u_glow * 0.8);
col += pal(3) * sigEdge(border - u_seam) * 0.4; col += pal(3) * inkStroke(border - u_seam) * 0.4;
col *= 0.75 + 0.25 * exp(-dot(p, p) * 0.25); col *= 0.75 + 0.25 * exp(-dot(p, p) * 0.25);
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -13,6 +13,7 @@ export const vortexDrift = {
name: 'Vortex Drift', name: 'Vortex Drift',
family: 'flow', family: 'flow',
kind: 'fragment', kind: 'fragment',
consumes: ['cast', 'ink'],
traits: ['shape', 'camera', 'style'], traits: ['shape', 'camera', 'style'],
params: { params: {
@ -64,14 +65,14 @@ vec4 scene(vec2 uv, vec2 p) {
// The eye itself, in the signature form. // The eye itself, in the signature form.
if (u_core > 0.004) { if (u_core > 0.004) {
float d = sigShape(q / u_core) * u_core; float d = castMain(q / u_core) * u_core;
col = mix(col, pal(0) * 0.05, smoothstep(0.004, -0.01, d)); col = mix(col, pal(0) * 0.05, smoothstep(0.004, -0.01, d));
col += pal(3) * sigEdge(d) * 0.8; col += pal(3) * inkStroke(d) * 0.8;
col += pal(4) * exp(-max(d, 0.0) * 18.0) * 0.35; col += pal(4) * exp(-max(d, 0.0) * 18.0) * 0.35;
} }
col += sigGrain(uv); col += sigGrain(uv);
return vec4(col, 1.0); return vec4(inkValue(col), 1.0);
} }
`, `,
}; };

View File

@ -103,43 +103,69 @@ const CONTRACT_UNIFORMS = new Set([
* cast in the hexagon video and be the one shot that looks filmed elsewhere. * cast in the hexagon video and be the one shot that looks filmed elsewhere.
* So the claim is machine-checked against the source rather than trusted. * So the claim is machine-checked against the source rather than trusted.
*/ */
// A stage that draws the song's CAST is expressing `shape` more completely than // A scene that draws the song's CAST is expressing `shape` more completely than
// sigShape ever did — the form is the subject rather than a hint applied to one // sigShape ever did — the form is the subject rather than a hint applied to one
// — so castMain/castChorus count as evidence. Likewise inkMask/inkValue are the // — so castMain/castChorus count as evidence.
// style trait carried out in full. See scenes/stage/README.md. //
// The INK does not work the same way and must not be listed under `style`. A
// trait is a property of the track that a scene may honour; an artifact is
// content it draws. Taking the ink says nothing about whether the scene
// responds to u_sigLine, and treating it as evidence let Eclipse Field claim a
// trait it had stopped honouring — which the runtime gate then caught.
// The same idea as TRAIT_EVIDENCE, for the identity artifacts: a scene that // The same idea as TRAIT_EVIDENCE, for the identity artifacts: a scene that
// declares it consumes the cast has to actually draw it. Without this, // declares it consumes the cast has to actually draw it. Without this,
// `consumes` is a comment, and the migration becomes unverifiable the moment it // `consumes` is a comment, and the migration becomes unverifiable the moment it
// is more than a handful of files. // is more than a handful of files.
const ARTIFACT_EVIDENCE = { const ARTIFACT_EVIDENCE = {
cast: /\bcast(Main|Chorus|SDF)\s*\(/, cast: /\bcast(Main|Chorus|SDF|Form)\s*\(/,
ink: /\bink(Mask|Value|Pattern)\s*\(/, ink: /\bink(Mask|Value|Pattern|Stroke)\s*\(/,
staging: /\b(stageNode|stageScale)\s*\(/, staging: /\b(stageNode|stageScale)\s*\(/,
}; };
const TRAIT_EVIDENCE = { const TRAIT_EVIDENCE = {
shape: /\b(sig(Shape|Form)|cast(Main|Chorus|SDF))\s*\(/, shape: /\b(sig(Shape|Form)|cast(Main|Chorus|SDF|Form))\s*\(/,
camera: /\bsigCamera\s*\(/, camera: /\bsigCamera\s*\(/,
space: /\b(sigHorizonY|sigAir)\s*\(|\bu_sig(Horizon|Depth|Wash)\b/, space: /\b(sigHorizonY|sigAir)\s*\(|\bu_sig(Horizon|Depth|Wash)\b/,
style: /\b(sigEdge|sigGrain|sigFolded|inkMask|inkValue|inkPattern)\s*\(|\bu_sig(Line|Soft|Texture|Fold)\b/, style: /\b(sigEdge|sigGrain|sigFolded)\s*\(|\bu_sig(Line|Soft|Texture|Fold)\b/,
}; };
// The shader preamble is a JS template literal, so a backtick anywhere inside // Every shader in this project lives inside a JS template literal, so a
// it silently closes the literal. Twice now that has produced a check page that // backtick anywhere in one silently closes it. Three times now that has cost a
// hangs on "starting…" with an empty console, which is an expensive way to find // debugging round: twice in the preamble, where it produced a check page that
// a typo. One grep is cheaper. // hung on "starting…" with an empty console, and once in a scene, where at
console.log('\nshader contract'); // least the module failed to parse loudly. A GLSL comment is the natural place
// to reach for backticks when quoting a param name, which is exactly why this
// keeps happening.
//
// Checked across the contract AND every scene, since the scene case is the one
// a mechanical pass over sixty files will keep reintroducing.
console.log('\nshader literals');
{ {
const src = readFileSync(join(SRC, 'engine/shader-contract.js'), 'utf8'); const targets = [join(SRC, 'engine/shader-contract.js'), ...walk(join(SRC, 'scenes'))];
const literal = src.slice(src.indexOf('export const PREAMBLE = `') + 25); let clean = 0;
const body = literal.slice(0, literal.indexOf('\n`;')); for (const file of targets) {
const stray = body.split('\n').filter((l) => l.includes('`') && !l.includes('${')); const src = readFileSync(file, 'utf8');
if (stray.length) { const rel = relative(SRC, file).replace(/\\/g, '/');
fail(`shader preamble contains a backtick, which closes the template literal:\n` + // Every template literal in the file, then the lines inside them that
stray.map((l) => ` ${l.trim()}`).join('\n')); // carry a backtick without being an interpolation.
} else { const stray = [];
ok('preamble has no stray backticks'); const rx = /`([\s\S]*?)`/g;
let m;
while ((m = rx.exec(src)) !== null) {
if (!/\bvec4 scene|precision highp|void main/.test(m[1])) continue;
for (const line of m[1].split('\n')) {
if (line.includes('`') && !line.includes('${')) stray.push(line);
} }
}
// A shader body that swallowed a closing backtick shows up as an
// unbalanced count across the file.
const ticks = (src.match(/`/g) || []).length;
if (stray.length || ticks % 2 !== 0) {
fail(`${rel}: backtick inside a shader literal — it closes the string` +
(stray.length ? `:\n${stray.map((l) => ` ${l.trim()}`).join('\n')}` : ''));
} else clean++;
}
if (clean === targets.length) ok(`${clean} shader literals balanced, no stray backticks`);
} }
console.log('\nscene schema lint'); console.log('\nscene schema lint');