Tags¶
The demo deck for this page is
tags.typ
(HTML,
PDF presentation,
PDF handouts).
It shows the initial states of a tag, one name at several sites, the wrappers that wrap chooses,
and tags inside math, a cetz canvas and a grid cell.
A tag marks a part of a slide and names it.
The animation argument of #slide defines the animation
within one slide through subslides, which reference the tags by name.
Each subslide specifies what happens to tagged contents to arrive at a new state.
#slide(animation: {
import anim: *
sub(reveal("caveat"))
})[
A short claim.
#tag("caveat")[With a caveat.]
]
Initial State of Tagged Content¶
The initial state of a tag is not written at the tag site. A tag takes its initial state from the first operation in the timeline that addresses each of its two slots:
- a tag whose first display operation is
revealstarts hidden: laid out, taking its space, and invisible. The tagcaveatabove is an example. - a tag whose first content operation is
resetstarts removed: not laid out at all, so it takes no space until theresetbrings it in.
The two are independent, so a tag the timeline resets and later reveals starts out both removed and hidden. Anything else starts visible and laid out as you wrote it.
One Name, Several Places¶
The same tag name may be used several times within one slide. The timeline then addresses all of them together, as if they were one element. Every site of one name starts in the same state, because that state is a property of the name in the timeline and not of a single tag site.
The same name in a different slide does not interfere.
A tag reads the plan of the slide it appears in and of no other,
so a deck that wraps #slide in a custom function changes nothing about how its tags
resolve.
A name the slide tags nowhere is refused rather than ignored, so a misspelt name in a timeline is a compile error and not an operation that does nothing.
A Name Is a Label¶
A tag name becomes the label of the group Animo emits,
which is the same namespace that the <label> syntax of typst writes into.
A slide that tags "figure" and also writes #box[..]<figure> has two groups of one name,
and in the browser both move.
Animo cannot tell them apart, because in the output they are the same thing.
Keep the names of a slide's tags out of the labels that slide writes for itself.
Animo keeps the names it generates apart as well:
animo- is a reserved prefix, and a tag or region named with it is refused.
What a Tag Site Becomes¶
A tag generally wraps its body with typst #box or #block,
because these are the only two elements that typst makes addressable in the browser.
The default auto wrapping usually does the right thing,
and the wrap: argument overrules it where a corner case requires a different wrapper.
wrap |
Wrapper |
|---|---|
auto |
#box for an inline body, #block(width: 100%) for a block-level one |
box |
#box, the wrapper for inline contents |
block |
#block(width: 100%), a wrapper for full-width contents |
none |
no wrapper and no label, the body returned untouched |
| a function | the function builds the inner slot, and has to produce a box or a block |
The none option is occasionally useful for a tag that is only addressed by reset and replace,
because those operations do not need a wrapper like move or scale.
While none limits animation functionality, the absence of a wrapper allows for content flowing across lines within a paragraph.
A function is accepted so that the wrapping element can be styled, which will also move and scale with the element:
What auto Measures¶
The choice auto makes is a function of the body alone and never of the timeline,
so adding a sub() call in the animation cannot reflow a paragraph,
and every output type lays the slide out the same way.
What auto reads off the body is whether it is inline or block-level,
as typst defines these terms.
Animo determines this by measuring whether the body pushes a zero-sized neighbour onto a
separate line.
The length of the body never enters into the choice.
The measurement is unbounded, so a paragraph never wraps while it is being measured,
and a run of text is inline whether it is three words or three lines.
An explicit \ linebreak in the body leaves it inline as well.
| The body is | The wrapper is |
|---|---|
text, inline math, inline raw, a box |
#box |
| a display equation, a list, a heading, a raw block | #block(width: 100%) |
a block, a rect, a circle, an image, a cetz canvas |
#block(width: 100%) |
a table, a grid, a stack, anything inside align(..) |
#block(width: 100%) |
| several paragraphs | #block(width: 100%) |
A rect, a circle and an image are block-level elements in typst,
so they take the filling block, while a box of the same size takes a #box.
The wrapper also decides what the continuous primitives mean on the tag.
A #box hugs its body, so scale scales about the centre of the body,
and the relto: anchor of move and pan reads the top-left corner of the body.
A #block(width: 100%) fills its container,
so those primitives read the centre and the corner of the container instead.
A list in a filling block therefore scales about the centre of the container,
even when its items take only the left half of it.
All three output types scale about the same centre.
A #block(width: 100%) also keeps content centred that its container was centring,
because a wrapper at width: auto left-aligns such content.
Tagging Limitations¶
Tagged content with a wrapper can not break across lines. A group that CSS can translate cannot be split over two lines. Tag a whole paragraph, or a short phrase that has room, rather than a long stretch in the middle of a paragraph.
A tagged heading shifts by a few points. A heading carries block spacing above and below it. Inside a tag, that spacing sits at the edge of the wrapper, where typst trims it. Typst does not expose the trimmed value, so Animo cannot restore it. Tag the heading's text instead when the shift matters:
Where a Tag May Sit¶
Anything typst lays out as content can be tagged.
Inside math a tag is an ordinary tag site:
Inside a cetz canvas a tag goes on a content() element,
and inside fletcher on a node label, because both hold ordinary content:
#cetz.canvas({
import cetz.draw: *
line((0, 0), (4, 0))
content((4, 0), tag("lab")[Hi], anchor: "west")
})
Raw cetz draw commands are refused.
A draw command such as grid((0, 0), (4, 2)) is an array of closures rather than content.
The closures are built where the command is written,
before any show rule exists that could resolve them for a subslide.
No primitive could ever reach a tag around a draw command,
so Animo refuses such a tag at the tag site rather than silently doing nothing in every output.
What to write instead is a figure that is a function of what it draws, tagged as a whole and replaced:
#let scene(mesh: true) = cetz.canvas({
import cetz.draw: *
circle((0, 0), radius: 1)
if mesh { grid((-2, -2), (2, 2), stroke: gray) }
})
#slide(
animation: {
import anim: *
sub(replace("fig")[#scene(mesh: false)])
},
)[
#tag("fig")[#scene()]
]
Anything on the figure that has to move smoothly, or be revealed and hidden, must be a typst content element with a tag.
A cell of a grid or a table is refused, and so is an item of a list, an enum or a terms
list.
A grid reads fill, colspan, align and stroke from its direct children.
A tag site is a context block between the grid and the cell,
so the grid takes the whole tag site as the content of a cell with default settings.
The fill of the cell would then be dropped in every output type, with no panic and no warning.
The same holds for region.
Write the tag inside the cell, where the grid still reads the cell itself:
#grid(
columns: (1fr, 1fr),
inset: 0.5cm,
grid.cell(fill: aqua)[Untagged],
grid.cell(fill: orange, tag("note")[Tagged]),
)
A tag inside a cell does not cover the cell's fill. The grid paints the fill in a separate
frame, outside every group a tag produces, so move, scale, pan, reveal and hide on
such a tag reach the text and leave the fill standing.
When the timeline has to move or hide the filled box itself, put the fill on a block that the
tag holds. A block takes a gradient as readily as a color:
#grid(
columns: (1fr,),
grid.cell(tag("panel", block(
fill: gradient.linear(aqua, orange),
width: 100%,
height: 100%,
inset: 0.5cm,
)[Summary])),
)
The tag's group then holds the block, and the fill moves, scales, reveals and hides with it.
A block with a fill is also the shorter answer when the grid was only there for the panel:
one #block(fill: ..) inside a #place or a #stack needs no cell at all.
A tag around a cell that sets nothing beside its body is accepted, because such a cell lays out the same either way. A tag around an item is refused whatever the item carries, because the list then takes the whole tag site as the body of a new item, so the tagged item turns into a nested list.
The Reference has the table of which primitives reach which kind of
tag site.
In that table, wrap: none is the special case:
such a tag has no group, so nothing continuous can address it,
and it has no box, so a structural primitive reaches it only inside a region.