feat: rebrand app and packaging icons to new Motionity logo
Sync Gitea releases to GitHub / sync-releases (push) Canceled after 0s
Sync Gitea releases to GitHub / sync-releases (push) Canceled after 0s
Swap the small icon mark, README banner, packaged app icon, and favicon over to the new brand assets. make-icon.cjs and the new make-favicon.cjs re-derive their bar geometry/colours from src/assets/logo.svg instead of the old white-bars mark. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env node
|
||||
// Renders src/favicon.ico (32x32) from the same geometry as
|
||||
// src/assets/logo.svg: three coloured rounded bars on the app's dark background.
|
||||
//
|
||||
// Hand-rolled so packaging needs no image toolchain (no ImageMagick, no sharp).
|
||||
|
||||
const { writeFileSync } = require('node:fs');
|
||||
const { resolve } = require('node:path');
|
||||
|
||||
const SIZE = 32;
|
||||
const SS = 8; // supersampling factor, for antialiased edges at small size
|
||||
const BG = [0x14, 0x16, 0x29];
|
||||
const BG_RADIUS = 6; // squircle-ish corner on the icon plate
|
||||
|
||||
// viewBox="0 0 157 183" in src/assets/logo.svg
|
||||
const VIEW = { w: 157, h: 183 };
|
||||
const BARS = [
|
||||
{ x: 79, y: 0, w: 79, h: 47, r: 23.5, fg: [0x75, 0x4b, 0xff] },
|
||||
{ x: 0, y: 68, w: 157, h: 47, r: 23.5, fg: [0x60, 0x2f, 0xff] },
|
||||
{ x: 0, y: 136, w: 79, h: 47, r: 23.5, fg: [0x06, 0x93, 0xc0] },
|
||||
];
|
||||
|
||||
function insideRoundedRect(px, py, { x, y, w, h, r }) {
|
||||
const dx = Math.max(x + r - px, 0, px - (x + w - r));
|
||||
const dy = Math.max(y + r - py, 0, py - (y + h - r));
|
||||
return dx * dx + dy * dy <= r * r;
|
||||
}
|
||||
|
||||
// The logo occupies 62% of the plate, centred.
|
||||
const scale = (SIZE * 0.62) / VIEW.h;
|
||||
const offsetX = (SIZE - VIEW.w * scale) / 2;
|
||||
const offsetY = (SIZE - VIEW.h * scale) / 2;
|
||||
|
||||
const plate = { x: 0, y: 0, w: SIZE, h: SIZE, r: BG_RADIUS };
|
||||
|
||||
// BGRA pixels, row-major, top-to-bottom.
|
||||
const pixels = Buffer.alloc(SIZE * SIZE * 4);
|
||||
for (let py = 0; py < SIZE; py++) {
|
||||
for (let px = 0; px < SIZE; px++) {
|
||||
let plateHits = 0;
|
||||
const colorSum = [0, 0, 0];
|
||||
for (let sy = 0; sy < SS; sy++) {
|
||||
for (let sx = 0; sx < SS; sx++) {
|
||||
const fx = px + (sx + 0.5) / SS;
|
||||
const fy = py + (sy + 0.5) / SS;
|
||||
if (!insideRoundedRect(fx, fy, plate)) continue;
|
||||
plateHits++;
|
||||
const lx = (fx - offsetX) / scale;
|
||||
const ly = (fy - offsetY) / scale;
|
||||
const bar = BARS.find((b) => insideRoundedRect(lx, ly, b));
|
||||
const pixel = bar ? bar.fg : BG;
|
||||
for (let c = 0; c < 3; c++) colorSum[c] += pixel[c];
|
||||
}
|
||||
}
|
||||
const total = SS * SS;
|
||||
const alpha = Math.round((plateHits / total) * 255);
|
||||
const o = (py * SIZE + px) * 4;
|
||||
const r = plateHits ? Math.round(colorSum[0] / plateHits) : 0;
|
||||
const g = plateHits ? Math.round(colorSum[1] / plateHits) : 0;
|
||||
const b = plateHits ? Math.round(colorSum[2] / plateHits) : 0;
|
||||
pixels[o] = b;
|
||||
pixels[o + 1] = g;
|
||||
pixels[o + 2] = r;
|
||||
pixels[o + 3] = alpha;
|
||||
}
|
||||
}
|
||||
|
||||
// Pack into a single-image .ico: ICONDIR + ICONDIRENTRY + BITMAPINFOHEADER
|
||||
// + 32bpp XOR data (bottom-up) + 1bpp AND mask (bottom-up, all-zero since
|
||||
// the XOR data already carries a full alpha channel).
|
||||
const xor = Buffer.alloc(SIZE * SIZE * 4);
|
||||
for (let py = 0; py < SIZE; py++) {
|
||||
const srcRow = py * SIZE * 4;
|
||||
const dstRow = (SIZE - 1 - py) * SIZE * 4; // bottom-up
|
||||
pixels.copy(xor, dstRow, srcRow, srcRow + SIZE * 4);
|
||||
}
|
||||
const andMaskRowBytes = Math.ceil(SIZE / 32) * 4; // 1bpp, padded to 4 bytes
|
||||
const and = Buffer.alloc(andMaskRowBytes * SIZE, 0);
|
||||
|
||||
const dib = Buffer.alloc(40);
|
||||
dib.writeUInt32LE(40, 0); // biSize
|
||||
dib.writeInt32LE(SIZE, 4); // biWidth
|
||||
dib.writeInt32LE(SIZE * 2, 8); // biHeight (XOR + AND)
|
||||
dib.writeUInt16LE(1, 12); // biPlanes
|
||||
dib.writeUInt16LE(32, 14); // biBitCount
|
||||
dib.writeUInt32LE(0, 16); // biCompression: BI_RGB
|
||||
|
||||
const imageData = Buffer.concat([dib, xor, and]);
|
||||
|
||||
const iconDir = Buffer.alloc(6);
|
||||
iconDir.writeUInt16LE(0, 0); // reserved
|
||||
iconDir.writeUInt16LE(1, 2); // type: icon
|
||||
iconDir.writeUInt16LE(1, 4); // image count
|
||||
|
||||
const entry = Buffer.alloc(16);
|
||||
entry[0] = SIZE; // width
|
||||
entry[1] = SIZE; // height
|
||||
entry[2] = 0; // color count
|
||||
entry[3] = 0; // reserved
|
||||
entry.writeUInt16LE(1, 4); // planes
|
||||
entry.writeUInt16LE(32, 6); // bit count
|
||||
entry.writeUInt32LE(imageData.length, 8); // bytes in resource
|
||||
entry.writeUInt32LE(6 + 16, 12); // image offset
|
||||
|
||||
const ico = Buffer.concat([iconDir, entry, imageData]);
|
||||
|
||||
const dest = resolve(__dirname, '..', 'src', 'favicon.ico');
|
||||
writeFileSync(dest, ico);
|
||||
console.log(`Wrote ${dest} (${SIZE}x${SIZE}, ${Math.round(ico.length / 1024)} KB)`);
|
||||
Reference in New Issue
Block a user