@mrigx/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/shaders.ts ADDED
@@ -0,0 +1,772 @@
1
+ /**
2
+ * @file shaders.ts
3
+ * @description WGSL shaders for the Mrig optical engine.
4
+ */
5
+
6
+ const sharedStructs = `
7
+ struct InteractionSource {
8
+ position: vec2<f32>,
9
+ velocity: vec2<f32>,
10
+ radius: f32,
11
+ strength: f32,
12
+ isActive: f32,
13
+ padding: f32,
14
+ };
15
+
16
+ struct SurfaceData {
17
+ bounds: vec4<f32>,
18
+ style: vec4<f32>,
19
+ atlasUV: vec4<f32>,
20
+ };
21
+
22
+ struct Uniforms {
23
+ time: f32,
24
+ scrollY: f32,
25
+ resolution: vec2<f32>,
26
+ sourceCount: u32,
27
+ surfaceCount: u32,
28
+ scrollHeight: f32,
29
+ scrollDelta: f32,
30
+ domWidth: f32,
31
+ reducedMotion: f32,
32
+ transitionState: f32,
33
+ transitionY: f32,
34
+ fluidViscosity: f32,
35
+ refractionScale: f32,
36
+ tearLimit: f32,
37
+ rippleHeight: f32,
38
+ abyssIntensity: f32,
39
+ parallaxIntensity: f32,
40
+ pad2: f32,
41
+ pad3: f32,
42
+ cursorData: vec4<f32>,
43
+ @align(16) sources: array<InteractionSource, 32>,
44
+ @align(16) surfaces: array<SurfaceData, 128>,
45
+ };
46
+
47
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
48
+ `;
49
+
50
+ export const simulationWGSL = sharedStructs + `
51
+ @group(0) @binding(1) var prevTexture: texture_2d<f32>;
52
+ @group(0) @binding(2) var textureSampler: sampler;
53
+ @group(0) @binding(3) var maskTexture: texture_2d<f32>;
54
+
55
+ struct VertexOutput {
56
+ @builtin(position) position: vec4<f32>,
57
+ @location(0) uv: vec2<f32>,
58
+ };
59
+
60
+ @vertex
61
+ fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
62
+ var pos = array<vec2<f32>, 3>(
63
+ vec2<f32>(-1.0, -1.0),
64
+ vec2<f32>( 3.0, -1.0),
65
+ vec2<f32>(-1.0, 3.0)
66
+ );
67
+ var output: VertexOutput;
68
+ output.position = vec4<f32>(pos[vertexIndex], 0.0, 1.0);
69
+ output.uv = pos[vertexIndex] * 0.5 + 0.5;
70
+ output.uv.y = 1.0 - output.uv.y;
71
+ return output;
72
+ }
73
+
74
+ @fragment
75
+ fn fs_main(@location(0) uv: vec2<f32>, @builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
76
+
77
+ // Exactly shift the read coordinates by the accumulated integer pixel scroll delta
78
+ let shiftY = i32(uniforms.scrollDelta);
79
+ let coord = vec2<i32>(pos.xy) + vec2<i32>(0, shiftY);
80
+ let res = vec2<i32>(uniforms.resolution);
81
+
82
+ // Helper to safely load pixels without bilinear filtering (prevents diffusion/smearing completely)
83
+ // We can't define a function inside a function in WGSL that captures outer scope cleanly,
84
+ // but doing clamp inline is easy.
85
+ let c = clamp(coord, vec2<i32>(0), res - vec2<i32>(1));
86
+ let state = textureLoad(prevTexture, c, 0);
87
+
88
+ let c_up = clamp(coord + vec2<i32>(0, 1), vec2<i32>(0), res - vec2<i32>(1));
89
+ let c_down = clamp(coord + vec2<i32>(0, -1), vec2<i32>(0), res - vec2<i32>(1));
90
+ let c_left = clamp(coord + vec2<i32>(-1, 0), vec2<i32>(0), res - vec2<i32>(1));
91
+ let c_right = clamp(coord + vec2<i32>(1, 0), vec2<i32>(0), res - vec2<i32>(1));
92
+
93
+ let h_cur = state.r;
94
+ let h_prev = state.g;
95
+ var ice = state.b;
96
+ var iceType = state.a;
97
+
98
+ let h_up = textureLoad(prevTexture, c_up, 0).r;
99
+ let h_down = textureLoad(prevTexture, c_down, 0).r;
100
+ let h_left = textureLoad(prevTexture, c_left, 0).r;
101
+ let h_right = textureLoad(prevTexture, c_right, 0).r;
102
+
103
+ let laplacian = h_up + h_down + h_left + h_right - 4.0 * h_cur;
104
+
105
+ let mask = textureSampleLevel(maskTexture, textureSampler, uv, 0.0);
106
+ let surfaceAlpha = mask.a;
107
+
108
+ // --- PHASE 5: Z-AXIS SUBMERSION (PHYSICS) ---
109
+ let maxScroll = max(1.0, uniforms.scrollHeight - uniforms.resolution.y);
110
+ let depth = clamp(uniforms.scrollY / maxScroll, 0.0, 1.0) * uniforms.abyssIntensity;
111
+
112
+ // Deeper water is denser. Waves travel slower but decay slower (heavier ripples).
113
+ let basePropagation = mix(0.48, 0.20, depth);
114
+ let baseDamping = mix(0.96, 0.99, depth);
115
+
116
+ let propagation = mix(basePropagation, 0.15, surfaceAlpha);
117
+ var damping = mix(baseDamping, 0.94, surfaceAlpha);
118
+
119
+ // Ice rapidly freezes the wave energy (damping drops to 0.1)
120
+ damping = mix(damping, 0.1, clamp(ice * 2.0, 0.0, 1.0));
121
+
122
+ var h_new = 2.0 * h_cur - h_prev + propagation * laplacian;
123
+ h_new *= damping;
124
+
125
+ let aspect = uniforms.resolution.x / uniforms.resolution.y;
126
+ var uvAspect = uv;
127
+ uvAspect.x *= aspect;
128
+
129
+ // --- FREEZER PLATE LOGIC (TEXT HIGHLIGHTS) ---
130
+ var highlightTargetIce = 0.0;
131
+ for (var j = 0u; j < uniforms.surfaceCount; j++) {
132
+ let s = uniforms.surfaces[j];
133
+ if (s.style.w > 0.5) { // isHighlight
134
+ let s_min = vec2<f32>(s.bounds.x, s.bounds.y);
135
+ let s_max = vec2<f32>(s.bounds.x + s.bounds.z, s.bounds.y + s.bounds.w);
136
+
137
+ let aspect_vec = vec2<f32>(aspect, 1.0);
138
+ let halfSizeAspect = (s_max - s_min) * 0.5 * aspect_vec;
139
+ let boxCenter = (s_min + s_max) * 0.5;
140
+ let p_uv = (uv - boxCenter) * aspect_vec;
141
+ let r = 0.008; // Soft rounded corners
142
+ let d_vec_sdf = abs(p_uv) - halfSizeAspect + vec2<f32>(r);
143
+ let d = length(max(d_vec_sdf, vec2<f32>(0.0))) + min(max(d_vec_sdf.x, d_vec_sdf.y), 0.0) - r;
144
+
145
+ let edge = 1.0 - smoothstep(-0.008, 0.008, d);
146
+ highlightTargetIce = max(highlightTargetIce, edge);
147
+ }
148
+ }
149
+
150
+ if (highlightTargetIce > 0.0) {
151
+ ice = min(highlightTargetIce, ice + 0.08); // Refreezes fast up to the SDF mask
152
+ iceType = 1.0;
153
+ }
154
+
155
+ // --- INTERACTION & ICE BREAKING ---
156
+ let wasIce = state.b;
157
+
158
+ for(var i = 0u; i < uniforms.sourceCount; i++) {
159
+ let src = uniforms.sources[i];
160
+ if (src.isActive > 0.0 && src.padding < 1500.0) { // Normal water disturbance (Cursor or Boat)
161
+ var ptrAspect = src.position;
162
+ ptrAspect.x *= aspect;
163
+ let isBoat = (src.padding > 500.0);
164
+
165
+ let localOffset = uvAspect - ptrAspect;
166
+ let dist = length(localOffset);
167
+ let r = max(0.01, src.radius);
168
+
169
+ if (wasIce > 0.05 || highlightTargetIce > 0.0) {
170
+ // We are breaking ice, or inside a freezer plate. No water ripple!
171
+ let breakForce = (1.0 - smoothstep(0.0, r, dist)) * src.strength;
172
+ ice = max(0.0, ice - breakForce * 1.5); // Soft shattered wake
173
+ } else {
174
+ // Normal water disturbance (only if no ice)
175
+ if (isBoat && dist < r * 2.0) {
176
+ let bAngle = src.padding - 1000.0;
177
+ let s = sin(-bAngle);
178
+ let c = cos(-bAngle);
179
+ // Rotate local space so boat points along +x
180
+ let lx = localOffset.x * c - localOffset.y * s;
181
+ let ly = localOffset.x * s + localOffset.y * c;
182
+
183
+ // Majestic, sweeping Kelvin V-wake
184
+ var wakeForce = 0.0;
185
+ if (lx < -0.01) { // Only behind the boat
186
+ let distBehind = -lx;
187
+ let wakeArmDist = abs(abs(ly) - distBehind * 0.35); // 19.5 degree Kelvin angle
188
+ // The wake gets wider and softer the further back it goes
189
+ let wakeThickness = r * 0.2 + distBehind * 0.3; // Scaled down spread
190
+ let armStrength = smoothstep(wakeThickness, 0.0, wakeArmDist);
191
+ // Fade out over a medium distance (not too short, not too massive)
192
+ let fade = smoothstep(0.15, 0.0, distBehind); // Reduced trail length
193
+ wakeForce = armStrength * fade * src.strength * 0.08; // Reduced overall force
194
+
195
+ // Also add the deep central propeller wash (negative displacement)
196
+ let centerWash = smoothstep(wakeThickness * 0.8, 0.0, abs(ly)) * fade * src.strength * -0.04;
197
+ wakeForce += centerWash;
198
+ }
199
+
200
+ // Bow wave (pushing water up)
201
+ let bowDist = distance(vec2<f32>(lx, ly), vec2<f32>(0.02, 0.0));
202
+ let bowForce = (1.0 - smoothstep(0.0, r * 0.5, bowDist)) * src.strength * 0.15; // Scaled down bow wave
203
+
204
+ h_new += bowForce + wakeForce;
205
+
206
+ } else if (!isBoat && dist < r) {
207
+ let force = (1.0 - smoothstep(0.0, r, dist)) * src.strength;
208
+ h_new += force * 0.15;
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ // --- MELT LOGIC ---
215
+ if (iceType > 0.5) {
216
+ // This is text highlight ice!
217
+ // If the text scrolled away or was deselected, instantly vanish to prevent trails!
218
+ if (highlightTargetIce == 0.0) {
219
+ ice = 0.0;
220
+ iceType = 0.0;
221
+ }
222
+ } else {
223
+ // Manual cursor ice slowly melts
224
+ ice = max(0.0, ice - 0.002);
225
+ }
226
+
227
+ h_new = clamp(h_new, -2.0, 2.0);
228
+ ice = clamp(ice, 0.0, 1.0);
229
+ return vec4<f32>(h_new, h_cur, ice, iceType);
230
+ }
231
+ `;
232
+
233
+ export const surfaceMaskWGSL = sharedStructs + `
234
+ struct VertexOutput {
235
+ @builtin(position) position: vec4<f32>,
236
+ @location(0) uv: vec2<f32>,
237
+ @location(1) @interpolate(flat) instanceIdx: u32,
238
+ };
239
+
240
+ @vertex
241
+ fn vs_main(
242
+ @builtin(vertex_index) vertexIndex: u32,
243
+ @builtin(instance_index) instanceIndex: u32
244
+ ) -> VertexOutput {
245
+ let surf = uniforms.surfaces[instanceIndex];
246
+
247
+ var pos = array<vec2<f32>, 6>(
248
+ vec2<f32>(0.0, 0.0), vec2<f32>(1.0, 0.0), vec2<f32>(0.0, 1.0),
249
+ vec2<f32>(1.0, 0.0), vec2<f32>(1.0, 1.0), vec2<f32>(0.0, 1.0)
250
+ );
251
+
252
+ let p = pos[vertexIndex];
253
+ let bounds = surf.bounds;
254
+
255
+ var screenPos = vec2<f32>(bounds.x + p.x * bounds.z, bounds.y + p.y * bounds.w);
256
+ var clipPos = screenPos * 2.0 - 1.0;
257
+ clipPos.y = -clipPos.y;
258
+
259
+ var output: VertexOutput;
260
+ output.position = vec4<f32>(clipPos, 0.0, 1.0);
261
+ output.uv = p;
262
+ output.instanceIdx = instanceIndex;
263
+ return output;
264
+ }
265
+
266
+ fn sdRoundRect(p: vec2<f32>, b: vec2<f32>, r: f32) -> f32 {
267
+ let q = abs(p) - b + vec2<f32>(r);
268
+ return min(max(q.x, q.y), 0.0) + length(max(q, vec2<f32>(0.0))) - r;
269
+ }
270
+
271
+ @fragment
272
+ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
273
+ let surf = uniforms.surfaces[in.instanceIdx];
274
+ let aspect = uniforms.resolution.x / uniforms.resolution.y;
275
+
276
+ var p = in.uv - 0.5;
277
+ let widthAspect = surf.bounds.z * aspect;
278
+ let height = surf.bounds.w;
279
+
280
+ p.x *= widthAspect;
281
+ p.y *= height;
282
+
283
+ let halfSize = vec2<f32>(widthAspect * 0.5, height * 0.5);
284
+ let r = surf.style.x;
285
+
286
+ let d = sdRoundRect(p, halfSize, r);
287
+ let pixelSize = 1.0 / uniforms.resolution.y;
288
+ let alpha = 1.0 - smoothstep(-pixelSize, pixelSize, d);
289
+
290
+ if (alpha < 0.01) {
291
+ discard;
292
+ }
293
+
294
+ let edgeRim = smoothstep(0.0, pixelSize * 40.0, -d);
295
+ let materialId = surf.style.y;
296
+ let strength = surf.style.z;
297
+
298
+ return vec4<f32>(materialId, strength, edgeRim, alpha);
299
+ }
300
+ `;
301
+
302
+ export const opticalWGSL = sharedStructs + `
303
+ @group(0) @binding(1) var simTexture: texture_2d<f32>;
304
+ @group(0) @binding(2) var textureSampler: sampler;
305
+ @group(0) @binding(3) var maskTexture: texture_2d<f32>;
306
+ @group(0) @binding(4) var domTexture: texture_2d<f32>;
307
+ @group(0) @binding(5) var atlasTexture: texture_2d<f32>;
308
+
309
+ struct VertexOutput {
310
+ @builtin(position) position: vec4<f32>,
311
+ @location(0) uv: vec2<f32>,
312
+ };
313
+
314
+ @vertex
315
+ fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
316
+ var pos = array<vec2<f32>, 3>(
317
+ vec2<f32>(-1.0, -1.0), vec2<f32>( 3.0, -1.0), vec2<f32>(-1.0, 3.0)
318
+ );
319
+ var output: VertexOutput;
320
+ output.position = vec4<f32>(pos[vertexIndex], 0.0, 1.0);
321
+ output.uv = pos[vertexIndex] * 0.5 + 0.5;
322
+ output.uv.y = 1.0 - output.uv.y;
323
+ return output;
324
+ }
325
+
326
+ fn mrigSpectral(t: f32) -> vec3<f32> {
327
+ var c = vec3<f32>(0.05, 0.09, 0.34); // Base: Deep Navy (#0D1757)
328
+
329
+ // Cobalt (#2C4FAB)
330
+ c = mix(c, vec3<f32>(0.17, 0.31, 0.67), smoothstep(0.00, 0.15, t));
331
+ // Electric Blue (#405FD2)
332
+ c = mix(c, vec3<f32>(0.25, 0.37, 0.82), smoothstep(0.10, 0.25, t));
333
+ // Cyan (soft)
334
+ c = mix(c, vec3<f32>(0.00, 0.80, 1.00), smoothstep(0.20, 0.35, t));
335
+ // Ice Blue / White (#DDEFF3) - Peak specular flash
336
+ c = mix(c, vec3<f32>(0.87, 0.94, 0.95), smoothstep(0.30, 0.45, t));
337
+ // Violet (#514A9A)
338
+ c = mix(c, vec3<f32>(0.32, 0.29, 0.60), smoothstep(0.40, 0.60, t));
339
+ // Magenta (#8A4D9A)
340
+ c = mix(c, vec3<f32>(0.54, 0.30, 0.60), smoothstep(0.55, 0.70, t));
341
+ // Coral / Red (#C25D53)
342
+ c = mix(c, vec3<f32>(0.76, 0.36, 0.33), smoothstep(0.65, 0.80, t));
343
+ // Iridescent Green (#5FAF7A)
344
+ c = mix(c, vec3<f32>(0.37, 0.69, 0.48), smoothstep(0.75, 0.90, t));
345
+ // Blue (#1A2F85)
346
+ c = mix(c, vec3<f32>(0.10, 0.18, 0.52), smoothstep(0.85, 1.00, t));
347
+
348
+ return c;
349
+ }
350
+
351
+ @fragment
352
+ fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
353
+ let state = textureSample(simTexture, textureSampler, uv);
354
+ let waveHeight = state.r;
355
+
356
+ let texel = 1.0 / uniforms.resolution;
357
+ let h_up = textureSample(simTexture, textureSampler, uv + vec2<f32>(0.0, texel.y)).r;
358
+ let h_down = textureSample(simTexture, textureSampler, uv + vec2<f32>(0.0, -texel.y)).r;
359
+ let h_left = textureSample(simTexture, textureSampler, uv + vec2<f32>(-texel.x, 0.0)).r;
360
+ let h_right = textureSample(simTexture, textureSampler, uv + vec2<f32>(texel.x, 0.0)).r;
361
+
362
+ // ==========================================
363
+ // LIGHTHOUSE POSITION & EXCLUSION ZONE
364
+ // ==========================================
365
+ let aspect = uniforms.resolution.x / uniforms.resolution.y;
366
+
367
+ // 1 & 2. MOVE THE LIGHTHOUSE (slightly further left and down)
368
+ let startLightX = aspect - 0.35;
369
+ let startLightY = 0.40;
370
+ let scrollOffset = uniforms.scrollY / uniforms.resolution.y;
371
+ let currentLightY = startLightY - (scrollOffset * uniforms.parallaxIntensity);
372
+
373
+ let lightPos = vec3<f32>(startLightX, currentLightY, 0.15);
374
+ let pixelPos = vec3<f32>(uv.x * aspect, uv.y, 0.0);
375
+ let distToLight = length(lightPos.xy - pixelPos.xy);
376
+
377
+ // REMOVE HOVER/CURSOR EFFECT FROM THE LIGHTHOUSE (Kept exactly as it is)
378
+ let exclusionMask = smoothstep(0.03, 0.16, distToLight); // Doubled exclusion radius
379
+
380
+ let gradRaw = vec2<f32>(h_right - h_left, h_up - h_down) * 5.0;
381
+ let waveCurvatureRaw = (h_up + h_down + h_left + h_right - 4.0 * waveHeight) * 20.0;
382
+
383
+ let grad = gradRaw * exclusionMask;
384
+ let waveCurvature = waveCurvatureRaw * exclusionMask;
385
+
386
+ // --- PHASE 5: Z-AXIS SUBMERSION ---
387
+ let maxScroll = max(1.0, uniforms.scrollHeight - uniforms.resolution.y);
388
+ let depth = clamp(uniforms.scrollY / maxScroll, 0.0, 1.0) * uniforms.abyssIntensity;
389
+ // ----------------------------------
390
+
391
+ // Deeper = higher refractive index, more liquid distortion
392
+ var fluidDistortion = grad * mix(0.02, 0.05 + (uniforms.transitionState * 1.5), depth);
393
+ let tDistort = uniforms.transitionState;
394
+ let tearLimit = uniforms.tearLimit + (tDistort * 0.4); // Allow MASSIVE tearing during splash!
395
+ fluidDistortion = clamp(fluidDistortion, vec2<f32>(-tearLimit), vec2<f32>(tearLimit));
396
+ var maskUv = uv + fluidDistortion;
397
+ maskUv = clamp(maskUv, vec2<f32>(0.0), vec2<f32>(1.0));
398
+ let mask = textureSample(maskTexture, textureSampler, maskUv);
399
+
400
+ let maskAlpha = mask.a;
401
+ let activeRefractionMult = mask.r / max(maskAlpha, 0.001);
402
+ let activeHighlight = mask.g / max(maskAlpha, 0.001);
403
+ // --- Phase 1: Buoyant Liquid Layouts (Rigid Body Warp Field) ---
404
+ var buoyantOffset = vec2<f32>(0.0);
405
+ var buoyantWeight = 0.001;
406
+ let ice = state.b;
407
+ var highlightIce = ice; // Use physical ice from simulation!
408
+
409
+ for (var i = 0u; i < uniforms.surfaceCount; i++) {
410
+ let s = uniforms.surfaces[i];
411
+ let s_min = vec2<f32>(s.bounds.x, s.bounds.y);
412
+ let s_max = vec2<f32>(s.bounds.x + s.bounds.z, s.bounds.y + s.bounds.w);
413
+ let center = (s_min + s_max) * 0.5;
414
+ let isHighlight = s.style.w;
415
+
416
+ let d_vec = max(s_min - uv, uv - s_max);
417
+ let dist = length(max(d_vec, vec2<f32>(0.0)));
418
+
419
+ if (isHighlight < 0.5) { // Only buoyant for actual UI elements
420
+ let weight = exp(-dist * 35.0);
421
+ if (weight > 0.005) {
422
+ let hCenter = textureSampleLevel(simTexture, textureSampler, center, 0.0).r;
423
+ let hRight = textureSampleLevel(simTexture, textureSampler, center + vec2<f32>(0.02, 0.0), 0.0).r;
424
+ let hTop = textureSampleLevel(simTexture, textureSampler, center + vec2<f32>(0.0, 0.02), 0.0).r;
425
+
426
+ let tilt = vec2<f32>(hRight - hCenter, hTop - hCenter) * 15.0;
427
+ let localUv = uv - center;
428
+ let scaleOffset = localUv * (hCenter * -0.003); // Reduced bulge for UI stability
429
+ let transOffset = tilt * 0.0001; // Drastically reduced rigid-body translation to stop UI elements from running away from the cursor
430
+
431
+ buoyantOffset += (scaleOffset + transOffset) * weight;
432
+ buoyantWeight += weight;
433
+ }
434
+ }
435
+ }
436
+ if (buoyantWeight > 1.0) { buoyantOffset /= buoyantWeight; }
437
+
438
+ // Micro-refraction (local glass distortion)
439
+ let defaultRefraction = fluidDistortion * 1.0;
440
+ let surfaceRefraction = fluidDistortion * activeRefractionMult;
441
+ var domRefraction = mix(defaultRefraction, surfaceRefraction, maskAlpha);
442
+ let iceWarpX = sin(uv.y * 150.0 + uniforms.time * 0.2) * 0.002;
443
+ let iceWarpY = cos(uv.x * 150.0 + uniforms.time * 0.2) * 0.002;
444
+
445
+ // Completely disable water refraction inside highlight ice!
446
+ let activeRefraction = domRefraction * (1.0 - highlightIce);
447
+ let finalRefraction = activeRefraction + vec2<f32>(iceWarpX, iceWarpY) * highlightIce;
448
+
449
+ // Combine buoyant rigid warp + active refraction
450
+ var finalUv = uv + buoyantOffset + finalRefraction;
451
+ finalUv.x = (finalUv.x * uniforms.resolution.x) / uniforms.domWidth;
452
+ finalUv.y = (finalUv.y * uniforms.resolution.y + uniforms.scrollY) / uniforms.scrollHeight;
453
+ finalUv = clamp(finalUv, vec2<f32>(0.0), vec2<f32>(1.0));
454
+
455
+ // Smooth multi-sample frost blur for elegant glass look
456
+ // Increase base frost and add massive localized frost on the ripples during splash
457
+ let localFrost = 2.0 + abs(waveHeight) * 150.0;
458
+ let depthBlur = (pow(depth, 2.5) * 4.0) + (tDistort * localFrost);
459
+ let blurOffset = vec2<f32>(0.003, 0.003) * highlightIce;
460
+
461
+ var domColor = vec4<f32>(0.0);
462
+
463
+ // WARP-AWARE CULLING: Only do the heavy texture sampling if we are inside a UI element
464
+ if (maskAlpha > 0.0) {
465
+ // ATLAS CHECK: Is this pixel inside a dynamic surface?
466
+ var isDynamicPixel = false;
467
+ var dynAtlasUV = vec4<f32>(0.0);
468
+ var dynBounds = vec4<f32>(0.0);
469
+
470
+ for (var i = 0u; i < uniforms.surfaceCount; i++) {
471
+ let s = uniforms.surfaces[i];
472
+ if (s.atlasUV.z > 0.0) { // Has atlas mapping
473
+ let s_min = vec2<f32>(s.bounds.x, s.bounds.y);
474
+ let s_max = vec2<f32>(s.bounds.x + s.bounds.z, s.bounds.y + s.bounds.w);
475
+ if (finalUv.x >= s_min.x && finalUv.x <= s_max.x && finalUv.y >= s_min.y && finalUv.y <= s_max.y) {
476
+ isDynamicPixel = true;
477
+ dynAtlasUV = s.atlasUV;
478
+ dynBounds = s.bounds;
479
+ break;
480
+ }
481
+ }
482
+ }
483
+
484
+ var baseColor: vec4<f32>;
485
+ var blur1: vec4<f32>;
486
+ var blur2: vec4<f32>;
487
+
488
+ if (isDynamicPixel) {
489
+ let localNorm = (finalUv - dynBounds.xy) / dynBounds.zw;
490
+ let sampleUV = mix(dynAtlasUV.xy, dynAtlasUV.zw, clamp(localNorm, vec2<f32>(0.0), vec2<f32>(1.0)));
491
+ let scaleUV = (dynAtlasUV.zw - dynAtlasUV.xy);
492
+ let atlasBlurOffset = blurOffset * scaleUV;
493
+
494
+ baseColor = textureSampleLevel(atlasTexture, textureSampler, sampleUV, depthBlur);
495
+ blur1 = textureSampleLevel(atlasTexture, textureSampler, clamp(sampleUV + atlasBlurOffset, dynAtlasUV.xy, dynAtlasUV.zw), depthBlur);
496
+ blur2 = textureSampleLevel(atlasTexture, textureSampler, clamp(sampleUV - atlasBlurOffset, dynAtlasUV.xy, dynAtlasUV.zw), depthBlur);
497
+ } else {
498
+ baseColor = textureSampleLevel(domTexture, textureSampler, finalUv, depthBlur);
499
+ blur1 = textureSampleLevel(domTexture, textureSampler, clamp(finalUv + blurOffset, vec2<f32>(0.0), vec2<f32>(1.0)), depthBlur);
500
+ blur2 = textureSampleLevel(domTexture, textureSampler, clamp(finalUv - blurOffset, vec2<f32>(0.0), vec2<f32>(1.0)), depthBlur);
501
+ }
502
+
503
+ var domColorRaw = (baseColor.rgb + blur1.rgb + blur2.rgb) / 3.0;
504
+ let abyssTint = mix(vec3<f32>(1.0), vec3<f32>(0.08, 0.15, 0.25), depth * 0.9);
505
+ domColorRaw *= abyssTint;
506
+ domColor = vec4<f32>(domColorRaw, baseColor.a);
507
+ } else {
508
+ // We are in the abyss (background).
509
+ let baseColor = textureSampleLevel(domTexture, textureSampler, finalUv, depthBlur);
510
+ let abyssTint = mix(vec3<f32>(1.0), vec3<f32>(0.08, 0.15, 0.25), depth * 0.9);
511
+ domColor = vec4<f32>(baseColor.rgb * abyssTint, baseColor.a);
512
+ }
513
+
514
+
515
+
516
+ var color = vec3<f32>(0.0);
517
+
518
+ // ==========================================
519
+ // LAYER 2: THE LIGHT SOURCE (Top-Down View + Cycling Color)
520
+ // ==========================================
521
+ let L = normalize(lightPos - pixelPos);
522
+ let dirFromLight = -L.xy;
523
+ let rayAngle = atan2(dirFromLight.y, dirFromLight.x);
524
+
525
+ let sweepSpeed = uniforms.time * 0.5;
526
+ let beam1 = pow(max(0.0, cos(rayAngle - sweepSpeed)), 48.0);
527
+ let beam2 = pow(max(0.0, cos(rayAngle - sweepSpeed + 3.14159265)), 48.0);
528
+ let rayBase = beam1 + beam2;
529
+
530
+ let rayDecay = mix(0.7, 4.5, depth); // Deep water scatters the light heavily
531
+ let rayIntensity = rayBase * exp(-distToLight * rayDecay); // Slower decay = longer throw of light
532
+
533
+ // 3. CYCLING LIGHT COLOR (Smooth 20-second spectral loop)
534
+ let cycleT = fract(uniforms.time * 0.05);
535
+ let beaconRayColor = mrigSpectral(cycleT);
536
+
537
+ color += beaconRayColor * rayIntensity * 0.35 * (1.0 - domColor.a); // Higher multiplier = more intense
538
+
539
+ // TOP-DOWN LIGHTHOUSE STRUCTURE (Doubled Size)
540
+ let core = exp(-distToLight * 45.0); // Doubled core
541
+ let housing = smoothstep(0.012, 0.00, abs(distToLight - 0.036)) * 0.15; // Doubled housing ring
542
+ let corona = exp(-distToLight * 8.0) * 0.25; // Doubled radiating corona
543
+
544
+ color += vec3<f32>(0.867, 0.937, 0.953) * core; // Core remains ice-white
545
+ color += beaconRayColor * (housing + corona); // Structure uses cycling emission color
546
+
547
+ // ==========================================
548
+ // LAYER 3: OPTICAL RESPONSE (Liquid reacting to Light)
549
+ // ==========================================
550
+ // VOLUMETRIC STAINED GLASS CAUSTICS
551
+ // Trace light rays from the DOM down to the simulation floor
552
+ let floorDepth = 0.08;
553
+ var floorUv = uv - grad * floorDepth; // Light bent by the surface gradient
554
+ floorUv.x = (floorUv.x * uniforms.resolution.x) / uniforms.domWidth;
555
+ floorUv.y = (floorUv.y * uniforms.resolution.y + uniforms.scrollY) / uniforms.scrollHeight;
556
+ floorUv = clamp(floorUv, vec2<f32>(0.0), vec2<f32>(1.0));
557
+
558
+ // Sample a blurred version of the DOM for ambient light projection
559
+ let domLightColor = textureSampleLevel(domTexture, textureSampler, floorUv, 2.5);
560
+
561
+ // Focus the light rays based on the curvature of the liquid surface (Laplacian)
562
+ let causticFocus = pow(max(0.0, waveCurvature * 2.5), 1.5) * 6.0;
563
+
564
+ // Cast caustics where there is water (not ice)
565
+ let causticStrength = causticFocus * (1.0 - highlightIce);
566
+ let stainedGlassCaustics = domLightColor.rgb * domLightColor.a * causticStrength * mix(1.0, 0.2, depth); // Caustics fade in the abyss
567
+
568
+ // Ambient caustic wash (unfocused light leaking through)
569
+ let ambientCaustics = domLightColor.rgb * domLightColor.a * clamp(waveCurvature * 0.5, -0.2, 0.5) * (1.0 - highlightIce);
570
+
571
+ color += (stainedGlassCaustics * 2.5) + (ambientCaustics * 1.5);
572
+
573
+ // ------------------------------------------
574
+ let N = normalize(vec3<f32>(-grad.x, -grad.y, 1.0));
575
+ let V = vec3<f32>(0.0, 0.0, 1.0);
576
+ let R = reflect(-L, N);
577
+
578
+ let specularHighlight = pow(max(dot(R, V), 0.0), 32.0) * (rayBase * 0.8 + 0.2);
579
+ let activity = clamp(length(grad) * 20.0, 0.0, 1.0);
580
+
581
+ // 4. AFFECTING THE LIQUID: The specular liquid flash is heavily tinted by the cycling emitted light
582
+ let specColor = mix(vec3<f32>(0.867, 0.937, 0.953), beaconRayColor, 0.65);
583
+ color += specColor * specularHighlight * activity * 1.8;
584
+
585
+ let intensity = clamp(length(grad) * 15.0 + abs(waveCurvature) * 3.0, 0.0, 1.0);
586
+ let spectralColor = mrigSpectral(intensity);
587
+ color = mix(color, domColor.rgb, domColor.a);
588
+
589
+ // Elegant bright icy tint, specifically mapping text highlights to glowing frost
590
+ let frostTint = vec3<f32>(0.92, 0.96, 1.0); // Restored the elegant ice white/blue
591
+ color = mix(color, frostTint, highlightIce * 0.35);
592
+
593
+ if (maskAlpha > 0.0) {
594
+ // Minimalist glass aesthetic: No thick colored 3D borders.
595
+ // Only subtly illuminate the edge if a physical wave passes through it.
596
+ let edgeRim = mask.b / max(maskAlpha, 0.001);
597
+ let edgeFresnel = (1.0 - edgeRim);
598
+ color += vec3<f32>(edgeFresnel * max(0.0, waveCurvature) * activeHighlight * maskAlpha);
599
+ }
600
+
601
+ // ==========================================
602
+ // LAYER 4: AUTONOMOUS BOATS (Abstract Glowing Entities)
603
+ // ==========================================
604
+ for (var i = 0u; i < uniforms.sourceCount; i++) {
605
+ let src = uniforms.sources[i];
606
+ if (src.padding > 500.0 && src.padding < 1500.0) {
607
+ let bAngle = src.padding - 1000.0;
608
+ let bPos = src.position;
609
+ let pixelAspect = vec2<f32>(bPos.x * aspect, bPos.y);
610
+ let bDist = distance(pixelPos.xy, pixelAspect);
611
+
612
+ if (bDist < 0.15) {
613
+ let localP = pixelPos.xy - pixelAspect;
614
+ let s = sin(-bAngle);
615
+ let c = cos(-bAngle);
616
+ let rotP = vec2<f32>(localP.x * c - localP.y * s, localP.x * s + localP.y * c);
617
+
618
+ // Design a proper nautical hull shape
619
+ let boatHalfLen = 0.025; // Bigger length
620
+ let boatHalfWid = 0.007; // Bigger width
621
+
622
+ // Normalized x from -1 (stern) to 1 (bow)
623
+ let nx = clamp(rotP.x / boatHalfLen, -1.0, 1.0);
624
+
625
+ // Width factor: flat at stern, pointed at bow
626
+ var wFactor = 1.0;
627
+ if (nx > 0.0) {
628
+ wFactor = 1.0 - (nx * nx); // Parabolic taper to front point
629
+ } else {
630
+ wFactor = 0.8 + 0.2 * (nx + 1.0); // Slight taper to stern
631
+ }
632
+
633
+ let localW = boatHalfWid * wFactor;
634
+
635
+ let dX = abs(rotP.x) - boatHalfLen;
636
+ let dY = abs(rotP.y) - localW;
637
+ let boatSDF = length(max(vec2<f32>(dX, dY), vec2<f32>(0.0))) + min(max(dX, dY), 0.0);
638
+ let boatAlpha = smoothstep(0.002, 0.0, boatSDF);
639
+
640
+ if (boatAlpha > 0.0) {
641
+ // Fake 3D rounded hull normal
642
+ let nz = sqrt(max(0.0, 1.0 - (nx*nx) - (rotP.y/(localW+0.0001))*(rotP.y/(localW+0.0001))));
643
+ let boatN = normalize(vec3<f32>(rotP.x / boatHalfLen, rotP.y / (localW+0.0001), nz + 0.2));
644
+
645
+ // Cabin geometry (small rectangular block in the rear half)
646
+ let cx = rotP.x + 0.008; // offset back
647
+ let cy = rotP.y;
648
+ let cX = abs(cx) - 0.005; // half len
649
+ let cY = abs(cy) - 0.003; // half wid
650
+ let cabinSDF = length(max(vec2<f32>(cX, cY), vec2<f32>(0.0))) + min(max(cX, cY), 0.0);
651
+ let isCabin = smoothstep(0.001, 0.0, cabinSDF);
652
+
653
+ // Base color
654
+ var boatCol = vec3<f32>(0.04, 0.05, 0.08); // Dark hull
655
+ if (isCabin > 0.0) {
656
+ boatCol = vec3<f32>(0.5, 0.55, 0.6); // Lighter cabin
657
+ }
658
+
659
+ // Catch lighthouse light
660
+ let bL = normalize(lightPos - vec3<f32>(pixelAspect, 0.0));
661
+ let diff = max(0.0, dot(boatN, bL));
662
+ let spec = pow(max(0.0, dot(reflect(-bL, boatN), V)), 32.0);
663
+
664
+ let distToL = length(lightPos.xy - pixelAspect);
665
+ let lIntensity = exp(-distToL * 0.7);
666
+
667
+ // Spectral hints on the hull edges (not the cabin)
668
+ let edge = 1.0 - max(0.0, boatN.z);
669
+ let hint = mrigSpectral(fract(uniforms.time * 0.03 + bPos.x)) * edge * 0.6 * (1.0 - isCabin);
670
+
671
+ // Illumination
672
+ boatCol += beaconRayColor * (diff * 0.3 + spec * (1.5 + isCabin * 2.0)) * lIntensity;
673
+ boatCol += hint * lIntensity;
674
+
675
+ // Boat reacts to the Stained Glass DOM light!
676
+ let bFloorUv = vec2<f32>((pixelAspect.x * uniforms.resolution.x) / uniforms.domWidth, (pixelAspect.y * uniforms.resolution.y + uniforms.scrollY) / uniforms.scrollHeight);
677
+ let bAmbient = textureSampleLevel(domTexture, textureSampler, clamp(bFloorUv, vec2(0.0), vec2(1.0)), 4.0);
678
+ boatCol += bAmbient.rgb * bAmbient.a * 1.2 * (1.0 - isCabin); // Hull picks up ambient DOM light
679
+
680
+ let bioluminescence = mix(0.0, 3.5, pow(depth, 2.0));
681
+ boatCol += vec3<f32>(0.1, 0.8, 1.0) * bioluminescence * (1.0 - isCabin);
682
+
683
+ color = mix(color, boatCol, boatAlpha);
684
+ }
685
+
686
+ // Clean directional headlamp pointing forward from the bow
687
+ let bowOffset = vec2<f32>(cos(bAngle), sin(bAngle)) * boatHalfLen;
688
+ let fromBow = localP - bowOffset;
689
+ let distFromBow = length(fromBow);
690
+ let fwd = vec2<f32>(cos(bAngle), sin(bAngle));
691
+
692
+ if (dot(fwd, fromBow) > 0.0) {
693
+ let toPixel = normalize(fromBow);
694
+ let dotFwd = dot(fwd, toPixel);
695
+ if (dotFwd > 0.85) {
696
+ let cone = pow(max(0.0, dotFwd), 32.0);
697
+ let falloff = exp(-distFromBow * 30.0);
698
+ // Deep abyss makes headlamps intensely visible like anglerfish
699
+ let bLightCol = mix(vec3<f32>(0.8, 0.9, 1.0), vec3<f32>(0.5, 0.9, 1.0), depth);
700
+ let intensity = mix(0.4, 1.5, depth);
701
+ color += bLightCol * cone * falloff * intensity;
702
+ }
703
+ }
704
+ } } }
705
+
706
+ // Darken slightly during massive transition splash
707
+ color = mix(color, color * vec3<f32>(0.2, 0.4, 0.6), tDistort * 0.7);
708
+
709
+ // ==========================================
710
+ // NATIVE WEBGPU OPTICAL CURSOR
711
+ // ==========================================
712
+ let cursorX = uniforms.cursorData.x * aspect;
713
+ let cursorY = uniforms.cursorData.y;
714
+ let isText = uniforms.cursorData.z > 0.5;
715
+ let isClick = uniforms.cursorData.w > 0.5;
716
+
717
+ let cDist = abs(pixelPos.xy - vec2<f32>(cursorX, cursorY));
718
+
719
+ if (isText) {
720
+ // Text hover state (tall pill)
721
+ // Size: width ~4px, height ~24px
722
+ let pw = 0.002;
723
+ let ph = 0.012;
724
+ let dX = max(cDist.x - pw, 0.0);
725
+ let dY = max(cDist.y - ph, 0.0);
726
+ let sdf = length(vec2<f32>(dX, dY));
727
+
728
+ // Core pill
729
+ let pill = smoothstep(0.002, 0.001, sdf);
730
+ // Glow
731
+ let glow = exp(-sdf * 120.0) * 0.6;
732
+
733
+ color = mix(color, vec3<f32>(1.0), pill + glow);
734
+ } else {
735
+ // Normal cursor (hollow ring + glow)
736
+ let rBase = select(0.015, 0.010, isClick);
737
+ let sdf = length(cDist);
738
+
739
+ // The solid ring edge
740
+ let ring = smoothstep(0.001, 0.0, abs(sdf - rBase));
741
+ // Inner and outer glow
742
+ let glow = exp(-abs(sdf - rBase) * 80.0) * 0.5;
743
+ // Faint inset fill
744
+ let fill = select(0.0, 0.1, sdf < rBase);
745
+
746
+ color = mix(color, vec3<f32>(1.0), ring + glow + fill);
747
+ }
748
+
749
+ return vec4<f32>(color, 1.0);
750
+ }
751
+ `;
752
+
753
+
754
+
755
+
756
+
757
+
758
+
759
+
760
+
761
+
762
+
763
+
764
+
765
+
766
+
767
+
768
+
769
+
770
+
771
+
772
+