Motion · Interaction

Command Palette Summon

The command palette descends — the whole screen dims and blurs, the ⌘K panel springs down with overshoot, candidate rows appear staggered, and the list narrows in real time as you type.

1920×108016:930 fps5.06s · 152 framesremotionLicense: Apache-2.0#shotcraft#remotion#interaction#ui-entrance

Source: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Remotion composition (React/TSX) — preview video, no in-browser player

When to use

The "the whole product lives in one input box" narrative for efficiency-focused products; the signature entrance for a command palette/search/shortcut feature.

Intention

The signature ritual of Raycast/Linear launch films: one soft sound, the whole UI world dims to make way, the ⌘K panel drops from above center, the candidate list appears staggered; type two letters, and the list narrows in real time — "everything you need lives in this one input box." The simulated typing follows real human typing speed (R3), and the narrowing's "squeeze" feel comes from row-height collapse, not fading.

Duration & energy

  • Duration: 4–5s.
  • Energy: Medium (a ritual feel; the drop and the narrowing are the two small impact beats).

Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)

意图

Raycast/Linear 发布片的标志性仪式:一声轻响,整个 UI 世界压暗 让路,⌘K 面板从中心上方弹落,候选列表错峰浮现;敲两个字母, 列表实时收窄——"你要的一切都在这个输入框里"。模拟交互按真人 操作速度走(R3),收窄的"挤压感"来自行高塌缩而非淡出。

动作阶段

| 阶段 | 帧段参考 | 内容 | |------|------|------| | 1 静置 | f0–12 | dashboard 初始状态 | | 2 让路 | f12–22 | 背景压暗至 45% + blur 10px | | 3 降临 | f18–33 | 面板 -20px 落入,过冲 +8px 回正(与压暗跨骑) | | 4 候选浮现 | f32–56 | 5 行每 4f 错峰上浮 | | 5 输入收窄 | ~f62–90 | 两次按键字符出现,行高塌缩收窄 5→2(塌缩完条件卸载) | | 6 选中定格 | 末 ≥40f | 首条高亮(底色+左描边),光标转常亮,真静止 |

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 压暗 | 亮度 45% + blur 10px,10f | 压不够面板不突出;全黑失去上下文 | | 弹落 | 起点上方 20px,overshoot 一拍回正 | 无过冲读作淡入,仪式感没了 | | 候选错峰 | 4f/行 | 同帧出读作贴图 | | 收窄 | height→0 + overflow hidden 塌缩 | 淡出收窄没有"挤压"感;塌缩完必须卸载 | | 光标 | 闪烁周期 16f;定格后强制常亮 | 闪到结尾就永不真静止(demo 判例) |

已知坑

  • demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿,

首次实战须以真实素材回验

  • 输入节奏按真人打字速度(R3 判例),两键间隔 ≥12f
  • 组合款见 theme-switch-moves B 式(面板收缩成点引发主题涟漪)
  • 实战候选行放真实功能名——这是功能清单的天然展位(P4 映射)
  • 声音:唤出一声轻响(非游戏音),按键 2 声键帽拟音,

高亮一声轻 pop(S1/S4 同源)

参考实现

demos/interaction/command-palette-summon/ (CommandPaletteSummon.tsx)

Reference implementation (Remotion)

  • demos/interaction/command-palette-summon/CommandPaletteSummon.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/interaction/command-palette-summon/CommandPaletteSummon.tsx

The component imports shared fixtures from demos/_fixtures and helpers from assets/lib in the repo; SKILL.md there documents the render workflow.

Everything on this page is also machine-readable. Point your agent at the JSON, or add the gimgs MCP server and ask it to search motion templates.

  1. Fetch the JSON — it holds the shot recipe (timing, easing, parameter table, pitfalls) and the full TSX source.
  2. Clone the video-shotcraft repo; the component imports shared fixtures from demos/_fixtures and helpers from assets/lib.
  3. Register the component as a <Composition> in a Remotion project and render with `npx remotion render`, or re-implement the recipe in your own engine.
