Motion · Light & emphasis

Scanline assemble fly-in

A scan line sweeps down an empty grid, flying each region's component in from off-screen — finishing as the scan ends.

1920×108016:930 fps5.1s · 153 framesremotionLicense: Apache-2.0#shotcraft#remotion#effects

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

When to use

An opening for "the page builds itself"; the core demo for AI website-builder/auto-layout products; a blank-to-finished capability narrative

Intention

Give "the page being generated" a believable construction order: the scan line is the progress bar, the components flying in from off-screen are the construction actions. The critical thing is that the last piece finishes assembling exactly as the scan finishes — if the scan line finishes early or a component trails behind, the causal link of "scan drives assembly" falls apart.

Duration & energy

  • Duration: About 4.6s (138f@30fps)
  • Energy: Medium-high (the scan line is steady, but every component fly-in is a burst, with rising density)

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

意图

让"页面被生成"这件事有一个可信的施工顺序:扫描线是施工进度条, 组件从画外飞进来贴合是施工动作。关键是扫完那一刻恰好装完最后一块 ——扫描线提前收工或组件拖尾都会让"扫描驱动装配"的因果关系散掉。

动效核心

  • 空页底只有暗网格(24px 双向 repeating-linear-gradient,

rgba(255,255,255,.025)、整层 opacity .5)——组件飞入前唯一可见的东西

  • 扫描线 t 0.05→0.72 匀速走 y −30→300,

0.03→0.08 淡入、0.72→0.77 淡出

  • 触发时刻由组件落点 y 反算:ft = 0.05 + ((y+30)/330)*0.67 − 0.02,

再钳制最小间隔 0.045;提前 0.02 起飞让组件与扫描线同时到位

  • 7 个组件各有独立飞入方案 from:[dx,dy] + rot:顶栏自上(0,−70),

logo 自左(−160,−30, −6°),模块卡自右(230,40, 5°),H1 自左下 (−260,60, −4°),CTA 自下(−60,130, 3°),页脚/社交自下方两侧

  • 飞入 seg(ft, ft+0.15, outBack) 过冲贴合;运动中按 1−a 加

blur(≤2.2px) 残影,a>0.97 即清零

  • 落位瞬间闪一道细亮边:inset:-3px 的 1px 强调色描边,

ft+0.11→+0.15 亮起、+0.15→+0.26 收掉

  • 状态行统计 a≥0.99 的组件数显示 BUILD · 0n/07,

0.80→0.86 后切 ASSEMBLY · COMPLETE

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 扫描线 | t 0.05→0.72 匀速,y −30→300 | 收工时刻要略晚于最后一块落位,才读作"扫完即装完" | | 触发换算 | ft = 0.05 + ((y+30)/330)·0.67 − 0.02 | 那个 −0.02 是提前起飞量:去掉就变成"扫过之后才动",慢半拍 | | 最小间隔 | prev + 0.045 | y 接近的页脚/社交会同时飞;钳制后才有"一块一块"的密度 | | 飞入位移 | 画外 60–260px,按组件所在方位定 | 位移小于 60px 读作"抖一下就位",不是从画外来 | | 飞入旋转 | ±3–8° | 超过 12° 就读作"翻滚",贴合时不服帖 | | 飞入缓动 | outBack,0.15 时长 | outBack 的过冲是"吸附贴合"的手感;outCubic 就成了滑入 | | 残影模糊 | blur ≤2.2px,随 1−a 衰减,a>0.97 清零 | 不清零会留一层永久糊;阈值必须留裕量 | | 落位闪边 | inset −3px、1px 强调色,0.04 亮 / 0.11 落 | 闪边是"咬合"的听觉替代品,去掉后落位没有实感 |

已知坑

  • 触发时刻与 plan 里的落点 y 绑定:换页面模板后每个组件的 y 要重测,

否则组件在扫描线到达前就飞入

  • filter 与 transform 同时作用于组件节点,落位后必须把 filter 设回

none(不是 blur(0))——留着合成层会持续吃 GPU

  • 闪边层是 append 到组件内部的子节点,组件本身若有 overflow:hidden

会把 inset:-3px 裁掉,需改成外层兄弟节点

  • 与 scanline-annotate-focus 共用同一套页面模板与扫描线;两张卡可以

