스킬 불러오는 중
스킬 불러오는 중
iart-ai/web-animation-skills · GitHub
This skill should be used when the user asks to "animate an SVG", "make a line draw itself on", "do a stroke draw-on / signature animation", "morph one shape into another", "move an element along a path", "animate an icon/logo", or "animate an SVG gradient or filter". Covers stroke-dashoffset draw-on, path morphing, motion-along-path, and animated icons/gradients/filters via CSS, SMIL, and GSAP.
프로젝트 폴더에서 아래 명령어를 실행하고, 설치할 에이전트를 선택하세요.
npx skills add iart-ai/web-animation-skills --skill svg-animation설치 명령을 직접 실행해야 적용됩니다. 지원 에이전트와 필요한 권한·라이선스는 제작자의 안내를 확인하세요.
README.mdreferences/svg-techniques.md# SVG Animation Techniques (detailed)
## 1. Stroke draw-on: the math
`stroke-dasharray` defines a dash pattern; `stroke-dashoffset` shifts where the pattern starts. Set the dash equal to the whole path length and offset it by that same length → the visible dash is pushed entirely off the path (invisible). Animating offset to 0 slides the dash into view, revealing the stroke progressively.
```
visible fraction = 1 - (dashoffset / pathLength)
```
So offset = length → 0% drawn; offset = 0 → 100% drawn; offset = length/2 → 50% drawn (useful for scroll mapping: `offset = length * (1 - progress)`).
### getTotalLength gotchas
```js
const path = document.querySelector("#sig");
const len = path.getTotalLength(); // returns user units of the path geometry
path.style.strokeDasharray = len;
path.style.strokeDashoffset = len;
requestAnimationFrame(() => { // next frame so the transition has a start value
path.style.transition = "stroke-dashoffset 1.6s ease";
path.style.strokeDashoffset = "0";
});
```
- `getTotalLength()` measures the path's own coordinate system, ignoring CSS transforms/scaling — which is what you want for dasharray.
- It only works on geometry elements (`path, line, polyline, polygon, circle, ellipse, rect`). For `<text>` draw-on you must convert text to paths first.
- Rounding: browsers can clip the last sub-pixel; pad slightly (`len + 1`) if the end never fully closes.
- Compound paths (multiple `M` subpaths in one `d`) draw all subpaths simultaneously. Split into separate `<path>`s with staggered `animation-delay` for sequential drawing.
### No-JS normalization with pathLength
`pathLength="1"` tells the browser to treat the path's length as exactly 1, regardless of real geometry. Then dash math is unit-free:
```svg
<path d="..." pathLength="1" fill="none" stroke="#111"
style="stroke-dasharray:1; stroke-dashoffset:1; animation:draw 1.4s ease forwards"/>
```
```css
@keyframes draw { to { stroke-dashoffset: 0; } }
```
This is the cleanest pure-CSS draw-on and avoids hardcoding measured lengths that break when the path is edited. Use `pathLength="100"` if you prefer to think in percentages.
### Scroll-driven draw-on (no library, via offset)
```js
const len = path.getTotalLength();
path.style.strokeDasharray = len;
function onScroll() {
const p = Math.min(1, Math.max(0, scrollProgress())); // 0..1
path.style.strokeDashoffset = len * (1 - p);
}
document.addEventListener("scroll", onScroll, { passive: true });
```
## 2. Morphing
### Why naive morphs break
Path interpolation walks the `d` command list of A and B in lockstep. If A has 4 cubic curves and B has 7, or A uses `L` where B uses `C`, the browser/animator either refuses or produces garbage. Fixes: match commands by hand, or use a library that resamples.
### GSAP MorphSVG (best for arbitrary shapes)
```js
import gsap from "gsap";
import { MorphSVGPlugin } from "gsap/MorphSVGPlugin";
gsap.registerPlugin(MorphSVGPlugin);
// any shape -> path so it can morph
MorphSVGPlugin.convertToPath("#circ, #star");
gsap.to("#circ", {
duration: 0.9, ease: "power2.inOut",
morphSVG: { shape: "#star", shapeIndex: "auto", type: "rotational" },
});
```
- `shapeIndex` rotates the starting point mapping to minimize twisting; `"auto"` lets GSAP pick, or pass a number to tune.
- `type: "rotational"` interpolates angles/lengths instead of raw coordinates — usually smoother for organic morphs.
- `map: "size" | "position" | "complexity"` changes how sub-paths in compound shapes are matched.
### Flubber (standalone, framework-friendly)
```js
import { interpolate, separate, combine } from "flubber";
// 1 -> 1 shape
const lerp = interpolate(pathA, pathB, { maxSegmentLength: 2 });
// splitting one shape into many (or vice versa)
const split = separate(oneShape, [shape1, shape2, shape3], { single: true });
```
Drive `lerp(t)` with any tween/`requestAnimationFrame`, or inside Framer Motion:
```jsx
import { motion, useMotionValue, useTransform, animate } from "motion/react";
const progress = useMotionValue(0);
const d = useTransform(progress, [0, 1], [0, 1], { clamp: false });
const path = useTransform(d, (v) => interpolate(A, B)(v));
// animate(progress, 1, { duration: 0.6 }) then <motion.path d={path} />
```
Use Flubber when you can't add GSAP or need React-native value plumbing; use MorphSVG for the highest-quality mapping and convenience.
### Hand-authored icon toggle (hamburger ↔ close) — no morph library
When you control both shapes, give them identical structure and animate transforms instead of `d`:
```svg
<svg viewBox="0 0 24 24" class="menu" aria-label="Menu">
<line class="top" x1="3" y1="6" x2="21" y2="6"/>
<line class="mid" x1="3" y1="12" x2="21" y2="12"/>
<line class="bot" x1="3" y1="18" x2="21" y2="18"/>
</svg>
```
```css
.menu line { stroke:#111; stroke-width:2; transition: transform .25s ease, opacity .2s ease;
transform-origin: center; transform-box: fill-box; }
.menu.open .top { transform: translateY(6px) rotate(45deg); }
.menu.open .mid { opacity: 0; }
.menu.open .bot { transform: translateY(-6px) rotate(-45deg); }
```
`transform-box: fill-box` makes `transform-origin: center` resolve to the element's own bounding box — essential for SVG rotation; without it the origin is the SVG viewport origin.
## 3. Motion along a path — details
### GSAP MotionPath
```js
gsap.to("#dot", {
duration: 5, repeat: -1, ease: "none",
motionPath: {
path: "#route",
align: "#route", // coordinates relative to the path element
alignOrigin: [0.5, 0.5], // pin the object's center to the path
autoRotate: true, // or a number to add a constant offset angle
start: 0, end: 1, // animate a sub-segment, e.g. 0.2..0.8
},
});
```
Convert a series of points into a smooth path: `MotionPathPlugin.convertToPath(...)`, or pass `motionPath: [{x:0,y:0},{x:100,y:50},...]` and GSAP draws a curve through them.
### CSS offset-path (declarative, modern)
```css
.dot {
offset-path: path("M10,80 C40,10 120,10 150,80");
offset-rotate: auto; /* face direction of travel */
animation: travel 3s linear infinite;
}
@keyframes travel { to { offset-distance: 100%; } }
```
Newer syntax also accepts `offset-path: url(#route)` referencing a `<path>` id and shapes like `ray()`. Good support in evergreen browsers; no JS needed.
### SMIL animateMotion
Self-contained inside the SVG; use `rotate="auto"` and `<mpath href="#id"/>`. Best for portable icon assets where you don't want external CSS/JS. Avoid when you need scroll-sync or broad legacy support.
## 4. SMIL vs CSS vs JS — tradeoffs
| Axis | SMIL | CSS | JS (GSAP/WAAPI) |
|------|------|-----|-----------------|
| Bundle cost | none (inline) | none | library (GSAP ~JS) |
| Works in `<img>` SVG | yes | yes (internal) | no |
| Reach (legacy IE/Edge) | no | partial | yes |
| Scroll/scrub sync | hard | hard | easy |
| Morph mismatched paths | no | no | yes (MorphSVG/Flubber) |
| Timeline orchestration | limited | limited | full |
| Animate `d`/gradients/filters | yes | partial | yes |
Default: CSS for simple declarative effects, GSAP for anything coordinated/scrubbable/morphing, SMIL only for portable self-animating assets in evergreen targets.
## 5. SVGO config tuned for animation
Aggressive SVGO defaults will strip the very things you animate. Use a config like:
```js
// svgo.config.js
module.exports = {
multipass: true,
plugins: [
{ name: "preset-default", params: { overrides: {
removeViewBox: false, // keep responsive scaling
cleanupIds: false, // DO NOT rename ids referenced by CSS/JS/SMIL/gradients
mergePaths: false, // keep sub-paths separate for staggered draw-on
convertShapeToPath: false, // keep <circle>/<rect> if SMIL/CSS targets them
removeHiddenElems: false, // keep elements you toggle visible via animation
inlineStyles: false, // preserve class hooks
} } },
{ name: "removeDimensions" }, // drop width/height, keep viewBox -> fluid scaling
{ name: "prefixIds" }, // namespace ids if inlining many SVGs to avoid id collisions
],
};
```
If multiple inlined SVGs share gradient/filter ids, id collisions make the LAST definition win for everyone — `prefixIds` (or unique authoring) prevents this classic bug.
## 6. Reduced motion
```css
@media (prefers-reduced-motion: reduce) {
.path { animation: none; stroke-dashoffset: 0; } /* show final drawn state instantly */
}
```
For looping decorative SVG (spinners, ambient gradients), pause or hide them under reduced-motion; for functional icon toggles, keep the state change but drop the easing duration.
---
Pick the right method per shape and SVG draws, morphs, and travels smoothly. Built by **[iart.ai](https://iart.ai/?utm_source=github&utm_medium=readme&utm_campaign=web-animation-skills&utm_content=skill_footer&utm_term=svg-animation)** — the AI motion agent for editable, on-brand motion graphics.
SKILL.md---
name: svg-animation
description: This skill should be used when the user asks to "animate an SVG", "make a line draw itself on", "do a stroke draw-on / signature animation", "morph one shape into another", "move an element along a path", "animate an icon/logo", or "animate an SVG gradient or filter". Covers stroke-dashoffset draw-on, path morphing, motion-along-path, and animated icons/gradients/filters via CSS, SMIL, and GSAP.
version: 0.1.0
---
# SVG Animation
Crisp, lightweight, infinitely scalable vector motion — ideal for icons, illustrations, logos, and data marks. SVG can be animated three ways: CSS (declarative, simple), SMIL (`<animate>` inside the SVG), and JS (GSAP/Web Animations, for control and morphing). Choose per task; the techniques below say which.
## When to use
- Stroke "draw-on" of icons, illustrations, signatures, maps
- Shape/path morphing and animated icon state changes (menu ↔ close, play ↔ pause)
- Moving an element along a path (motion path)
- Animated gradients, filters (glow, displacement), and animated logos
## Core techniques
### Stroke draw-on (the staple)
Draw the dash array as long as the path, offset it fully (invisible), then animate the offset to 0.
```css
.path {
stroke-dasharray: var(--len);
stroke-dashoffset: var(--len);
animation: draw 1.4s ease forwards;
}
@keyframes draw { to { stroke-dashoffset: 0; } }
```
Getting the length:
- JS (most reliable): `const len = path.getTotalLength(); path.style.setProperty("--len", len);`
- No-JS trick: set `pathLength="1"` on the `<path>`, then `stroke-dasharray: 1; stroke-dashoffset: 1;` and animate to `0`. This normalizes any path to a 0–1 length so no measurement is needed.
Reverse (erase) by animating offset from 0 back to `len`. Stagger multiple paths with `animation-delay`. Direction of drawing follows the path's point order; reverse it in the editor or negate the offset sign if it draws "backwards".
### Morphing one path into another
Paths interpolate point-by-point, so a naive morph requires both `d` attributes to have the **same number and type of commands**. Two robust approaches:
- **GSAP MorphSVG** (free as of GSAP 3.12) — handles mismatched point counts automatically and finds a good mapping:
```js
gsap.registerPlugin(MorphSVGPlugin);
gsap.to("#start", { morphSVG: "#end", duration: 0.8, ease: "power2.inOut" });
// Convert any shape to a morph-able path:
MorphSVGPlugin.convertToPath("circle, rect, ellipse, line, polygon");
```
- **Flubber** (small standalone lib) — generates interpolators without GSAP, good with React/Framer Motion:
```js
import { interpolate } from "flubber";
const interpolator = interpolate(pathA, pathB, { maxSegmentLength: 2 });
// interpolator(0) === pathA, interpolator(1) === pathB; feed t into a tween.
```
For hand-authored morphs (icon toggles), keep both paths with identical command structure and animate `d` directly via Web Animations or CSS (`d` is animatable in modern browsers via `path("...")`).
### Motion along a path
- **GSAP MotionPath** (preferred — control, alignment, scrub):
```js
gsap.registerPlugin(MotionPathPlugin);
gsap.to("#rocket", {
duration: 4, repeat: -1, ease: "none",
motionPath: { path: "#track", align: "#track", autoRotate: true, alignOrigin: [0.5, 0.5] },
});
```
`autoRotate: true` orients the object to the path tangent; `align` makes coordinates relative to the path element.
- **SMIL** (no JS):
```svg
<path id="track" d="M10,80 C40,10 120,10 150,80" fill="none"/>
<circle r="6" fill="#3b82f6">
<animateMotion dur="3s" repeatCount="indefinite" rotate="auto">
<mpath href="#track"/>
</animateMotion>
</circle>
```
- **CSS** offset-path (modern, declarative): `offset-path: path("M10,80 C..."); animation: move 3s linear infinite;` with `@keyframes move { to { offset-distance: 100%; } }` and `offset-rotate: auto`.
### Animated gradients and filters
Gradients: animate `gradientTransform` or stop offsets. A sheen sweep:
```svg
<linearGradient id="sheen">
<stop offset="0%" stop-color="#fff" stop-opacity="0"/>
<stop offset="50%" stop-color="#fff" stop-opacity=".8"/>
<stop offset="100%" stop-color="#fff" stop-opacity="0"/>
<animateTransform attributeName="gradientTransform" type="translate"
from="-1 0" to="1 0" dur="2s" repeatCount="indefinite"/>
</linearGradient>
```
Filters: animate `feDisplacementMap` `scale` for gooey/wobble, `feGaussianBlur` `stdDeviation` for focus pulls, or `feColorMatrix`/`feFlood` for glow. Filters are paint-heavy — animate sparingly and prefer `transform`/`opacity` where possible.
## Implementation choice (pick fast)
| Need | Use |
|------|-----|
| Single declarative draw/fade | CSS |
| Self-contained, no JS bundle | SMIL (`<animate*>` in the SVG) |
| Coordinated, scrubbable, scroll-tied | GSAP |
| Mismatched-point morph | GSAP MorphSVG or Flubber |
| Path following with rotation | GSAP MotionPath / CSS offset-path |
SMIL caveat: not supported in IE/old Edge and historically deprecation-flagged; for max reach or scroll-syncing, prefer CSS or JS. SMIL is still fine for self-contained icon assets in evergreen browsers.
## Authoring and optimization
- Build/clean with **SVGO**: keep `viewBox`, drop editor metadata, but disable `cleanupIds`/`removeViewBox` and any plugin that renames IDs you reference from CSS/JS/SMIL. Disable `mergePaths` and `convertShapeToPath` if you animate individual sub-paths or shapes.
- Inline the SVG in the DOM (not `<img src>`) so CSS/JS can reach its internals; `<img>`-embedded SVG can only self-animate via internal SMIL/CSS.
- Set explicit `viewBox` and avoid fixed `width`/`height` so the asset scales fluidly.
- For draw-on, ensure paths are actual strokes (`fill:none; stroke:...`), not filled outlines — dashoffset only affects strokes.
- Respect `prefers-reduced-motion`: gate looping/large motion; keep a static final state.
## 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 icon/logo/draw-on the deliverable is **one HTML file that opens directly in a browser** — inline the SVG in the markup, drive the animation with one mechanism, no build step. One file is the right tier for a vector asset; don't reach for a bundler.
**Output contract:**
- One `.html` file: inline `<svg>`, plus CSS `@keyframes` / a `<script>` with GSAP from CDN / SMIL `<animate*>` — pick one driver.
- Include the seek harness matching that driver so any moment can be frozen for a screenshot.
**Seek harness — freeze an exact moment.** `?t=N` seeks and pauses so a screenshot lands on a still frame. Use the mechanism that matches how the SVG animates:
```html
<script>
const t = new URLSearchParams(location.search).get("t");
if (t !== null) {
const N = parseFloat(t);
// SMIL: pause the SVG's own clock and scrub it
const svg = document.querySelector("svg");
svg.pauseAnimations(); svg.setCurrentTime(N);
// CSS @keyframes draw-on: el.style.animationDelay=(-N)+"s"; el.style.animationPlayState="paused";
// GSAP timeline: tl.pause(); tl.seek(N);
}
window.__ready = true;
</script>
```
**Verify loop — render → freeze → screenshot → check:** open the file at start / mid / end (`?t=0`, `?t=<dur/2>`, `?t=<dur>`), screenshot each, and check **fidelity** (stroke draws in the right direction, morph endpoints clean) plus **artifacts** (path clipped by `viewBox`, stroke vanishing from a stale dashoffset, FOUC, jank at the morph seam). Any headless tool works:
```bash
npx playwright screenshot --wait-for-timeout=500 "file://$PWD/icon.html?t=0.7" frame-mid.png
```
**Before you finish:**
1. Opens standalone — no console errors, inline SVG reachable, CDN (if any) loads.
2. The seek mechanism for your driver freezes a deterministic frame.
3. Screenshotted at start / mid / end — matches the brief, no clipping or off-`viewBox` strokes.
4. `prefers-reduced-motion` honored — looping/large motion gated, static final state kept.
5. Easing is intentional — `ease`/GSAP ease chosen on purpose, no accidental `linear` draw-on.
## Reference files
- `references/svg-techniques.md` — full dashoffset math and `getTotalLength` gotchas, the `pathLength="1"` normalization, GSAP MorphSVG vs Flubber decision guide with code, an icon-toggle morph (hamburger↔close), MotionPath/offset-path details, SMIL-vs-CSS-vs-JS tradeoffs, and an SVGO config tuned for animation.