正在做etani,ejtile的動畫套件

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

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

帖子 ejsoon »

代码: 全选

好的,這裏是您要求的 JavaScript 代碼更改。

-----

### 一、 `addDynamicStyles()` 函數中的 CSS 更改

**1. (合併 Inst 1, 2) 合併 `.etaniUpdate` 樣式並移除 `label`**

  * *位置:* `addDynamicStyles()` 函數中,約第 50 行起。
  * *將以下代碼:*
    ```javascript
            /* Styles for etaniContent, etaniMode, etaniAllAppend, etaniValueCtrl */
            .etaniContent, .etaniMode, .etaniAllAppend, .etaniValueCtrl {
                display: inline-block;
                vertical-align: top;
                padding: 5px;
                border: 1px solid #ccc;
                margin: 0 5px 5px 5px;
                text-align: left;
            }

            .etaniUpdate {
                display: inline-block;
                vertical-align: top;
                padding: 5px;
                border: 1px solid #ccc;
                margin: 0 5px 5px 5px;
                text-align: left;
                font-size: 14px;
            }

            .etaniUpdate label {
                margin-right: 5px;
            }
    ```
  * *替換為:*
    ```javascript
            /* Styles for etaniContent, etaniMode, etaniAllAppend, etaniValueCtrl, etaniUpdate (Inst 2) */
            .etaniContent, .etaniMode, .etaniAllAppend, .etaniValueCtrl, .etaniUpdate {
                display: inline-block;
                vertical-align: top;
                padding: 5px;
                border: 1px solid #ccc;
                margin: 0 5px 5px 5px;
                text-align: left;
            }

            .etaniUpdate {
                /* Properties merged above (Inst 2) */
                font-size: 14px;
            }

            /* .etaniUpdate label removed (Inst 1) */
    ```

**2. (Inst 3) 移除 `.etaniUpdateBoard` 樣式**

  * *位置:* `addDynamicStyles()` 函數中,約第 90 行起。
  * *將以下代碼:*
    ```javascript
            .etaniUpdateBoard {
                border: 1px solid #c06;
                color: #c06;
            }
    ```
  * *替換為:*
    ```javascript
            /* .etaniUpdateBoard removed (Inst 3) */
    ```

**3. (Inst 6) 更改 `.etaniAllAppend` 按鈕樣式為連結樣式**

  * *位置:* `addDynamicStyles()` 函數中,約第 140 行起。
  * *將以下代碼:*
    ```javascript
            /* Styles for etaniAllAppend buttons */
            .etaniAllAppend button {
                padding: 5px 10px;
                font-size: 16px;
                margin: 0 5px;
                cursor: pointer;
                border: 1px solid #333;
                background-color: #fff;
            }
    ```
  * *替換為:*
    ```javascript
            /* Styles for etaniAllAppend links (Inst 6) */
            .etaniAllAppend a {
                display: inline-block; /* Added for <a> */
                text-decoration: none; /* Added for <a> */
                padding: 5px 10px;
                font-size: 16px;
                margin: 0 5px;
                cursor: pointer;
                border: 1px solid #333;
                background-color: #fff;
                color: #333; /* Added for <a> */
            }
    ```

**4. (Inst 5) 更改 `.etaniItem` 和 `.etaniItemRight` 樣式**

  * *位置:* `addDynamicStyles()` 函數中,約第 191 行。
  * *將以下代碼:*
    ```javascript
            /* etaniItem structure */
            .etaniItem {
                min-height: 48px;
                border: 1px solid #ccc;
                box-sizing: border-box;
                width: 100%;
                margin-bottom: -1px;
                background-color: lightyellow;
            }
    ```
  * *替換為:*
    ```javascript
            /* etaniItem structure */
            .etaniItem {
                min-height: 48px;
                border: 1px solid #ccc;
                box-sizing: border-box;
                width: 100%;
                margin-bottom: -1px;
                background-color: lightyellow;
                display: inline-block; /* Added (Inst 5) */
            }
    ```
  * *位置:* `addDynamicStyles()` 函數中,約第 209 行。
  * *將以下代碼:*
    ```javascript
            .etaniItemRight {
                margin-left: 60px; 
                padding: 7px;
                min-height: 48px; 
                background-color: #fff;
            }
    ```
  * *替換為:*
    ```javascript
            .etaniItemRight {
                margin-left: 60px; 
                padding: 7px;
                min-height: 64px; /* Changed from 48px (Inst 5) */
                background-color: #fff;
            }
    ```

-----

### 二、 `handleUpdateTilesClick()` 函數更改

**1. (Inst 3) 合併 'board' 更新功能**

  * *位置:* `handleUpdateTilesClick()` 函數末尾,約第 1382 行。
  * *將以下代碼:*
    ```javascript
        });

        // Refresh all tile images as defs might have changed
        updateAllTileImages();
        updateEtaniResult();
    }
    ```
  * *替換為:* (在 `updateAllTileImages()` 之前插入 `handleUpdateBoardClick` 的邏輯)
    ```javascript
        });

        // Logic from handleUpdateBoardClick (merging board update) (Inst 3)
        const originalSvg_board = document.getElementById('etmain');
        if (originalSvg_board) {
            const originalBoard = originalSvg_board.querySelector('.etdrop .etboard');
            const cloneBoard = etani_clone.querySelector('.etdrop .etboard');
        
            if (originalBoard && cloneBoard) {
                cloneBoard.replaceWith(originalBoard.cloneNode(true));
            }
        }
        // End of merged logic

        // Refresh all tile images as defs might have changed
        updateAllTileImages();
        updateEtaniResult();
    }
    ```

-----

### 三、 移除 `handleUpdateBoardClick()` 函數

**1. (Inst 3) 移除整個函數**

  * *位置:* 約第 1386 行起。
  * *刪除以下整個函數:*
    ```javascript
    // Handle click event for the 'board' update button
    function handleUpdateBoardClick(e) {
        e.preventDefault();
        if (!etani_clone) return;

        const originalSvg = document.getElementById('etmain');
        if (!originalSvg) return;

        const originalBoard = originalSvg.querySelector('.etdrop .etboard');
        const cloneBoard = etani_clone.querySelector('.etdrop .etboard');

        if (originalBoard && cloneBoard) {
            cloneBoard.replaceWith(originalBoard.cloneNode(true));
            updateEtaniResult();
        }
    }
    ```

-----

### 四、 `createEtaniInner()` 函數更改

**1. (Inst 1, 3, 4) 調整 `etaniContent` 和 `etaniUpdate` 佈局**

  * *位置:* `createEtaniInner()` 函數中,約第 1455 行起。
  * *將以下代碼:*
    ```javascript
    const etaniContent = document.createElement('div');
    etaniContent.className = 'etaniContent';

    const contentHTMLLink = document.createElement('a');
    contentHTMLLink.className = 'etaniContentHTML';
    contentHTMLLink.textContent = 'Show HTML';
    contentHTMLLink.href = 'javascript:;';
    contentHTMLLink.addEventListener('click', handleContentHTMLClick);
    etaniContent.appendChild(contentHTMLLink);

    const centerLink = document.createElement('a');
    centerLink.className = 'etaniCenter';
    centerLink.textContent = 'Center';
    centerLink.href = 'javascript:;';
    centerLink.addEventListener('click', handleCenterClick);
    etaniContent.appendChild(centerLink);

    etaniCtrl.appendChild(etaniContent);

    const etaniUpdate = document.createElement('div');
    etaniUpdate.className = 'etaniUpdate';

    const updateLabel = document.createElement('label');
    updateLabel.textContent = 'update: ';
    etaniUpdate.appendChild(updateLabel);

    const updateTilesLink = document.createElement('a');
    updateTilesLink.className = 'etaniUpdateTiles';
    updateTilesLink.textContent = 'tiles';
    updateTilesLink.href = 'javascript:;';
    updateTilesLink.addEventListener('click', handleUpdateTilesClick);
    etaniUpdate.appendChild(updateTilesLink);

    const updateBoardLink = document.createElement('a');
    updateBoardLink.className = 'etaniUpdateBoard';
    updateBoardLink.textContent = 'board';
    updateBoardLink.href = 'javascript:;';
    updateBoardLink.addEventListener('click', handleUpdateBoardClick);
    etaniUpdate.appendChild(updateBoardLink);

    etaniCtrl.appendChild(etaniUpdate);
    ```
  * *替換為:* (移除 `centerLink` 從 `etaniContent`,移除 `label` 和 `updateBoardLink` 從 `etaniUpdate`,重命名 `updateTilesLink`,並將 `centerLink` 添加到 `etaniUpdate`)
    ```javascript
    const etaniContent = document.createElement('div');
    etaniContent.className = 'etaniContent';

    const contentHTMLLink = document.createElement('a');
    contentHTMLLink.className = 'etaniContentHTML';
    contentHTMLLink.textContent = 'Show HTML';
    contentHTMLLink.href = 'javascript:;';
    contentHTMLLink.addEventListener('click', handleContentHTMLClick);
    etaniContent.appendChild(contentHTMLLink);

    // const centerLink = document.createElement('a'); // Moved to etaniUpdate (Inst 4)
    // ...
    // etaniContent.appendChild(centerLink);

    etaniCtrl.appendChild(etaniContent);

    const etaniUpdate = document.createElement('div');
    etaniUpdate.className = 'etaniUpdate';

    // const updateLabel = document.createElement('label'); // Removed (Inst 1)
    // updateLabel.textContent = 'update: ';
    // etaniUpdate.appendChild(updateLabel);

    const updateTilesLink = document.createElement('a');
    updateTilesLink.className = 'etaniUpdateTiles';
    updateTilesLink.textContent = 'update'; // Renamed (Inst 3)
    updateTilesLink.href = 'javascript:;';
    updateTilesLink.addEventListener('click', handleUpdateTilesClick); // Function now handles both (Inst 3)
    etaniUpdate.appendChild(updateTilesLink);

    // const updateBoardLink = document.createElement('a'); // Removed (Inst 3)
    // ...
    // etaniUpdate.appendChild(updateBoardLink);

    // Moved from etaniContent (Inst 4)
    const centerLink = document.createElement('a');
    centerLink.className = 'etaniCenter';
    centerLink.textContent = 'Center';
    centerLink.href = 'javascript:;';
    centerLink.addEventListener('click', handleCenterClick);
    etaniUpdate.appendChild(centerLink);

    etaniCtrl.appendChild(etaniUpdate);
    ```

**2. (Inst 6) 更改 `etaniAllAppend` 元素為 `<a>` 連結**

  * *位置:* `createEtaniInner()` 函數中,約第 1506 行起。
  * *將以下代碼:*
    ```javascript
    const etaniAllAppend = document.createElement('div');
    etaniAllAppend.className = 'etaniAllAppend';

    const transformButton = document.createElement('button');
    transformButton.className = 'etaniAllAppendTransform';
    transformButton.textContent = 'transform';
    transformButton.addEventListener('click', handleAllAppendTransformClick);

    const opacityButton = document.createElement('button');
    opacityButton.className = 'etaniAllAppendOpacity';
    opacityButton.textContent = 'opacity';
    opacityButton.addEventListener('click', handleAllAppendOpacityClick);

    etaniAllAppend.appendChild(transformButton);
    etaniAllAppend.appendChild(opacityButton);
    etaniCtrl.appendChild(etaniAllAppend);
    ```
  * *替換為:*
    ```javascript
    const etaniAllAppend = document.createElement('div');
    etaniAllAppend.className = 'etaniAllAppend';

    const transformLink = document.createElement('a'); // Changed from button (Inst 6)
    transformLink.className = 'etaniAllAppendTransform';
    transformLink.textContent = 'transform';
    transformLink.href = 'javascript:;'; // Added (Inst 6)
    transformLink.addEventListener('click', handleAllAppendTransformClick);

    const opacityLink = document.createElement('a'); // Changed from button (Inst 6)
    opacityLink.className = 'etaniAllAppendOpacity';
    opacityLink.textContent = 'opacity';
    opacityLink.href = 'javascript:;'; // Added (Inst 6)
    opacityLink.addEventListener('click', handleAllAppendOpacityClick);

    etaniAllAppend.appendChild(transformLink); // Changed variable name
    etaniAllAppend.appendChild(opacityLink); // Changed variable name
    etaniCtrl.appendChild(etaniAllAppend);
    ```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3907
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 189 次
联系:

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

帖子 ejsoon »

對js代碼進行以下改動:

把所有的.etaniCtrl>div>a的樣式統一為:

{
  display: inline-block;
  text-decoration: none;
  padding: 2px 8px;
  font-size: 14px;
  margin: 0 2px;
  cursor: pointer;
}

只特別定義它們的border和color:

a.etaniContentHTML {
border: 1px solid #db3a32;
color: #db3a32;
}
a.etaniUpdateTiles {
border: 1px solid #008CBA;
color: #008CBA;
}
a.etaniCenter {
border: 1px solid green;
color: green;
}
a.etaniAllAppendTransform {
border: 1px solid #2e36b9;
color: #2e36b9;
}
a.etaniAllAppendOpacity {
border: 1px solid #b68942;
color: #b68942;
}
a.etaniValueCtrlUp {
border: 1px solid purple;
color: purple;
}

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

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

帖子 ejsoon »

代码: 全选

好的,這裏是您要求的 CSS 代碼更改。

此更改將統一 `addDynamicStyles()` 函數中 `.etaniCtrl` 內部 `div` 中的所有 `<a>` 標籤的基礎樣式,並移除它們各自的樣式定義中除 `border` 和 `color` 之外的屬性,同時更新 `border` 和 `color` 的值。

-----

### `addDynamicStyles()` 函數中的 CSS 更改

  * *位置:* `addDynamicStyles()` 函數中,約第 80 行起。

  * *將以下代碼:*

    ```javascript
            .etaniUpdate a {
                display: inline-block;
                text-decoration: none;
                padding: 2px 8px;
                font-size: 14px;
                margin: 0 2px;
                cursor: pointer;
            }
            
            .etaniUpdateTiles {
                border: 1px solid #008CBA;
                color: #008CBA;
            }

            /* .etaniUpdateBoard removed (Inst 3) */

            .etaniCol {
    ```

  * *替換為:* (插入統一的樣式規則 `.etaniCtrl > div > a`,並移除舊的 `.etaniUpdate a` 規則)

    ```javascript
            /* Unified styles for links in etaniCtrl divs (Inst 1) */
            .etaniCtrl > div > a {
                display: inline-block;
                text-decoration: none;
                padding: 2px 8px;
                font-size: 14px;
                margin: 0 2px;
                cursor: pointer;
            }
            /* .etaniUpdate a removed (unified above) */
            
            .etaniUpdateTiles {
                border: 1px solid #008CBA;
                color: #008CBA;
            }

            /* .etaniUpdateBoard removed (Inst 3) */

            .etaniCol {
    ```

  * *位置:* `addDynamicStyles()` 函數中,約第 99 行起。

  * *將以下代碼:*

    ```javascript
            /* Control and button styles */
            .etaniContent a, .etaniValueCtrl a {
                display: inline-block;
                margin: 0 5px;
                text-decoration: none;
                padding: 5px 10px;
                font-size: 14px;
            }
            
            .etaniCenter {
                border: 1px solid green;
                color: green;
            }
            
            .etaniContentHTML {
                border: 1px solid #0099ff;
                color: #0099ff;
                margin-right: 15px;
            }

            .etaniValueCtrlUp {
                border: 1px solid purple;
                color: purple;
            }

            /* Styles for etaniAllAppend links (Inst 6) */
            .etaniAllAppend a {
                display: inline-block; /* Added for <a> */
                text-decoration: none; /* Added for <a> */
                padding: 5px 10px;
                font-size: 16px;
                margin: 0 5px;
                cursor: pointer;
                border: 1px solid #333;
                background-color: #fff;
                color: #333; /* Added for <a> */
            }
    ```

  * *替換為:* (移除舊的基礎樣式,僅保留 `border` 和 `color`,並添加新的 `etaniAllAppendTransform` 和 `etaniAllAppendOpacity` 規則)

    ```javascript
            /* Control and button styles */
            /* .etaniContent a, .etaniValueCtrl a removed (unified above) (Inst 1) */
            
            .etaniCenter {
                border: 1px solid green;
                color: green;
            }
            
            .etaniContentHTML {
                border: 1px solid #db3a32; /* Color updated (Inst 3) */
                color: #db3a32; /* Color updated (Inst 3) */
                /* margin-right removed (Inst 2) */
            }

            .etaniValueCtrlUp {
                border: 1px solid purple;
                color: purple;
            }

            /* .etaniAllAppend a (generic) styles removed (Inst 1) */

            /* Specific styles for etaniAllAppend links (Inst 3) */
            .etaniAllAppendTransform {
                border: 1px solid #2e36b9;
                color: #2e36b9;
            }

            .etaniAllAppendOpacity {
                border: 1px solid #b68942;
                color: #b68942;
            }
    ```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3907
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 189 次
联系:

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

帖子 ejsoon »

代码: 全选

function addBoardImage () {
	// 把下方的waitouter換成當前的 etaniItemImageOuter
	for (let i = 0; i < boardsnap.length; i++) {
		let waitscale = getBoardWaitScale(i);
		let waitstrokewidth = getBoardWaitStrokeWidth(waitscale, 1);
		let newtilewaiting = createWaitBoard(
			boardsnap[i].getAttribute("d"),
			20 + Math.floor(i / 12) * 40, 20 + (i % 12) * 40, 1, 0,
			"transparent", colormove, waitstrokewidth, waitouter);
		let waittransform = newtilewaiting.getAttribute("transform")
			.replace(/(.*scale\().*?(\).*)/, "$1" + waitscale + "$2");
		newtilewaiting.setAttribute("transform", waittransform);
		newtilewaiting.setAttribute("stroke-width", 
			getWaitStrokeWidth(waitscale, 1));
	}
}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3907
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 189 次
联系:

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

帖子 ejsoon »

對js代碼進行以下改動:

在最開始,為etani_clone中的.etdrop加上id=etdropani。

在.etaniCtrl內新增div.etaniSelect,放在.etaniUpdate的後面,內含:
span.etaniSelectTiles{tiles}
span.etaniSelectMoving{moving}
span.etaniSelectBoard{board}

這三個是單選,默認選擇第一個。

當選擇第一個時,.etaniCol中的.etaniItem展示的是etani_clone中的.etdrop中的所有<use>,(但不包括.etboard內的<use>,.etboard也是.etdrop的子元素)。

當選擇第二個時,每隔1s就檢測#etmain中是否存在use.tilemoving,如果存在,則etaniItem只展示這個use,其餘隱藏。

當選擇第三個時,首先將etaniCol清空,之後第一個etaniItem的tileid為「board」,它所指的是etani_clone中的#etdropani。第二個往後就是etani_clone中的.etboard中所有包含id的子元素,它們的類名都是.boardsnap。

