vireuikit 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,271 +0,0 @@
1
- type VireUIKitMediumChannel = readonly [number, number, number];
2
- type VireUIKitMediumParams = {
3
- /** 1/s — how fast the TRACK density decays per second. Water (vapor+condensate) does not decay:
4
- * it is a conserved volume, only moving between phases and being transported. */
5
- decay: number;
6
- /** Spatial frequency of the curl-noise potential, 1/px of the simulation grid. */
7
- curlFreq: number;
8
- /** Drift speed of the noise domain over time, 1/s. */
9
- curlSpeed: number;
10
- /** Velocity field scale, grid-px/s — how far the smoke can be carried in a second. */
11
- advectSpeed: number;
12
- /** Turbulence gate, 0…1 (playback drives it: playing is alive, paused cools down). The
13
- * per-frame value is computed by `createMediumDynamics` (`dynamics.ts`) — it arrives here
14
- * already resolved. */
15
- turbulence: number;
16
- /** 1/s — the fraction of vapor EXCESS above `condensationFloor` that turns into condensate every
17
- * second from background seeding: weak, everywhere, all the time — the chamber is never
18
- * perfectly clean. */
19
- condensationRate: number;
20
- /** Vapor density (in seed-density units, a spot's peak ≈1) below which condensation does not
21
- * happen at all — only the excess above it condenses. Without a floor, the condensate would
22
- * exactly retrace the vapor spot's shape (the same outline, or softer from growth blur), and
23
- * the two phases would have nowhere to visually diverge: vapor is already soft, so the floor
24
- * concentrates condensate in dense turbulence cores, leaving the sparse vapor around it
25
- * uncondensed. Keep it NOTICEABLY below the settled mean vapor level (turbulence stirs spots
26
- * into a nearly flat background of ~0.3 over a few minutes — a known limit of the numerical
27
- * advection): a floor above that mean eventually stops finding any excess at all, and
28
- * condensation dies out for good. */
29
- condensationFloor: number;
30
- /** 1/s — the fraction of condensate evaporating back into vapor. Larger than the condensation
31
- * rate: at equilibrium most of the water stays vapor, which is the "supersaturated" one. */
32
- evaporationRate: number;
33
- /** grid-px/s — condensate drift downward, on top of the shared velocity field: gravity touches
34
- * droplets and leaves vapor alone. */
35
- condensateSettleSpeed: number;
36
- /** Multiplier on the ambient curl field's OWN x-component, added to settling UNCONDITIONALLY
37
- * (not gated by `turbulence` the way the shared velocity field is) — see
38
- * MEDIUM_ADVECT_VELOCITY_SETTLE in advect-shader.ts. Without it, a droplet falling at a constant
39
- * `condensateSettleSpeed` with only a turbulence-gated sideways push traces a near-straight
40
- * vertical line whenever turbulence is low (rest/paused: 0.045, over 20x below the gated push
41
- * needed to bend the fall) — many droplets in nearby columns then read as parallel vertical
42
- * streaks, one of the axis-aligned artifacts `check-medium.mjs`'s isotropy gate measures. Reuses
43
- * the SAME seamless curl field `vel` already reads rather than a second noise system, so it adds
44
- * no new seam of its own. Calibrated against that gate at the product's default grid (1920x952,
45
- * 74x37) — see the gate's own report for the measured ratio at this value. */
46
- condensateSettleWiggle: number;
47
- /** 1/s — the rate at which a droplet blends with its grid neighbors. There is no separate age
48
- * counter: the blend repeats every frame, and whatever has survived more frames ends up more
49
- * blended with its neighbors on its own — that IS "growth". Keep it NOTICEABLY below
50
- * `evaporationRate`: over a droplet's average lifetime (1/evaporationRate) only a small
51
- * fraction should get to blend — otherwise the blur outruns evaporation and the whole grid
52
- * levels into flat fog before a droplet gets a chance to return to vapor (visible by eye: the
53
- * contrast between "a patch of droplets" and the vapor background disappears within seconds). */
54
- condensateSpreadRate: number;
55
- /** 0…1 — how much of the local vapor hue shows through in the condensate. Droplets carry no
56
- * color of their own (droplets scatter light rather than tint it) — this is only a faint hint
57
- * of the surroundings. */
58
- condensateTint: number;
59
- /** Condensate's visibility multiplier in the Oklab mix relative to vapor at equal density —
60
- * droplets hold a small share of the water's mass, but they are mostly what's visible (as in a
61
- * real cloud: vapor is transparent, droplets create the visibility). */
62
- condensateGain: number;
63
- /** Near-white sRGB tone of droplets with no hue seeding (`condensateTint=0`) — droplets scatter
64
- * light rather than color it. */
65
- condensateColor: VireUIKitMediumChannel;
66
- /** Each vapor species' hue, sRGB 0…1. Derived as a family around one tone (`speciesFamily`),
67
- * not picked as three separate colors — see `species.ts`. */
68
- channelColors: readonly [VireUIKitMediumChannel, VireUIKitMediumChannel, VireUIKitMediumChannel];
69
- /** The platform's neutral tone — what the medium is, alone, with no source playing. */
70
- baseColor: VireUIKitMediumChannel;
71
- /** Background weight in the Oklab mix: keeps the platform tone visible even where the smoke is dense. */
72
- baseWeight: number;
73
- /** Field-sample scale for the far depth plane, k ≥ 1 (see `MEDIUM_COMPOSITE_SHADER`): the far
74
- * plane reads the SAME vapor/condensate field as the near plane, at `src * k + offset` instead
75
- * of `src`. A larger k folds more of the field into the same screen area, so the identical
76
- * evolving field reads as both finer-grained structure (features occupy fewer screen-px) and,
77
- * because the field's own motion maps to screen motion as v/k, slower apparent motion — parallax
78
- * and scale-of-structure from one number, with no second simulation. Measured against
79
- * `check-medium.mjs`'s parallax gate (a cross-correlation best-shift, not a centroid — see the
80
- * gate's own comment on why): 1.6 puts the far/near screen-speed ratio at 0.630, matching the
81
- * naive prediction 1/1.6 = 0.625 almost exactly, comfortably inside a band that reads as depth
82
- * without the far plane looking frozen. */
83
- depthFarScale: number;
84
- /** Offset into the shared field for the far plane, as a FRACTION of the grid's own width/height
85
- * (per axis) rather than a fixed grid-px value — the field wraps (`gl.REPEAT`), so a fixed
86
- * grid-px offset's EFFECTIVE (wrapped) distance from zero depends on whatever grid size it
87
- * happens to run at, and can land close to zero by accident at a size nobody tested against.
88
- * (This shipped once as `[41, 67]` grid-px: harmless at the 96px grid the depth gates tested
89
- * against, but 67 wraps to just 5px at the 72px grid height this package's own README uses as
90
- * its example — the far plane nearly re-read the near one, exactly the echo depth exists to
91
- * avoid.) A fraction keeps the wrapped distance proportional to the grid at any size — see
92
- * `depthFarOffsetPx` below and `check-medium.mjs`'s decorrelation gate, which measures this
93
- * directly at the README's own 128x72. */
94
- depthFarOffsetFrac: readonly [number, number];
95
- /** Grid-px radius of the far plane's 8-tap ring blur, rotated so no tap pair lies on a grid
96
- * axis. A spatial average cannot raise local
97
- * variance, so the same blur that softens the far plane's edges also, by construction, lowers
98
- * its measured contrast — one mechanism for both aerial-perspective cues (no separate contrast
99
- * knob exists). 0 disables it (the near plane stays sharp). Measured: reading the far plane's
100
- * own scale/offset with no blur already drops contrast ~6% (a fixed offset can land on a
101
- * locally denser or sparser patch of a field that isn't spatially uniform); this radius takes
102
- * the total to 24% — see the aerial-perspective gate, whose two thresholds isolate blur's own
103
- * share from that baseline. */
104
- depthFarBlurRadius: number;
105
- /** The far plane's share of the mix, relative to the near plane's implicit 1 — aerial
106
- * perspective's other half: even at equal density the far plane must not compete with the near
107
- * one for attention. */
108
- depthFarWeight: number;
109
- /** grid-px/s — a small downward drift added to VAPOR's own velocity, on top of the shared
110
- * curl-noise field (condensate already settles faster via `condensateSettleSpeed`). An order of
111
- * magnitude below it so the gas reads as "barely noticeable" drift rather than visibly falling.
112
- * ADDS NO DENSITY: on this periodic (`gl.REPEAT`) grid a uniform drift only ever translates the
113
- * field and wraps it back in at the top — it cannot accumulate mass anywhere, the way a real
114
- * floor would. It is a motion cue (which way is down), not a source of "denser at the bottom" —
115
- * that reading comes entirely from `gravityBottomBoost` below. See
116
- * `check-medium.mjs`'s vapor-drift gate, which checks DIRECTION over a short window rather than
117
- * a density ratio, for exactly this reason. */
118
- gravityVaporDrift: number;
119
- /** Density boost at the very bottom of the frame, added on top of 1 — a COMPOSITING-only effect:
120
- * it scales how the already-conserved vapor/condensate density is DISPLAYED, never the
121
- * simulated buffers themselves, so it cannot threaten the water-conservation invariant by
122
- * construction, and — unlike `gravityVaporDrift` above — this is the mechanism that actually
123
- * produces "denser at the bottom": it is a steady-state property of the compositing math, true
124
- * from the very first frame, independent of any warmup or mixing time (see
125
- * `check-medium.mjs`'s boost gate, which measures it with zero simulated steps for exactly that
126
- * reason). Large in absolute terms because `baseWeight` (0.9) dominates the Oklab mix wherever
127
- * gas density is thin — a small boost gets diluted into an invisible change in the final color;
128
- * measured this large to register as a real, visible band against that dilution. */
129
- gravityBottomBoost: number;
130
- /** Fraction of frame height (0 top, 1 bottom) where the bottom boost starts ramping in via
131
- * `smoothstep`. The brief asks for a denser LAYER at the floor of the chamber, not a boost that
132
- * reaches halfway up the frame — kept in the bottom quarter. */
133
- gravityBottomBoostStart: number;
134
- };
135
- declare const MEDIUM_DEFAULTS: VireUIKitMediumParams;
136
- /**
137
- * Resolves the far plane's fractional offset (`depthFarOffsetFrac`) into grid-px for a GIVEN grid
138
- * size — pure and exported so the wrap-safety it exists for is directly testable (see
139
- * `medium-depth-offset.test.ts`) without spinning up a WebGL2 context. Per-axis: `gridWidth` and
140
- * `gridHeight` are independent, and a non-square grid (the README's own 128x72 example) shouldn't
141
- * distort one axis relative to the other.
142
- */
143
- declare function depthFarOffsetPx(offsetFrac: readonly [number, number], gridWidth: number, gridHeight: number): readonly [number, number];
144
- /**
145
- * How far a value lands from the nearest wrap boundary (0 or `period`) on a periodic axis of the
146
- * given `period` — the quantity that actually matters for decorrelation, not the raw offset value
147
- * itself (a raw offset near the period is effectively a tiny one once the field wraps). Exported
148
- * for the same reason as `depthFarOffsetPx`: a plain number, testable without a GPU.
149
- */
150
- declare function wrappedDistanceFromZero(value: number, period: number): number;
151
- /** The material probe can't see texture finer than this: there is no gain in a denser simulation
152
- * grid than the probe's own floor. */
153
- declare const MEDIUM_PROBE_FLOOR_PX = 22;
154
- /** Device-px per simulation grid cell by default — noticeably coarser than the probe floor;
155
- * sharpening the detail visible right next to the glass is a later increment (emission and
156
- * palette). */
157
- declare const MEDIUM_DEFAULT_CELL_PX = 26;
158
- /**
159
- * The geometry of one track stamp (`emit-shader.ts`), in FRACTIONS of `min(gridW, gridH)` rather
160
- * than pixels — so a preset doesn't depend on the simulation grid's resolution. Two presets:
161
- * alpha is a thick, short, straight track; beta is a thin, long, ragged one. The third, natural,
162
- * is dim and short, for the occasional background track at rest (not tied to cover art).
163
- */
164
- type VireUIKitMediumTrackPreset = {
165
- lengthFrac: number;
166
- headWidthFrac: number;
167
- tailWidthFrac: number;
168
- /** Amplitude of the path's sideways noise, as a fraction of `min(gridW, gridH)`; 0 = a straight thread. */
169
- raggedFrac: number;
170
- raggedFreq: number;
171
- /** How far the tail has settled downward, as a fraction of `min(gridW, gridH)`. */
172
- settleFrac: number;
173
- /** How much dimmer the tail is than the head, 0…1. */
174
- tailDim: number;
175
- intensity: number;
176
- };
177
- declare const MEDIUM_TRACK_PRESETS: Readonly<Record<'alpha' | 'beta' | 'natural', VireUIKitMediumTrackPreset>>;
178
- /** Turbulence at rest/paused/with no active track — the "slow drift" baseline shared by all three
179
- * neutral states (they differ from each other in other ways, not in this number). */
180
- declare const MEDIUM_DRIFT_TURBULENCE = 0.045;
181
- /** Turbulence range during playback: the low end is a quiet stretch of the track, the high end is
182
- * peak amplitude. */
183
- declare const MEDIUM_PLAYING_TURBULENCE_RANGE: readonly [number, number];
184
- /** Turbulence smoothing time constant, seconds: both ramping up on play and cooling down on pause
185
- * go through this filter — smooth, never a jump. */
186
- declare const MEDIUM_TURBULENCE_TAU = 2.5;
187
- /** Emission cadence — once every this many beats, not on every beat: at 120–180 BPM that is
188
- * 0.5–0.75 Hz, an order of magnitude below the WCAG 2.3.1 photosensitivity ceiling (3 Hz). */
189
- declare const MEDIUM_EMISSION_BEATS = 4;
190
- /** Fraction of the beats that actually get an emission — breaks up the exact periodicity (a
191
- * metronome-perfect pulse reads as mechanical) without moving the average rate. */
192
- declare const MEDIUM_EMISSION_PROBABILITY = 0.55;
193
- /** Emergency floor between two emissions, seconds — a safeguard for the same WCAG 2.3.1
194
- * requirement, independent of `MEDIUM_EMISSION_BEATS`/BPM: even a mistake in the other constants
195
- * can't flash faster than this. */
196
- declare const MEDIUM_MIN_EMISSION_INTERVAL = 1.2;
197
- /** Average interval between solitary "background emission" tracks at rest, seconds, plus a random
198
- * `MEDIUM_NATURAL_JITTER` on top — rare, with no discernible rhythm. */
199
- declare const MEDIUM_NATURAL_INTERVAL = 25;
200
- declare const MEDIUM_NATURAL_JITTER = 35;
201
- /** The sensitive layer — fractions of height, top to bottom. Above it there isn't enough
202
- * supersaturation for a track to appear at all, so background emission is only ever visible here. */
203
- declare const MEDIUM_SENSITIVE_TOP = 0.55;
204
- declare const MEDIUM_SENSITIVE_BOTTOM = 0.95;
205
- /** Per-emission depth range applied to a track's width (`headWidthFrac`/`tailWidthFrac`), far…near.
206
- * The same knob reads as both "thin" and "soft": a Gaussian stamp's edge steepness scales with its
207
- * own width (`emit-shader.ts`'s only extent parameter), so shrinking it for a far track thins and
208
- * softens it in the same stroke — there is no second knob to hang an independent softness number
209
- * on. Floored at 0.55: below that a far `alpha` track's head narrows past what MacCormack's own
210
- * numerical smoothing already blurs it to, and the depth cue disappears into that noise floor. */
211
- declare const MEDIUM_TRACK_DEPTH_WIDTH_RANGE: readonly [number, number];
212
- /** Per-emission depth range applied to a track's intensity, far…near — "dim". Floored at 0.4, not
213
- * lower: a far track still has to clear `MEDIUM_DEFAULTS.condensationFloor`'s excess threshold
214
- * often enough to read as a track, or depth would look like tracks randomly failing to spawn
215
- * rather than fading into the distance. */
216
- declare const MEDIUM_TRACK_DEPTH_INTENSITY_RANGE: readonly [number, number];
217
-
218
- type VireUIKitMediumPlaybackState = 'idle' | 'playing' | 'paused' | 'stopped';
219
- type VireUIKitMediumTrackPresetName = keyof typeof MEDIUM_TRACK_PRESETS;
220
- /** The fully resolved geometry of one track stamp — the platform layer (`web/medium.ts`)
221
- * translates fractions into simulation-grid pixels and binds the uniforms, deciding nothing else:
222
- * which preset, where, and how bright is entirely this model's call. */
223
- type VireUIKitMediumEmission = VireUIKitMediumTrackPreset & {
224
- /** 0…1, a fraction of the grid on both axes — the source point (cover art while playing;
225
- * wherever it happens to land at rest). */
226
- source: readonly [number, number];
227
- /** Radians, direction from the source to the head. */
228
- angle: number;
229
- seed: number;
230
- channel: readonly [number, number, number];
231
- /** 0 (farthest) … 1 (nearest), random within the full range and deterministic from `seed` — which
232
- * plane the track reads as living on. Already folded into this emission's own `headWidthFrac`/
233
- * `tailWidthFrac`/`intensity` below (see `buildEmission`); carried here too so a caller inspecting
234
- * an emission can see why. */
235
- depth: number;
236
- };
237
- type VireUIKitMediumDynamicsInput = {
238
- state: VireUIKitMediumPlaybackState;
239
- /** Beats per minute of the active track; unread in states with no track. */
240
- bpm: number;
241
- /** 0…1 — a continuous volume envelope (e.g. from precomputed waveform peaks), NOT the beat itself. */
242
- amplitude: number;
243
- /** Seconds, a deterministic playback position (not wall-clock time) — the emission cadence has
244
- * to be derived from it rather than accumulating its own phase. */
245
- positionSeconds: number;
246
- /** 0…1 — the area of the active track's cover art that emission comes from while playing. */
247
- sourcePoint: readonly [number, number];
248
- dt: number;
249
- };
250
- type VireUIKitMediumDynamicsFrame = {
251
- turbulence: number;
252
- emissions: readonly VireUIKitMediumEmission[];
253
- };
254
- type VireUIKitMediumDynamics = {
255
- step(input: VireUIKitMediumDynamicsInput): VireUIKitMediumDynamicsFrame;
256
- };
257
- /**
258
- * Scales a track preset's width and intensity by depth (0 far … 1 near) — separated from
259
- * `buildEmission` below and exported so depth's effect on a preset is directly testable with two
260
- * plain numbers, independent of how a specific emission's depth got hashed from its seed. At
261
- * depth=1 this returns the preset's own numbers unchanged (both ranges' upper bound is 1).
262
- */
263
- declare function applyTrackDepth(preset: VireUIKitMediumTrackPreset, depth: number): VireUIKitMediumTrackPreset;
264
- /**
265
- * Stateful "playback dynamics" model: turbulence and emission scheduling per frame. Platform
266
- * neutral — the caller supplies the clock and the playback position (in a bench, that would be
267
- * its own render loop); it never reads `performance.now()` or the DOM itself.
268
- */
269
- declare function createMediumDynamics(): VireUIKitMediumDynamics;
270
-
271
- export { wrappedDistanceFromZero as A, MEDIUM_DEFAULTS as M, type VireUIKitMediumChannel as V, MEDIUM_DEFAULT_CELL_PX as a, MEDIUM_DRIFT_TURBULENCE as b, MEDIUM_EMISSION_BEATS as c, MEDIUM_EMISSION_PROBABILITY as d, MEDIUM_MIN_EMISSION_INTERVAL as e, MEDIUM_NATURAL_INTERVAL as f, MEDIUM_NATURAL_JITTER as g, MEDIUM_PLAYING_TURBULENCE_RANGE as h, MEDIUM_PROBE_FLOOR_PX as i, MEDIUM_SENSITIVE_BOTTOM as j, MEDIUM_SENSITIVE_TOP as k, MEDIUM_TRACK_DEPTH_INTENSITY_RANGE as l, MEDIUM_TRACK_DEPTH_WIDTH_RANGE as m, MEDIUM_TRACK_PRESETS as n, MEDIUM_TURBULENCE_TAU as o, type VireUIKitMediumDynamics as p, type VireUIKitMediumDynamicsFrame as q, type VireUIKitMediumDynamicsInput as r, type VireUIKitMediumEmission as s, type VireUIKitMediumParams as t, type VireUIKitMediumPlaybackState as u, type VireUIKitMediumTrackPreset as v, type VireUIKitMediumTrackPresetName as w, applyTrackDepth as x, createMediumDynamics as y, depthFarOffsetPx as z };