android-emulator-harne…
Bring up an Android app in a headless emulator on Linux/WSL and drive it for automated integration testing, the Android analog of Playwright for web. Boots an…
Specialization of playwright-harness for capturing an ANIMATED GIF of web content: drive a page/canvas/WebGL animation, grab a sequence of frames, and encode a looping GIF. Use when the task is "make a gif of this", "grab a gif of the demo/animation/shader", "record the canvas
$ npx -y skills add simiancraft/simiancraft-skills --skill playwright-gif-capture --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/playwright-gif-captureContext preview
The summary Claude sees to decide when to auto-load this skill.
Specialization of playwright-harness for capturing an ANIMATED GIF of web content: drive a page/canvas/WebGL animation, grab a sequence of frames, and encode a looping GIF. Use when the task is "make a gif of this", "grab a gif of the demo/animation/shader", "record the canvas
name: playwright-gif-capture description: >- Specialization of playwright-harness for capturing an ANIMATED GIF of web content: drive a page/canvas/WebGL animation, grab a sequence of frames, and encode a looping GIF. Use when the task is "make a gif of this", "grab a gif of the demo/animation/shader", "record the canvas as a gif", or "capture a looping clip for a PR/README/issue"; any time a still screenshot is not enough. Read playwright-harness FIRST for script conventions and execution. For the quality/size dials (preventing deep-fried color, janky frame rate, or oversized files) read references/gif-optimization.md rather than guessing. Validated on Linux/WSL, headless Chromium + ANGLE, against WebGL canvas content.
**Read `playwright-harness` first.** The base owns prerequisites, the run pattern (write to `/tmp/pw-*.mjs`, run with `playwright` resolvable), and the drive/assert
into frames. Encoding and the quality/size tradeoffs live in **`references/gif-optimization.md`**; open it before tuning, since a deep-fried, janky, or oversized GIF is easy to make by accident.
**Headless is required here** (not just the default): the encode is offline and you want clean, chrome-free frames.
gives a binary path, no system install.
`references/gif-optimization.md`. ImageMagick is an optional alternative.
The default for canvas, WebGL, or any clock-driven animation. Drive the clock yourself, one frame at a time, and screenshot each step: deterministic (same frames every run), evenly spaced (no wall-clock jitter, so no jank), croppable to an element, and it sidesteps the recordVideo GPU-death trap below.
// /tmp/pw-gif.mjs (run via the harness: node, with playwright resolvable)
import { chromium } from 'playwright';
import { mkdirSync } from 'node:fs';
const TARGET_URL = process.env.TARGET_URL || 'http://localhost:8080/';
const FRAMES = 48, FPS = 16, OUT = '/tmp/gif-frames';
mkdirSync(OUT, { recursive: true });
const browser = await chromium.launch({
headless: true,
args: ['--use-gl=angle', '--use-angle=gl', '--ignore-gpu-blocklist'], // real GPU under WSLg; see GPU note
});
const page = await browser.newPage({ viewport: { width: 960, height: 540 } });
await page.goto(TARGET_URL, { waitUntil: 'load' });
const target = page.locator('#canvas'); // element to crop to (omit -> full page)
for (let i = 0; i < FRAMES; i++) {
const t = i / FPS; // animation time for this frame (seconds)
// Step the animation deterministically. Best: the page exposes a render hook.
await page.evaluate((t) => window.__renderAtTime?.(t), t);
await page.waitForTimeout(30); // let the draw land (and the GPU flush)
await target.screenshot({ path: `${OUT}/f${String(i).padStart(4, '0')}.png` });
}
await browser.close();
console.log(`wrote ${FRAMES} frames to ${OUT}`);The clean version needs a **render-at-time hook** on the page (`window.__renderAtTime(t)` sets the clock and draws one frame). If the app only has a free-running `requestAnimationFrame` loop, either (a) add a tiny hook for the capture, or (b) fall back to wall-clock sampling: `waitForTimeout(1000/FPS)` between shots, accepting minor unevenness. Capture exactly one period of a periodic animation so it loops seamlessly.
const context = await browser.newContext({
recordVideo: { dir: '/tmp/gif-vid', size: { width: 960, height: 540 } },
});
const page = await context.newPage();
await page.goto(TARGET_URL);
await page.waitForTimeout(3000); // record real-time playback
await context.close(); // <- video finalizes on CONTEXT close, not page close
// -> /tmp/gif-vid/<hash>.webm, then encode (below)Real-time, whole viewport, no hook needed. Two costs: it records wall-clock (so it inherits any jank), and **under heavy GPU load it can kill the GPU process mid-record and the rest goes black** (cranked WebGL + recordVideo is the known offender). For canvas/shader content prefer method A, or record at a light load.
A sane default to get a watchable loop (`ffmpeg-static` via `npm i` in /tmp; npm, not bun, since the postinstall downloads the binary):
cd /tmp && npm i ffmpeg-static >/dev/null 2>&1
FF=$(node -e "process.stdout.write(require('/tmp/node_modules/ffmpeg-static'))")
# from frame PNGs (method A):
"$FF" -y -framerate 16 -i /tmp/gif-frames/f%04d.png \
-vf "scale=720:-1:flags=lanczos,split[a][b];[a]palettegen=max_colors=256:stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=5" \
-loop 0 /tmp/out.gif
# from a recorded webm (method B): swap "-framerate 16 -i .../f%04d.png" for "-i /tmp/gif-vid/*.webm" and add fps=16 to the -vf chain**Then stop guessing and open `references/gif-optimization.md`** for the color/fps/size tradeoff model, the tool matrix (ffmpeg vs gifski vs gifsicle vs ImageMagick), and the target-size loop. The short version: banding/deep-fried, raise the dither or switch to gifski; too large, trade dims, then duration, fps, colors, then a gifsicle lossy pass; janky, capture by method A at a fixed clock and encode at the capture fps (12 to 20 is the sweet spot).
Always print the output byte size and **Read the GIF back** (vision) before shipping.
The base's WebGL/GPU caveat applies verbatim; the launch args above already use it. For shader content the ANGLE relaunch is mandatory (heavy shaders no-op under SwiftShader headless), and you must let the animation settle a second or two (PSO compile) before the first captured fram
Claude Code skills for the full arc of a change: farm to table, with receipts. Curated Claude Code skills and agents from simiancraft. Most skill collections are grab bags. This one has a spine: it carries a change through its whole life.
Repo: simiancraft/simiancraft-skills
Bring up an Android app in a headless emulator on Linux/WSL and drive it for automated integration testing, the Android analog of Playwright for web. Boots an…
Specialization of android-emulator-harness for CAMERA / segmentation testing: get a real PERSON in front of the emulator camera so MediaPipe / ML Kit selfie…
Shrink a media asset to the smallest bytes that still serve its purpose, keyed on asset kind (raster, vector, animation, video, audio, model, document, font)…
Read and use the Expo / React Native in-app developer tools: the developer menu (reload, go home, performance monitor, element inspector, Open DevTools, and…
Run and drive an Expo / React Native app on the iOS Simulator. Pick an execution mode (Expo Go, dev client, Storybook-mobile, web-on-mobile), build and install…
Plans self-destruct when shipped (the Inspector Gadget Rule). Use this skill to draft tactical, hand-off-ready planning docs with a 150-word Goal cap, atomic…