@dopaminefx/effect-heartburst 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Heartburst Canvas2D PANEL drawing.
3
+ *
4
+ * The crisp, vector-y parts of Heartburst — the big swelling hero heart and the
5
+ * flurry of little hearts that fly out on the burst — are drawn into an OFFSCREEN
6
+ * Canvas2D each frame (cheap: a couple dozen filled paths) and uploaded as the
7
+ * "panel" texture; the fragment shader (heartburst-shader.ts) adds the soft warm
8
+ * bloom, the gloss highlight, the halftone blush, the noir↔pop styling, the beat
9
+ * flash and casts the warm light + soft shadow.
10
+ *
11
+ * Both the hero and the little hearts are a single parametric HEART CURVE (the
12
+ * classic `16sin³t` cardioid-ish heart) traced as a Canvas2D path, so the form
13
+ * is true vector geometry the GPU can't easily do.
14
+ *
15
+ * Panel channel encoding consumed by the shader:
16
+ * R = hero heart FILL · G = INK (outline) + gloss seed · B = burst hearts FILL
17
+ */
18
+ import { type PanelDraw } from "@dopaminefx/core";
19
+ /** Fraction of life occupied by the lub-dub beat phase before the burst. */
20
+ export declare const HEARTBEAT_PHASE = 0.3;
21
+ /**
22
+ * Heart SCALE multiplier over normalized life. A resting 1.0 with two beats
23
+ * superimposed, then it settles to rest through the burst and gently shrinks as
24
+ * it fades. `strength` scales beat swell; `doubleBeat` blends single → lub-dub.
25
+ */
26
+ export declare function heartbeatScale(life: number, strength?: number, doubleBeat?: number): number;
27
+ /**
28
+ * The amplitude/energy envelope (→ uAmp + shadow strength). Tracks the beats
29
+ * during the lub-dub then a bright flare at the burst, decaying through the
30
+ * afterglow. NOTE: shipped as DATA (`tempo.frame.amp`); this copy exists only
31
+ * so the tests can pin the data delta-0 against the readable formula.
32
+ */
33
+ export declare function heartburstEnvelope(life: number, strength?: number, doubleBeat?: number): number;
34
+ /**
35
+ * Burst progress 0..1 over the post-beat phase: 0 until the dub finishes, then
36
+ * eases out to 1 as the little hearts fly out and fade.
37
+ */
38
+ export declare function burstProgress(life: number): number;
39
+ /**
40
+ * Overall panel presence over normalized life: a quick snap-in, a proud hold
41
+ * through the beats + burst, then a clean fade at the tail so the panel clears.
42
+ */
43
+ export declare function heartPresence(life: number): number;
44
+ /** Resolved render params Heartburst's panel + shader consume. */
45
+ export interface HeartburstRenderParams {
46
+ durationMs: number;
47
+ palette: unknown;
48
+ style: number;
49
+ heartburstSeed: number;
50
+ heartScale: number;
51
+ burstCount: number;
52
+ burstSpread: number;
53
+ inkWeight: number;
54
+ beatStrength: number;
55
+ doubleBeat: number;
56
+ dotSize: number;
57
+ exposure: number;
58
+ glow: number;
59
+ gloss: number;
60
+ halftone: number;
61
+ saturation: number;
62
+ }
63
+ /**
64
+ * Draw the offscreen panel for this frame.
65
+ * - the hero heart at the current beat scale, fill in RED, outline in GREEN,
66
+ * plus a gloss-highlight blob painted into GREEN that sits ON the fill (the
67
+ * shader reads ink∩fill as the specular seed),
68
+ * - the little burst hearts flying outward along seeded arcs, fill in BLUE.
69
+ *
70
+ * `heartScaleMul` is the lub-dub beat multiplier on the hero (1 at rest).
71
+ * `presence` fades the whole panel in/out.
72
+ */
73
+ /**
74
+ * The per-frame panel draw in the generic `PanelDraw` shape — the ONE
75
+ * code-shaped hook the data-driven factory wires (`registerDopePanelEffect`).
76
+ * Computes the draw-side tempo (beat scale, presence, target span) and hands
77
+ * off to {@link drawHeartburstPanel}.
78
+ */
79
+ export declare const drawHeartburstFrame: PanelDraw;
80
+ export declare function drawHeartburstPanel(ctx: CanvasRenderingContext2D, w: number, h: number, params: HeartburstRenderParams, heartScaleMul: number, life: number, presence: number, dpr: number, center: {
81
+ x: number;
82
+ y: number;
83
+ }, span: number): void;
84
+ //# sourceMappingURL=heartburst-renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heartburst-renderer.d.ts","sourceRoot":"","sources":["../src/heartburst-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAqC,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAcrF,4EAA4E;AAC5E,eAAO,MAAM,eAAe,MAAM,CAAC;AAanC;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,SAAI,EAAE,UAAU,SAAI,GAAG,MAAM,CAOjF;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,SAAI,EAAE,UAAU,SAAI,GAAG,MAAM,CASrF;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIlD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMlD;AAED,kEAAkE;AAClE,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAkCD;;;;;;;;;GASG;AACH;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,EAAE,SAMjC,CAAC;AAEF,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,wBAAwB,EAC7B,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,MAAM,EAAE,sBAAsB,EAC9B,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAChC,IAAI,EAAE,MAAM,GACX,IAAI,CAsGN"}
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Heartburst Canvas2D PANEL drawing.
3
+ *
4
+ * The crisp, vector-y parts of Heartburst — the big swelling hero heart and the
5
+ * flurry of little hearts that fly out on the burst — are drawn into an OFFSCREEN
6
+ * Canvas2D each frame (cheap: a couple dozen filled paths) and uploaded as the
7
+ * "panel" texture; the fragment shader (heartburst-shader.ts) adds the soft warm
8
+ * bloom, the gloss highlight, the halftone blush, the noir↔pop styling, the beat
9
+ * flash and casts the warm light + soft shadow.
10
+ *
11
+ * Both the hero and the little hearts are a single parametric HEART CURVE (the
12
+ * classic `16sin³t` cardioid-ish heart) traced as a Canvas2D path, so the form
13
+ * is true vector geometry the GPU can't easily do.
14
+ *
15
+ * Panel channel encoding consumed by the shader:
16
+ * R = hero heart FILL · G = INK (outline) + gloss seed · B = burst hearts FILL
17
+ */
18
+ import { easeOutCubic, clamp01, mulberry32 } from "@dopaminefx/core";
19
+ // ---------------------------------------------------------------------------
20
+ // DRAW-side tempo. The per-frame UNIFORM logic (amp/presence/beat/burst/flash)
21
+ // is DATA — `tempo.frame` in heartburst.dope.json, evaluated by the core
22
+ // frame-expr evaluator (pinned delta-0 against these functions by the tests).
23
+ // The same curves are ALSO needed by the panel GEOMETRY (the hero swells with
24
+ // the beat, the little hearts fly out with the burst), and panel geometry is
25
+ // code by design — so the draw-side copies live here, next to the draw.
26
+ //
27
+ // life 0.00 .. 0.30 : LUB-DUB — two beats; the second tucked behind the first
28
+ // life 0.30 .. 1.00 : BURST + AFTERGLOW — little hearts fly out, hero fades
29
+ // ---------------------------------------------------------------------------
30
+ /** Fraction of life occupied by the lub-dub beat phase before the burst. */
31
+ export const HEARTBEAT_PHASE = 0.3;
32
+ /**
33
+ * A single soft beat pulse centred at `center` (in life units) with half-width
34
+ * `width`: rises fast, eases back down. Returns 0..1 (peak 1 at `center`).
35
+ */
36
+ function beatPulse(t, center, width) {
37
+ const x = (t - center) / width;
38
+ if (x <= -1 || x >= 1)
39
+ return 0;
40
+ const lobe = 0.5 + 0.5 * Math.cos(x * Math.PI);
41
+ return x < 0 ? Math.pow(lobe, 0.7) : Math.pow(lobe, 1.4);
42
+ }
43
+ /**
44
+ * Heart SCALE multiplier over normalized life. A resting 1.0 with two beats
45
+ * superimposed, then it settles to rest through the burst and gently shrinks as
46
+ * it fades. `strength` scales beat swell; `doubleBeat` blends single → lub-dub.
47
+ */
48
+ export function heartbeatScale(life, strength = 1, doubleBeat = 1) {
49
+ const t = clamp01(life);
50
+ const lub = beatPulse(t, 0.1, 0.1);
51
+ const dub = beatPulse(t, 0.21, 0.075) * 0.62 * clamp01(doubleBeat);
52
+ const beat = Math.max(lub, dub);
53
+ const sag = t > HEARTBEAT_PHASE ? 0.06 * easeOutCubic((t - HEARTBEAT_PHASE) / (1 - HEARTBEAT_PHASE)) : 0;
54
+ return 1 + beat * 0.42 * strength - sag;
55
+ }
56
+ /**
57
+ * The amplitude/energy envelope (→ uAmp + shadow strength). Tracks the beats
58
+ * during the lub-dub then a bright flare at the burst, decaying through the
59
+ * afterglow. NOTE: shipped as DATA (`tempo.frame.amp`); this copy exists only
60
+ * so the tests can pin the data delta-0 against the readable formula.
61
+ */
62
+ export function heartburstEnvelope(life, strength = 1, doubleBeat = 1) {
63
+ const t = clamp01(life);
64
+ if (t <= 0 || t >= 1)
65
+ return 0;
66
+ const lub = beatPulse(t, 0.1, 0.1);
67
+ const dub = beatPulse(t, 0.21, 0.075) * 0.62 * clamp01(doubleBeat);
68
+ const beats = Math.max(lub, dub) * 0.9 * strength;
69
+ const b = burstProgress(life);
70
+ const flare = b * Math.pow(1 - b, 1.1) * 2.4;
71
+ return clamp01(Math.max(beats, flare * (0.7 + 0.3 * strength)));
72
+ }
73
+ /**
74
+ * Burst progress 0..1 over the post-beat phase: 0 until the dub finishes, then
75
+ * eases out to 1 as the little hearts fly out and fade.
76
+ */
77
+ export function burstProgress(life) {
78
+ const t = clamp01(life);
79
+ if (t <= HEARTBEAT_PHASE)
80
+ return 0;
81
+ return easeOutCubic((t - HEARTBEAT_PHASE) / (1 - HEARTBEAT_PHASE));
82
+ }
83
+ /**
84
+ * Overall panel presence over normalized life: a quick snap-in, a proud hold
85
+ * through the beats + burst, then a clean fade at the tail so the panel clears.
86
+ */
87
+ export function heartPresence(life) {
88
+ const t = life < 0 ? 0 : life > 1 ? 1 : life;
89
+ if (t < 0.04)
90
+ return t / 0.04;
91
+ if (t < 0.8)
92
+ return 1;
93
+ const fade = 1 - (t - 0.8) / 0.2;
94
+ return Math.pow(Math.max(0, fade), 1.4);
95
+ }
96
+ /**
97
+ * Trace a parametric heart of half-size `s` centred at the current origin into
98
+ * the given context's current path. `rot` rotates it (radians). The classic
99
+ * heart curve, normalized so its bounding extent ≈ `s` and the cusp points UP.
100
+ */
101
+ /** Hero-heart size relative to the targeted element box (≈1.5×). See the Swift
102
+ * `HEARTBURST_TARGET_FILL` — keep the two in sync. */
103
+ const HEARTBURST_TARGET_FILL = 3.6;
104
+ function traceHeart(ctx, s, rot) {
105
+ const steps = 48;
106
+ ctx.beginPath();
107
+ for (let i = 0; i <= steps; i++) {
108
+ const t = (i / steps) * Math.PI * 2;
109
+ // x in [-16,16], y in roughly [-17,12] for the standard curve.
110
+ const x = 16 * Math.pow(Math.sin(t), 3);
111
+ const y = 13 * Math.cos(t) -
112
+ 5 * Math.cos(2 * t) -
113
+ 2 * Math.cos(3 * t) -
114
+ Math.cos(4 * t);
115
+ // Normalize to ~[-1,1] and flip Y so the lobes are at the top (canvas y-down).
116
+ const nx = (x / 17) * s;
117
+ const ny = (-y / 17) * s;
118
+ const cx = nx * Math.cos(rot) - ny * Math.sin(rot);
119
+ const cy = nx * Math.sin(rot) + ny * Math.cos(rot);
120
+ if (i === 0)
121
+ ctx.moveTo(cx, cy);
122
+ else
123
+ ctx.lineTo(cx, cy);
124
+ }
125
+ ctx.closePath();
126
+ }
127
+ /**
128
+ * Draw the offscreen panel for this frame.
129
+ * - the hero heart at the current beat scale, fill in RED, outline in GREEN,
130
+ * plus a gloss-highlight blob painted into GREEN that sits ON the fill (the
131
+ * shader reads ink∩fill as the specular seed),
132
+ * - the little burst hearts flying outward along seeded arcs, fill in BLUE.
133
+ *
134
+ * `heartScaleMul` is the lub-dub beat multiplier on the hero (1 at rest).
135
+ * `presence` fades the whole panel in/out.
136
+ */
137
+ /**
138
+ * The per-frame panel draw in the generic `PanelDraw` shape — the ONE
139
+ * code-shaped hook the data-driven factory wires (`registerDopePanelEffect`).
140
+ * Computes the draw-side tempo (beat scale, presence, target span) and hands
141
+ * off to {@link drawHeartburstPanel}.
142
+ */
143
+ export const drawHeartburstFrame = (pctx, w, h, params, info) => {
144
+ const p = params;
145
+ const scale = heartbeatScale(info.life, p.beatStrength, p.doubleBeat);
146
+ const presence = heartPresence(info.life);
147
+ const span = Math.min(info.targetPx.width, info.targetPx.height);
148
+ drawHeartburstPanel(pctx, w, h, p, scale, info.life, presence, info.dpr, info.centerPx, span);
149
+ };
150
+ export function drawHeartburstPanel(ctx, w, h, params, heartScaleMul, life, presence, dpr, center, span) {
151
+ ctx.clearRect(0, 0, w, h);
152
+ if (presence <= 0.001)
153
+ return;
154
+ // Position the hearts on the targeted element (centre) and size them to its box
155
+ // (`span`), so the centrepiece matches the page element instead of the canvas.
156
+ const cx = center.x;
157
+ const cy = center.y;
158
+ // The hero heart reads at ~150% of the targeted element (heartScale ~0.22 ⇒
159
+ // heart extent ≈ 1.5× the box), clamped to the canvas so a full-page fire
160
+ // (target == canvas) keeps its original size. Sync w/ HeartburstPanel.swift.
161
+ const minDim = Math.min(span * HEARTBURST_TARGET_FILL, Math.min(w, h));
162
+ const seed = (params.heartburstSeed * 1000) >>> 0;
163
+ const rng = mulberry32(seed);
164
+ const ink = Math.max(1, params.inkWeight * dpr);
165
+ // ---------- HERO HEART (R fill, G outline + gloss seed) ------------------
166
+ const heroS = minDim * params.heartScale * heartScaleMul;
167
+ // a tiny per-fire tilt so it feels hand-placed.
168
+ const tilt = ((params.heartburstSeed % 1) - 0.5) * 0.12;
169
+ // As the burst takes over, the hero heart shrinks/cracks open a touch so the
170
+ // little hearts read as having spilled OUT of it.
171
+ const b = burstProgress(life);
172
+ const heroPresence = presence * (1 - 0.65 * b);
173
+ ctx.save();
174
+ ctx.translate(cx, cy);
175
+ ctx.globalCompositeOperation = "lighter"; // additive into channels
176
+ if (heroPresence > 0.002) {
177
+ const heroFillA = Math.round(255 * heroPresence);
178
+ // FILL -> RED.
179
+ traceHeart(ctx, heroS, tilt);
180
+ ctx.fillStyle = `rgba(${heroFillA},0,0,1)`;
181
+ ctx.fill();
182
+ // OUTLINE -> GREEN.
183
+ traceHeart(ctx, heroS, tilt);
184
+ ctx.lineJoin = "round";
185
+ ctx.lineWidth = ink * 1.6;
186
+ ctx.strokeStyle = `rgba(0,${heroFillA},0,1)`;
187
+ ctx.stroke();
188
+ // GLOSS SEED -> GREEN, painted ON the fill (upper-left lobe). The shader
189
+ // reads ink∩fill as the specular highlight, so a soft blob here becomes the
190
+ // gel-heart shine. Clip to the heart so it never bleeds past the silhouette.
191
+ ctx.save();
192
+ traceHeart(ctx, heroS, tilt);
193
+ ctx.clip();
194
+ const gx = -heroS * 0.34;
195
+ const gy = -heroS * 0.42;
196
+ const gr = heroS * 0.42;
197
+ const grad = ctx.createRadialGradient(gx, gy, 0, gx, gy, gr);
198
+ grad.addColorStop(0, `rgba(0,${heroFillA},0,1)`);
199
+ grad.addColorStop(1, "rgba(0,0,0,0)");
200
+ ctx.fillStyle = grad;
201
+ ctx.beginPath();
202
+ ctx.arc(gx, gy, gr, 0, Math.PI * 2);
203
+ ctx.fill();
204
+ ctx.restore();
205
+ }
206
+ ctx.restore();
207
+ // ---------- BURST HEARTS (B fill) ----------------------------------------
208
+ // A flurry of little hearts fly outward along seeded directions, arc under a
209
+ // little "gravity", spin, and shrink as they go. Fully crisp vector hearts.
210
+ if (b > 0.001) {
211
+ const count = Math.max(0, Math.round(params.burstCount));
212
+ const maxDist = minDim * params.burstSpread;
213
+ ctx.save();
214
+ ctx.globalCompositeOperation = "lighter";
215
+ for (let i = 0; i < count; i++) {
216
+ // deterministic per-heart launch params.
217
+ const ang = (i / count) * Math.PI * 2 + (rng() - 0.5) * 0.9;
218
+ const speed = 0.55 + rng() * 0.45; // some fly farther
219
+ const spin = (rng() - 0.5) * 2.0;
220
+ const littleS = minDim * (0.035 + rng() * 0.04) * params.heartScale * 1.6;
221
+ // staggered launch so they don't all leave at once.
222
+ const stagger = rng() * 0.25;
223
+ const lp = Math.max(0, Math.min(1, (b - stagger) / (1 - stagger)));
224
+ if (lp <= 0)
225
+ continue;
226
+ const dist = maxDist * speed * lp;
227
+ // arc: a parabola so they rise then fall slightly.
228
+ const arc = minDim * 0.10 * speed * (lp - lp * lp) * 4.0;
229
+ const px = cx + Math.cos(ang) * dist;
230
+ const py = cy + Math.sin(ang) * dist - arc;
231
+ // fade + shrink late in the flight.
232
+ const fade = 1 - Math.pow(lp, 2.2);
233
+ if (fade <= 0.01)
234
+ continue;
235
+ const a = Math.round(255 * presence * fade);
236
+ const s = littleS * (0.6 + 0.4 * (1 - lp));
237
+ ctx.save();
238
+ ctx.translate(px, py);
239
+ traceHeart(ctx, s, spin * lp * Math.PI);
240
+ ctx.fillStyle = `rgba(0,0,${a},1)`;
241
+ ctx.fill();
242
+ ctx.restore();
243
+ }
244
+ ctx.restore();
245
+ }
246
+ }
247
+ //# sourceMappingURL=heartburst-renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heartburst-renderer.js","sourceRoot":"","sources":["../src/heartburst-renderer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * GLSL ES 3.00 source for **Heartburst** — Dopamine's love / like / favorite
3
+ * moment: a heart swells with a double-beat (lub-dub) then BURSTS into a flurry
4
+ * of small hearts that fly outward, arc, and fade.
5
+ *
6
+ * This is a HYBRID (Canvas2D-panel) effect. The crisp vector hearts — the big
7
+ * hero heart and the little burst hearts — are drawn with a parametric heart
8
+ * curve into an OFFSCREEN Canvas2D each frame (heartburst-renderer.ts) and
9
+ * handed to this shader as a single "panel" texture. The shader then does
10
+ * everything that wants to be procedural / screen-space:
11
+ * - a soft warm BLOOM behind the heart (the love glow),
12
+ * - a tight GLOSS / specular highlight on the hero heart at the photoreal end,
13
+ * - the NOIR ↔ POP styling: a moody near-monochrome heart with one warm spot
14
+ * color (noir) → a flat saturated cel "sticker" heart with a hard rim and
15
+ * halftone blush (pop), keyed off uStyle/uSaturation,
16
+ * - a hot beat FLASH that throws warm light onto the page (the cast).
17
+ *
18
+ * Everything is summed as light (canvas is black, composited via
19
+ * `mix-blend-mode: screen`, so black == no change, bright == cast light).
20
+ *
21
+ * Panel texture channel encoding (see heartburst-renderer.ts):
22
+ * R = hero heart FILL mask (the big swelling heart's interior)
23
+ * G = INK / contour mask (heart outline + the gloss seed highlight)
24
+ * B = burst hearts FILL (all the little flying hearts)
25
+ * A = unused
26
+ *
27
+ * Pure function of uniforms → frame-perfect & cheap under SwiftShader.
28
+ */
29
+ export declare const HEARTBURST_VERTEX_SRC = "#version 300 es\nout vec2 vUv;\nvoid main() {\n vec2 pos = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n vUv = pos;\n gl_Position = vec4(pos * 2.0 - 1.0, 0.0, 1.0);\n}";
30
+ export declare const HEARTBURST_FRAGMENT_SRC = "#version 300 es\nprecision highp float;\nin vec2 vUv;\nout vec4 fragColor;\n\nuniform sampler2D uPanel; // R=heartFill G=ink B=burstFill\nuniform vec2 uResolution; // device pixels\nuniform vec2 uOrigin; // heart centre (the anchor), device px\nuniform float uLife; // whole-effect progress 0..1\nuniform float uTimeS; // elapsed seconds\nuniform float uPresence; // panel opacity / presence 0..1\nuniform float uBeat; // 0..1 current beat amplitude (lub-dub thump)\nuniform float uBurst; // 0..1 burst progress (little hearts flying out)\nuniform float uFlash; // 0..1 warm beat/burst flash amount\nuniform float uExposure; // cast-light gain\nuniform float uGlow; // 0..1 soft bloom radius/strength behind the heart\nuniform float uGloss; // 0..1 specular gloss on the hero heart (photoreal)\nuniform float uHalftone; // 0..1 Ben-Day blush dot strength (pop)\nuniform float uDotSize; // halftone cell size in device px\nuniform float uSaturation; // 0..1 panel color saturation (noir->pop)\nuniform float uSeed; // per-fire hash\nuniform float uStyle; // 0..1 photoreal/noir -> flat cel sticker (whimsy)\nuniform float uShadow; // 0 = light pass (screen), 1 = shadow pass (multiply)\nuniform vec2 uShadowOffset; // device-px offset of the cast silhouette\nuniform float uShadowSoft; // penumbra softness in device px\nuniform float uShadowStrength;// 0..1 max darkening of the multiply layer\nuniform vec3 uC0; // hero heart core color (warm red)\nuniform vec3 uC1; // heart shade / burst color (pink/coral)\nuniform vec3 uC2; // accent / glow / blush color\n\n\n#define TAU 6.28318530718\n\n\nfloat hash11(float p){ p = fract(p * 0.1031); p *= p + 33.33; p *= p + p; return fract(p); }\nvec2 hash21(float p){\n vec3 p3 = fract(vec3(p) * vec3(0.1031, 0.1030, 0.0973));\n p3 += dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\n\nmat2 rot2(float a){ float s = sin(a), c = cos(a); return mat2(c, -s, s, c); }\n\n\nfloat benday(vec2 frag, float cell, float v, float ang){\n vec2 p = rot2(ang) * frag / cell;\n vec2 g = fract(p) - 0.5;\n float d = length(g);\n float r = 0.52 * sqrt(clamp(v, 0.0, 1.0));\n float aa = 0.7 / cell + fwidth(d);\n return 1.0 - smoothstep(r - aa, r + aa, d);\n}\n\n\nvec3 tonemapACES(vec3 x){\n const float a = 2.51, b = 0.03, c = 2.43, d = 0.59, e = 0.14;\n return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);\n}\n\n\nvec3 ditherAdd(vec3 col, vec2 frag, float t, float fade){\n float dz = hash11(dot(frag, vec2(12.989, 78.233)) + t) - 0.5;\n return col + (dz / 255.0) * fade;\n}\n\n\nvoid main(){\n vec2 frag = vUv * uResolution;\n vec2 res = uResolution;\n float minDim = min(res.x, res.y);\n\n // ---- SHADOW PASS (multiply layer) ---------------------------------------\n // Cheap occlusion: the panel's solid forms (hero + burst fills) sampled at an\n // offset toward the implied key light, with a small ring blur for a penumbra.\n // White = no shadow (multiply identity); darker = cast shadow. Presence fades\n // it with the effect.\n if (uShadow > 0.5) {\n vec2 px = 1.0 / res;\n vec2 souv = vUv - uShadowOffset * px;\n float occ = 0.0;\n for (int i = 0; i < 8; i++) {\n float a = float(i) / 8.0 * TAU;\n vec2 o = vec2(cos(a), sin(a)) * uShadowSoft * px;\n vec2 tuv = souv + o;\n vec2 inb = step(vec2(0.0), tuv) * step(tuv, vec2(1.0));\n float mask = inb.x * inb.y;\n vec4 s = texture(uPanel, tuv);\n occ += clamp(s.r + s.b, 0.0, 1.0) * mask;\n }\n occ /= 8.0;\n float dark = clamp(occ * uShadowStrength, 0.0, 1.0);\n fragColor = vec4(vec3(1.0 - dark), 1.0);\n return;\n }\n\n vec2 fromC = frag - uOrigin;\n float rad = length(fromC);\n\n vec4 panel = texture(uPanel, vUv);\n float heartFill = panel.r;\n float ink = panel.g;\n float burstFill = panel.b;\n\n vec3 col = vec3(0.0);\n\n // ---- SOFT BLOOM behind the heart (the love glow) ------------------------\n // A warm radial glow centred on the heart, pulsing with the beat + flaring on\n // the burst. Sampled as a smooth falloff so it reads as light blooming behind\n // the form, not a hard disc. Warmer (toward uC2) as it goes pop.\n float glowR = minDim * (0.18 + 0.30 * uGlow) * (1.0 + 0.25 * uBeat);\n float bloom = exp(-rad / glowR);\n float bloomAmp = (0.35 + 0.65 * uBeat) * (0.6 + 0.8 * uBurst * (1.0 - uBurst) * 3.0);\n vec3 glowCol = mix(uC0, uC2, 0.45 + 0.3 * uSaturation);\n col += glowCol * bloom * bloomAmp * uPresence * uGlow * uExposure * 0.9;\n\n // ---- HERO HEART ---------------------------------------------------------\n // The big swelling heart. A rich warm body with a vertical light->shade\n // gradient (top catches the key light). Photoreal end: smooth gradient + a\n // tight gloss highlight. Pop end: flatter, more saturated, with a halftone\n // blush and a crisp rim.\n // Vertical shading term: 1 at the top of the panel, 0 at the bottom.\n float vshade = clamp(1.0 - vUv.y, 0.0, 1.0);\n vec3 bodyLit = mix(uC1, uC0, 0.35 + 0.65 * uSaturation); // mid body\n vec3 bodyHi = clamp(bodyLit * 1.5 + 0.18, 0.0, 1.6); // lit top\n vec3 bodyLow = bodyLit * 0.55; // shaded base\n // Photoreal: smooth top->bottom gradient. Cel: snap to two flat zones.\n float g = smoothstep(0.15, 0.95, vshade);\n float gCel = step(0.5, vshade);\n float grad = mix(g, gCel, uStyle);\n vec3 heartCol = mix(bodyLow, bodyHi, grad);\n\n // Soft inner-rim self-shadow toward the silhouette so the form reads round\n // (photoreal); fades out toward the flat cel sticker.\n // (We approximate the rim from how isolated the fill is via a small blur.)\n float edge = 0.0;\n {\n vec2 px = 1.0 / res;\n for (int i = 0; i < 6; i++){\n float a = float(i) / 6.0 * TAU;\n edge += texture(uPanel, vUv + vec2(cos(a), sin(a)) * px * 3.0).r;\n }\n edge /= 6.0;\n }\n float rimDark = clamp((heartFill - edge), 0.0, 1.0); // bright near the outline\n heartCol *= 1.0 - rimDark * 0.5 * (1.0 - uStyle);\n\n // Halftone blush on the heart toward the pop end (printed sticker shading).\n float blush = benday(frag, uDotSize, mix(0.35, 0.6, uHalftone), radians(20.0) + uSeed);\n heartCol += (uC2 - heartCol) * blush * uHalftone * uStyle * 0.28;\n\n col += heartCol * heartFill * uPresence * uExposure * 1.6;\n\n // GLOSS: a tight specular highlight near the upper-left of the heart at the\n // photoreal end (a glassy gel-heart). Seeded by the ink-channel highlight blob\n // the renderer paints, modulated up by the beat (the heart \"shines\" as it\n // thumps). Vanishes toward the flat cel end.\n float gloss = ink * heartFill; // ink highlight that sits ON the fill\n float glossAmt = uGloss * (1.0 - uStyle) * (0.6 + 0.6 * uBeat);\n col += vec3(1.0) * gloss * glossAmt * uPresence * 1.4;\n\n // ---- BURST: the flurry of little hearts ---------------------------------\n // Drawn fully in the panel (positions/arc/scale computed in JS for crisp\n // vector hearts). Here we just light them \u2014 saturated warm fills with a soft\n // self-glow, fading as they fly out (uBurst late => dimmer).\n float burstFade = 1.0 - smoothstep(0.55, 1.0, uBurst);\n vec3 littleCol = mix(uC1, uC2, 0.3 + 0.4 * uSaturation);\n littleCol = clamp(littleCol * 1.25 + 0.1, 0.0, 1.5);\n col += littleCol * burstFill * uPresence * burstFade * uExposure * 1.5;\n // a soft sparkle bloom around the little hearts so they twinkle as they go.\n col += littleCol * burstFill * 0.4 * burstFade * (0.5 + 0.5 * sin(uTimeS * 30.0 + uSeed));\n\n // ---- INK / CONTOUR ------------------------------------------------------\n // Bold outline. On a screen-blend canvas ink is the ABSENCE of light, so the\n // contour CARVES the lit shapes (reads as a dark outline) \u2014 strongest toward\n // the flat cel sticker (a clean black keyline), softer at the photoreal end.\n // The gloss seed (ink ON the fill) is NOT carved (handled above as highlight).\n float contour = ink * (1.0 - heartFill); // outline pixels only\n float carve = contour * uPresence * mix(0.45, 0.95, uStyle);\n col *= (1.0 - carve);\n\n // ---- BEAT / BURST FLASH -------------------------------------------------\n // A warm flash that throws colored light onto the page on each thump and at\n // the burst. Fast spike (driven by uFlash), warm core.\n float flashFall = exp(-rad / (minDim * 0.40));\n vec3 flashCol = mix(uC0, vec3(1.0, 0.85, 0.8), 0.4 + 0.25 * uStyle);\n col += flashCol * flashFall * uFlash * uExposure * 1.2;\n // tiny white-hot core at the very centre on the strongest beats.\n float core = exp(-rad / (minDim * 0.08));\n col += vec3(1.0, 0.92, 0.9) * core * uFlash * uBeat * 1.3;\n\n // ---- TONE + FINISH ------------------------------------------------------\n col = tonemapACES(col * 0.9);\n\n // Cel posterize toward the pop/sticker end (flat printed color); leaves the\n // dark page untouched so we don't shatter it.\n if (uStyle > 0.001) {\n float lit = smoothstep(0.02, 0.2, max(max(col.r, col.g), col.b));\n vec3 q = floor(col * 4.0 + 0.5) / 4.0;\n col = mix(col, mix(col, q, lit), uStyle * 0.7);\n }\n\n // Ordered dither to kill banding the screen blend reveals (faded toward cel).\n col = ditherAdd(col, frag, uTimeS, 1.0 - uStyle * 0.7);\n\n fragColor = vec4(max(col, 0.0), 1.0);\n}";
31
+ //# sourceMappingURL=heartburst-shader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heartburst-shader.d.ts","sourceRoot":"","sources":["../src/heartburst-shader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAWH,eAAO,MAAM,qBAAqB,gMAMhC,CAAC;AAEH,eAAO,MAAM,uBAAuB,gzSAgLlC,CAAC"}
@@ -0,0 +1,214 @@
1
+ /**
2
+ * GLSL ES 3.00 source for **Heartburst** — Dopamine's love / like / favorite
3
+ * moment: a heart swells with a double-beat (lub-dub) then BURSTS into a flurry
4
+ * of small hearts that fly outward, arc, and fade.
5
+ *
6
+ * This is a HYBRID (Canvas2D-panel) effect. The crisp vector hearts — the big
7
+ * hero heart and the little burst hearts — are drawn with a parametric heart
8
+ * curve into an OFFSCREEN Canvas2D each frame (heartburst-renderer.ts) and
9
+ * handed to this shader as a single "panel" texture. The shader then does
10
+ * everything that wants to be procedural / screen-space:
11
+ * - a soft warm BLOOM behind the heart (the love glow),
12
+ * - a tight GLOSS / specular highlight on the hero heart at the photoreal end,
13
+ * - the NOIR ↔ POP styling: a moody near-monochrome heart with one warm spot
14
+ * color (noir) → a flat saturated cel "sticker" heart with a hard rim and
15
+ * halftone blush (pop), keyed off uStyle/uSaturation,
16
+ * - a hot beat FLASH that throws warm light onto the page (the cast).
17
+ *
18
+ * Everything is summed as light (canvas is black, composited via
19
+ * `mix-blend-mode: screen`, so black == no change, bright == cast light).
20
+ *
21
+ * Panel texture channel encoding (see heartburst-renderer.ts):
22
+ * R = hero heart FILL mask (the big swelling heart's interior)
23
+ * G = INK / contour mask (heart outline + the gloss seed highlight)
24
+ * B = burst hearts FILL (all the little flying hearts)
25
+ * A = unused
26
+ *
27
+ * Pure function of uniforms → frame-perfect & cheap under SwiftShader.
28
+ */
29
+ import { GLSL_CONSTANTS, GLSL_DITHER, GLSL_HALFTONE, GLSL_HASH, GLSL_ROT2, GLSL_TONEMAP_ACES, } from "@dopaminefx/core";
30
+ export const HEARTBURST_VERTEX_SRC = /* glsl */ `#version 300 es
31
+ out vec2 vUv;
32
+ void main() {
33
+ vec2 pos = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
34
+ vUv = pos;
35
+ gl_Position = vec4(pos * 2.0 - 1.0, 0.0, 1.0);
36
+ }`;
37
+ export const HEARTBURST_FRAGMENT_SRC = /* glsl */ `#version 300 es
38
+ precision highp float;
39
+ in vec2 vUv;
40
+ out vec4 fragColor;
41
+
42
+ uniform sampler2D uPanel; // R=heartFill G=ink B=burstFill
43
+ uniform vec2 uResolution; // device pixels
44
+ uniform vec2 uOrigin; // heart centre (the anchor), device px
45
+ uniform float uLife; // whole-effect progress 0..1
46
+ uniform float uTimeS; // elapsed seconds
47
+ uniform float uPresence; // panel opacity / presence 0..1
48
+ uniform float uBeat; // 0..1 current beat amplitude (lub-dub thump)
49
+ uniform float uBurst; // 0..1 burst progress (little hearts flying out)
50
+ uniform float uFlash; // 0..1 warm beat/burst flash amount
51
+ uniform float uExposure; // cast-light gain
52
+ uniform float uGlow; // 0..1 soft bloom radius/strength behind the heart
53
+ uniform float uGloss; // 0..1 specular gloss on the hero heart (photoreal)
54
+ uniform float uHalftone; // 0..1 Ben-Day blush dot strength (pop)
55
+ uniform float uDotSize; // halftone cell size in device px
56
+ uniform float uSaturation; // 0..1 panel color saturation (noir->pop)
57
+ uniform float uSeed; // per-fire hash
58
+ uniform float uStyle; // 0..1 photoreal/noir -> flat cel sticker (whimsy)
59
+ uniform float uShadow; // 0 = light pass (screen), 1 = shadow pass (multiply)
60
+ uniform vec2 uShadowOffset; // device-px offset of the cast silhouette
61
+ uniform float uShadowSoft; // penumbra softness in device px
62
+ uniform float uShadowStrength;// 0..1 max darkening of the multiply layer
63
+ uniform vec3 uC0; // hero heart core color (warm red)
64
+ uniform vec3 uC1; // heart shade / burst color (pink/coral)
65
+ uniform vec3 uC2; // accent / glow / blush color
66
+
67
+ ${GLSL_CONSTANTS}
68
+ ${GLSL_HASH}
69
+ ${GLSL_ROT2}
70
+ ${GLSL_HALFTONE}
71
+ ${GLSL_TONEMAP_ACES}
72
+ ${GLSL_DITHER}
73
+
74
+ void main(){
75
+ vec2 frag = vUv * uResolution;
76
+ vec2 res = uResolution;
77
+ float minDim = min(res.x, res.y);
78
+
79
+ // ---- SHADOW PASS (multiply layer) ---------------------------------------
80
+ // Cheap occlusion: the panel's solid forms (hero + burst fills) sampled at an
81
+ // offset toward the implied key light, with a small ring blur for a penumbra.
82
+ // White = no shadow (multiply identity); darker = cast shadow. Presence fades
83
+ // it with the effect.
84
+ if (uShadow > 0.5) {
85
+ vec2 px = 1.0 / res;
86
+ vec2 souv = vUv - uShadowOffset * px;
87
+ float occ = 0.0;
88
+ for (int i = 0; i < 8; i++) {
89
+ float a = float(i) / 8.0 * TAU;
90
+ vec2 o = vec2(cos(a), sin(a)) * uShadowSoft * px;
91
+ vec2 tuv = souv + o;
92
+ vec2 inb = step(vec2(0.0), tuv) * step(tuv, vec2(1.0));
93
+ float mask = inb.x * inb.y;
94
+ vec4 s = texture(uPanel, tuv);
95
+ occ += clamp(s.r + s.b, 0.0, 1.0) * mask;
96
+ }
97
+ occ /= 8.0;
98
+ float dark = clamp(occ * uShadowStrength, 0.0, 1.0);
99
+ fragColor = vec4(vec3(1.0 - dark), 1.0);
100
+ return;
101
+ }
102
+
103
+ vec2 fromC = frag - uOrigin;
104
+ float rad = length(fromC);
105
+
106
+ vec4 panel = texture(uPanel, vUv);
107
+ float heartFill = panel.r;
108
+ float ink = panel.g;
109
+ float burstFill = panel.b;
110
+
111
+ vec3 col = vec3(0.0);
112
+
113
+ // ---- SOFT BLOOM behind the heart (the love glow) ------------------------
114
+ // A warm radial glow centred on the heart, pulsing with the beat + flaring on
115
+ // the burst. Sampled as a smooth falloff so it reads as light blooming behind
116
+ // the form, not a hard disc. Warmer (toward uC2) as it goes pop.
117
+ float glowR = minDim * (0.18 + 0.30 * uGlow) * (1.0 + 0.25 * uBeat);
118
+ float bloom = exp(-rad / glowR);
119
+ float bloomAmp = (0.35 + 0.65 * uBeat) * (0.6 + 0.8 * uBurst * (1.0 - uBurst) * 3.0);
120
+ vec3 glowCol = mix(uC0, uC2, 0.45 + 0.3 * uSaturation);
121
+ col += glowCol * bloom * bloomAmp * uPresence * uGlow * uExposure * 0.9;
122
+
123
+ // ---- HERO HEART ---------------------------------------------------------
124
+ // The big swelling heart. A rich warm body with a vertical light->shade
125
+ // gradient (top catches the key light). Photoreal end: smooth gradient + a
126
+ // tight gloss highlight. Pop end: flatter, more saturated, with a halftone
127
+ // blush and a crisp rim.
128
+ // Vertical shading term: 1 at the top of the panel, 0 at the bottom.
129
+ float vshade = clamp(1.0 - vUv.y, 0.0, 1.0);
130
+ vec3 bodyLit = mix(uC1, uC0, 0.35 + 0.65 * uSaturation); // mid body
131
+ vec3 bodyHi = clamp(bodyLit * 1.5 + 0.18, 0.0, 1.6); // lit top
132
+ vec3 bodyLow = bodyLit * 0.55; // shaded base
133
+ // Photoreal: smooth top->bottom gradient. Cel: snap to two flat zones.
134
+ float g = smoothstep(0.15, 0.95, vshade);
135
+ float gCel = step(0.5, vshade);
136
+ float grad = mix(g, gCel, uStyle);
137
+ vec3 heartCol = mix(bodyLow, bodyHi, grad);
138
+
139
+ // Soft inner-rim self-shadow toward the silhouette so the form reads round
140
+ // (photoreal); fades out toward the flat cel sticker.
141
+ // (We approximate the rim from how isolated the fill is via a small blur.)
142
+ float edge = 0.0;
143
+ {
144
+ vec2 px = 1.0 / res;
145
+ for (int i = 0; i < 6; i++){
146
+ float a = float(i) / 6.0 * TAU;
147
+ edge += texture(uPanel, vUv + vec2(cos(a), sin(a)) * px * 3.0).r;
148
+ }
149
+ edge /= 6.0;
150
+ }
151
+ float rimDark = clamp((heartFill - edge), 0.0, 1.0); // bright near the outline
152
+ heartCol *= 1.0 - rimDark * 0.5 * (1.0 - uStyle);
153
+
154
+ // Halftone blush on the heart toward the pop end (printed sticker shading).
155
+ float blush = benday(frag, uDotSize, mix(0.35, 0.6, uHalftone), radians(20.0) + uSeed);
156
+ heartCol += (uC2 - heartCol) * blush * uHalftone * uStyle * 0.28;
157
+
158
+ col += heartCol * heartFill * uPresence * uExposure * 1.6;
159
+
160
+ // GLOSS: a tight specular highlight near the upper-left of the heart at the
161
+ // photoreal end (a glassy gel-heart). Seeded by the ink-channel highlight blob
162
+ // the renderer paints, modulated up by the beat (the heart "shines" as it
163
+ // thumps). Vanishes toward the flat cel end.
164
+ float gloss = ink * heartFill; // ink highlight that sits ON the fill
165
+ float glossAmt = uGloss * (1.0 - uStyle) * (0.6 + 0.6 * uBeat);
166
+ col += vec3(1.0) * gloss * glossAmt * uPresence * 1.4;
167
+
168
+ // ---- BURST: the flurry of little hearts ---------------------------------
169
+ // Drawn fully in the panel (positions/arc/scale computed in JS for crisp
170
+ // vector hearts). Here we just light them — saturated warm fills with a soft
171
+ // self-glow, fading as they fly out (uBurst late => dimmer).
172
+ float burstFade = 1.0 - smoothstep(0.55, 1.0, uBurst);
173
+ vec3 littleCol = mix(uC1, uC2, 0.3 + 0.4 * uSaturation);
174
+ littleCol = clamp(littleCol * 1.25 + 0.1, 0.0, 1.5);
175
+ col += littleCol * burstFill * uPresence * burstFade * uExposure * 1.5;
176
+ // a soft sparkle bloom around the little hearts so they twinkle as they go.
177
+ col += littleCol * burstFill * 0.4 * burstFade * (0.5 + 0.5 * sin(uTimeS * 30.0 + uSeed));
178
+
179
+ // ---- INK / CONTOUR ------------------------------------------------------
180
+ // Bold outline. On a screen-blend canvas ink is the ABSENCE of light, so the
181
+ // contour CARVES the lit shapes (reads as a dark outline) — strongest toward
182
+ // the flat cel sticker (a clean black keyline), softer at the photoreal end.
183
+ // The gloss seed (ink ON the fill) is NOT carved (handled above as highlight).
184
+ float contour = ink * (1.0 - heartFill); // outline pixels only
185
+ float carve = contour * uPresence * mix(0.45, 0.95, uStyle);
186
+ col *= (1.0 - carve);
187
+
188
+ // ---- BEAT / BURST FLASH -------------------------------------------------
189
+ // A warm flash that throws colored light onto the page on each thump and at
190
+ // the burst. Fast spike (driven by uFlash), warm core.
191
+ float flashFall = exp(-rad / (minDim * 0.40));
192
+ vec3 flashCol = mix(uC0, vec3(1.0, 0.85, 0.8), 0.4 + 0.25 * uStyle);
193
+ col += flashCol * flashFall * uFlash * uExposure * 1.2;
194
+ // tiny white-hot core at the very centre on the strongest beats.
195
+ float core = exp(-rad / (minDim * 0.08));
196
+ col += vec3(1.0, 0.92, 0.9) * core * uFlash * uBeat * 1.3;
197
+
198
+ // ---- TONE + FINISH ------------------------------------------------------
199
+ col = tonemapACES(col * 0.9);
200
+
201
+ // Cel posterize toward the pop/sticker end (flat printed color); leaves the
202
+ // dark page untouched so we don't shatter it.
203
+ if (uStyle > 0.001) {
204
+ float lit = smoothstep(0.02, 0.2, max(max(col.r, col.g), col.b));
205
+ vec3 q = floor(col * 4.0 + 0.5) / 4.0;
206
+ col = mix(col, mix(col, q, lit), uStyle * 0.7);
207
+ }
208
+
209
+ // Ordered dither to kill banding the screen blend reveals (faded toward cel).
210
+ col = ditherAdd(col, frag, uTimeS, 1.0 - uStyle * 0.7);
211
+
212
+ fragColor = vec4(max(col, 0.0), 1.0);
213
+ }`;
214
+ //# sourceMappingURL=heartburst-shader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heartburst-shader.js","sourceRoot":"","sources":["../src/heartburst-shader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EACL,cAAc,EACd,WAAW,EACX,aAAa,EACb,SAAS,EACT,SAAS,EACT,iBAAiB,GAClB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,CAAC,MAAM,qBAAqB,GAAG,UAAU,CAAC;;;;;;EAM9C,CAAC;AAEH,MAAM,CAAC,MAAM,uBAAuB,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8BhD,cAAc;EACd,SAAS;EACT,SAAS;EACT,aAAa;EACb,iBAAiB;EACjB,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6IX,CAAC"}