vireuikit 0.2.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.
package/dist/web/index.js CHANGED
@@ -1,7 +1,373 @@
1
- import { MEDIUM_VAPOR_FORWARD_SHADER, MEDIUM_VAPOR_CORRECT_SHADER, MEDIUM_VAPOR_REACT_SHADER, MEDIUM_CONDENSATE_FORWARD_SHADER, MEDIUM_CONDENSATE_CORRECT_SHADER, MEDIUM_CONDENSATE_REACT_SHADER, MEDIUM_TRACK_ADVECT_SHADER, MEDIUM_COMPOSITE_SHADER, MEDIUM_SEED_SHADER, MEDIUM_EMIT_SHADER, MEDIUM_RESAMPLE_SHADER, MEDIUM_DEFAULTS, srgbToOklab, depthFarOffsetPx, MEDIUM_TIME_PERIOD } from '../chunk-BV5P6MF7.js';
1
+ import { MEDIUM_TRACK_LAYER_FADE_GAMMA, MEDIUM_TRACK_LAYER_LIFE_SECONDS, MEDIUM_TRACK_LAYER_LOOK, MEDIUM_VAPOR_FORWARD_SHADER, MEDIUM_VAPOR_CORRECT_SHADER, MEDIUM_VAPOR_REACT_SHADER, MEDIUM_CONDENSATE_FORWARD_SHADER, MEDIUM_CONDENSATE_CORRECT_SHADER, MEDIUM_CONDENSATE_REACT_SHADER, MEDIUM_TRACK_ADVECT_SHADER, MEDIUM_COMPOSITE_SHADER, MEDIUM_SEED_SHADER, MEDIUM_EMIT_SHADER, MEDIUM_RESAMPLE_SHADER, MEDIUM_SHADOW_SHADER, MEDIUM_DEFAULTS, computeMediumSpatialPeriods, srgbToOklab, depthFarOffsetPx, MEDIUM_MAX_LIGHTS, MEDIUM_TRACK_LAYER_CURL_PUSH, MEDIUM_TRACK_LAYER_GRAVITY_SAG, MEDIUM_TRACK_LAYER_BURST_ACCENT, MEDIUM_TRACK_LAYER_BURST_SPREAD, MEDIUM_TRACK_LAYER_MAX_TRACKS, MEDIUM_TIME_PERIOD, vgPotentialJS } from '../chunk-VY22O2II.js';
2
2
  import { toGLSL } from 'vireglass';
3
- import { createProgram, locationCache, FULLSCREEN_TRIANGLE_VERTEX_SOURCE, bindTextureAt, drawFullscreenTriangle, setUniform, createTexture, createFramebuffer } from 'vireglass/web';
3
+ import { locationCache, createProgram, FULLSCREEN_TRIANGLE_VERTEX_SOURCE, bindTextureAt, drawFullscreenTriangle, setUniform, createTexture, createFramebuffer } from 'vireglass/web';
4
4
 
