feat(compiler): capture and replay Lottie animations

lottie-web renders vector animations from a JSON document at runtime. As
third-party JS it falls outside the declarative CSS path and the WAAPI / reveal /
marquee probes, so clones shipped the empty container and the animation was gone.
This adds a Lottie subset to Stage 5 motion: capture the deterministic part — the
source JSON (materialized local file, or inline animationData) plus playback
config — and emit a fixed DittoLottie client that re-mounts lottie-web on the
cid'd container.

Capture (compiler/src/capture/lottie.ts): in-page detector covering lottie-web's
registry (animationData in memory), <lottie-player>/<dotlottie-player>, Elementor
data-settings (source_json.url), and generic markup. Keyed by data-cid-cap.
motion.ts merges it into MotionCapture; capture.ts registers source URLs as assets
so the existing fallback fetch downloads + materializes them to /assets/cloned/lottie.

Generate (compiler/src/generate/lottie.ts): buildLottieSpec resolves cap->cid and
source URL->materialized local path (or embeds inline animationData), then emits
DittoLottie + wire/import helpers, mirroring DittoMotion. app.ts writes the
component, wires import+JSX, and injects lottie-web into the generated package.json.

Delivery (compiler/src/cli.ts): stripDeliveryDataCids now treats DittoLottie as a
runtime consumer, so its container keeps a semantic data-ditto-id anchor instead of
being stripped (without this the animation never finds its mount point).

Lottie-free clones stay byte-identical. Verified end-to-end: cloning a page with a
<lottie-player> scrapes the JSON, emits a wired DittoLottie, and lottie-web mounts
and plays the captured animation (rendered SVG viewBox matches the source JSON).
Unit tests cover buildLottieSpec resolution + drop rules.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
devteamaegis
2026-06-30 00:30:52 -04:00
co-authored by claude-flow
parent 7d0e85ddb4
commit 048fd8cdad
7 changed files with 510 additions and 11 deletions
+9 -4
View File
@@ -1,4 +1,5 @@
import type { Page } from "playwright";
import { captureLotties, type LottieSpec } from "./lottie.js";
/**
* Stage 5 motion capture. Runs at the canonical viewport AFTER `tagElements` (so every
@@ -55,6 +56,8 @@ export type MotionCapture = {
rotators: RotatorSpec[];
reveals: RevealSpec[]; // scroll-triggered entrance reveals (start hidden, reveal on view)
marquees: MarqueeSpec[]; // rAF-driven continuous tickers (Framer Motion etc.) — not in getAnimations()
lotties: LottieSpec[]; // lottie-web JSON animations (third-party JS the CSS/WAAPI paths can't reproduce)
lottieInline: Record<string, unknown>; // animationData only available in-memory, keyed by LottieSpec.inlineKey
cssAnimated: number; // elements with a computed animation-name (informational)
};
@@ -329,7 +332,7 @@ export async function captureMotion(page: Page, opts?: { observeMs?: number; log
return { waapi, rotators, reveals, cssAnimated };
}, observeMs),
new Promise<Omit<MotionCapture, "marquees">>((res) => setTimeout(() => res({ waapi: [], rotators: [], reveals: [], cssAnimated: 0 }), observeMs + 4000)),
new Promise<Omit<MotionCapture, "marquees" | "lotties" | "lottieInline">>((res) => setTimeout(() => res({ waapi: [], rotators: [], reveals: [], cssAnimated: 0 }), observeMs + 4000)),
]);
// Scroll-scrub reveals: scroll-linked panels probeReveals can't see (they start only
// partially hidden). Merge into reveals, preferring the probe-confirmed entry when a cap
@@ -341,10 +344,12 @@ export async function captureMotion(page: Page, opts?: { observeMs?: number; log
// Marquees run as a separate pass (scrolls each track into view to wake the paused
// rAF ticker; kept after the rotator MutationObserver window so it can't add false text rotators).
const marquees = await detectMarquees(page);
log({ event: "motion_captured", waapi: result.waapi.length, rotators: result.rotators.length, reveals: reveals.length, marquees: marquees.length, cssAnimated: result.cssAnimated });
return { ...result, reveals, marquees };
// Lottie: third-party JSON animations, captured separately (registry + static markup scan).
const lottie = await captureLotties(page, { log });
log({ event: "motion_captured", waapi: result.waapi.length, rotators: result.rotators.length, reveals: reveals.length, marquees: marquees.length, lotties: lottie.lotties.length, cssAnimated: result.cssAnimated });
return { ...result, reveals, marquees, lotties: lottie.lotties, lottieInline: lottie.inline };
} catch (e) {
log({ event: "motion_error", error: String(e).slice(0, 200) });
return { waapi: [], rotators: [], reveals: [], marquees: [], cssAnimated: 0 };
return { waapi: [], rotators: [], reveals: [], marquees: [], lotties: [], lottieInline: {}, cssAnimated: 0 };
}
}