Files
lanework/DESIGN/explorations/board-backgrounds-gallery.html
T
rzen ccf55f1d49 The backgrounds get a swatch gallery — sweep one of the bundled artwork exploration
DESIGN/explorations/board-backgrounds: working material, not a numbered
design doc — the app side of background images is settled, so the
deliverable is purely artwork. The model is base layer × filler layer,
swept across hue strategy, HSB family, filler shape, density, placement
and opacity; the gallery is one self-contained HTML generator, every
swatch procedural SVG off a seeded PRNG so swatch IDs stay stable
references across reloads. Status: first sweep published, awaiting review.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-07 11:24:10 -04:00

756 lines
31 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.
<title>Board Background Swatches</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; }
.page-head .subtitle{ font-size:13px; color:var(--ink-2); margin:0; }
.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; }
.section-label{ font-size:11px; font-weight:600; letter-spacing:.06em; text-transform:uppercase; color:var(--ink-2); margin:0 0 10px; }
.axes{ margin-bottom:40px; }
.axes-list{ display:grid; grid-template-columns:1fr 1fr; gap:7px 32px; margin:0; }
.axis{ display:flex; gap:6px; font-size:13px; line-height:1.5; }
.axis dt{ margin:0; font-weight:600; color:var(--ink-2); flex:0 0 auto; }
.axis dd{ margin:0; color:var(--ink-2); }
@media (max-width:720px){ .axes-list{ grid-template-columns:1fr; } }
.family{ margin-bottom:40px; }
.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; }
.grid{ display:grid; grid-template-columns:repeat(auto-fill, minmax(260px,1fr)); gap:14px; }
.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 Background Swatches</h1>
<p class="subtitle">96 procedural candidates — base layer × filler layer. Toggle the board overlay to judge them under content.</p>
</header>
<div class="controlbar">
<div class="chipgroup" id="family-chips" role="group" aria-label="Family filter">
<button type="button" class="chip active" data-family="all" aria-pressed="true">All</button>
<button type="button" class="chip" data-family="whisper" aria-pressed="false">Whisper</button>
<button type="button" class="chip" data-family="confetti" aria-pressed="false">Confetti</button>
<button type="button" class="chip" data-family="aurora" aria-pressed="false">Aurora</button>
<button type="button" class="chip" data-family="contours" aria-pressed="false">Contours</button>
<button type="button" class="chip" data-family="drift" aria-pressed="false">Drift</button>
<button type="button" class="chip" data-family="bubbles" aria-pressed="false">Bubbles</button>
<button type="button" class="chip" data-family="terrazzo" aria-pressed="false">Terrazzo</button>
<button type="button" class="chip" data-family="graph" aria-pressed="false">Graph</button>
<button type="button" class="chip" data-family="waves" aria-pressed="false">Waves</button>
<button type="button" class="chip" data-family="dusk" aria-pressed="false">Dusk</button>
<button type="button" class="chip" data-family="starfield" aria-pressed="false">Starfield</button>
<button type="button" class="chip" data-family="slate" aria-pressed="false">Slate</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>
</div>
<section class="axes">
<p class="section-label">Axes</p>
<dl class="axes-list">
<div class="axis"><dt>Base</dt><dd>solid · linear 2/3-stop (angle) · radial/corner glow · soft-blob mesh</dd></div>
<div class="axis"><dt>Hue strategy</dt><dd>mono · analogous · complementary accent · multicolor</dd></div>
<div class="axis"><dt>HSB family</dt><dd>pastel · muted · deep/dark</dd></div>
<div class="axis"><dt>Filler shape</dt><dd>dots · rings · blobs/chips · capsules · triangles · plus-signs · contour lines · sine bands · grid</dd></div>
<div class="axis"><dt>Density</dt><dd>sparse → dense</dd></div>
<div class="axis"><dt>Size</dt><dd>uniform vs power-law</dd></div>
<div class="axis"><dt>Placement</dt><dd>uniform scatter · diagonal band · corner-weighted · grid-jitter</dd></div>
<div class="axis"><dt>Filler color</dt><dd>same-hue tint · accent hue · multicolor · alpha-only</dd></div>
<div class="axis"><dt>Opacity</dt><dd>whisper 410% → visible 1525%</dd></div>
<div class="axis"><dt>Depth</dt><dd>crisp vs gradient-soft, layered sizes</dd></div>
</dl>
</section>
<section class="family" data-family="whisper" data-tone="light">
<h2>Whisper</h2>
<p class="recipe">A vertical pastel gradient ground carries roughly sixty faint dots at 610% opacity — the quietest family in the set.</p>
<div class="grid" id="grid-whisper"></div>
</section>
<section class="family" data-family="confetti" data-tone="light">
<h2>Confetti</h2>
<p class="recipe">A near-neutral ground is scattered with about seventy tiny circles, rotated squares and triangles across three related hues.</p>
<div class="grid" id="grid-confetti"></div>
</section>
<section class="family" data-family="aurora" data-tone="light">
<h2>Aurora</h2>
<p class="recipe">A flat pastel ground holds four to six large soft-edged gradient blobs that overlap and drift toward one corner, mesh-gradient style.</p>
<div class="grid" id="grid-aurora"></div>
</section>
<section class="family" data-family="contours" data-tone="light">
<h2>Contours</h2>
<p class="recipe">A flat ground is crossed by fourteen thin wavy horizontal lines built from summed sine harmonics, evoking topographic contours.</p>
<div class="grid" id="grid-contours"></div>
</section>
<section class="family" data-family="drift" data-tone="light">
<h2>Drift</h2>
<p class="recipe">A diagonal two-tone gradient ground carries rotated capsule shapes rejection-sampled into a diagonal band across the canvas.</p>
<div class="grid" id="grid-drift"></div>
</section>
<section class="family" data-family="bubbles" data-tone="light">
<h2>Bubbles</h2>
<p class="recipe">A top-left radial gradient ground carries a power-law scatter of circles — a few large, more medium, many small — mixing filled discs with stroked rings.</p>
<div class="grid" id="grid-bubbles"></div>
</section>
<section class="family" data-family="terrazzo" data-tone="light">
<h2>Terrazzo</h2>
<p class="recipe">A near-neutral ground is covered in about fifty-five small irregular polygon chips across four related colors, like stone terrazzo.</p>
<div class="grid" id="grid-terrazzo"></div>
</section>
<section class="family" data-family="graph" data-tone="light">
<h2>Graph</h2>
<p class="recipe">A pale ground is ruled into a 24px grid, with a few plus-mark intersections and small accent dots — graph paper.</p>
<div class="grid" id="grid-graph"></div>
</section>
<section class="family" data-family="waves" data-tone="light">
<h2>Waves</h2>
<p class="recipe">A horizontal gradient ground is layered with six translucent sine-wave bands, phase-shifted and stacked toward the bottom third like dunes.</p>
<div class="grid" id="grid-waves"></div>
</section>
<section class="family" data-family="dusk" data-tone="dark">
<h2>Dusk</h2>
<p class="recipe">A deep vertical gradient ground holds large soft gradient blobs and a faint horizon glow low in the frame — moody, no small shapes.</p>
<div class="grid" id="grid-dusk"></div>
</section>
<section class="family" data-family="starfield" data-tone="dark">
<h2>Starfield</h2>
<p class="recipe">A dark radial ground is scattered with about ninety tiny stars, a handful haloed and brighter, plus two faint nebula blobs.</p>
<div class="grid" id="grid-starfield"></div>
</section>
<section class="family" data-family="slate" data-tone="dark">
<h2>Slate</h2>
<p class="recipe">A flat dark ground carries the same wavy contour lines as Contours, with small accent dots resting on the lines — dark topographic.</p>
<div class="grid" id="grid-slate"></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 ----------
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){
const seed = xmur3(id)();
return mulberry32(seed);
}
// ---------- small helpers ----------
function mod360(h){ return ((h % 360) + 360) % 360; }
function hsl(h,s,l){ return "hsl(" + mod360(h) + "," + s + "%," + l + "%)"; }
function rnd(rng,min,max){ return min + rng() * (max - min); }
function rndInt(rng,min,max){ return Math.floor(rnd(rng,min,max+1)); }
function pick(rng,arr){ return arr[Math.floor(rng()*arr.length)]; }
function pad2(n){ return String(n).padStart(2,"0"); }
function f1(n){ return n.toFixed(1); }
function linGrad(id,x1,y1,x2,y2,stops){
let s = "";
for(let i=0;i<stops.length;i++){
s += '<stop offset="' + stops[i][0] + '" stop-color="' + stops[i][1] + '"/>';
}
return '<linearGradient id="' + id + '" x1="' + x1 + '" y1="' + y1 + '" x2="' + x2 + '" y2="' + y2 + '">' + s + '</linearGradient>';
}
function radGrad(id,cx,cy,r,stops){
let s = "";
for(let i=0;i<stops.length;i++){
const st = stops[i];
const op = st.length > 2 ? st[2] : 1;
s += '<stop offset="' + st[0] + '" stop-color="' + st[1] + '" stop-opacity="' + op + '"/>';
}
return '<radialGradient id="' + id + '" cx="' + cx + '" cy="' + cy + '" r="' + r + '">' + s + '</radialGradient>';
}
function triPoints(cx,cy,size,rotDeg){
const r = size * 0.7;
const rot = rotDeg * Math.PI / 180;
const pts = [];
for(let k=0;k<3;k++){
const a = rot + k*(Math.PI*2/3) - Math.PI/2;
pts.push(f1(cx + r*Math.cos(a)) + "," + f1(cy + r*Math.sin(a)));
}
return pts.join(" ");
}
// shared wavy-contour-line builder, used by "contours" and "slate"
function contourLines(rng,count,spacing,startY,stroke,opacity,widthAttr){
let out = "";
const allPoints = [];
for(let i=0;i<count;i++){
const baseY = startY + i*spacing;
const harmonics = rndInt(rng,2,3);
const comps = [];
let wsum = 0;
for(let k=0;k<harmonics;k++){
const c = { freq: rnd(rng,1,3), phase: rnd(rng,0,Math.PI*2), weight: rnd(rng,0.3,1) };
comps.push(c);
wsum += c.weight;
}
const amp = rnd(rng,6,14);
const pts = [];
for(let x=0;x<=480;x+=8){
let s = 0;
for(let k=0;k<comps.length;k++){
const c = comps[k];
s += c.weight * Math.sin(c.freq*(x/480)*Math.PI*2 + c.phase);
}
const y = baseY + amp*(s/wsum);
pts.push([x,y]);
}
allPoints.push(pts);
let d = "";
for(let p=0;p<pts.length;p++){
d += (p===0 ? "M" : "L") + f1(pts[p][0]) + "," + f1(pts[p][1]) + " ";
}
out += '<path d="' + d + '" stroke="' + stroke + '" stroke-width="' + widthAttr + '" fill="none" opacity="' + opacity + '"/>';
}
return { svg: out, lines: allPoints };
}
// ---------- board overlay (shared across all families) ----------
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;
}
// ---------- 12 families ----------
function famWhisper(rng,H,gid){
const baseId = gid + "-base";
let s = "<defs>" + linGrad(baseId,"0","0","0","1",[["0%",hsl(H,45,94)],["100%",hsl(H,40,88)]]) + "</defs>";
s += '<rect width="480" height="300" fill="url(#' + baseId + ')"/>';
let filler = "";
for(let i=0;i<60;i++){
const cx = rnd(rng,0,480), cy = rnd(rng,0,300), r = rnd(rng,2,5), op = rnd(rng,0.06,0.10);
filler += '<circle cx="' + f1(cx) + '" cy="' + f1(cy) + '" r="' + f1(r) + '" fill="' + hsl(H,50,55) + '" opacity="' + op.toFixed(2) + '"/>';
}
return s + filler;
}
function famConfetti(rng,H,gid){
let s = '<rect width="480" height="300" fill="' + hsl(H,12,95) + '"/>';
const hues = [H, H+120, H+240];
let filler = "";
for(let i=0;i<70;i++){
const type = pick(rng,["circle","square","triangle"]);
const hue = pick(rng,hues);
const color = hsl(hue,55,55);
const size = rnd(rng,3,8);
const x = rnd(rng,0,480), y = rnd(rng,0,300);
const op = rnd(rng,0.10,0.16).toFixed(2);
if(type === "circle"){
filler += '<circle cx="' + f1(x) + '" cy="' + f1(y) + '" r="' + f1(size/2) + '" fill="' + color + '" opacity="' + op + '"/>';
} else if(type === "square"){
const rot = rnd(rng,0,360).toFixed(1);
filler += '<rect x="' + f1(x-size/2) + '" y="' + f1(y-size/2) + '" width="' + f1(size) + '" height="' + f1(size) + '" fill="' + color + '" opacity="' + op + '" transform="rotate(' + rot + ' ' + f1(x) + ' ' + f1(y) + ')"/>';
} else {
const rot = rnd(rng,0,360);
filler += '<polygon points="' + triPoints(x,y,size,rot) + '" fill="' + color + '" opacity="' + op + '"/>';
}
}
return s + filler;
}
function famAurora(rng,H,gid){
const corners = [[0,0],[480,0],[0,300],[480,300]];
const corner = pick(rng,corners);
const n = rndInt(rng,4,6);
let defs = "", circles = "";
for(let i=0;i<n;i++){
const r = rnd(rng,90,180);
const rx = rnd(rng,0,480), ry = rnd(rng,0,300);
const t = rnd(rng,0.3,0.7);
const cx = rx + (corner[0]-rx)*t;
const cy = ry + (corner[1]-ry)*t;
const offset = pick(rng,[-25,25]);
const gidL = gid + "-aur-" + i;
defs += radGrad(gidL,"50%","50%","50%",[["0%",hsl(H+offset,55,80),0.55],["100%",hsl(H+offset,55,80),0]]);
circles += '<circle cx="' + f1(cx) + '" cy="' + f1(cy) + '" r="' + f1(r) + '" fill="url(#' + gidL + ')"/>';
}
let s = "<defs>" + defs + "</defs>";
s += '<rect width="480" height="300" fill="' + hsl(H,35,93) + '"/>';
return s + circles;
}
function famContours(rng,H,gid){
let s = '<rect width="480" height="300" fill="' + hsl(H,30,92) + '"/>';
const res = contourLines(rng,14,22,6,hsl(H,45,60),0.14,1.2);
return s + res.svg;
}
function famDrift(rng,H,gid){
const baseId = gid + "-base";
let s = "<defs>" + linGrad(baseId,"0","0","1","1",[["0%",hsl(H,40,93)],["100%",hsl(H+30,40,89)]]) + "</defs>";
s += '<rect width="480" height="300" fill="url(#' + baseId + ')"/>';
const a = 300, b = -480;
const denom = Math.sqrt(a*a + b*b);
let count = 0, tries = 0, filler = "";
while(count < 40 && tries < 2000){
tries++;
const x = rnd(rng,0,480), y = rnd(rng,0,300);
const dist = Math.abs(a*x + b*y) / denom;
if(dist < 90){
const len = rnd(rng,20,70), thick = rnd(rng,4,7);
const op = rnd(rng,0.08,0.14).toFixed(2);
filler += '<rect x="' + f1(-len/2) + '" y="' + f1(-thick/2) + '" width="' + f1(len) + '" height="' + f1(thick) + '" rx="' + f1(thick/2) + '" fill="' + hsl(H,45,60) + '" opacity="' + op + '" transform="translate(' + f1(x) + ' ' + f1(y) + ') rotate(-32)"/>';
count++;
}
}
return s + filler;
}
function famBubbles(rng,H,gid){
const baseId = gid + "-base";
let s = "<defs>" + radGrad(baseId,"0%","0%","100%",[["0%",hsl(H,45,95)],["100%",hsl(H,40,88)]]) + "</defs>";
s += '<rect width="480" height="300" fill="url(#' + baseId + ')"/>';
const groups = [[5,40,80,0.05],[15,12,30,0.08],[40,2,8,0.12]];
let filler = "";
for(let gi=0; gi<groups.length; gi++){
const n = groups[gi][0], rmin = groups[gi][1], rmax = groups[gi][2], op = groups[gi][3];
for(let i=0;i<n;i++){
const cx = rnd(rng,0,480), cy = rnd(rng,0,300), r = rnd(rng,rmin,rmax);
const ring = rng() < 0.4;
if(ring){
filler += '<circle cx="' + f1(cx) + '" cy="' + f1(cy) + '" r="' + f1(r) + '" fill="none" stroke="' + hsl(H,50,55) + '" stroke-width="1.5" opacity="' + op + '"/>';
} else {
filler += '<circle cx="' + f1(cx) + '" cy="' + f1(cy) + '" r="' + f1(r) + '" fill="' + hsl(H,50,55) + '" opacity="' + op + '"/>';
}
}
}
return s + filler;
}
function famTerrazzo(rng,H,gid){
let s = '<rect width="480" height="300" fill="' + hsl(H,10,94) + '"/>';
const colors = [hsl(H,45,60), hsl(H+40,40,55), hsl(H-40,40,50), hsl(40,8,55)];
let filler = "";
for(let i=0;i<55;i++){
const cx = rnd(rng,0,480), cy = rnd(rng,0,300);
const sides = rndInt(rng,5,7);
const rot = rnd(rng,0,Math.PI*2);
const color = pick(rng,colors);
const pts = [];
for(let k=0;k<sides;k++){
const ang = rot + k*(Math.PI*2/sides);
const r = rnd(rng,4,14);
pts.push(f1(cx + r*Math.cos(ang)) + "," + f1(cy + r*Math.sin(ang)));
}
filler += '<polygon points="' + pts.join(" ") + '" fill="' + color + '" opacity="0.18"/>';
}
return s + filler;
}
function famGraph(rng,H,gid){
let s = '<rect width="480" height="300" fill="' + hsl(H,25,96) + '"/>';
const pitch = 24;
const cols = Math.floor(480/pitch);
const rows = Math.floor(300/pitch);
let grid = "";
for(let c=0;c<=cols;c++){
const x = c*pitch;
grid += '<line x1="' + x + '" y1="0" x2="' + x + '" y2="300" stroke="' + hsl(H,35,65) + '" stroke-width="1" opacity="0.10"/>';
}
for(let r=0;r<=rows;r++){
const y = r*pitch;
grid += '<line x1="0" y1="' + y + '" x2="480" y2="' + y + '" stroke="' + hsl(H,35,65) + '" stroke-width="1" opacity="0.10"/>';
}
let plus = "";
for(let i=0;i<12;i++){
const gx = rndInt(rng,0,cols)*pitch, gy = rndInt(rng,0,rows)*pitch;
plus += '<line x1="' + (gx-4) + '" y1="' + gy + '" x2="' + (gx+4) + '" y2="' + gy + '" stroke="' + hsl(H,35,65) + '" stroke-width="1" opacity="0.25"/>';
plus += '<line x1="' + gx + '" y1="' + (gy-4) + '" x2="' + gx + '" y2="' + (gy+4) + '" stroke="' + hsl(H,35,65) + '" stroke-width="1" opacity="0.25"/>';
}
let dots = "";
const nd = rndInt(rng,3,4);
for(let i=0;i<nd;i++){
const gx = rndInt(rng,0,cols)*pitch, gy = rndInt(rng,0,rows)*pitch;
dots += '<circle cx="' + gx + '" cy="' + gy + '" r="3" fill="' + hsl(H,55,55) + '" opacity="0.35"/>';
}
return s + grid + plus + dots;
}
function famWaves(rng,H,gid){
const baseId = gid + "-base";
let s = "<defs>" + linGrad(baseId,"0","0","1","0",[["0%",hsl(H,38,94)],["100%",hsl(H,34,89)]]) + "</defs>";
s += '<rect width="480" height="300" fill="url(#' + baseId + ')"/>';
let filler = "";
for(let i=0;i<6;i++){
const baseline = 190 + i*18;
const amp = rnd(rng,10,24);
const freq = rnd(rng,1,2);
const phase = rnd(rng,0,Math.PI*2);
let d = "M0,300 ";
for(let x=0;x<=480;x+=12){
const y = baseline + amp*Math.sin(freq*(x/480)*Math.PI*2 + phase);
d += "L" + x + "," + f1(y) + " ";
}
d += "L480,300 Z";
filler += '<path d="' + d + '" fill="' + hsl(H,40,80) + '" opacity="0.10"/>';
}
return s + filler;
}
function famDusk(rng,H,gid){
const baseId = gid + "-base";
let defs = linGrad(baseId,"0","0","0","1",[["0%",hsl(H,35,16)],["100%",hsl(H,45,9)]]);
const n = rndInt(rng,4,5);
let blobs = "";
for(let i=0;i<n;i++){
const r = rnd(rng,90,180);
const cx = rnd(rng,0,480), cy = rnd(rng,0,220);
const offset = pick(rng,[-30,30]);
const gidL = gid + "-dusk-" + i;
defs += radGrad(gidL,"50%","50%","50%",[["0%",hsl(H+offset,55,35),0.5],["100%",hsl(H+offset,55,35),0]]);
blobs += '<circle cx="' + f1(cx) + '" cy="' + f1(cy) + '" r="' + f1(r) + '" fill="url(#' + gidL + ')"/>';
}
const horizonId = gid + "-horizon";
defs += radGrad(horizonId,"50%","50%","50%",[["0%",hsl(H,50,30),0.45],["100%",hsl(H,50,30),0]]);
const horizon = '<ellipse cx="240" cy="270" rx="320" ry="70" fill="url(#' + horizonId + ')"/>';
return "<defs>" + defs + "</defs>" + '<rect width="480" height="300" fill="url(#' + baseId + ')"/>' + blobs + horizon;
}
function famStarfield(rng,H,gid){
const baseId = gid + "-base";
let defs = radGrad(baseId,"50%","50%","75%",[["0%",hsl(H,40,14)],["100%",hsl(H,45,7)]]);
let nebula = "";
for(let i=0;i<2;i++){
const cx = rnd(rng,0,480), cy = rnd(rng,0,300), r = rnd(rng,120,200);
const nId = gid + "-neb-" + i;
defs += radGrad(nId,"50%","50%","50%",[["0%",hsl(H+40,50,30),0.3],["100%",hsl(H+40,50,30),0]]);
nebula += '<circle cx="' + f1(cx) + '" cy="' + f1(cy) + '" r="' + f1(r) + '" fill="url(#' + nId + ')"/>';
}
const brightIdx = new Set();
while(brightIdx.size < 8){ brightIdx.add(rndInt(rng,0,89)); }
let stars = "";
for(let i=0;i<90;i++){
const cx = rnd(rng,0,480), cy = rnd(rng,0,300);
if(brightIdx.has(i)){
const haloId = gid + "-star-" + i;
defs += radGrad(haloId,"50%","50%","50%",[["0%",hsl(H,30,88),0.9],["100%",hsl(H,30,88),0]]);
const r = rnd(rng,0.6,1.8);
stars += '<circle cx="' + f1(cx) + '" cy="' + f1(cy) + '" r="6" fill="url(#' + haloId + ')"/>';
stars += '<circle cx="' + f1(cx) + '" cy="' + f1(cy) + '" r="' + f1(r) + '" fill="' + hsl(H,30,88) + '" opacity="0.9"/>';
} else {
const r = rnd(rng,0.6,1.8);
const op = rnd(rng,0.25,0.5).toFixed(2);
stars += '<circle cx="' + f1(cx) + '" cy="' + f1(cy) + '" r="' + f1(r) + '" fill="' + hsl(H,30,88) + '" opacity="' + op + '"/>';
}
}
return "<defs>" + defs + "</defs>" + '<rect width="480" height="300" fill="url(#' + baseId + ')"/>' + nebula + stars;
}
function famSlate(rng,H,gid){
let s = '<rect width="480" height="300" fill="' + hsl(H,18,13) + '"/>';
const res = contourLines(rng,14,22,6,hsl(H,35,55),0.12,1.2);
s += res.svg;
let dots = "";
for(let i=0;i<10;i++){
const li = rndInt(rng,0,res.lines.length-1);
const line = res.lines[li];
const pi = rndInt(rng,0,line.length-1);
const pt = line[pi];
const r = rnd(rng,2,3);
dots += '<circle cx="' + f1(pt[0]) + '" cy="' + f1(pt[1]) + '" r="' + f1(r) + '" fill="' + hsl(H,60,60) + '" opacity="0.5"/>';
}
return s + dots;
}
// ---------- registry ----------
const HUES = [
{ name:"clay", h:8 },
{ name:"amber", h:38 },
{ name:"olive", h:80 },
{ name:"forest", h:140 },
{ name:"teal", h:175 },
{ name:"sky", h:215 },
{ name:"iris", h:262 },
{ name:"rose", h:335 }
];
const FAMILIES = [
{ id:"whisper", tone:"light", gen:famWhisper },
{ id:"confetti", tone:"light", gen:famConfetti },
{ id:"aurora", tone:"light", gen:famAurora },
{ id:"contours", tone:"light", gen:famContours },
{ id:"drift", tone:"light", gen:famDrift },
{ id:"bubbles", tone:"light", gen:famBubbles },
{ id:"terrazzo", tone:"light", gen:famTerrazzo },
{ id:"graph", tone:"light", gen:famGraph },
{ id:"waves", tone:"light", gen:famWaves },
{ id:"dusk", tone:"dark", gen:famDusk },
{ id:"starfield", tone:"dark", gen:famStarfield },
{ id:"slate", tone:"dark", gen:famSlate }
];
// ---------- render ----------
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();
});
let renderedCount = 0;
function renderAll(){
for(let fi=0; fi<FAMILIES.length; fi++){
const fam = FAMILIES[fi];
const grid = document.getElementById("grid-" + fam.id);
for(let hi=0; hi<HUES.length; hi++){
const hue = HUES[hi];
const id = fam.id + "-" + pad2(hi+1);
const rng = seededRng(id);
const inner = fam.gen(rng, hue.h, id);
const overlay = buildOverlay(rng, fam.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";
card.dataset.family = fam.id;
card.dataset.tone = fam.tone;
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 = hue.name;
caption.appendChild(idSpan);
caption.appendChild(noteSpan);
card.appendChild(frame);
card.appendChild(caption);
card.addEventListener("click", function(){ openLightbox(svgEl, id); });
grid.appendChild(card);
renderedCount++;
}
}
}
// ---------- filters ----------
let activeFamily = "all", activeTone = "all";
const familyChips = document.querySelectorAll("#family-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 fam = sec.dataset.family, tone = sec.dataset.tone;
const visible = (activeFamily === "all" || activeFamily === fam) && (activeTone === "all" || activeTone === tone);
sec.style.display = visible ? "" : "none";
}
}
familyChips.forEach(function(chip){
chip.addEventListener("click", function(){
familyChips.forEach(function(c){ c.classList.remove("active"); c.setAttribute("aria-pressed","false"); });
chip.classList.add("active");
chip.setAttribute("aria-pressed","true");
activeFamily = chip.dataset.family;
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);
});
renderAll();
})();
</script>