curl -s https://gimgs.net/motion/command-palette-summon.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/interaction/command-palette-summon/CommandPaletteSummon.tsx
npx remotion render <CompositionId> out.mp4
claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("command-palette-summon")

Prompt

Use the video-shotcraft shot recipe "Command Palette Summon" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/command-palette-summon.json
Raw TSX: https://gimgs.net/motion/command-palette-summon/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/interaction/command-palette-summon/CommandPaletteSummon.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 5.06s

1. Read the JSON and follow its "prompt" section (when to use, intention, parameter table, known pitfalls).
2. Adapt it for: <describe what you want>.
3. Either clone the repo, register the component as a <Composition> in a Remotion project and run npx remotion render <CompositionId> out.mp4 — or re-implement the recipe in your own engine (HTML/WAAPI works: keep the same timing and easing).

JSON · Markdown · llms.txt

5,711 chars · Apache-2.0Open raw template ↗
// ===== demos/interaction/command-palette-summon/CommandPaletteSummon.tsx =====
// command-palette-summon —— 命令面板降临
// FakeDashboard 静置 → 整屏压暗+blur → ⌘K 面板从中心上方 20px 带 overshoot 弹落
// → 5 条候选行错峰浮现 → 模拟输入 2 字母(灰块当字符)候选 5→3→2 收窄
// → 高亮首条。f=110 后全静止(40f)。光标 f<104 闪烁、之后常亮。
import React from 'react';
import { useCurrentFrame, interpolate, Easing } from 'remotion';
import { G, FakeDashboard } from '../../_fixtures/Fixtures';

const CL = { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } as const;

// 时间轴
const DIM0 = 12; // 压暗开始(前 12f 初始静置)
const DIM1 = 22;
const PANEL_IN = 18; // 面板开始弹落
const ROWS_START = 32; // 候选行开始错峰浮现
const KEY1 = 62; // 第一个字母
const KEY2 = 78; // 第二个字母
const HL = 94; // 高亮首条
const BLINK_END = 104; // 光标停止闪烁(常亮)

const PANEL_W = 780;
const PANEL_X = (1920 - PANEL_W) / 2;
const PANEL_Y = 290;
const ROW_H = 72;
const ROW_GAP = 8;
const EXIT_DUR = 10;

// exitAt: 0=留到最后,1=第一次按键后退出,2=第二次按键后退出
const ROWS = [
  { titleW: 52, exitAt: 0 },
  { titleW: 38, exitAt: 0 },
  { titleW: 61, exitAt: 2 },
  { titleW: 45, exitAt: 1 },
  { titleW: 56, exitAt: 1 },
];

const PaletteRow: React.FC<{ i: number; frame: number }> = ({ i, frame }) => {
  const { titleW, exitAt } = ROWS[i];
  const inStart = ROWS_START + i * 4;
  const exitStart = exitAt === 1 ? KEY1 + 3 : exitAt === 2 ? KEY2 + 3 : null;

  if (exitStart !== null && frame >= exitStart + EXIT_DUR) return null; // 条件卸载,非 opacity 0

  const inOp = interpolate(frame, [inStart, inStart + 8], [0, 1], CL);
  const inY = interpolate(frame, [inStart, inStart + 8], [12, 0], {
    easing: Easing.out(Easing.cubic),
    ...CL,
  });
  const exitT =
    exitStart === null
      ? 1
      : interpolate(frame, [exitStart, exitStart + EXIT_DUR], [1, 0], {
          easing: Easing.inOut(Easing.cubic),
          ...CL,
        });

  // 高亮首条
  const hl = i === 0 ? interpolate(frame, [HL, HL + 10], [0, 1], CL) : 0;

  return (
    <div
      style={{
        height: (ROW_H + ROW_GAP) * exitT,
        opacity: inOp * exitT,
        overflow: 'hidden',
      }}
    >
      <div
        style={{
          height: ROW_H,
          borderRadius: 12,
          background: hl > 0 ? `rgba(228,228,226,${hl})` : 'transparent',
          boxShadow: hl > 0 ? `inset 4px 0 0 rgba(47,47,47,${hl})` : 'none',
          display: 'flex',
          alignItems: 'center',
          gap: 20,
          padding: '0 22px',
          boxSizing: 'border-box',
          transform: `translateY(${inY}px)`,
        }}
      >
        <div style={{ width: 36, height: 36, borderRadius: 9, background: G.mid }} />
        <div style={{ height: 14, width: `${titleW}%`, background: G.bar, borderRadius: 7 }} />
        <div style={{ marginLeft: 'auto', width: 58, height: 24, borderRadius: 6, background: G.line }} />
      </div>
    </div>
  );
};

