Add theme-adaptive screenshots to README and fix manifest icons
Replace old screenshots with new mobile/tablet variants in light and dark mode. README now uses <picture> elements with prefers-color-scheme so screenshots automatically match the viewer's GitHub theme. Split manifest icon purpose field into separate "any" and "maskable" entries per PWA spec. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
"""Generate a showcase image for Oikos with screenshots in a row."""
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageFilter
|
||||
import os
|
||||
|
||||
# --- Config ---
|
||||
SCREENSHOTS_DIR = os.path.join(os.path.dirname(__file__), "screenshots")
|
||||
ASSETS_DIR = os.path.join(os.path.dirname(__file__), "..", "public", "assets")
|
||||
OUTPUT_DIR = os.path.dirname(__file__)
|
||||
|
||||
CANVAS_WIDTH = 2400
|
||||
BG_COLOR = "#F8FAFC" # Light gray background
|
||||
ACCENT_COLOR = "#2563EB" # Blue accent matching the logo
|
||||
TEXT_COLOR = "#1E293B" # Dark text
|
||||
SUBTITLE_COLOR = "#64748B" # Gray subtitle
|
||||
|
||||
SCREENSHOT_ORDER = ["dashboard.png", "tasks.png", "calendar.png", "shopping.png", "meals.png"]
|
||||
SCREENSHOT_HEIGHT = 700 # Target height for each screenshot
|
||||
PADDING = 60 # Padding around edges
|
||||
SCREENSHOT_GAP = 32 # Gap between screenshots
|
||||
CORNER_RADIUS = 24 # Rounded corners for screenshots
|
||||
SHADOW_OFFSET = 8
|
||||
SHADOW_BLUR = 20
|
||||
|
||||
APP_NAME = "Oikos"
|
||||
TAGLINE = "Dein Familienplaner für Zuhause"
|
||||
|
||||
# Font paths (Fedora)
|
||||
FONT_BOLD = "/usr/share/fonts/julietaula-montserrat-fonts/Montserrat-Bold.otf"
|
||||
FONT_REGULAR = "/usr/share/fonts/julietaula-montserrat-fonts/Montserrat-Regular.otf"
|
||||
FONT_FALLBACK_BOLD = "/usr/share/fonts/google-carlito-fonts/Carlito-Bold.ttf"
|
||||
FONT_FALLBACK_REG = "/usr/share/fonts/google-carlito-fonts/Carlito-Regular.ttf"
|
||||
|
||||
|
||||
def load_font(bold=True, size=48):
|
||||
"""Load best available font."""
|
||||
paths = [FONT_BOLD, FONT_FALLBACK_BOLD] if bold else [FONT_REGULAR, FONT_FALLBACK_REG]
|
||||
for path in paths:
|
||||
try:
|
||||
return ImageFont.truetype(path, size)
|
||||
except (OSError, IOError):
|
||||
continue
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
def add_rounded_corners(img, radius):
|
||||
"""Add rounded corners to an image."""
|
||||
mask = Image.new("L", img.size, 0)
|
||||
draw = ImageDraw.Draw(mask)
|
||||
draw.rounded_rectangle([(0, 0), img.size], radius=radius, fill=255)
|
||||
result = img.copy()
|
||||
result.putalpha(mask)
|
||||
return result
|
||||
|
||||
|
||||
def create_shadow(size, radius, offset=8, blur=20):
|
||||
"""Create a drop shadow."""
|
||||
shadow_size = (size[0] + blur * 4, size[1] + blur * 4)
|
||||
shadow = Image.new("RGBA", shadow_size, (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(shadow)
|
||||
x_off = blur * 2
|
||||
y_off = blur * 2 + offset
|
||||
draw.rounded_rectangle(
|
||||
[(x_off, y_off), (x_off + size[0], y_off + size[1])],
|
||||
radius=radius,
|
||||
fill=(0, 0, 0, 50),
|
||||
)
|
||||
shadow = shadow.filter(ImageFilter.GaussianBlur(blur))
|
||||
return shadow
|
||||
|
||||
|
||||
def main():
|
||||
# Load and resize screenshots
|
||||
screenshots = []
|
||||
for name in SCREENSHOT_ORDER:
|
||||
path = os.path.join(SCREENSHOTS_DIR, name)
|
||||
img = Image.open(path).convert("RGBA")
|
||||
ratio = SCREENSHOT_HEIGHT / img.height
|
||||
new_width = int(img.width * ratio)
|
||||
img = img.resize((new_width, SCREENSHOT_HEIGHT), Image.LANCZOS)
|
||||
screenshots.append(img)
|
||||
|
||||
# Calculate layout
|
||||
total_screenshots_width = sum(s.width for s in screenshots) + SCREENSHOT_GAP * (len(screenshots) - 1)
|
||||
|
||||
# Scale screenshots if they don't fit
|
||||
available_width = CANVAS_WIDTH - PADDING * 2
|
||||
if total_screenshots_width > available_width:
|
||||
scale = available_width / total_screenshots_width
|
||||
new_screenshots = []
|
||||
for s in screenshots:
|
||||
new_w = int(s.width * scale)
|
||||
new_h = int(s.height * scale)
|
||||
new_screenshots.append(s.resize((new_w, new_h), Image.LANCZOS))
|
||||
screenshots = new_screenshots
|
||||
total_screenshots_width = sum(s.width for s in screenshots) + SCREENSHOT_GAP * (len(screenshots) - 1)
|
||||
|
||||
screenshot_h = screenshots[0].height
|
||||
|
||||
# Header area: logo + text
|
||||
logo_size = 72
|
||||
title_font = load_font(bold=True, size=52)
|
||||
tagline_font = load_font(bold=False, size=28)
|
||||
|
||||
header_height = max(logo_size, 52 + 28 + 8) # logo or text stack height
|
||||
top_section = PADDING + header_height + 48 # padding + header + gap to screenshots
|
||||
canvas_height = top_section + screenshot_h + PADDING + SHADOW_BLUR * 2
|
||||
|
||||
# Create canvas
|
||||
canvas = Image.new("RGBA", (CANVAS_WIDTH, canvas_height), BG_COLOR)
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
|
||||
# --- Draw subtle gradient accent at top ---
|
||||
for y in range(min(6, canvas_height)):
|
||||
alpha = int(180 * (1 - y / 6))
|
||||
draw.line([(0, y), (CANVAS_WIDTH, y)], fill=(37, 99, 235, alpha))
|
||||
|
||||
# --- Draw logo ---
|
||||
logo_path = os.path.join(ASSETS_DIR, "oikos-icon-1024.png")
|
||||
logo = Image.open(logo_path).convert("RGBA")
|
||||
logo = logo.resize((logo_size, logo_size), Image.LANCZOS)
|
||||
|
||||
# Center header: logo + name + tagline
|
||||
title_bbox = draw.textbbox((0, 0), APP_NAME, font=title_font)
|
||||
title_w = title_bbox[2] - title_bbox[0]
|
||||
tagline_bbox = draw.textbbox((0, 0), TAGLINE, font=tagline_font)
|
||||
tagline_w = tagline_bbox[2] - tagline_bbox[0]
|
||||
|
||||
header_total_w = logo_size + 20 + max(title_w, tagline_w)
|
||||
header_x = (CANVAS_WIDTH - header_total_w) // 2
|
||||
header_y = PADDING
|
||||
|
||||
canvas.paste(logo, (header_x, header_y), logo)
|
||||
|
||||
text_x = header_x + logo_size + 20
|
||||
draw.text((text_x, header_y - 4), APP_NAME, fill=TEXT_COLOR, font=title_font)
|
||||
draw.text((text_x, header_y + 48), TAGLINE, fill=SUBTITLE_COLOR, font=tagline_font)
|
||||
|
||||
# --- Draw screenshots ---
|
||||
start_x = (CANVAS_WIDTH - total_screenshots_width) // 2
|
||||
x = start_x
|
||||
y = top_section
|
||||
|
||||
for s in screenshots:
|
||||
# Shadow
|
||||
shadow = create_shadow(s.size, CORNER_RADIUS, SHADOW_OFFSET, SHADOW_BLUR)
|
||||
sx = x - SHADOW_BLUR * 2
|
||||
sy = y - SHADOW_BLUR * 2
|
||||
canvas.paste(shadow, (sx, sy), shadow)
|
||||
|
||||
# Screenshot with rounded corners
|
||||
rounded = add_rounded_corners(s, CORNER_RADIUS)
|
||||
canvas.paste(rounded, (x, y), rounded)
|
||||
|
||||
x += s.width + SCREENSHOT_GAP
|
||||
|
||||
# Save
|
||||
output_path = os.path.join(OUTPUT_DIR, "showcase.png")
|
||||
final = canvas.convert("RGB")
|
||||
final.save(output_path, "PNG", optimize=True)
|
||||
print(f"Showcase image saved: {output_path}")
|
||||
print(f"Size: {CANVAS_WIDTH}x{canvas_height}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user