Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Create annotated animated GIF demos and screen recordings for pull requests and documentation. Covers frame capture, timing, imageio-based GIF creation, and per-frame annotation workflows.
.claude/skills/screen-recording/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | — | — |
| case-11 | ✗→✓ | ▲ Improved | — | — |
| case-02 | ✗→✓ | ▲ Improved | — | — |
| case-06 | ✗→✓ | ▲ Improved | — | — |
| case-16 | ✗→✓ | ▲ Improved | — | — |
Create animated GIF demos that show a feature or workflow in action — with annotations, variable timing, and proper pacing. Useful for PR descriptions, documentation, and release notes.
Use this skill when you need to:
bashpip install playwright Pillow imageio numpy scipy mss -q playwright install chromium
Use Playwright to step through the interaction and capture each frame:
pythonfrom playwright.async_api import async_playwright async def record_frames(url, steps, width=1400, height=900): """ steps: list of dicts with 'action' (async callable taking page) and 'name' (frame filename) """ async with async_playwright() as p: browser = await p.chromium.launch() page = await browser.new_page(viewport={"width": width, "height": height}) await page.goto(url, wait_until="networkidle") for step in steps: if step.get("action"): await step["action"](page) await page.wait_for_timeout(step.get("wait", 500)) await page.screenshot(path=step["name"]) await browser.close()
Use imageio, not PIL, for GIF writing — PIL's GIF encoder merges visually similar frames, which kills animations.
pythonimport imageio.v3 as iio from PIL import Image import numpy as np frames = [] durations = [] for frame_path, duration_ms in frame_list: img = Image.open(frame_path) frames.append(np.array(img)) durations.append(duration_ms) iio.imwrite("demo.gif", frames, duration=durations, loop=0)
Uniform timing makes everything feel either too fast or too slow. Use variable durations:
| Phase | Duration | Why | |-------|----------|-----| | Fast action (typing, clicking) | 100ms | Feels natural, keeps energy | | Pause after action | 600-800ms | Let the viewer process what happened | | Hero/final message | 500ms+ | Main takeaway needs time to land |
Apply annotations to specific frames using the image-annotations skill:
pythonfrom PIL import Image, ImageDraw, ImageFont def annotate_frame(frame_path, annotations, out_path): img = Image.open(frame_path) draw = ImageDraw.Draw(img) for ann in annotations: # Apply annotation (rect, arrow, label, etc.) pass img.save(out_path)
For smooth annotation appearance:
pythondef apply_fade(base_frame, annotation_layer, alpha): """Blend annotation onto frame at given alpha (0.0 to 1.0)""" blended = Image.blend( base_frame.convert("RGBA"), annotation_layer.convert("RGBA"), alpha ) return blended.convert("RGB") # 2-frame pop-in at 10fps: 50% then 100% faded_frames = [ apply_fade(base, annotations, 0.5), # frame 1: half opacity apply_fade(base, annotations, 1.0), # frame 2: full opacity ]
At 10fps, use 2 fade frames (0.2s total). At 30fps, use 3-4 frames. Easing curves look bad at low FPS — simple pop-in is snappier and more readable.
The annotation logic gets complex for anything beyond trivial demos. Write a dedicated script (e.g., annotate_gif.py) with functions instead of inline code. You'll iterate on timing and placement.
Always test in isolation first — don't rebuild the full demo to test a fade tweak:
python# Small test GIF: 10 bare frames → fade frames → 15 hold frames # Add a frame counter overlay for debugging: draw.text((10, height - 30), f"F{i}/{total} a={alpha:.0%} FADE", fill="white", font=small_font)
For recording desktop apps, terminals, or anything outside a browser. Uses mss for fast screen capture.
pythonimport mss from PIL import Image import time def record_gif(output_path, region=None, duration=5, fps=8): """Record screen region to GIF. region = {left, top, width, height} or None for full screen.""" with mss.mss() as sct: if region is None: region = sct.monitors[1] # primary monitor frames = [] t_end = time.time() + duration while time.time() < t_end: t0 = time.time() shot = sct.grab(region) frames.append(Image.frombytes('RGB', shot.size, shot.rgb)) time.sleep(max(0, 1 / fps - (time.time() - t0))) frames[0].save(output_path, save_all=True, append_images=frames[1:], duration=int(1000 / fps), loop=0, optimize=True) return len(frames) record_gif('demo.gif', region={'left': 0, 'top': 0, 'width': 800, 'height': 500}, duration=3)
Tested: 3s at 8fps → 24 frames, ~31KB. Keep fps ≤ 10 for reasonable file sizes.
Note: PIL.save(save_all=True) works for simple recordings but merges visually similar frames. For annotated GIFs with fade effects, use imageio.v3.imwrite instead.
python# Find window rect, then record it as a GIF # Reuse find_window() from the ui-screenshots skill import ctypes from ctypes import c_int, Structure, byref, windll class RECT(Structure): _fields_ = [('left', c_int), ('top', c_int), ('right', c_int), ('bottom', c_int)] hwnd = find_window('My App')[0][0] rect = RECT() windll.user32.GetWindowRect(hwnd, byref(rect)) region = {'left': rect.left, 'top': rect.top, 'width': rect.right - rect.left, 'height': rect.bottom - rect.top} record_gif('app-demo.gif', region=region, duration=5, fps=8)
Programmatically find changed regions between frames to decide what to annotate:
pythonimport numpy as np from scipy import ndimage def find_changed_clusters(frame_a, frame_b, threshold=30, min_pixels=300, dilate=5): """Find bounding boxes of changed regions between two frames.""" diff = np.abs(frame_b.astype(float) - frame_a.astype(float)).max(axis=2) mask = diff > threshold dilated = ndimage.binary_dilation(mask, iterations=dilate) labeled, n = ndimage.label(dilated) clusters = [] for i in range(1, n + 1): ys, xs = np.where(labeled == i) if len(ys) < min_pixels: continue clusters.append((xs.min(), ys.min(), xs.max(), ys.max(), len(ys))) return sorted(clusters, key=lambda c: -c[4]) # largest first
| Format | VS Code Preview | GitHub | Browser | |--------|----------------|--------|---------| | GIF | ✅ Animates | ✅ | ✅ | | WebP | ⚠️ Static only | ✅ | ✅ | | MP4 | ❌ Broken | ⚠️ | ✅ |
GIF is the only universally supported animated format across VS Code preview, GitHub markdown, and browsers.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 18 counted toward the lift figure. The other 4 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +64 percentage points is the difference between those two pass rates over the 18 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.