Motion · Tipografia

Gradient word charge sweep

A gradient of colored light sweeps fast left to right to "charge" a keyword in a black-scene tagline — brightest at the wavefront, decaying behind it, then settling into a steady lightning-breathing glow.

1920×108016:930 fps3.71s · 111 quadrosremotionLicença: Apache-2.0#shotcraft#remotion#typography#effects

Fonte: video-shotcraft (Vincent Wei) ↗ · Apache-2.0 · Composição Remotion (React/TSX) — vídeo de prévia, sem player no navegador

When to use

Charging a single verb/selling-point word in a tagline (Supercharged/faster/AI…); the typography moment of a high-energy peak segment; the slogan frame of a black-scene brand film.

Intention

Within an all-white sentence, only the keyword gets "electrified": a gradient of colored light sweeps fast left to right across the characters, like an energy injection — brightest the instant it passes (wavefront trail), then decaying to a steady state; once filled, fine lightning arcs jump between characters and the word keeps a breathing glow. Three crux points: fast (sweep-charge 15–20f; slower reads as a progress bar), brightest at the wavefront (the character that just lit up is strongest — without this gradient it's just a static clipped gradient), and restrained glow (After-Effects-style layered bloom, but don't let it smear into a blob; the lightning should be thin and sparse, a garnish not a flood).

Duration & energy

  • Duration: Sweep-charge ~15–20f (must be fast) + steady lightning-breathing 1–2s; whole segment 2.5–3.5s
  • Energy: High (a single-word peak; everything before and after should yield to it)

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

意图

整句白字里只有关键词被"通电":渐变彩光从左到右快速扫过字符, 像能量注入——扫过的瞬间最亮(波前尾迹),随后衰减到稳态; 填满后字符之间跳起勾连的细闪电,词保持泛光呼吸。三个命门: 快(扫充 15–20f,慢了读作进度条)、波前最亮(刚点亮的 字符效果最强,没有这个梯度就是静态渐变裁切)、辉光克制 (AE 式多层泛光但别糊成一团,闪电细而稀疏才是点缀)。

动效核心

  • 渐变填充:关键词做双层文字(白/渐变彩),彩层用从左到右推进的

mask 揭开;斜切 mask 边缘略糊(~8% 词宽)

  • 波前尾迹增亮:跟随 mask 前沿一条 ~34% 词宽的增亮带,波前处

辉光最强、向后衰减;填充结束后 ~10f 淡出到稳态

  • 辉光:4 层泛光(环境大柔光+中晕+近核+亮核),稳态 opacity

0.55–0.72 档(实测:0.75–0.95 档被裁"泛光感有点强")

  • 闪电:填满后字符间勾连细闪电,紫红系三层描边(外晕 6/中 2.4/

亮核 1.4),mulberry32 种子随机跳位闪烁,稀疏零星(不是持续电弧)

  • 禁:填充过程不带跟随移动的光点/星光(实测判例:跟随白色光点读作廉价装饰,删)

参数表

| 参数 | 典型值 | 调节手感 | |------|--------|----------| | 扫充时长 | 15–20f | 原片"速度很快";>28f 读作进度条 | | 波前增亮带 | ~34% 词宽,波前最亮向后衰减 | 无梯度=静态渐变;带太宽=整词一起亮没有"注入"感 | | 稳态泛光 | 4 层 opacity 0.55/0.55/0.62/0.72 | 0.75+ 档被裁太强;辉光层数<3 读作纯色 | | 闪电线宽 | 外 6/中 2.4/核 1.4 @1080p | 加倍即被裁"粗";颜色偏紫红(magenta 216,60,190 系) | | 闪电频率 | 稀疏零星(同屏 ≤2 道,间歇 >8f) | 频率高读作电弧故障;实测 demo 仍略密于原片 |

已知坑

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

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

  • 判例三条(用户逐字意见沉淀):①填充过程禁跟随光点;②闪电

必须紫红+细;③泛光宁弱勿强,波前梯度补足冲击力

  • 与 type-rhythm-sync/cel-flash-stomp 分工:那些是节拍/砸字;

本卡是单词"充能"的能量语义,词必须是句中卖点词

  • 关键词外的正文保持纯白静止——全句都动就没有"只有它通电"的戏

参考实现

demos/typography/gradient-word-sweep/ (GradientWordSweep.tsx) 原片出处:clickup-30.mp4

