// ===== demos/ui-entrance/element-body-moves/AxialStretch.tsx ===== // axial-stretch —— 轴向拉伸速度感 // 三张 Card 从右外依次横向飞入落位,飞行途中沿运动轴速度驱动拉伸 // (scaleX 峰值 ≈2.2 / scaleY ≈0.72,糖稀拉丝感),落点 Back.out 式回弹。 // 速度用位置差分 p(f)-p(f-1) 驱动,低于阈值不拉伸。收尾真静止 ≥35f。 import React from 'react'; import { useCurrentFrame, interpolate, Easing } from 'remotion'; import { G, Card, TitleBlock } from '../../_fixtures/Fixtures'; const W = 1920; const CARD_W = 380; const CARD_H = 230; const GAP = 60; const ROW_W = 3 * CARD_W + 2 * GAP; // 1260 const ROW_X0 = (W - ROW_W) / 2; // 330 const ROW_Y = (1080 - CARD_H) / 2; // 425 const START_X = 1980; // 完全在画面右外 const FLIGHT = 36; // 飞行帧数 const STAGGER = 12; // 错峰 const FIRST = 10; // 首卡起飞帧 const SQUASH = 8; // 落点回弹帧数 const VEL_MIN = 2; // px/frame,低于此不拉伸 const VEL_REF = 140; // px/frame,达到此速度即满拉伸 const STRETCH_X = 1.2; // scaleX 峰值 1 + 1.2 = 2.2 const SQUISH_Y = 0.28; // scaleY 谷值 1 - 0.28 = 0.72 // poly(4) in-out:中段速度峰值 ≈ 4× 平均速度,够冲 const flightEase = Easing.inOut(Easing.poly(4)); const posAt = (f: number, start: number, targetX: number): number => interpolate(f, [start, start + FLIGHT], [START_X, targetX], { easing: flightEase, extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const FlyCard: React.FC<{ i: number; frame: number }> = ({ i, frame }) => { const start = FIRST + i * STAGGER; const targetX = ROW_X0 + i * (CARD_W + GAP); const land = start + FLIGHT; const x = posAt(frame, start, targetX); // 速度 = 位置差分(帧时间解耦,纯由位置函数决定) const v = Math.abs(posAt(frame, start, targetX) - posAt(frame - 1, start, targetX)); const s = Math.min(Math.max((v - VEL_MIN) / (VEL_REF - VEL_MIN), 0), 1); const stretchX = 1 + STRETCH_X * s; const stretchY = 1 - SQUISH_Y * s; // 落点回弹:scaleX 过冲到 0.85 再回 1(横向被"撞停"压扁),8f const sqX = interpolate(frame, [land, land + SQUASH / 2, land + SQUASH], [1, 0.85, 1], { easing: Easing.out(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const sqY = interpolate(frame, [land, land + SQUASH / 2, land + SQUASH], [1, 1.1, 1], { easing: Easing.out(Easing.cubic), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); return (
); }; export const AxialStretch: React.FC = () => { const frame = useCurrentFrame(); const titleOp = interpolate(frame, [0, 10], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); return (
{/* 落位虚线槽,标出目标位置 */} {[0, 1, 2].map((i) => (
))} {[0, 1, 2].map((i) => ( ))}
); }; // ===== demos/ui-entrance/element-body-moves/ContactShadowLift.tsx ===== // contact-shadow-lift|接触阴影离面抬升 // 浅底上一排 3 张卡,逐张被"点名"抬起:卡 translateY(-28px)+scale(1.08), // 其正下方独立椭圆阴影同步放大变淡——纸片离桌感;落回时阴影收紧变实, // 落地 2f 卡壳 scale 0.99 微压。三张依次各来一遍。收尾真静止 ≥35f。 import React from 'react'; import { useCurrentFrame, interpolate, Easing } from 'remotion'; import { G, Card } from '../../_fixtures/Fixtures'; const outCubic = Easing.out(Easing.cubic); const inCubic = Easing.in(Easing.cubic); // 每张卡的局部时间轴(局部帧 t): // [0,10) 抬起 out-cubic // [10,28) 悬停 18f // [28,36) 落回 in-cubic // [36,38) 落地卡壳 scale 0.99 // [38,43) 回弹 0.99→1.0 out-cubic // t<0 或 t>=43 完全静止(rest 态) const LIFT_Y = -28; // 原案 -8 → 加码 -20 → QA 保险再到 -28 const LIFT_S = 1.08; const cardMotion = (t: number) => { const y = interpolate(t, [0, 10], [0, LIFT_Y], { easing: outCubic, extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }) + interpolate(t, [28, 36], [0, -LIFT_Y], { easing: inCubic, extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); let s: number; if (t < 28) { s = interpolate(t, [0, 10], [1, LIFT_S], { easing: outCubic, extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); } else if (t < 38) { s = interpolate(t, [28, 36], [LIFT_S, 0.99], { easing: inCubic, extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); } else { s = interpolate(t, [38, 43], [0.99, 1], { easing: outCubic, extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); } // 抬升进度 0(贴桌)→1(悬空),驱动阴影 const lift = interpolate(t, [0, 10], [0, 1], { easing: outCubic, extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }) - interpolate(t, [28, 36], [0, 1], { easing: inCubic, extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); return { y, s, lift }; }; const CARD_W = 360; const CARD_H = 220; const GAP = 120; const STARTS = [2, 42, 82]; // 三张卡依次点名,间隔 40f;末次动画止于 f125,留 35f 真静止 export const ContactShadowLift: React.FC = () => { const frame = useCurrentFrame(); const rowW = CARD_W * 3 + GAP * 2; const left0 = (1920 - rowW) / 2; const top = (1080 - CARD_H) / 2 - 20; return (
{/* 假 dashboard 式顶栏,给浅底一点场景感 */}
{[0, 1, 2].map((i) => { const t = frame - STARTS[i]; const { y, s, lift } = cardMotion(t); const x = left0 + i * (CARD_W + GAP); // 独立接触阴影(不是 box-shadow):卡正下方椭圆径向渐变 const shScale = 1 + 0.72 * lift; // 1.0 → 1.72(对比再拉大) const shOpacity = 0.55 - 0.37 * lift; // 0.55 → 0.18 const shW = CARD_W * 0.88; const shH = 44; return (
); })}
); };