scripts/mascot-bodies/sdf.ts
Rama main · réplica de lectura del repositorio original.
/**
* Signed distance field for a silhouette, so each "how far from the edge?" is an
* array lookup. Exact (Felzenszwalb & Huttenlocher's squared EDT, linear time per
* axis): a wrong distance shows up as a mouth hanging off the edge.
*/
import { FACE_BOX } from "../../src/components/cursor-face-data.ts"
export interface Sdf {
size: number
/** Signed distance in face-space units. Positive inside the silhouette. */
data: Float32Array
/** Face-space units per pixel. */
unitsPerPixel: number
}
/** Builds the signed field: distance to the nearest outside pixel, minus the reverse. */
export function fieldFromMask(mask: Uint8Array, size: number): Sdf {
const inside = edt(mask, size, 1)
const outside = edt(mask, size, 0)
const unitsPerPixel = FACE_BOX / size
const data = new Float32Array(size * size)
for (let i = 0; i < data.length; i++) {
data[i] = (mask[i] ? Math.sqrt(inside[i]) : -Math.sqrt(outside[i])) * unitsPerPixel
}
return { size, data, unitsPerPixel }
}
/**
* Exact squared Euclidean distance transform. Distance from every pixel to the nearest
* pixel whose mask value differs from `target`.
*/
function edt(mask: Uint8Array, size: number, target: number): Float64Array {
const INF = 1e20
const grid = new Float64Array(size * size)
for (let i = 0; i < grid.length; i++) grid[i] = mask[i] === target ? INF : 0
const f = new Float64Array(size)
const d = new Float64Array(size)
const v = new Int32Array(size)
const z = new Float64Array(size + 1)
// Columns, then rows — the 2D transform separates into two 1D passes.
for (let x = 0; x < size; x++) {
for (let y = 0; y < size; y++) f[y] = grid[y * size + x]
edt1d(f, d, v, z, size)
for (let y = 0; y < size; y++) grid[y * size + x] = d[y]
}
for (let y = 0; y < size; y++) {
const row = y * size
for (let x = 0; x < size; x++) f[x] = grid[row + x]
edt1d(f, d, v, z, size)
for (let x = 0; x < size; x++) grid[row + x] = d[x]
}
return grid
}
/** Lower envelope of parabolas — the 1D case the 2D transform is built from. */
function edt1d(f: Float64Array, d: Float64Array, v: Int32Array, z: Float64Array, n: number) {
let k = 0
v[0] = 0
z[0] = -1e20
z[1] = 1e20
for (let q = 1; q < n; q++) {
let s = (f[q] + q * q - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k])
while (s <= z[k]) {
k--
s = (f[q] + q * q - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k])
}
k++
v[k] = q
z[k] = s
z[k + 1] = 1e20
}
k = 0
for (let q = 0; q < n; q++) {
while (z[k + 1] < q) k++
const dist = q - v[k]
d[q] = dist * dist + f[v[k]]
}
}
/** Signed distance at a face-space point. Outside the grid reads as far outside. */
export function sampleSdf(sdf: Sdf, x: number, y: number): number {
const px = Math.round(x / sdf.unitsPerPixel)
const py = Math.round(y / sdf.unitsPerPixel)
if (px < 0 || py < 0 || px >= sdf.size || py >= sdf.size) return -999
return sdf.data[py * sdf.size + px]
}
/** The largest circle that fits inside the silhouette — where a face most wants to sit. */
export function largestInscribedCircle(sdf: Sdf): { x: number; y: number; radius: number } {
let best = -Infinity
let bi = 0
for (let i = 0; i < sdf.data.length; i++) {
if (sdf.data[i] > best) {
best = sdf.data[i]
bi = i
}
}
const py = Math.floor(bi / sdf.size)
const px = bi - py * sdf.size
return {
x: (px + 0.5) * sdf.unitsPerPixel,
y: (py + 0.5) * sdf.unitsPerPixel,
radius: Math.max(best, 0),
}
}