feat: complete WorkBuddy parity and Windows release workflow
This commit is contained in:
1 parent
143516b3a8
commit
d5512df549
434 files changed
+54977
-1854
No files matched your search
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 生成 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 = `<image x="${x}" y="${y + (128 - size) / 2}" width="${size}" height="${size}" href="data:image/png;base64,${tile(size)}"/>`
|
||||
x += size + 28
|
||||
return markup
|
||||
})
|
||||
.join('')
|
||||
const label = `<text x="40" y="${y + 68}" font-size="13" fill="${row.fg}" font-family="system-ui, 'Microsoft YaHei', sans-serif">${row.label}</text>`
|
||||
const block = `<rect x="20" y="${y}" width="640" height="128" rx="10" fill="${row.bg}"/>${label}${imgs}`
|
||||
y += 152
|
||||
return block
|
||||
})
|
||||
.join('')
|
||||
|
||||
const previewSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 680 ${y + 10}" width="680" height="${y + 10}">
|
||||
<rect width="680" height="${y + 10}" fill="#F7F8FA"/>
|
||||
<text x="20" y="34" font-size="15" font-weight="500" fill="#1F2328" font-family="system-ui, 'Microsoft YaHei', sans-serif">YCode 图标:128 / 64 / 48 / 32 / 16 px 实际尺寸对照</text>
|
||||
<text x="20" y="56" font-size="12" fill="#667085" font-family="system-ui, 'Microsoft YaHei', sans-serif">五颗星按国旗制式:一大四小(3:1),四颗小星各有一角尖对准大星中心;旗面为深红渐变+柔和光泽</text>
|
||||
${rowSvg}
|
||||
</svg>`
|
||||
|
||||
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')
|
||||
Reference in new issue
Block a user