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
|
// 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,
|
// src/art/manifest.js). Pure Node — hand-rolled PNG encoder + analytic AA.
|
||||||
// atmospheric charcoal motifs. Swap for real pencil art anytime.
|
// Bold charcoal motifs with large tonal regions so they read at thumbnail size.
|
||||||
// node tools/gen-art-placeholders.mjs
|
// node tools/gen-art-placeholders.mjs
|
||||||
import { deflateSync } from 'node:zlib';
|
import { deflateSync } from 'node:zlib';
|
||||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
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 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)); }
|
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 AA = 1.2;
|
||||||
const LIGHT = [206, 214, 222], DIM = [150, 160, 172], BRIGHT = [232, 238, 244], AMBER = [232, 200, 150];
|
const LIGHT = [222, 230, 238], DIM = [178, 188, 202], BRIGHT = [242, 246, 252], AMBER = [242, 208, 150], DARK = [9, 11, 15];
|
||||||
|
|
||||||
function canvas() {
|
function canvas() {
|
||||||
const buf = Buffer.alloc(W * H * 4);
|
const buf = Buffer.alloc(W * H * 4);
|
||||||
const api = {
|
return {
|
||||||
buf,
|
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; } } },
|
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 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); } },
|
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 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 discCov = (px, py, cx, cy, r) => 1 - ss(r - AA, r + AA, Math.hypot(px - cx, py - cy));
|
const disc = (px, py, cx, cy, r) => 1 - ss(r - AA, r + AA, Math.hypot(px - cx, py - cy));
|
||||||
|
|
||||||
const scenes = {
|
const scenes = {
|
||||||
// Detective in alley — converging walls, backlit figure.
|
|
||||||
'detective-alley'(c) {
|
'detective-alley'(c) {
|
||||||
c.bg([24, 27, 32], [12, 15, 20]);
|
c.bg([44, 49, 58], [15, 19, 25]);
|
||||||
c.each((x, y) => {
|
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
|
||||||
let a = 0, col = LIGHT;
|
c.each((x, y) => seg(x, y, [28, H, 236, 168], 3.5, LIGHT, 0.9));
|
||||||
const halo = Math.max(0, 1 - Math.hypot(x - 240, y - 500) / 150) * 0.4; // backlight
|
c.each((x, y) => seg(x, y, [452, H, 244, 168], 3.5, LIGHT, 0.9));
|
||||||
a = halo; col = [90, 96, 104];
|
c.each((x, y) => seg(x, y, [78, 566, 402, 566], 3, LIGHT, 0.75));
|
||||||
return [col, a];
|
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]; });
|
||||||
});
|
|
||||||
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)];
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
// Neon street — vertical light bars + reflections.
|
|
||||||
'neon-street'(c) {
|
'neon-street'(c) {
|
||||||
c.bg([20, 22, 30], [10, 12, 18]);
|
c.bg([34, 37, 48], [11, 13, 20]);
|
||||||
const bars = [[80, 70, 300, LIGHT], [150, 110, 330, AMBER], [300, 60, 280, BRIGHT], [372, 130, 340, LIGHT], [420, 90, 300, AMBER]];
|
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] of bars) {
|
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) => segCov(x, y, [bx, y0, bx, y1], 4)(col, 0.9));
|
c.each((x, y) => seg(x, y, [bx, y0, bx, y1], w, col, 0.96));
|
||||||
c.each((x, y) => segCov(x, y, [bx, 380, bx, 470], 5)(col, 0.18)); // reflection
|
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) {
|
'worn-portrait'(c) {
|
||||||
c.bg([26, 24, 26], [12, 11, 14]);
|
c.bg([44, 42, 47], [13, 12, 16]);
|
||||||
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) => [[0, 0, 0], ss(0.72, 1.2, Math.hypot((x - 240) / 300, (y - 320) / 380)) * 0.55]); // vignette
|
||||||
c.each((x, y) => segCov(x, y, [110, 600, 205, 455], 3)(DIM, 0.7));
|
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) => segCov(x, y, [370, 600, 275, 455], 3)(DIM, 0.7));
|
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) => [DIM, Math.max(discCov(x, y, 205, 300, 9), discCov(x, y, 275, 300, 9)) * 0.8]); // eyes
|
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) => segCov(x, y, [240, 300, 240, 350], 2)(DIM, 0.5)); // nose
|
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) {
|
'doorway-figure'(c) {
|
||||||
c.bg([16, 18, 24], [9, 11, 16]);
|
c.bg([24, 27, 34], [9, 11, 17]);
|
||||||
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) => { 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) => segCov(x, y, [170, 120, 170, 600], 2.5)(LIGHT, 0.6));
|
c.each((x, y) => seg(x, y, [167, 117, 167, 600], 3, BRIGHT, 0.75));
|
||||||
c.each((x, y) => segCov(x, y, [310, 120, 310, 600], 2.5)(LIGHT, 0.6));
|
c.each((x, y) => seg(x, y, [313, 117, 313, 600], 3, BRIGHT, 0.75));
|
||||||
c.each((x, y) => segCov(x, y, [170, 120, 310, 120], 2.5)(LIGHT, 0.6));
|
c.each((x, y) => seg(x, y, [167, 117, 313, 117], 3, BRIGHT, 0.75));
|
||||||
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.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) {
|
'rain-pier'(c) {
|
||||||
c.bg([22, 26, 32], [14, 18, 24]);
|
c.bg([36, 42, 51], [19, 25, 33]);
|
||||||
c.each((x, y) => segCov(x, y, [0, 300, W, 300], 1.5)(DIM, 0.4)); // horizon
|
c.each((x, y) => [[54, 63, 76], ss(298, 302, y) * 0.55]); // water region
|
||||||
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) => seg(x, y, [0, 300, W, 300], 2.5, LIGHT, 0.6)); // horizon
|
||||||
c.each((x, y) => segCov(x, y, [150, 300, 120, 470], 2)(DIM, 0.5)); // rails converging
|
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) => segCov(x, y, [330, 300, 360, 470], 2)(DIM, 0.5));
|
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);
|
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)); }
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||