Generates animated looping GIFs from CSS @keyframes captured via Playwright (default) or AI image-to-video. 800x800px canvas, 6 animation types, 4 style presets. Auto-triggers on GIF/animation requests.
npx claudepluginhub varnan-tech/opendirectory --plugin opendirectory-gtm-skillsThis skill uses the workspace's default tool permissions.
Generates an animated looping GIF from CSS animations or an AI image-to-video API. Output: `animation.gif`.
Creates AI-driven animations for logos, UI elements, icons, and social media assets from SVG, PNG, or Figma inputs. Exports Lottie JSON for web/mobile or GIF/MP4.
Generates Slack-optimized animated GIFs and emojis with size/dimension validators and composable primitives like shake, bounce. For GIF creation requests.
Creates animated GIFs optimized for Slack messages and emojis using Python validators for size/dimensions/FPS, composable animation primitives like shake/bounce, and utilities.
Share bugs, ideas, or general feedback.
Generates an animated looping GIF from CSS animations or an AI image-to-video API. Output: animation.gif.
Unlike every other graphic- skill that outputs a static PNG or PDF, this skill outputs an animated .gif. Uses CSS @keyframes animations captured frame-by-frame via Playwright and the Web Animations API (Option A, default), or an AI image-to-video pipeline via Kling (Option B).
ai-generated unless explicitly requested.clamp() values computed at 800px (1vw = 8px).<style>. Font CDN <link> only external dependency.setTimeout loops, NOT animation-delay tricks.Math.floor(duration_seconds * fps) frames. The frame at t=duration_ms MUST NOT be captured — it duplicates t=0 and causes a visible stutter at the loop point.clean-slate, terminal, electric-burst, brutalist.@keyframes percentages — frame seeking handles timing.Required: prompt (content description AND motion brief)
Optional with defaults:
| Parameter | Default | Options |
|---|---|---|
| animation_type | css-animated | css-animated / ai-generated |
| duration | 3.0 | seconds |
| fps | 12 | frames per second |
| loop | true | true / false |
| style | clean-slate | clean-slate / terminal / electric-burst / brutalist |
| dimensions | 800x800 | WxH in pixels |
| optimization | balanced | quality / balanced / filesize |
If prompt is missing or lacks motion description, ask exactly:
"What should the GIF show? Describe the content AND the motion (e.g., 'Stats count up: 73% of buyers read 3+ pieces of content before purchase. Typewriter effect, one character at a time. Style: terminal. 3 seconds, 12fps.')
Key settings (all optional, defaults shown):
- animation_type: css-animated (default) or ai-generated
- duration: 3.0 seconds
- fps: 12
- loop: true
- style: clean-slate (options: clean-slate / terminal / electric-burst / brutalist)
- dimensions: 800x800
- optimization: balanced (options: quality / balanced / filesize)"
If all required info is present → skip directly to Step 2.
For css-animated:
fade-in, slide-in, typewriter, counter, pulse, loop-scrollreferences/animation-library.md — find the chosen type's full HTML/CSS specreferences/style-presets.md — load the chosen style's CSS token blockMath.floor(duration_seconds * fps) — write this number down| Decision | Derive from |
|---|---|
| Tone | Emotional register for audience (mechanical / warm / electric / professional) |
| Signature element | ONE visual device used consistently (cursor blink, ghost number, scan-line overlay, accent border) |
| Motion style | Ease curve philosophy for this type (spring / linear / step / ease-in-out) |
| Unforgettable detail | The ONE thing a viewer will remember about this GIF |
For ai-generated:
POST https://api.klingai.com/v1/videos/image2video with image_url and prompt describing the motion# Two-pass palette for best color quality
ffmpeg -i input.mp4 -vf "fps=12,scale=800:800:flags=lanczos,palettegen=stats_mode=diff" palette.png
ffmpeg -i input.mp4 -i palette.png -vf "fps=12,scale=800:800:flags=lanczos,paletteuse=dither=bayer:bayer_scale=5" output.gif
Read references/animation-library.md and references/style-presets.md before generating.
Canvas base — required on every GIF:
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 800px;
height: 800px;
overflow: hidden;
background: var(--bg);
font-family: var(--font-body);
}
.canvas {
width: 800px;
height: 800px;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
Animation rules:
animation-fill-mode: forwards (or both) on ALL animated elementstypewriter → steps(N, end) where N = exact character countcounter → linearfade-in → cubic-bezier(0.22, 1, 0.36, 1) (ease-out)slide-in → cubic-bezier(0.34, 1.56, 0.64, 1) (spring overshoot)pulse → ease-in-out with animation-iteration-count: infiniteloop-scroll → linear with animation-iteration-count: infiniteanimation-iteration-count: 1 — looping happens at GIF levelanimation-delay — stagger is baked into @keyframes percentagesTypewriter N calculation: Count every character including spaces, punctuation, numbers:
steps(14, end)steps(13, end)Counter CSS @property (required for counter type):
@property --num {
syntax: '<integer>';
inherits: false;
initial-value: 0;
}
.counter {
animation: countUp var(--duration) linear forwards;
counter-reset: num var(--num);
}
.counter::after { content: counter(num); }
@keyframes countUp {
from { --num: 0; }
to { --num: var(--target); }
}
Loop-scroll: content MUST be duplicated:
<!-- 4 original items + 4 duplicate items -->
<div class="ticker-track" style="--item-count: 4;">
[item1][item2][item3][item4][item1][item2][item3][item4]
</div>
translateX(0 → -50%) with linear infinite.
Design quality rules (from commit in Step 2):
#fff for dark styles — use the preset's exact --bg value::after with repeating-linear-gradient at opacity: 0.034px solid #000 or 4px solid var(--accent) on key elementCanvas:
body and .canvas exactly 800×800px (or specified dimensions)overflow: hidden on both body and .canvasAnimations:
animation-delay anywhere — stagger is in @keyframes percentagest=0 (Web Animations API will seek from there)animation-fill-mode: forwards or both on all animated elementsanimation-iteration-count: 1animation-iteration-count: infiniteType-specific checks:
steps(N, end) = exact character count of text string@property --num declared with syntax: '<integer>' and initial-value: 0counter-reset: num var(--num) and ::after { content: counter(num) }Design:
references/style-presets.md — no free-floating hex colors<link> present for chosen style's fontDetermine slug from prompt (kebab-case, ≤30 chars). Create output directory:
mkdir -p [slug]
Save HTML:
[slug]/animation.html
Open in browser for quick visual check:
open [slug]/animation.html
Run export script (replace [skill-root] with the actual path to this skill):
bash [skill-root]/scripts/export-gif.sh \
[slug]/animation.html \
[slug]/animation.gif \
--duration [duration] \
--fps [fps] \
[--no-loop if loop=false] \
--optimization [optimization] \
--width [W] \
--height [H]
The script:
gifenc, sharp (or jimp), and playwright in a temp directorycapture-and-encode.mjs — pauses animations, seeks each frame, screenshots, assembles GIFgifsicle optimization pass if availableIf export script not found at [skill-root]/scripts/export-gif.sh, check that the skill was installed with its scripts/ folder intact.
Show after successful export:
## GIF: [1-line description]
Date: [YYYY-MM-DD] | Style: [style] | Animation: [type] | [duration]s @ [fps]fps
Dimensions: [WxH] | Frames: [N] | Loop: [true/false]
Files
Source: [slug]/animation.html
Output: [slug]/animation.gif
Size: [X] KB
Checklist
- [ ] Preview loops cleanly at start/end point (no stutter)
- [ ] Text legible at intended display size
- [ ] File size appropriate: email <500KB / social <3MB
Only use when animation_type: ai-generated is explicitly specified.
Requirements:
KLING_API_KEY (66 free credits/day, no credit card for free tier)ffmpeg installed locally for video→GIF conversionWorkflow:
# Quick screenshot via Playwright
node -e "
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: W, height: H } });
await page.goto('file://[slug]/animation.html');
await page.screenshot({ path: '[slug]/base-frame.png' });
await browser.close();
})();
"
When Kling is unavailable: Fall back to css-animated with a note to the user: "AI generation requires a Kling API key (KLING_API_KEY). Falling back to css-animated. Set the key to enable AI generation."
"Describe motion, not just content. 'Stats count up one by one' beats 'show stats'."
"Keep it simple for file size. 1–3 animated elements and a solid background."
"Think in loops. The animation should flow invisibly from end back to start."
"Specify the animation type explicitly.
typewriterandcounterare the most effective for social."✅ Good: "Create an animated GIF, css-animated, typewriter effect. Text: '73% of B2B buyers read 3+ pieces of content before contacting sales.' Each character types out one at a time. Style: terminal. 3 seconds, 12fps, loop=true."
❌ Bad: "make an animated gif of marketing tips"