- 아이콘: 게임패드 + 천사 고리 + 반짝이, scripts/pwa-icons.ts가 favicon.svg와 PNG를 한 디자인에서 생성 - 홈: 방 만들기·코드로 참여(/j)·컴퓨터와 연습 버튼, [게임 고르기 | 지금 열린 방] 탭 - 헤더: 로그인 전 [로그인] → /login, 로그인 뒤 닉네임 → [설정][로그아웃] 메뉴 - /login: 로그인만, /me(설정)는 로그인 안 했으면 /login?next=/me로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
59 lines
3.2 KiB
TypeScript
59 lines
3.2 KiB
TypeScript
/**
|
|
* Renders the site icon (docs/07 §11) with headless Chrome.
|
|
* bun scripts/pwa-icons.ts → apps/web/public/favicon.svg + apps/web/public/icons/*.png
|
|
* One design (아케이드 게임기 + 천사 고리 + 반짝이) is the source of every icon:
|
|
* "any" icons keep rounded corners; maskable/apple icons are full-bleed
|
|
* (the OS applies its own mask) with the art shrunk into the safe circle.
|
|
*/
|
|
import { chromium } from 'playwright-core';
|
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
|
|
const PUBLIC = join(import.meta.dir, '../apps/web/public');
|
|
const OUT = join(PUBLIC, 'icons');
|
|
mkdirSync(OUT, { recursive: true });
|
|
|
|
const INK = '#1e1b4b';
|
|
const DEFS = `<defs><linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#7c3aed"/><stop offset=".55" stop-color="#db2777"/><stop offset="1" stop-color="#f97316"/></linearGradient></defs>`;
|
|
const star = (x: number, y: number, r: number, fill: string) =>
|
|
`<path d="M${x} ${y - r}Q${x} ${y} ${x + r} ${y}Q${x} ${y} ${x} ${y + r}Q${x} ${y} ${x - r} ${y}Q${x} ${y} ${x} ${y - r}z" fill="${fill}"/>`;
|
|
const ART = [
|
|
// 천사 고리: "게임천국"
|
|
`<ellipse cx="32" cy="14.5" rx="11" ry="3.6" fill="none" stroke="#fde047" stroke-width="2.6"/>`,
|
|
// 게임 패드
|
|
`<path d="M20 21h24a10 10 0 0 1 9.6 7.2l3.3 11.6a5.6 5.6 0 0 1-9.5 5.3L43 40H21l-4.4 5.1a5.6 5.6 0 0 1-9.5-5.3l3.3-11.6A10 10 0 0 1 20 21z" fill="#fff" stroke="${INK}" stroke-width="2.2" stroke-linejoin="round"/>`,
|
|
`<path d="M19.6 26.2h2.8v3h3v2.8h-3v3h-2.8v-3h-3v-2.8h3z" fill="${INK}"/>`,
|
|
`<circle cx="43" cy="26.6" r="2.3" fill="#facc15" stroke="${INK}" stroke-width="1"/>`,
|
|
`<circle cx="46.8" cy="30.6" r="2.3" fill="#ef4444" stroke="${INK}" stroke-width="1"/>`,
|
|
`<circle cx="43" cy="34.6" r="2.3" fill="#22c55e" stroke="${INK}" stroke-width="1"/>`,
|
|
`<circle cx="39.2" cy="30.6" r="2.3" fill="#3b82f6" stroke="${INK}" stroke-width="1"/>`,
|
|
// 반짝이
|
|
star(53, 11, 4.6, '#fde047'),
|
|
star(10.5, 14, 3, '#fff'),
|
|
star(54, 53, 3, '#fff'),
|
|
star(11, 54, 2.2, '#fde047'),
|
|
].join('');
|
|
|
|
const rounded = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">${DEFS}<rect width="64" height="64" rx="14" fill="url(#bg)"/>${ART}</svg>\n`;
|
|
const fullBleed = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">${DEFS}<rect width="64" height="64" fill="url(#bg)"/><g transform="translate(32 32) scale(0.78) translate(-32 -32)">${ART}</g></svg>`;
|
|
|
|
writeFileSync(join(PUBLIC, 'favicon.svg'), rounded);
|
|
console.log('✓ favicon.svg');
|
|
|
|
const ICONS: [file: string, size: number, svg: string][] = [
|
|
['icon-192.png', 192, rounded],
|
|
['icon-512.png', 512, rounded],
|
|
['icon-maskable-512.png', 512, fullBleed],
|
|
['apple-touch-icon.png', 180, fullBleed],
|
|
];
|
|
|
|
const browser = await chromium.launch({ executablePath: process.env.CHROME ?? '/usr/bin/google-chrome', headless: true });
|
|
const page = await browser.newPage();
|
|
for (const [file, size, svg] of ICONS) {
|
|
await page.setViewportSize({ width: size, height: size });
|
|
await page.setContent(`<style>html,body{margin:0;background:transparent}svg{display:block;width:${size}px;height:${size}px}</style>${svg}`);
|
|
await page.screenshot({ path: join(OUT, file), omitBackground: true });
|
|
console.log(`✓ ${file}`);
|
|
}
|
|
await browser.close();
|