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:
@@ -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