Files
proof-of-work/workout-planner/scripts/generate-icons.js
T
2026-02-28 09:27:26 -06:00

227 lines
6.8 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* PWA Icon Generator for Workout App
* Generates PNG icons without external dependencies
* Uses only Node.js built-in modules (fs, zlib, path)
*/
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
// Configuration
const BACKGROUND_COLOR = { r: 10, g: 10, b: 10 }; // #0A0A0A
const TEXT_COLOR = { r: 255, g: 255, b: 255 }; // White
const ICON_SIZES = [72, 96, 128, 144, 152, 192, 384, 512];
const MASKABLE_SIZES = [192, 512];
const OUTPUT_DIR = path.join(__dirname, '..', 'public', 'icons');
/**
* Create output directory if it doesn't exist
*/
function ensureOutputDir() {
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
}
/**
* Draw a stylized "W" character on a pixel buffer
* Uses simple geometric shapes to create the letter
*/
function drawW(buffer, width, height) {
const canvasWidth = width;
const canvasHeight = height;
// Helper to set a pixel
const setPixel = (x, y, r, g, b, a = 255) => {
if (x < 0 || x >= canvasWidth || y < 0 || y >= canvasHeight) return;
const idx = (y * canvasWidth + x) * 4;
buffer[idx] = r;
buffer[idx + 1] = g;
buffer[idx + 2] = b;
buffer[idx + 3] = a;
};
// Helper to draw a filled rectangle (fast)
const fillRect = (x, y, w, h, r, g, b) => {
const x0 = Math.max(0, Math.floor(x));
const y0 = Math.max(0, Math.floor(y));
const x1 = Math.min(canvasWidth, Math.ceil(x + w));
const y1 = Math.min(canvasHeight, Math.ceil(y + h));
for (let yi = y0; yi < y1; yi++) {
for (let xi = x0; xi < x1; xi++) {
setPixel(xi, yi, r, g, b);
}
}
};
// Fill background
fillRect(0, 0, canvasWidth, canvasHeight, BACKGROUND_COLOR.r, BACKGROUND_COLOR.g, BACKGROUND_COLOR.b);
// Calculate dimensions for the "W"
const padding = Math.floor(canvasHeight * 0.1);
const letterWidth = canvasWidth - padding * 2;
const letterHeight = canvasHeight - padding * 2;
const startX = padding;
const startY = padding;
// Draw a stylized "W" using rectangles (five vertical bars forming W shape)
const barWidth = letterWidth * 0.12;
const spacing = letterWidth / 4;
// Create a V-V-V pattern (W shape)
// First V: two diagonal bars meeting at a point
const v1LeftX = startX + spacing * 0.2;
const v1RightX = startX + spacing * 0.8;
const v1MidX = (v1LeftX + v1RightX) / 2;
const v1BottomY = startY + letterHeight;
const v1TopY = startY;
// Left bar of first V
fillRect(v1LeftX, v1TopY, barWidth, letterHeight, TEXT_COLOR.r, TEXT_COLOR.g, TEXT_COLOR.b);
// Right bar of first V
fillRect(v1RightX, v1TopY, barWidth, letterHeight, TEXT_COLOR.r, TEXT_COLOR.g, TEXT_COLOR.b);
// Second V
const v2LeftX = startX + spacing * 0.9;
const v2RightX = startX + spacing * 1.5;
fillRect(v2LeftX, v1TopY, barWidth, letterHeight, TEXT_COLOR.r, TEXT_COLOR.g, TEXT_COLOR.b);
fillRect(v2RightX, v1TopY, barWidth, letterHeight, TEXT_COLOR.r, TEXT_COLOR.g, TEXT_COLOR.b);
// Third V (right side)
const v3LeftX = startX + spacing * 1.6;
const v3RightX = startX + spacing * 2.2;
fillRect(v3LeftX, v1TopY, barWidth, letterHeight, TEXT_COLOR.r, TEXT_COLOR.g, TEXT_COLOR.b);
fillRect(v3RightX, v1TopY, barWidth, letterHeight, TEXT_COLOR.r, TEXT_COLOR.g, TEXT_COLOR.b);
}
/**
* Create a PNG file from raw pixel data
* Uses PNG file format with zlib compression
*/
function createPNG(width, height, pixelBuffer, outputPath) {
// PNG signature
const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
// IHDR chunk (image header)
const ihdrData = Buffer.alloc(13);
ihdrData.writeUInt32BE(width, 0);
ihdrData.writeUInt32BE(height, 4);
ihdrData[8] = 8; // bit depth
ihdrData[9] = 6; // color type (RGBA)
ihdrData[10] = 0; // compression method
ihdrData[11] = 0; // filter method
ihdrData[12] = 0; // interlace method
const ihdr = createChunk('IHDR', ihdrData);
// IDAT chunk (image data)
// Each row needs a filter byte (0 = None)
const rawData = Buffer.alloc(height * (1 + width * 4));
for (let y = 0; y < height; y++) {
rawData[y * (1 + width * 4)] = 0; // filter type
const rowStart = y * (1 + width * 4) + 1;
pixelBuffer.copy(rawData, rowStart, y * width * 4, (y + 1) * width * 4);
}
const compressed = zlib.deflateSync(rawData);
const idat = createChunk('IDAT', compressed);
// IEND chunk (image end)
const iend = createChunk('IEND', Buffer.alloc(0));
// Combine all chunks
const png = Buffer.concat([pngSignature, ihdr, idat, iend]);
fs.writeFileSync(outputPath, png);
}
/**
* Create a PNG chunk with proper CRC
*/
function createChunk(type, data) {
const length = Buffer.alloc(4);
length.writeUInt32BE(data.length, 0);
const typeBuffer = Buffer.from(type, 'ascii');
const chunkData = Buffer.concat([typeBuffer, data]);
// Calculate CRC32
const crc = calculateCRC32(chunkData);
const crcBuffer = Buffer.alloc(4);
crcBuffer.writeUInt32BE(crc, 0);
return Buffer.concat([length, chunkData, crcBuffer]);
}
/**
* Calculate CRC32 checksum for PNG chunks
*/
function calculateCRC32(data) {
const table = [];
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;
}
let crc = 0xffffffff;
for (let i = 0; i < data.length; i++) {
crc = table[(crc ^ data[i]) & 0xff] ^ (crc >>> 8);
}
return (crc ^ 0xffffffff) >>> 0;
}
/**
* Generate all icon sizes
*/
function generateIcons() {
ensureOutputDir();
console.log('Generating PWA icons for Workout App...\n');
ICON_SIZES.forEach((size) => {
// Regular icon
const buffer = Buffer.alloc(size * size * 4);
drawW(buffer, size, size);
const outputPath = path.join(OUTPUT_DIR, `icon-${size}x${size}.png`);
createPNG(size, size, buffer, outputPath);
console.log(`✓ Created ${path.basename(outputPath)}`);
// Maskable variant (for adaptive icons on Android)
if (MASKABLE_SIZES.includes(size)) {
const maskableBuffer = Buffer.alloc(size * size * 4);
drawW(maskableBuffer, size, size);
const maskablePath = path.join(OUTPUT_DIR, `icon-${size}x${size}-maskable.png`);
createPNG(size, size, maskableBuffer, maskablePath);
console.log(`✓ Created ${path.basename(maskablePath)}`);
}
});
// Create a favicon SVG as well
const svgPath = path.join(OUTPUT_DIR, 'favicon.svg');
const svgContent = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192">
<rect width="192" height="192" fill="#0A0A0A"/>
<text x="96" y="132" font-size="120" font-weight="bold" text-anchor="middle" fill="white" font-family="Arial, sans-serif">W</text>
</svg>`;
fs.writeFileSync(svgPath, svgContent);
console.log(`✓ Created ${path.basename(svgPath)}`);
console.log(`\nAll icons generated successfully in ${OUTPUT_DIR}`);
}
// Run the generator
generateIcons();