/** * Génère l'icône Météo Exam V2 : papier quadrillé + 0 rouge manuscrit. */ import { writeFileSync, mkdirSync } from 'node:fs' import { resolve } from 'node:path' import { deflateSync } from 'node:zlib' const OUT = resolve(import.meta.dirname, '../web/public') mkdirSync(OUT, { recursive: true }) function crc32(buf) { 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 (const b of buf) crc = table[(crc ^ b) & 0xff] ^ (crc >>> 8) return (crc ^ 0xffffffff) >>> 0 } function chunk(type, data) { const len = Buffer.alloc(4) len.writeUInt32BE(data.length) const body = Buffer.concat([Buffer.from(type, 'ascii'), data]) const crc = Buffer.alloc(4) crc.writeUInt32BE(crc32(body)) return Buffer.concat([len, body, crc]) } function encodePng(width, height, rgba) { const raw = Buffer.alloc(height * (1 + width * 4)) for (let y = 0; y < height; y++) { raw[y * (1 + width * 4)] = 0 rgba.copy(raw, y * (1 + width * 4) + 1, y * width * 4, (y + 1) * width * 4) } const ihdr = Buffer.alloc(13) ihdr.writeUInt32BE(width, 0) ihdr.writeUInt32BE(height, 4) ihdr[8] = 8 ihdr[9] = 6 return Buffer.concat([ Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), chunk('IHDR', ihdr), chunk('IDAT', deflateSync(raw)), chunk('IEND', Buffer.alloc(0)), ]) } function drawIcon(size) { const buf = Buffer.alloc(size * size * 4) const paperR = 248, paperG = 250, paperB = 252 // #F8FAFC const gridR = 185, gridG = 213, gridB = 246 // #B9D5F6 const redR = 239, redG = 68, redB = 68 // #EF4444 const lineR = 236, lineG = 73, lineB = 73 // #EC4949 const cx = size / 2 const cy = size / 2 const gridSize = size / 10 for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { const i = (y * size + x) * 4 // Fond papier let r = paperR, g = paperG, b = paperB, a = 255 // Quadrillage const gx = x % gridSize const gy = y % gridSize const nearVx = gx < 1.5 || gx > gridSize - 1.5 const nearHy = gy < 1.5 || gy > gridSize - 1.5 if (nearVx || nearHy) { r = gridR; g = gridG; b = gridB } // Marge rouge verticale (style cahier) if (x < size * 0.08 && x > size * 0.05) { r = lineR; g = lineG; b = lineB } // Dessiner le "0" rouge (ellipse inclinée) const dx = (x - cx) / (size * 0.22) const dy = (y - cy) / (size * 0.3) const dist0 = Math.sqrt(dx * dx + dy * dy) // Anneau du 0 if (dist0 > 0.7 && dist0 < 1.15) { r = redR; g = redG; b = redB } // Rayons du soleil (petits traits autour du 0) const angle = Math.atan2(y - cy, x - cx) const rayDist = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2) const rayOuter = size * 0.42 const rayInner = size * 0.36 if (rayDist > rayInner && rayDist < rayOuter) { // 8 rayons const rayAngle = (Math.round(angle / (Math.PI / 4)) * Math.PI) / 4 const perpDist = Math.abs((x - cx) * Math.sin(rayAngle) - (y - cy) * Math.cos(rayAngle)) if (perpDist < size * 0.02) { r = redR; g = redG; b = redB } } // Soulignement double en bas const underY1 = cy + size * 0.38 const underY2 = cy + size * 0.42 if (Math.abs(y - underY1) < size * 0.015 && x > cx - size * 0.3 && x < cx + size * 0.3) { r = redR; g = redG; b = redB } if (Math.abs(y - underY2) < size * 0.015 && x > cx - size * 0.28 && x < cx + size * 0.32) { r = redR; g = redG; b = redB } buf[i] = r buf[i + 1] = g buf[i + 2] = b buf[i + 3] = a } } return buf } for (const size of [192, 512]) { const png = encodePng(size, size, drawIcon(size)) writeFileSync(resolve(OUT, `icon-${size}.png`), png) console.log(`icon-${size}.png (${png.length} octets)`) }