스킬 불러오는 중
스킬 불러오는 중
iart-ai/data-animation-skills · GitHub
This skill should be used when the user asks to "make an animated infographic", "create an infographic video", "build an infographic animation", "animate an explainer infographic", "do a sequenced reveal of icons and stats", "animate icons with key numbers and connectors", or "lay out a designed infographic that animates in sequence". Covers infographic layout & visual hierarchy, staggered element reveals, animating icons/pictograms, key-number counters, connectors/flow, and section pacing. For heavy charting (bar chart race, charts from a CSV) use the chart-animation skill instead.
프로젝트 폴더에서 아래 명령어를 실행하고, 설치할 에이전트를 선택하세요.
npx skills add iart-ai/data-animation-skills --skill animated-infographic설치 명령을 직접 실행해야 적용됩니다. 지원 에이전트와 필요한 권한·라이선스는 제작자의 안내를 확인하세요.
README.mdreferences/sequenced-infographic.md# Sequenced Infographic — runnable reference
A complete, runnable Remotion scene: a 3-step "How it works" infographic where each step reveals as an **icon pop → eased count-up stat → label**, joined by **draw-on connectors**, ending on a held static frame. Below the Remotion version is a master timing map, a template×data pattern, and a dependency-free inline-SVG variant.
## Mental model
Three clocks, one source:
1. **Frame** — `useCurrentFrame()` is the only clock. Every value is a pure function of it.
2. **Item-local frame** — `frame - itemStartFrame` gives each item its own 0-based timeline, so the same `Step` component animates correctly wherever it sits in the sequence.
3. **Element offset** — within an item, the icon leads (offset 0), the counter follows (offset ~6f), the label trails (offset ~12f). Hierarchy becomes timing.
Design the static layout first (positions below are final), then these clocks only decide *when* each placed element fades in.
## Remotion: `SequencedInfographic.tsx`
```tsx
import {
AbsoluteFill, useCurrentFrame, useVideoConfig, spring, interpolate, Easing, Sequence,
} from "remotion";
import { useRef } from "react";
// ---- Data is a PROP, never hardcoded (enables template × data, see below) ----
export type Step = { icon: "search" | "wrench" | "rocket"; value: number; suffix?: string; label: string };
export type Props = { title: string; steps: Step[]; theme?: Partial<Theme> };
type Theme = { bg: string; ink: string; sub: string; accent: string; line: string; font: string };
const DEFAULT_THEME: Theme = {
bg: "#0E1116", ink: "#F5F7FA", sub: "#9AA4B2", accent: "#5B8DEF", line: "#3A4250",
font: "Inter, system-ui, sans-serif",
};
// Timing constants (frames @ 30fps) — the choreography lives here
const TITLE_HOLD = 30; // 1.0s before steps begin
const STEP_STRIDE = 48; // 1.6s per step (pop → count → label, then connector overlaps)
const ICON_OFFSET = 0;
const COUNT_OFFSET = 6;
const LABEL_OFFSET = 14;
const CONNECTOR_OFFSET = 30; // draws after the source step, leading into the next
// ---------- Count-up number that settles (ease-out cubic) ----------
function Counter({ from = 0, to, suffix = "", startFrame }: { from?: number; to: number; suffix?: string; startFrame: number }) {
const frame = useCurrentFrame();
const raw = interpolate(frame - startFrame, [0, 30], [from, to], {
extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.out(Easing.cubic),
});
const n = Math.round(raw);
const text = `${new Intl.NumberFormat("en-US").format(n)}${suffix}`;
return <span style={{ fontVariantNumeric: "tabular-nums", lineHeight: 1 }}>{text}</span>;
}
// ---------- Minimal inline icon set (replace with your own SVG paths) ----------
function Icon({ name, color }: { name: Step["icon"]; color: string }) {
const paths: Record<Step["icon"], string> = {
search: "M11 19a8 8 0 1 1 5.3-2L21 21",
wrench: "M14 7a4 4 0 0 1-5 5L4 17l3 3 5-5a4 4 0 0 1 5-5l-3-3z",
rocket: "M5 15l4 4M9 11a8 8 0 0 1 9-6 8 8 0 0 1-6 9l-3 3-3-3 3-3z",
};
return (
<svg width={56} height={56} viewBox="0 0 24 24" fill="none"
stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
<path d={paths[name]} />
</svg>
);
}
// ---------- One step: icon pops, number counts, label fades — all on item-local frame ----------
function StepCard({ step, startFrame, theme }: { step: Step; startFrame: number; theme: Theme }) {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const local = frame - startFrame;
// Icon: spring with overshoot ("pop")
const pop = spring({ frame: local - ICON_OFFSET, fps, config: { damping: 10, stiffness: 180, mass: 0.5 } });
// Label: plain fade + rise (no drama — it's tier 4)
const labelIn = spring({ frame: local - LABEL_OFFSET, fps, config: { damping: 16, mass: 0.6 } });
return (
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 12, width: 240 }}>
<div style={{
width: 96, height: 96, borderRadius: 24, display: "grid", placeItems: "center",
background: "rgba(91,141,239,0.12)", transform: `scale(${pop})`, transformOrigin: "center",
}}>
<Icon name={step.icon} color={theme.accent} />
</div>
<div style={{ color: theme.ink, fontSize: 52, fontWeight: 800 }}>
<Counter to={step.value} suffix={step.suffix} startFrame={startFrame + COUNT_OFFSET} />
</div>
<div style={{
color: theme.sub, fontSize: 20, textAlign: "center",
opacity: labelIn, transform: `translateY(${(1 - labelIn) * 12}px)`,
}}>
{step.label}
</div>
</div>
);
}
// ---------- Connector: a horizontal line that draws on between two steps ----------
function Connector({ startFrame }: { startFrame: number }) {
const frame = useCurrentFrame();
const ref = useRef<SVGPathElement>(null);
const len = ref.current?.getTotalLength?.() ?? 120;
const draw = interpolate(frame - startFrame, [0, 18], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" });
return (
<svg width={120} height={48} viewBox="0 0 120 48" style={{ overflow: "visible" }}>
<path ref={ref} d="M8 24 H112" fill="none" stroke="#3A4250" strokeWidth={3} strokeLinecap="round"
strokeDasharray={len} strokeDashoffset={len * (1 - draw)} />
<path d="M104 18 L114 24 L104 30" fill="none" stroke="#3A4250" strokeWidth={3}
strokeLinecap="round" strokeLinejoin="round"
style={{ opacity: interpolate(frame - startFrame, [14, 18], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp" }) }} />
</svg>
);
}
export const SequencedInfographic: React.FC<Props> = ({ title, steps, theme: t }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const theme = { ...DEFAULT_THEME, ...t };
const titleIn = spring({ frame, fps, config: { damping: 18, mass: 0.7 } });
return (
<AbsoluteFill style={{ background: theme.bg, fontFamily: theme.font, padding: 80,
display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center", gap: 64 }}>
<h1 style={{ color: theme.ink, fontSize: 56, fontWeight: 800, margin: 0, textAlign: "center",
opacity: titleIn, transform: `translateY(${(1 - titleIn) * 20}px)` }}>
{title}
</h1>
<div style={{ display: "flex", alignItems: "flex-start", gap: 8 }}>
{steps.map((step, i) => {
const start = TITLE_HOLD + i * STEP_STRIDE;
return (
<div key={`${step.label}-${i}`} style={{ display: "flex", alignItems: "center", gap: 8 }}>
<StepCard step={step} startFrame={start} theme={theme} />
{i < steps.length - 1 && (
// connector draws ~1s after this step starts, leading into the next
<div style={{ marginTop: 36 }}>
<Connector startFrame={start + CONNECTOR_OFFSET} />
</div>
)}
</div>
);
})}
</div>
</AbsoluteFill>
);
};
```
### Register it (`Root.tsx`)
```tsx
import { Composition } from "remotion";
import { SequencedInfographic } from "./SequencedInfographic";
const DEMO = {
title: "How it works",
steps: [
{ icon: "search", value: 3, suffix: "k+", label: "Sources scanned" },
{ icon: "wrench", value: 92, suffix: "%", label: "Auto-matched" },
{ icon: "rocket", value: 1287, suffix: "", label: "Reports shipped" },
],
} as const;
export const Root = () => (
<Composition
id="SequencedInfographic"
component={SequencedInfographic}
durationInFrames={TITLE_HOLD_PLUS_STEPS()} // e.g. 30 + 3*48 + 90 hold ≈ 264
fps={30}
width={1920}
height={1080}
defaultProps={DEMO}
/>
);
function TITLE_HOLD_PLUS_STEPS() { return 30 + 3 * 48 + 90; }
```
```bash
npm i remotion @remotion/cli react react-dom
npx remotion studio # preview, scrub the timeline
npx remotion render SequencedInfographic out/infographic.mp4
```
## Master timing map (3 steps, 30fps)
| Frame | s | Event |
|---|---|---|
| 0–30 | 0.0–1.0 | Title springs in, holds |
| 30 | 1.0 | Step 1 icon pops |
| 36 | 1.2 | Step 1 counter starts (settles by ~66 / 2.2s) |
| 44 | 1.5 | Step 1 label fades up |
| 60 | 2.0 | Connector 1 draws → arrow into step 2 |
| 78 | 2.6 | Step 2 icon pops (stride 48f) |
| 126 | 4.2 | Step 3 icon pops |
| ~174 | 5.8 | All elements settled |
| 174–264 | 5.8–8.8 | **Held static infographic** (screenshot-able) |
Only one counter runs at a time (steps are 1.6s apart, a counter settles in ~1s) — the eye is never asked to read two moving numbers.
## Template × data — one design, many infographics
`steps` is already a prop, so the same composition renders any dataset. Keep theme/layout fixed; change only the data.
```bash
# render the same infographic template for every dataset in /data
for f in data/*.json; do
name=$(basename "$f" .json)
npx remotion render SequencedInfographic "out/$name.mp4" --props="$f"
done
```
```json
// data/q2-results.json — shape matches Props
{
"title": "Q2 by the numbers",
"steps": [
{ "icon": "search", "value": 48, "suffix": "k", "label": "New signups" },
{ "icon": "wrench", "value": 99, "suffix": "%", "label": "Uptime" },
{ "icon": "rocket", "value": 2400000,"suffix": "", "label": "API calls/day" }
]
}
```
Validate before rendering: confirm `steps.length` is 3–5, every `value` is a finite number, and `icon` is one of the known keys — bad data should fail loudly, not render a broken frame.
## Dependency-free variant (inline SVG + Web Animations API)
For a landing page or a quick capture without Remotion. The same choreography — staggered icon pop, counter, connector draw — driven by `requestAnimationFrame` (fine for live playback; for frame-exact MP4 export prefer the Remotion version above).
```html
<div id="infographic" style="display:flex;gap:8px;font-family:Inter,sans-serif;background:#0E1116;padding:60px">
<template id="step">
<div class="card" style="opacity:0;width:220px;text-align:center">
<div class="icon" style="display:inline-grid;place-items:center;width:96px;height:96px;border-radius:24px;
background:rgba(91,141,239,.12);transform:scale(0)"></div>
<div class="num" style="color:#F5F7FA;font-size:52px;font-weight:800;font-variant-numeric:tabular-nums">0</div>
<div class="lbl" style="color:#9AA4B2;font-size:20px"></div>
</div>
</template>
</div>
<script>
const STEPS = [
{ icon:"🔍", value:3, suffix:"k+", label:"Sources scanned" },
{ icon:"🔧", value:92, suffix:"%", label:"Auto-matched" },
{ icon:"🚀", value:1287, suffix:"", label:"Reports shipped" },
];
const root = document.getElementById("infographic");
const tpl = document.getElementById("step");
const easeOutCubic = t => 1 - Math.pow(1 - t, 3);
STEPS.forEach((s, i) => {
const node = tpl.content.cloneNode(true);
const card = node.querySelector(".card");
card.querySelector(".icon").textContent = s.icon;
card.querySelector(".lbl").textContent = s.label;
const numEl = card.querySelector(".num");
root.appendChild(node);
const delay = i * 480; // 480ms stagger between steps
card.animate([{opacity:0},{opacity:1}], {duration:300, delay, fill:"forwards"});
// icon pop with overshoot
card.querySelector(".icon").animate(
[{transform:"scale(0)"},{transform:"scale(1.15)"},{transform:"scale(1)"}],
{duration:500, delay:delay+60, fill:"forwards", easing:"cubic-bezier(.34,1.56,.64,1)"});
// counter (ease-out), starts after the icon
const t0 = delay + 200, dur = 900;
const tick = now => {
const p = Math.min(1, (now - start - t0) / dur);
if (p >= 0) numEl.textContent = Math.round(easeOutCubic(p) * s.value).toLocaleString() + s.suffix;
if (p < 1) requestAnimationFrame(tick);
};
let start; requestAnimationFrame(n => { start = n; requestAnimationFrame(tick); });
});
</script>
```
Swap the emoji for inline `<svg>` icons in production, and add SVG `<path>` connectors with an animated `stroke-dashoffset` (same draw-on technique as the Remotion `Connector`) to join the cards.
---
## Built by the team behind iart.ai
This skill is part of an open motion-graphics collection from iart.ai — the AI motion agent that turns data, scripts, and designs into editable motion graphics (Remotion → MP4). If you'd rather not hand-build this, iart.ai can build data-driven animated infographics from a template — change the text/data and re-export. → [iart.ai](https://iart.ai/?utm_source=github&utm_medium=reference&utm_campaign=data-animation-skills&utm_content=ref_footer&utm_term=animated-infographic)
SKILL.md---
name: animated-infographic
description: This skill should be used when the user asks to "make an animated infographic", "create an infographic video", "build an infographic animation", "animate an explainer infographic", "do a sequenced reveal of icons and stats", "animate icons with key numbers and connectors", or "lay out a designed infographic that animates in sequence". Covers infographic layout & visual hierarchy, staggered element reveals, animating icons/pictograms, key-number counters, connectors/flow, and section pacing. For heavy charting (bar chart race, charts from a CSV) use the chart-animation skill instead.
version: 0.1.0
---
# Animated Infographic
Build a *designed* infographic — icons, a few key numbers, short text, and simple shapes arranged with real visual hierarchy — then animate it in sequence so each element earns its moment. The craft is composition first, choreography second: a static layout that reads on its own, revealed in a staggered cascade that guides the eye exactly where you want it.
## When to use
- Mixed-layout explainers: "X in 3 steps", "how it works", "by the numbers", a stat-card grid.
- Sequenced reveal of icon + stat + label + connector as a single designed scene.
- Pictogram/icon animation, count-up key numbers, flow connectors between steps.
This skill owns the **composed infographic**. For a chart that *is* the content (bar chart race, animated line/area, a graph driven by a CSV), use **chart-animation** — and compose its chart as one block inside an infographic here rather than rebuilding it.
## The one rule: design the static frame first
A great animated infographic is a great *static* infographic that happens to move. Lay out and balance the full composition — every icon, number, and label in its final position — before adding a single keyframe. Animation then only controls *when* each already-placed element appears; it never decides layout. This prevents the most common failure: elements that fly to positions that were never designed, so the final held frame looks accidental.
Scope tightly: one central insight, **3–5 supporting data points**, in 30–90s. More than five competing elements and the cascade reads as chaos.
## Visual hierarchy → reveal order
Reveal order *is* the hierarchy. The eye follows appearance, so animate elements in importance order, not layout order. Map each element to a tier, then reveal tier by tier.
| Tier | Element | Reveal | Motion |
|---|---|---|---|
| 1 | Section title / central insight | first, alone | fade + slight rise |
| 2 | Icon (anchors each item) | per item, leads its group | pop / scale-overshoot |
| 3 | Key number (counter) | right after its icon | count-up, ease-out |
| 4 | Label / caption | settles under the number | fade, no motion drama |
| 5 | Connector / flow line | links items as they complete | draw-on (path length) |
Use size, weight, and color for static hierarchy; use **timing and motion** for the animated layer. One loud thing at a time — never count two numbers at once.
## Stagger: the cascade
Reveal sibling items with a fixed delay between them (a *stagger*) so the group reads as a sequence, not a flash. Drive every element from the current frame as a pure function — never wall-clock time or a library's animation loop — so renders are deterministic.
```jsx
import { useCurrentFrame, spring, useVideoConfig } from "remotion";
const STAGGER = 8; // frames between siblings (~0.27s @30fps)
function Item({ index, children }) {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const local = frame - index * STAGGER; // this item's own clock
const enter = spring({ frame: local, fps, config: { damping: 14, mass: 0.6 } });
return (
<div style={{ opacity: enter, transform: `translateY(${(1 - enter) * 24}px)` }}>
{children}
</div>
);
}
```
A stagger of 6–10 frames feels crisp; above ~15 it drags. Keep one enter curve across all siblings so the cascade reads as confidence, not noise.
## Animating icons: the pop
Icons anchor each item, so give them the strongest entrance — a scale-overshoot ("pop") that settles. Use a spring with low damping for the bounce; never linear scale (reads mechanical).
```jsx
const pop = spring({ frame: local, fps, config: { damping: 10, stiffness: 180, mass: 0.5 } });
// pop overshoots past 1 then settles → lively
<g transform={`scale(${pop})`} style={{ transformOrigin: "center" }}>{icon}</g>
```
For pictogram fills (e.g. "7 of 10 people"), reveal units on the same stagger and clip the partial unit with a mask rather than scaling it.
## Key numbers: counters that settle
Interpolate the underlying number, ease it, then format on render. Two musts: **round before formatting**, and use `tabular-nums` so the layout doesn't jitter as digits change. (Shared craft with chart-animation — see its counter section for currency/percent variants.)
```jsx
import { interpolate, Easing } from "remotion";
const raw = interpolate(frame - delay, [0, 30], [0, 1287], {
extrapolateLeft: "clamp", extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic), // "settling on a number"
});
const label = new Intl.NumberFormat("en-US").format(Math.round(raw)); // "1,287"
// CSS: font-variant-numeric: tabular-nums; line-height: 1;
```
Ease-out on a headline number reads as arriving; linear reads as a robotic odometer. Hold each number ~0.5s before the next item begins.
## Connectors & flow
Connectors turn a grid of cards into a *process*. Animate a line/arrow by drawing its path on, synced to land just as the item it points to finishes — motion that says "now look here next."
```jsx
const pathRef = useRef();
const len = pathRef.current?.getTotalLength?.() ?? 0;
const draw = interpolate(frame - delay, [0, 18], [0, 1], { extrapolateRight: "clamp" });
<path ref={pathRef} d="M40,0 C40,30 40,30 40,60" fill="none" stroke="#444" strokeWidth={3}
strokeDasharray={len} strokeDashoffset={len * (1 - draw)} />
```
Draw the connector *between* the two items it joins, in the gap after the source appears and before the target — the line leads the eye into the next reveal.
## Section pacing
Budget time per element, not per second of polish. A viewer can only track one moving thing at a time.
| Beat | Budget |
|---|---|
| Title hold (let it land) | 0.8–1.2s |
| Per item (icon pop → counter → label) | 1.2–2.0s |
| Connector draw | 0.4–0.6s, overlaps into next item |
| Final composed hold (screenshot-able) | 2–3s |
End on the **complete static infographic held still** for 2–3s — the takeaway is the assembled frame, so let it settle with no motion competing.
## Output checklist
- Static layout designed and balanced *before* any keyframes; held final frame looks intentional.
- One central insight, 3–5 data points, 30–90s.
- Reveal order follows hierarchy (title → icon → number → label → connector), not layout order.
- Siblings cascade on a consistent 6–10 frame stagger with one enter curve.
- Icons pop with a spring overshoot; numbers count up eased + `tabular-nums`; only one number animates at a time.
- Connectors draw on to lead the eye into the next reveal.
- Every value is a pure function of `useCurrentFrame()`; no library timers.
- Final composed frame holds ≥2s.
## Deliver & verify (rendered stills → MP4)
> **Packaged helper** (`scripts/`): tile your stills with `scripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png`, then assert the encode with `scripts/probe-mp4.sh out.mp4 [WxH] [fps]`. See `scripts/README.md`.
Remotion is frame-deterministic — every icon pop, counter value, and connector draw is a pure function of `useCurrentFrame()`, so you can render any exact frame headlessly with no seek harness. The infographic carries **key numbers**, so still-inspection catches a wrong stat or a stagger that lands off-canvas before you waste an encode.
**Output contract:**
- A Remotion project with the scene registered (`<Composition>` + zod `schema` + `defaultProps`), all motion frame-driven (no timers / `Date.now()` / `Math.random()`).
- Deliverable = the rendered `out/*.mp4` (plus the project, so the user can re-render with new stats/icons).
- Duration data-dependent (N items × per-item budget)? compute it in `calculateMetadata`, not by hand.
**Verify loop — render stills → inspect → encode.** Render single frames first (cheap, no video encode), then encode only once the layout and numbers are right.
```bash
# 1. Frame-exact stills — with the SHIPPED props. Pick frames that catch each tier:
npx remotion still Infographic out/f-title.png --frame=20 --props='{"items":[...]}' # title + first pop
npx remotion still Infographic out/f-mid.png --frame=90 --props='{"items":[...]}' # mid cascade
npx remotion still Infographic out/f-end.png --frame=149 --props='{"items":[...]}' # final composed hold (= durationInFrames - 1)
# 2. Inspect each PNG — FIDELITY (every key number exact, labels/captions correct, icons the right glyph)
# AND artifacts (text overflow, icon off-canvas, clipped safe-area, missing font, connector pointing
# at the wrong item, half-revealed element where a tier should be settled).
# 3. Only after the stills check out, encode:
npx remotion render Infographic out/infographic.mp4 --props='{"items":[...]}'
```
- Use `npx remotion compositions` to read `durationInFrames`/`fps`; the **final composed hold** is the money frame — verify it looks intentional, not mid-cascade.
- **Data-driven / batch**: verify ONE representative props set (stats + labels) via stills *before* batch-rendering all variants — catch a counter or layout bug once, not N times.
- **README demo GIF for free**: `npx remotion render Infographic out/demo.gif --codec=gif`.
**Before you finish:**
1. `npx remotion still` renders cleanly at title, mid-cascade, and final hold — no errors, no missing fonts/icons.
2. Every key number is **exact** (rounded, `tabular-nums`) and every element is inside the safe area at each frame.
3. Frame-driven only — no `Date.now()` / `Math.random()` / library timers (determinism holds in CI).
4. The **shipped** props render correctly (not just `defaultProps`) — right stats, right icons, right labels.
5. Full MP4 encoded and plays; (optional) GIF rendered for the README.
## Reference files
- `references/sequenced-infographic.md` — a complete runnable Remotion scene: a 3-step "how it works" infographic with staggered icon pops, eased count-up stats, draw-on connectors, a master timing map, and the make-it-data-driven (template × data) prop pattern. Includes a dependency-free inline-SVG variant for non-Remotion use.