// card-flock-tumble v5 —— 批次 14 按用户意见再改(承 v4):
// 用户意见(逐字):"首先三个页面不要加模糊效果;然后那个水波扩散一个
// 圆环就够,不是多个;但是效果你截图看看原片的样式,要一样"
// 落实:①三卡全程清晰——翻飞中段 motion blur、收束段 blur 全删;
// ②多圈同心水波删掉,只留一个环;③环样式对照原片密帧(12fps)重做:
// 原片是单个湍流烟雾环——边缘破碎起絮、环身明暗斑块交错、粉紫为主
// 顶部偏奶桃色、出现后减速外扩且全程缓慢长大、衰减极慢(片尾仍在,
// 弥散变淡而非熄灭)、中心无水面光。用 feTurbulence+位移贴图模拟。
import React, { useId } from 'react';
import { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion';
const mulberry32 = (a: number) => () => {
let t = (a += 0x6d2b79f5);
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
const FONT = '"Avenir Next", Futura, "Helvetica Neue", sans-serif';
// ---------- 时间轴(30fps / 130f) ----------
const WALL_UP = [6, 22] as const; // 墙亮起
const FLIGHT = [10, 54] as const; // 侧棱→翻飞→站定:一条连续样条
const CARD_OUT = [62, 72] as const; // 收束(快!10 帧向中心聚拢)
const RING_T0 = 70; // 波纹环自收束点扩散
const TEXT_T0 = 84; // STRONGER 出现
// ---------- 霓虹描边文字墙(S2:黄左→品红→紫→蓝右) ----------
const ROW_GRADS: [string, string][] = [
['#ffe14d', '#5ad0ff'],
['#ff5ad0', '#7d8bff'],
['#ffb84d', '#b46bff'],
];
const NeonWall: React.FC<{ frame: number }> = ({ frame }) => {
const up = interpolate(frame, [WALL_UP[0], WALL_UP[1]], [0.12, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const out = interpolate(frame, [CARD_OUT[0], CARD_OUT[1] + 4], [1, 0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const blurOut = interpolate(frame, [CARD_OUT[0], CARD_OUT[1] + 4], [0, 26], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
if (out <= 0.01) return null;
const drift = frame * 2.0;
return (
{Array.from({ length: 3 }).map((_, row) => {
const [cL, cR] = ROW_GRADS[row];
return (
{Array.from({ length: 4 }).map((_, i) => (
FASTER
))}
);
})}
);
};
// ---------- ClickUp 风格 UI 卡 ----------
const UiCard: React.FC<{ seed: number; title: string }> = ({ seed, title }) => (
{Array.from({ length: 8 }).map((_, i) => (
))}
{title}
{Array.from({ length: 6 }).map((_, i) => (
))}
);
// ---------- 单个湍流烟雾环(对照原片密帧重做) ----------
// 原片形态:只有一个环;边缘破碎起絮(湍流位移)、环身明暗斑块交错;
// 粉紫为主、顶部奶桃色;出现后减速外扩且全程缓慢长大;衰减极慢——
// 片尾仍清晰可见(弥散变淡而非熄灭);中心无水面光、背景纯黑。
const SmokeRing: React.FC<{ frame: number }> = ({ frame }) => {
// 滤镜/渐变 ID 按实例生成,多实例同场不串引(useId 的 «:» 在 url() 里非法,需清洗)
// hooks 必须在条件 return 之前调用
const uid = useId().replace(/[^a-zA-Z0-9]/g, '');
const t = frame - RING_T0;
if (t < 0) return null;
// 半径:小出现→减速外扩,之后仍缓慢长大(原片全程未停)
const grow = Easing.out(Easing.cubic)(Math.min(1, t / 52));
const R = 46 + 330 * grow + Math.max(0, t - 52) * 1.6;
// 衰减极慢:快速浮现后长期驻留,只轻微变淡弥散
const op = interpolate(t, [0, 5, 30, 60], [0, 1, 0.92, 0.72], { extrapolateRight: 'clamp' });
// 环身宽度:随半径同步长大,相对占比略微摊薄(早期 ~R/3)
const w = R * interpolate(t, [0, 52], [0.36, 0.27], { extrapolateRight: 'clamp' });
// 湍流位移幅度随环长大而放大(絮块尺寸跟环成比例)
const disp = 60 + grow * 90;
const rot = t * 0.35; // 纹理缓慢流动感
return (
);
};
// ---------- 卡片位姿:Catmull-Rom 样条连续插值(真 3D 丝滑转动) ----------
type Pose = { x: number; y: number; rx: number; ry: number; rz: number; s: number };
const POSE_KEYS: (keyof Pose)[] = ['x', 'y', 'rx', 'ry', 'rz', 's'];
// Catmull-Rom(端点重复),保证经过所有关键姿态且导数连续 → 无分段突变
const catmull = (p0: number, p1: number, p2: number, p3: number, t: number): number => {
const t2 = t * t;
const t3 = t2 * t;
return (
0.5 *
(2 * p1 +
(-p0 + p2) * t +
(2 * p0 - 5 * p1 + 4 * p2 - p3) * t2 +
(-p0 + 3 * p1 - 3 * p2 + p3) * t3)
);
};
const splinePose = (keys: Pose[], u: number): Pose => {
// keys.length === 3:两段样条,0.55 处过中间关键帧
const seg = u < 0.55 ? 0 : 1;
const lt = seg === 0 ? u / 0.55 : (u - 0.55) / 0.45;
const out = {} as Pose;
for (const k of POSE_KEYS) {
const v0 = keys[Math.max(0, seg - 1)][k];
const v1 = keys[seg][k];
const v2 = keys[seg + 1][k];
const v3 = keys[Math.min(2, seg + 2)][k];
out[k] = catmull(v0, v1, v2, v3, lt);
}
return out;
};
// 三卡(v4 再加码):间距再收紧(±125/±85 级,三卡大幅重叠成一叠阶梯)
// + 站定再放大(s≈1.5-1.6,对齐截图 3/4 里卡片群占满画面中部)
// k0 侧棱(近 90° 薄边)→ k1 翻飞中段 → k2 阶梯站定
const CARDS: { title: string; k: [Pose, Pose, Pose]; conv: Pose }[] = [
{
title: 'Inbox',
k: [
{ x: -8, y: -16, rx: 9, ry: 88, rz: 12, s: 1.05 },
{ x: -135, y: -92, rx: 16, ry: 44, rz: -8, s: 1.38 },
{ x: -108, y: -76, rx: 4, ry: 13, rz: -2, s: 1.62 },
],
conv: { x: -20, y: -12, rx: 0, ry: 55, rz: 4, s: 0.12 },
},
{
title: 'List view',
k: [
{ x: 0, y: 0, rx: 8, ry: 89, rz: 12, s: 1.0 },
{ x: -16, y: -7, rx: 13, ry: 38, rz: -7, s: 1.42 },
{ x: -5, y: -2, rx: 3, ry: 11, rz: -2, s: 1.68 },
],
conv: { x: 0, y: 0, rx: 0, ry: 60, rz: 4, s: 0.12 },
},
{
title: 'Home',
k: [
{ x: 8, y: 16, rx: 7, ry: 90, rz: 12, s: 0.95 },
{ x: 112, y: 78, rx: 11, ry: 34, rz: -6, s: 1.46 },
{ x: 90, y: 70, rx: 2, ry: 9, rz: -1, s: 1.74 },
],
conv: { x: 15, y: 10, rx: 0, ry: 65, rz: 4, s: 0.12 },
},
];
const lerpPose = (a: Pose, b: Pose, t: number): Pose => {
const out = {} as Pose;
for (const k of POSE_KEYS) out[k] = a[k] + (b[k] - a[k]) * t;
return out;
};
export const CardFlockTumble: React.FC = () => {
const frame = useCurrentFrame();
// 渐变 ID 按实例生成,多实例同场不串引
const gradId = `strokeGrad-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
// STRONGER 巨字
const st = frame - TEXT_T0;
const textScale = interpolate(st, [0, 34], [0.6, 1.0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
});
const textOpacity = interpolate(st, [0, 12, 34], [0, 0.8, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
return (
{/* 卡片群 */}
{frame < CARD_OUT[1] + 2 && (
{CARDS.map((c, i) => {
let pose: Pose;
let op = 1;
// v6(批次 15):用户意见"三个页面收缩之前不要停住,保持旋转
// (只不过旋转放慢"——站定后到收缩前 idle 慢转(低角速度连续
// 漂移,平方缓入避免与样条衔接处顿挫),收缩从漂移后姿态出发
const idleAt = (f: number) => {
const t = Math.min(f, CARD_OUT[0]) - FLIGHT[1] * 0.86;
if (t <= 0) return { ry: 0, rx: 0, rz: 0 };
const ramp = Math.min(1, t / 14) ** 2;
return { ry: t * 0.34 * ramp, rx: t * -0.1 * ramp, rz: t * 0.05 * ramp };
};
const drift = idleAt(frame);
if (frame < FLIGHT[0]) {
pose = c.k[0];
} else if (frame < CARD_OUT[0]) {
// 全程一条样条:侧棱→翻飞→站定,导数连续,丝滑 3D 转动
// (用户意见①:页面全程清晰,翻飞段 motion blur 已删)
const raw = Math.min(1, (frame - FLIGHT[0]) / (FLIGHT[1] - FLIGHT[0]));
const u = Easing.out(Easing.cubic)(raw);
pose = splinePose(c.k, u);
pose = { ...pose, ry: pose.ry + drift.ry, rx: pose.rx + drift.rx, rz: pose.rz + drift.rz };
} else {
// 收束:快!10 帧加速向中心聚拢缩小(收束段 blur 也删,全程清晰);
// 透明度前 60% 保持满格,让"吸入中心"的运动清晰可读
const r = Math.min(1, (frame - CARD_OUT[0]) / (CARD_OUT[1] - CARD_OUT[0]));
const e = Easing.in(Easing.quad)(r);
const from = { ...c.k[2], ry: c.k[2].ry + drift.ry, rx: c.k[2].rx + drift.rx, rz: c.k[2].rz + drift.rz };
pose = lerpPose(from, c.conv, e);
op = 1 - Easing.in(Easing.cubic)(Math.max(0, (r - 0.55) / 0.45));
}
return (
);
})}
)}
{/* 单个湍流烟雾环(对照原片密帧样式) */}
{/* STRONGER 巨字横贯全屏(S6) */}
{st >= 0 && (
)}
);
};