@dopaminefx/effect-comic 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,299 @@
1
+ /**
2
+ * Comic Impact Canvas2D PANEL drawing + bundled-font loading.
3
+ *
4
+ * The crisp, vector-y parts of Comic Impact — the jagged starburst and the
5
+ * blocky hand-lettered onomatopoeia word with bold ink outlines — are drawn into
6
+ * an OFFSCREEN Canvas2D each frame (cheap: a few paths) and uploaded as the
7
+ * "panel" texture; the fragment shader (comic-shader.ts) adds the Ben-Day
8
+ * halftone, action lines, flash and noir↔pop-art styling. `drawPanel` (the
9
+ * offscreen draw) and `ensureComicFonts` (the bundled-face loader) live here and
10
+ * are consumed by the Comic `EffectFactory` (effects/comic.ts), which owns the
11
+ * GL pass via the shared, program-cached context + the conductor.
12
+ *
13
+ * Panel channel encoding consumed by the shader:
14
+ * R = word fill · G = ink (all black contours) · B = starburst fill
15
+ */
16
+ import { isCheckmark } from "./comic-params.js";
17
+ import { mulberry32 } from "@dopaminefx/core";
18
+ import { impactScale, impactPresence } from "./comic-tempo.js";
19
+ import { EMBEDDED_FACES } from "./comic-fonts.js";
20
+ // ---------------------------------------------------------------------------
21
+ // BUNDLED FONT LOADING
22
+ //
23
+ // The effect must NOT silently depend on a host font being installed, so the
24
+ // SIL OFL display faces (Bangers / Anton / Luckiest Guy) ship base64-embedded
25
+ // (comic-fonts.ts) and are registered via the FontFace API. We kick this off
26
+ // once at module import and await it before the first paint; if it fails for
27
+ // any reason the renderer still draws using the robust fallback stack (and the
28
+ // mood/whimsy difference still reads via the procedural treatment).
29
+ // ---------------------------------------------------------------------------
30
+ let fontsReady = null;
31
+ function base64ToArrayBuffer(b64) {
32
+ const bin = atob(b64);
33
+ const len = bin.length;
34
+ const bytes = new Uint8Array(len);
35
+ for (let i = 0; i < len; i++)
36
+ bytes[i] = bin.charCodeAt(i);
37
+ return bytes.buffer;
38
+ }
39
+ /** Register + load the embedded faces once. Resolves even if loading fails. */
40
+ export function ensureComicFonts() {
41
+ if (fontsReady)
42
+ return fontsReady;
43
+ if (typeof document === "undefined" ||
44
+ typeof FontFace === "undefined" ||
45
+ !document.fonts) {
46
+ fontsReady = Promise.resolve();
47
+ return fontsReady;
48
+ }
49
+ fontsReady = (async () => {
50
+ await Promise.all(EMBEDDED_FACES.map(async (f) => {
51
+ try {
52
+ // Skip if a face by this family is already registered (e.g. host has it).
53
+ const face = new FontFace(f.family, base64ToArrayBuffer(f.base64));
54
+ await face.load();
55
+ document.fonts.add(face);
56
+ }
57
+ catch {
58
+ /* fall back to the system stack for this face */
59
+ }
60
+ }));
61
+ try {
62
+ await document.fonts.ready;
63
+ }
64
+ catch {
65
+ /* ignore */
66
+ }
67
+ })();
68
+ return fontsReady;
69
+ }
70
+ // Begin loading as soon as the module is imported so faces are usually ready by
71
+ // the time the user fires the effect.
72
+ if (typeof document !== "undefined")
73
+ void ensureComicFonts();
74
+ /**
75
+ * Draw the offscreen panel for this frame: a jagged starburst balloon, the
76
+ * onomatopoeia word in blocky outlined block caps, all at the current impact
77
+ * scale + a tiny rotation tilt. Encodes masks into channels:
78
+ * R = word fill, G = ink (contours), B = burst fill.
79
+ *
80
+ * We draw ink as the GREEN channel and the two fills as RED/BLUE so we can
81
+ * blend them independently in the shader. To keep channels independent we draw
82
+ * each layer onto the same 2D context but only write the intended channel.
83
+ */
84
+ /** Starburst + word size relative to the targeted element box (≈1.5×). See the
85
+ * Swift `COMIC_TARGET_FILL` — keep the two in sync. */
86
+ const COMIC_TARGET_FILL = 1.7;
87
+ /**
88
+ * The per-frame panel draw in the generic `PanelDraw` shape — the ONE
89
+ * code-shaped hook the data-driven factory wires (`registerDopePanelEffect`).
90
+ * Computes the DRAW-SIDE tempo (the per-letter slam SCALE + the panel presence,
91
+ * which stay code by design — the per-frame values the SHADER reads ride
92
+ * `tempo.frame`) and hands off to {@link drawPanel}.
93
+ */
94
+ export const drawComicFrame = (pctx, w, h, params, info) => {
95
+ const p = params;
96
+ const scale = impactScale(info.elapsedMs, p.overshoot);
97
+ const presence = impactPresence(info.life);
98
+ const span = Math.min(info.targetPx.width, info.targetPx.height);
99
+ drawPanel(pctx, w, h, p, scale, presence, info.dpr, info.centerPx, span);
100
+ };
101
+ export function drawPanel(ctx, w, h, params, scale, presence, dpr, center, span) {
102
+ ctx.clearRect(0, 0, w, h);
103
+ if (presence <= 0.001)
104
+ return;
105
+ // Position + size the word/starburst to the targeted element (defaults to the
106
+ // canvas centre + full canvas, reproducing the old screen-centred pose).
107
+ const cx = center.x;
108
+ const cy = center.y;
109
+ // The starburst + word read at ~150% of the targeted element, clamped to the
110
+ // canvas so a full-page fire (target == canvas) keeps its original size. Kept in
111
+ // sync with ComicPanel.swift. TUNABLE.
112
+ const minDim = Math.min(span * COMIC_TARGET_FILL, Math.min(w, h));
113
+ const rng = mulberry32((params.comicSeed * 1000) >>> 0);
114
+ // Deterministic per-fire tilt so the panel feels hand-placed (a few degrees).
115
+ const tilt = ((params.comicSeed % 1) - 0.5) * 0.18; // ~±5deg
116
+ // ---------- STARBURST BALLOON (B channel) --------------------------------
117
+ // A classic many-pointed jagged star: alternating long/short radii with
118
+ // per-point jitter. Drawn solid into BLUE; its bold outline into GREEN.
119
+ const points = Math.max(8, Math.round(params.burstPoints));
120
+ const outerR = minDim * params.scale * 1.3 * scale;
121
+ const innerR = outerR * 0.64;
122
+ const burstPts = [];
123
+ for (let i = 0; i < points * 2; i++) {
124
+ const t = i / (points * 2);
125
+ const a = t * Math.PI * 2 - Math.PI / 2 + tilt;
126
+ const even = i % 2 === 0;
127
+ const jitter = 0.82 + rng() * 0.36;
128
+ const r = (even ? outerR : innerR) * jitter;
129
+ burstPts.push([cx + Math.cos(a) * r, cy + Math.sin(a) * r]);
130
+ }
131
+ const tracePath = () => {
132
+ ctx.beginPath();
133
+ burstPts.forEach(([x, y], i) => (i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y)));
134
+ ctx.closePath();
135
+ };
136
+ const ink = params.inkWeight * dpr * scale;
137
+ // Burst FILL -> BLUE only.
138
+ ctx.save();
139
+ ctx.globalCompositeOperation = "lighter"; // additive into channels
140
+ tracePath();
141
+ ctx.fillStyle = `rgba(0,0,${Math.round(255 * presence)},1)`;
142
+ ctx.fill();
143
+ ctx.restore();
144
+ // Burst OUTLINE -> GREEN (ink). Thick bold contour.
145
+ ctx.save();
146
+ ctx.globalCompositeOperation = "lighter";
147
+ ctx.lineJoin = "miter";
148
+ ctx.miterLimit = 2;
149
+ tracePath();
150
+ ctx.lineWidth = ink * 1.3;
151
+ ctx.strokeStyle = `rgba(0,${Math.round(255 * presence)},0,1)`;
152
+ ctx.stroke();
153
+ ctx.restore();
154
+ // ---------- LETTERING (success word) or CHECKMARK ------------------------
155
+ // Mood selects the bundled display face + base character (skew/stretch/tilt);
156
+ // whimsy shifts the treatment from restrained inked caps (noir) to fat,
157
+ // inflated, multi-layer-inked, 3D-extruded, per-letter-bounced pop-art. The
158
+ // owner also wants a big bold ✓ as a selectable option — that's drawn as a
159
+ // VECTOR path (not a font glyph) so it's reliable everywhere.
160
+ const fillA = Math.round(255 * presence);
161
+ const inkStyle = `rgba(0,${fillA},0,1)`;
162
+ const fillStyle = `rgba(${fillA},0,0,1)`;
163
+ const round = params.inkRoundness;
164
+ // Per-letter / per-shape deterministic jitter, derived from the per-fire seed.
165
+ const jrng = mulberry32((params.comicSeed * 2654435761) >>> 0);
166
+ ctx.save();
167
+ ctx.translate(cx, cy);
168
+ ctx.rotate(tilt + params.fontTilt);
169
+ // Italic lean + non-uniform stretch as a shared transform on the whole word.
170
+ // matrix: [stretchX, 0, skewX, 1] (a=stretch horiz, c=shear).
171
+ ctx.transform(params.fontStretchX, 0, params.fontSkew, 1, 0, 0);
172
+ ctx.lineJoin = round > 0.5 ? "round" : "miter";
173
+ ctx.lineCap = round > 0.5 ? "round" : "butt";
174
+ ctx.miterLimit = 2;
175
+ ctx.globalCompositeOperation = "lighter"; // additive into channels
176
+ if (isCheckmark(params.word)) {
177
+ // ----- VECTOR CHECKMARK -----------------------------------------------
178
+ // A bold two-segment tick centred on the panel, sized to the burst's inner
179
+ // span. Drawn as a stroked path; ink contour + 3D extrude + bright fill use
180
+ // the same treatment knobs as the word path below.
181
+ const span = innerR * 1.25; // overall check width
182
+ const strokeW = span * 0.24 * (0.85 + round * 0.25);
183
+ const extrude = span * params.extrudeDepth;
184
+ // Check geometry (down-stroke then long up-flick), centred.
185
+ const pts = [
186
+ [-span * 0.42, span * 0.02],
187
+ [-span * 0.12, span * 0.34],
188
+ [span * 0.46, -span * 0.36],
189
+ ];
190
+ const traceCheck = () => {
191
+ ctx.beginPath();
192
+ pts.forEach(([x, y], i) => (i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y)));
193
+ };
194
+ // 3D extrude: stacked ink copies stepping down-right (pop-art only).
195
+ if (extrude > 0.5) {
196
+ const steps = 8;
197
+ for (let s = steps; s >= 1; s--) {
198
+ const dx = (extrude * s) / steps;
199
+ const dy = (extrude * s) / steps;
200
+ ctx.save();
201
+ ctx.translate(dx, dy);
202
+ traceCheck();
203
+ ctx.lineWidth = strokeW;
204
+ ctx.strokeStyle = inkStyle;
205
+ ctx.stroke();
206
+ ctx.restore();
207
+ }
208
+ }
209
+ // Bold ink contour (heavier toward pop-art via outlineLayers).
210
+ traceCheck();
211
+ ctx.lineWidth = strokeW + ink * (1.2 + params.outlineLayers * 0.5);
212
+ ctx.strokeStyle = inkStyle;
213
+ ctx.stroke();
214
+ // Bright fill body.
215
+ traceCheck();
216
+ ctx.lineWidth = strokeW;
217
+ ctx.strokeStyle = fillStyle;
218
+ ctx.stroke();
219
+ ctx.restore();
220
+ return;
221
+ }
222
+ // ----- WORD RUN ---------------------------------------------------------
223
+ const word = params.word;
224
+ ctx.textAlign = "center";
225
+ ctx.textBaseline = "middle";
226
+ const fontFor = (px) => `${px}px ${params.fontStack}`;
227
+ // Target size, then SHRINK-TO-FIT so longer words (GREAT!/DONE!) never spill
228
+ // out of the burst. Account for the extra horizontal stretch + tracking.
229
+ let fontPx = minDim * params.scale * 0.92 * scale;
230
+ ctx.font = fontFor(fontPx);
231
+ const chars = [...word];
232
+ const trackPx = () => fontPx * params.fontTracking;
233
+ const runWidth = () => {
234
+ let total = 0;
235
+ for (const ch of chars)
236
+ total += ctx.measureText(ch).width + trackPx();
237
+ return Math.max(1, total - trackPx());
238
+ };
239
+ const maxW = (innerR * 1.7) / Math.max(0.6, params.fontStretchX);
240
+ let measured = runWidth();
241
+ if (measured > maxW) {
242
+ fontPx *= maxW / measured;
243
+ ctx.font = fontFor(fontPx);
244
+ measured = runWidth();
245
+ }
246
+ const extrude = fontPx * params.extrudeDepth;
247
+ const inkLine = ink * (1.3 + (params.outlineLayers - 1) * 0.7);
248
+ // Lay out letters individually so we can apply per-letter rotation/baseline
249
+ // jitter (the pop-art bounce). Start at the left edge of the centred run.
250
+ let penX = -measured / 2;
251
+ const letters = chars.map((ch) => {
252
+ const wpx = ctx.measureText(ch).width;
253
+ const x = penX + wpx / 2;
254
+ penX += wpx + trackPx();
255
+ const rot = (jrng() - 0.5) * 2 * params.letterRotJitter;
256
+ const dy = (jrng() - 0.5) * 2 * params.letterBaselineJitter * fontPx;
257
+ return { ch, x, rot, dy, wgt: jrng() };
258
+ });
259
+ const drawLetters = (cb) => {
260
+ for (const l of letters) {
261
+ ctx.save();
262
+ ctx.translate(l.x, l.dy);
263
+ ctx.rotate(l.rot);
264
+ cb(ctx, l);
265
+ ctx.restore();
266
+ }
267
+ };
268
+ // 3D extrude / drop: stacked ink copies stepping down-right behind the body
269
+ // (pop-art pops, flat at noir).
270
+ if (extrude > 0.5) {
271
+ const steps = 8;
272
+ for (let s = steps; s >= 1; s--) {
273
+ const dx = (extrude * s) / steps;
274
+ const dy = (extrude * s) / steps;
275
+ drawLetters((c, l) => {
276
+ c.fillStyle = inkStyle;
277
+ c.fillText(l.ch, dx, dy);
278
+ });
279
+ }
280
+ }
281
+ // Bold INK contour — drawn under the fill so the outline frames the letters.
282
+ // outlineLayers stacks slightly fattening passes for the inflated balloon look.
283
+ for (let layer = params.outlineLayers; layer >= 1; layer--) {
284
+ const lw = inkLine * (1 + (layer - 1) * 0.5);
285
+ drawLetters((c, l) => {
286
+ c.lineJoin = round > 0.5 ? "round" : "miter";
287
+ c.lineWidth = lw;
288
+ c.strokeStyle = inkStyle;
289
+ c.strokeText(l.ch, 0, 0);
290
+ });
291
+ }
292
+ // Bright FILL body on top.
293
+ drawLetters((c, l) => {
294
+ c.fillStyle = fillStyle;
295
+ c.fillText(l.ch, 0, 0);
296
+ });
297
+ ctx.restore();
298
+ }
299
+ //# sourceMappingURL=comic-renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comic-renderer.js","sourceRoot":"","sources":["../src/comic-renderer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * GLSL ES 3.00 source for **Comic Impact** — Dopamine's third success effect: a
3
+ * Golden/Silver-Age comic-book "BAM! POW!" fight-panel impact.
4
+ *
5
+ * This is a HYBRID effect. Crisp blocky vector lettering and bold ink contours
6
+ * are hard to do well in a pure fragment shader, so the renderer draws the
7
+ * onomatopoeia word + jagged starburst + ink outlines into an OFFSCREEN Canvas2D
8
+ * and hands it to this shader as a single "panel" texture. The shader then does
9
+ * everything that wants to be procedural and screen-space:
10
+ * - Ben-Day / halftone DOT shading (rotated screen, dot radius driven by the
11
+ * underlying value) — subtle/fine at the noir end, loud/large at pop-art.
12
+ * - RADIATING action / speed lines bursting from the impact centre.
13
+ * - A FLASH that throws colored light onto the page (the screen-blend cast).
14
+ * - The NOIR ↔ POP-ART styling: near-monochrome high-contrast chiaroscuro with
15
+ * one spot color → screaming saturated pop, keyed off uStyle/uSaturation.
16
+ *
17
+ * Everything is summed as light (canvas is black, composited via
18
+ * `mix-blend-mode: screen`, so black == no change, bright == cast light).
19
+ *
20
+ * Panel texture channel encoding (see comic-renderer.ts):
21
+ * R = word FILL mask (letter interiors)
22
+ * G = INK mask (all black ink: letter + burst + line outlines)
23
+ * B = burst FILL mask (starburst balloon interior, behind the word)
24
+ * A = unused
25
+ *
26
+ * Pure function of uniforms → frame-perfect & cheap under SwiftShader.
27
+ */
28
+ export declare const COMIC_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}";
29
+ export declare const COMIC_FRAGMENT_SRC = "#version 300 es\nprecision highp float;\nin vec2 vUv;\nout vec4 fragColor;\n\nuniform sampler2D uPanel; // R=wordFill G=ink B=burstFill\nuniform vec2 uResolution; // device pixels\nuniform vec2 uTarget; // targeted element size (device px); scales the action lines\nuniform vec2 uOrigin; // impact 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 uFlash; // 0..1 impact flash amount (fast spike, decays)\nuniform float uExposure; // cast-light gain\nuniform float uHalftone; // 0..1 Ben-Day dot strength\nuniform float uDotSize; // Ben-Day cell size in device px\nuniform float uSaturation; // 0..1 panel color saturation (noir->pop)\nuniform float uActionLines; // count of radiating speed lines\nuniform float uInkBoost; // ink darkness/spread multiplier (pop fattens ink)\nuniform float uSeed; // per-fire hash\nuniform float uStyle; // 0..1 noir -> pop-art (whimsy)\nuniform float uShadow; // 0 = light pass (screen), 1 = shadow pass (multiply)\nuniform vec2 uShadowOffset; // device-px offset of the cast silhouette (away from light)\nuniform float uShadowSoft; // penumbra softness in device px (blur tap radius)\nuniform float uShadowStrength;// 0..1 max darkening of the multiply layer\nuniform vec3 uC0; // word fill color\nuniform vec3 uC1; // secondary / burst color\nuniform vec3 uC2; // dot / accent 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 // The word/burst (panel) are sized to the targeted element box; scale the\n // radiating action lines to the SAME basis so they streak off the word, not the\n // whole canvas. Clamped to the canvas so a full-page fire (uTarget == res) is\n // unchanged. Must match the comic renderer's COMIC_TARGET_FILL (1.7).\n float comicSpan = min(min(uTarget.x, uTarget.y) * 1.7, minDim);\n\n // ---- SHADOW PASS (multiply layer) ---------------------------------------\n // Cheap occlusion: the panel's solid forms (word fill + burst fill) sampled\n // at an offset toward the implied key light, with a small ring blur for a\n // penumbra. White = no shadow (multiply identity); darker = cast shadow. The\n // panel already encodes presence, so the shadow fades 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 // Gate samples that fall OUTSIDE the panel: the texture is CLAMP_TO_EDGE,\n // so without this an offset sample past an edge smears that edge row into\n // a phantom band (the streaks at the top of the frame). Outside == no\n // occluder == no shadow.\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 float ang = atan(fromC.y, fromC.x);\n\n vec4 panel = texture(uPanel, vUv);\n float wordFill = panel.r;\n float inkMask = clamp(panel.g * uInkBoost, 0.0, 1.0);\n float burstFill = panel.b;\n\n vec3 col = vec3(0.0);\n\n // ---- RADIATING ACTION / SPEED LINES -------------------------------------\n // Thin wedges bursting outward from the impact centre. Procedural so they're\n // crisp and cheap. They live in a ring OUTSIDE the burst balloon (so they\n // read as motion lines streaking off the hit, not hatching on the word).\n float lineN = max(uActionLines, 1.0);\n float a01 = (ang / TAU) + 0.5; // 0..1 around the circle\n float idx = floor(a01 * lineN);\n // per-line random angular jitter + length so they aren't a clean fan.\n float jr = hash11(idx + uSeed * 3.1);\n float jr2 = hash11(idx * 1.7 + uSeed * 7.3);\n float cellPhase = fract(a01 * lineN);\n float wedge = abs(cellPhase - 0.5);\n // Thin tapered streaks: a sharp spine that fattens slightly outward (classic\n // motion-line wedge), kept narrow so they read as speed lines, not pie slices.\n float thick = mix(0.05, 0.14, jr);\n float lineBody = 1.0 - smoothstep(thick * 0.35, thick, wedge);\n // radial extent: lines start OUTSIDE the burst and streak outward to the edge.\n float innerR = comicSpan * (0.30 + 0.05 * jr2);\n float outerR = comicSpan * (0.46 + 0.30 * jr);\n float radialMask = smoothstep(innerR, innerR + comicSpan * 0.015, rad)\n * (1.0 - smoothstep(outerR - comicSpan * 0.10, outerR, rad));\n // fade the lines in fast on impact, hold, then they thin out late.\n float linePresence = smoothstep(0.0, 0.06, uLife) * (1.0 - smoothstep(0.6, 1.0, uLife));\n // taper opacity along the line so the inner end is boldest (ink-streak feel).\n float taper = 1.0 - smoothstep(innerR, outerR, rad);\n float lines = lineBody * radialMask * linePresence * taper;\n // animate-on-twos flicker toward the pop end (snappy comic motion).\n float beat = floor(uTimeS * 12.0);\n float flick = mix(1.0, step(0.25, hash11(idx + beat + uSeed)), uStyle * 0.5);\n lines *= flick;\n\n // Action lines cast a thin streak of light off the hit. White/cool ink at the\n // noir end (a hard glint), the accent hue at the pop end. Kept dim so they\n // read as speed lines around the panel rather than flooding the frame.\n vec3 lineCol = mix(vec3(0.7, 0.74, 0.82), uC2, uStyle);\n col += lineCol * lines * 0.32 * uExposure;\n\n // ---- STARBURST BALLOON (behind the word) --------------------------------\n // Filled with the secondary hue; gets the strongest Ben-Day shading so it\n // reads as a flat printed color field. In noir it's a pale near-white field\n // with a fine subtle screen; in pop-art it's a saturated yellow/red blast.\n vec3 burstBase = mix(vec3(0.9), uC1, uSaturation);\n // tone for the dots: more dots where the field is \"darker\" value. We want a\n // lively mid coverage so the classic dot field shows.\n float burstTone = mix(0.35, 0.7, uHalftone);\n float dots = benday(frag, uDotSize, burstTone, radians(15.0) + uSeed);\n // Ben-Day strength: subtle at noir, dominant at pop. The dots ADD the accent\n // color on the printed field.\n vec3 burstCol = burstBase + (uC2 - burstBase) * dots * uHalftone * 0.55;\n col += burstCol * burstFill * uPresence * uExposure;\n\n // A second, finer rotated screen on the word fill for that printed sheen at\n // the pop end (kept subtle so letters stay legible). The word is the HERO:\n // a bright, saturated fill that screams off the page (pop) or a luminous\n // near-white with a spot tint (noir). Brighter than the burst so it reads\n // as the foreground shout.\n float wordDots = benday(frag, uDotSize * 0.7, 0.5, radians(75.0) + uSeed);\n vec3 wordBright = clamp(uC0 * 1.35 + 0.25, 0.0, 1.4);\n vec3 wordBase = mix(vec3(0.96, 0.97, 1.0), wordBright, clamp(uSaturation + 0.2, 0.0, 1.0));\n vec3 wordCol = wordBase + (uC2 - wordBase) * wordDots * uHalftone * 0.25 * uStyle;\n // Word fill is largely PROTECTED from ink suppression (its own outline should\n // frame it, not eat it), so render it after a softened ink mask below.\n col += wordCol * wordFill * uPresence * uExposure * 1.7;\n\n // ---- INK ----------------------------------------------------------------\n // Bold black contours. Ink is the ABSENCE of light on a screen-blend canvas,\n // so we can't literally darken the page from here \u2014 instead we let ink CARVE\n // the lit shapes (it suppresses the fills it overlaps) and, at the noir end,\n // we add a faint cool rim so the chiaroscuro edge still reads as light catches\n // the ink ridge. The actual black is achieved by NOT lighting those pixels.\n float ink = inkMask * uPresence;\n // Suppress fills under ink (so outlines punch through as unlit black). But\n // where the ink overlaps the WORD fill we soften the carve a lot, so the\n // outline frames the letters instead of eating their bright bodies.\n // Gentle carve: the outline should FRAME the burst, not gut it. A near-total\n // carve (old 0.96) left the burst outline reading as a transparent gap that\n // masked the balloon \u2014 worse at high whimsy, where inkBoost fattens the ink.\n // Keep the word's soft carve (0.26 at wordFill=1) but darken the burst outline\n // only partway so the balloon shows through.\n float carve = ink * (0.45 - 0.19 * wordFill);\n col *= (1.0 - carve);\n // Subtle chiaroscuro rim-light on ink edges toward the noir end (a glint).\n float rim = ink * (1.0 - uStyle) * 0.18;\n col += mix(uC2, vec3(0.8, 0.85, 1.0), 0.5) * rim * uExposure;\n\n // ---- IMPACT FLASH -------------------------------------------------------\n // A hot radial flash at the moment of impact that throws colored light onto\n // the page (the cast-light proof). Fast spike, quick decay (driven by uFlash).\n float flashFall = exp(-rad / (minDim * 0.42));\n vec3 flashCol = mix(mix(uC0, uC1, 0.5), vec3(1.0), 0.45 + 0.3 * uStyle);\n col += flashCol * flashFall * uFlash * uExposure * 1.4;\n // a tight white-hot core right at the centre on the very first frames.\n float core = exp(-rad / (minDim * 0.10));\n col += vec3(1.0) * core * uFlash * uFlash * 1.6;\n\n // ---- TONE + FINISH ------------------------------------------------------\n // ACES filmic tonemap (shared look/glsl) for a cleaner highlight rolloff than\n // the old x/(1+x) compress \u2014 the impact flash highlights roll off gracefully\n // while the saturated printed mids stay rich. A mild pre-exposure keeps the\n // pop-art color from dimming.\n col = tonemapACES(col * 0.85);\n\n // Pop-art posterize: snap the lit panel to a few flat ink levels toward the\n // pop end (flat printed color), leaving the dark page untouched so we don't\n // shatter it into camouflage. Noir stays smooth chiaroscuro.\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 (shared look/glsl) to kill banding the screen-blend reveals\n // (faded toward the pop end where the flat printed look is intended).\n col = ditherAdd(col, frag, uTimeS, 1.0 - uStyle * 0.7);\n\n fragColor = vec4(max(col, 0.0), 1.0);\n}";
30
+ //# sourceMappingURL=comic-shader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comic-shader.d.ts","sourceRoot":"","sources":["../src/comic-shader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAWH,eAAO,MAAM,gBAAgB,gMAM3B,CAAC;AAEH,eAAO,MAAM,kBAAkB,m/WAyM7B,CAAC"}
@@ -0,0 +1,238 @@
1
+ /**
2
+ * GLSL ES 3.00 source for **Comic Impact** — Dopamine's third success effect: a
3
+ * Golden/Silver-Age comic-book "BAM! POW!" fight-panel impact.
4
+ *
5
+ * This is a HYBRID effect. Crisp blocky vector lettering and bold ink contours
6
+ * are hard to do well in a pure fragment shader, so the renderer draws the
7
+ * onomatopoeia word + jagged starburst + ink outlines into an OFFSCREEN Canvas2D
8
+ * and hands it to this shader as a single "panel" texture. The shader then does
9
+ * everything that wants to be procedural and screen-space:
10
+ * - Ben-Day / halftone DOT shading (rotated screen, dot radius driven by the
11
+ * underlying value) — subtle/fine at the noir end, loud/large at pop-art.
12
+ * - RADIATING action / speed lines bursting from the impact centre.
13
+ * - A FLASH that throws colored light onto the page (the screen-blend cast).
14
+ * - The NOIR ↔ POP-ART styling: near-monochrome high-contrast chiaroscuro with
15
+ * one spot color → screaming saturated pop, keyed off uStyle/uSaturation.
16
+ *
17
+ * Everything is summed as light (canvas is black, composited via
18
+ * `mix-blend-mode: screen`, so black == no change, bright == cast light).
19
+ *
20
+ * Panel texture channel encoding (see comic-renderer.ts):
21
+ * R = word FILL mask (letter interiors)
22
+ * G = INK mask (all black ink: letter + burst + line outlines)
23
+ * B = burst FILL mask (starburst balloon interior, behind the word)
24
+ * A = unused
25
+ *
26
+ * Pure function of uniforms → frame-perfect & cheap under SwiftShader.
27
+ */
28
+ import { GLSL_CONSTANTS, GLSL_DITHER, GLSL_HALFTONE, GLSL_HASH, GLSL_ROT2, GLSL_TONEMAP_ACES, } from "@dopaminefx/core";
29
+ export const COMIC_VERTEX_SRC = /* glsl */ `#version 300 es
30
+ out vec2 vUv;
31
+ void main() {
32
+ vec2 pos = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
33
+ vUv = pos;
34
+ gl_Position = vec4(pos * 2.0 - 1.0, 0.0, 1.0);
35
+ }`;
36
+ export const COMIC_FRAGMENT_SRC = /* glsl */ `#version 300 es
37
+ precision highp float;
38
+ in vec2 vUv;
39
+ out vec4 fragColor;
40
+
41
+ uniform sampler2D uPanel; // R=wordFill G=ink B=burstFill
42
+ uniform vec2 uResolution; // device pixels
43
+ uniform vec2 uTarget; // targeted element size (device px); scales the action lines
44
+ uniform vec2 uOrigin; // impact 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 uFlash; // 0..1 impact flash amount (fast spike, decays)
49
+ uniform float uExposure; // cast-light gain
50
+ uniform float uHalftone; // 0..1 Ben-Day dot strength
51
+ uniform float uDotSize; // Ben-Day cell size in device px
52
+ uniform float uSaturation; // 0..1 panel color saturation (noir->pop)
53
+ uniform float uActionLines; // count of radiating speed lines
54
+ uniform float uInkBoost; // ink darkness/spread multiplier (pop fattens ink)
55
+ uniform float uSeed; // per-fire hash
56
+ uniform float uStyle; // 0..1 noir -> pop-art (whimsy)
57
+ uniform float uShadow; // 0 = light pass (screen), 1 = shadow pass (multiply)
58
+ uniform vec2 uShadowOffset; // device-px offset of the cast silhouette (away from light)
59
+ uniform float uShadowSoft; // penumbra softness in device px (blur tap radius)
60
+ uniform float uShadowStrength;// 0..1 max darkening of the multiply layer
61
+ uniform vec3 uC0; // word fill color
62
+ uniform vec3 uC1; // secondary / burst color
63
+ uniform vec3 uC2; // dot / accent color
64
+
65
+ ${GLSL_CONSTANTS}
66
+ ${GLSL_HASH}
67
+ ${GLSL_ROT2}
68
+ ${GLSL_HALFTONE}
69
+ ${GLSL_TONEMAP_ACES}
70
+ ${GLSL_DITHER}
71
+
72
+ void main(){
73
+ vec2 frag = vUv * uResolution;
74
+ vec2 res = uResolution;
75
+ float minDim = min(res.x, res.y);
76
+ // The word/burst (panel) are sized to the targeted element box; scale the
77
+ // radiating action lines to the SAME basis so they streak off the word, not the
78
+ // whole canvas. Clamped to the canvas so a full-page fire (uTarget == res) is
79
+ // unchanged. Must match the comic renderer's COMIC_TARGET_FILL (1.7).
80
+ float comicSpan = min(min(uTarget.x, uTarget.y) * 1.7, minDim);
81
+
82
+ // ---- SHADOW PASS (multiply layer) ---------------------------------------
83
+ // Cheap occlusion: the panel's solid forms (word fill + burst fill) sampled
84
+ // at an offset toward the implied key light, with a small ring blur for a
85
+ // penumbra. White = no shadow (multiply identity); darker = cast shadow. The
86
+ // panel already encodes presence, so the shadow fades with the effect.
87
+ if (uShadow > 0.5) {
88
+ vec2 px = 1.0 / res;
89
+ vec2 souv = vUv - uShadowOffset * px;
90
+ float occ = 0.0;
91
+ for (int i = 0; i < 8; i++) {
92
+ float a = float(i) / 8.0 * TAU;
93
+ vec2 o = vec2(cos(a), sin(a)) * uShadowSoft * px;
94
+ vec2 tuv = souv + o;
95
+ // Gate samples that fall OUTSIDE the panel: the texture is CLAMP_TO_EDGE,
96
+ // so without this an offset sample past an edge smears that edge row into
97
+ // a phantom band (the streaks at the top of the frame). Outside == no
98
+ // occluder == no shadow.
99
+ vec2 inb = step(vec2(0.0), tuv) * step(tuv, vec2(1.0));
100
+ float mask = inb.x * inb.y;
101
+ vec4 s = texture(uPanel, tuv);
102
+ occ += clamp(s.r + s.b, 0.0, 1.0) * mask;
103
+ }
104
+ occ /= 8.0;
105
+ float dark = clamp(occ * uShadowStrength, 0.0, 1.0);
106
+ fragColor = vec4(vec3(1.0 - dark), 1.0);
107
+ return;
108
+ }
109
+
110
+ vec2 fromC = frag - uOrigin;
111
+ float rad = length(fromC);
112
+ float ang = atan(fromC.y, fromC.x);
113
+
114
+ vec4 panel = texture(uPanel, vUv);
115
+ float wordFill = panel.r;
116
+ float inkMask = clamp(panel.g * uInkBoost, 0.0, 1.0);
117
+ float burstFill = panel.b;
118
+
119
+ vec3 col = vec3(0.0);
120
+
121
+ // ---- RADIATING ACTION / SPEED LINES -------------------------------------
122
+ // Thin wedges bursting outward from the impact centre. Procedural so they're
123
+ // crisp and cheap. They live in a ring OUTSIDE the burst balloon (so they
124
+ // read as motion lines streaking off the hit, not hatching on the word).
125
+ float lineN = max(uActionLines, 1.0);
126
+ float a01 = (ang / TAU) + 0.5; // 0..1 around the circle
127
+ float idx = floor(a01 * lineN);
128
+ // per-line random angular jitter + length so they aren't a clean fan.
129
+ float jr = hash11(idx + uSeed * 3.1);
130
+ float jr2 = hash11(idx * 1.7 + uSeed * 7.3);
131
+ float cellPhase = fract(a01 * lineN);
132
+ float wedge = abs(cellPhase - 0.5);
133
+ // Thin tapered streaks: a sharp spine that fattens slightly outward (classic
134
+ // motion-line wedge), kept narrow so they read as speed lines, not pie slices.
135
+ float thick = mix(0.05, 0.14, jr);
136
+ float lineBody = 1.0 - smoothstep(thick * 0.35, thick, wedge);
137
+ // radial extent: lines start OUTSIDE the burst and streak outward to the edge.
138
+ float innerR = comicSpan * (0.30 + 0.05 * jr2);
139
+ float outerR = comicSpan * (0.46 + 0.30 * jr);
140
+ float radialMask = smoothstep(innerR, innerR + comicSpan * 0.015, rad)
141
+ * (1.0 - smoothstep(outerR - comicSpan * 0.10, outerR, rad));
142
+ // fade the lines in fast on impact, hold, then they thin out late.
143
+ float linePresence = smoothstep(0.0, 0.06, uLife) * (1.0 - smoothstep(0.6, 1.0, uLife));
144
+ // taper opacity along the line so the inner end is boldest (ink-streak feel).
145
+ float taper = 1.0 - smoothstep(innerR, outerR, rad);
146
+ float lines = lineBody * radialMask * linePresence * taper;
147
+ // animate-on-twos flicker toward the pop end (snappy comic motion).
148
+ float beat = floor(uTimeS * 12.0);
149
+ float flick = mix(1.0, step(0.25, hash11(idx + beat + uSeed)), uStyle * 0.5);
150
+ lines *= flick;
151
+
152
+ // Action lines cast a thin streak of light off the hit. White/cool ink at the
153
+ // noir end (a hard glint), the accent hue at the pop end. Kept dim so they
154
+ // read as speed lines around the panel rather than flooding the frame.
155
+ vec3 lineCol = mix(vec3(0.7, 0.74, 0.82), uC2, uStyle);
156
+ col += lineCol * lines * 0.32 * uExposure;
157
+
158
+ // ---- STARBURST BALLOON (behind the word) --------------------------------
159
+ // Filled with the secondary hue; gets the strongest Ben-Day shading so it
160
+ // reads as a flat printed color field. In noir it's a pale near-white field
161
+ // with a fine subtle screen; in pop-art it's a saturated yellow/red blast.
162
+ vec3 burstBase = mix(vec3(0.9), uC1, uSaturation);
163
+ // tone for the dots: more dots where the field is "darker" value. We want a
164
+ // lively mid coverage so the classic dot field shows.
165
+ float burstTone = mix(0.35, 0.7, uHalftone);
166
+ float dots = benday(frag, uDotSize, burstTone, radians(15.0) + uSeed);
167
+ // Ben-Day strength: subtle at noir, dominant at pop. The dots ADD the accent
168
+ // color on the printed field.
169
+ vec3 burstCol = burstBase + (uC2 - burstBase) * dots * uHalftone * 0.55;
170
+ col += burstCol * burstFill * uPresence * uExposure;
171
+
172
+ // A second, finer rotated screen on the word fill for that printed sheen at
173
+ // the pop end (kept subtle so letters stay legible). The word is the HERO:
174
+ // a bright, saturated fill that screams off the page (pop) or a luminous
175
+ // near-white with a spot tint (noir). Brighter than the burst so it reads
176
+ // as the foreground shout.
177
+ float wordDots = benday(frag, uDotSize * 0.7, 0.5, radians(75.0) + uSeed);
178
+ vec3 wordBright = clamp(uC0 * 1.35 + 0.25, 0.0, 1.4);
179
+ vec3 wordBase = mix(vec3(0.96, 0.97, 1.0), wordBright, clamp(uSaturation + 0.2, 0.0, 1.0));
180
+ vec3 wordCol = wordBase + (uC2 - wordBase) * wordDots * uHalftone * 0.25 * uStyle;
181
+ // Word fill is largely PROTECTED from ink suppression (its own outline should
182
+ // frame it, not eat it), so render it after a softened ink mask below.
183
+ col += wordCol * wordFill * uPresence * uExposure * 1.7;
184
+
185
+ // ---- INK ----------------------------------------------------------------
186
+ // Bold black contours. Ink is the ABSENCE of light on a screen-blend canvas,
187
+ // so we can't literally darken the page from here — instead we let ink CARVE
188
+ // the lit shapes (it suppresses the fills it overlaps) and, at the noir end,
189
+ // we add a faint cool rim so the chiaroscuro edge still reads as light catches
190
+ // the ink ridge. The actual black is achieved by NOT lighting those pixels.
191
+ float ink = inkMask * uPresence;
192
+ // Suppress fills under ink (so outlines punch through as unlit black). But
193
+ // where the ink overlaps the WORD fill we soften the carve a lot, so the
194
+ // outline frames the letters instead of eating their bright bodies.
195
+ // Gentle carve: the outline should FRAME the burst, not gut it. A near-total
196
+ // carve (old 0.96) left the burst outline reading as a transparent gap that
197
+ // masked the balloon — worse at high whimsy, where inkBoost fattens the ink.
198
+ // Keep the word's soft carve (0.26 at wordFill=1) but darken the burst outline
199
+ // only partway so the balloon shows through.
200
+ float carve = ink * (0.45 - 0.19 * wordFill);
201
+ col *= (1.0 - carve);
202
+ // Subtle chiaroscuro rim-light on ink edges toward the noir end (a glint).
203
+ float rim = ink * (1.0 - uStyle) * 0.18;
204
+ col += mix(uC2, vec3(0.8, 0.85, 1.0), 0.5) * rim * uExposure;
205
+
206
+ // ---- IMPACT FLASH -------------------------------------------------------
207
+ // A hot radial flash at the moment of impact that throws colored light onto
208
+ // the page (the cast-light proof). Fast spike, quick decay (driven by uFlash).
209
+ float flashFall = exp(-rad / (minDim * 0.42));
210
+ vec3 flashCol = mix(mix(uC0, uC1, 0.5), vec3(1.0), 0.45 + 0.3 * uStyle);
211
+ col += flashCol * flashFall * uFlash * uExposure * 1.4;
212
+ // a tight white-hot core right at the centre on the very first frames.
213
+ float core = exp(-rad / (minDim * 0.10));
214
+ col += vec3(1.0) * core * uFlash * uFlash * 1.6;
215
+
216
+ // ---- TONE + FINISH ------------------------------------------------------
217
+ // ACES filmic tonemap (shared look/glsl) for a cleaner highlight rolloff than
218
+ // the old x/(1+x) compress — the impact flash highlights roll off gracefully
219
+ // while the saturated printed mids stay rich. A mild pre-exposure keeps the
220
+ // pop-art color from dimming.
221
+ col = tonemapACES(col * 0.85);
222
+
223
+ // Pop-art posterize: snap the lit panel to a few flat ink levels toward the
224
+ // pop end (flat printed color), leaving the dark page untouched so we don't
225
+ // shatter it into camouflage. Noir stays smooth chiaroscuro.
226
+ if (uStyle > 0.001) {
227
+ float lit = smoothstep(0.02, 0.2, max(max(col.r, col.g), col.b));
228
+ vec3 q = floor(col * 4.0 + 0.5) / 4.0;
229
+ col = mix(col, mix(col, q, lit), uStyle * 0.7);
230
+ }
231
+
232
+ // Ordered dither (shared look/glsl) to kill banding the screen-blend reveals
233
+ // (faded toward the pop end where the flat printed look is intended).
234
+ col = ditherAdd(col, frag, uTimeS, 1.0 - uStyle * 0.7);
235
+
236
+ fragColor = vec4(max(col, 0.0), 1.0);
237
+ }`;
238
+ //# sourceMappingURL=comic-shader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comic-shader.js","sourceRoot":"","sources":["../src/comic-shader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EACL,cAAc,EACd,WAAW,EACX,aAAa,EACb,SAAS,EACT,SAAS,EACT,iBAAiB,GAClB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,CAAC,MAAM,gBAAgB,GAAG,UAAU,CAAC;;;;;;EAMzC,CAAC;AAEH,MAAM,CAAC,MAAM,kBAAkB,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6B3C,cAAc;EACd,SAAS;EACT,SAAS;EACT,aAAa;EACb,iBAAiB;EACjB,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuKX,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Comic Impact bespoke timing — the slam/recoil + proud-hold-then-fade.
3
+ *
4
+ * The word arrives oversized and slams down past its rest size, recoils (a quick
5
+ * spring), holds, then eases out at the tail. Deliberately very short IMPACT so
6
+ * the word reads as a punch landing, not a tween. Built on `easeOutCubic`.
7
+ */
8
+ /** Window (ms) over which the comic onomatopoeia word SLAMS in. */
9
+ export declare const IMPACT_MS = 200;
10
+ /** Hold (ms) the word sits proud at full size before it begins to settle out. */
11
+ export declare const IMPACT_HOLD_MS = 650;
12
+ /**
13
+ * Comic impact SCALE over elapsed ms. Returns a multiplier on rest size: large
14
+ * at t≈0, slamming to ≈1 by IMPACT_MS (with a small spring), then resting.
15
+ * `overshoot` scales the slam magnitude (driven by intensity).
16
+ */
17
+ export declare function impactScale(elapsedMs: number, overshoot?: number): number;
18
+ /**
19
+ * Comic impact OPACITY/presence over normalized life (0..1). A near-instant
20
+ * appearance, a long proud hold, then a quick fade at the very end so the panel
21
+ * clears. The fade occupies the last ~18%.
22
+ */
23
+ export declare function impactPresence(life: number): number;
24
+ //# sourceMappingURL=comic-tempo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comic-tempo.d.ts","sourceRoot":"","sources":["../src/comic-tempo.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,mEAAmE;AACnE,eAAO,MAAM,SAAS,MAAM,CAAC;AAE7B,iFAAiF;AACjF,eAAO,MAAM,cAAc,MAAM,CAAC;AAElC;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,SAAI,GAAG,MAAM,CAWpE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMnD"}