113 lines7.4 KBJavaScript
1/*
2make-art.mjs: the XMB pictures and the in-app background, from the site.
3
4 node tools/make-art.mjs "G:\PS3 World Self Host Ver - Copy"
5
6 art/ICON0.PNG 320x176, the site's meta image (files/meta.jpg)
7 art/PIC1.PNG 1920x1080, the site's index-page waves
8 data/backdrop.png 1920x1080, the same waves for the app's screens
9 data/logo.png the site's wordmark (files/PS3-WORLD-logo.png)
10 data/cards/*.png the home and Homebrew cards: the site's .explore-card art (images/home)
11 at each card's size at 1080p (HI times what src/ui.c lays out), faded in
12 from the right as the site's mask does,
13 and bg.png, the cards' gradient (src/ui.c draws them)
14
15Uses sharp from generator/node_modules (cd generator && npm install first).
16*/
17import path from 'node:path';
18import fs from 'node:fs';
19import { fileURLToPath } from 'node:url';
20import { createRequire } from 'node:module';
21
22const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
23const sharp = createRequire(path.join(ROOT, 'generator/'))('sharp');
24const site = process.argv[2];
25if (!site) { console.error('usage: node tools/make-art.mjs <main site folder>'); process.exit(1); }
26
27// the index page's .page-waves: three blurred gradient waves over --bg, a glow and a fade
28function wavesSvg(w, h) {
29 const c = { a: '#0070d1', b: '#2b8fff', c: '#4ad9ff', d: '#1a3a7a', e: '#0a1a3a', f: '#6ad9ff' };
30 const waveH = h * 0.578, waveY = h - waveH;
31 const wave = (id, stops, d, blur) => `
32 <svg x="0" y="${waveY}" width="${w}" height="${waveH}" viewBox="0 380 1440 520" preserveAspectRatio="none" opacity="0.26">
33 <defs>
34 <linearGradient id="g${id}" x1="0" x2="1" y1="0" y2="0">
35 ${stops.map(([o, col, op]) => `<stop offset="${o}%" stop-color="${col}" stop-opacity="${op}"/>`).join('')}
36 </linearGradient>
37 ${blur ? `<filter id="b${id}" x="-10%" y="-10%" width="120%" height="120%"><feGaussianBlur stdDeviation="6"/></filter>` : ''}
38 </defs>
39 <path d="${d}" fill="url(#g${id})" ${blur ? `filter="url(#b${id})"` : ''}/>
40 </svg>`;
41 return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">
42 <defs>
43 <linearGradient id="bg" x1="0" x2="0" y1="0" y2="1"><stop offset="0" stop-color="#05070d"/><stop offset="1" stop-color="#0a0e18"/></linearGradient>
44 <radialGradient id="glow" cx="0.8" cy="0.5" r="0.6">
45 <stop offset="0" stop-color="#2b8fff" stop-opacity="0.18"/><stop offset="0.6" stop-color="#0070d1" stop-opacity="0.06"/><stop offset="1" stop-color="#0070d1" stop-opacity="0"/>
46 </radialGradient>
47 <linearGradient id="fade" x1="0" x2="0" y1="0" y2="1">
48 <stop offset="0" stop-color="#05070d" stop-opacity="0.12"/><stop offset="0.22" stop-color="#05070d" stop-opacity="0"/>
49 <stop offset="0.4" stop-color="#05070d" stop-opacity="0"/><stop offset="0.55" stop-color="#05070d" stop-opacity="0.12"/>
50 <stop offset="0.85" stop-color="#05070d" stop-opacity="0.3"/><stop offset="1" stop-color="#05070d" stop-opacity="0.35"/>
51 </linearGradient>
52 </defs>
53 <rect width="100%" height="100%" fill="url(#bg)"/>
54 <rect width="100%" height="100%" fill="url(#glow)" opacity="0.35"/>
55 ${wave(3, [[0, c.e, 0], [45, c.c, 0.25], [75, c.f, 0.4], [100, c.f, 0]], 'M0,720 C180,600 360,780 540,660 C720,540 900,760 1080,640 C1260,520 1380,700 1440,660 L1440,1400 L0,1400 Z', true)}
56 ${wave(2, [[0, c.d, 0], [45, c.a, 0.4], [75, c.b, 0.6], [100, c.b, 0]], 'M0,600 C180,500 360,660 540,560 C720,460 900,660 1080,560 C1260,460 1380,620 1440,580 L1440,1400 L0,1400 Z', true)}
57 ${wave(1, [[0, c.a, 0], [45, c.b, 0.5], [75, c.c, 0.7], [100, c.c, 0]], 'M0,500 C180,420 360,560 540,470 C720,380 900,560 1080,470 C1260,380 1380,530 1440,490 L1440,1400 L0,1400 Z', false)}
58 <rect width="100%" height="100%" fill="url(#fade)"/>
59 </svg>`;
60}
61
62// .explore-card's background: a 160deg panel gradient and a blue glow from the bottom right
63// (the app stretches it to each card, as the site's percentages do)
64const cardBg = `<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256">
65 <defs>
66 <linearGradient id="p" x1="0.33" y1="0.03" x2="0.67" y2="0.97"><stop offset="0" stop-color="#121826"/><stop offset="1" stop-color="#0a0e17"/></linearGradient>
67 <radialGradient id="g" cx="1" cy="1" r="1.2"><stop offset="0" stop-color="#2b8fff" stop-opacity="0.16"/><stop offset="0.6" stop-color="#2b8fff" stop-opacity="0"/></radialGradient>
68 </defs>
69 <rect width="256" height="256" fill="url(#p)"/><rect width="256" height="256" fill="url(#g)"/>
70</svg>`;
71
72// [file, the site's art (or a file in this repo), width, height, art height (default: the card's)]: the sizes src/ui.c draws them at
73// (in 720p canvas units: they're made HI times that, for the screen at 1080p)
74const HI = 1.5;
75const CARDS = [
76 ['home-homebrew', 'homebrew', 640, 408, 294], // smaller, so his face is below and right of the text
77 ['home-themes', 'themes', 504, 196],
78 ['home-avatars', 'resources', 504, 196], // the site has no avatars art: its Resources card links to them
79 ['cat-games', 'homebrew-games', 376, 256],
80 ['cat-ports', 'ports', 376, 256],
81 ['cat-edits', 'edits', 376, 256],
82 ['cat-tools', 'homebrew-tools', 376, 256],
83 ['cat-media', 'homebrew-media', 376, 256],
84 ['cat-misc', 'homebrew-misc', 376, 256],
85 ['cat-recent', 'tools/art/recently-added.webp', 376, 256, 190], // (ours: the site has none; smaller, his face below the title)
86];
87
88// the art at the card's height, against its right edge, under the site's
89// mask (linear-gradient(to left, #000 25%, transparent 85%)) at the selected card's 0.8
90async function cardArt(src, w, h, ah = h) {
91 let art = sharp(src).resize({ height: ah });
92 const { info } = await art.clone().toBuffer({ resolveWithObject: true });
93 if (info.width > w) art = art.extract({ left: info.width - w, top: 0, width: w, height: ah });
94 const aw = Math.min(info.width, w);
95 const mask = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">
96 <defs><linearGradient id="m" x1="0" x2="1" y1="0" y2="0"><stop offset="0.15" stop-color="#fff" stop-opacity="0"/><stop offset="0.75" stop-color="#fff" stop-opacity="0.8"/></linearGradient></defs>
97 <rect width="${w}" height="${h}" fill="url(#m)"/></svg>`;
98 return sharp({ create: { width: w, height: h, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } } })
99 .composite([{ input: await art.png().toBuffer(), left: w - aw, top: h - ah }, { input: Buffer.from(mask), blend: 'dest-in' }])
100 .png();
101}
102
103fs.mkdirSync(path.join(ROOT, 'art'), { recursive: true });
104fs.mkdirSync(path.join(ROOT, 'data/cards'), { recursive: true });
105await sharp(Buffer.from(cardBg)).png().toFile(path.join(ROOT, 'data/cards/bg.png'));
106for (const [name, src, w, h, ah] of CARDS)
107 await (await cardArt(src.includes('/') ? path.join(ROOT, src) : path.join(site, 'images/home', src + '.webp'), Math.round(w * HI), Math.round(h * HI), ah && Math.round(ah * HI)))
108 .toFile(path.join(ROOT, 'data/cards', name + '.png'));
109await sharp(path.join(site, 'files/meta.jpg')).resize(320, 176, { fit: 'cover' }).png().toFile(path.join(ROOT, 'art/ICON0.PNG'));
110await sharp(Buffer.from(wavesSvg(1920, 1080))).png().toFile(path.join(ROOT, 'art/PIC1.PNG'));
111await sharp(Buffer.from(wavesSvg(1920, 1080))).png().toFile(path.join(ROOT, 'data/backdrop.png'));
112fs.copyFileSync(path.join(site, 'files/PS3-WORLD-logo.png'), path.join(ROOT, 'data/logo.png'));
113console.log('art/ICON0.PNG, art/PIC1.PNG, data/backdrop.png, data/logo.png, data/cards/*.png');