Claude Skill

fig

Make a fig. A single looping animated SVG, one self-contained HTML file you can drop in an email or a slide. Use it when an idea moves (flows, loops, retries, queues, fan-outs). Faster than a paragraph, livelier than a static diagram. No player, no deck.

LLM Mart · 0 points · 18 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download smk-labs-claude-plugins-fig_skills_fig-35de111.zip · 7 KB
Part of smk-labs/claude-plugins — 19 skills

Install

skills CLI npx skills add https://github.com/smk-labs/claude-plugins/tree/main/fig/skills/fig
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install smk-labs-claude-plugins@llmmart
Git git clone https://github.com/smk-labs/claude-plugins.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole smk-labs/claude-plugins collection as a plugin from our marketplace. Git is the plain clone.

README

fig

A Claude skill for making a fig: a single looping animated SVG, one self-contained HTML file you can drop in an email or a slide.

Use it when an idea moves (flows, loops, retries, queues, fan-outs). Faster than a paragraph, livelier than a static diagram. No player, no deck.

Stack (1.1.0)

One inline <svg>, motion in CSS @keyframes or SMIL. No JavaScript, no CDN, no build step.

Before 1.1.0 the template drove motion from React plus Babel over a CDN, which contradicted the one thing a fig is for. A fig is made to be embedded: as an <img> in a report, an attachment in an email, a paste in a slide. In every one of those the svg is lifted out and rendered as its own document, where no script runs and nothing is fetched. Those figures froze on their first frame or showed nothing at all. Declarative motion survives the trip, and drops three CDN dependencies on the way out.

Consequences worth knowing: everything the figure needs lives inside the <svg>, the xmlns is mandatory, the markup is parsed as strict XML (no bare < or &, including inside a CSS comment), and RTL must be declared on the svg itself (style="direction:rtl") because dir is an HTML attribute that means nothing there.

Install

/plugin marketplace add smk-labs/claude-plugins
/plugin install fig@smk

What's in here

SKILL.md             The skill itself, loaded into Claude's context when triggered.
scripts/html2gif.sh  Optional helper. Converts a fig to a looping GIF
                     using Playwright + ffmpeg. Used only when asked.
LICENSE              MIT.

License

MIT.

Skill manifest

Some ideas are explained better by a 5-second looping animation than by 100 lines of text.

Sketch the figure in ASCII first: static layout, labelled motion, loop length. Get a yes before writing the svg.

After building, look at the rendered file as a stranger would and refine once before handing back. ASCII covers structure; visual issues (collisions, weak contrast, orphan elements, dated chrome, loose components that should be grouped) only appear on screen.

Guidelines

  • Subject fit. Invent a visual metaphor for this specific idea. The same concept can be drawn many ways (a flow as a path, ripples, falling sand, expanding rings; a network as nodes, a constellation, a colony of pulses). Reach past the obvious shape.
  • One accent. One thing moves meaningfully, in one accent colour. Show direction through motion, not a second hue.
  • Loop cleanly. End frame equals start frame, or fade-pause-fade. No jerk at the seam.
  • Caption economy. One short title plus a 5-word caption at most. The figure carries it.
  • Calm by default. 5 to 10 second loops, easeInOutCubic or easeInOutSine. Bounce and elastic read as toy.

Pick fonts, palette, background, canvas, and layout for the subject. Nothing below is a default.

Stack

One HTML file, one inline <svg>, motion in CSS. No JavaScript, no CDN, no build step.

That is not taste, it is the delivery format. A fig is made to be embedded: as an <img> in a report, as an attachment in an email, as a paste in a slide. Every one of those lifts the <svg> out and renders it as its own document. In that document no script ever runs and nothing is fetched. A figure animated from JavaScript freezes on its first frame there, or shows nothing at all. CSS @keyframes and SMIL are the only motion that survives the trip.

Five rules that document imposes. Break one and the figure dies quietly, looking fine on your screen:

  1. Everything lives inside <svg>. The <style>, the gradients, the filters. Only the svg element travels; whatever sits in <head> is left behind.
  2. xmlns="http://www.w3.org/2000/svg" on the svg. An HTML parser forgives a missing namespace. An <img> renders nothing without it.
  3. Strict XML. Every tag closes, and no bare < or & anywhere, including inside a CSS comment, where it is easiest to forget. Write &lt; and &amp;.
  4. No <script>, no event handlers, no requestAnimationFrame.
  5. RTL is declared, never inherited. dir="rtl" is an HTML attribute and means nothing to an svg document, and the host page's direction stops at the image boundary. Put style="direction:rtl" on the <svg> itself, or every text-anchor="start" label jumps to the wrong side of its anchor.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title></title>
