diff --git a/index.html b/index.html index 79e14f4..7bb6b88 100644 --- a/index.html +++ b/index.html @@ -7,6 +7,20 @@ + + + + + + + + + + + + + + diff --git a/package.json b/package.json index c9a9816..c0f4c12 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "vite", "generate-icons": "node scripts/generate-pwa-icons.mjs", + "generate-og-image": "bun run scripts/generate-og-image.mjs", "build": "bun run generate-icons && tsc -b && vite build", "lint": "oxlint", "test": "vitest run", diff --git a/public/og-image.png b/public/og-image.png new file mode 100644 index 0000000..178c5ee Binary files /dev/null and b/public/og-image.png differ diff --git a/scripts/generate-og-image.mjs b/scripts/generate-og-image.mjs new file mode 100644 index 0000000..d518621 --- /dev/null +++ b/scripts/generate-og-image.mjs @@ -0,0 +1,82 @@ +import { readFileSync, writeFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import sharp from 'sharp' + +const root = join(dirname(fileURLToPath(import.meta.url)), '..') +const WIDTH = 1200 +const HEIGHT = 630 + +// The social card shows a real map from the bundled sample project +const sample = JSON.parse(readFileSync(join(root, 'public/sample.anal'), 'utf8')) +const [, mapBase64] = sample.mapImage.src.split(',') +const mapPng = await sharp(Buffer.from(mapBase64, 'base64')) + .resize({ width: 960, withoutEnlargement: true }) + .jpeg({ quality: 88 }) + .toBuffer() +const mapMeta = await sharp(mapPng).metadata() + +const CARD_W = 430 +const CARD_H = Math.round((CARD_W * mapMeta.height) / mapMeta.width) +const card = { x: 732, y: (HEIGHT - CARD_H) / 2 - 6 } +const cardCentre = { x: card.x + CARD_W / 2, y: card.y + CARD_H / 2 } +const toCardPx = (fx, fy) => ({ x: card.x + fx * CARD_W, y: card.y + fy * CARD_H }) + +// Orange control markers placed over the map, in fractions of the card size +const markers = [ + { label: 'A', fx: 0.3, fy: 0.42 }, + { label: 'B', fx: 0.52, fy: 0.58 }, + { label: 'C', fx: 0.74, fy: 0.36 }, +] + +const logo = readFileSync(join(root, 'public/favicon.svg')).toString('base64') +const mapDataUrl = `data:image/jpeg;base64,${mapPng.toString('base64')}` + +const markerSvg = markers + .map(({ label, fx, fy }) => { + const p = toCardPx(fx, fy) + return ` + + ${label}` + }) + .join('') + +const svg = ` + + + + + + + + + + + + + + + + + + + + O-Analysis + + Analyze and annotate your + orienteering sessions + + + + + + + ${markerSvg} + + + oanalysis.com +` + +const png = await sharp(Buffer.from(svg)).png().toBuffer() +writeFileSync(join(root, 'public/og-image.png'), png) +console.log(`Generated og-image.png (${WIDTH}x${HEIGHT})`)