iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
Vibe Coding

使用 Vibe Coding 開發一套 Tutting 模擬器吧!系列 第 9

📐DAY 9 | 觀察真人舞者律動:破除呆板感!注入 Easing 與 BPM 靈魂

  • 分享至 

  • xImage
  •  

前幾天一直在專注於 XBOT 骨架與 FK 開發與時間軸,但當我按下播放鍵讓 3D 火柴人跳起連續招式時,心裡卻浮現出一個極大的問號:

「奇怪,明明關節角度都很準,為什麼虛擬舞者跳起來這麼像個機器人,看起來這麼呆板?」

今天我們暫時放下程式開發,先從觀察人類舞者開始並提出關鍵的解決方案!

https://ithelp.ithome.com.tw/upload/images/20260821/20144288mUqbvc6J1S.jpg

舞者實體觀察:人類肌肉發力

當我們仔細觀察真人 Tutting 舞者在卡點與過渡時的肌肉運作參考影片,會發現人體動作絕不是機械式的。這種勻速移動在視覺上會呈現出強烈的機械感與呆板感,完全失去了街舞的韌性與節奏律動。

  • 爆發與蓄力:動作啟動時有瞬間的加速度,接近目標角度時會有微小的減速與肌肉收縮,呈現非勻速的彈性流動。
  • 卡點定格(Freeze):抵達姿態終點時,肌肉鎖定會帶來極微小的反作用力拉扯(Micro-rebound),而不是突兀的零緩衝煞車。
  • 音樂卡點(On-Beat):舞者的所有過渡時間與停點長度,完全嚴格對齊音樂的 BPM 節奏與拍子子集(如 1/4 拍、1/8 拍)。

兩大核心演算法解法:注入舞者靈魂

為了讓數位舞者擺脫呆板,我們必須在時間軸渲染引擎中引入以下兩大機制:

1. 引入緩動函數(Easing Functions)

緩動函數(Easing Functions) 是用來控制參數隨時間變化速率的數學算式,核心作用在於精準掌握動畫或動態過渡中的「加速度變化」。

現實世界中的物體運動(如車子啟動、球體落地的反彈)極少以恆定速度進行,通常伴隨著加速與減速的慣性過程。透過緩動函數,能夠打破機械式「勻速直線運動」的生硬感,讓 UI 介面、遊戲以及動畫呈現出更貼近真實物理慣性、且符合視覺舒適度的動態品質。

在 Tutting 模擬器中,舞蹈動作極為講究頓點與力道爆發。因此,根據舞者的肢體慣性引入合適的緩動函數——例如呈現滑順質感的 easeInOutCubic,或是帶有微幅回彈與卡點震撼力的 easeOutBack——能大幅提升動態的視覺真實感,精準還原真人舞者的精神與肌肉爆發力。


圖片來源:https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSdHJbjk2vZcEsg_MByhwFb_xuQQFpBqVJhHg04l2GIlDBU8CcfBlImm84&s=10

2. BPM 節拍計時器與動態時間軸綁定

為了貼近舞者直覺的音樂語言,系統將動作過渡時間從傳統的「絕對秒數」轉換為基於 每分鐘節拍數(Beats Per Minute, BPM) 的音樂時間軸。在此機制下,每組 Pose 到 Pose 之間的插值時間,皆能動態換算為標準的音樂拍數(如半拍 1/2 beat 或四分之一拍 1/4 beat )。
所有關節的 Easing 曲線與過渡時長皆能與 BPM 節拍完美重合,確保每一次手盒折疊與幾何變幻,都能實現 100% 精準打擊音樂重拍的極致卡點。

結語:完美的角度只是骨架,彈性的曲線才是靈魂

完美的幾何角度給予了 Tutting 骨架,但緩動函數與 BPM 的節奏鎖定,才是賦予數位舞者肉體與靈魂的關鍵!

明天我們將與AI一同實作,完成緩動函數與 BPM 的節奏功能吧!!!!


以下是「Easing 函式動畫圖鑑」的prompt,可以直接複製給任何 AI 助手用來重新產生:

# Easing 函式動畫圖鑑 — 生成 Prompt

請幫我製作一個獨立的 HTML 檔案,視覺化展示以下 32 種 easing(緩動)函式,並加入動畫效果。

## Easing 函式清單(JavaScript 程式碼)

