Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Creates animated looping GIFs from CSS animations (default) or AI image-to-video. 800×800px default, 6 animation types, 4 style presets. Trigger when user says "create an animated gif", "make a looping gif", "animated banner", "CSS animation gif", "social media animation", "make this loop", "animated graphic", or "motion graphic".
.claude/skills/varnan-tech-graphic-gif/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | 231% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 18% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 45% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 138% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 124% | 0% |
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 motionbash # 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:
css*, *::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):
css@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:
html<!-- 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:
bashmkdir -p [slug]
Save HTML:
[slug]/animation.htmlOpen in browser for quick visual check:
bashopen [slug]/animation.html
Run export script (replace [skill-root] with the actual path to this skill):
bashbash [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 <3MBOnly 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:
bash # 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. typewriter and counter are 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"
Other measured skills in the registry, with their headline benchmark lift.