@sangwonl/pocato-core 0.4.3 → 0.4.4
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 +195 -34
- 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";
|
|
@@ -433,27 +460,34 @@ function loadVideoTexture(src, onError) {
|
|
|
433
460
|
video.addEventListener("error", () => {
|
|
434
461
|
pendingVideos.delete(video);
|
|
435
462
|
const msg = video.error?.message ?? "Unknown video error";
|
|
436
|
-
onError?.(new Error(`Failed to load video texture: ${src} (${msg})`));
|
|
463
|
+
onError?.(new Error(`Failed to load video texture: ${layer.src} (${msg})`));
|
|
437
464
|
video.remove();
|
|
438
465
|
resolve({ texture: getTransparentTexture() });
|
|
439
466
|
}, { once: true });
|
|
467
|
+
video.src = layer.src;
|
|
440
468
|
video.play().then(() => {
|
|
441
469
|
pendingVideos.delete(video);
|
|
442
470
|
const texture = new THREE2.VideoTexture(video);
|
|
443
471
|
resolve({ texture, videoEl: video });
|
|
444
472
|
}).catch(() => {
|
|
445
473
|
pendingVideos.delete(video);
|
|
446
|
-
onError?.(new Error(`Video autoplay blocked: ${src}`));
|
|
474
|
+
onError?.(new Error(`Video autoplay blocked: ${layer.src}`));
|
|
447
475
|
video.remove();
|
|
448
476
|
resolve({ texture: getTransparentTexture() });
|
|
449
477
|
});
|
|
450
478
|
});
|
|
451
479
|
}
|
|
452
|
-
function loadFrozenVideoTexture(
|
|
480
|
+
function loadFrozenVideoTexture(layer, freezeTime, onError) {
|
|
453
481
|
return new Promise((resolve) => {
|
|
482
|
+
debugLog("freeze load start", {
|
|
483
|
+
src: layer.src,
|
|
484
|
+
freezeTime,
|
|
485
|
+
freezeSeed: layer.freezeSeed
|
|
486
|
+
});
|
|
454
487
|
const video = document.createElement("video");
|
|
455
|
-
video.
|
|
488
|
+
video.crossOrigin = "anonymous";
|
|
456
489
|
video.muted = true;
|
|
490
|
+
applyVideoPlaybackOptions(video, layer);
|
|
457
491
|
video.playsInline = true;
|
|
458
492
|
video.preload = "auto";
|
|
459
493
|
video.style.position = "fixed";
|
|
@@ -464,31 +498,137 @@ function loadFrozenVideoTexture(src, freezeTime, onError) {
|
|
|
464
498
|
video.style.zIndex = "-9999";
|
|
465
499
|
document.body.appendChild(video);
|
|
466
500
|
pendingVideos.add(video);
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
501
|
+
let settled = false;
|
|
502
|
+
let targetTime = 0;
|
|
503
|
+
let retrySeekRegistered = false;
|
|
504
|
+
const captureFrame = () => {
|
|
505
|
+
if (settled) return;
|
|
506
|
+
settled = true;
|
|
507
|
+
video.pause();
|
|
508
|
+
debugLog("freeze capture frame", {
|
|
509
|
+
src: layer.src,
|
|
510
|
+
currentTime: video.currentTime,
|
|
511
|
+
duration: video.duration,
|
|
512
|
+
readyState: video.readyState,
|
|
513
|
+
videoWidth: video.videoWidth,
|
|
514
|
+
videoHeight: video.videoHeight
|
|
515
|
+
});
|
|
479
516
|
const canvas = document.createElement("canvas");
|
|
480
|
-
canvas.width = video.videoWidth;
|
|
481
|
-
canvas.height = video.videoHeight;
|
|
517
|
+
canvas.width = video.videoWidth || 1;
|
|
518
|
+
canvas.height = video.videoHeight || 1;
|
|
482
519
|
const ctx = canvas.getContext("2d");
|
|
483
|
-
ctx
|
|
520
|
+
if (!ctx) {
|
|
521
|
+
pendingVideos.delete(video);
|
|
522
|
+
video.src = "";
|
|
523
|
+
video.remove();
|
|
524
|
+
resolve({ texture: getTransparentTexture() });
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
try {
|
|
528
|
+
ctx.drawImage(video, 0, 0);
|
|
529
|
+
} catch (error) {
|
|
530
|
+
debugLog("freeze drawImage failed", {
|
|
531
|
+
src: layer.src,
|
|
532
|
+
error: error instanceof Error ? error.message : String(error)
|
|
533
|
+
});
|
|
534
|
+
pendingVideos.delete(video);
|
|
535
|
+
video.src = "";
|
|
536
|
+
video.remove();
|
|
537
|
+
onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
538
|
+
resolve({ texture: getTransparentTexture() });
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
484
541
|
const texture = new THREE2.CanvasTexture(canvas);
|
|
485
542
|
texture.needsUpdate = true;
|
|
543
|
+
debugLog("freeze canvas texture ready", {
|
|
544
|
+
src: layer.src,
|
|
545
|
+
width: canvas.width,
|
|
546
|
+
height: canvas.height
|
|
547
|
+
});
|
|
486
548
|
pendingVideos.delete(video);
|
|
487
|
-
video.pause();
|
|
488
549
|
video.src = "";
|
|
489
550
|
video.remove();
|
|
490
551
|
resolve({ texture });
|
|
552
|
+
};
|
|
553
|
+
const captureReadyFrame = () => {
|
|
554
|
+
debugLog("freeze wait decoded frame", {
|
|
555
|
+
src: layer.src,
|
|
556
|
+
currentTime: video.currentTime,
|
|
557
|
+
readyState: video.readyState
|
|
558
|
+
});
|
|
559
|
+
if ("requestVideoFrameCallback" in video) {
|
|
560
|
+
video.requestVideoFrameCallback(() => captureFrame());
|
|
561
|
+
}
|
|
562
|
+
const playPromise = video.play();
|
|
563
|
+
playPromise.catch(() => {
|
|
564
|
+
debugLog("freeze play rejected, fallback capture", { src: layer.src });
|
|
565
|
+
window.setTimeout(captureFrame, 0);
|
|
566
|
+
});
|
|
567
|
+
if (!("requestVideoFrameCallback" in video)) {
|
|
568
|
+
window.setTimeout(captureFrame, 0);
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
const retrySeekWhenReady = () => {
|
|
572
|
+
if (settled) return;
|
|
573
|
+
if (retrySeekRegistered) return;
|
|
574
|
+
retrySeekRegistered = true;
|
|
575
|
+
const retry = () => {
|
|
576
|
+
retrySeekRegistered = false;
|
|
577
|
+
if (settled) return;
|
|
578
|
+
debugLog("freeze retry seek", {
|
|
579
|
+
src: layer.src,
|
|
580
|
+
targetTime,
|
|
581
|
+
currentTime: video.currentTime,
|
|
582
|
+
duration: video.duration,
|
|
583
|
+
readyState: video.readyState
|
|
584
|
+
});
|
|
585
|
+
video.currentTime = targetTime;
|
|
586
|
+
};
|
|
587
|
+
video.addEventListener("durationchange", retry, { once: true });
|
|
588
|
+
video.addEventListener("canplay", retry, { once: true });
|
|
589
|
+
video.addEventListener("loadeddata", retry, { once: true });
|
|
590
|
+
};
|
|
591
|
+
video.addEventListener("error", () => {
|
|
592
|
+
pendingVideos.delete(video);
|
|
593
|
+
const msg = video.error?.message ?? "Unknown video error";
|
|
594
|
+
onError?.(new Error(`Failed to load video texture: ${layer.src} (${msg})`));
|
|
595
|
+
video.remove();
|
|
596
|
+
resolve({ texture: getTransparentTexture() });
|
|
597
|
+
}, { once: true });
|
|
598
|
+
video.addEventListener("loadedmetadata", () => {
|
|
599
|
+
const time = getFreezeTargetTime(freezeTime, video.duration, layer.freezeSeed);
|
|
600
|
+
targetTime = time;
|
|
601
|
+
debugLog("freeze metadata loaded", {
|
|
602
|
+
src: layer.src,
|
|
603
|
+
targetTime: time,
|
|
604
|
+
duration: video.duration,
|
|
605
|
+
readyState: video.readyState
|
|
606
|
+
});
|
|
607
|
+
if (isAtTargetTime(video, time)) {
|
|
608
|
+
if (video.readyState >= 2) {
|
|
609
|
+
captureReadyFrame();
|
|
610
|
+
} else {
|
|
611
|
+
video.addEventListener("loadeddata", () => captureReadyFrame(), { once: true });
|
|
612
|
+
}
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
video.currentTime = time;
|
|
491
616
|
}, { once: true });
|
|
617
|
+
video.addEventListener("seeked", () => {
|
|
618
|
+
debugLog("freeze seeked", {
|
|
619
|
+
src: layer.src,
|
|
620
|
+
targetTime,
|
|
621
|
+
currentTime: video.currentTime,
|
|
622
|
+
duration: video.duration,
|
|
623
|
+
readyState: video.readyState
|
|
624
|
+
});
|
|
625
|
+
if (!isAtTargetTime(video, targetTime)) {
|
|
626
|
+
retrySeekWhenReady();
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
captureReadyFrame();
|
|
630
|
+
});
|
|
631
|
+
video.src = layer.src;
|
|
492
632
|
video.load();
|
|
493
633
|
});
|
|
494
634
|
}
|
|
@@ -1823,6 +1963,7 @@ var MAX_LAYERS = 8;
|
|
|
1823
1963
|
var FaceRenderer = class {
|
|
1824
1964
|
constructor(shader, width, height, effectUniforms) {
|
|
1825
1965
|
this.loadedLayers = [];
|
|
1966
|
+
this.layerLoadVersion = 0;
|
|
1826
1967
|
bootstrapShaders();
|
|
1827
1968
|
this.scene = new THREE3.Scene();
|
|
1828
1969
|
this.camera = new THREE3.Camera();
|
|
@@ -1872,6 +2013,7 @@ var FaceRenderer = class {
|
|
|
1872
2013
|
}
|
|
1873
2014
|
}
|
|
1874
2015
|
async loadLayers(layers, onError) {
|
|
2016
|
+
const version = ++this.layerLoadVersion;
|
|
1875
2017
|
this.disposeLayers();
|
|
1876
2018
|
const count = Math.min(layers.length, MAX_LAYERS);
|
|
1877
2019
|
this.material.uniforms.uLayerCount.value = count;
|
|
@@ -1879,6 +2021,12 @@ var FaceRenderer = class {
|
|
|
1879
2021
|
(layer) => loadLayerTexture(layer, onError)
|
|
1880
2022
|
);
|
|
1881
2023
|
const results = await Promise.all(promises);
|
|
2024
|
+
if (version !== this.layerLoadVersion) {
|
|
2025
|
+
for (const loaded of results) {
|
|
2026
|
+
disposeLoadedLayer(loaded);
|
|
2027
|
+
}
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
1882
2030
|
this.loadedLayers = results;
|
|
1883
2031
|
for (let i = 0; i < results.length; i++) {
|
|
1884
2032
|
this.material.uniforms[`uLayer${i}`].value = results[i].texture;
|
|
@@ -1914,6 +2062,7 @@ var FaceRenderer = class {
|
|
|
1914
2062
|
this.material.uniforms.uLayerCount.value = 0;
|
|
1915
2063
|
}
|
|
1916
2064
|
destroy() {
|
|
2065
|
+
this.layerLoadVersion++;
|
|
1917
2066
|
this.disposeLayers();
|
|
1918
2067
|
this.mesh.geometry.dispose();
|
|
1919
2068
|
this.material.dispose();
|
|
@@ -2018,10 +2167,10 @@ var Renderer = class {
|
|
|
2018
2167
|
this.loadAllLayers();
|
|
2019
2168
|
this.setupResizeObserver();
|
|
2020
2169
|
this.setupIntersectionObserver();
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2170
|
+
}
|
|
2171
|
+
disposeWebGLRenderer(renderer) {
|
|
2172
|
+
renderer.forceContextLoss();
|
|
2173
|
+
renderer.dispose();
|
|
2025
2174
|
}
|
|
2026
2175
|
createFrontRenderer() {
|
|
2027
2176
|
if (this.frontRenderer) return;
|
|
@@ -2052,7 +2201,7 @@ var Renderer = class {
|
|
|
2052
2201
|
disposeFrontRenderer() {
|
|
2053
2202
|
if (!this.frontRenderer) return;
|
|
2054
2203
|
this.frontRenderer.render(this.frontFace.scene, this.frontFace.camera);
|
|
2055
|
-
this.frontRenderer
|
|
2204
|
+
this.disposeWebGLRenderer(this.frontRenderer);
|
|
2056
2205
|
this.frontRenderer = null;
|
|
2057
2206
|
}
|
|
2058
2207
|
disposeBackRenderer() {
|
|
@@ -2060,7 +2209,7 @@ var Renderer = class {
|
|
|
2060
2209
|
if (this.backFace) {
|
|
2061
2210
|
this.backRenderer.render(this.backFace.scene, this.backFace.camera);
|
|
2062
2211
|
}
|
|
2063
|
-
this.backRenderer
|
|
2212
|
+
this.disposeWebGLRenderer(this.backRenderer);
|
|
2064
2213
|
this.backRenderer = null;
|
|
2065
2214
|
}
|
|
2066
2215
|
activate() {
|
|
@@ -2103,7 +2252,13 @@ var Renderer = class {
|
|
|
2103
2252
|
promises.push(this.backFace.loadLayers(this.options.back.layers, this.onError));
|
|
2104
2253
|
}
|
|
2105
2254
|
await Promise.all(promises);
|
|
2106
|
-
this.
|
|
2255
|
+
if (this.destroyed) return;
|
|
2256
|
+
requestAnimationFrame(() => {
|
|
2257
|
+
if (this.destroyed) return;
|
|
2258
|
+
this.cardEl.classList.remove("pocato-loading");
|
|
2259
|
+
this.cardEl.classList.add("pocato-ready");
|
|
2260
|
+
this.onReady?.();
|
|
2261
|
+
});
|
|
2107
2262
|
}
|
|
2108
2263
|
injectStyles() {
|
|
2109
2264
|
const id = "pocato-styles";
|
|
@@ -2367,10 +2522,16 @@ var Renderer = class {
|
|
|
2367
2522
|
this.intersectionObserver?.disconnect();
|
|
2368
2523
|
cleanupPendingVideos();
|
|
2369
2524
|
this.frontFace.destroy();
|
|
2370
|
-
this.frontRenderer
|
|
2525
|
+
if (this.frontRenderer) {
|
|
2526
|
+
this.disposeWebGLRenderer(this.frontRenderer);
|
|
2527
|
+
this.frontRenderer = null;
|
|
2528
|
+
}
|
|
2371
2529
|
if (this.backFace) {
|
|
2372
2530
|
this.backFace.destroy();
|
|
2373
|
-
this.backRenderer
|
|
2531
|
+
if (this.backRenderer) {
|
|
2532
|
+
this.disposeWebGLRenderer(this.backRenderer);
|
|
2533
|
+
this.backRenderer = null;
|
|
2534
|
+
}
|
|
2374
2535
|
}
|
|
2375
2536
|
if (this.cardEl.parentNode) {
|
|
2376
2537
|
this.cardEl.parentNode.removeChild(this.cardEl);
|