New logo: diagonal transfer arrows + lock (SVG-first icon pipeline)

Glyph composed from mynaui arrow-diagonal-one (both diagonals) with the
mini-lock of lock-square centered between them — the macOS squircle plays the
role of lock-square's outer square. Indigo gradient + white strokes + amber
lock face for the app icon; a pure black+alpha variant for the macOS template
tray icon, plus 21 pre-rendered tray progress frames (faded arrows + fill bar,
0-100% in 5% steps) for the upcoming tray feature.

Pipeline: scripts/gen-icons.ts renders every size directly from the SVGs with
@resvg/resvg-js (dev-only dep, prebuilt binaries, no postinstall); replaces
make-icon.py + gen-iconset.sh and the sips/iconutil path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jokin
2026-07-11 21:36:40 +02:00
co-authored by Claude Fable 5
parent de75fb1480
commit c218cc2104
41 changed files with 127 additions and 125 deletions
+53
View File
@@ -0,0 +1,53 @@
// Regenerate every raster icon asset from the SVG sources (dev-time only).
// bun scripts/gen-icons.ts
// Produces: assets/icon.png (1024), assets/icon.iconset/* (macOS app icon),
// assets/tray/tray-44-template.png, assets/tray/progress/p{0..100}.png.
import { mkdir } from "node:fs/promises";
import path from "node:path";
import { Resvg } from "@resvg/resvg-js";
const root = path.join(import.meta.dir, "..");
const assets = path.join(root, "assets");
function renderPng(svg: string, sizePx: number): Uint8Array {
const resvg = new Resvg(svg, { fitTo: { mode: "width", value: sizePx } });
return resvg.render().asPng();
}
async function writePng(file: string, svg: string, sizePx: number): Promise<void> {
await mkdir(path.dirname(file), { recursive: true });
await Bun.write(file, renderPng(svg, sizePx));
}
// ---- app icon: iconset rendered per size (crisper than downscaling) ----
const logo = await Bun.file(path.join(assets, "logo.svg")).text();
await writePng(path.join(assets, "icon.png"), logo, 1024);
for (const size of [16, 32, 128, 256, 512]) {
await writePng(path.join(assets, "icon.iconset", `icon_${size}x${size}.png`), logo, size);
await writePng(path.join(assets, "icon.iconset", `icon_${size}x${size}@2x.png`), logo, size * 2);
}
// ---- tray base (22pt status item @2x) ----
const trayGlyph = await Bun.file(path.join(assets, "tray", "tray-glyph.svg")).text();
await writePng(path.join(assets, "tray", "tray-44-template.png"), trayGlyph, 44);
// ---- tray progress frames: faded arrows + centered bar filled to pct ----
function progressSvg(pct: number): string {
const trackX = 4;
const trackW = 16;
const fillW = (trackW * pct) / 100;
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g fill="none" stroke="#000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" opacity="0.35">
<path d="M9.2 9.2 4.6 4.6m0 0v4.2m0-4.2h4.2"/>
<path d="m14.8 14.8 4.6 4.6m0 0v-4.2m0 4.2h-4.2"/>
</g>
<rect x="${trackX}" y="10.7" width="${trackW}" height="2.6" rx="1.3" fill="#000" opacity="0.25"/>
${fillW > 0 ? `<rect x="${trackX}" y="10.7" width="${fillW.toFixed(2)}" height="2.6" rx="1.3" fill="#000"/>` : ""}
</svg>`;
}
for (let pct = 0; pct <= 100; pct += 5) {
await writePng(path.join(assets, "tray", "progress", `p${pct}.png`), progressSvg(pct), 44);
}
console.log("icons regenerated: app iconset, tray template, 21 progress frames");
-17
View File
@@ -1,17 +0,0 @@
#!/usr/bin/env bash
# Regenerate assets/icon.iconset from assets/icon.png (macOS: sips + iconutil).
# Electrobun compiles the .iconset folder into AppIcon.icns at build time.
set -euo pipefail
cd "$(dirname "$0")/.."
SRC="assets/icon.png"
SET="assets/icon.iconset"
rm -rf "$SET"
mkdir -p "$SET"
for size in 16 32 128 256 512; do
sips -z "$size" "$size" "$SRC" --out "$SET/icon_${size}x${size}.png" >/dev/null
sips -z $((size * 2)) $((size * 2)) "$SRC" --out "$SET/icon_${size}x${size}@2x.png" >/dev/null
done
echo "generated $SET"
-103
View File
@@ -1,103 +0,0 @@
#!/usr/bin/env python3
"""Generate a placeholder app icon (1024x1024 PNG) with a dual-panel + transfer
motif. No third-party deps — raw RGBA encoded via zlib. Replace assets/icon.png
(and re-run scripts/gen-iconset.sh) to rebrand."""
import struct
import zlib
import sys
N = 1024
def lerp(a, b, t):
return tuple(round(a[i] + (b[i] - a[i]) * t) for i in range(3))
def rounded_rect_alpha(x, y, x0, y0, x1, y1, r):
"""Coverage (0..1) for a rounded rect with ~1px antialiasing."""
# distance outside the rect's rounded boundary
dx = max(x0 + r - x, 0, x - (x1 - r))
dy = max(y0 + r - y, 0, y - (y1 - r))
if x < x0 or x > x1 or y < y0 or y > y1:
return 0.0
# corner regions
cx = None
if x < x0 + r and y < y0 + r:
cx, cy = x0 + r, y0 + r
elif x > x1 - r and y < y0 + r:
cx, cy = x1 - r, y0 + r
elif x < x0 + r and y > y1 - r:
cx, cy = x0 + r, y1 - r
elif x > x1 - r and y > y1 - r:
cx, cy = x1 - r, y1 - r
if cx is not None:
d = ((x - cx) ** 2 + (y - cy) ** 2) ** 0.5
return max(0.0, min(1.0, r - d + 0.5))
return 1.0
BG_TOP = (0x4F, 0x46, 0xE5) # indigo-600
BG_BOT = (0x18, 0x18, 0x1B) # zinc-900
PANEL = (0xFA, 0xFA, 0xFA) # near white
ROW = (0xC7, 0xCC, 0xD6)
ARROW = (0xFB, 0xBF, 0x24) # amber-400
buf = bytearray()
margin = 96
radius = 205
# panels
pgap = 70
ptop = 340
pbot = 760
pw = (N - 2 * 300 - pgap) / 2
lx0 = 300
lx1 = lx0 + pw
rx0 = lx1 + pgap
rx1 = rx0 + pw
for y in range(N):
buf.append(0) # PNG filter byte per scanline
for x in range(N):
outer = rounded_rect_alpha(x + 0.5, y + 0.5, margin, margin, N - margin, N - margin, radius)
t = y / N
col = lerp(BG_TOP, BG_BOT, t)
r, g, b = col
a = outer
# panels
for (x0, x1) in ((lx0, lx1), (rx0, rx1)):
pa = rounded_rect_alpha(x + 0.5, y + 0.5, x0, ptop, x1, pbot, 34)
if pa > 0:
r = round(r * (1 - pa) + PANEL[0] * pa)
g = round(g * (1 - pa) + PANEL[1] * pa)
b = round(b * (1 - pa) + PANEL[2] * pa)
# file rows
for row in range(4):
ry = ptop + 70 + row * 74
if ry <= y <= ry + 26 and x0 + 34 <= x <= x1 - 34:
r, g, b = ROW
# transfer arrows in the gap
cx0, cx1 = lx1 + 6, rx0 - 6
midy = (ptop + pbot) / 2
if cx0 <= x <= cx1 and abs(y - midy) < 90:
# two chevrons pointing right and left, forming <-> feel
rel = (x - cx0) / (cx1 - cx0)
band = abs(y - midy)
if band < 46 - abs(rel - 0.5) * 40:
r, g, b = ARROW
a = max(a, outer)
buf.extend((r, g, b, round(255 * a)))
def chunk(tag, data):
return struct.pack(">I", len(data)) + tag + data + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)
png = b"\x89PNG\r\n\x1a\n"
png += chunk(b"IHDR", struct.pack(">IIBBBBB", N, N, 8, 6, 0, 0, 0))
png += chunk(b"IDAT", zlib.compress(bytes(buf), 9))
png += chunk(b"IEND", b"")
out = sys.argv[1] if len(sys.argv) > 1 else "assets/icon.png"
with open(out, "wb") as f:
f.write(png)
print(f"wrote {out} ({N}x{N})")