Files
websperiments/standalone/goldberg-sphere/index.html
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

347 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Goldberg Polyhedron Paint</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: #080c10; color: #bcd; font-family: monospace; font-size: 13px;
display: flex; height: 100vh; overflow: hidden; }
/* ── sidebar ───────────────────────────────────────── */
#sidebar {
width: 240px; min-width: 240px;
background: #0c1218; border-right: 1px solid #1a2a3a;
padding: 10px; overflow-y: auto;
display: flex; flex-direction: column; gap: 12px;
}
h3 { color: #4af; font-size: 12px; text-transform: uppercase;
letter-spacing: 1px; border-bottom: 1px solid #1a2a3a; padding-bottom: 4px; }
/* stage buttons */
.stage-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 3px; }
.sb {
background: #0f1e2a; border: 1px solid #2a3a4a; color: #7ab;
padding: 6px 2px; cursor: pointer; font-family: monospace; font-size: 11px;
text-align: center; line-height: 1.3;
}
.sb:hover { background: #1a3040; color: #cef; }
.sb.active { background: #0a2d4a; border-color: #4af; color: #4af; }
/* param rows */
.param { display: flex; align-items: center; gap: 6px; }
.param label { width: 80px; color: #7ab; font-size: 11px; flex-shrink: 0; }
.param input[type=range] { flex: 1; max-width: 90px; }
.param input[type=number] {
width: 72px; background: #091520; border: 1px solid #1e3448; color: #cdf;
padding: 3px 5px; font-family: monospace; font-size: 12px;
}
.param select {
flex: 1; background: #091520; border: 1px solid #1e3448; color: #cdf;
padding: 3px 5px; font-family: monospace; font-size: 12px;
}
.val { width: 24px; text-align: right; color: #cdf; font-size: 11px; }
#spin-btn.active { background: #0a2d4a; border-color: #4af; color: #4af; }
.swatch { width: 18px; height: 18px; cursor: pointer; border: 1px solid #1a2a3a; flex-shrink: 0; }
.swatch:hover { outline: 2px solid #fff; outline-offset: 1px; }
#cd-swatch-grid .swatch { width: 28px; height: 28px; }
.swatch-tab { flex: 1; background: #0a1a2a; border: 1px solid #2a4a6a; color: #7ab; padding: 3px 2px; cursor: pointer; font-family: monospace; font-size: 10px; text-align: center; }
.swatch-tab.active { background: #0a2d4a; border-color: #4af; color: #4af; }
/* ── color picker dialog ─────────────────────────────── */
#color-dialog {
background: #0c1218; border: 1px solid #2a4a6a; color: #bcd;
padding: 16px; width: 420px; border-radius: 3px;
font-family: monospace; font-size: 12px;
position: fixed; inset: 0; margin: auto; height: fit-content;
}
#color-dialog::backdrop { background: rgba(0,0,0,0.65); }
.cd-model-tab {
flex: 1; background: #0a1a2a; border: 1px solid #2a4a6a; color: #7ab;
padding: 4px 2px; cursor: pointer; font-family: monospace; font-size: 11px; text-align: center;
}
.cd-model-tab.active { background: #0a2d4a; border-color: #4af; color: #4af; }
.cd-row { display: flex; align-items: center; gap: 6px; margin-top: 6px; }
.cd-row label { width: 16px; color: #7ab; font-size: 11px; flex-shrink: 0; }
.cd-row input[type=range] { flex: 1; }
.cd-row .val { width: 28px; text-align: right; color: #cdf; font-size: 11px; }
#paint-btn.active, #paint-brush-btn.active, #paint-circle-btn.active, #paint-fill-btn.active, #paint-line-btn.active, #paint-pick-btn.active { background: #1a2d0a; border-color: #8d4; color: #8d4; }
#build-btn {
background: #0a2d4a; border: 1px solid #4af; color: #4af;
padding: 7px; cursor: pointer; font-family: monospace; font-size: 13px; width: 100%;
}
#build-btn:hover { background: #1a3d5a; }
#build-btn:disabled { opacity: 0.45; cursor: default; }
#status { color: #fa8; font-size: 11px; min-height: 14px; }
#stats { color: #6a9; font-size: 11px; line-height: 1.7; white-space: pre; }
/* ── canvas ─────────────────────────────────────────── */
#canvas-wrap { flex: 1; position: relative; overflow: hidden; }
canvas { display: block; width: 100%; height: 100%; cursor: grab; }
canvas:active { cursor: grabbing; }
</style>
</head>
<body>
<div id="sidebar">
<div>
<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>Maze</h3>
<div class="param" style="margin-top:6px">
<label>Show</label>
<input type="checkbox" id="maze-show">
</div>
<div class="param">
<label>Wall width</label>
<input type="number" id="maze-width" value="0.015" min="0.003" max="0.08" step="0.003">
</div>
<div class="param">
<label>Algorithm</label>
<select id="maze-algo">
<option value="prim">Islands (Prim)</option>
<option value="walker">Edge walker</option>
</select>
</div>
<div id="prim-params">
<div class="param">
<label>Seeds</label>
<input type="number" id="maze-seeds" value="12" min="1" max="200" step="1">
</div>
<div class="param">
<label>Max cells</label>
<input type="number" id="maze-cells" value="40" min="3" max="500" step="5">
</div>
</div>
<div id="walker-params" style="display:none">
<div class="param">
<label>Walkers</label>
<input type="number" id="maze-walkers" value="20" min="1" max="500" step="1">
</div>
<div class="param">
<label>Stop %</label>
<input type="number" id="maze-stop" value="5" min="0" max="100" step="1">
</div>
<div class="param">
<label>Branch %</label>
<input type="number" id="maze-branch" value="15" min="0" max="100" step="1">
</div>
<div class="param">
<label>Close %</label>
<input type="number" id="maze-close" value="0" min="0" max="100" step="1">
</div>
</div>
<button id="maze-btn" style="margin-top:4px; background:#0a1f0a; border:1px solid #2a5a2a; color:#4c8; padding:5px; cursor:pointer; font-family:monospace; font-size:12px; width:100%">⟳ New maze</button>
</div>
<div>
<h3>Paint</h3>
<div style="display:flex; gap:4px; margin-top:6px">
<button id="brush-tab-left" style="flex:1; background:#0a2d4a; border:1px solid #4af; color:#4af; padding:5px; cursor:pointer; font-family:monospace; font-size:11px">◀ Left</button>
<button id="brush-tab-right" style="flex:1; background:#0a1a2a; border:1px solid #2a4a6a; color:#7ab; padding:5px; cursor:pointer; font-family:monospace; font-size:11px">Right ▶</button>
</div>
<div class="param" style="margin-top:6px">
<label>Color</label>
<div id="paint-color-btn" style="flex:1; height:26px; border:1px solid #1e3448; cursor:pointer; border-radius:2px;" title="Pick color…"></div>
</div>
<div id="sidebar-swatch-tabs" style="display:flex; gap:2px; margin-top:4px"></div>
<div id="sidebar-swatch-grid" style="display:flex; flex-wrap:wrap; gap:2px; margin-top:3px"></div>
<div class="param">
<label>Metallic</label>
<input type="range" id="metallic" min="0" max="1" step="0.01" value="0">
<span class="val" id="metallic-val">0.00</span>
</div>
<div class="param">
<label>Roughness</label>
<input type="range" id="roughness" min="0" max="1" step="0.01" value="0.6">
<span class="val" id="roughness-val">0.60</span>
</div>
<div style="display:flex; gap:3px; margin-top:6px; flex-wrap:wrap">
<button id="paint-btn" style="flex:1; background:#0a1a2a; border:1px solid #2a4a6a; color:#7ab; padding:5px 2px; cursor:pointer; font-family:monospace; font-size:11px">✏ Pen</button>
<button id="paint-brush-btn" style="flex:1; background:#0a1a2a; border:1px solid #2a4a6a; color:#7ab; padding:5px 2px; cursor:pointer; font-family:monospace; font-size:11px">⬤ Brush</button>
<button id="paint-circle-btn" style="flex:1; background:#0a1a2a; border:1px solid #2a4a6a; color:#7ab; padding:5px 2px; cursor:pointer; font-family:monospace; font-size:11px">○ Circle</button>
<button id="paint-fill-btn" style="flex:1; background:#0a1a2a; border:1px solid #2a4a6a; color:#7ab; padding:5px 2px; cursor:pointer; font-family:monospace; font-size:11px">⬛ Fill</button>
<button id="paint-line-btn" style="flex:1; background:#0a1a2a; border:1px solid #2a4a6a; color:#7ab; padding:5px 2px; cursor:pointer; font-family:monospace; font-size:11px"> Line</button>
<button id="paint-pick-btn" style="flex:1; background:#0a1a2a; border:1px solid #2a4a6a; color:#7ab; padding:5px 2px; cursor:pointer; font-family:monospace; font-size:11px">✦ Pick</button>
<button id="paint-clear" style="background:#1a0a0a; border:1px solid #4a2a2a; color:#a66; padding:5px 6px; cursor:pointer; font-family:monospace; font-size:11px"></button>
</div>
<div class="param" id="brush-params" style="display:none; margin-top:4px">
<label>Radius</label>
<input type="range" id="brush-radius" min="0" max="10" step="1" value="2">
<span class="val" id="brush-radius-val">2</span>
</div>
<div style="display:flex; gap:4px; margin-top:4px">
<button id="paint-save" style="flex:1; background:#0a1a2a; border:1px solid #2a4a6a; color:#7ab; padding:6px; cursor:pointer; font-family:monospace; font-size:12px">↓ Save</button>
<button id="paint-load" style="flex:1; background:#0a1a2a; border:1px solid #2a4a6a; color:#7ab; padding:6px; cursor:pointer; font-family:monospace; font-size:12px">↑ Load</button>
<input type="file" id="paint-load-input" accept=".json" style="display:none">
</div>
</div>
<div>
<h3>Noise</h3>
<div style="display:flex; gap:3px; margin-top:6px; flex-wrap:wrap">
<button class="noise-preset" data-preset="flat" style="flex:1; background:#0a1a2a; border:1px solid #2a4a6a; color:#7ab; padding:4px 2px; cursor:pointer; font-family:monospace; font-size:10px">Flat</button>
<button class="noise-preset" data-preset="snow" style="flex:1; background:#0a1a2a; border:1px solid #2a4a6a; color:#7ab; padding:4px 2px; cursor:pointer; font-family:monospace; font-size:10px">Snow</button>
<button class="noise-preset" data-preset="water" style="flex:1; background:#0a1a2a; border:1px solid #2a4a6a; color:#7ab; padding:4px 2px; cursor:pointer; font-family:monospace; font-size:10px">Water</button>
<button class="noise-preset" data-preset="dunes" style="flex:1; background:#0a1a2a; border:1px solid #2a4a6a; color:#7ab; padding:4px 2px; cursor:pointer; font-family:monospace; font-size:10px">Dunes</button>
<button class="noise-preset" data-preset="rock" style="flex:1; background:#0a1a2a; border:1px solid #2a4a6a; color:#7ab; padding:4px 2px; cursor:pointer; font-family:monospace; font-size:10px">Rock</button>
</div>
<div class="param" style="margin-top:4px">
<label>Scale</label>
<input type="range" id="noise-scale" min="0" max="20" step="0.5" value="0">
<span class="val" id="noise-scale-val">0.0</span>
</div>
<div class="param">
<label>Strength</label>
<input type="range" id="noise-strength" min="0" max="3" step="0.05" value="0">
<span class="val" id="noise-strength-val">0.00</span>
</div>
<div class="param">
<label>Octaves</label>
<input type="range" id="noise-octaves" min="1" max="8" step="1" value="4">
<span class="val" id="noise-octaves-val">4</span>
</div>
<div class="param">
<label>Gain</label>
<input type="range" id="noise-gain" min="0.2" max="0.8" step="0.05" value="0.5">
<span class="val" id="noise-gain-val">0.50</span>
</div>
</div>
<div>
<h3>Environment</h3>
<div style="color:#5a8; font-size:10px; margin-top:4px; margin-bottom:2px; letter-spacing:0.5px">LIGHT 1</div>
<div class="param">
<label>Azimuth</label>
<input type="range" id="l1-azim" min="-180" max="180" step="1" value="27">
<span class="val" id="l1-azim-val">27°</span>
</div>
<div class="param">
<label>Elevation</label>
<input type="range" id="l1-elev" min="-90" max="90" step="1" value="34">
<span class="val" id="l1-elev-val">34°</span>
</div>
<div class="param">
<label>Color</label>
<div id="l1-color-btn" style="flex:1; height:22px; border:1px solid #1e3448; cursor:pointer; border-radius:2px;" title="Pick color…"></div>
<input type="range" id="l1-intensity" min="0" max="4" step="0.05" value="1.5" style="flex:1; max-width:60px">
<span class="val" id="l1-intensity-val">1.50</span>
</div>
<div style="color:#5a8; font-size:10px; margin-top:6px; margin-bottom:2px; letter-spacing:0.5px">LIGHT 2</div>
<div class="param">
<label>Azimuth</label>
<input type="range" id="l2-azim" min="-180" max="180" step="1" value="-58">
<span class="val" id="l2-azim-val">-58°</span>
</div>
<div class="param">
<label>Elevation</label>
<input type="range" id="l2-elev" min="-90" max="90" step="1" value="-28">
<span class="val" id="l2-elev-val">-28°</span>
</div>
<div class="param">
<label>Color</label>
<div id="l2-color-btn" style="flex:1; height:22px; border:1px solid #1e3448; cursor:pointer; border-radius:2px;" title="Pick color…"></div>
<input type="range" id="l2-intensity" min="0" max="4" step="0.05" value="0.5" style="flex:1; max-width:60px">
<span class="val" id="l2-intensity-val">0.50</span>
</div>
<div style="color:#5a8; font-size:10px; margin-top:6px; margin-bottom:2px; letter-spacing:0.5px">AMBIENT</div>
<div class="param">
<label>Color</label>
<div id="amb-color-btn" style="flex:1; height:22px; border:1px solid #1e3448; cursor:pointer; border-radius:2px;" title="Pick color…"></div>
<input type="range" id="amb-intensity" min="0" max="4" step="0.05" value="0.07" style="flex:1; max-width:60px">
<span class="val" id="amb-intensity-val">0.07</span>
</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="build-btn">▶ Build</button>
<div>
<div id="status">Ready</div>
<div id="stats"></div>
</div>
</div>
<div id="canvas-wrap">
<canvas id="gl-canvas"></canvas>
</div>
<dialog id="color-dialog">
<div id="cd-preview" style="height:32px; border-radius:2px; margin-bottom:10px; border:1px solid #2a4a6a;"></div>
<div style="display:flex; gap:3px; margin-bottom:2px">
<button class="cd-model-tab active" data-model="hsl">HSL</button>
<button class="cd-model-tab" data-model="rgb">RGB</button>
<button class="cd-model-tab" data-model="hex">Hex</button>
</div>
<div id="cd-panel-hsl">
<div class="cd-row"><label>H</label><input type="range" id="cd-h" min="0" max="360" step="1"><span class="val" id="cd-h-val">0</span></div>
<div class="cd-row"><label>S</label><input type="range" id="cd-s" min="0" max="100" step="1"><span class="val" id="cd-s-val">0%</span></div>
<div class="cd-row"><label>L</label><input type="range" id="cd-l" min="0" max="100" step="1"><span class="val" id="cd-l-val">0%</span></div>
</div>
<div id="cd-panel-rgb" style="display:none">
<div class="cd-row"><label>R</label><input type="range" id="cd-r" min="0" max="255" step="1"><span class="val" id="cd-r-val">0</span></div>
<div class="cd-row"><label>G</label><input type="range" id="cd-g" min="0" max="255" step="1"><span class="val" id="cd-g-val">0</span></div>
<div class="cd-row"><label>B</label><input type="range" id="cd-b" min="0" max="255" step="1"><span class="val" id="cd-b-val">0</span></div>
</div>
<div id="cd-panel-hex" style="display:none; margin-top:6px">
<input type="text" id="cd-hex" maxlength="7" style="width:100%; background:#091520; border:1px solid #1e3448; color:#cdf; padding:5px; font-family:monospace; font-size:13px; text-align:center;">
</div>
<div style="margin-top:10px; border-top:1px solid #1a2a3a; padding-top:8px">
<div id="cd-swatch-tabs" style="display:flex; gap:2px; margin-bottom:4px"></div>
<div id="cd-swatch-grid" style="display:flex; flex-wrap:wrap; gap:2px;"></div>
</div>
<div style="display:flex; gap:6px; margin-top:10px">
<button id="cd-close" style="flex:1; background:#0a1a2a; border:1px solid #2a4a6a; color:#7ab; padding:6px; cursor:pointer; font-family:monospace; font-size:12px;">Close</button>
</div>
</dialog>
<script type="module" src="./playground_main.mjs"></script>
</body>
</html>