正在做etani,ejtile的動畫套件

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

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

帖子 ejsoon »

寫一個函式etaniAppendAnimate(id),功能描述如下:

在.etaniinner的最後增加以下HTML結構:

```
<div class="etaniWindow">
<div class="etaniAppendRow">
<strong>id: <span class="etaniAppendId">tile10</span></strong>
</div>
<div class="etaniAppendRow">
<a class="etaniAppendTransform" href="javascript:;">Transform</a>
<a class="etaniAppendMotion" href="javascript:;">Motion</a>
<span class="etaniAppendSet">Set</span>
</div>
<div class="etaniAppendRow">
<a class="etaniAppendOpacity" href="javascript:;">Opacity</a>
<a class="etaniAppendWriting" href="javascript:;">Writing</a>
</div>
<div class="etaniAppendRow">
<a class="etaniAppendFill" href="javascript:;">Fill</a>
<a class="etaniAppendStroke" href="javascript:;">Stroke</a>
<a class="etaniAppendWidth" href="javascript:;">Width</a>
</div>
<div class="etaniAppendRow">
<label>specify: </label>
<input type="text" class="etaniAppendSpecify">
</div>
<div class="etaniAppendRow">
<button>Confirm</button>
<button>Cancel</button>
</div>
</div>
```

為每個a.href="javascript"和etaniAppendSet以及button增加點擊事件。

只需要提供js,用繁體中文回答。代碼的注釋都要是英文。

代码: 全选

以下是函式 `etaniAppendAnimate(id)` 的 JavaScript 代碼:  

```javascript
function etaniAppendAnimate(id) {
    // Get the container element
    const container = document.querySelector('.etaniinner');
    if (!container) {
        console.error('Container .etaniinner not found');
        return;
    }

    // Create the main div element
    const windowDiv = document.createElement('div');
    windowDiv.className = 'etaniWindow';

    // Set the inner HTML structure
    windowDiv.innerHTML = `
        <div class="etaniAppendRow">
            <strong>id: <span class="etaniAppendId">${id}</span></strong>
        </div>
        <div class="etaniAppendRow">
            <a class="etaniAppendTransform" href="javascript:;">Transform</a>
            <a class="etaniAppendMotion" href="javascript:;">Motion</a>
            <span class="etaniAppendSet">Set</span>
        </div>
        <div class="etaniAppendRow">
            <a class="etaniAppendOpacity" href="javascript:;">Opacity</a>
            <a class="etaniAppendWriting" href="javascript:;">Writing</a>
        </div>
        <div class="etaniAppendRow">
            <a class="etaniAppendFill" href="javascript:;">Fill</a>
            <a class="etaniAppendStroke" href="javascript:;">Stroke</a>
            <a class="etaniAppendWidth" href="javascript:;">Width</a>
        </div>
        <div class="etaniAppendRow">
            <label>specify: </label>
            <input type="text" class="etaniAppendSpecify">
        </div>
        <div class="etaniAppendRow">
            <button>Confirm</button>
            <button>Cancel</button>
        </div>
    `;

    // Append the structure to the container
    container.appendChild(windowDiv);

    // Add click event listeners to all <a> with href="javascript:;", the <span class="etaniAppendSet">, and buttons
    const links = windowDiv.querySelectorAll('a[href="javascript:;"]');
    const setSpan = windowDiv.querySelector('.etaniAppendSet');
    const buttons = windowDiv.querySelectorAll('button');

    // Function to handle clicks (placeholder: logs the clicked element's class)
    const handleClick = (event) => {
        event.preventDefault(); // Prevent default behavior for links
        console.log('Clicked on element with class:', event.target.className);
        // Add specific logic here if needed
    };

    // Attach event listeners
    links.forEach(link => link.addEventListener('click', handleClick));
    if (setSpan) setSpan.addEventListener('click', handleClick);
    buttons.forEach(button => button.addEventListener('click', handleClick));
}
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3970
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 190 次
联系:

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

帖子 ejsoon »

代码: 全选

function etaniAppendAnimate(id) {
    // Get the .etaniinner element
    const inner = document.querySelector('.etaniinner');
    if (!inner) {
        console.error('Element with class .etaniinner not found.');
        return;
    }

    // Create the main div.etaniWindow
    const windowDiv = document.createElement('div');
    windowDiv.className = 'etaniWindow';

    // First row: <div class="etaniAppendRow"><strong>id: <span class="etaniAppendId">id</span></strong></div>
    const row1 = document.createElement('div');
    row1.className = 'etaniAppendRow';
    const strong = document.createElement('strong');
    strong.textContent = 'id: ';
    const spanId = document.createElement('span');
    spanId.className = 'etaniAppendId';
    spanId.textContent = id;
    strong.appendChild(spanId);
    row1.appendChild(strong);
    windowDiv.appendChild(row1);

    // Second row: <div class="etaniAppendRow"><a class="etaniAppendTransform" href="javascript:;">Transform</a><a class="etaniAppendMotion" href="javascript:;">Motion</a><span class="etaniAppendSet">Set</span></div>
    const row2 = document.createElement('div');
    row2.className = 'etaniAppendRow';
    const aTransform = document.createElement('a');
    aTransform.className = 'etaniAppendTransform';
    aTransform.href = 'javascript:;';
    aTransform.textContent = 'Transform';
    const aMotion = document.createElement('a');
    aMotion.className = 'etaniAppendMotion';
    aMotion.href = 'javascript:;';
    aMotion.textContent = 'Motion';
    const spanSet = document.createElement('span');
    spanSet.className = 'etaniAppendSet';
    spanSet.textContent = 'Set';
    row2.appendChild(aTransform);
    row2.appendChild(aMotion);
    row2.appendChild(spanSet);
    windowDiv.appendChild(row2);

    // Third row: <div class="etaniAppendRow"><a class="etaniAppendOpacity" href="javascript:;">Opacity</a><a class="etaniAppendWriting" href="javascript:;">Writing</a></div>
    const row3 = document.createElement('div');
    row3.className = 'etaniAppendRow';
    const aOpacity = document.createElement('a');
    aOpacity.className = 'etaniAppendOpacity';
    aOpacity.href = 'javascript:;';
    aOpacity.textContent = 'Opacity';
    const aWriting = document.createElement('a');
    aWriting.className = 'etaniAppendWriting';
    aWriting.href = 'javascript:;';
    aWriting.textContent = 'Writing';
    row3.appendChild(aOpacity);
    row3.appendChild(aWriting);
    windowDiv.appendChild(row3);

    // Fourth row: <div class="etaniAppendRow"><a class="etaniAppendFill" href="javascript:;">Fill</a><a class="etaniAppendStroke" href="javascript:;">Stroke</a><a class="etaniAppendWidth" href="javascript:;">Width</a></div>
    const row4 = document.createElement('div');
    row4.className = 'etaniAppendRow';
    const aFill = document.createElement('a');
    aFill.className = 'etaniAppendFill';
    aFill.href = 'javascript:;';
    aFill.textContent = 'Fill';
    const aStroke = document.createElement('a');
    aStroke.className = 'etaniAppendStroke';
    aStroke.href = 'javascript:;';
    aStroke.textContent = 'Stroke';
    const aWidth = document.createElement('a');
    aWidth.className = 'etaniAppendWidth';
    aWidth.href = 'javascript:;';
    aWidth.textContent = 'Width';
    row4.appendChild(aFill);
    row4.appendChild(aStroke);
    row4.appendChild(aWidth);
    windowDiv.appendChild(row4);

    // Fifth row: <div class="etaniAppendRow"><label>specify: </label><input type="text" class="etaniAppendSpecify"></div>
    const row5 = document.createElement('div');
    row5.className = 'etaniAppendRow';
    const label = document.createElement('label');
    label.textContent = 'specify: ';
    const input = document.createElement('input');
    input.type = 'text';
    input.className = 'etaniAppendSpecify';
    row5.appendChild(label);
    row5.appendChild(input);
    windowDiv.appendChild(row5);

    // Sixth row: <div class="etaniAppendRow"><button>Confirm</button><button>Cancel</button></div>
    const row6 = document.createElement('div');
    row6.className = 'etaniAppendRow';
    const btnConfirm = document.createElement('button');
    btnConfirm.textContent = 'Confirm';
    const btnCancel = document.createElement('button');
    btnCancel.textContent = 'Cancel';
    row6.appendChild(btnConfirm);
    row6.appendChild(btnCancel);
    windowDiv.appendChild(row6);

    // Append the windowDiv to .etaniinner
    inner.appendChild(windowDiv);

    // Add click events to all specified elements
    // For a elements with href="javascript:;"
    aTransform.addEventListener('click', function() {
        // TODO: Implement Transform click functionality
    });
    aMotion.addEventListener('click', function() {
        // TODO: Implement Motion click functionality
    });
    aOpacity.addEventListener('click', function() {
        // TODO: Implement Opacity click functionality
    });
    aWriting.addEventListener('click', function() {
        // TODO: Implement Writing click functionality
    });
    aFill.addEventListener('click', function() {
        // TODO: Implement Fill click functionality
    });
    aStroke.addEventListener('click', function() {
        // TODO: Implement Stroke click functionality
    });
    aWidth.addEventListener('click', function() {
        // TODO: Implement Width click functionality
    });

    // For span.etaniAppendSet
    spanSet.addEventListener('click', function() {
        // TODO: Implement Set click functionality
    });

    // For buttons
    btnConfirm.addEventListener('click', function() {
        // TODO: Implement Confirm click functionality
    });
    btnCancel.addEventListener('click', function() {
        // TODO: Implement Cancel click functionality
    });
}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3970
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 190 次
联系:

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

帖子 ejsoon »

寫一個函式etaniAppendAnimate(id, element_type, tag_name, animate_type, display_name),功能描述如下:

代码: 全选

在.etaniinner中的指定id的.etaniItem中的.etaniItemRight中增加以下html結構:

如果tag_name是animateTransform就增加:

```

```

如果tag_name是animate就增加:

```

```

如果tag_name是animateMotion就增加:

```

```

如果tag_name是set就增加:

```

```


之後在etani中找到一個位置,如果element_type是tile,那這個位置就是etani中的「.etdrop>use#id」內部的最後;如果element_type是board,這個位置就是etani內部的最後,並且會有href="#id"屬性。

在這個位置增加以下html結構:

如果tag_name是animateTransform就增加:

```

```

如果tag_name是animate就增加:

```

```

如果tag_name是animateMotion就增加:

```

```

如果tag_name是set就增加:

```

```




只需要提供js,用繁體中文回答。代碼的注釋都要是英文。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3970
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 190 次
联系:

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

帖子 ejsoon »

當前代碼:

代码: 全选

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

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

    const styleSheet = document.createElement('style');
    styleSheet.id = 'dynamic-et-styles'; 
    styleSheet.textContent = `
