import { episode, episodeSampleDescription } from "../episode"; import type { AnimationRouteSampleForVisual } from "../sample-episode-visual"; import "./image-puppet-episode.css"; export interface ImagePuppetEpisodeOptions { readonly sampleAt: (time: number) => AnimationRouteSampleForVisual; readonly duration: number; readonly fixedTime?: number; } export interface ImagePuppetEpisodeProof { readonly contractId: string; readonly episodeId: string; readonly title: string; readonly route: string; readonly mode: "image-derived-puppet-cutouts"; readonly sourceImage: string; readonly sourcePixelsAnimated: true; readonly notTrue3D: true; readonly screenshotTarget: "#image-puppet-episode"; readonly sample: AnimationRouteSampleForVisual; readonly movingCutouts: readonly string[]; readonly limitations: readonly string[]; } const sourceImage = "/aura-assets/moon-garden-feature-frame.png"; const movingCutouts = [ "blue robot source-pixel head/body cutout", "blue robot source-pixel broom area", "yellow robot source-pixel head/body cutout", "yellow robot source-pixel arm/tool area", "wheelbarrow source-pixel cutout", "foreground flower source-pixel layer", "moon source-pixel pulse layer" ] as const; export function installImagePuppetEpisode(options: ImagePuppetEpisodeOptions) { document.body.dataset.animationView = "image-puppet"; const root = document.createElement("section"); root.id = "image-puppet-episode"; root.className = "image-puppet"; root.setAttribute("aria-label", "Image-derived puppet animation using cutouts from the moon garden source PNG"); root.innerHTML = `
Image-derived cutouts Shot 1 / 3
`; document.body.appendChild(root); const caption = root.querySelector("[data-image-puppet-caption]"); const shotLabel = root.querySelector("[data-image-puppet-shot-label]"); const progress = root.querySelector("[data-image-puppet-progress]"); const query = new URLSearchParams(window.location.search); const queryTime = query.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 progressPercent = Math.max(0, Math.min(100, (sample.time / options.duration) * 100)); root.dataset.shotId = sample.shotId ?? ""; root.dataset.captionId = sample.captionId ?? ""; root.dataset.sourceImage = sourceImage; if (caption) { caption.textContent = sample.captionText ?? "The source image is duplicated into moving cutout layers."; } if (shotLabel) { shotLabel.textContent = `Shot ${shotIndex + 1} / ${episode.shotTimeline.shots.length}`; } if (progress) { progress.style.setProperty("--progress", `${progressPercent}%`); } window.__AURA3D_ANIMATION_IMAGE_PUPPET_PROOF__ = { contractId: episodeSampleDescription.contractId, episodeId: episode.episodePlan.episodeId, title: episode.episodePlan.title, route: "/?view=image-puppet", mode: "image-derived-puppet-cutouts", sourceImage, sourcePixelsAnimated: true, notTrue3D: true, screenshotTarget: "#image-puppet-episode", sample, movingCutouts, limitations: [ "The PNG is flattened, so these are approximate masked source-pixel cutouts, not clean production layers.", "The original still remains visible under the moving cutouts unless a real background plate or inpainted clean plate exists.", "This is image-derived 2D animation, not 3D reconstruction or rigged GLB character animation." ] }; } render(Number.isFinite(fixedTime) ? Number(fixedTime) : 0); const tick = () => { if (!Number.isFinite(fixedTime)) { render(((performance.now() - start) / 1000) % options.duration); } requestAnimationFrame(tick); }; tick(); return root; } declare global { interface Window { __AURA3D_ANIMATION_IMAGE_PUPPET_PROOF__?: ImagePuppetEpisodeProof; } } function clampTime(time: number, duration: number) { if (!Number.isFinite(time)) return 0; return Math.max(0, Math.min(duration - 1 / 30, time)); }