@dopaminefx/effect-fail 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,523 @@
1
+ {
2
+ "fmt": "dopamine-effect",
3
+ "v": "1.0.0",
4
+ "id": "dopamine.error.fail",
5
+ "meta": {
6
+ "name": "Fail Stamp",
7
+ "description": "A failure/denied moment — the emotional opposite of the success effects. A red/amber cross is stamped in over a recoiling error flare with a sharp hit + shake, then desaturates and collapses. Short and punchy.",
8
+ "tags": [
9
+ "error",
10
+ "fail",
11
+ "denied",
12
+ "negative"
13
+ ]
14
+ },
15
+ "controls": {
16
+ "mood": {
17
+ "type": "enum",
18
+ "label": "Mood",
19
+ "default": "error",
20
+ "options": [
21
+ "try-again",
22
+ "error",
23
+ "denied"
24
+ ],
25
+ "ui": "segmented"
26
+ },
27
+ "intensity": {
28
+ "type": "scalar",
29
+ "label": "Severity",
30
+ "default": 0.7,
31
+ "min": 0,
32
+ "max": 1,
33
+ "step": 0.01,
34
+ "ui": "slider",
35
+ "help": "How hard the failure lands: flare heat/size, shake amount, recoil."
36
+ },
37
+ "whimsy": {
38
+ "type": "scalar",
39
+ "label": "Whimsy",
40
+ "default": 0.5,
41
+ "min": 0,
42
+ "max": 1,
43
+ "step": 0.01,
44
+ "ui": "slider",
45
+ "help": "Photoreal flare (0) to desaturated RGB-split glitch collapse (1)."
46
+ },
47
+ "seed": {
48
+ "type": "int",
49
+ "label": "Seed",
50
+ "default": null,
51
+ "nullable": true,
52
+ "help": "Null = unique error palette per fire; pin to reproduce."
53
+ },
54
+ "origin": {
55
+ "type": "point",
56
+ "label": "Origin",
57
+ "default": "center"
58
+ },
59
+ "target": {
60
+ "type": "selector",
61
+ "label": "Target",
62
+ "default": "document.body"
63
+ }
64
+ },
65
+ "baselines": {
66
+ "try-again": {
67
+ "durationMs": 900,
68
+ "lightness": 0.78,
69
+ "chroma": 0.13,
70
+ "hueCenter": 70,
71
+ "hueRange": 40,
72
+ "shakeAmount": 0.5
73
+ },
74
+ "error": {
75
+ "durationMs": 760,
76
+ "lightness": 0.72,
77
+ "chroma": 0.17,
78
+ "hueCenter": 40,
79
+ "hueRange": 36,
80
+ "shakeAmount": 0.85
81
+ },
82
+ "denied": {
83
+ "durationMs": 640,
84
+ "lightness": 0.66,
85
+ "chroma": 0.21,
86
+ "hueCenter": 22,
87
+ "hueRange": 30,
88
+ "shakeAmount": 1.25
89
+ }
90
+ },
91
+ "palette": {
92
+ "model": "oklch",
93
+ "space": "linear-srgb",
94
+ "generator": "golden-angle",
95
+ "goldenAngleDeg": 137.50776405003785,
96
+ "stops": 3,
97
+ "hueSpread": 0.18,
98
+ "lightness": {
99
+ "baseline": "lightness",
100
+ "perStop": [
101
+ 0,
102
+ 0.06,
103
+ -0.05
104
+ ]
105
+ },
106
+ "chroma": {
107
+ "from": {
108
+ "mul": [
109
+ {
110
+ "baseline": "chroma"
111
+ },
112
+ {
113
+ "lerp": [
114
+ "intensity",
115
+ 0.8,
116
+ 1.4
117
+ ]
118
+ }
119
+ ]
120
+ },
121
+ "perStop": [
122
+ 0,
123
+ 0.02,
124
+ -0.01
125
+ ]
126
+ },
127
+ "seed": {
128
+ "deterministic": true,
129
+ "source": "controls.seed",
130
+ "prng": "mulberry32",
131
+ "note": "Algorithmic OKLCH biased to error reds/ambers: a narrow hueRange around a hot hueCenter keeps every fire inside the red/amber error band while still being generated (unique per fire)."
132
+ },
133
+ "perMood": {
134
+ "try-again": {
135
+ "hueCenter": 70,
136
+ "hueRange": 40,
137
+ "lightness": 0.78,
138
+ "chroma": 0.13
139
+ },
140
+ "error": {
141
+ "hueCenter": 40,
142
+ "hueRange": 36,
143
+ "lightness": 0.72,
144
+ "chroma": 0.17
145
+ },
146
+ "denied": {
147
+ "hueCenter": 22,
148
+ "hueRange": 30,
149
+ "lightness": 0.66,
150
+ "chroma": 0.21
151
+ }
152
+ }
153
+ },
154
+ "tempo": {
155
+ "durationMs": {
156
+ "from": {
157
+ "round": {
158
+ "mul": [
159
+ {
160
+ "baseline": "durationMs"
161
+ },
162
+ {
163
+ "lerp": [
164
+ "intensity",
165
+ 1.15,
166
+ 0.85
167
+ ]
168
+ }
169
+ ]
170
+ }
171
+ }
172
+ },
173
+ "frame": {
174
+ "amp": {
175
+ "lt": [
176
+ {
177
+ "clamp01": {
178
+ "input": "life"
179
+ }
180
+ },
181
+ 0.05,
182
+ {
183
+ "easeOutCubic": {
184
+ "div": [
185
+ {
186
+ "clamp01": {
187
+ "input": "life"
188
+ }
189
+ },
190
+ 0.05
191
+ ]
192
+ }
193
+ },
194
+ {
195
+ "lt": [
196
+ {
197
+ "clamp01": {
198
+ "input": "life"
199
+ }
200
+ },
201
+ 0.55,
202
+ 1,
203
+ {
204
+ "pow": [
205
+ {
206
+ "clamp01": {
207
+ "sub": [
208
+ 1,
209
+ {
210
+ "div": [
211
+ {
212
+ "sub": [
213
+ {
214
+ "clamp01": {
215
+ "input": "life"
216
+ }
217
+ },
218
+ 0.55
219
+ ]
220
+ },
221
+ 0.45
222
+ ]
223
+ }
224
+ ]
225
+ }
226
+ },
227
+ 1.7
228
+ ]
229
+ }
230
+ ]
231
+ }
232
+ ]
233
+ },
234
+ "extras": {
235
+ "stamp": {
236
+ "sub": [
237
+ 1,
238
+ {
239
+ "pow": [
240
+ {
241
+ "sub": [
242
+ 1,
243
+ {
244
+ "clamp01": {
245
+ "div": [
246
+ {
247
+ "input": "elapsedMs"
248
+ },
249
+ 170
250
+ ]
251
+ }
252
+ }
253
+ ]
254
+ },
255
+ 5
256
+ ]
257
+ }
258
+ ]
259
+ },
260
+ "shake": {
261
+ "lt": [
262
+ 0,
263
+ {
264
+ "input": "elapsedMs"
265
+ },
266
+ {
267
+ "mul": [
268
+ {
269
+ "sin": {
270
+ "mul": [
271
+ {
272
+ "div": [
273
+ {
274
+ "input": "elapsedMs"
275
+ },
276
+ 300
277
+ ]
278
+ },
279
+ 3.141592653589793,
280
+ 7
281
+ ]
282
+ }
283
+ },
284
+ {
285
+ "exp": {
286
+ "div": [
287
+ {
288
+ "sub": [
289
+ 0,
290
+ {
291
+ "input": "elapsedMs"
292
+ }
293
+ ]
294
+ },
295
+ {
296
+ "mul": [
297
+ 300,
298
+ 0.35
299
+ ]
300
+ }
301
+ ]
302
+ }
303
+ },
304
+ {
305
+ "param": "shakeAmount"
306
+ }
307
+ ]
308
+ },
309
+ 0
310
+ ]
311
+ }
312
+ }
313
+ },
314
+ "reducedMotion": {
315
+ "peakMs": 200,
316
+ "holdMs": 320
317
+ },
318
+ "note": "The negative, punchy counterpart to the held-breath success envelope: frame.amp is the slam/hold/collapse fail envelope; stamp (the ✗ slash-in over 170 ms) and shake (a damped ~3.5-oscillation recoil over 300 ms) run on the REAL un-stepped clock (elapsedMs) on every platform."
319
+ },
320
+ "geometry": {
321
+ "kind": "radial",
322
+ "viewBox": [
323
+ 0,
324
+ 0,
325
+ 100,
326
+ 100
327
+ ],
328
+ "outlines": {
329
+ "cross": {
330
+ "role": "deny-glyph",
331
+ "source": "baked-sdf",
332
+ "note": "GEOMETRY SEAM: the ✗ icon is driven by this svgPath, baked to the inline SDF below (scripts/bake-sdf.mjs). The fail shader samples it (uSdfTex). Swap the path + re-bake to change the rejected icon with no shader edit. Two diagonal bars (a clean cross).",
333
+ "svgPath": "M 24 24 L 76 76 M 76 24 L 24 76",
334
+ "sdf": {
335
+ "size": 64,
336
+ "range": 18,
337
+ "viewBox": [
338
+ 0,
339
+ 0,
340
+ 100,
341
+ 100
342
+ ],
343
+ "data": "data:application/octet-stream;base64,RFMAQP/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////59PHw8vX6///////////////////////////////////69fLw8fT5/////////////////////////////vPq497b2tzf5e32//////////////////////////////bt5d/c2tve4+rz/v//////////////////////+Ovg1s7IxcTGytDY4+/8/////////////////////////O/j2NDKxsTFyM7W4Ov4////////////////////9ufZzcK5s6+usLS7xdDd6/r/////////////////////+uvd0MW7tLCur7O5ws3Z5/b/////////////////+OfXyLqupZ2ZmJqfp7G+zNvr+v//////////////////+uvbzL6xp5+amJmdpa66yNfn+P///////////////uvZyLepm5CIg4KEipOfrbzL2+v6////////////////+uvby7ytn5OKhIKDiJCbqbfI2ev+//////////////PgzbqpmIl9c21sbnaAjZ2svMvb6/r/////////////+uvby7ysnY2Adm5sbXN9iZipus3g8//////////////q1sKum4l5al9YVVlibn2Nnay8y9vr+v//////////+uvby7ysnY19bmJZVVhfanmJm67C1ur////////////54865pZB9alpMQj9ET15ufY2drLzL2+v6////////+uvby7ysnY19bl5PRD9CTFpqfZCluc7j+f//////////9N7Is52Ic19MOi0pMD9OXm59jZ2svMvb6/r/////+uvby7ysnY19bl5OPzApLTpMX3OInbPI3vT///////////Hbxa+Zg21YQi0bEx8vP05ebn2Nnay8y9vr+v//+uvby7ysnY19bl5OPy8fExstQlhtg5mvxdvx///////////w2sSumIJsVT8pEwAQHy8/Tl5ufY2drLzL2+v6+uvby7ysnY19bl5OPy8fEAATKT9VbIKYrsTa8P//////////8tzGsJqEbllEMB8QABAfLz9OXm59jZ2svMvb6+vby7ysnY19bl5OPy8fEAAQHzBEWW6EmrDG3PL///////////XfyrSfinZiTz8vHxAAEB8vP05ebn2Nnay8y9vby7ysnY19bl5OPy8fEAAQHy8/T2J2ip+0yt/1///////////65dC7p5OAbl5OPy8fEAAQHy8/Tl5ufY2drLzLy7ysnY19bl5OPy8fEAAQHy8/Tl5ugJOnu9Dl+v///////////+3YxbGfjX1uXk4/Lx8QABAfLz9OXm59jZ2svLysnY19bl5OPy8fEAAQHy8/Tl5ufY2fscXY7f/////////////249C+rZ2NfW5eTj8vHxAAEB8vP05ebn2NnaysnY19bl5OPy8fEAAQHy8/Tl5ufY2drb7Q4/b//////////////+/dzLysnY19bl5OPy8fEAAQHy8/Tl5ufY2dnY19bl5OPy8fEAAQHy8/Tl5ufY2drLzM3e/////////////////869vLvKydjX1uXk4/Lx8QABAfLz9OXm59jY19bl5OPy8fEAAQHy8/Tl5ufY2drLzL2+v8//////////////////rr28u8rJ2NfW5eTj8vHxAAEB8vP05ebn19bl5OPy8fEAAQHy8/Tl5ufY2drLzL2+v6////////////////////+uvby7ysnY19bl5OPy8fEAAQHy8/Tl5ubl5OPy8fEAAQHy8/Tl5ufY2drLzL2+v6///////////////////////669vLvKydjX1uXk4/Lx8QABAfLz9OXl5OPy8fEAAQHy8/Tl5ufY2drLzL2+v6//////////////////////////rr28u8rJ2NfW5eTj8vHxAAEB8vP05OPy8fEAAQHy8/Tl5ufY2drLzL2+v6////////////////////////////+uvby7ysnY19bl5OPy8fEAAQHy8/Py8fEAAQHy8/Tl5ufY2drLzL2+v6///////////////////////////////669vLvKydjX1uXk4/Lx8QABAfLy8fEAAQHy8/Tl5ufY2drLzL2+v6//////////////////////////////////rr28u8rJ2NfW5eTj8vHxAAEB8fEAAQHy8/Tl5ufY2drLzL2+v6////////////////////////////////////+uvby7ysnY19bl5OPy8fEAAQEAAQHy8/Tl5ufY2drLzL2+v6///////////////////////////////////////669vLvKydjX1uXk4/Lx8QAAAQHy8/Tl5ufY2drLzL2+v6////////////////////////////////////////+uvby7ysnY19bl5OPy8fEAAAEB8vP05ebn2Nnay8y9vr+v//////////////////////////////////////+uvby7ysnY19bl5OPy8fEAAQEAAQHy8/Tl5ufY2drLzL2+v6////////////////////////////////////+uvby7ysnY19bl5OPy8fEAAQHx8QABAfLz9OXm59jZ2svMvb6/r/////////////////////////////////+uvby7ysnY19bl5OPy8fEAAQHy8vHxAAEB8vP05ebn2Nnay8y9vr+v//////////////////////////////+uvby7ysnY19bl5OPy8fEAAQHy8/Py8fEAAQHy8/Tl5ufY2drLzL2+v6////////////////////////////+uvby7ysnY19bl5OPy8fEAAQHy8/Tk4/Lx8QABAfLz9OXm59jZ2svMvb6/r/////////////////////////+uvby7ysnY19bl5OPy8fEAAQHy8/Tl5eTj8vHxAAEB8vP05ebn2Nnay8y9vr+v//////////////////////+uvby7ysnY19bl5OPy8fEAAQHy8/Tl5ubl5OPy8fEAAQHy8/Tl5ufY2drLzL2+v6////////////////////+uvby7ysnY19bl5OPy8fEAAQHy8/Tl5ufX1uXk4/Lx8QABAfLz9OXm59jZ2svMvb6/r//////////////////Ovby7ysnY19bl5OPy8fEAAQHy8/Tl5ufY2NfW5eTj8vHxAAEB8vP05ebn2Nnay8y9vr/P///////////////+/dzLysnY19bl5OPy8fEAAQHy8/Tl5ufY2dnY19bl5OPy8fEAAQHy8/Tl5ufY2drLzM3e////////////////bj0L6tnY19bl5OPy8fEAAQHy8/Tl5ufY2drKydjX1uXk4/Lx8QABAfLz9OXm59jZ2tvtDj9v/////////////t2MWxn419bl5OPy8fEAAQHy8/Tl5ufY2drLy8rJ2NfW5eTj8vHxAAEB8vP05ebn2Nn7HF2O3////////////65dC7p5OAbl5OPy8fEAAQHy8/Tl5ufY2drLzLy7ysnY19bl5OPy8fEAAQHy8/Tl5ugJOnu9Dl+v//////////9d/KtJ+KdmJPPy8fEAAQHy8/Tl5ufY2drLzL29vLvKydjX1uXk4/Lx8QABAfLz9PYnaKn7TK3/X///////////LcxrCahG5ZRDAfEAAQHy8/Tl5ufY2drLzL2+vr28u8rJ2NfW5eTj8vHxAAEB8wRFluhJqwxtzy///////////w2sSumIJsVT8pEwAQHy8/Tl5ufY2drLzL2+v6+uvby7ysnY19bl5OPy8fEAATKT9VbIKYrsTa8P//////////8dvFr5mDbVhCLRsTHy8/Tl5ufY2drLzL2+v6///669vLvKydjX1uXk4/Lx8TGy1CWG2Dma/F2/H///////////TeyLOdiHNfTDotKTA/Tl5ufY2drLzL2+v6//////rr28u8rJ2NfW5eTj8wKS06TF9ziJ2zyN70///////////54865pZB9alpMQj9ET15ufY2drLzL2+v6////////+uvby7ysnY19bl5PRD9CTFpqfZCluc7j+f///////////+rWwq6biXlqX1hVWWJufY2drLzL2+v6///////////669vLvKydjX1uYllVWF9qeYmbrsLW6v/////////////z4M26qZiJfXNtbG52gI2drLzL2+v6//////////////rr28u8rJ2NgHZubG1zfYmYqbrN4PP//////////////uvZyLepm5CIg4KEipOfrbzL2+v6////////////////+uvby7ytn5OKhIKDiJCbqbfI2ev+///////////////459fIuq6lnZmYmp+nsb7M2+v6///////////////////669vMvrGnn5qYmZ2lrrrI1+f4//////////////////bn2c3CubOvrrC0u8XQ3ev6//////////////////////rr3dDFu7Swrq+zucLN2ef2////////////////////+Ovg1s7IxcTGytDY4+/8/////////////////////////O/j2NDKxsTFyM7W4Ov4///////////////////////+8+rj3tva3N/l7fb/////////////////////////////9u3l39za297j6vP+////////////////////////////+fTx8PL1+v//////////////////////////////////+vXy8PH0+f////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8="
344
+ }
345
+ }
346
+ }
347
+ },
348
+ "content": {
349
+ "note": "No word set for the fail effect — the ✗ cross is the whole message. Kept here for symmetry with the other effects' content blocks.",
350
+ "tokens": [
351
+ "✗"
352
+ ]
353
+ },
354
+ "render": {
355
+ "params": {
356
+ "exposure": {
357
+ "type": "float",
358
+ "from": {
359
+ "lerp": [
360
+ "intensity",
361
+ 0.9,
362
+ 1.6
363
+ ]
364
+ }
365
+ },
366
+ "severity": {
367
+ "type": "float",
368
+ "from": {
369
+ "control": "intensity"
370
+ }
371
+ },
372
+ "shakeAmount": {
373
+ "type": "float",
374
+ "from": {
375
+ "mul": [
376
+ {
377
+ "baseline": "shakeAmount"
378
+ },
379
+ {
380
+ "lerp": [
381
+ "intensity",
382
+ 0.7,
383
+ 1.3
384
+ ]
385
+ }
386
+ ]
387
+ }
388
+ },
389
+ "style": {
390
+ "type": "float",
391
+ "from": {
392
+ "control": "whimsy"
393
+ }
394
+ }
395
+ },
396
+ "shadowHeightFrac": 0.42,
397
+ "pass": {
398
+ "note": "PER-PASS uniforms (evaluated once per pass, never per frame): the ✗ box half-size + SDF stroke/range in device px, sized to the TARGETED element (targetMinDimPx falls back to the full canvas when untargeted). sdfRange/sdfViewBoxW come from the cross sampler's baked-SDF metadata; sdfOn is deliberately NOT here — the web aux-texture binding flips it, natives keep the analytic ✗ at 0.",
399
+ "boxPx": {
400
+ "mul": [
401
+ 0.15,
402
+ {
403
+ "input": "targetMinDimPx"
404
+ }
405
+ ]
406
+ },
407
+ "sdfStrokePx": {
408
+ "mul": [
409
+ 0.15,
410
+ {
411
+ "input": "targetMinDimPx"
412
+ },
413
+ 0.13
414
+ ]
415
+ },
416
+ "sdfRangePx": {
417
+ "mul": [
418
+ {
419
+ "input": "sdfRange"
420
+ },
421
+ {
422
+ "div": [
423
+ {
424
+ "mul": [
425
+ 2,
426
+ {
427
+ "mul": [
428
+ 0.15,
429
+ {
430
+ "input": "targetMinDimPx"
431
+ }
432
+ ]
433
+ }
434
+ ]
435
+ },
436
+ {
437
+ "max": [
438
+ {
439
+ "input": "sdfViewBoxW"
440
+ },
441
+ 0.000001
442
+ ]
443
+ }
444
+ ]
445
+ }
446
+ ]
447
+ }
448
+ },
449
+ "consts": {},
450
+ "config": {
451
+ "usesOrigin": true
452
+ },
453
+ "backends": {
454
+ "webgl2": {
455
+ "stage": "fullscreen-triangle",
456
+ "blend": "screen",
457
+ "shader": {
458
+ "program": "fail"
459
+ }
460
+ }
461
+ },
462
+ "fallbackOrder": [
463
+ "webgl2"
464
+ ]
465
+ },
466
+ "binding": {
467
+ "note": "CROSS-PLATFORM uniform-binding contract. Which render.params are NOT shader uniforms, the seed-keyed scatter field (no scatterWeb — the fail shader reads no seed uniform), 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.",
468
+ "excludeParams": [
469
+ "style",
470
+ "shakeAmount",
471
+ "seed"
472
+ ],
473
+ "scatterKey": "failSeed",
474
+ "extras": [
475
+ {
476
+ "name": "stamp",
477
+ "type": "float",
478
+ "web": "uStamp",
479
+ "note": "stampProgress(animMs)"
480
+ },
481
+ {
482
+ "name": "shake",
483
+ "type": "float",
484
+ "web": "uShake",
485
+ "note": "signed recoil shake (-1..1)"
486
+ },
487
+ {
488
+ "name": "sdfOn",
489
+ "type": "float",
490
+ "web": "uSdfOn",
491
+ "note": "1 = drive the cross from the baked SDF"
492
+ },
493
+ {
494
+ "name": "sdfRangePx",
495
+ "type": "float",
496
+ "web": "uSdfRangePx",
497
+ "note": "device px mapping to the SDF 0..1 range"
498
+ },
499
+ {
500
+ "name": "sdfStrokePx",
501
+ "type": "float",
502
+ "web": "uSdfStrokePx",
503
+ "note": "half stroke width (device px)"
504
+ },
505
+ {
506
+ "name": "boxPx",
507
+ "type": "float",
508
+ "web": "uBoxPx",
509
+ "note": "half-size (device px) of the ✗ box"
510
+ }
511
+ ],
512
+ "samplers": [
513
+ {
514
+ "web": "uSdfTex",
515
+ "name": "sdfTex",
516
+ "texture": 1,
517
+ "outline": "cross",
518
+ "on": "sdfOn",
519
+ "note": "Declarative SDF source: the baked geometry.outlines.cross SDF binds here on web (flipping the sdfOn extra to 1); the native runtimes don't bind aux textures yet — sdfOn stays 0 and the shader's analytic two-bar ✗ renders."
520
+ }
521
+ ]
522
+ }
523
+ }
package/src/index.ts ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Fail / error effect — the emotional OPPOSITE of the three success effects.
3
+ *
4
+ * A red/amber ✗ cross is STAMPED in over a recoiling error flare with a sharp
5
+ * hit + damped shake, then desaturates and collapses. Short and punchy, not a
6
+ * celebratory bloom.
7
+ *
8
+ * FULLY DATA-DRIVEN: the params/palette/tempo come from fail.dope.json via the
9
+ * loader; the per-frame logic — the slam/hold/collapse `amp`, the 170 ms stamp
10
+ * and the damped recoil shake — is `tempo.frame` (stamp/shake run on the REAL
11
+ * un-stepped `elapsedMs`, matching the Swift/Android ports); and the ✗ plumbing
12
+ * that used to be code hooks is now data too: `render.pass` declares the
13
+ * box/stroke/range pixel uniforms (sized to `targetMinDimPx`) and the
14
+ * `binding.samplers` `outline`/`on` source declares the baked-SDF aux texture
15
+ * (geometry seam). The only hand-written web sources left are this
16
+ * registration shim (the fail moods are web-runtime-only) and the shader.
17
+ */
18
+
19
+ import { FAIL_FRAGMENT_SRC, FAIL_VERTEX_SRC } from "./fail-shader.js";
20
+ import {
21
+ registerDopeEffect,
22
+ registerMood,
23
+ parseDope,
24
+ type RGB,
25
+ type EffectFactory,
26
+ type PassParams,
27
+ } from "@dopaminefx/core";
28
+ import doc from "./fail.dope.json";
29
+
30
+ const DOPE = parseDope(doc as object);
31
+
32
+ /** Register the fail-appropriate moods so they light up the registry (energy). */
33
+ registerMood("try-again", { hueCenter: 70, hueRange: 40, lightness: 0.78, chroma: 0.13, energy: 0.2 });
34
+ registerMood("error", { hueCenter: 40, hueRange: 36, lightness: 0.72, chroma: 0.17, energy: 0.55 });
35
+ registerMood("denied", { hueCenter: 22, hueRange: 30, lightness: 0.66, chroma: 0.21, energy: 1.0 });
36
+
37
+ /** The fail render params (the loader bag + the typed fields the shader reads). */
38
+ export interface FailParams extends PassParams {
39
+ seed: number;
40
+ palette: [RGB, RGB, RGB];
41
+ exposure: number;
42
+ severity: number;
43
+ shakeAmount: number;
44
+ style: number;
45
+ failSeed: number;
46
+ }
47
+
48
+ // The whole factory (resolve / frame / pass uniforms / SDF aux texture /
49
+ // shadow / bindings / program registration) is data: fail.dope.json
50
+ // interpreted by the core backbone.
51
+ export const fail = registerDopeEffect(DOPE, {
52
+ vertex: FAIL_VERTEX_SRC,
53
+ fragment: FAIL_FRAGMENT_SRC,
54
+ }) as EffectFactory<PassParams> as EffectFactory<FailParams>;
55
+
56
+ export default fail;