Motion · Rhythm

Speed ramp and freeze-annotate

Two frame-remap pacing moves — speed-ramp (fast → 0.2x gaze → fast) and freeze-annotate (flowing → freeze with a circled callout → unfreeze).

1920×108016:930 fps4.54s · 136 framesremotionLicense: Apache-2.0#shotcraft#remotion#rhythm

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

Variants

speed-ramp
Nonlinear frame-number remap speed change — fast sweep suddenly drops to 0.2x to linger on the highlight, then accelerates away
freeze-annotate
The card stream freezes mid-flow on the full frame, a circled callout pops in to call out the point, then it unfreezes and keeps flowing

When to use

For slowing/stopping on one highlight in a card stream or long pan; use freeze-annotate in tutorial/explainer contexts

Intention

A constant-speed stream reads as a slideshow (R2); an all-fast stream leaves viewers unable to catch the highlight (R3). Speed-ramp creates a "sprint–gaze–sprint" inside one motion; freeze-annotate goes further — it simply stops, marker-circles the highlight, then moves on. Both share the same technical root (frame-number remap) but differ in meaning: speed-ramp is "a second glance while passing by," freeze is "stopping class to underline the point."

Duration & energy

  • Duration: Speed-ramp: 4–5s total (slow window ≥40f); freeze-annotate: 4–5s total (freeze hold ≥45f)
  • Energy: Medium-high (the speed contrast itself is the energy beat)

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

意图

匀速流读作 PPT(R2),全程快流观众抓不住重点(R3)。变速在一条 运动里制造"冲刺-凝视-冲刺";定格标注更进一步——干脆停下来, 马克笔圈出重点再走。两款同一技术根(帧号 remap),语义不同: 变速是"路过时多看一眼",定格是"停课划重点"。

动效核心

  • 源动画匀速,输出帧对源帧做非线性 remap:

src = interpolate(frame, [0,40,85,135], [0,88,97,207])(斜率即速率)

  • 变速款:快段斜率 ≥2、慢窗斜率 ≈0.2(≥10 倍反差才可感);

blur 联动速率开关——快段包 CameraMotionBlur、慢窗裸渲, "快糊-慢清"反差是手法成立的一半

  • 定格款:定格段斜率 =0(瞬时切换,无缓入——定格要干脆);

定格期强调色马克笔 SVG 椭圆 8f 描边(stroke-dashoffset)圈住目标 + 箭头 6f 点题,feTurbulence(scale≈7)给手绘抖动;解冻段斜率 >1 补偿时长,圈注 8f 淡出

  • 目标卡用高清纹理并对位屏中(凝视/圈注对象必须锐利、居中)

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 慢窗/定格时长 | ≥40f / ≥45f | 短了凝视不成立(R3 宁慢勿快) | | 快慢斜率反差 | ≥10 倍 | 反差不足读不出"慢下来了" | | 圈注描边 | 8f 划完 + 抖动 scale 7 | 太慢像加载动画;无抖动像机械标注 |

已知坑

  • 变速段 SFX 钉帧跟输出帧不跟源帧(画面动作在输出时间轴上发生);

拟音槽位:定格款配马克笔沙沙声(S4),本卡转正时未配、实战补

  • 定格标注一支 30s 片 ≤2 次(同 D 式克制原则)
  • 参数经占位素材调校转正,非实战定稿,首次实战后回验

参考实现

demos/rhythm/speed-ramp-freeze/ (FreezeAnnotateReal.tsx / SpeedRampReal.tsx)

Reference implementation (Remotion)

  • demos/rhythm/speed-ramp-freeze/FreezeAnnotateReal.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/speed-ramp-freeze/FreezeAnnotateReal.tsx
  • demos/rhythm/speed-ramp-freeze/SpeedRampReal.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/speed-ramp-freeze/SpeedRampReal.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/speed-ramp-freeze.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/rhythm/speed-ramp-freeze/FreezeAnnotateReal.tsx, demos/rhythm/speed-ramp-freeze/SpeedRampReal.tsx
npx remotion render <CompositionId> out.mp4
claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("speed-ramp-freeze")

Prompt

Use the video-shotcraft shot recipe "Speed ramp and freeze-annotate" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/speed-ramp-freeze.json
Raw TSX: https://gimgs.net/motion/speed-ramp-freeze/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/rhythm/speed-ramp-freeze/FreezeAnnotateReal.tsx, demos/rhythm/speed-ramp-freeze/SpeedRampReal.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 4.54s

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,236 chars · Apache-2.0Open raw template ↗
// ===== demos/rhythm/speed-ramp-freeze/FreezeAnnotateReal.tsx =====
// freeze-annotate 定格标注(轮 G)——真实卡片流运动中定格,
// 马克笔琥珀圈注(feTurbulence 手绘抖动)圈住目标卡 + 箭头点题,解冻继续。
// remap:0–45 流动 → 45–100 定格(斜率0)→ 100–135 解冻(1.4x 补偿)。
import { useId } from 'react';
import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame } from 'remotion';
import layout from '../../_textures/live-layout.json';

export const FREEZEANNOTATE_DUR = 135;

const CARD_W = 460;
const GAP = 60;
const RAIL = layout.projects.cards.slice(0, 9);
const TARGET_I = 5;
const AMBER = '#b45309';

