// ===== demos/interaction/hashtag-to-pill-materialize/HashtagToPillMaterialize.tsx ===== // hashtag-to-pill-materialize(bear-app 18–21.5s 重做版,按原片密帧逐帧对照) // 原片实测节奏(25fps 逐帧): // 1) 白底居中打字 "#music":几何无衬线(Futura 气质)、中灰墨色、红色实心光标恒亮不闪、人手节奏 // 2) 实体化 = 1 帧硬切:文字+光标 → 宽大浅灰无描边胶囊 + 灰色双八分音符图标 + "music"(字号不变,#被图标替换) // 3) 停约 0.6s → 整体平滑缩小(→~0.55x)并左移落到页面标签位(约 0.55s,easeInOut) // 4) 再 1 帧硬切揭示成品笔记页:奶油底、墨绿大标题 "My favorite bands"、胶囊换成鼠尾草绿、正文三行—— // 原片没有"胶囊飞入下方滑入卡片"的段落(批次 8 的飞行段为杜撰,已砍) import React from 'react'; import { AbsoluteFill, interpolate, useCurrentFrame, Easing } from 'remotion'; const FONT = "Futura, 'Century Gothic', 'Avenir Next', 'Trebuchet MS', sans-serif"; const C = { bgWhite: '#fcfcfb', bgCream: '#f4f1e5', ink: '#454543', cursor: '#e0453f', pillGray: '#e9e9e7', pillTextGray: '#4b4b49', iconGray: '#7e7e7c', pillSage: '#d5e0cf', iconSage: '#5c7a63', titleGreen: '#2d5c47', pillSageText: '#3f5e4c', body: '#4c4b43', }; // ---- mulberry32(仅用于打字节奏的人手抖动,确定性)---- function mulberry32(seed: number) { let a = seed >>> 0; return () => { a = (a + 0x6d2b79f5) >>> 0; let t = a; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } const TEXT = '#music'; // 打字起点 & 每字间隔(帧),4–8 帧带抖动,模拟原片真人节奏 const TYPE_START = 8; const rand = mulberry32(20260717); const TYPE_AT: number[] = (() => { const at: number[] = []; let f = TYPE_START; for (let i = 0; i < TEXT.length; i++) { at.push(f); f += 4 + Math.floor(rand() * 3); // 4–6 帧(原片 ~6字/秒) } return at; })(); // ---- 时间轴(30fps,共 132 帧,节奏对齐原片 18–21.5s)---- const MORPH = 48; // 1 帧硬切实体化(打完 hold ~0.5s,原片 0.45s) const MOVE_START = 66; // 胶囊开始缩小左移(morph 后 0.6s,原片同) const MOVE_END = 80; // 落位(0.47s,原片 ~0.45s) const REVEAL = 83; // 1 帧硬切揭示成品页,之后静置收尾 // ---- 几何(1920x1080,等比换算自原片 1280x720 实测像素)---- const FS = 132; // 打字/胶囊文字字号(原片 glyph 等高换算) const HERO = { x: 960, y: 540 }; // 大胶囊中心 const PILL_W = 740, PILL_H = 236; // 原片实测 493x157 @720p ×1.5 const END_SCALE = 0.554; // 落位缩放(原片 273/493) const SLOT = { x: 361, y: 473 }; // 标签位中心(原片灰胶囊落点 (244.5,317.5)×1.5 与揭示位折中) // 灰色双八分音符图标(原片是 beamed 双音符,非 ♪) const NoteIcon: React.FC<{ size: number; color: string }> = ({ size, color }) => ( ); // 胶囊(大字号绘制,整体 transform 缩放,保证实体化前后文字原位等大) const Pill: React.FC<{ bg: string; iconColor: string; textColor: string }> = ({ bg, iconColor, textColor }) => (
music
); export const HashtagToPillMaterialize: React.FC = () => { const frame = useCurrentFrame(); // ---- 打字 ---- const typedCount = TYPE_AT.filter((t) => frame >= t).length; const typed = TEXT.slice(0, typedCount); // ---- 缩小左移 ---- const moveT = interpolate(frame, [MOVE_START, MOVE_END], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.5, 0, 0.25, 1), }); const px = interpolate(moveT, [0, 1], [HERO.x, SLOT.x]); const py = interpolate(moveT, [0, 1], [HERO.y, SLOT.y]); const ps = interpolate(moveT, [0, 1], [1, END_SCALE]); // 实体化瞬间极轻微落定(原片近乎硬切,仅 3 帧 1.03→1,避免死板) const settle = interpolate(frame, [MORPH, MORPH + 3], [1.03, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.quad), }); const revealed = frame >= REVEAL; return ( {/* 成品页(硬切揭示,之后全静) */} {revealed && ( <>
My favorite bands
I want to share a few of my favorite bands
and the song that I always listen when driving
to home. Welcome. Bring headphones.
)} {/* 打字层:文字 + 恒亮红光标(原片光标不闪烁),实体化帧整体消失 */} {frame < MORPH && (
{typed}
)} {/* 胶囊层:实体化 1 帧硬切出现 → hold → 缩小左移落位 → 揭示帧换鼠尾草绿 */} {frame >= MORPH && (
{revealed ? : }
)}
); };