Files
lanework/DESIGN/explorations/board-backgrounds-faceted.html
T

543 lines
22 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 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 ≈8590%, dark ≈1721%) ±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°, H120°) 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 (BowyerWatson) ----------
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>