diff --git a/assets/generate_logo.py b/assets/generate_logo.py new file mode 100644 index 0000000..faee1dd --- /dev/null +++ b/assets/generate_logo.py @@ -0,0 +1,146 @@ +"""Genera i loghi brand MusicTools partendo dall'icona master. + +Produce: + - logo_mark.png 1024x1024 solo squircle + nota, bg trasparente + - logo_horizontal_dark.png 2400x720 mark + "MusicTools" testo bianco (per bg scuri) + - logo_horizontal_light.png 2400x720 mark + "MusicTools" testo nero (per bg chiari) + - logo_stacked.png 1280x1600 mark sopra + testo sotto, bianco (per bg scuri) + +Lo squircle riuso quello generato da generate_icon.py (assets/icon.png), +cosi' resta coerente con l'icona dell'app. +""" + +from __future__ import annotations + +from pathlib import Path +from PIL import Image, ImageDraw, ImageFilter, ImageFont + +HERE = Path(__file__).resolve().parent +ICON_PATH = HERE / "icon.png" + +ARIAL_BLACK = "/System/Library/Fonts/Supplemental/Arial Black.ttf" +ARIAL_BOLD = "/System/Library/Fonts/Supplemental/Arial Bold.ttf" + +WHITE = (255, 255, 255) +BLACK = (10, 10, 10) + + +def font(size: int, *, black: bool = True) -> ImageFont.FreeTypeFont: + path = ARIAL_BLACK if black else ARIAL_BOLD + try: + return ImageFont.truetype(path, size) + except OSError: + return ImageFont.load_default() + + +def load_mark(target_size: int) -> Image.Image: + """Carica l'icona master e la riscala alla dimensione richiesta. + + L'icona originale ha sfondo trasparente fuori dallo squircle + (vedi generate_icon.py), quindi va bene per overlay su qualunque bg. + """ + if not ICON_PATH.exists(): + raise FileNotFoundError( + f"icon.png non trovata: rilancia prima 'python3 assets/generate_icon.py'" + ) + icon = Image.open(ICON_PATH).convert("RGBA") + if icon.size != (target_size, target_size): + icon = icon.resize((target_size, target_size), Image.LANCZOS) + return icon + + +def measure_text(text: str, f: ImageFont.FreeTypeFont) -> tuple[int, int]: + """Ritorna (larghezza, altezza) effettiva del testo.""" + bbox = f.getbbox(text) + return bbox[2] - bbox[0], bbox[3] - bbox[1] + + +def draw_text_top_left(canvas: Image.Image, text: str, top_left: tuple[int, int], + f: ImageFont.FreeTypeFont, color: tuple) -> None: + """Disegna testo con angolo top-left = top_left (compensa font bbox).""" + draw = ImageDraw.Draw(canvas, "RGBA") + bbox = f.getbbox(text) + x = top_left[0] - bbox[0] + y = top_left[1] - bbox[1] + draw.text((x, y), text, font=f, fill=(*color, 255)) + + +def make_mark(path: Path) -> None: + mark = load_mark(1024) + mark.save(path, "PNG", optimize=True) + print(f"[ok] mark -> {path}") + + +def make_horizontal(path: Path, text_color: tuple) -> None: + """Layout: mark a sinistra + testo. Canvas si autodimensiona al testo.""" + H = 720 + mark_size = int(H * 0.92) + pad_left = 60 + gap = 70 + pad_right = 80 + + text = "MusicTools" + f = font(260, black=True) + tw, th = measure_text(text, f) + + W = pad_left + mark_size + gap + tw + pad_right + + canvas = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + + # Mark a sinistra, centrato verticalmente + mark = load_mark(mark_size) + mark_y = (H - mark_size) // 2 + canvas.alpha_composite(mark, (pad_left, mark_y)) + + # Testo allineato top-left calcolato in modo che la sua baseline + # sia visivamente centrata sull'altezza dello squircle. + text_x = pad_left + mark_size + gap + text_y = (H - th) // 2 + draw_text_top_left(canvas, text, (text_x, text_y), f, text_color) + + canvas.save(path, "PNG", optimize=True) + name = "dark" if text_color == WHITE else "light" + print(f"[ok] horizontal ({name}) -> {path} [{W}x{H}]") + + +def make_stacked(path: Path, text_color: tuple) -> None: + """Mark sopra + 'MusicTools' sotto. Canvas autodimensionato al testo.""" + mark_size = 1024 + f = font(180, black=True) + text = "MusicTools" + tw, th = measure_text(text, f) + + pad = 80 + gap = 50 + W = max(mark_size, tw) + 2 * pad + H = pad + mark_size + gap + th + pad + + canvas = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + + mark = load_mark(mark_size) + mark_x = (W - mark_size) // 2 + canvas.alpha_composite(mark, (mark_x, pad)) + + text_x = (W - tw) // 2 + text_y = pad + mark_size + gap + draw_text_top_left(canvas, text, (text_x, text_y), f, text_color) + + canvas.save(path, "PNG", optimize=True) + name = "dark" if text_color == WHITE else "light" + print(f"[ok] stacked ({name}) -> {path} [{W}x{H}]") + + +def main() -> None: + make_mark(HERE / "logo_mark.png") + make_horizontal(HERE / "logo_horizontal_dark.png", WHITE) # per sfondi scuri + make_horizontal(HERE / "logo_horizontal_light.png", BLACK) # per sfondi chiari + make_stacked(HERE / "logo_stacked_dark.png", WHITE) # per sfondi scuri + make_stacked(HERE / "logo_stacked_light.png", BLACK) # per sfondi chiari + # rimuovo eventuale vecchio file senza suffisso + old = HERE / "logo_stacked.png" + if old.exists(): + old.unlink() + + +if __name__ == "__main__": + main() diff --git a/assets/generate_ls_images.py b/assets/generate_ls_images.py new file mode 100644 index 0000000..6a82924 --- /dev/null +++ b/assets/generate_ls_images.py @@ -0,0 +1,296 @@ +"""Genera le immagini prodotto per Lemon Squeezy. + +Produce: + - ls_thumbnail.png 1024x1024 (thumbnail prodotto + checkout) + - ls_cover.png 1920x1080 (cover/hero per la pagina prodotto) + - ls_og.png 1200x630 (Open Graph per social share) + +La nota musicale e' disegnata geometricamente (no glyph Unicode) +per essere indipendente dai font installati sul sistema. +""" + +from __future__ import annotations + +import math +from pathlib import Path +from PIL import Image, ImageDraw, ImageFilter, ImageFont + +OUT_DIR = Path(__file__).resolve().parent + +GREEN_LIGHT = (29, 185, 84) +GREEN_DARK = (19, 122, 55) +BG_TOP = (10, 50, 26) +BG_BOTTOM = (6, 6, 6) +WHITE = (255, 255, 255) +MUTED = (190, 190, 190) + +ARIAL_BOLD = "/System/Library/Fonts/Supplemental/Arial Bold.ttf" +ARIAL_BLACK = "/System/Library/Fonts/Supplemental/Arial Black.ttf" +ARIAL_REG = "/System/Library/Fonts/Supplemental/Arial.ttf" + + +def font(size: int, *, black: bool = False, bold: bool = True) -> ImageFont.FreeTypeFont: + path = ARIAL_BLACK if black else (ARIAL_BOLD if bold else ARIAL_REG) + try: + return ImageFont.truetype(path, size) + except OSError: + return ImageFont.load_default() + + +def vgradient(size: tuple[int, int], top: tuple, bottom: tuple) -> Image.Image: + """Gradient verticale tra due colori (RGB).""" + w, h = size + img = Image.new("RGB", size, top) + px = img.load() + for y in range(h): + t = y / max(h - 1, 1) + r = int(top[0] + (bottom[0] - top[0]) * t) + g = int(top[1] + (bottom[1] - top[1]) * t) + b = int(top[2] + (bottom[2] - top[2]) * t) + for x in range(w): + px[x, y] = (r, g, b) + return img + + +def radial_glow(size: tuple[int, int], center: tuple[int, int], + radius: int, color: tuple, opacity: int = 90) -> Image.Image: + """Glow radiale soft (RGBA).""" + glow = Image.new("RGBA", size, (0, 0, 0, 0)) + draw = ImageDraw.Draw(glow) + cx, cy = center + draw.ellipse( + [cx - radius, cy - radius, cx + radius, cy + radius], + fill=(*color, opacity), + ) + return glow.filter(ImageFilter.GaussianBlur(radius // 3)) + + +def draw_text_centered(canvas: Image.Image, text: str, center: tuple[int, int], + size_px: int, color=WHITE, *, black: bool = True, + bold: bool = True) -> None: + draw = ImageDraw.Draw(canvas, "RGBA") + f = font(size_px, black=black, bold=bold) + bbox = draw.textbbox((0, 0), text, font=f) + tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1] + x = center[0] - tw // 2 - bbox[0] + y = center[1] - th // 2 - bbox[1] + draw.text((x, y), text, font=f, fill=(*color, 255)) + + +def draw_text_left(canvas: Image.Image, text: str, anchor: tuple[int, int], + size_px: int, color=WHITE, *, black: bool = True, + bold: bool = True) -> None: + """Disegna testo allineato a sinistra (anchor = baseline-left).""" + draw = ImageDraw.Draw(canvas, "RGBA") + f = font(size_px, black=black, bold=bold) + bbox = draw.textbbox(anchor, text, font=f) + draw.text(anchor, text, font=f, fill=(*color, 255)) + + +def draw_pill(canvas: Image.Image, center: tuple[int, int], text: str, + pad_x: int, pad_y: int, font_size: int, + fill=(255, 255, 255, 30), text_color=WHITE, + *, bold: bool = True) -> None: + draw = ImageDraw.Draw(canvas, "RGBA") + f = font(font_size, bold=bold, black=False) + bbox = draw.textbbox((0, 0), text, font=f) + tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1] + pill_w = tw + 2 * pad_x + pill_h = th + 2 * pad_y + x0 = center[0] - pill_w // 2 + y0 = center[1] - pill_h // 2 + draw.rounded_rectangle( + [x0, y0, x0 + pill_w, y0 + pill_h], + radius=pill_h // 2, + fill=fill, + ) + tx = center[0] - tw // 2 - bbox[0] + ty = center[1] - th // 2 - bbox[1] + draw.text((tx, ty), text, font=f, fill=(*text_color, 255)) + + +def draw_music_note(canvas: Image.Image, center: tuple[int, int], + size_px: int, color=WHITE) -> None: + """Disegna una nota musicale stilizzata (due crome connesse). + + Geometria approssimativa: + - 2 teste ellittiche (sinistra e destra) + - 2 aste verticali sottili + - barra orizzontale curva in alto che le connette + Tutto scalato in funzione di size_px (larghezza disegno). + """ + cx, cy = center + W = size_px + head_w = int(W * 0.30) # larghezza testa + head_h = int(head_w * 0.74) # altezza testa (ovale schiacciata) + stem_w = max(int(W * 0.035), 6) + stem_h = int(W * 0.78) + spacing = int(W * 0.40) # distanza tra le 2 aste + + layer = Image.new("RGBA", canvas.size, (0, 0, 0, 0)) + d = ImageDraw.Draw(layer) + + # Posizioni delle 2 aste + left_stem_x = cx - spacing // 2 + right_stem_x = cx + spacing // 2 + + top_y = cy - stem_h // 2 + bot_y = cy + stem_h // 2 + + # Aste (rettangoli verticali) + d.rounded_rectangle( + [left_stem_x - stem_w // 2, top_y, left_stem_x + stem_w // 2, bot_y], + radius=stem_w // 2, fill=color + (255,), + ) + d.rounded_rectangle( + [right_stem_x - stem_w // 2, top_y, right_stem_x + stem_w // 2, bot_y], + radius=stem_w // 2, fill=color + (255,), + ) + + # Barra orizzontale spessa in alto che unisce le aste (ramo) + bar_thick = max(int(W * 0.09), 14) + d.rounded_rectangle( + [left_stem_x - stem_w // 2 - 4, top_y, + right_stem_x + stem_w // 2 + 4, top_y + bar_thick], + radius=bar_thick // 3, fill=color + (255,), + ) + # Seconda barra subito sotto (la nota doppia ha sempre 2 ramificazioni) + second_bar_y = top_y + int(bar_thick * 1.6) + d.rounded_rectangle( + [left_stem_x - stem_w // 2 - 4, second_bar_y, + right_stem_x + stem_w // 2 + 4, second_bar_y + int(bar_thick * 0.7)], + radius=bar_thick // 3, fill=color + (255,), + ) + + # Teste ellittiche: in basso, leggermente a sinistra dell'asta (inclinate) + def head_at(stem_x: int) -> None: + # Centro testa: poco sotto al fondo dell'asta, leggermente a sinistra + hx = stem_x - int(head_w * 0.42) + hy = bot_y - int(head_h * 0.20) + # Disegnata su layer rotato per dare l'inclinazione + tmp = Image.new("RGBA", (head_w + 40, head_h + 40), (0, 0, 0, 0)) + td = ImageDraw.Draw(tmp) + td.ellipse([10, 10, 10 + head_w, 10 + head_h], fill=color + (255,)) + rotated = tmp.rotate(-22, resample=Image.BICUBIC, expand=True) + rw, rh = rotated.size + layer.alpha_composite(rotated, (hx - rw // 2 + head_w // 2, hy - rh // 2 + head_h // 2)) + + head_at(left_stem_x) + head_at(right_stem_x) + + # Glow morbido sotto la nota per profondita' + glow_src = layer.filter(ImageFilter.GaussianBlur(W // 22)) + # Riduco un po' l'alpha del glow + alpha = glow_src.split()[3].point(lambda a: int(a * 0.45)) + glow = Image.merge("RGBA", (*glow_src.split()[:3], alpha)) + canvas.alpha_composite(glow) + canvas.alpha_composite(layer) + + +# ============================================================ +# Thumbnail 1024x1024 +# ============================================================ +def make_thumbnail(path: Path) -> None: + W = H = 1024 + img = vgradient((W, H), BG_TOP, BG_BOTTOM).convert("RGBA") + img.alpha_composite(radial_glow((W, H), (W // 2, H // 2 - 80), 430, GREEN_LIGHT, 100)) + + # Eyebrow in cima (testo semplice, no pill) + draw_text_centered(img, "DESKTOP APP - MAC e WINDOWS", (W // 2, 165), + size_px=30, color=GREEN_LIGHT, black=False, bold=True) + + # Nota geometrica centrale + draw_music_note(img, (W // 2, H // 2 - 40), 360, color=WHITE) + + # Titolo grande + draw_text_centered(img, "MusicTools", (W // 2, H - 280), + size_px=110, color=WHITE, black=True) + + # Tagline + draw_text_centered(img, "Scarica - Modifica - Registra", (W // 2, H - 195), + size_px=38, color=MUTED, black=False, bold=True) + + # Pill prezzo in basso + draw_pill(img, (W // 2, H - 100), "Licenza a vita 39,90 EUR", + pad_x=26, pad_y=14, font_size=30, + fill=GREEN_LIGHT + (255,), text_color=(10, 10, 10), bold=True) + + img.convert("RGB").save(path, "PNG", optimize=True) + print(f"[ok] thumbnail -> {path}") + + +# ============================================================ +# Cover/Hero 1920x1080 +# ============================================================ +def make_cover(path: Path) -> None: + W, H = 1920, 1080 + img = vgradient((W, H), BG_TOP, BG_BOTTOM).convert("RGBA") + # Glow grosso a sinistra (zona del testo) + img.alpha_composite(radial_glow((W, H), (W // 3, H // 2), 640, GREEN_LIGHT, 85)) + # Glow secondario a destra (zona nota) + img.alpha_composite(radial_glow((W, H), (int(W * 0.78), H // 2), 380, GREEN_LIGHT, 55)) + + LEFT_X = 140 + # Eyebrow testo semplice + draw_text_left(img, "DESKTOP APP", (LEFT_X, 220), + size_px=30, color=GREEN_LIGHT, black=False, bold=True) + + # Heading 2 righe (sinistra) + draw_text_left(img, "La tua musica.", (LEFT_X, 310), + size_px=120, color=WHITE, black=True) + draw_text_left(img, "In un'unica app.", (LEFT_X, 460), + size_px=120, color=GREEN_LIGHT, black=True) + + # Sub-tagline + draw_text_left(img, "Scarica da Spotify e YouTube.", (LEFT_X, 640), + size_px=42, color=MUTED, black=False, bold=True) + draw_text_left(img, "Registra audio. Modifica metadati.", (LEFT_X, 700), + size_px=42, color=MUTED, black=False, bold=True) + + # Pill prezzo (sinistra, basso) + draw_pill(img, (LEFT_X + 320, H - 160), "Licenza a vita - 39,90 EUR", + pad_x=30, pad_y=18, font_size=40, + fill=GREEN_LIGHT + (255,), text_color=(10, 10, 10), bold=True) + + # Nota grande a destra + draw_music_note(img, (int(W * 0.78), H // 2 - 20), 540, color=WHITE) + + img.convert("RGB").save(path, "PNG", optimize=True) + print(f"[ok] cover -> {path}") + + +# ============================================================ +# OG image 1200x630 +# ============================================================ +def make_og(path: Path) -> None: + W, H = 1200, 630 + img = vgradient((W, H), BG_TOP, BG_BOTTOM).convert("RGBA") + img.alpha_composite(radial_glow((W, H), (W // 2, H // 2 - 50), 420, GREEN_LIGHT, 95)) + + # Nota in alto centro + draw_music_note(img, (W // 2, 180), 180, color=WHITE) + + # Titolo + draw_text_centered(img, "MusicTools", (W // 2, 360), + size_px=92, color=WHITE, black=True) + # Tagline + draw_text_centered(img, "Scarica - Modifica - Registra", (W // 2, 440), + size_px=32, color=MUTED, black=False, bold=True) + + # Pill prezzo in basso + draw_pill(img, (W // 2, H - 80), "Licenza a vita 39,90 EUR - Mac e Windows", + pad_x=24, pad_y=13, font_size=28, + fill=GREEN_LIGHT + (255,), text_color=(10, 10, 10), bold=True) + + img.convert("RGB").save(path, "PNG", optimize=True) + print(f"[ok] og -> {path}") + + +def main() -> None: + make_thumbnail(OUT_DIR / "ls_thumbnail.png") + make_cover(OUT_DIR / "ls_cover.png") + make_og(OUT_DIR / "ls_og.png") + + +if __name__ == "__main__": + main() diff --git a/assets/logo_horizontal_dark.png b/assets/logo_horizontal_dark.png new file mode 100644 index 0000000..06b6787 Binary files /dev/null and b/assets/logo_horizontal_dark.png differ diff --git a/assets/logo_horizontal_light.png b/assets/logo_horizontal_light.png new file mode 100644 index 0000000..3d2ed00 Binary files /dev/null and b/assets/logo_horizontal_light.png differ diff --git a/assets/logo_mark.png b/assets/logo_mark.png new file mode 100644 index 0000000..4736ec0 Binary files /dev/null and b/assets/logo_mark.png differ diff --git a/assets/logo_stacked_dark.png b/assets/logo_stacked_dark.png new file mode 100644 index 0000000..73c0d50 Binary files /dev/null and b/assets/logo_stacked_dark.png differ diff --git a/assets/logo_stacked_light.png b/assets/logo_stacked_light.png new file mode 100644 index 0000000..57b827b Binary files /dev/null and b/assets/logo_stacked_light.png differ diff --git a/assets/ls_cover.png b/assets/ls_cover.png new file mode 100644 index 0000000..41b7ece Binary files /dev/null and b/assets/ls_cover.png differ diff --git a/assets/ls_og.png b/assets/ls_og.png new file mode 100644 index 0000000..eeb708a Binary files /dev/null and b/assets/ls_og.png differ diff --git a/assets/ls_thumbnail.png b/assets/ls_thumbnail.png new file mode 100644 index 0000000..a196c6b Binary files /dev/null and b/assets/ls_thumbnail.png differ diff --git a/landing/css/style.css b/landing/css/style.css index 466edd7..932b6d5 100644 --- a/landing/css/style.css +++ b/landing/css/style.css @@ -775,3 +775,31 @@ a { color: inherit; text-decoration: none; } } .price-consent a { color: var(--text-muted); text-decoration: underline; } .price-consent a:hover { color: var(--accent); } + +/* ============================================================ + "Hai gia acquistato? Scarica" β€” link discreti + ============================================================ */ +.hero-customer { + margin: 18px 0 0; + font-size: 13.5px; + color: var(--text-dim); +} + +.hero-customer a { + color: var(--accent); + font-weight: 700; + text-decoration: none; +} +.hero-customer a:hover { text-decoration: underline; } + +.price-already { + margin: 10px 0 0 !important; + font-size: 13px !important; + color: var(--text-muted) !important; +} +.price-already a { + color: var(--accent); + font-weight: 700; + text-decoration: none; +} +.price-already a:hover { text-decoration: underline; } diff --git a/landing/download.html b/landing/download.html new file mode 100644 index 0000000..86796b0 --- /dev/null +++ b/landing/download.html @@ -0,0 +1,221 @@ + + + + + + Scarica MusicTools + + + + + + + + +
+
+ SCARICA +

