Communitygithub.com

长海报制作视频

生成1:4四页竖向长滚动海报(通过3:4样式锚点和图像参考),并创建超详细的1080p 42光标Figma线稿到成品制作过程视频,其中35到42个同步光标严格绑定到活动边界框手柄或屏幕上的打字插入符,零无目的游荡。当用户在Gemini、Grok、Claude或ChatGPT中要求生成多页长海报、高密度多光标线稿到成品设计过程视频或海报制作视频时使用。

长海报制作视频 是什么?

这是一个专为AI Agent和视频生成模型设计的技能,用于创建1:4比例的四页竖向长滚动海报及其超详细的1080p制作过程动画。该技能模拟了多达42个同步光标在设计软件(如Figma)中协作从线稿到成品的过程,确保每一帧中的光标都精确锁定在活动手柄或打字插入符上,无任何空闲游荡。它支持两种执行路径:路径A提供中英文双语提示词,可直接用于Grok Video、Sora、可灵、Runway等AI视频模型;路径B则提供一个完整可运行的Python渲染引擎,用户只需保存代码并运行,即可从任意比例的海报图像生成MP4视频。此技能在Gemini、Grok、Claude和ChatGPT等平台上均能有效使用,适用于需要展示高密度多光标协作设计流程的场景,如设计演示、教程制作或宣传材料生成。

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

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

name: long-poster-making-video

description: 生成1:4四页竖向长滚动海报(通过3:4样式锚点和图像参考),并创建超详细的1080p 42光标Figma线稿到成品制作过程视频,其中35到42个同步光标严格绑定到活动边界框手柄或屏幕上的打字插入符,零无目的游荡。当用户在Gemini、Grok、Claude或ChatGPT中要求生成多页长海报、高密度多光标线稿到成品设计过程视频或海报制作视频时使用。 allowed-tools: image_gen default_api google

长海报与42光标集群制作过程视频技能(自包含)

摘要

这是一套**模拟多设计师/多AI Agent协同建图(从线稿到成品)**的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)与手柄锁点精准可控、可数、可验。

