vireuikit 0.2.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.
@@ -3,125 +3,6 @@
3
3
  var vireglass = require('vireglass');
4
4
  var web = require('vireglass/web');
5
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
- depthFarScale: 1.6,
114
- depthFarOffsetFrac: [0.47, 0.53],
115
- depthFarBlurRadius: 5,
116
- depthFarWeight: 0.5,
117
- gravityVaporDrift: 0.6,
118
- gravityBottomBoost: 2,
119
- gravityBottomBoostStart: 0.75
120
- };
121
- function depthFarOffsetPx(offsetFrac, gridWidth, gridHeight) {
122
- return [offsetFrac[0] * gridWidth, offsetFrac[1] * gridHeight];
123
- }
124
-
125
6
  // src/medium/noise.ts
126
7
  var VG_VALUE_NOISE = `
127
8
  float vgHash21(float2 p) {
@@ -252,7 +133,210 @@ float2 vgCurlVelocity(float2 p, float phase) {
252
133
  float py0 = vgPotential(p - float2(0.0, e), phase);
253
134
  return float2(py1 - py0, -(px1 - px0)) / (2.0 * e);
254
135
  }
136
+
137
+ // vgPotential above is periodic in TIME only: the simulation domain is a torus (gl.REPEAT), but
138
+ // the lattice itself never wraps in x/y, so velocity jumps at the domain seam and dye on either
139
+ // side of it is transported differently \u2014 a straight seam, made worse wherever something re-reads
140
+ // the field at a different scale (the far depth plane's tile boundary). vgValueNoise3Seamless
141
+ // wraps ALL THREE lattice axes: xyPeriod/zPeriod fold BOTH neighbors of a cell into their own
142
+ // period before hashing, so the field is EXACTLY the same one period over, in space exactly as it
143
+ // already was in time.
144
+ //
145
+ // The period has to be an integer number of lattice cells, or the wrap would jump instead of
146
+ // closing seamlessly (the same requirement MEDIUM_TIME_OCTAVES/mediumOctavePeriod enforces for
147
+ // time) \u2014 but unlike time, the spatial period depends on the GRID SIZE, which this shader has no
148
+ // notion of. So xyPeriod travels together with a per-octave spatial frequency the CPU computes to
149
+ // make gridSize * frequency land on that exact integer (see computeMediumSpatialPeriods below);
150
+ // both arrive together as one uniform per octave (u_spatialN.xy = frequency, .zw = period),
151
+ // recomputed by web/medium.ts whenever the grid resizes.
152
+ float vgValueNoise3Seamless(float3 p, float2 xyPeriod, float zPeriod) {
153
+ float3 i = floor(p);
154
+ float3 f = fract(p);
155
+ float3 u = f * f * (3.0 - 2.0 * f);
156
+ float x0 = mod(i.x, xyPeriod.x);
157
+ float x1 = mod(i.x + 1.0, xyPeriod.x);
158
+ float y0 = mod(i.y, xyPeriod.y);
159
+ float y1 = mod(i.y + 1.0, xyPeriod.y);
160
+ float z0 = mod(i.z, zPeriod);
161
+ float z1 = mod(i.z + 1.0, zPeriod);
162
+ float n000 = vgHash31(float3(x0, y0, z0));
163
+ float n100 = vgHash31(float3(x1, y0, z0));
164
+ float n010 = vgHash31(float3(x0, y1, z0));
165
+ float n110 = vgHash31(float3(x1, y1, z0));
166
+ float n001 = vgHash31(float3(x0, y0, z1));
167
+ float n101 = vgHash31(float3(x1, y0, z1));
168
+ float n011 = vgHash31(float3(x0, y1, z1));
169
+ float n111 = vgHash31(float3(x1, y1, z1));
170
+ float nx00 = mix(n000, n100, u.x);
171
+ float nx10 = mix(n010, n110, u.x);
172
+ float nx01 = mix(n001, n101, u.x);
173
+ float nx11 = mix(n011, n111, u.x);
174
+ float nxy0 = mix(nx00, nx10, u.y);
175
+ float nxy1 = mix(nx01, nx11, u.y);
176
+ return mix(nxy0, nxy1, u.z) * 2.0 - 1.0;
177
+ }
178
+
179
+ // Same four-octave fbm as vgPotential, but every octave reads the seamless lattice above at its
180
+ // OWN (frequency, period) pair \u2014 sN.xy replaces that octave's compile-time \`scale\` constant (it
181
+ // has to be a uniform now: the adjustment depends on the live grid size), sN.zw is the matching
182
+ // integer period. \`p\` is still curlFreq-scaled exactly as vgPotential's caller scales it \u2014 only
183
+ // the per-octave multiplier changed from a constant to a grid-dependent uniform, so the finite
184
+ // difference in vgCurlVelocitySeamless below needs no epsilon change of its own.
185
+ float vgPotentialSeamless(float2 p, float phase, float4 s0, float4 s1, float4 s2, float4 s3) {
186
+ float v = vgValueNoise3Seamless(float3(p * s0.xy, phase * ${VG_OCT0.rate} + ${VG_OCT0.offset}), s0.zw, ${VG_OCT0.period}) * 0.5;
187
+ v += vgValueNoise3Seamless(float3(p * s1.xy, phase * ${VG_OCT1.rate} + ${VG_OCT1.offset}), s1.zw, ${VG_OCT1.period}) * 0.25;
188
+ v += vgValueNoise3Seamless(float3(p * s2.xy, phase * ${VG_OCT2.rate} + ${VG_OCT2.offset}), s2.zw, ${VG_OCT2.period}) * 0.125;
189
+ float macro = vgValueNoise3Seamless(float3(p * s3.xy, phase * ${VG_OCT3.rate} + ${VG_OCT3.offset}), s3.zw, ${VG_OCT3.period});
190
+ return v * (0.7 + 0.3 * macro);
191
+ }
192
+
193
+ float2 vgCurlVelocitySeamless(float2 p, float phase, float4 s0, float4 s1, float4 s2, float4 s3) {
194
+ float e = 0.6;
195
+ float px1 = vgPotentialSeamless(p + float2(e, 0.0), phase, s0, s1, s2, s3);
196
+ float px0 = vgPotentialSeamless(p - float2(e, 0.0), phase, s0, s1, s2, s3);
197
+ float py1 = vgPotentialSeamless(p + float2(0.0, e), phase, s0, s1, s2, s3);
198
+ float py0 = vgPotentialSeamless(p - float2(0.0, e), phase, s0, s1, s2, s3);
199
+ return float2(py1 - py0, -(px1 - px0)) / (2.0 * e);
200
+ }
201
+ `;
202
+ var MIN_SPATIAL_PERIOD = 2;
203
+ function computeMediumSpatialPeriods(gridWidth, gridHeight, curlFreq) {
204
+ const freq = Math.max(curlFreq, 1e-6);
205
+ return MEDIUM_TIME_OCTAVES.map((o) => {
206
+ const rawX = gridWidth * freq * o.scale;
207
+ const rawY = gridHeight * freq * o.scale;
208
+ const minRaw = Math.min(rawX, rawY);
209
+ const scaleUp = minRaw > 0 && minRaw < MIN_SPATIAL_PERIOD ? MIN_SPATIAL_PERIOD / minRaw : 1;
210
+ const periodX = Math.max(MIN_SPATIAL_PERIOD, Math.round(rawX * scaleUp));
211
+ const periodY = Math.max(MIN_SPATIAL_PERIOD, Math.round(rawY * scaleUp));
212
+ return {
213
+ freqX: periodX / (gridWidth * freq),
214
+ freqY: periodY / (gridHeight * freq),
215
+ periodX,
216
+ periodY
217
+ };
218
+ });
219
+ }
220
+
221
+ // src/medium/oklab.ts
222
+ var LINEAR_TO_LMS = [
223
+ [0.4122214708, 0.5363325363, 0.0514459929],
224
+ [0.2119034982, 0.6806995451, 0.1073969566],
225
+ [0.0883024619, 0.2817188376, 0.6299787005]
226
+ ];
227
+ var LMS_TO_OKLAB = [
228
+ [0.2104542553, 0.793617785, -0.0040720468],
229
+ [1.9779984951, -2.428592205, 0.4505937099],
230
+ [0.0259040371, 0.7827717662, -0.808675766]
231
+ ];
232
+ var OKLAB_TO_LMS = [
233
+ [1, 0.3963377774, 0.2158037573],
234
+ [1, -0.1055613458, -0.0638541728],
235
+ [1, -0.0894841775, -1.291485548]
236
+ ];
237
+ var LMS_TO_LINEAR = [
238
+ [4.0767416621, -3.3077115913, 0.2309699292],
239
+ [-1.2684380046, 2.6097574011, -0.3413193965],
240
+ [-0.0041960863, -0.7034186147, 1.707614701]
241
+ ];
242
+ var glsl = (v) => Number.isInteger(v) ? v.toFixed(1) : String(v);
243
+ var row = (m, i, vars) => m[i].map((k, j) => `${glsl(k)} * ${vars[j]}`).join(" + ");
244
+ var VG_OKLAB_TO_SRGB = `
245
+ float3 vgLinearToSrgb(float3 c) {
246
+ float3 lo = c * 12.92;
247
+ float3 hi = 1.055 * pow(max(c, float3(0.0)), float3(1.0 / 2.4)) - 0.055;
248
+ return mix(lo, hi, step(float3(0.0031308), c));
249
+ }
250
+
251
+ float3 vgOklabToLinear(float3 lab) {
252
+ float l_ = ${row(OKLAB_TO_LMS, 0, ["lab.x", "lab.y", "lab.z"])};
253
+ float m_ = ${row(OKLAB_TO_LMS, 1, ["lab.x", "lab.y", "lab.z"])};
254
+ float s_ = ${row(OKLAB_TO_LMS, 2, ["lab.x", "lab.y", "lab.z"])};
255
+ float l = l_ * l_ * l_;
256
+ float m = m_ * m_ * m_;
257
+ float s = s_ * s_ * s_;
258
+ return float3(
259
+ ${row(LMS_TO_LINEAR, 0, ["l", "m", "s"])},
260
+ ${row(LMS_TO_LINEAR, 1, ["l", "m", "s"])},
261
+ ${row(LMS_TO_LINEAR, 2, ["l", "m", "s"])}
262
+ );
263
+ }
255
264
  `;
