Add web animation skills (iart-ai/web-animation-skills)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GYeokZtSmqg42ZGWsLXX2C
This commit is contained in:
42 files changed
+4170
No files matched your search
Whitespace-only changes.
@@ -0,0 +1,180 @@
|
||||
---
|
||||
name: lottie-animation
|
||||
description: This skill should be used when the user asks to "add a Lottie animation", "play a .lottie or .json animation", "integrate a Bodymovin/After Effects animation on web or mobile", "control Lottie playback / segments", "make a scroll-driven Lottie", "recolor a Lottie at runtime", or "export an AE animation to Lottie". Covers dotLottie/lottie-web integration, playback control, interactivity, theming, and the AE export checklist.
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# Lottie Animation
|
||||
|
||||
The bridge from After Effects to product: vector animation shipped as JSON (or zipped `.lottie`), tiny and resolution-independent, played by a runtime on web, iOS, Android, and React Native. This skill covers integrating, controlling, and exporting Lottie.
|
||||
|
||||
## When to use
|
||||
|
||||
- Ship a designer's AE animation to web/iOS/Android/React Native
|
||||
- Looping illustrations, onboarding, empty states, animated icons
|
||||
- Scroll-driven or interaction-driven vector playback (hover, click, cursor)
|
||||
- Runtime theming/recoloring instead of re-exporting per brand
|
||||
|
||||
## Formats: .json vs .lottie
|
||||
|
||||
- `.json` — the raw Bodymovin output. Human-readable, larger.
|
||||
- `.lottie` — a zipped container (often 60–80% smaller) that can bundle multiple animations, images, and theming. Preferred for shipping. Player: `@lottiefiles/dotlottie-web` (modern, canvas + WASM) supersedes the older `lottie-web`.
|
||||
|
||||
Prefer the **dotLottie** runtime for new work; use `lottie-web` only for legacy `.json` + SVG renderer needs.
|
||||
|
||||
## Core workflow (web, dotLottie)
|
||||
|
||||
```bash
|
||||
npm i @lottiefiles/dotlottie-web # vanilla
|
||||
npm i @lottiefiles/dotlottie-react # React wrapper
|
||||
```
|
||||
|
||||
Vanilla:
|
||||
|
||||
```js
|
||||
import { DotLottie } from "@lottiefiles/dotlottie-web";
|
||||
|
||||
const dotLottie = new DotLottie({
|
||||
canvas: document.querySelector("#lottie"), // a <canvas> element
|
||||
src: "/hero.lottie", // or .json
|
||||
loop: true,
|
||||
autoplay: true,
|
||||
// speed: 1, mode: "forward" | "reverse" | "bounce", renderConfig: { autoResize: true }
|
||||
});
|
||||
```
|
||||
|
||||
React:
|
||||
|
||||
```jsx
|
||||
import { DotLottieReact } from "@lottiefiles/dotlottie-react";
|
||||
|
||||
function Hero() {
|
||||
return <DotLottieReact src="/hero.lottie" loop autoplay style={{ width: 320 }} />;
|
||||
}
|
||||
```
|
||||
|
||||
Capture the instance to control it:
|
||||
|
||||
```jsx
|
||||
const [dl, setDl] = useState(null);
|
||||
<DotLottieReact src="/icon.lottie" dotLottieRefCallback={setDl} />;
|
||||
// dl?.play(); dl?.pause(); dl?.setSpeed(2);
|
||||
```
|
||||
|
||||
## Playback control
|
||||
|
||||
```js
|
||||
dotLottie.play();
|
||||
dotLottie.pause();
|
||||
dotLottie.stop();
|
||||
dotLottie.setSpeed(1.5);
|
||||
dotLottie.setMode("bounce"); // forward | reverse | bounce | reverse-bounce
|
||||
dotLottie.setFrame(42); // jump to a frame
|
||||
dotLottie.setSegment(30, 90); // constrain playback to a frame range (e.g. a "loading" loop)
|
||||
```
|
||||
|
||||
Events drive sequencing and UI:
|
||||
|
||||
```js
|
||||
dotLottie.addEventListener("load", () => { /* totalFrames now available */ });
|
||||
dotLottie.addEventListener("complete", () => { /* non-looping playback finished */ });
|
||||
dotLottie.addEventListener("frame", ({ currentFrame }) => { /* per-frame */ });
|
||||
```
|
||||
|
||||
State-machine style (e.g. button that plays "checked" segment then idles): play a segment, listen for `complete`, then `setSegment` to the idle loop.
|
||||
|
||||
## Scroll-driven Lottie (no GSAP needed)
|
||||
|
||||
Map scroll progress (0–1) to a frame. Pause autoplay and drive frames yourself.
|
||||
|
||||
```js
|
||||
const dotLottie = new DotLottie({ canvas, src: "/scroll.lottie", autoplay: false });
|
||||
let total = 0;
|
||||
dotLottie.addEventListener("load", () => { total = dotLottie.totalFrames; });
|
||||
|
||||
window.addEventListener("scroll", () => {
|
||||
const el = canvas.closest(".scrolly");
|
||||
const rect = el.getBoundingClientRect();
|
||||
const p = clamp(-rect.top / (rect.height - window.innerHeight), 0, 1); // 0..1 through the section
|
||||
dotLottie.setFrame(p * (total - 1));
|
||||
}, { passive: true });
|
||||
|
||||
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
||||
```
|
||||
|
||||
For richer interaction (hover-to-play, click toggles, cursor-follow) without writing the wiring, use `@lottiefiles/lottie-interactivity`, which maps `scroll`, `hover`, `click`, `play`, and `cursor` modes to frame ranges declaratively.
|
||||
|
||||
## Runtime theming / recoloring
|
||||
|
||||
Two paths:
|
||||
- **dotLottie themes**: a `.lottie` can embed named color themes; switch with `dotLottie.setTheme("dark")` / `loadTheme(...)`. Authored in LottieFiles tooling — no re-export to recolor.
|
||||
- **Manual color override**: load the JSON, walk `layers[].shapes[]` and patch `c.k` color arrays (normalized 0–1 RGBA), then init the player with the modified object. Brittle (depends on layer structure) — prefer themes or CSS-driven SVG-renderer (`lottie-web` `rendererSettings`) when you must restyle by class.
|
||||
|
||||
## Mobile runtimes (parity)
|
||||
|
||||
- iOS — `lottie-ios` (`LottieAnimationView`): `.play()`, `.currentProgress`, `.play(fromFrame:toFrame:)`.
|
||||
- Android — `lottie-android` (`LottieAnimationView` / Compose `LottieAnimation`): `setMinAndMaxFrame`, `progress`.
|
||||
- React Native — `lottie-react-native` (`<LottieView source={...} progress={anim} />`, drive `progress` with `Animated`).
|
||||
|
||||
The same `.lottie`/`.json` plays across all; segments and progress concepts match the web API.
|
||||
|
||||
## Optimization
|
||||
|
||||
- Ship `.lottie` (zipped) over raw `.json`; lazy-load offscreen players and pause when not visible (`IntersectionObserver` → `play`/`pause`).
|
||||
- Prefer the canvas/WASM dotLottie renderer for many simultaneous animations; SVG renderer (lottie-web) is fine for one crisp icon but costs more DOM for complex files.
|
||||
- Keep AE comps small: fewer layers/keyframes, no large embedded rasters; flatten precomps where possible. File size tracks keyframe and path complexity, not duration.
|
||||
- Recolor at runtime (themes) instead of exporting a file per brand/theme.
|
||||
|
||||
## Deliver & verify (standalone HTML)
|
||||
|
||||
> **Packaged helper** (`scripts/`): `scripts/seek-shot.sh anim.html 0 1.5 3` freezes the `?t=N` harness and screenshots each moment; `scripts/contact-sheet.sh sheet.png frame-*.png` tiles them for one-glance review. See `scripts/README.md`.
|
||||
|
||||
For a self-contained Lottie demo (looping illustration, animated icon, empty state) the deliverable is **one HTML file that opens directly in a browser** — the dotLottie/lottie-web runtime from CDN, a `<canvas>`, and your `.lottie`/`.json` (a relative file or a data URL). No build step. One file is the right tier for shipping a player; don't reach for a bundler.
|
||||
|
||||
**Output contract:**
|
||||
- One `.html` file: the runtime via CDN (`@lottiefiles/dotlottie-web`), the canvas, and `autoplay:false` so the frame is yours to drive.
|
||||
- Include the seek harness so any frame can be frozen for a screenshot.
|
||||
|
||||
**Seek harness — freeze an exact frame.** `?t=N` jumps to a frame and holds it so a screenshot lands on a still, deterministic frame. Lottie's playhead is in frames, so stop on the frame directly:
|
||||
|
||||
```html
|
||||
<script>
|
||||
const dl = new DotLottie({ canvas, src: "/icon.lottie", autoplay: false });
|
||||
dl.addEventListener("load", () => {
|
||||
const t = new URLSearchParams(location.search).get("t");
|
||||
if (t !== null) dl.setFrame(parseFloat(t)); // freeze on frame N (lottie-web: anim.goToAndStop(N, true))
|
||||
else dl.play();
|
||||
console.log("totalFrames", dl.totalFrames); // read for end-frame
|
||||
window.__ready = true;
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
**Verify loop — render → freeze → screenshot → check:** open the file at first / mid / last frame (`?t=0`, `?t=<total/2>`, `?t=<total-1>`; read `totalFrames` from the console), screenshot each, and check **fidelity** (colors/theme correct, segment loops where intended) plus **artifacts** (canvas blurry from missing `autoResize`/DPR, clipped composition, FOUC before the file loads, jank). Any headless tool works:
|
||||
|
||||
```bash
|
||||
npx playwright screenshot --wait-for-timeout=600 "file://$PWD/lottie.html?t=42" frame-mid.png
|
||||
```
|
||||
|
||||
**Before you finish:**
|
||||
1. Opens standalone — no console errors, CDN runtime and the `.lottie`/`.json` load.
|
||||
2. `?t=N` (`setFrame`/`goToAndStop`) freezes the correct, deterministic frame.
|
||||
3. Screenshotted at first / mid / last frame — matches the brief, sharp, no clipping.
|
||||
4. `prefers-reduced-motion` honored — don't `autoplay` a loop; show a static frame or offer a play control.
|
||||
5. Easing is intentional — playback speed/segment chosen on purpose, motion baked in AE reads as designed.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Goal | Call |
|
||||
|------|------|
|
||||
| Play a range | `setSegment(start, end)` |
|
||||
| Jump to frame | `setFrame(n)` |
|
||||
| Reverse / bounce | `setMode("reverse"|"bounce")` |
|
||||
| Scroll-drive | `autoplay:false` + `setFrame(p*totalFrames)` |
|
||||
| Theme swap | `setTheme("dark")` |
|
||||
| Know length | `totalFrames` after `load` event |
|
||||
| Declarative interactivity | `@lottiefiles/lottie-interactivity` |
|
||||
|
||||
## Reference files
|
||||
|
||||
- `references/integration-and-export.md` — full dotLottie-web + React setup and event/segment control, `lottie-interactivity` mode configs, scroll/cursor patterns, runtime theming details, and the complete After Effects → Bodymovin export checklist with the list of unsupported AE features and how to work around them.
|
||||
@@ -0,0 +1,233 @@
|
||||
# Lottie Integration & Export (detailed)
|
||||
|
||||
## 1. dotLottie-web full setup
|
||||
|
||||
```bash
|
||||
npm i @lottiefiles/dotlottie-web
|
||||
```
|
||||
|
||||
```js
|
||||
import { DotLottie } from "@lottiefiles/dotlottie-web";
|
||||
|
||||
const dotLottie = new DotLottie({
|
||||
canvas: document.querySelector("#lottie"),
|
||||
src: "/assets/onboarding.lottie", // .lottie or .json (URL or same-origin path)
|
||||
autoplay: true,
|
||||
loop: true,
|
||||
speed: 1,
|
||||
mode: "forward", // forward | reverse | bounce | reverse-bounce
|
||||
useFrameInterpolation: true, // smoother than the source frame rate
|
||||
renderConfig: {
|
||||
autoResize: true, // keep the canvas crisp on resize/DPR changes
|
||||
devicePixelRatio: window.devicePixelRatio,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The canvas MUST be a real `<canvas>` element. For responsiveness, size the canvas via CSS and keep `autoResize: true`; the runtime handles the backing store.
|
||||
|
||||
### Lifecycle and events
|
||||
|
||||
```js
|
||||
dotLottie.addEventListener("load", () => console.log(dotLottie.totalFrames, dotLottie.duration));
|
||||
dotLottie.addEventListener("loadError", (e) => console.error("bad src", e));
|
||||
dotLottie.addEventListener("play", () => {});
|
||||
dotLottie.addEventListener("pause", () => {});
|
||||
dotLottie.addEventListener("complete", () => {}); // fires when a non-looping play finishes
|
||||
dotLottie.addEventListener("loop", ({ loopCount }) => {});
|
||||
dotLottie.addEventListener("frame", ({ currentFrame }) => {});
|
||||
|
||||
// always clean up (SPA/unmount)
|
||||
dotLottie.destroy();
|
||||
```
|
||||
|
||||
`totalFrames`, `duration`, and segment data are only valid AFTER the `load` event — read them there, not synchronously after construction.
|
||||
|
||||
### Multi-animation .lottie files
|
||||
|
||||
A single `.lottie` can hold several animations:
|
||||
|
||||
```js
|
||||
dotLottie.addEventListener("load", () => {
|
||||
console.log(dotLottie.manifest.animations); // list of animation ids
|
||||
});
|
||||
dotLottie.loadAnimation("success"); // switch by id without re-fetching
|
||||
```
|
||||
|
||||
## 2. React wrapper
|
||||
|
||||
```bash
|
||||
npm i @lottiefiles/dotlottie-react
|
||||
```
|
||||
|
||||
```jsx
|
||||
import { DotLottieReact } from "@lottiefiles/dotlottie-react";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
function PlayButton() {
|
||||
const [dl, setDl] = useState(null);
|
||||
const [playing, setPlaying] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dl) return;
|
||||
const onComplete = () => setPlaying(false);
|
||||
dl.addEventListener("complete", onComplete);
|
||||
return () => dl.removeEventListener("complete", onComplete);
|
||||
}, [dl]);
|
||||
|
||||
return (
|
||||
<DotLottieReact
|
||||
src="/play.lottie"
|
||||
autoplay
|
||||
loop={false}
|
||||
dotLottieRefCallback={setDl} // capture instance for imperative control
|
||||
onClick={() => (playing ? dl.pause() : dl.play(), setPlaying((p) => !p))}
|
||||
style={{ width: 200, height: 200 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Props mirror the constructor: `src, autoplay, loop, speed, mode, useFrameInterpolation, renderConfig, themeId, segment`.
|
||||
|
||||
## 3. Segments and state machines
|
||||
|
||||
```js
|
||||
// Loop only the "loading" portion (frames 30–90), then jump to a success segment.
|
||||
dotLottie.setSegment(30, 90);
|
||||
dotLottie.setLoop(true);
|
||||
dotLottie.play();
|
||||
|
||||
function resolveSuccess() {
|
||||
dotLottie.setLoop(false);
|
||||
dotLottie.setSegment(91, 140); // success burst
|
||||
dotLottie.play();
|
||||
}
|
||||
```
|
||||
|
||||
Pattern for icon toggles (e.g. like button): keep distinct frame ranges for each state in one file, and `setSegment` + `play` per interaction; listen for `complete` to settle into an idle range.
|
||||
|
||||
dotLottie also supports embedded **state machines** (`.lottie` with a `state-machine` definition): `dotLottie.stateMachineLoad("sm_id"); dotLottie.stateMachineStart();` then fire inputs/events — useful for complex interactive icons authored in LottieFiles without writing JS state logic.
|
||||
|
||||
## 4. lottie-interactivity (declarative)
|
||||
|
||||
```bash
|
||||
npm i @lottiefiles/lottie-interactivity
|
||||
```
|
||||
|
||||
Works with a `lottie-web`/dotLottie player instance (commonly the `<dotlottie-player>` / `<lottie-player>` web component):
|
||||
|
||||
```html
|
||||
<dotlottie-player id="firstLottie" src="/scroll.lottie" style="width:300px"></dotlottie-player>
|
||||
<script>
|
||||
LottieInteractivity.create({
|
||||
player: "#firstLottie",
|
||||
mode: "scroll",
|
||||
actions: [
|
||||
{ visibility: [0, 0.3], type: "stop", frames: [0] },
|
||||
{ visibility: [0.3, 1.0], type: "seek", frames: [0, 120] }, // map scroll → frames 0..120
|
||||
],
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
Modes:
|
||||
- `scroll` — tie frames to scroll position / element visibility (the canonical scrolly use).
|
||||
- `cursor` — drive frames from cursor position over the element (hover scrub).
|
||||
- `hover` — play on hover, control `loop`/`reset` on leave.
|
||||
- `click` — advance to the next sync state on each click.
|
||||
- `chain` — sequence multiple animations.
|
||||
|
||||
Use `type: "seek"` to scrub frames, `type: "loop"` / `"play"` / `"stop"` for segment behaviors.
|
||||
|
||||
## 5. Manual scroll mapping (full control, no extra lib)
|
||||
|
||||
```js
|
||||
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
||||
let total = 0;
|
||||
dotLottie.addEventListener("load", () => { total = dotLottie.totalFrames - 1; });
|
||||
|
||||
const section = document.querySelector(".scrolly");
|
||||
function update() {
|
||||
const r = section.getBoundingClientRect();
|
||||
const scrollable = r.height - window.innerHeight;
|
||||
const p = clamp(-r.top / scrollable, 0, 1);
|
||||
dotLottie.setFrame(p * total);
|
||||
}
|
||||
window.addEventListener("scroll", update, { passive: true });
|
||||
window.addEventListener("resize", update);
|
||||
```
|
||||
|
||||
Pause when offscreen to save CPU:
|
||||
|
||||
```js
|
||||
new IntersectionObserver(([e]) => e.isIntersecting ? dotLottie.unfreeze() : dotLottie.freeze())
|
||||
.observe(canvas);
|
||||
```
|
||||
|
||||
## 6. Runtime theming
|
||||
|
||||
### Embedded themes (recommended)
|
||||
|
||||
A `.lottie` authored with named color themes:
|
||||
|
||||
```js
|
||||
dotLottie.addEventListener("load", () => {
|
||||
console.log(dotLottie.manifest.themes); // available theme ids
|
||||
dotLottie.setTheme("dark"); // swap colors with no re-fetch / re-export
|
||||
});
|
||||
// or per slot:
|
||||
dotLottie.setThemeData({ /* theme JSON */ });
|
||||
```
|
||||
|
||||
### Manual color patching (last resort)
|
||||
|
||||
```js
|
||||
const json = await fetch("/icon.json").then((r) => r.json());
|
||||
// Lottie colors are normalized 0..1 [r,g,b,a] arrays at shapes[].it[].c.k
|
||||
function recolor(layers, hex) {
|
||||
const [r, g, b] = hexToRgb01(hex);
|
||||
for (const layer of layers) {
|
||||
for (const sh of layer.shapes ?? []) {
|
||||
for (const it of sh.it ?? []) {
|
||||
if (it.ty === "fl" || it.ty === "st") { it.c.k = [r, g, b, 1]; } // fill / stroke
|
||||
}
|
||||
}
|
||||
if (layer.layers) recolor(layer.layers, hex); // precomps
|
||||
}
|
||||
}
|
||||
recolor(json.layers, "#ff5500");
|
||||
new DotLottie({ canvas, data: JSON.stringify(json), autoplay: true });
|
||||
```
|
||||
|
||||
This depends on the exact layer/shape structure and breaks if the file changes — prefer embedded themes, or use the `lottie-web` SVG renderer and target paths with CSS where the markup is stable.
|
||||
|
||||
## 7. After Effects → Bodymovin export checklist
|
||||
|
||||
Plugin: **Bodymovin** (or LottieFiles plugin). `Window → Extensions → Bodymovin`. Select the comp, set destination, configure, Render.
|
||||
|
||||
Pre-export checklist:
|
||||
1. Comp set to a sensible size and frame rate (24/30/60); avoid huge resolutions — Lottie is vector, the canvas scales.
|
||||
2. Name layers/comps clearly — names survive into JSON and aid runtime theming/targeting.
|
||||
3. Convert text you want crisp/animated-by-path to shapes if it uses unsupported text features; otherwise enable "Glyphs"/"Fonts" in Bodymovin settings for live text.
|
||||
4. Trim the work area to exactly the frames you ship; trailing frames bloat the file.
|
||||
5. In Bodymovin settings: enable **Compress assets / .lottie** output for the zipped format; enable "Glyphs" if using text; toggle "Standalone"/"Demo" only as needed.
|
||||
6. Replace embedded raster images with vectors where possible; if rasters are required, they inflate `.json` (consider `.lottie` which bundles them).
|
||||
7. Test the exported file in the LottieFiles preview AND your target runtime before handoff — renderers differ.
|
||||
|
||||
### Unsupported / partially-supported AE features (avoid or rework)
|
||||
|
||||
- **Expressions** — not evaluated by runtimes. Bake expression-driven motion to keyframes (Bodymovin can pre-render some) or remove. Heavy-expression rigs are the #1 source of "looks wrong in product".
|
||||
- **Effects** — most AE effects (blurs, glows, distort, particle systems, CC effects) are NOT supported. Only a small set (e.g. some Gaussian blur, drop shadow, tint, fill via supported attributes) survive, and inconsistently across platforms. Recreate looks with shape layers, gradients, and mattes.
|
||||
- **3D layers / camera** — not supported; fake depth with 2D scaling/position.
|
||||
- **Time remapping / nested time stretch** — flaky; pre-compose and bake.
|
||||
- **Auto-Orient, certain merge-paths modes, some matte combinations** — test explicitly.
|
||||
- **Audio** — not part of Lottie.
|
||||
- **Adjustment layers, layer styles (bevel/emboss/inner shadow)** — largely unsupported; convert to shapes/gradients.
|
||||
- **Track mattes** — alpha/luma mattes generally work but increase cost; flatten when possible.
|
||||
- **Gradients on strokes / complex gradient feathering** — limited; verify.
|
||||
|
||||
Workarounds summary: bake expressions to keyframes, replace effects with native shape/gradient/matte equivalents, flatten precomps, drop rasters, and verify on every target runtime (web/iOS/Android render slightly differently). Keep layer/keyframe count low for performance and file size.
|
||||
|
||||
---
|
||||
Export clean from After Effects and the dotLottie plays light everywhere. Built by **[iart.ai](https://iart.ai/?utm_source=github&utm_medium=readme&utm_campaign=web-animation-skills&utm_content=skill_footer&utm_term=lottie-animation)** — the AI motion agent for editable, on-brand motion graphics.
|
||||
Reference in new issue
Block a user