chore(brand): refresh app icons + add brand source artwork
Replaces desktop and web icon binaries with the new brand mark, ships PNG raster exports under assets/brand/ alongside the existing SVG sources, adds Alternative Styles artwork variants, and updates the icon generation script and System Prompt doc. deploy.sh excludes the new local-only artifacts (*.rtfd, assets/brand, electronbuild.sh, multi-platform-roadmap.md) so they don't get rsync'd to the live boxes. Drops ARCHITECTURE_AUDIT.md (obsolete).
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 200 KiB |
@@ -49,8 +49,12 @@ EXCLUDES=(
|
|||||||
--exclude='outdated trash'
|
--exclude='outdated trash'
|
||||||
--exclude='docs/superpowers'
|
--exclude='docs/superpowers'
|
||||||
--exclude='*.rtf'
|
--exclude='*.rtf'
|
||||||
|
--exclude='*.rtfd'
|
||||||
--exclude='ARCHITECTURE_AUDIT.md'
|
--exclude='ARCHITECTURE_AUDIT.md'
|
||||||
--exclude='Backspace-design-prototype.html'
|
--exclude='Backspace-design-prototype.html'
|
||||||
|
--exclude='assets/brand'
|
||||||
|
--exclude='electronbuild.sh'
|
||||||
|
--exclude='multi-platform-roadmap.md'
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── Deploy function ─────────────────────────────────────────
|
# ── Deploy function ─────────────────────────────────────────
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 364 KiB After Width: | Height: | Size: 364 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 368 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 368 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 368 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 99 KiB |
@@ -1,6 +1,6 @@
|
|||||||
# Icon Generator
|
# Icon Generator
|
||||||
|
|
||||||
Regenerates every brand artefact from `assets/brand/*.svg`:
|
Regenerates every brand artefact from sources in `assets/brand/`:
|
||||||
|
|
||||||
- macOS `.icns`, Windows `.ico`, Linux per-size PNGs (`packages/desktop/build/`)
|
- macOS `.icns`, Windows `.ico`, Linux per-size PNGs (`packages/desktop/build/`)
|
||||||
- Tray icons for all three platforms (`packages/desktop/resources/`)
|
- Tray icons for all three platforms (`packages/desktop/resources/`)
|
||||||
@@ -35,12 +35,20 @@ same PR. That follow-up commit isn't an artwork change — it's encoder
|
|||||||
output drift, and gating it inside the dep PR keeps the artwork-change
|
output drift, and gating it inside the dep PR keeps the artwork-change
|
||||||
git history clean.
|
git history clean.
|
||||||
|
|
||||||
## Source SVGs
|
## Sources
|
||||||
|
|
||||||
| Brand source | Drives |
|
App-icon outputs use a hybrid pipeline: SVG <128 px (crisp pixel-grid alignment, where 3D detail wouldn't read), 3D-rendered raster ≥128 px (the designed render shows). The closest-fit picker selects the smallest raster source whose native dimension is ≥ the target output size, so every output is a downscale — no upscale anywhere.
|
||||||
|---------------------------------|-----------------------------------------------|
|
|
||||||
| `assets/brand/app-icon.svg` | Every full app icon, all favicons, PWA, in-app `logo.png` (with `#1d1d1b → #000` bg swap for sidebar contrast) |
|
| Brand source | Drives |
|
||||||
| `assets/brand/mark.svg` | Win/Linux tray, PWA maskable inner mark |
|
|------------------------------------|-------------------------------------------------------------------------------------|
|
||||||
| `assets/brand/mark-mono-dark.svg` | macOS menu-bar template (alpha + black) |
|
| `assets/brand/app-icon.svg` | App-icon outputs <128 px (favicons 16/32, small Linux launcher reps 16/32/48/64, small `.ico` reps 16/24/32/48/64) |
|
||||||
|
| `assets/brand/app-icon-x1.png` (149) | App-icon outputs ≥128 and ≤149 (Linux 128, Windows `.ico` 128 rep) |
|
||||||
|
| `assets/brand/app-icon-x2.png` (294) | App-icon outputs >149 and ≤294 (Linux 256, Windows `.ico` 256 rep, apple-touch 180, PWA 192, in-app `logo.png` 256) |
|
||||||
|
| `assets/brand/app-icon-x3.png` (440) | App-icon outputs >294 and ≤440 (currently unused — reserved for future intermediate targets) |
|
||||||
|
| `assets/brand/app-icon-1024.png` | App-icon outputs >440 (Linux 512, Linux 1024, `build/icon.png` 512, PWA 512, `.icns` synthesis input 1024) |
|
||||||
|
| `assets/brand/mark.svg` | Win/Linux tray, PWA maskable inner mark |
|
||||||
|
| `assets/brand/mark-mono-dark.svg` | macOS menu-bar template (alpha + black) |
|
||||||
|
|
||||||
|
After raster resize, every app-icon raster output is masked with a 22 %-radius rounded-square (matches Apple's macOS template ratio and the existing `app-icon.svg` geometry of `rx=32.42 / 147.46 ≈ 21.99 %`) so launchers/docks/homescreens that render the icon as-is produce the rounded silhouette they expect.
|
||||||
|
|
||||||
`Artworks-Backspace/` is the design archive — never read by this script.
|
`Artworks-Backspace/` is the design archive — never read by this script.
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
/**
|
/**
|
||||||
* Backspace icon generator
|
* Backspace icon generator
|
||||||
*
|
*
|
||||||
* Reads from assets/brand/*.svg and writes the entire desktop + web icon set:
|
* Reads from assets/brand/{app-icon.svg, app-icon-x{1,2,3}.png, mark.svg,
|
||||||
|
* mark-mono-dark.svg} and writes the entire desktop + web icon set:
|
||||||
* - macOS .icns (10-rep iconset)
|
* - macOS .icns (10-rep iconset)
|
||||||
* - Windows .ico (multi-size)
|
* - Windows .ico (multi-size)
|
||||||
* - Linux per-size PNGs (electron-builder dir mode)
|
* - Linux per-size PNGs (electron-builder dir mode)
|
||||||
@@ -13,6 +14,14 @@
|
|||||||
*
|
*
|
||||||
* Run via `pnpm gen-icons` after artwork changes; commit the diff.
|
* Run via `pnpm gen-icons` after artwork changes; commit the diff.
|
||||||
*
|
*
|
||||||
|
* APP-ICON HYBRID RENDERING: app-icon outputs ≥128 px source from the 3D
|
||||||
|
* raster PNGs (x1=149 / x2=294 / x3=440), routed by closest-fit (smallest
|
||||||
|
* source ≥ target) to minimise resampling, then masked to a rounded-square
|
||||||
|
* silhouette (22 %·side ≈ Apple's macOS template radius). Outputs <128 px
|
||||||
|
* source from app-icon.svg — flat geometry stays crisp at favicon sizes
|
||||||
|
* where the 3D detail wouldn't read anyway. Tray icons and the PWA
|
||||||
|
* maskable inner remain SVG-only.
|
||||||
|
*
|
||||||
* DETERMINISM: byte-stable for a given lockfile only. After bumping
|
* DETERMINISM: byte-stable for a given lockfile only. After bumping
|
||||||
* sharp / png-to-ico / png2icons, expect a follow-up regen+commit in
|
* sharp / png-to-ico / png2icons, expect a follow-up regen+commit in
|
||||||
* the dep-bump PR — that diff isn't an artwork change, just upstream
|
* the dep-bump PR — that diff isn't an artwork change, just upstream
|
||||||
@@ -31,9 +40,13 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|||||||
const ROOT = join(__dirname, '..');
|
const ROOT = join(__dirname, '..');
|
||||||
|
|
||||||
const SRC = {
|
const SRC = {
|
||||||
appIcon: join(ROOT, 'assets/brand/app-icon.svg'),
|
appIcon: join(ROOT, 'assets/brand/app-icon.svg'),
|
||||||
mark: join(ROOT, 'assets/brand/mark.svg'),
|
appIconPngX1: join(ROOT, 'assets/brand/app-icon-x1.png'),
|
||||||
markMonoDark: join(ROOT, 'assets/brand/mark-mono-dark.svg'),
|
appIconPngX2: join(ROOT, 'assets/brand/app-icon-x2.png'),
|
||||||
|
appIconPngX3: join(ROOT, 'assets/brand/app-icon-x3.png'),
|
||||||
|
appIconPng1024: join(ROOT, 'assets/brand/app-icon-1024.png'),
|
||||||
|
mark: join(ROOT, 'assets/brand/mark.svg'),
|
||||||
|
markMonoDark: join(ROOT, 'assets/brand/mark-mono-dark.svg'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const DESKTOP_BUILD = join(ROOT, 'packages/desktop/build');
|
const DESKTOP_BUILD = join(ROOT, 'packages/desktop/build');
|
||||||
@@ -51,9 +64,42 @@ const MASKABLE_BG = '#1d1d1b';
|
|||||||
// is fine and keeps output stable across all target sizes.
|
// is fine and keeps output stable across all target sizes.
|
||||||
const SVG_DENSITY = 1200;
|
const SVG_DENSITY = 1200;
|
||||||
|
|
||||||
|
// Sizes at or above this threshold render from the 3D raster PNG sources;
|
||||||
|
// smaller targets render from app-icon.svg. 128 picked because:
|
||||||
|
// - dock / launcher / homescreen / PWA tile thumbnails are all ≥128 px,
|
||||||
|
// where the 3D-rendered design intent reads;
|
||||||
|
// - <128 (favicons, small Linux launcher slots, small .ico subset) reads
|
||||||
|
// better from flat SVG — crisp pixel grid alignment, and the 3D detail
|
||||||
|
// wouldn't be visible at that size regardless.
|
||||||
|
const RASTER_THRESHOLD = 128;
|
||||||
|
|
||||||
|
// Rounded-square corner radius as a fraction of the side length. 0.22
|
||||||
|
// matches the existing app-icon.svg geometry (rx=32.42 on a 147.46 viewBox
|
||||||
|
// = 21.99 %) and sits within Apple's macOS app-icon template ratio
|
||||||
|
// (~22.37 % on the 824×824 grid) — both produce visually identical
|
||||||
|
// rounding at typical icon sizes.
|
||||||
|
const SQUIRCLE_RADIUS_RATIO = 0.22;
|
||||||
|
|
||||||
|
// Multi-resolution PNG sources for the 3D-rendered app icon. The x1/x2/x3
|
||||||
|
// variants are the user-supplied @1x/@2x/@3x exports of the same render;
|
||||||
|
// the 1024 variant is the full-resolution master. Each is independently
|
||||||
|
// sampled at its native DPI rather than downscaled from a single master.
|
||||||
|
// We pick the smallest source whose native dimension is ≥ the target
|
||||||
|
// output size: minimises resampling distance (closer source resolution
|
||||||
|
// → cleaner result), and the 1024 variant ensures every target ≤1024
|
||||||
|
// is a downscale (no upscale anywhere, including the 1024 Linux output
|
||||||
|
// and the .icns synthesis input).
|
||||||
|
const APP_ICON_PNG_SOURCES = [
|
||||||
|
{ key: 'appIconPngX1', size: 149 },
|
||||||
|
{ key: 'appIconPngX2', size: 294 },
|
||||||
|
{ key: 'appIconPngX3', size: 440 },
|
||||||
|
{ key: 'appIconPng1024', size: 1024 },
|
||||||
|
];
|
||||||
|
|
||||||
// ---- helpers ----
|
// ---- helpers ----
|
||||||
|
|
||||||
const loadSvg = (path) => readFileSync(path);
|
const loadSvg = (path) => readFileSync(path);
|
||||||
|
const loadPng = (path) => readFileSync(path);
|
||||||
|
|
||||||
async function renderPng(svg, size) {
|
async function renderPng(svg, size) {
|
||||||
// Render SVG → square PNG at exact target size. fit: 'contain' preserves
|
// Render SVG → square PNG at exact target size. fit: 'contain' preserves
|
||||||
@@ -68,12 +114,61 @@ async function renderPng(svg, size) {
|
|||||||
.toBuffer();
|
.toBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pickAppIconPngSource(sources, target) {
|
||||||
|
// Smallest source ≥ target; if every source is smaller, use the largest.
|
||||||
|
for (const s of APP_ICON_PNG_SOURCES) {
|
||||||
|
if (s.size >= target) return sources[s.key];
|
||||||
|
}
|
||||||
|
return sources[APP_ICON_PNG_SOURCES[APP_ICON_PNG_SOURCES.length - 1].key];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderAppIconPngFromRaster(sources, size) {
|
||||||
|
const raster = pickAppIconPngSource(sources, size);
|
||||||
|
// fit: 'cover' is safe — every PNG source is square, so cover/contain
|
||||||
|
// produce identical pixels but cover avoids gratuitous transparent
|
||||||
|
// padding logic if a future variant ships non-square.
|
||||||
|
// kernel: lanczos3 is sharp's standard high-quality resampler for both
|
||||||
|
// up- and down-scaling; chosen explicitly for byte-stable determinism
|
||||||
|
// across sharp versions that change the default kernel.
|
||||||
|
const resized = await sharp(raster)
|
||||||
|
.resize(size, size, { fit: 'cover', kernel: sharp.kernel.lanczos3 })
|
||||||
|
.png({ compressionLevel: 9, palette: false })
|
||||||
|
.toBuffer();
|
||||||
|
// Squircle mask: clip corners to transparent so launchers / docks /
|
||||||
|
// homescreens that render the icon as-is produce the rounded silhouette
|
||||||
|
// they expect, instead of a hard-edged square. Apple's macOS template
|
||||||
|
// ratio is ~22.37 %; we use 22 % to match the geometry of the existing
|
||||||
|
// app-icon.svg (rx=32.42/147.46 ≈ 21.99 %).
|
||||||
|
const r = Math.round(size * SQUIRCLE_RADIUS_RATIO);
|
||||||
|
const mask = Buffer.from(
|
||||||
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}"><rect x="0" y="0" width="${size}" height="${size}" rx="${r}" ry="${r}" fill="#fff"/></svg>`,
|
||||||
|
);
|
||||||
|
return sharp(resized)
|
||||||
|
.composite([{ input: mask, blend: 'dest-in' }])
|
||||||
|
.png({ compressionLevel: 9, palette: false })
|
||||||
|
.toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderAppIcon(sources, size) {
|
||||||
|
// Hybrid: raster ≥ threshold, vector below it. See RASTER_THRESHOLD.
|
||||||
|
if (size >= RASTER_THRESHOLD) {
|
||||||
|
return renderAppIconPngFromRaster(sources, size);
|
||||||
|
}
|
||||||
|
return renderPng(sources.appIcon, size);
|
||||||
|
}
|
||||||
|
|
||||||
async function writePng(path, svg, size) {
|
async function writePng(path, svg, size) {
|
||||||
mkdirSync(dirname(path), { recursive: true });
|
mkdirSync(dirname(path), { recursive: true });
|
||||||
const buf = await renderPng(svg, size);
|
const buf = await renderPng(svg, size);
|
||||||
writeFileSync(path, buf);
|
writeFileSync(path, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function writeAppIconPng(path, sources, size) {
|
||||||
|
mkdirSync(dirname(path), { recursive: true });
|
||||||
|
const buf = await renderAppIcon(sources, size);
|
||||||
|
writeFileSync(path, buf);
|
||||||
|
}
|
||||||
|
|
||||||
async function writeIco(path, svg, sizes) {
|
async function writeIco(path, svg, sizes) {
|
||||||
mkdirSync(dirname(path), { recursive: true });
|
mkdirSync(dirname(path), { recursive: true });
|
||||||
const buffers = await Promise.all(sizes.map((s) => renderPng(svg, s)));
|
const buffers = await Promise.all(sizes.map((s) => renderPng(svg, s)));
|
||||||
@@ -81,12 +176,30 @@ async function writeIco(path, svg, sizes) {
|
|||||||
writeFileSync(path, ico);
|
writeFileSync(path, ico);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function writeIcns(path, svg) {
|
async function writeAppIconIco(path, sources, sizes) {
|
||||||
// png2icons.createICNS takes one high-res PNG and synthesises the full
|
// Mixed-source .ico: each pixel size routes through renderAppIcon, so
|
||||||
// 10-rep iconset internally (16/16@2x, 32/32@2x, 128/128@2x, 256/256@2x,
|
// 16/24/32/48/64 come from the SVG and 128/256 from the raster — Windows
|
||||||
// 512/512@2x). Render at 1024 for full @2x coverage.
|
// auto-picks the closest size for the active DPI, getting flat-crisp
|
||||||
|
// small variants for taskbar/Properties and the designed render at the
|
||||||
|
// larger sizes that Alt+Tab and explorer thumbnails use.
|
||||||
mkdirSync(dirname(path), { recursive: true });
|
mkdirSync(dirname(path), { recursive: true });
|
||||||
const src = await renderPng(svg, 1024);
|
const buffers = await Promise.all(sizes.map((s) => renderAppIcon(sources, s)));
|
||||||
|
const ico = await pngToIco(buffers);
|
||||||
|
writeFileSync(path, ico);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeAppIconIcns(path, sources) {
|
||||||
|
// png2icons.createICNS takes a single high-res PNG and synthesises the
|
||||||
|
// full 10-rep iconset internally (16/16@2x, 32/32@2x, 128/128@2x,
|
||||||
|
// 256/256@2x, 512/512@2x). We feed it the 1024 raster output (designed
|
||||||
|
// 3D render, squircle-masked) and accept that the synthesised 16/32 reps
|
||||||
|
// are downsampled from raster rather than re-rendered from SVG. macOS
|
||||||
|
// surfaces .icns reps mostly at ≥128 (Dock, Mission Control, Launchpad)
|
||||||
|
// — the only place the small-rep softness shows is Finder column view,
|
||||||
|
// a worthwhile trade for a single-file .icns build that matches every
|
||||||
|
// other app-icon consumer's design intent.
|
||||||
|
mkdirSync(dirname(path), { recursive: true });
|
||||||
|
const src = await renderAppIcon(sources, 1024);
|
||||||
const icns = png2icons.createICNS(src, png2icons.BICUBIC, 0);
|
const icns = png2icons.createICNS(src, png2icons.BICUBIC, 0);
|
||||||
if (!icns) throw new Error(`png2icons.createICNS returned null for ${path}`);
|
if (!icns) throw new Error(`png2icons.createICNS returned null for ${path}`);
|
||||||
writeFileSync(path, icns);
|
writeFileSync(path, icns);
|
||||||
@@ -140,6 +253,14 @@ async function main() {
|
|||||||
const mark = loadSvg(SRC.mark);
|
const mark = loadSvg(SRC.mark);
|
||||||
const markMonoDark = loadSvg(SRC.markMonoDark);
|
const markMonoDark = loadSvg(SRC.markMonoDark);
|
||||||
|
|
||||||
|
const appIconSources = {
|
||||||
|
appIcon,
|
||||||
|
appIconPngX1: loadPng(SRC.appIconPngX1),
|
||||||
|
appIconPngX2: loadPng(SRC.appIconPngX2),
|
||||||
|
appIconPngX3: loadPng(SRC.appIconPngX3),
|
||||||
|
appIconPng1024: loadPng(SRC.appIconPng1024),
|
||||||
|
};
|
||||||
|
|
||||||
const written = [];
|
const written = [];
|
||||||
const trace = (label, path, info) =>
|
const trace = (label, path, info) =>
|
||||||
written.push({
|
written.push({
|
||||||
@@ -153,18 +274,22 @@ async function main() {
|
|||||||
const linuxSizes = [16, 32, 48, 64, 128, 256, 512, 1024];
|
const linuxSizes = [16, 32, 48, 64, 128, 256, 512, 1024];
|
||||||
for (const s of linuxSizes) {
|
for (const s of linuxSizes) {
|
||||||
const out = join(DESKTOP_BUILD, `icons/${s}x${s}.png`);
|
const out = join(DESKTOP_BUILD, `icons/${s}x${s}.png`);
|
||||||
await writePng(out, appIcon, s);
|
await writeAppIconPng(out, appIconSources, s);
|
||||||
trace('linux-png', out, `${s}x${s}`);
|
trace('linux-png', out, `${s}x${s} (${s >= RASTER_THRESHOLD ? 'raster' : 'svg'})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await writePng(join(DESKTOP_BUILD, 'icon.png'), appIcon, 512);
|
await writeAppIconPng(join(DESKTOP_BUILD, 'icon.png'), appIconSources, 512);
|
||||||
trace('build-icon', join(DESKTOP_BUILD, 'icon.png'), '512x512');
|
trace('build-icon', join(DESKTOP_BUILD, 'icon.png'), '512x512 (raster)');
|
||||||
|
|
||||||
await writeIcns(join(DESKTOP_BUILD, 'icon.icns'), appIcon);
|
await writeAppIconIcns(join(DESKTOP_BUILD, 'icon.icns'), appIconSources);
|
||||||
trace('mac-icns', join(DESKTOP_BUILD, 'icon.icns'), '10-rep iconset');
|
trace('mac-icns', join(DESKTOP_BUILD, 'icon.icns'), '10-rep iconset (raster)');
|
||||||
|
|
||||||
await writeIco(join(DESKTOP_BUILD, 'icon.ico'), appIcon, [16, 24, 32, 48, 64, 128, 256]);
|
await writeAppIconIco(
|
||||||
trace('win-ico', join(DESKTOP_BUILD, 'icon.ico'), '7 sizes');
|
join(DESKTOP_BUILD, 'icon.ico'),
|
||||||
|
appIconSources,
|
||||||
|
[16, 24, 32, 48, 64, 128, 256],
|
||||||
|
);
|
||||||
|
trace('win-ico', join(DESKTOP_BUILD, 'icon.ico'), '7 sizes (svg<128, raster≥128)');
|
||||||
|
|
||||||
// --- Desktop: tray ---
|
// --- Desktop: tray ---
|
||||||
await writePng(join(DESKTOP_RES, 'tray-iconTemplate.png'), markMonoDark, 22);
|
await writePng(join(DESKTOP_RES, 'tray-iconTemplate.png'), markMonoDark, 22);
|
||||||
@@ -180,20 +305,20 @@ async function main() {
|
|||||||
trace('tray-linux', join(DESKTOP_RES, 'tray-icon.png'), '22x22');
|
trace('tray-linux', join(DESKTOP_RES, 'tray-icon.png'), '22x22');
|
||||||
|
|
||||||
// --- Web: favicons + PWA + in-app ---
|
// --- Web: favicons + PWA + in-app ---
|
||||||
await writePng(join(WEB_ICONS, 'favicon-16.png'), appIcon, 16);
|
await writeAppIconPng(join(WEB_ICONS, 'favicon-16.png'), appIconSources, 16);
|
||||||
trace('favicon-16', join(WEB_ICONS, 'favicon-16.png'), '16');
|
trace('favicon-16', join(WEB_ICONS, 'favicon-16.png'), '16 (svg)');
|
||||||
|
|
||||||
await writePng(join(WEB_ICONS, 'favicon-32.png'), appIcon, 32);
|
await writeAppIconPng(join(WEB_ICONS, 'favicon-32.png'), appIconSources, 32);
|
||||||
trace('favicon-32', join(WEB_ICONS, 'favicon-32.png'), '32');
|
trace('favicon-32', join(WEB_ICONS, 'favicon-32.png'), '32 (svg)');
|
||||||
|
|
||||||
await writePng(join(WEB_ICONS, 'apple-touch-icon.png'), appIcon, 180);
|
await writeAppIconPng(join(WEB_ICONS, 'apple-touch-icon.png'), appIconSources, 180);
|
||||||
trace('apple-touch', join(WEB_ICONS, 'apple-touch-icon.png'), '180');
|
trace('apple-touch', join(WEB_ICONS, 'apple-touch-icon.png'), '180 (raster)');
|
||||||
|
|
||||||
await writePng(join(WEB_ICONS, 'icon-192.png'), appIcon, 192);
|
await writeAppIconPng(join(WEB_ICONS, 'icon-192.png'), appIconSources, 192);
|
||||||
trace('pwa-192', join(WEB_ICONS, 'icon-192.png'), '192');
|
trace('pwa-192', join(WEB_ICONS, 'icon-192.png'), '192 (raster)');
|
||||||
|
|
||||||
await writePng(join(WEB_ICONS, 'icon-512.png'), appIcon, 512);
|
await writeAppIconPng(join(WEB_ICONS, 'icon-512.png'), appIconSources, 512);
|
||||||
trace('pwa-512', join(WEB_ICONS, 'icon-512.png'), '512');
|
trace('pwa-512', join(WEB_ICONS, 'icon-512.png'), '512 (raster)');
|
||||||
|
|
||||||
await writeCenteredMarkPng(
|
await writeCenteredMarkPng(
|
||||||
join(WEB_ICONS, 'icon-maskable-512.png'),
|
join(WEB_ICONS, 'icon-maskable-512.png'),
|
||||||
@@ -204,19 +329,19 @@ async function main() {
|
|||||||
);
|
);
|
||||||
trace('pwa-maskable', join(WEB_ICONS, 'icon-maskable-512.png'), `512 (60% mark on ${MASKABLE_BG})`);
|
trace('pwa-maskable', join(WEB_ICONS, 'icon-maskable-512.png'), `512 (60% mark on ${MASKABLE_BG})`);
|
||||||
|
|
||||||
// Logo for the SpaceSidebar home tile: full Element 1 badge with the
|
// Logo for the SpaceSidebar home tile: routes through the standard
|
||||||
// bg fill swapped from #1d1d1b → #000000. The badge IS the brand
|
// app-icon hybrid path (raster ≥128, squircle-masked) — same designed
|
||||||
// identity at small sizes (full mark mass + own internal padding),
|
// 3D render as desktop launcher / dock / homescreen, just sized for
|
||||||
// and pure black against the sidebar's #1a1a23 reads as deliberately
|
// the sidebar slot. The squircle's 22 %-radius transparent corners are
|
||||||
// darker (intentional dark tile) rather than as a warm/cool mismatch
|
// fully contained by the sidebar's own CSS mask (`rounded-[20px → 13px]`
|
||||||
// (the original #1d1d1b looked like an off-grey rectangle). The
|
// on a 40×40 tile = 50 % → 32.5 % radius — both more aggressive than
|
||||||
// sidebar's overflow-hidden + rounded-[20px → 13px] morph clips the
|
// 22 %), so no transparent gaps show against the `#1a1a23` surface.
|
||||||
// badge cleanly because the bg is fully opaque.
|
// The new raster's vignetted dark-gradient corners visually replace the
|
||||||
const logoSvg = Buffer.from(
|
// legacy `#1d1d1b → #000000` SVG fill swap that was needed to make the
|
||||||
appIcon.toString('utf8').replace(/#1d1d1b/gi, '#000000'),
|
// flat badge read as an intentional dark tile rather than a warm-grey
|
||||||
);
|
// rectangle — the 3D render carries its own dark surround.
|
||||||
await writePng(join(WEB_ICONS, 'logo.png'), logoSvg, 256);
|
await writeAppIconPng(join(WEB_ICONS, 'logo.png'), appIconSources, 256);
|
||||||
trace('in-app-logo', join(WEB_ICONS, 'logo.png'), '256 (full badge, #000)');
|
trace('in-app-logo', join(WEB_ICONS, 'logo.png'), '256 (raster, sidebar tile)');
|
||||||
|
|
||||||
// --- Summary ---
|
// --- Summary ---
|
||||||
const fmtBytes = (n) => {
|
const fmtBytes = (n) => {
|
||||||
|
|||||||