vireuikit 0.3.0 → 0.4.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.
@@ -199,6 +199,51 @@ float2 vgCurlVelocitySeamless(float2 p, float phase, float4 s0, float4 s1, float
199
199
  return float2(py1 - py0, -(px1 - px0)) / (2.0 * e);
200
200
  }
201
201
  `;
202
+ function hash31JS(x, y, z) {
203
+ const h = x * 127.1 + y * 311.7 + z * 74.7;
204
+ const s = Math.sin(h) * 43758.5453123;
205
+ return s - Math.floor(s);
206
+ }
207
+ var smoothstep3JS = (f) => f * f * (3 - 2 * f);
208
+ var glslMod = (x, y) => x - y * Math.floor(x / y);
209
+ function vgValueNoise3PeriodicJS(px, py, pz, period) {
210
+ const ix = Math.floor(px);
211
+ const iy = Math.floor(py);
212
+ const iz = Math.floor(pz);
213
+ const ux = smoothstep3JS(px - ix);
214
+ const uy = smoothstep3JS(py - iy);
215
+ const uz = smoothstep3JS(pz - iz);
216
+ const z0 = glslMod(iz, period);
217
+ const z1 = glslMod(iz + 1, period);
218
+ const n000 = hash31JS(ix, iy, z0);
219
+ const n100 = hash31JS(ix + 1, iy, z0);
220
+ const n010 = hash31JS(ix, iy + 1, z0);
221
+ const n110 = hash31JS(ix + 1, iy + 1, z0);
222
+ const n001 = hash31JS(ix, iy, z1);
223
+ const n101 = hash31JS(ix + 1, iy, z1);
224
+ const n011 = hash31JS(ix, iy + 1, z1);
225
+ const n111 = hash31JS(ix + 1, iy + 1, z1);
226
+ const nx00 = n000 + (n100 - n000) * ux;
227
+ const nx10 = n010 + (n110 - n010) * ux;
228
+ const nx01 = n001 + (n101 - n001) * ux;
229
+ const nx11 = n011 + (n111 - n011) * ux;
230
+ const nxy0 = nx00 + (nx10 - nx00) * uy;
231
+ const nxy1 = nx01 + (nx11 - nx01) * uy;
232
+ return (nxy0 + (nxy1 - nxy0) * uz) * 2 - 1;
233
+ }
234
+ function vgPotentialJS(px, py, phase) {
235
+ const [o0, o1, o2, o3] = MEDIUM_TIME_OCTAVES;
236
+ let v = vgValueNoise3PeriodicJS(px * o0.scale, py * o0.scale, phase * o0.rate + o0.offset, mediumOctavePeriod(o0.rate)) * 0.5;
237
+ v += vgValueNoise3PeriodicJS(px * o1.scale, py * o1.scale, phase * o1.rate + o1.offset, mediumOctavePeriod(o1.rate)) * 0.25;
238
+ v += vgValueNoise3PeriodicJS(px * o2.scale, py * o2.scale, phase * o2.rate + o2.offset, mediumOctavePeriod(o2.rate)) * 0.125;
239
+ const macro = vgValueNoise3PeriodicJS(
240
+ px * o3.scale,
241
+ py * o3.scale,
242
+ phase * o3.rate + o3.offset,
243
+ mediumOctavePeriod(o3.rate)
244
+ );
245
+ return v * (0.7 + 0.3 * macro);
246
+ }
202
247
  var MIN_SPATIAL_PERIOD = 2;
203
248
  function computeMediumSpatialPeriods(gridWidth, gridHeight, curlFreq) {
204
249
  const freq = Math.max(curlFreq, 1e-6);
@@ -307,9 +352,46 @@ function neutralSpecies(baseColor) {
307
352
  }
308
353
 
309
354
  // src/medium/params.ts
355
+ var MEDIUM_MAX_LIGHTS = 3;
310
356
  var BASE_COLOR = [0.07, 0.08, 0.1];
357
+ var normalize2 = (v) => {
358
+ const len = Math.hypot(v[0], v[1]) || 1;
359
+ return [v[0] / len, v[1] / len];
360
+ };
361
+ var MEDIUM_DEFAULT_LIGHT_COLOR = [0.78, 0.85, 0.94];
362
+ var MEDIUM_DEFAULT_LIGHTS = [
363
+ {
364
+ position: [0.5, 0],
365
+ direction: [0, 1],
366
+ coneAngle: Math.PI,
367
+ falloff: 16,
368
+ color: MEDIUM_DEFAULT_LIGHT_COLOR,
369
+ intensity: 0.28
370
+ },
371
+ {
372
+ position: [0.12, 1.45],
373
+ direction: normalize2([0.38, -1.03]),
374
+ coneAngle: 9 * Math.PI / 180,
375
+ falloff: 0.7,
376
+ color: MEDIUM_DEFAULT_LIGHT_COLOR,
377
+ intensity: 1.1
378
+ },
379
+ {
380
+ position: [0.9, 1.4],
381
+ direction: normalize2([-0.4, -0.98]),
382
+ coneAngle: 8 * Math.PI / 180,
383
+ falloff: 0.75,
384
+ color: MEDIUM_DEFAULT_LIGHT_COLOR,
385
+ intensity: 0.9
386
+ }
387
+ ];
311
388
  var MEDIUM_DEFAULTS = {
312
- decay: 0.06,
389
+ // A stamped track has to visibly broaden and fade within the reference's "roughly one to two
390
+ // seconds", not the ~11s half-life a 0.06 decay gives — measured against check-medium.mjs's track
391
+ // gate (birth vs +1.5s cross-section width and total density): 1.1 leaves ~30% of the peak at 1s
392
+ // and ~17% at 1.5s, comfortably faded without dying before MacCormack's own diffusion has time to
393
+ // visibly broaden the stamp.
394
+ decay: 1.1,
313
395
  curlFreq: 0.045,
314
396
  curlSpeed: 0.12,
315
397
  advectSpeed: 26,
@@ -324,6 +406,7 @@ var MEDIUM_DEFAULTS = {
324
406
  condensateGain: 5,
325
407
  condensateColor: [0.94, 0.95, 0.97],
326
408
  channelColors: neutralSpecies(BASE_COLOR),
409
+ channelScatter: [1, 1, 1],
327
410
  baseColor: BASE_COLOR,
328
411
  baseWeight: 0.9,
329
412
  depthFarScale: 1.6,
@@ -332,11 +415,61 @@ var MEDIUM_DEFAULTS = {
332
415
  depthFarWeight: 0.5,
333
416
  gravityVaporDrift: 0.6,
334
417
  gravityBottomBoost: 2,
335
- gravityBottomBoostStart: 0.75
418
+ gravityBottomBoostStart: 0.75,
419
+ lights: MEDIUM_DEFAULT_LIGHTS,
420
+ shadowExtinction: 3,
421
+ trackLightFloor: 0.8,
422
+ trackLayerAmount: 1
336
423
  };
337
424
  function depthFarOffsetPx(offsetFrac, gridWidth, gridHeight) {
338
425
  return [offsetFrac[0] * gridWidth, offsetFrac[1] * gridHeight];
339
426
  }
427
+ var MEDIUM_TRACK_LAYER_LIFE_SECONDS = {
428
+ alpha: 1.8,
429
+ electron: 1.4,
430
+ muon: 2.2
431
+ };
432
+ var MEDIUM_TRACK_LAYER_MAX_TRACKS = 64;
433
+ var MEDIUM_TRACK_LAYER_LOOK = {
434
+ alpha: {
435
+ sigmaFrac: 22e-4,
436
+ endSigmaMul: 1.9,
437
+ broaden: 3.2,
438
+ wiggleFrac: 0,
439
+ beadFrac: 6e-3,
440
+ dropletFrac: 35e-4,
441
+ dropletChance: 0.5,
442
+ startFrac: 0.035,
443
+ gain: 1.8
444
+ },
445
+ electron: {
446
+ sigmaFrac: 9e-4,
447
+ endSigmaMul: 1.3,
448
+ broaden: 6,
449
+ wiggleFrac: 0.012,
450
+ beadFrac: 8e-3,
451
+ dropletFrac: 6e-3,
452
+ dropletChance: 0.55,
453
+ startFrac: 0,
454
+ gain: 2.2
455
+ },
456
+ muon: {
457
+ sigmaFrac: 8e-4,
458
+ endSigmaMul: 1,
459
+ broaden: 5,
460
+ wiggleFrac: 0,
461
+ beadFrac: 7e-3,
462
+ dropletFrac: 5e-3,
463
+ dropletChance: 0.5,
464
+ startFrac: 0,
465
+ gain: 2.2
466
+ }
467
+ };
468
+ var MEDIUM_TRACK_LAYER_BURST_SPREAD = 1.6;
469
+ var MEDIUM_TRACK_LAYER_BURST_ACCENT = 3;
470
+ var MEDIUM_TRACK_LAYER_FADE_GAMMA = 1.6;
471
+ var MEDIUM_TRACK_LAYER_GRAVITY_SAG = 0.018;
472
+ var MEDIUM_TRACK_LAYER_CURL_PUSH = 0.05;
340
473
 
341
474
  // src/medium/seed-shader.ts
342
475
  var MEDIUM_SEED_SHADER = `
