Files
dickendock/frontend/generate-icons.mjs

107 lines
3.6 KiB
JavaScript

// Generates icon-192.png and icon-512.png without external dependencies
// Uses a minimal PNG encoder written in pure JS
import { writeFileSync } from 'fs';
import { deflateSync } from 'zlib';
function createPNG(size) {
const r = Math.round(size * 0.22);
// Draw pixels
const pixels = new Uint8Array(size * size * 4);
const setPixel = (x, y, R, G, B, A = 255) => {
if (x < 0 || x >= size || y < 0 || y >= size) return;
const i = (y * size + x) * 4;
pixels[i] = R; pixels[i+1] = G; pixels[i+2] = B; pixels[i+3] = A;
};
// Gradient background with rounded corners
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
// Rounded corner check
const inCornerTL = x < r && y < r && (x-r)**2 + (y-r)**2 > r**2;
const inCornerTR = x >= size-r && y < r && (x-(size-r))**2 + (y-r)**2 > r**2;
const inCornerBL = x < r && y >= size-r && (x-r)**2 + (y-(size-r))**2 > r**2;
const inCornerBR = x >= size-r && y >= size-r && (x-(size-r))**2 + (y-(size-r))**2 > r**2;
if (inCornerTL || inCornerTR || inCornerBL || inCornerBR) continue;
// Linear gradient: top-left #4ecdc4 → bottom-right #ff6b9d
const t = (x + y) / (size * 2);
const R = Math.round(0x4e + (0xff - 0x4e) * t);
const G = Math.round(0xcd + (0x6b - 0xcd) * t);
const B = Math.round(0xc4 + (0x9d - 0xc4) * t);
setPixel(x, y, R, G, B);
}
}
// Draw ⚒ as a simple hammer shape (dark color on gradient)
const cx = Math.floor(size / 2);
const cy = Math.floor(size / 2);
const s = Math.floor(size * 0.18);
// Hammer head (rectangle)
for (let dy = -s; dy <= 0; dy++) {
for (let dx = -s; dx <= s; dx++) {
setPixel(cx + dx, cy + dy, 13, 13, 15);
}
}
// Handle
for (let dy = 0; dy <= s * 2; dy++) {
for (let dx = -Math.floor(s*0.25); dx <= Math.floor(s*0.25); dx++) {
setPixel(cx + dx, cy + dy, 13, 13, 15);
}
}
// Encode as PNG
const width = size, height = size;
const signature = Buffer.from([137,80,78,71,13,10,26,10]);
// IHDR
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(width, 0); ihdr.writeUInt32BE(height, 4);
ihdr[8] = 8; ihdr[9] = 2; // 8-bit RGB... wait we need RGBA
// Use RGBA (color type 6)
ihdr[9] = 6;
const ihdrChunk = makeChunk('IHDR', ihdr);
// IDAT - raw image data with filter bytes
const raw = Buffer.alloc(height * (1 + width * 4));
for (let y = 0; y < height; y++) {
raw[y * (1 + width * 4)] = 0; // filter type None
for (let x = 0; x < width; x++) {
const si = (y * width + x) * 4;
const di = y * (1 + width * 4) + 1 + x * 4;
raw[di] = pixels[si];
raw[di+1] = pixels[si+1];
raw[di+2] = pixels[si+2];
raw[di+3] = pixels[si+3] || (pixels[si] || pixels[si+1] || pixels[si+2] ? 255 : 0);
}
}
const compressed = deflateSync(raw);
const idatChunk = makeChunk('IDAT', compressed);
const iendChunk = makeChunk('IEND', Buffer.alloc(0));
return Buffer.concat([signature, ihdrChunk, idatChunk, iendChunk]);
}
function makeChunk(type, data) {
const len = Buffer.alloc(4); len.writeUInt32BE(data.length);
const typeB = Buffer.from(type);
const crc = crc32(Buffer.concat([typeB, data]));
const crcB = Buffer.alloc(4); crcB.writeUInt32BE(crc >>> 0);
return Buffer.concat([len, typeB, data, crcB]);
}
function crc32(buf) {
let crc = 0xFFFFFFFF;
for (const b of buf) {
crc ^= b;
for (let i = 0; i < 8; i++) crc = (crc >>> 1) ^ (crc & 1 ? 0xEDB88320 : 0);
}
return (crc ^ 0xFFFFFFFF) >>> 0;
}
writeFileSync('public/icon-192.png', createPNG(192));
writeFileSync('public/icon-512.png', createPNG(512));
console.log('Icons generated.');