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>
@@ -112,10 +112,11 @@ export const BRANDING = {
|
|||||||
} as const;
|
} as const;
|
||||||
```
|
```
|
||||||
|
|
||||||
To change the icon: replace `assets/icon.png` (1024×1024) and run
|
To change the icon: edit `assets/logo.svg` (app icon) and
|
||||||
`./scripts/gen-iconset.sh` (regenerates the `.iconset` with `sips`), or point
|
`assets/tray/tray-glyph.svg` (menu-bar glyph), then run
|
||||||
`icon` at your own `.iconset`. `scripts/make-icon.py` regenerates the current
|
`bun scripts/gen-icons.ts` — it regenerates the macOS iconset, `icon.png`, the
|
||||||
placeholder.
|
tray template image, and the tray progress frames from the SVGs
|
||||||
|
(`@resvg/resvg-js`, dev-only dependency).
|
||||||
|
|
||||||
> Renaming `appName` changes the app-data directory, so existing saved
|
> Renaming `appName` changes the app-data directory, so existing saved
|
||||||
> connections won't be found under the new name — copy `connections.json` across
|
> connections won't be found under the new name — copy `connections.json` across
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 736 B After Width: | Height: | Size: 619 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 241 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 241 KiB |
@@ -0,0 +1,29 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||||
|
<!-- BunSCP master logo. Glyph: the two diagonal arrows of mynaui
|
||||||
|
arrow-diagonal-one with the mini-lock of mynaui lock-square centered
|
||||||
|
between them; the squircle itself plays the role of lock-square's
|
||||||
|
outer square. Regenerate all raster assets with scripts/gen-icons.ts. -->
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0" stop-color="#818cf8"/>
|
||||||
|
<stop offset="0.5" stop-color="#4f46e5"/>
|
||||||
|
<stop offset="1" stop-color="#1e1b4b"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect x="100" y="100" width="824" height="824" rx="186" fill="url(#bg)"/>
|
||||||
|
|
||||||
|
<g transform="translate(232 232) scale(23.333)"
|
||||||
|
fill="none" stroke="#fafafa" stroke-width="1.5"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<!-- arrow to top-left -->
|
||||||
|
<path d="M8.2 8.2 4.6 4.6m0 0v4.2m0-4.2h4.2"/>
|
||||||
|
<!-- arrow to bottom-right -->
|
||||||
|
<path d="m15.8 15.8 3.6 3.6m0 0v-4.2m0 4.2h-4.2"/>
|
||||||
|
<!-- lock: shackle -->
|
||||||
|
<path d="M13.3 11.2V9.9c0-1.95-2.6-1.95-2.6 0v1.3"/>
|
||||||
|
<!-- lock: body (amber accent) -->
|
||||||
|
<rect x="9.4" y="11.2" width="5.2" height="4.6" rx="1.1"
|
||||||
|
fill="#fbbf24" stroke="#fafafa"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 449 B |
|
After Width: | Height: | Size: 447 B |
|
After Width: | Height: | Size: 468 B |
|
After Width: | Height: | Size: 473 B |
|
After Width: | Height: | Size: 479 B |
|
After Width: | Height: | Size: 486 B |
|
After Width: | Height: | Size: 477 B |
|
After Width: | Height: | Size: 487 B |
|
After Width: | Height: | Size: 477 B |
|
After Width: | Height: | Size: 485 B |
|
After Width: | Height: | Size: 457 B |
|
After Width: | Height: | Size: 476 B |
|
After Width: | Height: | Size: 482 B |
|
After Width: | Height: | Size: 479 B |
|
After Width: | Height: | Size: 485 B |
|
After Width: | Height: | Size: 492 B |
|
After Width: | Height: | Size: 485 B |
|
After Width: | Height: | Size: 483 B |
|
After Width: | Height: | Size: 482 B |
|
After Width: | Height: | Size: 477 B |
|
After Width: | Height: | Size: 481 B |
|
After Width: | Height: | Size: 583 B |
@@ -0,0 +1,11 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<!-- BunSCP tray glyph: black + alpha only (macOS template image).
|
||||||
|
Same geometry as logo.svg, slightly bolder for menu-bar sizes. -->
|
||||||
|
<g fill="none" stroke="#000" stroke-width="1.8"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M8.2 8.2 4.6 4.6m0 0v4.2m0-4.2h4.2"/>
|
||||||
|
<path d="m15.8 15.8 3.6 3.6m0 0v-4.2m0 4.2h-4.2"/>
|
||||||
|
<path d="M13.3 11.2V9.9c0-1.95-2.6-1.95-2.6 0v1.3"/>
|
||||||
|
<rect x="9.4" y="11.2" width="5.2" height="4.6" rx="1.1" fill="#000" stroke="#000"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 573 B |
@@ -9,9 +9,10 @@
|
|||||||
"ssh2": "^1.17.0",
|
"ssh2": "^1.17.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@resvg/resvg-js": "^2.6.2",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/ssh2": "^1.15.5",
|
"@types/ssh2": "^1.15.5",
|
||||||
"typescript": "~5.9.0",
|
"typescript": "~5.9.3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"ui": {
|
"ui": {
|
||||||
@@ -352,6 +353,32 @@
|
|||||||
|
|
||||||
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="],
|
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="],
|
||||||
|
|
||||||
|
"@resvg/resvg-js": ["@resvg/resvg-js@2.6.2", "", { "optionalDependencies": { "@resvg/resvg-js-android-arm-eabi": "2.6.2", "@resvg/resvg-js-android-arm64": "2.6.2", "@resvg/resvg-js-darwin-arm64": "2.6.2", "@resvg/resvg-js-darwin-x64": "2.6.2", "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2", "@resvg/resvg-js-linux-arm64-gnu": "2.6.2", "@resvg/resvg-js-linux-arm64-musl": "2.6.2", "@resvg/resvg-js-linux-x64-gnu": "2.6.2", "@resvg/resvg-js-linux-x64-musl": "2.6.2", "@resvg/resvg-js-win32-arm64-msvc": "2.6.2", "@resvg/resvg-js-win32-ia32-msvc": "2.6.2", "@resvg/resvg-js-win32-x64-msvc": "2.6.2" } }, "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q=="],
|
||||||
|
|
||||||
|
"@resvg/resvg-js-android-arm-eabi": ["@resvg/resvg-js-android-arm-eabi@2.6.2", "", { "os": "android", "cpu": "arm" }, "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA=="],
|
||||||
|
|
||||||
|
"@resvg/resvg-js-android-arm64": ["@resvg/resvg-js-android-arm64@2.6.2", "", { "os": "android", "cpu": "arm64" }, "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ=="],
|
||||||
|
|
||||||
|
"@resvg/resvg-js-darwin-arm64": ["@resvg/resvg-js-darwin-arm64@2.6.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A=="],
|
||||||
|
|
||||||
|
"@resvg/resvg-js-darwin-x64": ["@resvg/resvg-js-darwin-x64@2.6.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw=="],
|
||||||
|
|
||||||
|
"@resvg/resvg-js-linux-arm-gnueabihf": ["@resvg/resvg-js-linux-arm-gnueabihf@2.6.2", "", { "os": "linux", "cpu": "arm" }, "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw=="],
|
||||||
|
|
||||||
|
"@resvg/resvg-js-linux-arm64-gnu": ["@resvg/resvg-js-linux-arm64-gnu@2.6.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg=="],
|
||||||
|
|
||||||
|
"@resvg/resvg-js-linux-arm64-musl": ["@resvg/resvg-js-linux-arm64-musl@2.6.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg=="],
|
||||||
|
|
||||||
|
"@resvg/resvg-js-linux-x64-gnu": ["@resvg/resvg-js-linux-x64-gnu@2.6.2", "", { "os": "linux", "cpu": "x64" }, "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw=="],
|
||||||
|
|
||||||
|
"@resvg/resvg-js-linux-x64-musl": ["@resvg/resvg-js-linux-x64-musl@2.6.2", "", { "os": "linux", "cpu": "x64" }, "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ=="],
|
||||||
|
|
||||||
|
"@resvg/resvg-js-win32-arm64-msvc": ["@resvg/resvg-js-win32-arm64-msvc@2.6.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ=="],
|
||||||
|
|
||||||
|
"@resvg/resvg-js-win32-ia32-msvc": ["@resvg/resvg-js-win32-ia32-msvc@2.6.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w=="],
|
||||||
|
|
||||||
|
"@resvg/resvg-js-win32-x64-msvc": ["@resvg/resvg-js-win32-x64-msvc@2.6.2", "", { "os": "win32", "cpu": "x64" }, "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.2", "", { "os": "android", "cpu": "arm" }, "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw=="],
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.2", "", { "os": "android", "cpu": "arm" }, "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.2", "", { "os": "android", "cpu": "arm64" }, "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg=="],
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.2", "", { "os": "android", "cpu": "arm64" }, "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg=="],
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"ssh2": "^1.17.0"
|
"ssh2": "^1.17.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@resvg/resvg-js": "^2.6.2",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/ssh2": "^1.15.5",
|
"@types/ssh2": "^1.15.5",
|
||||||
"typescript": "~5.9.3"
|
"typescript": "~5.9.3"
|
||||||
|
|||||||
@@ -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");
|
||||||
@@ -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"
|
|
||||||
@@ -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})")
|
|
||||||