@editframe/elements 0.33.0-beta → 0.34.6-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 +100 -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,28 @@ 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
|
-
*
|
|
28
|
+
* Module-level state for render operations.
|
|
29
|
+
* Note: xmlSerializer is lazy-initialized for Node.js compatibility
|
|
38
30
|
*/
|
|
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;
|
|
31
|
+
const renderState = {
|
|
32
|
+
inlineImageCache: /* @__PURE__ */ new Map(),
|
|
33
|
+
layoutInitializedCanvases: /* @__PURE__ */ new WeakSet(),
|
|
34
|
+
xmlSerializer: null,
|
|
35
|
+
textEncoder: new TextEncoder(),
|
|
36
|
+
metrics: {
|
|
37
|
+
inlineImageCacheHits: 0,
|
|
38
|
+
inlineImageCacheMisses: 0,
|
|
39
|
+
inlineImageCacheEvictions: 0
|
|
95
40
|
}
|
|
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
|
-
}
|
|
41
|
+
};
|
|
147
42
|
/**
|
|
148
|
-
*
|
|
149
|
-
* Avoids the overhead of converting to binary string first.
|
|
150
|
-
* Uses lookup table for optimal performance.
|
|
43
|
+
* Reset cache metrics to zero.
|
|
151
44
|
*/
|
|
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;
|
|
45
|
+
function resetCacheMetrics() {
|
|
46
|
+
renderState.metrics.inlineImageCacheHits = 0;
|
|
47
|
+
renderState.metrics.inlineImageCacheMisses = 0;
|
|
48
|
+
renderState.metrics.inlineImageCacheEvictions = 0;
|
|
180
49
|
}
|
|
181
50
|
/**
|
|
182
51
|
* Reset all module state including profiling counters, caches, and logging flags.
|
|
@@ -184,22 +53,8 @@ function encodeBase64Fast(bytes) {
|
|
|
184
53
|
*/
|
|
185
54
|
function resetRenderState() {
|
|
186
55
|
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;
|
|
56
|
+
clearInlineImageCache();
|
|
57
|
+
resetCacheMetrics();
|
|
203
58
|
}
|
|
204
59
|
/**
|
|
205
60
|
* Create a debug label for showing render info.
|
|
@@ -234,135 +89,12 @@ function updateDebugLabel(label, renderWidth, renderHeight, resolutionScale) {
|
|
|
234
89
|
label.textContent = `Render: ${renderWidth}x${renderHeight} (${Math.round(resolutionScale * 100)}%)`;
|
|
235
90
|
}
|
|
236
91
|
/**
|
|
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
92
|
* Wait for next animation frame (allows browser to complete layout)
|
|
349
93
|
*/
|
|
350
94
|
function waitForFrame() {
|
|
351
95
|
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
352
96
|
}
|
|
353
97
|
/**
|
|
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
98
|
* Check if a canvas has any rendered content (not all transparent/uninitialized).
|
|
367
99
|
* Returns true if there's ANY non-transparent pixel.
|
|
368
100
|
*/
|
|
@@ -433,157 +165,6 @@ async function waitForVideoContent(timegroup, timeMs, maxWaitMs) {
|
|
|
433
165
|
};
|
|
434
166
|
}
|
|
435
167
|
/**
|
|
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
168
|
* Captures a frame from an already-seeked render clone.
|
|
588
169
|
* Used internally by captureBatch for efficiency (reuses one clone across all captures).
|
|
589
170
|
*
|
|
@@ -606,48 +187,58 @@ async function captureFromClone(renderClone, renderContainer, options = {}) {
|
|
|
606
187
|
const ctx = canvas.getContext("2d");
|
|
607
188
|
if (!ctx) throw new Error("Failed to get canvas 2d context");
|
|
608
189
|
const timeMs = renderClone.currentTimeMs;
|
|
190
|
+
await new FrameController(renderClone).renderFrame(timeMs, { waitForLitUpdate: false });
|
|
609
191
|
if (contentReadyMode === "blocking") {
|
|
610
192
|
const result = await waitForVideoContent(renderClone, timeMs, blockingTimeoutMs);
|
|
611
193
|
if (!result.ready) throw new ContentNotReadyError(timeMs, blockingTimeoutMs, result.blankVideos);
|
|
612
194
|
}
|
|
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
|
-
|
|
195
|
+
const renderContext = new RenderContext();
|
|
196
|
+
try {
|
|
197
|
+
let image;
|
|
198
|
+
if (getEffectiveRenderMode() === "native") {
|
|
199
|
+
renderContainer.style.cssText = `
|
|
200
|
+
position: fixed;
|
|
201
|
+
left: 0;
|
|
202
|
+
top: 0;
|
|
203
|
+
width: ${width}px;
|
|
204
|
+
height: ${height}px;
|
|
205
|
+
pointer-events: none;
|
|
206
|
+
overflow: hidden;
|
|
207
|
+
`;
|
|
208
|
+
image = await renderToImageNative(renderContainer, width, height, { skipDprScaling: true });
|
|
209
|
+
} else {
|
|
210
|
+
const t0 = performance.now();
|
|
211
|
+
const { container, syncState } = buildCloneStructure(renderClone, timeMs);
|
|
212
|
+
const buildTime = performance.now() - t0;
|
|
213
|
+
const bgSource = originalTimegroup ?? renderClone;
|
|
214
|
+
const previewContainer = createPreviewContainer({
|
|
215
|
+
width,
|
|
216
|
+
height,
|
|
217
|
+
background: getComputedStyle(bgSource).background || "#000"
|
|
218
|
+
});
|
|
219
|
+
const t1 = performance.now();
|
|
220
|
+
const styleEl = document.createElement("style");
|
|
221
|
+
styleEl.textContent = collectDocumentStyles();
|
|
222
|
+
const stylesTime = performance.now() - t1;
|
|
223
|
+
previewContainer.appendChild(styleEl);
|
|
224
|
+
previewContainer.appendChild(container);
|
|
225
|
+
overrideRootCloneStyles(syncState, true);
|
|
226
|
+
const t2 = performance.now();
|
|
227
|
+
image = await renderToImage(previewContainer, width, height, {
|
|
228
|
+
canvasScale: scale,
|
|
229
|
+
renderContext,
|
|
230
|
+
sourceMap: syncState.canvasSourceMap
|
|
231
|
+
});
|
|
232
|
+
const renderTime = performance.now() - t2;
|
|
233
|
+
logger.debug(`[captureFromClone] build=${buildTime.toFixed(0)}ms, styles=${stylesTime.toFixed(0)}ms, render=${renderTime.toFixed(0)}ms (canvasScale=${scale})`);
|
|
234
|
+
}
|
|
235
|
+
const srcWidth = image.width;
|
|
236
|
+
const srcHeight = image.height;
|
|
237
|
+
ctx.drawImage(image, 0, 0, srcWidth, srcHeight, 0, 0, canvas.width, canvas.height);
|
|
238
|
+
return canvas;
|
|
239
|
+
} finally {
|
|
240
|
+
renderContext.dispose();
|
|
646
241
|
}
|
|
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
242
|
}
|
|
652
243
|
/**
|
|
653
244
|
* Captures a single frame from a timegroup at a specific time.
|
|
@@ -745,6 +336,9 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
|
|
|
745
336
|
overrideRootCloneStyles(syncState);
|
|
746
337
|
let rendering = false;
|
|
747
338
|
let lastTimeMs = -1;
|
|
339
|
+
let disposed = false;
|
|
340
|
+
const renderContext = new RenderContext();
|
|
341
|
+
const frameController = new FrameController(timegroup);
|
|
748
342
|
let hasLoggedScale = false;
|
|
749
343
|
let pendingResolutionScale = null;
|
|
750
344
|
/**
|
|
@@ -779,7 +373,7 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
|
|
|
779
373
|
};
|
|
780
374
|
const getResolutionScale = () => pendingResolutionScale ?? currentResolutionScale;
|
|
781
375
|
const refresh = async () => {
|
|
782
|
-
if (rendering) return;
|
|
376
|
+
if (rendering || disposed) return;
|
|
783
377
|
const sourceTimeMs = timegroup.currentTimeMs ?? 0;
|
|
784
378
|
const userTimeMs = timegroup.userTimeMs ?? 0;
|
|
785
379
|
if (Math.abs(sourceTimeMs - userTimeMs) > TIME_EPSILON_MS) return;
|
|
@@ -790,14 +384,21 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
|
|
|
790
384
|
if (!hasLoggedScale) {
|
|
791
385
|
hasLoggedScale = true;
|
|
792
386
|
const mode = getEffectiveRenderMode();
|
|
793
|
-
|
|
387
|
+
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
388
|
}
|
|
795
389
|
try {
|
|
390
|
+
await frameController.renderFrame(userTimeMs);
|
|
796
391
|
syncStyles(syncState, toAbsoluteTime(timegroup, userTimeMs));
|
|
797
392
|
overrideRootCloneStyles(syncState);
|
|
393
|
+
const removedNodes = removeHiddenNodesForSerialization(syncState);
|
|
798
394
|
const t0 = performance.now();
|
|
799
|
-
const image = await renderToImage(previewContainer, renderWidth, renderHeight, {
|
|
395
|
+
const image = await renderToImage(previewContainer, renderWidth, renderHeight, {
|
|
396
|
+
canvasScale: currentResolutionScale,
|
|
397
|
+
renderContext,
|
|
398
|
+
sourceMap: syncState.canvasSourceMap
|
|
399
|
+
});
|
|
800
400
|
const renderTime = performance.now() - t0;
|
|
401
|
+
restoreHiddenNodes(removedNodes);
|
|
801
402
|
const targetWidth = Math.floor(renderWidth * scale * dpr);
|
|
802
403
|
const targetHeight = Math.floor(renderHeight * scale * dpr);
|
|
803
404
|
if (canvas.width !== targetWidth || canvas.height !== targetHeight) {
|
|
@@ -809,14 +410,23 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
|
|
|
809
410
|
ctx.drawImage(image, 0, 0);
|
|
810
411
|
ctx.restore();
|
|
811
412
|
defaultProfiler.incrementRenderCount();
|
|
812
|
-
if (defaultProfiler.shouldLogByFrameCount(60))
|
|
413
|
+
if (defaultProfiler.shouldLogByFrameCount(60)) logger.debug(`[renderTimegroupToCanvas] Frame render: ${renderTime.toFixed(1)}ms (resolutionScale=${currentResolutionScale}, image=${image.width}x${image.height})`);
|
|
813
414
|
updateDebugLabel(debugLabel, renderWidth, renderHeight, currentResolutionScale);
|
|
814
415
|
} catch (e) {
|
|
815
|
-
|
|
416
|
+
logger.error("Canvas preview render failed:", e);
|
|
816
417
|
} finally {
|
|
817
418
|
rendering = false;
|
|
818
419
|
}
|
|
819
420
|
};
|
|
421
|
+
/**
|
|
422
|
+
* Dispose the preview and release resources.
|
|
423
|
+
*/
|
|
424
|
+
const dispose = () => {
|
|
425
|
+
if (disposed) return;
|
|
426
|
+
disposed = true;
|
|
427
|
+
frameController.abort();
|
|
428
|
+
renderContext.dispose();
|
|
429
|
+
};
|
|
820
430
|
refresh();
|
|
821
431
|
return {
|
|
822
432
|
container: wrapperContainer,
|
|
@@ -824,7 +434,8 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
|
|
|
824
434
|
refresh,
|
|
825
435
|
syncState,
|
|
826
436
|
setResolutionScale,
|
|
827
|
-
getResolutionScale
|
|
437
|
+
getResolutionScale,
|
|
438
|
+
dispose
|
|
828
439
|
};
|
|
829
440
|
}
|
|
830
441
|
|