diff --git a/Makefile b/Makefile index debe7c4..f9418c9 100644 --- a/Makefile +++ b/Makefile @@ -71,6 +71,9 @@ sfondi-confronto: ## Rifà il confronto foto/immagini in assets/generated/index. $(PYTHON) tools/genera.py --gallery-only @xdg-open assets/generated/index.html >/dev/null 2>&1 || open assets/generated/index.html +miniature: ## Le miniature della mappa (assets/scenes/mini/) dagli sfondi + $(PYTHON) tools/miniature.py + background: ## Porta uno sfondo a 640x360 nel gioco (SRC=..., DST=..., CROP=x,y,w,h, COLORS=64) @test -n "$(SRC)" || { echo "uso: make background SRC=assets/generated/nome.png [CROP=x,y,w,h]"; exit 2; } $(PYTHON) tools/prepare_background.py $(SRC) $(DST) $(if $(CROP),--crop $(CROP)) --colors $(COLORS) diff --git a/assets/scenes/campomigliaio-circolo.png b/assets/scenes/campomigliaio-circolo.png new file mode 100644 index 0000000..f43cdaa Binary files /dev/null and b/assets/scenes/campomigliaio-circolo.png differ diff --git a/assets/scenes/campomigliaio-ponte.png b/assets/scenes/campomigliaio-ponte.png new file mode 100644 index 0000000..a090078 Binary files /dev/null and b/assets/scenes/campomigliaio-ponte.png differ diff --git a/assets/scenes/campomigliaio-strada.png b/assets/scenes/campomigliaio-strada.png new file mode 100644 index 0000000..878dfab Binary files /dev/null and b/assets/scenes/campomigliaio-strada.png differ diff --git a/assets/scenes/mini/area-camper.png b/assets/scenes/mini/area-camper.png new file mode 100644 index 0000000..85876fd Binary files /dev/null and b/assets/scenes/mini/area-camper.png differ diff --git a/assets/scenes/mini/area-feste-festa.png b/assets/scenes/mini/area-feste-festa.png new file mode 100644 index 0000000..70f48eb Binary files /dev/null and b/assets/scenes/mini/area-feste-festa.png differ diff --git a/assets/scenes/mini/area-feste.png b/assets/scenes/mini/area-feste.png new file mode 100644 index 0000000..45c57fa Binary files /dev/null and b/assets/scenes/mini/area-feste.png differ diff --git a/assets/scenes/mini/bivio-provinciale.png b/assets/scenes/mini/bivio-provinciale.png new file mode 100644 index 0000000..52151b1 Binary files /dev/null and b/assets/scenes/mini/bivio-provinciale.png differ diff --git a/assets/scenes/mini/bosco-ai-frati.png b/assets/scenes/mini/bosco-ai-frati.png new file mode 100644 index 0000000..7771769 Binary files /dev/null and b/assets/scenes/mini/bosco-ai-frati.png differ diff --git a/assets/scenes/mini/campeggio.png b/assets/scenes/mini/campeggio.png new file mode 100644 index 0000000..0a8c442 Binary files /dev/null and b/assets/scenes/mini/campeggio.png differ diff --git a/assets/scenes/mini/campo-giannini.png b/assets/scenes/mini/campo-giannini.png new file mode 100644 index 0000000..5d1eb28 Binary files /dev/null and b/assets/scenes/mini/campo-giannini.png differ diff --git a/assets/scenes/mini/campomigliaio-circolo.png b/assets/scenes/mini/campomigliaio-circolo.png new file mode 100644 index 0000000..3dec447 Binary files /dev/null and b/assets/scenes/mini/campomigliaio-circolo.png differ diff --git a/assets/scenes/mini/campomigliaio-ponte.png b/assets/scenes/mini/campomigliaio-ponte.png new file mode 100644 index 0000000..ee9b44e Binary files /dev/null and b/assets/scenes/mini/campomigliaio-ponte.png differ diff --git a/assets/scenes/mini/campomigliaio-strada.png b/assets/scenes/mini/campomigliaio-strada.png new file mode 100644 index 0000000..b976f05 Binary files /dev/null and b/assets/scenes/mini/campomigliaio-strada.png differ diff --git a/assets/scenes/mini/cardetole.png b/assets/scenes/mini/cardetole.png new file mode 100644 index 0000000..e302495 Binary files /dev/null and b/assets/scenes/mini/cardetole.png differ diff --git a/assets/scenes/mini/carza.png b/assets/scenes/mini/carza.png new file mode 100644 index 0000000..7106abb Binary files /dev/null and b/assets/scenes/mini/carza.png differ diff --git a/assets/scenes/mini/casa-maestro-bacci.png b/assets/scenes/mini/casa-maestro-bacci.png new file mode 100644 index 0000000..737591d Binary files /dev/null and b/assets/scenes/mini/casa-maestro-bacci.png differ diff --git a/assets/scenes/mini/comune-asilo.png b/assets/scenes/mini/comune-asilo.png new file mode 100644 index 0000000..77d88a9 Binary files /dev/null and b/assets/scenes/mini/comune-asilo.png differ diff --git a/assets/scenes/mini/forno-conti.png b/assets/scenes/mini/forno-conti.png new file mode 100644 index 0000000..d96b1d7 Binary files /dev/null and b/assets/scenes/mini/forno-conti.png differ diff --git a/assets/scenes/mini/fortezza-esterno.png b/assets/scenes/mini/fortezza-esterno.png new file mode 100644 index 0000000..13d1634 Binary files /dev/null and b/assets/scenes/mini/fortezza-esterno.png differ diff --git a/assets/scenes/mini/giardini-poste.png b/assets/scenes/mini/giardini-poste.png new file mode 100644 index 0000000..76ebdbc Binary files /dev/null and b/assets/scenes/mini/giardini-poste.png differ diff --git a/assets/scenes/mini/oratorio.png b/assets/scenes/mini/oratorio.png new file mode 100644 index 0000000..e486ea9 Binary files /dev/null and b/assets/scenes/mini/oratorio.png differ diff --git a/assets/scenes/mini/panorama-intro.png b/assets/scenes/mini/panorama-intro.png new file mode 100644 index 0000000..67e7ab8 Binary files /dev/null and b/assets/scenes/mini/panorama-intro.png differ diff --git a/assets/scenes/mini/piazza-colonna.png b/assets/scenes/mini/piazza-colonna.png new file mode 100644 index 0000000..0923308 Binary files /dev/null and b/assets/scenes/mini/piazza-colonna.png differ diff --git a/assets/scenes/mini/pieve-dalla-salita.png b/assets/scenes/mini/pieve-dalla-salita.png new file mode 100644 index 0000000..6be6908 Binary files /dev/null and b/assets/scenes/mini/pieve-dalla-salita.png differ diff --git a/assets/scenes/mini/pieve-interno.png b/assets/scenes/mini/pieve-interno.png new file mode 100644 index 0000000..fa748ea Binary files /dev/null and b/assets/scenes/mini/pieve-interno.png differ diff --git a/assets/scenes/mini/polisportiva-principale.png b/assets/scenes/mini/polisportiva-principale.png new file mode 100644 index 0000000..96b29e0 Binary files /dev/null and b/assets/scenes/mini/polisportiva-principale.png differ diff --git a/assets/scenes/mini/polisportiva-secondario.png b/assets/scenes/mini/polisportiva-secondario.png new file mode 100644 index 0000000..86ff2b3 Binary files /dev/null and b/assets/scenes/mini/polisportiva-secondario.png differ diff --git a/assets/scenes/mini/ponte-sieve.png b/assets/scenes/mini/ponte-sieve.png new file mode 100644 index 0000000..e33a7f2 Binary files /dev/null and b/assets/scenes/mini/ponte-sieve.png differ diff --git a/assets/scenes/mini/ponticino-carza.png b/assets/scenes/mini/ponticino-carza.png new file mode 100644 index 0000000..babaa65 Binary files /dev/null and b/assets/scenes/mini/ponticino-carza.png differ diff --git a/assets/scenes/mini/salita-villa-adami.png b/assets/scenes/mini/salita-villa-adami.png new file mode 100644 index 0000000..9c36895 Binary files /dev/null and b/assets/scenes/mini/salita-villa-adami.png differ diff --git a/assets/scenes/mini/scuola.png b/assets/scenes/mini/scuola.png new file mode 100644 index 0000000..c3f49f0 Binary files /dev/null and b/assets/scenes/mini/scuola.png differ diff --git a/assets/scenes/mini/stazione.png b/assets/scenes/mini/stazione.png new file mode 100644 index 0000000..9081534 Binary files /dev/null and b/assets/scenes/mini/stazione.png differ diff --git a/assets/scenes/mini/tennis-chiesa.png b/assets/scenes/mini/tennis-chiesa.png new file mode 100644 index 0000000..f79fdec Binary files /dev/null and b/assets/scenes/mini/tennis-chiesa.png differ diff --git a/assets/scenes/mini/titolo.png b/assets/scenes/mini/titolo.png new file mode 100644 index 0000000..fdf85d0 Binary files /dev/null and b/assets/scenes/mini/titolo.png differ diff --git a/assets/scenes/mini/trebbio.png b/assets/scenes/mini/trebbio.png new file mode 100644 index 0000000..b68d725 Binary files /dev/null and b/assets/scenes/mini/trebbio.png differ diff --git a/assets/scenes/mini/via-calimara.png b/assets/scenes/mini/via-calimara.png new file mode 100644 index 0000000..220b04c Binary files /dev/null and b/assets/scenes/mini/via-calimara.png differ diff --git a/assets/scenes/mini/via-de-medici.png b/assets/scenes/mini/via-de-medici.png new file mode 100644 index 0000000..56ef10a Binary files /dev/null and b/assets/scenes/mini/via-de-medici.png differ diff --git a/assets/scenes/mini/via-provinciale.png b/assets/scenes/mini/via-provinciale.png new file mode 100644 index 0000000..d7d66af Binary files /dev/null and b/assets/scenes/mini/via-provinciale.png differ diff --git a/assets/scenes/mini/viale-cipressi.png b/assets/scenes/mini/viale-cipressi.png new file mode 100644 index 0000000..104abed Binary files /dev/null and b/assets/scenes/mini/viale-cipressi.png differ diff --git a/assets/scenes/mini/villa-adami.png b/assets/scenes/mini/villa-adami.png new file mode 100644 index 0000000..227565b Binary files /dev/null and b/assets/scenes/mini/villa-adami.png differ diff --git a/docs/STATO.md b/docs/STATO.md index f70e918..f483b12 100644 --- a/docs/STATO.md +++ b/docs/STATO.md @@ -95,6 +95,12 @@ I prompt fissi (stile, scene, foglio sprite) sono in `prompts/STILE.md`, escluso descritto sul manifesto in piazza) e due occhi gialli che spariscono. La soluzione (40 passi, tragitti calcolati da `route`) passa per indovinello, oratorio e catena del ghiacciolo; il Trebbio ha un indizio per ogni tappa della torcia. Da fare: il disegno della fessura è provvisorio (forme in `art`); un taccuino con l'obiettivo corrente. +- **Mappa a gradini** (24 ottobre): una scheda per posto, con la miniatura dello sfondo (`make miniature`, + `assets/scenes/mini/`) e il nome, in quattro fasce dalla Fortezza ai campi; le strade fra le schede. Sta in una + schermata sul desktop; sul telefono si scorre in verticale. +- **Campomigliaio**: tre foto dell'Arcu (Marco Arcuri, ora nei crediti) trasformate da Codex: il ponte a due arcate sul + torrente, il Circolo SMS, la strada principale. Pronte in `assets/scenes/campomigliaio-{ponte,circolo,strada}.png`, + **ancora da collegare** (camminata, hotspot, uscite, posto sulla mappa). - **Pronuncia** (`tools/pronuncia.json`): le parole che XTTS legge male, scritte come vanno lette; vale solo per la voce. Per ora «celo» → «ce l'ho». Le 32 voci nuove del 24 ottobre non sono state controllate con Whisper (qui non installato). - **Sessione del 23-24 ottobre**: il paese è quasi tutto in gioco (circa 40 scene, sfondi di Codex dalle foto di diff --git a/src/engine/game.js b/src/engine/game.js index a35b4e2..3b55e68 100644 --- a/src/engine/game.js +++ b/src/engine/game.js @@ -32,6 +32,9 @@ export function check(cond, state) { // Un oggetto è «piccolo» per il dito sotto i 24×24 pixel di scena: col tocco ha la precedenza sui vicini grandi. const SMALL_AREA = 24 * 24; +// La miniatura di uno sfondo per la mappa (tools/miniature.py): assets/scenes/x.png -> assets/scenes/mini/x.png. +export const miniature = src => src?.replace(/([^/]+)$/, 'mini/$1') ?? null; + export const sayTime = text => Math.max(1.6, 0.9 + text.length * 0.055); export function newState(story) { @@ -313,7 +316,7 @@ export class Game { mapPlaces() { return Object.entries(this.story.scenes) .filter(([id, sc]) => sc.map && this.state.flags['visto:' + id]) - .map(([id, sc]) => ({ id, name: sc.name, at: sc.map, here: id === this.state.scene })); + .map(([id, sc]) => ({ id, name: sc.name, at: sc.map, here: id === this.state.scene, mini: miniature(sc.background) })); } mapRoads() { const seen = new Set(this.mapPlaces().map(p => p.id)), roads = new Map(); diff --git a/src/main.js b/src/main.js index 6c23c31..3c310ed 100644 --- a/src/main.js +++ b/src/main.js @@ -88,29 +88,56 @@ for (const b of document.querySelectorAll('.open-credits')) b.addEventListener(' $('close-credits').addEventListener('click', () => { $('credits').hidden = true; }); // --- Mappa: i posti visti e le strade fra loro; un clic su un posto ci porta (game.travel). +// La mappa: i posti visti come schede con la miniatura, a gradini come il paese (le quattro fasce della pianta, +// dalla Fortezza ai campi), e in ogni gradino da sinistra a destra. Le strade si disegnano dopo, fra le schede. +const MAP_BANDS = [25, 50, 75, Infinity]; function openMap() { if (!running || game.dialogue) return; - const board = $('map-board'), svg = $('map-roads'), places = game.mapPlaces(), at = Object.fromEntries(places.map(p => [p.id, p.at])); - svg.replaceChildren(...game.mapRoads().map(([a, b]) => { - const line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); - [['x1', at[a][0]], ['y1', at[a][1]], ['x2', at[b][0]], ['y2', at[b][1]]].forEach(([k, v]) => line.setAttribute(k, v)); - return line; - })); - board.replaceChildren(svg, ...places.map(p => { - const b = document.createElement('button'); - b.type = 'button'; - b.dataset.scene = p.id; - b.textContent = p.name; - b.classList.toggle('here', p.here); - b.style.left = p.at[0] + '%'; - b.style.top = p.at[1] + '%'; - b.addEventListener('click', () => { if (game.travel(p.id)) $('map').hidden = true; }); - return b; + const board = $('map-board'), svg = $('map-roads'), places = game.mapPlaces(); + const bands = MAP_BANDS.map(() => []); + for (const p of places) bands[MAP_BANDS.findIndex(top => p.at[1] < top)].push(p); + board.replaceChildren(svg, ...bands.filter(b => b.length).map(band => { + const row = document.createElement('div'); + row.className = 'map-band'; + row.append(...band.sort((a, b) => a.at[0] - b.at[0]).map(p => { + const b = document.createElement('button'); + b.type = 'button'; + b.dataset.scene = p.id; + b.title = p.name; + b.classList.toggle('here', p.here); + const img = document.createElement('img'); + img.alt = ''; + if (p.mini) img.src = p.mini; + const name = document.createElement('span'); + name.textContent = p.name; + b.append(img, name); + b.addEventListener('click', () => { if (game.travel(p.id)) $('map').hidden = true; }); + return b; + })); + return row; })); $('map').hidden = false; - // Sul telefono la pianta scorre: si parte col posto dove si è, al centro. + drawRoads(); + // Sul telefono la mappa scorre: si parte col posto dove si è, al centro. board.querySelector('.here')?.scrollIntoView({ block: 'center', inline: 'center' }); } +// Le strade fra le schede, misurate dopo l'impaginazione (e di nuovo se la finestra cambia misura). +function drawRoads() { + const board = $('map-board'), svg = $('map-roads'), origin = board.getBoundingClientRect(); + const center = id => { + const r = board.querySelector(`[data-scene="${id}"] img`)?.getBoundingClientRect(); + return r && [r.left + r.width / 2 - origin.left, r.top + r.height / 2 - origin.top]; + }; + svg.setAttribute('viewBox', `0 0 ${board.scrollWidth} ${board.scrollHeight}`); + svg.style.width = board.scrollWidth + 'px'; + svg.style.height = board.scrollHeight + 'px'; + svg.replaceChildren(...game.mapRoads().map(([a, b]) => [center(a), center(b)]).filter(([a, b]) => a && b).map(([a, b]) => { + const line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + [['x1', a[0]], ['y1', a[1]], ['x2', b[0]], ['y2', b[1]]].forEach(([k, v]) => line.setAttribute(k, v)); + return line; + })); +} +addEventListener('resize', () => { if (!$('map').hidden) drawRoads(); }); $('map-button').addEventListener('click', openMap); // --- «?»: per qualche secondo si vede tutto quello che si può toccare (sul telefono non c'è il mouse da passarci sopra). diff --git a/src/story/story.js b/src/story/story.js index d4a71f4..68434b7 100644 --- a/src/story/story.js +++ b/src/story/story.js @@ -54,7 +54,7 @@ export const story = { { role: 'Musica', names: ['Andrea Manzi, con Suno'] }, { role: 'Sfondi', names: ['dalle fotografie di San Piero di Andrea'] }, // Chi ci ha mandato foto, ricordi o idee: una riga per contributo, si allunga man mano. - { role: 'Fotografie', names: ['Mariano Mirannalti (l\'interno della Pieve)'] }, + { role: 'Fotografie', names: ['Mariano Mirannalti (l\'interno della Pieve)', 'Marco Arcuri «l\'Arcu» (Campomigliaio)'] }, { role: 'Opere d\'arte nel gioco', names: ['Simone Rocchi (l\'omino di ferro del tennis della chiesa)'] }, { role: 'Voci', names: ['XTTS-v2 di Coqui'] }, { role: 'Carattere', names: ['Press Start 2P di CodeMan38 (licenza OFL)'] }, diff --git a/style.css b/style.css index 315b002..7e98788 100644 --- a/style.css +++ b/style.css @@ -146,17 +146,23 @@ body.full #sound { position: fixed; z-index: 13; top: max(8px, env(safe-area-ins .load-error { margin: .6em 0 0; padding: .4em .8em; color: #fff; background: #7a1f1f; border-radius: 6px; font-size: max(9px, calc(var(--u) * 1.4)); } #map { z-index: 3; justify-content: flex-start; padding-top: 2.5%; background: rgba(15, 10, 20, .94); } #map h2 { font-size: clamp(12.96px, 2.16vw, 23.04px); } -/* Sui telefoni la pianta resta grande quanto serve per leggerla e si scorre col dito. */ -#map-scroll { width: 92%; flex: 1; min-height: 0; margin: .3em 0; overflow: auto; -webkit-overflow-scrolling: touch; } -#map-board { position: relative; width: 100%; height: 100%; min-width: 900px; min-height: 380px; - background: repeating-linear-gradient(transparent 0 calc(25% - 1px), rgba(185, 169, 194, .12) calc(25% - 1px) 25%), radial-gradient(circle at 50% 60%, #3a2d42, #231a29); border-radius: 8px; } -#map-roads { position: absolute; inset: 0; width: 100%; height: 100%; } -#map-roads line { stroke: #b9a9c2; stroke-width: 2.5; stroke-dasharray: 5 4; vector-effect: non-scaling-stroke; opacity: .7; } -#map-board button { position: absolute; transform: translate(-50%, -50%); font: 9px var(--font); padding: .35em .45em; - color: var(--bg); background: var(--ink); border: 2px solid #000; border-radius: 6px; cursor: pointer; white-space: nowrap; } -#map-board button:hover { background: var(--accent); } -#map-board button.here { color: var(--ink); background: #2f5fb3; cursor: default; } -#map-board button.here::after { content: ' · sei qui'; font-weight: normal; } +/* La mappa a gradini: una fascia per livello del paese, schede con miniatura e nome; si scorre se non ci sta. */ +#map-scroll { width: 94%; flex: 1; min-height: 0; margin: .3em 0; overflow: auto; -webkit-overflow-scrolling: touch; border-radius: 8px; + background: radial-gradient(circle at 50% 60%, #3a2d42, #231a29); } +#map-board { position: relative; display: flex; flex-direction: column; justify-content: space-around; gap: 4px; min-height: 100%; padding: 6px; box-sizing: border-box; } +#map-roads { position: absolute; left: 0; top: 0; pointer-events: none; } +#map-roads line { stroke: #b9a9c2; stroke-width: 2; stroke-dasharray: 4 4; opacity: .55; } +.map-band { position: relative; display: flex; flex-wrap: wrap; justify-content: space-around; align-items: flex-start; gap: 6px 4px; + padding-bottom: 4px; border-bottom: 1px solid rgba(185, 169, 194, .12); } +.map-band:last-child { border-bottom: 0; } +#map-board button { position: relative; display: flex; flex-direction: column; align-items: center; gap: 2px; width: 84px; padding: 3px; font: 7px var(--font); + color: var(--ink); background: #2a2031; border: 2px solid #4b3a55; border-radius: 6px; cursor: pointer; line-height: 1.2; } +#map-board button img { width: 76px; aspect-ratio: 16 / 9; image-rendering: pixelated; border-radius: 3px; background: #1b1420; } +#map-board button span { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; text-align: center; height: 4.2em; line-height: 1.4; } +#map-board button:hover, #map-board button:focus-visible { border-color: var(--accent); } +#map-board button.here { border-color: #5b8ff0; background: #2f5fb3; cursor: default; } +#map-board button.here::before { content: 'sei qui'; position: absolute; top: 1px; left: 50%; transform: translateX(-50%); padding: 1px 4px; + font-size: 6px; color: var(--bg); background: var(--accent); border-radius: 3px; white-space: nowrap; } /* I crediti crescono con chi contribuisce: scorre solo l'elenco, dedica e Chiudi restano sempre in vista. */ #credits { justify-content: flex-start; overflow: hidden; padding-top: 4%; background: rgba(15, 10, 20, .96); } #credits .credits-dedication { margin: .2em 0 .5em !important; font-size: clamp(10px, 1.6vw, 17px); } @@ -198,6 +204,9 @@ body.full #sound { position: fixed; z-index: 13; top: max(8px, env(safe-area-ins .buttons { gap: 6px; margin-top: .2em; } .buttons button { font-size: 10px; padding: .45em 1em; } .pause-buttons { min-width: 12em; } + /* Mappa: schede più piccole, ne stanno cinque o sei per riga. */ + #map-board button { width: 68px; } + #map-board button img { width: 60px; } #pause .hint { font-size: 8px; } #choices { min-height: 0; padding: 6px 8px 6px 2em; } } diff --git a/tests/story.test.mjs b/tests/story.test.mjs index 198bc41..14bf555 100644 --- a/tests/story.test.mjs +++ b/tests/story.test.mjs @@ -383,3 +383,11 @@ test('il Trebbio ha un indizio per ogni tappa della torcia, e la torcia usata tr early.act('use', 'fessura', 'torcia'); settle(early); assert.ok(!early.state.ended && !early.state.flags.palio_trovato, 'senza le impronte la fessura è solo buia'); }); + +test('ogni posto della mappa ha la sua miniatura (make miniature)', () => { + for (const p of new Game(story, { state: { scene: 'piazza', x: 250, y: 320, facing: 1, inventory: [], used: {}, ended: false, + flags: Object.fromEntries(Object.keys(story.scenes).map(id => ['visto:' + id, true])) } }).mapPlaces()) { + assert.ok(p.mini, `${p.id} senza sfondo`); + assert.ok(existsSync(new URL('../' + p.mini, import.meta.url)), `manca ${p.mini}: make miniature`); + } +}); diff --git a/tools/miniature.py b/tools/miniature.py new file mode 100644 index 0000000..9712eab --- /dev/null +++ b/tools/miniature.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Le miniature della mappa: ogni sfondo di assets/scenes/ rimpicciolito a 128x72 in assets/scenes/mini/. + + python3 tools/miniature.py (make miniature; rifà solo quelle più vecchie del loro sfondo) + +Si riduce con la media dei pixel (non a punti): a quella misura la pixel art si legge meglio. +""" +from pathlib import Path +from PIL import Image + +ROOT = Path(__file__).resolve().parent.parent +SCENES, MINI = ROOT / 'assets' / 'scenes', ROOT / 'assets' / 'scenes' / 'mini' +SIZE = (128, 72) + + +def main(): + MINI.mkdir(exist_ok=True) + done = 0 + for src in sorted(SCENES.glob('*.png')): + dst = MINI / src.name + if dst.exists() and dst.stat().st_mtime >= src.stat().st_mtime: + continue + img = Image.open(src).convert('RGB') + # Le panoramiche (più larghe di 16:9) si prendono dal centro. + w, h = img.size + cw = min(w, round(h * 16 / 9)) + img = img.crop(((w - cw) // 2, 0, (w - cw) // 2 + cw, h)).resize(SIZE, Image.Resampling.BOX) + # 64 colori come gli sfondi: il file pesa un terzo. + img.quantize(64, dither=Image.Dither.NONE).save(dst, optimize=True) + done += 1 + print(f'{done} miniature in {MINI.relative_to(ROOT)}') + + +if __name__ == '__main__': + main()