@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
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic Canvas2D "panel" runner — the shared backbone for HYBRID effects whose
|
|
3
|
+
* per-frame content is drawn with Canvas2D (vector / text / shape) and then lit
|
|
4
|
+
* by a fragment shader (Comic Impact's hand-lettered word + jagged starburst +
|
|
5
|
+
* ink, lit into Ben-Day halftone + action lines + flash).
|
|
6
|
+
*
|
|
7
|
+
* It owns ALL the renderer/texture/upload/shadow plumbing that a Canvas2D-panel
|
|
8
|
+
* effect would otherwise hand-wire:
|
|
9
|
+
* • the single offscreen panel canvas + its 2D context,
|
|
10
|
+
* • resizing the panel to track the live GL canvas each frame,
|
|
11
|
+
* • the per-frame draw → `texImage2D` UPLOAD into BOTH the light + shadow
|
|
12
|
+
* contexts (the panel pixels change every frame, so — unlike a static SDF
|
|
13
|
+
* aux — they are re-uploaded each frame), with the FLIP_Y + non-premultiplied
|
|
14
|
+
* channel-encoding convention the panel shaders expect,
|
|
15
|
+
* • the standard shader uniforms (resolution, center, life, time, style,
|
|
16
|
+
* palette, the shadow-pass uniforms via shadowGeometry) + scalar render.params
|
|
17
|
+
* auto-bound by name convention,
|
|
18
|
+
* • the light + (optional) shadow pass via the program-cached contexts, and
|
|
19
|
+
* • disposing the per-fire panel textures.
|
|
20
|
+
*
|
|
21
|
+
* What stays per-effect (the honest boundary): the GLSL, a small `draw()` panel
|
|
22
|
+
* program (the Canvas2D draw — genuinely code-shaped vector/text logic stays JS),
|
|
23
|
+
* and a tiny config naming the shader's uniforms + the per-frame timing.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { RGB } from "../engine/color.js";
|
|
27
|
+
import type { GLContext } from "../engine/context.js";
|
|
28
|
+
import type { EffectContext, EffectInstance } from "./effect.js";
|
|
29
|
+
import type { PassParams } from "./pass-runner.js";
|
|
30
|
+
import {
|
|
31
|
+
STANDARD_COMMON,
|
|
32
|
+
applyFloatMap,
|
|
33
|
+
beginProgram,
|
|
34
|
+
bindFrameUniforms,
|
|
35
|
+
bindPalette,
|
|
36
|
+
bindScalars,
|
|
37
|
+
bindShadowGeometry,
|
|
38
|
+
bindTarget,
|
|
39
|
+
compositeLightFragment,
|
|
40
|
+
computeScalarBinds,
|
|
41
|
+
} from "./pass-common.js";
|
|
42
|
+
|
|
43
|
+
/** Per-frame timing context for a panel effect's draw + frame hooks. */
|
|
44
|
+
export interface PanelFrameInfo {
|
|
45
|
+
/** Raw elapsed time since start, ms (panels don't snap "on twos"). */
|
|
46
|
+
elapsedMs: number;
|
|
47
|
+
/** Normalized life 0..1 (elapsedMs / durationMs, clamped). */
|
|
48
|
+
life: number;
|
|
49
|
+
/** Device-pixel ratio the panel is rendered at. */
|
|
50
|
+
dpr: number;
|
|
51
|
+
/**
|
|
52
|
+
* Targeted element CENTRE in panel device px (canvas space, y-down). The
|
|
53
|
+
* centrepiece is drawn here instead of the canvas centre. Defaults to the
|
|
54
|
+
* canvas centre.
|
|
55
|
+
*/
|
|
56
|
+
centerPx: { x: number; y: number };
|
|
57
|
+
/**
|
|
58
|
+
* Targeted element SIZE in device px. The centrepiece is sized to this box.
|
|
59
|
+
* Defaults to the full canvas.
|
|
60
|
+
*/
|
|
61
|
+
targetPx: { width: number; height: number };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A registered "panel program": the Canvas2D draw for one frame. */
|
|
65
|
+
export type PanelDraw<P extends PassParams = PassParams> = (
|
|
66
|
+
panelCtx: CanvasRenderingContext2D,
|
|
67
|
+
width: number,
|
|
68
|
+
height: number,
|
|
69
|
+
params: P,
|
|
70
|
+
info: PanelFrameInfo,
|
|
71
|
+
) => void;
|
|
72
|
+
|
|
73
|
+
/** Config for one Canvas2D-panel effect. */
|
|
74
|
+
export interface PanelConfig<P extends PassParams = PassParams> {
|
|
75
|
+
/** Vertex + fragment GLSL (the per-effect look). */
|
|
76
|
+
vertex: string;
|
|
77
|
+
fragment: string;
|
|
78
|
+
/** Every uniform name the shader reads. */
|
|
79
|
+
uniforms: readonly string[];
|
|
80
|
+
/** Sampler uniform for the uploaded panel (bound to TEXTURE0). Default "uPanel". */
|
|
81
|
+
panelSampler?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Explicit `param name → uniform name` overrides for the scalar auto-binding;
|
|
84
|
+
* map to `null` to skip a param that isn't a uniform (e.g. a scatter seed).
|
|
85
|
+
*/
|
|
86
|
+
bindings?: Record<string, string | null>;
|
|
87
|
+
/** The shadow occluder "height" as a fraction of min canvas dim. */
|
|
88
|
+
shadowHeightFrac: number | ((params: P) => number);
|
|
89
|
+
/** The Canvas2D panel program (draws one frame). */
|
|
90
|
+
draw: PanelDraw<P>;
|
|
91
|
+
/**
|
|
92
|
+
* Compute the genuinely effect-specific TIME-VARYING uniforms for a frame
|
|
93
|
+
* (presence, flash, …). Returns a map of uniform name → float; `amp` (a
|
|
94
|
+
* well-known key) feeds shadowGeometry.
|
|
95
|
+
*/
|
|
96
|
+
frame(info: PanelFrameInfo, params: P): { amp: number } & Record<string, number>;
|
|
97
|
+
/**
|
|
98
|
+
* Extra per-pass scalar uniforms that depend on the live canvas / dpr but are
|
|
99
|
+
* not plain params (e.g. `uDotSize = dotSize * dpr`, `uInkBoost`). Computed per
|
|
100
|
+
* pass.
|
|
101
|
+
*/
|
|
102
|
+
passUniforms?(
|
|
103
|
+
canvas: HTMLCanvasElement,
|
|
104
|
+
params: P,
|
|
105
|
+
dpr: number,
|
|
106
|
+
targetPx: { width: number; height: number },
|
|
107
|
+
): Record<string, number>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The panel runner adds `uCenter` (the panel composites around screen center)
|
|
111
|
+
// to the shared standard set — and binds the SAME anchor under the pass-runner's
|
|
112
|
+
// `uOrigin` name, so a panel shader may use either spelling (the single-source
|
|
113
|
+
// GLSL→MSL path maps `uOrigin` onto the packed struct's `origin` field).
|
|
114
|
+
const STANDARD = ["uCenter", "uOrigin", ...STANDARD_COMMON] as const;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build a drawable {@link EffectInstance} for a Canvas2D-panel effect from its
|
|
118
|
+
* config + resolved params + the runtime context.
|
|
119
|
+
*/
|
|
120
|
+
export function createPanelInstance<P extends PassParams>(
|
|
121
|
+
config: PanelConfig<P>,
|
|
122
|
+
params: P,
|
|
123
|
+
ctx: EffectContext,
|
|
124
|
+
): EffectInstance {
|
|
125
|
+
const pal = params.palette as RGB[];
|
|
126
|
+
const dpr = ctx.dpr;
|
|
127
|
+
const sampler = config.panelSampler ?? "uPanel";
|
|
128
|
+
const allUniforms = [...new Set([...STANDARD, sampler, ...config.uniforms])];
|
|
129
|
+
// Backdrop-aware mode: the light pass emits premultiplied light (source-over,
|
|
130
|
+
// visible on any surface); the shadow pass keeps the opaque multiply fragment.
|
|
131
|
+
const lightFragment = ctx.composite?.premultiplied
|
|
132
|
+
? compositeLightFragment(config.fragment)
|
|
133
|
+
: config.fragment;
|
|
134
|
+
|
|
135
|
+
// The numeric params that auto-bind to a uniform.
|
|
136
|
+
const scalarBinds = computeScalarBinds(params, config.bindings ?? {});
|
|
137
|
+
|
|
138
|
+
// One offscreen Canvas2D panel, shared by both passes (drawn once per frame).
|
|
139
|
+
const panel = document.createElement("canvas");
|
|
140
|
+
const pctx = panel.getContext("2d", { alpha: true })!;
|
|
141
|
+
|
|
142
|
+
const lightTex = ctx.light.gl.createTexture();
|
|
143
|
+
const shadowTex = ctx.shadow ? ctx.shadow.gl.createTexture() : null;
|
|
144
|
+
for (const glc of [ctx.light, ctx.shadow]) {
|
|
145
|
+
if (!glc) continue;
|
|
146
|
+
const { gl } = glc;
|
|
147
|
+
gl.bindTexture(gl.TEXTURE_2D, glc === ctx.light ? lightTex : shadowTex);
|
|
148
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
149
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
150
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
151
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const heightFrac = (): number =>
|
|
155
|
+
typeof config.shadowHeightFrac === "function" ? config.shadowHeightFrac(params) : config.shadowHeightFrac;
|
|
156
|
+
|
|
157
|
+
const drawPass = (
|
|
158
|
+
glc: GLContext,
|
|
159
|
+
tex: WebGLTexture,
|
|
160
|
+
info: PanelFrameInfo,
|
|
161
|
+
frameUniforms: { amp: number } & Record<string, number>,
|
|
162
|
+
isShadow: boolean,
|
|
163
|
+
): void => {
|
|
164
|
+
const { gl } = glc;
|
|
165
|
+
const c = glc.canvas;
|
|
166
|
+
const { u } = beginProgram(glc, config.vertex, isShadow ? config.fragment : lightFragment, allUniforms);
|
|
167
|
+
|
|
168
|
+
// Upload the freshly-drawn panel (changes every frame).
|
|
169
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
170
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
171
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
|
172
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
173
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, panel);
|
|
174
|
+
if (u[sampler]) gl.uniform1i(u[sampler], 0);
|
|
175
|
+
|
|
176
|
+
// Extra per-pass scalar uniforms (dpr-scaled etc.).
|
|
177
|
+
applyFloatMap(gl, u, config.passUniforms?.(c, params, dpr, info.targetPx));
|
|
178
|
+
|
|
179
|
+
// Standard uniforms.
|
|
180
|
+
gl.uniform2f(u.uResolution, c.width, c.height);
|
|
181
|
+
bindTarget(gl, u, c, ctx.targetSize, dpr);
|
|
182
|
+
// uCenter is the impact/heart centre the PROCEDURAL parts radiate from (comic's
|
|
183
|
+
// action lines, heartburst's bloom). It must match where the panel centrepiece
|
|
184
|
+
// lands — the anchor — NOT the canvas centre, or they split (centrepiece on the
|
|
185
|
+
// target, lines/glow stuck at screen centre). `frag` here is vUv*uResolution
|
|
186
|
+
// (y-up, matching the flipped panel texture), so flip the anchor's y exactly as
|
|
187
|
+
// the pure-shader runner does for uOrigin.
|
|
188
|
+
gl.uniform2f(u.uCenter, ctx.anchor.x * dpr, c.height - ctx.anchor.y * dpr);
|
|
189
|
+
if (u.uOrigin) gl.uniform2f(u.uOrigin, ctx.anchor.x * dpr, c.height - ctx.anchor.y * dpr);
|
|
190
|
+
gl.uniform1f(u.uLife, info.life);
|
|
191
|
+
gl.uniform1f(u.uTimeS, info.elapsedMs / 1000);
|
|
192
|
+
gl.uniform1f(u.uStyle, params.style);
|
|
193
|
+
bindPalette(gl, u, pal);
|
|
194
|
+
bindScalars(gl, u, params, scalarBinds);
|
|
195
|
+
bindFrameUniforms(gl, u, frameUniforms);
|
|
196
|
+
|
|
197
|
+
gl.uniform1f(u.uShadow, isShadow ? 1 : 0);
|
|
198
|
+
if (isShadow) bindShadowGeometry(gl, u, c, heightFrac(), frameUniforms.amp, params.style);
|
|
199
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
let disposed = false;
|
|
203
|
+
return {
|
|
204
|
+
durationMs: params.durationMs,
|
|
205
|
+
renderAt(elapsedMs: number): void {
|
|
206
|
+
if (disposed) return;
|
|
207
|
+
const c = ctx.light.canvas;
|
|
208
|
+
if (panel.width !== c.width || panel.height !== c.height) {
|
|
209
|
+
panel.width = c.width;
|
|
210
|
+
panel.height = c.height;
|
|
211
|
+
}
|
|
212
|
+
const life = Math.min(Math.max(elapsedMs, 0) / params.durationMs, 1);
|
|
213
|
+
// The targeted element box → panel device px (y-down canvas space). Defaults
|
|
214
|
+
// to the canvas centre + full canvas, reproducing the old screen-centred pose.
|
|
215
|
+
const centerPx = { x: ctx.anchor.x * dpr, y: ctx.anchor.y * dpr };
|
|
216
|
+
const targetPx = ctx.targetSize
|
|
217
|
+
? { width: ctx.targetSize.width * dpr, height: ctx.targetSize.height * dpr }
|
|
218
|
+
: { width: c.width, height: c.height };
|
|
219
|
+
const info: PanelFrameInfo = { elapsedMs, life, dpr, centerPx, targetPx };
|
|
220
|
+
const frameUniforms = config.frame(info, params);
|
|
221
|
+
// Draw the shared offscreen panel once, then composite into each pass.
|
|
222
|
+
config.draw(pctx, c.width, c.height, params, info);
|
|
223
|
+
if (ctx.shadow && shadowTex) drawPass(ctx.shadow, shadowTex, info, frameUniforms, true);
|
|
224
|
+
drawPass(ctx.light, lightTex!, info, frameUniforms, false);
|
|
225
|
+
},
|
|
226
|
+
dispose(): void {
|
|
227
|
+
if (disposed) return;
|
|
228
|
+
disposed = true;
|
|
229
|
+
if (lightTex) ctx.light.gl.deleteTexture(lightTex);
|
|
230
|
+
if (shadowTex && ctx.shadow) ctx.shadow.gl.deleteTexture(shadowTex);
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared plumbing for the two generic effect runners (pass-runner + panel-runner).
|
|
3
|
+
*
|
|
4
|
+
* The pure-shader runner (a full-screen triangle) and the Canvas2D-panel runner
|
|
5
|
+
* (an uploaded panel texture, then the same triangle) differ only in WHAT they
|
|
6
|
+
* upload/sample and one standard uniform (`uOrigin` vs `uCenter`). Everything
|
|
7
|
+
* else — the `name → u<Name>` scalar auto-binding, the palette + life/time/style
|
|
8
|
+
* standard uniforms, the per-frame uniform application, the shadow-pass geometry
|
|
9
|
+
* uniforms, and a linear/edge-clamped texture allocator — is identical glue and
|
|
10
|
+
* lives here ONCE so the two runners can't drift.
|
|
11
|
+
*
|
|
12
|
+
* The functions are deliberately tiny imperative helpers over a resolved uniform
|
|
13
|
+
* map (`u`) so the call order in each runner stays explicit and the emitted GL
|
|
14
|
+
* uniform writes remain byte-identical to the bespoke renderers these replaced.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { shadowGeometry } from "../engine/shadow.js";
|
|
18
|
+
import { GLSL_LIGHT_OUT } from "../engine/look/glsl.js";
|
|
19
|
+
import type { RGB } from "../engine/color.js";
|
|
20
|
+
import type { CachedProgram, GLContext } from "../engine/context.js";
|
|
21
|
+
|
|
22
|
+
// Cache the (idempotent) source transform by input string so repeated fires of
|
|
23
|
+
// the same effect don't re-run the regex; the GLContext program cache keys on
|
|
24
|
+
// the transformed source, so a backdrop-mode fire links its own variant once.
|
|
25
|
+
const compositeFragCache = new Map<string, string>();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Rewrite a light fragment for the BACKDROP-aware (premultiplied source-over)
|
|
29
|
+
* path: swap the opaque `fragColor = vec4(col, 1.0)` / `vec4(max(col,0.0),1.0)`
|
|
30
|
+
* emit for `fragColor = dopLightOut(col)` (alpha = brightness) and inject the
|
|
31
|
+
* `dopLightOut` helper. This is the same emit swap the Android build applies
|
|
32
|
+
* (`tools/dopamine/src/android-shader.mjs`), so the web backdrop path and the
|
|
33
|
+
* native overlays share one premultiplied-light convention.
|
|
34
|
+
*
|
|
35
|
+
* The replace is global on purpose: a hybrid's shader has a SHADOW branch with
|
|
36
|
+
* its own opaque emit, but the light pass runs with `uShadow == 0` so that
|
|
37
|
+
* branch is dead code here — rewriting it is harmless, and it guarantees the
|
|
38
|
+
* LIVE light-path emit (wherever it sits in the source) is the premultiplied
|
|
39
|
+
* one. The shadow CONTEXT keeps the original, untouched fragment.
|
|
40
|
+
*/
|
|
41
|
+
export function compositeLightFragment(fragment: string): string {
|
|
42
|
+
const cached = compositeFragCache.get(fragment);
|
|
43
|
+
if (cached) return cached;
|
|
44
|
+
let out = fragment
|
|
45
|
+
.replace(/fragColor\s*=\s*vec4\(\s*max\(\s*(\w+)\s*,\s*0\.0\s*\)\s*,\s*1\.0\s*\)\s*;/g, "fragColor = dopLightOut($1);")
|
|
46
|
+
.replace(/fragColor\s*=\s*vec4\(\s*(\w+)\s*,\s*1\.0\s*\)\s*;/g, "fragColor = dopLightOut($1);");
|
|
47
|
+
// Only inject the helper if we actually swapped an emit (an effect that
|
|
48
|
+
// already emits premultiplied light — e.g. a `vec4(col, brightness)` — is
|
|
49
|
+
// left exactly as-is and needs no helper).
|
|
50
|
+
if (out !== fragment) out = out.replace(/\nvoid main\s*\(/, "\n" + GLSL_LIGHT_OUT + "\nvoid main(");
|
|
51
|
+
compositeFragCache.set(fragment, out);
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A resolved uniform-location map (the output of `CachedProgram.uniforms`). */
|
|
56
|
+
export type UniformMap = Record<string, WebGLUniformLocation | null>;
|
|
57
|
+
|
|
58
|
+
/** `bloomRadius → uBloomRadius` — the auto-binding name convention. */
|
|
59
|
+
export const cap = (s: string): string => `u${s.charAt(0).toUpperCase()}${s.slice(1)}`;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The numeric `render.params` that auto-bind to a uniform: each `name → u<Name>`
|
|
63
|
+
* unless an explicit `bindings` entry overrides the uniform name (or maps it to
|
|
64
|
+
* `null` to skip — for a param the shader ignores, e.g. a scatter seed). The
|
|
65
|
+
* tempo `durationMs` is never a shader uniform.
|
|
66
|
+
*/
|
|
67
|
+
export function computeScalarBinds(
|
|
68
|
+
params: Record<string, unknown>,
|
|
69
|
+
bindings: Record<string, string | null>,
|
|
70
|
+
): Array<[string, string]> {
|
|
71
|
+
const out: Array<[string, string]> = [];
|
|
72
|
+
for (const [name, value] of Object.entries(params)) {
|
|
73
|
+
if (typeof value !== "number") continue;
|
|
74
|
+
if (name === "durationMs") continue; // tempo, not a shader uniform
|
|
75
|
+
const override = bindings[name];
|
|
76
|
+
if (override === null) continue; // explicitly not a uniform
|
|
77
|
+
out.push([name, override ?? cap(name)]);
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolve the program + uniform map for one pass and bind the program + the
|
|
84
|
+
* shared empty VAO. Returns both so the caller can finish binding.
|
|
85
|
+
*/
|
|
86
|
+
export function beginProgram(
|
|
87
|
+
glc: GLContext,
|
|
88
|
+
vertex: string,
|
|
89
|
+
fragment: string,
|
|
90
|
+
allUniforms: readonly string[],
|
|
91
|
+
): { prog: CachedProgram; u: UniformMap } {
|
|
92
|
+
const { gl } = glc;
|
|
93
|
+
const prog = glc.program(vertex, fragment);
|
|
94
|
+
const u = prog.uniforms(allUniforms);
|
|
95
|
+
gl.useProgram(prog.program);
|
|
96
|
+
gl.bindVertexArray(glc.vao);
|
|
97
|
+
return { prog, u };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Set a float uniform iff the shader actually declares it. */
|
|
101
|
+
export function setF(gl: WebGL2RenderingContext, u: UniformMap, name: string, v: number): void {
|
|
102
|
+
const loc = u[name];
|
|
103
|
+
if (loc) gl.uniform1f(loc, v);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Apply a `{ name → float }` map, skipping uniforms the shader doesn't declare. */
|
|
107
|
+
export function applyFloatMap(
|
|
108
|
+
gl: WebGL2RenderingContext,
|
|
109
|
+
u: UniformMap,
|
|
110
|
+
map: Record<string, number> | undefined,
|
|
111
|
+
): void {
|
|
112
|
+
if (!map) return;
|
|
113
|
+
for (const [n, v] of Object.entries(map)) setF(gl, u, n, v);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Bind the three palette stops (uC0/uC1/uC2). */
|
|
117
|
+
export function bindPalette(gl: WebGL2RenderingContext, u: UniformMap, pal: RGB[]): void {
|
|
118
|
+
const [c0, c1, c2] = pal;
|
|
119
|
+
if (c0) gl.uniform3f(u.uC0, c0.r, c0.g, c0.b);
|
|
120
|
+
if (c1) gl.uniform3f(u.uC1, c1.r, c1.g, c1.b);
|
|
121
|
+
if (c2) gl.uniform3f(u.uC2, c2.r, c2.g, c2.b);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Bind the auto-bound scalar params (`name → uniform`). */
|
|
125
|
+
export function bindScalars(
|
|
126
|
+
gl: WebGL2RenderingContext,
|
|
127
|
+
u: UniformMap,
|
|
128
|
+
params: Record<string, unknown>,
|
|
129
|
+
scalarBinds: ReadonlyArray<readonly [string, string]>,
|
|
130
|
+
): void {
|
|
131
|
+
for (const [name, uniformName] of scalarBinds) {
|
|
132
|
+
const loc = u[uniformName];
|
|
133
|
+
if (loc) gl.uniform1f(loc, params[name] as number);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Apply the per-frame uniform map from an effect's `frame()` hook. The
|
|
139
|
+
* well-known key `amp` maps to `uAmp`; every other key is its own uniform name.
|
|
140
|
+
*/
|
|
141
|
+
export function bindFrameUniforms(
|
|
142
|
+
gl: WebGL2RenderingContext,
|
|
143
|
+
u: UniformMap,
|
|
144
|
+
frameUniforms: Record<string, number>,
|
|
145
|
+
): void {
|
|
146
|
+
for (const [n, v] of Object.entries(frameUniforms)) {
|
|
147
|
+
const loc = u[n === "amp" ? "uAmp" : n];
|
|
148
|
+
if (loc) gl.uniform1f(loc, v);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Set the shadow-pass uniforms (offset/soft/strength) from {@link shadowGeometry}.
|
|
154
|
+
* Call only on the shadow pass; the caller already set `uShadow = 1`.
|
|
155
|
+
*/
|
|
156
|
+
export function bindShadowGeometry(
|
|
157
|
+
gl: WebGL2RenderingContext,
|
|
158
|
+
u: UniformMap,
|
|
159
|
+
canvas: HTMLCanvasElement,
|
|
160
|
+
heightFrac: number,
|
|
161
|
+
amp: number,
|
|
162
|
+
style: number,
|
|
163
|
+
): void {
|
|
164
|
+
const minDim = Math.min(canvas.width, canvas.height);
|
|
165
|
+
const sg = shadowGeometry({ minDim, heightFrac, amp, style });
|
|
166
|
+
gl.uniform2f(u.uShadowOffset, sg.offsetX, sg.offsetY);
|
|
167
|
+
gl.uniform1f(u.uShadowSoft, sg.soft);
|
|
168
|
+
gl.uniform1f(u.uShadowStrength, sg.strength);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Allocate a linear/edge-clamped texture (pixels uploaded later by the caller). */
|
|
172
|
+
export function allocTexture(glc: GLContext): WebGLTexture {
|
|
173
|
+
const { gl } = glc;
|
|
174
|
+
const tex = gl.createTexture();
|
|
175
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
176
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
177
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
178
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
179
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
180
|
+
return tex!;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** The standard uniforms common to BOTH runners (excludes uOrigin/uCenter). */
|
|
184
|
+
export const STANDARD_COMMON = [
|
|
185
|
+
"uResolution", "uTarget", "uLife", "uTimeS", "uLoopS", "uPhase", "uStyle", "uAmp",
|
|
186
|
+
"uC0", "uC1", "uC2", "uShadow", "uShadowOffset", "uShadowSoft", "uShadowStrength",
|
|
187
|
+
] as const;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The targeted element's box in DEVICE px, falling back to the full canvas
|
|
191
|
+
* when no element box was supplied (so untargeted fires are unchanged). The
|
|
192
|
+
* single formula behind the `uTarget` standard uniform AND the `render.pass`
|
|
193
|
+
* `targetMinDimPx` input — one fallback rule, never two.
|
|
194
|
+
*/
|
|
195
|
+
export function resolveTargetPx(
|
|
196
|
+
c: HTMLCanvasElement,
|
|
197
|
+
targetSize: { width: number; height: number } | undefined,
|
|
198
|
+
dpr: number,
|
|
199
|
+
): { width: number; height: number } {
|
|
200
|
+
return {
|
|
201
|
+
width: targetSize ? targetSize.width * dpr : c.width,
|
|
202
|
+
height: targetSize ? targetSize.height * dpr : c.height,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Bind `uTarget` — the targeted element's size (device px) the centrepiece is
|
|
208
|
+
* sized to — falling back to the full canvas when no element box was supplied
|
|
209
|
+
* (so untargeted fires are unchanged). Shared by both runners; a no-op for
|
|
210
|
+
* shaders that don't declare it.
|
|
211
|
+
*/
|
|
212
|
+
export function bindTarget(
|
|
213
|
+
gl: WebGL2RenderingContext,
|
|
214
|
+
u: Record<string, WebGLUniformLocation | null>,
|
|
215
|
+
c: HTMLCanvasElement,
|
|
216
|
+
targetSize: { width: number; height: number } | undefined,
|
|
217
|
+
dpr: number,
|
|
218
|
+
): void {
|
|
219
|
+
if (!u.uTarget) return;
|
|
220
|
+
const { width, height } = resolveTargetPx(c, targetSize, dpr);
|
|
221
|
+
gl.uniform2f(u.uTarget, width, height);
|
|
222
|
+
}
|