- DB : table day_weather (override admin par jour), flash_message sur classes - API : PUT/DELETE /api/config/day-weather, PUT /api/config/flash, PATCH /api/config/class - SSE : day_weather.updated/deleted, flash.updated, class.updated - Front : bandeau flash info, indicateur 'forcé' sur la frise, modale météo admin - Onglet Plus : panneau admin (flash info + édition classe), tout admin modifiable - PWA : manifest.webmanifest, service worker, meta Apple, icônes 192/512 - Web version accessible sur iOS via Safari (Ajouter à l'écran d'accueil)
108 lines
3.2 KiB
JavaScript
108 lines
3.2 KiB
JavaScript
/**
|
|
* Génère des icônes PWA simples (dégradé bleu + pastille blanche).
|
|
* Usage : node scripts/gen-icons.mjs
|
|
*/
|
|
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 c0 = [56, 189, 248]
|
|
const c1 = [99, 102, 241]
|
|
const cx = size / 2
|
|
const cy = size / 2
|
|
const rInner = size * 0.3
|
|
const rPad = size * 0.16
|
|
|
|
for (let y = 0; y < size; y++) {
|
|
for (let x = 0; x < size; x++) {
|
|
const i = (y * size + x) * 4
|
|
const dx = x - cx
|
|
const dy = y - cy
|
|
const dist = Math.sqrt(dx * dx + dy * dy)
|
|
|
|
// Fond arrondi : on coupe les coins
|
|
let transparent = false
|
|
if (x < rPad && y < rPad) transparent = Math.hypot(rPad - x, rPad - y) > rPad
|
|
else if (x >= size - rPad && y < rPad) transparent = Math.hypot(x - (size - rPad), rPad - y) > rPad
|
|
else if (x < rPad && y >= size - rPad) transparent = Math.hypot(rPad - x, y - (size - rPad)) > rPad
|
|
else if (x >= size - rPad && y >= size - rPad) transparent = Math.hypot(x - (size - rPad), y - (size - rPad)) > rPad
|
|
|
|
if (transparent) {
|
|
buf[i + 3] = 0
|
|
continue
|
|
}
|
|
|
|
const t = (x + y) / (size * 2)
|
|
let r = c0[0] + (c1[0] - c0[0]) * t
|
|
let g = c0[1] + (c1[1] - c0[1]) * t
|
|
let b = c0[2] + (c1[2] - c0[2]) * t
|
|
|
|
if (dist < rInner) {
|
|
r = 255
|
|
g = 255
|
|
b = 255
|
|
} else if (dist < rInner + size * 0.025) {
|
|
r = Math.min(255, r + 50)
|
|
g = Math.min(255, g + 50)
|
|
b = Math.min(255, b + 50)
|
|
}
|
|
|
|
buf[i] = Math.round(r)
|
|
buf[i + 1] = Math.round(g)
|
|
buf[i + 2] = Math.round(b)
|
|
buf[i + 3] = 255
|
|
}
|
|
}
|
|
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)`)
|
|
}
|