@dopaminefx/core 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.
Files changed (136) hide show
  1. package/dist/engine/color.d.ts +71 -0
  2. package/dist/engine/color.d.ts.map +1 -0
  3. package/dist/engine/color.js +107 -0
  4. package/dist/engine/color.js.map +1 -0
  5. package/dist/engine/context.d.ts +54 -0
  6. package/dist/engine/context.d.ts.map +1 -0
  7. package/dist/engine/context.js +0 -0
  8. package/dist/engine/context.js.map +1 -0
  9. package/dist/engine/gl.d.ts +9 -0
  10. package/dist/engine/gl.d.ts.map +1 -0
  11. package/dist/engine/gl.js +39 -0
  12. package/dist/engine/gl.js.map +1 -0
  13. package/dist/engine/look/glsl.d.ts +95 -0
  14. package/dist/engine/look/glsl.d.ts.map +1 -0
  15. package/dist/engine/look/glsl.js +171 -0
  16. package/dist/engine/look/glsl.js.map +1 -0
  17. package/dist/engine/look/particles.glsl.d.ts +21 -0
  18. package/dist/engine/look/particles.glsl.d.ts.map +1 -0
  19. package/dist/engine/look/particles.glsl.js +44 -0
  20. package/dist/engine/look/particles.glsl.js.map +1 -0
  21. package/dist/engine/sdf.d.ts +77 -0
  22. package/dist/engine/sdf.d.ts.map +1 -0
  23. package/dist/engine/sdf.js +255 -0
  24. package/dist/engine/sdf.js.map +1 -0
  25. package/dist/engine/seed.d.ts +10 -0
  26. package/dist/engine/seed.d.ts.map +1 -0
  27. package/dist/engine/seed.js +20 -0
  28. package/dist/engine/seed.js.map +1 -0
  29. package/dist/engine/shadow.d.ts +41 -0
  30. package/dist/engine/shadow.d.ts.map +1 -0
  31. package/dist/engine/shadow.js +39 -0
  32. package/dist/engine/shadow.js.map +1 -0
  33. package/dist/engine/tempo.d.ts +33 -0
  34. package/dist/engine/tempo.d.ts.map +1 -0
  35. package/dist/engine/tempo.js +51 -0
  36. package/dist/engine/tempo.js.map +1 -0
  37. package/dist/framework/conductor.d.ts +100 -0
  38. package/dist/framework/conductor.d.ts.map +1 -0
  39. package/dist/framework/conductor.js +493 -0
  40. package/dist/framework/conductor.js.map +1 -0
  41. package/dist/framework/content.d.ts +67 -0
  42. package/dist/framework/content.d.ts.map +1 -0
  43. package/dist/framework/content.js +72 -0
  44. package/dist/framework/content.js.map +1 -0
  45. package/dist/framework/dope-pass.d.ts +131 -0
  46. package/dist/framework/dope-pass.d.ts.map +1 -0
  47. package/dist/framework/dope-pass.js +346 -0
  48. package/dist/framework/dope-pass.js.map +1 -0
  49. package/dist/framework/dope-zip.d.ts +22 -0
  50. package/dist/framework/dope-zip.d.ts.map +1 -0
  51. package/dist/framework/dope-zip.js +116 -0
  52. package/dist/framework/dope-zip.js.map +1 -0
  53. package/dist/framework/effect.d.ts +128 -0
  54. package/dist/framework/effect.d.ts.map +1 -0
  55. package/dist/framework/effect.js +19 -0
  56. package/dist/framework/effect.js.map +1 -0
  57. package/dist/framework/frame-expr.d.ts +124 -0
  58. package/dist/framework/frame-expr.d.ts.map +1 -0
  59. package/dist/framework/frame-expr.js +135 -0
  60. package/dist/framework/frame-expr.js.map +1 -0
  61. package/dist/framework/load-effect.d.ts +77 -0
  62. package/dist/framework/load-effect.d.ts.map +1 -0
  63. package/dist/framework/load-effect.js +135 -0
  64. package/dist/framework/load-effect.js.map +1 -0
  65. package/dist/framework/loader.d.ts +309 -0
  66. package/dist/framework/loader.d.ts.map +1 -0
  67. package/dist/framework/loader.js +266 -0
  68. package/dist/framework/loader.js.map +1 -0
  69. package/dist/framework/mood-registry.d.ts +58 -0
  70. package/dist/framework/mood-registry.d.ts.map +1 -0
  71. package/dist/framework/mood-registry.js +58 -0
  72. package/dist/framework/mood-registry.js.map +1 -0
  73. package/dist/framework/panel-runner.d.ts +96 -0
  74. package/dist/framework/panel-runner.d.ts.map +1 -0
  75. package/dist/framework/panel-runner.js +137 -0
  76. package/dist/framework/panel-runner.js.map +1 -0
  77. package/dist/framework/pass-common.d.ts +97 -0
  78. package/dist/framework/pass-common.d.ts.map +1 -0
  79. package/dist/framework/pass-common.js +178 -0
  80. package/dist/framework/pass-common.js.map +1 -0
  81. package/dist/framework/pass-runner.d.ts +183 -0
  82. package/dist/framework/pass-runner.d.ts.map +1 -0
  83. package/dist/framework/pass-runner.js +212 -0
  84. package/dist/framework/pass-runner.js.map +1 -0
  85. package/dist/framework/programs.d.ts +54 -0
  86. package/dist/framework/programs.d.ts.map +1 -0
  87. package/dist/framework/programs.js +33 -0
  88. package/dist/framework/programs.js.map +1 -0
  89. package/dist/framework/registry.d.ts +29 -0
  90. package/dist/framework/registry.d.ts.map +1 -0
  91. package/dist/framework/registry.js +38 -0
  92. package/dist/framework/registry.js.map +1 -0
  93. package/dist/framework/runtime.d.ts +19 -0
  94. package/dist/framework/runtime.d.ts.map +1 -0
  95. package/dist/framework/runtime.js +37 -0
  96. package/dist/framework/runtime.js.map +1 -0
  97. package/dist/index.d.ts +63 -0
  98. package/dist/index.d.ts.map +1 -0
  99. package/dist/index.js +126 -0
  100. package/dist/index.js.map +1 -0
  101. package/dist/overlay.d.ts +46 -0
  102. package/dist/overlay.d.ts.map +1 -0
  103. package/dist/overlay.js +79 -0
  104. package/dist/overlay.js.map +1 -0
  105. package/dist/types.d.ts +68 -0
  106. package/dist/types.d.ts.map +1 -0
  107. package/dist/types.js +10 -0
  108. package/dist/types.js.map +1 -0
  109. package/package.json +37 -0
  110. package/src/engine/color.ts +154 -0
  111. package/src/engine/context.ts +0 -0
  112. package/src/engine/gl.ts +46 -0
  113. package/src/engine/look/glsl.ts +183 -0
  114. package/src/engine/look/particles.glsl.ts +44 -0
  115. package/src/engine/sdf.ts +298 -0
  116. package/src/engine/seed.ts +23 -0
  117. package/src/engine/shadow.ts +66 -0
  118. package/src/engine/tempo.ts +54 -0
  119. package/src/framework/conductor.ts +604 -0
  120. package/src/framework/content.ts +113 -0
  121. package/src/framework/dope-pass.ts +432 -0
  122. package/src/framework/dope-zip.ts +125 -0
  123. package/src/framework/effect.ts +127 -0
  124. package/src/framework/frame-expr.ts +217 -0
  125. package/src/framework/load-effect.ts +204 -0
  126. package/src/framework/loader.ts +502 -0
  127. package/src/framework/mood-registry.ts +87 -0
  128. package/src/framework/panel-runner.ts +233 -0
  129. package/src/framework/pass-common.ts +222 -0
  130. package/src/framework/pass-runner.ts +391 -0
  131. package/src/framework/programs.ts +62 -0
  132. package/src/framework/registry.ts +44 -0
  133. package/src/framework/runtime.ts +38 -0
  134. package/src/index.ts +227 -0
  135. package/src/overlay.ts +109 -0
  136. package/src/types.ts +63 -0
