'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. 512 px, weil electron-builder daraus die // Linux-Symbolgrößen herunterrechnet und mindestens 256 px verlangt. const file = path.join(OUT_DIR, 'icon.png'); fs.writeFileSync(file, makePng(512, ringShader(512, orange))); console.log(`geschrieben: ${file}`); } main();