@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EFMotionBlur.js","names":["result","u2","u3","EASING_KEYWORDS: Record<string, [number, number, number, number]>","seekable: Animation[]","currentTimes: number[]"],"sources":["../../src/elements/EFMotionBlur.ts"],"sourcesContent":["import { getTrackedAnimationsForSubtree } from \"./updateAnimations.js\";\n\n// ── Tuning constants ─────────────────────────────────────────────────────────\n// All magic numbers that control blur quality live here. Changing any value\n// is immediately visible as a single-location diff and can be reviewed\n// against the others.\n\n/** Multiplier on rotAmount → rotScale for centered displacement range. */\nexport const ROT_SCALE_FACTOR = 2;\n/** Multiplier on isotropicAmount → zoomScale. >2 amplifies zoom visibility;\n * higher values tilt the spiral angle toward radial. */\nexport const ZOOM_SCALE_FACTOR = 3;\n/** Maximum arc displacement in pixels (caps rotAmount). */\nconst MAX_ROT_AMOUNT_PX = 50;\n/** Maximum zoom displacement in pixels (caps isotropicAmount). */\nconst MAX_ZOOM_AMOUNT_PX = 50;\n/** Maximum translation displacement in pixels. */\nconst MAX_TRANS_AMOUNT_PX = 100;\n/** Gaussian sigma for temporal weights: sigma = max(n / SIGMA_DIVISOR, 2). */\nconst GAUSSIAN_SIGMA_DIVISOR = 5.5;\n/** Spiral output blur: coefficient on step spacing. Must stay small to\n * preserve zero-displacement center sharpness. */\nexport const SPIRAL_BLUR_COEFF = 0.15;\n/** Spiral output blur: hard cap in pixels. */\nexport const SPIRAL_BLUR_CAP = 0.8;\n/** Spiral output blur: floor in pixels. */\nconst SPIRAL_BLUR_FLOOR = 0.3;\n/** Translation output blur: coefficient on step spacing. Can be larger than\n * spiral because all pixels shift equally (no center-vs-edge gradient). */\nconst TRANS_BLUR_COEFF = 0.35;\n/** Translation output blur: hard cap in pixels. */\nconst TRANS_BLUR_CAP = 1.5;\n/** Translation output blur: floor in pixels. */\nconst TRANS_BLUR_FLOOR = 0.3;\n/** Spiral angle quantization: angles rounded to π/(12*2) ≈ 7.5° steps. */\nconst SPIRAL_ANGLE_QUANT = 12;\n\n/**\n * Extracts the 2D rotation angle (degrees) and uniform scale from a CSS\n * transform string. Handles `matrix(a,b,c,d,tx,ty)`, `rotate(Ndeg)`,\n * `scale(N)`, and `none`.\n */\nfunction parseMatrix(transform: string): { angle: number; scale: number } {\n if (!transform || transform === \"none\") return { angle: 0, scale: 1 };\n\n // matrix(a, b, c, d, tx, ty)\n const mm = transform.match(/^matrix\\(([^)]+)\\)/);\n if (mm) {\n const [a, b] = mm[1]!.split(\",\").map(Number) as [number, number, ...number[]];\n const scale = Math.sqrt(a! * a! + b! * b!);\n const angle = (Math.atan2(b!, a!) * 180) / Math.PI;\n return { angle, scale: scale || 1 };\n }\n\n // rotate(Ndeg) or rotate(Nrad)\n const rm = transform.match(/rotate\\(([^)]+)\\)/);\n if (rm) {\n const v = rm[1]!.trim();\n const angle = v.endsWith(\"rad\") ? (parseFloat(v) * 180) / Math.PI : parseFloat(v); // deg\n return { angle, scale: 1 };\n }\n\n // scale(N) or scale(N, N)\n const sm = transform.match(/^scale\\(([^)]+)\\)/);\n if (sm) {\n const parts = sm[1]!.split(\",\").map(Number);\n return { angle: 0, scale: parts[0] ?? 1 };\n }\n\n return { angle: 0, scale: 1 };\n}\n\n/**\n * Reads the rotation angle and scale from an element's current animation state.\n *\n * Prefers direct keyframe interpolation via `getComputedTiming().progress` +\n * `getKeyframes()` — this is synchronous and not subject to Chromium's deferred\n * WAAPI style application (where `getComputedStyle` may lag behind `currentTime`).\n *\n * Falls back to `commitStyles()` + `getComputedStyle` for animations whose keyframes\n * use complex transform lists that our simple interpolation can't handle.\n */\nfunction readTransformMatrix(\n child: HTMLElement,\n anims: Animation[],\n): { angle: number; scale: number } {\n // Preferred path: analytical interpolation from keyframes.\n // Works reliably for cancelled CSS animations whose getComputedStyle output\n // is deferred by Chromium until the next compositor frame.\n for (const anim of anims) {\n const effect = anim.effect;\n if (!(effect instanceof KeyframeEffect)) continue;\n const timing = effect.getComputedTiming();\n const progress = timing.progress;\n if (progress === null || progress === undefined) continue;\n const keyframes = effect.getKeyframes();\n const result = interpolateTransformKeyframes(keyframes, progress as number);\n if (result !== null) return result;\n }\n\n // Fallback: commitStyles() + getComputedStyle.\n let committed = false;\n for (const anim of anims) {\n try {\n (anim as any).commitStyles?.();\n committed = true;\n } catch (_) {\n /* fill:none or not replaceable */\n }\n }\n void child.getBoundingClientRect();\n const result = parseMatrix(getComputedStyle(child).transform);\n if (committed) child.style.removeProperty(\"transform\");\n return result;\n}\n\n/**\n * Decomposes a CSS transform string into cumulative rotation (degrees) and\n * uniform scale. Handles compound transforms like `scale(0.3) rotate(90deg)`,\n * pure rotations, pure scales, and matrix() values.\n *\n * Multiple rotate() functions are summed (so the orbit pattern\n * `rotate(360deg) translateX(90px) rotate(-360deg)` correctly yields 0°).\n *\n * Returns null only when no rotation or scale function is found and the\n * string is not a matrix.\n */\nfunction decomposeKeyframeTransform(transform: string): { angle: number; scale: number } | null {\n if (!transform || transform === \"none\") return { angle: 0, scale: 1 };\n\n const mm = transform.match(/^matrix\\(([^)]+)\\)/);\n if (mm) {\n const [a, b] = mm[1]!.split(\",\").map(Number) as [number, number, ...number[]];\n const scale = Math.sqrt(a! * a! + b! * b!);\n const angle = (Math.atan2(b!, a!) * 180) / Math.PI;\n return { angle, scale: scale || 1 };\n }\n\n let totalAngle = 0;\n let totalScale = 1;\n let found = false;\n\n for (const m of transform.matchAll(/rotate\\(([^)]+)\\)/g)) {\n const v = m[1]!.trim();\n totalAngle += v.endsWith(\"rad\") ? (parseFloat(v) * 180) / Math.PI : parseFloat(v);\n found = true;\n }\n\n const sm = transform.match(/scale\\(([^),]+)(?:,\\s*([^)]+))?\\)/);\n if (sm) {\n const sx = parseFloat(sm[1]!);\n const sy = sm[2] !== undefined ? parseFloat(sm[2]!) : sx;\n if (Math.abs(sx - sy) < 0.001) {\n totalScale = sx;\n found = true;\n }\n }\n\n return found ? { angle: totalAngle, scale: totalScale } : null;\n}\n\n/**\n * Solves a cubic-bezier curve: given control points (x1,y1,x2,y2) and input\n * parameter `t` (0–1), returns the output value y. Uses Newton-Raphson\n * iteration to invert the x(u) polynomial.\n */\nfunction solveCubicBezier(x1: number, y1: number, x2: number, y2: number, t: number): number {\n if (t <= 0) return 0;\n if (t >= 1) return 1;\n let u = t;\n for (let i = 0; i < 8; i++) {\n const u2 = u * u,\n u3 = u2 * u;\n const xu = 3 * (1 - u) * (1 - u) * u * x1 + 3 * (1 - u) * u2 * x2 + u3;\n const dxu = 3 * (1 - u) * (1 - u) * x1 + 6 * (1 - u) * u * (x2 - x1) + 3 * u2 * (1 - x2);\n if (Math.abs(dxu) < 1e-9) break;\n u -= (xu - t) / dxu;\n u = Math.max(0, Math.min(1, u));\n }\n const u2 = u * u,\n u3 = u2 * u;\n return 3 * (1 - u) * (1 - u) * u * y1 + 3 * (1 - u) * u2 * y2 + u3;\n}\n\nconst EASING_KEYWORDS: Record<string, [number, number, number, number]> = {\n ease: [0.25, 0.1, 0.25, 1.0],\n \"ease-in\": [0.42, 0, 1.0, 1.0],\n \"ease-out\": [0, 0, 0.58, 1.0],\n \"ease-in-out\": [0.42, 0, 0.58, 1.0],\n};\n\n/**\n * Applies a CSS easing function to a linear parameter t (0–1).\n * Handles keyword easings and `cubic-bezier(x1,y1,x2,y2)`.\n */\nfunction applyEasing(easing: string | undefined | null, t: number): number {\n if (!easing || easing === \"linear\") return t;\n const kw = EASING_KEYWORDS[easing];\n if (kw) return solveCubicBezier(kw[0], kw[1], kw[2], kw[3], t);\n const cbm = easing.match(/cubic-bezier\\(\\s*([^,]+),\\s*([^,]+),\\s*([^,]+),\\s*([^)]+)\\)/);\n if (cbm)\n return solveCubicBezier(\n parseFloat(cbm[1]!),\n parseFloat(cbm[2]!),\n parseFloat(cbm[3]!),\n parseFloat(cbm[4]!),\n t,\n );\n return t;\n}\n\n/**\n * Interpolates angle (degrees) and scale from a WAAPI keyframe list at the\n * given progress (0–1).\n *\n * Uses the original CSS value strings from the keyframes (not computed matrices)\n * so `rotate(0deg) → rotate(360deg)` interpolates correctly as 0→360°.\n *\n * Handles compound transforms (e.g. `scale(0.3) rotate(90deg)`) by decomposing\n * each keyframe individually, and applies per-keyframe easing so that blur\n * intensity tracks the actual eased velocity rather than the linear one.\n */\nfunction interpolateTransformKeyframes(\n keyframes: ComputedKeyframe[],\n progress: number,\n): { angle: number; scale: number } | null {\n const withTransform = keyframes.filter((k) => k.transform != null);\n if (withTransform.length < 2) return null;\n\n let lo = withTransform[0]!;\n let hi = withTransform[withTransform.length - 1]!;\n for (let i = 0; i < withTransform.length - 1; i++) {\n const a = withTransform[i]!;\n const b = withTransform[i + 1]!;\n const aOff = typeof a.offset === \"number\" ? a.offset : i / (withTransform.length - 1);\n const bOff = typeof b.offset === \"number\" ? b.offset : (i + 1) / (withTransform.length - 1);\n if (progress >= aOff && progress <= bOff) {\n lo = a;\n hi = b;\n break;\n }\n }\n\n const loOff = typeof lo.offset === \"number\" ? lo.offset : 0;\n const hiOff = typeof hi.offset === \"number\" ? hi.offset : 1;\n const linearT = hiOff === loOff ? 0 : (progress - loOff) / (hiOff - loOff);\n const t = applyEasing(lo.easing as string | undefined, linearT);\n\n const loD = decomposeKeyframeTransform(String(lo.transform ?? \"none\"));\n const hiD = decomposeKeyframeTransform(String(hi.transform ?? \"none\"));\n if (!loD || !hiD) return null;\n if (loD.angle === 0 && hiD.angle === 0 && loD.scale === 1 && hiD.scale === 1) return null;\n\n return {\n angle: loD.angle + (hiD.angle - loD.angle) * t,\n scale: loD.scale + (hiD.scale - loD.scale) * t,\n };\n}\n\n/**\n * Gaussian temporal weights for N blur steps.\n * Soft window (vs equal weights) eliminates the \"stack of discrete frames\" look.\n */\nfunction gaussianWeights(n: number): number[] {\n if (n <= 1) return [1];\n const mid = (n - 1) / 2;\n const sigma = Math.max(n / GAUSSIAN_SIGMA_DIVISOR, 2);\n return Array.from({ length: n }, (_, i) => Math.exp(-0.5 * ((i - mid) / sigma) ** 2));\n}\n\n/**\n * Maximum distance from a point to any corner of a rectangle.\n */\nfunction maxDistToCorner(cx: number, cy: number, w: number, h: number): number {\n let m = 0;\n for (const px of [0, w])\n for (const py of [0, h]) {\n const d = Math.hypot(px - cx, py - cy);\n if (d > m) m = d;\n }\n return m || 1;\n}\n\n/**\n * Generates a canvas displacement map encoding a **spiral** vector field.\n *\n * Each pixel encodes a 2D displacement direction as (R, G) where:\n * R = 128 + vx*127 G = 128 + vy*127\n *\n * The vector at each pixel is:\n * v = sin(spiralAngle) × tangent + cos(spiralAngle) × radial\n *\n * Because tangent ⊥ radial and both have magnitude r/maxR, the combined\n * vector also has magnitude r/maxR regardless of spiralAngle. This means\n * one map + one set of N displaced copies correctly captures simultaneous\n * rotation and zoom as a single spiral smear — no sequential stages needed.\n *\n * spiralAngle = atan2(rotScale, zoomScale):\n * π/2 → pure tangential (rotation only)\n * 0 → pure radial (zoom only)\n * between → spiral\n */\nfunction buildSpiralMap(\n pw: number,\n ph: number,\n ox: number,\n oy: number,\n iw: number,\n ih: number,\n cx: number,\n cy: number,\n spiralAngle: number,\n): string {\n const canvas = new OffscreenCanvas(pw, ph);\n const ctx = canvas.getContext(\"2d\")!;\n const img = ctx.createImageData(pw, ph);\n const maxR = maxDistToCorner(cx, cy, iw, ih);\n const sinA = Math.sin(spiralAngle);\n const cosA = Math.cos(spiralAngle);\n\n for (let py = 0; py < ph; py++) {\n for (let px = 0; px < pw; px++) {\n const ux = px - ox;\n const uy = py - oy;\n const i = (py * pw + px) * 4;\n const dx = ux - cx;\n const dy = uy - cy;\n const vx = sinA * (-dy / maxR) + cosA * (dx / maxR);\n const vy = sinA * (dx / maxR) + cosA * (dy / maxR);\n img.data[i] = Math.round(128 + vx * 127);\n img.data[i + 1] = Math.round(128 + vy * 127);\n img.data[i + 2] = 128;\n img.data[i + 3] = 255;\n }\n }\n ctx.putImageData(img, 0, 0);\n const c = document.createElement(\"canvas\");\n c.width = pw;\n c.height = ph;\n c.getContext(\"2d\")!.putImageData(img, 0, 0);\n return c.toDataURL();\n}\n\n/**\n * Appends the SVG filter primitives for one temporal displacement blur stage\n * into an existing filter string. Takes `inResult` as input and writes\n * the final blended output to `outResult`.\n *\n * When `centered=true`, steps run from -totalScale/2 → +totalScale/2, placing\n * the un-displaced copy at the center of the range. This is used for rotation\n * blur so the current frame sits in the middle of the smear.\n * When `centered=false` (default), steps run from 0 → totalScale.\n *\n * Produces N copies each displaced by the map at increasing scale,\n * blended with Gaussian temporal weights. A small feGaussianBlur softens slice edges.\n */\nfunction appendTemporalBlurStage(\n xml: string,\n prefix: string,\n inResult: string,\n outResult: string,\n steps: number,\n totalScale: number,\n mapResult: string,\n centered = false,\n): string {\n const weights = gaussianWeights(steps);\n // Minimal smoothing to merge adjacent displaced copies at the element edges.\n // Must be conservative because feGaussianBlur is isotropic — it softens the\n // CENTER (where displacement is zero and the image should stay sharp) just as\n // much as the edges. Gaussian temporal weighting already handles most blending.\n const stepSpacing = steps > 1 ? Math.abs(totalScale) / (steps - 1) / 2 : 0;\n const outBlur = Math.min(\n SPIRAL_BLUR_CAP,\n Math.max(SPIRAL_BLUR_FLOOR, stepSpacing * SPIRAL_BLUR_COEFF),\n );\n\n for (let i = 0; i < steps; i++) {\n const t = steps > 1 ? i / (steps - 1) : 0;\n const scale = centered ? ((t - 0.5) * totalScale).toFixed(4) : (t * totalScale).toFixed(4);\n xml += ` <feDisplacementMap in=\"${inResult}\" in2=\"${mapResult}\"\n scale=\"${scale}\" xChannelSelector=\"R\" yChannelSelector=\"G\"\n result=\"${prefix}s${i}\"/>\\n`;\n }\n\n xml += ` <feComposite in=\"${prefix}s0\" in2=\"${prefix}s0\" operator=\"arithmetic\"\n k1=\"0\" k2=\"1\" k3=\"0\" k4=\"0\" result=\"${prefix}avg0\"/>\\n`;\n let sumW = weights[0]!;\n for (let i = 1; i < steps; i++) {\n const sumWNew = sumW + weights[i]!;\n const k2 = (sumW / sumWNew).toFixed(8);\n const k3 = (weights[i]! / sumWNew).toFixed(8);\n xml += ` <feComposite in=\"${prefix}avg${i - 1}\" in2=\"${prefix}s${i}\" operator=\"arithmetic\"\n k1=\"0\" k2=\"${k2}\" k3=\"${k3}\" k4=\"0\" result=\"${prefix}avg${i}\"/>\\n`;\n sumW = sumWNew;\n }\n\n xml += ` <feGaussianBlur in=\"${prefix}avg${steps - 1}\"\n stdDeviation=\"${outBlur.toFixed(3)}\" result=\"${outResult}\"/>\\n`;\n return xml;\n}\n\n/**\n * Appends SVG filter primitives for a directional translation blur using\n * feOffset. Produces N copies of `inResult` shifted along (vx, vy) from\n * 0 → (totalDx, totalDy), blended with Gaussian temporal weights.\n * No wrapper rotation needed — direction is encoded directly as dx/dy offsets.\n */\nfunction appendTranslationBlurStage(\n xml: string,\n prefix: string,\n inResult: string,\n outResult: string,\n steps: number,\n totalDx: number,\n totalDy: number,\n): string {\n const weights = gaussianWeights(steps);\n const sweep = Math.sqrt(totalDx ** 2 + totalDy ** 2);\n // Translation: all pixels shift equally, so isotropic blur doesn't selectively\n // soften any region. Can tolerate more smoothing than the spiral stage.\n const stepSpacing = steps > 1 ? sweep / (steps - 1) : 0;\n const outBlur = Math.min(\n TRANS_BLUR_CAP,\n Math.max(TRANS_BLUR_FLOOR, stepSpacing * TRANS_BLUR_COEFF),\n );\n\n for (let i = 0; i < steps; i++) {\n const t = steps > 1 ? i / (steps - 1) : 0;\n const dx = (t * totalDx).toFixed(3);\n const dy = (t * totalDy).toFixed(3);\n xml += ` <feOffset in=\"${inResult}\" dx=\"${dx}\" dy=\"${dy}\" result=\"${prefix}s${i}\"/>\\n`;\n }\n\n xml += ` <feComposite in=\"${prefix}s0\" in2=\"${prefix}s0\" operator=\"arithmetic\"\n k1=\"0\" k2=\"1\" k3=\"0\" k4=\"0\" result=\"${prefix}avg0\"/>\\n`;\n let sumW = weights[0]!;\n for (let i = 1; i < steps; i++) {\n const sumWNew = sumW + weights[i]!;\n const k2 = (sumW / sumWNew).toFixed(8);\n const k3 = (weights[i]! / sumWNew).toFixed(8);\n xml += ` <feComposite in=\"${prefix}avg${i - 1}\" in2=\"${prefix}s${i}\" operator=\"arithmetic\"\n k1=\"0\" k2=\"${k2}\" k3=\"${k3}\" k4=\"0\" result=\"${prefix}avg${i}\"/>\\n`;\n sumW = sumWNew;\n }\n\n xml += ` <feGaussianBlur in=\"${prefix}avg${steps - 1}\"\n stdDeviation=\"${outBlur.toFixed(3)}\" result=\"${outResult}\"/>\\n`;\n return xml;\n}\n\n/**\n * Step-count limits for each render quality tier.\n *\n * \"preview\" — real-time playback in the workbench. Caps steps aggressively\n * to keep the SVG filter primitive count under ~30 per element so\n * imgLoad stays under 2ms for combined blur.\n *\n * \"render\" — final MP4 export. Uses the full step budget for smooth,\n * artifact-free motion blur at the cost of heavier SVG rendering.\n */\nconst STEP_LIMITS = {\n preview: { trans: 16, spiral: 16 },\n render: { trans: 31, spiral: 50 },\n} as const;\n\nfunction transSteps(sweepPx: number): number {\n if (sweepPx <= 0) return 2;\n const cap = STEP_LIMITS[EFMotionBlur.renderQuality].trans;\n return Math.max(2, Math.min(cap, Math.ceil(sweepPx) + 1));\n}\n\nfunction spiralSteps(sweepPx: number, rotDeg: number): number {\n const cap = STEP_LIMITS[EFMotionBlur.renderQuality].spiral;\n const fromPx = sweepPx > 0 ? Math.ceil(sweepPx / 1) + 1 : 2;\n const fromAngle = rotDeg > 0 ? Math.ceil(rotDeg / 0.65) + 1 : 2;\n return Math.max(2, Math.min(cap, Math.max(fromPx, fromAngle)));\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * `<ef-motionblur>` applies motion blur to its child for two types of motion:\n *\n * **Translation** — N `feOffset` copies along the velocity vector, Gaussian-blended.\n *\n * **Spiral** (rotation + zoom combined) — N displaced copies using a single\n * spiral vector map that encodes the weighted sum of tangential (rotation) and\n * radial (zoom) components. Because tangent ⊥ radial with equal magnitude,\n * the combined map always has magnitude r/maxR regardless of the rot/zoom ratio.\n * One pass correctly captures simultaneous rotation and scaling as a spiral\n * smear rather than the generic blur that sequential stages produce.\n */\nexport class EFMotionBlur extends HTMLElement {\n static observedAttributes = [\n \"angle\",\n \"amount\",\n \"shutter-angle\",\n \"fps\",\n \"sensitivity\",\n \"threshold\",\n ];\n\n /** Controls filter primitive density. \"preview\" for real-time playback, \"render\" for export. */\n static renderQuality: \"preview\" | \"render\" = \"preview\";\n\n private _outer: HTMLDivElement | null = null;\n private _filterEl: HTMLDivElement | null = null;\n private _svg: SVGSVGElement | null = null;\n private _filterId: string | null = null;\n\n // Cached spiral map data URI — rebuilt when element size or spiral angle changes\n private _spiralMapURI: string | null = null;\n private _lastMapW = 0;\n private _lastMapH = 0;\n private _lastSpiralAngle = NaN;\n\n // Cached SVG filter DOM nodes for attribute-only updates (avoids innerHTML rebuild)\n private _filterNode: SVGFilterElement | null = null;\n private _transOffsets: SVGFEOffsetElement[] = [];\n private _spiralDisplacements: SVGFEDisplacementMapElement[] = [];\n private _spiralFeImage: SVGFEImageElement | null = null;\n private _spiralBlur: SVGFEGaussianBlurElement | null = null;\n private _lastStepConfig = \"\"; // fingerprint of step counts for structural rebuild\n\n /** Translation blur: pixel displacement in X (screen space). */\n _computedDx: number = 0;\n /** Translation blur: pixel displacement in Y (screen space). */\n _computedDy: number = 0;\n _computedAngle: number = 0;\n _computedDirectionalAmount: number = 0;\n /** Signed rotation arc displacement (px at half-diagonal). Negative = CW, positive = CCW. */\n _computedRotAmount: number = 0;\n /** Absolute rotation delta in degrees (for step count calculation). */\n _computedRotDeg: number = 0;\n _computedIsotropicAmount: number = 0;\n\n _hasExplicitAngle: boolean = false;\n _hasExplicitAmount: boolean = false;\n\n get _computedAmount(): number {\n return this._computedDirectionalAmount;\n }\n set _computedAmount(v: number) {\n this._computedDirectionalAmount = v;\n }\n\n // ── Lifecycle ───────────────────────────────────────────────────────────────\n\n connectedCallback(): void {\n this._filterId = \"ef-mb-\" + Math.random().toString(36).slice(2);\n this._inject();\n this._update();\n }\n\n disconnectedCallback(): void {\n // Don't remove the SVG — it lives inside _outer which stays in our DOM tree.\n // If the element reconnects (e.g. after workbench auto-wrapping), _inject()\n // will re-acquire the reference. Removing it here caused filter:none because\n // _inject's querySelector(\"svg\") would find nothing on reconnect.\n }\n\n attributeChangedCallback(name: string, _old: string | null, newValue: string | null): void {\n if (name === \"angle\") this._hasExplicitAngle = newValue !== null;\n if (name === \"amount\") this._hasExplicitAmount = newValue !== null;\n if (this._outer) this._update();\n }\n\n // ── Public property API ─────────────────────────────────────────────────────\n\n get angle(): number {\n return parseFloat(this.getAttribute(\"angle\") ?? \"0\");\n }\n set angle(v: number) {\n this.setAttribute(\"angle\", String(v));\n }\n get amount(): number {\n return parseFloat(this.getAttribute(\"amount\") ?? \"0\");\n }\n set amount(v: number) {\n this.setAttribute(\"amount\", String(v));\n }\n get shutterAngle(): number {\n return parseFloat(this.getAttribute(\"shutter-angle\") ?? \"180\");\n }\n set shutterAngle(v: number) {\n this.setAttribute(\"shutter-angle\", String(v));\n }\n get fps(): number {\n return parseFloat(this.getAttribute(\"fps\") ?? \"30\");\n }\n set fps(v: number) {\n this.setAttribute(\"fps\", String(v));\n }\n get sensitivity(): number {\n return parseFloat(this.getAttribute(\"sensitivity\") ?? \"1\");\n }\n set sensitivity(v: number) {\n this.setAttribute(\"sensitivity\", String(v));\n }\n get threshold(): number {\n return parseFloat(this.getAttribute(\"threshold\") ?? \"0.5\");\n }\n set threshold(v: number) {\n this.setAttribute(\"threshold\", String(v));\n }\n\n get shutterMs(): number {\n return (this.shutterAngle / 360) * (1000 / this.fps);\n }\n\n // ── Private methods ─────────────────────────────────────────────────────────\n\n /**\n * One-time DOM setup. Creates the SVG filter host and two wrapper divs\n * (_outer for the SVG definition, _filterEl for the filtered content).\n * The SVG filter is rebuilt dynamically in _update().\n */\n private _inject(): void {\n const existingOuter = this.querySelector(\"[data-blur-outer]\") as HTMLDivElement | null;\n if (existingOuter) {\n this._outer = existingOuter;\n this._filterEl = existingOuter.querySelector(\"[data-blur-filter]\") as HTMLDivElement;\n this._svg = existingOuter.querySelector(\"svg\") as SVGSVGElement;\n if (!this._svg) {\n this._svg = document.createElementNS(\"http://www.w3.org/2000/svg\", \"svg\") as SVGSVGElement;\n this._svg.style.cssText =\n \"position:absolute;width:0;height:0;overflow:hidden;pointer-events:none\";\n existingOuter.insertBefore(this._svg, this._filterEl);\n }\n this._svg.innerHTML = `<defs><filter id=\"${this._filterId}\"></filter></defs>`;\n this.style.display = \"contents\";\n return;\n }\n\n this._svg = document.createElementNS(\"http://www.w3.org/2000/svg\", \"svg\") as SVGSVGElement;\n this._svg.style.cssText =\n \"position:absolute;width:0;height:0;overflow:hidden;pointer-events:none\";\n this._svg.innerHTML = `<defs><filter id=\"${this._filterId}\"></filter></defs>`;\n\n this._outer = document.createElement(\"div\");\n this._outer.dataset.blurOuter = \"\";\n this._outer.dataset.efStaticStyle =\n \"display:block;transform:none;transform-origin:center center\";\n this._outer.style.transformOrigin = \"center center\";\n\n this._filterEl = document.createElement(\"div\");\n this._filterEl.dataset.blurFilter = \"\";\n this._filterEl.style.transformOrigin = \"center center\";\n\n while (this.firstChild) this._filterEl.appendChild(this.firstChild);\n\n this._outer.appendChild(this._svg);\n this._outer.appendChild(this._filterEl);\n this.appendChild(this._outer);\n this.style.display = \"contents\";\n }\n\n /**\n * Rebuilds wrapper transforms and the SVG filter. Called on attribute changes\n * and after each sample().\n *\n * The SVG contains up to two filter stages:\n * 1. feOffset×N — directional (translation) blur\n * 2. Spiral (feImage + feDisplacementMap×N + feComposite blend + feGaussianBlur)\n * — combined rotation + zoom in one pass via a spiral vector map.\n *\n * On first call (or when step counts change), builds the full SVG filter DOM.\n * On subsequent calls, updates only the attributes that change per frame.\n */\n private _update(): void {\n if (!this._outer || !this._filterEl || !this._svg) return;\n\n this._outer.style.transform = \"none\";\n this._outer.style.display = \"block\";\n this._filterEl.style.transform = \"none\";\n this._filterEl.style.display = \"block\";\n\n const dx = this._hasExplicitAmount\n ? this.amount * Math.cos((this.angle * Math.PI) / 180)\n : this._computedDx;\n const dy = this._hasExplicitAmount\n ? this.amount * Math.sin((this.angle * Math.PI) / 180)\n : this._computedDy;\n const directionalMag = Math.sqrt(dx * dx + dy * dy);\n const rotAmount = this._hasExplicitAmount ? 0 : this._computedRotAmount;\n const isotropicAmount = this._hasExplicitAmount ? 0 : this._computedIsotropicAmount;\n\n const anyBlur =\n directionalMag > this.threshold ||\n Math.abs(rotAmount) > this.threshold ||\n isotropicAmount > this.threshold;\n this._filterEl.style.display = \"block\";\n\n if (!anyBlur) {\n this._filterEl.style.filter = \"none\";\n this._filterNode = null;\n this._svg.innerHTML = `<defs><filter id=\"${this._filterId}\"></filter></defs>`;\n if (this._outer) (this._outer as any)._cachedFilterSVG = undefined;\n return;\n }\n\n // Combined spiral: rotation (tangential) and zoom (radial) in one pass.\n // rotScale: physical rotation displacement (signed, ×2 for centered range)\n // zoomScale: mildly amplified zoom (×3 vs physical ×2). Stronger amplification\n // distorts the spiral angle for rotation-dominant animations, making arc\n // smears look radial. ×3 keeps the spiral close to physical while giving\n // zoom-only animations 50% more visible displacement.\n const rotScale = rotAmount * ROT_SCALE_FACTOR;\n const zoomScale = isotropicAmount * ZOOM_SCALE_FACTOR;\n const combinedScale = Math.sqrt(rotScale ** 2 + zoomScale ** 2);\n const spiralAngle = Math.atan2(rotScale, zoomScale);\n const anySpiralBlur = combinedScale > this.threshold;\n\n const child = (this._filterEl.firstElementChild as HTMLElement | null) ?? this._filterEl;\n const childRect = child.getBoundingClientRect();\n const filterElRect = this._filterEl.getBoundingClientRect();\n\n const filterElLogicalW = this._filterEl.offsetWidth || 1;\n const ancestorScale = filterElRect.width / filterElLogicalW || 1;\n\n const iw = (child as HTMLElement).offsetWidth || childRect.width || 100;\n const ih = (child as HTMLElement).offsetHeight || childRect.height || 100;\n\n const bw = childRect.width / ancestorScale;\n const bh = childRect.height / ancestorScale;\n const cx = ((childRect.left + childRect.right) / 2 - filterElRect.left) / ancestorScale;\n const cy = ((childRect.top + childRect.bottom) / 2 - filterElRect.top) / ancestorScale;\n\n const maxBBox = Math.ceil(Math.hypot(iw, ih));\n const mapPad = 64;\n const mapSide = maxBBox + 2 * mapPad;\n const mapContentOx = mapPad + Math.round((maxBBox - iw) / 2);\n const mapContentOy = mapPad + Math.round((maxBBox - ih) / 2);\n this._ensureMaps(mapSide, mapSide, mapContentOx, mapContentOy, iw, ih, spiralAngle);\n\n const halfDiag = Math.ceil(Math.hypot(bw, bh) / 2);\n const margin =\n halfDiag +\n Math.ceil(combinedScale / 2) +\n Math.ceil(Math.abs(dx)) +\n Math.ceil(Math.abs(dy)) +\n 64;\n const filterX = Math.floor(cx - margin);\n const filterY = Math.floor(cy - margin);\n const filterW = Math.ceil(margin * 2);\n const filterH = Math.ceil(margin * 2);\n const mapImgX = Math.round(cx - mapSide / 2);\n const mapImgY = Math.round(cy - mapSide / 2);\n\n const transStepCount = directionalMag > this.threshold ? transSteps(directionalMag) : 0;\n const spiralStepCount = anySpiralBlur ? spiralSteps(combinedScale, this._computedRotDeg) : 0;\n const stepConfig = `${transStepCount},${spiralStepCount}`;\n\n if (stepConfig !== this._lastStepConfig || !this._filterNode) {\n this._lastStepConfig = stepConfig;\n this._buildFilterDOM(transStepCount, spiralStepCount, mapSide);\n }\n\n const f = this._filterNode!;\n f.setAttribute(\"x\", String(filterX));\n f.setAttribute(\"y\", String(filterY));\n f.setAttribute(\"width\", String(filterW));\n f.setAttribute(\"height\", String(filterH));\n\n if (transStepCount > 0) {\n for (let i = 0; i < this._transOffsets.length; i++) {\n const t = this._transOffsets.length > 1 ? i / (this._transOffsets.length - 1) : 0;\n this._transOffsets[i]!.setAttribute(\"dx\", String(t * dx));\n this._transOffsets[i]!.setAttribute(\"dy\", String(t * dy));\n }\n }\n\n if (spiralStepCount > 0 && this._spiralFeImage) {\n this._spiralFeImage.setAttribute(\"x\", String(mapImgX));\n this._spiralFeImage.setAttribute(\"y\", String(mapImgY));\n if (this._spiralMapURI) {\n this._spiralFeImage.setAttribute(\"href\", this._spiralMapURI);\n }\n for (let i = 0; i < this._spiralDisplacements.length; i++) {\n const t =\n this._spiralDisplacements.length > 1 ? i / (this._spiralDisplacements.length - 1) : 0;\n this._spiralDisplacements[i]!.setAttribute(\"scale\", String((t - 0.5) * combinedScale));\n }\n const nSpiral = this._spiralDisplacements.length;\n const spiralSpacing = nSpiral > 1 ? combinedScale / (nSpiral - 1) / 2 : 0;\n this._spiralBlur?.setAttribute(\n \"stdDeviation\",\n String(\n Math.min(SPIRAL_BLUR_CAP, Math.max(SPIRAL_BLUR_FLOOR, spiralSpacing * SPIRAL_BLUR_COEFF)),\n ),\n );\n }\n\n this._filterEl.style.filter = `url(#${this._filterId})`;\n this._refreshSerializationCache();\n }\n\n /**\n * Refreshes the serialization cache on _outer. The serializer reads this\n * instead of calling XMLSerializer on the SVG defs, which is important\n * when rotation/zoom displacement maps embed large base64 data URIs.\n */\n private _refreshSerializationCache(): void {\n if (!this._filterNode || !this._outer) return;\n (this._outer as any)._cachedFilterSVG = new XMLSerializer().serializeToString(this._filterNode);\n }\n\n /**\n * Builds the full SVG filter DOM tree. Called once when step configuration changes.\n * Stores references to mutable elements for fast per-frame attribute updates.\n */\n private _buildFilterDOM(transStepCount: number, spiralStepCount: number, mapSide: number): void {\n const filterId = this._filterId!;\n\n let xml = `<filter id=\"${filterId}\" filterUnits=\"userSpaceOnUse\" primitiveUnits=\"userSpaceOnUse\" color-interpolation-filters=\"sRGB\">\\n`;\n let lastResult = \"SourceGraphic\";\n\n // ① Translation\n if (transStepCount > 0) {\n xml = appendTranslationBlurStage(xml, \"t\", lastResult, \"transblur\", transStepCount, 0, 0);\n lastResult = \"transblur\";\n }\n\n // ② Spiral (combined rotation + zoom)\n if (spiralStepCount > 0 && this._spiralMapURI) {\n xml += ` <feImage href=\"${this._spiralMapURI}\" preserveAspectRatio=\"none\" width=\"${mapSide}\" height=\"${mapSide}\" result=\"spiralmap\"/>\\n`;\n xml = appendTemporalBlurStage(\n xml,\n \"sp\",\n lastResult,\n \"spiralblur\",\n spiralStepCount,\n 0,\n \"spiralmap\",\n true,\n );\n lastResult = \"spiralblur\";\n }\n\n if (lastResult === \"SourceGraphic\") {\n xml += ` <feComposite in=\"SourceGraphic\" in2=\"SourceGraphic\" operator=\"arithmetic\" k1=\"0\" k2=\"1\" k3=\"0\" k4=\"0\"/>\\n`;\n }\n xml += `</filter>`;\n this._svg!.innerHTML = `<defs>${xml}</defs>`;\n\n this._filterNode = this._svg!.querySelector(\"filter\") as SVGFilterElement;\n this._transOffsets = Array.from(\n this._svg!.querySelectorAll(\"feOffset\"),\n ) as SVGFEOffsetElement[];\n this._spiralFeImage = this._svg!.querySelector(\n 'feImage[result=\"spiralmap\"]',\n ) as SVGFEImageElement | null;\n this._spiralDisplacements = Array.from(\n this._svg!.querySelectorAll('feDisplacementMap[result^=\"sps\"]'),\n ) as SVGFEDisplacementMapElement[];\n this._spiralBlur = this._svg!.querySelector(\n 'feGaussianBlur[result=\"spiralblur\"]',\n ) as SVGFEGaussianBlurElement | null;\n }\n\n /**\n * Generates or refreshes the spiral displacement map.\n * Cached by element size and spiral angle (quantized to ~5°).\n * The feImage href is updated in the fast path when the map URI changes.\n */\n private _ensureMaps(\n pw: number,\n ph: number,\n ox: number,\n oy: number,\n iw: number,\n ih: number,\n spiralAngle: number,\n ): void {\n const riw = Math.round(iw) || 100;\n const rih = Math.round(ih) || 100;\n const quantizedAngle = Math.round(spiralAngle * SPIRAL_ANGLE_QUANT) / SPIRAL_ANGLE_QUANT;\n\n const sizeChanged = Math.abs(riw - this._lastMapW) > 4 || Math.abs(rih - this._lastMapH) > 4;\n const angleChanged =\n Math.abs(quantizedAngle - this._lastSpiralAngle) > 0.001 || isNaN(this._lastSpiralAngle);\n\n if (!sizeChanged && !angleChanged) return;\n\n this._lastMapW = riw;\n this._lastMapH = rih;\n this._lastSpiralAngle = quantizedAngle;\n this._spiralMapURI = buildSpiralMap(pw, ph, ox, oy, riw, rih, riw / 2, rih / 2, quantizedAngle);\n }\n\n // ── Public API ──────────────────────────────────────────────────────────────\n\n /**\n * Samples the child's current position and transform, computing blur amounts.\n * Called by `updateAnimations` after all animation times are settled.\n */\n sample(_timestamp: number = performance.now()): void {\n if (this._hasExplicitAngle && this._hasExplicitAmount) return;\n if (!this._filterEl) return;\n\n const child = (this._filterEl.firstElementChild as HTMLElement | null) ?? this._filterEl;\n const det = this._computeDeterministicTransform(child);\n if (det !== null) {\n this._applyTransform(det.dx, det.dy, det.dAngle, det.dScale, det.halfDiag);\n }\n }\n\n /**\n * Deterministic two-seek measurement. Seeks all animations back by shutterMs,\n * reads positions and transforms, restores. Returns null when no seekable\n * animations exist or currentTime < shutterMs.\n */\n private _computeDeterministicTransform(child: HTMLElement): {\n dx: number;\n dy: number;\n dAngle: number;\n dScale: number;\n halfDiag: number;\n } | null {\n const tracked = getTrackedAnimationsForSubtree(child);\n const animations = (\n tracked.length > 0 ? tracked : child.getAnimations({ subtree: true })\n ) as Animation[];\n if (animations.length === 0) return null;\n\n const seekable: Animation[] = [];\n const currentTimes: number[] = [];\n for (const anim of animations) {\n const t = anim.currentTime;\n if (t !== null && typeof t === \"number\") {\n seekable.push(anim);\n currentTimes.push(t);\n }\n }\n if (seekable.length === 0) return null;\n\n const shutterMs = this.shutterMs;\n if (currentTimes.every((t) => t < shutterMs)) return null;\n\n // If all animations are in their fill phase (progress === null, activeDuration finite),\n // the element is stationary — no blur.\n const allInFill = seekable.every((anim) => {\n const timing = (anim.effect as KeyframeEffect)?.getComputedTiming();\n if (!timing) return false;\n const { activeDuration, progress } = timing;\n return progress === null && typeof activeDuration === \"number\" && isFinite(activeDuration);\n });\n if (allInFill) return null;\n\n const outerWas = this._outer!.style.transform;\n const filterWas = this._filterEl!.style.transform;\n this._outer!.style.transform = \"none\";\n this._filterEl!.style.transform = \"none\";\n\n // Ancestor-only scale from _filterEl (transform:none), not from the child\n // whose CSS animation rotation inflates its bbox.\n const filterElLogicalW = this._filterEl!.offsetWidth || 1;\n const filterElRect = this._filterEl!.getBoundingClientRect();\n const ancestorScale = filterElRect.width / filterElLogicalW || 1;\n\n // Read position and transform at t_now.\n const r1 = child.getBoundingClientRect();\n const cx1 = r1.left + r1.width / 2;\n const cy1 = r1.top + r1.height / 2;\n const halfDiag = Math.sqrt((child.offsetWidth || 1) ** 2 + (child.offsetHeight || 1) ** 2) / 2;\n const { angle: angle1, scale: scale1 } = readTransformMatrix(child, seekable);\n\n // Seek back to t_now - shutterMs, but never past the animation's active range.\n for (let i = 0; i < seekable.length; i++) {\n const timing = (seekable[i]!.effect as KeyframeEffect)?.getComputedTiming();\n const activeDuration = timing?.activeDuration;\n const endTime =\n typeof activeDuration === \"number\" && isFinite(activeDuration) ? activeDuration : Infinity;\n seekable[i]!.currentTime = Math.min(Math.max(0, currentTimes[i]! - shutterMs), endTime);\n }\n\n // Read position and transform at t_past.\n const r0 = child.getBoundingClientRect();\n const cx0 = r0.left + r0.width / 2;\n const cy0 = r0.top + r0.height / 2;\n const { angle: angle0, scale: scale0 } = readTransformMatrix(child, seekable);\n\n // Restore animation times and wrapper transforms.\n for (let i = 0; i < seekable.length; i++) {\n seekable[i]!.currentTime = currentTimes[i]!;\n }\n this._outer!.style.transform = outerWas;\n this._filterEl!.style.transform = filterWas;\n\n return {\n dx: (cx1 - cx0) / ancestorScale,\n dy: (cy1 - cy0) / ancestorScale,\n dAngle: angle1 - angle0,\n dScale: scale1 - scale0,\n halfDiag,\n };\n }\n\n /**\n * Converts measured deltas to blur amounts and calls _update() when changed.\n *\n * Translation → directional box blur amount + angle.\n * Rotation → arc displacement amount (signed: + = CCW, − = CW).\n * Arc length at half-diagonal radius: R × |Δθ_rad|.\n * Stored signed so the displacement map is applied in the\n * correct tangential direction.\n * Scale → radial displacement amount: |Δscale| × half-diagonal.\n */\n private _applyTransform(\n dx: number,\n dy: number,\n dAngle: number,\n dScale: number,\n halfDiag: number,\n ): void {\n const s = this.sensitivity;\n\n // Translation: store raw dx/dy scaled by sensitivity (capped at 100px)\n const mag = Math.sqrt(dx ** 2 + dy ** 2);\n const scaledMag = Math.min(mag * s, MAX_TRANS_AMOUNT_PX);\n const newDx = mag < this.threshold ? 0 : (dx / mag) * scaledMag;\n const newDy = mag < this.threshold ? 0 : (dy / mag) * scaledMag;\n const newDir = mag < this.threshold ? 0 : scaledMag;\n const newAngle =\n mag < this.threshold ? this._computedAngle : (Math.atan2(dy, dx) * 180) / Math.PI;\n\n // Rotation arc (signed)\n const dAngleRad = (dAngle * Math.PI) / 180;\n const arcLen = halfDiag * Math.abs(dAngleRad) * s;\n const newRot = Math.min(arcLen, MAX_ROT_AMOUNT_PX) * (dAngle > 0 ? -1 : 1);\n const newRotDeg = Math.abs(dAngle);\n\n // Scale zoom (same threshold as translation)\n const rawZoom = Math.abs(dScale) * halfDiag * s;\n const newZoom = rawZoom < this.threshold ? 0 : Math.min(rawZoom, MAX_ZOOM_AMOUNT_PX);\n\n const changed =\n newDx !== this._computedDx ||\n newDy !== this._computedDy ||\n newDir !== this._computedDirectionalAmount ||\n newAngle !== this._computedAngle ||\n newRot !== this._computedRotAmount ||\n newRotDeg !== this._computedRotDeg ||\n newZoom !== this._computedIsotropicAmount;\n\n if (!changed) return;\n\n this._computedDx = newDx;\n this._computedDy = newDy;\n this._computedDirectionalAmount = newDir;\n this._computedAngle = newAngle;\n this._computedRotAmount = newRot;\n this._computedRotDeg = newRotDeg;\n this._computedIsotropicAmount = newZoom;\n this._update();\n }\n}\n\ncustomElements.define(\"ef-motionblur\", EFMotionBlur);\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-motionblur\": EFMotionBlur;\n }\n}\n"],"mappings":"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"}
|
|
@@ -387,8 +387,7 @@ const EFTemporal = (superClass) => {
|
|
|
387
387
|
const offsetChanged = changedProperties.has("_offsetMs");
|
|
388
388
|
const durationChanged = changedProperties.has("_durationMs");
|
|
389
389
|
if (sourceChanged || trimChanged || becameReady || offsetChanged || durationChanged) {
|
|
390
|
-
|
|
391
|
-
if (this.rootTimegroup && !isRenderClone) this.rootTimegroup.requestFrameRender();
|
|
390
|
+
if (this.rootTimegroup && !this.rootTimegroup.isRenderClone) this.rootTimegroup.requestFrameRender();
|
|
392
391
|
else if (this.playbackController) this.playbackController.runThrottledFrameTask();
|
|
393
392
|
}
|
|
394
393
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EFTemporal.js","names":["isTimegroupCalculatingDurationFn: ((timegroup: EFTimegroup | undefined) => boolean) | null","fallbackFn: (timegroup: EFTimegroup | undefined) => boolean","elements: Array<TemporalMixinInterface & HTMLElement>","assignedElements: Element[]","temporalCache: Map<Element, TemporalMixinInterface[]>","host: EFTimegroup","temporal: TemporalMixinInterface & LitElement","#lastKnownTimeMs","#contentReadyState","state","#parentTimegroup","#ownCurrentTimeController","#rootTimegroupLocked","#loop","#parentTemporal","#offsetMs","#currentTimeMs"],"sources":["../../src/elements/EFTemporal.ts"],"sourcesContent":["import { consume, createContext } from \"@lit/context\";\nimport type { LitElement, ReactiveController } from \"lit\";\nimport { property, state } from \"lit/decorators.js\";\nimport { PlaybackController } from \"../gui/PlaybackController.js\";\nimport { durationConverter } from \"./durationConverter.js\";\nimport type { EFTimegroup } from \"./EFTimegroup.js\";\n// Lazy import to break circular dependency: EFTemporal -> EFTimegroup -> EFMedia -> EFTemporal\n// isTimegroupCalculatingDuration is only used at runtime in a getter, so we can import it lazily\n// Use a module-level variable that gets set when EFTimegroup module loads\nlet isTimegroupCalculatingDurationFn: ((timegroup: EFTimegroup | undefined) => boolean) | null =\n null;\n\n// This function will be called by EFTimegroup when it loads to register the function\nexport const registerIsTimegroupCalculatingDuration = (\n fn: (timegroup: EFTimegroup | undefined) => boolean,\n) => {\n isTimegroupCalculatingDurationFn = fn;\n};\n\nconst getIsTimegroupCalculatingDuration = (): ((timegroup: EFTimegroup | undefined) => boolean) => {\n if (isTimegroupCalculatingDurationFn) {\n return isTimegroupCalculatingDurationFn as (timegroup: EFTimegroup | undefined) => boolean;\n }\n\n // If not registered yet, try to import synchronously (only works if module is already loaded)\n // This is a fallback for cases where EFTimegroup hasn't called registerIsTimegroupCalculatingDuration\n // In practice, EFTimegroup will call registerIsTimegroupCalculatingDuration when it loads\n let fallbackFn: (timegroup: EFTimegroup | undefined) => boolean = () => false;\n try {\n // Access the function via a global or try to get it from the module cache\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const efTimegroupModule = (globalThis as any).__EFTimegroupModule;\n if (efTimegroupModule?.isTimegroupCalculatingDuration) {\n fallbackFn = efTimegroupModule.isTimegroupCalculatingDuration;\n }\n } catch {\n // Use default fallback\n }\n isTimegroupCalculatingDurationFn = fallbackFn;\n return fallbackFn;\n};\n\nexport const timegroupContext = createContext<EFTimegroup>(Symbol(\"timeGroupContext\"));\n\n// ============================================================================\n// Core Concept 1: Temporal Role\n// ============================================================================\n// A temporal element is either a root (controls its own playback) or a child\n// (delegates playback to its root timegroup). This is the fundamental invariant.\n// ============================================================================\n\n// ============================================================================\n// Core Concept 0: Content Readiness Protocol\n// ============================================================================\n// Every temporal element exposes a contentReadyState property and two events:\n// readystatechange — fires on state transitions (idle, loading, ready, error)\n// contentchange — fires when renderable output is invalidated\n//\n// The property solves late-subscriber: consumers check it on subscribe.\n// Events are non-bubbling: containers (timegroups) explicitly aggregate.\n// ============================================================================\n\nexport type ContentReadyState = \"idle\" | \"loading\" | \"ready\" | \"error\";\nexport type ContentChangeReason = \"source\" | \"bounds\" | \"structure\" | \"content\";\n\ntype TemporalRole = \"root\" | \"child\";\n\nfunction determineTemporalRole(parentTimegroup: EFTimegroup | undefined): TemporalRole {\n return parentTimegroup === undefined ? \"root\" : \"child\";\n}\n\n// ============================================================================\n// Core Concept 2: Duration Source\n// ============================================================================\n// Duration comes from one of three sources: intrinsic (media-based),\n// explicit (attribute), or inherited (from parent). This determines the base\n// duration before any modifications.\n// ============================================================================\n\ntype DurationSource = \"intrinsic\" | \"explicit\" | \"inherited\";\n\ninterface DurationSourceResult {\n source: DurationSource;\n baseDurationMs: number;\n}\n\nfunction determineDurationSource(\n intrinsicDurationMs: number | undefined,\n explicitDurationMs: number | undefined,\n parentDurationMs: number | undefined,\n): DurationSourceResult {\n if (intrinsicDurationMs !== undefined) {\n return { source: \"intrinsic\", baseDurationMs: intrinsicDurationMs };\n }\n if (explicitDurationMs !== undefined) {\n return { source: \"explicit\", baseDurationMs: explicitDurationMs };\n }\n if (parentDurationMs !== undefined) {\n return { source: \"inherited\", baseDurationMs: parentDurationMs };\n }\n return { source: \"inherited\", baseDurationMs: 0 };\n}\n\n// ============================================================================\n// Core Concept 3: Duration Modification Strategy\n// ============================================================================\n// Duration can be modified by trimming (removing from edges) or source\n// manipulation (selecting a portion of the source). These are mutually\n// exclusive strategies.\n// ============================================================================\n\ntype DurationModificationStrategy = \"none\" | \"trimming\" | \"source-manipulation\";\n\ninterface DurationModificationState {\n strategy: DurationModificationStrategy;\n trimStartMs: number | undefined;\n trimEndMs: number | undefined;\n sourceInMs: number | undefined;\n sourceOutMs: number | undefined;\n}\n\nfunction determineDurationModificationStrategy(\n trimStartMs: number | undefined,\n trimEndMs: number | undefined,\n sourceInMs: number | undefined,\n sourceOutMs: number | undefined,\n): DurationModificationState {\n if (trimStartMs !== undefined || trimEndMs !== undefined) {\n return {\n strategy: \"trimming\",\n trimStartMs,\n trimEndMs,\n sourceInMs: undefined,\n sourceOutMs: undefined,\n };\n }\n if (sourceInMs !== undefined || sourceOutMs !== undefined) {\n return {\n strategy: \"source-manipulation\",\n trimStartMs: undefined,\n trimEndMs: undefined,\n sourceInMs,\n sourceOutMs,\n };\n }\n return {\n strategy: \"none\",\n trimStartMs: undefined,\n trimEndMs: undefined,\n sourceInMs: undefined,\n sourceOutMs: undefined,\n };\n}\n\nfunction evaluateModifiedDuration(\n baseDurationMs: number,\n modification: DurationModificationState,\n): number {\n if (baseDurationMs === 0) {\n return 0;\n }\n\n switch (modification.strategy) {\n case \"trimming\": {\n const trimmedDurationMs =\n baseDurationMs - (modification.trimStartMs ?? 0) - (modification.trimEndMs ?? 0);\n return Math.max(0, trimmedDurationMs);\n }\n case \"source-manipulation\": {\n const sourceInMs = modification.sourceInMs ?? 0;\n const sourceOutMs = modification.sourceOutMs ?? baseDurationMs;\n if (sourceInMs >= sourceOutMs) {\n return 0;\n }\n return sourceOutMs - sourceInMs;\n }\n case \"none\":\n return baseDurationMs;\n }\n}\n\n// ============================================================================\n// Core Concept 4: Start Time Calculation Strategy\n// ============================================================================\n// Start time is calculated differently based on parent timegroup mode:\n// - Sequence mode: based on previous sibling\n// - Other modes: based on parent start + offset\n// ============================================================================\n\ntype StartTimeStrategy = \"sequence\" | \"offset\";\n\nfunction determineStartTimeStrategy(parentTimegroup: EFTimegroup | undefined): StartTimeStrategy {\n if (!parentTimegroup) {\n return \"offset\";\n }\n return parentTimegroup.mode === \"sequence\" ? \"sequence\" : \"offset\";\n}\n\nfunction evaluateStartTimeForSequence(\n _element: TemporalMixinInterface & HTMLElement,\n parentTimegroup: EFTimegroup,\n siblingTemporals: TemporalMixinInterface[],\n ownIndex: number,\n): number {\n if (ownIndex === 0) {\n return parentTimegroup.startTimeMs;\n }\n const previous = siblingTemporals[ownIndex - 1];\n if (!previous) {\n throw new Error(\"Previous temporal element not found\");\n }\n return previous.startTimeMs + previous.durationMs - parentTimegroup.overlapMs;\n}\n\nfunction evaluateStartTimeForOffset(parentTimegroup: EFTimegroup, offsetMs: number): number {\n return parentTimegroup.startTimeMs + offsetMs;\n}\n\nfunction evaluateStartTime(\n element: TemporalMixinInterface & HTMLElement,\n parentTimegroup: EFTimegroup | undefined,\n offsetMs: number,\n getSiblingTemporals: (parent: EFTimegroup) => TemporalMixinInterface[],\n): number {\n if (!parentTimegroup) {\n return 0;\n }\n\n const strategy = determineStartTimeStrategy(parentTimegroup);\n switch (strategy) {\n case \"sequence\": {\n const siblingTemporals = getSiblingTemporals(parentTimegroup);\n const ownIndex = siblingTemporals.indexOf(element);\n if (ownIndex === -1) {\n return 0;\n }\n return evaluateStartTimeForSequence(element, parentTimegroup, siblingTemporals, ownIndex);\n }\n case \"offset\":\n return evaluateStartTimeForOffset(parentTimegroup, offsetMs);\n }\n}\n\n// ============================================================================\n// Core Concept 5: Current Time Source\n// ============================================================================\n// Current time comes from one of three sources: playback controller (root\n// elements), root timegroup (child elements), or local storage (fallback).\n// ============================================================================\n\ntype CurrentTimeSource = \"playback-controller\" | \"root-timegroup\" | \"local\";\n\ninterface CurrentTimeSourceResult {\n source: CurrentTimeSource;\n timeMs: number;\n}\n\nfunction determineCurrentTimeSource(\n playbackController: PlaybackController | undefined,\n rootTimegroup: EFTimegroup | undefined,\n isRootTimegroup: boolean,\n localTimeMs: number,\n startTimeMs: number,\n durationMs: number,\n): CurrentTimeSourceResult {\n if (playbackController) {\n const timeMs = Math.min(Math.max(0, playbackController.currentTimeMs), durationMs);\n return { source: \"playback-controller\", timeMs };\n }\n\n if (rootTimegroup && !isRootTimegroup) {\n const timeMs = Math.min(Math.max(0, rootTimegroup.currentTimeMs - startTimeMs), durationMs);\n return { source: \"root-timegroup\", timeMs };\n }\n\n const timeMs = Math.min(Math.max(0, localTimeMs), durationMs);\n return { source: \"local\", timeMs };\n}\n\nexport declare class TemporalMixinInterface {\n playbackController?: PlaybackController;\n playing: boolean;\n loop: boolean;\n play(): void;\n pause(): void;\n\n get hasOwnDuration(): boolean;\n /**\n * Whether the element has a duration set as an attribute.\n */\n get hasExplicitDuration(): boolean;\n\n get sourceStartMs(): number;\n\n /**\n * Used to trim the start of the media.\n *\n * This can be set in either seconds or milliseconds.\n *\n * For example, `trimstart=\"10s\"` is equivalent to `trimstart=\"10000ms\"`.\n *\n * @domAttribute \"trimstart\"\n */\n get trimStartMs(): number | undefined;\n\n /**\n * Used to trim the end of the media.\n *\n * This can be set in either seconds or milliseconds.\n *\n * For example, `trimend=\"10s\"` is equivalent to `trimend=\"10000ms\"`.\n *\n * @domAttribute \"trimend\"\n */\n get trimEndMs(): number;\n\n set trimStartMs(value: number | undefined);\n set trimEndMs(value: number | undefined);\n set trimstart(value: string | undefined);\n set trimend(value: string | undefined);\n\n /**\n * The source in time of the element.\n *\n * This is an amount of time to trim off the beginning of the media.\n *\n * This can be set in either seconds or milliseconds.\n *\n * For example, `sourcein=\"10s\"` is equivalent to `sourcein=\"10000ms\"`.\n *\n * If the sourcein time is greater than the duration of the media, the media\n * will not be played.\n *\n * If the media is 20 seconds long, and the `sourcein` value is set to `10s`, the\n * media will play for 10 seconds, starting at the 10 second mark.\n *\n * Can be used in conjunction with `sourceout` to create a trimmed media.\n *\n * @domAttribute \"sourcein\"\n */\n get sourceInMs(): number | undefined;\n\n /**\n * The source out time of the element.\n *\n * This is the point in time in the media that will be treated as the end of\n * the media.\n *\n * This can be set in either seconds or milliseconds.\n *\n * For example, `sourceout=\"10s\"` is equivalent to `sourceout=\"10000ms\"`.\n *\n * If the sourceout time is greater than the duration of the media, the media\n * will play until the end of the media.\n *\n * If the media is 20 seconds long, and the `sourceout` value is set to `10s`,\n * the media will play for 10 seconds, starting at zero seconds and ending at\n * the 10 second mark.\n *\n * Can be used in conjunction with `sourcein` to create a trimmed media.\n *\n * @domAttribute \"sourceout\"\n */\n get sourceOutMs(): number | undefined;\n\n set sourceInMs(value: number | undefined);\n set sourceOutMs(value: number | undefined);\n set sourcein(value: string | undefined);\n set sourceout(value: string | undefined);\n\n /**\n * @domAttribute \"duration\"\n */\n get durationMs(): number;\n\n get explicitDurationMs(): number | undefined;\n\n get intrinsicDurationMs(): number | undefined;\n\n /**\n * The start time of the element within its root timegroup in milliseconds.\n *\n * This is an absolute time according to the highest scoped timegroup the media element is contained within.\n *\n * The calculated value will depend on the mode of the timegroup and the offset of the media element.\n *\n * If the parent time group is in `sequence` mode, the start time will be the\n * start time of the previous sibling element plus the previous sibling's duration\n * minus the overlap of the previous sibling and the current sibling.\n *\n * If the parent time group is in `contain` or `fixed` mode, the start time will be\n * the start time of the parent time group plus the offset of the media element.\n */\n get startTimeMs(): number;\n /**\n * The end time of the element within its root timegroup in milliseconds.\n *\n * This is an absolute time according to the highest scoped timegroup the media\n * element is contained within. Computed by adding the media's duration to its\n * start time.\n *\n * If the media element has been trimmed, its end time will be calculated according it\n * its trimmed duration, not its original duration.\n */\n get endTimeMs(): number;\n /**\n * The start time of the element within its parent timegroup in milliseconds.\n *\n * This is a relative time according to the closest timegroup the media element\n * is contained within. Unless the media element has been given any kind of specific offset\n * it is common for this time to be zero.\n */\n get startTimeWithinParentMs(): number;\n\n /**\n * The current time of the element in milliseconds.\n *\n * This is a relative time according to the closest timegroup the media element\n * is contained within.\n *\n * This is suitable for determining the percentage of the media that has been\n * played.\n */\n get ownCurrentTimeMs(): number;\n\n /**\n * Set the base local time (ms) used by ownCurrentTimeMs when no playback\n * controller is present. Used by seekForRender() on render clones.\n * @internal\n */\n _setLocalTimeMs(value: number): void;\n\n /**\n * Element's current time for progress calculation.\n * For timegroups: their timeline currentTimeMs\n * For other temporal elements: their ownCurrentTimeMs\n */\n get currentTimeMs(): number;\n set currentTimeMs(value: number);\n /**\n * The current time of the element in milliseconds, adjusted for trimming.\n *\n * This is suitable for mapping to internal media time codes for audio/video\n * elements.\n *\n * For example, if the media has a `sourcein` value of 10s, when `ownCurrentTimeMs` is 0s,\n * `currentSourceTimeMs` will be 10s.\n *\n * sourcein=10s sourceout=10s\n * / / /\n * |--------|=================|---------|\n * ^\n * |_\n * currentSourceTimeMs === 10s\n * |_\n * ownCurrentTimeMs === 0s\n */\n get currentSourceTimeMs(): number;\n\n set duration(value: string);\n get duration(): string;\n\n /**\n * The offset of the element within its parent timegroup in milliseconds.\n *\n * This can be set in either seconds or milliseconds.\n *\n * For example, `offset=\"10s\"` is equivalent to `offset=\"10000ms\"`.\n *\n * This can be used to create a negative or positive offset for the start time of the media.\n *\n * This will change the start time of the media relative to it's otherwise normal start time.\n *\n * The duration of the element, or it's parent, or the start and end time of it's temporal siblings will not\n * be affected by this offset.\n *\n * @domAttribute \"offset\"\n */\n set offset(value: string);\n get offset(): string;\n\n /**\n * A convenience property for getting the nearest containing timegroup of the media element.\n */\n parentTimegroup?: EFTimegroup;\n\n /**\n * A convenience property for getting the root timegroup of the media element.\n */\n rootTimegroup?: EFTimegroup;\n\n didBecomeRoot(): void;\n didBecomeChild(): void;\n\n /**\n * The readiness state of this element's content.\n * \"idle\" — no content / not connected\n * \"loading\" — async resources are loading\n * \"ready\" — element can render / extract frames\n * \"error\" — resource loading failed\n *\n * @domAttribute \"content-ready-state\"\n */\n contentReadyState: ContentReadyState;\n\n /**\n * Transition to a new readiness state.\n * Dispatches a non-bubbling `readystatechange` CustomEvent if the state changed.\n */\n setContentReadyState(state: ContentReadyState): void;\n\n /**\n * Dispatch a non-bubbling `contentchange` CustomEvent.\n * Signals that cached renderable output is stale.\n */\n emitContentChange(reason: ContentChangeReason): void;\n\n /**\n * Whether this element should auto-transition to \"ready\" after first update.\n * Override to return false for elements with async loading (EFMedia, EFCaptions).\n */\n shouldAutoReady(): boolean;\n\n updateComplete: Promise<boolean>;\n}\n\nexport const isEFTemporal = (obj: any): obj is TemporalMixinInterface => obj[EF_TEMPORAL];\n\nconst EF_TEMPORAL = Symbol(\"EF_TEMPORAL\");\n\nexport interface TemporalCollectionResult {\n /** Temporal elements to process (visible + pruned roots). */\n elements: Array<TemporalMixinInterface & HTMLElement>;\n /** Temporal elements whose subtrees were pruned (invisible containers). */\n pruned: Set<TemporalMixinInterface & HTMLElement>;\n}\n\nexport const deepGetTemporalElements = (\n element: Element,\n timeMs?: number,\n): TemporalCollectionResult => {\n const elements: Array<TemporalMixinInterface & HTMLElement> = [];\n const pruned = new Set<TemporalMixinInterface & HTMLElement>();\n\n const walk = (el: Element) => {\n const children = getChildrenIncludingSlotted(el);\n\n for (const child of children) {\n if (isEFTemporal(child)) {\n const temporal = child as TemporalMixinInterface & HTMLElement;\n elements.push(temporal);\n\n // Prune: if a time was provided and this temporal element is outside\n // its time range, skip its entire subtree. The caller is responsible\n // for setting display:none on these pruned roots; their children are\n // hidden by containment and never visited.\n if (timeMs !== undefined) {\n const startMs = temporal.startTimeMs;\n const endMs = temporal.endTimeMs;\n if (endMs > startMs && (timeMs < startMs || timeMs >= endMs)) {\n pruned.add(temporal);\n continue; // skip subtree\n }\n }\n }\n walk(child);\n }\n };\n\n walk(element);\n return { elements, pruned };\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 */\nconst getChildrenIncludingSlotted = (element: Element): 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 // Fallback to regular children\n return Array.from(element.children);\n};\n\nlet temporalCache: Map<Element, TemporalMixinInterface[]>;\nlet temporalCacheResetScheduled = false;\nexport const resetTemporalCache = () => {\n temporalCache = new Map();\n if (typeof requestAnimationFrame !== \"undefined\" && !temporalCacheResetScheduled) {\n temporalCacheResetScheduled = true;\n requestAnimationFrame(() => {\n temporalCacheResetScheduled = false;\n resetTemporalCache();\n });\n }\n};\nresetTemporalCache();\n\nexport const shallowGetTemporalElements = (\n element: Element,\n temporals: TemporalMixinInterface[] = [],\n) => {\n const cachedResult = temporalCache.get(element);\n if (cachedResult) {\n return cachedResult;\n }\n // Get children to walk - handle both regular children and slotted content\n const children = getChildrenIncludingSlotted(element);\n\n for (const child of children) {\n if (isEFTemporal(child)) {\n temporals.push(child);\n } else {\n shallowGetTemporalElements(child, temporals);\n }\n }\n temporalCache.set(element, temporals);\n return temporals;\n};\n\nexport class OwnCurrentTimeController implements ReactiveController {\n #lastKnownTimeMs: number | undefined = undefined;\n\n constructor(\n private host: EFTimegroup,\n private temporal: TemporalMixinInterface & LitElement,\n ) {\n host.addController(this);\n }\n\n hostUpdated() {\n // CRITICAL FIX: Only trigger child updates when root's currentTimeMs actually changes.\n // Previously, this fired on EVERY root update, causing 40+ child updates per root update.\n // With nested timegroups, this created cascading updates that locked up the main thread.\n const currentTimeMs = this.host.currentTimeMs;\n if (this.#lastKnownTimeMs === currentTimeMs) {\n return; // Time hasn't changed, no need to update children\n }\n this.#lastKnownTimeMs = currentTimeMs;\n\n // Defer update via queueMicrotask to avoid Lit warning about scheduling\n // updates during hostUpdated. Unlike setTimeout(0) this fires as a microtask,\n // so it resolves between await points without yielding a full macrotask turn\n // (eliminating 4-16ms dead time per frame in the render pipeline).\n queueMicrotask(() => {\n this.temporal.requestUpdate(\"ownCurrentTimeMs\");\n });\n }\n\n remove() {\n this.host.removeController(this);\n }\n}\n\ntype Constructor<T = {}> = new (...args: any[]) => T;\n\nlet startTimeMsCache = new WeakMap<Element, number>();\nlet startTimeMsCacheResetScheduled = false;\nconst resetStartTimeMsCache = () => {\n startTimeMsCache = new WeakMap();\n if (typeof requestAnimationFrame !== \"undefined\" && !startTimeMsCacheResetScheduled) {\n startTimeMsCacheResetScheduled = true;\n requestAnimationFrame(() => {\n startTimeMsCacheResetScheduled = false;\n resetStartTimeMsCache();\n });\n }\n};\nresetStartTimeMsCache();\n\nexport const flushStartTimeMsCache = () => {\n startTimeMsCache = new WeakMap();\n};\n\nexport const EFTemporal = <T extends Constructor<LitElement>>(superClass: T) => {\n class TemporalMixinClass extends superClass {\n // ---- Content Readiness Protocol ----\n\n #contentReadyState: ContentReadyState = \"idle\";\n\n @property({ type: String, reflect: true, attribute: \"content-ready-state\" })\n get contentReadyState(): ContentReadyState {\n return this.#contentReadyState;\n }\n\n set contentReadyState(value: ContentReadyState) {\n this.setContentReadyState(value);\n }\n\n setContentReadyState(state: ContentReadyState): void {\n if (state === this.#contentReadyState) return;\n const old = this.#contentReadyState;\n this.#contentReadyState = state;\n this.requestUpdate(\"contentReadyState\", old);\n this.dispatchEvent(\n new CustomEvent(\"readystatechange\", {\n detail: { state },\n bubbles: false,\n composed: false,\n }),\n );\n }\n\n emitContentChange(reason: ContentChangeReason): void {\n this.dispatchEvent(\n new CustomEvent(\"contentchange\", {\n detail: { reason },\n bubbles: false,\n composed: false,\n }),\n );\n }\n\n shouldAutoReady(): boolean {\n return true;\n }\n\n // ---- End Content Readiness Protocol ----\n\n #ownCurrentTimeController?: OwnCurrentTimeController;\n\n #parentTimegroup?: EFTimegroup;\n #rootTimegroupLocked = false; // When true, rootTimegroup won't be auto-recalculated\n\n @consume({ context: timegroupContext, subscribe: true })\n set parentTimegroup(value: EFTimegroup | undefined) {\n const oldParent = this.#parentTimegroup;\n const oldRole = determineTemporalRole(oldParent);\n const newRole = determineTemporalRole(value);\n\n this.#parentTimegroup = value;\n\n this.#ownCurrentTimeController?.remove();\n // Only auto-calculate rootTimegroup if it hasn't been locked\n // (locked means it was manually set, e.g., for render clones)\n if (!this.#rootTimegroupLocked) {\n this.rootTimegroup = this.getRootTimegroup();\n }\n if (this.rootTimegroup) {\n this.#ownCurrentTimeController = new OwnCurrentTimeController(\n this.rootTimegroup,\n this as InstanceType<Constructor<TemporalMixinInterface> & T>,\n );\n }\n\n // Only trigger callbacks if role actually changed\n if (oldRole !== newRole) {\n if (newRole === \"root\") {\n this.didBecomeRoot();\n } else {\n this.didBecomeChild();\n }\n }\n }\n\n /**\n * Lock the rootTimegroup to prevent auto-recalculation.\n * Used for render clones where the root must be fixed.\n * @internal\n */\n lockRootTimegroup() {\n this.#rootTimegroupLocked = true;\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n\n // Skip teardown when being moved for canvas preview capture.\n // The element is temporarily reparented to a capture canvas for native\n // rendering but must retain its PlaybackController and animation state.\n if ((this as any).canvasPreviewActive) return;\n\n this.#ownCurrentTimeController?.remove();\n\n if (this.playbackController) {\n this.playbackController.remove();\n this.playbackController = undefined;\n }\n\n // Clean up tracked animations to prevent memory leaks\n // Use dynamic import to avoid circular dependency with updateAnimations\n import(\"./updateAnimations.js\").then(({ cleanupTrackedAnimations }) => {\n cleanupTrackedAnimations(this);\n });\n }\n\n connectedCallback() {\n super.connectedCallback();\n\n // Skip re-initialization when being moved for canvas preview capture.\n // The element is temporarily reparented to a capture canvas for native\n // rendering — root detection and PlaybackController creation must be\n // skipped to avoid an infinite wrapWithWorkbench → initCanvasMode loop.\n if ((this as any).canvasPreviewActive) return;\n\n this.#ownCurrentTimeController?.remove();\n\n // Root detection: Check DOM structure to determine if this is truly a root.\n //\n // We can't rely on Lit Context (parentTimegroup) because context propagates\n // asynchronously during update cycles. Children may complete their first update\n // before ancestors have provided context, causing them to incorrectly think\n // they're roots.\n //\n // Instead, we check if there's an ancestor ef-timegroup in the DOM. This is\n // reliable because DOM structure is established synchronously at connection time.\n //\n // If there's NO ancestor timegroup, this is a true root → create PlaybackController.\n // If there IS an ancestor, wait for context to propagate (handled by parentTimegroup setter).\n // Note: closest() includes self, so we check from parentElement to find true ancestors.\n const hasAncestorTimegroup = this.parentElement?.closest(\"ef-timegroup\") != null;\n\n if (!hasAncestorTimegroup && !this.playbackController) {\n // True root: no ancestor timegroup in DOM\n // Defer slightly to allow element to fully initialize\n this.updateComplete.then(() => {\n if (!this.isConnected) return;\n if (!this.playbackController) {\n this.didBecomeRoot();\n }\n });\n }\n // For elements WITH ancestors, the parentTimegroup setter will be called\n // when Lit Context propagates, and if the role changes, didBecomeRoot/didBecomeChild\n // will be called appropriately.\n\n // Default readiness: trivially-ready elements (no async loading)\n // transition to \"ready\" after first update. Subclasses with async\n // loading (EFMedia, EFCaptions) override shouldAutoReady() to return\n // false and manage their own readiness lifecycle.\n if (this.shouldAutoReady()) {\n this.updateComplete.then(() => {\n if (!this.isConnected) return;\n if (this.#contentReadyState === \"idle\") {\n this.setContentReadyState(\"ready\");\n }\n });\n }\n }\n\n get parentTimegroup() {\n return this.#parentTimegroup;\n }\n\n playbackController?: PlaybackController;\n\n get playing(): boolean {\n if (!this.playbackController) {\n return false;\n }\n return this.playbackController.playing;\n }\n\n set playing(value: boolean) {\n if (!this.playbackController) {\n console.warn(\"Cannot set playing on non-root temporal element\", this);\n return;\n }\n this.playbackController.setPlaying(value);\n }\n\n play(): void {\n if (!this.playbackController) {\n console.warn(\"play() called on non-root temporal element\", this);\n return;\n }\n this.playbackController.play();\n }\n\n pause(): void {\n if (!this.playbackController) {\n console.warn(\"pause() called on non-root temporal element\", this);\n return;\n }\n this.playbackController.pause();\n }\n\n @property({ type: Boolean, reflect: true, attribute: \"loop\" })\n get loop(): boolean {\n return this.playbackController?.loop ?? this.#loop;\n }\n\n set loop(value: boolean) {\n const oldValue = this.#loop;\n this.#loop = value;\n if (this.playbackController) {\n this.playbackController.setLoop(value);\n }\n this.requestUpdate(\"loop\", oldValue);\n }\n\n @property({\n type: String,\n attribute: \"offset\",\n converter: durationConverter,\n })\n private _offsetMs = 0;\n\n @property({\n type: Number,\n attribute: \"duration\",\n converter: durationConverter,\n })\n private _durationMs?: number;\n\n set duration(value: string | undefined) {\n if (value !== undefined) {\n this.setAttribute(\"duration\", value);\n } else {\n this.removeAttribute(\"duration\");\n }\n }\n\n @property({\n type: Number,\n attribute: \"trimstart\",\n converter: durationConverter,\n })\n private _trimStartMs: number | undefined = undefined;\n\n get trimStartMs() {\n if (this._trimStartMs === undefined) {\n return undefined;\n }\n return Math.min(Math.max(this._trimStartMs, 0), this.intrinsicDurationMs ?? 0);\n }\n\n set trimStartMs(value: number | undefined) {\n this._trimStartMs = value;\n }\n\n @property({\n type: Number,\n attribute: \"trimend\",\n converter: durationConverter,\n })\n private _trimEndMs: number | undefined = undefined;\n\n get trimEndMs() {\n if (this._trimEndMs === undefined) {\n return undefined;\n }\n return Math.min(this._trimEndMs, this.intrinsicDurationMs ?? 0);\n }\n\n set trimEndMs(value: number | undefined) {\n this._trimEndMs = value;\n }\n\n @property({\n type: Number,\n attribute: \"sourcein\",\n converter: durationConverter,\n })\n private _sourceInMs: number | undefined = undefined;\n\n get sourceInMs() {\n if (this._sourceInMs === undefined) {\n return undefined;\n }\n return Math.max(this._sourceInMs, 0);\n }\n\n set sourceInMs(value: number | undefined) {\n this._sourceInMs = value;\n }\n\n @property({\n type: Number,\n attribute: \"sourceout\",\n converter: durationConverter,\n })\n private _sourceOutMs: number | undefined = undefined;\n\n get sourceOutMs() {\n if (this._sourceOutMs === undefined) {\n return undefined;\n }\n if (this.intrinsicDurationMs && this._sourceOutMs > this.intrinsicDurationMs) {\n return this.intrinsicDurationMs;\n }\n return Math.max(this._sourceOutMs, 0);\n }\n\n set sourceOutMs(value: number | undefined) {\n this._sourceOutMs = value;\n }\n\n override updated(changedProperties: Map<PropertyKey, unknown>): void {\n super.updated?.(changedProperties);\n\n // Re-render the current frame when source-mapping, trim, offset, or duration\n // properties change (the visible frame changes even though currentTimeMs hasn't).\n // Also render the initial frame when a standalone root becomes ready.\n const sourceChanged =\n changedProperties.has(\"_sourceInMs\") || changedProperties.has(\"_sourceOutMs\");\n const trimChanged =\n changedProperties.has(\"_trimStartMs\") || changedProperties.has(\"_trimEndMs\");\n const becameReady =\n changedProperties.has(\"contentReadyState\") &&\n changedProperties.get(\"contentReadyState\") !== \"ready\" &&\n this.contentReadyState === \"ready\";\n const offsetChanged = changedProperties.has(\"_offsetMs\");\n const durationChanged = changedProperties.has(\"_durationMs\");\n\n if (sourceChanged || trimChanged || becameReady || offsetChanged || durationChanged) {\n // Render clones are sequenced via seekForRender — don't trigger autonomous re-renders,\n // which would abort the in-progress seekForRender FrameController signal.\n const isRenderClone = this.rootTimegroup?.hasAttribute(\"data-no-playback-controller\");\n if (this.rootTimegroup && !isRenderClone) {\n this.rootTimegroup.requestFrameRender();\n } else if (this.playbackController) {\n this.playbackController.runThrottledFrameTask();\n }\n }\n }\n\n @property({\n type: Number,\n attribute: \"startoffset\",\n converter: durationConverter,\n })\n private _startOffsetMs = 0;\n public get startOffsetMs(): number {\n return this._startOffsetMs;\n }\n\n @state()\n rootTimegroup?: EFTimegroup = this.getRootTimegroup();\n\n private getRootTimegroup(): EFTimegroup | undefined {\n let parent = this.tagName === \"EF-TIMEGROUP\" ? this : this.parentTimegroup;\n while (parent?.parentTimegroup) {\n parent = parent.parentTimegroup;\n }\n return parent as EFTimegroup | undefined;\n }\n\n get hasExplicitDuration() {\n return this._durationMs !== undefined;\n }\n\n get explicitDurationMs() {\n if (this.hasExplicitDuration) {\n return this._durationMs;\n }\n return undefined;\n }\n\n get hasOwnDuration() {\n return this.intrinsicDurationMs !== undefined || this.hasExplicitDuration;\n }\n\n get intrinsicDurationMs() {\n return undefined;\n }\n\n get durationMs() {\n // Prevent infinite loops: don't call parent.durationMs if parent is currently calculating\n // Lazy import to break circular dependency: EFTemporal -> EFTimegroup -> EFMedia -> EFTemporal\n const isTimegroupCalculatingDuration = getIsTimegroupCalculatingDuration();\n const parentDurationMs = isTimegroupCalculatingDuration(this.parentTimegroup)\n ? undefined\n : this.parentTimegroup?.durationMs;\n const durationSource = determineDurationSource(\n this.intrinsicDurationMs,\n this._durationMs,\n parentDurationMs,\n );\n\n const modification = determineDurationModificationStrategy(\n this.trimStartMs,\n this.trimEndMs,\n this.sourceInMs,\n this.sourceOutMs,\n );\n\n return evaluateModifiedDuration(durationSource.baseDurationMs, modification);\n }\n\n get sourceStartMs() {\n return this.trimStartMs ?? this.sourceInMs ?? 0;\n }\n\n #offsetMs() {\n return this._offsetMs || 0;\n }\n\n #parentTemporal() {\n let parent = this.parentElement;\n while (parent && !isEFTemporal(parent)) {\n parent = parent.parentElement;\n }\n return parent;\n }\n\n /**\n * The start time of the element within its parent timegroup.\n */\n get startTimeWithinParentMs() {\n const parent = this.#parentTemporal();\n if (!parent) {\n return 0;\n }\n return this.startTimeMs - parent.startTimeMs;\n }\n\n #loop = false;\n\n get startTimeMs(): number {\n const cachedStartTime = startTimeMsCache.get(this);\n if (cachedStartTime !== undefined) {\n return cachedStartTime;\n }\n\n const startTime = evaluateStartTime(\n this as InstanceType<Constructor<TemporalMixinInterface> & T>,\n this.parentTimegroup,\n this.#offsetMs(),\n (parent) => shallowGetTemporalElements(parent),\n );\n\n startTimeMsCache.set(this, startTime);\n return startTime;\n }\n\n get endTimeMs(): number {\n return this.startTimeMs + this.durationMs;\n }\n\n #currentTimeMs = 0;\n\n /**\n * Set the base local time (ms) used by ownCurrentTimeMs when no playback\n * controller is present. Called by EFTimegroup.seekForRender() to keep the\n * mixin's internal time in sync with the timegroup's own time state.\n * @internal\n */\n _setLocalTimeMs(value: number) {\n this.#currentTimeMs = value;\n }\n\n /**\n * The current time of the element within itself.\n * Compare with `currentTimeMs` to see the current time with respect to the root timegroup\n */\n get ownCurrentTimeMs(): number {\n const timeSource = determineCurrentTimeSource(\n this.playbackController,\n this.rootTimegroup,\n this.rootTimegroup === (this as any as EFTimegroup),\n this.#currentTimeMs,\n this.startTimeMs,\n this.durationMs,\n );\n return timeSource.timeMs;\n }\n\n /**\n * Element's current time for progress calculation.\n * Non-timegroup temporal elements use their local time (ownCurrentTimeMs)\n */\n get currentTimeMs() {\n return this.ownCurrentTimeMs;\n }\n\n set currentTimeMs(value: number) {\n const role = determineTemporalRole(this.parentTimegroup);\n\n // Apply current time based on role\n switch (role) {\n case \"root\":\n if (this.playbackController) {\n this.playbackController.currentTime = value / 1000;\n } else {\n this.#currentTimeMs = value;\n this.requestUpdate(\"currentTimeMs\");\n }\n break;\n case \"child\":\n if (this.rootTimegroup && this.rootTimegroup !== (this as any as EFTimegroup)) {\n this.rootTimegroup.currentTimeMs = value;\n } else {\n this.#currentTimeMs = value;\n this.requestUpdate(\"currentTimeMs\");\n }\n break;\n }\n }\n\n /**\n * Used to calculate the internal currentTimeMs of the element. This is useful\n * for mapping to internal media time codes for audio/video elements.\n */\n get currentSourceTimeMs() {\n const leadingTrimMs = this.sourceInMs || this.trimStartMs || 0;\n return this.ownCurrentTimeMs + leadingTrimMs;\n }\n\n didBecomeRoot() {\n // Don't create PlaybackController if:\n // 1. Explicitly disabled via attribute (e.g., for render clones)\n // 2. Already exists\n // 3. In headless rendering mode (EF_FRAMEGEN active)\n const noPlayback = (this as any).hasAttribute?.(\"data-no-playback-controller\");\n const isRendering = typeof window !== \"undefined\" && \"FRAMEGEN_BRIDGE\" in window;\n if (noPlayback || this.playbackController || isRendering) {\n return;\n }\n\n this.playbackController = new PlaybackController(this as any);\n if (this.#loop) {\n this.playbackController.setLoop(this.#loop);\n }\n }\n\n didBecomeChild() {\n if (this.playbackController) {\n this.playbackController.remove();\n this.playbackController = undefined;\n }\n }\n }\n\n Object.defineProperty(TemporalMixinClass.prototype, EF_TEMPORAL, {\n value: true,\n });\n\n return TemporalMixinClass as unknown as Constructor<TemporalMixinInterface> & T;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EFTemporal.js","names":["isTimegroupCalculatingDurationFn: ((timegroup: EFTimegroup | undefined) => boolean) | null","fallbackFn: (timegroup: EFTimegroup | undefined) => boolean","elements: Array<TemporalMixinInterface & HTMLElement>","assignedElements: Element[]","temporalCache: Map<Element, TemporalMixinInterface[]>","host: EFTimegroup","temporal: TemporalMixinInterface & LitElement","#lastKnownTimeMs","#contentReadyState","state","#parentTimegroup","#ownCurrentTimeController","#rootTimegroupLocked","#loop","#parentTemporal","#offsetMs","#currentTimeMs"],"sources":["../../src/elements/EFTemporal.ts"],"sourcesContent":["import { consume, createContext } from \"@lit/context\";\nimport type { LitElement, ReactiveController } from \"lit\";\nimport { property, state } from \"lit/decorators.js\";\nimport { PlaybackController } from \"../gui/PlaybackController.js\";\nimport { durationConverter } from \"./durationConverter.js\";\nimport type { EFTimegroup } from \"./EFTimegroup.js\";\n// Lazy import to break circular dependency: EFTemporal -> EFTimegroup -> EFMedia -> EFTemporal\n// isTimegroupCalculatingDuration is only used at runtime in a getter, so we can import it lazily\n// Use a module-level variable that gets set when EFTimegroup module loads\nlet isTimegroupCalculatingDurationFn: ((timegroup: EFTimegroup | undefined) => boolean) | null =\n null;\n\n// This function will be called by EFTimegroup when it loads to register the function\nexport const registerIsTimegroupCalculatingDuration = (\n fn: (timegroup: EFTimegroup | undefined) => boolean,\n) => {\n isTimegroupCalculatingDurationFn = fn;\n};\n\nconst getIsTimegroupCalculatingDuration = (): ((timegroup: EFTimegroup | undefined) => boolean) => {\n if (isTimegroupCalculatingDurationFn) {\n return isTimegroupCalculatingDurationFn as (timegroup: EFTimegroup | undefined) => boolean;\n }\n\n // If not registered yet, try to import synchronously (only works if module is already loaded)\n // This is a fallback for cases where EFTimegroup hasn't called registerIsTimegroupCalculatingDuration\n // In practice, EFTimegroup will call registerIsTimegroupCalculatingDuration when it loads\n let fallbackFn: (timegroup: EFTimegroup | undefined) => boolean = () => false;\n try {\n // Access the function via a global or try to get it from the module cache\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const efTimegroupModule = (globalThis as any).__EFTimegroupModule;\n if (efTimegroupModule?.isTimegroupCalculatingDuration) {\n fallbackFn = efTimegroupModule.isTimegroupCalculatingDuration;\n }\n } catch {\n // Use default fallback\n }\n isTimegroupCalculatingDurationFn = fallbackFn;\n return fallbackFn;\n};\n\nexport const timegroupContext = createContext<EFTimegroup>(Symbol(\"timeGroupContext\"));\n\n// ============================================================================\n// Core Concept 1: Temporal Role\n// ============================================================================\n// A temporal element is either a root (controls its own playback) or a child\n// (delegates playback to its root timegroup). This is the fundamental invariant.\n// ============================================================================\n\n// ============================================================================\n// Core Concept 0: Content Readiness Protocol\n// ============================================================================\n// Every temporal element exposes a contentReadyState property and two events:\n// readystatechange — fires on state transitions (idle, loading, ready, error)\n// contentchange — fires when renderable output is invalidated\n//\n// The property solves late-subscriber: consumers check it on subscribe.\n// Events are non-bubbling: containers (timegroups) explicitly aggregate.\n// ============================================================================\n\nexport type ContentReadyState = \"idle\" | \"loading\" | \"ready\" | \"error\";\nexport type ContentChangeReason = \"source\" | \"bounds\" | \"structure\" | \"content\";\n\ntype TemporalRole = \"root\" | \"child\";\n\nfunction determineTemporalRole(parentTimegroup: EFTimegroup | undefined): TemporalRole {\n return parentTimegroup === undefined ? \"root\" : \"child\";\n}\n\n// ============================================================================\n// Core Concept 2: Duration Source\n// ============================================================================\n// Duration comes from one of three sources: intrinsic (media-based),\n// explicit (attribute), or inherited (from parent). This determines the base\n// duration before any modifications.\n// ============================================================================\n\ntype DurationSource = \"intrinsic\" | \"explicit\" | \"inherited\";\n\ninterface DurationSourceResult {\n source: DurationSource;\n baseDurationMs: number;\n}\n\nfunction determineDurationSource(\n intrinsicDurationMs: number | undefined,\n explicitDurationMs: number | undefined,\n parentDurationMs: number | undefined,\n): DurationSourceResult {\n if (intrinsicDurationMs !== undefined) {\n return { source: \"intrinsic\", baseDurationMs: intrinsicDurationMs };\n }\n if (explicitDurationMs !== undefined) {\n return { source: \"explicit\", baseDurationMs: explicitDurationMs };\n }\n if (parentDurationMs !== undefined) {\n return { source: \"inherited\", baseDurationMs: parentDurationMs };\n }\n return { source: \"inherited\", baseDurationMs: 0 };\n}\n\n// ============================================================================\n// Core Concept 3: Duration Modification Strategy\n// ============================================================================\n// Duration can be modified by trimming (removing from edges) or source\n// manipulation (selecting a portion of the source). These are mutually\n// exclusive strategies.\n// ============================================================================\n\ntype DurationModificationStrategy = \"none\" | \"trimming\" | \"source-manipulation\";\n\ninterface DurationModificationState {\n strategy: DurationModificationStrategy;\n trimStartMs: number | undefined;\n trimEndMs: number | undefined;\n sourceInMs: number | undefined;\n sourceOutMs: number | undefined;\n}\n\nfunction determineDurationModificationStrategy(\n trimStartMs: number | undefined,\n trimEndMs: number | undefined,\n sourceInMs: number | undefined,\n sourceOutMs: number | undefined,\n): DurationModificationState {\n if (trimStartMs !== undefined || trimEndMs !== undefined) {\n return {\n strategy: \"trimming\",\n trimStartMs,\n trimEndMs,\n sourceInMs: undefined,\n sourceOutMs: undefined,\n };\n }\n if (sourceInMs !== undefined || sourceOutMs !== undefined) {\n return {\n strategy: \"source-manipulation\",\n trimStartMs: undefined,\n trimEndMs: undefined,\n sourceInMs,\n sourceOutMs,\n };\n }\n return {\n strategy: \"none\",\n trimStartMs: undefined,\n trimEndMs: undefined,\n sourceInMs: undefined,\n sourceOutMs: undefined,\n };\n}\n\nfunction evaluateModifiedDuration(\n baseDurationMs: number,\n modification: DurationModificationState,\n): number {\n if (baseDurationMs === 0) {\n return 0;\n }\n\n switch (modification.strategy) {\n case \"trimming\": {\n const trimmedDurationMs =\n baseDurationMs - (modification.trimStartMs ?? 0) - (modification.trimEndMs ?? 0);\n return Math.max(0, trimmedDurationMs);\n }\n case \"source-manipulation\": {\n const sourceInMs = modification.sourceInMs ?? 0;\n const sourceOutMs = modification.sourceOutMs ?? baseDurationMs;\n if (sourceInMs >= sourceOutMs) {\n return 0;\n }\n return sourceOutMs - sourceInMs;\n }\n case \"none\":\n return baseDurationMs;\n }\n}\n\n// ============================================================================\n// Core Concept 4: Start Time Calculation Strategy\n// ============================================================================\n// Start time is calculated differently based on parent timegroup mode:\n// - Sequence mode: based on previous sibling\n// - Other modes: based on parent start + offset\n// ============================================================================\n\ntype StartTimeStrategy = \"sequence\" | \"offset\";\n\nfunction determineStartTimeStrategy(parentTimegroup: EFTimegroup | undefined): StartTimeStrategy {\n if (!parentTimegroup) {\n return \"offset\";\n }\n return parentTimegroup.mode === \"sequence\" ? \"sequence\" : \"offset\";\n}\n\nfunction evaluateStartTimeForSequence(\n _element: TemporalMixinInterface & HTMLElement,\n parentTimegroup: EFTimegroup,\n siblingTemporals: TemporalMixinInterface[],\n ownIndex: number,\n): number {\n if (ownIndex === 0) {\n return parentTimegroup.startTimeMs;\n }\n const previous = siblingTemporals[ownIndex - 1];\n if (!previous) {\n throw new Error(\"Previous temporal element not found\");\n }\n return previous.startTimeMs + previous.durationMs - parentTimegroup.overlapMs;\n}\n\nfunction evaluateStartTimeForOffset(parentTimegroup: EFTimegroup, offsetMs: number): number {\n return parentTimegroup.startTimeMs + offsetMs;\n}\n\nfunction evaluateStartTime(\n element: TemporalMixinInterface & HTMLElement,\n parentTimegroup: EFTimegroup | undefined,\n offsetMs: number,\n getSiblingTemporals: (parent: EFTimegroup) => TemporalMixinInterface[],\n): number {\n if (!parentTimegroup) {\n return 0;\n }\n\n const strategy = determineStartTimeStrategy(parentTimegroup);\n switch (strategy) {\n case \"sequence\": {\n const siblingTemporals = getSiblingTemporals(parentTimegroup);\n const ownIndex = siblingTemporals.indexOf(element);\n if (ownIndex === -1) {\n return 0;\n }\n return evaluateStartTimeForSequence(element, parentTimegroup, siblingTemporals, ownIndex);\n }\n case \"offset\":\n return evaluateStartTimeForOffset(parentTimegroup, offsetMs);\n }\n}\n\n// ============================================================================\n// Core Concept 5: Current Time Source\n// ============================================================================\n// Current time comes from one of three sources: playback controller (root\n// elements), root timegroup (child elements), or local storage (fallback).\n// ============================================================================\n\ntype CurrentTimeSource = \"playback-controller\" | \"root-timegroup\" | \"local\";\n\ninterface CurrentTimeSourceResult {\n source: CurrentTimeSource;\n timeMs: number;\n}\n\nfunction determineCurrentTimeSource(\n playbackController: PlaybackController | undefined,\n rootTimegroup: EFTimegroup | undefined,\n isRootTimegroup: boolean,\n localTimeMs: number,\n startTimeMs: number,\n durationMs: number,\n): CurrentTimeSourceResult {\n if (playbackController) {\n const timeMs = Math.min(Math.max(0, playbackController.currentTimeMs), durationMs);\n return { source: \"playback-controller\", timeMs };\n }\n\n if (rootTimegroup && !isRootTimegroup) {\n const timeMs = Math.min(Math.max(0, rootTimegroup.currentTimeMs - startTimeMs), durationMs);\n return { source: \"root-timegroup\", timeMs };\n }\n\n const timeMs = Math.min(Math.max(0, localTimeMs), durationMs);\n return { source: \"local\", timeMs };\n}\n\nexport declare class TemporalMixinInterface {\n playbackController?: PlaybackController;\n playing: boolean;\n loop: boolean;\n play(): void;\n pause(): void;\n\n get hasOwnDuration(): boolean;\n /**\n * Whether the element has a duration set as an attribute.\n */\n get hasExplicitDuration(): boolean;\n\n get sourceStartMs(): number;\n\n /**\n * Used to trim the start of the media.\n *\n * This can be set in either seconds or milliseconds.\n *\n * For example, `trimstart=\"10s\"` is equivalent to `trimstart=\"10000ms\"`.\n *\n * @domAttribute \"trimstart\"\n */\n get trimStartMs(): number | undefined;\n\n /**\n * Used to trim the end of the media.\n *\n * This can be set in either seconds or milliseconds.\n *\n * For example, `trimend=\"10s\"` is equivalent to `trimend=\"10000ms\"`.\n *\n * @domAttribute \"trimend\"\n */\n get trimEndMs(): number;\n\n set trimStartMs(value: number | undefined);\n set trimEndMs(value: number | undefined);\n set trimstart(value: string | undefined);\n set trimend(value: string | undefined);\n\n /**\n * The source in time of the element.\n *\n * This is an amount of time to trim off the beginning of the media.\n *\n * This can be set in either seconds or milliseconds.\n *\n * For example, `sourcein=\"10s\"` is equivalent to `sourcein=\"10000ms\"`.\n *\n * If the sourcein time is greater than the duration of the media, the media\n * will not be played.\n *\n * If the media is 20 seconds long, and the `sourcein` value is set to `10s`, the\n * media will play for 10 seconds, starting at the 10 second mark.\n *\n * Can be used in conjunction with `sourceout` to create a trimmed media.\n *\n * @domAttribute \"sourcein\"\n */\n get sourceInMs(): number | undefined;\n\n /**\n * The source out time of the element.\n *\n * This is the point in time in the media that will be treated as the end of\n * the media.\n *\n * This can be set in either seconds or milliseconds.\n *\n * For example, `sourceout=\"10s\"` is equivalent to `sourceout=\"10000ms\"`.\n *\n * If the sourceout time is greater than the duration of the media, the media\n * will play until the end of the media.\n *\n * If the media is 20 seconds long, and the `sourceout` value is set to `10s`,\n * the media will play for 10 seconds, starting at zero seconds and ending at\n * the 10 second mark.\n *\n * Can be used in conjunction with `sourcein` to create a trimmed media.\n *\n * @domAttribute \"sourceout\"\n */\n get sourceOutMs(): number | undefined;\n\n set sourceInMs(value: number | undefined);\n set sourceOutMs(value: number | undefined);\n set sourcein(value: string | undefined);\n set sourceout(value: string | undefined);\n\n /**\n * @domAttribute \"duration\"\n */\n get durationMs(): number;\n\n get explicitDurationMs(): number | undefined;\n\n get intrinsicDurationMs(): number | undefined;\n\n /**\n * The start time of the element within its root timegroup in milliseconds.\n *\n * This is an absolute time according to the highest scoped timegroup the media element is contained within.\n *\n * The calculated value will depend on the mode of the timegroup and the offset of the media element.\n *\n * If the parent time group is in `sequence` mode, the start time will be the\n * start time of the previous sibling element plus the previous sibling's duration\n * minus the overlap of the previous sibling and the current sibling.\n *\n * If the parent time group is in `contain` or `fixed` mode, the start time will be\n * the start time of the parent time group plus the offset of the media element.\n */\n get startTimeMs(): number;\n /**\n * The end time of the element within its root timegroup in milliseconds.\n *\n * This is an absolute time according to the highest scoped timegroup the media\n * element is contained within. Computed by adding the media's duration to its\n * start time.\n *\n * If the media element has been trimmed, its end time will be calculated according it\n * its trimmed duration, not its original duration.\n */\n get endTimeMs(): number;\n /**\n * The start time of the element within its parent timegroup in milliseconds.\n *\n * This is a relative time according to the closest timegroup the media element\n * is contained within. Unless the media element has been given any kind of specific offset\n * it is common for this time to be zero.\n */\n get startTimeWithinParentMs(): number;\n\n /**\n * The current time of the element in milliseconds.\n *\n * This is a relative time according to the closest timegroup the media element\n * is contained within.\n *\n * This is suitable for determining the percentage of the media that has been\n * played.\n */\n get ownCurrentTimeMs(): number;\n\n /**\n * Set the base local time (ms) used by ownCurrentTimeMs when no playback\n * controller is present. Used by seekForRender() on render clones.\n * @internal\n */\n _setLocalTimeMs(value: number): void;\n\n /**\n * Element's current time for progress calculation.\n * For timegroups: their timeline currentTimeMs\n * For other temporal elements: their ownCurrentTimeMs\n */\n get currentTimeMs(): number;\n set currentTimeMs(value: number);\n /**\n * The current time of the element in milliseconds, adjusted for trimming.\n *\n * This is suitable for mapping to internal media time codes for audio/video\n * elements.\n *\n * For example, if the media has a `sourcein` value of 10s, when `ownCurrentTimeMs` is 0s,\n * `currentSourceTimeMs` will be 10s.\n *\n * sourcein=10s sourceout=10s\n * / / /\n * |--------|=================|---------|\n * ^\n * |_\n * currentSourceTimeMs === 10s\n * |_\n * ownCurrentTimeMs === 0s\n */\n get currentSourceTimeMs(): number;\n\n set duration(value: string);\n get duration(): string;\n\n /**\n * The offset of the element within its parent timegroup in milliseconds.\n *\n * This can be set in either seconds or milliseconds.\n *\n * For example, `offset=\"10s\"` is equivalent to `offset=\"10000ms\"`.\n *\n * This can be used to create a negative or positive offset for the start time of the media.\n *\n * This will change the start time of the media relative to it's otherwise normal start time.\n *\n * The duration of the element, or it's parent, or the start and end time of it's temporal siblings will not\n * be affected by this offset.\n *\n * @domAttribute \"offset\"\n */\n set offset(value: string);\n get offset(): string;\n\n /**\n * A convenience property for getting the nearest containing timegroup of the media element.\n */\n parentTimegroup?: EFTimegroup;\n\n /**\n * A convenience property for getting the root timegroup of the media element.\n */\n rootTimegroup?: EFTimegroup;\n\n didBecomeRoot(): void;\n didBecomeChild(): void;\n\n /**\n * The readiness state of this element's content.\n * \"idle\" — no content / not connected\n * \"loading\" — async resources are loading\n * \"ready\" — element can render / extract frames\n * \"error\" — resource loading failed\n *\n * @domAttribute \"content-ready-state\"\n */\n contentReadyState: ContentReadyState;\n\n /**\n * Transition to a new readiness state.\n * Dispatches a non-bubbling `readystatechange` CustomEvent if the state changed.\n */\n setContentReadyState(state: ContentReadyState): void;\n\n /**\n * Dispatch a non-bubbling `contentchange` CustomEvent.\n * Signals that cached renderable output is stale.\n */\n emitContentChange(reason: ContentChangeReason): void;\n\n /**\n * Whether this element should auto-transition to \"ready\" after first update.\n * Override to return false for elements with async loading (EFMedia, EFCaptions).\n */\n shouldAutoReady(): boolean;\n\n updateComplete: Promise<boolean>;\n}\n\nexport const isEFTemporal = (obj: any): obj is TemporalMixinInterface => obj[EF_TEMPORAL];\n\nconst EF_TEMPORAL = Symbol(\"EF_TEMPORAL\");\n\nexport interface TemporalCollectionResult {\n /** Temporal elements to process (visible + pruned roots). */\n elements: Array<TemporalMixinInterface & HTMLElement>;\n /** Temporal elements whose subtrees were pruned (invisible containers). */\n pruned: Set<TemporalMixinInterface & HTMLElement>;\n}\n\nexport const deepGetTemporalElements = (\n element: Element,\n timeMs?: number,\n): TemporalCollectionResult => {\n const elements: Array<TemporalMixinInterface & HTMLElement> = [];\n const pruned = new Set<TemporalMixinInterface & HTMLElement>();\n\n const walk = (el: Element) => {\n const children = getChildrenIncludingSlotted(el);\n\n for (const child of children) {\n if (isEFTemporal(child)) {\n const temporal = child as TemporalMixinInterface & HTMLElement;\n elements.push(temporal);\n\n // Prune: if a time was provided and this temporal element is outside\n // its time range, skip its entire subtree. The caller is responsible\n // for setting display:none on these pruned roots; their children are\n // hidden by containment and never visited.\n if (timeMs !== undefined) {\n const startMs = temporal.startTimeMs;\n const endMs = temporal.endTimeMs;\n if (endMs > startMs && (timeMs < startMs || timeMs >= endMs)) {\n pruned.add(temporal);\n continue; // skip subtree\n }\n }\n }\n walk(child);\n }\n };\n\n walk(element);\n return { elements, pruned };\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 */\nconst getChildrenIncludingSlotted = (element: Element): 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 // Fallback to regular children\n return Array.from(element.children);\n};\n\nlet temporalCache: Map<Element, TemporalMixinInterface[]>;\nlet temporalCacheResetScheduled = false;\nexport const resetTemporalCache = () => {\n temporalCache = new Map();\n if (typeof requestAnimationFrame !== \"undefined\" && !temporalCacheResetScheduled) {\n temporalCacheResetScheduled = true;\n requestAnimationFrame(() => {\n temporalCacheResetScheduled = false;\n resetTemporalCache();\n });\n }\n};\nresetTemporalCache();\n\nexport const shallowGetTemporalElements = (\n element: Element,\n temporals: TemporalMixinInterface[] = [],\n) => {\n const cachedResult = temporalCache.get(element);\n if (cachedResult) {\n return cachedResult;\n }\n // Get children to walk - handle both regular children and slotted content\n const children = getChildrenIncludingSlotted(element);\n\n for (const child of children) {\n if (isEFTemporal(child)) {\n temporals.push(child);\n } else {\n shallowGetTemporalElements(child, temporals);\n }\n }\n temporalCache.set(element, temporals);\n return temporals;\n};\n\nexport class OwnCurrentTimeController implements ReactiveController {\n #lastKnownTimeMs: number | undefined = undefined;\n\n constructor(\n private host: EFTimegroup,\n private temporal: TemporalMixinInterface & LitElement,\n ) {\n host.addController(this);\n }\n\n hostUpdated() {\n // CRITICAL FIX: Only trigger child updates when root's currentTimeMs actually changes.\n // Previously, this fired on EVERY root update, causing 40+ child updates per root update.\n // With nested timegroups, this created cascading updates that locked up the main thread.\n const currentTimeMs = this.host.currentTimeMs;\n if (this.#lastKnownTimeMs === currentTimeMs) {\n return; // Time hasn't changed, no need to update children\n }\n this.#lastKnownTimeMs = currentTimeMs;\n\n // Defer update via queueMicrotask to avoid Lit warning about scheduling\n // updates during hostUpdated. Unlike setTimeout(0) this fires as a microtask,\n // so it resolves between await points without yielding a full macrotask turn\n // (eliminating 4-16ms dead time per frame in the render pipeline).\n queueMicrotask(() => {\n this.temporal.requestUpdate(\"ownCurrentTimeMs\");\n });\n }\n\n remove() {\n this.host.removeController(this);\n }\n}\n\ntype Constructor<T = {}> = new (...args: any[]) => T;\n\nlet startTimeMsCache = new WeakMap<Element, number>();\nlet startTimeMsCacheResetScheduled = false;\nconst resetStartTimeMsCache = () => {\n startTimeMsCache = new WeakMap();\n if (typeof requestAnimationFrame !== \"undefined\" && !startTimeMsCacheResetScheduled) {\n startTimeMsCacheResetScheduled = true;\n requestAnimationFrame(() => {\n startTimeMsCacheResetScheduled = false;\n resetStartTimeMsCache();\n });\n }\n};\nresetStartTimeMsCache();\n\nexport const flushStartTimeMsCache = () => {\n startTimeMsCache = new WeakMap();\n};\n\nexport const EFTemporal = <T extends Constructor<LitElement>>(superClass: T) => {\n class TemporalMixinClass extends superClass {\n // ---- Content Readiness Protocol ----\n\n #contentReadyState: ContentReadyState = \"idle\";\n\n @property({ type: String, reflect: true, attribute: \"content-ready-state\" })\n get contentReadyState(): ContentReadyState {\n return this.#contentReadyState;\n }\n\n set contentReadyState(value: ContentReadyState) {\n this.setContentReadyState(value);\n }\n\n setContentReadyState(state: ContentReadyState): void {\n if (state === this.#contentReadyState) return;\n const old = this.#contentReadyState;\n this.#contentReadyState = state;\n this.requestUpdate(\"contentReadyState\", old);\n this.dispatchEvent(\n new CustomEvent(\"readystatechange\", {\n detail: { state },\n bubbles: false,\n composed: false,\n }),\n );\n }\n\n emitContentChange(reason: ContentChangeReason): void {\n this.dispatchEvent(\n new CustomEvent(\"contentchange\", {\n detail: { reason },\n bubbles: false,\n composed: false,\n }),\n );\n }\n\n shouldAutoReady(): boolean {\n return true;\n }\n\n // ---- End Content Readiness Protocol ----\n\n #ownCurrentTimeController?: OwnCurrentTimeController;\n\n #parentTimegroup?: EFTimegroup;\n #rootTimegroupLocked = false; // When true, rootTimegroup won't be auto-recalculated\n\n @consume({ context: timegroupContext, subscribe: true })\n set parentTimegroup(value: EFTimegroup | undefined) {\n const oldParent = this.#parentTimegroup;\n const oldRole = determineTemporalRole(oldParent);\n const newRole = determineTemporalRole(value);\n\n this.#parentTimegroup = value;\n\n this.#ownCurrentTimeController?.remove();\n // Only auto-calculate rootTimegroup if it hasn't been locked\n // (locked means it was manually set, e.g., for render clones)\n if (!this.#rootTimegroupLocked) {\n this.rootTimegroup = this.getRootTimegroup();\n }\n if (this.rootTimegroup) {\n this.#ownCurrentTimeController = new OwnCurrentTimeController(\n this.rootTimegroup,\n this as InstanceType<Constructor<TemporalMixinInterface> & T>,\n );\n }\n\n // Only trigger callbacks if role actually changed\n if (oldRole !== newRole) {\n if (newRole === \"root\") {\n this.didBecomeRoot();\n } else {\n this.didBecomeChild();\n }\n }\n }\n\n /**\n * Lock the rootTimegroup to prevent auto-recalculation.\n * Used for render clones where the root must be fixed.\n * @internal\n */\n lockRootTimegroup() {\n this.#rootTimegroupLocked = true;\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n\n // Skip teardown when being moved for canvas preview capture.\n // The element is temporarily reparented to a capture canvas for native\n // rendering but must retain its PlaybackController and animation state.\n if ((this as any).canvasPreviewActive) return;\n\n this.#ownCurrentTimeController?.remove();\n\n if (this.playbackController) {\n this.playbackController.remove();\n this.playbackController = undefined;\n }\n\n // Clean up tracked animations to prevent memory leaks\n // Use dynamic import to avoid circular dependency with updateAnimations\n import(\"./updateAnimations.js\").then(({ cleanupTrackedAnimations }) => {\n cleanupTrackedAnimations(this);\n });\n }\n\n connectedCallback() {\n super.connectedCallback();\n\n // Skip re-initialization when being moved for canvas preview capture.\n // The element is temporarily reparented to a capture canvas for native\n // rendering — root detection and PlaybackController creation must be\n // skipped to avoid an infinite wrapWithWorkbench → initCanvasMode loop.\n if ((this as any).canvasPreviewActive) return;\n\n this.#ownCurrentTimeController?.remove();\n\n // Root detection: Check DOM structure to determine if this is truly a root.\n //\n // We can't rely on Lit Context (parentTimegroup) because context propagates\n // asynchronously during update cycles. Children may complete their first update\n // before ancestors have provided context, causing them to incorrectly think\n // they're roots.\n //\n // Instead, we check if there's an ancestor ef-timegroup in the DOM. This is\n // reliable because DOM structure is established synchronously at connection time.\n //\n // If there's NO ancestor timegroup, this is a true root → create PlaybackController.\n // If there IS an ancestor, wait for context to propagate (handled by parentTimegroup setter).\n // Note: closest() includes self, so we check from parentElement to find true ancestors.\n const hasAncestorTimegroup = this.parentElement?.closest(\"ef-timegroup\") != null;\n\n if (!hasAncestorTimegroup && !this.playbackController) {\n // True root: no ancestor timegroup in DOM\n // Defer slightly to allow element to fully initialize\n this.updateComplete.then(() => {\n if (!this.isConnected) return;\n if (!this.playbackController) {\n this.didBecomeRoot();\n }\n });\n }\n // For elements WITH ancestors, the parentTimegroup setter will be called\n // when Lit Context propagates, and if the role changes, didBecomeRoot/didBecomeChild\n // will be called appropriately.\n\n // Default readiness: trivially-ready elements (no async loading)\n // transition to \"ready\" after first update. Subclasses with async\n // loading (EFMedia, EFCaptions) override shouldAutoReady() to return\n // false and manage their own readiness lifecycle.\n if (this.shouldAutoReady()) {\n this.updateComplete.then(() => {\n if (!this.isConnected) return;\n if (this.#contentReadyState === \"idle\") {\n this.setContentReadyState(\"ready\");\n }\n });\n }\n }\n\n get parentTimegroup() {\n return this.#parentTimegroup;\n }\n\n playbackController?: PlaybackController;\n\n get playing(): boolean {\n if (!this.playbackController) {\n return false;\n }\n return this.playbackController.playing;\n }\n\n set playing(value: boolean) {\n if (!this.playbackController) {\n console.warn(\"Cannot set playing on non-root temporal element\", this);\n return;\n }\n this.playbackController.setPlaying(value);\n }\n\n play(): void {\n if (!this.playbackController) {\n console.warn(\"play() called on non-root temporal element\", this);\n return;\n }\n this.playbackController.play();\n }\n\n pause(): void {\n if (!this.playbackController) {\n console.warn(\"pause() called on non-root temporal element\", this);\n return;\n }\n this.playbackController.pause();\n }\n\n @property({ type: Boolean, reflect: true, attribute: \"loop\" })\n get loop(): boolean {\n return this.playbackController?.loop ?? this.#loop;\n }\n\n set loop(value: boolean) {\n const oldValue = this.#loop;\n this.#loop = value;\n if (this.playbackController) {\n this.playbackController.setLoop(value);\n }\n this.requestUpdate(\"loop\", oldValue);\n }\n\n @property({\n type: String,\n attribute: \"offset\",\n converter: durationConverter,\n })\n private _offsetMs = 0;\n\n @property({\n type: Number,\n attribute: \"duration\",\n converter: durationConverter,\n })\n private _durationMs?: number;\n\n set duration(value: string | undefined) {\n if (value !== undefined) {\n this.setAttribute(\"duration\", value);\n } else {\n this.removeAttribute(\"duration\");\n }\n }\n\n @property({\n type: Number,\n attribute: \"trimstart\",\n converter: durationConverter,\n })\n private _trimStartMs: number | undefined = undefined;\n\n get trimStartMs() {\n if (this._trimStartMs === undefined) {\n return undefined;\n }\n return Math.min(Math.max(this._trimStartMs, 0), this.intrinsicDurationMs ?? 0);\n }\n\n set trimStartMs(value: number | undefined) {\n this._trimStartMs = value;\n }\n\n @property({\n type: Number,\n attribute: \"trimend\",\n converter: durationConverter,\n })\n private _trimEndMs: number | undefined = undefined;\n\n get trimEndMs() {\n if (this._trimEndMs === undefined) {\n return undefined;\n }\n return Math.min(this._trimEndMs, this.intrinsicDurationMs ?? 0);\n }\n\n set trimEndMs(value: number | undefined) {\n this._trimEndMs = value;\n }\n\n @property({\n type: Number,\n attribute: \"sourcein\",\n converter: durationConverter,\n })\n private _sourceInMs: number | undefined = undefined;\n\n get sourceInMs() {\n if (this._sourceInMs === undefined) {\n return undefined;\n }\n return Math.max(this._sourceInMs, 0);\n }\n\n set sourceInMs(value: number | undefined) {\n this._sourceInMs = value;\n }\n\n @property({\n type: Number,\n attribute: \"sourceout\",\n converter: durationConverter,\n })\n private _sourceOutMs: number | undefined = undefined;\n\n get sourceOutMs() {\n if (this._sourceOutMs === undefined) {\n return undefined;\n }\n if (this.intrinsicDurationMs && this._sourceOutMs > this.intrinsicDurationMs) {\n return this.intrinsicDurationMs;\n }\n return Math.max(this._sourceOutMs, 0);\n }\n\n set sourceOutMs(value: number | undefined) {\n this._sourceOutMs = value;\n }\n\n override updated(changedProperties: Map<PropertyKey, unknown>): void {\n super.updated?.(changedProperties);\n\n // Re-render the current frame when source-mapping, trim, offset, or duration\n // properties change (the visible frame changes even though currentTimeMs hasn't).\n // Also render the initial frame when a standalone root becomes ready.\n const sourceChanged =\n changedProperties.has(\"_sourceInMs\") || changedProperties.has(\"_sourceOutMs\");\n const trimChanged =\n changedProperties.has(\"_trimStartMs\") || changedProperties.has(\"_trimEndMs\");\n const becameReady =\n changedProperties.has(\"contentReadyState\") &&\n changedProperties.get(\"contentReadyState\") !== \"ready\" &&\n this.contentReadyState === \"ready\";\n const offsetChanged = changedProperties.has(\"_offsetMs\");\n const durationChanged = changedProperties.has(\"_durationMs\");\n\n if (sourceChanged || trimChanged || becameReady || offsetChanged || durationChanged) {\n // Render clones are sequenced via seekForRender — don't trigger autonomous re-renders,\n // which would abort the in-progress seekForRender FrameController signal.\n if (this.rootTimegroup && !this.rootTimegroup.isRenderClone) {\n this.rootTimegroup.requestFrameRender();\n } else if (this.playbackController) {\n this.playbackController.runThrottledFrameTask();\n }\n }\n }\n\n @property({\n type: Number,\n attribute: \"startoffset\",\n converter: durationConverter,\n })\n private _startOffsetMs = 0;\n public get startOffsetMs(): number {\n return this._startOffsetMs;\n }\n\n @state()\n rootTimegroup?: EFTimegroup = this.getRootTimegroup();\n\n private getRootTimegroup(): EFTimegroup | undefined {\n let parent = this.tagName === \"EF-TIMEGROUP\" ? this : this.parentTimegroup;\n while (parent?.parentTimegroup) {\n parent = parent.parentTimegroup;\n }\n return parent as EFTimegroup | undefined;\n }\n\n get hasExplicitDuration() {\n return this._durationMs !== undefined;\n }\n\n get explicitDurationMs() {\n if (this.hasExplicitDuration) {\n return this._durationMs;\n }\n return undefined;\n }\n\n get hasOwnDuration() {\n return this.intrinsicDurationMs !== undefined || this.hasExplicitDuration;\n }\n\n get intrinsicDurationMs() {\n return undefined;\n }\n\n get durationMs() {\n // Prevent infinite loops: don't call parent.durationMs if parent is currently calculating\n // Lazy import to break circular dependency: EFTemporal -> EFTimegroup -> EFMedia -> EFTemporal\n const isTimegroupCalculatingDuration = getIsTimegroupCalculatingDuration();\n const parentDurationMs = isTimegroupCalculatingDuration(this.parentTimegroup)\n ? undefined\n : this.parentTimegroup?.durationMs;\n const durationSource = determineDurationSource(\n this.intrinsicDurationMs,\n this._durationMs,\n parentDurationMs,\n );\n\n const modification = determineDurationModificationStrategy(\n this.trimStartMs,\n this.trimEndMs,\n this.sourceInMs,\n this.sourceOutMs,\n );\n\n return evaluateModifiedDuration(durationSource.baseDurationMs, modification);\n }\n\n get sourceStartMs() {\n return this.trimStartMs ?? this.sourceInMs ?? 0;\n }\n\n #offsetMs() {\n return this._offsetMs || 0;\n }\n\n #parentTemporal() {\n let parent = this.parentElement;\n while (parent && !isEFTemporal(parent)) {\n parent = parent.parentElement;\n }\n return parent;\n }\n\n /**\n * The start time of the element within its parent timegroup.\n */\n get startTimeWithinParentMs() {\n const parent = this.#parentTemporal();\n if (!parent) {\n return 0;\n }\n return this.startTimeMs - parent.startTimeMs;\n }\n\n #loop = false;\n\n get startTimeMs(): number {\n const cachedStartTime = startTimeMsCache.get(this);\n if (cachedStartTime !== undefined) {\n return cachedStartTime;\n }\n\n const startTime = evaluateStartTime(\n this as InstanceType<Constructor<TemporalMixinInterface> & T>,\n this.parentTimegroup,\n this.#offsetMs(),\n (parent) => shallowGetTemporalElements(parent),\n );\n\n startTimeMsCache.set(this, startTime);\n return startTime;\n }\n\n get endTimeMs(): number {\n return this.startTimeMs + this.durationMs;\n }\n\n #currentTimeMs = 0;\n\n /**\n * Set the base local time (ms) used by ownCurrentTimeMs when no playback\n * controller is present. Called by EFTimegroup.seekForRender() to keep the\n * mixin's internal time in sync with the timegroup's own time state.\n * @internal\n */\n _setLocalTimeMs(value: number) {\n this.#currentTimeMs = value;\n }\n\n /**\n * The current time of the element within itself.\n * Compare with `currentTimeMs` to see the current time with respect to the root timegroup\n */\n get ownCurrentTimeMs(): number {\n const timeSource = determineCurrentTimeSource(\n this.playbackController,\n this.rootTimegroup,\n this.rootTimegroup === (this as any as EFTimegroup),\n this.#currentTimeMs,\n this.startTimeMs,\n this.durationMs,\n );\n return timeSource.timeMs;\n }\n\n /**\n * Element's current time for progress calculation.\n * Non-timegroup temporal elements use their local time (ownCurrentTimeMs)\n */\n get currentTimeMs() {\n return this.ownCurrentTimeMs;\n }\n\n set currentTimeMs(value: number) {\n const role = determineTemporalRole(this.parentTimegroup);\n\n // Apply current time based on role\n switch (role) {\n case \"root\":\n if (this.playbackController) {\n this.playbackController.currentTime = value / 1000;\n } else {\n this.#currentTimeMs = value;\n this.requestUpdate(\"currentTimeMs\");\n }\n break;\n case \"child\":\n if (this.rootTimegroup && this.rootTimegroup !== (this as any as EFTimegroup)) {\n this.rootTimegroup.currentTimeMs = value;\n } else {\n this.#currentTimeMs = value;\n this.requestUpdate(\"currentTimeMs\");\n }\n break;\n }\n }\n\n /**\n * Used to calculate the internal currentTimeMs of the element. This is useful\n * for mapping to internal media time codes for audio/video elements.\n */\n get currentSourceTimeMs() {\n const leadingTrimMs = this.sourceInMs || this.trimStartMs || 0;\n return this.ownCurrentTimeMs + leadingTrimMs;\n }\n\n didBecomeRoot() {\n // Don't create PlaybackController if:\n // 1. Explicitly disabled via attribute (e.g., for render clones)\n // 2. Already exists\n // 3. In headless rendering mode (EF_FRAMEGEN active)\n const noPlayback = (this as any).hasAttribute?.(\"data-no-playback-controller\");\n const isRendering = typeof window !== \"undefined\" && \"FRAMEGEN_BRIDGE\" in window;\n if (noPlayback || this.playbackController || isRendering) {\n return;\n }\n\n this.playbackController = new PlaybackController(this as any);\n if (this.#loop) {\n this.playbackController.setLoop(this.#loop);\n }\n }\n\n didBecomeChild() {\n if (this.playbackController) {\n this.playbackController.remove();\n this.playbackController = undefined;\n }\n }\n }\n\n Object.defineProperty(TemporalMixinClass.prototype, EF_TEMPORAL, {\n value: true,\n });\n\n return TemporalMixinClass as unknown as Constructor<TemporalMixinInterface> & T;\n};\n"],"mappings":"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"}
|
|
@@ -15,6 +15,20 @@ declare class EFText extends EFText_base {
|
|
|
15
15
|
staggerMs?: number;
|
|
16
16
|
private validateStagger;
|
|
17
17
|
easing: string;
|
|
18
|
+
/** Wraps each segment in an `<ef-motionblur>` element for auto-detected directional blur. */
|
|
19
|
+
motionBlur: boolean;
|
|
20
|
+
/** Forwarded to `ef-motionblur[sensitivity]`. */
|
|
21
|
+
blurSensitivity?: number;
|
|
22
|
+
/** Forwarded to `ef-motionblur[shutter-angle]`. */
|
|
23
|
+
blurShutterAngle?: number;
|
|
24
|
+
/** Forwarded to `ef-motionblur[fps]`. */
|
|
25
|
+
blurFps?: number;
|
|
26
|
+
/** Forwarded to `ef-motionblur[threshold]`. */
|
|
27
|
+
blurThreshold?: number;
|
|
28
|
+
/** Forwarded to `ef-motionblur[angle]` (manual override, suspends auto-detection). */
|
|
29
|
+
blurAngle?: number;
|
|
30
|
+
/** Forwarded to `ef-motionblur[amount]` (manual override, suspends auto-detection). */
|
|
31
|
+
blurAmount?: number;
|
|
18
32
|
private mutationObserver?;
|
|
19
33
|
private animationObserver?;
|
|
20
34
|
private lastTextContent;
|
|
@@ -43,6 +57,12 @@ declare class EFText extends EFText_base {
|
|
|
43
57
|
private propagateAnimationToSegments;
|
|
44
58
|
private getTextContent;
|
|
45
59
|
private splitText;
|
|
60
|
+
/**
|
|
61
|
+
* When `motionBlur` is enabled, wraps `segment` in a configured `<ef-motionblur>`
|
|
62
|
+
* element and returns the wrapper. Otherwise returns `segment` unchanged.
|
|
63
|
+
* Whitespace segments are never wrapped — they are layout-only spacers.
|
|
64
|
+
*/
|
|
65
|
+
private _wrapWithBlur;
|
|
46
66
|
private detectWordBoundaries;
|
|
47
67
|
private splitTextIntoSegments;
|
|
48
68
|
get intrinsicDurationMs(): number | undefined;
|
package/dist/elements/EFText.js
CHANGED
|
@@ -21,6 +21,7 @@ let EFText = class EFText$1 extends EFTemporal(LitElement) {
|
|
|
21
21
|
super(..._args);
|
|
22
22
|
this.split = "word";
|
|
23
23
|
this.easing = "linear";
|
|
24
|
+
this.motionBlur = false;
|
|
24
25
|
this.lastTextContent = "";
|
|
25
26
|
this._textContent = null;
|
|
26
27
|
this._templateElement = null;
|
|
@@ -140,7 +141,8 @@ let EFText = class EFText$1 extends EFTemporal(LitElement) {
|
|
|
140
141
|
}
|
|
141
142
|
updated(changedProperties) {
|
|
142
143
|
super.updated(changedProperties);
|
|
143
|
-
if (changedProperties.has("split") || changedProperties.has("staggerMs") || changedProperties.has("easing") || changedProperties.has("durationMs")) {
|
|
144
|
+
if (changedProperties.has("split") || changedProperties.has("staggerMs") || changedProperties.has("easing") || changedProperties.has("durationMs") || changedProperties.has("motionBlur") || changedProperties.has("blurSensitivity") || changedProperties.has("blurShutterAngle") || changedProperties.has("blurFps") || changedProperties.has("blurThreshold") || changedProperties.has("blurAngle") || changedProperties.has("blurAmount")) {
|
|
145
|
+
this.#segmentsInitialized = false;
|
|
144
146
|
this.emitContentChange("content");
|
|
145
147
|
this.splitText();
|
|
146
148
|
}
|
|
@@ -204,6 +206,7 @@ let EFText = class EFText$1 extends EFTemporal(LitElement) {
|
|
|
204
206
|
const element = node;
|
|
205
207
|
if (element.tagName === "EF-TEXT-SEGMENT") continue;
|
|
206
208
|
if (element.tagName === "TEMPLATE") continue;
|
|
209
|
+
if (element.tagName === "EF-MOTIONBLUR") continue;
|
|
207
210
|
text += element.textContent || "";
|
|
208
211
|
}
|
|
209
212
|
return text;
|
|
@@ -280,6 +283,7 @@ let EFText = class EFText$1 extends EFTemporal(LitElement) {
|
|
|
280
283
|
if (this.split === "char" && wordBoundaries) {
|
|
281
284
|
const originalCharIndex = textStartOffset + charIndex;
|
|
282
285
|
const wordIndex = wordBoundaries.get(originalCharIndex);
|
|
286
|
+
const nodeToAppend = this._wrapWithBlur(segment);
|
|
283
287
|
if (wordIndex !== void 0) {
|
|
284
288
|
if (wordIndex !== currentWordIndex) {
|
|
285
289
|
if (currentWordSpan) fragment.appendChild(currentWordSpan);
|
|
@@ -287,18 +291,18 @@ let EFText = class EFText$1 extends EFTemporal(LitElement) {
|
|
|
287
291
|
currentWordSpan = document.createElement("span");
|
|
288
292
|
currentWordSpan.style.whiteSpace = "nowrap";
|
|
289
293
|
}
|
|
290
|
-
if (currentWordSpan) currentWordSpan.appendChild(
|
|
291
|
-
else fragment.appendChild(
|
|
294
|
+
if (currentWordSpan) currentWordSpan.appendChild(nodeToAppend);
|
|
295
|
+
else fragment.appendChild(nodeToAppend);
|
|
292
296
|
} else {
|
|
293
297
|
if (currentWordSpan) {
|
|
294
298
|
fragment.appendChild(currentWordSpan);
|
|
295
299
|
currentWordSpan = null;
|
|
296
300
|
currentWordIndex = null;
|
|
297
301
|
}
|
|
298
|
-
fragment.appendChild(
|
|
302
|
+
fragment.appendChild(nodeToAppend);
|
|
299
303
|
}
|
|
300
304
|
charIndex += segmentText.length;
|
|
301
|
-
} else fragment.appendChild(segment);
|
|
305
|
+
} else fragment.appendChild(this._wrapWithBlur(segment));
|
|
302
306
|
});
|
|
303
307
|
} else {
|
|
304
308
|
const segment = document.createElement("ef-text-segment");
|
|
@@ -313,6 +317,7 @@ let EFText = class EFText$1 extends EFTemporal(LitElement) {
|
|
|
313
317
|
if (this.split === "char" && wordBoundaries) {
|
|
314
318
|
const originalCharIndex = textStartOffset + charIndex;
|
|
315
319
|
const wordIndex = wordBoundaries.get(originalCharIndex);
|
|
320
|
+
const nodeToAppend = this._wrapWithBlur(segment);
|
|
316
321
|
if (wordIndex !== void 0) {
|
|
317
322
|
if (wordIndex !== currentWordIndex) {
|
|
318
323
|
if (currentWordSpan) fragment.appendChild(currentWordSpan);
|
|
@@ -320,18 +325,18 @@ let EFText = class EFText$1 extends EFTemporal(LitElement) {
|
|
|
320
325
|
currentWordSpan = document.createElement("span");
|
|
321
326
|
currentWordSpan.style.whiteSpace = "nowrap";
|
|
322
327
|
}
|
|
323
|
-
if (currentWordSpan) currentWordSpan.appendChild(
|
|
324
|
-
else fragment.appendChild(
|
|
328
|
+
if (currentWordSpan) currentWordSpan.appendChild(nodeToAppend);
|
|
329
|
+
else fragment.appendChild(nodeToAppend);
|
|
325
330
|
} else {
|
|
326
331
|
if (currentWordSpan) {
|
|
327
332
|
fragment.appendChild(currentWordSpan);
|
|
328
333
|
currentWordSpan = null;
|
|
329
334
|
currentWordIndex = null;
|
|
330
335
|
}
|
|
331
|
-
fragment.appendChild(
|
|
336
|
+
fragment.appendChild(nodeToAppend);
|
|
332
337
|
}
|
|
333
338
|
charIndex += segmentText.length;
|
|
334
|
-
} else fragment.appendChild(segment);
|
|
339
|
+
} else fragment.appendChild(this._wrapWithBlur(segment));
|
|
335
340
|
}
|
|
336
341
|
});
|
|
337
342
|
if (this.split === "char" && currentWordSpan) fragment.appendChild(currentWordSpan);
|
|
@@ -365,6 +370,23 @@ let EFText = class EFText$1 extends EFTemporal(LitElement) {
|
|
|
365
370
|
});
|
|
366
371
|
this._segmentsReadyResolvers = [];
|
|
367
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* When `motionBlur` is enabled, wraps `segment` in a configured `<ef-motionblur>`
|
|
375
|
+
* element and returns the wrapper. Otherwise returns `segment` unchanged.
|
|
376
|
+
* Whitespace segments are never wrapped — they are layout-only spacers.
|
|
377
|
+
*/
|
|
378
|
+
_wrapWithBlur(segment) {
|
|
379
|
+
if (!this.motionBlur || segment.hasAttribute("data-whitespace")) return segment;
|
|
380
|
+
const mb = document.createElement("ef-motionblur");
|
|
381
|
+
if (this.blurSensitivity !== void 0) mb.setAttribute("sensitivity", String(this.blurSensitivity));
|
|
382
|
+
if (this.blurShutterAngle !== void 0) mb.setAttribute("shutter-angle", String(this.blurShutterAngle));
|
|
383
|
+
if (this.blurFps !== void 0) mb.setAttribute("fps", String(this.blurFps));
|
|
384
|
+
if (this.blurThreshold !== void 0) mb.setAttribute("threshold", String(this.blurThreshold));
|
|
385
|
+
if (this.blurAngle !== void 0) mb.setAttribute("angle", String(this.blurAngle));
|
|
386
|
+
if (this.blurAmount !== void 0) mb.setAttribute("amount", String(this.blurAmount));
|
|
387
|
+
mb.appendChild(segment);
|
|
388
|
+
return mb;
|
|
389
|
+
}
|
|
368
390
|
detectWordBoundaries(text) {
|
|
369
391
|
const boundaries = /* @__PURE__ */ new Map();
|
|
370
392
|
const trimmedText = text.trim();
|
|
@@ -432,6 +454,41 @@ __decorate([property({
|
|
|
432
454
|
type: String,
|
|
433
455
|
reflect: true
|
|
434
456
|
})], EFText.prototype, "easing", void 0);
|
|
457
|
+
__decorate([property({
|
|
458
|
+
type: Boolean,
|
|
459
|
+
attribute: "motion-blur",
|
|
460
|
+
reflect: true
|
|
461
|
+
})], EFText.prototype, "motionBlur", void 0);
|
|
462
|
+
__decorate([property({
|
|
463
|
+
type: Number,
|
|
464
|
+
attribute: "blur-sensitivity",
|
|
465
|
+
reflect: true
|
|
466
|
+
})], EFText.prototype, "blurSensitivity", void 0);
|
|
467
|
+
__decorate([property({
|
|
468
|
+
type: Number,
|
|
469
|
+
attribute: "blur-shutter-angle",
|
|
470
|
+
reflect: true
|
|
471
|
+
})], EFText.prototype, "blurShutterAngle", void 0);
|
|
472
|
+
__decorate([property({
|
|
473
|
+
type: Number,
|
|
474
|
+
attribute: "blur-fps",
|
|
475
|
+
reflect: true
|
|
476
|
+
})], EFText.prototype, "blurFps", void 0);
|
|
477
|
+
__decorate([property({
|
|
478
|
+
type: Number,
|
|
479
|
+
attribute: "blur-threshold",
|
|
480
|
+
reflect: true
|
|
481
|
+
})], EFText.prototype, "blurThreshold", void 0);
|
|
482
|
+
__decorate([property({
|
|
483
|
+
type: Number,
|
|
484
|
+
attribute: "blur-angle",
|
|
485
|
+
reflect: true
|
|
486
|
+
})], EFText.prototype, "blurAngle", void 0);
|
|
487
|
+
__decorate([property({
|
|
488
|
+
type: Number,
|
|
489
|
+
attribute: "blur-amount",
|
|
490
|
+
reflect: true
|
|
491
|
+
})], EFText.prototype, "blurAmount", void 0);
|
|
435
492
|
EFText = __decorate([customElement("ef-text")], EFText);
|
|
436
493
|
|
|
437
494
|
//#endregion
|