Communitygithub.com

long-poster-making-video

Generate 1:4 four-page vertical long-scroll posters (via a 3:4 style anchor and image_references) and create ultra-detailed 1080p 42-cursor Figma wireframe-to-live making-of videos where 35 to 42 simultaneous cursors are strictly bound to active bounding-box handles or typing carets on screen with zero aimless wandering. Use when the user asks to generate a multi-page long poster, a high-density multi-cursor wireframe-to-live design process video, or a poster making-of video in Gemini, Grok, Claude, or ChatGPT.

What is long-poster-making-video?

long-poster-making-video is a Claude Code agent skill that generate 1:4 four-page vertical long-scroll posters (via a 3:4 style anchor and image_references) and create ultra-detailed 1080p 42-cursor Figma wireframe-to-live making-of videos where 35 to 42 simultaneous cursors are strictly bound to active bounding-box handles or typing carets on screen with zero aimless wandering. Use when the user asks to generate a multi-page long poster, a high-density multi-cursor wireframe-to-live design process video, or a poster making-of video in Gemini, Grok, Claude, or ChatGPT.

Works with✓Claude Code~Codex CLI✓Cursor✓Gemini CLI
npx skills add https://github.com/zhu558573-ux/long-poster-making-video/tree/main

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

name: long-poster-making-video

description: Generate 1:4 four-page vertical long-scroll posters (via a 3:4 style anchor and image_references) and create ultra-detailed 1080p 42-cursor Figma wireframe-to-live making-of videos where 35 to 42 simultaneous cursors are strictly bound to active bounding-box handles or typing carets on screen with zero aimless wandering. Use when the user asks to generate a multi-page long poster, a high-density multi-cursor wireframe-to-live design process video, or a poster making-of video in Gemini, Grok, Claude, or ChatGPT. allowed-tools: image_gen default_api google

Long Poster & 42-Cursor Swarm Making-Of Video Skill (Self-Contained)

Summary

这是一套模拟多设计师/多 AI Agent 协同建图(From Wireframe to Live)的 1080p 制作过程动画技能包(非真实设计软件操作录屏)。本技能提供两条100% 完整可执行的路径:

  • 路径 A(AI 视频模型直出):提供中英文双语高密度 42 光标提示词与反偷懒负面词,直接投喂 Grok Video / Sora / 可灵 / Runway / Veo。

  • 路径 B(100% 完整可运行 Python 渲染引擎):下文的 Part 3 已完整内嵌可逐帧渲染并合成 MP4 的全部源码(无需任何外部隐藏脚本,复制保存即可直接运行),支持 1:4、9:16、3:4、1:1、16:9 任意比例图片,光标数量(N_CURSORS = 42)与手柄锁点精准可控、可数、可验。

