@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.
- package/README.md +175 -0
- package/examples/README.md +84 -0
- package/examples/p5js/index.html +39 -0
- package/examples/p5js/main.ts +136 -0
- package/examples/react/index.html +22 -0
- package/examples/react/main.tsx +310 -0
- package/examples/react-landing/README.md +64 -0
- package/examples/react-landing/index.html +14 -0
- package/examples/react-landing/package.json +23 -0
- package/examples/react-landing/public/boids-flocking-project.js +12 -0
- package/examples/react-landing/src/App.css +379 -0
- package/examples/react-landing/src/App.tsx +483 -0
- package/examples/react-landing/src/main.tsx +9 -0
- package/examples/react-landing/src/types.d.ts +24 -0
- package/examples/react-landing/tsconfig.json +20 -0
- package/examples/react-landing/vite.config.ts +9 -0
- package/examples/recording/index.html +113 -0
- package/examples/recording/main.ts +256 -0
- package/examples/threejs/index.html +40 -0
- package/examples/threejs/main.ts +196 -0
- package/examples/vanilla-canvas/index.html +77 -0
- package/examples/vanilla-canvas/main.ts +162 -0
- package/package.json +5 -1
|
@@ -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)});})();
|