@editframe/elements 0.33.0-beta → 0.34.5-beta
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/EF_FRAMEGEN.js +5 -3
- package/dist/EF_FRAMEGEN.js.map +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.94.0 → _@oxc-project_runtime@0.95.0}/helpers/decorate.js +1 -1
- package/dist/canvas/EFCanvas.d.ts +7 -4
- package/dist/canvas/EFCanvas.js +1 -1
- package/dist/canvas/EFCanvasItem.d.ts +4 -4
- package/dist/canvas/EFCanvasItem.js +1 -1
- package/dist/canvas/overlays/SelectionOverlay.d.ts +95 -0
- package/dist/canvas/overlays/SelectionOverlay.js +1 -1
- package/dist/canvas/selection/SelectionController.js +7 -11
- package/dist/canvas/selection/SelectionController.js.map +1 -1
- package/dist/elements/EFAudio.d.ts +25 -7
- package/dist/elements/EFAudio.js +31 -61
- package/dist/elements/EFAudio.js.map +1 -1
- package/dist/elements/EFCaptions.d.ts +65 -52
- package/dist/elements/EFCaptions.js +186 -400
- package/dist/elements/EFCaptions.js.map +1 -1
- package/dist/elements/EFImage.d.ts +34 -6
- package/dist/elements/EFImage.js +114 -79
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +17 -17
- package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/AssetMediaEngine.js +41 -25
- package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/BaseMediaEngine.js +4 -4
- package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.js +1 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.js.map +1 -1
- package/dist/elements/EFMedia/JitMediaEngine.js +31 -17
- package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -3
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +17 -9
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
- package/dist/elements/EFMedia.d.ts +66 -20
- package/dist/elements/EFMedia.js +412 -30
- package/dist/elements/EFMedia.js.map +1 -1
- package/dist/elements/EFPanZoom.d.ts +4 -4
- package/dist/elements/EFPanZoom.js +1 -1
- package/dist/elements/EFSourceMixin.js +43 -15
- package/dist/elements/EFSourceMixin.js.map +1 -1
- package/dist/elements/EFSurface.d.ts +23 -10
- package/dist/elements/EFSurface.js +64 -22
- package/dist/elements/EFSurface.js.map +1 -1
- package/dist/elements/EFTemporal.d.ts +8 -2
- package/dist/elements/EFTemporal.js +42 -31
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFText.d.ts +5 -4
- package/dist/elements/EFText.js +11 -2
- package/dist/elements/EFText.js.map +1 -1
- package/dist/elements/EFTextSegment.d.ts +4 -4
- package/dist/elements/EFTextSegment.js +1 -1
- package/dist/elements/EFThumbnailStrip.d.ts +4 -4
- package/dist/elements/EFThumbnailStrip.js +1 -1
- package/dist/elements/EFTimegroup.d.ts +22 -8
- package/dist/elements/EFTimegroup.js +203 -115
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.d.ts +57 -20
- package/dist/elements/EFVideo.js +324 -72
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/elements/EFWaveform.d.ts +33 -7
- package/dist/elements/EFWaveform.js +103 -59
- package/dist/elements/EFWaveform.js.map +1 -1
- package/dist/elements/renderTemporalAudio.js +14 -3
- package/dist/elements/renderTemporalAudio.js.map +1 -1
- package/dist/getRenderInfo.d.ts +2 -2
- package/dist/gui/ContextMixin.js +1 -1
- package/dist/gui/Controllable.d.ts +2 -0
- package/dist/gui/EFActiveRootTemporal.d.ts +4 -4
- package/dist/gui/EFActiveRootTemporal.js +1 -1
- package/dist/gui/EFConfiguration.d.ts +4 -4
- package/dist/gui/EFConfiguration.js +1 -1
- package/dist/gui/EFControls.d.ts +2 -2
- package/dist/gui/EFControls.js +1 -1
- package/dist/gui/EFDial.d.ts +4 -4
- package/dist/gui/EFDial.js +1 -1
- package/dist/gui/EFFilmstrip.d.ts +3 -2
- package/dist/gui/EFFilmstrip.js +1 -1
- package/dist/gui/EFFitScale.js +1 -1
- package/dist/gui/EFFocusOverlay.d.ts +4 -4
- package/dist/gui/EFFocusOverlay.js +1 -1
- package/dist/gui/EFOverlayItem.d.ts +4 -4
- package/dist/gui/EFOverlayItem.js +1 -1
- package/dist/gui/EFOverlayLayer.d.ts +4 -4
- package/dist/gui/EFOverlayLayer.js +1 -1
- package/dist/gui/EFPause.d.ts +4 -4
- package/dist/gui/EFPause.js +1 -1
- package/dist/gui/EFPlay.d.ts +4 -4
- package/dist/gui/EFPlay.js +1 -1
- package/dist/gui/EFPreview.d.ts +4 -4
- package/dist/gui/EFPreview.js +1 -1
- package/dist/gui/EFResizableBox.d.ts +4 -4
- package/dist/gui/EFResizableBox.js +1 -1
- package/dist/gui/EFScrubber.d.ts +4 -4
- package/dist/gui/EFScrubber.js +1 -1
- package/dist/gui/EFTimeDisplay.d.ts +4 -4
- package/dist/gui/EFTimeDisplay.js +1 -1
- package/dist/gui/EFTimelineRuler.d.ts +4 -4
- package/dist/gui/EFTimelineRuler.js +1 -1
- package/dist/gui/EFToggleLoop.d.ts +4 -4
- package/dist/gui/EFToggleLoop.js +1 -1
- package/dist/gui/EFTogglePlay.d.ts +4 -4
- package/dist/gui/EFTogglePlay.js +1 -1
- package/dist/gui/EFTransformHandles.d.ts +4 -4
- package/dist/gui/EFTransformHandles.js +1 -1
- package/dist/gui/EFWorkbench.d.ts +5 -4
- package/dist/gui/EFWorkbench.js +1 -1
- package/dist/gui/PlaybackController.d.ts +10 -2
- package/dist/gui/PlaybackController.js +52 -30
- package/dist/gui/PlaybackController.js.map +1 -1
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/TWMixin.js.map +1 -1
- package/dist/gui/TargetOrContextMixin.js +1 -1
- package/dist/gui/hierarchy/EFHierarchy.d.ts +4 -4
- package/dist/gui/hierarchy/EFHierarchy.js +1 -1
- package/dist/gui/hierarchy/EFHierarchyItem.d.ts +3 -3
- package/dist/gui/hierarchy/EFHierarchyItem.js +1 -1
- package/dist/gui/timeline/EFTimeline.d.ts +6 -2
- package/dist/gui/timeline/EFTimeline.js +1 -1
- package/dist/gui/timeline/EFTimelineRow.d.ts +57 -0
- package/dist/gui/timeline/EFTimelineRow.js +1 -1
- package/dist/gui/timeline/TrimHandles.d.ts +4 -4
- package/dist/gui/timeline/TrimHandles.js +1 -1
- package/dist/gui/timeline/tracks/AudioTrack.d.ts +2 -0
- package/dist/gui/timeline/tracks/AudioTrack.js +1 -1
- package/dist/gui/timeline/tracks/CaptionsTrack.d.ts +58 -0
- package/dist/gui/timeline/tracks/CaptionsTrack.js +1 -1
- package/dist/gui/timeline/tracks/HTMLTrack.d.ts +13 -0
- package/dist/gui/timeline/tracks/HTMLTrack.js +1 -1
- package/dist/gui/timeline/tracks/ImageTrack.d.ts +14 -0
- package/dist/gui/timeline/tracks/ImageTrack.js +1 -1
- package/dist/gui/timeline/tracks/TextTrack.d.ts +26 -0
- package/dist/gui/timeline/tracks/TextTrack.js +1 -1
- package/dist/gui/timeline/tracks/TimegroupTrack.d.ts +47 -0
- package/dist/gui/timeline/tracks/TimegroupTrack.js +4 -12
- package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TrackItem.d.ts +81 -0
- package/dist/gui/timeline/tracks/TrackItem.js +1 -1
- package/dist/gui/timeline/tracks/VideoTrack.d.ts +25 -0
- package/dist/gui/timeline/tracks/VideoTrack.js +1 -1
- package/dist/gui/timeline/tracks/WaveformTrack.d.ts +14 -0
- package/dist/gui/timeline/tracks/WaveformTrack.js +1 -1
- package/dist/gui/timeline/tracks/ensureTrackItemInit.d.ts +1 -0
- package/dist/gui/timeline/tracks/preloadTracks.d.ts +9 -0
- package/dist/gui/tree/EFTree.d.ts +5 -4
- package/dist/gui/tree/EFTree.js +1 -1
- package/dist/gui/tree/EFTreeItem.d.ts +4 -4
- package/dist/gui/tree/EFTreeItem.js +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/preview/AdaptiveResolutionTracker.js +6 -14
- package/dist/preview/AdaptiveResolutionTracker.js.map +1 -1
- package/dist/preview/FrameController.d.ts +123 -0
- package/dist/preview/FrameController.js +216 -0
- package/dist/preview/FrameController.js.map +1 -0
- package/dist/preview/RenderContext.d.ts +1 -0
- package/dist/preview/RenderContext.js +193 -0
- package/dist/preview/RenderContext.js.map +1 -0
- package/dist/preview/encoding/canvasEncoder.js +166 -0
- package/dist/preview/encoding/canvasEncoder.js.map +1 -0
- package/dist/preview/encoding/mainThreadEncoder.js +39 -0
- package/dist/preview/encoding/mainThreadEncoder.js.map +1 -0
- package/dist/preview/encoding/types.d.ts +1 -0
- package/dist/preview/encoding/workerEncoder.js +58 -0
- package/dist/preview/encoding/workerEncoder.js.map +1 -0
- package/dist/preview/logger.js +41 -0
- package/dist/preview/logger.js.map +1 -0
- package/dist/preview/previewTypes.js +11 -10
- package/dist/preview/previewTypes.js.map +1 -1
- package/dist/preview/renderTimegroupPreview.js +259 -236
- package/dist/preview/renderTimegroupPreview.js.map +1 -1
- package/dist/preview/renderTimegroupToCanvas.d.ts +5 -0
- package/dist/preview/renderTimegroupToCanvas.js +99 -489
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
- package/dist/preview/renderTimegroupToVideo.d.ts +1 -0
- package/dist/preview/renderTimegroupToVideo.js +80 -22
- package/dist/preview/renderTimegroupToVideo.js.map +1 -1
- package/dist/preview/renderers.js.map +1 -1
- package/dist/preview/rendering/inlineImages.js +56 -0
- package/dist/preview/rendering/inlineImages.js.map +1 -0
- package/dist/preview/rendering/renderToImage.d.ts +1 -0
- package/dist/preview/rendering/renderToImage.js +120 -0
- package/dist/preview/rendering/renderToImage.js.map +1 -0
- package/dist/preview/rendering/renderToImageForeignObject.js +135 -0
- package/dist/preview/rendering/renderToImageForeignObject.js.map +1 -0
- package/dist/preview/rendering/renderToImageNative.d.ts +1 -0
- package/dist/preview/rendering/renderToImageNative.js +129 -0
- package/dist/preview/rendering/renderToImageNative.js.map +1 -0
- package/dist/preview/rendering/svgSerializer.js +43 -0
- package/dist/preview/rendering/svgSerializer.js.map +1 -0
- package/dist/preview/rendering/types.d.ts +2 -0
- package/dist/preview/statsTrackingStrategy.js +3 -1
- package/dist/preview/statsTrackingStrategy.js.map +1 -1
- package/dist/preview/workers/WorkerPool.js +8 -57
- package/dist/preview/workers/WorkerPool.js.map +1 -1
- package/dist/render/EFRenderAPI.d.ts +35 -0
- package/dist/render/EFRenderAPI.js +1 -0
- package/dist/render/EFRenderAPI.js.map +1 -1
- package/dist/sandbox/PlaybackControls.d.ts +1 -0
- package/dist/sandbox/ScenarioRunner.d.ts +1 -0
- package/dist/sandbox/defineSandbox.d.ts +1 -0
- package/dist/sandbox/index.d.ts +3 -0
- package/dist/style.css +3 -0
- package/dist/transcoding/types/index.d.ts +6 -3
- package/package.json +2 -3
- package/test/EFVideo.framegen.browsertest.ts +8 -1
- package/test/profilingPlugin.ts +1 -3
- package/test/setup.ts +23 -1
- package/dist/EF_INTERACTIVE.js +0 -7
- package/dist/EF_INTERACTIVE.js.map +0 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +0 -50
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.d.ts +0 -12
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +0 -104
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js.map +0 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +0 -168
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js.map +0 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +0 -46
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js.map +0 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +0 -49
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js.map +0 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +0 -30
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js.map +0 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +0 -49
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js.map +0 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +0 -47
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js.map +0 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +0 -140
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js.map +0 -1
- package/dist/elements/EFMedia/shared/BufferUtils.d.ts +0 -13
- package/dist/elements/EFMedia/shared/BufferUtils.js +0 -86
- package/dist/elements/EFMedia/shared/BufferUtils.js.map +0 -1
- package/dist/elements/EFMedia/shared/MediaTaskUtils.d.ts +0 -17
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +0 -90
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +0 -80
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js +0 -49
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +0 -58
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +0 -71
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js +0 -52
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +0 -50
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +0 -109
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js.map +0 -1
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.d.ts +0 -12
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +0 -97
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js.map +0 -1
- package/dist/elements/SampleBuffer.d.ts +0 -19
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { FrameController } from "./FrameController.js";
|
|
2
|
+
import { logger } from "./logger.js";
|
|
3
|
+
import { DEFAULT_BLOCKING_TIMEOUT_MS, DEFAULT_HEIGHT, DEFAULT_THUMBNAIL_SCALE, DEFAULT_WIDTH, createPreviewContainer, isVisibleAtTime } from "./previewTypes.js";
|
|
4
|
+
import { buildCloneStructure, collectDocumentStyles, overrideRootCloneStyles, removeHiddenNodesForSerialization, restoreHiddenNodes, syncStyles } from "./renderTimegroupPreview.js";
|
|
3
5
|
import { getEffectiveRenderMode } from "./renderers.js";
|
|
4
|
-
import {
|
|
5
|
-
import { getEncoderWorkerUrl } from "./workers/encoderWorkerInline.js";
|
|
6
|
+
import { RenderContext } from "./RenderContext.js";
|
|
6
7
|
import { defaultProfiler } from "./RenderProfiler.js";
|
|
8
|
+
import { createDprCanvas, renderToImageNative } from "./rendering/renderToImageNative.js";
|
|
9
|
+
import { clearInlineImageCache } from "./rendering/inlineImages.js";
|
|
10
|
+
import { loadImageFromDataUri, renderToImage, renderToImageDirect } from "./rendering/renderToImage.js";
|
|
7
11
|
|
|
8
12
|
//#region src/preview/renderTimegroupToCanvas.ts
|
|
9
13
|
/** Number of rows to sample when checking canvas content */
|
|
10
14
|
const CANVAS_SAMPLE_STRIP_HEIGHT = 4;
|
|
11
|
-
/** Interval between profiling log outputs (ms) */
|
|
12
|
-
const PROFILING_LOG_INTERVAL_MS = 2e3;
|
|
13
|
-
/** Maximum number of cached inline images before eviction */
|
|
14
|
-
const MAX_INLINE_IMAGE_CACHE_SIZE = 100;
|
|
15
15
|
/**
|
|
16
16
|
* Error thrown when video content is not ready within the blocking timeout.
|
|
17
17
|
*/
|
|
@@ -24,159 +24,27 @@ var ContentNotReadyError = class extends Error {
|
|
|
24
24
|
this.name = "ContentNotReadyError";
|
|
25
25
|
}
|
|
26
26
|
};
|
|
27
|
-
/** Image cache for inlining external images as data URIs (foreignObject path) */
|
|
28
|
-
const _inlineImageCache = /* @__PURE__ */ new Map();
|
|
29
|
-
/** Track canvases that have been initialized for layoutsubtree (only need to wait once) */
|
|
30
|
-
const _layoutInitializedCanvases = /* @__PURE__ */ new WeakSet();
|
|
31
|
-
let _xmlSerializer = null;
|
|
32
|
-
let _textEncoder = null;
|
|
33
|
-
let _workerPool = null;
|
|
34
|
-
let _workerPoolWarningLogged = false;
|
|
35
27
|
/**
|
|
36
|
-
*
|
|
37
|
-
* Returns null if workers are not available.
|
|
28
|
+
* Module-level state for render operations.
|
|
38
29
|
*/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
_workerPool = new WorkerPool(getEncoderWorkerUrl());
|
|
50
|
-
if (!_workerPool.isAvailable()) {
|
|
51
|
-
const reason = _workerPool.workerCount === 0 ? "no workers created (check console for errors)" : "workers not available";
|
|
52
|
-
_workerPool = null;
|
|
53
|
-
if (!_workerPoolWarningLogged) {
|
|
54
|
-
_workerPoolWarningLogged = true;
|
|
55
|
-
console.warn(`[renderTimegroupToCanvas] Worker pool initialization failed (${reason}), using main thread fallback`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
} catch (error) {
|
|
59
|
-
_workerPool = null;
|
|
60
|
-
if (!_workerPoolWarningLogged) {
|
|
61
|
-
_workerPoolWarningLogged = true;
|
|
62
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
63
|
-
console.warn(`[renderTimegroupToCanvas] Failed to create worker pool: ${errorMessage} - using main thread fallback`);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return _workerPool;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Encode a single canvas to a data URL (fallback implementation for main thread).
|
|
70
|
-
*/
|
|
71
|
-
function encodeCanvasOnMainThread(canvas, canvasScale) {
|
|
72
|
-
try {
|
|
73
|
-
if (canvas.width === 0 || canvas.height === 0) return null;
|
|
74
|
-
const preserveAlpha = canvas.dataset.preserveAlpha === "true";
|
|
75
|
-
let dataUrl;
|
|
76
|
-
if (canvasScale < 1) {
|
|
77
|
-
const scaledWidth = Math.floor(canvas.width * canvasScale);
|
|
78
|
-
const scaledHeight = Math.floor(canvas.height * canvasScale);
|
|
79
|
-
const scaledCanvas = document.createElement("canvas");
|
|
80
|
-
scaledCanvas.width = scaledWidth;
|
|
81
|
-
scaledCanvas.height = scaledHeight;
|
|
82
|
-
const scaledCtx = scaledCanvas.getContext("2d");
|
|
83
|
-
if (scaledCtx) {
|
|
84
|
-
scaledCtx.drawImage(canvas, 0, 0, scaledWidth, scaledHeight);
|
|
85
|
-
const quality = canvasScale < .5 ? JPEG_QUALITY_MEDIUM : JPEG_QUALITY_HIGH;
|
|
86
|
-
dataUrl = preserveAlpha ? scaledCanvas.toDataURL("image/png") : scaledCanvas.toDataURL("image/jpeg", quality);
|
|
87
|
-
} else dataUrl = preserveAlpha ? canvas.toDataURL("image/png") : canvas.toDataURL("image/jpeg", JPEG_QUALITY_HIGH);
|
|
88
|
-
} else dataUrl = preserveAlpha ? canvas.toDataURL("image/png") : canvas.toDataURL("image/jpeg", JPEG_QUALITY_HIGH);
|
|
89
|
-
return {
|
|
90
|
-
dataUrl,
|
|
91
|
-
preserveAlpha
|
|
92
|
-
};
|
|
93
|
-
} catch (e) {
|
|
94
|
-
return null;
|
|
30
|
+
const renderState = {
|
|
31
|
+
inlineImageCache: /* @__PURE__ */ new Map(),
|
|
32
|
+
layoutInitializedCanvases: /* @__PURE__ */ new WeakSet(),
|
|
33
|
+
xmlSerializer: new XMLSerializer(),
|
|
34
|
+
textEncoder: new TextEncoder(),
|
|
35
|
+
metrics: {
|
|
36
|
+
inlineImageCacheHits: 0,
|
|
37
|
+
inlineImageCacheMisses: 0,
|
|
38
|
+
inlineImageCacheEvictions: 0
|
|
95
39
|
}
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Encode canvases to data URLs in parallel using worker pool.
|
|
99
|
-
* Falls back to main thread encoding if workers are unavailable.
|
|
100
|
-
*/
|
|
101
|
-
async function encodeCanvasesInParallel(canvases, canvasScale = 1) {
|
|
102
|
-
const workerPool = getWorkerPool();
|
|
103
|
-
if (!workerPool) {
|
|
104
|
-
const results = [];
|
|
105
|
-
for (const canvas of canvases) {
|
|
106
|
-
const encoded = encodeCanvasOnMainThread(canvas, canvasScale);
|
|
107
|
-
if (encoded) results.push({
|
|
108
|
-
canvas,
|
|
109
|
-
...encoded
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
return results;
|
|
113
|
-
}
|
|
114
|
-
const encodingTasks = canvases.map(async (canvas) => {
|
|
115
|
-
try {
|
|
116
|
-
if (canvas.width === 0 || canvas.height === 0) return null;
|
|
117
|
-
const preserveAlpha = canvas.dataset.preserveAlpha === "true";
|
|
118
|
-
let sourceCanvas = canvas;
|
|
119
|
-
if (canvasScale < 1) {
|
|
120
|
-
const scaledWidth = Math.floor(canvas.width * canvasScale);
|
|
121
|
-
const scaledHeight = Math.floor(canvas.height * canvasScale);
|
|
122
|
-
const scaledCanvas = document.createElement("canvas");
|
|
123
|
-
scaledCanvas.width = scaledWidth;
|
|
124
|
-
scaledCanvas.height = scaledHeight;
|
|
125
|
-
const scaledCtx = scaledCanvas.getContext("2d");
|
|
126
|
-
if (scaledCtx) {
|
|
127
|
-
scaledCtx.drawImage(canvas, 0, 0, scaledWidth, scaledHeight);
|
|
128
|
-
sourceCanvas = scaledCanvas;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return {
|
|
132
|
-
canvas,
|
|
133
|
-
dataUrl: await workerPool.execute((worker) => encodeCanvasInWorker(worker, sourceCanvas, preserveAlpha)),
|
|
134
|
-
preserveAlpha
|
|
135
|
-
};
|
|
136
|
-
} catch (error) {
|
|
137
|
-
const encoded = encodeCanvasOnMainThread(canvas, canvasScale);
|
|
138
|
-
if (encoded) return {
|
|
139
|
-
canvas,
|
|
140
|
-
...encoded
|
|
141
|
-
};
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
return (await Promise.all(encodingTasks)).filter((r) => r !== null);
|
|
146
|
-
}
|
|
40
|
+
};
|
|
147
41
|
/**
|
|
148
|
-
*
|
|
149
|
-
* Avoids the overhead of converting to binary string first.
|
|
150
|
-
* Uses lookup table for optimal performance.
|
|
42
|
+
* Reset cache metrics to zero.
|
|
151
43
|
*/
|
|
152
|
-
function
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const len = bytes.length;
|
|
157
|
-
while (i < len - 2) {
|
|
158
|
-
const byte1 = bytes[i++];
|
|
159
|
-
const byte2 = bytes[i++];
|
|
160
|
-
const byte3 = bytes[i++];
|
|
161
|
-
const bitmap = byte1 << 16 | byte2 << 8 | byte3;
|
|
162
|
-
result += base64Chars.charAt(bitmap >> 18 & 63);
|
|
163
|
-
result += base64Chars.charAt(bitmap >> 12 & 63);
|
|
164
|
-
result += base64Chars.charAt(bitmap >> 6 & 63);
|
|
165
|
-
result += base64Chars.charAt(bitmap & 63);
|
|
166
|
-
}
|
|
167
|
-
if (i < len) {
|
|
168
|
-
const byte1 = bytes[i++];
|
|
169
|
-
const bitmap = byte1 << 16;
|
|
170
|
-
result += base64Chars.charAt(bitmap >> 18 & 63);
|
|
171
|
-
result += base64Chars.charAt(bitmap >> 12 & 63);
|
|
172
|
-
if (i < len) {
|
|
173
|
-
const byte2 = bytes[i++];
|
|
174
|
-
const bitmap2 = byte1 << 16 | byte2 << 8;
|
|
175
|
-
result += base64Chars.charAt(bitmap2 >> 6 & 63);
|
|
176
|
-
result += "=";
|
|
177
|
-
} else result += "==";
|
|
178
|
-
}
|
|
179
|
-
return result;
|
|
44
|
+
function resetCacheMetrics() {
|
|
45
|
+
renderState.metrics.inlineImageCacheHits = 0;
|
|
46
|
+
renderState.metrics.inlineImageCacheMisses = 0;
|
|
47
|
+
renderState.metrics.inlineImageCacheEvictions = 0;
|
|
180
48
|
}
|
|
181
49
|
/**
|
|
182
50
|
* Reset all module state including profiling counters, caches, and logging flags.
|
|
@@ -184,22 +52,8 @@ function encodeBase64Fast(bytes) {
|
|
|
184
52
|
*/
|
|
185
53
|
function resetRenderState() {
|
|
186
54
|
defaultProfiler.reset();
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Create a canvas element with proper DPR handling.
|
|
191
|
-
* Buffer size is based on renderWidth/renderHeight (internal resolution).
|
|
192
|
-
* CSS size is based on fullWidth/fullHeight (logical display size).
|
|
193
|
-
*/
|
|
194
|
-
function createDprCanvas(options) {
|
|
195
|
-
const { renderWidth, renderHeight, scale, fullWidth, fullHeight } = options;
|
|
196
|
-
const dpr = options.dpr ?? window.devicePixelRatio ?? 1;
|
|
197
|
-
const canvas = document.createElement("canvas");
|
|
198
|
-
canvas.width = Math.floor(renderWidth * scale * dpr);
|
|
199
|
-
canvas.height = Math.floor(renderHeight * scale * dpr);
|
|
200
|
-
canvas.style.width = `${Math.floor(fullWidth * scale)}px`;
|
|
201
|
-
canvas.style.height = `${Math.floor(fullHeight * scale)}px`;
|
|
202
|
-
return canvas;
|
|
55
|
+
clearInlineImageCache();
|
|
56
|
+
resetCacheMetrics();
|
|
203
57
|
}
|
|
204
58
|
/**
|
|
205
59
|
* Create a debug label for showing render info.
|
|
@@ -234,135 +88,12 @@ function updateDebugLabel(label, renderWidth, renderHeight, resolutionScale) {
|
|
|
234
88
|
label.textContent = `Render: ${renderWidth}x${renderHeight} (${Math.round(resolutionScale * 100)}%)`;
|
|
235
89
|
}
|
|
236
90
|
/**
|
|
237
|
-
* Common SVG foreignObject serialization pipeline.
|
|
238
|
-
* Handles canvas encoding, serialization, and base64 encoding.
|
|
239
|
-
*
|
|
240
|
-
* @param container - The HTML element to serialize
|
|
241
|
-
* @param width - Output width
|
|
242
|
-
* @param height - Output height
|
|
243
|
-
* @param options - Serialization options
|
|
244
|
-
* @returns Serialization result with data URI and restore function
|
|
245
|
-
*/
|
|
246
|
-
async function serializeToSvgDataUri(container, width, height, options = {}) {
|
|
247
|
-
const { canvasScale = 1, inlineImages: shouldInlineImages = false, logEarlyRenders = false } = options;
|
|
248
|
-
const canvasRestoreInfo = [];
|
|
249
|
-
const canvasStart = performance.now();
|
|
250
|
-
const encodedResults = await encodeCanvasesInParallel(Array.from(container.querySelectorAll("canvas")), canvasScale);
|
|
251
|
-
for (const { canvas, dataUrl } of encodedResults) try {
|
|
252
|
-
const img = document.createElement("img");
|
|
253
|
-
img.src = dataUrl;
|
|
254
|
-
img.width = canvas.width;
|
|
255
|
-
img.height = canvas.height;
|
|
256
|
-
const style = canvas.getAttribute("style");
|
|
257
|
-
if (style) img.setAttribute("style", style);
|
|
258
|
-
const parent = canvas.parentNode;
|
|
259
|
-
if (parent) {
|
|
260
|
-
const nextSibling = canvas.nextSibling;
|
|
261
|
-
parent.replaceChild(img, canvas);
|
|
262
|
-
canvasRestoreInfo.push({
|
|
263
|
-
canvas,
|
|
264
|
-
parent,
|
|
265
|
-
nextSibling,
|
|
266
|
-
img
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
} catch {}
|
|
270
|
-
defaultProfiler.addTime("canvasEncode", performance.now() - canvasStart);
|
|
271
|
-
if (shouldInlineImages) {
|
|
272
|
-
const inlineStart = performance.now();
|
|
273
|
-
await inlineImages(container);
|
|
274
|
-
defaultProfiler.addTime("inline", performance.now() - inlineStart);
|
|
275
|
-
}
|
|
276
|
-
const serializeStart = performance.now();
|
|
277
|
-
const wrapper = document.createElement("div");
|
|
278
|
-
wrapper.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
|
279
|
-
wrapper.setAttribute("style", `width:${width}px;height:${height}px;overflow:hidden;position:relative;`);
|
|
280
|
-
wrapper.appendChild(container);
|
|
281
|
-
if (!_xmlSerializer) _xmlSerializer = new XMLSerializer();
|
|
282
|
-
const serialized = _xmlSerializer.serializeToString(wrapper);
|
|
283
|
-
defaultProfiler.addTime("serialize", performance.now() - serializeStart);
|
|
284
|
-
const restore = () => {
|
|
285
|
-
const restoreStart = performance.now();
|
|
286
|
-
wrapper.removeChild(container);
|
|
287
|
-
for (const { canvas, parent, nextSibling, img } of canvasRestoreInfo) if (img.parentNode === parent) if (nextSibling) {
|
|
288
|
-
parent.insertBefore(canvas, nextSibling);
|
|
289
|
-
parent.removeChild(img);
|
|
290
|
-
} else parent.replaceChild(canvas, img);
|
|
291
|
-
defaultProfiler.addTime("restore", performance.now() - restoreStart);
|
|
292
|
-
};
|
|
293
|
-
if (logEarlyRenders && defaultProfiler.isEarlyRender(2)) console.log(`[serializeToSvgDataUri] FO serialized: ${serialized.length} chars`);
|
|
294
|
-
const base64Start = performance.now();
|
|
295
|
-
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><foreignObject width="100%" height="100%">${serialized}</foreignObject></svg>`;
|
|
296
|
-
if (!_textEncoder) _textEncoder = new TextEncoder();
|
|
297
|
-
const utf8Bytes = _textEncoder.encode(svg);
|
|
298
|
-
let base64;
|
|
299
|
-
if (typeof Uint8Array.prototype.toBase64 === "function") base64 = utf8Bytes.toBase64();
|
|
300
|
-
else base64 = encodeBase64Fast(utf8Bytes);
|
|
301
|
-
const dataUri = `data:image/svg+xml;base64,${base64}`;
|
|
302
|
-
defaultProfiler.addTime("base64", performance.now() - base64Start);
|
|
303
|
-
return {
|
|
304
|
-
dataUri,
|
|
305
|
-
restore
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* Inline all images in a container as base64 data URIs.
|
|
310
|
-
* SVG foreignObject can't load external images due to security restrictions.
|
|
311
|
-
* Uses an LRU-style cache with size limits to prevent memory leaks.
|
|
312
|
-
*/
|
|
313
|
-
async function inlineImages(container) {
|
|
314
|
-
const images = container.querySelectorAll("img");
|
|
315
|
-
for (const image of images) {
|
|
316
|
-
const src = image.getAttribute("src");
|
|
317
|
-
if (!src || src.startsWith("data:")) continue;
|
|
318
|
-
const cached = _inlineImageCache.get(src);
|
|
319
|
-
if (cached) {
|
|
320
|
-
image.setAttribute("src", cached);
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
323
|
-
try {
|
|
324
|
-
const dataUrl = await blobToDataURL(await (await fetch(src)).blob());
|
|
325
|
-
image.setAttribute("src", dataUrl);
|
|
326
|
-
if (_inlineImageCache.size >= MAX_INLINE_IMAGE_CACHE_SIZE) {
|
|
327
|
-
const firstKey = _inlineImageCache.keys().next().value;
|
|
328
|
-
if (firstKey) _inlineImageCache.delete(firstKey);
|
|
329
|
-
}
|
|
330
|
-
_inlineImageCache.set(src, dataUrl);
|
|
331
|
-
} catch (e) {
|
|
332
|
-
console.warn("Failed to inline image:", src, e);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* Convert a Blob to a data URL.
|
|
338
|
-
*/
|
|
339
|
-
function blobToDataURL(blob) {
|
|
340
|
-
return new Promise((resolve, reject) => {
|
|
341
|
-
const reader = new FileReader();
|
|
342
|
-
reader.onload = () => resolve(reader.result);
|
|
343
|
-
reader.onerror = reject;
|
|
344
|
-
reader.readAsDataURL(blob);
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
91
|
* Wait for next animation frame (allows browser to complete layout)
|
|
349
92
|
*/
|
|
350
93
|
function waitForFrame() {
|
|
351
94
|
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
352
95
|
}
|
|
353
96
|
/**
|
|
354
|
-
* Wait for multiple animation frames to ensure all paints are flushed.
|
|
355
|
-
* This is necessary because video frame decoding and canvas painting may
|
|
356
|
-
* happen asynchronously even after seek() returns.
|
|
357
|
-
*/
|
|
358
|
-
function waitForPaintFlush() {
|
|
359
|
-
return new Promise((resolve) => {
|
|
360
|
-
requestAnimationFrame(() => {
|
|
361
|
-
requestAnimationFrame(() => resolve());
|
|
362
|
-
});
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
/**
|
|
366
97
|
* Check if a canvas has any rendered content (not all transparent/uninitialized).
|
|
367
98
|
* Returns true if there's ANY non-transparent pixel.
|
|
368
99
|
*/
|
|
@@ -433,157 +164,6 @@ async function waitForVideoContent(timegroup, timeMs, maxWaitMs) {
|
|
|
433
164
|
};
|
|
434
165
|
}
|
|
435
166
|
/**
|
|
436
|
-
* Render HTML content to canvas using native HTML-in-Canvas API (drawElementImage).
|
|
437
|
-
* This is much faster than the foreignObject approach and avoids canvas tainting.
|
|
438
|
-
*
|
|
439
|
-
* Note: The native API renders at device pixel ratio, so we capture at DPR scale
|
|
440
|
-
* and then downsample to logical pixels to match the foreignObject path's output.
|
|
441
|
-
*
|
|
442
|
-
* @param container - The HTML element to render
|
|
443
|
-
* @param width - Target width in logical pixels
|
|
444
|
-
* @param height - Target height in logical pixels
|
|
445
|
-
* @param options - Rendering options (skipWait for batch mode)
|
|
446
|
-
*
|
|
447
|
-
* @see https://github.com/WICG/html-in-canvas
|
|
448
|
-
*/
|
|
449
|
-
async function renderToImageNative(container, width, height, options = {}) {
|
|
450
|
-
const t0 = performance.now();
|
|
451
|
-
const { waitForPaint = false, reuseCanvas, skipDprScaling = false } = options;
|
|
452
|
-
const dpr = skipDprScaling ? 1 : window.devicePixelRatio || 1;
|
|
453
|
-
let captureCanvas;
|
|
454
|
-
let shouldCleanup = false;
|
|
455
|
-
if (reuseCanvas) {
|
|
456
|
-
captureCanvas = reuseCanvas;
|
|
457
|
-
const dpr$1 = skipDprScaling ? 1 : window.devicePixelRatio || 1;
|
|
458
|
-
const targetWidth = Math.floor(width * dpr$1);
|
|
459
|
-
const targetHeight = Math.floor(height * dpr$1);
|
|
460
|
-
if (captureCanvas.width !== targetWidth) captureCanvas.width = targetWidth;
|
|
461
|
-
if (captureCanvas.height !== targetHeight) captureCanvas.height = targetHeight;
|
|
462
|
-
captureCanvas.style.width = `${width}px`;
|
|
463
|
-
captureCanvas.style.height = `${height}px`;
|
|
464
|
-
if (!captureCanvas.hasAttribute("layoutsubtree")) {
|
|
465
|
-
captureCanvas.setAttribute("layoutsubtree", "");
|
|
466
|
-
captureCanvas.layoutSubtree = true;
|
|
467
|
-
}
|
|
468
|
-
if (!captureCanvas.parentNode) document.body.appendChild(captureCanvas);
|
|
469
|
-
if (container.parentElement !== captureCanvas) captureCanvas.appendChild(container);
|
|
470
|
-
if (getComputedStyle(container).display === "none") container.style.display = "block";
|
|
471
|
-
captureCanvas.offsetHeight;
|
|
472
|
-
container.offsetHeight;
|
|
473
|
-
getComputedStyle(captureCanvas).opacity;
|
|
474
|
-
getComputedStyle(container).opacity;
|
|
475
|
-
} else {
|
|
476
|
-
captureCanvas = document.createElement("canvas");
|
|
477
|
-
captureCanvas.width = Math.floor(width * dpr);
|
|
478
|
-
captureCanvas.height = Math.floor(height * dpr);
|
|
479
|
-
captureCanvas.setAttribute("layoutsubtree", "");
|
|
480
|
-
captureCanvas.layoutSubtree = true;
|
|
481
|
-
captureCanvas.appendChild(container);
|
|
482
|
-
captureCanvas.style.cssText = `
|
|
483
|
-
position: fixed;
|
|
484
|
-
left: 0;
|
|
485
|
-
top: 0;
|
|
486
|
-
width: ${width}px;
|
|
487
|
-
height: ${height}px;
|
|
488
|
-
opacity: 0;
|
|
489
|
-
pointer-events: none;
|
|
490
|
-
z-index: -9999;
|
|
491
|
-
`;
|
|
492
|
-
document.body.appendChild(captureCanvas);
|
|
493
|
-
shouldCleanup = true;
|
|
494
|
-
}
|
|
495
|
-
const t1 = performance.now();
|
|
496
|
-
defaultProfiler.addTime("setup", t1 - t0);
|
|
497
|
-
try {
|
|
498
|
-
getComputedStyle(container).opacity;
|
|
499
|
-
if (reuseCanvas && captureCanvas.layoutSubtree && !_layoutInitializedCanvases.has(captureCanvas)) {
|
|
500
|
-
await waitForFrame();
|
|
501
|
-
_layoutInitializedCanvases.add(captureCanvas);
|
|
502
|
-
if (!captureCanvas.parentNode) return captureCanvas;
|
|
503
|
-
}
|
|
504
|
-
if (waitForPaint) {
|
|
505
|
-
await waitForPaintFlush();
|
|
506
|
-
if (!captureCanvas.parentNode) return captureCanvas;
|
|
507
|
-
}
|
|
508
|
-
captureCanvas.getContext("2d").drawElementImage(container, 0, 0);
|
|
509
|
-
} finally {
|
|
510
|
-
if (shouldCleanup && captureCanvas.parentNode) captureCanvas.parentNode.removeChild(captureCanvas);
|
|
511
|
-
}
|
|
512
|
-
const t2 = performance.now();
|
|
513
|
-
defaultProfiler.addTime("draw", t2 - t1);
|
|
514
|
-
if (dpr === 1) {
|
|
515
|
-
defaultProfiler.incrementRenderCount();
|
|
516
|
-
return captureCanvas;
|
|
517
|
-
}
|
|
518
|
-
const outputCanvas = document.createElement("canvas");
|
|
519
|
-
outputCanvas.width = width;
|
|
520
|
-
outputCanvas.height = height;
|
|
521
|
-
outputCanvas.getContext("2d").drawImage(captureCanvas, 0, 0, captureCanvas.width, captureCanvas.height, 0, 0, width, height);
|
|
522
|
-
const t3 = performance.now();
|
|
523
|
-
defaultProfiler.addTime("downsample", t3 - t2);
|
|
524
|
-
defaultProfiler.incrementRenderCount();
|
|
525
|
-
defaultProfiler.shouldLogByTime(PROFILING_LOG_INTERVAL_MS);
|
|
526
|
-
return outputCanvas;
|
|
527
|
-
}
|
|
528
|
-
/**
|
|
529
|
-
* Render HTML content to an image (or canvas) for drawing.
|
|
530
|
-
*
|
|
531
|
-
* Supports two rendering modes (configurable via previewSettings):
|
|
532
|
-
* - "native": Chrome's experimental drawElementImage API (fastest when available)
|
|
533
|
-
* - "foreignObject": SVG foreignObject serialization (fallback, works everywhere)
|
|
534
|
-
*
|
|
535
|
-
* @param container - The HTML element to render
|
|
536
|
-
* @param width - Target width in logical pixels
|
|
537
|
-
* @param height - Target height in logical pixels
|
|
538
|
-
* @param options - Rendering options
|
|
539
|
-
* @returns HTMLCanvasElement when using native, HTMLImageElement when using foreignObject
|
|
540
|
-
*/
|
|
541
|
-
async function renderToImage(container, width, height, options) {
|
|
542
|
-
if (getEffectiveRenderMode() === "native") return renderToImageNative(container, width, height, options);
|
|
543
|
-
const originalCanvases = Array.from(container.querySelectorAll("canvas"));
|
|
544
|
-
const clone = container.cloneNode(true);
|
|
545
|
-
const clonedCanvases = clone.querySelectorAll("canvas");
|
|
546
|
-
const canvasScale = options?.canvasScale ?? 1;
|
|
547
|
-
const canvasStart = performance.now();
|
|
548
|
-
const encodedResults = await encodeCanvasesInParallel(originalCanvases, canvasScale);
|
|
549
|
-
for (let i = 0; i < originalCanvases.length; i++) {
|
|
550
|
-
const srcCanvas = originalCanvases[i];
|
|
551
|
-
const dstCanvas = clonedCanvases[i];
|
|
552
|
-
const encoded = encodedResults.find((r) => r.canvas === srcCanvas);
|
|
553
|
-
if (!srcCanvas || !dstCanvas || !encoded) continue;
|
|
554
|
-
try {
|
|
555
|
-
const img = document.createElement("img");
|
|
556
|
-
img.src = encoded.dataUrl;
|
|
557
|
-
img.width = srcCanvas.width;
|
|
558
|
-
img.height = srcCanvas.height;
|
|
559
|
-
const style = dstCanvas.getAttribute("style");
|
|
560
|
-
if (style) img.setAttribute("style", style);
|
|
561
|
-
dstCanvas.parentNode?.replaceChild(img, dstCanvas);
|
|
562
|
-
} catch {}
|
|
563
|
-
}
|
|
564
|
-
defaultProfiler.addTime("canvasEncode", performance.now() - canvasStart);
|
|
565
|
-
const inlineStart = performance.now();
|
|
566
|
-
await inlineImages(clone);
|
|
567
|
-
defaultProfiler.addTime("inline", performance.now() - inlineStart);
|
|
568
|
-
const { dataUri } = await serializeToSvgDataUri(clone, width, height);
|
|
569
|
-
return loadImageFromDataUri(dataUri);
|
|
570
|
-
}
|
|
571
|
-
/**
|
|
572
|
-
* Load an image from a data URI. Returns a Promise that resolves when loaded.
|
|
573
|
-
*/
|
|
574
|
-
function loadImageFromDataUri(dataUri) {
|
|
575
|
-
const img = new Image();
|
|
576
|
-
const imageLoadStart = performance.now();
|
|
577
|
-
return new Promise((resolve, reject) => {
|
|
578
|
-
img.onload = () => {
|
|
579
|
-
defaultProfiler.addTime("imageLoad", performance.now() - imageLoadStart);
|
|
580
|
-
resolve(img);
|
|
581
|
-
};
|
|
582
|
-
img.onerror = reject;
|
|
583
|
-
img.src = dataUri;
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
/**
|
|
587
167
|
* Captures a frame from an already-seeked render clone.
|
|
588
168
|
* Used internally by captureBatch for efficiency (reuses one clone across all captures).
|
|
589
169
|
*
|
|
@@ -606,48 +186,58 @@ async function captureFromClone(renderClone, renderContainer, options = {}) {
|
|
|
606
186
|
const ctx = canvas.getContext("2d");
|
|
607
187
|
if (!ctx) throw new Error("Failed to get canvas 2d context");
|
|
608
188
|
const timeMs = renderClone.currentTimeMs;
|
|
189
|
+
await new FrameController(renderClone).renderFrame(timeMs, { waitForLitUpdate: false });
|
|
609
190
|
if (contentReadyMode === "blocking") {
|
|
610
191
|
const result = await waitForVideoContent(renderClone, timeMs, blockingTimeoutMs);
|
|
611
192
|
if (!result.ready) throw new ContentNotReadyError(timeMs, blockingTimeoutMs, result.blankVideos);
|
|
612
193
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
194
|
+
const renderContext = new RenderContext();
|
|
195
|
+
try {
|
|
196
|
+
let image;
|
|
197
|
+
if (getEffectiveRenderMode() === "native") {
|
|
198
|
+
renderContainer.style.cssText = `
|
|
199
|
+
position: fixed;
|
|
200
|
+
left: 0;
|
|
201
|
+
top: 0;
|
|
202
|
+
width: ${width}px;
|
|
203
|
+
height: ${height}px;
|
|
204
|
+
pointer-events: none;
|
|
205
|
+
overflow: hidden;
|
|
206
|
+
`;
|
|
207
|
+
image = await renderToImageNative(renderContainer, width, height, { skipDprScaling: true });
|
|
208
|
+
} else {
|
|
209
|
+
const t0 = performance.now();
|
|
210
|
+
const { container, syncState } = buildCloneStructure(renderClone, timeMs);
|
|
211
|
+
const buildTime = performance.now() - t0;
|
|
212
|
+
const bgSource = originalTimegroup ?? renderClone;
|
|
213
|
+
const previewContainer = createPreviewContainer({
|
|
214
|
+
width,
|
|
215
|
+
height,
|
|
216
|
+
background: getComputedStyle(bgSource).background || "#000"
|
|
217
|
+
});
|
|
218
|
+
const t1 = performance.now();
|
|
219
|
+
const styleEl = document.createElement("style");
|
|
220
|
+
styleEl.textContent = collectDocumentStyles();
|
|
221
|
+
const stylesTime = performance.now() - t1;
|
|
222
|
+
previewContainer.appendChild(styleEl);
|
|
223
|
+
previewContainer.appendChild(container);
|
|
224
|
+
overrideRootCloneStyles(syncState, true);
|
|
225
|
+
const t2 = performance.now();
|
|
226
|
+
image = await renderToImage(previewContainer, width, height, {
|
|
227
|
+
canvasScale: scale,
|
|
228
|
+
renderContext,
|
|
229
|
+
sourceMap: syncState.canvasSourceMap
|
|
230
|
+
});
|
|
231
|
+
const renderTime = performance.now() - t2;
|
|
232
|
+
logger.debug(`[captureFromClone] build=${buildTime.toFixed(0)}ms, styles=${stylesTime.toFixed(0)}ms, render=${renderTime.toFixed(0)}ms (canvasScale=${scale})`);
|
|
233
|
+
}
|
|
234
|
+
const srcWidth = image.width;
|
|
235
|
+
const srcHeight = image.height;
|
|
236
|
+
ctx.drawImage(image, 0, 0, srcWidth, srcHeight, 0, 0, canvas.width, canvas.height);
|
|
237
|
+
return canvas;
|
|
238
|
+
} finally {
|
|
239
|
+
renderContext.dispose();
|
|
646
240
|
}
|
|
647
|
-
const srcWidth = image.width;
|
|
648
|
-
const srcHeight = image.height;
|
|
649
|
-
ctx.drawImage(image, 0, 0, srcWidth, srcHeight, 0, 0, canvas.width, canvas.height);
|
|
650
|
-
return canvas;
|
|
651
241
|
}
|
|
652
242
|
/**
|
|
653
243
|
* Captures a single frame from a timegroup at a specific time.
|
|
@@ -745,6 +335,9 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
|
|
|
745
335
|
overrideRootCloneStyles(syncState);
|
|
746
336
|
let rendering = false;
|
|
747
337
|
let lastTimeMs = -1;
|
|
338
|
+
let disposed = false;
|
|
339
|
+
const renderContext = new RenderContext();
|
|
340
|
+
const frameController = new FrameController(timegroup);
|
|
748
341
|
let hasLoggedScale = false;
|
|
749
342
|
let pendingResolutionScale = null;
|
|
750
343
|
/**
|
|
@@ -779,7 +372,7 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
|
|
|
779
372
|
};
|
|
780
373
|
const getResolutionScale = () => pendingResolutionScale ?? currentResolutionScale;
|
|
781
374
|
const refresh = async () => {
|
|
782
|
-
if (rendering) return;
|
|
375
|
+
if (rendering || disposed) return;
|
|
783
376
|
const sourceTimeMs = timegroup.currentTimeMs ?? 0;
|
|
784
377
|
const userTimeMs = timegroup.userTimeMs ?? 0;
|
|
785
378
|
if (Math.abs(sourceTimeMs - userTimeMs) > TIME_EPSILON_MS) return;
|
|
@@ -790,14 +383,21 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
|
|
|
790
383
|
if (!hasLoggedScale) {
|
|
791
384
|
hasLoggedScale = true;
|
|
792
385
|
const mode = getEffectiveRenderMode();
|
|
793
|
-
|
|
386
|
+
logger.debug(`[renderTimegroupToCanvas] Resolution scale: ${currentResolutionScale} (${width}x${height} → ${renderWidth}x${renderHeight}), canvas buffer: ${canvas.width}x${canvas.height}, CSS size: ${canvas.style.width}x${canvas.style.height}, renderMode: ${mode}`);
|
|
794
387
|
}
|
|
795
388
|
try {
|
|
389
|
+
await frameController.renderFrame(userTimeMs);
|
|
796
390
|
syncStyles(syncState, toAbsoluteTime(timegroup, userTimeMs));
|
|
797
391
|
overrideRootCloneStyles(syncState);
|
|
392
|
+
const removedNodes = removeHiddenNodesForSerialization(syncState);
|
|
798
393
|
const t0 = performance.now();
|
|
799
|
-
const image = await renderToImage(previewContainer, renderWidth, renderHeight, {
|
|
394
|
+
const image = await renderToImage(previewContainer, renderWidth, renderHeight, {
|
|
395
|
+
canvasScale: currentResolutionScale,
|
|
396
|
+
renderContext,
|
|
397
|
+
sourceMap: syncState.canvasSourceMap
|
|
398
|
+
});
|
|
800
399
|
const renderTime = performance.now() - t0;
|
|
400
|
+
restoreHiddenNodes(removedNodes);
|
|
801
401
|
const targetWidth = Math.floor(renderWidth * scale * dpr);
|
|
802
402
|
const targetHeight = Math.floor(renderHeight * scale * dpr);
|
|
803
403
|
if (canvas.width !== targetWidth || canvas.height !== targetHeight) {
|
|
@@ -809,14 +409,23 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
|
|
|
809
409
|
ctx.drawImage(image, 0, 0);
|
|
810
410
|
ctx.restore();
|
|
811
411
|
defaultProfiler.incrementRenderCount();
|
|
812
|
-
if (defaultProfiler.shouldLogByFrameCount(60))
|
|
412
|
+
if (defaultProfiler.shouldLogByFrameCount(60)) logger.debug(`[renderTimegroupToCanvas] Frame render: ${renderTime.toFixed(1)}ms (resolutionScale=${currentResolutionScale}, image=${image.width}x${image.height})`);
|
|
813
413
|
updateDebugLabel(debugLabel, renderWidth, renderHeight, currentResolutionScale);
|
|
814
414
|
} catch (e) {
|
|
815
|
-
|
|
415
|
+
logger.error("Canvas preview render failed:", e);
|
|
816
416
|
} finally {
|
|
817
417
|
rendering = false;
|
|
818
418
|
}
|
|
819
419
|
};
|
|
420
|
+
/**
|
|
421
|
+
* Dispose the preview and release resources.
|
|
422
|
+
*/
|
|
423
|
+
const dispose = () => {
|
|
424
|
+
if (disposed) return;
|
|
425
|
+
disposed = true;
|
|
426
|
+
frameController.abort();
|
|
427
|
+
renderContext.dispose();
|
|
428
|
+
};
|
|
820
429
|
refresh();
|
|
821
430
|
return {
|
|
822
431
|
container: wrapperContainer,
|
|
@@ -824,7 +433,8 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
|
|
|
824
433
|
refresh,
|
|
825
434
|
syncState,
|
|
826
435
|
setResolutionScale,
|
|
827
|
-
getResolutionScale
|
|
436
|
+
getResolutionScale,
|
|
437
|
+
dispose
|
|
828
438
|
};
|
|
829
439
|
}
|
|
830
440
|
|