// ===== demos/effects/riso-print-hits/RisoBeatPump.tsx =====
// 套印节拍泵(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 (
);
})}
);
};
// ===== demos/effects/riso-print-hits/RisoMisregistrationHit.tsx =====
// 套印错位冲击帧(riso-misregistration-hit)——标题撞停瞬间裂成两份单色"印版"
// (浅灰琥珀版 G.mid + 深墨版 G.ink,mix-blend-mode: multiply 叠加),像 riso 印刷
// 没对准版;两版反向错位(x 为主 y 少量)做衰减震荡 offset = A*cos(ωt)*exp(-t/τ)
// 抖两下,帧 72 啪地硬切回单一正体(套准合一),带 4f scale 1.03→1 脉冲收束。
// 结构:0–19f 空场 hold(只有底部装饰线);20–28f 标题从右画外 Easing.in(cubic)
// 撞入屏心(帧 28 命中);28–71f 双版错位震荡;72–75f 套准脉冲;76–119f 真静止 44f。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, TitleBlock } from '../../_fixtures/Fixtures';
const HIT = 28; // 撞停命中帧
const SNAP = 72; // 套准合一帧
const AX = 16; // 单版 x 错位振幅(两版反向 → 总分离 ~32px,肉眼明显)
const AY = 7; // 单版 y 错位振幅(少量,更像没对准版)
const OMEGA = (2 * Math.PI) / 18; // 震荡周期 18f,44f 内抖两下半
const TAU = 60; // 缓衰减:帧 72 前仍余 ~14px 总分离,被"啪地"硬切归零
// 与 TitleBlock 同字形的可调色标题(错位印版需要单色副本)
const Plate: React.FC<{ color: string; dx: number; dy: number }> = ({ color, dx, dy }) => (
);
export const RisoMisregistrationHit: React.FC = () => {
const frame = useCurrentFrame();
// 阶段判定
const entering = frame >= 20 && frame < HIT; // 撞入
const split = frame >= HIT && frame < SNAP; // 双版错位震荡
const showSingle = frame < HIT || frame >= SNAP; // 正体(画外/撞入/套准后)
// 撞入位移:右画外 1400px → 0,8f Easing.in(cubic)(加速撞停)
const slideX = interpolate(frame, [20, HIT], [1400, 0], {
easing: Easing.in(Easing.cubic),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
// 错位震荡包络:t 自命中起,衰减余弦(帧 72 前仍有可见残余,硬切归零成"啪")
const t = frame - HIT;
const m = split ? Math.cos(OMEGA * t) * Math.exp(-t / TAU) : 0;
const dx = AX * m;
const dy = AY * m;
// 套准合一脉冲:帧 72 起 4f scale 1.03 → 1,之后精确 1(保证结尾真静止)
const pulse =
frame >= SNAP && frame < SNAP + 4 ? 1 + 0.03 * (1 - (frame - SNAP) / 4) : 1;
return (
{/* 底部装饰线:全程静止的布景锚点 */}
{/* 正体:画外等待 / 撞入 / 套准后(撞入前 frame<20 时在画外,视觉等同空场) */}
{showSingle && (
)}
{/* 双版错位:浅灰版与深墨版反向偏移,multiply 叠加出"重影套印" */}
{split && (
<>
>
)}
);
};