polish: bolder placeholder art so thumbnails read at small size

Brighter backgrounds, thicker strokes, and large tonal regions (backlight
pool, lit doorway, face fill, water band) so the seed art is legible as
thumbnails instead of near-black cards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
slaguru666
2026-07-21 13:18:52 +01:00
co-authored by Claude Opus 4.8
parent 3bd85dd9c7
commit d430ee9574
6 changed files with 42 additions and 55 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 46 KiB

+42 -55
View File
@@ -1,6 +1,6 @@
// Reproducible B&W placeholder art for the seed library (the 5 entries in
// src/art/manifest.js). Pure Node — hand-rolled PNG encoder + analytic AA,
// atmospheric charcoal motifs. Swap for real pencil art anytime.
// src/art/manifest.js). Pure Node — hand-rolled PNG encoder + analytic AA.
// Bold charcoal motifs with large tonal regions so they read at thumbnail size.
// node tools/gen-art-placeholders.mjs
import { deflateSync } from 'node:zlib';
import { mkdirSync, writeFileSync } from 'node:fs';
@@ -25,78 +25,65 @@ const ss = (e0, e1, x) => { const t = clamp((x - e0) / (e1 - e0), 0, 1); return
function rng(seed) { let s = seed >>> 0; return () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; }; }
function distSeg(px, py, ax, ay, bx, by) { const dx = bx - ax, dy = by - ay, l2 = dx * dx + dy * dy; let t = l2 ? ((px - ax) * dx + (py - ay) * dy) / l2 : 0; t = clamp(t, 0, 1); return Math.hypot(px - (ax + t * dx), py - (ay + t * dy)); }
const AA = 1.1;
const LIGHT = [206, 214, 222], DIM = [150, 160, 172], BRIGHT = [232, 238, 244], AMBER = [232, 200, 150];
const AA = 1.2;
const LIGHT = [222, 230, 238], DIM = [178, 188, 202], BRIGHT = [242, 246, 252], AMBER = [242, 208, 150], DARK = [9, 11, 15];
function canvas() {
const buf = Buffer.alloc(W * H * 4);
const api = {
return {
buf,
bg(topRGB, botRGB) { for (let y = 0; y < H; y++) { const t = y / (H - 1); const c = [lerp(topRGB[0], botRGB[0], t), lerp(topRGB[1], botRGB[1], t), lerp(topRGB[2], botRGB[2], t)]; for (let x = 0; x < W; x++) { const i = (y * W + x) * 4; buf[i] = c[0]; buf[i + 1] = c[1]; buf[i + 2] = c[2]; buf[i + 3] = 255; } } },
each(fn) { for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) { const i = (y * W + x) * 4; const a = fn(x + 0.5, y + 0.5); if (!a) continue; const [col, alpha] = a; if (alpha <= 0) continue; buf[i] = lerp(buf[i], col[0], alpha); buf[i + 1] = lerp(buf[i + 1], col[1], alpha); buf[i + 2] = lerp(buf[i + 2], col[2], alpha); } },
bg(top, bot) { for (let y = 0; y < H; y++) { const t = y / (H - 1); const c = [lerp(top[0], bot[0], t), lerp(top[1], bot[1], t), lerp(top[2], bot[2], t)]; for (let x = 0; x < W; x++) { const i = (y * W + x) * 4; buf[i] = c[0]; buf[i + 1] = c[1]; buf[i + 2] = c[2]; buf[i + 3] = 255; } } },
each(fn) { for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) { const r = fn(x + 0.5, y + 0.5); if (!r) continue; const [col, a] = r; if (!(a > 0)) continue; const i = (y * W + x) * 4; buf[i] = lerp(buf[i], col[0], a); buf[i + 1] = lerp(buf[i + 1], col[1], a); buf[i + 2] = lerp(buf[i + 2], col[2], a); } },
};
return api;
}
const segCov = (px, py, a, hw) => (col, alpha) => [col, (1 - ss(hw - AA, hw + AA, distSeg(px, py, a[0], a[1], a[2], a[3]))) * alpha];
const discCov = (px, py, cx, cy, r) => 1 - ss(r - AA, r + AA, Math.hypot(px - cx, py - cy));
const seg = (px, py, a, hw, col, alpha) => [col, (1 - ss(hw - AA, hw + AA, distSeg(px, py, a[0], a[1], a[2], a[3]))) * alpha];
const disc = (px, py, cx, cy, r) => 1 - ss(r - AA, r + AA, Math.hypot(px - cx, py - cy));
const scenes = {
// Detective in alley — converging walls, backlit figure.
'detective-alley'(c) {
c.bg([24, 27, 32], [12, 15, 20]);
c.each((x, y) => {
let a = 0, col = LIGHT;
const halo = Math.max(0, 1 - Math.hypot(x - 240, y - 500) / 150) * 0.4; // backlight
a = halo; col = [90, 96, 104];
return [col, a];
});
c.each((x, y) => segCov(x, y, [40, H, 232, 175], 2)(DIM, 0.6)); // left perspective edge
c.each((x, y) => segCov(x, y, [440, H, 248, 175], 2)(DIM, 0.6)); // right perspective edge
c.each((x, y) => segCov(x, y, [90, 560, 390, 560], 2)(DIM, 0.5)); // ground
c.each((x, y) => { // figure silhouette: head + tapered body
const head = discCov(x, y, 240, 452, 16);
const halfW = lerp(20, 30, ss(470, 560, y)); // shoulders widen downward
const body = (1 - ss(halfW - AA, halfW + AA, Math.abs(x - 240))) * ss(468, 472, y) * (1 - ss(556, 560, y));
return [[8, 9, 12], Math.max(head, body)];
});
c.bg([44, 49, 58], [15, 19, 25]);
c.each((x, y) => { const g = Math.max(0, 1 - Math.hypot((x - 240) / 155, (y - 505) / 150)); return [[132, 140, 152], Math.pow(g, 1.4) * 0.75]; }); // backlight pool
c.each((x, y) => seg(x, y, [28, H, 236, 168], 3.5, LIGHT, 0.9));
c.each((x, y) => seg(x, y, [452, H, 244, 168], 3.5, LIGHT, 0.9));
c.each((x, y) => seg(x, y, [78, 566, 402, 566], 3, LIGHT, 0.75));
c.each((x, y) => { const head = disc(x, y, 240, 446, 21); const hw = lerp(27, 42, ss(466, 566, y)); const body = (1 - ss(hw - AA, hw + AA, Math.abs(x - 240))) * ss(462, 466, y) * (1 - ss(560, 564, y)); return [DARK, Math.max(head, body) * 0.96]; });
},
// Neon street — vertical light bars + reflections.
'neon-street'(c) {
c.bg([20, 22, 30], [10, 12, 18]);
const bars = [[80, 70, 300, LIGHT], [150, 110, 330, AMBER], [300, 60, 280, BRIGHT], [372, 130, 340, LIGHT], [420, 90, 300, AMBER]];
for (const [bx, y0, y1, col] of bars) {
c.each((x, y) => segCov(x, y, [bx, y0, bx, y1], 4)(col, 0.9));
c.each((x, y) => segCov(x, y, [bx, 380, bx, 470], 5)(col, 0.18)); // reflection
c.bg([34, 37, 48], [11, 13, 20]);
c.each((x, y) => [[64, 72, 100], (1 - ss(70, 200, Math.abs(y - 400))) * 0.55]); // street glow band
for (const [bx, y0, y1, col, w] of [[70, 58, 300, LIGHT, 8], [150, 100, 340, AMBER, 7], [240, 48, 288, BRIGHT, 11], [330, 118, 350, LIGHT, 7], [412, 78, 310, AMBER, 8]]) {
c.each((x, y) => seg(x, y, [bx, y0, bx, y1], w, col, 0.96));
c.each((x, y) => seg(x, y, [bx, 382, bx, 500], w * 0.9, col, 0.22));
}
c.each((x, y) => segCov(x, y, [0, 360, W, 360], 1.5)(DIM, 0.5)); // horizon
c.each((x, y) => seg(x, y, [0, 362, W, 362], 2, LIGHT, 0.5));
},
// Worn portrait — head outline + shoulders + eyes.
'worn-portrait'(c) {
c.bg([26, 24, 26], [12, 11, 14]);
c.each((x, y) => { const d = Math.hypot((x - 240) / 118, (y - 300) / 150); return [LIGHT, (1 - ss(0.97, 1.0, d)) * ss(0.9, 0.95, d) * 0.9]; }); // head ring (oval)
c.each((x, y) => segCov(x, y, [110, 600, 205, 455], 3)(DIM, 0.7));
c.each((x, y) => segCov(x, y, [370, 600, 275, 455], 3)(DIM, 0.7));
c.each((x, y) => [DIM, Math.max(discCov(x, y, 205, 300, 9), discCov(x, y, 275, 300, 9)) * 0.8]); // eyes
c.each((x, y) => segCov(x, y, [240, 300, 240, 350], 2)(DIM, 0.5)); // nose
c.bg([44, 42, 47], [13, 12, 16]);
c.each((x, y) => [[0, 0, 0], ss(0.72, 1.2, Math.hypot((x - 240) / 300, (y - 320) / 380)) * 0.55]); // vignette
c.each((x, y) => { const d = Math.hypot((x - 240) / 114, (y - 300) / 148); return [[156, 156, 164], (1 - ss(0.96, 1.03, d)) * 0.55]; }); // face fill
c.each((x, y) => { const d = Math.hypot((x - 240) / 114, (y - 300) / 148); return [LIGHT, (1 - ss(0.99, 1.03, d)) * ss(0.92, 0.97, d) * 0.9]; }); // rim
c.each((x, y) => [DARK, Math.max(disc(x, y, 206, 296, 11), disc(x, y, 274, 296, 11)) * 0.75]); // eyes
c.each((x, y) => seg(x, y, [240, 302, 240, 356], 3, DARK, 0.5)); // nose
c.each((x, y) => seg(x, y, [104, 600, 206, 452], 4, DIM, 0.75));
c.each((x, y) => seg(x, y, [376, 600, 274, 452], 4, DIM, 0.75));
},
// Figure in a doorway — lit rectangle, dark silhouette.
'doorway-figure'(c) {
c.bg([16, 18, 24], [9, 11, 16]);
c.each((x, y) => { const rx = 1 - ss(69, 71, Math.abs(x - 240)); const ry = ss(118, 120, y) * (1 - ss(598, 600, y)); return [[64, 70, 80], rx * ry * 0.85]; }); // lit doorway fill
c.each((x, y) => segCov(x, y, [170, 120, 170, 600], 2.5)(LIGHT, 0.6));
c.each((x, y) => segCov(x, y, [310, 120, 310, 600], 2.5)(LIGHT, 0.6));
c.each((x, y) => segCov(x, y, [170, 120, 310, 120], 2.5)(LIGHT, 0.6));
c.each((x, y) => { const body = (1 - ss(24, 26, Math.abs(x - 240))) * (1 - ss(560, 562, y)) * ss(300, 302, y); return [[10, 12, 16], Math.max(body, discCov(x, y, 240, 300, 24))]; }); // silhouette
c.bg([24, 27, 34], [9, 11, 17]);
c.each((x, y) => { const rx = 1 - ss(70, 73, Math.abs(x - 240)); const ry = ss(115, 119, y) * (1 - ss(597, 601, y)); return [[158, 166, 180], rx * ry * 0.92]; }); // lit doorway
c.each((x, y) => seg(x, y, [167, 117, 167, 600], 3, BRIGHT, 0.75));
c.each((x, y) => seg(x, y, [313, 117, 313, 600], 3, BRIGHT, 0.75));
c.each((x, y) => seg(x, y, [167, 117, 313, 117], 3, BRIGHT, 0.75));
c.each((x, y) => { const body = (1 - ss(26, 28, Math.abs(x - 240))) * ss(296, 300, y) * (1 - ss(560, 564, y)); return [DARK, Math.max(body, disc(x, y, 240, 294, 26))]; });
},
// Pier in the rain — planks, rails, rain streaks.
'rain-pier'(c) {
c.bg([22, 26, 32], [14, 18, 24]);
c.each((x, y) => segCov(x, y, [0, 300, W, 300], 1.5)(DIM, 0.4)); // horizon
for (const py of [345, 378, 414, 452]) c.each((x, y) => segCov(x, y, [140, py, 340, py], 2)(DIM, 0.55)); // planks
c.each((x, y) => segCov(x, y, [150, 300, 120, 470], 2)(DIM, 0.5)); // rails converging
c.each((x, y) => segCov(x, y, [330, 300, 360, 470], 2)(DIM, 0.5));
c.bg([36, 42, 51], [19, 25, 33]);
c.each((x, y) => [[54, 63, 76], ss(298, 302, y) * 0.55]); // water region
c.each((x, y) => seg(x, y, [0, 300, W, 300], 2.5, LIGHT, 0.6)); // horizon
for (const py of [344, 378, 414, 452]) c.each((x, y) => seg(x, y, [135, py, 345, py], 3, LIGHT, 0.72));
c.each((x, y) => seg(x, y, [152, 300, 118, 470], 3, LIGHT, 0.62));
c.each((x, y) => seg(x, y, [328, 300, 362, 470], 3, LIGHT, 0.62));
const r = rng(7);
for (let i = 0; i < 60; i++) { const x0 = r() * W, y0 = r() * H, len = 22 + r() * 26; c.each((x, y) => segCov(x, y, [x0, y0, x0 - len * 0.32, y0 + len], 0.8)(LIGHT, 0.22)); }
for (let i = 0; i < 55; i++) { const x0 = r() * W, y0 = r() * H, len = 26 + r() * 30; c.each((x, y) => seg(x, y, [x0, y0, x0 - len * 0.3, y0 + len], 1.1, LIGHT, 0.28)); }
},
};