Scarica MusicTools

+

Scegli il sistema operativo e procedi con l'installazione

+
+ + +
+

Inserisci le tue credenziali

+

Hai gia' acquistato? Inserisci l'email e la chiave per scaricare.

+
+ + +
+
+ + +
+ +
+ + + + +
+

Installazione macOS

+
    +
  1. Apri il file .zip scaricato β€” viene estratto in MusicTools.app
  2. +
  3. Trascina MusicTools.app nella cartella Applicazioni
  4. +
  5. Vai in Applicazioni, tasto destro su MusicTools β†’ Apri β†’ Apri (solo la prima volta, e' la procedura standard per app non firmate)
  6. +
  7. Inserisci email e chiave per attivare
  8. +
+
+ +
+

Installazione Windows

+
    +
  1. Estrai il file .zip in una cartella di tua scelta
  2. +
  3. Doppio click su MusicTools.exe
  4. +
  5. Se Windows mostra "Windows ha protetto il PC": click su Ulteriori informazioni β†’ Esegui comunque
  6. +
  7. Inserisci email e chiave per attivare
  8. +
+
+ +
+ Problemi con l'installazione o non hai ricevuto l'email con la chiave?
+ Scrivi a info@djluza.com β€” risposta entro 24 ore. +
+
+ + + + diff --git a/landing/index.html b/landing/index.html index 8130cbb..cfa6f12 100644 --- a/landing/index.html +++ b/landing/index.html @@ -28,8 +28,9 @@ Come funziona Prezzo FAQ + Download - Compra β€” 39,90 € + Compra β€” 39,90 € @@ -45,7 +46,7 @@ Modifica i metadati e le copertine. Tutto offline, senza account.

- Compra ora β€” 39,90 € + Compra ora β€” 39,90 € Scopri di piΓΉ
+ +

+ Hai giΓ  acquistato? Scarica l'app β†’ +

@@ -301,7 +308,7 @@

Pronto a semplificarti la vita?

Una sola app per tutto quello che fai con la musica.

- Compra ora β€” 39,90 € + Compra ora β€” 39,90 €
@@ -314,6 +321,7 @@ by LuZa