@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.
@@ -0,0 +1,385 @@
1
+ /**
2
+ * EffectManager - Manages geometry-specific visual effects
3
+ *
4
+ * Handles lifecycle management for:
5
+ * - SolarEclipse (sun geometry)
6
+ * - LunarEclipse (moon geometry)
7
+ * - CrystalSoul (crystal-type geometries)
8
+ *
9
+ * Extracted from Core3DManager to improve separation of concerns.
10
+ *
11
+ * @module 3d/managers/EffectManager
12
+ */
13
+
14
+ import { SolarEclipse } from '../effects/SolarEclipse.js';
15
+ import { LunarEclipse } from '../effects/LunarEclipse.js';
16
+ import { CrystalSoul } from '../effects/CrystalSoul.js';
17
+
18
+ /**
19
+ * Geometry types that use CrystalSoul effect
20
+ */
21
+ const CRYSTAL_SOUL_GEOMETRIES = ['crystal', 'rough', 'heart', 'star'];
22
+
23
+ export class EffectManager {
24
+ /**
25
+ * Create effect manager
26
+ * @param {ThreeRenderer} renderer - The Three.js renderer
27
+ * @param {string} assetBasePath - Base path for loading assets
28
+ */
29
+ constructor(renderer, assetBasePath = '/assets') {
30
+ this.renderer = renderer;
31
+ this.assetBasePath = assetBasePath;
32
+
33
+ // Effect instances
34
+ this.solarEclipse = null;
35
+ this.lunarEclipse = null;
36
+ this.crystalSoul = null;
37
+
38
+ // State tracking
39
+ this.currentGeometryType = null;
40
+ this.coreGlowEnabled = true;
41
+ }
42
+
43
+ /**
44
+ * Initialize effects for a specific geometry type
45
+ * @param {string} geometryType - The geometry type (sun, moon, crystal, etc.)
46
+ * @param {Object} options - Options for initialization
47
+ * @param {THREE.Mesh} options.coreMesh - The core mesh to attach effects to
48
+ * @param {Object} options.customMaterial - Custom material (for lunar eclipse)
49
+ * @param {number} options.sunRadius - Sun radius (for solar eclipse)
50
+ */
51
+ initializeForGeometry(geometryType, options = {}) {
52
+ const { coreMesh, customMaterial, sunRadius = 1.0 } = options;
53
+ this.currentGeometryType = geometryType;
54
+
55
+ // Clean up effects not needed for this geometry
56
+ this._cleanupUnneededEffects(geometryType);
57
+
58
+ // Initialize geometry-specific effects
59
+ if (geometryType === 'sun') {
60
+ this._initSolarEclipse(sunRadius, coreMesh);
61
+ } else if (geometryType === 'moon') {
62
+ this._initLunarEclipse(customMaterial);
63
+ } else if (CRYSTAL_SOUL_GEOMETRIES.includes(geometryType)) {
64
+ // CrystalSoul is initialized separately via createCrystalSoul()
65
+ // because it has complex async loading requirements
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Initialize solar eclipse effect
71
+ * @private
72
+ */
73
+ _initSolarEclipse(sunRadius, coreMesh) {
74
+ if (!this.solarEclipse && this.renderer?.scene) {
75
+ this.solarEclipse = new SolarEclipse(this.renderer.scene, sunRadius, coreMesh);
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Initialize lunar eclipse effect
81
+ * @private
82
+ */
83
+ _initLunarEclipse(customMaterial) {
84
+ if (!this.lunarEclipse && customMaterial) {
85
+ this.lunarEclipse = new LunarEclipse(customMaterial);
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Clean up effects that are not needed for the current geometry
91
+ * @private
92
+ */
93
+ _cleanupUnneededEffects(geometryType) {
94
+ // Dispose solar eclipse if not sun
95
+ if (geometryType !== 'sun' && this.solarEclipse) {
96
+ this.solarEclipse.dispose();
97
+ this.solarEclipse = null;
98
+ }
99
+
100
+ // Dispose lunar eclipse if not moon
101
+ if (geometryType !== 'moon' && this.lunarEclipse) {
102
+ this.lunarEclipse.dispose();
103
+ this.lunarEclipse = null;
104
+ }
105
+
106
+ // Dispose crystal soul if not a crystal-type geometry
107
+ if (!CRYSTAL_SOUL_GEOMETRIES.includes(geometryType) && this.crystalSoul) {
108
+ this.crystalSoul.dispose();
109
+ this.crystalSoul = null;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Create crystal soul effect (async due to inclusion geometry loading)
115
+ * @param {Object} options - Crystal soul options
116
+ * @param {THREE.Mesh} options.coreMesh - Core mesh to attach to
117
+ * @param {string} options.geometryType - Geometry type for configuration
118
+ * @returns {Object} Soul configuration { mesh, material, baseScale, shellBaseScale }
119
+ */
120
+ async createCrystalSoul(options = {}) {
121
+ const { coreMesh, geometryType } = options;
122
+
123
+ // Dispose existing soul
124
+ if (this.crystalSoul) {
125
+ this.crystalSoul.dispose();
126
+ this.crystalSoul = null;
127
+ }
128
+
129
+ if (!coreMesh) {
130
+ return null;
131
+ }
132
+
133
+ // Preload inclusion geometry
134
+ await CrystalSoul._loadInclusionGeometry(this.assetBasePath);
135
+
136
+ // Create new soul
137
+ this.crystalSoul = new CrystalSoul({
138
+ radius: 0.35,
139
+ detail: 1,
140
+ geometryType,
141
+ renderer: this.renderer,
142
+ assetBasePath: this.assetBasePath
143
+ });
144
+
145
+ this.crystalSoul.attachTo(coreMesh, this.renderer?.scene);
146
+
147
+ // Get geometry-specific scale configuration
148
+ const { shellBaseScale, soulScale } = this._getCrystalScaleConfig(geometryType);
149
+
150
+ this.crystalSoul.baseScale = soulScale;
151
+ this.crystalSoul.mesh.scale.setScalar(soulScale);
152
+ this.crystalSoul.setVisible(this.coreGlowEnabled);
153
+
154
+ return {
155
+ mesh: this.crystalSoul.mesh,
156
+ material: this.crystalSoul.material,
157
+ baseScale: this.crystalSoul.baseScale,
158
+ shellBaseScale
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Create crystal soul synchronously (non-async version)
164
+ * @param {Object} options - Crystal soul options
165
+ * @returns {Object} Soul configuration
166
+ */
167
+ createCrystalSoulSync(options = {}) {
168
+ const { coreMesh, geometryType } = options;
169
+
170
+ if (this.crystalSoul) {
171
+ this.crystalSoul.dispose();
172
+ this.crystalSoul = null;
173
+ }
174
+
175
+ if (!coreMesh) {
176
+ return null;
177
+ }
178
+
179
+ this.crystalSoul = new CrystalSoul({
180
+ radius: 0.35,
181
+ detail: 1,
182
+ geometryType,
183
+ renderer: this.renderer,
184
+ assetBasePath: this.assetBasePath
185
+ });
186
+
187
+ this.crystalSoul.attachTo(coreMesh, this.renderer?.scene);
188
+
189
+ const { shellBaseScale, soulScale } = this._getCrystalScaleConfig(geometryType);
190
+
191
+ this.crystalSoul.baseScale = soulScale;
192
+ this.crystalSoul.mesh.scale.setScalar(soulScale);
193
+ this.crystalSoul.setVisible(this.coreGlowEnabled);
194
+
195
+ return {
196
+ mesh: this.crystalSoul.mesh,
197
+ material: this.crystalSoul.material,
198
+ baseScale: this.crystalSoul.baseScale,
199
+ shellBaseScale
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Get scale configuration for crystal-type geometries
205
+ * @private
206
+ */
207
+ _getCrystalScaleConfig(geometryType) {
208
+ let shellBaseScale = 2.0; // Default crystal shell size
209
+ let soulScale = 1.0; // Default: full size
210
+
211
+ if (geometryType === 'heart') {
212
+ shellBaseScale = 2.4;
213
+ soulScale = 1.0;
214
+ } else if (geometryType === 'rough') {
215
+ shellBaseScale = 1.6;
216
+ soulScale = 1.0;
217
+ } else if (geometryType === 'star') {
218
+ shellBaseScale = 2.0;
219
+ soulScale = 1.4; // Larger soul for star to fill the shape
220
+ } else if (geometryType === 'crystal') {
221
+ shellBaseScale = 2.0;
222
+ soulScale = 1.0;
223
+ }
224
+
225
+ return { shellBaseScale, soulScale };
226
+ }
227
+
228
+ /**
229
+ * Set solar eclipse type
230
+ * @param {string} eclipseType - 'off', 'annular', or 'total'
231
+ * @returns {boolean} True if eclipse was set
232
+ */
233
+ setSolarEclipse(eclipseType) {
234
+ if (!this.solarEclipse) {
235
+ return false;
236
+ }
237
+ this.solarEclipse.setEclipseType(eclipseType);
238
+ return true;
239
+ }
240
+
241
+ /**
242
+ * Set lunar eclipse type
243
+ * @param {string} eclipseType - 'off', 'penumbral', 'partial', 'total'
244
+ * @returns {boolean} True if eclipse was set
245
+ */
246
+ setLunarEclipse(eclipseType) {
247
+ if (!this.lunarEclipse) {
248
+ return false;
249
+ }
250
+ this.lunarEclipse.setEclipseType(eclipseType);
251
+ return true;
252
+ }
253
+
254
+ /**
255
+ * Stop all eclipse effects
256
+ */
257
+ stopAllEclipses() {
258
+ if (this.solarEclipse) {
259
+ this.solarEclipse.setEclipseType('off');
260
+ }
261
+ if (this.lunarEclipse) {
262
+ this.lunarEclipse.setEclipseType('off');
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Update crystal soul
268
+ * @param {number} deltaTime - Time since last frame
269
+ * @param {Array} glowColor - RGB color [r, g, b]
270
+ * @param {number} breathScale - Breathing animation scale
271
+ */
272
+ updateCrystalSoul(deltaTime, glowColor, breathScale = 1.0) {
273
+ if (this.crystalSoul) {
274
+ this.crystalSoul.update(deltaTime, glowColor, breathScale);
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Update lunar eclipse
280
+ * @param {number} deltaTime - Time since last frame
281
+ */
282
+ updateLunarEclipse(deltaTime) {
283
+ if (this.lunarEclipse) {
284
+ this.lunarEclipse.update(deltaTime);
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Set crystal soul effect parameters
290
+ * @param {Object} params - Effect parameters
291
+ */
292
+ setCrystalSoulEffects(params) {
293
+ if (this.crystalSoul) {
294
+ this.crystalSoul.setEffects(params);
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Set crystal soul size
300
+ * @param {number} size - Size value
301
+ * @returns {number} The new base scale
302
+ */
303
+ setCrystalSoulSize(size) {
304
+ if (this.crystalSoul) {
305
+ this.crystalSoul.setSize(size);
306
+ return this.crystalSoul.baseScale;
307
+ }
308
+ return 1.0;
309
+ }
310
+
311
+ /**
312
+ * Set crystal soul visibility
313
+ * @param {boolean} visible - Whether soul should be visible
314
+ */
315
+ setCrystalSoulVisible(visible) {
316
+ this.coreGlowEnabled = visible;
317
+ if (this.crystalSoul) {
318
+ this.crystalSoul.setVisible(visible);
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Check if crystal soul exists
324
+ * @returns {boolean}
325
+ */
326
+ hasCrystalSoul() {
327
+ return !!this.crystalSoul;
328
+ }
329
+
330
+ /**
331
+ * Get crystal soul base scale
332
+ * @returns {number}
333
+ */
334
+ getCrystalSoulBaseScale() {
335
+ return this.crystalSoul?.baseScale ?? 1.0;
336
+ }
337
+
338
+ /**
339
+ * Check if solar eclipse is active
340
+ * @returns {boolean}
341
+ */
342
+ hasSolarEclipse() {
343
+ return !!this.solarEclipse;
344
+ }
345
+
346
+ /**
347
+ * Check if lunar eclipse is active
348
+ * @returns {boolean}
349
+ */
350
+ hasLunarEclipse() {
351
+ return !!this.lunarEclipse;
352
+ }
353
+
354
+ /**
355
+ * Get solar eclipse instance (for render pass)
356
+ * @returns {SolarEclipse|null}
357
+ */
358
+ getSolarEclipse() {
359
+ return this.solarEclipse;
360
+ }
361
+
362
+ /**
363
+ * Dispose all effects
364
+ */
365
+ dispose() {
366
+ if (this.solarEclipse) {
367
+ this.solarEclipse.dispose();
368
+ this.solarEclipse = null;
369
+ }
370
+
371
+ if (this.lunarEclipse) {
372
+ this.lunarEclipse.dispose();
373
+ this.lunarEclipse = null;
374
+ }
375
+
376
+ if (this.crystalSoul) {
377
+ this.crystalSoul.dispose();
378
+ this.crystalSoul = null;
379
+ }
380
+
381
+ this.renderer = null;
382
+ }
383
+ }
384
+
385
+ export default EffectManager;
@@ -75,10 +75,10 @@ function createCrystalMaterial(glowColor, glowIntensity, textureType = 'crystal'
75
75
  if (textureType) {
76
76
  const textureLoader = new THREE.TextureLoader();
77
77
  const texturePaths = {
78
- crystal: `${assetBasePath}/textures/Crystal/crystal.jpg`,
79
- rough: `${assetBasePath}/textures/Crystal/rough.jpg`,
80
- heart: `${assetBasePath}/textures/Crystal/heart.jpg`,
81
- star: `${assetBasePath}/textures/Crystal/star.jpg`
78
+ crystal: `${assetBasePath}/textures/Crystal/crystal.png`,
79
+ rough: `${assetBasePath}/textures/Crystal/rough.png`,
80
+ heart: `${assetBasePath}/textures/Crystal/heart.png`,
81
+ star: `${assetBasePath}/textures/Crystal/star.png`
82
82
  };
83
83
  const texturePath = texturePaths[textureType] || texturePaths.crystal;
84
84
  crystalTexture = textureLoader.load(texturePath,
@@ -191,7 +191,8 @@ function createCrystalMaterial(glowColor, glowIntensity, textureType = 'crystal'
191
191
  }
192
192
 
193
193
  function createMoonMaterial(textureLoader, glowColor, glowIntensity, materialVariant = null, assetBasePath = '/assets') {
194
- const resolution = '2k';
194
+ const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
195
+ const resolution = isMobile ? '2k' : '4k';
195
196
 
196
197
  if (materialVariant === 'multiplexer') {
197
198
  const material = createMoonMultiplexerMaterial(textureLoader, {
package/types/index.d.ts CHANGED
@@ -207,22 +207,28 @@ declare namespace EmotiveEngine {
207
207
  // Main Emotive Mascot
208
208
  export interface EmotiveMascotConfig {
209
209
  canvas?: HTMLCanvasElement;
210
+ canvasId?: string | HTMLCanvasElement;
210
211
  width?: number;
211
212
  height?: number;
212
213
  autoStart?: boolean;
213
214
  fps?: number;
215
+ targetFPS?: number;
214
216
  adaptive?: boolean;
215
217
  theme?: Theme;
216
218
  enableAudio?: boolean;
217
219
  enableParticles?: boolean;
220
+ enableGazeTracking?: boolean;
221
+ defaultEmotion?: string;
218
222
  }
219
223
 
220
224
  export interface Theme {
221
- primary: string;
222
- secondary: string;
223
- accent: string;
225
+ primary?: string;
226
+ secondary?: string;
227
+ accent?: string;
224
228
  background?: string;
225
229
  particles?: string[];
230
+ core?: string;
231
+ glow?: string;
226
232
  }
227
233
 
228
234
  export interface EmotionalState {
@@ -231,22 +237,212 @@ declare namespace EmotiveEngine {
231
237
  dominance?: number;
232
238
  }
233
239
 
240
+ export interface FeelResult {
241
+ success: boolean;
242
+ error: string | null;
243
+ parsed: ParsedIntent | null;
244
+ }
245
+
246
+ export interface ParsedIntent {
247
+ emotion: string | null;
248
+ gestures: string[];
249
+ shape: string | null;
250
+ undertone: string | null;
251
+ intensity: number;
252
+ }
253
+
254
+ export interface FeelVocabulary {
255
+ emotions: string[];
256
+ undertones: string[];
257
+ gestures: string[];
258
+ shapes: string[];
259
+ }
260
+
261
+ export interface BackdropOptions {
262
+ enabled?: boolean;
263
+ radius?: number;
264
+ shape?: 'circle' | 'ellipse' | 'fullscreen';
265
+ color?: string;
266
+ intensity?: number;
267
+ blendMode?: 'normal' | 'multiply' | 'overlay' | 'screen';
268
+ falloff?: 'linear' | 'smooth' | 'exponential' | 'custom';
269
+ falloffCurve?: Array<{ stop: number; alpha: number }>;
270
+ edgeSoftness?: number;
271
+ coreTransparency?: number;
272
+ blur?: number;
273
+ responsive?: boolean;
274
+ pulse?: boolean;
275
+ offset?: { x: number; y: number };
276
+ type?: 'radial-gradient' | 'vignette' | 'glow';
277
+ }
278
+
279
+ export interface AttachOptions {
280
+ offsetX?: number;
281
+ offsetY?: number;
282
+ animate?: boolean;
283
+ duration?: number;
284
+ scale?: number;
285
+ containParticles?: boolean;
286
+ }
287
+
288
+ export interface PerformanceMetrics {
289
+ fps: number;
290
+ frameTime: number;
291
+ particleCount?: number;
292
+ }
293
+
294
+ export interface AudioAnalysis {
295
+ beats?: any;
296
+ tempo?: number;
297
+ energy?: number;
298
+ }
299
+
300
+ export interface GazeState {
301
+ x: number;
302
+ y: number;
303
+ enabled: boolean;
304
+ }
305
+
306
+ export interface Capabilities {
307
+ audio: boolean;
308
+ recording: boolean;
309
+ timeline: boolean;
310
+ export: boolean;
311
+ shapes: boolean;
312
+ gestures: boolean;
313
+ emotions: boolean;
314
+ particles: boolean;
315
+ gazeTracking: boolean;
316
+ }
317
+
318
+ export interface TimelineEvent {
319
+ type: 'emotion' | 'gesture' | 'shape';
320
+ name: string;
321
+ time: number;
322
+ undertone?: string;
323
+ config?: any;
324
+ }
325
+
326
+ export interface ScaleOptions {
327
+ global?: number;
328
+ core?: number;
329
+ particles?: number;
330
+ }
331
+
234
332
  export class EmotiveMascot {
235
333
  constructor(config?: EmotiveMascotConfig);
236
- init(container: HTMLElement): void;
334
+
335
+ // Lifecycle
336
+ init(canvas: HTMLCanvasElement | string): Promise<void>;
237
337
  start(): void;
238
338
  stop(): void;
239
339
  pause(): void;
240
340
  resume(): void;
241
341
  destroy(): void;
242
- setEmotion(emotion: string | EmotionalState): void;
243
- setTheme(theme: Theme): void;
342
+
343
+ // LLM Integration - Natural Language API
344
+ feel(intent: string): FeelResult;
345
+ static getFeelVocabulary(): FeelVocabulary;
346
+ parseIntent(intent: string): ParsedIntent;
347
+
348
+ // Emotion & Expression
349
+ setEmotion(emotion: string, undertoneOrDurationOrOptions?: string | number | { undertone?: string; duration?: number }, timestamp?: number): void;
350
+ express(gestureName: string, timestamp?: number): void;
351
+ triggerGesture(gestureName: string, timestamp?: number): void;
352
+ chain(chainName: string): void;
353
+ updateUndertone(undertone: string | null): void;
354
+
355
+ // Shape Morphing
356
+ morphTo(shape: string, config?: any): void;
357
+ setShape(shape: string, configOrTimestamp?: any | number): void;
358
+
359
+ // Audio
360
+ loadAudio(source: string | Blob): Promise<void>;
361
+ connectAudio(audioElement: HTMLAudioElement): void;
362
+ disconnectAudio(audioElement?: HTMLAudioElement): void;
363
+ getAudioAnalysis(): AudioAnalysis;
364
+ getSpectrumData(): number[];
365
+ startRhythmSync(bpm?: number): void;
366
+ stopRhythmSync(): void;
367
+ setSoundEnabled(enabled: boolean): void;
244
368
  setBPM(bpm: number): void;
245
- addBehavior(behavior: Behavior): void;
246
- removeBehavior(name: string): void;
247
- on(event: string, handler: Function): void;
248
- off(event: string, handler?: Function): void;
249
- getState(): any;
369
+
370
+ // Gaze Tracking
371
+ enableGazeTracking(): void;
372
+ disableGazeTracking(): void;
373
+ setGazeTarget(x: number, y: number): void;
374
+ getGazeState(): GazeState | null;
375
+
376
+ // Positioning
377
+ setPosition(x: number, y: number, z?: number): void;
378
+ animateToPosition(x: number, y: number, z?: number, duration?: number, easing?: string): void;
379
+ attachToElement(elementOrSelector: string | HTMLElement, options?: AttachOptions): EmotiveMascot;
380
+ detachFromElement(): void;
381
+ isAttachedToElement(): boolean;
382
+ setContainment(bounds: { width: number; height: number } | null, scale?: number): void;
383
+
384
+ // Visual Customization
385
+ setScale(scaleOrOptions: number | ScaleOptions): EmotiveMascot;
386
+ getScale(): number;
387
+ setOpacity(opacity: number): EmotiveMascot;
388
+ getOpacity(): number;
389
+ fadeIn(duration?: number): EmotiveMascot;
390
+ fadeOut(duration?: number): EmotiveMascot;
391
+ setColor(color: string): EmotiveMascot;
392
+ setGlowColor(color: string): EmotiveMascot;
393
+ setTheme(theme: Theme): EmotiveMascot;
394
+ setBackdrop(options: BackdropOptions): EmotiveMascot;
395
+ getBackdrop(): BackdropOptions | null;
396
+
397
+ // Particles
398
+ clearParticles(): void;
399
+ setMaxParticles(maxParticles: number): EmotiveMascot;
400
+ getParticleCount(): number;
401
+ setParticleSystemCanvasDimensions(width: number, height: number): EmotiveMascot;
402
+
403
+ // Performance
404
+ setQuality(level: 'low' | 'medium' | 'high'): void;
405
+ setSpeed(speed: number): EmotiveMascot;
406
+ getSpeed(): number;
407
+ setFPS(fps: number): EmotiveMascot;
408
+ getFPS(): number;
409
+ isPaused(): boolean;
410
+ getPerformanceMetrics(): PerformanceMetrics;
411
+ batch(callback: (mascot: EmotiveMascot) => void): EmotiveMascot;
412
+
413
+ // Timeline Recording
414
+ startRecording(): void;
415
+ stopRecording(): TimelineEvent[];
416
+ playTimeline(timeline: TimelineEvent[]): void;
417
+ stopPlayback(): void;
418
+ getTimeline(): TimelineEvent[];
419
+ loadTimeline(timeline: TimelineEvent[]): void;
420
+ exportTimeline(): string;
421
+ importTimeline(json: string): void;
422
+ getCurrentTime(): number;
423
+ seek(time: number): void;
424
+
425
+ // Export
426
+ getFrameData(format?: string): string;
427
+ getFrameBlob(format?: string): Promise<Blob>;
428
+ getAnimationData(): any;
429
+
430
+ // Query
431
+ getAvailableGestures(): string[];
432
+ getAvailableEmotions(): string[];
433
+ getAvailableShapes(): string[];
434
+ getVersion(): string;
435
+ getCapabilities(): Capabilities;
436
+
437
+ // Events
438
+ on(event: string, handler: Function): EmotiveMascot;
439
+ off(event: string, handler?: Function): EmotiveMascot;
440
+
441
+ // Component access (safe proxies)
442
+ readonly renderer: any;
443
+ readonly shapeMorpher: any;
444
+ readonly gazeTracker: any;
445
+ readonly canvas: HTMLCanvasElement;
250
446
  }
251
447
 
252
448
  // Particle System