Staging: a shared lattice, and the element size the songs were all sharing
The cast and ink slice lowered the floor as predicted but left `scale` — feature
size — consistently WORSE than the legacy arm, 0.033 against 0.053 and well
outside the noise. Every stage still chose its own element size from its own
param range, so every song landed in the same place: the videos shared a cast
and also, accidentally, shared how big everything was.
Staging is the third artifact. A lattice — grid, radial, spiral, scatter,
strata — with jitter, spread, a size hierarchy and a bias toward the middle or
the edges, transported the same way the cast is: uniforms plus a `stageNode`
function in the preamble, no new engine plumbing. Constellation, Swarm and
Procession place on it; the stage keeps its motion and gives up its composition.
Soloist takes only the scale, since a close-up has no composition to share.
`elementScale` is the part that mattered. It is the song's answer to "how big is
this made of", spanning about a factor of six, and it is the decision that was
missing rather than mis-set. Measured, over twelve songs and three runs:
stages spread +0.0111 ±0.0043 → +0.0143 ±0.0008
scale block 0.033 ±0.004 → 0.041 ±0.007
Against the legacy arm at +0.0108 ±0.0071 the harness still says
indistinguishable, and it is right to: the gap is +0.0035 and the legacy arm's
own run-to-run range is twice that. What can be said is narrower and holds up.
Stages have the lowest floor of the three arms by a clear margin — 0.082 against
0.095 and 0.104 — so sharing content does make a video look like itself, which
was the central prediction. And the targeted fix moved the block it was aimed at
in the direction it was aimed.
Also added: a lint check that the shader preamble contains no backticks. Twice
now one has closed the template literal and produced a check page that hangs on
"starting…" with an empty console, which is an expensive way to find a typo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
577ec107f6
commit
6e4106048b
@ -108,6 +108,7 @@ async function main() {
|
||||
const { lines, ok, headline } = await experimentReportLines({
|
||||
songs: Number(params.get('count')) || 6,
|
||||
probes: Number(params.get('probes')) || 4,
|
||||
repeats: Number(params.get('repeats')) || 3,
|
||||
});
|
||||
out.innerHTML = `<pre class="scene-gate">${lines.join('\n')}</pre>`;
|
||||
summaryEl.textContent = `${headline} · ${((Date.now() - started) / 1000).toFixed(1)}s`;
|
||||
|
||||
@ -209,7 +209,7 @@ export async function songVarietyReportLines({ songs = 6, probes = 5 } = {}) {
|
||||
* their own content. Three arms, because two would not distinguish "stages are
|
||||
* better" from "a small pool is better".
|
||||
*/
|
||||
export async function experimentReportLines({ songs = 6, probes = 4 } = {}) {
|
||||
export async function experimentReportLines({ songs = 6, probes = 4, repeats = 3 } = {}) {
|
||||
const { scenes } = await import('../../scenes/registry.js');
|
||||
const byName = (n) => scenes.find((m) => m.name === n);
|
||||
|
||||
@ -239,38 +239,59 @@ export async function experimentReportLines({ songs = 6, probes = 4 } = {}) {
|
||||
lines.push(' Higher observed = two songs that look like different work.');
|
||||
lines.push('');
|
||||
|
||||
// Repeats, with error bars. The first run of this comparison used seven
|
||||
// songs and put stages 14% ahead; at twelve songs the ordering flipped. A
|
||||
// difference that changes sign with the sample is a difference that has to
|
||||
// be reported with its spread or not at all.
|
||||
const results = [];
|
||||
for (const arm of arms) {
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
const r = measureSongVariety({ songs, probes, pool: arm.pool });
|
||||
results.push({ arm, r });
|
||||
const runs = [];
|
||||
for (let k = 0; k < repeats; k++) {
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
runs.push(measureSongVariety({
|
||||
songs, probes, pool: arm.pool, seedSalt: k * 7919,
|
||||
}));
|
||||
}
|
||||
results.push({ arm, runs, r: runs[0] });
|
||||
}
|
||||
|
||||
lines.push(' arm floor observed spread ratio');
|
||||
lines.push(' ' + '-'.repeat(78));
|
||||
for (const { arm, r } of results) {
|
||||
const spread = r.observed - r.floor;
|
||||
lines.push(` ${arm.label.padEnd(44)}${r.floor.toFixed(4)} ${r.observed.toFixed(4)}` +
|
||||
` ${spread >= 0 ? '+' : ''}${spread.toFixed(4)} ${(r.observed / r.floor).toFixed(3)}`);
|
||||
const mean = (a) => a.reduce((x, y) => x + y, 0) / a.length;
|
||||
const half = (a) => (Math.max(...a) - Math.min(...a)) / 2;
|
||||
|
||||
lines.push(` arm floor observed spread (${repeats} runs)`);
|
||||
lines.push(' ' + '-'.repeat(80));
|
||||
for (const { arm, runs } of results) {
|
||||
const spreads = runs.map((x) => x.observed - x.floor);
|
||||
lines.push(` ${arm.label.padEnd(44)}${mean(runs.map((x) => x.floor)).toFixed(4)}` +
|
||||
` ${mean(runs.map((x) => x.observed)).toFixed(4)}` +
|
||||
` ${mean(spreads) >= 0 ? '+' : ''}${mean(spreads).toFixed(4)} ±${half(spreads).toFixed(4)}`);
|
||||
}
|
||||
lines.push('');
|
||||
|
||||
for (const { arm, r } of results) {
|
||||
const gap = mean(results[0].runs.map((x) => x.observed - x.floor))
|
||||
- mean(results[1].runs.map((x) => x.observed - x.floor));
|
||||
const noise = Math.max(
|
||||
half(results[0].runs.map((x) => x.observed - x.floor)),
|
||||
half(results[1].runs.map((x) => x.observed - x.floor)));
|
||||
lines.push(` stages minus legacy: ${gap >= 0 ? '+' : ''}${gap.toFixed(4)} against a noise band of ±${noise.toFixed(4)}`);
|
||||
lines.push(` → ${Math.abs(gap) > noise * 2 ? 'a real difference' : 'INDISTINGUISHABLE at this sample size'}`);
|
||||
lines.push('');
|
||||
|
||||
for (const { arm, runs, r } of results) {
|
||||
lines.push(` ${arm.label}`);
|
||||
for (const [name, b] of Object.entries(r.byBlock)) {
|
||||
lines.push(` ${name.padEnd(8)} between ${b.between.toFixed(3)}`);
|
||||
for (const [name] of Object.entries(r.byBlock)) {
|
||||
const v = runs.map((x) => x.byBlock[name].between);
|
||||
lines.push(` ${name.padEnd(8)} between ${mean(v).toFixed(3)} ±${half(v).toFixed(3)}`);
|
||||
}
|
||||
lines.push(` coupling ${r.coupling.toFixed(2)}`);
|
||||
const worst = r.pairs[0];
|
||||
lines.push(` closest pair ${worst.a} ≈ ${worst.b} at ${worst.total.toFixed(3)}`);
|
||||
const c = runs.map((x) => x.coupling);
|
||||
lines.push(` coupling ${mean(c).toFixed(2)} ±${half(c).toFixed(2)}`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
const stages = results[0].r, legacy = results[1].r;
|
||||
const ok = (stages.observed - stages.floor) > (legacy.observed - legacy.floor);
|
||||
const headline = `stages spread ${(stages.observed - stages.floor).toFixed(4)} ` +
|
||||
`vs legacy ${(legacy.observed - legacy.floor).toFixed(4)} — ` +
|
||||
(ok ? 'the inversion helps' : 'no improvement');
|
||||
const ok = gap > noise * 2;
|
||||
const headline = `stages ${gap >= 0 ? '+' : ''}${gap.toFixed(4)} vs legacy, noise ±${noise.toFixed(4)} — ` +
|
||||
(Math.abs(gap) > noise * 2 ? (gap > 0 ? 'the inversion helps' : 'the inversion hurts')
|
||||
: 'indistinguishable');
|
||||
return { lines, ok, headline };
|
||||
}
|
||||
|
||||
|
||||
@ -121,6 +121,13 @@ export const IDENTITY_UNIFORMS = {
|
||||
u_inkHatchScale: 'float',
|
||||
u_inkOutline: 'float', // 0..1 outline strength on top of the fill
|
||||
u_inkPosterize: 'float', // 0 = off, else levels
|
||||
|
||||
u_latKind: 'float', // index into Identity.LATTICES
|
||||
u_latJitter: 'float', // how far off the lattice things sit
|
||||
u_latSpread: 'float', // how much of the frame it occupies
|
||||
u_latScaleSpread: 'float', // 0 = all one size, 1 = a few large, many small
|
||||
u_latScaleBias: 'float', // + puts the large ones in the middle
|
||||
u_latScale: 'float', // the song's element size, ~0.1 tiny .. ~0.9 huge
|
||||
};
|
||||
|
||||
export const FRAME_UNIFORMS = [
|
||||
@ -350,6 +357,61 @@ float castChorus(vec2 q) {
|
||||
u_chorusNotchN, u_chorusNotchD, u_chorusHollow);
|
||||
}
|
||||
|
||||
// --- the staging -----------------------------------------------------------
|
||||
// Where things go. Shared, so two stages in one video agree about composition —
|
||||
// and so the SIZE HIERARCHY is a decision the song makes once rather than one
|
||||
// each stage makes for itself. The first four stages all placed similarly-sized
|
||||
// elements, which left feature scale out of the measurement entirely.
|
||||
|
||||
/** Node i of n on the song's lattice: xy position, z scale multiplier. */
|
||||
vec3 stageNode(float i, float n) {
|
||||
vec2 h = hash22(vec2(i * 1.37 + 3.1, i * 0.71 + 7.7));
|
||||
float total = max(n, 1.0);
|
||||
vec2 pos;
|
||||
|
||||
if (u_latKind < 0.5) { // grid
|
||||
float cols = max(1.0, floor(sqrt(total) + 0.5));
|
||||
float rows = max(1.0, ceil(total / cols));
|
||||
pos = vec2((mod(i, cols) / max(cols - 1.0, 1.0) - 0.5) * 2.0,
|
||||
(floor(i / cols) / max(rows - 1.0, 1.0) - 0.5) * 2.0);
|
||||
} else if (u_latKind < 1.5) { // radial rings
|
||||
float rings = max(1.0, floor(sqrt(total * 0.5) + 0.5));
|
||||
float ring = mod(i, rings) + 1.0;
|
||||
float a = (i / total) * 6.28318530718 * 3.0;
|
||||
pos = vec2(cos(a), sin(a)) * (ring / rings);
|
||||
} else if (u_latKind < 2.5) { // spiral, golden angle
|
||||
float a = i * 2.39996323;
|
||||
pos = vec2(cos(a), sin(a)) * sqrt(i / total);
|
||||
} else if (u_latKind < 3.5) { // scatter
|
||||
pos = (h - 0.5) * 2.0;
|
||||
} else { // strata
|
||||
float rows = max(1.0, floor(total / 4.0 + 0.5));
|
||||
pos = vec2((h.x - 0.5) * 2.0,
|
||||
(mod(i, rows) / max(rows - 1.0, 1.0) - 0.5) * 2.0);
|
||||
}
|
||||
|
||||
pos += (h - 0.5) * u_latJitter;
|
||||
pos *= u_latSpread;
|
||||
// Sits on the same ground every other scene in the track sits on.
|
||||
pos.y += sigHorizonY() * 0.3;
|
||||
|
||||
// A power law when the song wants a hierarchy, near-uniform when it does
|
||||
// not. Biased toward the middle or the edges.
|
||||
float u = max(hash11(i * 7.13 + 1.7), 0.001);
|
||||
float size = mix(1.0, pow(u, 1.0 + u_latScaleSpread * 2.5) * 2.4, u_latScaleSpread);
|
||||
size *= 1.0 + u_latScaleBias * (0.5 - length(pos) * 0.5);
|
||||
|
||||
// The song's own element size, relative to the neutral 0.35. A stage
|
||||
// multiplies its own size param by this rather than choosing outright, so
|
||||
// one song is made of a few huge forms and another of many small ones.
|
||||
size *= u_latScale / 0.35;
|
||||
|
||||
return vec3(pos, max(size, 0.05));
|
||||
}
|
||||
|
||||
/** The song's element size as a multiplier a stage applies to its own size. */
|
||||
float stageScale() { return u_latScale / 0.35; }
|
||||
|
||||
// --- the ink ---------------------------------------------------------------
|
||||
// How the cast is drawn. Changes every pixel of every stage at once, and does
|
||||
// it structurally rather than chromatically — which is the point, since colour
|
||||
|
||||
@ -29,6 +29,9 @@
|
||||
/** Fill treatments, as the shader's `u_inkFill` index. */
|
||||
export const FILLS = ['flat', 'ramp', 'hatch', 'stipple', 'halftone', 'hollow'];
|
||||
|
||||
/** Lattices, as the shader's `u_latKind` index. */
|
||||
export const LATTICES = ['grid', 'radial', 'spiral', 'scatter', 'strata'];
|
||||
|
||||
const clamp01 = (x) => Math.max(0, Math.min(1, x));
|
||||
|
||||
/**
|
||||
@ -110,7 +113,47 @@ export function generateIdentity(summary, rng, sections = 4) {
|
||||
posterize: rng.bool(0.3) ? rng.int(3, 6) : 0,
|
||||
};
|
||||
|
||||
return { cast: { protagonist, chorus }, ink, character: { angular, intricate, solid } };
|
||||
// STAGING: where things go, and how their sizes are distributed.
|
||||
//
|
||||
// The first four stages separated songs almost entirely on layout while
|
||||
// their feature SCALE collapsed — every stage placed similarly-sized
|
||||
// elements, so size stopped being a variable at all. A shared lattice fixes
|
||||
// both halves: the stages agree with each other about placement, which is
|
||||
// what makes a video look like itself, and the size hierarchy becomes a
|
||||
// decision the song makes rather than one each stage makes for itself.
|
||||
const lattice = {
|
||||
kind: rng.pickWeighted(LATTICES, [
|
||||
1 + angular * 3, // grid
|
||||
1 + (1 - angular) * 2, // radial
|
||||
1 + (1 - angular) * 2, // spiral
|
||||
2, // scatter
|
||||
1 + angular * 2, // strata
|
||||
]),
|
||||
jitter: clamp01(rng.range(0, 0.5) + (1 - angular) * 0.3),
|
||||
spread: rng.range(0.5, 0.85) + dynamic * 0.35,
|
||||
// A few large and many small, or all one size. Busy material earns the
|
||||
// hierarchy; a sparse track wants its elements to be equals.
|
||||
scaleSpread: clamp01(0.15 + intricate * 0.6 + rng.range(-0.2, 0.25)),
|
||||
// Whether the big ones sit in the middle or around the edges.
|
||||
scaleBias: rng.range(-1, 1),
|
||||
// How big the song's elements are AT ALL — a per-song decision rather
|
||||
// than a per-stage one.
|
||||
//
|
||||
// This is the block the measurements kept pointing at. Sharing a cast
|
||||
// lowered the floor as predicted, but `scale` — feature size — came out
|
||||
// consistently WORSE than the legacy arm (0.033 against 0.053, well
|
||||
// outside the noise), because every stage still chose its own element
|
||||
// size from its own param range and every song therefore landed in the
|
||||
// same place. A song made of six huge forms and a song made of four
|
||||
// hundred tiny ones are different videos before anything else is
|
||||
// decided; that decision belongs here.
|
||||
elementScale: 0.35 * 2 ** rng.range(-1.4, 1.4) * (1.25 - intricate * 0.5),
|
||||
};
|
||||
|
||||
return {
|
||||
cast: { protagonist, chorus }, ink, lattice,
|
||||
character: { angular, intricate, solid },
|
||||
};
|
||||
}
|
||||
|
||||
/** Neutral values, so a layer built without an identity renders as it always did. */
|
||||
@ -121,6 +164,8 @@ export const NEUTRAL_IDENTITY_UNIFORMS = {
|
||||
u_chorusNotchN: 0, u_chorusNotchD: 0, u_chorusHollow: 0,
|
||||
u_inkWeight: 0.3, u_inkEdge: 0.5, u_inkFill: 0, u_inkHatchAngle: 0,
|
||||
u_inkHatchScale: 80, u_inkOutline: 0, u_inkPosterize: 0,
|
||||
u_latKind: 3, u_latJitter: 0.5, u_latSpread: 0.9,
|
||||
u_latScaleSpread: 0.3, u_latScaleBias: 0, u_latScale: 0.35,
|
||||
};
|
||||
|
||||
/**
|
||||
@ -162,6 +207,13 @@ export function identityUniforms(identity, shape = null) {
|
||||
u_inkFill: FILLS.indexOf(ink.fill),
|
||||
u_inkHatchAngle: ink.hatchAngle, u_inkHatchScale: ink.hatchScale,
|
||||
u_inkOutline: ink.outline, u_inkPosterize: ink.posterize,
|
||||
|
||||
u_latKind: LATTICES.indexOf(identity.lattice.kind),
|
||||
u_latJitter: identity.lattice.jitter,
|
||||
u_latSpread: identity.lattice.spread,
|
||||
u_latScaleSpread: identity.lattice.scaleSpread,
|
||||
u_latScaleBias: identity.lattice.scaleBias,
|
||||
u_latScale: identity.lattice.elementScale,
|
||||
};
|
||||
}
|
||||
|
||||
@ -175,5 +227,7 @@ export function describeIdentity(identity) {
|
||||
const ink = identity.ink;
|
||||
return `cast ${form(a)} + ${form(b)} · ink ${ink.fill}` +
|
||||
`${ink.outline ? '+outline' : ''}${ink.posterize ? `/${ink.posterize}-tone` : ''}` +
|
||||
` w${ink.weight.toFixed(2)}`;
|
||||
` w${ink.weight.toFixed(2)} · on ${identity.lattice.kind}` +
|
||||
` at ${identity.lattice.elementScale < 0.2 ? 'tiny' :
|
||||
identity.lattice.elementScale > 0.6 ? 'huge' : 'mid'} scale`;
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ export const constellation = {
|
||||
name: 'Constellation',
|
||||
family: 'minimal',
|
||||
kind: 'fragment',
|
||||
consumes: ['cast', 'ink'],
|
||||
consumes: ['cast', 'ink', 'staging'],
|
||||
texture: 0.6,
|
||||
traits: ['shape', 'camera', 'space', 'style'],
|
||||
|
||||
@ -42,13 +42,15 @@ vec4 scene(vec2 uv, vec2 p) {
|
||||
float fi = float(i);
|
||||
vec2 h = hash22(vec2(fi + u_seed, fi * 1.7));
|
||||
|
||||
// A slow wander around a fixed home, so the constellation keeps its
|
||||
// shape while nothing in it is ever quite still.
|
||||
vec2 home = (h - 0.5) * 2.0 * u_spread;
|
||||
vec2 pos = home + vec2(sin(t + h.x * 6.28), cos(t * 0.83 + h.y * 6.28)) * 0.08;
|
||||
// Homes come from the song's lattice — the stage owns the wander, not
|
||||
// the composition. A slow drift around a fixed home, so the
|
||||
// constellation keeps its shape while nothing in it is ever quite still.
|
||||
vec3 node = stageNode(fi, float(u_count));
|
||||
vec2 pos = node.xy * u_spread
|
||||
+ vec2(sin(t + h.x * 6.28), cos(t * 0.83 + h.y * 6.28)) * 0.08;
|
||||
|
||||
float pulse = 0.7 + 0.3 * sin(t * 2.0 + fi) * u_twinkle;
|
||||
float size = u_size * (0.5 + h.x) * pulse;
|
||||
float size = u_size * node.z * pulse;
|
||||
|
||||
vec2 q = (p - pos) / max(size, 1e-3);
|
||||
float d = castChorus(q) * size;
|
||||
|
||||
@ -9,12 +9,12 @@ export const procession = {
|
||||
name: 'Procession',
|
||||
family: 'structural',
|
||||
kind: 'fragment',
|
||||
consumes: ['cast', 'ink'],
|
||||
consumes: ['cast', 'ink', 'staging'],
|
||||
texture: 0.4,
|
||||
traits: ['shape', 'camera', 'style'],
|
||||
|
||||
params: {
|
||||
columns: { type: 'int', range: [2, 9], default: 4, uniform: 'u_columns', bias: 'density' },
|
||||
columns: { type: 'int', range: [2, 8], default: 4, uniform: 'u_columns', bias: 'density' },
|
||||
depth: { type: 'int', range: [2, 8], default: 4, uniform: 'u_depth', bias: 'density' },
|
||||
march: { type: 'float', range: [0.05, 1.2],default: 0.3, uniform: 'u_march', bias: 'motion', rate: true },
|
||||
size: { type: 'float', range: [0.1, 0.5], default: 0.28,uniform: 'u_size' },
|
||||
@ -39,30 +39,29 @@ vec4 scene(vec2 uv, vec2 p) {
|
||||
// Rows recede toward the horizon the track shares with every other scene.
|
||||
float horizon = sigHorizonY();
|
||||
|
||||
for (int row = 0; row < 8; row++) {
|
||||
if (row >= u_depth) break;
|
||||
float fr = float(row);
|
||||
// Rows further back are smaller and closer to the horizon.
|
||||
float back = fr / max(float(u_depth), 1.0);
|
||||
float scale = mix(1.0, 0.35, back * u_recede);
|
||||
float y = mix(horizon - 0.9, horizon + 0.15, back);
|
||||
// The formation is the song's lattice; the march is this stage's own idea
|
||||
// of what to do with it. That split is the whole point — identity owns
|
||||
// placement, the stage owns motion.
|
||||
float total = min(float(u_columns * u_depth), 64.0);
|
||||
|
||||
for (int c = 0; c < 9; c++) {
|
||||
if (c >= u_columns) break;
|
||||
float fc = float(c);
|
||||
float lane = (fc / max(float(u_columns) - 1.0, 1.0) - 0.5) * 2.4;
|
||||
for (int i = 0; i < 64; i++) {
|
||||
float fi = float(i);
|
||||
if (fi >= total) break;
|
||||
|
||||
// The march: each row slides at its own pace, wrapping.
|
||||
float phase = fract(t * (0.4 + back * 0.6) + fr * u_stagger + fc * 0.017);
|
||||
float x = lane + (phase - 0.5) * 0.6;
|
||||
vec3 node = stageNode(fi, total);
|
||||
// Depth from the node's own height in the frame, so recession agrees
|
||||
// with wherever the lattice put it rather than with a row index.
|
||||
float back = clamp((node.y - (horizon - 0.9)) / 1.05, 0.0, 1.0);
|
||||
float scale = mix(1.0, 0.35, back * u_recede) * node.z;
|
||||
|
||||
vec2 q = (p - vec2(x, y)) / max(u_size * scale, 1e-3);
|
||||
q = rot(t * u_spin + fr) * q;
|
||||
// The march: each element slides at a pace set by its depth, wrapping.
|
||||
float phase = fract(t * (0.4 + back * 0.6) + fi * u_stagger * 0.13);
|
||||
vec2 pos = node.xy + vec2((phase - 0.5) * 0.6, 0.0);
|
||||
|
||||
float d = castMain(q) * u_size * scale;
|
||||
float a = inkMask(d, uv);
|
||||
col = mix(col, pal(row + c + 2), a * (0.35 + 0.65 * (1.0 - back)));
|
||||
}
|
||||
vec2 q = rot(t * u_spin + fi) * (p - pos) / max(u_size * scale, 1e-3);
|
||||
|
||||
float d = castMain(q) * u_size * scale;
|
||||
col = mix(col, pal(i + 2), inkMask(d, uv) * (0.35 + 0.65 * (1.0 - back)));
|
||||
}
|
||||
|
||||
col += sigGrain(uv);
|
||||
|
||||
@ -8,7 +8,7 @@ export const soloist = {
|
||||
name: 'Soloist',
|
||||
family: 'minimal',
|
||||
kind: 'fragment',
|
||||
consumes: ['cast', 'ink'],
|
||||
consumes: ['cast', 'ink', 'staging'],
|
||||
texture: 0.5,
|
||||
traits: ['shape', 'camera', 'style'],
|
||||
|
||||
@ -32,6 +32,9 @@ vec4 scene(vec2 uv, vec2 p) {
|
||||
p = sigCamera(p);
|
||||
p = sigFolded(p);
|
||||
float t = u_time * u_turn + u_seed;
|
||||
// Even a close-up obeys the song's sense of scale — pulled toward it rather
|
||||
// than replaced by it, since a soloist that shrinks to a dot stops being one.
|
||||
float size0 = u_size * mix(1.0, stageScale(), 0.5);
|
||||
|
||||
vec3 col = mix(pal(0) * 0.14, pal(1) * 0.18, length(p) * 0.5);
|
||||
|
||||
@ -39,17 +42,17 @@ vec4 scene(vec2 uv, vec2 p) {
|
||||
for (int i = 6; i >= 1; i--) {
|
||||
if (i > u_echoes) continue;
|
||||
float fi = float(i);
|
||||
float size = u_size * (1.0 + fi * u_echoStep);
|
||||
float size = size0 * (1.0 + fi * u_echoStep);
|
||||
vec2 q = rot(t * (1.0 - fi * 0.15)) * (p - u_offset) / max(size, 1e-3);
|
||||
float d = castMain(q) * size;
|
||||
float a = inkMask(d, uv) * (0.5 / fi);
|
||||
col = mix(col, pal(i + 1), a);
|
||||
}
|
||||
|
||||
vec2 q = rot(t) * (p - u_offset) / max(u_size, 1e-3);
|
||||
float d = castMain(q) * u_size;
|
||||
vec2 q = rot(t) * (p - u_offset) / max(size0, 1e-3);
|
||||
float d = castMain(q) * size0;
|
||||
col = mix(col, pal(2), inkMask(d, uv));
|
||||
col += pal(3) * u_halo * (1.0 - smoothstep(0.0, u_size * 1.5, abs(d))) * 0.5;
|
||||
col += pal(3) * u_halo * (1.0 - smoothstep(0.0, size0 * 1.5, abs(d))) * 0.5;
|
||||
|
||||
col += sigGrain(uv);
|
||||
return vec4(inkValue(col), 1.0);
|
||||
|
||||
@ -8,7 +8,7 @@ export const swarm = {
|
||||
name: 'Swarm',
|
||||
family: 'organic',
|
||||
kind: 'fragment',
|
||||
consumes: ['cast', 'ink'],
|
||||
consumes: ['cast', 'ink', 'staging'],
|
||||
texture: 0.5,
|
||||
traits: ['shape', 'camera', 'style'],
|
||||
|
||||
@ -37,18 +37,17 @@ vec4 scene(vec2 uv, vec2 p) {
|
||||
for (int i = 0; i < 48; i++) {
|
||||
if (i >= u_count) break;
|
||||
float fi = float(i);
|
||||
vec2 h = hash22(vec2(fi + u_seed * 0.5, fi * 2.3));
|
||||
|
||||
// Each member rides the same flow field, which is what makes it a
|
||||
// swarm rather than a scatter — cohesion decides how strictly.
|
||||
vec2 seed = (h - 0.5) * 2.4;
|
||||
vec2 flow = curl(seed * u_field + t * 0.15, t * 0.1);
|
||||
vec2 pos = seed + flow * mix(0.05, 0.45, u_cohesion);
|
||||
// Each member has a home on the song's lattice and rides the same flow
|
||||
// field away from it, which is what makes it a swarm rather than a
|
||||
// scatter — cohesion decides how far it is allowed to stray.
|
||||
vec3 node = stageNode(fi, float(u_count));
|
||||
vec2 flow = curl(node.xy * u_field + t * 0.15, t * 0.1);
|
||||
vec2 pos = node.xy + flow * mix(0.05, 0.45, u_cohesion);
|
||||
pos += vec2(sin(t * 0.7 + fi), cos(t * 0.6 + fi * 1.3)) * 0.06;
|
||||
|
||||
vec2 q = (p - pos) / max(u_size, 1e-3);
|
||||
vec2 q = (p - pos) / max(u_size * node.z, 1e-3);
|
||||
q = rot(atan(flow.y, flow.x)) * q; // they face where they go
|
||||
float d = castChorus(q) * u_size;
|
||||
float d = castChorus(q) * u_size * node.z;
|
||||
|
||||
col = mix(col, pal(i + 1), inkMask(d, uv));
|
||||
col += pal(2) * u_trail * 0.004 / (0.02 + abs(d));
|
||||
|
||||
@ -101,6 +101,24 @@ const TRAIT_EVIDENCE = {
|
||||
style: /\b(sigEdge|sigGrain|sigFolded|inkMask|inkValue|inkPattern)\s*\(|\bu_sig(Line|Soft|Texture|Fold)\b/,
|
||||
};
|
||||
|
||||
// The shader preamble is a JS template literal, so a backtick anywhere inside
|
||||
// it silently closes the literal. Twice now that has produced a check page that
|
||||
// hangs on "starting…" with an empty console, which is an expensive way to find
|
||||
// a typo. One grep is cheaper.
|
||||
console.log('\nshader contract');
|
||||
{
|
||||
const src = readFileSync(join(SRC, 'engine/shader-contract.js'), 'utf8');
|
||||
const literal = src.slice(src.indexOf('export const PREAMBLE = `') + 25);
|
||||
const body = literal.slice(0, literal.indexOf('\n`;'));
|
||||
const stray = body.split('\n').filter((l) => l.includes('`') && !l.includes('${'));
|
||||
if (stray.length) {
|
||||
fail(`shader preamble contains a backtick, which closes the template literal:\n` +
|
||||
stray.map((l) => ` ${l.trim()}`).join('\n'));
|
||||
} else {
|
||||
ok('preamble has no stray backticks');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\nscene schema lint');
|
||||
{
|
||||
const { scenes, FAMILIES } = await import(pathToFileURL(join(SRC, 'scenes/registry.js')).href);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user