Files
GitLite/scripts/generate-icon.mjs
T

327 lines
9.4 KiB
JavaScript

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)`);