#!/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 ` ${FONTS} ${headline({ text: 'DO YOU EVEN SKATE', cx: W / 2, y: 150, size: 92, width: 470, offset: 5, stroke: 2 })} ${headline({ text: 'BRO?', cx: W / 2, y: 330, size: 190, width: 330, offset: 8, stroke: 3 })} ${qrCard({ x: (W - qrSize) / 2, y: 420, size: qrSize, qr, radius: 44 })} \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 ` ${FONTS} ${ribbon({ y: 0, width: W, height: 74, dir: 'down', count: 13 })} ${ribbon({ y: H - 74, width: W, height: 74, dir: 'up', count: 13 })} SKATESPOTS.APP ${dashed({ y: 250, x1: 130, x2: W - 130 })} ${headline({ text: 'DO YOU EVEN SKATE', cx, y: 620, size: 300, width: 2350, offset: 12, stroke: 4 })} ${headline({ text: 'BRO?', cx, y: 1000, size: 520, width: 1150, offset: 18, stroke: 6 })} FIND SPOTS. SESH TOGETHER. NO CAP. ${dashed({ y: 1240, x1: 130, x2: W - 130 })} ${qrCard({ x: cx - qrSize / 2, y: 1370, size: qrSize, qr, radius: 70 })} ${dashed({ y: 3260, x1: 130, x2: W - 130 })} SCAN. DOWNLOAD. GET OUT THERE. zapstore.dev/apps/org.skatespots.app \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);