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>
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 46 KiB |
@@ -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)); }
|
||||
},
|
||||
};
|
||||
|
||||
|
||||