强制执行规则(5条核心强制铁律)

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

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

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

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

  5. 强制纯粹1080p画布(无显示器边框):纯 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)、全彩缩放吸附(伴随红色智能对齐线)及横幅逐字输入高亮锁定。

  • 英文提示词: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.

  • 负面提示词:单个光标, 少量光标, 空闲浮动光标, 游荡鼠标, 显示器边框, 人手, 模糊文本, 无线框直接淡入.

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):
    """生成60个重叠视觉模块,分布在4个波次,适用于任何宽高比。"""
    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
        # 每波次添加2个英雄重叠中心聚焦框
        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 / 线框架构", title, handle, "42光标并行切片初始化"),
        (5.30, 10.85, "02 / 蓝图草图与网格", "从线框", "到成品艺术", "蓝图墨水提取与灰度分块"),
        (10.90, 16.25, "03 / 色彩与智能参考线", "立方缓动吸附", "与对齐", "高分辨率全彩合成"),
        (16.30, 23.60, "04 / 主画板锁定", "42光标同步", "100%验证", f"输出: {W0}x{H0} | 30.0 FPS 纯画布"),
    ]
    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"来源 ({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), "蓝图草图", font=F_TINY, fill=(130, 210, 255, 255), anchor="mm")
        elif kind == "SPEC_CARD":
            for i, (col, lbl) in enumerate([((65, 155, 255), "线框"), ((204, 208, 212), "分块"), ((30, 45, 72), "线稿"), ((255, 210, 85), "42光标")]):
                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, (col, lbl) in enumerate([((65, 155, 255), "拖拽线框"), ((70, 70, 80), "灰块占位"), ((28, 43, 69), "蓝图墨水"), ((55, 145, 255), "全彩吸附")]):
                yy = sy1 + 10 + i * ((h - 20) // 4)
                draw.rectangle([x1 + 12, yy, x1 + 40, yy + 20], fill=col + (255,))
                draw.text((x1 + 50, yy + 10), lbl, font=F_TINY, fill=(200, 210, 225, 255), anchor="lm")
        elif kind == "PROGRESS_BAR":
            tw = max(w - 40, 100)
            for j in range(100):
                xp = x1 + 20 + j * (tw / 100)
                draw.rectangle([xp, sy1 + 18, xp + 3, sy1 + 28], fill=(100, 180, 255, 255) if j < 70 else (40, 45, 55, 255))
            draw.text((x1 + 20, sy1 + 8), "处理管道 70%", font=F_TINY, fill=(180, 200, 220, 255))
            draw.text((x1 + w - 20, sy1 + 8), "4/4波次", font=F_TINY, fill=(220, 180, 80, 255), anchor="ra")

    def render_left_typing(scr, draw, img, text_lines, font, first_color, following_color, accent_color, t, t_type_span, t_hold_start, t_hold_end):
        if t < t_type_span:
            return
        progress = min(1.0, (t - t_type_span) / (t_hold_start - t_type_span))
        n_chars = int(len(text_lines) * progress) if isinstance(text_lines, str) else int(len(text_lines) * progress)
        for i, line in enumerate([text_lines] if isinstance(text_lines, str) else text_lines):
            if i > n_chars:
                break
            col = first_color if i == 0 and line == text_lines else (following_color if isinstance(following_color, tuple) else first_color)
            y_pos = 58 + i * 40
            truncated = line[:int(len(line) * (n_chars - i))] if n_chars > i else ""
            draw.text((42, y_pos), truncated, font=font, fill=col + (255,))

    # 主渲染循环
    for frame_idx in range(TOTAL_FRAMES):
        t = frame_idx / FPS
        scr = Image.new("RGBA", (W, H), (16, 18, 24, 255))
        draw = ImageDraw.Draw(scr)
        # 网格背景
        for gx in range(0, W, 80):
            draw.line([gx, 0, gx, H], fill=(24, 28, 38, 255), width=1)
        for gy in range(0, H, 80):
            draw.line([0, gy, W, gy], fill=(24, 28, 38, 255), width=1)
        # 标尺
        for rx in range(0, W, 20):
            if rx % 80 == 0:
                draw.line([rx, 0, rx, 20], fill=(80, 120, 160, 255), width=2)
                draw.text((rx, 22), str(rx), font=F_TINY, fill=(100, 130, 170, 255))
        for ry in range(0, H, 20):
            if ry % 80 == 0:
                draw.line([0, ry, 20, ry], fill=(80, 120, 160, 255), width=2)
                draw.text((24, ry - 6), str(ry), font=F_TINY, fill=(100, 130, 170, 255))
        # 参考线
        draw.line([880, 0, 880, H], fill=(255, 60, 60, 180), width=2)
        draw.line([1860, 0, 1860, H], fill=(0, 180, 255, 180), width=2)
        # 左侧UI
        for (lx1, ly1, lx2, ly2, kind, t0, t1, t2, t3) in left_ui_cards:
            if t0 <= t <= t3:
                render_left_card(scr, draw, kind, lx1, ly1, lx2, ly2)
        # 左侧打字
        for (box, text, font, fcol, scol, acol, t0_type, t1_hold, t2_end, t3_end) in left_typing_modules:
            if t0_type <= t <= t3_end:
                render_left_typing(scr, draw, None, text, font, fcol, scol, acol, t, t0_type, t1_hold, t2_end)
        # 画板
        for (mx1, my1, mx2, my2, tw1, tw2, tw3, tw4, tw5, mtype) in poster_modules:
            if t < tw1 or t > tw5:
                continue
            progress = (t - tw1) / (tw5 - tw1)
            px1, py1, px2, py2 = mx1, my1 + get_scroll_y(t), mx2, my2 + get_scroll_y(t)
            if mtype == "ARTBOARD":
                if progress < 0.3:
                    draw.rectangle([px1, py1, px2, py2], outline=(55, 145, 255, 255), width=3)
                    dmx, dmy = (px1 + px2) // 2, (py1 + py2) // 2
                    draw.line([dmx - 20, dmy, dmx + 20, dmy], fill=(255, 80, 80, 200), width=2)
                    draw.line([dmx, dmy - 20, dmx, dmy + 20], fill=(255, 80, 80, 200), width=2)
                    draw.text((dmx + 6, dmy - 12), f"{px2-px1}x{py2-py1}", font=F_TINY, fill=(100, 180, 255, 255))
                elif progress < 0.55:
                    draw.rectangle([px1, py1, px2, py2], fill=(50, 50, 55, 200), outline=(70, 70, 80, 255), width=2)
                elif progress < 0.85:
                    overlay = poster_sketch.crop((0, int(get_scroll_y(t)), POSTER_W, min(POSTER_H, int(get_scroll_y(t)) + H - 52))).resize((px2-px1, py2-py1), Image.Resampling.LANCZOS)
                    scr.paste(overlay, (px1, py1), overlay)
                    draw.rectangle([px1, py1, px2, py2], outline=(28, 43, 69, 255), width=2)
                    draw.text((px1 + 8, py1 + 4), "SKETCH LAYER", font=F_TINY, fill=(130, 210, 255, 255))
                else:
                    overlay = poster_full.crop((0, int(get_scroll_y(t)), POSTER_W, min(POSTER_H, int(get_scroll_y(t)) + H - 52))).resize((px2-px1, py2-py1), Image.Resampling.LANCZOS)
                    scr.paste(overlay, (px1, py1))
                    if progress > 0.95:
                        for alx in range(px1, px2, 40):
                            draw.line([alx, py1, alx, py2], fill=(255, 60, 60, 120), width=1)
                        for aly in range(py1, py2, 40):
                            draw.line([px1, aly, px2, aly], fill=(255, 60, 60, 120), width=1)
            elif mtype == "HERO_GRAY":
                if progress < 0.4:
                    draw.rectangle([px1, py1, px2, py2], outline=(200, 200, 50, 200), width=3)
                    draw_selection_handles(draw, px1, py1, px2, py2, color=(200, 200, 50, 200), eight_handles=True)
                elif progress < 0.8:
                    draw.rectangle([px1, py1, px2, py2], fill=(40, 44, 52, 200), outline=(80, 80, 100, 255), width=2)
                else:
                    draw.rectangle([px1, py1, px2, py2], fill=(80, 90, 110, 150), outline=(200, 200, 50, 200), width=2)
            elif mtype == "COIN_SNAP":
                if progress < 0.3:
                    draw.rectangle([px1, py1, px2, py2], outline=(55, 145, 255, 255), width=2)
                elif progress < 0.6:
                    draw.rectangle([px1, py1, px2, py2], fill=(60, 65, 75, 200), outline=(80, 80, 100, 255), width=2)
                else:
                    draw.rectangle([px1, py1, px2, py2], fill=(60, 70, 90, 180), outline=(55, 145, 255, 255), width=2)
            elif mtype == "NORMAL":
                if progress < 0.3:
                    draw.rectangle([px1, py1, px2, py2], outline=(55, 145, 255, 255), width=2)
                elif progress < 0.6:
                    draw.rectangle([px1, py1, px2, py2], fill=(50, 55, 60, 180), outline=(70, 70, 80, 255), width=1)
                else:
                    draw.rectangle([px1, py1, px2, py2], fill=(55, 60, 70, 200), outline=(55, 145, 255, 100), width=1)
            elif mtype == "SEAMLESS":
                draw.rectangle([px1, py1, px2, py2], outline=(255, 255, 255, 40), width=1)
        # 光标
        cursor_positions = []
        for (mx1, my1, mx2, my2, tw1, tw2, tw3, tw4, tw5, mtype) in poster_modules:
            if t < tw1 or t > tw5:
                continue
            progress = (t - tw1) / (tw5 - tw1)
            if progress < 0.95:
                cx, cy = mx2, my2 + get_scroll_y(t)
                col = (255, 215, 85, 200) if progress < 0.2 else (255, 255, 255, 200)
                cursor_positions.append((cx, cy, "ARROW"))
                if progress < 0.15:
                    draw.ellipse([cx - 14, cy - 14, cx + 14, cy + 14], outline=(255, 215, 80, 150), width=2)
        # 确保至少35个光标
        for idx in range(max(0, 35 - len(cursor_positions))):
            bx = POSTER_X + np.random.randint(0, POSTER_W)
            by = POSTER_Y + np.random.randint(0, POSTER_H) + get_scroll_y(t)
            cursor_positions.append((bx, by, "CROSS"))
        for idx, (cx, cy, mode) in enumerate(cursor_positions[:N_CURSORS]):
            draw_cursor_icon(draw, cx, cy, mode)
        # 保存帧
        if preview_dir and frame_idx % 10 == 0:
            preview_path = os.path.join(preview_dir, f"frame_{frame_idx:05d}.png")
            scr.save(preview_path)
        scr_np = np.array(scr.convert("RGB"))
        out_video.write(cv2.cvtColor(scr_np, cv2.COLOR_RGB2BGR))

    out_video.release()
    if bgm_path:
        final_out = output_path.replace(".mp4", "_final.mp4")
        subprocess.run(["ffmpeg", "-i", output_path, "-i", bgm_path, "-shortest", "-c:v", "copy", "-c:a", "aac", "-b:a", "192k", final_out])
        os.replace(final_out, output_path)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="42光标Figma线稿到成品视频引擎")
    parser.add_argument("--poster", required=True, help="输入海报图片路径")
    parser.add_argument("--output", required=True, help="输出MP4路径")
    parser.add_argument("--title", default="DESIGN STUDIO 4K", help="视频标题")
    parser.add_argument("--handle", default="@Creator", help="创建者标识")
    parser.add_argument("--bgm", default=None, help="背景音乐路径")
    parser.add_argument("--preview-dir", default=None, help="预览帧输出目录")
    args = parser.parse_args()
    run_engine(args.poster, args.output, args.title, args.handle, args.bgm, args.preview_dir)

相关技能