← Files AgentBrokoARCHIVED FILE
src/video_forge/scenes/screenshot.py
3.4 KB · Sep 30, 2026 · 23:15 UTC
"""screenshot — a supplied image presented in a rounded device frame with a slow
Ken Burns push. Optional caption line beneath. Falls back to a placeholder card
if the image is missing or unreadable, so a spec never hard-fails on a bad path.
scene fields: image (path), caption (optional), fit ("contain"|"cover", default
"cover"), zoom (float end-scale, default 1.06)."""
import os
from PIL import Image
from ..easing import ease_out, lerp, ramp, smoothstep
from ._common import enter, new_canvas
def _load(path):
if not path or not os.path.exists(path):
return None
try:
im = Image.open(path)
im.load()
return im.convert("RGB")
except (OSError, ValueError):
return None
def render(ctx):
cv = new_canvas(ctx)
W, H = cv.W, cv.H
p = ctx.p
caption = ctx.scene.get("caption", "")
# frame geometry (design units)
margin_x = 260.0
top = 150.0
fw = W - margin_x * 2
fh = (H - top * 2) - (90 if caption else 0)
fx, fy = (W - fw) / 2, top
r = 26.0
ke = enter(p, 0.0, 0.5)
dy = (1.0 - ke) * 26.0
fy += dy
im = _load(ctx.scene.get("image"))
if im is None:
# graceful placeholder
cv.card(fx, fy, fw, fh, r=r, fill=cv.pal["card2"], alpha=ke, elevate=30)
cv.text_center(W / 2, fy + fh / 2, ctx.scene.get("image", "screenshot"),
"medium", 34, cv.pal["faint"], alpha=ke)
if caption:
cv.text_center(W / 2, fy + fh + 52, caption, "regular", 30,
cv.pal["muted"], alpha=ke)
return cv.finish()
# Ken Burns: slow push-in over the hold.
zoom_end = float(ctx.scene.get("zoom", 1.06))
z = lerp(1.0, zoom_end, smoothstep(p))
fit = ctx.scene.get("fit", "cover")
# target pixel box inside the frame
pfw, pfh = cv.s(fw), cv.s(fh)
iw, ih = im.size
scale_contain = min(pfw / iw, pfh / ih)
scale_cover = max(pfw / iw, pfh / ih)
base = scale_cover if fit == "cover" else scale_contain
scale = base * z
nw, nh = max(1, int(iw * scale)), max(1, int(ih * scale))
im2 = im.resize((nw, nh), Image.Resampling.LANCZOS)
# center-crop / center the scaled image to the frame box, with a gentle pan
pan = int((z - 1.0) * pfw * 0.10)
left = (nw - pfw) // 2 + pan
upper = (nh - pfh) // 2
left = max(0, min(left, max(0, nw - pfw)))
upper = max(0, min(upper, max(0, nh - pfh)))
if nw >= pfw and nh >= pfh:
crop = im2.crop((left, upper, left + pfw, upper + pfh))
else:
crop = Image.new("RGB", (pfw, pfh), tuple(cv.pal["card2"][:3]))
crop.paste(im2, ((pfw - nw) // 2, (pfh - nh) // 2))
# rounded-corner mask
mask = Image.new("L", (pfw, pfh), 0)
from PIL import ImageDraw
ImageDraw.Draw(mask).rounded_rectangle([0, 0, pfw - 1, pfh - 1],
radius=cv.s(r), fill=255)
cv.drop_shadow(fx, fy, fw, fh, r, spread=34,
alpha=(0.14 if cv.act == "light" else 0.5) * ke, dy=14)
if ke < 0.999:
mask = mask.point(lambda v: int(v * ease_out(ke)))
cv.img.paste(crop, (cv.s(fx), cv.s(fy)), mask)
# thin containing stroke
cv.rrect(fx, fy, fw, fh, r, outline=cv.pal["stroke"], width=1.2, alpha=ke)
if caption:
kc = ease_out(ramp(p, 0.25, 0.7))
cv.text_center(W / 2, fy + fh + 52, caption, "regular", 30,
cv.pal["muted"], alpha=kc)
return cv.finish()
SHA-256: e9b173bbbb482ac26605e56ad30728738ef1fa9039139bdebe797f911c9a959d