串联(先装配、后分析),但扫描线要换向或留空档,否则读作重复

  • 页面内容为中性占位模板,ACCENT(#9fb6e8)可按项目替换

参考实现

demos/effects/scanline-assemble-flyin/ (ScanlineAssembleFlyin.tsx)

Reference implementation (Remotion)

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

Prompt

Use the video-shotcraft shot recipe "Scanline assemble fly-in" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/scanline-assemble-flyin.json
Raw TSX: https://gimgs.net/motion/scanline-assemble-flyin/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/effects/scanline-assemble-flyin/ScanlineAssembleFlyin.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 5.1s

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

9,607 chars · Apache-2.0Open raw template ↗
// ===== demos/effects/scanline-assemble-flyin/ScanlineAssembleFlyin.tsx =====
// scanline-assemble-flyin — Scanline Assemble 扫描装配组件飞入(motion-lab 定稿转原生 Remotion)
// 页面开场为空的暗底网格,一条亮扫描线自上而下掠过;扫过每个区块的落点后,该处组件从画外
// 四面八方飞入(左上 logo 自左、右侧模块卡自右、H1 自左下、CTA 自下、页脚社交自下方两侧),
// 带轻微过冲和残影模糊,贴合落位瞬间闪一道细亮边。扫完整页恰好装配完成。
// 页面内容为中性占位模板,强调色可按项目替换。设计坐标 480×270(DesignStage 等比放大)。
import React from 'react';
import { DesignStage, E, lerp, seg, useT } from '../../_fixtures/Motion';

export const SCANLINE_ASSEMBLE_FLYIN_DURATION = 138; // 4600ms @30fps

const MONO = "'SF Mono',Menlo,Consolas,monospace";
const SERIF = "Georgia,'Times New Roman',serif";
const ACCENT = '#9fb6e8'; // 模板强调色,可按项目替换
const A_RGB = '159,182,232';

/* ---- 每个组件的飞入方案:y=扫描落点(触发用),from=画外起点位移,rot=起始旋转 ---- */
type Plan = { key: string; y: number; from: [number, number]; rot: number; ft: number };
const PLAN: Plan[] = (() => {
  const ps: Plan[] = [
    { key: 'topbar', y: 30, from: [0, -70], rot: 0, ft: 0 },
    { key: 'logo', y: 53, from: [-160, -30], rot: -6, ft: 0 },
    { key: 'module', y: 100, from: [230, 40], rot: 5, ft: 0 },
    { key: 'h1', y: 148, from: [-260, 60], rot: -4, ft: 0 },
    { key: 'cta', y: 192, from: [-60, 130], rot: 3, ft: 0 },
    { key: 'footer', y: 257, from: [-140, 70], rot: 2, ft: 0 },
    { key: 'social', y: 262, from: [150, 70], rot: -3, ft: 0 },
  ];
  // 触发时刻:扫描线(0.05→0.72 纵扫 -30→300)到达组件落点 y,依序钳制最小间隔
  let prev = -1;
  for (const pl of ps) {
    pl.ft = Math.max(0.05 + ((pl.y + 30) / 330) * 0.67 - 0.02, prev + 0.045);
    prev = pl.ft;
  }
  return ps;
})();
const FT: Record<string, number> = Object.fromEntries(PLAN.map((pl) => [pl.key, pl.ft]));

export const ScanlineAssembleFlyin: React.FC = () => {
  const t = useT();

  // 扫描线纵扫 + 首尾淡入淡出
  const ly = lerp(seg(t, 0.05, 0.72), -30, 300);
  const lineOpacity = seg(t, 0.03, 0.08) * (1 - seg(t, 0.72, 0.77));

  // 组件飞入样式:outBack 过冲贴合 + 运动残影 blur(落位即清晰)
  const fly = ({ from, rot, ft }: Plan): React.CSSProperties => {
    const a = seg(t, ft, ft + 0.15, E.outBack);
    const vis = t >= ft ? 1 : 0;
    const speed = a > 0 && a < 0.97 ? 1 - a : 0;
    return {
      position: 'absolute',
      opacity: vis * Math.min(1, seg(t, ft, ft + 0.06) * 1.4),
      transform: `translate(${lerp(a, from[0], 0)}px,${lerp(a, from[1], 0)}px) rotate(${lerp(a, rot, 0)}deg)`,
      filter: speed > 0.03 ? `blur(${speed * 2.2}px)` : 'none',
    };
  };
  // 贴合瞬间闪一道细亮边(组件内层 inset:-3px 描边)
  const flash = (ft: number): React.CSSProperties => ({
    position: 'absolute',
    inset: -3,
    border: `1px solid rgba(${A_RGB},.9)`,
    borderRadius: 4,
    opacity: seg(t, ft + 0.11, ft + 0.15) * (1 - seg(t, ft + 0.15, ft + 0.26)),
    pointerEvents: 'none',
  });

  // 已落位组件计数(a≥0.99 视为贴合完成)
  const placed = PLAN.reduce((acc, pl) => acc + (seg(t, pl.ft, pl.ft + 0.15, E.outBack) >= 0.99 ? 1 : 0), 0);
  const done = seg(t, 0.8, 0.86);

  return (
    <DesignStage bg="#0a0b0e">
      {/* 页面底渐变:原 CSS 为 #101116→#0c0d11,样片 x264 近黑量化后实测为
          rgb(15,17,20)→rgb(10,12,16),按对照帧校准取 #0f1114→#0a0c10 */}
      <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(180deg,#0f1114,#0a0c10)' }}>
        {/* 空页底:暗网格(组件飞入前唯一可见的东西) */}
        <div
          style={{
            position: 'absolute',
            inset: 0,
            opacity: 0.5,
            background:
              'repeating-linear-gradient(0deg,transparent 0 23px,rgba(255,255,255,.025) 23px 24px),' +
              'repeating-linear-gradient(90deg,transparent 0 23px,rgba(255,255,255,.025) 23px 24px)',
          }}
        />
        {/* 顶栏(url pill + 状态)——整组自上方落下 */}
        <div style={{ ...fly(PLAN[0]), left: 0, top: 0, width: 480, height: 30 }}>
          <div style={{ position: 'absolute', left: 18, top: 11, padding: '3px 10px', border: '1px solid #2a2c33', borderRadius: 9, font: `500 7px ${MONO}`, color: '#8d93a0', letterSpacing: 1 }}>app.example.com</div>
          <div style={{ position: 'absolute', right: 18, top: 14, font: `500 7px ${MONO}`, color: '#565b66', letterSpacing: 1.5 }}>200 OK · TLS</div>
          <div style={flash(FT.topbar)} />
        </div>
        {/* logo:点阵 mark + 衬线字标——自左飞入 */}
        <div style={{ ...fly(PLAN[1]), left: 24, top: 35, width: 100, height: 18 }}>
          {[0, 1, 2, 3].map((i) => (
            <div key={i} style={{ position: 'absolute', width: 4, height: 4, borderRadius: '50%', background: '#e8e9ee', left: (i % 2) * 6, top: 2 + (i >> 1) * 6 }} />
          ))}
          <div style={{ position: 'absolute', left: 17, top: 0, font: `400 13px ${SERIF}`, color: '#eceef2' }}>
            Acme <i>Studio</i>
          </div>
          <div style={flash(FT.logo)} />
        </div>
        {/* H1 两行 + 注脚——自左下飞入 */}
        <div style={{ ...fly(PLAN[3]), left: 22, top: 64, width: 242, height: 84 }}>
          <div style={{ position: 'absolute', left: 0, top: 0, font: `400 29px ${SERIF}`, color: '#f2f3f6', letterSpacing: 0.3 }}>The headline for</div>
          <div style={{ position: 'absolute', left: 0, top: 36, font: `italic 400 29px ${SERIF}`, color: '#f2f3f6', letterSpacing: 0.3 }}>your product here</div>
          <div style={{ position: 'absolute', left: 1, top: 79, font: `500 6.5px ${MONO}`, color: '#565b66', letterSpacing: 1.5 }}>H1 · UI-SERIF / GEORGIA</div>
          <div style={flash(FT.h1)} />
        </div>
        {/* CTA 行——自下飞入 */}
        <div style={{ ...fly(PLAN[4]), left: 22, top: 166, width: 136, height: 26 }}>
          <div style={{ position: 'absolute', left: 0, top: 0, padding: '6px 13px', border: '1px solid #3a3d46', borderRadius: 12, font: `600 7.5px ${MONO}`, color: '#e8e9ee', letterSpacing: 1.5 }}>GET STARTED</div>
          <div style={{ position: 'absolute', left: 100, top: 7, font: `500 7.5px ${MONO}`, color: '#6a707c', letterSpacing: 1.5 }}>DOCS</div>
          <div style={flash(FT.cta)} />
        </div>
        {/* 右侧模块卡——自右飞入 */}
        {/* 原采集页为 content-box:162×150 外加 1px 边框(总 164×152),补 boxSizing 对齐几何 */}
        <div style={{ ...fly(PLAN[2]), left: 296, top: 52, width: 162, height: 150, boxSizing: 'content-box', background: '#121319', border: '1px solid #23252d', borderRadius: 5 }}>
          <div style={{ position: 'absolute', left: 10, top: 9, font: `500 6.5px ${MONO}`, color: '#7c828e', letterSpacing: 1.5 }}>WORK</div>
          <div style={{ position: 'absolute', right: 10, top: 9, font: `500 6.5px ${MONO}`, color: '#565b66', letterSpacing: 1.5 }}>04 / 08</div>
          <div style={{ position: 'absolute', left: 0, top: 24, width: '100%', height: 1, background: '#1e2028' }} />
          <div style={{ position: 'absolute', left: 0, top: 44, width: '100%', textAlign: 'center', font: `italic 400 36px ${SERIF}`, color: '#f4f5f8' }}>sample</div>
          <div style={{ position: 'absolute', left: 0, top: 104, width: '100%', height: 1, background: '#1e2028' }} />
          <div style={{ position: 'absolute', left: 10, top: 112, font: `500 6px ${MONO}`, color: '#6a707c', letterSpacing: 1.5 }}>KINETIC TYPE · 04</div>
          {[0, 1, 2, 3].map((i) => (
            <div key={i} style={{ position: 'absolute', left: 10 + i * 30, top: 124, width: 24, height: 16, background: '#1a1c23', borderRadius: 2 }} />
          ))}
          <div style={{ position: 'absolute', right: 8, top: 129, font: `500 6px ${MONO}`, color: '#565b66' }}>00:30</div>
          <div style={flash(FT.module)} />
        </div>
        {/* 页脚——自左下飞入 */}
        <div style={{ ...fly(PLAN[5]), left: 18, top: 243, width: 220, height: 14 }}>
          <div style={{ position: 'absolute', left: 0, top: 3, font: `500 6.5px ${MONO}`, color: '#4c515c', letterSpacing: 1.5 }}>A PRODUCT OF ACME · ACME LABS, INC.</div>
          <div style={flash(FT.footer)} />
        </div>
        {/* 社交 chip——自右下飞入 */}
        <div style={{ ...fly(PLAN[6]), left: 352, top: 239, width: 108, height: 16 }}>
          {/* 同为 content-box:11×11 外加 1px 边框(总 13×13) */}
          <div style={{ position: 'absolute', left: 0, top: 2, width: 11, height: 11, boxSizing: 'content-box', border: '1px solid #3a3d46', borderRadius: 2, font: `600 7px ${MONO}`, color: '#c9cdd6', textAlign: 'center', lineHeight: '11px' }}>x</div>
          <div style={{ position: 'absolute', left: 17, top: 3, font: `600 7.5px ${MONO}`, color: '#c9cdd6', letterSpacing: 1.5 }}>@USERNAME</div>
          <div style={flash(FT.social)} />
        </div>
      </div>

      {/* ---- 扫描线(渐变拖尾 + 亮芯) ---- */}
      <div
        style={{
          position: 'absolute',
          left: 0,
          top: 0,
          width: '100%',
          height: 40,
          background: `linear-gradient(180deg,transparent,rgba(${A_RGB},.07) 55%,rgba(${A_RGB},.02) 96%,transparent)`,
          transform: `translateY(${ly - 40}px)`,
          opacity: lineOpacity,
        }}
      >
        <div
          style={{
            position: 'absolute',
            bottom: 0,
            left: 0,
            width: '100%',
            height: 1.5,
            background: 'rgba(238,244,255,.9)',
            boxShadow: `0 0 7px ${ACCENT},0 0 18px rgba(${A_RGB},.35)`,
          }}
        />
      </div>

      {/* ---- 顶部状态行:BUILD 计数 → ASSEMBLY · COMPLETE ---- */}
      <div
        style={{
          position: 'absolute',
          left: '50%',
          top: 14,
          transform: 'translateX(-50%)',
          font: `600 7px ${MONO}`,
          letterSpacing: 2,
          color: done >= 1 ? ACCENT : '#8d93a0',
          opacity: seg(t, 0.02, 0.07),
          zIndex: 60,
        }}
      >
        {done >= 1 ? 'ASSEMBLY · COMPLETE' : `BUILD · 0${placed}/0${PLAN.length}`}
      </div>
    </DesignStage>
  );
};


  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/effects/scanline-assemble-flyin/ScanlineAssembleFlyin.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