@sangwonl/pocato-core 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3 -0
- package/dist/index.js +236 -42
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,9 @@ interface LayerSource {
|
|
|
11
11
|
src: string;
|
|
12
12
|
type?: 'image' | 'video';
|
|
13
13
|
freeze?: number | 'random';
|
|
14
|
+
freezeSeed?: number;
|
|
15
|
+
loop?: boolean;
|
|
16
|
+
playbackRate?: number;
|
|
14
17
|
}
|
|
15
18
|
type EffectUniformValue = string | number | boolean | [number, number] | [number, number, number] | [number, number, number, number];
|
|
16
19
|
interface FaceFrameOptions {
|
package/dist/index.js
CHANGED
|
@@ -381,12 +381,38 @@ function getTransparentTexture() {
|
|
|
381
381
|
return transparentTexture;
|
|
382
382
|
}
|
|
383
383
|
var pendingVideos = /* @__PURE__ */ new Set();
|
|
384
|
+
function debugLog(message, data) {
|
|
385
|
+
try {
|
|
386
|
+
if (globalThis.localStorage?.getItem("pocatoDebug") === "1") {
|
|
387
|
+
console.warn(`[pocato] ${message}`, data ?? {});
|
|
388
|
+
}
|
|
389
|
+
} catch {
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function applyVideoPlaybackOptions(video, layer) {
|
|
393
|
+
const playbackRate = layer.playbackRate ?? 1;
|
|
394
|
+
video.defaultPlaybackRate = playbackRate;
|
|
395
|
+
video.playbackRate = playbackRate;
|
|
396
|
+
}
|
|
397
|
+
function getRandomValue(seed) {
|
|
398
|
+
if (seed == null || !Number.isFinite(seed)) return Math.random();
|
|
399
|
+
return Math.abs(seed) % 1;
|
|
400
|
+
}
|
|
401
|
+
function getFreezeTargetTime(freezeTime, duration, seed) {
|
|
402
|
+
if (freezeTime < 0) {
|
|
403
|
+
return getRandomValue(seed) * (Number.isFinite(duration) ? duration : 1);
|
|
404
|
+
}
|
|
405
|
+
return Number.isFinite(duration) ? Math.min(freezeTime, duration) : freezeTime;
|
|
406
|
+
}
|
|
407
|
+
function isAtTargetTime(video, targetTime) {
|
|
408
|
+
return Math.abs(video.currentTime - targetTime) < 0.05;
|
|
409
|
+
}
|
|
384
410
|
function loadLayerTexture(layer, onError) {
|
|
385
411
|
if (isVideoSource(layer)) {
|
|
386
412
|
if (layer.freeze != null) {
|
|
387
|
-
return loadFrozenVideoTexture(layer
|
|
413
|
+
return loadFrozenVideoTexture(layer, layer.freeze === "random" ? -1 : layer.freeze, onError);
|
|
388
414
|
}
|
|
389
|
-
return loadVideoTexture(layer
|
|
415
|
+
return loadVideoTexture(layer, onError);
|
|
390
416
|
}
|
|
391
417
|
return loadImageTexture(layer.src, onError);
|
|
392
418
|
}
|
|
@@ -414,12 +440,13 @@ function loadImageTexture(src, onError) {
|
|
|
414
440
|
}
|
|
415
441
|
return pending.then((texture) => ({ texture }));
|
|
416
442
|
}
|
|
417
|
-
function loadVideoTexture(
|
|
443
|
+
function loadVideoTexture(layer, onError) {
|
|
418
444
|
return new Promise((resolve) => {
|
|
419
445
|
const video = document.createElement("video");
|
|
420
|
-
video.
|
|
446
|
+
video.crossOrigin = "anonymous";
|
|
421
447
|
video.muted = true;
|
|
422
|
-
video.loop = true;
|
|
448
|
+
video.loop = layer.loop ?? true;
|
|
449
|
+
applyVideoPlaybackOptions(video, layer);
|
|
423
450
|
video.playsInline = true;
|
|
424
451
|
video.preload = "auto";
|
|
425
452
|
video.style.position = "fixed";
|
|
@@ -430,30 +457,47 @@ function loadVideoTexture(src, onError) {
|
|
|
430
457
|
video.style.zIndex = "-9999";
|
|
431
458
|
document.body.appendChild(video);
|
|
432
459
|
pendingVideos.add(video);
|
|
460
|
+
let settled = false;
|
|
433
461
|
video.addEventListener("error", () => {
|
|
462
|
+
if (settled) return;
|
|
463
|
+
settled = true;
|
|
434
464
|
pendingVideos.delete(video);
|
|
435
465
|
const msg = video.error?.message ?? "Unknown video error";
|
|
436
|
-
onError?.(new Error(`Failed to load video texture: ${src} (${msg})`));
|
|
466
|
+
onError?.(new Error(`Failed to load video texture: ${layer.src} (${msg})`));
|
|
437
467
|
video.remove();
|
|
438
468
|
resolve({ texture: getTransparentTexture() });
|
|
439
469
|
}, { once: true });
|
|
440
|
-
video.
|
|
470
|
+
video.addEventListener("loadeddata", () => {
|
|
471
|
+
if (settled) return;
|
|
472
|
+
settled = true;
|
|
473
|
+
video.pause();
|
|
441
474
|
pendingVideos.delete(video);
|
|
442
475
|
const texture = new THREE2.VideoTexture(video);
|
|
443
|
-
resolve({
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
476
|
+
resolve({
|
|
477
|
+
texture,
|
|
478
|
+
videoEl: video,
|
|
479
|
+
play: () => {
|
|
480
|
+
video.play().catch(() => {
|
|
481
|
+
onError?.(new Error(`Video autoplay blocked: ${layer.src}`));
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
}, { once: true });
|
|
486
|
+
video.src = layer.src;
|
|
487
|
+
video.load();
|
|
450
488
|
});
|
|
451
489
|
}
|
|
452
|
-
function loadFrozenVideoTexture(
|
|
490
|
+
function loadFrozenVideoTexture(layer, freezeTime, onError) {
|
|
453
491
|
return new Promise((resolve) => {
|
|
492
|
+
debugLog("freeze load start", {
|
|
493
|
+
src: layer.src,
|
|
494
|
+
freezeTime,
|
|
495
|
+
freezeSeed: layer.freezeSeed
|
|
496
|
+
});
|
|
454
497
|
const video = document.createElement("video");
|
|
455
|
-
video.
|
|
498
|
+
video.crossOrigin = "anonymous";
|
|
456
499
|
video.muted = true;
|
|
500
|
+
applyVideoPlaybackOptions(video, layer);
|
|
457
501
|
video.playsInline = true;
|
|
458
502
|
video.preload = "auto";
|
|
459
503
|
video.style.position = "fixed";
|
|
@@ -464,31 +508,137 @@ function loadFrozenVideoTexture(src, freezeTime, onError) {
|
|
|
464
508
|
video.style.zIndex = "-9999";
|
|
465
509
|
document.body.appendChild(video);
|
|
466
510
|
pendingVideos.add(video);
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
511
|
+
let settled = false;
|
|
512
|
+
let targetTime = 0;
|
|
513
|
+
let retrySeekRegistered = false;
|
|
514
|
+
const captureFrame = () => {
|
|
515
|
+
if (settled) return;
|
|
516
|
+
settled = true;
|
|
517
|
+
video.pause();
|
|
518
|
+
debugLog("freeze capture frame", {
|
|
519
|
+
src: layer.src,
|
|
520
|
+
currentTime: video.currentTime,
|
|
521
|
+
duration: video.duration,
|
|
522
|
+
readyState: video.readyState,
|
|
523
|
+
videoWidth: video.videoWidth,
|
|
524
|
+
videoHeight: video.videoHeight
|
|
525
|
+
});
|
|
479
526
|
const canvas = document.createElement("canvas");
|
|
480
|
-
canvas.width = video.videoWidth;
|
|
481
|
-
canvas.height = video.videoHeight;
|
|
527
|
+
canvas.width = video.videoWidth || 1;
|
|
528
|
+
canvas.height = video.videoHeight || 1;
|
|
482
529
|
const ctx = canvas.getContext("2d");
|
|
483
|
-
ctx
|
|
530
|
+
if (!ctx) {
|
|
531
|
+
pendingVideos.delete(video);
|
|
532
|
+
video.src = "";
|
|
533
|
+
video.remove();
|
|
534
|
+
resolve({ texture: getTransparentTexture() });
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
ctx.drawImage(video, 0, 0);
|
|
539
|
+
} catch (error) {
|
|
540
|
+
debugLog("freeze drawImage failed", {
|
|
541
|
+
src: layer.src,
|
|
542
|
+
error: error instanceof Error ? error.message : String(error)
|
|
543
|
+
});
|
|
544
|
+
pendingVideos.delete(video);
|
|
545
|
+
video.src = "";
|
|
546
|
+
video.remove();
|
|
547
|
+
onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
548
|
+
resolve({ texture: getTransparentTexture() });
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
484
551
|
const texture = new THREE2.CanvasTexture(canvas);
|
|
485
552
|
texture.needsUpdate = true;
|
|
553
|
+
debugLog("freeze canvas texture ready", {
|
|
554
|
+
src: layer.src,
|
|
555
|
+
width: canvas.width,
|
|
556
|
+
height: canvas.height
|
|
557
|
+
});
|
|
486
558
|
pendingVideos.delete(video);
|
|
487
|
-
video.pause();
|
|
488
559
|
video.src = "";
|
|
489
560
|
video.remove();
|
|
490
561
|
resolve({ texture });
|
|
562
|
+
};
|
|
563
|
+
const captureReadyFrame = () => {
|
|
564
|
+
debugLog("freeze wait decoded frame", {
|
|
565
|
+
src: layer.src,
|
|
566
|
+
currentTime: video.currentTime,
|
|
567
|
+
readyState: video.readyState
|
|
568
|
+
});
|
|
569
|
+
if ("requestVideoFrameCallback" in video) {
|
|
570
|
+
video.requestVideoFrameCallback(() => captureFrame());
|
|
571
|
+
}
|
|
572
|
+
const playPromise = video.play();
|
|
573
|
+
playPromise.catch(() => {
|
|
574
|
+
debugLog("freeze play rejected, fallback capture", { src: layer.src });
|
|
575
|
+
window.setTimeout(captureFrame, 0);
|
|
576
|
+
});
|
|
577
|
+
if (!("requestVideoFrameCallback" in video)) {
|
|
578
|
+
window.setTimeout(captureFrame, 0);
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
const retrySeekWhenReady = () => {
|
|
582
|
+
if (settled) return;
|
|
583
|
+
if (retrySeekRegistered) return;
|
|
584
|
+
retrySeekRegistered = true;
|
|
585
|
+
const retry = () => {
|
|
586
|
+
retrySeekRegistered = false;
|
|
587
|
+
if (settled) return;
|
|
588
|
+
debugLog("freeze retry seek", {
|
|
589
|
+
src: layer.src,
|
|
590
|
+
targetTime,
|
|
591
|
+
currentTime: video.currentTime,
|
|
592
|
+
duration: video.duration,
|
|
593
|
+
readyState: video.readyState
|
|
594
|
+
});
|
|
595
|
+
video.currentTime = targetTime;
|
|
596
|
+
};
|
|
597
|
+
video.addEventListener("durationchange", retry, { once: true });
|
|
598
|
+
video.addEventListener("canplay", retry, { once: true });
|
|
599
|
+
video.addEventListener("loadeddata", retry, { once: true });
|
|
600
|
+
};
|
|
601
|
+
video.addEventListener("error", () => {
|
|
602
|
+
pendingVideos.delete(video);
|
|
603
|
+
const msg = video.error?.message ?? "Unknown video error";
|
|
604
|
+
onError?.(new Error(`Failed to load video texture: ${layer.src} (${msg})`));
|
|
605
|
+
video.remove();
|
|
606
|
+
resolve({ texture: getTransparentTexture() });
|
|
491
607
|
}, { once: true });
|
|
608
|
+
video.addEventListener("loadedmetadata", () => {
|
|
609
|
+
const time = getFreezeTargetTime(freezeTime, video.duration, layer.freezeSeed);
|
|
610
|
+
targetTime = time;
|
|
611
|
+
debugLog("freeze metadata loaded", {
|
|
612
|
+
src: layer.src,
|
|
613
|
+
targetTime: time,
|
|
614
|
+
duration: video.duration,
|
|
615
|
+
readyState: video.readyState
|
|
616
|
+
});
|
|
617
|
+
if (isAtTargetTime(video, time)) {
|
|
618
|
+
if (video.readyState >= 2) {
|
|
619
|
+
captureReadyFrame();
|
|
620
|
+
} else {
|
|
621
|
+
video.addEventListener("loadeddata", () => captureReadyFrame(), { once: true });
|
|
622
|
+
}
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
video.currentTime = time;
|
|
626
|
+
}, { once: true });
|
|
627
|
+
video.addEventListener("seeked", () => {
|
|
628
|
+
debugLog("freeze seeked", {
|
|
629
|
+
src: layer.src,
|
|
630
|
+
targetTime,
|
|
631
|
+
currentTime: video.currentTime,
|
|
632
|
+
duration: video.duration,
|
|
633
|
+
readyState: video.readyState
|
|
634
|
+
});
|
|
635
|
+
if (!isAtTargetTime(video, targetTime)) {
|
|
636
|
+
retrySeekWhenReady();
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
captureReadyFrame();
|
|
640
|
+
});
|
|
641
|
+
video.src = layer.src;
|
|
492
642
|
video.load();
|
|
493
643
|
});
|
|
494
644
|
}
|
|
@@ -1823,6 +1973,7 @@ var MAX_LAYERS = 8;
|
|
|
1823
1973
|
var FaceRenderer = class {
|
|
1824
1974
|
constructor(shader, width, height, effectUniforms) {
|
|
1825
1975
|
this.loadedLayers = [];
|
|
1976
|
+
this.layerLoadVersion = 0;
|
|
1826
1977
|
bootstrapShaders();
|
|
1827
1978
|
this.scene = new THREE3.Scene();
|
|
1828
1979
|
this.camera = new THREE3.Camera();
|
|
@@ -1872,6 +2023,7 @@ var FaceRenderer = class {
|
|
|
1872
2023
|
}
|
|
1873
2024
|
}
|
|
1874
2025
|
async loadLayers(layers, onError) {
|
|
2026
|
+
const version = ++this.layerLoadVersion;
|
|
1875
2027
|
this.disposeLayers();
|
|
1876
2028
|
const count = Math.min(layers.length, MAX_LAYERS);
|
|
1877
2029
|
this.material.uniforms.uLayerCount.value = count;
|
|
@@ -1879,6 +2031,12 @@ var FaceRenderer = class {
|
|
|
1879
2031
|
(layer) => loadLayerTexture(layer, onError)
|
|
1880
2032
|
);
|
|
1881
2033
|
const results = await Promise.all(promises);
|
|
2034
|
+
if (version !== this.layerLoadVersion) {
|
|
2035
|
+
for (const loaded of results) {
|
|
2036
|
+
disposeLoadedLayer(loaded);
|
|
2037
|
+
}
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
1882
2040
|
this.loadedLayers = results;
|
|
1883
2041
|
for (let i = 0; i < results.length; i++) {
|
|
1884
2042
|
this.material.uniforms[`uLayer${i}`].value = results[i].texture;
|
|
@@ -1898,6 +2056,11 @@ var FaceRenderer = class {
|
|
|
1898
2056
|
updateResolution(width, height) {
|
|
1899
2057
|
this.material.uniforms.uResolution.value.set(width, height);
|
|
1900
2058
|
}
|
|
2059
|
+
playLoadedVideos() {
|
|
2060
|
+
for (const loaded of this.loadedLayers) {
|
|
2061
|
+
loaded.play?.();
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
1901
2064
|
updateShader(fragmentShader) {
|
|
1902
2065
|
this.material.fragmentShader = resolveIncludes(fragmentShader);
|
|
1903
2066
|
this.material.needsUpdate = true;
|
|
@@ -1914,6 +2077,7 @@ var FaceRenderer = class {
|
|
|
1914
2077
|
this.material.uniforms.uLayerCount.value = 0;
|
|
1915
2078
|
}
|
|
1916
2079
|
destroy() {
|
|
2080
|
+
this.layerLoadVersion++;
|
|
1917
2081
|
this.disposeLayers();
|
|
1918
2082
|
this.mesh.geometry.dispose();
|
|
1919
2083
|
this.material.dispose();
|
|
@@ -1933,11 +2097,12 @@ function toThreeUniformValue(value) {
|
|
|
1933
2097
|
|
|
1934
2098
|
// src/renderer/index.ts
|
|
1935
2099
|
var Renderer = class {
|
|
1936
|
-
constructor(container, options, onError, onReady) {
|
|
2100
|
+
constructor(container, options, onError, onReady, onFirstFrame) {
|
|
1937
2101
|
this.container = container;
|
|
1938
2102
|
this.options = options;
|
|
1939
2103
|
this.onError = onError;
|
|
1940
2104
|
this.onReady = onReady;
|
|
2105
|
+
this.onFirstFrame = onFirstFrame;
|
|
1941
2106
|
this.backFace = null;
|
|
1942
2107
|
this.frontRenderer = null;
|
|
1943
2108
|
this.backRenderer = null;
|
|
@@ -1948,6 +2113,9 @@ var Renderer = class {
|
|
|
1948
2113
|
this.intersectionObserver = null;
|
|
1949
2114
|
this.visible = false;
|
|
1950
2115
|
this.destroyed = false;
|
|
2116
|
+
this.layersReady = false;
|
|
2117
|
+
this.firstFrameEmitted = false;
|
|
2118
|
+
this.firstFrameScheduled = false;
|
|
1951
2119
|
if (!options.front?.layers?.length) {
|
|
1952
2120
|
console.warn("[pocato] front.layers must have at least 1 element");
|
|
1953
2121
|
}
|
|
@@ -2018,10 +2186,10 @@ var Renderer = class {
|
|
|
2018
2186
|
this.loadAllLayers();
|
|
2019
2187
|
this.setupResizeObserver();
|
|
2020
2188
|
this.setupIntersectionObserver();
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2189
|
+
}
|
|
2190
|
+
disposeWebGLRenderer(renderer) {
|
|
2191
|
+
renderer.forceContextLoss();
|
|
2192
|
+
renderer.dispose();
|
|
2025
2193
|
}
|
|
2026
2194
|
createFrontRenderer() {
|
|
2027
2195
|
if (this.frontRenderer) return;
|
|
@@ -2052,7 +2220,7 @@ var Renderer = class {
|
|
|
2052
2220
|
disposeFrontRenderer() {
|
|
2053
2221
|
if (!this.frontRenderer) return;
|
|
2054
2222
|
this.frontRenderer.render(this.frontFace.scene, this.frontFace.camera);
|
|
2055
|
-
this.frontRenderer
|
|
2223
|
+
this.disposeWebGLRenderer(this.frontRenderer);
|
|
2056
2224
|
this.frontRenderer = null;
|
|
2057
2225
|
}
|
|
2058
2226
|
disposeBackRenderer() {
|
|
@@ -2060,7 +2228,7 @@ var Renderer = class {
|
|
|
2060
2228
|
if (this.backFace) {
|
|
2061
2229
|
this.backRenderer.render(this.backFace.scene, this.backFace.camera);
|
|
2062
2230
|
}
|
|
2063
|
-
this.backRenderer
|
|
2231
|
+
this.disposeWebGLRenderer(this.backRenderer);
|
|
2064
2232
|
this.backRenderer = null;
|
|
2065
2233
|
}
|
|
2066
2234
|
activate() {
|
|
@@ -2103,8 +2271,26 @@ var Renderer = class {
|
|
|
2103
2271
|
promises.push(this.backFace.loadLayers(this.options.back.layers, this.onError));
|
|
2104
2272
|
}
|
|
2105
2273
|
await Promise.all(promises);
|
|
2274
|
+
if (this.destroyed) return;
|
|
2275
|
+
this.layersReady = true;
|
|
2106
2276
|
this.onReady?.();
|
|
2107
2277
|
}
|
|
2278
|
+
scheduleFirstFrame() {
|
|
2279
|
+
if (!this.layersReady || this.firstFrameEmitted || this.firstFrameScheduled) return;
|
|
2280
|
+
this.firstFrameScheduled = true;
|
|
2281
|
+
requestAnimationFrame(() => {
|
|
2282
|
+
if (this.destroyed || this.firstFrameEmitted) return;
|
|
2283
|
+
this.firstFrameEmitted = true;
|
|
2284
|
+
this.cardEl.classList.remove("pocato-loading");
|
|
2285
|
+
this.cardEl.classList.add("pocato-ready");
|
|
2286
|
+
this.onFirstFrame?.();
|
|
2287
|
+
requestAnimationFrame(() => {
|
|
2288
|
+
if (this.destroyed) return;
|
|
2289
|
+
this.frontFace.playLoadedVideos();
|
|
2290
|
+
this.backFace?.playLoadedVideos();
|
|
2291
|
+
});
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2108
2294
|
injectStyles() {
|
|
2109
2295
|
const id = "pocato-styles";
|
|
2110
2296
|
if (document.getElementById(id)) return;
|
|
@@ -2274,6 +2460,7 @@ var Renderer = class {
|
|
|
2274
2460
|
this.backFace.material.uniforms.uTime.value += delta;
|
|
2275
2461
|
this.backRenderer.render(this.backFace.scene, this.backFace.camera);
|
|
2276
2462
|
}
|
|
2463
|
+
this.scheduleFirstFrame();
|
|
2277
2464
|
};
|
|
2278
2465
|
this.rafId = requestAnimationFrame(animate);
|
|
2279
2466
|
}
|
|
@@ -2367,10 +2554,16 @@ var Renderer = class {
|
|
|
2367
2554
|
this.intersectionObserver?.disconnect();
|
|
2368
2555
|
cleanupPendingVideos();
|
|
2369
2556
|
this.frontFace.destroy();
|
|
2370
|
-
this.frontRenderer
|
|
2557
|
+
if (this.frontRenderer) {
|
|
2558
|
+
this.disposeWebGLRenderer(this.frontRenderer);
|
|
2559
|
+
this.frontRenderer = null;
|
|
2560
|
+
}
|
|
2371
2561
|
if (this.backFace) {
|
|
2372
2562
|
this.backFace.destroy();
|
|
2373
|
-
this.backRenderer
|
|
2563
|
+
if (this.backRenderer) {
|
|
2564
|
+
this.disposeWebGLRenderer(this.backRenderer);
|
|
2565
|
+
this.backRenderer = null;
|
|
2566
|
+
}
|
|
2374
2567
|
}
|
|
2375
2568
|
if (this.cardEl.parentNode) {
|
|
2376
2569
|
this.cardEl.parentNode.removeChild(this.cardEl);
|
|
@@ -2956,7 +3149,8 @@ var PocaCard = class extends EventEmitter {
|
|
|
2956
3149
|
preventTouchScroll: options.preventTouchScroll
|
|
2957
3150
|
},
|
|
2958
3151
|
(error) => this.emit("error", error),
|
|
2959
|
-
() => this.emit("ready")
|
|
3152
|
+
() => this.emit("ready"),
|
|
3153
|
+
() => this.emit("firstFrame")
|
|
2960
3154
|
);
|
|
2961
3155
|
this.interaction = new InteractionHandler(
|
|
2962
3156
|
this.renderer.getRotatorEl(),
|