Give it a real product screenshot and the parts whose reveal will help the viewer
understand the screen. Get exact image layers that enter on the chosen cues. Capture
the source through guide-pull-product-highlights when needed. The spec and commands
are hn4a-compositor's; choose an energetic or calm camera for the intended pace
(hn4a-compositor-fast-ad-camera, hn4a-compositor-calm-explainer-camera). Never
animate UI lettering with a video model: it warps the text.
When this leaf fits
Use a staged reveal when it makes a relationship, progression or target easier to follow. Prominence alone does not require motion: a full-frame screen may need to hold while the viewer reads or compares it. A decorative reveal can obscure the very evidence the shot is meant to show.
This leaf cuts existing pixels; it does not implement new UI behaviour. If data must
change, use hn4a-compositor-rebuild-product-screen. If an actual operation is the
evidence, use its recording. guide-show-a-product-on-screen owns that choice.
The cut-up treatment is local and took under an hour a screen in the worked use.
Inputs
-
The screenshot at full resolution, clean (no personal data, no notifications).
-
The parts, measured in its own px: read each box off a full-size gridded copy (
python-contact-sheet):exec python scripts/contact_sheet.py video work/dash_grid.png source/screens/dash.png --cols 1 --width 0 --grid 50Typical parts: each KPI card, the chart's plot area, the table's rows (one box, split into strips), a button. Parts don't overlap.
-
The cues: when the camera moves to each part (from the camera leaf's moves).
Run it
Write references/ui_layers.md's script into the job's scripts/ folder as
scripts/ui_layers.py and the
parts into ui/dash_parts.json:
{
"kpi_open": {"box": [258, 108, 404, 164], "fx": "pop", "bg": "outside"},
"kpi_done": {"box": [698, 108, 404, 164], "fx": "pop", "bg": "outside"},
"chart": {"box": [290, 360, 590, 545], "fx": "wipe"},
"rows": {"box": [950, 372, 590, 540], "split": 6, "fx": "rise"}
}
exec python scripts/ui_layers.py cut source/screens/dash.png ui/dash_parts.json ui/dash
exec python scripts/ui_layers.py test ui/dash ui/dash_test.json
exec hn4a-compositor still ui/dash_test.json ui/stills/dash_mid.png --at 2.6
bg: what the plate shows where the part was.inside(the default) paints it the commonest colour just inside the box: the card behind a chart or rows.outsidepaints it the colour around it: a whole card lifts off the page. Or a hex.split: N strips top to bottom, built 0.06 s apart: rows cascade.fx:rise,pop(cards, buttons),wipe(a chart drawing left to right),fade.
Then in the cut's build script (in scripts/ too, so the import works), inside the
camera group:
from ui_layers import layers, load, poi
dash = load("ui/dash")
SCREEN = (160, 40, 1600, 1000) # where the screen sits, in group px
P = {n: poi(dash, n, SCREEN) for n in ["kpi_done", "chart", "rows"]}
cues = {"kpi_done": 1.7, "chart": 4.2, "rows": 6.7} # each part builds as the camera starts towards it
spec_layers += layers(dash, "cam", cues, SCREEN, prefix="../")
A part with no cue is on from the start, finished. prefix makes the image paths
relative to the spec's own folder.
Choreograph it with the camera
- When the camera introduces a part, cue its build at the start of the camera's move to it, so the build finishes as the camera lands. Built before the camera gets there, the viewer missed it; built after, the camera waits on it.
- The pace follows the brief and reading load: rhythmic pushes when energy and
emphasis help (
hn4a-compositor-fast-ad-camera), calmer reframing and longer holds when the viewer needs to inspect (hn4a-compositor-calm-explainer-camera). A brand ad can be calm; an explainer can have an energetic opening. - The opening frame is the plate, the empty cards, so the first beat has something to build. Show the whole screen once before pushing in, so the viewer knows where they are.
- Only what the narration names builds. The nav, headers and chrome stay in the plate.
What you get
ui/dash/plate.png, one PNG per part, ui/dash/parts.json (each part's box and build),
and from layers() an image layer per part, boxed in group px, plus poi() for the
camera. With every part built, the frame is the screenshot to the pixel.
Tips and traps
- Measure on the full-size grid, never a thumbnail: a box a few px off leaves a sliver of the part on the plate, or cuts a part's edge.
- A part that sits on a gradient or a photo can't be painted out with one colour: rebuild that screen, or leave the part in the plate.
- A card's shadow belongs to the part: draw the box to include it, with
bgoutside. - A screenshot holds only its own pixels. Shown at 1600 px wide and pushed to 1.6×, a 1600 px screenshot is upscaled 1.6× and goes soft. Capture at 2× (a Retina display does) for a screen the camera pushes into, or rebuild it.
Check it
test's spec, stills before, during and after the builds: the plate has no ghost of any part, each part lands exactly where it was.- With every part built, a still against the screenshot at the same box: no difference.
- Stills at each camera landing (the camera leaf's Check it).


