import { episode, episodeSampleDescription, youtubeDraftMetadata } from "../episode";
import type { AnimationRouteSampleForVisual } from "../sample-episode-visual";
import "./puppet-episode-2d.css";
export interface PuppetEpisode2DOptions {
readonly sampleAt: (time: number) => AnimationRouteSampleForVisual;
readonly duration: number;
readonly fixedTime?: number;
}
export interface PuppetEpisode2DProof {
readonly contractId: string;
readonly episodeId: string;
readonly title: string;
readonly route: string;
readonly mode: "2d-puppet-animation-over-concept-art";
readonly sourceImage: string;
readonly notTrue3D: true;
readonly screenshotTarget: "#puppet-episode-2d";
readonly sample: AnimationRouteSampleForVisual;
readonly animatedParts: readonly string[];
readonly aura3dRole: readonly string[];
readonly limitations: readonly string[];
}
const sourceImage = "/aura-assets/moon-garden-feature-frame.png";
const animatedParts = [
"blue robot head bob",
"blue robot eyes blink",
"blue robot broom sweep",
"blue robot shoulder and elbow arcs",
"yellow robot head nod",
"yellow robot eyes blink",
"yellow robot rake/push arm",
"wheelbarrow body roll",
"moon ring pulse",
"foreground flower glow",
"caption/timeline timing"
] as const;
export function installPuppetEpisode2D(options: PuppetEpisode2DOptions) {
document.body.dataset.animationView = "puppet-2d";
const root = document.createElement("section");
root.id = "puppet-episode-2d";
root.className = "puppet-episode";
root.setAttribute("aria-label", "Aura3D 2D puppet animation animation over moon garden concept art");
root.innerHTML = `
2D puppet animation
Shot 1 / 3
`;
document.body.appendChild(root);
const hostSlot = document.querySelector("[data-aura-host-slot]");
const appHost = document.querySelector("#app");
if (hostSlot && appHost) {
hostSlot.replaceWith(appHost);
}
const caption = root.querySelector("[data-puppet-caption]");
const shotLabel = root.querySelector("[data-puppet-shot-label]");
const progress = root.querySelector("[data-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.puppetEpisode = "moon-garden-cleanup";
if (caption) {
caption.textContent = sample.captionText ?? "The robots sweep, blink, nod, and push the garden cart.";
}
if (shotLabel) {
shotLabel.textContent = `Shot ${shotIndex + 1} / ${episode.shotTimeline.shots.length}`;
}
if (progress) {
progress.style.setProperty("--progress", `${progressPercent}%`);
}
window.__AURA3D_ANIMATION_2D_PUPPET_PROOF__ = {
contractId: episodeSampleDescription.contractId,
episodeId: episode.episodePlan.episodeId,
title: episode.episodePlan.title,
route: "/?view=puppet-2d",
mode: "2d-puppet-animation-over-concept-art",
sourceImage,
notTrue3D: true,
screenshotTarget: "#puppet-episode-2d",
sample,
animatedParts,
aura3dRole: [
"episode contract and shot timing",
"caption timing",
"animation state/proof metadata",
"render/record evidence",
"optional replacement with typed 3D foreground assets"
],
limitations: [
"This is 2D puppet animation over concept art, not a generated 3D scene.",
"A production pass should replace CSS puppet shapes with separated art layers or rigged character assets.",
"The source frame supplies the art direction/backdrop; the moving robots are explicit overlay puppets."
]
};
}
if (Number.isFinite(fixedTime)) {
render(Number(fixedTime));
} else {
render(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_2D_PUPPET_PROOF__?: PuppetEpisode2DProof;
}
}
function clampTime(time: number, duration: number) {
if (!Number.isFinite(time)) return 0;
return Math.max(0, Math.min(duration - 1 / 30, time));
}