// ===== demos/typography/title-demote-to-label/TitleDemoteToLabel.tsx ===== // title-demote-to-label —— 大标题降格为节标签 // 源:perplexity-promo 16–18.5s。大标题居中显影站稳一拍,随后缩小 ~0.3x // 平移到左上角变小节标签,内容区(灰阶骨架块)在其下方生长。 // 附加变体(framer text-selection-title):标题登场带文本选中蓝高亮块、随后撤掉。 import React from 'react'; import { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion'; import { G } from '../../_fixtures/Fixtures'; const FONT = 'Helvetica, Arial, sans-serif'; const SEL = 'rgba(58, 128, 236, 0.35)'; // 内容骨架块:随 t 依次生长 const Skeleton: React.FC<{ t: number }> = ({ t }) => { const blocks = [ { w: 1500, h: 26 }, { w: 1280, h: 26 }, { w: 1420, h: 26 }, { w: 760, h: 26 }, { w: 1500, h: 300, card: true }, ]; return (
{blocks.map((b, i) => { const bt = interpolate(t, [i * 0.16, i * 0.16 + 0.3], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.cubic), }); return (
); })}
); }; // 一个完整的"显影→(可选高亮)→降格→内容生长"小节 const DemoteScene: React.FC<{ frame: number; title: string; withSelection: boolean; }> = ({ frame, title, withSelection }) => { // 时间轴(局部帧) const REVEAL = 0; // 0–12 显影 const SEL_ON = 14; // 高亮扫入 14–24 const SEL_OFF = 32; // 高亮撤掉 32–40 const DEMOTE = withSelection ? 44 : 32; // 降格开始 const DEMOTE_END = DEMOTE + 20; const GROW = DEMOTE + 12; // 显影:blur + 淡入 const rev = interpolate(frame, [REVEAL, REVEAL + 12], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.cubic), }); // 降格补间:scale 1 -> 0.3,中心 -> 左上 const dem = interpolate(frame, [DEMOTE, DEMOTE_END], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.inOut(Easing.cubic), }); const scale = interpolate(dem, [0, 1], [1, 0.3]); // 用 left/top 补间:起点居中(由外层 flex 定位换算),终点左上角 const x = interpolate(dem, [0, 1], [960, 150]); const y = interpolate(dem, [0, 1], [480, 110]); // 高亮块:从左扫入盖住文字,再从左撤掉 let selLeft = 0; let selWidth = 0; if (withSelection) { const on = interpolate(frame, [SEL_ON, SEL_ON + 10], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.quad), }); const off = interpolate(frame, [SEL_OFF, SEL_OFF + 8], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.in(Easing.quad), }); selLeft = off * 100; selWidth = Math.max(0, on * 100 - selLeft); } const growT = interpolate(frame, [GROW, GROW + 40], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); return ( {/* 内容骨架区 */}
{/* 标题:transform-origin 左中,位置补间 */}
{withSelection && selWidth > 0 && (
)} {title}
); }; export const TitleDemoteToLabel: React.FC = () => { const frame = useCurrentFrame(); const SPLIT = 92; // 变体 A 时长 if (frame < SPLIT) { return ; } // 变体 B:文本选中态高亮登场 const f = frame - SPLIT; // 白闪转场 3f const flash = interpolate(f, [0, 4], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); return ( ); };