對ejsolid181.js改進:
在增加動畫時,處於背面的面將set visibility=hidden,回到正面時再設回visible。正面與背面交換的時機,是一個面的所有端點都共線。
添加以下輸入框作為設置:
滑動與數字輸入框During Time(s),始終保留一位小數,默認為7.0;
滑動與數字輸入框Rotate Angle,整數,範圍是-180到180,以右方為0度,逆時針為正;
滑動與數字輸入框Set round to n decimal places,整數,set visibility的時間保留多少位小數,默認3;
滑動與數字輸入框Animate每n秒一幀,保留兩位小數,默認為0.05。
只需給出所要修改的地方,所有的代碼和注釋都要使用英文。
gemini pro:
代码: 全选
Here are the exact modifications needed to implement the requested animation features, including the new control inputs, visibility toggles based on collinear points, and the preservation of the initial `points` attribute.
### 1. Update `renderDOM()`
Replace the `<h2>Animation</h2>` block in the `this.container.innerHTML` assignment with the following HTML:
```javascript
// ... [Keep the previous HTML up to <h2>Face Colors</h2>] ...
<h2>Animation</h2>
<div class="ejsolid-control-group">
<label><input type="checkbox" class="ejs-anim-toggle" /> Rotation Animation</label>
<div class="ejsolid-row">
<span>Duration (s)</span>
<input type="range" class="ejs-anim-time-range" min="0.1" max="48.0" step="0.1" value="7.0" />
<input type="number" class="ejs-anim-time" min="0.1" max="48.0" step="0.1" value="7.0" style="width: 50px" />
</div>
<div class="ejsolid-row">
<span>Rotate Angle (°)</span>
<input type="range" class="ejs-anim-angle-range" min="-180" max="180" step="1" value="0" />
<input type="number" class="ejs-anim-angle" min="-180" max="180" step="1" value="0" style="width: 50px" />
</div>
<div class="ejsolid-row">
<span>Round decimals</span>
<input type="range" class="ejs-time-prec-range" min="0" max="6" step="1" value="3" />
<input type="number" class="ejs-time-prec" min="0" max="6" step="1" value="3" style="width: 50px" />
</div>
<div class="ejsolid-row">
<span>Frame interval (s)</span>
<input type="range" class="ejs-frame-int-range" min="0.01" max="1.00" step="0.01" value="0.05" />
<input type="number" class="ejs-frame-int" min="0.01" max="1.00" step="0.01" value="0.05" style="width: 50px" />
</div>
</div>
</div>
// ... [Keep the right panel HTML] ...
```
### 2. Update `cacheDOM()`
Add the new input elements to the `this.dom` object:
```javascript
cacheDOM() {
this.dom = {
// ... [Keep existing cached elements] ...
animToggle: this.container.querySelector('.ejs-anim-toggle'),
animTimeRange: this.container.querySelector('.ejs-anim-time-range'),
animTime: this.container.querySelector('.ejs-anim-time'),
animAngleRange: this.container.querySelector('.ejs-anim-angle-range'),
animAngle: this.container.querySelector('.ejs-anim-angle'),
timePrecRange: this.container.querySelector('.ejs-time-prec-range'),
timePrec: this.container.querySelector('.ejs-time-prec'),
frameIntRange: this.container.querySelector('.ejs-frame-int-range'),
frameInt: this.container.querySelector('.ejs-frame-int'),
svgContainer: this.container.querySelector('#ejs-svg-container'),
// ... [Keep the rest] ...
};
}
```
### 3. Update `bindEvents()`
Add synchronization for the new range and number inputs, and trigger the redraw:
```javascript
bindEvents() {
// ... [Keep existing events for colors, file upload, etc.] ...
// Helper to sync range and number inputs and redraw
const syncInputs = (rangeNode, numNode, isFloat, decimals) => {
const update = () => this.draw();
rangeNode.oninput = () => {
numNode.value = isFloat ? parseFloat(rangeNode.value).toFixed(decimals) : rangeNode.value;
update();
};
numNode.oninput = () => {
rangeNode.value = numNode.value;
update();
};
numNode.onblur = () => {
let val = parseFloat(numNode.value);
if (isNaN(val)) val = parseFloat(rangeNode.value);
numNode.value = isFloat ? val.toFixed(decimals) : Math.round(val);
rangeNode.value = numNode.value;
update();
};
};
// Bind new animation inputs
syncInputs(this.dom.animTimeRange, this.dom.animTime, true, 1);
syncInputs(this.dom.animAngleRange, this.dom.animAngle, false, 0);
syncInputs(this.dom.timePrecRange, this.dom.timePrec, false, 0);
syncInputs(this.dom.frameIntRange, this.dom.frameInt, true, 2);
// ... [Keep the existing mouse interactions and other oninput events] ...
}
```
### 4. Update Animation Logic in `draw()`
Replace the section inside `draw()` that handles the animation and SVG string building with this updated code:
```javascript
// ... [Keep the existing polygon array projection and depth sort logic up to polys.sort(...)] ...
// Handle Animation Frames
const doAnim = this.dom.animToggle.checked;
const animPolysData = [];
if (doAnim) {
const duration = parseFloat(this.dom.animTime.value) || 7.0;
const angleDeg = parseInt(this.dom.animAngle.value) || 0;
const timePrec = parseInt(this.dom.timePrec.value) || 3;
const frameInterval = parseFloat(this.dom.frameInt.value) || 0.05;
const totalFrames = Math.round(duration / frameInterval);
// Calculate axis of rotation (0 is right, counter-clockwise is positive)
const alphaRad = (angleDeg * Math.PI) / 180;
const axisX = Math.cos(alphaRad);
const axisY = -Math.sin(alphaRad);
let startAssigned = false;
// Initialize animation data structure
polys.forEach(p => {
animPolysData.push({
pointsValues: [],
sets: [],
lastCp: 0,
currentVis: false,
fIdx: p.fIdx
});
});
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;
// Transform matrix for this specific animation frame
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
];
// Combine animation matrix with current state to start seamlessly
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 rx = v[0] * curRot[0] + v[1] * curRot[1] + v[2] * curRot[2];
const ry = v[0] * curRot[3] + v[1] * curRot[4] + v[2] * curRot[5];
const rz = v[0] * curRot[6] + v[1] * curRot[7] + v[2] * 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]);
// Calculate cross product for back-face culling check
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;
const ptsStr = pts.map(pt => `${pt.x.toFixed(2)},${pt.y.toFixed(2)}`).join(' ');
animP.pointsValues.push(ptsStr);
// Configure visibility <set> tags based on cross product switching (collinear moment)
if (i === 0) {
animP.currentVis = isFront;
animP.lastCp = cp;
const initialVisStr = isFront ? 'visible' : 'hidden';
if (!startAssigned) {
animP.sets.push(`<set id="start" attributeName="visibility" to="${initialVisStr}" begin="0;start.begin+${duration.toFixed(1)}s" />`);
startAssigned = true;
} else {
animP.sets.push(`<set attributeName="visibility" to="${initialVisStr}" begin="start.begin" />`);
}
} else {
if (isFront !== animP.currentVis) {
animP.currentVis = isFront;
const visValue = isFront ? 'visible' : 'hidden';
// Linear interpolation of the cross-product to find exact collinear time
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(timePrec);
animP.sets.push(`<set attributeName="visibility" to="${visValue}" 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}">\n`;
svg += ` <g stroke-linejoin="round">\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';
if (doAnim) {
const animP = animPolysData[i];
const valuesStr = animP.pointsValues.join(';');
const durationStr = (parseFloat(this.dom.animTime.value) || 7.0).toFixed(1);
// Retain original 'points' attribute as requested to start with current state
svg += ` <polygon points="${pStr}" fill="${fill}" fill-opacity="${op}" stroke="${this.dom.strokeColor.value}" stroke-width="${sw}">\n`;
svg += ` <animate attributeName="points" values="${valuesStr}" dur="${durationStr}s" begin="start.begin" repeatCount="indefinite" />\n`;
animP.sets.forEach(setTag => {
svg += ` ${setTag}\n`;
});
svg += ` </polygon>\n`;
} else {
svg += ` <polygon points="${pStr}" fill="${fill}" fill-opacity="${op}" stroke="${this.dom.strokeColor.value}" stroke-width="${sw}" />\n`;
}
});
svg += ` </g>\n</svg>`;
this.dom.svgContainer.innerHTML = svg;
// ... [Keep the final file size logic] ...
```