export const CommandPaletteSummon: React.FC = () => {
  const frame = useCurrentFrame();

  // 背景压暗 + blur
  const dim = interpolate(frame, [DIM0, DIM1], [0, 0.45], CL);
  const blur = interpolate(frame, [DIM0, DIM1], [0, 10], CL);

  // 面板弹落:上方 20px → 过冲 +8px → 落回 0
  const panelY =
    frame < PANEL_IN + 9
      ? interpolate(frame, [PANEL_IN, PANEL_IN + 9], [-20, 8], {
          easing: Easing.out(Easing.cubic),
          ...CL,
        })
      : interpolate(frame, [PANEL_IN + 9, PANEL_IN + 15], [8, 0], {
          easing: Easing.inOut(Easing.cubic),
          ...CL,
        });
  const panelOp = interpolate(frame, [PANEL_IN, PANEL_IN + 7], [0, 1], CL);

  // 模拟输入:已敲字符数
  const typed = (frame >= KEY1 ? 1 : 0) + (frame >= KEY2 ? 1 : 0);
  // 光标闪烁(周期 16f),BLINK_END 后常亮保证收尾静止
  const cursorOn = frame >= BLINK_END ? true : (frame - PANEL_IN) % 16 < 8;

  return (
    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>
      <div style={{ filter: frame < DIM0 ? undefined : `blur(${blur}px)` }}>
        <FakeDashboard variant="A" />
      </div>
      <div style={{ position: 'absolute', inset: 0, background: `rgba(20,20,20,${dim})` }} />

      {frame >= PANEL_IN && (
        <div
          style={{
            position: 'absolute',
            left: PANEL_X,
            top: PANEL_Y,
            width: PANEL_W,
            transform: `translateY(${panelY}px)`,
            opacity: panelOp,
            background: G.card,
            borderRadius: 18,
            border: `2px solid ${G.border}`,
            boxShadow: '0 28px 90px rgba(0,0,0,0.4)',
            padding: 20,
            boxSizing: 'border-box',
          }}
        >
          {/* 输入框 */}
          <div
            style={{
              height: 76,
              borderBottom: `2px solid ${G.line}`,
              display: 'flex',
              alignItems: 'center',
              gap: 14,
              padding: '0 10px 14px 10px',
              boxSizing: 'border-box',
            }}
          >
            <div style={{ width: 32, height: 32, borderRadius: 8, background: G.bar }} />
            {/* 已敲入的字符灰块 */}
            {Array.from({ length: typed }).map((_, c) => (
              <div key={c} style={{ width: 28, height: 38, borderRadius: 6, background: '#4a4a48' }} />
            ))}
            {/* 光标 */}
            {cursorOn && <div style={{ width: 4, height: 42, background: G.ink, borderRadius: 2 }} />}
            {/* 占位提示条:敲第一个字母时卸载 */}
            {typed === 0 && (
              <div style={{ width: 260, height: 14, borderRadius: 7, background: G.line, opacity: 0.8 }} />
            )}
          </div>
          {/* 候选行 */}
          <div style={{ paddingTop: 14 }}>
            {ROWS.map((_, i) => (
              <PaletteRow key={i} i={i} frame={frame} />
            ))}
          </div>
        </div>
      )}
    </div>
  );
};


  1. Read the recipe
    Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points.
  2. Get the code
    tool: git
    git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install
  3. Register + render
    tool: remotion
    npx remotion render <CompositionId> out.mp4 # component: demos/interaction/command-palette-summon/CommandPaletteSummon.tsx
  4. Or re-implement
    Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing.
paramtypedefaultdescription
—

More in this category