From c352ed53e9987328d92a017bde6ef76b39071870 Mon Sep 17 00:00:00 2001 From: cammo22 <78992276+cammo22@users.noreply.github.com> Date: Mon, 9 Nov 2026 21:23:01 +0100 Subject: [PATCH] 2.2.8: le icone piccole si leggono, e la musica in Esplora file mostra copertina e onda. Co-authored-by: Cursor --- AGENTS.md | 7 + CHANGELOG.md | 6 + Directory.Build.props | 2 +- docs/GUIDA.md | 16 +- docs/ROADMAP.md | 34 +++++ src/DaPConvertitore.Motore/Lettore/Media.cs | 5 +- src/DaPConvertitore/Miniature/Documenti.cs | 1 + src/DaPConvertitore/Miniature/Glifi.cs | 160 ++++++++++++++++++- src/DaPConvertitore/Miniature/Icone.cs | 25 +-- src/DaPConvertitore/Miniature/Multimedia.cs | 161 ++++++++++++++------ src/DaPConvertitore/Miniature/Tela.cs | 29 +++- 11 files changed, 377 insertions(+), 69 deletions(-) create mode 100644 docs/ROADMAP.md diff --git a/AGENTS.md b/AGENTS.md index b33519c..2efa67a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -125,3 +125,10 @@ Cosa fa e come si usa: `README.md` (corto, per l'utente) e `docs/GUIDA.md` (comp - **La home** (`ui/src/app/home.ts`): le sei strade (il selettore si apre già filtrato: comando `scegli` con `categoria`), gli ultimi convertiti (`Storico.cs`, `storico.json`, solo l'app lo scrive: la coda lo chiama tramite `Coda.QuandoFatto`) e come sta il PC. - **Suoni** spenti di partenza: la proprietà si chiama `SuoniAccesi` (nome nuovo apposta, come `CestinoDaSolo`). +- **Icone e miniature** (dalla 2.2.8): `Icone.cs` scrive il .ico del tipo (16…256). Fino a 24 pixel è `Glifi.Mattonella` + (sagoma piena: a 16 il foglio piegato era una macchia); da 32 in su il foglio, col tratto in pixel e non + `1.45/scala` (quel conto a 32 pixel gonfiava il segno). La miniatura audio (`Multimedia.Audio`) mette la copertina + a sinistra — `Media.Copertina`, dentro il file o `cover.jpg` / `folder.jpg`, in silenzio — e l'onda vera a destra + (`Onde.Barre`). Sotto i 48 la copertina sta sopra e l'onda è una striscia; a 16–24 senza copertina si torna null e + resta il .ico. Il nome del file .ico porta la versione: al cambio si buttano i vecchi. Cosa non si fa ora (scheda + del file, tagli piccoli, niente montaggio: quello è DaProd Editor Video) sta in `docs/ROADMAP.md`. diff --git a/CHANGELOG.md b/CHANGELOG.md index be3e395..795d1d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ Le novità di ogni versione, dette come stanno. La più nuova sta in alto. +## 2.2.8 — 9 novembre 2026 + +- **Le icone dei file sono rifatte, e quelle piccole si leggono.** A 16, 20 e 24 pixel non c'è più il foglio piegato (diventava una macchia): c'è una mattonella tonda, viola notte, col bordo del colore del tipo e una sagoma piena. Video, musica, foto, PDF, documenti, fogli, presentazioni, codice, sottotitoli, archivi, caratteri e modelli hanno ognuno il suo segno. Dalle 32 pixel in su resta il foglio, col disegnino più nitido. +- **La musica in Esplora file mostra la copertina e l'onda vera.** A sinistra la copertina del brano (quella dentro il file, oppure `cover.jpg` / `folder.jpg` nella cartella), a destra la forma d'onda di quel brano, magenta e oro. Senza copertina c'è un disco. Sotto, il formato e la durata. Nelle viste piccole la copertina riempie l'icona e l'onda diventa una striscia; a 16 pixel, se il brano non ha copertina, resta la mattonella del tipo, che è più chiara. +- **In arrivo**, senza fretta e senza diventare un programma di montaggio: la scheda del file (titolo, artista, copertina, e le informazioni che si possono correggere) e i tagli piccoli. Il montaggio vero resta [DaProd Editor Video](https://github.com/cammo22/DaProdEditorVideo). I dettagli sono in `docs/ROADMAP.md`. + ## 2.2.7 — 4 novembre 2026 - **La colonna di mezzo non si rompe più con l'anteprima aperta.** Si regolava sulla larghezza della finestra, non sulla sua: con diff --git a/Directory.Build.props b/Directory.Build.props index 2360c39..979bf53 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ - 2.2.7 + 2.2.8 DaProd DaProdProduzioni DaP Convertitore diff --git a/docs/GUIDA.md b/docs/GUIDA.md index 5a636f2..564676f 100644 --- a/docs/GUIDA.md +++ b/docs/GUIDA.md @@ -233,13 +233,15 @@ Sempre: **Pag ↑ / Pag ↓** il file prima e dopo, **F11** schermo intero, **I* finestra e lo apre. **Icone e anteprime in Esplora file.** Ogni tipo ha la sua icona (magenta i video, verde la musica, ciano le foto, -rossa il PDF, oro i modelli 3D…) e nelle viste a icone grandi si vede il contenuto vero: la **foto**, il -**fotogramma del video** con la pellicola, il tasto play e la durata, la **forma d'onda** della musica (come i vocali -di WhatsApp, con la durata), il **modello 3D** su uno sfondo da studio, la prima pagina del **PDF** e dei documenti, -il testo e il codice colorato, le tabelle, i caratteri («Aa» nel carattere stesso), il contenuto dei file ZIP. Dove -Windows ne ha già una buona (Word con Office, per dire) resta la sua. Le anteprime vengono disegnate dall'exe -(`--miniatura`), lanciato da una piccola DLL (`menu\DaPMiniature.cpp`) registrata come «provider di miniature»; per -guardarne una a occhio: `DaPConvertitore.exe --miniatura-png file 256 uscita.png`. +rossa il PDF, oro i modelli 3D…). Nelle viste piccole è una mattonella col segno del tipo; in quelle grandi è un +foglio col bordo piegato. Accanto si vede il contenuto vero: la **foto**, il **fotogramma del video** con la +pellicola, il tasto play e la durata, la **musica** con la copertina a sinistra e la forma d'onda di quel brano a +destra (formato e durata sotto; senza copertina c'è un disco), il **modello 3D** su uno sfondo da studio, la prima +pagina del **PDF** e dei documenti, il testo e il codice colorato, le tabelle, i caratteri («Aa» nel carattere +stesso), il contenuto dei file ZIP. Dove Windows ne ha già una buona (Word con Office, per dire) resta la sua. Le +anteprime vengono disegnate dall'exe (`--miniatura`), lanciato da una piccola DLL (`menu\DaPMiniature.cpp`) +registrata come «provider di miniature»; per guardarne una a occhio: +`DaPConvertitore.exe --miniatura-png file 256 uscita.png`. **Al posto di Foto e Lettore multimediale.** Il lettore si mette in *Apri con* di Windows per tutti i tipi che conosce, senza prendersi niente da solo. Per aprire col doppio clic: *Impostazioni → App predefinite* nel diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..315c14d --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,34 @@ +# Più avanti + +Cose che ha senso fare, senza trasformare DaP Convertitore in un altro programma. Il montaggio vero sta in +[DaProd Editor Video](https://github.com/cammo22/DaProdEditorVideo): timeline, transizioni, colore, pagina Finale. +Qui restano i gesti piccoli, quelli che fai sul file che stai già guardando. + +## La scheda del file + +Nel lettore, **I** mostra già le proprietà. Il passo dopo è poterle **correggere e scriverle nel file**, con la +stessa bozza che già usano la rotazione e il ritocco (si scrive accanto, poi si sostituisce; se qualcosa va storto +l'originale resta). + +- **Musica**: titolo, artista, album, anno, numero di traccia, e la copertina (quella dentro il file, o un'immagine + scelta). I formati che la tengono sono MP3, M4A e FLAC; gli altri lo dicono. +- **Foto**: data dello scatto, descrizione, autore, dove il formato tiene i dati (JPG e TIFF). L'orientamento si + cambia già. +- **Video**: titolo e commento, sui contenitori che li tengono (MP4, MOV, MKV), senza ricodificare. + +Niente campi inventati e niente di quello che il formato non sa conservare. + +## Tagli piccoli + +Nel lettore video **Z** e **X** segnano già un pezzo e lo salvano accanto, senza perdite. Da Editor Video si prende +solo l'idea dei gesti corti (lì **1** taglia, **2** toglie, **Q** e **W** tengono la testa o la coda), non il banco. + +- Tenere solo la testa, solo la coda, o il pezzo fra due punti. Il file nuovo si chiama «(tagliato)» e l'originale + non si tocca. +- Lo stesso su un **brano**: un pezzo solo, accanto, senza ricodificare dove il formato lo permette e ricodificando + dove non si può (MP3). +- **Unire** due o tre file uno dopo l'altro, stesso tipo (due video, o due brani), senza transizioni e senza tracce + in più. Il risultato sta accanto. + +Niente timeline a più piste, niente dissolvenze, niente cubo, niente pagina del colore. Per quello si apre +DaProd Editor Video. diff --git a/src/DaPConvertitore.Motore/Lettore/Media.cs b/src/DaPConvertitore.Motore/Lettore/Media.cs index 07acc17..0f482f9 100644 --- a/src/DaPConvertitore.Motore/Lettore/Media.cs +++ b/src/DaPConvertitore.Motore/Lettore/Media.cs @@ -69,15 +69,16 @@ public static async Task Leggi(Strumenti s, string percorso) } /// La copertina: quella dentro il file, o cover.jpg / folder.jpg nella cartella. JPEG piccolo. - public static async Task Copertina(Strumenti s, string percorso, bool dentro) + public static async Task Copertina(Strumenti s, string percorso, bool dentro, bool silenzioso = false, int lato = 800) { if (dentro) { var jpg = Path.Combine(Strumenti.CartellaTemporanea, $"copertina-{Guid.NewGuid():N}.jpg"); try { + var scala = $"scale='min({Math.Clamp(lato, 32, 800)},iw)':-2"; var r = await Processi.Esegui(s.Ffmpeg, ["-hide_banner", "-loglevel", "error", "-y", "-i", percorso, "-map", "0:v:0", "-frames:v", "1", - "-vf", "scale='min(800,iw)':-2", "-q:v", "3", "-update", "1", jpg], CancellationToken.None, tieniUscita: false); + "-vf", scala, "-q:v", "3", "-update", "1", jpg], CancellationToken.None, tieniUscita: false, silenzioso: silenzioso); if (r.Codice == 0 && File.Exists(jpg)) return await File.ReadAllBytesAsync(jpg); } finally { try { File.Delete(jpg); } catch { } } diff --git a/src/DaPConvertitore/Miniature/Documenti.cs b/src/DaPConvertitore/Miniature/Documenti.cs index c9f4403..8dec0d3 100644 --- a/src/DaPConvertitore/Miniature/Documenti.cs +++ b/src/DaPConvertitore/Miniature/Documenti.cs @@ -498,6 +498,7 @@ public static Quadro Generica(string gruppo, string estensione, int L) { var tinta = Tavolozza.Tinta(gruppo); var est = estensione.TrimStart('.').ToUpperInvariant(); + if (L <= 32) return Tela.Fai(L, L, dc => Glifi.Mattonella(dc, gruppo, new Rect(0, 0, L, L))); return Tela.Fai(L, L, dc => { var r = new Rect(0, 0, L, L); diff --git a/src/DaPConvertitore/Miniature/Glifi.cs b/src/DaPConvertitore/Miniature/Glifi.cs index 8433e24..d83bc5c 100644 --- a/src/DaPConvertitore/Miniature/Glifi.cs +++ b/src/DaPConvertitore/Miniature/Glifi.cs @@ -47,11 +47,17 @@ static Geometry Forma(string gruppo) /// /// Disegna la figurina del gruppo dentro (un quadrato). è in - /// unità della griglia di 24: 1.8 è quello dell'interfaccia. + /// unità della griglia di 24: 1.8 è quello dell'interfaccia. Sotto i 20 pixel il tratto diventa un blob: lì si + /// usa la sagoma piena, fatta apposta per le misure piccole. /// public static void Disegna(DrawingContext dc, string gruppo, Rect dove, Color colore, double spessore = 1.8, double alfa = 1) { var lato = Math.Min(dove.Width, dove.Height); + if (lato < 20) + { + Pieno(dc, gruppo, dove, colore, alfa); + return; + } var s = lato / 24.0; dc.PushTransform(new TranslateTransform(dove.X + (dove.Width - lato) / 2, dove.Y + (dove.Height - lato) / 2)); dc.PushTransform(new ScaleTransform(s, s)); @@ -59,4 +65,156 @@ public static void Disegna(DrawingContext dc, string gruppo, Rect dove, Color co dc.Pop(); dc.Pop(); } + + /// + /// La mattonella delle misure piccole (16, 20, 24): un quadrato tondo, viola notte, col bordo del tipo e la + /// sagoma piena in mezzo. A 16 pixel una pagina piegata non si legge. + /// + public static void Mattonella(DrawingContext dc, string gruppo, Rect area) + { + var tinta = Tavolozza.Tinta(gruppo); + // a 16 pixel il bordo grosso e il segno piccolo si mangiavano il disegno: bordo da un pixel, segno grande + double m = area.Width <= 16 ? 0.35 : Math.Max(0.5, area.Width * 0.045); + double bordo = area.Width <= 16 ? 1 : Math.Max(1, area.Width * 0.065); + var r = new Rect(area.X + m + bordo / 2, area.Y + m + bordo / 2, Math.Max(1, area.Width - 2 * m - bordo), Math.Max(1, area.Height - 2 * m - bordo)); + var fondo = Tavolozza.P(Tavolozza.C("#160e2c")); + dc.DrawRoundedRectangle(fondo, new Pen(Tavolozza.P(tinta), bordo) { LineJoin = PenLineJoin.Round }, r, r.Width * 0.22, r.Width * 0.22); + var lato = r.Width * (area.Width <= 20 ? 0.78 : 0.62); + Pieno(dc, gruppo, new Rect(r.X + (r.Width - lato) / 2, r.Y + (r.Height - lato) / 2, lato, lato), tinta); + } + + /// La sagoma piena del tipo, su una griglia di 24. Pochi pezzi grossi: si legge anche a 16 pixel. + public static void Pieno(DrawingContext dc, string gruppo, Rect dove, Color colore, double alfa = 1) + { + var lato = Math.Min(dove.Width, dove.Height); + var ox = dove.X + (dove.Width - lato) / 2; + var oy = dove.Y + (dove.Height - lato) / 2; + double X(double v) => ox + v / 24.0 * lato; + double Y(double v) => oy + v / 24.0 * lato; + double S(double v) => v / 24.0 * lato; + var b = Tavolozza.P(colore, alfa); + + void Rett(double x, double y, double w, double h) + { + var rr = Math.Min(S(w), S(h)) * 0.42; + dc.DrawRoundedRectangle(b, null, new Rect(X(x), Y(y), S(w), S(h)), rr, rr); + } + + void Poli(params (double x, double y)[] p) + { + var g = new StreamGeometry(); + using (var c = g.Open()) + { + c.BeginFigure(new Point(X(p[0].x), Y(p[0].y)), true, true); + for (var i = 1; i < p.Length; i++) c.LineTo(new Point(X(p[i].x), Y(p[i].y)), true, true); + } + g.Freeze(); + dc.DrawGeometry(b, null, g); + } + + // sotto i 14 pixel la cinepresa, le parentesi e il diamante diventano una macchia: un segno solo, grosso + if (lato < 14) + { + switch (gruppo) + { + case "Video": + Poli((4.2, 3.2), (4.2, 20.8), (21, 12)); + return; + case "Codice": + Poli((1.4, 5.2), (9.2, 12), (1.4, 18.8)); + Poli((22.6, 5.2), (14.8, 12), (22.6, 18.8)); + return; + case "Immagine": + dc.DrawEllipse(b, null, new Point(X(7.2), Y(6.4)), S(2.6), S(2.6)); + Poli((2, 19.6), (12, 8.4), (22, 19.6)); + return; + case "Modello": + Poli((12, 1.6), (22.2, 20.4), (1.8, 20.4)); + return; + } + } + + switch (gruppo) + { + case "Video": + Rett(1.6, 6.4, 12.6, 11.2); + Poli((14.6, 8.4), (21.8, 12), (14.6, 15.6)); + break; + case "Audio": + Rett(3.4, 11, 4.2, 8.4); + Rett(9.9, 4.8, 4.2, 14.6); + Rett(16.4, 8, 4.2, 11.4); + break; + case "Immagine": + dc.DrawEllipse(b, null, new Point(X(8.2), Y(8)), S(2.5), S(2.5)); + Poli((2.4, 19.4), (10.2, 11.2), (15.2, 16.2), (18.2, 13.4), (21.8, 19.4)); + break; + case "Pdf": + Poli((5.5, 2), (13.2, 2), (19.2, 8), (19.2, 22), (5.5, 22)); + var piega = new StreamGeometry(); + using (var c = piega.Open()) + { + c.BeginFigure(new Point(X(13.2), Y(2)), true, true); + c.LineTo(new Point(X(19.2), Y(8)), true, true); + c.LineTo(new Point(X(13.2), Y(8)), true, true); + } + piega.Freeze(); + dc.DrawGeometry(Tavolozza.P(Colors.White, alfa * 0.92), null, piega); + break; + case "Documento": + Rett(4, 4.2, 16, 2.5); + Rett(4, 9.2, 16, 2.5); + Rett(4, 14.2, 16, 2.5); + Rett(4, 19.2, 10, 2.5); + break; + case "Presentazione": + Rett(2, 3.6, 20, 12.2); + Rett(10.4, 16.2, 3.2, 2.6); + Rett(6.2, 18.8, 11.6, 2.2); + break; + case "Tabella": + Rett(2.6, 2.6, 8.2, 8.2); + Rett(13.2, 2.6, 8.2, 8.2); + Rett(2.6, 13.2, 8.2, 8.2); + Rett(13.2, 13.2, 8.2, 8.2); + break; + case "Codice": + var penna = new Pen(b, Math.Max(1.15, S(2.6))) { StartLineCap = PenLineCap.Round, EndLineCap = PenLineCap.Round, LineJoin = PenLineJoin.Round }; + penna.Freeze(); + dc.DrawLine(penna, new Point(X(9.4), Y(5.2)), new Point(X(3.2), Y(12))); + dc.DrawLine(penna, new Point(X(3.2), Y(12)), new Point(X(9.4), Y(18.8))); + dc.DrawLine(penna, new Point(X(14.6), Y(5.2)), new Point(X(20.8), Y(12))); + dc.DrawLine(penna, new Point(X(20.8), Y(12)), new Point(X(14.6), Y(18.8))); + break; + case "Sottotitoli": + var cornice = new Pen(b, Math.Max(1.1, S(1.9))) { LineJoin = PenLineJoin.Round }; + cornice.Freeze(); + dc.DrawRoundedRectangle(null, cornice, new Rect(X(2.2), Y(5.4), S(19.6), S(13.2)), S(2.2), S(2.2)); + Rett(4.8, 10.4, 5.4, 2.2); + Rett(12, 10.4, 6.6, 2.2); + break; + case "Archivio": + Rett(6.6, 2.4, 10.8, 3.3); + Rett(6.6, 7.4, 10.8, 3.3); + Rett(6.6, 12.4, 10.8, 3.3); + Rett(9, 17.2, 6, 4.4); + break; + case "Font": + var lettera = Scritte.Testo("A", Scritte.Titolo, lato * 0.82, b); + dc.DrawText(lettera, new Point(ox + (lato - lettera.Width) / 2, oy + (lato - lettera.Height) / 2 - lato * 0.04)); + break; + case "Modello": + Poli((12, 1.8), (21.6, 12), (12, 22.2), (2.4, 12)); + break; + case "Testo": + Rett(3.2, 4, 17.6, 2.15); + Rett(3.2, 8.3, 17.6, 2.15); + Rett(3.2, 12.6, 14, 2.15); + Rett(3.2, 16.9, 11, 2.15); + break; + default: + Poli((6, 2), (14, 2), (19.5, 7.5), (19.5, 22), (6, 22)); + break; + } + } } diff --git a/src/DaPConvertitore/Miniature/Icone.cs b/src/DaPConvertitore/Miniature/Icone.cs index e40967c..977d581 100644 --- a/src/DaPConvertitore/Miniature/Icone.cs +++ b/src/DaPConvertitore/Miniature/Icone.cs @@ -4,9 +4,10 @@ namespace DaP.Convertitore.App.Miniature; /// -/// Le icone dei tipi di file (quelle che si vedono nell'elenco, nei file piccoli, sul desktop): un foglio col -/// bordo piegato, viola notte, col disegnino del tipo nella sua tinta (magenta i video, verde la musica, ciano le foto…). -/// Si disegnano al volo a tutte le misure e si scrivono in file .ico accanto ai dati dell'app. +/// Le icone dei tipi di file (quelle che si vedono nell'elenco, nei file piccoli, sul desktop). Dalle 32 pixel in su +/// è un foglio col bordo piegato, viola notte, col disegnino del tipo nella sua tinta. A 16, 20 e 24 il foglio non +/// si legge: è una mattonella tonda con la sagoma piena. Si disegnano al volo e si scrivono in file .ico accanto ai +/// dati dell'app. /// public static class Icone { @@ -15,10 +16,12 @@ public static class Icone /// Il disegno di un'icona a una misura. static Quadro Disegna(string gruppo, int S) { + if (S <= 24) return Tela.Fai(S, S, dc => Glifi.Mattonella(dc, gruppo, new Rect(0, 0, S, S))); + var tinta = Tavolozza.Tinta(gruppo); return Tela.Fai(S, S, dc => { - double x = S * 0.14, y = S * 0.05, w = S * 0.72, h = S * 0.9, taglio = S * 0.24; + double x = S * 0.12, y = S * 0.05, w = S * 0.76, h = S * 0.9, taglio = S * 0.22; var pagina = new StreamGeometry(); using (var g = pagina.Open()) { @@ -31,10 +34,9 @@ static Quadro Disegna(string gruppo, int S) pagina.Freeze(); var fondo = new LinearGradientBrush(Tavolozza.C("#3a2268"), Tavolozza.C("#150f27"), 100); fondo.Freeze(); - var bordo = Math.Max(1, S * 0.035); + var bordo = Math.Max(1, S * 0.028); dc.DrawGeometry(fondo, new Pen(Tavolozza.P(tinta, 0.95), bordo) { LineJoin = PenLineJoin.Round }, pagina); - // l'angolo piegato var piega = new StreamGeometry(); using (var g = piega.Open()) { @@ -43,13 +45,14 @@ static Quadro Disegna(string gruppo, int S) g.LineTo(new Point(x + w - taglio, y + taglio), true, true); } piega.Freeze(); - dc.DrawGeometry(Tavolozza.P(tinta, 0.9), null, piega); + dc.DrawGeometry(Tavolozza.P(tinta, 0.92), null, piega); - // il disegnino del tipo, grande: nelle misure piccole deve restare leggibile - var lato = S * (S <= 24 ? 0.5 : 0.46); + // il disegnino sta nella parte bassa del foglio, lontano dalla piega. Il tratto è in pixel, non in + // unità della griglia: prima a 32 pixel diventava un blob. + var lato = S * (S <= 48 ? 0.50 : 0.46); var scala = lato / 24.0; - var spessore = Math.Max(1.8, 1.45 / scala); - Glifi.Disegna(dc, gruppo, new Rect(x + (w - lato) / 2 - (S <= 24 ? 0 : 0), y + h * 0.4, lato, lato), tinta, spessore); + var pixel = S <= 48 ? 1.6 : Math.Min(4.4, 1.25 + S * 0.011); + Glifi.Disegna(dc, gruppo, new Rect(x + (w - lato) / 2, y + h * 0.36, lato, lato), tinta, pixel / scala); }); } diff --git a/src/DaPConvertitore/Miniature/Multimedia.cs b/src/DaPConvertitore/Miniature/Multimedia.cs index 3cdbb33..8cc82a4 100644 --- a/src/DaPConvertitore/Miniature/Multimedia.cs +++ b/src/DaPConvertitore/Miniature/Multimedia.cs @@ -7,8 +7,8 @@ namespace DaP.Convertitore.App.Miniature; /// -/// Le miniature che mostrano il contenuto: il fotogramma del video (col suo tasto play e la pellicola), la forma -/// d'onda della musica (come i vocali di WhatsApp), il modello 3D su uno sfondo da studio, la foto. +/// Le miniature che mostrano il contenuto: il fotogramma del video (col suo tasto play e la pellicola), la musica +/// con la copertina a sinistra e la forma d'onda vera a destra, il modello 3D su uno sfondo da studio, la foto. /// public static class Multimedia { @@ -63,8 +63,9 @@ public static class Multimedia } } - // il tasto play al centro: vetro scuro, anello magenta-oro, triangolo bianco - var d = Math.Max(minimo * 0.36, 14); + // il tasto play al centro: vetro scuro, anello magenta-oro, triangolo bianco. + // sotto i 48 pixel il minimo di 14 copriva tutto il fotogramma + var d = minimo * (L >= 48 ? 0.36 : 0.46); var c = new Point(fw / 2.0, fh / 2.0); dc.DrawEllipse(Tavolozza.P(Colors.Black, 0.28), null, c, d / 2 * 1.16, d / 2 * 1.16); dc.DrawEllipse(Tavolozza.P(Tavolozza.Fondo, 0.66), null, c, d / 2, d / 2); @@ -105,73 +106,143 @@ public static class Multimedia // ————————————————————————————————————— AUDIO ————————————————————————————————————— + /// + /// Dove sta la copertina e dove sta l'onda. Dalle 48 pixel in su la copertina è il riquadro a sinistra e l'onda + /// quello a destra; fra 32 e 47 la copertina sta sopra e l'onda è una striscia; sotto, se non c'è copertina, + /// non si disegna niente (resta l'icona del tipo, che a 16 pixel è più nitida di una copertina schiacciata). + /// + readonly record struct SchemaAudio(int W, int H, Rect Cover, Rect Onda, bool Pastiglie, int Barre); + + static SchemaAudio ImpaginaAudio(int L) + { + if (L < 48) + { + double m = Math.Max(1, L * 0.07); + double striscia = L <= 24 ? 0 : Math.Max(6, L * 0.34); + double lato = L <= 24 ? L - 2 * m : Math.Min(L - 2 * m, L - 2 * m - striscia - m * 0.45); + var cover = new Rect((L - lato) / 2, m, lato, lato); + var onda = striscia <= 0 ? Rect.Empty : new Rect(m, L - m - striscia, L - 2 * m, striscia); + var passo = Math.Max(2, L * 0.08); + var barre = onda.IsEmpty ? 0 : Math.Clamp((int)(onda.Width / passo), 5, 16); + return new SchemaAudio(L, L, cover, onda, false, barre); + } + + int W = L, H = (int)Math.Round(L * 0.72); + bool pastiglie = L >= 96; + double padX = W * 0.048, padY = H * 0.07, fascia = pastiglie ? H * 0.20 : padY; + double utile = H - padY * 0.45 - fascia; + double latoCover = Math.Min(utile * 0.96, W * 0.38); + double y = padY * 0.35 + Math.Max(0, (utile - latoCover) / 2); + var c = new Rect(padX, y, latoCover, latoCover); + double xOnda = c.Right + W * 0.04; + var z = new Rect(xOnda, y, Math.Max(4, W - padX - xOnda), latoCover); + int n = Math.Clamp((int)(z.Width / Math.Max(2.2, L * 0.0145)), 8, 64); + return new SchemaAudio(W, H, c, z, pastiglie, n); + } + public static Quadro? Audio(string percorso, int L, Strumenti s) { - int W = L, H = (int)Math.Round(L * 0.7); - var u = L / 256.0; - double larBarra = Math.Max(1.6, 3.3 * u), vuoto = larBarra * 0.72; - double x0 = W * 0.285, x1 = W * 0.93; - var barre = Math.Max(8, (int)((x1 - x0 + vuoto) / (larBarra + vuoto))); - var onda = Aspetta(() => Onde.Barre(s, percorso, barre, TimeSpan.FromSeconds(14))); - if (onda.Length == 0) return null; + var schema = ImpaginaAudio(L); var info = Sonda_(s, percorso); + BitmapSource? cover = null; + try + { + var lato = Math.Clamp((int)schema.Cover.Width * 2, 64, 512); + var jpg = Aspetta(() => Media.Copertina(s, percorso, info?.Copertina == true, silenzioso: true, lato: lato)); + cover = Tela.Immagine(jpg, lato); + } + catch { /* niente copertina: resta il disco */ } + + // a 16 e 24, senza copertina, l'icona del tipo è più chiara di un dischetto sfocato + if (L <= 24 && cover is null) return null; + + float[] onda = []; + if (schema.Barre > 0) + { + try { onda = Aspetta(() => Onde.Barre(s, percorso, schema.Barre, TimeSpan.FromSeconds(12))); } + catch { onda = []; } + } + if (onda.Length == 0 && cover is null) return null; + if (onda.Length != schema.Barre && schema.Barre > 0) + onda = onda.Length == 0 ? Enumerable.Repeat(0.18f, schema.Barre).ToArray() : onda; + var durata = info?.Durata ?? 0; var est = Path.GetExtension(percorso).TrimStart('.').ToUpperInvariant(); - var grande = L >= 96; + int W = schema.W, H = schema.H; return Tela.Fai(W, H, dc => { var r = new Rect(0, 0, W, H); - var raggio = H * 0.1; + var raggio = Math.Min(W, H) * (L < 48 ? 0.18 : 0.1); var fondo = new LinearGradientBrush(Tavolozza.ViolaChiaro, Tavolozza.Fondo, 125); fondo.Freeze(); dc.DrawRoundedRectangle(fondo, null, r, raggio, raggio); Tela.Ritaglia(dc, r, raggio); - var luce = new RadialGradientBrush(Color.FromArgb(70, 255, 61, 242), Color.FromArgb(0, 255, 61, 242)) { Center = new Point(0.12, 0.45), GradientOrigin = new Point(0.12, 0.45), RadiusX = 0.6, RadiusY = 0.9 }; + var luce = new RadialGradientBrush(Color.FromArgb(64, 255, 61, 242), Color.FromArgb(0, 255, 61, 242)) { Center = new Point(0.18, 0.4), GradientOrigin = new Point(0.18, 0.4), RadiusX = 0.55, RadiusY = 0.8 }; luce.Freeze(); dc.DrawRectangle(luce, null, r); - // il tasto play a sinistra, come nei vocali - double cy = H * (grande ? 0.43 : 0.5), d = H * 0.3; - var c = new Point(W * 0.145, cy); - var oro = new LinearGradientBrush(Tavolozza.Oro, Tavolozza.Oro2, 90); - oro.Freeze(); - dc.DrawEllipse(oro, null, c, d / 2, d / 2); - var tri = new StreamGeometry(); - using (var g = tri.Open()) - { - double k = d * 0.2; - g.BeginFigure(new Point(c.X - k * 0.6, c.Y - k * 1.1), true, true); - g.LineTo(new Point(c.X + k * 1.2, c.Y), true, false); - g.LineTo(new Point(c.X - k * 0.6, c.Y + k * 1.1), true, false); - } - tri.Freeze(); - dc.DrawGeometry(Tavolozza.P(Tavolozza.Fondo), null, tri); - - // le barre: la forma d'onda vera del brano, magenta all'inizio e oro alla fine - var sfum = new LinearGradientBrush(Tavolozza.Magenta, Tavolozza.Oro, new Point(x0, 0), new Point(x1, 0)) { MappingMode = BrushMappingMode.Absolute }; - sfum.Freeze(); - double massimo = H * (grande ? 0.31 : 0.4), minimo = larBarra * 0.6; - for (var i = 0; i < barre; i++) - { - var mezza = minimo + onda[i] * (massimo - minimo); - var x = x0 + i * (larBarra + vuoto); - dc.DrawRoundedRectangle(sfum, null, new Rect(x, cy - mezza, larBarra, mezza * 2), larBarra / 2, larBarra / 2); - } + DisegnaCopertina(dc, schema.Cover, cover); + if (!schema.Onda.IsEmpty && onda.Length > 0) DisegnaOnda(dc, schema.Onda, onda); - if (grande) + if (schema.Pastiglie) { var h = Math.Max(11, H * 0.115); - var y = H - h - H * 0.07; - var vetro = Tavolozza.P(Colors.Black, 0.38); + var y = H - h - H * 0.055; + var vetro = Tavolozza.P(Colors.Black, 0.45); if (est.Length is > 0 and <= 5) Tela.Pastiglia(dc, est, W * 0.045, y, h, vetro, Tavolozza.P(Tavolozza.Oro)); if (durata > 0) Tela.Pastiglia(dc, Tela.Durata(durata), W * 0.955, y, h, vetro, Tavolozza.P(Colors.White, 0.92), aDestra: true); } dc.Pop(); - dc.DrawRoundedRectangle(null, Tavolozza.Penna(Colors.White, 1, 0.12), new Rect(0.5, 0.5, W - 1, H - 1), raggio, raggio); + dc.DrawRoundedRectangle(null, Tavolozza.Penna(Colors.White, 1, 0.14), new Rect(0.5, 0.5, W - 1, H - 1), raggio, raggio); }); } + /// La copertina nel riquadro di sinistra, tagliata al centro. Senza immagine, un disco oro. + static void DisegnaCopertina(DrawingContext dc, Rect r, BitmapSource? img) + { + if (r.Width < 2 || r.Height < 2) return; + var rad = r.Width * 0.16; + if (img is not null) + { + Tela.Copri(dc, img, r, rad); + dc.DrawRoundedRectangle(null, Tavolozza.Penna(Tavolozza.Oro, Math.Max(1, r.Width * 0.035)), r, rad, rad); + return; + } + var c = new Point(r.X + r.Width / 2, r.Y + r.Height / 2); + double rx = r.Width / 2; + dc.PushClip(new RectangleGeometry(r, rad, rad)); + dc.DrawEllipse(Tavolozza.P(Tavolozza.C("#1a1030")), null, c, rx, rx); + dc.DrawEllipse(null, Tavolozza.Penna(Tavolozza.Oro, Math.Max(1, rx * 0.07)), c, rx * 0.78, rx * 0.78); + dc.DrawEllipse(null, Tavolozza.Penna(Tavolozza.Tinta("Audio"), Math.Max(1, rx * 0.05), 0.85), c, rx * 0.48, rx * 0.48); + dc.DrawEllipse(Tavolozza.P(Tavolozza.Oro), null, c, Math.Max(1.2, rx * 0.14), Math.Max(1.2, rx * 0.14)); + dc.Pop(); + dc.DrawRoundedRectangle(null, Tavolozza.Penna(Tavolozza.Oro, Math.Max(1, r.Width * 0.035), 0.85), r, rad, rad); + } + + /// L'onda vera del brano, nel riquadro a destra: magenta dove comincia, oro dove finisce. + static void DisegnaOnda(DrawingContext dc, Rect zona, float[] onda) + { + int n = onda.Length; + if (n == 0 || zona.Width < 2) return; + double lar = zona.Width / (n + (n - 1) * 0.42); + double vuoto = lar * 0.42; + double usato = n * lar + (n - 1) * vuoto; + double x0 = zona.X + (zona.Width - usato) / 2; + double cy = zona.Y + zona.Height / 2; + double massimo = zona.Height * 0.48; + double minimo = Math.Max(0.8, Math.Min(lar * 0.55, massimo * 0.18)); + var sfum = new LinearGradientBrush(Tavolozza.Magenta, Tavolozza.Oro, new Point(zona.X, 0), new Point(zona.Right, 0)) { MappingMode = BrushMappingMode.Absolute }; + sfum.Freeze(); + dc.DrawLine(Tavolozza.Penna(Colors.White, Math.Max(0.6, zona.Height * 0.012), 0.16), new Point(zona.X, cy), new Point(zona.Right, cy)); + for (var i = 0; i < n; i++) + { + var mezza = minimo + Math.Clamp(onda[i], 0f, 1f) * (massimo - minimo); + var x = x0 + i * (lar + vuoto); + dc.DrawRoundedRectangle(sfum, null, new Rect(x, cy - mezza, Math.Max(1, lar), mezza * 2), lar / 2, lar / 2); + } + } + // ————————————————————————————————————— MODELLO 3D ————————————————————————————————————— public static Quadro? Modello(string percorso, int L) diff --git a/src/DaPConvertitore/Miniature/Tela.cs b/src/DaPConvertitore/Miniature/Tela.cs index c883680..896ca20 100644 --- a/src/DaPConvertitore/Miniature/Tela.cs +++ b/src/DaPConvertitore/Miniature/Tela.cs @@ -111,13 +111,27 @@ public static byte[] Png(Quadro q) return m.ToArray(); } - /// Un'immagine da byte (JPEG, PNG…), già pronta e «congelata». - public static BitmapSource? Immagine(byte[]? dati) + /// + /// Un'immagine da byte (JPEG, PNG…), già pronta e «congelata». Con si rimpicciolisce + /// mentre si legge: le copertine degli album non vanno tenute intere per una miniatura. + /// + public static BitmapSource? Immagine(byte[]? dati, int latoMassimo = 0) { if (dati is not { Length: > 0 }) return null; try { using var m = new MemoryStream(dati); + if (latoMassimo > 0) + { + var bi = new BitmapImage(); + bi.BeginInit(); + bi.StreamSource = m; + bi.CacheOption = BitmapCacheOption.OnLoad; + bi.DecodePixelWidth = latoMassimo; + bi.EndInit(); + bi.Freeze(); + return bi; + } var dec = BitmapDecoder.Create(m, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad); var f = dec.Frames[0]; f.Freeze(); @@ -126,6 +140,17 @@ public static byte[] Png(Quadro q) catch { return null; } } + /// L'immagine riempie il rettangolo (tagliata al centro, come una copertina) con gli angoli tondi. + public static void Copri(DrawingContext dc, BitmapSource img, Rect r, double raggio) + { + var scala = Math.Max(r.Width / img.PixelWidth, r.Height / img.PixelHeight); + double w = img.PixelWidth * scala, h = img.PixelHeight * scala; + var dest = new Rect(r.X + (r.Width - w) / 2, r.Y + (r.Height - h) / 2, w, h); + dc.PushClip(new RectangleGeometry(r, raggio, raggio)); + dc.DrawImage(img, dest); + dc.Pop(); + } + /// Un angolo tondo: il disegno si ritaglia con questo. public static void Ritaglia(DrawingContext dc, Rect r, double raggio) => dc.PushClip(new RectangleGeometry(r, raggio, raggio));