vireuikit 0.1.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.
@@ -0,0 +1,967 @@
1
+ 'use strict';
2
+
3
+ var vireglass = require('vireglass');
4
+ var web = require('vireglass/web');
5
+
6
+ // src/medium/oklab.ts
7
+ var LINEAR_TO_LMS = [
8
+ [0.4122214708, 0.5363325363, 0.0514459929],
9
+ [0.2119034982, 0.6806995451, 0.1073969566],
10
+ [0.0883024619, 0.2817188376, 0.6299787005]
11
+ ];
12
+ var LMS_TO_OKLAB = [
13
+ [0.2104542553, 0.793617785, -0.0040720468],
14
+ [1.9779984951, -2.428592205, 0.4505937099],
15
+ [0.0259040371, 0.7827717662, -0.808675766]
16
+ ];
17
+ var OKLAB_TO_LMS = [
18
+ [1, 0.3963377774, 0.2158037573],
19
+ [1, -0.1055613458, -0.0638541728],
20
+ [1, -0.0894841775, -1.291485548]
21
+ ];
22
+ var LMS_TO_LINEAR = [
23
+ [4.0767416621, -3.3077115913, 0.2309699292],
24
+ [-1.2684380046, 2.6097574011, -0.3413193965],
25
+ [-0.0041960863, -0.7034186147, 1.707614701]
26
+ ];
27
+ var glsl = (v) => Number.isInteger(v) ? v.toFixed(1) : String(v);
28
+ var row = (m, i, vars) => m[i].map((k, j) => `${glsl(k)} * ${vars[j]}`).join(" + ");
29
+ var VG_OKLAB_TO_SRGB = `
30
+ float3 vgLinearToSrgb(float3 c) {
31
+ float3 lo = c * 12.92;
32
+ float3 hi = 1.055 * pow(max(c, float3(0.0)), float3(1.0 / 2.4)) - 0.055;
33
+ return mix(lo, hi, step(float3(0.0031308), c));
34
+ }
35
+
36
+ float3 vgOklabToLinear(float3 lab) {
37
+ float l_ = ${row(OKLAB_TO_LMS, 0, ["lab.x", "lab.y", "lab.z"])};
38
+ float m_ = ${row(OKLAB_TO_LMS, 1, ["lab.x", "lab.y", "lab.z"])};
39
+ float s_ = ${row(OKLAB_TO_LMS, 2, ["lab.x", "lab.y", "lab.z"])};
40
+ float l = l_ * l_ * l_;
41
+ float m = m_ * m_ * m_;
42
+ float s = s_ * s_ * s_;
43
+ return float3(
44
+ ${row(LMS_TO_LINEAR, 0, ["l", "m", "s"])},
45
+ ${row(LMS_TO_LINEAR, 1, ["l", "m", "s"])},
46
+ ${row(LMS_TO_LINEAR, 2, ["l", "m", "s"])}
47
+ );
48
+ }
49
+ `;
50
+ var apply = (m, v) => [
51
+ m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
52
+ m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
53
+ m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2]
54
+ ];
55
+ var srgbChannelToLinear = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
56
+ var linearChannelToSrgb = (c) => c <= 31308e-7 ? c * 12.92 : 1.055 * Math.pow(Math.max(c, 0), 1 / 2.4) - 0.055;
57
+ function srgbToOklab(rgb) {
58
+ const linear = rgb.map(srgbChannelToLinear);
59
+ const lms = apply(LINEAR_TO_LMS, linear);
60
+ return apply(LMS_TO_OKLAB, lms.map((v) => Math.cbrt(Math.max(v, 0))));
61
+ }
62
+ function oklabToSrgb(lab) {
63
+ const lms = apply(OKLAB_TO_LMS, lab).map((v) => v * v * v);
64
+ return apply(LMS_TO_LINEAR, lms).map(linearChannelToSrgb);
65
+ }
66
+
67
+ // src/medium/species.ts
68
+ var MEDIUM_SPECIES_LIGHTNESS = 0.44;
69
+ var MEDIUM_SPECIES_HUE_SPREAD = 32;
70
+ var clamp01 = (v) => Math.min(1, Math.max(0, v));
71
+ function characterOf(color) {
72
+ const [lightness, a, b] = srgbToOklab(color);
73
+ return {
74
+ hue: Math.atan2(b, a) * 180 / Math.PI,
75
+ chroma: Math.hypot(a, b),
76
+ lightness
77
+ };
78
+ }
79
+ function speciesFamily(character, spreadDeg = MEDIUM_SPECIES_HUE_SPREAD) {
80
+ const { hue, chroma, lightness } = character;
81
+ const tone = (deg) => {
82
+ const rad = deg * Math.PI / 180;
83
+ return oklabToSrgb([lightness, chroma * Math.cos(rad), chroma * Math.sin(rad)]).map(
84
+ clamp01
85
+ );
86
+ };
87
+ return [tone(hue - spreadDeg), tone(hue), tone(hue + spreadDeg)];
88
+ }
89
+ function neutralSpecies(baseColor) {
90
+ const { hue, chroma } = characterOf(baseColor);
91
+ return speciesFamily({ hue, chroma, lightness: MEDIUM_SPECIES_LIGHTNESS });
92
+ }
93
+
94
+ // src/medium/params.ts
95
+ var BASE_COLOR = [0.07, 0.08, 0.1];
96
+ var MEDIUM_DEFAULTS = {
97
+ decay: 0.06,
98
+ curlFreq: 0.045,
99
+ curlSpeed: 0.12,
100
+ advectSpeed: 26,
101
+ turbulence: 1,
102
+ condensationRate: 0.18,
103
+ condensationFloor: 0.12,
104
+ evaporationRate: 0.2,
105
+ condensateSettleSpeed: 8,
106
+ condensateSpreadRate: 0.08,
107
+ condensateTint: 0.2,
108
+ condensateGain: 5,
109
+ condensateColor: [0.94, 0.95, 0.97],
110
+ channelColors: neutralSpecies(BASE_COLOR),
111
+ baseColor: BASE_COLOR,
112
+ baseWeight: 0.9
113
+ };
114
+
115
+ // src/medium/noise.ts
116
+ var VG_VALUE_NOISE = `
117
+ float vgHash21(float2 p) {
118
+ float h = dot(p, float2(127.1, 311.7));
119
+ return fract(sin(h) * 43758.5453123);
120
+ }
121
+
122
+ float vgValueNoise(float2 p) {
123
+ float2 i = floor(p);
124
+ float2 f = fract(p);
125
+ float a = vgHash21(i);
126
+ float b = vgHash21(i + float2(1.0, 0.0));
127
+ float c = vgHash21(i + float2(0.0, 1.0));
128
+ float d = vgHash21(i + float2(1.0, 1.0));
129
+ float2 u = f * f * (3.0 - 2.0 * f);
130
+ return mix(mix(a, b, u.x), mix(c, d, u.x), u.y) * 2.0 - 1.0;
131
+ }
132
+ `;
133
+ var glslFloat = (v) => Number.isInteger(v) ? v.toFixed(1) : String(v);
134
+ var MEDIUM_TIME_PERIOD = 1e3;
135
+ var MEDIUM_TIME_OCTAVES = [
136
+ { rate: 0.6, offset: 0, scale: 1 },
137
+ { rate: 1.7, offset: 11, scale: 2.03 },
138
+ { rate: 0.9, offset: 37, scale: 4.11 },
139
+ { rate: 0.15, offset: 5, scale: 0.35 }
140
+ ];
141
+ function mediumOctavePeriod(rate) {
142
+ const period = rate * MEDIUM_TIME_PERIOD;
143
+ if (!Number.isInteger(period)) {
144
+ throw new Error(
145
+ `medium noise: octave rate ${rate} * MEDIUM_TIME_PERIOD ${MEDIUM_TIME_PERIOD} = ${period}, not an integer \u2014 the phase wrap would not be seamless`
146
+ );
147
+ }
148
+ return period;
149
+ }
150
+ var glslOctave = (o) => ({
151
+ rate: glslFloat(o.rate),
152
+ offset: glslFloat(o.offset),
153
+ scale: glslFloat(o.scale),
154
+ period: glslFloat(mediumOctavePeriod(o.rate))
155
+ });
156
+ var [VG_OCT0, VG_OCT1, VG_OCT2, VG_OCT3] = MEDIUM_TIME_OCTAVES.map(glslOctave);
157
+ var VG_CURL_NOISE = `
158
+ ${VG_VALUE_NOISE}
159
+
160
+ float vgHash31(float3 p) {
161
+ float h = dot(p, float3(127.1, 311.7, 74.7));
162
+ return fract(sin(h) * 43758.5453123);
163
+ }
164
+
165
+ // 3D value-noise: time is a third grid axis, not a coordinate shift. A shift would just SLIDE the
166
+ // pattern (a "traveling" look, when motion is meant to read as random), while trilinear
167
+ // interpolation over t CHANGES the pattern itself between grid nodes \u2014 nearby moments look alike,
168
+ // distant ones don't, and no repeat ever shows up.
169
+ float vgValueNoise3(float3 p) {
170
+ float3 i = floor(p);
171
+ float3 f = fract(p);
172
+ float3 u = f * f * (3.0 - 2.0 * f);
173
+ float n000 = vgHash31(i);
174
+ float n100 = vgHash31(i + float3(1.0, 0.0, 0.0));
175
+ float n010 = vgHash31(i + float3(0.0, 1.0, 0.0));
176
+ float n110 = vgHash31(i + float3(1.0, 1.0, 0.0));
177
+ float n001 = vgHash31(i + float3(0.0, 0.0, 1.0));
178
+ float n101 = vgHash31(i + float3(1.0, 0.0, 1.0));
179
+ float n011 = vgHash31(i + float3(0.0, 1.0, 1.0));
180
+ float n111 = vgHash31(i + float3(1.0, 1.0, 1.0));
181
+ float nx00 = mix(n000, n100, u.x);
182
+ float nx10 = mix(n010, n110, u.x);
183
+ float nx01 = mix(n001, n101, u.x);
184
+ float nx11 = mix(n011, n111, u.x);
185
+ float nxy0 = mix(nx00, nx10, u.y);
186
+ float nxy1 = mix(nx01, nx11, u.y);
187
+ return mix(nxy0, nxy1, u.z) * 2.0 - 1.0;
188
+ }
189
+
190
+ // Same as vgValueNoise3, but the z axis wraps at \`period\`: BOTH z-lattice neighbors (i.z and
191
+ // i.z + 1.0) are folded into [0, period) before hashing, so this is EXACTLY the same function of
192
+ // z and of z + period \u2014 not just close. vgPotential below calls this once per octave, each with
193
+ // the period matching its own rate (MEDIUM_TIME_OCTAVES), so a phase that wraps at
194
+ // MEDIUM_TIME_PERIOD reproduces the identical field instead of jumping to a new one.
195
+ float vgValueNoise3Periodic(float3 p, float period) {
196
+ float3 i = floor(p);
197
+ float3 f = fract(p);
198
+ float3 u = f * f * (3.0 - 2.0 * f);
199
+ float z0 = mod(i.z, period);
200
+ float z1 = mod(i.z + 1.0, period);
201
+ float n000 = vgHash31(float3(i.xy, z0));
202
+ float n100 = vgHash31(float3(i.xy + float2(1.0, 0.0), z0));
203
+ float n010 = vgHash31(float3(i.xy + float2(0.0, 1.0), z0));
204
+ float n110 = vgHash31(float3(i.xy + float2(1.0, 1.0), z0));
205
+ float n001 = vgHash31(float3(i.xy, z1));
206
+ float n101 = vgHash31(float3(i.xy + float2(1.0, 0.0), z1));
207
+ float n011 = vgHash31(float3(i.xy + float2(0.0, 1.0), z1));
208
+ float n111 = vgHash31(float3(i.xy + float2(1.0, 1.0), z1));
209
+ float nx00 = mix(n000, n100, u.x);
210
+ float nx10 = mix(n010, n110, u.x);
211
+ float nx01 = mix(n001, n101, u.x);
212
+ float nx11 = mix(n011, n111, u.x);
213
+ float nxy0 = mix(nx00, nx10, u.y);
214
+ float nxy1 = mix(nx01, nx11, u.y);
215
+ return mix(nxy0, nxy1, u.z) * 2.0 - 1.0;
216
+ }
217
+
218
+ // The potential is an fbm of three octaves, each evolving over the field's PHASE at its own speed
219
+ // (unrelated to the others, plus a phase offset): no single phase shift can align them all at
220
+ // once, so no traveling pattern remains. Each octave wraps its own z-lattice at the period that
221
+ // matches its rate (see MEDIUM_TIME_OCTAVES/vgValueNoise3Periodic), so
222
+ // vgPotential(p, phase) === vgPotential(p, phase + ${glslFloat(MEDIUM_TIME_PERIOD)}) exactly \u2014 a
223
+ // CPU-side phase accumulator can wrap at that period with no visible jump (see web/medium.ts). The
224
+ // large-scale factor is a slow non-uniformity layered over the sum \u2014 without it, a field that's
225
+ // uniform by statistics still reads as a regular cell pattern.
226
+ float vgPotential(float2 p, float phase) {
227
+ float v = vgValueNoise3Periodic(float3(p * ${VG_OCT0.scale}, phase * ${VG_OCT0.rate} + ${VG_OCT0.offset}), ${VG_OCT0.period}) * 0.5;
228
+ v += vgValueNoise3Periodic(float3(p * ${VG_OCT1.scale}, phase * ${VG_OCT1.rate} + ${VG_OCT1.offset}), ${VG_OCT1.period}) * 0.25;
229
+ v += vgValueNoise3Periodic(float3(p * ${VG_OCT2.scale}, phase * ${VG_OCT2.rate} + ${VG_OCT2.offset}), ${VG_OCT2.period}) * 0.125;
230
+ float macro = vgValueNoise3Periodic(float3(p * ${VG_OCT3.scale}, phase * ${VG_OCT3.rate} + ${VG_OCT3.offset}), ${VG_OCT3.period});
231
+ return v * (0.7 + 0.3 * macro);
232
+ }
233
+
234
+ // Velocity = (dPsi/dy, -dPsi/dx) \u2014 the curl of a scalar field, solenoidal for any Psi. Central
235
+ // differences instead of an analytic derivative: the potential is a sum of three value-noise
236
+ // calls, and there is no cheaper way to differentiate it by hand than four extra samples.
237
+ float2 vgCurlVelocity(float2 p, float phase) {
238
+ float e = 0.6;
239
+ float px1 = vgPotential(p + float2(e, 0.0), phase);
240
+ float px0 = vgPotential(p - float2(e, 0.0), phase);
241
+ float py1 = vgPotential(p + float2(0.0, e), phase);
242
+ float py0 = vgPotential(p - float2(0.0, e), phase);
243
+ return float2(py1 - py0, -(px1 - px0)) / (2.0 * e);
244
+ }
245
+ `;
246
+
247
+ // src/medium/seed-shader.ts
248
+ var MEDIUM_SEED_SHADER = `
249
+ uniform float2 u_spot0;
250
+ uniform float2 u_spot1;
251
+ uniform float2 u_spot2;
252
+ uniform float u_spotRadius;
253
+
254
+ float vgSpot(float2 p, float2 c, float r) {
255
+ float d = length(p - c) / max(r, 1.0);
256
+ return exp(-d * d * 2.0);
257
+ }
258
+
259
+ half4 main(float2 xy) {
260
+ float3 dye = float3(
261
+ vgSpot(xy, u_spot0, u_spotRadius),
262
+ vgSpot(xy, u_spot1, u_spotRadius),
263
+ vgSpot(xy, u_spot2, u_spotRadius)
264
+ );
265
+ return half4(half3(dye), half(1.0));
266
+ }
267
+ `;
268
+
269
+ // src/medium/resample-shader.ts
270
+ var MEDIUM_RESAMPLE_SHADER = `
271
+ uniform shader u_src;
272
+
273
+ half4 main(float2 xy) {
274
+ float2 uv = xy / u_resolution;
275
+ float2 src = uv * u_srcSize;
276
+ return u_src.eval(float2(src.x, u_srcSize.y - src.y));
277
+ }
278
+ `;
279
+
280
+ // src/medium/advect-shader.ts
281
+ var MEDIUM_ADVECT_UNIFORMS = `
282
+ uniform shader u_dye;
283
+ uniform float u_dt;
284
+ uniform float u_phase;
285
+ uniform float u_curlFreq;
286
+ uniform float u_advectSpeed;
287
+ uniform float u_turbulence;
288
+
289
+ ${VG_CURL_NOISE}
290
+ `;
291
+ var MEDIUM_ADVECT_VELOCITY = `
292
+ float2 vel = vgCurlVelocity(xy * u_curlFreq, u_phase) * u_advectSpeed * u_turbulence;
293
+ `;
294
+ var MEDIUM_ADVECT_VELOCITY_SETTLE = `
295
+ ${MEDIUM_ADVECT_VELOCITY}
296
+ vel.y += u_settleSpeed;
297
+ `;
298
+ var MEDIUM_VAPOR_FORWARD_SHADER = `
299
+ ${MEDIUM_ADVECT_UNIFORMS}
300
+
301
+ half4 main(float2 xy) {
302
+ ${MEDIUM_ADVECT_VELOCITY}
303
+ float2 src = xy - vel * u_dt;
304
+ return u_dye.eval(float2(src.x, u_dyeSize.y - src.y));
305
+ }
306
+ `;
307
+ var MEDIUM_CONDENSATE_FORWARD_SHADER = `
308
+ ${MEDIUM_ADVECT_UNIFORMS}
309
+ uniform float u_settleSpeed;
310
+
311
+ half4 main(float2 xy) {
312
+ ${MEDIUM_ADVECT_VELOCITY_SETTLE}
313
+ float2 src = xy - vel * u_dt;
314
+ return u_dye.eval(float2(src.x, u_dyeSize.y - src.y));
315
+ }
316
+ `;
317
+ var MEDIUM_MACCORMACK_CORRECT = `
318
+ float2 srcForward = xy - vel * u_dt;
319
+ float2 srcReverse = xy + vel * u_dt;
320
+ half4 forwardSelf = u_forward.eval(float2(xy.x, u_forwardSize.y - xy.y));
321
+ half4 forwardReverse = u_forward.eval(float2(srcReverse.x, u_forwardSize.y - srcReverse.y));
322
+ half4 prevSelf = u_dye.eval(float2(xy.x, u_dyeSize.y - xy.y));
323
+ half4 corrected = forwardSelf + half4(0.5) * (prevSelf - forwardReverse);
324
+
325
+ float2 src = float2(srcForward.x, u_dyeSize.y - srcForward.y);
326
+ float2 base = floor(src - 0.5);
327
+ half4 c00 = u_dye.eval(base + float2(0.5, 0.5));
328
+ half4 c10 = u_dye.eval(base + float2(1.5, 0.5));
329
+ half4 c01 = u_dye.eval(base + float2(0.5, 1.5));
330
+ half4 c11 = u_dye.eval(base + float2(1.5, 1.5));
331
+ half4 lo = min(min(c00, c10), min(c01, c11));
332
+ half4 hi = max(max(c00, c10), max(c01, c11));
333
+ half4 limited = clamp(corrected, lo, hi);
334
+ `;
335
+ var MEDIUM_VAPOR_CORRECT_SHADER = `
336
+ ${MEDIUM_ADVECT_UNIFORMS}
337
+ uniform shader u_forward;
338
+
339
+ half4 main(float2 xy) {
340
+ ${MEDIUM_ADVECT_VELOCITY}
341
+ ${MEDIUM_MACCORMACK_CORRECT}
342
+ return limited;
343
+ }
344
+ `;
345
+ var MEDIUM_CONDENSATE_CORRECT_SHADER = `
346
+ ${MEDIUM_ADVECT_UNIFORMS}
347
+ uniform float u_settleSpeed;
348
+ uniform shader u_forward;
349
+
350
+ half4 main(float2 xy) {
351
+ ${MEDIUM_ADVECT_VELOCITY_SETTLE}
352
+ ${MEDIUM_MACCORMACK_CORRECT}
353
+ return limited;
354
+ }
355
+ `;
356
+ var MEDIUM_VAPOR_REACT_SHADER = `
357
+ uniform shader u_transported;
358
+ uniform shader u_condensateTransported;
359
+ uniform float u_dt;
360
+ uniform float u_condensationRate;
361
+ uniform float u_condensationFloor;
362
+ uniform float u_evaporationRate;
363
+ uniform float u_totalScale;
364
+
365
+ half4 main(float2 xy) {
366
+ float3 prevVapor = float3(u_transported.eval(float2(xy.x, u_transportedSize.y - xy.y)).rgb);
367
+ float condensateAtXy = float(u_condensateTransported.eval(float2(xy.x, u_condensateTransportedSize.y - xy.y)).r);
368
+ float vaporTotal = prevVapor.r + prevVapor.g + prevVapor.b;
369
+
370
+ // Only the EXCESS of vapor above the background floor condenses, not any nonzero density \u2014
371
+ // otherwise condensate would exactly retrace the vapor spot's shape (the same outline, or
372
+ // softer from blur), leaving the two phases nowhere to visually diverge: vapor is already soft,
373
+ // and condensate needs to read brighter and sharper than it. The floor concentrates condensate
374
+ // in dense turbulence cores, leaving the sparse vapor around it uncondensed \u2014 that's where the
375
+ // contrast comes from.
376
+ float excess = max(0.0, vaporTotal - u_condensationFloor);
377
+ float condensedAmount = excess * (1.0 - exp(-u_condensationRate * u_dt));
378
+ float lossFraction = vaporTotal > 1e-6 ? condensedAmount / vaporTotal : 0.0;
379
+ float3 afterLoss = prevVapor * (1.0 - lossFraction);
380
+
381
+ float condKeep = exp(-u_evaporationRate * u_dt);
382
+ float returned = condensateAtXy * (1.0 - condKeep);
383
+
384
+ // Evaporated water is distributed by the CURRENT vapor proportion \u2014 the condensation/
385
+ // evaporation cycle doesn't shift the hue, water only oscillates between phases (color belongs
386
+ // to vapor).
387
+ float sumAfterLoss = afterLoss.r + afterLoss.g + afterLoss.b;
388
+ float3 hueShare = sumAfterLoss > 1e-6 ? afterLoss / sumAfterLoss : float3(1.0 / 3.0);
389
+ float3 vapor = (afterLoss + hueShare * returned) * u_totalScale;
390
+ return half4(half3(vapor), half(1.0));
391
+ }
392
+ `;
393
+ var MEDIUM_CONDENSATE_REACT_SHADER = `
394
+ uniform shader u_transported;
395
+ uniform shader u_vaporTransported;
396
+ uniform shader u_dye;
397
+ uniform float u_dt;
398
+ uniform float u_condensationRate;
399
+ uniform float u_condensationFloor;
400
+ uniform float u_evaporationRate;
401
+ uniform float u_spreadRate;
402
+ uniform float u_totalScale;
403
+
404
+ half4 main(float2 xy) {
405
+ float prevCondensate = float(u_transported.eval(float2(xy.x, u_transportedSize.y - xy.y)).r);
406
+ float3 vaporAtXy = float3(u_vaporTransported.eval(float2(xy.x, u_vaporTransportedSize.y - xy.y)).rgb);
407
+ float vaporTotal = vaporAtXy.r + vaporAtXy.g + vaporAtXy.b;
408
+
409
+ float excess = max(0.0, vaporTotal - u_condensationFloor);
410
+ float condensed = excess * (1.0 - exp(-u_condensationRate * u_dt));
411
+ float condKeep = exp(-u_evaporationRate * u_dt);
412
+ float afterReaction = prevCondensate * condKeep + condensed;
413
+
414
+ // Growth is an Euler step separate from transport: the neighbors are sampled at THIS cell's own
415
+ // xy in the ORIGINAL (pre-transport-this-frame) field u_dye, not the already
416
+ // transported/corrected value. On a non-uniform flow, warping compresses the domain in some
417
+ // places and stretches it in others, and blurring at a warped point stacks those distortions \u2014
418
+ // the error accumulates frame over frame (measured: condensate steadily gained about 5% extra
419
+ // water alongside its own mass growth). Right on the grid, with no warp, the same blur is an
420
+ // honest averaging kernel that conserves the sum on a periodic domain.
421
+ float2 selfSrc = float2(xy.x, u_dyeSize.y - xy.y);
422
+ float n0 = float(u_dye.eval(float2(selfSrc.x + 1.0, selfSrc.y)).r);
423
+ float n1 = float(u_dye.eval(float2(selfSrc.x - 1.0, selfSrc.y)).r);
424
+ float n2 = float(u_dye.eval(float2(selfSrc.x, selfSrc.y - 1.0)).r);
425
+ float n3 = float(u_dye.eval(float2(selfSrc.x, selfSrc.y + 1.0)).r);
426
+ float neighborAvg = (n0 + n1 + n2 + n3) * 0.25;
427
+ float spreadWeight = 1.0 - exp(-u_spreadRate * u_dt);
428
+ float grown = mix(afterReaction, neighborAvg, spreadWeight) * u_totalScale;
429
+
430
+ return half4(half3(grown, 0.0, 0.0), half(1.0));
431
+ }
432
+ `;
433
+ var MEDIUM_TRACK_ADVECT_SHADER = `
434
+ ${MEDIUM_ADVECT_UNIFORMS}
435
+ uniform float u_decay;
436
+
437
+ half4 main(float2 xy) {
438
+ ${MEDIUM_ADVECT_VELOCITY}
439
+ float2 back = xy - vel * u_dt;
440
+ half4 prev = u_dye.eval(float2(back.x, u_dyeSize.y - back.y));
441
+ float3 decayed = float3(prev.rgb) * exp(-u_decay * u_dt);
442
+ return half4(half3(decayed), half(1.0));
443
+ }
444
+ `;
445
+
446
+ // src/medium/composite-shader.ts
447
+ var MEDIUM_COMPOSITE_SHADER = `
448
+ uniform shader u_vapor;
449
+ uniform shader u_condensate;
450
+ uniform shader u_track;
451
+ uniform float2 u_dyeScale;
452
+ uniform float3 u_lab0;
453
+ uniform float3 u_lab1;
454
+ uniform float3 u_lab2;
455
+ uniform float3 u_labBg;
456
+ uniform float3 u_labCondensate;
457
+ uniform float u_baseWeight;
458
+ uniform float u_condensateTint;
459
+ uniform float u_condensateGain;
460
+
461
+ ${VG_OKLAB_TO_SRGB}
462
+
463
+ half4 main(float2 xy) {
464
+ float2 src = xy * u_dyeScale;
465
+ half4 vapor = u_vapor.eval(src);
466
+ half4 condensate = u_condensate.eval(src);
467
+ half4 track = u_track.eval(src);
468
+ float w0 = float(vapor.r) + float(track.r);
469
+ float w1 = float(vapor.g) + float(track.g);
470
+ float w2 = float(vapor.b) + float(track.b);
471
+ float wBg = u_baseWeight;
472
+
473
+ // A faint condensate hint from the LOCAL vapor, not its own color (droplets scatter light
474
+ // rather than tint it) \u2014 a fraction of the way from white to the vapor tone sampled right here.
475
+ float vaporLocal = max(w0 + w1 + w2, 1e-4);
476
+ float3 vaporHue = (u_lab0 * w0 + u_lab1 * w1 + u_lab2 * w2) / vaporLocal;
477
+ float3 condensateLab = mix(u_labCondensate, vaporHue, u_condensateTint);
478
+ float wCondensate = float(condensate.r) * u_condensateGain;
479
+
480
+ float total = max(w0 + w1 + w2 + wBg + wCondensate, 1e-4);
481
+ float3 mixLab =
482
+ (u_labBg * wBg + u_lab0 * w0 + u_lab1 * w1 + u_lab2 * w2 + condensateLab * wCondensate) / total;
483
+ float3 rgbLinear = clamp(vgOklabToLinear(mixLab), float3(0.0), float3(1.0));
484
+ float3 srgb = vgLinearToSrgb(rgbLinear);
485
+ return half4(half3(srgb), half(1.0));
486
+ }
487
+ `;
488
+
489
+ // src/medium/emit-shader.ts
490
+ var MEDIUM_EMIT_SHADER = `
491
+ uniform float2 u_source;
492
+ uniform float2 u_head;
493
+ uniform float u_headWidth;
494
+ uniform float u_tailWidth;
495
+ uniform float u_raggedAmp;
496
+ uniform float u_raggedFreq;
497
+ uniform float u_settle;
498
+ uniform float u_tailDim;
499
+ uniform float u_intensity;
500
+ uniform float u_seed;
501
+ uniform float3 u_channelMask;
502
+
503
+ ${VG_VALUE_NOISE}
504
+
505
+ half4 main(float2 xy) {
506
+ float2 dir = u_head - u_source;
507
+ float len = max(length(dir), 1.0);
508
+ float2 side = float2(-dir.y, dir.x) / len;
509
+
510
+ float best = 0.0;
511
+ const int STEPS = 12;
512
+ for (int i = 0; i < STEPS; i++) {
513
+ float s = float(i) / float(STEPS - 1);
514
+ float tailness = 1.0 - s;
515
+ float ragged = u_raggedAmp * tailness * vgValueNoise(float2(s * u_raggedFreq + u_seed, u_seed * 3.7));
516
+ float2 p = mix(u_source, u_head, s) + side * ragged + float2(0.0, u_settle * tailness * tailness);
517
+ float w = max(mix(u_tailWidth, u_headWidth, s), 0.5);
518
+ float amp = mix(u_tailDim, 1.0, s);
519
+ float d = length(xy - p);
520
+ float v = amp * exp(-(d * d) / (2.0 * w * w));
521
+ best = max(best, v);
522
+ }
523
+
524
+ float3 dye = u_channelMask * (best * u_intensity);
525
+ return half4(half3(dye), half(1.0));
526
+ }
527
+ `;
528
+ function pickDyeFormat(gl) {
529
+ const hasFloat = gl.getExtension("EXT_color_buffer_float") !== null;
530
+ return hasFloat ? { internalFormat: gl.RGBA16F, type: gl.HALF_FLOAT } : { internalFormat: gl.RGBA8, type: gl.UNSIGNED_BYTE };
531
+ }
532
+ function createMediumRuntime(gl, vertexSource) {
533
+ const format = pickDyeFormat(gl);
534
+ const vaporForwardProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_VAPOR_FORWARD_SHADER));
535
+ const vaporCorrectProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_VAPOR_CORRECT_SHADER));
536
+ const vaporReactProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_VAPOR_REACT_SHADER));
537
+ const condensateForwardProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_CONDENSATE_FORWARD_SHADER));
538
+ const condensateCorrectProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_CONDENSATE_CORRECT_SHADER));
539
+ const condensateReactProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_CONDENSATE_REACT_SHADER));
540
+ const trackAdvectProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_TRACK_ADVECT_SHADER));
541
+ const compositeProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_COMPOSITE_SHADER));
542
+ const seedProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_SEED_SHADER));
543
+ const emitProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_EMIT_SHADER));
544
+ const resampleProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_RESAMPLE_SHADER));
545
+ const vaporForwardLoc = web.locationCache(gl, vaporForwardProgram);
546
+ const vaporCorrectLoc = web.locationCache(gl, vaporCorrectProgram);
547
+ const vaporReactLoc = web.locationCache(gl, vaporReactProgram);
548
+ const condensateForwardLoc = web.locationCache(gl, condensateForwardProgram);
549
+ const condensateCorrectLoc = web.locationCache(gl, condensateCorrectProgram);
550
+ const condensateReactLoc = web.locationCache(gl, condensateReactProgram);
551
+ const trackAdvectLoc = web.locationCache(gl, trackAdvectProgram);
552
+ const compositeLoc = web.locationCache(gl, compositeProgram);
553
+ const seedLoc = web.locationCache(gl, seedProgram);
554
+ const emitLoc = web.locationCache(gl, emitProgram);
555
+ const resampleLoc = web.locationCache(gl, resampleProgram);
556
+ let gridW = 0;
557
+ let gridH = 0;
558
+ let vapor = null;
559
+ let condensate = null;
560
+ let track = null;
561
+ let vaporForward = null;
562
+ let vaporCorrected = null;
563
+ let condensateForward = null;
564
+ let condensateCorrected = null;
565
+ let front = 0;
566
+ function makeTarget(w, h) {
567
+ const texture = web.createTexture(gl, {
568
+ width: w,
569
+ height: h,
570
+ internalFormat: format.internalFormat,
571
+ type: format.type,
572
+ // REPEAT: a domain with no border — see TextureOptions.wrap. All buffers carry the same
573
+ // field, so the border needs to behave identically for each of them, or vapor would wrap
574
+ // while condensate or a track at the same coordinate cuts off. The MacCormack limiter also
575
+ // reads neighbors across this same border — it needs REPEAT exactly as much as transport does.
576
+ wrap: gl.REPEAT
577
+ });
578
+ const fbo = web.createFramebuffer(gl, texture);
579
+ return { texture, fbo };
580
+ }
581
+ function clearTarget(target, w, h) {
582
+ gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);
583
+ gl.viewport(0, 0, w, h);
584
+ gl.clearColor(0, 0, 0, 1);
585
+ gl.clear(gl.COLOR_BUFFER_BIT);
586
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
587
+ }
588
+ function destroyPair(pair) {
589
+ if (!pair) return;
590
+ for (const t of pair) {
591
+ gl.deleteTexture(t.texture);
592
+ gl.deleteFramebuffer(t.fbo);
593
+ }
594
+ }
595
+ function destroyTarget(target) {
596
+ if (!target) return;
597
+ gl.deleteTexture(target.texture);
598
+ gl.deleteFramebuffer(target.fbo);
599
+ }
600
+ function destroyTargets() {
601
+ destroyPair(vapor);
602
+ destroyPair(condensate);
603
+ destroyPair(track);
604
+ destroyTarget(vaporForward);
605
+ destroyTarget(vaporCorrected);
606
+ destroyTarget(condensateForward);
607
+ destroyTarget(condensateCorrected);
608
+ vapor = null;
609
+ condensate = null;
610
+ track = null;
611
+ vaporForward = null;
612
+ vaporCorrected = null;
613
+ condensateForward = null;
614
+ condensateCorrected = null;
615
+ }
616
+ function seed(w, h) {
617
+ if (!vapor || !condensate || !track) return;
618
+ gl.bindFramebuffer(gl.FRAMEBUFFER, vapor[front].fbo);
619
+ gl.viewport(0, 0, w, h);
620
+ gl.disable(gl.BLEND);
621
+ gl.useProgram(seedProgram);
622
+ web.setUniform(gl, seedLoc("u_resolution"), [w, h]);
623
+ web.setUniform(gl, seedLoc("u_spot0"), [w * 0.28, h * 0.64]);
624
+ web.setUniform(gl, seedLoc("u_spot1"), [w * 0.7, h * 0.32]);
625
+ web.setUniform(gl, seedLoc("u_spot2"), [w * 0.48, h * 0.84]);
626
+ web.setUniform(gl, seedLoc("u_spotRadius"), Math.max(w, h) * 0.18);
627
+ web.drawFullscreenTriangle(gl);
628
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
629
+ clearTarget(condensate[0], w, h);
630
+ clearTarget(condensate[1], w, h);
631
+ clearTarget(track[0], w, h);
632
+ clearTarget(track[1], w, h);
633
+ }
634
+ function resample(src, srcW, srcH, dst, dstW, dstH) {
635
+ gl.bindFramebuffer(gl.FRAMEBUFFER, dst.fbo);
636
+ gl.viewport(0, 0, dstW, dstH);
637
+ gl.disable(gl.BLEND);
638
+ gl.useProgram(resampleProgram);
639
+ web.bindTextureAt(gl, 0, src.texture, resampleProgram, "u_src");
640
+ web.setUniform(gl, resampleLoc("u_srcSize"), [srcW, srcH]);
641
+ web.setUniform(gl, resampleLoc("u_resolution"), [dstW, dstH]);
642
+ web.drawFullscreenTriangle(gl);
643
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
644
+ }
645
+ function resizeGrid(w, h) {
646
+ if (!vapor || !condensate || !track) return;
647
+ const oldVapor = vapor;
648
+ const oldCondensate = condensate;
649
+ const oldTrack = track;
650
+ const oldW = gridW;
651
+ const oldH = gridH;
652
+ const newVaporFront = makeTarget(w, h);
653
+ const newCondensateFront = makeTarget(w, h);
654
+ const newTrackFront = makeTarget(w, h);
655
+ resample(oldVapor[front], oldW, oldH, newVaporFront, w, h);
656
+ resample(oldCondensate[front], oldW, oldH, newCondensateFront, w, h);
657
+ resample(oldTrack[front], oldW, oldH, newTrackFront, w, h);
658
+ destroyTargets();
659
+ gridW = w;
660
+ gridH = h;
661
+ vapor = [newVaporFront, makeTarget(w, h)];
662
+ condensate = [newCondensateFront, makeTarget(w, h)];
663
+ track = [newTrackFront, makeTarget(w, h)];
664
+ vaporForward = makeTarget(w, h);
665
+ vaporCorrected = makeTarget(w, h);
666
+ condensateForward = makeTarget(w, h);
667
+ condensateCorrected = makeTarget(w, h);
668
+ front = 0;
669
+ const areaRatio = w * h / (oldW * oldH);
670
+ waterTargetTotal *= areaRatio;
671
+ }
672
+ function ensureGrid(gridWidth, gridHeight) {
673
+ const w = Math.max(2, Math.round(gridWidth));
674
+ const h = Math.max(2, Math.round(gridHeight));
675
+ if (vapor && condensate && track && w === gridW && h === gridH) return;
676
+ if (!vapor || !condensate || !track) {
677
+ gridW = w;
678
+ gridH = h;
679
+ vapor = [makeTarget(w, h), makeTarget(w, h)];
680
+ condensate = [makeTarget(w, h), makeTarget(w, h)];
681
+ track = [makeTarget(w, h), makeTarget(w, h)];
682
+ vaporForward = makeTarget(w, h);
683
+ vaporCorrected = makeTarget(w, h);
684
+ condensateForward = makeTarget(w, h);
685
+ condensateCorrected = makeTarget(w, h);
686
+ front = 0;
687
+ seed(w, h);
688
+ waterTargetTotal = readTarget(vapor[front]) + readTarget(condensate[front]);
689
+ waterScale = 1;
690
+ timeSinceRenorm = 0;
691
+ framesSinceRenorm = 0;
692
+ return;
693
+ }
694
+ resizeGrid(w, h);
695
+ }
696
+ function bindVelocityUniforms(loc, dt, phase2, p) {
697
+ web.setUniform(gl, loc("u_dyeSize"), [gridW, gridH]);
698
+ web.setUniform(gl, loc("u_resolution"), [gridW, gridH]);
699
+ web.setUniform(gl, loc("u_dt"), dt);
700
+ web.setUniform(gl, loc("u_phase"), phase2);
701
+ web.setUniform(gl, loc("u_curlFreq"), p.curlFreq);
702
+ web.setUniform(gl, loc("u_advectSpeed"), p.advectSpeed);
703
+ web.setUniform(gl, loc("u_turbulence"), p.turbulence);
704
+ }
705
+ let phase = 0;
706
+ function wrapPhase(value) {
707
+ const wrapped = value % MEDIUM_TIME_PERIOD;
708
+ return wrapped < 0 ? wrapped + MEDIUM_TIME_PERIOD : wrapped;
709
+ }
710
+ const WATER_RENORM_INTERVAL_S = 1;
711
+ let waterTargetTotal = 0;
712
+ let waterScale = 1;
713
+ let timeSinceRenorm = 0;
714
+ let framesSinceRenorm = 0;
715
+ function step(dt, params = {}) {
716
+ if (!vapor || !condensate || !track || !vaporForward || !vaporCorrected || !condensateForward || !condensateCorrected) {
717
+ return;
718
+ }
719
+ const p = { ...MEDIUM_DEFAULTS, ...params };
720
+ const clampedDt = Math.min(Math.max(dt, 0), 1 / 15);
721
+ phase = wrapPhase(phase + clampedDt * p.curlSpeed);
722
+ const back = front === 0 ? 1 : 0;
723
+ const gridSize = [gridW, gridH];
724
+ gl.viewport(0, 0, gridW, gridH);
725
+ gl.disable(gl.BLEND);
726
+ gl.bindFramebuffer(gl.FRAMEBUFFER, vaporForward.fbo);
727
+ gl.useProgram(vaporForwardProgram);
728
+ web.bindTextureAt(gl, 0, vapor[front].texture, vaporForwardProgram, "u_dye");
729
+ bindVelocityUniforms(vaporForwardLoc, clampedDt, phase, p);
730
+ web.drawFullscreenTriangle(gl);
731
+ gl.bindFramebuffer(gl.FRAMEBUFFER, condensateForward.fbo);
732
+ gl.useProgram(condensateForwardProgram);
733
+ web.bindTextureAt(gl, 0, condensate[front].texture, condensateForwardProgram, "u_dye");
734
+ bindVelocityUniforms(condensateForwardLoc, clampedDt, phase, p);
735
+ web.setUniform(gl, condensateForwardLoc("u_settleSpeed"), p.condensateSettleSpeed);
736
+ web.drawFullscreenTriangle(gl);
737
+ gl.bindFramebuffer(gl.FRAMEBUFFER, vaporCorrected.fbo);
738
+ gl.useProgram(vaporCorrectProgram);
739
+ web.bindTextureAt(gl, 0, vapor[front].texture, vaporCorrectProgram, "u_dye");
740
+ web.bindTextureAt(gl, 1, vaporForward.texture, vaporCorrectProgram, "u_forward");
741
+ web.setUniform(gl, vaporCorrectLoc("u_forwardSize"), gridSize);
742
+ bindVelocityUniforms(vaporCorrectLoc, clampedDt, phase, p);
743
+ web.drawFullscreenTriangle(gl);
744
+ gl.bindFramebuffer(gl.FRAMEBUFFER, condensateCorrected.fbo);
745
+ gl.useProgram(condensateCorrectProgram);
746
+ web.bindTextureAt(gl, 0, condensate[front].texture, condensateCorrectProgram, "u_dye");
747
+ web.bindTextureAt(gl, 1, condensateForward.texture, condensateCorrectProgram, "u_forward");
748
+ web.setUniform(gl, condensateCorrectLoc("u_forwardSize"), gridSize);
749
+ bindVelocityUniforms(condensateCorrectLoc, clampedDt, phase, p);
750
+ web.setUniform(gl, condensateCorrectLoc("u_settleSpeed"), p.condensateSettleSpeed);
751
+ web.drawFullscreenTriangle(gl);
752
+ gl.bindFramebuffer(gl.FRAMEBUFFER, vapor[back].fbo);
753
+ gl.useProgram(vaporReactProgram);
754
+ web.bindTextureAt(gl, 0, vaporCorrected.texture, vaporReactProgram, "u_transported");
755
+ web.bindTextureAt(gl, 1, condensateCorrected.texture, vaporReactProgram, "u_condensateTransported");
756
+ web.setUniform(gl, vaporReactLoc("u_transportedSize"), gridSize);
757
+ web.setUniform(gl, vaporReactLoc("u_condensateTransportedSize"), gridSize);
758
+ web.setUniform(gl, vaporReactLoc("u_resolution"), gridSize);
759
+ web.setUniform(gl, vaporReactLoc("u_dt"), clampedDt);
760
+ web.setUniform(gl, vaporReactLoc("u_condensationRate"), p.condensationRate);
761
+ web.setUniform(gl, vaporReactLoc("u_condensationFloor"), p.condensationFloor);
762
+ web.setUniform(gl, vaporReactLoc("u_evaporationRate"), p.evaporationRate);
763
+ web.setUniform(gl, vaporReactLoc("u_totalScale"), waterScale);
764
+ web.drawFullscreenTriangle(gl);
765
+ gl.bindFramebuffer(gl.FRAMEBUFFER, condensate[back].fbo);
766
+ gl.useProgram(condensateReactProgram);
767
+ web.bindTextureAt(gl, 0, condensateCorrected.texture, condensateReactProgram, "u_transported");
768
+ web.bindTextureAt(gl, 1, vaporCorrected.texture, condensateReactProgram, "u_vaporTransported");
769
+ web.bindTextureAt(gl, 2, condensate[front].texture, condensateReactProgram, "u_dye");
770
+ web.setUniform(gl, condensateReactLoc("u_transportedSize"), gridSize);
771
+ web.setUniform(gl, condensateReactLoc("u_vaporTransportedSize"), gridSize);
772
+ web.setUniform(gl, condensateReactLoc("u_dyeSize"), gridSize);
773
+ web.setUniform(gl, condensateReactLoc("u_resolution"), gridSize);
774
+ web.setUniform(gl, condensateReactLoc("u_dt"), clampedDt);
775
+ web.setUniform(gl, condensateReactLoc("u_condensationRate"), p.condensationRate);
776
+ web.setUniform(gl, condensateReactLoc("u_condensationFloor"), p.condensationFloor);
777
+ web.setUniform(gl, condensateReactLoc("u_evaporationRate"), p.evaporationRate);
778
+ web.setUniform(gl, condensateReactLoc("u_spreadRate"), p.condensateSpreadRate);
779
+ web.setUniform(gl, condensateReactLoc("u_totalScale"), waterScale);
780
+ web.drawFullscreenTriangle(gl);
781
+ gl.bindFramebuffer(gl.FRAMEBUFFER, track[back].fbo);
782
+ gl.useProgram(trackAdvectProgram);
783
+ web.bindTextureAt(gl, 0, track[front].texture, trackAdvectProgram, "u_dye");
784
+ bindVelocityUniforms(trackAdvectLoc, clampedDt, phase, p);
785
+ web.setUniform(gl, trackAdvectLoc("u_decay"), p.decay);
786
+ web.drawFullscreenTriangle(gl);
787
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
788
+ front = back;
789
+ framesSinceRenorm += 1;
790
+ timeSinceRenorm += clampedDt;
791
+ if (waterTargetTotal > 0 && timeSinceRenorm >= WATER_RENORM_INTERVAL_S && framesSinceRenorm > 0) {
792
+ const current = readTarget(vapor[front]) + readTarget(condensate[front]);
793
+ if (current > 1e-6) {
794
+ const totalRatio = waterTargetTotal / current;
795
+ const perFrame = Math.pow(totalRatio, 1 / framesSinceRenorm);
796
+ waterScale = Math.min(1.05, Math.max(0.95, perFrame));
797
+ }
798
+ timeSinceRenorm = 0;
799
+ framesSinceRenorm = 0;
800
+ }
801
+ }
802
+ function emit(emissions) {
803
+ if (!track || emissions.length === 0) return;
804
+ const minDim = Math.min(gridW, gridH);
805
+ gl.bindFramebuffer(gl.FRAMEBUFFER, track[front].fbo);
806
+ gl.viewport(0, 0, gridW, gridH);
807
+ gl.enable(gl.BLEND);
808
+ gl.blendFunc(gl.ONE, gl.ONE);
809
+ gl.useProgram(emitProgram);
810
+ web.setUniform(gl, emitLoc("u_resolution"), [gridW, gridH]);
811
+ for (const e of emissions) {
812
+ const source = [e.source[0] * gridW, e.source[1] * gridH];
813
+ const length = e.lengthFrac * minDim;
814
+ const head = [
815
+ source[0] + Math.cos(e.angle) * length,
816
+ source[1] + Math.sin(e.angle) * length
817
+ ];
818
+ web.setUniform(gl, emitLoc("u_source"), source);
819
+ web.setUniform(gl, emitLoc("u_head"), head);
820
+ web.setUniform(gl, emitLoc("u_headWidth"), e.headWidthFrac * minDim);
821
+ web.setUniform(gl, emitLoc("u_tailWidth"), e.tailWidthFrac * minDim);
822
+ web.setUniform(gl, emitLoc("u_raggedAmp"), e.raggedFrac * minDim);
823
+ web.setUniform(gl, emitLoc("u_raggedFreq"), e.raggedFreq);
824
+ web.setUniform(gl, emitLoc("u_settle"), e.settleFrac * minDim);
825
+ web.setUniform(gl, emitLoc("u_tailDim"), e.tailDim);
826
+ web.setUniform(gl, emitLoc("u_intensity"), e.intensity);
827
+ web.setUniform(gl, emitLoc("u_seed"), e.seed);
828
+ web.setUniform(gl, emitLoc("u_channelMask"), e.channel);
829
+ web.drawFullscreenTriangle(gl);
830
+ }
831
+ gl.disable(gl.BLEND);
832
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
833
+ }
834
+ function composite(contentWidth, contentHeight, params = {}) {
835
+ if (!vapor || !condensate || !track) return;
836
+ const p = { ...MEDIUM_DEFAULTS, ...params };
837
+ gl.viewport(0, 0, contentWidth, contentHeight);
838
+ gl.disable(gl.BLEND);
839
+ gl.useProgram(compositeProgram);
840
+ web.bindTextureAt(gl, 0, vapor[front].texture, compositeProgram, "u_vapor");
841
+ web.bindTextureAt(gl, 1, condensate[front].texture, compositeProgram, "u_condensate");
842
+ web.bindTextureAt(gl, 2, track[front].texture, compositeProgram, "u_track");
843
+ web.setUniform(gl, compositeLoc("u_vaporSize"), [gridW, gridH]);
844
+ web.setUniform(gl, compositeLoc("u_condensateSize"), [gridW, gridH]);
845
+ web.setUniform(gl, compositeLoc("u_trackSize"), [gridW, gridH]);
846
+ web.setUniform(gl, compositeLoc("u_resolution"), [contentWidth, contentHeight]);
847
+ web.setUniform(gl, compositeLoc("u_dyeScale"), [gridW / contentWidth, gridH / contentHeight]);
848
+ web.setUniform(gl, compositeLoc("u_lab0"), srgbToOklab(p.channelColors[0]));
849
+ web.setUniform(gl, compositeLoc("u_lab1"), srgbToOklab(p.channelColors[1]));
850
+ web.setUniform(gl, compositeLoc("u_lab2"), srgbToOklab(p.channelColors[2]));
851
+ web.setUniform(gl, compositeLoc("u_labBg"), srgbToOklab(p.baseColor));
852
+ web.setUniform(gl, compositeLoc("u_labCondensate"), srgbToOklab(p.condensateColor));
853
+ web.setUniform(gl, compositeLoc("u_baseWeight"), p.baseWeight);
854
+ web.setUniform(gl, compositeLoc("u_condensateTint"), p.condensateTint);
855
+ web.setUniform(gl, compositeLoc("u_condensateGain"), p.condensateGain);
856
+ web.drawFullscreenTriangle(gl);
857
+ }
858
+ function readTarget(target) {
859
+ gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);
860
+ let sum = 0;
861
+ if (format.type === gl.HALF_FLOAT) {
862
+ const buf = new Float32Array(gridW * gridH * 4);
863
+ gl.readPixels(0, 0, gridW, gridH, gl.RGBA, gl.FLOAT, buf);
864
+ for (let i = 0; i < buf.length; i += 4) sum += buf[i] + buf[i + 1] + buf[i + 2];
865
+ } else {
866
+ const buf = new Uint8Array(gridW * gridH * 4);
867
+ gl.readPixels(0, 0, gridW, gridH, gl.RGBA, gl.UNSIGNED_BYTE, buf);
868
+ for (let i = 0; i < buf.length; i += 4) sum += (buf[i] + buf[i + 1] + buf[i + 2]) / 255;
869
+ }
870
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
871
+ return sum;
872
+ }
873
+ function readTotals() {
874
+ if (!vapor || !condensate || !track) return null;
875
+ return {
876
+ vapor: readTarget(vapor[front]),
877
+ condensate: readTarget(condensate[front]),
878
+ track: readTarget(track[front])
879
+ };
880
+ }
881
+ function readVaporGrid() {
882
+ if (!vapor) return null;
883
+ gl.bindFramebuffer(gl.FRAMEBUFFER, vapor[front].fbo);
884
+ const cells = gridW * gridH;
885
+ const data = new Array(cells * 3);
886
+ if (format.type === gl.HALF_FLOAT) {
887
+ const buf = new Float32Array(cells * 4);
888
+ gl.readPixels(0, 0, gridW, gridH, gl.RGBA, gl.FLOAT, buf);
889
+ for (let i = 0; i < cells; i += 1) {
890
+ data[i * 3] = buf[i * 4];
891
+ data[i * 3 + 1] = buf[i * 4 + 1];
892
+ data[i * 3 + 2] = buf[i * 4 + 2];
893
+ }
894
+ } else {
895
+ const buf = new Uint8Array(cells * 4);
896
+ gl.readPixels(0, 0, gridW, gridH, gl.RGBA, gl.UNSIGNED_BYTE, buf);
897
+ for (let i = 0; i < cells; i += 1) {
898
+ data[i * 3] = buf[i * 4] / 255;
899
+ data[i * 3 + 1] = buf[i * 4 + 1] / 255;
900
+ data[i * 3 + 2] = buf[i * 4 + 2] / 255;
901
+ }
902
+ }
903
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
904
+ return { cols: gridW, rows: gridH, data };
905
+ }
906
+ function readCondensateGrid() {
907
+ if (!condensate) return null;
908
+ gl.bindFramebuffer(gl.FRAMEBUFFER, condensate[front].fbo);
909
+ const cells = gridW * gridH;
910
+ const data = new Array(cells);
911
+ if (format.type === gl.HALF_FLOAT) {
912
+ const buf = new Float32Array(cells * 4);
913
+ gl.readPixels(0, 0, gridW, gridH, gl.RGBA, gl.FLOAT, buf);
914
+ for (let i = 0; i < cells; i += 1) data[i] = buf[i * 4];
915
+ } else {
916
+ const buf = new Uint8Array(cells * 4);
917
+ gl.readPixels(0, 0, gridW, gridH, gl.RGBA, gl.UNSIGNED_BYTE, buf);
918
+ for (let i = 0; i < cells; i += 1) data[i] = buf[i * 4] / 255;
919
+ }
920
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
921
+ return { cols: gridW, rows: gridH, data };
922
+ }
923
+ function destroy() {
924
+ destroyTargets();
925
+ gl.deleteProgram(vaporForwardProgram);
926
+ gl.deleteProgram(vaporCorrectProgram);
927
+ gl.deleteProgram(vaporReactProgram);
928
+ gl.deleteProgram(condensateForwardProgram);
929
+ gl.deleteProgram(condensateCorrectProgram);
930
+ gl.deleteProgram(condensateReactProgram);
931
+ gl.deleteProgram(trackAdvectProgram);
932
+ gl.deleteProgram(compositeProgram);
933
+ gl.deleteProgram(seedProgram);
934
+ gl.deleteProgram(emitProgram);
935
+ gl.deleteProgram(resampleProgram);
936
+ }
937
+ return { ensureGrid, step, emit, composite, readTotals, readVaporGrid, readCondensateGrid, destroy };
938
+ }
939
+ function createMediumBackdrop() {
940
+ let runtime = null;
941
+ let owner = null;
942
+ return {
943
+ pass: (frame) => (gl, target) => {
944
+ if (owner && owner !== gl) throw new Error("vireuikit: the medium is already bound to another WebGL context");
945
+ owner = gl;
946
+ runtime ??= createMediumRuntime(gl, web.FULLSCREEN_TRIANGLE_VERTEX_SOURCE);
947
+ runtime.ensureGrid(frame.gridWidth, frame.gridHeight);
948
+ runtime.step(frame.dt, frame.params);
949
+ runtime.emit(frame.emissions ?? []);
950
+ gl.bindFramebuffer(gl.FRAMEBUFFER, target.framebuffer);
951
+ runtime.composite(target.width, target.height, frame.params);
952
+ },
953
+ readTotals: () => runtime?.readTotals() ?? null,
954
+ readVaporGrid: () => runtime?.readVaporGrid() ?? null,
955
+ readCondensateGrid: () => runtime?.readCondensateGrid() ?? null,
956
+ destroy: () => {
957
+ runtime?.destroy();
958
+ runtime = null;
959
+ owner = null;
960
+ }
961
+ };
962
+ }
963
+
964
+ exports.createMediumBackdrop = createMediumBackdrop;
965
+ exports.createMediumRuntime = createMediumRuntime;
966
+ //# sourceMappingURL=index.cjs.map
967
+ //# sourceMappingURL=index.cjs.map