將製作ejcees(中國象棋打譜程式)
Re: 將製作ejcees(中國象棋打譜程式)
一,整個Class設為Ejcees,將所有變量和方法都用this.來歸放。
二,style,svg.ejceespb都是動態加入。
三,當頁面上存在div.ejcees[mode=ejceesmain]時,說明它是單頁面,整個body只存在這個元素以及這個js的引用。則表現跟現在完全一樣。
四,當頁面上存在div.ejcees[mode=ejceessingle]時,說明它將插入到頁面上的一個主要的地方,比如文章正文。則它的css最大寬度的計算應跟它所插入的地方的父元素一致。div.ejcees[mode=ejceessingle]在一個頁面上只能存在一個,能使用鍵盤上的快捷鍵。
五,當頁面上存在一個或多個div.ejcees[mode=ejceesmini]時,說明將有ejceesmini插入到頁面上的任意地方,它的css最大寬度的計算應跟它所插入的地方的父元素一致。其css以及顯示元素要經過較大調整,ejceestool和ejceestext將不再顯示。ejceesstep的最左邊將放置ejceesoutput,固定其長寬,並且只顯示當前步數和總步數(如20/80)。ejceesstep的最右邊將顯示btn-autoplay按鈕。當有分支時,它將直接絕對定位背景半透明顯示在ejceessvg的左上角,當有評論時,它將直接絕對定位背景半透明顯示在ejceessvg的最底部。div.ejcees[mode=ejceesmini]將不能使用鍵盤上的快捷鍵。
先給出工作流程,製作方法以及注意事項。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將製作ejcees(中國象棋打譜程式)
代码: 全选
這是一個非常龐大且經典的前端重構任務。將面條式(Spaghetti code)的腳本轉換為物件導向(OOP),並且要支援多實例(Multi-instance)與不同的響應式佈局模式,需要非常嚴謹的架構規劃。
以下為您規劃的**工作流程**、**製作方法**以及**核心注意事項**,並附上重構的**程式碼骨架**。
---
### 一、 工作流程 (Workflow)
1. **環境與基礎類別建立:**
* 定義 `class Ejcees`。
* 在建構子(`constructor`)中接收 DOM 元素和配置參數。
* 設定靜態方法掃描頁面上的 `div.ejcees` 並自動實例化。
2. **動態資源注入 (CSS & SVG):**
* 將原本的 CSS 寫成字串,並在第一個實例創建時,動態插入到 `<head>` 中(全域只需插入一次)。
* 將 SVG 結構與 UI 結構寫成模板字串,根據不同的 `mode` 動態生成並插入到該實例的容器中。
3. **狀態與變數封裝:**
* 將原本全域的 `let` 變數(如 `currentTurn`, `historyFEN`, `isEditMode` 等)全部轉換為實例屬性(`this.currentTurn`, `this.historyFEN` 等)。
4. **DOM 選擇器作用域限制:**
* 將所有 `document.getElementById` 或 `document.querySelector` 替換為 `this.container.querySelector`,確保多實例情況下不會互相干擾。
5. **事件綁定與 `this` 指向修正:**
* 所有事件監聽器(Event Listeners)必須使用箭頭函數 `() => {}` 或 `.bind(this)`,否則 `this` 會指向 DOM 元素而非 `Ejcees` 實例。
6. **模式特異化開發 (Mode Specifics):**
* **ejceesmain:** 預設模式,滿版顯示。
* **ejceessingle:** 插入文章流中,寬度依賴父元素,需掛載鍵盤快捷鍵(全域唯一)。
* **ejceesmini:** 極簡模式,移除工具列與紀錄區,重構底部控制條,並將分支與評論改為絕對定位的懸浮層(Overlay)。禁止綁定鍵盤事件。
7. **測試與除錯:**
* 先測試單一實例(`ejceesmain`),確保基礎功能(走子、悔棋、動畫)正常。
* 再測試多實例(多個 `ejceesmini` 同時存在),檢查變數是否污染、SVG 渲染是否衝突。
---
### 二、 製作方法與架構設計
#### 1. 初始化與入口
在檔案底部加入一段自動掃描腳本。當 DOM 載入完成後,尋找所有帶有 `class="ejcees"` 的元素,讀取其 `mode` 屬性,並 `new Ejcees(element, mode)`。
#### 2. CSS 隔離與動態注入
原本的 CSS 是針對全螢幕設計的(`100vw`, `100vh`)。重構時:
* `.ejceesmain` 保持使用 `100vw` / `100vh`。
* `.ejceessingle` 與 `.ejceesmini` 應設定 `width: 100%; height: auto; aspect-ratio: ...` 或依賴父容器。
#### 3. SVG ID 衝突解決方案
這是多實例最容易出錯的地方!原本的 SVG 定義了 `<defs>`,裡面有 `#tile0`, `#tile1` 等。
如果頁面上有三個 `ejceesmini`,就會有三個 `<g id="tile0">`。這在 HTML 標準中是不合法的,且會導致渲染錯誤。
**解法:** 在實例化時,生成一個唯一的 `instanceId`(例如 `ejcees-1234`),將所有的 ID 與 `href` 引用加上後綴(如 `id="tile0-ejcees-1234"`, `href="#tile0-ejcees-1234"`)。
#### 4. UI 渲染邏輯重構 (`ejceesmini` 專屬)
在 `renderUI` 階段判斷:
* 如果是 `mini`,隱藏或不生成 `.ejceestool` 和 `.ejceestext`。
* 修改 `.ejceesstep` 結構:左側放入 `.ejceesoutput`,中間進度條,右側放入 AutoPlay 按鈕。
* 將原本顯示在側邊欄的評論(`this.historyFEN.c`)改寫為監聽步驟變更時,若有評論,則顯示在 `.ejceessvg` 內的絕對定位 `div` 中。
---
### 三、 核心注意事項 (Gotchas)
* **不要再用 ID 綁定 DOM:** 原本的 `id="tool-new"` 在多實例會崩潰。請全部改用 `class="tool-btn tool-new"` 或 `data-action="new"` 來選取。
* **鍵盤事件防護:** `keydown` 事件是綁定在 `document` 上的。
* 如果頁面上有一個 `ejceessingle` 和三個 `ejceesmini`,只有 `ejceessingle` 應該對按鍵做出反應。
* 在 `keydown` 處理函數開頭加入判斷:`if(this.mode === 'ejceesmini') return;`
* **動畫效能:** 如果頁面上有 10 個 `ejceesmini` 實例,確保不在畫面內(Viewport)的實例不要自動播放動畫(可使用 `IntersectionObserver` 進行優化,不過這算進階功能,可先放著)。
* **拖曳/點擊座標計算:** 原本的 `pt.matrixTransform(svgElement.getScreenCTM().inverse())` 非常依賴 SVG 的渲染尺寸。當容器大小隨父元素縮放時(`single` / `mini`),這段程式碼依然有效,但必須確保 SVG 的 `viewBox` 設定正確,且不要用寫死的 PX 去推算滑鼠位置。
---
### 四、 程式碼骨架 (Code Skeleton)
您可以依照這個架構,將原本的邏輯填入對應的方法中。
```javascript
class Ejcees {
// 靜態屬性:確保全域 CSS 只注入一次
static cssInjected = false;
// 靜態屬性:實例計數器,用於生成唯一 ID
static instanceCount = 0;
constructor(container, mode) {
this.container = container;
this.mode = mode || 'ejceesmain'; // 'ejceesmain' | 'ejceessingle' | 'ejceesmini'
this.instanceId = `ejcees-inst-${Ejcees.instanceCount++}`;
// === 1. 核心狀態變數 (從原本的 let 搬過來) ===
this.INITIAL_FEN = 'rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1';
this.currentTurn = 'w';
this.halfMoveClock = 0;
this.fullMoveNumber = 1;
this.historyFEN = { fen: this.INITIAL_FEN, move: null, lastMove: null, v: [] };
this.currentBranch = [];
this.currentStepIndex = 0;
// 棋盤映射
this.tileMap = new Map();
this.piecePos = new Map();
// UI 狀態
this.isEditMode = false;
this.isAutoPlaying = false;
// ... 其他原本的 let 變數 ...
// === 2. 初始化 ===
this.injectCSS();
this.renderDOM();
this.cacheDOM();
this.bindEvents();
this.initGame();
}
// --- 方法:全域 CSS 注入 ---
injectCSS() {
if (Ejcees.cssInjected) return;
const style = document.createElement('style');
style.innerHTML = `
/* 這裡放入原本的 CSS,並根據 mode 加上前綴限制作用域 */
.ejcees-container[data-mode="ejceesmain"] { width: 100vw; height: 100vh; display: flex; }
.ejcees-container[data-mode="ejceessingle"] { width: 100%; max-width: 100%; display: flex; }
.ejcees-container[data-mode="ejceesmini"] { width: 100%; position: relative; display: flex; flex-direction: column; }
/* Mini 模式專屬 Overlay */
.ejcees-overlay-comment { position: absolute; bottom: 0; left: 0; width: 100%; background: rgba(0,0,0,0.7); color: #fff; padding: 10px; display: none; }
.ejcees-overlay-branch { position: absolute; top: 0; left: 0; background: rgba(255,255,255,0.8); display: none; }
/* ... 其餘原本的 CSS ... */
`;
document.head.appendChild(style);
Ejcees.cssInjected = true;
}
// --- 方法:動態生成 DOM 結構 ---
renderDOM() {
this.container.classList.add('ejcees-container');
this.container.setAttribute('data-mode', this.mode);
// 動態替換 SVG 內的 ID,避免多實例衝突
let svgDefString = `... 這裡放原本 SVG <defs> 的內容,但所有的 id="tile0" 要改成 id="tile0-${this.instanceId}" ...`;
let htmlString = '';
if (this.mode === 'ejceesmain' || this.mode === 'ejceessingle') {
htmlString = `
<div class="ejceesmain">
<div class="ejceessvg-wrapper">
</div>
<div class="ejceesctrl">
<div class="ejceesoutput">...</div>
</div>
</div>
`;
} else if (this.mode === 'ejceesmini') {
htmlString = `
<div class="ejceesmini-wrapper" style="position: relative;">
<div class="ejcees-overlay-branch"></div>
<div class="ejcees-overlay-comment"></div>
</div>
<div class="ejceesstep mini-step-bar">
<div class="ejceesoutput mini-output">0/0</div>
<input type="range" class="ejceesstepdrop" min="0" max="0" value="0" />
<div class="btn-autoplay">Play</div>
</div>
`;
}
this.container.innerHTML = htmlString;
}
// --- 方法:快取 DOM 元素 ---
cacheDOM() {
this.svgElement = this.container.querySelector('.ejceespb');
this.outputElement = this.container.querySelector('.ejceesoutput');
this.stepSlider = this.container.querySelector('.ejceesstepdrop');
// ... 取代原本的 document.querySelector ...
}
// --- 方法:綁定事件 ---
bindEvents() {
// SVG 點擊事件 (注意 bind(this) 或是用箭頭函數)
if (this.svgElement) {
this.svgElement.addEventListener('click', (e) => this.handleSvgClick(e));
}
// 滑桿事件
if (this.stepSlider) {
this.stepSlider.addEventListener('input', (e) => this.handleSlider(e));
}
// 全域鍵盤事件 (只有 Single 或 Main 模式掛載)
if (this.mode !== 'ejceesmini') {
document.addEventListener('keydown', (e) => this.handleKeydown(e));
}
}
// --- 方法:核心邏輯區 ---
initGame() {
this.loadFEN(this.historyFEN.fen);
this.renderRecordUI();
}
loadFEN(fenStr) {
// 實作原本的 loadFEN,注意裡面創建 <use> 時,href 要加上 this.instanceId
}
handleSvgClick(event) {
// 實作原本的點擊走子邏輯
}
handleKeydown(e) {
// 實作快捷鍵
}
// --- 方法:UI 更新 ---
renderNoteUI() {
// 針對不同模式處理評論顯示
const node = this.getNodeAtStep(this.currentStepIndex);
if (!node) return;
if (this.mode === 'ejceesmini') {
const commentOverlay = this.container.querySelector('.ejcees-overlay-comment');
if (node.c && node.c.trim() !== '') {
commentOverlay.textContent = node.c;
commentOverlay.style.display = 'block';
} else {
commentOverlay.style.display = 'none';
}
} else {
// 原本 Main / Single 的右下角 UI 更新邏輯
}
}
// ... 其餘幾十個方法 (boardToFEN, isValidMove, etc.) 全部包進來 ...
}
// === 自動初始化腳本 ===
document.addEventListener('DOMContentLoaded', () => {
// 尋找所有帶有 class="ejcees" 的容器
const containers = document.querySelectorAll('.ejcees');
containers.forEach(container => {
const mode = container.getAttribute('mode');
// 實例化該組件
new Ejcees(container, mode);
});
});
```
根據這個骨架,您就可以開始將那 2000 多行的函數與物件逐步搬移進 `class Ejcees` 裡面。這是一個需要耐心的過程,強烈建議**先從 DOM 的綁定與 SVG 的隔離著手**,確保在一個頁面放兩個 `div.ejcees` 時,它們的版面和 SVG 渲染不會互相干擾,再來搬移核心的走棋邏輯。https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將製作ejcees(中國象棋打譜程式)
代码: 全选
這是在類別化重構中最重要的部分。在 `constructor` 中,我們需要將所有原本在全域範圍的變數轉化為**實例屬性**,並且處理 **SVG ID 唯一化**以防止多實例衝突。
以下是 `Ejcees` 類別的 `constructor` 實作:
```javascript
class Ejcees {
constructor(container, mode) {
// 1. 基本屬性與模式設定
this.container = container;
this.mode = mode || 'ejceesmain'; // 'ejceesmain', 'ejceessingle', 'ejceesmini'
// 生成唯一 ID,用於區分多個實例的 SVG Defs 引用
this.instanceId = 'ej_' + Math.random().toString(36).substr(2, 9);
// 2. 棋局核心狀態 (原本的全域 let 變數)
this.INITIAL_FEN = 'rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1';
this.currentTurn = 'w';
this.halfMoveClock = 0;
this.fullMoveNumber = 1;
// 歷史與分支數據
this.historyFEN = {
fen: this.INITIAL_FEN,
move: null,
lastMove: null,
c: '', // 評論
v: [] // 分支變體
};
this.currentBranch = [];
this.currentStepIndex = 0;
this.undoStack = [];
// 棋盤邏輯映射
this.tileMap = new Map(); // 座標 -> 元素
this.piecePos = new Map(); // 元素 -> 座標
// 3. UI 與 動畫狀態
this.selectedTile = null;
this.isEditMode = false;
this.isAutoPlaying = false;
this.moveInterval = 1000;
this.moveSpeed = 300;
this.isRotateEnabled = false;
this.isFlipEnabled = false;
this.pgnMetadata = { Event: 'Casual Game', Site: 'Local', Date: '', White: '', Black: '', Result: '*' };
// 4. 動態資源初始化
this.injectGlobalStyles(); // 靜態方法,全域只注入一次 CSS
this.renderLayout(); // 根據 mode 生成 HTML 結構
this.initSvgDefinitions(); // 初始化並唯一化 SVG <defs>
// 5. DOM 元素快取 (從 container 內尋找,不可使用 document.getElementById)
this.svgElement = this.container.querySelector('.ejceespb');
this.boardGroup = this.container.querySelector('.board-group');
this.piecesGroup = this.container.querySelector('.pieces-group');
this.marksGroup = this.container.querySelector('.marks-group');
this.stepSlider = this.container.querySelector('.ejceesstepdrop');
this.outputElement = this.container.querySelector('.ejceesoutput');
this.recordContainer = this.container.querySelector('.ejceestext-content');
this.commentArea = this.container.querySelector('.ejceestool-comment'); // 或 mini 模式的 overlay
// 6. 座標系統轉換工具 (SVG Point)
this.svgPoint = this.svgElement.createSVGPoint();
// 7. 綁定事件監聽
this.bindEvents();
// 8. 載入初始棋局
this.loadFEN(this.INITIAL_FEN);
this.updateUI();
}
// --- 以下為配套的私有初始化方法簡述 ---
renderLayout() {
// 這裡判斷 this.mode
// 若為 ejceesmini:
// - 隱藏 .ejceestool 和 .ejceestext
// - 在 .ejceesstep 左邊插入 .ejceesoutput (20/80 格式)
// - 在右邊插入 Play 按鈕
// - 插入懸浮的 Branch 和 Comment div
const layout = this.mode === 'ejceesmini'
? this.getMiniTemplate()
: this.getMainTemplate();
this.container.innerHTML = layout;
}
initSvgDefinitions() {
// 重點:將原本 SVG 的 <use href="#red_k"> 改成 <use href="#red_k_${this.instanceId}">
// 並且將 <g id="red_k"> 改成 <g id="red_k_${this.instanceId}">
// 這樣頁面上同時存在多個棋盤時,圖形才不會互相抓錯。
}
bindEvents() {
// 點擊棋盤
this.svgElement.addEventListener('click', (e) => this.handleBoardClick(e));
// 滑桿與按鈕
if(this.stepSlider) {
this.stepSlider.addEventListener('input', (e) => this.jumpToStep(parseInt(e.target.value)));
}
// 快捷鍵:僅在非 mini 模式下掛載於 document
if (this.mode !== 'ejceesmini') {
document.addEventListener('keydown', this.handleGlobalKeydown.bind(this));
}
}
}
```
### 注意事項提醒:
1. **ID 唯一化**:在 `constructor` 產生的 `this.instanceId` 必須應用到所有內部 HTML/SVG 的 `id` 屬性上。否則,如果你在同一個頁面放兩個 `ejceesmini`,第一個棋盤的棋子可能會「飛」到第二個棋盤上(因為 CSS 或 SVG 引用了重複的 ID)。
2. **鍵盤事件 (Mode 四 vs 五)**:`ejceesmini` 明確規定不能使用快捷鍵。所以在 `bindEvents` 中我加了判斷。如果是 `ejceessingle`,雖然可以使用快捷鍵,但要注意如果頁面上有多個 instance(雖你說 single 只有一個),通常會建議檢查 `document.activeElement` 是否在該容器內。
3. **this 綁定**:所有事件回調(Callbacks)建議使用箭頭函數 `(e) => this.method(e)`,這能確保函數內部的 `this` 永遠指向 `Ejcees` 的實例物件。https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將製作ejcees(中國象棋打譜程式)
代码: 全选
這是一個非常經典的需求。雖然 `<object>` 標籤通常用於加載外部檔案,但如果你手頭上的是 **SVG 字符串(文本)**,直接放入 `<object>` 會稍微繞一點路,因為 `<object>` 的 `data` 屬性預期的是一個 URL。
要實現這個目標,最優雅的方式是利用 **Blob** 或 **Data URI**。
---
## 💡 核心流程
1. **轉換**:將 SVG 字符串轉換為 `Blob` 對象。
2. **生成 URL**:使用 `URL.createObjectURL()` 生成一個臨時網址。
3. **掛載**:將該網址賦值給 `<object>` 的 `data` 屬性。
4. **操作**:在 `load` 事件觸發後,透過 `contentDocument` 存取內部元素。
---
## 🛠️ 實作程式碼
### 1. HTML 結構
```html
<div id="container">
<object id="mySvgObject" type="image/svg+xml" width="300" height="300"></object>
</div>
```
### 2. JavaScript 邏輯
```javascript
const svgString = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle id="myCircle" cx="50" cy="50" r="40" fill="royalblue" />
</svg>`;
const objElement = document.getElementById('mySvgObject');
// --- 第一步:將字符串轉為 URL ---
const blob = new Blob([svgString], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
// --- 第二步:加載 SVG ---
objElement.data = url;
// --- 第三步:操作內部的 DOM ---
objElement.addEventListener('load', () => {
// 必須在 load 事件後才能獲取 contentDocument
const svgDoc = objElement.contentDocument;
// 像操作普通 DOM 一樣操作它
const circle = svgDoc.getElementById('myCircle');
if (circle) {
circle.setAttribute('fill', 'orange');
circle.addEventListener('click', () => {
alert('你點擊了 SVG 內部的圓形!');
});
}
// 記得釋放記憶體(如果不再需要動態更換)
// URL.revokeObjectURL(url);
});
```
---
## ⚠️ 關鍵注意事項
### 1. 同源策略 (CORS)
雖然你是用 `Blob` 生成的 URL,通常不會有跨域問題,但如果你是加載 **遠端伺服器** 的 SVG 檔案到 `<object>` 中,瀏覽器會限制你存取 `contentDocument`。
### 2. 為什麼不直接用 `innerHTML`?
如果你只是想操作 SVG,將 SVG 字符串直接放入一個 `<div>` 的 `innerHTML` 是最簡單的:
* **優點**:直接渲染,操作方便,不需考慮異步加載。
* **缺點**:SVG 的樣式可能會受全域 CSS 影響(有時這反而是優點)。
> **如果你選擇使用 `<object>`**,通常是因為你希望將 SVG 的作用域「隔離」開來,避免 CSS 或 ID 衝突。
### 3. 操作時機
**這是最常出錯的地方:** 你不能在設置 `objElement.data` 後立即操作元素。因為 `<object>` 加載資源是異步的,必須監聽 `load` 事件,否則 `contentDocument` 將會是 `null`。
---
## 🔍 比較表
| 方法 | 操作複雜度 | 樣式隔離 | 適用場景 |
| :--- | :--- | :--- | :--- |
| **Object + Blob** | 中(需處理異步) | **高**(完全獨立) | 需要封裝感、避免樣式污染。 |
| **innerHTML** | 低(同步操作) | **低**(受全域影響) | 快速開發、需要與頁面 CSS 互動。 |
| **Iframe** | 中 | **極高** | 幾乎不建議僅為了 SVG 使用。 |
你目前的專案需求,是否必須限制在 `<object>` 容器內呢?如果沒有特殊隔離需求,直接把 SVG 文本注入到 `div` 會讓開發輕鬆許多。https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 將製作ejcees(中國象棋打譜程式)
- 附件
-
ejceesobj20260426.7z- (54.88 KiB) 已下载 3 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
在线用户
正浏览此版面之用户: Bing [Bot] 和 1 访客
