@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.
- package/dist/engine/color.d.ts +71 -0
- package/dist/engine/color.d.ts.map +1 -0
- package/dist/engine/color.js +107 -0
- package/dist/engine/color.js.map +1 -0
- package/dist/engine/context.d.ts +54 -0
- package/dist/engine/context.d.ts.map +1 -0
- package/dist/engine/context.js +0 -0
- package/dist/engine/context.js.map +1 -0
- package/dist/engine/gl.d.ts +9 -0
- package/dist/engine/gl.d.ts.map +1 -0
- package/dist/engine/gl.js +39 -0
- package/dist/engine/gl.js.map +1 -0
- package/dist/engine/look/glsl.d.ts +95 -0
- package/dist/engine/look/glsl.d.ts.map +1 -0
- package/dist/engine/look/glsl.js +171 -0
- package/dist/engine/look/glsl.js.map +1 -0
- package/dist/engine/look/particles.glsl.d.ts +21 -0
- package/dist/engine/look/particles.glsl.d.ts.map +1 -0
- package/dist/engine/look/particles.glsl.js +44 -0
- package/dist/engine/look/particles.glsl.js.map +1 -0
- package/dist/engine/sdf.d.ts +77 -0
- package/dist/engine/sdf.d.ts.map +1 -0
- package/dist/engine/sdf.js +255 -0
- package/dist/engine/sdf.js.map +1 -0
- package/dist/engine/seed.d.ts +10 -0
- package/dist/engine/seed.d.ts.map +1 -0
- package/dist/engine/seed.js +20 -0
- package/dist/engine/seed.js.map +1 -0
- package/dist/engine/shadow.d.ts +41 -0
- package/dist/engine/shadow.d.ts.map +1 -0
- package/dist/engine/shadow.js +39 -0
- package/dist/engine/shadow.js.map +1 -0
- package/dist/engine/tempo.d.ts +33 -0
- package/dist/engine/tempo.d.ts.map +1 -0
- package/dist/engine/tempo.js +51 -0
- package/dist/engine/tempo.js.map +1 -0
- package/dist/framework/conductor.d.ts +100 -0
- package/dist/framework/conductor.d.ts.map +1 -0
- package/dist/framework/conductor.js +493 -0
- package/dist/framework/conductor.js.map +1 -0
- package/dist/framework/content.d.ts +67 -0
- package/dist/framework/content.d.ts.map +1 -0
- package/dist/framework/content.js +72 -0
- package/dist/framework/content.js.map +1 -0
- package/dist/framework/dope-pass.d.ts +131 -0
- package/dist/framework/dope-pass.d.ts.map +1 -0
- package/dist/framework/dope-pass.js +346 -0
- package/dist/framework/dope-pass.js.map +1 -0
- package/dist/framework/dope-zip.d.ts +22 -0
- package/dist/framework/dope-zip.d.ts.map +1 -0
- package/dist/framework/dope-zip.js +116 -0
- package/dist/framework/dope-zip.js.map +1 -0
- package/dist/framework/effect.d.ts +128 -0
- package/dist/framework/effect.d.ts.map +1 -0
- package/dist/framework/effect.js +19 -0
- package/dist/framework/effect.js.map +1 -0
- package/dist/framework/frame-expr.d.ts +124 -0
- package/dist/framework/frame-expr.d.ts.map +1 -0
- package/dist/framework/frame-expr.js +135 -0
- package/dist/framework/frame-expr.js.map +1 -0
- package/dist/framework/load-effect.d.ts +77 -0
- package/dist/framework/load-effect.d.ts.map +1 -0
- package/dist/framework/load-effect.js +135 -0
- package/dist/framework/load-effect.js.map +1 -0
- package/dist/framework/loader.d.ts +309 -0
- package/dist/framework/loader.d.ts.map +1 -0
- package/dist/framework/loader.js +266 -0
- package/dist/framework/loader.js.map +1 -0
- package/dist/framework/mood-registry.d.ts +58 -0
- package/dist/framework/mood-registry.d.ts.map +1 -0
- package/dist/framework/mood-registry.js +58 -0
- package/dist/framework/mood-registry.js.map +1 -0
- package/dist/framework/panel-runner.d.ts +96 -0
- package/dist/framework/panel-runner.d.ts.map +1 -0
- package/dist/framework/panel-runner.js +137 -0
- package/dist/framework/panel-runner.js.map +1 -0
- package/dist/framework/pass-common.d.ts +97 -0
- package/dist/framework/pass-common.d.ts.map +1 -0
- package/dist/framework/pass-common.js +178 -0
- package/dist/framework/pass-common.js.map +1 -0
- package/dist/framework/pass-runner.d.ts +183 -0
- package/dist/framework/pass-runner.d.ts.map +1 -0
- package/dist/framework/pass-runner.js +212 -0
- package/dist/framework/pass-runner.js.map +1 -0
- package/dist/framework/programs.d.ts +54 -0
- package/dist/framework/programs.d.ts.map +1 -0
- package/dist/framework/programs.js +33 -0
- package/dist/framework/programs.js.map +1 -0
- package/dist/framework/registry.d.ts +29 -0
- package/dist/framework/registry.d.ts.map +1 -0
- package/dist/framework/registry.js +38 -0
- package/dist/framework/registry.js.map +1 -0
- package/dist/framework/runtime.d.ts +19 -0
- package/dist/framework/runtime.d.ts.map +1 -0
- package/dist/framework/runtime.js +37 -0
- package/dist/framework/runtime.js.map +1 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +126 -0
- package/dist/index.js.map +1 -0
- package/dist/overlay.d.ts +46 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +79 -0
- package/dist/overlay.js.map +1 -0
- package/dist/types.d.ts +68 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/package.json +37 -0
- package/src/engine/color.ts +154 -0
- package/src/engine/context.ts +0 -0
- package/src/engine/gl.ts +46 -0
- package/src/engine/look/glsl.ts +183 -0
- package/src/engine/look/particles.glsl.ts +44 -0
- package/src/engine/sdf.ts +298 -0
- package/src/engine/seed.ts +23 -0
- package/src/engine/shadow.ts +66 -0
- package/src/engine/tempo.ts +54 -0
- package/src/framework/conductor.ts +604 -0
- package/src/framework/content.ts +113 -0
- package/src/framework/dope-pass.ts +432 -0
- package/src/framework/dope-zip.ts +125 -0
- package/src/framework/effect.ts +127 -0
- package/src/framework/frame-expr.ts +217 -0
- package/src/framework/load-effect.ts +204 -0
- package/src/framework/loader.ts +502 -0
- package/src/framework/mood-registry.ts +87 -0
- package/src/framework/panel-runner.ts +233 -0
- package/src/framework/pass-common.ts +222 -0
- package/src/framework/pass-runner.ts +391 -0
- package/src/framework/programs.ts +62 -0
- package/src/framework/registry.ts +44 -0
- package/src/framework/runtime.ts +38 -0
- package/src/index.ts +227 -0
- package/src/overlay.ts +109 -0
- 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"}
|
package/dist/overlay.js
ADDED
|
@@ -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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
package/src/engine/gl.ts
ADDED
|
@@ -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
|
+
}
|