@joshtol/emotive-engine 3.2.2 → 3.2.3

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@joshtol/emotive-engine",
4
- "version": "3.2.2",
4
+ "version": "3.2.3",
5
5
  "description": "Open-source animation engine for AI-controlled emotional visualizations with musical time synchronization",
6
6
  "main": "dist/emotive-mascot.umd.js",
7
7
  "module": "dist/mascot.js",
@@ -33,12 +33,14 @@ import { Particle3DTranslator } from './particles/Particle3DTranslator.js';
33
33
  import { CrystalSoul } from './effects/CrystalSoul.js';
34
34
  import { Particle3DRenderer } from './particles/Particle3DRenderer.js';
35
35
  import { Particle3DOrchestrator } from './particles/Particle3DOrchestrator.js';
36
- import { SolarEclipse } from './effects/SolarEclipse.js';
37
- import { LunarEclipse } from './effects/LunarEclipse.js';
38
36
  import { updateMoonGlow, MOON_CALIBRATION_ROTATION, MOON_FACING_CONFIG } from './geometries/Moon.js';
39
37
  import { createCustomMaterial, disposeCustomMaterial } from './utils/MaterialFactory.js';
40
38
  import { resetGeometryState } from './GeometryStateManager.js';
41
39
  import * as GeometryCache from './utils/GeometryCache.js';
40
+ import { AnimationManager } from './managers/AnimationManager.js';
41
+ import { EffectManager } from './managers/EffectManager.js';
42
+ import { BehaviorController } from './managers/BehaviorController.js';
43
+ import { BreathingPhaseManager } from './managers/BreathingPhaseManager.js';
42
44
 
43
45
  // Crystal calibration rotation to show flat facet facing camera
44
46
  // Hexagonal crystal has vertices at 0°, 60°, 120°, etc.
