@editframe/elements 0.38.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/canvas/EFCanvas.d.ts +4 -4
  2. package/dist/canvas/EFCanvasItem.d.ts +4 -4
  3. package/dist/canvas/overlays/SelectionOverlay.d.ts +2 -2
  4. package/dist/canvas/overlays/SelectionOverlay.js.map +1 -1
  5. package/dist/elements/EFCaptions.js +1 -1
  6. package/dist/elements/EFCaptions.js.map +1 -1
  7. package/dist/elements/EFImage.js +3 -4
  8. package/dist/elements/EFImage.js.map +1 -1
  9. package/dist/elements/EFMedia/BufferedSeekingInput.js +1 -1
  10. package/dist/elements/EFMedia/CachedFetcher.js +99 -0
  11. package/dist/elements/EFMedia/CachedFetcher.js.map +1 -0
  12. package/dist/elements/EFMedia/MediaEngine.d.ts +19 -0
  13. package/dist/elements/EFMedia/MediaEngine.js +129 -0
  14. package/dist/elements/EFMedia/MediaEngine.js.map +1 -0
  15. package/dist/elements/EFMedia/SegmentIndex.d.ts +32 -0
  16. package/dist/elements/EFMedia/SegmentIndex.js +185 -0
  17. package/dist/elements/EFMedia/SegmentIndex.js.map +1 -0
  18. package/dist/elements/EFMedia/SegmentTransport.d.ts +12 -0
  19. package/dist/elements/EFMedia/SegmentTransport.js +69 -0
  20. package/dist/elements/EFMedia/SegmentTransport.js.map +1 -0
  21. package/dist/elements/EFMedia/TimingModel.d.ts +10 -0
  22. package/dist/elements/EFMedia/TimingModel.js +28 -0
  23. package/dist/elements/EFMedia/TimingModel.js.map +1 -0
  24. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +7 -6
  25. package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
  26. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +13 -34
  27. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
  28. package/dist/elements/EFMedia.d.ts +2 -1
  29. package/dist/elements/EFMedia.js +14 -31
  30. package/dist/elements/EFMedia.js.map +1 -1
  31. package/dist/elements/EFPanZoom.d.ts +4 -4
  32. package/dist/elements/EFSourceMixin.js +1 -1
  33. package/dist/elements/EFSourceMixin.js.map +1 -1
  34. package/dist/elements/EFSurface.d.ts +4 -4
  35. package/dist/elements/EFTemporal.js +2 -1
  36. package/dist/elements/EFTemporal.js.map +1 -1
  37. package/dist/elements/EFTimegroup.js +2 -1
  38. package/dist/elements/EFTimegroup.js.map +1 -1
  39. package/dist/elements/EFVideo.js +204 -187
  40. package/dist/elements/EFVideo.js.map +1 -1
  41. package/dist/gui/EFActiveRootTemporal.d.ts +4 -4
  42. package/dist/gui/EFConfiguration.d.ts +0 -7
  43. package/dist/gui/EFConfiguration.js +0 -5
  44. package/dist/gui/EFConfiguration.js.map +1 -1
  45. package/dist/gui/EFControls.d.ts +2 -2
  46. package/dist/gui/EFDial.d.ts +4 -4
  47. package/dist/gui/EFFocusOverlay.d.ts +4 -4
  48. package/dist/gui/EFOverlayItem.d.ts +4 -4
  49. package/dist/gui/EFOverlayLayer.d.ts +4 -4
  50. package/dist/gui/EFPause.d.ts +4 -4
  51. package/dist/gui/EFPlay.d.ts +4 -4
  52. package/dist/gui/EFResizableBox.d.ts +4 -4
  53. package/dist/gui/EFScrubber.d.ts +4 -4
  54. package/dist/gui/EFTimeDisplay.d.ts +4 -4
  55. package/dist/gui/EFTimelineRuler.d.ts +4 -4
  56. package/dist/gui/EFToggleLoop.d.ts +4 -4
  57. package/dist/gui/EFTogglePlay.d.ts +4 -4
  58. package/dist/gui/EFTransformHandles.d.ts +4 -4
  59. package/dist/gui/EFWorkbench.d.ts +2 -0
  60. package/dist/gui/EFWorkbench.js +68 -1
  61. package/dist/gui/EFWorkbench.js.map +1 -1
  62. package/dist/gui/PlaybackController.d.ts +2 -0
  63. package/dist/gui/PlaybackController.js +11 -1
  64. package/dist/gui/PlaybackController.js.map +1 -1
  65. package/dist/gui/ef-theme.css +11 -0
  66. package/dist/gui/timeline/EFTimeline.js.map +1 -1
  67. package/dist/gui/timeline/EFTimelineRow.d.ts +2 -2
  68. package/dist/gui/timeline/tracks/AudioTrack.js +28 -30
  69. package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -1
  70. package/dist/gui/timeline/tracks/EFThumbnailStrip.d.ts +1 -0
  71. package/dist/gui/timeline/tracks/EFThumbnailStrip.js +41 -8
  72. package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -1
  73. package/dist/gui/timeline/tracks/VideoTrack.js +2 -2
  74. package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -1
  75. package/dist/gui/timeline/tracks/waveformUtils.js +19 -19
  76. package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -1
  77. package/dist/gui/tree/EFTree.d.ts +4 -4
  78. package/dist/gui/tree/EFTreeItem.d.ts +4 -4
  79. package/dist/preview/QualityUpgradeScheduler.d.ts +8 -0
  80. package/dist/preview/QualityUpgradeScheduler.js +13 -1
  81. package/dist/preview/QualityUpgradeScheduler.js.map +1 -1
  82. package/dist/preview/renderTimegroupToCanvas.d.ts +144 -0
  83. package/dist/preview/renderTimegroupToCanvas.js +56 -3
  84. package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
  85. package/dist/preview/renderTimegroupToCanvas.types.d.ts +22 -1
  86. package/dist/preview/renderTimegroupToVideo.d.ts +27 -0
  87. package/dist/preview/renderTimegroupToVideo.js +13 -5
  88. package/dist/preview/renderTimegroupToVideo.js.map +1 -1
  89. package/dist/preview/renderVideoToVideo.js +5 -6
  90. package/dist/preview/renderVideoToVideo.js.map +1 -1
  91. package/dist/preview/renderers.d.ts +56 -0
  92. package/dist/preview/renderers.js +13 -1
  93. package/dist/preview/renderers.js.map +1 -1
  94. package/dist/preview/rendering/inlineImages.d.ts +13 -0
  95. package/dist/preview/rendering/inlineImages.js +7 -1
  96. package/dist/preview/rendering/inlineImages.js.map +1 -1
  97. package/dist/preview/rendering/loadImage.d.ts +8 -0
  98. package/dist/render/EFRenderAPI.js.map +1 -1
  99. package/dist/transcoding/types/index.d.ts +6 -94
  100. package/dist/transcoding/utils/UrlGenerator.d.ts +3 -12
  101. package/dist/transcoding/utils/UrlGenerator.js +3 -29
  102. package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
  103. package/package.json +26 -2
  104. package/test/setup.ts +1 -1
  105. package/test/useAssetMSW.ts +0 -100
  106. package/tsdown.config.ts +6 -1
  107. package/dist/elements/EFMedia/AssetMediaEngine.js +0 -284
  108. package/dist/elements/EFMedia/AssetMediaEngine.js.map +0 -1
  109. package/dist/elements/EFMedia/BaseMediaEngine.js +0 -200
  110. package/dist/elements/EFMedia/BaseMediaEngine.js.map +0 -1
  111. package/dist/elements/EFMedia/FileMediaEngine.js +0 -122
  112. package/dist/elements/EFMedia/FileMediaEngine.js.map +0 -1
  113. package/dist/elements/EFMedia/JitMediaEngine.js +0 -157
  114. package/dist/elements/EFMedia/JitMediaEngine.js.map +0 -1
@@ -1,8 +1,8 @@
1
1
  import { TreeItem } from "./treeContext.js";
2
2
  import "./EFTreeItem.js";
3
- import * as lit14 from "lit";
3
+ import * as lit13 from "lit";
4
4
  import { LitElement, PropertyValues } from "lit";
5
- import * as lit_html13 from "lit-html";
5
+ import * as lit_html12 from "lit-html";
6
6
 
7
7
  //#region src/gui/tree/EFTree.d.ts
8
8
 
