@joshtol/emotive-engine 3.3.3 → 3.3.5

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.3.3",
4
+ "version": "3.3.5",
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",
@@ -291,6 +291,8 @@ export class Core3DManager {
291
291
  this.glowColor = [1.0, 1.0, 1.0]; // RGB
292
292
  this.glowColorHex = '#FFFFFF'; // Hex color for luminance normalization
293
293
  this.glowIntensity = 1.0;
294
+ // OPTIMIZATION: Cache normalized color to avoid recalculating every frame
295
+ this._normalizedGlowColor = null;
294
296
  this.coreGlowEnabled = true; // Toggle to enable/disable core glow
295
297
  this.glowIntensityOverride = null; // Manual override for testing
296
298
  this.intensityCalibrationOffset = 0; // Universal filter calibration offset
@@ -419,6 +421,9 @@ export class Core3DManager {
419
421
  this.glowColor = rgb;
420
422
  // Store hex color for bloom luminance normalization
421
423
  this.glowColorHex = emotionData.visual.glowColor;
424
+ // OPTIMIZATION: Pre-compute normalized color (avoids recalculating every frame)
425
+ const normalized = normalizeRGBLuminance(rgb, 0.30);
426
+ this._normalizedGlowColor = [normalized.r, normalized.g, normalized.b];
422
427
 
423
428
  // Calculate intensity using universal filter based on color luminance
424
429
  // This ensures consistent visibility across all emotions regardless of color brightness
@@ -1832,11 +1837,9 @@ export class Core3DManager {
1832
1837
  this.customMaterial.uniforms.glowIntensity.value = effectiveGlowIntensity;
1833
1838
  }
1834
1839
 
1835
- // Normalize emotion color for consistent perceived brightness across all emotions
1840
+ // OPTIMIZATION: Use pre-computed normalized color (calculated when emotion changes)
1836
1841
  // This ensures yellow (joy) doesn't wash out the soul while blue (sadness) stays visible
1837
- // IMPORTANT: Use glowColor (RGB), not glowColorHex - glowColor has undertone saturation applied
1838
- const normalized = normalizeRGBLuminance(this.glowColor, 0.30);
1839
- const normalizedColor = [normalized.r, normalized.g, normalized.b];
1842
+ const normalizedColor = this._normalizedGlowColor || [1, 1, 1];
1840
1843
 
1841
1844
  // Update emotion color on outer shell (luminance-normalized)
1842
1845
  this.customMaterial.uniforms.emotionColor.value.setRGB(
@@ -1848,12 +1851,10 @@ export class Core3DManager {
1848
1851
  this.customMaterial.uniforms.blinkIntensity.value = blinkPulse;
1849
1852
  }
1850
1853
  }
1851
- // Update inner core color and animation (also use normalized color)
1854
+ // Update inner core color and animation (also use cached normalized color)
1852
1855
  // Only update if core glow is enabled
1853
- // IMPORTANT: Use glowColor (RGB), not glowColorHex - glowColor has undertone saturation applied
1854
1856
  if (this.coreGlowEnabled) {
1855
- const normalizedCore = normalizeRGBLuminance(this.glowColor, 0.30);
1856
- const normalizedCoreColor = [normalizedCore.r, normalizedCore.g, normalizedCore.b];
1857
+ const normalizedCoreColor = this._normalizedGlowColor || [1, 1, 1];
1857
1858
  this.updateCrystalInnerCore(normalizedCoreColor, deltaTime);
1858
1859
  }
1859
1860
  }
@@ -1878,7 +1879,10 @@ export class Core3DManager {
1878
1879
  calibrationRotation: this.calibrationRotation, // Applied on top of animated rotation
1879
1880
  solarEclipse: this.effectManager.getSolarEclipse(), // Pass eclipse manager for synchronized updates
1880
1881
  deltaTime, // Pass deltaTime for eclipse animation
1881
- morphProgress: morphState.isTransitioning ? morphState.visualProgress : null // For corona fade-in
1882
+ morphProgress: morphState.isTransitioning ? morphState.visualProgress : null, // For corona fade-in
1883
+ // OPTIMIZATION FLAGS: Skip render passes when not needed
1884
+ hasSoul: this.customMaterialType === 'crystal' && this.crystalSoul !== null,
1885
+ hasParticles: this.particleVisibility && this.particleOrchestrator !== null
1882
1886
  });
1883
1887
 
1884
1888
  // Update lunar eclipse animation (Blood Moon)