@@ -589,10 +722,35 @@ half4 ${fnName}(float2 px) {
589
722
  }
590
723
  `;
591
724
  }
725
+ var VG_LIGHT = `
726
+ float vgLightAmount(float2 xy, float2 lightPosFrac, float2 lightDir, float cosCone, float falloff, float2 resolution, float seed) {
727
+ float2 rel = xy - lightPosFrac * resolution;
728
+ // A cone of 180\xB0 or more is an edge strip: light spreads from a line, not a point.
729
+ if (cosCone <= -0.999) {
730
+ float depth = max(dot(rel, lightDir), 0.0);
731
+ return exp(-(depth / max(resolution.y, 1.0)) * falloff);
732
+ }
733
+ float dist = length(rel);
734
+ float2 dirN = dist > 1e-4 ? rel / dist : lightDir;
735
+ float cosAngle = dot(dirN, lightDir);
736
+ // A soft lobe with half intensity at the cone angle and no boundary at all: a lamp in haze has
737
+ // no drawn edge.
738
+ float lobe = log(0.5) / log(clamp(cosCone, 0.05, 0.999));
739
+ float edge = pow(max(cosAngle, 0.0), lobe);
740
+ float atten = exp(-(dist / max(resolution.y, 1.0)) * falloff);
741
+ if (edge * atten < 0.001) return 0.0;
742
+ // Static shafts inside the beam: brightness varies with the angle around the lamp, so streaks
743
+ // fan out from the source the way light through dusty air does.
744
+ float angle = atan(dirN.x * lightDir.y - dirN.y * lightDir.x, cosAngle);
745
+ float shafts = 0.6 + 0.32 * vgValueNoise(float2(angle * 14.0, seed)) + 0.18 * vgValueNoise(float2(angle * 33.0, seed + 5.3));
746
+ return edge * atten * clamp(shafts, 0.0, 1.0);
747
+ }
748
+ `;
592
749
  var MEDIUM_COMPOSITE_SHADER = `
593
750
  uniform shader u_vapor;
594
751
  uniform shader u_condensate;
595
752
  uniform shader u_track;
753
+ uniform shader u_shadow;
596
754
  uniform float2 u_dyeScale;
597
755
  uniform float3 u_lab0;
598
756
  uniform float3 u_lab1;
@@ -608,11 +766,33 @@ uniform float u_farBlurRadius;
608
766
  uniform float u_farWeight;
609
767
  uniform float u_gravityBoost;
610
768
  uniform float u_gravityBoostStart;
769
+ uniform float3 u_channelScatter;
770
+ uniform float2 u_light0Pos;
771
+ uniform float2 u_light0Dir;
772
+ uniform float u_light0CosCone;
773
+ uniform float u_light0Falloff;
774
+ uniform float3 u_light0Color;
775
+ uniform float u_light0Intensity;
776
+ uniform float2 u_light1Pos;
777
+ uniform float2 u_light1Dir;
778
+ uniform float u_light1CosCone;
779
+ uniform float u_light1Falloff;
780
+ uniform float3 u_light1Color;
781
+ uniform float u_light1Intensity;
782
+ uniform float2 u_light2Pos;
783
+ uniform float2 u_light2Dir;
784
+ uniform float u_light2CosCone;
785
+ uniform float u_light2Falloff;
786
+ uniform float3 u_light2Color;
787
+ uniform float u_light2Intensity;
788
+ uniform float u_trackLightFloor;
611
789
 
612
790
  ${VG_OKLAB_TO_SRGB}
613
791
  ${VG_CUBIC_WEIGHTS}
614
792
  ${vgBicubicSampler("vgBicubicVapor", "u_vapor")}
615
793
  ${vgBicubicSampler("vgBicubicCondensate", "u_condensate")}
794
+ ${VG_VALUE_NOISE}
795
+ ${VG_LIGHT}
616
796
 
