Enregistrement d'écran
Créez des démos animées en GIF montrant une fonctionnalité ou un workflow en action — avec annotations, timing variable et cadence appropriée. Utile pour les descriptions de PR, la documentation et les notes de version.
Quand utiliser cette compétence
Utilisez cette compétence quand vous avez besoin de :
- Enregistrer une interaction UI multi-étapes en GIF animé
- Créer une démo montrant un comportement avant/après
- Construire des walkthroughs annotés pour la documentation ou les notes de version
- Montrer la reproduction ou la correction d'un bug en action
Prérequis
pip install playwright Pillow imageio numpy scipy mss -q
playwright install chromium
Workflow principal
1. Capturer les frames
Utilisez Playwright pour progresser dans l'interaction et capturer chaque frame :
from playwright.async_api import async_playwright
async def record_frames(url, steps, width=1400, height=900):
"""
steps: liste de dicts avec 'action' (callable async prenant page)
et 'name' (nom du fichier du frame)
"""
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. Assembler le GIF avec imageio
Utilisez imageio, pas PIL, pour l'écriture de GIF — l'encodeur GIF de PIL fusionne les frames visuellement similaires, ce qui tue les animations.
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. Timing variable des frames
Un timing uniforme rend tout soit trop rapide soit trop lent. Utilisez des durées variables :
| Phase | Durée | Pourquoi |
|---|---|---|
| Action rapide (saisie, clic) | 100ms | Semble naturel, maintient l'énergie |
| Pause après action | 600-800ms | Laisser au spectateur le temps de traiter ce qui s'est passé |
| Message principal/final | 500ms+ | Le point clé a besoin de temps pour s'enregistrer |
4. Annoter les frames
Appliquez des annotations à des frames spécifiques en utilisant la compétence image-annotations :
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:
# Appliquer annotation (rect, flèche, label, etc.)
pass
img.save(out_path)
5. Annotations avec fondu
Pour une apparition fluide des annotations :
def apply_fade(base_frame, annotation_layer, alpha):
"""Fusionner annotation sur frame à alpha donné (0.0 à 1.0)"""
blended = Image.blend(
base_frame.convert("RGBA"),
annotation_layer.convert("RGBA"),
alpha
)
return blended.convert("RGB")
# Pop-in 2-frame à 10fps : 50% puis 100%
faded_frames = [
apply_fade(base, annotations, 0.5), # frame 1: opacité 50%
apply_fade(base, annotations, 1.0), # frame 2: opacité 100%
]
À 10fps, utilisez 2 frames de fondu (0,2s au total). À 30fps, utilisez 3-4 frames. Les courbes d'easing ont mauvaise allure en basse résolution FPS — un simple pop-in est plus percutant et lisible.
Construire comme un script
La logique d'annotation devient complexe au-delà des démos triviales. Écrivez un script dédié (par ex. annotate_gif.py) avec des fonctions au lieu du code inline. Vous itérerez sur le timing et le positionnement.
Tester les animations
Toujours tester isolément d'abord — ne reconstruisez pas la démo complète pour tester un ajustement de fondu :
# Petit GIF de test : 10 frames bruts → frames de fondu → 15 frames de maintien
# Ajouter un overlay compteur de frames pour le débogage :
draw.text((10, height - 30), f"F{i}/{total} a={alpha:.0%} FADE",
fill="white", font=small_font)
Enregistrement d'écran de bureau (mss)
Pour enregistrer des apps de bureau, des terminaux ou n'importe quoi en dehors d'un navigateur. Utilise mss pour une capture d'écran rapide.
import mss
from PIL import Image
import time
def record_gif(output_path, region=None, duration=5, fps=8):
"""Enregistrer région d'écran en GIF. region = {left, top, width, height} ou None pour écran complet."""
with mss.mss() as sct:
if region is None:
region = sct.monitors[1] # moniteur principal
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)
Testé : 3s à 8fps → 24 frames, ~31KB. Gardez fps ≤ 10 pour des tailles de fichier raisonnables.
Note : PIL.save(save_all=True) fonctionne pour les enregistrements simples mais fusionne les frames visuellement similaires. Pour les GIFs annotés avec effets de fondu, utilisez imageio.v3.imwrite à la place.
Combiner avec capture de fenêtre
# Trouver rect de fenêtre, puis l'enregistrer comme GIF
# Réutiliser find_window() de la compétence ui-screenshots
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)
Détection de clusters basée sur les différences
Trouvez par programme les régions modifiées entre frames pour décider quoi annoter :
import numpy as np
from scipy import ndimage
def find_changed_clusters(frame_a, frame_b, threshold=30, min_pixels=300, dilate=5):
"""Trouver les boîtes englobantes des régions modifiées entre deux 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]) # plus grand d'abord
Compatibilité des formats
| Format | Aperçu VS Code | GitHub | Navigateur |
|---|---|---|---|
| GIF | ✅ Anime | ✅ | ✅ |
| WebP | ⚠️ Statique uniquement | ✅ | ✅ |
| MP4 | ❌ Cassé | ⚠️ | ✅ |
GIF est le seul format animé universellement supporté sur l'aperçu VS Code, le markdown GitHub et les navigateurs.
Directives
- Saisie → pause → annotation — durant l'action rapide, NE PAS afficher d'annotation. D'abord faire la pause, puis annoter
- Le message principal a la plus grosse police — 64pt+ pour le point clé, 38pt pour les détails
- La palette GIF ne tue PAS les dégradés — 20 étapes alpha distinctes survivent à une palette de 256 couleurs
- Minimum 10fps pour la saisie/interaction — plus bas semble saccadé
- Construire itérativement — corriger d'abord la séquence de frames, ajouter les annotations ensuite, affiner le timing en dernier
Limitations
- GIF est limité à 256 couleurs par frame — acceptable pour les captures d'écran UI, peut montrer de la posterisation sur contenu photographique
- Les grands GIFs (50+ frames en haute résolution) peuvent faire plusieurs MB — envisagez de recadrer la zone pertinente
- Pas de support audio en GIF — utilisez MP4 pour les démos narratives (mais perdez le support d'aperçu VS Code)