Hypernatural

TechniqueBy HypernaturalVersion 2, Oct 29, 2026

hn4a-compositor-rebuild-product-screen

Rebuild one product screen (a SaaS table, a dashboard, an app view) from a marketing screenshot as hn4a-compositor layers, so a camera can push into it at 2x and stay crisp and every part that moves is its own layer: rows that cascade and insert, cells that type, pills that flip, counts that climb, donuts and bars that draw. Covers reading the screenshot (measuring in source pixels, sampling colours per pixel, the app's own font), the builder contract (POI plus build(group_id, cues) returning layers, and a test spec), believable data, the checks, and a brief for one subagent per screen. For a product ad, app demo, UI animation or explainer; titles and cameras over footage are hn4a-compositor-in-scene-titles.

SKILL.md

Rebuild a product screen as compositor layers

Give it one screenshot of a product screen (or two, when a part comes from another image), the brand's colours, the cue keys the cut needs and the data to show. You get a builder, ui/<screen>.py, that returns compose layers for that screen inside a camera group, a ui/<screen>_test.json spec with one camera push, and stills proving it matches the source and stays crisp at 2×. The spec format and the command line are hn4a-compositor-motion-graphics; the camera and the type helpers (camera, typing, pop_keys, pulse_keys, line_html, text_layer) are hn4a-compositor-in-scene-titles (references/titles-fx.md). For a whole ad around the screens, follow recipe-product-ui-video. Never animate a screenshot with a video model: it warps the text.

Inputs

Read the source before writing a line of the builder.

  • Crop and zoom to read small type and edges, then look at the result: exec ffmpeg -i src.png -vf "crop=400:240:340:300,scale=iw*4:ih*4:flags=neighbor" crop.png.
  • Measure the layout in the source's own pixels: window and panel edges, column x's and widths, row top and pitch (a table from a 1377×969 screenshot: first row at y 358, a 63 px pitch, 10 columns), nav item centres, list spacing (36.2 px). Write them as constants at the top of the builder.
  • Sample colours per pixel, never by eye: exec ffmpeg -v error -i src.png -vf "crop=1:1:X:Y" -frames:v 1 -f rawvideo -pix_fmt rgb24 px.rgb and read the three bytes in Python (or Pillow's getpixel). Get pill backgrounds and foregrounds, hairline borders (near #EFF1F3), muted text, the page tint.
  • The app's font is not the site's. The marketing site's computed styles said Brand Sans; the app in its screenshots was App Sans, judged from the glyphs and confirmed by a still side by side. Read sizes and weights off the zoomed crop (the table: body 12 px on 16, headers weight 500, muted dates 11.5 px).
  • Radii and shadows: card, pill and button radii; shadows are usually faint (0 2px 10px rgba(20,40,40,.03)).
  • Leave out: marketing overlays (review-site badges, a floating duplicate card over the screenshot); the customer's logo in the app's logo slot (a neutral rounded square with a line icon instead); real people's avatars (initials or a grey silhouette).
  • The data the voice mentions (references/subagent-brief.md, the data paragraph) and the cue times, read from the voice track (python-read-word-timings).

Run it

The contract. One file per screen in the job's ui/, no shared library code beyond titles/titles_fx.py:

POI = {"status_col": (x, y), "new_row": (x, y), "won_row": (x, y)}  # group px, for the camera
def build(group_id: str, cues: dict) -> list[dict]: ...  # every layer "parent": group_id
def main(): ...                                          # writes ui/<screen>_test.json
  • Boxes in the group's 1920×1080 space, every layer "parent": group_id. The cut wraps them in camera(...).
  • Cues in comp seconds, all but build optional, each documented in the module docstring with what it does. Say whether a missing cue means the piece is shown finished or not shown at all (a dashboard: finished; a table's new row: absent).
  • anim keys are relative to the layer's start. Write them in comp seconds and convert in one helper (rel(keys, start)), or every key shifts by the start.
  • POIs that move: a point on something that shifts mid-screen needs both positions (won_row after an inserted row pushes it down, won_row_before_insert before).
  • The test spec: 12 s, the app font in fonts, a camera with one push to 1.8–2× and back.

Static chrome in a few big html layers (frame, filters, left panel, table card; or window, panel, nav, card shells), each an html string of absolutely placed elements from a small at(x, y, inner, style, w, h) helper. Everything that moves is its own layer.

Two ways to place things, both crisp:

  • Source pixels inside a CSS zoom wrapper. K = 1824 / 1335 ≈ 1.366 mapped the source window's width onto the group's 48..1872; gbox(x, y, w, h) and gpt(x, y) convert source px to group px for layer boxes and POIs. zoom is layout, not a transform, so text is laid out and rasterized at its real size, and every number reads straight off the source. Best when the source is large and kept whole.
  • Directly in group px (WIN, PANEL, KPI boxes), scaling the source's sizes up by hand. Simpler when the source is small (1199 px wide) or recomposed from two images.

Icons: inline SVG line icons (24×24 viewBox, stroke="currentColor", round caps, stroke 1.6–1.8) in a dict, drawn at the source's icon size. Plain boxes (tints, outlines) are fill layers with a style (boxShadow: inset 0 0 0 2px <brand>, borderRadius).

Animation: each effect and the layers it takes (panels building, a row cascade, a row inserting, a cell typing, a pill flipping, a highlight, a count-up, a donut, bars, a value highlight, a voice chip): references/animation-patterns.md.

Iterate:

exec python ui/activities.py                       # writes ui/activities_test.json
exec hn4a-compositor page  ui/activities_test.json # scrub it
exec hn4a-compositor still ui/activities_test.json ui/stills/new_row_mid.png --at 6.4

(cwd the job folder.)

Many screens: one subagent per screen, in parallel, each with the brief in references/subagent-brief.md, filled in completely so it has nothing to discover.

What you get

ui/<screen>.py (the builder: POI, build, main), ui/<screen>_test.json, and stills: the side by side, one per cue and mid-animation, and the 2× push. From the builder's report: its cue keys with each animation's duration after its cue, its POI keys and what each frames, and known gaps.

Tips and traps

  • Believable data in the client's market. Replace the screenshot's placeholder rows (the same sample job and city on every row) with real-looking names, places and amounts in the client's market and region, ids counting down, and due dates a few days after the comp's "today". Keep the status vocabulary of the source (Active / Pending / Cancel). Share one "today" and date range across every screen: a dashboard whose filters read one year beside a table dated another was a nit on the worked job.
  • Name the font on every text layer (style.font): the runtime checks the layer's own font, not its html's, and stops on a family with no loaded face.
  • A still occasionally times out fetching Google Fonts: retry it.
  • A path at draw ≤ 0.005 is hidden; start a donut's draw at 0.006, never 0 (0 left a hairline at the far end on older runtimes).
  • Children of a group skip will-change, so Chrome re-rasterizes text at the zoomed scale; keep it that way (don't set willChange on them).
  • A layer keeps its own display, and html: "" works; older builders worked around both.

Check it

  1. Side by side with the source: a still of the test spec at 1.0× beside the screenshot at the same height: exec ffmpeg -i src.png -i still.png -filter_complex "[0]scale=-2:1080[a];[1]scale=-2:1080[b];[a][b]hstack" side.png. Layout, colours, type weight and size should match; the data and the dropped overlays are the intended differences.
  2. A still at every cue and mid-animation (the insert half-way, the pill mid-pop, the donut half-drawn).
  3. Crisp at 2×: a still with the test camera at 1.8–2× on a dense region, looked at 1:1.
  4. Fonts loaded: still and render abort on a missing face.
  5. The builder compiles: exec python -m py_compile ui/<screen>.py.

Go make something cool.

Install the app, open your agent, describe the video. That's the whole onboarding.

Sticker HypernaturalSticker BannerSticker TVBoy