Reference implementation (Remotion)

  • demos/typography/gradient-word-sweep/GradientWordSweep.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/typography/gradient-word-sweep/GradientWordSweep.tsx

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

Tudo nesta página também é legível por máquina. Aponte seu agente para o JSON ou adicione o servidor MCP do gimgs e peça para buscar templates de motion.

  1. Baixe o JSON — contém a receita do plano (timing, easing, tabela de parâmetros, armadilhas) e o código TSX completo.
  2. Clone o repositório video-shotcraft; o componente importa fixtures de demos/_fixtures e helpers de assets/lib.
  3. Registre o componente como <Composition> em um projeto Remotion e renderize com `npx remotion render`, ou reimplemente a receita no seu próprio engine.
curl -s https://gimgs.net/motion/gradient-word-sweep.json
git clone https://github.com/Vincentwei1021/video-shotcraft
# component: demos/typography/gradient-word-sweep/GradientWordSweep.tsx
npx remotion render <CompositionId> out.mp4
claude mcp add --transport http gimgs https://gimgs.net/mcp
# then: search_motion_templates / get_motion_template("gradient-word-sweep")

Prompt

Use the video-shotcraft shot recipe "Gradient word charge sweep" (Remotion composition, Apache-2.0) published on gimgs.net.
Recipe JSON (prompt = timing/easing/parameter table/pitfalls, files = TSX source): https://gimgs.net/motion/gradient-word-sweep.json
Raw TSX: https://gimgs.net/motion/gradient-word-sweep/template.html
Repository: https://github.com/Vincentwei1021/video-shotcraft  (component: demos/typography/gradient-word-sweep/GradientWordSweep.tsx; imports demos/_fixtures and assets/lib)
Spec: 1920x1080, 30 fps, 3.71s

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,931 chars · Apache-2.0Abrir template bruto ↗
// ===== demos/typography/gradient-word-sweep/GradientWordSweep.tsx =====
// gradient-word-sweep v3 —— 批次 12 按用户意见微调(v2 结构保留):
// 1) 闪电偏紫红色 + 线宽调细(约减半);
// 2) 整体泛光强度略降(各辉光层 opacity 下调);
// 3) 波前尾迹梯度:刚被点亮的字符辉光最强,随扫过距离衰减到稳态
//    (trailing-window 增亮层,填充结束后淡出到稳态呼吸)。
import React from 'react';
import { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion';

const mulberry32 = (a: number) => () => {
  let t = (a += 0x6d2b79f5);
  t = Math.imul(t ^ (t >>> 15), t | 1);
  t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
  return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};

const FONT = '"Avenir Next", Futura, "Helvetica Neue", sans-serif';
// 截图 5:S 偏蓝青 → 中段紫 → 粉 → 尾部琥珀
const GRAD = 'linear-gradient(92deg, #59c2ff 0%, #9d6bff 32%, #ff6ed4 62%, #ffc46b 100%)';

const FILL_START = 12;
const FILL_END = 30; // 18 帧 ≈ 0.6s,快扫
const LIGHT_START = FILL_END + 3;

// ---------- 种子化噪声与闪电 ----------
const rand = mulberry32(20260718);
const FLICKER: number[] = Array.from({ length: 160 }, () => rand());

type Bolt = { d: string; long: boolean };
const makeLongBolt = (r: () => number): Bolt => {
  // 词上方勾连长弧:横跨若干字符,锯齿折线
  const x0 = 30 + r() * 220;
  const x1 = x0 + 160 + r() * 320;
  const yBase = 38 + r() * 42;
  const n = 7 + Math.floor(r() * 4);
  let d = `M ${x0.toFixed(1)} ${(yBase + 26 + r() * 20).toFixed(1)}`;
  for (let i = 1; i <= n; i++) {
    const x = x0 + ((x1 - x0) * i) / n + (r() - 0.5) * 22;
    const arch = Math.sin((i / n) * Math.PI) * -22; // 中段拱起
    const y = yBase + arch + (r() - 0.5) * 30 + (i === n ? 30 + r() * 18 : 0);
    d += ` L ${x.toFixed(1)} ${y.toFixed(1)}`;
  }
  return { d, long: true };
};
const makeShortBolt = (r: () => number): Bolt => {
  // 字符之间的短勾连:竖向小锯齿
  const x = 60 + r() * 540;
  const y0 = 72 + r() * 24;
  const y1 = y0 + 55 + r() * 45;
  const n = 4 + Math.floor(r() * 3);
  let d = `M ${x.toFixed(1)} ${y0.toFixed(1)}`;
  for (let i = 1; i <= n; i++) {
    const y = y0 + ((y1 - y0) * i) / n;
    const xx = x + (r() - 0.5) * 30;
    d += ` L ${xx.toFixed(1)} ${y.toFixed(1)}`;
  }
  return { d, long: false };
};

const BOLTS: Bolt[] = Array.from({ length: 16 }, (_, i) =>
  i % 3 === 0 ? makeShortBolt(rand) : makeLongBolt(rand),
);
// 闪烁事件:帧窗内某条闪电点亮,随机跳位
type Flash = { at: number; life: number; bolt: number };
const FLASHES: Flash[] = Array.from({ length: 36 }, () => ({
  at: LIGHT_START + Math.floor(rand() * 70),
  life: 2 + Math.floor(rand() * 4),
  bolt: Math.floor(rand() * BOLTS.length),
}));

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

  const enter = interpolate(frame, [0, 12], [0, 1], {
    extrapolateRight: 'clamp',
    easing: Easing.out(Easing.cubic),
  });

  // 快速填充进度
  const p = interpolate(frame, [FILL_START, FILL_END], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
    easing: Easing.out(Easing.quad),
  });
  const pPct = p * 100;
  const filling = frame >= FILL_START && frame <= FILL_END + 4;
  const headFade = interpolate(frame, [FILL_END, FILL_END + 6], [1, 0], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });

  // 波前尾迹:刚点亮字符最亮,向后衰减;填充结束后整体淡出到稳态
  const trailFade = interpolate(frame, [FILL_END, FILL_END + 10], [1, 0], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
  });
  const TRAIL = 34; // 尾迹长度(% 宽度)
  const trailMask =
    `linear-gradient(90deg, transparent 0%, transparent ${Math.max(0, pPct - TRAIL)}%, ` +
    `rgba(0,0,0,0.9) ${Math.max(0, pPct - 3)}%, rgba(0,0,0,0.9) ${Math.min(100, pPct + 1)}%, ` +
    `transparent ${Math.min(100, pPct + 6)}%)`;

  const noise = FLICKER[Math.min(frame, FLICKER.length - 1)];
  // 当前帧活跃闪电
  const active = FLASHES.filter((f) => frame >= f.at && frame < f.at + f.life);
  const boltBoost = active.length > 0 ? 0.35 : 0;
  // 辉光呼吸:填满后带噪声微闪 + 闪电时增亮
  const glowLvl =
    interpolate(frame, [FILL_START, FILL_END], [0, 1], {
      extrapolateLeft: 'clamp',
      extrapolateRight: 'clamp',
    }) *
    (0.82 + 0.18 * noise) +
    boltBoost;

  // 软边遮罩:辉光层的填充边缘不生硬
  const softMask = (soft: number): string =>
    p >= 1
      ? 'none'
      : `linear-gradient(90deg, #000 0%, #000 ${Math.max(0, pPct - soft)}%, transparent ${Math.min(100, pPct + soft * 0.6)}%)`;
  const maskStyle = (soft: number): React.CSSProperties =>
    p >= 1
      ? {}
      : {
          WebkitMaskImage: softMask(soft),
          maskImage: softMask(soft),
        };

  const lineStyle: React.CSSProperties = {
    fontFamily: FONT,
    fontWeight: 700,
    fontStyle: 'italic',
    fontSize: 92,
    letterSpacing: -1,
    lineHeight: 1.28,
    color: '#ffffff',
    whiteSpace: 'nowrap',
  };

  const gradText: React.CSSProperties = {
    position: 'absolute',
    inset: 0,
    backgroundImage: GRAD,
    WebkitBackgroundClip: 'text',
    backgroundClip: 'text',
    color: 'transparent',
  };

  return (
    <AbsoluteFill style={{ background: '#050505', justifyContent: 'center', alignItems: 'center' }}>
      {/* 词后方环境泛光(大半径,随充能增强) */}
      <div
        style={{
          position: 'absolute',
          left: 500,
          top: 340,
          width: 820,
          height: 320,
          borderRadius: '50%',
          background:
            'radial-gradient(closest-side, rgba(180,110,255,0.6), rgba(255,110,212,0.25) 55%, transparent 78%)',
          filter: 'blur(38px)',
          opacity: 0.55 * glowLvl,
        }}
      />
      <div
        style={{
          textAlign: 'center',
          opacity: enter,
          transform: `translateY(${(1 - enter) * 36}px)`,
        }}
      >
        <div style={lineStyle}>
          <span style={{ position: 'relative', display: 'inline-block' }}>
            <span>Supercharged</span>
            {/* AE 式辉光:大半径柔光层(最糊) */}
            <span
              aria-hidden
              style={{
                ...gradText,
                ...maskStyle(14),
                filter: 'blur(46px) saturate(1.6)',
                opacity: 0.55 * glowLvl,
                transform: 'scale(1.05)',
              }}
            >
              Supercharged
            </span>
            {/* 中晕层 */}
            <span
              aria-hidden
              style={{
                ...gradText,
                ...maskStyle(10),
                filter: 'blur(18px) saturate(1.4) brightness(1.15)',
                opacity: 0.62 * glowLvl,
              }}
            >
              Supercharged
            </span>
            {/* 近核柔光 */}
            <span
              aria-hidden
              style={{
                ...gradText,
                ...maskStyle(7),
                filter: 'blur(6px) brightness(1.25)',
                opacity: 0.72 * Math.min(1, glowLvl + 0.1),
              }}
            >
              Supercharged
            </span>
            {/* 清晰渐变本体 */}
            <span
              aria-hidden
              style={{
                ...gradText,
                clipPath: `inset(-25% ${100 - pPct}% -25% 0)`,
              }}
            >
              Supercharged
            </span>
            {/* 波前尾迹增亮:刚点亮字符辉光最强,向后衰减到稳态 */}
            {trailFade > 0.01 && (
              <span
                aria-hidden
                style={{
                  ...gradText,
                  WebkitMaskImage: trailMask,
                  maskImage: trailMask,
                  filter: 'blur(9px) saturate(1.7) brightness(1.7)',
                  opacity: 0.95 * trailFade,
                }}
              >
                Supercharged
              </span>
            )}
            {/* 填充头字符过曝亮核(字符形状,非独立光点;仅填充期间) */}
            {filling && p < 1 && (
              <span
                aria-hidden
                style={{
                  position: 'absolute',
                  inset: 0,
                  color: '#fff',
                  clipPath: `inset(-25% ${Math.max(0, 100 - pPct)}% -25% ${Math.max(0, pPct - 10)}%)`,
                  filter: 'blur(3px)',
                  opacity: 0.9 * headFade,
                  textShadow: '0 0 22px rgba(255,255,255,0.9), 0 0 55px rgba(216,150,255,0.8)',
                }}
              >
                Supercharged
              </span>
            )}
            {/* 勾连闪电:填充完成后,字符之间/词上方随机闪烁跳位 */}
            <svg
              aria-hidden
              viewBox="0 0 700 240"
              style={{
                position: 'absolute',
                left: -25,
                top: -62,
                width: 700,
                height: 240,
                overflow: 'visible',
                pointerEvents: 'none',
              }}
            >
              {active.map((f, i) => {
                const b = BOLTS[f.bolt];
                const decay = 1 - (frame - f.at) / f.life;
                return (
                  <g key={`${f.at}-${i}`} opacity={Math.min(1, 1.1 * decay)}>
                    <path
                      d={b.d}
                      fill="none"
                      stroke="rgba(216,60,190,0.65)"
                      strokeWidth={b.long ? 6 : 4.5}
                      strokeLinejoin="miter"
                      style={{ filter: 'blur(6px)' }}
                    />
                    <path
                      d={b.d}
                      fill="none"
                      stroke="rgba(235,110,215,0.9)"
                      strokeWidth={b.long ? 2.4 : 1.9}
                      strokeLinejoin="miter"
                      style={{ filter: 'blur(1.5px)' }}
                    />
                    <path
                      d={b.d}
                      fill="none"
                      stroke="#ffd8f2"
                      strokeWidth={b.long ? 1.4 : 1.1}
                      strokeLinejoin="miter"
                    />
                  </g>
                );
              })}
            </svg>
          </span>{' '}
          <span>performance</span>
        </div>
        <div style={lineStyle}>with rock-solid reliability</div>
      </div>
    </AbsoluteFill>
  );
};


  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/typography/gradient-word-sweep/GradientWordSweep.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
—

Mais nesta categoria