# 光舞編輯器前端深度解析 – `index.html` × `main.js` × `dancer.js` > **閱讀對象**:具備 HTML/CSS 與 JavaScript 基礎語法的高中生。 > > **寫作風格**:第一人稱筆記,避開華麗詞藻,強調「…後」「…來」的流程連接詞;行文偏學生口吻,但保持條理。 --- ## 0 先講我為什麼要拆三檔案 * **`index.html`** 是舞台外框,決定有哪些區塊、元素、熱鍵。 * **`main.js`** 像導演,統籌「音波 → 影像 → Python」。 * **`dancer.js`** 則是演員類別,每一個舞者都是它的實例。 把三者分開,改動時才不會互相牽拖。下面我一檔一檔解析。 --- ## 1 `index.html` – HTML 架構 + CSS 造型 | 行數 | 重點片段 | 我怎麼讀 | | --------- | --------------------------------------------------------------------- | ------------------------------------------------------------ | | 1‑15 | `<!DOCTYPE html>` \~ `<meta charset>` | 先聲明 UTF‑8,避免中文亂碼。 | | 6‑13 | `<script src="https://unpkg.com/wavesurfer.js@6.6.3">` 連同三個插件 | 我先換成 v6.6.3,因為 v7 marker API 變動太大。 | | 20‑90 | `<style>` | 我用 CSS Grid 把版面切三欄:左工具列、中 Canvas、下波形;`--bg`、`--fg` 變數方便全站改色。 | | \~110 | `<div id="waveform"></div>` | WaveSurfer 容器,一行就好,寬度交給 CSS。 | | \~130 | `<canvas id="myCanvas" width="640" height="360"></canvas>` | 設 16:9 比例,後續 JavaScript 用 `ctx` 做 2D 繪圖。 | | \~150 | `<input id="zoom-slider" type="range">` | 滑桿一開始隱藏;當使用者按 `Ctrl + 滾輪` 我再 `slider.style.display='block'`。 | | \~160‑170 | `<script src="main.js"></script>` `<script src="dancer.js"></script>` | 載入順序要先 main.js,因為它會 new Dancer()。 | ### 1‑1 我在 CSS 做了哪些小技巧 1. **滑桿垂直翻轉** – `transform: rotate(-90deg);` 讓水平 `<input>` 變成上下拖拉。 2. **三欄自適應** – `grid-template-columns: 80px 1fr`,左側工具列固定 80px,中央跟著螢幕寬度伸縮。 3. **深色模式主題** – 用 `:root{--bg:#1e1e1e; --panel:#252525; --fg:#eee;}`,顏色集中管理。 > *如果只想改版面配色,動 CSS 就行,不用動 JS。* --- ## 2 `main.js` – 全局控制核心 我先列事件流程圖,再逐區塊拆碼。 ``` 使用者 → (鍵盤/滑鼠) → handleKeydown / handleWheel ↓ ↑ WaveSurfer --------------------> timeupdate ↓ ↑ QtWebChannel ← editor.py.receiveTime ← Python ``` ### 2‑1 全域變數(1‑30 行) ```js // main.js 行1‑12 export let N_DANCER = 0; export let allLight = []; export let frameTime = []; export let posData = []; export const colors = []; let canvas = document.getElementById('myCanvas'); let ctx = canvas.getContext('2d'); ``` * 我習慣先 `export` 以防其他檔案要引用(webpack 時最好用)。 * `canvas` 與 `ctx` 提前抓,之後 Dancer.draw 直接用。 ### 2‑2 初始化流程 ```js // main.js 行40‑70 async function boot() { await Promise.all([ initializeSettings(), // 抓 setting.json initializeData(), // 抓 data.json initializePosData() // 抓 pos.json ]); createWaveSurfer(); // 波形+音樂 createDancers(); // 建立 Dancer 實例 startAnimation(); // 進入 60FPS loop } window.addEventListener('load', boot); ``` > *我用 `Promise.all` 確保三份 JSON 都到手,才開始畫面。* 一開始我沒等就 `createDancers()`,結果因為 `colors` 還沒填完導致 `undefined`,踩過一次坑。 ### 2‑3 WaveSurfer 初始(80‑140 行) ```js // WaveSurfer 核心 wavesurfer = WaveSurfer.create({ container: '#waveform', height: 128, backend: 'WebAudio', waveColor: '#4fc3f7', progressColor: '#039be5' }); ``` 1. **plugins** – `regions.create({})`, `markers.create({})`, `minimap.create({})` 一次三個 plugin;`dragSelection` 記得設 `true` 才能拉區段。 2. **載音檔** – `wavesurfer.load('audio.mp3');` 路徑由 `setting.json.music` 指定。 3. **`timeupdate`** – 每幀 callback: ```js wavesurfer.on('timeupdate', (sec)=>{ window.qt_handler.receiveTime(sec); }); ``` *我把秒數丟給 Qt*,讓 Python 知道現在在哪裡。 ### 2‑4 動畫迴圈(150‑210 行) ```js function loop() { ctx.clearRect(0,0,canvas.width,canvas.height); for (let i=0;i<N_DANCER;i++) dancers[i].draw(wavesurfer.getCurrentTime()); requestAnimationFrame(loop); } ``` * 60 FPS 串成無限循環。 * `getCurrentTime()` 傳給每個 Dancer,讓他們自己決定該用哪個顏色幀。 ### 2‑5 鍵盤與滑鼠互動(220‑310 行) | 按鍵 | 行數 | 行為 | | ------------------------ | --- | ----------------------------------------- | | `Space` | 225 | play/pause toggle | | `ArrowLeft`/`ArrowRight` | 235 | ±5 秒快退/快轉 | | `<` `>` (`188` `190`) | 245 | ±0.1 秒微調 | | `Ctrl + Wheel` | 260 | 更新 `zoom-slider` + 呼叫 `wavesurfer.zoom()` | | 無修飾 Wheel | 275 | `wavesurfer.seekTo()` 左右捲動 | > *這些熱鍵對照 editor.py `setup_shortcuts()`,保持跨端一致。* --- ## 3 `dancer.js` – 角色類別詳解 ### 3‑1 類別雛形 ```js // dancer.js 行1‑40 export default class Dancer { constructor(id, baseX, baseY) { this.id = id; this.x = baseX; this.y = baseY; this.width = 64; this.height = 160; } // ... } ``` ### 3‑2 外部依賴 * `import { colors, allLight, frameTime, posData } from './main.js';` * 直接共用 main.js 的全域陣列,避免重複 fetch。 ### 3‑3 顏色取得流程 ```js getColor(segment, part) { const colorIdx = allLight[this.id][segment][part]; return colors[colorIdx]; // colors = [[r,g,b], ...] } ``` > *part 對照表:0/1 耳朵、2 頭、3‑6 上半身、…、12 鞋底,共 13 部。* ### 3‑4 `draw(time)` 渲染核心 1. **決定 segment** ```js const seg = getTimeSegmentIndex(frameTime, time*1000); ``` 2. **畫部位** ```js ctx.fillStyle = this.rgbToCss(this.getColor(seg, 2)); // 頭 ctx.fillRect(this.x+20 , this.y ,24,24); // ...對每部位重複 ``` 3. **若被選中 → 畫箭頭** ```js if(selectedId === this.id) this.drawArrow(); ``` ### 3‑5 互動:被點擊與拖曳 ```js isClicked(mx,my){ return mx>this.x && mx<this.x+this.width && my>this.y && my<this.y+this.height; } move(dx,dy){ this.x+=dx; this.y+=dy; } ``` * 在 `main.js` 的 `canvas.onmousedown` 先檢查所有 Dancer 是否 `isClicked()`;若是,就鎖定 `draggingIndex`。 * `mousemove` 時呼叫 `dancers[draggingIndex].move(dx,dy)`,同時 `qt_handler.updatepos({...})` 回寫 Python。 --- ## 4 三檔案聯合作業流程回顧 1. **載入** ‑ 瀏覽器開啟 → `index.html` 載 `main.js`、`dancer.js`。 2. **啟動** ‑ `boot()` 讀三個 JSON → 生成舞者 → `createWaveSurfer()`。 3. **播放** ‑ 使用者按 Space → WaveSurfer 播放音樂 → `timeupdate` 每 16ms 回報 → Qt 同步時間 → LED 服裝同步。 4. **編輯** ‑ 拖舞者 → `updatepos()` 寫 `pos.json` → Python 存檔 → 前端 `reloadDataAndRedraw()`。 5. **顏色改動** ‑ Python 側 ComboBox 改色 → `savejson()` → Firebase 觸發 → JS 拿到新 `data.json` → 畫面即時更新。 --- ## 5 我踩過的兩個雷 1. **音檔 CORS 問題** *第一次把 MP3 放 GitHub Pages 測,結果瀏覽器擋跨域。* 解法:同網域或在 headers 加 `Access-Control-Allow-Origin:*`。本地測試最穩。 2. **WaveSurfer v7 API 改動** 直接升 v7 會找不到 `markers.create()`,因為 plugin 改寫。若 marker 功能重要,先留在 v6。\* --- ## 6 實作建議 * **想讓音波左右捲動更滑順** → 把 `wheel` 事件改成 `requestAnimationFrame` 批次 seek,減少連續呼叫。 * **想支援 4K 螢幕** → `<canvas>` 用 `devicePixelRatio` 乘上寬高,再 `ctx.scale()` 回 1:1,避免模糊。 * **想加新版曲線走位** → 在 `dancer.js` 加貝茲曲線內插:`ctx.bezierCurveTo()` 畫軌跡,再丟控制點回 `pos.json`。 --- > 以上就是我對前端三檔案的完整拆解。若需要進一步 demo 或想要加上測試檔,留言給我,我再補。