.etaniinner {
  margin-top: 10px;
}
.etaniCtrl {
  margin-bottom: 10px;
  clear: both;
  padding: 5px;
  border: 1px solid #c0c0c0;
  text-align: center;
}
.etaniCtrl > div {
  display: inline-block;
  vertical-align: top;
  padding: 5px;
  border: 1px solid #ccc;
  margin: 0 5px 5px 5px;
  text-align: left;
}
.etaniCtrl > div > a, .etaniCtrl > div > span {
  display: inline-block;
  text-decoration: none;
  padding: 2px 8px;
  font-size: 14px;
  margin: 0 2px;
  cursor: pointer;
  user-select: none;
}
.etaniCtrl > div > span {
  border: 1px solid #888;
  background-color: #eee;
  color: #333;
}
.etaniCtrl > div > span.active {
  background-color: #008CBA;
  color: white;
  border-color: #008CBA;
}
.etaniContentHTML {
  border: 1px solid #db3a32;
  color: #db3a32;
}
.etaniUpdateTiles {
  border: 1px solid #008CBA;
  color: #008CBA;
}
.etaniCenter {
  border: 1px solid green;
  color: green;
}
.etaniAllAppendTransform {
  border: 1px solid #2e36b9;
  color: #2e36b9;
}
.etaniAllAppendOpacity {
  border: 1px solid #b68942;
  color: #b68942;
}
.etaniValueIncrease {
  border: 1px solid purple;
  color: purple;
}
.etaniCol {
  border: 1px solid #aaa;
  padding: 5px;
  margin-bottom: 10px;
  clear: both;
}
.etaniItem {
  min-height: 48px;
  border: 1px solid #ccc;
  box-sizing: border-box;
  width: 100%;
  margin-bottom: -1px;
  background-color: lightyellow;
  display: inline-block;
}
.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;
}
.etaniItemId {
  text-align: center;
  font-size: 12px;
  word-break: break-all;
  margin-top: 2px;
  cursor: pointer;
}
.etaniItemPlus {
  width: 12px;
  height: 12px;
  display: inline-block;
  margin-left: 2px;
}
.etaniItemRight {
  margin-left: 60px;
  padding: 7px;
  min-height: 64px;
  background-color: #fff;
}
.etaniWindow {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: white;
  padding: 24px;
  border: 1px solid #ccc;
  box-shadow: 0 4px 8px rgba(0,0,0,0.2);
  z-index: 777;
  width: auto;
  display: inline-block;
}
.etaniAppendRow {
  margin-bottom: 8px;
}
.etaniAppendRow a, .etaniAppendRow span {
  display: inline-block;
  padding: 4px 8px;
  margin-right: 5px;
  border: 1px solid #888;
  background-color: #eee;
  color: #333;
  cursor: pointer;
  text-decoration: none;
  font-size: 12px;
}
.etaniWindow label {
  display: inline;
  margin-bottom: 10px;
}
.etaniAppendSpecify {
  display: inline;
  width: 120px;
  box-sizing: border-box;
  padding: 4px;
  font-size: 12px;
}
.etaniWindow button {
  margin-right: 10px;
  padding: 5px 10px;
}
.etaniResult {
  text-align: center;
  margin-bottom: 10px;
  padding: 10px;
  border: 1px solid #bbb;
  box-sizing: border-box;
}
.etaniResultImage {
  display: block;
  max-width: 480px;
  width: 100%;
  height: auto;
  margin: 0 auto 10px auto;
  border: 1px solid #000;
  box-sizing: border-box;
}
.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;
}
.etaniResultSize {
  display: inline-block;
  margin-left: 10px;
  font-size: 12px;
  color: #555;
}
    `;

    document.head.appendChild(styleSheet);
}

// Append ctrl elements to etaniCtrl
function addEtaniCtrlElements(etaniinner) {
    // Create the etaniCtrl element
    const etaniCtrl = document.createElement('div');
    etaniCtrl.className = 'etaniCtrl';
    etaniinner.appendChild(etaniCtrl);

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

    // Create etaniContentHTML a
    const etaniContentHTML = document.createElement('a');
    etaniContentHTML.className = 'etaniContentHTML';
    etaniContentHTML.href = 'javascript:;';
    etaniContentHTML.innerHTML = 'HTML';
    etaniContent.appendChild(etaniContentHTML);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniContent);

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

    // Create etaniUpdateTiles a
    const etaniUpdateTiles = document.createElement('a');
    etaniUpdateTiles.className = 'etaniUpdateTiles';
    etaniUpdateTiles.href = 'javascript:;';
    etaniUpdateTiles.innerHTML = 'update';
    etaniUpdate.appendChild(etaniUpdateTiles);

    // Create etaniCenter a
    const etaniCenter = document.createElement('a');
    etaniCenter.className = 'etaniCenter';
    etaniCenter.href = 'javascript:;';
    etaniCenter.innerHTML = 'Center';
    etaniUpdate.appendChild(etaniCenter);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniUpdate);

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

    // Create etaniFilterTiles span
    const etaniFilterTiles = document.createElement('span');
    etaniFilterTiles.className = 'etaniFilterTiles active';
    etaniFilterTiles.innerHTML = 'tiles';
    etaniFilter.appendChild(etaniFilterTiles);

    // Create etaniFilterMoving span
    const etaniFilterMoving = document.createElement('span');
    etaniFilterMoving.className = 'etaniFilterMoving';
    etaniFilterMoving.innerHTML = 'moving';
    etaniFilter.appendChild(etaniFilterMoving);

    // Create etaniFilterBoard span
    const etaniFilterBoard = document.createElement('span');
    etaniFilterBoard.className = 'etaniFilterBoard';
    etaniFilterBoard.innerHTML = 'board';
    etaniFilter.appendChild(etaniFilterBoard);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniFilter);

    // Add click event listeners for etaniFilter spans
    const filterSpans = etaniFilter.querySelectorAll('span');
    filterSpans.forEach(function(span) {
        span.addEventListener('click', function() {
            // Remove active from all spans in etaniFilter
            filterSpans.forEach(function(s) { s.classList.remove('active'); });
            // Add active to clicked span
            this.classList.add('active');
            // Update visibility after filter change
            updateVisibility();

            const active = document.querySelector('.etaniFilter .active');
            if (active && active.classList[0] === 'etaniFilterMoving') {
                if (intervalMoving) clearInterval(intervalMoving);
                intervalMoving = setInterval(updateMovingTiles, 1000);
                // Call immediately
                updateMovingTiles();
            } else {
                if (intervalMoving) clearInterval(intervalMoving);
                intervalMoving = null;
            }
        });
    });

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

    // Create etaniModeRepeat span
    const etaniModeRepeat = document.createElement('span');
    etaniModeRepeat.className = 'etaniModeRepeat active';
    etaniModeRepeat.innerHTML = 'repeat';
    etaniMode.appendChild(etaniModeRepeat);

    // Create etaniModeFreeze span
    const etaniModeFreeze = document.createElement('span');
    etaniModeFreeze.className = 'etaniModeFreeze';
    etaniModeFreeze.innerHTML = 'freeze';
    etaniMode.appendChild(etaniModeFreeze);

    // Create etaniModeMixed span
    const etaniModeMixed = document.createElement('span');
    etaniModeMixed.className = 'etaniModeMixed';
    etaniModeMixed.innerHTML = 'mixed';
    etaniMode.appendChild(etaniModeMixed);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniMode);

    // Add click event listeners for etaniMode spans
    const modeSpans = etaniMode.querySelectorAll('span');
    modeSpans.forEach(function(span) {
        span.addEventListener('click', function() {
            // Remove active from all spans in etaniMode
            modeSpans.forEach(function(s) { s.classList.remove('active'); });
            // Add active to clicked span
            this.classList.add('active');
        });
    });

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

    // Create etaniAllAppendTransform a
    const etaniAllAppendTransform = document.createElement('a');
    etaniAllAppendTransform.className = 'etaniAllAppendTransform';
    etaniAllAppendTransform.href = 'javascript:;';
    etaniAllAppendTransform.innerHTML = 'transform';
    etaniAllAppend.appendChild(etaniAllAppendTransform);

    // Create etaniAllAppendOpacity a
    const etaniAllAppendOpacity = document.createElement('a');
    etaniAllAppendOpacity.className = 'etaniAllAppendOpacity';
    etaniAllAppendOpacity.href = 'javascript:;';
    etaniAllAppendOpacity.innerHTML = 'opacity';
    etaniAllAppend.appendChild(etaniAllAppendOpacity);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniAllAppend);

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

    // Create etaniValueIncrease a
    const etaniValueIncrease = document.createElement('a');
    etaniValueIncrease.className = 'etaniValueIncrease';
    etaniValueIncrease.href = 'javascript:;';
    etaniValueIncrease.innerHTML = 'increase';
    etaniValue.appendChild(etaniValueIncrease);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniValue);
}

// Define updateMovingTiles function
function updateMovingTiles() {
    const movingUse = document.querySelector('#etmain > .etdrop > use.tilemoving');
    const items = document.querySelectorAll('.etaniItem');
    items.forEach(function(item) {
        item.style.display = 'none';
    });
    if (movingUse) {
        const id = movingUse.getAttribute('href').slice(1);
        const item = document.querySelector('.etaniItem[data-id="' + id + '"]');
        if (item) {
            item.style.display = 'block';
        }
    }
}

// list Etani elements
function listEtaniItems() {
    // Get the etaniCol element
    let etaniCol = document.querySelector('.etaniCol');
    if (!etaniCol) return; // Exit if etaniCol not found

    // Clear existing content in etaniCol
    etaniCol.innerHTML = '';

    // Collect elements into etaniElementArray
    let uses = etani.querySelectorAll('.etdrop > use');
    let etanidrop = etani.getElementById('etanidrop');
    let boardElements = etani.querySelectorAll('.etdrop > .etboard [id]');
    let etaniElementArray = [...uses, etanidrop, ...boardElements];

    // Loop through etaniElementArray to create etaniItem elements
    etaniElementArray.forEach(function(element) {
        if (!element) return; // Skip if element is null

        // Determine itemId and dataType
        let itemId;
        let dataType;
        if (element.tagName === 'use') {
            itemId = element.getAttribute('href').slice(1);
            dataType = 'tile';
        } else if (element.id === 'etanidrop') {
            itemId = 'board';
            dataType = 'board';
        } else {
            itemId = element.id;
            dataType = 'board';
        }

        // Create etaniItem div
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';
        etaniItem.dataset.id = itemId;
        etaniItem.dataset.type = dataType;

        // Create etaniItemLeft div
        const etaniItemLeft = document.createElement('div');
        etaniItemLeft.className = 'etaniItemLeft';
        etaniItem.appendChild(etaniItemLeft);

        // Create etaniItemImageOuter div
        const etaniItemImageOuter = document.createElement('div');
        etaniItemImageOuter.className = 'etaniItemImageOuter';
        etaniItemLeft.appendChild(etaniItemImageOuter);

        if (dataType === 'tile') {
            // Create etaniItemImage img
            const etaniItemImage = document.createElement('img');
            etaniItemImage.className = 'etaniItemImage';
            etaniItemImage.src = generateTileImage(itemId);
            etaniItemImageOuter.appendChild(etaniItemImage);
        } else {
            // Generate background color
            etaniItemImageOuter.style.background = generateHexColor(itemId);
        }

        // Create etaniItemId div
        const etaniItemId = document.createElement('div');
        etaniItemId.className = 'etaniItemId';
        etaniItemLeft.appendChild(etaniItemId);
        etaniItemLeft.addEventListener('click', function() {
            etaniAppendAnimateWindow(itemId);
        });

        // Create etaniItemName span
        const etaniItemName = document.createElement('span');
        etaniItemName.className = 'etaniItemName';
        etaniItemName.innerHTML = itemId;
        etaniItemId.appendChild(etaniItemName);

        // Create etaniItemPlus img
        const etaniItemPlus = document.createElement('img');
        etaniItemPlus.width = 12;
        etaniItemPlus.height = 12;
        etaniItemPlus.className = 'etaniItemPlus';
        etaniItemPlus.src = 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEyIiBoZWlnaHQ9IjEyIiBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjEiPjxsaW5lIHN0cm9rZT0iZGFya2dyZWVuIiB4MT0iNiIgeTE9IjAiIHgyPSI2IiB5Mj0iMTIiPjwvbGluZT48bGluZSBzdHJva2U9ImRhcmtncmVlbiIgeDE9IjAiIHkxPSI2IiB4Mj0iMTIiIHkyPSI2Ij48L2xpbmU+PC9zdmc+';
        etaniItemId.appendChild(etaniItemPlus);

        // Create etaniItemRight div
        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        etaniItem.appendChild(etaniItemRight);

        // Append etaniItem to etaniCol
        etaniCol.appendChild(etaniItem);
    });
}

// Define updateVisibility function
function updateVisibility() {
    const active = document.querySelector('.etaniFilter .active');
    if (!active) return;

    const activeClass = active.classList[0]; // e.g., 'etaniFilterTiles'
    const items = document.querySelectorAll('.etaniItem');
    items.forEach(function(item) {
        item.style.display = 'none';
    });

    if (activeClass === 'etaniFilterTiles') {
        document.querySelectorAll('.etaniItem[data-type="tile"]').forEach(function(item) {
            item.style.display = 'block';
        });
    } else if (activeClass === 'etaniFilterBoard') {
        document.querySelectorAll('.etaniItem[data-type="board"]').forEach(function(item) {
            item.style.display = 'block';
        });
    } else if (activeClass === 'etaniFilterMoving') {
        document.querySelectorAll('.etaniItem[data-type="board"]').forEach(function(item) {
            item.style.display = 'block';
        });
        // Tiles visibility handled by interval
    }
}

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

// Define start button click function
function etaniStartClick() {
    const etaniouter = document.querySelector('.etaniouter');

    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etani = originalSvg.cloneNode(true);
    const etwaitElement = etani.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    etani.id = 'etani';
    const etdropClone = etani.querySelector('.etdrop');
    if (etdropClone) {
        etdropClone.id = 'etanidrop';
    }

    // Check if etaniinner already exists
    let etaniinner = etaniouter.querySelector('.etaniinner');
    if (etaniinner) {
        // Remove etaniinner
        etaniouter.removeChild(etaniinner);
        // Restore button text
        etaniStart.innerHTML = 'start ejtile animate';
    } else {
        // Create etaniinner div
        etaniinner = document.createElement('div');
        etaniinner.className = 'etaniinner';

        // Create etaniCtrl div
        addEtaniCtrlElements(etaniinner);

        // Create etaniCol div
        const etaniCol = document.createElement('div');
        etaniCol.className = 'etaniCol';
        etaniinner.appendChild(etaniCol);

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

        etaniinner.appendChild(etaniResult);

        // Append etaniinner to etaniouter
        etaniouter.appendChild(etaniinner);

        // Change button text
        etaniStart.innerHTML = 'close ejtile animate';

        // list etani elements
        listEtaniItems();

        // update etani elements display by etaniFilter
        updateVisibility();

        // update result
        updateEtaniResult();
    }
}

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

    const etdropUses = document.querySelectorAll('#etmain > .etdrop > use');
    const etwaitGroups = document.querySelectorAll('#etmain > .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.appendChild(tileclone);
    
    const svgString = new XMLSerializer().serializeToString(svgWrapper);
    return svgToBase64(svgString);
}

// generate hex color
function generateHexColor(seed) {
  let hash = 0;
  for (let i = 0; i < seed.length; i++) {
    hash = seed.charCodeAt(i) + ((hash << 5) - hash);
  }
  let color = (hash & 0x00FFFFFF).toString(16).toUpperCase();
  while (color.length < 6) {
    color = '0' + color;
  }
  return '#' + color;
}

// Open append animate window
function etaniAppendAnimateWindow(id) {
    // Get the .etaniinner element
    const inner = document.querySelector('.etaniinner');
    if (!inner) {
        console.error('Element with class .etaniinner not found.');
        return;
    }

    // Create the main div.etaniWindow
    const windowDiv = document.createElement('div');
    windowDiv.className = 'etaniWindow';

    // First row
    const row1 = document.createElement('div');
    row1.className = 'etaniAppendRow';
    const strong = document.createElement('strong');
    strong.textContent = 'id: ' + id;
    row1.appendChild(strong);
    windowDiv.appendChild(row1);

    // Second row
    const row2 = document.createElement('div');
    row2.className = 'etaniAppendRow';
    const aTransform = document.createElement('a');
    aTransform.className = 'etaniAppendTransform';
    aTransform.href = 'javascript:;';
    aTransform.textContent = 'Transform';
    const aMotion = document.createElement('a');
    aMotion.className = 'etaniAppendMotion';
    aMotion.href = 'javascript:;';
    aMotion.textContent = 'Motion';
    const spanSet = document.createElement('span');
    spanSet.className = 'etaniAppendSet';
    spanSet.textContent = 'Set';
    row2.appendChild(aTransform);
    row2.appendChild(aMotion);
    row2.appendChild(spanSet);
    windowDiv.appendChild(row2);

    // Third row
    const row3 = document.createElement('div');
    row3.className = 'etaniAppendRow';
    const aOpacity = document.createElement('a');
    aOpacity.className = 'etaniAppendOpacity';
    aOpacity.href = 'javascript:;';
    aOpacity.textContent = 'Opacity';
    const aWriting = document.createElement('a');
    aWriting.className = 'etaniAppendWriting';
    aWriting.href = 'javascript:;';
    aWriting.textContent = 'Writing';
    row3.appendChild(aOpacity);
    row3.appendChild(aWriting);
    windowDiv.appendChild(row3);

    // Fourth row
    const row4 = document.createElement('div');
    row4.className = 'etaniAppendRow';
    const aFill = document.createElement('a');
    aFill.className = 'etaniAppendFill';
    aFill.href = 'javascript:;';
    aFill.textContent = 'Fill';
    const aStroke = document.createElement('a');
    aStroke.className = 'etaniAppendStroke';
    aStroke.href = 'javascript:;';
    aStroke.textContent = 'Stroke';
    const aWidth = document.createElement('a');
    aWidth.className = 'etaniAppendWidth';
    aWidth.href = 'javascript:;';
    aWidth.textContent = 'Width';
    row4.appendChild(aFill);
    row4.appendChild(aStroke);
    row4.appendChild(aWidth);
    windowDiv.appendChild(row4);

    // Fifth row
    const row5 = document.createElement('div');
    row5.className = 'etaniAppendRow';
    const label = document.createElement('label');
    label.textContent = 'specify: ';
    const input = document.createElement('input');
    input.type = 'text';
    input.className = 'etaniAppendSpecify';
    row5.appendChild(label);
    row5.appendChild(input);
    windowDiv.appendChild(row5);

    // Sixth row
    const row6 = document.createElement('div');
    row6.className = 'etaniAppendRow';
    const btnConfirm = document.createElement('button');
    btnConfirm.textContent = 'Confirm';
    const btnCancel = document.createElement('button');
    btnCancel.textContent = 'Cancel';
    row6.appendChild(btnConfirm);
    row6.appendChild(btnCancel);
    windowDiv.appendChild(row6);

    // Append the windowDiv to .etaniinner
    inner.appendChild(windowDiv);

    // Add click events to all specified elements
    // For a elements with href="javascript:;"
    aTransform.addEventListener('click', function() {
        // TODO: Implement Transform click functionality
    });
    aMotion.addEventListener('click', function() {
        // TODO: Implement Motion click functionality
    });
    aOpacity.addEventListener('click', function() {
        // TODO: Implement Opacity click functionality
    });
    aWriting.addEventListener('click', function() {
        // TODO: Implement Writing click functionality
    });
    aFill.addEventListener('click', function() {
        // TODO: Implement Fill click functionality
    });
    aStroke.addEventListener('click', function() {
        // TODO: Implement Stroke click functionality
    });
    aWidth.addEventListener('click', function() {
        // TODO: Implement Width click functionality
    });

    // For span.etaniAppendSet
    spanSet.addEventListener('click', function() {
        // TODO: Implement Set click functionality
    });

    // For buttons
    btnConfirm.addEventListener('click', function() {
        // TODO: Implement Confirm click functionality
    });
    btnCancel.addEventListener('click', function() {
        // TODO: Implement Cancel click functionality
    });
}

// Update the result section with the current state of etani
function updateEtaniResult() {
    if (!etani) return;
    let etaniResult = document.querySelector('.etaniResult');
    if (!etaniResult) return;

    const svgString = new XMLSerializer().serializeToString(etani);
    const sizeInBytes = new Blob([svgString]).size;
    const base64Url = svgToBase64(svgString);
    
    // Generate default filename with current date and time
    const now = new Date();
    const defaultFilename = `ejtileAnimation_${now.toISOString().replace(/[-:T]/g, '').slice(0, 15)}.svg`;

    let imgElement = document.querySelector('.etaniResultImage');
    let downloadElementOuter = document.querySelector('.etaniResultDR');
    let downloadElement = document.querySelector('.etaniResultDownload');
    let renameElement = document.querySelector('.etaniResultRename');
    let sizeElement = document.querySelector('.etaniResultSize');
    if (!imgElement) {
        imgElement = document.createElement('img');
        imgElement.className = 'etaniResultImage';
        imgElement.alt = 'Rendered Ejtile Animation SVG';
        etaniResult.appendChild(imgElement);
    }
    if (!downloadElementOuter) {
        downloadElementOuter = document.createElement('div');
        downloadElementOuter.className = 'etaniResultDR';
    }
    if (!downloadElement) {
        downloadElement = document.createElement('a');
        downloadElement.className = 'etaniResultDownload';
        downloadElement.textContent = 'Download SVG';
        downloadElement.href = 'javascript:;';
        downloadElementOuter.appendChild(downloadElement);
        etaniResult.appendChild(downloadElementOuter);
    }
    if (!renameElement) {
        renameElement = document.createElement('a');
        renameElement.className = 'etaniResultRename';
        renameElement.textContent = 'Rename File';
        renameElement.href = 'javascript:;';
        downloadElementOuter.appendChild(renameElement);
        etaniResult.appendChild(downloadElementOuter);
    }
    if (!sizeElement) {
        sizeElement = document.createElement('span');
        sizeElement.className = 'etaniResultSize';
        etaniResult.appendChild(sizeElement);
    }

    if (imgElement && downloadElement && renameElement && sizeElement) {
        imgElement.src = base64Url;
        sizeElement.textContent = `Size: ${sizeInBytes} byte`;
        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}`);
            }
        };
    }
}

// Initialize the animation control panel on window load
window.addEventListener('load', function() {
    // Get the etmainouter element
    const etmainouter = document.getElementById('etmainouter');
    if (!etmainouter) return; // Exit if etmainouter not found

    // insert dynamic style
    addEtaniStyles();

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

    // Create etaniStart button
    const etaniStart = document.createElement('button');
    etaniStart.id = 'etaniStart';
    etaniStart.innerHTML = 'start ejtile animate';

    // Append button to etaniouter
    etaniouter.appendChild(etaniStart);

    // Insert etaniouter after etmainouter
    etmainouter.parentNode.insertBefore(etaniouter, etmainouter.nextSibling);

    // Add click event listener to etaniStart
    etaniStart.addEventListener('click', etaniStartClick);
});
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3970
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 190 次
联系:

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

帖子 ejsoon »

寫一個函式etaniAppendAnimate(id, elementtype, tagname, animatetype, defaultvalue),功能描述如下:

代码: 全选

寫一個函式etaniAppendAnimate(id, elementtype, tagname, animatetype, defaultvalue),功能描述如下:

在.etaniinner中的指定id的.etaniItem中的.etaniItemRight中增加以下html結構:

如果tag_name是animateTransform就增加:

```
<div class="etaniAnimate">
    <div class="etaniAnimateAttr">
        <span class="etaniAnimateName">transform</span>
        <span class="etaniAnimateDur">dur: 0s</span>
        <span class="etaniAnimateAttrAdd">+</span>
    </div>
    <div class="etaniAnimateValue">
        <div class="etaniAVCtrl">
            <img class="etaniAVAdd">
            <img class="etaniAVDelete">
            <img class="etaniAVCopy">
            <img class="etaniAVMove">
        </div>
        <span class="etaniAVLabel">values : </span>
        <div class="etaniAV">
            <span class="etaniAVItem">a</span>
        </div>
    </div>
</div>
```

如果tag_name是animate就增加:

```
<div class="etaniAnimate">
    <div class="etaniAnimateAttr">
        <span class="etaniAnimateName">animatetype</span>
        <span class="etaniAnimateDur">dur: 0s</span>
        <span class="etaniAnimateAttrAdd">+</span>
    </div>
    <div class="etaniAnimateValue">
        <div class="etaniAVCtrl">
            <img class="etaniAVAdd">
            <img class="etaniAVDelete">
            <img class="etaniAVCopy">
            <img class="etaniAVMove">
        </div>
        <span class="etaniAVLabel">values : </span>
        <div class="etaniAV">
            <span class="etaniAVItem">defaultvalue</span>
        </div>
    </div>
</div>
```

如果tag_name是animateMotion就增加:

```
<div class="etaniAnimate">
    <div class="etaniAnimateAttr">
        <span class="etaniAnimateName">motion</span>
        <span class="etaniAnimateDur">dur: 1s</span>
        <span class="etaniAnimateAttrAdd">+</span>
    </div>
    <div class="etaniAnimateValue">
        <span class="etaniAVLabel">move to : </span>
        <div class="etaniAV">
            <span class="etaniAVItem">defaultvalue</span>
        </div>
    </div>
</div>
```

如果tag_name是set就增加:

```
<div class="etaniAnimate">
    <div class="etaniAnimateAttr">
        <span class="etaniAnimateName">animatetype</span>
        <span class="etaniAnimateDur">dur: 1s</span>
        <span class="etaniAnimateAttrAdd">+</span>
    </div>
    <div class="etaniAnimateValue">
        <span class="etaniAVLabel">set to : </span>
        <div class="etaniAV">
            <span class="etaniAVItem">defaultvalue</span>
        </div>
    </div>
</div>
```


