Skills · howto-filmstrips

How-to filmstrips (live UI capture on a canvas)

TerminalCopy
cnotes skills install howto-filmstrips

Raw markdown: /skills/howto-filmstrips/skill.md

Turn a real product flow into a left-to-right filmstrip of screenshots on a CreatorNotes canvas. Each frame shows the full UI with a clear click (or type) target: crimson highlight on the real element box + soft radial spotlight dimming the rest.

This is the pixel-accurate sibling of fat-marker-flows. Use fat-marker for proposed / Shape Up sketches; use this skill for “how do I do this in the live product?”


Design lock (do not invent a new look)

ElementSpec
Highlight colourClassy crimson #C43036 (outline, soft wash, CLICK/TYPE pill)
SpotlightSoft radial fade, max opacity ~40% (up to ~55% on dark forms), long fade (fade_mult ≈ 5.5)
Done frameGreen #2F9E6A outline + DONE pill; lighter spotlight (~30%)
CaptionsCanvas richtext under the image (same node), not Notes
TitlesMiddle dot · as separator; no em dashes
LayoutOne path = one horizontal row; multiple paths = stacked sections
LanguageMatch the product UI; prefer UK English in captions unless the product is US-only

Do not use orange bullseyes, yellow highlighters, cyan brackets, or hard rectangular box-shadow: 0 0 0 9999px spotlights. The hard box-shadow gets clipped by parent overflow and often never appears in the PNG.


When to use / not use

Use when: documenting a live click path, comparing entry points (Path A/B/C), workshop how-tos, JTBD storyboards from a real app.

Do not use when: the UI does not exist yet (use fat-marker-flows), or the user wants a polished Figma mock (use a design tool).


Prerequisites

  1. Browser automation available (Cursor browser MCP / CDP Runtime.evaluate + screenshot).
  2. cnotes authenticated with a writable workspace (cnotes auth status, cnotes workspace current).
  3. This skill folder installed (cnotes skills install howto-filmstrips) so scripts resolve under the agent skills dir.

Step 0 — Scope the paths

  1. Name the job (e.g. “Create a new contact”).
  2. List each entry path separately (Path A, Path B…). One path = one row.
  3. For each path, list 4–7 frames: each frame is one action (click or type) or a final Done state.
  4. Write captions before capture: N · Click … / N · Type … / N · Done · ….

Step 1 — Inject highlight helpers (before any click)

Read scripts/inject-highlight.js and evaluate its entire file contents in the page via CDP Runtime.evaluate (or equivalent). That installs:

  • window.__cnHowto.highlight(el, { label: 'CLICK'|'TYPE'|'DONE' })
  • window.__cnHowto.clear()
  • window.__cnHowto.findExact(text, opts) — visible, sized matches; prefer menu-row / control size

The injector uses a fixed full-viewport radial overlay (not element box-shadow), so the spotlight survives screenshot and parent clipping.

Calibrate visually: after first inject, highlight one control and screenshot. Check that the crimson outline hugs the element box (not an inner span), the CLICK pill sits just above it and is legible, and the spotlight is a soft radial fade centred on the control — never a hard-edged rectangle. Adjust opacity only if the product chrome is unusually light or dark.


Step 2 — Capture loop (per frame)

For each click/type frame:

  1. Find the target with __cnHowto.findExact (exact label text) or a tight selector. Prefer the largest visible control that matches (menu row over inner text span).
  2. Highlight with the right label (CLICK / TYPE).
  3. Screenshot the viewport → save as A1.png, A2.png, …
  4. Clear highlight.
  5. Perform the action (click / type), wait for UI settle.
  6. Repeat.

For the Done frame: land on the success state, highlight the result (e.g. contact name) with { label: 'DONE' }, screenshot, clear.

Accuracy rules

  • Capture highlights live from element getBoundingClientRect. Do not guess pixel rings on old PNGs.
  • If a frame teaches picking an option that is already selected, switch to another option first, then highlight the target and shoot — so the frame shows the switch, not a no-op.
  • Retina / DPR: screenshots are often 2× CSS pixels. The injector uses CSS pixels for the overlay; if you fall back to soft-spotlight.py, pass --dpr (usually 2).

