@editframe/elements 0.45.1 → 0.45.3
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/DelayedLoadingState.js.map +1 -1
- package/dist/EF_FRAMEGEN.js.map +1 -1
- package/dist/EF_RENDERING.js.map +1 -1
- package/dist/canvas/EFCanvas.js +3 -3
- package/dist/canvas/EFCanvas.js.map +1 -1
- package/dist/canvas/EFCanvasItem.js.map +1 -1
- package/dist/canvas/api/CanvasAPI.js.map +1 -1
- package/dist/canvas/getElementBounds.js.map +1 -1
- package/dist/canvas/overlays/SelectionOverlay.js.map +1 -1
- package/dist/canvas/overlays/overlayState.js.map +1 -1
- package/dist/canvas/selection/SelectionController.js +25 -23
- package/dist/canvas/selection/SelectionController.js.map +1 -1
- package/dist/canvas/selection/SelectionModel.js.map +1 -1
- package/dist/canvas/selection/selectionContext.js.map +1 -1
- package/dist/elements/ContainerInfo.js.map +1 -1
- package/dist/elements/CrossUpdateController.js.map +1 -1
- package/dist/elements/EFAudio.d.ts +2 -2
- package/dist/elements/EFAudio.js.map +1 -1
- package/dist/elements/EFCaptions.d.ts +2 -2
- package/dist/elements/EFCaptions.js.map +1 -1
- package/dist/elements/EFImage.d.ts +4 -4
- package/dist/elements/EFImage.js +1 -1
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.js.map +1 -1
- package/dist/elements/EFMedia/CachedFetcher.js.map +1 -1
- package/dist/elements/EFMedia/MediaEngine.js.map +1 -1
- package/dist/elements/EFMedia/SegmentIndex.js.map +1 -1
- package/dist/elements/EFMedia/SegmentTransport.js.map +1 -1
- package/dist/elements/EFMedia/TimingModel.js.map +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/GlobalInputCache.js.map +1 -1
- package/dist/elements/EFMedia/shared/PrecisionUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
- package/dist/elements/EFMedia.js.map +1 -1
- package/dist/elements/EFPanZoom.js +9 -8
- package/dist/elements/EFPanZoom.js.map +1 -1
- package/dist/elements/EFSourceMixin.js.map +1 -1
- package/dist/elements/EFSurface.js.map +1 -1
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFText.d.ts +4 -4
- package/dist/elements/EFText.js.map +1 -1
- package/dist/elements/EFTextSegment.d.ts +4 -4
- package/dist/elements/EFTimegroup.d.ts +4 -4
- package/dist/elements/EFTimegroup.js +7 -8
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.d.ts +4 -4
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/elements/EFWaveform.d.ts +4 -4
- 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/SampleBuffer.js.map +1 -1
- package/dist/elements/TargetController.js.map +1 -1
- package/dist/elements/TimegroupController.js.map +1 -1
- package/dist/elements/cloneFactoryRegistry.js.map +1 -1
- package/dist/elements/durationConverter.js.map +1 -1
- package/dist/elements/easingUtils.js.map +1 -1
- package/dist/elements/renderTemporalAudio.js.map +1 -1
- package/dist/elements/setupTemporalHierarchy.js.map +1 -1
- package/dist/elements/updateAnimations.js +1 -1
- package/dist/elements/updateAnimations.js.map +1 -1
- package/dist/getRenderInfo.js.map +1 -1
- package/dist/gui/ContextMixin.js.map +1 -1
- package/dist/gui/Controllable.js.map +1 -1
- package/dist/gui/EFActiveRootTemporal.js.map +1 -1
- package/dist/gui/EFConfiguration.d.ts +4 -4
- package/dist/gui/EFControls.js.map +1 -1
- package/dist/gui/EFFilmstrip.js.map +1 -1
- package/dist/gui/EFFitScale.js.map +1 -1
- package/dist/gui/EFOverlayItem.js.map +1 -1
- package/dist/gui/EFOverlayLayer.js.map +1 -1
- package/dist/gui/EFPreview.js.map +1 -1
- package/dist/gui/EFResizableBox.js.map +1 -1
- package/dist/gui/EFScrubber.js.map +1 -1
- package/dist/gui/EFTimeDisplay.js.map +1 -1
- package/dist/gui/EFTimelineRuler.js.map +1 -1
- package/dist/gui/EFTogglePlay.js.map +1 -1
- package/dist/gui/EFTransformHandles.js.map +1 -1
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/FitScaleHelpers.js.map +1 -1
- package/dist/gui/PlaybackController.js.map +1 -1
- package/dist/gui/TWMixin2.js.map +1 -1
- package/dist/gui/TargetOrContextMixin.js.map +1 -1
- package/dist/gui/currentTimeContext.js.map +1 -1
- package/dist/gui/efContext.js.map +1 -1
- package/dist/gui/fetchContext.js.map +1 -1
- package/dist/gui/hierarchy/EFHierarchy.js.map +1 -1
- package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -1
- package/dist/gui/hierarchy/hierarchyContext.js.map +1 -1
- package/dist/gui/panZoomTransformContext.js.map +1 -1
- package/dist/gui/previewSettingsContext.js.map +1 -1
- package/dist/gui/theme.js.map +1 -1
- package/dist/gui/timeline/EFTimeline.js +0 -1
- package/dist/gui/timeline/EFTimeline.js.map +1 -1
- package/dist/gui/timeline/EFTimelineRow.js.map +1 -1
- package/dist/gui/timeline/TrimHandles.js.map +1 -1
- package/dist/gui/timeline/flattenHierarchy.js.map +1 -1
- package/dist/gui/timeline/timelineStateContext.js.map +1 -1
- package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -1
- package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TextTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/TrackItem.js.map +1 -1
- package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -1
- package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -1
- package/dist/gui/transformCalculations.js.map +1 -1
- package/dist/gui/transformUtils.js.map +1 -1
- package/dist/gui/tree/EFTree.js.map +1 -1
- package/dist/gui/tree/EFTreeItem.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/otel/BridgeSpanExporter.js.map +1 -1
- package/dist/otel/setupBrowserTracing.js.map +1 -1
- package/dist/otel/tracingHelpers.js.map +1 -1
- package/dist/preview/AdaptiveResolutionTracker.js.map +1 -1
- package/dist/preview/FrameController.js.map +1 -1
- package/dist/preview/QualityUpgradeScheduler.js.map +1 -1
- package/dist/preview/RenderContext.js.map +1 -1
- package/dist/preview/RenderProfiler.js.map +1 -1
- package/dist/preview/RenderStats.js.map +1 -1
- package/dist/preview/encoding/canvasEncoder.js.map +1 -1
- package/dist/preview/encoding/mainThreadEncoder.js +1 -1
- package/dist/preview/encoding/mainThreadEncoder.js.map +1 -1
- package/dist/preview/previewSettings.js.map +1 -1
- package/dist/preview/previewTypes.js.map +1 -1
- package/dist/preview/renderElementToCanvas.js.map +1 -1
- package/dist/preview/renderTimegroupToCanvas.js +2 -44
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
- package/dist/preview/renderTimegroupToVideo.js +2 -2
- package/dist/preview/renderTimegroupToVideo.js.map +1 -1
- package/dist/preview/renderVideoToVideo.js +2 -2
- package/dist/preview/renderVideoToVideo.js.map +1 -1
- package/dist/preview/renderers.js.map +1 -1
- package/dist/preview/rendering/ScaleConfig.js.map +1 -1
- package/dist/preview/rendering/loadImage.js.map +1 -1
- package/dist/preview/rendering/renderToImageNative.js.map +1 -1
- package/dist/preview/rendering/serializeTimelineDirect.js +1 -1
- package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -1
- package/dist/preview/statsTrackingStrategy.js.map +1 -1
- package/dist/preview/workers/WorkerPool.js.map +1 -1
- package/dist/render/EFRenderAPI.js.map +1 -1
- package/dist/transcoding/cache/RequestDeduplicator.js.map +1 -1
- package/dist/utils/LRUCache.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QualityUpgradeScheduler.js","names":["#requestFrameRender","#maxConcurrent","#isCached","#abortController","#queue","#activeTasks","#completedTasks","#processQueue","#startTask","results: UpgradeTaskStatus[]"],"sources":["../../src/preview/QualityUpgradeScheduler.ts"],"sourcesContent":["/**\n * QualityUpgradeScheduler: Centralized deadline-ordered work queue\n *\n * Coordinates main-quality segment fetching across multiple video elements.\n * Generic scheduler that doesn't understand media concepts (segments, renditions, etc.)\n * - only processes { key, deadlineMs, fetch, owner } tuples.\n *\n * Design principles:\n * - Deadline-based ordering: always process nearest deadline first\n * - Ground-truth cache validation: check cache before starting any fetch\n * - In-flight fetches never cancelled: they populate shared cache\n * - Event-driven: elements submit tasks only on state changes, not every frame\n */\n\nexport interface UpgradeTask {\n /** Opaque dedup key (e.g. \"${owner}:${segmentId}:${renditionId}\") */\n key: string;\n /** Fetch function that populates the cache */\n fetch: (signal: AbortSignal) => Promise<void>;\n /** Timeline time when this segment will be needed */\n deadlineMs: number;\n /** Element ID, for bulk operations */\n owner: string;\n}\n\nexport interface UpgradeTaskStatus {\n key: string;\n owner: string;\n deadlineMs: number;\n status: \"queued\" | \"active\" | \"completed\" | \"failed\";\n error?: string;\n}\n\nexport interface OwnerProgress {\n queued: number;\n active: number;\n completed: number;\n failed: number;\n}\n\ninterface ActiveTask {\n task: UpgradeTask;\n startedAt: number;\n promise: Promise<void>;\n}\n\ninterface CompletedTask {\n key: string;\n owner: string;\n status: \"completed\" | \"failed\";\n error?: string;\n}\n\nexport class QualityUpgradeScheduler {\n #maxConcurrent: number;\n #queue: UpgradeTask[] = [];\n #activeTasks = new Map<string, ActiveTask>();\n #completedTasks = new Map<string, CompletedTask>();\n #abortController: AbortController;\n #requestFrameRender: () => void;\n #isCached?: (key: string) => boolean;\n\n constructor(options: {\n requestFrameRender: () => void;\n maxConcurrent?: number;\n isCached?: (key: string) => boolean;\n }) {\n this.#requestFrameRender = options.requestFrameRender;\n this.#maxConcurrent = options.maxConcurrent ?? 4;\n this.#isCached = options.isCached;\n this.#abortController = new AbortController();\n }\n\n /**\n * Add tasks without affecting existing ones (additive).\n * Used for lookahead extension during playback.\n */\n enqueue(tasks: UpgradeTask[]): void {\n if (this.#abortController.signal.aborted) return;\n\n for (const task of tasks) {\n // Skip if already queued, active, or completed\n if (\n this.#queue.some((t) => t.key === task.key) ||\n this.#activeTasks.has(task.key) ||\n this.#completedTasks.has(task.key)\n ) {\n continue;\n }\n\n this.#queue.push(task);\n }\n\n // Sort queue by deadline (ascending)\n this.#queue.sort((a, b) => a.deadlineMs - b.deadlineMs);\n\n // Start processing if we have capacity\n this.#processQueue();\n }\n\n /**\n * Replace all queued tasks for an owner.\n * Used on seeks, trim changes, timeline position changes where old deadlines are stale.\n * Does NOT cancel in-flight tasks (they populate shared cache).\n */\n replaceForOwner(owner: string, tasks: UpgradeTask[]): void {\n if (this.#abortController.signal.aborted) return;\n\n // Remove queued (not active) tasks for this owner\n this.#queue = this.#queue.filter((t) => t.owner !== owner);\n\n // Add new tasks\n for (const task of tasks) {\n // Skip only if the fetch is already in-flight — it will populate the\n // cache when it completes. Completed tasks are intentionally NOT skipped\n // here so that cache eviction is handled correctly: if a segment was\n // previously fetched but has since been evicted from the LRU cache,\n // #computeLookaheadSegments will include it again and it must re-run.\n if (this.#activeTasks.has(task.key)) {\n continue;\n }\n\n this.#queue.push(task);\n }\n\n // Sort queue by deadline (ascending)\n this.#queue.sort((a, b) => a.deadlineMs - b.deadlineMs);\n\n // Start processing if we have capacity\n this.#processQueue();\n }\n\n /**\n * Cancel all tasks for an owner.\n * Removes queued tasks. Does NOT abort in-flight fetches.\n */\n cancelForOwner(owner: string): void {\n // Remove from queue\n this.#queue = this.#queue.filter((t) => t.owner !== owner);\n\n // Remove from completed tracking (allows resubmission)\n for (const [key, task] of this.#completedTasks.entries()) {\n if (task.owner === owner) {\n this.#completedTasks.delete(key);\n }\n }\n\n // Note: we do NOT cancel active tasks - they populate the shared cache\n }\n\n /**\n * Process the queue - start tasks up to maxConcurrent limit.\n */\n #processQueue(): void {\n if (this.#abortController.signal.aborted) return;\n\n while (\n this.#activeTasks.size < this.#maxConcurrent &&\n this.#queue.length > 0\n ) {\n const task = this.#queue.shift();\n if (!task) break;\n\n // Ground-truth cache check before starting\n if (this.#isCached?.(task.key)) {\n // Already cached from another path, mark as completed and continue\n this.#completedTasks.set(task.key, {\n key: task.key,\n owner: task.owner,\n status: \"completed\",\n });\n continue;\n }\n\n // Start the task\n this.#startTask(task);\n }\n }\n\n /**\n * Start a single task.\n */\n #startTask(task: UpgradeTask): void {\n const promise = task\n .fetch(this.#abortController.signal)\n .then(() => {\n // Success\n this.#activeTasks.delete(task.key);\n this.#completedTasks.set(task.key, {\n key: task.key,\n owner: task.owner,\n status: \"completed\",\n });\n\n // Trigger re-render so upgraded quality gets displayed\n this.#requestFrameRender();\n\n // Start next task if available\n this.#processQueue();\n })\n .catch((error) => {\n // Failure\n this.#activeTasks.delete(task.key);\n\n // Don't track AbortError as failure (intentional cancellation)\n const isAbortError =\n error instanceof DOMException && error.name === \"AbortError\";\n\n if (!isAbortError) {\n this.#completedTasks.set(task.key, {\n key: task.key,\n owner: task.owner,\n status: \"failed\",\n error: error instanceof Error ? error.message : String(error),\n });\n }\n\n // Continue processing queue even after failure\n this.#processQueue();\n });\n\n this.#activeTasks.set(task.key, {\n task,\n startedAt: performance.now(),\n promise,\n });\n }\n\n /**\n * Check whether a task is currently in-flight (started, not yet complete).\n */\n isActive(key: string): boolean {\n return this.#activeTasks.has(key);\n }\n\n /**\n * Check whether a task is waiting in the queue (submitted but not yet started).\n */\n isPending(key: string): boolean {\n return this.#queue.some((t) => t.key === key);\n }\n\n /**\n * Get snapshot of current queue state for debugging.\n */\n getQueueSnapshot(): UpgradeTaskStatus[] {\n const results: UpgradeTaskStatus[] = [];\n\n // Queued tasks\n for (const task of this.#queue) {\n results.push({\n key: task.key,\n owner: task.owner,\n deadlineMs: task.deadlineMs,\n status: \"queued\",\n });\n }\n\n // Active tasks\n for (const [key, activeTask] of this.#activeTasks.entries()) {\n results.push({\n key,\n owner: activeTask.task.owner,\n deadlineMs: activeTask.task.deadlineMs,\n status: \"active\",\n });\n }\n\n // Completed tasks\n for (const [key, completed] of this.#completedTasks.entries()) {\n results.push({\n key,\n owner: completed.owner,\n deadlineMs: 0, // No longer relevant\n status: completed.status as \"completed\" | \"failed\",\n error: completed.error,\n });\n }\n\n return results;\n }\n\n /**\n * Get progress for a specific owner.\n */\n getOwnerProgress(owner: string): OwnerProgress {\n const queued = this.#queue.filter((t) => t.owner === owner).length;\n\n let active = 0;\n for (const activeTask of this.#activeTasks.values()) {\n if (activeTask.task.owner === owner) {\n active++;\n }\n }\n\n let completed = 0;\n let failed = 0;\n for (const task of this.#completedTasks.values()) {\n if (task.owner === owner) {\n if (task.status === \"completed\") {\n completed++;\n } else {\n failed++;\n }\n }\n }\n\n return { queued, active, completed, failed };\n }\n\n /**\n * Dispose the scheduler - abort all in-flight work.\n */\n dispose(): void {\n // Suppress in-flight task rejections before aborting to avoid unhandled\n // rejection events from the synchronous abort signal firing.\n for (const activeTask of this.#activeTasks.values()) {\n activeTask.promise.catch(() => {});\n }\n this.#abortController.abort();\n this.#queue = [];\n this.#activeTasks.clear();\n this.#completedTasks.clear();\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"QualityUpgradeScheduler.js","names":["#requestFrameRender","#maxConcurrent","#isCached","#abortController","#queue","#activeTasks","#completedTasks","#processQueue","#startTask","results: UpgradeTaskStatus[]"],"sources":["../../src/preview/QualityUpgradeScheduler.ts"],"sourcesContent":["/**\n * QualityUpgradeScheduler: Centralized deadline-ordered work queue\n *\n * Coordinates main-quality segment fetching across multiple video elements.\n * Generic scheduler that doesn't understand media concepts (segments, renditions, etc.)\n * - only processes { key, deadlineMs, fetch, owner } tuples.\n *\n * Design principles:\n * - Deadline-based ordering: always process nearest deadline first\n * - Ground-truth cache validation: check cache before starting any fetch\n * - In-flight fetches never cancelled: they populate shared cache\n * - Event-driven: elements submit tasks only on state changes, not every frame\n */\n\nexport interface UpgradeTask {\n /** Opaque dedup key (e.g. \"${owner}:${segmentId}:${renditionId}\") */\n key: string;\n /** Fetch function that populates the cache */\n fetch: (signal: AbortSignal) => Promise<void>;\n /** Timeline time when this segment will be needed */\n deadlineMs: number;\n /** Element ID, for bulk operations */\n owner: string;\n}\n\nexport interface UpgradeTaskStatus {\n key: string;\n owner: string;\n deadlineMs: number;\n status: \"queued\" | \"active\" | \"completed\" | \"failed\";\n error?: string;\n}\n\nexport interface OwnerProgress {\n queued: number;\n active: number;\n completed: number;\n failed: number;\n}\n\ninterface ActiveTask {\n task: UpgradeTask;\n startedAt: number;\n promise: Promise<void>;\n}\n\ninterface CompletedTask {\n key: string;\n owner: string;\n status: \"completed\" | \"failed\";\n error?: string;\n}\n\nexport class QualityUpgradeScheduler {\n #maxConcurrent: number;\n #queue: UpgradeTask[] = [];\n #activeTasks = new Map<string, ActiveTask>();\n #completedTasks = new Map<string, CompletedTask>();\n #abortController: AbortController;\n #requestFrameRender: () => void;\n #isCached?: (key: string) => boolean;\n\n constructor(options: {\n requestFrameRender: () => void;\n maxConcurrent?: number;\n isCached?: (key: string) => boolean;\n }) {\n this.#requestFrameRender = options.requestFrameRender;\n this.#maxConcurrent = options.maxConcurrent ?? 4;\n this.#isCached = options.isCached;\n this.#abortController = new AbortController();\n }\n\n /**\n * Add tasks without affecting existing ones (additive).\n * Used for lookahead extension during playback.\n */\n enqueue(tasks: UpgradeTask[]): void {\n if (this.#abortController.signal.aborted) return;\n\n for (const task of tasks) {\n // Skip if already queued, active, or completed\n if (\n this.#queue.some((t) => t.key === task.key) ||\n this.#activeTasks.has(task.key) ||\n this.#completedTasks.has(task.key)\n ) {\n continue;\n }\n\n this.#queue.push(task);\n }\n\n // Sort queue by deadline (ascending)\n this.#queue.sort((a, b) => a.deadlineMs - b.deadlineMs);\n\n // Start processing if we have capacity\n this.#processQueue();\n }\n\n /**\n * Replace all queued tasks for an owner.\n * Used on seeks, trim changes, timeline position changes where old deadlines are stale.\n * Does NOT cancel in-flight tasks (they populate shared cache).\n */\n replaceForOwner(owner: string, tasks: UpgradeTask[]): void {\n if (this.#abortController.signal.aborted) return;\n\n // Remove queued (not active) tasks for this owner\n this.#queue = this.#queue.filter((t) => t.owner !== owner);\n\n // Add new tasks\n for (const task of tasks) {\n // Skip only if the fetch is already in-flight — it will populate the\n // cache when it completes. Completed tasks are intentionally NOT skipped\n // here so that cache eviction is handled correctly: if a segment was\n // previously fetched but has since been evicted from the LRU cache,\n // #computeLookaheadSegments will include it again and it must re-run.\n if (this.#activeTasks.has(task.key)) {\n continue;\n }\n\n this.#queue.push(task);\n }\n\n // Sort queue by deadline (ascending)\n this.#queue.sort((a, b) => a.deadlineMs - b.deadlineMs);\n\n // Start processing if we have capacity\n this.#processQueue();\n }\n\n /**\n * Cancel all tasks for an owner.\n * Removes queued tasks. Does NOT abort in-flight fetches.\n */\n cancelForOwner(owner: string): void {\n // Remove from queue\n this.#queue = this.#queue.filter((t) => t.owner !== owner);\n\n // Remove from completed tracking (allows resubmission)\n for (const [key, task] of this.#completedTasks.entries()) {\n if (task.owner === owner) {\n this.#completedTasks.delete(key);\n }\n }\n\n // Note: we do NOT cancel active tasks - they populate the shared cache\n }\n\n /**\n * Process the queue - start tasks up to maxConcurrent limit.\n */\n #processQueue(): void {\n if (this.#abortController.signal.aborted) return;\n\n while (this.#activeTasks.size < this.#maxConcurrent && this.#queue.length > 0) {\n const task = this.#queue.shift();\n if (!task) break;\n\n // Ground-truth cache check before starting\n if (this.#isCached?.(task.key)) {\n // Already cached from another path, mark as completed and continue\n this.#completedTasks.set(task.key, {\n key: task.key,\n owner: task.owner,\n status: \"completed\",\n });\n continue;\n }\n\n // Start the task\n this.#startTask(task);\n }\n }\n\n /**\n * Start a single task.\n */\n #startTask(task: UpgradeTask): void {\n const promise = task\n .fetch(this.#abortController.signal)\n .then(() => {\n // Success\n this.#activeTasks.delete(task.key);\n this.#completedTasks.set(task.key, {\n key: task.key,\n owner: task.owner,\n status: \"completed\",\n });\n\n // Trigger re-render so upgraded quality gets displayed\n this.#requestFrameRender();\n\n // Start next task if available\n this.#processQueue();\n })\n .catch((error) => {\n // Failure\n this.#activeTasks.delete(task.key);\n\n // Don't track AbortError as failure (intentional cancellation)\n const isAbortError = error instanceof DOMException && error.name === \"AbortError\";\n\n if (!isAbortError) {\n this.#completedTasks.set(task.key, {\n key: task.key,\n owner: task.owner,\n status: \"failed\",\n error: error instanceof Error ? error.message : String(error),\n });\n }\n\n // Continue processing queue even after failure\n this.#processQueue();\n });\n\n this.#activeTasks.set(task.key, {\n task,\n startedAt: performance.now(),\n promise,\n });\n }\n\n /**\n * Check whether a task is currently in-flight (started, not yet complete).\n */\n isActive(key: string): boolean {\n return this.#activeTasks.has(key);\n }\n\n /**\n * Check whether a task is waiting in the queue (submitted but not yet started).\n */\n isPending(key: string): boolean {\n return this.#queue.some((t) => t.key === key);\n }\n\n /**\n * Get snapshot of current queue state for debugging.\n */\n getQueueSnapshot(): UpgradeTaskStatus[] {\n const results: UpgradeTaskStatus[] = [];\n\n // Queued tasks\n for (const task of this.#queue) {\n results.push({\n key: task.key,\n owner: task.owner,\n deadlineMs: task.deadlineMs,\n status: \"queued\",\n });\n }\n\n // Active tasks\n for (const [key, activeTask] of this.#activeTasks.entries()) {\n results.push({\n key,\n owner: activeTask.task.owner,\n deadlineMs: activeTask.task.deadlineMs,\n status: \"active\",\n });\n }\n\n // Completed tasks\n for (const [key, completed] of this.#completedTasks.entries()) {\n results.push({\n key,\n owner: completed.owner,\n deadlineMs: 0, // No longer relevant\n status: completed.status as \"completed\" | \"failed\",\n error: completed.error,\n });\n }\n\n return results;\n }\n\n /**\n * Get progress for a specific owner.\n */\n getOwnerProgress(owner: string): OwnerProgress {\n const queued = this.#queue.filter((t) => t.owner === owner).length;\n\n let active = 0;\n for (const activeTask of this.#activeTasks.values()) {\n if (activeTask.task.owner === owner) {\n active++;\n }\n }\n\n let completed = 0;\n let failed = 0;\n for (const task of this.#completedTasks.values()) {\n if (task.owner === owner) {\n if (task.status === \"completed\") {\n completed++;\n } else {\n failed++;\n }\n }\n }\n\n return { queued, active, completed, failed };\n }\n\n /**\n * Dispose the scheduler - abort all in-flight work.\n */\n dispose(): void {\n // Suppress in-flight task rejections before aborting to avoid unhandled\n // rejection events from the synchronous abort signal firing.\n for (const activeTask of this.#activeTasks.values()) {\n activeTask.promise.catch(() => {});\n }\n this.#abortController.abort();\n this.#queue = [];\n this.#activeTasks.clear();\n this.#completedTasks.clear();\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RenderContext.js","names":["#canvasCache","#videoFrameCache","#disposed","#metrics","#getCanvasCacheKey","#getVideoFrameCacheKey","#documentStylesCache"],"sources":["../../src/preview/RenderContext.ts"],"sourcesContent":["/**\n * RenderContext manages scoped caches for the rendering pipeline.\n *\n * Used during foreignObject serialization to cache:\n * - Video frames by source timestamp (useful for freeze frames, slow-mo)\n * - Static element canvases by element identity + renderVersion\n *\n * The context should be created at the start of a render operation\n * and disposed when the render completes (success or failure).\n */\n\nimport { LRUCache } from \"../utils/LRUCache.js\";\nimport type { EFVideo } from \"../elements/EFVideo.js\";\n\n/**\n * Check if an element has a renderVersion property.\n */\nfunction hasRenderVersion(\n element: Element,\n): element is Element & { renderVersion: number } {\n return (\n \"renderVersion\" in element &&\n typeof (element as any).renderVersion === \"number\"\n );\n}\n\n/**\n * Module-level counter for generating unique element IDs.\n * This ensures uniqueness across all RenderContext instances.\n */\nlet nextElementId = 1;\n\n/**\n * WeakMap to store unique IDs for elements.\n * Using WeakMap ensures we don't prevent garbage collection of elements.\n * The ID is stable for the lifetime of the element.\n */\nconst elementUniqueIds = new WeakMap<Element, number>();\n\n/**\n * Get or create a unique ID for an element.\n * This guarantees uniqueness even for elements with the same id attribute\n * or no id attribute at all.\n */\nfunction getElementUniqueId(element: Element): number {\n let id = elementUniqueIds.get(element);\n if (id === undefined) {\n id = nextElementId++;\n elementUniqueIds.set(element, id);\n }\n return id;\n}\n\n/**\n * Result of capturing a video frame.\n */\nexport interface CapturedFrame {\n dataUrl: string;\n width: number;\n height: number;\n}\n\n/**\n * Options for creating a RenderContext.\n */\nexport interface RenderContextOptions {\n /** Maximum number of canvas dataURLs to cache (default: 50) */\n maxCanvasCacheSize?: number;\n /** Maximum number of video frame dataURLs to cache (default: 100) */\n maxVideoFrameCacheSize?: number;\n}\n\n/**\n * RenderContext provides scoped caching for render operations.\n *\n * Create at the start of a render, dispose when complete:\n * ```typescript\n * const context = new RenderContext();\n * try {\n * // ... render operations\n * } finally {\n * context.dispose();\n * }\n * ```\n */\nexport class RenderContext {\n /** Cache for static element canvases (ef-image, ef-waveform) */\n #canvasCache: LRUCache<string, string>;\n\n /** Cache for video frames by source timestamp */\n #videoFrameCache: LRUCache<string, CapturedFrame>;\n\n /** Cache for document styles (computed once per render session) */\n #documentStylesCache: string | null = null;\n\n /** Whether this context has been disposed */\n #disposed = false;\n\n /** Metrics for monitoring cache effectiveness */\n #metrics = {\n canvasCacheHits: 0,\n canvasCacheMisses: 0,\n videoFrameCacheHits: 0,\n videoFrameCacheMisses: 0,\n };\n\n constructor(options: RenderContextOptions = {}) {\n const { maxCanvasCacheSize = 50, maxVideoFrameCacheSize = 100 } = options;\n this.#canvasCache = new LRUCache(maxCanvasCacheSize);\n this.#videoFrameCache = new LRUCache(maxVideoFrameCacheSize);\n }\n\n /**\n * Check if the context has been disposed.\n */\n get disposed(): boolean {\n return this.#disposed;\n }\n\n /**\n * Get cache metrics for monitoring.\n */\n get metrics() {\n return { ...this.#metrics };\n }\n\n // ============================================================================\n // Static Element Cache (ef-image, ef-waveform)\n // ============================================================================\n\n /**\n * Generate a cache key for a static element.\n * Uses a unique element ID (via WeakMap) to ensure uniqueness even if\n * multiple elements have the same id attribute.\n * Returns null if the element doesn't support caching (no renderVersion).\n */\n #getCanvasCacheKey(element: Element): string | null {\n if (!hasRenderVersion(element)) {\n return null;\n }\n // Use unique element ID + render version for guaranteed uniqueness\n const uniqueId = getElementUniqueId(element);\n return `canvas:${uniqueId}:${element.renderVersion}`;\n }\n\n /**\n * Get a cached dataURL for a static element.\n * Returns undefined if not cached or element doesn't support caching.\n */\n getCachedCanvasDataUrl(element: Element): string | undefined {\n if (this.#disposed) return undefined;\n\n const key = this.#getCanvasCacheKey(element);\n if (!key) return undefined;\n\n const cached = this.#canvasCache.get(key);\n if (cached) {\n this.#metrics.canvasCacheHits++;\n } else {\n this.#metrics.canvasCacheMisses++;\n }\n return cached;\n }\n\n /**\n * Cache a dataURL for a static element.\n * Does nothing if the element doesn't support caching.\n */\n setCachedCanvasDataUrl(element: Element, dataUrl: string): void {\n if (this.#disposed) return;\n\n const key = this.#getCanvasCacheKey(element);\n if (key) {\n this.#canvasCache.set(key, dataUrl);\n }\n }\n\n // ============================================================================\n // Video Frame Cache\n // ============================================================================\n\n /**\n * Generate a cache key for a video frame.\n * Uses a unique element ID (via WeakMap) to ensure uniqueness even if\n * multiple videos have the same id attribute.\n */\n #getVideoFrameCacheKey(videoElement: Element, sourceTimeMs: number): string {\n const uniqueId = getElementUniqueId(videoElement);\n // Round to nearest ms to avoid floating point issues\n const roundedTime = Math.round(sourceTimeMs);\n return `video:${uniqueId}:${roundedTime}`;\n }\n\n /**\n * Get a cached video frame.\n * Returns undefined if not cached.\n */\n getCachedVideoFrame(\n videoElement: Element,\n sourceTimeMs: number,\n ): CapturedFrame | undefined {\n if (this.#disposed) return undefined;\n\n const key = this.#getVideoFrameCacheKey(videoElement, sourceTimeMs);\n const cached = this.#videoFrameCache.get(key);\n if (cached) {\n this.#metrics.videoFrameCacheHits++;\n } else {\n this.#metrics.videoFrameCacheMisses++;\n }\n return cached;\n }\n\n /**\n * Cache a video frame.\n */\n setCachedVideoFrame(\n videoElement: Element,\n sourceTimeMs: number,\n frame: CapturedFrame,\n ): void {\n if (this.#disposed) return;\n\n const key = this.#getVideoFrameCacheKey(videoElement, sourceTimeMs);\n this.#videoFrameCache.set(key, frame);\n }\n\n /**\n * Convenience method to get or capture a video frame.\n * Checks cache first, then captures if not cached.\n *\n * @param video - The ef-video element\n * @param sourceTimeMs - Source media timestamp\n * @param options - Capture options including quality and signal\n * @returns The captured frame data\n */\n async getOrCaptureVideoFrame(\n video: EFVideo,\n sourceTimeMs: number,\n options: {\n quality?: \"auto\" | \"scrub\" | \"main\";\n signal?: AbortSignal;\n } = {},\n ): Promise<CapturedFrame> {\n // Check cache first\n const cached = this.getCachedVideoFrame(video, sourceTimeMs);\n if (cached) {\n return cached;\n }\n\n // Capture frame using direct API\n const frame = await video.captureFrameAtSourceTime(sourceTimeMs, options);\n\n // Cache for future use\n this.setCachedVideoFrame(video, sourceTimeMs, frame);\n\n return frame;\n }\n\n // ============================================================================\n // Document Styles Cache\n // ============================================================================\n\n /**\n * Get cached document styles.\n * Returns undefined if not cached.\n */\n getCachedDocumentStyles(): string | undefined {\n if (this.#disposed) return undefined;\n return this.#documentStylesCache ?? undefined;\n }\n\n /**\n * Cache document styles.\n */\n setCachedDocumentStyles(styles: string): void {\n if (this.#disposed) return;\n this.#documentStylesCache = styles;\n }\n\n // ============================================================================\n // Cleanup\n // ============================================================================\n\n /**\n * Dispose the context and clear all caches.\n * Should be called when rendering is complete.\n */\n dispose(): void {\n if (this.#disposed) return;\n\n this.#canvasCache.clear();\n this.#videoFrameCache.clear();\n this.#documentStylesCache = null;\n this.#disposed = true;\n }\n\n /**\n * Symbol.dispose implementation for use with the `using` keyword.\n *\n * @example\n * ```typescript\n * using context = new RenderContext();\n * // ... render operations\n * // context is automatically disposed when scope exits\n * ```\n */\n [Symbol.dispose](): void {\n this.dispose();\n }\n\n /**\n * Get the current size of the canvas cache.\n */\n get canvasCacheSize(): number {\n return this.#canvasCache.size;\n }\n\n /**\n * Get the current size of the video frame cache.\n */\n get videoFrameCacheSize(): number {\n return this.#videoFrameCache.size;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"RenderContext.js","names":["#canvasCache","#videoFrameCache","#disposed","#metrics","#getCanvasCacheKey","#getVideoFrameCacheKey","#documentStylesCache"],"sources":["../../src/preview/RenderContext.ts"],"sourcesContent":["/**\n * RenderContext manages scoped caches for the rendering pipeline.\n *\n * Used during foreignObject serialization to cache:\n * - Video frames by source timestamp (useful for freeze frames, slow-mo)\n * - Static element canvases by element identity + renderVersion\n *\n * The context should be created at the start of a render operation\n * and disposed when the render completes (success or failure).\n */\n\nimport { LRUCache } from \"../utils/LRUCache.js\";\nimport type { EFVideo } from \"../elements/EFVideo.js\";\n\n/**\n * Check if an element has a renderVersion property.\n */\nfunction hasRenderVersion(element: Element): element is Element & { renderVersion: number } {\n return \"renderVersion\" in element && typeof (element as any).renderVersion === \"number\";\n}\n\n/**\n * Module-level counter for generating unique element IDs.\n * This ensures uniqueness across all RenderContext instances.\n */\nlet nextElementId = 1;\n\n/**\n * WeakMap to store unique IDs for elements.\n * Using WeakMap ensures we don't prevent garbage collection of elements.\n * The ID is stable for the lifetime of the element.\n */\nconst elementUniqueIds = new WeakMap<Element, number>();\n\n/**\n * Get or create a unique ID for an element.\n * This guarantees uniqueness even for elements with the same id attribute\n * or no id attribute at all.\n */\nfunction getElementUniqueId(element: Element): number {\n let id = elementUniqueIds.get(element);\n if (id === undefined) {\n id = nextElementId++;\n elementUniqueIds.set(element, id);\n }\n return id;\n}\n\n/**\n * Result of capturing a video frame.\n */\nexport interface CapturedFrame {\n dataUrl: string;\n width: number;\n height: number;\n}\n\n/**\n * Options for creating a RenderContext.\n */\nexport interface RenderContextOptions {\n /** Maximum number of canvas dataURLs to cache (default: 50) */\n maxCanvasCacheSize?: number;\n /** Maximum number of video frame dataURLs to cache (default: 100) */\n maxVideoFrameCacheSize?: number;\n}\n\n/**\n * RenderContext provides scoped caching for render operations.\n *\n * Create at the start of a render, dispose when complete:\n * ```typescript\n * const context = new RenderContext();\n * try {\n * // ... render operations\n * } finally {\n * context.dispose();\n * }\n * ```\n */\nexport class RenderContext {\n /** Cache for static element canvases (ef-image, ef-waveform) */\n #canvasCache: LRUCache<string, string>;\n\n /** Cache for video frames by source timestamp */\n #videoFrameCache: LRUCache<string, CapturedFrame>;\n\n /** Cache for document styles (computed once per render session) */\n #documentStylesCache: string | null = null;\n\n /** Whether this context has been disposed */\n #disposed = false;\n\n /** Metrics for monitoring cache effectiveness */\n #metrics = {\n canvasCacheHits: 0,\n canvasCacheMisses: 0,\n videoFrameCacheHits: 0,\n videoFrameCacheMisses: 0,\n };\n\n constructor(options: RenderContextOptions = {}) {\n const { maxCanvasCacheSize = 50, maxVideoFrameCacheSize = 100 } = options;\n this.#canvasCache = new LRUCache(maxCanvasCacheSize);\n this.#videoFrameCache = new LRUCache(maxVideoFrameCacheSize);\n }\n\n /**\n * Check if the context has been disposed.\n */\n get disposed(): boolean {\n return this.#disposed;\n }\n\n /**\n * Get cache metrics for monitoring.\n */\n get metrics() {\n return { ...this.#metrics };\n }\n\n // ============================================================================\n // Static Element Cache (ef-image, ef-waveform)\n // ============================================================================\n\n /**\n * Generate a cache key for a static element.\n * Uses a unique element ID (via WeakMap) to ensure uniqueness even if\n * multiple elements have the same id attribute.\n * Returns null if the element doesn't support caching (no renderVersion).\n */\n #getCanvasCacheKey(element: Element): string | null {\n if (!hasRenderVersion(element)) {\n return null;\n }\n // Use unique element ID + render version for guaranteed uniqueness\n const uniqueId = getElementUniqueId(element);\n return `canvas:${uniqueId}:${element.renderVersion}`;\n }\n\n /**\n * Get a cached dataURL for a static element.\n * Returns undefined if not cached or element doesn't support caching.\n */\n getCachedCanvasDataUrl(element: Element): string | undefined {\n if (this.#disposed) return undefined;\n\n const key = this.#getCanvasCacheKey(element);\n if (!key) return undefined;\n\n const cached = this.#canvasCache.get(key);\n if (cached) {\n this.#metrics.canvasCacheHits++;\n } else {\n this.#metrics.canvasCacheMisses++;\n }\n return cached;\n }\n\n /**\n * Cache a dataURL for a static element.\n * Does nothing if the element doesn't support caching.\n */\n setCachedCanvasDataUrl(element: Element, dataUrl: string): void {\n if (this.#disposed) return;\n\n const key = this.#getCanvasCacheKey(element);\n if (key) {\n this.#canvasCache.set(key, dataUrl);\n }\n }\n\n // ============================================================================\n // Video Frame Cache\n // ============================================================================\n\n /**\n * Generate a cache key for a video frame.\n * Uses a unique element ID (via WeakMap) to ensure uniqueness even if\n * multiple videos have the same id attribute.\n */\n #getVideoFrameCacheKey(videoElement: Element, sourceTimeMs: number): string {\n const uniqueId = getElementUniqueId(videoElement);\n // Round to nearest ms to avoid floating point issues\n const roundedTime = Math.round(sourceTimeMs);\n return `video:${uniqueId}:${roundedTime}`;\n }\n\n /**\n * Get a cached video frame.\n * Returns undefined if not cached.\n */\n getCachedVideoFrame(videoElement: Element, sourceTimeMs: number): CapturedFrame | undefined {\n if (this.#disposed) return undefined;\n\n const key = this.#getVideoFrameCacheKey(videoElement, sourceTimeMs);\n const cached = this.#videoFrameCache.get(key);\n if (cached) {\n this.#metrics.videoFrameCacheHits++;\n } else {\n this.#metrics.videoFrameCacheMisses++;\n }\n return cached;\n }\n\n /**\n * Cache a video frame.\n */\n setCachedVideoFrame(videoElement: Element, sourceTimeMs: number, frame: CapturedFrame): void {\n if (this.#disposed) return;\n\n const key = this.#getVideoFrameCacheKey(videoElement, sourceTimeMs);\n this.#videoFrameCache.set(key, frame);\n }\n\n /**\n * Convenience method to get or capture a video frame.\n * Checks cache first, then captures if not cached.\n *\n * @param video - The ef-video element\n * @param sourceTimeMs - Source media timestamp\n * @param options - Capture options including quality and signal\n * @returns The captured frame data\n */\n async getOrCaptureVideoFrame(\n video: EFVideo,\n sourceTimeMs: number,\n options: {\n quality?: \"auto\" | \"scrub\" | \"main\";\n signal?: AbortSignal;\n } = {},\n ): Promise<CapturedFrame> {\n // Check cache first\n const cached = this.getCachedVideoFrame(video, sourceTimeMs);\n if (cached) {\n return cached;\n }\n\n // Capture frame using direct API\n const frame = await video.captureFrameAtSourceTime(sourceTimeMs, options);\n\n // Cache for future use\n this.setCachedVideoFrame(video, sourceTimeMs, frame);\n\n return frame;\n }\n\n // ============================================================================\n // Document Styles Cache\n // ============================================================================\n\n /**\n * Get cached document styles.\n * Returns undefined if not cached.\n */\n getCachedDocumentStyles(): string | undefined {\n if (this.#disposed) return undefined;\n return this.#documentStylesCache ?? undefined;\n }\n\n /**\n * Cache document styles.\n */\n setCachedDocumentStyles(styles: string): void {\n if (this.#disposed) return;\n this.#documentStylesCache = styles;\n }\n\n // ============================================================================\n // Cleanup\n // ============================================================================\n\n /**\n * Dispose the context and clear all caches.\n * Should be called when rendering is complete.\n */\n dispose(): void {\n if (this.#disposed) return;\n\n this.#canvasCache.clear();\n this.#videoFrameCache.clear();\n this.#documentStylesCache = null;\n this.#disposed = true;\n }\n\n /**\n * Symbol.dispose implementation for use with the `using` keyword.\n *\n * @example\n * ```typescript\n * using context = new RenderContext();\n * // ... render operations\n * // context is automatically disposed when scope exits\n * ```\n */\n [Symbol.dispose](): void {\n this.dispose();\n }\n\n /**\n * Get the current size of the canvas cache.\n */\n get canvasCacheSize(): number {\n return this.#canvasCache.size;\n }\n\n /**\n * Get the current size of the video frame cache.\n */\n get videoFrameCacheSize(): number {\n return this.#videoFrameCache.size;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RenderProfiler.js","names":["parts: string[]"],"sources":["../../src/preview/RenderProfiler.ts"],"sourcesContent":["/**\n * Profiling utility for render operations.\n * Centralizes timing accumulation and logging to keep business logic clean.\n */\n\n/** Interval between profiling log outputs (ms) */\nconst DEFAULT_LOG_INTERVAL_MS = 2000;\n\n/** Interval for periodic frame logging (every N frames) */\nconst DEFAULT_FRAME_LOG_INTERVAL = 60;\n\n/**\n * Phases tracked during rendering.\n */\nexport interface RenderTimings {\n setup: number;\n draw: number;\n downsample: number;\n canvasEncode: number;\n inline: number;\n serialize: number;\n base64: number;\n imageLoad: number;\n restore: number;\n}\n\n/**\n * Profiler for render operations.\n * Accumulates timing data and provides structured logging.\n */\nexport class RenderProfiler {\n private _renderCount = 0;\n private _lastLogTime = 0;\n private _timingLoggedAt = 0;\n\n private _timings: RenderTimings = {\n setup: 0,\n draw: 0,\n downsample: 0,\n canvasEncode: 0,\n inline: 0,\n serialize: 0,\n base64: 0,\n imageLoad: 0,\n restore: 0,\n };\n\n /**\n * Reset all timing data.\n */\n reset(): void {\n this._renderCount = 0;\n this._lastLogTime = 0;\n this._timingLoggedAt = 0;\n\n for (const key of Object.keys(this._timings) as (keyof RenderTimings)[]) {\n this._timings[key] = 0;\n }\n }\n\n /**\n * Get current render count.\n */\n get renderCount(): number {\n return this._renderCount;\n }\n\n /**\n * Increment render count.\n */\n incrementRenderCount(): void {\n this._renderCount++;\n }\n\n /**\n * Add time to a specific phase.\n */\n addTime(phase: keyof RenderTimings, ms: number): void {\n this._timings[phase] += ms;\n }\n\n /**\n * Time a synchronous operation and add to the specified phase.\n */\n time<T>(phase: keyof RenderTimings, fn: () => T): T {\n const start = performance.now();\n const result = fn();\n this._timings[phase] += performance.now() - start;\n return result;\n }\n\n /**\n * Time an async operation and add to the specified phase.\n */\n async timeAsync<T>(
|
|
1
|
+
{"version":3,"file":"RenderProfiler.js","names":["parts: string[]"],"sources":["../../src/preview/RenderProfiler.ts"],"sourcesContent":["/**\n * Profiling utility for render operations.\n * Centralizes timing accumulation and logging to keep business logic clean.\n */\n\n/** Interval between profiling log outputs (ms) */\nconst DEFAULT_LOG_INTERVAL_MS = 2000;\n\n/** Interval for periodic frame logging (every N frames) */\nconst DEFAULT_FRAME_LOG_INTERVAL = 60;\n\n/**\n * Phases tracked during rendering.\n */\nexport interface RenderTimings {\n setup: number;\n draw: number;\n downsample: number;\n canvasEncode: number;\n inline: number;\n serialize: number;\n base64: number;\n imageLoad: number;\n restore: number;\n}\n\n/**\n * Profiler for render operations.\n * Accumulates timing data and provides structured logging.\n */\nexport class RenderProfiler {\n private _renderCount = 0;\n private _lastLogTime = 0;\n private _timingLoggedAt = 0;\n\n private _timings: RenderTimings = {\n setup: 0,\n draw: 0,\n downsample: 0,\n canvasEncode: 0,\n inline: 0,\n serialize: 0,\n base64: 0,\n imageLoad: 0,\n restore: 0,\n };\n\n /**\n * Reset all timing data.\n */\n reset(): void {\n this._renderCount = 0;\n this._lastLogTime = 0;\n this._timingLoggedAt = 0;\n\n for (const key of Object.keys(this._timings) as (keyof RenderTimings)[]) {\n this._timings[key] = 0;\n }\n }\n\n /**\n * Get current render count.\n */\n get renderCount(): number {\n return this._renderCount;\n }\n\n /**\n * Increment render count.\n */\n incrementRenderCount(): void {\n this._renderCount++;\n }\n\n /**\n * Add time to a specific phase.\n */\n addTime(phase: keyof RenderTimings, ms: number): void {\n this._timings[phase] += ms;\n }\n\n /**\n * Time a synchronous operation and add to the specified phase.\n */\n time<T>(phase: keyof RenderTimings, fn: () => T): T {\n const start = performance.now();\n const result = fn();\n this._timings[phase] += performance.now() - start;\n return result;\n }\n\n /**\n * Time an async operation and add to the specified phase.\n */\n async timeAsync<T>(phase: keyof RenderTimings, fn: () => Promise<T>): Promise<T> {\n const start = performance.now();\n const result = await fn();\n this._timings[phase] += performance.now() - start;\n return result;\n }\n\n /**\n * Check if enough time has passed since last log (for time-based logging).\n */\n shouldLogByTime(intervalMs: number = DEFAULT_LOG_INTERVAL_MS): boolean {\n const now = performance.now();\n if (now - this._lastLogTime > intervalMs) {\n this._lastLogTime = now;\n return true;\n }\n return false;\n }\n\n /**\n * Check if enough frames have passed since last log (for frame-based logging).\n */\n shouldLogByFrameCount(interval: number = DEFAULT_FRAME_LOG_INTERVAL): boolean {\n if (this._renderCount - this._timingLoggedAt >= interval) {\n this._timingLoggedAt = this._renderCount;\n return true;\n }\n return false;\n }\n\n /**\n * Check if this is an early render (for initial debug logging).\n */\n isEarlyRender(threshold: number = 2): boolean {\n return this._renderCount < threshold;\n }\n\n /**\n * Get timing summary string.\n */\n summary(): string {\n const t = this._timings;\n const parts: string[] = [];\n\n if (t.setup > 0) parts.push(`setup=${t.setup.toFixed(0)}ms`);\n if (t.draw > 0) parts.push(`draw=${t.draw.toFixed(0)}ms`);\n if (t.downsample > 0) parts.push(`downsample=${t.downsample.toFixed(0)}ms`);\n if (t.canvasEncode > 0) parts.push(`canvasEncode=${t.canvasEncode.toFixed(0)}ms`);\n if (t.inline > 0) parts.push(`inline=${t.inline.toFixed(0)}ms`);\n if (t.serialize > 0) parts.push(`serialize=${t.serialize.toFixed(0)}ms`);\n if (t.base64 > 0) parts.push(`base64=${t.base64.toFixed(0)}ms`);\n if (t.imageLoad > 0) parts.push(`imageLoad=${t.imageLoad.toFixed(0)}ms`);\n if (t.restore > 0) parts.push(`restore=${t.restore.toFixed(0)}ms`);\n\n return parts.join(\", \");\n }\n\n /**\n * Get raw timings object.\n */\n getTimings(): Readonly<RenderTimings> {\n return { ...this._timings };\n }\n}\n\n/**\n * Default shared profiler instance.\n * Can be replaced with a custom instance for testing.\n */\nexport const defaultProfiler = new RenderProfiler();\n"],"mappings":";;;;;;AAMA,MAAM,0BAA0B;;AAGhC,MAAM,6BAA6B;;;;;AAqBnC,IAAa,iBAAb,MAA4B;;sBACH;sBACA;yBACG;kBAEQ;GAChC,OAAO;GACP,MAAM;GACN,YAAY;GACZ,cAAc;GACd,QAAQ;GACR,WAAW;GACX,QAAQ;GACR,WAAW;GACX,SAAS;GACV;;;;;CAKD,QAAc;AACZ,OAAK,eAAe;AACpB,OAAK,eAAe;AACpB,OAAK,kBAAkB;AAEvB,OAAK,MAAM,OAAO,OAAO,KAAK,KAAK,SAAS,CAC1C,MAAK,SAAS,OAAO;;;;;CAOzB,IAAI,cAAsB;AACxB,SAAO,KAAK;;;;;CAMd,uBAA6B;AAC3B,OAAK;;;;;CAMP,QAAQ,OAA4B,IAAkB;AACpD,OAAK,SAAS,UAAU;;;;;CAM1B,KAAQ,OAA4B,IAAgB;EAClD,MAAM,QAAQ,YAAY,KAAK;EAC/B,MAAM,SAAS,IAAI;AACnB,OAAK,SAAS,UAAU,YAAY,KAAK,GAAG;AAC5C,SAAO;;;;;CAMT,MAAM,UAAa,OAA4B,IAAkC;EAC/E,MAAM,QAAQ,YAAY,KAAK;EAC/B,MAAM,SAAS,MAAM,IAAI;AACzB,OAAK,SAAS,UAAU,YAAY,KAAK,GAAG;AAC5C,SAAO;;;;;CAMT,gBAAgB,aAAqB,yBAAkC;EACrE,MAAM,MAAM,YAAY,KAAK;AAC7B,MAAI,MAAM,KAAK,eAAe,YAAY;AACxC,QAAK,eAAe;AACpB,UAAO;;AAET,SAAO;;;;;CAMT,sBAAsB,WAAmB,4BAAqC;AAC5E,MAAI,KAAK,eAAe,KAAK,mBAAmB,UAAU;AACxD,QAAK,kBAAkB,KAAK;AAC5B,UAAO;;AAET,SAAO;;;;;CAMT,cAAc,YAAoB,GAAY;AAC5C,SAAO,KAAK,eAAe;;;;;CAM7B,UAAkB;EAChB,MAAM,IAAI,KAAK;EACf,MAAMA,QAAkB,EAAE;AAE1B,MAAI,EAAE,QAAQ,EAAG,OAAM,KAAK,SAAS,EAAE,MAAM,QAAQ,EAAE,CAAC,IAAI;AAC5D,MAAI,EAAE,OAAO,EAAG,OAAM,KAAK,QAAQ,EAAE,KAAK,QAAQ,EAAE,CAAC,IAAI;AACzD,MAAI,EAAE,aAAa,EAAG,OAAM,KAAK,cAAc,EAAE,WAAW,QAAQ,EAAE,CAAC,IAAI;AAC3E,MAAI,EAAE,eAAe,EAAG,OAAM,KAAK,gBAAgB,EAAE,aAAa,QAAQ,EAAE,CAAC,IAAI;AACjF,MAAI,EAAE,SAAS,EAAG,OAAM,KAAK,UAAU,EAAE,OAAO,QAAQ,EAAE,CAAC,IAAI;AAC/D,MAAI,EAAE,YAAY,EAAG,OAAM,KAAK,aAAa,EAAE,UAAU,QAAQ,EAAE,CAAC,IAAI;AACxE,MAAI,EAAE,SAAS,EAAG,OAAM,KAAK,UAAU,EAAE,OAAO,QAAQ,EAAE,CAAC,IAAI;AAC/D,MAAI,EAAE,YAAY,EAAG,OAAM,KAAK,aAAa,EAAE,UAAU,QAAQ,EAAE,CAAC,IAAI;AACxE,MAAI,EAAE,UAAU,EAAG,OAAM,KAAK,WAAW,EAAE,QAAQ,QAAQ,EAAE,CAAC,IAAI;AAElE,SAAO,MAAM,KAAK,KAAK;;;;;CAMzB,aAAsC;AACpC,SAAO,EAAE,GAAG,KAAK,UAAU;;;;;;;AAQ/B,MAAa,kBAAkB,IAAI,gBAAgB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RenderStats.js","names":[],"sources":["../../src/preview/RenderStats.ts"],"sourcesContent":["/**\n * RenderStats: Always-on performance statistics collection\n *\n * This class continuously collects rendering performance data regardless of\n * whether stats are being displayed. It acts as a persistent data store that\n * the UI can read from at any time.\n *\n * Key principles:\n * - Collection is always active (not tied to display visibility)\n * - Data persists across mode changes and zoom operations\n * - Display is orthogonal to collection\n */\n\nimport type { AdaptiveResolutionTracker } from \"./AdaptiveResolutionTracker.js\";\n\n/**\n * Playback statistics for display.\n */\nexport interface PlaybackStats {\n fps: number;\n avgRenderTime: number | null;\n headroom: number | null;\n pressureState: string;\n pressureHistory: string[];\n renderWidth: number;\n renderHeight: number;\n resolutionScale: number | null;\n samplesAtCurrentScale?: number;\n canScaleUp?: boolean;\n canScaleDown?: boolean;\n}\n\n/**\n * RenderStats collects performance data from the rendering system.\n *\n * Usage:\n * ```typescript\n * const stats = new RenderStats(adaptiveTracker);\n *\n * // In render loop (always call this)\n * stats.recordFrame(renderTime, timestamp, isAtRest);\n *\n * // In display (only when visible)\n * const data = stats.getStats(renderWidth, renderHeight, resolutionScale);\n * ```\n */\nexport class RenderStats {\n private adaptiveTracker: AdaptiveResolutionTracker;\n\n // Frame timing data\n private renderTimes: number[] = [];\n private frameIntervals: number[] = [];\n private lastFrameTime = 0;\n\n private readonly ROLLING_WINDOW_SIZE = 30; // ~1 second at 30fps\n private readonly TARGET_FRAME_TIME_MS = 33.33; // 30fps target\n\n constructor(adaptiveTracker: AdaptiveResolutionTracker) {\n this.adaptiveTracker = adaptiveTracker;\n }\n\n /**\n * Record a completed frame render.\n * Call this from the render loop after each frame completes.\n *\n * @param renderTime - Time spent rendering this frame (ms)\n * @param timestamp - Current timestamp from performance.now() or rAF\n * @param isAtRest - Whether the system is at rest (not playing/scrubbing)\n */\n recordFrame(renderTime: number, timestamp: number, isAtRest: boolean): void {\n // Track render times for averaging\n this.renderTimes.push(renderTime);\n if (this.renderTimes.length > this.ROLLING_WINDOW_SIZE) {\n this.renderTimes.shift();\n }\n\n // Track frame intervals for FPS calculation\n if (this.lastFrameTime > 0) {\n const interval = timestamp - this.lastFrameTime;\n this.frameIntervals.push(interval);\n if (this.frameIntervals.length > this.ROLLING_WINDOW_SIZE) {\n this.frameIntervals.shift();\n }\n }\n this.lastFrameTime = timestamp;\n\n // Update adaptive tracker (only when in motion)\n if (!isAtRest) {\n this.adaptiveTracker.recordFrame(renderTime, timestamp);\n }\n }\n\n /**\n * Get current statistics for display.\n *\n * @param renderWidth - Current render width in pixels\n * @param renderHeight - Current render height in pixels\n * @param resolutionScale - Current resolution scale (0-1), or null if not applicable\n * @returns Current playback statistics\n */\n getStats(\n renderWidth: number,\n renderHeight: number,\n resolutionScale: number | null = null,\n ): PlaybackStats {\n // Calculate average render time\n const avgRenderTime =\n this.renderTimes.length > 0\n ? this.renderTimes.reduce((a, b) => a + b, 0) / this.renderTimes.length\n : null;\n\n // Calculate FPS from frame intervals\n const avgFrameInterval =\n this.frameIntervals.length > 0\n ? this.frameIntervals.reduce((a, b) => a + b, 0)
|
|
1
|
+
{"version":3,"file":"RenderStats.js","names":[],"sources":["../../src/preview/RenderStats.ts"],"sourcesContent":["/**\n * RenderStats: Always-on performance statistics collection\n *\n * This class continuously collects rendering performance data regardless of\n * whether stats are being displayed. It acts as a persistent data store that\n * the UI can read from at any time.\n *\n * Key principles:\n * - Collection is always active (not tied to display visibility)\n * - Data persists across mode changes and zoom operations\n * - Display is orthogonal to collection\n */\n\nimport type { AdaptiveResolutionTracker } from \"./AdaptiveResolutionTracker.js\";\n\n/**\n * Playback statistics for display.\n */\nexport interface PlaybackStats {\n fps: number;\n avgRenderTime: number | null;\n headroom: number | null;\n pressureState: string;\n pressureHistory: string[];\n renderWidth: number;\n renderHeight: number;\n resolutionScale: number | null;\n samplesAtCurrentScale?: number;\n canScaleUp?: boolean;\n canScaleDown?: boolean;\n}\n\n/**\n * RenderStats collects performance data from the rendering system.\n *\n * Usage:\n * ```typescript\n * const stats = new RenderStats(adaptiveTracker);\n *\n * // In render loop (always call this)\n * stats.recordFrame(renderTime, timestamp, isAtRest);\n *\n * // In display (only when visible)\n * const data = stats.getStats(renderWidth, renderHeight, resolutionScale);\n * ```\n */\nexport class RenderStats {\n private adaptiveTracker: AdaptiveResolutionTracker;\n\n // Frame timing data\n private renderTimes: number[] = [];\n private frameIntervals: number[] = [];\n private lastFrameTime = 0;\n\n private readonly ROLLING_WINDOW_SIZE = 30; // ~1 second at 30fps\n private readonly TARGET_FRAME_TIME_MS = 33.33; // 30fps target\n\n constructor(adaptiveTracker: AdaptiveResolutionTracker) {\n this.adaptiveTracker = adaptiveTracker;\n }\n\n /**\n * Record a completed frame render.\n * Call this from the render loop after each frame completes.\n *\n * @param renderTime - Time spent rendering this frame (ms)\n * @param timestamp - Current timestamp from performance.now() or rAF\n * @param isAtRest - Whether the system is at rest (not playing/scrubbing)\n */\n recordFrame(renderTime: number, timestamp: number, isAtRest: boolean): void {\n // Track render times for averaging\n this.renderTimes.push(renderTime);\n if (this.renderTimes.length > this.ROLLING_WINDOW_SIZE) {\n this.renderTimes.shift();\n }\n\n // Track frame intervals for FPS calculation\n if (this.lastFrameTime > 0) {\n const interval = timestamp - this.lastFrameTime;\n this.frameIntervals.push(interval);\n if (this.frameIntervals.length > this.ROLLING_WINDOW_SIZE) {\n this.frameIntervals.shift();\n }\n }\n this.lastFrameTime = timestamp;\n\n // Update adaptive tracker (only when in motion)\n if (!isAtRest) {\n this.adaptiveTracker.recordFrame(renderTime, timestamp);\n }\n }\n\n /**\n * Get current statistics for display.\n *\n * @param renderWidth - Current render width in pixels\n * @param renderHeight - Current render height in pixels\n * @param resolutionScale - Current resolution scale (0-1), or null if not applicable\n * @returns Current playback statistics\n */\n getStats(\n renderWidth: number,\n renderHeight: number,\n resolutionScale: number | null = null,\n ): PlaybackStats {\n // Calculate average render time\n const avgRenderTime =\n this.renderTimes.length > 0\n ? this.renderTimes.reduce((a, b) => a + b, 0) / this.renderTimes.length\n : null;\n\n // Calculate FPS from frame intervals\n const avgFrameInterval =\n this.frameIntervals.length > 0\n ? this.frameIntervals.reduce((a, b) => a + b, 0) / this.frameIntervals.length\n : 16.67;\n const fps = avgFrameInterval > 0 ? 1000 / avgFrameInterval : 0;\n\n // Calculate headroom (positive = faster than target, negative = slower)\n const headroom = avgRenderTime !== null ? this.TARGET_FRAME_TIME_MS - avgRenderTime : null;\n\n // Get adaptive tracker stats\n const trackerStats = this.adaptiveTracker.getStats();\n\n return {\n fps,\n avgRenderTime,\n headroom,\n pressureState: trackerStats.pressureState,\n pressureHistory: trackerStats.pressureHistory,\n renderWidth,\n renderHeight,\n resolutionScale,\n samplesAtCurrentScale: trackerStats.samplesAtCurrentScale,\n canScaleUp: trackerStats.canScaleUp,\n canScaleDown: trackerStats.canScaleDown,\n };\n }\n\n /**\n * Reset all collected statistics.\n * Useful when switching modes or starting a new session.\n */\n reset(): void {\n this.renderTimes = [];\n this.frameIntervals = [];\n this.lastFrameTime = 0;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AA8CA,IAAa,cAAb,MAAyB;CAWvB,YAAY,iBAA4C;qBAPxB,EAAE;wBACC,EAAE;uBACb;6BAEe;8BACC;AAGtC,OAAK,kBAAkB;;;;;;;;;;CAWzB,YAAY,YAAoB,WAAmB,UAAyB;AAE1E,OAAK,YAAY,KAAK,WAAW;AACjC,MAAI,KAAK,YAAY,SAAS,KAAK,oBACjC,MAAK,YAAY,OAAO;AAI1B,MAAI,KAAK,gBAAgB,GAAG;GAC1B,MAAM,WAAW,YAAY,KAAK;AAClC,QAAK,eAAe,KAAK,SAAS;AAClC,OAAI,KAAK,eAAe,SAAS,KAAK,oBACpC,MAAK,eAAe,OAAO;;AAG/B,OAAK,gBAAgB;AAGrB,MAAI,CAAC,SACH,MAAK,gBAAgB,YAAY,YAAY,UAAU;;;;;;;;;;CAY3D,SACE,aACA,cACA,kBAAiC,MAClB;EAEf,MAAM,gBACJ,KAAK,YAAY,SAAS,IACtB,KAAK,YAAY,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE,GAAG,KAAK,YAAY,SAC/D;EAGN,MAAM,mBACJ,KAAK,eAAe,SAAS,IACzB,KAAK,eAAe,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE,GAAG,KAAK,eAAe,SACrE;EACN,MAAM,MAAM,mBAAmB,IAAI,MAAO,mBAAmB;EAG7D,MAAM,WAAW,kBAAkB,OAAO,KAAK,uBAAuB,gBAAgB;EAGtF,MAAM,eAAe,KAAK,gBAAgB,UAAU;AAEpD,SAAO;GACL;GACA;GACA;GACA,eAAe,aAAa;GAC5B,iBAAiB,aAAa;GAC9B;GACA;GACA;GACA,uBAAuB,aAAa;GACpC,YAAY,aAAa;GACzB,cAAc,aAAa;GAC5B;;;;;;CAOH,QAAc;AACZ,OAAK,cAAc,EAAE;AACrB,OAAK,iBAAiB,EAAE;AACxB,OAAK,gBAAgB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvasEncoder.js","names":["_workerPool: WorkerPool | null","dataUrl: string"],"sources":["../../../src/preview/encoding/canvasEncoder.ts"],"sourcesContent":["/**\n * Canvas encoding orchestration with worker pool support.\n *\n * Supports caching via RenderContext:\n * - For ef-image/ef-waveform: caches by element + renderVersion\n */\n\nimport { logger } from \"../logger.js\";\nimport { WorkerPool } from \"../workers/WorkerPool.js\";\nimport { getEncoderWorkerUrl } from \"../workers/encoderWorkerInline.js\";\nimport { encodeCanvasOnMainThread } from \"./mainThreadEncoder.js\";\nimport { encodeCanvasInWorker } from \"./workerEncoder.js\";\nimport type { CanvasEncodeResult, CanvasEncodeOptions } from \"./types.js\";\n\n// Module-level worker pool state\nlet _workerPool: WorkerPool | null = null;\nlet _workerPoolWarningLogged = false;\n\n/**\n * Get or create the worker pool for canvas encoding.\n * Returns null if workers are not available.\n */\nfunction getWorkerPool(): WorkerPool | null {\n if (_workerPool) {\n return _workerPool;\n }\n\n // Check if workers are available\n if (\n typeof Worker === \"undefined\" ||\n typeof OffscreenCanvas === \"undefined\" ||\n typeof createImageBitmap === \"undefined\"\n ) {\n if (!_workerPoolWarningLogged) {\n _workerPoolWarningLogged = true;\n logger.warn(\n \"[canvasEncoder] Web Workers or OffscreenCanvas not available, using main thread fallback\",\n );\n }\n return null;\n }\n\n try {\n // Use inline worker URL - this works in any bundler environment\n // because the worker code is embedded in the bundle as a blob URL\n const workerUrl = getEncoderWorkerUrl();\n\n _workerPool = new WorkerPool(workerUrl);\n\n // Check if workers were actually created\n if (!_workerPool.isAvailable()) {\n const reason =\n _workerPool.workerCount === 0\n ? \"no workers created (check console for errors)\"\n : \"workers not available\";\n _workerPool = null;\n if (!_workerPoolWarningLogged) {\n _workerPoolWarningLogged = true;\n logger.warn(\n `[canvasEncoder] Worker pool initialization failed (${reason}), using main thread fallback`,\n );\n }\n }\n } catch (error) {\n _workerPool = null;\n if (!_workerPoolWarningLogged) {\n _workerPoolWarningLogged = true;\n const errorMessage
|
|
1
|
+
{"version":3,"file":"canvasEncoder.js","names":["_workerPool: WorkerPool | null","dataUrl: string"],"sources":["../../../src/preview/encoding/canvasEncoder.ts"],"sourcesContent":["/**\n * Canvas encoding orchestration with worker pool support.\n *\n * Supports caching via RenderContext:\n * - For ef-image/ef-waveform: caches by element + renderVersion\n */\n\nimport { logger } from \"../logger.js\";\nimport { WorkerPool } from \"../workers/WorkerPool.js\";\nimport { getEncoderWorkerUrl } from \"../workers/encoderWorkerInline.js\";\nimport { encodeCanvasOnMainThread } from \"./mainThreadEncoder.js\";\nimport { encodeCanvasInWorker } from \"./workerEncoder.js\";\nimport type { CanvasEncodeResult, CanvasEncodeOptions } from \"./types.js\";\n\n// Module-level worker pool state\nlet _workerPool: WorkerPool | null = null;\nlet _workerPoolWarningLogged = false;\n\n/**\n * Get or create the worker pool for canvas encoding.\n * Returns null if workers are not available.\n */\nfunction getWorkerPool(): WorkerPool | null {\n if (_workerPool) {\n return _workerPool;\n }\n\n // Check if workers are available\n if (\n typeof Worker === \"undefined\" ||\n typeof OffscreenCanvas === \"undefined\" ||\n typeof createImageBitmap === \"undefined\"\n ) {\n if (!_workerPoolWarningLogged) {\n _workerPoolWarningLogged = true;\n logger.warn(\n \"[canvasEncoder] Web Workers or OffscreenCanvas not available, using main thread fallback\",\n );\n }\n return null;\n }\n\n try {\n // Use inline worker URL - this works in any bundler environment\n // because the worker code is embedded in the bundle as a blob URL\n const workerUrl = getEncoderWorkerUrl();\n\n _workerPool = new WorkerPool(workerUrl);\n\n // Check if workers were actually created\n if (!_workerPool.isAvailable()) {\n const reason =\n _workerPool.workerCount === 0\n ? \"no workers created (check console for errors)\"\n : \"workers not available\";\n _workerPool = null;\n if (!_workerPoolWarningLogged) {\n _workerPoolWarningLogged = true;\n logger.warn(\n `[canvasEncoder] Worker pool initialization failed (${reason}), using main thread fallback`,\n );\n }\n }\n } catch (error) {\n _workerPool = null;\n if (!_workerPoolWarningLogged) {\n _workerPoolWarningLogged = true;\n const errorMessage = error instanceof Error ? error.message : String(error);\n logger.warn(\n `[canvasEncoder] Failed to create worker pool: ${errorMessage} - using main thread fallback`,\n );\n }\n }\n\n return _workerPool;\n}\n\n/**\n * Encode canvases to data URLs in parallel using worker pool.\n * Falls back to main thread encoding if workers are unavailable.\n *\n * When RenderContext and sourceMap are provided:\n * - Checks cache for static elements (ef-image, ef-waveform)\n *\n * @param canvases - Array of canvases to encode\n * @param options - Encoding options including optional renderContext and sourceMap\n * @returns Promise resolving to array of encoded results\n */\nexport async function encodeCanvasesInParallel(\n canvases: HTMLCanvasElement[],\n options: CanvasEncodeOptions = {},\n): Promise<CanvasEncodeResult[]> {\n const { scale: canvasScale = 1, renderContext, sourceMap } = options;\n const workerPool = getWorkerPool();\n\n // Helper to encode a single canvas (with caching)\n const encodeCanvas = async (canvas: HTMLCanvasElement): Promise<CanvasEncodeResult | null> => {\n try {\n if (canvas.width === 0 || canvas.height === 0) {\n return null;\n }\n\n const preserveAlpha = canvas.dataset.preserveAlpha === \"true\";\n const sourceElement = sourceMap?.get(canvas);\n\n // Check RenderContext cache for static elements (ef-image, ef-waveform)\n if (renderContext && sourceElement) {\n const cachedDataUrl = renderContext.getCachedCanvasDataUrl(sourceElement);\n if (cachedDataUrl) {\n return { canvas, dataUrl: cachedDataUrl, preserveAlpha };\n }\n }\n\n // Standard encoding path (fallback when no RenderContext cache)\n let sourceCanvas = canvas;\n\n // Handle canvas scaling on main thread before encoding\n if (canvasScale < 1) {\n const scaledWidth = Math.floor(canvas.width * canvasScale);\n const scaledHeight = Math.floor(canvas.height * canvasScale);\n const scaledCanvas = document.createElement(\"canvas\");\n scaledCanvas.width = scaledWidth;\n scaledCanvas.height = scaledHeight;\n const scaledCtx = scaledCanvas.getContext(\"2d\");\n if (scaledCtx) {\n scaledCtx.drawImage(canvas, 0, 0, scaledWidth, scaledHeight);\n sourceCanvas = scaledCanvas;\n }\n }\n\n let dataUrl: string;\n\n if (workerPool) {\n // Encode in worker\n dataUrl = await workerPool.execute((worker) =>\n encodeCanvasInWorker(worker, sourceCanvas, preserveAlpha),\n );\n } else {\n // Main thread fallback - warning already logged once in getWorkerPool()\n const encoded = encodeCanvasOnMainThread(sourceCanvas, canvasScale);\n if (!encoded) return null;\n dataUrl = encoded.dataUrl;\n }\n\n // Cache the result for static elements\n if (renderContext && sourceElement) {\n renderContext.setCachedCanvasDataUrl(sourceElement, dataUrl);\n }\n\n return { canvas, dataUrl, preserveAlpha };\n } catch (error) {\n // Fallback to main thread if worker encoding fails\n logger.warn(\"[canvasEncoder] Worker encoding failed, using main thread fallback:\", error);\n const encoded = encodeCanvasOnMainThread(canvas, canvasScale);\n if (encoded) {\n logger.warn(\"[canvasEncoder] Main thread fallback succeeded\");\n return { canvas, ...encoded };\n }\n\n // Cross-origin canvas or other error - skip\n logger.warn(\"[canvasEncoder] Main thread encoding also failed, skipping canvas:\", error);\n return null;\n }\n };\n\n // Encode all canvases in parallel\n const encodingTasks = canvases.map(encodeCanvas);\n const encodedResults = await Promise.all(encodingTasks);\n const validResults = encodedResults.filter((r): r is CanvasEncodeResult => r !== null);\n return validResults;\n}\n\n/**\n * Reset the worker pool state (for testing).\n */\nexport function resetWorkerPool(): void {\n if (_workerPool) {\n _workerPool.terminate();\n _workerPool = null;\n }\n _workerPoolWarningLogged = false;\n}\n"],"mappings":";;;;;;;AAeA,IAAIA,cAAiC;AACrC,IAAI,2BAA2B;;;;;AAM/B,SAAS,gBAAmC;AAC1C,KAAI,YACF,QAAO;AAIT,KACE,OAAO,WAAW,eAClB,OAAO,oBAAoB,eAC3B,OAAO,sBAAsB,aAC7B;AACA,MAAI,CAAC,0BAA0B;AAC7B,8BAA2B;AAC3B,UAAO,KACL,2FACD;;AAEH,SAAO;;AAGT,KAAI;AAKF,gBAAc,IAAI,WAFA,qBAAqB,CAEA;AAGvC,MAAI,CAAC,YAAY,aAAa,EAAE;GAC9B,MAAM,SACJ,YAAY,gBAAgB,IACxB,kDACA;AACN,iBAAc;AACd,OAAI,CAAC,0BAA0B;AAC7B,+BAA2B;AAC3B,WAAO,KACL,sDAAsD,OAAO,+BAC9D;;;UAGE,OAAO;AACd,gBAAc;AACd,MAAI,CAAC,0BAA0B;AAC7B,8BAA2B;GAC3B,MAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AAC3E,UAAO,KACL,iDAAiD,aAAa,+BAC/D;;;AAIL,QAAO;;;;;;;;;;;;;AAcT,eAAsB,yBACpB,UACA,UAA+B,EAAE,EACF;CAC/B,MAAM,EAAE,OAAO,cAAc,GAAG,eAAe,cAAc;CAC7D,MAAM,aAAa,eAAe;CAGlC,MAAM,eAAe,OAAO,WAAkE;AAC5F,MAAI;AACF,OAAI,OAAO,UAAU,KAAK,OAAO,WAAW,EAC1C,QAAO;GAGT,MAAM,gBAAgB,OAAO,QAAQ,kBAAkB;GACvD,MAAM,gBAAgB,WAAW,IAAI,OAAO;AAG5C,OAAI,iBAAiB,eAAe;IAClC,MAAM,gBAAgB,cAAc,uBAAuB,cAAc;AACzE,QAAI,cACF,QAAO;KAAE;KAAQ,SAAS;KAAe;KAAe;;GAK5D,IAAI,eAAe;AAGnB,OAAI,cAAc,GAAG;IACnB,MAAM,cAAc,KAAK,MAAM,OAAO,QAAQ,YAAY;IAC1D,MAAM,eAAe,KAAK,MAAM,OAAO,SAAS,YAAY;IAC5D,MAAM,eAAe,SAAS,cAAc,SAAS;AACrD,iBAAa,QAAQ;AACrB,iBAAa,SAAS;IACtB,MAAM,YAAY,aAAa,WAAW,KAAK;AAC/C,QAAI,WAAW;AACb,eAAU,UAAU,QAAQ,GAAG,GAAG,aAAa,aAAa;AAC5D,oBAAe;;;GAInB,IAAIC;AAEJ,OAAI,WAEF,WAAU,MAAM,WAAW,SAAS,WAClC,qBAAqB,QAAQ,cAAc,cAAc,CAC1D;QACI;IAEL,MAAM,UAAU,yBAAyB,cAAc,YAAY;AACnE,QAAI,CAAC,QAAS,QAAO;AACrB,cAAU,QAAQ;;AAIpB,OAAI,iBAAiB,cACnB,eAAc,uBAAuB,eAAe,QAAQ;AAG9D,UAAO;IAAE;IAAQ;IAAS;IAAe;WAClC,OAAO;AAEd,UAAO,KAAK,uEAAuE,MAAM;GACzF,MAAM,UAAU,yBAAyB,QAAQ,YAAY;AAC7D,OAAI,SAAS;AACX,WAAO,KAAK,iDAAiD;AAC7D,WAAO;KAAE;KAAQ,GAAG;KAAS;;AAI/B,UAAO,KAAK,sEAAsE,MAAM;AACxF,UAAO;;;CAKX,MAAM,gBAAgB,SAAS,IAAI,aAAa;AAGhD,SAFuB,MAAM,QAAQ,IAAI,cAAc,EACnB,QAAQ,MAA+B,MAAM,KAAK"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mainThreadEncoder.js","names":["dataUrl: string"],"sources":["../../../src/preview/encoding/mainThreadEncoder.ts"],"sourcesContent":["/**\n * Main thread canvas encoding (fallback implementation).\n */\n\n// JPEG quality constants\nexport const JPEG_QUALITY_HIGH = 0.92;\nexport const JPEG_QUALITY_MEDIUM = 0.85;\n\n/**\n * Encode a single canvas to a data URL on the main thread (fallback).\n * @param canvas - The canvas to encode\n * @param canvasScale - Scale factor for encoding (default: 1)\n * @returns Encoded result or null if encoding fails\n */\nexport function encodeCanvasOnMainThread(\n canvas: HTMLCanvasElement,\n canvasScale: number,\n): { dataUrl: string; preserveAlpha: boolean } | null {\n try {\n if (canvas.width === 0 || canvas.height === 0) {\n return null;\n }\n\n const preserveAlpha = canvas.dataset.preserveAlpha === \"true\";\n let dataUrl: string;\n\n if (canvasScale < 1) {\n // Scale down canvas before encoding\n const scaledWidth = Math.floor(canvas.width * canvasScale);\n const scaledHeight = Math.floor(canvas.height * canvasScale);\n const scaledCanvas = document.createElement(\"canvas\");\n scaledCanvas.width = scaledWidth;\n scaledCanvas.height = scaledHeight;\n const scaledCtx = scaledCanvas.getContext(\"2d\");\n if (scaledCtx) {\n scaledCtx.drawImage(canvas, 0, 0, scaledWidth, scaledHeight);\n const quality
|
|
1
|
+
{"version":3,"file":"mainThreadEncoder.js","names":["dataUrl: string"],"sources":["../../../src/preview/encoding/mainThreadEncoder.ts"],"sourcesContent":["/**\n * Main thread canvas encoding (fallback implementation).\n */\n\n// JPEG quality constants\nexport const JPEG_QUALITY_HIGH = 0.92;\nexport const JPEG_QUALITY_MEDIUM = 0.85;\n\n/**\n * Encode a single canvas to a data URL on the main thread (fallback).\n * @param canvas - The canvas to encode\n * @param canvasScale - Scale factor for encoding (default: 1)\n * @returns Encoded result or null if encoding fails\n */\nexport function encodeCanvasOnMainThread(\n canvas: HTMLCanvasElement,\n canvasScale: number,\n): { dataUrl: string; preserveAlpha: boolean } | null {\n try {\n if (canvas.width === 0 || canvas.height === 0) {\n return null;\n }\n\n const preserveAlpha = canvas.dataset.preserveAlpha === \"true\";\n let dataUrl: string;\n\n if (canvasScale < 1) {\n // Scale down canvas before encoding\n const scaledWidth = Math.floor(canvas.width * canvasScale);\n const scaledHeight = Math.floor(canvas.height * canvasScale);\n const scaledCanvas = document.createElement(\"canvas\");\n scaledCanvas.width = scaledWidth;\n scaledCanvas.height = scaledHeight;\n const scaledCtx = scaledCanvas.getContext(\"2d\");\n if (scaledCtx) {\n scaledCtx.drawImage(canvas, 0, 0, scaledWidth, scaledHeight);\n const quality = canvasScale < 0.5 ? JPEG_QUALITY_MEDIUM : JPEG_QUALITY_HIGH;\n dataUrl = preserveAlpha\n ? scaledCanvas.toDataURL(\"image/png\")\n : scaledCanvas.toDataURL(\"image/jpeg\", quality);\n } else {\n dataUrl = preserveAlpha\n ? canvas.toDataURL(\"image/png\")\n : canvas.toDataURL(\"image/jpeg\", JPEG_QUALITY_HIGH);\n }\n } else {\n dataUrl = preserveAlpha\n ? canvas.toDataURL(\"image/png\")\n : canvas.toDataURL(\"image/jpeg\", JPEG_QUALITY_HIGH);\n }\n\n return { dataUrl, preserveAlpha };\n } catch (_e) {\n // Cross-origin canvas or other error - skip\n return null;\n }\n}\n"],"mappings":";;;;AAKA,MAAa,oBAAoB;AACjC,MAAa,sBAAsB;;;;;;;AAQnC,SAAgB,yBACd,QACA,aACoD;AACpD,KAAI;AACF,MAAI,OAAO,UAAU,KAAK,OAAO,WAAW,EAC1C,QAAO;EAGT,MAAM,gBAAgB,OAAO,QAAQ,kBAAkB;EACvD,IAAIA;AAEJ,MAAI,cAAc,GAAG;GAEnB,MAAM,cAAc,KAAK,MAAM,OAAO,QAAQ,YAAY;GAC1D,MAAM,eAAe,KAAK,MAAM,OAAO,SAAS,YAAY;GAC5D,MAAM,eAAe,SAAS,cAAc,SAAS;AACrD,gBAAa,QAAQ;AACrB,gBAAa,SAAS;GACtB,MAAM,YAAY,aAAa,WAAW,KAAK;AAC/C,OAAI,WAAW;AACb,cAAU,UAAU,QAAQ,GAAG,GAAG,aAAa,aAAa;IAC5D,MAAM,UAAU,cAAc,KAAM,sBAAsB;AAC1D,cAAU,gBACN,aAAa,UAAU,YAAY,GACnC,aAAa,UAAU,cAAc,QAAQ;SAEjD,WAAU,gBACN,OAAO,UAAU,YAAY,GAC7B,OAAO,UAAU,cAAc,kBAAkB;QAGvD,WAAU,gBACN,OAAO,UAAU,YAAY,GAC7B,OAAO,UAAU,cAAc,kBAAkB;AAGvD,SAAO;GAAE;GAAS;GAAe;UAC1B,IAAI;AAEX,SAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"previewSettings.js","names":["_nativeApiAvailable: boolean | null","VALID_NUMERIC_SCALES: number[]"],"sources":["../../src/preview/previewSettings.ts"],"sourcesContent":["/**\n * Preview settings module with localStorage persistence.\n * Manages configuration for the preview rendering system.\n */\n\nconst STORAGE_KEY_NATIVE_CANVAS_API = \"ef-preview-native-canvas-api-enabled\";\nconst STORAGE_KEY_PRESENTATION_MODE = \"ef-preview-presentation-mode\";\nconst STORAGE_KEY_RENDER_MODE = \"ef-preview-render-mode\";\nconst STORAGE_KEY_RESOLUTION_SCALE = \"ef-preview-resolution-scale\";\nconst STORAGE_KEY_SHOW_STATS = \"ef-preview-show-stats\";\nconst STORAGE_KEY_SHOW_THUMBNAIL_TIMESTAMPS =\n \"ef-preview-show-thumbnail-timestamps\";\n\n/**\n * Render mode for HTML-to-canvas capture operations.\n * - \"foreignObject\": SVG foreignObject serialization (fallback, works everywhere)\n * - \"native\": Chrome's experimental drawElementImage API (fastest when available)\n */\nexport type RenderMode = \"foreignObject\" | \"native\";\n\n/**\n * Preview resolution scale factor.\n * Controls how much to reduce the preview render resolution for better performance.\n * - 1: Full resolution (default)\n * - 0.75: 3/4 resolution\n * - 0.5: Half resolution\n * - 0.25: Quarter resolution\n * - \"auto\": Adaptive resolution that scales down during motion to prevent dropped frames,\n * and renders at full resolution when at rest\n */\nexport type PreviewResolutionScale = 1 | 0.75 | 0.5 | 0.25 | \"auto\";\n\n/**\n * Preview presentation mode determines how content is rendered in the workbench.\n * - \"clone\": Show a clone with computed styles applied (alias for \"computed\")\n * - \"dom\": Show the original DOM content directly (alias for \"original\")\n * - \"original\": Show the original DOM content directly\n * - \"computed\": Show a clone with computed styles applied\n * - \"canvas\": Render to canvas using the active rendering path\n */\nexport type PreviewPresentationMode = \"dom\" | \"canvas\";\n\n/**\n * Cached detection result for native HTML-in-Canvas API availability.\n * This is separate from the user preference - it detects browser capability.\n */\nlet _nativeApiAvailable: boolean | null = null;\n\n/**\n * Detect if the native HTML-in-Canvas API (drawElementImage) is available in this browser.\n * This checks browser capability, not user preference.\n *\n * The API is available in Chrome Canary with chrome://flags/#canvas-draw-element\n * @see https://github.com/WICG/html-in-canvas\n */\nexport function isNativeCanvasApiAvailable(): boolean {\n if (_nativeApiAvailable === null) {\n const canvas = document.createElement(\"canvas\");\n const ctx = canvas.getContext(\"2d\");\n _nativeApiAvailable = ctx !== null && \"drawElementImage\" in ctx;\n }\n return _nativeApiAvailable;\n}\n\n/**\n * Check if the native Canvas API is enabled by the user.\n * Returns true only if:\n * 1. The API is available in the browser\n * 2. The user has not explicitly disabled it\n *\n * Default is enabled when available (opt-out model).\n */\nexport function isNativeCanvasApiEnabled(): boolean {\n if (!isNativeCanvasApiAvailable()) {\n return false;\n }\n\n try {\n const stored = localStorage.getItem(STORAGE_KEY_NATIVE_CANVAS_API);\n // Default to true (enabled) when available, unless explicitly disabled\n if (stored === null) {\n return true;\n }\n return stored === \"true\";\n } catch {\n // localStorage not available (e.g., private browsing)\n return true;\n }\n}\n\n/**\n * Set whether the native Canvas API should be used (when available).\n * Persists to localStorage and dispatches a change event.\n */\nexport function setNativeCanvasApiEnabled(enabled: boolean): void {\n try {\n localStorage.setItem(STORAGE_KEY_NATIVE_CANVAS_API, String(enabled));\n } catch {\n // localStorage not available\n }\n\n // Dispatch event so components can react to the change\n window.dispatchEvent(\n new CustomEvent(\"ef-preview-settings-changed\", {\n detail: { nativeCanvasApiEnabled: enabled },\n }),\n );\n}\n\n/**\n * Get the current raw user preference (ignoring availability).\n * Returns null if no preference is set.\n */\nexport function getNativeCanvasApiPreference(): boolean | null {\n try {\n const stored = localStorage.getItem(STORAGE_KEY_NATIVE_CANVAS_API);\n if (stored === null) {\n return null;\n }\n return stored === \"true\";\n } catch {\n return null;\n }\n}\n\n/**\n * Subscribe to preview settings changes.\n * @returns Unsubscribe function\n */\nexport function onPreviewSettingsChanged(\n callback: (detail: PreviewSettingsChangedDetail) => void,\n): () => void {\n const handler = (event: Event) => {\n callback((event as CustomEvent).detail);\n };\n window.addEventListener(\"ef-preview-settings-changed\", handler);\n return () =>\n window.removeEventListener(\"ef-preview-settings-changed\", handler);\n}\n\n/**\n * Detail object for preview settings change events.\n */\nexport interface PreviewSettingsChangedDetail {\n nativeCanvasApiEnabled?: boolean;\n presentationMode?: PreviewPresentationMode;\n renderMode?: RenderMode;\n resolutionScale?: PreviewResolutionScale;\n showStats?: boolean;\n showThumbnailTimestamps?: boolean;\n}\n\n/**\n * Get the current preview presentation mode.\n * Defaults to \"dom\" if not set.\n */\nexport function getPreviewPresentationMode(): PreviewPresentationMode {\n try {\n const stored = localStorage.getItem(STORAGE_KEY_PRESENTATION_MODE);\n if (stored === \"dom\" || stored === \"canvas\") {\n return stored;\n }\n return \"dom\";\n } catch {\n return \"dom\";\n }\n}\n\n/**\n * Set the preview presentation mode.\n * Persists to localStorage and dispatches a change event.\n */\nexport function setPreviewPresentationMode(\n mode: PreviewPresentationMode,\n): void {\n try {\n localStorage.setItem(STORAGE_KEY_PRESENTATION_MODE, mode);\n } catch {\n // localStorage not available\n }\n\n // Dispatch event so components can react to the change\n window.dispatchEvent(\n new CustomEvent(\"ef-preview-settings-changed\", {\n detail: { presentationMode: mode },\n }),\n );\n}\n\n/**\n * Get the current render mode for HTML-to-canvas capture.\n * Defaults to \"native\" if available, otherwise \"foreignObject\".\n *\n * Checks EF_NATIVE_RENDER URL parameter to force native mode when set.\n */\nexport function getRenderMode(): RenderMode {\n // Check URL parameter first (CLI flag override)\n try {\n const urlParams = new URLSearchParams(window.location.search);\n if (urlParams.get(\"EF_NATIVE_RENDER\") === \"1\") {\n // Force native mode if available, otherwise fall back to foreignObject\n return isNativeCanvasApiAvailable() ? \"native\" : \"foreignObject\";\n }\n } catch {\n // URL parsing failed, continue with normal logic\n }\n\n try {\n const stored = localStorage.getItem(STORAGE_KEY_RENDER_MODE);\n if (stored === \"foreignObject\" || stored === \"native\") {\n return stored;\n }\n // Default: prefer native if available, otherwise foreignObject\n return isNativeCanvasApiAvailable() ? \"native\" : \"foreignObject\";\n } catch {\n return isNativeCanvasApiAvailable() ? \"native\" : \"foreignObject\";\n }\n}\n\n/**\n * Set the render mode for HTML-to-canvas capture.\n * Persists to localStorage and dispatches a change event.\n */\nexport function setRenderMode(mode: RenderMode): void {\n try {\n localStorage.setItem(STORAGE_KEY_RENDER_MODE, mode);\n } catch {\n // localStorage not available\n }\n\n // Dispatch event so components can react to the change\n window.dispatchEvent(\n new CustomEvent(\"ef-preview-settings-changed\", {\n detail: { renderMode: mode },\n }),\n );\n}\n\n/**\n * Valid numeric resolution scale values.\n */\nconst VALID_NUMERIC_SCALES: number[] = [1, 0.75, 0.5, 0.25];\n\n/**\n * Get the current preview resolution scale.\n * Defaults to 1 (full resolution) if not set.\n */\nexport function getPreviewResolutionScale(): PreviewResolutionScale {\n try {\n const stored = localStorage.getItem(STORAGE_KEY_RESOLUTION_SCALE);\n if (stored !== null) {\n // Check for \"auto\" string first\n if (stored === \"auto\") {\n return \"auto\";\n }\n // Then check numeric values\n const parsed = parseFloat(stored);\n if (VALID_NUMERIC_SCALES.includes(parsed)) {\n return parsed as PreviewResolutionScale;\n }\n }\n return 1;\n } catch {\n return 1;\n }\n}\n\n/**\n * Set the preview resolution scale.\n * Persists to localStorage and dispatches a change event.\n */\nexport function setPreviewResolutionScale(scale: PreviewResolutionScale): void {\n try {\n localStorage.setItem(STORAGE_KEY_RESOLUTION_SCALE, String(scale));\n } catch {\n // localStorage not available\n }\n\n // Dispatch event so components can react to the change\n window.dispatchEvent(\n new CustomEvent(\"ef-preview-settings-changed\", {\n detail: { resolutionScale: scale },\n }),\n );\n}\n\n/**\n * Get whether performance stats should be shown.\n * Defaults to false (stats hidden by default).\n */\nexport function getShowStats(): boolean {\n try {\n const stored = localStorage.getItem(STORAGE_KEY_SHOW_STATS);\n return stored === \"true\";\n } catch {\n return false;\n }\n}\n\n/**\n * Set whether performance stats should be shown.\n * Persists to localStorage and dispatches a change event.\n */\nexport function setShowStats(enabled: boolean): void {\n try {\n localStorage.setItem(STORAGE_KEY_SHOW_STATS, String(enabled));\n } catch {\n // localStorage not available\n }\n\n // Dispatch event so components can react to the change\n window.dispatchEvent(\n new CustomEvent(\"ef-preview-settings-changed\", {\n detail: { showStats: enabled },\n }),\n );\n}\n\n/**\n * Get whether thumbnail timestamps should be shown.\n * Defaults to false (timestamps hidden by default).\n */\nexport function getShowThumbnailTimestamps(): boolean {\n try {\n const stored = localStorage.getItem(STORAGE_KEY_SHOW_THUMBNAIL_TIMESTAMPS);\n return stored === \"true\";\n } catch {\n return false;\n }\n}\n\n/**\n * Set whether thumbnail timestamps should be shown.\n * Persists to localStorage and dispatches a change event.\n */\nexport function setShowThumbnailTimestamps(enabled: boolean): void {\n try {\n localStorage.setItem(\n STORAGE_KEY_SHOW_THUMBNAIL_TIMESTAMPS,\n String(enabled),\n );\n } catch {\n // localStorage not available\n }\n\n // Dispatch event so components can react to the change\n window.dispatchEvent(\n new CustomEvent(\"ef-preview-settings-changed\", {\n detail: { showThumbnailTimestamps: enabled },\n }),\n );\n}\n"],"mappings":";AAMA,MAAM,gCAAgC;AACtC,MAAM,0BAA0B;AAChC,MAAM,+BAA+B;AACrC,MAAM,yBAAyB;AAC/B,MAAM,wCACJ;;;;;AAmCF,IAAIA,sBAAsC;;;;;;;;AAS1C,SAAgB,6BAAsC;AACpD,KAAI,wBAAwB,MAAM;EAEhC,MAAM,MADS,SAAS,cAAc,SAAS,CAC5B,WAAW,KAAK;AACnC,wBAAsB,QAAQ,QAAQ,sBAAsB;;AAE9D,QAAO;;;;;;AA+FT,SAAgB,6BAAsD;AACpE,KAAI;EACF,MAAM,SAAS,aAAa,QAAQ,8BAA8B;AAClE,MAAI,WAAW,SAAS,WAAW,SACjC,QAAO;AAET,SAAO;SACD;AACN,SAAO;;;;;;;AAQX,SAAgB,2BACd,MACM;AACN,KAAI;AACF,eAAa,QAAQ,+BAA+B,KAAK;SACnD;AAKR,QAAO,cACL,IAAI,YAAY,+BAA+B,EAC7C,QAAQ,EAAE,kBAAkB,MAAM,EACnC,CAAC,CACH;;;;;;;;AASH,SAAgB,gBAA4B;AAE1C,KAAI;AAEF,MADkB,IAAI,gBAAgB,OAAO,SAAS,OAAO,CAC/C,IAAI,mBAAmB,KAAK,IAExC,QAAO,4BAA4B,GAAG,WAAW;SAE7C;AAIR,KAAI;EACF,MAAM,SAAS,aAAa,QAAQ,wBAAwB;AAC5D,MAAI,WAAW,mBAAmB,WAAW,SAC3C,QAAO;AAGT,SAAO,4BAA4B,GAAG,WAAW;SAC3C;AACN,SAAO,4BAA4B,GAAG,WAAW;;;;;;;AAQrD,SAAgB,cAAc,MAAwB;AACpD,KAAI;AACF,eAAa,QAAQ,yBAAyB,KAAK;SAC7C;AAKR,QAAO,cACL,IAAI,YAAY,+BAA+B,EAC7C,QAAQ,EAAE,YAAY,MAAM,EAC7B,CAAC,CACH;;;;;AAMH,MAAMC,uBAAiC;CAAC;CAAG;CAAM;CAAK;CAAK;;;;;AAM3D,SAAgB,4BAAoD;AAClE,KAAI;EACF,MAAM,SAAS,aAAa,QAAQ,6BAA6B;AACjE,MAAI,WAAW,MAAM;AAEnB,OAAI,WAAW,OACb,QAAO;GAGT,MAAM,SAAS,WAAW,OAAO;AACjC,OAAI,qBAAqB,SAAS,OAAO,CACvC,QAAO;;AAGX,SAAO;SACD;AACN,SAAO;;;;;;;AA2BX,SAAgB,eAAwB;AACtC,KAAI;AAEF,SADe,aAAa,QAAQ,uBAAuB,KACzC;SACZ;AACN,SAAO;;;;;;;AAQX,SAAgB,aAAa,SAAwB;AACnD,KAAI;AACF,eAAa,QAAQ,wBAAwB,OAAO,QAAQ,CAAC;SACvD;AAKR,QAAO,cACL,IAAI,YAAY,+BAA+B,EAC7C,QAAQ,EAAE,WAAW,SAAS,EAC/B,CAAC,CACH;;;;;;AAOH,SAAgB,6BAAsC;AACpD,KAAI;AAEF,SADe,aAAa,QAAQ,sCAAsC,KACxD;SACZ;AACN,SAAO;;;;;;;AAQX,SAAgB,2BAA2B,SAAwB;AACjE,KAAI;AACF,eAAa,QACX,uCACA,OAAO,QAAQ,CAChB;SACK;AAKR,QAAO,cACL,IAAI,YAAY,+BAA+B,EAC7C,QAAQ,EAAE,yBAAyB,SAAS,EAC7C,CAAC,CACH"}
|
|
1
|
+
{"version":3,"file":"previewSettings.js","names":["_nativeApiAvailable: boolean | null","VALID_NUMERIC_SCALES: number[]"],"sources":["../../src/preview/previewSettings.ts"],"sourcesContent":["/**\n * Preview settings module with localStorage persistence.\n * Manages configuration for the preview rendering system.\n */\n\nconst STORAGE_KEY_NATIVE_CANVAS_API = \"ef-preview-native-canvas-api-enabled\";\nconst STORAGE_KEY_PRESENTATION_MODE = \"ef-preview-presentation-mode\";\nconst STORAGE_KEY_RENDER_MODE = \"ef-preview-render-mode\";\nconst STORAGE_KEY_RESOLUTION_SCALE = \"ef-preview-resolution-scale\";\nconst STORAGE_KEY_SHOW_STATS = \"ef-preview-show-stats\";\nconst STORAGE_KEY_SHOW_THUMBNAIL_TIMESTAMPS = \"ef-preview-show-thumbnail-timestamps\";\n\n/**\n * Render mode for HTML-to-canvas capture operations.\n * - \"foreignObject\": SVG foreignObject serialization (fallback, works everywhere)\n * - \"native\": Chrome's experimental drawElementImage API (fastest when available)\n */\nexport type RenderMode = \"foreignObject\" | \"native\";\n\n/**\n * Preview resolution scale factor.\n * Controls how much to reduce the preview render resolution for better performance.\n * - 1: Full resolution (default)\n * - 0.75: 3/4 resolution\n * - 0.5: Half resolution\n * - 0.25: Quarter resolution\n * - \"auto\": Adaptive resolution that scales down during motion to prevent dropped frames,\n * and renders at full resolution when at rest\n */\nexport type PreviewResolutionScale = 1 | 0.75 | 0.5 | 0.25 | \"auto\";\n\n/**\n * Preview presentation mode determines how content is rendered in the workbench.\n * - \"clone\": Show a clone with computed styles applied (alias for \"computed\")\n * - \"dom\": Show the original DOM content directly (alias for \"original\")\n * - \"original\": Show the original DOM content directly\n * - \"computed\": Show a clone with computed styles applied\n * - \"canvas\": Render to canvas using the active rendering path\n */\nexport type PreviewPresentationMode = \"dom\" | \"canvas\";\n\n/**\n * Cached detection result for native HTML-in-Canvas API availability.\n * This is separate from the user preference - it detects browser capability.\n */\nlet _nativeApiAvailable: boolean | null = null;\n\n/**\n * Detect if the native HTML-in-Canvas API (drawElementImage) is available in this browser.\n * This checks browser capability, not user preference.\n *\n * The API is available in Chrome Canary with chrome://flags/#canvas-draw-element\n * @see https://github.com/WICG/html-in-canvas\n */\nexport function isNativeCanvasApiAvailable(): boolean {\n if (_nativeApiAvailable === null) {\n const canvas = document.createElement(\"canvas\");\n const ctx = canvas.getContext(\"2d\");\n _nativeApiAvailable = ctx !== null && \"drawElementImage\" in ctx;\n }\n return _nativeApiAvailable;\n}\n\n/**\n * Check if the native Canvas API is enabled by the user.\n * Returns true only if:\n * 1. The API is available in the browser\n * 2. The user has not explicitly disabled it\n *\n * Default is enabled when available (opt-out model).\n */\nexport function isNativeCanvasApiEnabled(): boolean {\n if (!isNativeCanvasApiAvailable()) {\n return false;\n }\n\n try {\n const stored = localStorage.getItem(STORAGE_KEY_NATIVE_CANVAS_API);\n // Default to true (enabled) when available, unless explicitly disabled\n if (stored === null) {\n return true;\n }\n return stored === \"true\";\n } catch {\n // localStorage not available (e.g., private browsing)\n return true;\n }\n}\n\n/**\n * Set whether the native Canvas API should be used (when available).\n * Persists to localStorage and dispatches a change event.\n */\nexport function setNativeCanvasApiEnabled(enabled: boolean): void {\n try {\n localStorage.setItem(STORAGE_KEY_NATIVE_CANVAS_API, String(enabled));\n } catch {\n // localStorage not available\n }\n\n // Dispatch event so components can react to the change\n window.dispatchEvent(\n new CustomEvent(\"ef-preview-settings-changed\", {\n detail: { nativeCanvasApiEnabled: enabled },\n }),\n );\n}\n\n/**\n * Get the current raw user preference (ignoring availability).\n * Returns null if no preference is set.\n */\nexport function getNativeCanvasApiPreference(): boolean | null {\n try {\n const stored = localStorage.getItem(STORAGE_KEY_NATIVE_CANVAS_API);\n if (stored === null) {\n return null;\n }\n return stored === \"true\";\n } catch {\n return null;\n }\n}\n\n/**\n * Subscribe to preview settings changes.\n * @returns Unsubscribe function\n */\nexport function onPreviewSettingsChanged(\n callback: (detail: PreviewSettingsChangedDetail) => void,\n): () => void {\n const handler = (event: Event) => {\n callback((event as CustomEvent).detail);\n };\n window.addEventListener(\"ef-preview-settings-changed\", handler);\n return () => window.removeEventListener(\"ef-preview-settings-changed\", handler);\n}\n\n/**\n * Detail object for preview settings change events.\n */\nexport interface PreviewSettingsChangedDetail {\n nativeCanvasApiEnabled?: boolean;\n presentationMode?: PreviewPresentationMode;\n renderMode?: RenderMode;\n resolutionScale?: PreviewResolutionScale;\n showStats?: boolean;\n showThumbnailTimestamps?: boolean;\n}\n\n/**\n * Get the current preview presentation mode.\n * Defaults to \"dom\" if not set.\n */\nexport function getPreviewPresentationMode(): PreviewPresentationMode {\n try {\n const stored = localStorage.getItem(STORAGE_KEY_PRESENTATION_MODE);\n if (stored === \"dom\" || stored === \"canvas\") {\n return stored;\n }\n return \"dom\";\n } catch {\n return \"dom\";\n }\n}\n\n/**\n * Set the preview presentation mode.\n * Persists to localStorage and dispatches a change event.\n */\nexport function setPreviewPresentationMode(mode: PreviewPresentationMode): void {\n try {\n localStorage.setItem(STORAGE_KEY_PRESENTATION_MODE, mode);\n } catch {\n // localStorage not available\n }\n\n // Dispatch event so components can react to the change\n window.dispatchEvent(\n new CustomEvent(\"ef-preview-settings-changed\", {\n detail: { presentationMode: mode },\n }),\n );\n}\n\n/**\n * Get the current render mode for HTML-to-canvas capture.\n * Defaults to \"native\" if available, otherwise \"foreignObject\".\n *\n * Checks EF_NATIVE_RENDER URL parameter to force native mode when set.\n */\nexport function getRenderMode(): RenderMode {\n // Check URL parameter first (CLI flag override)\n try {\n const urlParams = new URLSearchParams(window.location.search);\n if (urlParams.get(\"EF_NATIVE_RENDER\") === \"1\") {\n // Force native mode if available, otherwise fall back to foreignObject\n return isNativeCanvasApiAvailable() ? \"native\" : \"foreignObject\";\n }\n } catch {\n // URL parsing failed, continue with normal logic\n }\n\n try {\n const stored = localStorage.getItem(STORAGE_KEY_RENDER_MODE);\n if (stored === \"foreignObject\" || stored === \"native\") {\n return stored;\n }\n // Default: prefer native if available, otherwise foreignObject\n return isNativeCanvasApiAvailable() ? \"native\" : \"foreignObject\";\n } catch {\n return isNativeCanvasApiAvailable() ? \"native\" : \"foreignObject\";\n }\n}\n\n/**\n * Set the render mode for HTML-to-canvas capture.\n * Persists to localStorage and dispatches a change event.\n */\nexport function setRenderMode(mode: RenderMode): void {\n try {\n localStorage.setItem(STORAGE_KEY_RENDER_MODE, mode);\n } catch {\n // localStorage not available\n }\n\n // Dispatch event so components can react to the change\n window.dispatchEvent(\n new CustomEvent(\"ef-preview-settings-changed\", {\n detail: { renderMode: mode },\n }),\n );\n}\n\n/**\n * Valid numeric resolution scale values.\n */\nconst VALID_NUMERIC_SCALES: number[] = [1, 0.75, 0.5, 0.25];\n\n/**\n * Get the current preview resolution scale.\n * Defaults to 1 (full resolution) if not set.\n */\nexport function getPreviewResolutionScale(): PreviewResolutionScale {\n try {\n const stored = localStorage.getItem(STORAGE_KEY_RESOLUTION_SCALE);\n if (stored !== null) {\n // Check for \"auto\" string first\n if (stored === \"auto\") {\n return \"auto\";\n }\n // Then check numeric values\n const parsed = parseFloat(stored);\n if (VALID_NUMERIC_SCALES.includes(parsed)) {\n return parsed as PreviewResolutionScale;\n }\n }\n return 1;\n } catch {\n return 1;\n }\n}\n\n/**\n * Set the preview resolution scale.\n * Persists to localStorage and dispatches a change event.\n */\nexport function setPreviewResolutionScale(scale: PreviewResolutionScale): void {\n try {\n localStorage.setItem(STORAGE_KEY_RESOLUTION_SCALE, String(scale));\n } catch {\n // localStorage not available\n }\n\n // Dispatch event so components can react to the change\n window.dispatchEvent(\n new CustomEvent(\"ef-preview-settings-changed\", {\n detail: { resolutionScale: scale },\n }),\n );\n}\n\n/**\n * Get whether performance stats should be shown.\n * Defaults to false (stats hidden by default).\n */\nexport function getShowStats(): boolean {\n try {\n const stored = localStorage.getItem(STORAGE_KEY_SHOW_STATS);\n return stored === \"true\";\n } catch {\n return false;\n }\n}\n\n/**\n * Set whether performance stats should be shown.\n * Persists to localStorage and dispatches a change event.\n */\nexport function setShowStats(enabled: boolean): void {\n try {\n localStorage.setItem(STORAGE_KEY_SHOW_STATS, String(enabled));\n } catch {\n // localStorage not available\n }\n\n // Dispatch event so components can react to the change\n window.dispatchEvent(\n new CustomEvent(\"ef-preview-settings-changed\", {\n detail: { showStats: enabled },\n }),\n );\n}\n\n/**\n * Get whether thumbnail timestamps should be shown.\n * Defaults to false (timestamps hidden by default).\n */\nexport function getShowThumbnailTimestamps(): boolean {\n try {\n const stored = localStorage.getItem(STORAGE_KEY_SHOW_THUMBNAIL_TIMESTAMPS);\n return stored === \"true\";\n } catch {\n return false;\n }\n}\n\n/**\n * Set whether thumbnail timestamps should be shown.\n * Persists to localStorage and dispatches a change event.\n */\nexport function setShowThumbnailTimestamps(enabled: boolean): void {\n try {\n localStorage.setItem(STORAGE_KEY_SHOW_THUMBNAIL_TIMESTAMPS, String(enabled));\n } catch {\n // localStorage not available\n }\n\n // Dispatch event so components can react to the change\n window.dispatchEvent(\n new CustomEvent(\"ef-preview-settings-changed\", {\n detail: { showThumbnailTimestamps: enabled },\n }),\n );\n}\n"],"mappings":";AAMA,MAAM,gCAAgC;AACtC,MAAM,0BAA0B;AAChC,MAAM,+BAA+B;AACrC,MAAM,yBAAyB;AAC/B,MAAM,wCAAwC;;;;;AAmC9C,IAAIA,sBAAsC;;;;;;;;AAS1C,SAAgB,6BAAsC;AACpD,KAAI,wBAAwB,MAAM;EAEhC,MAAM,MADS,SAAS,cAAc,SAAS,CAC5B,WAAW,KAAK;AACnC,wBAAsB,QAAQ,QAAQ,sBAAsB;;AAE9D,QAAO;;;;;;AA8FT,SAAgB,6BAAsD;AACpE,KAAI;EACF,MAAM,SAAS,aAAa,QAAQ,8BAA8B;AAClE,MAAI,WAAW,SAAS,WAAW,SACjC,QAAO;AAET,SAAO;SACD;AACN,SAAO;;;;;;;AAQX,SAAgB,2BAA2B,MAAqC;AAC9E,KAAI;AACF,eAAa,QAAQ,+BAA+B,KAAK;SACnD;AAKR,QAAO,cACL,IAAI,YAAY,+BAA+B,EAC7C,QAAQ,EAAE,kBAAkB,MAAM,EACnC,CAAC,CACH;;;;;;;;AASH,SAAgB,gBAA4B;AAE1C,KAAI;AAEF,MADkB,IAAI,gBAAgB,OAAO,SAAS,OAAO,CAC/C,IAAI,mBAAmB,KAAK,IAExC,QAAO,4BAA4B,GAAG,WAAW;SAE7C;AAIR,KAAI;EACF,MAAM,SAAS,aAAa,QAAQ,wBAAwB;AAC5D,MAAI,WAAW,mBAAmB,WAAW,SAC3C,QAAO;AAGT,SAAO,4BAA4B,GAAG,WAAW;SAC3C;AACN,SAAO,4BAA4B,GAAG,WAAW;;;;;;;AAQrD,SAAgB,cAAc,MAAwB;AACpD,KAAI;AACF,eAAa,QAAQ,yBAAyB,KAAK;SAC7C;AAKR,QAAO,cACL,IAAI,YAAY,+BAA+B,EAC7C,QAAQ,EAAE,YAAY,MAAM,EAC7B,CAAC,CACH;;;;;AAMH,MAAMC,uBAAiC;CAAC;CAAG;CAAM;CAAK;CAAK;;;;;AAM3D,SAAgB,4BAAoD;AAClE,KAAI;EACF,MAAM,SAAS,aAAa,QAAQ,6BAA6B;AACjE,MAAI,WAAW,MAAM;AAEnB,OAAI,WAAW,OACb,QAAO;GAGT,MAAM,SAAS,WAAW,OAAO;AACjC,OAAI,qBAAqB,SAAS,OAAO,CACvC,QAAO;;AAGX,SAAO;SACD;AACN,SAAO;;;;;;;AA2BX,SAAgB,eAAwB;AACtC,KAAI;AAEF,SADe,aAAa,QAAQ,uBAAuB,KACzC;SACZ;AACN,SAAO;;;;;;;AAQX,SAAgB,aAAa,SAAwB;AACnD,KAAI;AACF,eAAa,QAAQ,wBAAwB,OAAO,QAAQ,CAAC;SACvD;AAKR,QAAO,cACL,IAAI,YAAY,+BAA+B,EAC7C,QAAQ,EAAE,WAAW,SAAS,EAC/B,CAAC,CACH;;;;;;AAOH,SAAgB,6BAAsC;AACpD,KAAI;AAEF,SADe,aAAa,QAAQ,sCAAsC,KACxD;SACZ;AACN,SAAO;;;;;;;AAQX,SAAgB,2BAA2B,SAAwB;AACjE,KAAI;AACF,eAAa,QAAQ,uCAAuC,OAAO,QAAQ,CAAC;SACtE;AAKR,QAAO,cACL,IAAI,YAAY,+BAA+B,EAC7C,QAAQ,EAAE,yBAAyB,SAAS,EAC7C,CAAC,CACH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"previewTypes.js","names":[],"sources":["../../src/preview/previewTypes.ts"],"sourcesContent":["/**\n * Shared types and constants for preview rendering.\n *\n * Consolidates duplicate definitions from renderTimegroupToCanvas.ts and\n * renderTimegroupPreview.ts into a single source of truth.\n */\n\n// ============================================================================\n// Temporal Types\n// ============================================================================\n\n/**\n * Element with temporal properties (startTimeMs, endTimeMs).\n * Used for temporal visibility checks during preview rendering.\n */\nexport interface TemporalElement extends Element {\n startTimeMs?: number;\n endTimeMs?: number;\n src?: string;\n}\n\n/**\n * Type guard to check if an element has temporal properties.\n */\nexport function isTemporal(el: Element): el is TemporalElement {\n return \"startTimeMs\" in el && \"endTimeMs\" in el;\n}\n\n/**\n * Get temporal bounds for an element, treating invalid ranges as unbounded.\n * Invalid range (end <= start) means element hasn't computed its duration yet.\n *\n * NOTE: No caching - bounds are computed dynamically by timegroups based on\n * composition mode (sequence/contain/fit) and may change between frames.\n */\nexport function getTemporalBounds(el: Element): {\n startMs: number;\n endMs: number;\n} {\n // Non-temporal elements are always visible\n if (!isTemporal(el)) {\n return { startMs: -Infinity, endMs: Infinity };\n }\n\n // Compute bounds fresh each time\n const temporal = el as TemporalElement;\n let startMs = temporal.startTimeMs ?? -Infinity;\n let endMs = temporal.endTimeMs ?? Infinity;\n\n // If end <= start, treat as always visible (element hasn't computed duration yet)\n if (endMs <= startMs) {\n startMs = -Infinity;\n endMs = Infinity;\n }\n\n return { startMs, endMs };\n}\n\n/**\n * Check if an element is temporally visible at the given time.\n */\nexport function isVisibleAtTime(element: Element, timeMs: number): boolean {\n const { startMs, endMs } = getTemporalBounds(element);\n return timeMs >= startMs && timeMs <= endMs;\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n/** Default timegroup dimensions when not measurable */\nexport const DEFAULT_WIDTH = 1920;\nexport const DEFAULT_HEIGHT = 1080;\n\n/** Default scale for capture operations */\nexport const DEFAULT_CAPTURE_SCALE = 0.25;\n\n/** Default timeout for blocking content readiness mode (ms) */\nexport const DEFAULT_BLOCKING_TIMEOUT_MS = 5000;\n\n// ============================================================================\n// Container Creation\n// ============================================================================\n\n/**\n * Options for creating a preview container.\n */\nexport interface PreviewContainerOptions {\n width: number;\n height: number;\n background?: string;\n position?: \"relative\" | \"absolute\" | \"fixed\";\n}\n\n/**\n * Create a preview container with standard styling.\n * Consolidates the repeated container creation pattern across preview functions.\n */\nexport function createPreviewContainer(
|
|
1
|
+
{"version":3,"file":"previewTypes.js","names":[],"sources":["../../src/preview/previewTypes.ts"],"sourcesContent":["/**\n * Shared types and constants for preview rendering.\n *\n * Consolidates duplicate definitions from renderTimegroupToCanvas.ts and\n * renderTimegroupPreview.ts into a single source of truth.\n */\n\n// ============================================================================\n// Temporal Types\n// ============================================================================\n\n/**\n * Element with temporal properties (startTimeMs, endTimeMs).\n * Used for temporal visibility checks during preview rendering.\n */\nexport interface TemporalElement extends Element {\n startTimeMs?: number;\n endTimeMs?: number;\n src?: string;\n}\n\n/**\n * Type guard to check if an element has temporal properties.\n */\nexport function isTemporal(el: Element): el is TemporalElement {\n return \"startTimeMs\" in el && \"endTimeMs\" in el;\n}\n\n/**\n * Get temporal bounds for an element, treating invalid ranges as unbounded.\n * Invalid range (end <= start) means element hasn't computed its duration yet.\n *\n * NOTE: No caching - bounds are computed dynamically by timegroups based on\n * composition mode (sequence/contain/fit) and may change between frames.\n */\nexport function getTemporalBounds(el: Element): {\n startMs: number;\n endMs: number;\n} {\n // Non-temporal elements are always visible\n if (!isTemporal(el)) {\n return { startMs: -Infinity, endMs: Infinity };\n }\n\n // Compute bounds fresh each time\n const temporal = el as TemporalElement;\n let startMs = temporal.startTimeMs ?? -Infinity;\n let endMs = temporal.endTimeMs ?? Infinity;\n\n // If end <= start, treat as always visible (element hasn't computed duration yet)\n if (endMs <= startMs) {\n startMs = -Infinity;\n endMs = Infinity;\n }\n\n return { startMs, endMs };\n}\n\n/**\n * Check if an element is temporally visible at the given time.\n */\nexport function isVisibleAtTime(element: Element, timeMs: number): boolean {\n const { startMs, endMs } = getTemporalBounds(element);\n return timeMs >= startMs && timeMs <= endMs;\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n/** Default timegroup dimensions when not measurable */\nexport const DEFAULT_WIDTH = 1920;\nexport const DEFAULT_HEIGHT = 1080;\n\n/** Default scale for capture operations */\nexport const DEFAULT_CAPTURE_SCALE = 0.25;\n\n/** Default timeout for blocking content readiness mode (ms) */\nexport const DEFAULT_BLOCKING_TIMEOUT_MS = 5000;\n\n// ============================================================================\n// Container Creation\n// ============================================================================\n\n/**\n * Options for creating a preview container.\n */\nexport interface PreviewContainerOptions {\n width: number;\n height: number;\n background?: string;\n position?: \"relative\" | \"absolute\" | \"fixed\";\n}\n\n/**\n * Create a preview container with standard styling.\n * Consolidates the repeated container creation pattern across preview functions.\n */\nexport function createPreviewContainer(options: PreviewContainerOptions): HTMLDivElement {\n const { width, height, background = \"#000\", position = \"relative\" } = options;\n\n const container = document.createElement(\"div\");\n container.style.cssText = `\n width: ${width}px;\n height: ${height}px;\n position: ${position};\n overflow: hidden;\n background: ${background};\n `;\n return container;\n}\n\n// ============================================================================\n// Style Injection\n// ============================================================================\n\n/**\n * Inject document styles into a container for foreignObject rendering.\n * SVG foreignObject needs all CSS rules inlined since it can't access\n * the document's stylesheets.\n */\nexport function injectDocumentStyles(\n container: HTMLElement,\n collectStyles: () => string,\n): HTMLStyleElement {\n const styleEl = document.createElement(\"style\");\n styleEl.textContent = collectStyles();\n container.appendChild(styleEl);\n return styleEl;\n}\n"],"mappings":";;;;AAwBA,SAAgB,WAAW,IAAoC;AAC7D,QAAO,iBAAiB,MAAM,eAAe;;;;;;;;;AAU/C,SAAgB,kBAAkB,IAGhC;AAEA,KAAI,CAAC,WAAW,GAAG,CACjB,QAAO;EAAE,SAAS;EAAW,OAAO;EAAU;CAIhD,MAAM,WAAW;CACjB,IAAI,UAAU,SAAS,eAAe;CACtC,IAAI,QAAQ,SAAS,aAAa;AAGlC,KAAI,SAAS,SAAS;AACpB,YAAU;AACV,UAAQ;;AAGV,QAAO;EAAE;EAAS;EAAO;;;;;AAM3B,SAAgB,gBAAgB,SAAkB,QAAyB;CACzE,MAAM,EAAE,SAAS,UAAU,kBAAkB,QAAQ;AACrD,QAAO,UAAU,WAAW,UAAU;;;AAQxC,MAAa,gBAAgB;AAC7B,MAAa,iBAAiB;;AAG9B,MAAa,wBAAwB;;AAGrC,MAAa,8BAA8B;;;;;AAoB3C,SAAgB,uBAAuB,SAAkD;CACvF,MAAM,EAAE,OAAO,QAAQ,aAAa,QAAQ,WAAW,eAAe;CAEtE,MAAM,YAAY,SAAS,cAAc,MAAM;AAC/C,WAAU,MAAM,UAAU;aACf,MAAM;cACL,OAAO;gBACL,SAAS;;kBAEP,WAAW;;AAE3B,QAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderElementToCanvas.js","names":[],"sources":["../../src/preview/renderElementToCanvas.ts"],"sourcesContent":["/**\n * Render any DOM element to canvas.\n *\n * Low-level rendering function that renders elements as-is.\n * Supports both native (drawElementImage) and foreignObject render modes.\n *\n * Caller is responsible for clone management and seeking.\n */\n\nimport { getEffectiveRenderMode } from \"./renderers.js\";\nimport type { RenderMode } from \"./previewSettings.js\";\nimport { RenderContext } from \"./RenderContext.js\";\nimport { captureTimelineToDataUri } from \"./rendering/serializeTimelineDirect.js\";\nimport { loadImageFromDataUri } from \"./rendering/loadImage.js\";\nimport { renderToImageNative } from \"./rendering/renderToImageNative.js\";\nimport { DEFAULT_WIDTH, DEFAULT_HEIGHT } from \"./previewTypes.js\";\n\n/**\n * Options for rendering an element to canvas.\n */\nexport interface RenderElementOptions {\n /** Time to render at in milliseconds (used for serialization metadata) */\n timeMs: number;\n /** Scale factor for canvas encoding (default: 1.0) */\n scale?: number;\n /** Output width in pixels (defaults to element's computed width or 1920) */\n width?: number;\n /** Output height in pixels (defaults to element's computed height or 1080) */\n height?: number;\n /** Render context for canvas pixel caching */\n renderContext?: RenderContext;\n /** Override render mode (native or foreignObject) */\n renderMode?: RenderMode;\n}\n\n/**\n * Render any element to canvas or image.\n *\n * This is a low-level rendering function that renders the element as-is.\n * The caller is responsible for:\n * - Creating clones if needed\n * - Seeking to the correct time\n * - Finding the correct element to render\n *\n * Use cases:\n * - Preview: Pass prime timeline element (already at correct time)\n * - Video/thumbnails: Pass element from reused clone (already seeked)\n * - One-off capture: Create clone, seek, pass element, clean up\n *\n * @param element - Element to render (timegroup, temporal element, or plain DOM)\n * @param options - Render options\n * @returns Canvas or Image (both are CanvasImageSource)\n */\nexport async function renderElementToCanvas(\n element: Element,\n options: RenderElementOptions,\n): Promise<CanvasImageSource> {\n return await renderElementToImage(element, options);\n}\n\n/**\n * Render an element using either native or foreignObject mode.\n * Returns Canvas or Image directly without unnecessary copying.\n */\nasync function renderElementToImage(\n element: Element,\n options: RenderElementOptions,\n): Promise<CanvasImageSource> {\n const { timeMs, scale = 1.0 } = options;\n\n // Get element dimensions\n const computedStyle = getComputedStyle(element);\n const width
|
|
1
|
+
{"version":3,"file":"renderElementToCanvas.js","names":[],"sources":["../../src/preview/renderElementToCanvas.ts"],"sourcesContent":["/**\n * Render any DOM element to canvas.\n *\n * Low-level rendering function that renders elements as-is.\n * Supports both native (drawElementImage) and foreignObject render modes.\n *\n * Caller is responsible for clone management and seeking.\n */\n\nimport { getEffectiveRenderMode } from \"./renderers.js\";\nimport type { RenderMode } from \"./previewSettings.js\";\nimport { RenderContext } from \"./RenderContext.js\";\nimport { captureTimelineToDataUri } from \"./rendering/serializeTimelineDirect.js\";\nimport { loadImageFromDataUri } from \"./rendering/loadImage.js\";\nimport { renderToImageNative } from \"./rendering/renderToImageNative.js\";\nimport { DEFAULT_WIDTH, DEFAULT_HEIGHT } from \"./previewTypes.js\";\n\n/**\n * Options for rendering an element to canvas.\n */\nexport interface RenderElementOptions {\n /** Time to render at in milliseconds (used for serialization metadata) */\n timeMs: number;\n /** Scale factor for canvas encoding (default: 1.0) */\n scale?: number;\n /** Output width in pixels (defaults to element's computed width or 1920) */\n width?: number;\n /** Output height in pixels (defaults to element's computed height or 1080) */\n height?: number;\n /** Render context for canvas pixel caching */\n renderContext?: RenderContext;\n /** Override render mode (native or foreignObject) */\n renderMode?: RenderMode;\n}\n\n/**\n * Render any element to canvas or image.\n *\n * This is a low-level rendering function that renders the element as-is.\n * The caller is responsible for:\n * - Creating clones if needed\n * - Seeking to the correct time\n * - Finding the correct element to render\n *\n * Use cases:\n * - Preview: Pass prime timeline element (already at correct time)\n * - Video/thumbnails: Pass element from reused clone (already seeked)\n * - One-off capture: Create clone, seek, pass element, clean up\n *\n * @param element - Element to render (timegroup, temporal element, or plain DOM)\n * @param options - Render options\n * @returns Canvas or Image (both are CanvasImageSource)\n */\nexport async function renderElementToCanvas(\n element: Element,\n options: RenderElementOptions,\n): Promise<CanvasImageSource> {\n return await renderElementToImage(element, options);\n}\n\n/**\n * Render an element using either native or foreignObject mode.\n * Returns Canvas or Image directly without unnecessary copying.\n */\nasync function renderElementToImage(\n element: Element,\n options: RenderElementOptions,\n): Promise<CanvasImageSource> {\n const { timeMs, scale = 1.0 } = options;\n\n // Get element dimensions\n const computedStyle = getComputedStyle(element);\n const width = options.width ?? (parseFloat(computedStyle.width) || DEFAULT_WIDTH);\n const height = options.height ?? (parseFloat(computedStyle.height) || DEFAULT_HEIGHT);\n\n // Create render context for caching\n const renderContext = options.renderContext ?? new RenderContext();\n const shouldDisposeContext = !options.renderContext;\n\n try {\n // Determine render mode\n const renderMode = options.renderMode ?? getEffectiveRenderMode();\n\n if (renderMode === \"native\") {\n // NATIVE PATH: Render element using drawElementImage\n const elementContainer = document.createElement(\"div\");\n elementContainer.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n pointer-events: none;\n overflow: hidden;\n `;\n\n // Clone element into container\n elementContainer.appendChild(element.cloneNode(true));\n document.body.appendChild(elementContainer);\n\n try {\n // Return canvas directly - no copy needed!\n return await renderToImageNative(elementContainer, width, height, {\n skipDprScaling: true,\n });\n } finally {\n elementContainer.remove();\n }\n } else {\n // FOREIGNOBJECT PATH: Direct serialization\n const dataUri = await captureTimelineToDataUri(element, width, height, {\n renderContext,\n canvasScale: scale,\n timeMs,\n });\n\n // Return image directly - no copy needed!\n return await loadImageFromDataUri(dataUri);\n }\n } finally {\n if (shouldDisposeContext) {\n renderContext.dispose();\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAqDA,eAAsB,sBACpB,SACA,SAC4B;AAC5B,QAAO,MAAM,qBAAqB,SAAS,QAAQ;;;;;;AAOrD,eAAe,qBACb,SACA,SAC4B;CAC5B,MAAM,EAAE,QAAQ,QAAQ,MAAQ;CAGhC,MAAM,gBAAgB,iBAAiB,QAAQ;CAC/C,MAAM,QAAQ,QAAQ,UAAU,WAAW,cAAc,MAAM,IAAI;CACnE,MAAM,SAAS,QAAQ,WAAW,WAAW,cAAc,OAAO,IAAI;CAGtE,MAAM,gBAAgB,QAAQ,iBAAiB,IAAI,eAAe;CAClE,MAAM,uBAAuB,CAAC,QAAQ;AAEtC,KAAI;AAIF,OAFmB,QAAQ,cAAc,wBAAwB,MAE9C,UAAU;GAE3B,MAAM,mBAAmB,SAAS,cAAc,MAAM;AACtD,oBAAiB,MAAM,UAAU;;;;iBAItB,MAAM;kBACL,OAAO;;;;AAMnB,oBAAiB,YAAY,QAAQ,UAAU,KAAK,CAAC;AACrD,YAAS,KAAK,YAAY,iBAAiB;AAE3C,OAAI;AAEF,WAAO,MAAM,oBAAoB,kBAAkB,OAAO,QAAQ,EAChE,gBAAgB,MACjB,CAAC;aACM;AACR,qBAAiB,QAAQ;;QAW3B,QAAO,MAAM,qBAPG,MAAM,yBAAyB,SAAS,OAAO,QAAQ;GACrE;GACA,aAAa;GACb;GACD,CAAC,CAGwC;WAEpC;AACR,MAAI,qBACF,eAAc,SAAS"}
|
|
@@ -490,11 +490,6 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
|
|
|
490
490
|
lastTimeMs = -1;
|
|
491
491
|
};
|
|
492
492
|
const getResolutionScale = () => pendingResolutionScale ?? currentResolutionScale;
|
|
493
|
-
let frameCount = 0;
|
|
494
|
-
let totalFrameControllerMs = 0;
|
|
495
|
-
let totalCaptureMs = 0;
|
|
496
|
-
let totalCopyMs = 0;
|
|
497
|
-
let totalFrameMs = 0;
|
|
498
493
|
const refresh = async () => {
|
|
499
494
|
if (disposed) return;
|
|
500
495
|
const sourceTimeMs = timegroup.currentTimeMs ?? 0;
|
|
@@ -511,16 +506,12 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
|
|
|
511
506
|
logger.debug(`[renderTimegroupToCanvas] Resolution scale: ${currentResolutionScale} (${width}x${height} → ${renderWidth}x${renderHeight}), canvas buffer: ${canvas.width}x${canvas.height}, CSS size: ${canvas.style.width}x${canvas.style.height}, renderMode: ${mode}`);
|
|
512
507
|
}
|
|
513
508
|
try {
|
|
514
|
-
const tFrame = performance.now();
|
|
515
|
-
const tFC0 = performance.now();
|
|
516
509
|
await frameController.renderFrame(userTimeMs, {
|
|
517
510
|
waitForLitUpdate: false,
|
|
518
511
|
onAnimationsUpdate: (root) => {
|
|
519
512
|
updateAnimations(root);
|
|
520
513
|
}
|
|
521
514
|
});
|
|
522
|
-
const fcMs = performance.now() - tFC0;
|
|
523
|
-
const tCapture0 = performance.now();
|
|
524
515
|
if (useNative && captureCanvas && captureCtx) {
|
|
525
516
|
if (captureCanvas.width !== width || captureCanvas.height !== height) {
|
|
526
517
|
captureCtx.save();
|
|
@@ -528,8 +519,6 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
|
|
|
528
519
|
captureCtx.drawElementImage(timegroup, 0, 0);
|
|
529
520
|
captureCtx.restore();
|
|
530
521
|
} else captureCtx.drawElementImage(timegroup, 0, 0);
|
|
531
|
-
const captureMs = performance.now() - tCapture0;
|
|
532
|
-
const tCopy0 = performance.now();
|
|
533
522
|
const targetWidth = Math.floor(renderWidth * scale * dpr);
|
|
534
523
|
const targetHeight = Math.floor(renderHeight * scale * dpr);
|
|
535
524
|
if (canvas.width !== targetWidth || canvas.height !== targetHeight) {
|
|
@@ -537,32 +526,14 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
|
|
|
537
526
|
canvas.height = targetHeight;
|
|
538
527
|
} else ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
539
528
|
ctx.drawImage(captureCanvas, 0, 0, canvas.width, canvas.height);
|
|
540
|
-
const copyMs = performance.now() - tCopy0;
|
|
541
|
-
const frameMs = performance.now() - tFrame;
|
|
542
|
-
frameCount++;
|
|
543
|
-
totalFrameControllerMs += fcMs;
|
|
544
|
-
totalCaptureMs += captureMs;
|
|
545
|
-
totalCopyMs += copyMs;
|
|
546
|
-
totalFrameMs += frameMs;
|
|
547
529
|
defaultProfiler.incrementRenderCount();
|
|
548
|
-
if (defaultProfiler.shouldLogByFrameCount(60)) {
|
|
549
|
-
frameCount = 0;
|
|
550
|
-
totalFrameControllerMs = 0;
|
|
551
|
-
totalCaptureMs = 0;
|
|
552
|
-
totalCopyMs = 0;
|
|
553
|
-
totalFrameMs = 0;
|
|
554
|
-
}
|
|
555
530
|
} else {
|
|
556
531
|
const absoluteTimeMs = toAbsoluteTime(timegroup, userTimeMs);
|
|
557
|
-
const
|
|
532
|
+
const image = await loadImageFromDataUri(await captureTimelineToDataUri(timegroup, width, height, {
|
|
558
533
|
renderContext,
|
|
559
534
|
canvasScale: currentResolutionScale,
|
|
560
535
|
timeMs: absoluteTimeMs
|
|
561
|
-
});
|
|
562
|
-
const captureMs = performance.now() - tCapture0;
|
|
563
|
-
const tCopy0 = performance.now();
|
|
564
|
-
const image = await loadImageFromDataUri(dataUri);
|
|
565
|
-
const copyMs = performance.now() - tCopy0;
|
|
536
|
+
}));
|
|
566
537
|
const targetWidth = Math.floor(renderWidth * scale * dpr);
|
|
567
538
|
const targetHeight = Math.floor(renderHeight * scale * dpr);
|
|
568
539
|
if (canvas.width !== targetWidth || canvas.height !== targetHeight) {
|
|
@@ -573,20 +544,7 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
|
|
|
573
544
|
ctx.scale(dpr * scale, dpr * scale);
|
|
574
545
|
ctx.drawImage(image, 0, 0, renderWidth, renderHeight);
|
|
575
546
|
ctx.restore();
|
|
576
|
-
const frameMs = performance.now() - tFrame;
|
|
577
|
-
frameCount++;
|
|
578
|
-
totalFrameControllerMs += fcMs;
|
|
579
|
-
totalCaptureMs += captureMs;
|
|
580
|
-
totalCopyMs += copyMs;
|
|
581
|
-
totalFrameMs += frameMs;
|
|
582
547
|
defaultProfiler.incrementRenderCount();
|
|
583
|
-
if (defaultProfiler.shouldLogByFrameCount(60)) {
|
|
584
|
-
frameCount = 0;
|
|
585
|
-
totalFrameControllerMs = 0;
|
|
586
|
-
totalCaptureMs = 0;
|
|
587
|
-
totalCopyMs = 0;
|
|
588
|
-
totalFrameMs = 0;
|
|
589
|
-
}
|
|
590
548
|
}
|
|
591
549
|
} catch (e) {
|
|
592
550
|
logger.error("Canvas preview render failed:", e);
|