{
  "slug": "product-card-progressive-assemble",
  "title": "Product card progressive assemble",
  "tagline": "A detail card self-assembles field by field — image, title, breadcrumb pill pop in, the old price strikes through, a new price springs in.",
  "description": "A detail card self-assembles field by field — image, title, breadcrumb pill pop in, the old price strikes through, a new price springs in.",
  "category": "ui-entrance",
  "tags": [
    "shotcraft",
    "remotion",
    "ui-entrance"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 1920,
    "height": 1080,
    "fps": 30,
    "duration": 5.5,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/product-card-progressive-assemble/poster-db9509889623.jpg",
    "video": "https://gimgs.net/motion/media/product-card-progressive-assemble/video-8feed455903d.mp4"
  },
  "variants": [
    {
      "key": "product-card-progressive-assemble",
      "label": "product-card-progressive-assemble",
      "description": "A detail card self-assembles as if grabbed field by field — image → title → breadcrumb pill pop in sequence → the old price appears then strikes through and downgrades, a new accent-color price springs out → body copy reveals line by line with a highlight sweeping left to right → the color swatch lights up, the whole card slowly pushing forward in scale throughout.",
      "use": "",
      "video": "https://gimgs.net/motion/media/product-card-progressive-assemble/video-8feed455903d.mp4",
      "poster": "https://gimgs.net/motion/media/product-card-progressive-assemble/poster-db9509889623.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/product-card-progressive-assemble.html",
  "json_url": "https://gimgs.net/motion/product-card-progressive-assemble.json",
  "template_url": "https://gimgs.net/motion/product-card-progressive-assemble/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/product-card-progressive-assemble.md",
  "updated_at": "2026-09-24T07:40:15.609Z",
  "prompt": "## When to use\n\nA capability showcase for a product/listing detail page; the hero shot for \"structured extraction,\" \"auto-fill,\" or \"data growing itself\" narratives\n\n## Intention\n\nA detail card that just fades in reads to the viewer as \"there's a card.\" Landing it field by field turns the card into a **form being filled out live**: each field lands on its own, and the viewer's attention is walked through the card's information structure — where's the image, where's the title, where's the price, where's the key line of copy. The price strikethrough downgrade is the film's only semantic event (not an \"appearance\" but a \"change\"), so it's placed at the most prominent point, mid-sequence.\n\n## Duration & energy\n\n- Duration: About 5.0s (150f@30fps)\n- Energy: Medium (dense, continuous small events with no single burst; a slow push sustains the sense of progress)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n一张详情卡直接淡入，观众只看到\"有一张卡\"。逐字段落位则把卡变成一份**正在被填写的\n表单**：每个字段各自到位，观众的注意力被牵着走一遍卡的信息结构——图在哪、标题在哪、\n价格在哪、正文重点在哪。价格划线降级是全片唯一的语义事件（不是\"出现\"而是\"改变\"），\n所以它被安排在中段最显眼的位置。\n\n## 动效核心\n- 字段时间表用\"配方帧\"书写：`F = f => f/60`，把 60 分母的帧号折成 t。图 f=0、标题 f=4、\n  三枚 pill f=8/10/12、价格行 f=16、三行正文 f=30/32/34、三块色卡 f=40/42/44 ——\n  相邻字段间隔只有 2 帧，是\"抓取\"的密度而非\"逐条展示\"的密度\n- 每个字段共享同一条落位曲线：`seg(t, t0, t0+0.1, E.outCubic)`，两种落位模式——\n  `rise` 是 `translateY(6→0)`（图/标题/价格行/正文行），`pop` 是\n  `scale(lerp(E.outBack(k), 0.4, 1))`（pill 与色卡）；小件用 pop 大件用 rise\n- 价格降级发生在 f=26（配方帧），是**硬切**：`oldP` 同帧换色（`#17181c`→`#9a9da6`）、\n  加 `line-through`、字号 21px→15px 三件事一起发生；紧接 `seg(t, F(26), +0.12)` 让\n  强调色新价用 `E.spring(nk, 0.35)` 从 1.15 收到 1\n- 马克笔高亮：两个高亮片段各有一层绝对定位的 `ACCENT_SOFT` 底块，`transform-origin:\n  left center` + `scaleX(0→1)`，窗口是 `F(30+li*2+4)` 起、行程 0.085（≈5f）——比字段\n  落位还快，读作\"笔一划过去\"\n- 整卡呼吸：`seg(t, 0, 0.75, E.outQuad)` 驱动 `scale(1→1.06)`，缓起缓收摊在前 75%——\n  这层运动与所有字段无关，作用是让密集的小事件之间画面仍在推进\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 字段时间表 | 配方帧 0/4/8/10/12/16/30/32/34/40/42/44（`F(f)=f/60`） | 同类字段间隔 2 帧、跨类间隔 4–14 帧——间隔的疏密就是信息分组；全部改成等间隔会读作机械点名 |\n| 单字段行程 | 0.1（≈15f） | 行程远大于间隔 → 同时有五六个字段在动，画面是\"涌\"而非\"点\"；压到 0.04 以下会变成逐个硬闪 |\n| rise 位移 | 6px | 只有 6px，字段是\"沉下去一点点\"；>20px 会与整卡前推的方向感打架 |\n| pop 起始缩放 | 0.4 + `E.outBack` | 0.4 是明确的\"从无到有\"；pill 与色卡尺寸小，过冲不会撞到邻居，大件用 pop 就会显得廉价 |\n| 价格降级点 | 配方帧 26（t≈0.433） | 安排在价格行落位（f=16）之后 10 帧——必须留出\"先看清原价\"的时间，紧贴落位会读作从来就是打折价 |\n| 新价 spring | `E.spring(nk, 0.35)`，1.15→1，窗 0.12 | 全片唯一的过冲量级事件；阻尼调小会抖两下，与旁边的划线动作抢注意力 |\n| 高亮刷过 | 窗 0.085（≈5f），`scaleX` 从左 | 5f 是马克笔速度；>0.15 读作背景色渐变而不是划线 |\n| 整卡前推 | `scale 1→1.06`，窗 0→0.75，outQuad | 6% 摊在 3.75s 上是纯呼吸；>12% 卡会撞到画框，且 4.5% 的 padding 内的文字会被推出边界 |\n\n## 已知坑\n- 时间表用的是 60 分母的\"配方帧\"，而实际时长 5000ms = 150f@30fps。这两套帧号不通约：\n  想按 30fps 的帧数排事件，必须先把配方帧换算成 t 再对照（f=44 → t≈0.733 → 110f@30fps）\n- 最后一个字段在 t≈0.733 落位，之后 1.3s 只有整卡前推（且前推在 t=0.75 停止）——\n  末段有约 1.2s 的完全静止定格。这是给观众通读整卡的时间，别当成 bug 砍掉\n- 三行正文用 `white-space: nowrap` 写死不换行，卡宽 78%。换更长文案或中文时会溢出\n  被 `overflow` 裁掉，落地必须重新配字号与行长\n- 所有内容都是占位（\"Sample Product Title\"、`$249/$189`、\"Placeholder copy for…\"、\n  三个纯色色卡），字段结构本身才是可复用的部分。换成真实商品数据后字段数量往往变化，\n  时间表要按新的字段分组重排间隔\n- 高亮块的 `t0 = F(30+li*2+4)` 比所在行的落位（`F(30+li*2)`）晚 4 配方帧，两者是耦合\n  的偏移量；改行落位时间必须同步改高亮，否则出现\"高亮先到、文字后来\"\n- `ACCENT` / `ACCENT_SOFT` 是全卡唯一色相来源（新价文字 + 两块高亮），换品牌色改这\n  两个变量即可；其余全是中性灰阶\n\n## 参考实现\ndemos/ui-entrance/product-card-progressive-assemble/\n（ProductCardProgressiveAssemble.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/ui-entrance/product-card-progressive-assemble/ProductCardProgressiveAssemble.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/product-card-progressive-assemble/ProductCardProgressiveAssemble.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/ui-entrance/product-card-progressive-assemble/ProductCardProgressiveAssemble.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/ui-entrance/product-card-progressive-assemble/ProductCardProgressiveAssemble.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/product-card-progressive-assemble/ProductCardProgressiveAssemble.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": 165,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/product-card-progressive-assemble/ProductCardProgressiveAssemble.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/ui-entrance/product-card-progressive-assemble/ProductCardProgressiveAssemble.tsx =====\n// product-card-progressive-assemble — Progressive Assemble 字段逐个落位（motion-lab 定稿转原生 Remotion）\n// 详情卡像被逐字段抓取般自建：图→标题→breadcrumb pill 依次 pop→价格出现后被划线降级、\n// 强调色新价 spring 跳出→正文逐行揭示+强调色高亮块由左向右刷过→色卡点亮。\n// 整卡极慢 scale 前推保持呼吸。设计坐标 480×270（DesignStage 等比放大）。\nimport React from 'react';\nimport { DesignStage, E, lerp, seg, useT } from '../../_fixtures/Motion';\n\nexport const PRODUCT_CARD_PROGRESSIVE_ASSEMBLE_DURATION = 150; // 5000ms @30fps\n\nconst ACCENT = '#ff6a1f';\nconst ACCENT_SOFT = 'rgba(255,122,26,.42)';\n\nconst F = (f: number) => f / 60; // recipe 帧 → t\n\n// 字段落位样式：rise = 上移 6px 淡入；pop = outBack 从 0.4 弹到 1\nconst fieldStyle = (t: number, f0: number, mode: 'rise' | 'pop' = 'rise'): React.CSSProperties => {\n  const k = seg(t, F(f0), F(f0) + 0.1, E.outCubic);\n  return {\n    opacity: Math.min(1, k * 2),\n    transform:\n      mode === 'pop'\n        ? `scale(${lerp(E.outBack(Math.min(1, k)), 0.4, 1)})`\n        : `translateY(${lerp(k, 6, 0)}px)`,\n  };\n};\n\n// 描述行：[文本, 是否马克笔高亮]\nconst LINES: [string, boolean?][][] = [\n  [['Placeholder copy for '], ['a key highlight', true], [' in the']],\n  [['product body. '], ['Second highlight', true], [' sits here on']],\n  [['the third line of neutral sample text.']],\n];\n\nexport const ProductCardProgressiveAssemble: React.FC = () => {\n  const t = useT();\n  // 整卡呼吸前推\n  const push = seg(t, 0, 0.75, E.outQuad);\n  // 价格降级（f=26）：旧价划线缩小变灰，新价 spring 跳出\n  const cut = t >= F(26);\n  const nk = seg(t, F(26), F(26) + 0.12);\n  return (\n    <DesignStage bg=\"#0d0e13\" raster=\"zoom\">\n      <div\n        style={{\n          position: 'absolute',\n          left: '50%',\n          top: '50%',\n          width: '78%',\n          height: '74%',\n          transform: `translate(-50%,-50%) scale(${lerp(push, 1, 1.06)})`,\n          background: '#f6f5f2',\n          borderRadius: 12,\n          boxShadow: '0 18px 50px rgba(0,0,0,.5)',\n          fontFamily: '-apple-system,Helvetica,sans-serif',\n          display: 'flex',\n          padding: '4.5%',\n          boxSizing: 'border-box',\n          gap: '5%',\n        }}\n      >\n        {/* 左：商品图占位（f=0 rise 落位） */}\n        <div\n          style={{\n            flex: '0 0 38%',\n            background: '#e2e0da',\n            borderRadius: 8,\n            position: 'relative',\n            overflow: 'hidden',\n            ...fieldStyle(t, 0),\n          }}\n        >\n          <div\n            style={{\n              position: 'absolute',\n              left: '18%',\n              top: '14%',\n              width: '64%',\n              height: '72%',\n              background: 'linear-gradient(160deg,#2b3040,#171a24)',\n              borderRadius: '40% 40% 14% 14%/26% 26% 10% 10%',\n            }}\n          >\n            {/* 拉链 */}\n            <div style={{ position: 'absolute', left: '49%', top: '20%', width: '2%', height: '58%', background: '#525a70' }} />\n          </div>\n        </div>\n        {/* 右列 */}\n        <div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}>\n          {/* 标题（f=4） */}\n          <div style={{ fontSize: 19, fontWeight: 800, color: '#17181c', letterSpacing: '-.3px', ...fieldStyle(t, 4) }}>\n            Sample Product Title\n          </div>\n          {/* breadcrumb pills（f=8 起每 2 帧一个 pop） */}\n          <div style={{ display: 'flex', gap: 6, margin: '8px 0 10px' }}>\n            {['Category', 'Subgroup', 'Detail'].map((txt, i) => (\n              <div\n                key={txt}\n                style={{\n                  fontSize: 9,\n                  fontWeight: 600,\n                  color: '#5a5e6b',\n                  background: '#e9e7e1',\n                  padding: '3px 9px',\n                  borderRadius: 99,\n                  ...fieldStyle(t, 8 + i * 2, 'pop'),\n                }}\n              >\n                {txt}\n              </div>\n            ))}\n          </div>\n          {/* 价格行（f=16）：f=26 起旧价降级、新价 spring 跳出 */}\n          <div style={{ display: 'flex', alignItems: 'baseline', gap: 9, marginBottom: 11, ...fieldStyle(t, 16) }}>\n            <span\n              style={{\n                fontSize: cut ? 15 : 21,\n                fontWeight: 800,\n                color: cut ? '#9a9da6' : '#17181c',\n                textDecoration: cut ? 'line-through' : 'none',\n              }}\n            >\n              $249\n            </span>\n            <span\n              style={{\n                fontSize: 21,\n                fontWeight: 800,\n                color: ACCENT,\n                opacity: Math.min(1, nk * 3),\n                transform: `scale(${lerp(E.spring(nk, 0.35), 1.15, 1)})`,\n              }}\n            >\n              $189\n            </span>\n          </div>\n          {/* 描述行（f=30 起每 2 帧一行）+ 马克笔高亮（行 f0+4 起由左向右刷过） */}\n          <div style={{ fontSize: 10.5, lineHeight: 1.75, color: '#494d58' }}>\n            {LINES.map((segs, li) => (\n              <div key={li} style={{ whiteSpace: 'nowrap', ...fieldStyle(t, 30 + li * 2) }}>\n                {segs.map(([txt, isMark], si) =>\n                  !isMark ? (\n                    <span key={si}>{txt}</span>\n                  ) : (\n                    <span key={si} style={{ position: 'relative', display: 'inline-block' }}>\n                      <span\n                        style={{\n                          position: 'absolute',\n                          left: -2,\n                          right: -2,\n                          top: '8%',\n                          bottom: '4%',\n                          background: ACCENT_SOFT,\n                          borderRadius: 2,\n                          transform: `scaleX(${seg(t, F(30 + li * 2 + 4), F(30 + li * 2 + 4) + 0.085, E.outCubic)})`,\n                          transformOrigin: 'left center',\n                        }}\n                      />\n                      <span style={{ position: 'relative', fontWeight: 700, color: '#26282f' }}>{txt}</span>\n                    </span>\n                  ),\n                )}\n              </div>\n            ))}\n          </div>\n          {/* 色卡（f=40 起每 2 帧一个 pop） */}\n          <div style={{ display: 'flex', gap: 7, marginTop: 'auto' }}>\n            {['#191b20', '#8b8f99', '#3a5b8c'].map((cclr, i) => (\n              <div\n                key={cclr}\n                style={{\n                  width: 16,\n                  height: 16,\n                  borderRadius: 4,\n                  background: cclr,\n                  outline: '1.5px solid rgba(0,0,0,.12)',\n                  outlineOffset: 1.5,\n                  ...fieldStyle(t, 40 + i * 2, 'pop'),\n                }}\n              />\n            ))}\n          </div>\n        </div>\n      </div>\n    </DesignStage>\n  );\n};\n\n\n"
}