// fallow-ignore-file code-duplication complexity import { installRuntimeControlBridge, postRuntimeMessage } from "./bridge"; import { initRuntimeAnalytics, emitAnalyticsEvent } from "./analytics"; import { injectCompositionCssVariables } from "./getVariables"; import { createCssAdapter } from "./adapters/css"; import { createGsapAdapter } from "./adapters/gsap"; import { createAnimeJsAdapter } from "./adapters/animejs"; import { createLottieAdapter } from "./adapters/lottie"; import { createThreeAdapter } from "./adapters/three"; import { createMapboxAdapter } from "./adapters/mapbox"; import { createLeafletAdapter } from "./adapters/leaflet"; import { createGoogleMapsAdapter } from "./adapters/google-maps"; import { createMaplibreAdapter } from "./adapters/maplibre"; import { createD3Adapter } from "./adapters/d3"; import { createTypegpuAdapter } from "./adapters/typegpu"; import { patchVideoTextureCompat, patchWebGLVideoTextureCompat, } from "./adapters/video-texture-compat"; import { forceDispatchSeekEvent } from "./adapters/seek-dispatch"; import { createWaapiAdapter } from "./adapters/waapi"; import { refreshRuntimeMediaCache, syncRuntimeMedia } from "./media"; import { probeAndCacheElementVolume, type VolumeKeyframe } from "./mediaVolumeEnvelope.js"; import { createPickerModule } from "./picker"; import { createRuntimePlayer } from "./player"; import { createRuntimeState } from "./state"; import { collectRuntimeTimelinePayload } from "./timeline"; import { createRuntimeStartTimeResolver } from "./startResolver"; import { createClipTree } from "./clipTree"; import { loadExternalCompositions, loadInlineTemplateCompositions } from "./compositionLoader"; import { applyCaptionOverrides } from "./captionOverrides"; import { applyPositionEdits } from "./positionEdits"; import { createColorGradingRuntime, type RuntimeColorGradingApi } from "./colorGrading"; import { TransportClock } from "./clock"; import { WebAudioTransport } from "./webAudioTransport"; import { quantizeTimeToFrame } from "../inline-scripts/parityContract"; import { STUDIO_MANUAL_EDIT_GESTURE_ATTR } from "../studio-api/helpers/draftMarkers"; import type { RuntimeDeterministicAdapter, RuntimeJson, RuntimeSeekOptions, RuntimeTimelineLike, } from "./types"; import type { PlayerAPI } from "../core.types"; import { swallow } from "./diagnostics"; const AUTHORED_DURATION_ATTR = "data-hf-authored-duration"; const AUTHORED_END_ATTR = "data-hf-authored-end"; type ExportRenderFpsResolution = { fps: number | null; source: "render-options" | "default" | "unknown"; rawFpsSource: unknown; rawFps: unknown; fallbackReason?: "missing" | "invalid"; }; function resolveExportRenderFps(): ExportRenderFpsResolution { const config = window.__HF_EXPORT_RENDER_SEEK_CONFIG; const rawFps = config?.fps; const rawFpsSource = config?.fpsSource; const fps = Number(rawFps); if (!config || rawFps == null) { return { fps: null, source: "default", rawFpsSource, rawFps, fallbackReason: "missing" }; } if (!Number.isFinite(fps) || fps <= 0) { return { fps: null, source: "default", rawFpsSource, rawFps, fallbackReason: "invalid" }; } const source = rawFpsSource === "render-options" || rawFpsSource === "default" ? rawFpsSource : "unknown"; return { fps, source, rawFpsSource, rawFps, fallbackReason: config.fpsFallbackReason, }; } export function initSandboxRuntimeModular(): void { const state = createRuntimeState(); // SDK moveElement edits must render even when no usable GSAP timeline ever // binds (CSS/WAAPI-animated or fully static compositions) — apply at init. // This runs at DOMContentLoaded, after inline composition scripts have // parsed their tweens, so GSAP (when present) won't fold the translate. // Re-applied on every timeline bind for the rebind/soft-reload paths. applyPositionEdits(document); const exportRenderFps = resolveExportRenderFps(); state.canonicalFps = exportRenderFps.fps ?? state.canonicalFps; if (window.__HF_EXPORT_RENDER_SEEK_CONFIG) { console.info("[hyperframes] render runtime fps", { canonicalFps: state.canonicalFps, source: exportRenderFps.source, rawFpsSource: exportRenderFps.rawFpsSource, rawFps: exportRenderFps.rawFps, fallbackReason: exportRenderFps.fallbackReason, }); } let colorGradingRuntime: RuntimeColorGradingApi | null = null; let runtimeErrorListener: ((event: ErrorEvent) => void) | null = null; let runtimeUnhandledRejectionListener: ((event: PromiseRejectionEvent) => void) | null = null; const runtimeCleanupCallbacks: Array<() => void> = []; const postedDiagnosticKeys = new Set(); let rootStageDiagnosticRafId: number | null = null; if (typeof window.__hfRuntimeTeardown === "function") { try { window.__hfRuntimeTeardown(); } catch (err) { // keep runtime resilient across reinits swallow("runtime.init.site1", err); } } // `_auto` is a Studio-internal keyframe marker (an auto-tracked endpoint the // parser reads back), NOT an animatable property. Register it as a no-op GSAP // plugin so GSAP doesn't log "Invalid property _auto" on every tween build — // that per-frame warning destabilizes the preview and makes the selection // overlay stop tracking the pointer. Idempotent + best-effort. const ensureAutoMarkerNoop = (): void => { const g = window.gsap as { registerPlugin?: (plugin: unknown) => void } | undefined; const w = window as Window & { __hfAutoNoopRegistered?: boolean }; if (!g?.registerPlugin || w.__hfAutoNoopRegistered) return; try { g.registerPlugin({ name: "_auto", init: () => false }); w.__hfAutoNoopRegistered = true; } catch { // a stray warning is preferable to a broken runtime } }; ensureAutoMarkerNoop(); // Normalize html/body so browser defaults (8px margin, white background) never // bleed into renders as white bars. Runs in both preview and render contexts, // eliminating the preview/render parity gap that existed when only the React // component's normalizePreviewViewport call applied this normalization. if (document.documentElement) { document.documentElement.style.margin = "0"; document.documentElement.style.padding = "0"; document.documentElement.style.overflow = "hidden"; } if (document.body) { document.body.style.margin = "0"; document.body.style.padding = "0"; document.body.style.overflow = "hidden"; } // figma brand-token chain: define declared composition variables as CSS // custom properties so imported var(--slug, literal) fills resolve from the // live variable instead of always falling back to the frozen literal. try { injectCompositionCssVariables(document); } catch (err) { swallow("runtime.init.cssVariables", err); } window.__timelines = window.__timelines || {}; // Resolve the root composition element with the same priority the rest of // the runtime uses (explicit `data-root` marker first, then the topmost // non-nested composition, then first in DOM order). Defined here so the // array-normalization + data-start defaults below pick the same root the // closure-based `resolveRootCompositionElement` does on multi-comp pages. const findRootCompositionEl = (): HTMLElement | null => { const explicitRoot = document.querySelector('[data-composition-id][data-root="true"]'); if (explicitRoot instanceof HTMLElement) return explicitRoot; const nodes = Array.from(document.querySelectorAll("[data-composition-id]")) as HTMLElement[]; return ( nodes.find((node) => !node.parentElement?.closest("[data-composition-id]")) ?? nodes[0] ?? null ); }; // Agents often write `window.__timelines = [tl]` (array) instead of the // keyed-by-composition-id object the runtime expects. Normalize at init so // the rest of the pipeline can assume a Record. if (Array.isArray(window.__timelines)) { const arr = window.__timelines as unknown[]; const rootId = findRootCompositionEl()?.getAttribute("data-composition-id") ?? "root"; const normalized: Record = {}; if (arr.length === 1) { normalized[rootId] = arr[0]; } else { for (let i = 0; i < arr.length; i++) normalized[`tl-${i}`] = arr[i]; } (window as Record).__timelines = normalized; } // Agents sometimes omit data-start on the root composition element. The // runtime skips timed-visibility for elements without it, making clips // invisible and timelines non-seekable. Default to 0 for the root. const rootComp = findRootCompositionEl(); if (rootComp && !rootComp.hasAttribute("data-start")) { rootComp.setAttribute("data-start", "0"); } const registerRuntimeCleanup = (callback: () => void) => { runtimeCleanupCallbacks.push(callback); }; const postRuntimeDiagnosticOnce = ( code: string, details: Record, dedupeKey?: string, ) => { const key = dedupeKey ?? `${code}:${JSON.stringify(details)}`; if (postedDiagnosticKeys.has(key)) { return; } postedDiagnosticKeys.add(key); postRuntimeMessage({ source: "hf-preview", type: "diagnostic", code, details, }); }; const createPlayerApiCompat = (basePlayer: { _timeline: RuntimeTimelineLike | null; play: () => void; pause: () => void; seek: (timeSeconds: number, options?: { keepPlaying?: boolean }) => void; getTime: () => number; getDuration: () => number; isPlaying: () => boolean; renderSeek: (timeSeconds: number, options?: RuntimeSeekOptions) => void; }): PlayerAPI => { const defaultStageZoom: ReturnType = { scale: 1, focusX: 960, focusY: 540, }; const emptyStageZoomKeyframes: ReturnType = []; const emptyVisibleElements: ReturnType = []; const defaultRenderState: ReturnType = { time: basePlayer.getTime(), duration: basePlayer.getDuration(), isPlaying: basePlayer.isPlaying(), renderMode: false, timelineDirty: false, }; return { play: basePlayer.play, pause: basePlayer.pause, seek: basePlayer.seek, getTime: basePlayer.getTime, getDuration: basePlayer.getDuration, isPlaying: basePlayer.isPlaying, getMainTimeline: () => null, getElementBounds: () => {}, getElementsAtPoint: () => {}, setElementPosition: () => {}, previewElementPosition: () => {}, setElementKeyframes: () => {}, setElementScale: () => {}, setElementFontSize: () => {}, setElementTextContent: () => {}, setElementTextColor: () => {}, setElementTextShadow: () => {}, setElementTextFontWeight: () => {}, setElementTextFontFamily: () => {}, setElementTextOutline: () => {}, setElementTextHighlight: () => {}, setElementVolume: () => {}, setStageZoom: () => {}, getStageZoom: () => defaultStageZoom, setStageZoomKeyframes: () => {}, getStageZoomKeyframes: () => emptyStageZoomKeyframes, addElement: () => false, removeElement: () => false, updateElementTiming: () => false, setElementTiming: () => {}, updateElementSrc: () => false, updateElementLayer: () => false, updateElementBasePosition: () => false, markTimelineDirty: () => {}, isTimelineDirty: () => false, rebuildTimeline: () => {}, ensureTimeline: () => {}, enableRenderMode: () => {}, disableRenderMode: () => {}, renderSeek: basePlayer.renderSeek, getElementVisibility: () => ({ visible: false }), getVisibleElements: () => emptyVisibleElements, getRenderState: () => ({ ...defaultRenderState, time: basePlayer.getTime(), duration: basePlayer.getDuration(), isPlaying: basePlayer.isPlaying(), }), }; }; const MIN_VALID_TIMELINE_DURATION_SECONDS = 1 / 60; const TIMELINE_FLOOR_COVERAGE_RATIO = 0.75; const PLAY_REBIND_HOLD_SECONDS = 2; const METADATA_REBIND_MIN_DURATION_GAIN_SECONDS = 0.05; const METADATA_REBIND_DEBOUNCE_MS = 100; const MAX_DIAGNOSTIC_MESSAGE_LENGTH = 240; const normalizeDiagnosticMessage = (value: unknown): string => { if (value instanceof Error) { return value.message || String(value); } if (typeof value === "string") { return value; } try { return JSON.stringify(value); } catch { return String(value ?? ""); } }; const classifyRuntimeScriptFailure = ( rawMessage: string, ): { code: string; category: string; } => { const message = rawMessage.toLowerCase(); if ( message.includes("cannot read properties of null") || message.includes("cannot set properties of null") ) { return { code: "runtime_null_dom_access", category: "dom-null-access" }; } if (message.includes("failed to execute 'queryselector'")) { return { code: "runtime_invalid_selector", category: "selector-invalid" }; } if (message.includes("is not defined")) { return { code: "runtime_reference_missing", category: "reference-missing" }; } return { code: "runtime_script_error", category: "script-error" }; }; const parseDimensionPx = (value: string | null): string | null => { if (value == null || value.trim() === "") return null; const parsed = Number.parseFloat(value); if (!Number.isFinite(parsed) || parsed <= 0) return null; return `${parsed}px`; }; const resolveRootCompositionElement = (): HTMLElement | null => findRootCompositionEl(); const applyCompositionSizing = () => { const rootEl = resolveRootCompositionElement(); if (!rootEl) return; const forcedWidth = parseDimensionPx(rootEl.getAttribute("data-width")); const forcedHeight = parseDimensionPx(rootEl.getAttribute("data-height")); if (forcedWidth) rootEl.style.width = forcedWidth; if (forcedHeight) rootEl.style.height = forcedHeight; if (forcedWidth) rootEl.style.setProperty("--comp-width", forcedWidth); if (forcedHeight) rootEl.style.setProperty("--comp-height", forcedHeight); }; const sanitizeCompositionDurationAttributes = () => { const rootEl = resolveRootCompositionElement(); const compositionNodes = Array.from(document.querySelectorAll("[data-composition-id]")).filter( (n) => n.hasAttribute("data-duration") || n.hasAttribute("data-end"), ) as HTMLElement[]; for (const node of compositionNodes) { // Preserve explicit root duration so timeline payload can distinguish // authored finite duration from loop-inflated timeline duration. if (rootEl && node === rootEl) continue; // Preserve authored timing for reference-start resolution in Studio and // timeline payload generation. The runtime still strips the public attrs // so visibility/parity continues to derive from the live sub-timeline. const authoredDuration = node.getAttribute("data-duration"); const authoredEnd = node.getAttribute("data-end"); if (authoredDuration != null && !node.hasAttribute(AUTHORED_DURATION_ATTR)) { node.setAttribute(AUTHORED_DURATION_ATTR, authoredDuration); } if (authoredEnd != null && !node.hasAttribute(AUTHORED_END_ATTR)) { node.setAttribute(AUTHORED_END_ATTR, authoredEnd); } // Strip public timing attrs on non-root compositions after preserving // authored values privately. Runtime timing can still distinguish // authored host windows from live child timeline durations. node.removeAttribute("data-duration"); node.removeAttribute("data-end"); } }; const applyClipLayout = () => { const rootEl = resolveRootCompositionElement(); if (!rootEl) return; if (!rootEl.style.position) { rootEl.style.position = "relative"; } rootEl.style.overflow = "hidden"; const rootWidth = parseDimensionPx(rootEl.getAttribute("data-width")); const rootHeight = parseDimensionPx(rootEl.getAttribute("data-height")); if (rootWidth) rootEl.style.width = rootWidth; if (rootHeight) rootEl.style.height = rootHeight; const children = Array.from(rootEl.children) as HTMLElement[]; for (const el of children) { const tag = el.tagName.toLowerCase(); if (tag === "script" || tag === "style" || tag === "link" || tag === "meta") continue; if (!el.hasAttribute("data-start")) continue; // Runtime-stamped clips are NOT authored overlay clips. In Studio/preview // the runtime stamps `data-start` onto ID'd or GSAP-targeted flow children // (a
/