← Files AgentBrokoARCHIVED FILE
src/video_forge/scenes/feature_grid.py
2.95 KB · Sep 30, 2026 · 23:15 UTC
"""feature_grid — a set of labelled cards laid out in a responsive grid, each with
a numbered accent badge over a soft accent-tinted surface. Good for "what you get"
beats, and for pipelines where the numbering implies order.
scene fields: items: ["Title" | {"title":.., "desc":..}], optional columns (int).
"""
from ..theme import accent_family
from ._common import (bg_wash, enter, is_portrait, new_canvas, rise_dy,
side_margin)
def _grid(n, forced=None):
if forced:
return forced
if n <= 3:
return n
if n == 4:
return 2
return 3
def _on(color):
"""Ink that stays legible on a filled accent chip."""
lum = (0.299 * color[0] + 0.587 * color[1] + 0.114 * color[2]) / 255.0
return (20, 20, 20) if lum > 0.62 else (255, 255, 255)
def render(ctx):
cv = new_canvas(ctx)
W, H = cv.W, cv.H
p = ctx.p
bg_wash(cv, p)
items = ctx.scene.get("items") or []
n = max(1, len(items))
cols = _grid(n, ctx.scene.get("columns"))
rows = (n + cols - 1) // cols
gap = 30.0
margin = side_margin(cv)
cw = (W - margin * 2 - gap * (cols - 1)) / cols
# Portrait frames have height to spare: let the cards breathe rather than
# float as small squares in a tall, empty frame.
portrait = is_portrait(cv)
cap = 420.0 if portrait else 300.0
avail_h = H * (0.56 if portrait else 0.72)
ch = max(150.0, min(cap, (avail_h - gap * (rows - 1)) / rows))
grid_w = cw * cols + gap * (cols - 1)
grid_h = ch * rows + gap * (rows - 1)
x0 = (W - grid_w) / 2
y0 = (H - grid_h) / 2
fam = accent_family(cv.accent, n)
for i, it in enumerate(items):
if isinstance(it, dict):
title = str(it.get("title", ""))
desc = it.get("desc")
else:
title, desc = str(it), None
r, c = divmod(i, cols)
k = enter(p, 0.05 + i * (0.5 / n), 0.42)
if k <= 0.003:
continue
acc = fam[i]
x = x0 + c * (cw + gap)
y = y0 + r * (ch + gap) + rise_dy(k, 24)
cv.card(x, y, cw, ch, r=20, alpha=k)
cv.grad_rrect(x, y, cw, ch, 20,
cv.tint(acc, 0.28), cv.tint(acc, 0.04), alpha=k)
# numbered accent chip — a solid block of colour, and an order cue
br = min(38.0, ch * 0.11)
bcy = y + ch * 0.30
cv.ellipse(x + cw / 2, bcy, br, fill=acc, alpha=k)
cv.text_center(x + cw / 2, bcy + 1, f"{i + 1:02d}", "semibold",
br * 0.86, _on(acc), alpha=k)
tsize = cv.fit_size(title, "semibold", 40, cw - 68, min_size=24)
ty = y + ch * 0.62
cv.text_center(x + cw / 2, ty, title, "semibold", tsize,
(24, 24, 24), alpha=k)
if desc:
dsize = cv.fit_size(desc, "regular", 24, cw - 68, min_size=17)
cv.text_center(x + cw / 2, ty + tsize * 0.92, desc, "regular",
dsize, (110, 110, 108), alpha=k)
return cv.finish()
SHA-256: 40d41925a28f5443423535d2327e01ab8b80bf192ce3af5e8b0afce8d3853e8e