Mandatory Execution Rules (5 条核心强制铁律)

  1. 强制同屏 42 个并发工作光标(N_CURSORS = 42):每帧同时显示 35 至 42 个光标,涵盖白色箭头(ARROW)、十字准星(CROSS +)与工字打字光标(IBEAM I),起止帧带黄色点击波纹圈(#ffd750)。

  2. 强制双级手柄优先级分配(primary_points + secondary_points):第一优先级先为屏幕左右两侧每一个活跃选框的右下角主手柄 □ 或打字竖线 | 分配 1 个光标(消灭「无主框」);第二优先级再将剩余光标分配给各框的左上、右上、上中、右中手柄,实现多光标协同拉框。

  3. 强制零残影直接锁点(Zero Mid-Air Lag):严禁正弦波游荡,严禁跨框慢速飞行飘移。光标直接锁定当前活跃手柄实时坐标 tx, ty,确保 100% 的帧里每个光标尖端都咬住白色手柄方块 □ 或打字竖线 |。

  4. 强制高并发重叠时间轴(每波次 14 至 16 个模块同时施工):全片 23.5 秒分为 4 个波次,各模块启动时间仅错开 0.11s,保证画面随时有 15 个以上选框同步演进(线框拉出 -> 灰块占位 -> 蓝图线稿 -> 全彩对齐吸附)。

  5. 强制纯粹 1080p 画布(No Monitor Bezels):纯 1920x1080(30fps)深炭黑网格画布(#101218),带像素标尺、红蓝参考线,严禁物理显示器边框或人手。

Part 1: 两阶段 1:4 四页竖版长图海报生成模板

  • 阶段 1(3:4 视觉锚点):一张充满活力的手绘粉彩插画风格宣传海报,主题为「[核心主题]」。画面中心是[主要角色阵容及动作],周围环绕着[核心道具]。顶部粗体手绘标题「[主标题]」,下方彩色丝带副标题「[副标题]」。

  • 阶段 2(1:4 挂载参考图延展):传入阶段 1 图片作为参考图,显式分段描述 第一页(封面区)、第二页(第一步)、第三页(第二步)、第四页(第三步与底部行动口号) 及水平分割线。

Part 2: 路径 A — Grok Video / Sora / 可灵 / Runway 专用提示词

  • 中文提示词:纯正的 1080p 桌面 UI 设计软件全屏动画,无物理显示器外框。深炭黑色网格画布(#101218),带像素标尺与红蓝参考线。画面中同时有 35 到 42 个白色箭头光标、十字准星(+)与工字打字光标(I)在左右两侧高速协同工作,每个光标都死死咬住蓝色线框的白色控制柄(□)或打字竖线(|)并带黄色点击波纹,绝无空白处乱晃。左侧栏逐字敲出标题与规范卡片;右侧画板经历蓝色线框(带红色靶心与像素标签)、浅灰占位块、深蓝墨水线稿(SKETCH LAYER)、全彩缩放吸附(伴随红色智能对齐线)及横幅逐字输入高亮锁定。

  • English Prompt:Pure 1080p full-screen dark-mode Figma UI animation, NO monitor bezels, NO human hands. Dark pixel-grid canvas (#101218) with rulers and red/cyan guides. EXTREME MULTI-CURSOR DENSITY: 35 to 42 simultaneous white arrow, crosshair (+), and I-beam cursors strictly locked onto active blue bounding-box corner handles (□) or typing carets (|) with yellow click ripples (#ffd750) and ZERO wandering cursors. Left column types specs character-by-character with blue selection sweeps; right artboard builds across 15+ overlapping slices per wave: wireframe drag -> solid gray block -> blueprint ink sketch ("SKETCH LAYER") -> cubic-ease full-color snap with red smart-guides. Smooth 30fps.

  • Negative Prompt:single cursor, few cursors, idle floating cursors, wandering mouse, monitor bezel, human hands, blurry text, instant fade-in without wireframe.

Part 3: 路径 B — 100% 完整可运行的 Python 渲染引擎(复制即跑)

将以下代码保存为 render_making_video.py,执行 python3 render_making_video.py --poster your_image.png --output out.mp4 即可直接输出 1080p MP4(同样内置于技能包 scripts/render_making_video.py):

#!/usr/bin/env python3
"""
Self-Contained 42-Cursor Priority-Bound Figma Wireframe-to-Live Video Engine
Works out-of-the-box on ANY image aspect ratio (1:4, 9:16, 3:4, 1:1, 16:9).
Usage:
  python3 render_making_video.py --poster input.png --output out.mp4 [--title "MY POSTER"] [--handle "@creator"] [--bgm music.mp4] [--preview-dir ./previews]
"""
import argparse, os, subprocess
import cv2
import numpy as np
from PIL import Image, ImageDraw, ImageFilter, ImageFont

W, H, FPS = 1920, 1080, 30
DURATION, TOTAL_FRAMES, N_CURSORS = 23.5, int(23.5 * 30), 42


def load_font(size):
    for p in [
        "/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc",
        "/usr/share/fonts/truetype/noto/NotoSansCJK-Bold.ttc",
        "/System/Library/Fonts/PingFang.ttc",
        "C:/Windows/Fonts/msyhbd.ttc",
        "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf",
    ]:
        if os.path.exists(p):
            try:
                return ImageFont.truetype(p, size)
            except Exception:
                pass
    return ImageFont.load_default()


F_TINY, F_SM, F_MD = load_font(12), load_font(15), load_font(20)
F_LG, F_XL, F_XXL = load_font(28), load_font(42), load_font(52)


def ease_out_cubic(x):
    x = float(np.clip(x, 0.0, 1.0))
    return 1.0 - (1.0 - x) ** 3


def ease_in_out_cubic(x):
    x = float(np.clip(x, 0.0, 1.0))
    return 4 * x * x * x if x < 0.5 else 1 - ((-2 * x + 2) ** 3) / 2


def draw_selection_handles(draw, x1, y1, x2, y2, color=(55, 145, 255, 255), eight_handles=True):
    draw.rectangle([x1, y1, x2, y2], outline=color, width=2)
    s = 4
    pts = [(x1, y1), (x2, y1), (x1, y2), (x2, y2)]
    if eight_handles and (x2 - x1) > 40 and (y2 - y1) > 36:
        mx, my = (x1 + x2) // 2, (y1 + y2) // 2
        pts += [(mx, y1), (mx, y2), (x1, my), (x2, my)]
    for px, py in pts:
        draw.rectangle([px - s, py - s, px + s, py + s], fill=(255, 255, 255, 255), outline=color, width=2)


def draw_cursor_icon(draw, x, y, mode="ARROW"):
    ix, iy = int(x), int(y)
    if mode == "CROSS":
        r = 8
        draw.line([ix - r - 1, iy, ix + r + 1, iy], fill=(0, 0, 0, 235), width=4)
        draw.line([ix, iy - r - 1, ix, iy + r + 1], fill=(0, 0, 0, 235), width=4)
        draw.line([ix - r, iy, ix + r, iy], fill=(255, 255, 255, 255), width=2)
        draw.line([ix, iy - r, ix, iy + r], fill=(255, 255, 255, 255), width=2)
    elif mode == "IBEAM":
        h = 11
        draw.line([ix, iy - h, ix, iy + h], fill=(0, 0, 0, 235), width=4)
        draw.line([ix - 5, iy - h, ix + 5, iy - h], fill=(0, 0, 0, 235), width=4)
        draw.line([ix - 5, iy + h, ix + 5, iy + h], fill=(0, 0, 0, 235), width=4)
        draw.line([ix, iy - h, ix, iy + h], fill=(255, 255, 255, 255), width=2)
        draw.line([ix - 4, iy - h, ix + 4, iy - h], fill=(255, 255, 255, 255), width=2)
        draw.line([ix - 4, iy + h, ix + 4, iy + h], fill=(255, 255, 255, 255), width=2)
    else:
        poly = [(ix, iy), (ix, iy + 21), (ix + 5, iy + 16), (ix + 9, iy + 24),
                (ix + 13, iy + 22), (ix + 8, iy + 14), (ix + 15, iy + 14)]
        draw.polygon(poly, fill=(255, 255, 255, 255), outline=(10, 12, 16, 255))
        draw.line(poly + [poly[0]], fill=(10, 12, 16, 255), width=2)


def build_auto_slices(poster_x, poster_y, poster_w, poster_h):
    """Generate 60 overlapping visual modules across 4 waves for any aspect ratio."""
    modules = []
    max_scroll = max(0, poster_h + 95 - H)
    zone_ranges = [
        (0.22, 0, min(poster_h, H - 80)),
        (5.40, int(max_scroll * 0.33), min(poster_h, int(max_scroll * 0.33) + H - 80)),
        (11.00, int(max_scroll * 0.67), min(poster_h, int(max_scroll * 0.67) + H - 80)),
        (16.40, max_scroll, poster_h),
    ]
    for w_idx, (t_base, zy1, zy2) in enumerate(zone_ranges):
        if zy2 - zy1 < 120:
            zy1, zy2 = 0, poster_h
        modules.append(((poster_x, poster_y + zy1, poster_x + poster_w, poster_y + zy2),
                        t_base, t_base + 0.25, t_base + 0.55, t_base + 0.85, t_base + 1.05, "ARTBOARD"))
        cols, rows = 4, 3
        cw, ch = poster_w // cols, (zy2 - zy1) // rows
        idx = 0
        for r in range(rows):
            for c in range(cols):
                pad_x, pad_y = int(cw * 0.14), int(ch * 0.14)
                x1 = poster_x + max(0, c * cw - (pad_x if c > 0 else 0))
                y1 = poster_y + zy1 + max(0, r * ch - (pad_y if r > 0 else 0))
                x2 = poster_x + min(poster_w, (c + 1) * cw + (pad_x if c < cols - 1 else 0))
                y2 = poster_y + zy1 + min(zy2 - zy1, (r + 1) * ch + (pad_y if r < rows - 1 else 0))
                tw = t_base + 0.08 + idx * 0.11
                dur = 1.50
                mtype = "HERO_GRAY" if (r + c) % 3 == 0 else ("COIN_SNAP" if (r + c) % 3 == 1 else "NORMAL")
                modules.append(((x1, y1, x2, y2), tw, tw + dur * 0.27, tw + dur * 0.54, tw + dur * 0.80, tw + dur, mtype))
                idx += 1
        # Add 2 hero overlapping center focus boxes per wave
        for k, (fx1, fy1, fx2, fy2) in enumerate([(0.15, 0.12, 0.58, 0.72), (0.42, 0.24, 0.88, 0.86)]):
            bx1 = poster_x + int(poster_w * fx1)
            by1 = poster_y + zy1 + int((zy2 - zy1) * fy1)
            bx2 = poster_x + int(poster_w * fx2)
            by2 = poster_y + zy1 + int((zy2 - zy1) * fy2)
            tw = t_base + 0.35 + k * 0.45
            modules.append(((bx1, by1, bx2, by2), tw, tw + 0.40, tw + 0.80, tw + 1.20, tw + 1.50, "HERO_GRAY"))
        t_seal = t_base + 3.15
        modules.append(((poster_x, poster_y + zy1, poster_x + poster_w, poster_y + zy2),
                        t_seal, t_seal + 0.01, t_seal + 0.02, t_seal + 0.03, t_seal + 0.05, "SEAMLESS"))
    return modules


def run_engine(poster_path, output_path, title="DESIGN STUDIO 4K", handle="@Creator", bgm_path=None, preview_dir=None):
    orig_im = Image.open(poster_path).convert("RGB")
    W0, H0 = orig_im.size
    POSTER_X, POSTER_Y, POSTER_W = 880, 52, 980
    POSTER_H = int(round(POSTER_W * (H0 / float(W0))))
    if POSTER_H < 980:
        POSTER_Y = max(52, (H - POSTER_H) // 2)

    poster_full = orig_im.resize((POSTER_W, POSTER_H), Image.Resampling.LANCZOS)
    np_full = np.array(poster_full)
    gray_p = cv2.GaussianBlur(cv2.cvtColor(np_full, cv2.COLOR_RGB2GRAY), (3, 3), 0)
    edges_a = cv2.adaptiveThreshold(gray_p, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 13, 4)
    edges_c = cv2.Canny(gray_p, 35, 110)
    is_ink = (edges_a < 128) | (edges_c > 0)
    sketch_rgb = np.stack([np.where(is_ink, 28, 244), np.where(is_ink, 43, 240), np.where(is_ink, 69, 230)], axis=-1).astype(np.uint8)
    poster_sketch = Image.fromarray(sketch_rgb)

    artboard_underlay = Image.blend(
        poster_full.filter(ImageFilter.GaussianBlur(radius=16)),
        Image.new("RGB", (POSTER_W, POSTER_H), (20, 24, 32)),
        0.42,
    )

    poster_modules = build_auto_slices(POSTER_X, POSTER_Y, POSTER_W, POSTER_H)
    max_scroll = float(max(0, POSTER_H + 95 - H))

    def get_scroll_y(t):
        if max_scroll <= 0:
            return 0.0
        s1, s2, s3 = max_scroll * 0.33, max_scroll * 0.67, max_scroll
        if t < 4.8:
            return 0.0
        elif t < 5.6:
            return s1 * ease_in_out_cubic((t - 4.8) / 0.8)
        elif t < 10.4:
            return s1
        elif t < 11.2:
            return s1 + (s2 - s1) * ease_in_out_cubic((t - 10.4) / 0.8)
        elif t < 15.8:
            return s2
        elif t < 16.6:
            return s2 + (s3 - s2) * ease_in_out_cubic((t - 15.8) / 0.8)
        return s3

    left_ui_cards = []
    left_typing_modules = []
    wave_meta = [
        (0.00, 5.25, "01 / WIREFRAME ARCHITECTURE", title, handle, "42-Cursor Parallel Slice Initialization"),
        (5.30, 10.85, "02 / GENGA SKETCH & MESH", "FROM WIREFRAME", "TO LIVE ART.", "Blueprint Ink Extraction & Grayscale Blocking"),
        (10.90, 16.25, "03 / COLOR & SMART GUIDES", "CUBIC EASE SNAP", "& ALIGNMENT.", "High-Resolution Full-Color Composite"),
        (16.30, 23.60, "04 / MASTER ARTBOARD LOCK", "42 CURSORS SYNC", "100% VERIFIED.", f"Output: {W0}x{H0} | 30.0 FPS Pure Canvas"),
    ]
    for t0, t_exp, k0, k1, k2, k3 in wave_meta:
        left_typing_modules.extend([
            ((42, 58, 560, 88), k0, F_SM, (110, 180, 255), (110, 180, 255), None, t0 + 0.25, t0 + 1.15, t0 + 1.60, t_exp),
            ((42, 98, 750, 156), k1, F_XXL, (255, 255, 255), (255, 255, 255), None, t0 + 0.40, t0 + 1.45, t0 + 1.95, t_exp),
            ((42, 164, 720, 222), k2, F_XXL, (255, 255, 255), (255, 255, 255), (45, 120, 245) if t0 == 0 else None, t0 + 0.60, t0 + 1.70, t0 + 2.20, t_exp),
            ((42, 232, 760, 270), k3, F_MD, (255, 215, 90), (200, 215, 235), None, t0 + 0.80, t0 + 1.95, t0 + 2.40, t_exp),
        ])
        left_ui_cards.extend([
            (42, 295, 275, 490, "THUMB_COLOR", t0 + 0.45, t0 + 1.55, t0 + 2.50, t_exp),
            (295, 295, 530, 490, "THUMB_SKETCH", t0 + 0.60, t0 + 1.70, t0 + 2.65, t_exp),
            (550, 295, 785, 490, "SPEC_CARD", t0 + 0.75, t0 + 1.85, t0 + 2.80, t_exp),
            (42, 520, 785, 750, "PIPELINE_CARD", t0 + 0.90, t0 + 2.15, t0 + 3.25, t_exp),
            (42, 780, 740, 945, "PROGRESS_BAR", t0 + 1.05, t0 + 2.35, t0 + 3.50, t_exp),
        ])

    def render_left_card(scr, draw, kind, x1, sy1, x2, sy2):
        w, h = x2 - x1, sy2 - sy1
        draw.rectangle([x1, sy1, x2, sy2], fill=(22, 26, 34, 240), outline=(58, 68, 88, 255), width=1)
        if kind == "THUMB_COLOR":
            scr.paste(poster_full.crop((0, 0, POSTER_W, min(POSTER_H, POSTER_W))).resize((w - 16, h - 38), Image.Resampling.LANCZOS), (x1 + 8, sy1 + 8))
            draw.text((x1 + w // 2, sy2 - 15), f"SOURCE ({W0}x{H0})", font=F_TINY, fill=(170, 195, 235, 255), anchor="mm")
        elif kind == "THUMB_SKETCH":
            scr.paste(poster_sketch.crop((0, 0, POSTER_W, min(POSTER_H, POSTER_W))).resize((w - 16, h - 38), Image.Resampling.LANCZOS), (x1 + 8, sy1 + 8))
            draw.text((x1 + w // 2, sy2 - 15), "BLUEPRINT SKETCH", font=F_TINY, fill=(130, 210, 255, 255), anchor="mm")
        elif kind == "SPEC_CARD":
            for i, (col, lbl) in enumerate([((65, 155, 255), "WIREFRAME"), ((204, 208, 212), "BLOCKING"), ((30, 45, 72), "LINEART"), ((255, 210, 85), "42 CURSORS")]):
                yy = sy1 + 12 + i * ((h - 24) // 4)
                draw.rectangle([x1 + 12, yy, x1 + 52, yy + 28], fill=col + (255,))
                draw.text((x1 + 64, yy + 14), lbl, font=F_TINY, fill=(215, 225, 240, 255), anchor="lm")
        elif kind == "PIPELINE_CARD":
            for i, (txt, col) in enumerate([
                ("Stage 1: Crosshair (+) Wireframe Drag & Target Center", (115, 185, 255)),
                ("Stage 2: Solid Gray Placeholder (#ced2d0) & 8 Handles", (215, 225, 240)),
                ("Stage 3: Adaptive Blueprint Ink Sketch Layer", (150, 235, 175)),
                ("Stage 4: Cubic-Ease Color Snap + Smart Guides", (255, 215, 95)),
            ]):
                draw.text((x1 + 18, sy1 + 36 + i * 48), txt, font=F_SM, fill=col + (255,), anchor="lm")
        elif kind == "PROGRESS_BAR":
            draw.text((x1 + 18, sy1 + 32), "SWARM ENGINE : 42 ACTIVE CURSORS LOCKED TO HANDLES", font=F_SM, fill=(240, 245, 255, 255), anchor="lm")
            draw.rectangle([x1 + 18, sy1 + 68, x2 - 18, sy1 + 92], fill=(35, 42, 58, 255))
            draw.rectangle([x1 + 18, sy1 + 68, x2 - 42, sy1 + 92], fill=(65, 155, 255, 255))
            draw.text((x1 + 18, sy1 + 126), "Zero Aimless Wandering | Zero Mid-Air Lag | 1080p 30FPS", font=F_SM, fill=(135, 210, 255, 255), anchor="lm")

    def render_frame(t):
        scroll_y = int(round(get_scroll_y(t)))
        scr = Image.new("RGBA", (W, H), (16, 18, 24, 255))
        draw = ImageDraw.Draw(scr, "RGBA")
        for gx in range(30, W, 60):
            draw.line([gx, 24, gx, H], fill=(25, 28, 36, 255), width=1)
        for gy in range(50 - (scroll_y % 60), H, 60):
            if gy >= 24:
                draw.line([28, gy, W, gy], fill=(25, 28, 36, 255), width=1)

        ay1, ay2 = POSTER_Y - scroll_y, POSTER_Y + POSTER_H - scroll_y
        if t >= 0.20:
            scr.paste(artboard_underlay, (POSTER_X, ay1))
            draw.rectangle([POSTER_X - 1, ay1 - 1, POSTER_X + POSTER_W + 1, ay2 + 1], outline=(60, 70, 90, 255), width=2)

        primary_points, secondary_points = [], []

        for box, tw, tg, tsk, tc, td, mtype in poster_modules:
            x1, y1, x2, y2 = box
            sy1, sy2 = y1 - scroll_y, y2 - scroll_y
            if sy2 < -60 or sy1 > H + 60 or t < td:
                continue
            src = artboard_underlay if mtype == "ARTBOARD" else poster_full
            scr.paste(src.crop((x1 - POSTER_X, y1 - POSTER_Y, x2 - POSTER_X, y2 - POSTER_Y)), (x1, sy1))

        for box, tw, tg, tsk, tc, td, mtype in poster_modules:
            x1, y1, x2, y2 = box
            sy1, sy2 = y1 - scroll_y, y2 - scroll_y
            if sy2 < -60 or sy1 > H + 60 or t < tw or t >= td or mtype == "SEAMLESS":
                continue
            wb, hb = x2 - x1, y2 - y1
            rip = 0.7 if (t - tw < 0.15 or td - t < 0.15) else 0.0
            if t < tg:
                p = ease_out_cubic((t - tw) / max(0.04, tg - tw))
                cw, ch = max(28, int(wb * p)), max(28, int(hb * p))
                bx2, by2 = x1 + cw, sy1 + ch
                ov = Image.new("RGBA", (cw, ch), (95, 130, 180, 78))
                scr.paste(ov, (x1, sy1), ov)
                col = (240, 80, 125, 255) if mtype == "COIN_SNAP" else (75, 160, 255, 255)
                draw_selection_handles(draw, x1, sy1, bx2, by2, color=col, eight_handles=False)
                cx_m, cy_m = (x1 + bx2) // 2, (sy1 + by2) // 2
                draw.ellipse([cx_m - 10, cy_m - 10, cx_m + 10, cy_m + 10], outline=(240, 65, 60, 230), width=2)
                draw.rectangle([cx_m - 36, by2 + 4, cx_m + 36, by2 + 22], fill=(55, 140, 250, 255))
                draw.text((cx_m, by2 + 13), f"{cw}×{ch}", font=F_TINY, fill=(255, 255, 255, 255), anchor="mm")
                primary_points.append((bx2, by2, "CROSS", rip))
                secondary_points.extend([(x1, sy1, "ARROW", 0.0), (bx2, sy1, "CROSS", 0.0)])
            elif t < tsk:
                if mtype == "HERO_GRAY":
                    draw.rectangle([x1, sy1, x2, sy2], fill=(204, 208, 212, 255))
                else:
                    ov = Image.new("RGBA", (wb, hb), (110, 140, 185, 98))
                    scr.paste(ov, (x1, sy1), ov)
                draw_selection_handles(draw, x1, sy1, x2, sy2, color=(55, 145, 255, 255), eight_handles=True)
                primary_points.append((x2, sy2, "CROSS", rip))
                secondary_points.extend([((x1 + x2) // 2, sy1, "ARROW", 0.0), (x2, (sy1 + sy2) // 2, "ARROW", 0.0)])
            elif t < tc:
                scr.paste(poster_sketch.crop((x1 - POSTER_X, y1 - POSTER_Y, x2 - POSTER_X, y2 - POSTER_Y)), (x1, sy1))
                draw_selection_handles(draw, x1, sy1, x2, sy2, color=(95, 185, 255, 255), eight_handles=True)
                cx_m = (x1 + x2) // 2
                draw.rectangle([cx_m - 44, sy2 + 4, cx_m + 44, sy2 + 22], fill=(45, 115, 220, 255))
                draw.text((cx_m, sy2 + 13), "SKETCH LAYER", font=F_TINY, fill=(255, 255, 255, 255), anchor="mm")
                primary_points.append((x2, sy2, "ARROW", rip))
                secondary_points.extend([(x1, (sy1 + sy2) // 2, "ARROW", 0.0), (cx_m, sy2, "CROSS", 0.0)])
            else:
                p = ease_out_cubic((t - tc) / max(0.04, td - tc))
                src = artboard_underlay if mtype == "ARTBOARD" else poster_full
                scale = 0.90 + 0.10 * p
                cw, ch = max(18, int(wb * scale)), max(18, int(hb * scale))
                px, py = x1 + (wb - cw) // 2, sy1 + (hb - ch) // 2
                scr.paste(src.crop((x1 - POSTER_X, y1 - POSTER_Y, x2 - POSTER_X, y2 - POSTER_Y)).resize((cw, ch), Image.Resampling.LANCZOS), (px, py))
                if p < 0.75:
                    draw.line([POSTER_X, py, POSTER_X + POSTER_W, py], fill=(235, 65, 60, 185), width=1)
                    draw.line([px, 0, px, H], fill=(235, 65, 60, 185), width=1)
                col = (240, 80, 125, 255) if mtype == "COIN_SNAP" else (65, 155, 255, 255)
                draw_selection_handles(draw, px, py, px + cw, py + ch, color=col, eight_handles=True)
                primary_points.append((px + cw, py + ch, "ARROW", rip))
                secondary_points.extend([(px, py, "ARROW", 0.0), (px + cw, py + ch // 2, "CROSS", 0.0)])

        for x1, sy1, x2, sy2, kind, tw, tl, td, t_exp in left_ui_cards:
            if t < tw or t >= t_exp:
                continue
            wb, hb = x2 - x1, sy2 - sy1
            if t < tl:
                p = ease_out_cubic((t - tw) / max(0.04, tl - tw))
                cw, ch = max(20, int(wb * p)), max(20, int(hb * p))
                ov = Image.new("RGBA", (cw, ch), (95, 120, 160, 75))
                scr.paste(ov, (x1, sy1), ov)
                draw_selection_handles(draw, x1, sy1, x1 + cw, sy1 + ch, color=(75, 160, 255, 255), eight_handles=False)
                primary_points.append((x1 + cw, sy1 + ch, "CROSS", 0.5))
                secondary_points.append((x1, sy1, "ARROW", 0.0))
            elif t < td:
                render_left_card(scr, draw, kind, x1, sy1, x2, sy2)
                draw_selection_handles(draw, x1, sy1, x2, sy2, color=(65, 155, 255, 255), eight_handles=True)
                primary_points.append((x2, sy2, "ARROW", 0.3))
                secondary_points.append(((x1 + x2) // 2, sy1, "ARROW", 0.0))
            else:
                render_left_card(scr, draw, kind, x1, sy1, x2, sy2)

        for box, text_str, font_obj, init_col, final_col, bg_col, ts, tt, th, t_exp in left_typing_modules:
            x1, sy1, x2, sy2 = box
            if t < ts or t >= t_exp:
                continue
            ty = (sy1 + sy2) // 2
            if t < tt:
                p = np.clip((t - ts) / max(0.04, tt - ts), 0.0, 1.0)
                sub_txt = text_str[:max(1, int(len(text_str) * p))]
                draw.rectangle([x1 - 4, sy1 - 2, x2 + 4, sy2 + 2], fill=(22, 26, 36, 220))
                tw_px = draw.textbbox((0, 0), sub_txt, font=font_obj)[2]
                draw.text((x1, ty), sub_txt + "|", font=font_obj, fill=init_col + (255,), anchor="lm")
                bx_r = max(x1 + 55, x1 + tw_px + 22)
                draw_selection_handles(draw, x1 - 4, sy1 - 2, bx_r, sy2 + 2, color=(65, 155, 255, 255), eight_handles=False)
                primary_points.append((x1 + tw_px + 8, ty, "IBEAM", 0.4))
                secondary_points.append((bx_r, sy2 + 2, "ARROW", 0.0))
            elif t < th:
                tw_px = draw.textbbox((0, 0), text_str, font=font_obj)[2]
                sel = Image.new("RGBA", (tw_px + 16, max(8, sy2 - sy1 + 4)), (45, 120, 245, 185))
                scr.paste(sel, (x1 - 6, sy1 - 2), sel)
                draw.text((x1, ty), text_str, font=font_obj, fill=(255, 255, 255, 255), anchor="lm")
                draw_selection_handles(draw, x1 - 4, sy1 - 2, x1 + tw_px + 14, sy2 + 2, color=(95, 180, 255, 255), eight_handles=True)
                primary_points.append((x1 + tw_px + 14, sy2 + 2, "IBEAM", 0.6))
                secondary_points.append((x1 - 4, sy1 - 2, "ARROW", 0.0))
            else:
                if bg_col is not None:
                    tw_px = draw.textbbox((0, 0), text_str, font=font_obj)[2]
                    draw.rectangle([x1 - 6, sy1, x1 + tw_px + 10, sy2], fill=bg_col + (255,))
                draw.text((x1, ty), text_str, font=font_obj, fill=final_col + (255,), anchor="lm")

        draw.rectangle([0, 0, W, 24], fill=(24, 26, 32, 248))
        draw.rectangle([0, 0, 28, H], fill=(24, 26, 32, 248))
        for rx in range(50, W, 100):
            draw.line([rx, 15, rx, 24], fill=(125, 135, 155, 255), width=1)
            draw.text((rx + 4, 12), str(rx), font=F_TINY, fill=(135, 145, 165, 255), anchor="lm")
        for ry in range(50, H, 100):
            draw.line([18, ry, 28, ry], fill=(125, 135, 155, 255), width=1)
        draw.line([30, 0, 30, H], fill=(235, 55, 55, 235), width=2)
        draw.line([840, 0, 840, H], fill=(235, 55, 55, 175), width=1)
        draw.line([0, 50, W, 50], fill=(65, 150, 250, 195), width=1)

        live_work_points = primary_points + secondary_points
        if len(live_work_points) < N_CURSORS:
            vis = [(x1, y1 - scroll_y, x2, y2 - scroll_y) for (x1, y1, x2, y2), _, _, _, _, td, mt in poster_modules
                   if mt not in ("ARTBOARD", "SEAMLESS") and t >= td and 45 < y1 - scroll_y < H - 80]
            vis += [(x1, sy1, x2, sy2) for x1, sy1, x2, sy2, _, _, _, td, te in left_ui_cards if td <= t < te]
            i_v = 0
            while len(live_work_points) < N_CURSORS and vis:
                vx1, vsy1, vx2, vsy2 = vis[i_v % len(vis)]
                draw_selection_handles(draw, vx1, vsy1, vx2, vsy2, color=(65, 155, 255, 210), eight_handles=True)
                mx, my = (vx1 + vx2) // 2, (vsy1 + vsy2) // 2
                for hx, hy, hm in [(vx2, vsy2, "ARROW"), (vx1, vsy1, "CROSS"), (mx, vsy1, "ARROW"), (vx2, my, "CROSS")]:
                    if len(live_work_points) < N_CURSORS:
                        live_work_points.append((hx, hy, hm, 0.3))
                i_v += 1

        for idx_c, (tx, ty, cm, rip) in enumerate(live_work_points[:N_CURSORS]):
            if rip > 0.05:
                r_rad = int(8 + rip * 10)
                draw.ellipse([int(tx) - r_rad, int(ty) - r_rad, int(tx) + r_rad, int(ty) + r_rad], outline=(255, 215, 80, 190), width=2)
            draw_cursor_icon(draw, tx, ty, mode=cm)

        if t > 22.6:
            fade = np.clip((23.5 - t) / 0.9, 0.0, 1.0)
            return (np.array(scr.convert("RGB"), dtype=np.float32) * fade).astype(np.uint8)
        return np.array(scr.convert("RGB"))

    os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
    raw_video = output_path + ".raw.mp4"
    proc = subprocess.Popen([
        "ffmpeg", "-y", "-f", "rawvideo", "-vcodec", "rawvideo", "-s", f"{W}x{H}",
        "-pix_fmt", "rgb24", "-r", str(FPS), "-i", "-", "-c:v", "libx264",
        "-pix_fmt", "yuv420p", "-preset", "fast", "-crf", "17", "-movflags", "+faststart", raw_video
    ], stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    for f in range(TOTAL_FRAMES):
        rgb = render_frame(f / FPS)
        proc.stdin.write(rgb.tobytes())
        if preview_dir and f in {60, 195, 360, 525}:
            os.makedirs(preview_dir, exist_ok=True)
            Image.fromarray(rgb).save(os.path.join(preview_dir, f"preview_{f}.png"))
    proc.stdin.close()
    err = proc.stderr.read().decode("utf-8", errors="ignore")
    proc.wait()
    if proc.returncode != 0:
        raise RuntimeError(f"FFmpeg failed: {err}")
    if bgm_path and os.path.exists(bgm_path):
        subprocess.run(["ffmpeg", "-y", "-i", raw_video, "-i", bgm_path, "-map", "0:v:0", "-map", "1:a:0",
                        "-c:v", "copy", "-c:a", "aac", "-b:a", "192k", "-shortest", "-movflags", "+faststart", output_path], check=True)
        os.remove(raw_video)
    else:
        os.replace(raw_video, output_path)


if __name__ == "__main__":
    p = argparse.ArgumentParser()
    p.add_argument("--poster", required=True, help="Path to target poster/photo")
    p.add_argument("--output", required=True, help="Output MP4 path")
    p.add_argument("--title", default="DESIGN STUDIO 4K")
    p.add_argument("--handle", default="@Creator")
    p.add_argument("--bgm", default=None)
    p.add_argument("--preview-dir", default=None)
    args = p.parse_args()
    run_engine(args.poster, args.output, args.title, args.handle, args.bgm, args.preview_dir)

Related Skills