// ===== 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 (
);
})}
);
};