Files
claude-live-dashboard/tools/make-icon.js
T
Winkler, Stefan 868f31fa9a Initial commit: Claude Live Dashboard
Always-on-Top Electron-Widget für die Claude-Plan-Auslastung unter Windows.
Liest Transkripte lokal, fragt optional den /usage-Endpunkt ab, bietet einen
Einstellungsdialog und lässt sich als NSIS-Installer oder portable Fassung
bauen.
2026-07-22 12:59:54 +02:00

126 lines
3.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* Erzeugt die Tray-Symbole als PNG — ohne Bildbibliothek, nur mit zlib aus der
* Standardbibliothek. Einmalig aufzurufen:
*
* node tools/make-icon.js
*
* Motiv: ein Ring mit einem Sektor, der den Füllstand andeutet.
*/
const fs = require('node:fs');
const path = require('node:path');
const zlib = require('node:zlib');
const OUT_DIR = path.join(__dirname, '..', 'assets');
/** CRC32 nach PNG-Spezifikation. */
const CRC_TABLE = (() => {
const table = new Int32Array(256);
for (let n = 0; n < 256; n++) {
let c = n;
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
table[n] = c;
}
return table;
})();
function crc32(buf) {
let c = -1;
for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
return (c ^ -1) >>> 0;
}
function chunk(type, data) {
const out = Buffer.alloc(data.length + 12);
out.writeUInt32BE(data.length, 0);
out.write(type, 4, 'ascii');
data.copy(out, 8);
const crcInput = Buffer.concat([Buffer.from(type, 'ascii'), data]);
out.writeUInt32BE(crc32(crcInput), data.length + 8);
return out;
}
/**
* @param {number} size Kantenlänge
* @param {(x: number, y: number) => [number, number, number, number]} shade RGBA je Pixel
*/
function makePng(size, shade) {
// Scanlines mit Filterbyte 0 je Zeile
const raw = Buffer.alloc(size * (size * 4 + 1));
let p = 0;
for (let y = 0; y < size; y++) {
raw[p++] = 0;
for (let x = 0; x < size; x++) {
const [r, g, b, a] = shade(x, y);
raw[p++] = r;
raw[p++] = g;
raw[p++] = b;
raw[p++] = a;
}
}
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(size, 0);
ihdr.writeUInt32BE(size, 4);
ihdr[8] = 8; // Bittiefe
ihdr[9] = 6; // Farbtyp RGBA
ihdr[10] = 0;
ihdr[11] = 0;
ihdr[12] = 0;
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)),
]);
}
/** Ring mit Füllsektor; kantengeglättet über 4×4-Supersampling. */
function ringShader(size, color, fillFraction = 0.68) {
const c = (size - 1) / 2;
const rOuter = size * 0.46;
const rInner = size * 0.28;
const SS = 4;
return (x, y) => {
let hits = 0;
for (let sy = 0; sy < SS; sy++) {
for (let sx = 0; sx < SS; sx++) {
const px = x + (sx + 0.5) / SS - 0.5;
const py = y + (sy + 0.5) / SS - 0.5;
const dx = px - c;
const dy = py - c;
const dist = Math.hypot(dx, dy);
if (dist > rOuter || dist < rInner) continue;
// Winkel ab 12 Uhr im Uhrzeigersinn
let ang = Math.atan2(dx, -dy);
if (ang < 0) ang += Math.PI * 2;
if (ang <= fillFraction * Math.PI * 2) hits++;
}
}
const a = Math.round((hits / (SS * SS)) * 255);
return [color[0], color[1], color[2], a];
};
}
function main() {
fs.mkdirSync(OUT_DIR, { recursive: true });
const orange = [217, 89, 38]; // Slot 2 der Palette
for (const size of [16, 32]) {
const file = path.join(OUT_DIR, `tray${size === 16 ? '' : `@${size}`}.png`);
fs.writeFileSync(file, makePng(size, ringShader(size, orange)));
console.log(`geschrieben: ${file}`);
}
// Fenster- und Anwendungssymbol
const file = path.join(OUT_DIR, 'icon.png');
fs.writeFileSync(file, makePng(256, ringShader(256, orange)));
console.log(`geschrieben: ${file}`);
}
main();