@@ -259,4 +259,331 @@ function createSupernova(i, count) {
259
259
Math . sin ( phi ) * Math . sin ( theta ) * radius ,
260
260
Math . cos ( phi ) * radius
261
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
+ }
262
589
}
0 commit comments