/** * 生成 YCode 应用图标资产。 * * 输入:build/icon.svg(唯一矢量源) * 输出:build/icon.ico、build/icon.png、resources/icon.png、build/icon-preview.png * * 图案是深红旗面 + 按国旗制式排布的五颗金星(一大四小,3:1), * 星群坐标由 scripts/star-layout.mjs 算出,这里只负责栅格化与打包。 * * 用法:node scripts/gen-icon.mjs */ import { Resvg } from '@resvg/resvg-js' import { readFileSync, writeFileSync, mkdirSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' const root = join(dirname(fileURLToPath(import.meta.url)), '..') const buildDir = join(root, 'build') const resourcesDir = join(root, 'resources') const masterSvg = readFileSync(join(buildDir, 'icon.svg'), 'utf8') const ICO_SIZES = [16, 24, 32, 48, 64, 128, 256] function render(svg, size) { return new Resvg(svg, { fitTo: { mode: 'width', value: size } }).render().asPng() } function pick() { return masterSvg } /** 组装 Windows ICO:每个尺寸内嵌一张 PNG(Vista 及以后支持)。 */ function buildIco(entries) { const header = Buffer.alloc(6) header.writeUInt16LE(0, 0) header.writeUInt16LE(1, 2) header.writeUInt16LE(entries.length, 4) const dirEntries = [] let offset = 6 + 16 * entries.length for (const { size, png } of entries) { const entry = Buffer.alloc(16) entry.writeUInt8(size >= 256 ? 0 : size, 0) entry.writeUInt8(size >= 256 ? 0 : size, 1) entry.writeUInt8(0, 2) entry.writeUInt8(0, 3) entry.writeUInt16LE(1, 4) entry.writeUInt16LE(32, 6) entry.writeUInt32LE(png.length, 8) entry.writeUInt32LE(offset, 12) offset += png.length dirEntries.push(entry) } return Buffer.concat([header, ...dirEntries, ...entries.map((e) => e.png)]) } mkdirSync(resourcesDir, { recursive: true }) const icoEntries = ICO_SIZES.map((size) => ({ size, png: render(pick(), size) })) writeFileSync(join(buildDir, 'icon.ico'), buildIco(icoEntries)) writeFileSync(join(buildDir, 'icon.png'), render(masterSvg, 1024)) writeFileSync(join(resourcesDir, 'icon.png'), render(masterSvg, 512)) // 托盘:Windows 通知区域按 16/20/24/32 取用,给 32px 让系统自行缩放 writeFileSync(join(resourcesDir, 'tray.png'), render(masterSvg, 32)) // 备用图:常用尺寸各出一张 PNG,供托盘、商店页、文档等场景直接取用 const PNG_SIZES = [16, 20, 24, 32, 40, 48, 64, 96, 128, 180, 256, 512, 1024] const sizesDir = join(buildDir, 'sizes') mkdirSync(sizesDir, { recursive: true }) for (const size of PNG_SIZES) { writeFileSync(join(sizesDir, `icon-${size}.png`), render(pick(), size)) } // 对照表:浅色与深色背景下,各尺寸的真实观感 const sampleSizes = [128, 64, 48, 32, 16] function tile(size) { return render(pick(), size).toString('base64') } const rows = [ { label: '浅色任务栏', bg: '#FFFFFF', fg: '#475467' }, { label: '深色任务栏', bg: '#202124', fg: '#D0D5DD' } ] let y = 78 const rowSvg = rows .map((row) => { let x = 150 const imgs = sampleSizes .map((size) => { const markup = `` x += size + 28 return markup }) .join('') const label = `${row.label}` const block = `${label}${imgs}` y += 152 return block }) .join('') const previewSvg = ` YCode 图标:128 / 64 / 48 / 32 / 16 px 实际尺寸对照 五颗星按国旗制式:一大四小(3:1),四颗小星各有一角尖对准大星中心;旗面为深红渐变+柔和光泽 ${rowSvg} ` writeFileSync(join(buildDir, 'icon-preview.png'), new Resvg(previewSvg, { fitTo: { mode: 'width', value: 1360 } }).render().asPng()) console.log('icon.ico', icoEntries.length, 'sizes') console.log('icon.png / resources/icon.png / icon-preview.png done') console.log('build/sizes/icon-{', PNG_SIZES.join(','), '}.png done')