// ===== demos/effects/icon-performance-moves/AttentionBounce.tsx ===== // 求关注弹跳(attention-bounce)——macOS Dock 语汇:icon 原地起跳讨拍 // 半屏 app 图标(圆角方块+铃形符号)在地面线上连跳 4 次且一次比一次高 // (首跳 0.5 倍 icon 高 → 末跳 1.2 倍);每次落地帧压扁(宽 1.2x 高 0.8x) // + 落点尘点 2–3 颗;最高那跳镜头整体向 icon 轻推 8%("被吸引"); // 落定后 icon 右侧弹开一张功能面板卡收尾。 // 节拍:0–12 静置 → 12 起跳(4 跳递增,各 16/18/20/24f)→ ~90 落定 → // 92–104 面板卡弹出 → 110 后真静止 40f。帧确定,尘点用 sin 散列。 import React from 'react'; import { useCurrentFrame, interpolate, Easing } from 'remotion'; import { G, Card } from '../../_fixtures/Fixtures'; const AMBER = '#b45309'; const ICON = 400; // icon 边长(半屏级) const GROUND = 940; // 地面线 y(末跳 1.2x 顶点 + 8% 推近后仍贴画面上沿不出框) const CX = 760; // icon 中心 x(右侧留面板位) // 4 跳:起跳帧、时长、峰高(相对 icon 高) const JUMPS = [ { start: 12, dur: 16, peak: 0.5 * ICON }, { start: 30, dur: 18, peak: 0.75 * ICON }, { start: 50, dur: 20, peak: 0.95 * ICON }, { start: 72, dur: 24, peak: 1.2 * ICON }, ]; export const AttentionBounce: React.FC = () => { const f = useCurrentFrame(); // 弹跳高度 + 落地挤压 let y = 0; // 离地高度 let squash = 0; // 0–1 落地压扁强度 let stretch = 0; // 空中拉伸强度(速度感) JUMPS.forEach((j) => { const t = (f - j.start) / j.dur; if (t > 0 && t < 1) { y = j.peak * 4 * t * (1 - t); // 抛物线 stretch = Math.abs(1 - 2 * t) * 0.14; // 起跳/下落速度快时拉长 } // 落地帧后 5f 内压扁回弹 const land = j.start + j.dur; if (f >= land && f < land + 6) { squash = Math.max(squash, 1 - (f - land) / 6); } }); // 起跳前的预压(第一跳前 3f 蹲一下) if (f >= 9 && f < 12) squash = Math.max(squash, (f - 9) / 3 * 0.7); const sx = 1 + squash * 0.2 - stretch * 0.5; const sy = 1 - squash * 0.2 + stretch; // 镜头推近:第 4 跳(最高)期间整体 scale 1→1.08,落定后保持 const zoomT = interpolate(f, [72, 88], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.inOut(Easing.quad), }); const zoom = 1 + 0.08 * zoomT; // 尘点:每次落地帧生成 3 颗,向两侧飞散淡出(12f 生命) const dusts: Array<{ x: number; y: number; r: number; op: number }> = []; JUMPS.forEach((j, ji) => { const land = j.start + j.dur; const life = (f - land) / 12; if (life <= 0 || life >= 1) return; for (let k = 0; k < 3; k++) { const seed = ji * 3 + k; const dir = k === 1 ? 0 : k === 0 ? -1 : 1; const spread = (60 + 40 * Math.abs(Math.sin(seed * 5.7))) * (ji + 2) * 0.45; const e = Easing.out(Easing.cubic)(life); dusts.push({ x: CX + dir * (ICON * 0.42 + spread * e) + (dir === 0 ? 30 * Math.sin(seed * 3.1) * e : 0), y: GROUND - 14 - 46 * e * (0.6 + 0.5 * Math.abs(Math.sin(seed * 2.3))), r: 12 + 6 * Math.abs(Math.sin(seed * 4.9)) - 8 * life, op: (1 - life) * 0.8, }); } }); // 功能面板卡:落定后(f=98)从 icon 右侧弹出 const panelT = interpolate(f, [98, 110], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.back(1.8)), }); const iconTop = GROUND - ICON - y; return (
{/* 镜头层:整体向 icon 轻推 */}
{/* 地面线 */}
{/* 落地尘点 */} {dusts.map((d, i) => (
))} {/* 影子:随高度缩小变淡 */}
{/* app icon:圆角方块 + 铃形符号(自绘 SVG,半屏特写) */} {/* 符号:简洁铃形 */} {/* 功能面板卡:落定后弹出 */} {panelT > 0 && (
New
)}
); }; // ===== demos/effects/icon-performance-moves/PopBurstConfirm.tsx ===== // 爆花确认(pop-burst-confirm)——确认时刻的三连爆 // 半屏大对勾 icon(圆底+勾):先缩 0.6x 蓄力 3f → 弹 1.35x 过冲 → 落回 1x, // 释放帧同帧中心射出 10 根短线粒子(径向飞出后消失)+ 一圈描边圆环从 // icon 边缘扩到 2.5 倍直径淡出;随后 "Deployed" 小标签弹出。 // 节拍:0–20 静置(空心圆待确认)→ 20–23 缩 0.6x → 23–27 蓄力 3f → // 27 释放(勾画出+粒子+圆环)→ 27–44 过冲落回 → 40–50 标签弹出 → 55 后真静止 65f。 // 帧确定,无随机源(粒子角度抖动用 sin 散列)。 import React from 'react'; import { useCurrentFrame, interpolate, Easing } from 'remotion'; import { G } from '../../_fixtures/Fixtures'; const AMBER = '#b45309'; const POP = 27; // 释放帧 const DUR = 120; export const PopBurstConfirm: React.FC = () => { const f = useCurrentFrame(); // icon 缩放:蓄力→过冲→落回 const scale = (() => { if (f <= 20) return 1; if (f <= 23) return interpolate(f, [20, 23], [1, 0.6], { easing: Easing.in(Easing.quad) }); if (f <= POP) return 0.6; if (f <= 33) return interpolate(f, [POP, 33], [0.6, 1.35], { easing: Easing.out(Easing.cubic) }); return interpolate(f, [33, 44], [1.35, 1], { extrapolateRight: 'clamp', easing: Easing.out(Easing.back(2)), }); })(); // 对勾:释放帧起 8f 内画出(dashoffset 滑窗) const checkT = interpolate(f, [POP, POP + 8], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.cubic), }); // 粒子:释放帧起 14f 径向飞出(幅度加码到 190px 保半屏可感) const pt = interpolate(f, [POP, POP + 14], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const pDist = 210 + 190 * Easing.out(Easing.cubic)(pt); const pLen = 46 * (1 - pt); // 圆环:释放帧起 20f 从 icon 边缘扩到 2.5 倍直径淡出 const rt = interpolate(f, [POP, POP + 20], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', }); const ringR = 200 + 300 * Easing.out(Easing.cubic)(rt); const ringO = 0.85 * (1 - rt); const ringW = 16 - 12 * rt; // 标签:40f 弹出(back 超调) const tagT = interpolate(f, [40, 50], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.out(Easing.back(2.6)), }); const CX = 960; const CY = 470; return (
{/* 扩散圆环(不随 icon 缩放) */} {rt > 0 && rt < 1 && ( )} {/* 粒子:10 根短线径向飞出,角度带 sin 散列微抖 */} {pt > 0 && pt < 1 && ( {Array.from({ length: 10 }).map((_, i) => { const ang = ((i * 36 + 9 * Math.sin(i * 7.31)) * Math.PI) / 180; const x1 = 700 + Math.cos(ang) * pDist; const y1 = 700 + Math.sin(ang) * pDist; const x2 = 700 + Math.cos(ang) * (pDist + pLen); const y2 = 700 + Math.sin(ang) * (pDist + pLen); return ( ); })} )} {/* 主体 icon:圆底 + 对勾(半屏特写 ~480px) */} {checkT > 0 && ( )} {/* Deployed 小标签 */} {tagT > 0 && (
Deployed
)}
); };