@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,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recording Example
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates @hypertools/sdk recording capabilities:
|
|
5
|
+
* - Video recording with MediaRecorder
|
|
6
|
+
* - Image capture
|
|
7
|
+
* - Timeline-based keyframe animation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Experience } from '@hypertools/sdk';
|
|
11
|
+
import { VideoRecorder, ImageCapture, Timeline } from '@hypertools/sdk/recording';
|
|
12
|
+
|
|
13
|
+
// UI elements
|
|
14
|
+
const statusText = document.getElementById('status-text')!;
|
|
15
|
+
const frameCount = document.getElementById('frame-count')!;
|
|
16
|
+
const capturePngBtn = document.getElementById('capture-png') as HTMLButtonElement;
|
|
17
|
+
const startRecordingBtn = document.getElementById('start-recording') as HTMLButtonElement;
|
|
18
|
+
const stopRecordingBtn = document.getElementById('stop-recording') as HTMLButtonElement;
|
|
19
|
+
const playTimelineBtn = document.getElementById('play-timeline') as HTMLButtonElement;
|
|
20
|
+
const pauseTimelineBtn = document.getElementById('pause-timeline') as HTMLButtonElement;
|
|
21
|
+
const resetTimelineBtn = document.getElementById('reset-timeline') as HTMLButtonElement;
|
|
22
|
+
const seekSlider = document.getElementById('seek-timeline') as HTMLInputElement;
|
|
23
|
+
|
|
24
|
+
// Recording state
|
|
25
|
+
let recorder: ReturnType<typeof VideoRecorder.start> | null = null;
|
|
26
|
+
let canvas: HTMLCanvasElement | null = null;
|
|
27
|
+
|
|
28
|
+
// Create experience
|
|
29
|
+
const experience = new Experience({
|
|
30
|
+
mount: document.getElementById('canvas-container')!,
|
|
31
|
+
paramDefs: {
|
|
32
|
+
hue: { type: 'number', value: 0, min: 0, max: 360 },
|
|
33
|
+
saturation: { type: 'number', value: 80, min: 0, max: 100 },
|
|
34
|
+
scale: { type: 'number', value: 1, min: 0.5, max: 2 },
|
|
35
|
+
rotation: { type: 'number', value: 0, min: 0, max: 360 },
|
|
36
|
+
opacity: { type: 'number', value: 1, min: 0, max: 1 },
|
|
37
|
+
},
|
|
38
|
+
setup(context) {
|
|
39
|
+
canvas = document.createElement('canvas');
|
|
40
|
+
const ctx = canvas.getContext('2d')!;
|
|
41
|
+
canvas.width = 800;
|
|
42
|
+
canvas.height = 600;
|
|
43
|
+
context.mount.appendChild(canvas);
|
|
44
|
+
|
|
45
|
+
let time = 0;
|
|
46
|
+
|
|
47
|
+
function draw() {
|
|
48
|
+
const { hue, saturation, scale, rotation, opacity } = context.params as Record<string, number>;
|
|
49
|
+
|
|
50
|
+
// Clear
|
|
51
|
+
ctx.fillStyle = '#000';
|
|
52
|
+
ctx.fillRect(0, 0, canvas!.width, canvas!.height);
|
|
53
|
+
|
|
54
|
+
// Apply global opacity
|
|
55
|
+
ctx.globalAlpha = opacity;
|
|
56
|
+
|
|
57
|
+
// Center and transform
|
|
58
|
+
ctx.save();
|
|
59
|
+
ctx.translate(canvas!.width / 2, canvas!.height / 2);
|
|
60
|
+
ctx.rotate((rotation * Math.PI) / 180);
|
|
61
|
+
ctx.scale(scale, scale);
|
|
62
|
+
|
|
63
|
+
// Draw animated shapes
|
|
64
|
+
const color = `hsl(${hue}, ${saturation}%, 60%)`;
|
|
65
|
+
|
|
66
|
+
// Central shape
|
|
67
|
+
ctx.fillStyle = color;
|
|
68
|
+
ctx.beginPath();
|
|
69
|
+
const size = 100 + Math.sin(time * 2) * 30;
|
|
70
|
+
ctx.arc(0, 0, size, 0, Math.PI * 2);
|
|
71
|
+
ctx.fill();
|
|
72
|
+
|
|
73
|
+
// Orbiting shapes
|
|
74
|
+
const orbitCount = 6;
|
|
75
|
+
for (let i = 0; i < orbitCount; i++) {
|
|
76
|
+
const angle = (i / orbitCount) * Math.PI * 2 + time;
|
|
77
|
+
const orbitRadius = 180;
|
|
78
|
+
const x = Math.cos(angle) * orbitRadius;
|
|
79
|
+
const y = Math.sin(angle) * orbitRadius;
|
|
80
|
+
|
|
81
|
+
ctx.fillStyle = `hsl(${(hue + i * 30) % 360}, ${saturation}%, 50%)`;
|
|
82
|
+
ctx.beginPath();
|
|
83
|
+
ctx.arc(x, y, 30 + Math.sin(time * 3 + i) * 10, 0, Math.PI * 2);
|
|
84
|
+
ctx.fill();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
ctx.restore();
|
|
88
|
+
ctx.globalAlpha = 1;
|
|
89
|
+
|
|
90
|
+
time += 0.02;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
context.experience.on('frame', () => {
|
|
94
|
+
draw();
|
|
95
|
+
frameCount.textContent = `Frame: ${context.experience.currentFrame}`;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
draw();
|
|
99
|
+
|
|
100
|
+
return () => canvas?.remove();
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Timeline setup
|
|
105
|
+
const timeline = new Timeline({
|
|
106
|
+
duration: 5000,
|
|
107
|
+
loop: true,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Add keyframes for a complex animation sequence
|
|
111
|
+
timeline
|
|
112
|
+
// Start: warm colors, normal scale
|
|
113
|
+
.addKeyframe(0, {
|
|
114
|
+
hue: 0,
|
|
115
|
+
saturation: 80,
|
|
116
|
+
scale: 1,
|
|
117
|
+
rotation: 0,
|
|
118
|
+
opacity: 1,
|
|
119
|
+
})
|
|
120
|
+
// 1s: shift to cyan, scale up
|
|
121
|
+
.addKeyframe(1000, {
|
|
122
|
+
hue: 180,
|
|
123
|
+
saturation: 90,
|
|
124
|
+
scale: 1.3,
|
|
125
|
+
rotation: 45,
|
|
126
|
+
opacity: 1,
|
|
127
|
+
}, 'easeInOut')
|
|
128
|
+
// 2s: purple, rotate more
|
|
129
|
+
.addKeyframe(2000, {
|
|
130
|
+
hue: 280,
|
|
131
|
+
saturation: 70,
|
|
132
|
+
scale: 1,
|
|
133
|
+
rotation: 180,
|
|
134
|
+
opacity: 0.8,
|
|
135
|
+
}, 'easeInOut')
|
|
136
|
+
// 3s: green, scale down
|
|
137
|
+
.addKeyframe(3000, {
|
|
138
|
+
hue: 120,
|
|
139
|
+
saturation: 85,
|
|
140
|
+
scale: 0.7,
|
|
141
|
+
rotation: 270,
|
|
142
|
+
opacity: 1,
|
|
143
|
+
}, 'easeOutBack')
|
|
144
|
+
// 4s: back to red-ish
|
|
145
|
+
.addKeyframe(4000, {
|
|
146
|
+
hue: 30,
|
|
147
|
+
saturation: 90,
|
|
148
|
+
scale: 1.1,
|
|
149
|
+
rotation: 350,
|
|
150
|
+
opacity: 0.9,
|
|
151
|
+
}, 'easeInOut')
|
|
152
|
+
// 5s: return to start
|
|
153
|
+
.addKeyframe(5000, {
|
|
154
|
+
hue: 0,
|
|
155
|
+
saturation: 80,
|
|
156
|
+
scale: 1,
|
|
157
|
+
rotation: 360,
|
|
158
|
+
opacity: 1,
|
|
159
|
+
}, 'easeInOut');
|
|
160
|
+
|
|
161
|
+
// Timeline events
|
|
162
|
+
timeline.onUpdate((params) => {
|
|
163
|
+
experience.setParams(params);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
timeline.onComplete(() => {
|
|
167
|
+
statusText.textContent = 'Timeline complete (looping)';
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Capture PNG
|
|
171
|
+
capturePngBtn.addEventListener('click', async () => {
|
|
172
|
+
if (!canvas) return;
|
|
173
|
+
|
|
174
|
+
statusText.textContent = 'Capturing...';
|
|
175
|
+
const blob = await ImageCapture.capture(canvas, 'png');
|
|
176
|
+
|
|
177
|
+
if (blob) {
|
|
178
|
+
const url = URL.createObjectURL(blob);
|
|
179
|
+
const a = document.createElement('a');
|
|
180
|
+
a.href = url;
|
|
181
|
+
a.download = `capture-${Date.now()}.png`;
|
|
182
|
+
a.click();
|
|
183
|
+
URL.revokeObjectURL(url);
|
|
184
|
+
statusText.textContent = 'PNG captured!';
|
|
185
|
+
} else {
|
|
186
|
+
statusText.textContent = 'Capture failed';
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Video recording
|
|
191
|
+
startRecordingBtn.addEventListener('click', () => {
|
|
192
|
+
if (!canvas) return;
|
|
193
|
+
|
|
194
|
+
recorder = VideoRecorder.start(canvas, {
|
|
195
|
+
frameRate: 60,
|
|
196
|
+
videoBitsPerSecond: 5000000,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
startRecordingBtn.disabled = true;
|
|
200
|
+
startRecordingBtn.classList.add('recording');
|
|
201
|
+
stopRecordingBtn.disabled = false;
|
|
202
|
+
statusText.textContent = 'Recording...';
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
stopRecordingBtn.addEventListener('click', async () => {
|
|
206
|
+
if (!recorder) return;
|
|
207
|
+
|
|
208
|
+
statusText.textContent = 'Processing video...';
|
|
209
|
+
const blob = await recorder.stop();
|
|
210
|
+
|
|
211
|
+
if (blob) {
|
|
212
|
+
const url = URL.createObjectURL(blob);
|
|
213
|
+
const a = document.createElement('a');
|
|
214
|
+
a.href = url;
|
|
215
|
+
a.download = `recording-${Date.now()}.webm`;
|
|
216
|
+
a.click();
|
|
217
|
+
URL.revokeObjectURL(url);
|
|
218
|
+
statusText.textContent = 'Video saved!';
|
|
219
|
+
} else {
|
|
220
|
+
statusText.textContent = 'Recording failed';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
recorder = null;
|
|
224
|
+
startRecordingBtn.disabled = false;
|
|
225
|
+
startRecordingBtn.classList.remove('recording');
|
|
226
|
+
stopRecordingBtn.disabled = true;
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Timeline controls
|
|
230
|
+
playTimelineBtn.addEventListener('click', () => {
|
|
231
|
+
timeline.play();
|
|
232
|
+
statusText.textContent = 'Timeline playing...';
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
pauseTimelineBtn.addEventListener('click', () => {
|
|
236
|
+
timeline.pause();
|
|
237
|
+
statusText.textContent = 'Timeline paused';
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
resetTimelineBtn.addEventListener('click', () => {
|
|
241
|
+
timeline.seek(0);
|
|
242
|
+
timeline.pause();
|
|
243
|
+
seekSlider.value = '0';
|
|
244
|
+
statusText.textContent = 'Timeline reset';
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
seekSlider.addEventListener('input', () => {
|
|
248
|
+
timeline.seek(Number(seekSlider.value));
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Update seek slider during playback
|
|
252
|
+
setInterval(() => {
|
|
253
|
+
if (timeline.isPlaying) {
|
|
254
|
+
seekSlider.value = String(timeline.currentTime);
|
|
255
|
+
}
|
|
256
|
+
}, 50);
|
|
@@ -0,0 +1,40 @@
|
|
|
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 - Three.js Example</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: system-ui, sans-serif;
|
|
11
|
+
background: #000;
|
|
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: 1rem; }
|
|
20
|
+
#scene-container {
|
|
21
|
+
width: 800px;
|
|
22
|
+
height: 600px;
|
|
23
|
+
border-radius: 8px;
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
}
|
|
26
|
+
.info {
|
|
27
|
+
margin-top: 1rem;
|
|
28
|
+
font-size: 0.9rem;
|
|
29
|
+
color: #888;
|
|
30
|
+
}
|
|
31
|
+
</style>
|
|
32
|
+
</head>
|
|
33
|
+
<body>
|
|
34
|
+
<h1>Three.js Animated Scene</h1>
|
|
35
|
+
<div id="scene-container"></div>
|
|
36
|
+
<p class="info">Reactive 3D scene with configurable parameters</p>
|
|
37
|
+
|
|
38
|
+
<script type="module" src="./main.ts"></script>
|
|
39
|
+
</body>
|
|
40
|
+
</html>
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Three.js Example
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates @hypertools/sdk integration with Three.js.
|
|
5
|
+
* Creates an animated 3D scene with reactive parameters.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Experience } from '@hypertools/sdk';
|
|
9
|
+
import * as THREE from 'three';
|
|
10
|
+
|
|
11
|
+
new Experience({
|
|
12
|
+
mount: document.getElementById('scene-container')!,
|
|
13
|
+
paramDefs: {
|
|
14
|
+
cubeColor: {
|
|
15
|
+
type: 'color',
|
|
16
|
+
value: '#00ff88',
|
|
17
|
+
label: 'Cube Color',
|
|
18
|
+
},
|
|
19
|
+
torusColor: {
|
|
20
|
+
type: 'color',
|
|
21
|
+
value: '#ff6b6b',
|
|
22
|
+
label: 'Torus Color',
|
|
23
|
+
},
|
|
24
|
+
rotationSpeed: {
|
|
25
|
+
type: 'number',
|
|
26
|
+
value: 0.01,
|
|
27
|
+
min: 0,
|
|
28
|
+
max: 0.1,
|
|
29
|
+
step: 0.005,
|
|
30
|
+
label: 'Rotation Speed',
|
|
31
|
+
},
|
|
32
|
+
wireframe: {
|
|
33
|
+
type: 'boolean',
|
|
34
|
+
value: false,
|
|
35
|
+
label: 'Wireframe',
|
|
36
|
+
},
|
|
37
|
+
particleCount: {
|
|
38
|
+
type: 'number',
|
|
39
|
+
value: 1000,
|
|
40
|
+
min: 100,
|
|
41
|
+
max: 5000,
|
|
42
|
+
step: 100,
|
|
43
|
+
label: 'Particle Count',
|
|
44
|
+
},
|
|
45
|
+
cameraDistance: {
|
|
46
|
+
type: 'number',
|
|
47
|
+
value: 5,
|
|
48
|
+
min: 2,
|
|
49
|
+
max: 10,
|
|
50
|
+
step: 0.5,
|
|
51
|
+
label: 'Camera Distance',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
setup(context) {
|
|
55
|
+
// Scene setup
|
|
56
|
+
const scene = new THREE.Scene();
|
|
57
|
+
const camera = new THREE.PerspectiveCamera(75, 800 / 600, 0.1, 1000);
|
|
58
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
59
|
+
|
|
60
|
+
renderer.setSize(800, 600);
|
|
61
|
+
renderer.setPixelRatio(window.devicePixelRatio);
|
|
62
|
+
context.mount.appendChild(renderer.domElement);
|
|
63
|
+
|
|
64
|
+
// Lighting
|
|
65
|
+
const ambientLight = new THREE.AmbientLight(0x404040);
|
|
66
|
+
scene.add(ambientLight);
|
|
67
|
+
|
|
68
|
+
const pointLight = new THREE.PointLight(0xffffff, 1, 100);
|
|
69
|
+
pointLight.position.set(5, 5, 5);
|
|
70
|
+
scene.add(pointLight);
|
|
71
|
+
|
|
72
|
+
// Central cube
|
|
73
|
+
const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
|
|
74
|
+
const cubeMaterial = new THREE.MeshStandardMaterial({
|
|
75
|
+
color: context.params.cubeColor as string,
|
|
76
|
+
wireframe: context.params.wireframe as boolean,
|
|
77
|
+
});
|
|
78
|
+
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
|
|
79
|
+
scene.add(cube);
|
|
80
|
+
|
|
81
|
+
// Orbiting torus
|
|
82
|
+
const torusGeometry = new THREE.TorusGeometry(0.5, 0.2, 16, 100);
|
|
83
|
+
const torusMaterial = new THREE.MeshStandardMaterial({
|
|
84
|
+
color: context.params.torusColor as string,
|
|
85
|
+
wireframe: context.params.wireframe as boolean,
|
|
86
|
+
});
|
|
87
|
+
const torus = new THREE.Mesh(torusGeometry, torusMaterial);
|
|
88
|
+
scene.add(torus);
|
|
89
|
+
|
|
90
|
+
// Particle system
|
|
91
|
+
let particles: THREE.Points;
|
|
92
|
+
let particleGeometry: THREE.BufferGeometry;
|
|
93
|
+
|
|
94
|
+
function createParticles(count: number) {
|
|
95
|
+
if (particles) {
|
|
96
|
+
scene.remove(particles);
|
|
97
|
+
particleGeometry.dispose();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
particleGeometry = new THREE.BufferGeometry();
|
|
101
|
+
const positions = new Float32Array(count * 3);
|
|
102
|
+
|
|
103
|
+
for (let i = 0; i < count * 3; i += 3) {
|
|
104
|
+
positions[i] = (Math.random() - 0.5) * 10;
|
|
105
|
+
positions[i + 1] = (Math.random() - 0.5) * 10;
|
|
106
|
+
positions[i + 2] = (Math.random() - 0.5) * 10;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
particleGeometry.setAttribute(
|
|
110
|
+
'position',
|
|
111
|
+
new THREE.BufferAttribute(positions, 3)
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const particleMaterial = new THREE.PointsMaterial({
|
|
115
|
+
color: 0xffffff,
|
|
116
|
+
size: 0.02,
|
|
117
|
+
transparent: true,
|
|
118
|
+
opacity: 0.8,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
particles = new THREE.Points(particleGeometry, particleMaterial);
|
|
122
|
+
scene.add(particles);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
createParticles(context.params.particleCount as number);
|
|
126
|
+
|
|
127
|
+
// Camera position
|
|
128
|
+
camera.position.z = context.params.cameraDistance as number;
|
|
129
|
+
|
|
130
|
+
// Register objects for external access
|
|
131
|
+
context.registerObject('cube', cube, { type: 'mesh', name: 'Central Cube' });
|
|
132
|
+
context.registerObject('torus', torus, { type: 'mesh', name: 'Orbiting Torus' });
|
|
133
|
+
context.registerObject('scene', scene, { type: 'scene' });
|
|
134
|
+
context.registerObject('camera', camera, { type: 'camera' });
|
|
135
|
+
|
|
136
|
+
// Animation
|
|
137
|
+
let time = 0;
|
|
138
|
+
let lastParticleCount = context.params.particleCount as number;
|
|
139
|
+
|
|
140
|
+
context.experience.on('frame', () => {
|
|
141
|
+
const speed = context.params.rotationSpeed as number;
|
|
142
|
+
|
|
143
|
+
// Update cube
|
|
144
|
+
cube.rotation.x += speed;
|
|
145
|
+
cube.rotation.y += speed * 0.7;
|
|
146
|
+
cubeMaterial.color.set(context.params.cubeColor as string);
|
|
147
|
+
cubeMaterial.wireframe = context.params.wireframe as boolean;
|
|
148
|
+
|
|
149
|
+
// Update torus orbit
|
|
150
|
+
time += speed;
|
|
151
|
+
torus.position.x = Math.cos(time) * 2;
|
|
152
|
+
torus.position.z = Math.sin(time) * 2;
|
|
153
|
+
torus.rotation.x += speed * 1.5;
|
|
154
|
+
torus.rotation.y += speed;
|
|
155
|
+
torusMaterial.color.set(context.params.torusColor as string);
|
|
156
|
+
torusMaterial.wireframe = context.params.wireframe as boolean;
|
|
157
|
+
|
|
158
|
+
// Update particles
|
|
159
|
+
const particleCount = context.params.particleCount as number;
|
|
160
|
+
if (particleCount !== lastParticleCount) {
|
|
161
|
+
createParticles(particleCount);
|
|
162
|
+
lastParticleCount = particleCount;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (particles) {
|
|
166
|
+
particles.rotation.y += speed * 0.1;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Update camera
|
|
170
|
+
camera.position.z = context.params.cameraDistance as number;
|
|
171
|
+
|
|
172
|
+
// Render
|
|
173
|
+
renderer.render(scene, camera);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Initial render
|
|
177
|
+
renderer.render(scene, camera);
|
|
178
|
+
|
|
179
|
+
// Handle resize
|
|
180
|
+
context.environment.onResize((width, height) => {
|
|
181
|
+
camera.aspect = width / height;
|
|
182
|
+
camera.updateProjectionMatrix();
|
|
183
|
+
renderer.setSize(width, height);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Cleanup
|
|
187
|
+
return () => {
|
|
188
|
+
renderer.dispose();
|
|
189
|
+
cubeGeometry.dispose();
|
|
190
|
+
cubeMaterial.dispose();
|
|
191
|
+
torusGeometry.dispose();
|
|
192
|
+
torusMaterial.dispose();
|
|
193
|
+
particleGeometry?.dispose();
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
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 - Vanilla Canvas 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: 1rem; }
|
|
20
|
+
#canvas-container {
|
|
21
|
+
width: 800px;
|
|
22
|
+
height: 600px;
|
|
23
|
+
border-radius: 8px;
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
}
|
|
26
|
+
.controls {
|
|
27
|
+
margin-top: 1rem;
|
|
28
|
+
display: flex;
|
|
29
|
+
gap: 1rem;
|
|
30
|
+
align-items: center;
|
|
31
|
+
}
|
|
32
|
+
button {
|
|
33
|
+
padding: 0.5rem 1rem;
|
|
34
|
+
border: none;
|
|
35
|
+
border-radius: 4px;
|
|
36
|
+
background: #4a4a6a;
|
|
37
|
+
color: #fff;
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
}
|
|
40
|
+
button:hover { background: #5a5a7a; }
|
|
41
|
+
input[type="color"] {
|
|
42
|
+
width: 50px;
|
|
43
|
+
height: 32px;
|
|
44
|
+
border: none;
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
}
|
|
47
|
+
input[type="range"] { width: 150px; }
|
|
48
|
+
label { font-size: 0.9rem; }
|
|
49
|
+
</style>
|
|
50
|
+
</head>
|
|
51
|
+
<body>
|
|
52
|
+
<h1>Vanilla Canvas Example</h1>
|
|
53
|
+
<div id="canvas-container"></div>
|
|
54
|
+
|
|
55
|
+
<div class="controls">
|
|
56
|
+
<button id="toggle">Pause</button>
|
|
57
|
+
<button id="capture">Capture PNG</button>
|
|
58
|
+
|
|
59
|
+
<label>
|
|
60
|
+
Color:
|
|
61
|
+
<input type="color" id="color" value="#00ff88">
|
|
62
|
+
</label>
|
|
63
|
+
|
|
64
|
+
<label>
|
|
65
|
+
Speed:
|
|
66
|
+
<input type="range" id="speed" min="1" max="20" value="5">
|
|
67
|
+
</label>
|
|
68
|
+
|
|
69
|
+
<label>
|
|
70
|
+
<input type="checkbox" id="showTrails" checked>
|
|
71
|
+
Show Trails
|
|
72
|
+
</label>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<script type="module" src="./main.ts"></script>
|
|
76
|
+
</body>
|
|
77
|
+
</html>
|