diff --git a/public/img/insanity-hour-titolo.png b/public/img/insanity-hour-titolo.png new file mode 100644 index 0000000..8776f6c Binary files /dev/null and b/public/img/insanity-hour-titolo.png differ diff --git a/public/img/locandina-insanity-hour.png b/public/img/locandina-insanity-hour.png new file mode 100644 index 0000000..5bc2e7f Binary files /dev/null and b/public/img/locandina-insanity-hour.png differ diff --git a/public/locandina-insanity-hour.pdf b/public/locandina-insanity-hour.pdf new file mode 100644 index 0000000..cc95752 Binary files /dev/null and b/public/locandina-insanity-hour.pdf differ diff --git a/scripts/locandina-evento.py b/scripts/locandina-evento.py new file mode 100644 index 0000000..bf828bc --- /dev/null +++ b/scripts/locandina-evento.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +"""Genera la locandina dell'In-Sanity Hour in PDF vettoriale, con il QR code vero. + +Perché non si parte da un sorgente grafico: l'unico file disponibile è un JPEG da +612x751 px, che stampato su A4 darebbe 70 dpi. La locandina però è fatta di tre soli +colori piatti, quindi si può ricalcare: ogni forma — testi, calligrafia, bicchieri — +diventa un tracciato vettoriale che si stampa nitido a qualsiasi formato, senza dover +reinstallare i font originali (che non abbiamo) né riprodurli a occhio. + +Del JPEG resta solo la geometria. Il QR di prova viene coperto e ridisegnato con +l'URL vero della pagina evento, anch'esso vettoriale. + +Uso: python3 scripts/locandina-evento.py +Serve: pip install --user potracer qrcode cairosvg +""" +from pathlib import Path + +import cairosvg +import numpy as np +import potrace +import qrcode +from PIL import Image + +URL_EVENTO = 'https://insanitylab.it/promo-eventi/eventi' + +RADICE = Path(__file__).resolve().parent.parent +SORGENTE = RADICE.parent / 'Scaricati' / 'Locandina.jpeg' +USCITA_PDF = RADICE / 'public' / 'locandina-insanity-hour.pdf' +USCITA_PNG = RADICE / 'public' / 'img' / 'locandina-insanity-hour.png' +USCITA_TITOLO = RADICE / 'public' / 'img' / 'insanity-hour-titolo.png' + +# Riquadro stampato dentro il JPEG, senza il bordo bianco dello screenshot. +RIQUADRO = (22, 13, 597, 732) +# Blocco "SAVE THE DATE / IN-SANITY / Hour" usato come immagine nella pagina web. +TITOLO_WEB = (23, 24, 596, 275) +# Quadrato del QR di prova da sostituire, in coordinate del JPEG originale. +QR_PROVA = (387, 632, 475, 720) +# Bordo superiore della fascia beige: la toppa sul QR non deve sconfinare nel marrone. +FASCIA_Y = 627 + +FONDO, BEIGE, CREMA = '#4F3126', '#D0B49E', '#F6F1E9' +CENTRI = [(79, 49, 38), (208, 180, 158), (246, 241, 233)] +UPSCALE = 4 # si traccia sull'immagine ingrandita: bordi piu' morbidi, curve piu' fedeli +MODULI_QUIET = 4 # margine muto richiesto dallo standard QR, in moduli + + +def classifica(immagine: Image.Image) -> np.ndarray: + """Assegna a ogni pixel il piu' vicino dei tre colori piatti della locandina.""" + grande = immagine.resize((immagine.width * UPSCALE, immagine.height * UPSCALE), Image.LANCZOS) + px = np.asarray(grande).astype(np.int16) + distanze = np.stack([((px - np.array(c)) ** 2).sum(axis=2) for c in CENTRI]) + return np.argmin(distanze, axis=0) + + +def traccia(maschera: np.ndarray) -> str: + """Contorni della maschera come path SVG. potracer prende per foreground i pixel + falsi, quindi la maschera va negata.""" + percorso = potrace.Bitmap(~maschera).trace(turdsize=6, alphamax=1.0, opticurve=True, opttolerance=0.2) + d = [] + for curva in percorso.curves: + avvio = curva.start_point + d.append(f'M{avvio.x / UPSCALE:.2f},{avvio.y / UPSCALE:.2f}') + for seg in curva.segments: + fine = seg.end_point + if seg.is_corner: + d.append(f'L{seg.c.x / UPSCALE:.2f},{seg.c.y / UPSCALE:.2f}L{fine.x / UPSCALE:.2f},{fine.y / UPSCALE:.2f}') + else: + d.append( + f'C{seg.c1.x / UPSCALE:.2f},{seg.c1.y / UPSCALE:.2f}' + f' {seg.c2.x / UPSCALE:.2f},{seg.c2.y / UPSCALE:.2f}' + f' {fine.x / UPSCALE:.2f},{fine.y / UPSCALE:.2f}' + ) + d.append('Z') + return ''.join(d) + + +def qr_svg(x0: float, y0: float, x1: float, y1: float) -> str: + """QR dell'URL evento, disegnato a rettangoli dentro il riquadro dato. + + Il QR di prova sfiorava il bordo marrone: qui il modulo viene calcolato includendo + il margine muto, cosi' il codice resta leggibile anche stampato in piccolo. + """ + codice = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_M, box_size=1, border=0) + codice.add_data(URL_EVENTO) + codice.make(fit=True) + matrice = codice.get_matrix() + n = len(matrice) + + lato = min(x1 - x0, y1 - y0) + modulo = lato / (n + 2 * MODULI_QUIET) + off_x = x0 + (x1 - x0 - lato) / 2 + MODULI_QUIET * modulo + off_y = y0 + (y1 - y0 - lato) / 2 + MODULI_QUIET * modulo + + quadrati = [ + f'' + for r, riga in enumerate(matrice) + for c, pieno in enumerate(riga) + if pieno + ] + return f'{"".join(quadrati)}', n + + +def main() -> None: + originale = Image.open(SORGENTE).convert('RGB') + locandina = originale.crop(RIQUADRO) + larghezza, altezza = locandina.size + etichette = classifica(locandina) + + dx, dy = RIQUADRO[0], RIQUADRO[1] + qx0, qy0, qx1, qy1 = QR_PROVA[0] - dx, QR_PROVA[1] - dy, QR_PROVA[2] - dx, QR_PROVA[3] - dy + qr, moduli = qr_svg(qx0, qy0, qx1, qy1) + toppa_y = max(qy0 - 6, FASCIA_Y - dy + 1) + + svg = f''' + + + + +{qr} +''' + + USCITA_PDF.parent.mkdir(parents=True, exist_ok=True) + # Proporzioni native (575x719). cairosvg legge i px a 96 dpi: per una base di 210 mm + # (la larghezza di un A4) la pagina va chiesta in px, non in punti. + px_per_mm = 96 / 25.4 + scala = 210 * px_per_mm / larghezza + cairosvg.svg2pdf(bytestring=svg.encode(), write_to=str(USCITA_PDF), + output_width=larghezza * scala, output_height=altezza * scala) + cairosvg.svg2png(bytestring=svg.encode(), write_to=str(USCITA_PNG), + output_width=larghezza * 3, output_height=altezza * 3) + originale.crop(TITOLO_WEB).save(USCITA_TITOLO) + + print(f'locandina {larghezza}x{altezza} px -> {USCITA_PDF.relative_to(RADICE)} ' + f'({210:.0f}x{210 * altezza / larghezza:.0f} mm)') + print(f'anteprima {larghezza * 3}x{altezza * 3} -> {USCITA_PNG.relative_to(RADICE)}') + print(f'QR {moduli}x{moduli} moduli -> {URL_EVENTO}') + + +if __name__ == '__main__': + main() diff --git a/src/pages/promo-eventi/eventi.astro b/src/pages/promo-eventi/eventi.astro index 74277f6..a0abb89 100644 --- a/src/pages/promo-eventi/eventi.astro +++ b/src/pages/promo-eventi/eventi.astro @@ -1,38 +1,296 @@ --- import Base from '../../layouts/Base.astro'; -import PageHero from '../../components/PageHero.astro'; -import T from '../../components/content/T.astro'; -import { t } from '../../lib/content'; export const prerender = false; -// Gli eventi del centro. Oggi ce n'è uno solo — l'inaugurazione del 19 settembre — e il -// programma della giornata non c'è ancora: il testo lo dichiara invece di lasciare una -// pagina vuota, ed è editabile dal pannello, così il cliente lo completa senza un deploy. -const wa = t('global.whatsapp'); -const waLink = `https://wa.me/39${wa.replace(/\D/g, '')}`; ---- - - +// La landing dell'inaugurazione del 19 settembre. Il blocco del titolo è il ritaglio +// della locandina vera (font e calligrafia originali, non riprodotti): il resto della +// pagina usa la tipografia del sito e i colori campionati dalla locandina stessa. +// L'URL di questa pagina è quello stampato nel QR code, quindi non va cambiato. +const WHATSAPP = '351 753 5977'; +const waLink = `https://wa.me/39${WHATSAPP.replace(/\D/g, '')}`; -
+type Slot = { ora: string; nome?: string }; +type Gruppo = { label: string; slots: Slot[] }; + +const mattino: Gruppo[] = [ + { label: "Corsi Olistici — 20' a slot", slots: [ + { ora: '10:00–10:20', nome: 'Mobility' }, + { ora: '10:30–10:50', nome: 'Yoga' }, + { ora: '11:00–11:20', nome: 'Pilates Mat' }, + { ora: '11:30–11:50', nome: 'Reformer' }, + { ora: '12:00–12:20', nome: 'Reformer' }, + { ora: '12:30–12:50', nome: 'Reformer' }, + ] }, + { label: "Personal Training — 30' a slot", slots: ['10:30', '11:00', '11:30', '12:00', '12:30'].map((ora) => ({ ora })) }, + { label: "Check-up e Consulenze — 30' a slot", slots: ['10:30', '11:00', '11:30', '12:00', '12:30'].map((ora) => ({ ora })) }, +]; + +const pomeriggio: Gruppo[] = [ + { label: "Corsi Olistici — 20' a slot", slots: [ + { ora: '16:00–16:20', nome: 'Reformer' }, + { ora: '16:30–16:50', nome: 'Reformer' }, + { ora: '17:00–17:20', nome: 'Pilates' }, + { ora: '17:30–17:50', nome: 'Mobility' }, + ] }, + { label: "Personal Training — 30' a slot", slots: ['16:00', '16:30', '17:00', '17:30'].map((ora) => ({ ora })) }, + { label: "Check-up e Consulenze — 30' a slot", slots: ['16:00', '16:30', '17:00', '17:30'].map((ora) => ({ ora })) }, +]; +--- + +
+
+ Save the date — In-Sanity Hour +

