@editframe/elements 0.37.3-beta → 0.38.0
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 +17 -14
- package/dist/EF_FRAMEGEN.js.map +1 -1
- package/dist/EF_RENDERING.js.map +1 -1
- package/dist/canvas/EFCanvas.d.ts +9 -2
- package/dist/canvas/EFCanvas.js +14 -4
- package/dist/canvas/EFCanvas.js.map +1 -1
- package/dist/canvas/EFCanvasItem.d.ts +4 -4
- package/dist/canvas/overlays/SelectionOverlay.d.ts +10 -2
- package/dist/canvas/overlays/SelectionOverlay.js +5 -12
- package/dist/canvas/overlays/SelectionOverlay.js.map +1 -1
- package/dist/canvas/overlays/overlayState.js.map +1 -1
- package/dist/canvas/selection/SelectionController.js.map +1 -1
- package/dist/elements/EFAudio.d.ts +1 -11
- package/dist/elements/EFAudio.js +2 -10
- package/dist/elements/EFAudio.js.map +1 -1
- package/dist/elements/EFCaptions.d.ts +5 -9
- package/dist/elements/EFCaptions.js +34 -11
- package/dist/elements/EFCaptions.js.map +1 -1
- package/dist/elements/EFImage.d.ts +10 -8
- package/dist/elements/EFImage.js +117 -32
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFMedia/AssetMediaEngine.js +2 -2
- package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/BaseMediaEngine.js +15 -92
- package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.js +10 -11
- package/dist/elements/EFMedia/BufferedSeekingInput.js.map +1 -1
- package/dist/elements/EFMedia/{AssetIdMediaEngine.js → FileMediaEngine.js} +44 -24
- package/dist/elements/EFMedia/FileMediaEngine.js.map +1 -0
- package/dist/elements/EFMedia/JitMediaEngine.js +14 -13
- 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/shared/ThumbnailExtractor.js +12 -7
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
- package/dist/elements/EFMedia/shared/timeoutUtils.js +44 -0
- package/dist/elements/EFMedia/shared/timeoutUtils.js.map +1 -0
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +1 -1
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +4 -4
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
- package/dist/elements/EFMedia.d.ts +14 -8
- package/dist/elements/EFMedia.js +52 -19
- package/dist/elements/EFMedia.js.map +1 -1
- package/dist/elements/EFPanZoom.d.ts +2 -2
- package/dist/elements/EFPanZoom.js +1 -1
- package/dist/elements/EFPanZoom.js.map +1 -1
- package/dist/elements/EFSourceMixin.js +16 -8
- package/dist/elements/EFSourceMixin.js.map +1 -1
- package/dist/elements/EFSurface.d.ts +7 -10
- package/dist/elements/EFSurface.js +4 -43
- package/dist/elements/EFSurface.js.map +1 -1
- package/dist/elements/EFTemporal.d.ts +33 -8
- package/dist/elements/EFTemporal.js +92 -40
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFText.d.ts +3 -0
- package/dist/elements/EFText.js +54 -21
- package/dist/elements/EFText.js.map +1 -1
- package/dist/elements/EFTextSegment.js +8 -4
- package/dist/elements/EFTextSegment.js.map +1 -1
- package/dist/elements/EFTimegroup.d.ts +26 -43
- package/dist/elements/EFTimegroup.js +295 -314
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.d.ts +44 -42
- package/dist/elements/EFVideo.js +259 -172
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/elements/EFWaveform.d.ts +3 -8
- package/dist/elements/EFWaveform.js +18 -13
- package/dist/elements/EFWaveform.js.map +1 -1
- package/dist/elements/ElementPositionInfo.js.map +1 -1
- package/dist/elements/FetchMixin.js.map +1 -1
- package/dist/elements/TargetController.d.ts +0 -3
- package/dist/elements/TargetController.js +12 -35
- package/dist/elements/TargetController.js.map +1 -1
- package/dist/elements/TimegroupController.js.map +1 -1
- package/dist/elements/cloneFactoryRegistry.d.ts +14 -0
- package/dist/elements/cloneFactoryRegistry.js +15 -0
- package/dist/elements/cloneFactoryRegistry.js.map +1 -0
- package/dist/elements/renderTemporalAudio.js +8 -6
- package/dist/elements/renderTemporalAudio.js.map +1 -1
- package/dist/elements/setupTemporalHierarchy.js +62 -0
- package/dist/elements/setupTemporalHierarchy.js.map +1 -0
- package/dist/elements/updateAnimations.js +62 -87
- package/dist/elements/updateAnimations.js.map +1 -1
- package/dist/getRenderInfo.d.ts +3 -2
- package/dist/getRenderInfo.js +20 -4
- package/dist/getRenderInfo.js.map +1 -1
- package/dist/gui/ContextMixin.js +68 -12
- package/dist/gui/ContextMixin.js.map +1 -1
- package/dist/gui/Controllable.js +1 -1
- package/dist/gui/Controllable.js.map +1 -1
- package/dist/gui/EFActiveRootTemporal.d.ts +4 -4
- package/dist/gui/EFActiveRootTemporal.js.map +1 -1
- package/dist/gui/EFControls.d.ts +2 -2
- package/dist/gui/EFControls.js +2 -2
- package/dist/gui/EFControls.js.map +1 -1
- package/dist/gui/EFDial.d.ts +4 -4
- package/dist/gui/EFDial.js +12 -9
- package/dist/gui/EFDial.js.map +1 -1
- package/dist/gui/EFFilmstrip.d.ts +2 -0
- package/dist/gui/EFFilmstrip.js +18 -10
- package/dist/gui/EFFilmstrip.js.map +1 -1
- package/dist/gui/EFFitScale.d.ts +28 -4
- package/dist/gui/EFFitScale.js +88 -26
- package/dist/gui/EFFitScale.js.map +1 -1
- package/dist/gui/EFFocusOverlay.d.ts +4 -4
- package/dist/gui/EFFocusOverlay.js +3 -3
- package/dist/gui/EFFocusOverlay.js.map +1 -1
- package/dist/gui/EFOverlayItem.d.ts +4 -4
- package/dist/gui/EFOverlayLayer.d.ts +4 -4
- 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.js +1 -1
- package/dist/gui/EFResizableBox.d.ts +4 -4
- package/dist/gui/EFResizableBox.js +5 -5
- package/dist/gui/EFResizableBox.js.map +1 -1
- package/dist/gui/EFScrubber.d.ts +4 -4
- package/dist/gui/EFScrubber.js +8 -13
- package/dist/gui/EFScrubber.js.map +1 -1
- package/dist/gui/EFTimeDisplay.d.ts +8 -4
- package/dist/gui/EFTimeDisplay.js +25 -7
- package/dist/gui/EFTimeDisplay.js.map +1 -1
- package/dist/gui/EFTimelineRuler.d.ts +4 -4
- package/dist/gui/EFTimelineRuler.js +3 -3
- package/dist/gui/EFTimelineRuler.js.map +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 +6 -6
- package/dist/gui/EFTransformHandles.js.map +1 -1
- package/dist/gui/EFWorkbench.d.ts +40 -36
- package/dist/gui/EFWorkbench.js +436 -822
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/FitScaleHelpers.js.map +1 -1
- package/dist/gui/PlaybackController.d.ts +3 -8
- package/dist/gui/PlaybackController.js +59 -56
- 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 +43 -6
- package/dist/gui/TargetOrContextMixin.js.map +1 -1
- package/dist/gui/ef-theme.css +136 -0
- package/dist/gui/hierarchy/EFHierarchy.d.ts +2 -2
- package/dist/gui/hierarchy/EFHierarchy.js +14 -24
- package/dist/gui/hierarchy/EFHierarchy.js.map +1 -1
- package/dist/gui/hierarchy/EFHierarchyItem.d.ts +3 -3
- package/dist/gui/hierarchy/EFHierarchyItem.js +22 -10
- package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -1
- package/dist/gui/icons.js.map +1 -1
- package/dist/gui/previewSettingsContext.d.ts +18 -0
- package/dist/gui/previewSettingsContext.js.map +1 -1
- package/dist/gui/theme.js +34 -0
- package/dist/gui/theme.js.map +1 -0
- package/dist/gui/timeline/EFTimeline.d.ts +2 -2
- package/dist/gui/timeline/EFTimeline.js +70 -52
- package/dist/gui/timeline/EFTimeline.js.map +1 -1
- package/dist/gui/timeline/EFTimelineRow.d.ts +3 -1
- package/dist/gui/timeline/EFTimelineRow.js +55 -32
- package/dist/gui/timeline/EFTimelineRow.js.map +1 -1
- package/dist/gui/timeline/TrimHandles.d.ts +23 -9
- package/dist/gui/timeline/TrimHandles.js +224 -51
- package/dist/gui/timeline/TrimHandles.js.map +1 -1
- package/dist/gui/timeline/flattenHierarchy.js.map +1 -1
- package/dist/gui/timeline/timelineEditingContext.d.ts +34 -0
- package/dist/gui/timeline/timelineEditingContext.js +24 -0
- package/dist/gui/timeline/timelineEditingContext.js.map +1 -0
- package/dist/gui/timeline/timelineStateContext.js.map +1 -1
- package/dist/gui/timeline/tracks/AudioTrack.js +1 -1
- package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/CaptionsTrack.d.ts +2 -3
- package/dist/gui/timeline/tracks/CaptionsTrack.js +17 -75
- package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/EFThumbnailStrip.d.ts +52 -0
- package/dist/gui/timeline/tracks/EFThumbnailStrip.js +596 -0
- package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -0
- package/dist/gui/timeline/tracks/HTMLTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TextTrack.d.ts +3 -2
- package/dist/gui/timeline/tracks/TextTrack.js +17 -43
- package/dist/gui/timeline/tracks/TextTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TimegroupTrack.d.ts +3 -4
- package/dist/gui/timeline/tracks/TimegroupTrack.js +33 -23
- package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TrackItem.d.ts +7 -9
- package/dist/gui/timeline/tracks/TrackItem.js +18 -17
- package/dist/gui/timeline/tracks/TrackItem.js.map +1 -1
- package/dist/gui/timeline/tracks/VideoTrack.d.ts +3 -3
- package/dist/gui/timeline/tracks/VideoTrack.js +11 -14
- package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/WaveformTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -1
- package/dist/gui/timeline/tracks/waveformUtils.js +1 -1
- package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -1
- package/dist/gui/tree/EFTree.d.ts +4 -4
- package/dist/gui/tree/EFTree.js +8 -14
- package/dist/gui/tree/EFTree.js.map +1 -1
- package/dist/gui/tree/EFTreeItem.d.ts +4 -4
- package/dist/gui/tree/EFTreeItem.js +3 -3
- package/dist/gui/tree/EFTreeItem.js.map +1 -1
- package/dist/gui/tree/treeContext.js.map +1 -1
- package/dist/index.d.ts +10 -8
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +2 -2
- package/dist/node.js +2 -2
- package/dist/preview/AdaptiveResolutionTracker.js +3 -3
- package/dist/preview/AdaptiveResolutionTracker.js.map +1 -1
- package/dist/preview/FrameController.d.ts +2 -17
- package/dist/preview/FrameController.js +40 -63
- package/dist/preview/FrameController.js.map +1 -1
- package/dist/preview/QualityUpgradeScheduler.d.ts +76 -0
- package/dist/preview/QualityUpgradeScheduler.js +158 -0
- package/dist/preview/QualityUpgradeScheduler.js.map +1 -0
- package/dist/preview/RenderContext.d.ts +119 -1
- package/dist/preview/RenderContext.js +21 -3
- package/dist/preview/RenderContext.js.map +1 -1
- package/dist/preview/RenderProfiler.js.map +1 -1
- package/dist/preview/RenderStats.js +85 -0
- package/dist/preview/RenderStats.js.map +1 -0
- package/dist/preview/encoding/canvasEncoder.js +2 -52
- package/dist/preview/encoding/canvasEncoder.js.map +1 -1
- package/dist/preview/encoding/mainThreadEncoder.js.map +1 -1
- package/dist/preview/encoding/workerEncoder.js.map +1 -1
- package/dist/preview/logger.js.map +1 -1
- package/dist/preview/previewSettings.d.ts +34 -0
- package/dist/preview/previewSettings.js +29 -17
- package/dist/preview/previewSettings.js.map +1 -1
- package/dist/preview/previewTypes.js +4 -4
- package/dist/preview/previewTypes.js.map +1 -1
- package/dist/preview/renderElementToCanvas.d.ts +44 -0
- package/dist/preview/renderElementToCanvas.js +72 -0
- package/dist/preview/renderElementToCanvas.js.map +1 -0
- package/dist/preview/renderTimegroupToCanvas.js +267 -145
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
- package/dist/preview/renderTimegroupToCanvas.types.d.ts +30 -0
- package/dist/preview/renderTimegroupToVideo.js +85 -105
- package/dist/preview/renderTimegroupToVideo.js.map +1 -1
- package/dist/preview/{renderTimegroupToVideo.d.ts → renderTimegroupToVideo.types.d.ts} +9 -9
- package/dist/preview/renderVideoToVideo.js +286 -0
- package/dist/preview/renderVideoToVideo.js.map +1 -0
- package/dist/preview/renderers.js.map +1 -1
- package/dist/preview/rendering/ScaleConfig.js +74 -0
- package/dist/preview/rendering/ScaleConfig.js.map +1 -0
- package/dist/preview/rendering/inlineImages.js +1 -44
- package/dist/preview/rendering/inlineImages.js.map +1 -1
- package/dist/preview/rendering/loadImage.js +22 -0
- package/dist/preview/rendering/loadImage.js.map +1 -0
- package/dist/preview/rendering/renderToImageNative.js +3 -3
- package/dist/preview/rendering/renderToImageNative.js.map +1 -1
- package/dist/preview/rendering/serializeTimelineDirect.js +224 -68
- package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -1
- package/dist/preview/statsTrackingStrategy.js +1 -101
- package/dist/preview/statsTrackingStrategy.js.map +1 -1
- package/dist/preview/workers/WorkerPool.js +0 -1
- package/dist/preview/workers/WorkerPool.js.map +1 -1
- package/dist/preview/workers/encoderWorkerInline.js +21 -54
- package/dist/preview/workers/encoderWorkerInline.js.map +1 -1
- package/dist/render/EFRenderAPI.d.ts +2 -1
- package/dist/render/EFRenderAPI.js +12 -36
- package/dist/render/EFRenderAPI.js.map +1 -1
- package/dist/render/getRenderData.js +4 -4
- package/dist/render/getRenderData.js.map +1 -1
- package/dist/style.css +114 -163
- package/dist/transcoding/cache/RequestDeduplicator.js +1 -0
- package/dist/transcoding/cache/RequestDeduplicator.js.map +1 -1
- package/dist/transcoding/types/index.d.ts +1 -1
- package/dist/transcoding/utils/UrlGenerator.js +10 -3
- package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
- package/dist/utils/LRUCache.js +1 -0
- package/dist/utils/LRUCache.js.map +1 -1
- package/dist/utils/frameTime.js +23 -1
- package/dist/utils/frameTime.js.map +1 -1
- package/package.json +21 -8
- package/scripts/build-css.js +8 -1
- package/test/setup.ts +0 -1
- package/test/useAssetMSW.ts +50 -0
- package/test/visualRegressionUtils.ts +23 -9
- package/dist/_virtual/rolldown_runtime.js +0 -27
- package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +0 -1
- package/dist/elements/EFThumbnailStrip.d.ts +0 -167
- package/dist/elements/EFThumbnailStrip.js +0 -731
- package/dist/elements/EFThumbnailStrip.js.map +0 -1
- package/dist/elements/SessionThumbnailCache.js +0 -154
- package/dist/elements/SessionThumbnailCache.js.map +0 -1
- package/dist/node_modules/react/cjs/react-jsx-runtime.development.js +0 -688
- package/dist/node_modules/react/cjs/react-jsx-runtime.development.js.map +0 -1
- package/dist/node_modules/react/cjs/react.development.js +0 -1521
- package/dist/node_modules/react/cjs/react.development.js.map +0 -1
- package/dist/node_modules/react/index.js +0 -13
- package/dist/node_modules/react/index.js.map +0 -1
- package/dist/node_modules/react/jsx-runtime.js +0 -13
- package/dist/node_modules/react/jsx-runtime.js.map +0 -1
- package/dist/preview/encoding/types.d.ts +0 -1
- package/dist/preview/renderTimegroupPreview.js +0 -686
- package/dist/preview/renderTimegroupPreview.js.map +0 -1
- package/dist/preview/renderTimegroupToCanvas.d.ts +0 -42
- package/dist/preview/rendering/renderToImage.d.ts +0 -2
- package/dist/preview/rendering/renderToImage.js +0 -95
- package/dist/preview/rendering/renderToImage.js.map +0 -1
- package/dist/preview/rendering/renderToImageForeignObject.js +0 -163
- package/dist/preview/rendering/renderToImageForeignObject.js.map +0 -1
- package/dist/preview/rendering/renderToImageNative.d.ts +0 -1
- package/dist/preview/rendering/svgSerializer.js +0 -43
- package/dist/preview/rendering/svgSerializer.js.map +0 -1
- package/dist/preview/rendering/types.d.ts +0 -2
- package/dist/preview/thumbnailCacheSettings.js +0 -52
- package/dist/preview/thumbnailCacheSettings.js.map +0 -1
- package/dist/sandbox/PlaybackControls.d.ts +0 -1
- package/dist/sandbox/PlaybackControls.js +0 -10
- package/dist/sandbox/PlaybackControls.js.map +0 -1
- package/dist/sandbox/ScenarioRunner.d.ts +0 -1
- package/dist/sandbox/ScenarioRunner.js +0 -1
- package/dist/sandbox/defineSandbox.d.ts +0 -1
- package/dist/sandbox/index.d.ts +0 -3
- package/dist/sandbox/index.js +0 -2
- package/test/EFVideo.framegen.browsertest.ts +0 -80
- package/test/thumbnail-performance-test.html +0 -116
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ScaleConfig.js","names":[],"sources":["../../../src/preview/rendering/ScaleConfig.ts"],"sourcesContent":["/**\n * ScaleConfig - Unified scaling configuration for timeline serialization.\n *\n * Consolidates the multi-stage scaling architecture into a single,\n * well-defined abstraction with clear contracts.\n *\n * Previously, scaling was applied in 4 separate stages with implicit contracts:\n * 1. captureTimelineToDataUri: scaled output dimensions\n * 2. captureElementParts: CSS transform wrapper for DOM content\n * 3. serializeCanvas: independent optimalScale calculation per canvas\n * 4. encodeCanvasesInParallel: received pre-scaled snapshots\n *\n * Now, ScaleConfig centralizes all scaling logic and makes the contracts explicit.\n */\n\nexport interface CanvasScaleParams {\n /** Natural canvas pixel dimensions */\n naturalWidth: number;\n naturalHeight: number;\n /** CSS display dimensions (how big it appears) */\n displayWidth: number;\n displayHeight: number;\n}\n\n/**\n * Immutable scaling configuration for a serialization operation.\n *\n * All scaling decisions are computed once at construction and cached.\n * This ensures consistency across all stages of serialization.\n */\nexport class ScaleConfig {\n /** User-specified export scale (e.g., 0.25 for thumbnails, 1.0 for full resolution) */\n readonly exportScale: number;\n\n /** Input dimensions (before scaling) */\n readonly inputWidth: number;\n readonly inputHeight: number;\n\n /** Output SVG dimensions (after scaling) */\n readonly outputWidth: number;\n readonly outputHeight: number;\n\n /** Whether DOM content needs CSS transform:scale() wrapper */\n readonly needsDOMScaling: boolean;\n\n /** Quality multiplier for canvas encoding (1.5x for sharpness) */\n readonly qualityMultiplier: number = 1.5;\n\n constructor(width: number, height: number, exportScale: number) {\n this.inputWidth = width;\n this.inputHeight = height;\n this.exportScale = exportScale;\n\n // Compute output dimensions (Stage 1)\n this.outputWidth = Math.floor(width * exportScale);\n this.outputHeight = Math.floor(height * exportScale);\n\n // Determine if DOM needs CSS scaling (Stage 2)\n this.needsDOMScaling = exportScale < 1;\n\n // Freeze to ensure immutability\n Object.freeze(this);\n }\n\n /**\n * Compute optimal encoding scale for a canvas element.\n *\n * This is Stage 3 of the scaling architecture. Canvas pixels are scaled\n * independently from DOM content because they have intrinsic resolution.\n *\n * Algorithm:\n * 1. Calculate display scale (CSS size vs natural size)\n * 2. Multiply by export scale\n * 3. Multiply by quality multiplier (1.5x for sharpness)\n * 4. Cap at 1.0 (never upscale beyond natural resolution)\n *\n * @param params - Canvas dimensions (natural and display)\n * @returns Optimal scale for encoding (0.0 to 1.0)\n */\n computeCanvasScale(params: CanvasScaleParams): number {\n const { naturalWidth, naturalHeight, displayWidth, displayHeight } = params;\n\n // Calculate how much smaller the display is vs natural size\n const displayScaleX = displayWidth / naturalWidth;\n const displayScaleY = displayHeight / naturalHeight;\n const displayScale = Math.min(displayScaleX, displayScaleY);\n\n // Combine display scale, export scale, and quality multiplier\n // Cap at 1.0 to never upscale beyond natural resolution\n const optimalScale = Math.min(\n 1.0,\n displayScale * this.exportScale * this.qualityMultiplier,\n );\n\n return optimalScale;\n }\n\n /**\n * Get the CSS transform value for DOM scaling.\n * Returns null if no scaling is needed.\n */\n getDOMTransform(): string | null {\n return this.needsDOMScaling ? `scale(${this.exportScale})` : null;\n }\n\n /**\n * Get the wrapper dimensions for the CSS transform.\n * When DOM is scaled, the wrapper must be larger to accommodate\n * the scaled-down content.\n */\n getDOMWrapperDimensions(): { width: number; height: number } {\n if (!this.needsDOMScaling) {\n return { width: this.outputWidth, height: this.outputHeight };\n }\n\n return {\n width: Math.floor(this.outputWidth / this.exportScale),\n height: Math.floor(this.outputHeight / this.exportScale),\n };\n }\n\n /**\n * Create a ScaleConfig from legacy options.\n * Maintains backward compatibility with existing callsites.\n */\n static fromOptions(\n width: number,\n height: number,\n canvasScale: number,\n ): ScaleConfig {\n return new ScaleConfig(width, height, canvasScale);\n }\n}\n"],"mappings":";;;;;;;AA8BA,IAAa,cAAb,MAAa,YAAY;CAkBvB,YAAY,OAAe,QAAgB,aAAqB;2BAF3B;AAGnC,OAAK,aAAa;AAClB,OAAK,cAAc;AACnB,OAAK,cAAc;AAGnB,OAAK,cAAc,KAAK,MAAM,QAAQ,YAAY;AAClD,OAAK,eAAe,KAAK,MAAM,SAAS,YAAY;AAGpD,OAAK,kBAAkB,cAAc;AAGrC,SAAO,OAAO,KAAK;;;;;;;;;;;;;;;;;CAkBrB,mBAAmB,QAAmC;EACpD,MAAM,EAAE,cAAc,eAAe,cAAc,kBAAkB;EAGrE,MAAM,gBAAgB,eAAe;EACrC,MAAM,gBAAgB,gBAAgB;EACtC,MAAM,eAAe,KAAK,IAAI,eAAe,cAAc;AAS3D,SALqB,KAAK,IACxB,GACA,eAAe,KAAK,cAAc,KAAK,kBACxC;;;;;;CASH,kBAAiC;AAC/B,SAAO,KAAK,kBAAkB,SAAS,KAAK,YAAY,KAAK;;;;;;;CAQ/D,0BAA6D;AAC3D,MAAI,CAAC,KAAK,gBACR,QAAO;GAAE,OAAO,KAAK;GAAa,QAAQ,KAAK;GAAc;AAG/D,SAAO;GACL,OAAO,KAAK,MAAM,KAAK,cAAc,KAAK,YAAY;GACtD,QAAQ,KAAK,MAAM,KAAK,eAAe,KAAK,YAAY;GACzD;;;;;;CAOH,OAAO,YACL,OACA,QACA,aACa;AACb,SAAO,IAAI,YAAY,OAAO,QAAQ,YAAY"}
|
|
@@ -1,50 +1,7 @@
|
|
|
1
|
-
import { logger } from "../logger.js";
|
|
2
|
-
|
|
3
1
|
//#region src/preview/rendering/inlineImages.ts
|
|
4
|
-
/** Maximum number of cached inline images before eviction */
|
|
5
|
-
const MAX_INLINE_IMAGE_CACHE_SIZE = 100;
|
|
6
2
|
/** Image cache for inlining external images as data URIs (foreignObject path) */
|
|
7
3
|
const _inlineImageCache = /* @__PURE__ */ new Map();
|
|
8
4
|
/**
|
|
9
|
-
* Convert a Blob to a data URL.
|
|
10
|
-
*/
|
|
11
|
-
function blobToDataURL(blob) {
|
|
12
|
-
return new Promise((resolve, reject) => {
|
|
13
|
-
const reader = new FileReader();
|
|
14
|
-
reader.onload = () => resolve(reader.result);
|
|
15
|
-
reader.onerror = reject;
|
|
16
|
-
reader.readAsDataURL(blob);
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Inline all images in a container as base64 data URIs.
|
|
21
|
-
* SVG foreignObject can't load external images due to security restrictions.
|
|
22
|
-
* Uses an LRU-style cache with size limits to prevent memory leaks.
|
|
23
|
-
*/
|
|
24
|
-
async function inlineImages(container) {
|
|
25
|
-
const images = container.querySelectorAll("img");
|
|
26
|
-
for (const image of images) {
|
|
27
|
-
const src = image.getAttribute("src");
|
|
28
|
-
if (!src || src.startsWith("data:")) continue;
|
|
29
|
-
const cached = _inlineImageCache.get(src);
|
|
30
|
-
if (cached) {
|
|
31
|
-
image.setAttribute("src", cached);
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
try {
|
|
35
|
-
const dataUrl = await blobToDataURL(await (await fetch(src)).blob());
|
|
36
|
-
image.setAttribute("src", dataUrl);
|
|
37
|
-
if (_inlineImageCache.size >= MAX_INLINE_IMAGE_CACHE_SIZE) {
|
|
38
|
-
const firstKey = _inlineImageCache.keys().next().value;
|
|
39
|
-
if (firstKey) _inlineImageCache.delete(firstKey);
|
|
40
|
-
}
|
|
41
|
-
_inlineImageCache.set(src, dataUrl);
|
|
42
|
-
} catch (e) {
|
|
43
|
-
logger.warn("Failed to inline image:", src, e);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
5
|
* Clear the inline image cache. Useful for memory management in long-running sessions.
|
|
49
6
|
*/
|
|
50
7
|
function clearInlineImageCache() {
|
|
@@ -52,5 +9,5 @@ function clearInlineImageCache() {
|
|
|
52
9
|
}
|
|
53
10
|
|
|
54
11
|
//#endregion
|
|
55
|
-
export { clearInlineImageCache
|
|
12
|
+
export { clearInlineImageCache };
|
|
56
13
|
//# sourceMappingURL=inlineImages.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inlineImages.js","names":[],"sources":["../../../src/preview/rendering/inlineImages.ts"],"sourcesContent":["/**\n * Image inlining utilities for SVG foreignObject rendering.\n * SVG foreignObject can't load external images due to security restrictions,\n * so we convert them to base64 data URIs.\n */\n\nimport { logger } from \"../logger.js\";\n\n/** Maximum number of cached inline images before eviction */\nconst MAX_INLINE_IMAGE_CACHE_SIZE = 100;\n\n/** Image cache for inlining external images as data URIs (foreignObject path) */\nconst _inlineImageCache = new Map<string, string>();\n\n/**\n * Convert a Blob to a data URL.\n */\nfunction blobToDataURL(blob: Blob): Promise<string> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result as string);\n reader.onerror = reject;\n reader.readAsDataURL(blob);\n });\n}\n\n/**\n * Inline all images in a container as base64 data URIs.\n * SVG foreignObject can't load external images due to security restrictions.\n * Uses an LRU-style cache with size limits to prevent memory leaks.\n */\nexport async function inlineImages(container: HTMLElement): Promise<void> {\n const images = container.querySelectorAll(\"img\");\n for (const image of images) {\n const src = image.getAttribute(\"src\");\n if (!src || src.startsWith(\"data:\")) continue;\n\n const cached = _inlineImageCache.get(src);\n if (cached) {\n image.setAttribute(\"src\", cached);\n continue;\n }\n\n try {\n const response = await fetch(src);\n const blob = await response.blob();\n const dataUrl = await blobToDataURL(blob);\n image.setAttribute(\"src\", dataUrl);\n
|
|
1
|
+
{"version":3,"file":"inlineImages.js","names":[],"sources":["../../../src/preview/rendering/inlineImages.ts"],"sourcesContent":["/**\n * Image inlining utilities for SVG foreignObject rendering.\n * SVG foreignObject can't load external images due to security restrictions,\n * so we convert them to base64 data URIs.\n */\n\nimport { logger } from \"../logger.js\";\n\n/** Maximum number of cached inline images before eviction */\nconst MAX_INLINE_IMAGE_CACHE_SIZE = 100;\n\n/** Image cache for inlining external images as data URIs (foreignObject path) */\nconst _inlineImageCache = new Map<string, string>();\n\n/**\n * Convert a Blob to a data URL.\n */\nfunction blobToDataURL(blob: Blob): Promise<string> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result as string);\n reader.onerror = reject;\n reader.readAsDataURL(blob);\n });\n}\n\n/**\n * Inline all images in a container as base64 data URIs.\n * SVG foreignObject can't load external images due to security restrictions.\n * Uses an LRU-style cache with size limits to prevent memory leaks.\n */\nexport async function inlineImages(container: HTMLElement): Promise<void> {\n const images = container.querySelectorAll(\"img\");\n for (const image of images) {\n const src = image.getAttribute(\"src\");\n if (!src || src.startsWith(\"data:\")) continue;\n\n const cached = _inlineImageCache.get(src);\n if (cached) {\n image.setAttribute(\"src\", cached);\n continue;\n }\n\n try {\n const response = await fetch(src);\n const blob = await response.blob();\n const dataUrl = await blobToDataURL(blob);\n image.setAttribute(\"src\", dataUrl);\n\n // Evict oldest entries if cache is full (simple FIFO eviction)\n if (_inlineImageCache.size >= MAX_INLINE_IMAGE_CACHE_SIZE) {\n const firstKey = _inlineImageCache.keys().next().value;\n if (firstKey) _inlineImageCache.delete(firstKey);\n }\n _inlineImageCache.set(src, dataUrl);\n } catch (e) {\n logger.warn(\"Failed to inline image:\", src, e);\n }\n }\n}\n\n/**\n * Clear the inline image cache. Useful for memory management in long-running sessions.\n */\nexport function clearInlineImageCache(): void {\n _inlineImageCache.clear();\n}\n\n/**\n * Get current inline image cache size for diagnostics.\n */\nexport function getInlineImageCacheSize(): number {\n return _inlineImageCache.size;\n}\n"],"mappings":";;AAYA,MAAM,oCAAoB,IAAI,KAAqB;;;;AAoDnD,SAAgB,wBAA8B;AAC5C,mBAAkB,OAAO"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defaultProfiler } from "../RenderProfiler.js";
|
|
2
|
+
|
|
3
|
+
//#region src/preview/rendering/loadImage.ts
|
|
4
|
+
/**
|
|
5
|
+
* Load an image from a data URI. Returns a Promise that resolves when loaded.
|
|
6
|
+
*/
|
|
7
|
+
function loadImageFromDataUri(dataUri) {
|
|
8
|
+
const img = new Image();
|
|
9
|
+
const imageLoadStart = performance.now();
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
img.onload = () => {
|
|
12
|
+
defaultProfiler.addTime("imageLoad", performance.now() - imageLoadStart);
|
|
13
|
+
resolve(img);
|
|
14
|
+
};
|
|
15
|
+
img.onerror = reject;
|
|
16
|
+
img.src = dataUri;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
export { loadImageFromDataUri };
|
|
22
|
+
//# sourceMappingURL=loadImage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loadImage.js","names":[],"sources":["../../../src/preview/rendering/loadImage.ts"],"sourcesContent":["import { defaultProfiler } from \"../RenderProfiler.js\";\n\n/**\n * Load an image from a data URI. Returns a Promise that resolves when loaded.\n */\nexport function loadImageFromDataUri(\n dataUri: string,\n): Promise<HTMLImageElement> {\n const img = new Image();\n const imageLoadStart = performance.now();\n\n return new Promise<HTMLImageElement>((resolve, reject) => {\n img.onload = () => {\n defaultProfiler.addTime(\"imageLoad\", performance.now() - imageLoadStart);\n resolve(img);\n };\n img.onerror = reject;\n img.src = dataUri;\n });\n}\n"],"mappings":";;;;;;AAKA,SAAgB,qBACd,SAC2B;CAC3B,MAAM,MAAM,IAAI,OAAO;CACvB,MAAM,iBAAiB,YAAY,KAAK;AAExC,QAAO,IAAI,SAA2B,SAAS,WAAW;AACxD,MAAI,eAAe;AACjB,mBAAgB,QAAQ,aAAa,YAAY,KAAK,GAAG,eAAe;AACxE,WAAQ,IAAI;;AAEd,MAAI,UAAU;AACd,MAAI,MAAM;GACV"}
|
|
@@ -27,15 +27,15 @@ function createDprCanvas(options) {
|
|
|
27
27
|
/**
|
|
28
28
|
* Render HTML content to canvas using native HTML-in-Canvas API (drawElementImage).
|
|
29
29
|
* This is much faster than the foreignObject approach and avoids canvas tainting.
|
|
30
|
-
*
|
|
30
|
+
*
|
|
31
31
|
* Note: The native API renders at device pixel ratio, so we capture at DPR scale
|
|
32
32
|
* and then downsample to logical pixels to match the foreignObject path's output.
|
|
33
|
-
*
|
|
33
|
+
*
|
|
34
34
|
* @param container - The HTML element to render
|
|
35
35
|
* @param width - Target width in logical pixels
|
|
36
36
|
* @param height - Target height in logical pixels
|
|
37
37
|
* @param options - Rendering options (skipWait for batch mode)
|
|
38
|
-
*
|
|
38
|
+
*
|
|
39
39
|
* @see https://github.com/WICG/html-in-canvas
|
|
40
40
|
*/
|
|
41
41
|
async function renderToImageNative(container, width, height, options = {}) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderToImageNative.js","names":["captureCanvas: HTMLCanvasElement","dpr"],"sources":["../../../src/preview/rendering/renderToImageNative.ts"],"sourcesContent":["/**\n * Native HTML-in-Canvas rendering using drawElementImage API.\n */\n\nimport type { HtmlInCanvasContext, HtmlInCanvasElement, NativeRenderOptions } from \"./types.js\";\nimport { defaultProfiler } from \"../RenderProfiler.js\";\n\n/** Track canvases that have been initialized for layoutsubtree (only need to wait once) */\nconst _layoutInitializedCanvases = new WeakSet<HTMLCanvasElement>();\n\n/**\n * Wait for next animation frame (allows browser to complete layout)\n */\nfunction waitForFrame(): Promise<void> {\n return new Promise(resolve => requestAnimationFrame(() => resolve()));\n}\n\n/**\n * Create a canvas element with proper DPR handling.\n * Buffer size is based on renderWidth/renderHeight (internal resolution).\n * CSS size is based on fullWidth/fullHeight (logical display size).\n */\nexport function createDprCanvas(options: {\n renderWidth: number;\n renderHeight: number;\n scale: number;\n dpr?: number;\n fullWidth: number;\n fullHeight: number;\n}): HTMLCanvasElement {\n const { renderWidth, renderHeight, scale, fullWidth, fullHeight } = options;\n const dpr = options.dpr ?? window.devicePixelRatio ?? 1;\n \n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.floor(renderWidth * scale * dpr);\n canvas.height = Math.floor(renderHeight * scale * dpr);\n canvas.style.width = `${Math.floor(fullWidth * scale)}px`;\n canvas.style.height = `${Math.floor(fullHeight * scale)}px`;\n \n return canvas;\n}\n\n/**\n * Render HTML content to canvas using native HTML-in-Canvas API (drawElementImage).\n * This is much faster than the foreignObject approach and avoids canvas tainting.\n * \n * Note: The native API renders at device pixel ratio, so we capture at DPR scale\n * and then downsample to logical pixels to match the foreignObject path's output.\n * \n * @param container - The HTML element to render\n * @param width - Target width in logical pixels\n * @param height - Target height in logical pixels\n * @param options - Rendering options (skipWait for batch mode)\n * \n * @see https://github.com/WICG/html-in-canvas\n */\nexport async function renderToImageNative(\n container: HTMLElement,\n width: number,\n height: number,\n options: NativeRenderOptions = {},\n): Promise<HTMLCanvasElement> {\n const t0 = performance.now();\n const { reuseCanvas, skipDprScaling = false } = options;\n // Use 1x DPR when skipDprScaling is true (for video export) - 4x fewer pixels!\n const dpr = skipDprScaling ? 1 : (window.devicePixelRatio || 1);\n \n // Use provided canvas or create new one\n let captureCanvas: HTMLCanvasElement;\n let shouldCleanup = false;\n \n if (reuseCanvas) {\n captureCanvas = reuseCanvas;\n \n // Ensure canvas dimensions match (both attribute and CSS)\n const dpr = skipDprScaling ? 1 : (window.devicePixelRatio || 1);\n const targetWidth = Math.floor(width * dpr);\n const targetHeight = Math.floor(height * dpr);\n \n // Set attribute dimensions (pixel buffer size)\n if (captureCanvas.width !== targetWidth) {\n captureCanvas.width = targetWidth;\n }\n if (captureCanvas.height !== targetHeight) {\n captureCanvas.height = targetHeight;\n }\n \n // Ensure CSS dimensions and positioning (same as non-reuse path)\n // This ensures consistent behavior and avoids layout issues\n captureCanvas.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n opacity: 0;\n pointer-events: none;\n z-index: -9999;\n `;\n \n // Ensure layoutsubtree is set (required for drawElementImage)\n if (!captureCanvas.hasAttribute(\"layoutsubtree\")) {\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n }\n \n // Ensure canvas is in DOM (required for drawElementImage layout)\n if (!captureCanvas.parentNode) {\n document.body.appendChild(captureCanvas);\n }\n \n // Ensure container is child of canvas\n if (container.parentElement !== captureCanvas) {\n captureCanvas.appendChild(container);\n }\n \n // Ensure container is visible (not display: none) for layout\n // drawElementImage requires the element to be laid out\n const containerStyle = getComputedStyle(container);\n if (containerStyle.display === 'none') {\n container.style.display = 'block';\n }\n \n // Force synchronous layout ONLY on first use with this canvas\n // For batch rendering (video export), repeated layout forces are expensive\n // We only need to force layout once to ensure everything is ready\n if (!_layoutInitializedCanvases.has(captureCanvas)) {\n void captureCanvas.offsetHeight;\n void container.offsetHeight;\n getComputedStyle(captureCanvas).opacity;\n getComputedStyle(container).opacity;\n _layoutInitializedCanvases.add(captureCanvas);\n }\n } else {\n captureCanvas = document.createElement(\"canvas\");\n captureCanvas.width = Math.floor(width * dpr);\n captureCanvas.height = Math.floor(height * dpr);\n \n // Enable HTML-in-Canvas mode via layoutsubtree attribute/property\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n \n captureCanvas.appendChild(container);\n \n captureCanvas.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n opacity: 0;\n pointer-events: none;\n z-index: -9999;\n `;\n document.body.appendChild(captureCanvas);\n shouldCleanup = true;\n }\n \n const t1 = performance.now();\n defaultProfiler.addTime(\"setup\", t1 - t0);\n \n try {\n // Force style calculation to ensure CSS is computed before capture\n // This ensures both canvas and container are laid out (required for drawElementImage)\n getComputedStyle(container).opacity;\n \n // When reusing canvas with layoutsubtree, wait for initial layout (first use only)\n // Use a WeakSet to track canvases that have been initialized\n if (reuseCanvas && (captureCanvas as any).layoutSubtree && !_layoutInitializedCanvases.has(captureCanvas)) {\n await waitForFrame();\n _layoutInitializedCanvases.add(captureCanvas);\n \n // Canvas may have been detached during async wait (e.g., test cleanup)\n if (!captureCanvas.parentNode) {\n return captureCanvas;\n }\n }\n \n const ctx = captureCanvas.getContext(\"2d\") as HtmlInCanvasContext;\n ctx.drawElementImage(container, 0, 0);\n } finally {\n // Only clean up if we created the canvas\n if (shouldCleanup && captureCanvas.parentNode) {\n captureCanvas.parentNode.removeChild(captureCanvas);\n }\n }\n \n const t2 = performance.now();\n defaultProfiler.addTime(\"draw\", t2 - t1);\n \n // If DPR is 1, no downsampling needed - return as-is\n if (dpr === 1) {\n defaultProfiler.incrementRenderCount();\n return captureCanvas;\n }\n \n // Downsample to logical pixel dimensions to match foreignObject path output\n // This ensures consistent behavior regardless of which rendering path is used\n const outputCanvas = document.createElement(\"canvas\");\n outputCanvas.width = width;\n outputCanvas.height = height;\n \n const outputCtx = outputCanvas.getContext(\"2d\")!;\n // Draw the DPR-scaled capture onto the 1x output canvas\n outputCtx.drawImage(\n captureCanvas,\n 0, 0, captureCanvas.width, captureCanvas.height, // source (full DPR capture)\n 0, 0, width, height // destination (logical pixels)\n );\n \n const t3 = performance.now();\n defaultProfiler.addTime(\"downsample\", t3 - t2);\n defaultProfiler.incrementRenderCount();\n \n return outputCanvas;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"renderToImageNative.js","names":["captureCanvas: HTMLCanvasElement","dpr"],"sources":["../../../src/preview/rendering/renderToImageNative.ts"],"sourcesContent":["/**\n * Native HTML-in-Canvas rendering using drawElementImage API.\n */\n\nimport type {\n HtmlInCanvasContext,\n HtmlInCanvasElement,\n NativeRenderOptions,\n} from \"./types.js\";\nimport { defaultProfiler } from \"../RenderProfiler.js\";\n\n/** Track canvases that have been initialized for layoutsubtree (only need to wait once) */\nconst _layoutInitializedCanvases = new WeakSet<HTMLCanvasElement>();\n\n/**\n * Wait for next animation frame (allows browser to complete layout)\n */\nfunction waitForFrame(): Promise<void> {\n return new Promise((resolve) => requestAnimationFrame(() => resolve()));\n}\n\n/**\n * Create a canvas element with proper DPR handling.\n * Buffer size is based on renderWidth/renderHeight (internal resolution).\n * CSS size is based on fullWidth/fullHeight (logical display size).\n */\nexport function createDprCanvas(options: {\n renderWidth: number;\n renderHeight: number;\n scale: number;\n dpr?: number;\n fullWidth: number;\n fullHeight: number;\n}): HTMLCanvasElement {\n const { renderWidth, renderHeight, scale, fullWidth, fullHeight } = options;\n const dpr = options.dpr ?? window.devicePixelRatio ?? 1;\n\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.floor(renderWidth * scale * dpr);\n canvas.height = Math.floor(renderHeight * scale * dpr);\n canvas.style.width = `${Math.floor(fullWidth * scale)}px`;\n canvas.style.height = `${Math.floor(fullHeight * scale)}px`;\n\n return canvas;\n}\n\n/**\n * Render HTML content to canvas using native HTML-in-Canvas API (drawElementImage).\n * This is much faster than the foreignObject approach and avoids canvas tainting.\n *\n * Note: The native API renders at device pixel ratio, so we capture at DPR scale\n * and then downsample to logical pixels to match the foreignObject path's output.\n *\n * @param container - The HTML element to render\n * @param width - Target width in logical pixels\n * @param height - Target height in logical pixels\n * @param options - Rendering options (skipWait for batch mode)\n *\n * @see https://github.com/WICG/html-in-canvas\n */\nexport async function renderToImageNative(\n container: HTMLElement,\n width: number,\n height: number,\n options: NativeRenderOptions = {},\n): Promise<HTMLCanvasElement> {\n const t0 = performance.now();\n const { reuseCanvas, skipDprScaling = false } = options;\n // Use 1x DPR when skipDprScaling is true (for video export) - 4x fewer pixels!\n const dpr = skipDprScaling ? 1 : window.devicePixelRatio || 1;\n\n // Use provided canvas or create new one\n let captureCanvas: HTMLCanvasElement;\n let shouldCleanup = false;\n\n if (reuseCanvas) {\n captureCanvas = reuseCanvas;\n\n // Ensure canvas dimensions match (both attribute and CSS)\n const dpr = skipDprScaling ? 1 : window.devicePixelRatio || 1;\n const targetWidth = Math.floor(width * dpr);\n const targetHeight = Math.floor(height * dpr);\n\n // Set attribute dimensions (pixel buffer size)\n if (captureCanvas.width !== targetWidth) {\n captureCanvas.width = targetWidth;\n }\n if (captureCanvas.height !== targetHeight) {\n captureCanvas.height = targetHeight;\n }\n\n // Ensure CSS dimensions and positioning (same as non-reuse path)\n // This ensures consistent behavior and avoids layout issues\n captureCanvas.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n opacity: 0;\n pointer-events: none;\n z-index: -9999;\n `;\n\n // Ensure layoutsubtree is set (required for drawElementImage)\n if (!captureCanvas.hasAttribute(\"layoutsubtree\")) {\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n }\n\n // Ensure canvas is in DOM (required for drawElementImage layout)\n if (!captureCanvas.parentNode) {\n document.body.appendChild(captureCanvas);\n }\n\n // Ensure container is child of canvas\n if (container.parentElement !== captureCanvas) {\n captureCanvas.appendChild(container);\n }\n\n // Ensure container is visible (not display: none) for layout\n // drawElementImage requires the element to be laid out\n const containerStyle = getComputedStyle(container);\n if (containerStyle.display === \"none\") {\n container.style.display = \"block\";\n }\n\n // Force synchronous layout ONLY on first use with this canvas\n // For batch rendering (video export), repeated layout forces are expensive\n // We only need to force layout once to ensure everything is ready\n if (!_layoutInitializedCanvases.has(captureCanvas)) {\n void captureCanvas.offsetHeight;\n void container.offsetHeight;\n getComputedStyle(captureCanvas).opacity;\n getComputedStyle(container).opacity;\n _layoutInitializedCanvases.add(captureCanvas);\n }\n } else {\n captureCanvas = document.createElement(\"canvas\");\n captureCanvas.width = Math.floor(width * dpr);\n captureCanvas.height = Math.floor(height * dpr);\n\n // Enable HTML-in-Canvas mode via layoutsubtree attribute/property\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n\n captureCanvas.appendChild(container);\n\n captureCanvas.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n opacity: 0;\n pointer-events: none;\n z-index: -9999;\n `;\n document.body.appendChild(captureCanvas);\n shouldCleanup = true;\n }\n\n const t1 = performance.now();\n defaultProfiler.addTime(\"setup\", t1 - t0);\n\n try {\n // Force style calculation to ensure CSS is computed before capture\n // This ensures both canvas and container are laid out (required for drawElementImage)\n getComputedStyle(container).opacity;\n\n // When reusing canvas with layoutsubtree, wait for initial layout (first use only)\n // Use a WeakSet to track canvases that have been initialized\n if (\n reuseCanvas &&\n (captureCanvas as any).layoutSubtree &&\n !_layoutInitializedCanvases.has(captureCanvas)\n ) {\n await waitForFrame();\n _layoutInitializedCanvases.add(captureCanvas);\n\n // Canvas may have been detached during async wait (e.g., test cleanup)\n if (!captureCanvas.parentNode) {\n return captureCanvas;\n }\n }\n\n const ctx = captureCanvas.getContext(\"2d\") as HtmlInCanvasContext;\n ctx.drawElementImage(container, 0, 0);\n } finally {\n // Only clean up if we created the canvas\n if (shouldCleanup && captureCanvas.parentNode) {\n captureCanvas.parentNode.removeChild(captureCanvas);\n }\n }\n\n const t2 = performance.now();\n defaultProfiler.addTime(\"draw\", t2 - t1);\n\n // If DPR is 1, no downsampling needed - return as-is\n if (dpr === 1) {\n defaultProfiler.incrementRenderCount();\n return captureCanvas;\n }\n\n // Downsample to logical pixel dimensions to match foreignObject path output\n // This ensures consistent behavior regardless of which rendering path is used\n const outputCanvas = document.createElement(\"canvas\");\n outputCanvas.width = width;\n outputCanvas.height = height;\n\n const outputCtx = outputCanvas.getContext(\"2d\")!;\n // Draw the DPR-scaled capture onto the 1x output canvas\n outputCtx.drawImage(\n captureCanvas,\n 0,\n 0,\n captureCanvas.width,\n captureCanvas.height, // source (full DPR capture)\n 0,\n 0,\n width,\n height, // destination (logical pixels)\n );\n\n const t3 = performance.now();\n defaultProfiler.addTime(\"downsample\", t3 - t2);\n defaultProfiler.incrementRenderCount();\n\n return outputCanvas;\n}\n"],"mappings":"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"}
|
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
import { isVisibleAtTime } from "../previewTypes.js";
|
|
2
|
-
import { collectDocumentStyles } from "../renderTimegroupPreview.js";
|
|
3
1
|
import { encodeCanvasesInParallel } from "../encoding/canvasEncoder.js";
|
|
2
|
+
import { isTemporal, isVisibleAtTime } from "../previewTypes.js";
|
|
3
|
+
import { ScaleConfig } from "./ScaleConfig.js";
|
|
4
4
|
|
|
5
5
|
//#region src/preview/rendering/serializeTimelineDirect.ts
|
|
6
6
|
/**
|
|
7
|
+
* Collect document styles for shadow DOM injection.
|
|
8
|
+
*/
|
|
9
|
+
function collectDocumentStyles() {
|
|
10
|
+
const rules = [];
|
|
11
|
+
try {
|
|
12
|
+
for (const sheet of document.styleSheets) try {
|
|
13
|
+
if (sheet.cssRules) for (const rule of sheet.cssRules) rules.push(rule.cssText);
|
|
14
|
+
} catch {}
|
|
15
|
+
} catch (e) {
|
|
16
|
+
console.warn("[collectDocumentStyles] Failed to access document.styleSheets:", e);
|
|
17
|
+
}
|
|
18
|
+
return rules.join("\n");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
7
21
|
* Elements to skip entirely when serializing.
|
|
8
22
|
* NOTE: SLOT is NOT skipped - it's handled specially to serialize light DOM children.
|
|
9
23
|
*/
|
|
@@ -129,27 +143,50 @@ function escapeXML(str) {
|
|
|
129
143
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
130
144
|
}
|
|
131
145
|
/**
|
|
146
|
+
* Resolve the natural display value for an element that has display:none
|
|
147
|
+
* set as an inline style (e.g., from temporal visibility via updateAnimations).
|
|
148
|
+
*
|
|
149
|
+
* Temporarily removes the inline display override so getComputedStyle falls
|
|
150
|
+
* through to the element's stylesheet rules (including shadow DOM :host styles),
|
|
151
|
+
* reads the natural value, then restores the override.
|
|
152
|
+
*/
|
|
153
|
+
function resolveNaturalDisplay(element) {
|
|
154
|
+
const htmlEl = element;
|
|
155
|
+
if (htmlEl.style?.getPropertyValue("display") === "none" && htmlEl.style) {
|
|
156
|
+
htmlEl.style.removeProperty("display");
|
|
157
|
+
const natural = getComputedStyle(element).getPropertyValue("display");
|
|
158
|
+
htmlEl.style.setProperty("display", "none");
|
|
159
|
+
return natural || "block";
|
|
160
|
+
}
|
|
161
|
+
return "block";
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
132
164
|
* Serialize computed styles as inline style string.
|
|
133
|
-
* Handles display:none
|
|
134
|
-
*
|
|
165
|
+
* Handles display:none recovery for non-caption elements by resolving
|
|
166
|
+
* the element's natural display value from its stylesheet rules.
|
|
167
|
+
* @param element - The element to serialize styles for
|
|
168
|
+
* @param styles - Optional pre-computed CSSStyleDeclaration (avoids redundant getComputedStyle calls)
|
|
135
169
|
*/
|
|
136
|
-
function serializeComputedStyles(element) {
|
|
137
|
-
const
|
|
170
|
+
function serializeComputedStyles(element, styles) {
|
|
171
|
+
const computed = styles ?? getComputedStyle(element);
|
|
138
172
|
const styleParts = [];
|
|
139
173
|
const tagName = element.tagName;
|
|
140
174
|
const isCaptionChild = CAPTION_CHILD_TAGS.has(tagName);
|
|
175
|
+
const htmlEl = element;
|
|
176
|
+
const hasExplicitWidth = !!htmlEl.style?.getPropertyValue("width");
|
|
177
|
+
const hasExplicitHeight = !!htmlEl.style?.getPropertyValue("height");
|
|
141
178
|
for (const prop of SERIALIZED_STYLE_PROPERTIES) {
|
|
142
|
-
const
|
|
179
|
+
const kebab = prop.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
180
|
+
const value = computed.getPropertyValue(kebab);
|
|
143
181
|
if (!value || value === "") continue;
|
|
144
182
|
let finalValue = value;
|
|
145
183
|
if (prop === "display") {
|
|
146
|
-
if (
|
|
147
|
-
else if (tagName === "EF-TEXT-SEGMENT") finalValue = element.hasAttribute("data-line-segment") ? "block" : "inline-block";
|
|
148
|
-
else if (value === "none" && !isCaptionChild) finalValue = "block";
|
|
184
|
+
if (value === "none" && !isCaptionChild) finalValue = resolveNaturalDisplay(element);
|
|
149
185
|
}
|
|
150
186
|
if (prop === "visibility") finalValue = "visible";
|
|
151
187
|
if (prop === "clipPath") continue;
|
|
152
|
-
|
|
188
|
+
if (prop === "width" && !hasExplicitWidth) continue;
|
|
189
|
+
if (prop === "height" && !hasExplicitHeight) continue;
|
|
153
190
|
styleParts.push(`${kebab}:${finalValue}`);
|
|
154
191
|
}
|
|
155
192
|
styleParts.push("animation:none", "transition:none");
|
|
@@ -168,75 +205,153 @@ function serializeAttributes(element, parts) {
|
|
|
168
205
|
/**
|
|
169
206
|
* Check if a canvas element should preserve alpha channel.
|
|
170
207
|
* EF-WAVEFORM always needs alpha, EF-IMAGE checks hasAlpha property.
|
|
208
|
+
* EF-SURFACE needs alpha because:
|
|
209
|
+
* 1. Without a target, its canvas is transparent and CSS background should show through
|
|
210
|
+
* 2. The target element may have transparent content
|
|
211
|
+
* Raw canvas elements must preserve alpha - we don't know what they contain.
|
|
171
212
|
*/
|
|
172
213
|
function shouldPreserveAlpha(sourceElement) {
|
|
173
214
|
const tagName = sourceElement.tagName;
|
|
174
215
|
if (tagName === "EF-WAVEFORM") return true;
|
|
216
|
+
if (tagName === "EF-SURFACE") return true;
|
|
175
217
|
if (tagName === "EF-IMAGE") return "hasAlpha" in sourceElement && sourceElement.hasAlpha === true;
|
|
218
|
+
if (sourceElement instanceof HTMLCanvasElement) return true;
|
|
176
219
|
return false;
|
|
177
220
|
}
|
|
178
221
|
/**
|
|
222
|
+
* Find the capture proxy canvas for an offscreen-rendered canvas.
|
|
223
|
+
* When a canvas is transferred to offscreen via transferControlToOffscreen(),
|
|
224
|
+
* the main thread can no longer read pixels from it. OffscreenCompositionCanvas
|
|
225
|
+
* creates a hidden capture canvas (marked with data-offscreen-capture) that
|
|
226
|
+
* receives ImageBitmap frames from the worker.
|
|
227
|
+
*/
|
|
228
|
+
function findCaptureProxy(canvas) {
|
|
229
|
+
const container = canvas.parentElement;
|
|
230
|
+
if (!container) return null;
|
|
231
|
+
return container.querySelector("canvas[data-offscreen-capture=\"true\"]");
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Read pixels directly from a WebGL canvas's drawing buffer via gl.readPixels().
|
|
235
|
+
*
|
|
236
|
+
* drawImage(webglCanvas) reads from the compositor's "presented" surface, which
|
|
237
|
+
* is only refreshed during requestAnimationFrame / compositing cycles. In hidden
|
|
238
|
+
* browser tabs, compositing is suspended, so drawImage returns stale pixels even
|
|
239
|
+
* though gl.render() produced new content in the drawing buffer.
|
|
240
|
+
*
|
|
241
|
+
* readPixels() reads from the drawing buffer directly, bypassing the compositor.
|
|
242
|
+
*
|
|
243
|
+
* Returns null for non-WebGL canvases (getContext returns null when a different
|
|
244
|
+
* context type is already active).
|
|
245
|
+
*/
|
|
246
|
+
function readWebGLPixels(canvas) {
|
|
247
|
+
const gl = canvas.getContext("webgl2") ?? canvas.getContext("webgl");
|
|
248
|
+
if (!gl) return null;
|
|
249
|
+
const width = canvas.width;
|
|
250
|
+
const height = canvas.height;
|
|
251
|
+
if (width === 0 || height === 0) return null;
|
|
252
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
253
|
+
const pixels = new Uint8Array(width * height * 4);
|
|
254
|
+
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
255
|
+
const rowSize = width * 4;
|
|
256
|
+
const halfHeight = Math.floor(height / 2);
|
|
257
|
+
const temp = new Uint8Array(rowSize);
|
|
258
|
+
for (let y = 0; y < halfHeight; y++) {
|
|
259
|
+
const topOffset = y * rowSize;
|
|
260
|
+
const bottomOffset = (height - 1 - y) * rowSize;
|
|
261
|
+
temp.set(pixels.subarray(topOffset, topOffset + rowSize));
|
|
262
|
+
pixels.set(pixels.subarray(bottomOffset, bottomOffset + rowSize), topOffset);
|
|
263
|
+
pixels.set(temp, bottomOffset);
|
|
264
|
+
}
|
|
265
|
+
return new Uint8ClampedArray(pixels.buffer);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
179
268
|
* Create a snapshot copy of a canvas's current pixels.
|
|
180
269
|
* This captures the pixels synchronously before any async encoding,
|
|
181
270
|
* preventing race conditions where the source canvas is modified.
|
|
271
|
+
*
|
|
272
|
+
* For WebGL canvases, uses gl.readPixels() to bypass the compositor's
|
|
273
|
+
* presentation layer (which is suspended in hidden browser tabs).
|
|
274
|
+
*
|
|
275
|
+
* For offscreen-rendered canvases, this automatically uses the capture proxy
|
|
276
|
+
* canvas instead of the transferred display canvas.
|
|
182
277
|
*/
|
|
183
278
|
function snapshotCanvas(canvas, scale, preserveAlpha) {
|
|
184
|
-
const
|
|
185
|
-
const
|
|
279
|
+
const sourceCanvas = findCaptureProxy(canvas) ?? canvas;
|
|
280
|
+
const targetWidth = Math.max(1, Math.floor(sourceCanvas.width * scale));
|
|
281
|
+
const targetHeight = Math.max(1, Math.floor(sourceCanvas.height * scale));
|
|
186
282
|
const copy = document.createElement("canvas");
|
|
187
283
|
copy.width = targetWidth;
|
|
188
284
|
copy.height = targetHeight;
|
|
189
285
|
if (preserveAlpha) copy.dataset.preserveAlpha = "true";
|
|
190
286
|
const ctx = copy.getContext("2d");
|
|
191
|
-
if (ctx &&
|
|
287
|
+
if (ctx && sourceCanvas.width > 0 && sourceCanvas.height > 0) {
|
|
288
|
+
const glPixels = document.hidden ? readWebGLPixels(sourceCanvas) : null;
|
|
289
|
+
if (glPixels) {
|
|
290
|
+
const srcW = sourceCanvas.width;
|
|
291
|
+
const srcH = sourceCanvas.height;
|
|
292
|
+
const imageData = new ImageData(glPixels, srcW, srcH);
|
|
293
|
+
if (targetWidth === srcW && targetHeight === srcH) ctx.putImageData(imageData, 0, 0);
|
|
294
|
+
else {
|
|
295
|
+
const temp = document.createElement("canvas");
|
|
296
|
+
temp.width = srcW;
|
|
297
|
+
temp.height = srcH;
|
|
298
|
+
temp.getContext("2d").putImageData(imageData, 0, 0);
|
|
299
|
+
ctx.drawImage(temp, 0, 0, targetWidth, targetHeight);
|
|
300
|
+
}
|
|
301
|
+
} else ctx.drawImage(sourceCanvas, 0, 0, targetWidth, targetHeight);
|
|
302
|
+
}
|
|
192
303
|
return copy;
|
|
193
304
|
}
|
|
194
305
|
/**
|
|
195
306
|
* Serialize a canvas element as an <img> with base64 data URL.
|
|
196
307
|
* Creates a snapshot of current pixels before async encoding to prevent race conditions.
|
|
197
|
-
*
|
|
308
|
+
*
|
|
198
309
|
* OPTIMIZATION: Calculate optimal encoding resolution based on:
|
|
199
310
|
* 1. CSS display size (how big it actually appears)
|
|
200
311
|
* 2. Video export scale (output resolution multiplier)
|
|
201
312
|
* 3. Quality multiplier (for sharpness, default 1.5x)
|
|
202
313
|
*/
|
|
203
314
|
function serializeCanvas(sourceElement, canvas, parts, canvasJobs, options) {
|
|
204
|
-
const
|
|
205
|
-
const
|
|
315
|
+
const sourceCanvas = findCaptureProxy(canvas) ?? canvas;
|
|
316
|
+
const width = sourceCanvas.width;
|
|
317
|
+
const height = sourceCanvas.height;
|
|
206
318
|
if (width === 0 || height === 0) return;
|
|
207
|
-
const styleStr = serializeComputedStyles(sourceElement);
|
|
208
319
|
const computedStyle = getComputedStyle(sourceElement);
|
|
320
|
+
const styleStr = serializeComputedStyles(sourceElement, computedStyle);
|
|
209
321
|
const computedWidth = computedStyle.width;
|
|
210
322
|
const computedHeight = computedStyle.height;
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
323
|
+
const filteredParts = (styleStr ? styleStr.split(";").filter((s) => s.trim()) : []).filter((s) => {
|
|
324
|
+
const trimmed = s.trim();
|
|
325
|
+
return !trimmed.startsWith("width:") && !trimmed.startsWith("height:");
|
|
326
|
+
});
|
|
327
|
+
const displayWidth = computedWidth || `${width}px`;
|
|
328
|
+
const displayHeight = computedHeight || `${height}px`;
|
|
329
|
+
filteredParts.push(`width:${displayWidth}`);
|
|
330
|
+
filteredParts.push(`height:${displayHeight}`);
|
|
331
|
+
filteredParts.push(`display:block`);
|
|
332
|
+
const finalStyle = filteredParts.join(";");
|
|
218
333
|
const preserveAlpha = shouldPreserveAlpha(sourceElement);
|
|
219
|
-
let optimalScale = options.
|
|
220
|
-
const qualityMultiplier = 1.5;
|
|
334
|
+
let optimalScale = options.scaleConfig.exportScale;
|
|
221
335
|
try {
|
|
222
|
-
const cssWidth = parseFloat(computedWidth) ||
|
|
223
|
-
const cssHeight = parseFloat(computedHeight) ||
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
336
|
+
const cssWidth = parseFloat(computedWidth) || sourceCanvas.width;
|
|
337
|
+
const cssHeight = parseFloat(computedHeight) || sourceCanvas.height;
|
|
338
|
+
optimalScale = options.scaleConfig.computeCanvasScale({
|
|
339
|
+
naturalWidth: sourceCanvas.width,
|
|
340
|
+
naturalHeight: sourceCanvas.height,
|
|
341
|
+
displayWidth: cssWidth,
|
|
342
|
+
displayHeight: cssHeight
|
|
343
|
+
});
|
|
228
344
|
} catch (e) {
|
|
229
345
|
console.warn(`[serializeCanvas] Failed to get computed style for ${sourceElement.tagName}:`, e);
|
|
230
346
|
}
|
|
231
347
|
const snapshot = snapshotCanvas(canvas, optimalScale, preserveAlpha);
|
|
232
348
|
parts.push(`<img style="${escapeXML(finalStyle)}" src="`);
|
|
233
349
|
const promiseIndex = parts.length;
|
|
234
|
-
|
|
235
|
-
sourceMap.set(snapshot, sourceElement);
|
|
350
|
+
options.sourceMap.set(snapshot, sourceElement);
|
|
236
351
|
const encodePromise = encodeCanvasesInParallel([snapshot], {
|
|
237
352
|
scale: 1,
|
|
238
353
|
renderContext: options.renderContext,
|
|
239
|
-
sourceMap
|
|
354
|
+
sourceMap: options.sourceMap
|
|
240
355
|
}).then((results) => results[0]?.dataUrl || "");
|
|
241
356
|
parts.push(encodePromise);
|
|
242
357
|
canvasJobs.push({
|
|
@@ -280,7 +395,8 @@ function serializeElement(element, parts, canvasJobs, options, parentIsSVG = fal
|
|
|
280
395
|
serializeSlottedContent(slotHost, parts, canvasJobs, options, parentIsSVG);
|
|
281
396
|
return;
|
|
282
397
|
}
|
|
283
|
-
if (!
|
|
398
|
+
if (!isVisibleAtTime(element, options.timeMs)) return;
|
|
399
|
+
if (isTemporal(element) && element.style?.getPropertyValue("display") === "none") return;
|
|
284
400
|
if (element.tagName.includes("-") && element.shadowRoot) {
|
|
285
401
|
const shadowCanvas = element.shadowRoot.querySelector("canvas");
|
|
286
402
|
if (shadowCanvas) {
|
|
@@ -292,9 +408,11 @@ function serializeElement(element, parts, canvasJobs, options, parentIsSVG = fal
|
|
|
292
408
|
serializeImageAsCanvas(element, shadowImg, parts, canvasJobs, options);
|
|
293
409
|
return;
|
|
294
410
|
}
|
|
295
|
-
const
|
|
411
|
+
const computedStyle = getComputedStyle(element);
|
|
412
|
+
let computedDisplay = computedStyle.display;
|
|
413
|
+
if (computedDisplay === "none") computedDisplay = resolveNaturalDisplay(element);
|
|
296
414
|
const containerTag = computedDisplay === "inline" || computedDisplay === "inline-block" || computedDisplay === "inline-flex" ? "span" : "div";
|
|
297
|
-
let styleStr$1 = serializeComputedStyles(element);
|
|
415
|
+
let styleStr$1 = serializeComputedStyles(element, computedStyle);
|
|
298
416
|
parts.push(`<${containerTag}`);
|
|
299
417
|
for (const attr of element.attributes) {
|
|
300
418
|
const name = attr.name.toLowerCase();
|
|
@@ -334,46 +452,84 @@ function serializeElement(element, parts, canvasJobs, options, parentIsSVG = fal
|
|
|
334
452
|
parts.push(`</${tagName}>`);
|
|
335
453
|
}
|
|
336
454
|
/**
|
|
337
|
-
*
|
|
338
|
-
*
|
|
455
|
+
* TextEncoder instance for SVG-to-base64 encoding.
|
|
456
|
+
* encode() converts to UTF-8 bytes in a single native call, then we
|
|
457
|
+
* base64-encode the bytes. ~33% overhead vs ~200% for percent-encoding.
|
|
339
458
|
*/
|
|
340
|
-
|
|
341
|
-
if (!isVisibleAtTime(element, timeMs)) return false;
|
|
342
|
-
return true;
|
|
343
|
-
}
|
|
459
|
+
const textEncoder = new TextEncoder();
|
|
344
460
|
/**
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
461
|
+
* Synchronous DOM capture phase. Walks the element tree, snapshots canvas
|
|
462
|
+
* pixels, and kicks off async encoding. Returns parts array containing
|
|
463
|
+
* string fragments and encoding promises.
|
|
464
|
+
*
|
|
465
|
+
* After this function returns, the source element's DOM is no longer
|
|
466
|
+
* referenced — the clone can safely be seeked to the next frame.
|
|
467
|
+
*
|
|
468
|
+
* SCALING ARCHITECTURE (unified via ScaleConfig):
|
|
469
|
+
*
|
|
470
|
+
* ScaleConfig centralizes all scaling logic and provides:
|
|
471
|
+
* 1. Output SVG dimensions (width * exportScale, height * exportScale)
|
|
472
|
+
* 2. DOM scaling wrapper (CSS transform:scale when exportScale < 1)
|
|
473
|
+
* 3. Per-canvas optimal encoding scale via computeCanvasScale()
|
|
474
|
+
*
|
|
475
|
+
* Canvas scaling is independent from DOM scaling because:
|
|
476
|
+
* - Canvas elements have intrinsic pixel dimensions and can be downsampled
|
|
477
|
+
* efficiently before encoding (prevents encoding 1920px at full resolution
|
|
478
|
+
* when displayed at 420px)
|
|
479
|
+
* - DOM content has no intrinsic resolution and must be scaled via CSS
|
|
480
|
+
* transforms, which the browser handles during SVG foreignObject rendering
|
|
481
|
+
*
|
|
482
|
+
* Example: 1920x1080 @ 0.5 export scale
|
|
483
|
+
* - Output SVG: 960x540
|
|
484
|
+
* - DOM wrapper: transform:scale(0.5) on 1920x1080 content
|
|
485
|
+
* - Canvas (1920px displayed at 420px): encoded at ~0.16x (315px)
|
|
486
|
+
* via computeCanvasScale(420/1920 * 0.5 * 1.5 quality = 0.164)
|
|
352
487
|
*/
|
|
353
|
-
|
|
488
|
+
function captureElementParts(element, width, height, options) {
|
|
354
489
|
const parts = [];
|
|
355
490
|
const canvasJobs = [];
|
|
356
|
-
const
|
|
357
|
-
|
|
491
|
+
const sourceMap = /* @__PURE__ */ new WeakMap();
|
|
492
|
+
const scaleConfig = ScaleConfig.fromOptions(width, height, options.canvasScale);
|
|
493
|
+
const documentStyles = options.renderContext?.getCachedDocumentStyles() ?? collectDocumentStyles();
|
|
494
|
+
if (options.renderContext && documentStyles) options.renderContext.setCachedDocumentStyles(documentStyles);
|
|
495
|
+
parts.push(`<div xmlns="http://www.w3.org/1999/xhtml" style="width:${scaleConfig.outputWidth}px;height:${scaleConfig.outputHeight}px;overflow:hidden;position:relative;">`);
|
|
358
496
|
if (documentStyles) parts.push(`<style type="text/css"><![CDATA[${documentStyles}]]></style>`);
|
|
359
|
-
|
|
497
|
+
const domTransform = scaleConfig.getDOMTransform();
|
|
498
|
+
if (domTransform) {
|
|
499
|
+
const wrapperDims = scaleConfig.getDOMWrapperDimensions();
|
|
500
|
+
parts.push(`<div style="transform:${domTransform};transform-origin:0 0;width:${wrapperDims.width}px;height:${wrapperDims.height}px;">`);
|
|
501
|
+
}
|
|
502
|
+
serializeElement(element, parts, canvasJobs, {
|
|
503
|
+
renderContext: options.renderContext,
|
|
504
|
+
timeMs: options.timeMs,
|
|
505
|
+
scaleConfig,
|
|
506
|
+
sourceMap
|
|
507
|
+
});
|
|
508
|
+
if (domTransform) parts.push("</div>");
|
|
360
509
|
parts.push("</div>");
|
|
361
|
-
return
|
|
510
|
+
return parts;
|
|
362
511
|
}
|
|
363
512
|
/**
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
* @returns SVG data URI
|
|
513
|
+
* Synchronous capture with deferred data URI encoding.
|
|
514
|
+
*
|
|
515
|
+
* Walks the DOM and snapshots canvas pixels synchronously, then returns
|
|
516
|
+
* a promise that resolves to the SVG data URI once async canvas-to-base64
|
|
517
|
+
* encoding completes. The source element is NOT referenced after this
|
|
518
|
+
* function returns — the caller can immediately mutate/seek the clone.
|
|
371
519
|
*/
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
|
|
520
|
+
function captureTimelineToDataUri(element, width, height, options) {
|
|
521
|
+
const scaleConfig = ScaleConfig.fromOptions(width, height, options.canvasScale);
|
|
522
|
+
const parts = captureElementParts(element, width, height, options);
|
|
523
|
+
return Promise.all(parts).then((resolvedParts) => {
|
|
524
|
+
const xhtml = resolvedParts.join("");
|
|
525
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${scaleConfig.outputWidth}" height="${scaleConfig.outputHeight}"><foreignObject x="0" y="0" width="${scaleConfig.outputWidth}" height="${scaleConfig.outputHeight}">${xhtml}</foreignObject></svg>`;
|
|
526
|
+
const bytes = textEncoder.encode(svg);
|
|
527
|
+
let binary = "";
|
|
528
|
+
for (let i = 0; i < bytes.length; i += 8192) binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 8192));
|
|
529
|
+
return `data:image/svg+xml;base64,${btoa(binary)}`;
|
|
530
|
+
});
|
|
375
531
|
}
|
|
376
532
|
|
|
377
533
|
//#endregion
|
|
378
|
-
export {
|
|
534
|
+
export { captureTimelineToDataUri };
|
|
379
535
|
//# sourceMappingURL=serializeTimelineDirect.js.map
|