SOURCE / PINNED RELEASE
Made of little things.
Pickle Rustlers
- Release
- 039e51f55e34…
- Author-recorded commit
- 40608586a8a1…
- License
- LICENSE
- Author’s source reference
- nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-bad44e7ab35
Archive hash verified: f38e99afa0992052…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
// The shareable PNG: a wanted poster pinned over the victory fireworks.
import type { RunSummary } from './types.js';
export const CARD_W = 1200;
export const CARD_H = 675;
// Same app-owned palette and slab face as the HUD (src/styles.css).
const PAPER = '#f3e3c3';
const PAPER_DARK = '#e2c99a';
const INK = '#3b2412';
const RUST = '#b5541c';
const BRASS = '#c9a227';
const ROPE = '#c99a4f';
const SAUCE = '#d62828';
const SLAB = "'Rockwell', 'Rockwell Nova', 'Roboto Slab', 'Clarendon', 'Georgia', serif";
const n = (v: number) => v.toLocaleString('en-US');
/** Set a font, shrinking it until the text fits the width. */
function fitFont(ctx: CanvasRenderingContext2D, text: string, style: string, size: number, maxWidth: number): void {
let px = size;
ctx.font = `${style} ${px}px ${SLAB}`;
while (px > 12 && ctx.measureText(text).width > maxWidth) {
px -= 2;
ctx.font = `${style} ${px}px ${SLAB}`;
}
}
function drawScene(ctx: CanvasRenderingContext2D, scene: HTMLCanvasElement): void {
// Cover-crop the snapshot, then darken toward the edges so the poster reads.
const scale = Math.max(CARD_W / scene.width, CARD_H / scene.height);
const sw = CARD_W / scale;
const sh = CARD_H / scale;
ctx.fillStyle = '#2a1a10';
ctx.fillRect(0, 0, CARD_W, CARD_H);
if (scene.width > 0 && scene.height > 0) {
ctx.drawImage(scene, (scene.width - sw) / 2, (scene.height - sh) / 2, sw, sh, 0, 0, CARD_W, CARD_H);
}
const shade = ctx.createLinearGradient(0, 0, CARD_W, 0);
shade.addColorStop(0, 'rgba(59, 36, 18, 0.55)');
shade.addColorStop(0.6, 'rgba(59, 36, 18, 0)');
ctx.fillStyle = shade;
ctx.fillRect(0, 0, CARD_W, CARD_H);
const vignette = ctx.createRadialGradient(CARD_W / 2, CARD_H / 2, CARD_H * 0.45, CARD_W / 2, CARD_H / 2, CARD_W * 0.7);
vignette.addColorStop(0, 'rgba(0, 0, 0, 0)');
vignette.addColorStop(1, 'rgba(20, 10, 5, 0.45)');
ctx.fillStyle = vignette;
ctx.fillRect(0, 0, CARD_W, CARD_H);
}
function drawStar(ctx: CanvasRenderingContext2D, x: number, y: number, r: number): void {
const badge = ctx.createRadialGradient(x - r * 0.3, y - r * 0.35, r * 0.1, x, y, r);
badge.addColorStop(0, '#ffe48a');
badge.addColorStop(1, BRASS);
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fillStyle = badge;
ctx.fill();
ctx.lineWidth = 4;
ctx.strokeStyle = INK;
ctx.stroke();
ctx.beginPath();
for (let i = 0; i < 10; i++) {
const a = -Math.PI / 2 + (i * Math.PI) / 5;
const rr = i % 2 === 0 ? r * 0.62 : r * 0.26;
ctx.lineTo(x + Math.cos(a) * rr, y + Math.sin(a) * rr);
}
ctx.closePath();
ctx.fillStyle = INK;
ctx.fill();
}
function drawPoster(ctx: CanvasRenderingContext2D, run: RunSummary, name: string): void {
const w = 500;
const h = 590;
const x = 70;
const y = (CARD_H - h) / 2;
ctx.save();
ctx.translate(x + w / 2, y + h / 2);
ctx.rotate((-1.5 * Math.PI) / 180);
ctx.translate(-w / 2, -h / 2);
ctx.fillStyle = 'rgba(59, 36, 18, 0.55)';
ctx.fillRect(8, 10, w, h);
const paper = ctx.createLinearGradient(0, 0, 0, h);
paper.addColorStop(0, PAPER);
paper.addColorStop(1, PAPER_DARK);
ctx.fillStyle = paper;
ctx.fillRect(0, 0, w, h);
ctx.lineWidth = 5;
ctx.strokeStyle = INK;
ctx.strokeRect(0, 0, w, h);
ctx.setLineDash([12, 8]);
ctx.lineWidth = 4;
ctx.strokeStyle = ROPE;
ctx.strokeRect(16, 16, w - 32, h - 32);
ctx.setLineDash([]);
const cx = w / 2;
const inner = w - 80;
ctx.textAlign = 'center';
ctx.textBaseline = 'alphabetic';
drawStar(ctx, cx, 78, 40);
ctx.fillStyle = INK;
fitFont(ctx, 'THE WEST IS BRINED', '900', 46, inner);
ctx.fillText('THE WEST IS BRINED', cx, 170);
const sub = `${name} peeled the Onion King`;
ctx.fillStyle = RUST;
fitFont(ctx, sub, 'italic', 24, inner);
ctx.fillText(sub, cx, 208);
const stats: [string, string][] = [
['REWARD PAID', `$${n(run.score)}`],
['PICKLES BRINED', n(run.brined)],
['ONIONS PEELED', n(run.onions)],
['TEARS CRIED', n(run.tears)],
];
stats.forEach(([label, value], i) => {
const col = i % 2;
const row = Math.floor(i / 2);
const sx = 40 + col * (inner / 2) + inner / 4;
const sy = 268 + row * 108;
ctx.fillStyle = 'rgba(255, 255, 255, 0.35)';
ctx.fillRect(sx - inner / 4 + 6, sy - 26, inner / 2 - 12, 92);
ctx.fillStyle = RUST;
ctx.font = `700 15px ${SLAB}`;
ctx.fillText(label, sx, sy);
ctx.fillStyle = INK;
fitFont(ctx, value, '900', 46, inner / 2 - 28);
ctx.fillText(value, sx, sy + 50);
});
ctx.fillStyle = INK;
ctx.font = `900 22px ${SLAB}`;
ctx.fillText('PICKLE RUSTLERS', cx, h - 64);
ctx.fillStyle = RUST;
ctx.font = `italic 17px ${SLAB}`;
ctx.fillText(run.wins > 1 ? `★ West tamed ${run.wins}×` : 'Dead or brined', cx, h - 38);
if (run.newBest) {
ctx.save();
ctx.translate(w - 92, 88);
ctx.rotate((-12 * Math.PI) / 180);
ctx.lineWidth = 4;
ctx.strokeStyle = SAUCE;
ctx.strokeRect(-78, -26, 156, 48);
ctx.fillStyle = SAUCE;
ctx.font = `900 26px ${SLAB}`;
ctx.textBaseline = 'middle';
ctx.fillText('NEW BEST!', 0, -1);
ctx.restore();
}
ctx.restore();
}
export function drawScoreCard(run: RunSummary, scene: HTMLCanvasElement, name: string): HTMLCanvasElement {
const canvas = document.createElement('canvas');
canvas.width = CARD_W;
canvas.height = CARD_H;
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('2D canvas unavailable');
drawScene(ctx, scene);
drawPoster(ctx, run, name);
return canvas;
}
export function toPng(canvas: HTMLCanvasElement): Promise<Blob> {
return new Promise((resolve, reject) =>
canvas.toBlob((blob) => (blob ? resolve(blob) : reject(new Error('PNG encoding failed'))), 'image/png'),
);
}
