@editframe/elements 0.46.2 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/elements/EFCaptions.d.ts +2 -2
- package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +50 -0
- package/dist/elements/EFMedia/BufferedSeekingInput.js +6 -5
- package/dist/elements/EFMedia/BufferedSeekingInput.js.map +1 -1
- package/dist/elements/EFMedia/CachedFetcher.js +23 -33
- package/dist/elements/EFMedia/CachedFetcher.js.map +1 -1
- package/dist/elements/EFMedia/SegmentTransport.d.ts +2 -2
- package/dist/elements/EFMedia/SegmentTransport.js.map +1 -1
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +53 -0
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +20 -5
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.d.ts +48 -0
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +36 -7
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
- package/dist/elements/EFMedia.d.ts +2 -2
- package/dist/elements/EFMotionBlur.d.ts +130 -0
- package/dist/elements/EFMotionBlur.js +808 -0
- package/dist/elements/EFMotionBlur.js.map +1 -0
- package/dist/elements/EFTemporal.js +1 -2
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFText.d.ts +20 -0
- package/dist/elements/EFText.js +66 -9
- package/dist/elements/EFText.js.map +1 -1
- package/dist/elements/EFTimegroup.d.ts +12 -0
- package/dist/elements/EFTimegroup.js +43 -4
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.d.ts +26 -0
- package/dist/elements/EFVideo.js +114 -36
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/elements/SampleBuffer.d.ts +19 -0
- package/dist/elements/updateAnimations.js +132 -27
- package/dist/elements/updateAnimations.js.map +1 -1
- package/dist/gui/EFWorkbench.d.ts +1 -0
- package/dist/gui/EFWorkbench.js +15 -0
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/EFWorkbench.spacebar.js +26 -0
- package/dist/gui/EFWorkbench.spacebar.js.map +1 -0
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/TWMixin.js.map +1 -1
- package/dist/gui/timeline/EFTimeline.d.ts +18 -1
- package/dist/gui/timeline/EFTimeline.js +119 -25
- package/dist/gui/timeline/EFTimeline.js.map +1 -1
- package/dist/gui/timeline/timelineStateContext.d.ts +2 -0
- package/dist/gui/timeline/timelineStateContext.js.map +1 -1
- package/dist/gui/timeline/tracks/EFThumbnailStrip.js +14 -8
- package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/preview/FrameController.d.ts +22 -1
- package/dist/preview/FrameController.js +26 -5
- package/dist/preview/FrameController.js.map +1 -1
- package/dist/preview/QualityUpgradeScheduler.d.ts +11 -2
- package/dist/preview/QualityUpgradeScheduler.js +31 -21
- package/dist/preview/QualityUpgradeScheduler.js.map +1 -1
- package/dist/preview/renderTimegroupToCanvas.d.ts +4 -3
- package/dist/preview/renderTimegroupToCanvas.js +35 -33
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
- package/dist/preview/renderTimegroupToCanvas.types.d.ts +2 -0
- package/dist/preview/renderTimegroupToVideo.js +3 -0
- package/dist/preview/renderTimegroupToVideo.js.map +1 -1
- package/dist/preview/rendering/renderToImageNative.js +7 -2
- package/dist/preview/rendering/renderToImageNative.js.map +1 -1
- package/dist/preview/rendering/serializeTimelineDirect.js +30 -35
- package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -1
- package/dist/style.css +7 -0
- package/dist/utils/LRUCache.js +17 -5
- package/dist/utils/LRUCache.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EFThumbnailStrip.js","names":["#timestamps","EFThumbnailStrip","#timelineState","#thumbnailDimensions","#effectiveDurationMs","#effectivePixelsPerMs","#hostWidth","#targetController","#resizeObserver","#scheduleRender","#abortController","#cleanupTimegroupGenerator","#detachTargetListeners","#calculateThumbnailDimensions","#attachTargetListeners","#targetReadyStateHandler","#targetContentChangeHandler","thumbnails: ThumbnailDescriptor[]","#previousPixelsPerMs","#thumbnailPhase","#renderRequested","#renderThumbnails","#calculateVisibleThumbnails","#clearCanvas","#lastRequiredTimestamps","#updateVideoCapture","#updateTimegroupCapture","results: ThumbnailResult[]","#thumbnailCache","nearestTimeMs: number | null","#drawThumbnails","#getSourceTimeMs","#updateInProgress","#pendingTimestamps","#retryScheduled","#timegroupGenerator","#timegroupGeneratorAbort","#timegroupQueue","#timegroupClone","#previewContainer","#consumeTimegroupGenerator","#startTimegroupGenerator","updatePromises: Promise<any>[]","textUpdatePromises: Promise<any>[]","imagePromises: Promise<void>[]","#consumerRunning","#canvasContainer"],"sources":["../../../../src/gui/timeline/tracks/EFThumbnailStrip.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, LitElement } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { createRef, ref, type Ref } from \"lit/directives/ref.js\";\n\nimport { EFTimegroup } from \"../../../elements/EFTimegroup.js\";\nimport { EFVideo } from \"../../../elements/EFVideo.js\";\nimport { TargetController } from \"../../../elements/TargetController.js\";\nimport { ThumbnailExtractor } from \"../../../elements/EFMedia/shared/ThumbnailExtractor.js\";\nimport {\n generateThumbnailsFromClone,\n type GeneratedThumbnail,\n type ThumbnailQueue,\n} from \"../../../preview/renderTimegroupToCanvas.js\";\n\nimport { quantizeToFrameTimeMs } from \"../../../utils/frameTime.js\";\nimport { TWMixin } from \"../../TWMixin.js\";\nimport { timelineStateContext, type TimelineState } from \"../timelineStateContext.js\";\nimport { previewSettingsContext, type PreviewSettings } from \"../../previewSettingsContext.js\";\n\n/** Padding for virtual rendering */\nconst VIRTUAL_RENDER_PADDING_PX = 100;\n\n/**\n * Mutable queue for timestamp generation.\n * Allows updating timestamps while generator is consuming them.\n */\nclass MutableTimestampQueue implements ThumbnailQueue {\n #timestamps: number[] = [];\n\n /** Replace entire queue with new timestamps (sorted) */\n reset(timestamps: number[]): void {\n this.#timestamps = [...timestamps].sort((a, b) => a - b);\n }\n\n /** Keep only these specific timestamps (maintains order) */\n retainOnly(timestamps: number[]): void {\n const keep = new Set(timestamps);\n this.#timestamps = this.#timestamps.filter((t) => keep.has(t));\n }\n\n /** Append timestamps to end (sorted) */\n append(timestamps: number[]): void {\n this.#timestamps.push(...[...timestamps].sort((a, b) => a - b));\n }\n\n /** Get next timestamp (removes from front) */\n shift(): number | undefined {\n return this.#timestamps.shift();\n }\n\n /** Get remaining timestamps without modifying queue */\n remaining(): number[] {\n return [...this.#timestamps];\n }\n\n /** Check if queue is empty */\n isEmpty(): boolean {\n return this.#timestamps.length === 0;\n }\n}\n\n/**\n * Descriptor for a thumbnail to render\n */\ninterface ThumbnailDescriptor {\n timeMs: number;\n x: number;\n width: number;\n height: number;\n}\n\n/**\n * Result of thumbnail rendering (canvas or error)\n */\ninterface ThumbnailResult {\n canvas: CanvasImageSource | null;\n error?: Error;\n}\n\n/**\n * Thumbnail strip component that renders thumbnails for video or timegroup elements.\n *\n * Features:\n * - Targets ef-video or root ef-timegroup via target attribute\n * - Batch video thumbnail extraction via ThumbnailExtractor\n * - Canvas rendering for timegroups at low resolution\n * - Viewport-based lazy loading with scroll calculation\n * - Fixed visual spacing (consistent at all zoom levels)\n * - Error indicators for failed thumbnails\n */\n@customElement(\"ef-thumbnail-strip\")\nexport class EFThumbnailStrip extends TWMixin(LitElement) {\n static styles = [\n css`\n :host {\n display: block;\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n }\n\n .thumbnail-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n }\n\n .error-message {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n padding: 8px;\n color: var(--ef-color-error);\n font-size: 12px;\n background: color-mix(in srgb, var(--ef-color-error) 10%, transparent);\n }\n\n canvas {\n position: absolute;\n image-rendering: pixelated;\n image-rendering: crisp-edges;\n }\n\n .shimmer-overlay {\n display: none;\n position: absolute;\n inset: 0;\n background: linear-gradient(\n 90deg,\n color-mix(in srgb, var(--ef-color-text, #fafafa) 12%, transparent) 0%,\n color-mix(in srgb, var(--ef-color-text, #fafafa) 28%, transparent) 50%,\n color-mix(in srgb, var(--ef-color-text, #fafafa) 12%, transparent) 100%\n );\n background-size: 200% 100%;\n pointer-events: none;\n }\n\n .shimmer-overlay.active {\n display: block;\n animation: shimmer-strip var(--ef-loading-shimmer-duration, 1.5s) linear infinite;\n }\n\n @keyframes shimmer-strip {\n 0% { background-position: 200% 0; }\n 100% { background-position: -200% 0; }\n }\n `,\n ];\n\n @property({ type: String })\n target = \"\";\n\n @property({ attribute: false })\n targetElement: Element | null = null;\n\n @property({ type: Number, attribute: \"thumbnail-height\" })\n thumbnailHeight = 24;\n\n @property({ type: Number, attribute: \"thumbnail-spacing-px\" })\n thumbnailSpacingPx = 48;\n\n @property({ type: Number, attribute: \"pixels-per-ms\" })\n pixelsPerMs: number | null = null;\n\n @property({ type: Boolean, attribute: \"use-intrinsic-duration\" })\n useIntrinsicDuration = false;\n\n @consume({ context: timelineStateContext, subscribe: true })\n @state()\n timelineState?: TimelineState;\n\n @consume({ context: previewSettingsContext, subscribe: true })\n @state()\n previewSettings?: PreviewSettings;\n\n @state()\n thumbnailDimensions = { width: 0, height: 0 };\n\n @state()\n _isLoadingThumbnails = false;\n\n #targetController?: TargetController;\n #abortController: AbortController | null = null;\n #renderRequested = false;\n #canvasContainer: Ref<HTMLDivElement> = createRef();\n #lastRequiredTimestamps = \"\";\n #thumbnailCache = new Map<number, CanvasImageSource>();\n\n // Timegroup thumbnail generation state\n #timegroupQueue = new MutableTimestampQueue();\n #timegroupClone: {\n clone: EFTimegroup;\n container: HTMLElement;\n cleanup: () => void;\n } | null = null;\n #timegroupGenerator: AsyncGenerator<GeneratedThumbnail> | null = null;\n #timegroupGeneratorAbort: AbortController | null = null;\n #previewContainer: HTMLDivElement | null = null;\n #updateInProgress = false; // Lock to prevent concurrent updates\n #consumerRunning = false; // Lock to prevent concurrent consumers\n #pendingTimestamps = new Set<number>(); // Timestamps requested while update in progress\n #retryScheduled = false; // Flag to prevent duplicate retry schedules\n #thumbnailPhase: number = 0; // Phase offset for thumbnail grid\n #previousPixelsPerMs: number | null = null; // Track zoom changes\n #targetReadyStateHandler: (() => void) | null = null;\n #targetContentChangeHandler: (() => void) | null = null;\n #resizeObserver: ResizeObserver | null = null;\n #hostWidth = 0;\n\n /**\n * Check if target is valid (EFVideo or root EFTimegroup)\n */\n get isValidTarget(): boolean {\n const el = this.targetElement;\n if (!el) return false;\n\n if (el instanceof EFVideo) return true;\n\n if (el instanceof EFTimegroup) {\n // Only root timegroups\n return (el as any).isRootTimegroup === true;\n }\n\n return false;\n }\n\n get #timelineState(): TimelineState | undefined {\n return this.timelineState;\n }\n\n get #thumbnailDimensions() {\n return this.thumbnailDimensions;\n }\n\n get #effectiveDurationMs(): number {\n const element = this.targetElement;\n if (!element) return 0;\n if (this.useIntrinsicDuration) {\n return (element as any).intrinsicDurationMs ?? (element as any).durationMs ?? 0;\n }\n return (element as any).durationMs ?? 0;\n }\n\n get #effectivePixelsPerMs(): number {\n if (this.#timelineState?.pixelsPerMs != null) {\n return this.#timelineState.pixelsPerMs;\n }\n if (this.pixelsPerMs != null) {\n return this.pixelsPerMs;\n }\n const durationMs = this.#effectiveDurationMs;\n if (this.#hostWidth > 0 && durationMs > 0) {\n return this.#hostWidth / durationMs;\n }\n return 0.04;\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n // Only use TargetController if target is set and targetElement is not directly set\n if (this.target && !this.targetElement) {\n this.#targetController = new TargetController(this);\n }\n this.#resizeObserver = new ResizeObserver((entries) => {\n const entry = entries[0];\n if (!entry) return;\n const width = entry.contentRect.width;\n if (width !== this.#hostWidth) {\n this.#hostWidth = width;\n this.requestUpdate();\n this.#scheduleRender();\n }\n });\n this.#resizeObserver.observe(this);\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#abortController?.abort();\n this.#cleanupTimegroupGenerator();\n this.#detachTargetListeners(this.targetElement);\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n }\n\n protected willUpdate(changedProperties: Map<string | number | symbol, unknown>): void {\n super.willUpdate(changedProperties);\n\n // Create TargetController if target is set and targetElement is not directly set\n if (changedProperties.has(\"target\")) {\n if (this.target && !this.targetElement && !this.#targetController) {\n this.#targetController = new TargetController(this);\n }\n }\n\n // Recalculate thumbnail dimensions if target changed\n if (changedProperties.has(\"targetElement\") || changedProperties.has(\"thumbnailHeight\")) {\n this.thumbnailDimensions = this.#calculateThumbnailDimensions();\n }\n\n // Manage event listeners when target changes\n if (changedProperties.has(\"targetElement\")) {\n const oldTarget = changedProperties.get(\"targetElement\") as Element | null;\n this.#detachTargetListeners(oldTarget);\n this.#attachTargetListeners(this.targetElement);\n }\n }\n\n #attachTargetListeners(target: Element | null): void {\n if (!target) return;\n\n this.#targetReadyStateHandler = () => {\n this.requestUpdate();\n this.#scheduleRender();\n };\n this.#targetContentChangeHandler = () => {\n this.requestUpdate();\n this.#scheduleRender();\n };\n target.addEventListener(\"readystatechange\", this.#targetReadyStateHandler);\n target.addEventListener(\"contentchange\", this.#targetContentChangeHandler);\n\n // Late-subscriber: if the target already transitioned to \"ready\" before\n // we attached, the event was missed. The contentReadyState property\n // serves exactly this purpose — check it and render if needed.\n if ((target as any).contentReadyState === \"ready\") {\n this.requestUpdate();\n this.#scheduleRender();\n }\n }\n\n #detachTargetListeners(target: Element | null): void {\n if (!target) return;\n if (this.#targetReadyStateHandler) {\n target.removeEventListener(\"readystatechange\", this.#targetReadyStateHandler);\n this.#targetReadyStateHandler = null;\n }\n if (this.#targetContentChangeHandler) {\n target.removeEventListener(\"contentchange\", this.#targetContentChangeHandler);\n this.#targetContentChangeHandler = null;\n }\n }\n\n updated(changedProperties: Map<string | number | symbol, unknown>): void {\n super.updated(changedProperties);\n\n if (\n changedProperties.has(\"targetElement\") ||\n changedProperties.has(\"thumbnailSpacingPx\") ||\n changedProperties.has(\"pixelsPerMs\") ||\n changedProperties.has(\"thumbnailHeight\") ||\n changedProperties.has(\"timelineState\")\n ) {\n this.#scheduleRender();\n }\n }\n\n /**\n * Calculate thumbnail dimensions from target element's actual bounds\n */\n #calculateThumbnailDimensions(): { width: number; height: number } {\n const el = this.targetElement;\n if (!el) return { width: 0, height: 0 };\n\n // Get actual visible bounds from DOM\n const bounds = el.getBoundingClientRect();\n if (bounds.width === 0 || bounds.height === 0) {\n // Element not yet rendered or no size, use default aspect ratio\n return {\n width: this.thumbnailHeight * (16 / 9),\n height: this.thumbnailHeight,\n };\n }\n\n const aspectRatio = bounds.width / bounds.height;\n const width = Math.round(this.thumbnailHeight * aspectRatio);\n\n return { width, height: this.thumbnailHeight };\n }\n\n /**\n * Calculate visible thumbnails based on viewport\n */\n #calculateVisibleThumbnails(): ThumbnailDescriptor[] {\n if (!this.isValidTarget) return [];\n\n const element = this.targetElement;\n if (!element) return [];\n\n const scrollLeft = this.#timelineState?.viewportScrollLeft ?? 0;\n const viewportWidth = this.#timelineState?.viewportWidth ?? (this.#hostWidth || 800);\n const pixelsPerMs = this.#effectivePixelsPerMs;\n\n const durationMs = this.#effectiveDurationMs;\n if (durationMs === 0) return [];\n\n const trackWidthPx = durationMs * pixelsPerMs;\n\n // Get FPS for quantization\n const fps = (element as any).fps ?? 30;\n\n const visibleStartPx = scrollLeft - VIRTUAL_RENDER_PADDING_PX;\n const visibleEndPx = scrollLeft + viewportWidth + VIRTUAL_RENDER_PADDING_PX;\n\n const thumbnails: ThumbnailDescriptor[] = [];\n const { width, height } = this.#thumbnailDimensions;\n\n // Read minimum gap from CSS variable (--ef-thumbnail-gap, default 2px)\n const gapPx = parseFloat(getComputedStyle(this).getPropertyValue(\"--ef-thumbnail-gap\")) || 2;\n // Stride must be at least thumbnail width + gap to prevent overlap\n const thumbnailStride = Math.max(this.thumbnailSpacingPx, width + gapPx);\n\n // Detect zoom by checking if pixelsPerMs changed\n const isZoom = this.#previousPixelsPerMs !== null && this.#previousPixelsPerMs !== pixelsPerMs;\n\n if (this.#previousPixelsPerMs === null) {\n // First render: align grid to track start (t=0)\n this.#thumbnailPhase = 0;\n } else if (isZoom) {\n // On zoom: snap a thumbnail to near the left edge of viewport\n // This prevents visual slip during zoom operations\n this.#thumbnailPhase = scrollLeft % thumbnailStride;\n } else if (scrollLeft < thumbnailStride) {\n // When scrolled near the start, realign to t=0 to avoid left gap\n this.#thumbnailPhase = 0;\n }\n // During normal scroll: phase unchanged, grid scrolls naturally with track\n\n this.#previousPixelsPerMs = pixelsPerMs;\n\n // Generate thumbnail grid anchored at phase offset\n // Each thumbnail is at absolute track position: phase + (i * stride)\n // This means grid is stable in track space (scrolls naturally)\n const startIndex = Math.max(\n 0,\n Math.floor((visibleStartPx - this.#thumbnailPhase) / thumbnailStride),\n );\n const endIndex = Math.ceil((visibleEndPx - this.#thumbnailPhase) / thumbnailStride);\n\n for (let i = startIndex; i <= endIndex; i++) {\n const thumbX = this.#thumbnailPhase + i * thumbnailStride;\n\n // Only include if within track bounds\n if (thumbX >= 0 && thumbX < trackWidthPx) {\n // Convert position to time (leading edge)\n const rawTimeMs = thumbX / pixelsPerMs;\n const timeMs = quantizeToFrameTimeMs(rawTimeMs, fps);\n\n if (timeMs >= 0 && timeMs < durationMs) {\n thumbnails.push({ timeMs, x: thumbX, width, height });\n }\n }\n }\n\n return thumbnails;\n }\n\n /**\n * Schedule thumbnail render on next frame\n */\n #scheduleRender(): void {\n if (this.#renderRequested) return;\n this.#renderRequested = true;\n\n requestAnimationFrame(() => {\n this.#renderRequested = false;\n this.#renderThumbnails();\n });\n }\n\n /**\n * Render thumbnails with cancellation support\n */\n async #renderThumbnails(): Promise<void> {\n // Cancel previous render\n this.#abortController?.abort();\n this.#abortController = new AbortController();\n const signal = this.#abortController.signal;\n\n const visibleThumbnails = this.#calculateVisibleThumbnails();\n if (visibleThumbnails.length === 0) {\n this.#clearCanvas();\n return;\n }\n\n // Check if required timestamps changed\n const requiredTimestamps = visibleThumbnails.map((t) => t.timeMs);\n const timestampsString = requiredTimestamps.join(\", \");\n if (timestampsString !== this.#lastRequiredTimestamps) {\n this.#lastRequiredTimestamps = timestampsString;\n\n // Update capture queue\n if (this.targetElement instanceof EFVideo) {\n this.#updateVideoCapture(requiredTimestamps, signal).catch((error) => {\n // Ignore AbortErrors - these are expected when renders are cancelled\n if (error instanceof DOMException && error.name === \"AbortError\") {\n return;\n }\n console.error(\"Thumbnail capture error:\", error);\n });\n } else if (this.targetElement instanceof EFTimegroup) {\n this.#updateTimegroupCapture(requiredTimestamps);\n }\n }\n\n if (signal.aborted) return;\n\n // Draw thumbnails - use nearest neighbor if exact timestamp not cached\n const maxNeighborDistanceMs = 3000; // Don't use thumbnails more than 3s away\n const results: ThumbnailResult[] = visibleThumbnails.map((t) => {\n let canvas = this.#thumbnailCache.get(t.timeMs);\n\n // If exact match not found, find nearest cached thumbnail\n if (!canvas) {\n let nearestTimeMs: number | null = null;\n let minDistance = Infinity;\n\n for (const cachedTimeMs of this.#thumbnailCache.keys()) {\n const distance = Math.abs(cachedTimeMs - t.timeMs);\n if (distance < minDistance && distance <= maxNeighborDistanceMs) {\n minDistance = distance;\n nearestTimeMs = cachedTimeMs;\n }\n }\n\n if (nearestTimeMs !== null) {\n canvas = this.#thumbnailCache.get(nearestTimeMs);\n }\n }\n\n return { canvas: canvas ?? null };\n });\n this.#drawThumbnails(visibleThumbnails, results);\n\n const hasEmptySlots = results.some((r) => r.canvas === null);\n if (this._isLoadingThumbnails !== hasEmptySlots) {\n this._isLoadingThumbnails = hasEmptySlots;\n }\n }\n\n /**\n * Update video thumbnail capture\n */\n async #updateVideoCapture(timestamps: number[], signal: AbortSignal): Promise<void> {\n const video = this.targetElement as EFVideo;\n if (!video) return;\n\n // Filter out cached timestamps\n const uncached = timestamps.filter((t) => !this.#thumbnailCache.has(t));\n if (uncached.length === 0) return;\n\n const mediaEngineTask = video.mediaEngineTask;\n if (!mediaEngineTask) return;\n\n const mediaEngine = await mediaEngineTask.taskComplete;\n if (!mediaEngine) return;\n\n const sourceTimestamps = uncached.map((t) => this.#getSourceTimeMs(t));\n\n const extractor = new ThumbnailExtractor(mediaEngine);\n const videoTrack = mediaEngine.tracks.video ?? mediaEngine.tracks.scrub;\n if (!videoTrack) return;\n\n const results = await extractor.extractThumbnails(\n sourceTimestamps,\n videoTrack,\n video.durationMs ?? 0,\n signal,\n );\n\n // Store in cache and trigger redraw\n for (let i = 0; i < uncached.length; i++) {\n const thumbnail = results[i]?.thumbnail;\n const timestamp = uncached[i];\n if (thumbnail && timestamp !== undefined) {\n this.#thumbnailCache.set(timestamp, thumbnail);\n }\n }\n\n this.#scheduleRender();\n }\n\n /**\n * Update timegroup thumbnail capture using mutable queue\n */\n async #updateTimegroupCapture(timestamps: number[]): Promise<void> {\n const timegroup = this.targetElement as EFTimegroup;\n if (!timegroup) return;\n\n // Filter out cached timestamps\n const uncached = timestamps.filter((t) => !this.#thumbnailCache.has(t)).sort((a, b) => a - b);\n if (uncached.length === 0) {\n return;\n }\n\n // CRITICAL: If update already in progress, REPLACE pending (not add)\n // We only want the LATEST required timestamps, not a union of all previous ones\n if (this.#updateInProgress) {\n // Clear old pending and replace with latest\n this.#pendingTimestamps.clear();\n uncached.forEach((t) => this.#pendingTimestamps.add(t));\n\n // Schedule a retry (debounced via RAF)\n if (!this.#retryScheduled) {\n this.#retryScheduled = true;\n requestAnimationFrame(() => {\n this.#retryScheduled = false;\n if (this.#pendingTimestamps.size > 0) {\n const pending = Array.from(this.#pendingTimestamps);\n this.#pendingTimestamps.clear();\n this.#updateTimegroupCapture(pending);\n }\n });\n }\n return;\n }\n this.#updateInProgress = true;\n\n try {\n if (this.#timegroupGenerator) {\n // Generator is running - abort and reset queue to exactly what we need now\n // Abort in-flight capture\n this.#timegroupGeneratorAbort?.abort();\n\n // Create new abort controller for the updated queue\n this.#timegroupGeneratorAbort = new AbortController();\n\n // Reset queue to exactly what we need now\n this.#timegroupQueue.reset(uncached);\n } else if (this.#timegroupClone) {\n // Generator finished, restart with existing clone\n this.#timegroupQueue.reset(uncached);\n\n // Create new abort controller\n this.#timegroupGeneratorAbort = new AbortController();\n\n this.#timegroupGenerator = generateThumbnailsFromClone(\n this.#timegroupClone.clone,\n this.#previewContainer!,\n this.#timegroupQueue,\n {\n scale: 0.25,\n contentReadyMode: \"blocking\",\n blockingTimeoutMs: 1000,\n signal: this.#timegroupGeneratorAbort.signal,\n },\n );\n await this.#consumeTimegroupGenerator();\n } else {\n // No generator or clone, start fresh\n await this.#startTimegroupGenerator(timegroup, uncached);\n }\n } finally {\n this.#updateInProgress = false;\n\n // Check if there are pending timestamps that need processing\n // This happens when updates were skipped while this update was in progress\n if (this.#pendingTimestamps.size > 0 && !this.#retryScheduled) {\n this.#retryScheduled = true;\n requestAnimationFrame(() => {\n this.#retryScheduled = false;\n if (this.#pendingTimestamps.size > 0) {\n const pending = Array.from(this.#pendingTimestamps);\n this.#pendingTimestamps.clear();\n this.#updateTimegroupCapture(pending);\n }\n });\n }\n }\n }\n\n /**\n * Start timegroup thumbnail generator\n */\n async #startTimegroupGenerator(timegroup: EFTimegroup, timestamps: number[]): Promise<void> {\n // Create render clone\n this.#timegroupClone = await timegroup.createRenderClone();\n\n // Use the original container from createRenderClone (already configured)\n this.#previewContainer = this.#timegroupClone.container as HTMLDivElement;\n\n // CRITICAL: Wait for Lit to process shadow DOM updates after moving to new container\n await this.#timegroupClone.clone.updateComplete;\n\n // Also wait for all nested Lit elements to update\n const litElements = this.#previewContainer.querySelectorAll(\"*\");\n const updatePromises: Promise<any>[] = [];\n for (const el of litElements) {\n if (\"updateComplete\" in el) {\n updatePromises.push((el as any).updateComplete);\n }\n }\n await Promise.all(updatePromises);\n\n // Wait AGAIN specifically for text segments (they may need to re-render after move)\n const textSegments = this.#previewContainer.querySelectorAll(\"ef-text-segment\");\n const textUpdatePromises: Promise<any>[] = [];\n for (const seg of textSegments) {\n if (\"updateComplete\" in seg) {\n textUpdatePromises.push((seg as any).updateComplete);\n }\n }\n await Promise.all(textUpdatePromises);\n\n // CRITICAL: Wait for ef-text to split text into segments\n // EFText.connectedCallback schedules splitText in requestAnimationFrame\n // We must wait for that RAF to fire before capturing\n await new Promise((resolve) => requestAnimationFrame(resolve));\n\n // WARMUP: Do a seek to the first timestamp to \"prime\" the clone\n // Guard: clone may have been disposed during prior awaits\n if (!this.#timegroupClone) return;\n if (timestamps.length > 0) {\n await this.#timegroupClone.clone.seekForRender(timestamps[0]!);\n }\n\n // CRITICAL: Wait for fonts to load\n // Text won't render correctly if fonts aren't ready\n await document.fonts.ready;\n\n // CRITICAL: Wait for all images to load\n const images = this.#previewContainer.querySelectorAll(\"img\");\n const imagePromises: Promise<void>[] = [];\n for (const img of images) {\n if (!img.complete) {\n imagePromises.push(\n new Promise((resolve, _reject) => {\n img.onload = () => resolve();\n img.onerror = () => resolve(); // Don't block on errors\n // Timeout after 5s\n setTimeout(() => resolve(), 5000);\n }),\n );\n }\n }\n await Promise.all(imagePromises);\n\n // Guard: clone may have been disposed during prior awaits\n if (!this.#timegroupClone) return;\n\n // Initialize queue\n this.#timegroupQueue.reset(timestamps);\n\n // Create abort controller for this generator\n this.#timegroupGeneratorAbort = new AbortController();\n\n // Start generator using the fresh container\n this.#timegroupGenerator = generateThumbnailsFromClone(\n this.#timegroupClone.clone,\n this.#previewContainer,\n this.#timegroupQueue,\n {\n scale: 0.25,\n contentReadyMode: \"blocking\",\n blockingTimeoutMs: 1000,\n signal: this.#timegroupGeneratorAbort.signal,\n },\n );\n\n // Consume generator (CRITICAL: Must await to prevent concurrent consumers)\n await this.#consumeTimegroupGenerator();\n }\n\n /**\n * Consume generator and handle cleanup\n */\n async #consumeTimegroupGenerator(): Promise<void> {\n // CRITICAL: Prevent concurrent consumers\n if (this.#consumerRunning) {\n return;\n }\n this.#consumerRunning = true;\n\n if (!this.#timegroupGenerator) {\n this.#consumerRunning = false;\n return;\n }\n\n try {\n for await (const { timeMs, canvas } of this.#timegroupGenerator) {\n this.#thumbnailCache.set(timeMs, canvas);\n this.#scheduleRender();\n }\n } catch (err) {\n console.warn(\"Timegroup thumbnail generation error:\", err);\n } finally {\n // Generator finished, but keep clone alive for reuse\n this.#timegroupGenerator = null;\n this.#consumerRunning = false;\n }\n }\n\n /**\n * Cleanup timegroup generator and clone\n */\n #cleanupTimegroupGenerator(): void {\n // Abort any in-flight work\n this.#timegroupGeneratorAbort?.abort();\n this.#timegroupGeneratorAbort = null;\n\n this.#timegroupGenerator = null;\n\n // Remove preview container from DOM\n if (this.#previewContainer) {\n this.#previewContainer.remove();\n this.#previewContainer = null;\n }\n\n // Cleanup render clone\n if (this.#timegroupClone) {\n this.#timegroupClone.cleanup();\n this.#timegroupClone = null;\n }\n }\n\n /**\n * Clear all canvas elements\n */\n #clearCanvas(): void {\n const container = this.#canvasContainer.value;\n if (container) {\n container.innerHTML = \"\";\n }\n }\n\n /**\n * Translate composition time to source time for videos (handles trim)\n */\n #getSourceTimeMs(compositionTimeMs: number): number {\n if (this.useIntrinsicDuration) {\n return compositionTimeMs;\n }\n const el = this.targetElement;\n if (el instanceof EFVideo) {\n return compositionTimeMs + (el.sourceStartMs ?? 0);\n }\n return compositionTimeMs;\n }\n\n /**\n * Draw thumbnails to canvas elements\n */\n #drawThumbnails(thumbnails: ThumbnailDescriptor[], results: ThumbnailResult[]): void {\n const container = this.#canvasContainer.value;\n if (!container) return;\n\n // Clear existing canvases\n container.innerHTML = \"\";\n\n for (let i = 0; i < thumbnails.length; i++) {\n const thumbnail = thumbnails[i];\n const result = results[i];\n\n if (!thumbnail) continue;\n\n const canvas = document.createElement(\"canvas\");\n canvas.width = thumbnail.width;\n canvas.height = thumbnail.height;\n canvas.style.left = `${thumbnail.x}px`;\n canvas.style.top = \"0\";\n canvas.style.width = `${thumbnail.width}px`;\n canvas.style.height = `${thumbnail.height}px`;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) continue;\n\n if (result?.canvas) {\n // Draw actual thumbnail\n ctx.drawImage(result.canvas, 0, 0, thumbnail.width, thumbnail.height);\n\n // Draw timestamp overlay if enabled\n if (this.previewSettings?.showThumbnailTimestamps) {\n ctx.fillStyle = \"rgba(0, 0, 0, 0.8)\";\n ctx.fillRect(2, 2, 95, 16);\n ctx.fillStyle = \"yellow\";\n ctx.font = \"11px monospace\";\n ctx.textAlign = \"left\";\n ctx.textBaseline = \"top\";\n ctx.fillText(`${Math.round(thumbnail.timeMs)}ms`, 5, 4);\n }\n } else {\n // Draw placeholder with timestamp text\n const bgColor =\n getComputedStyle(this).getPropertyValue(\"--ef-color-bg-inset\").trim() ||\n \"rgba(100, 100, 100, 0.3)\";\n ctx.fillStyle = bgColor;\n ctx.fillRect(0, 0, thumbnail.width, thumbnail.height);\n\n const borderColor =\n getComputedStyle(this).getPropertyValue(\"--ef-color-border-subtle\").trim() ||\n \"rgba(150, 150, 150, 0.5)\";\n ctx.strokeStyle = borderColor;\n ctx.lineWidth = 1;\n ctx.strokeRect(0, 0, thumbnail.width, thumbnail.height);\n\n ctx.fillStyle = \"white\";\n ctx.font = \"10px monospace\";\n ctx.textAlign = \"center\";\n ctx.textBaseline = \"middle\";\n ctx.fillText(\n `${Math.round(thumbnail.timeMs)}ms`,\n thumbnail.width / 2,\n thumbnail.height / 2,\n );\n }\n\n container.appendChild(canvas);\n }\n }\n\n render() {\n // Error: No target specified (neither target string nor targetElement)\n if (!this.target && !this.targetElement) {\n return html`<div class=\"error-message\">No target specified</div>`;\n }\n\n // Error: Target element not found (when using target string)\n if (this.target && !this.targetElement) {\n return html`<div class=\"error-message\">\n Target element \"${this.target}\" not found\n </div>`;\n }\n\n // Error: Invalid target type\n if (!this.isValidTarget) {\n const elementType = (this.targetElement as any).tagName?.toLowerCase() || \"unknown\";\n return html`<div class=\"error-message\">\n Invalid target: \"${elementType}\" must be ef-video or root ef-timegroup\n </div>`;\n }\n\n // Calculate track width to clip thumbnails at track end\n const durationMs = this.#effectiveDurationMs;\n const pixelsPerMs = this.#effectivePixelsPerMs;\n const trackWidthPx = durationMs * pixelsPerMs;\n\n // Render canvas container with explicit width clipping, plus shimmer overlay\n return html`\n <div\n class=\"thumbnail-container\"\n style=\"max-width: ${trackWidthPx}px;\"\n ${ref(this.#canvasContainer)}\n ></div>\n <div\n class=\"shimmer-overlay ${this._isLoadingThumbnails ? \"active\" : \"\"}\"\n ></div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-thumbnail-strip\": EFThumbnailStrip;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EFThumbnailStrip.js","names":["#timestamps","EFThumbnailStrip","#timelineState","#thumbnailDimensions","#effectiveDurationMs","#effectivePixelsPerMs","#hostWidth","#targetController","#resizeObserver","#scheduleRender","#abortController","#cleanupTimegroupGenerator","#detachTargetListeners","#calculateThumbnailDimensions","#attachTargetListeners","#lastRequiredTimestamps","#targetReadyStateHandler","#targetContentChangeHandler","thumbnails: ThumbnailDescriptor[]","#previousPixelsPerMs","#thumbnailPhase","#renderRequested","#renderThumbnails","#calculateVisibleThumbnails","#clearCanvas","#updateVideoCapture","#updateTimegroupCapture","results: ThumbnailResult[]","#thumbnailCache","nearestTimeMs: number | null","#drawThumbnails","#getSourceTimeMs","#updateInProgress","#pendingTimestamps","#retryScheduled","#timegroupGenerator","#timegroupGeneratorAbort","#timegroupQueue","#timegroupClone","#previewContainer","#consumeTimegroupGenerator","#startTimegroupGenerator","updatePromises: Promise<any>[]","textUpdatePromises: Promise<any>[]","imagePromises: Promise<void>[]","#consumerRunning","#canvasContainer"],"sources":["../../../../src/gui/timeline/tracks/EFThumbnailStrip.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, LitElement } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { createRef, ref, type Ref } from \"lit/directives/ref.js\";\n\nimport { EFTimegroup } from \"../../../elements/EFTimegroup.js\";\nimport { EFVideo } from \"../../../elements/EFVideo.js\";\nimport { TargetController } from \"../../../elements/TargetController.js\";\nimport { ThumbnailExtractor } from \"../../../elements/EFMedia/shared/ThumbnailExtractor.js\";\nimport {\n ContentNotReadyError,\n generateThumbnailsFromClone,\n type GeneratedThumbnail,\n type ThumbnailQueue,\n} from \"../../../preview/renderTimegroupToCanvas.js\";\n\nimport { quantizeToFrameTimeMs } from \"../../../utils/frameTime.js\";\nimport { LRUCache } from \"../../../utils/LRUCache.js\";\nimport { TWMixin } from \"../../TWMixin.js\";\nimport { timelineStateContext, type TimelineState } from \"../timelineStateContext.js\";\nimport { previewSettingsContext, type PreviewSettings } from \"../../previewSettingsContext.js\";\n\n/** Maximum number of thumbnails cached per strip instance. */\nconst MAX_THUMBNAIL_CACHE_SIZE = 200;\n\n/** Padding for virtual rendering */\nconst VIRTUAL_RENDER_PADDING_PX = 100;\n\n/**\n * Mutable queue for timestamp generation.\n * Allows updating timestamps while generator is consuming them.\n */\nclass MutableTimestampQueue implements ThumbnailQueue {\n #timestamps: number[] = [];\n\n /** Replace entire queue with new timestamps (sorted) */\n reset(timestamps: number[]): void {\n this.#timestamps = [...timestamps].sort((a, b) => a - b);\n }\n\n /** Keep only these specific timestamps (maintains order) */\n retainOnly(timestamps: number[]): void {\n const keep = new Set(timestamps);\n this.#timestamps = this.#timestamps.filter((t) => keep.has(t));\n }\n\n /** Append timestamps to end (sorted) */\n append(timestamps: number[]): void {\n this.#timestamps.push(...[...timestamps].sort((a, b) => a - b));\n }\n\n /** Get next timestamp (removes from front) */\n shift(): number | undefined {\n return this.#timestamps.shift();\n }\n\n /** Get remaining timestamps without modifying queue */\n remaining(): number[] {\n return [...this.#timestamps];\n }\n\n /** Check if queue is empty */\n isEmpty(): boolean {\n return this.#timestamps.length === 0;\n }\n}\n\n/**\n * Descriptor for a thumbnail to render\n */\ninterface ThumbnailDescriptor {\n timeMs: number;\n x: number;\n width: number;\n height: number;\n}\n\n/**\n * Result of thumbnail rendering (canvas or error)\n */\ninterface ThumbnailResult {\n canvas: CanvasImageSource | null;\n error?: Error;\n}\n\n/**\n * Thumbnail strip component that renders thumbnails for video or timegroup elements.\n *\n * Features:\n * - Targets ef-video or root ef-timegroup via target attribute\n * - Batch video thumbnail extraction via ThumbnailExtractor\n * - Canvas rendering for timegroups at low resolution\n * - Viewport-based lazy loading with scroll calculation\n * - Fixed visual spacing (consistent at all zoom levels)\n * - Error indicators for failed thumbnails\n */\n@customElement(\"ef-thumbnail-strip\")\nexport class EFThumbnailStrip extends TWMixin(LitElement) {\n static styles = [\n css`\n :host {\n display: block;\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n }\n\n .thumbnail-container {\n position: relative;\n width: 100%;\n height: 100%;\n overflow: hidden;\n }\n\n .error-message {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n padding: 8px;\n color: var(--ef-color-error);\n font-size: 12px;\n background: color-mix(in srgb, var(--ef-color-error) 10%, transparent);\n }\n\n canvas {\n position: absolute;\n image-rendering: pixelated;\n image-rendering: crisp-edges;\n }\n\n .shimmer-overlay {\n display: none;\n position: absolute;\n inset: 0;\n background: linear-gradient(\n 90deg,\n color-mix(in srgb, var(--ef-color-text, #fafafa) 12%, transparent) 0%,\n color-mix(in srgb, var(--ef-color-text, #fafafa) 28%, transparent) 50%,\n color-mix(in srgb, var(--ef-color-text, #fafafa) 12%, transparent) 100%\n );\n background-size: 200% 100%;\n pointer-events: none;\n }\n\n .shimmer-overlay.active {\n display: block;\n animation: shimmer-strip var(--ef-loading-shimmer-duration, 1.5s) linear infinite;\n }\n\n @keyframes shimmer-strip {\n 0% { background-position: 200% 0; }\n 100% { background-position: -200% 0; }\n }\n `,\n ];\n\n @property({ type: String })\n target = \"\";\n\n @property({ attribute: false })\n targetElement: Element | null = null;\n\n @property({ type: Number, attribute: \"thumbnail-height\" })\n thumbnailHeight = 24;\n\n @property({ type: Number, attribute: \"thumbnail-spacing-px\" })\n thumbnailSpacingPx = 48;\n\n @property({ type: Number, attribute: \"pixels-per-ms\" })\n pixelsPerMs: number | null = null;\n\n @property({ type: Boolean, attribute: \"use-intrinsic-duration\" })\n useIntrinsicDuration = false;\n\n @consume({ context: timelineStateContext, subscribe: true })\n @state()\n timelineState?: TimelineState;\n\n @consume({ context: previewSettingsContext, subscribe: true })\n @state()\n previewSettings?: PreviewSettings;\n\n @state()\n thumbnailDimensions = { width: 0, height: 0 };\n\n @state()\n _isLoadingThumbnails = false;\n\n #targetController?: TargetController;\n #abortController: AbortController | null = null;\n #renderRequested = false;\n #canvasContainer: Ref<HTMLDivElement> = createRef();\n #lastRequiredTimestamps = \"\";\n #thumbnailCache = new LRUCache<number, CanvasImageSource>(MAX_THUMBNAIL_CACHE_SIZE);\n\n // Timegroup thumbnail generation state\n #timegroupQueue = new MutableTimestampQueue();\n #timegroupClone: {\n clone: EFTimegroup;\n container: HTMLElement;\n cleanup: () => void;\n } | null = null;\n #timegroupGenerator: AsyncGenerator<GeneratedThumbnail> | null = null;\n #timegroupGeneratorAbort: AbortController | null = null;\n #previewContainer: HTMLDivElement | null = null;\n #updateInProgress = false; // Lock to prevent concurrent updates\n #consumerRunning = false; // Lock to prevent concurrent consumers\n #pendingTimestamps = new Set<number>(); // Timestamps requested while update in progress\n #retryScheduled = false; // Flag to prevent duplicate retry schedules\n #thumbnailPhase: number = 0; // Phase offset for thumbnail grid\n #previousPixelsPerMs: number | null = null; // Track zoom changes\n #targetReadyStateHandler: (() => void) | null = null;\n #targetContentChangeHandler: (() => void) | null = null;\n #resizeObserver: ResizeObserver | null = null;\n #hostWidth = 0;\n\n /**\n * Check if target is valid (EFVideo or root EFTimegroup)\n */\n get isValidTarget(): boolean {\n const el = this.targetElement;\n if (!el) return false;\n\n if (el instanceof EFVideo) return true;\n\n if (el instanceof EFTimegroup) {\n // Only root timegroups\n return (el as any).isRootTimegroup === true;\n }\n\n return false;\n }\n\n get #timelineState(): TimelineState | undefined {\n return this.timelineState;\n }\n\n get #thumbnailDimensions() {\n return this.thumbnailDimensions;\n }\n\n get #effectiveDurationMs(): number {\n const element = this.targetElement;\n if (!element) return 0;\n if (this.useIntrinsicDuration) {\n return (element as any).intrinsicDurationMs ?? (element as any).durationMs ?? 0;\n }\n return (element as any).durationMs ?? 0;\n }\n\n get #effectivePixelsPerMs(): number {\n if (this.#timelineState?.pixelsPerMs != null) {\n return this.#timelineState.pixelsPerMs;\n }\n if (this.pixelsPerMs != null) {\n return this.pixelsPerMs;\n }\n const durationMs = this.#effectiveDurationMs;\n if (this.#hostWidth > 0 && durationMs > 0) {\n return this.#hostWidth / durationMs;\n }\n return 0.04;\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n // Only use TargetController if target is set and targetElement is not directly set\n if (this.target && !this.targetElement) {\n this.#targetController = new TargetController(this);\n }\n this.#resizeObserver = new ResizeObserver((entries) => {\n const entry = entries[0];\n if (!entry) return;\n const width = entry.contentRect.width;\n if (width !== this.#hostWidth) {\n this.#hostWidth = width;\n this.requestUpdate();\n this.#scheduleRender();\n }\n });\n this.#resizeObserver.observe(this);\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#abortController?.abort();\n this.#cleanupTimegroupGenerator();\n this.#detachTargetListeners(this.targetElement);\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n }\n\n protected willUpdate(changedProperties: Map<string | number | symbol, unknown>): void {\n super.willUpdate(changedProperties);\n\n // Create TargetController if target is set and targetElement is not directly set\n if (changedProperties.has(\"target\")) {\n if (this.target && !this.targetElement && !this.#targetController) {\n this.#targetController = new TargetController(this);\n }\n }\n\n // Recalculate thumbnail dimensions if target changed\n if (changedProperties.has(\"targetElement\") || changedProperties.has(\"thumbnailHeight\")) {\n this.thumbnailDimensions = this.#calculateThumbnailDimensions();\n }\n\n // Manage event listeners when target changes\n if (changedProperties.has(\"targetElement\")) {\n const oldTarget = changedProperties.get(\"targetElement\") as Element | null;\n this.#detachTargetListeners(oldTarget);\n this.#attachTargetListeners(this.targetElement);\n // Reset so the new target always starts a fresh extraction\n this.#lastRequiredTimestamps = \"\";\n }\n }\n\n #attachTargetListeners(target: Element | null): void {\n if (!target) return;\n\n this.#targetReadyStateHandler = () => {\n this.requestUpdate();\n this.#scheduleRender();\n };\n this.#targetContentChangeHandler = () => {\n this.requestUpdate();\n this.#scheduleRender();\n };\n target.addEventListener(\"readystatechange\", this.#targetReadyStateHandler);\n target.addEventListener(\"contentchange\", this.#targetContentChangeHandler);\n\n // Late-subscriber: if the target already transitioned to \"ready\" before\n // we attached, the event was missed. The contentReadyState property\n // serves exactly this purpose — check it and render if needed.\n if ((target as any).contentReadyState === \"ready\") {\n this.requestUpdate();\n this.#scheduleRender();\n }\n }\n\n #detachTargetListeners(target: Element | null): void {\n if (!target) return;\n if (this.#targetReadyStateHandler) {\n target.removeEventListener(\"readystatechange\", this.#targetReadyStateHandler);\n this.#targetReadyStateHandler = null;\n }\n if (this.#targetContentChangeHandler) {\n target.removeEventListener(\"contentchange\", this.#targetContentChangeHandler);\n this.#targetContentChangeHandler = null;\n }\n }\n\n updated(changedProperties: Map<string | number | symbol, unknown>): void {\n super.updated(changedProperties);\n\n if (\n changedProperties.has(\"targetElement\") ||\n changedProperties.has(\"thumbnailSpacingPx\") ||\n changedProperties.has(\"pixelsPerMs\") ||\n changedProperties.has(\"thumbnailHeight\") ||\n changedProperties.has(\"timelineState\")\n ) {\n this.#scheduleRender();\n }\n }\n\n /**\n * Calculate thumbnail dimensions from target element's actual bounds\n */\n #calculateThumbnailDimensions(): { width: number; height: number } {\n const el = this.targetElement;\n if (!el) return { width: 0, height: 0 };\n\n // Get actual visible bounds from DOM\n const bounds = el.getBoundingClientRect();\n if (bounds.width === 0 || bounds.height === 0) {\n // Element not yet rendered or no size, use default aspect ratio\n return {\n width: this.thumbnailHeight * (16 / 9),\n height: this.thumbnailHeight,\n };\n }\n\n const aspectRatio = bounds.width / bounds.height;\n const width = Math.round(this.thumbnailHeight * aspectRatio);\n\n return { width, height: this.thumbnailHeight };\n }\n\n /**\n * Calculate visible thumbnails based on viewport\n */\n #calculateVisibleThumbnails(): ThumbnailDescriptor[] {\n if (!this.isValidTarget) return [];\n\n const element = this.targetElement;\n if (!element) return [];\n\n const scrollLeft = this.#timelineState?.viewportScrollLeft ?? 0;\n const viewportWidth = this.#timelineState?.viewportWidth ?? (this.#hostWidth || 800);\n const pixelsPerMs = this.#effectivePixelsPerMs;\n\n const durationMs = this.#effectiveDurationMs;\n if (durationMs === 0) return [];\n\n const trackWidthPx = durationMs * pixelsPerMs;\n\n // Get FPS for quantization\n const fps = (element as any).fps ?? 30;\n\n const visibleStartPx = scrollLeft - VIRTUAL_RENDER_PADDING_PX;\n const visibleEndPx = scrollLeft + viewportWidth + VIRTUAL_RENDER_PADDING_PX;\n\n const thumbnails: ThumbnailDescriptor[] = [];\n const { width, height } = this.#thumbnailDimensions;\n\n // Read minimum gap from CSS variable (--ef-thumbnail-gap, default 2px)\n const gapPx = parseFloat(getComputedStyle(this).getPropertyValue(\"--ef-thumbnail-gap\")) || 2;\n // Stride must be at least thumbnail width + gap to prevent overlap\n const thumbnailStride = Math.max(this.thumbnailSpacingPx, width + gapPx);\n\n // Detect zoom by checking if pixelsPerMs changed\n const isZoom = this.#previousPixelsPerMs !== null && this.#previousPixelsPerMs !== pixelsPerMs;\n\n if (this.#previousPixelsPerMs === null) {\n // First render: align grid to track start (t=0)\n this.#thumbnailPhase = 0;\n } else if (isZoom) {\n // On zoom: snap a thumbnail to near the left edge of viewport\n // This prevents visual slip during zoom operations\n this.#thumbnailPhase = scrollLeft % thumbnailStride;\n } else if (scrollLeft < thumbnailStride) {\n // When scrolled near the start, realign to t=0 to avoid left gap\n this.#thumbnailPhase = 0;\n }\n // During normal scroll: phase unchanged, grid scrolls naturally with track\n\n this.#previousPixelsPerMs = pixelsPerMs;\n\n // Generate thumbnail grid anchored at phase offset\n // Each thumbnail is at absolute track position: phase + (i * stride)\n // This means grid is stable in track space (scrolls naturally)\n const startIndex = Math.max(\n 0,\n Math.floor((visibleStartPx - this.#thumbnailPhase) / thumbnailStride),\n );\n const endIndex = Math.ceil((visibleEndPx - this.#thumbnailPhase) / thumbnailStride);\n\n for (let i = startIndex; i <= endIndex; i++) {\n const thumbX = this.#thumbnailPhase + i * thumbnailStride;\n\n // Only include if within track bounds\n if (thumbX >= 0 && thumbX < trackWidthPx) {\n // Convert position to time (leading edge)\n const rawTimeMs = thumbX / pixelsPerMs;\n const timeMs = quantizeToFrameTimeMs(rawTimeMs, fps);\n\n if (timeMs >= 0 && timeMs < durationMs) {\n thumbnails.push({ timeMs, x: thumbX, width, height });\n }\n }\n }\n\n return thumbnails;\n }\n\n /**\n * Schedule thumbnail render on next frame\n */\n #scheduleRender(): void {\n if (this.#renderRequested) return;\n this.#renderRequested = true;\n\n requestAnimationFrame(() => {\n this.#renderRequested = false;\n this.#renderThumbnails();\n });\n }\n\n /**\n * Render thumbnails with cancellation support\n */\n async #renderThumbnails(): Promise<void> {\n const visibleThumbnails = this.#calculateVisibleThumbnails();\n if (visibleThumbnails.length === 0) {\n this.#abortController?.abort();\n this.#clearCanvas();\n return;\n }\n\n // Only cancel and restart extraction when the required timestamps change\n // (i.e. the viewport scrolled or zoomed to show different thumbnails).\n // Firing #scheduleRender for other reasons (readystatechange, Lit context\n // updates, etc.) with the same visible timestamps must NOT abort an\n // in-progress extraction — doing so leaves no replacement running and\n // thumbnails never load.\n const requiredTimestamps = visibleThumbnails.map((t) => t.timeMs);\n const timestampsString = requiredTimestamps.join(\", \");\n if (timestampsString !== this.#lastRequiredTimestamps) {\n this.#abortController?.abort();\n this.#abortController = new AbortController();\n this.#lastRequiredTimestamps = timestampsString;\n\n if (this.targetElement instanceof EFVideo) {\n this.#updateVideoCapture(requiredTimestamps, this.#abortController.signal).catch(\n (error) => {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n return;\n }\n // Non-abort failure: reset so the next render retries\n this.#lastRequiredTimestamps = \"\";\n console.error(\"Thumbnail capture error:\", error);\n },\n );\n } else if (this.targetElement instanceof EFTimegroup) {\n this.#updateTimegroupCapture(requiredTimestamps);\n }\n }\n\n const signal = this.#abortController?.signal;\n if (signal?.aborted) return;\n\n // Draw thumbnails - use nearest neighbor if exact timestamp not cached\n const maxNeighborDistanceMs = 3000; // Don't use thumbnails more than 3s away\n const results: ThumbnailResult[] = visibleThumbnails.map((t) => {\n let canvas = this.#thumbnailCache.get(t.timeMs);\n\n // If exact match not found, find nearest cached thumbnail\n if (!canvas) {\n let nearestTimeMs: number | null = null;\n let minDistance = Infinity;\n\n for (const cachedTimeMs of this.#thumbnailCache.keys()) {\n const distance = Math.abs(cachedTimeMs - t.timeMs);\n if (distance < minDistance && distance <= maxNeighborDistanceMs) {\n minDistance = distance;\n nearestTimeMs = cachedTimeMs;\n }\n }\n\n if (nearestTimeMs !== null) {\n canvas = this.#thumbnailCache.get(nearestTimeMs);\n }\n }\n\n return { canvas: canvas ?? null };\n });\n this.#drawThumbnails(visibleThumbnails, results);\n\n const hasEmptySlots = results.some((r) => r.canvas === null);\n if (this._isLoadingThumbnails !== hasEmptySlots) {\n this._isLoadingThumbnails = hasEmptySlots;\n }\n }\n\n /**\n * Update video thumbnail capture\n */\n async #updateVideoCapture(timestamps: number[], signal: AbortSignal): Promise<void> {\n const video = this.targetElement as EFVideo;\n if (!video) return;\n\n // Filter out cached timestamps\n const uncached = timestamps.filter((t) => !this.#thumbnailCache.has(t));\n if (uncached.length === 0) return;\n\n const mediaEngineTask = video.mediaEngineTask;\n if (!mediaEngineTask) return;\n\n const mediaEngine = await mediaEngineTask.taskComplete;\n if (!mediaEngine) return;\n\n const sourceTimestamps = uncached.map((t) => this.#getSourceTimeMs(t));\n\n const extractor = new ThumbnailExtractor(mediaEngine);\n const videoTrack = mediaEngine.tracks.scrub ?? mediaEngine.tracks.video;\n if (!videoTrack) return;\n\n const results = await extractor.extractThumbnails(\n sourceTimestamps,\n videoTrack,\n video.durationMs ?? 0,\n signal,\n );\n\n // Store in cache and trigger redraw\n for (let i = 0; i < uncached.length; i++) {\n const thumbnail = results[i]?.thumbnail;\n const timestamp = uncached[i];\n if (thumbnail && timestamp !== undefined) {\n this.#thumbnailCache.set(timestamp, thumbnail);\n }\n }\n\n this.#scheduleRender();\n }\n\n /**\n * Update timegroup thumbnail capture using mutable queue\n */\n async #updateTimegroupCapture(timestamps: number[]): Promise<void> {\n const timegroup = this.targetElement as EFTimegroup;\n if (!timegroup) return;\n\n // Filter out cached timestamps\n const uncached = timestamps.filter((t) => !this.#thumbnailCache.has(t)).sort((a, b) => a - b);\n if (uncached.length === 0) {\n return;\n }\n\n // CRITICAL: If update already in progress, REPLACE pending (not add)\n // We only want the LATEST required timestamps, not a union of all previous ones\n if (this.#updateInProgress) {\n // Clear old pending and replace with latest\n this.#pendingTimestamps.clear();\n uncached.forEach((t) => this.#pendingTimestamps.add(t));\n\n // Schedule a retry (debounced via RAF)\n if (!this.#retryScheduled) {\n this.#retryScheduled = true;\n requestAnimationFrame(() => {\n this.#retryScheduled = false;\n if (this.#pendingTimestamps.size > 0) {\n const pending = Array.from(this.#pendingTimestamps);\n this.#pendingTimestamps.clear();\n this.#updateTimegroupCapture(pending);\n }\n });\n }\n return;\n }\n this.#updateInProgress = true;\n\n try {\n if (this.#timegroupGenerator) {\n // Generator is running - abort and reset queue to exactly what we need now\n // Abort in-flight capture\n this.#timegroupGeneratorAbort?.abort();\n\n // Create new abort controller for the updated queue\n this.#timegroupGeneratorAbort = new AbortController();\n\n // Reset queue to exactly what we need now\n this.#timegroupQueue.reset(uncached);\n } else if (this.#timegroupClone) {\n // Generator finished, restart with existing clone\n this.#timegroupQueue.reset(uncached);\n\n // Create new abort controller\n this.#timegroupGeneratorAbort = new AbortController();\n\n this.#timegroupGenerator = generateThumbnailsFromClone(\n this.#timegroupClone.clone,\n this.#previewContainer!,\n this.#timegroupQueue,\n {\n scale: 0.25,\n contentReadyMode: \"blocking\",\n blockingTimeoutMs: 1000,\n signal: this.#timegroupGeneratorAbort.signal,\n },\n );\n await this.#consumeTimegroupGenerator();\n } else {\n // No generator or clone, start fresh\n await this.#startTimegroupGenerator(timegroup, uncached);\n }\n } finally {\n this.#updateInProgress = false;\n\n // Check if there are pending timestamps that need processing\n // This happens when updates were skipped while this update was in progress\n if (this.#pendingTimestamps.size > 0 && !this.#retryScheduled) {\n this.#retryScheduled = true;\n requestAnimationFrame(() => {\n this.#retryScheduled = false;\n if (this.#pendingTimestamps.size > 0) {\n const pending = Array.from(this.#pendingTimestamps);\n this.#pendingTimestamps.clear();\n this.#updateTimegroupCapture(pending);\n }\n });\n }\n }\n }\n\n /**\n * Start timegroup thumbnail generator\n */\n async #startTimegroupGenerator(timegroup: EFTimegroup, timestamps: number[]): Promise<void> {\n // Create render clone\n this.#timegroupClone = await timegroup.createRenderClone();\n\n // Use the original container from createRenderClone (already configured)\n this.#previewContainer = this.#timegroupClone.container as HTMLDivElement;\n\n // CRITICAL: Wait for Lit to process shadow DOM updates after moving to new container\n await this.#timegroupClone.clone.updateComplete;\n\n // Also wait for all nested Lit elements to update\n const litElements = this.#previewContainer.querySelectorAll(\"*\");\n const updatePromises: Promise<any>[] = [];\n for (const el of litElements) {\n if (\"updateComplete\" in el) {\n updatePromises.push((el as any).updateComplete);\n }\n }\n await Promise.all(updatePromises);\n\n // Wait AGAIN specifically for text segments (they may need to re-render after move)\n const textSegments = this.#previewContainer.querySelectorAll(\"ef-text-segment\");\n const textUpdatePromises: Promise<any>[] = [];\n for (const seg of textSegments) {\n if (\"updateComplete\" in seg) {\n textUpdatePromises.push((seg as any).updateComplete);\n }\n }\n await Promise.all(textUpdatePromises);\n\n // CRITICAL: Wait for ef-text to split text into segments\n // EFText.connectedCallback schedules splitText in requestAnimationFrame\n // We must wait for that RAF to fire before capturing\n await new Promise((resolve) => requestAnimationFrame(resolve));\n\n // WARMUP: Do a seek to the first timestamp to \"prime\" the clone\n // Guard: clone may have been disposed during prior awaits\n if (!this.#timegroupClone) return;\n if (timestamps.length > 0) {\n await this.#timegroupClone.clone.seekForRender(timestamps[0]!);\n }\n\n // CRITICAL: Wait for fonts to load\n // Text won't render correctly if fonts aren't ready\n await document.fonts.ready;\n\n // CRITICAL: Wait for all images to load\n const images = this.#previewContainer.querySelectorAll(\"img\");\n const imagePromises: Promise<void>[] = [];\n for (const img of images) {\n if (!img.complete) {\n imagePromises.push(\n new Promise((resolve, _reject) => {\n img.onload = () => resolve();\n img.onerror = () => resolve(); // Don't block on errors\n // Timeout after 5s\n setTimeout(() => resolve(), 5000);\n }),\n );\n }\n }\n await Promise.all(imagePromises);\n\n // Guard: clone may have been disposed during prior awaits\n if (!this.#timegroupClone) return;\n\n // Initialize queue\n this.#timegroupQueue.reset(timestamps);\n\n // Create abort controller for this generator\n this.#timegroupGeneratorAbort = new AbortController();\n\n // Start generator using the fresh container\n this.#timegroupGenerator = generateThumbnailsFromClone(\n this.#timegroupClone.clone,\n this.#previewContainer,\n this.#timegroupQueue,\n {\n scale: 0.25,\n contentReadyMode: \"blocking\",\n blockingTimeoutMs: 1000,\n signal: this.#timegroupGeneratorAbort.signal,\n },\n );\n\n // Consume generator (CRITICAL: Must await to prevent concurrent consumers)\n await this.#consumeTimegroupGenerator();\n }\n\n /**\n * Consume generator and handle cleanup\n */\n async #consumeTimegroupGenerator(): Promise<void> {\n // CRITICAL: Prevent concurrent consumers\n if (this.#consumerRunning) {\n return;\n }\n this.#consumerRunning = true;\n\n if (!this.#timegroupGenerator) {\n this.#consumerRunning = false;\n return;\n }\n\n try {\n for await (const { timeMs, canvas } of this.#timegroupGenerator) {\n this.#thumbnailCache.set(timeMs, canvas);\n this.#scheduleRender();\n }\n } catch (err) {\n if (err instanceof ContentNotReadyError) {\n // Media wasn't ready in time. Reset so the next render cycle retries\n // the uncached timestamps rather than treating this batch as complete.\n this.#lastRequiredTimestamps = \"\";\n }\n console.warn(\"Timegroup thumbnail generation error:\", err);\n } finally {\n // Generator finished, but keep clone alive for reuse\n this.#timegroupGenerator = null;\n this.#consumerRunning = false;\n }\n }\n\n /**\n * Cleanup timegroup generator and clone\n */\n #cleanupTimegroupGenerator(): void {\n // Abort any in-flight work\n this.#timegroupGeneratorAbort?.abort();\n this.#timegroupGeneratorAbort = null;\n\n this.#timegroupGenerator = null;\n\n // Remove preview container from DOM\n if (this.#previewContainer) {\n this.#previewContainer.remove();\n this.#previewContainer = null;\n }\n\n // Cleanup render clone\n if (this.#timegroupClone) {\n this.#timegroupClone.cleanup();\n this.#timegroupClone = null;\n }\n }\n\n /**\n * Clear all canvas elements\n */\n #clearCanvas(): void {\n const container = this.#canvasContainer.value;\n if (container) {\n container.innerHTML = \"\";\n }\n }\n\n /**\n * Translate composition time to source time for videos (handles trim)\n */\n #getSourceTimeMs(compositionTimeMs: number): number {\n if (this.useIntrinsicDuration) {\n return compositionTimeMs;\n }\n const el = this.targetElement;\n if (el instanceof EFVideo) {\n return compositionTimeMs + (el.sourceStartMs ?? 0);\n }\n return compositionTimeMs;\n }\n\n /**\n * Draw thumbnails to canvas elements\n */\n #drawThumbnails(thumbnails: ThumbnailDescriptor[], results: ThumbnailResult[]): void {\n const container = this.#canvasContainer.value;\n if (!container) return;\n\n // Clear existing canvases\n container.innerHTML = \"\";\n\n for (let i = 0; i < thumbnails.length; i++) {\n const thumbnail = thumbnails[i];\n const result = results[i];\n\n if (!thumbnail) continue;\n\n const canvas = document.createElement(\"canvas\");\n canvas.width = thumbnail.width;\n canvas.height = thumbnail.height;\n canvas.style.left = `${thumbnail.x}px`;\n canvas.style.top = \"0\";\n canvas.style.width = `${thumbnail.width}px`;\n canvas.style.height = `${thumbnail.height}px`;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) continue;\n\n if (result?.canvas) {\n // Draw actual thumbnail\n ctx.drawImage(result.canvas, 0, 0, thumbnail.width, thumbnail.height);\n\n // Draw timestamp overlay if enabled\n if (this.previewSettings?.showThumbnailTimestamps) {\n ctx.fillStyle = \"rgba(0, 0, 0, 0.8)\";\n ctx.fillRect(2, 2, 95, 16);\n ctx.fillStyle = \"yellow\";\n ctx.font = \"11px monospace\";\n ctx.textAlign = \"left\";\n ctx.textBaseline = \"top\";\n ctx.fillText(`${Math.round(thumbnail.timeMs)}ms`, 5, 4);\n }\n } else {\n // Draw placeholder with timestamp text\n const bgColor =\n getComputedStyle(this).getPropertyValue(\"--ef-color-bg-inset\").trim() ||\n \"rgba(100, 100, 100, 0.3)\";\n ctx.fillStyle = bgColor;\n ctx.fillRect(0, 0, thumbnail.width, thumbnail.height);\n\n const borderColor =\n getComputedStyle(this).getPropertyValue(\"--ef-color-border-subtle\").trim() ||\n \"rgba(150, 150, 150, 0.5)\";\n ctx.strokeStyle = borderColor;\n ctx.lineWidth = 1;\n ctx.strokeRect(0, 0, thumbnail.width, thumbnail.height);\n\n ctx.fillStyle = \"white\";\n ctx.font = \"10px monospace\";\n ctx.textAlign = \"center\";\n ctx.textBaseline = \"middle\";\n ctx.fillText(\n `${Math.round(thumbnail.timeMs)}ms`,\n thumbnail.width / 2,\n thumbnail.height / 2,\n );\n }\n\n container.appendChild(canvas);\n }\n }\n\n render() {\n // Error: No target specified (neither target string nor targetElement)\n if (!this.target && !this.targetElement) {\n return html`<div class=\"error-message\">No target specified</div>`;\n }\n\n // Error: Target element not found (when using target string)\n if (this.target && !this.targetElement) {\n return html`<div class=\"error-message\">\n Target element \"${this.target}\" not found\n </div>`;\n }\n\n // Error: Invalid target type\n if (!this.isValidTarget) {\n const elementType = (this.targetElement as any).tagName?.toLowerCase() || \"unknown\";\n return html`<div class=\"error-message\">\n Invalid target: \"${elementType}\" must be ef-video or root ef-timegroup\n </div>`;\n }\n\n // Calculate track width to clip thumbnails at track end\n const durationMs = this.#effectiveDurationMs;\n const pixelsPerMs = this.#effectivePixelsPerMs;\n const trackWidthPx = durationMs * pixelsPerMs;\n\n // Render canvas container with explicit width clipping, plus shimmer overlay\n return html`\n <div\n class=\"thumbnail-container\"\n style=\"max-width: ${trackWidthPx}px;\"\n ${ref(this.#canvasContainer)}\n ></div>\n <div\n class=\"shimmer-overlay ${this._isLoadingThumbnails ? \"active\" : \"\"}\"\n ></div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-thumbnail-strip\": EFThumbnailStrip;\n }\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { version } from "./version.js";
|
|
|
27
27
|
import { CloneFactory, CloneFactoryResult, getCloneFactory, registerCloneFactory, unregisterCloneFactory } from "./elements/cloneFactoryRegistry.js";
|
|
28
28
|
import { elementNeedsFitScale, needsFitScale } from "./gui/FitScaleHelpers.js";
|
|
29
29
|
import { EFImage } from "./elements/EFImage.js";
|
|
30
|
+
import { EFMotionBlur } from "./elements/EFMotionBlur.js";
|
|
30
31
|
import { EFTextSegment } from "./elements/EFTextSegment.js";
|
|
31
32
|
import { EFText } from "./elements/EFText.js";
|
|
32
33
|
import { EFWaveform } from "./elements/EFWaveform.js";
|
|
@@ -58,4 +59,4 @@ import "./render/EFRenderAPI.js";
|
|
|
58
59
|
import { getRenderData } from "./render/getRenderData.js";
|
|
59
60
|
import { CanvasPreviewOptions, CanvasPreviewResult, CaptureOptions, ContentReadyMode } from "./preview/renderTimegroupToCanvas.types.js";
|
|
60
61
|
import { RenderElementOptions, renderElementToCanvas } from "./preview/renderElementToCanvas.js";
|
|
61
|
-
export { type BoxBounds, CanvasAPI, type CanvasElementBounds, type CanvasElementData, type CanvasPreviewOptions, type CanvasPreviewResult, type CaptureOptions, type CloneFactory, type CloneFactoryResult, type ContainerInfo, type ContentReadyMode, type DialChangeDetail, EFActiveRootTemporal, EFAudio, EFAudioHierarchyItem, EFCanvas, EFCanvasItem, EFCaptions, EFCaptionsActiveWord, EFCaptionsActiveWordHierarchyItem, EFCaptionsAfterActiveWord, EFCaptionsBeforeActiveWord, EFCaptionsHierarchyItem, EFCaptionsSegment, EFConfiguration, EFControls, EFDial, EFFilmstrip, EFFitScale, EFFocusOverlay, EFHTMLHierarchyItem, EFHierarchy, EFHierarchyItem, EFImage, EFImageHierarchyItem, type EFMedia, EFOverlayItem, EFOverlayLayer, EFPanZoom, EFPause, EFPlay, EFPreview, EFResizableBox, EFScrubber, EFSurface, EFText, EFTextHierarchyItem, EFTextSegment, EFTextSegmentHierarchyItem, EFThumbnailStrip, EFTimeDisplay, EFTimegroup, EFTimegroupHierarchyItem, EFTimeline, EFTimelineRuler, EFToggleLoop, EFTogglePlay, EFTransformHandles, EFTree, EFTreeItem, EFTrimHandles, EFVideo, EFVideoHierarchyItem, EFWaveform, EFWaveformHierarchyItem, EFWorkbench, type ElementPositionInfo, type HierarchyActions, type HierarchyContext, type HierarchyState, type OverlayItemPosition, type PanZoomTransform, PositionInfoMixin, type RenderElementOptions, type RenderInfo, RenderInfoSchema, type RenderProgress, type RenderToVideoOptions, type ScaleInput, type ScaleOutput, SelectionModel, type SelectionState, type TemporalMixinInterface, type TraceContext, type TransformBounds, type TreeActions, type TreeContext, type TreeItem, type TreeState, type TrimChangeDetail, type TrimValue, calculateFrameIntervalMs, calculatePixelsPerFrame, collectAllIds, computeFitScale, elementNeedsFitScale, getCloneFactory, getContainerInfoFromElement, getCornerPoint, getOppositeCorner, getPositionInfoFromElement, getRenderData, getRenderInfo, hierarchyContext, isEFTemporal, needsFitScale, quantizeToFrameTimeMs, registerCloneFactory, renderElementToCanvas, rotatePoint, shouldShowFrameMarkers, treeContext, unregisterCloneFactory, version };
|
|
62
|
+
export { type BoxBounds, CanvasAPI, type CanvasElementBounds, type CanvasElementData, type CanvasPreviewOptions, type CanvasPreviewResult, type CaptureOptions, type CloneFactory, type CloneFactoryResult, type ContainerInfo, type ContentReadyMode, type DialChangeDetail, EFActiveRootTemporal, EFAudio, EFAudioHierarchyItem, EFCanvas, EFCanvasItem, EFCaptions, EFCaptionsActiveWord, EFCaptionsActiveWordHierarchyItem, EFCaptionsAfterActiveWord, EFCaptionsBeforeActiveWord, EFCaptionsHierarchyItem, EFCaptionsSegment, EFConfiguration, EFControls, EFDial, EFFilmstrip, EFFitScale, EFFocusOverlay, EFHTMLHierarchyItem, EFHierarchy, EFHierarchyItem, EFImage, EFImageHierarchyItem, type EFMedia, EFMotionBlur, EFOverlayItem, EFOverlayLayer, EFPanZoom, EFPause, EFPlay, EFPreview, EFResizableBox, EFScrubber, EFSurface, EFText, EFTextHierarchyItem, EFTextSegment, EFTextSegmentHierarchyItem, EFThumbnailStrip, EFTimeDisplay, EFTimegroup, EFTimegroupHierarchyItem, EFTimeline, EFTimelineRuler, EFToggleLoop, EFTogglePlay, EFTransformHandles, EFTree, EFTreeItem, EFTrimHandles, EFVideo, EFVideoHierarchyItem, EFWaveform, EFWaveformHierarchyItem, EFWorkbench, type ElementPositionInfo, type HierarchyActions, type HierarchyContext, type HierarchyState, type OverlayItemPosition, type PanZoomTransform, PositionInfoMixin, type RenderElementOptions, type RenderInfo, RenderInfoSchema, type RenderProgress, type RenderToVideoOptions, type ScaleInput, type ScaleOutput, SelectionModel, type SelectionState, type TemporalMixinInterface, type TraceContext, type TransformBounds, type TreeActions, type TreeContext, type TreeItem, type TreeState, type TrimChangeDetail, type TrimValue, calculateFrameIntervalMs, calculatePixelsPerFrame, collectAllIds, computeFitScale, elementNeedsFitScale, getCloneFactory, getContainerInfoFromElement, getCornerPoint, getOppositeCorner, getPositionInfoFromElement, getRenderData, getRenderInfo, hierarchyContext, isEFTemporal, needsFitScale, quantizeToFrameTimeMs, registerCloneFactory, renderElementToCanvas, rotatePoint, shouldShowFrameMarkers, treeContext, unregisterCloneFactory, version };
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import { EFThumbnailStrip } from "./gui/timeline/tracks/EFThumbnailStrip.js";
|
|
|
24
24
|
import { EFTimelineRuler, calculateFrameIntervalMs, calculatePixelsPerFrame, quantizeToFrameTimeMs, shouldShowFrameMarkers } from "./gui/EFTimelineRuler.js";
|
|
25
25
|
import { EFTimeline } from "./gui/timeline/EFTimeline.js";
|
|
26
26
|
import { EFFilmstrip } from "./gui/EFFilmstrip.js";
|
|
27
|
+
import { EFMotionBlur } from "./elements/EFMotionBlur.js";
|
|
27
28
|
import { EFFitScale, computeFitScale } from "./gui/EFFitScale.js";
|
|
28
29
|
import { EFWorkbench } from "./gui/EFWorkbench.js";
|
|
29
30
|
import { EFPanZoom } from "./elements/EFPanZoom.js";
|
|
@@ -59,5 +60,5 @@ import { renderElementToCanvas } from "./preview/renderElementToCanvas.js";
|
|
|
59
60
|
if (typeof window !== "undefined") window.EF_REGISTERED = true;
|
|
60
61
|
|
|
61
62
|
//#endregion
|
|
62
|
-
export { CanvasAPI, EFActiveRootTemporal, EFAudio, EFAudioHierarchyItem, EFCanvas, EFCanvasItem, EFCaptions, EFCaptionsActiveWord, EFCaptionsActiveWordHierarchyItem, EFCaptionsAfterActiveWord, EFCaptionsBeforeActiveWord, EFCaptionsHierarchyItem, EFCaptionsSegment, EFConfiguration, EFControls, EFDial, EFFilmstrip, EFFitScale, EFFocusOverlay, EFHTMLHierarchyItem, EFHierarchy, EFHierarchyItem, EFImage, EFImageHierarchyItem, EFOverlayItem, EFOverlayLayer, EFPanZoom, EFPause, EFPlay, EFPreview, EFResizableBox, EFScrubber, EFSurface, EFText, EFTextHierarchyItem, EFTextSegment, EFTextSegmentHierarchyItem, EFThumbnailStrip, EFTimeDisplay, EFTimegroup, EFTimegroupHierarchyItem, EFTimeline, EFTimelineRuler, EFToggleLoop, EFTogglePlay, EFTransformHandles, EFTree, EFTreeItem, EFTrimHandles, EFVideo, EFVideoHierarchyItem, EFWaveform, EFWaveformHierarchyItem, EFWorkbench, PositionInfoMixin, RenderInfoSchema, SelectionModel, calculateFrameIntervalMs, calculatePixelsPerFrame, collectAllIds, computeFitScale, elementNeedsFitScale, getCloneFactory, getContainerInfoFromElement, getCornerPoint, getOppositeCorner, getPositionInfoFromElement, getRenderData, getRenderInfo, hierarchyContext, isEFTemporal, needsFitScale, quantizeToFrameTimeMs, registerCloneFactory, renderElementToCanvas, rotatePoint, shouldShowFrameMarkers, treeContext, unregisterCloneFactory, version };
|
|
63
|
+
export { CanvasAPI, EFActiveRootTemporal, EFAudio, EFAudioHierarchyItem, EFCanvas, EFCanvasItem, EFCaptions, EFCaptionsActiveWord, EFCaptionsActiveWordHierarchyItem, EFCaptionsAfterActiveWord, EFCaptionsBeforeActiveWord, EFCaptionsHierarchyItem, EFCaptionsSegment, EFConfiguration, EFControls, EFDial, EFFilmstrip, EFFitScale, EFFocusOverlay, EFHTMLHierarchyItem, EFHierarchy, EFHierarchyItem, EFImage, EFImageHierarchyItem, EFMotionBlur, EFOverlayItem, EFOverlayLayer, EFPanZoom, EFPause, EFPlay, EFPreview, EFResizableBox, EFScrubber, EFSurface, EFText, EFTextHierarchyItem, EFTextSegment, EFTextSegmentHierarchyItem, EFThumbnailStrip, EFTimeDisplay, EFTimegroup, EFTimegroupHierarchyItem, EFTimeline, EFTimelineRuler, EFToggleLoop, EFTogglePlay, EFTransformHandles, EFTree, EFTreeItem, EFTrimHandles, EFVideo, EFVideoHierarchyItem, EFWaveform, EFWaveformHierarchyItem, EFWorkbench, PositionInfoMixin, RenderInfoSchema, SelectionModel, calculateFrameIntervalMs, calculatePixelsPerFrame, collectAllIds, computeFitScale, elementNeedsFitScale, getCloneFactory, getContainerInfoFromElement, getCornerPoint, getOppositeCorner, getPositionInfoFromElement, getRenderData, getRenderInfo, hierarchyContext, isEFTemporal, needsFitScale, quantizeToFrameTimeMs, registerCloneFactory, renderElementToCanvas, rotatePoint, shouldShowFrameMarkers, treeContext, unregisterCloneFactory, version };
|
|
63
64
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import \"./elements/EFTimegroup.js\";\nimport \"./version.js\";\n\nexport { version } from \"./version.js\";\nexport { EFTimegroup } from \"./elements/EFTimegroup.js\";\nexport {\n registerCloneFactory,\n unregisterCloneFactory,\n getCloneFactory,\n type CloneFactory,\n type CloneFactoryResult,\n} from \"./elements/cloneFactoryRegistry.js\";\nexport type { ContainerInfo } from \"./elements/ContainerInfo.js\";\nexport { getContainerInfoFromElement } from \"./elements/ContainerInfo.js\";\nexport type { ElementPositionInfo } from \"./elements/ElementPositionInfo.js\";\nexport { getPositionInfoFromElement, PositionInfoMixin } from \"./elements/ElementPositionInfo.js\";\nexport { needsFitScale, elementNeedsFitScale } from \"./gui/FitScaleHelpers.js\";\n\nimport \"./elements/EFImage.js\";\n\nexport { EFImage } from \"./elements/EFImage.js\";\n\nimport \"./elements/EFMedia.js\";\n\nexport type { EFMedia } from \"./elements/EFMedia.js\";\n\nimport \"./elements/EFAudio.js\";\n\nexport { EFAudio } from \"./elements/EFAudio.js\";\n\nimport \"./elements/EFVideo.js\";\n\nexport { EFVideo } from \"./elements/EFVideo.js\";\n\nimport \"./elements/EFCaptions.js\";\n\nexport {\n EFCaptions,\n EFCaptionsActiveWord,\n EFCaptionsAfterActiveWord,\n EFCaptionsBeforeActiveWord,\n EFCaptionsSegment,\n} from \"./elements/EFCaptions.js\";\n\nimport \"./elements/EFText.js\";\nimport \"./elements/EFTextSegment.js\";\n\nexport { EFText } from \"./elements/EFText.js\";\nexport { EFTextSegment } from \"./elements/EFTextSegment.js\";\n\nimport \"./elements/EFWaveform.js\";\n\nexport { EFWaveform } from \"./elements/EFWaveform.js\";\n\nimport \"./elements/EFTemporal.js\";\n\nexport { isEFTemporal } from \"./elements/EFTemporal.js\";\nexport type { TemporalMixinInterface } from \"./elements/EFTemporal.js\";\n\nimport \"./gui/EFConfiguration.ts\";\n\nexport { EFConfiguration } from \"./gui/EFConfiguration.ts\";\n\nimport \"./gui/EFWorkbench.js\";\n\nexport { EFWorkbench } from \"./gui/EFWorkbench.js\";\n\nimport \"./gui/EFPreview.js\";\n\nexport { EFPreview } from \"./gui/EFPreview.js\";\n\nimport \"./gui/EFFilmstrip.js\";\n\nexport { EFFilmstrip } from \"./gui/EFFilmstrip.js\";\n\nimport \"./gui/hierarchy/EFHierarchy.js\";\nimport \"./gui/hierarchy/EFHierarchyItem.js\";\n\nexport { EFHierarchy } from \"./gui/hierarchy/EFHierarchy.js\";\nexport {\n EFHierarchyItem,\n EFTimegroupHierarchyItem,\n EFAudioHierarchyItem,\n EFVideoHierarchyItem,\n EFCaptionsHierarchyItem,\n EFCaptionsActiveWordHierarchyItem,\n EFTextHierarchyItem,\n EFTextSegmentHierarchyItem,\n EFWaveformHierarchyItem,\n EFImageHierarchyItem,\n EFHTMLHierarchyItem,\n} from \"./gui/hierarchy/EFHierarchyItem.js\";\nexport type {\n HierarchyState,\n HierarchyActions,\n HierarchyContext,\n} from \"./gui/hierarchy/hierarchyContext.js\";\nexport { hierarchyContext } from \"./gui/hierarchy/hierarchyContext.js\";\n\n// Generic tree component\nimport \"./gui/tree/EFTree.js\";\nimport \"./gui/tree/EFTreeItem.js\";\n\nexport { EFTree } from \"./gui/tree/EFTree.js\";\nexport { EFTreeItem } from \"./gui/tree/EFTreeItem.js\";\nexport type { TreeItem, TreeState, TreeActions, TreeContext } from \"./gui/tree/treeContext.js\";\nexport { treeContext, collectAllIds } from \"./gui/tree/treeContext.js\";\n\nimport \"./gui/EFTogglePlay.js\";\n\nexport { EFTogglePlay } from \"./gui/EFTogglePlay.js\";\n\nimport \"./gui/EFPlay.js\";\n\nexport { EFPlay } from \"./gui/EFPlay.js\";\n\nimport \"./gui/EFPause.js\";\n\nexport { EFPause } from \"./gui/EFPause.js\";\n\nimport \"./gui/EFToggleLoop.js\";\n\nexport { EFToggleLoop } from \"./gui/EFToggleLoop.js\";\n\nimport \"./gui/EFScrubber.js\";\n\nexport { EFScrubber } from \"./gui/EFScrubber.js\";\n\nimport \"./gui/EFTimeDisplay.js\";\n\nexport { EFTimeDisplay } from \"./gui/EFTimeDisplay.js\";\n\nimport \"./gui/EFActiveRootTemporal.js\";\n\nexport { EFActiveRootTemporal } from \"./gui/EFActiveRootTemporal.js\";\n\nimport \"./gui/EFDial.js\";\n\nexport { type DialChangeDetail, EFDial } from \"./gui/EFDial.js\";\n\nimport \"./gui/EFControls.js\";\n\nexport { EFControls } from \"./gui/EFControls.js\";\n\nimport \"./gui/EFFocusOverlay.js\";\n\nexport { EFFocusOverlay } from \"./gui/EFFocusOverlay.js\";\n\nimport \"./gui/transformUtils.js\";\n\nexport { getCornerPoint, getOppositeCorner, rotatePoint } from \"./gui/transformUtils.js\";\n\nimport \"./gui/EFTransformHandles.ts\";\n\nexport { type TransformBounds, EFTransformHandles } from \"./gui/EFTransformHandles.ts\";\n\nimport \"./gui/EFResizableBox.ts\";\n\nexport { type BoxBounds, EFResizableBox } from \"./gui/EFResizableBox.ts\";\n\nimport \"./gui/EFFitScale.js\";\n\nexport {\n EFFitScale,\n computeFitScale,\n type ScaleInput,\n type ScaleOutput,\n} from \"./gui/EFFitScale.js\";\n\nimport \"./elements/EFSurface.ts\";\n\nexport { EFSurface } from \"./elements/EFSurface.ts\";\n\nimport \"./elements/EFPanZoom.js\";\n\nexport { EFPanZoom } from \"./elements/EFPanZoom.js\";\nexport type { PanZoomTransform } from \"./elements/EFPanZoom.js\";\n\nimport \"./canvas/EFCanvas.js\";\nimport \"./canvas/EFCanvasItem.js\";\n\nexport { EFCanvas } from \"./canvas/EFCanvas.js\";\nexport { EFCanvasItem } from \"./canvas/EFCanvasItem.js\";\nexport { CanvasAPI } from \"./canvas/api/CanvasAPI.js\";\nexport type { CanvasElementData, SelectionState, CanvasElementBounds } from \"./canvas/api/types.js\";\nexport { SelectionModel } from \"./canvas/selection/SelectionModel.js\";\n\nimport \"./gui/EFOverlayLayer.ts\";\n\nexport { EFOverlayLayer } from \"./gui/EFOverlayLayer.ts\";\n\nimport \"./gui/EFOverlayItem.ts\";\n\nexport { EFOverlayItem } from \"./gui/EFOverlayItem.ts\";\nexport type { OverlayItemPosition } from \"./gui/EFOverlayItem.ts\";\n\nimport \"./gui/EFTimelineRuler.ts\";\n\nexport {\n EFTimelineRuler,\n quantizeToFrameTimeMs,\n calculateFrameIntervalMs,\n calculatePixelsPerFrame,\n shouldShowFrameMarkers,\n} from \"./gui/EFTimelineRuler.ts\";\n\nimport \"./gui/timeline/EFTimeline.js\";\nimport \"./gui/timeline/TrimHandles.js\";\nimport \"./gui/timeline/tracks/EFThumbnailStrip.js\";\n\nexport { EFTimeline } from \"./gui/timeline/EFTimeline.js\";\nexport {\n EFTrimHandles,\n type TrimValue,\n type TrimChangeDetail,\n} from \"./gui/timeline/TrimHandles.js\";\nexport { EFThumbnailStrip } from \"./gui/timeline/tracks/EFThumbnailStrip.js\";\n\nif (typeof window !== \"undefined\") {\n // @ts-expect-error\n window.EF_REGISTERED = true;\n}\n\nimport \"./EF_FRAMEGEN.js\";\n\n// Initialize render API\nimport \"./render/EFRenderAPI.js\";\n\nexport { getRenderInfo, RenderInfoSchema, type RenderInfo } from \"./getRenderInfo.js\";\nexport { getRenderData } from \"./render/getRenderData.js\";\n// Export types only - actual render functions are loaded dynamically by EFTimegroup\nexport type {\n RenderToVideoOptions,\n RenderProgress,\n} from \"./preview/renderTimegroupToVideo.types.js\";\nexport type {\n ContentReadyMode,\n CaptureOptions,\n CanvasPreviewOptions,\n CanvasPreviewResult,\n} from \"./preview/renderTimegroupToCanvas.types.js\";\nexport type { TraceContext } from \"./otel/tracingHelpers.js\";\n\n// Element-to-canvas rendering\nexport {\n renderElementToCanvas,\n type RenderElementOptions,\n} from \"./preview/renderElementToCanvas.js\";\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import \"./elements/EFTimegroup.js\";\nimport \"./version.js\";\n\nexport { version } from \"./version.js\";\nexport { EFTimegroup } from \"./elements/EFTimegroup.js\";\nexport {\n registerCloneFactory,\n unregisterCloneFactory,\n getCloneFactory,\n type CloneFactory,\n type CloneFactoryResult,\n} from \"./elements/cloneFactoryRegistry.js\";\nexport type { ContainerInfo } from \"./elements/ContainerInfo.js\";\nexport { getContainerInfoFromElement } from \"./elements/ContainerInfo.js\";\nexport type { ElementPositionInfo } from \"./elements/ElementPositionInfo.js\";\nexport { getPositionInfoFromElement, PositionInfoMixin } from \"./elements/ElementPositionInfo.js\";\nexport { needsFitScale, elementNeedsFitScale } from \"./gui/FitScaleHelpers.js\";\n\nimport \"./elements/EFImage.js\";\n\nexport { EFImage } from \"./elements/EFImage.js\";\n\nimport \"./elements/EFMotionBlur.js\";\n\nexport { EFMotionBlur } from \"./elements/EFMotionBlur.js\";\n\nimport \"./elements/EFMedia.js\";\n\nexport type { EFMedia } from \"./elements/EFMedia.js\";\n\nimport \"./elements/EFAudio.js\";\n\nexport { EFAudio } from \"./elements/EFAudio.js\";\n\nimport \"./elements/EFVideo.js\";\n\nexport { EFVideo } from \"./elements/EFVideo.js\";\n\nimport \"./elements/EFCaptions.js\";\n\nexport {\n EFCaptions,\n EFCaptionsActiveWord,\n EFCaptionsAfterActiveWord,\n EFCaptionsBeforeActiveWord,\n EFCaptionsSegment,\n} from \"./elements/EFCaptions.js\";\n\nimport \"./elements/EFText.js\";\nimport \"./elements/EFTextSegment.js\";\n\nexport { EFText } from \"./elements/EFText.js\";\nexport { EFTextSegment } from \"./elements/EFTextSegment.js\";\n\nimport \"./elements/EFWaveform.js\";\n\nexport { EFWaveform } from \"./elements/EFWaveform.js\";\n\nimport \"./elements/EFTemporal.js\";\n\nexport { isEFTemporal } from \"./elements/EFTemporal.js\";\nexport type { TemporalMixinInterface } from \"./elements/EFTemporal.js\";\n\nimport \"./gui/EFConfiguration.ts\";\n\nexport { EFConfiguration } from \"./gui/EFConfiguration.ts\";\n\nimport \"./gui/EFWorkbench.js\";\n\nexport { EFWorkbench } from \"./gui/EFWorkbench.js\";\n\nimport \"./gui/EFPreview.js\";\n\nexport { EFPreview } from \"./gui/EFPreview.js\";\n\nimport \"./gui/EFFilmstrip.js\";\n\nexport { EFFilmstrip } from \"./gui/EFFilmstrip.js\";\n\nimport \"./gui/hierarchy/EFHierarchy.js\";\nimport \"./gui/hierarchy/EFHierarchyItem.js\";\n\nexport { EFHierarchy } from \"./gui/hierarchy/EFHierarchy.js\";\nexport {\n EFHierarchyItem,\n EFTimegroupHierarchyItem,\n EFAudioHierarchyItem,\n EFVideoHierarchyItem,\n EFCaptionsHierarchyItem,\n EFCaptionsActiveWordHierarchyItem,\n EFTextHierarchyItem,\n EFTextSegmentHierarchyItem,\n EFWaveformHierarchyItem,\n EFImageHierarchyItem,\n EFHTMLHierarchyItem,\n} from \"./gui/hierarchy/EFHierarchyItem.js\";\nexport type {\n HierarchyState,\n HierarchyActions,\n HierarchyContext,\n} from \"./gui/hierarchy/hierarchyContext.js\";\nexport { hierarchyContext } from \"./gui/hierarchy/hierarchyContext.js\";\n\n// Generic tree component\nimport \"./gui/tree/EFTree.js\";\nimport \"./gui/tree/EFTreeItem.js\";\n\nexport { EFTree } from \"./gui/tree/EFTree.js\";\nexport { EFTreeItem } from \"./gui/tree/EFTreeItem.js\";\nexport type { TreeItem, TreeState, TreeActions, TreeContext } from \"./gui/tree/treeContext.js\";\nexport { treeContext, collectAllIds } from \"./gui/tree/treeContext.js\";\n\nimport \"./gui/EFTogglePlay.js\";\n\nexport { EFTogglePlay } from \"./gui/EFTogglePlay.js\";\n\nimport \"./gui/EFPlay.js\";\n\nexport { EFPlay } from \"./gui/EFPlay.js\";\n\nimport \"./gui/EFPause.js\";\n\nexport { EFPause } from \"./gui/EFPause.js\";\n\nimport \"./gui/EFToggleLoop.js\";\n\nexport { EFToggleLoop } from \"./gui/EFToggleLoop.js\";\n\nimport \"./gui/EFScrubber.js\";\n\nexport { EFScrubber } from \"./gui/EFScrubber.js\";\n\nimport \"./gui/EFTimeDisplay.js\";\n\nexport { EFTimeDisplay } from \"./gui/EFTimeDisplay.js\";\n\nimport \"./gui/EFActiveRootTemporal.js\";\n\nexport { EFActiveRootTemporal } from \"./gui/EFActiveRootTemporal.js\";\n\nimport \"./gui/EFDial.js\";\n\nexport { type DialChangeDetail, EFDial } from \"./gui/EFDial.js\";\n\nimport \"./gui/EFControls.js\";\n\nexport { EFControls } from \"./gui/EFControls.js\";\n\nimport \"./gui/EFFocusOverlay.js\";\n\nexport { EFFocusOverlay } from \"./gui/EFFocusOverlay.js\";\n\nimport \"./gui/transformUtils.js\";\n\nexport { getCornerPoint, getOppositeCorner, rotatePoint } from \"./gui/transformUtils.js\";\n\nimport \"./gui/EFTransformHandles.ts\";\n\nexport { type TransformBounds, EFTransformHandles } from \"./gui/EFTransformHandles.ts\";\n\nimport \"./gui/EFResizableBox.ts\";\n\nexport { type BoxBounds, EFResizableBox } from \"./gui/EFResizableBox.ts\";\n\nimport \"./gui/EFFitScale.js\";\n\nexport {\n EFFitScale,\n computeFitScale,\n type ScaleInput,\n type ScaleOutput,\n} from \"./gui/EFFitScale.js\";\n\nimport \"./elements/EFSurface.ts\";\n\nexport { EFSurface } from \"./elements/EFSurface.ts\";\n\nimport \"./elements/EFPanZoom.js\";\n\nexport { EFPanZoom } from \"./elements/EFPanZoom.js\";\nexport type { PanZoomTransform } from \"./elements/EFPanZoom.js\";\n\nimport \"./canvas/EFCanvas.js\";\nimport \"./canvas/EFCanvasItem.js\";\n\nexport { EFCanvas } from \"./canvas/EFCanvas.js\";\nexport { EFCanvasItem } from \"./canvas/EFCanvasItem.js\";\nexport { CanvasAPI } from \"./canvas/api/CanvasAPI.js\";\nexport type { CanvasElementData, SelectionState, CanvasElementBounds } from \"./canvas/api/types.js\";\nexport { SelectionModel } from \"./canvas/selection/SelectionModel.js\";\n\nimport \"./gui/EFOverlayLayer.ts\";\n\nexport { EFOverlayLayer } from \"./gui/EFOverlayLayer.ts\";\n\nimport \"./gui/EFOverlayItem.ts\";\n\nexport { EFOverlayItem } from \"./gui/EFOverlayItem.ts\";\nexport type { OverlayItemPosition } from \"./gui/EFOverlayItem.ts\";\n\nimport \"./gui/EFTimelineRuler.ts\";\n\nexport {\n EFTimelineRuler,\n quantizeToFrameTimeMs,\n calculateFrameIntervalMs,\n calculatePixelsPerFrame,\n shouldShowFrameMarkers,\n} from \"./gui/EFTimelineRuler.ts\";\n\nimport \"./gui/timeline/EFTimeline.js\";\nimport \"./gui/timeline/TrimHandles.js\";\nimport \"./gui/timeline/tracks/EFThumbnailStrip.js\";\n\nexport { EFTimeline } from \"./gui/timeline/EFTimeline.js\";\nexport {\n EFTrimHandles,\n type TrimValue,\n type TrimChangeDetail,\n} from \"./gui/timeline/TrimHandles.js\";\nexport { EFThumbnailStrip } from \"./gui/timeline/tracks/EFThumbnailStrip.js\";\n\nif (typeof window !== \"undefined\") {\n // @ts-expect-error\n window.EF_REGISTERED = true;\n}\n\nimport \"./EF_FRAMEGEN.js\";\n\n// Initialize render API\nimport \"./render/EFRenderAPI.js\";\n\nexport { getRenderInfo, RenderInfoSchema, type RenderInfo } from \"./getRenderInfo.js\";\nexport { getRenderData } from \"./render/getRenderData.js\";\n// Export types only - actual render functions are loaded dynamically by EFTimegroup\nexport type {\n RenderToVideoOptions,\n RenderProgress,\n} from \"./preview/renderTimegroupToVideo.types.js\";\nexport type {\n ContentReadyMode,\n CaptureOptions,\n CanvasPreviewOptions,\n CanvasPreviewResult,\n} from \"./preview/renderTimegroupToCanvas.types.js\";\nexport type { TraceContext } from \"./otel/tracingHelpers.js\";\n\n// Element-to-canvas rendering\nexport {\n renderElementToCanvas,\n type RenderElementOptions,\n} from \"./preview/renderElementToCanvas.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8NA,IAAI,OAAO,WAAW,YAEpB,QAAO,gBAAgB"}
|
|
@@ -92,9 +92,30 @@ declare class FrameController {
|
|
|
92
92
|
currentTimeMs: number;
|
|
93
93
|
});
|
|
94
94
|
/**
|
|
95
|
-
* Cancel any in-progress render operation and
|
|
95
|
+
* Cancel any in-progress render operation and mark the last frame stale.
|
|
96
|
+
*
|
|
97
|
+
* Use this when a user-initiated seek or time change invalidates the current
|
|
98
|
+
* render and a fresh render is needed immediately.
|
|
99
|
+
*
|
|
100
|
+
* Do NOT call this from quality-upgrade callbacks — use markLastFrameStale()
|
|
101
|
+
* instead to avoid aborting an in-progress scrub seek.
|
|
96
102
|
*/
|
|
97
103
|
abort(): void;
|
|
104
|
+
/**
|
|
105
|
+
* Mark the last rendered frame stale WITHOUT aborting any in-progress render.
|
|
106
|
+
*
|
|
107
|
+
* Use this for quality-upgrade re-renders: the goal is to show a
|
|
108
|
+
* higher-quality frame at the same time, but aborting the current render
|
|
109
|
+
* would cancel an in-progress scrub seek. If quality-upgrade calls fire
|
|
110
|
+
* faster than scrub seeks can complete, every seek would be aborted before
|
|
111
|
+
* returning a sample — infinite loop, canvas stuck on first frame.
|
|
112
|
+
*
|
|
113
|
+
* With only the stale marker, the current render continues uninterrupted. When
|
|
114
|
+
* it finishes, the queued re-render fires and can upgrade to main quality.
|
|
115
|
+
*
|
|
116
|
+
* Do NOT call abort() from quality-upgrade callbacks — use this method instead.
|
|
117
|
+
*/
|
|
118
|
+
markLastFrameStale(): void;
|
|
98
119
|
/**
|
|
99
120
|
* Render a frame at the specified time.
|
|
100
121
|
*
|
|
@@ -54,7 +54,13 @@ var FrameController = class {
|
|
|
54
54
|
this.#rootElement = rootElement;
|
|
55
55
|
}
|
|
56
56
|
/**
|
|
57
|
-
* Cancel any in-progress render operation and
|
|
57
|
+
* Cancel any in-progress render operation and mark the last frame stale.
|
|
58
|
+
*
|
|
59
|
+
* Use this when a user-initiated seek or time change invalidates the current
|
|
60
|
+
* render and a fresh render is needed immediately.
|
|
61
|
+
*
|
|
62
|
+
* Do NOT call this from quality-upgrade callbacks — use markLastFrameStale()
|
|
63
|
+
* instead to avoid aborting an in-progress scrub seek.
|
|
58
64
|
*/
|
|
59
65
|
abort() {
|
|
60
66
|
this.#abortController?.abort();
|
|
@@ -62,6 +68,23 @@ var FrameController = class {
|
|
|
62
68
|
this.#lastRenderedTimeMs = -1;
|
|
63
69
|
}
|
|
64
70
|
/**
|
|
71
|
+
* Mark the last rendered frame stale WITHOUT aborting any in-progress render.
|
|
72
|
+
*
|
|
73
|
+
* Use this for quality-upgrade re-renders: the goal is to show a
|
|
74
|
+
* higher-quality frame at the same time, but aborting the current render
|
|
75
|
+
* would cancel an in-progress scrub seek. If quality-upgrade calls fire
|
|
76
|
+
* faster than scrub seeks can complete, every seek would be aborted before
|
|
77
|
+
* returning a sample — infinite loop, canvas stuck on first frame.
|
|
78
|
+
*
|
|
79
|
+
* With only the stale marker, the current render continues uninterrupted. When
|
|
80
|
+
* it finishes, the queued re-render fires and can upgrade to main quality.
|
|
81
|
+
*
|
|
82
|
+
* Do NOT call abort() from quality-upgrade callbacks — use this method instead.
|
|
83
|
+
*/
|
|
84
|
+
markLastFrameStale() {
|
|
85
|
+
this.#lastRenderedTimeMs = -1;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
65
88
|
* Render a frame at the specified time.
|
|
66
89
|
*
|
|
67
90
|
* This is the main entry point for frame rendering. It:
|
|
@@ -109,13 +132,11 @@ var FrameController = class {
|
|
|
109
132
|
const renderMs = performance.now() - tRender;
|
|
110
133
|
const tAnims = performance.now();
|
|
111
134
|
if (onAnimationsUpdate) onAnimationsUpdate(this.#rootElement);
|
|
112
|
-
const animsMs = performance.now() - tAnims;
|
|
113
|
-
this.#lastRenderedTimeMs = timeMs;
|
|
114
135
|
return {
|
|
115
136
|
queryMs,
|
|
116
137
|
prepareMs,
|
|
117
138
|
renderMs,
|
|
118
|
-
animsMs
|
|
139
|
+
animsMs: performance.now() - tAnims
|
|
119
140
|
};
|
|
120
141
|
} finally {
|
|
121
142
|
this.#renderInProgress = false;
|
|
@@ -123,7 +144,7 @@ var FrameController = class {
|
|
|
123
144
|
const pendingTime = this.#pendingRenderTime;
|
|
124
145
|
this.#pendingRenderTime = null;
|
|
125
146
|
this.renderFrame(pendingTime, options).catch(() => {});
|
|
126
|
-
}
|
|
147
|
+
} else this.#lastRenderedTimeMs = timeMs;
|
|
127
148
|
}
|
|
128
149
|
}
|
|
129
150
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FrameController.js","names":["#rootElement","#abortController","#lastRenderedTimeMs","#renderInProgress","#pendingRenderTime","#queryVisibleElements","result: FrameRenderable[]","#getChildrenIncludingSlotted","assignedElements: Element[]"],"sources":["../../src/preview/FrameController.ts"],"sourcesContent":["/**\n * FrameController: Centralized frame rendering control\n *\n * Replaces the distributed Lit Task hierarchy with a single control loop\n * that queries elements and coordinates rendering directly.\n *\n * Benefits over the previous Task-based system:\n * - Single abort controller instead of distributed abort handling\n * - Clear prepare → render phases\n * - All coordination visible in one place\n * - No Lit Task reactivity overhead\n */\n\nimport type { LitElement } from \"lit\";\n\n// ============================================================================\n// Priority Constants\n// ============================================================================\n// Lower numbers render first. Elements with dependencies should have higher\n// priority numbers than their dependencies.\n//\n// Example: Waveform depends on audio analysis data, so it renders after audio.\n// ============================================================================\n\n/**\n * Priority for video elements.\n * Video renders first as other elements may depend on video frames being ready.\n */\nexport const PRIORITY_VIDEO = 1;\n\n/**\n * Priority for captions elements.\n * Captions render after video so they can overlay correctly.\n */\nexport const PRIORITY_CAPTIONS = 2;\n\n/**\n * Priority for audio elements.\n * Audio renders after captions (no visual dependency, but keeps consistent ordering).\n */\nexport const PRIORITY_AUDIO = 3;\n\n/**\n * Priority for waveform elements.\n * Waveform renders after audio because it depends on audio analysis data.\n */\nexport const PRIORITY_WAVEFORM = 4;\n\n/**\n * Priority for image elements.\n * Images render with low priority as they're typically static.\n */\nexport const PRIORITY_IMAGE = 5;\n\n/**\n * Default priority for elements that don't specify one.\n * High number ensures custom elements render after standard elements.\n */\nexport const PRIORITY_DEFAULT = 100;\n\n/**\n * State returned by elements describing their readiness for a given time.\n */\nexport interface FrameState {\n /**\n * Whether async preparation is needed before rendering.\n * Examples: video needs to seek, captions need to load data.\n */\n needsPreparation: boolean;\n\n /**\n * Whether the element is ready to render synchronously.\n * True when all async work is complete and renderFrame() can be called.\n */\n isReady: boolean;\n\n /**\n * Rendering priority hint. Lower numbers render first.\n * Used to order render calls for elements with dependencies.\n *\n * Standard priorities:\n * - PRIORITY_VIDEO (1): Video elements\n * - PRIORITY_CAPTIONS (2): Caption overlays\n * - PRIORITY_AUDIO (3): Audio elements\n * - PRIORITY_WAVEFORM (4): Audio visualizers (depend on audio)\n * - PRIORITY_IMAGE (5): Static images\n * - PRIORITY_DEFAULT (100): Fallback for custom elements\n */\n priority: number;\n}\n\n/**\n * Interface that elements implement to participate in centralized frame rendering.\n * Elements keep their rendering logic local but expose a standardized interface.\n */\nexport interface FrameRenderable {\n /**\n * Query the element's readiness state for a given time.\n * Must be synchronous and cheap to call.\n */\n getFrameState(timeMs: number): FrameState;\n\n /**\n * Async preparation phase. Called when getFrameState().needsPreparation is true.\n * Performs any async work needed before rendering (seeking, loading, etc.).\n *\n * @param timeMs - The time to prepare for\n * @param signal - Abort signal for cancellation\n */\n prepareFrame(timeMs: number, signal: AbortSignal): Promise<void>;\n\n /**\n * Synchronous render phase. Called after all preparation is complete.\n * Performs the actual rendering (paint to canvas, update DOM, etc.).\n *\n * @param timeMs - The time to render\n */\n renderFrame(timeMs: number): void;\n}\n\n/**\n * Type guard to check if an element implements FrameRenderable.\n */\nexport function isFrameRenderable(element: unknown): element is FrameRenderable {\n return (\n typeof element === \"object\" &&\n element !== null &&\n \"getFrameState\" in element &&\n \"prepareFrame\" in element &&\n \"renderFrame\" in element &&\n typeof (element as FrameRenderable).getFrameState === \"function\" &&\n typeof (element as FrameRenderable).prepareFrame === \"function\" &&\n typeof (element as FrameRenderable).renderFrame === \"function\"\n );\n}\n\n/**\n * Per-phase timing data returned by FrameController.renderFrame().\n * All values are in milliseconds.\n */\nexport interface RenderFrameTiming {\n queryMs: number;\n prepareMs: number;\n renderMs: number;\n animsMs: number;\n}\n\n/**\n * Options for FrameController.renderFrame()\n */\nexport interface RenderFrameOptions {\n /**\n * Whether to wait for Lit updateComplete before querying elements.\n * Default: true\n */\n waitForLitUpdate?: boolean;\n\n /**\n * Callback to update CSS animations after frame rendering completes.\n * Called with the root element after all elements have rendered.\n * This centralizes animation synchronization in one place.\n */\n onAnimationsUpdate?: (rootElement: Element) => void;\n}\n\n/**\n * Central controller for frame rendering.\n * Lives at the root timegroup and orchestrates all element rendering.\n */\nexport class FrameController {\n #rootElement: LitElement & { currentTimeMs: number };\n #abortController: AbortController | null = null;\n #renderInProgress = false;\n #pendingRenderTime: number | null = null;\n /**\n * Last successfully rendered time. Used for deduplication when multiple\n * callers (e.g., PlaybackController RAF loop and canvas render loop)\n * both try to render the same frame within one animation frame.\n */\n #lastRenderedTimeMs: number = -1;\n\n constructor(rootElement: LitElement & { currentTimeMs: number }) {\n this.#rootElement = rootElement;\n }\n\n /**\n * Cancel any in-progress render operation and reset deduplication state.\n */\n abort(): void {\n this.#abortController?.abort();\n this.#abortController = null;\n // Reset deduplication state so next render goes through even if same time\n this.#lastRenderedTimeMs = -1;\n }\n\n /**\n * Render a frame at the specified time.\n *\n * This is the main entry point for frame rendering. It:\n * 1. Cancels any previous in-progress render\n * 2. Queries all visible FrameRenderable elements\n * 3. Runs preparation in parallel for elements that need it\n * 4. Runs render in priority order\n *\n * @param timeMs - The time in milliseconds to render\n * @param options - Optional configuration\n */\n async renderFrame(\n timeMs: number,\n options: RenderFrameOptions = {},\n ): Promise<RenderFrameTiming | null> {\n const { waitForLitUpdate = true, onAnimationsUpdate } = options;\n\n // Deduplicate: skip if we just rendered this exact time.\n // This prevents double-rendering when multiple RAF loops (e.g., PlaybackController\n // and canvas render loop) both call renderFrame() for the same frame.\n if (timeMs === this.#lastRenderedTimeMs) {\n return null;\n }\n\n // If a render is in progress, queue this one\n if (this.#renderInProgress) {\n this.#pendingRenderTime = timeMs;\n return null;\n }\n\n // Cancel any previous render operation\n this.#abortController?.abort();\n this.#abortController = new AbortController();\n const signal = this.#abortController.signal;\n\n this.#renderInProgress = true;\n\n try {\n if (waitForLitUpdate) {\n await this.#rootElement.updateComplete;\n signal.throwIfAborted();\n }\n\n const tQuery = performance.now();\n const elements = this.#queryVisibleElements(timeMs);\n const queryMs = performance.now() - tQuery;\n signal.throwIfAborted();\n\n const tPrepare = performance.now();\n const elementsNeedingPreparation = elements.filter(\n (el) => el.getFrameState(timeMs).needsPreparation,\n );\n\n if (elementsNeedingPreparation.length > 0) {\n await Promise.all(elementsNeedingPreparation.map((el) => el.prepareFrame(timeMs, signal)));\n signal.throwIfAborted();\n }\n const prepareMs = performance.now() - tPrepare;\n\n const tRender = performance.now();\n const sortedElements = [...elements].sort(\n (a, b) => a.getFrameState(timeMs).priority - b.getFrameState(timeMs).priority,\n );\n\n for (const element of sortedElements) {\n signal.throwIfAborted();\n element.renderFrame(timeMs);\n }\n const renderMs = performance.now() - tRender;\n\n const tAnims = performance.now();\n if (onAnimationsUpdate) {\n onAnimationsUpdate(this.#rootElement);\n }\n const animsMs = performance.now() - tAnims;\n\n this.#lastRenderedTimeMs = timeMs;\n return { queryMs, prepareMs, renderMs, animsMs };\n } finally {\n this.#renderInProgress = false;\n\n // Process any queued render\n if (this.#pendingRenderTime !== null) {\n const pendingTime = this.#pendingRenderTime;\n this.#pendingRenderTime = null;\n // Don't await - fire and forget to avoid recursive waiting\n this.renderFrame(pendingTime, options).catch(() => {\n // Silently ignore errors from queued renders (likely aborted)\n });\n }\n }\n }\n\n /**\n * Query all visible FrameRenderable elements in the tree.\n * Uses temporal visibility to filter out elements not visible at current time.\n *\n * IMPORTANT: For temporal elements, we use temporal visibility (startTimeMs/endTimeMs)\n * instead of CSS visibility. This is because updateAnimations sets display:none on\n * elements outside their time range, but that CSS state is from the PREVIOUS frame.\n * When seeking, we need to evaluate visibility based on the NEW time, not stale CSS.\n *\n * @param timeMs - The time to use for visibility checks. This should be the target\n * render time, not read from root element (which may be stale).\n */\n #queryVisibleElements(timeMs: number): FrameRenderable[] {\n const result: FrameRenderable[] = [];\n const currentTimeMs = timeMs;\n\n const walk = (element: Element): void => {\n // For temporal elements (ef-timegroup, ef-video, etc.), use temporal visibility\n // instead of CSS visibility. CSS display:none may be stale from previous frame.\n const isTemporal = \"startTimeMs\" in element && \"endTimeMs\" in element;\n\n if (isTemporal) {\n // Temporal element: check time-based visibility\n // Use exclusive end (< not <=) to avoid overlap at boundaries\n const startMs = (element as { startTimeMs?: number }).startTimeMs ?? -Infinity;\n const endMs = (element as { endTimeMs?: number }).endTimeMs ?? Infinity;\n const isTemporallyVisible = currentTimeMs >= startMs && currentTimeMs < endMs;\n\n if (!isTemporallyVisible) {\n // Skip this element AND its children (children's times are relative to parent)\n return;\n }\n\n // Element is temporally visible - include if it implements FrameRenderable\n if (isFrameRenderable(element)) {\n result.push(element);\n }\n } else {\n // Non-temporal element: only check inline display style (fast path).\n // Skip getComputedStyle — it forces synchronous style recalc and is\n // unnecessary because FrameRenderable elements are always temporal.\n // We only walk non-temporal elements to reach temporal children.\n if (element instanceof HTMLElement && element.style.display === \"none\") {\n return;\n }\n\n if (isFrameRenderable(element)) {\n result.push(element);\n }\n }\n\n // Walk children - handle both regular children and slotted content\n const children = this.#getChildrenIncludingSlotted(element);\n for (const child of children) {\n walk(child);\n }\n };\n\n walk(this.#rootElement);\n return result;\n }\n\n /**\n * Gets all child elements including slotted content for shadow DOM elements.\n * For elements with shadow DOM that contain slots, this returns the assigned\n * elements (slotted content) instead of just the shadow DOM children.\n */\n #getChildrenIncludingSlotted(element: Element): Iterable<Element> {\n // If element has shadowRoot with slots, get assigned elements\n if (element.shadowRoot) {\n const slots = element.shadowRoot.querySelectorAll(\"slot\");\n if (slots.length > 0) {\n const assignedElements: Element[] = [];\n for (const slot of slots) {\n assignedElements.push(...slot.assignedElements());\n }\n // Also include shadow DOM children that aren't slots (for mixed content)\n for (const child of element.shadowRoot.children) {\n if (child.tagName !== \"SLOT\") {\n assignedElements.push(child);\n }\n }\n return assignedElements;\n }\n }\n\n // Return HTMLCollection directly (iterable, no allocation)\n return element.children;\n }\n\n /**\n * Check if a render is currently in progress.\n */\n get isRendering(): boolean {\n return this.#renderInProgress;\n }\n}\n\n/**\n * Default frame state for elements that don't need special handling.\n * Use this for simple elements that are always ready.\n */\nexport const DEFAULT_FRAME_STATE: FrameState = {\n needsPreparation: false,\n isReady: true,\n priority: PRIORITY_DEFAULT,\n};\n\n/**\n * Helper to create a FrameRenderable mixin for elements.\n * Provides default implementations that can be overridden.\n */\nexport function createFrameRenderableMixin<T extends { new (...args: any[]): HTMLElement }>(\n Base: T,\n) {\n return class FrameRenderableMixin extends Base implements FrameRenderable {\n getFrameState(_timeMs: number): FrameState {\n return DEFAULT_FRAME_STATE;\n }\n\n async prepareFrame(_timeMs: number, _signal: AbortSignal): Promise<void> {\n // Default: no preparation needed\n }\n\n renderFrame(_timeMs: number): void {\n // Default: no explicit render needed\n }\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FrameController.js","names":["#rootElement","#abortController","#lastRenderedTimeMs","#renderInProgress","#pendingRenderTime","#queryVisibleElements","result: FrameRenderable[]","#getChildrenIncludingSlotted","assignedElements: Element[]"],"sources":["../../src/preview/FrameController.ts"],"sourcesContent":["/**\n * FrameController: Centralized frame rendering control\n *\n * Replaces the distributed Lit Task hierarchy with a single control loop\n * that queries elements and coordinates rendering directly.\n *\n * Benefits over the previous Task-based system:\n * - Single abort controller instead of distributed abort handling\n * - Clear prepare → render phases\n * - All coordination visible in one place\n * - No Lit Task reactivity overhead\n */\n\nimport type { LitElement } from \"lit\";\n\n// ============================================================================\n// Priority Constants\n// ============================================================================\n// Lower numbers render first. Elements with dependencies should have higher\n// priority numbers than their dependencies.\n//\n// Example: Waveform depends on audio analysis data, so it renders after audio.\n// ============================================================================\n\n/**\n * Priority for video elements.\n * Video renders first as other elements may depend on video frames being ready.\n */\nexport const PRIORITY_VIDEO = 1;\n\n/**\n * Priority for captions elements.\n * Captions render after video so they can overlay correctly.\n */\nexport const PRIORITY_CAPTIONS = 2;\n\n/**\n * Priority for audio elements.\n * Audio renders after captions (no visual dependency, but keeps consistent ordering).\n */\nexport const PRIORITY_AUDIO = 3;\n\n/**\n * Priority for waveform elements.\n * Waveform renders after audio because it depends on audio analysis data.\n */\nexport const PRIORITY_WAVEFORM = 4;\n\n/**\n * Priority for image elements.\n * Images render with low priority as they're typically static.\n */\nexport const PRIORITY_IMAGE = 5;\n\n/**\n * Default priority for elements that don't specify one.\n * High number ensures custom elements render after standard elements.\n */\nexport const PRIORITY_DEFAULT = 100;\n\n/**\n * State returned by elements describing their readiness for a given time.\n */\nexport interface FrameState {\n /**\n * Whether async preparation is needed before rendering.\n * Examples: video needs to seek, captions need to load data.\n */\n needsPreparation: boolean;\n\n /**\n * Whether the element is ready to render synchronously.\n * True when all async work is complete and renderFrame() can be called.\n */\n isReady: boolean;\n\n /**\n * Rendering priority hint. Lower numbers render first.\n * Used to order render calls for elements with dependencies.\n *\n * Standard priorities:\n * - PRIORITY_VIDEO (1): Video elements\n * - PRIORITY_CAPTIONS (2): Caption overlays\n * - PRIORITY_AUDIO (3): Audio elements\n * - PRIORITY_WAVEFORM (4): Audio visualizers (depend on audio)\n * - PRIORITY_IMAGE (5): Static images\n * - PRIORITY_DEFAULT (100): Fallback for custom elements\n */\n priority: number;\n}\n\n/**\n * Interface that elements implement to participate in centralized frame rendering.\n * Elements keep their rendering logic local but expose a standardized interface.\n */\nexport interface FrameRenderable {\n /**\n * Query the element's readiness state for a given time.\n * Must be synchronous and cheap to call.\n */\n getFrameState(timeMs: number): FrameState;\n\n /**\n * Async preparation phase. Called when getFrameState().needsPreparation is true.\n * Performs any async work needed before rendering (seeking, loading, etc.).\n *\n * @param timeMs - The time to prepare for\n * @param signal - Abort signal for cancellation\n */\n prepareFrame(timeMs: number, signal: AbortSignal): Promise<void>;\n\n /**\n * Synchronous render phase. Called after all preparation is complete.\n * Performs the actual rendering (paint to canvas, update DOM, etc.).\n *\n * @param timeMs - The time to render\n */\n renderFrame(timeMs: number): void;\n}\n\n/**\n * Type guard to check if an element implements FrameRenderable.\n */\nexport function isFrameRenderable(element: unknown): element is FrameRenderable {\n return (\n typeof element === \"object\" &&\n element !== null &&\n \"getFrameState\" in element &&\n \"prepareFrame\" in element &&\n \"renderFrame\" in element &&\n typeof (element as FrameRenderable).getFrameState === \"function\" &&\n typeof (element as FrameRenderable).prepareFrame === \"function\" &&\n typeof (element as FrameRenderable).renderFrame === \"function\"\n );\n}\n\n/**\n * Per-phase timing data returned by FrameController.renderFrame().\n * All values are in milliseconds.\n */\nexport interface RenderFrameTiming {\n queryMs: number;\n prepareMs: number;\n renderMs: number;\n animsMs: number;\n}\n\n/**\n * Options for FrameController.renderFrame()\n */\nexport interface RenderFrameOptions {\n /**\n * Whether to wait for Lit updateComplete before querying elements.\n * Default: true\n */\n waitForLitUpdate?: boolean;\n\n /**\n * Callback to update CSS animations after frame rendering completes.\n * Called with the root element after all elements have rendered.\n * This centralizes animation synchronization in one place.\n */\n onAnimationsUpdate?: (rootElement: Element) => void;\n}\n\n/**\n * Central controller for frame rendering.\n * Lives at the root timegroup and orchestrates all element rendering.\n */\nexport class FrameController {\n #rootElement: LitElement & { currentTimeMs: number };\n #abortController: AbortController | null = null;\n #renderInProgress = false;\n #pendingRenderTime: number | null = null;\n /**\n * Last successfully rendered time. Used for deduplication when multiple\n * callers (e.g., PlaybackController RAF loop and canvas render loop)\n * both try to render the same frame within one animation frame.\n */\n #lastRenderedTimeMs: number = -1;\n\n constructor(rootElement: LitElement & { currentTimeMs: number }) {\n this.#rootElement = rootElement;\n }\n\n /**\n * Cancel any in-progress render operation and mark the last frame stale.\n *\n * Use this when a user-initiated seek or time change invalidates the current\n * render and a fresh render is needed immediately.\n *\n * Do NOT call this from quality-upgrade callbacks — use markLastFrameStale()\n * instead to avoid aborting an in-progress scrub seek.\n */\n abort(): void {\n this.#abortController?.abort();\n this.#abortController = null;\n // Reset deduplication state so next render goes through even if same time\n this.#lastRenderedTimeMs = -1;\n }\n\n /**\n * Mark the last rendered frame stale WITHOUT aborting any in-progress render.\n *\n * Use this for quality-upgrade re-renders: the goal is to show a\n * higher-quality frame at the same time, but aborting the current render\n * would cancel an in-progress scrub seek. If quality-upgrade calls fire\n * faster than scrub seeks can complete, every seek would be aborted before\n * returning a sample — infinite loop, canvas stuck on first frame.\n *\n * With only the stale marker, the current render continues uninterrupted. When\n * it finishes, the queued re-render fires and can upgrade to main quality.\n *\n * Do NOT call abort() from quality-upgrade callbacks — use this method instead.\n */\n markLastFrameStale(): void {\n this.#lastRenderedTimeMs = -1;\n }\n\n /**\n * Render a frame at the specified time.\n *\n * This is the main entry point for frame rendering. It:\n * 1. Cancels any previous in-progress render\n * 2. Queries all visible FrameRenderable elements\n * 3. Runs preparation in parallel for elements that need it\n * 4. Runs render in priority order\n *\n * @param timeMs - The time in milliseconds to render\n * @param options - Optional configuration\n */\n async renderFrame(\n timeMs: number,\n options: RenderFrameOptions = {},\n ): Promise<RenderFrameTiming | null> {\n const { waitForLitUpdate = true, onAnimationsUpdate } = options;\n\n // Deduplicate: skip if we just rendered this exact time.\n // This prevents double-rendering when multiple RAF loops (e.g., PlaybackController\n // and canvas render loop) both call renderFrame() for the same frame.\n if (timeMs === this.#lastRenderedTimeMs) {\n return null;\n }\n\n // If a render is in progress, queue this one\n if (this.#renderInProgress) {\n this.#pendingRenderTime = timeMs;\n return null;\n }\n\n // Cancel any previous render operation\n this.#abortController?.abort();\n this.#abortController = new AbortController();\n const signal = this.#abortController.signal;\n\n this.#renderInProgress = true;\n\n try {\n if (waitForLitUpdate) {\n await this.#rootElement.updateComplete;\n signal.throwIfAborted();\n }\n\n const tQuery = performance.now();\n const elements = this.#queryVisibleElements(timeMs);\n const queryMs = performance.now() - tQuery;\n signal.throwIfAborted();\n\n const tPrepare = performance.now();\n const elementsNeedingPreparation = elements.filter(\n (el) => el.getFrameState(timeMs).needsPreparation,\n );\n\n if (elementsNeedingPreparation.length > 0) {\n await Promise.all(elementsNeedingPreparation.map((el) => el.prepareFrame(timeMs, signal)));\n signal.throwIfAborted();\n }\n const prepareMs = performance.now() - tPrepare;\n\n const tRender = performance.now();\n const sortedElements = [...elements].sort(\n (a, b) => a.getFrameState(timeMs).priority - b.getFrameState(timeMs).priority,\n );\n\n for (const element of sortedElements) {\n signal.throwIfAborted();\n element.renderFrame(timeMs);\n }\n const renderMs = performance.now() - tRender;\n\n const tAnims = performance.now();\n if (onAnimationsUpdate) {\n onAnimationsUpdate(this.#rootElement);\n }\n const animsMs = performance.now() - tAnims;\n\n return { queryMs, prepareMs, renderMs, animsMs };\n } finally {\n this.#renderInProgress = false;\n\n // Process any queued render. IMPORTANT: set #lastRenderedTimeMs only when\n // there is no pending follow-up render. If a pending render exists (e.g.\n // a quality-upgrade re-render queued while the scrub render was in-flight),\n // setting the dedup timestamp here would cause the pending renderFrame(T)\n // to be skipped by the dedup check (T === T). By deferring the assignment\n // we let the pending render run, and it will set #lastRenderedTimeMs itself.\n if (this.#pendingRenderTime !== null) {\n const pendingTime = this.#pendingRenderTime;\n this.#pendingRenderTime = null;\n // Don't await - fire and forget to avoid recursive waiting\n this.renderFrame(pendingTime, options).catch(() => {\n // Silently ignore errors from queued renders (likely aborted)\n });\n } else {\n this.#lastRenderedTimeMs = timeMs;\n }\n }\n }\n\n /**\n * Query all visible FrameRenderable elements in the tree.\n * Uses temporal visibility to filter out elements not visible at current time.\n *\n * IMPORTANT: For temporal elements, we use temporal visibility (startTimeMs/endTimeMs)\n * instead of CSS visibility. This is because updateAnimations sets display:none on\n * elements outside their time range, but that CSS state is from the PREVIOUS frame.\n * When seeking, we need to evaluate visibility based on the NEW time, not stale CSS.\n *\n * @param timeMs - The time to use for visibility checks. This should be the target\n * render time, not read from root element (which may be stale).\n */\n #queryVisibleElements(timeMs: number): FrameRenderable[] {\n const result: FrameRenderable[] = [];\n const currentTimeMs = timeMs;\n\n const walk = (element: Element): void => {\n // For temporal elements (ef-timegroup, ef-video, etc.), use temporal visibility\n // instead of CSS visibility. CSS display:none may be stale from previous frame.\n const isTemporal = \"startTimeMs\" in element && \"endTimeMs\" in element;\n\n if (isTemporal) {\n // Temporal element: check time-based visibility\n // Use exclusive end (< not <=) to avoid overlap at boundaries\n const startMs = (element as { startTimeMs?: number }).startTimeMs ?? -Infinity;\n const endMs = (element as { endTimeMs?: number }).endTimeMs ?? Infinity;\n const isTemporallyVisible = currentTimeMs >= startMs && currentTimeMs < endMs;\n\n if (!isTemporallyVisible) {\n // Skip this element AND its children (children's times are relative to parent)\n return;\n }\n\n // Element is temporally visible - include if it implements FrameRenderable\n if (isFrameRenderable(element)) {\n result.push(element);\n }\n } else {\n // Non-temporal element: only check inline display style (fast path).\n // Skip getComputedStyle — it forces synchronous style recalc and is\n // unnecessary because FrameRenderable elements are always temporal.\n // We only walk non-temporal elements to reach temporal children.\n if (element instanceof HTMLElement && element.style.display === \"none\") {\n return;\n }\n\n if (isFrameRenderable(element)) {\n result.push(element);\n }\n }\n\n // Walk children - handle both regular children and slotted content\n const children = this.#getChildrenIncludingSlotted(element);\n for (const child of children) {\n walk(child);\n }\n };\n\n walk(this.#rootElement);\n return result;\n }\n\n /**\n * Gets all child elements including slotted content for shadow DOM elements.\n * For elements with shadow DOM that contain slots, this returns the assigned\n * elements (slotted content) instead of just the shadow DOM children.\n */\n #getChildrenIncludingSlotted(element: Element): Iterable<Element> {\n // If element has shadowRoot with slots, get assigned elements\n if (element.shadowRoot) {\n const slots = element.shadowRoot.querySelectorAll(\"slot\");\n if (slots.length > 0) {\n const assignedElements: Element[] = [];\n for (const slot of slots) {\n assignedElements.push(...slot.assignedElements());\n }\n // Also include shadow DOM children that aren't slots (for mixed content)\n for (const child of element.shadowRoot.children) {\n if (child.tagName !== \"SLOT\") {\n assignedElements.push(child);\n }\n }\n return assignedElements;\n }\n }\n\n // Return HTMLCollection directly (iterable, no allocation)\n return element.children;\n }\n\n /**\n * Check if a render is currently in progress.\n */\n get isRendering(): boolean {\n return this.#renderInProgress;\n }\n}\n\n/**\n * Default frame state for elements that don't need special handling.\n * Use this for simple elements that are always ready.\n */\nexport const DEFAULT_FRAME_STATE: FrameState = {\n needsPreparation: false,\n isReady: true,\n priority: PRIORITY_DEFAULT,\n};\n\n/**\n * Helper to create a FrameRenderable mixin for elements.\n * Provides default implementations that can be overridden.\n */\nexport function createFrameRenderableMixin<T extends { new (...args: any[]): HTMLElement }>(\n Base: T,\n) {\n return class FrameRenderableMixin extends Base implements FrameRenderable {\n getFrameState(_timeMs: number): FrameState {\n return DEFAULT_FRAME_STATE;\n }\n\n async prepareFrame(_timeMs: number, _signal: AbortSignal): Promise<void> {\n // Default: no preparation needed\n }\n\n renderFrame(_timeMs: number): void {\n // Default: no explicit render needed\n }\n };\n}\n"],"mappings":";;;;;AA4BA,MAAa,iBAAiB;;;;;AAM9B,MAAa,oBAAoB;;;;;AAMjC,MAAa,iBAAiB;;;;;AAM9B,MAAa,oBAAoB;;;;;AAMjC,MAAa,iBAAiB;;;;;AAM9B,MAAa,mBAAmB;;;;AAiEhC,SAAgB,kBAAkB,SAA8C;AAC9E,QACE,OAAO,YAAY,YACnB,YAAY,QACZ,mBAAmB,WACnB,kBAAkB,WAClB,iBAAiB,WACjB,OAAQ,QAA4B,kBAAkB,cACtD,OAAQ,QAA4B,iBAAiB,cACrD,OAAQ,QAA4B,gBAAgB;;;;;;AAqCxD,IAAa,kBAAb,MAA6B;CAC3B;CACA,mBAA2C;CAC3C,oBAAoB;CACpB,qBAAoC;;;;;;CAMpC,sBAA8B;CAE9B,YAAY,aAAqD;AAC/D,QAAKA,cAAe;;;;;;;;;;;CAYtB,QAAc;AACZ,QAAKC,iBAAkB,OAAO;AAC9B,QAAKA,kBAAmB;AAExB,QAAKC,qBAAsB;;;;;;;;;;;;;;;;CAiB7B,qBAA2B;AACzB,QAAKA,qBAAsB;;;;;;;;;;;;;;CAe7B,MAAM,YACJ,QACA,UAA8B,EAAE,EACG;EACnC,MAAM,EAAE,mBAAmB,MAAM,uBAAuB;AAKxD,MAAI,WAAW,MAAKA,mBAClB,QAAO;AAIT,MAAI,MAAKC,kBAAmB;AAC1B,SAAKC,oBAAqB;AAC1B,UAAO;;AAIT,QAAKH,iBAAkB,OAAO;AAC9B,QAAKA,kBAAmB,IAAI,iBAAiB;EAC7C,MAAM,SAAS,MAAKA,gBAAiB;AAErC,QAAKE,mBAAoB;AAEzB,MAAI;AACF,OAAI,kBAAkB;AACpB,UAAM,MAAKH,YAAa;AACxB,WAAO,gBAAgB;;GAGzB,MAAM,SAAS,YAAY,KAAK;GAChC,MAAM,WAAW,MAAKK,qBAAsB,OAAO;GACnD,MAAM,UAAU,YAAY,KAAK,GAAG;AACpC,UAAO,gBAAgB;GAEvB,MAAM,WAAW,YAAY,KAAK;GAClC,MAAM,6BAA6B,SAAS,QACzC,OAAO,GAAG,cAAc,OAAO,CAAC,iBAClC;AAED,OAAI,2BAA2B,SAAS,GAAG;AACzC,UAAM,QAAQ,IAAI,2BAA2B,KAAK,OAAO,GAAG,aAAa,QAAQ,OAAO,CAAC,CAAC;AAC1F,WAAO,gBAAgB;;GAEzB,MAAM,YAAY,YAAY,KAAK,GAAG;GAEtC,MAAM,UAAU,YAAY,KAAK;GACjC,MAAM,iBAAiB,CAAC,GAAG,SAAS,CAAC,MAClC,GAAG,MAAM,EAAE,cAAc,OAAO,CAAC,WAAW,EAAE,cAAc,OAAO,CAAC,SACtE;AAED,QAAK,MAAM,WAAW,gBAAgB;AACpC,WAAO,gBAAgB;AACvB,YAAQ,YAAY,OAAO;;GAE7B,MAAM,WAAW,YAAY,KAAK,GAAG;GAErC,MAAM,SAAS,YAAY,KAAK;AAChC,OAAI,mBACF,oBAAmB,MAAKL,YAAa;AAIvC,UAAO;IAAE;IAAS;IAAW;IAAU,SAFvB,YAAY,KAAK,GAAG;IAEY;YACxC;AACR,SAAKG,mBAAoB;AAQzB,OAAI,MAAKC,sBAAuB,MAAM;IACpC,MAAM,cAAc,MAAKA;AACzB,UAAKA,oBAAqB;AAE1B,SAAK,YAAY,aAAa,QAAQ,CAAC,YAAY,GAEjD;SAEF,OAAKF,qBAAsB;;;;;;;;;;;;;;;CAiBjC,sBAAsB,QAAmC;EACvD,MAAMI,SAA4B,EAAE;EACpC,MAAM,gBAAgB;EAEtB,MAAM,QAAQ,YAA2B;AAKvC,OAFmB,iBAAiB,WAAW,eAAe,SAE9C;IAGd,MAAM,UAAW,QAAqC,eAAe;IACrE,MAAM,QAAS,QAAmC,aAAa;AAG/D,QAAI,EAFwB,iBAAiB,WAAW,gBAAgB,OAItE;AAIF,QAAI,kBAAkB,QAAQ,CAC5B,QAAO,KAAK,QAAQ;UAEjB;AAKL,QAAI,mBAAmB,eAAe,QAAQ,MAAM,YAAY,OAC9D;AAGF,QAAI,kBAAkB,QAAQ,CAC5B,QAAO,KAAK,QAAQ;;GAKxB,MAAM,WAAW,MAAKC,4BAA6B,QAAQ;AAC3D,QAAK,MAAM,SAAS,SAClB,MAAK,MAAM;;AAIf,OAAK,MAAKP,YAAa;AACvB,SAAO;;;;;;;CAQT,6BAA6B,SAAqC;AAEhE,MAAI,QAAQ,YAAY;GACtB,MAAM,QAAQ,QAAQ,WAAW,iBAAiB,OAAO;AACzD,OAAI,MAAM,SAAS,GAAG;IACpB,MAAMQ,mBAA8B,EAAE;AACtC,SAAK,MAAM,QAAQ,MACjB,kBAAiB,KAAK,GAAG,KAAK,kBAAkB,CAAC;AAGnD,SAAK,MAAM,SAAS,QAAQ,WAAW,SACrC,KAAI,MAAM,YAAY,OACpB,kBAAiB,KAAK,MAAM;AAGhC,WAAO;;;AAKX,SAAO,QAAQ;;;;;CAMjB,IAAI,cAAuB;AACzB,SAAO,MAAKL"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Coordinates main-quality segment fetching across multiple video elements.
|
|
6
6
|
* Generic scheduler that doesn't understand media concepts (segments, renditions, etc.)
|
|
7
|
-
* - only processes { key, deadlineMs, fetch, owner } tuples.
|
|
7
|
+
* - only processes { key, deadlineMs, fetch, isCached, owner } tuples.
|
|
8
8
|
*
|
|
9
9
|
* Design principles:
|
|
10
10
|
* - Deadline-based ordering: always process nearest deadline first
|
|
@@ -17,6 +17,11 @@ interface UpgradeTask {
|
|
|
17
17
|
key: string;
|
|
18
18
|
/** Fetch function that populates the cache */
|
|
19
19
|
fetch: (signal: AbortSignal) => Promise<void>;
|
|
20
|
+
/** Returns true if the segment is still present in the cache. Used to skip
|
|
21
|
+
* re-queuing completed tasks whose segment is still cached, and to allow
|
|
22
|
+
* re-queuing when the segment has been LRU-evicted. If absent, a completed
|
|
23
|
+
* task is conservatively treated as still cached (skip re-queue). */
|
|
24
|
+
isCached?: () => boolean;
|
|
20
25
|
/** Timeline time when this segment will be needed */
|
|
21
26
|
deadlineMs: number;
|
|
22
27
|
/** Element ID, for bulk operations */
|
|
@@ -40,7 +45,6 @@ declare class QualityUpgradeScheduler {
|
|
|
40
45
|
constructor(options: {
|
|
41
46
|
requestFrameRender: () => void;
|
|
42
47
|
maxConcurrent?: number;
|
|
43
|
-
isCached?: (key: string) => boolean;
|
|
44
48
|
});
|
|
45
49
|
/**
|
|
46
50
|
* Add tasks without affecting existing ones (additive).
|
|
@@ -78,6 +82,11 @@ declare class QualityUpgradeScheduler {
|
|
|
78
82
|
* Dispose the scheduler - abort all in-flight work.
|
|
79
83
|
*/
|
|
80
84
|
dispose(): void;
|
|
85
|
+
/**
|
|
86
|
+
* Revive a disposed scheduler so it can accept new tasks after the host
|
|
87
|
+
* element is reconnected to the DOM (e.g. workbench reparenting).
|
|
88
|
+
*/
|
|
89
|
+
revive(): void;
|
|
81
90
|
}
|
|
82
91
|
//#endregion
|
|
83
92
|
export { QualityUpgradeScheduler };
|