Skip to content

SDVX Wallpaper 程式碼整理

紀錄這次對 assets/js/model.jsassets/js/wallpaper.js 的修改。 model.js2920 行減到 2402 行,行為維持不變(下面有標「行為改變」的除外)。


1. 拖曳移動修好了

問題一:模型跑得比游標快

modelX / modelY視窗寬高的百分比,不是像素:

js
model.x = width / 2 + this.modelX * width / 100;

但拖曳的程式碼是直接加像素差 × 0.1:

js
model.modelX += deltaX * 0.1;   // 舊的

換算下來模型實際移動 deltaX * width / 1000 像素 —— 在 1920 寬的螢幕上等於游標的 1.9 倍,所以會越拖越偏離抓取的位置。

改成用實際視窗寬度換算,並且以按下去的那一點為基準計算,而不是每次累加:

js
let deltaX = (e.clientX - dragOriginX) * 100 / window.innerWidth;
model.modelX = clamp(dragOriginModelX + deltaX, -50, 50);

用原點計算還順便修掉一個問題:以前拖超過 ±50 的邊界再拖回來,模型會永遠偏移一段距離;現在拖回範圍內就會重新對齊游標。

順便加了 setPointerCapture,游標移出視窗邊緣時拖曳不會斷掉。

問題二:拖曳會卡頓

pointermove 每次都呼叫 model.onResize(1),而 onResize() 裡面有:

  • updateBg() —— 把背景 sprite 整個砍掉重建,重新 PIXI.Texture.from(videoURL),還對影片元素呼叫 .pause() / .play()
  • renderer.resize() —— 重新配置 WebGL frame buffer

滑鼠一秒可以送出上百個 pointermove,等於一秒重建上百次背景素材。

所以把單純移動模型的部分拆成新的方法:

js
updateModelTransform()   // 只改模型的 x / y / scale,拖曳和縮放用這個
onResize(event)          // 呼叫上面那個,再處理背景和 renderer,只有視窗真的改變大小才用

拖曳、雙指縮放、滾輪縮放都改成呼叫 updateModelTransform()


2. 換模型的等待時間改成讀 json

以前 wallpaper.js 裡有一個 85 行的 switch (oldModelname),每個模型硬寫一個毫秒數:

js
case "kureha_9thkac":
    setTimeout(() => { model.loadModel(modelName) }, 4117);

那些數字其實就是退場動作的長度。現在直接從模型自己的設定讀出來:

js
const settings = live2dModel.internalModel.settings;      // 已載入模型的 model3.json
const url = settings.resolveURL(settings.motions[group][0].File);
const motion = await fetch(url).then(r => r.json());
const duration = Math.round(motion.Meta.Duration * 1000);  // motion3.json 裡的長度
  • 讀過的會存進 outDurationCache,同一個模型只抓一次
  • 讀不到(沒有那個動作群組、fetch 失敗)就退回 1800ms,跟以前的 default: 一樣
  • 動作開始時記 performance.now(),抓完 json 只等剩下的時間,所以 fetch 的延遲不會加到換模型的等待上
  • 哪個模型用哪個退場動作(Out / Out1 / Final / EG4_out / Last)集中到 getOutMotionGroup(),播放和算長度共用同一個值,不會再對不上

驗證結果

對照 data.json 裡全部 53 個模型:原本有寫 case 的全部算出一模一樣的數字(1583、2050、4117、9880、12633…),代表那些硬寫的值確實就是 Meta.Duration × 1000

⚠️ 行為改變

有 25 個模型以前是掉進 default: 1800,動作被切掉或多等,現在會用真正的長度:

模型原本現在
grace_yukata_ver518004000
tsunomaki_watame18003067
left_hexathlon18002520
rasis_hexathlon18002467
kureha_ver618002167
right_hexathlon18002067
nianoa_egbpl / _sp17422058
其他 18 個18001517 ~ 1867

另外兩件事:

  • 舊的 case "grace_yukata"永遠不會執行的——data.json 傳進來的值是 grace_yukata_ver5。它的動作其實有 4 秒,等於以前每次都被切掉 2.2 秒。
  • ortlinde_akasha_game_kac 共用 12633ms,但它根本沒有 Out 動作群組(只有 In1 / In2 / Idle / Idle2 / Last),所以以前是空等 12.6 秒什麼都沒播。現在會退回 1800ms。如果想讓它跟 _kac 一樣播 Last,在 getOutMotionGroup() 加一行就好,長度會自動算對。

