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条核心强制铁律)
-
强制同屏42个并发工作光标(
N_CURSORS = 42):每帧同时显示35至42个光标,涵盖白色箭头(ARROW)、十字准星(CROSS +)与工字打字光标(IBEAM I),起止帧带黄色点击波纹圈(#ffd750)。 -
强制双级手柄优先级分配(
primary_points + secondary_points):第一优先级先为屏幕左右两侧每一个活跃选框的右下角主手柄□或打字竖线|分配1个光标(消灭「无主框」);第二优先级再将剩余光标分配给各框的左上、右上、上中、右中手柄,实现多光标协同拉框。 -
强制零残影直接锁点(零空中延迟):严禁正弦波游荡,严禁跨框慢速飞行飘移。光标直接锁定当前活跃手柄实时坐标
tx, ty,确保100%的帧里每个光标尖端都咬住白色手柄方块□或打字竖线|。 -
强制高并发重叠时间轴(每波次14至16个模块同时施工):全片23.5秒分为4个波次,各模块启动时间仅错开
0.11s,保证画面随时有15个以上选框同步演进(线框拉出 -> 灰块占位 -> 蓝图线稿 -> 全彩对齐吸附)。 -
强制纯粹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)