// ===== demos/typography/type-rhythm-sync/FontWeightPump.tsx =====
// 字重脉冲(font-weight-pump)——标题笔画随节拍变粗又弹回,像文字跟着低音鼓蹦迪。
// 节拍每 20f 一拍:帧 30/50/70/90/110 命中。命中瞬间 -webkit-text-stroke 0→10px
// 跳满,随后 10f 幂衰减 (1-t/10)^0.8 弹回 0;fontWeight 在命中窗口 400→900 离散跳变
// (env>0.15),配合 stroke 连续衰减读作"连续变粗"。第 3、5 拍(帧 70/110)为重音,
// 额外 scaleX 1→1.08 同衰减(transform 缩放不改排版)。底部 5 个节拍点作节拍参照。
// 结构:0–29f 静止 hold;30–119f 五拍脉冲;120–139f 真静止收尾(20f,110+10=120 无残留)。
import React from 'react';
import { useCurrentFrame } from 'remotion';
import { G } from '../../_fixtures/Fixtures';
const BEATS = [30, 50, 70, 90, 110];
const ACCENTS = new Set([2, 4]); // 第 3、5 拍重音
const DECAY = 10; // 衰减帧数
// 命中后 10f 幂衰减包络:t=0 → 1,t>=10 → 精确 0(保证结尾真静止)
const envAt = (frame: number, beat: number) => {
const t = frame - beat;
if (t < 0 || t >= DECAY) return 0;
return Math.pow(1 - t / DECAY, 0.8);
};
export const FontWeightPump: React.FC = () => {
const frame = useCurrentFrame();
// 只可能有一个活跃拍(拍距 20f > 衰减 10f),取最大包络及其拍序号
let env = 0;
let activeBeat = -1;
BEATS.forEach((b, i) => {
const e = envAt(frame, b);
if (e > env) {
env = e;
activeBeat = i;
}
});
const strokeW = 10 * env; // 笔画粗细连续衰减
const weight = env > 0.15 ? 900 : 400; // 命中窗口离散跳字重
const accent = activeBeat >= 0 && ACCENTS.has(activeBeat);
const scaleX = accent ? 1 + 0.08 * env : 1; // 重音拍变宽一挡
return (
{/* 定宽居中容器,transform 缩放不改排版 */}
{/* 底部节拍点:5 个,命中哪拍哪个点闪 ink 并放大 */}
{BEATS.map((b, i) => {
const e = envAt(frame, b);
const dotOpacity = e > 0.02 ? Math.min(1, 0.3 + e * 1.2) : 0;
const dotScale = 1 + 0.8 * e;
return (
);
})}
);
};
// ===== demos/typography/type-rhythm-sync/KaraokeFillSync.tsx =====
// 卡拉OK填色随读(karaoke-fill-sync)——旁白读到哪个词,哪个词就被深色从左到右
// 点亮。两行标语 "SHIP FASTER / BREAK NOTHING",每个词双层同文本叠放:底层 G.line
// 浅灰字,上层 G.ink 深字用 clip-path: inset(0 X% 0 0) 按词内进度线性填充(逐词独立
// 叠层,clip 百分比即词内进度,无需量测词在整行的像素占比)。词级时间表模拟语速:
// SHIP 20–38、FASTER 42–75(长词慢读)、BREAK 85–103、NOTHING 107–130,词间停顿。
// 正在填的词底下有 8px 深色下划线跟随填充右缘作读指。0–19f hold;130–149f 真静止。
import React from 'react';
import { useCurrentFrame, interpolate } from 'remotion';
import { G } from '../../_fixtures/Fixtures';
type Word = { text: string; start: number; end: number };
const LINES: Word[][] = [
[
{ text: 'SHIP', start: 20, end: 38 },
{ text: 'FASTER', start: 42, end: 75 },
],
[
{ text: 'BREAK', start: 85, end: 103 },
{ text: 'NOTHING', start: 107, end: 130 },
],
];
const KaraokeWord: React.FC<{ word: Word; frame: number }> = ({ word, frame }) => {
// 词内 linear 填充进度,clamp 保证读完保持
const p = interpolate(frame, [word.start, word.end], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const active = frame >= word.start && frame < word.end; // 正在读这个词
return (
{/* 底层:浅灰未读字 */}
{word.text}
{/* 上层:深字按进度从左到右揭开 */}
{word.text}
{/* 读指下划线:只在正在填的词下出现,右缘跟随填充进度 */}
{active && (
)}
);
};
export const KaraokeFillSync: React.FC = () => {
const frame = useCurrentFrame();
return (
{LINES.map((words, li) => (
{words.map((w) => (
))}
))}
);
};