正在做etani,ejtile的動畫套件

分享和讨论Javascript相关的话题
回复
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3908
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 189 次
联系:

Re: 正在做etani,ejtile的動畫套件

帖子 ejsoon »

準備做加屬性:

代码: 全选

<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>
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3908
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 189 次
联系:

Re: 正在做etani,ejtile的動畫套件

帖子 ejsoon »

在生成.tileid時,在其內部的最後加入以下元素:

<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>

要用createElement和appendChild的方式加入。

為.tileid添加點擊事件:當點擊時,彈出etaniWindow窗口,內有:

第一行:div>(a.etaniAppendTransform{Transform}+a.etaniAppendMotion{Motion}+span.etaniAppendSet{Set})
第二行:div>(a.etaniAppendOpacity{Opacity}+a.etaniAppendWriting{Writing})
第三行:div>(a.etaniAppendFill{Fill}+a.etaniAppendStroke{Stroke}+a.etaniAppendWidth{Width})
第四行:div>(label{specify: }+input[type=input])
第五行:div>(確認,取消)

請用繁體中文回答。只需告知所要更改的代碼和位置,以及要替換的內容。代碼的注釋都要是英文。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3908
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 189 次
联系:

Re: 正在做etani,ejtile的動畫套件

帖子 ejsoon »

代码: 全选

我來為您說明需要進行的代碼改動:

## 改動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. 點擊選項時可切換選中狀態(藍色高亮)
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3908
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 189 次
联系:

Re: 正在做etani,ejtile的動畫套件

帖子 ejsoon »

https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3908
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 189 次
联系:

Re: 正在做etani,ejtile的動畫套件

帖子 ejsoon »

https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3908
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 189 次
联系:

Re: 正在做etani,ejtile的動畫套件

帖子 ejsoon »

對所上傳的js作以下修改:

當點擊.tileid打開etaniWindow時,需要記住打開它的id。

這些按鈕裡面,只有span.etaniAppendSet是可以高亮的,即點一次高亮,再點一次取消。

當點擊.etaniAppendTransform時,會在當前的etani_clone內的.etdrop內的<use href=id>增加三個animateTransform標籤(跟etaniAllAppendTransform相似,只不過它只加在打開它的id)

同一個etaniItem裡面,可能會有多個同類型的動畫標籤,比如有多個<animateTransform>,按其順序來對應etaniItemRight。

當span.etaniAppendSet高亮時,.etaniAppendTransform將不能被點擊,應加上不能點擊的css狀態。

請用繁體中文回答。只需告知所要更改的代碼和位置,以及要替換的內容。代碼的注釋都要是英文。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3908
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 189 次
联系:

Re: 正在做etani,ejtile的動畫套件

帖子 ejsoon »

代码: 全选

### 修改位置 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 索引。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3908
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 189 次
联系:

Re: 正在做etani,ejtile的動畫套件

帖子 ejsoon »

刪掉了所有「handleAppendConfirm」的代碼。
當前代碼:
ejtileani202510232126.7z
(16.86 KiB) 已下载 1 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3908
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 189 次
联系:

Re: 正在做etani,ejtile的動畫套件

帖子 ejsoon »

在所上傳的js中:

檢查一下有沒有函數是沒有被調用的,檢查有哪些代碼是可以整合的,有哪些地方可以精簡。

請用繁體中文回答。只需告知所要更改的代碼和位置,以及要替換的內容。代碼的注釋都要是英文。

grok

代码: 全选

### 未被調用的函數
- 位置:在 `// 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 為這些變量。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3908
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 189 次
联系:

Re: 正在做etani,ejtile的動畫套件

帖子 ejsoon »

https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3908
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 189 次
联系:

Re: 正在做etani,ejtile的動畫套件

帖子 ejsoon »

在所上傳的js中:

當點擊.etaniAppendTransform時,關閉當前的etaniWindow,並在etani_clone的這個<use href=id>增加三個<animateTransform>。類似.etaniAllAppendTransform,但只給特定的id添加。

當etaniAppendSet選中時,點擊.etaniAppendTransform無效。

請用繁體中文回答。只需告知所要更改的代碼和位置,以及要替換的內容。代碼的注釋都要是英文。

claude.ai sonnet4.5

代码: 全选

我來幫你修改代碼,讓 `.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`
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3908
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 189 次
联系:

Re: 正在做etani,ejtile的動畫套件

帖子 ejsoon »

接著上方的改動:

當點擊.etaniAppendOpacity時,關閉當前的etaniWindow,並在etani_clone的這個<use href=id>增加三個<animate>。類似.etaniAllAppendOpacity,但只給特定的id添加。

當etaniAppendSet選中時,點擊.etaniAppendOpacity無效。

請用繁體中文回答。只需告知所要更改的代碼和位置,以及要替換的內容。代碼的注釋都要是英文。

代码: 全选

我來幫你添加 `.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`
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复

在线用户

正浏览此版面之用户: 没有注册用户 和 2 访客