blob: 83a307ec842d592b6b224c8d55e8d58b5d3d7f44 [file] [edit]
// Copyright (C) 2026 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Signed Distance Field (SDF) generation for closed polygons.
// SDFs enable resolution-independent rendering of shapes with smooth anti-aliasing.
import type {Point2D} from './../geom';
// Signed distance from point to line segment
function sdSegment(p: Point2D, a: Point2D, b: Point2D): number {
const pax = p.x - a.x;
const pay = p.y - a.y;
const bax = b.x - a.x;
const bay = b.y - a.y;
const h = Math.max(
0,
Math.min(1, (pax * bax + pay * bay) / (bax * bax + bay * bay)),
);
const dx = pax - bax * h;
const dy = pay - bay * h;
return Math.sqrt(dx * dx + dy * dy);
}
// Determine if point is inside a closed polygon using ray casting
function isInsidePolygon(p: Point2D, vertices: readonly Point2D[]): boolean {
let inside = false;
const n = vertices.length;
for (let i = 0, j = n - 1; i < n; j = i++) {
const vi = vertices[i];
const vj = vertices[j];
// Ray casting: count edge crossings to the right of the point
if (
vi.y > p.y !== vj.y > p.y &&
p.x < ((vj.x - vi.x) * (p.y - vi.y)) / (vj.y - vi.y) + vi.x
) {
inside = !inside;
}
}
return inside;
}
// Signed distance from point to closed polygon boundary
// Negative inside, positive outside
function sdPolygon(p: Point2D, vertices: readonly Point2D[]): number {
const n = vertices.length;
if (n < 3) return Infinity;
// Find minimum distance to any edge
let minDist = Infinity;
for (let i = 0; i < n; i++) {
const a = vertices[i];
const b = vertices[(i + 1) % n];
minDist = Math.min(minDist, sdSegment(p, a, b));
}
// Determine sign based on inside/outside
const inside = isInsidePolygon(p, vertices);
return inside ? -minDist : minDist;
}
/**
* Generate a signed distance field for a closed polygon.
*
* @param vertices - Array of vertices defining the polygon in normalized 0-1 coordinates
* @param size - Size of the output texture (size x size pixels)
* @param spread - How much distance (in normalized coords) maps to the 0-1 range.
* Larger values = more gradual falloff, smaller = sharper edges.
* Default 0.1 works well for most shapes.
* @returns Uint8Array of RGBA data (size * size * 4 bytes) where alpha channel
* contains the SDF: 0.5 = edge, <0.5 = inside, >0.5 = outside
*/
export function generatePolygonSDF(
vertices: readonly Point2D[],
size: number,
spread: number = 0.1,
): Uint8Array {
const data = new Uint8Array(size * size * 4);
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
// Map pixel to normalized coordinates (0-1), sampling at pixel centers
const p: Point2D = {
x: (x + 0.5) / size,
y: (y + 0.5) / size,
};
// Get signed distance (negative inside, positive outside)
const dist = sdPolygon(p, vertices);
// Normalize to 0-1 range: 0.5 = edge, <0.5 = inside, >0.5 = outside
const normalized = Math.max(0, Math.min(1, dist / spread + 0.5));
const idx = (y * size + x) * 4;
data[idx + 0] = 255; // R
data[idx + 1] = 255; // G
data[idx + 2] = 255; // B
data[idx + 3] = Math.round(normalized * 255); // A = SDF value
}
}
return data;
}
/**
* Create a WebGL texture from SDF data.
*
* @param gl - WebGL2 rendering context
* @param sdfData - RGBA data from generatePolygonSDF
* @param size - Size of the texture (must match the size used in generatePolygonSDF)
* @returns WebGL texture configured for SDF rendering
*/
export function createSDFTexture(
gl: WebGL2RenderingContext,
sdfData: Uint8Array,
size: number,
): WebGLTexture {
const texture = gl.createTexture()!;
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
size,
size,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
sdfData,
);
// Linear filtering is essential for SDF - it interpolates distance values smoothly
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
return texture;
}