Make grain a treatment, tempo a governor, and params commit harder
Grain was in every video. It was added twice unconditionally — every scene called sigGrain, and the grade added its own on top — so the only thing that varied between two tracks was how much of it there was. That makes grain the renderer's fingerprint rather than a decision about one video. It is now described rather than dialled (look/grain.js): a mode (off / constant / swell / sections / transient), a cell size in pixels, a refresh rate in frames, a mask (uniform, shadows, highlights, edges, bands) and a chroma amount. Roughly 45% of tracks get none at all. The non-constant modes carry a per-frame envelope computed in Show._postAt from frame and features only, so preview and export still agree. Scene-side grain is gated the same way, and a module can decline it outright with `texture: 0` — crisp line work should stay crisp. The post tab grew a real grain block so any of it can be forced per track. Slow songs got fast scenes. `motion` bias was mostly section energy with tempo as a small correction, so a 70bpm track's drop asked for nearly as much speed as a 150bpm one. Motion is now tempo-dominated, and every `rate: true` param is additionally scaled by a per-track rateScale — measured, 84bpm now samples its rate params at 0.276 of range against 148bpm's 0.571. Parameter sampling also commits harder: extremity starts at 0.45 rather than 0.25 and shapes the draw more aggressively. This was first pushed to 0.82 and backed off to 0.72, because the gates caught the overshoot — seeds began collapsing onto the same range ends and a sparse scene sampled at its low end rendered effectively black. Fallout worth recording: turning the default grade grain off exposed two scenes that were never really animating. Dust Chamber and Eclipse Field passed the Phase 7 movement gate only because per-pixel noise was moving underneath them; both now breathe on their own fixed clock, and Dust Chamber needed a brightness floor as well. Four scenes (Classic Wave, Silk Ribbon, Kaleido Tunnel, Slow Orb) express the style trait ONLY through grain and so cannot opt out yet; they hold a reduced share at 0.35 pending real edge and softness response. Phase 3's look-space check now measures its closest pair relative to image brightness, the same correction Phase 10 already documents for sparse scenes: the absolute number was being propped up by grain rather than by look-space width. Five new Phase 10 checks cover grain distribution, treatment variety, envelope range, the texture opt-out, and tempo. 93/93 pass. Two transport bugs, both stale state surfacing in the UI: - Loading a track replaces audio.src, which stops playback silently, so state.playing stayed true and the play button stayed on pause — the first click after a track change only flipped the flag back. Added stopPlayback(). - The controls row wrapped mid-song because two readouts that change length while playing were sized by their content: the clock crossing ten minutes and the section label whenever a scene name is long or a crossfade appears. The clock is now fixed-width with tabular figures and the section label is the row's only flexible item, laying out at zero width and ellipsising into whatever space is left. The two spacers competed with it for that space and are gone; its own text-align does their job. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,8 @@ export const cargoBelt = {
|
||||
name: 'Cargo Belt',
|
||||
family: 'structural',
|
||||
kind: 'fragment',
|
||||
// Takes the track's surface grain, but lightly — this is drawn, not filmed.
|
||||
texture: 0.4,
|
||||
traits: ['shape', 'camera', 'style'],
|
||||
|
||||
params: {
|
||||
|
||||
@@ -13,6 +13,8 @@ export const circuitBloom = {
|
||||
name: 'Circuit Bloom',
|
||||
family: 'geometric',
|
||||
kind: 'fragment',
|
||||
// Takes the track's surface grain, but lightly — this is drawn, not filmed.
|
||||
texture: 0.4,
|
||||
traits: ['shape', 'camera', 'style'],
|
||||
|
||||
params: {
|
||||
|
||||
@@ -7,6 +7,9 @@ export const classicWave = {
|
||||
family: 'flow',
|
||||
kind: 'fragment',
|
||||
// Personality: see look/Personality.js.
|
||||
// Grain is this scene's ONLY expression of the style trait — it draws no
|
||||
// hard edges to weight — so it keeps a share of it rather than opting out.
|
||||
texture: 0.35,
|
||||
traits: ['shape', 'camera', 'style'],
|
||||
|
||||
params: {
|
||||
|
||||
@@ -44,13 +44,25 @@ vec4 scene(vec2 uv, vec2 p) {
|
||||
float inBeam = exp(-pow((p.x - axis) / max(halfWidth, 1e-3), 2.0) * 2.2);
|
||||
inBeam *= smoothstep(floorY - 0.05, floorY + 0.5, p.y);
|
||||
|
||||
vec3 col = pal(0) * 0.05;
|
||||
col += pal(1) * inBeam * u_beam * 0.5;
|
||||
// The light is the thing that moves here, and it has to move on its own
|
||||
// clock rather than on u_drift: at a low drift the motes barely breathe and
|
||||
// the whole frame measured as static. Fixed rate, so it is not a param and
|
||||
// cannot be reacted into a phase jump.
|
||||
float breath = 0.82 + 0.18 * sin(u_time * 0.4 + u_seed);
|
||||
// Floored, because the shaft IS the scene. An intro sampling u_beam near
|
||||
// its minimum with a narrow beam produced a frame dark enough to fail the
|
||||
// live-frame gate — a dim room is the point, an empty one is a bug.
|
||||
float beam = (0.22 + u_beam * 0.78) * breath;
|
||||
|
||||
// Ambient fill. It carries the whole frame wherever the shaft is not, so it
|
||||
// is what decides whether "almost black" stays on the right side of black.
|
||||
vec3 col = pal(0) * 0.11;
|
||||
col += pal(1) * inBeam * beam * 0.5;
|
||||
|
||||
// The pool where the shaft meets the floor.
|
||||
float pool = exp(-abs(p.y - floorY) * 14.0)
|
||||
* exp(-pow((p.x - axis) / max(halfWidth * 1.3, 1e-3), 2.0));
|
||||
col += pal(2) * pool * u_beam * 0.7;
|
||||
col += pal(2) * pool * beam * 0.7;
|
||||
|
||||
// Motes: fixed positions, breathing brightness, only visible in the light.
|
||||
for (int i = 0; i < 40; i++) {
|
||||
@@ -66,7 +78,7 @@ vec4 scene(vec2 uv, vec2 p) {
|
||||
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 m = sigForm(p, at, u_moteSize * (0.6 + hash11(s + 3.3)));
|
||||
col += pal(3) * m * lit * pulse * (0.5 + u_beam);
|
||||
col += pal(3) * m * lit * pulse * (0.5 + beam);
|
||||
}
|
||||
|
||||
col = sigAir(col, p, smoothstep(0.0, 1.5, length(p)));
|
||||
|
||||
@@ -16,6 +16,8 @@ export const eclipseField = {
|
||||
name: 'Eclipse Field',
|
||||
family: 'minimal',
|
||||
kind: 'fragment',
|
||||
// Crisp line work: the track's surface grain would only fur the edges.
|
||||
texture: 0,
|
||||
traits: ['shape', 'camera', 'space', 'style'],
|
||||
|
||||
params: {
|
||||
@@ -58,13 +60,17 @@ vec4 scene(vec2 uv, vec2 p) {
|
||||
// the disc it must be zero, or the occluder stops occluding.
|
||||
float outside = smoothstep(0.0, 0.02, d);
|
||||
float ring = exp(-max(d, 0.0) / max(u_corona * 0.25, 0.01));
|
||||
col += palRamp(0.35 + d * 0.5) * ring * outside * (0.8 + u_corona);
|
||||
// The corona breathes on a fixed clock rather than on u_drift. At a low
|
||||
// drift — which is most of this scene's range, it is a minimal scene —
|
||||
// nothing else in the frame moved enough to register as animation at all.
|
||||
float breath = 0.82 + 0.18 * sin(u_time * 0.5 + u_seed * 1.7);
|
||||
col += palRamp(0.35 + d * 0.5) * ring * outside * (0.8 + u_corona) * breath;
|
||||
|
||||
// Rays: angular streaks in the corona, rotating slowly. Confined to the
|
||||
// corona by the same falloff, so they never light the whole frame.
|
||||
if (u_rays > 0.01) {
|
||||
float ang = atan(p.y - discAt.y, p.x - discAt.x);
|
||||
float spokes = 0.5 + 0.5 * sin(ang * u_rayCount + t * 2.0);
|
||||
float spokes = 0.5 + 0.5 * sin(ang * u_rayCount + t * 2.0 + u_time * 0.3);
|
||||
col += pal(4) * pow(spokes, 3.0) * ring * outside * u_rays;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ export const floatingGeometry = {
|
||||
kind: 'fragment',
|
||||
// Personality: see look/Personality.js. This scene is nothing but a handful
|
||||
// of shapes, so `shape` is the trait it exists to express.
|
||||
// Takes the track's surface grain, but lightly — this is drawn, not filmed.
|
||||
texture: 0.4,
|
||||
traits: ['shape', 'camera', 'style'],
|
||||
|
||||
params: {
|
||||
|
||||
@@ -14,6 +14,8 @@ export const gateCorridor = {
|
||||
name: 'Gate Corridor',
|
||||
family: 'structural',
|
||||
kind: 'fragment',
|
||||
// Takes the track's surface grain, but lightly — this is drawn, not filmed.
|
||||
texture: 0.4,
|
||||
traits: ['shape', 'camera', 'space', 'style'],
|
||||
|
||||
params: {
|
||||
|
||||
@@ -15,6 +15,8 @@ export const girderLattice = {
|
||||
name: 'Girder Lattice',
|
||||
family: 'structural',
|
||||
kind: 'fragment',
|
||||
// Crisp line work: the track's surface grain would only fur the edges.
|
||||
texture: 0,
|
||||
traits: ['shape', 'camera', 'space', 'style'],
|
||||
|
||||
params: {
|
||||
|
||||
@@ -7,6 +7,8 @@ export const horizonLines = {
|
||||
family: 'minimal',
|
||||
kind: 'fragment',
|
||||
// Personality: see look/Personality.js.
|
||||
// Crisp line work: the track's surface grain would only fur the edges.
|
||||
texture: 0,
|
||||
traits: ['space', 'camera', 'style'],
|
||||
|
||||
params: {
|
||||
|
||||
@@ -6,6 +6,9 @@ export const kaleidoTunnel = {
|
||||
family: 'geometric',
|
||||
kind: 'fragment',
|
||||
// Personality: see look/Personality.js.
|
||||
// Grain is this scene's ONLY expression of the style trait — it draws no
|
||||
// hard edges to weight — so it keeps a share of it rather than opting out.
|
||||
texture: 0.35,
|
||||
traits: ['shape', 'camera', 'style'],
|
||||
|
||||
params: {
|
||||
|
||||
@@ -10,6 +10,8 @@ export const moireGrid = {
|
||||
family: 'geometric',
|
||||
kind: 'fragment',
|
||||
// Personality: see look/Personality.js.
|
||||
// Crisp line work: the track's surface grain would only fur the edges.
|
||||
texture: 0,
|
||||
traits: ['camera', 'style'],
|
||||
|
||||
params: {
|
||||
|
||||
@@ -8,6 +8,8 @@ export const prismBloom = {
|
||||
family: 'geometric',
|
||||
kind: 'fragment',
|
||||
// Personality: see look/Personality.js.
|
||||
// Crisp line work: the track's surface grain would only fur the edges.
|
||||
texture: 0,
|
||||
traits: ['shape', 'camera', 'style'],
|
||||
|
||||
params: {
|
||||
|
||||
@@ -9,6 +9,8 @@ export const pylonGrid = {
|
||||
family: 'structural',
|
||||
kind: 'fragment',
|
||||
// Personality: see look/Personality.js.
|
||||
// Takes the track's surface grain, but lightly — this is drawn, not filmed.
|
||||
texture: 0.4,
|
||||
traits: ['shape', 'space'],
|
||||
|
||||
params: {
|
||||
|
||||
@@ -14,6 +14,8 @@ export const quasicrystal = {
|
||||
name: 'Quasicrystal',
|
||||
family: 'geometric',
|
||||
kind: 'fragment',
|
||||
// Crisp line work: the track's surface grain would only fur the edges.
|
||||
texture: 0,
|
||||
traits: ['camera', 'style'],
|
||||
|
||||
params: {
|
||||
|
||||
@@ -9,6 +9,9 @@ export const silkRibbon = {
|
||||
family: 'minimal',
|
||||
kind: 'fragment',
|
||||
// Personality: see look/Personality.js.
|
||||
// Grain is this scene's ONLY expression of the style trait — it draws no
|
||||
// hard edges to weight — so it keeps a share of it rather than opting out.
|
||||
texture: 0.35,
|
||||
traits: ['camera', 'style'],
|
||||
|
||||
params: {
|
||||
|
||||
@@ -10,6 +10,9 @@ export const slowOrb = {
|
||||
family: 'minimal',
|
||||
kind: 'fragment',
|
||||
// Personality: see look/Personality.js.
|
||||
// Grain is this scene's ONLY expression of the style trait — it draws no
|
||||
// hard edges to weight — so it keeps a share of it rather than opting out.
|
||||
texture: 0.35,
|
||||
traits: ['shape', 'camera', 'space', 'style'],
|
||||
|
||||
params: {
|
||||
|
||||
@@ -10,6 +10,8 @@ export const spectrumSculpture = {
|
||||
family: 'minimal',
|
||||
kind: 'fragment',
|
||||
// Personality: see look/Personality.js.
|
||||
// Takes the track's surface grain, but lightly — this is drawn, not filmed.
|
||||
texture: 0.4,
|
||||
traits: ['shape', 'camera', 'style'],
|
||||
|
||||
params: {
|
||||
|
||||
@@ -11,6 +11,8 @@ export const truchetFold = {
|
||||
name: 'Truchet Fold',
|
||||
family: 'geometric',
|
||||
kind: 'fragment',
|
||||
// Takes the track's surface grain, but lightly — this is drawn, not filmed.
|
||||
texture: 0.4,
|
||||
traits: ['camera', 'style'],
|
||||
|
||||
params: {
|
||||
|
||||
Reference in New Issue
Block a user