- 8 états météo : arc-en-ciel, ensoleillé, brumeux, nuageux, pluvieux, orageux, tempête, fin du monde - Fonts : Fredoka (titres) + Nunito (texte), palette V2 (bleu #3B82F6, orange #F59E0B, rouge #EF4444) - Nav flottante : Météo / Radar / Bilan / Paramètres (La Carte supprimée) - Thème clair / sombre (toggle dans Paramètres) - Icône app : papier quadrillé + 0 rouge manuscrit - Illustrations météo CSS pures (soleil, nuages, pluie, éclair, champignon nucléaire, arc-en-ciel) - Panel admin centralisé dans Paramètres (flash, classe, évaluations, météo, membres) - Bottom nav flottante avec safe-area pour ne plus toucher la nav système Android - Suppression des anciennes vues (cards.ts, map.ts, plus.ts, exam-form.ts)
129 lines
3.9 KiB
JavaScript
129 lines
3.9 KiB
JavaScript
/**
|
|
* 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)`)
|
|
}
|