@editframe/elements 0.30.2-beta.0 → 0.31.0-beta.1
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.d.ts +5 -0
- package/dist/EF_FRAMEGEN.js +20 -4
- package/dist/EF_FRAMEGEN.js.map +1 -1
- package/dist/EF_INTERACTIVE.js.map +1 -1
- package/dist/_virtual/rolldown_runtime.js +27 -0
- package/dist/canvas/EFCanvas.d.ts +311 -0
- package/dist/canvas/EFCanvas.js +1089 -0
- package/dist/canvas/EFCanvas.js.map +1 -0
- package/dist/canvas/EFCanvasItem.d.ts +55 -0
- package/dist/canvas/EFCanvasItem.js +72 -0
- package/dist/canvas/EFCanvasItem.js.map +1 -0
- package/dist/canvas/api/CanvasAPI.d.ts +115 -0
- package/dist/canvas/api/CanvasAPI.js +182 -0
- package/dist/canvas/api/CanvasAPI.js.map +1 -0
- package/dist/canvas/api/types.d.ts +42 -0
- package/dist/canvas/coordinateTransform.js +90 -0
- package/dist/canvas/coordinateTransform.js.map +1 -0
- package/dist/canvas/getElementBounds.js +40 -0
- package/dist/canvas/getElementBounds.js.map +1 -0
- package/dist/canvas/overlays/SelectionOverlay.js +265 -0
- package/dist/canvas/overlays/SelectionOverlay.js.map +1 -0
- package/dist/canvas/overlays/overlayState.js +153 -0
- package/dist/canvas/overlays/overlayState.js.map +1 -0
- package/dist/canvas/selection/SelectionController.js +105 -0
- package/dist/canvas/selection/SelectionController.js.map +1 -0
- package/dist/canvas/selection/SelectionModel.d.ts +98 -0
- package/dist/canvas/selection/SelectionModel.js +229 -0
- package/dist/canvas/selection/SelectionModel.js.map +1 -0
- package/dist/canvas/selection/selectionContext.d.ts +31 -0
- package/dist/canvas/selection/selectionContext.js +12 -0
- package/dist/canvas/selection/selectionContext.js.map +1 -0
- package/dist/elements/ContainerInfo.d.ts +29 -0
- package/dist/elements/ContainerInfo.js +30 -0
- package/dist/elements/ContainerInfo.js.map +1 -0
- package/dist/elements/EFAudio.d.ts +13 -3
- package/dist/elements/EFAudio.js +64 -10
- package/dist/elements/EFAudio.js.map +1 -1
- package/dist/elements/EFCaptions.d.ts +18 -16
- package/dist/elements/EFCaptions.js +110 -19
- package/dist/elements/EFCaptions.js.map +1 -1
- package/dist/elements/EFImage.d.ts +16 -6
- package/dist/elements/EFImage.js +79 -9
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +51 -4
- package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/AssetMediaEngine.js +125 -52
- package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/BaseMediaEngine.js +24 -6
- package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/JitMediaEngine.js +12 -8
- package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +46 -7
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +98 -73
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +28 -5
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +18 -6
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +8 -2
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +31 -6
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +28 -5
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js.map +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +97 -72
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js.map +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/BufferUtils.js +1 -1
- package/dist/elements/EFMedia/shared/BufferUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +25 -14
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +47 -16
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +37 -19
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +65 -21
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +8 -3
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js +32 -9
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +33 -10
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +23 -8
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js +34 -10
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +31 -8
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +31 -114
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +44 -8
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js.map +1 -1
- package/dist/elements/EFMedia.d.ts +18 -7
- package/dist/elements/EFMedia.js +23 -3
- package/dist/elements/EFMedia.js.map +1 -1
- package/dist/elements/EFPanZoom.d.ts +96 -0
- package/dist/elements/EFPanZoom.js +290 -0
- package/dist/elements/EFPanZoom.js.map +1 -0
- package/dist/elements/EFSourceMixin.js +7 -6
- package/dist/elements/EFSourceMixin.js.map +1 -1
- package/dist/elements/EFSurface.d.ts +6 -6
- package/dist/elements/EFSurface.js +7 -2
- package/dist/elements/EFSurface.js.map +1 -1
- package/dist/elements/EFTemporal.d.ts +2 -1
- package/dist/elements/EFTemporal.js +192 -71
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFText.d.ts +5 -4
- package/dist/elements/EFText.js +102 -13
- package/dist/elements/EFText.js.map +1 -1
- package/dist/elements/EFTextSegment.d.ts +32 -6
- package/dist/elements/EFTextSegment.js +53 -15
- package/dist/elements/EFTextSegment.js.map +1 -1
- package/dist/elements/EFThumbnailStrip.d.ts +129 -56
- package/dist/elements/EFThumbnailStrip.js +605 -359
- package/dist/elements/EFThumbnailStrip.js.map +1 -1
- package/dist/elements/EFTimegroup.d.ts +233 -25
- package/dist/elements/EFTimegroup.js +865 -144
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.d.ts +42 -5
- package/dist/elements/EFVideo.js +165 -11
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/elements/EFWaveform.d.ts +6 -6
- package/dist/elements/EFWaveform.js +2 -1
- package/dist/elements/EFWaveform.js.map +1 -1
- package/dist/elements/ElementPositionInfo.d.ts +35 -0
- package/dist/elements/ElementPositionInfo.js +49 -0
- package/dist/elements/ElementPositionInfo.js.map +1 -0
- package/dist/elements/FetchMixin.js +16 -1
- package/dist/elements/FetchMixin.js.map +1 -1
- package/dist/elements/SessionThumbnailCache.js +154 -0
- package/dist/elements/SessionThumbnailCache.js.map +1 -0
- package/dist/elements/TargetController.js +3 -1
- package/dist/elements/TargetController.js.map +1 -1
- package/dist/elements/TimegroupController.js +9 -3
- package/dist/elements/TimegroupController.js.map +1 -1
- package/dist/elements/findRootTemporal.js +30 -0
- package/dist/elements/findRootTemporal.js.map +1 -0
- package/dist/elements/renderTemporalAudio.js +18 -5
- package/dist/elements/renderTemporalAudio.js.map +1 -1
- package/dist/elements/updateAnimations.js +171 -28
- package/dist/elements/updateAnimations.js.map +1 -1
- package/dist/getRenderInfo.d.ts +2 -2
- package/dist/gui/ContextMixin.js +4 -2
- package/dist/gui/ContextMixin.js.map +1 -1
- package/dist/gui/Controllable.js +74 -1
- package/dist/gui/Controllable.js.map +1 -1
- package/dist/gui/EFActiveRootTemporal.d.ts +50 -0
- package/dist/gui/EFActiveRootTemporal.js +94 -0
- package/dist/gui/EFActiveRootTemporal.js.map +1 -0
- package/dist/gui/EFConfiguration.d.ts +7 -1
- package/dist/gui/EFConfiguration.js.map +1 -1
- package/dist/gui/EFControls.d.ts +2 -2
- package/dist/gui/EFControls.js +109 -13
- package/dist/gui/EFControls.js.map +1 -1
- package/dist/gui/EFDial.d.ts +4 -4
- package/dist/gui/EFFilmstrip.d.ts +11 -214
- package/dist/gui/EFFilmstrip.js +53 -1152
- package/dist/gui/EFFilmstrip.js.map +1 -1
- package/dist/gui/EFFitScale.d.ts +3 -3
- package/dist/gui/EFFitScale.js +39 -12
- package/dist/gui/EFFitScale.js.map +1 -1
- package/dist/gui/EFFocusOverlay.d.ts +4 -4
- package/dist/gui/EFOverlayItem.d.ts +48 -0
- package/dist/gui/EFOverlayItem.js +97 -0
- package/dist/gui/EFOverlayItem.js.map +1 -0
- package/dist/gui/EFOverlayLayer.d.ts +70 -0
- package/dist/gui/EFOverlayLayer.js +104 -0
- package/dist/gui/EFOverlayLayer.js.map +1 -0
- package/dist/gui/EFPause.d.ts +4 -4
- package/dist/gui/EFPlay.d.ts +4 -4
- package/dist/gui/EFResizableBox.d.ts +12 -16
- package/dist/gui/EFResizableBox.js +109 -451
- package/dist/gui/EFResizableBox.js.map +1 -1
- package/dist/gui/EFScrubber.d.ts +30 -5
- package/dist/gui/EFScrubber.js +224 -31
- package/dist/gui/EFScrubber.js.map +1 -1
- package/dist/gui/EFTimeDisplay.d.ts +4 -4
- package/dist/gui/EFTimeDisplay.js +4 -1
- package/dist/gui/EFTimeDisplay.js.map +1 -1
- package/dist/gui/EFTimelineRuler.d.ts +71 -0
- package/dist/gui/EFTimelineRuler.js +320 -0
- package/dist/gui/EFTimelineRuler.js.map +1 -0
- package/dist/gui/EFToggleLoop.d.ts +4 -4
- package/dist/gui/EFTogglePlay.d.ts +4 -4
- package/dist/gui/EFTransformHandles.d.ts +91 -0
- package/dist/gui/EFTransformHandles.js +393 -0
- package/dist/gui/EFTransformHandles.js.map +1 -0
- package/dist/gui/EFWorkbench.d.ts +178 -0
- package/dist/gui/EFWorkbench.js +2067 -22
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/FitScaleHelpers.d.ts +31 -0
- package/dist/gui/FitScaleHelpers.js +41 -0
- package/dist/gui/FitScaleHelpers.js.map +1 -0
- package/dist/gui/PlaybackController.d.ts +2 -1
- package/dist/gui/PlaybackController.js +46 -15
- 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/hierarchy/EFHierarchy.d.ts +65 -0
- package/dist/gui/hierarchy/EFHierarchy.js +338 -0
- package/dist/gui/hierarchy/EFHierarchy.js.map +1 -0
- package/dist/gui/hierarchy/EFHierarchyItem.d.ts +118 -0
- package/dist/gui/hierarchy/EFHierarchyItem.js +551 -0
- package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -0
- package/dist/gui/hierarchy/hierarchyContext.d.ts +38 -0
- package/dist/gui/hierarchy/hierarchyContext.js +8 -0
- package/dist/gui/hierarchy/hierarchyContext.js.map +1 -0
- package/dist/gui/icons.js +34 -0
- package/dist/gui/icons.js.map +1 -0
- package/dist/gui/panZoomTransformContext.js +12 -0
- package/dist/gui/panZoomTransformContext.js.map +1 -0
- package/dist/gui/previewSettingsContext.js +12 -0
- package/dist/gui/previewSettingsContext.js.map +1 -0
- package/dist/gui/timeline/EFTimeline.d.ts +270 -0
- package/dist/gui/timeline/EFTimeline.js +1369 -0
- package/dist/gui/timeline/EFTimeline.js.map +1 -0
- package/dist/gui/timeline/EFTimelineRow.js +374 -0
- package/dist/gui/timeline/EFTimelineRow.js.map +1 -0
- package/dist/gui/timeline/TrimHandles.d.ts +36 -0
- package/dist/gui/timeline/TrimHandles.js +204 -0
- package/dist/gui/timeline/TrimHandles.js.map +1 -0
- package/dist/gui/timeline/flattenHierarchy.js +31 -0
- package/dist/gui/timeline/flattenHierarchy.js.map +1 -0
- package/dist/gui/timeline/timelineStateContext.d.ts +26 -0
- package/dist/gui/timeline/timelineStateContext.js +42 -0
- package/dist/gui/timeline/timelineStateContext.js.map +1 -0
- package/dist/gui/timeline/tracks/AudioTrack.js +264 -0
- package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/CaptionsTrack.js +595 -0
- package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/HTMLTrack.js +19 -0
- package/dist/gui/timeline/tracks/HTMLTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/ImageTrack.js +53 -0
- package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/TextTrack.js +250 -0
- package/dist/gui/timeline/tracks/TextTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/TimegroupTrack.js +143 -0
- package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/TrackItem.js +269 -0
- package/dist/gui/timeline/tracks/TrackItem.js.map +1 -0
- package/dist/gui/timeline/tracks/VideoTrack.js +265 -0
- package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/WaveformTrack.js +19 -0
- package/dist/gui/timeline/tracks/WaveformTrack.js.map +1 -0
- package/dist/gui/timeline/tracks/ensureTrackItemInit.js +1 -0
- package/dist/gui/timeline/tracks/preloadTracks.js +9 -0
- package/dist/gui/timeline/tracks/renderTrackChildren.js +119 -0
- package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -0
- package/dist/gui/timeline/tracks/waveformUtils.js +80 -0
- package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -0
- package/dist/gui/transformCalculations.js +217 -0
- package/dist/gui/transformCalculations.js.map +1 -0
- package/dist/gui/transformUtils.d.ts +37 -0
- package/dist/gui/transformUtils.js +77 -0
- package/dist/gui/transformUtils.js.map +1 -0
- package/dist/gui/tree/EFTree.d.ts +59 -0
- package/dist/gui/tree/EFTree.js +174 -0
- package/dist/gui/tree/EFTree.js.map +1 -0
- package/dist/gui/tree/EFTreeItem.d.ts +38 -0
- package/dist/gui/tree/EFTreeItem.js +146 -0
- package/dist/gui/tree/EFTreeItem.js.map +1 -0
- package/dist/gui/tree/treeContext.d.ts +60 -0
- package/dist/gui/tree/treeContext.js +23 -0
- package/dist/gui/tree/treeContext.js.map +1 -0
- package/dist/index.d.ts +32 -8
- package/dist/index.js +30 -6
- package/dist/index.js.map +1 -1
- package/dist/node_modules/react/cjs/react-jsx-runtime.development.js +688 -0
- package/dist/node_modules/react/cjs/react-jsx-runtime.development.js.map +1 -0
- package/dist/node_modules/react/cjs/react.development.js +1521 -0
- package/dist/node_modules/react/cjs/react.development.js.map +1 -0
- package/dist/node_modules/react/index.js +13 -0
- package/dist/node_modules/react/index.js.map +1 -0
- package/dist/node_modules/react/jsx-runtime.js +13 -0
- package/dist/node_modules/react/jsx-runtime.js.map +1 -0
- package/dist/preview/AdaptiveResolutionTracker.js +228 -0
- package/dist/preview/AdaptiveResolutionTracker.js.map +1 -0
- package/dist/preview/RenderProfiler.js +135 -0
- package/dist/preview/RenderProfiler.js.map +1 -0
- package/dist/preview/previewSettings.js +131 -0
- package/dist/preview/previewSettings.js.map +1 -0
- package/dist/preview/previewTypes.js +64 -0
- package/dist/preview/previewTypes.js.map +1 -0
- package/dist/preview/renderTimegroupPreview.js +656 -0
- package/dist/preview/renderTimegroupPreview.js.map +1 -0
- package/dist/preview/renderTimegroupToCanvas.d.ts +37 -0
- package/dist/preview/renderTimegroupToCanvas.js +833 -0
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -0
- package/dist/preview/renderTimegroupToVideo.d.ts +39 -0
- package/dist/preview/renderTimegroupToVideo.js +274 -0
- package/dist/preview/renderTimegroupToVideo.js.map +1 -0
- package/dist/preview/renderers.js +16 -0
- package/dist/preview/renderers.js.map +1 -0
- package/dist/preview/statsTrackingStrategy.js +201 -0
- package/dist/preview/statsTrackingStrategy.js.map +1 -0
- package/dist/preview/thumbnailCacheSettings.js +52 -0
- package/dist/preview/thumbnailCacheSettings.js.map +1 -0
- package/dist/preview/workers/WorkerPool.js +178 -0
- package/dist/preview/workers/WorkerPool.js.map +1 -0
- package/dist/preview/workers/encoderWorkerInline.js +103 -0
- package/dist/preview/workers/encoderWorkerInline.js.map +1 -0
- package/dist/sandbox/PlaybackControls.js +10 -0
- package/dist/sandbox/PlaybackControls.js.map +1 -0
- package/dist/sandbox/ScenarioRunner.js +1 -0
- package/dist/sandbox/index.js +2 -0
- package/dist/style.css +71 -67
- package/dist/transcoding/types/index.d.ts +2 -1
- package/dist/transcoding/utils/UrlGenerator.d.ts +6 -1
- package/dist/transcoding/utils/UrlGenerator.js +12 -3
- package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
- package/dist/utils/LRUCache.js +1 -375
- package/dist/utils/LRUCache.js.map +1 -1
- package/dist/utils/frameTime.js +14 -0
- package/dist/utils/frameTime.js.map +1 -0
- package/package.json +3 -3
- package/test/profilingPlugin.ts +223 -0
- package/test/recordReplayProxyPlugin.js +22 -27
- package/test/thumbnail-performance-test.html +116 -0
- package/test/visualRegressionUtils.ts +286 -0
- package/types.json +1 -1
- package/dist/elements/TimegroupController.d.ts +0 -18
- package/dist/msToTimeCode.js +0 -17
- package/dist/msToTimeCode.js.map +0 -1
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
import { DEFAULT_BLOCKING_TIMEOUT_MS, DEFAULT_HEIGHT, DEFAULT_THUMBNAIL_SCALE, DEFAULT_WIDTH, JPEG_QUALITY_HIGH, JPEG_QUALITY_MEDIUM, createPreviewContainer, isVisibleAtTime } from "./previewTypes.js";
|
|
2
|
+
import { buildCloneStructure, collectDocumentStyles, overrideRootCloneStyles, syncStyles } from "./renderTimegroupPreview.js";
|
|
3
|
+
import { getEffectiveRenderMode } from "./renderers.js";
|
|
4
|
+
import { WorkerPool, encodeCanvasInWorker } from "./workers/WorkerPool.js";
|
|
5
|
+
import { getEncoderWorkerUrl } from "./workers/encoderWorkerInline.js";
|
|
6
|
+
import { defaultProfiler } from "./RenderProfiler.js";
|
|
7
|
+
|
|
8
|
+
//#region src/preview/renderTimegroupToCanvas.ts
|
|
9
|
+
/** Number of rows to sample when checking canvas content */
|
|
10
|
+
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
|
+
/**
|
|
16
|
+
* Error thrown when video content is not ready within the blocking timeout.
|
|
17
|
+
*/
|
|
18
|
+
var ContentNotReadyError = class extends Error {
|
|
19
|
+
constructor(timeMs, timeoutMs, blankVideos) {
|
|
20
|
+
super(`Video content not ready at ${timeMs}ms after ${timeoutMs}ms timeout. Blank videos: ${blankVideos.join(", ")}`);
|
|
21
|
+
this.timeMs = timeMs;
|
|
22
|
+
this.timeoutMs = timeoutMs;
|
|
23
|
+
this.blankVideos = blankVideos;
|
|
24
|
+
this.name = "ContentNotReadyError";
|
|
25
|
+
}
|
|
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
|
+
/**
|
|
36
|
+
* Get or create the worker pool for canvas encoding.
|
|
37
|
+
* Returns null if workers are not available.
|
|
38
|
+
*/
|
|
39
|
+
function getWorkerPool() {
|
|
40
|
+
if (_workerPool) return _workerPool;
|
|
41
|
+
if (typeof Worker === "undefined" || typeof OffscreenCanvas === "undefined" || typeof createImageBitmap === "undefined") {
|
|
42
|
+
if (!_workerPoolWarningLogged) {
|
|
43
|
+
_workerPoolWarningLogged = true;
|
|
44
|
+
console.warn("[renderTimegroupToCanvas] Web Workers or OffscreenCanvas not available, using main thread fallback");
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
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;
|
|
95
|
+
}
|
|
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
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Fast base64 encoding directly from Uint8Array.
|
|
149
|
+
* Avoids the overhead of converting to binary string first.
|
|
150
|
+
* Uses lookup table for optimal performance.
|
|
151
|
+
*/
|
|
152
|
+
function encodeBase64Fast(bytes) {
|
|
153
|
+
const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
154
|
+
let result = "";
|
|
155
|
+
let i = 0;
|
|
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;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Reset all module state including profiling counters, caches, and logging flags.
|
|
183
|
+
* Call at the start of export sessions to ensure clean state.
|
|
184
|
+
*/
|
|
185
|
+
function resetRenderState() {
|
|
186
|
+
defaultProfiler.reset();
|
|
187
|
+
_inlineImageCache.clear();
|
|
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;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Create a debug label for showing render info.
|
|
206
|
+
*/
|
|
207
|
+
function createDebugLabel() {
|
|
208
|
+
const debugLabel = document.createElement("div");
|
|
209
|
+
debugLabel.style.cssText = `
|
|
210
|
+
position: absolute;
|
|
211
|
+
top: -24px;
|
|
212
|
+
left: 0;
|
|
213
|
+
padding: 2px 8px;
|
|
214
|
+
font: bold 12px monospace;
|
|
215
|
+
background: rgba(0, 0, 0, 0.8);
|
|
216
|
+
border-radius: 3px;
|
|
217
|
+
white-space: nowrap;
|
|
218
|
+
z-index: 1000;
|
|
219
|
+
pointer-events: none;
|
|
220
|
+
`;
|
|
221
|
+
return debugLabel;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Update debug label with resolution info.
|
|
225
|
+
*/
|
|
226
|
+
function updateDebugLabel(label, renderWidth, renderHeight, resolutionScale) {
|
|
227
|
+
const scaleColors = {
|
|
228
|
+
1: "#00ff00",
|
|
229
|
+
.75: "#ffff00",
|
|
230
|
+
.5: "#ff8800",
|
|
231
|
+
.25: "#ff0000"
|
|
232
|
+
};
|
|
233
|
+
label.style.color = scaleColors[resolutionScale] || "#ffffff";
|
|
234
|
+
label.textContent = `Render: ${renderWidth}x${renderHeight} (${Math.round(resolutionScale * 100)}%)`;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
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
|
+
* Wait for next animation frame (allows browser to complete layout)
|
|
349
|
+
*/
|
|
350
|
+
function waitForFrame() {
|
|
351
|
+
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
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
|
+
* Check if a canvas has any rendered content (not all transparent/uninitialized).
|
|
367
|
+
* Returns true if there's ANY non-transparent pixel.
|
|
368
|
+
*/
|
|
369
|
+
function canvasHasContent(canvas) {
|
|
370
|
+
const ctx = canvas.getContext("2d");
|
|
371
|
+
if (!ctx) return false;
|
|
372
|
+
try {
|
|
373
|
+
const width = canvas.width;
|
|
374
|
+
const height = canvas.height;
|
|
375
|
+
if (width === 0 || height === 0) return false;
|
|
376
|
+
const stripY = Math.floor(height / 2);
|
|
377
|
+
const data = ctx.getImageData(0, stripY, width, CANVAS_SAMPLE_STRIP_HEIGHT).data;
|
|
378
|
+
for (let i = 3; i < data.length; i += 4) if (data[i] !== 0) return true;
|
|
379
|
+
return false;
|
|
380
|
+
} catch {
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Wait for video canvases within a timegroup to have content.
|
|
386
|
+
* Only checks videos that should be visible at the current time.
|
|
387
|
+
* Returns result with ready status and list of blank video names.
|
|
388
|
+
*/
|
|
389
|
+
async function waitForVideoContent(timegroup, timeMs, maxWaitMs) {
|
|
390
|
+
const startTime = performance.now();
|
|
391
|
+
const allVideos = timegroup.querySelectorAll("ef-video");
|
|
392
|
+
if (allVideos.length === 0) return {
|
|
393
|
+
ready: true,
|
|
394
|
+
blankVideos: []
|
|
395
|
+
};
|
|
396
|
+
const visibleVideos = Array.from(allVideos).filter((video) => {
|
|
397
|
+
if (!isVisibleAtTime(video, timeMs)) return false;
|
|
398
|
+
let parent = video.parentElement;
|
|
399
|
+
while (parent && parent !== timegroup) {
|
|
400
|
+
if (parent.tagName === "EF-TIMEGROUP" && !isVisibleAtTime(parent, timeMs)) return false;
|
|
401
|
+
parent = parent.parentElement;
|
|
402
|
+
}
|
|
403
|
+
return true;
|
|
404
|
+
});
|
|
405
|
+
if (visibleVideos.length === 0) return {
|
|
406
|
+
ready: true,
|
|
407
|
+
blankVideos: []
|
|
408
|
+
};
|
|
409
|
+
const getBlankVideoNames = () => visibleVideos.filter((video) => {
|
|
410
|
+
const shadowCanvas = video.shadowRoot?.querySelector("canvas");
|
|
411
|
+
return shadowCanvas && !canvasHasContent(shadowCanvas);
|
|
412
|
+
}).map((v) => v.src || v.id || "unnamed");
|
|
413
|
+
while (performance.now() - startTime < maxWaitMs) {
|
|
414
|
+
let allHaveContent = true;
|
|
415
|
+
for (const video of visibleVideos) {
|
|
416
|
+
const shadowCanvas = video.shadowRoot?.querySelector("canvas");
|
|
417
|
+
if (shadowCanvas && shadowCanvas.width > 0 && shadowCanvas.height > 0) {
|
|
418
|
+
if (!canvasHasContent(shadowCanvas)) {
|
|
419
|
+
allHaveContent = false;
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (allHaveContent) return {
|
|
425
|
+
ready: true,
|
|
426
|
+
blankVideos: []
|
|
427
|
+
};
|
|
428
|
+
await waitForFrame();
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
ready: false,
|
|
432
|
+
blankVideos: getBlankVideoNames()
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
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
|
+
* Captures a frame from an already-seeked render clone.
|
|
588
|
+
* Used internally by captureBatch for efficiency (reuses one clone across all captures).
|
|
589
|
+
*
|
|
590
|
+
* @param renderClone - A render clone that has already been seeked to the target time
|
|
591
|
+
* @param renderContainer - The container holding the render clone (from createRenderClone)
|
|
592
|
+
* @param options - Capture options
|
|
593
|
+
* @returns Canvas with the rendered frame
|
|
594
|
+
*/
|
|
595
|
+
async function captureFromClone(renderClone, renderContainer, options = {}) {
|
|
596
|
+
const { scale = DEFAULT_THUMBNAIL_SCALE, contentReadyMode = "immediate", blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS, originalTimegroup } = options;
|
|
597
|
+
const sourceForDimensions = originalTimegroup ?? renderClone;
|
|
598
|
+
const width = sourceForDimensions.offsetWidth || DEFAULT_WIDTH;
|
|
599
|
+
const height = sourceForDimensions.offsetHeight || DEFAULT_HEIGHT;
|
|
600
|
+
const dpr = window.devicePixelRatio || 1;
|
|
601
|
+
const canvas = document.createElement("canvas");
|
|
602
|
+
canvas.width = Math.floor(width * scale * dpr);
|
|
603
|
+
canvas.height = Math.floor(height * scale * dpr);
|
|
604
|
+
canvas.style.width = `${Math.floor(width * scale)}px`;
|
|
605
|
+
canvas.style.height = `${Math.floor(height * scale)}px`;
|
|
606
|
+
const ctx = canvas.getContext("2d");
|
|
607
|
+
if (!ctx) throw new Error("Failed to get canvas 2d context");
|
|
608
|
+
const timeMs = renderClone.currentTimeMs;
|
|
609
|
+
if (contentReadyMode === "blocking") {
|
|
610
|
+
const result = await waitForVideoContent(renderClone, timeMs, blockingTimeoutMs);
|
|
611
|
+
if (!result.ready) throw new ContentNotReadyError(timeMs, blockingTimeoutMs, result.blankVideos);
|
|
612
|
+
}
|
|
613
|
+
let image;
|
|
614
|
+
if (getEffectiveRenderMode() === "native") {
|
|
615
|
+
renderContainer.style.cssText = `
|
|
616
|
+
position: fixed;
|
|
617
|
+
left: 0;
|
|
618
|
+
top: 0;
|
|
619
|
+
width: ${width}px;
|
|
620
|
+
height: ${height}px;
|
|
621
|
+
pointer-events: none;
|
|
622
|
+
overflow: hidden;
|
|
623
|
+
`;
|
|
624
|
+
image = await renderToImageNative(renderContainer, width, height, { skipDprScaling: scale < 1 });
|
|
625
|
+
} else {
|
|
626
|
+
const t0 = performance.now();
|
|
627
|
+
const { container, syncState } = buildCloneStructure(renderClone, timeMs);
|
|
628
|
+
const buildTime = performance.now() - t0;
|
|
629
|
+
const bgSource = originalTimegroup ?? renderClone;
|
|
630
|
+
const previewContainer = createPreviewContainer({
|
|
631
|
+
width,
|
|
632
|
+
height,
|
|
633
|
+
background: getComputedStyle(bgSource).background || "#000"
|
|
634
|
+
});
|
|
635
|
+
const t1 = performance.now();
|
|
636
|
+
const styleEl = document.createElement("style");
|
|
637
|
+
styleEl.textContent = collectDocumentStyles();
|
|
638
|
+
const stylesTime = performance.now() - t1;
|
|
639
|
+
previewContainer.appendChild(styleEl);
|
|
640
|
+
previewContainer.appendChild(container);
|
|
641
|
+
overrideRootCloneStyles(syncState, true);
|
|
642
|
+
const t2 = performance.now();
|
|
643
|
+
image = await renderToImage(previewContainer, width, height, { canvasScale: scale });
|
|
644
|
+
const renderTime = performance.now() - t2;
|
|
645
|
+
console.log(`[captureFromClone] build=${buildTime.toFixed(0)}ms, styles=${stylesTime.toFixed(0)}ms, render=${renderTime.toFixed(0)}ms (canvasScale=${scale})`);
|
|
646
|
+
}
|
|
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
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Captures a single frame from a timegroup at a specific time.
|
|
654
|
+
*
|
|
655
|
+
* CLONE-TIMELINE ARCHITECTURE:
|
|
656
|
+
* Creates an independent render clone, seeks it to the target time, and captures.
|
|
657
|
+
* Prime-timeline is NEVER seeked - user can continue previewing/editing during capture.
|
|
658
|
+
*
|
|
659
|
+
* @param timegroup - The source timegroup
|
|
660
|
+
* @param options - Capture options including timeMs, scale, contentReadyMode
|
|
661
|
+
* @returns Canvas with the rendered frame
|
|
662
|
+
* @throws ContentNotReadyError if blocking mode times out waiting for video content
|
|
663
|
+
*/
|
|
664
|
+
async function captureTimegroupAtTime(timegroup, options) {
|
|
665
|
+
const { timeMs, scale = DEFAULT_THUMBNAIL_SCALE, contentReadyMode = "immediate", blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS } = options;
|
|
666
|
+
const { clone: renderClone, container: renderContainer, cleanup: cleanupRenderClone } = await timegroup.createRenderClone();
|
|
667
|
+
try {
|
|
668
|
+
await renderClone.seekForRender(timeMs);
|
|
669
|
+
return await captureFromClone(renderClone, renderContainer, {
|
|
670
|
+
scale,
|
|
671
|
+
contentReadyMode,
|
|
672
|
+
blockingTimeoutMs,
|
|
673
|
+
originalTimegroup: timegroup
|
|
674
|
+
});
|
|
675
|
+
} finally {
|
|
676
|
+
cleanupRenderClone();
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/** Epsilon for comparing time values (ms) - times within this are considered equal */
|
|
680
|
+
const TIME_EPSILON_MS = 1;
|
|
681
|
+
/** Default scale for preview rendering */
|
|
682
|
+
const DEFAULT_PREVIEW_SCALE = 1;
|
|
683
|
+
/** Default resolution scale (full resolution) */
|
|
684
|
+
const DEFAULT_RESOLUTION_SCALE = 1;
|
|
685
|
+
/**
|
|
686
|
+
* Convert relative time to absolute time for a timegroup.
|
|
687
|
+
* Nested timegroup children have ABSOLUTE startTimeMs values,
|
|
688
|
+
* so relative capture times must be converted for temporal culling.
|
|
689
|
+
*/
|
|
690
|
+
function toAbsoluteTime(timegroup, relativeTimeMs) {
|
|
691
|
+
return relativeTimeMs + (timegroup.startTimeMs ?? 0);
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Renders a timegroup preview to a canvas using SVG foreignObject.
|
|
695
|
+
*
|
|
696
|
+
* Optimized with:
|
|
697
|
+
* - Persistent clone structure (built once)
|
|
698
|
+
* - Temporal bucketing for time-based culling
|
|
699
|
+
* - Property split (static vs animated)
|
|
700
|
+
* - Parent index for O(1) visibility checks
|
|
701
|
+
* - Resolution scaling for performance (renders at lower resolution, CSS upscales)
|
|
702
|
+
*
|
|
703
|
+
* @param timegroup - The source timegroup to preview
|
|
704
|
+
* @param scaleOrOptions - Scale factor (default 1) or options object
|
|
705
|
+
* @returns Object with canvas and refresh function
|
|
706
|
+
*/
|
|
707
|
+
function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCALE) {
|
|
708
|
+
const options = typeof scaleOrOptions === "number" ? { scale: scaleOrOptions } : scaleOrOptions;
|
|
709
|
+
const scale = options.scale ?? DEFAULT_PREVIEW_SCALE;
|
|
710
|
+
let currentResolutionScale = options.resolutionScale ?? DEFAULT_RESOLUTION_SCALE;
|
|
711
|
+
const width = timegroup.offsetWidth || DEFAULT_WIDTH;
|
|
712
|
+
const height = timegroup.offsetHeight || DEFAULT_HEIGHT;
|
|
713
|
+
const dpr = window.devicePixelRatio || 1;
|
|
714
|
+
let renderWidth = Math.floor(width * currentResolutionScale);
|
|
715
|
+
let renderHeight = Math.floor(height * currentResolutionScale);
|
|
716
|
+
const canvas = createDprCanvas({
|
|
717
|
+
renderWidth,
|
|
718
|
+
renderHeight,
|
|
719
|
+
scale,
|
|
720
|
+
fullWidth: width,
|
|
721
|
+
fullHeight: height,
|
|
722
|
+
dpr
|
|
723
|
+
});
|
|
724
|
+
const wrapperContainer = document.createElement("div");
|
|
725
|
+
wrapperContainer.style.cssText = "position: relative; display: inline-block;";
|
|
726
|
+
const debugLabel = createDebugLabel();
|
|
727
|
+
wrapperContainer.appendChild(debugLabel);
|
|
728
|
+
wrapperContainer.appendChild(canvas);
|
|
729
|
+
const ctx = canvas.getContext("2d");
|
|
730
|
+
if (!ctx) throw new Error("Failed to get canvas 2d context");
|
|
731
|
+
const { container, syncState } = buildCloneStructure(timegroup, toAbsoluteTime(timegroup, timegroup.currentTimeMs ?? 0));
|
|
732
|
+
const previewContainer = createPreviewContainer({
|
|
733
|
+
width: renderWidth,
|
|
734
|
+
height: renderHeight,
|
|
735
|
+
background: getComputedStyle(timegroup).background || "#000"
|
|
736
|
+
});
|
|
737
|
+
if (currentResolutionScale < 1) {
|
|
738
|
+
container.style.transform = `scale(${currentResolutionScale})`;
|
|
739
|
+
container.style.transformOrigin = "top left";
|
|
740
|
+
}
|
|
741
|
+
const styleEl = document.createElement("style");
|
|
742
|
+
styleEl.textContent = collectDocumentStyles();
|
|
743
|
+
previewContainer.appendChild(styleEl);
|
|
744
|
+
previewContainer.appendChild(container);
|
|
745
|
+
overrideRootCloneStyles(syncState);
|
|
746
|
+
let rendering = false;
|
|
747
|
+
let lastTimeMs = -1;
|
|
748
|
+
let hasLoggedScale = false;
|
|
749
|
+
let pendingResolutionScale = null;
|
|
750
|
+
/**
|
|
751
|
+
* Apply pending resolution scale changes.
|
|
752
|
+
* Called at the start of refresh() before rendering, so the old content
|
|
753
|
+
* stays visible until new content is ready to be drawn.
|
|
754
|
+
*/
|
|
755
|
+
const applyPendingResolutionChange = () => {
|
|
756
|
+
if (pendingResolutionScale === null) return;
|
|
757
|
+
const newScale = pendingResolutionScale;
|
|
758
|
+
pendingResolutionScale = null;
|
|
759
|
+
currentResolutionScale = newScale;
|
|
760
|
+
renderWidth = Math.floor(width * currentResolutionScale);
|
|
761
|
+
renderHeight = Math.floor(height * currentResolutionScale);
|
|
762
|
+
previewContainer.style.width = `${renderWidth}px`;
|
|
763
|
+
previewContainer.style.height = `${renderHeight}px`;
|
|
764
|
+
if (currentResolutionScale < 1) {
|
|
765
|
+
container.style.transform = `scale(${currentResolutionScale})`;
|
|
766
|
+
container.style.transformOrigin = "top left";
|
|
767
|
+
} else container.style.transform = "";
|
|
768
|
+
};
|
|
769
|
+
/**
|
|
770
|
+
* Dynamically change resolution scale without rebuilding clone structure.
|
|
771
|
+
* The actual change is deferred until next refresh() to avoid blanking -
|
|
772
|
+
* old content stays visible until new content is ready.
|
|
773
|
+
*/
|
|
774
|
+
const setResolutionScale = (newScale) => {
|
|
775
|
+
newScale = Math.max(.1, Math.min(1, newScale));
|
|
776
|
+
if (newScale === currentResolutionScale && pendingResolutionScale === null) return;
|
|
777
|
+
pendingResolutionScale = newScale;
|
|
778
|
+
lastTimeMs = -1;
|
|
779
|
+
};
|
|
780
|
+
const getResolutionScale = () => pendingResolutionScale ?? currentResolutionScale;
|
|
781
|
+
const refresh = async () => {
|
|
782
|
+
if (rendering) return;
|
|
783
|
+
const sourceTimeMs = timegroup.currentTimeMs ?? 0;
|
|
784
|
+
const userTimeMs = timegroup.userTimeMs ?? 0;
|
|
785
|
+
if (Math.abs(sourceTimeMs - userTimeMs) > TIME_EPSILON_MS) return;
|
|
786
|
+
if (userTimeMs === lastTimeMs) return;
|
|
787
|
+
lastTimeMs = userTimeMs;
|
|
788
|
+
rendering = true;
|
|
789
|
+
applyPendingResolutionChange();
|
|
790
|
+
if (!hasLoggedScale) {
|
|
791
|
+
hasLoggedScale = true;
|
|
792
|
+
const mode = getEffectiveRenderMode();
|
|
793
|
+
console.log(`[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
|
+
}
|
|
795
|
+
try {
|
|
796
|
+
syncStyles(syncState, toAbsoluteTime(timegroup, userTimeMs));
|
|
797
|
+
overrideRootCloneStyles(syncState);
|
|
798
|
+
const t0 = performance.now();
|
|
799
|
+
const image = await renderToImage(previewContainer, renderWidth, renderHeight, { canvasScale: currentResolutionScale });
|
|
800
|
+
const renderTime = performance.now() - t0;
|
|
801
|
+
const targetWidth = Math.floor(renderWidth * scale * dpr);
|
|
802
|
+
const targetHeight = Math.floor(renderHeight * scale * dpr);
|
|
803
|
+
if (canvas.width !== targetWidth || canvas.height !== targetHeight) {
|
|
804
|
+
canvas.width = targetWidth;
|
|
805
|
+
canvas.height = targetHeight;
|
|
806
|
+
} else ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
807
|
+
ctx.save();
|
|
808
|
+
ctx.scale(dpr * scale, dpr * scale);
|
|
809
|
+
ctx.drawImage(image, 0, 0);
|
|
810
|
+
ctx.restore();
|
|
811
|
+
defaultProfiler.incrementRenderCount();
|
|
812
|
+
if (defaultProfiler.shouldLogByFrameCount(60)) console.log(`[renderTimegroupToCanvas] Frame render: ${renderTime.toFixed(1)}ms (resolutionScale=${currentResolutionScale}, image=${image.width}x${image.height})`);
|
|
813
|
+
updateDebugLabel(debugLabel, renderWidth, renderHeight, currentResolutionScale);
|
|
814
|
+
} catch (e) {
|
|
815
|
+
console.error("Canvas preview render failed:", e);
|
|
816
|
+
} finally {
|
|
817
|
+
rendering = false;
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
refresh();
|
|
821
|
+
return {
|
|
822
|
+
container: wrapperContainer,
|
|
823
|
+
canvas,
|
|
824
|
+
refresh,
|
|
825
|
+
syncState,
|
|
826
|
+
setResolutionScale,
|
|
827
|
+
getResolutionScale
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
//#endregion
|
|
832
|
+
export { captureFromClone, captureTimegroupAtTime, renderTimegroupToCanvas, resetRenderState };
|
|
833
|
+
//# sourceMappingURL=renderTimegroupToCanvas.js.map
|