// ===== demos/typography/word-relay-filmstrip/WordRelayFilmstrip.tsx =====
// word-relay-filmstrip v3 —— 批次 12 单点微调:右侧大词块(Computer+动词)
// 的垂直中心与左列当前页面卡的垂直中点(y=540)对齐(top 462→402)。
// 其余沿用 v2:对照用户截图重做(perplexity-promo01,7 张):
// ① 左列页面卡黑白相间、每张等高(940x530,间距 105);
// ② 左列平时静止,只在右侧切词的窗口内滚动一格(滚动与切词同步);
// ③ 尺寸/字号/位置按截图量取:卡 x=106 宽 940,词右对齐至 x≈1710,
// Didot 系衬线 116px,"Computer" 固定第一行,动词第二行原位灰化淡出换词。
import React from 'react';
import { AbsoluteFill, interpolate, useCurrentFrame } 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 CARD_W = 940;
const CARD_H = 530;
const GAP = 105;
const STEP = CARD_H + GAP;
// —— 页面卡集合:黑白相间(奇偶强制交替),内容各异 ——
const DarkArticle: React.FC<{ seed: number }> = ({ seed }) => {
const rand = mulberry32(seed);
return (
{[0, 1, 2, 3].map((i) =>
)}
{Array.from({ length: 6 }).map((_, i) => (
))}
{Array.from({ length: 4 }).map((_, i) => (
))}
);
};
const LightMedal: React.FC<{ seed: number }> = ({ seed }) => {
const rand = mulberry32(seed);
return (
{[64, 96, 78, 110, 70, 88, 92].map((w, i) => (
))}
{[0, 1, 2].map((c) => (
))}
);
};
const DarkStats: React.FC<{ seed: number }> = ({ seed }) => {
const rand = mulberry32(seed);
return (
{[
{ n: 92, accent: false }, { n: 74, accent: false }, { n: 58, accent: false }, { n: 118, accent: true },
].map((row, i) => (
))}
{[0, 1, 2].map((i) => (
))}
);
};
const LightTable: React.FC<{ seed: number }> = ({ seed }) => {
const rand = mulberry32(seed);
return (
{Array.from({ length: 7 }).map((_, i) => (
))}
);
};
const DarkMri: React.FC = () => (
{Array.from({ length: 5 }).map((_, i) => (
))}
{[[120, 100], [200, 150], [96, 210]].map(([x, y], i) => (
))}
{Array.from({ length: 8 }).map((_, i) => (
))}
);
const LightPortfolio: React.FC = () => (
{[420, 470, 300].map((w, i) => (
))}
{[0, 1, 2, 3].map((i) => (
))}
);
// 黑白相间的固定顺序(奇偶交替)
const CARDS: React.FC<{ seed: number }>[] = [
({ seed }) => ,
({ seed }) => ,
() => ,
({ seed }) => ,
({ seed }) => ,
() => ,
];
const WORDS = ['researches', 'builds', 'codes'];
// 切词窗口:第一个词入场 f14–30;换词 f62–78、f108–124
const SWITCHES = [14, 62, 108];
const SW_DUR = 16;
const SERIF = '"Didot", "Bodoni 72", "Playfair Display", Georgia, serif';
export const WordRelayFilmstrip: React.FC = () => {
const frame = useCurrentFrame();
// —— 滚动步进:平时静止,仅在切词窗口内滚一格(ease-in-out)——
let stepF = 0;
SWITCHES.forEach((s) => {
const p = interpolate(frame, [s, s + SW_DUR], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
// easeInOutCubic
stepF += p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;
});
const scroll = stepF * STEP;
// 卡片布局:中心卡顶 y=275,向两侧铺开;黑白相间
const total = CARDS.length * STEP;
const cards: React.ReactNode[] = [];
for (let rep = -1; rep < 2; rep++) {
CARDS.forEach((C, i) => {
const y = 275 + i * STEP + rep * total - scroll;
if (y > 1200 || y < -CARD_H - 120) return;
cards.push(
,
);
});
}
// —— 右侧词接力:原位交叉淡(旧词灰化淡出,新词淡入,不位移)——
const wordStyle: React.CSSProperties = {
fontFamily: SERIF, fontWeight: 400, fontSize: 116, lineHeight: 1.18,
letterSpacing: '0.002em', textAlign: 'right', whiteSpace: 'nowrap',
};
// 每个词的生命周期:入场淡入(黑)→ 常驻黑 → 下个切点前 14 帧灰化
//(截图 4/6 捕捉到的就是"旧词已灰、新词未现"的状态)→ 切点起淡出。
const smooth = (x: number) => x * x * (3 - 2 * x);
const wordNodes = WORDS.map((w, i) => {
const sIn = SWITCHES[i];
const sOut = i + 1 < SWITCHES.length ? SWITCHES[i + 1] : null;
// 新词后半窗口淡入(避免与旧词叠影)
const pIn = interpolate(frame, [sIn + 7, sIn + SW_DUR], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
});
if (i === 0 ? frame < sIn : pIn <= 0) return null;
const pInEff = i === 0 ? interpolate(frame, [sIn, sIn + 12], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
}) : pIn;
// 灰化:切点前 14 帧开始,切点时已全灰
const grey = sOut ? interpolate(frame, [sOut - 14, sOut - 2], [0, 1], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
}) : 0;
// 淡出:切点起前半窗口内出完
const pOut = sOut ? interpolate(frame, [sOut, sOut + 8], [1, 0], {
extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
}) : 1;
const op = smooth(pInEff) * smooth(pOut);
if (op <= 0) return null;
const mix = smooth(grey);
const ch = Math.round(0x19 + (0x9d - 0x19) * mix);
return (
{w}
);
});
return (
{cards}
{/* 右侧词组:Computer 固定第一行,动词第二行原位换词。
v3:块总高≈137(Computer 行)+140(词行容器)=277,垂直中心须对齐
当前页面卡中点 y=540(卡顶 275 + 卡高 530/2)→ top=540-277/2≈402 */}
);
};