board(#etdropani)的.etaniItemImage生成方式為:把當前的#etmain的clone之後,把clone的長寬設為40,viewBox="0 0 480 480",之後轉為base64,賦值至.etaniItemImage中。

.boardsnap的.etaniItemImage去掉,之後用以下方法填充.etaniItemImageOuter。

```
function addBoardImage () {
// 把下方的waitouter換成當前的 etaniItemImageOuter
for (let i = 0; i < boardsnap.length; i++) {
let waitscale = getBoardWaitScale(i);
let waitstrokewidth = getBoardWaitStrokeWidth(waitscale, 1);
let newtilewaiting = createWaitBoard(
boardsnap.getAttribute("d"),
20 + Math.floor(i / 12) * 40, 20 + (i % 12) * 40, 1, 0,
"transparent", colormove, waitstrokewidth, waitouter);
let waittransform = newtilewaiting.getAttribute("transform")
.replace(/(.*scale\().*?(\).*)/, "$1" + waitscale + "$2");
newtilewaiting.setAttribute("transform", waittransform);
newtilewaiting.setAttribute("stroke-width",
getWaitStrokeWidth(waitscale, 1));
}
}
```
boardsnap是個數組,對應著etani_clone中的.etdrop中的.etboard中的每一個.boardsnap,如果這個.boardsnap沒有id,就不展示。

當要為它加動畫時,這些動畫元素,包括<animate>、<animateTransform>、<animateMotion>、<set>,都加到#etmainani內部的最後,用href=#id指向所對應的元素。


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

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

帖子 ejsoon »

代码: 全选

好的,這是一項非常複雜的更改請求,涉及新增 UI、新的視圖邏輯以及對核心動畫處理機制的重大重構。

我將*假設*您在程式碼中已經全局定義了 `getBoardWaitScale`、`getBoardWaitStrokeWidth`、`createWaitBoard`、`getWaitStrokeWidth` 和 `colormove` 這些輔助函數和變量,因為它們在您的 `addBoardImage` 範例中被引用,但未在提供的 `ejtileani...js` 文件中定義。

以下是所需的代碼更改:

-----

### 一、 全局變量

  * *位置:* 檔案最頂部,`etani_clone` 變量聲明(約第 2 行)之後。
  * *插入以下代碼:*
    ```javascript
    // Track selection view mode and moving tile interval
    let currentEtaniView = 'tiles';
    let movingTileInterval = null;
    ```

-----

### 二、 `addDynamicStyles()` 函數

  * *位置:* `addDynamicStyles()` 函數中,`etaniMode` 樣式 (約第 155 行) 之後。
  * *插入以下代碼:*
    ```javascript
            /* Styles for etaniSelect (Inst 2) */
            .etaniSelect span {
                display: inline-block;
                cursor: pointer;
                padding: 4px 8px;
                margin: 0 3px;
                font-size: 14px;
                border: 1px solid #888;
                background-color: #eee;
                color: #333;
                user-select: none;
            }

            .etaniSelect span.active {
                background-color: #008CBA;
                color: white;
                border-color: #008CBA;
            }
    ```

-----

### 三、 `handleUpdateTilesClick()` 函數

  * *位置:* `handleUpdateTilesClick()` 函數末尾 (約第 1391 行)。
  * *將以下代碼:*
    ```javascript
        // Refresh all tile images as defs might have changed
        updateAllTileImages();
        updateEtaniResult();
    }
    ```
  * *替換為:*
    ```javascript
        // Refresh the current view to reflect changes (Inst 3)
        refreshCurrentEtaniView();
        updateEtaniResult();
    }
    ```

-----

### 四、 `createEtaniInner()` 函數

  * *位置:* `createEtaniInner()` 函數開頭 (約第 1422 行)。

  * *將以下代碼:*

    ```javascript
    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etani_clone = originalSvg.cloneNode(true);
    ```

  * *替換為:*

    ```javascript
    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etani_clone = originalSvg.cloneNode(true);
    // Add id to the cloned etdrop (Inst 1)
    const etdropClone = etani_clone.querySelector('.etdrop');
    if (etdropClone) {
        etdropClone.id = 'etdropani';
    }
    ```

  * *位置:* `createEtaniInner()` 函數中,`etaniCtrl.appendChild(etaniUpdate);` (約第 1494 行) 之後。

  * *插入以下代碼:*

    ```javascript
    // --- Add etaniSelect UI (Inst 2) ---
    const etaniSelect = document.createElement('div');
    etaniSelect.className = 'etaniSelect';

    const selectTiles = document.createElement('span');
    selectTiles.className = 'etaniSelectTiles active'; // Default
    selectTiles.textContent = 'tiles';
    selectTiles.addEventListener('click', () => switchEtaniView('tiles'));

    const selectMoving = document.createElement('span');
    selectMoving.className = 'etaniSelectMoving';
    selectMoving.textContent = 'moving';
    selectMoving.addEventListener('click', () => switchEtaniView('moving'));

    const selectBoard = document.createElement('span');
    selectBoard.className = 'etaniSelectBoard';
    selectBoard.textContent = 'board';
    selectBoard.addEventListener('click', () => switchEtaniView('board'));

    etaniSelect.appendChild(selectTiles);
    etaniSelect.appendChild(selectMoving);
    etaniSelect.appendChild(selectBoard);
    etaniCtrl.appendChild(etaniSelect);
    // --- End of etaniSelect UI ---
    ```

  * *位置:* `createEtaniInner()` 函數末尾,`etaniouter.appendChild(etaniinner);` (約第 1568 行) 之後。

  * *將以下代碼:*

    ```javascript
    const etdropUses = document.querySelectorAll('.etdrop use');

    etdropUses.forEach((useElement) => {
        const tileid = useElement.getAttribute('href').substring(1);
        createEtaniItem(tileid);
    });

    updateEtaniResult();
    ```

  * *替換為:*

    ```javascript
    // Initial population of the default view
    populateTilesView();

    updateEtaniResult();
    ```

-----

### 五、 `toggleAnimation()` 函數

  * *位置:* `toggleAnimation()` 函數中,`etani_clone = null;` (約第 1594 行) 之前。
  * *插入以下代碼:*
    ```javascript
        // Clear moving tile interval if it exists (Inst 4)
        if (movingTileInterval) {
            clearInterval(movingTileInterval);
            movingTileInterval = null;
        }
        currentEtaniView = 'tiles'; // Reset view
    ```

-----

### 六、 `handleAllAppendTransformClick()` 函數

  * *位置:* `handleAllAppendTransformClick()` 函數中,`etdropUses.forEach...` 循環 (約第 977 行) 內部。

  * *將以下代碼:*

    ```javascript
        const useId = originalUseElement.getAttribute('href').substring(1);
        const itemRight = etaniItemRights[i];
        
        if (!itemRight) return;

        // Check if transform animation already exists
        if (itemRight.querySelector('.etaniAnimate[data-type="transform"]')) return;

        let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;

        // Get the original transform values from the #etmain SVG
        const originalTransformString = originalUseElement.getAttribute('transform') || '';
        const originalTransforms = parseTransform(originalTransformString);
    ```

  * *替換為:*

    ```javascript
        // Find the etaniItem and targetId (Inst 6)
        const itemRight = etaniItemRights[i];
        if (!itemRight) return;
        
        const etaniItem = itemRight.closest('.etaniItem');
        if (!etaniItem) return;

        const targetId = etaniItem.getAttribute('data-target-id');
        if (!targetId) return;

        // Check if transform animation already exists
        if (itemRight.querySelector('.etaniAnimate[data-type="transform"]')) return;

        const targetElement = etani_clone.querySelector(`#${targetId}`);
        if (!targetElement) return;

        const isUseElement = targetElement.tagName.toLowerCase() === 'use';
        let originalTransformString = '';
        
        if (isUseElement) {
            // Logic for <use> elements
            const originalUseElement = document.querySelector(`#etmain .etdrop use[href="#${targetId}"]`);
            originalTransformString = originalUseElement ? originalUseElement.getAttribute('transform') || '' : '';
        } else if (targetElement.tagName.toLowerCase() !== 'path') {
            // Logic for non-path elements (like 'board' <g>) (Inst 6)
            const originalElement = document.querySelector(`#etmain #${targetId}`);
            originalTransformString = originalElement ? originalElement.getAttribute('transform') || '' : '';
        }
        // For <path> (boardsnap), originalTransformString remains '' and parseTransform will use defaults (Inst 6)
        
        const originalTransforms = parseTransform(originalTransformString);
    ```

  * *位置:* `handleAllAppendTransformClick()` 函數中,`baseAnimate` 輔助函數 (約第 1010 行) 內部。

  * *將以下代碼:*

    ```javascript
            // 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));
    ```

  * *替換為:*

    ```javascript
            // Add additive="sum" for scale and rotate, but not for translate
            if (type === 'scale' || type === 'rotate') {
                animate.setAttribute('additive', 'sum');
            }
            
            // Add href for external animation (board/boardsnap) (Inst 6)
            if (!isUseElement) {
                animate.setAttribute('href', `#${targetId}`);
            }
            return animate;
        };
        
        // Add all three animations
        // Order: translate, scale, rotate
        const animTranslate = baseAnimate('translate', originalTransforms.translate);
        const animScale = baseAnimate('scale', originalTransforms.scale);
        const animRotate = baseAnimate('rotate', originalTransforms.rotate);

        if (isUseElement) {
            // Append inside <use> element (Inst 6)
            targetElement.appendChild(animTranslate);
            targetElement.appendChild(animScale);
            targetElement.appendChild(animRotate);
        } else {
            // Append to root of SVG clone (Inst 6)
            etani_clone.appendChild(animTranslate);
            etani_clone.appendChild(animScale);
            etani_clone.appendChild(animRotate);
        }
    ```

  * *位置:* `handleAllAppendTransformClick()` 函數中,`durSpan` 的 `click` 監聽器 (約第 1033 行) 內部。

  * *將以下代碼:*

    ```javascript
                // Find animates inside the <use> element
                const animates = etani_clone.querySelectorAll(`.etdrop use[href="#${useId}"] animateTransform`);
    ```

  * *替換為:*

    ```javascript
                // Find animates (Inst 6)
                const animates = getAnimates('transform', targetId);
    ```

  * *位置:* `handleAllAppendTransformClick()` 函數中,`frSpan` 的 `click` 監聽器 (約第 1060 行) 內部。

  * *將以下代碼:*

    ```javascript
            const useId = itemRight.closest('.etaniItem').querySelector('.tileid').textContent;
            // Find animates inside the <use> element
            const animates = etani_clone.querySelectorAll(`.etdrop use[href="#${useId}"] animateTransform`);
    ```

  * *替換為:*

    ```javascript
            // const useId = ... // No longer needed
            // Find animates (Inst 6)
            const animates = getAnimates('transform', targetId);
    ```

  * *位置:* `handleAllAppendTransformClick()` 函數中,屬性添加/編輯的監聽器 (約第 1083 行) 內部。

  * *將以下代碼:*

    ```javascript
        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]);
            }
        });
    ```

  * *替換為:*

    ```javascript
        attrAddSpan.className = 'etaniAnimateAttrAdd';
        attrAddSpan.textContent = '+';
        attrAddSpan.addEventListener('click', (e) => showDropdown(e, animateType, targetId)); // Use targetId (Inst 6)
        etaniAnimateAttr.appendChild(attrAddSpan);

        // Handle clicks on existing attribute spans
        etaniAnimateAttr.addEventListener('click', (e) => {
            if (e.target.classList.contains('etaniAnimateId')) {
                editAttribute(e.target, animateType, targetId, 'id'); // Use targetId (Inst 6)
            } else if (e.target.classList.contains('etaniAnimateBegin')) {
                editAttribute(e.target, animateType, targetId, 'begin'); // Use targetId (Inst 6)
            } else if (e.target.classList.contains('etaniAnimateOther')) {
                editAttribute(e.target, animateType, targetId, 'other', e.target.textContent.split('=')[0]); // Use targetId (Inst 6)
            }
        });
    ```

  * *位置:* `handleAllAppendTransformClick()` 函數中,`avCtrlDiv` 創建 (約第 1102 行) 之後。

  * *將以下代碼:*

    ```javascript
        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        const avCtrlDiv = createControlButtons('transform', useId);
    ```

  * *替換為:*

    ```javascript
        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        const avCtrlDiv = createControlButtons('transform', targetId); // Use targetId (Inst 6)
    ```

-----

### 七、 `handleAllAppendOpacityClick()` 函數

  * *位置:* `handleAllAppendOpacityClick()` 函數中,`etdropUses.forEach...` 循環 (約第 1133 行) 內部。

  * *將以下代碼:*

    ```javascript
    etdropUses.forEach((originalUseElement, i) => {
        const useId = originalUseElement.getAttribute('href').substring(1);
        const itemRight = etaniItemRights[i];
        
        if (!itemRight) return;

        if (itemRight.querySelector('.etaniAnimate[data-type="opacity"]')) return;

        let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;
    ```

  * *替換為:*

    ```javascript
    etdropUses.forEach((originalUseElement, i) => {
        // Find the etaniItem and targetId (Inst 6)
        const itemRight = etaniItemRights[i];
        if (!itemRight) return;

        const etaniItem = itemRight.closest('.etaniItem');
        if (!etaniItem) return;

        const targetId = etaniItem.getAttribute('data-target-id');
        if (!targetId) return;

        // Check if opacity animation already exists
        if (itemRight.querySelector('.etaniAnimate[data-type="opacity"]')) return;

        const targetElement = etani_clone.querySelector(`#${targetId}`);
        if (!targetElement) return;

        const isUseElement = targetElement.tagName.toLowerCase() === 'use';
    ```

  * *位置:* `handleAllAppendOpacityClick()` 函數中,`animateOpacity` 創建 (約第 1152 行) 之後。

  * *將以下代碼:*

    ```javascript
        } else {
            animateOpacity.setAttribute('fill', 'freeze');
        }
        cloneUseElement.appendChild(animateOpacity);
    ```

  * *替換為:*

    ```javascript
        } else {
            animateOpacity.setAttribute('fill', 'freeze');
        }
        
        if (isUseElement) {
            // Append inside <use> element (Inst 6)
            targetElement.appendChild(animateOpacity);
        } else {
            // Append to root of SVG clone with href (Inst 6)
            animateOpacity.setAttribute('href', `#${targetId}`);
            etani_clone.appendChild(animateOpacity);
        }
    ```

  * *位置:* `handleAllAppendOpacityClick()` 函數中,`durSpan` 的 `click` 監聽器 (約第 1177 行) 內部。

  * *將以下代碼:*

    ```javascript
            if (newDur !== null && !isNaN(newDur) && newDur >= 0) {
                const animateOpacity = etani_clone.querySelector(`.etdrop use[href="#${useId}"] animate[attributeName="opacity"]`);
                if (animateOpacity) {
    ```

  * *替換為:*

    ```javascript
            if (newDur !== null && !isNaN(newDur) && newDur >= 0) {
                const animateOpacity = getAnimates('opacity', targetId)[0]; // Use getAnimates (Inst 6)
                if (animateOpacity) {
    ```

  * *位置:* `handleAllAppendOpacityClick()` 函數中,`frSpan` 的 `click` 監聽器 (約第 1208 行) 內部。

  * *將以下代碼:*

    ```javascript
            const useId = itemRight.closest('.etaniItem').querySelector('.tileid').textContent;
            const animateOpacity = etani_clone.querySelector(`.etdrop use[href="#${useId}"] animate[attributeName="opacity"]`);
    ```

  * *替換為:*

    ```javascript
            // const useId = ... // No longer needed
            const animateOpacity = getAnimates('opacity', targetId)[0]; // Use getAnimates (Inst 6)
    ```

  * *位置:* `handleAllAppendOpacityClick()` 函數中,屬性添加/編輯的監聽器 (約第 1229 行) 內部。

  * *將以下代碼:*

    ```javascript
        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]);
            }
        });
    ```

  * *替換為:*

    ```javascript
        const attrAddSpan = document.createElement('span');
        attrAddSpan.className = 'etaniAnimateAttrAdd';
        attrAddSpan.textContent = '+';
        attrAddSpan.addEventListener('click', (e) => showDropdown(e, animateType, targetId)); // Use targetId (Inst 6)
        etaniAnimateAttr.appendChild(attrAddSpan);

        // Handle clicks on existing attribute spans
        etaniAnimateAttr.addEventListener('click', (e) => {
            if (e.target.classList.contains('etaniAnimateId')) {
                editAttribute(e.target, animateType, targetId, 'id'); // Use targetId (Inst 6)
            } else if (e.target.classList.contains('etaniAnimateBegin')) {
                editAttribute(e.target, animateType, targetId, 'begin'); // Use targetId (Inst 6)
            } else if (e.target.classList.contains('etaniAnimateOther')) {
                editAttribute(e.target, animateType, targetId, 'other', e.target.textContent.split('=')[0]); // Use targetId (Inst 6)
            }
        });
    ```

  * *位置:* `handleAllAppendOpacityClick()` 函數中,`avCtrlDiv` 創建 (約第 1251 行) 之後。

  * *將以下代碼:*

    ```javascript
        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        const avCtrlDiv = createControlButtons('opacity', useId);
    ```

  * *替換為:*

    ```javascript
        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        const avCtrlDiv = createControlButtons('opacity', targetId); // Use targetId (Inst 6)
    ```

-----

### 八、 `handleValueCtrlUpClick()` 函數

  * *位置:* `handleValueCtrlUpClick()` 函數中,`while (values.length < maxValuesLength)` 循環 (約第 1298 行) 內部。

  * *將以下代碼:*

    ```javascript
            }
            animate.setAttribute('values', values.join(';'));
            const useId = animate.closest('use')?.getAttribute('href')?.substring(1) || 
                          animate.closest('g[data-use-href]')?.getAttribute('data-use-href')?.substring(1);
            if (useId) {
                const etaniAnimate = document.querySelector(`.etaniItem:has(.tileid:where(:text("${useId}"))) .etaniAnimate[data-type="${animate.getAttribute('attributeName') === 'opacity' ? 'opacity' : 'transform'}"]`);
    ```

  * *替換為:*

    ```javascript
            }
            animate.setAttribute('values', values.join(';'));
            
            // Find targetId either from <use> href or external anim href (Inst 6)
            let targetId = null;
            const useEl = animate.closest('use');
            if (useEl) {
                targetId = useEl.getAttribute('href')?.substring(1);
            } else if (animate.hasAttribute('href')) {
                targetId = animate.getAttribute('href')?.substring(1);
            }

            if (targetId) {
                const etaniAnimate = document.querySelector(`.etaniItem[data-target-id="${targetId}"] .etaniAnimate[data-type="${animate.getAttribute('attributeName') === 'opacity' ? 'opacity' : 'transform'}"]`);
    ```

  * *位置:* `handleValueCtrlUpClick()` 函數中,`etaniItemRights.forEach` 循環 (約第 1313 行) 內部。

  * *將以下代碼:*

    ```javascript
    etaniItemRights.forEach(itemRight => {
        const useId = itemRight.closest('.etaniItem').querySelector('.tileid').textContent;
        ['transform', 'opacity'].forEach(animateType => {
    ```

  * *替換為:*

    ```javascript
    etaniItemRights.forEach(itemRight => {
        const etaniItem = itemRight.closest('.etaniItem');
        if (!etaniItem) return;
        // const useId = etaniItem.querySelector('.tileid').textContent; // No longer reliable
        const targetId = etaniItem.getAttribute('data-target-id');
        if (!targetId) return;

        ['transform', 'opacity'].forEach(animateType => {
    ```

-----

### 九、 `createEtaniItem()` 函數

  * *位置:* `createEtaniItem()` 函數 (約第 1332 行)。
  * *將以下函數:*
    ```javascript
    // Create a single etaniItem in the UI
    function createEtaniItem(tileid) {
        const etaniCol = document.querySelector('.etaniCol');
        if (!etaniCol) return;

        // Check if item already exists
        if (etaniCol.querySelector(`.etaniItem[data-use-id="${tileid}"]`)) {
            return;
        }

        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';
        etaniItem.setAttribute('data-use-id', tileid);

        const etaniItemLeft = document.createElement('div');
    ...
        tileidDiv.textContent = tileid;
    ...
        etaniCol.appendChild(etaniItem);
    }
    ```
  * *替換為:*
    ```javascript
    // Create a single etaniItem in the UI
    function createEtaniItem(tileid, targetId) {
        const etaniCol = document.querySelector('.etaniCol');
        if (!etaniCol) return null; // Return null on failure

        targetId = targetId || tileid; // Default: targetId is tileid (Inst 6)

        // Check if item already exists using targetId for uniqueness
        if (etaniCol.querySelector(`.etaniItem[data-target-id="${targetId}"]`)) {
            return etaniCol.querySelector(`.etaniItem[data-target-id="${targetId}"]`); // Return existing
        }

        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';
        etaniItem.setAttribute('data-use-id', tileid); // This is the display ID
        etaniItem.setAttribute('data-target-id', targetId); // This is the real target ID (Inst 6)

        const etaniItemLeft = document.createElement('div');
        etaniItemLeft.className = 'etaniItemLeft';

        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        
        // Use generateTileImage to get the thumbnail (for tiles view)
        const base64Url = generateTileImage(tileid);
        
        const imageOuter = document.createElement('div'); // Create imageOuter regardless
        imageOuter.className = 'etaniItemImageOuter';

        if (base64Url) {
            const img = document.createElement('img');
            img.className = 'etaniItemImage';
            img.src = base64Url;
            img.alt = tileid;
            
            imageOuter.appendChild(img);
        }
        etaniItemLeft.appendChild(imageOuter);

        const tileidDiv = document.createElement('div');
        tileidDiv.className = 'tileid';
        tileidDiv.textContent = tileid;
        etaniItemLeft.appendChild(tileidDiv);

        etaniItem.appendChild(etaniItemLeft);
        etaniItem.appendChild(etaniItemRight);
        etaniCol.appendChild(etaniItem);
        
        return etaniItem; // Return the created item
    }
    ```

-----

### 十、 `handleAVAddClick()` 函數

  * *位置:* `handleAVAddClick()` 函數 (約第 624 行)。

  * *將以下函數簽名:*

    ```javascript
    function handleAVAddClick(e, useElementId, animateType = 'transform') {
    ```

  * *替換為:*

    ```javascript
    function handleAVAddClick(e, targetId, animateType = 'transform') { // Renamed param (Inst 6)
    ```

  * *位置:* `handleAVAddClick()` 函數中,`animateType === 'transform'` 區塊 (約第 632 行)。

  * *將以下代碼:*

    ```javascript
    if (animateType === 'transform') {
        // Get the original <use> element from #etmain to read its current transform
        const originalUseElement = document.querySelector(`#etmain .etdrop use[href="#${useElementId}"]`);
        if (!originalUseElement) return;

        const originalTransformString = originalUseElement.getAttribute('transform') || '';
        const originalTransforms = parseTransform(originalTransformString);

        // Get the corresponding <use> element in the clone
        const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useElementId}"]`);
        if (!cloneUseElement) return;

        // Find all animateTransform elements inside the cloned <use>
        const animates = cloneUseElement.querySelectorAll('animateTransform');
    ```

  * *替換為:*

    ```javascript
    if (animateType === 'transform') {
        const targetElement = etani_clone.querySelector(`#${targetId}`);
        if (!targetElement) return;
        const isUseElement = targetElement.tagName.toLowerCase() === 'use';

        let originalTransformString = '';
        if (isUseElement) {
            const originalUseElement = document.querySelector(`#etmain .etdrop use[href="#${targetId}"]`);
            if (originalUseElement) {
                originalTransformString = originalUseElement.getAttribute('transform') || '';
            }
        } else {
            const originalElement = document.querySelector(`#etmain #${targetId}`);
            if (originalElement && originalElement.tagName.toLowerCase() !== 'path') {
                originalTransformString = originalElement.getAttribute('transform') || '';
            }
        }
        
        const originalTransforms = parseTransform(originalTransformString);

        // Find all animateTransform elements (Inst 6)
        const animates = getAnimates('transform', targetId);
    ```

  * *位置:* `handleAVAddClick()` 函數中,`animateType === 'opacity'` 區塊 (約第 660 行)。

  * *將以下代碼:*

    ```javascript
    } else if (animateType === 'opacity') {
        const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useElementId}"]`);
        if (!cloneUseElement) return;

        const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
        if (!animateOpacity) return;
    ```

  * *替換為:*

    ```javascript
    } else if (animateType === 'opacity') {
        const targetElement = etani_clone.querySelector(`#${targetId}`);
        if (!targetElement) return;
        
        const animateOpacity = getAnimates('opacity', targetId)[0]; // Use getAnimates (Inst 6)
        if (!animateOpacity) return;
    ```

-----

### 十一、 `handleAVItemClick()` 函數

  * *位置:* `handleAVItemClick()` 函數開頭 (約第 778 行)。

  * *將以下代碼:*

    ```javascript
    const itemIndex = Array.from(item.parentNode.children).indexOf(item);
    const etaniItemRight = item.closest('.etaniItemRight');
    const etaniCol = item.closest('.etaniCol');
    const useId = etaniItemRight.closest('.etaniItem').querySelector('.tileid').textContent;
    ```

  * *替換為:*

    ```javascript
    const itemIndex = Array.from(item.parentNode.children).indexOf(item);
    const etaniItemRight = item.closest('.etaniItemRight');
    const etaniCol = item.closest('.etaniCol');
    const etaniItem = etaniItemRight.closest('.etaniItem'); // Get item (Inst 6)
    if (!etaniItem) return;
    const targetId = etaniItem.getAttribute('data-target-id'); // Get targetId (Inst 6)
    if (!targetId) return;
    // const useId = ... // No longer used
    ```

  * *位置:* `handleAVItemClick()` 函數中,`moving-mode` 邏輯 (約第 805 行)。

  * *將以下代碼:*

    ```javascript
            if (animateType === 'transform') {
                const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
                if (!cloneUseElement) return;

                const allAnimates = cloneUseElement.querySelectorAll('animateTransform');
    ```

  * *替換為:*

    ```javascript
            if (animateType === 'transform') {
                const allAnimates = getAnimates('transform', targetId); // Use getAnimates (Inst 6)
                if (!allAnimates || allAnimates.length === 0) return;
    ```

  * *位置:* `handleAVItemClick()` 函數中,`moving-mode` 邏輯 (約第 826 行)。

  * *將以下代碼:*

    ```javascript
            } else if (animateType === 'opacity') {
                const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
                if (!cloneUseElement) return;

                const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
                if (!animateOpacity) return;
    ```

  * *替換為:*

    ```javascript
            } else if (animateType === 'opacity') {
                const animateOpacity = getAnimates('opacity', targetId)[0]; // Use getAnimates (Inst 6)
                if (!animateOpacity) return;
    ```

  * *位置:* `handleAVItemClick()` 函數中,`copying-mode` 邏輯 (約第 858 行)。

  * *將以下代碼:*

    ```javascript
        if (animateType === 'transform') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return;

            const allAnimates = cloneUseElement.querySelectorAll('animateTransform');
    ```

  * *替換為:*

    ```javascript
        if (animateType === 'transform') {
            const allAnimates = getAnimates('transform', targetId); // Use getAnimates (Inst 6)
            if (!allAnimates || allAnimates.length === 0) return;
    ```

  * *位置:* `handleAVItemClick()` 函數中,`copying-mode` 邏輯 (約第 876 行)。

  * *將以下代碼:*

    ```javascript
        } else if (animateType === 'opacity') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return;

            const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
            if (!animateOpacity) return;
    ```

  * *替換為:*

    ```javascript
        } else if (animateType === 'opacity') {
            const animateOpacity = getAnimates('opacity', targetId)[0]; // Use getAnimates (Inst 6)
            if (!animateOpacity) return;
    ```

  * *位置:* `handleAVItemClick()` 函數中,`deleting-mode` 邏輯 (約第 900 行)。

  * *將以下代碼:*

    ```javascript
        if (animateType === 'transform') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return;

            const allAnimates = cloneUseElement.querySelectorAll('animateTransform');
    ```

  * *替換為:*

    ```javascript
        if (animateType === 'transform') {
            const allAnimates = getAnimates('transform', targetId); // Use getAnimates (Inst 6)
            if (!allAnimates || allAnimates.length === 0) return;
    ```

  * *位置:* `handleAVItemClick()` 函數中,`deleting-mode` 邏輯 (約第 931 行)。

  * *將以下代碼:*

    ```javascript
        } else if (animateType === 'opacity') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return;

            const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
            if (!animateOpacity) return;
    ```

  * *替K為:*

    ```javascript
        } else if (animateType === 'opacity') {
            const animateOpacity = getAnimates('opacity', targetId)[0]; // Use getAnimates (Inst 6)
            if (!animateOpacity) return;
    ```

  * *位置:* `handleAVItemClick()` 函數中,`opacity` 點擊/雙擊邏輯 (約第 966 行)。

  * *將以下代碼:*

    ```javascript
    } else if (animateType === 'opacity' && !etaniCol.classList.contains(`deleting-mode-${animateType}`) && !etaniCol.classList.contains(`copying-mode-${animateType}`)) {
        const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;

        const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
        if (!animateOpacity) return;
    ```

  * *替換為:*

    ```javascript
    } else if (animateType === 'opacity' && !etaniCol.classList.contains(`deleting-mode-${animateType}`) && !etaniCol.classList.contains(`copying-mode-${animateType}`)) {
        const animateOpacity = getAnimates('opacity', targetId)[0]; // Use getAnimates (Inst 6)
        if (!animateOpacity) return;
    ```

-----

### 十二、 `createControlButtons()` 函數

  * *位置:* `createControlButtons()` 函數 (約第 1604 行)。

  * *將以下函數簽名:*

    ```javascript
    function createControlButtons(animateType, useId) {
    ```

  * *替換為:*

    ```javascript
    function createControlButtons(animateType, targetId) { // Renamed param (Inst 6)
    ```

  * *位置:* `createControlButtons()` 函數中,`buttons` 數組 `handler` (約第 1618 行)。

  * *將以下代碼:*

    ```javascript
            handler: (e) => handleAVAddClick(e, useId, animateType)
    ```

  * *替換為:*

    ```javascript
            handler: (e) => handleAVAddClick(e, targetId, animateType) // Use targetId (Inst 6)
    ```

-----

### 十三、 `showDropdown()` 函數

  * *位置:* `showDropdown()` 函數 (約第 1681 行)。

  * *將以下函數簽名:*

    ```javascript
    function showDropdown(e, animateType, useId) {
    ```

  * *替換為:*

    ```javascript
    function showDropdown(e, animateType, targetId) { // Renamed param (Inst 6)
    ```

  * *位置:* `showDropdown()` 函數中,`div.addEventListener` (約第 1700 行)。

  * *將以下代碼:*

    ```javascript
        div.addEventListener('click', () => {
            showWindow(item, animateType, useId);
            dropdown.remove(); // Remove after selecting item
        });
    ```

  * *替換為:*

    ```javascript
        div.addEventListener('click', () => {
            showWindow(item, animateType, targetId); // Use targetId (Inst 6)
            dropdown.remove(); // Remove after selecting item
        });
    ```

-----

### 十四、 `showWindow()` 函數

  * *位置:* `showWindow()` 函數 (約第 1713 行)。

  * *將以下函數簽名:*

    ```javascript
    function showWindow(type, animateType, useId, existingSpan = null, otherAttrName = null) {
    ```

  * *替換為:*

    ```javascript
    function showWindow(type, animateType, targetId, existingSpan = null, otherAttrName = null) { // Renamed param (Inst 6)
    ```

  * *位置:* `showWindow()` 函數中,`confirmBtn` 的 `click` 監聽器 (約第 1774 行)。

  * *將以下代碼:*

    ```javascript
        confirmBtn.addEventListener('click', () => handleConfirm(type, animateType, useId, input1, input2, existingIdsDiv, existingSpan, windowDiv));
    ```

  * *替換為:*

    ```javascript
        confirmBtn.addEventListener('click', () => handleConfirm(type, animateType, targetId, input1, input2, existingIdsDiv, existingSpan, windowDiv)); // Use targetId (Inst 6)
    ```

  * *位置:* `showWindow()` 函數中,`deleteBtn` 的 `click` 監聽器 (約第 1780 行)。

  * *將以下代碼:*

    ```javascript
        deleteBtn.addEventListener('click', () => handleDelete(animateType, useId, type, otherAttrName, existingSpan, windowDiv));
    ```

  * *替換為:*

    ```javascript
        deleteBtn.addEventListener('click', () => handleDelete(animateType, targetId, type, otherAttrName, existingSpan, windowDiv)); // Use targetId (Inst 6)
    ```

-----

### 十五、 `handleConfirm()` 函數

  * *位置:* `handleConfirm()` 函數 (約第 1872 行)。

  * *將以下函數簽名:*

    ```javascript
    function handleConfirm(type, animateType, useId, input1, input2, existingIdsDiv, existingSpan, windowDiv) {
    ```

  * *替K為:*

    ```javascript
    function handleConfirm(type, animateType, targetId, input1, input2, existingIdsDiv, existingSpan, windowDiv) { // Renamed param (Inst 6)
    ```

  * *位置:* `handleConfirm()` 函數中,`getAnimates` 調用 (約第 1916 行)。

  * *將以下代碼:*

    ```javascript
        const animates = getAnimates(animateType, useId, attrName);
    ```

  * *替換為:*

    ```javascript
        const animates = getAnimates(animateType, targetId, attrName); // Use targetId (Inst 6)
    ```

  * *位置:* `handleConfirm()` 函數中,`attrAddSpan` 查找 (約第 1919 行)。

  * *將以下代碼:*

    ```javascript
        const attrAddSpan = existingSpan ? existingSpan.parentNode.querySelector('.etaniAnimateAttrAdd') : document.querySelector(`.etaniItem[data-use-id="${useId}"] .etaniAnimate[data-type="${animateType}"] .etaniAnimateAttrAdd`);
    ```

  * *替換為:*

    ```javascript
        const attrAddSpan = existingSpan ? existingSpan.parentNode.querySelector('.etaniAnimateAttrAdd') : document.querySelector(`.etaniItem[data-target-id="${targetId}"] .etaniAnimate[data-type="${animateType}"] .etaniAnimateAttrAdd`); // Use targetId (Inst 6)
    ```

-----

### 十六、 `handleDelete()` 函數

  * *位置:* `handleDelete()` 函數 (約第 1930 行)。

  * *將以下函數簽名:*

    ```javascript
    function handleDelete(animateType, useId, type, otherAttrName, existingSpan, windowDiv) {
    ```

  * *替換為:*

    ```javascript
    function handleDelete(animateType, targetId, type, otherAttrName, existingSpan, windowDiv) { // Renamed param (Inst 6)
    ```

  * *位置:* `handleDelete()` 函數中,`getAnimates` 調用 (約第 1932 行)。

  * *將以下代碼:*

    ```javascript
        const animates = getAnimates(animateType, useId);
    ```

  * *替換為:*

    ```javascript
        const animates = getAnimates(animateType, targetId); // Use targetId (Inst 6)
    ```

-----

### 十七、 `getAnimates()` 函數

  * *位置:* `getAnimates()` 函數 (約第 1943 行)。
  * *將以下函數:*
    ```javascript
    // Get animate elements for the specific useId and type
    function getAnimates(animateType, useId, attrType = null) {
        if (animateType === 'transform') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return [];

            if (attrType === 'id') {
                // Per original logic and example, 'id' applies only to the rotate animation
                return cloneUseElement.querySelectorAll('animateTransform[type="rotate"]') || [];
            } else {
                // Other attributes (like 'begin') apply to all three
                return cloneUseElement.querySelectorAll('animateTransform') || [];
            }
        } else {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            return cloneUseElement?.querySelectorAll('animate[attributeName="opacity"]') || [];
        }
    }
    ```
  * *替換為:* (重構以處理 `<use>` 和外部 `href` 動畫)
    ```javascript
    // Get animate elements for the specific targetId and type (Inst 6)
    function getAnimates(animateType, targetId, attrType = null) {
        const targetElement = etani_clone.querySelector(`#${targetId}`);
        if (!targetElement) return [];

        const isUseElement = targetElement.tagName.toLowerCase() === 'use';

        if (isUseElement) {
            // --- Logic for <use> elements (animations inside) ---
            if (animateType === 'transform') {
                if (attrType === 'id') {
                    // 'id' applies only to the rotate animation
                    return targetElement.querySelectorAll('animateTransform[type="rotate"]') || [];
                } else {
                    // Other attributes (like 'begin') apply to all three
                    return targetElement.querySelectorAll('animateTransform') || [];
                }
            } else {
                // opacity
                return targetElement.querySelectorAll('animate[attributeName="opacity"]') || [];
            }
        } else {
            // --- Logic for board/boardsnap (animations are external) ---
            const hrefTarget = `#${targetId}`;
            if (animateType === 'transform') {
                if (attrType === 'id') {
                    // 'id' applies only to rotate
                    return etani_clone.querySelectorAll(`animateTransform[type="rotate"][href="${hrefTarget}"]`) || [];
                } else {
                    // Others apply to all
                    return etani_clone.querySelectorAll(`animateTransform[href="${hrefTarget}"]`) || [];
                }
            } else {
                // opacity
                return etani_clone.querySelectorAll(`animate[attributeName="opacity"][href="${hrefTarget}"]`) || [];
            }
        }
    }
    ```

-----

### 十八、 `editAttribute()` 函數

  * *位置:* `editAttribute()` 函數 (約第 1963 行)。
  * *將以下函數簽名:*
    ```javascript
    function editAttribute(span, animateType, useId, type, otherAttrName) {
    ```
  * *替換為:*
    ```javascript
    function editAttribute(span, animateType, targetId, type, otherAttrName) { // Renamed param (Inst 6)
    ```
  * *位置:* `editAttribute()` 函數中,`showWindow` 調用 (約第 1964 行)。
  * *將以下代碼:*
    ```javascript
        showWindow(type, animateType, useId, span, otherAttrName);
    ```
  * *替換為:*
    ```javascript
        showWindow(type, animateType, targetId, span, otherAttrName); // Use targetId (Inst 6)
    ```

-----

### 十九、 在文件末尾添加新函數

  * *位置:* 整個 `ejtileani...js` 文件的最末尾。
  * *插入以下新函數:*
    ```javascript
    // --- New View Switching and Population Functions (Inst 2, 3, 4, 5) ---

    /**
     * Clear interval and switch the view mode
     */
    function switchEtaniView(mode) {
        if (currentEtaniView === mode) return; // No change
        currentEtaniView = mode;

        // Clear moving tile interval if it exists
        if (movingTileInterval) {
            clearInterval(movingTileInterval);
            movingTileInterval = null;
        }
        
        // Update active class on selector spans
        document.querySelectorAll('.etaniSelect span').forEach(span => {
            span.classList.remove('active');
        });
        const activeSpan = document.querySelector(`.etaniSelect${mode.charAt(0).toUpperCase() + mode.slice(1)}`);
        if (activeSpan) {
            activeSpan.classList.add('active');
        }

        // Populate the corresponding view
        refreshCurrentEtaniView();
    }

    /**
     * Refreshes the .etaniCol content based on the current view mode
     */
    function refreshCurrentEtaniView() {
        const etaniCol = document.querySelector('.etaniCol');
        if (!etaniCol) return;
        etaniCol.innerHTML = ''; // Clear content before repopulating

        if (currentEtaniView === 'tiles') {
            populateTilesView();
        } else if (currentEtaniView === 'moving') {
            // 'moving' view shows the same items as 'tiles' but filters them
            populateTilesView();
            startMovingInterval();
        } else if (currentEtaniView === 'board') {
            populateBoardView();
        }
    }

    /**
     * Populates the .etaniCol with <use> elements (default view)
     */
    function populateTilesView() {
        if (!etani_clone) return;
        // Select only <use> elements that are *direct children* of #etdropani (Inst 3)
        const etdropUses = etani_clone.querySelectorAll('#etdropani > use[href]');
        
        etdropUses.forEach((useElement) => {
            const tileid = useElement.getAttribute('href').substring(1);
            createEtaniItem(tileid, tileid); // Pass tileid as both display and target
        });
    }

    /**
     * Starts the interval to check for the .tilemoving element
     */
    function startMovingInterval() {
        // Clear existing interval just in case
        if (movingTileInterval) {
            clearInterval(movingTileInterval);
        }

        const etaniCol = document.querySelector('.etaniCol');
        if (!etaniCol) return;

        const checkMoving = () => {
            const movingTile = document.querySelector('#etmain use.tilemoving');
            const etaniItems = etaniCol.querySelectorAll('.etaniItem');

            if (movingTile) {
                const tileid = movingTile.getAttribute('href').substring(1);
                etaniItems.forEach(item => {
                    // Check against data-target-id (which is the tileid in this view)
                    if (item.getAttribute('data-target-id') === tileid) {
                        item.style.display = 'inline-block';
                    } else {
                        item.style.display = 'none';
                    }
                });
            } else {
                // No moving tile found, hide all items
                etaniItems.forEach(item => {
                    item.style.display = 'none';
                });
            }
        };

        movingTileInterval = setInterval(checkMoving, 1000);
        checkMoving(); // Run once immediately
    }

    /**
     * Populates the .etaniCol with 'board' and '.boardsnap' items
     */
    function populateBoardView() {
        if (!etani_clone || !document.getElementById('etmain')) return;

        // 1. Create 'board' item (#etdropani) (Inst 5)
        const boardItem = createEtaniItem('board', 'etdropani');
        if (boardItem) {
            const img = boardItem.querySelector('.etaniItemImage');
            if (img) {
                const mainClone = document.getElementById('etmain').cloneNode(true);
                mainClone.setAttribute('width', '40');
                mainClone.setAttribute('height', '40');
                mainClone.setAttribute('viewBox', '0 0 480 480');
                const svgString = new XMLSerializer().serializeToString(mainClone);
                img.src = svgToBase64(svgString);
            }
        }

        // 2. Create '.boardsnap' items (Inst 5)
        const boardsnapElements = etani_clone.querySelectorAll('.etboard .boardsnap[id]');
        
        // This assumes 'colormove' and helper functions are defined globally
        // as they were not provided in the script.
        const colormove = (typeof colormove !== 'undefined') ? colormove : '#FF0000'; // Default fallback

        boardsnapElements.forEach((snapEl, i) => {
            const snapId = snapEl.id;
            const snapItem = createEtaniItem(snapId, snapId); // tileid and targetId are the same
            
            if (snapItem) {
                snapItem.classList.add('boardsnap-item');
                const imgOuter = snapItem.querySelector('.etaniItemImageOuter');
                const img = snapItem.querySelector('.etaniItemImage');
                
                if (img) img.remove(); // Remove default <img>

                if (imgOuter) {
                    // --- Adapt user's provided logic snippet (Inst 5) ---
                    // This logic requires global functions: getBoardWaitScale, 
                    // getBoardWaitStrokeWidth, createWaitBoard, getWaitStrokeWidth.
                    try {
                        let waitscale = getBoardWaitScale(i);
                        let waitstrokewidth = getBoardWaitStrokeWidth(waitscale, 1);
                        let newtilewaiting = createWaitBoard(
                            snapEl.getAttribute("d"),
                            20 + Math.floor(i / 12) * 40, 20 + (i % 12) * 40, 1, 0,
                            "transparent", colormove, waitstrokewidth, 
                            imgOuter // Pass current imgOuter as 'waitouter'
                        );
                        let waittransform = newtilewaiting.getAttribute("transform")
                            .replace(/(.*scale\().*?(\).*)/, "$1" + waitscale + "$2");
                        newtilewaiting.setAttribute("transform", waittransform);
                        newtilewaiting.setAttribute("stroke-width", 
                            getWaitStrokeWidth(waitscale, 1));
                    } catch (e) {
                        console.error(`Failed to create board snap image for ${snapId}. Make sure helper functions (getBoardWaitScale, createWaitBoard, etc.) are defined.`, e);
                        imgOuter.textContent = 'ERR'; // Show error in UI
                    }
                    // --- End of adapted logic ---
                }
            }
        });
    }
    ```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3907
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 189 次
联系:

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

帖子 ejsoon »

當前代碼(前一個未開始):

代码: 全选

// Global variable to hold the SVG clone and ensure state is maintained
let etani_clone = null;

// Track copy, move, and delete mode states, selected item, and last click time for double-click detection
let isCopyMode = false;
let isMoveMode = false;
let isDeleteMode = false;
let selectedMoveItem = null;
let lastClickTime = 0;
let lastClickedItem = null;

// Add dynamic CSS styles to the document
function addDynamicStyles() {
    if (document.getElementById('dynamic-et-styles')) {
        return;
    }

    const styleSheet = document.createElement('style');
    styleSheet.id = 'dynamic-et-styles'; 
    styleSheet.textContent = `
        /* Styles for etaniouter and button */
        .etaniouter {
            margin-top: 10px;
            padding: 10px;
            border: 1px solid #ddd;
            background-color: #f5f5f5;
        }

        /* Styles for etaniinner, etaniCtrl, etaniCol, etaniResult */
        .etaniinner {
            margin-top: 10px;
        }
        
        .etaniCtrl {
            margin-bottom: 10px; 
            clear: both;
            padding: 5px;
            border: 1px solid #c0c0c0;
            text-align: center;
        }
        
        /* Styles for etaniContent, etaniMode, etaniAllAppend, etaniValueCtrl */
        .etaniContent, .etaniMode, .etaniAllAppend, .etaniValueCtrl, .etaniUpdate {
            display: inline-block;
            vertical-align: top;
            padding: 5px;
            border: 1px solid #ccc;
            margin: 0 5px 5px 5px;
            text-align: left;
        }

        .etaniUpdate {
            /* Properties merged above (Inst 2) */
            font-size: 14px;
        }

        .etaniCtrl > div > a {
            display: inline-block;
            text-decoration: none;
            padding: 2px 8px;
            font-size: 14px;
            margin: 0 2px;
            cursor: pointer;
        }
        
        .etaniUpdateTiles {
            border: 1px solid #008CBA;
            color: #008CBA;
        }

        .etaniCol {
            border: 1px solid #aaa;
            padding: 5px;
            margin-bottom: 10px; 
            clear: both;
        }

        /* Styles for etaniResult */
        .etaniResult {
            text-align: center;
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #bbb;
            box-sizing: border-box;
        }
        
        /* Control and button styles */
        
        .etaniCenter {
            border: 1px solid green;
            color: green;
        }
        
        .etaniContentHTML {
            border: 1px solid #db3a32; /* Color updated (Inst 3) */
            color: #db3a32; /* Color updated (Inst 3) */
            /* margin-right removed (Inst 2) */
        }

        .etaniValueCtrlUp {
            border: 1px solid purple;
            color: purple;
        }

        /* .etaniAllAppend a (generic) styles removed (Inst 1) */

        /* Specific styles for etaniAllAppend links (Inst 3) */
        .etaniAllAppendTransform {
            border: 1px solid #2e36b9;
            color: #2e36b9;
        }

        .etaniAllAppendOpacity {
            border: 1px solid #b68942;
            color: #b68942;
        }
        
        /* Custom radio button styles */
        .etaniMode span {
            display: inline-block;
            cursor: pointer;
            padding: 4px 8px;
            margin: 0 3px;
            font-size: 14px;
            border: 1px solid #888;
            background-color: #eee;
            color: #333;
            user-select: none;
        }

        .etaniMode span.active {
            background-color: #008CBA;
            color: white;
            border-color: #008CBA;
        }

        /* Result and animation item styles */
        .etaniResultDR {
            text-align: center;
            margin-bottom: 10px;
        }

        .etaniResultDownload, .etaniResultRename {
            display: inline-block;
            margin-right: 15px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 16px; 
        }
        
        .etaniResultDownload {
            border: 1px solid blue;
            color: blue;
        }

        .etaniResultRename {
            border: 1px solid brown;
            color: brown;
        }

        .etaniResultImage {
            display: block;
            max-width: 480px;
            width: 100%;
            height: auto;
            margin: 0 auto 10px auto;
            border: 1px solid #000;
            box-sizing: border-box;
        }
        
        .etaniResultSize {
            display: inline-block;
            margin-left: 10px;
            font-size: 12px; 
            color: #555;
        }
        
        /* etaniItem structure */
        .etaniItem {
            min-height: 48px;
            border: 1px solid #ccc;
            box-sizing: border-box;
            width: 100%;
            margin-bottom: -1px;
            background-color: lightyellow;
            display: inline-block; /* Added (Inst 5) */
        }
        
        .etaniItemLeft {
            float: left; 
            width: 60px;
            min-height: 48px;
            padding: 2px 0;
            text-align: center;
        }
        
        .etaniItemImageOuter {
            width: 40px;
            height: 40px;
            margin: 0 auto;
        }
        
        .etaniItemImage {
            width: 100%;
            height: 100%;
            display: block;
        }
        
        .etaniItemRight {
            margin-left: 60px; 
            padding: 7px;
            min-height: 64px; 
            background-color: #fff;
        }

        .tileid {
            text-align: center;
            font-size: 12px; 
            word-break: break-all;
            padding-top: 2px;
        }
        
        /* Animation controls */
        .etaniAnimate {
            border: 1px solid #999;
            padding: 5px;
            margin-bottom: 5px;
        }

        .etaniAnimateAttr {
            margin-bottom: 5px;
        }

        .etaniAnimateName {
            display: inline-block;
            padding: 2px 5px;
            background-color: #555;
            color: white;
            margin-right: 10px;
            font-size: 12px;
        }

        .etaniAnimateAttr > span {
            cursor: pointer;
        }

        .etaniAnimateAttr > span:not(.etaniAnimateName) {
            display: inline-block;
            padding: 2px 5px;
            font-size: 12px;
            margin-right: 7px;
            box-sizing: border-box;
            border-width: 1px;
            border-style: solid;
        }

        .etaniAnimateAttrAdd {
            border-color: #2c8c12;
            color: #2c8c12;
        }

        .etaniAnimateId {
            border-color: #742bcc;
            color: #742bcc;
        }

        .etaniAnimateBegin {
            border-color: #b6533c;
            color: #b6533c;
        }

        .etaniAnimateOther {
            border-color: #1cadca;
            color: #1cadca;
        }

        /* Dropdown menu styles */
        .etaniDropdown {
            position: absolute;
            background-color: #fff;
            border: 1px solid #ccc;
            box-shadow: 0 4px 8px rgba(0,0,0,0.1);
            z-index: 1002;
        }

        .etaniDropdownItem {
            padding: 8px 12px;
            cursor: pointer;
            font-size: 12px;
        }

        .etaniDropdownItem:hover {
            background-color: #f0f0f0;
        }

        /* Window modal styles */
        .etaniWindow {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 15px;
            border: 1px solid #ccc;
            box-shadow: 0 4px 8px rgba(0,0,0,0.2);
            z-index: 1003;
            width: 300px;
        }

        .etaniWindow label, .etaniWindow input {
            display: block;
            margin-bottom: 10px;
        }

        .etaniWindow button {
            margin-right: 10px;
            padding: 5px 10px;
        }

        .etaniExistingIds, .etaniAnimateBeginPlusOuter {
            margin-bottom: 10px;
            font-size: 12px;
        }

        .etaniIdItem {
            cursor: pointer;
            padding: 2px 5px;
            margin-right: 5px;
            border: 1px solid #ddd;
            display: inline-block;
        }

        .etaniIdItem.selected {
            background-color: #008CBA;
            color: white;
        }

        .etaniIdItem.zero-s.selected {
            color: white;
        }

        .etaniIdItem.zero-s {
            color: #888;
        }

        .etaniAnimateDur {
            border-color: blue;
            color: blue;
        }

        .etaniAnimateFR {
            border-color: #78229f;
            color: #78229f;
        }

        .etaniAnimateValue {
            margin-top: 5px;
        }

        .etaniAVCtrl {
            display: inline-block;
            vertical-align: top;
            margin-right: 5px;
            margin-bottom: 3px;
        }
        
        .etaniAVCtrl svg {
            margin-left: -1px;
            margin-top: -1px;
        }

        .etaniAVCtrl span {
            display: inline-block;
            width: 24px;
            height: 24px;
            line-height: 24px;
            text-align: center;
            cursor: pointer;
            box-sizing: border-box;
            vertical-align: top;
            margin-right: 3px;
        }

        .etaniAVAdd {
            background-color: #a7fca7;
            border: 1px solid #71c371;
        }
        
        .etaniAVDelete {
            background-color: #ffcccc;
            border: 1px solid #cc3333;
        }

        .etaniAVDelete.deleting {
            background-color: #cc3333;
            color: white;
        }

        .etaniAVCopy {
            background-color: #ccccff;
            border: 1px solid #6666cc;
        }

        .etaniAVCopy.copying {
            background-color: #6666cc;
            color: white;
        }

        .etaniAVMove {
            background-color: #ffcc99;
            border: 1px solid #cc9966;
        }

        .etaniAVMove.moving {
            background-color: #cc9966;
            color: white;
        }
        
        .etaniAVLabel {
            font-size: 14px;
            margin-right: 5px;
        }

        .etaniAV {
            display: inline-block;
            vertical-align: top;
        }

        .etaniAVItem {
            display: inline-block;
            height: 24px;
            background-color: #ff9933;
            border: 1px dashed #00bfff;
            margin: 0 5px 3px;
            padding: 0 5px;
            box-sizing: border-box;
            cursor: pointer;
            position: relative;
            text-align: center;
            line-height: 24px;
            font-size: 12px;
            color: #333;
        }

        .etaniAVItem.deleting-target, .etaniAVItem.copying-target, .etaniAVItem.moving-target {
            background-color: #ff4d4d;
            border: 2px solid red;
        }

        .etaniAVItem.selected-move {
            background-color: #66ccff;
            border: 2px solid #0066cc;
        }
        
        /* Modal/popup styles */
        .modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.5);
            z-index: 1000;
        }
        .modal-content {
            position: fixed;
            width: 98%;
            height: 48%;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 10px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
            z-index: 1001;
            box-sizing: border-box;
        }
        .modal-content textarea {
            width: 100%;
            height: calc(100% - 50px);
            resize: none;
            border: 1px solid #ccc;
            font-size: 12px;
            box-sizing: border-box;
        }
        .modal-close {
            position: absolute;
            top: 0;
            right: 10px;
            font-size: 48px;
            line-height: 1; 
            cursor: pointer;
            color: #333;
        }
    `;

    document.head.appendChild(styleSheet);
}

// Find the first missing letter in the sequence starting from 'a'
function findFirstMissingLetter(existingLetters) {
    let letter = 'a';
    while (existingLetters.includes(letter)) {
        const code = letter.charCodeAt(0);
        if (code >= 97 && code < 122) { // lowercase a-y
            letter = String.fromCharCode(code + 1);
        } else if (code === 122) { // z -> A
            letter = 'A';
        } else if (code >= 65 && code < 90) { // uppercase A-Y
            letter = String.fromCharCode(code + 1);
        } else if (code === 90) { // Z -> a (loop back)
            letter = 'a';
        }
    }
    return letter;
}

// Extract a specific transform function and its value from a transform string
function extractTransformPart(transformString, type) {
    const regex = new RegExp(`(${type})\\(([^)]+)\\)`, 'i');
    const match = transformString.match(regex);
    if (match) {
        return { func: match[1], value: match[2].trim() };
    }
    return { func: '', value: '' };
}

// Parse transform string to get individual transform values
function parseTransform(transformString) {
    const defaultTransform = { translate: '0,0', scale: '1,1', rotate: '0' };
    if (!transformString) return defaultTransform;

    const transform = {};

    const getMatch = (type) => {
        const part = extractTransformPart(transformString, type).value;
        if (!part) return null;
        return part.split(/[,\s]+/).join(',');
    };

    transform.translate = getMatch('translate') || defaultTransform.translate;
    transform.scale = getMatch('scale') || defaultTransform.scale;
    if (transform.scale.split(',').length === 1) {
        transform.scale += `,${transform.scale}`;
    }
    transform.rotate = getMatch('rotate') || defaultTransform.rotate;

    return transform;
}

// Convert SVG string to a Base64 data URL
function svgToBase64(svgString) {
    const base64 = btoa(unescape(encodeURIComponent(svgString)));
    return `data:image/svg+xml;base64,${base64}`;
}

// Format byte size into human-readable string
function formatBytes(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

// Update the result section with the current state of etani_clone
function updateEtaniResult() {
    if (!etani_clone) return;

    const svgString = new XMLSerializer().serializeToString(etani_clone);
    const sizeInBytes = new Blob([svgString]).size;
    const base64Url = svgToBase64(svgString);
    
    const now = new Date();
    const yyyy = now.getFullYear();
    const mm = String(now.getMonth() + 1).padStart(2, '0');
    const dd = String(now.getDate()).padStart(2, '0');
    const hh = String(now.getHours()).padStart(2, '0');
    const mi = String(now.getMinutes()).padStart(2, '0');
    const ss = String(now.getSeconds()).padStart(2, '0');
    const defaultFilename = `ejtileAnimation_${yyyy}${mm}${dd}_${hh}${mi}${ss}.svg`;

    const imgElement = document.querySelector('.etaniResultImage');
    const downloadElement = document.querySelector('.etaniResultDownload');
    const renameElement = document.querySelector('.etaniResultRename');
    const sizeElement = document.querySelector('.etaniResultSize');

    if (imgElement && downloadElement && renameElement && sizeElement) {
        imgElement.src = base64Url;
        sizeElement.textContent = `Size: ${formatBytes(sizeInBytes)}`;
        downloadElement.href = base64Url;
        downloadElement.download = defaultFilename;
        
        renameElement.onclick = (e) => {
            e.preventDefault();
            let currentDownloadName = downloadElement.download;
            let newFilename = prompt("Enter new filename:", currentDownloadName);
            if (newFilename) {
                if (!newFilename.toLowerCase().endsWith('.svg')) {
                    newFilename += '.svg';
                }
                downloadElement.download = newFilename;
                alert(`Filename changed to: ${newFilename}`);
            }
        };
    }
}

// Handle the 'Center' button click to reset the .etdrop transform
function handleCenterClick(e) {
    e.preventDefault();
    if (etani_clone) {
        const etdrop = etani_clone.querySelector('.etdrop');
        if (etdrop) {
            etdrop.setAttribute('transform', 'translate(240,240) scale(1,1)');
            updateEtaniResult();
        }
    }
}

// Handle the HTML popup window
function handleContentHTMLClick(e) {
    e.preventDefault();
    if (!etani_clone) return;

    const overlay = document.createElement('div');
    overlay.className = 'modal-overlay';
    
    const content = document.createElement('div');
    content.className = 'modal-content';
    
    const close = document.createElement('span');
    close.className = 'modal-close';
    close.innerHTML = '&times;';
    
    const textarea = document.createElement('textarea');
    textarea.value = new XMLSerializer().serializeToString(etani_clone);

    const closeModal = () => {
        if (document.body.contains(overlay)) document.body.removeChild(overlay);
        if (document.body.contains(content)) document.body.removeChild(content);
    };
    close.onclick = closeModal;
    overlay.onclick = closeModal;

    content.appendChild(close);
    content.appendChild(textarea);
    
    document.body.appendChild(overlay);
    document.body.appendChild(content);
}

// Calculate additive transform or opacity value for a given type
function calculateAdditiveValue(type, originalValue, currentBaseValue, scaleValue = '1,1') {
    const roundToThreeDecimals = (value) => {
        return Math.round(value * 1000) / 1000;
    };
    
    if (type === 'rotate') {
        const ovAngle = parseFloat(currentBaseValue.trim());
        const cvAngle = parseFloat(originalValue.trim());
        return roundToThreeDecimals(cvAngle - ovAngle);
    } else if (type === 'scale') {
        const ovScales = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
        const cvScales = originalValue.split(',').map(c => parseFloat(c.trim()));
        const v1 = roundToThreeDecimals(cvScales[0] / ovScales[0]);
        const v2 = roundToThreeDecimals(cvScales.length > 1 ? (cvScales[1] / ovScales[1]) : (cvScales[0] / ovScales[0]));
        return v1 === v2 ? `${v1}` : `${v1},${v2}`;
    } else if (type === 'translate') {
        const ovCoords = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
        const cvCoords = originalValue.split(',').map(c => parseFloat(c.trim()));
        const scaleFactors = scaleValue.split(',').map(c => parseFloat(c.trim()));
        const v1 = roundToThreeDecimals(cvCoords[0] - (ovCoords[0] * scaleFactors[0]));
        const v2 = roundToThreeDecimals(cvCoords[1] - (ovCoords[1] * (scaleFactors.length > 1 ? scaleFactors[1] : scaleFactors[0])));
        return `${v1},${v2}`;
    } else if (type === 'opacity') {
        return originalValue;
    }
    return '';
}

// Update duration based on the number of values
function updateDuration(animate, etaniAnimate, newValuesLength) {
    const currentDur = parseFloat(etaniAnimate.querySelector('.etaniAnimateDur').textContent.replace('dur: ', '').replace('s', ''));
    const isIntegerDur = Number.isInteger(currentDur);
    const newDur = isIntegerDur ? (newValuesLength > 1 ? newValuesLength - 1 : 0) : currentDur;
    if (newDur > 0) {
        animate.setAttribute('dur', `${newDur}s`);
    } else {
        animate.removeAttribute('dur');
    }

    etaniAnimate.querySelector('.etaniAnimateDur').textContent = `dur: ${newDur}s`;
}

// Handle click event for the '+' button for transform or opacity
function handleAVAddClick(e, useElementId, animateType = 'transform') {
    e.preventDefault();
    if (!etani_clone) return;

    // Reset all modes before adding a new value
    resetModes(animateType);

    const etaniItemRight = e.currentTarget.closest('.etaniItemRight');
    const etaniAV = etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"] .etaniAV`);
    const etaniAnimate = etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`);

    if (animateType === 'transform') {
        // Get the original <use> element from #etmain to read its current transform
        const originalUseElement = document.querySelector(`#etmain .etdrop use[href="#${useElementId}"]`);
        if (!originalUseElement) return;

        const originalTransformString = originalUseElement.getAttribute('transform') || '';
        const originalTransforms = parseTransform(originalTransformString);

        // Get the corresponding <use> element in the clone
        const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useElementId}"]`);
        if (!cloneUseElement) return;

        // Find all animateTransform elements inside the cloned <use>
        const animates = cloneUseElement.querySelectorAll('animateTransform');
        
        animates.forEach(animate => {
            const type = animate.getAttribute('type').toLowerCase();
            let currentValueString = animate.getAttribute('values') || '';
            let newValue = '';
            
            // Get the new value directly from the parsed original transform
            if (type === 'translate') {
                newValue = originalTransforms.translate;
            } else if (type === 'scale') {
                newValue = originalTransforms.scale;
            } else if (type === 'rotate') {
                newValue = originalTransforms.rotate;
            }
            
            const newValueString = (currentValueString ? currentValueString + ';' : '') + newValue;
            animate.setAttribute('values', newValueString);
            updateDuration(animate, etaniAnimate, newValueString.split(';').length);
        });
    } else if (animateType === 'opacity') {
        const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useElementId}"]`);
        if (!cloneUseElement) return;

        const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
        if (!animateOpacity) return;

        const currentValueString = animateOpacity.getAttribute('values') || '';
        const values = currentValueString ? currentValueString.split(';') : [];
        const newValue = values.length > 0 ? values[values.length - 1] : '1';
        const newValueString = (currentValueString ? currentValueString + ';' : '') + newValue;
        animateOpacity.setAttribute('values', newValueString);
        updateDuration(animateOpacity, etaniAnimate, newValueString.split(';').length);
    }

    // Add new etaniAVItem with appropriate label
    if (etaniAV && etaniAnimate) {
        const newAVItem = document.createElement('span');
        newAVItem.className = 'etaniAVItem';
        newAVItem.addEventListener('click', (e) => handleAVItemClick(e, animateType));

        const existingItems = etaniAV.querySelectorAll('.etaniAVItem');
        if (animateType === 'transform') {
            const existingLetters = Array.from(existingItems).map(item => item.textContent);
            newAVItem.textContent = findFirstMissingLetter(existingLetters);
        } else if (animateType === 'opacity') {
            const values = animateType === 'opacity' ? 
                etaniAnimate.querySelector('animate[attributeName="opacity"]')?.getAttribute('values')?.split(';') || ['1'] :
                ['1'];
            newAVItem.textContent = values[values.length - 1];
        }

        etaniAV.appendChild(newAVItem);
    }

    updateEtaniResult();
}

// Reset all mode states for a specific animateType
function resetModes(animateType, excludeMode = null) {
    const etaniCol = document.querySelector('.etaniCol');
    const avItems = document.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);

    if (excludeMode !== 'delete') {
        isDeleteMode = false;
        etaniCol.classList.remove(`deleting-mode-${animateType}`);
        document.querySelectorAll(`.etaniAVDelete[data-type="${animateType}"]`).forEach(btn => {
            btn.classList.remove('deleting');
            btn.title = 'Delete Value';
        });
    }

    if (excludeMode !== 'copy') {
        isCopyMode = false;
        etaniCol.classList.remove(`copying-mode-${animateType}`);
        document.querySelectorAll(`.etaniAVCopy[data-type="${animateType}"]`).forEach(btn => {
            btn.classList.remove('copying');
            btn.title = 'Copy Value';
        });
    }

    if (excludeMode !== 'move') {
        isMoveMode = false;
        selectedMoveItem = null;
        etaniCol.classList.remove(`moving-mode-${animateType}`);
        document.querySelectorAll(`.etaniAVMove[data-type="${animateType}"]`).forEach(btn => {
            btn.classList.remove('moving');
            btn.title = 'Move Value';
        });
    }

    avItems.forEach(item => {
        item.classList.remove('deleting-target', 'copying-target', 'moving-target', 'selected-move');
    });
}

// Handle click event for the '-' button to toggle deletion mode
function handleAVDeleteToggle(e, animateType = 'transform') {
    e.preventDefault();
    const deleteButton = e.currentTarget;
    resetModes(animateType, 'delete');
    isDeleteMode = !isDeleteMode;
    deleteButton.classList.toggle('deleting', isDeleteMode);
    
    const etaniItemRight = deleteButton.closest('.etaniItemRight');
    const etaniCol = deleteButton.closest('.etaniCol');
    
    const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
    
    avItems.forEach(item => {
        item.classList.toggle('deleting-target', isDeleteMode);
    });

    etaniCol.classList.toggle(`deleting-mode-${animateType}`, isDeleteMode);

    if (isDeleteMode) {
        deleteButton.title = "Click value item to delete (Click again to cancel)";
    } else {
        deleteButton.title = "Delete Value";
        
        avItems.forEach(item => {
            item.classList.remove('deleting-target');
        });
    }
}

// Handle click event for the 'Copy' button to toggle copy mode
function handleAVCopyToggle(e, animateType) {
    e.preventDefault();
    const copyButton = e.currentTarget;
    resetModes(animateType, 'copy');
    isCopyMode = !isCopyMode;
    copyButton.classList.toggle('copying', isCopyMode);
    
    const etaniItemRight = copyButton.closest('.etaniItemRight');
    const etaniCol = copyButton.closest('.etaniCol');
    
    const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
    
    avItems.forEach(item => {
        item.classList.toggle('copying-target', isCopyMode);
    });

    etaniCol.classList.toggle(`copying-mode-${animateType}`, isCopyMode);

    if (isCopyMode) {
        copyButton.title = "Click value item to copy (Click again to cancel)";
    } else {
        copyButton.title = "Copy Value";
        
        avItems.forEach(item => {
            item.classList.remove('copying-target');
        });
    }
}

// Handle click event for the 'Move' button to toggle move mode
function handleAVMoveToggle(e, animateType) {
    e.preventDefault();
    const moveButton = e.currentTarget;
    resetModes(animateType, 'move');
    isMoveMode = !isMoveMode;
    moveButton.classList.toggle('moving', isMoveMode);
    
    const etaniItemRight = moveButton.closest('.etaniItemRight');
    const etaniCol = moveButton.closest('.etaniCol');
    
    const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
    
    avItems.forEach(item => {
        item.classList.toggle('moving-target', isMoveMode);
    });

    etaniCol.classList.toggle(`moving-mode-${animateType}`, isMoveMode);

    if (isMoveMode) {
        moveButton.title = "Click value item to select, then click another to move (Click again to cancel)";
    } else {
        moveButton.title = "Move Value";
        selectedMoveItem = null;
        
        avItems.forEach(item => {
            item.classList.remove('moving-target');
            item.classList.remove('selected-move');
        });
    }
}

// Handle click event for an .etaniAVItem in deletion, copy, or move mode
function handleAVItemClick(e, animateType = 'transform') {
    const item = e.currentTarget;
    const itemIndex = Array.from(item.parentNode.children).indexOf(item);
    const etaniItemRight = item.closest('.etaniItemRight');
    const etaniCol = item.closest('.etaniCol');
    const useId = etaniItemRight.closest('.etaniItem').querySelector('.tileid').textContent;

    const currentTime = Date.now();
    const isDoubleClick = (item === lastClickedItem && (currentTime - lastClickTime) < 2000);

    lastClickTime = currentTime;
    lastClickedItem = item;

    if (etaniCol.classList.contains(`moving-mode-${animateType}`)) {
        const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
        
        if (!selectedMoveItem) {
            selectedMoveItem = item;
            item.classList.add('selected-move');
            return;
        } else if (selectedMoveItem === item) {
            selectedMoveItem.classList.remove('selected-move');
            selectedMoveItem = null;
            return;
        } else {
            const targetIndex = itemIndex;
            const sourceIndex = Array.from(item.parentNode.children).indexOf(selectedMoveItem);
            
            const parent = item.parentNode;
            if (sourceIndex < targetIndex) {
                if (item.nextSibling) {
                    parent.insertBefore(selectedMoveItem, item.nextSibling);
                } else {
                    parent.appendChild(selectedMoveItem);
                }
            } else {
                parent.insertBefore(selectedMoveItem, item);
            }

            if (animateType === 'transform') {
                const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
                if (!cloneUseElement) return;

                const allAnimates = cloneUseElement.querySelectorAll('animateTransform');
                
                allAnimates.forEach(animate => {
                    const values = animate.getAttribute('values').split(';');
                    const valueToMove = values[sourceIndex];
                    values.splice(sourceIndex, 1);
                    if (sourceIndex < targetIndex) {
                        values.splice(targetIndex, 0, valueToMove);
                    } else {
                        values.splice(targetIndex, 0, valueToMove);
                    }
                    animate.setAttribute('values', values.join(';'));
                });
            } else if (animateType === 'opacity') {
                const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
                if (!cloneUseElement) return;

                const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
                if (!animateOpacity) return;

                const values = animateOpacity.getAttribute('values').split(';');
                const valueToMove = values[sourceIndex];
                values.splice(sourceIndex, 1);
                if (sourceIndex < targetIndex) {
                    values.splice(targetIndex, 0, valueToMove);
                } else {
                    values.splice(targetIndex, 0, valueToMove);
                }
                animateOpacity.setAttribute('values', values.join(';'));
            }

            selectedMoveItem.classList.remove('selected-move');
            selectedMoveItem = null;
            updateEtaniResult();
            return;
        }
    }

    if (etaniCol.classList.contains(`copying-mode-${animateType}`)) {
        const etaniAV = item.parentNode;
        const newAVItem = document.createElement('span');
        newAVItem.className = 'etaniAVItem';
        newAVItem.textContent = item.textContent;
        newAVItem.addEventListener('click', (e) => handleAVItemClick(e, animateType));
        etaniAV.insertBefore(newAVItem, item.nextSibling);

        if (animateType === 'transform') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return;

            const allAnimates = cloneUseElement.querySelectorAll('animateTransform');
            
            allAnimates.forEach(animate => {
                const values = animate.getAttribute('values').split(';');
                if (itemIndex < values.length) {
                    values.splice(itemIndex + 1, 0, values[itemIndex]);
                    animate.setAttribute('values', values.join(';'));
                    updateDuration(animate, etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`), values.length);
                }
            });
        } else if (animateType === 'opacity') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return;

            const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
            if (!animateOpacity) return;

            const values = animateOpacity.getAttribute('values').split(';');
            if (itemIndex < values.length) {
                values.splice(itemIndex + 1, 0, values[itemIndex]);
                animateOpacity.setAttribute('values', values.join(';'));
                updateDuration(animateOpacity, etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`), values.length);
            }
        }

        updateEtaniResult();
        return;
    }

    if (etaniCol.classList.contains(`deleting-mode-${animateType}`)) {
        const deleteButton = etaniItemRight.querySelector(`.etaniAVDelete[data-type="${animateType}"]`);
        const etaniAnimate = etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`);
        
        if (animateType === 'transform') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return;

            const allAnimates = cloneUseElement.querySelectorAll('animateTransform');
            let newValuesLength = 0;
            
            allAnimates.forEach((animate, index) => {
                const values = animate.getAttribute('values').split(';');
                
                if (itemIndex < values.length) {
                    values.splice(itemIndex, 1);
                    newValuesLength = values.length; // All animations will have the same new length
                    
                    if (newValuesLength > 0) {
                        animate.setAttribute('values', values.join(';'));
                        // Only update duration on the first animate to avoid multiple calls
                        if (index === 0) { 
                            updateDuration(animate, etaniAnimate, newValuesLength);
                        }
                    }
                }
            });

            // If all values were removed, clean up
            if (newValuesLength === 0 && allAnimates.length > 0) {
                allAnimates.forEach(anim => anim.remove());
                etaniAnimate.remove();
            }
        } else if (animateType === 'opacity') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return;

            const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
            if (!animateOpacity) return;

            const values = animateOpacity.getAttribute('values').split(';');
            if (itemIndex < values.length) {
                values.splice(itemIndex, 1);
                if (values.length === 0) {
                    // Remove the opacity animation
                    animateOpacity.remove();
                    etaniAnimate.remove();
                } else {
                    animateOpacity.setAttribute('values', values.join(';'));
                    updateDuration(animateOpacity, etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`), values.length);
                }
            }
        }

        item.remove();

        const remainingItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
        
        if (remainingItems.length === 0) {
            // The .etaniAnimate element (containing the deleteButton) was just removed
            // because it was the last item.
            // We cannot .click() the button, as it's no longer in the DOM.
            // Instead, we must manually reset the delete mode globally.
            resetModes(animateType);
        } else {
            // If items remain, just re-apply the 'deleting-target' class to them.
            remainingItems.forEach(item => {
                item.classList.add('deleting-target'); 
            });
        }

        updateEtaniResult();
        
    } else if (animateType === 'opacity' && !etaniCol.classList.contains(`deleting-mode-${animateType}`) && !etaniCol.classList.contains(`copying-mode-${animateType}`)) {
        const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;

        const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
        if (!animateOpacity) return;

        const values = animateOpacity.getAttribute('values').split(';');
        if (itemIndex >= values.length) return;

        if (isDoubleClick) {
            const newValue = prompt("Enter opacity value (0 to 1):", values[itemIndex]);
            if (newValue !== null && !isNaN(newValue) && newValue >= 0 && newValue <= 1) {
                values[itemIndex] = newValue;
                item.textContent = newValue;
                animateOpacity.setAttribute('values', values.join(';'));
                updateEtaniResult();
            }
        } else {
            values[itemIndex] = values[itemIndex] === '0' ? '1' : '0';
            item.textContent = values[itemIndex];
            animateOpacity.setAttribute('values', values.join(';'));
            updateEtaniResult();
        }
    }
}

// Handle click event for the 'Transform' button
function handleAllAppendTransformClick() {
    if (!etani_clone) 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 transform animation already exists
        if (itemRight.querySelector('.etaniAnimate[data-type="transform"]')) return;

        let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;

        // 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));

        // --- Start of HTML control creation ---
        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';
        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) {
                // Find animates inside the <use> element
                const animates = etani_clone.querySelectorAll(`.etdrop use[href="#${useId}"] animateTransform`);
                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 useId = itemRight.closest('.etaniItem').querySelector('.tileid').textContent;
            // Find animates inside the <use> element
            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';
        
        const avItemSpan = document.createElement('span');
        avItemSpan.className = 'etaniAVItem';
        avItemSpan.textContent = 'a';
        avItemSpan.addEventListener('click', (e) => handleAVItemClick(e, 'transform'));
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avCtrlDiv);
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

        etaniAnimate.appendChild(valueDiv);
        itemRight.appendChild(etaniAnimate);
    });

    updateEtaniResult();
}

// Handle click event for the 'Opacity' button
function handleAllAppendOpacityClick() {
    if (!etani_clone) 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;

        if (itemRight.querySelector('.etaniAnimate[data-type="opacity"]')) return;

        let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;

        const animateOpacity = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
        animateOpacity.setAttribute('attributeName', 'opacity');
        animateOpacity.setAttribute('values', '1');
        if (isRepeat) {
            animateOpacity.setAttribute('repeatCount', 'indefinite');
        } else {
            animateOpacity.setAttribute('fill', 'freeze');
        }
        cloneUseElement.appendChild(animateOpacity);

        const etaniAnimate = document.createElement('div');
        etaniAnimate.className = 'etaniAnimate';
        etaniAnimate.setAttribute('data-type', 'opacity');

        const nameSpan = document.createElement('span');
        nameSpan.className = 'etaniAnimateName';
        nameSpan.textContent = 'opacity';

        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) {
                const animateOpacity = etani_clone.querySelector(`.etdrop use[href="#${useId}"] animate[attributeName="opacity"]`);
                if (animateOpacity) {
                    if (newDur > 0) {
                        animateOpacity.setAttribute('dur', `${newDur}s`);
                        if (isRepeat) {
                            animateOpacity.removeAttribute('fill');
                            animateOpacity.setAttribute('repeatCount', 'indefinite');
                        } else {
                            animateOpacity.removeAttribute('repeatCount');
                            animateOpacity.setAttribute('fill', 'freeze');
                        }
                    } else {
                        animateOpacity.removeAttribute('dur');
                        animateOpacity.removeAttribute('fill');
                        animateOpacity.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 useId = itemRight.closest('.etaniItem').querySelector('.tileid').textContent;
            const animateOpacity = etani_clone.querySelector(`.etdrop use[href="#${useId}"] animate[attributeName="opacity"]`);
            if (animateOpacity &&
                (animateOpacity.hasAttribute('fill') || animateOpacity.hasAttribute('repeatCount'))) {
                if (newValue === 'repeat') {
                    animateOpacity.removeAttribute('fill');
                    animateOpacity.setAttribute('repeatCount', 'indefinite');
                } else {
                    animateOpacity.removeAttribute('repeatCount');
                    animateOpacity.setAttribute('fill', 'freeze');
                }
            }
            updateEtaniResult();
        });
        etaniAnimateAttr.appendChild(frSpan);

        // Add the attribute add button
        const animateType = 'opacity';
        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('opacity', 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, 'opacity'));
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avCtrlDiv);
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

        etaniAnimate.appendChild(valueDiv);
        itemRight.appendChild(etaniAnimate);
    });

    updateEtaniResult();
}

// Handle click event for the 'Fill up values' button
function handleValueCtrlUpClick() {
    if (!etani_clone) return;

    const allAnimates = etani_clone.querySelectorAll('animateTransform, animate[attributeName="opacity"]');
    let maxValuesLength = 0;

    allAnimates.forEach(animate => {
        const values = animate.getAttribute('values')?.split(';') || [];
        maxValuesLength = Math.max(maxValuesLength, values.length);
    });

    allAnimates.forEach(animate => {
        const values = animate.getAttribute('values')?.split(';') || [];
        if (values.length < maxValuesLength && values.length > 0) {
            const lastValue = values[values.length - 1];
            while (values.length < maxValuesLength) {
                values.push(lastValue);
            }
            animate.setAttribute('values', values.join(';'));
            const useId = animate.closest('use')?.getAttribute('href')?.substring(1) || 
                          animate.closest('g[data-use-href]')?.getAttribute('data-use-href')?.substring(1);
            if (useId) {
                const etaniAnimate = document.querySelector(`.etaniItem:has(.tileid:where(:text("${useId}"))) .etaniAnimate[data-type="${animate.getAttribute('attributeName') === 'opacity' ? 'opacity' : 'transform'}"]`);
                if (etaniAnimate) {
                    updateDuration(animate, etaniAnimate, values.length);
                }
            }
        }
    });

    const etaniItemRights = document.querySelectorAll('.etaniItemRight');
    etaniItemRights.forEach(itemRight => {
        const useId = itemRight.closest('.etaniItem').querySelector('.tileid').textContent;
        ['transform', 'opacity'].forEach(animateType => {
            const etaniAV = itemRight.querySelector(`.etaniAnimate[data-type="${animateType}"] .etaniAV`);
            if (!etaniAV) return;

            const currentItems = etaniAV.querySelectorAll('.etaniAVItem');
            const currentValuesLength = currentItems.length;
            if (currentValuesLength >= maxValuesLength) return;

            const lastItem = currentItems[currentItems.length - 1];
            let lastValue = lastItem.textContent;

            for (let i = currentValuesLength; i < maxValuesLength; i++) {
                const newAVItem = document.createElement('span');
                newAVItem.className = 'etaniAVItem';
                newAVItem.addEventListener('click', (e) => handleAVItemClick(e, animateType));
                if (animateType === 'transform') {
                    newAVItem.textContent = lastValue; // Use the last letter instead of incrementing
                } else if (animateType === 'opacity') {
                    newAVItem.textContent = lastValue;
                }
                etaniAV.appendChild(newAVItem);
            }
        });
    });

    updateEtaniResult();
}

// Handle setting mode switch (Repeat/Freeze)
function handleModeChange(mode) {
    if (!etani_clone) return;

    const animates = etani_clone.querySelectorAll('animateTransform, animate[attributeName="opacity"]');
    const isRepeat = mode === 'repeat';
    const isFreeze = mode === 'freeze';
    const isMixed = mode === 'mixed';

    if (!isMixed) {
        animates.forEach(animate => {
            if (animate.hasAttribute('fill') || animate.hasAttribute('repeatCount')) {
                if (isRepeat) {
                    animate.removeAttribute('fill');
                    animate.setAttribute('repeatCount', 'indefinite');
                } else if (isFreeze) {
                    animate.removeAttribute('repeatCount');
                    animate.setAttribute('fill', 'freeze');
                }
            }
        });
    }

    document.querySelectorAll('.etaniMode span').forEach(span => {
        span.classList.remove('active');
    });
    document.querySelector(`.etaniMode${mode.charAt(0).toUpperCase() + mode.slice(1)}`).classList.add('active');

    const frElements = document.querySelectorAll('.etaniAnimateFR');
    frElements.forEach(fr => {
        fr.style.display = isMixed ? 'inline-block' : 'none';
    });

    updateEtaniResult();
}

// Create a single etaniItem in the UI
function createEtaniItem(tileid) {
    const etaniCol = document.querySelector('.etaniCol');
    if (!etaniCol) return;

    // Check if item already exists
    if (etaniCol.querySelector(`.etaniItem[data-use-id="${tileid}"]`)) {
        return;
    }

    const etaniItem = document.createElement('div');
    etaniItem.className = 'etaniItem';
    etaniItem.setAttribute('data-use-id', tileid);

    const etaniItemLeft = document.createElement('div');
    etaniItemLeft.className = 'etaniItemLeft';

    const etaniItemRight = document.createElement('div');
    etaniItemRight.className = 'etaniItemRight';
    
    // Use generateTileImage to get the thumbnail
    const base64Url = generateTileImage(tileid);
    
    if (base64Url) {
        const imageOuter = document.createElement('div');
        imageOuter.className = 'etaniItemImageOuter';
        
        const img = document.createElement('img');
        img.className = 'etaniItemImage';
        img.src = base64Url;
        img.alt = tileid;
        
        imageOuter.appendChild(img);
        etaniItemLeft.appendChild(imageOuter);
    }

    const tileidDiv = document.createElement('div');
    tileidDiv.className = 'tileid';
    tileidDiv.textContent = tileid;
    etaniItemLeft.appendChild(tileidDiv);

    etaniItem.appendChild(etaniItemLeft);
    etaniItem.appendChild(etaniItemRight);
    etaniCol.appendChild(etaniItem);
}

// Handle click event for the 'tiles' update button
function handleUpdateTilesClick(e) {
    e.preventDefault();
    if (!etani_clone) return;

    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) return;

    const originalDefs = originalSvg.querySelector('defs');
    const cloneDefs = etani_clone.querySelector('defs');
    const originalDrop = originalSvg.querySelector('.etdrop');
    const cloneDrop = etani_clone.querySelector('.etdrop');
    
    if (!originalDefs || !cloneDefs || !originalDrop || !cloneDrop) return;

    const originalTiles = originalDefs.querySelectorAll('g[id]');
    const cloneTilesMap = new Map(Array.from(cloneDefs.querySelectorAll('g[id]')).map(g => [g.id, g]));
    const originalUsesMap = new Map(Array.from(originalDrop.querySelectorAll('use[href]')).map(u => [u.getAttribute('href').substring(1), u]));
    const cloneUsesMap = new Map(Array.from(cloneDrop.querySelectorAll('use[href]')).map(u => [u.getAttribute('href').substring(1), u]));

    originalTiles.forEach(originalTileG => {
        const tileId = originalTileG.id;
        const cloneTileG = cloneTilesMap.get(tileId);
        const originalUse = originalUsesMap.get(tileId);
        const cloneUse = cloneUsesMap.get(tileId);

        if (cloneTileG) {
            // Update existing tile definition
            cloneTileG.replaceWith(originalTileG.cloneNode(true));
            
            // Update corresponding <use> transform
            if (originalUse && cloneUse) {
                const transform = originalUse.getAttribute('transform');
                if (transform) {
                    cloneUse.setAttribute('transform', transform);
                } else {
                    cloneUse.removeAttribute('transform');
                }
            }
        } else {
            // Add new tile definition
            cloneDefs.appendChild(originalTileG.cloneNode(true));
            
            // Add new <use> element
            if (originalUse) {
                cloneDrop.appendChild(originalUse.cloneNode(true));
            }
            
            // Add new etaniItem to UI
            createEtaniItem(tileId);
        }
    });

    // Logic from handleUpdateBoardClick (merging board update) (Inst 3)
    const originalSvg_board = document.getElementById('etmain');
    if (originalSvg_board) {
        const originalBoard = originalSvg_board.querySelector('.etdrop .etboard');
        const cloneBoard = etani_clone.querySelector('.etdrop .etboard');
    
        if (originalBoard && cloneBoard) {
            cloneBoard.replaceWith(originalBoard.cloneNode(true));
        }
    }
    // End of merged logic

    // Refresh all tile images as defs might have changed
    updateAllTileImages();
    updateEtaniResult();
}

// Create the internal animation control structure
function createEtaniInner(etaniouter) {
    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etani_clone = originalSvg.cloneNode(true);
    const etwaitElement = etani_clone.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    etani_clone.id = 'etmainani';

    const etaniinner = document.createElement('div');
    etaniinner.className = 'etaniinner';
    etaniinner.id = 'etaniinner';

    const etaniCtrl = document.createElement('div');
    etaniCtrl.className = 'etaniCtrl';
    
    const etaniContent = document.createElement('div');
    etaniContent.className = 'etaniContent';

    const contentHTMLLink = document.createElement('a');
    contentHTMLLink.className = 'etaniContentHTML';
    contentHTMLLink.textContent = 'HTML';
    contentHTMLLink.href = 'javascript:;';
    contentHTMLLink.addEventListener('click', handleContentHTMLClick);
    etaniContent.appendChild(contentHTMLLink);

    etaniCtrl.appendChild(etaniContent);

    const etaniUpdate = document.createElement('div');
    etaniUpdate.className = 'etaniUpdate';

    const updateTilesLink = document.createElement('a');
    updateTilesLink.className = 'etaniUpdateTiles';
    updateTilesLink.textContent = 'update'; // Renamed (Inst 3)
    updateTilesLink.href = 'javascript:;';
    updateTilesLink.addEventListener('click', handleUpdateTilesClick);
    etaniUpdate.appendChild(updateTilesLink);

    // Moved from etaniContent (Inst 4)
    const centerLink = document.createElement('a');
    centerLink.className = 'etaniCenter';
    centerLink.textContent = 'Center';
    centerLink.href = 'javascript:;';
    centerLink.addEventListener('click', handleCenterClick);
    etaniUpdate.appendChild(centerLink);

    etaniCtrl.appendChild(etaniUpdate);

    const etaniMode = document.createElement('div');
    etaniMode.className = 'etaniMode';

    const repeatRadio = document.createElement('span');
    repeatRadio.className = 'etaniModeRepeat active';
    repeatRadio.textContent = 'Repeat';
    repeatRadio.setAttribute('data-mode', 'repeat');
    repeatRadio.addEventListener('click', () => handleModeChange('repeat'));

    const freezeRadio = document.createElement('span');
    freezeRadio.className = 'etaniModeFreeze';
    freezeRadio.textContent = 'Freeze';
    freezeRadio.setAttribute('data-mode', 'freeze');
    freezeRadio.addEventListener('click', () => handleModeChange('freeze'));

    const mixedRadio = document.createElement('span');
    mixedRadio.className = 'etaniModeMixed';
    mixedRadio.textContent = 'Mixed';
    mixedRadio.setAttribute('data-mode', 'mixed');
    mixedRadio.addEventListener('click', () => handleModeChange('mixed'));

    etaniMode.appendChild(repeatRadio);
    etaniMode.appendChild(freezeRadio);
    etaniMode.appendChild(mixedRadio);
    etaniCtrl.appendChild(etaniMode);

    const etaniAllAppend = document.createElement('div');
    etaniAllAppend.className = 'etaniAllAppend';

    const transformLink = document.createElement('a'); 
    transformLink.className = 'etaniAllAppendTransform';
    transformLink.textContent = 'transform';
    transformLink.href = 'javascript:;'; 
    transformLink.addEventListener('click', handleAllAppendTransformClick);

    const opacityLink = document.createElement('a'); 
    opacityLink.className = 'etaniAllAppendOpacity';
    opacityLink.textContent = 'opacity';
    opacityLink.href = 'javascript:;'; 
    opacityLink.addEventListener('click', handleAllAppendOpacityClick);

    etaniAllAppend.appendChild(transformLink); 
    etaniAllAppend.appendChild(opacityLink); 
    etaniCtrl.appendChild(etaniAllAppend);

    const etaniValueCtrl = document.createElement('div');
    etaniValueCtrl.className = 'etaniValueCtrl';

    const valueCtrlUpLink = document.createElement('a');
    valueCtrlUpLink.className = 'etaniValueCtrlUp';
    valueCtrlUpLink.textContent = 'fill up values';
    valueCtrlUpLink.href = 'javascript:;';
    valueCtrlUpLink.addEventListener('click', handleValueCtrlUpClick);
    etaniValueCtrl.appendChild(valueCtrlUpLink);
    etaniCtrl.appendChild(etaniValueCtrl);

    const etaniCol = document.createElement('div');
    etaniCol.className = 'etaniCol';

    const etaniResult = document.createElement('div');
    etaniResult.className = 'etaniResult';
    
    const resultImg = document.createElement('img');
    resultImg.className = 'etaniResultImage';
    resultImg.alt = 'Rendered Ejtile Animation SVG';

    const resultDRDiv = document.createElement('div');
    resultDRDiv.className = 'etaniResultDR';

    const downloadLink = document.createElement('a');
    downloadLink.className = 'etaniResultDownload';
    downloadLink.textContent = 'Download SVG';
    downloadLink.href = 'javascript:;';

    const renameLink = document.createElement('a');
    renameLink.className = 'etaniResultRename';
    renameLink.textContent = 'Rename File';
    renameLink.href = 'javascript:;';
    
    const sizeSpan = document.createElement('span');
    sizeSpan.className = 'etaniResultSize';

    resultDRDiv.appendChild(downloadLink);
    resultDRDiv.appendChild(renameLink);
    
    etaniResult.appendChild(resultImg);
    etaniResult.appendChild(resultDRDiv);
    etaniResult.appendChild(sizeSpan);
    
    etaniinner.appendChild(etaniCtrl);
    etaniinner.appendChild(etaniCol);
    etaniinner.appendChild(etaniResult);

    etaniouter.appendChild(etaniinner);

    const etdropUses = document.querySelectorAll('.etdrop use');

    etdropUses.forEach((useElement) => {
        const tileid = useElement.getAttribute('href').substring(1);
        createEtaniItem(tileid);
    });
    
    updateEtaniResult();
}

// Toggle the visibility and content of the animation control panel
function toggleAnimation(event) {
    const etanibutton = event.currentTarget;
    const etaniouter = etanibutton.parentNode;
    const etaniinner = document.getElementById('etaniinner');

    if (etanibutton.textContent === 'Animate it') {
        etanibutton.textContent = 'Close Ejtile Ani';
        createEtaniInner(etaniouter);
    } else if (etanibutton.textContent === 'Close Ejtile Ani') {
        etanibutton.textContent = 'Animate it';
        if (etaniinner) {
            etaniinner.remove();
        }
        etani_clone = null;
        isCopyMode = false;
        isMoveMode = false;
        isDeleteMode = false;
        selectedMoveItem = null;
        lastClickTime = 0;
        lastClickedItem = null;
    }
}

// Create control buttons when animate is appending
function createControlButtons(animateType, useId) {
    const avCtrlDiv = document.createElement('div');
    avCtrlDiv.className = 'etaniAVCtrl';

    const buttons = [
        {
            className: 'etaniAVAdd',
            title: `Add ${animateType.charAt(0).toUpperCase() + animateType.slice(1)} Value`,
            svg: {
                lines: [
                    { x1: '12', y1: '5', x2: '12', y2: '19' },
                    { x1: '5', y1: '12', x2: '19', y2: '12' }
                ]
            },
            handler: (e) => handleAVAddClick(e, useId, animateType)
        },
        {
            className: 'etaniAVDelete',
            title: 'Delete Value',
            svg: {
                lines: [
                    { x1: '5', y1: '12', x2: '19', y2: '12' }
                ]
            },
            handler: (e) => handleAVDeleteToggle(e, animateType)
        },
        {
            className: 'etaniAVCopy',
            title: 'Copy Value',
            svg: {
                rect: { x: '9', y: '9', width: '13', height: '13', rx: '2', ry: '2' },
                path: { d: 'M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1' }
            },
            handler: (e) => handleAVCopyToggle(e, animateType)
        },
        {
            className: 'etaniAVMove',
            title: 'Move Value',
            svg: {
                path: { d: 'M 7,4 L 3,8 L 7,12 M 3,8 L 18,8 M 17,12 L 21,16 L 17,20 M 21,16 L 6,16' }
            },
            handler: (e) => handleAVMoveToggle(e, animateType)
        }
    ];

    buttons.forEach(button => {
        const span = document.createElement('span');
        span.className = button.className;
        span.title = button.title;
        span.setAttribute('data-type', animateType);
        span.addEventListener('click', button.handler);

        const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svg.setAttribute('width', '24');
        svg.setAttribute('height', '24');
        svg.setAttribute('viewBox', '0 0 24 24');
        svg.setAttribute('fill', 'none');
        svg.setAttribute('stroke', 'currentColor');
        svg.setAttribute('stroke-width', '1');
        svg.setAttribute('stroke-linecap', 'round');
        svg.setAttribute('stroke-linejoin', 'round');

        if (button.svg.lines) {
            button.svg.lines.forEach(line => {
                const lineElement = document.createElementNS('http://www.w3.org/2000/svg', 'line');
                Object.entries(line).forEach(([key, value]) => lineElement.setAttribute(key, value));
                svg.appendChild(lineElement);
            });
        }
        if (button.svg.rect) {
            const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
            Object.entries(button.svg.rect).forEach(([key, value]) => rect.setAttribute(key, value));
            svg.appendChild(rect);
        }
        if (button.svg.path) {
            const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
            Object.entries(button.svg.path).forEach(([key, value]) => path.setAttribute(key, value));
            svg.appendChild(path);
        }

        span.appendChild(svg);
        avCtrlDiv.appendChild(span);
    });

    return avCtrlDiv;
}

// Show dropdown menu for adding attributes
function showDropdown(e, animateType, useId) {
    const attrAddSpan = e.target;
    const existingDropdown = attrAddSpan.parentNode.querySelector('.etaniDropdown');
    if (existingDropdown) {
        // Toggle: if already open, remove it
        existingDropdown.remove();
        return;
    }

    const dropdown = document.createElement('div');
    dropdown.className = 'etaniDropdown';
    // Dynamically determine items based on existing attributes
    const attrParent = attrAddSpan.parentNode;
    const hasId = attrParent.querySelector('.etaniAnimateId');
    const hasBegin = attrParent.querySelector('.etaniAnimateBegin');
    const items = [];
    if (!hasId) items.push('id');
    if (!hasBegin) items.push('begin');
    items.push('other');
    items.forEach(item => {
        const div = document.createElement('div');
        div.className = 'etaniDropdownItem';
        div.textContent = item;
        div.addEventListener('click', () => {
            showWindow(item, animateType, useId);
            dropdown.remove(); // Remove after selecting item
        });
        dropdown.appendChild(div);
    });

    attrAddSpan.parentNode.style.position = 'relative'; // Ensure parent is relative for absolute positioning
    attrAddSpan.parentNode.appendChild(dropdown);
    dropdown.style.left = `${attrAddSpan.offsetLeft}px`;
    dropdown.style.top = `${attrAddSpan.offsetTop + attrAddSpan.offsetHeight}px`;
}

    // Show window for adding/editing attributes
    function showWindow(type, animateType, useId, existingSpan = null, otherAttrName = null) {
        const windowDiv = document.createElement('div');
        windowDiv.className = 'etaniWindow';

        const existingIds = getExistingIds();
        const isEdit = !!existingSpan;
        let input1, input2, existingIdsDiv, historyDiv, confirmBtn, deleteBtn, cancelBtn;

        if (type === 'id') {
            const label = document.createElement('label');
            label.textContent = 'Enter ID:';
            input1 = document.createElement('input');
            input1.type = 'text';
            if (isEdit) input1.value = existingSpan.textContent.replace('id=', '');
            windowDiv.appendChild(label);
            windowDiv.appendChild(input1);

            existingIdsDiv = createIdsDisplay(existingIds, false);
            windowDiv.appendChild(existingIdsDiv);
        } else if (type === 'begin') {
            const parsed = isEdit ? parseBeginValue(existingSpan.textContent) : { selected: [], plus: null, time: null };
            existingIdsDiv = createIdsDisplay(existingIds, true, parsed.selected);
            windowDiv.appendChild(existingIdsDiv);
            // Create a div wrapper for plus input with text
            const plusDiv = document.createElement('div');
            plusDiv.className = 'etaniAnimateBeginPlusOuter';
            plusDiv.textContent = 'plus: ';

            const plusInput = document.createElement('input');
            plusInput.type = 'number';
            plusInput.min = '0';
            plusInput.step = '0.1';
            plusInput.className = 'etaniAnimateBeginPlus';
            plusInput.style.width = '60px';
            plusInput.style.display = 'inline';
            if (isEdit) {
                const parsed = parseBeginValue(existingSpan.textContent);
                plusInput.value = parsed.plus || parsed.time || '';
            }
            plusDiv.appendChild(plusInput);

            const sText = document.createTextNode(' s');
            plusDiv.appendChild(sText);

            windowDiv.appendChild(plusDiv);
        } else if (type === 'other') {
            const label1 = document.createElement('label');
            label1.textContent = 'Attribute Name:';
            input1 = document.createElement('input');
            input1.type = 'text';
            if (isEdit) input1.value = otherAttrName;

            const label2 = document.createElement('label');
            label2.textContent = 'Attribute Value:';
            input2 = document.createElement('input');
            input2.type = 'text';
            if (isEdit) input2.value = existingSpan.textContent.split('=')[1];

            windowDiv.appendChild(label1);
            windowDiv.appendChild(input1);
            windowDiv.appendChild(label2);
            windowDiv.appendChild(input2);
        }

        const buttonsDiv = document.createElement('div');
        confirmBtn = document.createElement('button');
        confirmBtn.textContent = 'Confirm';
        confirmBtn.addEventListener('click', () => handleConfirm(type, animateType, useId, input1, input2, existingIdsDiv, existingSpan, windowDiv));
        buttonsDiv.appendChild(confirmBtn);

        if (isEdit) {
            deleteBtn = document.createElement('button');
            deleteBtn.textContent = 'Delete';
            deleteBtn.addEventListener('click', () => handleDelete(animateType, useId, type, otherAttrName, existingSpan, windowDiv));
            buttonsDiv.appendChild(deleteBtn);
        }

        cancelBtn = document.createElement('button');
        cancelBtn.textContent = 'Cancel';
        cancelBtn.addEventListener('click', () => document.body.removeChild(windowDiv));
        buttonsDiv.appendChild(cancelBtn);

        windowDiv.appendChild(buttonsDiv);
        document.body.appendChild(windowDiv);
    }

    // Create display for existing IDs with selection option
    function createIdsDisplay(existingIds, selectable, selected = []) {
        const div = document.createElement('div');
        div.className = 'etaniExistingIds';
        div.textContent = 'Existing IDs: ';

        if (selectable) {
            const zeroS = document.createElement('span');
            zeroS.className = 'etaniIdItem zero-s';
            zeroS.textContent = '0s';
            zeroS.classList.toggle('selected', selected.includes('0s'));
            zeroS.addEventListener('click', () => zeroS.classList.toggle('selected'));
            div.appendChild(zeroS);
        }

        existingIds.forEach(id => {
            const span = document.createElement('span');
            span.className = 'etaniIdItem';
            span.textContent = id;
            if (selectable) {
                span.addEventListener('click', () => {
                    if (span.classList.contains('selected')) {
                        // If already selected, deselect it
                        span.classList.remove('selected');
                    } else {
                        // Otherwise, deselect all other IDs and select this one
                        div.querySelectorAll('.etaniIdItem:not(.zero-s)').forEach(s => s.classList.remove('selected'));
                        span.classList.add('selected');
                    }
                });
                if (selected.includes(id)) span.classList.add('selected');
            }
            div.appendChild(span);
        });
        return div;
    }

    // Get all existing animation IDs globally, shared across types
    function getExistingIds() {
        const animates = etani_clone.querySelectorAll('animateTransform, animate[attributeName="opacity"]');
        return Array.from(animates)
            .map(animate => animate.getAttribute('id'))
            .filter(id => id);
    }

// Parse begin value to extract zeros, id, plus, and time
function parseBeginValue(beginStr) {
    const selected = [];
    let plus = null;
    let time = null;
    let id = null;

    // Remove 'begin=' prefix if present
    beginStr = beginStr.replace('begin=', '');

    // Case 1: "0s"
    if (beginStr === '0s') {
        selected.push('0s');
    } 
    // Case 2: "3s" (or any number s)
    else if (/^\d+(\.\d+)?s$/.test(beginStr)) {
        time = parseFloat(beginStr.replace('s', ''));
    } 
    // Case 3: "0s;id.end"
    else if (/^0s;[^.]+.end$/.test(beginStr)) {
        const parts = beginStr.split(';');
        selected.push('0s');
        id = parts[1].replace('.end', '');
        selected.push(id);
    } 
    // Case 4: "id.end"
    else if (/^[^.]+.end$/.test(beginStr)) {
        id = beginStr.replace('.end', '');
        selected.push(id);
    } 
    // Case 6: "0s;id.end+3s"
    else if (/^0s;[^.]+.end\+\d+(\.\d+)?s$/.test(beginStr)) {
        const parts = beginStr.split(';');
        selected.push('0s');
        const [idPart, plusPart] = parts[1].split('+');
        id = idPart.replace('.end', '');
        selected.push(id);
        plus = parseFloat(plusPart.replace('s', ''));
    }
    // Case 5: "id.end+3s"
    else if (/^[^.]+.end\+\d+(\.\d+)?s$/.test(beginStr)) {
        const [idPart, plusPart] = beginStr.split('+');
        id = idPart.replace('.end', '');
        selected.push(id);
        plus = parseFloat(plusPart.replace('s', ''));
    } 

    return { selected, plus, time };
}

    // Handle confirm button for adding/editing
    function handleConfirm(type, animateType, useId, input1, input2, existingIdsDiv, existingSpan, windowDiv) {
        let value, attrName;
        if (type === 'id') {
            value = input1.value.trim();
            // Validate: only allow alphanumeric and underscore, no spaces
            if (!value || !/^[a-zA-Z0-9_]+$/.test(value) || getExistingIds().includes(value)) {
                alert('ID must contain only letters, numbers, and underscores (no spaces)');
                return;
            }
            attrName = 'id';
        } else if (type === 'begin') {
            const selectedZero = existingIdsDiv.querySelector('.zero-s.selected');
            const selectedId = existingIdsDiv.querySelector('.etaniIdItem:not(.zero-s).selected');
            const plusInput = windowDiv.querySelector('.etaniAnimateBeginPlus');
            const plusValue = plusInput && plusInput.value ? parseFloat(plusInput.value) : null;
            let parts = [];
            if (selectedZero && selectedId) {
                parts.push('0s');
            } // Only push '0s' if there is a selectedId
            let idPart = selectedId ? `${selectedId.textContent}.end` : null;
            if (plusValue > 0) {
                if (idPart) {
                    idPart += `+${plusValue}s`;
                } else {
                    idPart = `${plusValue}s`; // If no id, use Xs directly, ignore 0s
                }
            }
            if (idPart) {
                parts.push(idPart);
            }
            value = parts.join(';');
            // Handle case where only '0s' is selected without id or plus
            if (!value && selectedZero) {
                value = '0s';
            }
            if (!value) return; // No selection
            attrName = 'begin';
        } else if (type === 'other') {
            attrName = input1.value.trim();
            value = input2.value.trim();
            if (!attrName || !value) return; // Empty
            // Check duplicate attributes (simplified, assume no duplicates for same attrName)
        }

        const animates = getAnimates(animateType, useId, attrName);
        animates.forEach(animate => animate.setAttribute(attrName, value));

        const attrAddSpan = existingSpan ? existingSpan.parentNode.querySelector('.etaniAnimateAttrAdd') : document.querySelector(`.etaniItem[data-use-id="${useId}"] .etaniAnimate[data-type="${animateType}"] .etaniAnimateAttrAdd`);
        const className = `etaniAnimate${type.charAt(0).toUpperCase() + type.slice(1)}`;
        const newSpan = existingSpan || document.createElement('span');
        newSpan.className = className;
        newSpan.textContent = type === 'other' ? `${attrName}=${value}` : `${attrName}=${value}`;
        if (!existingSpan) attrAddSpan.parentNode.insertBefore(newSpan, attrAddSpan);

        updateEtaniResult();
        document.body.removeChild(existingIdsDiv.closest('.etaniWindow'));
    }

    // Handle delete button
    function handleDelete(animateType, useId, type, otherAttrName, existingSpan, windowDiv) {
        const attrName = type === 'other' ? otherAttrName : type;
        const animates = getAnimates(animateType, useId);
        animates.forEach(animate => animate.removeAttribute(attrName));
        existingSpan.remove();
        updateEtaniResult();
        if (document.body.contains(windowDiv)) {
            document.body.removeChild(windowDiv);
        }
    }

    // Get animate elements for the specific useId and type
    function getAnimates(animateType, useId, attrType = null) {
        if (animateType === 'transform') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return [];

            if (attrType === 'id') {
                // Per original logic and example, 'id' applies only to the rotate animation
                return cloneUseElement.querySelectorAll('animateTransform[type="rotate"]') || [];
            } else {
                // Other attributes (like 'begin') apply to all three
                return cloneUseElement.querySelectorAll('animateTransform') || [];
            }
        } else {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            return cloneUseElement?.querySelectorAll('animate[attributeName="opacity"]') || [];
        }
    }

    // Edit existing attribute
    function editAttribute(span, animateType, useId, type, otherAttrName) {
        showWindow(type, animateType, useId, span, otherAttrName);
    }

// Generate base64 image for a tile
function generateTileImage(tileid) {
    const originalTile = document.querySelector(`defs g#${tileid}`);
    if (!originalTile) return null;

    const etdropUses = document.querySelectorAll('.etdrop use');
    const etwaitGroups = document.querySelectorAll('.etwait g');
    
    let targetUse = null;
    for (const group of etwaitGroups) {
        const useInGroup = group.querySelector(`use[href="#${tileid}"]`);
        if (useInGroup) {
            targetUse = useInGroup;
            break;
        }
    }
    
    let etwaittransform = '', etwaitfill = '', etwaitstroke = '', etwaitstrokeWidth = '';
    if (targetUse) {
        etwaittransform = targetUse.getAttribute('transform') || '';
        etwaitfill = targetUse.getAttribute('fill') || '';
        etwaitstroke = targetUse.getAttribute('stroke') || '';
        etwaitstrokeWidth = targetUse.getAttribute('stroke-width') || '';

        const scaleMatch = etwaittransform.match(/scale\(([^)]+)\)/);
        const rotateMatch = etwaittransform.match(/rotate\(([^)]+)\)/);
        const scalePart = scaleMatch ? scaleMatch[0] : '';
        const rotatePart = rotateMatch ? rotateMatch[0] : '';
        etwaittransform = `translate(20,20) ${scalePart} ${rotatePart}`.trim().replace(/\s+/g, ' ');
    }
    
    const tileclone = originalTile.cloneNode(true);
    tileclone.removeAttribute('id');
    if (etwaittransform) tileclone.setAttribute('transform', etwaittransform);
    if (etwaitfill) tileclone.setAttribute('fill', etwaitfill);
    if (etwaitstroke) tileclone.setAttribute('stroke', etwaitstroke);
    if (etwaitstrokeWidth) tileclone.setAttribute('stroke-width', etwaitstrokeWidth);
    
    const svgWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svgWrapper.setAttribute('width', '40');
    svgWrapper.setAttribute('height', '40');
    svgWrapper.setAttribute('version', '1.1');
    svgWrapper.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
    svgWrapper.setAttribute('xmlns:svg', 'http://www.w3.org/2000/svg');
    
    svgWrapper.appendChild(tileclone);
    
    const svgString = new XMLSerializer().serializeToString(svgWrapper);
    return svgToBase64(svgString);
}

