Board artwork: one component draws the game, the hall's still, and the share card, with gradients on the pieces, plank joins and inlaid corners on the oak, and the stone as it was
npm run art renders the standalone SVGs and the 1200 by 630 card through Sharp, a development dependency only.
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
# Board artwork
|
||||
|
||||
`src/lib/components/BoardArtwork.svelte` owns both palettes, surface filters,
|
||||
wear, grid, inlays and counters. `Board.svelte` supplies interactive state;
|
||||
`Hall.svelte` supplies an opening position from the game engine. Update the
|
||||
shared component to change either illustration and the game together.
|
||||
|
||||
After editing the artwork, run:
|
||||
|
||||
```sh
|
||||
npm run art
|
||||
```
|
||||
|
||||
This renders the same Svelte component into `static/board-copenhagen.svg` and
|
||||
`static/board-tablut.svg`, then regenerates `docs/og-card.svg` and the 1200 × 630
|
||||
`static/og.png`. Preview the share composition in `docs/og-card.html`.
|
||||
The share composition and copy live in `docs/render-art.mjs`. Commit generated
|
||||
assets with source changes. The export uses Sharp as a development dependency;
|
||||
it does not add any image-processing code to the shipped app.
|
||||
+12
-32
@@ -1,33 +1,13 @@
|
||||
<!doctype html>
|
||||
<html><head><meta charset="utf-8"><style>
|
||||
html,body{margin:0;background:#14181a}
|
||||
.card{width:1200px;height:630px;display:flex;align-items:center;gap:64px;padding:0 80px;box-sizing:border-box;background:#14181a;color:#e6dfcf;font-family:'Iowan Old Style',Georgia,serif}
|
||||
.board{flex:0 0 470px;border-radius:8px;box-shadow:0 20px 50px rgba(0,0,0,.6)}
|
||||
h1{font-weight:300;font-size:92px;letter-spacing:-.01em;margin:0 0 14px}
|
||||
p{font-size:31px;line-height:1.35;color:#9ea59e;margin:0}
|
||||
p b{color:#e6dfcf;font-weight:500}
|
||||
.foot{margin-top:28px;font-size:24px;color:#626b66}
|
||||
</style></head><body><div class="card">
|
||||
<svg class="board" viewBox="0 0 110 110" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="110" height="110" rx="4" fill="#8a5a2b"/>
|
||||
<g id="grid"></g>
|
||||
</svg>
|
||||
<div>
|
||||
<h1>Hnefatafl</h1>
|
||||
<p><b>The Viking board game.</b> One king, one throne, four corners, and a ring of attackers closing in. Play a housecarl or a friend, by Copenhagen rules or Linnaeus's Tablut.</p>
|
||||
<p class="foot">tafl.kestrelsnest.social · free, no accounts</p>
|
||||
</div></div>
|
||||
<script>
|
||||
const g = document.getElementById('grid'); const ns = 'http://www.w3.org/2000/svg';
|
||||
const el = (t, a) => { const e = document.createElementNS(ns, t); for (const k in a) e.setAttribute(k, a[k]); g.appendChild(e); };
|
||||
for (let r = 0; r < 11; r++) for (let c = 0; c < 11; c++) {
|
||||
const corner = (r === 0 || r === 10) && (c === 0 || c === 10), throne = r === 5 && c === 5;
|
||||
el('rect', { x: c * 10, y: r * 10, width: 10, height: 10, fill: corner ? 'rgba(224,101,92,0.4)' : throne ? 'rgba(240,165,58,0.4)' : 'rgba(255,240,210,0.08)', stroke: 'rgba(30,16,6,0.5)', 'stroke-width': 0.4 });
|
||||
}
|
||||
const a = [[3,0],[4,0],[5,0],[6,0],[7,0],[5,1],[0,3],[10,3],[0,4],[10,4],[0,5],[1,5],[9,5],[10,5],[0,6],[10,6],[0,7],[10,7],[5,9],[3,10],[4,10],[5,10],[6,10],[7,10]];
|
||||
const d = [[5,3],[4,4],[5,4],[6,4],[3,5],[4,5],[6,5],[7,5],[4,6],[5,6],[6,6],[5,7]];
|
||||
for (const [x, y] of a) el('circle', { cx: x * 10 + 5, cy: y * 10 + 5, r: 3.4, fill: '#2a2320', stroke: '#0d0a08', 'stroke-width': 0.6 });
|
||||
for (const [x, y] of d) el('circle', { cx: x * 10 + 5, cy: y * 10 + 5, r: 3.4, fill: '#ece4d0', stroke: '#b9ab8b', 'stroke-width': 0.6 });
|
||||
el('circle', { cx: 55, cy: 55, r: 3.6, fill: '#f0d58a', stroke: '#a67c1a', 'stroke-width': 0.6 });
|
||||
el('path', { d: 'M52.6 56.4 l0 -2.6 l1.3 1.4 l1.1 -2.3 l1.1 2.3 l1.3 -1.4 l0 2.6 z', fill: '#6b4a0f' });
|
||||
</script></body></html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Hnefatafl share card</title>
|
||||
<style>html,body{margin:0;background:#14181a}img{display:block;width:1200px;height:630px;max-width:100%;object-fit:contain;object-position:top left}</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Generated from BoardArtwork.svelte by npm run art; also produces static/og.png. -->
|
||||
<img src="og-card.svg" width="1200" height="630" alt="Hnefatafl. The Viking board game. Play a housecarl or a friend, Copenhagen or Linnaeus’s Tablut. Free, no accounts. tafl.kestrelsnest.social">
|
||||
</body>
|
||||
</html>
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 57 KiB |
@@ -0,0 +1,56 @@
|
||||
// Run with npm run art. Both illustrations originate in the live board component.
|
||||
import { readFile, writeFile, unlink } from 'node:fs/promises';
|
||||
import { compile } from 'svelte/compiler';
|
||||
import { render } from 'svelte/server';
|
||||
import sharp from 'sharp';
|
||||
import { createGame } from '../src/lib/game/index.ts';
|
||||
const root = new URL('../', import.meta.url);
|
||||
const source = await readFile(new URL('src/lib/components/BoardArtwork.svelte', root), 'utf8');
|
||||
const compiled = compile(source.replace("'$lib/game'", "'../src/lib/game/index.ts'"), { generate: 'server', css: 'injected', filename: 'BoardArtwork.svelte' });
|
||||
const temporary = new URL(`.board-art-${process.pid}.mjs`, import.meta.url);
|
||||
try {
|
||||
await writeFile(temporary, compiled.js.code);
|
||||
const { default: BoardArtwork } = await import(temporary.href);
|
||||
for (const set of ['copenhagen', 'tablut']) {
|
||||
const result = render(BoardArtwork, { props: { g: createGame({ a: 'Defenders', b: 'Attackers' }, 0, undefined, { set }), decorative: true } });
|
||||
let svg = result.body.replace(/<!--[\s\S]*?-->/g, '');
|
||||
const css = [...result.head.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g)].map(m => m[1]).join('\n');
|
||||
svg = svg.replace(/(<svg[^>]*>)/, '$1<style>' + css + '</style>');
|
||||
// Resolve custom properties for standalone SVG rasterizers, which do not
|
||||
// consistently implement CSS variable inheritance. Browsers use the component.
|
||||
const paletteSource = source.slice(source.indexOf('/* Copenhagen:'));
|
||||
const base = paletteSource.slice(0, paletteSource.indexOf('/* Tablut:'));
|
||||
const overrides = set === 'tablut' ? paletteSource.slice(paletteSource.indexOf('/* Tablut:'), paletteSource.indexOf('\n\t.artwork {')) : '';
|
||||
const values = Object.fromEntries([...`${base}\n${overrides}`.matchAll(/(--[\w-]+):\s*([^;]+);/g)].map(m => [m[1], m[2]]));
|
||||
values['--piece-rim'] = values['--defender-rim'];
|
||||
values['--font'] = 'Georgia, serif';
|
||||
values['--frost'] = '#9ccbdd';
|
||||
svg = svg.replace(/:where\((\.[\w-]+)\)/g, '$1');
|
||||
svg = svg.replace(/var\((--[\w-]+)\)/g, (_, key) => values[key] ?? 'inherit');
|
||||
svg = svg.replace('</style>', ['attacker', 'defender', 'king'].map(kind => `.piece.${kind} .base{fill:${values[`--${kind}-rim`]}}.piece.${kind} .body,.piece.${kind} .turned{stroke:${values[`--${kind}-rim`]}}`).join('') + '</style>');
|
||||
await writeFile(new URL(`static/board-${set}.svg`, root), svg);
|
||||
}
|
||||
const board = await readFile(new URL('static/board-copenhagen.svg', root), 'utf8');
|
||||
const image = Buffer.from(board).toString('base64');
|
||||
const share = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
|
||||
<rect width="1200" height="630" fill="#14181a"/>
|
||||
<image x="62" y="80" width="470" height="470" href="data:image/svg+xml;base64,${image}"/>
|
||||
<g font-family="Georgia, serif">
|
||||
<text x="590" y="180" font-size="82" fill="#e6dfcf">Hnefatafl</text>
|
||||
<text x="594" y="246" font-size="30" fill="#e6dfcf">The Viking board game.</text>
|
||||
<text x="594" y="294" font-size="27" fill="#aeb3ab">
|
||||
<tspan x="594">One king, one throne, four corners,</tspan>
|
||||
<tspan x="594" dy="38">and a ring of attackers closing in.</tspan>
|
||||
<tspan x="594" dy="50">Play a housecarl or a friend.</tspan>
|
||||
<tspan x="594" dy="38">Copenhagen or Linnaeus’s Tablut.</tspan>
|
||||
</text>
|
||||
<text x="594" y="491" font-size="24" fill="#c5ae7b">tafl.kestrelsnest.social</text>
|
||||
<text x="594" y="526" font-size="22" fill="#9ea59e">Free · no accounts</text>
|
||||
</g>
|
||||
</svg>`;
|
||||
await writeFile(new URL('docs/og-card.svg', root), share);
|
||||
await sharp(Buffer.from(share)).png().toFile(new URL('static/og.png', root).pathname);
|
||||
console.log('Generated both board illustrations and static/og.png (1200 × 630).');
|
||||
} finally {
|
||||
await unlink(temporary).catch(() => {});
|
||||
}
|
||||
Reference in New Issue
Block a user