Files
GitLite/scripts/generate-icon.mjs
Christoph Brandau a57e52a38b feat(icons): generate application icons in multiple sizes
This update introduces a new script to generate application icons in various sizes, including 32x32, 64x64, 128x128, and 512x512 pixels. The generated icons are now included in the Tauri configuration, ensuring that the application has the appropriate icons for different contexts.

- Added a script for generating icons from a source image
- Updated Tauri configuration to reference new icon sizes
- Included multiple icon formats for better compatibility
2026-07-08 13:09:14 +02:00

331 lines
9.4 KiB
JavaScript

import "./generate-app-icons.mjs";
process.exit(0);
import { Buffer } from "node:buffer";
import { mkdirSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { deflateSync } from "node:zlib";
const root = dirname(dirname(fileURLToPath(import.meta.url)));
const iconsDir = join(root, "src-tauri", "icons");
const icoSizes = [16, 24, 32, 48, 64, 128, 256];
function clamp(value, min = 0, max = 1) {
return Math.min(max, Math.max(min, value));
}
function smoothstep(edge0, edge1, value) {
const t = clamp((value - edge0) / (edge1 - edge0));
return t * t * (3 - 2 * t);
}
function hex(value) {
const clean = value.replace("#", "");
return {
r: Number.parseInt(clean.slice(0, 2), 16),
g: Number.parseInt(clean.slice(2, 4), 16),
b: Number.parseInt(clean.slice(4, 6), 16),
};
}
function mix(a, b, t) {
const u = clamp(t);
return {
r: a.r + (b.r - a.r) * u,
g: a.g + (b.g - a.g) * u,
b: a.b + (b.b - a.b) * u,
};
}
function addGlow(color, glow, amount) {
return {
r: clamp(color.r + glow.r * amount, 0, 255),
g: clamp(color.g + glow.g * amount, 0, 255),
b: clamp(color.b + glow.b * amount, 0, 255),
};
}
function blend(base, layer) {
const alpha = clamp(layer.a);
const outA = alpha + base.a * (1 - alpha);
if (outA <= 0) return { r: 0, g: 0, b: 0, a: 0 };
return {
r: (layer.r * alpha + base.r * base.a * (1 - alpha)) / outA,
g: (layer.g * alpha + base.g * base.a * (1 - alpha)) / outA,
b: (layer.b * alpha + base.b * base.a * (1 - alpha)) / outA,
a: outA,
};
}
function roundedRectDistance(x, y, cx, cy, hx, hy, radius) {
const qx = Math.abs(x - cx) - hx + radius;
const qy = Math.abs(y - cy) - hy + radius;
const outsideX = Math.max(qx, 0);
const outsideY = Math.max(qy, 0);
return Math.hypot(outsideX, outsideY) + Math.min(Math.max(qx, qy), 0) - radius;
}
function segmentDistance(px, py, ax, ay, bx, by) {
const vx = bx - ax;
const vy = by - ay;
const wx = px - ax;
const wy = py - ay;
const lenSq = vx * vx + vy * vy;
const t = lenSq === 0 ? 0 : clamp((wx * vx + wy * vy) / lenSq);
return Math.hypot(px - (ax + vx * t), py - (ay + vy * t));
}
function polygonContains(x, y, points) {
let inside = false;
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
const xi = points[i][0];
const yi = points[i][1];
const xj = points[j][0];
const yj = points[j][1];
const intersects = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
if (intersects) inside = !inside;
}
return inside;
}
function polygonDistance(x, y, points) {
let distance = Infinity;
for (let i = 0; i < points.length; i += 1) {
const a = points[i];
const b = points[(i + 1) % points.length];
distance = Math.min(distance, segmentDistance(x, y, a[0], a[1], b[0], b[1]));
}
return polygonContains(x, y, points) ? -distance : distance;
}
function drawStroke(color, x, y, ax, ay, bx, by, radius, strokeColor, alpha = 1) {
const d = segmentDistance(x, y, ax, ay, bx, by);
const a = smoothstep(radius + 1.4, radius - 0.8, d) * alpha;
if (a <= 0) return color;
return blend(color, { ...strokeColor, a });
}
function drawCircle(color, x, y, cx, cy, radius, circleColor, alpha = 1) {
const d = Math.hypot(x - cx, y - cy);
const a = smoothstep(radius + 1.4, radius - 0.8, d) * alpha;
if (a <= 0) return color;
return blend(color, { ...circleColor, a });
}
function sampleScene(x, y) {
const dark = hex("#10111f");
const purple = hex("#bd34fe");
const indigo = hex("#646cff");
const cyan = hex("#41d1ff");
const yellow = hex("#ffd343");
const white = hex("#f7fbff");
let color = { r: 0, g: 0, b: 0, a: 0 };
const badge = roundedRectDistance(x, y, 128, 128, 108, 108, 48);
const shadowA = Math.exp(-Math.max(badge, 0) / 12) * 0.22;
if (badge > 0 && shadowA > 0.005) {
color = blend(color, { r: 0, g: 0, b: 0, a: shadowA });
}
const badgeA = smoothstep(1.6, -1.2, badge);
if (badgeA > 0) {
const diagonal = (x + y) / 512;
let base = mix(dark, indigo, diagonal * 0.75);
const purpleGlow = Math.exp(-((x - 68) ** 2 + (y - 62) ** 2) / 6200);
const cyanGlow = Math.exp(-((x - 184) ** 2 + (y - 72) ** 2) / 5600);
const yellowGlow = Math.exp(-((x - 184) ** 2 + (y - 202) ** 2) / 8400);
base = addGlow(base, purple, purpleGlow * 0.58);
base = addGlow(base, cyan, cyanGlow * 0.55);
base = addGlow(base, yellow, yellowGlow * 0.22);
color = blend(color, { ...base, a: badgeA });
const rim = smoothstep(4.2, 0.4, Math.abs(badge));
color = blend(color, { ...white, a: rim * 0.18 * badgeA });
}
const bolt = [
[144, 40],
[198, 40],
[164, 111],
[207, 111],
[113, 218],
[143, 142],
[103, 142],
];
const boltD = polygonDistance(x, y, bolt);
const boltA = smoothstep(2.5, -1, boltD);
if (boltA > 0) {
color = blend(color, { ...yellow, a: boltA * 0.24 });
}
const glow = hex("#41d1ff");
const line = hex("#eef7ff");
const nodeFill = hex("#131827");
for (const segment of [
[82, 73, 82, 181],
[82, 116, 166, 116],
]) {
color = drawStroke(color, x, y, ...segment, 12, glow, 0.24);
color = drawStroke(color, x, y, ...segment, 5.2, line, 0.94);
}
for (const [cx, cy, accent] of [
[82, 73, cyan],
[166, 116, purple],
[82, 181, yellow],
]) {
color = drawCircle(color, x, y, cx, cy, 22, accent, 0.25);
color = drawCircle(color, x, y, cx, cy, 14.5, white, 0.96);
color = drawCircle(color, x, y, cx, cy, 8.4, nodeFill, 1);
color = drawCircle(color, x, y, cx, cy, 4.2, accent, 0.95);
}
const spark = [
[186, 161],
[195, 181],
[215, 190],
[195, 199],
[186, 219],
[177, 199],
[157, 190],
[177, 181],
];
const sparkD = polygonDistance(x, y, spark);
const sparkA = smoothstep(1.8, -0.8, sparkD);
if (sparkA > 0) {
color = blend(color, { ...yellow, a: sparkA * 0.92 });
}
return color;
}
function renderPng(size) {
const scale = 256 / size;
const samples = size <= 32 ? 5 : 3;
const data = Buffer.alloc(size * size * 4);
for (let y = 0; y < size; y += 1) {
for (let x = 0; x < size; x += 1) {
let r = 0;
let g = 0;
let b = 0;
let a = 0;
for (let sy = 0; sy < samples; sy += 1) {
for (let sx = 0; sx < samples; sx += 1) {
const scene = sampleScene((x + (sx + 0.5) / samples) * scale, (y + (sy + 0.5) / samples) * scale);
r += scene.r * scene.a;
g += scene.g * scene.a;
b += scene.b * scene.a;
a += scene.a;
}
}
const total = samples * samples;
const alpha = a / total;
const offset = (y * size + x) * 4;
data[offset] = alpha > 0 ? Math.round(r / a) : 0;
data[offset + 1] = alpha > 0 ? Math.round(g / a) : 0;
data[offset + 2] = alpha > 0 ? Math.round(b / a) : 0;
data[offset + 3] = Math.round(alpha * 255);
}
}
return encodePng(size, size, data);
}
const crcTable = new Uint32Array(256);
for (let n = 0; n < 256; n += 1) {
let c = n;
for (let k = 0; k < 8; k += 1) {
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
}
crcTable[n] = c >>> 0;
}
function crc32(buffer) {
let crc = 0xffffffff;
for (const byte of buffer) {
crc = crcTable[(crc ^ byte) & 0xff] ^ (crc >>> 8);
}
return (crc ^ 0xffffffff) >>> 0;
}
function chunk(type, payload) {
const typeBytes = Buffer.from(type);
const out = Buffer.alloc(12 + payload.length);
out.writeUInt32BE(payload.length, 0);
typeBytes.copy(out, 4);
payload.copy(out, 8);
out.writeUInt32BE(crc32(Buffer.concat([typeBytes, payload])), out.length - 4);
return out;
}
function encodePng(width, height, rgba) {
const header = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(width, 0);
ihdr.writeUInt32BE(height, 4);
ihdr[8] = 8;
ihdr[9] = 6;
const raw = Buffer.alloc((width * 4 + 1) * height);
for (let y = 0; y < height; y += 1) {
const rowStart = y * (width * 4 + 1);
raw[rowStart] = 0;
rgba.copy(raw, rowStart + 1, y * width * 4, (y + 1) * width * 4);
}
return Buffer.concat([
header,
chunk("IHDR", ihdr),
chunk("IDAT", deflateSync(raw, { level: 9 })),
chunk("IEND", Buffer.alloc(0)),
]);
}
function makeIco(images) {
const header = Buffer.alloc(6);
header.writeUInt16LE(0, 0);
header.writeUInt16LE(1, 2);
header.writeUInt16LE(images.length, 4);
const entries = Buffer.alloc(images.length * 16);
let offset = 6 + images.length * 16;
images.forEach((image, index) => {
const entry = index * 16;
entries[entry] = image.size >= 256 ? 0 : image.size;
entries[entry + 1] = image.size >= 256 ? 0 : image.size;
entries[entry + 2] = 0;
entries[entry + 3] = 0;
entries.writeUInt16LE(1, entry + 4);
entries.writeUInt16LE(32, entry + 6);
entries.writeUInt32LE(image.png.length, entry + 8);
entries.writeUInt32LE(offset, entry + 12);
offset += image.png.length;
});
return Buffer.concat([header, entries, ...images.map((image) => image.png)]);
}
mkdirSync(iconsDir, { recursive: true });
const iconPng = renderPng(512);
const icoImages = icoSizes.map((size) => ({ size, png: renderPng(size) }));
writeFileSync(join(iconsDir, "icon.png"), iconPng);
writeFileSync(join(iconsDir, "icon.ico"), makeIco(icoImages));
console.log(`Wrote ${join("src-tauri", "icons", "icon.png")} (${iconPng.length} bytes)`);
console.log(`Wrote ${join("src-tauri", "icons", "icon.ico")} (${icoImages.length} sizes)`);