617
797
  half4 main(float2 xy) {
618
798
  float2 baseSrc = xy * u_dyeScale;
@@ -680,7 +860,34 @@ half4 main(float2 xy) {
680
860
  float total = max(w0 + w1 + w2 + wBg + wCondensate, 1e-4);
681
861
  float3 mixLab =
682
862
  (u_labBg * wBg + u_lab0 * w0 + u_lab1 * w1 + u_lab2 * w2 + condensateLab * wCondensate) / total;
683
- float3 rgbLinear = clamp(vgOklabToLinear(mixLab), float3(0.0), float3(1.0));
863
+ // The dark ambient base \u2014 this is where the OLD mechanism stops (see the file header on
864
+ // VG_LIGHT): unclamped here, clamped once at the very end, after the lit layer adds in.
865
+ float3 rgbLinearBase = vgOklabToLinear(mixLab);
866
+
867
+ // Spotlights reach this point dimmed by the gas in front of them (shadow-shader.ts).
868
+ half4 shadow = u_shadow.eval(baseSrc);
869
+ float light0 = vgLightAmount(xy, u_light0Pos, u_light0Dir, u_light0CosCone, u_light0Falloff, u_resolution, 1.7) * u_light0Intensity;
870
+ float light1 = vgLightAmount(xy, u_light1Pos, u_light1Dir, u_light1CosCone, u_light1Falloff, u_resolution, 4.1) * u_light1Intensity
871
+ * float(shadow.r);
872
+ float light2 = vgLightAmount(xy, u_light2Pos, u_light2Dir, u_light2CosCone, u_light2Falloff, u_resolution, 8.3) * u_light2Intensity
873
+ * float(shadow.g);
874
+ float3 illum = u_light0Color * light0 + u_light1Color * light1 + u_light2Color * light2;
875
+
876
+ float totalScatter = gasR * u_channelScatter.x + gasG * u_channelScatter.y + gasB * u_channelScatter.z;
877
+ float fogDensity = totalScatter + wCondensate;
878
+ float mist = 0.006 + fogDensity * 0.2;
879
+
880
+ // TRACKS: lit the same way as the mist, plus a floor so a track already on screen never vanishes
881
+ // entirely just because it drifted out of a cone (a fresh ionization trail reads brighter than
882
+ // ambient mist in a real chamber). The floor is added to ITS OWN color, not multiplied into illum:
883
+ // illum is exactly [0,0,0] in a fully unlit spot (every light's amount is 0 there), so a floor
884
+ // multiplied by illum would vanish exactly where it exists to help \u2014 it needs a color of its own
885
+ // (a neutral, self-luminous glow) rather than borrowing whichever light happens to be nearby.
886
+ float trackDensity = max(float(track.r), max(float(track.g), float(track.b)));
887
+ float3 trackGlow = (1.0 - exp(-trackDensity * 0.4)) * (illum + float3(u_trackLightFloor)) * 0.08;
888
+
889
+ float3 rgbLinear = rgbLinearBase * 0.025 + illum * mist + trackGlow;
890
+ rgbLinear = float3(1.0) - exp(-rgbLinear * 1.0);
684
891
  float3 srgb = vgLinearToSrgb(rgbLinear);
685
892
  return half4(half3(srgb), half(1.0));
686
893
  }
@@ -698,7 +905,6 @@ uniform float u_settle;
698
905
  uniform float u_tailDim;
699
906
  uniform float u_intensity;
700
907
  uniform float u_seed;
701
- uniform float3 u_channelMask;
702
908
 
703
909
  ${VG_VALUE_NOISE}
704
910
 
@@ -721,10 +927,407 @@ half4 main(float2 xy) {
721
927
  best = max(best, v);
722
928
  }
723
929
 
724
- float3 dye = u_channelMask * (best * u_intensity);
930
+ float3 dye = float3(best * u_intensity);
725
931
  return half4(half3(dye), half(1.0));
726
932
  }
