icons: per-platform icon set — real Liquid Glass on macOS, flat design on Windows/Linux
The hand-packed icns was pixel-clean (verified by unpacking and scanning every size), but macOS 26 composites legacy .icns icons into the Liquid Glass system at render time; stacked on Icon Composer's baked PNG-export lighting, that clips the rim highlights into the near-corner specks and abrupt highlight ends (owner report 2026-10-13). The fix is the real layered icon: mac.icon now points at the Icon Composer document (build/icon.icon, copied from ~/Documents/Affinity/lh-logo) and electron-builder compiles it via actool into Assets.car — macOS 26+ renders the true Liquid Glass design (dark/tinted variants included). actool's backwards-compat icns caps at 256px, so scripts/after-pack.cjs swaps in the existing full-res build/icon.icns before signing: pre-Tahoe macOS keeps today's quality, Tahoe gets the layered icon. Windows/Linux get the owner's flat design (rounded squircle with baked transparency): scripts/make-icons.mjs renders build/icon-flat.svg via resvg into build/icon.ico (16→256 PNG entries) + build/icon.png (512), and electron-builder.yml pins win.icon/linux.icon. desktop:dist injects DEVELOPER_DIR (actool needs Xcode; the machine selects CommandLineTools by default, CI's setup-xcode takes precedence via the :- default). The release workflow's mac job selects Xcode 26+ explicitly. Verified locally: mac --dir build (Assets.car + checksummed full-res fallback + CFBundleIconName/IconFile) and the win-unpacked exe carries our ico bytes; NSIS/AppImage assembly is CI's (cross-build tooling can't run on arm64 macOS). Runbook updated. 1610/1610.
This commit is contained in:
@@ -113,6 +113,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: "26"
|
node-version: "26"
|
||||||
cache: npm
|
cache: npm
|
||||||
|
# The mac icon is the Icon Composer document (build/icon.icon);
|
||||||
|
# compiling it needs actool ≥ 26 (Xcode 26+). macos-15 images carry
|
||||||
|
# several Xcodes with an older default — select the latest stable
|
||||||
|
# explicitly. No-op for the windows/linux matrix legs.
|
||||||
|
- name: Select Xcode 26+ for actool (mac icon compile)
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
uses: maxim-lobanov/setup-xcode@v1
|
||||||
|
with:
|
||||||
|
xcode-version: latest-stable
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- name: Native renderer tests on this OS
|
- name: Native renderer tests on this OS
|
||||||
run: npx electron --import tsx --test tests/pdf-native-render.test.ts tests/pdf-ops.test.ts tests/svg-native-render.test.ts tests/browser-service.test.ts
|
run: npx electron --import tsx --test tests/pdf-native-render.test.ts tests/pdf-ops.test.ts tests/svg-native-render.test.ts tests/browser-service.test.ts
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<path d="M1023.994,665.998C1023.994,679.668 1024,693.336 1023.914,707.006C1023.847,718.519 1023.715,730.031 1023.403,741.541C1022.723,766.623 1021.247,791.924 1016.788,816.727C1012.265,841.89 1004.883,865.309 993.246,888.171C981.808,910.642 966.862,931.205 949.033,949.033C931.205,966.862 910.642,981.808 888.171,993.246C865.309,1004.883 841.89,1012.265 816.727,1016.788C791.924,1021.247 766.623,1022.723 741.541,1023.403C730.031,1023.715 718.519,1023.847 707.006,1023.914C693.336,1024 679.668,1023.994 665.998,1023.994L358.002,1023.994C344.332,1023.994 330.664,1024 316.994,1023.914C305.481,1023.847 293.969,1023.715 282.459,1023.403C257.377,1022.723 232.076,1021.247 207.272,1016.788C182.11,1012.265 158.691,1004.883 135.829,993.246C113.358,981.808 92.795,966.862 74.967,949.033C57.138,931.205 42.192,910.642 30.754,888.171C19.117,865.309 11.735,841.89 7.212,816.727C2.753,791.924 1.277,766.623 0.597,741.541C0.285,730.031 0.153,718.519 0.086,707.006C0,693.336 0.006,679.668 0.006,665.998L0.006,358.002C0.006,344.332 0,330.664 0.086,316.994C0.153,305.481 0.285,293.969 0.597,282.459C1.277,257.377 2.753,232.076 7.212,207.272C11.735,182.11 19.117,158.691 30.754,135.829C42.192,113.358 57.138,92.795 74.967,74.967C92.795,57.138 113.358,42.192 135.829,30.754C158.691,19.117 182.11,11.735 207.272,7.212C232.076,2.753 257.377,1.277 282.459,0.597C293.969,0.285 305.481,0.153 316.994,0.086C330.664,0 344.332,0.006 358.002,0.006L665.998,0.006C679.668,0.006 693.336,0 707.006,0.086C718.519,0.153 730.031,0.285 741.541,0.597C766.623,1.277 791.924,2.753 816.727,7.212C841.89,11.735 865.309,19.117 888.171,30.754C910.642,42.192 931.205,57.138 949.033,74.967C966.862,92.795 981.808,113.358 993.246,135.829C1004.883,158.691 1012.265,182.11 1016.788,207.272C1021.247,232.076 1022.723,257.377 1023.403,282.459C1023.715,293.969 1023.847,305.481 1023.914,316.994C1024,330.664 1023.994,344.332 1023.994,358.002L1023.994,665.998Z" style="fill:rgb(20,20,20);"/>
|
||||||
|
<g transform="matrix(1,0,0,1,0.001713,-0.000688)">
|
||||||
|
<g transform="matrix(0.939904,0,0,0.939904,30.769428,30.766742)">
|
||||||
|
<g transform="matrix(-0.232199,0,0,0.232199,1384.403797,-416.791824)">
|
||||||
|
<g transform="matrix(0.573992,-2.142166,2.142166,0.573992,-5516.07298,4432.117471)">
|
||||||
|
<path d="M1234.728,3305.937C871.642,3324.965 587,3624.915 587,3988.5C587,4365.734 893.266,4672 1270.5,4672C1647.734,4672 1954,4365.734 1954,3988.5C1954,3880.637 1928.472,3774.304 1879.503,3678.197L1794.243,3721.64C1836.356,3804.292 1858.31,3895.737 1858.31,3988.5C1858.31,4312.921 1594.921,4576.31 1270.5,4576.31C946.079,4576.31 682.69,4312.921 682.69,3988.5C682.69,3675.817 927.482,3417.86 1239.736,3401.496L1234.728,3305.937Z" style="fill:rgb(148,148,148);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.261034,0,0,1.261034,1170.373407,-2014.261188)">
|
||||||
|
<path d="M1270.5,3305C893.266,3305 587,3611.266 587,3988.5C587,4365.734 893.266,4672 1270.5,4672C1647.734,4672 1954,4365.734 1954,3988.5C1954,3611.266 1647.734,3305 1270.5,3305ZM1270.5,3471.537C1555.819,3471.537 1787.463,3703.181 1787.463,3988.5C1787.463,4273.819 1555.819,4505.463 1270.5,4505.463C985.181,4505.463 753.537,4273.819 753.537,3988.5C753.537,3703.181 985.181,3471.537 1270.5,3471.537Z" style="fill:white;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.261034,0,0,1.261034,3139.627182,-45.007413)">
|
||||||
|
<circle cx="1270.5" cy="3988.5" r="683.5" style="fill:white;"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(0.939904,0,0,0.939904,30.769428,30.766742)">
|
||||||
|
<g transform="matrix(-0.13328,-0.497408,-0.497408,0.13328,2665.228569,612.339928)">
|
||||||
|
<path d="M1234.728,3305.937C871.642,3324.965 587,3624.915 587,3988.5C587,4365.734 893.266,4672 1270.5,4672C1647.734,4672 1954,4365.734 1954,3988.5C1954,3880.637 1928.472,3774.304 1879.503,3678.197L1794.243,3721.64C1836.356,3804.292 1858.31,3895.737 1858.31,3988.5C1858.31,4312.921 1594.921,4576.31 1270.5,4576.31C946.079,4576.31 682.69,4312.921 682.69,3988.5C682.69,3675.817 927.482,3417.86 1239.736,3401.496L1234.728,3305.937Z" style="fill:rgb(148,148,148);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(0.939904,0,0,0.939904,30.769428,30.766742)">
|
||||||
|
<g transform="matrix(-0.29281,-0,-0,0.29281,1112.644657,-884.500579)">
|
||||||
|
<path d="M1270.5,3305C893.266,3305 587,3611.266 587,3988.5C587,4365.734 893.266,4672 1270.5,4672C1647.734,4672 1954,4365.734 1954,3988.5C1954,3611.266 1647.734,3305 1270.5,3305ZM1270.5,3471.537C1555.819,3471.537 1787.463,3703.181 1787.463,3988.5C1787.463,4273.819 1555.819,4505.463 1270.5,4505.463C985.181,4505.463 753.537,4273.819 753.537,3988.5C753.537,3703.181 985.181,3471.537 1270.5,3471.537Z" style="fill:white;"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(0.939904,0,0,0.939904,30.769428,30.766742)">
|
||||||
|
<g transform="matrix(-0.29281,0,0,0.29281,655.386562,-427.242485)">
|
||||||
|
<circle cx="1270.5" cy="3988.5" r="683.5" style="fill:white;"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 698 B |
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"fill" : {
|
||||||
|
"automatic-gradient" : "srgb:0.07843,0.07843,0.07843,1.00000"
|
||||||
|
},
|
||||||
|
"groups" : [
|
||||||
|
{
|
||||||
|
"layers" : [
|
||||||
|
{
|
||||||
|
"image-name" : "lh_2 2.svg",
|
||||||
|
"name" : "lh_2 2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image-name" : "lh_1 2.svg",
|
||||||
|
"name" : "lh_1 2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"image-name" : "lh_0 2.svg",
|
||||||
|
"name" : "lh_0 2"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shadow" : {
|
||||||
|
"kind" : "neutral",
|
||||||
|
"opacity" : 0.5
|
||||||
|
},
|
||||||
|
"translucency" : {
|
||||||
|
"enabled" : true,
|
||||||
|
"value" : 0.3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"supported-platforms" : {
|
||||||
|
"circles" : [
|
||||||
|
"watchOS"
|
||||||
|
],
|
||||||
|
"squares" : "shared"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -12,7 +12,7 @@ Windows signing is deliberately disabled for this first release. SmartScreen may
|
|||||||
|
|
||||||
The custom app bar stays on all three platforms. macOS keeps its left-side traffic lights; Windows/Linux use Electron's native right-side Window Controls Overlay, with CSS space reserved from the overlay's actual geometry. Windows/Linux hide the native menu until Alt is pressed. macOS keeps its system menu.
|
The custom app bar stays on all three platforms. macOS keeps its left-side traffic lights; Windows/Linux use Electron's native right-side Window Controls Overlay, with CSS space reserved from the overlay's actual geometry. Windows/Linux hide the native menu until Alt is pressed. macOS keeps its system menu.
|
||||||
|
|
||||||
The existing `build/icon.icns` is the source for all platforms: electron-builder converts it to ICO/Linux PNGs. Linux's desktop identity is `learnharness.desktop`. Public package-maintainer metadata uses the owner's GitHub no-reply address; support goes through [GitHub Issues](https://github.com/amosblomqvist/learnharness/issues).
|
Icons are per-platform (2026-10-13): macOS compiles the Icon Composer document `build/icon.icon` via actool into `Assets.car` (macOS 26 Liquid Glass) plus a fallback icns that `scripts/after-pack.cjs` replaces with the hand-packed full-res `build/icon.icns` (actool's caps at 256px); Windows embeds `build/icon.ico` and Linux uses `build/icon.png`, both generated from the flat design (`build/icon-flat.svg`) by `node scripts/make-icons.mjs`. Building the mac icon needs Xcode 26+ (`actool`) — `desktop:dist` injects `DEVELOPER_DIR` locally and the release workflow selects Xcode via setup-xcode. Linux's desktop identity is `learnharness.desktop`. Public package-maintainer metadata uses the owner's GitHub no-reply address; support goes through [GitHub Issues](https://github.com/amosblomqvist/learnharness/issues).
|
||||||
|
|
||||||
## Build pipeline
|
## Build pipeline
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,19 @@ appId: com.learnharness.LearnHarness
|
|||||||
productName: LearnHarness
|
productName: LearnHarness
|
||||||
directories:
|
directories:
|
||||||
output: dist
|
output: dist
|
||||||
|
# Icons (owner ruling, 2026-10-13) — per-platform sources in build/:
|
||||||
|
# mac build/icon.icon — the Icon Composer document (layered Liquid
|
||||||
|
# Glass); actool (Xcode 26+) compiles Assets.car (CFBundleIconName,
|
||||||
|
# macOS 26+ renders the true layered icon) + an actool fallback icns
|
||||||
|
# capped at 256px, which scripts/after-pack.cjs swaps for the
|
||||||
|
# hand-packed full-res build/icon.icns (pre-Tahoe keeps 16→1024).
|
||||||
|
# Requires actool ≥ 26: desktop:dist injects DEVELOPER_DIR locally;
|
||||||
|
# the release workflow's mac job runs setup-xcode.
|
||||||
|
# win build/icon.ico — generated (16→256) from the flat design by
|
||||||
|
# scripts/make-icons.mjs.
|
||||||
|
# linux build/icon.png — 512×512, same script. Rounded squircle with
|
||||||
|
# baked transparency on both (owner ruling) — Windows/Linux don't
|
||||||
|
# mask, the shape ships in the artwork.
|
||||||
files:
|
files:
|
||||||
# Only the shell goes into the asar. ORDER IS LOAD-BEARING (payload
|
# Only the shell goes into the asar. ORDER IS LOAD-BEARING (payload
|
||||||
# wave, 2026-09-24): electron-builder's dependency collector packs the
|
# wave, 2026-09-24): electron-builder's dependency collector packs the
|
||||||
@@ -82,6 +95,7 @@ extraResources:
|
|||||||
filter:
|
filter:
|
||||||
- "standalone/**/*"
|
- "standalone/**/*"
|
||||||
mac:
|
mac:
|
||||||
|
icon: build/icon.icon
|
||||||
# zip rides next to dmg (flipped 2026-10-08): Squirrel.Mac — Electron's
|
# zip rides next to dmg (flipped 2026-10-08): Squirrel.Mac — Electron's
|
||||||
# macOS auto-updater — installs from ZIP and cannot read DMGs. DMG is
|
# macOS auto-updater — installs from ZIP and cannot read DMGs. DMG is
|
||||||
# for humans; ZIP + blockmap are the updater's payload.
|
# for humans; ZIP + blockmap are the updater's payload.
|
||||||
@@ -114,7 +128,9 @@ mac:
|
|||||||
notarize: true
|
notarize: true
|
||||||
dmg:
|
dmg:
|
||||||
title: "${productName} ${version}"
|
title: "${productName} ${version}"
|
||||||
|
afterPack: scripts/after-pack.cjs
|
||||||
win:
|
win:
|
||||||
|
icon: build/icon.ico
|
||||||
target:
|
target:
|
||||||
- target: nsis
|
- target: nsis
|
||||||
arch:
|
arch:
|
||||||
@@ -128,6 +144,7 @@ nsis:
|
|||||||
oneClick: true
|
oneClick: true
|
||||||
perMachine: false
|
perMachine: false
|
||||||
linux:
|
linux:
|
||||||
|
icon: build/icon.png
|
||||||
target:
|
target:
|
||||||
- target: AppImage
|
- target: AppImage
|
||||||
arch:
|
arch:
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@
|
|||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"desktop:dev": "concurrently -k \"next dev\" \"LH_DEV_URL=http://localhost:3000 electron .\"",
|
"desktop:dev": "concurrently -k \"next dev\" \"LH_DEV_URL=http://localhost:3000 electron .\"",
|
||||||
"dev:fresh": "bash scripts/dev-fresh.sh",
|
"dev:fresh": "bash scripts/dev-fresh.sh",
|
||||||
"desktop:dist": "rm -rf dist .next/standalone && next build && node scripts/build-server.mjs && node scripts/smoke-server.mjs && APPLE_KEYCHAIN_PROFILE=LH_NOTARY APPLE_TEAM_ID=JS6XN22HY6 electron-builder --mac --arm64"
|
"desktop:dist": "rm -rf dist .next/standalone && next build && node scripts/build-server.mjs && node scripts/smoke-server.mjs && DEVELOPER_DIR=${DEVELOPER_DIR:-/Applications/Xcode.app/Contents/Developer} APPLE_KEYCHAIN_PROFILE=LH_NOTARY APPLE_TEAM_ID=JS6XN22HY6 electron-builder --mac --arm64"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/pi-agent-core": "1.0.2",
|
"@earendil-works/pi-agent-core": "1.0.2",
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// electron-builder afterPack hook — mac only.
|
||||||
|
//
|
||||||
|
// WHY (owner ruling 2026-10-13): mac.icon is the Icon Composer document
|
||||||
|
// (build/icon.icon); actool compiles it into Assets.car (the macOS 26
|
||||||
|
// Liquid Glass render) PLUS a backwards-compat icns — but actool's
|
||||||
|
// fallback icns caps at 256px (known actool behavior), which looks soft
|
||||||
|
// at Finder/Launchpad sizes on pre-Tahoe macOS. This hook swaps in the
|
||||||
|
// hand-packed full-res build/icon.icns (all 10 entries, 16→1024, packed
|
||||||
|
// via iconutil from the design's PNG exports) AFTER packaging and BEFORE
|
||||||
|
// signing, so:
|
||||||
|
//
|
||||||
|
// macOS 26+ → Assets.car (CFBundleIconName) — true layered glass icon;
|
||||||
|
// older mac → icon.icns (CFBundleIconFile) — today's full-res flatten.
|
||||||
|
//
|
||||||
|
// Windows/Linux builds return immediately (their icons are static files:
|
||||||
|
// build/icon.ico / build/icon.png).
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
|
||||||
|
module.exports = async function afterPack(context) {
|
||||||
|
if (context.electronPlatformName !== "darwin") return;
|
||||||
|
const appBundle = path.join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`);
|
||||||
|
const target = path.join(appBundle, "Contents", "Resources", "icon.icns");
|
||||||
|
const source = path.join(context.packager.info.projectDir, "build", "icon.icns");
|
||||||
|
fs.copyFileSync(source, target);
|
||||||
|
console.log(`[after-pack] pre-Tahoe fallback icns (full-res) → ${target}`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// Regenerate the Windows/Linux icon set from the flat design source
|
||||||
|
// (build/icon-flat.svg — the owner's Affinity export, rounded squircle
|
||||||
|
// with baked transparency, owner ruling 2026-10-13).
|
||||||
|
//
|
||||||
|
// build/icon.ico — Windows (.ico with 16/24/32/48/64/128/256 PNG entries)
|
||||||
|
// build/icon.png — Linux (512×512; AppImage/deb/desktop entry)
|
||||||
|
//
|
||||||
|
// macOS does NOT come from here: build/icon.icon (the Icon Composer
|
||||||
|
// document) is the mac source of truth, compiled by actool at package
|
||||||
|
// time into Assets.car (macOS 26 Liquid Glass) + a fallback icns; the
|
||||||
|
// hand-packed build/icon.icns replaces that fallback's 256px cap via
|
||||||
|
// scripts/after-pack.cjs (pre-Tahoe keeps full-res).
|
||||||
|
//
|
||||||
|
// Renders each size DIRECTLY from the SVG (resvg) — sharper squircle
|
||||||
|
// edges than downsampling a raster. Committed outputs; rerun only when
|
||||||
|
// the design changes: `node scripts/make-icons.mjs`.
|
||||||
|
import { Resvg } from "@resvg/resvg-js";
|
||||||
|
import { readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
||||||
|
const svg = readFileSync(path.join(root, "build", "icon-flat.svg"), "utf8");
|
||||||
|
|
||||||
|
const render = (size) => new Resvg(svg, { fitTo: { mode: "width", value: size } }).render(size, size).asPng();
|
||||||
|
|
||||||
|
// ---- build/icon.ico: ICONDIR + PNG entries ------------------------------
|
||||||
|
const ICO_SIZES = [16, 24, 32, 48, 64, 128, 256];
|
||||||
|
const pngs = ICO_SIZES.map(render);
|
||||||
|
// Header (6) + one 16-byte directory entry per image, then the PNGs.
|
||||||
|
const header = Buffer.alloc(6 + ICO_SIZES.length * 16);
|
||||||
|
header.writeUInt16LE(0, 0); // reserved
|
||||||
|
header.writeUInt16LE(1, 2); // type: icon
|
||||||
|
header.writeUInt16LE(ICO_SIZES.length, 4);
|
||||||
|
let offset = header.length;
|
||||||
|
ICO_SIZES.forEach((size, i) => {
|
||||||
|
const e = 6 + i * 16;
|
||||||
|
header.writeUInt8(size === 256 ? 0 : size, e); // 0 encodes 256
|
||||||
|
header.writeUInt8(size === 256 ? 0 : size, e + 1);
|
||||||
|
header.writeUInt8(0, e + 2); // colors
|
||||||
|
header.writeUInt8(0, e + 3); // reserved
|
||||||
|
header.writeUInt16LE(1, e + 4); // planes
|
||||||
|
header.writeUInt16LE(32, e + 6); // bpp
|
||||||
|
header.writeUInt32LE(pngs[i].length, e + 8);
|
||||||
|
header.writeUInt32LE(offset, e + 12);
|
||||||
|
offset += pngs[i].length;
|
||||||
|
});
|
||||||
|
const ico = Buffer.concat([header, ...pngs]);
|
||||||
|
writeFileSync(path.join(root, "build", "icon.ico"), ico);
|
||||||
|
console.log(`build/icon.ico ${ICO_SIZES.join("/")} — ${(ico.length / 1024).toFixed(1)} KB`);
|
||||||
|
|
||||||
|
// ---- build/icon.png: the Linux icon -------------------------------------
|
||||||
|
const png512 = render(512);
|
||||||
|
writeFileSync(path.join(root, "build", "icon.png"), png512);
|
||||||
|
console.log(`build/icon.png 512×512 — ${(png512.length / 1024).toFixed(1)} KB`);
|
||||||
Reference in New Issue
Block a user