PageSourceSearch

https://static.expo.dev/static/workers/liquid-metal-texture.js

js expo.dev collected 2026-09-24 08:26:51 UTC 4,667 bytes, 165 lines download raw bytes

1const TEXTURE_SIZE = 512;
2const POISSON_ITERATIONS = 64;
3
4/**
5 * @param {Float32Array} alpha
6 * @param {number} size
7 */
8function buildContourField(alpha, size) {
9  const pixelCount = size * size;
10  const shapeMask = new Uint8Array(pixelCount);
11  const interiorPixels = [];
12  const redPixels = [];
13  const blackPixels = [];
14
15  for (let i = 0; i < pixelCount; i++) {
16    shapeMask[i] = alpha[i] > 0 ? 1 : 0;
17  }
18
19  for (let y = 0; y < size; y++) {
20    for (let x = 0; x < size; x++) {
21      const idx = y * size + x;
22      if (!shapeMask[idx]) {
23        continue;
24      }
25
26      let isBoundary = x === 0 || x === size - 1 || y === 0 || y === size - 1;
27      for (let oy = -1; oy <= 1 && !isBoundary; oy++) {
28        for (let ox = -1; ox <= 1; ox++) {
29          if (ox === 0 && oy === 0) {
30            continue;
31          }
32          if (!shapeMask[(y + oy) * size + x + ox]) {
33            isBoundary = true;
34            break;
35          }
36        }
37      }
38
39      if (!isBoundary) {
40        interiorPixels.push(idx);
41        if ((x + y) % 2 === 0) {
42          redPixels.push(idx);
43        } else {
44          blackPixels.push(idx);
45        }
46      }
47    }
48  }
49
50  const values = new Float32Array(pixelCount);
51  const omega = 1.9;
52  const passes = [redPixels, blackPixels];
53
54  for (let iter = 0; iter < POISSON_ITERATIONS; iter++) {
55    for (const pixels of passes) {
56      for (const idx of pixels) {
57        const east = idx + 1;
58        const west = idx - 1;
59        const north = idx - size;
60        const south = idx + size;
61        const sum =
62          (shapeMask[east] ? values[east] : 0) +
63          (shapeMask[west] ? values[west] : 0) +
64          (shapeMask[north] ? values[north] : 0) +
65          (shapeMask[south] ? values[south] : 0);
66        const next = (0.01 + sum) / 4;
67        values[idx] = omega * next + (1 - omega) * values[idx];
68      }
69    }
70  }
71
72  const smoothed = new Float32Array(pixelCount);
73  for (let pass = 0; pass < 2; pass++) {
74    smoothed.fill(0);
75    for (const idx of interiorPixels) {
76      let sum = values[idx] * 4;
77      let weight = 4;
78
79      for (let oy = -1; oy <= 1; oy++) {
80        for (let ox = -1; ox <= 1; ox++) {
81          if (ox === 0 && oy === 0) {
82            continue;
83          }
84
85          const neighbor = idx + oy * size + ox;
86          if (!shapeMask[neighbor]) {
87            continue;
88          }
89
90          const neighborWeight = ox === 0 || oy === 0 ? 2 : 1;
91          sum += values[neighbor] * neighborWeight;
92          weight += neighborWeight;
93        }
94      }
95
96      smoothed[idx] = sum / weight;
97    }
98    values.set(smoothed);
99  }
100
101  let maxValue = 0;
102  for (const idx of interiorPixels) {
103    maxValue = Math.max(maxValue, values[idx]);
104  }
105  maxValue = maxValue || 1;
106
107  const contour = new Uint8Array(pixelCount);
108  for (let i = 0; i < pixelCount; i++) {
109    contour[i] = shapeMask[i] ? Math.round(255 * (1 - values[i] / maxValue)) : 255;
110  }
111
112  return contour;
113}
114
115/**
116 * @param {string} path
117 * @param {number} viewBoxSize
118 */
119function getShapeTextureData(path, viewBoxSize) {
120  const canvas = new OffscreenCanvas(TEXTURE_SIZE, TEXTURE_SIZE);
121  const ctx = canvas.getContext('2d', { willReadFrequently: true });
122  if (!ctx) {
123    return null;
124  }
125
126  const scale = TEXTURE_SIZE / viewBoxSize;
127  const shapePath = new Path2D(path);
128
129  // G channel: sharp opacity mask.
130  ctx.fillStyle = 'white';
131  ctx.save();
132  ctx.scale(scale, scale);
133  ctx.fill(shapePath);
134  ctx.restore();
135  const sharp = ctx.getImageData(0, 0, TEXTURE_SIZE, TEXTURE_SIZE).data;
136  const sharpAlpha = new Float32Array(TEXTURE_SIZE * TEXTURE_SIZE);
137  for (let i = 0; i < TEXTURE_SIZE * TEXTURE_SIZE; i++) {
138    sharpAlpha[i] = sharp[i * 4 + 3];
139  }
140
141  const contour = buildContourField(sharpAlpha, TEXTURE_SIZE);
142
143  // Rows are written bottom-up, replacing the UNPACK_FLIP_Y_WEBGL upload flag
144  // that is deprecated for typed-array uploads.
145  const data = new Uint8Array(TEXTURE_SIZE * TEXTURE_SIZE * 4);
146  for (let y = 0; y < TEXTURE_SIZE; y++) {
147    const sourceRow = TEXTURE_SIZE - 1 - y;
148    for (let x = 0; x < TEXTURE_SIZE; x++) {
149      const sourceIndex = sourceRow * TEXTURE_SIZE + x;
150      const destinationIndex = (y * TEXTURE_SIZE + x) * 4;
151      data[destinationIndex + 0] = contour[sourceIndex];
152      data[destinationIndex + 1] = sharp[sourceIndex * 4 + 3];
153      data[destinationIndex + 2] = 0;
154      data[destinationIndex + 3] = 255;
155    }
156  }
157
158  return data;
159}
160
161/** @param {MessageEvent<{ path: string;
161 viewBoxSize: number }>} event */
162self.onmessage = (event) => {
163  const data = getShapeTextureData(event.data.path, event.data.viewBoxSize);
164  self.postMessage(data, { transfer: data ? [data.buffer] : [] });
165};

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.