@@ -29,7 +29,7 @@ import * as lit_html13 from "lit-html";
29
29
  * ```
30
30
  */
31
31
  declare class EFTree extends LitElement {
32
- static styles: lit14.CSSResult;
32
+ static styles: lit13.CSSResult;
33
33
  /** Tree items to display */
34
34
  items: TreeItem[];
35
35
  /** Optional header text */
@@ -48,7 +48,7 @@ declare class EFTree extends LitElement {
48
48
  protected willUpdate(changedProperties: PropertyValues): void;
49
49
  protected updated(changedProperties: PropertyValues): void;
50
50
  connectedCallback(): void;
51
- render(): lit_html13.TemplateResult<1>;
51
+ render(): lit_html12.TemplateResult<1>;
52
52
  }
53
53
  declare global {
54
54
  interface HTMLElementTagNameMap {
@@ -1,7 +1,7 @@
1
1
  import { TreeContext, TreeItem } from "./treeContext.js";
2
- import * as lit15 from "lit";
2
+ import * as lit14 from "lit";
3
3
  import { LitElement, nothing } from "lit";
4
- import * as lit_html14 from "lit-html";
4
+ import * as lit_html13 from "lit-html";
5
5
 
6
6
  //#region src/gui/tree/EFTreeItem.d.ts
7
7
 
@@ -17,7 +17,7 @@ import * as lit_html14 from "lit-html";
17
17
  * @fires tree-item-click - When item is clicked (for selection)
18
18
  */
19
19
  declare class EFTreeItem extends LitElement {
20
- static styles: lit15.CSSResult;
20
+ static styles: lit14.CSSResult;
21
21
  treeContext?: TreeContext;
22
22
  item: TreeItem;
23
23
  private localExpanded;
@@ -26,7 +26,7 @@ declare class EFTreeItem extends LitElement {
26
26
  get hasChildren(): boolean;
27
27
  private handleClick;
28
28
  private handleExpandClick;
29
- render(): lit_html14.TemplateResult<1> | typeof nothing;
29
+ render(): lit_html13.TemplateResult<1> | typeof nothing;
30
30
  }
31
31
  declare global {
32
32
  interface HTMLElementTagNameMap {
@@ -58,6 +58,14 @@ declare class QualityUpgradeScheduler {
58
58
  * Removes queued tasks. Does NOT abort in-flight fetches.
59
59
  */
60
60
  cancelForOwner(owner: string): void;
61
+ /**
62
+ * Check whether a task is currently in-flight (started, not yet complete).
63
+ */
64
+ isActive(key: string): boolean;
65
+ /**
66
+ * Check whether a task is waiting in the queue (submitted but not yet started).
67
+ */
68
+ isPending(key: string): boolean;
61
69
  /**
62
70
  * Get snapshot of current queue state for debugging.
63
71
  */
@@ -35,7 +35,7 @@ var QualityUpgradeScheduler = class {
35
35
  if (this.#abortController.signal.aborted) return;
36
36
  this.#queue = this.#queue.filter((t) => t.owner !== owner);
37
37
  for (const task of tasks) {
38
- if (this.#activeTasks.has(task.key) || this.#completedTasks.has(task.key)) continue;
38
+ if (this.#activeTasks.has(task.key)) continue;
39
39
  this.#queue.push(task);
40
40
  }
41
41
  this.#queue.sort((a, b) => a.deadlineMs - b.deadlineMs);
@@ -98,6 +98,18 @@ var QualityUpgradeScheduler = class {
98
98
  });
99
99
  }
100
100
  /**
101
+ * Check whether a task is currently in-flight (started, not yet complete).
102
+ */
103
+ isActive(key) {
104
+ return this.#activeTasks.has(key);
105
+ }
106
+ /**
107
+ * Check whether a task is waiting in the queue (submitted but not yet started).
108
+ */
109
+ isPending(key) {
110
+ return this.#queue.some((t) => t.key === key);
111
+ }
112
+ /**
101
113
  * Get snapshot of current queue state for debugging.
102
114
  */
103
115
  getQueueSnapshot() {
@@ -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 if already active or completed\n if (\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 * 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 * 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 (\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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"}
@@ -0,0 +1,144 @@
1
+ import { EFTimegroup } from "../elements/EFTimegroup.js";
2
+ import { CanvasPreviewOptions, CanvasPreviewResult, CaptureFromCloneOptions, CaptureOptions, ContentReadyMode, GenerateThumbnailsOptions, GeneratedThumbnail, ThumbnailQueue } from "./renderTimegroupToCanvas.types.js";
3
+ import { RenderOptions, RenderResult, Renderer, getEffectiveRenderMode, isCanvas, isImage } from "./renderers.js";
4
+ import { loadImageFromDataUri } from "./rendering/loadImage.js";
5
+ import { clearInlineImageCache, getInlineImageCacheSize } from "./rendering/inlineImages.js";
6
+
7
+ //#region src/preview/renderTimegroupToCanvas.d.ts
8
+
9
+ /**
10
+ * Error thrown when video content is not ready within the blocking timeout.
11
+ */
12
+ declare class ContentNotReadyError extends Error {
13
+ readonly timeMs: number;
14
+ readonly timeoutMs: number;
15
+ readonly blankVideos: string[];
16
+ constructor(timeMs: number, timeoutMs: number, blankVideos: string[]);
17
+ }
18
+ /**
19
+ * Module-level render state including caches and reusable objects.
20
+ */
21
+ interface RenderState {
22
+ inlineImageCache: Map<string, string>;
23
+ layoutInitializedCanvases: WeakSet<HTMLCanvasElement>;
24
+ xmlSerializer: XMLSerializer | null;
25
+ textEncoder: TextEncoder;
26
+ metrics: {
27
+ inlineImageCacheHits: number;
28
+ inlineImageCacheMisses: number;
29
+ inlineImageCacheEvictions: number;
30
+ };
31
+ }
32
+ /**
33
+ * Get the current render state for testing and debugging.
34
+ * @returns The module-level render state object
35
+ */
36
+ declare function getRenderState(): RenderState;
37
+ /**
38
+ * Get cache metrics for monitoring performance.
39
+ * @returns Object with cache hit/miss/eviction counts
40
+ */
41
+ declare function getCacheMetrics(): RenderState["metrics"];
42
+ /**
43
+ * Reset cache metrics to zero.
44
+ */
45
+ declare function resetCacheMetrics(): void;
46
+ /**
47
+ * Reset all module state including profiling counters, caches, and logging flags.
48
+ * Call at the start of export sessions to ensure clean state.
49
+ */
50
+ declare function resetRenderState(): void;
51
+ interface WaitForVideoContentResult {
52
+ ready: boolean;
53
+ blankVideos: string[];
54
+ }
55
+ /**
56
+ * Wait for video canvases within a timegroup to have content.
57
+ * Only checks videos that should be visible at the current time.
58
+ * Returns result with ready status and list of blank video names.
59
+ */
60
+ declare function waitForVideoContent(timegroup: EFTimegroup, timeMs: number, maxWaitMs: number): Promise<WaitForVideoContentResult>;
61
+ /**
62
+ * Captures a frame from an already-seeked render clone.
63
+ * Used internally by captureBatch for efficiency (reuses one clone across all captures).
64
+ *
65
+ * @param renderClone - A render clone that has already been seeked to the target time
66
+ * @param renderContainer - The container holding the render clone (from createRenderClone)
67
+ * @param options - Capture options
68
+ * @returns Canvas or Image with the rendered frame (both are CanvasImageSource)
69
+ */
70
+ declare function captureFromClone(renderClone: EFTimegroup, _renderContainer: HTMLElement, options?: CaptureFromCloneOptions): Promise<CanvasImageSource>;
71
+ /**
72
+ * Captures a single frame from a timegroup at a specific time.
73
+ *
74
+ * CLONE-TIMELINE ARCHITECTURE:
75
+ * Creates an independent render clone, seeks it to the target time, and captures.
76
+ * Prime-timeline is NEVER seeked - user can continue previewing/editing during capture.
77
+ *
78
+ * @param timegroup - The source timegroup
79
+ * @param options - Capture options including timeMs, scale, contentReadyMode
80
+ * @returns Canvas with the rendered frame
81
+ * @throws ContentNotReadyError if blocking mode times out waiting for video content
82
+ */
83
+ declare function captureTimegroupAtTime(timegroup: EFTimegroup, options: CaptureOptions): Promise<CanvasImageSource>;
84
+ /**
85
+ * Generate thumbnails using an existing render clone and mutable queue.
86
+ * The queue can be modified while generation is in progress.
87
+ *
88
+ * @param renderClone - Pre-created render clone to use
89
+ * @param renderContainer - Container for the render clone
90
+ * @param queue - Mutable queue that provides timestamps
91
+ * @param options - Capture options (scale, contentReadyMode, etc.)
92
+ * @yields Objects with { timeMs, canvas } for each captured thumbnail
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * const queue = new MutableTimestampQueue();
97
+ * queue.reset([0, 100, 200]);
98
+ *
99
+ * for await (const { timeMs, canvas } of generateThumbnailsFromClone(clone, container, queue)) {
100
+ * cache.set(timeMs, canvas);
101
+ * // Queue can be modified here while generator continues
102
+ * }
103
+ * ```
104
+ */
105
+ declare function generateThumbnailsFromClone(renderClone: EFTimegroup, renderContainer: HTMLElement, queue: ThumbnailQueue, options?: GenerateThumbnailsOptions): AsyncGenerator<GeneratedThumbnail>;
106
+ /**
107
+ * Generate thumbnails for multiple timestamps efficiently using a single render clone.
108
+ * This avoids the overhead of creating/destroying a clone for each thumbnail.
109
+ *
110
+ * @param timegroup - The timegroup to capture
111
+ * @param timestamps - Array of timestamps to capture (in milliseconds)
112
+ * @param options - Capture options (scale, contentReadyMode, etc.)
113
+ * @param signal - Optional AbortSignal to cancel generation
114
+ * @yields Objects with { timeMs, canvas } for each captured thumbnail
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * for await (const { timeMs, canvas } of generateThumbnails(tg, [0, 100, 200])) {
119
+ * console.log(`Got thumbnail for ${timeMs}ms`);
120
+ * thumbnailCache.set(timeMs, canvas);
121
+ * }
122
+ * ```
123
+ */
124
+ declare function generateThumbnails(timegroup: EFTimegroup, timestamps: number[], options?: GenerateThumbnailsOptions, signal?: AbortSignal): AsyncGenerator<GeneratedThumbnail>;
125
+ /**
126
+ * Renders a timegroup preview to a canvas using SVG foreignObject.
127
+ *
128
+ * Captures the prime timeline's current visual state including DOM changes
129
+ * from frame tasks (SVG paths, canvas content, text updates, etc.).
130
+ *
131
+ * Optimized with:
132
+ * - Passive clone structure rebuilt each frame from prime's current state
133
+ * - Temporal bucketing for time-based culling
134
+ * - RenderContext for canvas pixel caching across frames
135
+ * - Resolution scaling for performance (renders at lower resolution, CSS upscales)
136
+ *
137
+ * @param timegroup - The source timegroup to preview (prime timeline)
138
+ * @param scaleOrOptions - Scale factor (default 1) or options object
139
+ * @returns Object with canvas and refresh function
140
+ */
141
+ declare function renderTimegroupToCanvas(timegroup: EFTimegroup, scaleOrOptions?: number | CanvasPreviewOptions): CanvasPreviewResult;
142
+ //#endregion
143
+ export { type CanvasPreviewOptions, type CanvasPreviewResult, type CaptureFromCloneOptions, type CaptureOptions, ContentNotReadyError, type ContentReadyMode, type GenerateThumbnailsOptions, type GeneratedThumbnail, type RenderOptions, type RenderResult, type Renderer, type ThumbnailQueue, captureFromClone, captureTimegroupAtTime, clearInlineImageCache, generateThumbnails, generateThumbnailsFromClone, getCacheMetrics, getEffectiveRenderMode, getInlineImageCacheSize, getRenderState, isCanvas, isImage, loadImageFromDataUri, renderTimegroupToCanvas, resetCacheMetrics, resetRenderState, waitForVideoContent };
144
+ //# sourceMappingURL=renderTimegroupToCanvas.d.ts.map
@@ -5,11 +5,11 @@ import { RenderContext } from "./RenderContext.js";
5
5
  import { DEFAULT_BLOCKING_TIMEOUT_MS, DEFAULT_CAPTURE_SCALE, DEFAULT_HEIGHT, DEFAULT_WIDTH, isVisibleAtTime } from "./previewTypes.js";
6
6
  import { captureTimelineToDataUri } from "./rendering/serializeTimelineDirect.js";
7
7
  import { getRenderMode, isNativeCanvasApiAvailable } from "./previewSettings.js";
8
- import { getEffectiveRenderMode } from "./renderers.js";
8
+ import { getEffectiveRenderMode, isCanvas, isImage } from "./renderers.js";
9
9
  import { defaultProfiler } from "./RenderProfiler.js";
10
10
  import { loadImageFromDataUri } from "./rendering/loadImage.js";
11
11
  import { createDprCanvas, renderToImageNative } from "./rendering/renderToImageNative.js";
12
- import { clearInlineImageCache } from "./rendering/inlineImages.js";
12
+ import { clearInlineImageCache, getInlineImageCacheSize } from "./rendering/inlineImages.js";
13
13
 
14
14
  //#region src/preview/renderTimegroupToCanvas.ts
15
15
  /** Number of rows to sample when checking canvas content */
@@ -42,6 +42,20 @@ const renderState = {
42
42
  }
43
43
  };
44
44
  /**
45
+ * Get the current render state for testing and debugging.
46
+ * @returns The module-level render state object
47
+ */
48
+ function getRenderState() {
49
+ return renderState;
50
+ }
51
+ /**
52
+ * Get cache metrics for monitoring performance.
53
+ * @returns Object with cache hit/miss/eviction counts
54
+ */
55
+ function getCacheMetrics() {
56
+ return { ...renderState.metrics };
57
+ }
58
+ /**
45
59
  * Reset cache metrics to zero.
46
60
  */
47
61
  function resetCacheMetrics() {
@@ -317,6 +331,45 @@ async function* generateThumbnailsFromClone(renderClone, renderContainer, queue,
317
331
  };
318
332
  }
319
333
  }
