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>
118 lines
4.1 KiB
JavaScript
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,
|
|
},
|
|
};
|
|
}
|