@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/package.json +26 -0
- package/src/AtlasManager.ts +125 -0
- package/src/DomRasterSystem.ts +113 -0
- package/src/MrigEngine.ts +834 -0
- package/src/WebGPUManager.ts +65 -0
- package/src/index.ts +5 -0
- package/src/shaders.ts +772 -0
- package/src/types.ts +38 -0
- package/src/utils.ts +26 -0
- package/tsconfig.json +32 -0
- package/vite.config.ts +35 -0
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
|
+
|