// ===== demos/effects/fui-hud-moves/LineUnfoldPanel.tsx =====
// line-unfold-panel —— 一线展面(Jarvis/FUI 母题)
// 暗底。入场两拍:3px 细线从中点向两侧极快抽出(5f)→ 定宽后纵向
// 撑开成 Card 面板(9f,out 缓动)→ 内容延迟淡入。
// 静置展示后反向退场:压扁成线(7f)→ 线缩成点 → 熄灭,像老 CRT 关机。
// f0–12 空场静置;入场 f12–34;持面板至 f78;退场 f78–98;末静止 ≥42f(140f)。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, Card } from '../../_fixtures/Fixtures';
const PANEL_W = 760;
const PANEL_H = 460;
const CX = 960;
const CY = 540;
// —— 入场时间表 ——
const T0 = 12; // 点亮起点
const LINE_END = T0 + 5; // 线抽出完成 f17
const UNFOLD_END = LINE_END + 9; // 面板撑开完成 f26
const CONTENT_END = UNFOLD_END + 8; // 内容淡入完成 f34
// —— 退场时间表 ——
const OUT0 = 78; // 开始压扁
const COLLAPSE_END = OUT0 + 7; // 压成线 f85
const SHRINK_END = COLLAPSE_END + 6; // 线缩成点 f91
const OFF = SHRINK_END + 4; // 点熄灭 f95
const clamp = { extrapolateLeft: 'clamp' as const, extrapolateRight: 'clamp' as const };
export const LineUnfoldPanel: React.FC = () => {
const frame = useCurrentFrame();
// 入场:scaleX(线抽出)快进快停,入场后保持 1
const inSX = interpolate(frame, [T0, LINE_END], [0.004, 1], {
easing: Easing.out(Easing.poly(4)),
...clamp,
});
// 入场:scaleY(纵向撑开),线阶段压在 3px
const inSY = interpolate(frame, [LINE_END, UNFOLD_END], [3 / PANEL_H, 1], {
easing: Easing.out(Easing.cubic),
...clamp,
});
// 内容淡入(面板撑开过半才开始)
const contentOp = interpolate(frame, [UNFOLD_END - 3, CONTENT_END], [0, 1], {
easing: Easing.out(Easing.quad),
...clamp,
});
// 退场:先压 Y 回线,再缩 X 回点
const outSY = interpolate(frame, [OUT0, COLLAPSE_END], [1, 3 / PANEL_H], {
easing: Easing.in(Easing.cubic),
...clamp,
});
const outSX = interpolate(frame, [COLLAPSE_END, SHRINK_END], [1, 0.004], {
easing: Easing.in(Easing.poly(4)),
...clamp,
});
// 内容在压扁前先撤
const contentOutOp = interpolate(frame, [OUT0 - 4, OUT0 + 2], [1, 0], clamp);
const sx = frame < OUT0 ? inSX : outSX;
const sy = frame < OUT0 ? inSY : outSY;
// 末点熄灭:opacity 快落。f >= OFF 后整个元素条件卸载 → 真静止
const dotOp = interpolate(frame, [SHRINK_END, OFF], [1, 0], {
easing: Easing.in(Easing.quad),
...clamp,
});
const alive = frame >= T0 && frame < OFF;
// 面板阶段(sy 足够大)显示卡片内容;线/点阶段显示发光条
const isPanel = sy > 0.15;
return (
LINE UNFOLD PANEL
{alive && (
{isPanel ? (
<>
{/* 内容层单独控 opacity:盖一层暗板模拟"内容未亮" */}
>
) : (
// 线/点阶段:白色发光条填满整个盒(被 scale 压成线)
)}
)}
);
};
// ===== demos/effects/fui-hud-moves/ReticleLockOn.tsx =====
// reticle-lock-on —— 准星咬合(钢铁侠 HUD / 安德的游戏)
// FakeDashboard 静置。四个 L 形角标组成的取景框从画外飞入(大框),
// 超调回弹后收缩贴紧目标卡片四角"咔"地咬合定格;咬合帧卡片微亮 +
// 旁弹一行小标签。画面不冻结,是运动中的捕获。
// f0–14 面板静置;f14–24 飞入;f24–46 收缩(含过冲回弹);f46 咬合;
// 标签 f46–56 弹出;f56 后真静止 ≥84f(140f 总长)。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, FakeDashboard } from '../../_fixtures/Fixtures';
// 目标:variant A 网格第 2 张卡(第一行中间)。
// 布局推导:侧栏 220 + padding 36,网格 3 列 gap 28,
// 内容区宽 1700-72=1628 → 卡宽 (1628-56)/3 ≈ 524,卡高 (1080-72-72-28)/2 ≈ 454...
// 实测近似:目标框取第一行中间卡的外接矩形(含少量呼吸边距)。
const CARD_X = 220 + 36 + 524 + 28; // ≈ 808
const CARD_Y = 72 + 36; // 108
const CARD_W = 524;
const CARD_H = 425;
const PAD = 14; // 咬合后角标与卡的呼吸距
const TCX = CARD_X + CARD_W / 2;
const TCY = CARD_Y + CARD_H / 2;
const FLY_IN = 14; // 飞入开始
const FLY_END = 24; // 飞入结束(大框就位)
const LOCK_END = 46; // 咬合帧
const LABEL_END = 56;
const ARM = 56; // L 臂长
const THICK = 10; // L 粗
const clamp = { extrapolateLeft: 'clamp' as const, extrapolateRight: 'clamp' as const };
export const ReticleLockOn: React.FC = () => {
const frame = useCurrentFrame();
// 飞入:整框从右下画外冲进来,同时是个放大 2.2× 的大框
const flyT = interpolate(frame, [FLY_IN, FLY_END], [0, 1], {
easing: Easing.out(Easing.cubic),
...clamp,
});
const flyDX = (1 - flyT) * 1100;
const flyDY = (1 - flyT) * 620;
// 收缩咬合:半宽/半高从 2.2× 收到贴紧,带超调(收过头 6% 再弹回)
const shrink = interpolate(
frame,
[FLY_END, LOCK_END - 8, LOCK_END - 3, LOCK_END],
[2.2, 1.06, 0.94, 1],
{ easing: Easing.inOut(Easing.cubic), ...clamp },
);
const hw = (CARD_W / 2 + PAD) * shrink;
const hh = (CARD_H / 2 + PAD) * shrink;
// 咬合帧:卡片微亮(一层白色 overlay 快闪后停在 0.28)
const glow = interpolate(frame, [LOCK_END, LOCK_END + 3, LOCK_END + 10], [0, 0.55, 0.28], clamp);
// 标签:咬合后从角标右上弹出(scaleX 展开 + 淡入)
const labelT = interpolate(frame, [LOCK_END + 2, LABEL_END], [0, 1], {
easing: Easing.out(Easing.back(1.8)),
...clamp,
});
const showReticle = frame >= FLY_IN;
const locked = frame >= LOCK_END;
// 四个 L 角:位置 = 中心 ± (hw, hh),各自镜像
const corners = [
{ x: TCX - hw, y: TCY - hh, sx: 1, sy: 1 },
{ x: TCX + hw, y: TCY - hh, sx: -1, sy: 1 },
{ x: TCX - hw, y: TCY + hh, sx: 1, sy: -1 },
{ x: TCX + hw, y: TCY + hh, sx: -1, sy: -1 },
];
return (
{/* 咬合微亮层:条件渲染,锁定前不存在 */}
{locked && (
)}
{showReticle && (
{corners.map((c, i) => (
))}
{/* 小标签:右上角标外侧弹出 */}
{frame >= LOCK_END + 2 && (
TARGET · CARD_02
)}
)}
);
};