textures
this page is in a subdirectory to test nested content.
overview
texture mapping is a method for defining high frequency detail on surfaces.
barycentric coordinates
barycentric coordinates express a point's position relative to triangle vertices. each coordinate (u, v, w) represents the "weight" of each vertex, where u + v + w = 1. this is fundamental for texture mapping - interpolating UV coordinates across a triangle.
move your mouse over the triangle to see the coordinates. the RGB color at each point is (u, v, w) mapped to (red, green, blue).
signed distance fields (8SSEDT)
a signed distance field (SDF) stores the distance from each pixel to the nearest boundary. the sign indicates inside (negative) vs outside (positive). SDFs are useful for anti-aliased rendering, collision detection, and text rendering.
this demo uses 8SSEDT (8-point Signed Sequential Euclidean Distance Transform) - a two-pass algorithm that propagates distance vectors using 8 neighboring pixels.
- inside triangle: blue (boundary) → yellow (deep inside)
- outside triangle: black (boundary) → white (far away)
drag the vertices to reshape the triangle. hover to see the exact signed distance in pixels.
low-resolution SDF
the same algorithm at 1/16th resolution (32×32 grid on a 512×512 canvas). this shows the actual SDF cells as visible blocks, making the distance propagation easier to visualize.
Felzenszwalb & Huttenlocher
an alternative SDF algorithm from their 2012 paper "Distance Transforms of Sampled Functions". instead of propagating distance vectors through neighbors, this computes exact squared euclidean distance using a clever 1D algorithm based on lower envelopes of parabolas.
the key insight: the squared distance function along any row/column is the lower envelope of parabolas centered at each feature point. the algorithm computes this envelope in O(n) time per row, then applies it separably (rows then columns).
both algorithms produce identical results for this binary input. F&H is often faster for large images since it's separable and has better cache locality.
algorithm comparison
this visualization shows the delta (absolute difference) between 8SSEDT and F&H. dark green = no difference, yellow/red = difference exists.
hover over cells to see the exact values from each algorithm. for simple convex shapes like triangles, both algorithms typically produce identical results.
6-point polygon comparison
the same comparison but with a 6-point polygon. drag any vertex to create concave or self-intersecting shapes and observe where differences appear.
textured cube (WebGL)
a 3D cube rendered using WebGL with texture mapping. the curry image is mapped to each face of the cube using UV coordinates.
click to pause/resume rotation.
pixel drawer
a simple 32×32 pixel art editor. each cell has an embossed look. click to toggle pixels between black and white, drag to paint multiple cells.
source code:
// Pixel Drawer - 32x32 grid editor
// click to toggle cells, drag to paint
export function run(canvas) {
const ctx = canvas.getContext('2d');
const gridSize = 32;
const cellSize = Math.floor(canvas.width / gridSize);
// pixel data (false = black, true = white)
const pixels = new Array(gridSize * gridSize).fill(false);
// drawing state
let isDrawing = false;
let drawColor = true; // color to paint while dragging
let lastCell = null; // track last cell for line drawing
// emboss colors
const lightEdge = 'rgba(255, 255, 255, 0.3)';
const darkEdge = 'rgba(0, 0, 0, 0.5)';
const blackCell = '#1a1a1a';
const whiteCell = '#e0e0e0';
function drawCell(x, y) {
const px = x * cellSize;
const py = y * cellSize;
const idx = y * gridSize + x;
const isWhite = pixels[idx];
// fill base color
ctx.fillStyle = isWhite ? whiteCell : blackCell;
ctx.fillRect(px, py, cellSize, cellSize);
// emboss effect: light edge top-left, dark edge bottom-right
const inset = 1;
// top edge (light)
ctx.fillStyle = isWhite ? lightEdge : 'rgba(80, 80, 80, 0.5)';
ctx.fillRect(px, py, cellSize, inset);
// left edge (light)
ctx.fillRect(px, py, inset, cellSize);
// bottom edge (dark)
ctx.fillStyle = isWhite ? darkEdge : 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(px, py + cellSize - inset, cellSize, inset);
// right edge (dark)
ctx.fillRect(px + cellSize - inset, py, inset, cellSize);
}
function render() {
ctx.fillStyle = '#2a2a2a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (let y = 0; y < gridSize; y++) {
for (let x = 0; x < gridSize; x++) {
drawCell(x, y);
}
}
}
function getCellCoords(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const touch = e.touches ? e.touches[0] : e;
const mx = (touch.clientX - rect.left) * scaleX;
const my = (touch.clientY - rect.top) * scaleY;
const x = Math.floor(mx / cellSize);
const y = Math.floor(my / cellSize);
if (x >= 0 && x < gridSize && y >= 0 && y < gridSize) {
return { x, y };
}
return null;
}
function setPixel(x, y, value) {
const idx = y * gridSize + x;
if (pixels[idx] !== value) {
pixels[idx] = value;
drawCell(x, y);
}
}
// Bresenham's line algorithm - draw line between two cells
function drawLine(x0, y0, x1, y1, value) {
const dx = Math.abs(x1 - x0);
const dy = Math.abs(y1 - y0);
const sx = x0 < x1 ? 1 : -1;
const sy = y0 < y1 ? 1 : -1;
let err = dx - dy;
while (true) {
setPixel(x0, y0, value);
if (x0 === x1 && y0 === y1) break;
const e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
}
}
// initial render
render();
// mouse events
canvas.addEventListener('mousedown', function(e) {
e.preventDefault();
const cell = getCellCoords(e);
if (cell) {
isDrawing = true;
// toggle the first cell and use that as the draw color
const idx = cell.y * gridSize + cell.x;
drawColor = !pixels[idx];
setPixel(cell.x, cell.y, drawColor);
lastCell = cell;
}
});
canvas.addEventListener('mousemove', function(e) {
if (!isDrawing) return;
const cell = getCellCoords(e);
if (cell) {
if (lastCell) {
drawLine(lastCell.x, lastCell.y, cell.x, cell.y, drawColor);
} else {
setPixel(cell.x, cell.y, drawColor);
}
lastCell = cell;
}
});
canvas.addEventListener('mouseup', function() {
isDrawing = false;
lastCell = null;
});
canvas.addEventListener('mouseleave', function() {
isDrawing = false;
lastCell = null;
});
// touch events
canvas.addEventListener('touchstart', function(e) {
e.preventDefault();
const cell = getCellCoords(e);
if (cell) {
isDrawing = true;
const idx = cell.y * gridSize + cell.x;
drawColor = !pixels[idx];
setPixel(cell.x, cell.y, drawColor);
lastCell = cell;
}
});
canvas.addEventListener('touchmove', function(e) {
e.preventDefault();
if (!isDrawing) return;
const cell = getCellCoords(e);
if (cell) {
if (lastCell) {
drawLine(lastCell.x, lastCell.y, cell.x, cell.y, drawColor);
} else {
setPixel(cell.x, cell.y, drawColor);
}
lastCell = cell;
}
});
canvas.addEventListener('touchend', function() {
isDrawing = false;
lastCell = null;
});
}
types
- diffuse maps
- normal maps
- specular maps
- displacement maps