@@ -200,21 +202,30 @@ export class Core3DManager {
200
202
  // Animation controller
201
203
  this.animator = new ProceduralAnimator();
202
204
 
205
+ // Gesture blender
206
+ this.gestureBlender = new GestureBlender();
207
+
208
+ // Animation manager (orchestrates gesture playback and blending)
209
+ this.animationManager = new AnimationManager(this.animator, this.gestureBlender);
210
+
211
+ // Effect manager (manages SolarEclipse, LunarEclipse, CrystalSoul effects)
212
+ this.effectManager = new EffectManager(this.renderer, this.assetBasePath);
213
+
214
+ // Behavior controller (manages rotation, righting, and facing behaviors)
215
+ this.behaviorController = new BehaviorController({
216
+ rotationDisabled: options.autoRotate === false,
217
+ wobbleEnabled: true
218
+ });
219
+
220
+ // Breathing phase manager (imperative meditation-style breathing control)
221
+ this.breathingPhaseManager = new BreathingPhaseManager();
222
+
203
223
  // Breathing animator
204
224
  this.breathingAnimator = new BreathingAnimator();
205
225
  this.breathingEnabled = options.enableBreathing !== false; // Enabled by default
206
226
 
207
- // Imperative breathing phase animation (for meditation)
208
- // Allows explicit control: breathePhase('inhale', 4) to animate scale over 4 seconds
209
- this._breathPhase = null; // 'inhale' | 'hold' | 'exhale' | null
210
- this._breathPhaseStartTime = 0;
211
- this._breathPhaseDuration = 0;
212
- this._breathPhaseStartScale = 1.0;
213
- this._breathPhaseTargetScale = 1.0;
214
- this._breathPhaseScale = 1.0; // Current animated scale (1.0 = normal)
215
-
216
- // Gesture blender
217
- this.gestureBlender = new GestureBlender();
227
+ // Note: Imperative breathing phase animation state is now managed by BreathingPhaseManager
228
+ // See: breathePhase(), stopBreathingPhase(), _updateBreathingPhase()
218
229
 
219
230
  // Geometry morpher for smooth shape transitions
220
231
  this.geometryMorpher = new GeometryMorpher();
@@ -346,20 +357,15 @@ export class Core3DManager {
346
357
  particleRenderer.geometry.setDrawRange(0, 0);
347
358
  }
348
359
 
349
- // Initialize solar eclipse system for sun geometry
350
- if (this.geometryType === 'sun') {
351
- const sunRadius = this.geometry.parameters?.radius || 0.5; // Sun geometry radius is 0.5
352
- this.solarEclipse = new SolarEclipse(this.renderer.scene, sunRadius, this.coreMesh);
353
- }
354
-
355
- // Initialize lunar eclipse system for moon geometry
356
- if (this.geometryType === 'moon' && this.customMaterial) {
357
- this.lunarEclipse = new LunarEclipse(this.customMaterial);
358
- }
360
+ // Initialize geometry-specific effects via EffectManager
361
+ const sunRadius = this.geometry?.parameters?.radius || 0.5;
362
+ this.effectManager.initializeForGeometry(this.geometryType, {
363
+ coreMesh: this.coreMesh,
364
+ customMaterial: this.customMaterial,
365
+ sunRadius
366
+ });
359
367
 
360
- // Virtual particle object pool for gesture animations (prevent closure memory leaks)
361
- this.virtualParticlePool = this.createVirtualParticlePool(5); // Pool of 5 reusable particles
362
- this.nextPoolIndex = 0;
368
+ // Note: Virtual particle pool is now managed by AnimationManager
363
369
 
364
370
  // Apply default glass mode for initial geometry (if specified)
365
371
  // Crystal and diamond geometries have defaultGlassMode: true
@@ -372,50 +378,8 @@ export class Core3DManager {
372
378
  this.setEmotion(this.emotion);
373
379
  }
374
380
 
375
- /**
376
- * Create reusable virtual particle object pool
377
- * @param {number} size - Pool size
378
- * @returns {Array} Array of reusable particle objects
379
- */
380
- createVirtualParticlePool(size) {
381
- const pool = [];
382
- for (let i = 0; i < size; i++) {
383
- pool.push({
384
- x: 0,
385
- y: 0,
386
- vx: 0,
387
- vy: 0,
388
- size: 1,
389
- baseSize: 1,
390
- opacity: 1,
391
- scaleFactor: 1,
392
- gestureData: null
393
- });
394
- }
395
- return pool;
396
- }
397
-
398
- /**
399
- * Get next virtual particle from pool (round-robin)
400
- * @returns {Object} Reusable virtual particle object
401
- */
402
- getVirtualParticleFromPool() {
403
- const particle = this.virtualParticlePool[this.nextPoolIndex];
404
- this.nextPoolIndex = (this.nextPoolIndex + 1) % this.virtualParticlePool.length;
405
-
406
- // Reset particle to default state
407
- particle.x = 0;
408
- particle.y = 0;
409
- particle.vx = 0;
410
- particle.vy = 0;
411
- particle.size = 1;
412
- particle.baseSize = 1;
413
- particle.opacity = 1;
414
- particle.scaleFactor = 1;
415
- particle.gestureData = null;
416
-
417
- return particle;
418
- }
381
+ // Note: createVirtualParticlePool and getVirtualParticleFromPool
382
+ // have been moved to AnimationManager
419
383
 
420
384
  /**
421
385
  * Set emotional state
@@ -671,115 +635,29 @@ export class Core3DManager {
671
635
 
672
636
  /**
673
637
  * Play gesture animation using 2D gesture data translated to 3D
638
+ * Delegates to AnimationManager for gesture orchestration
674
639
  */
675
640
  playGesture(gestureName) {
676
- // Get the 2D gesture definition
677
- const gesture2D = getGesture(gestureName);
678
-
679
- if (!gesture2D) {
680
- console.warn(`Unknown gesture: ${gestureName}`);
681
- return;
682
- }
683
-
684
- // Get reusable virtual particle from pool (prevent closure memory leaks)
685
- const virtualParticle = this.getVirtualParticleFromPool();
686
-
687
- // Get gesture config for duration
688
- const config = gesture2D.config || {};
689
- const duration = config.musicalDuration?.musical
690
- ? (config.musicalDuration.beats || 2) * 500 // Assume 120 BPM (500ms per beat)
691
- : (config.duration || 800);
692
-
693
- // Start time-based animation
694
- const startTime = this.animator.time;
695
-
696
- const gestureState = {
697
- virtualParticle,
698
- gesture: gesture2D,
699
- duration,
700
- startTime,
701
- startPosition: [...this.position],
702
- startRotation: [...this.rotation],
703
- startScale: this.scale
704
- };
705
-
706
- // Enforce animation array size limit (prevent unbounded growth memory leak)
707
- const MAX_ACTIVE_ANIMATIONS = 10;
708
- if (this.animator.animations.length >= MAX_ACTIVE_ANIMATIONS) {
709
- // Remove oldest animation (FIFO cleanup)
710
- const removed = this.animator.animations.shift();
711
- console.warn(`⚠️ Animation limit reached (${MAX_ACTIVE_ANIMATIONS}), removed oldest: ${removed.gestureName || 'unknown'}`);
712
- }
713
-
714
- // Add to animator's active animations
715
- // Create persistent gesture data object for this gesture instance
716
- const gestureData = { initialized: false };
717
-
718
- this.animator.animations.push({
719
- gestureName, // Store gesture name for particle system
720
- duration,
721
- startTime,
722
- config, // Store config for particle system
723
- evaluate: t => {
724
- // Reset virtual particle to center each frame
725
- virtualParticle.x = 0;
726
- virtualParticle.y = 0;
727
- virtualParticle.vx = 0;
728
- virtualParticle.vy = 0;
729
- virtualParticle.size = 1;
730
- virtualParticle.opacity = 1;
731
-
732
- // All gestures now have native 3D implementations
733
- // Apply gesture to virtual particle if needed
734
- if (gesture2D.apply) {
735
- gesture2D.apply(virtualParticle, gestureData, config, t, 1.0, 0, 0);
641
+ this.animationManager.playGesture(gestureName, {
642
+ onUpdate: (props, _progress) => {
643
+ if (props.position) this.position = props.position;
644
+ if (props.rotation) {
645
+ // Convert gesture Euler rotation to quaternion
646
+ this.tempEuler.set(props.rotation[0], props.rotation[1], props.rotation[2], 'XYZ');
647
+ this.gestureQuaternion.setFromEuler(this.tempEuler);
736
648
  }
737
-
738
- // Call gesture's 3D evaluate function with particle data
739
- const motion = {
740
- ...config,
741
- particle: virtualParticle,
742
- config,
743
- strength: config.strength || 1.0
744
- };
745
-
746
- // Safety check: if gesture doesn't have 3D implementation, return neutral transform
747
- if (!gesture2D['3d'] || !gesture2D['3d'].evaluate) {
748
- return {
749
- position: [0, 0, 0],
750
- rotation: [0, 0, 0],
751
- scale: 1.0
752
- };
649
+ if (props.scale !== undefined) this.scale = this.baseScale * props.scale;
650
+ // Apply glow intensity as multiplier on base intensity (not absolute override)
651
+ if (props.glowIntensity !== undefined) {
652
+ this.glowIntensity = this.baseGlowIntensity * props.glowIntensity;
753
653
  }
754
-
755
- // Call with gesture2D as context so 'this.config' works
756
- return gesture2D['3d'].evaluate.call(gesture2D, t, motion);
757
654
  },
758
- callbacks: {
759
- onUpdate: (props, _progress) => {
760
- if (props.position) this.position = props.position;
761
- if (props.rotation) {
762
- // Convert gesture Euler rotation to quaternion
763
- this.tempEuler.set(props.rotation[0], props.rotation[1], props.rotation[2], 'XYZ');
764
- this.gestureQuaternion.setFromEuler(this.tempEuler);
765
- }
766
- if (props.scale !== undefined) this.scale = this.baseScale * props.scale;
767
- // Apply glow intensity as multiplier on base intensity (not absolute override)
768
- if (props.glowIntensity !== undefined) {
769
- this.glowIntensity = this.baseGlowIntensity * props.glowIntensity;
770
- }
771
- },
772
- onComplete: () => {
773
- // Clean up gesture
774
- if (gesture2D.cleanup) {
775
- gesture2D.cleanup(virtualParticle);
776
- }
777
- // Reset to base state
778
- this.position = [0, 0, 0];
779
- // NOTE: Don't reset rotation - it's computed from quaternions in render()
780
- // gestureQuaternion will be reset to identity in render() when no gestures active
781
- this.scale = this.baseScale;
782
- }
655
+ onComplete: () => {
656
+ // Reset to base state
657
+ this.position = [0, 0, 0];
658
+ // NOTE: Don't reset rotation - it's computed from quaternions in render()
659
+ // gestureQuaternion will be reset to identity in render() when no gestures active
660
+ this.scale = this.baseScale;
783
661
  }
784
662
  });
785
663
  }
@@ -789,13 +667,13 @@ export class Core3DManager {
789
667
  * @param {string} eclipseType - Eclipse type: 'off', 'annular', or 'total'
790
668
  */
791
669
  setSunShadow(eclipseType = 'off') {
792
- if (this.geometryType !== 'sun' || !this.solarEclipse) {
670
+ if (this.geometryType !== 'sun' || !this.effectManager.hasSolarEclipse()) {
793
671
  console.warn('⚠️ Eclipse only available for sun geometry');
794
672
  return;
795
673
  }
796
674
 
797
- // Set eclipse type on the solar eclipse manager
798
- this.solarEclipse.setEclipseType(eclipseType);
675
+ // Set eclipse type via EffectManager
676
+ this.effectManager.setSolarEclipse(eclipseType);
799
677
  }
800
678
 
801
679
  /**
@@ -808,8 +686,8 @@ export class Core3DManager {
808
686
  const eclipseType = options.type || 'total';
809
687
 
810
688
  // If already on sun, just trigger eclipse
811
- if (this.geometryType === 'sun' && this.solarEclipse) {
812
- this.solarEclipse.setEclipseType(eclipseType);
689
+ if (this.geometryType === 'sun' && this.effectManager.hasSolarEclipse()) {
690
+ this.effectManager.setSolarEclipse(eclipseType);
813
691
  return;
814
692
  }
815
693
 
@@ -819,8 +697,8 @@ export class Core3DManager {
819
697
  // Wait for morph to complete (shrink + grow phases)
820
698
  // Default morph duration is 500ms, so wait a bit longer
821
699
  setTimeout(() => {
822
- if (this.solarEclipse) {
823
- this.solarEclipse.setEclipseType(eclipseType);
700
+ if (this.effectManager.hasSolarEclipse()) {
701
+ this.effectManager.setSolarEclipse(eclipseType);
824
702
  }
825
703
  }, 600);
826
704
  }
@@ -835,8 +713,8 @@ export class Core3DManager {
835
713
  const eclipseType = options.type || 'total';
836
714
 
837
715
  // If already on moon, just trigger eclipse
838
- if (this.geometryType === 'moon' && this.lunarEclipse) {
839
- this.lunarEclipse.setEclipseType(eclipseType);
716
+ if (this.geometryType === 'moon' && this.effectManager.hasLunarEclipse()) {
717
+ this.effectManager.setLunarEclipse(eclipseType);
840
718
  return;
841
719
  }
842
720
 
@@ -845,8 +723,8 @@ export class Core3DManager {
845
723
 
846
724
  // Wait for morph to complete (shrink + grow phases)
847
725
  setTimeout(() => {
848
- if (this.lunarEclipse) {
849
- this.lunarEclipse.setEclipseType(eclipseType);
726
+ if (this.effectManager.hasLunarEclipse()) {
727
+ this.effectManager.setLunarEclipse(eclipseType);
850
728
  }
851
729
  }, 600);
852
730
  }
@@ -855,12 +733,7 @@ export class Core3DManager {
855
733
  * Stop any active eclipse animation
856
734
  */
857
735
  stopEclipse() {
858
- if (this.solarEclipse) {
859
- this.solarEclipse.setEclipseType('off');
860
- }
861
- if (this.lunarEclipse) {
862
- this.lunarEclipse.setEclipseType('off');
863
- }
736
+ this.effectManager.stopAllEclipses();
864
737
  }
865
738
 
866
739
  /**
@@ -868,13 +741,13 @@ export class Core3DManager {
868
741
  * @param {string} eclipseType - 'off', 'penumbral', 'partial', 'total'
869
742
  */
870
743
  setMoonEclipse(eclipseType = 'off') {
871
- if (this.geometryType !== 'moon' || !this.lunarEclipse) {
744
+ if (this.geometryType !== 'moon' || !this.effectManager.hasLunarEclipse()) {
872
745
  console.warn('⚠️ Lunar eclipse only available for moon geometry');
873
746
  return;
874
747
  }
875
748
 
876
- // Set eclipse type on the lunar eclipse manager
877
- this.lunarEclipse.setEclipseType(eclipseType);
749
+ // Set eclipse type via EffectManager
750
+ this.effectManager.setLunarEclipse(eclipseType);
878
751
  }
879
752
 
880
753
  /**
@@ -1273,41 +1146,17 @@ export class Core3DManager {
1273
1146
  * @param {number} durationSec - Duration in seconds for the animation
1274
1147
  */
1275
1148
  breathePhase(phase, durationSec) {
1276
- // Clamp duration to reasonable values (0.5s to 30s)
1277
- const duration = Math.max(0.5, Math.min(30, durationSec));
1278
-
1279
- // Store current scale as starting point
1280
- this._breathPhaseStartScale = this._breathPhaseScale;
1281
- this._breathPhaseStartTime = performance.now();
1282
- this._breathPhaseDuration = duration * 1000; // Convert to ms
1283
- this._breathPhase = phase;
1284
-
1285
- // Set target scale based on phase
1286
- switch (phase) {
1287
- case 'inhale':
1288
- this._breathPhaseTargetScale = 1.3; // Max inhale size
1289
- break;
1290
- case 'exhale':
1291
- this._breathPhaseTargetScale = 0.85; // Min exhale size
1292
- break;
1293
- case 'hold':
1294
- default:
1295
- // Hold at current scale - no animation needed
1296
- this._breathPhaseTargetScale = this._breathPhaseStartScale;
1297
- break;
1298
- }
1299
-
1300
- console.log(`[Core3D] breathePhase: ${phase} for ${duration}s (${this._breathPhaseStartScale.toFixed(2)} → ${this._breathPhaseTargetScale.toFixed(2)})`);
1149
+ // Delegate to BreathingPhaseManager
1150
+ this.breathingPhaseManager.startPhase(phase, durationSec);
1151
+ const state = this.breathingPhaseManager.getState();
1152
+ console.log(`[Core3D] breathePhase: ${phase} for ${durationSec}s (${state.startScale.toFixed(2)} → ${state.targetScale.toFixed(2)})`);
1301
1153
  }
1302
1154
 
1303
1155
  /**
1304
1156
  * Stop any active breathing phase animation and reset to neutral scale
1305
1157
  */
1306
1158
  stopBreathingPhase() {
1307
- this._breathPhase = null;
1308
- this._breathPhaseScale = 1.0;
1309
- this._breathPhaseStartScale = 1.0;
1310
- this._breathPhaseTargetScale = 1.0;
1159
+ this.breathingPhaseManager.stop();
1311
1160
  console.log('[Core3D] breathePhase stopped, scale reset to 1.0');
1312
1161
  }
1313
1162
 
@@ -1315,38 +1164,12 @@ export class Core3DManager {
1315
1164
  * Update imperative breathing phase animation
1316
1165
  * Called from render loop
1317
1166
  * @private
1318
- * @param {number} _deltaTime - Time since last frame in ms (unused, we use elapsed time)
1167
+ * @param {number} deltaTime - Time since last frame in ms
1319
1168
  * @returns {number} Current breathing phase scale multiplier (1.0 if inactive)
1320
1169
  */
1321
- _updateBreathingPhase(_deltaTime) {
1322
- // If no active phase, return neutral scale
1323
- if (!this._breathPhase) {
1324
- return this._breathPhaseScale;
1325
- }
1326
-
1327
- const now = performance.now();
1328
- const elapsed = now - this._breathPhaseStartTime;
1329
- const duration = this._breathPhaseDuration;
1330
-
1331
- // Calculate progress (0 to 1)
1332
- const progress = Math.min(1.0, elapsed / duration);
1333
-
1334
- // Use sine easing for natural breathing rhythm
1335
- // sin(0 to π/2) maps 0→1 smoothly, reaches target exactly at end
1336
- // This feels more like natural breathing than cubic easing
1337
- const eased = Math.sin(progress * Math.PI / 2);
1338
-
1339
- // Interpolate between start and target scale
1340
- this._breathPhaseScale = this._breathPhaseStartScale +
1341
- (this._breathPhaseTargetScale - this._breathPhaseStartScale) * eased;
1342
-
1343
- // Clear phase when complete
1344
- if (progress >= 1.0) {
1345
- this._breathPhaseScale = this._breathPhaseTargetScale;
1346
- this._breathPhase = null;
1347
- }
1348
-
1349
- return this._breathPhaseScale;
1170
+ _updateBreathingPhase(deltaTime) {
1171
+ // Delegate to BreathingPhaseManager
1172
+ return this.breathingPhaseManager.update(deltaTime);
1350
1173
  }
1351
1174
 
1352
1175
  /**
@@ -1553,36 +1376,16 @@ export class Core3DManager {
1553
1376
  // Reset Euler angles to upright [pitch=0, yaw=0, roll=0]
1554
1377
  this.rotation = [0, 0, 0];
1555
1378
 
1556
- // Dispose or create solar eclipse for sun geometry
1557
- if (this._targetGeometryType === 'sun') {
1558
- // Create solar eclipse if morphing to sun
1559
- if (!this.solarEclipse) {
1560
- const sunRadius = this.geometry.parameters?.radius || 0.5; // Sun geometry radius is 0.5
1561
- this.solarEclipse = new SolarEclipse(this.renderer.scene, sunRadius, this.renderer.coreMesh);
1562
- }
1563
- } else {
1564
- // Dispose solar eclipse if morphing away from sun
1565
- if (this.solarEclipse) {
1566
- this.solarEclipse.dispose();
1567
- this.solarEclipse = null;
1568
- }
1569
- }
1570
-
1571
- // Dispose or create lunar eclipse for moon geometry
1572
- if (this._targetGeometryType === 'moon') {
1573
- // Create lunar eclipse if morphing to moon and custom material exists
1574
- if (!this.lunarEclipse && this.customMaterial) {
1575
- this.lunarEclipse = new LunarEclipse(this.customMaterial);
1576
- }
1577
- } else {
1578
- // Dispose lunar eclipse if morphing away from moon
1579
- if (this.lunarEclipse) {
1580
- this.lunarEclipse.dispose();
1581
- this.lunarEclipse = null;
1582
- }
1583
- }
1379
+ // Initialize effects for target geometry via EffectManager
1380
+ // This automatically disposes effects not needed for the target geometry
1381
+ const sunRadius = this.geometry.parameters?.radius || 0.5;
1382
+ this.effectManager.initializeForGeometry(this._targetGeometryType, {
1383
+ coreMesh: this.renderer.coreMesh,
1384
+ customMaterial: this.customMaterial,
1385
+ sunRadius
1386
+ });
1584
1387
 
1585
- // Create or dispose crystal inner core
1388
+ // Create or dispose crystal inner core (still uses createCrystalInnerCore for now)
1586
1389
  if (this._targetGeometryType === 'crystal' || this._targetGeometryType === 'rough' || this._targetGeometryType === 'heart' || this._targetGeometryType === 'star') {
1587
1390
  // Create inner core if morphing to crystal/rough/heart/star
1588
1391
  if (this.customMaterialType === 'crystal') {
@@ -1770,9 +1573,7 @@ export class Core3DManager {
1770
1573
  // ═══════════════════════════════════════════════════════════════════════════
1771
1574
  // GESTURE BLENDING SYSTEM - Blend multiple simultaneous gestures
1772
1575
  // ═══════════════════════════════════════════════════════════════════════════
1773
- const blended = this.gestureBlender.blend(
1774
- this.animator.animations,
1775
- this.animator.time,
1576
+ const blended = this.animationManager.blend(
1776
1577
  this.baseQuaternion,
1777
1578
  this.baseScale,
1778
1579
  this.baseGlowIntensity
@@ -1786,7 +1587,7 @@ export class Core3DManager {
1786
1587
 
1787
1588
  // Apply blended results with rhythm modulation
1788
1589
  // Position: add groove offset when no active gestures
1789
- const hasActiveGestures = this.animator.animations.length > 0;
1590
+ const hasActiveGestures = this.animationManager.hasActiveAnimations();
1790
1591
  if (rhythmMod && !hasActiveGestures) {
1791
1592
  // Apply ambient groove when idle
1792
1593
  this.position = [
@@ -1872,8 +1673,8 @@ export class Core3DManager {
1872
1673
  deltaTime,
1873
1674
  this.emotion,
1874
1675
  this.undertone,
1875
- this.animator.animations, // Active gestures
1876
- this.animator.time, // Current animation time
1676
+ this.animationManager.getActiveAnimations(), // Active gestures
1677
+ this.animationManager.getTime(), // Current animation time
1877
1678
  { x: this.position[0], y: this.position[1], z: this.position[2] }, // Core position
1878
1679
  { width: this.canvas.width, height: this.canvas.height }, // Canvas size
1879
1680
  // Rotation state for orbital physics
@@ -1974,17 +1775,15 @@ export class Core3DManager {
1974
1775
  glowColor: this.glowColor,
1975
1776
  glowColorHex: this.glowColorHex, // For bloom luminance normalization
1976
1777
  glowIntensity: effectiveGlowIntensity,
1977
- hasActiveGesture: this.animator.animations.length > 0, // Faster lerp during gestures
1778
+ hasActiveGesture: this.animationManager.hasActiveAnimations(), // Faster lerp during gestures
1978
1779
  calibrationRotation: this.calibrationRotation, // Applied on top of animated rotation
1979
- solarEclipse: this.solarEclipse, // Pass eclipse manager for synchronized updates
1780
+ solarEclipse: this.effectManager.getSolarEclipse(), // Pass eclipse manager for synchronized updates
1980
1781
  deltaTime, // Pass deltaTime for eclipse animation
1981
1782
  morphProgress: morphState.isTransitioning ? morphState.visualProgress : null // For corona fade-in
1982
1783
  });
1983
1784
 
1984
1785
  // Update lunar eclipse animation (Blood Moon)
1985
- if (this.lunarEclipse) {
1986
- this.lunarEclipse.update(deltaTime);
1987
- }
1786
+ this.effectManager.updateLunarEclipse(deltaTime);
1988
1787
  }
1989
1788
 
1990
1789
  /**
@@ -2230,17 +2029,23 @@ export class Core3DManager {
2230
2029
  this.particleOrchestrator = null;
2231
2030
  }
2232
2031
 
2233
- // Clean up solar eclipse system (remove from scene before disposing)
2234
- if (this.solarEclipse) {
2235
- // Eclipse cleanup will remove all meshes from scene internally
2236
- this.solarEclipse.dispose();
2237
- this.solarEclipse = null;
2032
+ // Clean up effect manager (handles eclipse and crystal soul disposal)
2033
+ // Note: EffectManager.dispose() removes eclipse meshes from scene internally
2034
+ if (this.effectManager) {
2035
+ this.effectManager.dispose();
2036
+ this.effectManager = null;
2238
2037
  }
2239
2038
 
2240
- // Clean up lunar eclipse system
2241
- if (this.lunarEclipse) {
2242
- this.lunarEclipse.dispose();
2243
- this.lunarEclipse = null;
2039
+ // Clean up behavior controller
2040
+ if (this.behaviorController) {
2041
+ this.behaviorController.dispose();
2042
+ this.behaviorController = null;
2043
+ }
2044
+
2045
+ // Clean up breathing phase manager
2046
+ if (this.breathingPhaseManager) {
2047
+ this.breathingPhaseManager.dispose();
2048
+ this.breathingPhaseManager = null;
2244
2049
  }
2245
2050
 
2246
2051
  // Dispose custom material textures if they exist
@@ -2257,16 +2062,14 @@ export class Core3DManager {
2257
2062
  }
2258
2063
 
2259
2064
  // Stop animations before destroying renderer
2260
- this.animator.stopAll();
2065
+ this.animationManager.stopAll();
2261
2066
 
2262
2067
  // Destroy renderer LAST (after all scene children are cleaned up)
2263
2068
  this.renderer.destroy();
2264
2069
 
2265
- // Clean up virtual particle pool
2266
- if (this.virtualParticlePool) {
2267
- this.virtualParticlePool.length = 0;
2268
- this.virtualParticlePool = null;
2269
- }
2070
+ // Clean up animation manager (includes virtual particle pool)
2071
+ this.animationManager.dispose();
2072
+ this.animationManager = null;
2270
2073
 
2271
2074
  // Clean up animator sub-components
2272
2075
  this.animator.destroy?.();