graphic-gif — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited graphic-gif (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.gifRead 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 level@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.htmlOpen in browser for quick visual check:
open [slug]/animation.htmlRun 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 <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:
# 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"
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.