132 lines
4.6 KiB
JavaScript
132 lines
4.6 KiB
JavaScript
import { deflateSync } from "node:zlib";
|
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
const [packageRoot, uiImageRoot] = process.argv.slice(2);
|
|
if (!packageRoot || !uiImageRoot) {
|
|
throw new Error("usage: node generate-fnos-icons.mjs package-root ui-image-root");
|
|
}
|
|
|
|
const crcTable = new Uint32Array(256);
|
|
for (let index = 0; index < 256; index++) {
|
|
let value = index;
|
|
for (let bit = 0; bit < 8; bit++) {
|
|
value = (value & 1) !== 0 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
|
}
|
|
crcTable[index] = value >>> 0;
|
|
}
|
|
|
|
function crc32(buffer) {
|
|
let value = 0xffffffff;
|
|
for (const byte of buffer) {
|
|
value = crcTable[(value ^ byte) & 0xff] ^ (value >>> 8);
|
|
}
|
|
return (value ^ 0xffffffff) >>> 0;
|
|
}
|
|
|
|
function chunk(type, data) {
|
|
const typeBuffer = Buffer.from(type, "ascii");
|
|
const length = Buffer.alloc(4);
|
|
length.writeUInt32BE(data.length);
|
|
const checksum = Buffer.alloc(4);
|
|
checksum.writeUInt32BE(crc32(Buffer.concat([typeBuffer, data])));
|
|
return Buffer.concat([length, typeBuffer, data, checksum]);
|
|
}
|
|
|
|
function roundedRectangleDistance(x, y, left, top, right, bottom, radius) {
|
|
const centerX = (left + right) / 2;
|
|
const centerY = (top + bottom) / 2;
|
|
const halfWidth = (right - left) / 2 - radius;
|
|
const halfHeight = (bottom - top) / 2 - radius;
|
|
const dx = Math.max(Math.abs(x - centerX) - halfWidth, 0);
|
|
const dy = Math.max(Math.abs(y - centerY) - halfHeight, 0);
|
|
return Math.hypot(dx, dy) - radius;
|
|
}
|
|
|
|
function render(size) {
|
|
const pixels = Buffer.alloc(size * size * 4);
|
|
const samples = 3;
|
|
for (let y = 0; y < size; y++) {
|
|
for (let x = 0; x < size; x++) {
|
|
const rgba = [0, 0, 0, 0];
|
|
for (let sy = 0; sy < samples; sy++) {
|
|
for (let sx = 0; sx < samples; sx++) {
|
|
const px = x + (sx + 0.5) / samples;
|
|
const py = y + (sy + 0.5) / samples;
|
|
const scale = size / 256;
|
|
let color = [0, 0, 0, 0];
|
|
const background = roundedRectangleDistance(px, py, 12 * scale, 12 * scale, 244 * scale, 244 * scale, 50 * scale);
|
|
if (background <= 0) {
|
|
const mix = Math.min(1, Math.max(0, (px + py) / (512 * scale)));
|
|
color = [Math.round(27 + 33 * mix), Math.round(94 + 66 * mix), Math.round(180 + 49 * mix), 255];
|
|
}
|
|
|
|
const body = roundedRectangleDistance(px, py, 51 * scale, 75 * scale, 190 * scale, 185 * scale, 22 * scale);
|
|
if (body <= 0) {
|
|
color = [245, 249, 255, 255];
|
|
}
|
|
|
|
const lensDistance = Math.hypot(px - 120 * scale, py - 130 * scale);
|
|
if (lensDistance <= 36 * scale) {
|
|
color = lensDistance <= 23 * scale ? [42, 109, 196, 255] : [128, 185, 239, 255];
|
|
}
|
|
|
|
const viewfinder = roundedRectangleDistance(px, py, 73 * scale, 56 * scale, 122 * scale, 86 * scale, 9 * scale);
|
|
if (viewfinder <= 0) {
|
|
color = [232, 241, 253, 255];
|
|
}
|
|
|
|
if (px >= 190 * scale && px <= 222 * scale && py >= 101 * scale && py <= 159 * scale) {
|
|
const edge = Math.abs(py - 130 * scale) / (29 * scale);
|
|
const leftEdge = 190 * scale + edge * 16 * scale;
|
|
if (px >= leftEdge) {
|
|
color = [237, 244, 253, 255];
|
|
}
|
|
}
|
|
|
|
const statusDistance = Math.hypot(px - 165 * scale, py - 98 * scale);
|
|
if (statusDistance <= 9 * scale) {
|
|
color = [244, 85, 93, 255];
|
|
}
|
|
|
|
for (let channel = 0; channel < 4; channel++) {
|
|
rgba[channel] += color[channel];
|
|
}
|
|
}
|
|
}
|
|
const offset = (y * size + x) * 4;
|
|
for (let channel = 0; channel < 4; channel++) {
|
|
pixels[offset + channel] = Math.round(rgba[channel] / (samples * samples));
|
|
}
|
|
}
|
|
}
|
|
|
|
const scanlines = Buffer.alloc((size * 4 + 1) * size);
|
|
for (let row = 0; row < size; row++) {
|
|
const target = row * (size * 4 + 1);
|
|
scanlines[target] = 0;
|
|
pixels.copy(scanlines, target + 1, row * size * 4, (row + 1) * size * 4);
|
|
}
|
|
|
|
const header = Buffer.alloc(13);
|
|
header.writeUInt32BE(size, 0);
|
|
header.writeUInt32BE(size, 4);
|
|
header[8] = 8;
|
|
header[9] = 6;
|
|
return Buffer.concat([
|
|
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
|
|
chunk("IHDR", header),
|
|
chunk("IDAT", deflateSync(scanlines, { level: 9 })),
|
|
chunk("IEND", Buffer.alloc(0))
|
|
]);
|
|
}
|
|
|
|
mkdirSync(packageRoot, { recursive: true });
|
|
mkdirSync(uiImageRoot, { recursive: true });
|
|
const icon64 = render(64);
|
|
const icon256 = render(256);
|
|
writeFileSync(join(packageRoot, "ICON.PNG"), icon64);
|
|
writeFileSync(join(packageRoot, "ICON_256.PNG"), icon256);
|
|
writeFileSync(join(uiImageRoot, "icon_64.png"), icon64);
|
|
writeFileSync(join(uiImageRoot, "icon_256.png"), icon256);
|