DATUM
← home

the method

How a creature is built

A formal account of the growth engine: from a tabular dataset to a rendered organism. Every creature shares this pipeline; only the data mapping changes between species.

The system is a deterministic generative model. Let the input be an ordered sequence of records r0, r1, …, rN−1, each carrying a timestamp and one or more measured fields. Growth proceeds in N discrete steps; step i consumes record ri and appends one vertex to a growing set of branches. The drawing is fully determined by the data and a fixed pseudo-random hash, so the same dataset always yields the same organism. The canvas is W×H = 720×560 with margin m = 30.

1 · channel extraction

Each record is reduced to four scalar channels and a shock flag. The assignment of source fields to channels is the only thing that varies per species; the mapping prefers a genuinely measured directional field for θ over any calendar-derived proxy.

θi = direction(ri) ∈ [0, 2π) — growth heading ℓi = energy(ri) ∈ [0, 1] — step length driver wi = mass(ri) ∈ [0, 1] — thickness driver hi = colour(ri) ∈ [0, 360) — hue (HSL) σi ∈ {0, 1} — shock indicator

Continuous channels are normalised by min–max scaling over the dataset, with values clamped to a robust percentile band (typically the 1st–99th) so a single outlier cannot dominate:

x̂ = clamp​( (x − xlo) / (xhi − xlo), 0, 1 )

2 · temporal aggregation

The model targets a legible range of roughly 400 ≤ N ≤ 1300 steps. Let R be the number of usable rows. Aggregation is chosen accordingly:

  • R > 1300: bin into days, weeks or months. Counts are summed per bin, levels are averaged: v̄bin = (1/|bin|) ∑ vt.
  • 400 ≤ R ≤ 1300: one row per step, no aggregation.
  • R < 400: linear interpolation between consecutive records with S substeps each, N ≈ (R−1)·S, so sparse data still yields a smooth curve. A lean dataset is allowed to grow a lean creature; richness is never fabricated.

3 · the growth recurrence

Growth is not a single walker but a set of active tips, each a branch with a position p = (x, y) and heading α. At every step each tip advances once. The heading is updated by summing several angular terms: a data-direction push, a weak phototropic correction, deterministic noise, and (per species) small drift terms. The exact coupling differs between species, since the directional field differs; the general shape of the update is

α′ = α + δdir(ri) + ρ·Δphoto + ηi + drift

δdir is the data-driven angular push, ρ the phototropic gain, ηi the noise. As one concrete instance, Datum ventosa maps wind direction through a sine to a push, δdir = 0.8·sin((r − 90°)), and uses ρ = 0.22; another species substitutes its own mapping.

The phototropic term keeps the organism from curling in on itself. It is computed against a running centroid (cx, cy), the mean of all vertices placed so far, updated incrementally after each step. Let φ = atan2(y − cy, x − cx) be the outward angle from the centroid; the correction Δphoto is the wrapped difference between φ and the current heading, nudging the tip outward:

c ← (c·n + p′) / (n+1), n ← n+1 (running mean) Δphoto = wrap( φ − α ) ∈ (−π, π]

The step length scales an energy channel into pixels, si = s0 + ℓi·Δs, and the tip advances by (sicosα′, sisinα′).

4 · branching and pruning

The tip set is dynamic. A shock can spawn a child branch at an offset angle, up to a cap (e.g. 14 simultaneous tips), so the organism splits where the data spiked. Conversely, weak tips are pruned: a tip is dropped with a probability rising as its mass channel falls and its branch generation deepens, provided more than a few tips remain. If every tip dies, a fresh one is seeded at the centroid.

pdeath = (1 − wi)·0.05 + g·0.012 (pruning probability)

Both branching and pruning draw their decisions from the deterministic hash of §8, so the topology is reproducible.

5 · containment (reflection + clamp)

No vertex or control point may leave the frame. The canonical rule, used by most species, reflects the heading when a proposed tip would cross a margin and recomputes the position:

x′ ∉ [m, W−m] → α′ ← π − α′ y′ ∉ [m, H−m] → α′ ← −α′

Some specimens use a softer variant that steers the heading vector away from the wall near the margin rather than reflecting it; the visible effect is the same. In all cases two hard guarantees follow: every tip is clamped to [m, W−m]×[m, H−m], every Bézier control point (§6) is clamped identically so the curve cannot bulge out, and a clip() rectangle on each frame is the final backstop.

6 · rendering: piecewise cubic Bézier

A branch is a polyline of vertices pk. Drawing straight segments would read as mechanical, so each segment pk → pk+1 is rendered as a cubic Bézier whose control points are Catmull–Rom-style tangents estimated from the neighbours pk−1 and pk+2 (clamped at the ends):

c1 = pk + (pk+1 − pk−1) / 6 c2 = pk+1 − (pk+2 − pk) / 6 B(t) = (1−t)³ pk + 3(1−t)²t·c1 + 3(1−t)t²·c2 + t³ pk+1

The factor 1/6 is the uniform Catmull–Rom tangent. Note this is the uniform parameterisation (not chordal or centripetal) and the tangents are not arc-length normalised, which is acceptable here because vertex spacing is already bounded by §3.

raw vertices cubic Bezier chain (C1)

Left: raw vertices with naive straight segments. Right: the same vertices as a C¹-continuous Bézier chain, with the two control handles of one segment shown.

7 · stroke weight, colour and age

Line width tapers from base to tip, thins with branch generation g, and swells with the mass channel; a shock adds a scar proportional to its severity ν:

frac = k / (L−1), taper = 1 − 0.7·frac w = (0.6 + 2.4·wk)·taper·(1 − 0.1·g) w ← w + 1.2 + 3.2·ν (if shock), w ← max(w, 0.4)

Colour is computed in HSL and converted to RGB. Crucially, two orthogonal axes are encoded: hue carries the colour data field, lightness carries age (old growth dark, young bright), so time and the colour datum never collide. Saturation is fixed mid-range; a shock saturates fully and brightens:

H = hi, L = 0.30 + 0.30·(i / N), S = 0.55 shock: S = 0.95, L ↑ · age: a = i / N

The HSL→RGB map is the standard piecewise transform with chroma C = (1 − |2L − 1|)·S, X = C·(1 − |(H/60) mod 2 − 1|), offset m = L − C/2.

8 · deterministic noise

All "randomness" is a reproducible hash of the step index, so an organism is identical on every run and every machine. No PRNG state, no seed drift:

rnd(s) = frac( sin(s · 12.9898) · 43758.5453 )

The classic sine-hash. The noise term ηi in §3 and the branch-split jitter both draw from this function, keyed on the step and branch indices.

9 · shocks and the glow field

A shock is a threshold crossing in a chosen field, typically the 95th percentile of the energy or shock channel, or a domain-natural bound (a storm norm, a crash limit, an excess-mortality ratio). Its severity ν ∈ [0,1] scales the scar (§7) and a warm radial glow drawn beneath the stroke with additive compositing, so the line stays crisp while the halo decays over the following 40 steps:

age = inow − ishock, recent = max(0, 1 − age/40) glow = (0.12 + 0.30·ν) · (0.35 + 0.65·recent) radius = 10 + 44·ν + 12·recent

The glow is rendered under the branches with globalCompositeOperation = "lighter"; the stroke is drawn afterwards in source-over, keeping edges sharp.

10 · death and the print frame

The terminal frame is treated as a print and must stay clean, so death carries no full-frame effect. Each branch tip receives a withered dot in the death colour, and the final step is a foreshortened stub rather than a complete shoot, encoding that growth was cut off, not finished:

rdot = max( 1.4, 1.8 + 1.8·wtip·(1 − 0.1·g) )

For a creature grown from a fixed dataset, the date of death equals the timestamp of the last record. Because the maker chooses when to export the data, the maker chooses the moment of death. That is the conceptual core, carried by a label beneath the canvas rather than by any alteration of the image.

11 · living specimens

Two creatures, the seismic and the rain-fed one, are not grown from a fixed file but from an open, still-growing source. They share the entire rendering and player above; only the data layer differs, and the difference is deliberate.

Death is no longer the maker's to set. A living creature is alive while its source keeps feeding it, and dies when the feed falls silent. The test is a property of the data, not of the network connection: compute the median interval between consecutive readings (median, not mean, to be robust against gaps), measure the age of the most recent reading, and declare death only when that age exceeds three times the median interval. The factor three is a heartbeat threshold, long enough to ignore a single missed reading, short enough to catch a real stop, and it scales itself to the source: a daily feed dies after about three days of silence, a monthly one after about three months.

alive ⇔ (now − tlast) ≤ 3 · median( Δt )

The test runs before the first frame, so the creature opens in the correct state rather than flickering from dead to alive. While alive, the status reads alive · last fed <date> and the creature grows to its full length with no withered dots and no stub; the death grammar of §10 appears only once the test fails.

To stay a single self-contained file while depending on a live source, each living specimen is a hybrid. It bakes in a full historical stem with its own parser, exactly like a dead creature. On opening, it issues one fetch() to the open source (USGS for quakes, Open-Meteo for weather, both CORS-enabled and key-free), pulls every reading since the stem boundary, and grows on to today. If the fetch fails, offline or the source moved, it silently falls back to the baked-in stem.

An honest caveat the work states plainly: the baked-in stem of both living specimens is currently synthetic, structurally faithful but invented, because the build environment had no open internet to fetch the real history. The live layer corrects this the moment the file is opened online, growing on genuine data from the stem boundary forward; but the history shown before that boundary is plausible, not measured.

read the full statement →

DATUM · Jurgen De Wever
jrgndwvr.be