@hypertools/sdk 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,310 @@
1
+ /**
2
+ * React Example
3
+ *
4
+ * Demonstrates @hypertools/sdk React integration with useExperience hook.
5
+ * Creates a visualization with React-controlled parameters.
6
+ */
7
+
8
+ import React, { useState } from 'react';
9
+ import { createRoot } from 'react-dom/client';
10
+ import { useExperience, ExperienceView } from '@hypertools/sdk/react';
11
+
12
+ // Styles
13
+ const styles = {
14
+ container: {
15
+ maxWidth: '900px',
16
+ margin: '0 auto',
17
+ },
18
+ title: {
19
+ marginBottom: '1rem',
20
+ textAlign: 'center' as const,
21
+ },
22
+ experienceContainer: {
23
+ borderRadius: '8px',
24
+ overflow: 'hidden',
25
+ marginBottom: '1.5rem',
26
+ },
27
+ controls: {
28
+ display: 'grid',
29
+ gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
30
+ gap: '1rem',
31
+ padding: '1rem',
32
+ background: '#2a2a4a',
33
+ borderRadius: '8px',
34
+ },
35
+ controlGroup: {
36
+ display: 'flex',
37
+ flexDirection: 'column' as const,
38
+ gap: '0.5rem',
39
+ },
40
+ label: {
41
+ fontSize: '0.9rem',
42
+ color: '#aaa',
43
+ },
44
+ input: {
45
+ padding: '0.5rem',
46
+ borderRadius: '4px',
47
+ border: '1px solid #4a4a6a',
48
+ background: '#1a1a2e',
49
+ color: '#fff',
50
+ },
51
+ button: {
52
+ padding: '0.75rem 1.5rem',
53
+ borderRadius: '4px',
54
+ border: 'none',
55
+ background: '#4a4a6a',
56
+ color: '#fff',
57
+ cursor: 'pointer',
58
+ fontSize: '1rem',
59
+ },
60
+ buttonPrimary: {
61
+ background: '#00ff88',
62
+ color: '#000',
63
+ },
64
+ status: {
65
+ marginTop: '1rem',
66
+ padding: '1rem',
67
+ background: '#2a2a4a',
68
+ borderRadius: '8px',
69
+ fontFamily: 'monospace',
70
+ fontSize: '0.85rem',
71
+ },
72
+ };
73
+
74
+ function WaveVisualization() {
75
+ const {
76
+ experience,
77
+ isReady,
78
+ isPlaying,
79
+ params,
80
+ setParam,
81
+ play,
82
+ pause,
83
+ toggle,
84
+ } = useExperience({
85
+ paramDefs: {
86
+ waveColor: {
87
+ type: 'color',
88
+ value: '#00ff88',
89
+ label: 'Wave Color',
90
+ },
91
+ backgroundColor: {
92
+ type: 'color',
93
+ value: '#1a1a2e',
94
+ label: 'Background',
95
+ },
96
+ amplitude: {
97
+ type: 'number',
98
+ value: 50,
99
+ min: 10,
100
+ max: 150,
101
+ label: 'Amplitude',
102
+ },
103
+ frequency: {
104
+ type: 'number',
105
+ value: 0.02,
106
+ min: 0.005,
107
+ max: 0.05,
108
+ step: 0.005,
109
+ label: 'Frequency',
110
+ },
111
+ waveCount: {
112
+ type: 'number',
113
+ value: 5,
114
+ min: 1,
115
+ max: 10,
116
+ label: 'Wave Count',
117
+ },
118
+ speed: {
119
+ type: 'number',
120
+ value: 0.05,
121
+ min: 0.01,
122
+ max: 0.2,
123
+ step: 0.01,
124
+ label: 'Speed',
125
+ },
126
+ },
127
+ setup(context) {
128
+ const canvas = document.createElement('canvas');
129
+ const ctx = canvas.getContext('2d')!;
130
+ canvas.width = 800;
131
+ canvas.height = 400;
132
+ context.mount.appendChild(canvas);
133
+
134
+ let time = 0;
135
+
136
+ context.experience.on('frame', () => {
137
+ const {
138
+ waveColor,
139
+ backgroundColor,
140
+ amplitude,
141
+ frequency,
142
+ waveCount,
143
+ speed,
144
+ } = context.params as Record<string, unknown>;
145
+
146
+ // Clear
147
+ ctx.fillStyle = backgroundColor as string;
148
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
149
+
150
+ // Draw waves
151
+ for (let w = 0; w < (waveCount as number); w++) {
152
+ const offset = w * 0.5;
153
+ const alpha = 1 - w / (waveCount as number) * 0.5;
154
+
155
+ ctx.beginPath();
156
+ ctx.strokeStyle = (waveColor as string) + Math.floor(alpha * 255).toString(16).padStart(2, '0');
157
+ ctx.lineWidth = 3;
158
+
159
+ for (let x = 0; x < canvas.width; x++) {
160
+ const y =
161
+ canvas.height / 2 +
162
+ Math.sin((x * (frequency as number) + time + offset) * Math.PI * 2) *
163
+ (amplitude as number) *
164
+ (1 - w / (waveCount as number) * 0.3);
165
+
166
+ if (x === 0) {
167
+ ctx.moveTo(x, y);
168
+ } else {
169
+ ctx.lineTo(x, y);
170
+ }
171
+ }
172
+
173
+ ctx.stroke();
174
+ }
175
+
176
+ time += speed as number;
177
+ });
178
+
179
+ return () => canvas.remove();
180
+ },
181
+ });
182
+
183
+ const [captureStatus, setCaptureStatus] = useState<string>('');
184
+
185
+ const handleCapture = async () => {
186
+ if (!experience) return;
187
+ setCaptureStatus('Capturing...');
188
+ const blob = await experience.captureImage('png');
189
+ if (blob) {
190
+ const url = URL.createObjectURL(blob);
191
+ const a = document.createElement('a');
192
+ a.href = url;
193
+ a.download = 'wave-capture.png';
194
+ a.click();
195
+ URL.revokeObjectURL(url);
196
+ setCaptureStatus('Captured!');
197
+ } else {
198
+ setCaptureStatus('Capture failed');
199
+ }
200
+ setTimeout(() => setCaptureStatus(''), 2000);
201
+ };
202
+
203
+ return (
204
+ <div style={styles.container}>
205
+ <h1 style={styles.title}>React Integration Example</h1>
206
+
207
+ <div style={styles.experienceContainer}>
208
+ <ExperienceView
209
+ experience={experience}
210
+ style={{ width: '100%', height: '400px' }}
211
+ />
212
+ </div>
213
+
214
+ <div style={styles.controls}>
215
+ <div style={styles.controlGroup}>
216
+ <label style={styles.label}>Wave Color</label>
217
+ <input
218
+ type="color"
219
+ value={params.waveColor as string}
220
+ onChange={(e) => setParam('waveColor', e.target.value)}
221
+ style={styles.input}
222
+ />
223
+ </div>
224
+
225
+ <div style={styles.controlGroup}>
226
+ <label style={styles.label}>Background</label>
227
+ <input
228
+ type="color"
229
+ value={params.backgroundColor as string}
230
+ onChange={(e) => setParam('backgroundColor', e.target.value)}
231
+ style={styles.input}
232
+ />
233
+ </div>
234
+
235
+ <div style={styles.controlGroup}>
236
+ <label style={styles.label}>Amplitude: {params.amplitude}</label>
237
+ <input
238
+ type="range"
239
+ min={10}
240
+ max={150}
241
+ value={params.amplitude as number}
242
+ onChange={(e) => setParam('amplitude', Number(e.target.value))}
243
+ style={styles.input}
244
+ />
245
+ </div>
246
+
247
+ <div style={styles.controlGroup}>
248
+ <label style={styles.label}>Frequency: {(params.frequency as number)?.toFixed(3)}</label>
249
+ <input
250
+ type="range"
251
+ min={0.005}
252
+ max={0.05}
253
+ step={0.005}
254
+ value={params.frequency as number}
255
+ onChange={(e) => setParam('frequency', Number(e.target.value))}
256
+ style={styles.input}
257
+ />
258
+ </div>
259
+
260
+ <div style={styles.controlGroup}>
261
+ <label style={styles.label}>Wave Count: {params.waveCount}</label>
262
+ <input
263
+ type="range"
264
+ min={1}
265
+ max={10}
266
+ value={params.waveCount as number}
267
+ onChange={(e) => setParam('waveCount', Number(e.target.value))}
268
+ style={styles.input}
269
+ />
270
+ </div>
271
+
272
+ <div style={styles.controlGroup}>
273
+ <label style={styles.label}>Speed: {(params.speed as number)?.toFixed(2)}</label>
274
+ <input
275
+ type="range"
276
+ min={0.01}
277
+ max={0.2}
278
+ step={0.01}
279
+ value={params.speed as number}
280
+ onChange={(e) => setParam('speed', Number(e.target.value))}
281
+ style={styles.input}
282
+ />
283
+ </div>
284
+ </div>
285
+
286
+ <div style={{ display: 'flex', gap: '1rem', marginTop: '1rem' }}>
287
+ <button
288
+ style={{ ...styles.button, ...(isPlaying ? {} : styles.buttonPrimary) }}
289
+ onClick={toggle}
290
+ >
291
+ {isPlaying ? 'Pause' : 'Play'}
292
+ </button>
293
+
294
+ <button style={styles.button} onClick={handleCapture}>
295
+ {captureStatus || 'Capture PNG'}
296
+ </button>
297
+ </div>
298
+
299
+ <div style={styles.status}>
300
+ <div>Ready: {isReady ? 'Yes' : 'No'}</div>
301
+ <div>Playing: {isPlaying ? 'Yes' : 'No'}</div>
302
+ <div>Frame: {experience?.currentFrame ?? 0}</div>
303
+ </div>
304
+ </div>
305
+ );
306
+ }
307
+
308
+ // Mount React app
309
+ const root = createRoot(document.getElementById('root')!);
310
+ root.render(<WaveVisualization />);
@@ -0,0 +1,64 @@
1
+ # React Landing Page Example
2
+
3
+ This example demonstrates how to use an exported HyperTools experience as an interactive background in a React application, and how to build custom features on top of it using the `@hypertools/sdk`.
4
+
5
+ ## What This Example Shows
6
+
7
+ ### Core SDK Usage
8
+ - Using `ExperienceController` to connect to an exported web component
9
+ - Setting parameters programmatically with `setParam()` and `setParams()`
10
+ - Dispatching synthetic events with `dispatchToCanvas()` for triggering interactions
11
+ - Listening to parameter changes with the event system
12
+
13
+ ### Custom Features Built on Top
14
+ This example showcases what developers can build beyond what the HyperTools platform provides:
15
+
16
+ 1. **Preset System** - Pre-configured settings with visual selection indicator
17
+ 2. **Click-to-Form Mode** - Click anywhere to trigger formation at that position
18
+ 3. **Auto-pilot Mode** - Automatically cycles through presets
19
+ 4. **Idle Screensaver** - Starts auto-pilot after 10 seconds of inactivity
20
+ 5. **Time-Based Theme** - Applies text based on time of day
21
+ 6. **Share Configuration URL** - Generate shareable URLs with encoded settings
22
+ 7. **Keyboard Shortcuts** - Full keyboard control for power users
23
+
24
+ ## Getting Started
25
+
26
+ 1. Install dependencies:
27
+ ```bash
28
+ bun install
29
+ ```
30
+
31
+ 2. Place your exported HyperTools experience in the `public/` folder (e.g., `boids-flocking-project.js`)
32
+
33
+ 3. Update `index.html` to load your experience:
34
+ ```html
35
+ <script src="/your-experience-name.js"></script>
36
+ ```
37
+
38
+ 4. Update `src/App.tsx` to use your experience's tag name:
39
+ ```tsx
40
+ <your-experience-name ref={experienceRef} />
41
+ ```
42
+
43
+ 5. Run the dev server:
44
+ ```bash
45
+ bun run dev
46
+ ```
47
+
48
+ ## Keyboard Shortcuts
49
+
50
+ | Key | Action |
51
+ |-----|--------|
52
+ | `Space` | Trigger formation |
53
+ | `↑` / `↓` | Adjust bird count |
54
+ | `R` | Toggle rainbow mode |
55
+ | `S` | Share config URL |
56
+ | `A` | Toggle auto-pilot |
57
+ | `1-4` | Apply presets |
58
+ | `?` | Show help |
59
+
60
+ ## Key Files
61
+
62
+ - `src/App.tsx` - Main React component with SDK integration and custom features
63
+ - `src/App.css` - Styling for the control panel and overlays
64
+ - `public/` - Place your exported experience here
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Landing Page with Boids Background</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <!-- Load the exported experience web component -->
11
+ <script src="/boids-flocking-project.js"></script>
12
+ <script type="module" src="/src/main.tsx"></script>
13
+ </body>
14
+ </html>
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "react-landing-example",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@hypertools/sdk": "latest",
13
+ "react": "^18.3.1",
14
+ "react-dom": "^18.3.1"
15
+ },
16
+ "devDependencies": {
17
+ "@types/react": "^18.3.18",
18
+ "@types/react-dom": "^18.3.5",
19
+ "@vitejs/plugin-react": "^4.3.4",
20
+ "typescript": "~5.6.2",
21
+ "vite": "^5.4.11"
22
+ }
23
+ }
@@ -0,0 +1,12 @@
1
+ (()=>{var nt={birdCount:810,maxSpeed:5.5,separationRadius:40,alignmentRadius:50,cohesionRadius:50,separationWeight:.7000000000000001,alignmentWeight:.4,cohesionWeight:.6000000000000001,turnFactor:.9500000000000001,birdSize:2.9000000000000004,birdColor:"#ffc0cb",background:"#0a0a0a",showTrails:!1,trailOpacity:.16,liquidEffect:!1,mouseAction:2,rainbowMode:!0,colorSpeed:3.2,displayText:"\u0425 \u0423 \u0419"},V=!1,dt="transparent",X=null;async function ot(v){X=v;class n extends HTMLElement{_mount=null;_controlsContainer=null;_cleanup=void 0;_params={};_pane=null;constructor(){super(),this.attachShadow({mode:"open"})}connectedCallback(){if(!this.shadowRoot||!X)return;let o=document.createElement("style");o.textContent=`
2
+ ${V&&typeof __THEME_CSS__<"u"?__THEME_CSS__:""}
3
+ :host { display: block; width: 100%; height: 100%; }
4
+ .mount {
5
+ width: 100%;
6
+ height: 100%;
7
+ position: relative;
8
+ overflow: hidden;
9
+ background: ${dt};
10
+ }
11
+
12
+ `,this.shadowRoot.appendChild(o),this._mount=document.createElement("div"),this._mount.className="mount",this.shadowRoot.appendChild(this._mount),V&&(this._controlsContainer=document.createElement("div"),this._controlsContainer.className="controls-container",this.shadowRoot.appendChild(this._controlsContainer));let i=X.controls?.definitions||{};for(let[t,a]of Object.entries(i))this.getAttribute(t)!==null?this._params[t]=this._parseAttr(t,this.getAttribute(t),a):t in nt?this._params[t]=nt[t]:this._params[t]=a.value;let e=this;this._params=new Proxy(this._params,{set:(t,a,r)=>{let m=t[a];return t[a]=r,m!==r&&e.dispatchEvent(new CustomEvent("paramchange",{detail:{key:a,value:r,previousValue:m},bubbles:!0})),!0}});let l={mount:this._mount,params:this._params,exports:{setFilename:()=>{},useDefaultCanvasCapture:()=>{},captureImage:async()=>{let t=this._mount?.querySelector("canvas");if(!t)throw new Error("No canvas found");return new Promise((a,r)=>{t.toBlob(m=>m?a(m):r(new Error("Failed to capture")))})}},environment:{onResize:t=>{let a=new ResizeObserver(()=>t());return a.observe(this._mount),()=>a.disconnect()},window,document},controls:null};this._cleanup=X.setup(l),V&&this._controlsContainer&&typeof __createControls__=="function"&&__createControls__(this._params,i,this._controlsContainer,this.shadowRoot).then(t=>{this._pane=t,this._pane&&typeof __setupDraggable__=="function"&&__setupDraggable__(this._controlsContainer,this._pane)}),this.dispatchEvent(new CustomEvent("ready",{bubbles:!0}))}disconnectedCallback(){this._cleanup&&this._cleanup(),this._pane?.dispose&&this._pane.dispose()}_parseAttr(o,i,e){switch(e.type){case"number":return parseFloat(i);case"boolean":return i==="true"||i==="";default:return i}}setParam(o,i){this._params[o]=i,this._pane?.refresh()}setParams(o){for(let[i,e]of Object.entries(o))this._params[i]=e;this._pane?.refresh()}getParams(){return{...this._params}}getParamDefs(){return X?.controls?.definitions||{}}}customElements.get("boids-flocking-project")||customElements.define("boids-flocking-project",n)}var J={birdCount:{type:"number",label:"Bird Count",value:100,min:10,max:1e3,step:10},maxSpeed:{type:"number",label:"Max Speed",value:4,min:1,max:10,step:.5},separationRadius:{type:"number",label:"Separation Radius",value:25,min:10,max:100,step:5},alignmentRadius:{type:"number",label:"Alignment Radius",value:50,min:10,max:150,step:5},cohesionRadius:{type:"number",label:"Cohesion Radius",value:50,min:10,max:150,step:5},separationWeight:{type:"number",label:"Separation Weight",value:1.5,min:0,max:3,step:.1},alignmentWeight:{type:"number",label:"Alignment Weight",value:1,min:0,max:3,step:.1},cohesionWeight:{type:"number",label:"Cohesion Weight",value:1,min:0,max:3,step:.1},turnFactor:{type:"number",label:"Turn Factor",value:.2,min:.05,max:1,step:.05},birdSize:{type:"number",label:"Size Scale",value:1,min:.5,max:3,step:.1},birdColor:{type:"color",label:"Bird Color",value:"#ffc0cb"},background:{type:"color",label:"Background",value:"#0a0a0a"},showTrails:{type:"boolean",label:"Show Trails",value:!1},trailOpacity:{type:"number",label:"Trail Fade",value:.1,min:.01,max:.5,step:.01},liquidEffect:{type:"boolean",label:"Liquid Glass",value:!1},mouseAction:{type:"number",label:"Mouse: 0=Off 1=Attr 2=Repel",value:1,min:0,max:2,step:1},rainbowMode:{type:"boolean",label:"Rainbow Mode",value:!1},colorSpeed:{type:"number",label:"Color Cycle Speed",value:1,min:.1,max:10,step:.1},displayText:{type:"text",label:"Display Text",value:":==> \u0425 \u0423 \u0419 :==>"}};function it(v){v=v.replace(/^#/,"");let n=parseInt(v,16),s=n>>16&255,o=n>>8&255,i=n&255;return`${s}, ${o}, ${i}`}function q(v,n,s,o){let i=[],e=document.createElement("canvas"),l=2;e.width=s*l,e.height=o*l;let t=e.getContext("2d");if(!t)return i;let a=Math.min(s,o)*.15*l;t.font=`bold ${a}px Arial, sans-serif`,t.fillStyle="white",t.textAlign="center",t.textBaseline="middle",t.fillText(v,s*l/2,o*l/2);let m=t.getImageData(0,0,e.width,e.height).data,p=[];for(let c=0;c<e.height;c+=2)for(let d=0;d<e.width;d+=2){let _=(c*e.width+d)*4;m[_+3]>128&&p.push({x:d/l,y:c/l})}if(p.length>0)for(let c=0;c<n;c++){let d=p[Math.floor(Math.random()*p.length)];i.push({x:d.x,y:d.y})}if(i.length===0)for(let c=0;c<n;c++)i.push({x:Math.random()*s,y:Math.random()*o});return i}var I=class{x;y;vx;vy;targetX;targetY;constructor(n,s,o){o?(this.x=o.x,this.y=o.y,this.targetX=o.x,this.targetY=o.y):(this.x=Math.random()*n,this.y=Math.random()*s,this.targetX=this.x,this.targetY=this.y);let i=Math.random()*Math.PI*2,e=Math.random()*2+1;this.vx=Math.cos(i)*e,this.vy=Math.sin(i)*e}update(n,s,o,i,e=!1,l){let t=i.maxSpeed??4,a=i.separationRadius??25,r=i.alignmentRadius??50,m=i.cohesionRadius??50,p=i.separationWeight??1.5,c=i.alignmentWeight??1,d=i.cohesionWeight??1,_=i.turnFactor??.2,P=50;if(l&&l.mode!==0){let u=this.x-l.x,f=this.y-l.y,y=u*u+f*f,x=200;if(y<x*x){let A=Math.sqrt(y),k=(1-A/x)*.5;l.mode===1?(this.vx-=u/A*k*2,this.vy-=f/A*k*2):l.mode===2&&(this.vx+=u/A*k*4,this.vy+=f/A*k*4)}}if(e){let u=this.targetX-this.x,f=this.targetY-this.y,y=Math.sqrt(u*u+f*f);y>1&&(this.vx+=u/y*.5,this.vy+=f/y*.5)}let W=0,L=0,w=0,M=0,E=0,T=0,F=0,Y=0,H=0;for(let u of n){if(u===this)continue;let f=this.x-u.x,y=this.y-u.y,x=Math.sqrt(f*f+y*y);x<a&&x>0&&(W+=f/x,L+=y/x,w++),x<r&&(M+=u.vx,E+=u.vy,T++),x<m&&(F+=u.x,Y+=u.y,H++)}w>0&&(W/=w,L/=w,this.vx+=W*p*.05,this.vy+=L*p*.05),T>0&&(M/=T,E/=T,this.vx+=(M-this.vx)*c*.05,this.vy+=(E-this.vy)*c*.05),H>0&&(F/=H,Y/=H,this.vx+=(F-this.x)*d*.001,this.vy+=(Y-this.y)*d*.001),this.x<P&&(this.vx+=_),this.x>s-P&&(this.vx-=_),this.y<P&&(this.vy+=_),this.y>o-P&&(this.vy-=_);let z=Math.sqrt(this.vx*this.vx+this.vy*this.vy);z>t&&(this.vx=this.vx/z*t,this.vy=this.vy/z*t),this.x+=this.vx,this.y+=this.vy,this.x<0&&(this.x=s),this.x>s&&(this.x=0),this.y<0&&(this.y=o),this.y>o&&(this.y=0)}draw(n,s,o,i,e,l){let t=e?`hsl(${l}, 80%, 60%)`:s;if(i){n.beginPath(),n.arc(this.x,this.y,6*o,0,Math.PI*2),n.fillStyle=t,n.fill();return}let a=Math.atan2(this.vy,this.vx);n.save(),n.translate(this.x,this.y),n.rotate(a),n.fillStyle=t,n.strokeStyle=t,n.lineWidth=1*o;let r=15*o,m=5*o;n.fillRect(-r/2,-m/2,r,m);let p=4*o;n.beginPath(),n.arc(r/2,0,p,0,Math.PI*2),n.fill();let c=3*o,d=2*o;n.beginPath(),n.arc(-r/2-c/2,-d,c,0,Math.PI*2),n.fill(),n.beginPath(),n.arc(-r/2-c/2,d,c,0,Math.PI*2),n.fill(),n.restore()}};function st(v){let{mount:n,params:s,exports:o,environment:i}=v,e=document.createElement("canvas");e.style.width="100%",e.style.height="100%",e.style.display="block",n.appendChild(e);let l=e.getContext("2d");if(!l)throw new Error("Unable to obtain 2D rendering context");o.setFilename("boids-flocking"),o.useDefaultCanvasCapture(!0);let t=[],a=0,r=0,m=!1,p=!1,c=0,d=0,_=!1,P=s.displayText??"HYPERTOOL",W=0,L=0,w=100,M=[],E=0,T=0,F=()=>{let{clientWidth:b,clientHeight:R}=n,O=window.devicePixelRatio||1;e.width=Math.max(1,Math.floor(b*O)),e.height=Math.max(1,Math.floor(R*O)),l.resetTransform(),l.scale(O,O),a=e.clientWidth,r=e.clientHeight,E=Math.ceil(a/w),T=Math.ceil(r/w),M=new Array(E*T);for(let C=0;C<M.length;C++)M[C]=[];if(m){let C=Math.floor(s.birdCount??100);if(t.length!==C){let S=s.displayText??"HYPERTOOL",$=q(S,C,a,r);t=[];for(let B=0;B<C;B++)t.push(new I(a,r,$[B]))}}};F(),i.onResize(F);let Y=Math.floor(s.birdCount??100),H=s.displayText??"HYPERTOOL",z=q(H,Y,a,r);for(let b=0;b<Y;b++)t.push(new I(a,r,z[b]));m=!0;let u=()=>{_=!0,d=Date.now()},f=()=>{_=!1},y=()=>{_=!1},x=b=>{let R=e.getBoundingClientRect();W=b.clientX-R.left,L=b.clientY-R.top};e.addEventListener("mousedown",u),e.addEventListener("mouseup",f),e.addEventListener("mouseleave",y),e.addEventListener("mousemove",x);let A=()=>{if(_&&!p&&Date.now()-d>=500){p=!0,c=180,_=!1;let R=t.length,O=s.displayText??"HYPERTOOL",C=q(O,R,a,r);for(let S=0;S<t.length;S++)t[S].targetX=C[S].x,t[S].targetY=C[S].y}},k=0,Q=()=>{A(),p&&(c--,c<=0&&(p=!1));let b=s.displayText??"HYPERTOOL";if(b!==P){P=b;let h=q(b,t.length,a,r);for(let g=0;g<t.length;g++)t[g].targetX=h[g].x,t[g].targetY=h[g].y}let R=Math.floor(s.birdCount??100);if(t.length<R){let h=R-t.length,g=q(b,h,a,r);for(let D=0;D<h;D++)t.push(new I(a,r,g[D]))}else t.length>R&&(t=t.slice(0,R));let O=s.showTrails??!1,C=s.trailOpacity??.1,S=s.background??"#0a0a0a",$=s.birdColor??"#ffc0cb",B=s.birdSize??1;O?l.fillStyle=`rgba(${it(S)}, ${C})`:l.fillStyle=S,l.fillRect(0,0,a,r);let Z=s.liquidEffect??!1,at=s.rainbowMode??!1,rt=s.colorSpeed??1,lt=s.mouseAction??1,ct=Date.now()*.05*rt;Z?e.style.filter="blur(8px) contrast(15)":e.style.filter="none";for(let h=0;h<M.length;h++)M[h].length=0;for(let h of t){let g=Math.floor(h.x/w),D=Math.floor(h.y/w);g>=0&&g<E&&D>=0&&D<T&&M[D*E+g].push(h)}let ut={x:W,y:L,mode:lt};t.forEach((h,g)=>{let D=Math.floor(h.x/w),ht=Math.floor(h.y/w),tt=[];for(let j=-1;j<=1;j++)for(let N=-1;N<=1;N++){let G=D+N,U=ht+j;if(G>=0&&G<E&&U>=0&&U<T){let et=M[U*E+G];for(let K=0;K<et.length;K++)tt.push(et[K])}}h.update(tt,a,r,s,p,ut),h.draw(l,$,B,Z,at,ct+g*5)}),k=window.requestAnimationFrame(Q)};return k=window.requestAnimationFrame(Q),()=>{window.cancelAnimationFrame(k),e.removeEventListener("mousedown",u),e.removeEventListener("mouseup",f),e.removeEventListener("mouseleave",y),e.removeEventListener("mousemove",x),e.remove()}}ot({controls:{definitions:J,options:{title:"BOIDS Flocking"}},exportWidget:{filename:"boids-flocking",useCanvasCapture:!0,enabled:!0},setup:st}).catch(v=>{console.error("[boids] Failed to initialise sandbox",v)});})();