之後在etani中找到插入位置,如果element_type是tile,那這個位置就是etani中的「.etdrop>use[href=#id]」內部的最後;如果element_type是board,這個位置就是etani內部的最後,並且會有href="#id"屬性。

在這個位置增加以下html結構:

如果tag_name是animateTransform就增加:

```
<animateTransform attributeName="transform" attributeType="XML" type="translate" values="" repeatCount="indefinite"/>
<animateTransform attributeName="transform" attributeType="XML" type="scale" values="" repeatCount="indefinite" additive="sum"/>
<animateTransform attributeName="transform" attributeType="XML" type="rotate" values="" repeatCount="indefinite" additive="sum"/>
```
其中values要在當前的「#etmain>.etdrop>use[href=#id]」的transform屬性中獲取,比如<use href="#tile2" transform="translate(70.469,33.332) scale(1,1) rotate(-60)"/>,那麼它的三個value分別為「translate=70.469,33.332、scale=1,1、rotate=-60」。

如果tag_name是animate就增加:

```
<animate attributeName="animatetype" values="defaultvalue" repeatCount="indefinite"/>
```

如果tag_name是animateMotion就增加:
```
<animateMotion dur="1s" repeatCount="indefinite" path="defaultvalue" />
```

如果tag_name是set就增加:
```
<set attributeName="animatetype" to="defaultvalue" dur="1s" />
```

在以上的這四種標籤中,如果.etaniModeRepeat包含.active,則會有repeatCount="indefinite"屬性,否則不會有這個屬性,取而代之的是fill="freeze"屬性。
set不會有repeatCount="indefinite"屬性,不過當.etaniModeRepeat不包含.active時會有fill="freeze"屬性。

只需要提供js,用繁體中文回答。代碼的注釋都要是英文。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3970
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 190 次
联系:

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

帖子 ejsoon »

gemini 2.5pro:

代码: 全选

這是一個符合您需求的 JavaScript 函式。

這個函式會執行兩個主要任務:

1.  **UI 插入**:根據 `tagname` 決定要插入的 HTML 結構,並將其附加到 `.etaniinner .etaniItem#${id} .etaniItemRight` 中。
2.  **SVG 插入**:根據 `tagname`、`elementtype` 和 `.etaniModeRepeat` 的狀態,建立相應的 SVG 動畫標籤 (例如 `<animate>`),並將其附加到 `.etani` 中正確的目標 SVG 元素(由 `href="#${id}"` 標識)內部。

<!-- end list -->

```javascript
/**
 * 在 etani 介面和對應的 SVG 元素中附加動畫。
 * * @param {string} id - 目標 .etaniItem 的 ID。
 * @param {string} elementtype - 元素類型 ('tile' 或 'board'),用於決定 SVG 插入位置。
 * @param {string} tagname - 動畫標籤名稱 ('animate', 'animateTransform', 'animateMotion', 'set')。
 * @param {string} animatetype - 動畫的 attributeName (例如 'opacity', 'fill')。
 * @param {string} defaultvalue - 動畫的預設值 (例如 '0;1;0', 'M 0 0 L 100 100')。
 */
function etaniAppendAnimate(id, elementtype, tagname, animatetype, defaultvalue) {

    // --- 內部輔助函式 ---

    /**
     * 建立一個帶有特定命名空間 (namespace) 的 SVG 元素。
     * @param {string} name - SVG 標籤名稱 (e.g., 'animate')。
     * @param {object} attrs - 一個包含屬性鍵值的物件。
     * @returns {Element} - 建立的 SVG 元素。
     */
    function createSVGElement(name, attrs) {
        const el = document.createElementNS('http://www.w3.org/2000/svg', name);
        for (const key in attrs) {
            el.setAttribute(key, attrs[key]);
        }
        return el;
    }

    /**
     * 解析 SVG transform 屬性字串。
     * @param {string} transformStr - 來自 <use> 標籤的 transform 屬性字串。
     * @returns {object} - 包含 translate, scale, 和 rotate 值的物件。
     */
    function parseTransformValues(transformStr) {
        const transforms = {
            translate: '0,0',
            scale: '1,1',
            rotate: '0'
        };
        
        if (!transformStr) {
            return transforms;
        }

        // Extract translate values
        const translateMatch = /translate\(([^)]+)\)/.exec(transformStr);
        if (translateMatch) {
            transforms.translate = translateMatch[1].trim();
        }

        // Extract scale values
        const scaleMatch = /scale\(([^)]V+)\)/.exec(transformStr);
        if (scaleMatch) {
            transforms.scale = scaleMatch[1].trim();
        }

        // Extract rotate values (can be 1 or 3 parameters, we take them all)
        const rotateMatch = /rotate\(([^)]+)\)/.exec(transformStr);
        if (rotateMatch) {
            transforms.rotate = rotateMatch[1].trim();
        }
        
        return transforms;
    }

    // --- 任務 1: 附加 HTML 到 UI ---

    // Find the UI target parent element
    const targetUIParent = document.querySelector(`.etaniinner .etaniItem#${id} .etaniItemRight`);
    
    if (!targetUIParent) {
        console.error(`Error: UI target (.etaniItem#${id} .etaniItemRight) not found.`);
        return; // Stop execution if UI target is missing
    }

    let htmlToAppend = '';

    // Determine which HTML block to append based on tagname
    switch (tagname) {
        case 'animateTransform':
            htmlToAppend = `
            <div class="etaniAnimate">
                <div class="etaniAnimateAttr">
                    <span class="etaniAnimateName">transform</span>
                    <span class="etaniAnimateDur">dur: 0s</span>
                    <span class="etaniAnimateAttrAdd">+</span>
                </div>
                <div class="etaniAnimateValue">
                    <div class="etaniAVCtrl">
                        <img class="etaniAVAdd">
                        <img class="etaniAVDelete">
                        <img class="etaniAVCopy">
                        <img class="etaniAVMove">
                    </div>
                    <span class="etaniAVLabel">values : </span>
                    <div class="etaniAV">
                        <span class="etaniAVItem">a</span>
                    </div>
                </div>
            </div>`;
            break;
            
        case 'animate':
            htmlToAppend = `
            <div class="etaniAnimate">
                <div class="etaniAnimateAttr">
                    <span class="etaniAnimateName">${animatetype}</span>
                    <span class="etaniAnimateDur">dur: 0s</span>
                    <span class="etaniAnimateAttrAdd">+</span>
                </div>
                <div class="etaniAnimateValue">
                    <div class="etaniAVCtrl">
                        <img class="etaniAVAdd">
                        <img class="etaniAVDelete">
                        <img class="etaniAVCopy">
                        <img class="etaniAVMove">
                    </div>
                    <span class="etaniAVLabel">values : </span>
                    <div class="etaniAV">
                        <span class="etaniAVItem">${defaultvalue}</span>
                    </div>
                </div>
            </div>`;
            break;

        case 'animateMotion':
            htmlToAppend = `
            <div class="etaniAnimate">
                <div class="etaniAnimateAttr">
                    <span class="etaniAnimateName">motion</span>
                    <span class="etaniAnimateDur">dur: 1s</span>
                    <span class="etaniAnimateAttrAdd">+</span>
                </div>
                <div class="etaniAnimateValue">
                    <span class="etaniAVLabel">move to : </span>
                    <div class="etaniAV">
                        <span class="etaniAVItem">${defaultvalue}</span>
                    </div>
                </div>
            </div>`;
            break;

        case 'set':
            htmlToAppend = `
            <div class="etaniAnimate">
                <div class="etaniAnimateAttr">
                    <span class="etaniAnimateName">${animatetype}</span>
                    <span class="etaniAnimateDur">dur: 1s</span>
                    <span class="etaniAnimateAttrAdd">+</span>
                </div>
                <div class="etaniAnimateValue">
                    <span class="etaniAVLabel">set to : </span>
                    <div class="etaniAV">
                        <span class="etaniAVItem">${defaultvalue}</span>
                    </div>
                </div>
            </div>`;
            break;
            
        default:
            console.error(`Error: Unknown tagname "${tagname}".`);
            return; // Exit if tagname is not recognized
    }

    // Append the generated HTML string to the UI target
    targetUIParent.insertAdjacentHTML('beforeend', htmlToAppend.trim());

    // --- 任務 2: 附加 SVG 元素 ---

    // 1. Find the SVG insertion target based on elementtype
    let targetSVGParent;
    if (elementtype === 'tile') {
        // For 'tile', the target is the <use> element in .etani .etdrop
        targetSVGParent = document.querySelector(`.etani .etdrop use[href="#${id}"]`);
    } else if (elementtype === 'board') {
        // For 'board', the target is the element (e.g., <g>) inside .etani with the matching href
        targetSVGParent = document.querySelector(`.etani [href="#${id}"]`);
    }

    if (!targetSVGParent) {
        console.error(`Error: SVG target parent (href="#${id}") not found in .etani.`);
        return; // Stop if SVG target is missing
    }

    // 2. Determine repeat or fill attributes
    const repeatModeActive = document.querySelector('.etaniModeRepeat.active');
    let repeatOrFillAttrs = {};

    if (tagname === 'set') {
        // 'set' never has repeatCount
        if (!repeatModeActive) {
            repeatOrFillAttrs = { fill: "freeze" };
        }
    } else {
        // Other animation types
        if (repeatModeActive) {
            repeatOrFillAttrs = { repeatCount: "indefinite" };
        } else {
            repeatOrFillAttrs = { fill: "freeze" };
        }
    }

    // 3. Create and append SVG elements
    switch (tagname) {
        case 'animateTransform':
            // Find the source <use> element in #etmain to read the transform from
            const sourceElement = document.querySelector(`#etmain .etdrop use[href="#${id}"]`);
            const transformString = sourceElement ? sourceElement.getAttribute('transform') : '';
            
            // Parse the existing transform values
            const transformValues = parseTransformValues(transformString);

            // Define base attributes for all 3 transform animations
            const baseAttrs = {
                attributeName: "transform",
                attributeType: "XML",
                ...repeatOrFillAttrs // Add the repeat/fill logic
            };

            // Create and append <animateTransform> for translate
            const elTranslate = createSVGElement('animateTransform', {
                ...baseAttrs,
                type: "translate",
                values: transformValues.translate
            });
            targetSVGParent.appendChild(elTranslate);

            // Create and append <animateTransform> for scale
            const elScale = createSVGElement('animateTransform', {
                ...baseAttrs,
                type: "scale",
                values: transformValues.scale,
                additive: "sum"
            });
            targetSVGParent.appendChild(elScale);

            // Create and append <animateTransform> for rotate
            const elRotate = createSVGElement('animateTransform', {
                ...baseAttrs,
                type: "rotate",
                values: transformValues.rotate,
                additive: "sum"
            });
            targetSVGParent.appendChild(elRotate);
            break;

        case 'animate':
            const elAnimate = createSVGElement('animate', {
                attributeName: animatetype,
                values: defaultvalue,
                ...repeatOrFillAttrs
            });
            targetSVGParent.appendChild(elAnimate);
            break;

        case 'animateMotion':
            const elMotion = createSVGElement('animateMotion', {
                dur: "1s",
                path: defaultvalue,
                ...repeatOrFillAttrs
            });
            targetSVGParent.appendChild(elMotion);
            break;

        case 'set':
            const elSet = createSVGElement('set', {
                attributeName: animatetype,
                to: defaultvalue,
                dur: "1s",
                ...repeatOrFillAttrs // Will be {fill: "freeze"} or {}
            });
            targetSVGParent.appendChild(elSet);
            break;
    }
}
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3970
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 190 次
联系:

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

帖子 ejsoon »

改進:

一,js中的所有注釋都應是英文

二,etani是一個已經定義的全局變量,可以直接使用,比如etani.querySelector(...)

三,所有的元素都要用createElement和appensChild的方式來添加。

gemini 2.5pro:

代码: 全选

/**
 * Appends animation UI controls and the corresponding SVG animation element.
 * * @param {string} id - The ID of the target .etaniItem.
 * @param {string} elementtype - The element type ('tile' or 'board') to determine the SVG insertion target.
 * @param {string} tagname - The animation tag name ('animate', 'animateTransform', 'animateMotion', 'set').
 * @param {string} animatetype - The attributeName to animate (e.g., 'opacity', 'fill').
 * @param {string} defaultvalue - The default value for the animation (e.g., '0;1;0', 'M 0 0 L 100 100').
 */
function etaniAppendAnimate(id, elementtype, tagname, animatetype, defaultvalue) {

    // --- Helper Functions ---

    /**
     * Creates a standard HTML element with optional class and text content.
     * @param {string} tag - The HTML tag name (e.g., 'div', 'span').
     * @param {string} [className] - Optional CSS class name.
     * @param {string} [textContent] - Optional text content.
     * @returns {HTMLElement} The created HTML element.
     */
    function createEl(tag, className, textContent) {
        const el = document.createElement(tag);
        if (className) {
            el.className = className;
        }
        if (textContent) {
            el.textContent = textContent;
        }
        return el;
    }

    /**
     * Creates an SVG element with the specified namespace and attributes.
     * @param {string} name - The SVG tag name (e.g., 'animate').
     * @param {object} attrs - An object of key-value attribute pairs.
     * @returns {Element} The created SVG element.
     */
    function createSVGElement(name, attrs) {
        const el = document.createElementNS('http://www.w3.org/2000/svg', name);
        for (const key in attrs) {
            el.setAttribute(key, attrs[key]);
        }
        return el;
    }

    /**
     * Parses an SVG transform attribute string into its components.
     * @param {string} transformStr - The transform attribute string from a <use> tag.
     * @returns {object} An object with translate, scale, and rotate values.
     */
    function parseTransformValues(transformStr) {
        const transforms = {
            translate: '0,0',
            scale: '1,1',
            rotate: '0'
        };
        
        if (!transformStr) {
            return transforms;
        }

        // Extract translate values
        const translateMatch = /translate\(([^)]+)\)/.exec(transformStr);
        if (translateMatch) {
            transforms.translate = translateMatch[1].trim();
        }

        // Extract scale values
        const scaleMatch = /scale\(([^)]+)\)/.exec(transformStr);
        if (scaleMatch) {
            transforms.scale = scaleMatch[1].trim();
        }

        // Extract rotate values
        const rotateMatch = /rotate\(([^)]+)\)/.exec(transformStr);
        if (rotateMatch) {
            transforms.rotate = rotateMatch[1].trim();
        }
        
        return transforms;
    }

    // --- Task 1: Append UI Element using DOM creation ---

    // Find the UI target parent element (in the .etaniinner UI panel)
    const targetUIParent = document.querySelector(`.etaniinner .etaniItem#${id} .etaniItemRight`);
    
    if (!targetUIParent) {
        console.error(`Error: UI target (.etaniItem#${id} .etaniItemRight) not found.`);
        return; 
    }

    // Build the UI structure
    const elAnimate = createEl('div', 'etaniAnimate');
    
    // 1. Build .etaniAnimateAttr
    const elAnimateAttr = createEl('div', 'etaniAnimateAttr');
    const elAnimateName = createEl('span', 'etaniAnimateName');
    const elAnimateDur = createEl('span', 'etaniAnimateDur');
    const elAnimateAttrAdd = createEl('span', 'etaniAnimateAttrAdd', '+');
    
    elAnimateAttr.appendChild(elAnimateName);
    elAnimateAttr.appendChild(elAnimateDur);
    elAnimateAttr.appendChild(elAnimateAttrAdd);

    // 2. Build .etaniAnimateValue
    const elAnimateValue = createEl('div', 'etaniAnimateValue');
    const elAVLabel = createEl('span', 'etaniAVLabel');
    const elAV = createEl('div', 'etaniAV');
    const elAVItem = createEl('span', 'etaniAVItem');
    
    elAV.appendChild(elAVItem);

    // 3. Apply logic based on tagname
    switch (tagname) {
        case 'animateTransform':
            elAnimateName.textContent = 'transform';
            elAnimateDur.textContent = 'dur: 0s';
            elAVLabel.textContent = 'values : ';
            elAVItem.textContent = 'a'; // Default placeholder

            // Add controls for this type
            const elAVCtrl_T = createEl('div', 'etaniAVCtrl');
            elAVCtrl_T.appendChild(createEl('img', 'etaniAVAdd'));
            elAVCtrl_T.appendChild(createEl('img', 'etaniAVDelete'));
            elAVCtrl_T.appendChild(createEl('img', 'etaniAVCopy'));
            elAVCtrl_T.appendChild(createEl('img', 'etaniAVMove'));
            // Insert controls before the label
            elAnimateValue.appendChild(elAVCtrl_T);
            break;
            
        case 'animate':
            elAnimateName.textContent = animatetype;
            elAnimateDur.textContent = 'dur: 0s';
            elAVLabel.textContent = 'values : ';
            elAVItem.textContent = defaultvalue;
            
            // Add controls for this type
            const elAVCtrl_A = createEl('div', 'etaniAVCtrl');
            elAVCtrl_A.appendChild(createEl('img', 'etaniAVAdd'));
            elAVCtrl_A.appendChild(createEl('img', 'etaniAVDelete'));
            elAVCtrl_A.appendChild(createEl('img', 'etaniAVCopy'));
            elAVCtrl_A.appendChild(createEl('img', 'etaniAVMove'));
            // Insert controls before the label
            elAnimateValue.appendChild(elAVCtrl_A);
            break;

        case 'animateMotion':
            elAnimateName.textContent = 'motion';
            elAnimateDur.textContent = 'dur: 1s';
            elAVLabel.textContent = 'move to : ';
            elAVItem.textContent = defaultvalue;
            // No controls for this type
            break;

        case 'set':
            elAnimateName.textContent = animatetype;
            elAnimateDur.textContent = 'dur: 1s';
            elAVLabel.textContent = 'set to : ';
            elAVItem.textContent = defaultvalue;
            // No controls for this type
            break;
            
        default:
            console.error(`Error: Unknown tagname "${tagname}" for UI.`);
            return; 
    }

    // 4. Assemble the .etaniAnimateValue children
    elAnimateValue.appendChild(elAVLabel);
    elAnimateValue.appendChild(elAV);
    
    // 5. Assemble the final element
    elAnimate.appendChild(elAnimateAttr);
    elAnimate.appendChild(elAnimateValue);

    // 6. Append the fully constructed UI element to the DOM
    targetUIParent.appendChild(elAnimate);


    // --- Task 2: Append SVG Element ---

    // 1. Find the SVG insertion target using the 'etani' global variable
    let targetSVGParent;
    if (elementtype === 'tile') {
        // For 'tile', the target is the <use> element in .etdrop
        targetSVGParent = etani.querySelector(`.etdrop use[href="#${id}"]`);
    } else if (elementtype === 'board') {
        // For 'board', the target is the element (e.g., <g>) with the matching href
        targetSVGParent = etani.querySelector(`[href="#${id}"]`);
    }

    if (!targetSVGParent) {
        console.error(`Error: SVG target parent (href="#${id}") not found in 'etani' variable.`);
        return; 
    }

    // 2. Determine repeat or fill attributes from the UI state
    const repeatModeActive = document.querySelector('.etaniModeRepeat.active');
    let repeatOrFillAttrs = {};

    if (tagname === 'set') {
        // 'set' never has repeatCount
        if (!repeatModeActive) {
            repeatOrFillAttrs = { fill: "freeze" };
        }
    } else {
        // Other animation types
        if (repeatModeActive) {
            repeatOrFillAttrs = { repeatCount: "indefinite" };
        } else {
            repeatOrFillAttrs = { fill: "freeze" };
        }
    }

    // 3. Create and append SVG elements
    switch (tagname) {
        case 'animateTransform':
            // Find the source <use> element in #etmain to read the transform from
            const sourceElement = document.querySelector(`#etmain .etdrop use[href="#${id}"]`);
            const transformString = sourceElement ? sourceElement.getAttribute('transform') : '';
            
            // Parse the existing transform values
            const transformValues = parseTransformValues(transformString);

            // Define base attributes for all 3 transform animations
            const baseAttrs = {
                attributeName: "transform",
                attributeType: "XML",
                ...repeatOrFillAttrs // Add the repeat/fill logic
            };

            // Create and append <animateTransform> for translate
            const elTranslate = createSVGElement('animateTransform', {
                ...baseAttrs,
                type: "translate",
                values: transformValues.translate
            });
            targetSVGParent.appendChild(elTranslate);

            // Create and append <animateTransform> for scale
            const elScale = createSVGElement('animateTransform', {
                ...baseAttrs,
                type: "scale",
                values: transformValues.scale,
                additive: "sum"
            });
            targetSVGParent.appendChild(elScale);

            // Create and append <animateTransform> for rotate
            const elRotate = createSVGElement('animateTransform', {
                ...baseAttrs,
                type: "rotate",
                values: transformValues.rotate,
                additive: "sum"
            });
            targetSVGParent.appendChild(elRotate);
            break;

        case 'animate':
            const elAnimateSVG = createSVGElement('animate', {
                attributeName: animatetype,
                values: defaultvalue,
                ...repeatOrFillAttrs
            });
            targetSVGParent.appendChild(elAnimateSVG);
            break;

        case 'animateMotion':
            const elMotion = createSVGElement('animateMotion', {
                dur: "1s",
                path: defaultvalue,
                ...repeatOrFillAttrs
            });
            targetSVGParent.appendChild(elMotion);
            break;

        case 'set':
            const elSet = createSVGElement('set', {
                attributeName: animatetype,
                to: defaultvalue,
                dur: "1s",
                ...repeatOrFillAttrs // Will be {fill: "freeze"} or {}
            });
            targetSVGParent.appendChild(elSet);
            break;
    }
}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3970
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 190 次
联系:

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

帖子 ejsoon »

當前代碼:

代码: 全选

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

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

    const styleSheet = document.createElement('style');
    styleSheet.id = 'dynamic-et-styles'; 
    styleSheet.textContent = `
.etaniinner {
  margin-top: 10px;
}
.etaniCtrl {
  margin-bottom: 10px;
  clear: both;
  padding: 5px;
  border: 1px solid #c0c0c0;
  text-align: center;
}
.etaniCtrl > div {
  display: inline-block;
  vertical-align: top;
  padding: 5px;
  border: 1px solid #ccc;
  margin: 0 5px 5px 5px;
  text-align: left;
}
.etaniCtrl > div > a, .etaniCtrl > div > span {
  display: inline-block;
  text-decoration: none;
  padding: 2px 8px;
  font-size: 14px;
  margin: 0 2px;
  cursor: pointer;
  user-select: none;
}
.etaniCtrl > div > span {
  border: 1px solid #888;
  background-color: #eee;
  color: #333;
}
.etaniCtrl > div > span.active {
  background-color: #008CBA;
  color: white;
  border-color: #008CBA;
}
.etaniContentHTML {
  border: 1px solid #db3a32;
  color: #db3a32;
}
.etaniUpdateTiles {
  border: 1px solid #008CBA;
  color: #008CBA;
}
.etaniCenter {
  border: 1px solid green;
  color: green;
}
.etaniAllAppendTransform {
  border: 1px solid #2e36b9;
  color: #2e36b9;
}
.etaniAllAppendOpacity {
  border: 1px solid #b68942;
  color: #b68942;
}
.etaniValueIncrease {
  border: 1px solid purple;
  color: purple;
}
.etaniCol {
  border: 1px solid #aaa;
  padding: 5px;
  margin-bottom: 10px;
  clear: both;
}
.etaniItem {
  min-height: 48px;
  border: 1px solid #ccc;
  box-sizing: border-box;
  width: 100%;
  margin-bottom: -1px;
  background-color: lightyellow;
  display: inline-block;
}
.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;
}
.etaniItemId {
  text-align: center;
  font-size: 12px;
  word-break: break-all;
  margin-top: 2px;
  cursor: pointer;
}
.etaniItemPlus {
  width: 12px;
  height: 12px;
  display: inline-block;
  margin-left: 2px;
}
.etaniItemRight {
  margin-left: 60px;
  padding: 7px;
  min-height: 64px;
  background-color: #fff;
}
.etaniWindow {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: white;
  padding: 24px;
  border: 1px solid #ccc;
  box-shadow: 0 4px 8px rgba(0,0,0,0.2);
  z-index: 777;
  width: auto;
  display: inline-block;
}
.etaniAppendRow {
  margin-bottom: 8px;
}
.etaniAppendRow a, .etaniAppendRow span {
  display: inline-block;
  padding: 4px 8px;
  margin-right: 5px;
  border: 1px solid #888;
  background-color: #eee;
  color: #333;
  cursor: pointer;
  text-decoration: none;
  font-size: 12px;
}
.etaniWindow label {
  display: inline;
  margin-bottom: 10px;
}
.etaniAppendSpecify {
  display: inline;
  width: 120px;
  box-sizing: border-box;
  padding: 4px;
  font-size: 12px;
}
.etaniWindow button {
  margin-right: 10px;
  padding: 5px 10px;
}
.etaniResult {
  text-align: center;
  margin-bottom: 10px;
  padding: 10px;
  border: 1px solid #bbb;
  box-sizing: border-box;
}
.etaniResultImage {
  display: block;
  max-width: 480px;
  width: 100%;
  height: auto;
  margin: 0 auto 10px auto;
  border: 1px solid #000;
  box-sizing: border-box;
}
.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;
}
.etaniResultSize {
  display: inline-block;
  margin-left: 10px;
  font-size: 12px;
  color: #555;
}
    `;

    document.head.appendChild(styleSheet);
}