@@ -143,6 +143,13 @@ export class ThreeRenderer {
143
143
  this._zAxis = new THREE.Vector3(0, 0, 1);
144
144
  this._cameraToMesh = new THREE.Vector3();
145
145
  this._cameraDir = new THREE.Vector3();
146
+
147
+ // OPTIMIZATION: Reusable temp vector for soul position projection (avoids allocation per frame)
148
+ this._soulPosTemp = new THREE.Vector3();
149
+ // OPTIMIZATION: Cached soul mesh reference (avoids scene.traverse every frame)
150
+ this._cachedSoulMesh = null;
151
+ // OPTIMIZATION: Reusable Vector2 for drawing buffer size queries
152
+ this._drawingBufferSize = new THREE.Vector2();
146
153
  }
147
154
 
148
155
  /**
@@ -1270,6 +1277,8 @@ export class ThreeRenderer {
1270
1277
  * @param {number} [params.cameraRoll=0] - Camera-space roll rotation
1271
1278
  * @param {SolarEclipse} [params.solarEclipse=null] - Solar eclipse manager for synchronized updates
1272
1279
  * @param {number} [params.deltaTime=0] - Delta time for eclipse animation (seconds)
1280
+ * @param {boolean} [params.hasSoul=false] - Whether this geometry has a soul layer (optimization)
1281
+ * @param {boolean} [params.hasParticles=true] - Whether particles are enabled (optimization)
1273
1282
  */
