// ===== demos/rhythm/quad-split-parallel-scenes/QuadSplitParallelScenes.tsx =====
// quad-split-parallel-scenes — Quad Split 四宫并行蒙太奇(motion-lab 定稿转原生 Remotion)
// 手法卡:画面硬切成 2×2 四宫格,四个象限并行跑各自独立的微场景(格内内容可任意替换,
// 此处仅为示例):TL 迷你浏览器打字 + 标签堆积 + 慢推,TR mono 打字 + whip 急推,
// BL 三词逐个弹入,BR pill 滑入 → 光标贝塞尔飞行点击 → 卡片弹出。
// 关键节拍互相错开 3-6 帧、全程无转场,靠并行密度制造信息轰炸。
// 设计坐标 480×270(DesignStage 等比放大),参数以此坐标系标定。
import React from 'react';
import { DesignStage, E, lerp, seg, useT } from '../../_fixtures/Motion';
export const QUAD_SPLIT_PARALLEL_SCENES_DURATION = 63; // 2100ms @30fps
const ACCENT = '#7c5cff';
const ACCENT_SOFT = '#c9bcff';
const F = 'system-ui,-apple-system,sans-serif';
// 四宫格底色走中性灰阶(深浅交替保证格线可读),需要主题色时换成项目色板
const BGS = ['#c3c6cc', '#f2f1ef', '#e7e6e3', '#b8bcc3'];
const TRAFFIC = ['#ff5f57', '#febc2e', '#28c840'];
const TABNAMES = ['Tab One', 'Tab Two', 'Tab Three', 'Tab Four', 'Tab Five', 'Tab Six'];
const TXT1 = 'Placeholder headline text';
const TXT2 = 'and a second line of copy';
const WORDS = ['One', 'clear', 'message'];
// 二次贝塞尔取点(BR 象限光标飞行路径,坐标单位 %)
const qBez = (a: number[], b: number[], c: number[], t: number): [number, number] => {
const u = 1 - t;
return [
u * u * a[0] + 2 * u * t * b[0] + t * t * c[0],
u * u * a[1] + 2 * u * t * b[1] + t * t * c[1],
];
};
// TL —— 迷你浏览器:逐字符打字 + tab outBack 弹入 + 全程 inQuad 慢推
const QuadTL: React.FC<{ t: number; frame: number }> = ({ t, frame }) => {
const n1 = Math.floor(seg(t, 0.02, 0.95) * TXT1.length);
return (
{TRAFFIC.map((c) => (
))}
{TABNAMES.map((n, i) => {
const k = seg(t, 0.08 + i * 0.13, 0.08 + i * 0.13 + 0.09, E.outBack);
return (
{n}
);
})}
◆
{TXT1.slice(0, n1)}
{/* 光标按帧闪烁:16 帧一个周期 */}
);
};
// TR —— mono 打字 + whip 急推(错拍:0.42-0.54,推近时带运动模糊)
const QuadTR: React.FC<{ t: number; frame: number }> = ({ t, frame }) => {
const n2 = Math.floor(seg(t, 0.06, 0.9) * TXT2.length);
const zip = seg(t, 0.42, 0.54, E.inOutCubic);
return (
{TRAFFIC.map((c) => (
))}
✦ Section label ›
{TXT2.slice(0, n2)}
{/* 光标错拍闪烁:与 TL 相位差 5 帧、周期 14 帧 */}
_
);
};
// BL —— 三词逐个 outBack 弹入(0.24 / 0.46 / 0.56,与其他象限错开)
const QuadBL: React.FC<{ t: number }> = ({ t }) => (
{WORDS.map((w, i) => {
const k = seg(t, [0.24, 0.46, 0.56][i], [0.24, 0.46, 0.56][i] + 0.1, E.outBack);
return (
{w}
);
})}
);
// BR —— 五步交互:pill 滑入 → 光标贝塞尔飞行 → 点击缩放 → 打字回复 → 卡片弹出
const QuadBR: React.FC<{ t: number }> = ({ t }) => {
const slide = seg(t, 0.12, 0.3, E.outBack);
const m1 = seg(t, 0.3, 0.42, E.inOutCubic);
const m2 = seg(t, 0.62, 0.74, E.inOutCubic);
// 光标路径:先飞向 pill,再二段飞向发送键
const p = m2 > 0 ? qBez([44, 66], [66, 52], [82, 68], m2) : qBez([88, 30], [50, 40], [44, 66], m1);
const c1 = seg(t, 0.42, 0.47);
const c2 = seg(t, 0.74, 0.79);
const n4 = Math.floor(seg(t, 0.46, 0.62) * 9);
const pop = seg(t, 0.8, 0.88, E.outBack);
return (
{/* 评论卡片:点击发送后弹出 */}
You · just now
All good!
{/* 输入 pill:从右侧滑入 */}
00:00
{t < 0.44 ? 'Leave your comment...' : 'All good!'.slice(0, n4)}
= 9 ? ACCENT : ACCENT_SOFT }}>➤
{/* 光标圆点:两段点击各缩一次 */}
);
};
export const QuadSplitParallelScenes: React.FC = () => {
const t = useT();
const frame = Math.floor(t * 63); // dur 2100ms @30fps
const scenes = [
,
,
,
,
];
return (
{scenes.map((scene, i) => (
> 1) * 50}%`,
width: '50%',
height: '50%',
overflow: 'hidden',
background: BGS[i],
}}
>
{scene}
))}
);
};