265
+ var apply = (m, v) => [
266
+ m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
267
+ m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
268
+ m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2]
269
+ ];
270
+ var srgbChannelToLinear = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
271
+ var linearChannelToSrgb = (c) => c <= 31308e-7 ? c * 12.92 : 1.055 * Math.pow(Math.max(c, 0), 1 / 2.4) - 0.055;
272
+ function srgbToOklab(rgb) {
273
+ const linear = rgb.map(srgbChannelToLinear);
274
+ const lms = apply(LINEAR_TO_LMS, linear);
275
+ return apply(LMS_TO_OKLAB, lms.map((v) => Math.cbrt(Math.max(v, 0))));
276
+ }
277
+ function oklabToSrgb(lab) {
278
+ const lms = apply(OKLAB_TO_LMS, lab).map((v) => v * v * v);
279
+ return apply(LMS_TO_LINEAR, lms).map(linearChannelToSrgb);
280
+ }
281
+
282
+ // src/medium/species.ts
283
+ var MEDIUM_SPECIES_LIGHTNESS = 0.44;
284
+ var MEDIUM_SPECIES_HUE_SPREAD = 32;
285
+ var clamp01 = (v) => Math.min(1, Math.max(0, v));
286
+ function characterOf(color) {
287
+ const [lightness, a, b] = srgbToOklab(color);
288
+ return {
289
+ hue: Math.atan2(b, a) * 180 / Math.PI,
290
+ chroma: Math.hypot(a, b),
291
+ lightness
292
+ };
293
+ }
294
+ function speciesFamily(character, spreadDeg = MEDIUM_SPECIES_HUE_SPREAD) {
295
+ const { hue, chroma, lightness } = character;
296
+ const tone = (deg) => {
297
+ const rad = deg * Math.PI / 180;
298
+ return oklabToSrgb([lightness, chroma * Math.cos(rad), chroma * Math.sin(rad)]).map(
299
+ clamp01
300
+ );
301
+ };
302
+ return [tone(hue - spreadDeg), tone(hue), tone(hue + spreadDeg)];
303
+ }
304
+ function neutralSpecies(baseColor) {
305
+ const { hue, chroma } = characterOf(baseColor);
306
+ return speciesFamily({ hue, chroma, lightness: MEDIUM_SPECIES_LIGHTNESS });
307
+ }
308
+
309
+ // src/medium/params.ts
310
+ var BASE_COLOR = [0.07, 0.08, 0.1];
311
+ var MEDIUM_DEFAULTS = {
312
+ decay: 0.06,
313
+ curlFreq: 0.045,
314
+ curlSpeed: 0.12,
315
+ advectSpeed: 26,
316
+ turbulence: 1,
317
+ condensationRate: 0.18,
318
+ condensationFloor: 0.12,
319
+ evaporationRate: 0.2,
320
+ condensateSettleSpeed: 8,
321
+ condensateSettleWiggle: 0.4,
322
+ condensateSpreadRate: 0.08,
323
+ condensateTint: 0.2,
324
+ condensateGain: 5,
325
+ condensateColor: [0.94, 0.95, 0.97],
326
+ channelColors: neutralSpecies(BASE_COLOR),
327
+ baseColor: BASE_COLOR,
328
+ baseWeight: 0.9,
329
+ depthFarScale: 1.6,
330
+ depthFarOffsetFrac: [0.47, 0.53],
331
+ depthFarBlurRadius: 5,
332
+ depthFarWeight: 0.5,
333
+ gravityVaporDrift: 0.6,
334
+ gravityBottomBoost: 2,
335
+ gravityBottomBoostStart: 0.75
336
+ };
337
+ function depthFarOffsetPx(offsetFrac, gridWidth, gridHeight) {
338
+ return [offsetFrac[0] * gridWidth, offsetFrac[1] * gridHeight];
339
+ }
256
340
 
