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

118 lines
4.1 KiB
JavaScript

import { PAINT_BG, PAINT_BG_PBR, PAINT_BG_NOISE } from './paint_state.mjs';
export function desaturate(c, s) {
const lum = 0.299*c[0] + 0.587*c[1] + 0.114*c[2];
return [lum + (c[0]-lum)*s, lum + (c[1]-lum)*s, lum + (c[2]-lum)*s];
}
export function distortion_color(ratio) {
const t = Math.min(Math.max((ratio - 1) / 0.4, 0), 1);
return desaturate([Math.min(1, 2*t), t < 0.5 ? t*1.4 : 0.7-(t-0.5)*1.4, Math.max(0, 1-2*t)], 0.45);
}
export function shape_distortion(verts) {
const n = verts.length;
let cx=0, cy=0, cz=0;
for (const v of verts) { cx+=v.x; cy+=v.y; cz+=v.z; }
cx/=n; cy/=n; cz/=n;
let mn=Infinity, mx=0;
for (const v of verts) {
const d = Math.sqrt((v.x-cx)**2+(v.y-cy)**2+(v.z-cz)**2);
if (d < mn) { mn=d; } if (d > mx) { mx=d; }
}
return mn > 1e-12 ? mx/mn : 1;
}
// Triangle mesh (icosahedron / subdivided).
export function build_triangle_geo(poly, palette) {
const fpos=[], fcol=[], fpbr=[], fnoise=[], epos=[];
let worst_shape = 0;
for (const face of poly.faces) {
const v = face.vertices.map(vi => poly.vertices[vi].to_vec3());
const e1x=v[1].x-v[0].x, e1y=v[1].y-v[0].y, e1z=v[1].z-v[0].z;
const e2x=v[2].x-v[0].x, e2y=v[2].y-v[0].y, e2z=v[2].z-v[0].z;
const nx=e1y*e2z-e1z*e2y, ny=e1z*e2x-e1x*e2z, nz=e1x*e2y-e1y*e2x;
const cx=(v[0].x+v[1].x+v[2].x)/3, cy=(v[0].y+v[1].y+v[2].y)/3, cz=(v[0].z+v[1].z+v[2].z)/3;
if (nx*cx + ny*cy + nz*cz < 0) { [v[1], v[2]] = [v[2], v[1]]; }
let mn=Infinity, mx=0;
for (let i=0; i<3; i++) {
const a=v[i], b=v[(i+1)%3];
const l=Math.sqrt((b.x-a.x)**2+(b.y-a.y)**2+(b.z-a.z)**2);
if (l<mn) { mn=l; } if (l>mx) { mx=l; }
}
const ratio = mn>1e-12 ? mx/mn : 1;
if (ratio > worst_shape) { worst_shape = ratio; }
const c = palette.face_color(ratio, false);
for (const vert of v) {
fpos.push(vert.x,vert.y,vert.z);
fcol.push(c[0],c[1],c[2]);
fpbr.push(0.0,0.6);
fnoise.push(...PAINT_BG_NOISE);
}
for (let i=0; i<3; i++) {
const a=v[i], b=v[(i+1)%3];
epos.push(a.x,a.y,a.z, b.x,b.y,b.z);
}
}
return {
face_pos: new Float32Array(fpos), face_col: new Float32Array(fcol),
face_pbr: new Float32Array(fpbr), face_noise: new Float32Array(fnoise),
face_verts: fpos.length/3,
edge_pos: new Float32Array(epos), edge_verts: epos.length/3,
stats: { faces: poly.faces.length, vertices: poly.vertices.length, worst_shape },
};
}
// Goldberg polygonal mesh. paint is a Paint_State (or null for non-paint mode).
export function build_goldberg_geo(goldberg, use_relaxed, paint, palette) {
const fpos=[], fcol=[], fpbr=[], fnoise=[], epos=[];
let worst_shape = 0;
for (let fi=0; fi<goldberg.faces.length; fi++) {
const face = goldberg.faces[fi];
const verts = use_relaxed
? (face.relaxed_vertices_3d ?? face.vertices_3d)
: face.vertices_3d;
const n = verts.length;
const sr = shape_distortion(verts);
if (sr > worst_shape) { worst_shape = sr; }
let c, pbr, noise;
if (paint?.enabled) {
const in_preview = paint.preview?.faces.has(fi);
c = in_preview ? paint.preview.color : (paint.face_colors.get(fi) ?? PAINT_BG);
pbr = in_preview ? (paint.preview.pbr ?? PAINT_BG_PBR) : (paint.face_pbr.get(fi) ?? PAINT_BG_PBR);
noise = in_preview ? (paint.preview.noise ?? PAINT_BG_NOISE) : (paint.face_noise.get(fi) ?? PAINT_BG_NOISE);
} else {
c = palette.face_color(sr, face.is_pentagon);
pbr = [0.0, 0.6];
noise = PAINT_BG_NOISE;
}
for (let i=1; i<n-1; i++) {
for (const v of [verts[0], verts[i], verts[i+1]]) {
fpos.push(v.x,v.y,v.z);
fcol.push(c[0],c[1],c[2]);
fpbr.push(pbr[0],pbr[1]);
fnoise.push(noise[0],noise[1],noise[2],noise[3]);
}
}
for (let i=0; i<n; i++) {
const a=verts[i], b=verts[(i+1)%n];
epos.push(a.x,a.y,a.z, b.x,b.y,b.z);
}
}
return {
face_pos: new Float32Array(fpos), face_col: new Float32Array(fcol),
face_pbr: new Float32Array(fpbr), face_noise: new Float32Array(fnoise),
face_verts: fpos.length/3,
edge_pos: new Float32Array(epos), edge_verts: epos.length/3,
stats: {
faces: goldberg.faces.length,
pentagons: goldberg.faces.filter(f=>f.is_pentagon).length,
worst_shape,
},
};
}