vireuikit 0.1.0 → 0.3.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.
- package/README.md +27 -0
- package/dist/{chunk-ZFX2MLKK.js → chunk-2AFVQ65S.js} +240 -18
- package/dist/chunk-2AFVQ65S.js.map +1 -0
- package/dist/dynamics-7Bq3LLds.d.cts +271 -0
- package/dist/dynamics-7Bq3LLds.d.ts +271 -0
- package/dist/index.cjs +224 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/web/index.cjs +340 -126
- package/dist/web/index.cjs.map +1 -1
- package/dist/web/index.d.cts +1 -1
- package/dist/web/index.d.ts +1 -1
- package/dist/web/index.js +21 -7
- package/dist/web/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-ZFX2MLKK.js.map +0 -1
- package/dist/dynamics-BqBoj03x.d.cts +0 -160
- package/dist/dynamics-BqBoj03x.d.ts +0 -160
|
@@ -0,0 +1,271 @@
|
|
|
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 };
|
package/dist/index.cjs
CHANGED
|
@@ -285,14 +285,29 @@ var MEDIUM_DEFAULTS = {
|
|
|
285
285
|
condensationFloor: 0.12,
|
|
286
286
|
evaporationRate: 0.2,
|
|
287
287
|
condensateSettleSpeed: 8,
|
|
288
|
+
condensateSettleWiggle: 0.4,
|
|
288
289
|
condensateSpreadRate: 0.08,
|
|
289
290
|
condensateTint: 0.2,
|
|
290
291
|
condensateGain: 5,
|
|
291
292
|
condensateColor: [0.94, 0.95, 0.97],
|
|
292
293
|
channelColors: neutralSpecies(BASE_COLOR),
|
|
293
294
|
baseColor: BASE_COLOR,
|
|
294
|
-
baseWeight: 0.9
|
|
295
|
+
baseWeight: 0.9,
|
|
296
|
+
depthFarScale: 1.6,
|
|
297
|
+
depthFarOffsetFrac: [0.47, 0.53],
|
|
298
|
+
depthFarBlurRadius: 5,
|
|
299
|
+
depthFarWeight: 0.5,
|
|
300
|
+
gravityVaporDrift: 0.6,
|
|
301
|
+
gravityBottomBoost: 2,
|
|
302
|
+
gravityBottomBoostStart: 0.75
|
|
295
303
|
};
|
|
304
|
+
function depthFarOffsetPx(offsetFrac, gridWidth, gridHeight) {
|
|
305
|
+
return [offsetFrac[0] * gridWidth, offsetFrac[1] * gridHeight];
|
|
306
|
+
}
|
|
307
|
+
function wrappedDistanceFromZero(value, period) {
|
|
308
|
+
const wrapped = (value % period + period) % period;
|
|
309
|
+
return Math.min(wrapped, period - wrapped);
|
|
310
|
+
}
|
|
296
311
|
var MEDIUM_PROBE_FLOOR_PX = 22;
|
|
297
312
|
var MEDIUM_DEFAULT_CELL_PX = 26;
|
|
298
313
|
var MEDIUM_TRACK_PRESETS = {
|
|
@@ -337,6 +352,8 @@ var MEDIUM_NATURAL_INTERVAL = 25;
|
|
|
337
352
|
var MEDIUM_NATURAL_JITTER = 35;
|
|
338
353
|
var MEDIUM_SENSITIVE_TOP = 0.55;
|
|
339
354
|
var MEDIUM_SENSITIVE_BOTTOM = 0.95;
|
|
355
|
+
var MEDIUM_TRACK_DEPTH_WIDTH_RANGE = [0.55, 1];
|
|
356
|
+
var MEDIUM_TRACK_DEPTH_INTENSITY_RANGE = [0.4, 1];
|
|
340
357
|
|
|
341
358
|
// src/medium/noise.ts
|
|
342
359
|
var VG_VALUE_NOISE = `
|
|
@@ -468,6 +485,71 @@ float2 vgCurlVelocity(float2 p, float phase) {
|
|
|
468
485
|
float py0 = vgPotential(p - float2(0.0, e), phase);
|
|
469
486
|
return float2(py1 - py0, -(px1 - px0)) / (2.0 * e);
|
|
470
487
|
}
|
|
488
|
+
|
|
489
|
+
// vgPotential above is periodic in TIME only: the simulation domain is a torus (gl.REPEAT), but
|
|
490
|
+
// the lattice itself never wraps in x/y, so velocity jumps at the domain seam and dye on either
|
|
491
|
+
// side of it is transported differently \u2014 a straight seam, made worse wherever something re-reads
|
|
492
|
+
// the field at a different scale (the far depth plane's tile boundary). vgValueNoise3Seamless
|
|
493
|
+
// wraps ALL THREE lattice axes: xyPeriod/zPeriod fold BOTH neighbors of a cell into their own
|
|
494
|
+
// period before hashing, so the field is EXACTLY the same one period over, in space exactly as it
|
|
495
|
+
// already was in time.
|
|
496
|
+
//
|
|
497
|
+
// The period has to be an integer number of lattice cells, or the wrap would jump instead of
|
|
498
|
+
// closing seamlessly (the same requirement MEDIUM_TIME_OCTAVES/mediumOctavePeriod enforces for
|
|
499
|
+
// time) \u2014 but unlike time, the spatial period depends on the GRID SIZE, which this shader has no
|
|
500
|
+
// notion of. So xyPeriod travels together with a per-octave spatial frequency the CPU computes to
|
|
501
|
+
// make gridSize * frequency land on that exact integer (see computeMediumSpatialPeriods below);
|
|
502
|
+
// both arrive together as one uniform per octave (u_spatialN.xy = frequency, .zw = period),
|
|
503
|
+
// recomputed by web/medium.ts whenever the grid resizes.
|
|
504
|
+
float vgValueNoise3Seamless(float3 p, float2 xyPeriod, float zPeriod) {
|
|
505
|
+
float3 i = floor(p);
|
|
506
|
+
float3 f = fract(p);
|
|
507
|
+
float3 u = f * f * (3.0 - 2.0 * f);
|
|
508
|
+
float x0 = mod(i.x, xyPeriod.x);
|
|
509
|
+
float x1 = mod(i.x + 1.0, xyPeriod.x);
|
|
510
|
+
float y0 = mod(i.y, xyPeriod.y);
|
|
511
|
+
float y1 = mod(i.y + 1.0, xyPeriod.y);
|
|
512
|
+
float z0 = mod(i.z, zPeriod);
|
|
513
|
+
float z1 = mod(i.z + 1.0, zPeriod);
|
|
514
|
+
float n000 = vgHash31(float3(x0, y0, z0));
|
|
515
|
+
float n100 = vgHash31(float3(x1, y0, z0));
|
|
516
|
+
float n010 = vgHash31(float3(x0, y1, z0));
|
|
517
|
+
float n110 = vgHash31(float3(x1, y1, z0));
|
|
518
|
+
float n001 = vgHash31(float3(x0, y0, z1));
|
|
519
|
+
float n101 = vgHash31(float3(x1, y0, z1));
|
|
520
|
+
float n011 = vgHash31(float3(x0, y1, z1));
|
|
521
|
+
float n111 = vgHash31(float3(x1, y1, z1));
|
|
522
|
+
float nx00 = mix(n000, n100, u.x);
|
|
523
|
+
float nx10 = mix(n010, n110, u.x);
|
|
524
|
+
float nx01 = mix(n001, n101, u.x);
|
|
525
|
+
float nx11 = mix(n011, n111, u.x);
|
|
526
|
+
float nxy0 = mix(nx00, nx10, u.y);
|
|
527
|
+
float nxy1 = mix(nx01, nx11, u.y);
|
|
528
|
+
return mix(nxy0, nxy1, u.z) * 2.0 - 1.0;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Same four-octave fbm as vgPotential, but every octave reads the seamless lattice above at its
|
|
532
|
+
// OWN (frequency, period) pair \u2014 sN.xy replaces that octave's compile-time \`scale\` constant (it
|
|
533
|
+
// has to be a uniform now: the adjustment depends on the live grid size), sN.zw is the matching
|
|
534
|
+
// integer period. \`p\` is still curlFreq-scaled exactly as vgPotential's caller scales it \u2014 only
|
|
535
|
+
// the per-octave multiplier changed from a constant to a grid-dependent uniform, so the finite
|
|
536
|
+
// difference in vgCurlVelocitySeamless below needs no epsilon change of its own.
|
|
537
|
+
float vgPotentialSeamless(float2 p, float phase, float4 s0, float4 s1, float4 s2, float4 s3) {
|
|
538
|
+
float v = vgValueNoise3Seamless(float3(p * s0.xy, phase * ${VG_OCT0.rate} + ${VG_OCT0.offset}), s0.zw, ${VG_OCT0.period}) * 0.5;
|
|
539
|
+
v += vgValueNoise3Seamless(float3(p * s1.xy, phase * ${VG_OCT1.rate} + ${VG_OCT1.offset}), s1.zw, ${VG_OCT1.period}) * 0.25;
|
|
540
|
+
v += vgValueNoise3Seamless(float3(p * s2.xy, phase * ${VG_OCT2.rate} + ${VG_OCT2.offset}), s2.zw, ${VG_OCT2.period}) * 0.125;
|
|
541
|
+
float macro = vgValueNoise3Seamless(float3(p * s3.xy, phase * ${VG_OCT3.rate} + ${VG_OCT3.offset}), s3.zw, ${VG_OCT3.period});
|
|
542
|
+
return v * (0.7 + 0.3 * macro);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
float2 vgCurlVelocitySeamless(float2 p, float phase, float4 s0, float4 s1, float4 s2, float4 s3) {
|
|
546
|
+
float e = 0.6;
|
|
547
|
+
float px1 = vgPotentialSeamless(p + float2(e, 0.0), phase, s0, s1, s2, s3);
|
|
548
|
+
float px0 = vgPotentialSeamless(p - float2(e, 0.0), phase, s0, s1, s2, s3);
|
|
549
|
+
float py1 = vgPotentialSeamless(p + float2(0.0, e), phase, s0, s1, s2, s3);
|
|
550
|
+
float py0 = vgPotentialSeamless(p - float2(0.0, e), phase, s0, s1, s2, s3);
|
|
551
|
+
return float2(py1 - py0, -(px1 - px0)) / (2.0 * e);
|
|
552
|
+
}
|
|
471
553
|
`;
|
|
472
554
|
|
|
473
555
|
// src/medium/seed-shader.ts
|
|
@@ -511,21 +593,36 @@ uniform float u_phase;
|
|
|
511
593
|
uniform float u_curlFreq;
|
|
512
594
|
uniform float u_advectSpeed;
|
|
513
595
|
uniform float u_turbulence;
|
|
596
|
+
// One float4 per MEDIUM_TIME_OCTAVES entry: xy is that octave's seamless spatial frequency, zw its
|
|
597
|
+
// matching integer lattice period \u2014 both grid-size-dependent, so they can't be compile-time
|
|
598
|
+
// constants (see vgPotentialSeamless/computeMediumSpatialPeriods in noise.ts). Recomputed by
|
|
599
|
+
// web/medium.ts's bindVelocityUniforms every frame from the live grid dimensions.
|
|
600
|
+
uniform float4 u_spatial0;
|
|
601
|
+
uniform float4 u_spatial1;
|
|
602
|
+
uniform float4 u_spatial2;
|
|
603
|
+
uniform float4 u_spatial3;
|
|
514
604
|
|
|
515
605
|
${VG_CURL_NOISE}
|
|
516
606
|
`;
|
|
517
607
|
var MEDIUM_ADVECT_VELOCITY = `
|
|
518
|
-
float2
|
|
608
|
+
float2 curl = vgCurlVelocitySeamless(xy * u_curlFreq, u_phase, u_spatial0, u_spatial1, u_spatial2, u_spatial3);
|
|
609
|
+
float2 vel = curl * u_advectSpeed * u_turbulence;
|
|
519
610
|
`;
|
|
520
611
|
var MEDIUM_ADVECT_VELOCITY_SETTLE = `
|
|
521
612
|
${MEDIUM_ADVECT_VELOCITY}
|
|
522
613
|
vel.y += u_settleSpeed;
|
|
614
|
+
vel.x += curl.x * u_advectSpeed * u_settleWiggle;
|
|
615
|
+
`;
|
|
616
|
+
var MEDIUM_ADVECT_VELOCITY_VAPOR = `
|
|
617
|
+
${MEDIUM_ADVECT_VELOCITY}
|
|
618
|
+
vel.y += u_vaporDrift;
|
|
523
619
|
`;
|
|
524
620
|
var MEDIUM_VAPOR_FORWARD_SHADER = `
|
|
525
621
|
${MEDIUM_ADVECT_UNIFORMS}
|
|
622
|
+
uniform float u_vaporDrift;
|
|
526
623
|
|
|
527
624
|
half4 main(float2 xy) {
|
|
528
|
-
${
|
|
625
|
+
${MEDIUM_ADVECT_VELOCITY_VAPOR}
|
|
529
626
|
float2 src = xy - vel * u_dt;
|
|
530
627
|
return u_dye.eval(float2(src.x, u_dyeSize.y - src.y));
|
|
531
628
|
}
|
|
@@ -533,6 +630,7 @@ ${MEDIUM_ADVECT_VELOCITY}
|
|
|
533
630
|
var MEDIUM_CONDENSATE_FORWARD_SHADER = `
|
|
534
631
|
${MEDIUM_ADVECT_UNIFORMS}
|
|
535
632
|
uniform float u_settleSpeed;
|
|
633
|
+
uniform float u_settleWiggle;
|
|
536
634
|
|
|
537
635
|
half4 main(float2 xy) {
|
|
538
636
|
${MEDIUM_ADVECT_VELOCITY_SETTLE}
|
|
@@ -561,9 +659,10 @@ var MEDIUM_MACCORMACK_CORRECT = `
|
|
|
561
659
|
var MEDIUM_VAPOR_CORRECT_SHADER = `
|
|
562
660
|
${MEDIUM_ADVECT_UNIFORMS}
|
|
563
661
|
uniform shader u_forward;
|
|
662
|
+
uniform float u_vaporDrift;
|
|
564
663
|
|
|
565
664
|
half4 main(float2 xy) {
|
|
566
|
-
${
|
|
665
|
+
${MEDIUM_ADVECT_VELOCITY_VAPOR}
|
|
567
666
|
${MEDIUM_MACCORMACK_CORRECT}
|
|
568
667
|
return limited;
|
|
569
668
|
}
|
|
@@ -571,6 +670,7 @@ ${MEDIUM_MACCORMACK_CORRECT}
|
|
|
571
670
|
var MEDIUM_CONDENSATE_CORRECT_SHADER = `
|
|
572
671
|
${MEDIUM_ADVECT_UNIFORMS}
|
|
573
672
|
uniform float u_settleSpeed;
|
|
673
|
+
uniform float u_settleWiggle;
|
|
574
674
|
uniform shader u_forward;
|
|
575
675
|
|
|
576
676
|
half4 main(float2 xy) {
|
|
@@ -670,6 +770,39 @@ ${MEDIUM_ADVECT_VELOCITY}
|
|
|
670
770
|
`;
|
|
671
771
|
|
|
672
772
|
// src/medium/composite-shader.ts
|
|
773
|
+
var VG_CUBIC_WEIGHTS = `
|
|
774
|
+
float4 vgCubicWeights(float v) {
|
|
775
|
+
float4 n = float4(1.0, 2.0, 3.0, 4.0) - v;
|
|
776
|
+
float4 s = n * n * n;
|
|
777
|
+
float x = s.x;
|
|
778
|
+
float y = s.y - 4.0 * s.x;
|
|
779
|
+
float z = s.z - 4.0 * s.y + 6.0 * s.x;
|
|
780
|
+
float w = 6.0 - x - y - z;
|
|
781
|
+
return float4(x, y, z, w) * (1.0 / 6.0);
|
|
782
|
+
}
|
|
783
|
+
`;
|
|
784
|
+
function vgBicubicSampler(fnName, textureName) {
|
|
785
|
+
return `
|
|
786
|
+
half4 ${fnName}(float2 px) {
|
|
787
|
+
float2 shifted = px - 0.5;
|
|
788
|
+
float2 fxy = fract(shifted);
|
|
789
|
+
float2 base = shifted - fxy;
|
|
790
|
+
float4 xw = vgCubicWeights(fxy.x);
|
|
791
|
+
float4 yw = vgCubicWeights(fxy.y);
|
|
792
|
+
float2 sx = float2(xw.x + xw.y, xw.z + xw.w);
|
|
793
|
+
float2 sy = float2(yw.x + yw.y, yw.z + yw.w);
|
|
794
|
+
float2 offsetX = float2(base.x - 0.5 + xw.y / sx.x, base.x + 1.5 + xw.w / sx.y);
|
|
795
|
+
float2 offsetY = float2(base.y - 0.5 + yw.y / sy.x, base.y + 1.5 + yw.w / sy.y);
|
|
796
|
+
half4 s00 = ${textureName}.eval(float2(offsetX.x, offsetY.x));
|
|
797
|
+
half4 s10 = ${textureName}.eval(float2(offsetX.y, offsetY.x));
|
|
798
|
+
half4 s01 = ${textureName}.eval(float2(offsetX.x, offsetY.y));
|
|
799
|
+
half4 s11 = ${textureName}.eval(float2(offsetX.y, offsetY.y));
|
|
800
|
+
float wx = sx.x / (sx.x + sx.y);
|
|
801
|
+
float wy = sy.x / (sy.x + sy.y);
|
|
802
|
+
return mix(mix(s11, s01, wx), mix(s10, s00, wx), wy);
|
|
803
|
+
}
|
|
804
|
+
`;
|
|
805
|
+
}
|
|
673
806
|
var MEDIUM_COMPOSITE_SHADER = `
|
|
674
807
|
uniform shader u_vapor;
|
|
675
808
|
uniform shader u_condensate;
|
|
@@ -683,17 +816,71 @@ uniform float3 u_labCondensate;
|
|
|
683
816
|
uniform float u_baseWeight;
|
|
684
817
|
uniform float u_condensateTint;
|
|
685
818
|
uniform float u_condensateGain;
|
|
819
|
+
uniform float u_farScale;
|
|
820
|
+
uniform float2 u_farOffset;
|
|
821
|
+
uniform float u_farBlurRadius;
|
|
822
|
+
uniform float u_farWeight;
|
|
823
|
+
uniform float u_gravityBoost;
|
|
824
|
+
uniform float u_gravityBoostStart;
|
|
686
825
|
|
|
687
826
|
${VG_OKLAB_TO_SRGB}
|
|
827
|
+
${VG_CUBIC_WEIGHTS}
|
|
828
|
+
${vgBicubicSampler("vgBicubicVapor", "u_vapor")}
|
|
829
|
+
${vgBicubicSampler("vgBicubicCondensate", "u_condensate")}
|
|
688
830
|
|
|
689
831
|
half4 main(float2 xy) {
|
|
690
|
-
float2
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
832
|
+
float2 baseSrc = xy * u_dyeScale;
|
|
833
|
+
|
|
834
|
+
// Near plane \u2014 the calibrated look lives here. Bicubic (see the file header) on vapor and
|
|
835
|
+
// condensate; track stays a plain bilinear read.
|
|
836
|
+
half4 vapor = vgBicubicVapor(baseSrc);
|
|
837
|
+
half4 condensate = vgBicubicCondensate(baseSrc);
|
|
838
|
+
half4 track = u_track.eval(baseSrc);
|
|
839
|
+
|
|
840
|
+
// Far plane \u2014 the identical field, scaled and offset (see the file header). An 8-tap ring, not
|
|
841
|
+
// the near plane's \xB1x/\xB1y box: axis-aligned taps read the SAME column/row on both sides of any
|
|
842
|
+
// residual axis-aligned structure (a seam, a resize artifact), which is exactly how a straight
|
|
843
|
+
// line gets doubled into four parallel copies instead of softened away \u2014 measured directly via
|
|
844
|
+
// check-medium.mjs's isotropy gate (see its report). The ring is rotated a half-step (22.5\xB0) off
|
|
845
|
+
// the axes so no tap direction ever coincides with a grid row or column. Radius stays in the same
|
|
846
|
+
// far-plane texture-space unit as before (u_farBlurRadius, grid-px) \u2014 only the tap PATTERN
|
|
847
|
+
// changed, not what it measures in.
|
|
848
|
+
float2 farSrc = baseSrc * u_farScale + u_farOffset;
|
|
849
|
+
float2 ring0 = float2( 0.9238795, 0.3826834) * u_farBlurRadius;
|
|
850
|
+
float2 ring1 = float2( 0.3826834, 0.9238795) * u_farBlurRadius;
|
|
851
|
+
float2 ring2 = float2(-0.3826834, 0.9238795) * u_farBlurRadius;
|
|
852
|
+
float2 ring3 = float2(-0.9238795, 0.3826834) * u_farBlurRadius;
|
|
853
|
+
float2 ring4 = float2(-0.9238795, -0.3826834) * u_farBlurRadius;
|
|
854
|
+
float2 ring5 = float2(-0.3826834, -0.9238795) * u_farBlurRadius;
|
|
855
|
+
float2 ring6 = float2( 0.3826834, -0.9238795) * u_farBlurRadius;
|
|
856
|
+
float2 ring7 = float2( 0.9238795, -0.3826834) * u_farBlurRadius;
|
|
857
|
+
half4 vaporFar = (u_vapor.eval(farSrc + ring0) + u_vapor.eval(farSrc + ring1) +
|
|
858
|
+
u_vapor.eval(farSrc + ring2) + u_vapor.eval(farSrc + ring3) +
|
|
859
|
+
u_vapor.eval(farSrc + ring4) + u_vapor.eval(farSrc + ring5) +
|
|
860
|
+
u_vapor.eval(farSrc + ring6) + u_vapor.eval(farSrc + ring7)) * half4(0.125);
|
|
861
|
+
half4 condensateFar = (u_condensate.eval(farSrc + ring0) + u_condensate.eval(farSrc + ring1) +
|
|
862
|
+
u_condensate.eval(farSrc + ring2) + u_condensate.eval(farSrc + ring3) +
|
|
863
|
+
u_condensate.eval(farSrc + ring4) + u_condensate.eval(farSrc + ring5) +
|
|
864
|
+
u_condensate.eval(farSrc + ring6) + u_condensate.eval(farSrc + ring7)) * half4(0.125);
|
|
865
|
+
|
|
866
|
+
// Gravity's "denser layer at the bottom of the chamber" \u2014 a compositing-only boost (see
|
|
867
|
+
// MEDIUM_DEFAULTS.gravityBottomBoost): it scales how density already in the conserved buffers is
|
|
868
|
+
// DISPLAYED, never the buffers themselves, so it can't threaten water conservation. Shared by
|
|
869
|
+
// both planes: the chamber's floor is denser everywhere along its depth, not just up close.
|
|
870
|
+
//
|
|
871
|
+
// Verified with a headless render, not assumed: ramping this with xy.y directly lit up the TOP
|
|
872
|
+
// of the frame, not the bottom (contentTexture's own orientation contract flips y once more
|
|
873
|
+
// between this pass and the final scene \u2014 see the file header) \u2014 hence the 1.0 - below.
|
|
874
|
+
float bottomFrac = 1.0 - xy.y / u_resolution.y;
|
|
875
|
+
float bottomT = smoothstep(u_gravityBoostStart, 1.0, bottomFrac);
|
|
876
|
+
float bottomBoost = 1.0 + u_gravityBoost * bottomT;
|
|
877
|
+
|
|
878
|
+
float gasR = (float(vapor.r) + float(vaporFar.r) * u_farWeight) * bottomBoost;
|
|
879
|
+
float gasG = (float(vapor.g) + float(vaporFar.g) * u_farWeight) * bottomBoost;
|
|
880
|
+
float gasB = (float(vapor.b) + float(vaporFar.b) * u_farWeight) * bottomBoost;
|
|
881
|
+
float w0 = gasR + float(track.r);
|
|
882
|
+
float w1 = gasG + float(track.g);
|
|
883
|
+
float w2 = gasB + float(track.b);
|
|
697
884
|
float wBg = u_baseWeight;
|
|
698
885
|
|
|
699
886
|
// A faint condensate hint from the LOCAL vapor, not its own color (droplets scatter light
|
|
@@ -701,7 +888,8 @@ half4 main(float2 xy) {
|
|
|
701
888
|
float vaporLocal = max(w0 + w1 + w2, 1e-4);
|
|
702
889
|
float3 vaporHue = (u_lab0 * w0 + u_lab1 * w1 + u_lab2 * w2) / vaporLocal;
|
|
703
890
|
float3 condensateLab = mix(u_labCondensate, vaporHue, u_condensateTint);
|
|
704
|
-
float wCondensate =
|
|
891
|
+
float wCondensate =
|
|
892
|
+
(float(condensate.r) + float(condensateFar.r) * u_farWeight) * u_condensateGain * bottomBoost;
|
|
705
893
|
|
|
706
894
|
float total = max(w0 + w1 + w2 + wBg + wCondensate, 1e-4);
|
|
707
895
|
float3 mixLab =
|
|
@@ -761,15 +949,31 @@ function channelFromSeed(seed) {
|
|
|
761
949
|
const idx = Math.floor(hash01(seed * 1.37) * 3) % 3;
|
|
762
950
|
return idx === 0 ? [1, 0, 0] : idx === 1 ? [0, 1, 0] : [0, 0, 1];
|
|
763
951
|
}
|
|
952
|
+
function lerp(range, t) {
|
|
953
|
+
return range[0] + (range[1] - range[0]) * t;
|
|
954
|
+
}
|
|
955
|
+
function applyTrackDepth(preset, depth) {
|
|
956
|
+
const widthScale = lerp(MEDIUM_TRACK_DEPTH_WIDTH_RANGE, depth);
|
|
957
|
+
const intensityScale = lerp(MEDIUM_TRACK_DEPTH_INTENSITY_RANGE, depth);
|
|
958
|
+
return {
|
|
959
|
+
...preset,
|
|
960
|
+
headWidthFrac: preset.headWidthFrac * widthScale,
|
|
961
|
+
tailWidthFrac: preset.tailWidthFrac * widthScale,
|
|
962
|
+
intensity: preset.intensity * intensityScale
|
|
963
|
+
};
|
|
964
|
+
}
|
|
764
965
|
function buildEmission(presetName, source, seed, intensityScale) {
|
|
765
966
|
const preset = MEDIUM_TRACK_PRESETS[presetName];
|
|
967
|
+
const depth = hash01(seed * 4.63);
|
|
968
|
+
const depthApplied = applyTrackDepth(preset, depth);
|
|
766
969
|
return {
|
|
767
|
-
...
|
|
768
|
-
intensity:
|
|
970
|
+
...depthApplied,
|
|
971
|
+
intensity: depthApplied.intensity * intensityScale,
|
|
769
972
|
source,
|
|
770
973
|
angle: hash01(seed * 7.31) * Math.PI * 2,
|
|
771
974
|
seed,
|
|
772
|
-
channel: channelFromSeed(seed)
|
|
975
|
+
channel: channelFromSeed(seed),
|
|
976
|
+
depth
|
|
773
977
|
};
|
|
774
978
|
}
|
|
775
979
|
function createMediumDynamics() {
|
|
@@ -845,6 +1049,8 @@ exports.MEDIUM_SPECIES_HUE_SPREAD = MEDIUM_SPECIES_HUE_SPREAD;
|
|
|
845
1049
|
exports.MEDIUM_SPECIES_LIGHTNESS = MEDIUM_SPECIES_LIGHTNESS;
|
|
846
1050
|
exports.MEDIUM_TIME_PERIOD = MEDIUM_TIME_PERIOD;
|
|
847
1051
|
exports.MEDIUM_TRACK_ADVECT_SHADER = MEDIUM_TRACK_ADVECT_SHADER;
|
|
1052
|
+
exports.MEDIUM_TRACK_DEPTH_INTENSITY_RANGE = MEDIUM_TRACK_DEPTH_INTENSITY_RANGE;
|
|
1053
|
+
exports.MEDIUM_TRACK_DEPTH_WIDTH_RANGE = MEDIUM_TRACK_DEPTH_WIDTH_RANGE;
|
|
848
1054
|
exports.MEDIUM_TRACK_PRESETS = MEDIUM_TRACK_PRESETS;
|
|
849
1055
|
exports.MEDIUM_TURBULENCE_TAU = MEDIUM_TURBULENCE_TAU;
|
|
850
1056
|
exports.MEDIUM_VAPOR_CORRECT_SHADER = MEDIUM_VAPOR_CORRECT_SHADER;
|
|
@@ -854,12 +1060,14 @@ exports.OKLAB_TO_LMS = OKLAB_TO_LMS;
|
|
|
854
1060
|
exports.VG_CURL_NOISE = VG_CURL_NOISE;
|
|
855
1061
|
exports.VG_OKLAB_TO_SRGB = VG_OKLAB_TO_SRGB;
|
|
856
1062
|
exports.VG_VALUE_NOISE = VG_VALUE_NOISE;
|
|
1063
|
+
exports.applyTrackDepth = applyTrackDepth;
|
|
857
1064
|
exports.atomAtRest = atomAtRest;
|
|
858
1065
|
exports.atomLift = atomLift;
|
|
859
1066
|
exports.atomMaterial = atomMaterial;
|
|
860
1067
|
exports.characterOf = characterOf;
|
|
861
1068
|
exports.createGrowthState = createGrowthState;
|
|
862
1069
|
exports.createMediumDynamics = createMediumDynamics;
|
|
1070
|
+
exports.depthFarOffsetPx = depthFarOffsetPx;
|
|
863
1071
|
exports.growthFrame = growthFrame;
|
|
864
1072
|
exports.growthInk = growthInk;
|
|
865
1073
|
exports.growthPhase = growthPhase;
|
|
@@ -870,5 +1078,6 @@ exports.speciesFamily = speciesFamily;
|
|
|
870
1078
|
exports.srgbToOklab = srgbToOklab;
|
|
871
1079
|
exports.stepGrowth = stepGrowth;
|
|
872
1080
|
exports.stepSpring = stepSpring;
|
|
1081
|
+
exports.wrappedDistanceFromZero = wrappedDistanceFromZero;
|
|
873
1082
|
//# sourceMappingURL=index.cjs.map
|
|
874
1083
|
//# sourceMappingURL=index.cjs.map
|