257
341
  // src/medium/seed-shader.ts
258
342
  var MEDIUM_SEED_SHADER = `
@@ -295,15 +379,25 @@ uniform float u_phase;
295
379
  uniform float u_curlFreq;
296
380
  uniform float u_advectSpeed;
297
381
  uniform float u_turbulence;
382
+ // One float4 per MEDIUM_TIME_OCTAVES entry: xy is that octave's seamless spatial frequency, zw its
383
+ // matching integer lattice period \u2014 both grid-size-dependent, so they can't be compile-time
384
+ // constants (see vgPotentialSeamless/computeMediumSpatialPeriods in noise.ts). Recomputed by
385
+ // web/medium.ts's bindVelocityUniforms every frame from the live grid dimensions.
386
+ uniform float4 u_spatial0;
387
+ uniform float4 u_spatial1;
388
+ uniform float4 u_spatial2;
389
+ uniform float4 u_spatial3;
298
390
 
299
391
  ${VG_CURL_NOISE}
300
392
  `;
301
393
  var MEDIUM_ADVECT_VELOCITY = `
302
- float2 vel = vgCurlVelocity(xy * u_curlFreq, u_phase) * u_advectSpeed * u_turbulence;
394
+ float2 curl = vgCurlVelocitySeamless(xy * u_curlFreq, u_phase, u_spatial0, u_spatial1, u_spatial2, u_spatial3);
395
+ float2 vel = curl * u_advectSpeed * u_turbulence;
303
396
  `;