// Update all tile images in .etaniItemLeft
function updateAllTileImages() {
    const etaniItems = document.querySelectorAll('.etaniItem');
    etaniItems.forEach(item => {
        const tileid = item.querySelector('.tileid').textContent;
        const imageOuter = item.querySelector('.etaniItemImageOuter');
        const img = imageOuter ? imageOuter.querySelector('.etaniItemImage') : null;
        
        if (img) {
            const base64Url = generateTileImage(tileid);
            if (base64Url) {
                img.src = base64Url;
            }
        }
    });
}

// Initialize the animation control panel on window load
window.addEventListener('load', () => {
    addDynamicStyles();

    const etmainouter = document.getElementById('etmainouter');
    if (etmainouter) {
        const etaniouter = document.createElement('div');
        etaniouter.className = 'etaniouter';

        const etanibutton = document.createElement('button');
        etanibutton.id = 'etanibutton';
        etanibutton.textContent = 'Animate it';
        etanibutton.addEventListener('click', toggleAnimation);

        etaniouter.appendChild(etanibutton);
        etmainouter.parentNode.insertBefore(etaniouter, etmainouter.nextSibling);
    } else {
        console.error('Div with ID "etmainouter" not found.');
    }
});
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3907
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 189 次
联系:

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

帖子 ejsoon »

