@viji-dev/core 0.2.19 → 0.3.0

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/index.js CHANGED
@@ -1,2616 +1,8 @@
1
- class VijiCoreError extends Error {
2
- constructor(message, code, context) {
3
- super(message);
4
- this.code = code;
5
- this.context = context;
6
- this.name = "VijiCoreError";
7
- }
8
- }
9
- class IFrameManager {
10
- constructor(hostContainer) {
11
- this.hostContainer = hostContainer;
12
- }
13
- iframe = null;
14
- canvas = null;
15
- offscreenCanvas = null;
16
- isInitialized = false;
17
- scale = 1;
18
- // Debug logging control
19
- debugMode = false;
20
- /**
21
- * Enable or disable debug logging
22
- */
23
- setDebugMode(enabled) {
24
- this.debugMode = enabled;
25
- }
26
- /**
27
- * Debug logging helper
28
- */
29
- debugLog(message, ...args) {
30
- if (this.debugMode) {
31
- console.log(message, ...args);
32
- }
33
- }
34
- // Phase 7: Interaction event listeners
35
- interactionListeners = /* @__PURE__ */ new Map();
36
- isInteractionEnabled = true;
37
- // Mouse canvas tracking
38
- isMouseInCanvas = false;
39
- // Touch tracking for gesture recognition
40
- activeTouchIds = /* @__PURE__ */ new Set();
41
- /**
42
- * Creates a secure IFrame with proper sandbox attributes
43
- */
44
- async createSecureIFrame() {
45
- try {
46
- const iframe = document.createElement("iframe");
47
- iframe.sandbox.add("allow-scripts");
48
- iframe.sandbox.add("allow-same-origin");
49
- iframe.style.width = "100%";
50
- iframe.style.height = "100%";
51
- iframe.style.border = "none";
52
- iframe.style.display = "block";
53
- const iframeContent = this.generateIFrameHTML();
54
- const blob = new Blob([iframeContent], { type: "text/html" });
55
- iframe.src = URL.createObjectURL(blob);
56
- this.hostContainer.appendChild(iframe);
57
- await new Promise((resolve, reject) => {
58
- const timeout = setTimeout(() => {
59
- reject(new VijiCoreError("IFrame load timeout", "IFRAME_TIMEOUT"));
60
- }, 5e3);
61
- const checkReady = () => {
62
- if (iframe.contentDocument && iframe.contentDocument.readyState === "complete") {
63
- clearTimeout(timeout);
64
- resolve();
65
- }
66
- };
67
- iframe.onload = () => {
68
- if (iframe.contentDocument?.readyState === "complete") {
69
- clearTimeout(timeout);
70
- resolve();
71
- } else {
72
- iframe.contentDocument?.addEventListener("DOMContentLoaded", checkReady);
73
- setTimeout(checkReady, 100);
74
- }
75
- };
76
- iframe.onerror = () => {
77
- clearTimeout(timeout);
78
- reject(new VijiCoreError("IFrame load failed", "IFRAME_LOAD_ERROR"));
79
- };
80
- });
81
- this.iframe = iframe;
82
- return iframe;
83
- } catch (error) {
84
- throw new VijiCoreError(
85
- `Failed to create secure IFrame: ${error}`,
86
- "IFRAME_CREATION_ERROR",
87
- { error }
88
- );
89
- }
90
- }
91
- /**
92
- * Creates canvas inside the IFrame and returns OffscreenCanvas for WebWorker
93
- */
94
- async createCanvas() {
95
- if (!this.iframe?.contentWindow) {
96
- throw new VijiCoreError("IFrame not ready for canvas creation", "IFRAME_NOT_READY");
97
- }
98
- try {
99
- const iframeDoc = await this.waitForIFrameDocument();
100
- const canvas = iframeDoc.createElement("canvas");
101
- canvas.id = "viji-canvas";
102
- this.canvas = canvas;
103
- const { width, height } = this.calculateCanvasSize();
104
- canvas.width = width * this.scale;
105
- canvas.height = height * this.scale;
106
- canvas.style.width = "100%";
107
- canvas.style.height = "100%";
108
- canvas.style.display = "block";
109
- const body = iframeDoc.querySelector("body");
110
- if (!body) {
111
- throw new VijiCoreError("IFrame body not found", "IFRAME_BODY_ERROR");
112
- }
113
- body.appendChild(canvas);
114
- this.setupInteractionListeners(canvas, iframeDoc);
115
- const offscreenCanvas = canvas.transferControlToOffscreen();
116
- this.offscreenCanvas = offscreenCanvas;
117
- this.isInitialized = true;
118
- return offscreenCanvas;
119
- } catch (error) {
120
- throw new VijiCoreError(
121
- `Failed to create canvas: ${error}`,
122
- "CANVAS_CREATION_ERROR",
123
- { error }
124
- );
125
- }
126
- }
127
- /**
128
- * Waits for iframe document to be accessible with retry logic
129
- */
130
- async waitForIFrameDocument() {
131
- const maxRetries = 15;
132
- const retryDelay = 150;
133
- for (let i = 0; i < maxRetries; i++) {
134
- try {
135
- const iframeDoc = this.iframe?.contentDocument;
136
- if (iframeDoc && (iframeDoc.readyState === "complete" || iframeDoc.readyState === "interactive") && iframeDoc.body && this.iframe?.contentWindow) {
137
- this.debugLog(`IFrame document ready after ${i + 1} attempts`);
138
- return iframeDoc;
139
- }
140
- this.debugLog(`IFrame not ready attempt ${i + 1}/${maxRetries}:`, {
141
- hasDocument: !!iframeDoc,
142
- readyState: iframeDoc?.readyState,
143
- hasBody: !!iframeDoc?.body,
144
- hasWindow: !!this.iframe?.contentWindow
145
- });
146
- await new Promise((resolve) => setTimeout(resolve, retryDelay));
147
- } catch (error) {
148
- this.debugLog(`IFrame access error attempt ${i + 1}/${maxRetries}:`, error);
149
- await new Promise((resolve) => setTimeout(resolve, retryDelay));
150
- }
151
- }
152
- throw new VijiCoreError("Cannot access IFrame document after retries", "IFRAME_ACCESS_ERROR");
153
- }
154
- /**
155
- * Updates canvas resolution setting (scaling handled in worker)
156
- */
157
- updateScale(scale) {
158
- this.scale = scale;
159
- return this.getEffectiveResolution();
160
- }
161
- /**
162
- * Gets current scale
163
- */
164
- getScale() {
165
- return this.scale;
166
- }
167
- /**
168
- * Destroys the IFrame and cleans up resources
169
- */
170
- destroy() {
171
- try {
172
- if (this.iframe) {
173
- if (this.iframe.src.startsWith("blob:")) {
174
- URL.revokeObjectURL(this.iframe.src);
175
- }
176
- this.iframe.remove();
177
- this.iframe = null;
178
- }
179
- this.offscreenCanvas = null;
180
- this.isInitialized = false;
181
- } catch (error) {
182
- console.warn("Error during IFrame cleanup:", error);
183
- }
184
- }
185
- /**
186
- * Checks if IFrame is ready for use
187
- */
188
- get ready() {
189
- return this.isInitialized && this.iframe !== null && this.offscreenCanvas !== null;
190
- }
191
- /**
192
- * Gets the IFrame element
193
- */
194
- get element() {
195
- return this.iframe;
196
- }
197
- /**
198
- * Generates the HTML content for the secure IFrame
199
- */
200
- generateIFrameHTML() {
201
- return `
202
- <!DOCTYPE html>
203
- <html>
204
- <head>
205
- <meta charset="utf-8">
206
- <meta name="viewport" content="width=device-width, initial-scale=1">
207
- <title>Viji Scene Container</title>
208
- <style>
209
- * {
210
- margin: 0;
211
- padding: 0;
212
- box-sizing: border-box;
213
- }
214
-
215
- html, body {
216
- width: 100%;
217
- height: 100%;
218
- overflow: hidden;
219
- background: #000;
220
- }
221
-
222
- canvas {
223
- display: block;
224
- width: 100%;
225
- height: 100%;
226
- image-rendering: auto;
227
- }
228
- </style>
229
- </head>
230
- <body>
231
- <!-- Canvas will be created dynamically -->
232
- </body>
233
- </html>
234
- `.trim();
235
- }
236
- /**
237
- * Calculates canvas dimensions based on container size
238
- * Canvas internal size starts as container size, then worker updates it based on resolution
239
- */
240
- calculateCanvasSize() {
241
- const containerRect = this.hostContainer.getBoundingClientRect();
242
- return {
243
- width: Math.round(containerRect.width),
244
- height: Math.round(containerRect.height)
245
- };
246
- }
247
- /**
248
- * Gets the effective resolution that should be used for rendering
249
- */
250
- getEffectiveResolution() {
251
- const containerRect = this.hostContainer.getBoundingClientRect();
252
- const containerWidth = Math.round(containerRect.width);
253
- const containerHeight = Math.round(containerRect.height);
254
- const scale = Math.max(0.1, Math.min(1, this.scale));
255
- return {
256
- width: Math.round(containerWidth * scale),
257
- height: Math.round(containerHeight * scale)
258
- };
259
- }
260
- // Phase 7: Interaction Management Methods
261
- /**
262
- * Sets up interaction event listeners on the canvas and document
263
- */
264
- setupInteractionListeners(canvas, iframeDoc) {
265
- if (!this.isInteractionEnabled) return;
266
- canvas.tabIndex = 0;
267
- canvas.style.outline = "none";
268
- canvas.addEventListener("mousedown", this.handleMouseEvent.bind(this), { passive: false });
269
- canvas.addEventListener("mousemove", this.handleMouseEvent.bind(this), { passive: false });
270
- canvas.addEventListener("mouseup", this.handleMouseEvent.bind(this), { passive: false });
271
- canvas.addEventListener("mouseenter", this.handleMouseEnter.bind(this), { passive: false });
272
- canvas.addEventListener("mouseleave", this.handleMouseLeave.bind(this), { passive: false });
273
- canvas.addEventListener("wheel", this.handleWheelEvent.bind(this), { passive: false });
274
- canvas.addEventListener("contextmenu", (e) => e.preventDefault());
275
- iframeDoc.addEventListener("keydown", this.handleKeyboardEvent.bind(this), { passive: false });
276
- iframeDoc.addEventListener("keyup", this.handleKeyboardEvent.bind(this), { passive: false });
277
- canvas.addEventListener("touchstart", this.handleTouchEvent.bind(this), { passive: false });
278
- canvas.addEventListener("touchmove", this.handleTouchEvent.bind(this), { passive: false });
279
- canvas.addEventListener("touchend", this.handleTouchEvent.bind(this), { passive: false });
280
- canvas.addEventListener("touchcancel", this.handleTouchEvent.bind(this), { passive: false });
281
- canvas.addEventListener("mousedown", () => canvas.focus());
282
- canvas.addEventListener("touchstart", () => canvas.focus());
283
- }
284
- /**
285
- * Handles mouse events and transforms coordinates
286
- */
287
- handleMouseEvent(event) {
288
- if (!this.canvas || !this.isInteractionEnabled) return;
289
- event.preventDefault();
290
- const rect = this.canvas.getBoundingClientRect();
291
- const x = (event.clientX - rect.left) * (this.canvas.width / rect.width);
292
- const y = (event.clientY - rect.top) * (this.canvas.height / rect.height);
293
- const deltaX = event.movementX || 0;
294
- const deltaY = event.movementY || 0;
295
- const data = {
296
- x,
297
- y,
298
- buttons: event.buttons,
299
- deltaX,
300
- deltaY,
301
- wheelDeltaX: 0,
302
- wheelDeltaY: 0,
303
- isInCanvas: this.isMouseInCanvas,
304
- timestamp: performance.now()
305
- };
306
- this.emitInteractionEvent("mouse-update", data);
307
- }
308
- /**
309
- * Handles mouse enter events
310
- */
311
- handleMouseEnter(event) {
312
- if (!this.isInteractionEnabled) return;
313
- this.isMouseInCanvas = true;
314
- this.handleMouseEvent(event);
315
- }
316
- /**
317
- * Handles mouse leave events
318
- */
319
- handleMouseLeave(event) {
320
- if (!this.isInteractionEnabled) return;
321
- this.isMouseInCanvas = false;
322
- this.handleMouseEvent(event);
323
- }
324
- /**
325
- * Handles wheel events
326
- */
327
- handleWheelEvent(event) {
328
- if (!this.canvas || !this.isInteractionEnabled) return;
329
- event.preventDefault();
330
- const rect = this.canvas.getBoundingClientRect();
331
- const x = (event.clientX - rect.left) * (this.canvas.width / rect.width);
332
- const y = (event.clientY - rect.top) * (this.canvas.height / rect.height);
333
- const data = {
334
- x,
335
- y,
336
- buttons: event.buttons,
337
- deltaX: 0,
338
- deltaY: 0,
339
- wheelDeltaX: event.deltaX,
340
- wheelDeltaY: event.deltaY,
341
- timestamp: performance.now()
342
- };
343
- this.emitInteractionEvent("mouse-update", data);
344
- }
345
- /**
346
- * Handles keyboard events
347
- */
348
- handleKeyboardEvent(event) {
349
- if (!this.isInteractionEnabled) return;
350
- const allowedKeys = ["Tab", "F1", "F2", "F3", "F4", "F5", "F11", "F12"];
351
- if (!allowedKeys.includes(event.key)) {
352
- event.preventDefault();
353
- }
354
- const data = {
355
- type: event.type,
356
- key: event.key,
357
- code: event.code,
358
- shiftKey: event.shiftKey,
359
- ctrlKey: event.ctrlKey,
360
- altKey: event.altKey,
361
- metaKey: event.metaKey,
362
- timestamp: performance.now()
363
- };
364
- this.emitInteractionEvent("keyboard-update", data);
365
- }
366
- /**
367
- * Handles touch events and tracks multi-touch
368
- */
369
- handleTouchEvent(event) {
370
- if (!this.canvas || !this.isInteractionEnabled) return;
371
- event.preventDefault();
372
- const rect = this.canvas.getBoundingClientRect();
373
- const scaleX = this.canvas.width / rect.width;
374
- const scaleY = this.canvas.height / rect.height;
375
- const touches = Array.from(event.touches).map((touch) => ({
376
- identifier: touch.identifier,
377
- clientX: (touch.clientX - rect.left) * scaleX,
378
- clientY: (touch.clientY - rect.top) * scaleY,
379
- pressure: touch.pressure || 0,
380
- radiusX: touch.radiusX || 10,
381
- radiusY: touch.radiusY || 10,
382
- rotationAngle: touch.rotationAngle || 0,
383
- force: touch.force || touch.pressure || 0
384
- }));
385
- if (event.type === "touchstart") {
386
- for (const touch of event.changedTouches) {
387
- this.activeTouchIds.add(touch.identifier);
388
- }
389
- } else if (event.type === "touchend" || event.type === "touchcancel") {
390
- for (const touch of event.changedTouches) {
391
- this.activeTouchIds.delete(touch.identifier);
392
- }
393
- }
394
- const data = {
395
- type: event.type,
396
- touches,
397
- timestamp: performance.now()
398
- };
399
- this.emitInteractionEvent("touch-update", data);
400
- }
401
- /**
402
- * Emits an interaction event to registered listeners
403
- */
404
- emitInteractionEvent(eventType, data) {
405
- const listener = this.interactionListeners.get(eventType);
406
- if (listener) {
407
- listener(data);
408
- }
409
- }
410
- /**
411
- * Registers an interaction event listener
412
- */
413
- onInteractionEvent(eventType, listener) {
414
- this.interactionListeners.set(eventType, listener);
415
- }
416
- /**
417
- * Removes an interaction event listener
418
- */
419
- offInteractionEvent(eventType) {
420
- this.interactionListeners.delete(eventType);
421
- }
422
- /**
423
- * Enables or disables interaction event capture
424
- */
425
- setInteractionEnabled(enabled) {
426
- this.isInteractionEnabled = enabled;
427
- }
428
- /**
429
- * Gets the canvas element (for coordinate calculations)
430
- */
431
- getCanvas() {
432
- return this.canvas;
433
- }
434
- }
435
- function WorkerWrapper(options) {
436
- return new Worker(
437
- "" + new URL("assets/viji.worker-C8mrsLDG.js", import.meta.url).href,
438
- {
439
- type: "module",
440
- name: options?.name
441
- }
442
- );
443
- }
444
- class WorkerManager {
445
- constructor(sceneCode, offscreenCanvas) {
446
- this.sceneCode = sceneCode;
447
- this.offscreenCanvas = offscreenCanvas;
448
- }
449
- worker = null;
450
- messageId = 0;
451
- pendingMessages = /* @__PURE__ */ new Map();
452
- messageHandlers = /* @__PURE__ */ new Map();
453
- isInitialized = false;
454
- /**
455
- * Creates and initializes the WebWorker with artist code
456
- */
457
- async createWorker() {
458
- try {
459
- this.worker = new WorkerWrapper();
460
- this.setupMessageHandling();
461
- await this.initializeWorker();
462
- this.postMessage("set-scene-code", { sceneCode: this.sceneCode });
463
- this.isInitialized = true;
464
- } catch (error) {
465
- throw new VijiCoreError(
466
- `Failed to create worker: ${error}`,
467
- "WORKER_CREATION_ERROR",
468
- { error }
469
- );
470
- }
471
- }
472
- /**
473
- * Sends a message to the worker and returns a promise for the response
474
- */
475
- async sendMessage(type, data, timeout = 5e3) {
476
- if (!this.worker) {
477
- throw new VijiCoreError("Worker not initialized", "WORKER_NOT_READY");
478
- }
479
- const id = `msg_${++this.messageId}`;
480
- const message = {
481
- type,
482
- id,
483
- timestamp: Date.now(),
484
- data
485
- };
486
- return new Promise((resolve, reject) => {
487
- const timeoutId = setTimeout(() => {
488
- this.pendingMessages.delete(id);
489
- reject(new VijiCoreError(`Message timeout: ${type}`, "MESSAGE_TIMEOUT"));
490
- }, timeout);
491
- this.pendingMessages.set(id, {
492
- resolve,
493
- reject,
494
- timeout: timeoutId
495
- });
496
- this.worker.postMessage(message);
497
- });
498
- }
499
- postMessage(type, data, transfer) {
500
- if (!this.worker) {
501
- console.warn("Attempted to post message to uninitialized worker");
502
- return;
503
- }
504
- const message = {
505
- type,
506
- id: `fire_${++this.messageId}`,
507
- timestamp: Date.now(),
508
- data
509
- };
510
- if (transfer && transfer.length > 0) {
511
- this.worker.postMessage(message, transfer);
512
- } else {
513
- this.worker.postMessage(message);
514
- }
515
- }
516
- /**
517
- * Registers a handler for worker messages
518
- */
519
- onMessage(type, handler) {
520
- this.messageHandlers.set(type, handler);
521
- }
522
- /**
523
- * Removes a message handler
524
- */
525
- offMessage(type) {
526
- this.messageHandlers.delete(type);
527
- }
528
- /**
529
- * Terminates the worker and cleans up resources
530
- */
531
- destroy() {
532
- try {
533
- this.pendingMessages.forEach(({ timeout, reject }) => {
534
- clearTimeout(timeout);
535
- reject(new VijiCoreError("Worker destroyed", "WORKER_DESTROYED"));
536
- });
537
- this.pendingMessages.clear();
538
- this.messageHandlers.clear();
539
- if (this.worker) {
540
- this.worker.terminate();
541
- this.worker = null;
542
- }
543
- this.isInitialized = false;
544
- } catch (error) {
545
- console.warn("Error during worker cleanup:", error);
546
- }
547
- }
548
- /**
549
- * Checks if worker is ready for use
550
- */
551
- get ready() {
552
- return this.isInitialized && this.worker !== null;
553
- }
554
- /**
555
- * Gets the worker instance
556
- */
557
- get instance() {
558
- return this.worker;
559
- }
560
- /**
561
- * Sets up message handling for worker communication
562
- */
563
- setupMessageHandling() {
564
- if (!this.worker) return;
565
- this.worker.onmessage = (event) => {
566
- const message = event.data;
567
- if (this.pendingMessages.has(message.id)) {
568
- const pending = this.pendingMessages.get(message.id);
569
- clearTimeout(pending.timeout);
570
- this.pendingMessages.delete(message.id);
571
- if (message.type === "error") {
572
- pending.reject(new VijiCoreError(
573
- message.data?.message || "Worker error",
574
- message.data?.code || "WORKER_ERROR",
575
- message.data
576
- ));
577
- } else {
578
- pending.resolve(message.data);
579
- }
580
- return;
581
- }
582
- const handler = this.messageHandlers.get(message.type);
583
- if (handler) {
584
- try {
585
- handler(message.data);
586
- } catch (error) {
587
- console.error(`Error in message handler for ${message.type}:`, error);
588
- }
589
- }
590
- };
591
- this.worker.onerror = (error) => {
592
- console.error("Worker error:", error);
593
- this.pendingMessages.forEach(({ timeout, reject }) => {
594
- clearTimeout(timeout);
595
- reject(new VijiCoreError("Worker error", "WORKER_ERROR", error));
596
- });
597
- this.pendingMessages.clear();
598
- };
599
- }
600
- /**
601
- * Initializes the worker with canvas and basic setup
602
- * Waits for init-response message (worker will also send separate 'ready' event)
603
- */
604
- async initializeWorker() {
605
- if (!this.worker) {
606
- throw new VijiCoreError("Worker not created", "WORKER_NOT_CREATED");
607
- }
608
- const id = `msg_${++this.messageId}`;
609
- const message = {
610
- type: "init",
611
- id,
612
- timestamp: Date.now(),
613
- data: {
614
- canvas: this.offscreenCanvas
615
- }
616
- };
617
- return new Promise((resolve, reject) => {
618
- const timeoutId = setTimeout(() => {
619
- this.pendingMessages.delete(id);
620
- reject(new VijiCoreError("Canvas transfer timeout", "CANVAS_TRANSFER_TIMEOUT"));
621
- }, 1e4);
622
- this.pendingMessages.set(id, {
623
- resolve,
624
- reject,
625
- timeout: timeoutId
626
- });
627
- this.worker.postMessage(message, [this.offscreenCanvas]);
628
- });
629
- }
630
- }
631
- class InteractionManager {
632
- // Mouse state
633
- mouseState = {
634
- x: 0,
635
- y: 0,
636
- isInCanvas: false,
637
- isPressed: false,
638
- leftButton: false,
639
- rightButton: false,
640
- middleButton: false,
641
- velocity: { x: 0, y: 0 },
642
- deltaX: 0,
643
- deltaY: 0,
644
- wheelDelta: 0,
645
- wheelX: 0,
646
- wheelY: 0,
647
- wasPressed: false,
648
- wasReleased: false,
649
- wasMoved: false
650
- };
651
- // Mouse velocity tracking
652
- mouseVelocityHistory = [];
653
- // Keyboard state
654
- keyboardState = {
655
- isPressed: (key) => this.activeKeys.has(key.toLowerCase()),
656
- wasPressed: (key) => this.pressedThisFrame.has(key.toLowerCase()),
657
- wasReleased: (key) => this.releasedThisFrame.has(key.toLowerCase()),
658
- activeKeys: /* @__PURE__ */ new Set(),
659
- pressedThisFrame: /* @__PURE__ */ new Set(),
660
- releasedThisFrame: /* @__PURE__ */ new Set(),
661
- lastKeyPressed: "",
662
- lastKeyReleased: "",
663
- shift: false,
664
- ctrl: false,
665
- alt: false,
666
- meta: false
667
- };
668
- activeKeys = /* @__PURE__ */ new Set();
669
- pressedThisFrame = /* @__PURE__ */ new Set();
670
- releasedThisFrame = /* @__PURE__ */ new Set();
671
- // Touch state
672
- touchState = {
673
- points: [],
674
- count: 0,
675
- started: [],
676
- moved: [],
677
- ended: [],
678
- primary: null,
679
- gestures: {
680
- isPinching: false,
681
- isRotating: false,
682
- isPanning: false,
683
- isTapping: false,
684
- pinchScale: 1,
685
- pinchDelta: 0,
686
- rotationAngle: 0,
687
- rotationDelta: 0,
688
- panDelta: { x: 0, y: 0 },
689
- tapCount: 0,
690
- lastTapTime: 0,
691
- tapPosition: null
692
- }
693
- };
694
- activeTouches = /* @__PURE__ */ new Map();
695
- gestureState = {
696
- initialDistance: 0,
697
- initialAngle: 0,
698
- lastPinchScale: 1,
699
- lastRotationAngle: 0,
700
- panStartPosition: { x: 0, y: 0 },
701
- tapStartTime: 0,
702
- tapCount: 0,
703
- lastTapTime: 0
704
- };
705
- constructor() {
706
- }
707
- /**
708
- * Processes mouse update from the host
709
- */
710
- updateMouse(data) {
711
- const canvasX = data.x;
712
- const canvasY = data.y;
713
- const deltaX = canvasX - this.mouseState.x;
714
- const deltaY = canvasY - this.mouseState.y;
715
- this.updateMouseVelocity(deltaX, deltaY, data.timestamp);
716
- const prevPressed = this.mouseState.isPressed;
717
- const currentPressed = data.buttons > 0;
718
- this.mouseState.wasPressed = !prevPressed && currentPressed;
719
- this.mouseState.wasReleased = prevPressed && !currentPressed;
720
- this.mouseState.wasMoved = deltaX !== 0 || deltaY !== 0;
721
- this.mouseState.x = canvasX;
722
- this.mouseState.y = canvasY;
723
- this.mouseState.deltaX = deltaX;
724
- this.mouseState.deltaY = deltaY;
725
- this.mouseState.isPressed = currentPressed;
726
- this.mouseState.leftButton = (data.buttons & 1) !== 0;
727
- this.mouseState.rightButton = (data.buttons & 2) !== 0;
728
- this.mouseState.middleButton = (data.buttons & 4) !== 0;
729
- this.mouseState.isInCanvas = data.isInCanvas !== void 0 ? data.isInCanvas : true;
730
- this.mouseState.wheelDelta = data.wheelDeltaY;
731
- this.mouseState.wheelX = data.wheelDeltaX;
732
- this.mouseState.wheelY = data.wheelDeltaY;
733
- }
734
- /**
735
- * Updates mouse velocity with smoothing
736
- */
737
- updateMouseVelocity(deltaX, deltaY, timestamp) {
738
- this.mouseVelocityHistory.push({ x: deltaX, y: deltaY, time: timestamp });
739
- const cutoff = timestamp - 100;
740
- this.mouseVelocityHistory = this.mouseVelocityHistory.filter((sample) => sample.time > cutoff);
741
- if (this.mouseVelocityHistory.length > 1) {
742
- const recent = this.mouseVelocityHistory.slice(-5);
743
- const avgX = recent.reduce((sum, s) => sum + s.x, 0) / recent.length;
744
- const avgY = recent.reduce((sum, s) => sum + s.y, 0) / recent.length;
745
- this.mouseState.velocity.x = avgX;
746
- this.mouseState.velocity.y = avgY;
747
- } else {
748
- this.mouseState.velocity.x = deltaX;
749
- this.mouseState.velocity.y = deltaY;
750
- }
751
- }
752
- /**
753
- * Processes keyboard update from the host
754
- */
755
- updateKeyboard(data) {
756
- const key = data.key.toLowerCase();
757
- if (data.type === "keydown") {
758
- if (!this.activeKeys.has(key)) {
759
- this.activeKeys.add(key);
760
- this.pressedThisFrame.add(key);
761
- this.keyboardState.lastKeyPressed = data.key;
762
- }
763
- } else if (data.type === "keyup") {
764
- this.activeKeys.delete(key);
765
- this.releasedThisFrame.add(key);
766
- this.keyboardState.lastKeyReleased = data.key;
767
- }
768
- this.keyboardState.shift = data.shiftKey;
769
- this.keyboardState.ctrl = data.ctrlKey;
770
- this.keyboardState.alt = data.altKey;
771
- this.keyboardState.meta = data.metaKey;
772
- }
773
- /**
774
- * Processes touch update from the host
775
- */
776
- updateTouch(data) {
777
- this.touchState.started = [];
778
- this.touchState.moved = [];
779
- this.touchState.ended = [];
780
- if (data.type === "touchstart") {
781
- this.processTouchStart(data.touches, data.timestamp);
782
- } else if (data.type === "touchmove") {
783
- this.processTouchMove(data.touches, data.timestamp);
784
- } else if (data.type === "touchend" || data.type === "touchcancel") {
785
- this.processTouchEnd(data.touches, data.timestamp);
786
- }
787
- this.touchState.points = Array.from(this.activeTouches.values());
788
- this.touchState.count = this.touchState.points.length;
789
- this.touchState.primary = this.touchState.points[0] || null;
790
- this.updateGestures();
791
- }
792
- /**
793
- * Processes touch start events
794
- */
795
- processTouchStart(touches, timestamp) {
796
- for (const touch of touches) {
797
- const touchPoint = this.createTouchPoint(touch, timestamp, true);
798
- this.activeTouches.set(touch.identifier, touchPoint);
799
- this.touchState.started.push(touchPoint);
800
- }
801
- if (this.touchState.count === 1) {
802
- this.gestureState.tapStartTime = timestamp;
803
- const touch = this.touchState.points[0];
804
- this.touchState.gestures.tapPosition = { x: touch.x, y: touch.y };
805
- }
806
- }
807
- /**
808
- * Processes touch move events
809
- */
810
- processTouchMove(touches, timestamp) {
811
- for (const touch of touches) {
812
- const existing = this.activeTouches.get(touch.identifier);
813
- if (existing) {
814
- const updated = this.createTouchPoint(touch, timestamp, false, existing);
815
- this.activeTouches.set(touch.identifier, updated);
816
- this.touchState.moved.push(updated);
817
- }
818
- }
819
- }
820
- /**
821
- * Processes touch end events
822
- */
823
- processTouchEnd(touches, timestamp) {
824
- for (const touch of touches) {
825
- const existing = this.activeTouches.get(touch.identifier);
826
- if (existing) {
827
- const ended = { ...existing, isEnding: true, isActive: false };
828
- this.touchState.ended.push(ended);
829
- this.activeTouches.delete(touch.identifier);
830
- }
831
- }
832
- if (this.touchState.count === 0 && this.gestureState.tapStartTime > 0) {
833
- const tapDuration = timestamp - this.gestureState.tapStartTime;
834
- if (tapDuration < 300) {
835
- this.handleTap(timestamp);
836
- }
837
- this.gestureState.tapStartTime = 0;
838
- }
839
- }
840
- /**
841
- * Creates a touch point from raw touch data
842
- */
843
- createTouchPoint(touch, timestamp, isNew, previous) {
844
- const x = touch.clientX;
845
- const y = touch.clientY;
846
- const deltaX = previous ? x - previous.x : 0;
847
- const deltaY = previous ? y - previous.y : 0;
848
- const timeDelta = previous ? timestamp - previous.timestamp : 16;
849
- const velocityX = timeDelta > 0 ? deltaX / timeDelta * 1e3 : 0;
850
- const velocityY = timeDelta > 0 ? deltaY / timeDelta * 1e3 : 0;
851
- return {
852
- id: touch.identifier,
853
- x,
854
- y,
855
- pressure: touch.pressure || 0,
856
- radius: Math.max(touch.radiusX || 0, touch.radiusY || 0),
857
- radiusX: touch.radiusX || 0,
858
- radiusY: touch.radiusY || 0,
859
- rotationAngle: touch.rotationAngle || 0,
860
- force: touch.force || touch.pressure || 0,
861
- deltaX,
862
- deltaY,
863
- velocity: { x: velocityX, y: velocityY },
864
- isNew,
865
- isActive: true,
866
- isEnding: false
867
- };
868
- }
869
- /**
870
- * Updates gesture recognition
871
- */
872
- updateGestures() {
873
- const touches = this.touchState.points;
874
- const gestures = this.touchState.gestures;
875
- if (touches.length === 2) {
876
- const touch1 = touches[0];
877
- const touch2 = touches[1];
878
- const distance = Math.sqrt(
879
- Math.pow(touch2.x - touch1.x, 2) + Math.pow(touch2.y - touch1.y, 2)
880
- );
881
- const angle = Math.atan2(touch2.y - touch1.y, touch2.x - touch1.x);
882
- if (this.gestureState.initialDistance === 0) {
883
- this.gestureState.initialDistance = distance;
884
- this.gestureState.initialAngle = angle;
885
- this.gestureState.lastPinchScale = 1;
886
- this.gestureState.lastRotationAngle = 0;
887
- }
888
- const scale = distance / this.gestureState.initialDistance;
889
- const scaleDelta = scale - this.gestureState.lastPinchScale;
890
- gestures.isPinching = Math.abs(scaleDelta) > 0.01;
891
- gestures.pinchScale = scale;
892
- gestures.pinchDelta = scaleDelta;
893
- this.gestureState.lastPinchScale = scale;
894
- const rotationAngle = angle - this.gestureState.initialAngle;
895
- const rotationDelta = rotationAngle - this.gestureState.lastRotationAngle;
896
- gestures.isRotating = Math.abs(rotationDelta) > 0.02;
897
- gestures.rotationAngle = rotationAngle;
898
- gestures.rotationDelta = rotationDelta;
899
- this.gestureState.lastRotationAngle = rotationAngle;
900
- } else {
901
- this.gestureState.initialDistance = 0;
902
- gestures.isPinching = false;
903
- gestures.isRotating = false;
904
- gestures.pinchDelta = 0;
905
- gestures.rotationDelta = 0;
906
- }
907
- if (touches.length > 0) {
908
- const primaryTouch = touches[0];
909
- if (this.gestureState.panStartPosition.x === 0) {
910
- this.gestureState.panStartPosition = { x: primaryTouch.x, y: primaryTouch.y };
911
- }
912
- const panDeltaX = primaryTouch.x - this.gestureState.panStartPosition.x;
913
- const panDeltaY = primaryTouch.y - this.gestureState.panStartPosition.y;
914
- const panDistance = Math.sqrt(panDeltaX * panDeltaX + panDeltaY * panDeltaY);
915
- gestures.isPanning = panDistance > 10;
916
- gestures.panDelta = { x: panDeltaX, y: panDeltaY };
917
- } else {
918
- this.gestureState.panStartPosition = { x: 0, y: 0 };
919
- gestures.isPanning = false;
920
- gestures.panDelta = { x: 0, y: 0 };
921
- }
922
- }
923
- /**
924
- * Handles tap gesture detection
925
- */
926
- handleTap(timestamp) {
927
- const timeSinceLastTap = timestamp - this.gestureState.lastTapTime;
928
- if (timeSinceLastTap < 300) {
929
- this.gestureState.tapCount++;
930
- } else {
931
- this.gestureState.tapCount = 1;
932
- }
933
- this.touchState.gestures.tapCount = this.gestureState.tapCount;
934
- this.touchState.gestures.lastTapTime = timestamp;
935
- this.touchState.gestures.isTapping = true;
936
- this.gestureState.lastTapTime = timestamp;
937
- }
938
- /**
939
- * Called at the start of each frame to reset frame-based events
940
- */
941
- frameStart() {
942
- this.mouseState.wasPressed = false;
943
- this.mouseState.wasReleased = false;
944
- this.mouseState.wasMoved = false;
945
- this.mouseState.wheelDelta = 0;
946
- this.mouseState.wheelX = 0;
947
- this.mouseState.wheelY = 0;
948
- this.pressedThisFrame.clear();
949
- this.releasedThisFrame.clear();
950
- this.touchState.gestures.isTapping = false;
951
- this.touchState.gestures.pinchDelta = 0;
952
- this.touchState.gestures.rotationDelta = 0;
953
- }
954
- /**
955
- * Get current mouse state (read-only)
956
- */
957
- getMouseState() {
958
- return this.mouseState;
959
- }
960
- /**
961
- * Get current keyboard state (read-only)
962
- */
963
- getKeyboardState() {
964
- return this.keyboardState;
965
- }
966
- /**
967
- * Get current touch state (read-only)
968
- */
969
- getTouchState() {
970
- return this.touchState;
971
- }
972
- /**
973
- * Cleanup resources
974
- */
975
- destroy() {
976
- this.mouseVelocityHistory.length = 0;
977
- this.activeTouches.clear();
978
- this.activeKeys.clear();
979
- this.pressedThisFrame.clear();
980
- this.releasedThisFrame.clear();
981
- }
982
- }
983
- class AudioSystem {
984
- // Audio context and analysis nodes
985
- audioContext = null;
986
- analyser = null;
987
- mediaStreamSource = null;
988
- currentStream = null;
989
- // Debug logging control
990
- debugMode = false;
991
- /**
992
- * Enable or disable debug logging
993
- */
994
- setDebugMode(enabled) {
995
- this.debugMode = enabled;
996
- }
997
- /**
998
- * Debug logging helper
999
- */
1000
- debugLog(message, ...args) {
1001
- if (this.debugMode) {
1002
- console.log(message, ...args);
1003
- }
1004
- }
1005
- // Analysis configuration (good balance, leaning towards quality)
1006
- fftSize = 2048;
1007
- // Good balance for quality vs performance
1008
- smoothingTimeConstant = 0.8;
1009
- // Smooth but responsive
1010
- // Analysis data arrays
1011
- frequencyData = null;
1012
- timeDomainData = null;
1013
- // Audio analysis state (host-side state)
1014
- audioState = {
1015
- isConnected: false,
1016
- volume: {
1017
- rms: 0,
1018
- peak: 0
1019
- },
1020
- bands: {
1021
- bass: 0,
1022
- mid: 0,
1023
- treble: 0,
1024
- subBass: 0,
1025
- lowMid: 0,
1026
- highMid: 0,
1027
- presence: 0,
1028
- brilliance: 0
1029
- }
1030
- };
1031
- // Analysis loop
1032
- analysisLoopId = null;
1033
- isAnalysisRunning = false;
1034
- // Callback to send results to worker
1035
- sendAnalysisResults = null;
1036
- constructor(sendAnalysisResultsCallback) {
1037
- this.handleAudioStreamUpdate = this.handleAudioStreamUpdate.bind(this);
1038
- this.performAnalysis = this.performAnalysis.bind(this);
1039
- this.sendAnalysisResults = sendAnalysisResultsCallback || null;
1040
- }
1041
- /**
1042
- * Get the current audio analysis state (for host-side usage)
1043
- */
1044
- getAudioState() {
1045
- return { ...this.audioState };
1046
- }
1047
- /**
1048
- * Handle audio stream update (called from VijiCore)
1049
- */
1050
- handleAudioStreamUpdate(data) {
1051
- try {
1052
- if (data.audioStream) {
1053
- this.setAudioStream(data.audioStream);
1054
- } else {
1055
- this.disconnectAudioStream();
1056
- }
1057
- if (data.analysisConfig) {
1058
- this.updateAnalysisConfig(data.analysisConfig);
1059
- }
1060
- } catch (error) {
1061
- console.error("Error handling audio stream update:", error);
1062
- this.audioState.isConnected = false;
1063
- this.sendAnalysisResultsToWorker();
1064
- }
1065
- }
1066
- /**
1067
- * Set the audio stream for analysis
1068
- */
1069
- async setAudioStream(audioStream) {
1070
- this.disconnectAudioStream();
1071
- const audioTracks = audioStream.getAudioTracks();
1072
- if (audioTracks.length === 0) {
1073
- console.warn("No audio tracks in provided stream");
1074
- this.audioState.isConnected = false;
1075
- this.sendAnalysisResultsToWorker();
1076
- return;
1077
- }
1078
- try {
1079
- if (!this.audioContext) {
1080
- this.audioContext = new AudioContext();
1081
- if (this.audioContext.state === "suspended") {
1082
- await this.audioContext.resume();
1083
- }
1084
- }
1085
- this.analyser = this.audioContext.createAnalyser();
1086
- this.analyser.fftSize = this.fftSize;
1087
- this.analyser.smoothingTimeConstant = this.smoothingTimeConstant;
1088
- this.mediaStreamSource = this.audioContext.createMediaStreamSource(audioStream);
1089
- this.mediaStreamSource.connect(this.analyser);
1090
- const bufferLength = this.analyser.frequencyBinCount;
1091
- this.frequencyData = new Uint8Array(bufferLength);
1092
- this.timeDomainData = new Uint8Array(bufferLength);
1093
- this.currentStream = audioStream;
1094
- this.audioState.isConnected = true;
1095
- this.startAnalysisLoop();
1096
- this.debugLog("Audio stream connected successfully (host-side)", {
1097
- sampleRate: this.audioContext.sampleRate,
1098
- fftSize: this.fftSize,
1099
- bufferLength
1100
- });
1101
- } catch (error) {
1102
- console.error("Failed to set up audio analysis:", error);
1103
- this.audioState.isConnected = false;
1104
- this.disconnectAudioStream();
1105
- }
1106
- this.sendAnalysisResultsToWorker();
1107
- }
1108
- /**
1109
- * Disconnect current audio stream and clean up resources
1110
- */
1111
- disconnectAudioStream() {
1112
- this.stopAnalysisLoop();
1113
- if (this.mediaStreamSource) {
1114
- this.mediaStreamSource.disconnect();
1115
- this.mediaStreamSource = null;
1116
- }
1117
- if (this.analyser) {
1118
- this.analyser.disconnect();
1119
- this.analyser = null;
1120
- }
1121
- this.frequencyData = null;
1122
- this.timeDomainData = null;
1123
- this.currentStream = null;
1124
- this.audioState.isConnected = false;
1125
- this.resetAudioValues();
1126
- this.sendAnalysisResultsToWorker();
1127
- this.debugLog("Audio stream disconnected (host-side)");
1128
- }
1129
- /**
1130
- * Update analysis configuration
1131
- */
1132
- updateAnalysisConfig(config) {
1133
- let needsReconnect = false;
1134
- if (config.fftSize && config.fftSize !== this.fftSize) {
1135
- this.fftSize = config.fftSize;
1136
- needsReconnect = true;
1137
- }
1138
- if (config.smoothing !== void 0) {
1139
- this.smoothingTimeConstant = config.smoothing;
1140
- if (this.analyser) {
1141
- this.analyser.smoothingTimeConstant = this.smoothingTimeConstant;
1142
- }
1143
- }
1144
- if (needsReconnect && this.currentStream) {
1145
- const stream = this.currentStream;
1146
- this.setAudioStream(stream);
1147
- }
1148
- }
1149
- /**
1150
- * Start the audio analysis loop
1151
- */
1152
- startAnalysisLoop() {
1153
- if (this.isAnalysisRunning) return;
1154
- this.isAnalysisRunning = true;
1155
- this.performAnalysis();
1156
- }
1157
- /**
1158
- * Stop the audio analysis loop
1159
- */
1160
- stopAnalysisLoop() {
1161
- this.isAnalysisRunning = false;
1162
- if (this.analysisLoopId !== null) {
1163
- cancelAnimationFrame(this.analysisLoopId);
1164
- this.analysisLoopId = null;
1165
- }
1166
- }
1167
- /**
1168
- * Perform audio analysis (called every frame)
1169
- */
1170
- performAnalysis() {
1171
- if (!this.isAnalysisRunning || !this.analyser || !this.frequencyData || !this.timeDomainData) {
1172
- return;
1173
- }
1174
- this.analyser.getByteFrequencyData(this.frequencyData);
1175
- this.analyser.getByteTimeDomainData(this.timeDomainData);
1176
- this.calculateVolumeMetrics();
1177
- this.calculateFrequencyBands();
1178
- this.sendAnalysisResultsToWorker();
1179
- this.analysisLoopId = requestAnimationFrame(() => this.performAnalysis());
1180
- }
1181
- /**
1182
- * Calculate RMS and peak volume from time domain data
1183
- */
1184
- calculateVolumeMetrics() {
1185
- if (!this.timeDomainData) return;
1186
- let rmsSum = 0;
1187
- let peak = 0;
1188
- for (let i = 0; i < this.timeDomainData.length; i++) {
1189
- const sample = (this.timeDomainData[i] - 128) / 128;
1190
- rmsSum += sample * sample;
1191
- const absValue = Math.abs(sample);
1192
- if (absValue > peak) {
1193
- peak = absValue;
1194
- }
1195
- }
1196
- const rms = Math.sqrt(rmsSum / this.timeDomainData.length);
1197
- this.audioState.volume.rms = rms;
1198
- this.audioState.volume.peak = peak;
1199
- }
1200
- /**
1201
- * Calculate frequency band values from frequency data
1202
- */
1203
- calculateFrequencyBands() {
1204
- if (!this.frequencyData || !this.audioContext) return;
1205
- const nyquist = this.audioContext.sampleRate / 2;
1206
- const binCount = this.frequencyData.length;
1207
- const bands = {
1208
- subBass: { min: 20, max: 60 },
1209
- // Sub-bass
1210
- bass: { min: 60, max: 250 },
1211
- // Bass
1212
- lowMid: { min: 250, max: 500 },
1213
- // Low midrange
1214
- mid: { min: 500, max: 2e3 },
1215
- // Midrange
1216
- highMid: { min: 2e3, max: 4e3 },
1217
- // High midrange
1218
- presence: { min: 4e3, max: 6e3 },
1219
- // Presence
1220
- brilliance: { min: 6e3, max: 2e4 },
1221
- // Brilliance
1222
- treble: { min: 2e3, max: 2e4 }
1223
- // Treble (combined high frequencies)
1224
- };
1225
- for (const [bandName, range] of Object.entries(bands)) {
1226
- const startBin = Math.floor(range.min / nyquist * binCount);
1227
- const endBin = Math.min(Math.floor(range.max / nyquist * binCount), binCount - 1);
1228
- let sum = 0;
1229
- let count = 0;
1230
- for (let i = startBin; i <= endBin; i++) {
1231
- sum += this.frequencyData[i];
1232
- count++;
1233
- }
1234
- const average = count > 0 ? sum / count : 0;
1235
- this.audioState.bands[bandName] = average / 255;
1236
- }
1237
- }
1238
- /**
1239
- * Send analysis results to worker
1240
- */
1241
- sendAnalysisResultsToWorker() {
1242
- if (this.sendAnalysisResults) {
1243
- const frequencyData = this.frequencyData ? new Uint8Array(this.frequencyData) : new Uint8Array(0);
1244
- this.sendAnalysisResults({
1245
- type: "audio-analysis-update",
1246
- data: {
1247
- ...this.audioState,
1248
- frequencyData,
1249
- // For getFrequencyData() access
1250
- timestamp: performance.now()
1251
- }
1252
- });
1253
- }
1254
- }
1255
- /**
1256
- * Reset audio values to defaults
1257
- */
1258
- resetAudioValues() {
1259
- this.audioState.volume.rms = 0;
1260
- this.audioState.volume.peak = 0;
1261
- for (const band in this.audioState.bands) {
1262
- this.audioState.bands[band] = 0;
1263
- }
1264
- }
1265
- /**
1266
- * Reset all audio state (called when destroying)
1267
- */
1268
- resetAudioState() {
1269
- this.disconnectAudioStream();
1270
- if (this.audioContext && this.audioContext.state !== "closed") {
1271
- this.audioContext.close();
1272
- this.audioContext = null;
1273
- }
1274
- this.resetAudioValues();
1275
- }
1276
- /**
1277
- * Get current analysis configuration
1278
- */
1279
- getAnalysisConfig() {
1280
- return {
1281
- fftSize: this.fftSize,
1282
- smoothing: this.smoothingTimeConstant
1283
- };
1284
- }
1285
- }
1286
- class VideoCoordinator {
1287
- // Video elements for MediaStream processing
1288
- videoElement = null;
1289
- canvas = null;
1290
- ctx = null;
1291
- // Note: currentStream was removed as it was unused
1292
- // Transfer coordination
1293
- transferLoopId = null;
1294
- isTransferRunning = false;
1295
- lastTransferTime = 0;
1296
- transferInterval = 1e3 / 30;
1297
- // Transfer at 30 FPS
1298
- // Video state (lightweight - main state is in worker)
1299
- coordinatorState = {
1300
- isConnected: false,
1301
- sourceType: "",
1302
- frameWidth: 0,
1303
- frameHeight: 0
1304
- };
1305
- // Track if OffscreenCanvas has been sent to worker
1306
- hasTransferredCanvas = false;
1307
- // Callback to send data to worker
1308
- sendToWorker = null;
1309
- // Debug logging control
1310
- debugMode = false;
1311
- /**
1312
- * Enable or disable debug logging
1313
- */
1314
- setDebugMode(enabled) {
1315
- this.debugMode = enabled;
1316
- }
1317
- /**
1318
- * Debug logging helper
1319
- */
1320
- debugLog(message, ...args) {
1321
- if (this.debugMode) {
1322
- console.log(message, ...args);
1323
- }
1324
- }
1325
- constructor(sendToWorkerCallback) {
1326
- this.handleVideoStreamUpdate = this.handleVideoStreamUpdate.bind(this);
1327
- this.transferVideoFrame = this.transferVideoFrame.bind(this);
1328
- this.sendToWorker = sendToWorkerCallback || null;
1329
- }
1330
- /**
1331
- * Get the current video coordinator state (for host-side usage)
1332
- */
1333
- getCoordinatorState() {
1334
- return { ...this.coordinatorState };
1335
- }
1336
- /**
1337
- * Handle video stream update (called from VijiCore)
1338
- */
1339
- handleVideoStreamUpdate(data) {
1340
- try {
1341
- if (data.videoStream) {
1342
- this.setVideoStream(data.videoStream);
1343
- } else {
1344
- this.disconnectVideoStream();
1345
- }
1346
- if (data.targetFrameRate || data.cvConfig) {
1347
- this.sendConfigurationToWorker({
1348
- ...data.targetFrameRate && { targetFrameRate: data.targetFrameRate },
1349
- ...data.cvConfig && { cvConfig: data.cvConfig },
1350
- timestamp: data.timestamp
1351
- });
1352
- }
1353
- } catch (error) {
1354
- console.error("Error handling video stream update:", error);
1355
- this.coordinatorState.isConnected = false;
1356
- this.sendDisconnectionToWorker();
1357
- }
1358
- }
1359
- /**
1360
- * Set the video stream for processing
1361
- */
1362
- async setVideoStream(videoStream) {
1363
- this.disconnectVideoStream();
1364
- const videoTracks = videoStream.getVideoTracks();
1365
- if (videoTracks.length === 0) {
1366
- console.warn("No video tracks in provided stream");
1367
- this.coordinatorState.isConnected = false;
1368
- this.sendDisconnectionToWorker();
1369
- return;
1370
- }
1371
- try {
1372
- this.videoElement = document.createElement("video");
1373
- this.videoElement.autoplay = true;
1374
- this.videoElement.muted = true;
1375
- this.videoElement.playsInline = true;
1376
- this.canvas = document.createElement("canvas");
1377
- this.ctx = this.canvas.getContext("2d");
1378
- if (!this.ctx) {
1379
- throw new Error("Failed to get 2D context from canvas");
1380
- }
1381
- this.videoElement.srcObject = videoStream;
1382
- await new Promise((resolve, reject) => {
1383
- const timeout = setTimeout(() => reject(new Error("Video metadata load timeout")), 5e3);
1384
- this.videoElement.addEventListener("loadedmetadata", async () => {
1385
- clearTimeout(timeout);
1386
- try {
1387
- await this.videoElement.play();
1388
- this.debugLog("✅ Video element is now playing:", {
1389
- videoWidth: this.videoElement.videoWidth,
1390
- videoHeight: this.videoElement.videoHeight,
1391
- readyState: this.videoElement.readyState,
1392
- paused: this.videoElement.paused
1393
- });
1394
- resolve();
1395
- } catch (playError) {
1396
- console.error("🔴 Failed to start video playback:", playError);
1397
- reject(playError);
1398
- }
1399
- }, { once: true });
1400
- this.videoElement.addEventListener("error", (e) => {
1401
- clearTimeout(timeout);
1402
- reject(new Error(`Video load error: ${e}`));
1403
- }, { once: true });
1404
- });
1405
- this.canvas.width = this.videoElement.videoWidth;
1406
- this.canvas.height = this.videoElement.videoHeight;
1407
- this.coordinatorState.isConnected = true;
1408
- this.coordinatorState.frameWidth = this.videoElement.videoWidth;
1409
- this.coordinatorState.frameHeight = this.videoElement.videoHeight;
1410
- this.coordinatorState.sourceType = "MediaStream";
1411
- await this.transferOffscreenCanvasToWorker(this.videoElement.videoWidth, this.videoElement.videoHeight);
1412
- this.startTransferLoop();
1413
- this.debugLog("Video stream connected successfully (host-side coordinator)", {
1414
- width: this.videoElement.videoWidth,
1415
- height: this.videoElement.videoHeight
1416
- });
1417
- } catch (error) {
1418
- console.error("Failed to set up video coordination:", error);
1419
- this.coordinatorState.isConnected = false;
1420
- this.disconnectVideoStream();
1421
- }
1422
- }
1423
- /**
1424
- * ✅ CORRECT: Transfer OffscreenCanvas to worker BEFORE getting context
1425
- */
1426
- async transferOffscreenCanvasToWorker(width, height) {
1427
- if (this.hasTransferredCanvas) {
1428
- this.sendConfigurationToWorker({
1429
- width,
1430
- height,
1431
- timestamp: performance.now()
1432
- });
1433
- return;
1434
- }
1435
- try {
1436
- const offscreenCanvas = new OffscreenCanvas(width, height);
1437
- if (this.sendToWorker) {
1438
- this.sendToWorker({
1439
- type: "video-canvas-setup",
1440
- data: {
1441
- offscreenCanvas,
1442
- width,
1443
- height,
1444
- timestamp: performance.now()
1445
- }
1446
- }, [offscreenCanvas]);
1447
- this.hasTransferredCanvas = true;
1448
- this.debugLog("✅ OffscreenCanvas transferred to worker (correct approach)", {
1449
- width,
1450
- height
1451
- });
1452
- }
1453
- } catch (error) {
1454
- console.error("Failed to transfer OffscreenCanvas to worker:", error);
1455
- throw error;
1456
- }
1457
- }
1458
- /**
1459
- * Disconnect current video stream and clean up resources
1460
- */
1461
- disconnectVideoStream() {
1462
- this.stopTransferLoop();
1463
- if (this.videoElement) {
1464
- this.videoElement.pause();
1465
- this.videoElement.srcObject = null;
1466
- this.videoElement = null;
1467
- }
1468
- if (this.canvas) {
1469
- this.canvas = null;
1470
- this.ctx = null;
1471
- }
1472
- this.coordinatorState.isConnected = false;
1473
- this.coordinatorState.frameWidth = 0;
1474
- this.coordinatorState.frameHeight = 0;
1475
- this.coordinatorState.sourceType = "";
1476
- this.hasTransferredCanvas = false;
1477
- this.sendDisconnectionToWorker();
1478
- this.debugLog("Video stream disconnected (host-side coordinator)");
1479
- }
1480
- /**
1481
- * Start the video frame transfer loop
1482
- */
1483
- startTransferLoop() {
1484
- if (this.isTransferRunning) return;
1485
- this.isTransferRunning = true;
1486
- this.lastTransferTime = performance.now();
1487
- this.transferVideoFrame();
1488
- }
1489
- /**
1490
- * Stop the video frame transfer loop
1491
- */
1492
- stopTransferLoop() {
1493
- this.isTransferRunning = false;
1494
- if (this.transferLoopId !== null) {
1495
- cancelAnimationFrame(this.transferLoopId);
1496
- this.transferLoopId = null;
1497
- }
1498
- }
1499
- /**
1500
- * Transfer video frame to worker using ImageBitmap (for worker to draw on its OffscreenCanvas)
1501
- */
1502
- transferVideoFrame() {
1503
- if (!this.isTransferRunning || !this.videoElement || !this.canvas || !this.ctx) {
1504
- if (!this.isTransferRunning) {
1505
- this.debugLog("🔴 Transfer loop stopped");
1506
- }
1507
- return;
1508
- }
1509
- const currentTime = performance.now();
1510
- const deltaTime = currentTime - this.lastTransferTime;
1511
- if (deltaTime >= this.transferInterval) {
1512
- if (Math.random() < 0.01) {
1513
- this.debugLog(`🔄 Transfer loop tick: ${deltaTime.toFixed(1)}ms since last frame`);
1514
- }
1515
- this.transferFrameToWorker().catch((error) => {
1516
- console.error("🔴 Error transferring video frame to worker:", error);
1517
- });
1518
- this.lastTransferTime = currentTime;
1519
- }
1520
- this.transferLoopId = requestAnimationFrame(() => this.transferVideoFrame());
1521
- }
1522
- /**
1523
- * Async frame transfer using ImageBitmap (for worker to draw)
1524
- */
1525
- async transferFrameToWorker() {
1526
- if (!this.videoElement || !this.canvas || !this.ctx) {
1527
- console.warn("🔴 Frame transfer called but missing elements:", {
1528
- hasVideo: !!this.videoElement,
1529
- hasCanvas: !!this.canvas,
1530
- hasCtx: !!this.ctx
1531
- });
1532
- return;
1533
- }
1534
- try {
1535
- if (this.videoElement.readyState < 2) {
1536
- console.warn("🔴 Video not ready for frame capture, readyState:", this.videoElement.readyState);
1537
- return;
1538
- }
1539
- if (this.videoElement.videoWidth === 0 || this.videoElement.videoHeight === 0) {
1540
- console.warn("🔴 Video has no dimensions:", {
1541
- width: this.videoElement.videoWidth,
1542
- height: this.videoElement.videoHeight
1543
- });
1544
- return;
1545
- }
1546
- this.ctx.drawImage(this.videoElement, 0, 0, this.canvas.width, this.canvas.height);
1547
- const imageBitmap = await createImageBitmap(this.canvas);
1548
- if (Math.random() < 0.01) {
1549
- this.debugLog("✅ Frame captured and ImageBitmap created:", {
1550
- videoDimensions: `${this.videoElement.videoWidth}x${this.videoElement.videoHeight}`,
1551
- canvasDimensions: `${this.canvas.width}x${this.canvas.height}`,
1552
- bitmapDimensions: `${imageBitmap.width}x${imageBitmap.height}`
1553
- });
1554
- }
1555
- this.sendFrameToWorker(imageBitmap);
1556
- } catch (error) {
1557
- console.error("🔴 Failed to create ImageBitmap:", error);
1558
- }
1559
- }
1560
- /**
1561
- * Send ImageBitmap frame to worker (for worker to draw on its OffscreenCanvas)
1562
- */
1563
- sendFrameToWorker(imageBitmap) {
1564
- if (this.sendToWorker) {
1565
- this.sendToWorker({
1566
- type: "video-frame-update",
1567
- data: {
1568
- imageBitmap,
1569
- timestamp: performance.now()
1570
- }
1571
- }, [imageBitmap]);
1572
- }
1573
- }
1574
- /**
1575
- * Send configuration updates to worker
1576
- */
1577
- sendConfigurationToWorker(config) {
1578
- if (this.sendToWorker) {
1579
- this.sendToWorker({
1580
- type: "video-config-update",
1581
- data: config
1582
- });
1583
- }
1584
- }
1585
- /**
1586
- * Send disconnection notification to worker
1587
- */
1588
- sendDisconnectionToWorker() {
1589
- if (this.sendToWorker) {
1590
- this.sendToWorker({
1591
- type: "video-config-update",
1592
- data: {
1593
- disconnect: true,
1594
- timestamp: performance.now()
1595
- }
1596
- });
1597
- }
1598
- }
1599
- /**
1600
- * Handle image file as video source
1601
- */
1602
- async setImageSource(imageFile) {
1603
- try {
1604
- const img = new Image();
1605
- const url = URL.createObjectURL(imageFile);
1606
- await new Promise((resolve, reject) => {
1607
- img.onload = () => {
1608
- URL.revokeObjectURL(url);
1609
- resolve();
1610
- };
1611
- img.onerror = () => {
1612
- URL.revokeObjectURL(url);
1613
- reject(new Error("Failed to load image"));
1614
- };
1615
- img.src = url;
1616
- });
1617
- this.disconnectVideoStream();
1618
- this.coordinatorState.isConnected = true;
1619
- this.coordinatorState.frameWidth = img.width;
1620
- this.coordinatorState.frameHeight = img.height;
1621
- this.coordinatorState.sourceType = "Image";
1622
- await this.transferOffscreenCanvasToWorker(img.width, img.height);
1623
- const imageBitmap = await createImageBitmap(img);
1624
- this.sendFrameToWorker(imageBitmap);
1625
- this.debugLog("Image source set successfully (host-side coordinator)", {
1626
- width: img.width,
1627
- height: img.height
1628
- });
1629
- } catch (error) {
1630
- console.error("Failed to set image source:", error);
1631
- this.coordinatorState.isConnected = false;
1632
- this.sendDisconnectionToWorker();
1633
- }
1634
- }
1635
- /**
1636
- * Reset all video coordinator state (called when destroying)
1637
- */
1638
- resetVideoState() {
1639
- this.disconnectVideoStream();
1640
- }
1641
- /**
1642
- * Get current coordinator configuration
1643
- */
1644
- getCoordinatorConfig() {
1645
- return {
1646
- transferInterval: this.transferInterval
1647
- };
1648
- }
1649
- }
1650
- class VijiCore {
1651
- iframeManager = null;
1652
- workerManager = null;
1653
- interactionManager = null;
1654
- audioSystem = null;
1655
- videoCoordinator = null;
1656
- isInitialized = false;
1657
- isDestroyed = false;
1658
- isInitializing = false;
1659
- instanceId;
1660
- screenRefreshRate = 60;
1661
- // Will be detected
1662
- debugMode = false;
1663
- // Debug logging control
1664
- /**
1665
- * Debug logging helper
1666
- */
1667
- debugLog(message, ...args) {
1668
- if (this.debugMode) {
1669
- console.log(message, ...args);
1670
- }
1671
- }
1672
- // Configuration
1673
- config;
1674
- // Audio stream management
1675
- currentAudioStream = null;
1676
- // Video stream management
1677
- currentVideoStream = null;
1678
- // Interaction state management
1679
- currentInteractionEnabled;
1680
- // Parameter system for Phase 2
1681
- parameterGroups = /* @__PURE__ */ new Map();
1682
- parameterValues = /* @__PURE__ */ new Map();
1683
- parametersInitialized = false;
1684
- // Event listeners for parameter system
1685
- parameterListeners = /* @__PURE__ */ new Map();
1686
- parameterDefinedListeners = /* @__PURE__ */ new Set();
1687
- parameterErrorListeners = /* @__PURE__ */ new Set();
1688
- capabilitiesChangeListeners = /* @__PURE__ */ new Set();
1689
- // Performance tracking (basic for Phase 1)
1690
- stats = {
1691
- frameTime: 0,
1692
- resolution: { width: 0, height: 0 },
1693
- scale: 1,
1694
- frameRate: {
1695
- mode: "full",
1696
- screenRefreshRate: 60,
1697
- effectiveRefreshRate: 60
1698
- },
1699
- rendererType: "native",
1700
- parameterCount: 0
1701
- };
1702
- constructor(config) {
1703
- this.validateConfig(config);
1704
- this.instanceId = `viji-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1705
- this.config = {
1706
- ...config,
1707
- frameRateMode: config.frameRateMode || "full",
1708
- autoOptimize: config.autoOptimize ?? true,
1709
- parameters: config.parameters || [],
1710
- noInputs: config.noInputs ?? false,
1711
- allowUserInteraction: config.allowUserInteraction ?? true
1712
- };
1713
- this.currentInteractionEnabled = this.config.allowUserInteraction;
1714
- this.debugLog(`VijiCore instance created: ${this.instanceId}`);
1715
- }
1716
- /**
1717
- * Capture current scene frame as an image Blob.
1718
- * Resolution can be a scale (0-1+) or explicit width/height with center-crop.
1719
- */
1720
- async captureFrame(options = {}) {
1721
- if (!this.workerManager || !this.workerManager.ready) {
1722
- throw new VijiCoreError("Core not initialized", "NOT_INITIALIZED");
1723
- }
1724
- const response = await this.workerManager.sendMessage("capture-frame", {
1725
- type: options.type,
1726
- resolution: options.resolution
1727
- }, 1e4);
1728
- const buffer = response.buffer;
1729
- const mimeType = response.mimeType || options.type || "image/jpeg";
1730
- return new Blob([buffer], { type: mimeType });
1731
- }
1732
- /**
1733
- * Enable or disable debug logging
1734
- */
1735
- setDebugMode(enabled) {
1736
- this.debugMode = enabled;
1737
- if (this.iframeManager && "setDebugMode" in this.iframeManager) {
1738
- this.iframeManager.setDebugMode(enabled);
1739
- }
1740
- if (this.workerManager && "setDebugMode" in this.workerManager) {
1741
- this.workerManager.setDebugMode(enabled);
1742
- }
1743
- if (this.interactionManager && "setDebugMode" in this.interactionManager) {
1744
- this.interactionManager.setDebugMode(enabled);
1745
- }
1746
- if (this.audioSystem && "setDebugMode" in this.audioSystem) {
1747
- this.audioSystem.setDebugMode(enabled);
1748
- }
1749
- if (this.videoCoordinator && "setDebugMode" in this.videoCoordinator) {
1750
- this.videoCoordinator.setDebugMode(enabled);
1751
- }
1752
- if (this.workerManager) {
1753
- this.workerManager.postMessage("debug-mode", { enabled });
1754
- }
1755
- }
1756
- /**
1757
- * Get current debug mode status
1758
- */
1759
- getDebugMode() {
1760
- return this.debugMode;
1761
- }
1762
- /**
1763
- * Initializes the core components in sequence
1764
- */
1765
- async initialize() {
1766
- try {
1767
- if (this.isDestroyed) {
1768
- throw new VijiCoreError("Cannot initialize destroyed instance", "INSTANCE_DESTROYED");
1769
- }
1770
- if (this.isInitializing) {
1771
- throw new VijiCoreError("Initialization already in progress", "CONCURRENT_INITIALIZATION");
1772
- }
1773
- if (this.isInitialized) {
1774
- throw new VijiCoreError("Core already initialized", "ALREADY_INITIALIZED");
1775
- }
1776
- this.isInitializing = true;
1777
- this.debugLog(`Starting VijiCore initialization... (${this.instanceId})`);
1778
- this.config.hostContainer.innerHTML = "";
1779
- this.iframeManager = new IFrameManager(
1780
- this.config.hostContainer
1781
- );
1782
- await this.iframeManager.createSecureIFrame();
1783
- const offscreenCanvas = await this.createCanvasWithRetry();
1784
- this.interactionManager = new InteractionManager();
1785
- this.setupInteractionSystem();
1786
- this.workerManager = new WorkerManager(
1787
- this.config.sceneCode,
1788
- offscreenCanvas
1789
- );
1790
- this.setupCommunication();
1791
- await this.workerManager.createWorker();
1792
- this.audioSystem = new AudioSystem((message) => {
1793
- if (this.workerManager) {
1794
- this.workerManager.postMessage(message.type, message.data);
1795
- }
1796
- });
1797
- this.videoCoordinator = new VideoCoordinator((message, transfer) => {
1798
- if (this.workerManager) {
1799
- if (transfer && transfer.length > 0) {
1800
- this.workerManager.postMessage(message.type, message.data, transfer);
1801
- } else {
1802
- this.workerManager.postMessage(message.type, message.data);
1803
- }
1804
- }
1805
- });
1806
- const effectiveResolution = this.iframeManager.getEffectiveResolution();
1807
- this.workerManager.postMessage("resolution-update", {
1808
- effectiveWidth: effectiveResolution.width,
1809
- effectiveHeight: effectiveResolution.height
1810
- });
1811
- await this.detectScreenRefreshRate();
1812
- this.workerManager.postMessage("refresh-rate-update", {
1813
- screenRefreshRate: this.screenRefreshRate
1814
- });
1815
- if (this.config.audioStream) {
1816
- await this.setAudioStream(this.config.audioStream);
1817
- }
1818
- if (this.config.videoStream) {
1819
- await this.setVideoStream(this.config.videoStream);
1820
- }
1821
- this.stats.resolution = effectiveResolution;
1822
- this.stats.scale = this.iframeManager.getScale();
1823
- this.updateFrameRateStats();
1824
- this.isInitialized = true;
1825
- this.isInitializing = false;
1826
- this.debugLog(`VijiCore initialized successfully (${this.instanceId}):`, {
1827
- resolution: `${effectiveResolution.width}x${effectiveResolution.height}`,
1828
- frameRateMode: this.config.frameRateMode,
1829
- hasAudio: !!this.config.audioStream,
1830
- hasVideo: !!this.config.videoStream
1831
- });
1832
- } catch (error) {
1833
- this.isInitializing = false;
1834
- await this.cleanup();
1835
- throw new VijiCoreError(
1836
- `Failed to initialize VijiCore: ${error}`,
1837
- "INITIALIZATION_ERROR",
1838
- { error, config: this.config }
1839
- );
1840
- }
1841
- }
1842
- /**
1843
- * Creates canvas with retry logic to handle timing issues
1844
- */
1845
- async createCanvasWithRetry() {
1846
- const maxRetries = 3;
1847
- const retryDelay = 200;
1848
- for (let i = 0; i < maxRetries; i++) {
1849
- if (this.isDestroyed || !this.isInitializing) {
1850
- throw new VijiCoreError("Initialization cancelled", "INITIALIZATION_CANCELLED");
1851
- }
1852
- if (!this.iframeManager) {
1853
- throw new VijiCoreError("IFrameManager not available", "MANAGER_NOT_READY");
1854
- }
1855
- try {
1856
- return await this.iframeManager.createCanvas();
1857
- } catch (error) {
1858
- console.warn(`Canvas creation attempt ${i + 1}/${maxRetries} failed:`, error);
1859
- if (i === maxRetries - 1) {
1860
- throw error;
1861
- }
1862
- await new Promise((resolve) => setTimeout(resolve, retryDelay));
1863
- }
1864
- }
1865
- throw new VijiCoreError("Canvas creation failed after all retries", "CANVAS_CREATION_TIMEOUT");
1866
- }
1867
- /**
1868
- * Sets up the interaction system for Phase 7
1869
- */
1870
- setupInteractionSystem() {
1871
- if (!this.iframeManager || !this.interactionManager) return;
1872
- this.iframeManager.onInteractionEvent("mouse-update", (data) => {
1873
- this.interactionManager?.updateMouse(data);
1874
- if (this.workerManager) {
1875
- this.workerManager.postMessage("mouse-update", data);
1876
- }
1877
- });
1878
- this.iframeManager.onInteractionEvent("keyboard-update", (data) => {
1879
- this.interactionManager?.updateKeyboard(data);
1880
- if (this.workerManager) {
1881
- this.workerManager.postMessage("keyboard-update", data);
1882
- }
1883
- });
1884
- this.iframeManager.onInteractionEvent("touch-update", (data) => {
1885
- this.interactionManager?.updateTouch(data);
1886
- if (this.workerManager) {
1887
- this.workerManager.postMessage("touch-update", data);
1888
- }
1889
- });
1890
- this.iframeManager.setInteractionEnabled(this.currentInteractionEnabled);
1891
- }
1892
- /**
1893
- * Sets up communication between components
1894
- */
1895
- setupCommunication() {
1896
- if (!this.workerManager) return;
1897
- this.workerManager.onMessage("ready", (data) => {
1898
- this.debugLog("Worker ready:", data);
1899
- if (data.rendererType !== void 0) {
1900
- this.stats.rendererType = data.rendererType;
1901
- }
1902
- });
1903
- this.workerManager.onMessage("error", (data) => {
1904
- console.error("Worker error:", data);
1905
- });
1906
- this.workerManager.onMessage("performance-warning", (data) => {
1907
- console.warn("Performance warning:", data);
1908
- });
1909
- this.workerManager.onMessage("performance-update", (data) => {
1910
- if (data.effectiveRefreshRate !== void 0) {
1911
- this.stats.frameRate.effectiveRefreshRate = data.effectiveRefreshRate;
1912
- this.stats.frameRate.mode = data.frameRateMode;
1913
- this.stats.frameRate.screenRefreshRate = data.screenRefreshRate;
1914
- }
1915
- if (data.rendererType !== void 0) {
1916
- this.stats.rendererType = data.rendererType;
1917
- }
1918
- if (data.parameterCount !== void 0) {
1919
- this.stats.parameterCount = data.parameterCount;
1920
- }
1921
- if (data.cv !== void 0) {
1922
- this.stats.cv = data.cv;
1923
- }
1924
- });
1925
- this.workerManager.onMessage("parameters-defined", (data) => {
1926
- this.handleParametersDefined(data);
1927
- });
1928
- this.workerManager.onMessage("parameter-validation-error", (data) => {
1929
- this.handleParameterError(data);
1930
- });
1931
- }
1932
- // Parameter system implementation for Phase 2
1933
- /**
1934
- * Handle parameter definitions received from worker
1935
- */
1936
- handleParametersDefined(data) {
1937
- try {
1938
- this.parameterGroups.clear();
1939
- this.parameterValues.clear();
1940
- for (const group of data.groups) {
1941
- this.parameterGroups.set(group.groupName, group);
1942
- for (const [paramName, paramDef] of Object.entries(group.parameters)) {
1943
- this.parameterValues.set(paramName, paramDef.defaultValue);
1944
- }
1945
- }
1946
- this.parametersInitialized = true;
1947
- this.debugLog(`Parameters initialized: ${this.parameterValues.size} parameters in ${this.parameterGroups.size} groups`);
1948
- this.syncAllParametersToWorker();
1949
- for (const listener of this.parameterDefinedListeners) {
1950
- try {
1951
- listener(Array.from(this.parameterGroups.values()));
1952
- } catch (error) {
1953
- console.error("Error in parameter defined listener:", error);
1954
- }
1955
- }
1956
- } catch (error) {
1957
- console.error("Error handling parameters defined:", error);
1958
- this.handleParameterError({
1959
- message: `Failed to process parameter definitions: ${error.message}`,
1960
- code: "PARAMETER_PROCESSING_ERROR"
1961
- });
1962
- }
1963
- }
1964
- /**
1965
- * Handle parameter validation errors
1966
- */
1967
- handleParameterError(error) {
1968
- console.error("Parameter error:", error);
1969
- for (const listener of this.parameterErrorListeners) {
1970
- try {
1971
- listener(error);
1972
- } catch (listenerError) {
1973
- console.error("Error in parameter error listener:", listenerError);
1974
- }
1975
- }
1976
- }
1977
- /**
1978
- * Get parameter definition by name
1979
- */
1980
- getParameterDefinition(name) {
1981
- for (const group of this.parameterGroups.values()) {
1982
- if (group.parameters[name]) {
1983
- return group.parameters[name];
1984
- }
1985
- }
1986
- return void 0;
1987
- }
1988
- /**
1989
- * Set a single parameter value
1990
- * Handles all parameter types including images intelligently
1991
- */
1992
- async setParameter(name, value) {
1993
- this.validateReady();
1994
- if (!this.parametersInitialized) {
1995
- throw new VijiCoreError("Parameters not yet initialized", "PARAMETERS_NOT_INITIALIZED");
1996
- }
1997
- if (!this.parameterValues.has(name)) {
1998
- throw new VijiCoreError(`Unknown parameter: ${name}`, "UNKNOWN_PARAMETER");
1999
- }
2000
- const paramDef = this.getParameterDefinition(name);
2001
- if (paramDef?.type === "image") {
2002
- await this.handleImageParameter(name, value);
2003
- return;
2004
- }
2005
- const oldValue = this.parameterValues.get(name);
2006
- this.parameterValues.set(name, value);
2007
- if (this.workerManager) {
2008
- this.workerManager.postMessage("parameter-update", {
2009
- name,
2010
- value
2011
- });
2012
- }
2013
- if (oldValue !== value) {
2014
- this.notifyParameterListeners(name, value);
2015
- }
2016
- }
2017
- /**
2018
- * Internal method to handle image parameter loading and transfer
2019
- * Loads the image on the host side and creates TWO ImageBitmaps:
2020
- * - One for the host (listeners, previews)
2021
- * - One to transfer to the worker
2022
- */
2023
- async handleImageParameter(name, value) {
2024
- try {
2025
- let hostImageBitmap = null;
2026
- let workerImageBitmap = null;
2027
- if (value === null || value === void 0) {
2028
- hostImageBitmap = null;
2029
- workerImageBitmap = null;
2030
- } else if (typeof value === "string") {
2031
- const response = await fetch(value);
2032
- const blob = await response.blob();
2033
- [hostImageBitmap, workerImageBitmap] = await Promise.all([
2034
- createImageBitmap(blob),
2035
- createImageBitmap(blob)
2036
- ]);
2037
- } else if (value instanceof File || value instanceof Blob) {
2038
- [hostImageBitmap, workerImageBitmap] = await Promise.all([
2039
- createImageBitmap(value),
2040
- createImageBitmap(value)
2041
- ]);
2042
- } else if (value instanceof ImageBitmap) {
2043
- const canvas = document.createElement("canvas");
2044
- canvas.width = value.width;
2045
- canvas.height = value.height;
2046
- const ctx = canvas.getContext("2d");
2047
- ctx.drawImage(value, 0, 0);
2048
- hostImageBitmap = value;
2049
- workerImageBitmap = await createImageBitmap(canvas);
2050
- } else {
2051
- throw new Error(`Invalid value type for image parameter. Expected File, Blob, string (URL), or null.`);
2052
- }
2053
- const oldValue = this.parameterValues.get(name);
2054
- this.parameterValues.set(name, hostImageBitmap);
2055
- if (this.workerManager) {
2056
- const messageData = {
2057
- name,
2058
- value: workerImageBitmap
2059
- };
2060
- const transferList = workerImageBitmap ? [workerImageBitmap] : [];
2061
- this.workerManager.postMessage("parameter-update", messageData, transferList);
2062
- }
2063
- if (oldValue !== hostImageBitmap) {
2064
- this.notifyParameterListeners(name, hostImageBitmap);
2065
- }
2066
- this.debugLog(`Image parameter '${name}' ${hostImageBitmap ? "loaded" : "cleared"} (${this.instanceId})`);
2067
- } catch (error) {
2068
- throw new VijiCoreError(
2069
- `Failed to load image for parameter '${name}': ${error}`,
2070
- "IMAGE_LOAD_ERROR",
2071
- { error, name }
2072
- );
2073
- }
2074
- }
2075
- /**
2076
- * Set multiple parameter values efficiently
2077
- */
2078
- async setParameters(values) {
2079
- this.validateReady();
2080
- if (!this.parametersInitialized) {
2081
- throw new VijiCoreError("Parameters not yet initialized", "PARAMETERS_NOT_INITIALIZED");
2082
- }
2083
- const updates = [];
2084
- const changedParams = [];
2085
- for (const [name, value] of Object.entries(values)) {
2086
- if (!this.parameterValues.has(name)) {
2087
- console.warn(`Unknown parameter: ${name}`);
2088
- continue;
2089
- }
2090
- const oldValue = this.parameterValues.get(name);
2091
- this.parameterValues.set(name, value);
2092
- updates.push({
2093
- name,
2094
- value
2095
- });
2096
- if (oldValue !== value) {
2097
- changedParams.push({ name, value });
2098
- }
2099
- }
2100
- if (updates.length > 0 && this.workerManager) {
2101
- this.workerManager.postMessage("parameter-batch-update", {
2102
- updates
2103
- });
2104
- }
2105
- for (const { name, value } of changedParams) {
2106
- this.notifyParameterListeners(name, value);
2107
- }
2108
- }
2109
- /**
2110
- * Get current parameter value
2111
- */
2112
- getParameter(name) {
2113
- return this.parameterValues.get(name);
2114
- }
2115
- /**
2116
- * Get all current parameter values
2117
- */
2118
- getParameterValues() {
2119
- const values = {};
2120
- for (const [name, value] of this.parameterValues) {
2121
- values[name] = value;
2122
- }
2123
- return values;
2124
- }
2125
- /**
2126
- * Get parameter groups (for UI generation)
2127
- */
2128
- getParameterGroups() {
2129
- return Array.from(this.parameterGroups.values());
2130
- }
2131
- /**
2132
- * Get current core capabilities (what's currently active)
2133
- */
2134
- getCapabilities() {
2135
- return {
2136
- hasAudio: this.currentAudioStream !== null,
2137
- hasVideo: this.currentVideoStream !== null,
2138
- hasInteraction: this.currentInteractionEnabled,
2139
- hasGeneral: true
2140
- // General parameters are always available
2141
- };
2142
- }
2143
- /**
2144
- * Get parameter groups filtered by active capabilities
2145
- */
2146
- getVisibleParameterGroups() {
2147
- const capabilities = this.getCapabilities();
2148
- const allGroups = this.getParameterGroups();
2149
- return allGroups.filter((group) => {
2150
- switch (group.category) {
2151
- case "audio":
2152
- return capabilities.hasAudio;
2153
- case "video":
2154
- return capabilities.hasVideo;
2155
- case "interaction":
2156
- return capabilities.hasInteraction;
2157
- case "general":
2158
- default:
2159
- return capabilities.hasGeneral;
2160
- }
2161
- }).map((group) => ({
2162
- ...group,
2163
- // Also filter individual parameters within groups
2164
- parameters: Object.fromEntries(
2165
- Object.entries(group.parameters).filter(([_, paramDef]) => {
2166
- switch (paramDef.category) {
2167
- case "audio":
2168
- return capabilities.hasAudio;
2169
- case "video":
2170
- return capabilities.hasVideo;
2171
- case "interaction":
2172
- return capabilities.hasInteraction;
2173
- case "general":
2174
- default:
2175
- return capabilities.hasGeneral;
2176
- }
2177
- })
2178
- )
2179
- }));
2180
- }
2181
- /**
2182
- * Get all parameter groups without capability filtering.
2183
- * Returns a deep-cloned structure to prevent external mutation.
2184
- */
2185
- getAllParameterGroups() {
2186
- const allGroups = this.getParameterGroups();
2187
- return allGroups.map((group) => ({
2188
- ...group,
2189
- parameters: Object.fromEntries(
2190
- Object.entries(group.parameters).map(([name, def]) => {
2191
- const clonedDef = { ...def };
2192
- if (def.config) {
2193
- clonedDef.config = { ...def.config };
2194
- }
2195
- return [name, clonedDef];
2196
- })
2197
- )
2198
- }));
2199
- }
2200
- /**
2201
- * Check if a specific parameter category is currently active
2202
- */
2203
- isCategoryActive(category) {
2204
- const capabilities = this.getCapabilities();
2205
- switch (category) {
2206
- case "audio":
2207
- return capabilities.hasAudio;
2208
- case "video":
2209
- return capabilities.hasVideo;
2210
- case "interaction":
2211
- return capabilities.hasInteraction;
2212
- case "general":
2213
- default:
2214
- return capabilities.hasGeneral;
2215
- }
2216
- }
2217
- /**
2218
- * Check if parameters have been initialized
2219
- */
2220
- get parametersReady() {
2221
- return this.parametersInitialized;
2222
- }
2223
- /**
2224
- * Send all current parameter values to worker (used for initial sync)
2225
- */
2226
- syncAllParametersToWorker() {
2227
- if (!this.workerManager || this.parameterValues.size === 0) {
2228
- return;
2229
- }
2230
- const updates = [];
2231
- for (const [name, value] of this.parameterValues) {
2232
- updates.push({
2233
- name,
2234
- value
2235
- });
2236
- }
2237
- this.workerManager.postMessage("parameter-batch-update", {
2238
- updates
2239
- });
2240
- this.debugLog(`Synced ${updates.length} parameter values to worker`);
2241
- }
2242
- /**
2243
- * Add listener for when parameters are defined
2244
- */
2245
- onParametersDefined(listener) {
2246
- this.parameterDefinedListeners.add(listener);
2247
- }
2248
- /**
2249
- * Remove parameter defined listener
2250
- */
2251
- offParametersDefined(listener) {
2252
- this.parameterDefinedListeners.delete(listener);
2253
- }
2254
- /**
2255
- * Add listener for parameter value changes
2256
- */
2257
- onParameterChange(parameterName, listener) {
2258
- if (!this.parameterListeners.has(parameterName)) {
2259
- this.parameterListeners.set(parameterName, /* @__PURE__ */ new Set());
2260
- }
2261
- this.parameterListeners.get(parameterName).add(listener);
2262
- }
2263
- /**
2264
- * Remove parameter change listener
2265
- */
2266
- offParameterChange(parameterName, listener) {
2267
- const listeners = this.parameterListeners.get(parameterName);
2268
- if (listeners) {
2269
- listeners.delete(listener);
2270
- if (listeners.size === 0) {
2271
- this.parameterListeners.delete(parameterName);
2272
- }
2273
- }
2274
- }
2275
- /**
2276
- * Add listener for parameter errors
2277
- */
2278
- onParameterError(listener) {
2279
- this.parameterErrorListeners.add(listener);
2280
- }
2281
- /**
2282
- * Add listener for when core capabilities change (audio/video/interaction state)
2283
- */
2284
- onCapabilitiesChange(listener) {
2285
- this.capabilitiesChangeListeners.add(listener);
2286
- }
2287
- /**
2288
- * Remove capabilities change listener
2289
- */
2290
- removeCapabilitiesListener(listener) {
2291
- this.capabilitiesChangeListeners.delete(listener);
2292
- }
2293
- /**
2294
- * Notify capability change listeners
2295
- */
2296
- notifyCapabilitiesChange() {
2297
- const capabilities = this.getCapabilities();
2298
- for (const listener of this.capabilitiesChangeListeners) {
2299
- try {
2300
- listener(capabilities);
2301
- } catch (error) {
2302
- console.error("Error in capabilities change listener:", error);
2303
- }
2304
- }
2305
- }
2306
- /**
2307
- * Remove parameter error listener
2308
- */
2309
- offParameterError(listener) {
2310
- this.parameterErrorListeners.delete(listener);
2311
- }
2312
- /**
2313
- * Notify parameter change listeners
2314
- */
2315
- notifyParameterListeners(name, value) {
2316
- const listeners = this.parameterListeners.get(name);
2317
- if (listeners) {
2318
- for (const listener of listeners) {
2319
- try {
2320
- listener(value);
2321
- } catch (error) {
2322
- console.error(`Error in parameter listener for '${name}':`, error);
2323
- }
2324
- }
2325
- }
2326
- }
2327
- /**
2328
- * Sets the scene frame rate mode
2329
- * @param mode - 'full' for every animation frame, 'half' for every second frame
2330
- */
2331
- async setFrameRate(mode) {
2332
- this.validateReady();
2333
- this.config.frameRateMode = mode;
2334
- if (this.workerManager) {
2335
- this.workerManager.postMessage("frame-rate-update", { mode });
2336
- }
2337
- this.updateFrameRateStats();
2338
- this.debugLog(`Scene frame rate set to ${mode} (${this.instanceId})`);
2339
- }
2340
- /**
2341
- * Sets the CV processing frame rate mode (relative to scene frame rate)
2342
- * @param mode - CV processing rate: 'full', 'half', 'quarter', or 'eighth' of scene rate
2343
- */
2344
- async setCVFrameRate(mode) {
2345
- this.validateReady();
2346
- if (this.workerManager) {
2347
- this.workerManager.postMessage("cv-frame-rate-update", { mode });
2348
- }
2349
- this.debugLog(`CV frame rate set to ${mode} of scene rate (${this.instanceId})`);
2350
- }
2351
- /**
2352
- * Updates the canvas resolution by sending effective dimensions to the worker
2353
- */
2354
- updateResolution() {
2355
- this.validateReady();
2356
- if (!this.iframeManager || !this.workerManager) {
2357
- throw new VijiCoreError("Managers not available", "MANAGERS_NOT_READY");
2358
- }
2359
- const effectiveResolution = this.iframeManager.getEffectiveResolution();
2360
- this.workerManager.postMessage("resolution-update", {
2361
- effectiveWidth: effectiveResolution.width,
2362
- effectiveHeight: effectiveResolution.height
2363
- });
2364
- this.stats.resolution = effectiveResolution;
2365
- this.stats.scale = this.iframeManager.getScale();
2366
- this.debugLog(`Resolution updated successfully (${this.instanceId})`, effectiveResolution);
2367
- }
2368
- /**
2369
- * Sets the audio stream for analysis
2370
- */
2371
- async setAudioStream(audioStream) {
2372
- if (this.isInitialized && !this.isInitializing) {
2373
- this.validateReady();
2374
- }
2375
- const previouslyHadAudio = this.currentAudioStream !== null;
2376
- this.currentAudioStream = audioStream;
2377
- const nowHasAudio = this.currentAudioStream !== null;
2378
- if (this.audioSystem) {
2379
- this.audioSystem.handleAudioStreamUpdate({
2380
- audioStream,
2381
- ...this.config.analysisConfig && { analysisConfig: this.config.analysisConfig },
2382
- timestamp: performance.now()
2383
- });
2384
- }
2385
- if (previouslyHadAudio !== nowHasAudio) {
2386
- this.notifyCapabilitiesChange();
2387
- }
2388
- this.debugLog(`Audio stream ${audioStream ? "connected" : "disconnected"} (${this.instanceId})`);
2389
- }
2390
- /**
2391
- * Sets the video stream for processing
2392
- */
2393
- async setVideoStream(videoStream) {
2394
- if (this.isInitialized && !this.isInitializing) {
2395
- this.validateReady();
2396
- }
2397
- const previouslyHadVideo = this.currentVideoStream !== null;
2398
- this.currentVideoStream = videoStream;
2399
- const nowHasVideo = this.currentVideoStream !== null;
2400
- if (this.videoCoordinator) {
2401
- this.videoCoordinator.handleVideoStreamUpdate({
2402
- videoStream,
2403
- targetFrameRate: 30,
2404
- // Default target FPS
2405
- timestamp: performance.now()
2406
- });
2407
- }
2408
- if (previouslyHadVideo !== nowHasVideo) {
2409
- this.notifyCapabilitiesChange();
2410
- }
2411
- this.debugLog(`Video stream ${videoStream ? "connected" : "disconnected"} (${this.instanceId})`);
2412
- }
2413
- /**
2414
- * Gets the current audio stream
2415
- */
2416
- getAudioStream() {
2417
- return this.currentAudioStream;
2418
- }
2419
- /**
2420
- * Gets the current video stream
2421
- */
2422
- getVideoStream() {
2423
- return this.currentVideoStream;
2424
- }
2425
- /**
2426
- * Enables or disables user interactions (mouse, keyboard, touch) at runtime
2427
- */
2428
- async setInteractionEnabled(enabled) {
2429
- if (this.isInitialized && !this.isInitializing) {
2430
- this.validateReady();
2431
- }
2432
- const previouslyHadInteraction = this.currentInteractionEnabled;
2433
- this.currentInteractionEnabled = enabled;
2434
- if (this.iframeManager) {
2435
- this.iframeManager.setInteractionEnabled(enabled);
2436
- }
2437
- if (this.workerManager) {
2438
- this.workerManager.postMessage("interaction-enabled", { enabled });
2439
- }
2440
- if (previouslyHadInteraction !== enabled) {
2441
- this.notifyCapabilitiesChange();
2442
- }
2443
- this.debugLog(`Interaction ${enabled ? "enabled" : "disabled"} (${this.instanceId})`);
2444
- }
2445
- /**
2446
- * Gets the current interaction enabled state
2447
- */
2448
- getInteractionEnabled() {
2449
- return this.currentInteractionEnabled;
2450
- }
2451
- /**
2452
- * Updates audio analysis configuration
2453
- */
2454
- async setAudioAnalysisConfig(config) {
2455
- this.validateReady();
2456
- this.config.analysisConfig = { ...this.config.analysisConfig, ...config };
2457
- if (this.audioSystem) {
2458
- this.audioSystem.handleAudioStreamUpdate({
2459
- audioStream: this.currentAudioStream,
2460
- ...this.config.analysisConfig && { analysisConfig: this.config.analysisConfig },
2461
- timestamp: performance.now()
2462
- });
2463
- }
2464
- this.debugLog(`Audio analysis config updated (${this.instanceId})`, config);
2465
- }
2466
- /**
2467
- * Updates the canvas resolution by scale
2468
- */
2469
- setResolution(scale) {
2470
- this.validateReady();
2471
- if (!this.iframeManager || !this.workerManager) {
2472
- throw new VijiCoreError("Managers not available", "MANAGERS_NOT_READY");
2473
- }
2474
- this.debugLog(`Updating resolution scale to:`, scale, `(${this.instanceId})`);
2475
- const effectiveResolution = this.iframeManager.updateScale(scale);
2476
- this.workerManager.postMessage("resolution-update", {
2477
- effectiveWidth: effectiveResolution.width,
2478
- effectiveHeight: effectiveResolution.height
2479
- });
2480
- this.stats.resolution = effectiveResolution;
2481
- this.stats.scale = scale;
2482
- this.debugLog(`Resolution updated successfully (${this.instanceId})`, effectiveResolution);
2483
- }
2484
- /**
2485
- * Detects the screen refresh rate
2486
- */
2487
- async detectScreenRefreshRate() {
2488
- return new Promise((resolve) => {
2489
- let frameCount = 0;
2490
- let startTime = performance.now();
2491
- const measureFrames = () => {
2492
- frameCount++;
2493
- if (frameCount === 60) {
2494
- const elapsed = performance.now() - startTime;
2495
- this.screenRefreshRate = Math.round(6e4 / elapsed);
2496
- this.debugLog("Detected screen refresh rate:", this.screenRefreshRate + "Hz");
2497
- resolve();
2498
- } else if (frameCount < 60) {
2499
- requestAnimationFrame(measureFrames);
2500
- }
2501
- };
2502
- requestAnimationFrame(measureFrames);
2503
- });
2504
- }
2505
- /**
2506
- * Updates frame rate statistics
2507
- */
2508
- updateFrameRateStats() {
2509
- this.stats.frameRate = {
2510
- mode: this.config.frameRateMode,
2511
- screenRefreshRate: this.screenRefreshRate,
2512
- effectiveRefreshRate: 0
2513
- // Will be updated by worker during execution
2514
- };
2515
- }
2516
- /**
2517
- * Gets current performance statistics
2518
- */
2519
- getStats() {
2520
- this.validateReady();
2521
- if (!this.iframeManager) {
2522
- throw new VijiCoreError("IFrame manager not available", "MANAGER_NOT_READY");
2523
- }
2524
- return this.stats;
2525
- }
2526
- /**
2527
- * Checks if the core is ready for use
2528
- */
2529
- get ready() {
2530
- return this.isInitialized && !this.isDestroyed && this.iframeManager?.ready === true && this.workerManager?.ready === true;
2531
- }
2532
- /**
2533
- * Gets the current configuration
2534
- */
2535
- get configuration() {
2536
- return { ...this.config };
2537
- }
2538
- /**
2539
- * Destroys the core instance and cleans up all resources
2540
- */
2541
- async destroy() {
2542
- if (this.isDestroyed) {
2543
- return;
2544
- }
2545
- this.isDestroyed = true;
2546
- this.parameterGroups.clear();
2547
- this.parameterValues.clear();
2548
- this.parameterListeners.clear();
2549
- this.parameterDefinedListeners.clear();
2550
- this.parameterErrorListeners.clear();
2551
- this.capabilitiesChangeListeners.clear();
2552
- if (this.audioSystem) {
2553
- this.audioSystem.resetAudioState();
2554
- this.audioSystem = null;
2555
- }
2556
- this.currentAudioStream = null;
2557
- if (this.videoCoordinator) {
2558
- this.videoCoordinator.resetVideoState();
2559
- this.videoCoordinator = null;
2560
- }
2561
- this.currentVideoStream = null;
2562
- await this.cleanup();
2563
- this.debugLog(`VijiCore destroyed (${this.instanceId})`);
2564
- }
2565
- /**
2566
- * Validates that the core is ready for operations
2567
- */
2568
- validateReady() {
2569
- if (this.isDestroyed) {
2570
- throw new VijiCoreError("Core instance has been destroyed", "INSTANCE_DESTROYED");
2571
- }
2572
- if (!this.ready) {
2573
- throw new VijiCoreError("Core is not ready", "CORE_NOT_READY");
2574
- }
2575
- }
2576
- /**
2577
- * Validates the provided configuration
2578
- */
2579
- validateConfig(config) {
2580
- if (!config.hostContainer) {
2581
- throw new VijiCoreError("hostContainer is required", "INVALID_CONFIG");
2582
- }
2583
- if (!config.sceneCode || typeof config.sceneCode !== "string") {
2584
- throw new VijiCoreError("sceneCode must be a non-empty string", "INVALID_CONFIG");
2585
- }
2586
- if (config.frameRateMode !== void 0 && config.frameRateMode !== "full" && config.frameRateMode !== "half") {
2587
- throw new VijiCoreError('frameRateMode must be either "full" or "half"', "INVALID_CONFIG");
2588
- }
2589
- }
2590
- /**
2591
- * Cleans up all resources
2592
- */
2593
- async cleanup() {
2594
- try {
2595
- if (this.workerManager) {
2596
- this.workerManager.destroy();
2597
- this.workerManager = null;
2598
- }
2599
- if (this.iframeManager) {
2600
- this.iframeManager.destroy();
2601
- this.iframeManager = null;
2602
- }
2603
- this.isInitialized = false;
2604
- this.isInitializing = false;
2605
- } catch (error) {
2606
- console.warn("Error during cleanup:", error);
2607
- }
2608
- }
2609
- }
2610
- const VERSION = "0.2.18";
1
+ import { A, V, a, b } from "./index-BdLMCFEN.js";
2611
2
  export {
2612
- VERSION,
2613
- VijiCore,
2614
- VijiCoreError
3
+ A as AudioSystem,
4
+ V as VERSION,
5
+ a as VijiCore,
6
+ b as VijiCoreError
2615
7
  };
2616
8
  //# sourceMappingURL=index.js.map