LiveOverlayExtension/tools/gen-icons.mjs

127 lines
3.7 KiB
JavaScript

import fs from 'node:fs'
import path from 'node:path'
import { deflateSync } from 'node:zlib'
const ICON_DIR = path.resolve('icons')
const BASE_SVG_PATH = path.join(ICON_DIR, 'base.svg')
const SIZES = [128, 48, 32, 16]
const start = [0x63, 0x66, 0xf1]
const end = [0x22, 0xd3, 0xee]
const outerColor = [255, 255, 255]
const innerColor = [15, 23, 42]
function ensureBaseSvg() {
if (fs.existsSync(BASE_SVG_PATH)) return
fs.mkdirSync(ICON_DIR, { recursive: true })
const svg = `<?xml version="1.0" encoding="UTF-8"?>\n<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">\n <defs>\n <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">\n <stop offset="0%" stop-color="#6366f1" />\n <stop offset="100%" stop-color="#22d3ee" />\n </linearGradient>\n </defs>\n <rect width="128" height="128" rx="24" fill="url(#g)" />\n <circle cx="64" cy="64" r="28" fill="#ffffff" opacity="0.92" />\n <circle cx="64" cy="64" r="10" fill="#0f172a" opacity="0.9" />\n</svg>`
fs.writeFileSync(BASE_SVG_PATH, svg)
}
function gradientColor(x, y, size) {
const denom = size > 1 ? size - 1 : 1
const t = (x / denom + y / denom) / 2
return start.map((s, i) => Math.round(s + (end[i] - s) * t))
}
function blend(base, overlay, alpha) {
return base.map((channel, i) => Math.round(channel * (1 - alpha) + overlay[i] * alpha))
}
function buildCrcTable() {
const table = new Uint32Array(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 >>> 0
}
return table
}
const CRC_TABLE = buildCrcTable()
function crc32(buffer) {
let crc = 0xffffffff
for (let i = 0; i < buffer.length; i++) {
crc = CRC_TABLE[(crc ^ buffer[i]) & 0xff] ^ (crc >>> 8)
}
return (crc ^ 0xffffffff) >>> 0
}
function chunk(tag, data) {
const tagBuffer = Buffer.from(tag, 'ascii')
const lengthBuffer = Buffer.alloc(4)
lengthBuffer.writeUInt32BE(data.length)
const crcBuffer = Buffer.alloc(4)
crcBuffer.writeUInt32BE(crc32(Buffer.concat([tagBuffer, data])))
return Buffer.concat([lengthBuffer, tagBuffer, data, crcBuffer])
}
function buildPng(size) {
const width = size
const height = size
const scale = size / 128
const outerRadius = 28 * scale
const innerRadius = 10 * scale
const cx = (size - 1) / 2
const cy = (size - 1) / 2
const stride = 1 + width * 4
const raw = Buffer.alloc(stride * height)
for (let y = 0; y < height; y++) {
const rowOffset = y * stride
raw[rowOffset] = 0
for (let x = 0; x < width; x++) {
const baseColor = gradientColor(x, y, size)
let color = baseColor
const dx = x - cx
const dy = y - cy
const distance = Math.hypot(dx, dy)
if (distance <= outerRadius) {
color = blend(color, outerColor, 0.85)
}
if (distance <= innerRadius) {
color = innerColor
}
const offset = rowOffset + 1 + x * 4
raw[offset] = color[0]
raw[offset + 1] = color[1]
raw[offset + 2] = color[2]
raw[offset + 3] = 255
}
}
const compressed = deflateSync(raw)
const ihdr = Buffer.alloc(13)
ihdr.writeUInt32BE(width, 0)
ihdr.writeUInt32BE(height, 4)
ihdr[8] = 8
ihdr[9] = 6
ihdr[10] = 0
ihdr[11] = 0
ihdr[12] = 0
const png = Buffer.concat([
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
chunk('IHDR', ihdr),
chunk('IDAT', compressed),
chunk('IEND', Buffer.alloc(0)),
])
fs.mkdirSync(ICON_DIR, { recursive: true })
const filePath = path.join(ICON_DIR, `icon-${size}.png`)
fs.writeFileSync(filePath, png)
console.log('Generated', filePath)
}
function main() {
ensureBaseSvg()
for (const size of SIZES) {
buildPng(size)
}
}
main()