3. model.js 整理

按鈕:35 段複製貼上 → makeButton()

檔案裡有 35 個按鈕,每個都是同樣的 15 行樣板(白色圓形 + 藍色文字 + zIndex 5 + interactive + 推進 buttonArray + 加到 stage),只有文字、字級、位置不一樣。

現在統一由一個方法生成:

js
makeButton(label, x, y, onClick, fontSize = 24)

呼叫端從 15 行變 3 行:

js
this.makeButton('OK', 35, 35, () => {
    this.model.motion("Ok", 0, 3);
});

這是這次最大的一塊,省掉約 450 行。

35 個按鈕的 handler 內容有逐字比對過,除了下面刻意改寫的 3 個之外,其餘 32 個完全沒動。

反應動作:3 段重複 → playReaction()

Good / Bad / VGood 三個按鈕各有 20 行一模一樣的邏輯(播動作 → 把 Idle 的 fade 拉長到 1.5 秒 → 等動作播完再改回 0.015)。抽成:

js
this.playReaction("R_Good");

小差異

setTimeout 裡面改成用呼叫當下抓好的 motionGroups 參考,而不是等 timer 觸發時再讀 this.model。如果在動作播完前就換了模型,舊的寫法會去改到新模型的 fade(或因為 this.model 已經是 undefined 而報錯),新的寫法不會。

模型路徑:40 行 switch → MODEL_JSON_PATHS

loadModel() 裡找 model3.json 的那段 switch 改成一張表,只列不規則的 23 個,其餘一律套 <name>/<name>_0101.model3.json

js
const MODEL_JSON_PATHS = {
    'grace_ver5':       'grace_ver5/grace_ver5_0102',
    'rasis_ver5':       'rasis_ver5/rasis_ver5_0103',
    'bpl3':             'left_right_hexathlon_bpl/left_right_hexathlon_bpl',
    // ...
};

const modelJsonPath = (name) => './assets/live2d/' + (MODEL_JSON_PATHS[name] || `${name}/${name}_0101`) + '.model3.json';

54 個模型的路徑都跟舊的 switch 比對過,全部一致,而且檔案都確實存在

其他小地方

  • 音效:8 個 new Howl({...}) 各 6 行 → loadSound() 一行一個
  • updateModelTransform()model2 / model3 / lrVFXmodel / rasisVFXmodel 各自重複四行一樣的定位程式碼 → 收進一個陣列跑迴圈
  • destroy() 的記憶體問題buttonArraysubContainers 只把東西從 stage 移除,陣列本身從來沒清空,每換一次模型就越積越多。現在會重設成 []
  • case 'maxima_vm' 沒有 break:是故意的(maxima 除了眼鏡按鈕還要有一般按鈕),加了註解免得以後自己看到以為是 bug

沒有動的地方

generate_button_sudden() 還是 700 行。裡面是 sudden death 那一整段演出的流程(sprite、filter、時間軸),不是重複的樣板,硬拆反而更難讀,所以留著。

同理,27 個 PIXI.Sprite.from(...) 的設定區塊有 10 種不同的屬性組合,包裝成通用 helper 要傳一堆 optional 參數,不見得比現在好讀。

還沒驗證的部分

以上都通過 node --check 語法檢查,換模型的等待時間和模型路徑也用腳本對全部模型驗算過。但沒有實際跑起來看畫面 —— 這台機器沒有跑這個站的 server,Live2D 也需要 WebGL。建議自己開起來確認:

  1. 右鍵拖曳模型(應該完全貼著游標走)
  2. 滾輪縮放、雙指縮放
  3. 換幾個模型,特別是 grace_yukata_ver5tsunomaki_watameortlinde_akasha_game 這三個等待時間有變的
  4. 各個特殊模型的按鈕(bpl3、mion、666、grace、akasha、sudden、maxima)

原始檔案的備份在 scratchpad:model.js.bak / wallpaper.js.bak

© 2026 拉特. 保留所有權利.