對js代碼進行以下改動:

在為etani_clone加上id = etmainani的同時,也為etani_clone中的.etdrop加上id=etdropani。

在.etaniCtrl內新增div.etaniSelect,放在.etaniUpdate的後面,內含:
span.etaniSelectTiles{tiles}
span.etaniSelectMoving{moving}
span.etaniSelectBoard{board}

這三個是單選,默認選擇第一個。

當選擇第一個時,.etaniCol中的.etaniItem展示的是etani_clone中的.etdrop中的所有<use>,(但不包括.etboard內的<use>,.etboard也是.etdrop的子元素)。

當選擇第二個時,每隔1s就檢測#etmain中是否存在use.tilemoving,如果存在,則etaniItem只展示這個use,其餘隱藏。

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

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

帖子 ejsoon »

claude sonnet4.5

代码: 全选

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

## 改動1:為 `.etdrop` 添加 id

**位置**:在 `createEtaniInner` 函數中,找到設置 `etani_clone.id` 的位置

**原代碼**(約第1258行):
```javascript
etani_clone.id = 'etmainani';
```

**改為**:
```javascript
etani_clone.id = 'etmainani';
const etdropClone = etani_clone.querySelector('.etdrop');
if (etdropClone) {
    etdropClone.id = 'etdropani';
}
```