<style>html, body { margin: 0; padding: 0; }</style>
</head>
<body>
<!-- One element, self-contained. Add style="direction:rtl" for an RTL figure. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 450" role="img" aria-label="">
<style>
  /* Palette, type, layout: design for the subject. */
  svg { --accent: #4f46e5; }

  /* Easing, as the cubic-bezier of the curve you want.
     easeInOutCubic  cubic-bezier(.65, 0, .35, 1)
     easeInOutSine   cubic-bezier(.37, 0, .63, 1)
     easeOutCubic    cubic-bezier(.33, 1, .68, 1)      */

  /* ONE duration for every animation in the figure, so the seam lines up.
     Offset a copy in time with a NEGATIVE delay, never a different duration. */
  .pulse   { animation: pulse 8s cubic-bezier(.65, 0, .35, 1) infinite; }
  .pulse.b { animation-delay: -2.67s; }

  /* First and last keyframe identical: that is what makes the loop seamless. */
  @keyframes pulse {
    0%, 100% { opacity: .25; transform: translateX(0); }
    50%      { opacity: 1;   transform: translateX(120px); }
  }

  @media (prefers-reduced-motion: reduce) { * { animation: none; } }
</style>

<!-- The figure. -->

</svg>
</body>
</html>

transform on an SVG element animates around the element's own origin; set transform-box: fill-box; transform-origin: center when you want it to rotate or scale in place. Reach for SMIL (<animateMotion>, <animate>) for the two things CSS cannot do here: moving along a <path>, and animating a geometry attribute that is not a CSS property in every renderer.

Check it before handing it back

Open the file. Then open it a second way, because that is the way it will actually be seen:

<img src="fig.html" alt="">

If the figure is still and the first view moved, the motion is in the wrong place. Static frame plus a broken-image glyph means the XML is malformed: rule 2 or 3.

GIF (only if the user asks)

A fig is HTML. If the user wants a GIF (Slack previews, slide screenshots, mail clients that strip <style> even out of an svg), use the bundled converter:

bash scripts/html2gif.sh <file.html> <loop_seconds>

It needs Playwright and ffmpeg installed locally. If either is missing, the script will tell the user how to install it. Do not install dependencies yourself, just relay what the script reports. The loop_seconds argument must match the figure's actual loop, otherwise the GIF jumps at the seam.

Never

No play/pause, no scrub, no multi-scene. Those belong to web-animation-engine. The recipient opens the file and the idea plays itself.

Files (claude-plugins)
  • scripts
    • html2gif.sh 6.6 KB
      #!/bin/bash
      # html2gif: convert an animated HTML file (a fig) to a perfectly looping HQ GIF.
      #
      # Pipeline: Playwright opens the page, pauses every clock it can reach and
      # seeks them to an exact time per frame (CSS/Web Animations playheads, the
      # SMIL timeline, and a stubbed performance.now + requestAnimationFrame for
      # older js-driven figs), screenshots each one. ffmpeg assembles the
      # frames into a GIF with palettegen+paletteuse and diff_mode=rectangle
      # so static regions don't get re-encoded; that's why static-bg +
      # moving-pulse animations end up tiny.
      #
      # Usage:
      #   html2gif.sh <input.html> [loop_sec] [width] [fps] [vp_w] [vp_h]
      #
      # Examples:
      #   html2gif.sh fig.html                      # all defaults
      #   html2gif.sh fig.html 5                    # 5-second loop
      #   html2gif.sh fig.html 8 800 20 1200 700    # everything custom
      
      set -euo pipefail
      
      # ====== PARAMETERS (override via CLI args) ======
      INPUT="${1:?usage: html2gif.sh <input.html> [loop_sec=8] [width=1000] [fps=15] [vp_w=1500] [vp_h=820]}"
      LOOP_SEC="${2:-8}"      # seconds per loop. must match the source animation's period
      WIDTH="${3:-1000}"      # output GIF width in px (height auto-scaled, aspect preserved)
      FPS="${4:-15}"          # output frame rate (15 is plenty for slow easings; 20+ for fast motion)
      VP_W="${5:-1500}"       # browser viewport width (the rendering canvas)
      VP_H="${6:-820}"        # browser viewport height
      
      # Locate Playwright. Auto-detects via `npm root -g`. Override via env if yours
      # lives elsewhere: NODE_PATH_PW=/path/to/node_modules html2gif.sh ...
      NODE_PATH_PW="${NODE_PATH_PW:-$(npm root -g 2>/dev/null)/@playwright/test/node_modules}"
      
      # ffmpeg encoding tuning:
      MAX_COLORS=128          # palette size (128 is fine for near-monochrome diagrams; 256 max)
      DITHER="bayer:bayer_scale=5"   # bayer = clean ordered dither; alternatives: sierra2_4a, none
      # =================================================
      
      # Dependency checks. Do not auto-install; just report what's missing.
      command -v node >/dev/null || { echo "Missing: node. Install from https://nodejs.org" >&2; exit 1; }
      command -v ffmpeg >/dev/null || { echo "Missing: ffmpeg. Install with 'brew install ffmpeg' (macOS) or 'apt install ffmpeg' (Linux)." >&2; exit 1; }
      [ -d "$NODE_PATH_PW/playwright" ] || { echo "Missing: Playwright at $NODE_PATH_PW. Install with 'npm install -g @playwright/test && npx playwright install chromium', or set NODE_PATH_PW to your own install path." >&2; exit 1; }
      
      ABSPATH="$(cd "$(dirname "$INPUT")" && pwd)/$(basename "$INPUT")"
      [ -f "$ABSPATH" ] || { echo "Not found: $ABSPATH" >&2; exit 1; }
      
      OUTPUT="${ABSPATH%.html}.gif"
      TMPDIR=$(mktemp -d)
      FRAMES="$TMPDIR/frames"
      mkdir -p "$FRAMES"
      trap "rm -rf $TMPDIR" EXIT
      
      N_FRAMES=$(( LOOP_SEC * FPS ))
      echo "→ Capturing $N_FRAMES frames (${LOOP_SEC}s × ${FPS}fps) at ${VP_W}×${VP_H}"
      
      NODE_PATH="$NODE_PATH_PW" node -e "
      const { chromium } = require('playwright');
      (async () => {
        const browser = await chromium.launch();
        // reducedMotion is pinned: a machine that prefers reduced motion would
        // otherwise honour the fig's own @media block and record a still GIF.
        const ctx = await browser.newContext({
          viewport: { width: $VP_W, height: $VP_H },
          reducedMotion: 'no-preference',
        });
        const page = await ctx.newPage();
      
        // Virtual clock: animation progress is driven by us, not wall time. A fig
        // animates with css @keyframes or SMIL and never touches rAF, so stubbing
        // rAF alone (what this did until 1.1.0) left every frame identical and the
        // GIF frozen. Each clock is paused and SEEKED to an absolute time instead,
        // which also drops the drift an incremental advance accumulated.
        await page.addInitScript(() => {
          let virtual = 0;
          Object.defineProperty(performance, 'now',
            { value: () => virtual, configurable: true, writable: true });
          let nextId = 1;
          const queue = new Map();
          window.requestAnimationFrame = (cb) => { const id = nextId++; queue.set(id, cb); return id; };
          window.cancelAnimationFrame  = (id) => queue.delete(id);
          window.__seek = (ms) => {
            virtual = ms;
            // css animations and transitions, via the Web Animations playhead.
            if (document.getAnimations) {
              document.getAnimations().forEach(a => {
                try { a.pause(); a.currentTime = ms; } catch (e) {}
              });
            }
            // SMIL keeps its own timeline, in seconds.
            document.querySelectorAll('svg').forEach(s => {
              try { s.pauseAnimations(); s.setCurrentTime(ms / 1000); } catch (e) {}
            });
            // ...and rAF, for a js-driven fig from before the css stack.
            const cbs = Array.from(queue.values());
            queue.clear();
            cbs.forEach(cb => { try { cb(virtual); } catch(e) {} });
          };
          window.__queueSize = () => queue.size;
        });
      
        await page.goto('file://$ABSPATH', { waitUntil: 'load', timeout: 60000 });
        await page.evaluate(() => document.fonts && document.fonts.ready).catch(() => {});
      
        // Wait until something is actually animating. A css fig satisfies this on
        // the first tick; the js branch is the slow one (Babel compile plus React
        // mount ran 1 to 30+ seconds on a cold load), which is why this waits on a
        // signal rather than sleeping.
        await page.waitForFunction(() => {
          if (document.getAnimations && document.getAnimations().length > 0) return true;
          if (document.querySelector('animate, animateTransform, animateMotion, set')) return true;
          const root = document.getElementById('root');
          return !!(root && root.children.length > 0 && window.__queueSize && window.__queueSize() > 0);
        }, { timeout: 90000 })
          .catch(() => { console.error('Warning: nothing was animating after 90s. The figure may be deliberately static, or its motion may be neither css @keyframes, SMIL, nor requestAnimationFrame. Frames may all be identical.'); });
        await page.waitForTimeout(200);   // fonts settle, react flushes initial state
      
        const N  = $N_FRAMES;
        const dt = 1000 / $FPS;
        const pad = (n) => String(n).padStart(4, '0');
      
        for (let i = 0; i < N; i++) {
          await page.evaluate((ms) => window.__seek(ms), i * dt);
          await page.waitForTimeout(8);   // let a js fig flush setState then DOM
          await page.screenshot({ path: '$FRAMES/f_' + pad(i) + '.png' });
        }
        await browser.close();
      })().catch(e => { console.error(e); process.exit(1); });
      "
      
      echo "→ Encoding GIF (width=${WIDTH}px, palettegen+paletteuse, diff_mode=rectangle)"
      ffmpeg -hide_banner -loglevel warning -y \
        -framerate "$FPS" -i "$FRAMES/f_%04d.png" \
        -filter_complex "scale=$WIDTH:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=$MAX_COLORS:stats_mode=diff[p];[s1][p]paletteuse=dither=$DITHER:diff_mode=rectangle" \
        -loop 0 \
        "$OUTPUT"
      
      SIZE=$(ls -lh "$OUTPUT" | awk '{print $5}')
      echo "✓ $OUTPUT ($SIZE)"
      
  • LICENSE 1 KB · in bundle
  • README.md 1.6 KB
    # fig
    
    A Claude skill for making a **fig**: a single looping animated SVG, one self-contained HTML file you can drop in an email or a slide.
    
    Use it when an idea moves (flows, loops, retries, queues, fan-outs). Faster than a paragraph, livelier than a static diagram. No player, no deck.
    
    ## Stack (1.1.0)
    
    One inline `<svg>`, motion in CSS `@keyframes` or SMIL. No JavaScript, no CDN, no build step.
    
    Before 1.1.0 the template drove motion from React plus Babel over a CDN, which contradicted the one thing a fig is for. A fig is made to be embedded: as an `<img>` in a report, an attachment in an email, a paste in a slide. In every one of those the svg is lifted out and rendered as its own document, where no script runs and nothing is fetched. Those figures froze on their first frame or showed nothing at all. Declarative motion survives the trip, and drops three CDN dependencies on the way out.
    
    Consequences worth knowing: everything the figure needs lives inside the `<svg>`, the `xmlns` is mandatory, the markup is parsed as strict XML (no bare `<` or `&`, including inside a CSS comment), and RTL must be declared on the svg itself (`style="direction:rtl"`) because `dir` is an HTML attribute that means nothing there.
    
    ## Install
    
    ```
    /plugin marketplace add smk-labs/claude-plugins
    /plugin install fig@smk
    ```
    
    ## What's in here
    
    ```
    SKILL.md             The skill itself, loaded into Claude's context when triggered.
    scripts/html2gif.sh  Optional helper. Converts a fig to a looping GIF
                         using Playwright + ffmpeg. Used only when asked.
    LICENSE              MIT.
    ```
    
    ## License
    
    MIT.
    
  • SKILL.md 5.5 KB
    ---
    name: fig
    description: >-
      One self-contained HTML file holding a looping animated SVG, for ideas that move: flows, loops,
      retries, queues, fan-outs. Use when the user says "fig" or a diagram would be better animated.
    ---
    
    Some ideas are explained better by a 5-second looping animation than by 100 lines of text.
    
    Sketch the figure in ASCII first: static layout, labelled motion, loop length. Get a yes before writing the svg.
    
    After building, look at the rendered file as a stranger would and refine once before handing back. ASCII covers structure; visual issues (collisions, weak contrast, orphan elements, dated chrome, loose components that should be grouped) only appear on screen.
    
    ## Guidelines
    
    - **Subject fit.** Invent a visual metaphor for this specific idea. The same concept can be drawn many ways (a flow as a path, ripples, falling sand, expanding rings; a network as nodes, a constellation, a colony of pulses). Reach past the obvious shape.
    - **One accent.** One thing moves meaningfully, in one accent colour. Show direction through motion, not a second hue.
    - **Loop cleanly.** End frame equals start frame, or fade-pause-fade. No jerk at the seam.
    - **Caption economy.** One short title plus a 5-word caption at most. The figure carries it.
    - **Calm by default.** 5 to 10 second loops, `easeInOutCubic` or `easeInOutSine`. Bounce and elastic read as toy.
    
    Pick fonts, palette, background, canvas, and layout for the subject. Nothing below is a default.
    
    ## Stack
    
    One HTML file, one inline `<svg>`, motion in CSS. No JavaScript, no CDN, no build step.
    
    That is not taste, it is the delivery format. A fig is made to be embedded: as an `<img>` in a report, as an attachment in an email, as a paste in a slide. Every one of those lifts the `<svg>` out and renders it as its own document. In that document **no script ever runs and nothing is fetched**. A figure animated from JavaScript freezes on its first frame there, or shows nothing at all. CSS `@keyframes` and SMIL are the only motion that survives the trip.
    
    Five rules that document imposes. Break one and the figure dies quietly, looking fine on your screen:
    
    1. **Everything lives inside `<svg>`.** The `<style>`, the gradients, the filters. Only the svg element travels; whatever sits in `<head>` is left behind.
    2. **`xmlns="http://www.w3.org/2000/svg"` on the svg.** An HTML parser forgives a missing namespace. An `<img>` renders nothing without it.
    3. **Strict XML.** Every tag closes, and no bare `<` or `&` anywhere, including inside a CSS comment, where it is easiest to forget. Write `&lt;` and `&amp;`.
    4. **No `<script>`, no event handlers, no `requestAnimationFrame`.**
    5. **RTL is declared, never inherited.** `dir="rtl"` is an HTML attribute and means nothing to an svg document, and the host page's direction stops at the image boundary. Put `style="direction:rtl"` on the `<svg>` itself, or every `text-anchor="start"` label jumps to the wrong side of its anchor.
    
    ```html
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <title></title>
    <style>html, body { margin: 0; padding: 0; }</style>
    </head>
    <body>
    <!-- One element, self-contained. Add style="direction:rtl" for an RTL figure. -->
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 450" role="img" aria-label="">
    <style>
      /* Palette, type, layout: design for the subject. */
      svg { --accent: #4f46e5; }
    
      /* Easing, as the cubic-bezier of the curve you want.
         easeInOutCubic  cubic-bezier(.65, 0, .35, 1)
         easeInOutSine   cubic-bezier(.37, 0, .63, 1)
         easeOutCubic    cubic-bezier(.33, 1, .68, 1)      */
    
      /* ONE duration for every animation in the figure, so the seam lines up.
         Offset a copy in time with a NEGATIVE delay, never a different duration. */
      .pulse   { animation: pulse 8s cubic-bezier(.65, 0, .35, 1) infinite; }
      .pulse.b { animation-delay: -2.67s; }
    
      /* First and last keyframe identical: that is what makes the loop seamless. */
      @keyframes pulse {
        0%, 100% { opacity: .25; transform: translateX(0); }
        50%      { opacity: 1;   transform: translateX(120px); }
      }
    
      @media (prefers-reduced-motion: reduce) { * { animation: none; } }
    </style>
    
    <!-- The figure. -->
    
    </svg>
    </body>
    </html>
    ```
    
    `transform` on an SVG element animates around the element's own origin; set `transform-box: fill-box; transform-origin: center` when you want it to rotate or scale in place. Reach for SMIL (`<animateMotion>`, `<animate>`) for the two things CSS cannot do here: moving along a `<path>`, and animating a geometry attribute that is not a CSS property in every renderer.
    
    ## Check it before handing it back
    
    Open the file. Then open it a second way, because that is the way it will actually be seen:
    
    ```html
    <img src="fig.html" alt="">
    ```
    
    If the figure is still and the first view moved, the motion is in the wrong place. Static frame plus a broken-image glyph means the XML is malformed: rule 2 or 3.
    
    ## GIF (only if the user asks)
    
    A fig is HTML. If the user wants a GIF (Slack previews, slide screenshots, mail clients that strip `<style>` even out of an svg), use the bundled converter:
    
    `bash scripts/html2gif.sh <file.html> <loop_seconds>`
    
    It needs Playwright and ffmpeg installed locally. If either is missing, the script will tell the user how to install it. Do not install dependencies yourself, just relay what the script reports. The `loop_seconds` argument must match the figure's actual loop, otherwise the GIF jumps at the seam.
    
    ## Never
    
    No play/pause, no scrub, no multi-scene. Those belong to `web-animation-engine`. The recipient opens the file and the idea plays itself.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related