Files
Art-gallery/Documentation/movement-tree.md
T

6.5 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()

  1. Time grows upward. Oldest movements at the bottom, newest at the crown. Y is a plain year → pixel mapping; the layout engine never touches it.
  2. One trunk, at the centre. MOVEMENT_LINEAGE is 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.
  3. Extra parents become grafts. The predecessors that lost step 2 are still drawn — as thin, low-opacity limbs behind the tree — so Post-Impressionism → Cubism survives even though Cubism hangs structurally off Fauvism.
  4. 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.
  5. 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 its influence_link_count.
  6. Branches lean outward across their own lifespan, by at most the slack left inside their slot — organic, and collision-free by construction.
  7. 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

  1. 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 at MAX_FIT_BOOST). The chart grows as you walk up it. Whatever is on screen is always clamped to fit the canvas.
  2. 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_PX long, a junction climbs at least MIN_JUNCTION_RISE_PX before 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.
  3. 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.
  4. 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.
  5. 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.

Colours and captions

  • Limb fill/shading uses utils/movementColor.ts (vividMovementColor, shadeMovementColor) — the same helpers as the classic and vertical charts. Malformed catalogue hex falls back to the input string; values longer than six digits keep the first six (#rrggbbaa#rrggbb).
  • The chart hint under the canvas is captionTreeFlow in locales/{en,ru}/home.json (same pattern as the other layouts).

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.