Skip to content
Back to skills

Motionize

BSecurity

Upgrade a generic UI icon or loading/empty state into a traced, motion-animated SVG. Generates flat trace-friendly art (via /leonardo tools), validates with Qwen vision, vectorizes to a clean multi-path SVG, and emits a motion reveal component driven by the shared motion-preset library (draw, staggered-draw, fade-pop, float, pulse, hover-response, success-settle). For icon + loading-state visual upgrades - NOT raw image generation (use /leonardo for that).

  • 2 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added October 19, 2026
developmentgobashreactangularnoderailsgitapi

Works with

  • api

Security analysis

B84/100
  • criticalSends environment variables or credentials to an external URL
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 11 files and shows the line behind each finding

Scanned October 19, 2026

npx -y skills add xkazm04/ai-registry --skill motionize --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Motionize?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Motionize
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/xkazm04-motionize/badge)](https://www.skillsdirectory.com/skills/xkazm04-motionize)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: motionize
memory: none
category: other
description: Upgrade a generic UI icon or loading/empty state into a traced, motion-animated SVG. Generates flat trace-friendly art (via /leonardo tools), validates with Qwen vision, vectorizes to a clean multi-path SVG, and emits a motion reveal component driven by the shared motion-preset library (draw, staggered-draw, fade-pop, float, pulse, hover-response, success-settle). For icon + loading-state visual upgrades - NOT raw image generation (use /leonardo for that).
allowed-tools: Read, Write, Edit, Glob, Grep, Bash(node *), Bash(npx *), Bash(cd *)
argument-hint: <UI surface to upgrade, e.g. "teams empty state icon">
version: 1.4.1
---

# Motionize — traced, animated SVG upgrades for icons & loading states

Turn a generic lucide icon + a basic fade into a **traced, self-drawing SVG** whose
every element is under your control (draw order, per-path timing, easing). This
skill is the *upgrade* layer; **`/leonardo` stays the pure image/illustration
generator**. Motionize consumes flat art and produces animated React components.

**The core idea:** neon/glow art traces badly (soft edges → speckle). Generate a
**flat** twin (solid fills, hard edges, no glow/gradients), trace *that*, then add
glow back as an SVG/CSS filter — that separation is what puts traces "under control".

**Read [`ART_STYLE.md`](./ART_STYLE.md) first** — the shared visual language (concept-
art / cinematic feel via a dark surface + tight neon accent set + emissive SVG-filter
glow). Every glyph ships **dark AND light**: geometry is identical, only fills change,
and the shared renderer emits `[data-theme^="light"]` per-color overrides automatically
(the tracer's negative-space `var(--background)` flips with the theme for free).

## Pipeline (four steps)

### 0. One-time setup
Install the skill's self-contained deps (kept out of the app's package.json):
```bash
cd .claude/skills/motionize && npm install && cd -
```

### 1. Generate flat, trace-friendly art (via /leonardo tools)
Use the existing generators, but with a FLAT profile — no glow, no gradients:
```bash
export $(grep -E '^(OPENAI_API_KEY|LEONARDO_API_KEY|QWEN_API_KEY)=' .env | xargs)
# transparent bg is ideal for icons (gpt-image-2 supports it natively):
node .claude/skills/leonardo/tools/openai-image.mjs generate \
  --prompt "Flat vector icon of <SUBJECT>: solid fills, crisp hard edges, thick uniform outlines, no gradients, no glow, no shadow, limited palette (<=4 colors), centered" \
  --output .claude/skills/motionize/out/<name>-flat.png --size 1024x1024 --background transparent --quality high
# (or leonardo-gpt-image.mjs / leonardo-image.mjs when only a Leonardo key is present)
```

### 2. Validate with Qwen (free, Gemini-free)
```bash
node .claude/skills/motionize/tools/qwen-recognize.mjs \
  --input .claude/skills/motionize/out/<name>-flat.png \
  --prompt "Is this a FLAT icon (solid fills, hard edges, no gradients/glow)? How many distinct colors? Name the shapes. Reply JSON {flat:bool, colors:int, shapes:[...]}."
```
DashScope OpenAI-compatible endpoint, `qwen3.7-plus` + quota-fallbacks, `$QWEN_API_KEY`
(free 1M tokens / 90 days per model). If it isn't flat/clean, re-prompt step 1.

### 3. Trace → clean SVG (+ animatable data, one pass)
```bash
node .claude/skills/motionize/tools/trace.mjs \
  --input .claude/skills/motionize/out/<name>-flat.png \
  --output .claude/skills/motionize/out/<name>.svg \
  --mode spline --color-precision 4 --filter-speckle 6 \
  --emit src/features/shared/glyph/glyphs/<name>Glyph.ts --name <NAME>_GLYPH
```
`@neplex/vectorizer` (VTracer) → one `<path>` per color region + SVGO cleanup.
`--emit`/`--name` bakes the paths into a `TracedGlyph` module (`{ viewBox, data:
{d, fill, delay}[] }`) in the same pass — radial `delay` = distance to centre for a
center-out reveal; `--order angular` for a clockwise sweep. Committed glyph modules
live in **`src/features/shared/glyph/glyphs/`** (~10–16KB gzipped each — consumers
import the one they render, never a shared registry). Handles the trace gotchas
(drops full-canvas bg, recolors large negative-space to `var(--background)`,
preserves paint order). For an icon SET, use `trace-set.mjs --split` (one module per
glyph); `emit-glyph.mjs` is the shared core.

### 4. Motionize → React component (shared renderer + preset library)
**Do not emit a bespoke animated component.** Render the emitted glyph through the
shared catalog primitive
**`src/features/shared/components/display/MotionizedGlyph.tsx`** and pick motion
from the **preset library** (next section):
```tsx
<MotionizedGlyph data={GOALS_GLYPH.data} viewBox={GOALS_GLYPH.viewBox} spread={1} glow />
```
- **CSS keyframes, NOT framer-motion** — learned the hard way: the app wraps
  everything in `<MotionConfig reducedMotion={visible ? 'user' : 'always'}>`, and
  under `always` framer snaps EVERY animation (opacity included), which silently
  killed reveals. CSS animations aren't governed by MotionConfig; an
  IntersectionObserver replays the entrance on viewport re-entry. Use framer only
  for surfaces outside that trap, and still source timings from the presets.
- Light theme, reduced motion, and the emissive `glow` filter (feGaussianBlur+feMerge
  on accent paths) are built into the renderer. A faint `<radialGradient>` behind =
  "fog". For frame-exact rendered assets, drive the same paths with Remotion
  `interpolate`.

## Motion system — the preset library

All motion comes from **one shared module**:
`src/features/shared/components/display/motionPresets.ts` (next to
`MotionizedGlyph.tsx`). It **exists** — `MotionizedGlyph` reads from it via
`entrance` / `ambient` / `hover` props (default `entrance="staggered-draw"`).
**Extend it — never inline variants/keyframes in an emitted component.** Adding or
tuning a preset = editing that one file; every motionized surface picks it up.

Two composition details the renderer already handles, worth knowing before you add a
preset: ambient loops are emitted with `fill-mode: forwards`, **not** `both` (under
`both` the loop's backwards fill applies its dimmed from-state during the start delay
and fights the entrance), and the loop's start delay is the second value in the
path's inline `animation-delay` (the entrance stagger is the first).

Each preset is data the renderer turns into scoped `@keyframes` + `animation`:

```ts
export type MotionPresetName =
  | 'draw' | 'staggered-draw' | 'fade-pop'            // entrances (one-shot)
  | 'float' | 'pulse'                                 // ambient loops
  | 'hover-response'                                  // interaction layer
  | 'success-settle';                                 // one-shot completion

export interface MotionPreset {
  kind: 'entrance' | 'loop' | 'hover' | 'oneshot';
  /** @keyframes body (from/to or % steps), scoped per instance by the renderer. */
  keyframes: string;
  durationS: number;                    // loops: the period
  ease: string;                         // e.g. 'cubic-bezier(0.16, 1, 0.3, 1)'
  /** Entrances: map a path's 0..1 delay into seconds (default: d => 0.08 + d * spread). */
  stagger?: (delay: number, spread: number) => number;
  iteration?: 'infinite' | 1;           // loops: infinite + alternate
  /** prefers-reduced-motion fallback: cross-fade only, or don't run at all. */
  reduced: 'opacity-only' | 'none';
  /** Loops/hover: apply only to accent paths (max channel > 0x80), not line-work. */
  accentOnly?: boolean;
}
```

Vocabulary (defaults; tune in the module, not per component):

| Preset | Kind | Default | Reduced |
|---|---|---|---|
| `draw` | entrance | `pathLength`-style stroke trace, 0.9s, ease-out — **stroke/`--mono` traces ONLY** (on fills it traces the boundary, messy) | opacity-only |
| `staggered-draw` | entrance | per-path opacity 0→1 + scale 0.35→1, 0.5s each, `cubic-bezier(0.16,1,0.3,1)`, staggered by emitted `delay` × `spread` | opacity-only |
| `fade-pop` | entrance | whole-glyph opacity 0→1 + scale 0.92→1, 0.35s — for small icons where a stagger is noise | opacity-only |
| `float` | loop | translateY ±2px + opacity ±0.06, 5s, alternate, infinite — ambient idle | none |
| `pulse` | loop | accent opacity 0.75→1 (+ glow stdDeviation swell), 3.5s, alternate — attention/activity | none |
| `hover-response` | hover | scale 1→1.03 + glow/accent intensify, 0.18s — a `transition` on the group, not an animation | opacity accent only |
| `success-settle` | oneshot | scale 1→1.12→1 overshoot, 0.42s, `cubic-bezier(0.34,1.56,0.64,1)` (matches `animate-inbox-zero-pop`) — fires once on completion, never loops | opacity-only |

### Composition rules

- **Sequence, don't overlap:** entrance finishes before any ambient loop starts.
  In CSS, comma-chain the two animations and give the loop
  `animation-delay: <entrance total + 0.2s>`; in framer contexts use
  `onAnimationComplete`. The IntersectionObserver replay restarts the **entrance
  only** — loops keep their own clock.
- **One ambient loop per glyph.** `float` OR `pulse`, never both on the same paths.
  Ambient loops are `accentOnly` — line-work stays still.
- **`hover-response` layers on anything** (it's a transition on the wrapper `<g>`,
  orthogonal to entrance/loop).
- Per surface:
  - **Empty states** → `staggered-draw` entrance (+ optional `float` on accents).
    First-run "nothing here yet" only — a self-drawing 128px illustration is wrong
    for a filtered-to-zero list (see `ScenarioEmptyState`'s `glyph` prop docs).
  - **Loading states** → `pulse` (motion may imply activity ONLY where work is
    actually happening; never on a static/idle state).
  - **Icons / interactive chrome** → static render + `hover-response`.
  - **Completion moments** → `success-settle`, one-shot, gated on the actual event
    (the `InboxZero celebrate` pattern).

### Taste guardrails

- **Entrance total ≤ ~1.2s** (last stagger delay + duration). Quiet and deliberate.
- **Ambient loops are barely-there:** translate ≤ 2–3px, opacity delta ≤ 0.08,
  periods 3–6s. If a screenshot 3s apart looks obviously different, it's too much.
- **Never loop transforms that imply progress on a static state** — a spinning or
  sweeping motion on an empty state reads as "loading" and is a lie.
- **Every preset degrades under `prefers-reduced-motion`** per its `reduced` field —
  opacity-only cross-fade for entrances/one-shots, loops off entirely. No preset may
  hard-snap.
- **Colors/glow come from the app's tokens** — `var(--background)` negative space,
  ART_STYLE accent hexes, light-theme overrides via the renderer's
  `[data-theme^="light"]` rules. Never hardcode theme-specific colors in a preset.

## Gotchas (learned)

- **VTracer traces FILLED regions, and the background is one of them.** Drop the
  full-canvas bg path, but **RECOLOR interior white paths to `var(--background)`**
  — don't drop them, or connective lines/holes fill solid. Verify by rendering the
  composed SVG on the target surface (`sharp(Buffer.from(svg)).png()`) *before*
  wiring the component.
- **Noise → path explosion.** Anti-aliased edges yield hundreds of micro-paths.
  Push `--filter-speckle` (10–40) and lower `--color-precision` (3–4) until the
  path count matches the number of *real* regions.
- **More content = more control.** A richer flat scene traces into many addressable
  paths — group by role and orchestrate (hub → links → figures → accents).

## Conventions

- Scratch art + SVGs live in `.claude/skills/motionize/out/` (git-ignored). The
  FINAL committed artifacts: the glyph data module in
  `src/features/shared/glyph/glyphs/` + the consuming surface's `MotionizedGlyph`
  usage (+ `motionPresets.ts` if a preset was added/changed). No runtime tracing.
- Reduced motion: CSS media query in the renderer; for React logic use
  `useReducedMotion` from `@/hooks/utility/interaction/useMotion`.
- Env: `QWEN_API_KEY` (recognition) + `OPENAI_API_KEY`/`LEONARDO_API_KEY`
  (generation). Load from `.env` before running.

## Reference implementations

Shipped empty-state glyphs: `goalsGlyph` / `kpisGlyph` / `feedsGlyph` /
`coachingGlyph` / `relayGlyph` / `ratelimitGlyph` in
`src/features/shared/glyph/glyphs/`, rendered via `ScenarioEmptyState`'s `glyph`
prop and directly in e.g. `src/features/teams/sub_goals/GoalsEmptyGlyph.tsx` and
`src/features/teams/sub_teamWorkspace/TeamList.tsx`.

The **live-roadmap** pair is the reference for the ambient/entrance split and for
cropping a traced canvas — `roadmapRouteGlyph` → `RoadmapRouteRail.tsx` (a narrow
vertical rail; renders a *windowed* viewBox because the traced 1024² canvas has wide
empty margins, and runs `pulse` because the hero really is the in-progress item) and
`roadmapWaypointGlyph` → `RoadmapLaneEmptyGlyph.tsx` (`fade-pop`, no ambient — an
empty lane is idle). Both in `src/features/home/sub_releases/`.

Two things learned tracing that pair, already fixed in the tools:
- `trace.mjs --emit` (via `emit-glyph.mjs`) now writes the same `TracedGlyph` shape
  `trace-set.mjs` does. It used to emit a bare array plus a separate `_VIEWBOX`
  const, so a single-glyph trace produced a module that couldn't be fed straight into
  `<MotionizedGlyph data viewBox>` like every committed set-traced glyph.
- **A hollow ring traces as an opaque white blob** unless you lower `--white-keep`
  (default 0.1 keeps small white interiors as literal white). `--white-keep 0.002`
  demoted the ring's interior to `var(--background)`. Conversely `--filter-speckle 30`
  on hairline art collapsed the whole glyph to **one** path — 12–14 was the usable
  band. Always render-verify on the target surface before wiring.
- Don't `tight-crop` a tall composition into a square box — it squashes the aspect and
  bakes ellipses into the geometry. Trace the undistorted original and window the
  viewBox at the call site instead.

---

<!-- clause: skill-reflection v6 - stamped by scripts/apply-skill-clauses.mjs from docs/skill-clauses/skill-reflection.md; edit the template, then re-stamp -->
## Skill Reflection

After the work, record only useful observations supported by this run. No lesson is
a valid result. Reflection inherits the task's authorization; it grants no additional
permission to edit another repository, send data, commit, or publish.

**Run log.** Unlike a lesson, this is written on every run that started work - failed and
aborted runs included; skip read-only info modes and runs cancelled before any work. Append
ONE line to `.ai/skill-runs.local.jsonl` at the root of the task's MAIN checkout: local,
gitignored run output inside the task's own repository, never a write into the registry.
A run that worked in a linked worktree logs in the main checkout that worktree belongs to
(`git rev-parse --git-common-dir`, minus `/.git`) - a disposable worktree takes the line
with it when it is removed. `log-run.mjs` resolves this on its own.
The registry pulls it later (`/librarian skills` on the same machine). When a registry
checkout is reachable (`registry.local` in `.ai/manifest.yaml`), prefer its writer, which
stamps project, device and version for you:

```sh
node <registry>/scripts/log-run.mjs --skill motionize --outcome <o> --difficulty <1-5> \
  --provider <claude|openai|xai|qwen|google|other> --model <your model id> [--effort <level>] \
  [--tokens-est <n>] --result "<one sentence>" --comment "<self-reflection>"
```

Otherwise write the line yourself: `{"ts":"<ISO, UTC Z>","skill":"motionize","outcome":…,
"difficulty":…,"provider":…,"model":…,"effort":…|null,"tokensEst":…|null,"result":…,"comment":…}`.

- `outcome`: `shipped` (the goal landed) / `partial` / `no-op` (ran correctly, nothing to
  do) / `parked` (designed or staged, deliberately not landed) / `failed` / `aborted`.
- `difficulty` rates the task as this run met it: 1 trivial - mechanical; 2 routine - the
  method as written; 3 demanding - real judgment calls or one detour; 4 hard - dead ends,
  rework or an operator course-correction; 5 at the edge - partial or failed on the merits.
- `model`/`effort` as your harness states them (`null` effort when you cannot see it).
  `tokensEst` is the drop in the harness's remaining-token counter since this skill was
  invoked, or `null` (with `log-run.mjs`, omit `--tokens-est`); exact figures are measured
  later from transcripts - never guess one.
- `result` is one line (max 240 chars). `comment` (max 2000) is the self-reflection a
  reviewer reads: what worked, what the method made harder, where its instructions were
  wrong, missing or ignored. No filesystem paths or email addresses.
- Never read run logs during a run. They are evidence ABOUT this skill for its reviewer;
  an executor that reads its own diagnosis contaminates the next measurement.

**Project learning.** Only when this run produced an observation that would change how a
future run behaves. A run that went as the method describes writes nothing: an entry that
restates the procedure, records "no issues", or repeats the task is a defect, not a
deliverable. When there is such an observation and local edits are within scope, put one
dated line in the overlay this skill's `## Project overlay` section names, under
`## Skill improvement log`. **Write only into an overlay that already exists.** If the
project has none, put the observation in the response instead - creating a new tracked
file for a reflection is scope the task did not ask for, and a reader who never asked for
the skill has to review it. If the overlay is a structured config (YAML, TOML, JSON),
record the note as comments so the file keeps parsing, or use the response.
Use a supplied memory contract only when its destination and writes are authorized.
Keep project details out of the shared method.

**Method learning.** Identify the installation before editing anything. A local
`.ai/registry-installation.local.json` receipt can identify development versus release,
the registry revision, and selected skill versions. Verify any link's actual target;
do not assume a skill directory is a writable registry link.

- For a pinned release, marketplace cache, ordinary copy, or unknown installation,
  keep a proposal in the project overlay or response. Do not edit the installed method
  or silently relink it. Adoption and rollback are explicit installation operations.
- For a development link, edit the registry only when that checkout is already within
  the accepted task scope. Otherwise report a proposal. Authorized changes belong in
  the source checkout, followed by its gates; commit only when the task authorizes it.
- Record an actual lesson in `LESSONS.md` against the version **used**:
  `## <version-used> - <YYYY-MM-DD> - <project-name>` and concise bullets. A proposal
  must be labeled as such; structural checks are not evidence of field effectiveness.
- Applied skill changes require a version bump: patch for wording, minor for a step
  refinement, major for method redesign. A lesson alone needs no bump. Shared stamped
  clauses are edited in the registry's `docs/skill-clauses/` and regenerated with
  `scripts/apply-skill-clauses.mjs`, never patched in individual installed skills.

**Domain learning.** Follow `## Knowledge sync` when present, within the same scope
and privacy boundaries. A method lesson and a domain knowledge lead are different
artifacts; do not fabricate either to fill a reflection quota.
<!-- /clause: skill-reflection -->

Files in this skill

  • .gitignore19 B
  • ART_STYLE.md6 KB
  • LESSONS.md3.5 KB
  • SKILL.md19.1 KB
  • package-lock.json20.3 KB
  • package.json262 B
  • tests/test_emit-glyph.mjs9.2 KB
  • tools/emit-glyph.mjs8.7 KB
  • tools/qwen-recognize.mjs3.4 KB
  • tools/trace-set.mjs6.9 KB
  • tools/trace.mjs3.7 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.