代码: 全选
<svg width="12" height="12" fill="none" stroke-width="1"><rect x="0" y="0" width="12" height="12" fill="darkgreen"></rect><line stroke="white" x1="6" y1="0" x2="6" y2="12"></line><line stroke="white" x1="0" y1="6" x2="12" y2="6"></line></svg>
代码: 全选
<svg width="12" height="12" fill="none" stroke-width="1"><rect x="0" y="0" width="12" height="12" fill="darkgreen"></rect><line stroke="white" x1="6" y1="0" x2="6" y2="12"></line><line stroke="white" x1="0" y1="6" x2="12" y2="6"></line></svg>
代码: 全选
我來為您說明需要進行的代碼改動:
## 改動1:在 `.tileid` 內添加 SVG 圖標
**位置**:在 `createEtaniItem` 函數中,找到創建 `tileidDiv` 的部分(約第1237行)
**找到**:
```javascript
const tileidDiv = document.createElement('div');
tileidDiv.className = 'tileid';
tileidDiv.textContent = tileid;
etaniItemLeft.appendChild(tileidDiv);
```
**改為**:
```javascript
const tileidDiv = document.createElement('div');
tileidDiv.className = 'tileid';
tileidDiv.textContent = tileid;
// Create SVG icon inside tileid
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '12');
svg.setAttribute('height', '12');
svg.setAttribute('fill', 'none');
svg.setAttribute('stroke-width', '1');
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', '0');
rect.setAttribute('y', '0');
rect.setAttribute('width', '12');
rect.setAttribute('height', '12');
rect.setAttribute('fill', 'darkgreen');
const line1 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line1.setAttribute('stroke', 'white');
line1.setAttribute('x1', '6');
line1.setAttribute('y1', '0');
line1.setAttribute('x2', '6');
line1.setAttribute('y2', '12');
const line2 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line2.setAttribute('stroke', 'white');
line2.setAttribute('x1', '0');
line2.setAttribute('y1', '6');
line2.setAttribute('x2', '12');
line2.setAttribute('y2', '6');
svg.appendChild(rect);
svg.appendChild(line1);
svg.appendChild(line2);
tileidDiv.appendChild(svg);
// Add click event to tileid
tileidDiv.addEventListener('click', (e) => {
e.stopPropagation();
showAppendWindow(tileid);
});
etaniItemLeft.appendChild(tileidDiv);
```
---
## 改動2:為 `.tileid` 添加 CSS 樣式
**位置**:在 `addDynamicStyles` 函數中,找到 `.tileid` 的樣式(約第213行)
**找到**:
```css
.tileid {
text-align: center;
font-size: 12px;
word-break: break-all;
padding-top: 2px;
}
```
**改為**:
```css
.tileid {
text-align: center;
font-size: 12px;
word-break: break-all;
padding-top: 2px;
cursor: pointer;
}
.tileid svg {
display: block;
margin: 2px auto 0 auto;
}
```
---
## 改動3:為窗口內的按鈕添加 CSS 樣式
**位置**:在 `addDynamicStyles` 函數中,找到 `.etaniWindow` 樣式之後(約第351行之後)
**在 `.etaniWindow button` 樣式之後添加**:
```css
.etaniAppendRow {
margin-bottom: 8px;
}
.etaniAppendRow a, .etaniAppendRow span {
display: inline-block;
padding: 4px 8px;
margin-right: 5px;
border: 1px solid #888;
background-color: #eee;
color: #333;
cursor: pointer;
text-decoration: none;
font-size: 12px;
}
.etaniAppendRow a:hover, .etaniAppendRow span:hover {
background-color: #ddd;
}
.etaniAppendRow a.selected, .etaniAppendRow span.selected {
background-color: #008CBA;
color: white;
border-color: #008CBA;
}
.etaniAppendTransform {
border-color: #2e36b9;
color: #2e36b9;
}
.etaniAppendMotion {
border-color: #c9254e;
color: #c9254e;
}
.etaniAppendSet {
border-color: #7a4d9e;
color: #7a4d9e;
}
.etaniAppendOpacity {
border-color: #b68942;
color: #b68942;
}
.etaniAppendWriting {
border-color: #2a8a5e;
color: #2a8a5e;
}
.etaniAppendFill {
border-color: #d4534f;
color: #d4534f;
}
.etaniAppendStroke {
border-color: #5f8ec4;
color: #5f8ec4;
}
.etaniAppendWidth {
border-color: #b8873e;
color: #b8873e;
}
.etaniAppendSpecify {
width: 100%;
box-sizing: border-box;
padding: 4px;
font-size: 12px;
}
```
---
## 改動4:創建顯示添加動畫窗口的函數
**位置**:在 `editAttribute` 函數之後(約第1800行之後)
**添加新函數**:
```javascript
// Show append animation window when clicking tileid
function showAppendWindow(useId) {
const windowDiv = document.createElement('div');
windowDiv.className = 'etaniWindow';
// First row: Transform, Motion, Set
const row1 = document.createElement('div');
row1.className = 'etaniAppendRow';
const transformLink = document.createElement('a');
transformLink.className = 'etaniAppendTransform';
transformLink.textContent = 'Transform';
transformLink.href = 'javascript:;';
transformLink.addEventListener('click', function(e) {
e.preventDefault();
toggleSelection(this);
});
const motionLink = document.createElement('a');
motionLink.className = 'etaniAppendMotion';
motionLink.textContent = 'Motion';
motionLink.href = 'javascript:;';
motionLink.addEventListener('click', function(e) {
e.preventDefault();
toggleSelection(this);
});
const setSpan = document.createElement('span');
setSpan.className = 'etaniAppendSet';
setSpan.textContent = 'Set';
setSpan.addEventListener('click', function() {
toggleSelection(this);
});
row1.appendChild(transformLink);
row1.appendChild(motionLink);
row1.appendChild(setSpan);
// Second row: Opacity, Writing
const row2 = document.createElement('div');
row2.className = 'etaniAppendRow';
const opacityLink = document.createElement('a');
opacityLink.className = 'etaniAppendOpacity';
opacityLink.textContent = 'Opacity';
opacityLink.href = 'javascript:;';
opacityLink.addEventListener('click', function(e) {
e.preventDefault();
toggleSelection(this);
});
const writingLink = document.createElement('a');
writingLink.className = 'etaniAppendWriting';
writingLink.textContent = 'Writing';
writingLink.href = 'javascript:;';
writingLink.addEventListener('click', function(e) {
e.preventDefault();
toggleSelection(this);
});
row2.appendChild(opacityLink);
row2.appendChild(writingLink);
// Third row: Fill, Stroke, Width
const row3 = document.createElement('div');
row3.className = 'etaniAppendRow';
const fillLink = document.createElement('a');
fillLink.className = 'etaniAppendFill';
fillLink.textContent = 'Fill';
fillLink.href = 'javascript:;';
fillLink.addEventListener('click', function(e) {
e.preventDefault();
toggleSelection(this);
});
const strokeLink = document.createElement('a');
strokeLink.className = 'etaniAppendStroke';
strokeLink.textContent = 'Stroke';
strokeLink.href = 'javascript:;';
strokeLink.addEventListener('click', function(e) {
e.preventDefault();
toggleSelection(this);
});
const widthLink = document.createElement('a');
widthLink.className = 'etaniAppendWidth';
widthLink.textContent = 'Width';
widthLink.href = 'javascript:;';
widthLink.addEventListener('click', function(e) {
e.preventDefault();
toggleSelection(this);
});
row3.appendChild(fillLink);
row3.appendChild(strokeLink);
row3.appendChild(widthLink);
// Fourth row: Specify input
const row4 = document.createElement('div');
row4.className = 'etaniAppendRow';
const specifyLabel = document.createElement('label');
specifyLabel.textContent = 'specify: ';
const specifyInput = document.createElement('input');
specifyInput.type = 'text';
specifyInput.className = 'etaniAppendSpecify';
row4.appendChild(specifyLabel);
row4.appendChild(specifyInput);
// Fifth row: Confirm and Cancel buttons
const row5 = document.createElement('div');
row5.className = 'etaniAppendRow';
const confirmBtn = document.createElement('button');
confirmBtn.textContent = '確認';
confirmBtn.addEventListener('click', () => {
handleAppendConfirm(windowDiv, useId);
});
const cancelBtn = document.createElement('button');
cancelBtn.textContent = '取消';
cancelBtn.addEventListener('click', () => {
document.body.removeChild(windowDiv);
});
row5.appendChild(confirmBtn);
row5.appendChild(cancelBtn);
// Append all rows to window
windowDiv.appendChild(row1);
windowDiv.appendChild(row2);
windowDiv.appendChild(row3);
windowDiv.appendChild(row4);
windowDiv.appendChild(row5);
document.body.appendChild(windowDiv);
}
// Toggle selection state for append window items
function toggleSelection(element) {
element.classList.toggle('selected');
}
// Handle confirm button in append window
function handleAppendConfirm(windowDiv, useId) {
const selectedItems = windowDiv.querySelectorAll('.etaniAppendRow .selected');
const specifyValue = windowDiv.querySelector('.etaniAppendSpecify').value.trim();
// Collect selected animation types
const selections = Array.from(selectedItems).map(item => {
if (item.classList.contains('etaniAppendTransform')) return 'transform';
if (item.classList.contains('etaniAppendMotion')) return 'motion';
if (item.classList.contains('etaniAppendSet')) return 'set';
if (item.classList.contains('etaniAppendOpacity')) return 'opacity';
if (item.classList.contains('etaniAppendWriting')) return 'writing';
if (item.classList.contains('etaniAppendFill')) return 'fill';
if (item.classList.contains('etaniAppendStroke')) return 'stroke';
if (item.classList.contains('etaniAppendWidth')) return 'width';
return null;
}).filter(item => item !== null);
if (selections.length === 0) {
alert('Please select at least one animation type');
return;
}
// TODO: Implement the logic to add animations based on selections
console.log('Selected animations:', selections);
console.log('Specify value:', specifyValue);
console.log('Use ID:', useId);
// For now, just close the window
document.body.removeChild(windowDiv);
}
```
---
這些改動將實現:
1. 在 `.tileid` 內添加一個 12×12 的綠色十字 SVG 圖標
2. 為 `.tileid` 添加游標指針樣式
3. 點擊 `.tileid` 時彈出包含 8 個動畫類型選項的窗口
4. 窗口內有指定輸入框和確認/取消按鈕
5. 點擊選項時可切換選中狀態(藍色高亮)代码: 全选
### 修改位置 1: 在 showAppendWindow 函數中,調整 toggleSelection 的應用,只讓 .etaniAppendSet 可以高亮/取消高亮,其他按鈕點擊時不 toggle。
原代碼片段(在 showAppendWindow 內的每個按鈕的 addEventListener):
```javascript
transformLink.addEventListener('click', function(e) {
e.preventDefault();
toggleSelection(this);
});
// 其他按鈕類似
```
替換為:
```javascript
// Only allow etaniAppendSet to be toggled
transformLink.addEventListener('click', function(e) {
e.preventDefault();
// No toggle for this button
});
motionLink.addEventListener('click', function(e) {
e.preventDefault();
// No toggle for this button
});
setSpan.addEventListener('click', function() {
toggleSelection(this);
});
opacityLink.addEventListener('click', function(e) {
e.preventDefault();
// No toggle for this button
});
writingLink.addEventListener('click', function(e) {
e.preventDefault();
// No toggle for this button
});
fillLink.addEventListener('click', function(e) {
e.preventDefault();
// No toggle for this button
});
strokeLink.addEventListener('click', function(e) {
e.preventDefault();
// No toggle for this button
});
widthLink.addEventListener('click', function(e) {
e.preventDefault();
// No toggle for this button
});
```
### 修改位置 2: 在 showAppendWindow 函數中,記住打開它的 id(useId 已傳入,無需額外修改,但確保在 handleAppendConfirm 中使用 useId 處理單個 tile)。
無需替換代碼,此處已記住 useId(透過函數參數),後續邏輯會使用。
### 修改位置 3: 添加禁用 .etaniAppendTransform 的邏輯,當 .etaniAppendSet 高亮時,禁用它並添加 CSS 狀態。
在 showAppendWindow 函數末尾(row5.appendChild(cancelBtn); 之後),添加以下代碼:
```javascript
// Disable etaniAppendTransform when etaniAppendSet is selected
setSpan.addEventListener('click', function() {
if (setSpan.classList.contains('selected')) {
transformLink.style.pointerEvents = 'none';
transformLink.style.opacity = '0.5'; // Add disabled CSS state
} else {
transformLink.style.pointerEvents = 'auto';
transformLink.style.opacity = '1';
}
});
```
### 修改位置 4: 修改 handleAppendConfirm 函數,處理點擊 .etaniAppendTransform 時,只針對單個 useId 添加三個 animateTransform 標籤(類似 handleAllAppendTransformClick,但只處理一個 itemRight)。
原 handleAppendConfirm 函數:
```javascript
function handleAppendConfirm(windowDiv, useId) {
const selectedItems = windowDiv.querySelectorAll('.etaniAppendRow .selected');
const specifyValue = windowDiv.querySelector('.etaniAppendSpecify').value.trim();
// Collect selected animations types
const selections = Array.from(selectedItems).map(item => {
if (item.classList.contains('etaniAppendTransform')) return 'transform';
if (item.classList.contains('etaniAppendMotion')) return 'motion';
if (item.classList.contains('etaniAppendSet')) return 'set';
if (item.classList.contains('etaniAppendOpacity')) return 'opacity';
if (item.classList.contains('etaniAppendWriting')) return 'writing';
if (item.classList.contains('etaniAppendFill')) return 'fill';
if (item.classList.contains('etaniAppendStroke')) return 'stroke';
if (item.classList.contains('etaniAppendWidth')) return 'width';
return null;
}).filter(item => item !== null);
if (selections.length === 0) {
alert('Please select at least one animation type');
return;
}
// TODO: Implement the logic to add animations based on selections
console.log('Selected animations:', selections);
console.log('Specify value:', specifyValue);
console.log('Use ID:', useId);
// For now, just close the window
document.body.removeChild(windowDiv);
}
```
替換為:
```javascript
function handleAppendConfirm(windowDiv, useId) {
const selectedItems = windowDiv.querySelectorAll('.etaniAppendRow .selected');
const specifyValue = windowDiv.querySelector('.etaniAppendSpecify').value.trim();
// Collect selected animation types
const selections = Array.from(selectedItems).map(item => {
if (item.classList.contains('etaniAppendTransform')) return 'transform';
if (item.classList.contains('etaniAppendMotion')) return 'motion';
if (item.classList.contains('etaniAppendSet')) return 'set';
if (item.classList.contains('etaniAppendOpacity')) return 'opacity';
if (item.classList.contains('etaniAppendWriting')) return 'writing';
if (item.classList.contains('etaniAppendFill')) return 'fill';
if (item.classList.contains('etaniAppendStroke')) return 'stroke';
if (item.classList.contains('etaniAppendWidth')) return 'width';
return null;
}).filter(item => item !== null);
if (selections.length === 0) {
alert('Please select at least one animation type');
return;
}
const itemRight = document.querySelector(`.etaniItem[data-use-id="${useId}"] .etaniItemRight`);
if (!itemRight) return;
// Check if transform animation already exists for this tile
if (selections.includes('transform') && !itemRight.querySelector('.etaniAnimate[data-type="transform"]')) {
const originalUseElement = document.querySelector(`#etmain .etdrop use[href="#${useId}"]`);
if (!originalUseElement) return;
const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return;
const originalTransformString = originalUseElement.getAttribute('transform') || '';
const originalTransforms = parseTransform(originalTransformString);
const isRepeat = document.querySelector('.etaniModeRepeat.active');
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: translate, scale, rotate
cloneUseElement.appendChild(baseAnimate('translate', originalTransforms.translate));
cloneUseElement.appendChild(baseAnimate('scale', originalTransforms.scale));
cloneUseElement.appendChild(baseAnimate('rotate', originalTransforms.rotate));
// Create UI for the new transform animation (similar to handleAllAppendTransformClick, but for single item)
const etaniAnimate = document.createElement('div');
etaniAnimate.className = 'etaniAnimate';
etaniAnimate.setAttribute('data-type', 'transform');
const nameSpan = document.createElement('span');
nameSpan.className = 'etaniAnimateName';
nameSpan.textContent = 'transform';
const durSpan = document.createElement('span');
durSpan.className = 'etaniAnimateDur';
durSpan.textContent = 'dur: 0s';
// Add durSpan click handler (copy from handleAllAppendTransformClick)
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) {
const animates = etani_clone.querySelectorAll(`.etdrop use[href="#${useId}"] animateTransform`);
const currentValues = animates[0]?.getAttribute('values')?.split(';') || [];
const currentValuesCount = currentValues.length;
const isIntegerDur = Number.isInteger(currentDur);
const isIntegerNewDur = Number.isInteger(newDur);
if (isIntegerDur && isIntegerNewDur && newDur > currentDur) {
const valuesToAdd = newDur - currentDur;
const originalUseElement = document.querySelector(`#etmain .etdrop use[href="#${useId}"]`);
if (originalUseElement) {
const originalTransformString = originalUseElement.getAttribute('transform') || '';
const originalTransforms = parseTransform(originalTransformString);
animates.forEach(animate => {
const type = animate.getAttribute('type').toLowerCase();
const currentValueString = animate.getAttribute('values') || '';
let valueToAdd = '';
if (type === 'translate') {
valueToAdd = originalTransforms.translate;
} else if (type === 'scale') {
valueToAdd = originalTransforms.scale;
} else if (type === 'rotate') {
valueToAdd = originalTransforms.rotate;
}
let newValues = currentValueString ? currentValueString.split(';') : [];
for (let i = 0; i < valuesToAdd; i++) {
newValues.push(valueToAdd);
}
animate.setAttribute('values', newValues.join(';'));
});
const etaniAV = itemRight.querySelector(`.etaniAnimate[data-type="transform"] .etaniAV`);
if (etaniAV) {
const existingItems = etaniAV.querySelectorAll('.etaniAVItem');
const 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 = findFirstMissingLetter(existingLetters);
existingLetters.push(newAVItem.textContent);
newAVItem.addEventListener('click', (e) => handleAVItemClick(e, 'transform'));
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;
const animates = etani_clone.querySelectorAll(`.etdrop use[href="#${useId}"] animateTransform`);
animates.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 animateType = 'transform';
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('transform', useId);
const avLabelSpan = document.createElement('span');
avLabelSpan.className = 'etaniAVLabel';
avLabelSpan.textContent = 'values : ';
const avDiv = document.createElement('div');
avDiv.className = 'etaniAV';
// Add initial AVItem
const existingLetters = [];
const newAVItem = document.createElement('span');
newAVItem.className = 'etaniAVItem';
newAVItem.textContent = findFirstMissingLetter(existingLetters);
newAVItem.addEventListener('click', (e) => handleAVItemClick(e, 'transform'));
avDiv.appendChild(newAVItem);
valueDiv.appendChild(avCtrlDiv);
valueDiv.appendChild(avLabelSpan);
valueDiv.appendChild(avDiv);
etaniAnimate.appendChild(valueDiv);
itemRight.appendChild(etaniAnimate);
}
// TODO: Handle other selections similarly, allowing multiple same-type animations
updateEtaniResult();
document.body.removeChild(windowDiv);
}
```
### 修改位置 5: 修改 createEtaniInner 和相關 UI 創建邏輯,以支援同一個 etaniItemRight 中有多個相同類型的 etaniAnimate div(按順序對應多個 <animateTransform> 組)。
在 createEtaniInner 函數中,etdropUses.forEach 創建 etaniItem 後,添加以下代碼來初始化多個動畫 UI:
```javascript
// After creating etaniItem, initialize multiple animations if exist
etdropUses.forEach((useElement) => {
const tileid = useElement.getAttribute('href').substring(1);
createEtaniItem(tileid);
// Check for existing animations in clone and create multiple etaniAnimate if needed
const cloneUse = etani_clone.querySelector(`.etdrop use[href="#${tileid}"]`);
if (cloneUse) {
const animGroups = {}; // Group by type
const animates = cloneUse.querySelectorAll('animateTransform, animate[attributeName]');
animates.forEach(anim => {
const type = anim.tagName === 'animateTransform' ? 'transform' : anim.getAttribute('attributeName');
if (!animGroups[type]) animGroups[type] = [];
animGroups[type].push(anim);
});
const itemRight = document.querySelector(`.etaniItem[data-use-id="${tileid}"] .etaniItemRight`);
Object.keys(animGroups).forEach(type => {
animGroups[type].forEach((anim, index) => {
// Create separate etaniAnimate for each animation instance
// Reuse or adapt code from handleAllAppendTransformClick / handleAllAppendOpacityClick for each
// For simplicity, assume transform has groups of 3 animateTransform as one etaniAnimate
if (type === 'transform' && animGroups[type].length % 3 === 0) {
for (let i = 0; i < animGroups[type].length; i += 3) {
// Create one etaniAnimate for every 3 animateTransform
// Implement similar to single creation, but append multiple
}
} else {
// For other types, one per etaniAnimate
}
});
});
}
});
```
注意:此處的支援多個相同類型邏輯需根據實際動畫結構調整,假設 transform 以每三個 animateTransform 為一組(translate/scale/rotate),其他類型單獨一組。完整實現需擴展 handleAV* 等函數以處理多個 etaniAnimate 索引。代码: 全选
### 未被調用的函數
- 位置:在 `// 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 為這些變量。代码: 全选
我來幫你修改代碼,讓 `.etaniAppendTransform` 可以為特定的 tile ID 添加動畫。
## 需要修改的位置
### 1. 在 `showAppendWindow` 函數中修改 `transformLink` 的事件監聽器
**位置:** 找到創建 `transformLink` 的代碼(大約在第 1181 行附近)
**原始代碼:**
```javascript
const transformLink = document.createElement('a');
transformLink.className = 'etaniAppendTransform';
transformLink.textContent = 'Transform';
transformLink.href = 'javascript:;';
// Only allow etaniAppendSet to be toggled
transformLink.addEventListener('click', function(e) {
e.preventDefault();
});
```
**替換為:**
```javascript
const transformLink = document.createElement('a');
transformLink.className = 'etaniAppendTransform';
transformLink.textContent = 'Transform';
transformLink.href = 'javascript:;';
transformLink.addEventListener('click', function(e) {
e.preventDefault();
// Check if Set is selected, if so, do nothing
const setSpan = windowDiv.querySelector('.etaniAppendSet');
if (setSpan && setSpan.classList.contains('selected')) {
return;
}
// Add transform animation for this specific useId
handleAppendTransform(useId);
// Close the window
document.body.removeChild(windowDiv);
});
```
### 2. 添加新函數 `handleAppendTransform`
**位置:** 在 `handleAppendConfirm` 函數之前添加(大約在第 1328 行附近)
**添加以下代碼:**
```javascript
// Handle adding transform animation for a specific tile
function handleAppendTransform(useId) {
if (!etani_clone) return;
// Only work in tiles mode
if (currentSelectMode !== 'tiles') {
alert('Transform can only be added in tiles mode');
return;
}
const originalUseElement = document.querySelector(`#etmain .etdrop use[href="#${useId}"]`);
const itemRight = document.querySelector(`.etaniItem[data-use-id="${useId}"] .etaniItemRight`);
if (!originalUseElement || !itemRight) return;
// Check if animation already exists
if (itemRight.querySelector('.etaniAnimate[data-type="transform"]')) {
alert('Transform animation already exists for this tile');
return;
}
let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return;
const isRepeat = document.querySelector('.etaniModeRepeat.active');
// 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));
// Create HTML control structure
const animateType = 'transform';
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 = etani_clone.querySelectorAll(`.etdrop use[href="#${useId}"] animateTransform`);
// 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) {
const originalTransformString = originalUseElement.getAttribute('transform') || '';
const originalTransforms = parseTransform(originalTransformString);
animates.forEach(animate => {
const currentValueString = animate.getAttribute('values') || '';
const type = animate.getAttribute('type').toLowerCase();
const 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 = findFirstMissingLetter(existingLetters);
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 = etani_clone.querySelectorAll(`.etdrop use[href="#${useId}"] animateTransform`);
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 = 'a';
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();
}
```
## 總結
這些修改實現了以下功能:
1. **點擊 `.etaniAppendTransform`** 時,會為特定的 tile ID 添加 transform 動畫(三個 `<animateTransform>` 元素)
2. **當 `etaniAppendSet` 被選中時**,點擊 `.etaniAppendTransform` 會直接返回,不執行任何操作
3. **添加動畫後**,會自動關閉 `etaniWindow` 窗口
4. **新增的 `handleAppendTransform` 函數**處理單個 tile 的 transform 動畫添加,邏輯與 `handleAllAppendAnimationClick` 類似,但只針對特定的 `useId`代码: 全选
我來幫你添加 `.etaniAppendOpacity` 的功能。
## 需要修改的位置
### 1. 在 `showAppendWindow` 函數中修改 `opacityLink` 的事件監聽器
**位置:** 找到創建 `opacityLink` 的代碼(大約在第 1204 行附近)
**原始代碼:**
```javascript
const opacityLink = document.createElement('a');
opacityLink.className = 'etaniAppendOpacity';
opacityLink.textContent = 'Opacity';
opacityLink.href = 'javascript:;';
opacityLink.addEventListener('click', function(e) {
e.preventDefault();
});
```
**替換為:**
```javascript
const opacityLink = document.createElement('a');
opacityLink.className = 'etaniAppendOpacity';
opacityLink.textContent = 'Opacity';
opacityLink.href = 'javascript:;';
opacityLink.addEventListener('click', function(e) {
e.preventDefault();
// Check if Set is selected, if so, do nothing
const setSpan = windowDiv.querySelector('.etaniAppendSet');
if (setSpan && setSpan.classList.contains('selected')) {
return;
}
// Add opacity animation for this specific useId
handleAppendOpacity(useId);
// Close the window
document.body.removeChild(windowDiv);
});
```
### 2. 添加新函數 `handleAppendOpacity`
**位置:** 在 `handleAppendTransform` 函數之後添加(緊接著上次添加的函數)
**添加以下代碼:**
```javascript
// Handle adding opacity animation for a specific tile
function handleAppendOpacity(useId) {
if (!etani_clone) return;
// Only work in tiles mode
if (currentSelectMode !== 'tiles') {
alert('Opacity can only be added in tiles mode');
return;
}
const itemRight = document.querySelector(`.etaniItem[data-use-id="${useId}"] .etaniItemRight`);
if (!itemRight) return;
// Check if animation already exists
if (itemRight.querySelector('.etaniAnimate[data-type="opacity"]')) {
alert('Opacity animation already exists for this tile');
return;
}
let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return;
const isRepeat = document.querySelector('.etaniModeRepeat.active');
// Create opacity animation
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);
// Create HTML control structure
const animateType = 'opacity';
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 = [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 valueToAdd = '1';
animates.forEach(animate => {
const currentValueString = animate.getAttribute('values') || '';
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) {
for (let i = 0; i < valuesToAdd; i++) {
const newAVItem = document.createElement('span');
newAVItem.className = 'etaniAVItem';
newAVItem.textContent = '1';
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 = [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 = '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();
}
```
## 總結
這些修改實現了以下功能:
1. **點擊 `.etaniAppendOpacity`** 時,會為特定的 tile ID 添加 opacity 動畫(一個 `<animate attributeName="opacity">` 元素)
2. **當 `etaniAppendSet` 被選中時**,點擊 `.etaniAppendOpacity` 會直接返回,不執行任何操作
3. **添加動畫後**,會自動關閉 `etaniWindow` 窗口
4. **新增的 `handleAppendOpacity` 函數**處理單個 tile 的 opacity 動畫添加,邏輯與 `handleAllAppendAnimationClick` 的 opacity 部分類似,但只針對特定的 `useId`正浏览此版面之用户: 没有注册用户 和 2 访客