Fallback if the in-page spotlight is weak

On very dark UIs the radial overlay can look faint. After capture, run:

python3 "<skillDir>/scripts/soft-spotlight.py" \
  --in A3.png --out A3.png \
  --cx 276 --cy 219 --dpr 2 \
  --opacity 0.55

--cx/--cy are CSS-pixel centres from getBoundingClientRect (the script multiplies by --dpr).


Step 3 — Upload and place on the canvas

WS=<workspaceId>
CANVAS=<canvasId>
OUT=/tmp/howto-filmstrip
mkdir -p "$OUT"

cnotes operations begin -w "$WS" --prompt "How-to filmstrip: <job>" --json >/dev/null

# Upload in frame order; collect storageIds
> "$OUT/ids.txt"
for f in A1 A2 A3 A4 A5 A6; do
  sid=$(cnotes files upload "$OUT/$f.png" -w "$WS" --json \
    | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('storageId') or d.get('id') or '')")
  echo "$sid" >> "$OUT/ids.txt"
done

Build a place spec: header + one section per path, each with a grid of richtext cells (image markdown + caption). Prefer columns: N matching frame count (e.g. 6). Use size: "medium" for frames.

Image URL form inside richtext:

![Short alt](/api/images/convex/<storageId>)

1 · Click Create new +

Clear existing storyboard nodes if rebuilding the same canvas:

cnotes canvas get "$CANVAS" -w "$WS" --json | python3 -c "
import sys,json
d=json.load(sys.stdin)
ids=[n['id'] for n in (d.get('allNodes') or [])]
for s in d.get('sectionNodes') or []:
  if s['id'] not in ids: ids.append(s['id'])
open('/tmp/howto-remove.json','w').write(json.dumps(ids))
"
cnotes canvas agent-run wrap --canvas "$CANVAS" --prompt "Clear before how-to rebuild" -- \
  cnotes canvas bulk-remove "$CANVAS" --nodes "$(cat /tmp/howto-remove.json)" -w "$WS" --json

Place:

cnotes canvas agent-run wrap --canvas "$CANVAS" --prompt "Place how-to filmstrip" -- \
  cnotes canvas place "$CANVAS" --spec "$OUT/unified.json" -w "$WS" --json
cnotes operations end -w "$WS" --json

Multi-path layout sketch

{
  "root": {
    "kind": "stack",
    "axis": "vertical",
    "gap": "spacious",
    "align": "start",
    "items": [
      { "kind": "item", "type": "richtext", "size": "large",
        "content": "# How to …\n\nSame job, several entry points. Read each row left to right." },
      {
        "kind": "section",
        "name": "Path A · …",
        "child": {
          "kind": "stack",
          "axis": "vertical",
          "gap": "tight",
          "align": "start",
          "items": [
            { "kind": "item", "type": "richtext", "size": "small",
              "content": "**Path A** · …" },
            { "kind": "grid", "columns": 6, "gap": "medium", "items": [/* frames */] }
          ]
        }
      }
    ]
  }
}

Gotchas (learned the hard way)

  • box-shadow: 9999px spotlights get clipped and vanish from screenshots. Always use the fixed radial overlay from inject-highlight.js.
  • Wrong element: loose regexes match sibling labels (/save/i also matches “Save and close”). Prefer exact text + min width/height + prefer larger hit targets.
  • CLICK pill vs outline mismatch: place the pill from the highlighted element’s box after layout; re-highlight after fixing helpers if the first shot drifted.
  • Dark forms: bump spotlight opacity toward 0.55 so the fade reads.
  • DPR: post-process centres must be CSS × devicePixelRatio.
  • place appends below existing content unless you bulk-remove first when rebuilding.
  • Verify images by node count / positions, not by grepping ![ in stored content (TipTap converts markdown images).
  • Do not commit secrets from the product under test; use throwaway trial data in screenshots.

Pairing with fat-marker-flows

SkillFidelitySource
fat-marker-flowsRough Shape Up sketchesImage model
howto-filmstripsReal product UIBrowser capture + CSS

Same canvas can hold a fat-marker “proposed” row and a how-to “as shipped” row — keep the visual language of each skill intact so readers never confuse sketch with screenshot.