@joshtol/emotive-engine 3.2.1 → 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/dist/emotive-mascot-3d.js +1 -1
- package/dist/emotive-mascot-3d.js.map +1 -1
- package/dist/emotive-mascot-3d.umd.js +1 -1
- package/dist/emotive-mascot-3d.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/3d/Core3DManager.js +111 -308
- package/src/3d/ThreeRenderer.js +95 -10
- package/src/3d/effects/CrystalSoul.js +10 -16
- package/src/3d/effects/SolarEclipse.js +6 -8
- package/src/3d/geometries/Moon.js +3 -3
- package/src/3d/index.js +24 -8
- package/src/3d/managers/AnimationManager.js +269 -0
- package/src/3d/managers/BehaviorController.js +248 -0
- package/src/3d/managers/BreathingPhaseManager.js +163 -0
- package/src/3d/managers/CameraPresetManager.js +182 -0
- package/src/3d/managers/EffectManager.js +385 -0
- package/src/3d/utils/MaterialFactory.js +6 -5
- package/types/index.d.ts +207 -11
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@joshtol/emotive-engine",
|
|
4
|
-
"version": "3.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",
|
package/src/3d/Core3DManager.js
CHANGED
|
@@ -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
|
|
208
|
-
//
|
|
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
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
//
|
|
739
|
-
|
|
740
|
-
|
|
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
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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.
|
|
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
|
|
798
|
-
this.
|
|
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.
|
|
812
|
-
this.
|
|
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.
|
|
823
|
-
this.
|
|
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.
|
|
839
|
-
this.
|
|
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.
|
|
849
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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
|
|
877
|
-
this.
|
|
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
|
-
//
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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.
|
|
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}
|
|
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(
|
|
1322
|
-
//
|
|
1323
|
-
|
|
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
|
-
//
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
}
|
|
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.
|
|
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.
|
|
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.
|
|
1876
|
-
this.
|
|
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.
|
|
1778
|
+
hasActiveGesture: this.animationManager.hasActiveAnimations(), // Faster lerp during gestures
|
|
1978
1779
|
calibrationRotation: this.calibrationRotation, // Applied on top of animated rotation
|
|
1979
|
-
solarEclipse: this.
|
|
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
|
-
|
|
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
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
this.
|
|
2237
|
-
this.
|
|
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
|
|
2241
|
-
if (this.
|
|
2242
|
-
this.
|
|
2243
|
-
this.
|
|
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.
|
|
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
|
-
|
|
2267
|
-
|
|
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?.();
|