334
+ /**
335
+ * Generate thumbnails for multiple timestamps efficiently using a single render clone.
336
+ * This avoids the overhead of creating/destroying a clone for each thumbnail.
337
+ *
338
+ * @param timegroup - The timegroup to capture
339
+ * @param timestamps - Array of timestamps to capture (in milliseconds)
340
+ * @param options - Capture options (scale, contentReadyMode, etc.)
341
+ * @param signal - Optional AbortSignal to cancel generation
342
+ * @yields Objects with { timeMs, canvas } for each captured thumbnail
343
+ *
344
+ * @example
345
+ * ```ts
346
+ * for await (const { timeMs, canvas } of generateThumbnails(tg, [0, 100, 200])) {
347
+ * console.log(`Got thumbnail for ${timeMs}ms`);
348
+ * thumbnailCache.set(timeMs, canvas);
349
+ * }
350
+ * ```
351
+ */
352
+ async function* generateThumbnails(timegroup, timestamps, options = {}, signal) {
353
+ const { scale = DEFAULT_CAPTURE_SCALE, contentReadyMode = "immediate", blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS } = options;
354
+ const { clone: renderClone, container: renderContainer, cleanup: cleanupRenderClone } = await timegroup.createRenderClone();
355
+ try {
356
+ for (const timeMs of timestamps) {
357
+ signal?.throwIfAborted();
358
+ await renderClone.seekForRender(timeMs);
359
+ yield {
360
+ timeMs,
361
+ canvas: await captureFromClone(renderClone, renderContainer, {
362
+ scale,
363
+ contentReadyMode,
364
+ blockingTimeoutMs,
365
+ originalTimegroup: timegroup
366
+ })
367
+ };
368
+ }
369
+ } finally {
370
+ cleanupRenderClone();
371
+ }
372
+ }
320
373
  /** Epsilon for comparing time values (ms) - times within this are considered equal */
321
374
  const TIME_EPSILON_MS = 1;
322
375
  /** Default scale for preview rendering */
@@ -561,5 +614,5 @@ function renderTimegroupToCanvas(timegroup, scaleOrOptions = DEFAULT_PREVIEW_SCA
561
614
  }
562
615
 
563
616
  //#endregion
