Skip to content

Commit 598360c

Browse files
nohwndCopilot
andcommitted
Add visual-pr plugin: screenshot capture, annotation, PR embedding, and screen recording
Four skills that teach Copilot to capture UI screenshots (Playwright + PIL), annotate them with algorithmic label placement, embed before/after images in PR descriptions, and record animated GIF demos. Includes demo images showing the annotation engine on GitHub Issues. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1d75827 commit 598360c

10 files changed

Lines changed: 878 additions & 0 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "visual-pr",
3+
"description": "Capture, annotate, and embed screenshots and animated GIF demos in pull request descriptions. Includes Playwright-based UI capture, PIL image annotations, PR embedding workflows for GitHub and Azure DevOps, and screen recording with variable timing.",
4+
"version": "1.0.0",
5+
"keywords": [
6+
"screenshots",
7+
"pull-request",
8+
"before-after",
9+
"annotations",
10+
"playwright",
11+
"gif",
12+
"screen-recording",
13+
"visual"
14+
],
15+
"author": { "name": "Awesome Copilot Community" },
16+
"repository": "https://github.com/github/awesome-copilot",
17+
"license": "MIT",
18+
"skills": [
19+
"./skills/image-annotations/",
20+
"./skills/pr-screenshots/",
21+
"./skills/screen-recording/",
22+
"./skills/ui-screenshots/"
23+
]
24+
}

plugins/visual-pr/README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Visual PR Plugin
2+
3+
When you change how something looks — a layout, a chart, a form — the PR description should show the change. Not describe it in words. Show it. A before/after screenshot tells your reviewer exactly what happened, and they don't have to check out the branch to see it.
4+
5+
This plugin teaches Copilot to capture screenshots of your web app (or any UI), annotate them with callouts, and embed them in the PR description. Once you get used to having up-to-date screenshots on every visual change, going back to text-only PRs feels like reviewing code with your eyes closed.
6+
7+
## Demo 🎬
8+
9+
It is 2009. You just added the feature to show labels on issues. You better include screenshots because this is huge.
10+
11+
Here is how such PR description would look like with our plugin:
12+
13+
> **Before** — issues list without labels:
14+
>
15+
> <img src="demo/before.png" width="600" alt="Issues list without labels">
16+
>
17+
> **After** — labels are shown directly on each issue row:
18+
>
19+
> <img src="demo/after.png" width="600" alt="Issues list with labels annotated">
20+
21+
---
22+
23+
## It's not just for PRs
24+
25+
The same annotation engine works for any screenshot. Here's a single prompt:
26+
27+
> *"Go to the GitHub issues page of github/awesome-copilot. Screenshot the issues list and annotate: any issue icon, ready-for-review label, repo name, pinned issues, my avatar, most commented issue, New Issue button."*
28+
29+
![Multipurpose annotation — 6 callouts from one prompt](demo/multi.png)
30+
31+
The agent captured the page, identified 6 of 7 requested elements, and annotated them all. It correctly reported that the 7th (user avatar) isn't visible because the page was captured without authentication — no hallucinated annotations.
32+
33+
### Debug mode
34+
35+
Every annotation run can produce a debug heatmap showing how the algorithm chose label placements — contrast scoring, exclusion zones, and candidate rankings:
36+
37+
![Debug heatmap showing placement algorithm](demo/debug.png)
38+
39+
---
40+
41+
## Skills Included
42+
43+
| Skill | What it does |
44+
|-------|-------------|
45+
| [ui-screenshots](../../skills/ui-screenshots/SKILL.md) | Capture web UI screenshots with Playwright + PIL crop workflow |
46+
| [image-annotations](../../skills/image-annotations/SKILL.md) | Annotate any image with callout rectangles, arrows, labels, and color-coded highlights |
47+
| [pr-screenshots](../../skills/pr-screenshots/SKILL.md) | Embed before/after images in PR descriptions (GitHub + Azure DevOps) |
48+
| [screen-recording](../../skills/screen-recording/SKILL.md) | Create annotated animated GIF demos with variable timing |
49+
50+
## Use Cases
51+
52+
- **Visual PRs** — show reviewers what changed without checking out the branch
53+
- **Release notes** — embed GIF demos of new features
54+
- **Bug reports** — before/after screenshots proving the fix
55+
- **Documentation** — annotated screenshots with callouts highlighting key areas
56+
57+
## Requirements
58+
59+
This plugin needs a model that can view images — the workflow relies on looking at screenshots to find crop coordinates and verify annotations. The demo images in this README were generated with **Claude Opus 4.6**.

plugins/visual-pr/demo/after.png

53.2 KB
Loading

plugins/visual-pr/demo/before.png

39.1 KB
Loading

plugins/visual-pr/demo/debug.png

143 KB
Loading

plugins/visual-pr/demo/multi.png

127 KB
Loading

