// ===== 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 细节) */} {[15, 52, 88, 123, 160, 197, 231, 268, 305, 341].map((deg, i) => { const ln = 46 + ((i * 37) % 3) * 16; return ( ); })} {/* X 形对角针状星芒——对照 IMG_2505 复刻要点:光束通体明亮饱和 (青蓝贯穿全长、只在最末端才灭),束身带同色辉光包裹 */} {/* v9.6:更纯蓝(参考图光束是饱和蓝,白段只在根部一瞬) */} {/* 长对角轴:辉光包层(宽糊、同色发光)+ 明亮主束 + 过曝细芯 */} {needle(-38, 826, 26, 1)} {needle(-38, 840, 8, 1)} {needle(-38, 819, 3, 1)} {/* 短对角轴:同三层 */} {needle(52, 413, 20, 1)} {needle(52, 420, 6.5, 1)} {needle(52, 410, 2.6, 1)}
); }; 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 = () => ( <> );