// ===== demos/opening/magician-card-flourish/MagicianCardFlourish.tsx =====
// magician-card-flourish v6 —— 批次 15(无截图,用户意见即全部真相):
// 用户意见(逐字)"这个光需要带点辉光,0.8秒就行;然后十字星应该做成更符合
// 光学实感的,现在太5毛特效了;卡片最开始出来的时候,要做成那种弹射出来的
// 效果,就是刚开始弹射出来速度慢,然后划弧线的过程减速"
// ① 光效段 1.5s→0.8s(24f),辉光加厚(三层柔晕包裹);
// ② 十字星光学实感重做:等宽矩形条改衍射尖峰(径向渐变楔形——越远越细
// 且亮度指数渐隐),三层叠色(白细芯/蓝中层/宽淡外层近似色散),
// 旋转放慢(24f 转 ~40°,真实星芒旋转是镜头效应,快了就假);
// ③ 弹射出场:slow-in → burst → decelerating arc(起飞先慢速蓄力挤出,
// 急加速弹射,弧线段整体减速抵达中心硬定格)。
// v5 其余保留:中心极远飞出+13 圈自旋+94% 定格+sheen 扫光。总长 141 帧。
import React, { useId } from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { CameraMotionBlur } from '@remotion/motion-blur';
import { G } from '../../_fixtures/Fixtures';
const CARD_W = 380;
const CARD_H = 540;
// 对角线轴(卡片自身对角线方向,归一化)
const AX = CARD_W / Math.hypot(CARD_W, CARD_H);
const AY = CARD_H / Math.hypot(CARD_W, CARD_H);
// —— 时间轴(30fps / 126 帧)——
// v9.6(批次 18):光效 0.5s→0.3s(9f)
const TAKEOFF = 9; // 开场光效 0.3s(9f)后卡片才起飞
const FLASH_END = 12; // 闪光尾焰完全消失(坍缩略拖 3 帧)
const FLIGHT = 50; // 飞行帧数
const LAND = TAKEOFF + FLIGHT; // f=53 硬定格
const SHEEN_START = LAND + 8; // 定格稳住 8 帧后开始扫光
const SHEEN_DUR = 26; // 扫光时长(一次性)
const TURNS = 13; // 飞行总圈数(整数→定格瞬间恰好正面朝镜头)
const FINAL_SCALE = 1.88; // 终态:卡高 540×1.88≈1015 ≈ 94% 画面高(1080)
// 卡片正面:灰阶海报卡
const CardFace: React.FC = () => (
);
// 卡背:深灰斜纹
const CardBack: React.FC = () => (
);
// —— 开场闪光(v8,批次 17):以用户参考图 IMG_2505 为基准 ——
// 参考图特征(真实镜头 lens flare 星芒,密看归纳):
// ① X 形对角星芒(4 条 45° 斜向针状光束),不是 + 形正交;
// ② 光束极细(针状),其中一条对角轴显著更长、近乎横贯画面;
// ③ 核心小而极亮(过曝白点),辉光晕很克制(小半径即衰减入黑);
// ④ 青蓝色(参考图上星青色/下星蓝色,取青蓝中间调);
// ⑤ 背景纯黑,光束边缘干净锐利(真实衍射,无宽糊外层)。
// 时长 0.5s:渐亮(f0–4)→ 全芒微闪(f4–10)→ 坍缩(f10–15)。
const SpawnFlash: React.FC<{ f: number }> = ({ f }) => {
// 渐变 ID 按实例生成,多实例同场不串引(useId 的 «:» 在 url() 里非法,需清洗)
// hooks 必须在条件 return 之前调用
const SPIKE_ID = `mcf-needle-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
if (f > FLASH_END) return null;
const grow = interpolate(f, [0, 2.5], [0.2, 1], {
extrapolateRight: 'clamp', easing: Easing.out(Easing.quad),
});
const shrink = interpolate(f, [6, TAKEOFF, FLASH_END], [1, 0.3, 0.05], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.in(Easing.quad),
});
const s = grow * shrink;
// 亮度微闪(真实光源级细微抖动)
const flicker = 0.94 + 0.06 * (0.5 + 0.5 * Math.sin(f * 1.9) * Math.sin(f * 0.83 + 1.7));
const opacity = interpolate(f, [0, 2, 6, FLASH_END], [0, 1, 1, 0], {
extrapolateRight: 'clamp',
}) * flicker;
// v9:用户意见"开场光点的光芒要旋转90度"——闪光期间星芒动态旋转 90°
const rot = interpolate(f, [0, FLASH_END], [0, 90], { extrapolateRight: 'clamp', easing: Easing.inOut(Easing.quad) });
// 针状光束:极细楔形(根部窄、尖端归零)+ 亮度指数渐隐——对照参考图
// v9(批次 18):用户意见"开场光点的光芒要旋转90度"——整体转 90°:长轴 -38°→52°(陡斜近竖贯),短轴 52°→142°
const needle = (deg: number, len: number, w0: number, op: number) => (
);
return (
{/* v9.6 中心重做(对照用户参考图②):不是白色圆球——
极小过曝亮点 + 蓝色辉光 + 一圈放射状小光芒(短刺) */}
{/* 放射状小光芒:核心周围一圈短刺(参考图中心的 starburst 细节) */}
{/* X 形对角针状星芒——对照 IMG_2505 复刻要点:光束通体明亮饱和
(青蓝贯穿全长、只在最末端才灭),束身带同色辉光包裹 */}
);
};
const Scene: React.FC = () => {
const f = useCurrentFrame();
// 硬定格:闪光后 f=TAKEOFF 起飞,到达(f=LAND)后时间冻结——所有量按 tEff 计算
const tEff = Math.min(1, Math.max(0, (f - TAKEOFF) / FLIGHT));
const airborne = f >= TAKEOFF;
// —— 自旋(v7):用户意见"靠近镜头时,卡片自旋速度开始随距离靠近而衰减"
// 前 40% 行程近匀速极快,之后角速度随 tEff 持续衰减(easeOut 2.4 次幂),
// spinP(1)=1 保证整圈数——定格瞬间仍恰好正面朝镜头
const spinP = tEff < 0.4
? tEff * 1.55
: 0.62 + 0.38 * (1 - Math.pow(1 - (tEff - 0.4) / 0.6, 2.4));
const theta = TURNS * 360 * Math.min(1, spinP);
// —— 轨迹:画面中心的 3D 纵深远处(极小)→ 弧线飞向镜头 → 近满幅定格于中心 ——
// v6 弹射曲线(用户意见"刚开始弹射出来速度慢,然后划弧线的过程减速"):
// slow-in(前 14% 慢速蓄力挤出 6% 行程)→ 斜率跳变=弹射踢出 →
// 弧线段 easeOut(cubic) 全程减速抵达中心
const tp = tEff < 0.14
? 0.06 * (tEff / 0.14) * (tEff / 0.14)
: 0.06 + 0.94 * (1 - Math.pow(1 - (tEff - 0.14) / 0.86, 3));
const arc = Math.sin(tp * Math.PI);
const cx = 960 + arc * 360;
const cy = 540 - arc * 250;
// 纵深:真透视 scale=F/(F+z)×FINAL_SCALE,z 从极远推近到 0——
// 终态卡高≈94% 画面高(基本贴近上下界)
const FOCAL = 900;
const z = interpolate(tp, [0, 1], [14000, 0]);
const scale = FINAL_SCALE * (FOCAL / (FOCAL + z)); // 0.11 → 1.88,无过冲
// 背面判定
const facingBack = Math.cos((theta * Math.PI) / 180) < 0;
// —— 定格后 sheen 扫光:一道高光斜带从卡面左侧扫到右侧(一次性)——
const sheenP = interpolate(f, [SHEEN_START, SHEEN_START + SHEEN_DUR], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
easing: Easing.inOut(Easing.cubic),
});
const sheenVisible = f >= SHEEN_START && f <= SHEEN_START + SHEEN_DUR;
// 扫光经过时整卡亮度微抬升(正弦单拱),保证"整体光泽"肉眼可感
const sheenLift = sheenVisible ? 0.07 * Math.sin(sheenP * Math.PI) : 0;
return (
{/* —— 卡片:闪光过后从中心飞出,纯黑空间中飞行 —— */}
0 ? `brightness(${1 + sheenLift})` : undefined,
}}>
{/* 转动中的动态侧光(贴在卡面上,非环境光);定格后随 theta 冻结 */}
{/* 定格后的一次性 sheen 扫光:斜高光带左→右扫过整卡 */}
{sheenVisible && (
{/* 叠一层 screen 提亮,让灰色区块上的高光带更亮 */}
)}
);
};
// 闪光层独立于 CameraMotionBlur(细针光束旋转会被采样拆成条纹分身)
const FlashLayer: React.FC = () => {
const f = useCurrentFrame();
return (
);
};
export const MagicianCardFlourish: React.FC = () => (
<>
>
);