Files
mikael-lovqvists-claude-agent 9600a2bc2a Add Goldberg Polyhedron Paint experiment
Interactive WebGL Goldberg polyhedron viewer and painter with PBR
shading, adjustable environment lighting, paint tools (pen, brush,
circle, fill, line, pick), undo/redo, colour palettes, and mesh
relaxation. Added to the standalone experiments index.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:00:19 +00:00

99 lines
3.0 KiB
JavaScript

// Pure 3D geometry utilities — no DOM, no topology, only math.
export const GOLDEN_RATIO = (1 + Math.sqrt(5)) / 2;
export class Vec3 {
constructor(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
}
add(v) { return new Vec3(this.x + v.x, this.y + v.y, this.z + v.z); }
sub(v) { return new Vec3(this.x - v.x, this.y - v.y, this.z - v.z); }
scale(s) { return new Vec3(this.x * s, this.y * s, this.z * s); }
dot(v) { return this.x * v.x + this.y * v.y + this.z * v.z; }
cross(v) {
return new Vec3(
this.y * v.z - this.z * v.y,
this.z * v.x - this.x * v.z,
this.x * v.y - this.y * v.x,
);
}
length() { return Math.sqrt(this.dot(this)); }
normalize() { return this.scale(1 / this.length()); }
lerp(v, t) { return this.scale(1 - t).add(v.scale(t)); }
to_array() { return [this.x, this.y, this.z]; }
toString() { return `Vec3(${this.x.toFixed(4)}, ${this.y.toFixed(4)}, ${this.z.toFixed(4)})`; }
}
// Project a Vec3 onto the unit sphere.
export function normalize_to_sphere(v) {
return v.normalize();
}
// Midpoint between two Vec3 (not yet normalized).
export function midpoint(a, b) {
return a.add(b).scale(0.5);
}
// Returns n [x, y] pairs for a regular n-gon centered at origin with given circumradius.
// Vertices start at angle `start_angle` (default: top, -π/2).
export function regular_polygon_2d(n, circumradius, start_angle = -Math.PI / 2) {
const pts = [];
for (let i = 0; i < n; i++) {
const angle = start_angle + (2 * Math.PI * i) / n;
pts.push([circumradius * Math.cos(angle), circumradius * Math.sin(angle)]);
}
return pts;
}
// Centroid of an array of [x, y] points.
export function centroid_2d(pts) {
let sx = 0, sy = 0;
for (const [x, y] of pts) { sx += x; sy += y; }
return [sx / pts.length, sy / pts.length];
}
// Apply a 2D similarity transform (rotation + uniform scale + translation)
// represented as { a, b, tx, ty } where the mapping is:
// [x', y'] = [a*x - b*y + tx, b*x + a*y + ty]
// (a + bi is the complex multiplier, tx+ty is the translation)
export function apply_transform_2d(transform, pt) {
const { a, b, tx, ty } = transform;
return [
a * pt[0] - b * pt[1] + tx,
b * pt[0] + a * pt[1] + ty,
];
}
// Compute the similarity transform that maps (src0 -> dst0, src1 -> dst1).
// Returns { a, b, tx, ty }.
export function similarity_transform_2d(src0, src1, dst0, dst1) {
// Treat points as complex numbers.
// We want w = c * z + d where c = a+bi, d = tx+ty*i
// c = (dst1 - dst0) / (src1 - src0) [complex division]
// d = dst0 - c * src0
const [sx0, sy0] = src0;
const [sx1, sy1] = src1;
const [dx0, dy0] = dst0;
const [dx1, dy1] = dst1;
// src_vec = src1 - src0
const svx = sx1 - sx0, svy = sy1 - sy0;
// dst_vec = dst1 - dst0
const dvx = dx1 - dx0, dvy = dy1 - dy0;
// c = dst_vec / src_vec (complex division)
const denom = svx * svx + svy * svy;
const a = (dvx * svx + dvy * svy) / denom;
const b = (dvy * svx - dvx * svy) / denom;
// d = dst0 - c * src0
const tx = dx0 - (a * sx0 - b * sy0);
const ty = dy0 - (b * sx0 + a * sy0);
return { a, b, tx, ty };
}