// pill-slot-cycle —— 句中词槽轮换:固定句干 + 句尾 pill 老虎机式滚一格
// 源:notion-ai 4.5–8.5s。旧 pill 上飞淡出、新 pill 从下带运动模糊滑入,
// 连换 6 次后 pill 消失、句子落成 "One AI tool to do it all." 收束。
import React from 'react';
import { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion';
import { G } from '../../_fixtures/Fixtures';
const FONT = 'Helvetica, Arial, sans-serif';
const PILLS = [
{ label: 'Ask a question', icon: '?' },
{ label: 'Find in Drive', icon: '▲' },
{ label: 'Find in Slack', icon: '#' },
{ label: 'Summarize', icon: '≡' },
{ label: 'Improve writing', icon: '✎' },
{ label: 'Draft an agenda', icon: '☰' },
];
const BEAT = 21; // ~0.7s @30fps
const INTRO = 12; // 句干入场
const CYCLES = PILLS.length;
const Pill: React.FC<{ label: string; icon: string; style?: React.CSSProperties }> = ({
label,
icon,
style,
}) => (
{icon}
{label}
);
export const PillSlotCycle: React.FC = () => {
const frame = useCurrentFrame();
// 句干入场:ease-out 上浮淡入
const stemT = interpolate(frame, [0, INTRO], [0, 1], {
extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
});
const cycleStart = INTRO;
const cycleEnd = cycleStart + CYCLES * BEAT;
// 当前处于第几个 pill 拍
const rel = frame - cycleStart;
const idx = Math.min(Math.floor(rel / BEAT), CYCLES - 1);
const beatFrame = rel - idx * BEAT;
const SWAP = 8; // 每拍前 8f 完成换位
// 收束段:pill 上飞消失,"do it all." 从下滑入落位
const isFinale = frame >= cycleEnd;
const finT = interpolate(frame, [cycleEnd, cycleEnd + 14], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.out(Easing.back(1.4)),
});
// 槽内容渲染
let slot: React.ReactNode = null;
if (!isFinale && rel >= 0) {
const incoming = PILLS[idx];
const outgoing = idx > 0 ? PILLS[idx - 1] : null;
// 新 pill:从下 +120px 带 blur 滑入,ease-out
const inT = interpolate(beatFrame, [0, SWAP], [0, 1], {
extrapolateRight: 'clamp',
easing: Easing.out(Easing.cubic),
});
const inY = interpolate(inT, [0, 1], [120, 0]);
const inBlur = interpolate(inT, [0, 0.7, 1], [14, 4, 0]);
// 旧 pill:向上 -120px 加速飞出淡掉
const outT = interpolate(beatFrame, [0, SWAP - 1], [0, 1], {
extrapolateRight: 'clamp',
easing: Easing.in(Easing.cubic),
});
const outY = interpolate(outT, [0, 1], [0, -130]);
slot = (
{/* 撑起槽宽度的隐形占位(当前 pill) */}
{outgoing && outT < 1 && (
)}
);
} else if (isFinale) {
// 最后一个 pill 上飞离场(前 8f),然后 "do it all." 落位
const lastOutT = interpolate(frame, [cycleEnd, cycleEnd + 7], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: Easing.in(Easing.cubic),
});
slot = (
do it all.
{lastOutT < 1 && (
)}
);
}
return (
{/* 句干固定不动:整行左端锚死,不随 pill 宽度居中重排 */}
One AI tool to
{slot}
);
};