---

## 改動2:添加全局變量追蹤選擇狀態

**位置**:在文件頂部的全局變量區域(約第8行之後)

**添加**:
```javascript
let currentSelectMode = 'tiles'; // Track current selection mode: 'tiles', 'moving', or 'board'
let movingCheckInterval = null; // Interval for checking tilemoving
```

---

## 改動3:創建 `.etaniSelect` 區域

**位置**:在 `createEtaniInner` 函數中,找到 `etaniCtrl.appendChild(etaniUpdate);` 之後(約第1305行)

**在此行之後添加**:
```javascript
// Create etaniSelect section
const etaniSelect = document.createElement('div');
etaniSelect.className = 'etaniSelect';

const selectTiles = document.createElement('span');
selectTiles.className = 'etaniSelectTiles active';
selectTiles.textContent = 'tiles';
selectTiles.setAttribute('data-mode', 'tiles');
selectTiles.addEventListener('click', () => handleSelectChange('tiles'));

const selectMoving = document.createElement('span');
selectMoving.className = 'etaniSelectMoving';
selectMoving.textContent = 'moving';
selectMoving.setAttribute('data-mode', 'moving');
selectMoving.addEventListener('click', () => handleSelectChange('moving'));

const selectBoard = document.createElement('span');
selectBoard.className = 'etaniSelectBoard';
selectBoard.textContent = 'board';
selectBoard.setAttribute('data-mode', 'board');
selectBoard.addEventListener('click', () => handleSelectChange('board'));

etaniSelect.appendChild(selectTiles);
etaniSelect.appendChild(selectMoving);
etaniSelect.appendChild(selectBoard);
etaniCtrl.appendChild(etaniSelect);
```

---

## 改動4:添加 CSS 樣式

**位置**:在 `addDynamicStyles` 函數的 `styleSheet.textContent` 中,找到 `.etaniUpdate` 樣式之後(約第44行)

**在 `.etaniUpdate` 樣式後添加**:
```css
.etaniSelect {
    display: inline-block;
    vertical-align: top;
    padding: 5px;
    border: 1px solid #ccc;
    margin: 0 5px 5px 5px;
    text-align: left;
}

.etaniSelect span {
    display: inline-block;
    cursor: pointer;
    padding: 4px 8px;
    margin: 0 3px;
    font-size: 14px;
    border: 1px solid #888;
    background-color: #eee;
    color: #333;
    user-select: none;
}

.etaniSelect span.active {
    background-color: #008CBA;
    color: white;
    border-color: #008CBA;
}
```

---

## 改動5:添加處理選擇變更的函數

**位置**:在 `handleModeChange` 函數之後(約第1217行之後)

**添加新函數**:
```javascript
// Handle selection mode change (tiles/moving/board)
function handleSelectChange(mode) {
    currentSelectMode = mode;
    
    // Update active state
    document.querySelectorAll('.etaniSelect span').forEach(span => {
        span.classList.remove('active');
    });
    document.querySelector(`.etaniSelect${mode.charAt(0).toUpperCase() + mode.slice(1)}`).classList.add('active');
    
    // Clear existing interval if any
    if (movingCheckInterval) {
        clearInterval(movingCheckInterval);
        movingCheckInterval = null;
    }
    
    if (mode === 'tiles') {
        // Show all etaniItems except board tiles
        updateTilesDisplay();
    } else if (mode === 'moving') {
        // Start checking for tilemoving every 1 second
        movingCheckInterval = setInterval(() => {
            checkAndDisplayMovingTile();
        }, 1000);
        // Initial check
        checkAndDisplayMovingTile();
    } else if (mode === 'board') {
        // Show only board tiles (will be implemented later)
        updateBoardDisplay();
    }
}

// Update display to show all tiles (excluding board tiles)
function updateTilesDisplay() {
    const etaniItems = document.querySelectorAll('.etaniItem');
    const etboardUses = document.querySelectorAll('#etmain .etdrop .etboard use');
    const boardTileIds = Array.from(etboardUses).map(use => use.getAttribute('href')?.substring(1));
    
    etaniItems.forEach(item => {
        const tileid = item.querySelector('.tileid')?.textContent;
        if (boardTileIds.includes(tileid)) {
            item.style.display = 'none';
        } else {
            item.style.display = 'inline-block';
        }
    });
}

// Check for tilemoving and display only that tile
function checkAndDisplayMovingTile() {
    const movingUse = document.querySelector('#etmain use.tilemoving');
    const etaniItems = document.querySelectorAll('.etaniItem');
    
    if (movingUse) {
        const movingTileId = movingUse.getAttribute('href')?.substring(1);
        etaniItems.forEach(item => {
            const tileid = item.querySelector('.tileid')?.textContent;
            item.style.display = (tileid === movingTileId) ? 'inline-block' : 'none';
        });
    } else {
        // If no tilemoving found, hide all
        etaniItems.forEach(item => {
            item.style.display = 'none';
        });
    }
}

// Update display to show only board tiles
function updateBoardDisplay() {
    const etaniItems = document.querySelectorAll('.etaniItem');
    const etboardUses = document.querySelectorAll('#etmain .etdrop .etboard use');
    const boardTileIds = Array.from(etboardUses).map(use => use.getAttribute('href')?.substring(1));
    
    etaniItems.forEach(item => {
        const tileid = item.querySelector('.tileid')?.textContent;
        item.style.display = boardTileIds.includes(tileid) ? 'inline-block' : 'none';
    });
}
```

---

## 改動6:初始化時設置顯示狀態

**位置**:在 `createEtaniInner` 函數的末尾,`updateEtaniResult();` 之前(約第1407行)

**在 `updateEtaniResult();` 之前添加**:
```javascript
// Initialize display based on default selection mode
updateTilesDisplay();
```

---

## 改動7:清理資源

**位置**:在 `toggleAnimation` 函數中,關閉動畫面板時清理(約第1423行)

**找到**:
```javascript
} else if (etanibutton.textContent === 'Close Ejtile Ani') {
    etanibutton.textContent = 'Animate it';
    if (etaniinner) {
        etaniinner.remove();
    }
    etani_clone = null;
```

**改為**:
```javascript
} else if (etanibutton.textContent === 'Close Ejtile Ani') {
    etanibutton.textContent = 'Animate it';
    if (etaniinner) {
        etaniinner.remove();
    }
    // Clear moving check interval
    if (movingCheckInterval) {
        clearInterval(movingCheckInterval);
        movingCheckInterval = null;
    }
    etani_clone = null;
    currentSelectMode = 'tiles';
```

---

這些改動將實現您要求的功能:
1. 為 `etani_clone` 中的 `.etdrop` 添加 `id="etdropani"`
2. 創建三個選擇按鈕(tiles/moving/board)
3. tiles 模式顯示所有非 board 的 tiles
4. moving 模式每秒檢測 `use.tilemoving` 並只顯示該 tile
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3907
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 189 次
联系:

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

帖子 ejsoon »

按claude sonnet4.5改完之後,很多功能都不行了,比如加value

代码: 全选

// Global variable to hold the SVG clone and ensure state is maintained
let etani_clone = null;

// Track copy, move, and delete mode states, selected item, and last click time for double-click detection
let isCopyMode = false;
let isMoveMode = false;
let isDeleteMode = false;
let selectedMoveItem = null;
let lastClickTime = 0;
let lastClickedItem = null;
let currentSelectMode = 'tiles'; // Track current selection mode: 'tiles', 'moving', or 'board'
let movingCheckInterval = null; // Interval for checking tilemoving

