Merge pull request 'Strip pipeline/palette/depth/relax UI; lock to paint mode' (#4) from mikael-lovqvists-claude-agent/websperiments:defaults-and-draw-mode into main

Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
2026-05-10 19:26:04 +00:00
2 changed files with 10 additions and 137 deletions

View File

@@ -90,48 +90,6 @@
<h3>Sphere Playground</h3> <h3>Sphere Playground</h3>
</div> </div>
<div>
<h3>Pipeline stage</h3>
<div class="stage-grid" style="margin-top:6px">
<button class="sb" data-stage="ico">Icosahedron</button>
<button class="sb" data-stage="subdiv">Subdivided</button>
<button class="sb" data-stage="goldberg">Goldberg</button>
<button class="sb active" data-stage="relaxed">Relaxed</button>
</div>
</div>
<div>
<h3>Palette</h3>
<div id="palette-grid" class="stage-grid" style="margin-top:6px">
<!-- populated by JS -->
</div>
</div>
<div>
<h3>Mesh</h3>
<div class="param" style="margin-top:6px">
<label>Depth</label>
<input type="range" id="depth" min="1" max="5" value="5">
<span class="val" id="depth-val">5</span>
</div>
</div>
<div>
<h3>Relaxation</h3>
<div class="param" style="margin-top:6px">
<label>Iterations</label>
<input type="number" id="iters" value="500" min="0" step="50">
</div>
<div class="param">
<label>α edge</label>
<input type="number" id="alpha-edge" value="0" min="0" step="0.01">
</div>
<div class="param">
<label>α centroid</label>
<input type="number" id="alpha-centroid" value="0.04" min="0" step="0.001">
</div>
</div>
<div> <div>
<h3>Paint</h3> <h3>Paint</h3>
<div style="display:flex; gap:4px; margin-top:6px"> <div style="display:flex; gap:4px; margin-top:6px">
@@ -252,7 +210,6 @@
</div> </div>
<button id="spin-btn" style="background:#0a1a2a; border:1px solid #2a4a6a; color:#7ab; padding:7px; cursor:pointer; font-family:monospace; font-size:13px; width:100%">↻ Auto-spin</button> <button id="spin-btn" style="background:#0a1a2a; border:1px solid #2a4a6a; color:#7ab; padding:7px; cursor:pointer; font-family:monospace; font-size:13px; width:100%">↻ Auto-spin</button>
<button id="build-btn">▶ Build</button>
<div> <div>
<div id="status">Ready</div> <div id="status">Ready</div>

View File

@@ -5,11 +5,11 @@ import { mat4_mul, mat4_perspective, mat4_translate_z, quat_identity, quat_from_
import { create_program, make_buffer } from './gl_utils.mjs'; import { create_program, make_buffer } from './gl_utils.mjs';
import { PAINT_BG, PAINT_BG_PBR, PAINT_BG_NOISE, Paint_State } from './paint_state.mjs'; import { PAINT_BG, PAINT_BG_PBR, PAINT_BG_NOISE, Paint_State } from './paint_state.mjs';
import { Spin_State } from './spin_state.mjs'; import { Spin_State } from './spin_state.mjs';
import { shape_distortion, build_triangle_geo, build_goldberg_geo } from './render_geo.mjs'; import { shape_distortion, build_goldberg_geo } from './render_geo.mjs';
import { build_goldberg_adjacency } from './topology.mjs'; import { build_goldberg_adjacency } from './topology.mjs';
import { Undo_State } from './undo_state.mjs'; import { Undo_State } from './undo_state.mjs';
import { Env_State } from './env_state.mjs'; import { Env_State } from './env_state.mjs';
import { PALETTES, DEFAULT_PALETTE } from './palettes.mjs'; import { DEFAULT_PALETTE } from './palettes.mjs';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Load shaders // Load shaders
@@ -32,7 +32,6 @@ const paint = new Paint_State();
const undo_state = new Undo_State(); const undo_state = new Undo_State();
const env = new Env_State(); const env = new Env_State();
let current_geo = null; let current_geo = null;
let current_stage = 'relaxed';
let current_palette = DEFAULT_PALETTE; let current_palette = DEFAULT_PALETTE;
let building = false; let building = false;
let active_brush_side = 'left'; let active_brush_side = 'left';
@@ -42,12 +41,7 @@ let paint_dirty = false;
let debounce_timer = null; let debounce_timer = null;
function get_params() { function get_params() {
return { return { depth: 5, iters: 500, alpha_edge: 0, alpha_centroid: 0.04 };
depth: parseInt(document.getElementById('depth').value, 10),
iters: parseInt(document.getElementById('iters').value, 10),
alpha_edge: parseFloat(document.getElementById('alpha-edge').value),
alpha_centroid: parseFloat(document.getElementById('alpha-centroid').value),
};
} }
@@ -106,7 +100,7 @@ function upload_geo() {
function rebuild_face_colors() { function rebuild_face_colors() {
if (!cache.goldberg || !face_col_buf) { return; } if (!cache.goldberg || !face_col_buf) { return; }
const goldberg = cache.goldberg; const goldberg = cache.goldberg;
const use_relaxed = current_stage === 'relaxed'; const use_relaxed = true;
const fcol = [], fpbr = [], fnoise = []; const fcol = [], fpbr = [], fnoise = [];
for (let fi = 0; fi < goldberg.faces.length; fi++) { for (let fi = 0; fi < goldberg.faces.length; fi++) {
const face = goldberg.faces[fi]; const face = goldberg.faces[fi];
@@ -202,8 +196,6 @@ function apply_history(side) {
async function build() { async function build() {
if (building) { return; } if (building) { return; }
building = true; building = true;
const btn = document.getElementById('build-btn');
btn.disabled = true;
const p = get_params(); const p = get_params();
const t0 = Date.now(); const t0 = Date.now();
@@ -217,14 +209,6 @@ async function build() {
cache.poly = null; cache.poly = null;
cache.goldberg = null; cache.goldberg = null;
} }
if (current_stage === 'ico') {
current_geo = build_triangle_geo(cache.ico, current_palette);
upload_geo();
cache.depth = p.depth;
set_stats(current_geo, 'Icosahedron', Date.now() - t0);
set_status('');
return;
}
if (depth_changed || !cache.poly) { if (depth_changed || !cache.poly) {
let poly = cache.ico; let poly = cache.ico;
@@ -235,14 +219,6 @@ async function build() {
cache.poly = poly; cache.poly = poly;
cache.goldberg = null; cache.goldberg = null;
} }
if (current_stage === 'subdiv') {
current_geo = build_triangle_geo(cache.poly, current_palette);
upload_geo();
cache.depth = p.depth;
set_stats(current_geo, `Subdivided ×${p.depth}`, Date.now() - t0);
set_status('');
return;
}
if (depth_changed || !cache.goldberg) { if (depth_changed || !cache.goldberg) {
set_status('Building Goldberg dual…'); await yield_ui(30); set_status('Building Goldberg dual…'); await yield_ui(30);
@@ -250,16 +226,8 @@ async function build() {
cache.adj = null; cache.adj = null;
undo_state.invalidate(); undo_state.invalidate();
} }
if (current_stage === 'goldberg') {
current_geo = build_goldberg_geo(cache.goldberg, false, paint, current_palette);
upload_geo();
cache.depth = p.depth;
set_stats(current_geo, 'Goldberg (no relax)', Date.now() - t0);
set_status('');
return;
}
set_status(`Relaxing (${p.iters} iters, edge=${p.alpha_edge}, centroid=${p.alpha_centroid})…`); set_status(`Relaxing (${p.iters} iters)…`);
await yield_ui(50); await yield_ui(50);
cache.goldberg.relax_sphere(p.iters, p.alpha_edge, p.alpha_centroid); cache.goldberg.relax_sphere(p.iters, p.alpha_edge, p.alpha_centroid);
current_geo = build_goldberg_geo(cache.goldberg, true, paint, current_palette); current_geo = build_goldberg_geo(cache.goldberg, true, paint, current_palette);
@@ -270,7 +238,6 @@ async function build() {
} finally { } finally {
building = false; building = false;
btn.disabled = false;
snapshot_now(); snapshot_now();
} }
} }
@@ -302,7 +269,7 @@ function pick_face(px, py) {
const hx = ro[0]+t*rd[0], hy = ro[1]+t*rd[1], hz = ro[2]+t*rd[2]; const hx = ro[0]+t*rd[0], hy = ro[1]+t*rd[1], hz = ro[2]+t*rd[2];
const goldberg = cache.goldberg; const goldberg = cache.goldberg;
const use_relaxed = current_stage === 'relaxed'; const use_relaxed = true;
let best_fi = -1, best_dot = -Infinity; let best_fi = -1, best_dot = -Infinity;
for (let fi = 0; fi < goldberg.faces.length; fi++) { for (let fi = 0; fi < goldberg.faces.length; fi++) {
const face = goldberg.faces[fi]; const face = goldberg.faces[fi];
@@ -409,7 +376,7 @@ function faces_at_radius(center_fi, radius) {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function save_paint() { function save_paint() {
const depth = parseInt(document.getElementById('depth').value); const depth = 5;
const faces = []; const faces = [];
for (const [fi, c] of paint.face_colors) { for (const [fi, c] of paint.face_colors) {
const h = v => Math.round(v*255).toString(16).padStart(2, '0'); const h = v => Math.round(v*255).toString(16).padStart(2, '0');
@@ -432,10 +399,7 @@ function load_paint(file) {
reader.onload = e => { reader.onload = e => {
try { try {
const data = JSON.parse(e.target.result); const data = JSON.parse(e.target.result);
const depth = parseInt(document.getElementById('depth').value); if (data.depth !== 5) {
if (data.depth !== depth) {
document.getElementById('depth').value = data.depth;
document.getElementById('depth-val').textContent = data.depth;
cache.poly = null; cache.goldberg = null; cache.adj = null; cache.poly = null; cache.goldberg = null; cache.adj = null;
} }
paint.face_colors.clear(); paint.face_colors.clear();
@@ -730,50 +694,6 @@ function render_frame(ts) {
// UI wiring // UI wiring
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Palette buttons (built dynamically from PALETTES array).
{
const grid = document.getElementById('palette-grid');
for (const p of PALETTES) {
const btn = document.createElement('button');
btn.className = 'sb' + (p === DEFAULT_PALETTE ? ' active' : '');
btn.textContent = p.name;
btn.addEventListener('click', () => {
current_palette = p;
grid.querySelectorAll('.sb').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
rebuild_face_colors();
// Triangle stages need a full rebuild since positions are shared.
if (current_stage === 'ico' || current_stage === 'subdiv') { build(); }
});
grid.appendChild(btn);
}
}
document.querySelectorAll('.sb').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.sb').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
current_stage = btn.dataset.stage;
build();
});
});
document.getElementById('depth').addEventListener('input', () => {
const d = parseInt(document.getElementById('depth').value);
document.getElementById('depth-val').textContent = d;
document.getElementById('iters').value = d * 8;
cache.poly = null;
cache.goldberg = null;
cache.adj = null;
});
document.getElementById('build-btn').addEventListener('click', () => {
if (current_stage === 'relaxed') {
cache.goldberg = null;
cache.adj = null;
}
build();
});
document.getElementById('spin-btn').addEventListener('click', () => { document.getElementById('spin-btn').addEventListener('click', () => {
@@ -802,12 +722,8 @@ const update_paint_buttons = () => {
}; };
const make_tool_handler = tool => () => { const make_tool_handler = tool => () => {
if (!paint.enabled || paint.tool !== tool) { paint.enabled = true;
paint.enabled = true; paint.tool = tool;
paint.tool = tool;
} else {
paint.enabled = false;
}
update_paint_buttons(); update_paint_buttons();
}; };