6.0 KiB
Tree of Art — alternative start page
An alternative landing layout for the timeline: the year axis runs bottom → top
and the art movements are drawn as a growing tree instead of parallel streams.
Reached from the 🌳 Tree of art link at the top-left of every timeline page
(layoutTree); the classic and vertical layouts stay untouched and are one click away.
Shareable URL: ?layout=tree (also vertical / omit or classic for the other layouts).
| Classic | Vertical | Tree | |
|---|---|---|---|
| Component | MovementBands |
VerticalMovementBands |
MovementTree |
| Time axis | left → right | bottom → top | bottom → top |
| Layout | temporal lanes | centre-out lanes | spanning tree |
| Recomputed on zoom | yes | yes | no — structure is fixed |
| Artist portraits | yes | no | no |
Both bottom-up layouts share the VerticalTimeline
axis, so eras, event marks, zoom and pan behave identically across them.
Layout rules
The geometry splits in two: utils/movementTree.ts
decides the shape of the tree (horizontal, view-independent), and MovementTree
maps that shape onto the current year window (vertical) each frame.
Structure — buildMovementTree()
- Time grows upward. Oldest movements at the bottom, newest at the crown.
Y is a plain
year → pixelmapping; the layout engine never touches it. - One trunk, at the centre.
MOVEMENT_LINEAGEis a DAG, so it is reduced to a spanning tree: each movement keeps its most immediate predecessor (the parent with the latest start year that still precedes it) as its structural parent. Ranking by start year first makes cycles impossible by construction. - Extra parents become grafts. The predecessors that lost step 2 are still drawn —
as thin, low-opacity limbs behind the tree — so
Post-Impressionism → Cubismsurvives even though Cubism hangs structurally off Fauvism. - Children split the parent's slot. Each node reserves a slot as wide as its whole
subtree (
max(own limb, Σ children)); children are packed side by side and centred on the parent. A single-child chain inherits the parent's x exactly — the trunk stays straight until it forks, forks spread symmetrically, and later generations land further from the centre. - Leonardo's rule for thickness. A limb is as thick as the limbs it carries:
base² = own² + Σ child.base². The trunk is the thickest thing on screen and every branch tapers as it rises and sheds children. A movement's own thickness comes from itsinfluence_link_count. - Branches lean outward across their own lifespan, by at most the slack left inside their slot — organic, and collision-free by construction.
- Unlinked movements are saplings. A movement with no lineage edge is its own root; extra roots are planted alternately right and left of the trunk, widest subtree first, so the main trunk keeps x = 0 (canvas centre). Roots get a small root flare.
Because the structure is built from the whole catalogue, zooming never reshuffles the tree — you keep your bearings, unlike the lane-packed layouts which re-pack on every view change.
Rendering — MovementTree
- Spread follows zoom, not the canvas. 90 % of the catalogue lives in the last 15 %
of the time axis, so a tree stretched to full width with all of history in view is one
long trunk under a flat bar. The whole-history view draws the tree at
FULL_VIEW_WIDTH_SHARE(52 %) of the available width, and each zoom step fans the crown out ((totalSpan / visibleSpan) ^ 0.45, capped atMAX_FIT_BOOST). The chart grows as you walk up it. Whatever is on screen is always clamped to fit the canvas. - Readability floors, never date changes. A 30-year movement is ~8 px tall with 2 900
years in view. So a limb is drawn at least
MIN_LIMB_RISE_PXlong, a junction climbs at leastMIN_JUNCTION_RISE_PXbefore it spreads sideways, and a limb is never thicker than 55 % of its own length. Positions still come from real years; only the drawn length and thickness have a floor, and the junction slides down the parent limb (never off it) to find its rise. - Ribbons, not strokes. Limbs are filled ribbons sampled along a cubic and offset along the curve normal, so a junction stays solid even when a zoomed-out view squeezes it almost flat. Shading runs dark → colour → dark across each limb for a rounded, woody read.
- Greedy label declutter. Every visible movement asks for a name; closest to the trunk wins, and names that would collide with a placed one — or fall off the canvas — stay hidden until you zoom in on them.
- Hover lights the descent line. Hovering a movement brightens its whole path back to the root (grafts included) and dims the rest — the fastest way to read "where did this come from".
Clicking any limb or label opens the movement's artist picker and then its 3D movement gallery, exactly as the other two layouts do.
Tuning
All constants sit at the top of the two files and are safe to tune:
| Constant | File | Effect |
|---|---|---|
LEAF_SLOT_PX, LIMB_GAP_PX |
movementTree.ts |
how far apart branches sit |
MIN_LIMB_PX, MAX_OWN_LIMB_PX, MAX_TRUNK_PX |
movementTree.ts |
thickness range |
LEAN_SLACK, MAX_LEAN_PX |
movementTree.ts |
how much limbs bend outward |
FULL_VIEW_WIDTH_SHARE, ZOOM_SPREAD_EXPONENT, MAX_FIT_BOOST |
MovementTree.tsx |
crown spread vs. zoom |
MIN_LIMB_RISE_PX, MIN_JUNCTION_RISE_PX, MAX_THICKNESS_OF_LENGTH |
MovementTree.tsx |
crown legibility at full zoom-out |
New lineage edges only need adding to
client/src/data/movement-lineage.ts — the tree
picks up parents, grafts, thickness and spacing from there automatically.