``javascript
const EASINGS = {
  linear:        t => t,
  smoothStep:    t => t*t*(3 - 2*t),
  easeInSine:    t => 1 - Math.cos((t*Math.PI)/2),
  easeOutSine:   t => Math.sin((t*Math.PI)/2),
  easeInOutSine: t => -(Math.cos(Math.PI*t) - 1)/2,
  easeInQuad:    t => t*t,
  easeOutQuad:   t => 1 - (1-t)*(1-t),
  easeInOutQuad: t => t < 0.5 ? 2*t*t : 1 - Math.pow(-2*t+2, 2)/2,
  easeInCubic:    t => t*t*t,
  easeOutCubic:   t => 1 - Math.pow(1-t, 3),
  easeInOutCubic: t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2,
  easeInQuart:    t => t*t*t*t,
  easeOutQuart:   t => 1 - Math.pow(1-t, 4),
  easeInOutQuart: t => t < 0.5 ? 8*t*t*t*t : 1 - Math.pow(-2*t+2, 4)/2,
  easeInQuint:    t => t*t*t*t*t,
  easeOutQuint:   t => 1 - Math.pow(1-t, 5),
  easeInOutQuint: t => t < 0.5 ? 16*t*t*t*t*t : 1 - Math.pow(-2*t+2, 5)/2,
  easeInExpo:    t => t === 0 ? 0 : Math.pow(2, 10*t - 10),
  easeOutExpo:   t => t === 1 ? 1 : 1 - Math.pow(2, -10*t),
  easeInOutExpo: t => t === 0 ? 0 : t === 1 ? 1 : t < 0.5 ? Math.pow(2, 20*t-10)/2 : (2 - Math.pow(2, -20*t+10))/2,
  easeInCirc:    t => 1 - Math.sqrt(1 - Math.pow(t, 2)),
  easeOutCirc:   t => Math.sqrt(1 - Math.pow(t-1, 2)),
  easeInOutCirc: t => t < 0.5 ? (1 - Math.sqrt(1 - Math.pow(2*t, 2)))/2 : (Math.sqrt(1 - Math.pow(-2*t+2, 2)) + 1)/2,
  easeInBack: t => { const c1=1.70158, c3=c1+1; return c3*t*t*t - c1*t*t; },
  easeOutBack: t => { const c1=1.70158, c3=c1+1; return 1 + c3*Math.pow(t-1,3) + c1*Math.pow(t-1,2); },
  easeInOutBack: t => {
    const c1=1.70158, c2=c1*1.525;
    return t < 0.5
      ? (Math.pow(2*t, 2) * ((c2+1)*2*t - c2)) / 2
      : (Math.pow(2*t-2, 2) * ((c2+1)*(t*2-2) + c2) + 2) / 2;
  },
  easeInElastic: t => {
    const c4 = (2*Math.PI)/3;
    return t === 0 ? 0 : t === 1 ? 1 : -Math.pow(2, 10*t-10) * Math.sin((t*10-10.75)*c4);
  },
  easeOutElastic: t => {
    const c4 = (2*Math.PI)/3;
    return t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10*t) * Math.sin((t*10-0.75)*c4) + 1;
  },
  easeInOutElastic: t => {
    const c5 = (2*Math.PI)/4.5;
    return t === 0 ? 0 : t === 1 ? 1 : t < 0.5
      ? -(Math.pow(2, 20*t-10) * Math.sin((20*t-11.125)*c5))/2
      : (Math.pow(2, -20*t+10) * Math.sin((20*t-11.125)*c5))/2 + 1;
  },
  easeOutBounce,
  easeInBounce:    t => 1 - easeOutBounce(1 - t),
  easeInOutBounce: t => t < 0.5 ? (1 - easeOutBounce(1 - 2*t))/2 : (1 + easeOutBounce(2*t - 1))/2
};

const EASING_GROUPS = [
  { label:"基本", items:[["linear","Linear(線性)"],["smoothStep","SmoothStep(平滑)"]] },
  { label:"Sine",    items:[["easeInSine","In"],["easeOutSine","Out"],["easeInOutSine","InOut"]] },
  { label:"Quad",    items:[["easeInQuad","In"],["easeOutQuad","Out"],["easeInOutQuad","InOut"]] },
  { label:"Cubic",   items:[["easeInCubic","In"],["easeOutCubic","Out"],["easeInOutCubic","InOut"]] },
  { label:"Quart",   items:[["easeInQuart","In"],["easeOutQuart","Out"],["easeInOutQuart","InOut"]] },
  { label:"Quint",   items:[["easeInQuint","In"],["easeOutQuint","Out"],["easeInOutQuint","InOut"]] },
  { label:"Expo",    items:[["easeInExpo","In"],["easeOutExpo","Out"],["easeInOutExpo","InOut"]] },
  { label:"Circ",    items:[["easeInCirc","In"],["easeOutCirc","Out"],["easeInOutCirc","InOut"]] },
  { label:"Back(甩過頭)",    items:[["easeInBack","In"],["easeOutBack","Out"],["easeInOutBack","InOut"]] },
  { label:"Elastic(彈簧)",   items:[["easeInElastic","In"],["easeOutElastic","Out"],["easeInOutElastic","InOut"]] },
  { label:"Bounce(彈跳)",    items:[["easeInBounce","In"],["easeOutBounce","Out"],["easeInOutBounce","InOut"]] }
];
``

## 需求

1. 產生一個**單一、可獨立開啟**的 `.html` 檔案(不依賴外部框架)。
2. 依照 `EASING_GROUPS` 分組,每組標題顯示家族名稱(如 Sine、Quad、Back 等),底下用網格排列該組所有 easing 的卡片。
3. 每張卡片包含:
   - 一個小型 SVG 曲線圖,畫出該 easing 函式在 t=0~1 區間的輸出曲線(含格線、對角參考虛線)。
   - 曲線上有一個會動的圓點,即時對應該函式在目前時間下的輸出值。
   - 曲線下方一條水平「軌道」,其中一顆小球依照該 easing 函式移動,直觀呈現實際運動的加速/減速/回彈手感。
   - 函式名稱標籤(中英對照,例如「In (easeInSine)」)。
4. **動畫需求**:所有卡片的動畫同步循環播放(例如跑 1.4 秒、停頓 0.5 秒後重來),並提供「暫停/播放」按鈕。自動偵測系統的「減少動態效果」(`prefers-reduced-motion`)設定,若使用者開啟則預設暫停動畫。
5. 支援亮色/暗色模式自動切換(使用 CSS 變數 + `prefers-color-scheme`)。
6. 版面簡潔、卡片式設計,寬度自適應(`grid-template-columns: repeat(auto-fit, minmax(140px, 1fr))`)。


上一篇
📐DAY 08 | 用 MVP 最小改動打造「8拍動態編舞」
系列文
使用 Vibe Coding 開發一套 Tutting 模擬器吧!9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言