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