304
397
  var MEDIUM_ADVECT_VELOCITY_SETTLE = `
305
398
  ${MEDIUM_ADVECT_VELOCITY}
306
399
  vel.y += u_settleSpeed;
400
+ vel.x += curl.x * u_advectSpeed * u_settleWiggle;
307
401
  `;
308
402
  var MEDIUM_ADVECT_VELOCITY_VAPOR = `
309
403
  ${MEDIUM_ADVECT_VELOCITY}
@@ -322,6 +416,7 @@ ${MEDIUM_ADVECT_VELOCITY_VAPOR}
322
416
  var MEDIUM_CONDENSATE_FORWARD_SHADER = `
323
417
  ${MEDIUM_ADVECT_UNIFORMS}
324
418
  uniform float u_settleSpeed;
419
+ uniform float u_settleWiggle;
325
420
 
326
421
  half4 main(float2 xy) {
327
422
  ${MEDIUM_ADVECT_VELOCITY_SETTLE}
@@ -361,6 +456,7 @@ ${MEDIUM_MACCORMACK_CORRECT}
361
456
  var MEDIUM_CONDENSATE_CORRECT_SHADER = `
362
457
  ${MEDIUM_ADVECT_UNIFORMS}
363
458
  uniform float u_settleSpeed;
459
+ uniform float u_settleWiggle;
364
460
  uniform shader u_forward;
365
461
 
366
462
  half4 main(float2 xy) {
@@ -460,6 +556,39 @@ ${MEDIUM_ADVECT_VELOCITY}
460
556
  `;
461
557
 
462
558
  // src/medium/composite-shader.ts
559
+ var VG_CUBIC_WEIGHTS = `
560
+ float4 vgCubicWeights(float v) {
561
+ float4 n = float4(1.0, 2.0, 3.0, 4.0) - v;
562
+ float4 s = n * n * n;
563
+ float x = s.x;
564
+ float y = s.y - 4.0 * s.x;
565
+ float z = s.z - 4.0 * s.y + 6.0 * s.x;
566
+ float w = 6.0 - x - y - z;
567
+ return float4(x, y, z, w) * (1.0 / 6.0);
568
+ }
569
+ `;
570
+ function vgBicubicSampler(fnName, textureName) {
571
+ return `
572
+ half4 ${fnName}(float2 px) {
573
+ float2 shifted = px - 0.5;
574
+ float2 fxy = fract(shifted);
575
+ float2 base = shifted - fxy;
576
+ float4 xw = vgCubicWeights(fxy.x);
577
+ float4 yw = vgCubicWeights(fxy.y);
578
+ float2 sx = float2(xw.x + xw.y, xw.z + xw.w);
579
+ float2 sy = float2(yw.x + yw.y, yw.z + yw.w);
580
+ float2 offsetX = float2(base.x - 0.5 + xw.y / sx.x, base.x + 1.5 + xw.w / sx.y);
581
+ float2 offsetY = float2(base.y - 0.5 + yw.y / sy.x, base.y + 1.5 + yw.w / sy.y);
582
+ half4 s00 = ${textureName}.eval(float2(offsetX.x, offsetY.x));
583
+ half4 s10 = ${textureName}.eval(float2(offsetX.y, offsetY.x));
584
+ half4 s01 = ${textureName}.eval(float2(offsetX.x, offsetY.y));
585
+ half4 s11 = ${textureName}.eval(float2(offsetX.y, offsetY.y));
586
+ float wx = sx.x / (sx.x + sx.y);
587
+ float wy = sy.x / (sy.x + sy.y);
588
+ return mix(mix(s11, s01, wx), mix(s10, s00, wx), wy);
589
+ }
590
+ `;
591
+ }
463
592
  var MEDIUM_COMPOSITE_SHADER = `
464
593
  uniform shader u_vapor;
465
594
  uniform shader u_condensate;
@@ -481,25 +610,44 @@ uniform float u_gravityBoost;
481
610
  uniform float u_gravityBoostStart;
482
611
 
483
612
  ${VG_OKLAB_TO_SRGB}
613
+ ${VG_CUBIC_WEIGHTS}
614
+ ${vgBicubicSampler("vgBicubicVapor", "u_vapor")}
615
+ ${vgBicubicSampler("vgBicubicCondensate", "u_condensate")}
484
616
 
485
617
  half4 main(float2 xy) {
486
618
  float2 baseSrc = xy * u_dyeScale;
487
619
 
488
- // Near plane \u2014 sampled exactly as before the depth work; the calibrated look lives here.
489
- half4 vapor = u_vapor.eval(baseSrc);
490
- half4 condensate = u_condensate.eval(baseSrc);
620
+ // Near plane \u2014 the calibrated look lives here. Bicubic (see the file header) on vapor and
621
+ // condensate; track stays a plain bilinear read.
622
+ half4 vapor = vgBicubicVapor(baseSrc);
623
+ half4 condensate = vgBicubicCondensate(baseSrc);
491
624
  half4 track = u_track.eval(baseSrc);
492
625
 
493
- // Far plane \u2014 the identical field, scaled and offset (see the file header). A 4-tap box blur
494
- // (no separate center sample, the cheapest isotropic blur available) supplies aerial perspective's
495
- // blur and, as a side effect of averaging, its lower contrast too.
626
+ // Far plane \u2014 the identical field, scaled and offset (see the file header). An 8-tap ring, not
627
+ // the near plane's \xB1x/\xB1y box: axis-aligned taps read the SAME column/row on both sides of any
628
+ // residual axis-aligned structure (a seam, a resize artifact), which is exactly how a straight
629
+ // line gets doubled into four parallel copies instead of softened away \u2014 measured directly via
630
+ // check-medium.mjs's isotropy gate (see its report). The ring is rotated a half-step (22.5\xB0) off
631
+ // the axes so no tap direction ever coincides with a grid row or column. Radius stays in the same
632
+ // far-plane texture-space unit as before (u_farBlurRadius, grid-px) \u2014 only the tap PATTERN
633
+ // changed, not what it measures in.
496
634
  float2 farSrc = baseSrc * u_farScale + u_farOffset;
497
- float2 blurX = float2(u_farBlurRadius, 0.0);
498
- float2 blurY = float2(0.0, u_farBlurRadius);
499
- half4 vaporFar = (u_vapor.eval(farSrc + blurX) + u_vapor.eval(farSrc - blurX) +
500
- u_vapor.eval(farSrc + blurY) + u_vapor.eval(farSrc - blurY)) * half4(0.25);
501
- half4 condensateFar = (u_condensate.eval(farSrc + blurX) + u_condensate.eval(farSrc - blurX) +
502
- u_condensate.eval(farSrc + blurY) + u_condensate.eval(farSrc - blurY)) * half4(0.25);
635
+ float2 ring0 = float2( 0.9238795, 0.3826834) * u_farBlurRadius;
636
+ float2 ring1 = float2( 0.3826834, 0.9238795) * u_farBlurRadius;
637
+ float2 ring2 = float2(-0.3826834, 0.9238795) * u_farBlurRadius;
638
+ float2 ring3 = float2(-0.9238795, 0.3826834) * u_farBlurRadius;
639
+ float2 ring4 = float2(-0.9238795, -0.3826834) * u_farBlurRadius;
640
+ float2 ring5 = float2(-0.3826834, -0.9238795) * u_farBlurRadius;
641
+ float2 ring6 = float2( 0.3826834, -0.9238795) * u_farBlurRadius;
642
+ float2 ring7 = float2( 0.9238795, -0.3826834) * u_farBlurRadius;
643
+ half4 vaporFar = (u_vapor.eval(farSrc + ring0) + u_vapor.eval(farSrc + ring1) +
644
+ u_vapor.eval(farSrc + ring2) + u_vapor.eval(farSrc + ring3) +
645
+ u_vapor.eval(farSrc + ring4) + u_vapor.eval(farSrc + ring5) +
646
+ u_vapor.eval(farSrc + ring6) + u_vapor.eval(farSrc + ring7)) * half4(0.125);
647
+ half4 condensateFar = (u_condensate.eval(farSrc + ring0) + u_condensate.eval(farSrc + ring1) +
648
+ u_condensate.eval(farSrc + ring2) + u_condensate.eval(farSrc + ring3) +
649
+ u_condensate.eval(farSrc + ring4) + u_condensate.eval(farSrc + ring5) +
650
+ u_condensate.eval(farSrc + ring6) + u_condensate.eval(farSrc + ring7)) * half4(0.125);
503
651
 
504
652
  // Gravity's "denser layer at the bottom of the chamber" \u2014 a compositing-only boost (see
505
653
  // MEDIUM_DEFAULTS.gravityBottomBoost): it scales how density already in the conserved buffers is
@@ -745,7 +893,7 @@ function createMediumRuntime(gl, vertexSource) {
745
893
  }
746
894
  resizeGrid(w, h);
747
895
  }
748
- function bindVelocityUniforms(loc, dt, phase2, p) {
896
+ function bindVelocityUniforms(loc, dt, phase2, p, spatial) {
749
897
  web.setUniform(gl, loc("u_dyeSize"), [gridW, gridH]);
750
898
  web.setUniform(gl, loc("u_resolution"), [gridW, gridH]);
751
899
  web.setUniform(gl, loc("u_dt"), dt);
@@ -754,6 +902,10 @@ function createMediumRuntime(gl, vertexSource) {
754
902
  web.setUniform(gl, loc("u_advectSpeed"), p.advectSpeed);
755
903
  web.setUniform(gl, loc("u_turbulence"), p.turbulence);
756
904
  web.setUniform(gl, loc("u_vaporDrift"), p.gravityVaporDrift);
905
+ web.setUniform(gl, loc("u_spatial0"), [spatial[0].freqX, spatial[0].freqY, spatial[0].periodX, spatial[0].periodY]);
906
+ web.setUniform(gl, loc("u_spatial1"), [spatial[1].freqX, spatial[1].freqY, spatial[1].periodX, spatial[1].periodY]);
907
+ web.setUniform(gl, loc("u_spatial2"), [spatial[2].freqX, spatial[2].freqY, spatial[2].periodX, spatial[2].periodY]);
908
+ web.setUniform(gl, loc("u_spatial3"), [spatial[3].freqX, spatial[3].freqY, spatial[3].periodX, spatial[3].periodY]);
757
909
  }
758
910
  let phase = 0;
759
911
  function wrapPhase(value) {
@@ -770,6 +922,7 @@ function createMediumRuntime(gl, vertexSource) {
770
922
  return;
771
923
  }
772
924
  const p = { ...MEDIUM_DEFAULTS, ...params };
925
+ const spatial = computeMediumSpatialPeriods(gridW, gridH, p.curlFreq);
773
926
  const clampedDt = Math.min(Math.max(dt, 0), 1 / 15);
774
927
  phase = wrapPhase(phase + clampedDt * p.curlSpeed);
775
928
  const back = front === 0 ? 1 : 0;
@@ -779,28 +932,30 @@ function createMediumRuntime(gl, vertexSource) {
779
932
  gl.bindFramebuffer(gl.FRAMEBUFFER, vaporForward.fbo);
780
933
  gl.useProgram(vaporForwardProgram);
781
934
  web.bindTextureAt(gl, 0, vapor[front].texture, vaporForwardProgram, "u_dye");
782
- bindVelocityUniforms(vaporForwardLoc, clampedDt, phase, p);
935
+ bindVelocityUniforms(vaporForwardLoc, clampedDt, phase, p, spatial);
783
936
  web.drawFullscreenTriangle(gl);
784
937
  gl.bindFramebuffer(gl.FRAMEBUFFER, condensateForward.fbo);
785
938
  gl.useProgram(condensateForwardProgram);
786
939
  web.bindTextureAt(gl, 0, condensate[front].texture, condensateForwardProgram, "u_dye");
787
- bindVelocityUniforms(condensateForwardLoc, clampedDt, phase, p);
940
+ bindVelocityUniforms(condensateForwardLoc, clampedDt, phase, p, spatial);
788
941
  web.setUniform(gl, condensateForwardLoc("u_settleSpeed"), p.condensateSettleSpeed);
942
+ web.setUniform(gl, condensateForwardLoc("u_settleWiggle"), p.condensateSettleWiggle);
789
943
  web.drawFullscreenTriangle(gl);
790
944
  gl.bindFramebuffer(gl.FRAMEBUFFER, vaporCorrected.fbo);
791
945
  gl.useProgram(vaporCorrectProgram);
792
946
  web.bindTextureAt(gl, 0, vapor[front].texture, vaporCorrectProgram, "u_dye");
793
947
  web.bindTextureAt(gl, 1, vaporForward.texture, vaporCorrectProgram, "u_forward");
794
948
  web.setUniform(gl, vaporCorrectLoc("u_forwardSize"), gridSize);
795
- bindVelocityUniforms(vaporCorrectLoc, clampedDt, phase, p);
949
+ bindVelocityUniforms(vaporCorrectLoc, clampedDt, phase, p, spatial);
796
950
  web.drawFullscreenTriangle(gl);
797
951
  gl.bindFramebuffer(gl.FRAMEBUFFER, condensateCorrected.fbo);
798
952
  gl.useProgram(condensateCorrectProgram);
799
953
  web.bindTextureAt(gl, 0, condensate[front].texture, condensateCorrectProgram, "u_dye");
800
954
  web.bindTextureAt(gl, 1, condensateForward.texture, condensateCorrectProgram, "u_forward");
801
955
  web.setUniform(gl, condensateCorrectLoc("u_forwardSize"), gridSize);
802
- bindVelocityUniforms(condensateCorrectLoc, clampedDt, phase, p);
956
+ bindVelocityUniforms(condensateCorrectLoc, clampedDt, phase, p, spatial);
803
957
  web.setUniform(gl, condensateCorrectLoc("u_settleSpeed"), p.condensateSettleSpeed);
958
+ web.setUniform(gl, condensateCorrectLoc("u_settleWiggle"), p.condensateSettleWiggle);
804
959
  web.drawFullscreenTriangle(gl);
805
960
  gl.bindFramebuffer(gl.FRAMEBUFFER, vapor[back].fbo);
806
961
  gl.useProgram(vaporReactProgram);
@@ -834,7 +989,7 @@ function createMediumRuntime(gl, vertexSource) {
834
989
  gl.bindFramebuffer(gl.FRAMEBUFFER, track[back].fbo);
835
990
  gl.useProgram(trackAdvectProgram);
836
991
  web.bindTextureAt(gl, 0, track[front].texture, trackAdvectProgram, "u_dye");
837
- bindVelocityUniforms(trackAdvectLoc, clampedDt, phase, p);
992
+ bindVelocityUniforms(trackAdvectLoc, clampedDt, phase, p, spatial);
838
993
  web.setUniform(gl, trackAdvectLoc("u_decay"), p.decay);
839
994
  web.drawFullscreenTriangle(gl);
840
995
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);