Skip to content

Callouts

A callout marks one region. shotlist works out the position, extends the canvas if the label needs room outside the image, and moves a label that would overlap one already drawn. The canvas also grows for a box or a numbered disc that would otherwise be cut by the edge of the shot.

Field Default What it does
markrequiredWhich mark to draw on
textLabel text: one string, or a list with a line each
nNumber shown in a disc, for matching a numbered list in the prose
placeautoauto, left, right, top, bottom, or corner for a disc
badgetlWhich anchor, with place: corner: tl tc tr ml mr bl bc br
boxtrueWhether to draw the outline
insidesee belowWhether the label or disc sits over the shot
dx0Nudge across, in image pixels
dy0Nudge down, in image pixels
padstyle'sDistance between the outline and the element
gapstyle'sDistance between the label and the outline

Where a label goes

place defaults to auto, which picks a side. A label to the left or right grows the canvas by its width, one above or below by its height — on a wide shot that is a difference of hundreds of pixels. auto weighs that against how far the arrow would have to travel, and against whether its path would cross another mark or a masked region.

It also decides whether the label goes over the shot or in a margin, by reading the pixels the label would cover and counting how many of them are far from that region's own average colour. A flat panel scores as empty however dark it is, and so does a gradient; text or a chart over either does not. Over the shot costs no canvas and needs only a stub of an arrow, so it wins wherever the shot turns out to have nothing there. When the pixels cannot be read at all, the decision falls back to the geometry.

What it cannot judge is what that detail is for. An arrow drawn across a paragraph is not something it knows to avoid, so name a side when the shot needs one — an explicit place is obeyed exactly, and so is an explicit inside.

Left unsaid, a disc (place: corner) sits inside, on the box; a label on a named side sits outside, in a margin the canvas grows to make.

Numbered discs

numbered: [a, b, c] is shorthand for one numbered disc per mark, in the order given.

Written as a mapping instead, marks: carries that list and every other key is applied to all of them: box, badge, inside, dx, dy and pad. There is no place among them — a numbered disc is always corner, and badge is which corner. Discs sit inside the box unless inside: false pushes them clear.

screenshots/recipes/statblock.yaml
marks:
  header: { css: '.statblock-header' }
  abilities: { css: '.abilities' }
  defenses: { css: '.defenses' }

numbered:
  marks: [header, abilities, defenses]
  box: false # number the sections without outlining them
  badge: ml # anchored to the middle of the left edge
  inside: false # pushed clear, into the margin

Masking a region

To keep a region out of the image, list a query for it under mask. It is painted over before the callouts are drawn. Use it for the part of a shot that differs on every re-shoot — a clock, a live total, a face — which would otherwise make --check report a change every time. check: false is the blunt version: it turns the check off for the whole image, where a mask leaves the rest of it checkable.

screenshots/recipes/dashboard.yaml
name: dashboard
clip: { css: '.panel' }
mask:
  - { within: clip, css: '.updated-at' }
  - { css: '.avatar' }

Key a mask on something that survives the value changing. A class, a test id, a position — never the content itself. { text: $42.00 } matches the figure you are hiding today and nothing at all tomorrow, when it reads $51.00. That failure is loud rather than silent — a mask matching nothing stops the run — but it does mean a mask written against the content breaks the run rather than degrading.

Any query works, so an element with no class and no test id is still reachable. A mask covers every element its query matches, not the first: a page has three avatars far more often than it has one. Naming pick or nth says you mean a single element and still gets one.

screenshots/recipes/dashboard.yaml
mask:
  - { within: clip, css: span, nth: 2 } # the third span, whatever it says
  - { within: clip, child: 2 } # the third child of the clip
  - { rect: [172, 84, 52, 20] } # a literal box, measured once

The regions are painted before the callouts are drawn, so a callout may still point at, and outline, a masked box. Masking only helps if the rest of the shot holds still: a value that changes width reflows what is beside it, and no mask covers that — give the column a width, or mask the row. With source: file there is no page to query, so a mask is a literal rect, the same as a mark.