7 Commits
main ... main

Author SHA1 Message Date
eaf5389a07 Merge pull request 'Add grid color and opacity controls' (#5) from mikael-lovqvists-claude-agent/websperiments:grid-color-opacity into main
Reviewed-on: mikael-lovqvist/websperiments#5
2026-05-10 19:53:07 +00:00
7c60252904 Add grid color and opacity controls
Edge shader now takes u_edge_color and u_edge_opacity uniforms.
Grid section added to sidebar with a color picker and opacity slider.
Blending enabled around edge draw call to support sub-1 opacity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 19:49:46 +00:00
2ec1eb1882 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: mikael-lovqvist/websperiments#4
2026-05-10 19:26:04 +00:00
f7b21311a8 Strip pipeline/palette/depth/relax UI; lock to paint mode
- Remove Pipeline stage, Palette, Mesh depth, Relaxation UI sections
- Remove Build button; mesh always builds as relaxed Goldberg depth-5
- Hardcode get_params() — no DOM reads for build parameters
- Remove current_stage; use_relaxed is always true
- Tool buttons no longer toggle paint off when re-clicking active tool

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 19:19:11 +00:00
d45ee8be36 Merge pull request 'Tune Goldberg Paint defaults: depth 5, 500 iters, start in draw mode' (#3) from mikael-lovqvists-claude-agent/websperiments:defaults-and-draw-mode into main
Reviewed-on: mikael-lovqvist/websperiments#3
2026-05-10 16:29:20 +00:00
1dd4582c9d Tune defaults: depth 5, 500 iters, start in draw mode
- Default subdivision depth 4 → 5
- Default relaxation iterations 32 → 500
- Paint mode enabled at startup (pen tool active)
- Remove worst-shape distortion ratio from stats display

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:28:32 +00:00
fdea58f18d Merge pull request 'Add Goldberg Polyhedron Paint experiment' (#1) from mikael-lovqvists-claude-agent/websperiments:main into main
Reviewed-on: mikael-lovqvist/websperiments#1
2026-05-10 16:07:52 +00:00
3 changed files with 70 additions and 141 deletions

View File

@@ -90,48 +90,6 @@
<h3>Sphere Playground</h3>
</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="4">
<span class="val" id="depth-val">4</span>
</div>
</div>
<div>
<h3>Relaxation</h3>
<div class="param" style="margin-top:6px">
<label>Iterations</label>
<input type="number" id="iters" value="32" 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>
<h3>Paint</h3>
<div style="display:flex; gap:4px; margin-top:6px">
@@ -175,6 +133,19 @@
</div>
</div>
<div>
<h3>Grid</h3>
<div class="param" style="margin-top:6px">
<label>Color</label>
<div id="grid-color-btn" style="flex:1; height:22px; border:1px solid #1e3448; cursor:pointer; border-radius:2px;" title="Pick color…"></div>
</div>
<div class="param">
<label>Opacity</label>
<input type="range" id="grid-opacity" min="0" max="1" step="0.01" value="1">
<span class="val" id="grid-opacity-val">1.00</span>
</div>
</div>
<div>
<h3>Noise</h3>
<div style="display:flex; gap:3px; margin-top:6px; flex-wrap:wrap">
@@ -252,7 +223,6 @@
</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="build-btn">▶ Build</button>
<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 { PAINT_BG, PAINT_BG_PBR, PAINT_BG_NOISE, Paint_State } from './paint_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 { Undo_State } from './undo_state.mjs';
import { Env_State } from './env_state.mjs';
import { PALETTES, DEFAULT_PALETTE } from './palettes.mjs';
import { DEFAULT_PALETTE } from './palettes.mjs';
// ---------------------------------------------------------------------------
// Load shaders
@@ -31,8 +31,8 @@ const spin = new Spin_State();
const paint = new Paint_State();
const undo_state = new Undo_State();
const env = new Env_State();
const grid_state = { color: [0.05, 0.05, 0.05], opacity: 1.0 };
let current_geo = null;
let current_stage = 'relaxed';
let current_palette = DEFAULT_PALETTE;
let building = false;
let active_brush_side = 'left';
@@ -42,12 +42,7 @@ let paint_dirty = false;
let debounce_timer = null;
function get_params() {
return {
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),
};
return { depth: 5, iters: 500, alpha_edge: 0, alpha_centroid: 0.04 };
}
@@ -61,9 +56,6 @@ function set_stats(geo, stage_name, ms) {
if (s.vertices !== undefined) { lines += ` Verts: ${s.vertices}`; }
if (s.pentagons !== undefined) { lines += ` (${s.pentagons} pent)`; }
lines += '\n';
if (s.worst_shape !== null) {
lines += `Worst shape_r: ${s.worst_shape.toFixed(4)}\n`;
}
lines += `Build: ${ms}ms`;
stats_el.textContent = lines;
}
@@ -109,7 +101,7 @@ function upload_geo() {
function rebuild_face_colors() {
if (!cache.goldberg || !face_col_buf) { return; }
const goldberg = cache.goldberg;
const use_relaxed = current_stage === 'relaxed';
const use_relaxed = true;
const fcol = [], fpbr = [], fnoise = [];
for (let fi = 0; fi < goldberg.faces.length; fi++) {
const face = goldberg.faces[fi];
@@ -205,8 +197,6 @@ function apply_history(side) {
async function build() {
if (building) { return; }
building = true;
const btn = document.getElementById('build-btn');
btn.disabled = true;
const p = get_params();
const t0 = Date.now();
@@ -220,14 +210,6 @@ async function build() {
cache.poly = 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) {
let poly = cache.ico;
@@ -238,14 +220,6 @@ async function build() {
cache.poly = poly;
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) {
set_status('Building Goldberg dual…'); await yield_ui(30);
@@ -253,16 +227,8 @@ async function build() {
cache.adj = null;
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);
cache.goldberg.relax_sphere(p.iters, p.alpha_edge, p.alpha_centroid);
current_geo = build_goldberg_geo(cache.goldberg, true, paint, current_palette);
@@ -273,7 +239,6 @@ async function build() {
} finally {
building = false;
btn.disabled = false;
snapshot_now();
}
}
@@ -305,7 +270,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 goldberg = cache.goldberg;
const use_relaxed = current_stage === 'relaxed';
const use_relaxed = true;
let best_fi = -1, best_dot = -Infinity;
for (let fi = 0; fi < goldberg.faces.length; fi++) {
const face = goldberg.faces[fi];
@@ -412,7 +377,7 @@ function faces_at_radius(center_fi, radius) {
// ---------------------------------------------------------------------------
function save_paint() {
const depth = parseInt(document.getElementById('depth').value);
const depth = 5;
const faces = [];
for (const [fi, c] of paint.face_colors) {
const h = v => Math.round(v*255).toString(16).padStart(2, '0');
@@ -435,10 +400,7 @@ function load_paint(file) {
reader.onload = e => {
try {
const data = JSON.parse(e.target.result);
const depth = parseInt(document.getElementById('depth').value);
if (data.depth !== depth) {
document.getElementById('depth').value = data.depth;
document.getElementById('depth-val').textContent = data.depth;
if (data.depth !== 5) {
cache.poly = null; cache.goldberg = null; cache.adj = null;
}
paint.face_colors.clear();
@@ -720,10 +682,15 @@ function render_frame(ts) {
gl.disable(gl.POLYGON_OFFSET_FILL);
gl.useProgram(edge_prog);
gl.uniformMatrix4fv(gl.getUniformLocation(edge_prog, 'u_mvp'), false, mvp);
gl.uniform3fv(gl.getUniformLocation(edge_prog, 'u_edge_color'), grid_state.color);
gl.uniform1f(gl.getUniformLocation(edge_prog, 'u_edge_opacity'), grid_state.opacity);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
const ep = gl.getAttribLocation(edge_prog, 'a_pos');
gl.bindBuffer(gl.ARRAY_BUFFER, edge_pos_buf); gl.enableVertexAttribArray(ep); gl.vertexAttribPointer(ep, 3, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.LINES, 0, edge_verts);
gl.disableVertexAttribArray(ep);
gl.disable(gl.BLEND);
gl.enable(gl.POLYGON_OFFSET_FILL);
@@ -733,50 +700,6 @@ function render_frame(ts) {
// 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', () => {
@@ -805,12 +728,8 @@ const update_paint_buttons = () => {
};
const make_tool_handler = tool => () => {
if (!paint.enabled || paint.tool !== tool) {
paint.enabled = true;
paint.tool = tool;
} else {
paint.enabled = false;
}
update_paint_buttons();
};
@@ -1160,6 +1079,42 @@ function _hsl_to_rgb(h,s,l) {
sync_env_ui();
}
// ---------------------------------------------------------------------------
// Grid UI wiring
// ---------------------------------------------------------------------------
{
const btn = document.getElementById('grid-color-btn');
const opacity = document.getElementById('grid-opacity');
const opacity_val = document.getElementById('grid-opacity-val');
const hex_from = c => {
const h = v => Math.round(Math.min(v, 1) * 255).toString(16).padStart(2, '0');
return `#${h(c[0])}${h(c[1])}${h(c[2])}`;
};
btn.style.background = hex_from(grid_state.color);
btn.addEventListener('click', () => {
color_dialog_open({
get: () => hex_from(grid_state.color),
set: hex => {
const r = parseInt(hex.slice(1, 3), 16) / 255;
const g = parseInt(hex.slice(3, 5), 16) / 255;
const b = parseInt(hex.slice(5, 7), 16) / 255;
grid_state.color = [r, g, b];
btn.style.background = hex;
request_render();
},
});
});
opacity.addEventListener('input', () => {
grid_state.opacity = parseFloat(opacity.value);
opacity_val.textContent = grid_state.opacity.toFixed(2);
request_render();
});
}
// ---------------------------------------------------------------------------
// Keyboard shortcuts
// ---------------------------------------------------------------------------
@@ -1179,5 +1134,7 @@ window.addEventListener('keydown', e => {
// Boot
// ---------------------------------------------------------------------------
paint.enabled = true;
update_paint_buttons();
resize();
build();

View File

@@ -1,2 +1,4 @@
precision mediump float;
void main() { gl_FragColor = vec4(0.05, 0.05, 0.05, 1.0); }
uniform vec3 u_edge_color;
uniform float u_edge_opacity;
void main() { gl_FragColor = vec4(u_edge_color, u_edge_opacity); }