// Append ctrl elements to etaniCtrl
function addEtaniCtrlElements(etaniinner) {
    // Create the etaniCtrl element
    const etaniCtrl = document.createElement('div');
    etaniCtrl.className = 'etaniCtrl';
    etaniinner.appendChild(etaniCtrl);

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

    // Create etaniContentHTML a
    const etaniContentHTML = document.createElement('a');
    etaniContentHTML.className = 'etaniContentHTML';
    etaniContentHTML.href = 'javascript:;';
    etaniContentHTML.innerHTML = 'HTML';
    etaniContent.appendChild(etaniContentHTML);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniContent);

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

    // Create etaniUpdateTiles a
    const etaniUpdateTiles = document.createElement('a');
    etaniUpdateTiles.className = 'etaniUpdateTiles';
    etaniUpdateTiles.href = 'javascript:;';
    etaniUpdateTiles.innerHTML = 'update';
    etaniUpdate.appendChild(etaniUpdateTiles);

    // Create etaniCenter a
    const etaniCenter = document.createElement('a');
    etaniCenter.className = 'etaniCenter';
    etaniCenter.href = 'javascript:;';
    etaniCenter.innerHTML = 'Center';
    etaniUpdate.appendChild(etaniCenter);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniUpdate);

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

    // Create etaniFilterTiles span
    const etaniFilterTiles = document.createElement('span');
    etaniFilterTiles.className = 'etaniFilterTiles active';
    etaniFilterTiles.innerHTML = 'tiles';
    etaniFilter.appendChild(etaniFilterTiles);

    // Create etaniFilterMoving span
    const etaniFilterMoving = document.createElement('span');
    etaniFilterMoving.className = 'etaniFilterMoving';
    etaniFilterMoving.innerHTML = 'moving';
    etaniFilter.appendChild(etaniFilterMoving);

    // Create etaniFilterBoard span
    const etaniFilterBoard = document.createElement('span');
    etaniFilterBoard.className = 'etaniFilterBoard';
    etaniFilterBoard.innerHTML = 'board';
    etaniFilter.appendChild(etaniFilterBoard);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniFilter);

    // Add click event listeners for etaniFilter spans
    const filterSpans = etaniFilter.querySelectorAll('span');
    filterSpans.forEach(function(span) {
        span.addEventListener('click', function() {
            // Remove active from all spans in etaniFilter
            filterSpans.forEach(function(s) { s.classList.remove('active'); });
            // Add active to clicked span
            this.classList.add('active');
            // Update visibility after filter change
            updateVisibility();

            const active = document.querySelector('.etaniFilter .active');
            if (active && active.classList[0] === 'etaniFilterMoving') {
                if (intervalMoving) clearInterval(intervalMoving);
                intervalMoving = setInterval(updateMovingTiles, 1000);
                // Call immediately
                updateMovingTiles();
            } else {
                if (intervalMoving) clearInterval(intervalMoving);
                intervalMoving = null;
            }
        });
    });

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

    // Create etaniModeRepeat span
    const etaniModeRepeat = document.createElement('span');
    etaniModeRepeat.className = 'etaniModeRepeat active';
    etaniModeRepeat.innerHTML = 'repeat';
    etaniMode.appendChild(etaniModeRepeat);

    // Create etaniModeFreeze span
    const etaniModeFreeze = document.createElement('span');
    etaniModeFreeze.className = 'etaniModeFreeze';
    etaniModeFreeze.innerHTML = 'freeze';
    etaniMode.appendChild(etaniModeFreeze);

    // Create etaniModeMixed span
    const etaniModeMixed = document.createElement('span');
    etaniModeMixed.className = 'etaniModeMixed';
    etaniModeMixed.innerHTML = 'mixed';
    etaniMode.appendChild(etaniModeMixed);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniMode);

    // Add click event listeners for etaniMode spans
    const modeSpans = etaniMode.querySelectorAll('span');
    modeSpans.forEach(function(span) {
        span.addEventListener('click', function() {
            // Remove active from all spans in etaniMode
            modeSpans.forEach(function(s) { s.classList.remove('active'); });
            // Add active to clicked span
            this.classList.add('active');
        });
    });

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

    // Create etaniAllAppendTransform a
    const etaniAllAppendTransform = document.createElement('a');
    etaniAllAppendTransform.className = 'etaniAllAppendTransform';
    etaniAllAppendTransform.href = 'javascript:;';
    etaniAllAppendTransform.innerHTML = 'transform';
    etaniAllAppend.appendChild(etaniAllAppendTransform);

    // Create etaniAllAppendOpacity a
    const etaniAllAppendOpacity = document.createElement('a');
    etaniAllAppendOpacity.className = 'etaniAllAppendOpacity';
    etaniAllAppendOpacity.href = 'javascript:;';
    etaniAllAppendOpacity.innerHTML = 'opacity';
    etaniAllAppend.appendChild(etaniAllAppendOpacity);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniAllAppend);

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

    // Create etaniValueIncrease a
    const etaniValueIncrease = document.createElement('a');
    etaniValueIncrease.className = 'etaniValueIncrease';
    etaniValueIncrease.href = 'javascript:;';
    etaniValueIncrease.innerHTML = 'increase';
    etaniValue.appendChild(etaniValueIncrease);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniValue);
}

// Define updateMovingTiles function
function updateMovingTiles() {
    const movingUse = document.querySelector('#etmain > .etdrop > use.tilemoving');
    const items = document.querySelectorAll('.etaniItem');
    items.forEach(function(item) {
        item.style.display = 'none';
    });
    if (movingUse) {
        const id = movingUse.getAttribute('href').slice(1);
        const item = document.querySelector('.etaniItem[data-id="' + id + '"]');
        if (item) {
            item.style.display = 'block';
        }
    }
}

// list Etani elements
function listEtaniItems() {
    // Get the etaniCol element
    let etaniCol = document.querySelector('.etaniCol');
    if (!etaniCol) return; // Exit if etaniCol not found

    // Clear existing content in etaniCol
    etaniCol.innerHTML = '';

    // Collect elements into etaniElementArray
    let uses = etani.querySelectorAll('.etdrop > use');
    let etanidrop = etani.getElementById('etanidrop');
    let boardElements = etani.querySelectorAll('.etdrop > .etboard [id]');
    let etaniElementArray = [...uses, etanidrop, ...boardElements];

    // Loop through etaniElementArray to create etaniItem elements
    etaniElementArray.forEach(function(element) {
        if (!element) return; // Skip if element is null

        // Determine itemId and dataType
        let itemId;
        let dataType;
        if (element.tagName === 'use') {
            itemId = element.getAttribute('href').slice(1);
            dataType = 'tile';
        } else if (element.id === 'etanidrop') {
            itemId = 'board';
            dataType = 'board';
        } else {
            itemId = element.id;
            dataType = 'board';
        }

        // Create etaniItem div
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';
        etaniItem.dataset.id = itemId;
        etaniItem.dataset.type = dataType;

        // Create etaniItemLeft div
        const etaniItemLeft = document.createElement('div');
        etaniItemLeft.className = 'etaniItemLeft';
        etaniItem.appendChild(etaniItemLeft);

        // Create etaniItemImageOuter div
        const etaniItemImageOuter = document.createElement('div');
        etaniItemImageOuter.className = 'etaniItemImageOuter';
        etaniItemLeft.appendChild(etaniItemImageOuter);

        if (dataType === 'tile') {
            // Create etaniItemImage img
            const etaniItemImage = document.createElement('img');
            etaniItemImage.className = 'etaniItemImage';
            etaniItemImage.src = generateTileImage(itemId);
            etaniItemImageOuter.appendChild(etaniItemImage);
        } else {
            // Generate background color
            etaniItemImageOuter.style.background = generateHexColor(itemId);
        }

        // Create etaniItemId div
        const etaniItemId = document.createElement('div');
        etaniItemId.className = 'etaniItemId';
        etaniItemLeft.appendChild(etaniItemId);
        etaniItemLeft.addEventListener('click', function() {
            etaniAppendAnimateWindow(itemId, dataType);
        });

        // Create etaniItemName span
        const etaniItemName = document.createElement('span');
        etaniItemName.className = 'etaniItemName';
        etaniItemName.innerHTML = itemId;
        etaniItemId.appendChild(etaniItemName);

        // Create etaniItemPlus img
        const etaniItemPlus = document.createElement('img');
        etaniItemPlus.width = 12;
        etaniItemPlus.height = 12;
        etaniItemPlus.className = 'etaniItemPlus';
        etaniItemPlus.src = 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEyIiBoZWlnaHQ9IjEyIiBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjEiPjxsaW5lIHN0cm9rZT0iZGFya2dyZWVuIiB4MT0iNiIgeTE9IjAiIHgyPSI2IiB5Mj0iMTIiPjwvbGluZT48bGluZSBzdHJva2U9ImRhcmtncmVlbiIgeDE9IjAiIHkxPSI2IiB4Mj0iMTIiIHkyPSI2Ij48L2xpbmU+PC9zdmc+';
        etaniItemId.appendChild(etaniItemPlus);

        // Create etaniItemRight div
        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        etaniItem.appendChild(etaniItemRight);

        // Append etaniItem to etaniCol
        etaniCol.appendChild(etaniItem);
    });
}

// Define updateVisibility function
function updateVisibility() {
    const active = document.querySelector('.etaniFilter .active');
    if (!active) return;

    const activeClass = active.classList[0]; // e.g., 'etaniFilterTiles'
    const items = document.querySelectorAll('.etaniItem');
    items.forEach(function(item) {
        item.style.display = 'none';
    });

    if (activeClass === 'etaniFilterTiles') {
        document.querySelectorAll('.etaniItem[data-type="tile"]').forEach(function(item) {
            item.style.display = 'block';
        });
    } else if (activeClass === 'etaniFilterBoard') {
        document.querySelectorAll('.etaniItem[data-type="board"]').forEach(function(item) {
            item.style.display = 'block';
        });
    } else if (activeClass === 'etaniFilterMoving') {
        document.querySelectorAll('.etaniItem[data-type="board"]').forEach(function(item) {
            item.style.display = 'block';
        });
        // Tiles visibility handled by interval
    }
}

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

// Define start button click function
function etaniStartClick() {
    const etaniouter = document.querySelector('.etaniouter');

    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etani = originalSvg.cloneNode(true);
    const etwaitElement = etani.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    etani.id = 'etani';
    const etdropClone = etani.querySelector('.etdrop');
    if (etdropClone) {
        etdropClone.id = 'etanidrop';
    }

    // Check if etaniinner already exists
    let etaniinner = etaniouter.querySelector('.etaniinner');
    if (etaniinner) {
        // Remove etaniinner
        etaniouter.removeChild(etaniinner);
        // Restore button text
        etaniStart.innerHTML = 'start ejtile animate';
    } else {
        // Create etaniinner div
        etaniinner = document.createElement('div');
        etaniinner.className = 'etaniinner';

        // Create etaniCtrl div
        addEtaniCtrlElements(etaniinner);

        // Create etaniCol div
        const etaniCol = document.createElement('div');
        etaniCol.className = 'etaniCol';
        etaniinner.appendChild(etaniCol);

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

        etaniinner.appendChild(etaniResult);

        // Append etaniinner to etaniouter
        etaniouter.appendChild(etaniinner);

        // Change button text
        etaniStart.innerHTML = 'close ejtile animate';

        // list etani elements
        listEtaniItems();

        // update etani elements display by etaniFilter
        updateVisibility();

        // update result
        updateEtaniResult();
    }
}

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

    const etdropUses = document.querySelectorAll('#etmain > .etdrop > use');
    const etwaitGroups = document.querySelectorAll('#etmain > .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.appendChild(tileclone);
    
    const svgString = new XMLSerializer().serializeToString(svgWrapper);
    return svgToBase64(svgString);
}

// generate hex color
function generateHexColor(seed) {
  let hash = 0;
  for (let i = 0; i < seed.length; i++) {
    hash = seed.charCodeAt(i) + ((hash << 5) - hash);
  }
  let color = (hash & 0x00FFFFFF).toString(16).toUpperCase();
  while (color.length < 6) {
    color = '0' + color;
  }
  return '#' + color;
}

// Open append animate window
function etaniAppendAnimateWindow(id, dataType) {
    // Get the .etaniinner element
    const inner = document.querySelector('.etaniinner');
    if (!inner) {
        console.error('Element with class .etaniinner not found.');
        return;
    }

    // Create the main div.etaniWindow
    const windowDiv = document.createElement('div');
    windowDiv.className = 'etaniWindow';

    // First row
    const row1 = document.createElement('div');
    row1.className = 'etaniAppendRow';
    const strong = document.createElement('strong');
    strong.textContent = 'id: ' + id;
    row1.appendChild(strong);
    windowDiv.appendChild(row1);

    // Second row
    const row2 = document.createElement('div');
    row2.className = 'etaniAppendRow';
    const aTransform = document.createElement('a');
    aTransform.className = 'etaniAppendTransform';
    aTransform.href = 'javascript:;';
    aTransform.textContent = 'Transform';
    const aMotion = document.createElement('a');
    aMotion.className = 'etaniAppendMotion';
    aMotion.href = 'javascript:;';
    aMotion.textContent = 'Motion';
    const spanSet = document.createElement('span');
    spanSet.className = 'etaniAppendSet';
    spanSet.textContent = 'Set';
    row2.appendChild(aTransform);
    row2.appendChild(aMotion);
    row2.appendChild(spanSet);
    windowDiv.appendChild(row2);

    // Third row
    const row3 = document.createElement('div');
    row3.className = 'etaniAppendRow';
    const aOpacity = document.createElement('a');
    aOpacity.className = 'etaniAppendOpacity';
    aOpacity.href = 'javascript:;';
    aOpacity.textContent = 'Opacity';
    const aWriting = document.createElement('a');
    aWriting.className = 'etaniAppendWriting';
    aWriting.href = 'javascript:;';
    aWriting.textContent = 'Writing';
    row3.appendChild(aOpacity);
    row3.appendChild(aWriting);
    windowDiv.appendChild(row3);

    // Fourth row
    const row4 = document.createElement('div');
    row4.className = 'etaniAppendRow';
    const aFill = document.createElement('a');
    aFill.className = 'etaniAppendFill';
    aFill.href = 'javascript:;';
    aFill.textContent = 'Fill';
    const aStroke = document.createElement('a');
    aStroke.className = 'etaniAppendStroke';
    aStroke.href = 'javascript:;';
    aStroke.textContent = 'Stroke';
    const aWidth = document.createElement('a');
    aWidth.className = 'etaniAppendWidth';
    aWidth.href = 'javascript:;';
    aWidth.textContent = 'Width';
    row4.appendChild(aFill);
    row4.appendChild(aStroke);
    row4.appendChild(aWidth);
    windowDiv.appendChild(row4);

    // Fifth row
    const row5 = document.createElement('div');
    row5.className = 'etaniAppendRow';
    const label = document.createElement('label');
    label.textContent = 'specify: ';
    const input = document.createElement('input');
    input.type = 'text';
    input.className = 'etaniAppendSpecify';
    row5.appendChild(label);
    row5.appendChild(input);
    windowDiv.appendChild(row5);

    // Sixth row
    const row6 = document.createElement('div');
    row6.className = 'etaniAppendRow';
    const btnConfirm = document.createElement('button');
    btnConfirm.textContent = 'Confirm';
    const btnCancel = document.createElement('button');
    btnCancel.textContent = 'Cancel';
    row6.appendChild(btnConfirm);
    row6.appendChild(btnCancel);
    windowDiv.appendChild(row6);

    // Append the windowDiv to .etaniinner
    inner.appendChild(windowDiv);

    // Add click events to all specified elements
    // For a elements with href="javascript:;"
    aTransform.addEventListener('click', function() {
		etaniAppendAnimate(id, dataType, "animateTransform", "transform");
    });
    aMotion.addEventListener('click', function() {
        // TODO: Implement Motion click functionality
    });
    aOpacity.addEventListener('click', function() {
        // TODO: Implement Opacity click functionality
    });
    aWriting.addEventListener('click', function() {
        // TODO: Implement Writing click functionality
    });
    aFill.addEventListener('click', function() {
        // TODO: Implement Fill click functionality
    });
    aStroke.addEventListener('click', function() {
        // TODO: Implement Stroke click functionality
    });
    aWidth.addEventListener('click', function() {
        // TODO: Implement Width click functionality
    });

    // For span.etaniAppendSet
    spanSet.addEventListener('click', function() {
        // TODO: Implement Set click functionality
    });

    // For buttons
    btnConfirm.addEventListener('click', function() {
        // TODO: Implement Confirm click functionality
    });
    btnCancel.addEventListener('click', function() {
        // TODO: Implement Cancel click functionality
    });
}

