bubble-maker 0.0.0-stage → 2.0.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/index.js ADDED
@@ -0,0 +1,736 @@
1
+ // src/engine.ts
2
+ var BubbleField = class {
3
+ constructor(o = {}) {
4
+ this.bubbles = [];
5
+ this.pointer = { x: -1e5, y: -1e5, inside: false };
6
+ this.focused = null;
7
+ this.hoverEnabled = true;
8
+ this.bounds = null;
9
+ this.pv = { x: 0, y: 0 };
10
+ // pointer velocity (px/s), eased
11
+ this.last = { x: -1e5, y: -1e5, t: 0 };
12
+ this.stiffness = o.stiffness ?? 40;
13
+ this.damping = o.damping ?? 6;
14
+ this.iterations = o.iterations ?? 3;
15
+ this.gap = o.gap ?? 2;
16
+ this.reaction = o.reaction ?? 3;
17
+ }
18
+ add(init) {
19
+ const b = {
20
+ px: init.x,
21
+ py: init.y,
22
+ r: init.base,
23
+ grow: 1.55,
24
+ hover: 0,
25
+ hx: init.x,
26
+ hy: init.y,
27
+ shape: 0,
28
+ alpha: 1,
29
+ ghost: false,
30
+ fixed: false,
31
+ kind: "pearl",
32
+ ...init
33
+ };
34
+ this.bubbles.push(b);
35
+ return b;
36
+ }
37
+ /** Gives a bubble an instant velocity (px/s). */
38
+ kick(b, vx, vy, dt = 1 / 60) {
39
+ b.px -= vx * dt;
40
+ b.py -= vy * dt;
41
+ }
42
+ hit(x, y) {
43
+ for (let i = this.bubbles.length - 1; i >= 0; i--) {
44
+ const b = this.bubbles[i];
45
+ const dx = x - b.x;
46
+ const dy = y - b.y;
47
+ if (dx * dx + dy * dy < b.r * b.r && b.alpha > 0.2) return b;
48
+ }
49
+ return null;
50
+ }
51
+ step(dtIn) {
52
+ const dt = Math.min(dtIn, 1 / 30);
53
+ const bs = this.bubbles;
54
+ const ease = (rate) => 1 - Math.exp(-dt * rate);
55
+ const hovered = this.hoverEnabled && this.pointer.inside ? this.hit(this.pointer.x, this.pointer.y) : null;
56
+ const friction = Math.exp(-this.damping * dt);
57
+ const level = Math.min(Math.max(this.reaction, 0), 10);
58
+ const growScale = Math.min(level / 3, 1) * (1 + Math.max(level - 3, 0) / 7 * 0.6);
59
+ const air = Math.max(level - 2, 0) / 8;
60
+ const pt = this.pointer;
61
+ this.last.t += dt;
62
+ if (pt.x !== this.last.x || pt.y !== this.last.y) {
63
+ if (this.last.x > -1e4 && pt.inside) {
64
+ const t = Math.max(this.last.t, 1 / 240);
65
+ this.pv.x += ((pt.x - this.last.x) / t - this.pv.x) * 0.6;
66
+ this.pv.y += ((pt.y - this.last.y) / t - this.pv.y) * 0.6;
67
+ }
68
+ this.last.x = pt.x;
69
+ this.last.y = pt.y;
70
+ this.last.t = 0;
71
+ } else if (this.last.t > 0.06) {
72
+ this.pv.x *= 1 - ease(10);
73
+ this.pv.y *= 1 - ease(10);
74
+ }
75
+ const blowing = air > 0 && this.hoverEnabled && pt.inside;
76
+ const reach = 60 + 160 * air;
77
+ for (const b of bs) {
78
+ const on = b === hovered || b === this.focused ? 1 : 0;
79
+ b.hover += (on - b.hover) * ease(9);
80
+ const target = b.base * (1 + (b.grow - 1) * growScale * b.hover);
81
+ b.r += (target - b.r) * ease(11);
82
+ if (b.fixed) {
83
+ b.px = b.x;
84
+ b.py = b.y;
85
+ continue;
86
+ }
87
+ const vx = (b.x - b.px) * friction;
88
+ const vy = (b.y - b.py) * friction;
89
+ let ax = this.stiffness * (b.hx - b.x);
90
+ let ay = this.stiffness * (b.hy - b.y);
91
+ if (blowing && b.alpha > 0.2 && b !== hovered) {
92
+ const dx = b.x - pt.x;
93
+ const dy = b.y - pt.y;
94
+ const d = Math.hypot(dx, dy) || 1;
95
+ const gap = d - b.r;
96
+ if (gap > 0 && gap < reach) {
97
+ const f = (1 - gap / reach) ** 2;
98
+ ax += dx / d * 1600 * air * f + (this.pv.x - vx / dt) * 6 * air * f;
99
+ ay += dy / d * 1600 * air * f + (this.pv.y - vy / dt) * 6 * air * f;
100
+ }
101
+ }
102
+ b.px = b.x;
103
+ b.py = b.y;
104
+ b.x += vx + ax * dt * dt;
105
+ b.y += vy + ay * dt * dt;
106
+ }
107
+ for (let it = 0; it < this.iterations; it++) {
108
+ for (let i = 0; i < bs.length; i++) {
109
+ const a = bs[i];
110
+ if (a.ghost) continue;
111
+ for (let j = i + 1; j < bs.length; j++) {
112
+ const b = bs[j];
113
+ if (b.ghost) continue;
114
+ const dx = b.x - a.x;
115
+ const dy = b.y - a.y;
116
+ const min = a.r + b.r + this.gap;
117
+ const d2 = dx * dx + dy * dy;
118
+ if (d2 >= min * min || d2 < 1e-6) continue;
119
+ const d = Math.sqrt(d2);
120
+ const overlap = min - d;
121
+ const nx = dx / d;
122
+ const ny = dy / d;
123
+ const ma = a.r * a.r * (1 + a.hover * 4);
124
+ const mb = b.r * b.r * (1 + b.hover * 4);
125
+ let wa = a.fixed ? 0 : mb / (ma + mb);
126
+ let wb = b.fixed ? 0 : ma / (ma + mb);
127
+ if (a.fixed && b.fixed) continue;
128
+ if (a.fixed) wb = 1;
129
+ if (b.fixed) wa = 1;
130
+ a.x -= nx * overlap * wa;
131
+ a.y -= ny * overlap * wa;
132
+ b.x += nx * overlap * wb;
133
+ b.y += ny * overlap * wb;
134
+ }
135
+ }
136
+ }
137
+ const box = this.bounds;
138
+ if (box) {
139
+ for (const b of bs) {
140
+ if (b.fixed) continue;
141
+ b.x = Math.min(Math.max(b.x, box.x + b.r), box.x + box.w - b.r);
142
+ b.y = Math.min(Math.max(b.y, box.y + b.r), box.y + box.h - b.r);
143
+ }
144
+ }
145
+ }
146
+ };
147
+ function prng(seed = 2011) {
148
+ let s = seed >>> 0;
149
+ return () => {
150
+ s = s + 1831565813 >>> 0;
151
+ let t = s;
152
+ t = Math.imul(t ^ t >>> 15, t | 1);
153
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
154
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
155
+ };
156
+ }
157
+ var clamp = (v, a = 0, b = 1) => Math.min(Math.max(v, a), b);
158
+ var lerp = (a, b, t) => a + (b - a) * t;
159
+ var smooth = (e0, e1, x) => {
160
+ const t = clamp((x - e0) / (e1 - e0));
161
+ return t * t * (3 - 2 * t);
162
+ };
163
+ var easeInOut = (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
164
+
165
+ // src/color.ts
166
+ function parseColor(input) {
167
+ const s = input.trim().toLowerCase();
168
+ if (s.startsWith("#")) {
169
+ let h = s.slice(1);
170
+ if (h.length === 3 || h.length === 4) h = [...h].map((c) => c + c).join("");
171
+ if (h.length !== 6 && h.length !== 8) throw new Error(`bubble-maker: invalid colour "${input}"`);
172
+ const v = parseInt(h, 16);
173
+ if (Number.isNaN(v)) throw new Error(`bubble-maker: invalid colour "${input}"`);
174
+ return h.length === 6 ? [v >> 16 & 255, v >> 8 & 255, v & 255, 1] : [v >>> 24 & 255, v >> 16 & 255, v >> 8 & 255, (v & 255) / 255];
175
+ }
176
+ const m = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:[\s,/]+([\d.]+%?))?\s*\)$/.exec(s);
177
+ if (!m) throw new Error(`bubble-maker: invalid colour "${input}"`);
178
+ const a = m[4] === void 0 ? 1 : m[4].endsWith("%") ? parseFloat(m[4]) / 100 : parseFloat(m[4]);
179
+ return [Math.round(+m[1]), Math.round(+m[2]), Math.round(+m[3]), Math.min(1, Math.max(0, a))];
180
+ }
181
+ var rgbaString = (c, alpha = 1) => `rgba(${c[0]},${c[1]},${c[2]},${(c[3] * alpha).toFixed(3)})`;
182
+
183
+ // src/palettes.ts
184
+ var clone = (p) => ({ ...p, opacity: [...p.opacity], face: [...p.face] });
185
+ var defaultPalettes = Object.freeze({
186
+ // lustrous pearl: milky, slightly translucent, faint nacre at the edge
187
+ pearl: {
188
+ light: "#fffffd",
189
+ body: "#faf7f0",
190
+ edge: "#eee6d6",
191
+ rim: "#dcd0ba",
192
+ opacity: [0.96, 0.9, 0.92, 0.95],
193
+ clarity: 0.3,
194
+ face: ["#fdfbf6", "#e6ddcc"],
195
+ shine: 1,
196
+ nacre: { color: "255,226,214", alpha: 0.28 },
197
+ caustic: { color: "255,248,228", alpha: 0.55 },
198
+ rimShade: "rgba(255,248,230,0.72)",
199
+ rimWidth: 0.06
200
+ },
201
+ // champagne: soft satin, a touch of transparency, no metallic bands
202
+ gold: {
203
+ light: "#f2dfac",
204
+ body: "#dcbe7c",
205
+ edge: "#c4a15a",
206
+ rim: "#ac8b44",
207
+ opacity: [1, 1, 1, 1],
208
+ clarity: 0.3,
209
+ face: ["#e4cb92", "#b99650"],
210
+ shine: 0.62,
211
+ nacre: null,
212
+ glow: "240, 206, 128",
213
+ caustic: { color: "255,234,184", alpha: 0.38 },
214
+ rimShade: "rgba(255,236,186,0.55)",
215
+ rimWidth: 0.06
216
+ },
217
+ // crystal / soap: the room shows through, the edge catches the light
218
+ glass: {
219
+ light: "#ffffff",
220
+ body: "#fffdf5",
221
+ edge: "#fffaec",
222
+ rim: "#ffffff",
223
+ opacity: [0.04, 0.1, 0.38, 0.7],
224
+ clarity: 0,
225
+ face: ["rgba(255,255,255,0.35)", "rgba(234,216,168,0.25)"],
226
+ shine: 1,
227
+ nacre: { color: "255,214,170", alpha: 0.22 },
228
+ caustic: { color: "255,248,228", alpha: 0.55 },
229
+ rimShade: "rgba(255,248,230,0.72)",
230
+ rimWidth: 0.045
231
+ }
232
+ });
233
+ var current = Object.fromEntries(Object.entries(defaultPalettes).map(([k, v]) => [k, clone(v)]));
234
+ var version = 0;
235
+ var paletteVersion = () => version;
236
+ function registerPalettes(palettes) {
237
+ for (const [kind, p] of Object.entries(palettes)) {
238
+ const base = current[kind];
239
+ const merged = { ...base ?? {}, ...p };
240
+ for (const f of ["light", "body", "edge", "rim", "opacity", "clarity", "face"]) {
241
+ if (merged[f] === void 0) throw new Error(`bubble-maker: palette "${kind}" is missing "${f}"`);
242
+ }
243
+ current[kind] = merged;
244
+ }
245
+ version++;
246
+ }
247
+ function getPalette(kind) {
248
+ return current[kind] ?? current.pearl;
249
+ }
250
+ function listPalettes() {
251
+ return Object.keys(current);
252
+ }
253
+ function resetPalettes() {
254
+ current = Object.fromEntries(Object.entries(defaultPalettes).map(([k, v]) => [k, clone(v)]));
255
+ version++;
256
+ }
257
+
258
+ // src/draw.ts
259
+ var SIN60 = Math.sqrt(3) / 2;
260
+ var TAU = Math.PI * 2;
261
+ function cellPath(ctx, x, y, a, t) {
262
+ const c = a * (1 - t);
263
+ const R = (a - c) / SIN60;
264
+ ctx.beginPath();
265
+ if (R < 0.01) {
266
+ ctx.arc(x, y, a, 0, TAU);
267
+ return;
268
+ }
269
+ for (let i = 0; i < 6; i++) {
270
+ const th = Math.PI / 3 * i;
271
+ ctx.arc(x + R * Math.cos(th), y + R * Math.sin(th), c, th - Math.PI / 6, th + Math.PI / 6);
272
+ }
273
+ ctx.closePath();
274
+ }
275
+ var SEE = [0.35, 0.75, 0.45, 0.2];
276
+ var prepared = /* @__PURE__ */ new Map();
277
+ var seen = -1;
278
+ function prepare(pal) {
279
+ const cols = [pal.light, pal.body, pal.edge, pal.rim].map(parseColor);
280
+ const tint = pal.caustic?.color ?? "255,248,228";
281
+ return {
282
+ stops: [0, 0.62, 0.9, 1].map((st, i) => [st, cols[i], pal.opacity[i] * (1 - pal.clarity * SEE[i]), pal.opacity[i]]),
283
+ face: [pal.face[0], pal.face[1]],
284
+ nacre: pal.nacre === null ? null : pal.nacre ? [pal.nacre.color, pal.nacre.alpha] : ["255,226,214", 0.28],
285
+ causticTint: tint,
286
+ causticAlpha: pal.caustic?.alpha ?? 0.55,
287
+ glow: pal.glow ?? null,
288
+ shine: pal.shine ?? 1,
289
+ rimShade: pal.rimShade ?? "rgba(255,248,230,0.72)",
290
+ rimWidth: pal.rimWidth ?? 0.06
291
+ };
292
+ }
293
+ function palette(kind) {
294
+ if (seen !== paletteVersion()) {
295
+ prepared.clear();
296
+ seen = paletteVersion();
297
+ }
298
+ let p = prepared.get(kind);
299
+ if (!p) {
300
+ p = prepare(getPalette(kind));
301
+ prepared.set(kind, p);
302
+ }
303
+ return p;
304
+ }
305
+ function nearestLight(x, y, lights) {
306
+ let best = lights[0];
307
+ let bd = Infinity;
308
+ for (const l of lights) {
309
+ const d = (l.x - x) ** 2 + (l.y - y) ** 2;
310
+ if (d < bd) {
311
+ bd = d;
312
+ best = l;
313
+ }
314
+ }
315
+ return best;
316
+ }
317
+ var ROOM = ["rgba(250,244,232,0.5)", "rgba(226,212,186,0.62)"];
318
+ function paintBubble(ctx, b, o = {}) {
319
+ const a = b.r;
320
+ const t = b.shape;
321
+ if (a < 0.5 || b.alpha <= 0.01) return;
322
+ const P = palette(b.kind);
323
+ const lights = o.lights?.length ? o.lights : [{ x: b.x - 1e3, y: b.y - 1600 }];
324
+ const L = nearestLight(b.x, b.y, lights);
325
+ let lx = L.x - b.x;
326
+ let ly = L.y - b.y;
327
+ const ll = Math.hypot(lx, ly) || 1;
328
+ lx /= ll;
329
+ ly /= ll;
330
+ ctx.save();
331
+ ctx.globalAlpha = b.alpha;
332
+ ctx.translate(b.x, b.y);
333
+ const vx = b.x - b.px;
334
+ const vy = b.y - b.py;
335
+ const stretch = o.flex ? Math.min(Math.hypot(vx, vy) / (a * 1.5 + 4), 0.12) * (1 - t) : 0;
336
+ if (stretch > 0.01) {
337
+ const ang = Math.atan2(vy, vx);
338
+ ctx.rotate(ang);
339
+ ctx.scale(1 + stretch * 0.6, 1 - stretch * 0.4);
340
+ ctx.rotate(-ang);
341
+ }
342
+ let body;
343
+ if (t > 0.6) {
344
+ body = ctx.createLinearGradient(lx * a, ly * a, -lx * a, -ly * a);
345
+ body.addColorStop(0, P.face[0]);
346
+ body.addColorStop(1, P.face[1]);
347
+ } else {
348
+ body = ctx.createRadialGradient(lx * a * 0.35, ly * a * 0.35, a * 0.05, 0, 0, a);
349
+ const k = Math.min(1, t / 0.6);
350
+ const solid = k * k * (3 - 2 * k);
351
+ for (const [s, c, clear, firm] of P.stops) body.addColorStop(s, rgbaString(c, clear + (firm - clear) * solid));
352
+ if (o.dark) {
353
+ const [r0, r1] = o.room ?? ROOM;
354
+ const room = ctx.createRadialGradient(lx * a * 0.3, ly * a * 0.3, 0, 0, 0, a);
355
+ room.addColorStop(0, r0);
356
+ room.addColorStop(1, r1);
357
+ cellPath(ctx, 0, 0, a, t);
358
+ ctx.fillStyle = room;
359
+ ctx.fill();
360
+ }
361
+ }
362
+ if (o.wallShadow && t > 0.6) {
363
+ ctx.shadowColor = `rgba(96, 72, 30, ${0.32 * (t - 0.6) / 0.4})`;
364
+ ctx.shadowBlur = a * 0.7;
365
+ ctx.shadowOffsetX = -lx * a * 0.18;
366
+ ctx.shadowOffsetY = a * 0.22;
367
+ }
368
+ if (P.glow && o.glow) {
369
+ ctx.shadowColor = `rgba(${P.glow}, ${0.65 * o.glow})`;
370
+ ctx.shadowBlur = a * 1.8 * o.glow;
371
+ ctx.shadowOffsetX = 0;
372
+ ctx.shadowOffsetY = 0;
373
+ }
374
+ cellPath(ctx, 0, 0, a, t);
375
+ ctx.fillStyle = body;
376
+ ctx.fill();
377
+ ctx.shadowColor = "transparent";
378
+ ctx.shadowBlur = 0;
379
+ ctx.shadowOffsetX = 0;
380
+ ctx.shadowOffsetY = 0;
381
+ ctx.save();
382
+ cellPath(ctx, 0, 0, a, t);
383
+ ctx.clip();
384
+ if (P.nacre && t < 0.7) {
385
+ const n = ctx.createRadialGradient(-lx * a * 0.55, -ly * a * 0.55, 0, -lx * a * 0.55, -ly * a * 0.55, a * 0.9);
386
+ n.addColorStop(0, `rgba(${P.nacre[0]},${P.nacre[1]})`);
387
+ n.addColorStop(0.5, "rgba(214,236,240,0.14)");
388
+ n.addColorStop(1, "rgba(255,255,255,0)");
389
+ ctx.fillStyle = n;
390
+ ctx.fillRect(-a, -a, a * 2, a * 2);
391
+ }
392
+ const bubbleness = 1 - Math.min(1, t / 0.75);
393
+ for (const l of lights) {
394
+ let dx = l.x - b.x;
395
+ let dy = l.y - b.y;
396
+ const d = Math.hypot(dx, dy) || 1;
397
+ dx /= d;
398
+ dy /= d;
399
+ const near = l === L ? 1 : 0.55;
400
+ const px = dx * a * 0.52;
401
+ const py = dy * a * 0.52;
402
+ const w = a * (0.16 + 0.1 * near);
403
+ const h = a * (0.07 + 0.05 * near);
404
+ const g = ctx.createRadialGradient(px, py, 0, px, py, w);
405
+ const strength = Math.min(1, P.shine * near * (0.35 + 0.65 * bubbleness));
406
+ g.addColorStop(0, `rgba(255,255,255,${strength})`);
407
+ g.addColorStop(1, "rgba(255,255,255,0)");
408
+ ctx.fillStyle = g;
409
+ ctx.beginPath();
410
+ ctx.ellipse(px, py, w, h, Math.atan2(dy, dx) + Math.PI / 2, 0, TAU);
411
+ ctx.fill();
412
+ if (near === 1 && bubbleness > 0 && a > 6) {
413
+ const core = ctx.createRadialGradient(px, py, 0, px, py, a * 0.075);
414
+ core.addColorStop(0, `rgba(255,255,255,${(0.95 * bubbleness).toFixed(3)})`);
415
+ core.addColorStop(1, "rgba(255,255,255,0)");
416
+ ctx.fillStyle = core;
417
+ ctx.beginPath();
418
+ ctx.ellipse(px, py, a * 0.075, a * 0.05, Math.atan2(dy, dx) + Math.PI / 2, 0, TAU);
419
+ ctx.fill();
420
+ }
421
+ }
422
+ if (bubbleness > 0) {
423
+ const cx = -lx * a * 0.58;
424
+ const cy = -ly * a * 0.58;
425
+ const c = ctx.createRadialGradient(cx, cy, 0, cx, cy, a * 0.42);
426
+ c.addColorStop(0, `rgba(${P.causticTint},${(P.causticAlpha * bubbleness).toFixed(3)})`);
427
+ c.addColorStop(1, `rgba(${P.causticTint},0)`);
428
+ ctx.fillStyle = c;
429
+ ctx.fillRect(-a, -a, a * 2, a * 2);
430
+ }
431
+ if (t > 0.5) {
432
+ const s = ctx.createLinearGradient(lx * a, ly * a, -lx * a, -ly * a);
433
+ s.addColorStop(0, `rgba(255,255,255,${0.32 * (t - 0.5) * 2})`);
434
+ s.addColorStop(0.45, "rgba(255,255,255,0)");
435
+ ctx.fillStyle = s;
436
+ ctx.fillRect(-a, -a, a * 2, a * 2);
437
+ }
438
+ ctx.restore();
439
+ const rim = ctx.createLinearGradient(lx * a, ly * a, -lx * a, -ly * a);
440
+ rim.addColorStop(0, "rgba(255,255,255,1)");
441
+ rim.addColorStop(0.35, "rgba(255,255,255,0)");
442
+ rim.addColorStop(0.7, "rgba(255,255,255,0)");
443
+ rim.addColorStop(1, P.rimShade);
444
+ ctx.lineWidth = Math.max(0.8, a * P.rimWidth);
445
+ ctx.strokeStyle = rim;
446
+ cellPath(ctx, 0, 0, a - ctx.lineWidth / 2, t);
447
+ ctx.stroke();
448
+ ctx.restore();
449
+ }
450
+
451
+ // src/bubbles.ts
452
+ var DEFAULT_KINDS = { pearl: 0.62, gold: 0.24, glass: 0.14 };
453
+ function createBubbles(target, options = {}) {
454
+ const canvas = typeof target === "string" ? document.querySelector(target) : target;
455
+ const ctx = canvas?.getContext("2d");
456
+ if (!canvas || !ctx) throw new Error("bubble-maker: a <canvas> element is needed");
457
+ const o = {
458
+ count: "auto",
459
+ maxBubbles: 90,
460
+ size: [0.035, 0.11],
461
+ kinds: DEFAULT_KINDS,
462
+ reaction: 5,
463
+ hoverGrow: 1.6,
464
+ float: 0.015,
465
+ flex: true,
466
+ clickToCreate: true,
467
+ honeycomb: 0.045,
468
+ background: null,
469
+ maxDpr: 2,
470
+ seed: 7,
471
+ respectReducedMotion: true,
472
+ dark: false,
473
+ ...options
474
+ };
475
+ if (o.palettes) registerPalettes(o.palettes);
476
+ const rand = prng(o.seed);
477
+ const field = new BubbleField({ stiffness: 18, damping: 3.2, iterations: 4, gap: 2, ...o.physics, reaction: o.reaction });
478
+ const meta = /* @__PURE__ */ new Map();
479
+ const reducedMotion = o.respectReducedMotion && typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
480
+ let W = 0;
481
+ let H = 0;
482
+ let unit = 1;
483
+ let compressed = false;
484
+ let morph = 0;
485
+ let running = false;
486
+ let visible = true;
487
+ let last = 0;
488
+ let raf = 0;
489
+ let destroyed = false;
490
+ const pickKind = () => {
491
+ const entries = Object.entries(o.kinds);
492
+ let total = 0;
493
+ for (const [, w] of entries) total += w;
494
+ let r = rand() * total;
495
+ for (const [k, w] of entries) if ((r -= w) <= 0) return k;
496
+ return entries[0]?.[0] ?? "pearl";
497
+ };
498
+ const phone = () => W < 600;
499
+ const randomRadius = () => unit * (phone() ? o.size[0] * 1.5 + rand() * o.size[1] * 0.7 : o.size[0] + rand() * (o.size[1] - o.size[0]));
500
+ function add(init = {}) {
501
+ const x = init.x ?? lerp(W * 0.08, W * 0.92, rand());
502
+ const y = init.y ?? lerp(H * 0.12, H * 0.88, rand());
503
+ const r = init.radius ?? randomRadius();
504
+ const b = field.add({ x, y, base: r * 0.2, kind: init.kind ?? pickKind(), grow: o.hoverGrow });
505
+ meta.set(b, { x, y, r, phase: rand() * Math.PI * 2 });
506
+ if (compressed) lattice();
507
+ return b;
508
+ }
509
+ function resize() {
510
+ const r = canvas.getBoundingClientRect();
511
+ W = r.width;
512
+ H = r.height;
513
+ const dpr = Math.min(window.devicePixelRatio || 1, o.maxDpr);
514
+ canvas.width = Math.max(1, Math.round(W * dpr));
515
+ canvas.height = Math.max(1, Math.round(H * dpr));
516
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
517
+ unit = Math.min(W, H * 1.4);
518
+ field.bounds = { x: 0, y: 0, w: W, h: H };
519
+ for (const m of meta.values()) {
520
+ m.x = Math.min(m.x, W - 10);
521
+ m.y = Math.min(m.y, H - 10);
522
+ }
523
+ if (compressed) lattice();
524
+ }
525
+ function lattice() {
526
+ const list = [...meta.keys()];
527
+ const a = unit * (phone() ? o.honeycomb * 1.5 : o.honeycomb);
528
+ const R = (2 * a + 2) / Math.sqrt(3);
529
+ const slots = [];
530
+ const n2 = Math.ceil(Math.sqrt(list.length)) + 3;
531
+ for (let q = -n2; q <= n2; q++) {
532
+ for (let r = -n2; r <= n2; r++) {
533
+ const x = W / 2 + 1.5 * R * q;
534
+ const y = H / 2 + Math.sqrt(3) * R * (r + q / 2);
535
+ if (x < a || x > W - a || y < a || y > H - a) continue;
536
+ slots.push({ x, y, d: Math.hypot(x - W / 2, (y - H / 2) * 1.2) });
537
+ }
538
+ }
539
+ slots.sort((s1, s2) => s1.d - s2.d);
540
+ const pool = new Set(list);
541
+ for (const m of meta.values()) delete m.slot;
542
+ for (const s of slots) {
543
+ if (!pool.size) break;
544
+ let best = null;
545
+ let bd = Infinity;
546
+ for (const b of pool) {
547
+ const d = (b.x - s.x) ** 2 + (b.y - s.y) ** 2;
548
+ if (d < bd) {
549
+ bd = d;
550
+ best = b;
551
+ }
552
+ }
553
+ if (!best) break;
554
+ pool.delete(best);
555
+ meta.get(best).slot = { x: s.x, y: s.y, a };
556
+ }
557
+ }
558
+ const lightsNow = () => typeof o.lights === "function" ? o.lights(W, H) : o.lights ?? [{ x: W * 0.3, y: -H * 0.35 }, { x: W * 0.72, y: -H * 0.3 }];
559
+ function frame(now) {
560
+ if (!running || destroyed) return;
561
+ const dt = Math.min(Math.max((now - last) / 1e3, 0), 0.25);
562
+ last = now;
563
+ const t = now / 1e3;
564
+ morph = lerp(morph, compressed ? 1 : 0, 1 - Math.exp(-dt * 3));
565
+ field.hoverEnabled = !compressed;
566
+ for (const [b, m] of meta) {
567
+ if (compressed && m.slot) {
568
+ b.hx = m.slot.x;
569
+ b.hy = m.slot.y;
570
+ b.base = lerp(b.base, m.slot.a, 0.12);
571
+ } else {
572
+ b.hx = m.x + Math.sin(t * 0.7 + m.phase) * unit * o.float;
573
+ b.hy = m.y + Math.cos(t * 0.6 + m.phase) * unit * o.float;
574
+ b.base = lerp(b.base, m.r, 0.08);
575
+ }
576
+ b.shape = smooth(0.35, 1, morph);
577
+ b.grow = o.hoverGrow;
578
+ }
579
+ const steps = Math.min(8, Math.max(1, Math.ceil(dt * 60)));
580
+ for (let i = 0; i < steps; i++) field.step(dt / steps);
581
+ draw();
582
+ if (visible) raf = requestAnimationFrame(frame);
583
+ else running = false;
584
+ }
585
+ function draw() {
586
+ if (o.background) {
587
+ ctx.fillStyle = o.background;
588
+ ctx.fillRect(0, 0, W, H);
589
+ } else ctx.clearRect(0, 0, W, H);
590
+ const lights = lightsNow();
591
+ for (const b of meta.keys()) paintBubble(ctx, b, { lights, glow: morph * 0.5, wallShadow: morph > 0.4, flex: o.flex, dark: o.dark });
592
+ }
593
+ function start() {
594
+ if (running || destroyed) return;
595
+ running = true;
596
+ last = performance.now();
597
+ raf = requestAnimationFrame(frame);
598
+ }
599
+ function stop() {
600
+ running = false;
601
+ cancelAnimationFrame(raf);
602
+ }
603
+ const pointerAt = (e) => {
604
+ const r = canvas.getBoundingClientRect();
605
+ field.pointer.x = e.clientX - r.left;
606
+ field.pointer.y = e.clientY - r.top;
607
+ field.pointer.inside = true;
608
+ };
609
+ const onMove = (e) => pointerAt(e);
610
+ const onLeave = () => field.pointer.inside = false;
611
+ const onDown = (e) => e.pointerType !== "mouse" && pointerAt(e);
612
+ let releaseTimer = 0;
613
+ const onUp = (e) => {
614
+ if (e.pointerType === "mouse") return;
615
+ clearTimeout(releaseTimer);
616
+ releaseTimer = window.setTimeout(() => field.pointer.inside = false, 450);
617
+ };
618
+ const onClick = (e) => {
619
+ if (!o.clickToCreate) return;
620
+ const r = canvas.getBoundingClientRect();
621
+ const x = e.clientX - r.left;
622
+ const y = e.clientY - r.top;
623
+ if (field.hit(x, y)) return;
624
+ if (meta.size >= o.maxBubbles) return;
625
+ const b = add({ x, y, radius: unit * (phone() ? 0.05 + rand() * 0.06 : 0.03 + rand() * 0.05) });
626
+ o.onCreate?.(b);
627
+ };
628
+ canvas.addEventListener("pointermove", onMove);
629
+ canvas.addEventListener("pointerleave", onLeave);
630
+ canvas.addEventListener("pointerdown", onDown);
631
+ canvas.addEventListener("pointerup", onUp);
632
+ canvas.addEventListener("pointercancel", onUp);
633
+ canvas.addEventListener("click", onClick);
634
+ const ro = typeof ResizeObserver === "function" ? new ResizeObserver(() => resize()) : null;
635
+ ro?.observe(canvas);
636
+ const io = typeof IntersectionObserver === "function" ? new IntersectionObserver(([entry]) => {
637
+ visible = entry.isIntersecting;
638
+ if (visible) start();
639
+ }) : null;
640
+ io?.observe(canvas);
641
+ resize();
642
+ const n = o.count === "auto" ? Math.round(Math.min(48, Math.max(20, W * H / 9e3))) : o.count;
643
+ for (let i = 0; i < n; i++) add();
644
+ for (let i = 0; i < 200; i++) field.step(1 / 60);
645
+ if (reducedMotion) {
646
+ compressed = true;
647
+ morph = 1;
648
+ lattice();
649
+ for (const [b, m] of meta) {
650
+ b.x = b.px = m.slot?.x ?? b.x;
651
+ b.y = b.py = m.slot?.y ?? b.y;
652
+ b.base = b.r = m.slot?.a ?? b.base;
653
+ b.shape = 1;
654
+ }
655
+ draw();
656
+ } else start();
657
+ return {
658
+ field,
659
+ options: o,
660
+ get bubbles() {
661
+ return [...meta.keys()];
662
+ },
663
+ get compressed() {
664
+ return compressed;
665
+ },
666
+ add,
667
+ remove(b) {
668
+ meta.delete(b);
669
+ const i = field.bubbles.indexOf(b);
670
+ if (i >= 0) field.bubbles.splice(i, 1);
671
+ if (compressed) lattice();
672
+ },
673
+ clear() {
674
+ meta.clear();
675
+ field.bubbles.length = 0;
676
+ },
677
+ compress() {
678
+ compressed = true;
679
+ lattice();
680
+ },
681
+ release() {
682
+ compressed = false;
683
+ },
684
+ toggle() {
685
+ compressed = !compressed;
686
+ if (compressed) lattice();
687
+ return compressed;
688
+ },
689
+ shake(strength = 2400) {
690
+ compressed = false;
691
+ for (const b of meta.keys()) field.kick(b, (rand() - 0.5) * strength, (rand() - 0.5) * strength);
692
+ },
693
+ setReaction(level) {
694
+ field.reaction = level;
695
+ },
696
+ setPalette(kind, palette2) {
697
+ registerPalettes({ [kind]: palette2 });
698
+ },
699
+ resize,
700
+ start,
701
+ stop,
702
+ destroy() {
703
+ destroyed = true;
704
+ stop();
705
+ clearTimeout(releaseTimer);
706
+ ro?.disconnect();
707
+ io?.disconnect();
708
+ canvas.removeEventListener("pointermove", onMove);
709
+ canvas.removeEventListener("pointerleave", onLeave);
710
+ canvas.removeEventListener("pointerdown", onDown);
711
+ canvas.removeEventListener("pointerup", onUp);
712
+ canvas.removeEventListener("pointercancel", onUp);
713
+ canvas.removeEventListener("click", onClick);
714
+ meta.clear();
715
+ field.bubbles.length = 0;
716
+ }
717
+ };
718
+ }
719
+ export {
720
+ BubbleField,
721
+ cellPath,
722
+ clamp,
723
+ createBubbles,
724
+ defaultPalettes,
725
+ easeInOut,
726
+ getPalette,
727
+ lerp,
728
+ listPalettes,
729
+ paintBubble,
730
+ parseColor,
731
+ prng,
732
+ registerPalettes,
733
+ resetPalettes,
734
+ smooth
735
+ };
736
+ //# sourceMappingURL=index.js.map