三,當製作旋轉動畫時,如果ejs-opacity-range等於1,保持現在的方式不變。如果小於1,則把所有polygon加以「ejs數字」為前綴的id,把polygon的父元素g加上visibility=hidden。在svg下方新增g#back和g#front,都加進全部use[href=polygon的id」。正面則在front加set[visibility=visible],back加set-hidden,背面相反。
代码: 全选
Here are the specific modifications required to implement your requested improvements. All explanations, code, and comments are provided in English.
### 1. Update State & Internalize OBJ Data
Remove the `<textarea>` dependency and cache the parsed model so we don't recalculate static vertices and faces unless the raw data actually changes.
**In `constructor(containerSelector)`:**
```javascript
// Update state initialization
this.state = {
rotMatrix: [1, 0, 0, 0, 1, 0, 0, 0, 1],
lastSvg: '',
isDragging: false,
lastX: 0,
lastY: 0,
// Store raw data internally instead of relying on a textarea
objData: `v 0.0 0.0 1.077364\nv 0.7442063 0.0 0.7790187\nv 0.3123013 0.6755079 0.7790187\nv -0.482096 0.5669449 0.7790187\nv -0.7169181 -0.1996786 0.7790187\nv -0.1196038 -0.7345325 0.7790187\nv 0.6246025 -0.7345325 0.4806734\nv 1.056508 -0.1996786 0.06806912\nv 0.8867128 0.5669449 0.2302762\nv 0.2621103 1.042774 0.06806912\nv -0.532287 0.9342111 0.06806912\nv -1.006317 0.3082417 0.2302762\nv -0.7020817 -0.784071 0.2302762\nv 0.02728827 -1.074865 0.06806912\nv 0.6667271 -0.784071 -0.3184664\nv 0.8216855 -0.09111555 -0.6908285\nv 0.6518908 0.6755079 -0.5286215\nv -0.1196038 0.8751866 -0.6168117\nv -0.8092336 0.4758293 -0.5286215\nv -0.9914803 -0.2761507 -0.3184664\nv -0.4467414 -0.825648 -0.5286215\nv 0.1926974 -0.5348539 -0.915157\nv 0.1846311 0.2587032 -1.029416\nv -0.5049987 -0.1406541 -0.9412258\nf 1 2 3\nf 1 3 4\nf 1 4 5\nf 1 5 6\nf 2 7 8\nf 2 8 9\nf 2 9 3\nf 3 9 10\nf 4 11 12\nf 4 12 5\nf 5 13 6\nf 6 13 14\nf 6 14 7\nf 7 14 15\nf 7 15 8\nf 8 15 16\nf 9 17 10\nf 10 17 18\nf 10 18 11\nf 11 18 19\nf 11 19 12\nf 12 19 20\nf 13 20 21\nf 13 21 14\nf 15 22 16\nf 16 22 23\nf 16 23 17\nf 17 23 18\nf 19 24 20\nf 20 24 21\nf 21 24 22\nf 22 24 23\nf 1 6 7 2\nf 3 10 11 4\nf 5 12 20 13\nf 8 16 17 9\nf 14 21 22 15\nf 18 23 24 19`,
parsedModel: null // Cache for parsed vertices, faces, cx, cy, cz, and maxRadius
};
```
### 2. Apply Modern Toggle Checkbox Styles & Remove Textarea
Inject new CSS to make the checkboxes look like sleek toggle switches, and modify the DOM template.
**In `injectCSS()`:**
```javascript
// Add this to the end of your styleSheet.innerText string:
`
.ejsolid-control-group label { display: flex; align-items: center; gap: 10px; cursor: pointer; user-select: none; }
.ejsolid-control-group input[type="checkbox"] { appearance: none; -webkit-appearance: none; width: 44px; height: 24px; background: #e0e0e0; border-radius: 24px; position: relative; cursor: pointer; outline: none; transition: background 0.3s ease; margin: 0; flex-shrink: 0; }
.ejsolid-control-group input[type="checkbox"]::after { content: ''; position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; background: white; border-radius: 50%; transition: transform 0.3s ease; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
.ejsolid-control-group input[type="checkbox"]:checked { background: #3a86ff; }
.ejsolid-control-group input[type="checkbox"]:checked::after { transform: translateX(20px); }
`;
```
**In `renderDOM()`:**
```javascript
// Replace the "Raw Data" control group with this (removed the textarea):
` <h2>Raw Data (${this.mode})</h2>
<div class="ejsolid-control-group">
<input type="file" class="ejs-file-input" accept=".obj" style="display: none" />
<button type="button" class="ejs-upload-trigger">Choose OBJ File</button>
</div>`
```
**In `cacheDOM()`:**
```javascript
// Remove the objInput reference
// delete this.dom.objInput;
```
**In `bindEvents()`:**
```javascript
// Update the file input logic to write to internal state and invalidate cache
this.dom.fileInput.onchange = e => {
const reader = new FileReader();
reader.onload = ev => {
this.state.objData = ev.target.result;
this.state.parsedModel = null; // Invalidate cached parsing
this.draw();
};
reader.readAsText(e.target.files[0]);
};
// Replace the old inputs update array at the bottom of bindEvents:
// [this.dom.objInput, this.dom.perspective].forEach(...)
// with:
this.dom.perspective.oninput = () => this.draw();
```
### 3. Update Drawing Logic for Optimization and Alpha/Opacity Sorting
Replace the entire `draw()` method to include data caching and the complex SVG restructuring when opacity is $< 1$.
**Replace the `draw()` method:**
```javascript
draw() {
// 1. Parse and cache the model data if not already done
if (!this.state.parsedModel) {
const vertices = [], faces = [];
this.state.objData.split('\n').forEach(line => {
const parts = line.trim().split(/\s+/);
if (parts[0] === 'v') vertices.push(parts.slice(1, 4).map(Number));
if (parts[0] === 'f') faces.push(parts.slice(1).map(p => parseInt(p.split('/')[0]) - 1));
});
if (!vertices.length) return;
let cx = 0, cy = 0, cz = 0;
vertices.forEach(v => { cx += v[0]; cy += v[1]; cz += v[2]; });
cx /= vertices.length; cy /= vertices.length; cz /= vertices.length;
let maxRadiusSq = 0;
vertices.forEach(v => {
const dx = v[0] - cx, dy = v[1] - cy, dz = v[2] - cz;
maxRadiusSq = Math.max(maxRadiusSq, dx * dx + dy * dy + dz * dz);
});
const maxRadius = Math.sqrt(maxRadiusSq) || 1;
this.state.parsedModel = { vertices, faces, cx, cy, cz, maxRadius };
}
const { vertices, faces, cx, cy, cz, maxRadius } = this.state.parsedModel;
// Fetch dynamic sizes from DOM
const size = parseInt(this.dom.canvasNum.value) || 480;
const contentSize = parseInt(this.dom.contentNum.value) || 360;
const scale = contentSize / (maxRadius * 2);
const offset = size / 2;
const op = parseFloat(this.dom.opacityNum.value);
const sw = parseFloat(this.dom.strokeWidthNum.value);
const strokeColor = this.dom.strokeColorPicker.value;
// Project vertices for the current static frame
const projected = vertices.map(v => {
const x = v[0] - cx, y = v[1] - cy, z = v[2] - cz;
const rx = x * this.state.rotMatrix[0] + y * this.state.rotMatrix[1] + z * this.state.rotMatrix[2];
const ry = x * this.state.rotMatrix[3] + y * this.state.rotMatrix[4] + z * this.state.rotMatrix[5];
const rz = x * this.state.rotMatrix[6] + y * this.state.rotMatrix[7] + z * this.state.rotMatrix[8];
const f = this.dom.perspective.checked ? 4 / (4 - rz) : 1;
return { x: rx * scale * f + offset, y: -ry * scale * f + offset, z: rz };
});
const polys = [];
faces.forEach((fIdx, idx) => {
const pts = fIdx.map(i => projected[i]);
polys.push({ pts, avgZ: pts.reduce((s, p) => s + p.z, 0) / pts.length, sides: pts.length, fIdx, id: `ejs-${idx}` });
});
polys.sort((a, b) => a.avgZ - b.avgZ);
const doAnim = this.dom.animToggle.checked;
const splitOpacity = doAnim && op < 1; // Trigger new logic if animated AND transparent
const animPolysData = [];
if (doAnim) {
const duration = parseFloat(this.dom.animTime.value) || 7.0;
const angleDeg = parseInt(this.dom.animAngle.value) || 0;
const frameInterval = parseFloat(this.dom.frameInt.value) || 0.25;
const totalFrames = Math.round(duration / frameInterval);
const alphaRad = (angleDeg * Math.PI) / 180;
const axisX = Math.sin(alphaRad), axisY = Math.cos(alphaRad);
let startAssigned = false;
polys.forEach(p => {
animPolysData.push({
pointsValues: [],
sets: [], // Standard visibility tags
frontSets: [], // Visibility tags for <g id="front">
backSets: [], // Visibility tags for <g id="back">
lastCp: 0,
currentVis: false,
fIdx: p.fIdx,
id: p.id
});
});
for (let i = 0; i <= totalFrames; i++) {
const angle = (i / totalFrames) * 2 * Math.PI;
const c = Math.cos(angle), s = Math.sin(angle), t = 1 - c;
const rAnim = [
t * axisX * axisX + c, t * axisX * axisY, s * axisY,
t * axisX * axisY, t * axisY * axisY + c, -s * axisX,
-s * axisY, s * axisX, c
];
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]
];
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.dom.perspective.checked ? 4 / (4 - rz) : 1;
return { x: rx * scale * f + offset, y: -ry * scale * f + offset };
});
animPolysData.forEach(animP => {
const pts = animP.fIdx.map(idx => frameProjected[idx]);
let cp = 0;
for (let j = 0; j < pts.length; j++) {
let k = (j + 1) % pts.length;
cp += pts[j].x * pts[k].y - pts[k].x * pts[j].y;
}
const isFront = cp < 0;
animP.pointsValues.push(pts.map(pt => `${pt.x.toFixed(2)},${pt.y.toFixed(2)}`).join(' '));
if (i === 0) {
animP.currentVis = isFront;
animP.lastCp = cp;
const visStr = isFront ? 'visible' : 'hidden';
const invVisStr = isFront ? 'hidden' : 'visible';
if (!startAssigned) {
// Master timer for standard implementation or the front group
animP.sets.push(`<set id="start" attributeName="visibility" to="${visStr}" begin="0;start.begin+${duration.toFixed(1)}s" />`);
animP.frontSets.push(`<set id="start" attributeName="visibility" to="${visStr}" begin="0;start.begin+${duration.toFixed(1)}s" />`);
animP.backSets.push(`<set attributeName="visibility" to="${invVisStr}" begin="0;start.begin+${duration.toFixed(1)}s" />`);
startAssigned = true;
} else {
animP.sets.push(`<set attributeName="visibility" to="${visStr}" begin="start.begin" />`);
animP.frontSets.push(`<set attributeName="visibility" to="${visStr}" begin="start.begin" />`);
animP.backSets.push(`<set attributeName="visibility" to="${invVisStr}" begin="start.begin" />`);
}
} else {
if (isFront !== animP.currentVis) {
animP.currentVis = isFront;
const visStr = isFront ? 'visible' : 'hidden';
const invVisStr = isFront ? 'hidden' : 'visible';
let fraction = Math.abs(animP.lastCp - cp) > 1e-6 ? Math.abs(animP.lastCp / (animP.lastCp - cp)) : 0.5;
let exactTime = ((i - 1 + fraction) * frameInterval).toFixed(3);
animP.sets.push(`<set attributeName="visibility" to="${visStr}" begin="start.begin+${exactTime}s" />`);
animP.frontSets.push(`<set attributeName="visibility" to="${visStr}" begin="start.begin+${exactTime}s" />`);
animP.backSets.push(`<set attributeName="visibility" to="${invVisStr}" begin="start.begin+${exactTime}s" />`);
}
animP.lastCp = cp;
}
});
}
}
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`;
polys.forEach((p, i) => {
const pStr = p.pts.map(pt => `${pt.x.toFixed(2)},${pt.y.toFixed(2)}`).join(' ');
const fill = this.container.querySelector(`.hex-${p.sides}`)?.value || '#ccc';
const polyIdAttr = splitOpacity ? ` id="${p.id}"` : ``;
if (doAnim) {
const animP = animPolysData[i];
const valuesStr = animP.pointsValues.join(';');
const durationStr = (parseFloat(this.dom.animTime.value) || 7.0).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 not splitting opacity, embed the set tags directly
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 to ensure backfaces are rendered behind frontfaces
if (splitOpacity) {
let backSvg = ` <g id="back">\n`;
let frontSvg = ` <g id="front">\n`;
polys.forEach((p, i) => {
const animP = animPolysData[i];
backSvg += ` <use href="#${animP.id}">\n`;
animP.backSets.forEach(setTag => { backSvg += ` ${setTag}\n`; });
backSvg += ` </use>\n`;
frontSvg += ` <use href="#${animP.id}">\n`;
animP.frontSets.forEach(setTag => { frontSvg += ` ${setTag}\n`; });
frontSvg += ` </use>\n`;
});
backSvg += ` </g>\n`;
frontSvg += ` </g>\n`;
svg += backSvg + frontSvg;
}
svg += `</svg>`;
this.dom.svgContainer.innerHTML = svg;
this.state.lastSvg = svg;
this.dom.fileSize.textContent = `File Size: ${new Blob([svg]).size} Bytes`;
}
```