代码: 全选
為了滿足您新增的「Element Diff 算法」動畫生成需求,我們需要修改 `ejsolid19683.js` 中的多個核心模塊,包含:**配置初始化**、**UI 渲染**、**DOM 快取與事件綁定**,以及核心的 **`generateSvg` 渲染與動畫生成算法**。
所有程式碼與注釋皆已遵照指示完全使用英文撰寫。以下是需要修改的所有地方:
### 1. 修改 `constructor` 中的 `this.config`
新增 `elementDiff` 的預設配置參數(預設關閉 `false`)。
```javascript
// Locate inside constructor -> this.config
this.config = {
sides: [3, 4, 5, 6, 8, 10],
defaultColors: { 3: '#00b4d8', 4: '#48cae4', 5: '#90e0ef', 6: '#0077b6', 8: '#023e8a', 10: '#03045e' },
labels: { 3: 'Triangle', 4: 'Square', 5: 'Pentagon', 6: 'Hexagon', 8: 'Octagon', 10: 'Decagon' },
canvasSize: 480,
contentSize: 360,
opacity: 0.7,
strokeWidth: 1.5,
strokeColor: '#03045e',
perspective: false,
animate: false,
elementDiff: false, // New: Tracks if the Element Diff algorithm toggle button is enabled
animTime: 7.0,
animAngle: 36,
frameInt: 0.25
};
```
---
### 2. 修改 `renderDOM` 方法
在「Rotation Animation」的切換按鈕下方新增一個「Element Diff Algorithm」的 HTML 切換控制項(僅在 `main` 模式下)。
```javascript
// Locate inside renderDOM() -> main mode section under Animation heading
<h2>Animation</h2>
<div class="ejsolid-control-group">
<label><input type="checkbox" class="ejs-anim-toggle" /> Rotation Animation</label>
<label><input type="checkbox" class="ejs-element-diff-toggle" /> Element Diff Algorithm</label>
<div class="ejsolid-row">
<span>Duration (s)</span>
```
---
### 3. 修改 `cacheDOM` 方法
快取新加入的 UI 按鈕節點。
```javascript
// Locate inside cacheDOM()
this.dom = {
fileInput: this.container.querySelector('.ejs-file-input'),
uploadBtn: this.container.querySelector('.ejs-upload-trigger'),
perspective: this.container.querySelector('.ejs-perspective'),
testFrontBtn: this.container.querySelector('.ejs-test-front-btn'),
resetTestBtn: this.container.querySelector('.ejs-reset-test-btn'),
testResults: this.container.querySelector('.ejs-test-results'),
opacityRange: this.container.querySelector('.ejs-opacity-range'),
opacityNum: this.container.querySelector('.ejs-opacity-num'),
strokeWidthRange: this.container.querySelector('.ejs-stroke-width-range'),
strokeWidthNum: this.container.querySelector('.ejs-stroke-width-num'),
strokeColorHex: this.container.querySelector('.ejs-stroke-color-hex'),
strokeColorPicker: this.container.querySelector('.ejs-stroke-color-picker'),
animToggle: this.container.querySelector('.ejs-anim-toggle'),
elementDiffToggle: this.container.querySelector('.ejs-element-diff-toggle'), // New cached element
animTimeRange: this.container.querySelector('.ejs-anim-time-range'),
```
---
### 4. 修改 `bindEvents` 方法
為新按鈕綁定事件,以便在切換狀態時同步配置並重新調用 `this.draw()` 重繪。
```javascript
// Locate inside bindEvents() near the animToggle binding
if (this.dom.animToggle) {
this.dom.animToggle.onchange = e => {
this.config.animate = e.target.checked;
if (this.dom.miniAnim) this.dom.miniAnim.classList.toggle('active', this.config.animate);
this.draw();
};
}
if (this.dom.elementDiffToggle) {
this.dom.elementDiffToggle.onchange = e => {
this.config.elementDiff = e.target.checked;
this.draw();
};
}
```
---
### 5. 修改 `syncDOM` 方法
確保匯入 JSON 組態數據時,按鈕的打勾狀態可以被正確同步。
```javascript
// Locate inside syncDOM()
if (this.dom.perspective) this.dom.perspective.checked = this.config.perspective;
if (this.dom.animToggle) this.dom.animToggle.checked = this.config.animate;
if (this.dom.elementDiffToggle) this.dom.elementDiffToggle.checked = this.config.elementDiff; // New synchronization
if (this.dom.animTime) this.dom.animTime.value = this.config.animTime;
```
---
### 6. 修改 `generateSvg` 方法中的核心動畫追蹤與計算邏輯
這是本次修改的核心。我們調整 `splitOpacity` 的判斷條件,使 `elementDiff` 開啟時強制走雙圖層動畫分支;並在隨後的旋轉每幀計算中,利用程式算法(`check2DOverlap` 與 `checkEdgeNormalRelation`)即時判定**正面干涉**,來對 `front` 圖層中判定為前的元件進行動態複製(最少複製原則)以及控制 `visibility`。
以下是完整的 `generateSvg` 中間段落程式碼重構:
```javascript
// Replace the basic sort with advanced topological sort
polys = this.sortPolygonsTopological(polys);
// Call render test results if test mode is active
if (this.state.testMode) {
this.renderTestResults(polys.filter(p => p.isFront));
}
const doAnim = this.config.animate;
// Requirement 1: If elementDiff is enabled, always split into front and back layers regardless of current opacity
const splitOpacity = doAnim && (op < 1 || this.config.elementDiff);
const animPolysData = [];
if (doAnim) {
const duration = this.config.animTime;
const frameInterval = this.config.frameInt;
const totalFrames = Math.max(1, Math.round(duration / frameInterval));
const baseAngle = (this.config.animAngle * Math.PI) / 180;
// Extract bounding box centers for continuous rotation computations
const xs = vertices.map(v => v[0]), ys = vertices.map(v => v[1]), zs = vertices.map(v => v[2]);
const minX = Math.min(...xs), maxX = Math.max(...xs), cx = (minX + maxX) / 2;
const minY = Math.min(...ys), maxY = Math.max(...ys), cy = (minY + maxY) / 2;
const minZ = Math.min(...zs), maxZ = Math.max(...zs), cz = (minZ + maxZ) / 2;
// Pre-populate structural descriptors for animation parameters maps tracking
polys.forEach((p, originalOrderIdx) => {
animPolysData.push({
id: p.id,
sides: p.sides,
fIdx: p.fIdx,
isFront: p.isFront,
originalOrderIdx: originalOrderIdx, // Track its initial sequential position in polys
pointsValues: [],
sets: [],
frontSets: [],
backSets: [],
lastCp: 0,
// Requirement 2: Track dynamic visibility modifications for front layer under Element Diff mode
elementDiffSets: []
});
});
// Track interference and relation matrix over time for Element Diff algorithm
// Keyed by front polygon IDs that are determined to be 'in front' during a specific interference change
const elementDiffClones = {};
for (let i = 0; i <= totalFrames; i++) {
const angle = (baseAngle * i) / totalFrames;
const cosA = Math.cos(angle), sinA = Math.sin(angle);
const rAnim = [cosA, 0, sinA, 0, 1, 0, -sinA, 0, cosA];
const curRot = [
rAnim[0] * this.state.rotMatrix[0] + rAnim[1] * this.state.rotMatrix[3] + rAnim[2] * this.state.rotMatrix[6],
rAnim[0] * this.state.rotMatrix[1] + rAnim[1] * this.state.rotMatrix[4] + rAnim[2] * this.state.rotMatrix[7],
rAnim[0] * this.state.rotMatrix[2] + rAnim[1] * this.state.rotMatrix[5] + rAnim[2] * this.state.rotMatrix[8],
rAnim[3] * this.state.rotMatrix[0] + rAnim[4] * this.state.rotMatrix[3] + rAnim[5] * this.state.rotMatrix[6],
rAnim[3] * this.state.rotMatrix[1] + rAnim[4] * this.state.rotMatrix[4] + rAnim[5] * this.state.rotMatrix[7],
rAnim[3] * this.state.rotMatrix[2] + rAnim[4] * this.state.rotMatrix[5] + rAnim[5] * this.state.rotMatrix[8],
rAnim[6] * this.state.rotMatrix[0] + rAnim[7] * this.state.rotMatrix[3] + rAnim[8] * this.state.rotMatrix[6],
rAnim[6] * this.state.rotMatrix[1] + rAnim[7] * this.state.rotMatrix[4] + rAnim[8] * this.state.rotMatrix[7],
rAnim[6] * this.state.rotMatrix[2] + rAnim[7] * this.state.rotMatrix[5] + rAnim[8] * this.state.rotMatrix[8]
];
// Evaluate projected frame vertices positions structures array maps
const frameProjected = vertices.map(v => {
const x = v[0] - cx, y = v[1] - cy, z = v[2] - cz;
const rx = x * curRot[0] + y * curRot[1] + z * curRot[2];
const ry = x * curRot[3] + y * curRot[4] + z * curRot[5];
const rz = x * curRot[6] + y * curRot[7] + z * curRot[8];
const f = this.config.perspective ? 4 / (4 - rz) : 1;
return { x: rx * scale * f + offset, y: -ry * scale * f + offset };
});
// 3D calculation loop for dynamic geometry components fields parameters
const currentFramePolys = animPolysData.map(animP => {
const pts = animP.fIdx.map(idx => frameProjected[idx]);
let cp = 0;
for (let j = 0; j < pts.length; j++) {
const next = (j + 1) % pts.length;
cp += pts[j].x * pts[next].y - pts[next].x * pts[j].y;
}
// Reconstruct temporary 3D context elements to run algorithmic interference check
const pts3D = animP.fIdx.map(idx => {
const v = vertices[idx];
const x = v[0] - cx, y = v[1] - cy, z = v[2] - cz;
return {
x: x * curRot[0] + y * curRot[1] + z * curRot[2],
y: x * curRot[3] + y * curRot[4] + z * curRot[5],
z: x * curRot[6] + y * curRot[7] + z * curRot[8]
};
});
let normal3D = { x: 0, y: 0, z: 0 };
if (pts3D.length >= 3) {
const v1 = { x: pts3D[1].x - pts3D[0].x, y: pts3D[1].y - pts3D[0].y, z: pts3D[1].z - pts3D[0].z };
const v2 = { x: pts3D[2].x - pts3D[0].x, y: pts3D[2].y - pts3D[0].y, z: pts3D[2].z - pts3D[0].z };
normal3D = { x: v1.y * v2.z - v1.z * v2.y, y: v1.z * v2.x - v1.x * v2.z, z: v1.x * v2.y - v1.y * v2.x };
const len = Math.sqrt(normal3D.x * normal3D.x + normal3D.y * normal3D.y + normal3D.z * normal3D.z);
if (len > 0) { normal3D.x /= len; normal3D.y /= len; normal3D.z /= len; }
}
return {
animP: animP,
pts: pts,
pts3D: pts3D,
normal3D: normal3D,
isFront: cp < 0,
avgZ: pts3D.reduce((s, p) => s + p.z, 0) / pts3D.length,
id: animP.id
};
});
// Requirement 3: Run programmatic detection of front faces interference on each frame
if (this.config.elementDiff && i < totalFrames) {
const frontOnFrame = currentFramePolys.filter(p => p.isFront);
const exactTime = (i * frameInterval).toFixed(3);
for (let a = 0; a < frontOnFrame.length; a++) {
for (let b = a + 1; b < frontOnFrame.length; b++) {
const faceA = frontOnFrame[a];
const faceB = frontOnFrame[b];
// Check if they interfere in 2D projection space
if (this.check2DOverlap(faceA.pts, faceB.pts)) {
const isAInFront = this.checkEdgeNormalRelation(faceA, faceB);
if (isAInFront !== null) {
const frontFace = isAInFront ? faceA : faceB;
const behindFace = isAInFront ? faceB : faceA;
// Minimum copy mechanism: Only if the calculated rendering layer sequence
// violates their natural original template indices order layout
if (frontFace.animP.originalOrderIdx < behindFace.animP.originalOrderIdx) {
const polyId = frontFace.id;
// Initialize the visibility tracking configuration stack for this clone instance if needed
if (!elementDiffClones[polyId]) {
elementDiffClones[polyId] = {
animP: frontFace.animP,
// Initialize original element as visible at start
originalVis: [{ time: "0.000", visible: true }],
// Stack containing lists of set operations scheduled for subsequent clones sequential stacks
clones: []
};
}
const track = elementDiffClones[polyId];
// Add new cloned element copy instance into list representation
track.clones.push({
time: exactTime,
insertAfterIdx: behindFace.animP.originalOrderIdx
});
// Update visibility timings: hide the front original template element, show clone element
track.originalVis.push({ time: exactTime, visible: false });
// Ensure that visibility changes back to default once interference clears out or shifts
// (Will be handled at the end of the frame scanning processing timeline loop)
track.lastInterferenceFrame = i;
}
}
}
}
}
// Scan all tracked elements to restore original visibility if interference criteria no longer applies
Object.keys(elementDiffClones).forEach(polyId => {
const track = elementDiffClones[polyId];
if (track.lastInterferenceFrame === i) {
// Ensure visibility is restored for the subsequent frame step interval
const nextTime = ((i + 1) * frameInterval).toFixed(3);
track.originalVis.push({ time: nextTime, visible: true });
}
});
}
// Apply string point compilation tracking arrays calculations
currentFramePolys.forEach(item => {
const pStr = item.pts.map(pt => `${pt.x.toFixed(2)},${pt.y.toFixed(2)}`).join(' ');
item.animP.pointsValues.push(pStr);
// Standard front/back visibility conversion transition hooks checks
const cp = item.isFront ? -1 : 1;
if (i > 0) {
if (item.animP.lastCp === 0) item.animP.lastCp = cp;
if ((item.animP.lastCp < 0 && cp > 0) || (item.animP.lastCp > 0 && cp < 0)) {
const visStr = cp < 0 ? 'visible' : 'hidden';
const invVisStr = cp < 0 ? 'hidden' : 'visible';
let fraction = item.animP.lastCp !== cp ? Math.abs(item.animP.lastCp / (item.animP.lastCp - cp)) : 0.5;
let exactTime = ((i - 1 + fraction) * frameInterval).toFixed(3);
item.animP.sets.push(`<set attributeName="visibility" to="${visStr}" begin="start.begin+${exactTime}s" />`);
item.animP.frontSets.push(`<set attributeName="visibility" to="${visStr}" begin="start.begin+${exactTime}s" />`);
item.animP.backSets.push(`<set attributeName="visibility" to="${invVisStr}" begin="start.begin+${exactTime}s" />`);
}
item.animP.lastCp = cp;
}
});
}
// Compile and write generated `<set>` structures tags for compiled Element Diff clones configuration metrics
Object.keys(elementDiffClones).forEach(polyId => {
const track = elementDiffClones[polyId];
// 1. Build visibility tags timeline for the original parent element use instance
track.originalVis.forEach(state => {
track.animP.elementDiffSets.push(
`<set attributeName="visibility" to="${state.visible ? 'visible' : 'hidden'}" begin="start.begin+${state.time}s" />`
);
});
// 2. Map and append the dynamic visibility transitions targets for compiled auxiliary cloned layers nodes
track.clones.forEach((cloneData, index) => {
cloneData.setTags = [
`<set attributeName="visibility" to="hidden" begin="start.begin" />`,
`<set attributeName="visibility" to="visible" begin="start.begin+${cloneData.time}s" />`
];
// Hide it again when the next frame interval takes over
const endTime = (parseFloat(cloneData.time) + frameInterval).toFixed(3);
cloneData.setTags.push(`<set attributeName="visibility" to="hidden" begin="start.begin+${endTime}s" />`);
});
});
// Save references into class states definitions to use during DOM construction processing blocks
this.state.elementDiffClones = elementDiffClones;
}
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}" preserveAspectRatio="xMidYMid meet">\n`;
// Hide original geometry group if we are splitting by opacity
const groupAttr = splitOpacity ? ` visibility="hidden"` : ``;
svg += ` <g stroke-linejoin="round"${groupAttr}>\n`;
// Render primary geometry templates loop blocks
polys.forEach((p, i) => {
const pStr = p.pts.map(pt => `${pt.x.toFixed(2)},${pt.y.toFixed(2)}`).join(' ');
let fill = this.config.defaultColors[p.sides] || '#ccc';
if (this.state.testMode && p.isFront && this.state.testColors[p.id]) {
fill = this.state.testColors[p.id];
}
const polyIdAttr = splitOpacity ? ` id="${p.id}"` : ``;
if (doAnim) {
const animP = animPolysData[i];
const valuesStr = animP.pointsValues.join(';');
const durationStr = this.config.animTime.toFixed(1);
svg += ` <polygon${polyIdAttr} points="${pStr}" fill="${fill}" fill-opacity="${op}" stroke="${strokeColor}" stroke-width="${sw}">\n`;
svg += ` <animate attributeName="points" values="${valuesStr}" dur="${durationStr}s" begin="start.begin" repeatCount="indefinite" />\n`;
if (!splitOpacity) {
animP.sets.forEach(setTag => { svg += ` ${setTag}\n`; });
}
svg += ` </polygon>\n`;
} else {
svg += ` <polygon${polyIdAttr} points="${pStr}" fill="${fill}" fill-opacity="${op}" stroke="${strokeColor}" stroke-width="${sw}" />\n`;
}
});
svg += ` </g>\n`;
// Re-use polygons logic block for split layer rendering setups
if (splitOpacity) {
// 1. Render standard structural back mapping container nodes
svg += ` <g id="back-layer" stroke-linejoin="round">\n`;
polys.forEach((p, i) => {
if (doAnim) {
const animP = animPolysData[i];
const startVis = p.isFront ? 'hidden' : 'visible';
svg += ` <use href="#${p.id}" visibility="${startVis}">\n`;
animP.backSets.forEach(setTag => { svg += ` ${setTag}\n`; });
svg += ` </use>\n`;
} else {
if (!p.isFront) svg += ` <use href="#${p.id}" />\n`;
}
});
svg += ` </g>\n`;
// 2. Render front mapping container layer with Element Diff injection enhancements support
svg += ` <g id="front-layer" stroke-linejoin="round">\n`;
// Structure definitions data maps references helper to track array insertion pipelines
const frontLayerElements = [];
polys.forEach((p, i) => {
let elementStr = "";
if (doAnim) {
const animP = animPolysData[i];
const startVis = p.isFront ? 'visible' : 'hidden';
elementStr += ` <use href="#${p.id}" visibility="${startVis}">\n`;
// Inject conditional animation timeline changes sets tags depending on mode
if (this.config.elementDiff && this.state.elementDiffClones && this.state.elementDiffClones[p.id]) {
this.state.elementDiffClones[p.id].animP.elementDiffSets.forEach(setTag => { elementStr += ` ${setTag}\n`; });
} else {
animP.frontSets.forEach(setTag => { elementStr += ` ${setTag}\n`; });
}
elementStr += ` </use>\n`;
} else {
if (p.isFront) elementStr += ` <use href="#${p.id}" />\n`;
}
frontLayerElements.push({
originalOrderIdx: i,
id: p.id,
payload: elementStr
});
});
// Insert clones to front layer array under Element Diff minimum copying criteria rules
if (doAnim && this.config.elementDiff && this.state.elementDiffClones) {
Object.keys(this.state.elementDiffClones).forEach(polyId => {
const track = this.state.elementDiffClones[polyId];
track.clones.forEach(cloneInstance => {
let cloneStr = ` <use href="#${polyId}">\n`;
cloneInstance.setTags.forEach(tag => { cloneStr += ` ${tag}\n`; });
cloneStr += ` </use>\n`;
// Append into sequential output execution timeline tracking arrays structure
frontLayerElements.push({
originalOrderIdx: cloneInstance.insertAfterIdx, // Force placement behind the specific target it interfered with
id: polyId + "_clone",
payload: cloneStr,
isClone: true
});
});
});
// Sort execution sequences to ensure the appended clones are positioned correctly behind their respective targets
frontLayerElements.sort((a, b) => {
if (a.originalOrderIdx !== b.originalOrderIdx) {
return a.originalOrderIdx - b.originalOrderIdx;
}
// If referencing the same index location anchor, place the clone first (so it renders behind the base template)
if (a.isClone && !b.isClone) return -1;
if (!a.isClone && b.isClone) return 1;
return 0;
});
}
// Output structural string tags content entries into main wrapper stream buffer arrays
frontLayerElements.forEach(el => {
svg += el.payload;
});
svg += ` </g>\n`;
}
```
當在旋轉時,檢測到有正面發生干涉,則在front圖層中,把判定結果為前的use,複製一個相同的use到後面並加set visibility=hidden,前面相同的use則set visibility=visible。