← Files AgentBrokoARCHIVED FILE

src/video_forge/scenes/node_stack.py

3.46 KB · Sep 30, 2026 · 23:15 UTC

↓ Download file

"""node_stack — nested rounded cards in columns (a system/architecture diagram).
Reference: the "Your agent" + "Integrations" beat.

scene fields:
  columns: [ { "title": str, "rows": [ row, ... ] }, ... ]
    where a row is either a string (full-width sub-card) or a list of strings
    (split into equal sub-cards on one line).
  Falls back to a single column built from `items` if `columns` is absent.

Column width is derived from the canvas rather than fixed, and in portrait the
columns stack vertically: a fixed side-by-side width overflowed 1080-wide frames
and clipped both the titles and the outer cells.
"""

from ..theme import accent_family
from ._common import (bg_wash, content_w, enter, is_portrait, new_canvas,
                      rise_dy, side_margin)

HDR_H = 70.0
ROW_H = 66.0
RGAP = 14.0
COL_GAP = 64.0
STACK_GAP = 46.0
MAX_COL_W = 600.0


def _columns(scene):
    cols = scene.get("columns")
    if cols:
        return cols
    return [{"title": scene.get("title", ""), "rows": scene.get("items", [])}]


def _col_height(col):
    h = (HDR_H + RGAP) if col.get("title") else 0.0
    rows = col.get("rows", [])
    h += len(rows) * (ROW_H + RGAP)
    return h - RGAP if h else 0.0


def render(ctx):
    cv = new_canvas(ctx)
    W, H = cv.W, cv.H
    p = ctx.p
    bg_wash(cv, p, 0.9)

    cols = _columns(ctx.scene)
    ncol = len(cols)
    margin = side_margin(cv)
    avail = content_w(cv)
    stacked = is_portrait(cv) and ncol > 1

    if stacked:
        col_w = avail
        total_h = sum(_col_height(c) for c in cols) + STACK_GAP * (ncol - 1)
        top0 = (H - total_h) / 2
    else:
        col_w = min(MAX_COL_W, (avail - COL_GAP * (ncol - 1)) / ncol)
        total_w = col_w * ncol + COL_GAP * (ncol - 1)
        x0 = (W - total_w) / 2
        top0 = (H - max(_col_height(c) for c in cols)) / 2

    fam = accent_family(cv.accent, ncol, spread=0.03)
    ink_dark = (24, 24, 24)
    y_cursor = top0

    for i, col in enumerate(cols):
        acc = fam[i]
        kcol = enter(p, 0.04 + i * 0.12, 0.4)
        cx0 = margin if stacked else x0 + i * (col_w + COL_GAP)
        y = (y_cursor if stacked else top0) + rise_dy(kcol, 22)

        title = col.get("title")
        if title:
            cv.card(cx0, y, col_w, HDR_H, r=16, alpha=kcol, elevate=22)
            cv.grad_rrect(cx0, y, col_w, HDR_H, 16,
                          cv.tint(acc, 0.30), cv.tint(acc, 0.10), alpha=kcol)
            cv.ellipse(cx0 + 40, y + HDR_H / 2, 11, fill=acc, alpha=kcol)
            tsize = cv.fit_size(title, "medium", 30, col_w - 100, min_size=18)
            cv.text(cx0 + 68, y + HDR_H / 2, title, "medium", tsize, ink_dark,
                    anchor="lm", alpha=kcol)
            y += HDR_H + RGAP

        for j, row in enumerate(col.get("rows", [])):
            kr = enter(p, 0.12 + i * 0.1 + j * 0.06, 0.4)
            cells = row if isinstance(row, list) else [row]
            m = len(cells)
            sw = (col_w - RGAP * (m - 1)) / m
            for c, cell in enumerate(cells):
                sx = cx0 + c * (sw + RGAP)
                cv.grad_rrect(sx, y, sw, ROW_H, 14,
                              cv.tint(acc, 0.15), cv.tint(acc, 0.05), alpha=kr)
                size = cv.fit_size(str(cell), "regular", 26, sw - 40, min_size=16)
                cv.text_center(sx + sw / 2, y + ROW_H / 2, str(cell), "regular",
                               size, ink_dark, alpha=kr)
            y += ROW_H + RGAP

        if stacked:
            y_cursor += _col_height(col) + STACK_GAP
    return cv.finish()

SHA-256: 61f8e739da07aad0712ff77dc2670bc941bf1f2167e0ab0670102dd519e3f276