package/dist/index.js ADDED
@@ -0,0 +1,126 @@
1
+ /**
2
+ * @dopaminefx/core — the EFFECT-FREE runtime + public API.
3
+ *
4
+ * This is the slim runtime: the conductor (overlay + shared program-cached GL
5
+ * contexts + RAF loop), the registry, the mood registry, the `.dope` loader +
6
+ * `loadEffect`, the generic runners (pass + panel), the shared engine bits
7
+ * (color, sdf, shadow, seed, context, gl, the `look/` GLSL chunks, tempo
8
+ * PRIMITIVES) and the generic `play(name, …)` / `prepare(name, …)` API.
9
+ *
10
+ * Core imports + registers NO effect. Each effect ships as its own
11
+ * `@dopaminefx/effect-<name>` package that depends on this and self-registers on
12
+ * import; the `@dopaminefx/effects` umbrella bundles all nine + the `celebrate*`
13
+ * conveniences + the `<dopamine-success>` element.
14
+ */
15
+ import { play as conductorPlay, prepare as conductorPrepare, } from "./framework/conductor.js";
16
+ import { getEffect } from "./framework/registry.js";
17
+ import { parseBackdrop } from "./engine/color.js";
18
+ import { randomSeed } from "./engine/seed.js";
19
+ import { isBrowser } from "./framework/runtime.js";
20
+ export { buildPalette, oklchToLinearSrgb, wrapHue, GOLDEN_ANGLE_DEG } from "./engine/color.js";
21
+ export { mulberry32, randomSeed } from "./engine/seed.js";
22
+ export { registerEffect, getEffect, hasEffect, effectNames } from "./framework/registry.js";
23
+ export { registerMood, resolveMood, hasMood, moodNames, } from "./framework/mood-registry.js";
24
+ export { teardown } from "./framework/conductor.js";
25
+ export { loadEffect, loadEffectSync, } from "./framework/load-effect.js";
26
+ export { registerProgram, getProgram, programNames } from "./framework/programs.js";
27
+ export { parseDope, resolveDopeParams, getOutline, } from "./framework/loader.js";
28
+ export { dopePassConfig, dopePanelConfig, registerDopeEffect, registerDopePanelEffect, } from "./framework/dope-pass.js";
29
+ export { evalFrameExpr, evalParamExpr, evalPassExpr, } from "./framework/frame-expr.js";
30
+ export { pickFromList, pickBand, resolveTypography, } from "./framework/content.js";
31
+ export { bakeSdf, decodeSdf, parseSvgPath } from "./engine/sdf.js";
32
+ // The generic runners — for authoring new pure-shader / Canvas2D-panel effects.
33
+ export { createPassInstance, } from "./framework/pass-runner.js";
34
+ export { createPanelInstance, } from "./framework/panel-runner.js";
35
+ // Tempo PRIMITIVES — the generic easing/envelope building blocks effects build
36
+ // their bespoke timing on top of (each effect's bespoke envelope lives in its
37
+ // own package's `<name>-tempo.ts`).
38
+ export { clamp01, easeOutCubic, easeOutBack, envelope, NPR_TIME_STEP_MS, } from "./engine/tempo.js";
39
+ // The shared GLSL "look" chunks — reusable shader fragments (hash, fbm, palette
40
+ // mix, tonemap, dither, halftone, …) an effect's shader composes into its source.
41
+ export * from "./engine/look/glsl.js";
42
+ export { GLSL_PARTICLES } from "./engine/look/particles.glsl.js";
43
+ const DEFAULTS = { mood: "celebratory", intensity: 0.7, whimsy: 0.5 };
44
+ /**
45
+ * Resolve the shared options into a target, a feeling, and an overlay-local
46
+ * anchor. Effects that aren't anchored (Verdict, Comic) simply ignore the anchor.
47
+ */
48
+ function resolveRequest(effect, options) {
49
+ const factory = getEffect(effect);
50
+ if (!factory)
51
+ throw new Error(`dopamine: unknown effect "${effect}"`);
52
+ const target = options.target ?? document.body;
53
+ const seed = options.seed ?? randomSeed();
54
+ const feeling = {
55
+ mood: options.mood ?? DEFAULTS.mood,
56
+ intensity: options.intensity ?? DEFAULTS.intensity,
57
+ whimsy: options.whimsy ?? DEFAULTS.whimsy,
58
+ seed,
59
+ };
60
+ const rect = target.getBoundingClientRect();
61
+ const origin = options.origin ?? {
62
+ x: rect.left + rect.width / 2,
63
+ y: rect.top + rect.height / 2,
64
+ };
65
+ const anchor = target === document.body || target === document.documentElement
66
+ ? origin
67
+ : { x: origin.x - rect.left, y: origin.y - rect.top };
68
+ // The element box the centrepiece is sized to (CSS px). Defaults to the target's
69
+ // own rect, so the centrepiece matches whatever element was fired on; an explicit
70
+ // `targetSize` lets a caller match a child element under a full-page overlay.
71
+ const targetSize = options.targetSize ?? { width: rect.width, height: rect.height };
72
+ // A `backdrop` colour opts into surface-aware compositing (visible on light /
73
+ // arbitrary surfaces); we keep only its luminance — the colour itself isn't a
74
+ // shader input (the light layer composites source-over against the live page).
75
+ const bd = options.backdrop ? parseBackdrop(options.backdrop) : null;
76
+ const composite = bd ? { luminance: bd.luminance } : null;
77
+ return { factory, target, anchor, targetSize, feeling, composite };
78
+ }
79
+ /**
80
+ * Generic real-time fire: play a registered effect by name. Resolves when the
81
+ * animation has fully played out. A CONTINUOUS effect (one whose `.dope`
82
+ * declares `tempo.loop`, e.g. halo) loops seamlessly until the host calls the
83
+ * returned handle's `stop()`. The handle's `pause()`/`resume()` freeze and
84
+ * resume the timeline drift-free (parking a perpetual loop so it costs no
85
+ * battery; the conductor also auto-pauses on a hidden tab). SSR-safe (resolves
86
+ * immediately off-DOM). The effect must already be registered (import
87
+ * `@dopaminefx/effect-<name>` or the `@dopaminefx/effects` umbrella, or load one
88
+ * via `loadEffect`).
89
+ */
90
+ export function play(effect, options = {}) {
91
+ const noop = Object.assign(Promise.resolve(), { stop() { }, pause() { }, resume() { } });
92
+ if (!isBrowser())
93
+ return noop;
94
+ const req = resolveRequest(effect, options);
95
+ if (!req || !req.factory)
96
+ return noop;
97
+ return conductorPlay({
98
+ factory: req.factory,
99
+ target: req.target,
100
+ anchor: req.anchor,
101
+ targetSize: req.targetSize,
102
+ feeling: req.feeling,
103
+ composite: req.composite,
104
+ });
105
+ }
106
+ /**
107
+ * Generic prepared effect: mount the overlay and return a renderer you drive
108
+ * yourself via `renderAt(elapsedMs)`. Call `dispose()` when finished. Returns
109
+ * `null` in non-DOM environments.
110
+ */
111
+ export function prepare(effect, options = {}) {
112
+ if (!isBrowser())
113
+ return null;
114
+ const req = resolveRequest(effect, options);
115
+ if (!req || !req.factory)
116
+ return null;
117
+ return conductorPrepare({
118
+ factory: req.factory,
119
+ target: req.target,
120
+ anchor: req.anchor,
121
+ targetSize: req.targetSize,
122
+ feeling: req.feeling,
123
+ composite: req.composite,
124
+ });
125
+ }
126
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Full-bleed overlay host. Creates fixed, click-through canvases layered over
3
+ * the target.
4
+ *
5
+ * Two stacked compositing layers give the effect real physical presence:
6
+ *
7
+ * - LIGHT layer (`mix-blend-mode: screen`): black pixels leave content
8
+ * untouched, bright pixels lighten it — this is what makes the effect cast
9
+ * coloured light onto the UI beneath.
10
+ * - SHADOW layer (`mix-blend-mode: multiply`): white pixels leave content
11
+ * untouched, dark pixels darken it — a soft, offset occlusion silhouette of
12
+ * the effect's bright forms, so the effect reads as floating ABOVE the page
13
+ * and throwing shadow into it, not just glowing on top of it.
14
+ *
15
+ * The shadow layer sits BENEATH the light layer in z-order, so the bright core
16
+ * always wins where the two overlap (the shadow is pushed out to the edges /
17
+ * away from the light, which is physically what an offset penumbra does).
18
+ *
19
+ * Back-compat: `createOverlay(target)` still returns an object whose `.canvas`
20
+ * is the single light canvas and `.destroy()` tears everything down — existing
21
+ * single-canvas callers are unaffected. Pass `{ shadow: true }` to additionally
22
+ * get a `shadow` canvas (`overlay.shadow`).
23
+ */
24
+ export interface Overlay {
25
+ /** The light-casting canvas (`mix-blend-mode: screen`). */
26
+ canvas: HTMLCanvasElement;
27
+ /**
28
+ * The shadow-casting canvas (`mix-blend-mode: multiply`), present only when
29
+ * the overlay was created with `{ shadow: true }`.
30
+ */
31
+ shadow?: HTMLCanvasElement;
32
+ /**
33
+ * Lazily create (or return the existing) shadow canvas, inserting it beneath
34
+ * the light layer. Lets a persistent overlay gain a shadow layer when a later
35
+ * effect needs one without recreating the whole overlay.
36
+ */
37
+ ensureShadow: () => HTMLCanvasElement;
38
+ /** Remove the overlay (all layers) from the DOM. */
39
+ destroy: () => void;
40
+ }
41
+ export interface OverlayOptions {
42
+ /** Also create a multiply "shadow" layer beneath the light layer. */
43
+ shadow?: boolean;
44
+ }
45
+ export declare function createOverlay(target: HTMLElement, options?: OverlayOptions): Overlay;
46
+ //# sourceMappingURL=overlay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../src/overlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,WAAW,OAAO;IACtB,2DAA2D;IAC3D,MAAM,EAAE,iBAAiB,CAAC;IAC1B;;;OAGG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B;;;;OAIG;IACH,YAAY,EAAE,MAAM,iBAAiB,CAAC;IACtC,oDAAoD;IACpD,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAwBD,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAuCxF"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Full-bleed overlay host. Creates fixed, click-through canvases layered over
3
+ * the target.
4
+ *
5
+ * Two stacked compositing layers give the effect real physical presence:
6
+ *
7
+ * - LIGHT layer (`mix-blend-mode: screen`): black pixels leave content
8
+ * untouched, bright pixels lighten it — this is what makes the effect cast
9
+ * coloured light onto the UI beneath.
10
+ * - SHADOW layer (`mix-blend-mode: multiply`): white pixels leave content
11
+ * untouched, dark pixels darken it — a soft, offset occlusion silhouette of
12
+ * the effect's bright forms, so the effect reads as floating ABOVE the page
13
+ * and throwing shadow into it, not just glowing on top of it.
14
+ *
15
+ * The shadow layer sits BENEATH the light layer in z-order, so the bright core
16
+ * always wins where the two overlap (the shadow is pushed out to the edges /
17
+ * away from the light, which is physically what an offset penumbra does).
18
+ *
19
+ * Back-compat: `createOverlay(target)` still returns an object whose `.canvas`
20
+ * is the single light canvas and `.destroy()` tears everything down — existing
21
+ * single-canvas callers are unaffected. Pass `{ shadow: true }` to additionally
22
+ * get a `shadow` canvas (`overlay.shadow`).
23
+ */
24
+ const LIGHT_Z = "2147483646";
25
+ // One below the light layer so the bright core composites over the shadow.
26
+ const SHADOW_Z = "2147483645";
27
+ function styleCanvas(canvas, blend, zIndex, scoped) {
28
+ const s = canvas.style;
29
+ s.position = scoped ? "absolute" : "fixed";
30
+ s.inset = "0";
31
+ s.width = "100%";
32
+ s.height = "100%";
33
+ s.pointerEvents = "none";
34
+ s.zIndex = zIndex;
35
+ s.mixBlendMode = blend;
36
+ s.display = "block";
37
+ canvas.setAttribute("aria-hidden", "true");
38
+ }
39
+ export function createOverlay(target, options = {}) {
40
+ const scoped = target !== document.body && target !== document.documentElement;
41
+ if (scoped) {
42
+ const cs = getComputedStyle(target);
43
+ if (cs.position === "static")
44
+ target.style.position = "relative";
45
+ }
46
+ // Shadow layer is created (and appended) first so it sits beneath the light
47
+ // layer both in z-index and DOM order.
48
+ let shadow;
49
+ const makeShadow = () => {
50
+ const s = document.createElement("canvas");
51
+ styleCanvas(s, "multiply", SHADOW_Z, scoped);
52
+ s.dataset.dopamine = "shadow";
53
+ // Insert at the front so it sits beneath the (later-appended) light canvas.
54
+ target.insertBefore(s, target.firstChild);
55
+ return s;
56
+ };
57
+ if (options.shadow)
58
+ shadow = makeShadow();
59
+ const canvas = document.createElement("canvas");
60
+ styleCanvas(canvas, "screen", LIGHT_Z, scoped);
61
+ canvas.dataset.dopamine = "solarbloom";
62
+ target.appendChild(canvas);
63
+ return {
64
+ canvas,
65
+ get shadow() {
66
+ return shadow;
67
+ },
68
+ ensureShadow() {
69
+ if (!shadow)
70
+ shadow = makeShadow();
71
+ return shadow;
72
+ },
73
+ destroy: () => {
74
+ canvas.remove();
75
+ shadow?.remove();
76
+ },
77
+ };
78
+ }
79
+ //# sourceMappingURL=overlay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlay.js","sourceRoot":"","sources":["../src/overlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAyBH,MAAM,OAAO,GAAG,YAAY,CAAC;AAC7B,2EAA2E;AAC3E,MAAM,QAAQ,GAAG,YAAY,CAAC;AAE9B,SAAS,WAAW,CAClB,MAAyB,EACzB,KAA4B,EAC5B,MAAc,EACd,MAAe;IAEf,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC;IACvB,CAAC,CAAC,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC;IAC3C,CAAC,CAAC,KAAK,GAAG,GAAG,CAAC;IACd,CAAC,CAAC,KAAK,GAAG,MAAM,CAAC;IACjB,CAAC,CAAC,MAAM,GAAG,MAAM,CAAC;IAClB,CAAC,CAAC,aAAa,GAAG,MAAM,CAAC;IACzB,CAAC,CAAC,MAAM,GAAG,MAAM,CAAC;IAClB,CAAC,CAAC,YAAY,GAAG,KAAK,CAAC;IACvB,CAAC,CAAC,OAAO,GAAG,OAAO,CAAC;IACpB,MAAM,CAAC,YAAY,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAmB,EAAE,UAA0B,EAAE;IAC7E,MAAM,MAAM,GAAG,MAAM,KAAK,QAAQ,CAAC,IAAI,IAAI,MAAM,KAAK,QAAQ,CAAC,eAAe,CAAC;IAC/E,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,EAAE,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;QACpC,IAAI,EAAE,CAAC,QAAQ,KAAK,QAAQ;YAAE,MAAM,CAAC,KAAK,CAAC,QAAQ,GAAG,UAAU,CAAC;IACnE,CAAC;IAED,4EAA4E;IAC5E,uCAAuC;IACvC,IAAI,MAAqC,CAAC;IAC1C,MAAM,UAAU,GAAG,GAAsB,EAAE;QACzC,MAAM,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAC3C,WAAW,CAAC,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC7C,CAAC,CAAC,OAAO,CAAC,QAAQ,GAAG,QAAQ,CAAC;QAC9B,4EAA4E;QAC5E,MAAM,CAAC,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;QAC1C,OAAO,CAAC,CAAC;IACX,CAAC,CAAC;IACF,IAAI,OAAO,CAAC,MAAM;QAAE,MAAM,GAAG,UAAU,EAAE,CAAC;IAE1C,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;IAChD,WAAW,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;IAC/C,MAAM,CAAC,OAAO,CAAC,QAAQ,GAAG,YAAY,CAAC;IACvC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IAE3B,OAAO;QACL,MAAM;QACN,IAAI,MAAM;YACR,OAAO,MAAM,CAAC;QAChB,CAAC;QACD,YAAY;YACV,IAAI,CAAC,MAAM;gBAAE,MAAM,GAAG,UAAU,EAAE,CAAC;YACnC,OAAO,MAAM,CAAC;QAChB,CAAC;QACD,OAAO,EAAE,GAAG,EAAE;YACZ,MAAM,CAAC,MAAM,EAAE,CAAC;YAChB,MAAM,EAAE,MAAM,EAAE,CAAC;QACnB,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Public types for Dopamine's success effect.
3
+ *
4
+ * The whole point of the API is that callers choose a *feeling* — a mood, how
5
+ * intense it should be, and how much whimsy — rather than tuning low-level
6
+ * particle counts and easing curves. Those get derived internally (see
7
+ * `engine/mood.ts`).
8
+ */
9
+ /** Emotional register of the celebration. */
10
+ export type DopamineMood = "serene" | "celebratory" | "electric";
11
+ export interface DopamineSuccessOptions {
12
+ /**
13
+ * Emotional register. Default `"celebratory"`. A built-in success mood, or any
14
+ * mood registered via `registerMood` (e.g. the fail effect's `try-again` /
15
+ * `error` / `denied`).
16
+ */
17
+ mood?: DopamineMood | (string & {});
18
+ /**
19
+ * How strong the reward feels, 0..1. Drives saturation, brightness, bloom
20
+ * size, mote count and overshoot — grounded in the finding that saturated,
21
+ * bright color raises both arousal and positive valence. Default `0.7`.
22
+ */
23
+ intensity?: number;
24
+ /**
25
+ * How playful/organic the motion is, 0..1. Widens the hue spread and the
26
+ * turbulence of the drifting motes. Default `0.5`.
27
+ */
28
+ whimsy?: number;
29
+ /**
30
+ * Seed for the algorithmic color + motion. Omit to get a fresh, unique
31
+ * palette every fire (the variable-reward / novelty lever). Provide a fixed
32
+ * value for reproducible output (e.g. tests, snapshots).
33
+ */
34
+ seed?: number;
35
+ /** Origin of the bloom in viewport pixels. Default: center of `target`. */
36
+ origin?: {
37
+ x: number;
38
+ y: number;
39
+ };
40
+ /**
41
+ * Size (CSS px) of the element the effect's centrepiece (checkmark, ✗, comic
42
+ * word, hero heart, ink gesture) is sized to. Default: the `target`'s own box.
43
+ * Set this to match a CHILD element while the overlay still covers the page.
44
+ */
45
+ targetSize?: {
46
+ width: number;
47
+ height: number;
48
+ };
49
+ /**
50
+ * Element the full-bleed overlay is mounted over. Default `document.body`,
51
+ * i.e. the whole page. Light is cast (via `mix-blend-mode`) onto whatever
52
+ * sits beneath the overlay.
53
+ */
54
+ target?: HTMLElement;
55
+ /**
56
+ * The page colour the effect composites against, as any CSS colour string
57
+ * (e.g. `"#ffffff"`, `"rgb(20 24 37)"`, a named colour). Omit (the default)
58
+ * for the classic dark compositing: the light layer uses
59
+ * `mix-blend-mode: screen`, which is rich on a dark UI but mathematically
60
+ * invisible on white. Pass the actual surface colour and the runtime switches
61
+ * the light layer to PREMULTIPLIED source-over light — visible on ANY surface,
62
+ * white included — and strengthens the multiply shadow as the surface
63
+ * lightens. Use this whenever the effect plays over a light or unknown-colour
64
+ * background.
65
+ */
66
+ backdrop?: string;
67
+ }
68
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,6CAA6C;AAC7C,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,aAAa,GAAG,UAAU,CAAC;AAEjE,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,IAAI,CAAC,EAAE,YAAY,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;IACpC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,MAAM,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAClC;;;;OAIG;IACH,UAAU,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C;;;;OAIG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
package/dist/types.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Public types for Dopamine's success effect.
3
+ *
4
+ * The whole point of the API is that callers choose a *feeling* — a mood, how
5
+ * intense it should be, and how much whimsy — rather than tuning low-level
6
+ * particle counts and easing curves. Those get derived internally (see
7
+ * `engine/mood.ts`).
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@dopaminefx/core",
3
+ "version": "0.1.0",
4
+ "description": "Framework-agnostic slim runtime + shared engine for Dopamine visual effects (vanilla TS + WebGL2). Effects ship as separate @dopaminefx/effect-* packages.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "src"
18
+ ],
19
+ "sideEffects": false,
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.json"
22
+ },
23
+ "license": "MIT",
24
+ "author": "10in30",
25
+ "homepage": "https://github.com/10in30/dopamine#readme",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/10in30/dopamine.git",
29
+ "directory": "packages/core"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/10in30/dopamine/issues"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Algorithmic color in OKLCH.
3
+ *
4
+ * OKLCH is perceptually uniform, so walking hue by the golden angle (137.5°)
5
+ * yields palettes that are always harmonious yet never repeat — the novelty
6
+ * that keeps a reward from habituating. Lightness/chroma come from the mood
7
+ * (saturated + bright == higher arousal *and* positive valence).
8
+ *
9
+ * We hand the shader *linear* sRGB, because light should be summed in linear
10
+ * space; sRGB gamma is only for talking to CSS.
11
+ */
12
+
13
+ import type { Rng } from "./seed.js";
14
+
15
+ /** Linear sRGB, nominally 0..1 (may exceed before clamping). */
16
+ export interface RGB {
17
+ r: number;
18
+ g: number;
19
+ b: number;
20
+ }
21
+
22
+ export interface OKLCH {
23
+ /** Perceptual lightness, 0..1. */
24
+ L: number;
25
+ /** Chroma (colorfulness), ~0..0.4. */
26
+ C: number;
27
+ /** Hue in degrees, 0..360. */
28
+ h: number;
29
+ }
30
+
31
+ export const GOLDEN_ANGLE_DEG = 137.50776405003785;
32
+
33
+ const clamp01 = (x: number): number => (x < 0 ? 0 : x > 1 ? 1 : x);
34
+
35
+ /** Positive modulo into [0, 360). */
36
+ export const wrapHue = (h: number): number => ((h % 360) + 360) % 360;
37
+
38
+ /**
39
+ * OKLCH → linear sRGB (Björn Ottosson's OKLab matrices). Result is gamut-clamped
40
+ * to [0, 1] per channel.
41
+ */
42
+ export function oklchToLinearSrgb({ L, C, h }: OKLCH): RGB {
43
+ const hr = (h * Math.PI) / 180;
44
+ const a = C * Math.cos(hr);
45
+ const b = C * Math.sin(hr);
46
+
47
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
48
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
49
+ const s_ = L - 0.0894841775 * a - 1.291485548 * b;
50
+
51
+ const l = l_ * l_ * l_;
52
+ const m = m_ * m_ * m_;
53
+ const s = s_ * s_ * s_;
54
+
55
+ return {
56
+ r: clamp01(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s),
57
+ g: clamp01(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s),
58
+ b: clamp01(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s),
59
+ };
60
+ }
61
+
62
+ export interface PaletteParams {
63
+ /** Base lightness for the stops. */
64
+ lightness: number;
65
+ /** Base chroma for the stops. */
66
+ chroma: number;
67
+ /** Center of the hue range this mood prefers, in degrees. */
68
+ hueCenter: number;
69
+ /** Width of the random hue range around the center, in degrees. */
70
+ hueRange: number;
71
+ /** 0..1 — how far the golden-angle stops fan out from the base hue. */
72
+ hueSpread: number;
73
+ }
74
+
75
+ /**
76
+ * Build a 3-stop linear-RGB palette. The base hue is drawn from `rng` (so an
77
+ * un-pinned seed gives a unique palette each fire), biased toward the mood's
78
+ * preferred range. Successive stops step by the golden angle, scaled by whimsy.
79
+ * Lightness and chroma breathe slightly across the stops for depth.
80
+ */
81
+ export function buildPalette(rng: Rng, p: PaletteParams): RGB[] {
82
+ const baseHue = wrapHue(p.hueCenter + (rng() - 0.5) * p.hueRange);
83
+ const step = GOLDEN_ANGLE_DEG * (0.35 + 0.65 * p.hueSpread);
84
+ const lightSteps = [0.0, 0.06, -0.05];
85
+ const chromaSteps = [0.0, 0.02, -0.01];
86
+
87
+ return [0, 1, 2].map((i) =>
88
+ oklchToLinearSrgb({
89
+ L: clamp01(p.lightness + lightSteps[i]!),
90
+ C: Math.max(0, p.chroma + chromaSteps[i]!),
91
+ h: wrapHue(baseHue + step * i),
92
+ }),
93
+ );
94
+ }
95
+
96
+ /** A parsed backdrop colour the overlay composites against. */
97
+ export interface Backdrop {
98
+ /** sRGB 0..1 (the value as authored — NOT linearised). */
99
+ rgb: RGB;
100
+ /** Rec.709 relative luminance in sRGB space, 0 (black) .. 1 (white). */
101
+ luminance: number;
102
+ }
103
+
104
+ /** Parse `#rgb[a]` / `#rrggbb[aa]` hex (3/4/6/8 digits) into sRGB 0..1, or null. */
105
+ function parseHex(s: string): RGB | null {
106
+ const m = /^#([0-9a-f]{3,8})$/i.exec(s);
107
+ if (!m) return null;
108
+ const h = m[1]!;
109
+ const dup = (c: string): number => parseInt(c + c, 16) / 255;
110
+ if (h.length === 3 || h.length === 4) return { r: dup(h[0]!), g: dup(h[1]!), b: dup(h[2]!) };
111
+ if (h.length === 6 || h.length === 8) {
112
+ const at = (i: number): number => parseInt(h.slice(i, i + 2), 16) / 255;
113
+ return { r: at(0), g: at(2), b: at(4) };
114
+ }
115
+ return null;
116
+ }
117
+
118
+ /** Parse `rgb()/rgba()` (comma- or space-separated, 0–255 or %), or null. */
119
+ function parseRgbFunc(s: string): RGB | null {
120
+ const m = /^rgba?\(([^)]+)\)$/i.exec(s.trim());
121
+ if (!m) return null;
122
+ const parts = m[1]!.split(/[\s,/]+/).filter(Boolean).slice(0, 3);
123
+ if (parts.length < 3) return null;
124
+ const chan = (p: string): number =>
125
+ p.endsWith("%") ? clamp01(parseFloat(p) / 100) : clamp01(parseFloat(p) / 255);
126
+ return { r: chan(parts[0]!), g: chan(parts[1]!), b: chan(parts[2]!) };
127
+ }
128
+
129
+ /**
130
+ * Parse any CSS colour string into a {@link Backdrop} (sRGB 0..1 + luminance),
131
+ * or `null` if it can't be understood. Handles hex and `rgb()/rgba()` directly;
132
+ * for everything else (named colours, `hsl()`, `color()`) it falls back to the
133
+ * browser's own normaliser via a throwaway 2D canvas when a DOM is present. The
134
+ * runtime uses the luminance to decide how strongly to cast shadow as a surface
135
+ * lightens; the colour itself isn't sent to the shader (the light layer
136
+ * composites source-over against the live page).
137
+ */
138
+ export function parseBackdrop(css: string): Backdrop | null {
139
+ let rgb = parseHex(css) ?? parseRgbFunc(css);
140
+ if (!rgb && typeof document !== "undefined") {
141
+ try {
142
+ const ctx = document.createElement("canvas").getContext("2d");
143
+ if (ctx) {
144
+ ctx.fillStyle = "#000";
145
+ ctx.fillStyle = css; // the browser normalises to #rrggbb or rgb(a)(...)
146
+ rgb = parseHex(ctx.fillStyle) ?? parseRgbFunc(ctx.fillStyle);
147
+ }
148
+ } catch {
149
+ /* no canvas — fall through to null */
150
+ }
151
+ }
152
+ if (!rgb) return null;
153
+ return { rgb, luminance: 0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b };
154
+ }
Binary file
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Low-level WebGL2 helpers: shader compilation + program linking. Kept separate
3
+ * from any particular effect so every effect (and the shared context's program
4
+ * cache) reuses the exact same, well-tested path. This is the single place a
5
+ * GLSL program is compiled in the whole library.
6
+ */
7
+
8
+ function compileShader(
9
+ gl: WebGL2RenderingContext,
10
+ type: number,
11
+ src: string,
12
+ ): WebGLShader {
13
+ const sh = gl.createShader(type);
14
+ if (!sh) throw new Error("dopamine: failed to create shader");
15
+ gl.shaderSource(sh, src);
16
+ gl.compileShader(sh);
17
+ if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
18
+ const log = gl.getShaderInfoLog(sh);
19
+ gl.deleteShader(sh);
20
+ throw new Error(`dopamine: shader compile error\n${log ?? ""}`);
21
+ }
22
+ return sh;
23
+ }
24
+
25
+ /** Compile + link a vertex/fragment pair into a program. */
26
+ export function linkProgram(
27
+ gl: WebGL2RenderingContext,
28
+ vertexSrc: string,
29
+ fragmentSrc: string,
30
+ ): WebGLProgram {
31
+ const vs = compileShader(gl, gl.VERTEX_SHADER, vertexSrc);
32
+ const fs = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSrc);
33
+ const program = gl.createProgram();
34
+ if (!program) throw new Error("dopamine: failed to create program");
35
+ gl.attachShader(program, vs);
36
+ gl.attachShader(program, fs);
37
+ gl.linkProgram(program);
38
+ gl.deleteShader(vs);
39
+ gl.deleteShader(fs);
40
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
41
+ const log = gl.getProgramInfoLog(program);
42
+ gl.deleteProgram(program);
43
+ throw new Error(`dopamine: program link error\n${log ?? ""}`);
44
+ }
45
+ return program;
46
+ }