screen-recording

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

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.
创建可展示功能或工作流的动画GIF演示——包含注释、可变计时和合理节奏。适用于PR描述、文档和发布说明。

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
当你需要以下操作时使用此技能:
  • 将多步骤UI交互录制为动画GIF
  • 创建展示前后行为对比的演示
  • 为文档或发布说明构建带注释的演练流程
  • 展示错误复现或修复的过程

Prerequisites

前置条件

bash
pip install playwright Pillow imageio numpy scipy mss -q
playwright install chromium
bash
pip install playwright Pillow imageio numpy scipy mss -q
playwright install chromium

Core Workflow

核心工作流

1. Capture frames

1. 捕获帧

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()
使用Playwright逐步执行交互并捕获每一帧:
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

2. 使用imageio组装GIF

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)
使用imageio而非PIL来生成GIF——PIL的GIF编码器会合并视觉相似的帧,这会破坏动画效果。
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

3. 可变帧计时

Uniform timing makes everything feel either too fast or too slow. Use variable durations:
PhaseDurationWhy
Fast action (typing, clicking)100msFeels natural, keeps energy
Pause after action600-800msLet the viewer process what happened
Hero/final message500ms+Main takeaway needs time to land
统一的计时会让所有内容要么太快要么太慢。请使用可变时长:
阶段时长原因
快速操作(打字、点击)100ms符合自然感受,保持节奏
操作后暂停600-800ms让观看者理解所发生的内容
核心/最终信息500ms+关键结论需要时间被接收

4. Annotate frames

4. 注释帧

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)
使用
image-annotations
技能为特定帧添加注释:
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

5. 注释淡入效果

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")
实现平滑的注释显示效果:
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%

2帧弹出效果(10fps):先50%透明度,再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.
faded_frames = [ apply_fade(base, annotations, 0.5), # 第1帧:半透明 apply_fade(base, annotations, 1.0), # 第2帧:完全不透明 ]

在10fps下,使用2个淡入帧(总计0.2秒)。在30fps下,使用3-4个帧。低帧率下缓动曲线效果不佳——简单的弹出效果更明快且可读性更强。

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.
对于非简单演示,注释逻辑会变得复杂。编写专用脚本(例如
annotate_gif.py
)并包含相关函数,而非使用内联代码。你需要反复调整计时和注释位置。

Testing Animations

测试动画

Always test in isolation first — don't rebuild the full demo to test a fade tweak:
python
undefined
始终先单独测试——不要为了测试淡入效果而重建整个演示:
python
undefined

Small test GIF: 10 bare frames → fade frames → 15 hold frames

小型测试GIF:10个空白帧 → 淡入帧 → 15个保持帧

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)
undefined
draw.text((10, height - 30), f"F{i}/{total} a={alpha:.0%} FADE", fill="white", font=small_font)
undefined

Desktop Screen Recording (mss)

桌面屏幕录制(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.
用于录制桌面应用、终端或浏览器外的任何内容。使用
mss
实现快速屏幕捕获。
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)
测试结果:3秒8fps → 24帧,约31KB。为保证合理的文件大小,保持fps ≤10。
注意:
PIL.save(save_all=True)
适用于简单录制,但会合并视觉相似的帧。对于带有淡入效果的注释GIF,请使用
imageio.v3.imwrite
替代。

Combining with window capture

结合窗口捕获

python
undefined
python
undefined

Find window rect, then record it as a GIF

查找窗口矩形,然后将其录制为GIF

Reuse find_window() from the ui-screenshots skill

复用ui-screenshots技能中的find_window()

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)
undefined
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)
undefined

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
通过编程方式查找帧之间的变化区域,以确定需要注释的内容:
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

格式兼容性

FormatVS Code PreviewGitHubBrowser
GIF✅ Animates
WebP⚠️ Static only
MP4❌ Broken⚠️
GIF is the only universally supported animated format across VS Code preview, GitHub markdown, and browsers.
格式VS Code预览GitHub浏览器
GIF✅ 可播放
WebP⚠️ 仅静态
MP4❌ 无法正常显示⚠️
GIF是唯一在VS Code预览、GitHub Markdown和浏览器中均支持的动画格式

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
  1. 输入 → 暂停 → 注释——快速操作期间不要显示注释。先暂停,再添加注释
  2. 核心信息使用最大字号——关键结论使用64pt+字号,细节使用38pt
  3. GIF调色板不会破坏渐变——20种不同的透明度层级可在256色调色板下保留
  4. 打字/交互最低10fps——更低帧率会显得卡顿
  5. 迭代式构建——先确定帧序列,再添加注释,最后调整计时

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)
  • GIF每帧最多支持256色——适合UI截图,但在照片内容上可能出现色带
  • 大型GIF(50+高分辨率帧)可能达到数MB——考虑裁剪到相关区域
  • GIF不支持音频——如需旁白演示请使用MP4(但会失去VS Code预览支持)