Capture & emission fidelity wave: dotLottie support, scroll-state isolation, authored-geometry trust, honest quality scoring
Capture: - Preserve real asset extensions (.lottie no longer truncated); extract the default animation JSON from dotLottie ZIP containers at materialization (minimal built-in ZIP reader, no new deps) - Reset scroll + settle before every viewport snapshot so scroll-linked styles can't bake into captured computed values - Marquee detection now requires content overflow (>1.35x), sustained velocity across separated windows, scroll independence (jiggle test), and genuine content repetition — kills scroll-settle-lerp false positives - Sizing probe trusts authored explicit heights (px/vh/calc) over circular parent/child reflow verdicts, fixing dropped 100vh heroes Normalize/generate: - Canonicalize identity transforms to "none" per viewport; emit explicit transform resets across bands when any band has a real transform - DittoLottie keeps the captured placeholder until DOMLoaded; failed loads no longer blank the container - mx-auto only for true auto-margin centering; literal per-band margins are preserved (fixes full-bleed regressions) - Spacing-scale snap tightened to 0.25px (3.5px stays p-[3.5px]); emit whitespace-nowrap for wrap-vulnerable single-line text leaves - Grid recipes no longer override captured track counts or column spans; responsive re-flow plans apply only when geometry agrees Quality scoring: - Rewritten 6-dimension rubric (payload, decomposition, duplication, semantics, hygiene, runtime) with catastrophe caps and per-dimension reporting; thresholds are documented calibration guides 218 tests pass (74 new), typecheck clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
77be868ee3
commit
d3ec154bae
@@ -126,7 +126,7 @@ const byCid = (cid: string): HTMLElement | null => document.querySelector('[data
|
||||
export default function DittoLottie({ spec }: { spec: LottieSpec }) {
|
||||
useEffect(() => {
|
||||
const stopped = (window as any).__dittoMotionStopped === true;
|
||||
const anims: Array<{ destroy: () => void; goToAndStop: (value: number, isFrame?: boolean) => void }> = [];
|
||||
const anims: Array<{ destroy: () => void; goToAndStop: (value: number, isFrame?: boolean) => void; addEventListener: (name: string, cb: () => void) => void }> = [];
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const lottie = (await import("lottie-web")).default;
|
||||
@@ -134,17 +134,38 @@ export default function DittoLottie({ spec }: { spec: LottieSpec }) {
|
||||
for (const it of spec.items) {
|
||||
const el = byCid(it.cid);
|
||||
if (!el) continue;
|
||||
// clear the captured placeholder frame so it never stacks with the live render
|
||||
el.innerHTML = "";
|
||||
try {
|
||||
// Mount the live animation into an OVERLAY child, keeping the captured placeholder
|
||||
// frame in the DOM until lottie signals a successful load — a failed load (bad JSON,
|
||||
// network) then leaves the placeholder intact instead of erasing the container to blank.
|
||||
const mount = document.createElement("div");
|
||||
mount.style.position = "absolute";
|
||||
mount.style.inset = "0";
|
||||
mount.style.opacity = "0";
|
||||
const cs = getComputedStyle(el);
|
||||
if (cs.position === "static") el.style.position = "relative";
|
||||
el.appendChild(mount);
|
||||
const anim = lottie.loadAnimation({
|
||||
container: el,
|
||||
container: mount,
|
||||
renderer: it.renderer === "canvas" ? "canvas" : "svg",
|
||||
loop: it.loop,
|
||||
autoplay: it.autoplay && !stopped,
|
||||
...(it.path ? { path: it.path } : { animationData: it.animationData as object }),
|
||||
rendererSettings: { preserveAspectRatio: "xMidYMid meet" },
|
||||
});
|
||||
// Swap only once the animation is genuinely ready: reveal the live render and remove
|
||||
// every original child (the placeholder) so the two never stack.
|
||||
const reveal = () => {
|
||||
mount.style.opacity = "1";
|
||||
for (const child of Array.from(el.childNodes)) if (child !== mount) el.removeChild(child);
|
||||
mount.style.position = "";
|
||||
mount.style.inset = "";
|
||||
};
|
||||
// DOMLoaded fires only after the JSON parsed and the first frame rendered — the
|
||||
// right moment to reveal the live render and drop the placeholder. data_failed
|
||||
// (bad JSON / fetch error) tears the empty mount back out, leaving the placeholder.
|
||||
anim.addEventListener("DOMLoaded", reveal);
|
||||
anim.addEventListener("data_failed", () => { try { el.removeChild(mount); } catch {} });
|
||||
if (stopped) { try { anim.goToAndStop(0, true); } catch {} }
|
||||
anims.push(anim);
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user