Fix Lottie replay for Next builds and fallback sources
This commit is contained in:
@@ -89,6 +89,13 @@ export async function captureLotties(
|
|||||||
if (!u || typeof u !== "string") return null;
|
if (!u || typeof u !== "string") return null;
|
||||||
try { return new URL(u, document.baseURI).href; } catch { return null; }
|
try { return new URL(u, document.baseURI).href; } catch { return null; }
|
||||||
};
|
};
|
||||||
|
const registrySrc = (path: string, fileName: string): string | null => {
|
||||||
|
if (!path) return null;
|
||||||
|
if (/\.json(?:[?#]|$)/i.test(path)) return abs(path);
|
||||||
|
if (!fileName) return null;
|
||||||
|
const base = path.endsWith("/") ? path : path + "/";
|
||||||
|
return abs(base + (fileName.includes(".") ? fileName : fileName + ".json"));
|
||||||
|
};
|
||||||
// climb to the nearest ancestor (or self) carrying a data-cid-cap
|
// climb to the nearest ancestor (or self) carrying a data-cid-cap
|
||||||
const capOf = (node: Element | null): string | null => {
|
const capOf = (node: Element | null): string | null => {
|
||||||
let el: Element | null = node;
|
let el: Element | null = node;
|
||||||
@@ -144,10 +151,12 @@ export async function captureLotties(
|
|||||||
// fall back to the in-memory animationData document.
|
// fall back to the in-memory animationData document.
|
||||||
const path = (a.path as string) || "";
|
const path = (a.path as string) || "";
|
||||||
const fileName = (a.fileName as string) || "";
|
const fileName = (a.fileName as string) || "";
|
||||||
let src: string | null = null;
|
const src = registrySrc(path, fileName);
|
||||||
if (path) src = abs(path.endsWith("/") || !fileName ? path + (fileName.endsWith(".json") ? fileName : "data.json") : path + (fileName ? (fileName.includes(".") ? fileName : fileName + ".json") : ""));
|
// Keep in-memory data as a fallback even when the registry reports a URL:
|
||||||
let inlineKey: string | null = null;
|
// some sites expose only a directory-ish path, and guessed `data.json`
|
||||||
if (!src && a.animationData) inlineKey = stashInline(a.animationData);
|
// URLs 404. Generation prefers a downloaded local path, then falls back
|
||||||
|
// to this JSON if the URL is absent or unreachable.
|
||||||
|
const inlineKey = a.animationData ? stashInline(a.animationData) : null;
|
||||||
const rendererType = ((a.renderer as { rendererType?: string })?.rendererType)
|
const rendererType = ((a.renderer as { rendererType?: string })?.rendererType)
|
||||||
|| (a.renderer && a.renderer.constructor && a.renderer.constructor.name === "CanvasRenderer" ? "canvas" : undefined);
|
|| (a.renderer && a.renderer.constructor && a.renderer.constructor.name === "CanvasRenderer" ? "canvas" : undefined);
|
||||||
push({ container, via: "registry", src, inlineKey, renderer: rendererType, loop: a.loop, autoplay: a.autoplay });
|
push({ container, via: "registry", src, inlineKey, renderer: rendererType, loop: a.loop, autoplay: a.autoplay });
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ export function lottieWireJsx(spec: LottieSpec, indent: number): string {
|
|||||||
|
|
||||||
export const DITTO_LOTTIE_TSX = `"use client";
|
export const DITTO_LOTTIE_TSX = `"use client";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import lottie from "lottie-web";
|
|
||||||
|
|
||||||
type RTLottie = {
|
type RTLottie = {
|
||||||
cid: string;
|
cid: string;
|
||||||
@@ -127,28 +126,33 @@ const byCid = (cid: string): HTMLElement | null => document.querySelector('[data
|
|||||||
export default function DittoLottie({ spec }: { spec: LottieSpec }) {
|
export default function DittoLottie({ spec }: { spec: LottieSpec }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const stopped = (window as any).__dittoMotionStopped === true;
|
const stopped = (window as any).__dittoMotionStopped === true;
|
||||||
const anims: Array<ReturnType<typeof lottie.loadAnimation>> = [];
|
const anims: Array<{ destroy: () => void; goToAndStop: (value: number, isFrame?: boolean) => void }> = [];
|
||||||
for (const it of spec.items) {
|
let cancelled = false;
|
||||||
const el = byCid(it.cid);
|
void (async () => {
|
||||||
if (!el) continue;
|
const lottie = (await import("lottie-web")).default;
|
||||||
// clear the captured placeholder frame so it never stacks with the live render
|
if (cancelled) return;
|
||||||
el.innerHTML = "";
|
for (const it of spec.items) {
|
||||||
try {
|
const el = byCid(it.cid);
|
||||||
const anim = lottie.loadAnimation({
|
if (!el) continue;
|
||||||
container: el,
|
// clear the captured placeholder frame so it never stacks with the live render
|
||||||
renderer: it.renderer === "canvas" ? "canvas" : "svg",
|
el.innerHTML = "";
|
||||||
loop: it.loop,
|
try {
|
||||||
autoplay: it.autoplay && !stopped,
|
const anim = lottie.loadAnimation({
|
||||||
...(it.path ? { path: it.path } : { animationData: it.animationData as object }),
|
container: el,
|
||||||
rendererSettings: { preserveAspectRatio: "xMidYMid meet" },
|
renderer: it.renderer === "canvas" ? "canvas" : "svg",
|
||||||
});
|
loop: it.loop,
|
||||||
if (stopped) { try { anim.goToAndStop(0, true); } catch {} }
|
autoplay: it.autoplay && !stopped,
|
||||||
anims.push(anim);
|
...(it.path ? { path: it.path } : { animationData: it.animationData as object }),
|
||||||
} catch {
|
rendererSettings: { preserveAspectRatio: "xMidYMid meet" },
|
||||||
/* a single bad animation must not break the page */
|
});
|
||||||
|
if (stopped) { try { anim.goToAndStop(0, true); } catch {} }
|
||||||
|
anims.push(anim);
|
||||||
|
} catch {
|
||||||
|
/* a single bad animation must not break the page */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})().catch(() => {});
|
||||||
return () => { for (const a of anims) { try { a.destroy(); } catch {} } };
|
return () => { cancelled = true; for (const a of anims) { try { a.destroy(); } catch {} } };
|
||||||
}, [spec]);
|
}, [spec]);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,20 @@ describe("buildLottieSpec", () => {
|
|||||||
assert.equal(spec.items[0]!.renderer, "canvas");
|
assert.equal(spec.items[0]!.renderer, "canvas");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("falls back to inline animationData when a source URL was detected but not downloaded", () => {
|
||||||
|
const ir = irWith(node("n0", {}, [node("n6", { "data-cid-cap": "cap-1" })]));
|
||||||
|
const motion = emptyMotion({
|
||||||
|
lotties: [lottie({ src: "https://x/missing.json", inlineKey: "k0" })],
|
||||||
|
lottieInline: { k0: { v: "5.7", layers: [] } },
|
||||||
|
});
|
||||||
|
const graph: AssetGraph = { entries: [], byUrl: new Map() };
|
||||||
|
|
||||||
|
const spec = buildLottieSpec(ir, motion, graph);
|
||||||
|
assert.equal(spec.items.length, 1);
|
||||||
|
assert.equal(spec.items[0]!.path, null);
|
||||||
|
assert.deepEqual(spec.items[0]!.animationData, { v: "5.7", layers: [] });
|
||||||
|
});
|
||||||
|
|
||||||
it("drops a lottie whose container node didn't survive into the IR", () => {
|
it("drops a lottie whose container node didn't survive into the IR", () => {
|
||||||
const ir = irWith(node("n0", {}, [node("n6", { "data-cid-cap": "other-cap" })]));
|
const ir = irWith(node("n0", {}, [node("n6", { "data-cid-cap": "other-cap" })]));
|
||||||
const motion = emptyMotion({ lotties: [lottie({ src: "https://x/anim.json" })] });
|
const motion = emptyMotion({ lotties: [lottie({ src: "https://x/anim.json" })] });
|
||||||
|
|||||||
Reference in New Issue
Block a user