screen-recording
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseScreen 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 chromiumbash
pip install playwright Pillow imageio numpy scipy mss -q
playwright install chromiumCore 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:
| 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 |
统一的计时会让所有内容要么太快要么太慢。请使用可变时长:
| 阶段 | 时长 | 原因 |
|---|---|---|
| 快速操作(打字、点击) | 100ms | 符合自然感受,保持节奏 |
| 操作后暂停 | 600-800ms | 让观看者理解所发生的内容 |
| 核心/最终信息 | 500ms+ | 关键结论需要时间被接收 |
4. Annotate frames
4. 注释帧
Apply annotations to specific frames using the skill:
image-annotationspython
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-annotationspython
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., ) with functions instead of inline code. You'll iterate on timing and placement.
annotate_gif.py对于非简单演示,注释逻辑会变得复杂。编写专用脚本(例如)并包含相关函数,而非使用内联代码。你需要反复调整计时和注释位置。
annotate_gif.pyTesting Animations
测试动画
Always test in isolation first — don't rebuild the full demo to test a fade tweak:
python
undefined始终先单独测试——不要为了测试淡入效果而重建整个演示:
python
undefinedSmall 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)
undefineddraw.text((10, height - 30), f"F{i}/{total} a={alpha:.0%} FADE",
fill="white", font=small_font)
undefinedDesktop Screen Recording (mss)
桌面屏幕录制(mss)
For recording desktop apps, terminals, or anything outside a browser. Uses for fast screen capture.
msspython
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: works for simple recordings but merges visually similar frames. For annotated GIFs with fade effects, use instead.
PIL.save(save_all=True)imageio.v3.imwrite用于录制桌面应用、终端或浏览器外的任何内容。使用实现快速屏幕捕获。
msspython
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。
注意: 适用于简单录制,但会合并视觉相似的帧。对于带有淡入效果的注释GIF,请使用替代。
PIL.save(save_all=True)imageio.v3.imwriteCombining with window capture
结合窗口捕获
python
undefinedpython
undefinedFind 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)
undefinedimport 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)
undefinedDiff-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 firstFormat 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.
| 格式 | VS Code预览 | GitHub | 浏览器 |
|---|---|---|---|
| GIF | ✅ 可播放 | ✅ | ✅ |
| WebP | ⚠️ 仅静态 | ✅ | ✅ |
| MP4 | ❌ 无法正常显示 | ⚠️ | ✅ |
GIF是唯一在VS Code预览、GitHub Markdown和浏览器中均支持的动画格式。
Guidelines
指南
- Type → pause → annotate — during fast action, show NO annotation. Pause first, then annotate
- Hero message gets the biggest font — 64pt+ for the main takeaway, 38pt for details
- GIF palette does NOT kill gradients — 20 distinct alpha steps survive 256-color palette
- 10fps minimum for typing/interaction — lower looks stuttery
- Build iteratively — get the frame sequence right first, add annotations second, tune timing last
- 输入 → 暂停 → 注释——快速操作期间不要显示注释。先暂停,再添加注释
- 核心信息使用最大字号——关键结论使用64pt+字号,细节使用38pt
- GIF调色板不会破坏渐变——20种不同的透明度层级可在256色调色板下保留
- 打字/交互最低10fps——更低帧率会显得卡顿
- 迭代式构建——先确定帧序列,再添加注释,最后调整计时
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预览支持)