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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2013-2026 José Manuel Ligero Márquez (neoligero)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.es.md ADDED
@@ -0,0 +1,47 @@
1
+ # Bubble Maker 2.0
2
+
3
+ Burbujas de jabón para la web: crecen al tocarlas, se empujan unas a otras, se mueven con el cursor y se comprimen en un panal de hexágonos. Sin dependencias, unos 14 KB minificado y totalmente configurable.
4
+
5
+ **[Demo en vivo](https://neoligero.github.io/bubble-maker-2.0/)** · [English](README.md) · [Bubble Maker 1.0 (2013)](https://github.com/neoligero/Bubble_maker)
6
+
7
+ Bubble Maker nació en 2013 como la navegación de una web personal: cada sección era una burbuja que crecía al pasar el ratón y empujaba a sus vecinas con temporizadores encadenados. La 2.0 es una reescritura desde cero: un pequeño motor de física más un pintor. Es lo que da vida a [neoligero.com](https://www.neoligero.com).
8
+
9
+ ## Instalación
10
+
11
+ ```bash
12
+ npm install bubble-maker
13
+ ```
14
+
15
+ ## Uso rápido
16
+
17
+ ```html
18
+ <canvas id="burbujas" style="width: 100%; height: 480px"></canvas>
19
+ ```
20
+
21
+ ```js
22
+ import { createBubbles } from 'bubble-maker';
23
+
24
+ const burbujas = createBubbles('#burbujas', {
25
+ reaction: 7, // 0 quietas … 3 solo crece al pasar … 10 muy vivas
26
+ palettes: { pearl: { body: '#fdf3f6' } }, // colores
27
+ });
28
+
29
+ burbujas.compress(); // todas al panal
30
+ burbujas.release(); // y vuelven a ser burbujas
31
+ ```
32
+
33
+ ## Qué se puede configurar
34
+
35
+ Todo es opcional. Las más útiles:
36
+
37
+ - **`reaction`** (0–10): cuánto reaccionan al cursor. `0` nada, `3` solo crece la que tocas, `10` el aire alrededor del cursor las empuja y arrastra.
38
+ - **`count`, `maxBubbles`, `size`, `kinds`**: cuántas, el límite, su tamaño y la mezcla de tipos (perla, oro, cristal o los tuyos).
39
+ - **`palettes`**: colores de cada tipo (centro iluminado, cuerpo, borde, aro), transparencia, brillo, nácar, aura. Acepta `#hex`, `rgb()` y `rgba()`. Puedes cambiar un solo campo de una paleta existente o crear tipos nuevos.
40
+ - **`physics`**: `stiffness` (cuánto vuelven a su sitio), `damping` (cuánto se calman), `iterations` y `gap` (separación entre burbujas).
41
+ - **`hoverGrow`, `float`, `lights`, `dark`, `flex`, `honeycomb`, `background`, `seed`…**
42
+
43
+ La tabla completa de opciones, la API (`add`, `remove`, `compress`, `release`, `shake`, `setReaction`, `setPalette`, `destroy`…) y el uso a bajo nivel (`BubbleField` y `paintBubble`, por ejemplo para convertir un menú HTML en burbujas) están en el [README en inglés](README.md).
44
+
45
+ ## Licencia
46
+
47
+ [MIT](LICENSE) © 2013–2026 José Manuel Ligero Márquez
package/README.md CHANGED
@@ -1,3 +1,150 @@
1
- # Temporary Holding Version
1
+ # Bubble Maker 2.0
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Soap bubbles for the web: bubbles that grow when you touch them, push each other out of the way, drift with the cursor and pack into a honeycomb of hexagons. No dependencies, about 14 KB minified, fully configurable.
4
+
5
+ **[Live demo](https://neoligero.github.io/bubble-maker-2.0/)** · [Español](README.es.md) · [Bubble Maker 1.0 (2013)](https://github.com/neoligero/Bubble_maker)
6
+
7
+ Bubble Maker started in 2013 as the navigation of a personal website: every section was a bubble that grew on hover and pushed its neighbours with chained timeouts. Version 2.0 is a rewrite from scratch as a small physics engine plus a painter, and it powers [neoligero.com](https://www.neoligero.com).
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install bubble-maker
13
+ ```
14
+
15
+ or, without a bundler:
16
+
17
+ ```html
18
+ <script src="https://cdn.jsdelivr.net/npm/bubble-maker/dist/bubble-maker.global.js"></script>
19
+ <script>
20
+ BubbleMaker.createBubbles(document.querySelector('canvas'));
21
+ </script>
22
+ ```
23
+
24
+ ## Quick start
25
+
26
+ ```html
27
+ <canvas id="bubbles" style="width: 100%; height: 480px"></canvas>
28
+ ```
29
+
30
+ ```js
31
+ import { createBubbles } from 'bubble-maker';
32
+
33
+ const bubbles = createBubbles('#bubbles', {
34
+ reaction: 7, // 0 still … 3 grow on hover … 10 very lively
35
+ palettes: { pearl: { body: '#fdf3f6' } },
36
+ });
37
+
38
+ bubbles.compress(); // pack everything into a honeycomb
39
+ bubbles.release(); // …and let the bubbles go again
40
+ ```
41
+
42
+ `createBubbles` gives you a canvas that floats, reacts to mouse and touch, creates a bubble on click, pauses when it is off screen, respects `prefers-reduced-motion` and handles high-DPI screens.
43
+
44
+ ## Options
45
+
46
+ Everything is optional.
47
+
48
+ | Option | Default | What it does |
49
+ |---|---|---|
50
+ | `count` | `'auto'` | Bubbles at the start, or `'auto'` to fit the canvas (20–48). |
51
+ | `maxBubbles` | `90` | Limit when visitors add more. |
52
+ | `size` | `[0.035, 0.11]` | Radius range, as a fraction of the canvas size (phones get larger bubbles). |
53
+ | `kinds` | pearl 0.62, gold 0.24, glass 0.14 | Relative weight of each palette in new bubbles. |
54
+ | `palettes` | built-ins | Colours, see below. Merged field by field. |
55
+ | `reaction` | `5` | How much bubbles react to the cursor: `0` nothing, `3` only the bubble under the cursor grows, `10` the air around the cursor pushes bubbles away and drags them along. |
56
+ | `hoverGrow` | `1.6` | Radius multiplier of the bubble under the cursor. |
57
+ | `float` | `0.015` | Idle drift, as a fraction of the canvas size. `0` keeps them still. |
58
+ | `physics` | see below | `stiffness`, `damping`, `iterations`, `gap`. |
59
+ | `lights` | two above the canvas | `[{ x, y }]` in canvas pixels, or `(width, height) => lights`. Bubbles are lit by the nearest one. |
60
+ | `dark` | `false` | For dark backgrounds: a faint room shows through each bubble. |
61
+ | `flex` | `true` | Bubbles stretch along their motion. |
62
+ | `clickToCreate` | `true` | Clicking an empty spot creates a bubble. |
63
+ | `honeycomb` | `0.045` | Cell size of the honeycomb, as a fraction of the canvas size. |
64
+ | `background` | `null` | Fill colour for each frame; `null` keeps the canvas transparent. |
65
+ | `maxDpr` | `2` | Cap on the device pixel ratio. |
66
+ | `seed` | `7` | Seed of the pseudo-random layout: the same one on every load. |
67
+ | `respectReducedMotion` | `true` | Start as a still honeycomb for visitors who prefer reduced motion. |
68
+ | `onCreate` | – | Called with each bubble created by a click. |
69
+
70
+ Physics defaults for `createBubbles`: `{ stiffness: 18, damping: 3.2, iterations: 4, gap: 2 }`. Higher `stiffness` pulls bubbles back home faster, higher `damping` calms them, `iterations` is the number of collision passes per step, `gap` the breathing room in pixels.
71
+
72
+ ### The object you get back
73
+
74
+ ```ts
75
+ bubbles.add({ x, y, radius, kind }) // create a bubble
76
+ bubbles.remove(bubble) bubbles.clear()
77
+ bubbles.compress() bubbles.release() bubbles.toggle()
78
+ bubbles.shake(strength = 2400)
79
+ bubbles.setReaction(8)
80
+ bubbles.setPalette('gold', { body: '#caa' }) // live
81
+ bubbles.options.hoverGrow = 2 // live: hoverGrow, float, flex, dark, background, clickToCreate
82
+ bubbles.field // the physics field
83
+ bubbles.bubbles bubbles.compressed
84
+ bubbles.resize() bubbles.start() bubbles.stop() bubbles.destroy()
85
+ ```
86
+
87
+ ## Colours
88
+
89
+ A palette describes a kind of bubble. Colours go from the lit centre to the rim and accept `#rgb`, `#rrggbb`, `#rrggbbaa`, `rgb()` and `rgba()`.
90
+
91
+ ```js
92
+ import { registerPalettes } from 'bubble-maker';
93
+
94
+ registerPalettes({
95
+ // change one field of a built-in…
96
+ pearl: { body: '#fdf3f6' },
97
+ // …or add a new kind (it must be complete)
98
+ rose: {
99
+ light: '#fffafc', body: '#fbe0ea', edge: '#f4bdd0', rim: '#e59ab4',
100
+ opacity: [0.95, 0.9, 0.92, 0.95], // centre, body, edge, rim once settled
101
+ clarity: 0.4, // 0 solid … 1 very see-through while floating
102
+ face: ['#fdeef3', '#ebc3d2'], // lit and shaded side of the hexagon tile
103
+ shine: 1, // optional: highlights, 0..1
104
+ nacre: { color: '255,226,214', alpha: 0.28 }, // optional: iridescence, or null
105
+ glow: '244, 189, 208', // optional: aura colour "r, g, b"
106
+ caustic: { color: '255,248,228', alpha: 0.55 }, // optional: light focused through the bubble
107
+ rimShade: 'rgba(255,248,230,0.72)', // optional
108
+ rimWidth: 0.06, // optional: fraction of the radius
109
+ },
110
+ });
111
+
112
+ createBubbles(canvas, { kinds: { rose: 3, pearl: 1 } });
113
+ ```
114
+
115
+ Built-in palettes: `pearl`, `gold` and `glass`. `getPalette(kind)`, `listPalettes()` and `resetPalettes()` are also exported.
116
+
117
+ ## Lower level
118
+
119
+ `createBubbles` is built from two parts you can use on their own, for example to turn a menu made of DOM elements into bubbles:
120
+
121
+ ```js
122
+ import { BubbleField, paintBubble } from 'bubble-maker';
123
+
124
+ const field = new BubbleField({ stiffness: 26, damping: 4, iterations: 4, gap: 6, reaction: 5 });
125
+ const a = field.add({ x: 200, y: 120, base: 50, kind: 'gold', grow: 1.35 });
126
+ field.pointer.x = mouseX; field.pointer.y = mouseY; field.pointer.inside = true;
127
+
128
+ function frame(dt) {
129
+ field.step(dt); // seconds
130
+ for (const b of field.bubbles) paintBubble(ctx, b, { lights: [{ x: 80, y: -300 }] });
131
+ }
132
+ ```
133
+
134
+ A bubble has `x, y` (position), `r` (current radius), `base` (resting radius), `grow`, `hx, hy` (the point it springs back to: move it to move the bubble), `shape` (`0` circle … `1` hexagon), `alpha`, `ghost` (ignores collisions), `fixed` and `kind`. `field.hit(x, y)` finds the bubble under a point and `field.kick(bubble, vx, vy)` pushes it. Also exported: `prng`, `clamp`, `lerp`, `smooth`, `easeInOut`, `cellPath`, `parseColor`.
135
+
136
+ ## Browsers
137
+
138
+ Any browser with Canvas 2D and `PointerEvent`. ES2020 output, TypeScript types included.
139
+
140
+ ## Development
141
+
142
+ ```bash
143
+ npm install
144
+ npm test # builds and runs the tests (node:test)
145
+ npm run demo # builds the demo into docs/ (served by GitHub Pages)
146
+ ```
147
+
148
+ ## License
149
+
150
+ [MIT](LICENSE) © 2013–2026 José Manuel Ligero Márquez
@@ -0,0 +1 @@
1
+ "use strict";var BubbleMaker=(()=>{var ee=Object.defineProperty;var Me=Object.getOwnPropertyDescriptor;var we=Object.getOwnPropertyNames;var Pe=Object.prototype.hasOwnProperty;var Be=(e,o)=>{for(var n in o)ee(e,n,{get:o[n],enumerable:!0})},ke=(e,o,n,t)=>{if(o&&typeof o=="object"||typeof o=="function")for(let r of we(o))!Pe.call(e,r)&&r!==n&&ee(e,r,{get:()=>o[r],enumerable:!(t=Me(o,r))||t.enumerable});return e};var Ce=e=>ke(ee({},"__esModule",{value:!0}),e);var Ie={};Be(Ie,{BubbleField:()=>D,cellPath:()=>X,clamp:()=>te,createBubbles:()=>xe,defaultPalettes:()=>_,easeInOut:()=>he,getPalette:()=>J,lerp:()=>W,listPalettes:()=>pe,paintBubble:()=>Q,parseColor:()=>V,prng:()=>U,registerPalettes:()=>Y,resetPalettes:()=>ge,smooth:()=>K});var D=class{constructor(o={}){this.bubbles=[];this.pointer={x:-1e5,y:-1e5,inside:!1};this.focused=null;this.hoverEnabled=!0;this.bounds=null;this.pv={x:0,y:0};this.last={x:-1e5,y:-1e5,t:0};this.stiffness=o.stiffness??40,this.damping=o.damping??6,this.iterations=o.iterations??3,this.gap=o.gap??2,this.reaction=o.reaction??3}add(o){let n={px:o.x,py:o.y,r:o.base,grow:1.55,hover:0,hx:o.x,hy:o.y,shape:0,alpha:1,ghost:!1,fixed:!1,kind:"pearl",...o};return this.bubbles.push(n),n}kick(o,n,t,r=1/60){o.px-=n*r,o.py-=t*r}hit(o,n){for(let t=this.bubbles.length-1;t>=0;t--){let r=this.bubbles[t],i=o-r.x,b=n-r.y;if(i*i+b*b<r.r*r.r&&r.alpha>.2)return r}return null}step(o){let n=Math.min(o,.03333333333333333),t=this.bubbles,r=a=>1-Math.exp(-n*a),i=this.hoverEnabled&&this.pointer.inside?this.hit(this.pointer.x,this.pointer.y):null,b=Math.exp(-this.damping*n),f=Math.min(Math.max(this.reaction,0),10),g=Math.min(f/3,1)*(1+Math.max(f-3,0)/7*.6),c=Math.max(f-2,0)/8,d=this.pointer;if(this.last.t+=n,d.x!==this.last.x||d.y!==this.last.y){if(this.last.x>-1e4&&d.inside){let a=Math.max(this.last.t,.004166666666666667);this.pv.x+=((d.x-this.last.x)/a-this.pv.x)*.6,this.pv.y+=((d.y-this.last.y)/a-this.pv.y)*.6}this.last.x=d.x,this.last.y=d.y,this.last.t=0}else this.last.t>.06&&(this.pv.x*=1-r(10),this.pv.y*=1-r(10));let L=c>0&&this.hoverEnabled&&d.inside,x=60+160*c;for(let a of t){let S=a===i||a===this.focused?1:0;a.hover+=(S-a.hover)*r(9);let p=a.base*(1+(a.grow-1)*g*a.hover);if(a.r+=(p-a.r)*r(11),a.fixed){a.px=a.x,a.py=a.y;continue}let u=(a.x-a.px)*b,h=(a.y-a.py)*b,y=this.stiffness*(a.hx-a.x),C=this.stiffness*(a.hy-a.y);if(L&&a.alpha>.2&&a!==i){let v=a.x-d.x,M=a.y-d.y,P=Math.hypot(v,M)||1,B=P-a.r;if(B>0&&B<x){let $=(1-B/x)**2;y+=v/P*1600*c*$+(this.pv.x-u/n)*6*c*$,C+=M/P*1600*c*$+(this.pv.y-h/n)*6*c*$}}a.px=a.x,a.py=a.y,a.x+=u+y*n*n,a.y+=h+C*n*n}for(let a=0;a<this.iterations;a++)for(let S=0;S<t.length;S++){let p=t[S];if(!p.ghost)for(let u=S+1;u<t.length;u++){let h=t[u];if(h.ghost)continue;let y=h.x-p.x,C=h.y-p.y,v=p.r+h.r+this.gap,M=y*y+C*C;if(M>=v*v||M<1e-6)continue;let P=Math.sqrt(M),B=v-P,$=y/P,F=C/P,G=p.r*p.r*(1+p.hover*4),T=h.r*h.r*(1+h.hover*4),A=p.fixed?0:T/(G+T),q=h.fixed?0:G/(G+T);p.fixed&&h.fixed||(p.fixed&&(q=1),h.fixed&&(A=1),p.x-=$*B*A,p.y-=F*B*A,h.x+=$*B*q,h.y+=F*B*q)}}let w=this.bounds;if(w)for(let a of t)a.fixed||(a.x=Math.min(Math.max(a.x,w.x+a.r),w.x+w.w-a.r),a.y=Math.min(Math.max(a.y,w.y+a.r),w.y+w.h-a.r))}};function U(e=2011){let o=e>>>0;return()=>{o=o+1831565813>>>0;let n=o;return n=Math.imul(n^n>>>15,n|1),n^=n+Math.imul(n^n>>>7,n|61),((n^n>>>14)>>>0)/4294967296}}var te=(e,o=0,n=1)=>Math.min(Math.max(e,o),n),W=(e,o,n)=>e+(o-e)*n,K=(e,o,n)=>{let t=te((n-e)/(o-e));return t*t*(3-2*t)},he=e=>e<.5?4*e*e*e:1-Math.pow(-2*e+2,3)/2;function V(e){let o=e.trim().toLowerCase();if(o.startsWith("#")){let r=o.slice(1);if((r.length===3||r.length===4)&&(r=[...r].map(b=>b+b).join("")),r.length!==6&&r.length!==8)throw new Error(`bubble-maker: invalid colour "${e}"`);let i=parseInt(r,16);if(Number.isNaN(i))throw new Error(`bubble-maker: invalid colour "${e}"`);return r.length===6?[i>>16&255,i>>8&255,i&255,1]:[i>>>24&255,i>>16&255,i>>8&255,(i&255)/255]}let n=/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:[\s,/]+([\d.]+%?))?\s*\)$/.exec(o);if(!n)throw new Error(`bubble-maker: invalid colour "${e}"`);let t=n[4]===void 0?1:n[4].endsWith("%")?parseFloat(n[4])/100:parseFloat(n[4]);return[Math.round(+n[1]),Math.round(+n[2]),Math.round(+n[3]),Math.min(1,Math.max(0,t))]}var fe=(e,o=1)=>`rgba(${e[0]},${e[1]},${e[2]},${(e[3]*o).toFixed(3)})`;var me=e=>({...e,opacity:[...e.opacity],face:[...e.face]}),_=Object.freeze({pearl:{light:"#fffffd",body:"#faf7f0",edge:"#eee6d6",rim:"#dcd0ba",opacity:[.96,.9,.92,.95],clarity:.3,face:["#fdfbf6","#e6ddcc"],shine:1,nacre:{color:"255,226,214",alpha:.28},caustic:{color:"255,248,228",alpha:.55},rimShade:"rgba(255,248,230,0.72)",rimWidth:.06},gold:{light:"#f2dfac",body:"#dcbe7c",edge:"#c4a15a",rim:"#ac8b44",opacity:[1,1,1,1],clarity:.3,face:["#e4cb92","#b99650"],shine:.62,nacre:null,glow:"240, 206, 128",caustic:{color:"255,234,184",alpha:.38},rimShade:"rgba(255,236,186,0.55)",rimWidth:.06},glass:{light:"#ffffff",body:"#fffdf5",edge:"#fffaec",rim:"#ffffff",opacity:[.04,.1,.38,.7],clarity:0,face:["rgba(255,255,255,0.35)","rgba(234,216,168,0.25)"],shine:1,nacre:{color:"255,214,170",alpha:.22},caustic:{color:"255,248,228",alpha:.55},rimShade:"rgba(255,248,230,0.72)",rimWidth:.045}}),N=Object.fromEntries(Object.entries(_).map(([e,o])=>[e,me(o)])),ne=0,re=()=>ne;function Y(e){for(let[o,n]of Object.entries(e)){let r={...N[o]??{},...n};for(let i of["light","body","edge","rim","opacity","clarity","face"])if(r[i]===void 0)throw new Error(`bubble-maker: palette "${o}" is missing "${i}"`);N[o]=r}ne++}function J(e){return N[e]??N.pearl}function pe(){return Object.keys(N)}function ge(){N=Object.fromEntries(Object.entries(_).map(([e,o])=>[e,me(o)])),ne++}var Se=Math.sqrt(3)/2,ae=Math.PI*2;function X(e,o,n,t,r){let i=t*(1-r),b=(t-i)/Se;if(e.beginPath(),b<.01){e.arc(o,n,t,0,ae);return}for(let f=0;f<6;f++){let g=Math.PI/3*f;e.arc(o+b*Math.cos(g),n+b*Math.sin(g),i,g-Math.PI/6,g+Math.PI/6)}e.closePath()}var Re=[.35,.75,.45,.2],oe=new Map,ye=-1;function Ee(e){let o=[e.light,e.body,e.edge,e.rim].map(V),n=e.caustic?.color??"255,248,228";return{stops:[0,.62,.9,1].map((t,r)=>[t,o[r],e.opacity[r]*(1-e.clarity*Re[r]),e.opacity[r]]),face:[e.face[0],e.face[1]],nacre:e.nacre===null?null:e.nacre?[e.nacre.color,e.nacre.alpha]:["255,226,214",.28],causticTint:n,causticAlpha:e.caustic?.alpha??.55,glow:e.glow??null,shine:e.shine??1,rimShade:e.rimShade??"rgba(255,248,230,0.72)",rimWidth:e.rimWidth??.06}}function Oe(e){ye!==re()&&(oe.clear(),ye=re());let o=oe.get(e);return o||(o=Ee(J(e)),oe.set(e,o)),o}function Le(e,o,n){let t=n[0],r=1/0;for(let i of n){let b=(i.x-e)**2+(i.y-o)**2;b<r&&(r=b,t=i)}return t}var $e=["rgba(250,244,232,0.5)","rgba(226,212,186,0.62)"];function Q(e,o,n={}){let t=o.r,r=o.shape;if(t<.5||o.alpha<=.01)return;let i=Oe(o.kind),b=n.lights?.length?n.lights:[{x:o.x-1e3,y:o.y-1600}],f=Le(o.x,o.y,b),g=f.x-o.x,c=f.y-o.y,d=Math.hypot(g,c)||1;g/=d,c/=d,e.save(),e.globalAlpha=o.alpha,e.translate(o.x,o.y);let L=o.x-o.px,x=o.y-o.py,w=n.flex?Math.min(Math.hypot(L,x)/(t*1.5+4),.12)*(1-r):0;if(w>.01){let u=Math.atan2(x,L);e.rotate(u),e.scale(1+w*.6,1-w*.4),e.rotate(-u)}let a;if(r>.6)a=e.createLinearGradient(g*t,c*t,-g*t,-c*t),a.addColorStop(0,i.face[0]),a.addColorStop(1,i.face[1]);else{a=e.createRadialGradient(g*t*.35,c*t*.35,t*.05,0,0,t);let u=Math.min(1,r/.6),h=u*u*(3-2*u);for(let[y,C,v,M]of i.stops)a.addColorStop(y,fe(C,v+(M-v)*h));if(n.dark){let[y,C]=n.room??$e,v=e.createRadialGradient(g*t*.3,c*t*.3,0,0,0,t);v.addColorStop(0,y),v.addColorStop(1,C),X(e,0,0,t,r),e.fillStyle=v,e.fill()}}if(n.wallShadow&&r>.6&&(e.shadowColor=`rgba(96, 72, 30, ${.32*(r-.6)/.4})`,e.shadowBlur=t*.7,e.shadowOffsetX=-g*t*.18,e.shadowOffsetY=t*.22),i.glow&&n.glow&&(e.shadowColor=`rgba(${i.glow}, ${.65*n.glow})`,e.shadowBlur=t*1.8*n.glow,e.shadowOffsetX=0,e.shadowOffsetY=0),X(e,0,0,t,r),e.fillStyle=a,e.fill(),e.shadowColor="transparent",e.shadowBlur=0,e.shadowOffsetX=0,e.shadowOffsetY=0,e.save(),X(e,0,0,t,r),e.clip(),i.nacre&&r<.7){let u=e.createRadialGradient(-g*t*.55,-c*t*.55,0,-g*t*.55,-c*t*.55,t*.9);u.addColorStop(0,`rgba(${i.nacre[0]},${i.nacre[1]})`),u.addColorStop(.5,"rgba(214,236,240,0.14)"),u.addColorStop(1,"rgba(255,255,255,0)"),e.fillStyle=u,e.fillRect(-t,-t,t*2,t*2)}let S=1-Math.min(1,r/.75);for(let u of b){let h=u.x-o.x,y=u.y-o.y,C=Math.hypot(h,y)||1;h/=C,y/=C;let v=u===f?1:.55,M=h*t*.52,P=y*t*.52,B=t*(.16+.1*v),$=t*(.07+.05*v),F=e.createRadialGradient(M,P,0,M,P,B),G=Math.min(1,i.shine*v*(.35+.65*S));if(F.addColorStop(0,`rgba(255,255,255,${G})`),F.addColorStop(1,"rgba(255,255,255,0)"),e.fillStyle=F,e.beginPath(),e.ellipse(M,P,B,$,Math.atan2(y,h)+Math.PI/2,0,ae),e.fill(),v===1&&S>0&&t>6){let T=e.createRadialGradient(M,P,0,M,P,t*.075);T.addColorStop(0,`rgba(255,255,255,${(.95*S).toFixed(3)})`),T.addColorStop(1,"rgba(255,255,255,0)"),e.fillStyle=T,e.beginPath(),e.ellipse(M,P,t*.075,t*.05,Math.atan2(y,h)+Math.PI/2,0,ae),e.fill()}}if(S>0){let u=-g*t*.58,h=-c*t*.58,y=e.createRadialGradient(u,h,0,u,h,t*.42);y.addColorStop(0,`rgba(${i.causticTint},${(i.causticAlpha*S).toFixed(3)})`),y.addColorStop(1,`rgba(${i.causticTint},0)`),e.fillStyle=y,e.fillRect(-t,-t,t*2,t*2)}if(r>.5){let u=e.createLinearGradient(g*t,c*t,-g*t,-c*t);u.addColorStop(0,`rgba(255,255,255,${.32*(r-.5)*2})`),u.addColorStop(.45,"rgba(255,255,255,0)"),e.fillStyle=u,e.fillRect(-t,-t,t*2,t*2)}e.restore();let p=e.createLinearGradient(g*t,c*t,-g*t,-c*t);p.addColorStop(0,"rgba(255,255,255,1)"),p.addColorStop(.35,"rgba(255,255,255,0)"),p.addColorStop(.7,"rgba(255,255,255,0)"),p.addColorStop(1,i.rimShade),e.lineWidth=Math.max(.8,t*i.rimWidth),e.strokeStyle=p,X(e,0,0,t-e.lineWidth/2,r),e.stroke(),e.restore()}var Te={pearl:.62,gold:.24,glass:.14};function xe(e,o={}){let n=typeof e=="string"?document.querySelector(e):e,t=n?.getContext("2d");if(!n||!t)throw new Error("bubble-maker: a <canvas> element is needed");let r={count:"auto",maxBubbles:90,size:[.035,.11],kinds:Te,reaction:5,hoverGrow:1.6,float:.015,flex:!0,clickToCreate:!0,honeycomb:.045,background:null,maxDpr:2,seed:7,respectReducedMotion:!0,dark:!1,...o};r.palettes&&Y(r.palettes);let i=U(r.seed),b=new D({stiffness:18,damping:3.2,iterations:4,gap:2,...r.physics,reaction:r.reaction}),f=new Map,g=r.respectReducedMotion&&typeof matchMedia=="function"&&matchMedia("(prefers-reduced-motion: reduce)").matches,c=0,d=0,L=1,x=!1,w=0,a=!1,S=!0,p=0,u=0,h=!1,y=()=>{let s=Object.entries(r.kinds),l=0;for(let[,R]of s)l+=R;let k=i()*l;for(let[R,m]of s)if((k-=m)<=0)return R;return s[0]?.[0]??"pearl"},C=()=>c<600,v=()=>L*(C()?r.size[0]*1.5+i()*r.size[1]*.7:r.size[0]+i()*(r.size[1]-r.size[0]));function M(s={}){let l=s.x??W(c*.08,c*.92,i()),k=s.y??W(d*.12,d*.88,i()),R=s.radius??v(),m=b.add({x:l,y:k,base:R*.2,kind:s.kind??y(),grow:r.hoverGrow});return f.set(m,{x:l,y:k,r:R,phase:i()*Math.PI*2}),x&&B(),m}function P(){let s=n.getBoundingClientRect();c=s.width,d=s.height;let l=Math.min(window.devicePixelRatio||1,r.maxDpr);n.width=Math.max(1,Math.round(c*l)),n.height=Math.max(1,Math.round(d*l)),t.setTransform(l,0,0,l,0,0),L=Math.min(c,d*1.4),b.bounds={x:0,y:0,w:c,h:d};for(let k of f.values())k.x=Math.min(k.x,c-10),k.y=Math.min(k.y,d-10);x&&B()}function B(){let s=[...f.keys()],l=L*(C()?r.honeycomb*1.5:r.honeycomb),k=(2*l+2)/Math.sqrt(3),R=[],m=Math.ceil(Math.sqrt(s.length))+3;for(let O=-m;O<=m;O++)for(let I=-m;I<=m;I++){let j=c/2+1.5*k*O,z=d/2+Math.sqrt(3)*k*(I+O/2);j<l||j>c-l||z<l||z>d-l||R.push({x:j,y:z,d:Math.hypot(j-c/2,(z-d/2)*1.2)})}R.sort((O,I)=>O.d-I.d);let E=new Set(s);for(let O of f.values())delete O.slot;for(let O of R){if(!E.size)break;let I=null,j=1/0;for(let z of E){let de=(z.x-O.x)**2+(z.y-O.y)**2;de<j&&(j=de,I=z)}if(!I)break;E.delete(I),f.get(I).slot={x:O.x,y:O.y,a:l}}}let $=()=>typeof r.lights=="function"?r.lights(c,d):r.lights??[{x:c*.3,y:-d*.35},{x:c*.72,y:-d*.3}];function F(s){if(!a||h)return;let l=Math.min(Math.max((s-p)/1e3,0),.25);p=s;let k=s/1e3;w=W(w,x?1:0,1-Math.exp(-l*3)),b.hoverEnabled=!x;for(let[m,E]of f)x&&E.slot?(m.hx=E.slot.x,m.hy=E.slot.y,m.base=W(m.base,E.slot.a,.12)):(m.hx=E.x+Math.sin(k*.7+E.phase)*L*r.float,m.hy=E.y+Math.cos(k*.6+E.phase)*L*r.float,m.base=W(m.base,E.r,.08)),m.shape=K(.35,1,w),m.grow=r.hoverGrow;let R=Math.min(8,Math.max(1,Math.ceil(l*60)));for(let m=0;m<R;m++)b.step(l/R);G(),S?u=requestAnimationFrame(F):a=!1}function G(){r.background?(t.fillStyle=r.background,t.fillRect(0,0,c,d)):t.clearRect(0,0,c,d);let s=$();for(let l of f.keys())Q(t,l,{lights:s,glow:w*.5,wallShadow:w>.4,flex:r.flex,dark:r.dark})}function T(){a||h||(a=!0,p=performance.now(),u=requestAnimationFrame(F))}function A(){a=!1,cancelAnimationFrame(u)}let q=s=>{let l=n.getBoundingClientRect();b.pointer.x=s.clientX-l.left,b.pointer.y=s.clientY-l.top,b.pointer.inside=!0},se=s=>q(s),ie=()=>b.pointer.inside=!1,le=s=>s.pointerType!=="mouse"&&q(s),Z=0,H=s=>{s.pointerType!=="mouse"&&(clearTimeout(Z),Z=window.setTimeout(()=>b.pointer.inside=!1,450))},ce=s=>{if(!r.clickToCreate)return;let l=n.getBoundingClientRect(),k=s.clientX-l.left,R=s.clientY-l.top;if(b.hit(k,R)||f.size>=r.maxBubbles)return;let m=M({x:k,y:R,radius:L*(C()?.05+i()*.06:.03+i()*.05)});r.onCreate?.(m)};n.addEventListener("pointermove",se),n.addEventListener("pointerleave",ie),n.addEventListener("pointerdown",le),n.addEventListener("pointerup",H),n.addEventListener("pointercancel",H),n.addEventListener("click",ce);let be=typeof ResizeObserver=="function"?new ResizeObserver(()=>P()):null;be?.observe(n);let ue=typeof IntersectionObserver=="function"?new IntersectionObserver(([s])=>{S=s.isIntersecting,S&&T()}):null;ue?.observe(n),P();let ve=r.count==="auto"?Math.round(Math.min(48,Math.max(20,c*d/9e3))):r.count;for(let s=0;s<ve;s++)M();for(let s=0;s<200;s++)b.step(1/60);if(g){x=!0,w=1,B();for(let[s,l]of f)s.x=s.px=l.slot?.x??s.x,s.y=s.py=l.slot?.y??s.y,s.base=s.r=l.slot?.a??s.base,s.shape=1;G()}else T();return{field:b,options:r,get bubbles(){return[...f.keys()]},get compressed(){return x},add:M,remove(s){f.delete(s);let l=b.bubbles.indexOf(s);l>=0&&b.bubbles.splice(l,1),x&&B()},clear(){f.clear(),b.bubbles.length=0},compress(){x=!0,B()},release(){x=!1},toggle(){return x=!x,x&&B(),x},shake(s=2400){x=!1;for(let l of f.keys())b.kick(l,(i()-.5)*s,(i()-.5)*s)},setReaction(s){b.reaction=s},setPalette(s,l){Y({[s]:l})},resize:P,start:T,stop:A,destroy(){h=!0,A(),clearTimeout(Z),be?.disconnect(),ue?.disconnect(),n.removeEventListener("pointermove",se),n.removeEventListener("pointerleave",ie),n.removeEventListener("pointerdown",le),n.removeEventListener("pointerup",H),n.removeEventListener("pointercancel",H),n.removeEventListener("click",ce),f.clear(),b.bubbles.length=0}}}return Ce(Ie);})();
@@ -0,0 +1,82 @@
1
+ import { BubbleField, type Bubble, type FieldOptions } from './engine';
2
+ import { type Light } from './draw';
3
+ import { type BubblePalette } from './palettes';
4
+ export interface BubblesOptions {
5
+ /** How many bubbles to start with, or 'auto' to fit the canvas area. Default 'auto'. */
6
+ count?: number | 'auto';
7
+ /** Hard limit when the visitor adds more. Default 90. */
8
+ maxBubbles?: number;
9
+ /** Radius range as a fraction of the canvas size. Default [0.035, 0.11] (larger on phones). */
10
+ size?: readonly [number, number];
11
+ /** Relative weight of each palette kind in new bubbles. Default pearl 62 %, gold 24 %, glass 14 %. */
12
+ kinds?: Record<string, number>;
13
+ /** Registers or overrides palettes (merged field by field). */
14
+ palettes?: Record<string, Partial<BubblePalette>>;
15
+ /** Physics of the field. */
16
+ physics?: FieldOptions;
17
+ /** How much the bubbles react to the cursor, 0 (nothing) … 3 (grow on hover) … 10 (very lively). Default 5. */
18
+ reaction?: number;
19
+ /** Radius multiplier of the bubble under the cursor at maximum reaction. Default 1.6. */
20
+ hoverGrow?: number;
21
+ /** Idle drift, as a fraction of the canvas size. 0 keeps them still. Default 0.015. */
22
+ float?: number;
23
+ /** Light sources in canvas pixels, or a function of the canvas size. Default: two lights above the canvas. */
24
+ lights?: Light[] | ((width: number, height: number) => Light[]);
25
+ /** Paint for dark backgrounds (a faint room shows through each bubble). */
26
+ dark?: boolean;
27
+ /** Flexible glass: bubbles stretch along their motion. Default true. */
28
+ flex?: boolean;
29
+ /** Clicking an empty spot creates a bubble. Default true. */
30
+ clickToCreate?: boolean;
31
+ /** Honeycomb cell size as a fraction of the canvas size. Default 0.045 (larger on phones). */
32
+ honeycomb?: number;
33
+ /** Fill the canvas with a colour each frame. Default null (transparent). */
34
+ background?: string | null;
35
+ /** Cap on the device pixel ratio. Default 2. */
36
+ maxDpr?: number;
37
+ /** Seed of the pseudo-random layout, so it is the same on every load. Default 7. */
38
+ seed?: number;
39
+ /** Start as a still honeycomb when the visitor prefers reduced motion. Default true. */
40
+ respectReducedMotion?: boolean;
41
+ /** Called when a bubble is created by a click. */
42
+ onCreate?: (bubble: Bubble) => void;
43
+ }
44
+ export interface BubbleCanvas {
45
+ /** The physics field: add, remove or inspect bubbles directly. */
46
+ readonly field: BubbleField;
47
+ readonly bubbles: Bubble[];
48
+ readonly compressed: boolean;
49
+ /** Options in use. Changing hoverGrow, float, flex, dark, background or clickToCreate takes effect on the next frame. */
50
+ readonly options: {
51
+ hoverGrow: number;
52
+ float: number;
53
+ flex: boolean;
54
+ dark: boolean;
55
+ background: string | null;
56
+ clickToCreate: boolean;
57
+ };
58
+ /** Adds a bubble at a position (default: random). `radius` is in pixels. */
59
+ add(init?: {
60
+ x?: number;
61
+ y?: number;
62
+ radius?: number;
63
+ kind?: string;
64
+ }): Bubble;
65
+ remove(b: Bubble): void;
66
+ clear(): void;
67
+ /** Packs every bubble into a honeycomb of hexagonal tiles. */
68
+ compress(): void;
69
+ /** Lets the tiles go back to being bubbles. */
70
+ release(): void;
71
+ toggle(): boolean;
72
+ /** Gives every bubble a random push. Default strength 2400 px/s. */
73
+ shake(strength?: number): void;
74
+ setReaction(level: number): void;
75
+ /** Changes colours at runtime. */
76
+ setPalette(kind: string, palette: Partial<BubblePalette>): void;
77
+ resize(): void;
78
+ start(): void;
79
+ stop(): void;
80
+ destroy(): void;
81
+ }
82
+ export declare function createBubbles(target: HTMLCanvasElement | string, options?: BubblesOptions): BubbleCanvas;
@@ -0,0 +1,4 @@
1
+ export type Rgba = [number, number, number, number];
2
+ /** Parses #rgb, #rgba, #rrggbb, #rrggbbaa, rgb() and rgba() into [r, g, b, a] (a in 0..1). */
3
+ export declare function parseColor(input: string): Rgba;
4
+ export declare const rgbaString: (c: Rgba, alpha?: number) => string;
package/dist/draw.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ import type { Bubble } from './engine';
2
+ /**
3
+ * Traces a cell that morphs from a circle (t = 0) to a flat-top hexagon
4
+ * (t = 1) while keeping the same apothem `a`: a hexagon of circumradius R
5
+ * whose corners are rounded with radius c, where c shrinks as t grows.
6
+ */
7
+ export declare function cellPath(ctx: CanvasRenderingContext2D, x: number, y: number, a: number, t: number): void;
8
+ /** A light in the scene, in canvas pixels. Bubbles are lit by the nearest one. */
9
+ export interface Light {
10
+ x: number;
11
+ y: number;
12
+ }
13
+ export interface PaintOptions {
14
+ /** Light sources. Default: one far up and to the left of each bubble. */
15
+ lights?: Light[];
16
+ /** 0..1 aura around bubbles whose palette defines `glow`. */
17
+ glow?: number;
18
+ /** Hexagons mounted on a wall cast a soft shadow. */
19
+ wallShadow?: boolean;
20
+ /** Stretch along the motion (flexible glass). */
21
+ flex?: boolean;
22
+ /** On a dark background: carry a faint room inside the bubble, as the marble does on a light one. */
23
+ dark?: boolean;
24
+ /** Colours of that room, centre and edge. */
25
+ room?: readonly [string, string];
26
+ }
27
+ /** Paints one bubble (or hexagonal tile) on a 2D canvas. Colours come from the palette of `b.kind`. */
28
+ export declare function paintBubble(ctx: CanvasRenderingContext2D, b: Bubble, o?: PaintOptions): void;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Bubble Maker 2.0 — physics core.
3
+ *
4
+ * Rewrite of the 2013–2015 Bubble Maker (github.com/neoligero/Bubble_maker).
5
+ * The original grew a bubble on hover and pushed its neighbours with chained
6
+ * 200 ms timeouts, splitting the push by |dx|/(|dx|+|dy|). This version runs
7
+ * a continuous simulation instead:
8
+ * - Verlet integration with a damped spring towards each bubble's home,
9
+ * - position-based collision relaxation along the true centre-to-centre
10
+ * direction, weighted by mass (area), so chain reactions resolve in-frame,
11
+ * - hover / focus growth with eased radius, touch friendly,
12
+ * - the cursor moves the air: nearby bubbles drift away and follow its wake,
13
+ * with a single 0–10 `reaction` dial,
14
+ * - a `shape` value (0 circle → 1 hexagon) used by renderers to compress
15
+ * bubbles into a honeycomb.
16
+ */
17
+ export interface Bubble {
18
+ x: number;
19
+ y: number;
20
+ px: number;
21
+ py: number;
22
+ r: number;
23
+ base: number;
24
+ grow: number;
25
+ hover: number;
26
+ hx: number;
27
+ hy: number;
28
+ shape: number;
29
+ alpha: number;
30
+ ghost: boolean;
31
+ fixed: boolean;
32
+ kind: string;
33
+ el?: HTMLElement;
34
+ }
35
+ export interface FieldOptions {
36
+ stiffness?: number;
37
+ damping?: number;
38
+ iterations?: number;
39
+ gap?: number;
40
+ reaction?: number;
41
+ }
42
+ export declare class BubbleField {
43
+ bubbles: Bubble[];
44
+ pointer: {
45
+ x: number;
46
+ y: number;
47
+ inside: boolean;
48
+ };
49
+ focused: Bubble | null;
50
+ hoverEnabled: boolean;
51
+ bounds: {
52
+ x: number;
53
+ y: number;
54
+ w: number;
55
+ h: number;
56
+ } | null;
57
+ stiffness: number;
58
+ damping: number;
59
+ iterations: number;
60
+ gap: number;
61
+ reaction: number;
62
+ private pv;
63
+ private last;
64
+ constructor(o?: FieldOptions);
65
+ add(init: Partial<Bubble> & {
66
+ x: number;
67
+ y: number;
68
+ base: number;
69
+ }): Bubble;
70
+ /** Gives a bubble an instant velocity (px/s). */
71
+ kick(b: Bubble, vx: number, vy: number, dt?: number): void;
72
+ hit(x: number, y: number): Bubble | null;
73
+ step(dtIn: number): void;
74
+ }
75
+ /** Small deterministic PRNG so layouts are stable between reloads. */
76
+ export declare function prng(seed?: number): () => number;
77
+ export declare const clamp: (v: number, a?: number, b?: number) => number;
78
+ export declare const lerp: (a: number, b: number, t: number) => number;
79
+ export declare const smooth: (e0: number, e1: number, x: number) => number;
80
+ export declare const easeInOut: (t: number) => number;
@@ -0,0 +1,9 @@
1
+ export { BubbleField, prng, clamp, lerp, smooth, easeInOut } from './engine';
2
+ export type { Bubble, FieldOptions } from './engine';
3
+ export { paintBubble, cellPath } from './draw';
4
+ export type { Light, PaintOptions } from './draw';
5
+ export { defaultPalettes, getPalette, listPalettes, registerPalettes, resetPalettes } from './palettes';
6
+ export type { BubblePalette, PaletteMap } from './palettes';
7
+ export { parseColor } from './color';
8
+ export { createBubbles } from './bubbles';
9
+ export type { BubbleCanvas, BubblesOptions } from './bubbles';