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/LICENSE +21 -0
- package/README.es.md +47 -0
- package/README.md +149 -2
- package/dist/bubble-maker.global.js +1 -0
- package/dist/bubbles.d.ts +82 -0
- package/dist/color.d.ts +4 -0
- package/dist/draw.d.ts +28 -0
- package/dist/engine.d.ts +80 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +736 -0
- package/dist/index.js.map +7 -0
- package/dist/palettes.d.ts +51 -0
- package/package.json +52 -4
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
|