Initial commit for Start9 packaging
This commit is contained in:
Executable
+226
@@ -0,0 +1,226 @@
|
||||
#!/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();
|
||||
Reference in New Issue
Block a user