564
- export { generateThumbnailsFromClone, renderTimegroupToCanvas, resetRenderState, waitForVideoContent };
617
+ export { ContentNotReadyError, captureFromClone, captureTimegroupAtTime, clearInlineImageCache, generateThumbnails, generateThumbnailsFromClone, getCacheMetrics, getEffectiveRenderMode, getInlineImageCacheSize, getRenderState, isCanvas, isImage, loadImageFromDataUri, renderTimegroupToCanvas, resetCacheMetrics, resetRenderState, waitForVideoContent };
565
618
  //# sourceMappingURL=renderTimegroupToCanvas.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"renderTimegroupToCanvas.js","names":["timeMs: number","timeoutMs: number","blankVideos: string[]","renderState: RenderState","options: CanvasPreviewOptions","pendingResolutionScale: number | null","captureCanvas: HTMLCanvasElement | null","captureCtx: HtmlInCanvasContext | null","originalParent: ParentNode | null","originalNextSibling: ChildNode | null"],"sources":["../../src/preview/renderTimegroupToCanvas.ts"],"sourcesContent":["import type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport type {\n CaptureOptions,\n CaptureFromCloneOptions,\n GeneratedThumbnail,\n GenerateThumbnailsOptions,\n ThumbnailQueue,\n CanvasPreviewResult,\n CanvasPreviewOptions,\n} from \"./renderTimegroupToCanvas.types.js\";\nimport { RenderContext } from \"./RenderContext.js\";\nimport { FrameController } from \"./FrameController.js\";\nimport { captureTimelineToDataUri } from \"./rendering/serializeTimelineDirect.js\";\nimport {\n updateAnimations,\n type AnimatableElement,\n} from \"../elements/updateAnimations.js\";\n\n// Re-export renderer types for external use\nexport type { RenderOptions, RenderResult, Renderer } from \"./renderers.js\";\nexport { getEffectiveRenderMode, isCanvas, isImage } from \"./renderers.js\";\nimport {\n type TemporalElement,\n isVisibleAtTime,\n DEFAULT_WIDTH,\n DEFAULT_HEIGHT,\n DEFAULT_CAPTURE_SCALE,\n DEFAULT_BLOCKING_TIMEOUT_MS,\n} from \"./previewTypes.js\";\nimport { defaultProfiler } from \"./RenderProfiler.js\";\nimport { logger } from \"./logger.js\";\n\n// Import rendering modules\nimport { loadImageFromDataUri } from \"./rendering/loadImage.js\";\nimport {\n createDprCanvas,\n renderToImageNative,\n} from \"./rendering/renderToImageNative.js\";\nimport {\n clearInlineImageCache,\n getInlineImageCacheSize,\n} from \"./rendering/inlineImages.js\";\nimport {\n isNativeCanvasApiAvailable,\n getRenderMode,\n} from \"./previewSettings.js\";\nimport type {\n HtmlInCanvasContext,\n HtmlInCanvasElement,\n} from \"./rendering/types.js\";\n\n// Re-export rendering types and functions for external use\nexport { loadImageFromDataUri };\n\n// ============================================================================\n// Constants (module-specific, not shared)\n// ============================================================================\n\n/** Number of rows to sample when checking canvas content */\nconst CANVAS_SAMPLE_STRIP_HEIGHT = 4;\n\n// ============================================================================\n// Types\n// ============================================================================\n\n// Re-export types from type-only module (zero side effects)\nexport type {\n ContentReadyMode,\n CaptureOptions,\n CaptureFromCloneOptions,\n GeneratedThumbnail,\n GenerateThumbnailsOptions,\n ThumbnailQueue,\n CanvasPreviewResult,\n CanvasPreviewOptions,\n} from \"./renderTimegroupToCanvas.types.js\";\n\n/**\n * Error thrown when video content is not ready within the blocking timeout.\n */\nexport class ContentNotReadyError extends Error {\n constructor(\n public readonly timeMs: number,\n public readonly timeoutMs: number,\n public readonly blankVideos: string[],\n ) {\n super(\n `Video content not ready at ${timeMs}ms after ${timeoutMs}ms timeout. Blank videos: ${blankVideos.join(\", \")}`,\n );\n this.name = \"ContentNotReadyError\";\n }\n}\n\n// ============================================================================\n// Module State (reset via resetRenderState)\n// ============================================================================\n\n/**\n * Module-level render state including caches and reusable objects.\n */\ninterface RenderState {\n inlineImageCache: Map<string, string>;\n layoutInitializedCanvases: WeakSet<HTMLCanvasElement>;\n xmlSerializer: XMLSerializer | null;\n textEncoder: TextEncoder;\n metrics: {\n inlineImageCacheHits: number;\n inlineImageCacheMisses: number;\n inlineImageCacheEvictions: number;\n };\n}\n\n/**\n * Module-level state for render operations.\n * Note: xmlSerializer is lazy-initialized for Node.js compatibility\n */\nconst renderState: RenderState = {\n inlineImageCache: new Map(),\n layoutInitializedCanvases: new WeakSet(),\n xmlSerializer: null, // Lazy-initialized in browser context\n textEncoder: new TextEncoder(),\n metrics: {\n inlineImageCacheHits: 0,\n inlineImageCacheMisses: 0,\n inlineImageCacheEvictions: 0,\n },\n};\n\n/**\n * Get the current render state for testing and debugging.\n * @returns The module-level render state object\n */\nexport function getRenderState(): RenderState {\n return renderState;\n}\n\n/**\n * Get cache metrics for monitoring performance.\n * @returns Object with cache hit/miss/eviction counts\n */\nexport function getCacheMetrics(): RenderState[\"metrics\"] {\n return { ...renderState.metrics };\n}\n\n/**\n * Reset cache metrics to zero.\n */\nexport function resetCacheMetrics(): void {\n renderState.metrics.inlineImageCacheHits = 0;\n renderState.metrics.inlineImageCacheMisses = 0;\n renderState.metrics.inlineImageCacheEvictions = 0;\n}\n\n/**\n * Reset all module state including profiling counters, caches, and logging flags.\n * Call at the start of export sessions to ensure clean state.\n */\nexport function resetRenderState(): void {\n defaultProfiler.reset();\n clearInlineImageCache();\n resetCacheMetrics();\n}\n\n// Re-export cache management functions\nexport { clearInlineImageCache, getInlineImageCacheSize };\n\n/**\n * DEBUG: Capture a single thumbnail at the current time.\n * Call from console: window.debugCaptureThumbnail()\n */\nif (typeof window !== \"undefined\") {\n (window as any).debugCaptureThumbnail = async function () {\n const timegroup = document.querySelector(\"ef-timegroup\") as any;\n if (!timegroup) {\n console.error(\"No timegroup found\");\n return;\n }\n\n const currentTime = timegroup.currentTimeMs ?? 0;\n\n try {\n const result = await captureTimegroupAtTime(timegroup, {\n timeMs: currentTime,\n scale: 0.25,\n contentReadyMode: \"blocking\",\n blockingTimeoutMs: 1000,\n });\n\n // Create a temporary img element to display the result\n const img = document.createElement(\"img\");\n if (result instanceof HTMLCanvasElement) {\n img.src = result.toDataURL();\n } else if (result instanceof HTMLImageElement) {\n img.src = result.src;\n }\n img.style.cssText =\n \"position:fixed;top:10px;right:10px;border:2px solid red;z-index:99999;\";\n document.body.appendChild(img);\n\n return result;\n } catch (err) {\n console.error(\"[DEBUG] Capture failed:\", err);\n throw err;\n }\n };\n}\n\n// ============================================================================\n// Internal Helpers\n// ============================================================================\n\n/**\n * Wait for next animation frame (allows browser to complete layout)\n */\nfunction waitForFrame(): Promise<void> {\n return new Promise((resolve) => requestAnimationFrame(() => resolve()));\n}\n\n/**\n * Check if a canvas has any rendered content (not all transparent/uninitialized).\n * Returns true if there's ANY non-transparent pixel.\n */\nfunction canvasHasContent(canvas: HTMLCanvasElement): boolean {\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true });\n if (!ctx) return false;\n\n try {\n const width = canvas.width;\n const height = canvas.height;\n if (width === 0 || height === 0) return false;\n\n // Sample a horizontal strip across the middle of the canvas\n // This catches most video content even if edges are black\n const stripY = Math.floor(height / 2);\n const imageData = ctx.getImageData(\n 0,\n stripY,\n width,\n CANVAS_SAMPLE_STRIP_HEIGHT,\n );\n const data = imageData.data;\n\n // Check if ANY pixel has non-zero alpha (is not transparent)\n // A truly blank/uninitialized canvas has all pixels at [0,0,0,0]\n // A black video frame would have pixels at [0,0,0,255] (opaque black)\n for (let i = 3; i < data.length; i += 4) {\n if (data[i] !== 0) {\n return true;\n }\n }\n\n return false;\n } catch {\n // Canvas might be tainted, assume it has content\n return true;\n }\n}\n\ninterface WaitForVideoContentResult {\n ready: boolean;\n blankVideos: string[];\n}\n\n/**\n * Wait for video canvases within a timegroup to have content.\n * Only checks videos that should be visible at the current time.\n * Returns result with ready status and list of blank video names.\n */\nexport async function waitForVideoContent(\n timegroup: EFTimegroup,\n timeMs: number,\n maxWaitMs: number,\n): Promise<WaitForVideoContentResult> {\n const startTime = performance.now();\n\n // Find all video elements in the timegroup (including nested)\n const allVideos = timegroup.querySelectorAll(\"ef-video\");\n if (allVideos.length === 0) return { ready: true, blankVideos: [] };\n\n // Filter to only videos that should be visible at this time\n const visibleVideos = Array.from(allVideos).filter((video) => {\n // Check if video itself is in time range\n if (!isVisibleAtTime(video, timeMs)) return false;\n\n // Check if all ancestor timegroups are in time range\n let parent = video.parentElement;\n while (parent && parent !== timegroup) {\n if (\n parent.tagName === \"EF-TIMEGROUP\" &&\n !isVisibleAtTime(parent, timeMs)\n ) {\n return false;\n }\n parent = parent.parentElement;\n }\n return true;\n });\n\n if (visibleVideos.length === 0) return { ready: true, blankVideos: [] };\n\n const getBlankVideoNames = () =>\n visibleVideos\n .filter((video) => {\n const shadowCanvas = video.shadowRoot?.querySelector(\"canvas\");\n return shadowCanvas && !canvasHasContent(shadowCanvas);\n })\n .map((v) => (v as TemporalElement).src || v.id || \"unnamed\");\n\n while (performance.now() - startTime < maxWaitMs) {\n let allHaveContent = true;\n\n for (const video of visibleVideos) {\n const shadowCanvas = video.shadowRoot?.querySelector(\"canvas\");\n if (shadowCanvas && shadowCanvas.width > 0 && shadowCanvas.height > 0) {\n if (!canvasHasContent(shadowCanvas)) {\n allHaveContent = false;\n break;\n }\n }\n }\n\n if (allHaveContent) return { ready: true, blankVideos: [] };\n\n // Wait a bit and check again\n await waitForFrame();\n }\n\n return { ready: false, blankVideos: getBlankVideoNames() };\n}\n\n/**\n * Captures a frame from an already-seeked render clone.\n * Used internally by captureBatch for efficiency (reuses one clone across all captures).\n *\n * @param renderClone - A render clone that has already been seeked to the target time\n * @param renderContainer - The container holding the render clone (from createRenderClone)\n * @param options - Capture options\n * @returns Canvas or Image with the rendered frame (both are CanvasImageSource)\n */\nexport async function captureFromClone(\n renderClone: EFTimegroup,\n _renderContainer: HTMLElement,\n options: CaptureFromCloneOptions = {},\n): Promise<CanvasImageSource> {\n const {\n scale = DEFAULT_CAPTURE_SCALE,\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n originalTimegroup,\n timeMs: explicitTimeMs,\n canvasMode,\n } = options;\n\n // Use explicit time if provided, otherwise fall back to clone's currentTimeMs\n // CRITICAL: Using explicit time ensures temporal visibility checks are accurate\n // NOTE: Must be defined BEFORE any logging that references timeMs\n const timeMs = explicitTimeMs ?? renderClone.currentTimeMs;\n\n // Use original timegroup dimensions if available, otherwise clone dimensions\n const sourceForDimensions = originalTimegroup ?? renderClone;\n const width = sourceForDimensions.offsetWidth || DEFAULT_WIDTH;\n const height = sourceForDimensions.offsetHeight || DEFAULT_HEIGHT;\n\n // NOTE: seekForRender() has already:\n // 1. Called frameController.renderFrame() to coordinate FrameRenderable elements\n // 2. Awaited #executeCustomFrameTasks() so frame tasks are complete\n // No need to call frameController.renderFrame() again - it would fire tasks redundantly\n\n if (contentReadyMode === \"blocking\") {\n const result = await waitForVideoContent(\n renderClone,\n timeMs,\n blockingTimeoutMs,\n );\n if (!result.ready) {\n throw new ContentNotReadyError(\n timeMs,\n blockingTimeoutMs,\n result.blankVideos,\n );\n }\n }\n\n // Determine effective canvas mode:\n // 1. If explicitly specified, use that\n // 2. If \"native\" is requested but not available, fall back to foreignObject\n // 3. If not specified, default to foreignObject for compatibility\n const effectiveCanvasMode = (() => {\n if (!canvasMode) return \"foreignObject\";\n if (canvasMode === \"native\" && !isNativeCanvasApiAvailable()) {\n logger.debug(\n \"[captureFromClone] Native canvas mode requested but not available, falling back to foreignObject\",\n );\n return \"foreignObject\";\n }\n return canvasMode;\n })();\n\n // Create RenderContext for caching during this capture operation (only needed for foreignObject)\n const renderContext = new RenderContext();\n\n try {\n if (effectiveCanvasMode === \"native\") {\n // NATIVE PATH: Use drawElementImage API (~1.76x faster than foreignObject)\n // No DOM serialization, no canvas-to-dataURL encoding, no image loading\n // Direct browser-native rendering\n\n const t0 = performance.now();\n const canvas = await renderToImageNative(renderClone, width, height, {\n skipDprScaling: true, // Use 1x DPR for video export (4x fewer pixels!)\n });\n const renderTime = performance.now() - t0;\n\n logger.debug(\n `[captureFromClone] native render=${renderTime.toFixed(0)}ms (canvasScale=${scale})`,\n );\n\n return canvas;\n } else {\n // FOREIGNOBJECT PATH: Serialize DOM → SVG → Image → Canvas\n // More compatible but slower than native path\n\n // NOTE: seekForRender() has already ensured rendering is complete, including:\n // - Lit updates propagated\n // - All LitElement descendants updated\n // - frameController.renderFrame() called for FrameRenderable elements\n // - Layout stabilization complete\n // No additional RAF wait needed - can serialize immediately\n\n const t0 = performance.now();\n const dataUri = await captureTimelineToDataUri(\n renderClone,\n width,\n height,\n {\n renderContext,\n canvasScale: scale,\n timeMs,\n },\n );\n const serializeTime = performance.now() - t0;\n\n const t1 = performance.now();\n const image = await loadImageFromDataUri(dataUri);\n const loadTime = performance.now() - t1;\n\n logger.debug(\n `[captureFromClone] foreignObject serialize=${serializeTime.toFixed(0)}ms, load=${loadTime.toFixed(0)}ms (canvasScale=${scale})`,\n );\n\n // Return image directly - no copy needed!\n return image;\n }\n } finally {\n // Ensure RenderContext is disposed even if an error occurs\n renderContext.dispose();\n }\n}\n\n/**\n * Captures a single frame from a timegroup at a specific time.\n *\n * CLONE-TIMELINE ARCHITECTURE:\n * Creates an independent render clone, seeks it to the target time, and captures.\n * Prime-timeline is NEVER seeked - user can continue previewing/editing during capture.\n *\n * @param timegroup - The source timegroup\n * @param options - Capture options including timeMs, scale, contentReadyMode\n * @returns Canvas with the rendered frame\n * @throws ContentNotReadyError if blocking mode times out waiting for video content\n */\nexport async function captureTimegroupAtTime(\n timegroup: EFTimegroup,\n options: CaptureOptions,\n): Promise<CanvasImageSource> {\n const {\n timeMs,\n scale = DEFAULT_CAPTURE_SCALE,\n // skipRestore is deprecated with Clone-timeline (Prime is never seeked)\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n canvasMode,\n skipClone = false,\n } = options;\n\n if (skipClone) {\n // DIRECT RENDERING: Skip clone creation for headless server rendering\n // Seek prime timeline directly and capture from it\n // WARNING: This modifies the prime timeline! Only use in headless contexts.\n\n const seekStart = performance.now();\n await timegroup.seekForRender(timeMs);\n const seekMs = performance.now() - seekStart;\n\n const renderStart = performance.now();\n // Use timegroup's actual container (parentElement or document.body as fallback)\n const container = (timegroup.parentElement || document.body) as HTMLElement;\n const result = await captureFromClone(timegroup, container, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n originalTimegroup: undefined, // No original since we're rendering the prime\n canvasMode,\n timeMs, // Pass explicit time since we're not using a clone\n });\n const renderMs = performance.now() - renderStart;\n\n // Store timing (no clone time since we skipped it)\n if (typeof result === \"object\" && result !== null) {\n (result as any).__perfTiming = { cloneMs: 0, seekMs, renderMs };\n }\n\n return result;\n }\n\n // CLONE-TIMELINE: Create a short-lived render clone for this capture\n // Prime-timeline is NEVER seeked - clone is fully independent\n const cloneStart = performance.now();\n const {\n clone: renderClone,\n container: renderContainer,\n cleanup: cleanupRenderClone,\n } = await timegroup.createRenderClone();\n const cloneMs = performance.now() - cloneStart;\n\n try {\n // Seek the clone to target time (Prime stays at user position)\n // Use seekForRender which bypasses duration clamping - render clones may have\n // zero duration initially until media durations are computed, but we still\n // want to seek to the requested time for capture purposes.\n const seekStart = performance.now();\n await renderClone.seekForRender(timeMs);\n const seekMs = performance.now() - seekStart;\n\n // Use the shared capture helper\n const renderStart = performance.now();\n const result = await captureFromClone(renderClone, renderContainer, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n originalTimegroup: timegroup,\n canvasMode,\n });\n const renderMs = performance.now() - renderStart;\n\n // Store timing on the result for access by callers (if they need it)\n // Note: CanvasImageSource doesn't support custom properties, but we can attach them anyway\n if (typeof result === \"object\" && result !== null) {\n (result as any).__perfTiming = { cloneMs, seekMs, renderMs };\n }\n\n return result;\n } finally {\n // Clean up the render clone\n cleanupRenderClone();\n }\n}\n\n/**\n * Generate thumbnails using an existing render clone and mutable queue.\n * The queue can be modified while generation is in progress.\n *\n * @param renderClone - Pre-created render clone to use\n * @param renderContainer - Container for the render clone\n * @param queue - Mutable queue that provides timestamps\n * @param options - Capture options (scale, contentReadyMode, etc.)\n * @yields Objects with { timeMs, canvas } for each captured thumbnail\n *\n * @example\n * ```ts\n * const queue = new MutableTimestampQueue();\n * queue.reset([0, 100, 200]);\n *\n * for await (const { timeMs, canvas } of generateThumbnailsFromClone(clone, container, queue)) {\n * cache.set(timeMs, canvas);\n * // Queue can be modified here while generator continues\n * }\n * ```\n */\nexport async function* generateThumbnailsFromClone(\n renderClone: EFTimegroup,\n renderContainer: HTMLElement,\n queue: ThumbnailQueue,\n options: GenerateThumbnailsOptions = {},\n): AsyncGenerator<GeneratedThumbnail> {\n const {\n scale = DEFAULT_CAPTURE_SCALE,\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n signal,\n } = options;\n\n while (true) {\n // Check if aborted before starting work\n if (signal?.aborted) {\n break;\n }\n\n const timeMs = queue.shift();\n if (timeMs === undefined) {\n // Queue is empty, generator exits\n break;\n }\n\n // Seek the clone to the target time\n await renderClone.seekForRender(timeMs);\n\n // Check if aborted after seek (before expensive capture)\n if (signal?.aborted) {\n break;\n }\n\n // Capture from the seeked clone, passing explicit timeMs\n const canvas = await captureFromClone(renderClone, renderContainer, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n timeMs, // CRITICAL: Pass explicit time for accurate temporal visibility\n });\n\n // Yield the result with explicit timestamp association\n yield { timeMs, canvas };\n }\n}\n\n/**\n * Generate thumbnails for multiple timestamps efficiently using a single render clone.\n * This avoids the overhead of creating/destroying a clone for each thumbnail.\n *\n * @param timegroup - The timegroup to capture\n * @param timestamps - Array of timestamps to capture (in milliseconds)\n * @param options - Capture options (scale, contentReadyMode, etc.)\n * @param signal - Optional AbortSignal to cancel generation\n * @yields Objects with { timeMs, canvas } for each captured thumbnail\n *\n * @example\n * ```ts\n * for await (const { timeMs, canvas } of generateThumbnails(tg, [0, 100, 200])) {\n * console.log(`Got thumbnail for ${timeMs}ms`);\n * thumbnailCache.set(timeMs, canvas);\n * }\n * ```\n */\nexport async function* generateThumbnails(\n timegroup: EFTimegroup,\n timestamps: number[],\n options: GenerateThumbnailsOptions = {},\n signal?: AbortSignal,\n): AsyncGenerator<GeneratedThumbnail> {\n const {\n scale = DEFAULT_CAPTURE_SCALE,\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n } = options;\n\n // Create a single render clone for all thumbnails\n const {\n clone: renderClone,\n container: renderContainer,\n cleanup: cleanupRenderClone,\n } = await timegroup.createRenderClone();\n\n try {\n for (const timeMs of timestamps) {\n // Check for abort before each capture\n signal?.throwIfAborted();\n\n // Seek the clone to the target time\n await renderClone.seekForRender(timeMs);\n\n // Capture from the seeked clone\n const canvas = await captureFromClone(renderClone, renderContainer, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n originalTimegroup: timegroup,\n });\n\n // Yield the result with explicit timestamp association\n yield { timeMs, canvas };\n }\n } finally {\n // Always clean up the render clone\n cleanupRenderClone();\n }\n}\n\n/** Epsilon for comparing time values (ms) - times within this are considered equal */\nconst TIME_EPSILON_MS = 1;\n\n/** Default scale for preview rendering */\nconst DEFAULT_PREVIEW_SCALE = 1;\n\n/** Default resolution scale (full resolution) */\nconst DEFAULT_RESOLUTION_SCALE = 1;\n\n/**\n * Convert relative time to absolute time for a timegroup.\n * Nested timegroup children have ABSOLUTE startTimeMs values,\n * so relative capture times must be converted for temporal culling.\n */\nfunction toAbsoluteTime(\n timegroup: EFTimegroup,\n relativeTimeMs: number,\n): number {\n return relativeTimeMs + (timegroup.startTimeMs ?? 0);\n}\n\n/**\n * Renders a timegroup preview to a canvas using SVG foreignObject.\n *\n * Captures the prime timeline's current visual state including DOM changes\n * from frame tasks (SVG paths, canvas content, text updates, etc.).\n *\n * Optimized with:\n * - Passive clone structure rebuilt each frame from prime's current state\n * - Temporal bucketing for time-based culling\n * - RenderContext for canvas pixel caching across frames\n * - Resolution scaling for performance (renders at lower resolution, CSS upscales)\n *\n * @param timegroup - The source timegroup to preview (prime timeline)\n * @param scaleOrOptions - Scale factor (default 1) or options object\n * @returns Object with canvas and refresh function\n */\nexport function renderTimegroupToCanvas(\n timegroup: EFTimegroup,\n scaleOrOptions: number | CanvasPreviewOptions = DEFAULT_PREVIEW_SCALE,\n): CanvasPreviewResult {\n // Normalize options\n const options: CanvasPreviewOptions =\n typeof scaleOrOptions === \"number\"\n ? { scale: scaleOrOptions }\n : scaleOrOptions;\n\n const scale = options.scale ?? DEFAULT_PREVIEW_SCALE;\n // These are mutable to support dynamic resolution changes\n let currentResolutionScale =\n options.resolutionScale ?? DEFAULT_RESOLUTION_SCALE;\n\n const width = timegroup.offsetWidth || DEFAULT_WIDTH;\n const height = timegroup.offsetHeight || DEFAULT_HEIGHT;\n const dpr =\n (typeof window !== \"undefined\" ? window.devicePixelRatio : 1) || 1;\n\n // Calculate effective render dimensions (internal resolution) - mutable\n let renderWidth = Math.floor(width * currentResolutionScale);\n let renderHeight = Math.floor(height * currentResolutionScale);\n\n // Create canvas with proper DPR handling\n const canvas = createDprCanvas({\n renderWidth,\n renderHeight,\n scale,\n fullWidth: width,\n fullHeight: height,\n dpr,\n });\n\n // Return canvas directly - no wrapper needed\n const wrapperContainer = canvas;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n throw new Error(\"Failed to get canvas 2d context\");\n }\n\n // Track render state\n let rendering = false;\n let lastTimeMs = -1;\n let disposed = false;\n\n // Create RenderContext for caching across refresh calls (foreignObject only)\n const renderContext = new RenderContext();\n\n // Create FrameController for coordinating element rendering\n // Cached for the lifetime of this preview instance\n const frameController = new FrameController(timegroup);\n\n // Log resolution scale on first render for debugging\n let hasLoggedScale = false;\n\n // Pending resolution change - applied at start of next refresh to avoid blanking\n let pendingResolutionScale: number | null = null;\n\n // Use the user's render mode preference. Native requires the timegroup to be\n // inside a <canvas layoutsubtree> for drawElementImage to work.\n const useNative =\n getRenderMode() === \"native\" && isNativeCanvasApiAvailable();\n let captureCanvas: HTMLCanvasElement | null = null;\n let captureCtx: HtmlInCanvasContext | null = null;\n let originalParent: ParentNode | null = null;\n let originalNextSibling: ChildNode | null = null;\n let savedClipPath = \"\";\n let savedPointerEvents = \"\";\n\n if (useNative) {\n captureCanvas = document.createElement(\"canvas\");\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n captureCanvas.width = renderWidth;\n captureCanvas.height = renderHeight;\n captureCanvas.style.cssText = `position:fixed;left:0;top:0;width:${width}px;height:${height}px;opacity:0;pointer-events:none;z-index:-9999;`;\n originalParent = timegroup.parentNode;\n originalNextSibling = timegroup.nextSibling;\n savedClipPath = timegroup.style.clipPath;\n savedPointerEvents = timegroup.style.pointerEvents;\n timegroup.style.clipPath = \"\";\n timegroup.style.pointerEvents = \"\";\n captureCanvas.appendChild(timegroup);\n document.body.appendChild(captureCanvas);\n captureCtx = captureCanvas.getContext(\"2d\") as HtmlInCanvasContext;\n void captureCanvas.offsetHeight;\n void timegroup.offsetHeight;\n }\n\n /**\n * Apply pending resolution scale changes.\n * Called at the start of refresh() before rendering, so the old content\n * stays visible until new content is ready to be drawn.\n */\n const applyPendingResolutionChange = (): void => {\n if (pendingResolutionScale === null) return;\n\n const newScale = pendingResolutionScale;\n pendingResolutionScale = null;\n\n currentResolutionScale = newScale;\n renderWidth = Math.floor(width * currentResolutionScale);\n renderHeight = Math.floor(height * currentResolutionScale);\n\n if (captureCanvas) {\n captureCanvas.width = renderWidth;\n captureCanvas.height = renderHeight;\n }\n };\n\n /**\n * Dynamically change resolution scale without rebuilding clone structure.\n * The actual change is deferred until next refresh() to avoid blanking -\n * old content stays visible until new content is ready.\n */\n const setResolutionScale = (newScale: number): void => {\n // Clamp to valid range\n newScale = Math.max(0.1, Math.min(1, newScale));\n\n if (newScale === currentResolutionScale && pendingResolutionScale === null)\n return;\n\n // Queue the change - will be applied at start of next refresh\n pendingResolutionScale = newScale;\n\n // Force re-render on next refresh by invalidating lastTimeMs\n lastTimeMs = -1;\n };\n\n const getResolutionScale = (): number =>\n pendingResolutionScale ?? currentResolutionScale;\n\n // Rolling timing stats for per-phase profiling\n let frameCount = 0;\n let totalFrameControllerMs = 0;\n let totalCaptureMs = 0;\n let totalCopyMs = 0;\n let totalFrameMs = 0;\n\n const refresh = async (): Promise<void> => {\n if (disposed) return;\n\n const sourceTimeMs = timegroup.currentTimeMs ?? 0;\n const userTimeMs = timegroup.userTimeMs ?? 0;\n\n if (Math.abs(sourceTimeMs - userTimeMs) > TIME_EPSILON_MS) return;\n if (userTimeMs === lastTimeMs) return;\n if (rendering) return;\n\n lastTimeMs = userTimeMs;\n rendering = true;\n\n applyPendingResolutionChange();\n\n if (!hasLoggedScale) {\n hasLoggedScale = true;\n const mode = useNative ? \"native\" : \"foreignObject\";\n logger.debug(\n `[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}`,\n );\n }\n\n try {\n const tFrame = performance.now();\n\n const tFC0 = performance.now();\n await frameController.renderFrame(userTimeMs, {\n waitForLitUpdate: false,\n onAnimationsUpdate: (root) => {\n updateAnimations(root as AnimatableElement);\n },\n });\n const fcMs = performance.now() - tFC0;\n\n const tCapture0 = performance.now();\n\n if (useNative && captureCanvas && captureCtx) {\n if (captureCanvas.width !== width || captureCanvas.height !== height) {\n captureCtx.save();\n captureCtx.scale(\n captureCanvas.width / width,\n captureCanvas.height / height,\n );\n captureCtx.drawElementImage(timegroup, 0, 0);\n captureCtx.restore();\n } else {\n captureCtx.drawElementImage(timegroup, 0, 0);\n }\n const captureMs = performance.now() - tCapture0;\n\n const tCopy0 = performance.now();\n const targetWidth = Math.floor(renderWidth * scale * dpr);\n const targetHeight = Math.floor(renderHeight * scale * dpr);\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n } else {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n }\n ctx.drawImage(captureCanvas, 0, 0, canvas.width, canvas.height);\n const copyMs = performance.now() - tCopy0;\n\n const frameMs = performance.now() - tFrame;\n frameCount++;\n totalFrameControllerMs += fcMs;\n totalCaptureMs += captureMs;\n totalCopyMs += copyMs;\n totalFrameMs += frameMs;\n\n defaultProfiler.incrementRenderCount();\n if (defaultProfiler.shouldLogByFrameCount(60)) {\n frameCount = 0;\n totalFrameControllerMs = 0;\n totalCaptureMs = 0;\n totalCopyMs = 0;\n totalFrameMs = 0;\n }\n } else {\n const absoluteTimeMs = toAbsoluteTime(timegroup, userTimeMs);\n\n const dataUri = await captureTimelineToDataUri(\n timegroup,\n width,\n height,\n {\n renderContext,\n canvasScale: currentResolutionScale,\n timeMs: absoluteTimeMs,\n },\n );\n const captureMs = performance.now() - tCapture0;\n\n const tCopy0 = performance.now();\n const image = await loadImageFromDataUri(dataUri);\n const copyMs = performance.now() - tCopy0;\n\n const targetWidth = Math.floor(renderWidth * scale * dpr);\n const targetHeight = Math.floor(renderHeight * scale * dpr);\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n } else {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n }\n\n ctx.save();\n ctx.scale(dpr * scale, dpr * scale);\n ctx.drawImage(image, 0, 0, renderWidth, renderHeight);\n ctx.restore();\n\n const frameMs = performance.now() - tFrame;\n frameCount++;\n totalFrameControllerMs += fcMs;\n totalCaptureMs += captureMs;\n totalCopyMs += copyMs;\n totalFrameMs += frameMs;\n\n defaultProfiler.incrementRenderCount();\n if (defaultProfiler.shouldLogByFrameCount(60)) {\n frameCount = 0;\n totalFrameControllerMs = 0;\n totalCaptureMs = 0;\n totalCopyMs = 0;\n totalFrameMs = 0;\n }\n }\n } catch (e) {\n logger.error(\"Canvas preview render failed:\", e);\n } finally {\n rendering = false;\n }\n };\n\n /**\n * Dispose the preview and release resources.\n */\n const dispose = (): void => {\n if (disposed) return;\n disposed = true;\n frameController.abort();\n renderContext.dispose();\n\n // Restore timegroup to original DOM position if native mode moved it\n if (useNative && originalParent) {\n timegroup.style.clipPath = savedClipPath;\n timegroup.style.pointerEvents = savedPointerEvents;\n if (originalNextSibling) {\n originalParent.insertBefore(timegroup, originalNextSibling);\n } else {\n originalParent.appendChild(timegroup);\n }\n captureCanvas?.remove();\n }\n };\n\n // Do initial render\n refresh();\n\n return {\n container: wrapperContainer,\n canvas,\n refresh,\n setResolutionScale,\n getResolutionScale,\n dispose,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"renderTimegroupToCanvas.js","names":["timeMs: number","timeoutMs: number","blankVideos: string[]","renderState: RenderState","options: CanvasPreviewOptions","pendingResolutionScale: number | null","captureCanvas: HTMLCanvasElement | null","captureCtx: HtmlInCanvasContext | null","originalParent: ParentNode | null","originalNextSibling: ChildNode | null"],"sources":["../../src/preview/renderTimegroupToCanvas.ts"],"sourcesContent":["import type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport type {\n CaptureOptions,\n CaptureFromCloneOptions,\n GeneratedThumbnail,\n GenerateThumbnailsOptions,\n ThumbnailQueue,\n CanvasPreviewResult,\n CanvasPreviewOptions,\n} from \"./renderTimegroupToCanvas.types.js\";\nimport { RenderContext } from \"./RenderContext.js\";\nimport { FrameController } from \"./FrameController.js\";\nimport { captureTimelineToDataUri } from \"./rendering/serializeTimelineDirect.js\";\nimport {\n updateAnimations,\n type AnimatableElement,\n} from \"../elements/updateAnimations.js\";\n\n// Re-export renderer types for external use\nexport type { RenderOptions, RenderResult, Renderer } from \"./renderers.js\";\nexport { getEffectiveRenderMode, isCanvas, isImage } from \"./renderers.js\";\nimport {\n type TemporalElement,\n isVisibleAtTime,\n DEFAULT_WIDTH,\n DEFAULT_HEIGHT,\n DEFAULT_CAPTURE_SCALE,\n DEFAULT_BLOCKING_TIMEOUT_MS,\n} from \"./previewTypes.js\";\nimport { defaultProfiler } from \"./RenderProfiler.js\";\nimport { logger } from \"./logger.js\";\n\n// Import rendering modules\nimport { loadImageFromDataUri } from \"./rendering/loadImage.js\";\nimport {\n createDprCanvas,\n renderToImageNative,\n} from \"./rendering/renderToImageNative.js\";\nimport {\n clearInlineImageCache,\n getInlineImageCacheSize,\n} from \"./rendering/inlineImages.js\";\nimport {\n isNativeCanvasApiAvailable,\n getRenderMode,\n} from \"./previewSettings.js\";\nimport type {\n HtmlInCanvasContext,\n HtmlInCanvasElement,\n} from \"./rendering/types.js\";\n\n// Re-export rendering types and functions for external use\nexport { loadImageFromDataUri };\n\n// ============================================================================\n// Constants (module-specific, not shared)\n// ============================================================================\n\n/** Number of rows to sample when checking canvas content */\nconst CANVAS_SAMPLE_STRIP_HEIGHT = 4;\n\n// ============================================================================\n// Types\n// ============================================================================\n\n// Re-export types from type-only module (zero side effects)\nexport type {\n ContentReadyMode,\n CaptureOptions,\n CaptureFromCloneOptions,\n GeneratedThumbnail,\n GenerateThumbnailsOptions,\n ThumbnailQueue,\n CanvasPreviewResult,\n CanvasPreviewOptions,\n} from \"./renderTimegroupToCanvas.types.js\";\n\n/**\n * Error thrown when video content is not ready within the blocking timeout.\n */\nexport class ContentNotReadyError extends Error {\n constructor(\n public readonly timeMs: number,\n public readonly timeoutMs: number,\n public readonly blankVideos: string[],\n ) {\n super(\n `Video content not ready at ${timeMs}ms after ${timeoutMs}ms timeout. Blank videos: ${blankVideos.join(\", \")}`,\n );\n this.name = \"ContentNotReadyError\";\n }\n}\n\n// ============================================================================\n// Module State (reset via resetRenderState)\n// ============================================================================\n\n/**\n * Module-level render state including caches and reusable objects.\n */\ninterface RenderState {\n inlineImageCache: Map<string, string>;\n layoutInitializedCanvases: WeakSet<HTMLCanvasElement>;\n xmlSerializer: XMLSerializer | null;\n textEncoder: TextEncoder;\n metrics: {\n inlineImageCacheHits: number;\n inlineImageCacheMisses: number;\n inlineImageCacheEvictions: number;\n };\n}\n\n/**\n * Module-level state for render operations.\n * Note: xmlSerializer is lazy-initialized for Node.js compatibility\n */\nconst renderState: RenderState = {\n inlineImageCache: new Map(),\n layoutInitializedCanvases: new WeakSet(),\n xmlSerializer: null, // Lazy-initialized in browser context\n textEncoder: new TextEncoder(),\n metrics: {\n inlineImageCacheHits: 0,\n inlineImageCacheMisses: 0,\n inlineImageCacheEvictions: 0,\n },\n};\n\n/**\n * Get the current render state for testing and debugging.\n * @returns The module-level render state object\n */\nexport function getRenderState(): RenderState {\n return renderState;\n}\n\n/**\n * Get cache metrics for monitoring performance.\n * @returns Object with cache hit/miss/eviction counts\n */\nexport function getCacheMetrics(): RenderState[\"metrics\"] {\n return { ...renderState.metrics };\n}\n\n/**\n * Reset cache metrics to zero.\n */\nexport function resetCacheMetrics(): void {\n renderState.metrics.inlineImageCacheHits = 0;\n renderState.metrics.inlineImageCacheMisses = 0;\n renderState.metrics.inlineImageCacheEvictions = 0;\n}\n\n/**\n * Reset all module state including profiling counters, caches, and logging flags.\n * Call at the start of export sessions to ensure clean state.\n */\nexport function resetRenderState(): void {\n defaultProfiler.reset();\n clearInlineImageCache();\n resetCacheMetrics();\n}\n\n// Re-export cache management functions\nexport { clearInlineImageCache, getInlineImageCacheSize };\n\n/**\n * DEBUG: Capture a single thumbnail at the current time.\n * Call from console: window.debugCaptureThumbnail()\n */\nif (typeof window !== \"undefined\") {\n (window as any).debugCaptureThumbnail = async function () {\n const timegroup = document.querySelector(\"ef-timegroup\") as any;\n if (!timegroup) {\n console.error(\"No timegroup found\");\n return;\n }\n\n const currentTime = timegroup.currentTimeMs ?? 0;\n\n try {\n const result = await captureTimegroupAtTime(timegroup, {\n timeMs: currentTime,\n scale: 0.25,\n contentReadyMode: \"blocking\",\n blockingTimeoutMs: 1000,\n });\n\n // Create a temporary img element to display the result\n const img = document.createElement(\"img\");\n if (result instanceof HTMLCanvasElement) {\n img.src = result.toDataURL();\n } else if (result instanceof HTMLImageElement) {\n img.src = result.src;\n }\n img.style.cssText =\n \"position:fixed;top:10px;right:10px;border:2px solid red;z-index:99999;\";\n document.body.appendChild(img);\n\n return result;\n } catch (err) {\n console.error(\"[DEBUG] Capture failed:\", err);\n throw err;\n }\n };\n}\n\n// ============================================================================\n// Internal Helpers\n// ============================================================================\n\n/**\n * Wait for next animation frame (allows browser to complete layout)\n */\nfunction waitForFrame(): Promise<void> {\n return new Promise((resolve) => requestAnimationFrame(() => resolve()));\n}\n\n/**\n * Check if a canvas has any rendered content (not all transparent/uninitialized).\n * Returns true if there's ANY non-transparent pixel.\n */\nfunction canvasHasContent(canvas: HTMLCanvasElement): boolean {\n const ctx = canvas.getContext(\"2d\", { willReadFrequently: true });\n if (!ctx) return false;\n\n try {\n const width = canvas.width;\n const height = canvas.height;\n if (width === 0 || height === 0) return false;\n\n // Sample a horizontal strip across the middle of the canvas\n // This catches most video content even if edges are black\n const stripY = Math.floor(height / 2);\n const imageData = ctx.getImageData(\n 0,\n stripY,\n width,\n CANVAS_SAMPLE_STRIP_HEIGHT,\n );\n const data = imageData.data;\n\n // Check if ANY pixel has non-zero alpha (is not transparent)\n // A truly blank/uninitialized canvas has all pixels at [0,0,0,0]\n // A black video frame would have pixels at [0,0,0,255] (opaque black)\n for (let i = 3; i < data.length; i += 4) {\n if (data[i] !== 0) {\n return true;\n }\n }\n\n return false;\n } catch {\n // Canvas might be tainted, assume it has content\n return true;\n }\n}\n\ninterface WaitForVideoContentResult {\n ready: boolean;\n blankVideos: string[];\n}\n\n/**\n * Wait for video canvases within a timegroup to have content.\n * Only checks videos that should be visible at the current time.\n * Returns result with ready status and list of blank video names.\n */\nexport async function waitForVideoContent(\n timegroup: EFTimegroup,\n timeMs: number,\n maxWaitMs: number,\n): Promise<WaitForVideoContentResult> {\n const startTime = performance.now();\n\n // Find all video elements in the timegroup (including nested)\n const allVideos = timegroup.querySelectorAll(\"ef-video\");\n if (allVideos.length === 0) return { ready: true, blankVideos: [] };\n\n // Filter to only videos that should be visible at this time\n const visibleVideos = Array.from(allVideos).filter((video) => {\n // Check if video itself is in time range\n if (!isVisibleAtTime(video, timeMs)) return false;\n\n // Check if all ancestor timegroups are in time range\n let parent = video.parentElement;\n while (parent && parent !== timegroup) {\n if (\n parent.tagName === \"EF-TIMEGROUP\" &&\n !isVisibleAtTime(parent, timeMs)\n ) {\n return false;\n }\n parent = parent.parentElement;\n }\n return true;\n });\n\n if (visibleVideos.length === 0) return { ready: true, blankVideos: [] };\n\n const getBlankVideoNames = () =>\n visibleVideos\n .filter((video) => {\n const shadowCanvas = video.shadowRoot?.querySelector(\"canvas\");\n return shadowCanvas && !canvasHasContent(shadowCanvas);\n })\n .map((v) => (v as TemporalElement).src || v.id || \"unnamed\");\n\n while (performance.now() - startTime < maxWaitMs) {\n let allHaveContent = true;\n\n for (const video of visibleVideos) {\n const shadowCanvas = video.shadowRoot?.querySelector(\"canvas\");\n if (shadowCanvas && shadowCanvas.width > 0 && shadowCanvas.height > 0) {\n if (!canvasHasContent(shadowCanvas)) {\n allHaveContent = false;\n break;\n }\n }\n }\n\n if (allHaveContent) return { ready: true, blankVideos: [] };\n\n // Wait a bit and check again\n await waitForFrame();\n }\n\n return { ready: false, blankVideos: getBlankVideoNames() };\n}\n\n/**\n * Captures a frame from an already-seeked render clone.\n * Used internally by captureBatch for efficiency (reuses one clone across all captures).\n *\n * @param renderClone - A render clone that has already been seeked to the target time\n * @param renderContainer - The container holding the render clone (from createRenderClone)\n * @param options - Capture options\n * @returns Canvas or Image with the rendered frame (both are CanvasImageSource)\n */\nexport async function captureFromClone(\n renderClone: EFTimegroup,\n _renderContainer: HTMLElement,\n options: CaptureFromCloneOptions = {},\n): Promise<CanvasImageSource> {\n const {\n scale = DEFAULT_CAPTURE_SCALE,\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n originalTimegroup,\n timeMs: explicitTimeMs,\n canvasMode,\n } = options;\n\n // Use explicit time if provided, otherwise fall back to clone's currentTimeMs\n // CRITICAL: Using explicit time ensures temporal visibility checks are accurate\n // NOTE: Must be defined BEFORE any logging that references timeMs\n const timeMs = explicitTimeMs ?? renderClone.currentTimeMs;\n\n // Use original timegroup dimensions if available, otherwise clone dimensions\n const sourceForDimensions = originalTimegroup ?? renderClone;\n const width = sourceForDimensions.offsetWidth || DEFAULT_WIDTH;\n const height = sourceForDimensions.offsetHeight || DEFAULT_HEIGHT;\n\n // NOTE: seekForRender() has already:\n // 1. Called frameController.renderFrame() to coordinate FrameRenderable elements\n // 2. Awaited #executeCustomFrameTasks() so frame tasks are complete\n // No need to call frameController.renderFrame() again - it would fire tasks redundantly\n\n if (contentReadyMode === \"blocking\") {\n const result = await waitForVideoContent(\n renderClone,\n timeMs,\n blockingTimeoutMs,\n );\n if (!result.ready) {\n throw new ContentNotReadyError(\n timeMs,\n blockingTimeoutMs,\n result.blankVideos,\n );\n }\n }\n\n // Determine effective canvas mode:\n // 1. If explicitly specified, use that\n // 2. If \"native\" is requested but not available, fall back to foreignObject\n // 3. If not specified, default to foreignObject for compatibility\n const effectiveCanvasMode = (() => {\n if (!canvasMode) return \"foreignObject\";\n if (canvasMode === \"native\" && !isNativeCanvasApiAvailable()) {\n logger.debug(\n \"[captureFromClone] Native canvas mode requested but not available, falling back to foreignObject\",\n );\n return \"foreignObject\";\n }\n return canvasMode;\n })();\n\n // Create RenderContext for caching during this capture operation (only needed for foreignObject)\n const renderContext = new RenderContext();\n\n try {\n if (effectiveCanvasMode === \"native\") {\n // NATIVE PATH: Use drawElementImage API (~1.76x faster than foreignObject)\n // No DOM serialization, no canvas-to-dataURL encoding, no image loading\n // Direct browser-native rendering\n\n const t0 = performance.now();\n const canvas = await renderToImageNative(renderClone, width, height, {\n skipDprScaling: true, // Use 1x DPR for video export (4x fewer pixels!)\n });\n const renderTime = performance.now() - t0;\n\n logger.debug(\n `[captureFromClone] native render=${renderTime.toFixed(0)}ms (canvasScale=${scale})`,\n );\n\n return canvas;\n } else {\n // FOREIGNOBJECT PATH: Serialize DOM → SVG → Image → Canvas\n // More compatible but slower than native path\n\n // NOTE: seekForRender() has already ensured rendering is complete, including:\n // - Lit updates propagated\n // - All LitElement descendants updated\n // - frameController.renderFrame() called for FrameRenderable elements\n // - Layout stabilization complete\n // No additional RAF wait needed - can serialize immediately\n\n const t0 = performance.now();\n const dataUri = await captureTimelineToDataUri(\n renderClone,\n width,\n height,\n {\n renderContext,\n canvasScale: scale,\n timeMs,\n },\n );\n const serializeTime = performance.now() - t0;\n\n const t1 = performance.now();\n const image = await loadImageFromDataUri(dataUri);\n const loadTime = performance.now() - t1;\n\n logger.debug(\n `[captureFromClone] foreignObject serialize=${serializeTime.toFixed(0)}ms, load=${loadTime.toFixed(0)}ms (canvasScale=${scale})`,\n );\n\n // Return image directly - no copy needed!\n return image;\n }\n } finally {\n // Ensure RenderContext is disposed even if an error occurs\n renderContext.dispose();\n }\n}\n\n/**\n * Captures a single frame from a timegroup at a specific time.\n *\n * CLONE-TIMELINE ARCHITECTURE:\n * Creates an independent render clone, seeks it to the target time, and captures.\n * Prime-timeline is NEVER seeked - user can continue previewing/editing during capture.\n *\n * @param timegroup - The source timegroup\n * @param options - Capture options including timeMs, scale, contentReadyMode\n * @returns Canvas with the rendered frame\n * @throws ContentNotReadyError if blocking mode times out waiting for video content\n */\nexport async function captureTimegroupAtTime(\n timegroup: EFTimegroup,\n options: CaptureOptions,\n): Promise<CanvasImageSource> {\n const {\n timeMs,\n scale = DEFAULT_CAPTURE_SCALE,\n // skipRestore is deprecated with Clone-timeline (Prime is never seeked)\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n canvasMode,\n skipClone = false,\n } = options;\n\n if (skipClone) {\n // DIRECT RENDERING: Skip clone creation for headless server rendering\n // Seek prime timeline directly and capture from it\n // WARNING: This modifies the prime timeline! Only use in headless contexts.\n\n const seekStart = performance.now();\n await timegroup.seekForRender(timeMs);\n const seekMs = performance.now() - seekStart;\n\n const renderStart = performance.now();\n // Use timegroup's actual container (parentElement or document.body as fallback)\n const container = (timegroup.parentElement || document.body) as HTMLElement;\n const result = await captureFromClone(timegroup, container, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n originalTimegroup: undefined, // No original since we're rendering the prime\n canvasMode,\n timeMs, // Pass explicit time since we're not using a clone\n });\n const renderMs = performance.now() - renderStart;\n\n // Store timing (no clone time since we skipped it)\n if (typeof result === \"object\" && result !== null) {\n (result as any).__perfTiming = { cloneMs: 0, seekMs, renderMs };\n }\n\n return result;\n }\n\n // CLONE-TIMELINE: Create a short-lived render clone for this capture\n // Prime-timeline is NEVER seeked - clone is fully independent\n const cloneStart = performance.now();\n const {\n clone: renderClone,\n container: renderContainer,\n cleanup: cleanupRenderClone,\n } = await timegroup.createRenderClone();\n const cloneMs = performance.now() - cloneStart;\n\n try {\n // Seek the clone to target time (Prime stays at user position)\n // Use seekForRender which bypasses duration clamping - render clones may have\n // zero duration initially until media durations are computed, but we still\n // want to seek to the requested time for capture purposes.\n const seekStart = performance.now();\n await renderClone.seekForRender(timeMs);\n const seekMs = performance.now() - seekStart;\n\n // Use the shared capture helper\n const renderStart = performance.now();\n const result = await captureFromClone(renderClone, renderContainer, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n originalTimegroup: timegroup,\n canvasMode,\n });\n const renderMs = performance.now() - renderStart;\n\n // Store timing on the result for access by callers (if they need it)\n // Note: CanvasImageSource doesn't support custom properties, but we can attach them anyway\n if (typeof result === \"object\" && result !== null) {\n (result as any).__perfTiming = { cloneMs, seekMs, renderMs };\n }\n\n return result;\n } finally {\n // Clean up the render clone\n cleanupRenderClone();\n }\n}\n\n/**\n * Generate thumbnails using an existing render clone and mutable queue.\n * The queue can be modified while generation is in progress.\n *\n * @param renderClone - Pre-created render clone to use\n * @param renderContainer - Container for the render clone\n * @param queue - Mutable queue that provides timestamps\n * @param options - Capture options (scale, contentReadyMode, etc.)\n * @yields Objects with { timeMs, canvas } for each captured thumbnail\n *\n * @example\n * ```ts\n * const queue = new MutableTimestampQueue();\n * queue.reset([0, 100, 200]);\n *\n * for await (const { timeMs, canvas } of generateThumbnailsFromClone(clone, container, queue)) {\n * cache.set(timeMs, canvas);\n * // Queue can be modified here while generator continues\n * }\n * ```\n */\nexport async function* generateThumbnailsFromClone(\n renderClone: EFTimegroup,\n renderContainer: HTMLElement,\n queue: ThumbnailQueue,\n options: GenerateThumbnailsOptions = {},\n): AsyncGenerator<GeneratedThumbnail> {\n const {\n scale = DEFAULT_CAPTURE_SCALE,\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n signal,\n } = options;\n\n while (true) {\n // Check if aborted before starting work\n if (signal?.aborted) {\n break;\n }\n\n const timeMs = queue.shift();\n if (timeMs === undefined) {\n // Queue is empty, generator exits\n break;\n }\n\n // Seek the clone to the target time\n await renderClone.seekForRender(timeMs);\n\n // Check if aborted after seek (before expensive capture)\n if (signal?.aborted) {\n break;\n }\n\n // Capture from the seeked clone, passing explicit timeMs\n const canvas = await captureFromClone(renderClone, renderContainer, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n timeMs, // CRITICAL: Pass explicit time for accurate temporal visibility\n });\n\n // Yield the result with explicit timestamp association\n yield { timeMs, canvas };\n }\n}\n\n/**\n * Generate thumbnails for multiple timestamps efficiently using a single render clone.\n * This avoids the overhead of creating/destroying a clone for each thumbnail.\n *\n * @param timegroup - The timegroup to capture\n * @param timestamps - Array of timestamps to capture (in milliseconds)\n * @param options - Capture options (scale, contentReadyMode, etc.)\n * @param signal - Optional AbortSignal to cancel generation\n * @yields Objects with { timeMs, canvas } for each captured thumbnail\n *\n * @example\n * ```ts\n * for await (const { timeMs, canvas } of generateThumbnails(tg, [0, 100, 200])) {\n * console.log(`Got thumbnail for ${timeMs}ms`);\n * thumbnailCache.set(timeMs, canvas);\n * }\n * ```\n */\nexport async function* generateThumbnails(\n timegroup: EFTimegroup,\n timestamps: number[],\n options: GenerateThumbnailsOptions = {},\n signal?: AbortSignal,\n): AsyncGenerator<GeneratedThumbnail> {\n const {\n scale = DEFAULT_CAPTURE_SCALE,\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n } = options;\n\n // Create a single render clone for all thumbnails\n const {\n clone: renderClone,\n container: renderContainer,\n cleanup: cleanupRenderClone,\n } = await timegroup.createRenderClone();\n\n try {\n for (const timeMs of timestamps) {\n // Check for abort before each capture\n signal?.throwIfAborted();\n\n // Seek the clone to the target time\n await renderClone.seekForRender(timeMs);\n\n // Capture from the seeked clone\n const canvas = await captureFromClone(renderClone, renderContainer, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n originalTimegroup: timegroup,\n });\n\n // Yield the result with explicit timestamp association\n yield { timeMs, canvas };\n }\n } finally {\n // Always clean up the render clone\n cleanupRenderClone();\n }\n}\n\n/** Epsilon for comparing time values (ms) - times within this are considered equal */\nconst TIME_EPSILON_MS = 1;\n\n/** Default scale for preview rendering */\nconst DEFAULT_PREVIEW_SCALE = 1;\n\n/** Default resolution scale (full resolution) */\nconst DEFAULT_RESOLUTION_SCALE = 1;\n\n/**\n * Convert relative time to absolute time for a timegroup.\n * Nested timegroup children have ABSOLUTE startTimeMs values,\n * so relative capture times must be converted for temporal culling.\n */\nfunction toAbsoluteTime(\n timegroup: EFTimegroup,\n relativeTimeMs: number,\n): number {\n return relativeTimeMs + (timegroup.startTimeMs ?? 0);\n}\n\n/**\n * Renders a timegroup preview to a canvas using SVG foreignObject.\n *\n * Captures the prime timeline's current visual state including DOM changes\n * from frame tasks (SVG paths, canvas content, text updates, etc.).\n *\n * Optimized with:\n * - Passive clone structure rebuilt each frame from prime's current state\n * - Temporal bucketing for time-based culling\n * - RenderContext for canvas pixel caching across frames\n * - Resolution scaling for performance (renders at lower resolution, CSS upscales)\n *\n * @param timegroup - The source timegroup to preview (prime timeline)\n * @param scaleOrOptions - Scale factor (default 1) or options object\n * @returns Object with canvas and refresh function\n */\nexport function renderTimegroupToCanvas(\n timegroup: EFTimegroup,\n scaleOrOptions: number | CanvasPreviewOptions = DEFAULT_PREVIEW_SCALE,\n): CanvasPreviewResult {\n // Normalize options\n const options: CanvasPreviewOptions =\n typeof scaleOrOptions === \"number\"\n ? { scale: scaleOrOptions }\n : scaleOrOptions;\n\n const scale = options.scale ?? DEFAULT_PREVIEW_SCALE;\n // These are mutable to support dynamic resolution changes\n let currentResolutionScale =\n options.resolutionScale ?? DEFAULT_RESOLUTION_SCALE;\n\n const width = timegroup.offsetWidth || DEFAULT_WIDTH;\n const height = timegroup.offsetHeight || DEFAULT_HEIGHT;\n const dpr =\n (typeof window !== \"undefined\" ? window.devicePixelRatio : 1) || 1;\n\n // Calculate effective render dimensions (internal resolution) - mutable\n let renderWidth = Math.floor(width * currentResolutionScale);\n let renderHeight = Math.floor(height * currentResolutionScale);\n\n // Create canvas with proper DPR handling\n const canvas = createDprCanvas({\n renderWidth,\n renderHeight,\n scale,\n fullWidth: width,\n fullHeight: height,\n dpr,\n });\n\n // Return canvas directly - no wrapper needed\n const wrapperContainer = canvas;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n throw new Error(\"Failed to get canvas 2d context\");\n }\n\n // Track render state\n let rendering = false;\n let lastTimeMs = -1;\n let disposed = false;\n\n // Create RenderContext for caching across refresh calls (foreignObject only)\n const renderContext = new RenderContext();\n\n // Create FrameController for coordinating element rendering\n // Cached for the lifetime of this preview instance\n const frameController = new FrameController(timegroup);\n\n // Log resolution scale on first render for debugging\n let hasLoggedScale = false;\n\n // Pending resolution change - applied at start of next refresh to avoid blanking\n let pendingResolutionScale: number | null = null;\n\n // Use the user's render mode preference. Native requires the timegroup to be\n // inside a <canvas layoutsubtree> for drawElementImage to work.\n const useNative =\n getRenderMode() === \"native\" && isNativeCanvasApiAvailable();\n let captureCanvas: HTMLCanvasElement | null = null;\n let captureCtx: HtmlInCanvasContext | null = null;\n let originalParent: ParentNode | null = null;\n let originalNextSibling: ChildNode | null = null;\n let savedClipPath = \"\";\n let savedPointerEvents = \"\";\n\n if (useNative) {\n captureCanvas = document.createElement(\"canvas\");\n captureCanvas.setAttribute(\"layoutsubtree\", \"\");\n (captureCanvas as HtmlInCanvasElement).layoutSubtree = true;\n captureCanvas.width = renderWidth;\n captureCanvas.height = renderHeight;\n captureCanvas.style.cssText = `position:fixed;left:0;top:0;width:${width}px;height:${height}px;opacity:0;pointer-events:none;z-index:-9999;`;\n originalParent = timegroup.parentNode;\n originalNextSibling = timegroup.nextSibling;\n savedClipPath = timegroup.style.clipPath;\n savedPointerEvents = timegroup.style.pointerEvents;\n timegroup.style.clipPath = \"\";\n timegroup.style.pointerEvents = \"\";\n captureCanvas.appendChild(timegroup);\n document.body.appendChild(captureCanvas);\n captureCtx = captureCanvas.getContext(\"2d\") as HtmlInCanvasContext;\n void captureCanvas.offsetHeight;\n void timegroup.offsetHeight;\n }\n\n /**\n * Apply pending resolution scale changes.\n * Called at the start of refresh() before rendering, so the old content\n * stays visible until new content is ready to be drawn.\n */\n const applyPendingResolutionChange = (): void => {\n if (pendingResolutionScale === null) return;\n\n const newScale = pendingResolutionScale;\n pendingResolutionScale = null;\n\n currentResolutionScale = newScale;\n renderWidth = Math.floor(width * currentResolutionScale);\n renderHeight = Math.floor(height * currentResolutionScale);\n\n if (captureCanvas) {\n captureCanvas.width = renderWidth;\n captureCanvas.height = renderHeight;\n }\n };\n\n /**\n * Dynamically change resolution scale without rebuilding clone structure.\n * The actual change is deferred until next refresh() to avoid blanking -\n * old content stays visible until new content is ready.\n */\n const setResolutionScale = (newScale: number): void => {\n // Clamp to valid range\n newScale = Math.max(0.1, Math.min(1, newScale));\n\n if (newScale === currentResolutionScale && pendingResolutionScale === null)\n return;\n\n // Queue the change - will be applied at start of next refresh\n pendingResolutionScale = newScale;\n\n // Force re-render on next refresh by invalidating lastTimeMs\n lastTimeMs = -1;\n };\n\n const getResolutionScale = (): number =>\n pendingResolutionScale ?? currentResolutionScale;\n\n // Rolling timing stats for per-phase profiling\n let frameCount = 0;\n let totalFrameControllerMs = 0;\n let totalCaptureMs = 0;\n let totalCopyMs = 0;\n let totalFrameMs = 0;\n\n const refresh = async (): Promise<void> => {\n if (disposed) return;\n\n const sourceTimeMs = timegroup.currentTimeMs ?? 0;\n const userTimeMs = timegroup.userTimeMs ?? 0;\n\n if (Math.abs(sourceTimeMs - userTimeMs) > TIME_EPSILON_MS) return;\n if (userTimeMs === lastTimeMs) return;\n if (rendering) return;\n\n lastTimeMs = userTimeMs;\n rendering = true;\n\n applyPendingResolutionChange();\n\n if (!hasLoggedScale) {\n hasLoggedScale = true;\n const mode = useNative ? \"native\" : \"foreignObject\";\n logger.debug(\n `[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}`,\n );\n }\n\n try {\n const tFrame = performance.now();\n\n const tFC0 = performance.now();\n await frameController.renderFrame(userTimeMs, {\n waitForLitUpdate: false,\n onAnimationsUpdate: (root) => {\n updateAnimations(root as AnimatableElement);\n },\n });\n const fcMs = performance.now() - tFC0;\n\n const tCapture0 = performance.now();\n\n if (useNative && captureCanvas && captureCtx) {\n if (captureCanvas.width !== width || captureCanvas.height !== height) {\n captureCtx.save();\n captureCtx.scale(\n captureCanvas.width / width,\n captureCanvas.height / height,\n );\n captureCtx.drawElementImage(timegroup, 0, 0);\n captureCtx.restore();\n } else {\n captureCtx.drawElementImage(timegroup, 0, 0);\n }\n const captureMs = performance.now() - tCapture0;\n\n const tCopy0 = performance.now();\n const targetWidth = Math.floor(renderWidth * scale * dpr);\n const targetHeight = Math.floor(renderHeight * scale * dpr);\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n } else {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n }\n ctx.drawImage(captureCanvas, 0, 0, canvas.width, canvas.height);\n const copyMs = performance.now() - tCopy0;\n\n const frameMs = performance.now() - tFrame;\n frameCount++;\n totalFrameControllerMs += fcMs;\n totalCaptureMs += captureMs;\n totalCopyMs += copyMs;\n totalFrameMs += frameMs;\n\n defaultProfiler.incrementRenderCount();\n if (defaultProfiler.shouldLogByFrameCount(60)) {\n frameCount = 0;\n totalFrameControllerMs = 0;\n totalCaptureMs = 0;\n totalCopyMs = 0;\n totalFrameMs = 0;\n }\n } else {\n const absoluteTimeMs = toAbsoluteTime(timegroup, userTimeMs);\n\n const dataUri = await captureTimelineToDataUri(\n timegroup,\n width,\n height,\n {\n renderContext,\n canvasScale: currentResolutionScale,\n timeMs: absoluteTimeMs,\n },\n );\n const captureMs = performance.now() - tCapture0;\n\n const tCopy0 = performance.now();\n const image = await loadImageFromDataUri(dataUri);\n const copyMs = performance.now() - tCopy0;\n\n const targetWidth = Math.floor(renderWidth * scale * dpr);\n const targetHeight = Math.floor(renderHeight * scale * dpr);\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n } else {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n }\n\n ctx.save();\n ctx.scale(dpr * scale, dpr * scale);\n ctx.drawImage(image, 0, 0, renderWidth, renderHeight);\n ctx.restore();\n\n const frameMs = performance.now() - tFrame;\n frameCount++;\n totalFrameControllerMs += fcMs;\n totalCaptureMs += captureMs;\n totalCopyMs += copyMs;\n totalFrameMs += frameMs;\n\n defaultProfiler.incrementRenderCount();\n if (defaultProfiler.shouldLogByFrameCount(60)) {\n frameCount = 0;\n totalFrameControllerMs = 0;\n totalCaptureMs = 0;\n totalCopyMs = 0;\n totalFrameMs = 0;\n }\n }\n } catch (e) {\n logger.error(\"Canvas preview render failed:\", e);\n } finally {\n rendering = false;\n }\n };\n\n /**\n * Dispose the preview and release resources.\n */\n const dispose = (): void => {\n if (disposed) return;\n disposed = true;\n frameController.abort();\n renderContext.dispose();\n\n // Restore timegroup to original DOM position if native mode moved it\n if (useNative && originalParent) {\n timegroup.style.clipPath = savedClipPath;\n timegroup.style.pointerEvents = savedPointerEvents;\n if (originalNextSibling) {\n originalParent.insertBefore(timegroup, originalNextSibling);\n } else {\n originalParent.appendChild(timegroup);\n }\n captureCanvas?.remove();\n }\n };\n\n // Do initial render\n refresh();\n\n return {\n container: wrapperContainer,\n canvas,\n refresh,\n setResolutionScale,\n getResolutionScale,\n dispose,\n };\n}\n"],"mappings":"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"}