// Update the result section with the current state of etani
function updateEtaniResult() {
    if (!etani) return;
    let etaniResult = document.querySelector('.etaniResult');
    if (!etaniResult) return;

    const svgString = new XMLSerializer().serializeToString(etani);
    const sizeInBytes = new Blob([svgString]).size;
    const base64Url = svgToBase64(svgString);
    
    // Generate default filename with current date and time
    const now = new Date();
    const defaultFilename = `ejtileAnimation_${now.toISOString().replace(/[-:T]/g, '').slice(0, 15)}.svg`;

    let imgElement = document.querySelector('.etaniResultImage');
    let downloadElementOuter = document.querySelector('.etaniResultDR');
    let downloadElement = document.querySelector('.etaniResultDownload');
    let renameElement = document.querySelector('.etaniResultRename');
    let sizeElement = document.querySelector('.etaniResultSize');
    if (!imgElement) {
        imgElement = document.createElement('img');
        imgElement.className = 'etaniResultImage';
        imgElement.alt = 'Rendered Ejtile Animation SVG';
        etaniResult.appendChild(imgElement);
    }
    if (!downloadElementOuter) {
        downloadElementOuter = document.createElement('div');
        downloadElementOuter.className = 'etaniResultDR';
    }
    if (!downloadElement) {
        downloadElement = document.createElement('a');
        downloadElement.className = 'etaniResultDownload';
        downloadElement.textContent = 'Download SVG';
        downloadElement.href = 'javascript:;';
        downloadElementOuter.appendChild(downloadElement);
        etaniResult.appendChild(downloadElementOuter);
    }
    if (!renameElement) {
        renameElement = document.createElement('a');
        renameElement.className = 'etaniResultRename';
        renameElement.textContent = 'Rename File';
        renameElement.href = 'javascript:;';
        downloadElementOuter.appendChild(renameElement);
        etaniResult.appendChild(downloadElementOuter);
    }
    if (!sizeElement) {
        sizeElement = document.createElement('span');
        sizeElement.className = 'etaniResultSize';
        etaniResult.appendChild(sizeElement);
    }

    if (imgElement && downloadElement && renameElement && sizeElement) {
        imgElement.src = base64Url;
        sizeElement.textContent = `Size: ${sizeInBytes} byte`;
        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}`);
            }
        };
    }
}

/**
 * Appends animation UI controls and the corresponding SVG animation element.
 * * @param {string} id - The ID of the target .etaniItem.
 * @param {string} elementtype - The element type ('tile' or 'board') to determine the SVG insertion target.
 * @param {string} tagname - The animation tag name ('animate', 'animateTransform', 'animateMotion', 'set').
 * @param {string} animatetype - The attributeName to animate (e.g., 'opacity', 'fill').
 * @param {string} defaultvalue - The default value for the animation (e.g., '0;1;0', 'M 0 0 L 100 100').
 */
function etaniAppendAnimate(id, elementtype, tagname, animatetype, defaultvalue = null) {

    // --- Helper Functions ---

    /**
     * Creates a standard HTML element with optional class and text content.
     * @param {string} tag - The HTML tag name (e.g., 'div', 'span').
     * @param {string} [className] - Optional CSS class name.
     * @param {string} [textContent] - Optional text content.
     * @returns {HTMLElement} The created HTML element.
     */
    function createEl(tag, className, textContent) {
        const el = document.createElement(tag);
        if (className) {
            el.className = className;
        }
        if (textContent) {
            el.textContent = textContent;
        }
        return el;
    }

    /**
     * Creates an SVG element with the specified namespace and attributes.
     * @param {string} name - The SVG tag name (e.g., 'animate').
     * @param {object} attrs - An object of key-value attribute pairs.
     * @returns {Element} The created SVG element.
     */
    function createSVGElement(name, attrs) {
        const el = document.createElementNS('http://www.w3.org/2000/svg', name);
        for (const key in attrs) {
            el.setAttribute(key, attrs[key]);
        }
        return el;
    }

    /**
     * Parses an SVG transform attribute string into its components.
     * @param {string} transformStr - The transform attribute string from a <use> tag.
     * @returns {object} An object with translate, scale, and rotate values.
     */
    function parseTransformValues(transformStr) {
        const transforms = {
            translate: '0,0',
            scale: '1,1',
            rotate: '0'
        };
        
        if (!transformStr) {
            return transforms;
        }

        // Extract translate values
        const translateMatch = /translate\(([^)]+)\)/.exec(transformStr);
        if (translateMatch) {
            transforms.translate = translateMatch[1].trim();
        }

        // Extract scale values
        const scaleMatch = /scale\(([^)]+)\)/.exec(transformStr);
        if (scaleMatch) {
            transforms.scale = scaleMatch[1].trim();
        }

        // Extract rotate values
        const rotateMatch = /rotate\(([^)]+)\)/.exec(transformStr);
        if (rotateMatch) {
            transforms.rotate = rotateMatch[1].trim();
        }
        
        return transforms;
    }

    // --- Task 1: Append UI Element using DOM creation ---

    // Find the UI target parent element (in the .etaniinner UI panel)
    const targetUIParent = document.querySelector(`.etaniinner .etaniItem[data-id="${id}"] .etaniItemRight`);
    
    if (!targetUIParent) {
        console.error(`Error: UI target (.etaniItem[data-id="${id}"] .etaniItemRight) not found.`);
        return; 
    }

    // Build the UI structure
    const elAnimate = createEl('div', 'etaniAnimate');
    
    // 1. Build .etaniAnimateAttr
    const elAnimateAttr = createEl('div', 'etaniAnimateAttr');
    const elAnimateName = createEl('span', 'etaniAnimateName');
    const elAnimateDur = createEl('span', 'etaniAnimateDur');
    const elAnimateAttrAdd = createEl('span', 'etaniAnimateAttrAdd', '+');
    
    elAnimateAttr.appendChild(elAnimateName);
    elAnimateAttr.appendChild(elAnimateDur);
    elAnimateAttr.appendChild(elAnimateAttrAdd);

    // 2. Build .etaniAnimateValue
    const elAnimateValue = createEl('div', 'etaniAnimateValue');
    const elAVLabel = createEl('span', 'etaniAVLabel');
    const elAV = createEl('div', 'etaniAV');
    const elAVItem = createEl('span', 'etaniAVItem');
    
    elAV.appendChild(elAVItem);

    // 3. Apply logic based on tagname
    switch (tagname) {
        case 'animateTransform':
            elAnimateName.textContent = 'transform';
            elAnimateDur.textContent = 'dur: 0s';
            elAVLabel.textContent = 'values : ';
            elAVItem.textContent = 'a'; // Default placeholder

            // Add controls for this type
            const elAVCtrl_T = createEl('div', 'etaniAVCtrl');
            elAVCtrl_T.appendChild(createEl('img', 'etaniAVAdd'));
            elAVCtrl_T.appendChild(createEl('img', 'etaniAVDelete'));
            elAVCtrl_T.appendChild(createEl('img', 'etaniAVCopy'));
            elAVCtrl_T.appendChild(createEl('img', 'etaniAVMove'));
            // Insert controls before the label
            elAnimateValue.appendChild(elAVCtrl_T);
            break;
            
        case 'animate':
            elAnimateName.textContent = animatetype;
            elAnimateDur.textContent = 'dur: 0s';
            elAVLabel.textContent = 'values : ';
            elAVItem.textContent = defaultvalue;
            
            // Add controls for this type
            const elAVCtrl_A = createEl('div', 'etaniAVCtrl');
            elAVCtrl_A.appendChild(createEl('img', 'etaniAVAdd'));
            elAVCtrl_A.appendChild(createEl('img', 'etaniAVDelete'));
            elAVCtrl_A.appendChild(createEl('img', 'etaniAVCopy'));
            elAVCtrl_A.appendChild(createEl('img', 'etaniAVMove'));
            // Insert controls before the label
            elAnimateValue.appendChild(elAVCtrl_A);
            break;

        case 'animateMotion':
            elAnimateName.textContent = 'motion';
            elAnimateDur.textContent = 'dur: 1s';
            elAVLabel.textContent = 'move to : ';
            elAVItem.textContent = defaultvalue;
            // No controls for this type
            break;

        case 'set':
            elAnimateName.textContent = animatetype;
            elAnimateDur.textContent = 'dur: 1s';
            elAVLabel.textContent = 'set to : ';
            elAVItem.textContent = defaultvalue;
            // No controls for this type
            break;
            
        default:
            console.error(`Error: Unknown tagname "${tagname}" for UI.`);
            return; 
    }

    // 4. Assemble the .etaniAnimateValue children
    elAnimateValue.appendChild(elAVLabel);
    elAnimateValue.appendChild(elAV);
    
    // 5. Assemble the final element
    elAnimate.appendChild(elAnimateAttr);
    elAnimate.appendChild(elAnimateValue);

    // 6. Append the fully constructed UI element to the DOM
    targetUIParent.appendChild(elAnimate);


    // --- Task 2: Append SVG Element ---

    // 1. Find the SVG insertion target using the 'etani' global variable
    let targetSVGParent;
    if (elementtype === 'tile') {
        // For 'tile', the target is the <use> element in .etdrop
        targetSVGParent = etani.querySelector(`.etdrop > use[href="#${id}"]`);
    } else if (elementtype === 'board') {
        // For 'board', the target is the element (e.g., <g>) with the matching id
        targetSVGParent = etani.querySelector(`#${id}`);
    }

    if (!targetSVGParent) {
        console.error(`Error: SVG target parent (href="#${id}" or "#${id}") not found in 'etani' variable.`);
        return; 
    }

    // 2. Determine repeat or fill attributes from the UI state
    const repeatModeActive = document.querySelector('.etaniModeRepeat.active');
    let repeatOrFillAttrs = {};

    if (tagname === 'set') {
        // 'set' never has repeatCount
        if (!repeatModeActive) {
            repeatOrFillAttrs = { fill: "freeze" };
        }
    } else {
        // Other animation types
        if (repeatModeActive) {
            repeatOrFillAttrs = { repeatCount: "indefinite" };
        } else {
            repeatOrFillAttrs = { fill: "freeze" };
        }
    }

    // 3. Create and append SVG elements
    switch (tagname) {
        case 'animateTransform':
            // Find the source <use> element in #etmain to read the transform from
            const sourceElement = document.querySelector(`#etmain .etdrop > use[href="#${id}"]`);
            const transformString = sourceElement ? sourceElement.getAttribute('transform') : '';
            
            // Parse the existing transform values
            const transformValues = parseTransformValues(transformString);

            // Define base attributes for all 3 transform animations
            const baseAttrs = {
                attributeName: "transform",
                attributeType: "XML",
                ...repeatOrFillAttrs // Add the repeat/fill logic
            };

            // Create and append <animateTransform> for translate
            const elTranslate = createSVGElement('animateTransform', {
                ...baseAttrs,
                type: "translate",
                values: transformValues.translate
            });
            targetSVGParent.appendChild(elTranslate);

            // Create and append <animateTransform> for scale
            const elScale = createSVGElement('animateTransform', {
                ...baseAttrs,
                type: "scale",
                values: transformValues.scale,
                additive: "sum"
            });
            targetSVGParent.appendChild(elScale);

            // Create and append <animateTransform> for rotate
            const elRotate = createSVGElement('animateTransform', {
                ...baseAttrs,
                type: "rotate",
                values: transformValues.rotate,
                additive: "sum"
            });
            targetSVGParent.appendChild(elRotate);
            break;

        case 'animate':
            const elAnimateSVG = createSVGElement('animate', {
                attributeName: animatetype,
                values: defaultvalue,
                ...repeatOrFillAttrs
            });
            targetSVGParent.appendChild(elAnimateSVG);
            break;

        case 'animateMotion':
            const elMotion = createSVGElement('animateMotion', {
                dur: "1s",
                path: defaultvalue,
                ...repeatOrFillAttrs
            });
            targetSVGParent.appendChild(elMotion);
            break;

        case 'set':
            const elSet = createSVGElement('set', {
                attributeName: animatetype,
                to: defaultvalue,
                dur: "1s",
                ...repeatOrFillAttrs // Will be {fill: "freeze"} or {}
            });
            targetSVGParent.appendChild(elSet);
            break;
    }
}

// Initialize the animation control panel on window load
window.addEventListener('load', function() {
    // Get the etmainouter element
    const etmainouter = document.getElementById('etmainouter');
    if (!etmainouter) return; // Exit if etmainouter not found

    // insert dynamic style
    addEtaniStyles();

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

    // Create etaniStart button
    const etaniStart = document.createElement('button');
    etaniStart.id = 'etaniStart';
    etaniStart.innerHTML = 'start ejtile animate';

    // Append button to etaniouter
    etaniouter.appendChild(etaniStart);

    // Insert etaniouter after etmainouter
    etmainouter.parentNode.insertBefore(etaniouter, etmainouter.nextSibling);

    // Add click event listener to etaniStart
    etaniStart.addEventListener('click', etaniStartClick);
});
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3970
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 190 次
联系:

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

帖子 ejsoon »

代码: 全选

            etaniAVAdd.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>';
            etaniAVDelete.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>';
            etaniAVCopy.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
            etaniAVMove.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><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"></path></svg>';
    function createSVGElement(name, attrs) {
        const el = document.createElementNS('http://www.w3.org/2000/svg', name);
        for (const key in attrs) {
            el.setAttribute(key, attrs[key]);
        }
        return el;
    }
把innerHTML = 改寫成createSVGElement和appendChild方式加入。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3970
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 190 次
联系:

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

帖子 ejsoon »

代码: 全选

```javascript
// For etaniAVAdd
const svgAdd = createSVGElement('svg', {
    width: "24",
    height: "24",
    fill: "none",
    stroke: "currentColor",
    "stroke-width": "1",
    "stroke-linecap": "round",
    "stroke-linejoin": "round"
});
const lineAdd1 = createSVGElement('line', { x1: "12", y1: "5", x2: "12", y2: "19" });
const lineAdd2 = createSVGElement('line', { x1: "5", y1: "12", x2: "19", y2: "12" });
svgAdd.appendChild(lineAdd1);
svgAdd.appendChild(lineAdd2);
etaniAVAdd.innerHTML = ''; // Clear existing content if any
etaniAVAdd.appendChild(svgAdd);

// For etaniAVDelete
const svgDelete = createSVGElement('svg', {
    width: "24",
    height: "24",
    fill: "none",
    stroke: "currentColor",
    "stroke-width": "1",
    "stroke-linecap": "round",
    "stroke-linejoin": "round"
});
const lineDelete = createSVGElement('line', { x1: "5", y1: "12", x2: "19", y2: "12" });
svgDelete.appendChild(lineDelete);
etaniAVDelete.innerHTML = ''; // Clear existing content if any
etaniAVDelete.appendChild(svgDelete);

// For etaniAVCopy
const svgCopy = createSVGElement('svg', {
    width: "24",
    height: "24",
    fill: "none",
    stroke: "currentColor",
    "stroke-width": "1",
    "stroke-linecap": "round",
    "stroke-linejoin": "round"
});
const rectCopy = createSVGElement('rect', { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" });
const pathCopy = createSVGElement('path', { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" });
svgCopy.appendChild(rectCopy);
svgCopy.appendChild(pathCopy);
etaniAVCopy.innerHTML = ''; // Clear existing content if any
etaniAVCopy.appendChild(svgCopy);

// For etaniAVMove
const svgMove = createSVGElement('svg', {
    width: "24",
    height: "24",
    fill: "none",
    stroke: "currentColor",
    "stroke-width": "1",
    "stroke-linecap": "round",
    "stroke-linejoin": "round"
});
const pathMove = createSVGElement('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" });
svgMove.appendChild(pathMove);
etaniAVMove.innerHTML = ''; // Clear existing content if any
etaniAVMove.appendChild(svgMove);
```
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3970
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 190 次
联系:

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

帖子 ejsoon »

代码: 全选

### 在 HTML 中設置 data-value 屬性

在 HTML 中,`data-value` 是一種自訂數據屬性(data-* 屬性),用來儲存元素相關的自訂數據。它可以直接在 HTML 標籤中設置,例如:

```html
<div id="myElement" data-value="initialValue">這是一個元素</div>
```

這裡,`data-value` 的值被設置為 `"initialValue"`。你可以根據需要替換為任何字串、數字或其他值。

### 在 JavaScript 中操作 data-value 屬性

在 JavaScript 中,你可以使用 DOM API 來設置、更改或移除 `data-value` 屬性。推薦使用 `dataset` 屬性,因為它更方便且專門處理 data-* 屬性(自動處理駝峰命名轉換,例如 `data-my-value` 變成 `dataset.myValue`)。

假設我們有一個 HTML 元素:
```html
<div id="myElement" data-value="initialValue">這是一個元素</div>
```

#### 1. **設置(或更改)data-value**
- 使用 `dataset`:
  ```javascript
  const element = document.getElementById('myElement');
  element.dataset.value = 'newValue';  // 設置或更改為 'newValue'
  ```

- 使用 `setAttribute`(更通用,但較低階):
  ```javascript
  const element = document.getElementById('myElement');
  element.setAttribute('data-value', 'newValue');  // 設置或更改為 'newValue'
  ```

  注意:如果屬性已存在,這會直接更改其值;如果不存在,則新增。

#### 2. **讀取 data-value**
  雖然問題沒問,但為了完整性:
  - 使用 `dataset`:
    ```javascript
    const value = element.dataset.value;  // 返回 'newValue'
    ```
  - 使用 `getAttribute`:
    ```javascript
    const value = element.getAttribute('data-value');  // 返回 'newValue'
    ```

#### 3. **移除 data-value**
- 使用 `dataset`:
  ```javascript
  delete element.dataset.value;  // 移除屬性
  ```

- 使用 `removeAttribute`:
  ```javascript
  element.removeAttribute('data-value');  // 移除屬性
  ```

  移除後,如果嘗試讀取 `element.dataset.value`,會返回 `undefined`。

### 注意事項
- `data-*` 屬性是 HTML5 標準的一部分,主要用於 JavaScript 與 HTML 之間的數據傳遞,不會影響元素的樣式或行為。
- 在 `dataset` 中,屬性名會自動轉換為駝峰式(camelCase),例如 `data-my-value` 變成 `dataset.myValue`。
- 這些操作會即時反映在 DOM 中,你可以用瀏覽器的開發者工具檢查元素屬性變化。
- 如果你在框架如 React 或 Vue 中使用,建議使用框架的數據綁定方式,但基本 DOM 操作仍適用。

如果有特定情境或範例代碼需求,請提供更多細節!
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3970
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 190 次
联系:

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

帖子 ejsoon »

簡化以下js 代碼:

代码: 全选

    etaniAVAdd.addEventListener('click', function () {
        const elAVCtrlActive = elAVCtrl.querySelector('.active');
        if (elAVCtrlActive) elAVCtrlActive.classList.remove('active');
        elAVCtrl.dataset.mode = 'edit';
    });
    etaniAVDelete.addEventListener('click', function () {
        if (elAVCtrl.dataset.mode == 'delete') {
            this.classList.remove('active');
            elAVCtrl.dataset.mode = 'edit';
        } else {
            const elAVCtrlActive = elAVCtrl.querySelector('.active');
            if (elAVCtrlActive) elAVCtrlActive.classList.remove('active');
            this.classList.add('active');
            elAVCtrl.dataset.mode = 'delete';
        }
    });
    etaniAVCopy.addEventListener('click', function () {
        if (elAVCtrl.dataset.mode == 'copy') {
            this.classList.remove('active');
            elAVCtrl.dataset.mode = 'edit';
        } else {
            const elAVCtrlActive = elAVCtrl.querySelector('.active');
            if (elAVCtrlActive) elAVCtrlActive.classList.remove('active');
            this.classList.add('active');
            elAVCtrl.dataset.mode = 'copy';
        }
    });
    etaniAVMove.addEventListener('click', function () {
        if (elAVCtrl.dataset.mode == 'move') {
            this.classList.remove('active');
            elAVCtrl.dataset.mode = 'edit';
        } else {
            const elAVCtrlActive = elAVCtrl.querySelector('.active');
            if (elAVCtrlActive) elAVCtrlActive.classList.remove('active');
            this.classList.add('active');
            elAVCtrl.dataset.mode = 'move';
        }
    });
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3970
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 190 次
联系:

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

帖子 ejsoon »

代码: 全选

function removeActive() {
    const active = elAVCtrl.querySelector('.active');
    if (active) active.classList.remove('active');
}

etaniAVAdd.addEventListener('click', () => {
    removeActive();
    elAVCtrl.dataset.mode = 'edit';
});

const toggleButtons = {
    delete: { element: etaniAVDelete, mode: 'delete' },
    copy: { element: etaniAVCopy, mode: 'copy' },
    move: { element: etaniAVMove, mode: 'move' }
};

Object.values(toggleButtons).forEach(({ element, mode }) => {
    element.addEventListener('click', function () {
        if (elAVCtrl.dataset.mode === mode) {
            this.classList.remove('active');
            elAVCtrl.dataset.mode = 'edit';
        } else {
            removeActive();
            this.classList.add('active');
            elAVCtrl.dataset.mode = mode;
        }
    });
});
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3970
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 190 次
联系:

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

帖子 ejsoon »

當前代碼:

代码: 全选

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

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

    const styleSheet = document.createElement('style');
    styleSheet.id = 'dynamic-et-styles'; 
    styleSheet.textContent = `
.etaniinner {
  margin-top: 10px;
}
.etaniCtrl {
  margin-bottom: 10px;
  clear: both;
  padding: 5px;
  border: 1px solid #c0c0c0;
  text-align: center;
}
.etaniCtrl > div {
  display: inline-block;
  vertical-align: top;
  padding: 5px;
  border: 1px solid #ccc;
  margin: 0 5px 5px 5px;
  text-align: left;
}
.etaniCtrl > div > a, .etaniCtrl > div > span {
  display: inline-block;
  text-decoration: none;
  padding: 2px 8px;
  font-size: 14px;
  margin: 0 2px;
  cursor: pointer;
  user-select: none;
}
.etaniCtrl > div > span {
  border: 1px solid #888;
  background-color: #eee;
  color: #333;
}
.etaniCtrl > div > span.active {
  background-color: #008CBA;
  color: white;
  border-color: #008CBA;
}
.etaniContentHTML {
  border: 1px solid #db3a32;
  color: #db3a32;
}
.etaniUpdateTiles {
  border: 1px solid #008CBA;
  color: #008CBA;
}
.etaniCenter {
  border: 1px solid green;
  color: green;
}
.etaniAllAppendTransform {
  border: 1px solid #2e36b9;
  color: #2e36b9;
}
.etaniAllAppendOpacity {
  border: 1px solid #b68942;
  color: #b68942;
}
.etaniValueIncrease {
  border: 1px solid purple;
  color: purple;
}
.etaniCol {
  border: 1px solid #aaa;
  padding: 5px;
  margin-bottom: 10px;
  clear: both;
}
.etaniItem {
  min-height: 48px;
  border: 1px solid #ccc;
  box-sizing: border-box;
  width: 100%;
  margin-bottom: -1px;
  background-color: lightyellow;
  display: inline-block;
}
.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;
}
.etaniItemId {
  text-align: center;
  font-size: 12px;
  word-break: break-all;
  margin-top: 2px;
  cursor: pointer;
}
.etaniItemPlus {
  width: 12px;
  height: 12px;
  display: inline-block;
  margin-left: 2px;
}
.etaniItemRight {
  margin-left: 60px;
  padding: 7px;
  min-height: 64px;
  background-color: #fff;
}
.etaniWindow {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: white;
  padding: 24px;
  border: 1px solid #ccc;
  box-shadow: 0 4px 8px rgba(0,0,0,0.2);
  z-index: 777;
  width: auto;
  display: inline-block;
}
.etaniAppendRow {
  margin-bottom: 8px;
}
.etaniAppendRow a, .etaniAppendRow span {
  display: inline-block;
  padding: 4px 8px;
  margin-right: 5px;
  border: 1px solid #888;
  background-color: #eee;
  color: #333;
  cursor: pointer;
  text-decoration: none;
  font-size: 12px;
}
.etaniWindow label {
  display: inline;
  margin-bottom: 10px;
}
.etaniAppendSpecify {
  display: inline;
  width: 120px;
  box-sizing: border-box;
  padding: 4px;
  font-size: 12px;
}
.etaniWindow button {
  margin-right: 10px;
  padding: 5px 10px;
}
.etaniResult {
  text-align: center;
  margin-bottom: 10px;
  padding: 10px;
  border: 1px solid #bbb;
  box-sizing: border-box;
}
.etaniResultImage {
  display: block;
  max-width: 480px;
  width: 100%;
  height: auto;
  margin: 0 auto 10px auto;
  border: 1px solid #000;
  box-sizing: border-box;
}
.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;
}
.etaniResultSize {
  display: inline-block;
  margin-left: 10px;
  font-size: 12px;
  color: #555;
}
.etaniAnimate {
  border: 1px solid #999;
  padding: 5px;
  margin-bottom: 5px;
}
.etaniAnimateAttr {
  margin-bottom: 5px;
}
.etaniAnimateAttr > span {
  cursor: pointer;
  display: inline-block;
  padding: 2px 5px;
  font-size: 12px;
  box-sizing: border-box;
  border-width: 1px;
  border-style: solid;
}
.etaniAnimateName {
  background-color: #555;
  border-color: #555;
  color: white;
  margin-right: 10px;
}
.etaniAnimateAttr > span:not(.etaniAnimateName) {
  margin-right: 7px;
}
.etaniAnimateDur {
  border-color: blue;
  color: blue;
}
.etaniAnimateFR {
  border-color: #78229f;
  color: #78229f;
}
.etaniAnimateAttrAdd {
  border-color: #2c8c12;
  color: #2c8c12;
}
.etaniAVCtrl {
  display: inline-block;
  vertical-align: top;
  margin-right: 5px;
  margin-bottom: 3px;
}
.etaniAVCtrl > span {
  display: inline-block;
  width: 24px;
  height: 24px;
  cursor: pointer;
  vertical-align: top;
  margin-right: 3px;
  box-sizing: border-box;
}
.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;
}
.etaniAVAdd {
  background-color: #a7fca7;
  border: 1px solid #71c371;
}
.etaniAVDelete {
  background-color: #ffcccc;
  border: 1px solid #cc3333;
}
.etaniAVCopy {
  background-color: #ccccff;
  border: 1px solid #6666cc;
}
.etaniAVMove {
  background-color: #ffcc99;
  border: 1px solid #cc9966;
}
.etaniAVDelete.active {
  background-color: #cc3333;
  color: white;
}
.etaniAVCopy.active {
  background-color: #6666cc;
  color: white;
}
.etaniAVMove.active {
  background-color: #cc9966;
  color: white;
}
.etaniAVCtrl > span > svg {
  margin-left: -1px;
  margin-top: -1px;
}
    `;

    document.head.appendChild(styleSheet);
}

