@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.
- package/dist/heartburst-renderer.d.ts +84 -0
- package/dist/heartburst-renderer.d.ts.map +1 -0
- package/dist/heartburst-renderer.js +247 -0
- package/dist/heartburst-renderer.js.map +1 -0
- package/dist/heartburst-shader.d.ts +31 -0
- package/dist/heartburst-shader.d.ts.map +1 -0
- package/dist/heartburst-shader.js +214 -0
- package/dist/heartburst-shader.js.map +1 -0
- package/dist/heartburst.dope.json +1827 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
- package/src/heartburst-renderer.ts +289 -0
- package/src/heartburst-shader.ts +223 -0
- package/src/heartburst.dope.json +1827 -0
- package/src/index.ts +40 -0
|
@@ -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"}
|