| 1 | /* |
| 2 | make-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 |
|
| 15 | Uses sharp from generator/node_modules (cd generator && npm install first). |
| 16 | */ |
| 17 | import path from 'node:path'; |
| 18 | import fs from 'node:fs'; |
| 19 | import { fileURLToPath } from 'node:url'; |
| 20 | import { createRequire } from 'node:module'; |
| 21 |
|
| 22 | const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); |
| 23 | const sharp = createRequire(path.join(ROOT, 'generator/'))('sharp'); |
| 24 | const site = process.argv[2]; |
| 25 | if (!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 |
| 28 | function 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) |
| 64 | const 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) |
| 74 | const HI = 1.5; |
| 75 | const 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 |
| 90 | async 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 |
|
| 103 | fs.mkdirSync(path.join(ROOT, 'art'), { recursive: true }); |
| 104 | fs.mkdirSync(path.join(ROOT, 'data/cards'), { recursive: true }); |
| 105 | await sharp(Buffer.from(cardBg)).png().toFile(path.join(ROOT, 'data/cards/bg.png')); |
| 106 | for (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')); |
| 109 | await sharp(path.join(site, 'files/meta.jpg')).resize(320, 176, { fit: 'cover' }).png().toFile(path.join(ROOT, 'art/ICON0.PNG')); |
| 110 | await sharp(Buffer.from(wavesSvg(1920, 1080))).png().toFile(path.join(ROOT, 'art/PIC1.PNG')); |
| 111 | await sharp(Buffer.from(wavesSvg(1920, 1080))).png().toFile(path.join(ROOT, 'data/backdrop.png')); |
| 112 | fs.copyFileSync(path.join(site, 'files/PS3-WORLD-logo.png'), path.join(ROOT, 'data/logo.png')); |
| 113 | console.log('art/ICON0.PNG, art/PIC1.PNG, data/backdrop.png, data/logo.png, data/cards/*.png'); |