{
  "slug": "element-body-moves",
  "title": "Element body moves",
  "tagline": "Two element-body-feel styles: axial-stretch taffy-pull stretching, and contact-shadow-lift lifting off the surface with a contact shadow.",
  "description": "Two element-body-feel styles: axial-stretch taffy-pull stretching, and contact-shadow-lift lifting off the surface with a contact shadow.",
  "category": "ui-entrance",
  "tags": [
    "shotcraft",
    "remotion",
    "ui-entrance"
  ],
  "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/element-body-moves/poster-8374a7546aa8.jpg",
    "video": "https://gimgs.net/motion/media/element-body-moves/video-7fa92fd9c8d3.mp4"
  },
  "variants": [
    {
      "key": "axial-stretch",
      "label": "axial-stretch · Speed-driven axial stretch",
      "description": "Velocity-delta-driven axial stretch — the faster it flies, the longer it stretches (full stretch scaleX 2.2 / scaleY 0.72), squashes and rebounds over 8f at landing.",
      "use": "`p(f)−p(f−1)`",
      "video": "https://gimgs.net/motion/media/element-body-moves/video-7fa92fd9c8d3.mp4",
      "poster": "https://gimgs.net/motion/media/element-body-moves/poster-8374a7546aa8.jpg"
    },
    {
      "key": "contact-shadow-lift",
      "label": "contact-shadow-lift · Contact-shadow lift",
      "description": "Lift over 10f out-cubic: card translateY(−28px) + scale(1.08), a separate elliptical shadow scales 1→1.72 / opacity 0.55→0.18 inversely at the same progress; drops back over 8f in-cubic + 2f micro-squash settle.",
      "use": "One-by-one card call-outs; sets up the off-surface feel for 2.5D segments",
      "video": "https://gimgs.net/motion/media/element-body-moves/video-9227185104c2.mp4",
      "poster": "https://gimgs.net/motion/media/element-body-moves/poster-contact-shadow-lift-8f8a9d1e5e85.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/element-body-moves.html",
  "json_url": "https://gimgs.net/motion/element-body-moves.json",
  "template_url": "https://gimgs.net/motion/element-body-moves/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/element-body-moves.md",
  "updated_at": "2026-09-24T07:39:57.084Z",
  "prompt": "## When to use\n\nAdds \"the body is changing\" on top of \"the position is changing\": high-speed fly-ins get a physical body of speed (A), card call-outs get proof of hovering (B); pair A with a lateral-rush entrance, pair B with 2.5D camera moves and one-by-one call-outs.\n\n## Intention\n\nThe library's motion cards all manage \"the position is changing\" — flying in, sliding, bouncing, while the element itself stays a rigid body. These two styles manage **the body changing**: A gives speed a visible body — the faster it flies, the longer it stretches along its motion axis, like taffy, squashing and rebounding straight at landing, a UI translation of squash & stretch; B gives hovering credible proof — as a card lifts, the shadow directly beneath it scales up and fades in sync, paper leaving the table, a physical line of dialogue that sets up a 2.5D camera move via the staging principle. Difference from smear-multiples: that's countable afterimages (discrete ghosting), A is continuous stretching (taffy that keeps stringing); difference from CameraMotionBlur: shutter drag is the camera's business, stretching is the body's — don't stack both on the same element.\n\n## Duration & energy\n\n- Duration: A ~4.7s / B ~5.3s\n- Energy: A: Medium-high / B: Low-medium\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n库内运动卡全在管\"位置在变\"——飞入、滑动、弹跳，元素本身始终是刚体。\n这两式管**身体在变**：A 给速度一个可见的肉身——飞得越快沿运动轴拉得\n越长，一块糖稀，落点压扁回弹收正，squash & stretch 的 UI 翻译；B 给\n悬浮一个可信的证据——卡抬起时正下方阴影同步放大变淡，纸片离桌，\nstaging 法则给 2.5D 运镜垫的物理台词。与 smear-multiples 的区别：那是\n可数残像（离散鬼影），A 是连续拉伸（糖稀不断丝）；与 CameraMotionBlur\n的区别：快门拖影是相机的事，拉伸是身体的事，同一元素别叠加。\n\n## 两式选型\n| 式 | 做法 | 适用 |\n|----|------|------|\n| A axial-stretch | 速度差分驱动轴向 scale：v=\\|p(f)−p(f−1)\\|，v<2px/f 不拉伸、≥140px/f 满拉伸（scaleX 2.2 / scaleY 0.72）；落点 8f 压扁回弹 | 高速飞入/横冲入场；多卡错峰填坑 |\n| B contact-shadow-lift | 抬起 10f out-cubic：卡 translateY(−28px)+scale(1.08)，独立椭圆阴影 scale 1→1.72 / opacity 0.55→0.18 同进度反向；落回 8f in-cubic + 2f 微压卡壳 | 逐张点名强调；2.5D 段落的离面铺垫 |\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| A 拉伸映射 | v∈[2,140]px/f → 拉伸 0→满（scaleX 2.2/scaleY 0.72） | 峰值 <1.6 不可感；>2.6 读作故障拉丝 |\n| A origin/顺序 | transformOrigin 设运动后缘（右飞 100% 50%）；translate 再 scale | origin 居中读作缩放不是拉伸；顺序反了位移跟着放大 |\n| A 落点回弹 | 8f out-cubic：scaleX 过冲 0.85、scaleY 1.1 再回 1 | 无回弹的拉伸像刹不住车 |\n| A 错峰 | 三卡起飞间隔 12f，飞行 36f | 同帧齐飞读不出各自的拉丝 |\n| B 抬升幅度 | −28px + scale 1.08 | 判例锁死：<12px 禁用（见已知坑） |\n| B 阴影 | 独立 radial-gradient 椭圆 div；卡本体 boxShadow: none | box-shadow 跟卡走，给不出\"影子留在桌上\" |\n| B 落地 | scale 0.99 卡壳 2f 再 5f 回弹到 1 | 无微压读作飘落，没有重量 |\n| 收尾 | A 末卡回弹后 62f / B 全落回后 35f 真静止 | 收尾帧须与 rest 态逐像素一致 |\n\n## 已知坑\n- demo 在灰阶/占位素材上调校通过——参数是调校起点非实战定稿，\n  首次实战须以真实素材回验\n- B 微幅加码判例：原案 8px 抬升不可感，加码到 28px 才过——实战禁回调到 12px 以下，宁可不用不可用弱\n- A 速度必须位置差分实算，不许拿 easing 进度近似——差分才保证\n  \"低于阈值不拉伸\"，起步与将落时自动收正\n- A 竖向运动轴要换：竖飞拉 scaleY 压 scaleX，origin 设上/下缘，\n  照抄横版参数会拉错轴（原案就把轴写反过）\n- B 一次只抬一张——两张同时离桌，观众找不到被点名的是谁（P4）\n- 声音：A 落点一声闷撞、B 抬起轻吸/落回轻放（sound-design §4.5）\n\n## 参考实现\ndemos/ui-entrance/element-body-moves/\n（AxialStretch.tsx / ContactShadowLift.tsx）\n\n## Reference implementation (Remotion)\n\n- demos/ui-entrance/element-body-moves/AxialStretch.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/element-body-moves/AxialStretch.tsx\n- demos/ui-entrance/element-body-moves/ContactShadowLift.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/element-body-moves/ContactShadowLift.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/element-body-moves/AxialStretch.tsx, demos/ui-entrance/element-body-moves/ContactShadowLift.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/element-body-moves/AxialStretch.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/element-body-moves/AxialStretch.tsx"
    },
    {
      "path": "demos/ui-entrance/element-body-moves/ContactShadowLift.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/element-body-moves/ContactShadowLift.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/ui-entrance/element-body-moves/AxialStretch.tsx",
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/ui-entrance/element-body-moves/ContactShadowLift.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/element-body-moves/AxialStretch.tsx =====\n// axial-stretch —— 轴向拉伸速度感\n// 三张 Card 从右外依次横向飞入落位，飞行途中沿运动轴速度驱动拉伸\n// （scaleX 峰值 ≈2.2 / scaleY ≈0.72，糖稀拉丝感），落点 Back.out 式回弹。\n// 速度用位置差分 p(f)-p(f-1) 驱动，低于阈值不拉伸。收尾真静止 ≥35f。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, Card, TitleBlock } from '../../_fixtures/Fixtures';\n\nconst W = 1920;\nconst CARD_W = 380;\nconst CARD_H = 230;\nconst GAP = 60;\nconst ROW_W = 3 * CARD_W + 2 * GAP; // 1260\nconst ROW_X0 = (W - ROW_W) / 2; // 330\nconst ROW_Y = (1080 - CARD_H) / 2; // 425\n\nconst START_X = 1980; // 完全在画面右外\nconst FLIGHT = 36; // 飞行帧数\nconst STAGGER = 12; // 错峰\nconst FIRST = 10; // 首卡起飞帧\nconst SQUASH = 8; // 落点回弹帧数\n\nconst VEL_MIN = 2; // px/frame，低于此不拉伸\nconst VEL_REF = 140; // px/frame，达到此速度即满拉伸\nconst STRETCH_X = 1.2; // scaleX 峰值 1 + 1.2 = 2.2\nconst SQUISH_Y = 0.28; // scaleY 谷值 1 - 0.28 = 0.72\n\n// poly(4) in-out：中段速度峰值 ≈ 4× 平均速度，够冲\nconst flightEase = Easing.inOut(Easing.poly(4));\n\nconst posAt = (f: number, start: number, targetX: number): number =>\n  interpolate(f, [start, start + FLIGHT], [START_X, targetX], {\n    easing: flightEase,\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\nconst FlyCard: React.FC<{ i: number; frame: number }> = ({ i, frame }) => {\n  const start = FIRST + i * STAGGER;\n  const targetX = ROW_X0 + i * (CARD_W + GAP);\n  const land = start + FLIGHT;\n\n  const x = posAt(frame, start, targetX);\n  // 速度 = 位置差分（帧时间解耦，纯由位置函数决定）\n  const v = Math.abs(posAt(frame, start, targetX) - posAt(frame - 1, start, targetX));\n  const s = Math.min(Math.max((v - VEL_MIN) / (VEL_REF - VEL_MIN), 0), 1);\n\n  const stretchX = 1 + STRETCH_X * s;\n  const stretchY = 1 - SQUISH_Y * s;\n\n  // 落点回弹：scaleX 过冲到 0.85 再回 1（横向被\"撞停\"压扁），8f\n  const sqX = interpolate(frame, [land, land + SQUASH / 2, land + SQUASH], [1, 0.85, 1], {\n    easing: Easing.out(Easing.cubic),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n  const sqY = interpolate(frame, [land, land + SQUASH / 2, land + SQUASH], [1, 1.1, 1], {\n    easing: Easing.out(Easing.cubic),\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  return (\n    <div\n      style={{\n        position: 'absolute',\n        left: 0,\n        top: ROW_Y,\n        // 顺序：先 translate 再 scale；transformOrigin 设运动后缘（向左飞 → 右缘）\n        transform: `translateX(${x}px) scaleX(${stretchX * sqX}) scaleY(${stretchY * sqY})`,\n        transformOrigin: '100% 50%',\n      }}\n    >\n      <Card w={CARD_W} h={CARD_H} seed={i + 1} />\n    </div>\n  );\n};\n\nexport const AxialStretch: React.FC = () => {\n  const frame = useCurrentFrame();\n  const titleOp = interpolate(frame, [0, 10], [0, 1], {\n    extrapolateLeft: 'clamp',\n    extrapolateRight: 'clamp',\n  });\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>\n      <div style={{ position: 'absolute', top: 120, width: '100%', textAlign: 'center', opacity: titleOp }}>\n        <TitleBlock text=\"AXIAL STRETCH\" size={72} />\n      </div>\n      {/* 落位虚线槽，标出目标位置 */}\n      {[0, 1, 2].map((i) => (\n        <div\n          key={`slot-${i}`}\n          style={{\n            position: 'absolute',\n            left: ROW_X0 + i * (CARD_W + GAP),\n            top: ROW_Y,\n            width: CARD_W,\n            height: CARD_H,\n            border: `2px dashed ${G.bar}`,\n            borderRadius: 14,\n            boxSizing: 'border-box',\n          }}\n        />\n      ))}\n      {[0, 1, 2].map((i) => (\n        <FlyCard key={i} i={i} frame={frame} />\n      ))}\n    </div>\n  );\n};\n\n\n// ===== demos/ui-entrance/element-body-moves/ContactShadowLift.tsx =====\n// contact-shadow-lift｜接触阴影离面抬升\n// 浅底上一排 3 张卡，逐张被\"点名\"抬起：卡 translateY(-28px)+scale(1.08)，\n// 其正下方独立椭圆阴影同步放大变淡——纸片离桌感；落回时阴影收紧变实，\n// 落地 2f 卡壳 scale 0.99 微压。三张依次各来一遍。收尾真静止 ≥35f。\nimport React from 'react';\nimport { useCurrentFrame, interpolate, Easing } from 'remotion';\nimport { G, Card } from '../../_fixtures/Fixtures';\n\nconst outCubic = Easing.out(Easing.cubic);\nconst inCubic = Easing.in(Easing.cubic);\n\n// 每张卡的局部时间轴（局部帧 t）：\n// [0,10)   抬起  out-cubic\n// [10,28)  悬停 18f\n// [28,36)  落回  in-cubic\n// [36,38)  落地卡壳 scale 0.99\n// [38,43)  回弹 0.99→1.0 out-cubic\n// t<0 或 t>=43 完全静止（rest 态）\nconst LIFT_Y = -28; // 原案 -8 → 加码 -20 → QA 保险再到 -28\nconst LIFT_S = 1.08;\n\nconst cardMotion = (t: number) => {\n  const y = interpolate(t, [0, 10], [0, LIFT_Y], {\n    easing: outCubic, extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  }) + interpolate(t, [28, 36], [0, -LIFT_Y], {\n    easing: inCubic, extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n\n  let s: number;\n  if (t < 28) {\n    s = interpolate(t, [0, 10], [1, LIFT_S], {\n      easing: outCubic, extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    });\n  } else if (t < 38) {\n    s = interpolate(t, [28, 36], [LIFT_S, 0.99], {\n      easing: inCubic, extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    });\n  } else {\n    s = interpolate(t, [38, 43], [0.99, 1], {\n      easing: outCubic, extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n    });\n  }\n\n  // 抬升进度 0（贴桌）→1（悬空），驱动阴影\n  const lift = interpolate(t, [0, 10], [0, 1], {\n    easing: outCubic, extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  }) - interpolate(t, [28, 36], [0, 1], {\n    easing: inCubic, extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n\n  return { y, s, lift };\n};\n\nconst CARD_W = 360;\nconst CARD_H = 220;\nconst GAP = 120;\nconst STARTS = [2, 42, 82]; // 三张卡依次点名，间隔 40f；末次动画止于 f125，留 35f 真静止\n\nexport const ContactShadowLift: React.FC = () => {\n  const frame = useCurrentFrame();\n  const rowW = CARD_W * 3 + GAP * 2;\n  const left0 = (1920 - rowW) / 2;\n  const top = (1080 - CARD_H) / 2 - 20;\n\n  return (\n    <div style={{ width: 1920, height: 1080, background: G.bg, position: 'relative', overflow: 'hidden' }}>\n      {/* 假 dashboard 式顶栏，给浅底一点场景感 */}\n      <div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 84, background: G.panel, borderBottom: `2px solid ${G.line}`, display: 'flex', alignItems: 'center', padding: '0 48px', gap: 24, boxSizing: 'border-box' }}>\n        <div style={{ width: 40, height: 40, borderRadius: 10, background: G.side }} />\n        <div style={{ height: 18, width: 220, background: G.bar, borderRadius: 9 }} />\n        <div style={{ marginLeft: 'auto', width: 36, height: 36, borderRadius: 18, background: G.mid }} />\n      </div>\n\n      {[0, 1, 2].map((i) => {\n        const t = frame - STARTS[i];\n        const { y, s, lift } = cardMotion(t);\n        const x = left0 + i * (CARD_W + GAP);\n\n        // 独立接触阴影（不是 box-shadow）：卡正下方椭圆径向渐变\n        const shScale = 1 + 0.72 * lift;     // 1.0 → 1.72（对比再拉大）\n        const shOpacity = 0.55 - 0.37 * lift; // 0.55 → 0.18\n        const shW = CARD_W * 0.88;\n        const shH = 44;\n\n        return (\n          <React.Fragment key={i}>\n            <div\n              style={{\n                position: 'absolute',\n                left: x + (CARD_W - shW) / 2,\n                top: top + CARD_H - shH / 2 - 4,\n                width: shW,\n                height: shH,\n                borderRadius: '50%',\n                background: 'radial-gradient(ellipse at center, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.45) 42%, rgba(0,0,0,0) 72%)',\n                transform: `scale(${shScale})`,\n                opacity: shOpacity,\n              }}\n            />\n            <Card\n              w={CARD_W}\n              h={CARD_H}\n              seed={i + 2}\n              style={{\n                position: 'absolute',\n                left: x,\n                top,\n                boxShadow: 'none',\n                transform: `translateY(${y}px) scale(${s})`,\n              }}\n            />\n          </React.Fragment>\n        );\n      })}\n    </div>\n  );\n};\n\n\n"
}