// ===== demos/rhythm/beat-step-list-theme-cycle/BeatStepListThemeCycle.tsx ===== // beat-step-list-theme-cycle(bear 22.3–24.6s) // 深色场形容词列表逐拍步进:每拍列表上移一行 + 选中胶囊跳到下一行并换色 // (绿→紫→红),整场底色同步换(深棕→深紫→深藏青)。 // 一拍之内"行、色、场"三通道同步跳变,~0.6s 一拍。 import React from 'react'; import { AbsoluteFill, interpolate, useCurrentFrame } from 'remotion'; const FONT = 'Helvetica, Arial, sans-serif'; const ROW_H = 150; const WORDS = ['modern', 'playful', 'expressive', 'seamless', 'intuitive']; // 拍点:~0.6s 一拍 = 18 帧 const BEAT_LEN = 18; const FIRST_BEAT = 30; // 前 1s 静置铺垫 const N_BEATS = 3; // 每拍的(胶囊色 / 场底色)——起始态 + 三拍 const THEMES = [ { pill: '#d8d8d4', bg: '#241a12', ink: '#2a2018' }, // modern:灰白胶囊 / 深棕场 { pill: '#4fae62', bg: '#1e2416', ink: '#173015' }, // playful:绿 { pill: '#8e6fd8', bg: '#221a33', ink: '#2a2144' }, // expressive:紫 { pill: '#d64d55', bg: '#141c2e', ink: '#1a2440' }, // seamless:红 / 深藏青 ]; // 拍内跳变:5 帧内完成,ease-out + 微过冲 const snap = (t: number) => interpolate(t, [0, 1], [0, 1], { easing: (x) => 1 - Math.pow(1 - x, 3.2), extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); export const BeatStepListThemeCycle: React.FC = () => { const frame = useCurrentFrame(); // 当前拍序号与拍内进度(跳变只占拍头 6 帧) const raw = (frame - FIRST_BEAT) / BEAT_LEN; const beat = Math.min(N_BEATS, Math.max(0, Math.floor(raw) + 1)); // 已触发的拍数 const beatStartFrame = FIRST_BEAT + (beat - 1) * BEAT_LEN; const tInBeat = beat === 0 ? 1 : snap((frame - beatStartFrame) / 6); // 连续步进量:整数拍 + 拍头 6 帧内的插值 const step = beat === 0 ? 0 : (beat - 1) + tInBeat; // 三通道 —— 1) 列表上移一行 const listY = -step * ROW_H; // 2) 胶囊换色(拍头硬跳,带一点 cross-fade) const themePrev = THEMES[Math.max(0, beat - 1)]; const themeNow = THEMES[beat]; const mixT = beat === 0 ? 1 : tInBeat; const mix = (a: string, b: string, t: number) => { const pa = [1, 3, 5].map((i) => parseInt(a.slice(i, i + 2), 16)); const pb = [1, 3, 5].map((i) => parseInt(b.slice(i, i + 2), 16)); return `rgb(${pa.map((v, i) => Math.round(v + (pb[i] - v) * t)).join(',')})`; }; const pillColor = mix(themePrev.pill, themeNow.pill, mixT); const bgColor = mix(themePrev.bg, themeNow.bg, mixT); // 胶囊每拍落位时 squash 弹一下 const pop = beat === 0 ? 1 : interpolate(tInBeat, [0, 0.6, 1], [1.12, 0.97, 1]); // 选中行文字反白判定:胶囊固定在视口中央行,选中词 = WORDS[beat] const selectedIdx = beat; return ( {/* 中央固定胶囊(列表在其下滚动,视觉上"胶囊跳到下一行") */}
{/* 词列表 */}
{WORDS.map((w, i) => { const isSel = i === selectedIdx; return (
{w}
); })}
{/* 视口上下羽化 */}
); };