727
933
  `;
934
+
935
+ // src/medium/shadow-shader.ts
936
+ var MEDIUM_SHADOW_SHADER = `
937
+ uniform shader u_vapor;
938
+ uniform shader u_condensate;
939
+ uniform float u_condensateGain;
940
+ uniform float2 u_light1Grid;
941
+ uniform float2 u_light2Grid;
942
+ uniform float u_extinction;
943
+
944
+ float vgOpticalDepth(float2 p, float2 lightP) {
945
+ float2 toLight = lightP - p;
946
+ float optical = 0.0;
947
+ for (int i = 1; i <= 24; i++) {
948
+ float2 q = p + toLight * (float(i) / 24.0);
949
+ float inside = step(0.0, q.x) * step(q.x, u_resolution.x) * step(0.0, q.y) * step(q.y, u_resolution.y);
950
+ half4 v = u_vapor.eval(q);
951
+ half4 c = u_condensate.eval(q);
952
+ optical += inside * (float(v.r + v.g + v.b) + float(c.r) * u_condensateGain);
953
+ }
954
+ return optical * length(toLight) / 24.0 / max(u_resolution.y, 1.0);
955
+ }
956
+
957
+ half4 main(float2 xy) {
958
+ float2 p = float2(xy.x, u_resolution.y - xy.y);
959
+ float t1 = exp(-vgOpticalDepth(p, u_light1Grid) * u_extinction);
960
+ float t2 = exp(-vgOpticalDepth(p, u_light2Grid) * u_extinction);
961
+ return half4(half(t1), half(t2), half(0.0), half(1.0));
962
+ }
963
+ `;
964
+
965
+ // src/web/track-layer.ts
966
+ function trackLayerFade(age, life) {
967
+ if (life <= 0) return 0;
968
+ const t = Math.min(Math.max(age / life, 0), 1);
969
+ return (1 - t) ** MEDIUM_TRACK_LAYER_FADE_GAMMA;
970
+ }
971
+ function trackLayerSigmaMul(age, broaden) {
972
+ return Math.sqrt(1 + broaden * Math.max(age, 0));
973
+ }
974
+ function hash01(n) {
975
+ const s = Math.sin(n * 91.3458) * 47453.5453;
976
+ return s - Math.floor(s);
977
+ }
978
+ function curlVelocityJS(px, py, phase) {
979
+ const e = 0.6;
980
+ const px1 = vgPotentialJS(px + e, py, phase);
981
+ const px0 = vgPotentialJS(px - e, py, phase);
982
+ const py1 = vgPotentialJS(px, py + e, phase);
983
+ const py0 = vgPotentialJS(px, py - e, phase);
984
+ return [(py1 - py0) / (2 * e), -(px1 - px0) / (2 * e)];
985
+ }
986
+ var CURL_SAMPLE_SCALE = 40;
987
+ var PHASE_WRAP = 1e4;
988
+ function createTrackLayer() {
989
+ let tracks = [];
990
+ let phase = 0;
991
+ function step(dt, emissions) {
992
+ phase = (phase + dt) % PHASE_WRAP;
993
+ const next = [];
994
+ for (const track of tracks) {
995
+ const life = MEDIUM_TRACK_LAYER_LIFE_SECONDS[track.emission.kind];
996
+ const age = track.age + dt;
997
+ if (age >= life) continue;
998
+ if (age <= 0) {
999
+ next.push({ ...track, age });
1000
+ continue;
1001
+ }
1002
+ const midX = track.emission.source[0] + Math.cos(track.emission.angle) * track.emission.lengthFrac * 0.5;
1003
+ const midY = track.emission.source[1] + Math.sin(track.emission.angle) * track.emission.lengthFrac * 0.5;
1004
+ const [curlX, curlY] = curlVelocityJS(midX * CURL_SAMPLE_SCALE, midY * CURL_SAMPLE_SCALE, phase);
1005
+ const activeDt = Math.min(dt, age);
1006
+ next.push({
1007
+ emission: track.emission,
1008
+ age,
1009
+ driftX: track.driftX + curlX * MEDIUM_TRACK_LAYER_CURL_PUSH * activeDt,
1010
+ driftY: track.driftY + (curlY * MEDIUM_TRACK_LAYER_CURL_PUSH - MEDIUM_TRACK_LAYER_GRAVITY_SAG) * activeDt
1011
+ });
1012
+ }
1013
+ let burstIndex = 0;
1014
+ for (const emission of emissions) {
1015
+ let age = 0;
1016
+ if (emission.kind === "alpha") {
1017
+ if (burstIndex >= MEDIUM_TRACK_LAYER_BURST_ACCENT) {
1018
+ age = -hash01(emission.seed * 3.17 + burstIndex) * MEDIUM_TRACK_LAYER_BURST_SPREAD;
1019
+ }
1020
+ burstIndex += 1;
1021
+ }
1022
+ next.push({ emission, age, driftX: 0, driftY: 0 });
1023
+ }
1024
+ tracks = next.length > MEDIUM_TRACK_LAYER_MAX_TRACKS ? next.slice(next.length - MEDIUM_TRACK_LAYER_MAX_TRACKS) : next;
1025
+ return tracks;
1026
+ }
1027
+ return { step };
1028
+ }
1029
+ var TRACK_LAYER_VERTEX_SOURCE = `#version 300 es
1030
+ layout(location = 0) in vec2 a_corner;
1031
+ layout(location = 1) in vec4 a_geom;
1032
+ layout(location = 2) in vec4 a_span;
1033
+ layout(location = 3) in vec4 a_path;
1034
+ layout(location = 4) in vec4 a_grain;
1035
+
1036
+ uniform vec2 u_resolution;
1037
+ uniform float u_trackLightFloor;
1038
+ uniform sampler2D u_shadow;
1039
+ uniform vec2 u_light0Pos;
1040
+ uniform vec2 u_light0Dir;
1041
+ uniform float u_light0CosCone;
1042
+ uniform float u_light0Falloff;
1043
+ uniform vec3 u_light0Color;
1044
+ uniform float u_light0Intensity;
1045
+ uniform vec2 u_light1Pos;
1046
+ uniform vec2 u_light1Dir;
1047
+ uniform float u_light1CosCone;
1048
+ uniform float u_light1Falloff;
1049
+ uniform vec3 u_light1Color;
1050
+ uniform float u_light1Intensity;
1051
+ uniform vec2 u_light2Pos;
1052
+ uniform vec2 u_light2Dir;
1053
+ uniform float u_light2CosCone;
1054
+ uniform float u_light2Falloff;
1055
+ uniform vec3 u_light2Color;
1056
+ uniform float u_light2Intensity;
1057
+
1058
+ out vec2 v_local;
1059
+ flat out vec4 v_span;
1060
+ flat out vec4 v_path;
1061
+ flat out vec4 v_grain;
1062
+ flat out float v_reach;
1063
+ out vec3 v_illum;
1064
+
1065
+ float vgHash21(vec2 p) {
1066
+ float h = dot(p, vec2(127.1, 311.7));
1067
+ return fract(sin(h) * 43758.5453123);
1068
+ }
1069
+
1070
+ float vgValueNoise(vec2 p) {
1071
+ vec2 i = floor(p);
1072
+ vec2 f = fract(p);
1073
+ float a = vgHash21(i);
1074
+ float b = vgHash21(i + vec2(1.0, 0.0));
1075
+ float c = vgHash21(i + vec2(0.0, 1.0));
1076
+ float d = vgHash21(i + vec2(1.0, 1.0));
1077
+ vec2 u = f * f * (3.0 - 2.0 * f);
1078
+ return mix(mix(a, b, u.x), mix(c, d, u.x), u.y) * 2.0 - 1.0;
1079
+ }
1080
+
1081
+ // Keep in step with vgLightAmount in composite-shader.ts.
1082
+ float vgLightAmount(vec2 xy, vec2 lightPosFrac, vec2 lightDir, float cosCone, float falloff, vec2 resolution, float seed) {
1083
+ vec2 rel = xy - lightPosFrac * resolution;
1084
+ if (cosCone <= -0.999) {
1085
+ float depth = max(dot(rel, lightDir), 0.0);
1086
+ return exp(-(depth / max(resolution.y, 1.0)) * falloff);
1087
+ }
1088
+ float dist = length(rel);
1089
+ vec2 dirN = dist > 1e-4 ? rel / dist : lightDir;
1090
+ float cosAngle = dot(dirN, lightDir);
1091
+ float lobe = log(0.5) / log(clamp(cosCone, 0.05, 0.999));
1092
+ float edge = pow(max(cosAngle, 0.0), lobe);
1093
+ float atten = exp(-(dist / max(resolution.y, 1.0)) * falloff);
1094
+ if (edge * atten < 0.001) return 0.0;
1095
+ float angle = atan(dirN.x * lightDir.y - dirN.y * lightDir.x, cosAngle);
1096
+ float shafts = 0.6 + 0.32 * vgValueNoise(vec2(angle * 14.0, seed)) + 0.18 * vgValueNoise(vec2(angle * 33.0, seed + 5.3));
1097
+ return edge * atten * clamp(shafts, 0.0, 1.0);
1098
+ }
1099
+
1100
+ void main() {
1101
+ vec2 dir = a_geom.zw;
1102
+ vec2 side = vec2(-dir.y, dir.x);
1103
+ // Beads widen sigma by up to 20%; 3.5 sigma leaves under 1% of the core outside the quad.
1104
+ float reach = max(a_span.z, a_span.w) * 1.2 * 3.5 + 1.5;
1105
+ float margin = reach + a_path.x * 1.7;
1106
+ float u = mix(a_span.y - reach, a_span.x + reach, a_corner.y);
1107
+ float s = a_corner.x * margin;
1108
+ vec2 pos = a_geom.xy + dir * u + side * s;
1109
+ v_local = vec2(u, s);
1110
+ v_span = a_span;
1111
+ v_path = a_path;
1112
+ v_grain = a_grain;
1113
+ v_reach = reach;
1114
+ // Lit per corner: light varies over hundreds of px, a track is a few px wide. The backdrop
1115
+ // target stores the scene top row at y = 0; lights use y up.
1116
+ vec2 xy = vec2(pos.x, u_resolution.y - pos.y);
1117
+ // The same gas shadow the composite applies to the spotlights (shadow-shader.ts, y up).
1118
+ vec4 shadow = texture(u_shadow, xy / u_resolution);
1119
+ float light0 = vgLightAmount(xy, u_light0Pos, u_light0Dir, u_light0CosCone, u_light0Falloff, u_resolution, 1.7) * u_light0Intensity;
1120
+ float light1 = vgLightAmount(xy, u_light1Pos, u_light1Dir, u_light1CosCone, u_light1Falloff, u_resolution, 4.1) * u_light1Intensity * shadow.r;
1121
+ float light2 = vgLightAmount(xy, u_light2Pos, u_light2Dir, u_light2CosCone, u_light2Falloff, u_resolution, 8.3) * u_light2Intensity * shadow.g;
1122
+ v_illum = u_light0Color * light0 + u_light1Color * light1 + u_light2Color * light2 + vec3(u_trackLightFloor);
1123
+ gl_Position = vec4((pos / u_resolution) * 2.0 - 1.0, 0.0, 1.0);
1124
+ }
1125
+ `;
1126
+ var TRACK_LAYER_FRAGMENT_SOURCE = `#version 300 es
1127
+ precision highp float;
1128
+
1129
+ in vec2 v_local;
1130
+ flat in vec4 v_span;
1131
+ flat in vec4 v_path;
1132
+ flat in vec4 v_grain;
1133
+ flat in float v_reach;
1134
+ in vec3 v_illum;
1135
+
1136
+ out vec4 fragColor;
1137
+
1138
+ float vgHash(float a, float b) {
1139
+ uint x = floatBitsToUint(a) * 0x9E3779B1u ^ (floatBitsToUint(b) + 0x7F4A7C15u);
1140
+ x ^= x >> 16;
1141
+ x *= 0x7FEB352Du;
1142
+ x ^= x >> 15;
1143
+ x *= 0x846CA68Bu;
1144
+ x ^= x >> 16;
1145
+ return float(x) * (1.0 / 4294967295.0);
1146
+ }
1147
+
1148
+ float vgNoise1(float x, float salt) {
1149
+ float i = floor(x);
1150
+ float f = fract(x);
1151
+ float u = f * f * (3.0 - 2.0 * f);
1152
+ return mix(vgHash(i, salt), vgHash(i + 1.0, salt), u);
1153
+ }
1154
+
1155
+ void main() {
1156
+ float len = v_span.x;
1157
+ float start = v_span.y;
1158
+ float sigma0 = v_span.z;
1159
+ float sigma1 = v_span.w;
1160
+ float wiggleAmp = v_path.x;
1161
+ float wiggleFreq = v_path.y;
1162
+ float amp = v_path.z;
1163
+ float seed = v_path.w;
1164
+ float beadPx = v_grain.x;
1165
+ float dropPx = v_grain.y;
1166
+ float dropChance = v_grain.z;
1167
+ float blur = v_grain.w;
1168
+
1169
+ float u = v_local.x;
1170
+ float s = v_local.y;
1171
+ float t = clamp((u - start) / max(len - start, 1.0), 0.0, 1.0);
1172
+
1173
+ float wiggle = 0.0;
1174
+ if (wiggleAmp > 0.0) {
1175
+ float x = u * wiggleFreq;
1176
+ wiggle = wiggleAmp * smoothstep(0.0, 0.2, t) *
1177
+ ((vgNoise1(x, seed) - 0.5) * 2.0 + (vgNoise1(x * 3.1, seed + 1.0) - 0.5) * 0.7);
1178
+ }
1179
+ float across = s - wiggle;
1180
+ if (abs(across) > v_reach) discard;
1181
+
1182
+ float bead = vgNoise1(u / beadPx, seed + 2.0);
1183
+ // Blur (age, defocus) washes the beads out rather than stretching them into ribs.
1184
+ float sigma = mix(sigma0, sigma1, t * t) * (1.0 + (bead - 0.5) * 0.4 / blur);
1185
+ float beyond = u < start ? start - u : max(u - len, 0.0);
1186
+ float core = exp(-0.5 * (across * across + beyond * beyond) / (sigma * sigma));
1187
+ float along = 1.0 - 0.35 / blur * vgNoise1(u / (beadPx * 2.3), seed + 3.0);
1188
+
1189
+ float drops = 0.0;
1190
+ float cell0 = floor(u / dropPx);
1191
+ for (int k = -1; k <= 1; k++) {
1192
+ float c = cell0 + float(k);
1193
+ if (vgHash(c, seed + 4.0) > dropChance) continue;
1194
+ float cu = (c + vgHash(c, seed + 5.0)) * dropPx;
1195
+ if (cu < start || cu > len) continue;
1196
+ float cs = (vgHash(c, seed + 6.0) + vgHash(c, seed + 7.0) - 1.0) * 1.7 * sigma + wiggle;
1197
+ float r = max(0.7, sigma * mix(0.35, 0.8, vgHash(c, seed + 8.0)));
1198
+ float dx = u - cu;
1199
+ float dy = s - cs;
1200
+ drops += mix(0.35, 1.0, vgHash(c, seed + 9.0)) * exp(-0.5 * (dx * dx + dy * dy) / (r * r));
1201
+ }
1202
+
1203
+ float lum = amp * (core * along + drops * 0.7 / (blur * blur));
1204
+ if (lum < 0.002) discard;
1205
+
1206
+ vec3 glow = lum * v_illum;
1207
+ fragColor = vec4(vec3(1.0) - exp(-glow), 1.0);
1208
+ }
1209
+ `;
1210
+ var FLOATS_PER_INSTANCE = 16;
1211
+ var DEFOCUS_FROM_DEPTH = 0.7;
1212
+ var DEFOCUS_MAX = 2.4;
1213
+ function buildTrackLayerInstances(tracks, contentWidth, contentHeight, amount) {
1214
+ const minDim = Math.min(contentWidth, contentHeight);
1215
+ const data = new Float32Array(tracks.length * FLOATS_PER_INSTANCE);
1216
+ let count = 0;
1217
+ if (amount <= 0) return { data, count };
1218
+ for (const track of tracks) {
1219
+ if (track.age < 0) continue;
1220
+ const e = track.emission;
1221
+ const fade = trackLayerFade(track.age, MEDIUM_TRACK_LAYER_LIFE_SECONDS[e.kind]);
1222
+ if (fade <= 1e-4) continue;
1223
+ const look = MEDIUM_TRACK_LAYER_LOOK[e.kind];
1224
+ const sigmaMul = trackLayerSigmaMul(track.age, look.broaden);
1225
+ const near = Math.max(0, (e.depth - DEFOCUS_FROM_DEPTH) / (1 - DEFOCUS_FROM_DEPTH));
1226
+ const defocus = 1 + (DEFOCUS_MAX - 1) * near * near;
1227
+ const sigma0 = Math.max(look.sigmaFrac * minDim, 0.6) * sigmaMul * defocus;
1228
+ const o = count * FLOATS_PER_INSTANCE;
1229
+ data[o + 0] = e.source[0] * contentWidth + track.driftX * minDim;
1230
+ data[o + 1] = e.source[1] * contentHeight - track.driftY * minDim;
1231
+ data[o + 2] = Math.cos(e.angle);
1232
+ data[o + 3] = Math.sin(e.angle);
1233
+ const len = e.lengthFrac * minDim;
1234
+ data[o + 4] = len;
1235
+ data[o + 5] = Math.min(look.startFrac * minDim, len * 0.5);
1236
+ data[o + 6] = sigma0;
1237
+ data[o + 7] = sigma0 * look.endSigmaMul;
1238
+ data[o + 8] = e.raggedFrac > 0 ? look.wiggleFrac * minDim : 0;
1239
+ data[o + 9] = e.raggedFreq / Math.max(len, 1);
1240
+ data[o + 10] = e.intensity * look.gain * fade * amount / (sigmaMul ** 0.7 * defocus);
1241
+ data[o + 11] = e.seed;
1242
+ data[o + 12] = look.beadFrac * minDim;
1243
+ data[o + 13] = look.dropletFrac * minDim;
1244
+ data[o + 14] = look.dropletChance;
1245
+ data[o + 15] = sigmaMul * defocus;
1246
+ count += 1;
1247
+ }
1248
+ return { data, count };
1249
+ }
1250
+ function compile(gl, type, source, label) {
1251
+ const shader = gl.createShader(type);
1252
+ if (!shader) throw new Error(`vireuikit: track layer ${label} shader creation failed`);
1253
+ gl.shaderSource(shader, source);
1254
+ gl.compileShader(shader);
1255
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
1256
+ throw new Error(`vireuikit: track layer ${label} shader failed:
1257
+ ${gl.getShaderInfoLog(shader) ?? ""}`);
1258
+ }
1259
+ return shader;
1260
+ }
1261
+ function createTrackLayerProgram(gl) {
1262
+ const vertex = compile(gl, gl.VERTEX_SHADER, TRACK_LAYER_VERTEX_SOURCE, "vertex");
1263
+ const fragment = compile(gl, gl.FRAGMENT_SHADER, TRACK_LAYER_FRAGMENT_SOURCE, "fragment");
1264
+ const program = gl.createProgram();
1265
+ if (!program) throw new Error("vireuikit: track layer program creation failed");
1266
+ gl.attachShader(program, vertex);
1267
+ gl.attachShader(program, fragment);
1268
+ gl.linkProgram(program);
1269
+ gl.deleteShader(vertex);
1270
+ gl.deleteShader(fragment);
1271
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
1272
+ throw new Error(`vireuikit: track layer program failed to link:
1273
+ ${gl.getProgramInfoLog(program) ?? ""}`);
1274
+ }
1275
+ const loc = web.locationCache(gl, program);
1276
+ const quadBuffer = gl.createBuffer();
1277
+ gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
1278
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, 0, 1, 0, -1, 1, 1, 1]), gl.STATIC_DRAW);
1279
+ const instanceBuffer = gl.createBuffer();
1280
+ const vao = gl.createVertexArray();
1281
+ gl.bindVertexArray(vao);
1282
+ gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
1283
+ gl.enableVertexAttribArray(0);
1284
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
1285
+ const stride = FLOATS_PER_INSTANCE * 4;
1286
+ gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);
1287
+ for (let location = 1; location <= 4; location += 1) {
1288
+ gl.enableVertexAttribArray(location);
1289
+ gl.vertexAttribPointer(location, 4, gl.FLOAT, false, stride, (location - 1) * 16);
1290
+ gl.vertexAttribDivisor(location, 1);
1291
+ }
1292
+ gl.bindVertexArray(null);
1293
+ function bindLight(slot, light) {
1294
+ const position = light?.position ?? [0, 0];
1295
+ const direction = light?.direction ?? [0, 1];
1296
+ const dirLen = Math.hypot(direction[0], direction[1]) || 1;
1297
+ web.setUniform(gl, loc(`u_light${slot}Pos`), position);
1298
+ web.setUniform(gl, loc(`u_light${slot}Dir`), [direction[0] / dirLen, direction[1] / dirLen]);
1299
+ web.setUniform(gl, loc(`u_light${slot}CosCone`), Math.cos(light?.coneAngle ?? 0));
1300
+ web.setUniform(gl, loc(`u_light${slot}Falloff`), light?.falloff ?? 0);
1301
+ web.setUniform(gl, loc(`u_light${slot}Color`), light?.color ?? [0, 0, 0]);
1302
+ web.setUniform(gl, loc(`u_light${slot}Intensity`), light?.intensity ?? 0);
1303
+ }
1304
+ function draw(contentWidth, contentHeight, data, count, trackLightFloor, lights, shadow) {
1305
+ if (count <= 0) return;
1306
+ gl.useProgram(program);
1307
+ gl.bindVertexArray(vao);
1308
+ gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);
1309
+ gl.bufferData(gl.ARRAY_BUFFER, data.subarray(0, count * FLOATS_PER_INSTANCE), gl.DYNAMIC_DRAW);
1310
+ web.setUniform(gl, loc("u_resolution"), [contentWidth, contentHeight]);
1311
+ web.setUniform(gl, loc("u_trackLightFloor"), trackLightFloor);
1312
+ for (let slot = 0; slot < 3; slot += 1) bindLight(slot, lights[slot]);
1313
+ web.bindTextureAt(gl, 0, shadow, program, "u_shadow");
1314
+ gl.enable(gl.BLEND);
1315
+ gl.blendFunc(gl.ONE, gl.ONE);
1316
+ gl.disable(gl.DEPTH_TEST);
1317
+ gl.viewport(0, 0, contentWidth, contentHeight);
1318
+ gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, count);
1319
+ gl.bindVertexArray(null);
1320
+ }
1321
+ function destroy() {
1322
+ gl.deleteBuffer(quadBuffer);
1323
+ gl.deleteBuffer(instanceBuffer);
1324
+ gl.deleteVertexArray(vao);
1325
+ gl.deleteProgram(program);
1326
+ }
1327
+ return { draw, destroy };
1328
+ }
1329
+
1330
+ // src/web/medium.ts
728
1331
  function pickDyeFormat(gl) {
729
1332
  const hasFloat = gl.getExtension("EXT_color_buffer_float") !== null;
730
1333
  return hasFloat ? { internalFormat: gl.RGBA16F, type: gl.HALF_FLOAT } : { internalFormat: gl.RGBA8, type: gl.UNSIGNED_BYTE };
@@ -742,6 +1345,10 @@ function createMediumRuntime(gl, vertexSource) {
742
1345
  const seedProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_SEED_SHADER));
743
1346
  const emitProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_EMIT_SHADER));
744
1347
  const resampleProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_RESAMPLE_SHADER));
1348
+ const shadowProgram = web.createProgram(gl, vertexSource, vireglass.toGLSL(MEDIUM_SHADOW_SHADER));
1349
+ const trackLayerProgram = createTrackLayerProgram(gl);
1350
+ const trackLayer = createTrackLayer();
1351
+ let liveTracks = [];
745
1352
  const vaporForwardLoc = web.locationCache(gl, vaporForwardProgram);
746
1353
  const vaporCorrectLoc = web.locationCache(gl, vaporCorrectProgram);
747
1354
  const vaporReactLoc = web.locationCache(gl, vaporReactProgram);
@@ -753,6 +1360,7 @@ function createMediumRuntime(gl, vertexSource) {
753
1360
  const seedLoc = web.locationCache(gl, seedProgram);
754
1361
  const emitLoc = web.locationCache(gl, emitProgram);
755
1362
  const resampleLoc = web.locationCache(gl, resampleProgram);
1363
+ const shadowLoc = web.locationCache(gl, shadowProgram);
756
1364
  let gridW = 0;
757
1365
  let gridH = 0;
758
1366
  let vapor = null;
@@ -762,6 +1370,7 @@ function createMediumRuntime(gl, vertexSource) {
762
1370
  let vaporCorrected = null;
763
1371
  let condensateForward = null;
764
1372
  let condensateCorrected = null;
1373
+ let shadow = null;
765
1374
  let front = 0;
766
1375
  function makeTarget(w, h) {
767
1376
  const texture = web.createTexture(gl, {
@@ -805,6 +1414,7 @@ function createMediumRuntime(gl, vertexSource) {
805
1414
  destroyTarget(vaporCorrected);
806
1415
  destroyTarget(condensateForward);
807
1416
  destroyTarget(condensateCorrected);
1417
+ destroyTarget(shadow);
808
1418
  vapor = null;
809
1419
  condensate = null;
810
1420
  track = null;
@@ -812,6 +1422,7 @@ function createMediumRuntime(gl, vertexSource) {
812
1422
  vaporCorrected = null;
813
1423
  condensateForward = null;
814
1424
  condensateCorrected = null;
1425
+ shadow = null;
815
1426
  }
816
1427
  function seed(w, h) {
817
1428
  if (!vapor || !condensate || !track) return;
@@ -865,6 +1476,7 @@ function createMediumRuntime(gl, vertexSource) {
865
1476
  vaporCorrected = makeTarget(w, h);
866
1477
  condensateForward = makeTarget(w, h);
867
1478
  condensateCorrected = makeTarget(w, h);
1479
+ shadow = makeTarget(w, h);
868
1480
  front = 0;
869
1481
  const areaRatio = w * h / (oldW * oldH);
870
1482
  waterTargetTotal *= areaRatio;
@@ -883,6 +1495,7 @@ function createMediumRuntime(gl, vertexSource) {
883
1495
  vaporCorrected = makeTarget(w, h);
884
1496
  condensateForward = makeTarget(w, h);
885
1497
  condensateCorrected = makeTarget(w, h);
1498
+ shadow = makeTarget(w, h);
886
1499
  front = 0;
887
1500
  seed(w, h);
888
1501
  waterTargetTotal = readTarget(vapor[front]) + readTarget(condensate[front]);
@@ -893,6 +1506,17 @@ function createMediumRuntime(gl, vertexSource) {
893
1506
  }
894
1507
  resizeGrid(w, h);
895
1508
  }
1509
+ function bindLightUniforms(slot, light) {
1510
+ const position = light?.position ?? [0, 0];
1511
+ const direction = light?.direction ?? [0, 1];
1512
+ const dirLen = Math.hypot(direction[0], direction[1]) || 1;
1513
+ web.setUniform(gl, compositeLoc(`u_light${slot}Pos`), position);
1514
+ web.setUniform(gl, compositeLoc(`u_light${slot}Dir`), [direction[0] / dirLen, direction[1] / dirLen]);
1515
+ web.setUniform(gl, compositeLoc(`u_light${slot}CosCone`), Math.cos(light?.coneAngle ?? 0));
1516
+ web.setUniform(gl, compositeLoc(`u_light${slot}Falloff`), light?.falloff ?? 0);
1517
+ web.setUniform(gl, compositeLoc(`u_light${slot}Color`), light?.color ?? [0, 0, 0]);
1518
+ web.setUniform(gl, compositeLoc(`u_light${slot}Intensity`), light?.intensity ?? 0);
1519
+ }
896
1520
  function bindVelocityUniforms(loc, dt, phase2, p, spatial) {
897
1521
  web.setUniform(gl, loc("u_dyeSize"), [gridW, gridH]);
898
1522
  web.setUniform(gl, loc("u_resolution"), [gridW, gridH]);
@@ -918,7 +1542,7 @@ function createMediumRuntime(gl, vertexSource) {
918
1542
  let timeSinceRenorm = 0;
919
1543
  let framesSinceRenorm = 0;
920
1544
  function step(dt, params = {}) {
921
- if (!vapor || !condensate || !track || !vaporForward || !vaporCorrected || !condensateForward || !condensateCorrected) {
1545
+ if (!vapor || !condensate || !track || !vaporForward || !vaporCorrected || !condensateForward || !condensateCorrected || !shadow) {
922
1546
  return;
923
1547
  }
924
1548
  const p = { ...MEDIUM_DEFAULTS, ...params };
@@ -992,6 +1616,20 @@ function createMediumRuntime(gl, vertexSource) {
992
1616
  bindVelocityUniforms(trackAdvectLoc, clampedDt, phase, p, spatial);
993
1617
  web.setUniform(gl, trackAdvectLoc("u_decay"), p.decay);
994
1618
  web.drawFullscreenTriangle(gl);
1619
+ gl.bindFramebuffer(gl.FRAMEBUFFER, shadow.fbo);
1620
+ gl.useProgram(shadowProgram);
1621
+ web.bindTextureAt(gl, 0, vapor[back].texture, shadowProgram, "u_vapor");
1622
+ web.bindTextureAt(gl, 1, condensate[back].texture, shadowProgram, "u_condensate");
1623
+ web.setUniform(gl, shadowLoc("u_vaporSize"), gridSize);
1624
+ web.setUniform(gl, shadowLoc("u_condensateSize"), gridSize);
1625
+ web.setUniform(gl, shadowLoc("u_resolution"), gridSize);
1626
+ web.setUniform(gl, shadowLoc("u_condensateGain"), p.condensateGain);
1627
+ web.setUniform(gl, shadowLoc("u_extinction"), p.shadowExtinction);
1628
+ for (const slot of [1, 2]) {
1629
+ const position = p.lights[slot]?.position ?? [0, 0];
1630
+ web.setUniform(gl, shadowLoc(`u_light${slot}Grid`), [position[0] * gridW, position[1] * gridH]);
1631
+ }
1632
+ web.drawFullscreenTriangle(gl);
995
1633
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
996
1634
  front = back;
997
1635
  framesSinceRenorm += 1;
@@ -1033,14 +1671,16 @@ function createMediumRuntime(gl, vertexSource) {
1033
1671
  web.setUniform(gl, emitLoc("u_tailDim"), e.tailDim);
1034
1672
  web.setUniform(gl, emitLoc("u_intensity"), e.intensity);
1035
1673
  web.setUniform(gl, emitLoc("u_seed"), e.seed);
1036
- web.setUniform(gl, emitLoc("u_channelMask"), e.channel);
1037
1674
  web.drawFullscreenTriangle(gl);
1038
1675
  }
1039
1676
  gl.disable(gl.BLEND);
1040
1677
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
1041
1678
  }
1679
+ function stepTrackLayer(dt, emissions) {
1680
+ liveTracks = trackLayer.step(dt, emissions);
1681
+ }
1042
1682
  function composite(contentWidth, contentHeight, params = {}) {
1043
- if (!vapor || !condensate || !track) return;
1683
+ if (!vapor || !condensate || !track || !shadow) return;
1044
1684
  const p = { ...MEDIUM_DEFAULTS, ...params };
1045
1685
  gl.viewport(0, 0, contentWidth, contentHeight);
1046
1686
  gl.disable(gl.BLEND);
@@ -1048,9 +1688,11 @@ function createMediumRuntime(gl, vertexSource) {
1048
1688
  web.bindTextureAt(gl, 0, vapor[front].texture, compositeProgram, "u_vapor");
1049
1689
  web.bindTextureAt(gl, 1, condensate[front].texture, compositeProgram, "u_condensate");
1050
1690
  web.bindTextureAt(gl, 2, track[front].texture, compositeProgram, "u_track");
1691
+ web.bindTextureAt(gl, 3, shadow.texture, compositeProgram, "u_shadow");
1051
1692
  web.setUniform(gl, compositeLoc("u_vaporSize"), [gridW, gridH]);
1052
1693
  web.setUniform(gl, compositeLoc("u_condensateSize"), [gridW, gridH]);
1053
1694
  web.setUniform(gl, compositeLoc("u_trackSize"), [gridW, gridH]);
1695
+ web.setUniform(gl, compositeLoc("u_shadowSize"), [gridW, gridH]);
1054
1696
  web.setUniform(gl, compositeLoc("u_resolution"), [contentWidth, contentHeight]);
1055
1697
  web.setUniform(gl, compositeLoc("u_dyeScale"), [gridW / contentWidth, gridH / contentHeight]);
1056
1698
  web.setUniform(gl, compositeLoc("u_lab0"), srgbToOklab(p.channelColors[0]));
@@ -1067,7 +1709,12 @@ function createMediumRuntime(gl, vertexSource) {
1067
1709
  web.setUniform(gl, compositeLoc("u_farWeight"), p.depthFarWeight);
1068
1710
  web.setUniform(gl, compositeLoc("u_gravityBoost"), p.gravityBottomBoost);
1069
1711
  web.setUniform(gl, compositeLoc("u_gravityBoostStart"), p.gravityBottomBoostStart);
1712
+ web.setUniform(gl, compositeLoc("u_channelScatter"), p.channelScatter);
1713
+ for (let slot = 0; slot < MEDIUM_MAX_LIGHTS; slot += 1) bindLightUniforms(slot, p.lights[slot]);
1714
+ web.setUniform(gl, compositeLoc("u_trackLightFloor"), p.trackLightFloor);
1070
1715
  web.drawFullscreenTriangle(gl);
1716
+ const { data, count } = buildTrackLayerInstances(liveTracks, contentWidth, contentHeight, p.trackLayerAmount);
1717
+ trackLayerProgram.draw(contentWidth, contentHeight, data, count, p.trackLightFloor, p.lights, shadow.texture);
1071
1718
  }
1072
1719
  function readTarget(target) {
1073
1720
  gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);
@@ -1134,6 +1781,23 @@ function createMediumRuntime(gl, vertexSource) {
1134
1781
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
1135
1782
  return { cols: gridW, rows: gridH, data };
1136
1783
  }
1784
+ function readTrackGrid() {
1785
+ if (!track) return null;
1786
+ gl.bindFramebuffer(gl.FRAMEBUFFER, track[front].fbo);
1787
+ const cells = gridW * gridH;
1788
+ const data = new Array(cells);
1789
+ if (format.type === gl.HALF_FLOAT) {
1790
+ const buf = new Float32Array(cells * 4);
1791
+ gl.readPixels(0, 0, gridW, gridH, gl.RGBA, gl.FLOAT, buf);
1792
+ for (let i = 0; i < cells; i += 1) data[i] = buf[i * 4];
1793
+ } else {
1794
+ const buf = new Uint8Array(cells * 4);
1795
+ gl.readPixels(0, 0, gridW, gridH, gl.RGBA, gl.UNSIGNED_BYTE, buf);
1796
+ for (let i = 0; i < cells; i += 1) data[i] = buf[i * 4] / 255;
1797
+ }
1798
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
1799
+ return { cols: gridW, rows: gridH, data };
1800
+ }
1137
1801
  function destroy() {
1138
1802
  destroyTargets();
1139
1803
  gl.deleteProgram(vaporForwardProgram);
@@ -1146,9 +1810,22 @@ function createMediumRuntime(gl, vertexSource) {
1146
1810
  gl.deleteProgram(compositeProgram);
1147
1811
  gl.deleteProgram(seedProgram);
1148
1812
  gl.deleteProgram(emitProgram);
1813
+ gl.deleteProgram(shadowProgram);
1149
1814
  gl.deleteProgram(resampleProgram);
1815
+ trackLayerProgram.destroy();
1150
1816
  }
1151
- return { ensureGrid, step, emit, composite, readTotals, readVaporGrid, readCondensateGrid, destroy };
1817
+ return {
1818
+ ensureGrid,
1819
+ step,
1820
+ emit,
1821
+ stepTrackLayer,
1822
+ composite,
1823
+ readTotals,
1824
+ readVaporGrid,
1825
+ readCondensateGrid,
1826
+ readTrackGrid,
1827
+ destroy
1828
+ };
1152
1829
  }
1153
1830
  function createMediumBackdrop() {
1154
1831
  let runtime = null;
@@ -1161,12 +1838,14 @@ function createMediumBackdrop() {
1161
1838
  runtime.ensureGrid(frame.gridWidth, frame.gridHeight);
1162
1839
  runtime.step(frame.dt, frame.params);
1163
1840
  runtime.emit(frame.emissions ?? []);
1841
+ runtime.stepTrackLayer(frame.dt, frame.emissions ?? []);
1164
1842
  gl.bindFramebuffer(gl.FRAMEBUFFER, target.framebuffer);
1165
1843
  runtime.composite(target.width, target.height, frame.params);
1166
1844
  },
1167
1845
  readTotals: () => runtime?.readTotals() ?? null,
1168
1846
  readVaporGrid: () => runtime?.readVaporGrid() ?? null,
1169
1847
  readCondensateGrid: () => runtime?.readCondensateGrid() ?? null,
1848
+ readTrackGrid: () => runtime?.readTrackGrid() ?? null,
1170
1849
  destroy: () => {
1171
1850
  runtime?.destroy();
1172
1851
  runtime = null;
@@ -1175,7 +1854,12 @@ function createMediumBackdrop() {
1175
1854
  };
1176
1855
  }
1177
1856
 
1857
+ exports.buildTrackLayerInstances = buildTrackLayerInstances;
1178
1858
  exports.createMediumBackdrop = createMediumBackdrop;
1179
1859
  exports.createMediumRuntime = createMediumRuntime;
1860
+ exports.createTrackLayer = createTrackLayer;
1861
+ exports.createTrackLayerProgram = createTrackLayerProgram;
1862
+ exports.trackLayerFade = trackLayerFade;
1863
+ exports.trackLayerSigmaMul = trackLayerSigmaMul;
1180
1864
  //# sourceMappingURL=index.cjs.map
1181
1865
  //# sourceMappingURL=index.cjs.map