543 lines
22 KiB
HTML
543 lines
22 KiB
HTML
<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>
|