// 套印节拍泵(riso-beat-pump)——beat-punch-in(卡点顿推)× riso-misregistration-hit
// (套印错位)的组合节奏。节拍帧 [30,54,78,102](每 24f 一拍),每命中帧:
// ① 整画面 scale 一帧瞬跳 1.08(无渐入),14f 内按 exp(-t/3) 指数衰减回 1;
// ② 标题裂成 G.mid/G.ink 双色印版 multiply 错位,初始错位逐拍加码 4/7/11/16px
// (每版反向 → 总分离 8/14/22/32px),12f 衰减余弦震荡收敛套准;
// ③ 底部对应节拍刻度点闪深并常驻。结构:0–29f hold;30–115f 四拍;116–139f 真静止。
import React from 'react';
import { useCurrentFrame } from 'remotion';
import { G, Card, TitleBlock } from '../../_fixtures/Fixtures';
const HITS = [30, 54, 78, 102]; // 节拍命中帧
const AMP = [4, 7, 11, 16]; // 每拍单版初始错位(px),逐拍加码
const PUMP_WIN = 14; // scale 泵窗口:14f 后精确归 1(保证结尾真静止)
const SPLIT_WIN = 12; // 错位窗口:12f 后精确归 0(余量 <0.4px,硬切套准)
// 与 TitleBlock 同字形的单色印版(错位需要可调色副本)
const Plate: React.FC<{ color: string; dx: number; dy: number }> = ({ color, dx, dy }) => (
);
export const RisoBeatPump: React.FC = () => {
const frame = useCurrentFrame();
// 找最近一次已命中的节拍(24f 间隔 > 14f 窗口,永远只有一拍在作用)
let beatIdx = -1;
for (let i = 0; i < HITS.length; i++) {
if (frame >= HITS[i]) beatIdx = i;
}
const t = beatIdx >= 0 ? frame - HITS[beatIdx] : Infinity;
// ① 整画面泵:命中帧一帧到位 1.08(t=0 即满值,无渐入),指数衰减回 1
const pump = t < PUMP_WIN ? 1 + 0.08 * Math.exp(-t / 3) : 1;
// ② 标题错位:衰减余弦震荡(周期 6f 抖两下),窗口外精确 0 = 套准
const split = t < SPLIT_WIN;
const m = split ? Math.cos((2 * Math.PI * t) / 6) * Math.exp(-t / 3) : 0;
const dx = beatIdx >= 0 ? AMP[beatIdx] * m : 0;
const dy = dx * 0.45; // y 少量,更像没对准版
return (
{/* 整画面容器:scale 泵作用在全部内容上 */}
{/* 标题区:正体 / 双版错位互斥切换 */}
{!split && (
)}
{split && (
<>
>
)}
{/* 底下一排 3 张小卡 */}
{/* 节拍刻度:命中即闪深(8f 缩放脉冲 1.8→1)并常驻深色 */}
{HITS.map((hit, i) => {
const dt = frame - hit;
const on = dt >= 0;
const s = on && dt < 8 ? 1 + 0.8 * (1 - dt / 8) : 1;
return (
);
})}
);
};