@dopaminefx/effect-dots 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.
package/dist/index.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Dots (the calm ambient "thinking" indicator) as an `EffectFactory` on the
3
+ * Dopamine backbone.
4
+ *
5
+ * FULLY DATA-DRIVEN: everything that isn't the GLSL lives in dots.dope.json —
6
+ * the mood→params mapping + OKLCH palette (the loader), AND the per-frame logic:
7
+ * `tempo.frame` (the steady periodic breathe gate), `render.shadowHeightFrac`
8
+ * (the dots' outer reach), `render.consts` (MAX_DOTS/MIN_DOTS), `render.config`
9
+ * and the uniform `binding` contract. `registerDopeEffect` interprets that data
10
+ * through the generic pass runner; this module is just the dot-row SHADER + the
11
+ * registration call. No swift/ or android/ folder — those factories + the MSL /
12
+ * Kotlin shaders are generated from this one .dope plus the GLSL source.
13
+ *
14
+ * CONTINUOUS / LOOPING. Dots is Dopamine's second continuous effect (after halo):
15
+ * it declares the first-class `tempo.loop` contract (`periodMs = 1000`): the
16
+ * parser validates the seam invariants (the period is exactly 12
17
+ * "animate-on-twos" steps and `durationMs = 4000` is exactly 4 periods), the
18
+ * runner derives the standard periodic clocks `uPhase`/`uLoopS` every frame, and
19
+ * `tempo.frame.amp` is a STEADY periodic breathe of that phase —
20
+ * `0.85 + 0.15·sin(2π·phase)` — so the frame at `t == durationMs` matches
21
+ * `t == 0` at every whimsy. The conductor re-arms it at every `durationMs` seam;
22
+ * the host stops it (and can pause/resume it) via the handle `play()` returns.
23
+ */
24
+ import { DOTS_FRAGMENT_SRC, DOTS_VERTEX_SRC } from "./dots-shader.js";
25
+ import { parseDope, registerDopeEffect } from "@dopaminefx/core";
26
+ import doc from "./dots.dope.json";
27
+ const DOPE = parseDope(doc);
28
+ // The whole factory (resolve / create / reducedMotion / program registration)
29
+ // is data: dots.dope.json interpreted by the core backbone.
30
+ export const dots = registerDopeEffect(DOPE, {
31
+ vertex: DOTS_VERTEX_SRC,
32
+ fragment: DOTS_FRAGMENT_SRC,
33
+ });
34
+ export default dots;
35
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAuC,MAAM,kBAAkB,CAAC;AACtG,OAAO,GAAG,MAAM,kBAAkB,CAAC;AAEnC,MAAM,IAAI,GAAG,SAAS,CAAC,GAAa,CAAC,CAAC;AActC,8EAA8E;AAC9E,4DAA4D;AAC5D,MAAM,CAAC,MAAM,IAAI,GAAG,kBAAkB,CAAC,IAAI,EAAE;IAC3C,MAAM,EAAE,eAAe;IACvB,QAAQ,EAAE,iBAAiB;CAC5B,CAA2D,CAAC;AAE7D,eAAe,IAAI,CAAC"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@dopaminefx/effect-dots",
3
+ "version": "0.1.0",
4
+ "description": "Dots — a calm ambient looping 'thinking' dot-row indicator for Dopamine.",
5
+ "keywords": [
6
+ "dopamine-effect"
7
+ ],
8
+ "type": "module",
9
+ "main": "./dist/index.js",
10
+ "module": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src"
21
+ ],
22
+ "sideEffects": [
23
+ "./src/index.ts",
24
+ "./dist/index.js"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.json"
28
+ },
29
+ "dependencies": {
30
+ "@dopaminefx/core": "^0.1.0"
31
+ },
32
+ "license": "MIT",
33
+ "author": "10in30",
34
+ "homepage": "https://github.com/10in30/dopamine#readme",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/10in30/dopamine.git",
38
+ "directory": "effects/dots/web"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/10in30/dopamine/issues"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ }
46
+ }
@@ -0,0 +1,239 @@
1
+ /**
2
+ * GLSL ES 3.00 source for **Dots** — Dopamine's calm ambient "thinking" indicator.
3
+ *
4
+ * Governing metaphor: a centred ROW of soft luminous dots, centred on the action
5
+ * point (`uOrigin`). They gently BREATHE together (a slow sine on radius +
6
+ * brightness) while a brightness PULSE travels across the row in sequence (the
7
+ * classic "typing… / loading…" read). Electric / serene OKLCH hues from the
8
+ * seeded palette.
9
+ *
10
+ * Dopamine's SECOND CONTINUOUS effect (after halo). Like halo it rides the
11
+ * first-class `tempo.loop` contract:
12
+ * - The `.dope` declares `tempo.loop.periodMs = 1000`; the runner derives the
13
+ * standard periodic clocks from it each frame — `uPhase` (normalized loop
14
+ * phase in [0, 1)) and `uLoopS` (seconds within the loop) — off the SAME
15
+ * "animate on twos"-snapped clock as `uTimeS`. The parser validates the seam
16
+ * invariants (the period is 12 on-twos steps; `durationMs` 4000 = 4 whole
17
+ * periods), so the frame at `t == durationMs` equals `t == 0` at EVERY whimsy.
18
+ * - ALL animation here is a periodic function of `uPhase`: `sin(TAU·uPhase)`
19
+ * for the breathe, and a chase whose head position is `fract(uPhase)` so it
20
+ * winds exactly ONE turn around the row per period and rejoins seamlessly.
21
+ * Nothing reads a monotonic clock (`uLife`/`uTimeS`), so every period
22
+ * boundary is seamless.
23
+ * - `frame()` returns a STEADY periodic `amp = 0.85 + 0.15·sin(TAU·phase)`
24
+ * (never `envelope(life)`), so there is no one-shot fade to break the seam.
25
+ * - The conductor re-arms the effect at `durationMs` instead of tearing down;
26
+ * the host stops it via the play handle (and can pause/resume it drift-free).
27
+ *
28
+ * whimsy == uStyle:
29
+ * 0 = photoreal soft glow — gaussian dots + a smooth feathered chase.
30
+ * 1 = cel / flat — the dots become hard flat discs and the chase snaps into a
31
+ * single posterized "lit" dot stepping along the row.
32
+ */
33
+
34
+ import {
35
+ GLSL_CONSTANTS,
36
+ GLSL_DITHER,
37
+ GLSL_HASH,
38
+ GLSL_PALETTE_MIX,
39
+ GLSL_TONEMAP_ACES,
40
+ } from "@dopaminefx/core";
41
+
42
+ /** Compile-time cap on the dot count: BOTH the GLSL `#define MAX_DOTS` (below)
43
+ * and the integer-clamp const the `.dope` mapping references (`MAX_DOTS`). */
44
+ export const MAX_DOTS = 7;
45
+
46
+ export const DOTS_VERTEX_SRC = /* glsl */ `#version 300 es
47
+ out vec2 vUv;
48
+ void main() {
49
+ // Single full-screen triangle from gl_VertexID — no vertex buffers needed.
50
+ vec2 pos = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
51
+ vUv = pos;
52
+ gl_Position = vec4(pos * 2.0 - 1.0, 0.0, 1.0);
53
+ }`;
54
+
55
+ export const DOTS_FRAGMENT_SRC = /* glsl */ `#version 300 es
56
+ precision highp float;
57
+ out vec4 fragColor;
58
+
59
+ uniform vec2 uResolution; // device pixels
60
+ uniform vec2 uOrigin; // row centre, gl coords (y up)
61
+ uniform float uAmp; // STEADY periodic breathe gate (~0.85..1.0), not an envelope
62
+ uniform float uPhase; // normalized loop phase [0,1) (tempo.loop) — drives ALL motion
63
+ uniform float uLoopS; // seconds within the current loop (the dither's temporal seed)
64
+ uniform float uExposure;
65
+ uniform float uDotCount; // number of dots in the row (clamped to MAX_DOTS)
66
+ uniform float uDotRadius; // dot radius as a fraction of min viewport dim
67
+ uniform float uDotGap; // centre-to-centre spacing as a fraction of min viewport dim
68
+ uniform float uBreathe; // 0..1 breathe depth (radius/brightness sine swing)
69
+ uniform float uChase; // 0..1+ sharpness of the traveling pulse (tighter = livelier)
70
+ uniform float uGlow; // 0..1 ambient under-glow brightness
71
+ uniform float uStyle; // 0..1 photoreal soft glow -> cel/flat discs (whimsy)
72
+ uniform float uShadow; // 0 = light pass (screen), 1 = shadow pass (multiply)
73
+ uniform vec2 uShadowOffset; // device-px offset of the cast silhouette (away from light)
74
+ uniform float uShadowSoft; // penumbra softness in device px (blur tap radius)
75
+ uniform float uShadowStrength;// 0..1 max darkening of the multiply layer
76
+ uniform vec3 uC0; // dot core color
77
+ uniform vec3 uC1; // mid
78
+ uniform vec3 uC2; // pulse accent
79
+
80
+ #define MAX_DOTS ${MAX_DOTS}
81
+
82
+ ${GLSL_CONSTANTS}
83
+ ${GLSL_HASH}
84
+ ${GLSL_PALETTE_MIX}
85
+ ${GLSL_TONEMAP_ACES}
86
+ ${GLSL_DITHER}
87
+
88
+ // The live count, clamped to the compile-time cap so the loop bound is constant.
89
+ int dotCount(){
90
+ return int(clamp(uDotCount, 1.0, float(MAX_DOTS)) + 0.5);
91
+ }
92
+
93
+ // The breathing dot radius. One slow sine per loop period swings the radius by
94
+ // ±uBreathe·radius around the base — the gentle pulse "breath". Periodic in
95
+ // uPhase, so it returns to its t=0 value at every seam.
96
+ float liveRadius(){
97
+ return uDotRadius * (1.0 + sin(TAU * uPhase) * uBreathe * 0.45);
98
+ }
99
+
100
+ // The normalized x-centre of dot i: a row of dotCount dots, spaced uDotGap apart,
101
+ // centred on the origin (so the whole row is symmetric about x = 0).
102
+ float dotCenterX(int i, int count){
103
+ float c = float(count);
104
+ return (float(i) - (c - 1.0) * 0.5) * uDotGap;
105
+ }
106
+
107
+ // 0..1 "lit" weight of dot i under the traveling pulse. The pulse HEAD winds one
108
+ // full turn around the row per period (head = fract(uPhase) -> seamless); each
109
+ // dot lights as the head sweeps over its slot, with a comet-like falloff set by
110
+ // uChase. Wrapped distance keeps it continuous across the seam.
111
+ float pulseLit(int i, int count){
112
+ float head = fract(uPhase) * float(count); // 0..count head position
113
+ float d = abs(float(i) + 0.5 - head);
114
+ d = min(d, float(count) - d); // wrap around the row
115
+ float sharp = mix(0.9, 2.4, clamp(uChase, 0.0, 1.5) / 1.5);
116
+ return exp(-(d * d) * sharp);
117
+ }
118
+
119
+ // The whole row's emitted light at a fragment.
120
+ vec3 dotsLight(vec2 frag, float minDim){
121
+ vec2 rel = (frag - uOrigin) / minDim; // normalized, row-centred (y up)
122
+ int count = dotCount();
123
+ float radius = max(liveRadius(), 1e-3);
124
+
125
+ // The breathe also gently modulates overall brightness (brighter on the inhale).
126
+ float breatheB = 1.0 + sin(TAU * uPhase) * uBreathe * 0.5;
127
+ float gain = uAmp * uExposure * breatheB;
128
+
129
+ vec3 col = vec3(0.0);
130
+ for (int i = 0; i < MAX_DOTS; i++) {
131
+ if (i >= count) break;
132
+ float cx = dotCenterX(i, count);
133
+ vec2 dpos = rel - vec2(cx, 0.0);
134
+ float dist = length(dpos);
135
+
136
+ // Per-dot hue register: stops walk along the row (C0 -> C1 -> C2), so the row
137
+ // reads as a coherent gradient rather than identical clones.
138
+ float tcol = (count > 1) ? float(i) / float(count - 1) : 0.5;
139
+ vec3 dotCol = paletteMix(clamp(tcol, 0.0, 1.0));
140
+
141
+ // The pulse brightens whichever dot the head is sweeping over.
142
+ float lit = pulseLit(i, count);
143
+ float bright = 0.35 + 0.65 * lit; // dim base, bright under the pulse
144
+
145
+ // 1. CORE: a soft gaussian dot.
146
+ float cov = exp(-(dist * dist) / (2.0 * radius * radius));
147
+ col += dotCol * cov * gain * bright;
148
+
149
+ // 2. GLOW: a wider, dim ambient halo under each dot.
150
+ float gr = radius * 2.6;
151
+ float glow = exp(-(dist * dist) / (2.0 * gr * gr));
152
+ col += mix(dotCol, uC2, lit * 0.4) * glow * uGlow * gain * 0.3 * bright;
153
+ }
154
+ return col;
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // SHADOW silhouette — the dots are small floating discs, so each casts a faint
159
+ // soft occlusion. Sample the row coverage at the offset shadow point and darken
160
+ // in proportion, kept subtle (small discs throw little shadow).
161
+ float dotsOcclusion(vec2 frag, float minDim){
162
+ vec2 rel = (frag - uOrigin) / minDim;
163
+ int count = dotCount();
164
+ float radius = max(liveRadius(), 1e-3);
165
+ float occ = 0.0;
166
+ for (int i = 0; i < MAX_DOTS; i++) {
167
+ if (i >= count) break;
168
+ vec2 dpos = rel - vec2(dotCenterX(i, count), 0.0);
169
+ float dist = length(dpos);
170
+ occ = max(occ, exp(-(dist * dist) / (2.0 * radius * radius)));
171
+ }
172
+ return clamp(occ * uAmp, 0.0, 1.0);
173
+ }
174
+
175
+ vec4 dotsShadowColor(vec2 frag, float minDim){
176
+ vec2 sp = frag - uShadowOffset;
177
+ float soft = uShadowSoft;
178
+ float occ = dotsOcclusion(sp, minDim);
179
+ occ += dotsOcclusion(sp + vec2( soft, 0.0), minDim);
180
+ occ += dotsOcclusion(sp + vec2(-soft, 0.0), minDim);
181
+ occ += dotsOcclusion(sp + vec2(0.0, soft), minDim);
182
+ occ += dotsOcclusion(sp + vec2(0.0, -soft), minDim);
183
+ occ /= 5.0;
184
+ float dark = clamp(occ, 0.0, 1.0) * uShadowStrength * 0.5;
185
+ vec3 tint = mix(vec3(1.0), 0.6 + 0.4 * normalize(uC0 + 1e-3), 0.2);
186
+ vec3 mul = mix(vec3(1.0), tint, dark);
187
+ return vec4(mul, 1.0);
188
+ }
189
+
190
+ void main(){
191
+ vec2 frag = gl_FragCoord.xy;
192
+ vec2 res = uResolution;
193
+ float minDim = min(res.x, res.y);
194
+
195
+ if (uShadow > 0.5) {
196
+ fragColor = dotsShadowColor(frag, minDim);
197
+ return;
198
+ }
199
+
200
+ vec3 col = dotsLight(frag, minDim);
201
+
202
+ // ---- Tone + finishing ----
203
+ col = tonemapACES(col * 0.95);
204
+
205
+ // ---- Non-photoreal pass: cel / flat discs (whimsy). ----
206
+ // Toward the cel end the soft gaussians become hard flat DISCS and the chase
207
+ // snaps to a single posterized "lit" dot. The pass-runner already steps the
208
+ // clock "on twos" (periodic — tempo.loop tiles the grid — so the loop seam
209
+ // survives); here we flatten the tone.
210
+ if (uStyle > 0.001) {
211
+ vec2 rel = (frag - uOrigin) / minDim;
212
+ int count = dotCount();
213
+ float radius = max(liveRadius(), 1e-3);
214
+ float breatheB = 1.0 + sin(TAU * uPhase) * uBreathe * 0.5;
215
+ float gain = uAmp * uExposure * breatheB;
216
+ vec3 cel = vec3(0.0);
217
+ for (int i = 0; i < MAX_DOTS; i++) {
218
+ if (i >= count) break;
219
+ vec2 dpos = rel - vec2(dotCenterX(i, count), 0.0);
220
+ float dist = length(dpos);
221
+ float disc = 1.0 - smoothstep(radius * 0.85, radius, dist); // hard flat disc
222
+ float tcol = (count > 1) ? float(i) / float(count - 1) : 0.5;
223
+ vec3 dotCol = clamp(paletteMix(clamp(tcol, 0.0, 1.0)) * 1.25, 0.0, 1.2);
224
+ float lit = pulseLit(i, count);
225
+ float litStep = step(0.6, lit); // one dot lit at a time
226
+ cel += dotCol * disc * (0.4 + 0.6 * litStep)
227
+ + mix(uC2, vec3(1.0), 0.5) * disc * litStep * 0.4;
228
+ }
229
+ cel *= gain;
230
+ col = mix(col, cel, uStyle);
231
+ }
232
+
233
+ // Ordered dither (~1/255) to kill banding the screen blend reveals; faded out
234
+ // toward the cel end where hard discs are intended. Seeded by the LOOP clock,
235
+ // so even the dither field repeats exactly at the seam.
236
+ col = ditherAdd(col, frag, uLoopS, 1.0 - uStyle);
237
+
238
+ fragColor = vec4(max(col, 0.0), 1.0);
239
+ }`;
@@ -0,0 +1,331 @@
1
+ {
2
+ "fmt": "dopamine-effect",
3
+ "v": "1.0.0",
4
+ "id": "dopamine.loading.dots",
5
+ "meta": {
6
+ "name": "Dots",
7
+ "description": "A calm ambient 'thinking' indicator: a centred row of soft luminous dots that gently breathe while a brightness PULSE travels across them in sequence (the classic typing / loading read). Dopamine's second CONTINUOUS effect — like halo it LOOPS SEAMLESSLY via the first-class tempo.loop contract: all motion is driven by the standard periodic clocks uPhase/uLoopS the runners derive from tempo.loop.periodMs (1000 ms). The parser validates the seam invariants (the period is exactly 12 'animate-on-twos' steps, NPR_TIME_STEP_MS = 1000/12, and durationMs 4000 is exactly 4 periods), so the frame at t==durationMs matches t==0 at every whimsy. The conductor re-arms at durationMs until the host stops the play handle; amp is a steady gentle periodic breathe of phase (NOT envelope(life)), so there is no one-shot fade to break the seam. Fully declarative — no swift/ or android/ folder; the toolchain generates the Metal + Kotlin factories and shaders from this one .dope plus the single GLSL ES 3.00 source.",
8
+ "tags": [
9
+ "loading",
10
+ "ambient",
11
+ "continuous",
12
+ "looping",
13
+ "thinking"
14
+ ]
15
+ },
16
+ "controls": {
17
+ "mood": {
18
+ "type": "enum",
19
+ "default": "celebratory",
20
+ "options": [
21
+ "serene",
22
+ "celebratory",
23
+ "electric"
24
+ ],
25
+ "ui": "segmented"
26
+ },
27
+ "intensity": {
28
+ "type": "scalar",
29
+ "default": 0.7,
30
+ "min": 0,
31
+ "max": 1,
32
+ "step": 0.01,
33
+ "ui": "slider"
34
+ },
35
+ "whimsy": {
36
+ "type": "scalar",
37
+ "default": 0.5,
38
+ "min": 0,
39
+ "max": 1,
40
+ "step": 0.01,
41
+ "ui": "slider"
42
+ },
43
+ "seed": {
44
+ "type": "int",
45
+ "default": null,
46
+ "nullable": true
47
+ },
48
+ "origin": {
49
+ "type": "point",
50
+ "default": "center"
51
+ },
52
+ "target": {
53
+ "type": "selector",
54
+ "default": "document.body"
55
+ }
56
+ },
57
+ "baselines": {
58
+ "serene": {
59
+ "durationMs": 4000,
60
+ "lightness": 0.86,
61
+ "chroma": 0.08,
62
+ "hueCenter": 225,
63
+ "hueRange": 110,
64
+ "dotCount": 3,
65
+ "dotRadius": 0.032,
66
+ "dotGap": 0.11,
67
+ "breathe": 0.16,
68
+ "chase": 0.6,
69
+ "glow": 0.5
70
+ },
71
+ "celebratory": {
72
+ "durationMs": 4000,
73
+ "lightness": 0.82,
74
+ "chroma": 0.15,
75
+ "hueCenter": 200,
76
+ "hueRange": 280,
77
+ "dotCount": 4,
78
+ "dotRadius": 0.03,
79
+ "dotGap": 0.1,
80
+ "breathe": 0.2,
81
+ "chase": 0.85,
82
+ "glow": 0.65
83
+ },
84
+ "electric": {
85
+ "durationMs": 4000,
86
+ "lightness": 0.8,
87
+ "chroma": 0.22,
88
+ "hueCenter": 195,
89
+ "hueRange": 160,
90
+ "dotCount": 5,
91
+ "dotRadius": 0.026,
92
+ "dotGap": 0.092,
93
+ "breathe": 0.24,
94
+ "chase": 1.1,
95
+ "glow": 0.9
96
+ }
97
+ },
98
+ "palette": {
99
+ "model": "oklch",
100
+ "space": "linear-srgb",
101
+ "generator": "golden-angle",
102
+ "goldenAngleDeg": 137.50776405003785,
103
+ "stops": 3,
104
+ "hueSpread": 0.55,
105
+ "lightness": {
106
+ "baseline": "lightness",
107
+ "perStop": [
108
+ 0,
109
+ 0.06,
110
+ -0.05
111
+ ]
112
+ },
113
+ "chroma": {
114
+ "from": {
115
+ "mul": [
116
+ {
117
+ "baseline": "chroma"
118
+ },
119
+ {
120
+ "lerp": [
121
+ "intensity",
122
+ 0.7,
123
+ 1.5
124
+ ]
125
+ }
126
+ ]
127
+ },
128
+ "perStop": [
129
+ 0,
130
+ 0.02,
131
+ -0.01
132
+ ]
133
+ },
134
+ "seed": {
135
+ "deterministic": true,
136
+ "source": "controls.seed",
137
+ "prng": "mulberry32"
138
+ },
139
+ "perMood": {
140
+ "serene": {
141
+ "hueCenter": 225,
142
+ "hueRange": 110,
143
+ "lightness": 0.86,
144
+ "chroma": 0.08
145
+ },
146
+ "celebratory": {
147
+ "hueCenter": 200,
148
+ "hueRange": 280,
149
+ "lightness": 0.82,
150
+ "chroma": 0.15
151
+ },
152
+ "electric": {
153
+ "hueCenter": 195,
154
+ "hueRange": 160,
155
+ "lightness": 0.8,
156
+ "chroma": 0.22
157
+ }
158
+ }
159
+ },
160
+ "tempo": {
161
+ "durationMs": {
162
+ "from": {
163
+ "baseline": "durationMs"
164
+ }
165
+ },
166
+ "loop": {
167
+ "periodMs": 1000
168
+ },
169
+ "frame": {
170
+ "amp": {
171
+ "add": [
172
+ 0.85,
173
+ {
174
+ "mul": [
175
+ 0.15,
176
+ {
177
+ "sin": {
178
+ "mul": [
179
+ 6.283185307179586,
180
+ {
181
+ "input": "phase"
182
+ }
183
+ ]
184
+ }
185
+ }
186
+ ]
187
+ }
188
+ ]
189
+ },
190
+ "extras": {}
191
+ },
192
+ "reducedMotion": {
193
+ "peakMs": 0,
194
+ "holdMs": 600
195
+ }
196
+ },
197
+ "render": {
198
+ "params": {
199
+ "exposure": {
200
+ "type": "float",
201
+ "from": {
202
+ "lerp": [
203
+ "intensity",
204
+ 0.7,
205
+ 1.45
206
+ ]
207
+ }
208
+ },
209
+ "dotCount": {
210
+ "type": "int",
211
+ "from": {
212
+ "round": {
213
+ "baseline": "dotCount"
214
+ }
215
+ },
216
+ "clampMax": "MAX_DOTS",
217
+ "clampMin": "MIN_DOTS"
218
+ },
219
+ "dotRadius": {
220
+ "type": "float",
221
+ "from": {
222
+ "mul": [
223
+ {
224
+ "baseline": "dotRadius"
225
+ },
226
+ {
227
+ "lerp": [
228
+ "intensity",
229
+ 1.2,
230
+ 0.82
231
+ ]
232
+ }
233
+ ]
234
+ }
235
+ },
236
+ "dotGap": {
237
+ "type": "float",
238
+ "from": {
239
+ "baseline": "dotGap"
240
+ }
241
+ },
242
+ "breathe": {
243
+ "type": "float",
244
+ "from": {
245
+ "baseline": "breathe"
246
+ }
247
+ },
248
+ "chase": {
249
+ "type": "float",
250
+ "from": {
251
+ "mul": [
252
+ {
253
+ "baseline": "chase"
254
+ },
255
+ {
256
+ "lerp": [
257
+ "intensity",
258
+ 0.8,
259
+ 1.25
260
+ ]
261
+ }
262
+ ]
263
+ }
264
+ },
265
+ "glow": {
266
+ "type": "float",
267
+ "from": {
268
+ "mul": [
269
+ {
270
+ "baseline": "glow"
271
+ },
272
+ {
273
+ "lerp": [
274
+ "intensity",
275
+ 0.6,
276
+ 1.3
277
+ ]
278
+ }
279
+ ]
280
+ }
281
+ },
282
+ "style": {
283
+ "type": "float",
284
+ "from": {
285
+ "control": "whimsy"
286
+ }
287
+ }
288
+ },
289
+ "shadowHeightFrac": {
290
+ "min": [
291
+ {
292
+ "add": [
293
+ {
294
+ "param": "dotRadius"
295
+ },
296
+ 0.04
297
+ ]
298
+ },
299
+ 1
300
+ ]
301
+ },
302
+ "consts": {
303
+ "MAX_DOTS": 7,
304
+ "MIN_DOTS": 2
305
+ },
306
+ "config": {
307
+ "usesOrigin": true
308
+ },
309
+ "backends": {
310
+ "webgl2": {
311
+ "stage": "fullscreen-triangle",
312
+ "blend": "screen",
313
+ "shader": {
314
+ "program": "dots"
315
+ }
316
+ }
317
+ },
318
+ "fallbackOrder": [
319
+ "webgl2"
320
+ ]
321
+ },
322
+ "binding": {
323
+ "note": "CROSS-PLATFORM uniform-binding contract; SHIPS in the portable .dope. dotsSeed feeds the seeded palette only (no scatterWeb — the shader reads no seed uniform). dotCount is an integer uniform the shader reads to lay out the row. style is whimsy (set automatically), never a uniform.",
324
+ "excludeParams": [
325
+ "style"
326
+ ],
327
+ "scatterKey": "dotsSeed",
328
+ "extras": [],
329
+ "samplers": []
330
+ }
331
+ }