代码: 全选
### 未被調用的函數
- 位置:在 `// Calculate additive transform or opacity value for a given type` 之後的 `calculateAdditiveValue` 函數定義處。
- 更改內容:刪除整個函數,因為它未被任何地方調用。替換為空行或移除。
### 可整合的代碼
- 位置:在 `handleAllAppendTransformClick` 函數定義處,以及 `handleAllAppendOpacityClick` 函數定義處。
- 更改內容:將兩個函數整合成一個通用函數 `handleAllAppendAnimationClick(animateType)`,其中 `animateType` 為 'transform' 或 'opacity'。替換原兩個函數為:
```
// Handle click event for adding animation (transform or opacity)
function handleAllAppendAnimationClick(animateType) {
if (!etani_clone) return;
// Only work in tiles mode
if (currentSelectMode !== 'tiles') {
alert(`${animateType.charAt(0).toUpperCase() + animateType.slice(1)} can only be added in tiles mode`);
return;
}
const etdropUses = document.querySelectorAll('#etmain .etdrop use');
const etaniItemRights = document.querySelectorAll('.etaniItemRight');
const isRepeat = document.querySelector('.etaniModeRepeat.active');
etdropUses.forEach((originalUseElement, i) => {
const useId = originalUseElement.getAttribute('href').substring(1);
const itemRight = etaniItemRights[i];
if (!itemRight) return;
// Check if animation already exists
if (itemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`)) return;
let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return;
if (animateType === 'transform') {
// Get the original transform values from the #etmain SVG
const originalTransformString = originalUseElement.getAttribute('transform') || '';
const originalTransforms = parseTransform(originalTransformString);
const baseAnimate = (type, initialValue) => {
const animate = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
animate.setAttribute('attributeName', 'transform');
animate.setAttribute('attributeType', 'XML');
animate.setAttribute('type', type);
animate.setAttribute('values', initialValue);
animate.setAttribute('fill', isRepeat ? 'remove' : 'freeze');
if (isRepeat) animate.setAttribute('repeatCount', 'indefinite');
// Add additive="sum" for scale and rotate, but not for translate
if (type === 'scale' || type === 'rotate') {
animate.setAttribute('additive', 'sum');
}
return animate;
};
// Add all three animations directly to the <use> element
// Order: translate, scale, rotate
cloneUseElement.appendChild(baseAnimate('translate', originalTransforms.translate));
cloneUseElement.appendChild(baseAnimate('scale', originalTransforms.scale));
cloneUseElement.appendChild(baseAnimate('rotate', originalTransforms.rotate));
} else if (animateType === 'opacity') {
const animateOpacity = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
animateOpacity.setAttribute('attributeName', 'opacity');
animateOpacity.setAttribute('values', '1');
animateOpacity.setAttribute('fill', isRepeat ? 'remove' : 'freeze');
if (isRepeat) animateOpacity.setAttribute('repeatCount', 'indefinite');
cloneUseElement.appendChild(animateOpacity);
}
// --- Start of HTML control creation ---
const etaniAnimate = document.createElement('div');
etaniAnimate.className = 'etaniAnimate';
etaniAnimate.setAttribute('data-type', animateType);
const nameSpan = document.createElement('span');
nameSpan.className = 'etaniAnimateName';
nameSpan.textContent = animateType;
const durSpan = document.createElement('span');
durSpan.className = 'etaniAnimateDur';
durSpan.textContent = 'dur: 0s';
durSpan.addEventListener('click', () => {
const currentDur = parseFloat(durSpan.textContent.replace('dur: ', '').replace('s', ''));
const newDur = prompt('Enter duration in seconds:', currentDur);
if (newDur !== null && !isNaN(newDur) && newDur >= 0) {
let animates;
if (animateType === 'transform') {
animates = etani_clone.querySelectorAll(`.etdrop use[href="#${useId}"] animateTransform`);
} else if (animateType === 'opacity') {
animates = [etani_clone.querySelector(`.etdrop use[href="#${useId}"] animate[attributeName="opacity"]`)];
}
// Get current values count to determine if we need to add values
const currentValues = animates[0]?.getAttribute('values')?.split(';') || [];
const currentValuesCount = currentValues.length;
const isIntegerDur = Number.isInteger(currentDur);
const isIntegerNewDur = Number.isInteger(newDur);
// If dur is changing from an integer to a larger integer, add values
if (isIntegerDur && isIntegerNewDur && newDur > currentDur) {
const valuesToAdd = newDur - currentDur;
const originalUseElement = document.querySelector(`#etmain .etdrop use[href="#${useId}"]`);
if (originalUseElement) {
let originalTransforms;
let valueToAdd;
if (animateType === 'transform') {
const originalTransformString = originalUseElement.getAttribute('transform') || '';
originalTransforms = parseTransform(originalTransformString);
} else if (animateType === 'opacity') {
valueToAdd = '1';
}
animates.forEach(animate => {
const currentValueString = animate.getAttribute('values') || '';
if (animateType === 'transform') {
const type = animate.getAttribute('type').toLowerCase();
valueToAdd = type === 'translate' ? originalTransforms.translate :
type === 'scale' ? originalTransforms.scale :
type === 'rotate' ? originalTransforms.rotate : '';
}
let newValues = currentValueString ? currentValueString.split(';') : [];
for (let i = 0; i < valuesToAdd; i++) {
newValues.push(valueToAdd);
}
animate.setAttribute('values', newValues.join(';'));
});
// Update UI to add new etaniAVItems
const etaniAV = itemRight.querySelector(`.etaniAnimate[data-type="${animateType}"] .etaniAV`);
if (etaniAV) {
const existingItems = etaniAV.querySelectorAll('.etaniAVItem');
let existingLetters = Array.from(existingItems).map(item => item.textContent);
for (let i = 0; i < valuesToAdd; i++) {
const newAVItem = document.createElement('span');
newAVItem.className = 'etaniAVItem';
newAVItem.textContent = animateType === 'transform' ? findFirstMissingLetter(existingLetters) : '1';
if (animateType === 'transform') existingLetters.push(newAVItem.textContent);
newAVItem.addEventListener('click', (e) => handleAVItemClick(e, animateType));
etaniAV.appendChild(newAVItem);
}
}
}
}
animates.forEach(animate => {
if (newDur > 0) {
animate.setAttribute('dur', `${newDur}s`);
if (isRepeat) {
animate.removeAttribute('fill');
animate.setAttribute('repeatCount', 'indefinite');
} else {
animate.removeAttribute('repeatCount');
animate.setAttribute('fill', 'freeze');
}
} else {
animate.removeAttribute('dur');
animate.removeAttribute('fill');
animate.removeAttribute('repeatCount');
}
});
durSpan.textContent = `dur: ${newDur}s`;
updateEtaniResult();
}
});
const etaniAnimateAttr = document.createElement('div');
etaniAnimateAttr.className = 'etaniAnimateAttr';
etaniAnimateAttr.appendChild(nameSpan);
etaniAnimateAttr.appendChild(durSpan);
etaniAnimate.appendChild(etaniAnimateAttr);
const frSpan = document.createElement('span');
frSpan.className = 'etaniAnimateFR';
frSpan.textContent = document.querySelector('.etaniModeMixed.active') ? 'freeze' : 'repeat';
frSpan.style.display = document.querySelector('.etaniModeMixed.active') ? 'inline-block' : 'none';
frSpan.addEventListener('click', () => {
const currentValue = frSpan.textContent;
const newValue = currentValue === 'freeze' ? 'repeat' : 'freeze';
frSpan.textContent = newValue;
let animateElem;
if (animateType === 'transform') {
animateElem = etani_clone.querySelectorAll(`.etdrop use[href="#${useId}"] animateTransform`);
} else if (animateType === 'opacity') {
animateElem = [etani_clone.querySelector(`.etdrop use[href="#${useId}"] animate[attributeName="opacity"]`)];
}
animateElem.forEach(animate => {
if (animate.hasAttribute('fill') || animate.hasAttribute('repeatCount')) {
if (newValue === 'repeat') {
animate.removeAttribute('fill');
animate.setAttribute('repeatCount', 'indefinite');
} else {
animate.removeAttribute('repeatCount');
animate.setAttribute('fill', 'freeze');
}
}
});
updateEtaniResult();
});
etaniAnimateAttr.appendChild(frSpan);
// Add the attribute add button
const attrAddSpan = document.createElement('span');
attrAddSpan.className = 'etaniAnimateAttrAdd';
attrAddSpan.textContent = '+';
attrAddSpan.addEventListener('click', (e) => showDropdown(e, animateType, useId));
etaniAnimateAttr.appendChild(attrAddSpan);
// Handle clicks on existing attribute spans
etaniAnimateAttr.addEventListener('click', (e) => {
if (e.target.classList.contains('etaniAnimateId')) {
editAttribute(e.target, animateType, useId, 'id');
} else if (e.target.classList.contains('etaniAnimateBegin')) {
editAttribute(e.target, animateType, useId, 'begin');
} else if (e.target.classList.contains('etaniAnimateOther')) {
editAttribute(e.target, animateType, useId, 'other', e.target.textContent.split('=')[0]);
}
});
const valueDiv = document.createElement('div');
valueDiv.className = 'etaniAnimateValue';
const avCtrlDiv = createControlButtons(animateType, useId);
const avLabelSpan = document.createElement('span');
avLabelSpan.className = 'etaniAVLabel';
avLabelSpan.textContent = 'values : ';
const avDiv = document.createElement('div');
avDiv.className = 'etaniAV';
const avItemSpan = document.createElement('span');
avItemSpan.className = 'etaniAVItem';
avItemSpan.textContent = animateType === 'transform' ? 'a' : '1';
avItemSpan.addEventListener('click', (e) => handleAVItemClick(e, animateType));
avDiv.appendChild(avItemSpan);
valueDiv.appendChild(avCtrlDiv);
valueDiv.appendChild(avLabelSpan);
valueDiv.appendChild(avDiv);
etaniAnimate.appendChild(valueDiv);
itemRight.appendChild(etaniAnimate);
});
updateEtaniResult();
}
```
- 在 `etaniAllAppend` 創建處,將 `transformLink.addEventListener('click', handleAllAppendTransformClick);` 替換為 `transformLink.addEventListener('click', () => handleAllAppendAnimationClick('transform'));`。
- 將 `opacityLink.addEventListener('click', handleAllAppendOpacityClick);` 替換為 `opacityLink.addEventListener('click', () => handleAllAppendAnimationClick('opacity'));`。
- 刪除原 `handleAllAppendOpacityClick` 函數,因為已整合。
- 位置:在 `handleAVDeleteToggle`、`handleAVCopyToggle`、`handleAVMoveToggle` 函數定義處。
- 更改內容:將三個函數的相似邏輯整合成一個通用函數 `handleAVModeToggle(e, animateType, mode)`,其中 `mode` 為 'delete'、'copy' 或 'move'。替換原三個函數為:
```
// Handle toggle for delete, copy, or move mode
function handleAVModeToggle(e, animateType, mode) {
e.preventDefault();
const button = e.currentTarget;
resetModes(animateType, mode);
const isMode = (mode === 'delete' ? !isDeleteMode : mode === 'copy' ? !isCopyMode : !isMoveMode);
if (mode === 'delete') isDeleteMode = isMode;
if (mode === 'copy') isCopyMode = isMode;
if (mode === 'move') isMoveMode = isMode;
button.classList.toggle(`${mode}ing`, isMode);
const etaniItemRight = button.closest('.etaniItemRight');
const etaniCol = button.closest('.etaniCol');
const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
avItems.forEach(item => {
item.classList.toggle(`${mode}ing-target`, isMode);
});
etaniCol.classList.toggle(`${mode}ing-mode-${animateType}`, isMode);
if (isMode) {
button.title = `Click value item to ${mode} (Click again to cancel)`;
} else {
button.title = `${mode.charAt(0).toUpperCase() + mode.slice(1)} Value`;
avItems.forEach(item => {
item.classList.remove(`${mode}ing-target`);
});
if (mode === 'move') selectedMoveItem = null;
}
}
```
- 在 `createControlButtons` 中,將 `handler: (e) => handleAVDeleteToggle(e, animateType)` 替換為 `handler: (e) => handleAVModeToggle(e, animateType, 'delete')`。
- 同樣替換 copy 和 move 的 handler 為 `'copy'` 和 `'move'`。
- 刪除原 `handleAVDeleteToggle`、`handleAVCopyToggle`、`handleAVMoveToggle` 函數,因為已整合。
### 可精簡的地方
- 位置:在 `updateEtaniResult` 函數中,`const now = new Date();` 之後的日期格式化代碼。
- 更改內容:精簡為使用 `toISOString()` 並替換字符。替換為:
```
// Generate default filename with current date and time
const now = new Date();
const defaultFilename = `ejtileAnimation_${now.toISOString().replace(/[-:T]/g, '').slice(0, 15)}.svg`;
```
- 位置:在 `createEtaniInner` 函數中,重複的 `const etdropUses = document.querySelectorAll('.etdrop use');` 和後續循環。
- 更改內容:合併兩個 `etdropUses.forEach` 循環為一個,刪除第二個循環,將其邏輯移入第一個循環的末尾(在 `createEtaniItem(tileid);` 之後檢查現有動畫並創建多個 etaniAnimate)。
- 位置:在 `handleValueCtrlUpClick` 函數中,重複的 `etani_clone.querySelectorAll('animateTransform, animate[attributeName="opacity"]')` 和後續邏輯。
- 更改內容:精簡為先計算 maxValuesLength,然後在單一循環中更新所有 animates 和 UI。無需額外替換,但移除不必要的 `if (values.length < maxValuesLength && values.length > 0)` 檢查中的重複變量。
- 位置:在整個文件中,多處 `document.querySelector` 如 `.etaniModeRepeat.active`。
- 更改內容:在 `createEtaniInner` 開始處 cache 常用元素,如:
```
// Cache common elements for performance
const modeRepeatActive = document.querySelector('.etaniModeRepeat.active');
const modeMixedActive = document.querySelector('.etaniModeMixed.active');
```
- 在後續代碼中替換相應的 querySelector 為這些變量。