327 lines
9.4 KiB
JavaScript
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)`);
|