PageSourceSearch

https://www.fproject.gg/vendor/paper-shaders/shaders/liquid-metal.js

js fproject.gg collected 2026-10-02 01:55:12 UTC 22,658 bytes, 627 lines download raw bytes

1/* * * * * * * * * * * * * * * * * * * * * * * * * * * *
2 *                    Paper Shaders                    *
3 *       https://github.com/paper-design/shaders       *
4 * * * * * * * * * * * * * * * * * * * * * * * * * * * */
5
6import { sizingVariablesDeclaration } from "../shader-sizing.js";
7import { declarePI, rotation2, simplexNoise, colorBandingFix } from "../shader-utils.js";
8const liquidMetalFragmentShader = `#version 300 es
9precision mediump float;
10
11uniform sampler2D u_image;
12uniform float u_imageAspectRatio;
13
14uniform vec2 u_resolution;
15uniform float u_time;
16
17uniform vec4 u_colorBack;
18uniform vec4 u_colorTint;
19
20uniform float u_softness;
21uniform float u_repetition;
22uniform float u_shiftRed;
23uniform float u_shiftBlue;
24uniform float u_distortion;
25uniform float u_contour;
26uniform float u_angle;
27
28uniform float u_shape;
29uniform bool u_isImage;
30
31${sizingVariablesDeclaration}
32
33out vec4 fragColor;
34
35${declarePI}
36${rotation2}
37${simplexNoise}
38
39float getColorChanges(float c1, float c2, float stripe_p, vec3 w, float blur, float bump, float tint) {
40
41  float ch = mix(c2, c1, smoothstep(.0, 2. * blur, stripe_p));
42
43  float border = w[0];
44  ch = mix(ch, c2, smoothstep(border, border + 2. * blur, stripe_p));
45
46  if (u_isImage == true) {
47    bump = smoothstep(.2, .8, bump);
48  }
49  border = w[0] + .4 * (1. - bump) * w[1];
50  ch = mix(ch, c1, smoothstep(border, border + 2. * blur, stripe_p));
51
52  border = w[0] + .5 * (1. - bump) * w[1];
53  ch = mix(ch, c2, smoothstep(border, border + 2. * blur, stripe_p));
54
55  border = w[0] + w[1];
56  ch = mix(ch, c1, smoothstep(border, border + 2. * blur, stripe_p));
57
58  float gradient_t = (stripe_p - w[0] - w[1]) / w[2];
59  float gradient = mix(c1, c2, smoothstep(0., 1., gradient_t));
60  ch = mix(ch, gradient, smoothstep(border, border + .5 * blur, stripe_p));
61
62  // Tint color is applied with color burn blending
63  ch = mix(ch, 1. - min(1., (1. - ch) / max(tint, 0.0001)), u_colorTint.a);
64  return ch;
65}
66
67float getImgFrame(vec2 uv, float th) {
68  float frame = 1.;
69  frame *= smoothstep(0., th, uv.y);
70  frame *= 1.0 - smoothstep(1. - th, 1., uv.y);
71  frame *= smoothstep(0., th, uv.x);
72  frame *= 1.0 - smoothstep(1. - th, 1., uv.x);
73  return frame;
74}
75
76float blurEdge3x3(sampler2D tex, vec2 uv, vec2 dudx, vec2 dudy, float radius, float centerSample) {
77  vec2 texel = 1.0 / vec2(textureSize(tex, 0));
78  vec2 r = radius * texel;
79
80  float w1 = 1.0, w2 = 2.0, w4 = 4.0;
81  float norm = 16.0;
82  float sum = w4 * centerSample;
83
84  sum += w2 * textureGrad(tex, uv + vec2(0.0, -r.y), dudx, dudy).r;
85  sum += w2 * textureGrad(tex, uv + vec2(0.0, r.y), dudx, dudy).r;
86  sum += w2 * textureGrad(tex, uv + vec2(-r.x, 0.0), dudx, dudy).r;
87  sum += w2 * textureGrad(tex, uv + vec2(r.x, 0.0), dudx, dudy).r;
88
89  sum += w1 * textureGrad(tex, uv + vec2(-r.x, -r.y), dudx, dudy).r;
90  sum += w1 * textureGrad(tex, uv + vec2(r.x, -r.y), dudx, dudy).r;
91  sum += w1 * textureGrad(tex, uv + vec2(-r.x, r.y), dudx, dudy).r;
92  sum += w1 * textureGrad(tex, uv + vec2(r.x, r.y), dudx, dudy).r;
93
94  return sum / norm;
95}
96
97float lst(float edge0, float edge1, float x) {
98  return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
99}
100
101void main() {
102
103  const float firstFrameOffset = 2.8;
104  float t = .3 * (u_time + firstFrameOffset);
105
106  vec2 uv = v_imageUV;
107  vec2 dudx = dFdx(v_imageUV);
108  vec2 dudy = dFdy(v_imageUV);
109  vec4 img = textureGrad(u_image, uv, dudx, dudy);
110
111  if (u_isImage == false) {
112    uv = v_objectUV + .5;
113    uv.y = 1. - uv.y;
114  }
115
116  float cycleWidth = u_repetition;
117  float edge = 0.;
118  float contOffset = 1.;
119
120  vec2 rotatedUV = uv - vec2(.5);
121  float angle = (-u_angle + 70.) * PI / 180.;
122  float cosA = cos(angle);
123  float sinA = sin(angle);
124  rotatedUV = vec2(
125  rotatedUV.x * cosA - rotatedUV.y * sinA,
126  rotatedUV.x * sinA + rotatedUV.y * cosA
127  ) + vec2(.5);
128
129  if (u_isImage == true) {
130    float edgeRaw = img.r;
131    edge = blurEdge3x3(u_image, uv, dudx, dudy, 6., edgeRaw);
132    edge = pow(edge, 1.6);
133    edge *= mix(0.0, 1.0, smoothstep(0.0, 0.4, u_contour));
134  } else {
135    if (u_shape < 1.) {
136      // full-fill on canvas
137      vec2 borderUV = v_responsiveUV + .5;
138      float ratio = v_responsiveBoxGivenSize.x / v_responsiveBoxGivenSize.y;
139      vec2 mask = min(borderUV, 1. - borderUV);
140      vec2 pixel_thickness = 250. / v_responsiveBoxGivenSize;
141      float maskX = smoothstep(0.0, pixel_thickness.x, mask.x);
142      float maskY = smoothstep(0.0, pixel_thickness.y, mask.y);
143      maskX = pow(maskX, .25);
144      maskY = pow(maskY, .25);
145      edge = clamp(1. - maskX * maskY, 0., 1.);
146
147      uv = v_responsiveUV;
148      if (ratio > 1.) {
149        uv.y /= ratio;
150      } else {
151        uv.x *= ratio;
152      }
153      uv += .5;
154      uv.y = 1. - uv.y;
155
156      cycleWidth *= 2.;
157      contOffset = 1.5;
158
159    } else if (u_shape < 2.) {
160      // circle
161      vec2 shapeUV = uv - .5;
162      shapeUV *= .67;
163      edge = pow(clamp(3. * length(shapeUV), 0., 1.), 18.);
164    } else if (u_shape < 3.) {
165      // daisy
166      vec2 shapeUV = uv - .5;
167      shapeUV *= 1.68;
168
169      float r = length(shapeUV) * 2.;
170      float a = atan(shapeUV.y, shapeUV.x) + .2;
171      r *= (1. + .05 * sin(3. * a + 2. * t));
172      float f = abs(cos(a * 3.));
173      edge = smoothstep(f, f + .7, r);
174      edge *= edge;
175
176      uv *= .8;
177      cycleWidth *= 1.6;
178
179    } else if (u_shape < 4.) {
180      // diamond
181      vec2 shapeUV = uv - .5;
182      shapeUV = rotate(shapeUV, .25 * PI);
183      shapeUV *= 1.42;
184      shapeUV += .5;
185      vec2 mask = min(shapeUV, 1. - shapeUV);
186      vec2 pixel_thickness = vec2(.15);
187      float maskX = smoothstep(0.0, pixel_thickness.x, mask.x);
188      float maskY = smoothstep(0.0, pixel_thickness.y, mask.y);
189      maskX = pow(maskX, .25);
190      maskY = pow(maskY, .25);
191      edge = clamp(1. - maskX * maskY, 0., 1.);
192    } else if (u_shape < 5.) {
193      // metaballs
194      vec2 shapeUV = uv - .5;
195      shapeUV *= 1.3;
196      edge = 0.;
197      for (int i = 0; i < 5; i++) {
198        float fi = float(i);
199        float speed = 1.5 + 2./3. * sin(fi * 12.345);
200        float angle = -fi * 1.5;
201        vec2 dir1 = vec2(cos(angle), sin(angle));
202        vec2 dir2 = vec2(cos(angle + 1.57), sin(angle + 1.));
203        vec2 traj = .4 * (dir1 * sin(t * speed + fi * 1.23) + dir2 * cos(t * (speed * 0.7) + fi * 2.17));
204        float d = length(shapeUV + traj);
205        edge += pow(1.0 - clamp(d, 0.0, 1.0), 4.0);
206      }
207      edge = 1. - smoothstep(.65, .9, edge);
208      edge = pow(edge, 4.);
209    }
210
211    edge = mix(smoothstep(.9 - 2. * fwidth(edge), .9, edge), edge, smoothstep(0.0, 0.4, u_contour));
212
213  }
214
215  float opacity = 0.;
216  if (u_isImage == true) {
217    opacity = img.g;
218    float frame = getImgFrame(v_imageUV, 0.);
219    opacity *= frame;
220  } else {
221    opacity = 1. - smoothstep(.9 - 2. * fwidth(edge), .9, edge);
222    if (u_shape < 2.) {
223      edge = 1.2 * edge;
224    } else if (u_shape < 5.) {
225      edge = 1.8 * pow(edge, 1.5);
226    }
227  }
228
229  float diagBLtoTR = rotatedUV.x - rotatedUV.y;
230  float diagTLtoBR = rotatedUV.x + rotatedUV.y;
231
232  vec3 color = vec3(0.);
233  vec3 color1 = vec3(.98, 0.98, 1.);
234  vec3 color2 = vec3(.1, .1, .1 + .1 * smoothstep(.7, 1.3, diagTLtoBR));
235
236  vec2 grad_uv = uv - .5;
237
238  float dist = length(grad_uv + vec2(0., .2 * diagBLtoTR));
239  grad_uv = rotate(grad_uv, (.25 - .2 * diagBLtoTR) * PI);
240  float direction = grad_uv.x;
241
242  float bump = pow(1.8 * dist, 1.2);
243  bump = 1. - bump;
244  bump *= pow(uv.y, .3);
245
246
247  float thin_strip_1_ratio = .12 / cycleWidth * (1. - .4 * bump);
248  float thin_strip_2_ratio = .07 / cycleWidth * (1. + .4 * bump);
249  float wide_strip_ratio = (1. - thin_strip_1_ratio - thin_strip_2_ratio);
250
251  float thin_strip_1_width = cycleWidth * thin_strip_1_ratio;
252  float thin_strip_2_width = cycleWidth * thin_strip_2_ratio;
253
254  float noise = snoise(uv - t);
255
256  edge += (1. - edge) * u_distortion * noise;
257
258  direction += diagBLtoTR;
259  float contour = 0.;
260  direction -= 2. * noise * diagBLtoTR * (smoothstep(0., 1., edge) * (1.0 - smoothstep(0., 1., edge)));
vendor: 14,723 bytes, lines 260-627
260
261  direction *= mix(1., 1. - edge, smoothstep(.5, 1., u_contour));
262  direction -= 1.7 * edge * smoothstep(.5, 1., u_contour);
263  direction += .2 * pow(u_contour, 4.) * (1.0 - smoothstep(0., 1., edge));
264
265  bump *= clamp(pow(uv.y, .1), .3, 1.);
266  direction *= (.1 + (1.1 - edge) * bump);
267
268  direction *= (.4 + .6 * (1.0 - smoothstep(.5, 1., edge)));
269  direction += .18 * (smoothstep(.1, .2, uv.y) * (1.0 - smoothstep(.2, .4, uv.y)));
270  direction += .03 * (smoothstep(.1, .2, 1. - uv.y) * (1.0 - smoothstep(.2, .4, 1. - uv.y)));
271
272  direction *= (.5 + .5 * pow(uv.y, 2.));
273  direction *= cycleWidth;
274  direction -= t;
275
276
277  float colorDispersion = (1. - bump);
278  colorDispersion = clamp(colorDispersion, 0., 1.);
279  float dispersionRed = colorDispersion;
280  dispersionRed += .03 * bump * noise;
281  dispersionRed += 5. * (smoothstep(-.1, .2, uv.y) * (1.0 - smoothstep(.1, .5, uv.y))) * (smoothstep(.4, .6, bump) * (1.0 - smoothstep(.4, 1., bump)));
282  dispersionRed -= diagBLtoTR;
283
284  float dispersionBlue = colorDispersion;
285  dispersionBlue *= 1.3;
286  dispersionBlue += (smoothstep(0., .4, uv.y) * (1.0 - smoothstep(.1, .8, uv.y))) * (smoothstep(.4, .6, bump) * (1.0 - smoothstep(.4, .8, bump)));
287  dispersionBlue -= .2 * edge;
288
289  dispersionRed *= (u_shiftRed / 20.);
290  dispersionBlue *= (u_shiftBlue / 20.);
291
292  float blur = 0.;
293  float rExtraBlur = 0.;
294  float gExtraBlur = 0.;
295  if (u_isImage == true) {
296    float softness = 0.05 * u_softness;
297    blur = softness + .5 * smoothstep(1., 10., u_repetition) * smoothstep(.0, 1., edge);
298    float smallCanvasT = 1.0 - smoothstep(100., 500., min(u_resolution.x, u_resolution.y));
299    blur += smallCanvasT * smoothstep(.0, 1., edge);
300    rExtraBlur = softness * (0.05 + .1 * (u_shiftRed / 20.) * bump);
301    gExtraBlur = softness * 0.05 / max(0.001, abs(1. - diagBLtoTR));
302  } else {
303    blur = u_softness / 15. + .3 * contour;
304  }
305
306  vec3 w = vec3(thin_strip_1_width, thin_strip_2_width, wide_strip_ratio);
307  w[1] -= .02 * smoothstep(.0, 1., edge + bump);
308  float stripe_r = fract(direction + dispersionRed);
309  float r = getColorChanges(color1.r, color2.r, stripe_r, w, blur + fwidth(stripe_r) + rExtraBlur, bump, u_colorTint.r);
310  float stripe_g = fract(direction);
311  float g = getColorChanges(color1.g, color2.g, stripe_g, w, blur + fwidth(stripe_g) + gExtraBlur, bump, u_colorTint.g);
312  float stripe_b = fract(direction - dispersionBlue);
313  float b = getColorChanges(color1.b, color2.b, stripe_b, w, blur + fwidth(stripe_b), bump, u_colorTint.b);
314
315  color = vec3(r, g, b);
316  color *= opacity;
317
318  vec3 bgColor = u_colorBack.rgb * u_colorBack.a;
319  color = color + bgColor * (1. - opacity);
320  opacity = opacity + u_colorBack.a * (1. - opacity);
321
322  ${colorBandingFix}
323
324  fragColor = vec4(color, opacity);
325}
326`;
327const POISSON_CONFIG_OPTIMIZED = {
328  measurePerformance: false,
329  // Set to true to see performance metrics
330  workingSize: 512,
331  // Size to solve Poisson at (will upscale to original size)
332  iterations: 40
333  // SOR converges ~2-20x faster than standard Gauss-Seidel
334};
335function toProcessedLiquidMetal(file) {
336  const canvas = document.createElement("canvas");
337  const ctx = canvas.getContext("2d");
338  const isBlob = typeof file === "string" && file.startsWith("blob:");
339  return new Promise((resolve, reject) => {
340    if (!file || !ctx) {
341      reject(new Error("Invalid file or canvas context"));
342      return;
343    }
344    const blobContentTypePromise = isBlob && fetch(file).then((res) => res.headers.get("Content-Type"));
345    const img = new Image();
346    img.crossOrigin = "anonymous";
347    const totalStartTime = performance.now();
348    img.onload = async () => {
349      let isSVG;
350      const blobContentType = await blobContentTypePromise;
351      if (blobContentType) {
352        isSVG = blobContentType === "image/svg+xml";
353      } else if (typeof file === "string") {
354        isSVG = file.endsWith(".svg") || file.startsWith("data:image/svg+xml");
355      } else {
356        isSVG = file.type === "image/svg+xml";
357      }
358      let originalWidth = img.width || img.naturalWidth;
359      let originalHeight = img.height || img.naturalHeight;
360      if (isSVG) {
361        const svgMaxSize = 4096;
362        const aspectRatio = originalWidth / originalHeight;
363        if (originalWidth > originalHeight) {
364          originalWidth = svgMaxSize;
365          originalHeight = svgMaxSize / aspectRatio;
366        } else {
367          originalHeight = svgMaxSize;
368          originalWidth = svgMaxSize * aspectRatio;
369        }
370        img.width = originalWidth;
371        img.height = originalHeight;
372      }
373      const minDimension = Math.min(originalWidth, originalHeight);
374      const targetSize = POISSON_CONFIG_OPTIMIZED.workingSize;
375      const scaleFactor = targetSize / minDimension;
376      const width = Math.round(originalWidth * scaleFactor);
377      const height = Math.round(originalHeight * scaleFactor);
378      if (POISSON_CONFIG_OPTIMIZED.measurePerformance) {
379        console.log(`[Processing Mode]`);
380        console.log(`  Original: ${originalWidth}\xD7${originalHeight}`);
381        console.log(`  Working: ${width}\xD7${height} (${(scaleFactor * 100).toFixed(1)}% scale)`);
382        if (scaleFactor < 1) {
383          console.log(`  Speedup: ~${Math.round(1 / (scaleFactor * scaleFactor))}\xD7`);
384        }
385      }
386      canvas.width = originalWidth;
387      canvas.height = originalHeight;
388      const shapeCanvas = document.createElement("canvas");
389      shapeCanvas.width = width;
390      shapeCanvas.height = height;
391      const shapeCtx = shapeCanvas.getContext("2d");
392      shapeCtx.drawImage(img, 0, 0, width, height);
393      const startMask = performance.now();
394      const shapeImageData = shapeCtx.getImageData(0, 0, width, height);
395      const data = shapeImageData.data;
396      const shapeMask = new Uint8Array(width * height);
397      const boundaryMask = new Uint8Array(width * height);
398      let shapePixelCount = 0;
399      for (let i = 0, idx = 0; i < data.length; i += 4, idx++) {
400        const a = data[i + 3];
401        const isShape = a === 0 ? 0 : 1;
402        shapeMask[idx] = isShape;
403        shapePixelCount += isShape;
404      }
405      const boundaryIndices = [];
406      const interiorIndices = [];
407      for (let y = 0; y < height; y++) {
408        for (let x = 0; x < width; x++) {
409          const idx = y * width + x;
410          if (!shapeMask[idx]) continue;
411          let isBoundary = false;
412          if (x === 0 || x === width - 1 || y === 0 || y === height - 1) {
413            isBoundary = true;
414          } else {
415            isBoundary = !shapeMask[idx - 1] || // left
416            !shapeMask[idx + 1] || // right
417            !shapeMask[idx - width] || // top
418            !shapeMask[idx + width] || // bottom
419            !shapeMask[idx - width - 1] || // top-left
420            !shapeMask[idx - width + 1] || // top-right
421            !shapeMask[idx + width - 1] || // bottom-left
422            !shapeMask[idx + width + 1];
423          }
424          if (isBoundary) {
425            boundaryMask[idx] = 1;
426            boundaryIndices.push(idx);
427          } else {
428            interiorIndices.push(idx);
429          }
430        }
431      }
432      if (POISSON_CONFIG_OPTIMIZED.measurePerformance) {
433        console.log(`[Mask Building] Time: ${(performance.now() - startMask).toFixed(2)}ms`);
434        console.log(
435          `  Shape pixels: ${shapePixelCount} / ${width * height} (${(shapePixelCount / (width * height) * 100).toFixed(1)}%)`
436        );
437        console.log(`  Interior pixels: ${interiorIndices.length}`);
438        console.log(`  Boundary pixels: ${boundaryIndices.length}`);
439      }
440      const sparseData = buildSparseData(
441        shapeMask,
442        boundaryMask,
443        new Uint32Array(interiorIndices),
444        new Uint32Array(boundaryIndices),
445        width,
446        height
447      );
448      const startSolve = performance.now();
449      const u = solvePoissonSparse(sparseData, shapeMask, boundaryMask, width, height);
450      if (POISSON_CONFIG_OPTIMIZED.measurePerformance) {
451        console.log(`[Poisson Solve] Time: ${(performance.now() - startSolve).toFixed(2)}ms`);
452      }
453      let maxVal = 0;
454      let finalImageData;
455      for (let i = 0; i < interiorIndices.length; i++) {
456        const idx = interiorIndices[i];
457        if (u[idx] > maxVal) maxVal = u[idx];
458      }
459      const tempCanvas = document.createElement("canvas");
460      tempCanvas.width = width;
461      tempCanvas.height = height;
462      const tempCtx = tempCanvas.getContext("2d");
463      const tempImg = tempCtx.createImageData(width, height);
464      for (let y = 0; y < height; y++) {
465        for (let x = 0; x < width; x++) {
466          const idx = y * width + x;
467          const px = idx * 4;
468          if (!shapeMask[idx]) {
469            tempImg.data[px] = 255;
470            tempImg.data[px + 1] = 255;
471            tempImg.data[px + 2] = 255;
472            tempImg.data[px + 3] = 0;
473          } else {
474            const poissonRatio = u[idx] / maxVal;
475            const gray = 255 * (1 - poissonRatio);
476            tempImg.data[px] = gray;
477            tempImg.data[px + 1] = gray;
478            tempImg.data[px + 2] = gray;
479            tempImg.data[px + 3] = 255;
480          }
481        }
482      }
483      tempCtx.putImageData(tempImg, 0, 0);
484      ctx.imageSmoothingEnabled = true;
485      ctx.imageSmoothingQuality = "high";
486      ctx.drawImage(tempCanvas, 0, 0, width, height, 0, 0, originalWidth, originalHeight);
487      const outImg = ctx.getImageData(0, 0, originalWidth, originalHeight);
488      const originalCanvas = document.createElement("canvas");
489      originalCanvas.width = originalWidth;
490      originalCanvas.height = originalHeight;
491      const originalCtx = originalCanvas.getContext("2d");
492      originalCtx.drawImage(img, 0, 0, originalWidth, originalHeight);
493      const originalData = originalCtx.getImageData(0, 0, originalWidth, originalHeight);
494      for (let i = 0; i < outImg.data.length; i += 4) {
495        const a = originalData.data[i + 3];
496        const upscaledAlpha = outImg.data[i + 3];
497        if (a === 0) {
498          outImg.data[i] = 255;
499          outImg.data[i + 1] = 0;
500        } else {
501          outImg.data[i] = upscaledAlpha === 0 ? 0 : outImg.data[i];
502          outImg.data[i + 1] = a;
503        }
504        outImg.data[i + 2] = 255;
505        outImg.data[i + 3] = 255;
506      }
507      ctx.putImageData(outImg, 0, 0);
508      finalImageData = outImg;
509      canvas.toBlob((blob) => {
510        if (!blob) {
511          reject(new Error("Failed to create PNG blob"));
512          return;
513        }
514        if (POISSON_CONFIG_OPTIMIZED.measurePerformance) {
515          const totalTime = performance.now() - totalStartTime;
516          console.log(`[Total Processing Time] ${totalTime.toFixed(2)}ms`);
517          if (scaleFactor < 1) {
518            const estimatedFullResTime = totalTime * Math.pow(originalWidth * originalHeight / (width * height), 1.5);
519            console.log(`[Estimated time at full resolution] ~${estimatedFullResTime.toFixed(0)}ms`);
520            console.log(
521              `[Time saved] ~${(estimatedFullResTime - totalTime).toFixed(0)}ms (${Math.round(estimatedFullResTime / totalTime)}\xD7 faster)`
522            );
523          }
524        }
525        resolve({
526          imageData: finalImageData,
527          pngBlob: blob
528        });
529      }, "image/png");
530    };
531    img.onerror = () => reject(new Error("Failed to load image"));
532    img.src = typeof file === "string" ? file : URL.createObjectURL(file);
533  });
534}
535function buildSparseData(shapeMask, boundaryMask, interiorPixels, boundaryPixels, width, height) {
536  const pixelCount = interiorPixels.length;
537  const neighborIndices = new Int32Array(pixelCount * 4);
538  for (let i = 0; i < pixelCount; i++) {
539    const idx = interiorPixels[i];
540    const x = idx % width;
541    const y = Math.floor(idx / width);
542    neighborIndices[i * 4 + 0] = x < width - 1 && shapeMask[idx + 1] ? idx + 1 : -1;
543    neighborIndices[i * 4 + 1] = x > 0 && shapeMask[idx - 1] ? idx - 1 : -1;
544    neighborIndices[i * 4 + 2] = y > 0 && shapeMask[idx - width] ? idx - width : -1;
545    neighborIndices[i * 4 + 3] = y < height - 1 && shapeMask[idx + width] ? idx + width : -1;
546  }
547  return {
548    interiorPixels,
549    boundaryPixels,
550    pixelCount,
551    neighborIndices
552  };
553}
554function solvePoissonSparse(sparseData, shapeMask, boundaryMask, width, height) {
555  const ITERATIONS = POISSON_CONFIG_OPTIMIZED.iterations;
556  const C = 0.01;
557  const u = new Float32Array(width * height);
558  const { interiorPixels, neighborIndices, pixelCount } = sparseData;
559  const startTime = performance.now();
560  const omega = 1.9;
561  const redPixels = [];
562  const blackPixels = [];
563  for (let i = 0; i < pixelCount; i++) {
564    const idx = interiorPixels[i];
565    const x = idx % width;
566    const y = Math.floor(idx / width);
567    if ((x + y) % 2 === 0) {
568      redPixels.push(i);
569    } else {
570      blackPixels.push(i);
571    }
572  }
573  for (let iter = 0; iter < ITERATIONS; iter++) {
574    for (const i of redPixels) {
575      const idx = interiorPixels[i];
576      const eastIdx = neighborIndices[i * 4 + 0];
577      const westIdx = neighborIndices[i * 4 + 1];
578      const northIdx = neighborIndices[i * 4 + 2];
579      const southIdx = neighborIndices[i * 4 + 3];
580      let sumN = 0;
581      if (eastIdx >= 0) sumN += u[eastIdx];
582      if (westIdx >= 0) sumN += u[westIdx];
583      if (northIdx >= 0) sumN += u[northIdx];
584      if (southIdx >= 0) sumN += u[southIdx];
585      const newValue = (C + sumN) / 4;
586      u[idx] = omega * newValue + (1 - omega) * u[idx];
587    }
588    for (const i of blackPixels) {
589      const idx = interiorPixels[i];
590      const eastIdx = neighborIndices[i * 4 + 0];
591      const westIdx = neighborIndices[i * 4 + 1];
592      const northIdx = neighborIndices[i * 4 + 2];
593      const southIdx = neighborIndices[i * 4 + 3];
594      let sumN = 0;
595      if (eastIdx >= 0) sumN += u[eastIdx];
596      if (westIdx >= 0) sumN += u[westIdx];
597      if (northIdx >= 0) sumN += u[northIdx];
598      if (southIdx >= 0) sumN += u[southIdx];
599      const newValue = (C + sumN) / 4;
600      u[idx] = omega * newValue + (1 - omega) * u[idx];
601    }
602  }
603  if (POISSON_CONFIG_OPTIMIZED.measurePerformance) {
604    const elapsed = performance.now() - startTime;
605    console.log(`[Optimized Poisson Solver (SOR \u03C9=${omega})]`);
606    console.log(`  Working size: ${width}\xD7${height}`);
607    console.log(`  Iterations: ${ITERATIONS}`);
608    console.log(`  Time: ${elapsed.toFixed(2)}ms`);
609    console.log(`  Interior pixels processed: ${pixelCount}`);
610    console.log(`  Speed: ${(ITERATIONS * pixelCount / (elapsed * 1e3)).toFixed(2)} Mpixels/sec`);
611  }
612  return u;
613}
614const LiquidMetalShapes = {
615  none: 0,
616  circle: 1,
617  daisy: 2,
618  diamond: 3,
619  metaballs: 4
620};
621export {
622  LiquidMetalShapes,
623  POISSON_CONFIG_OPTIMIZED,
624  liquidMetalFragmentShader,
625  toProcessedLiquidMetal
626};
627//# sourceMappingURL=liquid-metal.js.map

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.