├── .DS_Store
├── .vscode
└── settings.json
├── README.md
├── assets
├── .DS_Store
└── siteOGImage.png
├── chromatic-shader.js
├── geometry.js
├── index.html
├── main.js
├── styles.css
└── voice-control.js
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collidingScopes/iron-interface/21400f57c71844a8384ede8050681db9528e9266/.DS_Store
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "liveServer.settings.port": 5501
3 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Iron Interface
2 |
3 | An interactive 3D particle visualization controlled through hand gestures and voice.
4 |
5 | [Live Demo](https://collidingscopes.github.io/iron-interface/) | [Video Example](https://www.instagram.com/reel/DH9oY0sR4sJ/)
6 |
7 |
8 |
9 | ## Features
10 |
11 | - Control the camera with your hands
12 | - Right hand: pinch to zoom in/out
13 | - Left hand: rotate to orbit the camera
14 | - Speak to change to a new pattern ("Jarvis, change to a sphere")
15 |
16 | ## Technology
17 |
18 | Built with Three.js, MediaPipe Hand Tracking, and Web Speech API.
19 |
20 | ## Related Projects
21 |
22 | You might also like some of my other open source projects:
23 |
24 | - [Threejs shape creator](https://collidingScopes.github.io/shape-creator-tutorial) - create / control 3D shapes with threejs and MediaPipe computer vision
25 | - [Threejs hand tracking tutorial](https://collidingScopes.github.io/threejs-handtracking-101) - Basic hand tracking setup with threejs and MediaPipe computer vision
26 | - [Particular Drift](https://collidingScopes.github.io/particular-drift) - Turn photos into flowing particle animations
27 | - [Liquid Logo](https://collidingScopes.github.io/liquid-logo) - Transform logos and icons into liquid metal animations
28 | - [Video-to-ASCII](https://collidingScopes.github.io/ascii) - Convert videos into ASCII pixel art
29 |
30 | ## Contact
31 |
32 | - Instagram: [@stereo.drift](https://www.instagram.com/stereo.drift/)
33 | - Twitter/X: [@measure_plan](https://x.com/measure_plan)
34 | - Email: [stereodriftvisuals@gmail.com](mailto:stereodriftvisuals@gmail.com)
35 | - GitHub: [collidingScopes](https://github.com/collidingScopes)
36 |
37 | ## Donations
38 |
39 | If you found this tool useful, feel free to buy me a coffee.
40 |
41 | My name is Alan, and I enjoy building open source software for computer vision, games, and more. This would be much appreciated during late-night coding sessions!
42 |
43 | [](https://www.buymeacoffee.com/stereoDrift)
--------------------------------------------------------------------------------
/assets/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collidingScopes/iron-interface/21400f57c71844a8384ede8050681db9528e9266/assets/.DS_Store
--------------------------------------------------------------------------------
/assets/siteOGImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collidingScopes/iron-interface/21400f57c71844a8384ede8050681db9528e9266/assets/siteOGImage.png
--------------------------------------------------------------------------------
/chromatic-shader.js:
--------------------------------------------------------------------------------
1 | // chromatic-shader.js
2 | const ChromaticAberrationShader = {
3 | uniforms: {
4 | "tDiffuse": { value: null },
5 | "resolution": { value: new THREE.Vector2(1, 1) },
6 | "strength": { value: 0.5 } // Strength of the chromatic aberration effect
7 | },
8 |
9 | vertexShader: /* glsl */`
10 | varying vec2 vUv;
11 | void main() {
12 | vUv = uv;
13 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
14 | }
15 | `,
16 |
17 | fragmentShader: /* glsl */`
18 | uniform sampler2D tDiffuse;
19 | uniform vec2 resolution;
20 | uniform float strength;
21 | varying vec2 vUv;
22 |
23 | void main() {
24 | // Calculate distance from center (0.5, 0.5)
25 | vec2 uv = vUv - 0.5;
26 | float dist = length(uv);
27 |
28 | // Apply a smooth falloff near the center
29 | // This creates a "neutral zone" with no chromatic aberration
30 | float factor = smoothstep(0.0, 0.4, dist);
31 |
32 | // Normalize direction vector
33 | vec2 direction = normalize(uv);
34 |
35 | // Scale the offset by both the distance factor and our strength parameter
36 | vec2 redOffset = direction * strength * factor * dist;
37 | vec2 greenOffset = direction * strength * 0.6 * factor * dist;
38 | vec2 blueOffset = direction * strength * 0.3 * factor * dist;
39 |
40 | // Sample each color channel with its offset
41 | float r = texture2D(tDiffuse, vUv - redOffset).r;
42 | float g = texture2D(tDiffuse, vUv - greenOffset).g;
43 | float b = texture2D(tDiffuse, vUv - blueOffset).b;
44 |
45 | gl_FragColor = vec4(r, g, b, 1.0);
46 | }
47 | `
48 | };
--------------------------------------------------------------------------------
/geometry.js:
--------------------------------------------------------------------------------
1 | // --- PATTERN FUNCTIONS ---
2 | function createGrid(i, count) {
3 | const sideLength = Math.ceil(Math.cbrt(count));
4 | const spacing = 60 / sideLength;
5 | const halfGrid = (sideLength - 1) * spacing / 2;
6 |
7 | // Determine which side of the cube this particle should be on
8 | const totalSides = 6; // A cube has 6 sides
9 | const pointsPerSide = Math.floor(count / totalSides);
10 | const side = Math.floor(i / pointsPerSide);
11 | const indexOnSide = i % pointsPerSide;
12 |
13 | // Calculate a grid position on a 2D plane
14 | const sideLength2D = Math.ceil(Math.sqrt(pointsPerSide));
15 | const ix = indexOnSide % sideLength2D;
16 | const iy = Math.floor(indexOnSide / sideLength2D);
17 |
18 | // Map to relative coordinates (0 to 1)
19 | const rx = ix / (sideLength2D - 1 || 1);
20 | const ry = iy / (sideLength2D - 1 || 1);
21 |
22 | // Convert to actual coordinates with proper spacing (-halfGrid to +halfGrid)
23 | const x = rx * spacing * (sideLength - 1) - halfGrid;
24 | const y = ry * spacing * (sideLength - 1) - halfGrid;
25 |
26 | // Place on the appropriate face of the cube
27 | switch(side % totalSides) {
28 | case 0: return new THREE.Vector3(x, y, halfGrid); // Front face
29 | case 1: return new THREE.Vector3(x, y, -halfGrid); // Back face
30 | case 2: return new THREE.Vector3(x, halfGrid, y); // Top face
31 | case 3: return new THREE.Vector3(x, -halfGrid, y); // Bottom face
32 | case 4: return new THREE.Vector3(halfGrid, x, y); // Right face
33 | case 5: return new THREE.Vector3(-halfGrid, x, y); // Left face
34 | default: return new THREE.Vector3(0, 0, 0);
35 | }
36 | }
37 |
38 | function createSphere(i, count) {
39 | // Sphere distribution using spherical coordinates for surface only
40 | const t = i / count;
41 | const phi = Math.acos(2 * t - 1); // Full range from 0 to PI
42 | const theta = 2 * Math.PI * (i / count) * Math.sqrt(count); // Golden ratio distribution
43 |
44 | // Fixed radius for surface-only distribution
45 | const radius = 30;
46 |
47 | return new THREE.Vector3(
48 | Math.sin(phi) * Math.cos(theta) * radius,
49 | Math.sin(phi) * Math.sin(theta) * radius,
50 | Math.cos(phi) * radius
51 | );
52 | }
53 |
54 | function createSpiral(i, count) {
55 | const t = i / count;
56 | const numArms = 3;
57 | const armIndex = i % numArms;
58 | const angleOffset = (2 * Math.PI / numArms) * armIndex;
59 | const angle = Math.pow(t, 0.7) * 15 + angleOffset;
60 | const radius = t * 40;
61 |
62 | // This is a 2D shape with particles on a thin plane by design
63 | const height = 0; // Set to zero or a very small noise value for thickness
64 |
65 | return new THREE.Vector3(
66 | Math.cos(angle) * radius,
67 | Math.sin(angle) * radius,
68 | height
69 | );
70 | }
71 |
72 | function createHelix(i, count) {
73 | const numHelices = 2;
74 | const helixIndex = i % numHelices;
75 | const t = Math.floor(i / numHelices) / Math.floor(count / numHelices);
76 | const angle = t * Math.PI * 10;
77 |
78 | // Fixed radius for surface-only distribution
79 | const radius = 15;
80 | const height = (t - 0.5) * 60;
81 | const angleOffset = helixIndex * Math.PI;
82 |
83 | return new THREE.Vector3(
84 | Math.cos(angle + angleOffset) * radius,
85 | Math.sin(angle + angleOffset) * radius,
86 | height
87 | );
88 | }
89 |
90 | function createTorus(i, count) {
91 | // Torus parameters
92 | const R = 30; // Major radius (distance from center of tube to center of torus)
93 | const r = 10; // Minor radius (radius of the tube)
94 |
95 | // Use a uniform distribution on the torus surface
96 | // by using uniform sampling in the 2 angle parameters
97 | const u = (i / count) * 2 * Math.PI; // Angle around the center of the torus
98 | const v = (i * Math.sqrt(5)) * 2 * Math.PI; // Angle around the tube
99 |
100 | // Parametric equation of a torus
101 | return new THREE.Vector3(
102 | (R + r * Math.cos(v)) * Math.cos(u),
103 | (R + r * Math.cos(v)) * Math.sin(u),
104 | r * Math.sin(v)
105 | );
106 | }
107 |
108 | function createVortex(i, count) {
109 | // Vortex parameters
110 | const height = 60; // Total height of the vortex
111 | const maxRadius = 35; // Maximum radius at the top
112 | const minRadius = 5; // Minimum radius at the bottom
113 | const numRotations = 3; // Number of full rotations from top to bottom
114 |
115 | // Calculate normalized height position (0 = bottom, 1 = top)
116 | const t = i / count;
117 |
118 | // Add some randomness to distribute particles more naturally
119 | const randomOffset = 0.05 * Math.random();
120 | const heightPosition = t + randomOffset;
121 |
122 | // Calculate radius that decreases from top to bottom
123 | const radius = minRadius + (maxRadius - minRadius) * heightPosition;
124 |
125 | // Calculate angle with more rotations at the bottom
126 | const angle = numRotations * Math.PI * 2 * (1 - heightPosition) + (i * 0.1);
127 |
128 | // Calculate the vertical position (from bottom to top)
129 | const y = (heightPosition - 0.5) * height;
130 |
131 | return new THREE.Vector3(
132 | Math.cos(angle) * radius,
133 | y,
134 | Math.sin(angle) * radius
135 | );
136 | }
137 |
138 | function createGalaxy(i, count) {
139 | // Galaxy parameters
140 | const numArms = 4; // Number of spiral arms
141 | const armWidth = 0.15; // Width of each arm (0-1)
142 | const maxRadius = 40; // Maximum radius of the galaxy
143 | const thickness = 5; // Vertical thickness
144 | const twistFactor = 2.5; // How much the arms twist
145 |
146 | // Determine which arm this particle belongs to
147 | const armIndex = i % numArms;
148 | const indexInArm = Math.floor(i / numArms) / Math.floor(count / numArms);
149 |
150 | // Calculate radial distance from center
151 | const radialDistance = indexInArm * maxRadius;
152 |
153 | // Add some randomness for arm width
154 | const randomOffset = (Math.random() * 2 - 1) * armWidth;
155 |
156 | // Calculate angle with twist that increases with distance
157 | const armOffset = (2 * Math.PI / numArms) * armIndex;
158 | const twistAmount = twistFactor * indexInArm;
159 | const angle = armOffset + twistAmount + randomOffset;
160 |
161 | // Add height variation that decreases with distance from center
162 | const verticalPosition = (Math.random() * 2 - 1) * thickness * (1 - indexInArm * 0.8);
163 |
164 | return new THREE.Vector3(
165 | Math.cos(angle) * radialDistance,
166 | verticalPosition,
167 | Math.sin(angle) * radialDistance
168 | );
169 | }
170 |
171 | function createWave(i, count) {
172 | // Wave/ocean parameters
173 | const width = 60; // Total width of the wave field
174 | const depth = 60; // Total depth of the wave field
175 | const waveHeight = 10; // Maximum height of waves
176 | const waveDensity = 0.1; // Controls wave frequency
177 |
178 | // Create a grid of points (similar to your grid function but for a 2D plane)
179 | const gridSize = Math.ceil(Math.sqrt(count));
180 | const spacingX = width / gridSize;
181 | const spacingZ = depth / gridSize;
182 |
183 | // Calculate 2D grid position
184 | const ix = i % gridSize;
185 | const iz = Math.floor(i / gridSize);
186 |
187 | // Convert to actual coordinates with proper spacing
188 | const halfWidth = width / 2;
189 | const halfDepth = depth / 2;
190 | const x = ix * spacingX - halfWidth;
191 | const z = iz * spacingZ - halfDepth;
192 |
193 | // Create wave pattern using multiple sine waves for a more natural look
194 | // We use the x and z coordinates to create a position-based wave pattern
195 | const y = Math.sin(x * waveDensity) * Math.cos(z * waveDensity) * waveHeight +
196 | Math.sin(x * waveDensity * 2.5) * Math.cos(z * waveDensity * 2.1) * (waveHeight * 0.3);
197 |
198 | return new THREE.Vector3(x, y, z);
199 | }
200 |
201 | function createMobius(i, count) {
202 | // Möbius strip parameters
203 | const radius = 25; // Major radius of the strip
204 | const width = 10; // Width of the strip
205 |
206 | // Distribute points evenly along the length of the Möbius strip
207 | // and across its width
208 | const lengthSteps = Math.sqrt(count);
209 | const widthSteps = count / lengthSteps;
210 |
211 | // Calculate position along length and width of strip
212 | const lengthIndex = i % lengthSteps;
213 | const widthIndex = Math.floor(i / lengthSteps) % widthSteps;
214 |
215 | // Normalize to 0-1 range
216 | const u = lengthIndex / lengthSteps; // Position around the strip (0 to 1)
217 | const v = (widthIndex / widthSteps) - 0.5; // Position across width (-0.5 to 0.5)
218 |
219 | // Parametric equations for Möbius strip
220 | const theta = u * Math.PI * 2; // Full loop around
221 |
222 | // Calculate the Möbius strip coordinates
223 | // This creates a half-twist in the strip
224 | const x = (radius + width * v * Math.cos(theta / 2)) * Math.cos(theta);
225 | const y = (radius + width * v * Math.cos(theta / 2)) * Math.sin(theta);
226 | const z = width * v * Math.sin(theta / 2);
227 |
228 | return new THREE.Vector3(x, y, z);
229 | }
230 |
231 | function createSupernova(i, count) {
232 | // Supernova parameters
233 | const maxRadius = 40; // Maximum explosion radius
234 | const coreSize = 0.2; // Size of the dense core (0-1)
235 | const outerDensity = 0.7; // Density of particles in outer shell
236 |
237 | // Use golden ratio distribution for even spherical coverage
238 | const phi = Math.acos(1 - 2 * (i / count));
239 | const theta = Math.PI * 2 * i * (1 + Math.sqrt(5));
240 |
241 | // Calculate radial distance with more particles near center and at outer shell
242 | let normalizedRadius;
243 | const random = Math.random();
244 |
245 | if (i < count * coreSize) {
246 | // Dense core - distribute within inner radius
247 | normalizedRadius = Math.pow(random, 0.5) * 0.3;
248 | } else {
249 | // Explosion wave - distribute with more particles at the outer shell
250 | normalizedRadius = 0.3 + Math.pow(random, outerDensity) * 0.7;
251 | }
252 |
253 | // Scale to max radius
254 | const radius = normalizedRadius * maxRadius;
255 |
256 | // Convert spherical to Cartesian coordinates
257 | return new THREE.Vector3(
258 | Math.sin(phi) * Math.cos(theta) * radius,
259 | Math.sin(phi) * Math.sin(theta) * radius,
260 | Math.cos(phi) * radius
261 | );
262 | }
263 |
264 | function createKleinBottle(i, count) {
265 | // Klein Bottle parameters
266 | const a = 15; // Main radius
267 | const b = 4; // Tube radius
268 | const scale = 2.5; // Overall scale
269 |
270 | // Use uniform distribution across the surface
271 | const lengthSteps = Math.ceil(Math.sqrt(count * 0.5));
272 | const circSteps = Math.ceil(count / lengthSteps);
273 |
274 | // Calculate position in the parametric space
275 | const lengthIndex = i % lengthSteps;
276 | const circIndex = Math.floor(i / lengthSteps) % circSteps;
277 |
278 | // Normalize to appropriate ranges
279 | const u = (lengthIndex / lengthSteps) * Math.PI * 2; // 0 to 2π
280 | const v = (circIndex / circSteps) * Math.PI * 2; // 0 to 2π
281 |
282 | // Klein Bottle parametric equation
283 | let x, y, z;
284 |
285 | // The Klein Bottle has different regions with different parametric equations
286 | if (u < Math.PI) {
287 | // First half (handle and transition region)
288 | x = scale * (a * (1 - Math.cos(u) / 2) * Math.cos(v) - b * Math.sin(u) / 2);
289 | y = scale * (a * (1 - Math.cos(u) / 2) * Math.sin(v));
290 | z = scale * (a * Math.sin(u) / 2 + b * Math.sin(u) * Math.cos(v));
291 | } else {
292 | // Second half (main bottle body)
293 | x = scale * (a * (1 + Math.cos(u) / 2) * Math.cos(v) + b * Math.sin(u) / 2);
294 | y = scale * (a * (1 + Math.cos(u) / 2) * Math.sin(v));
295 | z = scale * (-a * Math.sin(u) / 2 + b * Math.sin(u) * Math.cos(v));
296 | }
297 |
298 | return new THREE.Vector3(x, y, z);
299 | }
300 |
301 | function createFlower(i, count) {
302 | // Flower/Dandelion parameters
303 | const numPetals = 12; // Number of petals
304 | const petalLength = 25; // Length of petals
305 | const centerRadius = 10; // Radius of center sphere
306 | const petalWidth = 0.3; // Width of petals (0-1)
307 | const petalCurve = 0.6; // How much petals curve outward (0-1)
308 |
309 | // Calculate whether this particle is in the center or on a petal
310 | const centerParticleCount = Math.floor(count * 0.3); // 30% of particles in center
311 | const isCenter = i < centerParticleCount;
312 |
313 | if (isCenter) {
314 | // Center particles form a sphere
315 | const t = i / centerParticleCount;
316 | const phi = Math.acos(2 * t - 1);
317 | const theta = 2 * Math.PI * i * (1 + Math.sqrt(5)); // Golden ratio distribution
318 |
319 | // Create a sphere for the center
320 | return new THREE.Vector3(
321 | Math.sin(phi) * Math.cos(theta) * centerRadius,
322 | Math.sin(phi) * Math.sin(theta) * centerRadius,
323 | Math.cos(phi) * centerRadius
324 | );
325 | } else {
326 | // Petal particles
327 | const petalParticleCount = count - centerParticleCount;
328 | const petalIndex = i - centerParticleCount;
329 |
330 | // Determine which petal this particle belongs to
331 | const petalId = petalIndex % numPetals;
332 | const positionInPetal = Math.floor(petalIndex / numPetals) / Math.floor(petalParticleCount / numPetals);
333 |
334 | // Calculate angle of this petal
335 | const petalAngle = (petalId / numPetals) * Math.PI * 2;
336 |
337 | // Calculate radial distance from center
338 | // Use a curve so particles are denser at tip and base
339 | const radialT = Math.pow(positionInPetal, 0.7); // Adjust density along petal
340 | const radialDist = centerRadius + (petalLength * radialT);
341 |
342 | // Calculate width displacement (thicker at base, thinner at tip)
343 | const widthFactor = petalWidth * (1 - radialT * 0.7);
344 | const randomWidth = (Math.random() * 2 - 1) * widthFactor * petalLength;
345 |
346 | // Calculate curve displacement (petals curve outward)
347 | const curveFactor = petalCurve * Math.sin(positionInPetal * Math.PI);
348 |
349 | // Convert to Cartesian coordinates
350 | // Main direction follows the petal angle
351 | const x = Math.cos(petalAngle) * radialDist +
352 | Math.cos(petalAngle + Math.PI/2) * randomWidth;
353 |
354 | const y = Math.sin(petalAngle) * radialDist +
355 | Math.sin(petalAngle + Math.PI/2) * randomWidth;
356 |
357 | // Z coordinate creates the upward curve of petals
358 | const z = curveFactor * petalLength * (1 - Math.cos(positionInPetal * Math.PI));
359 |
360 | return new THREE.Vector3(x, y, z);
361 | }
362 | }
363 |
364 | function createFractalTree(i, count) {
365 | // Fractal Tree parameters
366 | const trunkLength = 35; // Initial trunk length
367 | const branchRatio = 0.67; // Each branch is this ratio of parent length
368 | const maxDepth = 6; // Maximum branching depth
369 | const branchAngle = Math.PI / 5; // Angle between branches (36 degrees)
370 |
371 | // Pre-calculate the total particles needed per depth level
372 | // Distribute particles more towards deeper levels
373 | const particlesPerLevel = [];
374 | let totalWeight = 0;
375 |
376 | for (let depth = 0; depth <= maxDepth; depth++) {
377 | // More branches at deeper levels, distribute particles accordingly
378 | // Each level has 2^depth branches
379 | const branches = Math.pow(2, depth);
380 | const weight = branches * Math.pow(branchRatio, depth);
381 | totalWeight += weight;
382 | particlesPerLevel.push(weight);
383 | }
384 |
385 | // Normalize to get actual count per level
386 | let cumulativeCount = 0;
387 | const particleCount = [];
388 |
389 | for (let depth = 0; depth <= maxDepth; depth++) {
390 | const levelCount = Math.floor((particlesPerLevel[depth] / totalWeight) * count);
391 | particleCount.push(levelCount);
392 | cumulativeCount += levelCount;
393 | }
394 |
395 | // Adjust the last level to ensure we use exactly count particles
396 | particleCount[maxDepth] += (count - cumulativeCount);
397 |
398 | // Determine which depth level this particle belongs to
399 | let depth = 0;
400 | let levelStartIndex = 0;
401 |
402 | while (depth < maxDepth && i >= levelStartIndex + particleCount[depth]) {
403 | levelStartIndex += particleCount[depth];
404 | depth++;
405 | }
406 |
407 | // Calculate the relative index within this depth level
408 | const indexInLevel = i - levelStartIndex;
409 | const levelCount = particleCount[depth];
410 |
411 | // Calculate position parameters
412 | const t = indexInLevel / (levelCount || 1); // Normalized position in level
413 |
414 | // For the trunk (depth 0)
415 | if (depth === 0) {
416 | // Simple line for the trunk
417 | return new THREE.Vector3(
418 | (Math.random() * 2 - 1) * 0.5, // Small random spread for thickness
419 | -trunkLength / 2 + t * trunkLength,
420 | (Math.random() * 2 - 1) * 0.5 // Small random spread for thickness
421 | );
422 | }
423 |
424 | // For branches at higher depths
425 | // Determine which branch in the current depth
426 | const branchCount = Math.pow(2, depth);
427 | const branchIndex = Math.floor(t * branchCount) % branchCount;
428 | const positionInBranch = (t * branchCount) % 1;
429 |
430 | // Calculate the position based on branch path
431 | let x = 0, y = trunkLength / 2, z = 0; // Start at top of trunk
432 | let currentLength = trunkLength;
433 | let currentAngle = 0;
434 |
435 | // For the first depth level (branching from trunk)
436 | if (depth >= 1) {
437 | currentLength *= branchRatio;
438 | // Determine left or right branch
439 | currentAngle = (branchIndex % 2 === 0) ? branchAngle : -branchAngle;
440 |
441 | // Move up the branch
442 | x += Math.sin(currentAngle) * currentLength * positionInBranch;
443 | y += Math.cos(currentAngle) * currentLength * positionInBranch;
444 | }
445 |
446 | // For higher depths, calculate the full path
447 | for (let d = 2; d <= depth; d++) {
448 | currentLength *= branchRatio;
449 |
450 | // Determine branch direction at this depth
451 | // Use bit operations to determine left vs right at each branch
452 | const pathBit = (branchIndex >> (depth - d)) & 1;
453 | const nextAngle = pathBit === 0 ? branchAngle : -branchAngle;
454 |
455 | // Only apply movement for the branches we've completed
456 | if (d < depth) {
457 | // Rotate the current direction and move full branch length
458 | currentAngle += nextAngle;
459 | x += Math.sin(currentAngle) * currentLength;
460 | y += Math.cos(currentAngle) * currentLength;
461 | } else {
462 | // For the final branch, move partially based on positionInBranch
463 | currentAngle += nextAngle;
464 | x += Math.sin(currentAngle) * currentLength * positionInBranch;
465 | y += Math.cos(currentAngle) * currentLength * positionInBranch;
466 | }
467 | }
468 |
469 | // Add small random offsets for volume
470 | const randomSpread = 0.8 * (1 - Math.pow(branchRatio, depth));
471 | x += (Math.random() * 2 - 1) * randomSpread;
472 | z += (Math.random() * 2 - 1) * randomSpread;
473 |
474 | return new THREE.Vector3(x, y, z);
475 | }
476 |
477 | function createVoronoi(i, count) {
478 | // Voronoi parameters
479 | const radius = 30; // Maximum radius of the sphere to place points on
480 | const numSites = 25; // Number of Voronoi sites (cells)
481 | const cellThickness = 2.5; // Thickness of the cell boundaries
482 | const jitter = 0.5; // Random jitter to make edges look more natural
483 |
484 | // First, we generate fixed pseudorandom Voronoi sites (cell centers)
485 | // We use a deterministic approach to ensure sites are the same for each call
486 | const sites = [];
487 | for (let s = 0; s < numSites; s++) {
488 | // Use a specific seed formula for each site to ensure repeatability
489 | const seed1 = Math.sin(s * 42.5) * 10000;
490 | const seed2 = Math.cos(s * 15.3) * 10000;
491 | const seed3 = Math.sin(s * 33.7) * 10000;
492 |
493 | // Generate points on a sphere using spherical coordinates
494 | const theta = 2 * Math.PI * (seed1 - Math.floor(seed1));
495 | const phi = Math.acos(2 * (seed2 - Math.floor(seed2)) - 1);
496 |
497 | sites.push(new THREE.Vector3(
498 | Math.sin(phi) * Math.cos(theta) * radius,
499 | Math.sin(phi) * Math.sin(theta) * radius,
500 | Math.cos(phi) * radius
501 | ));
502 | }
503 |
504 | // Now we generate points that lie primarily along the boundaries between Voronoi cells
505 |
506 | // First, decide if this is a site point (center of a cell) or a boundary point
507 | const sitePoints = Math.min(numSites, Math.floor(count * 0.1)); // 10% of points are sites
508 |
509 | if (i < sitePoints) {
510 | // Place this point at a Voronoi site center
511 | const siteIndex = i % sites.length;
512 | const site = sites[siteIndex];
513 |
514 | // Return the site position with small random variation
515 | return new THREE.Vector3(
516 | site.x + (Math.random() * 2 - 1) * jitter,
517 | site.y + (Math.random() * 2 - 1) * jitter,
518 | site.z + (Math.random() * 2 - 1) * jitter
519 | );
520 | } else {
521 | // This is a boundary point
522 | // Generate a random point on the sphere
523 | const u = Math.random();
524 | const v = Math.random();
525 | const theta = 2 * Math.PI * u;
526 | const phi = Math.acos(2 * v - 1);
527 |
528 | const point = new THREE.Vector3(
529 | Math.sin(phi) * Math.cos(theta) * radius,
530 | Math.sin(phi) * Math.sin(theta) * radius,
531 | Math.cos(phi) * radius
532 | );
533 |
534 | // Find the two closest sites to this point
535 | let closestDist = Infinity;
536 | let secondClosestDist = Infinity;
537 | let closestSite = null;
538 | let secondClosestSite = null;
539 |
540 | for (const site of sites) {
541 | const dist = point.distanceTo(site);
542 |
543 | if (dist < closestDist) {
544 | secondClosestDist = closestDist;
545 | secondClosestSite = closestSite;
546 | closestDist = dist;
547 | closestSite = site;
548 | } else if (dist < secondClosestDist) {
549 | secondClosestDist = dist;
550 | secondClosestSite = site;
551 | }
552 | }
553 |
554 | // Check if this point is near the boundary between the two closest cells
555 | const distDiff = Math.abs(closestDist - secondClosestDist);
556 |
557 | if (distDiff < cellThickness) {
558 | // This point is on a boundary
559 |
560 | // Add small random jitter to make the boundary look more natural
561 | point.x += (Math.random() * 2 - 1) * jitter;
562 | point.y += (Math.random() * 2 - 1) * jitter;
563 | point.z += (Math.random() * 2 - 1) * jitter;
564 |
565 | // Project the point back onto the sphere
566 | point.normalize().multiplyScalar(radius);
567 |
568 | return point;
569 | } else {
570 | // Not a boundary point, retry with a different approach
571 | // Move the point slightly toward the boundary
572 | const midpoint = new THREE.Vector3().addVectors(closestSite, secondClosestSite).multiplyScalar(0.5);
573 | const dirToMid = new THREE.Vector3().subVectors(midpoint, point).normalize();
574 |
575 | // Move point toward the midpoint between cells
576 | point.add(dirToMid.multiplyScalar(distDiff * 0.7));
577 |
578 | // Add small random jitter
579 | point.x += (Math.random() * 2 - 1) * jitter;
580 | point.y += (Math.random() * 2 - 1) * jitter;
581 | point.z += (Math.random() * 2 - 1) * jitter;
582 |
583 | // Project back onto the sphere
584 | point.normalize().multiplyScalar(radius);
585 |
586 | return point;
587 | }
588 | }
589 | }
590 |
591 | function createParticleTexture() {
592 | const canvas = document.createElement('canvas');
593 | canvas.width = 64;
594 | canvas.height = 64;
595 |
596 | const context = canvas.getContext('2d');
597 | const gradient = context.createRadialGradient(
598 | canvas.width / 2,
599 | canvas.height / 2,
600 | 0,
601 | canvas.width / 2,
602 | canvas.height / 2,
603 | canvas.width / 2
604 | );
605 |
606 | gradient.addColorStop(0, 'rgba(255,255,255,1)');
607 | gradient.addColorStop(0.2, 'rgba(255,255,255,0.8)');
608 | gradient.addColorStop(0.4, 'rgba(255,255,255,0.4)');
609 | gradient.addColorStop(1, 'rgba(255,255,255,0)');
610 |
611 | context.fillStyle = gradient;
612 | context.fillRect(0, 0, canvas.width, canvas.height);
613 |
614 | const texture = new THREE.Texture(canvas);
615 | texture.needsUpdate = true;
616 | return texture;
617 | }
618 |
619 | // --- COLOR PALETTES ---
620 | const colorPalettes = [
621 | [ new THREE.Color(0x3399ff), new THREE.Color(0x44ccff), new THREE.Color(0x0055cc) ],
622 | [ new THREE.Color(0xff3399), new THREE.Color(0xcc00ff), new THREE.Color(0x660099), new THREE.Color(0xaa33ff) ],
623 | [ new THREE.Color(0x33ff99), new THREE.Color(0x33ff99), new THREE.Color(0x99ff66), new THREE.Color(0x008844) ],
624 | [ new THREE.Color(0xff9933), new THREE.Color(0xffcc33), new THREE.Color(0xff6600), new THREE.Color(0xffaa55) ],
625 | [ new THREE.Color(0x9933ff), new THREE.Color(0xff66aa), new THREE.Color(0xff0066), new THREE.Color(0xcc0055) ]
626 | ];
627 |
628 | // --- PARTICLE SYSTEM ---
629 | function createParticleSystem() {
630 | const geometry = new THREE.BufferGeometry();
631 | const positions = new Float32Array(params.particleCount * 3);
632 | const colors = new Float32Array(params.particleCount * 3);
633 | const sizes = new Float32Array(params.particleCount);
634 |
635 | const initialPattern = patterns[0];
636 | const initialPalette = colorPalettes[0];
637 |
638 | for (let i = 0; i < params.particleCount; i++) {
639 |
640 | const pos = initialPattern(i, params.particleCount);
641 | positions[i * 3] = pos.x;
642 | positions[i * 3 + 1] = pos.y;
643 | positions[i * 3 + 2] = pos.z;
644 |
645 | const baseColor = initialPalette[0];
646 | const variation = 1.0; // Add variation
647 |
648 | colors[i * 3] = baseColor.r * variation;
649 | colors[i * 3 + 1] = baseColor.g * variation;
650 | colors[i * 3 + 2] = baseColor.b * variation;
651 |
652 | sizes[i] = 1.0; // Assign individual size variation
653 | }
654 |
655 | geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
656 | geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
657 | geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1)); // Store base sizes
658 | geometry.userData.currentColors = new Float32Array(colors); // Store initial colors for transitions
659 |
660 | const material = new THREE.PointsMaterial({
661 | size: params.particleSize,
662 | vertexColors: true,
663 | transparent: true,
664 | opacity: 0.5,
665 | blending: THREE.AdditiveBlending,
666 | sizeAttenuation: true, // Make distant particles smaller
667 |
668 | //map: createParticleTexture()
669 | // depthWrite: false // Often needed with AdditiveBlending if particles overlap strangely
670 | });
671 | return new THREE.Points(geometry, material);
672 |
673 | }
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |