@xenosystem/time-core
v0.1.0
Published
The canonical XENO time model shared by the timeline family — integer XenoTick, a linear wall-clock XenoTimebase, the four label-only display formatters, and the dead-reckoning XenoPlayheadClock that renders 60fps from a 2-10Hz push. Zero dependencies, ze
Readme
@xenosystem/time-core
The canonical XENO time model. Integer ticks, a linear wall-clock timebase, four label-only display formatters, and a dead-reckoning playhead clock that renders 60 fps from a 2–10 Hz push. Zero dependencies, zero DOM.
Tier 0 infrastructure, alongside @xenosystem/data-core and @xenosystem/tree-core. Consumed by
xeno.core.transport, xeno.core.timeline, and (later) xeno.core.curve.
Why this is a package and not a copy
Every other shared helper in this catalog was deliberately copied rather than extracted —
throttle and virtualize live in several panels with attribution, and a shared find/comments
cursor helper was rejected outright. The test each time was whether the semantics matched, not
whether the shape did.
The time model passes that test in a way nothing else has. XENO TIMELINE PANELS - UNION SPEC.md §2
does not describe a helper that two panels happen to need; it locks one model for the whole
family. Transport and Timeline are on screen together, over the same ruler and the same
playhead. A formatter that drifts by one frame between two copies is not a cosmetic difference — it
is a transport and a ruler disagreeing about where the playhead is.
That is the same test that produced @xenosystem/tree-core, where tree and layers genuinely shared
semantics.
The model
XenoTick is an integer, always. Never a float.
The host supplies a XenoTimebase: { ticksPerSecond, display, fps?, dropFrame?, bpm?,
timeSignature?, tempoMap?, displayOrigin? }.
Ticks are linear wall-clock, which is what keeps zoom a single scalar (pxPerTick) and stops
hit-testing ever having to invert a tempo curve. Bars and beats are a ruler-formatting concern
— the formatter may consult tempoMap, but pixel geometry stays linear.
formatTimecode / timecodeToFrame are inverses (Duncan/Heidelberger drop-frame). Parse is the
half Motion's EDL round-trip needs. That parity gate has run (packages/time-core/test/motion-parity.test.ts
matches Motion's 29.97 drop-frame at 1800 / 17982 / 107892). Motion's local algorithm is gone;
engine/interchange/types.ts is a product-signature wrapper over this pair.
This package is on the umbrella: @xenosystem/core/time re-exports every symbol here
(xeno-core/src/time.ts → dist/time.*). Products should import @xenosystem/core/time.
Panels that already depend on @xenosystem/time-core keep that path — both resolve to this
source. Never a production file: dep of the inner package.
| Host | Bridge |
|---|---|
| Motion | tick = frame, tps = frameRate — preserves frame accuracy; never float seconds for an edit point |
| Sound | tick = round(sec × 48000), display: 'bars' |
| Pixel | tps = 1000 (milliseconds) |
| 3D | tick = frame, displayOrigin: 1 — the one 1-based host, and the origin is applied at format time only |
The playhead clock
60 fps without streaming 60 messages a second across a sandbox boundary.
The host pushes { playing, originTick, originTime, rateTicksPerSecond, loop? } at 2–10 Hz. The
panel runs a display-only RAF extrapolating tick = originTick + (now − originTime) × rate, snaps to
each push as it arrives, and clamps against the live duration every tick.
Dead reckoning is sandbox-safe, cheaper than polling, and incidentally fixes the 10–12 fps playhead stutter the reference implementation has today.
playing is host-authoritative. The panel never infers it.
Usage
// Products (Motion, Sound, …):
import { normalizeTimebase, extrapolateTick, formatTick } from '@xenosystem/core/time'
// Panels that already depend on the inner package may keep:
import { normalizeTimebase, extrapolateTick, formatTick } from '@xenosystem/time-core'
const timebase = normalizeTimebase({ ticksPerSecond: 30, display: 'timecode', fps: 30 })
const tick = extrapolateTick(clock, performance.now(), durationTicks)
const label = formatTick(tick, timebase) // '00:00:04:12'Compatibility
@xenosystem/panel-transport re-exports every symbol here, so its public API is unchanged and no existing
import path had to move.
