@hypertools/sdk 0.3.2 → 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/dist/core/ExperienceController.d.ts +39 -0
- package/dist/core/ExperienceController.d.ts.map +1 -1
- package/dist/core/index.js +2 -2
- package/dist/core/index.js.map +3 -3
- package/dist/index.js +39 -39
- package/dist/index.js.map +3 -3
- 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,483 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
2
|
+
import { ExperienceController } from '@hypertools/sdk';
|
|
3
|
+
import type { ExportedExperienceElement } from '@hypertools/sdk';
|
|
4
|
+
import './App.css';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* React Landing Page with HyperTools Experience Background
|
|
8
|
+
*
|
|
9
|
+
* This example shows how to:
|
|
10
|
+
* 1. Use an exported HyperTools experience as a background
|
|
11
|
+
* 2. Control it programmatically via ExperienceController from @hypertools/sdk
|
|
12
|
+
* 3. Build CUSTOM features on top that the platform doesn't provide
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// === CUSTOM FEATURE: Preset configurations ===
|
|
16
|
+
const PRESETS = {
|
|
17
|
+
default: { birdCount: 810, rainbowMode: true, displayText: 'HYPERTOOLS' },
|
|
18
|
+
calm: { birdCount: 100, rainbowMode: false, displayText: 'ZEN' },
|
|
19
|
+
party: { birdCount: 800, rainbowMode: true, displayText: 'PARTY!' },
|
|
20
|
+
chaos: { birdCount: 1000, rainbowMode: true, displayText: 'CHAOS' },
|
|
21
|
+
minimal: { birdCount: 50, rainbowMode: false, displayText: '' },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type PresetName = keyof typeof PRESETS;
|
|
25
|
+
|
|
26
|
+
// === CUSTOM FEATURE: Time-based themes ===
|
|
27
|
+
const getTimeBasedTheme = () => {
|
|
28
|
+
const hour = new Date().getHours();
|
|
29
|
+
if (hour >= 6 && hour < 12) return { name: 'Morning', displayText: 'RISE & SHINE' };
|
|
30
|
+
if (hour >= 12 && hour < 17) return { name: 'Afternoon', displayText: 'PRODUCTIVE' };
|
|
31
|
+
if (hour >= 17 && hour < 21) return { name: 'Evening', displayText: 'GOLDEN HOUR' };
|
|
32
|
+
return { name: 'Night', displayText: 'DREAM MODE' };
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function App() {
|
|
36
|
+
const experienceRef = useRef<ExportedExperienceElement>(null);
|
|
37
|
+
const controllerRef = useRef<ExperienceController | null>(null);
|
|
38
|
+
|
|
39
|
+
// Track some params for the UI
|
|
40
|
+
const [birdCount, setBirdCount] = useState(810);
|
|
41
|
+
const [rainbowMode, setRainbowMode] = useState(true);
|
|
42
|
+
const [displayText, setDisplayText] = useState('HYPERTOOLS');
|
|
43
|
+
const [isReady, setIsReady] = useState(false);
|
|
44
|
+
|
|
45
|
+
// === CUSTOM FEATURE: Selected preset tracking ===
|
|
46
|
+
const [selectedPreset, setSelectedPreset] = useState<PresetName | null>('default');
|
|
47
|
+
|
|
48
|
+
// === CUSTOM FEATURE: Auto-pilot mode ===
|
|
49
|
+
const [autoPilot, setAutoPilot] = useState(false);
|
|
50
|
+
const autoPilotRef = useRef<number | null>(null);
|
|
51
|
+
|
|
52
|
+
// === CUSTOM FEATURE: Keyboard shortcuts ===
|
|
53
|
+
const [showShortcuts, setShowShortcuts] = useState(false);
|
|
54
|
+
|
|
55
|
+
// === CUSTOM FEATURE: Click-to-form mode ===
|
|
56
|
+
const [clickToForm, setClickToForm] = useState(false);
|
|
57
|
+
|
|
58
|
+
// === CUSTOM FEATURE: Idle screensaver ===
|
|
59
|
+
const [idleMode, setIdleMode] = useState(false);
|
|
60
|
+
const idleTimerRef = useRef<number | null>(null);
|
|
61
|
+
const IDLE_TIMEOUT = 10000; // 10 seconds of inactivity
|
|
62
|
+
|
|
63
|
+
// === Check if current values match any preset ===
|
|
64
|
+
const detectCurrentPreset = useCallback((): PresetName | null => {
|
|
65
|
+
for (const [name, preset] of Object.entries(PRESETS)) {
|
|
66
|
+
if (
|
|
67
|
+
preset.birdCount === birdCount &&
|
|
68
|
+
preset.rainbowMode === rainbowMode &&
|
|
69
|
+
preset.displayText === displayText
|
|
70
|
+
) {
|
|
71
|
+
return name as PresetName;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null; // Custom configuration
|
|
75
|
+
}, [birdCount, rainbowMode, displayText]);
|
|
76
|
+
|
|
77
|
+
// Update selected preset when values change
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
setSelectedPreset(detectCurrentPreset());
|
|
80
|
+
}, [detectCurrentPreset]);
|
|
81
|
+
|
|
82
|
+
// Connect SDK controller when component mounts
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const element = experienceRef.current;
|
|
85
|
+
if (!element) return;
|
|
86
|
+
|
|
87
|
+
const onReady = () => {
|
|
88
|
+
// Connect ExperienceController from @hypertools/sdk
|
|
89
|
+
const controller = new ExperienceController({
|
|
90
|
+
element,
|
|
91
|
+
initialParams: {
|
|
92
|
+
birdCount: 810,
|
|
93
|
+
rainbowMode: true,
|
|
94
|
+
displayText: 'HYPERTOOLS',
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Listen to param changes
|
|
99
|
+
controller.on('paramChange', (event) => {
|
|
100
|
+
console.log('Param changed:', event.key, '=', event.value);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
controllerRef.current = controller;
|
|
104
|
+
setIsReady(true);
|
|
105
|
+
console.log('ExperienceController connected!');
|
|
106
|
+
console.log('Available params:', controller.getParamDefs());
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Check if already ready or wait for ready event
|
|
110
|
+
if ((element as any)._cleanup) {
|
|
111
|
+
onReady();
|
|
112
|
+
} else {
|
|
113
|
+
element.addEventListener('ready', onReady, { once: true });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return () => {
|
|
117
|
+
element.removeEventListener('ready', onReady);
|
|
118
|
+
controllerRef.current?.destroy();
|
|
119
|
+
};
|
|
120
|
+
}, []);
|
|
121
|
+
|
|
122
|
+
// Handlers for UI controls
|
|
123
|
+
const handleBirdCountChange = (value: number) => {
|
|
124
|
+
setBirdCount(value);
|
|
125
|
+
controllerRef.current?.setParam('birdCount', value);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const handleRainbowToggle = () => {
|
|
129
|
+
const newValue = !rainbowMode;
|
|
130
|
+
setRainbowMode(newValue);
|
|
131
|
+
controllerRef.current?.setParam('rainbowMode', newValue);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const handleTextChange = (value: string) => {
|
|
135
|
+
setDisplayText(value);
|
|
136
|
+
controllerRef.current?.setParam('displayText', value);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const handleReset = () => {
|
|
140
|
+
controllerRef.current?.resetParams();
|
|
141
|
+
// Sync local state with defaults
|
|
142
|
+
const defs = controllerRef.current?.getParamDefs();
|
|
143
|
+
if (defs) {
|
|
144
|
+
setBirdCount(defs.birdCount?.value as number ?? 100);
|
|
145
|
+
setRainbowMode(defs.rainbowMode?.value as boolean ?? false);
|
|
146
|
+
setDisplayText(defs.displayText?.value as string ?? '');
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Trigger formation by dispatching synthetic mouse events
|
|
151
|
+
const handleTriggerFormation = useCallback(() => {
|
|
152
|
+
const controller = controllerRef.current;
|
|
153
|
+
if (!controller) return;
|
|
154
|
+
|
|
155
|
+
// Simulate long press: mousedown -> wait 600ms -> mouseup
|
|
156
|
+
controller.dispatchToCanvas('mousedown', { clientX: 400, clientY: 300 });
|
|
157
|
+
setTimeout(() => {
|
|
158
|
+
controller.dispatchToCanvas('mouseup', { clientX: 400, clientY: 300 });
|
|
159
|
+
}, 600);
|
|
160
|
+
}, []);
|
|
161
|
+
|
|
162
|
+
// === CUSTOM FEATURE: Apply preset ===
|
|
163
|
+
const applyPreset = useCallback((presetName: PresetName) => {
|
|
164
|
+
const preset = PRESETS[presetName];
|
|
165
|
+
controllerRef.current?.setParams(preset);
|
|
166
|
+
setBirdCount(preset.birdCount);
|
|
167
|
+
setRainbowMode(preset.rainbowMode);
|
|
168
|
+
setDisplayText(preset.displayText);
|
|
169
|
+
setSelectedPreset(presetName);
|
|
170
|
+
}, []);
|
|
171
|
+
|
|
172
|
+
// === CUSTOM FEATURE: Reset to default ===
|
|
173
|
+
const handleResetToDefault = useCallback(() => {
|
|
174
|
+
applyPreset('default');
|
|
175
|
+
}, [applyPreset]);
|
|
176
|
+
|
|
177
|
+
// === CUSTOM FEATURE: Share configuration URL ===
|
|
178
|
+
const handleShareConfig = useCallback(() => {
|
|
179
|
+
const config = { birdCount, rainbowMode, displayText };
|
|
180
|
+
const params = new URLSearchParams({
|
|
181
|
+
config: btoa(JSON.stringify(config))
|
|
182
|
+
});
|
|
183
|
+
const url = `${window.location.origin}${window.location.pathname}?${params}`;
|
|
184
|
+
navigator.clipboard.writeText(url);
|
|
185
|
+
alert('Configuration URL copied to clipboard!');
|
|
186
|
+
}, [birdCount, rainbowMode, displayText]);
|
|
187
|
+
|
|
188
|
+
// === CUSTOM FEATURE: Load config from URL on mount ===
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
const params = new URLSearchParams(window.location.search);
|
|
191
|
+
const configParam = params.get('config');
|
|
192
|
+
if (configParam && isReady) {
|
|
193
|
+
try {
|
|
194
|
+
const config = JSON.parse(atob(configParam));
|
|
195
|
+
if (config.birdCount) handleBirdCountChange(config.birdCount);
|
|
196
|
+
if (config.rainbowMode !== undefined) {
|
|
197
|
+
setRainbowMode(config.rainbowMode);
|
|
198
|
+
controllerRef.current?.setParam('rainbowMode', config.rainbowMode);
|
|
199
|
+
}
|
|
200
|
+
if (config.displayText !== undefined) handleTextChange(config.displayText);
|
|
201
|
+
} catch (e) {
|
|
202
|
+
console.warn('Invalid config in URL');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}, [isReady]);
|
|
206
|
+
|
|
207
|
+
// === CUSTOM FEATURE: Apply time-based theme ===
|
|
208
|
+
const applyTimeTheme = useCallback(() => {
|
|
209
|
+
const theme = getTimeBasedTheme();
|
|
210
|
+
handleTextChange(theme.displayText);
|
|
211
|
+
}, []);
|
|
212
|
+
|
|
213
|
+
// === CUSTOM FEATURE: Auto-pilot cycling through presets ===
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
if (!autoPilot || !isReady) {
|
|
216
|
+
if (autoPilotRef.current) {
|
|
217
|
+
clearInterval(autoPilotRef.current);
|
|
218
|
+
autoPilotRef.current = null;
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const presetNames = Object.keys(PRESETS) as (keyof typeof PRESETS)[];
|
|
224
|
+
let index = 0;
|
|
225
|
+
|
|
226
|
+
const cycle = () => {
|
|
227
|
+
applyPreset(presetNames[index]);
|
|
228
|
+
// Trigger formation on each cycle
|
|
229
|
+
setTimeout(handleTriggerFormation, 500);
|
|
230
|
+
index = (index + 1) % presetNames.length;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
cycle(); // Apply first preset immediately
|
|
234
|
+
autoPilotRef.current = window.setInterval(cycle, 4000);
|
|
235
|
+
|
|
236
|
+
return () => {
|
|
237
|
+
if (autoPilotRef.current) {
|
|
238
|
+
clearInterval(autoPilotRef.current);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}, [autoPilot, isReady, applyPreset, handleTriggerFormation]);
|
|
242
|
+
|
|
243
|
+
// === CUSTOM FEATURE: Idle screensaver mode ===
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
const resetIdleTimer = () => {
|
|
246
|
+
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
|
|
247
|
+
if (idleMode) {
|
|
248
|
+
setIdleMode(false);
|
|
249
|
+
setAutoPilot(false);
|
|
250
|
+
}
|
|
251
|
+
idleTimerRef.current = window.setTimeout(() => {
|
|
252
|
+
setIdleMode(true);
|
|
253
|
+
setAutoPilot(true); // Start auto-pilot when idle
|
|
254
|
+
}, IDLE_TIMEOUT);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const events = ['mousemove', 'mousedown', 'keydown', 'touchstart', 'scroll'];
|
|
258
|
+
events.forEach((event) => window.addEventListener(event, resetIdleTimer));
|
|
259
|
+
resetIdleTimer();
|
|
260
|
+
|
|
261
|
+
return () => {
|
|
262
|
+
events.forEach((event) => window.removeEventListener(event, resetIdleTimer));
|
|
263
|
+
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
|
|
264
|
+
};
|
|
265
|
+
}, [idleMode, IDLE_TIMEOUT]);
|
|
266
|
+
|
|
267
|
+
// === CUSTOM FEATURE: Click-to-form (click anywhere to trigger formation at that position) ===
|
|
268
|
+
useEffect(() => {
|
|
269
|
+
if (!clickToForm || !isReady) return;
|
|
270
|
+
|
|
271
|
+
const handleClick = (e: MouseEvent) => {
|
|
272
|
+
// Ignore clicks on the control panel
|
|
273
|
+
if ((e.target as HTMLElement).closest('.control-panel')) return;
|
|
274
|
+
|
|
275
|
+
const controller = controllerRef.current;
|
|
276
|
+
if (!controller) return;
|
|
277
|
+
|
|
278
|
+
// Trigger formation at click position
|
|
279
|
+
controller.dispatchToCanvas('mousedown', { clientX: e.clientX, clientY: e.clientY });
|
|
280
|
+
setTimeout(() => {
|
|
281
|
+
controller.dispatchToCanvas('mouseup', { clientX: e.clientX, clientY: e.clientY });
|
|
282
|
+
}, 600);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
window.addEventListener('click', handleClick);
|
|
286
|
+
return () => window.removeEventListener('click', handleClick);
|
|
287
|
+
}, [clickToForm, isReady]);
|
|
288
|
+
|
|
289
|
+
// === CUSTOM FEATURE: Keyboard shortcuts ===
|
|
290
|
+
useEffect(() => {
|
|
291
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
292
|
+
if (!isReady) return;
|
|
293
|
+
// Ignore if typing in input
|
|
294
|
+
if (e.target instanceof HTMLInputElement) return;
|
|
295
|
+
|
|
296
|
+
switch (e.key.toLowerCase()) {
|
|
297
|
+
case ' ':
|
|
298
|
+
e.preventDefault();
|
|
299
|
+
handleTriggerFormation();
|
|
300
|
+
break;
|
|
301
|
+
case 'arrowup':
|
|
302
|
+
e.preventDefault();
|
|
303
|
+
handleBirdCountChange(Math.min(birdCount + 50, 1000));
|
|
304
|
+
break;
|
|
305
|
+
case 'arrowdown':
|
|
306
|
+
e.preventDefault();
|
|
307
|
+
handleBirdCountChange(Math.max(birdCount - 50, 10));
|
|
308
|
+
break;
|
|
309
|
+
case 'r':
|
|
310
|
+
setRainbowMode((prev) => {
|
|
311
|
+
const newValue = !prev;
|
|
312
|
+
controllerRef.current?.setParam('rainbowMode', newValue);
|
|
313
|
+
return newValue;
|
|
314
|
+
});
|
|
315
|
+
break;
|
|
316
|
+
case 's':
|
|
317
|
+
handleShareConfig();
|
|
318
|
+
break;
|
|
319
|
+
case 'a':
|
|
320
|
+
setAutoPilot((prev) => !prev);
|
|
321
|
+
break;
|
|
322
|
+
case '1':
|
|
323
|
+
applyPreset('calm');
|
|
324
|
+
break;
|
|
325
|
+
case '2':
|
|
326
|
+
applyPreset('party');
|
|
327
|
+
break;
|
|
328
|
+
case '3':
|
|
329
|
+
applyPreset('chaos');
|
|
330
|
+
break;
|
|
331
|
+
case '4':
|
|
332
|
+
applyPreset('minimal');
|
|
333
|
+
break;
|
|
334
|
+
case '?':
|
|
335
|
+
setShowShortcuts((prev) => !prev);
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
341
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
342
|
+
}, [isReady, birdCount, handleTriggerFormation, handleShareConfig, applyPreset]);
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
<div className="landing-page">
|
|
346
|
+
{/* Background: Exported HyperTools Experience */}
|
|
347
|
+
<div className="experience-background">
|
|
348
|
+
{/* @ts-expect-error - Custom element */}
|
|
349
|
+
<boids-flocking-project ref={experienceRef} />
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
{/* Foreground: Control Panel at bottom center */}
|
|
353
|
+
<div className="landing-content">
|
|
354
|
+
{!isReady && <p className="loading">Loading experience...</p>}
|
|
355
|
+
|
|
356
|
+
{/* Demo: Programmatic control via ExperienceController */}
|
|
357
|
+
<section className="control-panel">
|
|
358
|
+
<div className="panel-header">
|
|
359
|
+
<h2>Control the Background</h2>
|
|
360
|
+
<button className="help-button" onClick={() => setShowShortcuts(true)}>?</button>
|
|
361
|
+
</div>
|
|
362
|
+
<p className="sdk-info">Custom features built with @hypertools/sdk</p>
|
|
363
|
+
|
|
364
|
+
{/* === CUSTOM FEATURE: Preset buttons with selection indicator === */}
|
|
365
|
+
<div className="preset-group">
|
|
366
|
+
{(Object.keys(PRESETS) as PresetName[]).map((preset) => (
|
|
367
|
+
<button
|
|
368
|
+
key={preset}
|
|
369
|
+
className={`preset-button ${selectedPreset === preset ? 'selected' : ''}`}
|
|
370
|
+
onClick={() => applyPreset(preset)}
|
|
371
|
+
>
|
|
372
|
+
{preset}
|
|
373
|
+
</button>
|
|
374
|
+
))}
|
|
375
|
+
</div>
|
|
376
|
+
{selectedPreset === null && (
|
|
377
|
+
<p className="custom-config-hint">Custom configuration</p>
|
|
378
|
+
)}
|
|
379
|
+
|
|
380
|
+
<div className="control-group">
|
|
381
|
+
<label>
|
|
382
|
+
Bird Count: {birdCount}
|
|
383
|
+
<input
|
|
384
|
+
type="range"
|
|
385
|
+
min={10}
|
|
386
|
+
max={1000}
|
|
387
|
+
step={10}
|
|
388
|
+
value={birdCount}
|
|
389
|
+
onChange={(e) => handleBirdCountChange(Number(e.target.value))}
|
|
390
|
+
/>
|
|
391
|
+
</label>
|
|
392
|
+
</div>
|
|
393
|
+
|
|
394
|
+
<div className="control-group">
|
|
395
|
+
<label>
|
|
396
|
+
<input
|
|
397
|
+
type="checkbox"
|
|
398
|
+
checked={rainbowMode}
|
|
399
|
+
onChange={handleRainbowToggle}
|
|
400
|
+
/>
|
|
401
|
+
Rainbow Mode
|
|
402
|
+
</label>
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
<div className="control-group">
|
|
406
|
+
<label>
|
|
407
|
+
Display Text:
|
|
408
|
+
<input
|
|
409
|
+
type="text"
|
|
410
|
+
value={displayText}
|
|
411
|
+
onChange={(e) => handleTextChange(e.target.value)}
|
|
412
|
+
/>
|
|
413
|
+
</label>
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
{/* === CUSTOM FEATURES: Action buttons === */}
|
|
417
|
+
<div className="button-group">
|
|
418
|
+
<button className="action-button" onClick={handleTriggerFormation}>
|
|
419
|
+
Formation
|
|
420
|
+
</button>
|
|
421
|
+
<button
|
|
422
|
+
className={`action-button ${clickToForm ? 'active' : ''}`}
|
|
423
|
+
onClick={() => setClickToForm(!clickToForm)}
|
|
424
|
+
title="Click anywhere to trigger formation at that position"
|
|
425
|
+
>
|
|
426
|
+
{clickToForm ? 'Click: ON' : 'Click Mode'}
|
|
427
|
+
</button>
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
<div className="button-group">
|
|
431
|
+
<button
|
|
432
|
+
className={`action-button ${autoPilot ? 'active' : ''}`}
|
|
433
|
+
onClick={() => setAutoPilot(!autoPilot)}
|
|
434
|
+
>
|
|
435
|
+
{autoPilot ? 'Stop Auto' : 'Auto-pilot'}
|
|
436
|
+
</button>
|
|
437
|
+
<button className="action-button" onClick={applyTimeTheme}>
|
|
438
|
+
Time Theme
|
|
439
|
+
</button>
|
|
440
|
+
</div>
|
|
441
|
+
|
|
442
|
+
<div className="button-group">
|
|
443
|
+
<button className="action-button" onClick={handleShareConfig}>
|
|
444
|
+
Share Config
|
|
445
|
+
</button>
|
|
446
|
+
<button className="reset-button" onClick={handleResetToDefault}>
|
|
447
|
+
Reset
|
|
448
|
+
</button>
|
|
449
|
+
</div>
|
|
450
|
+
|
|
451
|
+
{/* === Idle mode indicator === */}
|
|
452
|
+
{idleMode && (
|
|
453
|
+
<p className="idle-indicator">Screensaver mode active</p>
|
|
454
|
+
)}
|
|
455
|
+
</section>
|
|
456
|
+
</div>
|
|
457
|
+
|
|
458
|
+
{/* === CUSTOM FEATURE: Keyboard shortcuts overlay === */}
|
|
459
|
+
{showShortcuts && (
|
|
460
|
+
<div className="shortcuts-overlay" onClick={() => setShowShortcuts(false)}>
|
|
461
|
+
<div className="shortcuts-panel" onClick={(e) => e.stopPropagation()}>
|
|
462
|
+
<h3>Keyboard Shortcuts</h3>
|
|
463
|
+
<p className="shortcuts-subtitle">Custom controls built on top of the SDK</p>
|
|
464
|
+
<ul>
|
|
465
|
+
<li><kbd>Space</kbd> Trigger formation</li>
|
|
466
|
+
<li><kbd>↑</kbd> / <kbd>↓</kbd> Adjust bird count</li>
|
|
467
|
+
<li><kbd>R</kbd> Toggle rainbow mode</li>
|
|
468
|
+
<li><kbd>S</kbd> Share config URL</li>
|
|
469
|
+
<li><kbd>A</kbd> Toggle auto-pilot</li>
|
|
470
|
+
<li><kbd>1-4</kbd> Apply presets</li>
|
|
471
|
+
<li><kbd>?</kbd> Show/hide this help</li>
|
|
472
|
+
</ul>
|
|
473
|
+
<button className="close-button" onClick={() => setShowShortcuts(false)}>
|
|
474
|
+
Close
|
|
475
|
+
</button>
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
)}
|
|
479
|
+
</div>
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export default App;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Type definitions for the exported web component
|
|
2
|
+
export interface BoidsFlockingProjectElement extends HTMLElement {
|
|
3
|
+
setParam(key: string, value: unknown): void;
|
|
4
|
+
setParams(params: Record<string, unknown>): void;
|
|
5
|
+
getParams(): Record<string, unknown>;
|
|
6
|
+
getParamDefs(): Record<string, { type: string; value: unknown; [key: string]: unknown }>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
declare global {
|
|
10
|
+
namespace JSX {
|
|
11
|
+
interface IntrinsicElements {
|
|
12
|
+
'boids-flocking-project': React.DetailedHTMLProps<
|
|
13
|
+
React.HTMLAttributes<BoidsFlockingProjectElement> & {
|
|
14
|
+
ref?: React.Ref<BoidsFlockingProjectElement>;
|
|
15
|
+
},
|
|
16
|
+
BoidsFlockingProjectElement
|
|
17
|
+
>;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface HTMLElementTagNameMap {
|
|
22
|
+
'boids-flocking-project': BoidsFlockingProjectElement;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"jsx": "react-jsx",
|
|
14
|
+
"strict": true,
|
|
15
|
+
"noUnusedLocals": true,
|
|
16
|
+
"noUnusedParameters": true,
|
|
17
|
+
"noFallthroughCasesInSwitch": true
|
|
18
|
+
},
|
|
19
|
+
"include": ["src"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
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>HyperTool SDK - Recording Example</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: system-ui, sans-serif;
|
|
11
|
+
background: #1a1a2e;
|
|
12
|
+
color: #fff;
|
|
13
|
+
min-height: 100vh;
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
align-items: center;
|
|
17
|
+
padding: 2rem;
|
|
18
|
+
}
|
|
19
|
+
h1 { margin-bottom: 0.5rem; }
|
|
20
|
+
.subtitle { color: #888; margin-bottom: 1.5rem; }
|
|
21
|
+
#canvas-container {
|
|
22
|
+
width: 800px;
|
|
23
|
+
height: 600px;
|
|
24
|
+
border-radius: 8px;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
background: #000;
|
|
27
|
+
}
|
|
28
|
+
.controls {
|
|
29
|
+
margin-top: 1.5rem;
|
|
30
|
+
display: flex;
|
|
31
|
+
gap: 1rem;
|
|
32
|
+
flex-wrap: wrap;
|
|
33
|
+
justify-content: center;
|
|
34
|
+
}
|
|
35
|
+
button {
|
|
36
|
+
padding: 0.75rem 1.5rem;
|
|
37
|
+
border: none;
|
|
38
|
+
border-radius: 4px;
|
|
39
|
+
background: #4a4a6a;
|
|
40
|
+
color: #fff;
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
font-size: 1rem;
|
|
43
|
+
transition: background 0.2s;
|
|
44
|
+
}
|
|
45
|
+
button:hover { background: #5a5a7a; }
|
|
46
|
+
button:disabled {
|
|
47
|
+
opacity: 0.5;
|
|
48
|
+
cursor: not-allowed;
|
|
49
|
+
}
|
|
50
|
+
button.recording {
|
|
51
|
+
background: #ff4444;
|
|
52
|
+
animation: pulse 1s infinite;
|
|
53
|
+
}
|
|
54
|
+
button.primary { background: #00ff88; color: #000; }
|
|
55
|
+
@keyframes pulse {
|
|
56
|
+
0%, 100% { opacity: 1; }
|
|
57
|
+
50% { opacity: 0.7; }
|
|
58
|
+
}
|
|
59
|
+
.status {
|
|
60
|
+
margin-top: 1rem;
|
|
61
|
+
padding: 1rem;
|
|
62
|
+
background: #2a2a4a;
|
|
63
|
+
border-radius: 8px;
|
|
64
|
+
font-family: monospace;
|
|
65
|
+
font-size: 0.9rem;
|
|
66
|
+
min-width: 300px;
|
|
67
|
+
}
|
|
68
|
+
.timeline-section {
|
|
69
|
+
margin-top: 2rem;
|
|
70
|
+
padding: 1.5rem;
|
|
71
|
+
background: #2a2a4a;
|
|
72
|
+
border-radius: 8px;
|
|
73
|
+
width: 100%;
|
|
74
|
+
max-width: 800px;
|
|
75
|
+
}
|
|
76
|
+
.timeline-section h2 { margin-bottom: 1rem; font-size: 1.2rem; }
|
|
77
|
+
.timeline-controls { display: flex; gap: 1rem; flex-wrap: wrap; }
|
|
78
|
+
input[type="range"] { width: 200px; }
|
|
79
|
+
</style>
|
|
80
|
+
</head>
|
|
81
|
+
<body>
|
|
82
|
+
<h1>Recording & Timeline Example</h1>
|
|
83
|
+
<p class="subtitle">Video capture and keyframe animation</p>
|
|
84
|
+
|
|
85
|
+
<div id="canvas-container"></div>
|
|
86
|
+
|
|
87
|
+
<div class="controls">
|
|
88
|
+
<button id="capture-png">Capture PNG</button>
|
|
89
|
+
<button id="start-recording">Start Recording</button>
|
|
90
|
+
<button id="stop-recording" disabled>Stop Recording</button>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div class="status">
|
|
94
|
+
<div id="status-text">Ready</div>
|
|
95
|
+
<div id="frame-count">Frame: 0</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div class="timeline-section">
|
|
99
|
+
<h2>Timeline Animation</h2>
|
|
100
|
+
<div class="timeline-controls">
|
|
101
|
+
<button id="play-timeline" class="primary">Play Timeline</button>
|
|
102
|
+
<button id="pause-timeline">Pause</button>
|
|
103
|
+
<button id="reset-timeline">Reset</button>
|
|
104
|
+
<label>
|
|
105
|
+
Seek:
|
|
106
|
+
<input type="range" id="seek-timeline" min="0" max="5000" value="0">
|
|
107
|
+
</label>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<script type="module" src="./main.ts"></script>
|
|
112
|
+
</body>
|
|
113
|
+
</html>
|