@dopaminefx/effect-aurora 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,287 @@
1
+ /**
2
+ * GLSL ES 3.00 source for **Aurora** — a calm success / ambient effect, and a
3
+ * deliberate DIVERGENCE from Solarbloom's centered radial bloom and Verdict's
4
+ * single directional stroke.
5
+ *
6
+ * Governing metaphor: HANGING CURTAINS OF POLAR LIGHT (aurora borealis). The
7
+ * composition is a horizontal BAND of vertical light RIBBONS that drape across
8
+ * the upper field, sway, and sweep sideways, then gently brighten and fade. It
9
+ * is DIRECTIONAL/curtain — emphatically NOT a radial bloom: there is no bright
10
+ * core, no concentric falloff. The light reads as sheets hanging from an unseen
11
+ * line near the top of the frame, feathering downward into nothing.
12
+ *
13
+ * Layers, summed as light (canvas is black, composited `mix-blend-mode: screen`,
14
+ * so black == no change, bright == cast light onto the page beneath):
15
+ * 1. SKY WASH — a very faint cool gradient hugging the top of the band, so the
16
+ * curtains hang from a soft glow rather than empty black.
17
+ * 2. THE CURTAINS — several vertical ribbons. Each ribbon's horizontal centre
18
+ * is displaced by layered fbm (slow, nature-informed drift) plus a global
19
+ * sideways SWEEP; coverage is a soft horizontal lobe. Vertical STRIATIONS
20
+ * (fine fluting) ride each ribbon. Brightness peaks high in the band and
21
+ * feathers toward the bottom edge (the draped hem).
22
+ * 3. RAYS — faint brighter vertical streaks inside the curtains (the classic
23
+ * "searchlight" pillars), modulated by a second noise so they twinkle.
24
+ * 4. CROWN SHIMMER — a slow hue/intensity breathing across the whole band as
25
+ * it settles, so the colour wanders the way a real aurora pulses.
26
+ *
27
+ * Reward timing lives in uniforms (uAmp = envelope amplitude, brighten→fade;
28
+ * uLife = whole-effect progress; uSweep = accumulated sideways drift). Pure
29
+ * function of uTimeS — frame-perfect & cheap under SwiftShader (analytic noise,
30
+ * single pass, no loops over large counts).
31
+ *
32
+ * whimsy == uStyle: 0 = photoreal soft VOLUMETRIC curtains (smooth gradients,
33
+ * feathered hems, dithered); 1 = stylized HARD CEL bands — posterized ribbons
34
+ * with crisp edges + a bright rim, the drift snapped (the pass runner already
35
+ * animates the clock "on twos" as style rises).
36
+ */
37
+
38
+ import {
39
+ GLSL_CONSTANTS,
40
+ GLSL_DITHER,
41
+ GLSL_FBM,
42
+ GLSL_HASH,
43
+ GLSL_PALETTE_MIX,
44
+ GLSL_TONEMAP_ACES,
45
+ } from "@dopaminefx/core";
46
+
47
+ /**
48
+ * Number of curtain ribbons. Single source of truth: BOTH the GLSL
49
+ * `#define CURTAINS` (interpolated below) and the integer-clamp const the
50
+ * `.dope` mapping references (passed to the loader as `MAX_CURTAINS`).
51
+ */
52
+ export const MAX_CURTAINS = 7;
53
+
54
+ export const AURORA_VERTEX_SRC = /* glsl */ `#version 300 es
55
+ void main() {
56
+ vec2 pos = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
57
+ gl_Position = vec4(pos * 2.0 - 1.0, 0.0, 1.0);
58
+ }`;
59
+
60
+ export const AURORA_FRAGMENT_SRC = /* glsl */ `#version 300 es
61
+ precision highp float;
62
+ out vec4 fragColor;
63
+
64
+ uniform vec2 uResolution; // device pixels
65
+ uniform float uLife; // whole-effect progress 0..1
66
+ uniform float uTimeS; // elapsed seconds
67
+ uniform float uAmp; // envelope amplitude (brighten -> fade; peaks > 1)
68
+ uniform float uExposure; // overall light gain
69
+ uniform float uCoverage; // 0..1 band height + ribbon count fraction (intensity)
70
+ uniform float uBandY; // band vertical centre as fraction of height (0=bottom,1=top)
71
+ uniform float uBandHeight; // band half-height as fraction of height
72
+ uniform float uSway; // horizontal drift amplitude (fraction of width)
73
+ uniform float uSweep; // global sideways sweep offset (fraction of width)
74
+ uniform float uStriation; // 0..1 vertical fluting strength
75
+ uniform float uRays; // 0..1 searchlight-pillar strength
76
+ uniform float uSeed; // per-fire hash offset
77
+ uniform float uStyle; // 0..1 photoreal volumetric -> cel posterized (whimsy)
78
+ uniform float uShadow; // 0 = light pass (screen), 1 = shadow pass (multiply)
79
+ uniform vec2 uShadowOffset; // device-px offset of the cast silhouette (away from light)
80
+ uniform float uShadowSoft; // penumbra softness in device px (blur tap radius)
81
+ uniform float uShadowStrength;// 0..1 max darkening of the multiply layer
82
+ uniform vec3 uC0; // curtain core color
83
+ uniform vec3 uC1; // mid
84
+ uniform vec3 uC2; // crown / accent
85
+
86
+ #define CURTAINS ${MAX_CURTAINS}
87
+ ${GLSL_CONSTANTS}
88
+ ${GLSL_HASH}
89
+ ${GLSL_FBM}
90
+ ${GLSL_PALETTE_MIX}
91
+ ${GLSL_TONEMAP_ACES}
92
+ ${GLSL_DITHER}
93
+
94
+ // Vertical envelope of the curtain band: bright high in the band, feathering to
95
+ // nothing at the draped hem below and to a soft top above. ny is the normalized
96
+ // vertical position WITHIN the band (0 = hem/bottom, 1 = top), so the curtains
97
+ // hang from the top and fade downward like real sheets of light.
98
+ float bandProfile(float ny){
99
+ // Feather the bottom hem (long, soft) and the top (soft, generous) so the
100
+ // curtain reads as a hanging SHEET — no hard edges top or bottom.
101
+ float hem = smoothstep(0.0, 0.45, ny); // long fade up from the hem
102
+ float top = 1.0 - smoothstep(0.7, 1.0, ny); // soft, early top falloff
103
+ // Bias brightness upward (the top of a curtain glows hardest).
104
+ float bias = mix(0.6, 1.0, smoothstep(0.1, 0.85, ny));
105
+ return clamp(hem * top * bias, 0.0, 1.0);
106
+ }
107
+
108
+ // One curtain ribbon's coverage at horizontal position x (fraction 0..1) for a
109
+ // given band-vertical ny. Each ribbon has its OWN base x, sway phase and width;
110
+ // its centre is displaced by slow layered fbm (the living drift) + the global
111
+ // sweep, and is bowed slightly with height so the sheet drapes rather than
112
+ // standing dead-vertical. Returns 0..1 soft horizontal coverage.
113
+ float curtain(int i, float x, float ny, out float along){
114
+ float fi = float(i);
115
+ vec2 h = hash21(fi * 3.17 + uSeed);
116
+ // Base horizontal slot, spread across the frame with a little jitter.
117
+ float base = (fi + 0.5) / float(CURTAINS) + (h.x - 0.5) * 0.10;
118
+ // Slow nature-informed drift: two fbm samples at different rates, scrolled by
119
+ // time, so the ribbon wanders organically rather than oscillating mechanically.
120
+ float n1 = fbm(vec2(fi * 1.7 + uSeed, ny * 1.3 + uTimeS * 0.13)) - 0.5;
121
+ float n2 = fbm(vec2(fi * 0.9 + uSeed + 7.0, ny * 2.6 - uTimeS * 0.07)) - 0.5;
122
+ float drift = (n1 * 0.7 + n2 * 0.3) * uSway;
123
+ // Drape bow: the hem swings further than the top (parallax of a hanging sheet).
124
+ float bow = (h.y - 0.5) * uSway * 0.6 * (1.0 - ny);
125
+ float cx = base + drift + bow + uSweep;
126
+ along = x - cx;
127
+ // Ribbon width breathes a touch per-ribbon; soft horizontal lobe.
128
+ float w = mix(0.045, 0.085, h.y) * (0.85 + 0.3 * uCoverage);
129
+ float cov = exp(-pow(along / w, 2.0));
130
+ return cov;
131
+ }
132
+
133
+ // The full curtain field at fragment uv (0..1, y up): sum the ribbons (capped by
134
+ // coverage so low intensity shows fewer sheets), shaped vertically by the band
135
+ // profile, with vertical striations + searchlight rays riding the light. Outputs
136
+ // total coverage 'cov' and a 0..1 hue coordinate 'hue' (left->right across the
137
+ // band, wandering with the crown shimmer) for the palette.
138
+ float auroraField(vec2 uv, out float cov, out float hue){
139
+ // Vertical position within the band.
140
+ float top = uBandY + uBandHeight;
141
+ float bot = uBandY - uBandHeight;
142
+ float ny = (uv.y - bot) / max(top - bot, 1e-3); // 0 at hem, 1 at top
143
+ float vprof = bandProfile(ny);
144
+ cov = 0.0;
145
+ hue = 0.0;
146
+ if (vprof <= 0.0) return 0.0;
147
+
148
+ // How many ribbons are "lit" scales with coverage (intensity): low intensity
149
+ // shows a few calm sheets, high shows the full curtain.
150
+ float lit = mix(2.5, float(CURTAINS), clamp(uCoverage, 0.0, 1.0));
151
+
152
+ float total = 0.0;
153
+ float hueAccum = 0.0;
154
+ for (int i = 0; i < CURTAINS; i++) {
155
+ float gate = clamp(lit - float(i), 0.0, 1.0); // soft last-ribbon fade-in
156
+ if (gate <= 0.0) break;
157
+ float along;
158
+ float c = curtain(i, uv.x, ny, along) * gate;
159
+ if (c <= 0.001) continue;
160
+ total += c;
161
+ // Hue coordinate: ribbon's place across the band, nudged by its own offset.
162
+ float hi = (float(i) + 0.5) / float(CURTAINS);
163
+ hueAccum += c * hi;
164
+ }
165
+ cov = total * vprof;
166
+ hue = total > 1e-3 ? hueAccum / total : 0.5;
167
+
168
+ // Vertical STRIATIONS: fine fluting along the curtains (the characteristic
169
+ // ribbon texture). A medium-frequency noise in x that gently darkens/brightens
170
+ // narrow vertical lanes, only inside the lit region so the background stays
171
+ // clean. Kept bounded so it textures the sheet without shredding its edge.
172
+ float flute = fbm(vec2(uv.x * 55.0 + uSeed, uv.y * 4.0 - uTimeS * 0.2));
173
+ float striate = 1.0 + uStriation * (flute - 0.5) * 0.7;
174
+ cov *= striate;
175
+
176
+ // SEARCHLIGHT RAYS: a few brighter vertical pillars that twinkle — soft, fairly
177
+ // wide bands in x gated by a slow noise so they come and go. Scaled by the
178
+ // existing coverage so rays live INSIDE the curtains, never as bare spikes.
179
+ float rayBand = pow(max(0.0, sin(uv.x * 60.0 + fbm(vec2(uv.x * 5.0, uTimeS * 0.3)) * 5.0)), 3.0);
180
+ float rayGate = smoothstep(0.5, 0.95, fbm(vec2(uv.x * 9.0 + uSeed, uTimeS * 0.25)));
181
+ cov += rayBand * rayGate * uRays * smoothstep(0.05, 0.5, cov) * 0.5;
182
+
183
+ return cov;
184
+ }
185
+
186
+ // SHADOW silhouette — a cheap occlusion field for the curtain mass (no striation
187
+ // detail / rays), so the faint cast shadow tracks the hanging sheets without an
188
+ // extra heavy pass under software WebGL.
189
+ float auroraOcclusion(vec2 frag){
190
+ vec2 uv = frag / uResolution;
191
+ float top = uBandY + uBandHeight;
192
+ float bot = uBandY - uBandHeight;
193
+ float ny = (uv.y - bot) / max(top - bot, 1e-3);
194
+ float vprof = bandProfile(ny);
195
+ if (vprof <= 0.0) return 0.0;
196
+ float lit = mix(2.5, float(CURTAINS), clamp(uCoverage, 0.0, 1.0));
197
+ float total = 0.0;
198
+ for (int i = 0; i < CURTAINS; i++) {
199
+ float gate = clamp(lit - float(i), 0.0, 1.0);
200
+ if (gate <= 0.0) break;
201
+ float along;
202
+ total += curtain(i, uv.x, ny, along) * gate;
203
+ }
204
+ return clamp(total * vprof * uAmp, 0.0, 1.0);
205
+ }
206
+
207
+ vec4 auroraShadowColor(vec2 frag){
208
+ vec2 sp = frag - uShadowOffset;
209
+ float occ = auroraOcclusion(sp);
210
+ float soft = uShadowSoft;
211
+ occ += auroraOcclusion(sp + vec2( soft, 0.0));
212
+ occ += auroraOcclusion(sp + vec2(-soft, 0.0));
213
+ occ += auroraOcclusion(sp + vec2(0.0, soft));
214
+ occ += auroraOcclusion(sp + vec2(0.0, -soft));
215
+ occ /= 5.0;
216
+ // A real aurora casts almost no shadow; keep it very faint.
217
+ float dark = clamp(occ, 0.0, 1.0) * uShadowStrength * 0.35;
218
+ vec3 tint = mix(vec3(1.0), 0.6 + 0.4 * normalize(uC0 + 1e-3), 0.2);
219
+ vec3 mul = mix(vec3(1.0), tint, dark);
220
+ return vec4(mul, 1.0);
221
+ }
222
+
223
+ void main(){
224
+ vec2 frag = gl_FragCoord.xy;
225
+ vec2 res = uResolution;
226
+
227
+ if (uShadow > 0.5) {
228
+ fragColor = auroraShadowColor(frag);
229
+ return;
230
+ }
231
+
232
+ vec2 uv = frag / res;
233
+ vec3 col = vec3(0.0);
234
+ float gain = uAmp * uExposure;
235
+
236
+ // ---- SKY WASH: a faint cool glow the curtains hang from. ----
237
+ // A soft horizontal band centred near the top of the curtain band, so there is
238
+ // a gentle ground for the light without a radial core.
239
+ float washY = exp(-pow((uv.y - (uBandY + uBandHeight * 0.45)) / max(uBandHeight, 1e-3), 2.0));
240
+ col += mix(uC0, uC2, 0.5) * washY * 0.06 * gain;
241
+
242
+ // ---- THE CURTAINS ----
243
+ float cov, hue;
244
+ auroraField(uv, cov, hue);
245
+
246
+ // CROWN SHIMMER: the aurora pulses — a slow drift of the hue coordinate and a
247
+ // gentle global breathing of intensity, so the colour wanders as it settles.
248
+ float pulse = 0.85 + 0.15 * sin(uTimeS * 0.9 + hue * 4.0 + uSeed);
249
+ float hueShift = hue + 0.15 * sin(uTimeS * 0.4 + uSeed * 6.28) + 0.1 * (fbm(vec2(uv.x * 3.0, uTimeS * 0.2)) - 0.5);
250
+
251
+ vec3 curtainCol = paletteMix(clamp(hueShift, 0.0, 1.0));
252
+ col += curtainCol * clamp(cov, 0.0, 4.0) * pulse * gain;
253
+
254
+ // A subtle brighter crown along the very top edge of each lit column (where a
255
+ // real curtain glows hottest), tinted toward the accent.
256
+ float crown = smoothstep(0.0, 0.5, cov) * smoothstep(uBandY + uBandHeight * 0.2, uBandY + uBandHeight, uv.y);
257
+ col += uC2 * crown * 0.4 * gain;
258
+
259
+ // ---- Tone + finishing (ACES filmic, shared look/glsl) ----
260
+ col = tonemapACES(col * 0.9);
261
+
262
+ // ---- Non-photoreal pass: hard CEL posterized ribbons (whimsy) ----
263
+ // Toward the cel end the soft volumetric curtains snap into flat posterized
264
+ // bands with crisp edges + a bright rim — a stylized stained-glass aurora.
265
+ if (uStyle > 0.001) {
266
+ // Posterize the curtain luminance into a few hard tones (don't quantize the
267
+ // dark background — that shatters the wash into camouflage blocks).
268
+ float lum = clamp(cov * pulse * uExposure * uAmp, 0.0, 1.5);
269
+ float steps = mix(6.0, 3.0, uStyle); // fewer bands at full cel
270
+ float q = floor(lum * steps) / steps;
271
+ vec3 celCol = paletteMix(clamp(hueShift, 0.0, 1.0)) * (q * 1.15 + 0.05);
272
+ // Bright crisp rim at each posterized step edge.
273
+ float band = lum * steps;
274
+ float edge = abs(fract(band) - 0.5);
275
+ float rim = (1.0 - smoothstep(0.0, 0.12, edge)) * smoothstep(0.06, 0.2, lum);
276
+ celCol += clamp(uC2 * 1.5 + 0.1, 0.0, 1.4) * rim * 0.6;
277
+ float mask = smoothstep(0.04, 0.14, lum); // only inside the curtains
278
+ vec3 styled = mix(col, celCol, mask);
279
+ col = mix(col, styled, uStyle);
280
+ }
281
+
282
+ // Ordered dither (~1/255, shared look/glsl) to kill banding the screen blend
283
+ // would reveal on the page beneath; faded out toward the cel end.
284
+ col = ditherAdd(col, frag, uTimeS, 1.0 - uStyle);
285
+
286
+ fragColor = vec4(max(col, 0.0), 1.0);
287
+ }`;
@@ -0,0 +1,417 @@
1
+ {
2
+ "fmt": "dopamine-effect",
3
+ "v": "1.0.0",
4
+ "id": "dopamine.success.aurora",
5
+ "meta": {
6
+ "name": "Aurora",
7
+ "description": "Hanging curtains of polar light that drape across the upper field, sway and sweep, then gently brighten and fade.",
8
+ "tags": [
9
+ "success",
10
+ "ambient",
11
+ "aurora",
12
+ "directional",
13
+ "curtain"
14
+ ]
15
+ },
16
+ "controls": {
17
+ "mood": {
18
+ "type": "enum",
19
+ "label": "Mood",
20
+ "default": "serene",
21
+ "options": [
22
+ "serene",
23
+ "celebratory",
24
+ "electric"
25
+ ],
26
+ "ui": "segmented"
27
+ },
28
+ "intensity": {
29
+ "type": "scalar",
30
+ "label": "Intensity",
31
+ "default": 0.7,
32
+ "min": 0,
33
+ "max": 1,
34
+ "step": 0.01,
35
+ "ui": "slider",
36
+ "help": "Brightness, curtain coverage and band height."
37
+ },
38
+ "whimsy": {
39
+ "type": "scalar",
40
+ "label": "Whimsy",
41
+ "default": 0.4,
42
+ "min": 0,
43
+ "max": 1,
44
+ "step": 0.01,
45
+ "ui": "slider",
46
+ "help": "Photoreal soft volumetric curtains (0) to stylized hard cel / posterized ribbons (1)."
47
+ },
48
+ "seed": {
49
+ "type": "int",
50
+ "label": "Seed",
51
+ "default": null,
52
+ "nullable": true,
53
+ "help": "Null = unique palette per fire; pin to reproduce."
54
+ },
55
+ "target": {
56
+ "type": "selector",
57
+ "label": "Target",
58
+ "default": "document.body"
59
+ }
60
+ },
61
+ "baselines": {
62
+ "serene": {
63
+ "durationMs": 3200,
64
+ "lightness": 0.86,
65
+ "chroma": 0.1,
66
+ "hueCenter": 168,
67
+ "hueRange": 70,
68
+ "coverage": 0.55,
69
+ "bandY": 0.66,
70
+ "bandHeight": 0.28,
71
+ "sway": 0.06,
72
+ "striation": 0.6,
73
+ "rays": 0.35,
74
+ "overshoot": 0.4
75
+ },
76
+ "celebratory": {
77
+ "durationMs": 2600,
78
+ "lightness": 0.85,
79
+ "chroma": 0.16,
80
+ "hueCenter": 190,
81
+ "hueRange": 200,
82
+ "coverage": 0.7,
83
+ "bandY": 0.64,
84
+ "bandHeight": 0.31,
85
+ "sway": 0.08,
86
+ "striation": 0.72,
87
+ "rays": 0.5,
88
+ "overshoot": 0.7
89
+ },
90
+ "electric": {
91
+ "durationMs": 2100,
92
+ "lightness": 0.84,
93
+ "chroma": 0.23,
94
+ "hueCenter": 320,
95
+ "hueRange": 150,
96
+ "coverage": 0.85,
97
+ "bandY": 0.62,
98
+ "bandHeight": 0.34,
99
+ "sway": 0.11,
100
+ "striation": 0.88,
101
+ "rays": 0.7,
102
+ "overshoot": 1
103
+ }
104
+ },
105
+ "palette": {
106
+ "model": "oklch",
107
+ "space": "linear-srgb",
108
+ "generator": "golden-angle",
109
+ "goldenAngleDeg": 137.50776405003785,
110
+ "stops": 3,
111
+ "hueSpread": 0.22,
112
+ "lightness": {
113
+ "baseline": "lightness",
114
+ "perStop": [
115
+ 0,
116
+ 0.06,
117
+ -0.05
118
+ ]
119
+ },
120
+ "chroma": {
121
+ "from": {
122
+ "mul": [
123
+ {
124
+ "baseline": "chroma"
125
+ },
126
+ {
127
+ "lerp": [
128
+ "intensity",
129
+ 0.7,
130
+ 1.5
131
+ ]
132
+ }
133
+ ]
134
+ },
135
+ "perStop": [
136
+ 0,
137
+ 0.02,
138
+ -0.01
139
+ ]
140
+ },
141
+ "seed": {
142
+ "deterministic": true,
143
+ "source": "controls.seed",
144
+ "prng": "mulberry32"
145
+ },
146
+ "perMood": {
147
+ "serene": {
148
+ "hueCenter": 165,
149
+ "hueRange": 110,
150
+ "lightness": 0.84,
151
+ "chroma": 0.11
152
+ },
153
+ "celebratory": {
154
+ "hueCenter": 200,
155
+ "hueRange": 300,
156
+ "lightness": 0.83,
157
+ "chroma": 0.17
158
+ },
159
+ "electric": {
160
+ "hueCenter": 320,
161
+ "hueRange": 180,
162
+ "lightness": 0.82,
163
+ "chroma": 0.24
164
+ }
165
+ }
166
+ },
167
+ "tempo": {
168
+ "durationMs": {
169
+ "from": {
170
+ "round": {
171
+ "mul": [
172
+ {
173
+ "baseline": "durationMs"
174
+ },
175
+ {
176
+ "lerp": [
177
+ "intensity",
178
+ 1.1,
179
+ 0.9
180
+ ]
181
+ }
182
+ ]
183
+ }
184
+ }
185
+ },
186
+ "frame": {
187
+ "amp": {
188
+ "envelope": [
189
+ {
190
+ "input": "life"
191
+ },
192
+ {
193
+ "param": "overshoot"
194
+ }
195
+ ]
196
+ },
197
+ "extras": {
198
+ "sweep": {
199
+ "mul": [
200
+ 0.02,
201
+ {
202
+ "div": [
203
+ {
204
+ "input": "animMs"
205
+ },
206
+ 1000
207
+ ]
208
+ },
209
+ {
210
+ "sub": [
211
+ 1,
212
+ {
213
+ "mul": [
214
+ 0.5,
215
+ {
216
+ "input": "life"
217
+ }
218
+ ]
219
+ }
220
+ ]
221
+ }
222
+ ]
223
+ }
224
+ }
225
+ },
226
+ "reducedMotion": {
227
+ "peakMs": 520,
228
+ "holdMs": 520
229
+ }
230
+ },
231
+ "geometry": {
232
+ "kind": "directional",
233
+ "viewBox": [
234
+ 0,
235
+ 0,
236
+ 100,
237
+ 100
238
+ ]
239
+ },
240
+ "render": {
241
+ "params": {
242
+ "exposure": {
243
+ "type": "float",
244
+ "from": {
245
+ "lerp": [
246
+ "intensity",
247
+ 0.85,
248
+ 1.55
249
+ ]
250
+ }
251
+ },
252
+ "overshoot": {
253
+ "type": "float",
254
+ "from": {
255
+ "mul": [
256
+ {
257
+ "baseline": "overshoot"
258
+ },
259
+ {
260
+ "lerp": [
261
+ "intensity",
262
+ 0.7,
263
+ 1.2
264
+ ]
265
+ }
266
+ ]
267
+ }
268
+ },
269
+ "coverage": {
270
+ "type": "float",
271
+ "clamp01": true,
272
+ "from": {
273
+ "mul": [
274
+ {
275
+ "baseline": "coverage"
276
+ },
277
+ {
278
+ "lerp": [
279
+ "intensity",
280
+ 0.7,
281
+ 1.25
282
+ ]
283
+ }
284
+ ]
285
+ }
286
+ },
287
+ "bandY": {
288
+ "type": "float",
289
+ "from": {
290
+ "baseline": "bandY"
291
+ }
292
+ },
293
+ "bandHeight": {
294
+ "type": "float",
295
+ "clamp01": true,
296
+ "from": {
297
+ "mul": [
298
+ {
299
+ "baseline": "bandHeight"
300
+ },
301
+ {
302
+ "lerp": [
303
+ "intensity",
304
+ 0.85,
305
+ 1.15
306
+ ]
307
+ }
308
+ ]
309
+ }
310
+ },
311
+ "sway": {
312
+ "type": "float",
313
+ "from": {
314
+ "mul": [
315
+ {
316
+ "baseline": "sway"
317
+ },
318
+ {
319
+ "lerp": [
320
+ "intensity",
321
+ 0.85,
322
+ 1.2
323
+ ]
324
+ }
325
+ ]
326
+ }
327
+ },
328
+ "striation": {
329
+ "type": "float",
330
+ "clamp01": true,
331
+ "from": {
332
+ "mul": [
333
+ {
334
+ "baseline": "striation"
335
+ },
336
+ {
337
+ "lerp": [
338
+ "whimsy",
339
+ 0.9,
340
+ 1.1
341
+ ]
342
+ }
343
+ ]
344
+ }
345
+ },
346
+ "rays": {
347
+ "type": "float",
348
+ "clamp01": true,
349
+ "from": {
350
+ "mul": [
351
+ {
352
+ "baseline": "rays"
353
+ },
354
+ {
355
+ "lerp": [
356
+ "intensity",
357
+ 0.8,
358
+ 1.2
359
+ ]
360
+ }
361
+ ]
362
+ }
363
+ },
364
+ "style": {
365
+ "type": "float",
366
+ "from": {
367
+ "control": "whimsy"
368
+ }
369
+ }
370
+ },
371
+ "shadowHeightFrac": {
372
+ "mul": [
373
+ {
374
+ "param": "bandHeight"
375
+ },
376
+ 0.6
377
+ ]
378
+ },
379
+ "consts": {
380
+ "MAX_CURTAINS": 7
381
+ },
382
+ "config": {
383
+ "usesOrigin": false
384
+ },
385
+ "backends": {
386
+ "webgl2": {
387
+ "stage": "fullscreen-triangle",
388
+ "blend": "screen",
389
+ "shader": {
390
+ "program": "aurora"
391
+ }
392
+ }
393
+ },
394
+ "fallbackOrder": [
395
+ "webgl2"
396
+ ]
397
+ },
398
+ "binding": {
399
+ "note": "CROSS-PLATFORM uniform-binding contract. Which render.params are NOT shader uniforms, the seed-keyed scatter field, the per-frame/host extras, and the texture samplers — one source of truth for the web u<Name> list, the Swift struct + packer, and the MSL struct. SHIPS in the portable .dope (the runtime derives its uniform bindings from it); the toolchain consumes it too for the Metal struct codegen. Only `x-build`, `slug` and `kind` stay toolchain-only.",
400
+ "excludeParams": [
401
+ "style",
402
+ "overshoot",
403
+ "durationMs"
404
+ ],
405
+ "scatterKey": "auroraSeed",
406
+ "scatterWeb": "uSeed",
407
+ "extras": [
408
+ {
409
+ "name": "sweep",
410
+ "type": "float",
411
+ "web": "uSweep",
412
+ "note": "accumulated sideways sweep (fraction of width)"
413
+ }
414
+ ],
415
+ "samplers": []
416
+ }
417
+ }