#!/usr/bin/env node
/**
* Generates the print-ready VECTOR artwork served by the App Shop:
* run with `node scripts/make-print-art.mjs` (output is committed, so this is
* only needed when the artwork changes).
* public/images/print/skatespots-sticker.svg (55 × 86 mm)
* public/images/print/skatespots-poster.svg (A3, 297 × 420 mm)
*
* Self-contained: brand fonts (Outfit variable + Barlow Condensed) and the logo
* are embedded as data URIs, and the QR is real vector from `qrcode`. That means
* it prints crisp at any size and needs no network.
*/
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const require = createRequire(join(ROOT, 'package.json'));
const QRCode = require('qrcode');
const OUT = join(ROOT, 'public/images/print');
mkdirSync(OUT, { recursive: true });
const ORANGE = '#f08c2a';
const WHITE = '#ffffff';
const BG = '#0a0a0a';
const QR_URL = 'https://skate-spots.org';
// ── assets ───────────────────────────────────────────────────────────────────
const b64 = (p) => readFileSync(p).toString('base64');
const outfit = b64(join(ROOT, 'node_modules/@fontsource-variable/outfit/files/outfit-latin-wght-normal.woff2'));
const barlow = b64(join(ROOT, 'node_modules/@fontsource/barlow-condensed/files/barlow-condensed-latin-600-normal.woff2'));
const logoPng = b64(join(ROOT, 'public/images/icon-1024.png'));
const FONTS = `
`;
/** Real QR module path (no background) + its viewBox size. */
async function qrPath() {
const svg = await QRCode.toString(QR_URL, {
type: 'svg',
margin: 0,
errorCorrectionLevel: 'H',
color: { dark: '#000000', light: '#ffffff' },
});
const m = svg.match(/]*d="([^"]+)"[^>]*\/?>/g) || [];
// The first path is the solid background; the rest are the modules.
const modulePath = (svg.match(/
`;
}
/** Block-shadowed headline: orange offset copy behind a white one. */
function headline({ text, cx, y, size, width, offset, stroke = 0, letterSpacing = '0.01em' }) {
const common = `text-anchor="middle" x="${cx}" y="${y}" font-size="${size}" letter-spacing="${letterSpacing}"` +
(width ? ` textLength="${width}" lengthAdjust="spacingAndGlyphs"` : '');
return `
${text}
${text}`;
}
/** Sawtooth ribbon of alternating triangles. dir: 'down' means apex points down. */
function ribbon({ y, width, height, dir, count = 13 }) {
const w = width / count;
let out = '';
for (let i = 0; i < count; i++) {
const x = i * w;
const fill = i % 2 === 0 ? ORANGE : WHITE;
const pts =
dir === 'down'
? `${x},${y} ${x + w / 2},${y + height} ${x + w},${y}`
: `${x},${y + height} ${x + w / 2},${y} ${x + w},${y + height}`;
out += ``;
}
return out;
}
const dashed = ({ y, x1, x2, color = '#3a3a3a', w = 4 }) =>
``;
// ── sticker (portrait, 55 × 86 mm at 0.1 mm units) ───────────────────────────
async function sticker() {
const qr = await qrPath();
const W = 550, H = 860;
const qrSize = 400;
return `\n`;
}
// ── poster (A3 portrait, 0.1 mm units → 2970 × 4200) ─────────────────────────
async function poster() {
const qr = await qrPath();
const W = 2970, H = 4200;
const qrSize = 1500;
const cx = W / 2;
return `\n`;
}
const s = await sticker();
const p = await poster();
writeFileSync(join(OUT, 'skatespots-sticker.svg'), s);
writeFileSync(join(OUT, 'skatespots-poster.svg'), p);
console.log('sticker:', s.length, 'bytes');
console.log('poster :', p.length, 'bytes');
console.log('→', OUT);