The Background tab fills in — facets rendered to order, eight hues in a carousel
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -53,7 +53,9 @@ One **style editor** component — a background palette grid and a curated symbo
|
||||
|
||||
## Board popover
|
||||
|
||||
**Restructure in progress (2026-08-07): the popover is going tabbed.** The symbol/name header stays at the top; below it sit three tabs — **Info**, **Background**, **Git** — each the settings surface for one aspect of board configuration, each designed in its own dedicated session (Background and Git are empty placeholders until theirs). The bullets below describe the popover's pre-tab content; what of it rehomes into which tab — and where the embedded style editor lands — is those sessions' to settle. The rulings inside the bullets (rename semantics, the git postures, the popover/sheet split) stand; only their placement is in motion.
|
||||
**Restructure in progress (2026-08-07): the popover is going tabbed.** The symbol/name header stays at the top; below it sit three tabs — **Info**, **Background**, **Git** — each the settings surface for one aspect of board configuration, each designed in its own dedicated session (Git is an empty placeholder until its; Info and Background are settled). The bullets below describe the popover's pre-tab content; what of it rehomes into which tab — and where the git section lands — is the Git session's to settle. The rulings inside the bullets (rename semantics, the git postures, the popover/sheet split) stand; only their placement is in motion.
|
||||
|
||||
**The Background tab** (settled 2026-08-07, its dedicated session): two surfaces, manual then generated. The **Color** section is the re-homed style-editor embed — background half only, `showsSymbols: false`, exactly the pre-tab composition rule (§ Styling ▸ Controls: the popover's symbol picker beside the rename field owns the board glyph) — same write path, same recents, so the embed's re-homing changed its address and nothing else. The **Generated** section is the faceted-background picker (DESIGN/explorations/board-backgrounds.md ▸ Faceted gallery, the reviewed recipe): four segmented filters — Tone (defaults to the current appearance), Colors mono/duo/trio, Mesh coarse/medium/fine, Saturation soft/mid/rich — over a horizontal carousel of the eight wheel hues, one seed per hue, plus a Reroll button that re-mints the seeds (filters change the treatment, Reroll changes the geometry; the preview and the applied file share a seed, so what's clicked is what lands). Clicking a swatch renders the recipe at the 3072 px decode ceiling off-main and lands it in one write: the PNG into the board root as `facets.png` (Finder-ladder rename only when a foreign file owns the name), `background.image` pointed at it, and `background.color` set to the recipe's primary color — the underlay that stands in while the image decodes or if the file ever goes missing. **Backgrounds ship as static pixels, never live-rendered views** (the perf/sync ruling, 2026-08-07): the generator runs at pick time, the render loop only ever composites a decoded bitmap. Native undo restores the two fields, not the overwritten bytes — regenerating over our own PNG is destructive, documented, and accepted (the escape hatch remains the raw file). The whole tab disables under the read-only lock as one surface.
|
||||
|
||||
### Info tab (settled 2026-08-07)
|
||||
|
||||
|
||||
@@ -0,0 +1,542 @@
|
||||
<title>Board Backgrounds — Facets</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#FAFAF7;
|
||||
--ink:#1D1C1A;
|
||||
--ink-2:#6E6A63;
|
||||
--line:#E4E1DA;
|
||||
--chip:#F0EEE8;
|
||||
--accent:#5B6E8C;
|
||||
--paper-a85: rgba(250,250,247,.85);
|
||||
}
|
||||
@media (prefers-color-scheme: dark){
|
||||
:root:not([data-theme="light"]){
|
||||
--paper:#181715;
|
||||
--ink:#ECEAE5;
|
||||
--ink-2:#98938A;
|
||||
--line:#2E2C28;
|
||||
--chip:#242220;
|
||||
--accent:#8FA3C0;
|
||||
--paper-a85: rgba(24,23,21,.85);
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"]{
|
||||
--paper:#181715;
|
||||
--ink:#ECEAE5;
|
||||
--ink-2:#98938A;
|
||||
--line:#2E2C28;
|
||||
--chip:#242220;
|
||||
--accent:#8FA3C0;
|
||||
--paper-a85: rgba(24,23,21,.85);
|
||||
}
|
||||
|
||||
*, *::before, *::after{ box-sizing:border-box; }
|
||||
|
||||
body{
|
||||
margin:0;
|
||||
background:var(--paper);
|
||||
color:var(--ink);
|
||||
font-family:-apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
|
||||
.wrap{ max-width:1240px; margin:0 auto; padding:0 32px 64px; }
|
||||
|
||||
.page-head{ padding:32px 0 16px; }
|
||||
.page-head h1{ font-size:22px; font-weight:600; margin:0 0 6px; text-wrap:balance; }
|
||||
.page-head .subtitle{ font-size:13px; color:var(--ink-2); margin:0; max-width:760px; }
|
||||
|
||||
.controlbar{
|
||||
position:sticky; top:0; z-index:10;
|
||||
display:flex; align-items:center; gap:20px; row-gap:10px; flex-wrap:wrap;
|
||||
padding:12px 0;
|
||||
background:var(--paper-a85);
|
||||
-webkit-backdrop-filter:blur(8px);
|
||||
backdrop-filter:blur(8px);
|
||||
border-bottom:1px solid var(--line);
|
||||
margin-bottom:28px;
|
||||
}
|
||||
.chipgroup{ display:flex; gap:6px; flex-wrap:wrap; }
|
||||
.chip{
|
||||
font:inherit; font-size:12px; padding:5px 11px; border-radius:999px;
|
||||
border:1px solid var(--line); background:var(--chip); color:var(--ink);
|
||||
cursor:pointer; line-height:1.3;
|
||||
}
|
||||
.chip:hover{ border-color:var(--accent); }
|
||||
.chip.active{ background:var(--accent); border-color:var(--accent); color:var(--paper); }
|
||||
.chip:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }
|
||||
|
||||
.toggle{ display:flex; align-items:center; gap:6px; font-size:12px; color:var(--ink); cursor:pointer; user-select:none; }
|
||||
.toggle input{ width:14px; height:14px; accent-color:var(--accent); }
|
||||
.toggle input:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }
|
||||
|
||||
.gen-note{ font-size:12px; color:var(--ink-2); font-family:ui-monospace,"SF Mono",Menlo,monospace; }
|
||||
|
||||
.recipe-box{ margin-bottom:40px; }
|
||||
.section-label{ font-size:11px; font-weight:600; letter-spacing:.06em; text-transform:uppercase; color:var(--ink-2); margin:0 0 10px; }
|
||||
.recipe-list{ display:grid; grid-template-columns:1fr 1fr; gap:7px 32px; margin:0; }
|
||||
.recipe-item{ display:flex; gap:6px; font-size:13px; line-height:1.5; }
|
||||
.recipe-item dt{ margin:0; font-weight:600; color:var(--ink-2); flex:0 0 auto; }
|
||||
.recipe-item dd{ margin:0; color:var(--ink-2); }
|
||||
@media (max-width:720px){ .recipe-list{ grid-template-columns:1fr; } }
|
||||
|
||||
.family{ margin-bottom:44px; }
|
||||
.family h2{ font-size:15px; font-weight:600; margin:0 0 4px; }
|
||||
.family .recipe{ font-size:13px; color:var(--ink-2); margin:0 0 14px; max-width:820px; }
|
||||
|
||||
.colheads{
|
||||
display:grid; grid-template-columns:repeat(3, 1fr); gap:14px;
|
||||
margin-bottom:8px;
|
||||
position:sticky; top:56px; z-index:5;
|
||||
background:var(--paper-a85);
|
||||
-webkit-backdrop-filter:blur(8px);
|
||||
backdrop-filter:blur(8px);
|
||||
padding:4px 0;
|
||||
}
|
||||
.colheads span{
|
||||
font-size:11px; font-weight:600; letter-spacing:.06em; text-transform:uppercase;
|
||||
color:var(--ink-2);
|
||||
}
|
||||
.grid{ display:grid; grid-template-columns:repeat(3, 1fr); gap:14px; }
|
||||
@media (max-width:720px){
|
||||
.colheads{ display:none; }
|
||||
.grid{ grid-template-columns:1fr; }
|
||||
}
|
||||
|
||||
.card{ cursor:pointer; }
|
||||
.frame{
|
||||
aspect-ratio:16/10; border-radius:8px; border:1px solid var(--line);
|
||||
overflow:hidden; background:var(--chip);
|
||||
}
|
||||
.frame svg, .lightbox-frame svg{ display:block; width:100%; height:100%; }
|
||||
.caption{ display:flex; justify-content:space-between; align-items:baseline; margin-top:6px; font-size:11px; gap:8px; }
|
||||
.caption .swatch-id{ font-family:ui-monospace,"SF Mono",Menlo,monospace; color:var(--ink); }
|
||||
.caption .note{ color:var(--ink-2); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
|
||||
.board-overlay{ display:none; }
|
||||
body.show-overlay .board-overlay{ display:inline; }
|
||||
|
||||
.lightbox{
|
||||
position:fixed; inset:0; z-index:100;
|
||||
display:flex; flex-direction:column; align-items:center; justify-content:center; gap:14px;
|
||||
background:rgba(0,0,0,.55);
|
||||
opacity:0; pointer-events:none;
|
||||
transition:opacity .12s ease;
|
||||
}
|
||||
.lightbox.open{ opacity:1; pointer-events:auto; }
|
||||
.lightbox-frame{
|
||||
width:min(92vw, 1100px); aspect-ratio:16/10; border-radius:10px; overflow:hidden;
|
||||
box-shadow:0 24px 60px rgba(0,0,0,.45);
|
||||
background:var(--chip);
|
||||
}
|
||||
.lightbox-id{ font-family:ui-monospace,"SF Mono",Menlo,monospace; font-size:12px; color:#fff; }
|
||||
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
.lightbox{ transition:none; }
|
||||
}
|
||||
</style>
|
||||
<div class="wrap">
|
||||
<header class="page-head">
|
||||
<h1>Board Backgrounds — Facets, round 2</h1>
|
||||
<p class="subtitle">The triangle mesh swept on the full axis set: vertex count (columns), color count (mono · complementary duo · contrasting trio), tone, and saturation. Brightness stays a narrow per-triangle jitter around the tone base. Hue wheel narrowed to four representatives this round — amber, forest, sky, rose — the full wheel returns for finals. 3 color counts × 2 tones × 4 hues × 3 saturations × 3 densities = 216 swatches. Reroll to reseed geometry and color assignment under the same recipes.</p>
|
||||
</header>
|
||||
|
||||
<div class="controlbar">
|
||||
<div class="chipgroup" id="strategy-chips" role="group" aria-label="Color-count filter">
|
||||
<button type="button" class="chip active" data-strategy="all" aria-pressed="true">All</button>
|
||||
<button type="button" class="chip" data-strategy="mono" aria-pressed="false">Mono</button>
|
||||
<button type="button" class="chip" data-strategy="duo" aria-pressed="false">Duo</button>
|
||||
<button type="button" class="chip" data-strategy="trio" aria-pressed="false">Trio</button>
|
||||
</div>
|
||||
<div class="chipgroup" id="tone-chips" role="group" aria-label="Tone filter">
|
||||
<button type="button" class="chip active" data-tone="all" aria-pressed="true">All</button>
|
||||
<button type="button" class="chip" data-tone="light" aria-pressed="false">Light</button>
|
||||
<button type="button" class="chip" data-tone="dark" aria-pressed="false">Dark</button>
|
||||
</div>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" id="overlay-toggle">
|
||||
<span>Board overlay</span>
|
||||
</label>
|
||||
<button type="button" class="chip" id="reroll">↻ Reroll geometry</button>
|
||||
<span class="gen-note" id="gen-note">gen 1</span>
|
||||
</div>
|
||||
|
||||
<section class="recipe-box">
|
||||
<p class="section-label">Recipe parameters</p>
|
||||
<dl class="recipe-list">
|
||||
<div class="recipe-item"><dt>Vertices</dt><dd>coarse 5×3 cells (~20 triangles) · medium 9×6 (~97) · fine 14×9 (~230), grid-jittered then Delaunay-triangulated</dd></div>
|
||||
<div class="recipe-item"><dt>Colors</dt><dd>mono · duo = complement H+180° weighted 65/35 · trio = triad H±120° weighted 50/30/20, picked per triangle</dd></div>
|
||||
<div class="recipe-item"><dt>Saturation</dt><dd>soft · mid · rich row bands, jittered ±15% per triangle</dd></div>
|
||||
<div class="recipe-item"><dt>Brightness</dt><dd>narrow band — tone base (light ≈85–90%, dark ≈17–21%) ±4.5 per triangle; all hues in a swatch share it</dd></div>
|
||||
<div class="recipe-item"><dt>Hue</dt><dd>amber 38° · forest 140° · sky 215° · rose 335° (±3° per triangle)</dd></div>
|
||||
<div class="recipe-item"><dt>Randomness</dt><dd>geometry and per-triangle color assignment reseed on reroll; the swatch ID names the recipe</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="family" data-strategy="mono" data-tone="light">
|
||||
<h2>Mono — Light</h2>
|
||||
<p class="recipe">One hue; only the brightness jitter draws the mesh. Rows: each hue at soft, mid, rich saturation.</p>
|
||||
<div class="colheads"><span>Coarse</span><span>Medium</span><span>Fine</span></div>
|
||||
<div class="grid" id="grid-mono-light"></div>
|
||||
</section>
|
||||
|
||||
<section class="family" data-strategy="duo" data-tone="light">
|
||||
<h2>Duo — Light</h2>
|
||||
<p class="recipe">Base hue plus its complement, weighted 65/35 — a dominant field with contrasting inclusions.</p>
|
||||
<div class="colheads"><span>Coarse</span><span>Medium</span><span>Fine</span></div>
|
||||
<div class="grid" id="grid-duo-light"></div>
|
||||
</section>
|
||||
|
||||
<section class="family" data-strategy="trio" data-tone="light">
|
||||
<h2>Trio — Light</h2>
|
||||
<p class="recipe">A contrasting triad (H, H+120°, H−120°) weighted 50/30/20 — closest in spirit to the low-poly reference.</p>
|
||||
<div class="colheads"><span>Coarse</span><span>Medium</span><span>Fine</span></div>
|
||||
<div class="grid" id="grid-trio-light"></div>
|
||||
</section>
|
||||
|
||||
<section class="family" data-strategy="mono" data-tone="dark">
|
||||
<h2>Mono — Dark</h2>
|
||||
<p class="recipe">The mono mesh on the dark base — facets catching light like slate.</p>
|
||||
<div class="colheads"><span>Coarse</span><span>Medium</span><span>Fine</span></div>
|
||||
<div class="grid" id="grid-mono-dark"></div>
|
||||
</section>
|
||||
|
||||
<section class="family" data-strategy="duo" data-tone="dark">
|
||||
<h2>Duo — Dark</h2>
|
||||
<p class="recipe">Complementary pair on the dark base; at low brightness the hue contrast turns ember-like.</p>
|
||||
<div class="colheads"><span>Coarse</span><span>Medium</span><span>Fine</span></div>
|
||||
<div class="grid" id="grid-duo-dark"></div>
|
||||
</section>
|
||||
|
||||
<section class="family" data-strategy="trio" data-tone="dark">
|
||||
<h2>Trio — Dark</h2>
|
||||
<p class="recipe">The triad on the dark base — stained glass at dusk.</p>
|
||||
<div class="colheads"><span>Coarse</span><span>Medium</span><span>Fine</span></div>
|
||||
<div class="grid" id="grid-trio-dark"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="lightbox" id="lightbox">
|
||||
<div class="lightbox-frame" id="lightbox-frame"></div>
|
||||
<div class="lightbox-id" id="lightbox-id"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
"use strict";
|
||||
|
||||
// ---------- seeded PRNG (same as sweep-1 gallery) ----------
|
||||
function xmur3(str){
|
||||
let h = 1779033703 ^ str.length;
|
||||
for(let i=0;i<str.length;i++){
|
||||
h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
|
||||
h = (h << 13) | (h >>> 19);
|
||||
}
|
||||
return function(){
|
||||
h = Math.imul(h ^ (h >>> 16), 2246822507);
|
||||
h = Math.imul(h ^ (h >>> 13), 3266489909);
|
||||
h ^= h >>> 16;
|
||||
return h >>> 0;
|
||||
};
|
||||
}
|
||||
function mulberry32(a){
|
||||
return function(){
|
||||
let t = (a += 0x6D2B79F5);
|
||||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
function seededRng(id){
|
||||
return mulberry32(xmur3(id)());
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
function mod360(h){ return ((h % 360) + 360) % 360; }
|
||||
function clamp(v,min,max){ return v < min ? min : (v > max ? max : v); }
|
||||
function hsl(h,s,l){ return "hsl(" + f1(mod360(h)) + "," + f1(clamp(s,0,100)) + "%," + f1(clamp(l,0,100)) + "%)"; }
|
||||
function rnd(rng,min,max){ return min + rng() * (max - min); }
|
||||
function rndInt(rng,min,max){ return Math.floor(rnd(rng,min,max+1)); }
|
||||
function f1(n){ return n.toFixed(1); }
|
||||
|
||||
// ---------- Delaunay (Bowyer–Watson) ----------
|
||||
function circumcircle(a,b,c){
|
||||
const ax=a[0],ay=a[1],bx=b[0],by=b[1],cx=c[0],cy=c[1];
|
||||
const d = 2*(ax*(by-cy)+bx*(cy-ay)+cx*(ay-by));
|
||||
if(Math.abs(d) < 1e-9) return null;
|
||||
const a2 = ax*ax+ay*ay, b2 = bx*bx+by*by, c2 = cx*cx+cy*cy;
|
||||
const ux = (a2*(by-cy)+b2*(cy-ay)+c2*(ay-by))/d;
|
||||
const uy = (a2*(cx-bx)+b2*(ax-cx)+c2*(bx-ax))/d;
|
||||
const dx = ax-ux, dy = ay-uy;
|
||||
return { x:ux, y:uy, r2:dx*dx+dy*dy };
|
||||
}
|
||||
function triangulate(points){
|
||||
const pts = points.slice();
|
||||
const st = pts.length;
|
||||
pts.push([-3000,-3000],[3500,-3000],[240,3600]);
|
||||
let tris = [{ i:[st,st+1,st+2], cc:circumcircle(pts[st],pts[st+1],pts[st+2]) }];
|
||||
for(let pi=0; pi<st; pi++){
|
||||
const p = pts[pi];
|
||||
const bad = [];
|
||||
for(let t=0;t<tris.length;t++){
|
||||
const cc = tris[t].cc;
|
||||
if(cc){
|
||||
const dx = p[0]-cc.x, dy = p[1]-cc.y;
|
||||
if(dx*dx+dy*dy < cc.r2) bad.push(tris[t]);
|
||||
}
|
||||
}
|
||||
const edgeCount = new Map();
|
||||
for(let b=0;b<bad.length;b++){
|
||||
const idx = bad[b].i;
|
||||
for(let e=0;e<3;e++){
|
||||
const u = idx[e], v = idx[(e+1)%3];
|
||||
const key = u < v ? u+"_"+v : v+"_"+u;
|
||||
edgeCount.set(key,(edgeCount.get(key)||0)+1);
|
||||
}
|
||||
}
|
||||
const badSet = new Set(bad);
|
||||
tris = tris.filter(function(t){ return !badSet.has(t); });
|
||||
for(let b=0;b<bad.length;b++){
|
||||
const idx = bad[b].i;
|
||||
for(let e=0;e<3;e++){
|
||||
const u = idx[e], v = idx[(e+1)%3];
|
||||
const key = u < v ? u+"_"+v : v+"_"+u;
|
||||
if(edgeCount.get(key) === 1){
|
||||
const cc = circumcircle(pts[u],pts[v],p);
|
||||
if(cc) tris.push({ i:[u,v,pi], cc:cc });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const out = [];
|
||||
for(let t=0;t<tris.length;t++){
|
||||
const idx = tris[t].i;
|
||||
if(idx[0] < st && idx[1] < st && idx[2] < st) out.push(idx);
|
||||
}
|
||||
return { pts: pts, tris: out };
|
||||
}
|
||||
|
||||
// ---------- color ----------
|
||||
// Weighted per-triangle hue pick; weights sum to 1.
|
||||
function pickHue(rng, hueList){
|
||||
let x = rng();
|
||||
for(let i=0;i<hueList.length;i++){
|
||||
if(x < hueList[i].w) return hueList[i].h;
|
||||
x -= hueList[i].w;
|
||||
}
|
||||
return hueList[hueList.length-1].h;
|
||||
}
|
||||
function triangleColor(rng, hueList, spec){
|
||||
const h = pickHue(rng, hueList) + rnd(rng,-3,3);
|
||||
const s = spec.sat * rnd(rng,0.85,1.15);
|
||||
const l = spec.lBase + rnd(rng,-spec.lJit,spec.lJit);
|
||||
return hsl(h,s,l);
|
||||
}
|
||||
|
||||
// ---------- facets generator ----------
|
||||
// Points scatter beyond the 480x300 viewBox so the mesh covers the frame edge-to-edge.
|
||||
function genFacets(rng, hueList, spec, density){
|
||||
const pts = [];
|
||||
const x0 = -36, y0 = -36, x1 = 516, y1 = 336;
|
||||
const cols = density.cols, rows = density.rows;
|
||||
const cw = (x1-x0)/cols, ch = (y1-y0)/rows;
|
||||
for(let c=0;c<cols;c++){
|
||||
for(let r=0;r<rows;r++){
|
||||
pts.push([ x0 + (c + rnd(rng,0.08,0.92))*cw, y0 + (r + rnd(rng,0.08,0.92))*ch ]);
|
||||
}
|
||||
}
|
||||
const mesh = triangulate(pts);
|
||||
let s = '<rect width="480" height="300" fill="' + hsl(hueList[0].h, spec.sat, spec.lBase) + '"/>';
|
||||
for(let t=0;t<mesh.tris.length;t++){
|
||||
const idx = mesh.tris[t];
|
||||
const a = mesh.pts[idx[0]], b = mesh.pts[idx[1]], c = mesh.pts[idx[2]];
|
||||
const color = triangleColor(rng, hueList, spec);
|
||||
const ptsAttr = f1(a[0])+","+f1(a[1])+" "+f1(b[0])+","+f1(b[1])+" "+f1(c[0])+","+f1(c[1]);
|
||||
s += '<polygon points="' + ptsAttr + '" fill="' + color + '" stroke="' + color + '" stroke-width="0.7"/>';
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// ---------- board overlay (same as sweep-1 gallery) ----------
|
||||
function buildOverlay(rng, tone){
|
||||
const laneFill = tone === "dark" ? "rgba(20,20,24,0.40)" : "rgba(255,255,255,0.38)";
|
||||
const cardFill = tone === "dark" ? "#26262B" : "#FFFFFF";
|
||||
const cardStroke = tone === "dark" ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.08)";
|
||||
const titleFill = tone === "dark" ? "rgba(22,22,26,0.55)" : "rgba(255,255,255,0.55)";
|
||||
let g = '<g class="board-overlay">';
|
||||
g += '<rect x="0" y="0" width="480" height="34" fill="' + titleFill + '"/>';
|
||||
const laneX = [16,168,320];
|
||||
for(let li=0; li<laneX.length; li++){
|
||||
const lx = laneX[li];
|
||||
g += '<rect x="' + lx + '" y="40" width="144" height="250" rx="8" fill="' + laneFill + '"/>';
|
||||
let y = 48;
|
||||
const count = rndInt(rng,2,4);
|
||||
for(let c=0;c<count;c++){
|
||||
const h = rnd(rng,26,44);
|
||||
if(y + h > 282) break;
|
||||
g += '<rect x="' + (lx+8) + '" y="' + f1(y) + '" width="128" height="' + f1(h) + '" rx="5" fill="' + cardFill + '" stroke="' + cardStroke + '"/>';
|
||||
y += h + 8;
|
||||
}
|
||||
}
|
||||
g += "</g>";
|
||||
return g;
|
||||
}
|
||||
|
||||
// ---------- registry ----------
|
||||
const HUES = [
|
||||
{ code:"am", name:"amber", h:38 },
|
||||
{ code:"fo", name:"forest", h:140 },
|
||||
{ code:"sk", name:"sky", h:215 },
|
||||
{ code:"ro", name:"rose", h:335 }
|
||||
];
|
||||
|
||||
const STRATEGIES = [
|
||||
{ id:"mono", hues:function(H){ return [{h:H, w:1}]; } },
|
||||
{ id:"duo", hues:function(H){ return [{h:H, w:0.65},{h:H+180, w:0.35}]; } },
|
||||
{ id:"trio", hues:function(H){ return [{h:H, w:0.5},{h:H+120, w:0.3},{h:H-120, w:0.2}]; } }
|
||||
];
|
||||
|
||||
const DENSITIES = [
|
||||
{ id:"c", label:"coarse", cols:5, rows:3 },
|
||||
{ id:"m", label:"medium", cols:9, rows:6 },
|
||||
{ id:"f", label:"fine", cols:14, rows:9 }
|
||||
];
|
||||
|
||||
// Saturation rows and brightness bases per tone. Rich rows get a touch of
|
||||
// brightness headroom so the saturation actually shows.
|
||||
const TONES = {
|
||||
light: { lJit:4.5, levels:[ {label:"soft", sat:20, lBase:90}, {label:"mid", sat:42, lBase:88}, {label:"rich", sat:68, lBase:85} ] },
|
||||
dark: { lJit:4.5, levels:[ {label:"soft", sat:16, lBase:17}, {label:"mid", sat:34, lBase:19}, {label:"rich", sat:52, lBase:21} ] }
|
||||
};
|
||||
|
||||
// ---------- lightbox ----------
|
||||
let openState = null;
|
||||
const lightbox = document.getElementById("lightbox");
|
||||
const lightboxFrame = document.getElementById("lightbox-frame");
|
||||
const lightboxId = document.getElementById("lightbox-id");
|
||||
|
||||
function openLightbox(svgEl, id){
|
||||
openState = { svgEl:svgEl, parent:svgEl.parentNode, next:svgEl.nextSibling };
|
||||
lightboxFrame.appendChild(svgEl);
|
||||
lightboxId.textContent = id;
|
||||
lightbox.classList.add("open");
|
||||
}
|
||||
function closeLightbox(){
|
||||
if(!openState) return;
|
||||
if(openState.next){
|
||||
openState.parent.insertBefore(openState.svgEl, openState.next);
|
||||
} else {
|
||||
openState.parent.appendChild(openState.svgEl);
|
||||
}
|
||||
openState = null;
|
||||
lightbox.classList.remove("open");
|
||||
}
|
||||
lightbox.addEventListener("click", function(e){
|
||||
if(e.target === lightbox) closeLightbox();
|
||||
});
|
||||
document.addEventListener("keydown", function(e){
|
||||
if(e.key === "Escape") closeLightbox();
|
||||
});
|
||||
|
||||
// ---------- render ----------
|
||||
let gen = 1;
|
||||
|
||||
function renderAll(){
|
||||
closeLightbox();
|
||||
const toneKeys = ["light","dark"];
|
||||
for(let si=0; si<STRATEGIES.length; si++){
|
||||
const strategy = STRATEGIES[si];
|
||||
for(let ti=0; ti<toneKeys.length; ti++){
|
||||
const tone = toneKeys[ti];
|
||||
const toneSpec = TONES[tone];
|
||||
const grid = document.getElementById("grid-" + strategy.id + "-" + tone);
|
||||
grid.innerHTML = "";
|
||||
// rows: hue × saturation; columns: density
|
||||
for(let hi=0; hi<HUES.length; hi++){
|
||||
for(let li=0; li<toneSpec.levels.length; li++){
|
||||
const level = toneSpec.levels[li];
|
||||
for(let di=0; di<DENSITIES.length; di++){
|
||||
const density = DENSITIES[di];
|
||||
const id = "facets-" + HUES[hi].code + "-" + strategy.id + "-" + density.id + "-" + tone.charAt(0) + (li+1);
|
||||
const rng = seededRng(id + "/g" + gen);
|
||||
const spec = { sat:level.sat, lBase:level.lBase, lJit:toneSpec.lJit };
|
||||
const hueList = strategy.hues(HUES[hi].h);
|
||||
const inner = genFacets(rng, hueList, spec, density);
|
||||
const overlay = buildOverlay(rng, tone);
|
||||
const svgMarkup = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 300" data-id="' + id + '">' + inner + overlay + "</svg>";
|
||||
|
||||
const card = document.createElement("div");
|
||||
card.className = "card";
|
||||
|
||||
const frame = document.createElement("div");
|
||||
frame.className = "frame";
|
||||
frame.innerHTML = svgMarkup;
|
||||
const svgEl = frame.firstElementChild;
|
||||
|
||||
const caption = document.createElement("div");
|
||||
caption.className = "caption";
|
||||
const idSpan = document.createElement("span");
|
||||
idSpan.className = "swatch-id";
|
||||
idSpan.textContent = id;
|
||||
const noteSpan = document.createElement("span");
|
||||
noteSpan.className = "note";
|
||||
noteSpan.textContent = HUES[hi].name + " · " + level.label;
|
||||
caption.appendChild(idSpan);
|
||||
caption.appendChild(noteSpan);
|
||||
|
||||
card.appendChild(frame);
|
||||
card.appendChild(caption);
|
||||
card.addEventListener("click", function(){ openLightbox(svgEl, id); });
|
||||
|
||||
grid.appendChild(card);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
document.getElementById("gen-note").textContent = "gen " + gen;
|
||||
}
|
||||
|
||||
// ---------- filters ----------
|
||||
let activeStrategy = "all", activeTone = "all";
|
||||
const strategyChips = document.querySelectorAll("#strategy-chips .chip");
|
||||
const toneChips = document.querySelectorAll("#tone-chips .chip");
|
||||
|
||||
function applyFilters(){
|
||||
const sections = document.querySelectorAll(".family");
|
||||
for(let i=0;i<sections.length;i++){
|
||||
const sec = sections[i];
|
||||
const strat = sec.dataset.strategy, tone = sec.dataset.tone;
|
||||
const visible = (activeStrategy === "all" || activeStrategy === strat) && (activeTone === "all" || activeTone === tone);
|
||||
sec.style.display = visible ? "" : "none";
|
||||
}
|
||||
}
|
||||
strategyChips.forEach(function(chip){
|
||||
chip.addEventListener("click", function(){
|
||||
strategyChips.forEach(function(c){ c.classList.remove("active"); c.setAttribute("aria-pressed","false"); });
|
||||
chip.classList.add("active");
|
||||
chip.setAttribute("aria-pressed","true");
|
||||
activeStrategy = chip.dataset.strategy;
|
||||
applyFilters();
|
||||
});
|
||||
});
|
||||
toneChips.forEach(function(chip){
|
||||
chip.addEventListener("click", function(){
|
||||
toneChips.forEach(function(c){ c.classList.remove("active"); c.setAttribute("aria-pressed","false"); });
|
||||
chip.classList.add("active");
|
||||
chip.setAttribute("aria-pressed","true");
|
||||
activeTone = chip.dataset.tone;
|
||||
applyFilters();
|
||||
});
|
||||
});
|
||||
document.getElementById("overlay-toggle").addEventListener("change", function(e){
|
||||
document.body.classList.toggle("show-overlay", e.target.checked);
|
||||
});
|
||||
document.getElementById("reroll").addEventListener("click", function(){
|
||||
gen++;
|
||||
renderAll();
|
||||
});
|
||||
|
||||
renderAll();
|
||||
})();
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
# Board background image set — exploration notes
|
||||
|
||||
Status: **first sweep published, awaiting swatch review** (2026-08-07). Not a numbered design doc — this is working material for producing a set of bundled background images; the schema and rendering side is already settled in 03-board-ui.md § Styling ▸ Capabilities.
|
||||
Status: **sweep 1 + faceted gallery published, awaiting swatch review** (2026-08-07). Not a numbered design doc — this is working material for producing a set of bundled background images; the schema and rendering side is already settled in 03-board-ui.md § Styling ▸ Capabilities.
|
||||
|
||||
## What this is
|
||||
|
||||
@@ -47,6 +47,24 @@ Families (1–9 light, 10–12 dark):
|
||||
|
||||
Rendering constraints baked into the generator (keep for sweep 2): no per-swatch SVG filters (all glow via radial gradients fading to alpha 0 — 96 filters would crawl), ≤~120 filler elements per swatch, gradient defs namespaced by swatch ID, light grounds ≥82% lightness / dark ≤20%.
|
||||
|
||||
## Faceted gallery — user-designed originals (2026-08-07)
|
||||
|
||||
Separate from the sweep-1 axes model: patterns designed to the user's own criteria — canvas tiled by shapes, per-shape **brightness jitter within a narrow band** doing the drawing, base brightness set by light/dark tone. Geometry is random on every render (that's part of the concept), so the gallery has a **Reroll** button; a swatch ID names the recipe, not a fixed layout.
|
||||
|
||||
- **Artifact**: https://claude.ai/code/artifact/d27ad77f-9298-4cf1-9093-19d13a186752 (republished in place each round)
|
||||
- **Generator source**: `board-backgrounds-faceted.html` (this folder) — same seeded-PRNG + board-overlay-toggle infrastructure as sweep 1; seed = swatch ID + generation counter.
|
||||
- **facets**: grid-jittered dot scatter over an extended canvas → Bowyer–Watson Delaunay triangulation, each triangle stroked with its own fill color to kill antialiasing seams. Reference image: low-poly example on the Redesign board.
|
||||
- **bubbles**: same color logic on ~64 opaque circles, power-law radii, drawn large-first. **Set aside after round 1** ("we'll need more work on that") — round-1 recipe kept in git history of the generator file.
|
||||
|
||||
**Round 2 (current, facets only)** — axes per the user: vertex count, color count, tone, saturation; brightness stays the narrow in-image jitter. 216 swatches = 3 color strategies × 2 tones × 4 hues × 3 saturations × 3 densities. Hue wheel narrowed to 4 representatives (amber/forest/sky/rose) to keep the cross reviewable; full wheel returns for finals.
|
||||
|
||||
- Densities (columns): coarse 5×3 cells ≈20 triangles · medium 9×6 ≈97 · fine 14×9 ≈230.
|
||||
- Color strategies (sections): mono · duo = complement H+180° weighted 65/35 · trio = triad H±120° weighted 50/30/20, hue picked per triangle.
|
||||
- IDs `facets-<hue>-<strategy>-<density>-<tone><sat>`, e.g. `facets-sk-duo-f-l2` = sky duo fine light mid.
|
||||
- Jitters: hue ±3°, sat ±15%, brightness ±4.5 L around tone base (light 85–90, dark 17–21, rich rows get slight headroom shifts).
|
||||
|
||||
**Shipped in-app (2026-08-07)**: the recipe is live in the board popover's Background tab (03-board-ui.md § Board popover ▸ Background tab) — Swift port in `Kanban/UI/Board/Backgrounds/`, rendered to a static `facets.png` at pick time (never a live view — the perf/sync ruling). One deliberate deviation from the gallery: the scatter grid gained a sacrificial boundary ring per density (coarse 7×5 @ margin 0.305, medium 10×7 @ 0.14, fine 15×10 @ 0.12 — interior cell size unchanged) because the gallery's fixed 0.075 margin let the ground notch the frame edge (coarse by up to 0.163 of the width — visible flat borders on the reviewed coarse swatches; medium 0.044, fine 0.004). Full-bleed coverage is now a tested invariant (`margin ≥ 0.92 × cell` both axes). Packaging question from sweep 1 is thereby answered for facets: generated on demand, not bundled.
|
||||
|
||||
## Next steps
|
||||
|
||||
1. Review with overlay on; pick surviving families / specific IDs and direction tweaks ("aurora but duskier", "contours denser").
|
||||
|
||||
Reference in New Issue
Block a user