agentsclimarketplace

Playwright gif capture

Skill simiancraft/simiancraft-skills/skills/playwright-gif-capture

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.From its SKILL.md

Install
npx -y skills add simiancraft/simiancraft-skills --skill playwright-gif-capture

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

3 things to look at

  • 6 stars6 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
  • runs commandsInstructs the agent to run 5 commands, including `cd /tmp && npm i ffmpeg-static >/dev/null 2>&1` and 4 more.
  • fetches URLsInstructs the agent to fetch 1 URL, including TARGET_URL (page.goto; defaults to http://localhost:8080/).

SKILL.md

7.2 KB, ~1.7k tokens by cl100k_base, as published. Nobody here has run it

GIF Capture: specialization of playwright-harness

Read playwright-harness first. The base owns prerequisites, the run pattern (write to /tmp/pw-*.mjs, run with playwright resolvable), and the drive/assert

  • WebGL-GPU patterns. This skill changes only how you turn a running animation 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.

Prerequisites (in addition to the base)

  • ffmpeg to encode frames/video into a GIF; npm i ffmpeg-static in a /tmp dir gives a binary path, no system install.
  • For tuning, gifski (best on gradients) and gifsicle (size reduction); see references/gif-optimization.md. ImageMagick is an optional alternative.

Capture the frames. Two ways.

A. Frame-by-frame screenshots (default)

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.

B. Playwright recordVideo (DOM/CSS animations you cannot time-step)

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.

Encode to a GIF

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.

GPU note (delta)

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 frame. See playwright-harness, "WebGL / GPU caveat".

Embedding the GIF in a GitHub PR / issue / README

GitHub's inline drag-and-drop upload (user-attachments) has no API, so an agent cannot post one that way. The working pattern: commit the GIF to a never-merge assets branch (e.g. evidence/issue-verification) and embed it by raw.githubusercontent URL (![demo](https://raw.githubusercontent.com/<org>/<repo>/evidence/issue-verification/<path>.gif)), which renders and animates inline. Push that branch from a detached worktree with --no-verify (an assets worktree has no node_modules, so a pre-commit hook manager such as lefthook or husky would fail). Never open a PR from the assets branch. For a committed README asset, a normal commit on the feature branch is fine; mind the repo's size budget.

Scope

  • CAN: produce a reproducible, tight animated GIF of any web/canvas/WebGL animation; crop to an element; control frame count, fps, size, and palette; capture the real deployed site (point TARGET_URL at it).
  • CANNOT: prove real-time FPS/perf (frame-by-frame is a controlled reconstruction at a chosen fps, not a measurement; use a GPU-time meter for cost); capture audio; or render a single frame so heavy it trips the OS GPU watchdog (split the work or lower the load first).

What ships with it: 1 file

7.5 KB alongside SKILL.md

references/

Gives 0 of the 12 instructions most e2e browser skills give in ~1.7k tokens

Counted across 499 of the 513 authors here whose files we hold, read 2026-09-06

  • Capture screenshots, videos, and traces on failurein 32 of 499, across 23 files
  • Close the browser when donein 22 of 499
  • Interact with elements using snapshot refsin 21 of 499, across 20 files
  • Wait for specific network responses instead of fixed timeoutsin 20 of 499, across 10 files
  • Keep tests independent with no shared statein 19 of 499, across 17 files
  • Use Page Object Model classes to encapsulate page interactionsin 19 of 499, across 9 files
  • Locate elements with data-testid attributesin 19 of 499, across 10 files
  • Quarantine flaky tests with fixme or skipin 17 of 499, across 7 files
  • Upload test artifacts after every CI runin 17 of 499, across 8 files
  • Wait on conditions instead of using fixed sleepsin 17 of 499, across 13 files
  • Clean up test data after each testin 17 of 499, across 16 files
  • Test user-visible behavior, not implementation detailsin 16 of 499, across 10 files

Said here and by no other author read

  • Read the playwright-harness skill first
  • Step the animation clock and screenshot each frame
  • Capture exactly one period so the loop is seamless
  • Close the context, not the page, to finalize recording
  • Install ffmpeg-static with npm into /tmp
  • Read gif-optimization.md before tuning quality or size

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.