Timelines and dopesheets
Two blocks in this chapter draw time, and they draw different time. timeline draws calendar time: the 20th of February, the Mondays in March, the leap day in 2024. dopesheet draws animation time: frame 0, then frame 1, then frame 2, twelve of them a second. One is a dated axis computed by the build; the other is a window sliding over a sprite sheet in the reader's browser. They share a chapter because the question they answer looks the same and their answers have nothing in common — and because both are @native, for two different reasons. Two kinds of time at the end of this chapter is that comparison.
Both are diagram shapes. Neither is a page block: a timeline and a dopesheet are children of a diagram or a container, positioned with x / y like a rect, and connectable by edges. The diagram canvas covers the surface they sit on.
Every diagram on this page was built before it was written down. Type them and compare.
§ 1A dated axis
Here is a complete document. Save it as roadmap.wcl:
import <wdoc.wcl>
site roadmap {
title = "Roadmap"
}
page plan {
title = "The 2026 plan"
h1 "The 2026 plan"
diagram { width = 560 height = 200
timeline { width = 560.0 height = 200.0
title = "2026 roadmap"
start = "2026-01-01"
end = "2026-12-31"
unit = :months
items = [
{ label: "Kickoff", on: "2026-02-20" },
{ label: "Beta", on: "2026-05-10" },
{ label: "Release", on: "2026-09-20", side: :far },
]
phases = [
{ label: "Build", from: "2026-02-01", to: "2026-06-15" },
{ label: "Polish", from: "2026-06-15", to: "2026-11-01" },
]
}
}
}
Build it into a folder and read the result from a server:
$ wcl wdoc build roadmap.wcl --out _site
wrote 1 page
The page holds one diagram, and the diagram holds the timeline declared above — the same items and phases, rendered:
Read the drawing against the source. The spine runs from start to end. The tick marks under it are real calendar months — the 1st of each, with the year printed on January. The three items are the dated markers, each with a lead line and a label, alternating above and below the axis until side: :far pins one. The two phases are the vertical dividers with a heading between them.
Everything after this section refines that one picture.
A timeline does not fill its diagram
width and height are the timeline's own, and they default to 480.0 × 200.0 whatever the enclosing diagram measures. A timeline with no size inside a 900 × 300 diagram draws 480 × 200, and the diagram's viewBox shrinks around it — the axis is not clipped, but it is not the diagram either. Write the diagram's numbers twice, as every example here does. Note the types: a diagram takes integers, a timeline takes floats.
§ 2Dates
There is no date type in WCL. Every date on a timeline is a plain string, and the timeline parses it. Four spellings are accepted:
| Written | Means |
|---|---|
| "2026-03-15" | midnight at the start of 15 March 2026 |
| "2026-03-15 14:30" | 14:30 that day |
| "2026-03-15T14:30" | the same instant, ISO-8601 spelling |
| "2026-03-15 14:30:05" | seconds are optional, and parsed when given |
Zero-padding is not required: "2026-3-1" parses as 1 March. There is no timezone and no offset — a timeline places instants on one unbroken scale, and reads every date on it the same way. Values and primitives covers the string forms themselves.
An unreadable date is dropped, not reported
A date the parser cannot read yields nothing at all: the item, the phase boundary or the card is skipped, the build prints no warning, and the page renders without it. "15/03/2026" and "March 15" both vanish this way. The same silence covers start and end — an unparseable end falls back to the fitted scale, so the axis quietly becomes an auto-fitted one. If a milestone is missing from a drawing, check its date string first.
§ 3The scale
start and end set the two ends of the axis. Both are optional, and what happens when you omit them is worth knowing exactly, because the timeline fits itself to your data:
| What you write | The scale runs |
|---|---|
| start and end | exactly between them |
| one of the two | from the one you gave to the fitted other end |
| neither | from the first dated event to the last |
| neither, and one event only | from that instant to 24 hours later |
| nothing an end can be fitted from | a bare spine — no ticks, nothing pinned |
"Dated event" means every date the block carries: each item's on, each phase's from and to, and each card's on. A phase that reaches past the last milestone widens the axis, and so does a card.
The last row is the one to read twice. Both ends have to resolve before a single tick is drawn, so a start with nothing dated to fit the other end against draws the bare spine too — not a half-fitted axis. A timeline that has lost its ticks has usually lost its events.
The fitted scale has a consequence you can see. With no start, the earliest event sits exactly on the left end of the spine, with no room before it. Give a start a week earlier when a milestone needs breathing space:
Preview
Example
diagram {
width = 520
height = 150
timeline {
width = 520.0
height = 150.0
title = "Fitted — the first item is the left end"
items = [{ label: "Kickoff", on: "2026-02-20" }, { label: "Beta", on: "2026-05-10" }]
}
}§ 4Ticks land on calendar boundaries
A tick is not the axis divided into equal parts. It is a real calendar boundary: the 1st of a month, a Monday, an hour. The unit field picks which boundary, and the timeline walks the calendar from the start of the scale to its end.
§ 4.1Choosing the unit
Omit unit and the timeline picks one from the length of the scale:
| Scale longer than | Unit chosen |
|---|---|
| 730 days | :years |
| 75 days | :months |
| 21 days | :weeks |
| 2 days | :days |
| 2 hours | :hours |
| anything shorter | :minutes |
Write unit yourself when the automatic choice reads badly. A five-month plan is :months either way, but a three-week sprint auto-picks :weeks where you may want :days.
§ 4.2Tick labels
Each unit floors to its own boundary and prints its own label:
| Unit | Boundary | Label |
|---|---|---|
| :minutes | the minute | 09:05 |
| :hours | the hour | 09:00 |
| :days | midnight | 5 Mar |
| :weeks | Monday midnight | 9 Mar |
| :months | the 1st | Mar — and Jan 2026 on January |
| :years | 1 January | 2026 |
The year rides on the January tick and nowhere else, which is what makes a multi-year :months axis readable without printing 2026 twelve times.
The walk starts by flooring the scale's start to the unit, so it can begin before the axis does. A tick that lands outside start..end is not drawn. That is why the fitted timeline above, whose first item is 20 February, shows its first tick at 1 March: the 1 February boundary is off the left end.
§ 4.3The tick interval
every sets how many units apart the ticks are. Omit it and the timeline picks from a ladder per unit, taking the first interval that brings the count to twelve or fewer:
| Unit | Intervals tried |
|---|---|
| :minutes | 1, 2, 5, 10, 15, 30, 60 |
| :hours | 1, 2, 3, 6, 12, 24 |
| :days | 1, 2, 5, 7, 14, 28 |
| :weeks | 1, 2, 4, 8 |
| :months | 1, 2, 3, 6, 12 |
| :years | 1, 2, 5, 10, 20, 50 |
If no interval on the ladder gets there, the last one is used and the axis simply carries more ticks. A nine-hour window therefore ticks hourly, and a two-week window ticks every second day:
Preview
Example
diagram {
width = 520
height = 140
timeline {
width = 520.0
height = 140.0
title = "Launch day"
start = "2026-03-15 09:00"
end = "2026-03-15 18:00"
items = [{ label: "Freeze", on: "2026-03-15 10:30" }, { label: "Deploy", on: "2026-03-15 13:00" }, { label: "All-clear", on: "2026-03-15 16:45" }]
}
}The walk stops after 256 ticks
A tick loop that walked a calendar for ever would hang a build, so it stops at 256 boundaries and the rest of the spine stays bare. Two things get you there, and both involve overriding an automatic choice. An every far too fine for the span: unit = :years with every = 1 across 1600–2200 labels 1600 to 1855 and stops. And a unit far too fine for the span, because each ladder has a coarsest rung — unit = :months over the same 600 years exhausts its ladder at 12 months and stops at Jan 1855 too. An axis that chooses both for itself will not reach the cap. Coarsen the unit, or widen every, rather than shortening the axis.
§ 5Milestones
items is the list of point events. Each one is a label and a date:
items = [
{ label: "Kickoff", on: "2026-02-20" },
{ label: "Beta", on: "2026-05-10" },
{ label: "Release", on: "2026-09-20", side: :far },
]
Each item draws three things: a small filled marker on the spine, a lead line out from it, and the label at the end of the lead. Items alternate: the first goes to the near side, the second to the far, the third back to the near, and so on down the list.
side overrides the alternation for one item. :near is above a horizontal axis and left of a vertical one; :far is below and right. :auto means the same as writing nothing.
The two spellings above are two variants of one union, and which one you get is decided by the shape of the record you wrote:
union TimelineItem {
On { label: utf8 on: utf8 }
OnSided { label: utf8 on: utf8 side: symbol }
}
A bare { label: …, on: … } coerces to On; add a side: key and it coerces to OnSided. That is the ordinary bare-record rule — see Lists, tensors and records. The practical consequence is that there is no partial item. An item needs both a label and an on, and a record matching neither variant fails the build rather than leaving a marker silently out of the drawing. Drop the date from roadmap.wcl's first item and the build stops:
$ wcl wdoc build roadmap.wcl --out _site
wcl::eval::schema_violation
× no variant of 'TimelineItem' matches the supplied shape
╭─[roadmap.wcl:18:7]
17 │ unit = :months
18 │ ╭─▶ items = [
19 │ │ { label: "Kickoff" },
20 │ │ { label: "Beta", on: "2026-05-10" },
21 │ │ { label: "Release", on: "2026-09-20", side: :far },
22 │ ├─▶ ]
· ╰──── schema violation
23 │ phases = [
╰────
1 schema violation
Contrast that with a date the parser cannot read, which is dropped in silence. The shape of an item is checked by the schema; the content of its date string is not.
§ 6Phases
A phase is a named stretch of the axis — discovery, build, launch. Each one is a label and two dates:
Preview
Example
diagram {
width = 560
height = 190
timeline {
width = 560.0
height = 190.0
title = "Project phases"
start = "2026-01-01"
end = "2026-12-31"
unit = :months
phases = [{ label: "Discovery", from: "2026-01-01", to: "2026-03-01" }, { label: "Design", from: "2026-03-01", to: "2026-05-15" }, { label: "Build", from: "2026-05-15", to: "2026-10-01" }, { label: "Launch", from: "2026-10-01", to: "2026-12-31" }]
items = [{ label: "Sign-off", on: "2026-04-20" }, { label: "GA", on: "2026-11-10" }]
}
}A phase is drawn as two boundary dividers and a heading, not as a filled band. The dividers stand perpendicular to the axis at from and at to, and the label sits centred between them. Four back-to-back phases like the ones above share their boundaries, so the drawing reads as one divided ruler rather than four separate bars.
Each phase takes the next colour from the wdoc-series-1 … wdoc-series-8 palette, by its position in the list, wrapping after eight. Those are the same series classes the charts use, so a site theme recolours a timeline and a chart together — see Themes and styling. The rest of the drawing carries wdoc-axis and wdoc-axis-label on the spine and ticks, wdoc-timeline-divider and wdoc-timeline-phase-label on the phases, wdoc-timeline-marker, wdoc-timeline-connector and wdoc-timeline-label on the items, and wdoc-chart-title — the chart's, shared — on the title. A class block naming any of them restyles every timeline on the site.
A timeline's own class and id go nowhere
class and id are declared on Timeline — they are on the field table below — but the renderer emits neither. timeline { class = ["mine"] id = roadmap } produces no mine class and no id attribute anywhere in the SVG, and there is no error to tell you so. Style through the wdoc-* classes above, and reach for the site theme when a whole book needs one palette. The class a card child carries is emitted; it is only the timeline itself that drops them.
Phases may overlap, and nothing checks that from precedes to. A phase written backwards draws its two dividers in the same two places and its heading between them, which is rarely what you meant.
§ 7Event cards
A milestone label is one line of plain text. When an event needs a paragraph, a list or a callout, pin a card to the axis instead. A card is the ordinary rich-text diagram shape — Trees, node tables and cards covers it in full — and a timeline reads two of its fields to place it: on, the date it hangs from, and side.
Preview
Example
diagram {
width = 700
height = 300
timeline {
width = 700.0
height = 300.0
title = "Release history"
unit = :months
start = "2026-01-01"
end = "2026-12-31"
card {
on = "2026-02-01"
title = "1.0"
width = 200.0
height = 96.0
text {
span "First "
span "**stable**"
span " release. The public API is frozen."
}
}
card {
on = "2026-05-15"
side = :far
title = "1.4"
width = 200.0
height = 96.0
text {
span "Plugin system, and the "
span "**wdoc**"
span " generator."
}
}
card {
on = "2026-10-01"
title = "2.0"
width = 200.0
height = 96.0
text {
span "Rewrite: lazy evaluation, schema checking, LSP."
}
}
}
}Cards carry their own width and height (160.0 × 90.0 by default) and alternate sides exactly as items do — but on their own count. The first card goes near whether or not an item sat there already, so a timeline mixing items and cards can put both on one side of the spine. Pin the ones that clash with side.
A card is clamped inside the timeline's box. A card near the end of the axis slides back in rather than hanging off the edge, which keeps the diagram tight but means a wide card near a boundary no longer sits centred on its date. Its lead line still points at the right place.
§ 8Vertical timelines
direction = :vertical turns the axis on its side: time runs down the page, and items and cards alternate left and right. Everything else — units, ticks, phases, cards — behaves identically. A vertical timeline suits a long release history in a narrow column, where a horizontal one would crush its labels together.
Preview
Example
diagram {
width = 300
height = 320
timeline {
width = 300.0
height = 320.0
direction = :vertical
title = "Release history"
unit = :months
items = [{ label: "1.0 alpha", on: "2025-10-15" }, { label: "1.0", on: "2026-01-10" }, { label: "2.0 beta", on: "2026-05-01" }, { label: "2.0", on: "2026-07-10" }]
phases = [{ label: "1.x", from: "2025-09-01", to: "2026-02-01" }, { label: "2.x", from: "2026-02-01", to: "2026-08-01" }]
}
}§ 9Timeline fields
| Property | Type | Required | Description |
|---|---|---|---|
| x | f64 | no | Timeline x position within the enclosing diagram. |
| y | f64 | no | Timeline y position within the enclosing diagram. |
| width | f64 | no | Timeline width — match the enclosing diagram. |
| height | f64 | no | Timeline height — match the enclosing diagram. |
| id | identifier | no | Optional explicit HTML id. |
| class | list<utf8> | no | Optional style classes. |
| title | utf8 | no | Timeline title. |
| direction | symbol | no | Axis direction: :horizontal (default) or :vertical. |
| unit | symbol | no | Tick granularity: :minutes / :hours / :days / :weeks / :months / :years (auto from the span when omitted). |
| start | utf8 | no | ISO date scale start (auto-fits from the items when omitted). |
| end | utf8 | no | ISO date scale end (auto-fits from the items when omitted). |
| every | i64 | no | Override the tick interval in units (auto ~6–12 ticks when omitted). |
| phases | list<TimelinePhase> | no | Dated bands — list<TimelinePhase>, each { label, from, to }. |
| items | list<TimelineItem> | no | Dated point events — list<TimelineItem>, each { label, on } (auto-alternates side; add side: :near|:far to pin one). |
| connect_points | list<AnchorSide> | no | Diagram edge-attach sides, like any shape. |
Child blocks
| Slot | Accepts | Multiple | Description |
|---|---|---|---|
| cards | card | yes | Rich-text event cards, each pinned to a date via on. |
§ 10A sprite sheet, played
A dopesheet is the other half of this chapter, and it starts from a picture rather than a date. Give it a sprite sheet, tell it how big one frame is, and it plays the frames:
Preview
Example
diagram {
width = 96
height = 96
dopesheet "../assets/pixel-coin.png" {
frame_width = 12
frame_height = 12
scale = 6.0
fps = 12.0
x = 12.0
y = 12.0
}
}That sheet is 72 × 12 pixels: six 12 × 12 frames in one row. Nothing in the block says "six" — the build reads the sheet's pixel dimensions off the file and divides. scale = 6.0 draws each 12-pixel frame at 72, and fps = 12.0 runs it at twelve frames a second. Click the coin to pause it, and click again to resume.
The sheet is named by an inline label, the way an image block names its source, and the path is relative to the build entry file — not to the page that declares it. That is why the snippet above reads ../assets/pixel-coin.png: this book's entry sits one folder below the assets it shares, and the page's own location has nothing to do with it. Images, videos and file assets covers the rule. The file is copied into the output's _wdoc/ folder and referenced by URL, so the frames appear when the output is served and not when a page is opened directly from disk.
A dopesheet with no frame size draws nothing
frame_width and frame_height have no defaults, and a dopesheet missing either — or carrying an empty source — renders as nothing at all. No error, no placeholder, no gap in the diagram. That is the same best-effort silence a failed shape lowering has. A missing sprite-sheet file is different: that one fails the build when the copy step reaches it.
§ 11Frame geometry
A sheet is a grid, and a few numbers describe it. The ones beyond the frame size handle sheets that are padded or gapped:
| Field | Default | What it measures |
|---|---|---|
| frame_width / frame_height | none — required | one frame, in sheet pixels |
| offset_x / offset_y | 0 | pixels before the first frame |
| stride_x / stride_y | the frame size | origin-to-origin step between frames |
| columns | as many whole strides as fit across the sheet | frames per row |
Stride is separate from frame size because a sheet may put a gutter between its frames. A 16-pixel frame drawn every 18 pixels is frame_width = 16 with stride_x = 18; the two pixels between are never sampled.
The default columns is a division, and it truncates:
columns = (sheet_width - offset_x) / stride_x # integer division, at least 1
rows = (sheet_height - offset_y) / stride_y # same, and never authored
total = columns * rows
On the coin sheet that is (72 - 0) / 12 = 6 columns, one row, six frames. Add offset_x = 4 and stride_x = 14 and it becomes (72 - 4) / 14 = 4. Note what is being counted: whole strides, not whole frames. A fifth stride would have to start at pixel 60 and reach 74, past the right edge, so it is not counted — even though a fifth 12-pixel frame starting at 60 would have fitted. Say columns = 5 when you mean that. rows is always computed; there is no field for it.
A frame is addressed by one flat index, and the index walks the grid row by row:
column = index % columns # 0-based
row = index / columns # integer division
window = ( offset_x + column * stride_x,
offset_y + row * stride_y,
frame_width, frame_height )
So a 4-column sheet plays 0, 1, 2, 3 across the top row and then wraps to 4 at the start of the second. That arithmetic runs twice: once in the build, which windows the group on the from frame, and once in the player script, which advances it. Both read the same numbers, because the build stamps them onto the rendered group as data-dope-* attributes and the player reads them back.
columns is a claim, not a measurement
An explicit columns is trusted. Write columns = 10 for the six-frame coin sheet and the play range runs to frame 9, whose window lies past the right edge of the image and draws blank. Give columns when the build cannot measure the sheet — an http(s):// source, or a format whose header it cannot read — and let it be computed otherwise.
§ 12The play range and playback
from and to are flat frame indices, and the range they name is inclusive. Both are clamped to the sheet: from into 0..total - 1, and to into from..total - 1, so to = 99 on a six-frame sheet plays to frame 5 rather than failing. Omit to and it is the last frame; omit from and it is 0.
The dopesheet below plays only the first three frames, at half the speed, and starts stopped. Click it to play:
Preview
Example
diagram {
width = 96
height = 96
dopesheet "../assets/pixel-coin.png" {
frame_width = 12
frame_height = 12
from = 0
to = 2
fps = 6.0
autoplay = false
scale = 6.0
x = 12.0
y = 12.0
}
}Six more fields control the rest of the playback:
| Field | Default | Effect |
|---|---|---|
| fps | 12.0 | frames a second |
| loop | true | restart at the end; false stops on the last frame |
| autoplay | true | start on load; false waits for a click |
| controls | true | draw the centred play glyph |
| scale | 1.0 | display size — a 12-pixel frame at 6.0 draws 72 wide |
| smooth | false | true opts into the browser's image smoothing |
The glyph is ▶ while a dopesheet is paused and ↻ once a non-looping one has finished, and it is hidden while the animation runs. Pixel art is drawn with nearest-neighbour scaling by default, which is what smooth = false means. Set smooth = true for a sheet of photographs or smooth vector frames, where hard pixel edges would be an artefact rather than the style.
controls = false hides the glyph, not the click
The whole dopesheet group listens for a click, and controls = false only stops the play glyph being drawn. A reader can still pause a dopesheet that shows no control, and will then have no way of telling that it is paused. Reach for controls = false when a dopesheet is decorative and loops for ever, not when you want it uninterruptible.
§ 12.1Tracks and keys
The block is named after the animator's dope sheet — the grid of tracks down one axis and frames across the other, with a key marked in each cell that changes. A wdoc dopesheet is one of those tracks: the sprite sheet is the track, and its keys are the frame indices, one per frame with no gaps between them. There is no track child block and no per-key field.
Several tracks are several dopesheets in one diagram. Each carries its own sheet, its own range and its own fps, and each is placed with x / y like any shape — so a character, its shadow and a spinning pickup are three blocks that happen to overlap. Shapes are drawn in the order they are declared, so a dopesheet written later overlays one written earlier. That draw order is the whole compositing model: there is no z-index and no shared clock, and two dopesheets at different fps values drift apart as they run.
§ 13Dopesheet fields
| Property | Type | Required | Description |
|---|---|---|---|
| source | utf8 | yes | Spritesheet image path (the inline label), relative to the build entry file. |
| frame_width | i64 | yes | Size of one frame in the sheet (pixel width). |
| frame_height | i64 | yes | Size of one frame in the sheet (pixel height). |
| offset_x | i64 | no | Pixel offset to the first frame's left edge (default 0). |
| offset_y | i64 | no | Pixel offset to the first frame's top edge (default 0). |
| stride_x | i64 | no | Origin-to-origin x step between frames (default = frame_width). |
| stride_y | i64 | no | Origin-to-origin y step between frames (default = frame_height). |
| columns | i64 | no | Frames per sheet row (default: fit from the sheet width). |
| from | i64 | no | First frame index to play (default 0). |
| to | i64 | no | Last frame index to play, inclusive (default: the last frame). |
| fps | f64 | no | Playback rate in frames/second (default 12). |
| loop | bool | no | Restart at the end (default true). |
| autoplay | bool | no | Start playing on load (default true). |
| controls | bool | no | Click play/pause toggle (default true). |
| smooth | bool | no | Anti-alias instead of the default image-rendering: pixelated. |
| scale | f64 | no | Display scale (default 1.0). |
| x | f64 | no | Position x within the enclosing diagram / container. |
| y | f64 | no | Position y within the enclosing diagram / container. |
| id | identifier | no | Optional explicit HTML id. |
| class | list<utf8> | no | Optional style classes. |
| anchor_left | f64 | no | Diagram anchor insets (left/right/top/bottom), like any SvgBlock. |
| connect_points | list<AnchorSide> | no | Diagram edge-attach sides, like any SvgBlock. |
§ 14Two kinds of time
Here is the comparison, now that both halves are on the table. The two blocks share a subject and almost nothing else:
| timeline | dopesheet | |
|---|---|---|
| A position on it is | a date | a frame index |
| Time comes from | ISO strings you author | counting up from from at fps |
| Who computes it | the build, against a real calendar | the reader's browser, while the page is open |
| What reaches the page | finished SVG — spine, ticks, markers, cards | one windowed <svg> plus the frame geometry |
| The scale is set by | start / end, or a fit to the events | the sheet's own pixel dimensions |
| In Markdown and PDF | the same drawing | the from frame, frozen |
| Reach for it when | the reader needs to see when | the reader needs to see motion |
§ 14.1Why both are native
Most wdoc blocks are written in WCL. A block declares a lower function returning shapes, the renderer recurses until only leaves remain, and no Rust is involved — that is how the charts work, and how Writing your own blocks says you add your own. Both blocks in this chapter carry @native instead, which is the declaration that a kind is drawn by the renderer and has no lower at all. A wdoc block declares exactly one of the two; neither and both are build errors.
For the timeline, the missing piece is the calendar. Placing a tick on 1 March means knowing that February had 28 days this year and 29 the last. Stepping a month means adding a variable number of days. Flooring to a week means knowing which day was Monday. WCL has arithmetic and it has strings, but it has no date type and no calendar, so a WCL lower could not work out where the ticks go — there would be nothing for it to return. The renderer does the calendar work and hands back finished geometry.
For the dopesheet, the missing pieces are the file and the clock. The frame grid depends on the sheet's pixel dimensions, read off the image file while the site builds, and a WCL expression cannot open a file. The animation is not a drawing at all: it is a script advancing one viewBox on a timer, shipped once per site and wired to the data attributes the build stamped on the group. Neither is expressible as a value a lower could return.
The two reasons are worth keeping apart, because they generalise differently. A block is native when what it needs is knowledge the language does not have — a calendar, a font metric, the bytes of a file. It is also native when what it produces is not a shape — a script, an HTML fragment, a copied asset. timeline is the first kind and dopesheet is mostly the second. A block of your own that fits neither description should be lowering in WCL instead.
Native does not mean HTML-only
Both kinds declare coverage on all four backends, so neither needs an @except to survive a Markdown or PDF build. A timeline renders the same static drawing everywhere. A dopesheet renders its from frame and stops — the geometry is still stamped on the group, but no player runs beside a .md file or inside a PDF. Output targets covers what each backend does with a diagram.
§ 15Where to go next
- The diagram canvas — the surface both blocks sit on, and how x / y, anchors and layout place a shape on it.
- Charts — the other data-driven axis, and the one that is written in WCL. Compare its lower with this chapter's two natives.
- Trees, node tables and cards — the card shape a timeline pins to a date.
- Tilemaps and maps — the other blocks that crop an external image into a grid.
- Images, videos and file assets — how a sprite sheet's path resolves, and where the copy lands.
- Writing your own blocks — lower, @native, and which one a new block of yours should carry.