// Append ctrl elements to etaniCtrl
function addEtaniCtrlElements(etaniinner) {
    // Create the etaniCtrl element
    const etaniCtrl = document.createElement('div');
    etaniCtrl.className = 'etaniCtrl';
    etaniinner.appendChild(etaniCtrl);

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

    // Create etaniContentHTML a
    const etaniContentHTML = document.createElement('a');
    etaniContentHTML.className = 'etaniContentHTML';
    etaniContentHTML.href = 'javascript:;';
    etaniContentHTML.innerHTML = 'HTML';
    etaniContent.appendChild(etaniContentHTML);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniContent);

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

    // Create etaniUpdateTiles a
    const etaniUpdateTiles = document.createElement('a');
    etaniUpdateTiles.className = 'etaniUpdateTiles';
    etaniUpdateTiles.href = 'javascript:;';
    etaniUpdateTiles.innerHTML = 'update';
    etaniUpdate.appendChild(etaniUpdateTiles);

    // Create etaniCenter a
    const etaniCenter = document.createElement('a');
    etaniCenter.className = 'etaniCenter';
    etaniCenter.href = 'javascript:;';
    etaniCenter.innerHTML = 'Center';
    etaniUpdate.appendChild(etaniCenter);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniUpdate);

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

    // Create etaniFilterTiles span
    const etaniFilterTiles = document.createElement('span');
    etaniFilterTiles.className = 'etaniFilterTiles active';
    etaniFilterTiles.innerHTML = 'tiles';
    etaniFilter.appendChild(etaniFilterTiles);

    // Create etaniFilterMoving span
    const etaniFilterMoving = document.createElement('span');
    etaniFilterMoving.className = 'etaniFilterMoving';
    etaniFilterMoving.innerHTML = 'moving';
    etaniFilter.appendChild(etaniFilterMoving);

    // Create etaniFilterBoard span
    const etaniFilterBoard = document.createElement('span');
    etaniFilterBoard.className = 'etaniFilterBoard';
    etaniFilterBoard.innerHTML = 'board';
    etaniFilter.appendChild(etaniFilterBoard);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniFilter);

    // Add click event listeners for etaniFilter spans
    const filterSpans = etaniFilter.querySelectorAll('span');
    filterSpans.forEach(function(span) {
        span.addEventListener('click', function() {
            // Remove active from all spans in etaniFilter
            filterSpans.forEach(function(s) { s.classList.remove('active'); });
            // Add active to clicked span
            this.classList.add('active');
            // Update visibility after filter change
            updateVisibility();

            const active = document.querySelector('.etaniFilter .active');
            if (active && active.classList[0] === 'etaniFilterMoving') {
                if (intervalMoving) clearInterval(intervalMoving);
                intervalMoving = setInterval(updateMovingTiles, 1000);
                // Call immediately
                updateMovingTiles();
            } else {
                if (intervalMoving) clearInterval(intervalMoving);
                intervalMoving = null;
            }
        });
    });

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

    // Create etaniModeRepeat span
    const etaniModeRepeat = document.createElement('span');
    etaniModeRepeat.className = 'etaniModeRepeat active';
    etaniModeRepeat.innerHTML = 'repeat';
    etaniMode.appendChild(etaniModeRepeat);

    // Create etaniModeFreeze span
    const etaniModeFreeze = document.createElement('span');
    etaniModeFreeze.className = 'etaniModeFreeze';
    etaniModeFreeze.innerHTML = 'freeze';
    etaniMode.appendChild(etaniModeFreeze);

    // Create etaniModeMixed span
    const etaniModeMixed = document.createElement('span');
    etaniModeMixed.className = 'etaniModeMixed';
    etaniModeMixed.innerHTML = 'mixed';
    etaniMode.appendChild(etaniModeMixed);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniMode);

    // Add click event listeners for etaniMode spans
    const modeSpans = etaniMode.querySelectorAll('span');
    modeSpans.forEach(function(span) {
        span.addEventListener('click', function() {
            // Remove active from all spans in etaniMode
            modeSpans.forEach(function(s) { s.classList.remove('active'); });
            // Add active to clicked span
            this.classList.add('active');
        });
    });

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

    // Create etaniAllAppendTransform a
    const etaniAllAppendTransform = document.createElement('a');
    etaniAllAppendTransform.className = 'etaniAllAppendTransform';
    etaniAllAppendTransform.href = 'javascript:;';
    etaniAllAppendTransform.innerHTML = 'transform';
    etaniAllAppend.appendChild(etaniAllAppendTransform);

    // Create etaniAllAppendOpacity a
    const etaniAllAppendOpacity = document.createElement('a');
    etaniAllAppendOpacity.className = 'etaniAllAppendOpacity';
    etaniAllAppendOpacity.href = 'javascript:;';
    etaniAllAppendOpacity.innerHTML = 'opacity';
    etaniAllAppend.appendChild(etaniAllAppendOpacity);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniAllAppend);

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

    // Create etaniValueIncrease a
    const etaniValueIncrease = document.createElement('a');
    etaniValueIncrease.className = 'etaniValueIncrease';
    etaniValueIncrease.href = 'javascript:;';
    etaniValueIncrease.innerHTML = 'increase';
    etaniValue.appendChild(etaniValueIncrease);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniValue);
}

// Define updateMovingTiles
function updateMovingTiles() {
    const movingUse = document.querySelector('#etmain > .etdrop > use.tilemoving');
    const items = document.querySelectorAll('.etaniItem');
    items.forEach(function(item) {
        item.style.display = 'none';
    });
    if (movingUse) {
        const id = movingUse.getAttribute('href').slice(1);
        const item = document.querySelector('.etaniItem[data-id="' + id + '"]');
        if (item) {
            item.style.display = 'block';
        }
    }
}

// list Etani elements
function listEtaniItems() {
    // Get the etaniCol element
    let etaniCol = document.querySelector('.etaniCol');
    if (!etaniCol) return; // Exit if etaniCol not found

    // Clear existing content in etaniCol
    etaniCol.innerHTML = '';

    // Collect elements into etaniElementArray
    let uses = etani.querySelectorAll('.etdrop > use');
    let etanidrop = etani.getElementById('etanidrop');
    let boardElements = etani.querySelectorAll('.etdrop > .etboard [id]');
    let etaniElementArray = [...uses, etanidrop, ...boardElements];

    // Loop through etaniElementArray to create etaniItem elements
    etaniElementArray.forEach(function(element) {
        if (!element) return; // Skip if element is null

        // Determine itemId and dataType
        let itemId;
        let dataType;
        if (element.tagName === 'use') {
            itemId = element.getAttribute('href').slice(1);
            dataType = 'tile';
        } else if (element.id === 'etanidrop') {
            itemId = 'board';
            dataType = 'board';
        } else {
            itemId = element.id;
            dataType = 'board';
        }

        // Create etaniItem div
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';
        etaniItem.dataset.id = itemId;
        etaniItem.dataset.type = dataType;

        // Create etaniItemLeft div
        const etaniItemLeft = document.createElement('div');
        etaniItemLeft.className = 'etaniItemLeft';
        etaniItem.appendChild(etaniItemLeft);

        // Create etaniItemImageOuter div
        const etaniItemImageOuter = document.createElement('div');
        etaniItemImageOuter.className = 'etaniItemImageOuter';
        etaniItemLeft.appendChild(etaniItemImageOuter);

        if (dataType === 'tile') {
            // Create etaniItemImage img
            const etaniItemImage = document.createElement('img');
            etaniItemImage.className = 'etaniItemImage';
            etaniItemImage.src = generateTileImage(itemId);
            etaniItemImageOuter.appendChild(etaniItemImage);
        } else {
            // Generate background color
            etaniItemImageOuter.style.background = generateHexColor(itemId);
        }

        // Create etaniItemId div
        const etaniItemId = document.createElement('div');
        etaniItemId.className = 'etaniItemId';
        etaniItemLeft.appendChild(etaniItemId);
        etaniItemId.addEventListener('click', function() {
            etaniAppendAnimateWindow(itemId, dataType);
        });

        // Create etaniItemName span
        const etaniItemName = document.createElement('span');
        etaniItemName.className = 'etaniItemName';
        etaniItemName.innerHTML = itemId;
        etaniItemId.appendChild(etaniItemName);

        // Create etaniItemPlus img
        const etaniItemPlus = document.createElement('img');
        etaniItemPlus.width = 12;
        etaniItemPlus.height = 12;
        etaniItemPlus.className = 'etaniItemPlus';
        etaniItemPlus.src = 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEyIiBoZWlnaHQ9IjEyIiBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjEiPjxsaW5lIHN0cm9rZT0iZGFya2dyZWVuIiB4MT0iNiIgeTE9IjAiIHgyPSI2IiB5Mj0iMTIiPjwvbGluZT48bGluZSBzdHJva2U9ImRhcmtncmVlbiIgeDE9IjAiIHkxPSI2IiB4Mj0iMTIiIHkyPSI2Ij48L2xpbmU+PC9zdmc+';
        etaniItemId.appendChild(etaniItemPlus);

        // Create etaniItemRight div
        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        etaniItem.appendChild(etaniItemRight);

        // Append etaniItem to etaniCol
        etaniCol.appendChild(etaniItem);
    });
}

// Define updateVisibility
function updateVisibility() {
    const active = document.querySelector('.etaniFilter .active');
    if (!active) return;

    const activeClass = active.classList[0]; // e.g., 'etaniFilterTiles'
    const items = document.querySelectorAll('.etaniItem');
    items.forEach(function(item) {
        item.style.display = 'none';
    });

    if (activeClass === 'etaniFilterTiles') {
        document.querySelectorAll('.etaniItem[data-type="tile"]').forEach(function(item) {
            item.style.display = 'block';
        });
    } else if (activeClass === 'etaniFilterBoard') {
        document.querySelectorAll('.etaniItem[data-type="board"]').forEach(function(item) {
            item.style.display = 'block';
        });
    } else if (activeClass === 'etaniFilterMoving') {
        document.querySelectorAll('.etaniItem[data-type="board"]').forEach(function(item) {
            item.style.display = 'block';
        });
        // Tiles visibility handled by interval
    }
}

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

// Define start button click
function etaniStartClick() {
    const etaniouter = document.querySelector('.etaniouter');

    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etani = originalSvg.cloneNode(true);
    const etwaitElement = etani.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    etani.id = 'etani';
    const etdropClone = etani.querySelector('.etdrop');
    if (etdropClone) {
        etdropClone.id = 'etanidrop';
    }

    // Check if etaniinner already exists
    let etaniinner = etaniouter.querySelector('.etaniinner');
    if (etaniinner) {
        // Remove etaniinner
        etaniouter.removeChild(etaniinner);
        // Restore button text
        etaniStart.innerHTML = 'start ejtile animate';
    } else {
        // Create etaniinner div
        etaniinner = document.createElement('div');
        etaniinner.className = 'etaniinner';

        // Create etaniCtrl div
        addEtaniCtrlElements(etaniinner);

        // Create etaniCol div
        const etaniCol = document.createElement('div');
        etaniCol.className = 'etaniCol';
        etaniinner.appendChild(etaniCol);

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

        etaniinner.appendChild(etaniResult);

        // Append etaniinner to etaniouter
        etaniouter.appendChild(etaniinner);

        // Change button text
        etaniStart.innerHTML = 'close ejtile animate';

        // list etani elements
        listEtaniItems();

        // update etani elements display by etaniFilter
        updateVisibility();

        // update result
        updateEtaniResult();
    }
}

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

    const etdropUses = document.querySelectorAll('#etmain > .etdrop > use');
    const etwaitGroups = document.querySelectorAll('#etmain > .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.appendChild(tileclone);
    
    const svgString = new XMLSerializer().serializeToString(svgWrapper);
    return svgToBase64(svgString);
}

// generate hex color
function generateHexColor(seed) {
  let hash = 0;
  for (let i = 0; i < seed.length; i++) {
    hash = seed.charCodeAt(i) + ((hash << 5) - hash);
  }
  let color = (hash & 0x00FFFFFF).toString(16).toUpperCase();
  while (color.length < 6) {
    color = '0' + color;
  }
  return '#' + color;
}

// Open append animate window
function etaniAppendAnimateWindow(id, dataType) {
    // Get the .etaniinner element
    const inner = document.querySelector('.etaniinner');
    if (!inner) {
        console.error('Element with class .etaniinner not found.');
        return;
    }
    let windowDiv = inner.querySelector('.etaniWindow');
    if (windowDiv) return;

    // Create the main div.etaniWindow
    windowDiv = document.createElement('div');
    windowDiv.className = 'etaniWindow';

    // First row
    const row1 = document.createElement('div');
    row1.className = 'etaniAppendRow';
    const strong = document.createElement('strong');
    strong.textContent = 'id: ' + id;
    row1.appendChild(strong);
    windowDiv.appendChild(row1);

    // Second row
    const row2 = document.createElement('div');
    row2.className = 'etaniAppendRow';
    const aTransform = document.createElement('a');
    aTransform.className = 'etaniAppendTransform';
    aTransform.href = 'javascript:;';
    aTransform.textContent = 'Transform';
    const aMotion = document.createElement('a');
    aMotion.className = 'etaniAppendMotion';
    aMotion.href = 'javascript:;';
    aMotion.textContent = 'Motion';
    const spanSet = document.createElement('span');
    spanSet.className = 'etaniAppendSet';
    spanSet.textContent = 'Set';
    row2.appendChild(aTransform);
    row2.appendChild(aMotion);
    row2.appendChild(spanSet);
    windowDiv.appendChild(row2);

    // Third row
    const row3 = document.createElement('div');
    row3.className = 'etaniAppendRow';
    const aOpacity = document.createElement('a');
    aOpacity.className = 'etaniAppendOpacity';
    aOpacity.href = 'javascript:;';
    aOpacity.textContent = 'Opacity';
    const aWriting = document.createElement('a');
    aWriting.className = 'etaniAppendWriting';
    aWriting.href = 'javascript:;';
    aWriting.textContent = 'Writing';
    row3.appendChild(aOpacity);
    row3.appendChild(aWriting);
    windowDiv.appendChild(row3);

    // Fourth row
    const row4 = document.createElement('div');
    row4.className = 'etaniAppendRow';
    const aFill = document.createElement('a');
    aFill.className = 'etaniAppendFill';
    aFill.href = 'javascript:;';
    aFill.textContent = 'Fill';
    const aStroke = document.createElement('a');
    aStroke.className = 'etaniAppendStroke';
    aStroke.href = 'javascript:;';
    aStroke.textContent = 'Stroke';
    const aWidth = document.createElement('a');
    aWidth.className = 'etaniAppendWidth';
    aWidth.href = 'javascript:;';
    aWidth.textContent = 'Width';
    row4.appendChild(aFill);
    row4.appendChild(aStroke);
    row4.appendChild(aWidth);
    windowDiv.appendChild(row4);

    // Fifth row
    const row5 = document.createElement('div');
    row5.className = 'etaniAppendRow';
    const label = document.createElement('label');
    label.textContent = 'specify: ';
    const input = document.createElement('input');
    input.type = 'text';
    input.className = 'etaniAppendSpecify';
    row5.appendChild(label);
    row5.appendChild(input);
    windowDiv.appendChild(row5);

    // Sixth row
    const row6 = document.createElement('div');
    row6.className = 'etaniAppendRow';
    const btnConfirm = document.createElement('button');
    btnConfirm.textContent = 'Confirm';
    const btnCancel = document.createElement('button');
    btnCancel.textContent = 'Cancel';
    row6.appendChild(btnConfirm);
    row6.appendChild(btnCancel);
    windowDiv.appendChild(row6);

    // Append the windowDiv to .etaniinner
    inner.appendChild(windowDiv);

    // Add click events to all specified elements
    // For a elements with href="javascript:;"
    aTransform.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, "animateTransform", "transform");
        windowDiv.remove();
    });
    aMotion.addEventListener('click', function() {
        // TODO: Implement Motion click functionality
    });
    aOpacity.addEventListener('click', function() {
        // TODO: Implement Opacity click functionality
    });
    aWriting.addEventListener('click', function() {
        // TODO: Implement Writing click functionality
    });
    aFill.addEventListener('click', function() {
        // TODO: Implement Fill click functionality
    });
    aStroke.addEventListener('click', function() {
        // TODO: Implement Stroke click functionality
    });
    aWidth.addEventListener('click', function() {
        // TODO: Implement Width click functionality
    });

    // For span.etaniAppendSet
    spanSet.addEventListener('click', function() {
        // TODO: Implement Set click functionality
    });

    // For buttons
    btnConfirm.addEventListener('click', function() {
        // TODO: Implement Confirm click functionality
    });
    btnCancel.addEventListener('click', function() {
        windowDiv.remove();
    });
}

