ASO: renderers dark-panorama (iPhone + iPad) para screenshots 1.5.0

frame_panorama_codex.py / _ipad.py: panorámica premium oscura (Impact, marco real,
+22.7% verde, línea continua) con copy localizado nativo en 7 idiomas. Usado para
las screenshots publicadas de la 1.5.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
This commit is contained in:
alexandrev-tibco
2026-07-25 11:47:52 +02:00
parent 8d5f309f21
commit 20a73ea957
4 changed files with 1223 additions and 26 deletions
+24 -13
View File
@@ -92,7 +92,8 @@ def with_shadow(rgba: Image.Image, blur: int, alpha: float, off=(0, 40)) -> Imag
canvas = Image.new("RGBA", (rgba.width + pad * 2, rgba.height + pad * 2), (0, 0, 0, 0))
sh = Image.new("RGBA", rgba.size, (0, 0, 0, 0))
sh.putalpha(rgba.split()[3].point(lambda p: int(p * alpha)))
dark = Image.new("RGBA", rgba.size, (10, 20, 40, 255))
# cool light glow (not a dark drop-shadow) so the device lifts off the dark bg
dark = Image.new("RGBA", rgba.size, (78, 110, 180, 255))
dark.putalpha(sh.split()[3])
canvas.alpha_composite(dark, (pad + off[0], pad + off[1]))
canvas = canvas.filter(ImageFilter.GaussianBlur(blur))
@@ -107,9 +108,9 @@ def _blob(W, H, cx, cy, r, a):
def make_bg(W, H, accent, accent2, n=1):
"""Light, premium background: near-white vertical wash + large soft brand-color
blobs (blue + green) for depth. `n` = number of panels (for even blob spread)."""
top, bottom = (251, 252, 254), (238, 242, 248)
"""Dark premium background: near-black vertical wash (slate-900 -> near black)
+ large soft brand-color glow blobs for subtle depth. `n` = number of panels."""
top, bottom = (15, 23, 42), (2, 6, 23) # #0F172A -> #020617
# fast vertical gradient: build a 1px-wide column then stretch to full width
col = Image.new("RGB", (1, H))
cp = col.load()
@@ -120,11 +121,12 @@ def make_bg(W, H, accent, accent2, n=1):
for i in range(n):
col = accent if i % 2 == 0 else accent2
cx = pw * (i + 0.5)
# low-alpha glows so the dark stays premium, not garish
base = Image.composite(Image.new("RGBA", (W, H), col + (255,)), base,
_blob(W, H, cx, H * 0.20, pw * 0.42, 60).point(lambda p: int(p * 0.55)))
_blob(W, H, cx, H * 0.16, pw * 0.5, 60).point(lambda p: int(p * 0.26)))
col2 = accent2 if i % 2 == 0 else accent
base = Image.composite(Image.new("RGBA", (W, H), col2 + (255,)), base,
_blob(W, H, cx, H * 0.86, pw * 0.5, 45).point(lambda p: int(p * 0.5)))
_blob(W, H, cx, H * 0.90, pw * 0.55, 45).point(lambda p: int(p * 0.22)))
return base
@@ -146,16 +148,23 @@ def draw_headline(canvas, text, cx, top_y, panel_w, color, max_lines=2):
`max_lines` lines within the panel margins (never clips). Returns the y
coordinate just below the headline block."""
d = ImageDraw.Draw(canvas)
maxw = int(panel_w * 0.82)
size = int(panel_w * 0.074)
# keep the block well inside the panel so nothing spills into the neighbour
maxw = int(panel_w * 0.78)
size = int(panel_w * 0.078)
fnt = font(size, text)
lines = wrap(d, text, fnt, maxw)
while size > int(panel_w * 0.042):
floor = int(panel_w * 0.034)
while size > floor:
fnt = font(size, text)
lines = wrap(d, text, fnt, maxw)
if len(lines) <= max_lines and max(d.textlength(ln, font=fnt) for ln in lines) <= maxw:
break
size -= 3
# hard safety: shrink until every line truly fits, even below the floor
while size > 14 and max(d.textlength(ln, font=fnt) for ln in lines) > maxw:
size -= 2
fnt = font(size, text)
lines = wrap(d, text, fnt, maxw)
asc, desc = fnt.getmetrics()
lh = int((asc + desc) * 1.06)
for i, ln in enumerate(lines):
@@ -189,8 +198,8 @@ def main():
accent, accent2 = hexrgb(a.accent), hexrgb(a.accent2)
canvas = make_bg(W * n, H, accent, accent2, n=n)
text_color = (13, 22, 38)
sub_color = (90, 102, 120)
text_color = (248, 250, 252) # near-white on dark
sub_color = (148, 163, 184) # slate-400
dev_w = int(W * a.device_scale)
angles = [-6, 5, -5, 6, -5, 5]
y_off = [0.0, 0.05, 0.0, 0.05, 0.0, 0.05]
@@ -205,13 +214,15 @@ def main():
lines = wrap(d, sub, sf, int(W * 0.78))
asc, desc = sf.getmetrics(); lh = int((asc + desc) * 1.05)
sy = hy + int(H * 0.012)
# positive/stat sublines (▲ ↑ +) pop in green; the rest stay muted
sub_fill = accent2 if sub.strip()[:1] in "▲↑+" else sub_color
for j, ln in enumerate(lines):
w = d.textlength(ln, font=sf)
d.text((panel_cx - w / 2, sy + j * lh), ln, font=sf, fill=sub_color)
d.text((panel_cx - w / 2, sy + j * lh), ln, font=sf, fill=sub_fill)
shot = Image.open(path).convert("RGB")
dev = build_device(shot, dev_w)
dev = with_shadow(dev, blur=int(W * 0.04), alpha=0.34, off=(0, 46))
dev = with_shadow(dev, blur=int(W * 0.05), alpha=0.30, off=(0, 18))
ang = angles[i % len(angles)]
dev = dev.rotate(ang, expand=True, resample=Image.BICUBIC)
# nudge horizontally so adjacent devices overlap across the panel border