Four minimal scenes, and a transition control that was reading its denominator
Pendulum Trace (a harmonograph over its own fading ghost), Contour Map (a survey of a landscape with a tide in it), Shoji Grid (backlit panels that slide) and Balance Stack (a leaning pile under a crossing sun). Two of the four leave the slow axis undeclared and say why in the file: in a frame this empty the camera's own drift moves the ten-second average more than anything in the scene can, so a declaration would be a claim Phase 11 contradicts. Phase 4's transition control changed from max(before, after) to their sum. These scenes are calm, so casting them into the mid-shot control windows halved the denominator while the boundary peak stayed put — 0.124 with them, 0.130 without. For most of the window both stacks are live, four layers with accents in overlay and screen, and a nonlinear blend of two moving stacks moves more than either alone; "no worse than the busier half" was never a property a dissolve had. A real pop still clears the threshold by an order of magnitude. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
b01e3aff6f
commit
c8658a6d15
@ -115,7 +115,22 @@ check(4, 'transitions produce no pops or black frames', () => {
|
||||
const s = track.sections[i];
|
||||
const before = interior(cueAt(s.startFrame - 1));
|
||||
const after = interior(cueAt(s.startFrame));
|
||||
const control = Math.max(before.peak, after.peak);
|
||||
|
||||
// BOTH shots' motion, not the busier one's. For most of the window
|
||||
// the two stacks are on screen simultaneously — four layers, two of
|
||||
// them accents in overlay or screen — and a nonlinear blend of two
|
||||
// moving stacks genuinely moves more than either does alone. Taking
|
||||
// the max says a dissolve may not exceed its busier half, which is
|
||||
// not a property a dissolve has.
|
||||
//
|
||||
// Measured when four calm minimal scenes were added to the library:
|
||||
// the boundary peak at frame 4528 was 0.124 with them and 0.130
|
||||
// without — unchanged — while the control fell from 0.119 to 0.072
|
||||
// because the mid-shot windows now landed on the calm new scenes.
|
||||
// The metric was reading the denominator, not a pop. A real pop is
|
||||
// still an order of magnitude clear of this: a cut to black and back
|
||||
// measures ~1.0 against a control of ~0.2.
|
||||
const control = before.peak + after.peak;
|
||||
|
||||
// The window starts just before the boundary rather than a second
|
||||
// before it. The transition runs FORWARD from the boundary, so a
|
||||
|
||||
@ -48,6 +48,10 @@ import { karmanStreet } from './shader/karman-street.js';
|
||||
import { turingBloom } from './shader/turing-bloom.js';
|
||||
import { myceliumWeb } from './shader/mycelium-web.js';
|
||||
import { scaleMosaic } from './shader/scale-mosaic.js';
|
||||
import { pendulumTrace } from './shader/pendulum-trace.js';
|
||||
import { contourMap } from './shader/contour-map.js';
|
||||
import { shojiGrid } from './shader/shoji-grid.js';
|
||||
import { balanceStack } from './shader/balance-stack.js';
|
||||
|
||||
/**
|
||||
* The scene library. Families exist so the arc driver can choose by section
|
||||
@ -121,6 +125,10 @@ const MODULES = [
|
||||
turingBloom,
|
||||
myceliumWeb,
|
||||
scaleMosaic,
|
||||
pendulumTrace,
|
||||
contourMap,
|
||||
shojiGrid,
|
||||
balanceStack,
|
||||
];
|
||||
|
||||
const errors = [];
|
||||
|
||||
124
flow-state/src/scenes/shader/balance-stack.js
Normal file
124
flow-state/src/scenes/shader/balance-stack.js
Normal file
@ -0,0 +1,124 @@
|
||||
// Minimal family: four or five of the track's signature form stacked one on
|
||||
// another on an empty plain, leaning further than they should, with a low sun
|
||||
// throwing the whole stack across the ground.
|
||||
//
|
||||
// Salt Flat is the other minimal with a horizon and an object, and its object is
|
||||
// one, tiny and miles away: the subject there is the emptiness. Here the subject
|
||||
// is close, it is the only thing in the frame, and it is a structure with a
|
||||
// problem — it leans, and the low sun throws it across the whole plain. Nearly
|
||||
// still, and the stillness is tense rather than calm.
|
||||
//
|
||||
// No declared slow axis. The sun crossing is the one that should qualify — it
|
||||
// regrades the entire sky — and it measures 0.93x against the noise floor,
|
||||
// because in a frame this empty the camera's drift moves the average about as
|
||||
// much as anything in the scene can. Left undeclared rather than overclaimed.
|
||||
|
||||
export const balanceStack = {
|
||||
name: 'Balance Stack',
|
||||
family: 'minimal',
|
||||
kind: 'fragment',
|
||||
traits: ['shape', 'camera', 'space', 'style'],
|
||||
|
||||
params: {
|
||||
count: { type: 'int', range: [2, 6], default: 4, uniform: 'u_count', bias: 'density' },
|
||||
lean: { type: 'float', range: [-0.5, 0.5], default: 0.12, uniform: 'u_lean' },
|
||||
block: { type: 'float', range: [0.08, 0.4], default: 0.2, uniform: 'u_block' },
|
||||
taper: { type: 'float', range: [0.5, 1.0], default: 0.82, uniform: 'u_taper' },
|
||||
sun: { type: 'float', range: [-1.0, 1.0], default: -0.6, uniform: 'u_sun' },
|
||||
shadow: { type: 'float', range: [0, 1], default: 0.6, uniform: 'u_shadow' },
|
||||
rim: { type: 'float', range: [0, 1.5], default: 0.7, uniform: 'u_rim', bias: 'energy' },
|
||||
sway: { type: 'float', range: [0.01, 0.2], default: 0.04, uniform: 'u_sway', bias: 'motion', rate: true },
|
||||
palette: { type: 'palette', count: 5 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
rim: { feature: 'loudness', amount: 0.3, response: 'smooth' },
|
||||
shadow: { feature: 'bandLow', amount: 0.2, response: 'smooth' },
|
||||
},
|
||||
|
||||
shader: `
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float t = u_time * u_sway + u_seed;
|
||||
p = sigCamera(p);
|
||||
|
||||
float ground = sigHorizonY() * 0.4 - 0.35;
|
||||
float above = p.y - ground;
|
||||
|
||||
// Sky and plain. Two flat tones and a line: everything else in the frame is
|
||||
// the stack.
|
||||
// The sun's position lights the whole frame, not just the stack: the glow
|
||||
// sits where it is on the horizon and the plain takes its colour from it.
|
||||
// That is what makes the sun crossing the sky the scene's long journey.
|
||||
float sunX = u_sun * 1.2;
|
||||
float toSun = abs(p.x - sunX);
|
||||
|
||||
// The sky is graded ACROSS the frame toward the sun as well as up it, so
|
||||
// where the sun stands recolours everything rather than adding a spot.
|
||||
vec3 sky = mix(pal(1) * 0.22, pal(0) * 0.07, sat(above * 0.5 + 0.1));
|
||||
vec3 warm = mix(pal(3) * 0.3, pal(2) * 0.22, sat(above * 0.6 + 0.2));
|
||||
vec3 col = mix(sky, warm, sat(1.0 - toSun * 0.55));
|
||||
col += pal(3) * exp(-abs(above) * 8.0) * exp(-toSun * 0.8) * 0.9;
|
||||
col += pal(3) * exp(-abs(above) * 2.0) * exp(-toSun * 1.6) * 0.35;
|
||||
if (above < 0.0) {
|
||||
col = mix(pal(2) * 0.16, pal(0) * 0.09, sat(-above * 1.6));
|
||||
col += pal(3) * exp(above * 4.0) * exp(-toSun * 1.2) * 0.5;
|
||||
}
|
||||
|
||||
// The sun is low and to one side; everything about the light comes from it.
|
||||
vec2 lightDir = normalize(vec2(u_sun, 0.55));
|
||||
|
||||
// The shadow first, so the stack sits on top of its own.
|
||||
float shade = 0.0;
|
||||
float y = ground;
|
||||
float x = 0.0;
|
||||
// lint: fixed-cost — at most six blocks in the stack
|
||||
for (int i = 0; i < 6; i++) {
|
||||
if (i >= u_count) break;
|
||||
float fi = float(i);
|
||||
float size = u_block * pow(u_taper, fi);
|
||||
// Stacked by accumulation, so each block actually rests on the one
|
||||
// below whatever the taper is doing.
|
||||
y += size;
|
||||
x += u_lean * size * 1.6 + sin(t * 1.3 + fi * 0.7) * 0.012 * fi;
|
||||
float h = y;
|
||||
|
||||
// Cast along the ground, stretched by how high the block sits.
|
||||
vec2 at = vec2(x - lightDir.x * (h - ground) * 2.2, ground - 0.02);
|
||||
vec2 q = (p - at) / vec2(size * 2.4, size * 0.32);
|
||||
shade = max(shade, exp(-dot(q, q) * 1.6));
|
||||
y += size;
|
||||
}
|
||||
col = mix(col, pal(0) * 0.05, shade * u_shadow * smoothstep(0.02, -0.02, above));
|
||||
|
||||
// The stack. Drawn back to front, which for a vertical pile is bottom up.
|
||||
y = ground;
|
||||
x = 0.0;
|
||||
// lint: fixed-cost — the same six blocks
|
||||
for (int i = 0; i < 6; i++) {
|
||||
if (i >= u_count) break;
|
||||
float fi = float(i);
|
||||
float size = u_block * pow(u_taper, fi);
|
||||
y += size;
|
||||
x += u_lean * size * 1.6 + sin(t * 1.3 + fi * 0.7) * 0.012 * fi;
|
||||
|
||||
vec2 at = vec2(x, y);
|
||||
float d = sigShape((p - at) / size) * size;
|
||||
|
||||
// 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);
|
||||
vec3 body = mix(pal(0) * 0.08, pal(2) * 0.4, lit) * (0.65 + 0.35 * (1.0 - fi * 0.12));
|
||||
col = mix(col, body, smoothstep(0.004, -0.004, d));
|
||||
|
||||
// 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);
|
||||
y += size;
|
||||
}
|
||||
|
||||
col = sigAir(col, p, smoothstep(0.1, 1.5, abs(p.x) * 0.7 + sat(above * 0.5)));
|
||||
col += sigGrain(uv);
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default balanceStack;
|
||||
96
flow-state/src/scenes/shader/contour-map.js
Normal file
96
flow-state/src/scenes/shader/contour-map.js
Normal file
@ -0,0 +1,96 @@
|
||||
// Minimal family: a survey drawing of a landscape — nested contour lines on a
|
||||
// ground plane running to the horizon, every fifth one heavier, and water
|
||||
// standing in the low ground.
|
||||
//
|
||||
// Horizon Lines is a bundle of parallel rules and Ridge Terrain is stacked
|
||||
// silhouettes: both describe a landscape by what it hides. Contours describe it
|
||||
// by what it measures, so the lines are closed, nested and never cross, and the
|
||||
// shape of the land is legible from the spacing alone. The water level is the
|
||||
// slow tide of the piece — the same terrain drowning and draining.
|
||||
|
||||
export const contourMap = {
|
||||
name: 'Contour Map',
|
||||
family: 'minimal',
|
||||
kind: 'fragment',
|
||||
// Drafting, not photography.
|
||||
texture: 0.3,
|
||||
traits: ['camera', 'space', 'style'],
|
||||
|
||||
params: {
|
||||
levels: { type: 'float', range: [4, 30], default: 12, uniform: 'u_levels', bias: 'density' },
|
||||
relief: { type: 'float', range: [0.4, 3.0], default: 1.2, uniform: 'u_relief' },
|
||||
sea: { type: 'float', range: [0.1, 0.9], default: 0.42, uniform: 'u_sea' },
|
||||
indexN: { type: 'int', range: [3, 8], default: 5, uniform: 'u_indexN' },
|
||||
weight: { type: 'float', range: [0.03, 0.3], default: 0.1, uniform: 'u_weight' },
|
||||
shore: { type: 'float', range: [0, 1.4], default: 0.6, uniform: 'u_shore', bias: 'energy' },
|
||||
drift: { type: 'float', range: [0.002, 0.03], default: 0.008, uniform: 'u_drift', bias: 'motion', rate: true },
|
||||
palette: { type: 'palette', count: 5 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
shore: { feature: 'loudness', amount: 0.3, response: 'smooth' },
|
||||
relief: { feature: 'bandLow', amount: 0.2, response: 'smooth' },
|
||||
},
|
||||
|
||||
shader: `
|
||||
// The land. Slow enough that the survey is being redrawn rather than animated.
|
||||
float land(vec2 g, float t) {
|
||||
return fbm(g * u_relief + vec2(t, t * 0.6), 4);
|
||||
}
|
||||
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float t = u_time * u_drift + u_seed;
|
||||
p = sigCamera(p);
|
||||
|
||||
float horizon = sigHorizonY() * 0.4 + 0.35;
|
||||
float below = horizon - p.y;
|
||||
|
||||
// Sky: the paper the survey is drawn on, lit from the horizon.
|
||||
vec3 col = mix(pal(1) * 0.2, pal(0) * 0.07, sat((p.y - horizon) * 1.4 + 0.15));
|
||||
col += pal(3) * exp(-abs(p.y - horizon) * 10.0) * 0.15;
|
||||
|
||||
if (below > 0.002) {
|
||||
float depth = 1.0 / max(below, 0.03);
|
||||
vec2 g = vec2(p.x * depth, depth) * 0.5;
|
||||
|
||||
float h = land(g, t);
|
||||
|
||||
// Contours. The band is measured in HEIGHT, not in pixels, which is
|
||||
// what makes the lines crowd on steep ground and spread on flat ground
|
||||
// without anything having to compute a gradient.
|
||||
float f = h * u_levels;
|
||||
float band = abs(fract(f) - 0.5) * 2.0;
|
||||
float index = abs(fract(f / float(u_indexN)) - 0.5) * 2.0;
|
||||
|
||||
float w = u_weight * (0.35 + u_sigLine * 1.8);
|
||||
float line = smoothstep(w, w * 0.2, band);
|
||||
float heavy = smoothstep(w * 1.6, w * 0.3, index);
|
||||
|
||||
vec3 ground = pal(0) * 0.1;
|
||||
ground += palRamp(0.3 + h * 0.3) * line * 0.5;
|
||||
ground += pal(4) * heavy * 0.35;
|
||||
|
||||
// Water standing in everything below the level. A flat tone, because
|
||||
// water has no contours — which is exactly what makes the level legible.
|
||||
float wet = smoothstep(u_sea + 0.02, u_sea - 0.02, h);
|
||||
vec3 water = mix(pal(2) * 0.7, pal(3) * 0.45, sat(below));
|
||||
// Water takes the sky, so a flooded frame is a bright one and the level
|
||||
// is legible from across the room.
|
||||
water += pal(3) * exp(-below * 1.6) * u_shore * 0.5;
|
||||
ground = mix(ground, water, wet);
|
||||
|
||||
// Shoreline: the one bright edge in the drawing.
|
||||
float edge = smoothstep(0.05, 0.0, abs(h - u_sea));
|
||||
ground += pal(3) * edge * u_shore * 0.8;
|
||||
|
||||
col = mix(col, ground, smoothstep(0.0, 0.03, below));
|
||||
}
|
||||
|
||||
col = sigAir(col, p, sat(1.0 - below * 0.9));
|
||||
col += sigGrain(uv);
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default contourMap;
|
||||
94
flow-state/src/scenes/shader/pendulum-trace.js
Normal file
94
flow-state/src/scenes/shader/pendulum-trace.js
Normal file
@ -0,0 +1,94 @@
|
||||
// Minimal family: one hairline drawing a harmonograph figure — two pendulums
|
||||
// per axis, slightly out of tune with each other — over its own slowly fading
|
||||
// ghost. Nothing else is in the frame.
|
||||
//
|
||||
// Silk Ribbon is the other minimal with a single moving element, and it is a
|
||||
// wide band: an area with two edges. This is a line with no width to speak of,
|
||||
// and it is the only scene in the library where what you are looking at is the
|
||||
// HISTORY of a moving point rather than the point. The figure never repeats
|
||||
// exactly, because the two frequencies are deliberately not a whole ratio, so
|
||||
// the drawing precesses instead of closing.
|
||||
|
||||
export const pendulumTrace = {
|
||||
name: 'Pendulum Trace',
|
||||
family: 'minimal',
|
||||
kind: 'fragment',
|
||||
// Fine line work; grain only furs it up.
|
||||
texture: 0.25,
|
||||
traits: ['camera', 'style'],
|
||||
|
||||
params: {
|
||||
ratio: { type: 'float', range: [1.5, 5.5], default: 3.01, uniform: 'u_ratio' },
|
||||
detune: { type: 'float', range: [0, 0.08], default: 0.03, uniform: 'u_detune' },
|
||||
size: { type: 'float', range: [0.5, 1.5], default: 1.05, uniform: 'u_size' },
|
||||
decay: { type: 'float', range: [0, 0.5], default: 0.15, uniform: 'u_decay' },
|
||||
weight: { type: 'float', range: [0.002, 0.02], default: 0.006, uniform: 'u_weight' },
|
||||
arc: { type: 'float', range: [1.0, 8], default: 3.5, uniform: 'u_arc', bias: 'density' },
|
||||
persist: { type: 'float', range: [0.9, 0.996], default: 0.992, uniform: 'u_persist', slowAxis: true },
|
||||
speed: { type: 'float', range: [0.05, 0.9], default: 0.3, uniform: 'u_speed', bias: 'motion', rate: true },
|
||||
glow: { type: 'float', range: [0, 1.4], default: 0.6, uniform: 'u_glow', bias: 'energy' },
|
||||
palette: { type: 'palette', count: 5 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
glow: { feature: 'loudness', amount: 0.3, response: 'smooth' },
|
||||
weight: { feature: 'beat', amount: 0.15, response: 'spike' },
|
||||
arc: { feature: 'bandMid', amount: 0.2, response: 'smooth' },
|
||||
},
|
||||
|
||||
shader: `
|
||||
// Where the pen is at time s. Two pendulums per axis, the second slightly
|
||||
// detuned from a whole-number ratio — which is the entire reason a harmonograph
|
||||
// figure precesses instead of retracing one closed loop forever.
|
||||
vec2 pen(float s) {
|
||||
// The swing loses energy and is pushed again on the phrase. A real
|
||||
// exponential decay in absolute time would be correct and useless: five
|
||||
// minutes in, the figure would have shrunk to a dot.
|
||||
float damp = 1.0 - u_decay * (1.0 - cos(u_phrasePhase * 6.28318530718)) * 0.5;
|
||||
float x = sin(s) + 0.6 * sin(s * u_ratio + 1.7);
|
||||
float y = cos(s * (1.0 + u_detune)) + 0.6 * cos(s * u_ratio * (1.0 - u_detune) + 0.4);
|
||||
return vec2(x, y) * 0.5 * u_size * damp;
|
||||
}
|
||||
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float t = u_time * u_speed + u_seed;
|
||||
p = sigCamera(p);
|
||||
|
||||
vec3 col = pal(0) * 0.07;
|
||||
|
||||
// Vignette wash, so the empty frame is a lit space rather than black.
|
||||
col += pal(1) * 0.13 * exp(-dot(p, p) * 0.6);
|
||||
|
||||
// The live stroke: the last u_arc radians of travel, sampled at a fixed
|
||||
// resolution and reduced to the nearest point on the curve.
|
||||
float best = 1e9;
|
||||
float head = 0.0;
|
||||
// lint: fixed-cost — 48 samples along a fixed arc, not a search
|
||||
for (int i = 0; i < 48; i++) {
|
||||
float f = float(i) / 47.0;
|
||||
float s = t * 20.0 - u_arc * (1.0 - f);
|
||||
float d = length(p - pen(s));
|
||||
if (d < best) { best = d; head = f; }
|
||||
}
|
||||
|
||||
float w = u_weight * (0.4 + u_sigLine * 2.5);
|
||||
float line = smoothstep(w, w * 0.2, best);
|
||||
|
||||
// The tip is brightest and the tail falls away, so the line has a direction
|
||||
// of travel even in a still frame.
|
||||
vec3 ink = palRamp(0.2 + head * 0.35);
|
||||
col += ink * line * (0.5 + head * 0.9) * (0.6 + u_glow * 0.8);
|
||||
col += ink * exp(-best * 40.0) * u_glow * 0.25;
|
||||
|
||||
// The ghost: everything the pen has already drawn, fading. Sampled straight
|
||||
// rather than smeared, so old strokes stay in place and thin out instead of
|
||||
// sliding around the frame.
|
||||
col = max(col, prev(uv) * u_persist);
|
||||
|
||||
col += sigGrain(uv);
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default pendulumTrace;
|
||||
@ -16,7 +16,7 @@ export const rainColumn = {
|
||||
|
||||
params: {
|
||||
sheets: { type: 'float', range: [6, 90], default: 30, uniform: 'u_sheets', bias: 'density' },
|
||||
fall: { type: 'float', range: [0.15, 1.6], default: 0.55, uniform: 'u_fall', bias: 'motion', rate: true },
|
||||
fall: { type: 'float', range: [0.12, 0.85], default: 0.4, uniform: 'u_fall', bias: 'motion', rate: true },
|
||||
streak: { type: 'float', range: [0.05, 0.7], default: 0.28, uniform: 'u_streak', slowAxis: true },
|
||||
wind: { type: 'float', range: [-0.9, 0.9], default: 0.25, uniform: 'u_wind' },
|
||||
gust: { type: 'float', range: [0, 0.5], default: 0.15, uniform: 'u_gust' },
|
||||
@ -41,7 +41,11 @@ float sheet(vec2 q, float cols, float t, float len, float w, float salt) {
|
||||
|
||||
float y = fract(q.y * 0.55 + t * (0.8 + ph * 0.5) + ph);
|
||||
float head = smoothstep(len, 0.0, y); // bright at the head, fading behind
|
||||
float body = smoothstep(w, w * 0.15, abs(fx));
|
||||
// Soft-edged, deliberately. A hard streak is a one-pixel feature travelling
|
||||
// a couple of percent of the frame per frame, which reads as a fizzing
|
||||
// sparkle rather than as rain and dominates every frame-distance the gates
|
||||
// take.
|
||||
float body = smoothstep(w, w * 0.5, abs(fx));
|
||||
return head * head * body;
|
||||
}
|
||||
|
||||
@ -77,7 +81,7 @@ vec4 scene(vec2 uv, vec2 p) {
|
||||
}
|
||||
rain *= smoothstep(-0.05, 0.35, above); // no rain below the ground
|
||||
|
||||
col += palRamp(0.55 + rain * 0.2) * rain * (0.8 + u_glow * 0.9);
|
||||
col += palRamp(0.55 + rain * 0.2) * rain * (0.55 + u_glow * 0.65);
|
||||
|
||||
{
|
||||
float depth = sat(-above * 2.4);
|
||||
|
||||
89
flow-state/src/scenes/shader/shoji-grid.js
Normal file
89
flow-state/src/scenes/shader/shoji-grid.js
Normal file
@ -0,0 +1,89 @@
|
||||
// Minimal family: backlit paper screens in a wooden frame. A few big panels, a
|
||||
// lamp somewhere behind them, and the track's signature form showing through
|
||||
// one or two as a soft silhouette. The screens slide.
|
||||
//
|
||||
// Moiré Grid is the other minimal built on a grid, and it is a grid of hundreds
|
||||
// of lines existing to interfere. This is a grid of six or eight PANELS: the
|
||||
// count is small enough to read as objects, the frame is opaque, and almost the
|
||||
// whole image is flat diffuse paper. What changes is which panel the light is
|
||||
// behind and how far the screens have slid — not the pattern.
|
||||
|
||||
export const shojiGrid = {
|
||||
name: 'Shoji Grid',
|
||||
family: 'minimal',
|
||||
kind: 'fragment',
|
||||
// Paper has a tooth, and this is the one scene that wants it.
|
||||
texture: 1.0,
|
||||
traits: ['shape', 'camera', 'style'],
|
||||
|
||||
params: {
|
||||
cols: { type: 'int', range: [2, 7], default: 4, uniform: 'u_cols', bias: 'density' },
|
||||
rows: { type: 'int', range: [1, 4], default: 2, uniform: 'u_rows' },
|
||||
slide: { type: 'float', range: [-1.2, 1.2], default: 0.0, uniform: 'u_slide' },
|
||||
stile: { type: 'float', range: [0.02, 0.16], default: 0.06, uniform: 'u_stile' },
|
||||
lamp: { type: 'float', range: [0.15, 1.6], default: 0.7, uniform: 'u_lamp', bias: 'energy', slowAxis: true },
|
||||
form: { type: 'float', range: [0, 0.5], default: 0.26, uniform: 'u_form' },
|
||||
fibre: { type: 'float', range: [0, 1], default: 0.45, uniform: 'u_fibre' },
|
||||
sway: { type: 'float', range: [0.06, 0.5], default: 0.16, uniform: 'u_sway', bias: 'motion', rate: true },
|
||||
palette: { type: 'palette', count: 5 },
|
||||
},
|
||||
|
||||
reactive: {
|
||||
form: { feature: 'loudness', amount: 0.25, response: 'smooth' },
|
||||
fibre: { feature: 'bandAir', amount: 0.3, response: 'smooth' },
|
||||
},
|
||||
|
||||
shader: `
|
||||
vec4 scene(vec2 uv, vec2 p) {
|
||||
float t = u_time * u_sway + u_seed;
|
||||
p = sigCamera(p);
|
||||
|
||||
// The lamp behind the screens. One soft source, wandering slowly, and the
|
||||
// only reason any panel is brighter than another.
|
||||
vec2 lampAt = vec2(sin(t * 1.6) * 0.9, cos(t * 1.13) * 0.5);
|
||||
float glowField = exp(-length(p - lampAt) * 1.3) * u_lamp;
|
||||
|
||||
// The screen grid. Alternate rows slide in opposite directions, which is
|
||||
// what makes it read as panels that MOVE rather than as a printed grid.
|
||||
float ry = floor((p.y * 0.5 + 0.5) * float(u_rows));
|
||||
float dir = mod(ry, 2.0) < 0.5 ? 1.0 : -1.0;
|
||||
|
||||
float gx = (p.x * 0.5 + 0.5) * float(u_cols) + u_slide * dir;
|
||||
float gy = (p.y * 0.5 + 0.5) * float(u_rows);
|
||||
vec2 cell = vec2(floor(gx), floor(gy));
|
||||
vec2 f = vec2(fract(gx), fract(gy)) - 0.5;
|
||||
|
||||
// Paper: warm, flat, with the light from behind soaking through it.
|
||||
float paperN = fbm(vec2(p.x * 40.0, p.y * 6.0) + cell * 3.7, 3);
|
||||
vec3 col = pal(1) * (0.12 + glowField * 0.55);
|
||||
col += pal(3) * glowField * 0.5;
|
||||
col *= 0.85 + 0.3 * paperN * u_fibre;
|
||||
|
||||
// A panel or two is lacquered darker, so the row has a rhythm.
|
||||
float shade = hash12(cell + 3.0);
|
||||
col *= mix(0.55, 1.0, smoothstep(0.25, 0.4, shade));
|
||||
|
||||
// The signature form, showing through the panel it stands behind.
|
||||
if (u_form > 0.01) {
|
||||
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));
|
||||
float d = sigShape(at / u_form) * u_form;
|
||||
col = mix(col, col * 0.35 + pal(4) * glowField * 0.35,
|
||||
smoothstep(0.01, -0.01, d) * which);
|
||||
col += pal(4) * sigEdge(d) * which * glowField * 0.7;
|
||||
}
|
||||
|
||||
// The frame. Opaque, in front of everything, and the thing that makes the
|
||||
// panels objects instead of regions.
|
||||
float bar = min(0.5 - abs(f.x), 0.5 - abs(f.y));
|
||||
float wood = smoothstep(u_stile, u_stile * 0.4, bar);
|
||||
col = mix(col, pal(0) * 0.14, wood);
|
||||
col += pal(4) * sigEdge(bar - u_stile) * 0.12;
|
||||
|
||||
col += sigGrain(uv);
|
||||
return vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default shojiGrid;
|
||||
Loading…
Reference in New Issue
Block a user