// Update the result section with the current state of etani
function updateEtaniResult() {
    if (!etani) return;
    let etaniResult = document.querySelector('.etaniResult');
    if (!etaniResult) return;

    const svgString = new XMLSerializer().serializeToString(etani);
    const sizeInBytes = new Blob([svgString]).size;
    const base64Url = svgToBase64(svgString);
    
    // Generate default filename with current date and time
    const now = new Date();
    const defaultFilename = `ejtileAnimation_${now.toISOString().replace(/[-:T]/g, '').slice(0, 15)}.svg`;

    let imgElement = document.querySelector('.etaniResultImage');
    let downloadElementOuter = document.querySelector('.etaniResultDR');
    let downloadElement = document.querySelector('.etaniResultDownload');
    let renameElement = document.querySelector('.etaniResultRename');
    let sizeElement = document.querySelector('.etaniResultSize');
    if (!imgElement) {
        imgElement = document.createElement('img');
        imgElement.className = 'etaniResultImage';
        imgElement.alt = 'Rendered Ejtile Animation SVG';
        etaniResult.appendChild(imgElement);
    }
    if (!downloadElementOuter) {
        downloadElementOuter = document.createElement('div');
        downloadElementOuter.className = 'etaniResultDR';
    }
    if (!downloadElement) {
        downloadElement = document.createElement('a');
        downloadElement.className = 'etaniResultDownload';
        downloadElement.textContent = 'Download SVG';
        downloadElement.href = 'javascript:;';
        downloadElementOuter.appendChild(downloadElement);
        etaniResult.appendChild(downloadElementOuter);
    }
    if (!renameElement) {
        renameElement = document.createElement('a');
        renameElement.className = 'etaniResultRename';
        renameElement.textContent = 'Rename File';
        renameElement.href = 'javascript:;';
        downloadElementOuter.appendChild(renameElement);
        etaniResult.appendChild(downloadElementOuter);
    }
    if (!sizeElement) {
        sizeElement = document.createElement('span');
        sizeElement.className = 'etaniResultSize';
        etaniResult.appendChild(sizeElement);
    }

    if (imgElement && downloadElement && renameElement && sizeElement) {
        imgElement.src = base64Url;
        sizeElement.textContent = `Size: ${sizeInBytes} byte`;
        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}`);
            }
        };
    }
}

// create element and set className
function createEl(tag, className, textContent) {
    const el = document.createElement(tag);
    if (className) {
        el.className = className;
    }
    if (textContent) {
        el.textContent = textContent;
    }
    return el;
}

// parse transform values
function parseTransformValues(transformStr) {
    const transforms = {
        translate: '0,0',
        scale: '1,1',
        rotate: '0'
    };
    
    if (!transformStr) {
        return transforms;
    }

    // Extract translate values
    const translateMatch = /translate\(([^)]+)\)/.exec(transformStr);
    if (translateMatch) {
        transforms.translate = translateMatch[1].trim();
    }

    // Extract scale values
    const scaleMatch = /scale\(([^)]+)\)/.exec(transformStr);
    if (scaleMatch) {
        transforms.scale = scaleMatch[1].trim();
    }

    // Extract rotate values
    const rotateMatch = /rotate\(([^)]+)\)/.exec(transformStr);
    if (rotateMatch) {
        transforms.rotate = rotateMatch[1].trim();
    }
    
    return transforms;
}

// append etaniAnimate to etaniItemRight
function appendAnimateToResult(id, elementtype, tagname, animatetype, defaultvalue = null) {
    // 1. Find the SVG insertion target using the 'etani' global variable
    let targetSVGParent;
    if (elementtype === 'tile') {
        // For 'tile', the target is the <use> element in .etdrop
        targetSVGParent = etani.querySelector(`.etdrop > use[href="#${id}"]`);
    } else if (elementtype === 'board') {
        // For 'board', the target is the element (e.g., <g>) with the matching id
        targetSVGParent = etani.querySelector(`#${id}`);
    }

    if (!targetSVGParent) {
        console.error(`Error: SVG target parent (href="#${id}" or "#${id}") not found in 'etani' variable.`);
        return; 
    }

    // 2. Determine repeat or fill attributes from the UI state
    const repeatModeActive = document.querySelector('.etaniModeRepeat.active');
    let repeatOrFillAttrs = {};

    if (tagname === 'set') {
        // 'set' never has repeatCount
        if (!repeatModeActive) {
            repeatOrFillAttrs = { fill: "freeze" };
        }
    } else {
        // Other animation types
        if (repeatModeActive) {
            repeatOrFillAttrs = { repeatCount: "indefinite" };
        } else {
            repeatOrFillAttrs = { fill: "freeze" };
        }
    }

    // 3. Create and append SVG elements
    switch (tagname) {
        case 'animateTransform':
            // Find the source <use> element in #etmain to read the transform from
            const sourceElement = document.querySelector(`#etmain .etdrop > use[href="#${id}"]`);
            const transformString = sourceElement ? sourceElement.getAttribute('transform') : '';
            
            // Parse the existing transform values
            const transformValues = parseTransformValues(transformString);

            // Define base attributes for all 3 transform animations
            const baseAttrs = {
                attributeName: "transform",
                attributeType: "XML",
                ...repeatOrFillAttrs // Add the repeat/fill logic
            };

            // Create and append <animateTransform> for translate
            const elTranslate = createSVGElement('animateTransform', {
                ...baseAttrs,
                type: "translate",
                values: transformValues.translate
            });
            targetSVGParent.appendChild(elTranslate);

            // Create and append <animateTransform> for scale
            const elScale = createSVGElement('animateTransform', {
                ...baseAttrs,
                type: "scale",
                values: transformValues.scale,
                additive: "sum"
            });
            targetSVGParent.appendChild(elScale);

            // Create and append <animateTransform> for rotate
            const elRotate = createSVGElement('animateTransform', {
                ...baseAttrs,
                type: "rotate",
                values: transformValues.rotate,
                additive: "sum"
            });
            targetSVGParent.appendChild(elRotate);
            break;

        case 'animate':
            const elAnimateSVG = createSVGElement('animate', {
                attributeName: animatetype,
                values: defaultvalue,
                ...repeatOrFillAttrs
            });
            targetSVGParent.appendChild(elAnimateSVG);
            break;

        case 'animateMotion':
            const elMotion = createSVGElement('animateMotion', {
                dur: "1s",
                path: defaultvalue,
                ...repeatOrFillAttrs
            });
            targetSVGParent.appendChild(elMotion);
            break;

        case 'set':
            const elSet = createSVGElement('set', {
                attributeName: animatetype,
                to: defaultvalue,
                dur: "1s",
                ...repeatOrFillAttrs // Will be {fill: "freeze"} or {}
            });
            targetSVGParent.appendChild(elSet);
            break;
    }
}

// append etaniAnimate to etaniItemRight
function appendAnimateToItemRight(id, elementtype, tagname, animatetype, defaultvalue = null) {
    // Find the UI target parent element (in the .etaniinner UI panel)
    const targetUIParent = document.querySelector(`.etaniinner .etaniItem[data-id="${id}"] > .etaniItemRight`);
    
    if (!targetUIParent) {
        console.error(`Error: UI target (.etaniinner .etaniItem[data-id="${id}"] > .etaniItemRight) not found.`);
        return; 
    }

    // Build the UI structure
    const elAnimate = createEl('div', 'etaniAnimate');
    elAnimate.dataset.tagname = tagname;
    elAnimate.dataset.animatetype = animatetype;
    
    // 1. Build .etaniAnimateAttr
    const elAnimateAttr = createEl('div', 'etaniAnimateAttr');
    const elAnimateName = createEl('span', 'etaniAnimateName');
    const elAnimateDur = createEl('span', 'etaniAnimateDur');
    const elAnimateAttrAdd = createEl('span', 'etaniAnimateAttrAdd', '+');
    
    elAnimateAttr.appendChild(elAnimateName);
    elAnimateAttr.appendChild(elAnimateDur);
    elAnimateAttr.appendChild(elAnimateAttrAdd);

    // 2. Build .etaniAnimateValue
    const elAnimateValue = createEl('div', 'etaniAnimateValue');
    const elAVLabel = createEl('span', 'etaniAVLabel');
    const elAV = createEl('div', 'etaniAV');
    const elAVItem = createEl('span', 'etaniAVItem');
    
    elAV.appendChild(elAVItem);

    // create controls for this type
    const elAVCtrl = createEl('div', 'etaniAVCtrl');
    const etaniAVAdd = createEl('span', 'etaniAVAdd');
    etaniAVAdd.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>';
    const etaniAVDelete = createEl('span', 'etaniAVDelete');
    etaniAVDelete.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>';
    const etaniAVCopy = createEl('span', 'etaniAVCopy');
    etaniAVCopy.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
    const etaniAVMove = createEl('span', 'etaniAVMove');
    etaniAVMove.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><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"></path></svg>';
    elAVCtrl.appendChild(etaniAVAdd);
    elAVCtrl.appendChild(etaniAVDelete);
    elAVCtrl.appendChild(etaniAVCopy);
    elAVCtrl.appendChild(etaniAVMove);
    elAVCtrl.dataset.mode = 'edit';
    etaniAVAdd.addEventListener('click', () => {
        const active = elAVCtrl.querySelector('.active');
        if (active) active.classList.remove('active');
        elAnimateValue.dataset.mode = 'edit';
    });

    const toggleButtons = {
        delete: { element: etaniAVDelete, mode: 'delete' },
        copy: { element: etaniAVCopy, mode: 'copy' },
        move: { element: etaniAVMove, mode: 'move' }
    };

    Object.values(toggleButtons).forEach(({ element, mode }) => {
        element.addEventListener('click', function () {
            if (elAnimateValue.dataset.mode === mode) {
                this.classList.remove('active');
                elAnimateValue.dataset.mode = 'edit';
            } else {
                const active = elAnimateValue.querySelector('.active');
                if (active) active.classList.remove('active');
                this.classList.add('active');
                elAnimateValue.dataset.mode = mode;
            }
        });
    });

    // 3. Apply logic based on tagname
    switch (tagname) {
        case 'animateTransform':
            elAnimateName.textContent = 'transform';
            elAnimateDur.textContent = 'dur: 0s';
            elAVLabel.textContent = 'values : ';
            elAVItem.textContent = 'a'; // Default placeholder
            elAnimateValue.appendChild(elAVCtrl);
            break;
            
        case 'animate':
            elAnimateName.textContent = animatetype;
            elAnimateDur.textContent = 'dur: 0s';
            elAVLabel.textContent = 'values : ';
            elAVItem.textContent = defaultvalue;
            elAnimateValue.appendChild(elAVCtrl);
            break;

        case 'animateMotion':
            elAnimateName.textContent = 'motion';
            elAnimateDur.textContent = 'dur: 1s';
            elAVLabel.textContent = 'move to : ';
            elAVItem.textContent = defaultvalue;
            // No controls for this type
            break;

        case 'set':
            elAnimateName.textContent = animatetype;
            elAnimateDur.textContent = 'dur: 1s';
            elAVLabel.textContent = 'set to : ';
            elAVItem.textContent = defaultvalue;
            // No controls for this type
            break;
            
        default:
            console.error(`Error: Unknown tagname "${tagname}" for UI.`);
            return; 
    }

    // 4. Assemble the .etaniAnimateValue children
    elAnimateValue.appendChild(elAVLabel);
    elAnimateValue.appendChild(elAV);
    
    // 5. Assemble the final element
    elAnimate.appendChild(elAnimateAttr);
    elAnimate.appendChild(elAnimateValue);

    // 6. Append the fully constructed UI element to the DOM
    targetUIParent.appendChild(elAnimate);


}

// create SVG element
function createSVGElement(name, attrs) {
    const el = document.createElementNS('http://www.w3.org/2000/svg', name);
    for (const key in attrs) {
        el.setAttribute(key, attrs[key]);
    }
    return el;
}

// Appends animation UI controls and the corresponding SVG animation element.
function etaniAppendAnimate(id, elementtype, tagname, animatetype, defaultvalue = null) {
    appendAnimateToItemRight(id, elementtype, tagname, animatetype, defaultvalue);
    appendAnimateToResult(id, elementtype, tagname, animatetype, defaultvalue);
}

// Initialize the animation control panel on window load
window.addEventListener('load', function() {
    // Get the etmainouter element
    const etmainouter = document.getElementById('etmainouter');
    if (!etmainouter) return; // Exit if etmainouter not found

    // insert dynamic style
    addEtaniStyles();

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

    // Create etaniStart button
    const etaniStart = document.createElement('button');
    etaniStart.id = 'etaniStart';
    etaniStart.innerHTML = 'start ejtile animate';

    // Append button to etaniouter
    etaniouter.appendChild(etaniStart);

    // Insert etaniouter after etmainouter
    etmainouter.parentNode.insertBefore(etaniouter, etmainouter.nextSibling);

    // Add click event listener to etaniStart
    etaniStart.addEventListener('click', etaniStartClick);
});
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3970
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 190 次
联系:

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

帖子 ejsoon »

代码: 全选

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

/**
 * Creates a standard HTML element with optional class and text content.
 * @param {string} tag - The HTML tag name (e.g., 'div', 'span').
 * @param {string} [className] - Optional CSS class name.
 * @param {string} [textContent] - Optional text content.
 * @returns {HTMLElement} The created HTML element.
 */
function createEl(tag, className, textContent) {
    const el = document.createElement(tag);
    if (className) {
        el.className = className;
    }
    if (textContent) {
        el.textContent = textContent;
    }
    return el;
}

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

    const styleSheet = createEl('style');
    styleSheet.id = 'dynamic-et-styles'; 
    styleSheet.textContent = `
.etaniinner {
  margin-top: 10px;
}
.etaniCtrl {
  margin-bottom: 10px;
  clear: both;
  padding: 5px;
  border: 1px solid #c0c0c0;
  text-align: center;
}
.etaniCtrl > div {
  display: inline-block;
  vertical-align: top;
  padding: 5px;
  border: 1px solid #ccc;
  margin: 0 5px 5px 5px;
  text-align: left;
}
.etaniCtrl > div > a, .etaniCtrl > div > span {
  display: inline-block;
  text-decoration: none;
  padding: 2px 8px;
  font-size: 14px;
  margin: 0 2px;
  cursor: pointer;
  user-select: none;
}
.etaniCtrl > div > span {
  border: 1px solid #888;
  background-color: #eee;
  color: #333;
}
.etaniCtrl > div > span.active {
  background-color: #008CBA;
  color: white;
  border-color: #008CBA;
}
.etaniContentHTML {
  border: 1px solid #db3a32;
  color: #db3a32;
}
.etaniUpdateTiles {
  border: 1px solid #008CBA;
  color: #008CBA;
}
.etaniCenter {
  border: 1px solid green;
  color: green;
}
.etaniAllAppendTransform {
  border: 1px solid #2e36b9;
  color: #2e36b9;
}
.etaniAllAppendOpacity {
  border: 1px solid #b68942;
  color: #b68942;
}
.etaniValueIncrease {
  border: 1px solid purple;
  color: purple;
}
.etaniCol {
  border: 1px solid #aaa;
  padding: 5px;
  margin-bottom: 10px;
  clear: both;
}
.etaniItem {
  min-height: 48px;
  border: 1px solid #ccc;
  box-sizing: border-box;
  width: 100%;
  margin-bottom: -1px;
  background-color: lightyellow;
  display: inline-block;
}
.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;
}
.etaniItemId {
  text-align: center;
  font-size: 12px;
  word-break: break-all;
  margin-top: 2px;
  cursor: pointer;
}
.etaniItemPlus {
  width: 12px;
  height: 12px;
  display: inline-block;
  margin-left: 2px;
}
.etaniItemRight {
  margin-left: 60px;
  padding: 7px;
  min-height: 64px;
  background-color: #fff;
}
.etaniWindow {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: white;
  padding: 24px;
  border: 1px solid #ccc;
  box-shadow: 0 4px 8px rgba(0,0,0,0.2);
  z-index: 777;
  width: auto;
  display: inline-block;
}
.etaniAppendRow {
  margin-bottom: 8px;
}
.etaniAppendRow a, .etaniAppendRow span {
  display: inline-block;
  padding: 4px 8px;
  margin-right: 5px;
  border: 1px solid #888;
  background-color: #eee;
  color: #333;
  cursor: pointer;
  text-decoration: none;
  font-size: 12px;
}
.etaniWindow label {
  display: inline;
  margin-bottom: 10px;
}
.etaniAppendSpecify {
  display: inline;
  width: 120px;
  box-sizing: border-box;
  padding: 4px;
  font-size: 12px;
}
.etaniWindow button {
  margin-right: 10px;
  padding: 5px 10px;
}
.etaniResult {
  text-align: center;
  margin-bottom: 10px;
  padding: 10px;
  border: 1px solid #bbb;
  box-sizing: border-box;
}
.etaniResultImage {
  display: block;
  max-width: 480px;
  width: 100%;
  height: auto;
  margin: 0 auto 10px auto;
  border: 1px solid #000;
  box-sizing: border-box;
}
.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;
}
.etaniResultSize {
  display: inline-block;
  margin-left: 10px;
  font-size: 12px;
  color: #555;
}
    `;

    document.head.appendChild(styleSheet);
}

// Append ctrl elements to etaniCtrl
function addEtaniCtrlElements(etaniinner) {
    // Create the etaniCtrl element
    const etaniCtrl = createEl('div', 'etaniCtrl');
    etaniinner.appendChild(etaniCtrl);

    // Create etaniContent div
    const etaniContent = createEl('div', 'etaniContent');

    // Create etaniContentHTML a
    const etaniContentHTML = createEl('a', 'etaniContentHTML');
    etaniContentHTML.href = 'javascript:;';
    etaniContentHTML.textContent = 'HTML';
    etaniContent.appendChild(etaniContentHTML);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniContent);

    // Create etaniUpdate div
    const etaniUpdate = createEl('div', 'etaniUpdate');

    // Create etaniUpdateTiles a
    const etaniUpdateTiles = createEl('a', 'etaniUpdateTiles');
    etaniUpdateTiles.href = 'javascript:;';
    etaniUpdateTiles.textContent = 'update';
    etaniUpdate.appendChild(etaniUpdateTiles);

    // Create etaniCenter a
    const etaniCenter = createEl('a', 'etaniCenter');
    etaniCenter.href = 'javascript:;';
    etaniCenter.textContent = 'Center';
    etaniUpdate.appendChild(etaniCenter);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniUpdate);

    // Create etaniFilter div
    const etaniFilter = createEl('div', 'etaniFilter');

    // Create etaniFilterTiles span
    const etaniFilterTiles = createEl('span', 'etaniFilterTiles active', 'tiles');
    etaniFilter.appendChild(etaniFilterTiles);

    // Create etaniFilterMoving span
    const etaniFilterMoving = createEl('span', 'etaniFilterMoving', 'moving');
    etaniFilter.appendChild(etaniFilterMoving);

    // Create etaniFilterBoard span
    const etaniFilterBoard = createEl('span', 'etaniFilterBoard', 'board');
    etaniFilter.appendChild(etaniFilterBoard);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniFilter);

    // Add click event listeners for etaniFilter spans
    const filterSpans = etaniFilter.querySelectorAll('span');
    filterSpans.forEach(function(span) {
        span.addEventListener('click', function() {
            // Remove active from all spans in etaniFilter
            filterSpans.forEach(function(s) { s.classList.remove('active'); });
            // Add active to clicked span
            this.classList.add('active');
            // Update visibility after filter change
            updateVisibility();

            const active = document.querySelector('.etaniFilter .active');
            if (active && active.classList[0] === 'etaniFilterMoving') {
                if (intervalMoving) clearInterval(intervalMoving);
                intervalMoving = setInterval(updateMovingTiles, 1000);
                // Call immediately
                updateMovingTiles();
            } else {
                if (intervalMoving) clearInterval(intervalMoving);
                intervalMoving = null;
            }
        });
    });

    // Create etaniMode div
    const etaniMode = createEl('div', 'etaniMode');

    // Create etaniModeRepeat span
    const etaniModeRepeat = createEl('span', 'etaniModeRepeat active', 'repeat');
    etaniMode.appendChild(etaniModeRepeat);

    // Create etaniModeFreeze span
    const etaniModeFreeze = createEl('span', 'etaniModeFreeze', 'freeze');
    etaniMode.appendChild(etaniModeFreeze);

    // Create etaniModeMixed span
    const etaniModeMixed = createEl('span', 'etaniModeMixed', 'mixed');
    etaniMode.appendChild(etaniModeMixed);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniMode);

    // Add click event listeners for etaniMode spans
    const modeSpans = etaniMode.querySelectorAll('span');
    modeSpans.forEach(function(span) {
        span.addEventListener('click', function() {
            // Remove active from all spans in etaniMode
            modeSpans.forEach(function(s) { s.classList.remove('active'); });
            // Add active to clicked span
            this.classList.add('active');
        });
    });

    // Create etaniAllAppend div
    const etaniAllAppend = createEl('div', 'etaniAllAppend');

    // Create etaniAllAppendTransform a
    const etaniAllAppendTransform = createEl('a', 'etaniAllAppendTransform');
    etaniAllAppendTransform.href = 'javascript:;';
    etaniAllAppendTransform.textContent = 'transform';
    etaniAllAppend.appendChild(etaniAllAppendTransform);

    // Create etaniAllAppendOpacity a
    const etaniAllAppendOpacity = createEl('a', 'etaniAllAppendOpacity');
    etaniAllAppendOpacity.href = 'javascript:;';
    etaniAllAppendOpacity.textContent = 'opacity';
    etaniAllAppend.appendChild(etaniAllAppendOpacity);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniAllAppend);

    // Create etaniValue div
    const etaniValue = createEl('div', 'etaniValue');

    // Create etaniValueIncrease a
    const etaniValueIncrease = createEl('a', 'etaniValueIncrease');
    etaniValueIncrease.href = 'javascript:;';
    etaniValueIncrease.textContent = 'increase';
    etaniValue.appendChild(etaniValueIncrease);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniValue);
}

// Define updateMovingTiles function
function updateMovingTiles() {
    const movingUse = document.querySelector('#etmain > .etdrop > use.tilemoving');
    const items = document.querySelectorAll('.etaniItem');
    items.forEach(function(item) {
        item.style.display = 'none';
    });
    if (movingUse) {
        const id = movingUse.getAttribute('href').slice(1);
        const item = document.querySelector('.etaniItem[data-id="' + id + '"]');
        if (item) {
            item.style.display = 'block';
        }
    }
}

