#!/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 = ` W `; fs.writeFileSync(svgPath, svgContent); console.log(`✓ Created ${path.basename(svgPath)}`); console.log(`\nAll icons generated successfully in ${OUTPUT_DIR}`); } // Run the generator generateIcons();