// Add dynamic CSS styles to the document
function addDynamicStyles() {
    if (document.getElementById('dynamic-et-styles')) {
        return;
    }

    const styleSheet = document.createElement('style');
    styleSheet.id = 'dynamic-et-styles'; 
    styleSheet.textContent = `
        /* Styles for etaniouter and button */
        .etaniouter {
            margin-top: 10px;
            padding: 10px;
            border: 1px solid #ddd;
            background-color: #f5f5f5;
        }

        /* Styles for etaniinner, etaniCtrl, etaniCol, etaniResult */
        .etaniinner {
            margin-top: 10px;
        }
        
        .etaniCtrl {
            margin-bottom: 10px; 
            clear: both;
            padding: 5px;
            border: 1px solid #c0c0c0;
            text-align: center;
        }
        
        /* Styles for etaniContent, etaniMode, etaniAllAppend, etaniValueCtrl */
        .etaniContent, .etaniMode, .etaniAllAppend, .etaniValueCtrl, .etaniUpdate {
            display: inline-block;
            vertical-align: top;
            padding: 5px;
            border: 1px solid #ccc;
            margin: 0 5px 5px 5px;
            text-align: left;
        }

        .etaniUpdate {
            /* Properties merged above (Inst 2) */
            font-size: 14px;
        }

        .etaniSelect {
            display: inline-block;
            vertical-align: top;
            padding: 5px;
            border: 1px solid #ccc;
            margin: 0 5px 5px 5px;
            text-align: left;
        }

        .etaniSelect span {
            display: inline-block;
            cursor: pointer;
            padding: 4px 8px;
            margin: 0 3px;
            font-size: 14px;
            border: 1px solid #888;
            background-color: #eee;
            color: #333;
            user-select: none;
        }

        .etaniSelect span.active {
            background-color: #008CBA;
            color: white;
            border-color: #008CBA;
        }

        .etaniCtrl > div > a {
            display: inline-block;
            text-decoration: none;
            padding: 2px 8px;
            font-size: 14px;
            margin: 0 2px;
            cursor: pointer;
        }
        
        .etaniUpdateTiles {
            border: 1px solid #008CBA;
            color: #008CBA;
        }

        .etaniCol {
            border: 1px solid #aaa;
            padding: 5px;
            margin-bottom: 10px; 
            clear: both;
        }

        /* Styles for etaniResult */
        .etaniResult {
            text-align: center;
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #bbb;
            box-sizing: border-box;
        }
        
        /* Control and button styles */
        
        .etaniCenter {
            border: 1px solid green;
            color: green;
        }
        
        .etaniContentHTML {
            border: 1px solid #db3a32; /* Color updated (Inst 3) */
            color: #db3a32; /* Color updated (Inst 3) */
            /* margin-right removed (Inst 2) */
        }

        .etaniValueCtrlUp {
            border: 1px solid purple;
            color: purple;
        }

        /* .etaniAllAppend a (generic) styles removed (Inst 1) */

        /* Specific styles for etaniAllAppend links (Inst 3) */
        .etaniAllAppendTransform {
            border: 1px solid #2e36b9;
            color: #2e36b9;
        }

        .etaniAllAppendOpacity {
            border: 1px solid #b68942;
            color: #b68942;
        }
        
        /* Custom radio button styles */
        .etaniMode span {
            display: inline-block;
            cursor: pointer;
            padding: 4px 8px;
            margin: 0 3px;
            font-size: 14px;
            border: 1px solid #888;
            background-color: #eee;
            color: #333;
            user-select: none;
        }

        .etaniMode span.active {
            background-color: #008CBA;
            color: white;
            border-color: #008CBA;
        }

        /* Result and animation item styles */
        .etaniResultDR {
            text-align: center;
            margin-bottom: 10px;
        }

        .etaniResultDownload, .etaniResultRename {
            display: inline-block;
            margin-right: 15px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 16px; 
        }
        
        .etaniResultDownload {
            border: 1px solid blue;
            color: blue;
        }

        .etaniResultRename {
            border: 1px solid brown;
            color: brown;
        }

        .etaniResultImage {
            display: block;
            max-width: 480px;
            width: 100%;
            height: auto;
            margin: 0 auto 10px auto;
            border: 1px solid #000;
            box-sizing: border-box;
        }
        
        .etaniResultSize {
            display: inline-block;
            margin-left: 10px;
            font-size: 12px; 
            color: #555;
        }
        
        /* etaniItem structure */
        .etaniItem {
            min-height: 48px;
            border: 1px solid #ccc;
            box-sizing: border-box;
            width: 100%;
            margin-bottom: -1px;
            background-color: lightyellow;
            display: inline-block; /* Added (Inst 5) */
        }
        
        .etaniItemLeft {
            float: left; 
            width: 60px;
            min-height: 48px;
            padding: 2px 0;
            text-align: center;
        }
        
        .etaniItemImageOuter {
            width: 40px;
            height: 40px;
            margin: 0 auto;
        }
        
        .etaniItemImage {
            width: 100%;
            height: 100%;
            display: block;
        }
        
        .etaniItemRight {
            margin-left: 60px; 
            padding: 7px;
            min-height: 64px; 
            background-color: #fff;
        }

        .tileid {
            text-align: center;
            font-size: 12px; 
            word-break: break-all;
            padding-top: 2px;
        }
        
        /* Animation controls */
        .etaniAnimate {
            border: 1px solid #999;
            padding: 5px;
            margin-bottom: 5px;
        }

        .etaniAnimateAttr {
            margin-bottom: 5px;
        }

        .etaniAnimateName {
            display: inline-block;
            padding: 2px 5px;
            background-color: #555;
            color: white;
            margin-right: 10px;
            font-size: 12px;
        }

        .etaniAnimateAttr > span {
            cursor: pointer;
        }

        .etaniAnimateAttr > span:not(.etaniAnimateName) {
            display: inline-block;
            padding: 2px 5px;
            font-size: 12px;
            margin-right: 7px;
            box-sizing: border-box;
            border-width: 1px;
            border-style: solid;
        }

        .etaniAnimateAttrAdd {
            border-color: #2c8c12;
            color: #2c8c12;
        }

        .etaniAnimateId {
            border-color: #742bcc;
            color: #742bcc;
        }

        .etaniAnimateBegin {
            border-color: #b6533c;
            color: #b6533c;
        }

        .etaniAnimateOther {
            border-color: #1cadca;
            color: #1cadca;
        }

        /* Dropdown menu styles */
        .etaniDropdown {
            position: absolute;
            background-color: #fff;
            border: 1px solid #ccc;
            box-shadow: 0 4px 8px rgba(0,0,0,0.1);
            z-index: 1002;
        }

        .etaniDropdownItem {
            padding: 8px 12px;
            cursor: pointer;
            font-size: 12px;
        }

        .etaniDropdownItem:hover {
            background-color: #f0f0f0;
        }

        /* Window modal styles */
        .etaniWindow {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 15px;
            border: 1px solid #ccc;
            box-shadow: 0 4px 8px rgba(0,0,0,0.2);
            z-index: 1003;
            width: 300px;
        }

        .etaniWindow label, .etaniWindow input {
            display: block;
            margin-bottom: 10px;
        }

        .etaniWindow button {
            margin-right: 10px;
            padding: 5px 10px;
        }

        .etaniExistingIds, .etaniAnimateBeginPlusOuter {
            margin-bottom: 10px;
            font-size: 12px;
        }

        .etaniIdItem {
            cursor: pointer;
            padding: 2px 5px;
            margin-right: 5px;
            border: 1px solid #ddd;
            display: inline-block;
        }

        .etaniIdItem.selected {
            background-color: #008CBA;
            color: white;
        }

        .etaniIdItem.zero-s.selected {
            color: white;
        }

        .etaniIdItem.zero-s {
            color: #888;
        }

        .etaniAnimateDur {
            border-color: blue;
            color: blue;
        }

        .etaniAnimateFR {
            border-color: #78229f;
            color: #78229f;
        }

        .etaniAnimateValue {
            margin-top: 5px;
        }

        .etaniAVCtrl {
            display: inline-block;
            vertical-align: top;
            margin-right: 5px;
            margin-bottom: 3px;
        }
        
        .etaniAVCtrl svg {
            margin-left: -1px;
            margin-top: -1px;
        }

        .etaniAVCtrl span {
            display: inline-block;
            width: 24px;
            height: 24px;
            line-height: 24px;
            text-align: center;
            cursor: pointer;
            box-sizing: border-box;
            vertical-align: top;
            margin-right: 3px;
        }

        .etaniAVAdd {
            background-color: #a7fca7;
            border: 1px solid #71c371;
        }
        
        .etaniAVDelete {
            background-color: #ffcccc;
            border: 1px solid #cc3333;
        }

        .etaniAVDelete.deleting {
            background-color: #cc3333;
            color: white;
        }

        .etaniAVCopy {
            background-color: #ccccff;
            border: 1px solid #6666cc;
        }

        .etaniAVCopy.copying {
            background-color: #6666cc;
            color: white;
        }

        .etaniAVMove {
            background-color: #ffcc99;
            border: 1px solid #cc9966;
        }

        .etaniAVMove.moving {
            background-color: #cc9966;
            color: white;
        }
        
        .etaniAVLabel {
            font-size: 14px;
            margin-right: 5px;
        }

        .etaniAV {
            display: inline-block;
            vertical-align: top;
        }

        .etaniAVItem {
            display: inline-block;
            height: 24px;
            background-color: #ff9933;
            border: 1px dashed #00bfff;
            margin: 0 5px 3px;
            padding: 0 5px;
            box-sizing: border-box;
            cursor: pointer;
            position: relative;
            text-align: center;
            line-height: 24px;
            font-size: 12px;
            color: #333;
        }

        .etaniAVItem.deleting-target, .etaniAVItem.copying-target, .etaniAVItem.moving-target {
            background-color: #ff4d4d;
            border: 2px solid red;
        }

        .etaniAVItem.selected-move {
            background-color: #66ccff;
            border: 2px solid #0066cc;
        }
        
        /* Modal/popup styles */
        .modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.5);
            z-index: 1000;
        }
        .modal-content {
            position: fixed;
            width: 98%;
            height: 48%;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 10px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
            z-index: 1001;
            box-sizing: border-box;
        }
        .modal-content textarea {
            width: 100%;
            height: calc(100% - 50px);
            resize: none;
            border: 1px solid #ccc;
            font-size: 12px;
            box-sizing: border-box;
        }
        .modal-close {
            position: absolute;
            top: 0;
            right: 10px;
            font-size: 48px;
            line-height: 1; 
            cursor: pointer;
            color: #333;
        }
    `;

    document.head.appendChild(styleSheet);
}

// Find the first missing letter in the sequence starting from 'a'
function findFirstMissingLetter(existingLetters) {
    let letter = 'a';
    while (existingLetters.includes(letter)) {
        const code = letter.charCodeAt(0);
        if (code >= 97 && code < 122) { // lowercase a-y
            letter = String.fromCharCode(code + 1);
        } else if (code === 122) { // z -> A
            letter = 'A';
        } else if (code >= 65 && code < 90) { // uppercase A-Y
            letter = String.fromCharCode(code + 1);
        } else if (code === 90) { // Z -> a (loop back)
            letter = 'a';
        }
    }
    return letter;
}

// Extract a specific transform function and its value from a transform string
function extractTransformPart(transformString, type) {
    const regex = new RegExp(`(${type})\\(([^)]+)\\)`, 'i');
    const match = transformString.match(regex);
    if (match) {
        return { func: match[1], value: match[2].trim() };
    }
    return { func: '', value: '' };
}

// Parse transform string to get individual transform values
function parseTransform(transformString) {
    const defaultTransform = { translate: '0,0', scale: '1,1', rotate: '0' };
    if (!transformString) return defaultTransform;

    const transform = {};

    const getMatch = (type) => {
        const part = extractTransformPart(transformString, type).value;
        if (!part) return null;
        return part.split(/[,\s]+/).join(',');
    };

    transform.translate = getMatch('translate') || defaultTransform.translate;
    transform.scale = getMatch('scale') || defaultTransform.scale;
    if (transform.scale.split(',').length === 1) {
        transform.scale += `,${transform.scale}`;
    }
    transform.rotate = getMatch('rotate') || defaultTransform.rotate;

    return transform;
}

// Convert SVG string to a Base64 data URL
function svgToBase64(svgString) {
    const base64 = btoa(unescape(encodeURIComponent(svgString)));
    return `data:image/svg+xml;base64,${base64}`;
}

// Format byte size into human-readable string
function formatBytes(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

// Update the result section with the current state of etani_clone
function updateEtaniResult() {
    if (!etani_clone) return;

    const svgString = new XMLSerializer().serializeToString(etani_clone);
    const sizeInBytes = new Blob([svgString]).size;
    const base64Url = svgToBase64(svgString);
    
    const now = new Date();
    const yyyy = now.getFullYear();
    const mm = String(now.getMonth() + 1).padStart(2, '0');
    const dd = String(now.getDate()).padStart(2, '0');
    const hh = String(now.getHours()).padStart(2, '0');
    const mi = String(now.getMinutes()).padStart(2, '0');
    const ss = String(now.getSeconds()).padStart(2, '0');
    const defaultFilename = `ejtileAnimation_${yyyy}${mm}${dd}_${hh}${mi}${ss}.svg`;

    const imgElement = document.querySelector('.etaniResultImage');
    const downloadElement = document.querySelector('.etaniResultDownload');
    const renameElement = document.querySelector('.etaniResultRename');
    const sizeElement = document.querySelector('.etaniResultSize');

    if (imgElement && downloadElement && renameElement && sizeElement) {
        imgElement.src = base64Url;
        sizeElement.textContent = `Size: ${formatBytes(sizeInBytes)}`;
        downloadElement.href = base64Url;
        downloadElement.download = defaultFilename;
        
        renameElement.onclick = (e) => {
            e.preventDefault();
            let currentDownloadName = downloadElement.download;
            let newFilename = prompt("Enter new filename:", currentDownloadName);
            if (newFilename) {
                if (!newFilename.toLowerCase().endsWith('.svg')) {
                    newFilename += '.svg';
                }
                downloadElement.download = newFilename;
                alert(`Filename changed to: ${newFilename}`);
            }
        };
    }
}

// Handle the 'Center' button click to reset the .etdrop transform
function handleCenterClick(e) {
    e.preventDefault();
    if (etani_clone) {
        const etdrop = etani_clone.querySelector('.etdrop');
        if (etdrop) {
            etdrop.setAttribute('transform', 'translate(240,240) scale(1,1)');
            updateEtaniResult();
        }
    }
}

// Handle the HTML popup window
function handleContentHTMLClick(e) {
    e.preventDefault();
    if (!etani_clone) return;

    const overlay = document.createElement('div');
    overlay.className = 'modal-overlay';
    
    const content = document.createElement('div');
    content.className = 'modal-content';
    
    const close = document.createElement('span');
    close.className = 'modal-close';
    close.innerHTML = '&times;';
    
    const textarea = document.createElement('textarea');
    textarea.value = new XMLSerializer().serializeToString(etani_clone);

    const closeModal = () => {
        if (document.body.contains(overlay)) document.body.removeChild(overlay);
        if (document.body.contains(content)) document.body.removeChild(content);
    };
    close.onclick = closeModal;
    overlay.onclick = closeModal;

    content.appendChild(close);
    content.appendChild(textarea);
    
    document.body.appendChild(overlay);
    document.body.appendChild(content);
}

// Calculate additive transform or opacity value for a given type
function calculateAdditiveValue(type, originalValue, currentBaseValue, scaleValue = '1,1') {
    const roundToThreeDecimals = (value) => {
        return Math.round(value * 1000) / 1000;
    };
    
    if (type === 'rotate') {
        const ovAngle = parseFloat(currentBaseValue.trim());
        const cvAngle = parseFloat(originalValue.trim());
        return roundToThreeDecimals(cvAngle - ovAngle);
    } else if (type === 'scale') {
        const ovScales = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
        const cvScales = originalValue.split(',').map(c => parseFloat(c.trim()));
        const v1 = roundToThreeDecimals(cvScales[0] / ovScales[0]);
        const v2 = roundToThreeDecimals(cvScales.length > 1 ? (cvScales[1] / ovScales[1]) : (cvScales[0] / ovScales[0]));
        return v1 === v2 ? `${v1}` : `${v1},${v2}`;
    } else if (type === 'translate') {
        const ovCoords = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
        const cvCoords = originalValue.split(',').map(c => parseFloat(c.trim()));
        const scaleFactors = scaleValue.split(',').map(c => parseFloat(c.trim()));
        const v1 = roundToThreeDecimals(cvCoords[0] - (ovCoords[0] * scaleFactors[0]));
        const v2 = roundToThreeDecimals(cvCoords[1] - (ovCoords[1] * (scaleFactors.length > 1 ? scaleFactors[1] : scaleFactors[0])));
        return `${v1},${v2}`;
    } else if (type === 'opacity') {
        return originalValue;
    }
    return '';
}

// Update duration based on the number of values
function updateDuration(animate, etaniAnimate, newValuesLength) {
    const currentDur = parseFloat(etaniAnimate.querySelector('.etaniAnimateDur').textContent.replace('dur: ', '').replace('s', ''));
    const isIntegerDur = Number.isInteger(currentDur);
    const newDur = isIntegerDur ? (newValuesLength > 1 ? newValuesLength - 1 : 0) : currentDur;
    if (newDur > 0) {
        animate.setAttribute('dur', `${newDur}s`);
    } else {
        animate.removeAttribute('dur');
    }

    etaniAnimate.querySelector('.etaniAnimateDur').textContent = `dur: ${newDur}s`;
}

// Handle click event for the '+' button for transform or opacity
function handleAVAddClick(e, useElementId, animateType = 'transform') {
    e.preventDefault();
    if (!etani_clone) return;

    // Reset all modes before adding a new value
    resetModes(animateType);

    const etaniItemRight = e.currentTarget.closest('.etaniItemRight');
    const etaniAV = etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"] .etaniAV`);
    const etaniAnimate = etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`);

    if (animateType === 'transform') {
        // Get the original <use> element from #etmain to read its current transform
        const originalUseElement = document.querySelector(`#etmain .etdrop use[href="#${useElementId}"]`);
        if (!originalUseElement) return;

        const originalTransformString = originalUseElement.getAttribute('transform') || '';
        const originalTransforms = parseTransform(originalTransformString);

        // Get the corresponding <use> element in the clone
        const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useElementId}"]`);
        if (!cloneUseElement) return;

        // Find all animateTransform elements inside the cloned <use>
        const animates = cloneUseElement.querySelectorAll('animateTransform');
        
        animates.forEach(animate => {
            const type = animate.getAttribute('type').toLowerCase();
            let currentValueString = animate.getAttribute('values') || '';
            let newValue = '';
            
            // Get the new value directly from the parsed original transform
            if (type === 'translate') {
                newValue = originalTransforms.translate;
            } else if (type === 'scale') {
                newValue = originalTransforms.scale;
            } else if (type === 'rotate') {
                newValue = originalTransforms.rotate;
            }
            
            const newValueString = (currentValueString ? currentValueString + ';' : '') + newValue;
            animate.setAttribute('values', newValueString);
            updateDuration(animate, etaniAnimate, newValueString.split(';').length);
        });
    } else if (animateType === 'opacity') {
        const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useElementId}"]`);
        if (!cloneUseElement) return;

        const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
        if (!animateOpacity) return;

        const currentValueString = animateOpacity.getAttribute('values') || '';
        const values = currentValueString ? currentValueString.split(';') : [];
        const newValue = values.length > 0 ? values[values.length - 1] : '1';
        const newValueString = (currentValueString ? currentValueString + ';' : '') + newValue;
        animateOpacity.setAttribute('values', newValueString);
        updateDuration(animateOpacity, etaniAnimate, newValueString.split(';').length);
    }

    // Add new etaniAVItem with appropriate label
    if (etaniAV && etaniAnimate) {
        const newAVItem = document.createElement('span');
        newAVItem.className = 'etaniAVItem';
        newAVItem.addEventListener('click', (e) => handleAVItemClick(e, animateType));

        const existingItems = etaniAV.querySelectorAll('.etaniAVItem');
        if (animateType === 'transform') {
            const existingLetters = Array.from(existingItems).map(item => item.textContent);
            newAVItem.textContent = findFirstMissingLetter(existingLetters);
        } else if (animateType === 'opacity') {
            const values = animateType === 'opacity' ? 
                etaniAnimate.querySelector('animate[attributeName="opacity"]')?.getAttribute('values')?.split(';') || ['1'] :
                ['1'];
            newAVItem.textContent = values[values.length - 1];
        }

        etaniAV.appendChild(newAVItem);
    }

    updateEtaniResult();
}

// Reset all mode states for a specific animateType
function resetModes(animateType, excludeMode = null) {
    const etaniCol = document.querySelector('.etaniCol');
    const avItems = document.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);

    if (excludeMode !== 'delete') {
        isDeleteMode = false;
        etaniCol.classList.remove(`deleting-mode-${animateType}`);
        document.querySelectorAll(`.etaniAVDelete[data-type="${animateType}"]`).forEach(btn => {
            btn.classList.remove('deleting');
            btn.title = 'Delete Value';
        });
    }

    if (excludeMode !== 'copy') {
        isCopyMode = false;
        etaniCol.classList.remove(`copying-mode-${animateType}`);
        document.querySelectorAll(`.etaniAVCopy[data-type="${animateType}"]`).forEach(btn => {
            btn.classList.remove('copying');
            btn.title = 'Copy Value';
        });
    }

    if (excludeMode !== 'move') {
        isMoveMode = false;
        selectedMoveItem = null;
        etaniCol.classList.remove(`moving-mode-${animateType}`);
        document.querySelectorAll(`.etaniAVMove[data-type="${animateType}"]`).forEach(btn => {
            btn.classList.remove('moving');
            btn.title = 'Move Value';
        });
    }

    avItems.forEach(item => {
        item.classList.remove('deleting-target', 'copying-target', 'moving-target', 'selected-move');
    });
}

// Handle click event for the '-' button to toggle deletion mode
function handleAVDeleteToggle(e, animateType = 'transform') {
    e.preventDefault();
    const deleteButton = e.currentTarget;
    resetModes(animateType, 'delete');
    isDeleteMode = !isDeleteMode;
    deleteButton.classList.toggle('deleting', isDeleteMode);
    
    const etaniItemRight = deleteButton.closest('.etaniItemRight');
    const etaniCol = deleteButton.closest('.etaniCol');
    
    const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
    
    avItems.forEach(item => {
        item.classList.toggle('deleting-target', isDeleteMode);
    });

    etaniCol.classList.toggle(`deleting-mode-${animateType}`, isDeleteMode);

    if (isDeleteMode) {
        deleteButton.title = "Click value item to delete (Click again to cancel)";
    } else {
        deleteButton.title = "Delete Value";
        
        avItems.forEach(item => {
            item.classList.remove('deleting-target');
        });
    }
}

// Handle click event for the 'Copy' button to toggle copy mode
function handleAVCopyToggle(e, animateType) {
    e.preventDefault();
    const copyButton = e.currentTarget;
    resetModes(animateType, 'copy');
    isCopyMode = !isCopyMode;
    copyButton.classList.toggle('copying', isCopyMode);
    
    const etaniItemRight = copyButton.closest('.etaniItemRight');
    const etaniCol = copyButton.closest('.etaniCol');
    
    const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
    
    avItems.forEach(item => {
        item.classList.toggle('copying-target', isCopyMode);
    });

    etaniCol.classList.toggle(`copying-mode-${animateType}`, isCopyMode);

    if (isCopyMode) {
        copyButton.title = "Click value item to copy (Click again to cancel)";
    } else {
        copyButton.title = "Copy Value";
        
        avItems.forEach(item => {
            item.classList.remove('copying-target');
        });
    }
}

// Handle click event for the 'Move' button to toggle move mode
function handleAVMoveToggle(e, animateType) {
    e.preventDefault();
    const moveButton = e.currentTarget;
    resetModes(animateType, 'move');
    isMoveMode = !isMoveMode;
    moveButton.classList.toggle('moving', isMoveMode);
    
    const etaniItemRight = moveButton.closest('.etaniItemRight');
    const etaniCol = moveButton.closest('.etaniCol');
    
    const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
    
    avItems.forEach(item => {
        item.classList.toggle('moving-target', isMoveMode);
    });

    etaniCol.classList.toggle(`moving-mode-${animateType}`, isMoveMode);

    if (isMoveMode) {
        moveButton.title = "Click value item to select, then click another to move (Click again to cancel)";
    } else {
        moveButton.title = "Move Value";
        selectedMoveItem = null;
        
        avItems.forEach(item => {
            item.classList.remove('moving-target');
            item.classList.remove('selected-move');
        });
    }
}

// Handle click event for an .etaniAVItem in deletion, copy, or move mode
function handleAVItemClick(e, animateType = 'transform') {
    const item = e.currentTarget;
    const itemIndex = Array.from(item.parentNode.children).indexOf(item);
    const etaniItemRight = item.closest('.etaniItemRight');
    const etaniCol = item.closest('.etaniCol');
    const useId = etaniItemRight.closest('.etaniItem').querySelector('.tileid').textContent;

    const currentTime = Date.now();
    const isDoubleClick = (item === lastClickedItem && (currentTime - lastClickTime) < 2000);

    lastClickTime = currentTime;
    lastClickedItem = item;

    if (etaniCol.classList.contains(`moving-mode-${animateType}`)) {
        const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
        
        if (!selectedMoveItem) {
            selectedMoveItem = item;
            item.classList.add('selected-move');
            return;
        } else if (selectedMoveItem === item) {
            selectedMoveItem.classList.remove('selected-move');
            selectedMoveItem = null;
            return;
        } else {
            const targetIndex = itemIndex;
            const sourceIndex = Array.from(item.parentNode.children).indexOf(selectedMoveItem);
            
            const parent = item.parentNode;
            if (sourceIndex < targetIndex) {
                if (item.nextSibling) {
                    parent.insertBefore(selectedMoveItem, item.nextSibling);
                } else {
                    parent.appendChild(selectedMoveItem);
                }
            } else {
                parent.insertBefore(selectedMoveItem, item);
            }

            if (animateType === 'transform') {
                const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
                if (!cloneUseElement) return;

                const allAnimates = cloneUseElement.querySelectorAll('animateTransform');
                
                allAnimates.forEach(animate => {
                    const values = animate.getAttribute('values').split(';');
                    const valueToMove = values[sourceIndex];
                    values.splice(sourceIndex, 1);
                    if (sourceIndex < targetIndex) {
                        values.splice(targetIndex, 0, valueToMove);
                    } else {
                        values.splice(targetIndex, 0, valueToMove);
                    }
                    animate.setAttribute('values', values.join(';'));
                });
            } else if (animateType === 'opacity') {
                const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
                if (!cloneUseElement) return;

                const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
                if (!animateOpacity) return;

                const values = animateOpacity.getAttribute('values').split(';');
                const valueToMove = values[sourceIndex];
                values.splice(sourceIndex, 1);
                if (sourceIndex < targetIndex) {
                    values.splice(targetIndex, 0, valueToMove);
                } else {
                    values.splice(targetIndex, 0, valueToMove);
                }
                animateOpacity.setAttribute('values', values.join(';'));
            }

            selectedMoveItem.classList.remove('selected-move');
            selectedMoveItem = null;
            updateEtaniResult();
            return;
        }
    }

    if (etaniCol.classList.contains(`copying-mode-${animateType}`)) {
        const etaniAV = item.parentNode;
        const newAVItem = document.createElement('span');
        newAVItem.className = 'etaniAVItem';
        newAVItem.textContent = item.textContent;
        newAVItem.addEventListener('click', (e) => handleAVItemClick(e, animateType));
        etaniAV.insertBefore(newAVItem, item.nextSibling);

        if (animateType === 'transform') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return;

            const allAnimates = cloneUseElement.querySelectorAll('animateTransform');
            
            allAnimates.forEach(animate => {
                const values = animate.getAttribute('values').split(';');
                if (itemIndex < values.length) {
                    values.splice(itemIndex + 1, 0, values[itemIndex]);
                    animate.setAttribute('values', values.join(';'));
                    updateDuration(animate, etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`), values.length);
                }
            });
        } else if (animateType === 'opacity') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return;

            const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
            if (!animateOpacity) return;

            const values = animateOpacity.getAttribute('values').split(';');
            if (itemIndex < values.length) {
                values.splice(itemIndex + 1, 0, values[itemIndex]);
                animateOpacity.setAttribute('values', values.join(';'));
                updateDuration(animateOpacity, etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`), values.length);
            }
        }

        updateEtaniResult();
        return;
    }

    if (etaniCol.classList.contains(`deleting-mode-${animateType}`)) {
        const deleteButton = etaniItemRight.querySelector(`.etaniAVDelete[data-type="${animateType}"]`);
        const etaniAnimate = etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`);
        
        if (animateType === 'transform') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return;

            const allAnimates = cloneUseElement.querySelectorAll('animateTransform');
            let newValuesLength = 0;
            
            allAnimates.forEach((animate, index) => {
                const values = animate.getAttribute('values').split(';');
                
                if (itemIndex < values.length) {
                    values.splice(itemIndex, 1);
                    newValuesLength = values.length; // All animations will have the same new length
                    
                    if (newValuesLength > 0) {
                        animate.setAttribute('values', values.join(';'));
                        // Only update duration on the first animate to avoid multiple calls
                        if (index === 0) { 
                            updateDuration(animate, etaniAnimate, newValuesLength);
                        }
                    }
                }
            });

            // If all values were removed, clean up
            if (newValuesLength === 0 && allAnimates.length > 0) {
                allAnimates.forEach(anim => anim.remove());
                etaniAnimate.remove();
            }
        } else if (animateType === 'opacity') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return;

            const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
            if (!animateOpacity) return;

            const values = animateOpacity.getAttribute('values').split(';');
            if (itemIndex < values.length) {
                values.splice(itemIndex, 1);
                if (values.length === 0) {
                    // Remove the opacity animation
                    animateOpacity.remove();
                    etaniAnimate.remove();
                } else {
                    animateOpacity.setAttribute('values', values.join(';'));
                    updateDuration(animateOpacity, etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`), values.length);
                }
            }
        }

        item.remove();

        const remainingItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
        
        if (remainingItems.length === 0) {
            // The .etaniAnimate element (containing the deleteButton) was just removed
            // because it was the last item.
            // We cannot .click() the button, as it's no longer in the DOM.
            // Instead, we must manually reset the delete mode globally.
            resetModes(animateType);
        } else {
            // If items remain, just re-apply the 'deleting-target' class to them.
            remainingItems.forEach(item => {
                item.classList.add('deleting-target'); 
            });
        }

        updateEtaniResult();
        
    } else if (animateType === 'opacity' && !etaniCol.classList.contains(`deleting-mode-${animateType}`) && !etaniCol.classList.contains(`copying-mode-${animateType}`)) {
        const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;

        const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
        if (!animateOpacity) return;

        const values = animateOpacity.getAttribute('values').split(';');
        if (itemIndex >= values.length) return;

        if (isDoubleClick) {
            const newValue = prompt("Enter opacity value (0 to 1):", values[itemIndex]);
            if (newValue !== null && !isNaN(newValue) && newValue >= 0 && newValue <= 1) {
                values[itemIndex] = newValue;
                item.textContent = newValue;
                animateOpacity.setAttribute('values', values.join(';'));
                updateEtaniResult();
            }
        } else {
            values[itemIndex] = values[itemIndex] === '0' ? '1' : '0';
            item.textContent = values[itemIndex];
            animateOpacity.setAttribute('values', values.join(';'));
            updateEtaniResult();
        }
    }
}