// list Etani elements
function listEtaniItems() {
    // Get the etaniCol element
    let etaniCol = document.querySelector('.etaniCol');
    if (!etaniCol) return; // Exit if etaniCol not found

    // Clear existing content in etaniCol
    etaniCol.innerHTML = '';

    // Collect elements into etaniElementArray
    let uses = etani.querySelectorAll('.etdrop > use');
    let etanidrop = etani.getElementById('etanidrop');
    let boardElements = etani.querySelectorAll('.etdrop > .etboard [id]');
    let etaniElementArray = [...uses, etanidrop, ...boardElements];

    // Loop through etaniElementArray to create etaniItem elements
    etaniElementArray.forEach(function(element) {
        if (!element) return; // Skip if element is null

        // Determine itemId and dataType
        let itemId;
        let dataType;
        if (element.tagName === 'use') {
            itemId = element.getAttribute('href').slice(1);
            dataType = 'tile';
        } else if (element.id === 'etanidrop') {
            itemId = 'board';
            dataType = 'board';
        } else {
            itemId = element.id;
            dataType = 'board';
        }

        // Create etaniItem div
        const etaniItem = createEl('div', 'etaniItem');
        etaniItem.dataset.id = itemId;
        etaniItem.dataset.type = dataType;

        // Create etaniItemLeft div
        const etaniItemLeft = createEl('div', 'etaniItemLeft');
        etaniItem.appendChild(etaniItemLeft);

        // Create etaniItemImageOuter div
        const etaniItemImageOuter = createEl('div', 'etaniItemImageOuter');
        etaniItemLeft.appendChild(etaniItemImageOuter);

        if (dataType === 'tile') {
            // Create etaniItemImage img
            const etaniItemImage = createEl('img', 'etaniItemImage');
            etaniItemImage.src = generateTileImage(itemId);
            etaniItemImageOuter.appendChild(etaniItemImage);
        } else {
            // Generate background color
            etaniItemImageOuter.style.background = generateHexColor(itemId);
        }

        // Create etaniItemId div
        const etaniItemId = createEl('div', 'etaniItemId');
        etaniItemLeft.appendChild(etaniItemId);
        etaniItemLeft.addEventListener('click', function() {
            etaniAppendAnimateWindow(itemId, dataType);
        });

        // Create etaniItemName span
        const etaniItemName = createEl('span', 'etaniItemName', itemId);
        etaniItemId.appendChild(etaniItemName);

        // Create etaniItemPlus img
        const etaniItemPlus = createEl('img', 'etaniItemPlus');
        etaniItemPlus.width = 12;
        etaniItemPlus.height = 12;
        etaniItemPlus.src = 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEyIiBoZWlnaHQ9IjEyIiBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjEiPjxsaW5lIHN0cm9rZT0iZGFya2dyZWVuIiB4MT0iNiIgeTE9IjAiIHgyPSI2IiB5Mj0iMTIiPjwvbGluZT48bGluZSBzdHJva2U9ImRhcmtncmVlbiIgeDE9IjAiIHkxPSI2IiB4Mj0iMTIiIHkyPSI2Ij48L2xpbmU+PC9zdmc+';
        etaniItemId.appendChild(etaniItemPlus);

        // Create etaniItemRight div
        const etaniItemRight = createEl('div', 'etaniItemRight');
        etaniItem.appendChild(etaniItemRight);

        // Append etaniItem to etaniCol
        etaniCol.appendChild(etaniItem);
    });
}

// Define updateVisibility function
function updateVisibility() {
    const active = document.querySelector('.etaniFilter .active');
    if (!active) return;

    const activeClass = active.classList[0]; // e.g., 'etaniFilterTiles'
    const items = document.querySelectorAll('.etaniItem');
    items.forEach(function(item) {
        item.style.display = 'none';
    });

    if (activeClass === 'etaniFilterTiles') {
        document.querySelectorAll('.etaniItem[data-type="tile"]').forEach(function(item) {
            item.style.display = 'block';
        });
    } else if (activeClass === 'etaniFilterBoard') {
        document.querySelectorAll('.etaniItem[data-type="board"]').forEach(function(item) {
            item.style.display = 'block';
        });
    } else if (activeClass === 'etaniFilterMoving') {
        document.querySelectorAll('.etaniItem[data-type="board"]').forEach(function(item) {
            item.style.display = 'block';
        });
        // Tiles visibility handled by interval
    }
}

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

// Define start button click function
function etaniStartClick() {
    const etaniouter = document.querySelector('.etaniouter');

    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etani = originalSvg.cloneNode(true);
    const etwaitElement = etani.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    etani.id = 'etani';
    const etdropClone = etani.querySelector('.etdrop');
    if (etdropClone) {
        etdropClone.id = 'etanidrop';
    }

    // Check if etaniinner already exists
    let etaniinner = etaniouter.querySelector('.etaniinner');
    if (etaniinner) {
        // Remove etaniinner
        etaniouter.removeChild(etaniinner);
        // Restore button text
        etaniStart.textContent = 'start ejtile animate';
    } else {
        // Create etaniinner div
        etaniinner = createEl('div', 'etaniinner');

        // Create etaniCtrl div
        addEtaniCtrlElements(etaniinner);

        // Create etaniCol div
        const etaniCol = createEl('div', 'etaniCol');
        etaniinner.appendChild(etaniCol);

        // Create etaniResult div
        const etaniResult = createEl('div', 'etaniResult');

        etaniinner.appendChild(etaniResult);

        // Append etaniinner to etaniouter
        etaniouter.appendChild(etaniinner);

        // Change button text
        etaniStart.textContent = 'close ejtile animate';

        // list etani elements
        listEtaniItems();

        // update etani elements display by etaniFilter
        updateVisibility();

        // update result
        updateEtaniResult();
    }
}

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

    const etdropUses = document.querySelectorAll('#etmain > .etdrop > use');
    const etwaitGroups = document.querySelectorAll('#etmain > .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.appendChild(tileclone);
    
    const svgString = new XMLSerializer().serializeToString(svgWrapper);
    return svgToBase64(svgString);
}

// generate hex color
function generateHexColor(seed) {
  let hash = 0;
  for (let i = 0; i < seed.length; i++) {
    hash = seed.charCodeAt(i) + ((hash << 5) - hash);
  }
  let color = (hash & 0x00FFFFFF).toString(16).toUpperCase();
  while (color.length < 6) {
    color = '0' + color;
  }
  return '#' + color;
}

// Open append animate window
function etaniAppendAnimateWindow(id, dataType) {
    // Get the .etaniinner element
    const inner = document.querySelector('.etaniinner');
    if (!inner) {
        console.error('Element with class .etaniinner not found.');
        return;
    }

    // Create the main div.etaniWindow
    const windowDiv = createEl('div', 'etaniWindow');

    // First row
    const row1 = createEl('div', 'etaniAppendRow');
    const strong = createEl('strong', null, 'id: ' + id);
    row1.appendChild(strong);
    windowDiv.appendChild(row1);

    // Second row
    const row2 = createEl('div', 'etaniAppendRow');
    const aTransform = createEl('a', 'etaniAppendTransform', 'Transform');
    aTransform.href = 'javascript:;';
    const aMotion = createEl('a', 'etaniAppendMotion', 'Motion');
    aMotion.href = 'javascript:;';
    const spanSet = createEl('span', 'etaniAppendSet', 'Set');
    row2.appendChild(aTransform);
    row2.appendChild(aMotion);
    row2.appendChild(spanSet);
    windowDiv.appendChild(row2);

    // Third row
    const row3 = createEl('div', 'etaniAppendRow');
    const aOpacity = createEl('a', 'etaniAppendOpacity', 'Opacity');
    aOpacity.href = 'javascript:;';
    const aWriting = createEl('a', 'etaniAppendWriting', 'Writing');
    aWriting.href = 'javascript:;';
    row3.appendChild(aOpacity);
    row3.appendChild(aWriting);
    windowDiv.appendChild(row3);

    // Fourth row
    const row4 = createEl('div', 'etaniAppendRow');
    const aFill = createEl('a', 'etaniAppendFill', 'Fill');
    aFill.href = 'javascript:;';
    const aStroke = createEl('a', 'etaniAppendStroke', 'Stroke');
    aStroke.href = 'javascript:;';
    const aWidth = createEl('a', 'etaniAppendWidth', 'Width');
    aWidth.href = 'javascript:;';
    row4.appendChild(aFill);
    row4.appendChild(aStroke);
    row4.appendChild(aWidth);
    windowDiv.appendChild(row4);

    // Fifth row
    const row5 = createEl('div', 'etaniAppendRow');
    const label = createEl('label', null, 'specify: ');
    const input = createEl('input', 'etaniAppendSpecify');
    input.type = 'text';
    row5.appendChild(label);
    row5.appendChild(input);
    windowDiv.appendChild(row5);

    // Sixth row
    const row6 = createEl('div', 'etaniAppendRow');
    const btnConfirm = createEl('button', null, 'Confirm');
    const btnCancel = createEl('button', null, 'Cancel');
    row6.appendChild(btnConfirm);
    row6.appendChild(btnCancel);
    windowDiv.appendChild(row6);

    // Append the windowDiv to .etaniinner
    inner.appendChild(windowDiv);

    // Add click events to all specified elements
    // For a elements with href="javascript:;"
    aTransform.addEventListener('click', function() {
		etaniAppendAnimate(id, dataType, "animateTransform", "transform");
    });
    aMotion.addEventListener('click', function() {
        // TODO: Implement Motion click functionality
    });
    aOpacity.addEventListener('click', function() {
        // TODO: Implement Opacity click functionality
    });
    aWriting.addEventListener('click', function() {
        // TODO: Implement Writing click functionality
    });
    aFill.addEventListener('click', function() {
        // TODO: Implement Fill click functionality
    });
    aStroke.addEventListener('click', function() {
        // TODO: Implement Stroke click functionality
    });
    aWidth.addEventListener('click', function() {
        // TODO: Implement Width click functionality
    });

    // For span.etaniAppendSet
    spanSet.addEventListener('click', function() {
        // TODO: Implement Set click functionality
    });

    // For buttons
    btnConfirm.addEventListener('click', function() {
        // TODO: Implement Confirm click functionality
    });
    btnCancel.addEventListener('click', function() {
        // TODO: Implement Cancel click functionality
    });
}

// Update the result section with the current state of etani
function updateEtaniResult() {
    if (!etani) return;
    let etaniResult = document.querySelector('.etaniResult');
    if (!etaniResult) return;

    const svgString = new XMLSerializer().serializeToString(etani);
    const sizeInBytes = new Blob([svgString]).size;
    const base64Url = svgToBase64(svgString);
    
    // Generate default filename with current date and time
    const now = new Date();
    const defaultFilename = `ejtileAnimation_${now.toISOString().replace(/[-:T]/g, '').slice(0, 15)}.svg`;

    let imgElement = document.querySelector('.etaniResultImage');
    let downloadElementOuter = document.querySelector('.etaniResultDR');
    let downloadElement = document.querySelector('.etaniResultDownload');
    let renameElement = document.querySelector('.etaniResultRename');
    let sizeElement = document.querySelector('.etaniResultSize');
    if (!imgElement) {
        imgElement = createEl('img', 'etaniResultImage');
        imgElement.alt = 'Rendered Ejtile Animation SVG';
        etaniResult.appendChild(imgElement);
    }
    if (!downloadElementOuter) {
        downloadElementOuter = createEl('div', 'etaniResultDR');
    }
    if (!downloadElement) {
        downloadElement = createEl('a', 'etaniResultDownload', 'Download SVG');
        downloadElement.href = 'javascript:;';
        downloadElementOuter.appendChild(downloadElement);
        etaniResult.appendChild(downloadElementOuter);
    }
    if (!renameElement) {
        renameElement = createEl('a', 'etaniResultRename', 'Rename File');
        renameElement.href = 'javascript:;';
        downloadElementOuter.appendChild(renameElement);
        etaniResult.appendChild(downloadElementOuter);
    }
    if (!sizeElement) {
        sizeElement = createEl('span', 'etaniResultSize');
        etaniResult.appendChild(sizeElement);
    }

    if (imgElement && downloadElement && renameElement && sizeElement) {
        imgElement.src = base64Url;
        sizeElement.textContent = `Size: ${sizeInBytes} byte`;
        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}`);
            }
        };
    }
}

/**
 * Appends animation UI controls and the corresponding SVG animation element.
 * * @param {string} id - The ID of the target .etaniItem.
 * @param {string} elementtype - The element type ('tile' or 'board') to determine the SVG insertion target.
 * @param {string} tagname - The animation tag name ('animate', 'animateTransform', 'animateMotion', 'set').
 * @param {string} animatetype - The attributeName to animate (e.g., 'opacity', 'fill').
 * @param {string} defaultvalue - The default value for the animation (e.g., '0;1;0', 'M 0 0 L 100 100').
 */
function etaniAppendAnimate(id, elementtype, tagname, animatetype, defaultvalue = null) {

    // --- Helper Functions ---

    /**
     * Creates an SVG element with the specified namespace and attributes.
     * @param {string} name - The SVG tag name (e.g., 'animate').
     * @param {object} attrs - An object of key-value attribute pairs.
     * @returns {Element} The created SVG element.
     */
    function createSVGElement(name, attrs) {
        const el = document.createElementNS('http://www.w3.org/2000/svg', name);
        for (const key in attrs) {
            el.setAttribute(key, attrs[key]);
        }
        return el;
    }

    /**
     * Parses an SVG transform attribute string into its components.
     * @param {string} transformStr - The transform attribute string from a <use> tag.
     * @returns {object} An object with translate, scale, and rotate values.
     */
    function parseTransformValues(transformStr) {
        const transforms = {
            translate: '0,0',
            scale: '1,1',
            rotate: '0'
        };
        
        if (!transformStr) {
            return transforms;
        }

        // Extract translate values
        const translateMatch = /translate\(([^)]+)\)/.exec(transformStr);
        if (translateMatch) {
            transforms.translate = translateMatch[1].trim();
        }

        // Extract scale values
        const scaleMatch = /scale\(([^)]+)\)/.exec(transformStr);
        if (scaleMatch) {
            transforms.scale = scaleMatch[1].trim();
        }

        // Extract rotate values
        const rotateMatch = /rotate\(([^)]+)\)/.exec(transformStr);
        if (rotateMatch) {
            transforms.rotate = rotateMatch[1].trim();
        }
        
        return transforms;
    }

    // --- Task 1: Append UI Element using DOM creation ---

    // Find the UI target parent element (in the .etaniinner UI panel)
    const targetUIParent = document.querySelector(`.etaniinner .etaniItem[data-id="${id}"] .etaniItemRight`);
    
    if (!targetUIParent) {
        console.error(`Error: UI target (.etaniItem[data-id="${id}"] .etaniItemRight) not found.`);
        return; 
    }

    // Build the UI structure
    const elAnimate = createEl('div', 'etaniAnimate');
    
    // 1. Build .etaniAnimateAttr
    const elAnimateAttr = createEl('div', 'etaniAnimateAttr');
    const elAnimateName = createEl('span', 'etaniAnimateName');
    const elAnimateDur = createEl('span', 'etaniAnimateDur');
    const elAnimateAttrAdd = createEl('span', 'etaniAnimateAttrAdd', '+');
    
    elAnimateAttr.appendChild(elAnimateName);
    elAnimateAttr.appendChild(elAnimateDur);
    elAnimateAttr.appendChild(elAnimateAttrAdd);

    // 2. Build .etaniAnimateValue
    const elAnimateValue = createEl('div', 'etaniAnimateValue');
    const elAVLabel = createEl('span', 'etaniAVLabel');
    const elAV = createEl('div', 'etaniAV');
    const elAVItem = createEl('span', 'etaniAVItem');
    
    elAV.appendChild(elAVItem);

    // 3. Apply logic based on tagname
    switch (tagname) {
        case 'animateTransform':
            elAnimateName.textContent = 'transform';
            elAnimateDur.textContent = 'dur: 0s';
            elAVLabel.textContent = 'values : ';
            elAVItem.textContent = 'a'; // Default placeholder

            // Add controls for this type
            const elAVCtrl_T = createEl('div', 'etaniAVCtrl');
            elAVCtrl_T.appendChild(createEl('img', 'etaniAVAdd'));
            elAVCtrl_T.appendChild(createEl('img', 'etaniAVDelete'));
            elAVCtrl_T.appendChild(createEl('img', 'etaniAVCopy'));
            elAVCtrl_T.appendChild(createEl('img', 'etaniAVMove'));
            // Insert controls before the label
            elAnimateValue.appendChild(elAVCtrl_T);
            break;
            
        case 'animate':
            elAnimateName.textContent = animatetype;
            elAnimateDur.textContent = 'dur: 0s';
            elAVLabel.textContent = 'values : ';
            elAVItem.textContent = defaultvalue;
            
            // Add controls for this type
            const elAVCtrl_A = createEl('div', 'etaniAVCtrl');
            elAVCtrl_A.appendChild(createEl('img', 'etaniAVAdd'));
            elAVCtrl_A.appendChild(createEl('img', 'etaniAVDelete'));
            elAVCtrl_A.appendChild(createEl('img', 'etaniAVCopy'));
            elAVCtrl_A.appendChild(createEl('img', 'etaniAVMove'));
            // Insert controls before the label
            elAnimateValue.appendChild(elAVCtrl_A);
            break;

        case 'animateMotion':
            elAnimateName.textContent = 'motion';
            elAnimateDur.textContent = 'dur: 1s';
            elAVLabel.textContent = 'move to : ';
            elAVItem.textContent = defaultvalue;
            // No controls for this type
            break;

        case 'set':
            elAnimateName.textContent = animatetype;
            elAnimateDur.textContent = 'dur: 1s';
            elAVLabel.textContent = 'set to : ';
            elAVItem.textContent = defaultvalue;
            // No controls for this type
            break;
            
        default:
            console.error(`Error: Unknown tagname "${tagname}" for UI.`);
            return; 
    }

    // 4. Assemble the .etaniAnimateValue children
    elAnimateValue.appendChild(elAVLabel);
    elAnimateValue.appendChild(elAV);
    
    // 5. Assemble the final element
    elAnimate.appendChild(elAnimateAttr);
    elAnimate.appendChild(elAnimateValue);

    // 6. Append the fully constructed UI element to the DOM
    targetUIParent.appendChild(elAnimate);


    // --- Task 2: Append SVG Element ---

    // 1. Find the SVG insertion target using the 'etani' global variable
    let targetSVGParent;
    if (elementtype === 'tile') {
        // For 'tile', the target is the <use> element in .etdrop
        targetSVGParent = etani.querySelector(`.etdrop > use[href="#${id}"]`);
    } else if (elementtype === 'board') {
        // For 'board', the target is the element (e.g., <g>) with the matching id
        targetSVGParent = etani.querySelector(`#${id}`);
    }

    if (!targetSVGParent) {
        console.error(`Error: SVG target parent (href="#${id}" or "#${id}") not found in 'etani' variable.`);
        return; 
    }

    // 2. Determine repeat or fill attributes from the UI state
    const repeatModeActive = document.querySelector('.etaniModeRepeat.active');
    let repeatOrFillAttrs = {};

    if (tagname === 'set') {
        // 'set' never has repeatCount
        if (!repeatModeActive) {
            repeatOrFillAttrs = { fill: "freeze" };
        }
    } else {
        // Other animation types
        if (repeatModeActive) {
            repeatOrFillAttrs = { repeatCount: "indefinite" };
        } else {
            repeatOrFillAttrs = { fill: "freeze" };
        }
    }

    // 3. Create and append SVG elements
    switch (tagname) {
        case 'animateTransform':
            // Find the source <use> element in #etmain to read the transform from
            const sourceElement = document.querySelector(`#etmain .etdrop > use[href="#${id}"]`);
            const transformString = sourceElement ? sourceElement.getAttribute('transform') : '';
            
            // Parse the existing transform values
            const transformValues = parseTransformValues(transformString);

            // Define base attributes for all 3 transform animations
            const baseAttrs = {
                attributeName: "transform",
                attributeType: "XML",
                ...repeatOrFillAttrs // Add the repeat/fill logic
            };

            // Create and append <animateTransform> for translate
            const elTranslate = createSVGElement('animateTransform', {
                ...baseAttrs,
                type: "translate",
                values: transformValues.translate
            });
            targetSVGParent.appendChild(elTranslate);

            // Create and append <animateTransform> for scale
            const elScale = createSVGElement('animateTransform', {
                ...baseAttrs,
                type: "scale",
                values: transformValues.scale,
                additive: "sum"
            });
            targetSVGParent.appendChild(elScale);

            // Create and append <animateTransform> for rotate
            const elRotate = createSVGElement('animateTransform', {
                ...baseAttrs,
                type: "rotate",
                values: transformValues.rotate,
                additive: "sum"
            });
            targetSVGParent.appendChild(elRotate);
            break;

        case 'animate':
            const elAnimateSVG = createSVGElement('animate', {
                attributeName: animatetype,
                values: defaultvalue,
                ...repeatOrFillAttrs
            });
            targetSVGParent.appendChild(elAnimateSVG);
            break;

        case 'animateMotion':
            const elMotion = createSVGElement('animateMotion', {
                dur: "1s",
                path: defaultvalue,
                ...repeatOrFillAttrs
            });
            targetSVGParent.appendChild(elMotion);
            break;

        case 'set':
            const elSet = createSVGElement('set', {
                attributeName: animatetype,
                to: defaultvalue,
                dur: "1s",
                ...repeatOrFillAttrs // Will be {fill: "freeze"} or {}
            });
            targetSVGParent.appendChild(elSet);
            break;
    }
}

// Initialize the animation control panel on window load
window.addEventListener('load', function() {
    // Get the etmainouter element
    const etmainouter = document.getElementById('etmainouter');
    if (!etmainouter) return; // Exit if etmainouter not found

    // insert dynamic style
    addEtaniStyles();

    // Create etaniouter div
    const etaniouter = createEl('div', 'etaniouter');

    // Create etaniStart button
    const etaniStart = createEl('button', null, 'start ejtile animate');
    etaniStart.id = 'etaniStart';

    // Append button to etaniouter
    etaniouter.appendChild(etaniStart);

    // Insert etaniouter after etmainouter
    etmainouter.parentNode.insertBefore(etaniouter, etmainouter.nextSibling);

    // Add click event listener to etaniStart
    etaniStart.addEventListener('click', etaniStartClick);
});
grok expert
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复

在线用户

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