1274
1283
  render(params = {}) {
1275
1284
  // Guard against calls after destroy
@@ -1347,7 +1356,9 @@ export class ThreeRenderer {
1347
1356
  cameraRoll = 0, // Camera-space roll rotation applied after all other rotations
1348
1357
  solarEclipse = null, // Solar eclipse manager for synchronized updates
1349
1358
  deltaTime = 0, // Delta time for eclipse animation
1350
- morphProgress = null // Morph progress for corona fade-in (null = no morph, 0-1 = morphing)
1359
+ morphProgress = null, // Morph progress for corona fade-in (null = no morph, 0-1 = morphing)
1360
+ hasSoul = false, // Whether this geometry has a soul layer (skip soul pass if false)
1361
+ hasParticles = true // Whether particles are enabled (skip particle pass if false)
1351
1362
  } = params;
1352
1363
 
1353
1364
  // Update camera controls FIRST before any rendering
@@ -1488,15 +1499,17 @@ export class ThreeRenderer {
1488
1499
  // Render with post-processing if enabled, otherwise direct render
1489
1500
  if (this.composer) {
1490
1501
  // === STEP 0: Render soul (layer 2) to texture for refraction sampling ===
1491
- if (this.soulRenderTarget) {
1492
- // Find soul mesh in scene (needed for screen center calculation)
1493
- let soulMesh = null;
1494
- this.scene.traverse(obj => {
1495
- if (obj.name === 'crystalSoul') soulMesh = obj;
1496
- });
1502
+ // OPTIMIZATION: Skip soul pass entirely if geometry doesn't have a soul
1503
+ if (this.soulRenderTarget && hasSoul) {
1504
+ // OPTIMIZATION: Use cached soul mesh reference instead of traversing every frame
1505
+ if (!this._cachedSoulMesh) {
1506
+ this.scene.traverse(obj => {
1507
+ if (obj.name === 'crystalSoul') this._cachedSoulMesh = obj;
1508
+ });
1509
+ }
1510
+ const soulMesh = this._cachedSoulMesh;
1497
1511
 
1498
1512
  this.renderer.setRenderTarget(this.soulRenderTarget);
1499
- this.renderer.setClearColor(0x000000, 0);
1500
1513
  this.renderer.clear();
1501
1514
 
1502
1515
  // Render only soul layer (layer 2)
@@ -1515,17 +1528,17 @@ export class ThreeRenderer {
1515
1528
  }
1516
1529
  // Compute soul's screen center position for refraction sampling
1517
1530
  if (this.coreMesh.material.uniforms.soulScreenCenter && soulMesh) {
1518
- const soulWorldPos = soulMesh.position.clone();
1519
- const soulNDC = soulWorldPos.project(this.camera);
1531
+ // OPTIMIZATION: Reuse pooled vector instead of cloning every frame
1532
+ this._soulPosTemp.copy(soulMesh.position);
1533
+ this._soulPosTemp.project(this.camera);
1520
1534
  // Convert from NDC (-1 to 1) to UV (0 to 1)
1521
- const soulScreenU = (soulNDC.x + 1.0) * 0.5;
1522
- const soulScreenV = (soulNDC.y + 1.0) * 0.5;
1535
+ const soulScreenU = (this._soulPosTemp.x + 1.0) * 0.5;
1536
+ const soulScreenV = (this._soulPosTemp.y + 1.0) * 0.5;
1523
1537
  this.coreMesh.material.uniforms.soulScreenCenter.value.set(soulScreenU, soulScreenV);
1524
1538
  }
1525
1539
  }
1526
1540
 
1527
1541
  this.renderer.setRenderTarget(null);
1528
- this.renderer.setClearColor(0x000000, 0);
1529
1542
  }
1530
1543
 
1531
1544
  // === STEP 1: Render main scene (layer 0) through bloom to screen ===
@@ -1533,7 +1546,8 @@ export class ThreeRenderer {
1533
1546
  this.composer.render();
1534
1547
 
1535
1548
  // === STEP 2: Render particles (layer 1) to separate render target ===
1536
- if (this.particleRenderTarget && this.particleBloomPass) {
1549
+ // OPTIMIZATION: Skip particle pass entirely if particles are disabled
1550
+ if (hasParticles && this.particleRenderTarget && this.particleBloomPass) {
1537
1551
  // Clear particle render target with WHITE (non-black) to prevent dark halos
1538
1552
  this.renderer.setRenderTarget(this.particleRenderTarget);
1539
1553
  this.renderer.setClearColor(0xffffff, 0); // White RGB, but 0 alpha
@@ -1565,7 +1579,7 @@ export class ThreeRenderer {
1565
1579
  // Reset clear color
1566
1580
  this.renderer.setClearColor(0x000000, 0);
1567
1581
  this.renderer.setRenderTarget(null);
1568
- } else {
1582
+ } else if (hasParticles) {
1569
1583
  // Fallback: Render particles directly (no bloom)
1570
1584
  this.camera.layers.set(1);
1571
1585
  this.renderer.render(this.scene, this.camera);
@@ -95,18 +95,14 @@ const soulFragmentShader = `
95
95
 
96
96
  if (driftEnabled > 0.5) {
97
97
  float t = time * driftSpeed;
98
- // Primary drift - moving in one direction
98
+ // OPTIMIZED: Reduced from 4 noise calls to 2 for better performance
99
+ // Primary drift - single noise call with combined coordinates
99
100
  float drift1 = noise3D(vPosition * 2.0 + vec3(t, t * 0.7, t * 0.3));
100
- float drift2 = noise3D(vPosition * 3.0 - vec3(t * 0.5, t, t * 0.8));
101
- // Use max instead of multiply to avoid near-zero products
102
- primaryDrift = max(drift1, drift2);
103
- primaryDrift = max(0.0, primaryDrift - 0.4) * 2.0; // Rescale after threshold
101
+ primaryDrift = max(0.0, drift1 - 0.3) * 1.5; // Adjusted threshold for single noise
104
102
 
105
- // Secondary drift - offset in opposite direction to fill gaps
106
- float drift3 = noise3D(vPosition * 2.5 - vec3(t * 0.8, t * 0.4, t));
107
- float drift4 = noise3D(vPosition * 1.8 + vec3(t * 0.6, t * 0.9, t * 0.2));
108
- secondaryDrift = max(drift3, drift4);
109
- secondaryDrift = max(0.0, secondaryDrift - 0.4) * 2.0;
103
+ // Secondary drift - offset phase to fill gaps
104
+ float drift2 = noise3D(vPosition * 2.5 - vec3(t * 0.6, t * 0.4, t));
105
+ secondaryDrift = max(0.0, drift2 - 0.3) * 1.5;
110
106
 
111
107
  driftEnergy = primaryDrift + secondaryDrift;
112
108
  }
@@ -295,7 +291,7 @@ export class CrystalSoul {
295
291
  uniforms: {
296
292
  time: { value: 0 },
297
293
  emotionColor: { value: new THREE.Color(1, 1, 1) },
298
- energyIntensity: { value: 1.5 },
294
+ energyIntensity: { value: 0.8 }, // Fixed value - no per-frame update needed
299
295
  driftEnabled: { value: 1.0 },
300
296
  driftSpeed: { value: 0.5 },
301
297
  crossWaveEnabled: { value: 1.0 },
@@ -424,17 +420,15 @@ export class CrystalSoul {
424
420
  uniforms.time.value += deltaTime / 1000;
425
421
  }
426
422
 
427
- // Update emotion color
423
+ // Update emotion color only if changed (avoid unnecessary GPU uniform sync)
428
424
  if (uniforms.emotionColor && glowColor) {
429
- uniforms.emotionColor.value.setRGB(
430
- glowColor[0], glowColor[1], glowColor[2]
431
- );
425
+ const current = uniforms.emotionColor.value;
426
+ if (current.r !== glowColor[0] || current.g !== glowColor[1] || current.b !== glowColor[2]) {
427
+ current.setRGB(glowColor[0], glowColor[1], glowColor[2]);
428
+ }
432
429
  }
433
430
 
434
- // Fixed intensity matching original implementation
435
- if (uniforms.energyIntensity) {
436
- uniforms.energyIntensity.value = 0.8;
437
- }
431
+ // Note: energyIntensity is fixed at 0.8 (set in constructor, no per-frame update needed)
438
432
 
439
433
  // Apply breathing scale
440
434
  if (this.mesh) {