fix(brand): logo.png is bare mark on transparent, 75% padded

The previous Element-1 (full badge) approach produced a visible
warm-grey #1d1d1b square inside the cool-dark #1a1a23 sidebar — the
two near-blacks differ enough in hue to read as a foreign rectangle
in the slot.

The right answer (third time lucky): the SpaceSidebar's 40×40
rounded-[20px] tile already IS the dark squircle frame. logo.png now
composites the bare gradient mark at 75% on a transparent canvas:
the sidebar's tile bg shows through, the 25% padding keeps the mark
off the squircle edges and clear of the active-state ring, and there
is no foreign-bg colour conflict.

Refactored writeMaskablePng → writeCenteredMarkPng to share one code
path between the maskable PWA (opaque #1d1d1b background, 60% mark)
and the in-app logo (transparent background, 75% mark). Both use the
same composition pipeline; only canvas / scale / bg differ.

Only logo.png regenerated; other 21 outputs byte-identical
(determinism gate verified twice).
This commit is contained in:
Jannis Braun
2026-04-27 15:20:29 +02:00
parent 6b71aecc28
commit 7a8d0ce0e2
3 changed files with 21 additions and 14 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 10 KiB

+2 -2
View File
@@ -39,8 +39,8 @@ git history clean.
| Brand source | Drives |
|---------------------------------|-----------------------------------------------|
| `assets/brand/app-icon.svg` | Every full app icon, all favicons, PWA, in-app `logo.png` |
| `assets/brand/mark.svg` | Win/Linux tray, PWA maskable inner mark |
| `assets/brand/app-icon.svg` | Every full app icon, all favicons, PWA |
| `assets/brand/mark.svg` | Win/Linux tray, in-app `logo.png`, PWA maskable inner mark |
| `assets/brand/mark-mono-dark.svg` | macOS menu-bar template (alpha + black) |
`Artworks-Backspace/` is the design archive — never read by this script.
+19 -12
View File
@@ -92,10 +92,14 @@ async function writeIcns(path, svg) {
writeFileSync(path, icns);
}
async function writeMaskablePng(path, markSvg, canvas, scale, bgHex) {
// Compose: solid square background + bare mark centred at scale × canvas.
// 60% scale guarantees content survives Android's most aggressive launcher
// masks (Samsung One UI's full circle, Pixel's squircle).
async function writeCenteredMarkPng(path, markSvg, canvas, scale, bgHex) {
// Compose: square canvas + bare mark centred at scale × canvas.
// bgHex = hex string → opaque background (PWA maskable: survives
// Android launcher masks like Samsung One UI's
// full circle, Pixel's squircle).
// bgHex = null/undefined → transparent canvas (in-app slots whose
// container provides the visual frame, e.g.
// SpaceSidebar's 40×40 squircle tile).
const innerSize = Math.round(canvas * scale);
const inner = await sharp(markSvg, { density: SVG_DENSITY })
.resize(innerSize, innerSize, {
@@ -110,7 +114,7 @@ async function writeMaskablePng(path, markSvg, canvas, scale, bgHex) {
width: canvas,
height: canvas,
channels: 4,
background: bgHex,
background: bgHex ?? { r: 0, g: 0, b: 0, alpha: 0 },
},
})
.composite([{ input: inner, gravity: 'center' }])
@@ -191,7 +195,7 @@ async function main() {
await writePng(join(WEB_ICONS, 'icon-512.png'), appIcon, 512);
trace('pwa-512', join(WEB_ICONS, 'icon-512.png'), '512');
await writeMaskablePng(
await writeCenteredMarkPng(
join(WEB_ICONS, 'icon-maskable-512.png'),
mark,
512,
@@ -200,12 +204,15 @@ async function main() {
);
trace('pwa-maskable', join(WEB_ICONS, 'icon-maskable-512.png'), `512 (60% mark on ${MASKABLE_BG})`);
// SpaceSidebar's 40×40 rounded-[20px] tile uses object-cover. The full
// badge (Element 1) integrates cleanly because its dark bg fills the tile
// and its internal padding keeps the mark off the tile edges. Bare mark
// on transparent touched the tile edges and conflicted with active rings.
await writePng(join(WEB_ICONS, 'logo.png'), appIcon, 256);
trace('in-app-logo', join(WEB_ICONS, 'logo.png'), '256 (full badge)');
// SpaceSidebar's 40×40 rounded-[20px] tile uses object-cover and provides
// its own dark surface (`bg-surface-*`). Render the bare mark on a
// transparent canvas at 75% scale: the sidebar's tile is the visual frame,
// the 25% padding keeps the mark off the squircle edges and out of the
// way of the active-state ring, and there's no warm-bg/cool-sidebar
// mismatch (the previous full-badge approach made #1d1d1b read as a
// visible warm-grey square inside the cool #1a1a23 sidebar).
await writeCenteredMarkPng(join(WEB_ICONS, 'logo.png'), mark, 256, 0.75, null);
trace('in-app-logo', join(WEB_ICONS, 'logo.png'), '256 (75% mark, transparent)');
// --- Summary ---
const fmtBytes = (n) => {