import { episode, episodeSampleDescription, youtubeDraftMetadata } from "./episode"; export interface AnimationRouteSampleForVisual { readonly time: number; readonly frame: number; readonly shotId?: string; readonly captionId?: string; readonly captionText?: string; readonly dialogueLineId?: string; readonly visemeId?: string; readonly nodeUpdates?: readonly { readonly characterId?: string; readonly action?: string; readonly emotion?: string; }[]; } export interface SampleEpisodeVisualOptions { readonly sampleAt: (time: number) => AnimationRouteSampleForVisual; readonly duration: number; readonly fixedTime?: number; readonly usesTypedAssets?: boolean; } export function installSampleEpisodeVisual(options: SampleEpisodeVisualOptions) { const root = document.createElement("section"); root.id = "sample-episode-visual"; root.className = "sample-episode"; root.setAttribute("aria-label", "Aura3D animation sample episode preview"); root.innerHTML = `
${robotMarkup("miko", "Miko")} ${robotMarkup("luma", "Luma")}

${escapeHtml(episode.episodePlan.title)}

Shot 1 / 3
Aura3D animation contract · captions · visemes · render queue
`; document.body.appendChild(root); const frame = root.querySelector(".sample-episode__frame"); const hostSlot = root.querySelector("[data-aura-host-slot]"); const appHost = document.querySelector("#app"); if (frame && hostSlot && appHost) { hostSlot.replaceWith(appHost); } const caption = root.querySelector("[data-sample-caption]"); const shotLabel = root.querySelector("[data-sample-shot-label]"); const progress = root.querySelector("[data-sample-progress]"); const queryTime = new URLSearchParams(window.location.search).get("sampleTime"); const fixedTime = options.fixedTime ?? (queryTime ? Number(queryTime) : undefined); const start = performance.now(); function render(time: number) { const sample = options.sampleAt(clampTime(time, options.duration)); const shotIndex = Math.max(0, episode.shotTimeline.shots.findIndex((shot) => shot.shotId === sample.shotId)); const speaker = sample.nodeUpdates?.find((update) => update.action === "speak")?.characterId ?? ""; const progressPercent = Math.max(0, Math.min(100, (sample.time / options.duration) * 100)); root.dataset.shotId = sample.shotId ?? ""; root.dataset.captionId = sample.captionId ?? ""; root.dataset.speaker = speaker; root.dataset.visemeId = sample.visemeId ?? ""; root.dataset.usesTypedAssets = options.usesTypedAssets ? "true" : "false"; root.dataset.animationSampleEpisode = "moon-garden-cleanup"; root.dataset.youtubeDraftTitle = youtubeDraftMetadata.title; if (caption) { caption.textContent = sample.captionText ?? "Two tiny robots clean a glowing moon garden."; } if (shotLabel) { shotLabel.textContent = `Shot ${shotIndex + 1} / ${episode.shotTimeline.shots.length}`; } if (progress) { progress.style.setProperty("--progress", `${progressPercent}%`); } window.__AURA3D_ANIMATION_SAMPLE_EPISODE__ = { contractId: episodeSampleDescription.contractId, episodeId: episode.episodePlan.episodeId, title: episode.episodePlan.title, route: "/", sample, visualLayer: { id: root.id, deterministicSampleTime: fixedTime ?? null, screenshotTarget: "#sample-episode-visual", renderedBy: "aura3d-scene", usesTypedAssets: Boolean(options.usesTypedAssets), characters: episode.episodePlan.characters.map((character) => character.id), styleGuide: episode.storyBible.styleGuide.visualStyle, youtubeDraftTitle: youtubeDraftMetadata.title } }; } if (Number.isFinite(fixedTime)) { render(Number(fixedTime)); return root; } const tick = () => { render(((performance.now() - start) / 1000) % options.duration); requestAnimationFrame(tick); }; tick(); return root; } declare global { interface Window { __AURA3D_ANIMATION_SAMPLE_EPISODE__?: { readonly contractId: string; readonly episodeId: string; readonly title: string; readonly route: string; readonly sample: AnimationRouteSampleForVisual; readonly visualLayer: { readonly id: string; readonly deterministicSampleTime: number | null; readonly screenshotTarget: string; readonly renderedBy: "aura3d-scene"; readonly usesTypedAssets: boolean; readonly characters: readonly string[]; readonly styleGuide: string; readonly youtubeDraftTitle: string; }; }; } } function robotMarkup(id: "miko" | "luma", label: string) { return ` `; } function clampTime(time: number, duration: number) { if (!Number.isFinite(time)) return 0; return Math.max(0, Math.min(duration - 1 / 30, time)); } function escapeHtml(value: string) { return value.replace(/[&<>"']/g, (character) => { switch (character) { case "&": return "&"; case "<": return "<"; case ">": return ">"; case '"': return """; case "'": return "'"; default: return character; } }); }