agentleFS
jfkconstructjfkconstruct/claude-skills/skills/gif-loop-capture/SKILL.md

gif-loop-capture

Skill0 starsChanged 54 days agoMITView on GitHub

Turn any local HTML game/app/animation into an animated-webp gameplay loop (playwright frame-burst -> PIL assemble), with parallel fan-out across many targets, state-injection for hard-to-reach screens, and the reduced-motion wire-in pattern. Use when the user asks for gameplay gifs/loops, animated tiles, or "capture X in motion".

  • Installs packages

The file

Files
SKILL.md

Gameplay loop capture (animated webp)

Templates in this directory: capture_template.py, assemble_template.py.

Setup

pip install playwright pillow
python -m playwright install chromium

Optional for mp4 output: ffmpeg on PATH.

Pipeline

  1. Serve the target dir once, shared: python -m http.server 8091 --bind 127.0.0.1 from the app root, background. One read-only server for every capture; start it once, stop it when the batch is done (it is yours, nothing else owns it). If the port is taken, pick another and change URL in the template.
  2. One capture per target, in its own working directory (run them in parallel with subagents if you have many), all writing to ONE shared out/ dir with UNIQUE per-target filenames (<slug>-loop.webp). Each capture needs: URL + source file path, how to reach visible motion (or "read the game and find it"), the capture/assemble specs below, and the <=20-line return contract.
  3. Capture spec (see capture_template.py): headless chromium, viewport 960x720, wait load + document.fonts.ready + 2 rAF + settle so frame 1 has no loading flash. 28-36 frames of the canvas/game element spaced ~140ms. Drive inputs DURING the burst so motion spans frames.
  4. Assemble spec (see assemble_template.py): PIL, RGB, cover-crop+resize to exactly 720x540, save_all=True, duration=140, loop=0, quality=68, method=4. Re-save at ~24f/q60 only if >2000 KB. Observed sizes: 54-780 KB per 32-frame loop.
  5. Orchestrator verify, never skip: (a) PIL reopen each: is_animated, n_frames, size==(720,540); (b) md5 across all outputs, identical hashes = workdir collision; (c) frame-diff first/mid/last nonzero = real motion; (d) build a first/mid/last contact sheet PNG and EYEBALL it (probe-invisible defects are a recurring class).

Reaching hard game states

  • Save-inject beats playing the game: add_init_script sets the game's localStorage save key BEFORE page scripts run; read the game's loadGame() for required fields. Worked on two games that gate a state behind progress.
  • Pre-seed flags the same way to skip tutorials or demos (for example a demoSeen:true key).
  • Calling the game's own JS (load(4)) to jump levels is fine; the RENDERED result must be genuine gameplay.
  • Real pointer/key events over test hooks for the visible action itself.

Wire-in (site tiles)

Known gotchas

  • NEW filenames on every recapture; browsers cache images past a css cache-bust.
  • Probing lazy swap at narrow widths: off-screen tiles legitimately DON'T swap until scrolled near; scroll to the LAST tile before counting, or the probe fails on correct behavior.
  • img.decode() on a loading="lazy" image that never started loading NEVER settles (the probe hangs silently): scroll-sweep the page to trigger lazy loads first, and always Promise.race the decode-wait against a timeout.
  • Bigger display target = bigger RASTER, same composition: zoom: 1.5 on the rig stage re-renders the approved layout at a larger glyph raster (real sharpness, unlike upscaling the asset) with zero layout edits; capture viewport must fit the ZOOMED dims ( band, 07-31).
  • Animated-webp write support: Pillow >=11 always has it; features.check('webp_anim') warns Unknown, ignore it.
  • libwebp merges pixel-identical consecutive frames; n_frames < captured count is not a bug if source PNGs md5-match.
  • Long readable holds: design the rig so holds are EXACTLY static (all eases saturated, counters stopped), then dedupe at capture: merge identical frames (im.tobytes() compare) and pass duration= a per-frame LIST; a 116-frame 16.2s cycle shipped as 54 frames / 274 KB with timing intact (applied-demo capture.py, 07-31).
  • Verifying per-frame durations on readback: PIL fills info["duration"] on load(), not on seek(); seek-then-read reports 0 and looks like the durations were dropped when they were not.

Posting to LinkedIn

LinkedIn accepts mp4 (as video) or gif/png/jpg (as image), never webp. Keep the webp for the page and convert a copy for the post: python assemble_template.py loop.mp4 or loop.gif (the assembler picks the format from the extension; mp4 needs ffmpeg). A 1080x1350 portrait frame reads best in the feed; hide buttons, kbd hints, and captions before the burst so the loop shows the work, not the chrome.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.