// Handle click event for the 'Transform' button
function handleAllAppendTransformClick() {
    if (!etani_clone) 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 transform animation already exists
        if (itemRight.querySelector('.etaniAnimate[data-type="transform"]')) return;

        let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;

        // 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));

        // --- Start of HTML control creation ---
        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';
        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) {
                // Find animates inside the <use> element
                const animates = etani_clone.querySelectorAll(`.etdrop use[href="#${useId}"] animateTransform`);
                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 useId = itemRight.closest('.etaniItem').querySelector('.tileid').textContent;
            // Find animates inside the <use> element
            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';
        
        const avItemSpan = document.createElement('span');
        avItemSpan.className = 'etaniAVItem';
        avItemSpan.textContent = 'a';
        avItemSpan.addEventListener('click', (e) => handleAVItemClick(e, 'transform'));
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avCtrlDiv);
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

        etaniAnimate.appendChild(valueDiv);
        itemRight.appendChild(etaniAnimate);
    });

    updateEtaniResult();
}

// Handle click event for the 'Opacity' button
function handleAllAppendOpacityClick() {
    if (!etani_clone) 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;

        if (itemRight.querySelector('.etaniAnimate[data-type="opacity"]')) return;

        let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;

        const animateOpacity = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
        animateOpacity.setAttribute('attributeName', 'opacity');
        animateOpacity.setAttribute('values', '1');
        if (isRepeat) {
            animateOpacity.setAttribute('repeatCount', 'indefinite');
        } else {
            animateOpacity.setAttribute('fill', 'freeze');
        }
        cloneUseElement.appendChild(animateOpacity);

        const etaniAnimate = document.createElement('div');
        etaniAnimate.className = 'etaniAnimate';
        etaniAnimate.setAttribute('data-type', 'opacity');

        const nameSpan = document.createElement('span');
        nameSpan.className = 'etaniAnimateName';
        nameSpan.textContent = 'opacity';

        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) {
                const animateOpacity = etani_clone.querySelector(`.etdrop use[href="#${useId}"] animate[attributeName="opacity"]`);
                if (animateOpacity) {
                    if (newDur > 0) {
                        animateOpacity.setAttribute('dur', `${newDur}s`);
                        if (isRepeat) {
                            animateOpacity.removeAttribute('fill');
                            animateOpacity.setAttribute('repeatCount', 'indefinite');
                        } else {
                            animateOpacity.removeAttribute('repeatCount');
                            animateOpacity.setAttribute('fill', 'freeze');
                        }
                    } else {
                        animateOpacity.removeAttribute('dur');
                        animateOpacity.removeAttribute('fill');
                        animateOpacity.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 useId = itemRight.closest('.etaniItem').querySelector('.tileid').textContent;
            const animateOpacity = etani_clone.querySelector(`.etdrop use[href="#${useId}"] animate[attributeName="opacity"]`);
            if (animateOpacity &&
                (animateOpacity.hasAttribute('fill') || animateOpacity.hasAttribute('repeatCount'))) {
                if (newValue === 'repeat') {
                    animateOpacity.removeAttribute('fill');
                    animateOpacity.setAttribute('repeatCount', 'indefinite');
                } else {
                    animateOpacity.removeAttribute('repeatCount');
                    animateOpacity.setAttribute('fill', 'freeze');
                }
            }
            updateEtaniResult();
        });
        etaniAnimateAttr.appendChild(frSpan);

        // Add the attribute add button
        const animateType = 'opacity';
        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('opacity', 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, 'opacity'));
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avCtrlDiv);
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

        etaniAnimate.appendChild(valueDiv);
        itemRight.appendChild(etaniAnimate);
    });

    updateEtaniResult();
}

// Handle click event for the 'Fill up values' button
function handleValueCtrlUpClick() {
    if (!etani_clone) return;

    const allAnimates = etani_clone.querySelectorAll('animateTransform, animate[attributeName="opacity"]');
    let maxValuesLength = 0;

    allAnimates.forEach(animate => {
        const values = animate.getAttribute('values')?.split(';') || [];
        maxValuesLength = Math.max(maxValuesLength, values.length);
    });

    allAnimates.forEach(animate => {
        const values = animate.getAttribute('values')?.split(';') || [];
        if (values.length < maxValuesLength && values.length > 0) {
            const lastValue = values[values.length - 1];
            while (values.length < maxValuesLength) {
                values.push(lastValue);
            }
            animate.setAttribute('values', values.join(';'));
            const useId = animate.closest('use')?.getAttribute('href')?.substring(1) || 
                          animate.closest('g[data-use-href]')?.getAttribute('data-use-href')?.substring(1);
            if (useId) {
                const etaniAnimate = document.querySelector(`.etaniItem:has(.tileid:where(:text("${useId}"))) .etaniAnimate[data-type="${animate.getAttribute('attributeName') === 'opacity' ? 'opacity' : 'transform'}"]`);
                if (etaniAnimate) {
                    updateDuration(animate, etaniAnimate, values.length);
                }
            }
        }
    });

    const etaniItemRights = document.querySelectorAll('.etaniItemRight');
    etaniItemRights.forEach(itemRight => {
        const useId = itemRight.closest('.etaniItem').querySelector('.tileid').textContent;
        ['transform', 'opacity'].forEach(animateType => {
            const etaniAV = itemRight.querySelector(`.etaniAnimate[data-type="${animateType}"] .etaniAV`);
            if (!etaniAV) return;

            const currentItems = etaniAV.querySelectorAll('.etaniAVItem');
            const currentValuesLength = currentItems.length;
            if (currentValuesLength >= maxValuesLength) return;

            const lastItem = currentItems[currentItems.length - 1];
            let lastValue = lastItem.textContent;

            for (let i = currentValuesLength; i < maxValuesLength; i++) {
                const newAVItem = document.createElement('span');
                newAVItem.className = 'etaniAVItem';
                newAVItem.addEventListener('click', (e) => handleAVItemClick(e, animateType));
                if (animateType === 'transform') {
                    newAVItem.textContent = lastValue; // Use the last letter instead of incrementing
                } else if (animateType === 'opacity') {
                    newAVItem.textContent = lastValue;
                }
                etaniAV.appendChild(newAVItem);
            }
        });
    });

    updateEtaniResult();
}

// Handle setting mode switch (Repeat/Freeze)
function handleModeChange(mode) {
    if (!etani_clone) return;

    const animates = etani_clone.querySelectorAll('animateTransform, animate[attributeName="opacity"]');
    const isRepeat = mode === 'repeat';
    const isFreeze = mode === 'freeze';
    const isMixed = mode === 'mixed';

    if (!isMixed) {
        animates.forEach(animate => {
            if (animate.hasAttribute('fill') || animate.hasAttribute('repeatCount')) {
                if (isRepeat) {
                    animate.removeAttribute('fill');
                    animate.setAttribute('repeatCount', 'indefinite');
                } else if (isFreeze) {
                    animate.removeAttribute('repeatCount');
                    animate.setAttribute('fill', 'freeze');
                }
            }
        });
    }

    document.querySelectorAll('.etaniMode span').forEach(span => {
        span.classList.remove('active');
    });
    document.querySelector(`.etaniMode${mode.charAt(0).toUpperCase() + mode.slice(1)}`).classList.add('active');

    const frElements = document.querySelectorAll('.etaniAnimateFR');
    frElements.forEach(fr => {
        fr.style.display = isMixed ? 'inline-block' : 'none';
    });

    updateEtaniResult();
}

// Handle selection mode change (tiles/moving/board)
function handleSelectChange(mode) {
    currentSelectMode = mode;
    
    // Update active state
    document.querySelectorAll('.etaniSelect span').forEach(span => {
        span.classList.remove('active');
    });
    document.querySelector(`.etaniSelect${mode.charAt(0).toUpperCase() + mode.slice(1)}`).classList.add('active');
    
    // Clear existing interval if any
    if (movingCheckInterval) {
        clearInterval(movingCheckInterval);
        movingCheckInterval = null;
    }
    
    if (mode === 'tiles') {
        // Show all etaniItems except board tiles
        updateTilesDisplay();
    } else if (mode === 'moving') {
        // Start checking for tilemoving every 1 second
        movingCheckInterval = setInterval(() => {
            checkAndDisplayMovingTile();
        }, 1000);
        // Initial check
        checkAndDisplayMovingTile();
    } else if (mode === 'board') {
        // Show only board tiles (will be implemented later)
        updateBoardDisplay();
    }
}

// Update display to show all tiles (excluding board tiles)
function updateTilesDisplay() {
    const etaniItems = document.querySelectorAll('.etaniItem');
    const etboardUses = document.querySelectorAll('#etmain .etdrop .etboard use');
    const boardTileIds = Array.from(etboardUses).map(use => use.getAttribute('href')?.substring(1));
    
    etaniItems.forEach(item => {
        const tileid = item.querySelector('.tileid')?.textContent;
        if (boardTileIds.includes(tileid)) {
            item.style.display = 'none';
        } else {
            item.style.display = 'inline-block';
        }
    });
}

// Check for tilemoving and display only that tile
function checkAndDisplayMovingTile() {
    const movingUse = document.querySelector('#etmain use.tilemoving');
    const etaniItems = document.querySelectorAll('.etaniItem');
    
    if (movingUse) {
        const movingTileId = movingUse.getAttribute('href')?.substring(1);
        etaniItems.forEach(item => {
            const tileid = item.querySelector('.tileid')?.textContent;
            item.style.display = (tileid === movingTileId) ? 'inline-block' : 'none';
        });
    } else {
        // If no tilemoving found, hide all
        etaniItems.forEach(item => {
            item.style.display = 'none';
        });
    }
}

// Update display to show only board tiles
function updateBoardDisplay() {
    const etaniItems = document.querySelectorAll('.etaniItem');
    const etboardUses = document.querySelectorAll('#etmain .etdrop .etboard use');
    const boardTileIds = Array.from(etboardUses).map(use => use.getAttribute('href')?.substring(1));
    
    etaniItems.forEach(item => {
        const tileid = item.querySelector('.tileid')?.textContent;
        item.style.display = boardTileIds.includes(tileid) ? 'inline-block' : 'none';
    });
}

// Create a single etaniItem in the UI
function createEtaniItem(tileid) {
    const etaniCol = document.querySelector('.etaniCol');
    if (!etaniCol) return;

    // Check if item already exists
    if (etaniCol.querySelector(`.etaniItem[data-use-id="${tileid}"]`)) {
        return;
    }

    const etaniItem = document.createElement('div');
    etaniItem.className = 'etaniItem';
    etaniItem.setAttribute('data-use-id', tileid);

    const etaniItemLeft = document.createElement('div');
    etaniItemLeft.className = 'etaniItemLeft';

    const etaniItemRight = document.createElement('div');
    etaniItemRight.className = 'etaniItemRight';
    
    // Use generateTileImage to get the thumbnail
    const base64Url = generateTileImage(tileid);
    
    if (base64Url) {
        const imageOuter = document.createElement('div');
        imageOuter.className = 'etaniItemImageOuter';
        
        const img = document.createElement('img');
        img.className = 'etaniItemImage';
        img.src = base64Url;
        img.alt = tileid;
        
        imageOuter.appendChild(img);
        etaniItemLeft.appendChild(imageOuter);
    }

    const tileidDiv = document.createElement('div');
    tileidDiv.className = 'tileid';
    tileidDiv.textContent = tileid;
    etaniItemLeft.appendChild(tileidDiv);

    etaniItem.appendChild(etaniItemLeft);
    etaniItem.appendChild(etaniItemRight);
    etaniCol.appendChild(etaniItem);
}

// Handle click event for the 'tiles' update button
function handleUpdateTilesClick(e) {
    e.preventDefault();
    if (!etani_clone) return;

    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) return;

    const originalDefs = originalSvg.querySelector('defs');
    const cloneDefs = etani_clone.querySelector('defs');
    const originalDrop = originalSvg.querySelector('.etdrop');
    const cloneDrop = etani_clone.querySelector('.etdrop');
    
    if (!originalDefs || !cloneDefs || !originalDrop || !cloneDrop) return;

    const originalTiles = originalDefs.querySelectorAll('g[id]');
    const cloneTilesMap = new Map(Array.from(cloneDefs.querySelectorAll('g[id]')).map(g => [g.id, g]));
    const originalUsesMap = new Map(Array.from(originalDrop.querySelectorAll('use[href]')).map(u => [u.getAttribute('href').substring(1), u]));
    const cloneUsesMap = new Map(Array.from(cloneDrop.querySelectorAll('use[href]')).map(u => [u.getAttribute('href').substring(1), u]));

    originalTiles.forEach(originalTileG => {
        const tileId = originalTileG.id;
        const cloneTileG = cloneTilesMap.get(tileId);
        const originalUse = originalUsesMap.get(tileId);
        const cloneUse = cloneUsesMap.get(tileId);

        if (cloneTileG) {
            // Update existing tile definition
            cloneTileG.replaceWith(originalTileG.cloneNode(true));
            
            // Update corresponding <use> transform
            if (originalUse && cloneUse) {
                const transform = originalUse.getAttribute('transform');
                if (transform) {
                    cloneUse.setAttribute('transform', transform);
                } else {
                    cloneUse.removeAttribute('transform');
                }
            }
        } else {
            // Add new tile definition
            cloneDefs.appendChild(originalTileG.cloneNode(true));
            
            // Add new <use> element
            if (originalUse) {
                cloneDrop.appendChild(originalUse.cloneNode(true));
            }
            
            // Add new etaniItem to UI
            createEtaniItem(tileId);
        }
    });

    // Logic from handleUpdateBoardClick (merging board update) (Inst 3)
    const originalSvg_board = document.getElementById('etmain');
    if (originalSvg_board) {
        const originalBoard = originalSvg_board.querySelector('.etdrop .etboard');
        const cloneBoard = etani_clone.querySelector('.etdrop .etboard');
    
        if (originalBoard && cloneBoard) {
            cloneBoard.replaceWith(originalBoard.cloneNode(true));
        }
    }
    // End of merged logic

    // Refresh all tile images as defs might have changed
    updateAllTileImages();
    updateEtaniResult();
}

// Create the internal animation control structure
function createEtaniInner(etaniouter) {
    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etani_clone = originalSvg.cloneNode(true);
    const etwaitElement = etani_clone.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    etani_clone.id = 'etmainani';
    const etdropClone = etani_clone.querySelector('.etdrop');
    if (etdropClone) {
        etdropClone.id = 'etdropani';
    }

    const etaniinner = document.createElement('div');
    etaniinner.className = 'etaniinner';
    etaniinner.id = 'etaniinner';

    const etaniCtrl = document.createElement('div');
    etaniCtrl.className = 'etaniCtrl';
    
    const etaniContent = document.createElement('div');
    etaniContent.className = 'etaniContent';

    const contentHTMLLink = document.createElement('a');
    contentHTMLLink.className = 'etaniContentHTML';
    contentHTMLLink.textContent = 'HTML';
    contentHTMLLink.href = 'javascript:;';
    contentHTMLLink.addEventListener('click', handleContentHTMLClick);
    etaniContent.appendChild(contentHTMLLink);

    etaniCtrl.appendChild(etaniContent);

    const etaniUpdate = document.createElement('div');
    etaniUpdate.className = 'etaniUpdate';

    const updateTilesLink = document.createElement('a');
    updateTilesLink.className = 'etaniUpdateTiles';
    updateTilesLink.textContent = 'update'; // Renamed (Inst 3)
    updateTilesLink.href = 'javascript:;';
    updateTilesLink.addEventListener('click', handleUpdateTilesClick);
    etaniUpdate.appendChild(updateTilesLink);

    // Moved from etaniContent (Inst 4)
    const centerLink = document.createElement('a');
    centerLink.className = 'etaniCenter';
    centerLink.textContent = 'Center';
    centerLink.href = 'javascript:;';
    centerLink.addEventListener('click', handleCenterClick);
    etaniUpdate.appendChild(centerLink);

    etaniCtrl.appendChild(etaniUpdate);

    // Create etaniSelect section
    const etaniSelect = document.createElement('div');
    etaniSelect.className = 'etaniSelect';

    const selectTiles = document.createElement('span');
    selectTiles.className = 'etaniSelectTiles active';
    selectTiles.textContent = 'tiles';
    selectTiles.setAttribute('data-mode', 'tiles');
    selectTiles.addEventListener('click', () => handleSelectChange('tiles'));

    const selectMoving = document.createElement('span');
    selectMoving.className = 'etaniSelectMoving';
    selectMoving.textContent = 'moving';
    selectMoving.setAttribute('data-mode', 'moving');
    selectMoving.addEventListener('click', () => handleSelectChange('moving'));

    const selectBoard = document.createElement('span');
    selectBoard.className = 'etaniSelectBoard';
    selectBoard.textContent = 'board';
    selectBoard.setAttribute('data-mode', 'board');
    selectBoard.addEventListener('click', () => handleSelectChange('board'));

    etaniSelect.appendChild(selectTiles);
    etaniSelect.appendChild(selectMoving);
    etaniSelect.appendChild(selectBoard);
    etaniCtrl.appendChild(etaniSelect);

    const etaniMode = document.createElement('div');
    etaniMode.className = 'etaniMode';

    const repeatRadio = document.createElement('span');
    repeatRadio.className = 'etaniModeRepeat active';
    repeatRadio.textContent = 'Repeat';
    repeatRadio.setAttribute('data-mode', 'repeat');
    repeatRadio.addEventListener('click', () => handleModeChange('repeat'));

    const freezeRadio = document.createElement('span');
    freezeRadio.className = 'etaniModeFreeze';
    freezeRadio.textContent = 'Freeze';
    freezeRadio.setAttribute('data-mode', 'freeze');
    freezeRadio.addEventListener('click', () => handleModeChange('freeze'));

    const mixedRadio = document.createElement('span');
    mixedRadio.className = 'etaniModeMixed';
    mixedRadio.textContent = 'Mixed';
    mixedRadio.setAttribute('data-mode', 'mixed');
    mixedRadio.addEventListener('click', () => handleModeChange('mixed'));

    etaniMode.appendChild(repeatRadio);
    etaniMode.appendChild(freezeRadio);
    etaniMode.appendChild(mixedRadio);
    etaniCtrl.appendChild(etaniMode);

    const etaniAllAppend = document.createElement('div');
    etaniAllAppend.className = 'etaniAllAppend';

    const transformLink = document.createElement('a'); 
    transformLink.className = 'etaniAllAppendTransform';
    transformLink.textContent = 'transform';
    transformLink.href = 'javascript:;'; 
    transformLink.addEventListener('click', handleAllAppendTransformClick);

    const opacityLink = document.createElement('a'); 
    opacityLink.className = 'etaniAllAppendOpacity';
    opacityLink.textContent = 'opacity';
    opacityLink.href = 'javascript:;'; 
    opacityLink.addEventListener('click', handleAllAppendOpacityClick);

    etaniAllAppend.appendChild(transformLink); 
    etaniAllAppend.appendChild(opacityLink); 
    etaniCtrl.appendChild(etaniAllAppend);

    const etaniValueCtrl = document.createElement('div');
    etaniValueCtrl.className = 'etaniValueCtrl';

    const valueCtrlUpLink = document.createElement('a');
    valueCtrlUpLink.className = 'etaniValueCtrlUp';
    valueCtrlUpLink.textContent = 'fill up values';
    valueCtrlUpLink.href = 'javascript:;';
    valueCtrlUpLink.addEventListener('click', handleValueCtrlUpClick);
    etaniValueCtrl.appendChild(valueCtrlUpLink);
    etaniCtrl.appendChild(etaniValueCtrl);

    const etaniCol = document.createElement('div');
    etaniCol.className = 'etaniCol';

    const etaniResult = document.createElement('div');
    etaniResult.className = 'etaniResult';
    
    const resultImg = document.createElement('img');
    resultImg.className = 'etaniResultImage';
    resultImg.alt = 'Rendered Ejtile Animation SVG';

    const resultDRDiv = document.createElement('div');
    resultDRDiv.className = 'etaniResultDR';

    const downloadLink = document.createElement('a');
    downloadLink.className = 'etaniResultDownload';
    downloadLink.textContent = 'Download SVG';
    downloadLink.href = 'javascript:;';

    const renameLink = document.createElement('a');
    renameLink.className = 'etaniResultRename';
    renameLink.textContent = 'Rename File';
    renameLink.href = 'javascript:;';
    
    const sizeSpan = document.createElement('span');
    sizeSpan.className = 'etaniResultSize';

    resultDRDiv.appendChild(downloadLink);
    resultDRDiv.appendChild(renameLink);
    
    etaniResult.appendChild(resultImg);
    etaniResult.appendChild(resultDRDiv);
    etaniResult.appendChild(sizeSpan);
    
    etaniinner.appendChild(etaniCtrl);
    etaniinner.appendChild(etaniCol);
    etaniinner.appendChild(etaniResult);

    etaniouter.appendChild(etaniinner);

    const etdropUses = document.querySelectorAll('.etdrop use');

    etdropUses.forEach((useElement) => {
        const tileid = useElement.getAttribute('href').substring(1);
        createEtaniItem(tileid);
    });
    
    // Initialize display based on default selection mode
    updateTilesDisplay();

    updateEtaniResult();
}

// Toggle the visibility and content of the animation control panel
function toggleAnimation(event) {
    const etanibutton = event.currentTarget;
    const etaniouter = etanibutton.parentNode;
    const etaniinner = document.getElementById('etaniinner');

    if (etanibutton.textContent === 'Animate it') {
        etanibutton.textContent = 'Close Ejtile Ani';
        createEtaniInner(etaniouter);
    } else if (etanibutton.textContent === 'Close Ejtile Ani') {
        etanibutton.textContent = 'Animate it';
        if (etaniinner) {
            etaniinner.remove();
        }
        // Clear moving check interval
        if (movingCheckInterval) {
            clearInterval(movingCheckInterval);
            movingCheckInterval = null;
        }
        etani_clone = null;
        currentSelectMode = 'tiles';
        isCopyMode = false;
        isMoveMode = false;
        isDeleteMode = false;
        selectedMoveItem = null;
        lastClickTime = 0;
        lastClickedItem = null;
    }
}

// Create control buttons when animate is appending
function createControlButtons(animateType, useId) {
    const avCtrlDiv = document.createElement('div');
    avCtrlDiv.className = 'etaniAVCtrl';

    const buttons = [
        {
            className: 'etaniAVAdd',
            title: `Add ${animateType.charAt(0).toUpperCase() + animateType.slice(1)} Value`,
            svg: {
                lines: [
                    { x1: '12', y1: '5', x2: '12', y2: '19' },
                    { x1: '5', y1: '12', x2: '19', y2: '12' }
                ]
            },
            handler: (e) => handleAVAddClick(e, useId, animateType)
        },
        {
            className: 'etaniAVDelete',
            title: 'Delete Value',
            svg: {
                lines: [
                    { x1: '5', y1: '12', x2: '19', y2: '12' }
                ]
            },
            handler: (e) => handleAVDeleteToggle(e, animateType)
        },
        {
            className: 'etaniAVCopy',
            title: 'Copy Value',
            svg: {
                rect: { x: '9', y: '9', width: '13', height: '13', rx: '2', ry: '2' },
                path: { d: 'M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1' }
            },
            handler: (e) => handleAVCopyToggle(e, animateType)
        },
        {
            className: 'etaniAVMove',
            title: 'Move Value',
            svg: {
                path: { d: 'M 7,4 L 3,8 L 7,12 M 3,8 L 18,8 M 17,12 L 21,16 L 17,20 M 21,16 L 6,16' }
            },
            handler: (e) => handleAVMoveToggle(e, animateType)
        }
    ];

    buttons.forEach(button => {
        const span = document.createElement('span');
        span.className = button.className;
        span.title = button.title;
        span.setAttribute('data-type', animateType);
        span.addEventListener('click', button.handler);

        const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svg.setAttribute('width', '24');
        svg.setAttribute('height', '24');
        svg.setAttribute('viewBox', '0 0 24 24');
        svg.setAttribute('fill', 'none');
        svg.setAttribute('stroke', 'currentColor');
        svg.setAttribute('stroke-width', '1');
        svg.setAttribute('stroke-linecap', 'round');
        svg.setAttribute('stroke-linejoin', 'round');

        if (button.svg.lines) {
            button.svg.lines.forEach(line => {
                const lineElement = document.createElementNS('http://www.w3.org/2000/svg', 'line');
                Object.entries(line).forEach(([key, value]) => lineElement.setAttribute(key, value));
                svg.appendChild(lineElement);
            });
        }
        if (button.svg.rect) {
            const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
            Object.entries(button.svg.rect).forEach(([key, value]) => rect.setAttribute(key, value));
            svg.appendChild(rect);
        }
        if (button.svg.path) {
            const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
            Object.entries(button.svg.path).forEach(([key, value]) => path.setAttribute(key, value));
            svg.appendChild(path);
        }

        span.appendChild(svg);
        avCtrlDiv.appendChild(span);
    });

    return avCtrlDiv;
}

