feat: update app icon generation for improved Windows and macOS support
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 43 KiB |
@@ -32,6 +32,12 @@ const OUT_TS = path.join(APP_DIR, 'src', 'main', 'brand-icons.ts')
|
|||||||
// Brand palette — keep in sync with the renderer (#1677ff is Ant Design blue).
|
// Brand palette — keep in sync with the renderer (#1677ff is Ant Design blue).
|
||||||
const BRAND_BLUE = '#1677ff'
|
const BRAND_BLUE = '#1677ff'
|
||||||
const DANGER_RED = '#ff4d4f'
|
const DANGER_RED = '#ff4d4f'
|
||||||
|
const MAC_APP_ICON_PLATE = 824
|
||||||
|
const MAC_APP_ICON_LOGO_RATIO = 0.62
|
||||||
|
const APP_PNG_PLATE = 896
|
||||||
|
const APP_PNG_LOGO_RATIO = 0.72
|
||||||
|
const WINDOWS_ICO_PLATE = 1024
|
||||||
|
const WINDOWS_ICO_LOGO_RATIO = 1
|
||||||
|
|
||||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'brand-icons-'))
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'brand-icons-'))
|
||||||
const cleanup = () => fs.rmSync(tmpDir, { recursive: true, force: true })
|
const cleanup = () => fs.rmSync(tmpDir, { recursive: true, force: true })
|
||||||
@@ -212,19 +218,36 @@ function forceLogoColor(source, fill) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** A square app icon: logo centered on a rounded-rect (squircle-ish) bg. */
|
/** A square app icon: logo centered on a rounded-rect (squircle-ish) bg. */
|
||||||
function squareIconSvg(bg, logoSource = logo, boundsSource = logoBounds) {
|
function squareIconSvg(bg, logoSource = logo, boundsSource = logoBounds, options = {}) {
|
||||||
const CANVAS = 1024
|
const CANVAS = 1024
|
||||||
// macOS icon grid: 824x824 art area inside a 1024 canvas (100px margin).
|
const PLATE = options.plate ?? MAC_APP_ICON_PLATE
|
||||||
const PLATE = 824
|
const LOGO_RATIO = options.logoRatio ?? MAC_APP_ICON_LOGO_RATIO
|
||||||
|
const radius = options.radius ?? Math.round(PLATE * 0.225)
|
||||||
const MARGIN = (CANVAS - PLATE) / 2
|
const MARGIN = (CANVAS - PLATE) / 2
|
||||||
const target = PLATE * 0.62
|
const target = PLATE * LOGO_RATIO
|
||||||
const { scale, tx, ty } = fitLogo(boundsSource, target, target, MARGIN + PLATE / 2, MARGIN + PLATE / 2)
|
const { scale, tx, ty } = fitLogo(boundsSource, target, target, MARGIN + PLATE / 2, MARGIN + PLATE / 2)
|
||||||
return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${CANVAS}" height="${CANVAS}" viewBox="0 0 ${CANVAS} ${CANVAS}">
|
return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${CANVAS}" height="${CANVAS}" viewBox="0 0 ${CANVAS} ${CANVAS}">
|
||||||
<rect x="${MARGIN}" y="${MARGIN}" width="${PLATE}" height="${PLATE}" rx="185" ry="185" fill="${bg}"/>
|
<rect x="${MARGIN}" y="${MARGIN}" width="${PLATE}" height="${PLATE}" rx="${radius}" ry="${radius}" fill="${bg}"/>
|
||||||
<g transform="translate(${tx.toFixed(2)},${ty.toFixed(2)}) scale(${scale.toFixed(6)})">${logoSource.inner}</g>
|
<g transform="translate(${tx.toFixed(2)},${ty.toFixed(2)}) scale(${scale.toFixed(6)})">${logoSource.inner}</g>
|
||||||
</svg>`
|
</svg>`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function windowsIcoIconSvg(bg, logoSource = windowsTrayLogo, boundsSource = windowsTrayLogoBounds) {
|
||||||
|
// Windows .ico taskbar layers are tiny in practice; this denser composition
|
||||||
|
// keeps the foreground mark legible at 24-32px without changing icon.png.
|
||||||
|
return squareIconSvg(bg, logoSource, boundsSource, {
|
||||||
|
plate: WINDOWS_ICO_PLATE,
|
||||||
|
logoRatio: WINDOWS_ICO_LOGO_RATIO,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function appPngIconSvg(bg, logoSource = windowsTrayLogo, boundsSource = windowsTrayLogoBounds) {
|
||||||
|
return squareIconSvg(bg, logoSource, boundsSource, {
|
||||||
|
plate: APP_PNG_PLATE,
|
||||||
|
logoRatio: APP_PNG_LOGO_RATIO,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tray glyph: the logo centered (with padding) inside a 32px square canvas.
|
* Tray glyph: the logo centered (with padding) inside a 32px square canvas.
|
||||||
* tray.ts loads it with scaleFactor 2, so it shows at ~16pt on retina menu
|
* tray.ts loads it with scaleFactor 2, so it shows at ~16pt on retina menu
|
||||||
@@ -284,12 +307,19 @@ function pngsToIco(pngBuffers) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildAppIcons() {
|
function buildAppIcons() {
|
||||||
// 1024 master for the normal app icon.
|
|
||||||
const masterPng = svgToPng(squareIconSvg('#ffffff', windowsTrayLogo, windowsTrayLogoBounds), 'app-normal', ['-w', '1024', '-h', '1024'])
|
|
||||||
fs.mkdirSync(BUILD_DIR, { recursive: true })
|
fs.mkdirSync(BUILD_DIR, { recursive: true })
|
||||||
fs.copyFileSync(masterPng, path.join(BUILD_DIR, 'icon.png'))
|
|
||||||
|
// 1024 master for the normal app icon.
|
||||||
|
const appPng = svgToPng(appPngIconSvg('#ffffff'), 'app-normal', ['-w', '1024', '-h', '1024'])
|
||||||
|
fs.copyFileSync(appPng, path.join(BUILD_DIR, 'icon.png'))
|
||||||
|
|
||||||
// .icns via an .iconset downscaled with sips.
|
// .icns via an .iconset downscaled with sips.
|
||||||
|
const macMasterPng = svgToPng(squareIconSvg('#ffffff', windowsTrayLogo, windowsTrayLogoBounds), 'app-normal-mac', [
|
||||||
|
'-w',
|
||||||
|
'1024',
|
||||||
|
'-h',
|
||||||
|
'1024',
|
||||||
|
])
|
||||||
const iconset = path.join(tmpDir, 'icon.iconset')
|
const iconset = path.join(tmpDir, 'icon.iconset')
|
||||||
fs.mkdirSync(iconset, { recursive: true })
|
fs.mkdirSync(iconset, { recursive: true })
|
||||||
const variants = [
|
const variants = [
|
||||||
@@ -306,7 +336,7 @@ function buildAppIcons() {
|
|||||||
]
|
]
|
||||||
for (const [size, file] of variants) {
|
for (const [size, file] of variants) {
|
||||||
const dest = path.join(iconset, file)
|
const dest = path.join(iconset, file)
|
||||||
execFileSync('sips', ['-z', String(size), String(size), masterPng, '--out', dest], {
|
execFileSync('sips', ['-z', String(size), String(size), macMasterPng, '--out', dest], {
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -315,7 +345,7 @@ function buildAppIcons() {
|
|||||||
// .ico with every size Windows asks for (taskbar/Explorer/Alt-Tab/tiles).
|
// .ico with every size Windows asks for (taskbar/Explorer/Alt-Tab/tiles).
|
||||||
const icoSizes = [16, 24, 32, 48, 64, 128, 256]
|
const icoSizes = [16, 24, 32, 48, 64, 128, 256]
|
||||||
const icoPngs = icoSizes.map((s) =>
|
const icoPngs = icoSizes.map((s) =>
|
||||||
fs.readFileSync(svgToPng(squareIconSvg('#ffffff', windowsTrayLogo, windowsTrayLogoBounds), `app-ico-${s}`, ['-w', String(s), '-h', String(s)])),
|
fs.readFileSync(svgToPng(windowsIcoIconSvg('#ffffff'), `app-ico-${s}`, ['-w', String(s), '-h', String(s)])),
|
||||||
)
|
)
|
||||||
fs.writeFileSync(path.join(BUILD_DIR, 'icon.ico'), pngsToIco(icoPngs))
|
fs.writeFileSync(path.join(BUILD_DIR, 'icon.ico'), pngsToIco(icoPngs))
|
||||||
}
|
}
|
||||||
@@ -356,7 +386,7 @@ function buildMainProcessIcons() {
|
|||||||
'// Tray icon shown when accounts have health issues (rendered @2x, not a template).',
|
'// Tray icon shown when accounts have health issues (rendered @2x, not a template).',
|
||||||
`export const TRAY_DANGER_ICON_BASE64 =\n '${trayDanger}'`,
|
`export const TRAY_DANGER_ICON_BASE64 =\n '${trayDanger}'`,
|
||||||
'',
|
'',
|
||||||
'// Dock / window icon — logo on the brand-blue plate.',
|
'// Dock / window icon — logo on the white plate, tuned for Windows taskbar visibility.',
|
||||||
`export const APP_ICON_NORMAL_BASE64 =\n '${appNormal}'`,
|
`export const APP_ICON_NORMAL_BASE64 =\n '${appNormal}'`,
|
||||||
'',
|
'',
|
||||||
'// Dock / window icon — logo on a danger plate when issues exist.',
|
'// Dock / window icon — logo on a danger plate when issues exist.',
|
||||||
|
|||||||
Reference in New Issue
Block a user