screen-recording
$
npx mdskill add github/awesome-copilot/screen-recordingGenerates annotated animated GIFs and screen recordings for documentation and PRs
- Solves the need for visual demonstrations of UI workflows and changes
- Uses Playwright, imageio, and Pillow for frame capture and GIF creation
- Follows step-by-step actions with timing and per-frame annotations
- Exports recordings as GIFs or video files for documentation or PRs
SKILL.md
.github/skills/screen-recordingView on GitHub ↗
---
name: screen-recording
description: '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.'
---
# Screen Recording
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.
## When to Use This Skill
Use this skill when you need to:
- Record a multi-step UI interaction as an animated GIF
- Create a demo showing before/after behavior
- Build annotated walkthroughs for documentation or release notes
- Show a bug reproduction or fix in action
## Prerequisites
```bash
pip install playwright Pillow imageio numpy scipy mss -q
playwright install chromium
```
## Core Workflow
### 1. Capture frames
Use Playwright to step through the interaction and capture each frame:
```python
from 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()
```
### 2. Assemble GIF with imageio
**Use imageio, not PIL, for GIF writing** — PIL's GIF encoder merges visually similar frames, which kills animations.
```python
import 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)
```
### 3. Variable frame timing
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 |
### 4. Annotate frames
Apply annotations to specific frames using the `image-annotations` skill:
```python
from 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)
```
### 5. Fade-in annotations
For smooth annotation appearance:
```python
def 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.
## Build as a Script
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.
## Testing Animations
**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)
```
## Desktop Screen Recording (mss)
For recording desktop apps, terminals, or anything outside a browser. Uses `mss` for fast screen capture.
```python
import 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.
### Combining with window capture
```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)
```
## Diff-Based Cluster Detection
Programmatically find changed regions between frames to decide what to annotate:
```python
import 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 Compatibility
| 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.
## Guidelines
1. **Type → pause → annotate** — during fast action, show NO annotation. Pause first, then annotate
2. **Hero message gets the biggest font** — 64pt+ for the main takeaway, 38pt for details
3. **GIF palette does NOT kill gradients** — 20 distinct alpha steps survive 256-color palette
4. **10fps minimum** for typing/interaction — lower looks stuttery
5. **Build iteratively** — get the frame sequence right first, add annotations second, tune timing last
## Limitations
- GIF is limited to 256 colors per frame — fine for UI screenshots, may show banding on photographic content
- Large GIFs (50+ frames at high resolution) can be several MB — consider cropping to the relevant area
- No audio support in GIF — use MP4 for narrated demos (but lose VS Code preview support)
More from github/awesome-copilot
- acquire-codebase-knowledgeUse this skill when the user explicitly asks to map, document, or onboard into an existing codebase. Trigger for prompts like "map this codebase", "document this architecture", "onboard me to this repo", or "create codebase docs". Do not trigger for routine feature implementation, bug fixes, or narrow code edits unless the user asks for repository-level discovery.
- acreadiness-assessRun the AgentRC readiness assessment on the current repository and produce a static HTML dashboard at reports/index.html. Wraps `npx github:microsoft/agentrc readiness` and hands off rendering to the @ai-readiness-reporter custom agent. Supports policies (--policy) for org-specific scoring. Use when asked to assess, audit, or score the AI readiness of a repo.
- acreadiness-generate-instructionsGenerate tailored AI agent instruction files via AgentRC instructions command. Produces .github/copilot-instructions.md (default, recommended for Copilot in VS Code) plus optional per-area .instructions.md files with applyTo globs for monorepos. Use after running /acreadiness-assess to close gaps in the AI Tooling pillar.
- acreadiness-policyHelp the user pick, write, or apply an AgentRC policy. Policies customise readiness scoring by disabling irrelevant checks, overriding impact/level, setting pass-rate thresholds, or chaining org baselines with team overrides. Use when the user asks about strict mode, AI-only scoring, custom weights, CI gating, or wants org-wide standardisation.
- add-educational-comments'Add educational comments to the file specified, or prompt asking for file to comment if one is not provided.'
- adobe-illustrator-scriptingWrite, debug, and optimize Adobe Illustrator automation scripts using ExtendScript (JavaScript/JSX). Use when creating or modifying scripts that manipulate documents, layers, paths, text frames, colors, symbols, artboards, or any Illustrator DOM objects. Covers the complete JavaScript object model, coordinate system, measurement units, export workflows, and scripting best practices.
- agent-governance|
- agent-owasp-compliance|
- agent-supply-chain|
- agentic-eval|