@vibes.diy/prompts 0.0.0-dev-fresh-data
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +232 -0
- package/README.md +29 -0
- package/catalog.d.ts +1 -0
- package/catalog.js +4 -0
- package/catalog.js.map +1 -0
- package/chat.d.ts +144 -0
- package/chat.js +2 -0
- package/chat.js.map +1 -0
- package/component-export-transforms.d.ts +12 -0
- package/component-export-transforms.js +32 -0
- package/component-export-transforms.js.map +1 -0
- package/component-transforms.d.ts +3 -0
- package/component-transforms.js +327 -0
- package/component-transforms.js.map +1 -0
- package/index.d.ts +11 -0
- package/index.js +12 -0
- package/index.js.map +1 -0
- package/json-docs.d.ts +21 -0
- package/json-docs.js +25 -0
- package/json-docs.js.map +1 -0
- package/llms/callai.d.ts +2 -0
- package/llms/callai.js +10 -0
- package/llms/callai.js.map +1 -0
- package/llms/callai.txt +455 -0
- package/llms/d3.d.ts +2 -0
- package/llms/d3.js +10 -0
- package/llms/d3.js.map +1 -0
- package/llms/d3.md +679 -0
- package/llms/fireproof.d.ts +2 -0
- package/llms/fireproof.js +10 -0
- package/llms/fireproof.js.map +1 -0
- package/llms/fireproof.txt +451 -0
- package/llms/image-gen.d.ts +2 -0
- package/llms/image-gen.js +10 -0
- package/llms/image-gen.js.map +1 -0
- package/llms/image-gen.txt +128 -0
- package/llms/index.d.ts +8 -0
- package/llms/index.js +21 -0
- package/llms/index.js.map +1 -0
- package/llms/three-js.d.ts +2 -0
- package/llms/three-js.js +10 -0
- package/llms/three-js.js.map +1 -0
- package/llms/three-js.md +2232 -0
- package/llms/types.d.ts +10 -0
- package/llms/types.js +2 -0
- package/llms/types.js.map +1 -0
- package/llms/web-audio.d.ts +2 -0
- package/llms/web-audio.js +9 -0
- package/llms/web-audio.js.map +1 -0
- package/llms/web-audio.txt +220 -0
- package/load-docs.d.ts +2 -0
- package/load-docs.js +17 -0
- package/load-docs.js.map +1 -0
- package/package.json +39 -0
- package/prompts.d.ts +43 -0
- package/prompts.js +315 -0
- package/prompts.js.map +1 -0
- package/segment-parser.d.ts +4 -0
- package/segment-parser.js +135 -0
- package/segment-parser.js.map +1 -0
- package/settings.d.ts +16 -0
- package/settings.js +2 -0
- package/settings.js.map +1 -0
- package/style-prompts.d.ts +7 -0
- package/style-prompts.js +63 -0
- package/style-prompts.js.map +1 -0
- package/tsconfig.json +21 -0
- package/txt-docs.d.ts +15 -0
- package/txt-docs.js +53 -0
- package/txt-docs.js.map +1 -0
- package/view-state.d.ts +17 -0
- package/view-state.js +2 -0
- package/view-state.js.map +1 -0
package/llms/three-js.md
ADDED
|
@@ -0,0 +1,2232 @@
|
|
|
1
|
+
# Three.js API
|
|
2
|
+
|
|
3
|
+
_Essential classes, methods, and patterns for Three.js development_
|
|
4
|
+
|
|
5
|
+
## Core Setup
|
|
6
|
+
|
|
7
|
+
### Scene Graph Hierarchy
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
import * as THREE from "three";
|
|
11
|
+
|
|
12
|
+
// Core trinity
|
|
13
|
+
const scene = new THREE.Scene();
|
|
14
|
+
const camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000);
|
|
15
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
16
|
+
|
|
17
|
+
// Everything is an Object3D
|
|
18
|
+
scene.add(mesh); // Mesh extends Object3D
|
|
19
|
+
group.add(light); // Light extends Object3D
|
|
20
|
+
parent.add(child); // Hierarchical transforms
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Essential Classes
|
|
24
|
+
|
|
25
|
+
### Cameras
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
// Perspective (most common)
|
|
29
|
+
const camera = new THREE.PerspectiveCamera(
|
|
30
|
+
75, // field of view
|
|
31
|
+
aspect, // aspect ratio
|
|
32
|
+
0.1, // near plane
|
|
33
|
+
1000, // far plane
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// Orthographic (2D/technical)
|
|
37
|
+
const camera = new THREE.OrthographicCamera(
|
|
38
|
+
left,
|
|
39
|
+
right,
|
|
40
|
+
top,
|
|
41
|
+
bottom,
|
|
42
|
+
near,
|
|
43
|
+
far,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Camera controls
|
|
47
|
+
camera.position.set(x, y, z);
|
|
48
|
+
camera.lookAt(target);
|
|
49
|
+
camera.updateProjectionMatrix(); // After changing properties
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Geometries
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
// Primitive geometries
|
|
56
|
+
const box = new THREE.BoxGeometry(1, 1, 1);
|
|
57
|
+
const sphere = new THREE.SphereGeometry(1, 32, 32);
|
|
58
|
+
const plane = new THREE.PlaneGeometry(1, 1);
|
|
59
|
+
const cylinder = new THREE.CylinderGeometry(1, 1, 2, 32);
|
|
60
|
+
|
|
61
|
+
// Custom geometry
|
|
62
|
+
const geometry = new THREE.BufferGeometry();
|
|
63
|
+
geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
|
|
64
|
+
geometry.setAttribute("normal", new THREE.BufferAttribute(normals, 3));
|
|
65
|
+
geometry.setAttribute("uv", new THREE.BufferAttribute(uvs, 2));
|
|
66
|
+
geometry.setIndex(indices);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Materials
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
// Basic materials
|
|
73
|
+
const basic = new THREE.MeshBasicMaterial({ color: 0xff0000 });
|
|
74
|
+
const lambert = new THREE.MeshLambertMaterial({ color: 0x00ff00 });
|
|
75
|
+
const phong = new THREE.MeshPhongMaterial({ color: 0x0000ff });
|
|
76
|
+
|
|
77
|
+
// PBR materials (most realistic)
|
|
78
|
+
const standard = new THREE.MeshStandardMaterial({
|
|
79
|
+
color: 0xffffff,
|
|
80
|
+
metalness: 0.5,
|
|
81
|
+
roughness: 0.5,
|
|
82
|
+
map: texture,
|
|
83
|
+
normalMap: normalTexture,
|
|
84
|
+
envMap: environmentTexture,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const physical = new THREE.MeshPhysicalMaterial({
|
|
88
|
+
...standard,
|
|
89
|
+
clearcoat: 1.0,
|
|
90
|
+
transmission: 0.5,
|
|
91
|
+
thickness: 1.0,
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Lights
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
// Ambient (global illumination)
|
|
99
|
+
const ambient = new THREE.AmbientLight(0xffffff, 0.6);
|
|
100
|
+
|
|
101
|
+
// Directional (sun-like)
|
|
102
|
+
const directional = new THREE.DirectionalLight(0xffffff, 1);
|
|
103
|
+
directional.position.set(1, 1, 1);
|
|
104
|
+
directional.castShadow = true;
|
|
105
|
+
|
|
106
|
+
// Point (bulb-like)
|
|
107
|
+
const point = new THREE.PointLight(0xffffff, 1, 100);
|
|
108
|
+
point.position.set(0, 10, 0);
|
|
109
|
+
|
|
110
|
+
// Spot (flashlight-like)
|
|
111
|
+
const spot = new THREE.SpotLight(0xffffff, 1, 100, Math.PI / 4);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Textures
|
|
115
|
+
|
|
116
|
+
```javascript
|
|
117
|
+
// Texture loading
|
|
118
|
+
const loader = new THREE.TextureLoader();
|
|
119
|
+
const texture = loader.load("path/to/texture.jpg");
|
|
120
|
+
|
|
121
|
+
// Texture properties
|
|
122
|
+
texture.wrapS = THREE.RepeatWrapping;
|
|
123
|
+
texture.wrapT = THREE.RepeatWrapping;
|
|
124
|
+
texture.repeat.set(2, 2);
|
|
125
|
+
texture.flipY = false;
|
|
126
|
+
|
|
127
|
+
// HDR textures
|
|
128
|
+
const hdrLoader = new THREE.HDRLoader();
|
|
129
|
+
const envMap = hdrLoader.load("environment.hdr");
|
|
130
|
+
envMap.mapping = THREE.EquirectangularReflectionMapping;
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Object3D Fundamentals
|
|
134
|
+
|
|
135
|
+
### Transform Properties
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
// Position
|
|
139
|
+
object.position.set(x, y, z);
|
|
140
|
+
object.position.copy(otherObject.position);
|
|
141
|
+
object.translateX(distance);
|
|
142
|
+
|
|
143
|
+
// Rotation (Euler angles)
|
|
144
|
+
object.rotation.set(x, y, z);
|
|
145
|
+
object.rotation.y = Math.PI / 4;
|
|
146
|
+
object.rotateY(Math.PI / 4);
|
|
147
|
+
|
|
148
|
+
// Scale
|
|
149
|
+
object.scale.set(2, 2, 2);
|
|
150
|
+
object.scale.multiplyScalar(0.5);
|
|
151
|
+
|
|
152
|
+
// Quaternion (preferred for animations)
|
|
153
|
+
object.quaternion.setFromAxisAngle(axis, angle);
|
|
154
|
+
object.lookAt(target);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Hierarchy Operations
|
|
158
|
+
|
|
159
|
+
```javascript
|
|
160
|
+
// Adding/removing children
|
|
161
|
+
parent.add(child);
|
|
162
|
+
parent.remove(child);
|
|
163
|
+
scene.add(mesh, light, helper);
|
|
164
|
+
|
|
165
|
+
// Traversal
|
|
166
|
+
object.traverse((child) => {
|
|
167
|
+
if (child.isMesh) {
|
|
168
|
+
child.material.wireframe = true;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Finding objects
|
|
173
|
+
const found = scene.getObjectByName("myObject");
|
|
174
|
+
const found = scene.getObjectById(id);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Math Utilities
|
|
178
|
+
|
|
179
|
+
### Vectors
|
|
180
|
+
|
|
181
|
+
```javascript
|
|
182
|
+
// Vector3 (most common)
|
|
183
|
+
const v = new THREE.Vector3(1, 2, 3);
|
|
184
|
+
v.add(otherVector);
|
|
185
|
+
v.multiplyScalar(2);
|
|
186
|
+
v.normalize();
|
|
187
|
+
v.cross(otherVector);
|
|
188
|
+
v.dot(otherVector);
|
|
189
|
+
v.distanceTo(otherVector);
|
|
190
|
+
|
|
191
|
+
// Vector2 (UV coordinates)
|
|
192
|
+
const uv = new THREE.Vector2(0.5, 0.5);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Matrices
|
|
196
|
+
|
|
197
|
+
```javascript
|
|
198
|
+
// Matrix4 (transformations)
|
|
199
|
+
const matrix = new THREE.Matrix4();
|
|
200
|
+
matrix.makeTranslation(x, y, z);
|
|
201
|
+
matrix.makeRotationY(angle);
|
|
202
|
+
matrix.makeScale(x, y, z);
|
|
203
|
+
matrix.multiply(otherMatrix);
|
|
204
|
+
|
|
205
|
+
// Apply to object
|
|
206
|
+
object.applyMatrix4(matrix);
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Colors
|
|
210
|
+
|
|
211
|
+
```javascript
|
|
212
|
+
const color = new THREE.Color();
|
|
213
|
+
color.set(0xff0000); // hex
|
|
214
|
+
color.setRGB(1, 0, 0); // RGB values 0-1
|
|
215
|
+
color.setHSL(0, 1, 0.5); // HSL values
|
|
216
|
+
color.lerp(targetColor, 0.1); // interpolation
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Raycasting (Mouse Interaction)
|
|
220
|
+
|
|
221
|
+
```javascript
|
|
222
|
+
const raycaster = new THREE.Raycaster();
|
|
223
|
+
const mouse = new THREE.Vector2();
|
|
224
|
+
|
|
225
|
+
function onMouseClick(event) {
|
|
226
|
+
// Normalize mouse coordinates (-1 to +1)
|
|
227
|
+
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
|
228
|
+
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
|
229
|
+
|
|
230
|
+
// Cast ray from camera through mouse position
|
|
231
|
+
raycaster.setFromCamera(mouse, camera);
|
|
232
|
+
|
|
233
|
+
// Find intersections
|
|
234
|
+
const intersects = raycaster.intersectObjects(scene.children, true);
|
|
235
|
+
|
|
236
|
+
if (intersects.length > 0) {
|
|
237
|
+
const object = intersects[0].object;
|
|
238
|
+
const point = intersects[0].point;
|
|
239
|
+
// Handle intersection
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Animation System
|
|
245
|
+
|
|
246
|
+
### Animation Mixer
|
|
247
|
+
|
|
248
|
+
```javascript
|
|
249
|
+
// For GLTF animations
|
|
250
|
+
const mixer = new THREE.AnimationMixer(model);
|
|
251
|
+
const action = mixer.clipAction(animationClip);
|
|
252
|
+
action.play();
|
|
253
|
+
|
|
254
|
+
// Update in render loop
|
|
255
|
+
function animate() {
|
|
256
|
+
const delta = clock.getDelta();
|
|
257
|
+
mixer.update(delta);
|
|
258
|
+
renderer.render(scene, camera);
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Manual Animation
|
|
263
|
+
|
|
264
|
+
```javascript
|
|
265
|
+
const clock = new THREE.Clock();
|
|
266
|
+
|
|
267
|
+
function animate() {
|
|
268
|
+
const time = clock.getElapsedTime();
|
|
269
|
+
|
|
270
|
+
// Rotate object
|
|
271
|
+
mesh.rotation.y = time * 0.5;
|
|
272
|
+
|
|
273
|
+
// Oscillate position
|
|
274
|
+
mesh.position.y = Math.sin(time) * 2;
|
|
275
|
+
|
|
276
|
+
renderer.render(scene, camera);
|
|
277
|
+
}
|
|
278
|
+
renderer.setAnimationLoop(animate);
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Loading Assets
|
|
282
|
+
|
|
283
|
+
### GLTF Models (Recommended)
|
|
284
|
+
|
|
285
|
+
```javascript
|
|
286
|
+
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
|
287
|
+
|
|
288
|
+
const loader = new GLTFLoader();
|
|
289
|
+
loader.load("model.gltf", (gltf) => {
|
|
290
|
+
const model = gltf.scene;
|
|
291
|
+
scene.add(model);
|
|
292
|
+
|
|
293
|
+
// Access animations
|
|
294
|
+
if (gltf.animations.length > 0) {
|
|
295
|
+
const mixer = new THREE.AnimationMixer(model);
|
|
296
|
+
gltf.animations.forEach((clip) => {
|
|
297
|
+
mixer.clipAction(clip).play();
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Other Loaders
|
|
304
|
+
|
|
305
|
+
```javascript
|
|
306
|
+
// OBJ files
|
|
307
|
+
import { OBJLoader } from "three/addons/loaders/OBJLoader.js";
|
|
308
|
+
|
|
309
|
+
// FBX files
|
|
310
|
+
import { FBXLoader } from "three/addons/loaders/FBXLoader.js";
|
|
311
|
+
|
|
312
|
+
// Textures
|
|
313
|
+
const textureLoader = new THREE.TextureLoader();
|
|
314
|
+
const cubeLoader = new THREE.CubeTextureLoader();
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Renderer Configuration
|
|
318
|
+
|
|
319
|
+
### Basic Setup
|
|
320
|
+
|
|
321
|
+
```javascript
|
|
322
|
+
const renderer = new THREE.WebGLRenderer({
|
|
323
|
+
canvas: canvasElement, // Existing canvas
|
|
324
|
+
antialias: true, // Smooth edges
|
|
325
|
+
alpha: true, // Transparent background
|
|
326
|
+
powerPreference: "high-performance",
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
renderer.setSize(width, height);
|
|
330
|
+
renderer.setPixelRatio(window.devicePixelRatio);
|
|
331
|
+
renderer.setClearColor(0x000000, 1);
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Advanced Settings
|
|
335
|
+
|
|
336
|
+
```javascript
|
|
337
|
+
// Shadows
|
|
338
|
+
renderer.shadowMap.enabled = true;
|
|
339
|
+
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
340
|
+
|
|
341
|
+
// Tone mapping (HDR)
|
|
342
|
+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
343
|
+
renderer.toneMappingExposure = 1.0;
|
|
344
|
+
|
|
345
|
+
// Color space
|
|
346
|
+
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
347
|
+
|
|
348
|
+
// Performance
|
|
349
|
+
renderer.setAnimationLoop(animate); // Preferred over requestAnimationFrame
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Common Patterns
|
|
353
|
+
|
|
354
|
+
### Responsive Canvas
|
|
355
|
+
|
|
356
|
+
```javascript
|
|
357
|
+
function onWindowResize() {
|
|
358
|
+
camera.aspect = window.innerWidth / window.innerHeight;
|
|
359
|
+
camera.updateProjectionMatrix();
|
|
360
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
361
|
+
}
|
|
362
|
+
window.addEventListener("resize", onWindowResize);
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Performance Optimization
|
|
366
|
+
|
|
367
|
+
```javascript
|
|
368
|
+
// Frustum culling
|
|
369
|
+
object.frustumCulled = true;
|
|
370
|
+
|
|
371
|
+
// LOD (Level of Detail)
|
|
372
|
+
const lod = new THREE.LOD();
|
|
373
|
+
lod.addLevel(highDetailMesh, 0);
|
|
374
|
+
lod.addLevel(lowDetailMesh, 100);
|
|
375
|
+
|
|
376
|
+
// Instancing for many objects
|
|
377
|
+
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);
|
|
378
|
+
const matrix = new THREE.Matrix4();
|
|
379
|
+
for (let i = 0; i < count; i++) {
|
|
380
|
+
matrix.setPosition(x, y, z);
|
|
381
|
+
instancedMesh.setMatrixAt(i, matrix);
|
|
382
|
+
}
|
|
383
|
+
instancedMesh.instanceMatrix.needsUpdate = true;
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Dispose Pattern (Memory Management)
|
|
387
|
+
|
|
388
|
+
```javascript
|
|
389
|
+
// Clean up resources
|
|
390
|
+
geometry.dispose();
|
|
391
|
+
material.dispose();
|
|
392
|
+
texture.dispose();
|
|
393
|
+
renderer.dispose();
|
|
394
|
+
|
|
395
|
+
// Traverse and dispose
|
|
396
|
+
object.traverse((child) => {
|
|
397
|
+
if (child.geometry) child.geometry.dispose();
|
|
398
|
+
if (child.material) {
|
|
399
|
+
if (Array.isArray(child.material)) {
|
|
400
|
+
child.material.forEach((m) => m.dispose());
|
|
401
|
+
} else {
|
|
402
|
+
child.material.dispose();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## Buffer Attributes (Advanced)
|
|
409
|
+
|
|
410
|
+
### Custom Geometry Data
|
|
411
|
+
|
|
412
|
+
```javascript
|
|
413
|
+
const geometry = new THREE.BufferGeometry();
|
|
414
|
+
|
|
415
|
+
// Vertex positions (required)
|
|
416
|
+
const positions = new Float32Array([
|
|
417
|
+
-1,
|
|
418
|
+
-1,
|
|
419
|
+
0, // vertex 0
|
|
420
|
+
1,
|
|
421
|
+
-1,
|
|
422
|
+
0, // vertex 1
|
|
423
|
+
0,
|
|
424
|
+
1,
|
|
425
|
+
0, // vertex 2
|
|
426
|
+
]);
|
|
427
|
+
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
|
428
|
+
|
|
429
|
+
// Vertex colors
|
|
430
|
+
const colors = new Float32Array([
|
|
431
|
+
1,
|
|
432
|
+
0,
|
|
433
|
+
0, // red
|
|
434
|
+
0,
|
|
435
|
+
1,
|
|
436
|
+
0, // green
|
|
437
|
+
0,
|
|
438
|
+
0,
|
|
439
|
+
1, // blue
|
|
440
|
+
]);
|
|
441
|
+
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
|
|
442
|
+
|
|
443
|
+
// Custom attributes for shaders
|
|
444
|
+
const customData = new Float32Array(vertexCount);
|
|
445
|
+
geometry.setAttribute(
|
|
446
|
+
"customAttribute",
|
|
447
|
+
new THREE.BufferAttribute(customData, 1),
|
|
448
|
+
);
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
## Events and Interaction
|
|
452
|
+
|
|
453
|
+
### Event Dispatcher
|
|
454
|
+
|
|
455
|
+
```javascript
|
|
456
|
+
// Custom events
|
|
457
|
+
const emitter = new THREE.EventDispatcher();
|
|
458
|
+
|
|
459
|
+
emitter.addEventListener("customEvent", (event) => {
|
|
460
|
+
console.log("Event fired:", event.data);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
emitter.dispatchEvent({ type: "customEvent", data: "hello" });
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Built-in Events
|
|
467
|
+
|
|
468
|
+
```javascript
|
|
469
|
+
// Loading progress
|
|
470
|
+
loader.onProgress = (progress) => {
|
|
471
|
+
console.log(`Loading: ${(progress.loaded / progress.total) * 100}%`);
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
// Window resize
|
|
475
|
+
window.addEventListener("resize", onWindowResize);
|
|
476
|
+
|
|
477
|
+
// Mouse events
|
|
478
|
+
canvas.addEventListener("click", onMouseClick);
|
|
479
|
+
canvas.addEventListener("mousemove", onMouseMove);
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
## Constants Reference
|
|
483
|
+
|
|
484
|
+
### Material Constants
|
|
485
|
+
|
|
486
|
+
```javascript
|
|
487
|
+
// Blending modes
|
|
488
|
+
THREE.NormalBlending;
|
|
489
|
+
THREE.AdditiveBlending;
|
|
490
|
+
THREE.SubtractiveBlending;
|
|
491
|
+
THREE.MultiplyBlending;
|
|
492
|
+
|
|
493
|
+
// Culling
|
|
494
|
+
THREE.FrontSide;
|
|
495
|
+
THREE.BackSide;
|
|
496
|
+
THREE.DoubleSide;
|
|
497
|
+
|
|
498
|
+
// Depth modes
|
|
499
|
+
THREE.NeverDepth;
|
|
500
|
+
THREE.AlwaysDepth;
|
|
501
|
+
THREE.LessDepth;
|
|
502
|
+
THREE.LessEqualDepth;
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Texture Constants
|
|
506
|
+
|
|
507
|
+
```javascript
|
|
508
|
+
// Wrapping
|
|
509
|
+
THREE.RepeatWrapping;
|
|
510
|
+
THREE.ClampToEdgeWrapping;
|
|
511
|
+
THREE.MirroredRepeatWrapping;
|
|
512
|
+
|
|
513
|
+
// Filtering
|
|
514
|
+
THREE.NearestFilter;
|
|
515
|
+
THREE.LinearFilter;
|
|
516
|
+
THREE.NearestMipmapNearestFilter;
|
|
517
|
+
THREE.LinearMipmapLinearFilter;
|
|
518
|
+
|
|
519
|
+
// Formats
|
|
520
|
+
THREE.RGBAFormat;
|
|
521
|
+
THREE.RGBFormat;
|
|
522
|
+
THREE.RedFormat;
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Rendering Constants
|
|
526
|
+
|
|
527
|
+
```javascript
|
|
528
|
+
// Shadow types
|
|
529
|
+
THREE.BasicShadowMap;
|
|
530
|
+
THREE.PCFShadowMap;
|
|
531
|
+
THREE.PCFSoftShadowMap;
|
|
532
|
+
THREE.VSMShadowMap;
|
|
533
|
+
|
|
534
|
+
// Tone mapping
|
|
535
|
+
THREE.NoToneMapping;
|
|
536
|
+
THREE.LinearToneMapping;
|
|
537
|
+
THREE.ReinhardToneMapping;
|
|
538
|
+
THREE.CineonToneMapping;
|
|
539
|
+
THREE.ACESFilmicToneMapping;
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
## Common Gotchas
|
|
543
|
+
|
|
544
|
+
### Matrix Updates
|
|
545
|
+
|
|
546
|
+
```javascript
|
|
547
|
+
// Force matrix update after transform changes
|
|
548
|
+
object.updateMatrix();
|
|
549
|
+
object.updateMatrixWorld();
|
|
550
|
+
|
|
551
|
+
// Automatic updates (default: true)
|
|
552
|
+
object.matrixAutoUpdate = false; // Manual control
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Geometry Modifications
|
|
556
|
+
|
|
557
|
+
```javascript
|
|
558
|
+
// After modifying geometry attributes
|
|
559
|
+
geometry.attributes.position.needsUpdate = true;
|
|
560
|
+
geometry.computeBoundingSphere();
|
|
561
|
+
geometry.computeBoundingBox();
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
### Material Updates
|
|
565
|
+
|
|
566
|
+
```javascript
|
|
567
|
+
// After changing material properties
|
|
568
|
+
material.needsUpdate = true;
|
|
569
|
+
|
|
570
|
+
// Texture updates
|
|
571
|
+
texture.needsUpdate = true;
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
## Performance Tips
|
|
575
|
+
|
|
576
|
+
### Efficient Rendering
|
|
577
|
+
|
|
578
|
+
```javascript
|
|
579
|
+
// Batch similar objects
|
|
580
|
+
const geometry = new THREE.InstancedBufferGeometry();
|
|
581
|
+
const material = new THREE.MeshStandardMaterial();
|
|
582
|
+
const instancedMesh = new THREE.InstancedMesh(geometry, material, 1000);
|
|
583
|
+
|
|
584
|
+
// Freeze objects that don't move
|
|
585
|
+
object.matrixAutoUpdate = false;
|
|
586
|
+
object.updateMatrix();
|
|
587
|
+
|
|
588
|
+
// Use appropriate geometry detail
|
|
589
|
+
const sphere = new THREE.SphereGeometry(1, 8, 6); // Low poly
|
|
590
|
+
const sphere = new THREE.SphereGeometry(1, 32, 32); // High poly
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### Memory Management
|
|
594
|
+
|
|
595
|
+
```javascript
|
|
596
|
+
// Remove from scene
|
|
597
|
+
scene.remove(object);
|
|
598
|
+
|
|
599
|
+
// Dispose resources
|
|
600
|
+
object.traverse((child) => {
|
|
601
|
+
if (child.geometry) child.geometry.dispose();
|
|
602
|
+
if (child.material) child.material.dispose();
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// Clear references
|
|
606
|
+
object = null;
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
## Quick Reference
|
|
610
|
+
|
|
611
|
+
### Essential Imports
|
|
612
|
+
|
|
613
|
+
```javascript
|
|
614
|
+
// Core
|
|
615
|
+
import * as THREE from "three";
|
|
616
|
+
|
|
617
|
+
// Controls
|
|
618
|
+
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
|
619
|
+
import { FlyControls } from "three/addons/controls/FlyControls.js";
|
|
620
|
+
|
|
621
|
+
// Loaders
|
|
622
|
+
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
|
623
|
+
import { OBJLoader } from "three/addons/loaders/OBJLoader.js";
|
|
624
|
+
|
|
625
|
+
// Post-processing
|
|
626
|
+
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
|
|
627
|
+
|
|
628
|
+
// Helpers
|
|
629
|
+
import { GUI } from "three/addons/libs/lil-gui.module.min.js";
|
|
630
|
+
import Stats from "three/addons/libs/stats.module.js";
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### Minimal Working Example
|
|
634
|
+
|
|
635
|
+
```javascript
|
|
636
|
+
import * as THREE from "three";
|
|
637
|
+
|
|
638
|
+
const scene = new THREE.Scene();
|
|
639
|
+
const camera = new THREE.PerspectiveCamera(
|
|
640
|
+
75,
|
|
641
|
+
window.innerWidth / window.innerHeight,
|
|
642
|
+
);
|
|
643
|
+
const renderer = new THREE.WebGLRenderer();
|
|
644
|
+
|
|
645
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
646
|
+
document.body.appendChild(renderer.domElement);
|
|
647
|
+
|
|
648
|
+
const geometry = new THREE.BoxGeometry();
|
|
649
|
+
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
|
|
650
|
+
const cube = new THREE.Mesh(geometry, material);
|
|
651
|
+
scene.add(cube);
|
|
652
|
+
|
|
653
|
+
camera.position.z = 5;
|
|
654
|
+
|
|
655
|
+
function animate() {
|
|
656
|
+
cube.rotation.x += 0.01;
|
|
657
|
+
cube.rotation.y += 0.01;
|
|
658
|
+
renderer.render(scene, camera);
|
|
659
|
+
}
|
|
660
|
+
renderer.setAnimationLoop(animate);
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
# Three.js Condensed Guide: Most Impressive Examples
|
|
666
|
+
|
|
667
|
+
_A curated collection of Three.js's most visually stunning and technically advanced examples_
|
|
668
|
+
|
|
669
|
+
## Quick Start Template
|
|
670
|
+
|
|
671
|
+
```javascript
|
|
672
|
+
import * as THREE from "three";
|
|
673
|
+
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
|
674
|
+
|
|
675
|
+
// Basic setup
|
|
676
|
+
const scene = new THREE.Scene();
|
|
677
|
+
const camera = new THREE.PerspectiveCamera(
|
|
678
|
+
75,
|
|
679
|
+
window.innerWidth / window.innerHeight,
|
|
680
|
+
0.1,
|
|
681
|
+
1000,
|
|
682
|
+
);
|
|
683
|
+
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
684
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
685
|
+
document.body.appendChild(renderer.domElement);
|
|
686
|
+
|
|
687
|
+
// Controls
|
|
688
|
+
const controls = new OrbitControls(camera, renderer.domElement);
|
|
689
|
+
camera.position.set(5, 5, 5);
|
|
690
|
+
controls.update();
|
|
691
|
+
|
|
692
|
+
// Animation loop
|
|
693
|
+
function animate() {
|
|
694
|
+
controls.update();
|
|
695
|
+
renderer.render(scene, camera);
|
|
696
|
+
}
|
|
697
|
+
renderer.setAnimationLoop(animate);
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
## 1. Spectacular Visual Effects
|
|
701
|
+
|
|
702
|
+
### Galaxy Generator (WebGPU + TSL)
|
|
703
|
+
|
|
704
|
+
Creates a procedural spiral galaxy with thousands of animated particles.
|
|
705
|
+
|
|
706
|
+
```javascript
|
|
707
|
+
import * as THREE from "three/webgpu";
|
|
708
|
+
import { color, cos, sin, time, uniform, range, vec3, PI2 } from "three/tsl";
|
|
709
|
+
|
|
710
|
+
const material = new THREE.SpriteNodeMaterial({
|
|
711
|
+
depthWrite: false,
|
|
712
|
+
blending: THREE.AdditiveBlending,
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
// Procedural galaxy structure
|
|
716
|
+
const radiusRatio = range(0, 1);
|
|
717
|
+
const radius = radiusRatio.pow(1.5).mul(5);
|
|
718
|
+
const branches = 3;
|
|
719
|
+
const branchAngle = range(0, branches).floor().mul(PI2.div(branches));
|
|
720
|
+
const angle = branchAngle.add(time.mul(radiusRatio.oneMinus()));
|
|
721
|
+
|
|
722
|
+
const position = vec3(cos(angle), 0, sin(angle)).mul(radius);
|
|
723
|
+
material.positionNode = position.add(randomOffset);
|
|
724
|
+
|
|
725
|
+
// Dynamic colors
|
|
726
|
+
const colorInside = uniform(color("#ffa575"));
|
|
727
|
+
const colorOutside = uniform(color("#311599"));
|
|
728
|
+
material.colorNode = mix(colorInside, colorOutside, radiusRatio);
|
|
729
|
+
|
|
730
|
+
const galaxy = new THREE.InstancedMesh(
|
|
731
|
+
new THREE.PlaneGeometry(1, 1),
|
|
732
|
+
material,
|
|
733
|
+
20000,
|
|
734
|
+
);
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
### Ocean Shaders
|
|
738
|
+
|
|
739
|
+
Realistic water simulation with dynamic waves and sky reflections.
|
|
740
|
+
|
|
741
|
+
```javascript
|
|
742
|
+
import { Water } from "three/addons/objects/Water.js";
|
|
743
|
+
import { Sky } from "three/addons/objects/Sky.js";
|
|
744
|
+
|
|
745
|
+
const waterGeometry = new THREE.PlaneGeometry(10000, 10000);
|
|
746
|
+
const water = new Water(waterGeometry, {
|
|
747
|
+
textureWidth: 512,
|
|
748
|
+
textureHeight: 512,
|
|
749
|
+
waterNormals: new THREE.TextureLoader().load("textures/waternormals.jpg"),
|
|
750
|
+
sunDirection: new THREE.Vector3(),
|
|
751
|
+
sunColor: 0xffffff,
|
|
752
|
+
waterColor: 0x001e0f,
|
|
753
|
+
distortionScale: 3.7,
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
// Sky system
|
|
757
|
+
const sky = new Sky();
|
|
758
|
+
sky.scale.setScalar(10000);
|
|
759
|
+
const skyUniforms = sky.material.uniforms;
|
|
760
|
+
skyUniforms["turbidity"].value = 10;
|
|
761
|
+
skyUniforms["rayleigh"].value = 2;
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
### Unreal Bloom Effect
|
|
765
|
+
|
|
766
|
+
Cinematic glow and HDR post-processing.
|
|
767
|
+
|
|
768
|
+
```javascript
|
|
769
|
+
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
|
|
770
|
+
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
|
|
771
|
+
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
|
|
772
|
+
|
|
773
|
+
const composer = new EffectComposer(renderer);
|
|
774
|
+
const renderPass = new RenderPass(scene, camera);
|
|
775
|
+
composer.addPass(renderPass);
|
|
776
|
+
|
|
777
|
+
const bloomPass = new UnrealBloomPass(
|
|
778
|
+
new THREE.Vector2(window.innerWidth, window.innerHeight),
|
|
779
|
+
1.5, // strength
|
|
780
|
+
0.4, // radius
|
|
781
|
+
0.85, // threshold
|
|
782
|
+
);
|
|
783
|
+
composer.addPass(bloomPass);
|
|
784
|
+
|
|
785
|
+
// Render with bloom
|
|
786
|
+
composer.render();
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
## 2. Advanced GPU Computing
|
|
790
|
+
|
|
791
|
+
### Flocking Birds (GPGPU)
|
|
792
|
+
|
|
793
|
+
GPU-accelerated boid simulation with emergent flocking behavior.
|
|
794
|
+
|
|
795
|
+
```javascript
|
|
796
|
+
// Position computation shader
|
|
797
|
+
const fragmentShaderPosition = `
|
|
798
|
+
uniform float time;
|
|
799
|
+
uniform float delta;
|
|
800
|
+
|
|
801
|
+
void main() {
|
|
802
|
+
vec2 uv = gl_FragCoord.xy / resolution.xy;
|
|
803
|
+
vec4 tmpPos = texture2D(texturePosition, uv);
|
|
804
|
+
vec3 position = tmpPos.xyz;
|
|
805
|
+
vec3 velocity = texture2D(textureVelocity, uv).xyz;
|
|
806
|
+
|
|
807
|
+
gl_FragColor = vec4(position + velocity * delta * 15.0, tmpPos.w);
|
|
808
|
+
}`;
|
|
809
|
+
|
|
810
|
+
// Velocity computation (separation, alignment, cohesion)
|
|
811
|
+
const fragmentShaderVelocity = `
|
|
812
|
+
uniform float separationDistance;
|
|
813
|
+
uniform float alignmentDistance;
|
|
814
|
+
uniform float cohesionDistance;
|
|
815
|
+
uniform vec3 predator;
|
|
816
|
+
|
|
817
|
+
void main() {
|
|
818
|
+
// Boid algorithm implementation
|
|
819
|
+
// ...separation, alignment, cohesion logic
|
|
820
|
+
}`;
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
### Cloth Physics (WebGPU Compute)
|
|
824
|
+
|
|
825
|
+
Real-time fabric simulation using compute shaders.
|
|
826
|
+
|
|
827
|
+
```javascript
|
|
828
|
+
import { Fn, uniform, attribute, Loop } from "three/tsl";
|
|
829
|
+
|
|
830
|
+
// Verlet integration in compute shader
|
|
831
|
+
const computeVertexForces = Fn(() => {
|
|
832
|
+
const position = attribute("position");
|
|
833
|
+
const velocity = attribute("velocity");
|
|
834
|
+
|
|
835
|
+
// Spring forces, wind, gravity
|
|
836
|
+
const force = uniform("wind").add(uniform("gravity"));
|
|
837
|
+
|
|
838
|
+
// Verlet integration
|
|
839
|
+
const newPosition = position.add(velocity.mul(uniform("deltaTime")));
|
|
840
|
+
|
|
841
|
+
return newPosition;
|
|
842
|
+
})();
|
|
843
|
+
|
|
844
|
+
const clothMaterial = new THREE.MeshPhysicalMaterial({
|
|
845
|
+
color: 0x204080,
|
|
846
|
+
roughness: 0.8,
|
|
847
|
+
transmission: 0.2,
|
|
848
|
+
sheen: 0.5,
|
|
849
|
+
});
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
## 3. Impressive 3D Scenes
|
|
853
|
+
|
|
854
|
+
### Photorealistic Car
|
|
855
|
+
|
|
856
|
+
Advanced PBR materials with interactive customization.
|
|
857
|
+
|
|
858
|
+
```javascript
|
|
859
|
+
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
|
860
|
+
import { HDRLoader } from "three/addons/loaders/HDRLoader.js";
|
|
861
|
+
|
|
862
|
+
// Environment setup
|
|
863
|
+
scene.environment = new HDRLoader().load(
|
|
864
|
+
"textures/equirectangular/venice_sunset_1k.hdr",
|
|
865
|
+
);
|
|
866
|
+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
867
|
+
renderer.toneMappingExposure = 0.85;
|
|
868
|
+
|
|
869
|
+
// Load car model
|
|
870
|
+
const loader = new GLTFLoader();
|
|
871
|
+
const gltf = await loader.loadAsync("models/gltf/ferrari.glb");
|
|
872
|
+
|
|
873
|
+
// Material customization
|
|
874
|
+
gltf.scene.traverse((child) => {
|
|
875
|
+
if (child.isMesh && child.material.name === "body") {
|
|
876
|
+
child.material.color.setHex(bodyColor);
|
|
877
|
+
child.material.metalness = 1.0;
|
|
878
|
+
child.material.roughness = 0.5;
|
|
879
|
+
child.material.clearcoat = 1.0;
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
### Minecraft World Generator
|
|
885
|
+
|
|
886
|
+
Procedural voxel terrain with optimized geometry merging.
|
|
887
|
+
|
|
888
|
+
```javascript
|
|
889
|
+
import { ImprovedNoise } from "three/addons/math/ImprovedNoise.js";
|
|
890
|
+
import * as BufferGeometryUtils from "three/addons/utils/BufferGeometryUtils.js";
|
|
891
|
+
|
|
892
|
+
function generateTerrain(width, depth) {
|
|
893
|
+
const noise = new ImprovedNoise();
|
|
894
|
+
const data = [];
|
|
895
|
+
|
|
896
|
+
for (let x = 0; x < width; x++) {
|
|
897
|
+
for (let z = 0; z < depth; z++) {
|
|
898
|
+
// Multi-octave noise
|
|
899
|
+
const height =
|
|
900
|
+
noise.noise(x / 100, z / 100, 0) * 50 +
|
|
901
|
+
noise.noise(x / 50, z / 50, 0) * 25;
|
|
902
|
+
data.push(Math.floor(height));
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
return data;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Merge geometries for performance
|
|
910
|
+
const geometries = [];
|
|
911
|
+
// ...create individual cube geometries
|
|
912
|
+
const mergedGeometry = BufferGeometryUtils.mergeGeometries(geometries);
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
## 4. Interactive Experiences
|
|
916
|
+
|
|
917
|
+
### VR Painting
|
|
918
|
+
|
|
919
|
+
Virtual reality 3D painting with hand tracking.
|
|
920
|
+
|
|
921
|
+
```javascript
|
|
922
|
+
// WebXR setup
|
|
923
|
+
renderer.xr.enabled = true;
|
|
924
|
+
document.body.appendChild(VRButton.createButton(renderer));
|
|
925
|
+
|
|
926
|
+
// Hand input
|
|
927
|
+
const controller1 = renderer.xr.getController(0);
|
|
928
|
+
const controller2 = renderer.xr.getController(1);
|
|
929
|
+
|
|
930
|
+
controller1.addEventListener("selectstart", onSelectStart);
|
|
931
|
+
controller1.addEventListener("selectend", onSelectEnd);
|
|
932
|
+
|
|
933
|
+
function onSelectStart(event) {
|
|
934
|
+
// Start painting stroke
|
|
935
|
+
const geometry = new THREE.BufferGeometry();
|
|
936
|
+
const material = new THREE.LineBasicMaterial({
|
|
937
|
+
color: currentColor,
|
|
938
|
+
linewidth: brushSize,
|
|
939
|
+
});
|
|
940
|
+
const line = new THREE.Line(geometry, material);
|
|
941
|
+
scene.add(line);
|
|
942
|
+
}
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
### Physics Vehicle Controller
|
|
946
|
+
|
|
947
|
+
Real-time vehicle physics with Rapier.js integration.
|
|
948
|
+
|
|
949
|
+
```javascript
|
|
950
|
+
import { World } from "@dimforge/rapier3d-compat";
|
|
951
|
+
|
|
952
|
+
// Physics world
|
|
953
|
+
const world = new World({ x: 0, y: -9.81, z: 0 });
|
|
954
|
+
|
|
955
|
+
// Vehicle setup
|
|
956
|
+
const vehicleDesc = world.createRigidBody({
|
|
957
|
+
type: "dynamic",
|
|
958
|
+
translation: { x: 0, y: 1, z: 0 },
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
// Wheel constraints
|
|
962
|
+
wheels.forEach((wheel, index) => {
|
|
963
|
+
const wheelJoint = world.createImpulseJoint(
|
|
964
|
+
vehicleDesc,
|
|
965
|
+
wheel.body,
|
|
966
|
+
wheelConstraints[index],
|
|
967
|
+
);
|
|
968
|
+
});
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
## 5. Cutting-Edge WebGPU Features
|
|
972
|
+
|
|
973
|
+
### Path Tracing
|
|
974
|
+
|
|
975
|
+
Realistic ray-traced lighting with global illumination.
|
|
976
|
+
|
|
977
|
+
```javascript
|
|
978
|
+
import { PathTracingRenderer } from "three/addons/renderers/PathTracingRenderer.js";
|
|
979
|
+
|
|
980
|
+
const ptRenderer = new PathTracingRenderer(renderer);
|
|
981
|
+
ptRenderer.setSize(window.innerWidth, window.innerHeight);
|
|
982
|
+
|
|
983
|
+
// Progressive rendering
|
|
984
|
+
let sampleCount = 0;
|
|
985
|
+
function animate() {
|
|
986
|
+
if (sampleCount < 1000) {
|
|
987
|
+
ptRenderer.update();
|
|
988
|
+
sampleCount++;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
### TSL (Three.js Shading Language)
|
|
994
|
+
|
|
995
|
+
Modern node-based shader programming.
|
|
996
|
+
|
|
997
|
+
```javascript
|
|
998
|
+
import { mix, noise, time, uv, vec3, sin, cos } from "three/tsl";
|
|
999
|
+
|
|
1000
|
+
// Procedural materials with TSL
|
|
1001
|
+
const proceduralMaterial = new THREE.MeshStandardNodeMaterial();
|
|
1002
|
+
|
|
1003
|
+
// Animated noise texture
|
|
1004
|
+
const noiseValue = noise(uv().mul(10).add(time.mul(0.1)));
|
|
1005
|
+
const colorA = vec3(1, 0.5, 0.2);
|
|
1006
|
+
const colorB = vec3(0.2, 0.5, 1);
|
|
1007
|
+
|
|
1008
|
+
proceduralMaterial.colorNode = mix(colorA, colorB, noiseValue);
|
|
1009
|
+
proceduralMaterial.roughnessNode = noiseValue.mul(0.5).add(0.3);
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
## Performance Tips for Impressive Results
|
|
1013
|
+
|
|
1014
|
+
### Instancing for Massive Scenes
|
|
1015
|
+
|
|
1016
|
+
```javascript
|
|
1017
|
+
const instancedMesh = new THREE.InstancedMesh(geometry, material, 100000);
|
|
1018
|
+
const matrix = new THREE.Matrix4();
|
|
1019
|
+
|
|
1020
|
+
for (let i = 0; i < instancedMesh.count; i++) {
|
|
1021
|
+
matrix.setPosition(
|
|
1022
|
+
Math.random() * 2000 - 1000,
|
|
1023
|
+
Math.random() * 2000 - 1000,
|
|
1024
|
+
Math.random() * 2000 - 1000,
|
|
1025
|
+
);
|
|
1026
|
+
instancedMesh.setMatrixAt(i, matrix);
|
|
1027
|
+
}
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
### LOD for Complex Models
|
|
1031
|
+
|
|
1032
|
+
```javascript
|
|
1033
|
+
const lod = new THREE.LOD();
|
|
1034
|
+
lod.addLevel(highDetailMesh, 0);
|
|
1035
|
+
lod.addLevel(mediumDetailMesh, 50);
|
|
1036
|
+
lod.addLevel(lowDetailMesh, 200);
|
|
1037
|
+
```
|
|
1038
|
+
|
|
1039
|
+
### Render Targets for Effects
|
|
1040
|
+
|
|
1041
|
+
```javascript
|
|
1042
|
+
const renderTarget = new THREE.WebGLRenderTarget(1024, 1024);
|
|
1043
|
+
renderer.setRenderTarget(renderTarget);
|
|
1044
|
+
renderer.render(effectScene, effectCamera);
|
|
1045
|
+
renderer.setRenderTarget(null);
|
|
1046
|
+
|
|
1047
|
+
// Use render target as texture
|
|
1048
|
+
material.map = renderTarget.texture;
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
## Essential Setup for Maximum Impact
|
|
1052
|
+
|
|
1053
|
+
### HDR Environment
|
|
1054
|
+
|
|
1055
|
+
```javascript
|
|
1056
|
+
import { HDRLoader } from "three/addons/loaders/HDRLoader.js";
|
|
1057
|
+
|
|
1058
|
+
const hdrTexture = new HDRLoader().load("environment.hdr");
|
|
1059
|
+
hdrTexture.mapping = THREE.EquirectangularReflectionMapping;
|
|
1060
|
+
scene.environment = hdrTexture;
|
|
1061
|
+
scene.background = hdrTexture;
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
### Tone Mapping
|
|
1065
|
+
|
|
1066
|
+
```javascript
|
|
1067
|
+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
1068
|
+
renderer.toneMappingExposure = 1.0;
|
|
1069
|
+
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
### Post-Processing Chain
|
|
1073
|
+
|
|
1074
|
+
```javascript
|
|
1075
|
+
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
|
|
1076
|
+
|
|
1077
|
+
const composer = new EffectComposer(renderer);
|
|
1078
|
+
composer.addPass(new RenderPass(scene, camera));
|
|
1079
|
+
composer.addPass(new UnrealBloomPass(resolution, strength, radius, threshold));
|
|
1080
|
+
composer.addPass(new OutputPass());
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
---
|
|
1084
|
+
|
|
1085
|
+
_This guide focuses on Three.js's most impressive capabilities. Each example demonstrates advanced techniques that create visually stunning results with minimal code complexity._
|
|
1086
|
+
|
|
1087
|
+
# Real world example
|
|
1088
|
+
|
|
1089
|
+
```javascript
|
|
1090
|
+
import React, { useState, useEffect, useRef, useCallback } from "react";
|
|
1091
|
+
import { useFireproof } from "use-fireproof";
|
|
1092
|
+
import * as THREE from "three";
|
|
1093
|
+
|
|
1094
|
+
export default function SkyGlider() {
|
|
1095
|
+
const { database, useLiveQuery } = useFireproof("sky-glider-scores");
|
|
1096
|
+
const canvasRef = useRef(null);
|
|
1097
|
+
const gameStateRef = useRef({
|
|
1098
|
+
scene: null,
|
|
1099
|
+
camera: null,
|
|
1100
|
+
renderer: null,
|
|
1101
|
+
glider: null,
|
|
1102
|
+
clouds: [],
|
|
1103
|
+
coins: [],
|
|
1104
|
+
glowEffects: [],
|
|
1105
|
+
smokeTrail: [],
|
|
1106
|
+
lastSmokeTime: 0,
|
|
1107
|
+
score: 0,
|
|
1108
|
+
gameRunning: false,
|
|
1109
|
+
keys: {},
|
|
1110
|
+
velocity: { x: 0, y: 0, z: 0 },
|
|
1111
|
+
heading: 0,
|
|
1112
|
+
forwardSpeed: 0,
|
|
1113
|
+
pitch: 0,
|
|
1114
|
+
roll: 0,
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
const [currentScore, setCurrentScore] = useState(0);
|
|
1118
|
+
const { docs: scoreData } = useLiveQuery("type", { key: "score" }) || {
|
|
1119
|
+
docs: [],
|
|
1120
|
+
};
|
|
1121
|
+
|
|
1122
|
+
const saveScore = useCallback(
|
|
1123
|
+
async (score) => {
|
|
1124
|
+
await database.put({
|
|
1125
|
+
_id: `score-${Date.now()}`,
|
|
1126
|
+
type: "score",
|
|
1127
|
+
value: score,
|
|
1128
|
+
timestamp: Date.now(),
|
|
1129
|
+
});
|
|
1130
|
+
},
|
|
1131
|
+
[database],
|
|
1132
|
+
);
|
|
1133
|
+
|
|
1134
|
+
const createGlowEffect = useCallback((position) => {
|
|
1135
|
+
const state = gameStateRef.current;
|
|
1136
|
+
if (!state.scene) return;
|
|
1137
|
+
|
|
1138
|
+
const glowSphere = new THREE.Mesh(
|
|
1139
|
+
new THREE.SphereGeometry(8, 16, 16),
|
|
1140
|
+
new THREE.MeshBasicMaterial({
|
|
1141
|
+
color: 0xffd670,
|
|
1142
|
+
transparent: true,
|
|
1143
|
+
opacity: 0.8,
|
|
1144
|
+
}),
|
|
1145
|
+
);
|
|
1146
|
+
|
|
1147
|
+
glowSphere.position.copy(position);
|
|
1148
|
+
state.scene.add(glowSphere);
|
|
1149
|
+
|
|
1150
|
+
const glowEffect = {
|
|
1151
|
+
mesh: glowSphere,
|
|
1152
|
+
createdAt: Date.now(),
|
|
1153
|
+
scale: 1,
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
state.glowEffects.push(glowEffect);
|
|
1157
|
+
|
|
1158
|
+
// Remove after animation
|
|
1159
|
+
setTimeout(() => {
|
|
1160
|
+
state.scene.remove(glowSphere);
|
|
1161
|
+
const index = state.glowEffects.indexOf(glowEffect);
|
|
1162
|
+
if (index > -1) state.glowEffects.splice(index, 1);
|
|
1163
|
+
}, 1000);
|
|
1164
|
+
}, []);
|
|
1165
|
+
|
|
1166
|
+
const createSmokeCloud = useCallback((position) => {
|
|
1167
|
+
const state = gameStateRef.current;
|
|
1168
|
+
if (!state.scene) return;
|
|
1169
|
+
|
|
1170
|
+
const smokeGeometry = new THREE.SphereGeometry(
|
|
1171
|
+
0.1 + Math.random() * 0.05,
|
|
1172
|
+
4,
|
|
1173
|
+
3,
|
|
1174
|
+
);
|
|
1175
|
+
const smokeMaterial = new THREE.MeshLambertMaterial({
|
|
1176
|
+
color: 0x242424,
|
|
1177
|
+
transparent: true,
|
|
1178
|
+
opacity: 0.7 + Math.random() * 0.2,
|
|
1179
|
+
});
|
|
1180
|
+
const smokeCloud = new THREE.Mesh(smokeGeometry, smokeMaterial);
|
|
1181
|
+
|
|
1182
|
+
// Position behind the glider
|
|
1183
|
+
const heading = state.heading;
|
|
1184
|
+
const offsetX = Math.sin(heading) * -4;
|
|
1185
|
+
const offsetZ = Math.cos(heading) * -4;
|
|
1186
|
+
|
|
1187
|
+
smokeCloud.position.set(
|
|
1188
|
+
position.x + offsetX + (Math.random() - 0.5) * 0.2,
|
|
1189
|
+
position.y - 0.2 + (Math.random() - 0.5) * 0.1,
|
|
1190
|
+
position.z + offsetZ + Math.random() * 0.3,
|
|
1191
|
+
);
|
|
1192
|
+
|
|
1193
|
+
state.scene.add(smokeCloud);
|
|
1194
|
+
state.smokeTrail.push({
|
|
1195
|
+
mesh: smokeCloud,
|
|
1196
|
+
createdAt: Date.now(),
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
// Keep trail manageable
|
|
1200
|
+
while (state.smokeTrail.length > 100) {
|
|
1201
|
+
const oldSmoke = state.smokeTrail.shift();
|
|
1202
|
+
state.scene.remove(oldSmoke.mesh);
|
|
1203
|
+
}
|
|
1204
|
+
}, []);
|
|
1205
|
+
|
|
1206
|
+
const createTexturedCoin = useCallback((scene, position) => {
|
|
1207
|
+
// Create procedural gold texture
|
|
1208
|
+
const canvas = document.createElement("canvas");
|
|
1209
|
+
canvas.width = canvas.height = 128;
|
|
1210
|
+
const ctx = canvas.getContext("2d");
|
|
1211
|
+
|
|
1212
|
+
// Gold gradient
|
|
1213
|
+
const gradient = ctx.createRadialGradient(64, 64, 20, 64, 64, 64);
|
|
1214
|
+
gradient.addColorStop(0, "#ffd670");
|
|
1215
|
+
gradient.addColorStop(0.5, "#ff9770");
|
|
1216
|
+
gradient.addColorStop(1, "#ffb347");
|
|
1217
|
+
ctx.fillStyle = gradient;
|
|
1218
|
+
ctx.fillRect(0, 0, 128, 128);
|
|
1219
|
+
|
|
1220
|
+
// Add metallic shine lines
|
|
1221
|
+
ctx.strokeStyle = "#ffffff";
|
|
1222
|
+
ctx.lineWidth = 2;
|
|
1223
|
+
ctx.setLineDash([5, 3]);
|
|
1224
|
+
for (let i = 0; i < 8; i++) {
|
|
1225
|
+
const angle = (i / 8) * Math.PI * 2;
|
|
1226
|
+
ctx.beginPath();
|
|
1227
|
+
ctx.moveTo(64 + Math.cos(angle) * 30, 64 + Math.sin(angle) * 30);
|
|
1228
|
+
ctx.lineTo(64 + Math.cos(angle) * 50, 64 + Math.sin(angle) * 50);
|
|
1229
|
+
ctx.stroke();
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
1233
|
+
const coin = new THREE.Mesh(
|
|
1234
|
+
new THREE.CylinderGeometry(2, 2, 0.3, 16),
|
|
1235
|
+
new THREE.MeshStandardMaterial({
|
|
1236
|
+
map: texture,
|
|
1237
|
+
metalness: 0.8,
|
|
1238
|
+
roughness: 0.2,
|
|
1239
|
+
}),
|
|
1240
|
+
);
|
|
1241
|
+
|
|
1242
|
+
coin.position.copy(position);
|
|
1243
|
+
coin.rotation.z = Math.PI / 2;
|
|
1244
|
+
scene.add(coin);
|
|
1245
|
+
|
|
1246
|
+
return {
|
|
1247
|
+
mesh: coin,
|
|
1248
|
+
collected: false,
|
|
1249
|
+
rotation: Math.random() * 0.02 + 0.01,
|
|
1250
|
+
};
|
|
1251
|
+
}, []);
|
|
1252
|
+
|
|
1253
|
+
const initThreeJS = useCallback(() => {
|
|
1254
|
+
if (!canvasRef.current) return;
|
|
1255
|
+
|
|
1256
|
+
const scene = new THREE.Scene();
|
|
1257
|
+
scene.background = new THREE.Color(0x70d6ff);
|
|
1258
|
+
scene.fog = new THREE.Fog(0x70d6ff, 50, 300);
|
|
1259
|
+
|
|
1260
|
+
const camera = new THREE.PerspectiveCamera(
|
|
1261
|
+
75,
|
|
1262
|
+
window.innerWidth / window.innerHeight,
|
|
1263
|
+
0.1,
|
|
1264
|
+
1000,
|
|
1265
|
+
);
|
|
1266
|
+
camera.position.set(0, 10, 20);
|
|
1267
|
+
|
|
1268
|
+
const renderer = new THREE.WebGLRenderer({
|
|
1269
|
+
canvas: canvasRef.current,
|
|
1270
|
+
antialias: true,
|
|
1271
|
+
});
|
|
1272
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
1273
|
+
|
|
1274
|
+
// Lighting
|
|
1275
|
+
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
|
1276
|
+
scene.add(ambientLight);
|
|
1277
|
+
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
1278
|
+
directionalLight.position.set(50, 100, 50);
|
|
1279
|
+
scene.add(directionalLight);
|
|
1280
|
+
|
|
1281
|
+
// Glider
|
|
1282
|
+
const glider = new THREE.Group();
|
|
1283
|
+
const body = new THREE.Mesh(
|
|
1284
|
+
new THREE.ConeGeometry(2, 8, 3),
|
|
1285
|
+
new THREE.MeshLambertMaterial({ color: 0xff70a6 }),
|
|
1286
|
+
);
|
|
1287
|
+
body.rotation.x = Math.PI / 2;
|
|
1288
|
+
glider.add(body);
|
|
1289
|
+
|
|
1290
|
+
glider.position.set(0, 10, 0);
|
|
1291
|
+
scene.add(glider);
|
|
1292
|
+
|
|
1293
|
+
// Create simple clouds
|
|
1294
|
+
const clouds = [];
|
|
1295
|
+
for (let i = 0; i < 30; i++) {
|
|
1296
|
+
const cloud = new THREE.Mesh(
|
|
1297
|
+
new THREE.SphereGeometry(Math.random() * 5 + 3, 8, 6),
|
|
1298
|
+
new THREE.MeshLambertMaterial({
|
|
1299
|
+
color: 0xffffff,
|
|
1300
|
+
transparent: true,
|
|
1301
|
+
opacity: 0.7,
|
|
1302
|
+
}),
|
|
1303
|
+
);
|
|
1304
|
+
cloud.position.set(
|
|
1305
|
+
(Math.random() - 0.5) * 400,
|
|
1306
|
+
Math.random() * 30 + 10,
|
|
1307
|
+
(Math.random() - 0.5) * 400,
|
|
1308
|
+
);
|
|
1309
|
+
scene.add(cloud);
|
|
1310
|
+
clouds.push({
|
|
1311
|
+
mesh: cloud,
|
|
1312
|
+
drift: {
|
|
1313
|
+
x: (Math.random() - 0.5) * 0.01,
|
|
1314
|
+
y: 0,
|
|
1315
|
+
z: (Math.random() - 0.5) * 0.01,
|
|
1316
|
+
},
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// Create initial coins
|
|
1321
|
+
const coins = [];
|
|
1322
|
+
for (let i = 0; i < 20; i++) {
|
|
1323
|
+
const coin = createTexturedCoin(
|
|
1324
|
+
scene,
|
|
1325
|
+
new THREE.Vector3(
|
|
1326
|
+
(Math.random() - 0.5) * 200,
|
|
1327
|
+
Math.random() * 40 + 10,
|
|
1328
|
+
(Math.random() - 0.5) * 200,
|
|
1329
|
+
),
|
|
1330
|
+
);
|
|
1331
|
+
coins.push(coin);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
gameStateRef.current = {
|
|
1335
|
+
...gameStateRef.current,
|
|
1336
|
+
scene,
|
|
1337
|
+
camera,
|
|
1338
|
+
renderer,
|
|
1339
|
+
glider,
|
|
1340
|
+
clouds,
|
|
1341
|
+
coins,
|
|
1342
|
+
lastSmokeTime: Date.now(),
|
|
1343
|
+
heading: 0,
|
|
1344
|
+
forwardSpeed: 0.1,
|
|
1345
|
+
pitch: 0,
|
|
1346
|
+
roll: 0,
|
|
1347
|
+
};
|
|
1348
|
+
|
|
1349
|
+
const gameLoop = () => {
|
|
1350
|
+
if (gameStateRef.current.gameRunning) {
|
|
1351
|
+
updateGame();
|
|
1352
|
+
requestAnimationFrame(gameLoop);
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
gameStateRef.current.gameRunning = true;
|
|
1356
|
+
gameLoop();
|
|
1357
|
+
}, [createTexturedCoin]);
|
|
1358
|
+
|
|
1359
|
+
const checkCoinCollisions = useCallback(() => {
|
|
1360
|
+
const state = gameStateRef.current;
|
|
1361
|
+
if (!state.glider) return;
|
|
1362
|
+
|
|
1363
|
+
state.coins.forEach((coin) => {
|
|
1364
|
+
if (!coin.collected) {
|
|
1365
|
+
const distance = state.glider.position.distanceTo(coin.mesh.position);
|
|
1366
|
+
if (distance < 4) {
|
|
1367
|
+
coin.collected = true;
|
|
1368
|
+
coin.mesh.visible = false;
|
|
1369
|
+
createGlowEffect(coin.mesh.position);
|
|
1370
|
+
state.score += 1;
|
|
1371
|
+
setCurrentScore(state.score);
|
|
1372
|
+
|
|
1373
|
+
// Respawn coin at random location
|
|
1374
|
+
setTimeout(() => {
|
|
1375
|
+
coin.mesh.position.set(
|
|
1376
|
+
(Math.random() - 0.5) * 200,
|
|
1377
|
+
Math.random() * 40 + 10,
|
|
1378
|
+
(Math.random() - 0.5) * 200,
|
|
1379
|
+
);
|
|
1380
|
+
coin.mesh.visible = true;
|
|
1381
|
+
coin.collected = false;
|
|
1382
|
+
}, 5000);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
}, [createGlowEffect]);
|
|
1387
|
+
|
|
1388
|
+
const handleKeyDown = useCallback((event) => {
|
|
1389
|
+
if (event.code === "Space") event.preventDefault();
|
|
1390
|
+
gameStateRef.current.keys[event.code] = true;
|
|
1391
|
+
}, []);
|
|
1392
|
+
|
|
1393
|
+
const handleKeyUp = useCallback((event) => {
|
|
1394
|
+
if (event.code === "Space") event.preventDefault();
|
|
1395
|
+
gameStateRef.current.keys[event.code] = false;
|
|
1396
|
+
}, []);
|
|
1397
|
+
|
|
1398
|
+
const updateGame = useCallback(() => {
|
|
1399
|
+
const state = gameStateRef.current;
|
|
1400
|
+
if (!state.gameRunning || !state.glider) return;
|
|
1401
|
+
|
|
1402
|
+
const { keys, glider } = state;
|
|
1403
|
+
|
|
1404
|
+
// Controls
|
|
1405
|
+
if (keys["ArrowLeft"] || keys["KeyA"]) state.heading += 0.03;
|
|
1406
|
+
if (keys["ArrowRight"] || keys["KeyD"]) state.heading -= 0.03;
|
|
1407
|
+
if (keys["ArrowUp"] || keys["KeyW"]) state.pitch += 0.01;
|
|
1408
|
+
if (keys["ArrowDown"] || keys["KeyS"]) state.pitch -= 0.01;
|
|
1409
|
+
if (keys["Space"])
|
|
1410
|
+
state.forwardSpeed = Math.min(0.3, state.forwardSpeed + 0.005);
|
|
1411
|
+
|
|
1412
|
+
// Physics
|
|
1413
|
+
state.forwardSpeed = Math.max(0.05, state.forwardSpeed * 0.995);
|
|
1414
|
+
state.velocity.x =
|
|
1415
|
+
Math.sin(state.heading) * Math.cos(state.pitch) * state.forwardSpeed;
|
|
1416
|
+
state.velocity.y = Math.sin(-state.pitch) * state.forwardSpeed;
|
|
1417
|
+
state.velocity.z =
|
|
1418
|
+
Math.cos(state.heading) * Math.cos(state.pitch) * state.forwardSpeed;
|
|
1419
|
+
|
|
1420
|
+
glider.position.add(
|
|
1421
|
+
new THREE.Vector3(state.velocity.x, state.velocity.y, state.velocity.z),
|
|
1422
|
+
);
|
|
1423
|
+
|
|
1424
|
+
// Point glider in thrust vector direction
|
|
1425
|
+
const thrustDirection = new THREE.Vector3(
|
|
1426
|
+
state.velocity.x,
|
|
1427
|
+
state.velocity.y,
|
|
1428
|
+
state.velocity.z,
|
|
1429
|
+
).normalize();
|
|
1430
|
+
if (thrustDirection.length() > 0) {
|
|
1431
|
+
glider.lookAt(glider.position.clone().add(thrustDirection));
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// Camera follow
|
|
1435
|
+
const cameraDistance = 15;
|
|
1436
|
+
state.camera.position.set(
|
|
1437
|
+
glider.position.x - Math.sin(state.heading) * cameraDistance,
|
|
1438
|
+
glider.position.y + 10,
|
|
1439
|
+
glider.position.z - Math.cos(state.heading) * cameraDistance,
|
|
1440
|
+
);
|
|
1441
|
+
state.camera.lookAt(glider.position);
|
|
1442
|
+
|
|
1443
|
+
// Create smoke trail
|
|
1444
|
+
const currentTime = Date.now();
|
|
1445
|
+
const timeSinceLastSmoke = currentTime - state.lastSmokeTime;
|
|
1446
|
+
const smokeInterval = 150 + Math.random() * 200;
|
|
1447
|
+
|
|
1448
|
+
if (timeSinceLastSmoke > smokeInterval) {
|
|
1449
|
+
createSmokeCloud(glider.position);
|
|
1450
|
+
state.lastSmokeTime = currentTime;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// Animate
|
|
1454
|
+
checkCoinCollisions();
|
|
1455
|
+
state.coins.forEach((coin) => {
|
|
1456
|
+
if (!coin.collected) coin.mesh.rotation.y += coin.rotation;
|
|
1457
|
+
});
|
|
1458
|
+
state.clouds.forEach((cloud) => {
|
|
1459
|
+
cloud.mesh.position.add(
|
|
1460
|
+
new THREE.Vector3(cloud.drift.x, cloud.drift.y, cloud.drift.z),
|
|
1461
|
+
);
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
// Animate glow effects
|
|
1465
|
+
state.glowEffects.forEach((effect) => {
|
|
1466
|
+
const age = Date.now() - effect.createdAt;
|
|
1467
|
+
const progress = age / 1000;
|
|
1468
|
+
effect.scale = 1 + progress * 2;
|
|
1469
|
+
effect.mesh.scale.setScalar(effect.scale);
|
|
1470
|
+
effect.mesh.material.opacity = 0.8 * (1 - progress);
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
// Fade smoke trail
|
|
1474
|
+
state.smokeTrail.forEach((smoke) => {
|
|
1475
|
+
const age = currentTime - smoke.createdAt;
|
|
1476
|
+
const maxAge = 15000;
|
|
1477
|
+
if (age > maxAge) {
|
|
1478
|
+
smoke.mesh.material.opacity = 0;
|
|
1479
|
+
} else if (age > 7500) {
|
|
1480
|
+
const fadeProgress = (age - 7500) / 7500;
|
|
1481
|
+
smoke.mesh.material.opacity =
|
|
1482
|
+
(0.7 + Math.random() * 0.2) * (1 - fadeProgress);
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
state.renderer.render(state.scene, state.camera);
|
|
1487
|
+
}, [checkCoinCollisions, createSmokeCloud]);
|
|
1488
|
+
|
|
1489
|
+
useEffect(() => {
|
|
1490
|
+
initThreeJS();
|
|
1491
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
1492
|
+
window.addEventListener("keyup", handleKeyUp);
|
|
1493
|
+
return () => {
|
|
1494
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
1495
|
+
window.removeEventListener("keyup", handleKeyUp);
|
|
1496
|
+
gameStateRef.current.gameRunning = false;
|
|
1497
|
+
};
|
|
1498
|
+
}, [initThreeJS, handleKeyDown, handleKeyUp]);
|
|
1499
|
+
|
|
1500
|
+
return (
|
|
1501
|
+
<div className="relative h-screen w-full bg-sky-200">
|
|
1502
|
+
<canvas ref={canvasRef} className="absolute inset-0" />
|
|
1503
|
+
<div className="absolute top-4 left-4 rounded bg-white p-4 shadow">
|
|
1504
|
+
<h2 className="text-lg font-bold">Sky Glider</h2>
|
|
1505
|
+
<p>Score: {currentScore}</p>
|
|
1506
|
+
<p className="mt-2 text-sm">WASD/Arrows: Fly, Space: Thrust</p>
|
|
1507
|
+
</div>
|
|
1508
|
+
{scoreData.length > 0 && (
|
|
1509
|
+
<div className="absolute top-4 right-4 rounded bg-white p-4 shadow">
|
|
1510
|
+
<h3 className="font-bold">High Scores</h3>
|
|
1511
|
+
{scoreData
|
|
1512
|
+
.sort((a, b) => b.value - a.value)
|
|
1513
|
+
.slice(0, 3)
|
|
1514
|
+
.map((score, i) => (
|
|
1515
|
+
<div key={score._id} className="text-sm">
|
|
1516
|
+
#{i + 1}: {score.value}
|
|
1517
|
+
</div>
|
|
1518
|
+
))}
|
|
1519
|
+
</div>
|
|
1520
|
+
)}
|
|
1521
|
+
</div>
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
```
|
|
1525
|
+
|
|
1526
|
+
# Visual effects example
|
|
1527
|
+
|
|
1528
|
+
```javascript
|
|
1529
|
+
import React, { useState, useEffect, useRef, useCallback } from "react";
|
|
1530
|
+
import { useFireproof } from "use-fireproof";
|
|
1531
|
+
import * as THREE from "three";
|
|
1532
|
+
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
|
1533
|
+
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
|
|
1534
|
+
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
|
|
1535
|
+
import { HalftonePass } from "three/addons/postprocessing/HalftonePass.js";
|
|
1536
|
+
|
|
1537
|
+
export default function HalftoneArtStudio() {
|
|
1538
|
+
const { database, useLiveQuery } = useFireproof("halftone-studio");
|
|
1539
|
+
const canvasRef = useRef(null);
|
|
1540
|
+
const sceneRef = useRef(null);
|
|
1541
|
+
const [currentPreset, setCurrentPreset] = useState(null);
|
|
1542
|
+
const [presetName, setPresetName] = useState("");
|
|
1543
|
+
const [isGenerating, setIsGenerating] = useState(false);
|
|
1544
|
+
const [showParameters, setShowParameters] = useState(false);
|
|
1545
|
+
|
|
1546
|
+
const { docs: presets } = useLiveQuery("type", { key: "preset" }) || {
|
|
1547
|
+
docs: [],
|
|
1548
|
+
};
|
|
1549
|
+
const { docs: parameterHistory } = useLiveQuery("type", {
|
|
1550
|
+
key: "parameter-state",
|
|
1551
|
+
}) || {
|
|
1552
|
+
docs: [],
|
|
1553
|
+
};
|
|
1554
|
+
|
|
1555
|
+
const [parameters, setParameters] = useState({
|
|
1556
|
+
shape: 1, // 1=Dot, 2=Ellipse, 3=Line, 4=Square
|
|
1557
|
+
radius: 4,
|
|
1558
|
+
rotateR: 15,
|
|
1559
|
+
rotateG: 30,
|
|
1560
|
+
rotateB: 45,
|
|
1561
|
+
scatter: 0,
|
|
1562
|
+
blending: 1,
|
|
1563
|
+
blendingMode: 1, // 1=Linear, 2=Multiply, 3=Add, 4=Lighter, 5=Darker
|
|
1564
|
+
greyscale: false,
|
|
1565
|
+
disable: false,
|
|
1566
|
+
objectCount: 25,
|
|
1567
|
+
rotationSpeed: 1,
|
|
1568
|
+
colorTheme: 0, // 0=Rainbow, 1=Warm, 2=Cool, 3=Monochrome
|
|
1569
|
+
});
|
|
1570
|
+
|
|
1571
|
+
const saveParameterState = useCallback(
|
|
1572
|
+
async (params, action = "manual") => {
|
|
1573
|
+
await database.put({
|
|
1574
|
+
_id: `param-state-${Date.now()}`,
|
|
1575
|
+
type: "parameter-state",
|
|
1576
|
+
parameters: { ...params },
|
|
1577
|
+
action,
|
|
1578
|
+
timestamp: Date.now(),
|
|
1579
|
+
});
|
|
1580
|
+
},
|
|
1581
|
+
[database],
|
|
1582
|
+
);
|
|
1583
|
+
|
|
1584
|
+
const savePreset = useCallback(async () => {
|
|
1585
|
+
if (!presetName.trim()) return;
|
|
1586
|
+
|
|
1587
|
+
await database.put({
|
|
1588
|
+
_id: `preset-${Date.now()}`,
|
|
1589
|
+
type: "preset",
|
|
1590
|
+
name: presetName,
|
|
1591
|
+
parameters: { ...parameters },
|
|
1592
|
+
timestamp: Date.now(),
|
|
1593
|
+
});
|
|
1594
|
+
|
|
1595
|
+
setPresetName("");
|
|
1596
|
+
}, [database, presetName, parameters]);
|
|
1597
|
+
|
|
1598
|
+
const loadPreset = useCallback((preset) => {
|
|
1599
|
+
setParameters({ ...preset.parameters });
|
|
1600
|
+
setCurrentPreset(preset);
|
|
1601
|
+
}, []);
|
|
1602
|
+
|
|
1603
|
+
const loadParameterState = useCallback((state) => {
|
|
1604
|
+
setParameters({ ...state.parameters });
|
|
1605
|
+
}, []);
|
|
1606
|
+
|
|
1607
|
+
const generateRandomScene = useCallback(async () => {
|
|
1608
|
+
setIsGenerating(true);
|
|
1609
|
+
|
|
1610
|
+
// Save current state before randomizing
|
|
1611
|
+
await saveParameterState(parameters, "before-randomize");
|
|
1612
|
+
|
|
1613
|
+
// Generate random parameters
|
|
1614
|
+
const newParams = {
|
|
1615
|
+
shape: Math.floor(Math.random() * 4) + 1,
|
|
1616
|
+
radius: Math.random() * 20 + 2,
|
|
1617
|
+
rotateR: Math.random() * 90,
|
|
1618
|
+
rotateG: Math.random() * 90,
|
|
1619
|
+
rotateB: Math.random() * 90,
|
|
1620
|
+
scatter: Math.random(),
|
|
1621
|
+
blending: Math.random(),
|
|
1622
|
+
blendingMode: Math.floor(Math.random() * 5) + 1,
|
|
1623
|
+
greyscale: Math.random() > 0.7,
|
|
1624
|
+
disable: false,
|
|
1625
|
+
objectCount: Math.floor(Math.random() * 40) + 10,
|
|
1626
|
+
rotationSpeed: Math.random() * 3 + 0.5,
|
|
1627
|
+
colorTheme: Math.floor(Math.random() * 4),
|
|
1628
|
+
};
|
|
1629
|
+
|
|
1630
|
+
setParameters(newParams);
|
|
1631
|
+
|
|
1632
|
+
// Save the new randomized state
|
|
1633
|
+
setTimeout(async () => {
|
|
1634
|
+
await saveParameterState(newParams, "randomized");
|
|
1635
|
+
setIsGenerating(false);
|
|
1636
|
+
}, 500);
|
|
1637
|
+
}, [parameters, saveParameterState]);
|
|
1638
|
+
|
|
1639
|
+
// Save parameter changes for history
|
|
1640
|
+
useEffect(() => {
|
|
1641
|
+
const timeoutId = setTimeout(() => {
|
|
1642
|
+
saveParameterState(parameters, "manual");
|
|
1643
|
+
}, 1000);
|
|
1644
|
+
|
|
1645
|
+
return () => clearTimeout(timeoutId);
|
|
1646
|
+
}, [parameters, saveParameterState]);
|
|
1647
|
+
|
|
1648
|
+
useEffect(() => {
|
|
1649
|
+
if (!canvasRef.current) return;
|
|
1650
|
+
|
|
1651
|
+
// Scene setup
|
|
1652
|
+
const scene = new THREE.Scene();
|
|
1653
|
+
scene.background = new THREE.Color(0x242424);
|
|
1654
|
+
|
|
1655
|
+
const camera = new THREE.PerspectiveCamera(
|
|
1656
|
+
75,
|
|
1657
|
+
window.innerWidth / window.innerHeight,
|
|
1658
|
+
1,
|
|
1659
|
+
1000,
|
|
1660
|
+
);
|
|
1661
|
+
camera.position.z = 12;
|
|
1662
|
+
|
|
1663
|
+
const renderer = new THREE.WebGLRenderer({
|
|
1664
|
+
canvas: canvasRef.current,
|
|
1665
|
+
antialias: true,
|
|
1666
|
+
preserveDrawingBuffer: true,
|
|
1667
|
+
});
|
|
1668
|
+
renderer.setPixelRatio(window.devicePixelRatio);
|
|
1669
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
1670
|
+
|
|
1671
|
+
// Controls
|
|
1672
|
+
const controls = new OrbitControls(camera, renderer.domElement);
|
|
1673
|
+
controls.enableDamping = true;
|
|
1674
|
+
controls.dampingFactor = 0.05;
|
|
1675
|
+
controls.autoRotate = true;
|
|
1676
|
+
controls.autoRotateSpeed = 0.5;
|
|
1677
|
+
|
|
1678
|
+
// Group for all objects
|
|
1679
|
+
const group = new THREE.Group();
|
|
1680
|
+
scene.add(group);
|
|
1681
|
+
|
|
1682
|
+
// Lighting
|
|
1683
|
+
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
|
|
1684
|
+
scene.add(ambientLight);
|
|
1685
|
+
|
|
1686
|
+
const pointLight = new THREE.PointLight(0xffffff, 1, 100);
|
|
1687
|
+
pointLight.position.set(10, 10, 10);
|
|
1688
|
+
scene.add(pointLight);
|
|
1689
|
+
|
|
1690
|
+
// Post-processing
|
|
1691
|
+
const composer = new EffectComposer(renderer);
|
|
1692
|
+
const renderPass = new RenderPass(scene, camera);
|
|
1693
|
+
composer.addPass(renderPass);
|
|
1694
|
+
|
|
1695
|
+
const halftonePass = new HalftonePass({
|
|
1696
|
+
shape: parameters.shape,
|
|
1697
|
+
radius: parameters.radius,
|
|
1698
|
+
rotateR: parameters.rotateR * (Math.PI / 180),
|
|
1699
|
+
rotateG: parameters.rotateG * (Math.PI / 180),
|
|
1700
|
+
rotateB: parameters.rotateB * (Math.PI / 180),
|
|
1701
|
+
scatter: parameters.scatter,
|
|
1702
|
+
blending: parameters.blending,
|
|
1703
|
+
blendingMode: parameters.blendingMode,
|
|
1704
|
+
greyscale: parameters.greyscale,
|
|
1705
|
+
disable: parameters.disable,
|
|
1706
|
+
});
|
|
1707
|
+
composer.addPass(halftonePass);
|
|
1708
|
+
|
|
1709
|
+
// Store refs
|
|
1710
|
+
sceneRef.current = {
|
|
1711
|
+
scene,
|
|
1712
|
+
camera,
|
|
1713
|
+
renderer,
|
|
1714
|
+
composer,
|
|
1715
|
+
halftonePass,
|
|
1716
|
+
group,
|
|
1717
|
+
controls,
|
|
1718
|
+
objects: [],
|
|
1719
|
+
};
|
|
1720
|
+
|
|
1721
|
+
// Create initial objects
|
|
1722
|
+
const createObjects = () => {
|
|
1723
|
+
// Clear existing objects
|
|
1724
|
+
sceneRef.current.objects.forEach((obj) => {
|
|
1725
|
+
group.remove(obj);
|
|
1726
|
+
});
|
|
1727
|
+
sceneRef.current.objects = [];
|
|
1728
|
+
|
|
1729
|
+
// Color themes
|
|
1730
|
+
const colorThemes = [
|
|
1731
|
+
[0xff70a6, 0x70d6ff, 0xffd670, 0xe9ff70, 0xff9770], // Rainbow
|
|
1732
|
+
[0xff9770, 0xffd670, 0xff70a6], // Warm
|
|
1733
|
+
[0x70d6ff, 0xe9ff70, 0x242424], // Cool
|
|
1734
|
+
[0xffffff, 0x242424], // Monochrome
|
|
1735
|
+
];
|
|
1736
|
+
|
|
1737
|
+
const colors = colorThemes[parameters.colorTheme] || colorThemes[0];
|
|
1738
|
+
|
|
1739
|
+
// Shader material for interesting effects
|
|
1740
|
+
const material = new THREE.ShaderMaterial({
|
|
1741
|
+
uniforms: {
|
|
1742
|
+
time: { value: 0 },
|
|
1743
|
+
},
|
|
1744
|
+
vertexShader: `
|
|
1745
|
+
varying vec2 vUv;
|
|
1746
|
+
varying vec3 vNormal;
|
|
1747
|
+
varying vec3 vPosition;
|
|
1748
|
+
uniform float time;
|
|
1749
|
+
|
|
1750
|
+
void main() {
|
|
1751
|
+
vUv = uv;
|
|
1752
|
+
vNormal = normalize(normalMatrix * normal);
|
|
1753
|
+
vPosition = position;
|
|
1754
|
+
|
|
1755
|
+
vec3 pos = position;
|
|
1756
|
+
pos += sin(pos * 2.0 + time) * 0.1;
|
|
1757
|
+
|
|
1758
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
|
|
1759
|
+
}
|
|
1760
|
+
`,
|
|
1761
|
+
fragmentShader: `
|
|
1762
|
+
varying vec2 vUv;
|
|
1763
|
+
varying vec3 vNormal;
|
|
1764
|
+
varying vec3 vPosition;
|
|
1765
|
+
uniform float time;
|
|
1766
|
+
|
|
1767
|
+
void main() {
|
|
1768
|
+
vec3 color = abs(vNormal) + vec3(vUv, sin(time + vPosition.x));
|
|
1769
|
+
color = mix(color, vec3(1.0, 0.4, 0.6), sin(time + vPosition.y) * 0.5 + 0.5);
|
|
1770
|
+
gl_FragColor = vec4(color, 1.0);
|
|
1771
|
+
}
|
|
1772
|
+
`,
|
|
1773
|
+
});
|
|
1774
|
+
|
|
1775
|
+
// Create various geometric shapes
|
|
1776
|
+
const geometries = [
|
|
1777
|
+
new THREE.BoxGeometry(2, 2, 2),
|
|
1778
|
+
new THREE.SphereGeometry(1.2, 16, 16),
|
|
1779
|
+
new THREE.ConeGeometry(1, 2, 8),
|
|
1780
|
+
new THREE.CylinderGeometry(0.8, 0.8, 2, 8),
|
|
1781
|
+
new THREE.OctahedronGeometry(1.2),
|
|
1782
|
+
new THREE.TetrahedronGeometry(1.5),
|
|
1783
|
+
new THREE.DodecahedronGeometry(1),
|
|
1784
|
+
new THREE.IcosahedronGeometry(1.2),
|
|
1785
|
+
];
|
|
1786
|
+
|
|
1787
|
+
for (let i = 0; i < parameters.objectCount; i++) {
|
|
1788
|
+
const geometry =
|
|
1789
|
+
geometries[Math.floor(Math.random() * geometries.length)];
|
|
1790
|
+
const basicMaterial = new THREE.MeshPhongMaterial({
|
|
1791
|
+
color: colors[Math.floor(Math.random() * colors.length)],
|
|
1792
|
+
shininess: 100,
|
|
1793
|
+
transparent: true,
|
|
1794
|
+
opacity: 0.8 + Math.random() * 0.2,
|
|
1795
|
+
});
|
|
1796
|
+
|
|
1797
|
+
const mesh = new THREE.Mesh(
|
|
1798
|
+
geometry,
|
|
1799
|
+
Math.random() > 0.3 ? basicMaterial : material,
|
|
1800
|
+
);
|
|
1801
|
+
|
|
1802
|
+
mesh.position.set(
|
|
1803
|
+
(Math.random() - 0.5) * 20,
|
|
1804
|
+
(Math.random() - 0.5) * 20,
|
|
1805
|
+
(Math.random() - 0.5) * 20,
|
|
1806
|
+
);
|
|
1807
|
+
|
|
1808
|
+
mesh.rotation.set(
|
|
1809
|
+
Math.random() * Math.PI * 2,
|
|
1810
|
+
Math.random() * Math.PI * 2,
|
|
1811
|
+
Math.random() * Math.PI * 2,
|
|
1812
|
+
);
|
|
1813
|
+
|
|
1814
|
+
mesh.scale.setScalar(0.5 + Math.random() * 1.5);
|
|
1815
|
+
|
|
1816
|
+
group.add(mesh);
|
|
1817
|
+
sceneRef.current.objects.push(mesh);
|
|
1818
|
+
}
|
|
1819
|
+
};
|
|
1820
|
+
|
|
1821
|
+
createObjects();
|
|
1822
|
+
|
|
1823
|
+
// Animation loop
|
|
1824
|
+
const clock = new THREE.Clock();
|
|
1825
|
+
const animate = () => {
|
|
1826
|
+
const delta = clock.getDelta();
|
|
1827
|
+
const elapsed = clock.getElapsedTime();
|
|
1828
|
+
|
|
1829
|
+
// Update material uniforms
|
|
1830
|
+
sceneRef.current.objects.forEach((obj) => {
|
|
1831
|
+
if (obj.material.uniforms && obj.material.uniforms.time) {
|
|
1832
|
+
obj.material.uniforms.time.value = elapsed;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
// Animate objects
|
|
1836
|
+
obj.rotation.x += delta * parameters.rotationSpeed * 0.2;
|
|
1837
|
+
obj.rotation.y += delta * parameters.rotationSpeed * 0.3;
|
|
1838
|
+
obj.rotation.z += delta * parameters.rotationSpeed * 0.1;
|
|
1839
|
+
});
|
|
1840
|
+
|
|
1841
|
+
controls.update();
|
|
1842
|
+
composer.render();
|
|
1843
|
+
requestAnimationFrame(animate);
|
|
1844
|
+
};
|
|
1845
|
+
|
|
1846
|
+
animate();
|
|
1847
|
+
|
|
1848
|
+
// Handle resize
|
|
1849
|
+
const handleResize = () => {
|
|
1850
|
+
camera.aspect = window.innerWidth / window.innerHeight;
|
|
1851
|
+
camera.updateProjectionMatrix();
|
|
1852
|
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
1853
|
+
composer.setSize(window.innerWidth, window.innerHeight);
|
|
1854
|
+
};
|
|
1855
|
+
|
|
1856
|
+
window.addEventListener("resize", handleResize);
|
|
1857
|
+
|
|
1858
|
+
return () => {
|
|
1859
|
+
window.removeEventListener("resize", handleResize);
|
|
1860
|
+
renderer.dispose();
|
|
1861
|
+
};
|
|
1862
|
+
}, [parameters]);
|
|
1863
|
+
|
|
1864
|
+
// Update halftone parameters
|
|
1865
|
+
useEffect(() => {
|
|
1866
|
+
if (sceneRef.current?.halftonePass) {
|
|
1867
|
+
const pass = sceneRef.current.halftonePass;
|
|
1868
|
+
pass.uniforms.shape.value = parameters.shape;
|
|
1869
|
+
pass.uniforms.radius.value = parameters.radius;
|
|
1870
|
+
pass.uniforms.rotateR.value = parameters.rotateR * (Math.PI / 180);
|
|
1871
|
+
pass.uniforms.rotateG.value = parameters.rotateG * (Math.PI / 180);
|
|
1872
|
+
pass.uniforms.rotateB.value = parameters.rotateB * (Math.PI / 180);
|
|
1873
|
+
pass.uniforms.scatter.value = parameters.scatter;
|
|
1874
|
+
pass.uniforms.blending.value = parameters.blending;
|
|
1875
|
+
pass.uniforms.blendingMode.value = parameters.blendingMode;
|
|
1876
|
+
pass.uniforms.greyscale.value = parameters.greyscale;
|
|
1877
|
+
pass.uniforms.disable.value = parameters.disable;
|
|
1878
|
+
}
|
|
1879
|
+
}, [parameters]);
|
|
1880
|
+
|
|
1881
|
+
const shapeName =
|
|
1882
|
+
["", "Dot", "Ellipse", "Line", "Square"][parameters.shape] || "Dot";
|
|
1883
|
+
const blendModeName =
|
|
1884
|
+
["", "Linear", "Multiply", "Add", "Lighter", "Darker"][
|
|
1885
|
+
parameters.blendingMode
|
|
1886
|
+
] || "Linear";
|
|
1887
|
+
const actionNames = {
|
|
1888
|
+
"before-randomize": "🎲 Before Random",
|
|
1889
|
+
randomized: "✨ Randomized",
|
|
1890
|
+
manual: "✏️ Manual Edit",
|
|
1891
|
+
};
|
|
1892
|
+
|
|
1893
|
+
return (
|
|
1894
|
+
<div className="relative h-screen w-full overflow-hidden bg-[#242424]">
|
|
1895
|
+
{/* Background pattern */}
|
|
1896
|
+
<div
|
|
1897
|
+
className="absolute inset-0 opacity-10"
|
|
1898
|
+
style={{
|
|
1899
|
+
backgroundImage: `radial-gradient(circle at 25px 25px, #70d6ff 2px, transparent 2px)`,
|
|
1900
|
+
backgroundSize: "50px 50px",
|
|
1901
|
+
}}
|
|
1902
|
+
/>
|
|
1903
|
+
|
|
1904
|
+
<canvas ref={canvasRef} className="absolute inset-0" />
|
|
1905
|
+
|
|
1906
|
+
{/* Main Control Panel */}
|
|
1907
|
+
<div
|
|
1908
|
+
className={`absolute top-4 left-4 max-h-[calc(100vh-2rem)] overflow-y-auto rounded-lg border-4 border-[#242424] bg-[#ffffff] p-4 shadow-lg transition-all duration-300 ${showParameters ? "w-80" : "w-64"}`}
|
|
1909
|
+
>
|
|
1910
|
+
<h2 className="mb-4 text-lg font-bold text-[#242424]">
|
|
1911
|
+
RGB Halftone Studio
|
|
1912
|
+
</h2>
|
|
1913
|
+
|
|
1914
|
+
{/* Always visible controls */}
|
|
1915
|
+
<div className="mb-4 space-y-3">
|
|
1916
|
+
<button
|
|
1917
|
+
onClick={generateRandomScene}
|
|
1918
|
+
disabled={isGenerating}
|
|
1919
|
+
className="w-full rounded border-2 border-[#242424] bg-[#ff70a6] px-4 py-3 font-bold text-[#242424] hover:bg-[#ff9770] disabled:opacity-50"
|
|
1920
|
+
>
|
|
1921
|
+
{isGenerating ? "Generating..." : "🎲 Random Art"}
|
|
1922
|
+
</button>
|
|
1923
|
+
|
|
1924
|
+
<button
|
|
1925
|
+
onClick={() => setShowParameters(!showParameters)}
|
|
1926
|
+
className="w-full rounded border-2 border-[#242424] bg-[#70d6ff] px-4 py-2 font-bold text-[#242424] hover:bg-[#e9ff70]"
|
|
1927
|
+
>
|
|
1928
|
+
{showParameters ? "🔼 Hide Controls" : "🔽 Show Controls"}
|
|
1929
|
+
</button>
|
|
1930
|
+
</div>
|
|
1931
|
+
|
|
1932
|
+
{/* Expandable parameter controls */}
|
|
1933
|
+
{showParameters && (
|
|
1934
|
+
<div className="space-y-4">
|
|
1935
|
+
{/* Shape Controls */}
|
|
1936
|
+
<div>
|
|
1937
|
+
<label className="mb-2 block text-sm font-bold text-[#242424]">
|
|
1938
|
+
Shape: {shapeName}
|
|
1939
|
+
</label>
|
|
1940
|
+
<select
|
|
1941
|
+
value={parameters.shape}
|
|
1942
|
+
onChange={(e) =>
|
|
1943
|
+
setParameters((prev) => ({
|
|
1944
|
+
...prev,
|
|
1945
|
+
shape: parseInt(e.target.value),
|
|
1946
|
+
}))
|
|
1947
|
+
}
|
|
1948
|
+
className="w-full rounded border-2 border-[#242424] p-2 text-[#242424]"
|
|
1949
|
+
>
|
|
1950
|
+
<option value={1}>Dot</option>
|
|
1951
|
+
<option value={2}>Ellipse</option>
|
|
1952
|
+
<option value={3}>Line</option>
|
|
1953
|
+
<option value={4}>Square</option>
|
|
1954
|
+
</select>
|
|
1955
|
+
</div>
|
|
1956
|
+
|
|
1957
|
+
{/* Size Controls */}
|
|
1958
|
+
<div>
|
|
1959
|
+
<label className="mb-2 block text-sm font-bold text-[#242424]">
|
|
1960
|
+
Size: {parameters.radius.toFixed(1)}
|
|
1961
|
+
</label>
|
|
1962
|
+
<input
|
|
1963
|
+
type="range"
|
|
1964
|
+
min="1"
|
|
1965
|
+
max="25"
|
|
1966
|
+
step="0.5"
|
|
1967
|
+
value={parameters.radius}
|
|
1968
|
+
onChange={(e) =>
|
|
1969
|
+
setParameters((prev) => ({
|
|
1970
|
+
...prev,
|
|
1971
|
+
radius: parseFloat(e.target.value),
|
|
1972
|
+
}))
|
|
1973
|
+
}
|
|
1974
|
+
className="w-full"
|
|
1975
|
+
/>
|
|
1976
|
+
</div>
|
|
1977
|
+
|
|
1978
|
+
{/* Color Rotation */}
|
|
1979
|
+
<div className="grid grid-cols-3 gap-2">
|
|
1980
|
+
<div>
|
|
1981
|
+
<label className="mb-1 block text-xs font-bold text-[#ff70a6]">
|
|
1982
|
+
Red: {parameters.rotateR.toFixed(0)}°
|
|
1983
|
+
</label>
|
|
1984
|
+
<input
|
|
1985
|
+
type="range"
|
|
1986
|
+
min="0"
|
|
1987
|
+
max="90"
|
|
1988
|
+
value={parameters.rotateR}
|
|
1989
|
+
onChange={(e) =>
|
|
1990
|
+
setParameters((prev) => ({
|
|
1991
|
+
...prev,
|
|
1992
|
+
rotateR: parseFloat(e.target.value),
|
|
1993
|
+
}))
|
|
1994
|
+
}
|
|
1995
|
+
className="w-full"
|
|
1996
|
+
/>
|
|
1997
|
+
</div>
|
|
1998
|
+
<div>
|
|
1999
|
+
<label className="mb-1 block text-xs font-bold text-[#e9ff70]">
|
|
2000
|
+
Green: {parameters.rotateG.toFixed(0)}°
|
|
2001
|
+
</label>
|
|
2002
|
+
<input
|
|
2003
|
+
type="range"
|
|
2004
|
+
min="0"
|
|
2005
|
+
max="90"
|
|
2006
|
+
value={parameters.rotateG}
|
|
2007
|
+
onChange={(e) =>
|
|
2008
|
+
setParameters((prev) => ({
|
|
2009
|
+
...prev,
|
|
2010
|
+
rotateG: parseFloat(e.target.value),
|
|
2011
|
+
}))
|
|
2012
|
+
}
|
|
2013
|
+
className="w-full"
|
|
2014
|
+
/>
|
|
2015
|
+
</div>
|
|
2016
|
+
<div>
|
|
2017
|
+
<label className="mb-1 block text-xs font-bold text-[#70d6ff]">
|
|
2018
|
+
Blue: {parameters.rotateB.toFixed(0)}°
|
|
2019
|
+
</label>
|
|
2020
|
+
<input
|
|
2021
|
+
type="range"
|
|
2022
|
+
min="0"
|
|
2023
|
+
max="90"
|
|
2024
|
+
value={parameters.rotateB}
|
|
2025
|
+
onChange={(e) =>
|
|
2026
|
+
setParameters((prev) => ({
|
|
2027
|
+
...prev,
|
|
2028
|
+
rotateB: parseFloat(e.target.value),
|
|
2029
|
+
}))
|
|
2030
|
+
}
|
|
2031
|
+
className="w-full"
|
|
2032
|
+
/>
|
|
2033
|
+
</div>
|
|
2034
|
+
</div>
|
|
2035
|
+
|
|
2036
|
+
{/* Effects */}
|
|
2037
|
+
<div>
|
|
2038
|
+
<label className="mb-2 block text-sm font-bold text-[#242424]">
|
|
2039
|
+
Scatter: {(parameters.scatter * 100).toFixed(0)}%
|
|
2040
|
+
</label>
|
|
2041
|
+
<input
|
|
2042
|
+
type="range"
|
|
2043
|
+
min="0"
|
|
2044
|
+
max="1"
|
|
2045
|
+
step="0.01"
|
|
2046
|
+
value={parameters.scatter}
|
|
2047
|
+
onChange={(e) =>
|
|
2048
|
+
setParameters((prev) => ({
|
|
2049
|
+
...prev,
|
|
2050
|
+
scatter: parseFloat(e.target.value),
|
|
2051
|
+
}))
|
|
2052
|
+
}
|
|
2053
|
+
className="w-full"
|
|
2054
|
+
/>
|
|
2055
|
+
</div>
|
|
2056
|
+
|
|
2057
|
+
<div>
|
|
2058
|
+
<label className="mb-2 block text-sm font-bold text-[#242424]">
|
|
2059
|
+
Blend: {(parameters.blending * 100).toFixed(0)}%
|
|
2060
|
+
</label>
|
|
2061
|
+
<input
|
|
2062
|
+
type="range"
|
|
2063
|
+
min="0"
|
|
2064
|
+
max="1"
|
|
2065
|
+
step="0.01"
|
|
2066
|
+
value={parameters.blending}
|
|
2067
|
+
onChange={(e) =>
|
|
2068
|
+
setParameters((prev) => ({
|
|
2069
|
+
...prev,
|
|
2070
|
+
blending: parseFloat(e.target.value),
|
|
2071
|
+
}))
|
|
2072
|
+
}
|
|
2073
|
+
className="w-full"
|
|
2074
|
+
/>
|
|
2075
|
+
</div>
|
|
2076
|
+
|
|
2077
|
+
<div>
|
|
2078
|
+
<label className="mb-2 block text-sm font-bold text-[#242424]">
|
|
2079
|
+
Blend Mode: {blendModeName}
|
|
2080
|
+
</label>
|
|
2081
|
+
<select
|
|
2082
|
+
value={parameters.blendingMode}
|
|
2083
|
+
onChange={(e) =>
|
|
2084
|
+
setParameters((prev) => ({
|
|
2085
|
+
...prev,
|
|
2086
|
+
blendingMode: parseInt(e.target.value),
|
|
2087
|
+
}))
|
|
2088
|
+
}
|
|
2089
|
+
className="w-full rounded border-2 border-[#242424] p-2 text-[#242424]"
|
|
2090
|
+
>
|
|
2091
|
+
<option value={1}>Linear</option>
|
|
2092
|
+
<option value={2}>Multiply</option>
|
|
2093
|
+
<option value={3}>Add</option>
|
|
2094
|
+
<option value={4}>Lighter</option>
|
|
2095
|
+
<option value={5}>Darker</option>
|
|
2096
|
+
</select>
|
|
2097
|
+
</div>
|
|
2098
|
+
|
|
2099
|
+
{/* Toggles */}
|
|
2100
|
+
<div className="space-y-2">
|
|
2101
|
+
<label className="flex items-center">
|
|
2102
|
+
<input
|
|
2103
|
+
type="checkbox"
|
|
2104
|
+
checked={parameters.greyscale}
|
|
2105
|
+
onChange={(e) =>
|
|
2106
|
+
setParameters((prev) => ({
|
|
2107
|
+
...prev,
|
|
2108
|
+
greyscale: e.target.checked,
|
|
2109
|
+
}))
|
|
2110
|
+
}
|
|
2111
|
+
className="mr-2"
|
|
2112
|
+
/>
|
|
2113
|
+
<span className="text-sm font-bold text-[#242424]">
|
|
2114
|
+
Greyscale
|
|
2115
|
+
</span>
|
|
2116
|
+
</label>
|
|
2117
|
+
|
|
2118
|
+
<label className="flex items-center">
|
|
2119
|
+
<input
|
|
2120
|
+
type="checkbox"
|
|
2121
|
+
checked={parameters.disable}
|
|
2122
|
+
onChange={(e) =>
|
|
2123
|
+
setParameters((prev) => ({
|
|
2124
|
+
...prev,
|
|
2125
|
+
disable: e.target.checked,
|
|
2126
|
+
}))
|
|
2127
|
+
}
|
|
2128
|
+
className="mr-2"
|
|
2129
|
+
/>
|
|
2130
|
+
<span className="text-sm font-bold text-[#242424]">
|
|
2131
|
+
Disable Effect
|
|
2132
|
+
</span>
|
|
2133
|
+
</label>
|
|
2134
|
+
</div>
|
|
2135
|
+
|
|
2136
|
+
{/* Save Preset */}
|
|
2137
|
+
<div>
|
|
2138
|
+
<input
|
|
2139
|
+
type="text"
|
|
2140
|
+
placeholder="Preset name..."
|
|
2141
|
+
value={presetName}
|
|
2142
|
+
onChange={(e) => setPresetName(e.target.value)}
|
|
2143
|
+
className="mb-2 w-full rounded border-2 border-[#242424] p-2 text-[#242424]"
|
|
2144
|
+
/>
|
|
2145
|
+
<button
|
|
2146
|
+
onClick={savePreset}
|
|
2147
|
+
disabled={!presetName.trim()}
|
|
2148
|
+
className="w-full rounded border-2 border-[#242424] bg-[#ffd670] px-4 py-2 font-bold text-[#242424] hover:bg-[#e9ff70] disabled:opacity-50"
|
|
2149
|
+
>
|
|
2150
|
+
💾 Save Preset
|
|
2151
|
+
</button>
|
|
2152
|
+
</div>
|
|
2153
|
+
|
|
2154
|
+
{/* Saved Presets */}
|
|
2155
|
+
{presets.length > 0 && (
|
|
2156
|
+
<div>
|
|
2157
|
+
<h4 className="mb-2 text-sm font-bold text-[#242424]">
|
|
2158
|
+
💾 Saved Presets
|
|
2159
|
+
</h4>
|
|
2160
|
+
<div className="max-h-32 space-y-2 overflow-y-auto">
|
|
2161
|
+
{presets
|
|
2162
|
+
.sort((a, b) => b.timestamp - a.timestamp)
|
|
2163
|
+
.map((preset) => (
|
|
2164
|
+
<div
|
|
2165
|
+
key={preset._id}
|
|
2166
|
+
className={`cursor-pointer rounded border-2 p-2 transition-colors ${
|
|
2167
|
+
currentPreset?._id === preset._id
|
|
2168
|
+
? "border-[#242424] bg-[#ff70a6]"
|
|
2169
|
+
: "border-[#242424] bg-[#ffffff] hover:bg-[#e9ff70]"
|
|
2170
|
+
}`}
|
|
2171
|
+
onClick={() => loadPreset(preset)}
|
|
2172
|
+
>
|
|
2173
|
+
<div className="text-xs font-bold text-[#242424]">
|
|
2174
|
+
{preset.name}
|
|
2175
|
+
</div>
|
|
2176
|
+
<div className="text-xs text-[#242424] opacity-75">
|
|
2177
|
+
{
|
|
2178
|
+
["", "Dot", "Ellipse", "Line", "Square"][
|
|
2179
|
+
preset.parameters.shape
|
|
2180
|
+
]
|
|
2181
|
+
}{" "}
|
|
2182
|
+
• {preset.parameters.greyscale ? "B&W" : "Color"}
|
|
2183
|
+
</div>
|
|
2184
|
+
</div>
|
|
2185
|
+
))}
|
|
2186
|
+
</div>
|
|
2187
|
+
</div>
|
|
2188
|
+
)}
|
|
2189
|
+
|
|
2190
|
+
{/* Parameter History */}
|
|
2191
|
+
{parameterHistory.length > 0 && (
|
|
2192
|
+
<div>
|
|
2193
|
+
<h4 className="mb-2 text-sm font-bold text-[#242424]">
|
|
2194
|
+
📜 Parameter History
|
|
2195
|
+
</h4>
|
|
2196
|
+
<div className="max-h-40 space-y-2 overflow-y-auto">
|
|
2197
|
+
{parameterHistory
|
|
2198
|
+
.sort((a, b) => b.timestamp - a.timestamp)
|
|
2199
|
+
.slice(0, 10)
|
|
2200
|
+
.map((state) => (
|
|
2201
|
+
<div
|
|
2202
|
+
key={state._id}
|
|
2203
|
+
className="cursor-pointer rounded border-2 border-[#242424] p-2 transition-colors hover:bg-[#e9ff70]"
|
|
2204
|
+
onClick={() => loadParameterState(state)}
|
|
2205
|
+
>
|
|
2206
|
+
<div className="text-xs font-bold text-[#242424]">
|
|
2207
|
+
{actionNames[state.action] || "⚙️ Unknown"}
|
|
2208
|
+
</div>
|
|
2209
|
+
<div className="text-xs text-[#242424] opacity-75">
|
|
2210
|
+
{
|
|
2211
|
+
["", "Dot", "Ellipse", "Line", "Square"][
|
|
2212
|
+
state.parameters.shape
|
|
2213
|
+
]
|
|
2214
|
+
}{" "}
|
|
2215
|
+
• Size: {state.parameters.radius.toFixed(1)} •{" "}
|
|
2216
|
+
{state.parameters.greyscale ? "B&W" : "Color"}
|
|
2217
|
+
</div>
|
|
2218
|
+
<div className="text-xs text-[#242424] opacity-50">
|
|
2219
|
+
{new Date(state.timestamp).toLocaleTimeString()}
|
|
2220
|
+
</div>
|
|
2221
|
+
</div>
|
|
2222
|
+
))}
|
|
2223
|
+
</div>
|
|
2224
|
+
</div>
|
|
2225
|
+
)}
|
|
2226
|
+
</div>
|
|
2227
|
+
)}
|
|
2228
|
+
</div>
|
|
2229
|
+
</div>
|
|
2230
|
+
);
|
|
2231
|
+
}
|
|
2232
|
+
```
|