Files
CrossRelay/scripts/gen-icons.mjs
T
kate522 f8acf31ebc CrossRelay stage 0+1: Go gateway with Anthropic<->Chat Completions relay, Tauri shell, bilingual management UI
- Go gateway (stdlib only): /v1/messages non-stream + SSE, /v1/messages/count_tokens,
  management API /api/* (providers CRUD, apply/restore, settings, backups, logs)
- Protocol conversion: Anthropic Messages <-> Chat Completions with a streaming
  state machine keyed by content block index (text, tool_use, parallel calls)
- ~/.claude/settings.json guarded read/write: timestamped backups, managed env
  keys only, one-click restore
- Tauri 2 shell: spawns the gateway sidecar, parses the random port from stdout,
  stdin pipe watchdog prevents orphans, kills the child on exit
- React frontend: Dashboard/Providers/Settings, zh-CN + en-US, TanStack Query
- Tests: Go table-driven + httptest integration (all green), vitest + RTL
2026-10-18 17:19:13 +08:00

173 lines
5.7 KiB
JavaScript

// Generates the Tauri app icons (PNG + ICO) with a hand-drawn "relay"
// glyph: two arrows passing each other on a teal tile.
// Run: node scripts/gen-icons.mjs
import zlib from "node:zlib";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const outDir = path.join(
path.dirname(path.dirname(fileURLToPath(import.meta.url))),
"src-tauri",
"icons",
);
function crc32(buf) {
let c = ~0;
for (let i = 0; i < buf.length; i++) {
c ^= buf[i];
for (let k = 0; k < 8; k++) c = (c >>> 1) ^ (0xedb88320 & -(c & 1));
}
return ~c >>> 0;
}
function chunk(type, data) {
const len = Buffer.alloc(4);
len.writeUInt32BE(data.length);
const body = Buffer.concat([Buffer.from(type, "ascii"), data]);
const crc = Buffer.alloc(4);
crc.writeUInt32BE(crc32(body));
return Buffer.concat([len, body, crc]);
}
function png(width, height, rgba) {
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(width, 0);
ihdr.writeUInt32BE(height, 4);
ihdr[8] = 8; // bit depth
ihdr[9] = 6; // color type RGBA
const raw = Buffer.alloc((width * 4 + 1) * height);
for (let y = 0; y < height; y++) {
raw[y * (width * 4 + 1)] = 0; // filter: none
rgba.copy(raw, y * (width * 4 + 1) + 1, y * width * 4, (y + 1) * width * 4);
}
return Buffer.concat([
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
chunk("IHDR", ihdr),
chunk("IDAT", zlib.deflateSync(raw, { level: 9 })),
chunk("IEND", Buffer.alloc(0)),
]);
}
const TEAL = [13, 148, 136]; // #0d9488
const TEAL_DARK = [15, 118, 110]; // #0f766e
const WHITE = [255, 255, 255];
function drawRgba(size) {
const rgba = Buffer.alloc(size * size * 4);
const radius = size * 0.22;
const center = (size - 1) / 2;
const bar = Math.max(1, Math.round(size * 0.075));
const headLong = size * 0.17;
const headShort = size * 0.11;
const gap = size * 0.1;
const armLen = size * 0.3;
const inTile = (x, y) => {
const dx = Math.min(x, size - 1 - x);
const dy = Math.min(y, size - 1 - y);
if (dx >= radius || dy >= radius) return true;
const cx = dx - (radius - 1);
const cy = dy - (radius - 1);
return cx * cx + cy * cy <= 1;
};
const inArrow = (x, y, dir) => {
// dir = 1: arrow points right (top), -1: points left (bottom)
const cy = dir === 1 ? center - gap : center + gap;
const tipX = center + dir * (armLen + headLong);
const baseX = center - dir * armLen;
// shaft
if (Math.abs(y - cy) <= bar / 2 && x >= Math.min(baseX, tipX) && x <= Math.max(baseX, tipX)) {
return true;
}
// head: triangle growing away from the shaft
const headStart = dir === 1 ? tipX - headLong : tipX;
const headEnd = dir === 1 ? tipX : tipX + headLong;
if (x < Math.min(headStart, headEnd) || x > Math.max(headStart, headEnd)) return false;
const t = dir === 1 ? (x - headStart) / headLong : (headEnd - x) / headLong;
return Math.abs(y - cy) <= (bar / 2) * (1 - t) + headShort * t;
};
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const i = (y * size + x) * 4;
if (!inTile(x, y)) {
rgba[i + 3] = 0;
continue;
}
// subtle vertical gradient on the tile
const mix = y / size;
const base = TEAL.map((c, k) => Math.round(c * (1 - mix) + TEAL_DARK[k] * mix));
let color = base;
if (inArrow(x, y, 1) || inArrow(x, y, -1)) color = WHITE;
rgba[i] = color[0];
rgba[i + 1] = color[1];
rgba[i + 2] = color[2];
rgba[i + 3] = 255;
}
}
return rgba;
}
function drawIcon(size) {
return png(size, size, drawRgba(size));
}
// Converts an RGBA buffer (top-down) into a bottom-up BGRA DIB plus a
// transparent AND mask, the classic BMP-in-ICO layout RC.exe expects.
function dibFromRgba(size, rgba) {
const header = Buffer.alloc(40);
header.writeUInt32LE(40, 0);
header.writeInt32LE(size, 4);
header.writeInt32LE(size * 2, 8); // height includes the AND mask
header.writeUInt16LE(1, 12);
header.writeUInt16LE(32, 14);
const pixels = Buffer.alloc(size * size * 4);
for (let y = 0; y < size; y++) {
const srcRow = (size - 1 - y) * size * 4;
rgba.copy(pixels, y * size * 4, srcRow, srcRow + size * 4);
for (let x = 0; x < size; x++) {
const i = y * size * 4 + x * 4;
const r = pixels[i];
pixels[i] = pixels[i + 2];
pixels[i + 2] = r; // RGBA -> BGRA
}
}
const maskRowBytes = Math.ceil(size / 8 / 4) * 4 || 4;
const mask = Buffer.alloc(maskRowBytes * size); // all zero: fully opaque
return Buffer.concat([header, pixels, mask]);
}
function ico(entries) {
// entries: [{ size, dib }]
const header = Buffer.alloc(6);
header.writeUInt16LE(0, 0);
header.writeUInt16LE(1, 2);
header.writeUInt16LE(entries.length, 4);
let offset = 6 + entries.length * 16;
const dirEntries = entries.map(({ size, dib }) => {
const entry = Buffer.alloc(16);
entry[0] = size >= 256 ? 0 : size;
entry[1] = size >= 256 ? 0 : size;
entry[2] = 0;
entry[3] = 0;
entry.writeUInt16LE(1, 4);
entry.writeUInt16LE(32, 6);
entry.writeUInt32LE(dib.length, 8);
entry.writeUInt32LE(offset, 12);
offset += dib.length;
return entry;
});
return Buffer.concat([header, ...dirEntries, ...entries.map((e) => e.dib)]);
}
fs.mkdirSync(outDir, { recursive: true });
const sizes = [16, 32, 48, 256];
const dibs = sizes.map((size) => ({ size, dib: dibFromRgba(size, drawRgba(size)) }));
fs.writeFileSync(path.join(outDir, "32x32.png"), drawIcon(32));
fs.writeFileSync(path.join(outDir, "128x128.png"), drawIcon(128));
fs.writeFileSync(path.join(outDir, "128x128@2x.png"), drawIcon(256));
fs.writeFileSync(path.join(outDir, "icon.ico"), ico(dibs));
console.log(`icons written to ${outDir}`);