feat(extension): générer les assets visuels Chrome Web Store (icon, tiles, screenshots, marquee)
- extension/scripts/generate-store-assets.py : générateur Python/PIL avec la palette x.ai premium (BG #0B0A09, TEXT #F4F1EA, OCHRE #D4A373). - extension/store-assets/ : * icon-128.png (128×128) * promo-tile-small.png (440×280) * promo-tile-large.png (920×680) * marquee.png (1400×560) * screenshot-1.png (1280×800) — sidebar idle avec sélecteur de carnet * screenshot-2.png (1280×800) — confirmation IA avec résumé + tags * screenshot-3.png (1280×800) — vue RTL/persan (font DejaVu pour glyphes) Tous les assets respectent les dimensions Chrome Web Store et sont prêts pour la soumission.
516
memento-note/extension/scripts/generate-store-assets.py
Normal file
@@ -0,0 +1,516 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Génère les assets visuels du Chrome Web Store pour Memento Web Clipper.
|
||||||
|
|
||||||
|
Crée :
|
||||||
|
- icon-128.png (icône Store 128×128)
|
||||||
|
- promo-tile-small.png (440×280)
|
||||||
|
- promo-tile-large.png (920×680)
|
||||||
|
- marquee.png (1400×560)
|
||||||
|
- screenshot-1.png (1280×800) — sidebar idle
|
||||||
|
- screenshot-2.png (1280×800) — confirmation IA
|
||||||
|
- screenshot-3.png (1280×800) — vue RTL/persan
|
||||||
|
|
||||||
|
Usage : python3 generate-store-assets.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from PIL import Image, ImageDraw, ImageFont, ImageFilter
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
|
# Palette & polices (alignées sur la landing x.ai premium)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
BG = (11, 10, 9) # #0B0A09 — fond principal
|
||||||
|
TEXT = (244, 241, 234) # #F4F1EA — texte principal
|
||||||
|
TEXT_DIM = (200, 195, 184) # ~55% opacité
|
||||||
|
TEXT_FAINT = (130, 125, 115) # ~25% opacité
|
||||||
|
OCHRE = (212, 163, 115) # #D4A373 — accent
|
||||||
|
OCHRE_SOFT = (212, 163, 115, 80)
|
||||||
|
CARD = (28, 26, 24) # fond carte élevée
|
||||||
|
CARD_BORDER = (50, 47, 43) # bordure carte
|
||||||
|
WHITE_06 = (255, 255, 255, 16)
|
||||||
|
WHITE_03 = (255, 255, 255, 8)
|
||||||
|
|
||||||
|
FONT_SERIF_BOLD = "/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf"
|
||||||
|
FONT_SERIF_REG = "/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf"
|
||||||
|
FONT_SANS_BOLD = "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf"
|
||||||
|
FONT_SANS_REG = "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf"
|
||||||
|
FONT_DEJAVU = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" # couvre latin + arabe/persan (Vazirmatn fallback)
|
||||||
|
|
||||||
|
OUT_DIR = os.path.join(os.path.dirname(__file__), "..", "store-assets")
|
||||||
|
os.makedirs(OUT_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
|
# Utilitaires
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def load_font(path, size):
|
||||||
|
try:
|
||||||
|
return ImageFont.truetype(path, size)
|
||||||
|
except Exception:
|
||||||
|
return ImageFont.load_default()
|
||||||
|
|
||||||
|
def text_w(draw, txt, font):
|
||||||
|
bbox = draw.textbbox((0, 0), txt, font=font)
|
||||||
|
return bbox[2] - bbox[0]
|
||||||
|
|
||||||
|
def text_h(draw, txt, font):
|
||||||
|
bbox = draw.textbbox((0, 0), txt, font=font)
|
||||||
|
return bbox[3] - bbox[1]
|
||||||
|
|
||||||
|
def rounded_rect(draw, xy, radius, fill=None, outline=None, width=1):
|
||||||
|
draw.rounded_rectangle(xy, radius=radius, fill=fill, outline=outline, width=width)
|
||||||
|
|
||||||
|
def radial_glow(size, center, radius, color, alpha=80):
|
||||||
|
"""Crée un calque de glow radial."""
|
||||||
|
w, h = size
|
||||||
|
glow = Image.new("RGBA", size, (0, 0, 0, 0))
|
||||||
|
cx, cy = center
|
||||||
|
for r in range(radius, 0, -2):
|
||||||
|
a = int(alpha * (1 - r / radius) ** 2)
|
||||||
|
draw = ImageDraw.Draw(glow)
|
||||||
|
draw.ellipse(
|
||||||
|
[cx - r, cy - r, cx + r, cy + r],
|
||||||
|
fill=color + (a,),
|
||||||
|
)
|
||||||
|
return glow
|
||||||
|
|
||||||
|
def base_canvas(w, h, bg=BG):
|
||||||
|
"""Crée un fond avec léger grain + glow ochre en haut-droite."""
|
||||||
|
img = Image.new("RGB", (w, h), bg)
|
||||||
|
overlay = Image.new("RGBA", (w, h), (0, 0, 0, 0))
|
||||||
|
# Glow ochre discret
|
||||||
|
g = radial_glow((w, h), (int(w * 0.85), int(h * 0.15)), max(w, h) // 2, OCHRE, alpha=40)
|
||||||
|
overlay = Image.alpha_composite(overlay, g)
|
||||||
|
img = Image.alpha_composite(img.convert("RGBA"), overlay).convert("RGB")
|
||||||
|
return img
|
||||||
|
|
||||||
|
def draw_logo_m(draw, cx, cy, size, color=TEXT):
|
||||||
|
"""Dessine le logo 'M' dans un carré arrondi (style landing)."""
|
||||||
|
box = size
|
||||||
|
x0, y0 = cx - box // 2, cy - box // 2
|
||||||
|
x1, y1 = cx + box // 2, cy + box // 2
|
||||||
|
radius = int(box * 0.22)
|
||||||
|
rounded_rect(draw, (x0, y0, x1, y1), radius, fill=color)
|
||||||
|
# M text
|
||||||
|
f = load_font(FONT_SERIF_BOLD, int(size * 0.55))
|
||||||
|
t = "M"
|
||||||
|
w = text_w(draw, t, f)
|
||||||
|
h = text_h(draw, t, f)
|
||||||
|
draw.text(
|
||||||
|
(cx - w // 2, cy - h // 2 - int(size * 0.05)),
|
||||||
|
t, font=f, fill=BG,
|
||||||
|
)
|
||||||
|
|
||||||
|
def draw_brand(draw, x, y, logo_size=44, gap=12, color=TEXT):
|
||||||
|
"""Logo + wordmark 'Memento' alignés."""
|
||||||
|
draw_logo_m(draw, x + logo_size // 2, y + logo_size // 2, logo_size, color=color)
|
||||||
|
f = load_font(FONT_SERIF_REG, int(logo_size * 0.55))
|
||||||
|
draw.text((x + logo_size + gap, y + logo_size // 2 - text_h(draw, "Memento", f) // 2 - 2),
|
||||||
|
"Memento", font=f, fill=color)
|
||||||
|
|
||||||
|
def draw_subtle_border(draw, xy, r=10, alpha=16):
|
||||||
|
"""Bordure très subtile (style landing)."""
|
||||||
|
draw.rounded_rectangle(xy, radius=r, outline=(255, 255, 255, alpha), width=1)
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
|
# Générateurs
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def gen_icon_128():
|
||||||
|
"""Icône Store 128×128 — fond carré, gros M centré."""
|
||||||
|
img = Image.new("RGBA", (128, 128), (0, 0, 0, 0))
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
# Fond arrondi
|
||||||
|
rounded_rect(draw, (0, 0, 127, 127), 28, fill=TEXT)
|
||||||
|
# M en serif bold
|
||||||
|
f = load_font(FONT_SERIF_BOLD, 80)
|
||||||
|
t = "M"
|
||||||
|
w = text_w(draw, t, f)
|
||||||
|
h = text_h(draw, t, f)
|
||||||
|
draw.text(((128 - w) // 2, (128 - h) // 2 - 10), t, font=f, fill=BG)
|
||||||
|
# Petite tache ochre en haut à droite
|
||||||
|
draw.ellipse((95, 12, 116, 33), fill=OCHRE)
|
||||||
|
return img.convert("RGB")
|
||||||
|
|
||||||
|
|
||||||
|
def gen_promo_tile(w, h, filename):
|
||||||
|
"""Petite ou grande promo tile."""
|
||||||
|
img = base_canvas(w, h)
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
# Marges
|
||||||
|
pad = max(28, int(w * 0.07))
|
||||||
|
inner_w = w - 2 * pad
|
||||||
|
|
||||||
|
# Logo + wordmark en haut
|
||||||
|
draw_brand(draw, pad, pad, logo_size=int(w * 0.11))
|
||||||
|
|
||||||
|
# Tagline en dessous
|
||||||
|
f_eyebrow = load_font(FONT_SANS_BOLD, int(w * 0.028))
|
||||||
|
f_headline = load_font(FONT_SERIF_BOLD, int(w * 0.075))
|
||||||
|
f_sub = load_font(FONT_SANS_REG, int(w * 0.030))
|
||||||
|
|
||||||
|
y = pad + int(w * 0.11) + int(w * 0.04)
|
||||||
|
eyebrow = "WEB CLIPPER"
|
||||||
|
draw.text((pad, y), eyebrow, font=f_eyebrow, fill=OCHRE)
|
||||||
|
y += text_h(draw, eyebrow, f_eyebrow) + int(w * 0.02)
|
||||||
|
|
||||||
|
headline = "Save anything\nto your Second Brain."
|
||||||
|
# Wrap simple sur \n
|
||||||
|
for line in headline.split("\n"):
|
||||||
|
draw.text((pad, y), line, font=f_headline, fill=TEXT)
|
||||||
|
y += text_h(draw, line, f_headline) + int(w * 0.008)
|
||||||
|
|
||||||
|
y += int(w * 0.015)
|
||||||
|
sub = "AI-powered summary · 15 languages · RTL ready"
|
||||||
|
draw.text((pad, y), sub, font=f_sub, fill=TEXT_DIM)
|
||||||
|
|
||||||
|
# Decorative dots/brand mark bas droite
|
||||||
|
cx = w - pad - int(w * 0.04)
|
||||||
|
cy = h - pad - int(w * 0.04)
|
||||||
|
r = int(w * 0.035)
|
||||||
|
for i, alpha in enumerate([180, 120, 60]):
|
||||||
|
x0 = cx - r + i * int(w * 0.018)
|
||||||
|
y0 = cy - r // 2
|
||||||
|
draw.ellipse((x0, y0, x0 + int(w * 0.012), y0 + int(w * 0.012)), fill=OCHRE)
|
||||||
|
|
||||||
|
img.save(os.path.join(OUT_DIR, filename), "PNG", optimize=True)
|
||||||
|
|
||||||
|
|
||||||
|
def gen_marquee():
|
||||||
|
"""Marquee 1400×560 — bandeau hero très large."""
|
||||||
|
w, h = 1400, 560
|
||||||
|
img = base_canvas(w, h)
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
pad_x = 80
|
||||||
|
# Logo + wordmark
|
||||||
|
draw_brand(draw, pad_x, 60, logo_size=72)
|
||||||
|
|
||||||
|
# Headline très large
|
||||||
|
f_eyebrow = load_font(FONT_SANS_BOLD, 18)
|
||||||
|
f_h1 = load_font(FONT_SERIF_BOLD, 92)
|
||||||
|
f_h2 = load_font(FONT_SERIF_REG, 56)
|
||||||
|
f_sub = load_font(FONT_SANS_REG, 22)
|
||||||
|
|
||||||
|
y = 200
|
||||||
|
draw.text((pad_x, y), "WEB CLIPPER", font=f_eyebrow, fill=OCHRE)
|
||||||
|
y += 36
|
||||||
|
draw.text((pad_x, y), "Your Second Brain,", font=f_h1, fill=TEXT)
|
||||||
|
y += 100
|
||||||
|
draw.text((pad_x, y), "one click away.", font=f_h2, fill=TEXT_DIM)
|
||||||
|
y += 80
|
||||||
|
draw.text((pad_x, y), "Capture any web page or selection · AI summary · 15 languages", font=f_sub, fill=TEXT_DIM)
|
||||||
|
|
||||||
|
# Decorative arc + dots côté droit
|
||||||
|
cx = 1100
|
||||||
|
cy = h // 2
|
||||||
|
for r in [200, 140, 90]:
|
||||||
|
draw.ellipse((cx - r, cy - r, cx + r, cy + r), outline=(255, 255, 255, 22), width=1)
|
||||||
|
draw.ellipse((cx - 50, cy - 50, cx + 50, cy + 50), fill=OCHRE)
|
||||||
|
|
||||||
|
img.save(os.path.join(OUT_DIR, "marquee.png"), "PNG", optimize=True)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_browser_chrome(draw, x, y, w, h):
|
||||||
|
"""Dessine un cadre de browser factice (toolbar + onglet)."""
|
||||||
|
# Fond fenêtre
|
||||||
|
rounded_rect(draw, (x, y, x + w, y + h), 12, fill=CARD, outline=CARD_BORDER, width=1)
|
||||||
|
# Toolbar
|
||||||
|
tb_h = 44
|
||||||
|
draw.rectangle((x, y, x + w, y + tb_h), fill=(20, 18, 16))
|
||||||
|
# Trafic lights
|
||||||
|
for i, c in enumerate([(255, 95, 86), (255, 189, 46), (39, 201, 63)]):
|
||||||
|
cx = x + 18 + i * 22
|
||||||
|
cy = y + tb_h // 2
|
||||||
|
draw.ellipse((cx - 6, cy - 6, cx + 6, cy + 6), fill=c)
|
||||||
|
# URL bar
|
||||||
|
bar_x = x + 100
|
||||||
|
bar_y = y + 12
|
||||||
|
bar_w = w - 220
|
||||||
|
bar_h = 22
|
||||||
|
rounded_rect(draw, (bar_x, bar_y, bar_x + bar_w, bar_y + bar_h), 11,
|
||||||
|
fill=(40, 36, 32))
|
||||||
|
# Texte URL simulé
|
||||||
|
f = load_font(FONT_SANS_REG, 11)
|
||||||
|
draw.text((bar_x + 12, bar_y + 4), " memento-note.com/notebooks/research", font=f, fill=TEXT_DIM)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_side_panel(draw, x, y, w, h, state="idle"):
|
||||||
|
"""Panneau latéral mockup. state: idle | confirm | rtl."""
|
||||||
|
rounded_rect(draw, (x, y, x + w, y + h), 8, fill=(20, 18, 16), outline=CARD_BORDER, width=1)
|
||||||
|
|
||||||
|
# Header (brand + settings)
|
||||||
|
header_h = 56
|
||||||
|
draw.rectangle((x, y, x + w, y + header_h), fill=(14, 13, 12))
|
||||||
|
draw.line((x, y + header_h, x + w, y + header_h), fill=CARD_BORDER, width=1)
|
||||||
|
# M logo mini
|
||||||
|
logo_size = 26
|
||||||
|
draw_logo_m(draw, x + 20 + logo_size // 2, y + header_h // 2, logo_size)
|
||||||
|
# "Memento" + "Web Clipper"
|
||||||
|
f_brand = load_font(FONT_SERIF_BOLD, 14)
|
||||||
|
f_sub_brand = load_font(FONT_SANS_REG, 9)
|
||||||
|
bx = x + 20 + logo_size + 8
|
||||||
|
draw.text((bx, y + 14), "Memento", font=f_brand, fill=TEXT)
|
||||||
|
draw.text((bx, y + 32), "Web Clipper", font=f_sub_brand, fill=TEXT_FAINT)
|
||||||
|
|
||||||
|
# Content area
|
||||||
|
cy = y + header_h + 16
|
||||||
|
cx = x + 16
|
||||||
|
cw = w - 32
|
||||||
|
|
||||||
|
if state == "confirm":
|
||||||
|
draw_confirm_content(draw, cx, cy, cw)
|
||||||
|
elif state == "rtl":
|
||||||
|
draw_rtl_content(draw, cx, cy, cw)
|
||||||
|
else:
|
||||||
|
draw_idle_content(draw, cx, cy, cw)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_idle_content(draw, cx, cy, cw):
|
||||||
|
"""État idle : carnet + page card + sélection hint + boutons."""
|
||||||
|
# Label "Carnet de destination"
|
||||||
|
f_label = load_font(FONT_SANS_BOLD, 9)
|
||||||
|
draw.text((cx, cy), "CARNET DE DESTINATION", font=f_label, fill=TEXT_FAINT)
|
||||||
|
cy += 18
|
||||||
|
|
||||||
|
# Select field
|
||||||
|
f_select = load_font(FONT_SANS_REG, 12)
|
||||||
|
rounded_rect(draw, (cx, cy, cx + cw, cy + 32), 6, fill=(30, 27, 25), outline=CARD_BORDER, width=1)
|
||||||
|
draw.text((cx + 10, cy + 9), "📁 Research Notes", font=f_select, fill=TEXT)
|
||||||
|
cy += 48
|
||||||
|
|
||||||
|
# Page card
|
||||||
|
rounded_rect(draw, (cx, cy, cx + cw, cy + 56), 6, fill=(26, 23, 21), outline=CARD_BORDER, width=1)
|
||||||
|
f_sub = load_font(FONT_SANS_REG, 8)
|
||||||
|
draw.text((cx + 10, cy + 8), "ACTIVE PAGE", font=f_sub, fill=TEXT_FAINT)
|
||||||
|
# Favicon placeholder + title + url
|
||||||
|
draw.rectangle((cx + 10, cy + 22, cx + 26, cy + 38), fill=OCHRE)
|
||||||
|
f_title = load_font(FONT_SANS_BOLD, 11)
|
||||||
|
f_url = load_font(FONT_SANS_REG, 9)
|
||||||
|
draw.text((cx + 34, cy + 22), "Building a Second Brain", font=f_title, fill=TEXT)
|
||||||
|
draw.text((cx + 34, cy + 38), " memento-note.com/blog/...", font=f_url, fill=TEXT_DIM)
|
||||||
|
cy += 72
|
||||||
|
|
||||||
|
# Selection hint
|
||||||
|
rounded_rect(draw, (cx, cy, cx + cw, cy + 36), 6, fill=(26, 23, 21), outline=CARD_BORDER, width=1)
|
||||||
|
f_hint = load_font(FONT_SANS_REG, 10)
|
||||||
|
draw.text((cx + 10, cy + 12), "Astuce : surlignez du texte pour", font=f_hint, fill=TEXT_DIM)
|
||||||
|
draw.text((cx + 10, cy + 24), "clipper une sélection précise.", font=f_hint, fill=TEXT_DIM)
|
||||||
|
cy += 48
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
btn_w = (cw - 8) // 2
|
||||||
|
# Primary "Clipper cette page"
|
||||||
|
rounded_rect(draw, (cx, cy, cx + cw, cy + 32), 6, fill=OCHRE)
|
||||||
|
f_btn = load_font(FONT_SANS_BOLD, 10)
|
||||||
|
text = "Clipper cette page"
|
||||||
|
w = text_w(draw, text, f_btn)
|
||||||
|
draw.text((cx + (cw - w) // 2, cy + 10), text, font=f_btn, fill=BG)
|
||||||
|
cy += 40
|
||||||
|
|
||||||
|
# Secondary "Enregistrer le lien"
|
||||||
|
rounded_rect(draw, (cx, cy, cx + cw, cy + 26), 6, fill=(30, 27, 25), outline=CARD_BORDER, width=1)
|
||||||
|
text = "Enregistrer le lien uniquement"
|
||||||
|
w = text_w(draw, text, f_btn)
|
||||||
|
draw.text((cx + (cw - w) // 2, cy + 8), text, font=f_btn, fill=TEXT_DIM)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_confirm_content(draw, cx, cy, cw):
|
||||||
|
"""État confirmation : titre éditable, résumé, tags, save."""
|
||||||
|
f_label = load_font(FONT_SANS_BOLD, 9)
|
||||||
|
draw.text((cx, cy), "VÉRIFIER AVANT D'ENREGISTRER", font=f_label, fill=TEXT_FAINT)
|
||||||
|
cy += 18
|
||||||
|
|
||||||
|
# Title field
|
||||||
|
rounded_rect(draw, (cx, cy, cx + cw, cy + 32), 6, fill=(30, 27, 25), outline=CARD_BORDER, width=1)
|
||||||
|
f_title = load_font(FONT_SANS_BOLD, 12)
|
||||||
|
draw.text((cx + 10, cy + 9), "Building a Second Brain", font=f_title, fill=TEXT)
|
||||||
|
cy += 42
|
||||||
|
|
||||||
|
# Reading time
|
||||||
|
f_meta = load_font(FONT_SANS_REG, 9)
|
||||||
|
draw.text((cx, cy), "≈ 4 minutes de lecture", font=f_meta, fill=TEXT_FAINT)
|
||||||
|
cy += 22
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
f_summary = load_font(FONT_SANS_REG, 10)
|
||||||
|
summary_lines = [
|
||||||
|
"Capturez des pages web et du texte",
|
||||||
|
"surligné dans vos carnets Memento.",
|
||||||
|
"L'IA extrait le résumé, les tags et",
|
||||||
|
"les embeddings sémantiques.",
|
||||||
|
]
|
||||||
|
for line in summary_lines:
|
||||||
|
draw.text((cx, cy), line, font=f_summary, fill=TEXT)
|
||||||
|
cy += 16
|
||||||
|
cy += 8
|
||||||
|
|
||||||
|
# Tags chips
|
||||||
|
f_tag = load_font(FONT_SANS_BOLD, 9)
|
||||||
|
tags = ["notes", "productivity", "ai"]
|
||||||
|
x = cx
|
||||||
|
for t in tags:
|
||||||
|
tw = text_w(draw, t, f_tag) + 14
|
||||||
|
rounded_rect(draw, (x, cy, x + tw, cy + 18), 9, fill=(30, 27, 25), outline=CARD_BORDER, width=1)
|
||||||
|
draw.text((x + 7, cy + 4), t, font=f_tag, fill=TEXT)
|
||||||
|
x += tw + 6
|
||||||
|
cy += 30
|
||||||
|
|
||||||
|
# Save button (primary)
|
||||||
|
btn_h = 36
|
||||||
|
rounded_rect(draw, (cx, cy, cx + cw, cy + btn_h), 6, fill=OCHRE)
|
||||||
|
f_btn = load_font(FONT_SANS_BOLD, 12)
|
||||||
|
text = "Enregistrer dans Memento"
|
||||||
|
w = text_w(draw, text, f_btn)
|
||||||
|
draw.text((cx + (cw - w) // 2, cy + 11), text, font=f_btn, fill=BG)
|
||||||
|
cy += btn_h + 8
|
||||||
|
|
||||||
|
# Back link
|
||||||
|
f_link = load_font(FONT_SANS_REG, 10)
|
||||||
|
text = "← Retour"
|
||||||
|
w = text_w(draw, text, f_link)
|
||||||
|
draw.text((cx + (cw - w) // 2, cy), text, font=f_link, fill=TEXT_DIM)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_rtl_content(draw, cx, cy, cw):
|
||||||
|
"""État RTL : panneau en mode persan."""
|
||||||
|
# Label en persan (gauche visuelle, mais le text est RTL)
|
||||||
|
f_label = load_font(FONT_DEJAVU, 9)
|
||||||
|
draw.text((cx, cy), "دفتر مقصد", font=f_label, fill=TEXT_FAINT)
|
||||||
|
cy += 22
|
||||||
|
|
||||||
|
# Select field
|
||||||
|
f_select = load_font(FONT_DEJAVU, 12)
|
||||||
|
rounded_rect(draw, (cx, cy, cx + cw, cy + 32), 6, fill=(30, 27, 25), outline=CARD_BORDER, width=1)
|
||||||
|
draw.text((cx + 10, cy + 9), "یادداشتهای پژوهشی", font=f_select, fill=TEXT)
|
||||||
|
cy += 48
|
||||||
|
|
||||||
|
# Page card
|
||||||
|
rounded_rect(draw, (cx, cy, cx + cw, cy + 56), 6, fill=(26, 23, 21), outline=CARD_BORDER, width=1)
|
||||||
|
f_sub = load_font(FONT_DEJAVU, 8)
|
||||||
|
draw.text((cx + 10, cy + 8), "صفحه فعال", font=f_sub, fill=TEXT_FAINT)
|
||||||
|
draw.rectangle((cx + cw - 26, cy + 22, cx + cw - 10, cy + 38), fill=OCHRE)
|
||||||
|
f_title = load_font(FONT_DEJAVU, 11)
|
||||||
|
f_url = load_font(FONT_SANS_REG, 9)
|
||||||
|
draw.text((cx + 10, cy + 22), "ساختن مغز دوم", font=f_title, fill=TEXT)
|
||||||
|
draw.text((cx + 10, cy + 38), "bbc.com/persian/...", font=f_url, fill=TEXT_DIM)
|
||||||
|
cy += 72
|
||||||
|
|
||||||
|
# Selection hint RTL
|
||||||
|
rounded_rect(draw, (cx, cy, cx + cw, cy + 36), 6, fill=(26, 23, 21), outline=CARD_BORDER, width=1)
|
||||||
|
f_hint = load_font(FONT_DEJAVU, 10)
|
||||||
|
draw.text((cx + 10, cy + 12), "نکته: متن را انتخاب کنید", font=f_hint, fill=TEXT_DIM)
|
||||||
|
cy += 48
|
||||||
|
|
||||||
|
# Buttons (right-aligned for RTL feel)
|
||||||
|
btn_w = cw
|
||||||
|
rounded_rect(draw, (cx, cy, cx + btn_w, cy + 32), 6, fill=OCHRE)
|
||||||
|
f_btn = load_font(FONT_DEJAVU, 10)
|
||||||
|
text = "این صفحه را ذخیره کنید"
|
||||||
|
w = text_w(draw, text, f_btn)
|
||||||
|
draw.text((cx + (btn_w - w) // 2, cy + 10), text, font=f_btn, fill=BG)
|
||||||
|
cy += 40
|
||||||
|
|
||||||
|
rounded_rect(draw, (cx, cy, cx + btn_w, cy + 26), 6, fill=(30, 27, 25), outline=CARD_BORDER, width=1)
|
||||||
|
text = "فقط ذخیره پیوند"
|
||||||
|
w = text_w(draw, text, f_btn)
|
||||||
|
draw.text((cx + (btn_w - w) // 2, cy + 8), text, font=f_btn, fill=TEXT_DIM)
|
||||||
|
|
||||||
|
|
||||||
|
def gen_screenshot_panel_only(state, filename):
|
||||||
|
"""Screenshot 1280×800 d'un mock browser avec le side panel."""
|
||||||
|
w, h = 1280, 800
|
||||||
|
img = base_canvas(w, h)
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
# Browser chrome
|
||||||
|
bx, by = 40, 40
|
||||||
|
bw, bh = w - 80, h - 80
|
||||||
|
draw_browser_chrome(draw, bx, by, bw, bh)
|
||||||
|
|
||||||
|
# Page content area (mock article)
|
||||||
|
pg_x = bx + 12
|
||||||
|
pg_y = by + 56
|
||||||
|
pg_w = bw - 24 - 360 # laisse la place au panel
|
||||||
|
# Article mock
|
||||||
|
f_article_title = load_font(FONT_SERIF_BOLD, 28)
|
||||||
|
f_article_meta = load_font(FONT_SANS_REG, 11)
|
||||||
|
f_article_p = load_font(FONT_SANS_REG, 12)
|
||||||
|
f_article_h2 = load_font(FONT_SERIF_BOLD, 16)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
draw.text((pg_x + 40, pg_y + 40), "Building a Second Brain", font=f_article_title, fill=TEXT)
|
||||||
|
# Meta
|
||||||
|
draw.text((pg_x + 40, pg_y + 80), "by Tiago Forte · 8 min read · Memento Blog", font=f_article_meta, fill=TEXT_DIM)
|
||||||
|
# Divider
|
||||||
|
draw.line((pg_x + 40, pg_y + 105, pg_x + pg_w - 40, pg_y + 105), fill=CARD_BORDER, width=1)
|
||||||
|
|
||||||
|
# H2
|
||||||
|
draw.text((pg_x + 40, pg_y + 130), "Why capture matters", font=f_article_h2, fill=TEXT)
|
||||||
|
# Body paragraphs (lignes simulées)
|
||||||
|
y = pg_y + 160
|
||||||
|
paragraphs = [
|
||||||
|
"Modern knowledge workers are overwhelmed by information.",
|
||||||
|
"Notes are not the goal — connections are. A Second Brain",
|
||||||
|
"is not about collecting more; it's about retrieving the right",
|
||||||
|
"piece at the right moment. Capture, organize, distill, express.",
|
||||||
|
"",
|
||||||
|
"When you highlight a paragraph and clip it to Memento,",
|
||||||
|
"the AI extracts the gist, suggests tags, and links it to",
|
||||||
|
"related notes you have written months ago. Memory Echo",
|
||||||
|
"surfaces connections you would have missed otherwise.",
|
||||||
|
]
|
||||||
|
for p in paragraphs:
|
||||||
|
draw.text((pg_x + 40, y), p, font=f_article_p, fill=TEXT_DIM)
|
||||||
|
y += 22
|
||||||
|
|
||||||
|
# Side panel (à droite)
|
||||||
|
panel_w = 340
|
||||||
|
panel_h = bh - 56
|
||||||
|
panel_x = bx + bw - panel_w - 12
|
||||||
|
panel_y = by + 56
|
||||||
|
draw_side_panel(draw, panel_x, panel_y, panel_w, panel_h, state=state)
|
||||||
|
|
||||||
|
# Caption bas
|
||||||
|
img.save(os.path.join(OUT_DIR, filename), "PNG", optimize=True)
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
|
# Main
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"📁 Output : {os.path.abspath(OUT_DIR)}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print(" • icon-128.png")
|
||||||
|
gen_icon_128().save(os.path.join(OUT_DIR, "icon-128.png"), "PNG", optimize=True)
|
||||||
|
|
||||||
|
print(" • promo-tile-small.png (440×280)")
|
||||||
|
gen_promo_tile(440, 280, "promo-tile-small.png")
|
||||||
|
|
||||||
|
print(" • promo-tile-large.png (920×680)")
|
||||||
|
gen_promo_tile(920, 680, "promo-tile-large.png")
|
||||||
|
|
||||||
|
print(" • marquee.png (1400×560)")
|
||||||
|
gen_marquee()
|
||||||
|
|
||||||
|
print(" • screenshot-1.png — sidebar idle")
|
||||||
|
gen_screenshot_panel_only("idle", "screenshot-1.png")
|
||||||
|
|
||||||
|
print(" • screenshot-2.png — confirmation IA")
|
||||||
|
gen_screenshot_panel_only("confirm", "screenshot-2.png")
|
||||||
|
|
||||||
|
print(" • screenshot-3.png — RTL/persan")
|
||||||
|
gen_screenshot_panel_only("rtl", "screenshot-3.png")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("✅ Assets générés.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
BIN
memento-note/extension/store-assets/icon-128.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
memento-note/extension/store-assets/marquee.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
memento-note/extension/store-assets/promo-tile-large.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
memento-note/extension/store-assets/promo-tile-small.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
memento-note/extension/store-assets/screenshot-1.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
memento-note/extension/store-assets/screenshot-2.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
memento-note/extension/store-assets/screenshot-3.png
Normal file
|
After Width: | Height: | Size: 49 KiB |