fuzzyworld

bubbles / files / scripts / generate-assets.ts

generate-assets.ts

3.4 KB · history · blame

1/**
2 * Asset generation script.
3 * Run with: pnpm gen-assets
4 *
5 * Reads wind.png from the repo root, applies a blue tint, and writes:
6 * static/favicon.ico (32×32 ICO via PNG fallback — modern browsers accept PNG as .ico)
7 * static/og-image.png (1200×630 social card with logo centred on a dark background)
8 */
9
10import sharp from 'sharp';
11import path from 'path';
12import fs from 'fs';
13import { fileURLToPath } from 'url';
14
15const __dirname = path.dirname(fileURLToPath(import.meta.url));
16const root = path.resolve(__dirname, '..');
17const src = path.join(root, 'wind.png');
18const staticDir = path.join(root, 'static');
19
20if (!fs.existsSync(staticDir)) fs.mkdirSync(staticDir, { recursive: true });
21
22// ── Shared: tinted white→blue version of the logo ─────────────────────────
23
24async function makeTintedLogo(size: number): Promise<Buffer> {
25 // 1. Resize to target size
26 // 2. Ensure RGBA
27 // 3. Tint: the source is black-on-white; we invert so strokes are white,
28 // then composite a blue colour behind it.
29 const logoAlpha = await sharp(src)
30 .resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
31 .negate({ alpha: false }) // invert: black strokes → white strokes
32 .toColorspace('srgb')
33 .ensureAlpha()
34 .raw()
35 .toBuffer({ resolveWithObject: true });
36
37 // Build a solid blue background of the same size
38 const blue = await sharp({
39 create: { width: size, height: size, channels: 4, background: { r: 14, g: 165, b: 233, alpha: 255 } },
40 })
41 .png()
42 .toBuffer();
43
44 // Composite white logo on blue background
45 return sharp(blue)
46 .composite([{ input: logoAlpha.data, raw: { width: size, height: size, channels: 4 }, blend: 'over' }])
47 .png()
48 .toBuffer();
49}
50
51// ── favicon.ico (PNG saved as .ico — accepted by all modern browsers) ──────
52
53async function genFavicon(): Promise<void> {
54 const buf = await makeTintedLogo(32);
55 const out = path.join(staticDir, 'favicon.ico');
56 fs.writeFileSync(out, buf);
57 console.log(`✓ favicon.ico (32×32)`);
58}
59
60// ── og-image.png (1200×630) ───────────────────────────────────────────────
61
62async function genOgImage(): Promise<void> {
63 const W = 1200, H = 630;
64 const logoSize = 220;
65
66 const logo = await makeTintedLogo(logoSize);
67
68 // Dark-blue background
69 const bg = await sharp({
70 create: { width: W, height: H, channels: 3, background: { r: 15, g: 23, b: 42 } },
71 })
72 .png()
73 .toBuffer();
74
75 const out = await sharp(bg)
76 .composite([{
77 input: logo,
78 left: Math.round((W - logoSize) / 2),
79 top: Math.round((H - logoSize) / 2) - 40,
80 }])
81 .png()
82 .toBuffer();
83
84 fs.writeFileSync(path.join(staticDir, 'og-image.png'), out);
85 console.log(`✓ og-image.png (${W}×${H})`);
86}
87
88// ── run ────────────────────────────────────────────────────────────────────
89
90async function main(): Promise<void> {
91 await genFavicon();
92 await genOgImage();
93 console.log('Asset generation complete.');
94}
95
96main().catch((err: unknown) => { console.error(err); process.exit(1); });