{
  "slug": "spectrum-morph-ui",
  "title": "Spectrum-morph underline",
  "tagline": "Spectrum UI morph — the title underline splits into vertical bars that pulse like a spectrum for two bars, then closes back into a straight line; music visualization living inside the UI.",
  "description": "Spectrum UI morph — the title underline splits into vertical bars that pulse like a spectrum for two bars, then closes back into a straight line; music visual",
  "category": "rhythm",
  "tags": [
    "shotcraft",
    "remotion",
    "rhythm",
    "typography"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 4.72,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/spectrum-morph-ui/poster-b05bcb8a5d3d.jpg",
    "video": "https://gimgs.net/motion/media/spectrum-morph-ui/video-311f897d7127.mp4"
  },
  "variants": [
    {
      "key": "spectrum-morph-ui",
      "label": "spectrum-morph-ui",
      "description": "Spectrum UI morph — the title underline splits into vertical bars that pulse like a spectrum for two bars, then closes back into a straight line; music visualization living inside the UI",
      "use": "",
      "video": "https://gimgs.net/motion/media/spectrum-morph-ui/video-311f897d7127.mp4",
      "poster": "https://gimgs.net/motion/media/spectrum-morph-ui/poster-b05bcb8a5d3d.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/spectrum-morph-ui.html",
  "json_url": "https://gimgs.net/motion/spectrum-morph-ui.json",
  "template_url": "https://gimgs.net/motion/spectrum-morph-ui/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/spectrum-morph-ui.md",
  "updated_at": "2026-09-24T07:40:23.556Z",
  "prompt": "## When to use\n\nAudio-synced highlight passages in tracks with music (BGM chorus entry / dense drum hits); underline/divider elements on title cards and chapter pages\n\n## Intention\n\nThe library already has type-rhythm-sync A (font-weight pulse) for audio sync — that's \"the text itself moving.\" This card is \"a UI element turning into an equalizer\": the underline, the most unassuming element, is borrowed by the music for two bars, splitting into 28 vertical bars that jump as a spectrum, then **closing back and returning intact**. The borrow-and-return structure is the whole elegance — the viewer first recognizes \"this is the underline,\" then watches it become something else, then watches it become that again; if it started as spectrum bars, it would just be another music visualizer. It must return to that exact 8px straight line, pixel for pixel, when the pulsing ends.\n\n## Duration & energy\n\n- Duration: ~4.7s (8f split + 64f pulsing + 12f close + 39f still)\n- Energy: Medium\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n声画同步库内已有 type-rhythm-sync A（字重脉冲）——那是\"文字自己动\"；\n本卡是\"UI 构件变成均衡器\"：下划线这个最不起眼的构件被音乐借用两小节，\n裂成 28 根竖条跳出频谱，然后**收拢还原、完璧归赵**。借用-归还结构是\n全部优雅所在——观众先认出\"这是下划线\"，再看它变成别的东西，最后\n看它变回来；若一开始就是频谱条，只是又一个音乐可视化。跳完必须还原\n成那条 8px 直线，一像素不差。\n\n## 动效核心\n- 28 根条底边锁在原下划线 y 上**只向上生长**，收拢结束条宽 + gap\n  恰好拼回 720px 整线（gap 0↔6px 同步过渡，gap<1 时条宽 +0.5px 防细缝）\n- 非跳动段条件挂载回整条直线（摘罩式）——像素级静止靠\"根本不渲染\n  条组\"，不是条高归零\n- demo 为伪 FFT：|sin(i·0.7+f·0.31)| × 每 4 帧换挡 seed hash 抖动 ×\n  低频高/高频矮包络 × ramp 幅度包络，帧确定无随机\n- **实战换真 FFT**：@remotion/media-utils 的 visualizeAudio() 取\n  频谱映射条高，伪 FFT 只是灰阶占位的节拍替身\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 条数/条宽 | 28 根、gap 6px 时 ≈20px 宽 | >40 根读作噪声；<16 根读作柱状图 |\n| 峰值幅度 | AMP=92，实测峰值 75–85px | spec 62 实测只有 ~55px 不够响，已加码 1.5×（可感性判例） |\n| 包络 | 左高右矮（低频高/高频矮） | 均匀高度像栅栏不像频谱 |\n| 换挡节拍 | seed 每 4 帧换挡 | 逐帧换读作电噪；8f 以上读作慢动作 |\n| 时间轴 | 裂开 8f out-cubic / 跳动 64f（两小节）/ 收拢 12f | 跳动 <1 小节读作故障闪了一下 |\n| 收尾 | 收线后真静止 ≥39f，像素级 diff 为零 | 归还之后再动一下，借用结构就毁了 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- **声音强依赖**：跳动段必须真的踩在音乐上——无音轨的频谱跳动读作\n  UI 故障；实战必须 visualizeAudio() 对齐 BGM，伪 FFT 不许上片\n- 全片 ≤1 次，且只借用一个构件——标题下划线在跳、卡片分隔线也在跳，\n  均衡器就成了壁纸（P4）\n- 与 type-rhythm-sync 同为声画同步族，同一段落二选一——文字在脉冲、\n  下划线在跳频谱，两个声画主角互相拆台\n- 收拢帧后的静止段是\"归还仪式\"的一部分，从重给（≥35f），别急着切\n\n## 参考实现\ndemos/rhythm/spectrum-morph-ui/\n（SpectrumMorphUi.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/rhythm/spectrum-morph-ui/SpectrumMorphUi.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/spectrum-morph-ui/SpectrumMorphUi.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/rhythm/spectrum-morph-ui/SpectrumMorphUi.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/rhythm/spectrum-morph-ui/SpectrumMorphUi.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/spectrum-morph-ui/SpectrumMorphUi.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": 142,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/rhythm/spectrum-morph-ui/SpectrumMorphUi.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/rhythm/spectrum-morph-ui/SpectrumMorphUi.tsx =====\n// spectrum-morph-ui —— 频谱化 UI\n// 标题 \"LAUNCH WEEK\" 下方 8px ink 下划线（720px）。25f 起裂成 28 根竖条\n// （20px 宽、6px 间隙），条高按伪 FFT 跳动 64f（低频端高、高频端矮的包络），\n// 条底对齐原线、向上生长。两小节后 12f 收拢回 8px 直线，收线后真静止 ≥35f。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, TitleBlock } from '../../_fixtures/Fixtures';\n\n// 库内标准 seed hash（帧确定，无 Math.random）\nconst h = (n: number) => {\n  const s = Math.sin(n * 127.3) * 43758.5453;\n  return s - Math.floor(s);\n};\n\nconst LINE_W = 720;\nconst LINE_H = 8;\nconst LINE_X = (1920 - LINE_W) / 2; // 600\nconst LINE_BOTTOM = 620; // 下划线底边 y（条从这里向上长）\n\nconst N_BARS = 28;\nconst GAP_MAX = 6;\n\nconst SPLIT = 25; // 裂开起点\nconst SPLIT_DUR = 8; // 裂开 & 幅度爬升\nconst DANCE = 64; // 跳动两小节\nconst COLLAPSE_START = SPLIT + DANCE; // 89\nconst COLLAPSE_DUR = 12;\nconst COLLAPSE_END = COLLAPSE_START + COLLAPSE_DUR; // 101 → 之后真静止 39f\n\nconst AMP = 92; // 理论峰值 8 + 92 = 100px；wobble×jitter×env 很少同时取满，实测峰值 ~80px\n\n// 低频端高、高频端矮的包络\nconst env = (i: number) => 0.4 + 0.6 * Math.pow(1 - i / (N_BARS - 1), 1.1);\n\nexport const SpectrumMorphUi: React.FC = () => {\n  const frame = useCurrentFrame();\n\n  const titleOp = interpolate(frame, [0, 10], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  // 幅度爬升（扩散 → out-cubic）与收拢（12f 归零）\n  const rampIn = interpolate(frame, [SPLIT, SPLIT + SPLIT_DUR], [0, 1], {\n    easing: Easing.out(Easing.cubic),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const rampOut = interpolate(frame, [COLLAPSE_START, COLLAPSE_END], [1, 0], {\n    easing: Easing.out(Easing.cubic),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const amp = rampIn * rampOut;\n\n  // 间隙：裂开时 0→6，收拢时 6→0（收拢结束恰好合成整线）\n  const gapIn = interpolate(frame, [SPLIT, SPLIT + SPLIT_DUR], [0, GAP_MAX], {\n    easing: Easing.out(Easing.cubic),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const gapOut = interpolate(frame, [COLLAPSE_START, COLLAPSE_END], [GAP_MAX, 0], {\n    easing: Easing.out(Easing.cubic),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const gap = Math.min(gapIn, gapOut);\n  const barW = (LINE_W - (N_BARS - 1) * gap) / N_BARS; // gap=6 时 ≈20px\n\n  // 条形阶段以外条件挂载整线（摘罩=条件挂载，保证收尾像素级静止）\n  const barsActive = frame >= SPLIT && frame < COLLAPSE_END;\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>\n      <div\n        style={{\n          position: 'absolute',\n          top: 330,\n          width: '100%',\n          textAlign: 'center',\n          opacity: titleOp,\n          fontFamily: 'Helvetica, Arial, sans-serif',\n          fontWeight: 800,\n          fontSize: 120,\n          color: G.ink,\n          letterSpacing: -1,\n        }}\n      >\n        LAUNCH WEEK\n      </div>\n\n      {!barsActive && (\n        <div\n          style={{\n            position: 'absolute',\n            left: LINE_X,\n            top: LINE_BOTTOM - LINE_H,\n            width: LINE_W,\n            height: LINE_H,\n            background: G.ink,\n            borderRadius: 4,\n          }}\n        />\n      )}\n\n      {barsActive &&\n        Array.from({ length: N_BARS }).map((_, i) => {\n          // 伪 FFT：|sin| 摆动 × 每 4 帧换挡的 seed 随机 × 频段包络 × 幅度包络\n          const wobble = Math.abs(Math.sin(i * 0.7 + frame * 0.31));\n          const jitter = 0.4 + 0.6 * h(i * 13 + Math.floor(frame / 4));\n          const barH = LINE_H + AMP * wobble * jitter * env(i) * amp;\n          return (\n            <div\n              key={i}\n              style={{\n                position: 'absolute',\n                left: LINE_X + i * (barW + gap),\n                top: LINE_BOTTOM - barH, // 底边对齐原线，向上长\n                width: barW + (gap < 1 ? 0.5 : 0), // gap 收到 0 时补 0.5px 防细缝\n                height: barH,\n                background: G.ink,\n                borderRadius: 3,\n              }}\n            />\n          );\n        })}\n    </div>\n  );\n};\n\n\n"
}