{
  "slug": "light-play-moves",
  "title": "Light play moves",
  "tagline": "Three light styles: spotlight-sweep sweeping a spotlight across text, sheen a single sheen sweep, and halation-bloom a crash-stop glow bloom.",
  "description": "Three light styles: spotlight-sweep sweeping a spotlight across text, sheen a single sheen sweep, and halation-bloom a crash-stop glow bloom.",
  "category": "effects",
  "tags": [
    "shotcraft",
    "remotion",
    "effects",
    "typography"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 5.38,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/light-play-moves/poster-dce43209c3ff.jpg",
    "video": "https://gimgs.net/motion/media/light-play-moves/video-7a146cfee821.mp4"
  },
  "variants": [
    {
      "key": "spotlight-sweep",
      "label": "spotlight-sweep · Spotlight sweep",
      "description": "Two layers of the same text: a dark version stays at 0.07 opacity, a bright version follows a radial mask tied to the light spot; the cone of light swings back and forth twice before freezing at full brightness.",
      "use": "A dark-scene big-title reveal opener",
      "video": "https://gimgs.net/motion/media/light-play-moves/video-7a146cfee821.mp4",
      "poster": "https://gimgs.net/motion/media/light-play-moves/poster-dce43209c3ff.jpg"
    },
    {
      "key": "sheen-sweep",
      "label": "sheen-sweep · Sheen sweep",
      "description": "A 45° highlight band translateX-sweeps once across a deep-ink card, clipped by overflow:hidden rounded corners.",
      "use": "A single coronation during a hero card's still moment",
      "video": "https://gimgs.net/motion/media/light-play-moves/video-26f88c8266ca.mp4",
      "poster": "https://gimgs.net/motion/media/light-play-moves/poster-sheen-sweep-d2f7289dad37.jpg"
    },
    {
      "key": "halation-bloom",
      "label": "halation-bloom · Halation bloom",
      "description": "A duplicated copy of the text underneath, blurred and brightened as a glow layer; on the crash-stop frame it surges into a ring then settles into steady soft light.",
      "use": "An impact boost on a crash-zoom stop frame",
      "video": "https://gimgs.net/motion/media/light-play-moves/video-5547a29e0e8e.mp4",
      "poster": "https://gimgs.net/motion/media/light-play-moves/poster-halation-bloom-4dabd44a5230.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/light-play-moves.html",
  "json_url": "https://gimgs.net/motion/light-play-moves.json",
  "template_url": "https://gimgs.net/motion/light-play-moves/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/light-play-moves.md",
  "updated_at": "2026-09-24T07:40:05.077Z",
  "prompt": "## When to use\n\nTreats light as a fourth kind of brushstroke (sweep/wipe/bloom): a dark-scene title reveal (A), crowning a hero card (B), impact on a crash-stop frame (D).\n\n## Intention\n\nThe library previously only had one \"light\" technique; these three styles build light into a system, three brushstrokes with three jobs: A is **sweeping** — a title, almost invisible in the dark, gets swept bright by an oscillating cone of light; B is **wiping** — a 45° highlight band sweeps once across a deep-ink hero card, clipped by its rounded corners, a silent coronation; D is **blooming** — on a white-text crash-stop frame, a ring of soft glow surges up and bursts, then settles into a resting breathing glow. All of this lives on dark backgrounds or dark hero subjects — **glow is invisible on white backgrounds** is the bedrock of this whole card.\n\n## Duration & energy\n\n- Duration: A: ~5.3s / B: ~4.7s / D: ~4.8s\n- Energy: A: Medium / B: Low / D: High\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n库内\"光\"一直只有 spotlight-hero-card 一个点射手法；这三式把光做成\n体系，三种笔触三种职责：A 是**扫**——暗场里几乎不可见的标题被锥形\n光摆动扫亮，光到哪哪亮，与 spotlight-hero-card 的区别是那个打卡片、\n这个扫文字；B 是**擦**——深墨主角卡上一道 45° 高光带缓扫一次，被\n圆角裁住，无声的加冕；D 是**晕**——白字撞停帧一圈柔晕猛涨炸开再\n回落成驻留呼吸光，与 B 的区别是 B 移动扫过、D 驻留在高光边缘。\n全部深底或深主角——**白底上提亮不可见**（判例）是这整张卡的地基。\n\n## 三式选型\n| 式 | 做法 | 适用 |\n|----|------|------|\n| A spotlight-sweep | 双层同文本：暗版 0.07 常驻，亮版 radial mask 随光斑走，锥形光摆两个来回后全亮定格 | 暗场大标题揭示开场 |\n| B sheen-sweep | 深墨卡上 45° 高光带 translateX 扫一次，overflow hidden 圆角裁剪 | 主角卡静止段的一次加冕 |\n| D halation-bloom | 文字复制底层 blur+brightness 当晕层，撞停帧起猛涨一圈回落成稳态柔光 | crash-zoom 撞停帧的冲击加成 |\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| A 反差 | 暗版 0.07 vs 全亮 1.0，白 #f5f5f3 对 #2a2a28 暗场 | 反差是本式全部；暗版 >0.15 就没有\"点亮\"了 |\n| A 光斑 | circle 380px（0.95 硬芯+柔边），x = 960+560·sin(2πf/55)，两来回至 f110 | f110–125 全亮 out-cubic、光效线性消散（扩散/消散解耦判例） |\n| B 高光带 | 宽 1.6×卡宽，115° 渐变峰值 rgba(255,255,255,0.32)，40–68f inOut cubic 扫一次 | 0.32 在深墨底上足够响；双向 clamp 保证只扫一次 |\n| D 晕层 | blur 22px + brightness 1.8；scale 1→1.3 用 6f out-cubic 猛涨，opacity 20f 线性回落 0.35 再 15f 缓收 0.22 稳态 | 扩散 out-cubic/消散线性解耦（判例）；同快涨快消读作闪光灯 |\n| 摘罩 | 三式光效层全部条件挂载，扫完/消散完即卸载 | opacity 0 残留 = 永不真静止（feTurbulence 摘罩判例同族） |\n| 收尾 | A 35f / B 72f / D 89f 真静止 | — |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- **全片光效合计 ≤2 次**——光是最快掉价的手法，每个卡都闪一下\n  等于廉价模板\n- B 式是高标准重试通过：旧否决（2026-07-13 判例）针对当时实现不好看，\n  非路径不可行；四约束 = 单点+主角+圆角裁剪+只扫一次，违反任何\n  一条回到否决区\n- 深底是地基：三式全部依赖深底/深主角，白底上做光效直接跳过本卡\n- D 与 crash-zoom-punch 撞停帧天然联动（撞停即起晕）；A 与\n  spotlight-hero-card 同片二选一，都是\"聚光\"语汇\n\n## 参考实现\ndemos/effects/light-play-moves/\n（HalationBloom.tsx / SheenSweepRetry.tsx / SpotlightSweepReveal.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/effects/light-play-moves/HalationBloom.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/light-play-moves/HalationBloom.tsx\n- demos/effects/light-play-moves/SheenSweepRetry.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/light-play-moves/SheenSweepRetry.tsx\n- demos/effects/light-play-moves/SpotlightSweepReveal.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/light-play-moves/SpotlightSweepReveal.tsx\n\nThe component imports shared fixtures from `demos/_fixtures` and helpers from `assets/lib` in the repo; SKILL.md there documents the render workflow.\n",
  "flow": [
    {
      "step": "Read the recipe",
      "note": "Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points."
    },
    {
      "step": "Get the code",
      "tool": "git",
      "cmd": "git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install"
    },
    {
      "step": "Register + render",
      "tool": "remotion",
      "cmd": "npx remotion render <CompositionId> out.mp4   # component: demos/effects/light-play-moves/HalationBloom.tsx, demos/effects/light-play-moves/SheenSweepRetry.tsx, demos/effects/light-play-moves/SpotlightSweepReveal.tsx"
    },
    {
      "step": "Or re-implement",
      "note": "Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing."
    }
  ],
  "files": [
    {
      "path": "demos/effects/light-play-moves/HalationBloom.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/light-play-moves/HalationBloom.tsx"
    },
    {
      "path": "demos/effects/light-play-moves/SheenSweepRetry.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/light-play-moves/SheenSweepRetry.tsx"
    },
    {
      "path": "demos/effects/light-play-moves/SpotlightSweepReveal.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/light-play-moves/SpotlightSweepReveal.tsx"
    }
  ],
  "contract": "React/Remotion composition (TSX) from the open-source video-shotcraft library (Apache-2.0). It is not a browser-playable template: clone the repo, register the component in a Remotion project and render with `npx remotion render`. The recipe (prompt) describes timing, easing and parameters so you can also re-implement the shot in any engine.",
  "render": {
    "engine": "remotion",
    "frames": 161,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/light-play-moves/HalationBloom.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/light-play-moves/SheenSweepRetry.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/effects/light-play-moves/SpotlightSweepReveal.tsx"
    ],
    "note": "Clone https://github.com/Vincentwei1021/video-shotcraft (npm install). The component lives under demos/{category}/{name}/ and imports shared fixtures from demos/_fixtures and helpers from assets/lib. Copy it (with those imports) into a Remotion project, register a <Composition> with the template's width/height/fps/duration in Root.tsx, then `npx remotion render <CompositionId> out.mp4`. SKILL.md in the repo documents the full Claude Code / Codex workflow. The prompt below is the shot recipe (timing, easing, parameter table, pitfalls) — enough to re-implement the shot in HTML/WAAPI or another engine if you prefer."
  },
  "source": "// ===== demos/effects/light-play-moves/HalationBloom.tsx =====\n// halation-bloom —— 高光晕染\n// 深灰底大白字 \"10x\" crash-zoom 急停入场（2.4→1，7f in-quad + 2f 回弹）。\n// 撞停帧起：底层复制文字（blur+提亮的白色晕层）猛涨一圈（scale 1→1.3，6f out-cubic 扩散），\n// 再 20f 线性回落到 0.35 驻留柔晕（扩散/消散解耦判例），随后 15f 缓收到 0.22 稳态。\n// 深底白字：白底上提亮不可见判例。收尾真静止 ≥40f。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\n\nconst BG = '#2a2a28';\nconst WHITE = '#f7f7f5';\nconst MID = '#8f8f8d';\n\n// —— 时间轴（30fps）——\nconst ZOOM_START = 8; // 入场起始\nconst IMPACT = 15; // 撞停帧（7f 急速缩入）\nconst REBOUND_END = 17; // 2f 回弹\nconst POP_END = IMPACT + 6; // 晕层猛涨 6f → f21\nconst FALL_END = POP_END + 20; // 20f 线性回落 → f41\nconst SETTLE_END = FALL_END + 15; // 15f 缓收 → f56，此后全静止\n// 总时长 145f → 静止 89f ≥ 40f\n\nconst TextBlock: React.FC<{ color: string }> = ({ color }) => (\n  <div\n    style={{\n      fontFamily: 'Helvetica, Arial, sans-serif',\n      fontSize: 260,\n      fontWeight: 800,\n      color,\n      letterSpacing: '-0.02em',\n      lineHeight: 1,\n      whiteSpace: 'nowrap',\n    }}\n  >\n    10x\n  </div>\n);\n\nexport const HalationBloom: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // —— crash-zoom 入场：scale 2.4 → 0.94（7f in-quad 加速撞停）→ 1（2f 回弹）——\n  const zoomIn = interpolate(frame, [ZOOM_START, IMPACT], [2.4, 0.94], {\n    easing: Easing.in(Easing.quad),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const rebound = interpolate(frame, [IMPACT, REBOUND_END], [0.94, 1], {\n    easing: Easing.out(Easing.quad),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const textScale = frame < IMPACT ? zoomIn : rebound;\n  const textOpacity = interpolate(frame, [ZOOM_START, ZOOM_START + 3], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  // —— 晕层：撞停帧起条件挂载 ——\n  // 扩散（scale）：out-cubic，6f 猛涨 1 → 1.3\n  const bloomScale = interpolate(frame, [IMPACT, POP_END], [1, 1.3], {\n    easing: Easing.out(Easing.cubic),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  // 消散（opacity）：与扩散解耦——猛涨段保持全亮，随后 20f 线性回落到 0.35\n  const bloomFall = interpolate(frame, [POP_END, FALL_END], [1, 0.35], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  // 驻留晕 15f 缓收到 0.22 稳态\n  const bloomSettle = interpolate(frame, [FALL_END, SETTLE_END], [0.35, 0.22], {\n    easing: Easing.out(Easing.quad),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const bloomOpacity = frame < FALL_END ? bloomFall : bloomSettle;\n\n  return (\n    <div\n      style={{\n        width: 1920,\n        height: 1080,\n        background: BG,\n        position: 'relative',\n        overflow: 'hidden',\n      }}\n    >\n      {/* 小标签，全程静态 */}\n      <div\n        style={{\n          position: 'absolute',\n          top: 110,\n          width: '100%',\n          textAlign: 'center',\n          fontFamily: 'Helvetica, Arial, sans-serif',\n          fontSize: 30,\n          fontWeight: 700,\n          letterSpacing: '0.35em',\n          color: MID,\n        }}\n      >\n        HALATION BLOOM\n      </div>\n\n      {/* 晕层：文字复制底层，blur + 提亮，撞停帧起条件挂载 */}\n      {frame >= IMPACT && (\n        <div\n          style={{\n            position: 'absolute',\n            inset: 0,\n            display: 'flex',\n            alignItems: 'center',\n            justifyContent: 'center',\n            transform: `scale(${bloomScale})`,\n            opacity: bloomOpacity,\n            filter: 'blur(22px) brightness(1.8)',\n          }}\n        >\n          <TextBlock color={WHITE} />\n        </div>\n      )}\n\n      {/* 本体文字：crash-zoom 急停 */}\n      <div\n        style={{\n          position: 'absolute',\n          inset: 0,\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          transform: `scale(${textScale})`,\n          opacity: textOpacity,\n        }}\n      >\n        <TextBlock color={WHITE} />\n      </div>\n    </div>\n  );\n};\n\n\n// ===== demos/effects/light-play-moves/SheenSweepRetry.tsx =====\n// sheen-sweep-retry —— 单点扫光（高标准重试）\n// 深墨大卡居中，一道 45° 高光带在 40–68f 从左外扫到右外，仅此一次。\n// 约束：单点(只扫主角卡)、圆角裁剪(overflow hidden)、扫前扫后完全静止。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G } from '../../_fixtures/Fixtures';\n\nconst CARD_W = 760;\nconst CARD_H = 420;\nconst SHEEN_W = CARD_W * 1.6; // 1216\n\nexport const SheenSweepRetry: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  // 扫光：40–68f，从卡左外(-SHEEN_W)扫到卡右外(CARD_W)，inOut(cubic)，只一次\n  const sweepActive = frame >= 40 && frame <= 68;\n  const x = interpolate(frame, [40, 68], [-SHEEN_W, CARD_W], {\n    easing: Easing.inOut(Easing.cubic),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  return (\n    <div\n      style={{\n        width: 1920,\n        height: 1080,\n        background: G.bg,\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'center',\n        fontFamily: 'Helvetica, Arial, sans-serif',\n      }}\n    >\n      <div\n        style={{\n          width: CARD_W,\n          height: CARD_H,\n          background: G.side,\n          borderRadius: 24,\n          overflow: 'hidden', // 圆角裁剪：高光带被卡的圆角裁住\n          position: 'relative',\n          boxShadow: '0 12px 40px rgba(0,0,0,0.22)',\n          boxSizing: 'border-box',\n          padding: '64px 72px',\n          display: 'flex',\n          flexDirection: 'column',\n          justifyContent: 'center',\n          gap: 28,\n        }}\n      >\n        <div\n          style={{\n            fontSize: 130,\n            fontWeight: 800,\n            color: '#f5f5f3',\n            letterSpacing: 2,\n            lineHeight: 1,\n          }}\n        >\n          PRO\n        </div>\n        <div style={{ height: 16, width: 380, background: G.sideBar, borderRadius: 8 }} />\n        <div style={{ height: 16, width: 260, background: G.sideBar, borderRadius: 8 }} />\n\n        {/* 高光带：条件挂载，扫完即摘罩，收尾真静止 */}\n        {sweepActive && (\n          <div\n            style={{\n              position: 'absolute',\n              top: 0,\n              left: 0,\n              width: SHEEN_W,\n              height: CARD_H,\n              transform: `translateX(${x}px)`,\n              background:\n                'linear-gradient(115deg, transparent 42%, rgba(255,255,255,0.32) 50%, transparent 58%)',\n              pointerEvents: 'none',\n            }}\n          />\n        )}\n      </div>\n    </div>\n  );\n};\n\n\n// ===== demos/effects/light-play-moves/SpotlightSweepReveal.tsx =====\nimport React from 'react';\nimport { AbsoluteFill, useCurrentFrame, interpolate, Easing } from 'remotion';\n\n// spotlight-sweep-reveal: 聚光灯摆动扫字\n// 暗场 #2a2a28，标题两行常驻 opacity 0.07；亮版同文本用 radial-gradient mask，\n// 光斑 x 按 sin 摆动扫两个来回（周期 ~55f），f110 后全字提亮定格，\n// 光斑/光锥线性消散并在 f>=125 条件卸载，125–160 真静止 35f。\n\nconst BG = '#2a2a28';\nconst INKW = '#f5f5f3';\n\nconst CX = 960;\nconst CY = 540;\nconst AMP = 560; // 摆动幅度\nconst PERIOD = 55; // 单个来回帧数\nconst SWEEP_END = 110; // 两个来回结束\nconst REVEAL_END = 125; // 提亮完成，之后真静止\n\nconst textStyle: React.CSSProperties = {\n  fontFamily: 'Helvetica Neue, Helvetica, Arial, sans-serif',\n  fontSize: 150,\n  fontWeight: 800,\n  lineHeight: 1.15,\n  letterSpacing: '0.04em',\n  color: INKW,\n  textAlign: 'center',\n  whiteSpace: 'pre',\n};\n\nconst TitleText: React.FC = () => (\n  <AbsoluteFill style={{ justifyContent: 'center', alignItems: 'center' }}>\n    <div style={textStyle}>{'SPOTLIGHT\\nON'}</div>\n  </AbsoluteFill>\n);\n\nexport const SpotlightSweepReveal: React.FC = () => {\n  const f = useCurrentFrame();\n\n  // 光斑 x：sin 摆动，两个来回后停在中心\n  const sweepF = Math.min(f, SWEEP_END);\n  const x = CX + AMP * Math.sin((2 * Math.PI * sweepF) / PERIOD);\n\n  // 全字提亮（扩散感 out-cubic）\n  const brighten = interpolate(f, [SWEEP_END, REVEAL_END], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n    easing: Easing.out(Easing.cubic),\n  });\n  // 光斑/光锥消散（线性）\n  const fadeOut = interpolate(f, [SWEEP_END, REVEAL_END], [1, 0], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const sweepAlive = f < REVEAL_END; // 条件卸载，保证末段真静止\n\n  const maskGrad = `radial-gradient(circle 380px at ${x}px ${CY}px, rgba(0,0,0,1) 0%, rgba(0,0,0,0.95) 45%, rgba(0,0,0,0) 100%)`;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: BG }}>\n      {/* 暗版文字常驻 */}\n      <AbsoluteFill style={{ opacity: 0.07 }}>\n        <TitleText />\n      </AbsoluteFill>\n\n      {sweepAlive && (\n        <>\n          {/* 光锥：从顶部收拢到光斑 */}\n          <AbsoluteFill\n            style={{\n              opacity: fadeOut,\n              clipPath: `polygon(${CX - 70}px -40px, ${CX + 70}px -40px, ${x + 420}px ${CY + 230}px, ${x - 420}px ${CY + 230}px)`,\n              background:\n                'linear-gradient(to bottom, rgba(245,245,243,0.16), rgba(245,245,243,0.03) 85%, rgba(245,245,243,0) 100%)',\n            }}\n          />\n          {/* 落在暗墙上的柔光斑 */}\n          <AbsoluteFill\n            style={{\n              opacity: fadeOut,\n              background: `radial-gradient(circle 460px at ${x}px ${CY}px, rgba(245,245,243,0.22) 0%, rgba(245,245,243,0.08) 55%, rgba(245,245,243,0) 100%)`,\n            }}\n          />\n          {/* 亮版文字，按帧移动的 radial mask */}\n          <AbsoluteFill\n            style={{\n              opacity: fadeOut === 1 ? 1 : Math.max(fadeOut, 0),\n              WebkitMaskImage: maskGrad,\n              maskImage: maskGrad,\n            }}\n          >\n            <TitleText />\n          </AbsoluteFill>\n        </>\n      )}\n\n      {/* 全亮定格层 */}\n      <AbsoluteFill style={{ opacity: brighten }}>\n        <TitleText />\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n};\n\n\n"
}