5
+ // src/web/track-layer.ts
6
+ function trackLayerFade(age, life) {
7
+ if (life <= 0) return 0;
8
+ const t = Math.min(Math.max(age / life, 0), 1);
9
+ return (1 - t) ** MEDIUM_TRACK_LAYER_FADE_GAMMA;
10
+ }
11
+ function trackLayerSigmaMul(age, broaden) {
12
+ return Math.sqrt(1 + broaden * Math.max(age, 0));
13
+ }
14
+ function hash01(n) {
15
+ const s = Math.sin(n * 91.3458) * 47453.5453;
16
+ return s - Math.floor(s);
17
+ }
18
+ function curlVelocityJS(px, py, phase) {
19
+ const e = 0.6;
20
+ const px1 = vgPotentialJS(px + e, py, phase);
21
+ const px0 = vgPotentialJS(px - e, py, phase);
22
+ const py1 = vgPotentialJS(px, py + e, phase);
23
+ const py0 = vgPotentialJS(px, py - e, phase);
24
+ return [(py1 - py0) / (2 * e), -(px1 - px0) / (2 * e)];
25
+ }
26
+ var CURL_SAMPLE_SCALE = 40;
27
+ var PHASE_WRAP = 1e4;
28
+ function createTrackLayer() {
29
+ let tracks = [];
30
+ let phase = 0;
31
+ function step(dt, emissions) {
32
+ phase = (phase + dt) % PHASE_WRAP;
33
+ const next = [];
34
+ for (const track of tracks) {
35
+ const life = MEDIUM_TRACK_LAYER_LIFE_SECONDS[track.emission.kind];
36
+ const age = track.age + dt;
37
+ if (age >= life) continue;
38
+ if (age <= 0) {
39
+ next.push({ ...track, age });
40
+ continue;
41
+ }
42
+ const midX = track.emission.source[0] + Math.cos(track.emission.angle) * track.emission.lengthFrac * 0.5;
43
+ const midY = track.emission.source[1] + Math.sin(track.emission.angle) * track.emission.lengthFrac * 0.5;
44
+ const [curlX, curlY] = curlVelocityJS(midX * CURL_SAMPLE_SCALE, midY * CURL_SAMPLE_SCALE, phase);
45
+ const activeDt = Math.min(dt, age);
46
+ next.push({
47
+ emission: track.emission,
48
+ age,
49
+ driftX: track.driftX + curlX * MEDIUM_TRACK_LAYER_CURL_PUSH * activeDt,
50
+ driftY: track.driftY + (curlY * MEDIUM_TRACK_LAYER_CURL_PUSH - MEDIUM_TRACK_LAYER_GRAVITY_SAG) * activeDt
51
+ });
52
+ }
53
+ let burstIndex = 0;
54
+ for (const emission of emissions) {
55
+ let age = 0;
56
+ if (emission.kind === "alpha") {
57
+ if (burstIndex >= MEDIUM_TRACK_LAYER_BURST_ACCENT) {
58
+ age = -hash01(emission.seed * 3.17 + burstIndex) * MEDIUM_TRACK_LAYER_BURST_SPREAD;
59
+ }
60
+ burstIndex += 1;
61
+ }
62
+ next.push({ emission, age, driftX: 0, driftY: 0 });
63
+ }
64
+ tracks = next.length > MEDIUM_TRACK_LAYER_MAX_TRACKS ? next.slice(next.length - MEDIUM_TRACK_LAYER_MAX_TRACKS) : next;
65
+ return tracks;
66
+ }
67
+ return { step };
68
+ }
69
+ var TRACK_LAYER_VERTEX_SOURCE = `#version 300 es
70
+ layout(location = 0) in vec2 a_corner;
71
+ layout(location = 1) in vec4 a_geom;
72
+ layout(location = 2) in vec4 a_span;
73
+ layout(location = 3) in vec4 a_path;
74
+ layout(location = 4) in vec4 a_grain;
75
+
76
+ uniform vec2 u_resolution;
77
+ uniform float u_trackLightFloor;
78
+ uniform sampler2D u_shadow;
79
+ uniform vec2 u_light0Pos;
80
+ uniform vec2 u_light0Dir;
81
+ uniform float u_light0CosCone;
82
+ uniform float u_light0Falloff;
83
+ uniform vec3 u_light0Color;
84
+ uniform float u_light0Intensity;
85
+ uniform vec2 u_light1Pos;
86
+ uniform vec2 u_light1Dir;
87
+ uniform float u_light1CosCone;
88
+ uniform float u_light1Falloff;
89
+ uniform vec3 u_light1Color;
90
+ uniform float u_light1Intensity;
91
+ uniform vec2 u_light2Pos;
92
+ uniform vec2 u_light2Dir;
93
+ uniform float u_light2CosCone;
94
+ uniform float u_light2Falloff;
95
+ uniform vec3 u_light2Color;
96
+ uniform float u_light2Intensity;
97
+
98
+ out vec2 v_local;
99
+ flat out vec4 v_span;
100
+ flat out vec4 v_path;
101
+ flat out vec4 v_grain;
102
+ flat out float v_reach;
103
+ out vec3 v_illum;
104
+
105
+ float vgHash21(vec2 p) {
106
+ float h = dot(p, vec2(127.1, 311.7));
107
+ return fract(sin(h) * 43758.5453123);
108
+ }
109
+
110
+ float vgValueNoise(vec2 p) {
111
+ vec2 i = floor(p);
112
+ vec2 f = fract(p);
113
+ float a = vgHash21(i);
114
+ float b = vgHash21(i + vec2(1.0, 0.0));
115
+ float c = vgHash21(i + vec2(0.0, 1.0));
116
+ float d = vgHash21(i + vec2(1.0, 1.0));
117
+ vec2 u = f * f * (3.0 - 2.0 * f);
118
+ return mix(mix(a, b, u.x), mix(c, d, u.x), u.y) * 2.0 - 1.0;
119
+ }
120
+
121
+ // Keep in step with vgLightAmount in composite-shader.ts.
122
+ float vgLightAmount(vec2 xy, vec2 lightPosFrac, vec2 lightDir, float cosCone, float falloff, vec2 resolution, float seed) {
123
+ vec2 rel = xy - lightPosFrac * resolution;
124
+ if (cosCone <= -0.999) {
125
+ float depth = max(dot(rel, lightDir), 0.0);
126
+ return exp(-(depth / max(resolution.y, 1.0)) * falloff);
127
+ }
128
+ float dist = length(rel);
129
+ vec2 dirN = dist > 1e-4 ? rel / dist : lightDir;
130
+ float cosAngle = dot(dirN, lightDir);
131
+ float lobe = log(0.5) / log(clamp(cosCone, 0.05, 0.999));
132
+ float edge = pow(max(cosAngle, 0.0), lobe);
133
+ float atten = exp(-(dist / max(resolution.y, 1.0)) * falloff);
134
+ if (edge * atten < 0.001) return 0.0;
135
+ float angle = atan(dirN.x * lightDir.y - dirN.y * lightDir.x, cosAngle);
136
+ float shafts = 0.6 + 0.32 * vgValueNoise(vec2(angle * 14.0, seed)) + 0.18 * vgValueNoise(vec2(angle * 33.0, seed + 5.3));
137
+ return edge * atten * clamp(shafts, 0.0, 1.0);
138
+ }
139
+
140
+ void main() {
141
+ vec2 dir = a_geom.zw;
142
+ vec2 side = vec2(-dir.y, dir.x);
143
+ // Beads widen sigma by up to 20%; 3.5 sigma leaves under 1% of the core outside the quad.
144
+ float reach = max(a_span.z, a_span.w) * 1.2 * 3.5 + 1.5;
145
+ float margin = reach + a_path.x * 1.7;
146
+ float u = mix(a_span.y - reach, a_span.x + reach, a_corner.y);
147
+ float s = a_corner.x * margin;
148
+ vec2 pos = a_geom.xy + dir * u + side * s;
149
+ v_local = vec2(u, s);
150
+ v_span = a_span;
151
+ v_path = a_path;
152
+ v_grain = a_grain;
153
+ v_reach = reach;
154
+ // Lit per corner: light varies over hundreds of px, a track is a few px wide. The backdrop
155
+ // target stores the scene top row at y = 0; lights use y up.
156
+ vec2 xy = vec2(pos.x, u_resolution.y - pos.y);
157
+ // The same gas shadow the composite applies to the spotlights (shadow-shader.ts, y up).
158
+ vec4 shadow = texture(u_shadow, xy / u_resolution);
159
+ float light0 = vgLightAmount(xy, u_light0Pos, u_light0Dir, u_light0CosCone, u_light0Falloff, u_resolution, 1.7) * u_light0Intensity;
160
+ float light1 = vgLightAmount(xy, u_light1Pos, u_light1Dir, u_light1CosCone, u_light1Falloff, u_resolution, 4.1) * u_light1Intensity * shadow.r;
161
+ float light2 = vgLightAmount(xy, u_light2Pos, u_light2Dir, u_light2CosCone, u_light2Falloff, u_resolution, 8.3) * u_light2Intensity * shadow.g;
162
+ v_illum = u_light0Color * light0 + u_light1Color * light1 + u_light2Color * light2 + vec3(u_trackLightFloor);
163
+ gl_Position = vec4((pos / u_resolution) * 2.0 - 1.0, 0.0, 1.0);
164
+ }
165
+ `;
166
+ var TRACK_LAYER_FRAGMENT_SOURCE = `#version 300 es
167
+ precision highp float;
168
+
169
+ in vec2 v_local;
170
+ flat in vec4 v_span;
171
+ flat in vec4 v_path;
172
+ flat in vec4 v_grain;
173
+ flat in float v_reach;
174
+ in vec3 v_illum;
175
+
176
+ out vec4 fragColor;
177
+
178
+ float vgHash(float a, float b) {
179
+ uint x = floatBitsToUint(a) * 0x9E3779B1u ^ (floatBitsToUint(b) + 0x7F4A7C15u);
180
+ x ^= x >> 16;
181
+ x *= 0x7FEB352Du;
182
+ x ^= x >> 15;
183
+ x *= 0x846CA68Bu;
184
+ x ^= x >> 16;
185
+ return float(x) * (1.0 / 4294967295.0);
186
+ }
187
+
188
+ float vgNoise1(float x, float salt) {
189
+ float i = floor(x);
190
+ float f = fract(x);
191
+ float u = f * f * (3.0 - 2.0 * f);
192
+ return mix(vgHash(i, salt), vgHash(i + 1.0, salt), u);
193
+ }
194
+
195
+ void main() {
196
+ float len = v_span.x;
197
+ float start = v_span.y;
198
+ float sigma0 = v_span.z;
199
+ float sigma1 = v_span.w;
200
+ float wiggleAmp = v_path.x;
201
+ float wiggleFreq = v_path.y;
202
+ float amp = v_path.z;
203
+ float seed = v_path.w;
204
+ float beadPx = v_grain.x;
205
+ float dropPx = v_grain.y;
206
+ float dropChance = v_grain.z;
207
+ float blur = v_grain.w;
208
+
209
+ float u = v_local.x;
210
+ float s = v_local.y;
211
+ float t = clamp((u - start) / max(len - start, 1.0), 0.0, 1.0);
212
+
213
+ float wiggle = 0.0;
214
+ if (wiggleAmp > 0.0) {
215
+ float x = u * wiggleFreq;
216
+ wiggle = wiggleAmp * smoothstep(0.0, 0.2, t) *
217
+ ((vgNoise1(x, seed) - 0.5) * 2.0 + (vgNoise1(x * 3.1, seed + 1.0) - 0.5) * 0.7);
218
+ }
219
+ float across = s - wiggle;
220
+ if (abs(across) > v_reach) discard;
221
+
222
+ float bead = vgNoise1(u / beadPx, seed + 2.0);
223
+ // Blur (age, defocus) washes the beads out rather than stretching them into ribs.
224
+ float sigma = mix(sigma0, sigma1, t * t) * (1.0 + (bead - 0.5) * 0.4 / blur);
225
+ float beyond = u < start ? start - u : max(u - len, 0.0);
226
+ float core = exp(-0.5 * (across * across + beyond * beyond) / (sigma * sigma));
227
+ float along = 1.0 - 0.35 / blur * vgNoise1(u / (beadPx * 2.3), seed + 3.0);
228
+
229
+ float drops = 0.0;
230
+ float cell0 = floor(u / dropPx);
231
+ for (int k = -1; k <= 1; k++) {
232
+ float c = cell0 + float(k);
233
+ if (vgHash(c, seed + 4.0) > dropChance) continue;
234
+ float cu = (c + vgHash(c, seed + 5.0)) * dropPx;
235
+ if (cu < start || cu > len) continue;
236
+ float cs = (vgHash(c, seed + 6.0) + vgHash(c, seed + 7.0) - 1.0) * 1.7 * sigma + wiggle;
237
+ float r = max(0.7, sigma * mix(0.35, 0.8, vgHash(c, seed + 8.0)));
238
+ float dx = u - cu;
239
+ float dy = s - cs;
240
+ drops += mix(0.35, 1.0, vgHash(c, seed + 9.0)) * exp(-0.5 * (dx * dx + dy * dy) / (r * r));
241
+ }
242
+
243
+ float lum = amp * (core * along + drops * 0.7 / (blur * blur));
244
+ if (lum < 0.002) discard;
245
+
246
+ vec3 glow = lum * v_illum;
247
+ fragColor = vec4(vec3(1.0) - exp(-glow), 1.0);
248
+ }
249
+ `;
250
+ var FLOATS_PER_INSTANCE = 16;
251
+ var DEFOCUS_FROM_DEPTH = 0.7;
252
+ var DEFOCUS_MAX = 2.4;
253
+ function buildTrackLayerInstances(tracks, contentWidth, contentHeight, amount) {
254
+ const minDim = Math.min(contentWidth, contentHeight);
255
+ const data = new Float32Array(tracks.length * FLOATS_PER_INSTANCE);
256
+ let count = 0;
257
+ if (amount <= 0) return { data, count };
258
+ for (const track of tracks) {
259
+ if (track.age < 0) continue;
260
+ const e = track.emission;
261
+ const fade = trackLayerFade(track.age, MEDIUM_TRACK_LAYER_LIFE_SECONDS[e.kind]);
262
+ if (fade <= 1e-4) continue;
263
+ const look = MEDIUM_TRACK_LAYER_LOOK[e.kind];
264
+ const sigmaMul = trackLayerSigmaMul(track.age, look.broaden);
265
+ const near = Math.max(0, (e.depth - DEFOCUS_FROM_DEPTH) / (1 - DEFOCUS_FROM_DEPTH));
266
+ const defocus = 1 + (DEFOCUS_MAX - 1) * near * near;
267
+ const sigma0 = Math.max(look.sigmaFrac * minDim, 0.6) * sigmaMul * defocus;
268
+ const o = count * FLOATS_PER_INSTANCE;
269
+ data[o + 0] = e.source[0] * contentWidth + track.driftX * minDim;
270
+ data[o + 1] = e.source[1] * contentHeight - track.driftY * minDim;
271
+ data[o + 2] = Math.cos(e.angle);
272
+ data[o + 3] = Math.sin(e.angle);
273
+ const len = e.lengthFrac * minDim;
274
+ data[o + 4] = len;
275
+ data[o + 5] = Math.min(look.startFrac * minDim, len * 0.5);
276
+ data[o + 6] = sigma0;
277
+ data[o + 7] = sigma0 * look.endSigmaMul;
278
+ data[o + 8] = e.raggedFrac > 0 ? look.wiggleFrac * minDim : 0;
279
+ data[o + 9] = e.raggedFreq / Math.max(len, 1);
280
+ data[o + 10] = e.intensity * look.gain * fade * amount / (sigmaMul ** 0.7 * defocus);
281
+ data[o + 11] = e.seed;
282
+ data[o + 12] = look.beadFrac * minDim;
283
+ data[o + 13] = look.dropletFrac * minDim;
284
+ data[o + 14] = look.dropletChance;
285
+ data[o + 15] = sigmaMul * defocus;
286
+ count += 1;
287
+ }
288
+ return { data, count };
289
+ }
290
+ function compile(gl, type, source, label) {
291
+ const shader = gl.createShader(type);
292
+ if (!shader) throw new Error(`vireuikit: track layer ${label} shader creation failed`);
293
+ gl.shaderSource(shader, source);
294
+ gl.compileShader(shader);
295
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
296
+ throw new Error(`vireuikit: track layer ${label} shader failed:
297
+ ${gl.getShaderInfoLog(shader) ?? ""}`);
298
+ }
299
+ return shader;
300
+ }
301
+ function createTrackLayerProgram(gl) {
302
+ const vertex = compile(gl, gl.VERTEX_SHADER, TRACK_LAYER_VERTEX_SOURCE, "vertex");
303
+ const fragment = compile(gl, gl.FRAGMENT_SHADER, TRACK_LAYER_FRAGMENT_SOURCE, "fragment");
304
+ const program = gl.createProgram();
305
+ if (!program) throw new Error("vireuikit: track layer program creation failed");
306
+ gl.attachShader(program, vertex);
307
+ gl.attachShader(program, fragment);
308
+ gl.linkProgram(program);
309
+ gl.deleteShader(vertex);
310
+ gl.deleteShader(fragment);
311
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
312
+ throw new Error(`vireuikit: track layer program failed to link:
313
+ ${gl.getProgramInfoLog(program) ?? ""}`);
314
+ }
315
+ const loc = locationCache(gl, program);
316
+ const quadBuffer = gl.createBuffer();
317
+ gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
318
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, 0, 1, 0, -1, 1, 1, 1]), gl.STATIC_DRAW);
319
+ const instanceBuffer = gl.createBuffer();
320
+ const vao = gl.createVertexArray();
321
+ gl.bindVertexArray(vao);
322
+ gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
323
+ gl.enableVertexAttribArray(0);
324
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
325
+ const stride = FLOATS_PER_INSTANCE * 4;
326
+ gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);
327
+ for (let location = 1; location <= 4; location += 1) {
328
+ gl.enableVertexAttribArray(location);
329
+ gl.vertexAttribPointer(location, 4, gl.FLOAT, false, stride, (location - 1) * 16);
330
+ gl.vertexAttribDivisor(location, 1);
331
+ }
332
+ gl.bindVertexArray(null);
333
+ function bindLight(slot, light) {
334
+ const position = light?.position ?? [0, 0];
335
+ const direction = light?.direction ?? [0, 1];
336
+ const dirLen = Math.hypot(direction[0], direction[1]) || 1;
337
+ setUniform(gl, loc(`u_light${slot}Pos`), position);
338
+ setUniform(gl, loc(`u_light${slot}Dir`), [direction[0] / dirLen, direction[1] / dirLen]);
339
+ setUniform(gl, loc(`u_light${slot}CosCone`), Math.cos(light?.coneAngle ?? 0));
340
+ setUniform(gl, loc(`u_light${slot}Falloff`), light?.falloff ?? 0);
341
+ setUniform(gl, loc(`u_light${slot}Color`), light?.color ?? [0, 0, 0]);
342
+ setUniform(gl, loc(`u_light${slot}Intensity`), light?.intensity ?? 0);
343
+ }
344
+ function draw(contentWidth, contentHeight, data, count, trackLightFloor, lights, shadow) {
345
+ if (count <= 0) return;
346
+ gl.useProgram(program);
347
+ gl.bindVertexArray(vao);
348
+ gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);
349
+ gl.bufferData(gl.ARRAY_BUFFER, data.subarray(0, count * FLOATS_PER_INSTANCE), gl.DYNAMIC_DRAW);
350
+ setUniform(gl, loc("u_resolution"), [contentWidth, contentHeight]);
351
+ setUniform(gl, loc("u_trackLightFloor"), trackLightFloor);
352
+ for (let slot = 0; slot < 3; slot += 1) bindLight(slot, lights[slot]);
353
+ bindTextureAt(gl, 0, shadow, program, "u_shadow");
354
+ gl.enable(gl.BLEND);
355
+ gl.blendFunc(gl.ONE, gl.ONE);
356
+ gl.disable(gl.DEPTH_TEST);
357
+ gl.viewport(0, 0, contentWidth, contentHeight);
358
+ gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, count);
359
+ gl.bindVertexArray(null);
360
+ }
361
+ function destroy() {
362
+ gl.deleteBuffer(quadBuffer);
363
+ gl.deleteBuffer(instanceBuffer);
364
+ gl.deleteVertexArray(vao);
365
+ gl.deleteProgram(program);
366
+ }
367
+ return { draw, destroy };
368
+ }
369
+
370
+ // src/web/medium.ts
5
371
  function pickDyeFormat(gl) {
6
372
  const hasFloat = gl.getExtension("EXT_color_buffer_float") !== null;
7
373
  return hasFloat ? { internalFormat: gl.RGBA16F, type: gl.HALF_FLOAT } : { internalFormat: gl.RGBA8, type: gl.UNSIGNED_BYTE };
@@ -19,6 +385,10 @@ function createMediumRuntime(gl, vertexSource) {
19
385
  const seedProgram = createProgram(gl, vertexSource, toGLSL(MEDIUM_SEED_SHADER));
20
386
  const emitProgram = createProgram(gl, vertexSource, toGLSL(MEDIUM_EMIT_SHADER));
21
387
  const resampleProgram = createProgram(gl, vertexSource, toGLSL(MEDIUM_RESAMPLE_SHADER));
388
+ const shadowProgram = createProgram(gl, vertexSource, toGLSL(MEDIUM_SHADOW_SHADER));
389
+ const trackLayerProgram = createTrackLayerProgram(gl);
390
+ const trackLayer = createTrackLayer();
391
+ let liveTracks = [];
22
392
  const vaporForwardLoc = locationCache(gl, vaporForwardProgram);
23
393
  const vaporCorrectLoc = locationCache(gl, vaporCorrectProgram);
24
394
  const vaporReactLoc = locationCache(gl, vaporReactProgram);
@@ -30,6 +400,7 @@ function createMediumRuntime(gl, vertexSource) {
30
400
  const seedLoc = locationCache(gl, seedProgram);
31
401
  const emitLoc = locationCache(gl, emitProgram);
32
402
  const resampleLoc = locationCache(gl, resampleProgram);
403
+ const shadowLoc = locationCache(gl, shadowProgram);
33
404
  let gridW = 0;
34
405
  let gridH = 0;
35
406
  let vapor = null;
@@ -39,6 +410,7 @@ function createMediumRuntime(gl, vertexSource) {
39
410
  let vaporCorrected = null;
40
411
  let condensateForward = null;
41
412
  let condensateCorrected = null;
413
+ let shadow = null;
42
414
  let front = 0;
43
415
  function makeTarget(w, h) {
44
416
  const texture = createTexture(gl, {
@@ -82,6 +454,7 @@ function createMediumRuntime(gl, vertexSource) {
82
454
  destroyTarget(vaporCorrected);
83
455
  destroyTarget(condensateForward);
84
456
  destroyTarget(condensateCorrected);
457
+ destroyTarget(shadow);
85
458
  vapor = null;
86
459
  condensate = null;
87
460
  track = null;
@@ -89,6 +462,7 @@ function createMediumRuntime(gl, vertexSource) {
89
462
  vaporCorrected = null;
90
463
  condensateForward = null;
91
464
  condensateCorrected = null;
465
+ shadow = null;
92
466
  }
93
467
  function seed(w, h) {
94
468
  if (!vapor || !condensate || !track) return;
@@ -142,6 +516,7 @@ function createMediumRuntime(gl, vertexSource) {
142
516
  vaporCorrected = makeTarget(w, h);
143
517
  condensateForward = makeTarget(w, h);
144
518
  condensateCorrected = makeTarget(w, h);
519
+ shadow = makeTarget(w, h);
145
520
  front = 0;
146
521
  const areaRatio = w * h / (oldW * oldH);
147
522
  waterTargetTotal *= areaRatio;
@@ -160,6 +535,7 @@ function createMediumRuntime(gl, vertexSource) {
160
535
  vaporCorrected = makeTarget(w, h);
161
536
  condensateForward = makeTarget(w, h);
162
537
  condensateCorrected = makeTarget(w, h);
538
+ shadow = makeTarget(w, h);
163
539
  front = 0;
164
540
  seed(w, h);
165
541
  waterTargetTotal = readTarget(vapor[front]) + readTarget(condensate[front]);
@@ -170,7 +546,18 @@ function createMediumRuntime(gl, vertexSource) {
170
546
  }
171
547
  resizeGrid(w, h);
172
548
  }
173
- function bindVelocityUniforms(loc, dt, phase2, p) {
549
+ function bindLightUniforms(slot, light) {
550
+ const position = light?.position ?? [0, 0];
551
+ const direction = light?.direction ?? [0, 1];
552
+ const dirLen = Math.hypot(direction[0], direction[1]) || 1;
553
+ setUniform(gl, compositeLoc(`u_light${slot}Pos`), position);
554
+ setUniform(gl, compositeLoc(`u_light${slot}Dir`), [direction[0] / dirLen, direction[1] / dirLen]);
555
+ setUniform(gl, compositeLoc(`u_light${slot}CosCone`), Math.cos(light?.coneAngle ?? 0));
556
+ setUniform(gl, compositeLoc(`u_light${slot}Falloff`), light?.falloff ?? 0);
557
+ setUniform(gl, compositeLoc(`u_light${slot}Color`), light?.color ?? [0, 0, 0]);
558
+ setUniform(gl, compositeLoc(`u_light${slot}Intensity`), light?.intensity ?? 0);
559
+ }
560
+ function bindVelocityUniforms(loc, dt, phase2, p, spatial) {
174
561
  setUniform(gl, loc("u_dyeSize"), [gridW, gridH]);
175
562
  setUniform(gl, loc("u_resolution"), [gridW, gridH]);
176
563
  setUniform(gl, loc("u_dt"), dt);
@@ -179,6 +566,10 @@ function createMediumRuntime(gl, vertexSource) {
179
566
  setUniform(gl, loc("u_advectSpeed"), p.advectSpeed);
180
567
  setUniform(gl, loc("u_turbulence"), p.turbulence);
181
568
  setUniform(gl, loc("u_vaporDrift"), p.gravityVaporDrift);
569
+ setUniform(gl, loc("u_spatial0"), [spatial[0].freqX, spatial[0].freqY, spatial[0].periodX, spatial[0].periodY]);
570
+ setUniform(gl, loc("u_spatial1"), [spatial[1].freqX, spatial[1].freqY, spatial[1].periodX, spatial[1].periodY]);
571
+ setUniform(gl, loc("u_spatial2"), [spatial[2].freqX, spatial[2].freqY, spatial[2].periodX, spatial[2].periodY]);
572
+ setUniform(gl, loc("u_spatial3"), [spatial[3].freqX, spatial[3].freqY, spatial[3].periodX, spatial[3].periodY]);
182
573
  }
183
574
  let phase = 0;
184
575
  function wrapPhase(value) {
@@ -191,10 +582,11 @@ function createMediumRuntime(gl, vertexSource) {
191
582
  let timeSinceRenorm = 0;
192
583
  let framesSinceRenorm = 0;
193
584
  function step(dt, params = {}) {
194
- if (!vapor || !condensate || !track || !vaporForward || !vaporCorrected || !condensateForward || !condensateCorrected) {
585
+ if (!vapor || !condensate || !track || !vaporForward || !vaporCorrected || !condensateForward || !condensateCorrected || !shadow) {
195
586
  return;
196
587
  }
197
588
  const p = { ...MEDIUM_DEFAULTS, ...params };
589
+ const spatial = computeMediumSpatialPeriods(gridW, gridH, p.curlFreq);
198
590
  const clampedDt = Math.min(Math.max(dt, 0), 1 / 15);
199
591
  phase = wrapPhase(phase + clampedDt * p.curlSpeed);
200
592
  const back = front === 0 ? 1 : 0;
@@ -204,28 +596,30 @@ function createMediumRuntime(gl, vertexSource) {
204
596
  gl.bindFramebuffer(gl.FRAMEBUFFER, vaporForward.fbo);
205
597
  gl.useProgram(vaporForwardProgram);
206
598
  bindTextureAt(gl, 0, vapor[front].texture, vaporForwardProgram, "u_dye");
207
- bindVelocityUniforms(vaporForwardLoc, clampedDt, phase, p);
599
+ bindVelocityUniforms(vaporForwardLoc, clampedDt, phase, p, spatial);
208
600
  drawFullscreenTriangle(gl);
209
601
  gl.bindFramebuffer(gl.FRAMEBUFFER, condensateForward.fbo);
210
602
  gl.useProgram(condensateForwardProgram);
211
603
  bindTextureAt(gl, 0, condensate[front].texture, condensateForwardProgram, "u_dye");
212
- bindVelocityUniforms(condensateForwardLoc, clampedDt, phase, p);
604
+ bindVelocityUniforms(condensateForwardLoc, clampedDt, phase, p, spatial);
213
605
  setUniform(gl, condensateForwardLoc("u_settleSpeed"), p.condensateSettleSpeed);
606
+ setUniform(gl, condensateForwardLoc("u_settleWiggle"), p.condensateSettleWiggle);
214
607
  drawFullscreenTriangle(gl);
215
608
  gl.bindFramebuffer(gl.FRAMEBUFFER, vaporCorrected.fbo);
216
609
  gl.useProgram(vaporCorrectProgram);
217
610
  bindTextureAt(gl, 0, vapor[front].texture, vaporCorrectProgram, "u_dye");
218
611
  bindTextureAt(gl, 1, vaporForward.texture, vaporCorrectProgram, "u_forward");
219
612
  setUniform(gl, vaporCorrectLoc("u_forwardSize"), gridSize);
220
- bindVelocityUniforms(vaporCorrectLoc, clampedDt, phase, p);
613
+ bindVelocityUniforms(vaporCorrectLoc, clampedDt, phase, p, spatial);
221
614
  drawFullscreenTriangle(gl);
222
615
  gl.bindFramebuffer(gl.FRAMEBUFFER, condensateCorrected.fbo);
223
616
  gl.useProgram(condensateCorrectProgram);
224
617
  bindTextureAt(gl, 0, condensate[front].texture, condensateCorrectProgram, "u_dye");
225
618
  bindTextureAt(gl, 1, condensateForward.texture, condensateCorrectProgram, "u_forward");
226
619
  setUniform(gl, condensateCorrectLoc("u_forwardSize"), gridSize);
227
- bindVelocityUniforms(condensateCorrectLoc, clampedDt, phase, p);
620
+ bindVelocityUniforms(condensateCorrectLoc, clampedDt, phase, p, spatial);
228
621
  setUniform(gl, condensateCorrectLoc("u_settleSpeed"), p.condensateSettleSpeed);
622
+ setUniform(gl, condensateCorrectLoc("u_settleWiggle"), p.condensateSettleWiggle);
229
623
  drawFullscreenTriangle(gl);
230
624
  gl.bindFramebuffer(gl.FRAMEBUFFER, vapor[back].fbo);
231
625
  gl.useProgram(vaporReactProgram);
@@ -259,9 +653,23 @@ function createMediumRuntime(gl, vertexSource) {
259
653
  gl.bindFramebuffer(gl.FRAMEBUFFER, track[back].fbo);
260
654
  gl.useProgram(trackAdvectProgram);
261
655
  bindTextureAt(gl, 0, track[front].texture, trackAdvectProgram, "u_dye");
262
- bindVelocityUniforms(trackAdvectLoc, clampedDt, phase, p);
656
+ bindVelocityUniforms(trackAdvectLoc, clampedDt, phase, p, spatial);
263
657
  setUniform(gl, trackAdvectLoc("u_decay"), p.decay);
264
658
  drawFullscreenTriangle(gl);
659
+ gl.bindFramebuffer(gl.FRAMEBUFFER, shadow.fbo);
660
+ gl.useProgram(shadowProgram);
661
+ bindTextureAt(gl, 0, vapor[back].texture, shadowProgram, "u_vapor");
662
+ bindTextureAt(gl, 1, condensate[back].texture, shadowProgram, "u_condensate");
663
+ setUniform(gl, shadowLoc("u_vaporSize"), gridSize);
664
+ setUniform(gl, shadowLoc("u_condensateSize"), gridSize);
665
+ setUniform(gl, shadowLoc("u_resolution"), gridSize);
666
+ setUniform(gl, shadowLoc("u_condensateGain"), p.condensateGain);
667
+ setUniform(gl, shadowLoc("u_extinction"), p.shadowExtinction);
668
+ for (const slot of [1, 2]) {
669
+ const position = p.lights[slot]?.position ?? [0, 0];
670
+ setUniform(gl, shadowLoc(`u_light${slot}Grid`), [position[0] * gridW, position[1] * gridH]);
671
+ }
672
+ drawFullscreenTriangle(gl);
265
673
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
266
674
  front = back;
267
675
  framesSinceRenorm += 1;
@@ -303,14 +711,16 @@ function createMediumRuntime(gl, vertexSource) {
303
711
  setUniform(gl, emitLoc("u_tailDim"), e.tailDim);
304
712
  setUniform(gl, emitLoc("u_intensity"), e.intensity);
305
713
  setUniform(gl, emitLoc("u_seed"), e.seed);
306
- setUniform(gl, emitLoc("u_channelMask"), e.channel);
307
714
  drawFullscreenTriangle(gl);
308
715
  }
309
716
  gl.disable(gl.BLEND);
310
717
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
311
718
  }
719
+ function stepTrackLayer(dt, emissions) {
720
+ liveTracks = trackLayer.step(dt, emissions);
721
+ }
312
722
  function composite(contentWidth, contentHeight, params = {}) {
313
- if (!vapor || !condensate || !track) return;
723
+ if (!vapor || !condensate || !track || !shadow) return;
314
724
  const p = { ...MEDIUM_DEFAULTS, ...params };
315
725
  gl.viewport(0, 0, contentWidth, contentHeight);
316
726
  gl.disable(gl.BLEND);
@@ -318,9 +728,11 @@ function createMediumRuntime(gl, vertexSource) {
318
728
  bindTextureAt(gl, 0, vapor[front].texture, compositeProgram, "u_vapor");
319
729
  bindTextureAt(gl, 1, condensate[front].texture, compositeProgram, "u_condensate");
320
730
  bindTextureAt(gl, 2, track[front].texture, compositeProgram, "u_track");
731
+ bindTextureAt(gl, 3, shadow.texture, compositeProgram, "u_shadow");
321
732
  setUniform(gl, compositeLoc("u_vaporSize"), [gridW, gridH]);
322
733
  setUniform(gl, compositeLoc("u_condensateSize"), [gridW, gridH]);
323
734
  setUniform(gl, compositeLoc("u_trackSize"), [gridW, gridH]);
735
+ setUniform(gl, compositeLoc("u_shadowSize"), [gridW, gridH]);
324
736
  setUniform(gl, compositeLoc("u_resolution"), [contentWidth, contentHeight]);
325
737
  setUniform(gl, compositeLoc("u_dyeScale"), [gridW / contentWidth, gridH / contentHeight]);
326
738
  setUniform(gl, compositeLoc("u_lab0"), srgbToOklab(p.channelColors[0]));
@@ -337,7 +749,12 @@ function createMediumRuntime(gl, vertexSource) {
337
749
  setUniform(gl, compositeLoc("u_farWeight"), p.depthFarWeight);
338
750
  setUniform(gl, compositeLoc("u_gravityBoost"), p.gravityBottomBoost);
339
751
  setUniform(gl, compositeLoc("u_gravityBoostStart"), p.gravityBottomBoostStart);
752
+ setUniform(gl, compositeLoc("u_channelScatter"), p.channelScatter);
753
+ for (let slot = 0; slot < MEDIUM_MAX_LIGHTS; slot += 1) bindLightUniforms(slot, p.lights[slot]);
754
+ setUniform(gl, compositeLoc("u_trackLightFloor"), p.trackLightFloor);
340
755
  drawFullscreenTriangle(gl);
756
+ const { data, count } = buildTrackLayerInstances(liveTracks, contentWidth, contentHeight, p.trackLayerAmount);
757
+ trackLayerProgram.draw(contentWidth, contentHeight, data, count, p.trackLightFloor, p.lights, shadow.texture);
341
758
  }
342
759
  function readTarget(target) {
343
760
  gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);
@@ -404,6 +821,23 @@ function createMediumRuntime(gl, vertexSource) {
404
821
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
405
822
  return { cols: gridW, rows: gridH, data };
406
823
  }
824
+ function readTrackGrid() {
825
+ if (!track) return null;
826
+ gl.bindFramebuffer(gl.FRAMEBUFFER, track[front].fbo);
827
+ const cells = gridW * gridH;
828
+ const data = new Array(cells);
829
+ if (format.type === gl.HALF_FLOAT) {
830
+ const buf = new Float32Array(cells * 4);
831
+ gl.readPixels(0, 0, gridW, gridH, gl.RGBA, gl.FLOAT, buf);
832
+ for (let i = 0; i < cells; i += 1) data[i] = buf[i * 4];
833
+ } else {
834
+ const buf = new Uint8Array(cells * 4);
835
+ gl.readPixels(0, 0, gridW, gridH, gl.RGBA, gl.UNSIGNED_BYTE, buf);
836
+ for (let i = 0; i < cells; i += 1) data[i] = buf[i * 4] / 255;
837
+ }
838
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
839
+ return { cols: gridW, rows: gridH, data };
840
+ }
407
841
  function destroy() {
408
842
  destroyTargets();
409
843
  gl.deleteProgram(vaporForwardProgram);
@@ -416,9 +850,22 @@ function createMediumRuntime(gl, vertexSource) {
416
850
  gl.deleteProgram(compositeProgram);
417
851
  gl.deleteProgram(seedProgram);
418
852
  gl.deleteProgram(emitProgram);
853
+ gl.deleteProgram(shadowProgram);
419
854
  gl.deleteProgram(resampleProgram);
855
+ trackLayerProgram.destroy();
420
856
  }
421
- return { ensureGrid, step, emit, composite, readTotals, readVaporGrid, readCondensateGrid, destroy };
857
+ return {
858
+ ensureGrid,
859
+ step,
860
+ emit,
861
+ stepTrackLayer,
862
+ composite,
863
+ readTotals,
864
+ readVaporGrid,
865
+ readCondensateGrid,
866
+ readTrackGrid,
867
+ destroy
868
+ };
422
869
  }
423
870
  function createMediumBackdrop() {
424
871
  let runtime = null;
@@ -431,12 +878,14 @@ function createMediumBackdrop() {
431
878
  runtime.ensureGrid(frame.gridWidth, frame.gridHeight);
432
879
  runtime.step(frame.dt, frame.params);
433
880
  runtime.emit(frame.emissions ?? []);
881
+ runtime.stepTrackLayer(frame.dt, frame.emissions ?? []);
434
882
  gl.bindFramebuffer(gl.FRAMEBUFFER, target.framebuffer);
435
883
  runtime.composite(target.width, target.height, frame.params);
436
884
  },
437
885
  readTotals: () => runtime?.readTotals() ?? null,
438
886
  readVaporGrid: () => runtime?.readVaporGrid() ?? null,
439
887
  readCondensateGrid: () => runtime?.readCondensateGrid() ?? null,
888
+ readTrackGrid: () => runtime?.readTrackGrid() ?? null,
440
889
  destroy: () => {
441
890
  runtime?.destroy();
442
891
  runtime = null;
@@ -445,6 +894,6 @@ function createMediumBackdrop() {
445
894
  };
446
895
  }
447
896
 
448
- export { createMediumBackdrop, createMediumRuntime };
897
+ export { buildTrackLayerInstances, createMediumBackdrop, createMediumRuntime, createTrackLayer, createTrackLayerProgram, trackLayerFade, trackLayerSigmaMul };
449
898
  //# sourceMappingURL=index.js.map
450
899
  //# sourceMappingURL=index.js.map