19

+

Settembre

+

Inaugurazione

+

Un giorno solo per vivere il Lab da vicino.

+

DJ set · Aperitivo

+

Via Pietro di Mattiolo, 5 — Bologna

+ Scarica la locandina +
+
+ +
+
+

+ Una giornata aperta a tutti — clienti e non — per scoprire la nuova struttura e conoscere + il nostro staff. Vivi l'esperienza In-Sanity Lab da vicino: mini class, allenamenti, + consulenze e check up avanzato. +

+

Solo per oggi, tutto gratuito

+

+ L'ingresso e l'aperitivo serale sono liberi, porta chi vuoi. La prenotazione serve solo + per assicurarti il posto nei singoli slot di prova. +

+ Prenotati su WhatsApp +

{WHATSAPP} — rispondiamo entro la giornata

+
+
+ +
-
- - - - {t('eventi.cta.label')} +

Programma della giornata

+

Come si svolge l'In-Sanity Hour

+ +
+

10:00

+

Apertura

+

+ Apertura ufficiale del Lab. Visita libera degli spazi, promozioni dedicate attive + per tutta la giornata. +

-

{t('eventi.torna.label')}

+ +
+

10:00 – 13:00

+

Prova i nostri servizi

+

Primo piano

+ {mattino.map((gruppo) => ( +
+

{gruppo.label}

+
    + {gruppo.slots.map((slot) => ( +
  • + {slot.ora} + {slot.nome && {slot.nome}} +
  • + ))} +
+
+ ))} + Prenota uno slot +

{WHATSAPP} — scrivici per riservare uno slot del mattino

+
+ +
+

13:00 – 16:00

+

Pausa

+
+ +
+

16:00 – 18:00

+

Prova i nostri servizi

+

Primo piano

+ {pomeriggio.map((gruppo) => ( +
+

{gruppo.label}

+
    + {gruppo.slots.map((slot) => ( +
  • + {slot.ora} + {slot.nome && {slot.nome}} +
  • + ))} +
+
+ ))} + Prenota uno slot +

{WHATSAPP} — scrivici per riservare uno slot del pomeriggio

+
+ +
+

18:00 – 22:00

+

Si festeggia!

+

+ DJ set e aperitivo per tutti. Brindisi e la nuova sede finalmente aperta a tutti. + Tra un drink e l'altro, la serata avrà anche i suoi momenti a sorpresa. + Promozioni esclusive valide solo durante l'evento. +

+
+
+
+ +
+
+
+ Qui non ti alleni per somigliare a nessuno,
ti muovi per ricordare chi sei. +
+

Via Pietro di Mattiolo, 5 — 40139, Bologna

+

← Torna a promo ed eventi