// Show dropdown menu for adding attributes
function showDropdown(e, animateType, useId) {
    const attrAddSpan = e.target;
    const existingDropdown = attrAddSpan.parentNode.querySelector('.etaniDropdown');
    if (existingDropdown) {
        // Toggle: if already open, remove it
        existingDropdown.remove();
        return;
    }

    const dropdown = document.createElement('div');
    dropdown.className = 'etaniDropdown';
    // Dynamically determine items based on existing attributes
    const attrParent = attrAddSpan.parentNode;
    const hasId = attrParent.querySelector('.etaniAnimateId');
    const hasBegin = attrParent.querySelector('.etaniAnimateBegin');
    const items = [];
    if (!hasId) items.push('id');
    if (!hasBegin) items.push('begin');
    items.push('other');
    items.forEach(item => {
        const div = document.createElement('div');
        div.className = 'etaniDropdownItem';
        div.textContent = item;
        div.addEventListener('click', () => {
            showWindow(item, animateType, useId);
            dropdown.remove(); // Remove after selecting item
        });
        dropdown.appendChild(div);
    });

    attrAddSpan.parentNode.style.position = 'relative'; // Ensure parent is relative for absolute positioning
    attrAddSpan.parentNode.appendChild(dropdown);
    dropdown.style.left = `${attrAddSpan.offsetLeft}px`;
    dropdown.style.top = `${attrAddSpan.offsetTop + attrAddSpan.offsetHeight}px`;
}

    // Show window for adding/editing attributes
    function showWindow(type, animateType, useId, existingSpan = null, otherAttrName = null) {
        const windowDiv = document.createElement('div');
        windowDiv.className = 'etaniWindow';

        const existingIds = getExistingIds();
        const isEdit = !!existingSpan;
        let input1, input2, existingIdsDiv, historyDiv, confirmBtn, deleteBtn, cancelBtn;

        if (type === 'id') {
            const label = document.createElement('label');
            label.textContent = 'Enter ID:';
            input1 = document.createElement('input');
            input1.type = 'text';
            if (isEdit) input1.value = existingSpan.textContent.replace('id=', '');
            windowDiv.appendChild(label);
            windowDiv.appendChild(input1);

            existingIdsDiv = createIdsDisplay(existingIds, false);
            windowDiv.appendChild(existingIdsDiv);
        } else if (type === 'begin') {
            const parsed = isEdit ? parseBeginValue(existingSpan.textContent) : { selected: [], plus: null, time: null };
            existingIdsDiv = createIdsDisplay(existingIds, true, parsed.selected);
            windowDiv.appendChild(existingIdsDiv);
            // Create a div wrapper for plus input with text
            const plusDiv = document.createElement('div');
            plusDiv.className = 'etaniAnimateBeginPlusOuter';
            plusDiv.textContent = 'plus: ';

            const plusInput = document.createElement('input');
            plusInput.type = 'number';
            plusInput.min = '0';
            plusInput.step = '0.1';
            plusInput.className = 'etaniAnimateBeginPlus';
            plusInput.style.width = '60px';
            plusInput.style.display = 'inline';
            if (isEdit) {
                const parsed = parseBeginValue(existingSpan.textContent);
                plusInput.value = parsed.plus || parsed.time || '';
            }
            plusDiv.appendChild(plusInput);

            const sText = document.createTextNode(' s');
            plusDiv.appendChild(sText);

            windowDiv.appendChild(plusDiv);
        } else if (type === 'other') {
            const label1 = document.createElement('label');
            label1.textContent = 'Attribute Name:';
            input1 = document.createElement('input');
            input1.type = 'text';
            if (isEdit) input1.value = otherAttrName;

            const label2 = document.createElement('label');
            label2.textContent = 'Attribute Value:';
            input2 = document.createElement('input');
            input2.type = 'text';
            if (isEdit) input2.value = existingSpan.textContent.split('=')[1];

            windowDiv.appendChild(label1);
            windowDiv.appendChild(input1);
            windowDiv.appendChild(label2);
            windowDiv.appendChild(input2);
        }

        const buttonsDiv = document.createElement('div');
        confirmBtn = document.createElement('button');
        confirmBtn.textContent = 'Confirm';
        confirmBtn.addEventListener('click', () => handleConfirm(type, animateType, useId, input1, input2, existingIdsDiv, existingSpan, windowDiv));
        buttonsDiv.appendChild(confirmBtn);

        if (isEdit) {
            deleteBtn = document.createElement('button');
            deleteBtn.textContent = 'Delete';
            deleteBtn.addEventListener('click', () => handleDelete(animateType, useId, type, otherAttrName, existingSpan, windowDiv));
            buttonsDiv.appendChild(deleteBtn);
        }

        cancelBtn = document.createElement('button');
        cancelBtn.textContent = 'Cancel';
        cancelBtn.addEventListener('click', () => document.body.removeChild(windowDiv));
        buttonsDiv.appendChild(cancelBtn);

        windowDiv.appendChild(buttonsDiv);
        document.body.appendChild(windowDiv);
    }

    // Create display for existing IDs with selection option
    function createIdsDisplay(existingIds, selectable, selected = []) {
        const div = document.createElement('div');
        div.className = 'etaniExistingIds';
        div.textContent = 'Existing IDs: ';

        if (selectable) {
            const zeroS = document.createElement('span');
            zeroS.className = 'etaniIdItem zero-s';
            zeroS.textContent = '0s';
            zeroS.classList.toggle('selected', selected.includes('0s'));
            zeroS.addEventListener('click', () => zeroS.classList.toggle('selected'));
            div.appendChild(zeroS);
        }

        existingIds.forEach(id => {
            const span = document.createElement('span');
            span.className = 'etaniIdItem';
            span.textContent = id;
            if (selectable) {
                span.addEventListener('click', () => {
                    if (span.classList.contains('selected')) {
                        // If already selected, deselect it
                        span.classList.remove('selected');
                    } else {
                        // Otherwise, deselect all other IDs and select this one
                        div.querySelectorAll('.etaniIdItem:not(.zero-s)').forEach(s => s.classList.remove('selected'));
                        span.classList.add('selected');
                    }
                });
                if (selected.includes(id)) span.classList.add('selected');
            }
            div.appendChild(span);
        });
        return div;
    }

    // Get all existing animation IDs globally, shared across types
    function getExistingIds() {
        const animates = etani_clone.querySelectorAll('animateTransform, animate[attributeName="opacity"]');
        return Array.from(animates)
            .map(animate => animate.getAttribute('id'))
            .filter(id => id);
    }

// Parse begin value to extract zeros, id, plus, and time
function parseBeginValue(beginStr) {
    const selected = [];
    let plus = null;
    let time = null;
    let id = null;

    // Remove 'begin=' prefix if present
    beginStr = beginStr.replace('begin=', '');

    // Case 1: "0s"
    if (beginStr === '0s') {
        selected.push('0s');
    } 
    // Case 2: "3s" (or any number s)
    else if (/^\d+(\.\d+)?s$/.test(beginStr)) {
        time = parseFloat(beginStr.replace('s', ''));
    } 
    // Case 3: "0s;id.end"
    else if (/^0s;[^.]+.end$/.test(beginStr)) {
        const parts = beginStr.split(';');
        selected.push('0s');
        id = parts[1].replace('.end', '');
        selected.push(id);
    } 
    // Case 4: "id.end"
    else if (/^[^.]+.end$/.test(beginStr)) {
        id = beginStr.replace('.end', '');
        selected.push(id);
    } 
    // Case 6: "0s;id.end+3s"
    else if (/^0s;[^.]+.end\+\d+(\.\d+)?s$/.test(beginStr)) {
        const parts = beginStr.split(';');
        selected.push('0s');
        const [idPart, plusPart] = parts[1].split('+');
        id = idPart.replace('.end', '');
        selected.push(id);
        plus = parseFloat(plusPart.replace('s', ''));
    }
    // Case 5: "id.end+3s"
    else if (/^[^.]+.end\+\d+(\.\d+)?s$/.test(beginStr)) {
        const [idPart, plusPart] = beginStr.split('+');
        id = idPart.replace('.end', '');
        selected.push(id);
        plus = parseFloat(plusPart.replace('s', ''));
    } 

    return { selected, plus, time };
}

    // Handle confirm button for adding/editing
    function handleConfirm(type, animateType, useId, input1, input2, existingIdsDiv, existingSpan, windowDiv) {
        let value, attrName;
        if (type === 'id') {
            value = input1.value.trim();
            // Validate: only allow alphanumeric and underscore, no spaces
            if (!value || !/^[a-zA-Z0-9_]+$/.test(value) || getExistingIds().includes(value)) {
                alert('ID must contain only letters, numbers, and underscores (no spaces)');
                return;
            }
            attrName = 'id';
        } else if (type === 'begin') {
            const selectedZero = existingIdsDiv.querySelector('.zero-s.selected');
            const selectedId = existingIdsDiv.querySelector('.etaniIdItem:not(.zero-s).selected');
            const plusInput = windowDiv.querySelector('.etaniAnimateBeginPlus');
            const plusValue = plusInput && plusInput.value ? parseFloat(plusInput.value) : null;
            let parts = [];
            if (selectedZero && selectedId) {
                parts.push('0s');
            } // Only push '0s' if there is a selectedId
            let idPart = selectedId ? `${selectedId.textContent}.end` : null;
            if (plusValue > 0) {
                if (idPart) {
                    idPart += `+${plusValue}s`;
                } else {
                    idPart = `${plusValue}s`; // If no id, use Xs directly, ignore 0s
                }
            }
            if (idPart) {
                parts.push(idPart);
            }
            value = parts.join(';');
            // Handle case where only '0s' is selected without id or plus
            if (!value && selectedZero) {
                value = '0s';
            }
            if (!value) return; // No selection
            attrName = 'begin';
        } else if (type === 'other') {
            attrName = input1.value.trim();
            value = input2.value.trim();
            if (!attrName || !value) return; // Empty
            // Check duplicate attributes (simplified, assume no duplicates for same attrName)
        }

        const animates = getAnimates(animateType, useId, attrName);
        animates.forEach(animate => animate.setAttribute(attrName, value));

        const attrAddSpan = existingSpan ? existingSpan.parentNode.querySelector('.etaniAnimateAttrAdd') : document.querySelector(`.etaniItem[data-use-id="${useId}"] .etaniAnimate[data-type="${animateType}"] .etaniAnimateAttrAdd`);
        const className = `etaniAnimate${type.charAt(0).toUpperCase() + type.slice(1)}`;
        const newSpan = existingSpan || document.createElement('span');
        newSpan.className = className;
        newSpan.textContent = type === 'other' ? `${attrName}=${value}` : `${attrName}=${value}`;
        if (!existingSpan) attrAddSpan.parentNode.insertBefore(newSpan, attrAddSpan);

        updateEtaniResult();
        document.body.removeChild(existingIdsDiv.closest('.etaniWindow'));
    }

    // Handle delete button
    function handleDelete(animateType, useId, type, otherAttrName, existingSpan, windowDiv) {
        const attrName = type === 'other' ? otherAttrName : type;
        const animates = getAnimates(animateType, useId);
        animates.forEach(animate => animate.removeAttribute(attrName));
        existingSpan.remove();
        updateEtaniResult();
        if (document.body.contains(windowDiv)) {
            document.body.removeChild(windowDiv);
        }
    }

    // Get animate elements for the specific useId and type
    function getAnimates(animateType, useId, attrType = null) {
        if (animateType === 'transform') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return [];

            if (attrType === 'id') {
                // Per original logic and example, 'id' applies only to the rotate animation
                return cloneUseElement.querySelectorAll('animateTransform[type="rotate"]') || [];
            } else {
                // Other attributes (like 'begin') apply to all three
                return cloneUseElement.querySelectorAll('animateTransform') || [];
            }
        } else {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            return cloneUseElement?.querySelectorAll('animate[attributeName="opacity"]') || [];
        }
    }

    // Edit existing attribute
    function editAttribute(span, animateType, useId, type, otherAttrName) {
        showWindow(type, animateType, useId, span, otherAttrName);
    }

// Generate base64 image for a tile
function generateTileImage(tileid) {
    const originalTile = document.querySelector(`defs g#${tileid}`);
    if (!originalTile) return null;

    const etdropUses = document.querySelectorAll('.etdrop use');
    const etwaitGroups = document.querySelectorAll('.etwait g');
    
    let targetUse = null;
    for (const group of etwaitGroups) {
        const useInGroup = group.querySelector(`use[href="#${tileid}"]`);
        if (useInGroup) {
            targetUse = useInGroup;
            break;
        }
    }
    
    let etwaittransform = '', etwaitfill = '', etwaitstroke = '', etwaitstrokeWidth = '';
    if (targetUse) {
        etwaittransform = targetUse.getAttribute('transform') || '';
        etwaitfill = targetUse.getAttribute('fill') || '';
        etwaitstroke = targetUse.getAttribute('stroke') || '';
        etwaitstrokeWidth = targetUse.getAttribute('stroke-width') || '';

        const scaleMatch = etwaittransform.match(/scale\(([^)]+)\)/);
        const rotateMatch = etwaittransform.match(/rotate\(([^)]+)\)/);
        const scalePart = scaleMatch ? scaleMatch[0] : '';
        const rotatePart = rotateMatch ? rotateMatch[0] : '';
        etwaittransform = `translate(20,20) ${scalePart} ${rotatePart}`.trim().replace(/\s+/g, ' ');
    }
    
    const tileclone = originalTile.cloneNode(true);
    tileclone.removeAttribute('id');
    if (etwaittransform) tileclone.setAttribute('transform', etwaittransform);
    if (etwaitfill) tileclone.setAttribute('fill', etwaitfill);
    if (etwaitstroke) tileclone.setAttribute('stroke', etwaitstroke);
    if (etwaitstrokeWidth) tileclone.setAttribute('stroke-width', etwaitstrokeWidth);
    
    const svgWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svgWrapper.setAttribute('width', '40');
    svgWrapper.setAttribute('height', '40');
    svgWrapper.setAttribute('version', '1.1');
    svgWrapper.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
    svgWrapper.setAttribute('xmlns:svg', 'http://www.w3.org/2000/svg');
    
    svgWrapper.appendChild(tileclone);
    
    const svgString = new XMLSerializer().serializeToString(svgWrapper);
    return svgToBase64(svgString);
}

// Update all tile images in .etaniItemLeft
function updateAllTileImages() {
    const etaniItems = document.querySelectorAll('.etaniItem');
    etaniItems.forEach(item => {
        const tileid = item.querySelector('.tileid').textContent;
        const imageOuter = item.querySelector('.etaniItemImageOuter');
        const img = imageOuter ? imageOuter.querySelector('.etaniItemImage') : null;
        
        if (img) {
            const base64Url = generateTileImage(tileid);
            if (base64Url) {
                img.src = base64Url;
            }
        }
    });
}

// Initialize the animation control panel on window load
window.addEventListener('load', () => {
    addDynamicStyles();

    const etmainouter = document.getElementById('etmainouter');
    if (etmainouter) {
        const etaniouter = document.createElement('div');
        etaniouter.className = 'etaniouter';

        const etanibutton = document.createElement('button');
        etanibutton.id = 'etanibutton';
        etanibutton.textContent = 'Animate it';
        etanibutton.addEventListener('click', toggleAnimation);

        etaniouter.appendChild(etanibutton);
        etmainouter.parentNode.insertBefore(etaniouter, etmainouter.nextSibling);
    } else {
        console.error('Div with ID "etmainouter" not found.');
    }
});
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3907
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 189 次
联系:

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

帖子 ejsoon »

僅當.etaniSelect選擇的是tiles,etaniAllAppend才會生效。

修復:當dur增加時,animateTransform中的translate並未增加。

修復:當增加etaniAnimateOther時,確認會發生報錯:existingIdsDiv is undefined。在函數handleConfirm中。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3907
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 189 次
联系:

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

帖子 ejsoon »

claude sonnet4.5

代码: 全选

我來為您說明需要進行的修復:

## 修復1:讓 `.etaniAllAppend` 僅在 tiles 模式下生效

**位置**:在 `handleAllAppendTransformClick` 和 `handleAllAppendOpacityClick` 函數開頭(約第995行和第1129行)

**在兩個函數的開頭添加檢查**:

**`handleAllAppendTransformClick` 函數**:
```javascript
// Handle click event for the 'Transform' button
function handleAllAppendTransformClick() {
    if (!etani_clone) return;
    
    // Only work in tiles mode
    if (currentSelectMode !== 'tiles') {
        alert('Transform can only be added in tiles mode');
        return;
    }
```

**`handleAllAppendOpacityClick` 函數**:
```javascript
// Handle click event for the 'Opacity' button
function handleAllAppendOpacityClick() {
    if (!etani_clone) return;
    
    // Only work in tiles mode
    if (currentSelectMode !== 'tiles') {
        alert('Opacity can only be added in tiles mode');
        return;
    }
```

---

## 修復2:當 dur 增加時,translate 未增加的問題

**位置**:在 `handleAllAppendTransformClick` 函數中,找到 `durSpan.addEventListener('click'` 部分(約第1060行)

**找到**:
```javascript
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) {
        // Find animates inside the <use> element
        const animates = etani_clone.querySelectorAll(`.etdrop use[href="#${useId}"] animateTransform`);
        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();
    }
});
```

**改為**:
```javascript
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) {
        // Find animates inside the <use> element
        const 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 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(';'));
                });
                
                // Update UI to add new etaniAVItems
                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();
    }
});
```

同樣的修復也需要應用到 `handleAllAppendOpacityClick` 函數中的 `durSpan.addEventListener('click'` 部分(約第1177行):

**找到**:
```javascript
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 animateOpacity = etani_clone.querySelector(`.etdrop use[href="#${useId}"] animate[attributeName="opacity"]`);
        if (animateOpacity) {
            if (newDur > 0) {
                animateOpacity.setAttribute('dur', `${newDur}s`);
                if (isRepeat) {
                    animateOpacity.removeAttribute('fill');
                    animateOpacity.setAttribute('repeatCount', 'indefinite');
                } else {
                    animateOpacity.removeAttribute('repeatCount');
                    animateOpacity.setAttribute('fill', 'freeze');
                }
            } else {
                animateOpacity.removeAttribute('dur');
                animateOpacity.removeAttribute('fill');
                animateOpacity.removeAttribute('repeatCount');
            }
        }
        durSpan.textContent = `dur: ${newDur}s`;
        updateEtaniResult();
    }
});
```

**改為**:
```javascript
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 animateOpacity = etani_clone.querySelector(`.etdrop use[href="#${useId}"] animate[attributeName="opacity"]`);
        
        const currentValues = animateOpacity?.getAttribute('values')?.split(';') || [];
        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 && animateOpacity) {
            const valuesToAdd = newDur - currentDur;
            const lastValue = currentValues.length > 0 ? currentValues[currentValues.length - 1] : '1';
            
            for (let i = 0; i < valuesToAdd; i++) {
                currentValues.push(lastValue);
            }
            animateOpacity.setAttribute('values', currentValues.join(';'));
            
            // Update UI to add new etaniAVItems
            const etaniAV = itemRight.querySelector(`.etaniAnimate[data-type="opacity"] .etaniAV`);
            if (etaniAV) {
                for (let i = 0; i < valuesToAdd; i++) {
                    const newAVItem = document.createElement('span');
                    newAVItem.className = 'etaniAVItem';
                    newAVItem.textContent = lastValue;
                    newAVItem.addEventListener('click', (e) => handleAVItemClick(e, 'opacity'));
                    etaniAV.appendChild(newAVItem);
                }
            }
        }
        
        if (animateOpacity) {
            if (newDur > 0) {
                animateOpacity.setAttribute('dur', `${newDur}s`);
                if (isRepeat) {
                    animateOpacity.removeAttribute('fill');
                    animateOpacity.setAttribute('repeatCount', 'indefinite');
                } else {
                    animateOpacity.removeAttribute('repeatCount');
                    animateOpacity.setAttribute('fill', 'freeze');
                }
            } else {
                animateOpacity.removeAttribute('dur');
                animateOpacity.removeAttribute('fill');
                animateOpacity.removeAttribute('repeatCount');
            }
        }
        durSpan.textContent = `dur: ${newDur}s`;
        updateEtaniResult();
    }
});
```

---

## 修復3:handleConfirm 中 existingIdsDiv is undefined 的錯誤

**位置**:在 `handleConfirm` 函數中(約第1720行)

**找到**:
```javascript
updateEtaniResult();
document.body.removeChild(existingIdsDiv.closest('.etaniWindow'));
```

**改為**:
```javascript
updateEtaniResult();
document.body.removeChild(windowDiv);
```

---

這些修復將解決:
1. Transform 和 Opacity 僅在 tiles 模式下可添加
2. 當 dur 從整數增加到更大的整數時,自動添加相應數量的值(使用原始 transform 值)
3. 修復 handleConfirm 中的 undefined 錯誤,使用正確的 windowDiv 參數
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3907
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 189 次
联系:

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

帖子 ejsoon »

在etaniAVDelete刪除而導致的transform的dur變動,只有translate變了,scale和rotate都沒變。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3907
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 189 次
联系:

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

帖子 ejsoon »

代码: 全选

我來為您說明需要修復的問題:

## 修復:刪除值時所有 animateTransform 的 dur 都應該更新

**位置**:在 `handleAVItemClick` 函數中,處理刪除模式的部分(約第918行)

**找到**:
```javascript
if (etaniCol.classList.contains(`deleting-mode-${animateType}`)) {
    const deleteButton = etaniItemRight.querySelector(`.etaniAVDelete[data-type="${animateType}"]`);
    const etaniAnimate = etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`);
    
    if (animateType === 'transform') {
        const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;

        const allAnimates = cloneUseElement.querySelectorAll('animateTransform');
        let newValuesLength = 0;
        
        allAnimates.forEach((animate, index) => {
            const values = animate.getAttribute('values').split(';');
            
            if (itemIndex < values.length) {
                values.splice(itemIndex, 1);
                newValuesLength = values.length; // All animations will have the same new length
                
                if (newValuesLength > 0) {
                    animate.setAttribute('values', values.join(';'));
                    // Only update duration on the first animate to avoid multiple calls
                    if (index === 0) { 
                        updateDuration(animate, etaniAnimate, newValuesLength);
                    }
                }
            }
        });

        // If all values were removed, clean up
        if (newValuesLength === 0 && allAnimates.length > 0) {
            allAnimates.forEach(anim => anim.remove());
            etaniAnimate.remove();
        }
    }
```

**改為**:
```javascript
if (etaniCol.classList.contains(`deleting-mode-${animateType}`)) {
    const deleteButton = etaniItemRight.querySelector(`.etaniAVDelete[data-type="${animateType}"]`);
    const etaniAnimate = etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`);
    
    if (animateType === 'transform') {
        const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;

        const allAnimates = cloneUseElement.querySelectorAll('animateTransform');
        let newValuesLength = 0;
        
        allAnimates.forEach((animate) => {
            const values = animate.getAttribute('values').split(';');
            
            if (itemIndex < values.length) {
                values.splice(itemIndex, 1);
                newValuesLength = values.length; // All animations will have the same new length
                
                if (newValuesLength > 0) {
                    animate.setAttribute('values', values.join(';'));
                }
            }
        });

        // Update duration once for all animates (they all share the same duration)
        if (newValuesLength > 0 && allAnimates.length > 0) {
            // Update dur attribute for all animates
            const currentDur = parseFloat(etaniAnimate.querySelector('.etaniAnimateDur').textContent.replace('dur: ', '').replace('s', ''));
            const isIntegerDur = Number.isInteger(currentDur);
            const newDur = isIntegerDur ? (newValuesLength > 1 ? newValuesLength - 1 : 0) : currentDur;
            
            allAnimates.forEach(animate => {
                if (newDur > 0) {
                    animate.setAttribute('dur', `${newDur}s`);
                } else {
                    animate.removeAttribute('dur');
                }
            });
            
            etaniAnimate.querySelector('.etaniAnimateDur').textContent = `dur: ${newDur}s`;
        }

        // If all values were removed, clean up
        if (newValuesLength === 0 && allAnimates.length > 0) {
            allAnimates.forEach(anim => anim.remove());
            etaniAnimate.remove();
        }
    }
```

---

這個修復確保:
1. 當刪除一個值時,所有三個 animateTransform(translate、scale、rotate)的 values 都會被更新
2. 所有三個 animateTransform 的 dur 屬性都會被正確更新(因為它們共享相同的 duration)
3. UI 中的 dur 顯示也會正確更新

原本的問題是只在第一個 animate(translate)上調用了 `updateDuration`,而 scale 和 rotate 的 dur 屬性沒有被更新。現在改為統一處理所有 animateTransform 的 dur 更新。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3907
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 189 次
联系:

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

帖子 ejsoon »

生到第三次

代码: 全选

我來為您說明需要進行的修復:

## 修復:刪除時所有 animateTransform 的 dur 都需要更新

**位置**:在 `handleAVItemClick` 函數中,處理刪除模式的部分(約第895行)

**找到**:
```javascript
if (etaniCol.classList.contains(`deleting-mode-${animateType}`)) {
    const deleteButton = etaniItemRight.querySelector(`.etaniAVDelete[data-type="${animateType}"]`);
    const etaniAnimate = etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`);
    
    if (animateType === 'transform') {
        const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;

        const allAnimates = cloneUseElement.querySelectorAll('animateTransform');
        let newValuesLength = 0;
        
        allAnimates.forEach((animate, index) => {
            const values = animate.getAttribute('values').split(';');
            
            if (itemIndex < values.length) {
                values.splice(itemIndex, 1);
                newValuesLength = values.length; // All animations will have the same new length
                
                if (newValuesLength > 0) {
                    animate.setAttribute('values', values.join(';'));
                    // Only update duration on the first animate to avoid multiple calls
                    if (index === 0) { 
                        updateDuration(animate, etaniAnimate, newValuesLength);
                    }
                }
            }
        });

        // If all values were removed, clean up
        if (newValuesLength === 0 && allAnimates.length > 0) {
            allAnimates.forEach(anim => anim.remove());
            etaniAnimate.remove();
        }
    }
```

**改為**:
```javascript
if (etaniCol.classList.contains(`deleting-mode-${animateType}`)) {
    const deleteButton = etaniItemRight.querySelector(`.etaniAVDelete[data-type="${animateType}"]`);
    const etaniAnimate = etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`);
    
    if (animateType === 'transform') {
        const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;

        const allAnimates = cloneUseElement.querySelectorAll('animateTransform');
        let newValuesLength = 0;
        
        allAnimates.forEach((animate) => {
            const values = animate.getAttribute('values').split(';');
            
            if (itemIndex < values.length) {
                values.splice(itemIndex, 1);
                newValuesLength = values.length; // All animations will have the same new length
                
                if (newValuesLength > 0) {
                    animate.setAttribute('values', values.join(';'));
                }
            }
        });

        // Update duration once for all animates after removing values
        if (newValuesLength > 0 && allAnimates.length > 0) {
            const currentDur = parseFloat(etaniAnimate.querySelector('.etaniAnimateDur').textContent.replace('dur: ', '').replace('s', ''));
            const isIntegerDur = Number.isInteger(currentDur);
            const newDur = isIntegerDur ? (newValuesLength > 1 ? newValuesLength - 1 : 0) : currentDur;
            
            allAnimates.forEach(animate => {
                if (newDur > 0) {
                    animate.setAttribute('dur', `${newDur}s`);
                } else {
                    animate.removeAttribute('dur');
                }
            });
            
            etaniAnimate.querySelector('.etaniAnimateDur').textContent = `dur: ${newDur}s`;
        }

        // If all values were removed, clean up
        if (newValuesLength === 0 && allAnimates.length > 0) {
            allAnimates.forEach(anim => anim.remove());
            etaniAnimate.remove();
        }
    }
```

---

這個修復的關鍵改動是:

1. **移除了 `index === 0` 的條件判斷**:原本只在第一個 animate 上調用 `updateDuration`,導致其他 animate(scale 和 rotate)的 dur 屬性沒有更新。

2. **在循環外統一更新所有 animate 的 dur**:刪除值後,手動計算新的 dur,然後遍歷所有 animateTransform 元素統一設置 dur 屬性。

3. **保持了 dur 的計算邏輯**:如果是整數 dur,則根據新的 values 長度計算(length - 1);如果不是整數 dur,則保持不變。

這樣可以確保當刪除一個值時,translate、scale 和 rotate 三個 animateTransform 的 dur 屬性都會同步更新。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复

在线用户

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