skills/image-annotations/SKILL.md

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
---
2+
name: image-annotations
3+
description: 'Annotate screenshots, diagrams, and images with callout rectangles, arrows, labels, and color-coded highlights using PIL. Includes rules for animated GIF annotations with timing and pacing.'
4+
---
5+
6+
# Image Annotations
7+
8+
Add visual callouts to any image — screenshots, diagrams, architecture docs, demo frames — using PIL/Pillow. Highlights what changed or what to look at, so reviewers don't have to guess.
9+
10+
## When to Use This Skill
11+
12+
Use this skill when you need to:
13+
14+
- Highlight a specific area in a screenshot for a PR description
15+
- Annotate before/after images to show what changed
16+
- Add labels and callouts to diagrams or architecture images
17+
- Create annotated frames for animated GIF demos
18+
19+
## Prerequisites
20+
21+
```bash
22+
pip install Pillow -q
23+
```
24+
25+
## Color Rules
26+
27+
- **Red (`#E63946`)** — only for "bad" / "removed" things (e.g., circling a bug being fixed)
28+
- **Yellowish-orange (`#FF9F1C`)** — for neutral highlights ("look here", "new feature", etc.)
29+
- Never use red just because it's eye-catching — red = bad/removed
30+
31+
## Font
32+
33+
- Use **Ink Free** (`C:/Windows/Fonts/Inkfree.ttf`) for a handwritten look on Windows
34+
- On Linux/macOS, fall back to `ImageFont.load_default()`
35+
- Size **36** for annotations on ~1400px-wide images
36+
- `stroke_width=1` with `stroke_fill=<same color as fill>` — gives body without being too thick
37+
- Do NOT use white stroke — looks like a bad glow effect
38+
39+
## Shapes
40+
41+
- Prefer **rounded rectangles** over circles/ellipses — less pixelation at edges
42+
- `draw.rounded_rectangle([x1, y1, x2, y2], radius=14, outline=color, width=5)`
43+
- **Padding 18px** around the target content
44+
45+
## Reference Snippet
46+
47+
```python
48+
from PIL import Image, ImageDraw, ImageFont
49+
50+
# Setup
51+
font = ImageFont.truetype('C:/Windows/Fonts/Inkfree.ttf', 36) # or load_default()
52+
color = '#FF9F1C' # orange for highlights
53+
stroke = 5
54+
pad = 18
55+
56+
img = Image.open('screenshot.png')
57+
draw = ImageDraw.Draw(img)
58+
59+
# Rounded rect with padding
60+
draw.rounded_rectangle(
61+
[x1 - pad, y1 - pad, x2 + pad, y2 + pad],
62+
radius=14, outline=color, width=stroke
63+
)
64+
65+
# Leader line (same thickness as rect)
66+
draw.line([x2 + pad, cy, x2 + pad + 40, cy - 30], fill=color, width=stroke)
67+
68+
# Label — same-color stroke for body, NO white stroke
69+
draw.text(
70+
(x2 + pad + 45, cy - 60), 'label text',
71+
fill=color, font=font, stroke_width=1, stroke_fill=color
72+
)
73+
74+
img.save('annotated.png')
75+
```
76+
77+
## Algorithmic Annotation
78+
79+
For images with multiple elements to annotate, use an algorithmic approach that automatically places labels without overlapping.
80+
81+
### Quick start
82+
83+
```python
84+
from annotate import annotate_image
85+
86+
result = annotate_image(
87+
'screenshot.png',
88+
[
89+
{'elem': (560, 275, 635, 390), 'label': 'arrow on card', 'draw_box': True},
90+
{'elem': (105, 453, 236, 470), 'label': 'direction text'},
91+
],
92+
debug=True,
93+
)
94+
result.save('annotated.png')
95+
```
96+
97+
- `elem`: `(x1, y1, x2, y2)` tight bounding box — must be exact pixel coordinates
98+
- `label`: text label (supports `\n` for multi-line)
99+
- `draw_box`: if `True`, draws a rounded rectangle around the element. If `False` (default), draws a V-arrowhead pointing at the element
100+
- `debug`: shows targeting rectangles and candidate heatmap for placement validation
101+
102+
### Coordinate grid helper
103+
104+
**Always use `grid_image()` before annotating an unfamiliar image.** Scaled-down previews display images smaller than actual pixel dimensions — the error compounds as you move away from (0,0).
105+
106+
```python
107+
from annotate import grid_image
108+
109+
grid = grid_image('screenshot.png', step=100)
110+
grid.save('grid.png')
111+
```
112+
113+
Then verify with small crops:
114+
115+
```python
116+
from PIL import Image
117+
img = Image.open('screenshot.png')
118+
crop = img.crop((x1 - 20, y1 - 20, x2 + 20, y2 + 20))
119+
crop.save('verify.png')
120+
```
121+
122+
### Algorithm overview
123+
124+
1. **Ring search**: candidates between MIN_ARROW (25px) and MAX_ARROW (120px) from element edge
125+
2. **Contrast scoring**: prefers placements where label text is readable — `abs(avg_brightness - 147) - std * 0.3 - dist * 0.02`
126+
3. **Joint resolution**: candidates computed independently, placed greedily (best score first)
127+
4. **Hard blocks**: labels cannot overlap any other annotation's element or breathing box
128+
5. **Proximity penalty**: labels within 40px of other placed boxes get a score penalty
129+
6. **Arrow crossing penalty**: -50 for arrows crossing already-placed arrows
130+
131+
### Debug mode colors
132+
133+
| Color | Meaning |
134+
|-------|---------|
135+
| Cyan | Target element box (elem + padding) |
136+
| Gray | Exclusion zone (MIN_ARROW buffer) |
137+
| Red→Green | Candidate heatmap (red=bad, green=good) |
138+
| Magenta | Chosen label position |
139+
| Orange | Final rendered annotation |
140+
141+
### Arrow styles
142+
143+
- **`draw_box=True`**: rounded rectangle + straight line to label, no arrowhead
144+
- **`draw_box=False`**: V-shaped arrowhead with rounded line caps
145+
146+
## Image Diffing
147+
148+
Find what changed between two screenshots programmatically. Use as a safety net for subtle changes — when the difference is obvious, annotate directly instead.
149+
150+
```python
151+
from annotate import diff_images
152+
153+
clusters, debug_img = diff_images(
154+
'before.png', 'after.png',
155+
threshold=30, # pixel difference floor (0-255)
156+
min_pixels=300, # ignore tiny noise clusters
157+
dilate=5, # merge nearby changed pixels
158+
debug=True, # render heatmap overlay
159+
)
160+
161+
# clusters = [(x1, y1, x2, y2, pixel_count), ...] sorted largest-first
162+
if debug_img:
163+
debug_img.save('diff-debug.png')
164+
165+
# Feed clusters into annotate_image:
166+
annotations = [
167+
{'elem': (x1, y1, x2, y2), 'label': f'Change #{i+1}', 'draw_box': True}
168+
for i, (x1, y1, x2, y2, _) in enumerate(clusters[:3])
169+
]
170+
```
171+
172+
**Debug heatmap colors:** Blue = small difference, Yellow = medium, Red = large, Cyan boxes = cluster bounding boxes.
173+
174+
**When to use:** subtle opacity changes, dashed lines, minor color shifts, anti-aliasing differences.
175+
**When NOT to use:** any change you can see by eye — annotate directly for better labels.
176+
177+
## Animated GIF Annotations
178+
179+
Different from static images — animations have timing, transitions, and competing visual motion.
180+
181+
### Element highlighting
182+
183+
1. **Rects for big areas, arrows for small elements** — 500x300px area = rect, 200x25px element = arrow
184+
2. **Labels go RIGHT NEXT to what they describe** — short arrow (30-80px), label adjacent. Viewer's eye shouldn't travel more than ~100px
185+
3. **Arrow must not cross its own label** — pick the edge closest to the target
186+
4. **No bottom bar / subtitle approach** — eyes jump between content and bar. Contextual placement only
187+
5. **Hero message gets a bigger font** — main takeaway 64pt+, detail annotations 38pt
188+
189+
### Timing and pacing
190+
191+
6. **Fade: 2-frame pop-in at 10fps** — 50% → 100% opacity (0.2s total). Easing curves look bad at low FPS
192+
7. **Type → pause → annotate** — during fast action, show NO annotation. Pause, then add it
193+
8. **Variable frame duration** — fast during action (100ms), slow during pauses (600-800ms), long hold for hero (500ms)
194+
9. **Higher FPS for smooth motion** — 10fps minimum for typing/interaction
195+
196+
### Pop-in fade implementation
197+
198+
```python
199+
# 2-frame pop-in at 10fps
200+
FADE_ALPHAS = [0.50, 1.00]
201+
202+
for frame_idx in range(total_frames):
203+
if annotation_just_changed and local_idx < len(FADE_ALPHAS):
204+
alpha = FADE_ALPHAS[local_idx]
205+
else:
206+
alpha = 1.0
207+
# Apply alpha to annotation elements:
208+
# - pill background: fill=(r, g, b, int(base_alpha * alpha))
209+
# - text: fill=(*color, int(255 * alpha))
210+
# - rect outline: outline=(*color, int(255 * alpha))
211+
```
212+
213+
## Guidelines
214+
215+
1. **All elements same thickness** — rect `width`, line `width`, and visual text weight should feel consistent (~5px)
216+
2. Place labels **close to the rect** — short leader line (25-35px)
217+
3. Labels can overlap content — the stroke gives enough contrast
218+
4. **Show locally first** — verify before uploading to a PR
219+
5. **Take screenshots at native 1x, control display size in HTML** — use `<img width="300">` in markdown, never resize with PIL (creates artifacts)
220+
6. **Always check `Image.open(path).size` first** — HiDPI screenshots are larger than they appear (150% scaling = 1.5x CSS pixel dimensions)
221+
7. **Short labels work better** — wide labels have fewer valid placements. Use 1-3 words when possible
222+
8. **Verify with debug=True** — always check the first annotation of a new image with debug mode
223+
224+
## Limitations
225+
226+
- Ink Free font is Windows-only; other platforms need a fallback font
227+
- PIL text rendering is basic — no rich text, no markdown
228+
- Animated GIF annotations require frame-by-frame processing which can be slow for long recordings
229+
- Algorithmic placement works best with 2-6 annotations; more than that may produce crowded results

0 commit comments

Comments
 (0)