export const FreezeAnnotateReal: React.FC = () => {
  const frame = useCurrentFrame();
  // 滤镜 ID 按实例生成,多实例同场不串引(useId 的 «:» 在 url() 里非法,需清洗)
  const roughId = `rough-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
  const src = interpolate(frame, [0, 45, 100, 135], [0, 45, 45, 94], {
    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
  });
  const dx = src * 22;
  const draw = interpolate(frame, [52, 60], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
  const arrowDraw = interpolate(frame, [60, 66], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
  const fade = interpolate(frame, [96, 104], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
  // 导轨起点 -880:定格时(dx=990)目标卡 k=5 中心 = -880+2600-990+230 = 960 屏中
  const X0 = -880;
  const targetX = X0 + TARGET_I * (CARD_W + GAP) - dx + CARD_W / 2;
  const C = 1750; // 椭圆周长近似
  return (
    <AbsoluteFill style={{ backgroundColor: '#f9f6f1', overflow: 'hidden' }}>
      <div style={{ position: 'absolute', top: 370, transform: `translateX(${-dx + X0}px)` }}>
        {Array.from({ length: 14 }).map((_, k) => {
          const c = RAIL[k % RAIL.length];
          const isTarget = k === TARGET_I;
          return (
            <Img
              key={k}
              src={staticFile(`textures/live/${isTarget ? 'card4-hires.png' : c.file}`)}
              style={{
                position: 'absolute', left: k * (CARD_W + GAP), top: 0,
                width: CARD_W, borderRadius: 12,
                boxShadow: '0 4px 16px rgba(31,28,23,0.10)',
              }}
            />
          );
        })}
      </div>
      <svg width={1920} height={1080} style={{ position: 'absolute', inset: 0, pointerEvents: 'none', opacity: fade }}>
        <defs>
          <filter id={roughId}>
            <feTurbulence type="fractalNoise" baseFrequency="0.02" numOctaves="2" seed="7" result="n" />
            <feDisplacementMap in="SourceGraphic" in2="n" scale="7" />
          </filter>
        </defs>
        <ellipse
          cx={targetX} cy={560} rx={290} ry={230}
          fill="none" stroke={AMBER} strokeWidth={8} strokeLinecap="round"
          strokeDasharray={C} strokeDashoffset={C * (1 - draw)}
          transform={`rotate(-6 ${targetX} 560)`}
          filter={`url(#${roughId})`}
        />
        <path
          d={`M ${targetX + 330} 190 Q ${targetX + 220} 240 ${targetX + 140} 320`}
          fill="none" stroke={AMBER} strokeWidth={8} strokeLinecap="round"
          strokeDasharray={420} strokeDashoffset={420 * (1 - arrowDraw)}
          filter={`url(#${roughId})`}
        />
      </svg>
    </AbsoluteFill>
  );
};


// ===== demos/rhythm/speed-ramp-freeze/SpeedRampReal.tsx =====
// speed-ramp 变速(轮 C)——真实卡片流帧号 remap:快(斜率2.2) →
// 0.2x 慢速展示窗 → 快。慢速窗中目标卡(card4-hires)清晰滑过屏中。
// blur 联动速率:快段包 blur、慢段不包,反差即"凝视感"。
import { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame } from 'remotion';
import { CameraMotionBlur } from '@remotion/motion-blur';
import layout from '../../_textures/live-layout.json';

export const SPEEDRAMP_DUR = 135;

const CARD_W = 460;
const GAP = 60;
const RAIL = layout.projects.cards.slice(0, 9);
const TARGET_I = 5;

const Scene: React.FC = () => {
  const frame = useCurrentFrame();
  // remap:0–40f 走 88 源帧(快),40–85f 走 9 源帧(0.2x),85–135f 走 110(快)
  const src = interpolate(frame, [0, 40, 85, 135], [0, 88, 97, 207], {
    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',
  });
  const dx = src * 28;
  return (
    <AbsoluteFill style={{ backgroundColor: '#f9f6f1', overflow: 'hidden' }}>
      {/* 偏移 717:让慢速窗中点(src≈92.4,dx≈2587)时目标卡 k=5 中心落屏中 */}
      <div style={{ position: 'absolute', top: 360, transform: `translateX(${-dx + 717}px)` }}>
        {Array.from({ length: 16 }).map((_, k) => {
          const c = RAIL[k % RAIL.length];
          const isTarget = k === TARGET_I;
          return (
            <Img
              key={k}
              src={staticFile(`textures/live/${isTarget ? 'card4-hires.png' : c.file}`)}
              style={{
                position: 'absolute', left: k * (CARD_W + GAP), top: isTarget ? -16 : 0,
                width: CARD_W, borderRadius: 12,
                boxShadow: isTarget
                  ? '0 10px 32px rgba(31,28,23,0.18)'
                  : '0 4px 16px rgba(31,28,23,0.10)',
              }}
            />
          );
        })}
      </div>
    </AbsoluteFill>
  );
};

export const SpeedRampReal: React.FC = () => {
  const frame = useCurrentFrame();
  const fast = frame < 42 || frame > 83;
  return fast ? (
    <CameraMotionBlur shutterAngle={200} samples={20}>
      <Scene />
    </CameraMotionBlur>
  ) : (
    <Scene />
  );
};


  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/rhythm/speed-ramp-freeze/FreezeAnnotateReal.tsx, demos/rhythm/speed-ramp-freeze/SpeedRampReal.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