正在做etani,ejtile的動畫套件

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

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

帖子 ejsoon »

將下面代碼提取出一個通用函式function etaniOpenWindow(content, confirm_function)。

代码: 全选

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

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

帖子 ejsoon »

claude sonnet4.5

代码: 全选

/**
 * Open a modal window with custom content
 * @param {HTMLElement|string} content - The content to display in the window (HTML element or HTML string)
 * @param {Function} confirmFunction - Callback function to execute when confirm button is clicked
 * @param {Function} cancelFunction - Optional callback function to execute when cancel button is clicked
 * @returns {HTMLElement|null} The created window element or null if already exists
 */
function etaniOpenWindow(content, confirmFunction, cancelFunction = null) {
    // Get the .etaniinner element
    const inner = document.querySelector('.etaniinner');
    if (!inner) {
        console.error('Element with class .etaniinner not found.');
        return null;
    }
    
    // Check if window already exists
    let windowDiv = inner.querySelector('.etaniWindow');
    if (windowDiv) return null;
    
    // Create the main div.etaniWindow
    windowDiv = createEl('div', 'etaniWindow');
    
    // Add content to window
    if (typeof content === 'string') {
        windowDiv.innerHTML = content;
    } else if (content instanceof HTMLElement) {
        windowDiv.appendChild(content);
    }
    
    // Create button row
    const buttonRow = createEl('div', 'etaniAppendRow');
    const btnConfirm = createEl('button', null, 'Confirm');
    const btnCancel = createEl('button', null, 'Cancel');
    buttonRow.appendChild(btnConfirm);
    buttonRow.appendChild(btnCancel);
    windowDiv.appendChild(buttonRow);
    
    // Append the windowDiv to .etaniinner
    inner.appendChild(windowDiv);
    
    // Add button event listeners
    btnConfirm.addEventListener('click', function() {
        if (typeof confirmFunction === 'function') {
            confirmFunction(windowDiv);
        }
        windowDiv.remove();
    });
    
    btnCancel.addEventListener('click', function() {
        if (typeof cancelFunction === 'function') {
            cancelFunction(windowDiv);
        }
        windowDiv.remove();
    });
    
    return windowDiv;
}
grok expert

代码: 全选

// Open a generic window
function etaniOpenWindow(content, confirm_function) {
    // 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 = createEl('div', 'etaniWindow');
    
    // Append custom content to the window
    if (typeof content === 'string') {
        windowDiv.innerHTML = content;
    } else if (content instanceof Element) {
        windowDiv.appendChild(content);
    } else {
        console.error('Invalid content type provided.');
        return;
    }
    
    // Add confirm and cancel buttons if not already in content
    let btnConfirm = windowDiv.querySelector('button.confirm'); // Assume class or id for identification
    let btnCancel = windowDiv.querySelector('button.cancel');
    if (!btnConfirm || !btnCancel) {
        const rowButtons = createEl('div', 'etaniAppendRow');
        btnConfirm = createEl('button', null, 'Confirm');
        btnCancel = createEl('button', null, 'Cancel');
        rowButtons.appendChild(btnConfirm);
        rowButtons.appendChild(btnCancel);
        windowDiv.appendChild(rowButtons);
    }
    
    // Append the windowDiv to .etaniinner
    inner.appendChild(windowDiv);
    
    // Add event listeners
    if (btnConfirm) {
        btnConfirm.addEventListener('click', function() {
            if (typeof confirm_function === 'function') {
                confirm_function();
            }
            // TODO: Implement additional Confirm functionality if needed
            windowDiv.remove();
        });
    }
    if (btnCancel) {
        btnCancel.addEventListener('click', function() {
            windowDiv.remove();
        });
    }
    
    // Note: Other specific event listeners (e.g., for Transform, Motion) should be handled within the custom content or separately
}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4360
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 170 次
被圈友点赞: 197 次
联系:

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 = 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;
}
.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;
}
textarea.etaniHTMLTextarea {
  width: calc(100vw - 72px);
  height: calc(50vh - 24px);
  resize: none;
  border: 1px solid #ccc;
  font-size: 12px;
  box-sizing: border-box;
}
.etaniWindowRow {
  padding-top: 12px;
}
    `;

    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';
    etaniContentHTML.addEventListener('click', handleContentHTMLClick);
    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 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');
        etaniItemId.addEventListener('click', function() {
            etaniAppendAnimateWindow(itemId, dataType);
        });
        etaniItemLeft.appendChild(etaniItemId);

        // 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 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.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;
}

// Handle the HTML popup window
function handleContentHTMLClick() {
    if (!etani) return;
    const textarea = createEl('textarea', 'etaniHTMLTextarea');
    textarea.value = new XMLSerializer().serializeToString(etani);
    etaniWindow(textarea);
}

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

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

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

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

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

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

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

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

// Open a generic window
function etaniWindow(content, confirm_function = null) {
    // 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 = createEl('div', 'etaniWindow');
    
    // Append custom content to the window
    if (typeof content === 'string') {
        windowDiv.innerHTML = content;
    } else if (content instanceof Element) {
        windowDiv.appendChild(content);
    } else {
        console.error('Invalid content type provided.');
        return;
    }
    
    // Add confirm and cancel buttons if not already in content
    let btnConfirm = windowDiv.querySelector('button.confirm'); // Assume class or id for identification
    let btnCancel = windowDiv.querySelector('button.cancel');
    if (!btnConfirm || !btnCancel) {
        const rowButtons = createEl('div', 'etaniWindowRow');
        btnConfirm = createEl('button', 'confirm', 'Confirm');
        btnCancel = createEl('button', 'cancel', 'Cancel');
        rowButtons.appendChild(btnConfirm);
        rowButtons.appendChild(btnCancel);
        windowDiv.appendChild(rowButtons);
    }
    
    // Append the windowDiv to .etaniinner
    inner.appendChild(windowDiv);
    
    // Add event listeners
    if (btnConfirm) {
        btnConfirm.addEventListener('click', function() {
            if (typeof confirm_function === 'function') {
                confirm_function();
            }
            windowDiv.remove();
        });
    }
    if (btnCancel) {
        btnCancel.addEventListener('click', function() {
            windowDiv.remove();
        });
    }
}

// Open append animate window
function etaniAppendAnimateWindow(id, dataType) {
    const appendAnimateWindow = createEl('div', 'appendAnimateWindow');
    // First row
    const row1 = createEl('div', 'etaniAppendRow');
    const strong = createEl('strong', null, 'id: ' + id);
    row1.appendChild(strong);
    appendAnimateWindow.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);
    appendAnimateWindow.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);
    appendAnimateWindow.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);
    appendAnimateWindow.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);
    appendAnimateWindow.appendChild(row5);
    etaniWindow(appendAnimateWindow);

    // Add click events to all specified elements
    // For a elements with href="javascript:;"
    aTransform.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, "animateTransform", "transform");
        document.querySelector('.etaniWindow').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
    });
}

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

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

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

帖子 ejsoon »

當前代碼(加了set選擇高亮):

代码: 全选

// 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 = 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;
}
.etaniAppendSet.active {
  background-color: #008CBA;
  color: white;
  border-color: #008CBA;
}
.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;
}
textarea.etaniHTMLTextarea {
  width: calc(100vw - 72px);
  height: calc(50vh - 24px);
  resize: none;
  border: 1px solid #ccc;
  font-size: 12px;
  box-sizing: border-box;
}
.etaniWindowRow {
  padding-top: 12px;
}
    `;

    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';
    etaniContentHTML.addEventListener('click', handleContentHTMLClick);
    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 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');
        etaniItemId.addEventListener('click', function() {
            etaniAppendAnimateWindow(itemId, dataType);
        });
        etaniItemLeft.appendChild(etaniItemId);

        // 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 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.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;
}

// Handle the HTML popup window
function handleContentHTMLClick() {
    if (!etani) return;
    const textarea = createEl('textarea', 'etaniHTMLTextarea');
    textarea.value = new XMLSerializer().serializeToString(etani);
    etaniWindow(textarea);
}

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

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

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

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

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

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

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

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

// Open a generic window
function etaniWindow(content, confirm_function = null) {
    // 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 = createEl('div', 'etaniWindow');
    
    // Append custom content to the window
    if (typeof content === 'string') {
        windowDiv.innerHTML = content;
    } else if (content instanceof Element) {
        windowDiv.appendChild(content);
    } else {
        console.error('Invalid content type provided.');
        return;
    }
    
    // Add confirm and cancel buttons if not already in content
    let btnConfirm = windowDiv.querySelector('button.confirm'); // Assume class or id for identification
    let btnCancel = windowDiv.querySelector('button.cancel');
    if (!btnConfirm || !btnCancel) {
        const rowButtons = createEl('div', 'etaniWindowRow');
        btnConfirm = createEl('button', 'confirm', 'Confirm');
        btnCancel = createEl('button', 'cancel', 'Cancel');
        rowButtons.appendChild(btnConfirm);
        rowButtons.appendChild(btnCancel);
        windowDiv.appendChild(rowButtons);
    }
    
    // Append the windowDiv to .etaniinner
    inner.appendChild(windowDiv);
    
    // Add event listeners
    if (btnConfirm) {
        btnConfirm.addEventListener('click', function() {
            if (typeof confirm_function === 'function') {
                confirm_function();
            }
            windowDiv.remove();
        });
    }
    if (btnCancel) {
        btnCancel.addEventListener('click', function() {
            windowDiv.remove();
        });
    }
}

// Open append animate window
function etaniAppendAnimateWindow(id, dataType) {
    const appendAnimateWindow = createEl('div', 'appendAnimateWindow');
    // First row
    const row1 = createEl('div', 'etaniAppendRow');
    const strong = createEl('strong', null, 'id: ' + id);
    row1.appendChild(strong);
    appendAnimateWindow.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);
    appendAnimateWindow.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);
    appendAnimateWindow.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);
    appendAnimateWindow.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);
    appendAnimateWindow.appendChild(row5);
    etaniWindow(appendAnimateWindow);

    // Add click events to all specified elements
    // For a elements with href="javascript:;"
    aTransform.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, "animateTransform", "transform");
        document.querySelector('.etaniWindow').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() {
        if (spanSet.classList.contains('active')) {
            this.classList.remove('active');
            aTransform.style.pointerEvents = 'auto';
            aTransform.style.opacity = '1'; // Add disabled CSS state
            aMotion.style.pointerEvents = 'auto';
            aMotion.style.opacity = '1'; // Add disabled CSS state
        } else {
            this.classList.add('active');
            aTransform.style.pointerEvents = 'none';
            aTransform.style.opacity = '0.5'; // Add disabled CSS state
            aMotion.style.pointerEvents = 'none';
            aMotion.style.opacity = '0.5'; // Add disabled CSS state
        }
    });
}

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

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

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

帖子 ejsoon »

etaniAVAdd點擊事件:

代码: 全选

寫一段js代碼:

一,在DOM結構中往前迴溯至etaniAVAdd的一個class='etaniAnimate'的父元素,拿到它的data-animatetype和data-tagname,並得到這個父元素在同級的index,賦值給animateIndex。

二,在DOM結構中往前迴溯至etaniAVAdd的一個class='etaniItem'的父元素,拿到它的data-id和data-type。

三,找到目標動畫元素。

根據data-type的不同,分為以下兩種情況:

3.1如果data-type==='tile',找到etani中的「.etdrop>use[href=#id]」,在它的內部,將所有的動畫元素存進一個數組中。動畫元素包括「animateTransform, animate, animateMotion, set」這四種。注意:animateTransform會連續三個出現,把它們當成一個。之後用animateIndex來確定指的是哪一個動畫元素。

3.2如果data-type==='board',找到etani內部的子元素中的動畫元素,即「#etani>animateTransform」、「#etani>animate」、「#etani>animateMotion」、「#etani>set」這四種元素,用它們的href='#id'篩選之後,將結果按順序保存進一個數組中。注意:animateTransform會連續三個出現,把它們當成一個。之後用animateIndex來確定指的是哪一個動畫元素。

四,在目標動畫元素中插入值

如果data-tagname是animateTransform,就先拿到「#etmain>.etdrop>use[href=#id]」的transform屬性,提取出「translate、scale、rotate」三個值,再把它們加到目標動畫元素中的values的最後。
如果data-tagname是animate,就把默認值加到目標動畫元素中的values的最後。默認值根據data-animatetype的不同而有所不同,如果是'opacity',默認值為1,如果是'stroke-width',默認值為2,如果是其它,默認值是0。

五,在DOM結構中往前迴溯至etaniAVAdd的一個class='etaniAnimateValue'的父元素,它有一個子元素class='etaniAV',在這個子元素的最後加上<span class="etaniAVItem"></span>。

如果data-tagname是animateTransform,它的innerHTML就是a(TODO: change char by value)。
如果data-tagname是animate,它的innerHTML就是默認值。

etaniAVAdd是已定義的變量,可以直接使用。只需要提供js代碼。用繁體中文回答。js代碼中的注釋要全部是英文。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4360
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 170 次
被圈友点赞: 197 次
联系:

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

帖子 ejsoon »

gemini 2.5pro

代码: 全选

// --- Helper Functions ---

/**
 * Appends a value to an element's 'values' attribute, separated by semicolons.
 * @param {Element} element - The animation element.
 * @param {string|number} value - The value to append.
 */
function appendValueToAttribute(element, value) {
    // Do not append if element or value is invalid
    if (!element || value === null || typeof value === 'undefined') return;
    
    let currentValues = element.getAttribute('values');
    const stringValue = String(value); // Ensure value is a string

    if (currentValues && currentValues.trim() !== '') {
        // Add with a semicolon if values already exist
        element.setAttribute('values', currentValues + ';' + stringValue);
    } else {
        // Set as the first value
        element.setAttribute('values', stringValue);
    }
}

/**
 * Extracts a specific transform function's value (e.g., "10 20" from "translate(10 20)")
 * @param {string} type - The transform type (e.g., 'translate', 'scale').
 * @param {string} transformString - The full transform attribute string.
 * @returns {string|null} The extracted value or null if not found.
 */
function getTransformValue(type, transformString) {
    if (!transformString) return null;
    // Regex to find the type and capture the content inside the parentheses
    const regex = new RegExp(`${type}\\(([^)]+)\\)`);
    const match = transformString.match(regex);
    return match ? match[1] : null; // Return the captured group (the values)
}

// --- Main Logic ---

// Note: This code assumes 'etaniAVAdd' is a pre-defined DOM element variable.
// It also assumes elements with id 'etani' and 'etmain' exist in the DOM.

try {
    // --- Step 1: Get .etaniAnimate parent data ---
    const animateParent = etaniAVAdd.closest('.etaniAnimate');
    if (!animateParent) {
        throw new Error('Could not find parent .etaniAnimate');
    }

    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;

    // Get the index of this element among its siblings
    const parentChildren = Array.from(animateParent.parentNode.children);
    const animateIndex = parentChildren.indexOf(animateParent);

    if (animateIndex === -1) {
         throw new Error('Could not determine animateIndex');
    }

    // --- Step 2: Get .etaniItem parent data ---
    const itemParent = etaniAVAdd.closest('.etaniItem');
    if (!itemParent) {
        throw new Error('Could not find parent .etaniItem');
    }

    const dataId = itemParent.dataset.id;
    const dataType = itemParent.dataset.type;
    const hrefId = `#${dataId}`; // Prepare for attribute selector

    // --- Step 3: Find target animation elements ---
    const etani = document.getElementById('etani');
    if (!etani) {
        throw new Error('Could not find #etani element');
    }

    const animationTags = 'animateTransform, animate, animateMotion, set';
    let animations = [];

    if (dataType === 'tile') {
        const useElement = etani.querySelector(`.etdrop > use[href="${hrefId}"]`);
        if (useElement) {
            // Find all animation elements inside the <use> tag
            animations = Array.from(useElement.querySelectorAll(animationTags));
        }
    } else if (dataType === 'board') {
        // Find all direct children of #etani matching the tags and href
        const selector = `:scope > animateTransform[href="${hrefId}"], 
                        :scope > animate[href="${hrefId}"], 
                        :scope > animateMotion[href="${hrefId}"], 
                        :scope > set[href="${hrefId}"]`;
        animations = Array.from(etani.querySelectorAll(selector));
    }

    // Group consecutive animateTransforms (3 at a time)
    const groupedAnimations = [];
    for (let i = 0; i < animations.length; ) {
        const currentAnim = animations[i];
        if (currentAnim.tagName.toLowerCase() === 'animatetransform') {
            // Assume 3 consecutive animateTransforms
            if (i + 2 < animations.length &&
                animations[i+1].tagName.toLowerCase() === 'animatetransform' &&
                animations[i+2].tagName.toLowerCase() === 'animatetransform') 
            {
                groupedAnimations.push([animations[i], animations[i+1], animations[i+2]]);
                i += 3;
            } else {
                // Handle incomplete groups: log warning and skip this element
                console.warn('Incomplete or non-consecutive animateTransform group found.', currentAnim);
                i++; // Skip this one to avoid infinite loop
            }
        } else {
            // Add other animation types as single items
            groupedAnimations.push(currentAnim);
            i++;
        }
    }

    // Get the specific target animation (group) using the index
    const targetAnimation = groupedAnimations[animateIndex];
    if (!targetAnimation) {
        throw new Error(`No animation element found at index ${animateIndex}`);
    }

    // --- Step 4: Insert values into target element ---
    let defaultValue; // This will be used for both Step 4 and 5

    if (tagName === 'animatetransform') {
        // Check if targetAnimation is valid (an array of 3)
        if (Array.isArray(targetAnimation) && targetAnimation.length === 3) {
            const etmain = document.getElementById('etmain');
            if (!etmain) {
                throw new Error('Could not find #etmain element');
            }

            const mainUseElement = etmain.querySelector(`.etdrop > use[href="${hrefId}"]`);
            
            if (mainUseElement) {
                const transformString = mainUseElement.getAttribute('transform');
                
                // Extract values
                const translateVal = getTransformValue('translate', transformString);
                const scaleVal = getTransformValue('scale', transformString);
                const rotateVal = getTransformValue('rotate', transformString);

                // Add values to the 'values' attribute of each corresponding animation
                // Assuming order: [0] = translate, [1] = scale, [2] = rotate
                appendValueToAttribute(targetAnimation[0], translateVal);
                appendValueToAttribute(targetAnimation[1], scaleVal);
                appendValueToAttribute(targetAnimation[2], rotateVal);
            }
        } else {
             console.warn('Expected targetAnimation to be a group of 3, but it was not.', targetAnimation);
        }
    } else if (tagName === 'animate') {
        // Check if targetAnimation is a single element
        if (targetAnimation && !Array.isArray(targetAnimation)) {
            // Determine default value based on animateType
            if (animateType === 'opacity') {
                defaultValue = 1;
            } else if (animateType === 'stroke-width') {
                defaultValue = 2;
            } else {
                defaultValue = 0;
            }
            
            // Add to values attribute
            appendValueToAttribute(targetAnimation, defaultValue);
        } else {
            console.warn('Expected targetAnimation to be a single element, but it was not.', targetAnimation);
        }
    }

    // --- Step 5: Add <span> to .etaniAV ---
    const avParent = etaniAVAdd.closest('.etaniAnimateValue');
    if (avParent) {
        const etaniAV = avParent.querySelector('.etaniAV');
        if (etaniAV) {
            const newSpan = document.createElement('span');
            newSpan.className = 'etaniAVItem';

            if (tagName === 'animatetransform') {
                // As requested by the prompt
                newSpan.innerHTML = 'a'; // TODO: change char by value
            } else if (tagName === 'animate') {
                // Use the defaultValue calculated in step 4
                newSpan.innerHTML = defaultValue;
            }

            // Add the new span to the end
            etaniAV.appendChild(newSpan);
        } else {
            console.warn('Could not find .etaniAV child in .etaniAnimateValue');
        }
    } else {
        console.warn('Could not find .etaniAnimateValue parent');
    }

} catch (error) {
    console.error('An error occurred during the animation value processing:', error.message);
}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4360
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 170 次
被圈友点赞: 197 次
联系:

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

帖子 ejsoon »

寫一段js函式function setUIAnimateValues,改變動畫元素的values之後,刷新面板。

代码: 全选

寫一段js函式function setUIAnimateValues(tagname, targetAnimation, valuesElement):

一,當tagname === 'animateTransform'
則targetAnimation將是一個數組,內含三個Element,分別是translate, scale, rotate。
用getAttribute('values')的方式拿到這三項的values,用;切分,得到三個數組。這三個數組的長度是相同的。
再把這三個數組對應的每一項,用「;」連接起來,然後保存到一個數組中。
之後逐個檢視這個數組的每一項的值,得到它的代表字母。
第一項的代表字母是a。如果後面的每一項的值都不相同,則為字母序「abcdefg...xyzABCDEFG...XYZ」。
當某一項的值跟前面的某一項相同,則使用同一個代表字母,比如「abcad」說明第四項的值跟第一項相同。
把每一項的代表字母用以下代碼放進一個span標籤中:
const newSpan = createEl('span', 'etaniAVItem', '代表字母');
再把它們append進valuesElement。

二,當tagname === 'animate'
targetAnimation將是一個Element。拿到它的values的值用;切分,得到一個數組。
把每一項的值用以下代碼放進一個span標籤中:
const newSpan = createEl('span', 'etaniAVItem', '每一項的值');
再把它們append進valuesElement。

三,當tagname === 'animateMotion'
targetAnimation將是一個Element。
如果它存在path屬性,則將它的值填入
const newSpan = createEl('span', 'etaniAVItem', 'path的值');
如果它不存在path屬性,則將它內部的<mpath>中的href的值(帶#)填入
const newSpan = createEl('span', 'etaniAVItem', 'href的值');
然後再把這個newSpan放進valuesElement。

四,當tagname === 'set'
targetAnimation將是一個Element。拿到它的屬性to的值,放進一個span標籤中:
const newSpan = createEl('span', 'etaniAVItem', 'to的值');
再把這個newSpan放進valuesElement。

只需要提供js代碼。用繁體中文回答。js代碼中的注釋要全部是英文。
上次由 ejsoon 在 2025年 10月 31日 11:44,总共编辑 1 次。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4360
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 170 次
被圈友点赞: 197 次
联系:

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

帖子 ejsoon »

claude.ai sonnet 4.5

代码: 全选

function setUIAnimateValues(tagname, targetAnimation, valuesElement) {
  if (tagname === 'animateTransform') {
    // targetAnimation is an array with three elements: translate, scale, rotate
    const translateValues = targetAnimation[0].getAttribute('values').split(';');
    const scaleValues = targetAnimation[1].getAttribute('values').split(';');
    const rotateValues = targetAnimation[2].getAttribute('values').split(';');
    
    // Combine corresponding items from three arrays with space separator
    const combinedValues = [];
    for (let i = 0; i < translateValues.length; i++) {
      combinedValues.push(`${translateValues[i]} ${scaleValues[i]} ${rotateValues[i]}`);
    }
    
    // Generate letter representation for each unique value
    const letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    const valueToLetter = new Map();
    let letterIndex = 0;
    
    combinedValues.forEach(value => {
      if (!valueToLetter.has(value)) {
        valueToLetter.set(value, letters[letterIndex++]);
      }
      const letter = valueToLetter.get(value);
      const newSpan = createEl('span', 'etaniAVItem', letter);
      valuesElement.appendChild(newSpan);
    });
    
  } else if (tagname === 'animate') {
    // targetAnimation is a single element
    const values = targetAnimation.getAttribute('values').split(';');
    
    values.forEach(value => {
      const newSpan = createEl('span', 'etaniAVItem', value);
      valuesElement.appendChild(newSpan);
    });
    
  } else if (tagname === 'animateMotion') {
    // targetAnimation is a single element
    let pathValue = targetAnimation.getAttribute('path');
    
    if (pathValue) {
      // Use path attribute value
      const newSpan = createEl('span', 'etaniAVItem', pathValue);
      valuesElement.appendChild(newSpan);
    } else {
      // Get href from mpath element (including #)
      const mpath = targetAnimation.querySelector('mpath');
      if (mpath) {
        const hrefValue = mpath.getAttribute('href');
        const newSpan = createEl('span', 'etaniAVItem', hrefValue);
        valuesElement.appendChild(newSpan);
      }
    }
    
  } else if (tagname === 'set') {
    // targetAnimation is a single element
    const toValue = targetAnimation.getAttribute('to');
    const newSpan = createEl('span', 'etaniAVItem', toValue);
    valuesElement.appendChild(newSpan);
  }
}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4360
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 170 次
被圈友点赞: 197 次
联系:

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

帖子 ejsoon »

gemini 2.5pro

代码: 全选

/**
 * Populates a UI element with spans representing animation values,
 * based on the type of SMIL animation tag.
 * Assumes a helper function createEl(tagName, className, textContent) exists.
 *
 * @param {string} tagname - The tag name of the animation element (e.g., 'animateTransform', 'animate').
 * @param {Element|Element[]} targetAnimation - The animation element or an array of elements.
 * @param {HTMLElement} valuesElement - The container element to append the value spans to.
 */
function setUIAnimateValues(tagname, targetAnimation, valuesElement) {
    // Clear the container first
    valuesElement.innerHTML = '';

    // --- Case 1: animateTransform ---
    if (tagname === 'animateTransform') {
        // targetAnimation is expected to be an array [translate, scale, rotate]
        if (!Array.isArray(targetAnimation) || targetAnimation.length < 3) {
            console.error('animateTransform expects an array of 3 elements [translate, scale, rotate].');
            return;
        }

        const [translate, scale, rotate] = targetAnimation;

        // Helper to get 'values' attribute and split by ';', with fallback
        const getValues = (el) => (el && el.getAttribute('values')) ? el.getAttribute('values').split(';') : [];

        const translateValues = getValues(translate);
        const scaleValues = getValues(scale);
        const rotateValues = getValues(rotate);

        // Find the number of keyframes (prompt assumes lengths are equal)
        const len = Math.max(translateValues.length, scaleValues.length, rotateValues.length);
        if (len === 0) {
            // No values to display
            return;
        }

        const combinedValues = [];
        // Combine values at each keyframe index
        for (let i = 0; i < len; i++) {
            const t = translateValues[i] || ''; // Use empty string if this animation is shorter
            const s = scaleValues[i] || '';
            const r = rotateValues[i] || '';
            combinedValues.push(`${t};${s};${r}`);
        }

        // Map combined values to representative letters (e.g., "a", "b", "c", "a")
        const valueMap = new Map();
        const letters = [];
        const charSequence = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
        let charIndex = 0; // Index for the *next available* letter

        for (const value of combinedValues) {
            if (!valueMap.has(value)) {
                // This is a new, unique value. Assign a new letter.
                if (charIndex < charSequence.length) {
                    valueMap.set(value, charSequence[charIndex]);
                    charIndex++; // Increment index *only* for new unique values
                } else {
                    // Fallback if we run out of letters (more than 52 unique keyframes)
                    valueMap.set(value, '?');
                }
            }
            // Add the (new or existing) letter to the array
            letters.push(valueMap.get(value));
        }

        // Create and append spans for each letter
        letters.forEach(letter => {
            const newSpan = createEl('span', 'etaniAVItem', letter);
            valuesElement.appendChild(newSpan);
        });

    // --- Case 2: animate ---
    } else if (tagname === 'animate') {
        // targetAnimation is a single Element
        const valuesStr = targetAnimation.getAttribute('values');
        if (!valuesStr) return; // No 'values' attribute

        const values = valuesStr.split(';');

        // Apply the same letter-mapping logic as in Case 1
        // This aligns with the user's example: createEl('span', 'etaniAVItem', '代表字母')
        const valueMap = new Map();
        const letters = [];
        const charSequence = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
        let charIndex = 0;

        for (const value of values) {
            if (!valueMap.has(value)) {
                if (charIndex < charSequence.length) {
                    valueMap.set(value, charSequence[charIndex]);
                    charIndex++;
                } else {
                    valueMap.set(value, '?');
                }
            }
            letters.push(valueMap.get(value));
        }

        // Create and append spans for each letter
        letters.forEach(letter => {
            const newSpan = createEl('span', 'etaniAVItem', letter);
            valuesElement.appendChild(newSpan);
        });

    // --- Case 3: animateMotion ---
    } else if (tagname === 'animateMotion') {
        // targetAnimation is a single Element
        let content = '';
        if (targetAnimation.hasAttribute('path')) {
            // Priority 1: Use the 'path' attribute
            content = targetAnimation.getAttribute('path');
        } else {
            // Priority 2: Find the <mpath> child element
            const mpath = targetAnimation.querySelector('mpath');
            if (mpath) {
                // Check for 'href' (standard) or 'xlink:href' (legacy)
                if (mpath.hasAttribute('href')) {
                    content = mpath.getAttribute('href');
                } else if (mpath.hasAttribute('xlink:href')) {
                    content = mpath.getAttribute('xlink:href'); // Fallback for older SVGs
                }
            }
        }

        // If we found content, create a single span for it
        if (content) {
            const newSpan = createEl('span', 'etaniAVItem', content);
            valuesElement.appendChild(newSpan);
        }

    // --- Case 4: set ---
    } else if (tagname === 'set') {
        // targetAnimation is a single Element
        // Get the 'to' attribute value
        if (targetAnimation.hasAttribute('to')) {
            const toValue = targetAnimation.getAttribute('to');
            const newSpan = createEl('span', 'etaniAVItem', toValue);
            valuesElement.appendChild(newSpan);
        }
    }
}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4360
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 170 次
被圈友点赞: 197 次
联系:

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

帖子 ejsoon »

claude.ai完全不行,比起gemini差遠了。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4360
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 170 次
被圈友点赞: 197 次
联系:

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

帖子 ejsoon »

gemini 2.5 pro:

代码: 全选

/**
 * Populates a UI element with spans representing animation values,
 * based on the type of animation tag.
 * * Assumes the existence of a helper function: 
 * createEl(tagName, className, innerText)
 *
 * @param {string} tagname - The tag name of the animation element 
 * (e.g., 'animateTransform', 'animate', 'animateMotion', 'set').
 * @param {Element|Element[]} targetAnimation - The target animation element(s).
 * @param {Element} valuesElement - The container element to append value spans to.
 */
function setUIAnimateValues(tagname, targetAnimation, valuesElement) {
    // Clear the target container first
    valuesElement.innerHTML = '';

    switch (tagname) {
        case 'animateTransform':
            // targetAnimation is an array [translate, scale, rotate]
            if (!Array.isArray(targetAnimation) || targetAnimation.length < 3) {
                console.error('animateTransform expects an array of 3 elements.');
                return;
            }

            // Get values from all three transform elements
            const translateVals = (targetAnimation[0]?.getAttribute('values') || '').split(';');
            const scaleVals = (targetAnimation[1]?.getAttribute('values') || '').split(';');
            const rotateVals = (targetAnimation[2]?.getAttribute('values') || '').split(';');

            // Assume all arrays have the same length, based on the first one
            const valuesLength = translateVals.length;
            if (valuesLength === 0 || scaleVals.length !== valuesLength || rotateVals.length !== valuesLength) {
                console.warn('animateTransform value arrays have mismatched lengths or are empty.');
                // Continue anyway, but might produce incomplete results
            }

            const combinedValues = [];
            for (let i = 0; i < valuesLength; i++) {
                // Combine corresponding values with ';'
                const combined = `${translateVals[i] || ''};${scaleVals[i] || ''};${rotateVals[i] || ''}`;
                combinedValues.push(combined);
            }

            // Map unique combined values to representative letters (a-z, A-Z)
            const valueMap = new Map();
            const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
            let charIndex = 0;
            const representativeLetters = [];

            for (const value of combinedValues) {
                let letter = valueMap.get(value);
                if (!letter) {
                    // Assign a new letter if this value hasn't been seen
                    if (charIndex < alphabet.length) {
                        letter = alphabet[charIndex];
                        charIndex++;
                    } else {
                        // Fallback if we run out of letters (more than 52 unique steps)
                        letter = `?${charIndex - alphabet.length + 1}`;
                    }
                    valueMap.set(value, letter);
                }
                representativeLetters.push(letter);
            }

            // Create and append spans for each representative letter
            for (const letter of representativeLetters) {
                const newSpan = createEl('span', 'etaniAVItem', letter);
                valuesElement.appendChild(newSpan);
            }
            break;

        case 'animate':
            // targetAnimation is a single Element
            const values = targetAnimation.getAttribute('values');
            if (values) {
                const valueArray = values.split(';');
                // Create a span for each value
                for (const val of valueArray) {
                    if (val.trim() !== '') { // Avoid creating spans for empty values (e.g., from "a;;b")
                        const newSpan = createEl('span', 'etaniAVItem', val);
                        valuesElement.appendChild(newSpan);
                    }
                }
            }
            break;

        case 'animateMotion':
            // targetAnimation is a single Element
            const pathValue = targetAnimation.getAttribute('path');
            let motionValue = '';

            if (pathValue) {
                // Use 'path' attribute if it exists
                motionValue = pathValue;
            } else {
                // Otherwise, find the <mpath> element and use its 'href'
                const mpathElement = targetAnimation.querySelector('mpath');
                if (mpathElement) {
                    motionValue = mpathElement.getAttribute('href') || '';
                }
            }
            
            const motionSpan = createEl('span', 'etaniAVItem', motionValue);
            valuesElement.appendChild(motionSpan);
            break;

        case 'set':
            // targetAnimation is a single Element
            const toValue = targetAnimation.getAttribute('to') || '';
            
            // Create a span for the 'to' attribute value
            const setSpan = createEl('span', 'etaniAVItem', toValue);
            valuesElement.appendChild(setSpan);
            break;

        default:
            // Handle unknown tagname
            console.warn(`Unhandled animation tag: ${tagname}`);
    }
}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4360
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 170 次
被圈友点赞: 197 次
联系:

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 = 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;
}
.etaniAppendSet.active {
  background-color: #008CBA;
  color: white;
  border-color: #008CBA;
}
.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;
}
textarea.etaniHTMLTextarea {
  width: calc(100vw - 72px);
  height: calc(50vh - 24px);
  resize: none;
  border: 1px solid #ccc;
  font-size: 12px;
  box-sizing: border-box;
}
.etaniWindowRow {
  padding-top: 12px;
  text-align: center;
}
.etaniWindowRow button {
  margin: 0 12px;
}
    `;

    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';
    etaniContentHTML.addEventListener('click', handleContentHTMLClick);
    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 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 = 'etanidrop';
            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');
        etaniItemId.addEventListener('click', function() {
            etaniAppendAnimateWindow(itemId, dataType);
        });
        etaniItemLeft.appendChild(etaniItemId);

        // Create etaniItemName span
        let itemName = itemId === 'etanidrop' ? 'board' : itemId;
        const etaniItemName = createEl('span', 'etaniItemName', itemName);
        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 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.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;
}

// Handle the HTML popup window
function handleContentHTMLClick() {
    if (!etani) return;
    const textarea = createEl('textarea', 'etaniHTMLTextarea');
    textarea.value = new XMLSerializer().serializeToString(etani);
    etaniWindow(textarea);
}

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

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

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

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

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

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

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

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

// Open a generic window
function etaniWindow(content, confirm_function = null) {
    // 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 = createEl('div', 'etaniWindow');
    
    // Append custom content to the window
    if (typeof content === 'string') {
        windowDiv.innerHTML = content;
    } else if (content instanceof Element) {
        windowDiv.appendChild(content);
    } else {
        console.error('Invalid content type provided.');
        return;
    }
    
    // Add confirm and cancel buttons if not already in content
    let btnConfirm = windowDiv.querySelector('button.confirm'); // Assume class or id for identification
    let btnCancel = windowDiv.querySelector('button.cancel');
    if (!btnConfirm || !btnCancel) {
        const rowButtons = createEl('div', 'etaniWindowRow');
        btnConfirm = createEl('button', 'confirm', 'Confirm');
        btnCancel = createEl('button', 'cancel', 'Cancel');
        rowButtons.appendChild(btnConfirm);
        rowButtons.appendChild(btnCancel);
        windowDiv.appendChild(rowButtons);
    }
    
    // Append the windowDiv to .etaniinner
    inner.appendChild(windowDiv);
    
    // Add event listeners
    if (btnConfirm) {
        btnConfirm.addEventListener('click', function() {
            if (typeof confirm_function === 'function') {
                confirm_function();
            }
            windowDiv.remove();
        });
    }
    if (btnCancel) {
        btnCancel.addEventListener('click', function() {
            windowDiv.remove();
        });
    }
}

// Open append animate window
function etaniAppendAnimateWindow(id, dataType) {
    const appendAnimateWindow = createEl('div', 'appendAnimateWindow');
    // First row
    const row1 = createEl('div', 'etaniAppendRow');
    const strong = createEl('strong', null, 'id: ' + id);
    row1.appendChild(strong);
    appendAnimateWindow.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);
    appendAnimateWindow.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);
    appendAnimateWindow.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);
    appendAnimateWindow.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);
    appendAnimateWindow.appendChild(row5);
    etaniWindow(appendAnimateWindow);

    // Add click events to all specified elements
    // For a elements with href="javascript:;"
    aTransform.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, "animateTransform", "transform");
        document.querySelector('.etaniWindow').remove();
    });
    aMotion.addEventListener('click', function() {
        // TODO: Implement Motion click functionality
    });
    aOpacity.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, "animate", "opacity", 1);
        document.querySelector('.etaniWindow').remove();
    });
    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() {
        etaniAppendAnimate(id, dataType, "animate", "stroke-width", 2);
        document.querySelector('.etaniWindow').remove();
    });
    // For span.etaniAppendSet
    spanSet.addEventListener('click', function() {
        if (spanSet.classList.contains('active')) {
            this.classList.remove('active');
            aTransform.style.pointerEvents = 'auto';
            aTransform.style.opacity = '1'; // Add disabled CSS state
            aMotion.style.pointerEvents = 'auto';
            aMotion.style.opacity = '1'; // Add disabled CSS state
        } else {
            this.classList.add('active');
            aTransform.style.pointerEvents = 'none';
            aTransform.style.opacity = '0.5'; // Add disabled CSS state
            aMotion.style.pointerEvents = 'none';
            aMotion.style.opacity = '0.5'; // Add disabled CSS state
        }
    });
}

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

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

// get target animate element by etaniAVCtrl button
function getTargetAnimation(valueBtn) {
    // --- Step 1: Get .etaniAnimate parent data ---
    const animateParent = valueBtn.closest('.etaniAnimate');
    if (!animateParent) {
        throw new Error('Could not find parent .etaniAnimate');
    }

    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;

    // Get the index of this element among its siblings
    const parentChildren = Array.from(animateParent.parentNode.children);
    const animateIndex = parentChildren.indexOf(animateParent);

    if (animateIndex === -1) {
         throw new Error('Could not determine animateIndex');
    }

    // --- Step 2: Get .etaniItem parent data ---
    const itemParent = valueBtn.closest('.etaniItem');
    if (!itemParent) {
        throw new Error('Could not find parent .etaniItem');
    }

    const dataId = itemParent.dataset.id;
    const dataType = itemParent.dataset.type;
    const hrefId = `#${dataId}`; // Prepare for attribute selector

    // --- Step 3: Find target animation elements ---
    const animationTags = 'animateTransform, animate, animateMotion, set';
    let animations = [];

    if (dataType === 'tile') {
        const useElement = etani.querySelector(`.etdrop > use[href="${hrefId}"]`);
        if (useElement) {
            // Find all animation elements inside the <use> tag
            animations = Array.from(useElement.querySelectorAll(animationTags));
        }
    } else if (dataType === 'board') {
        // Find all direct children of #etani matching the tags and href
        const selector = `:scope > animateTransform[href="${hrefId}"], 
                        :scope > animate[href="${hrefId}"], 
                        :scope > animateMotion[href="${hrefId}"], 
                        :scope > set[href="${hrefId}"]`;
        animations = Array.from(etani.querySelectorAll(selector));
    }

    // Group consecutive animateTransforms (3 at a time)
    const groupedAnimations = [];
    for (let i = 0; i < animations.length; ) {
        const currentAnim = animations[i];
        if (currentAnim.tagName === 'animateTransform') {
            // Assume 3 consecutive animateTransforms
            if (i + 2 < animations.length &&
                animations[i+1].tagName === 'animateTransform' &&
                animations[i+2].tagName === 'animateTransform') 
            {
                groupedAnimations.push([animations[i], animations[i+1], animations[i+2]]);
                i += 3;
            } else {
                // Handle incomplete groups: log warning and skip this element
                console.warn('Incomplete or non-consecutive animateTransform group found.', currentAnim);
                i++; // Skip this one to avoid infinite loop
            }
        } else {
            // Add other animation types as single items
            groupedAnimations.push(currentAnim);
            i++;
        }
    }

    // Get the specific target animation (group) using the index
    const targetAnimation = groupedAnimations[animateIndex];
    if (!targetAnimation) {
        throw new Error(`No animation element found at index ${animateIndex}`);
    }
    return targetAnimation;
}

/**
 * Appends a value to an element's 'values' attribute, separated by semicolons.
 * @param {Element} element - The animation element.
 * @param {string|number} value - The value to append.
 */
function appendValueToAttribute(element, value) {
    // Do not append if element or value is invalid
    if (!element || value === null || typeof value === 'undefined') return;
    
    let currentValues = element.getAttribute('values');
    const stringValue = String(value); // Ensure value is a string

    if (currentValues && currentValues.trim() !== '') {
        // Add with a semicolon if values already exist
        element.setAttribute('values', currentValues + ';' + stringValue);
    } else {
        // Set as the first value
        element.setAttribute('values', stringValue);
    }
}

/**
 * Extracts a specific transform function's value (e.g., "10 20" from "translate(10 20)")
 * @param {string} type - The transform type (e.g., 'translate', 'scale').
 * @param {string} transformString - The full transform attribute string.
 * @returns {string|null} The extracted value or null if not found.
 */
function getTransformValue(type, transformString) {
    if (!transformString) return null;
    // Regex to find the type and capture the content inside the parentheses
    const regex = new RegExp(`${type}\\(([^)]+)\\)`);
    const match = transformString.match(regex);
    return match ? match[1] : null; // Return the captured group (the values)
}

// Add animate value
function etaniAVAddClick(etaniAVAdd) {
    // --- Step 1-3: get targetAnimation element(s) ---
    let targetAnimation = getTargetAnimation(etaniAVAdd);
    // --- Step 4: Insert values into target element ---
    let defaultValue; // This will be used for both Step 4 and 5
    const animateParent = etaniAVAdd.closest('.etaniAnimate');
    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;
    const itemParent = etaniAVAdd.closest('.etaniItem');
    const dataId = itemParent.dataset.id;
    const hrefId = `#${dataId}`; // Prepare for attribute selector

    if (tagName === 'animateTransform') {
        // Check if targetAnimation is valid (an array of 3)
        if (Array.isArray(targetAnimation) && targetAnimation.length === 3) {
            const mainUseElement = etmain.querySelector(`.etdrop > use[href="${hrefId}"]`);
            
            if (mainUseElement) {
                const transformString = mainUseElement.getAttribute('transform');
                
                // Extract values
                const translateVal = getTransformValue('translate', transformString);
                const scaleVal = getTransformValue('scale', transformString);
                const rotateVal = getTransformValue('rotate', transformString);

                // Add values to the 'values' attribute of each corresponding animation
                // Assuming order: [0] = translate, [1] = scale, [2] = rotate
                appendValueToAttribute(targetAnimation[0], translateVal);
                appendValueToAttribute(targetAnimation[1], scaleVal);
                appendValueToAttribute(targetAnimation[2], rotateVal);
            }
        } else {
             console.warn('Expected targetAnimation to be a group of 3, but it was not.', targetAnimation);
        }
    } else if (tagName === 'animate') {
        // Check if targetAnimation is a single element
        if (targetAnimation && !Array.isArray(targetAnimation)) {
            // Determine default value based on animateType
            if (animateType === 'opacity') {
                defaultValue = 1;
            } else if (animateType === 'stroke-width') {
                defaultValue = 2;
            } else {
                defaultValue = 0;
            }
            
            // Add to values attribute
            appendValueToAttribute(targetAnimation, defaultValue);
        } else {
            console.warn('Expected targetAnimation to be a single element, but it was not.', targetAnimation);
        }
    }

    // --- Step 5: Add <span> to .etaniAV ---
    const avParent = etaniAVAdd.closest('.etaniAnimateValue');
    const etaniAV = avParent.querySelector('.etaniAV');
    setUIAnimateValues(tagName, targetAnimation, etaniAV);
    updateEtaniResult();
}

// 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');
    elAnimateValue.dataset.mode = 'edit';
    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);
    etaniAVAdd.addEventListener('click', () => {
        const active = elAVCtrl.querySelector('.active');
        if (active) active.classList.remove('active');
        elAnimateValue.dataset.mode = 'edit';
        etaniAVAddClick(etaniAVAdd);
    });

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

    // convert animateName
    let cName;
    if (animatetype === 'stroke-width') {
        cName = 'width';
    } else {
        cName = animatetype;
    }

    // 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 = cName;
            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 = cName;
            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);
}

/**
 * Populates a UI element with spans representing animation values,
 * based on the type of animation tag.
 *
 * @param {string} tagname - The tag name of the animation element 
 * (e.g., 'animateTransform', 'animate', 'animateMotion', 'set').
 * @param {Element|Element[]} targetAnimation - The target animation element(s).
 * @param {Element} valuesElement - The container element to append value spans to.
 */
function setUIAnimateValues(tagname, targetAnimation, valuesElement) {
    // Clear the target container first
    valuesElement.innerHTML = '';

    switch (tagname) {
        case 'animateTransform':
            // targetAnimation is an array [translate, scale, rotate]
            if (!Array.isArray(targetAnimation) || targetAnimation.length < 3) {
                console.error('animateTransform expects an array of 3 elements.');
                return;
            }

            // Get values from all three transform elements
            const translateVals = (targetAnimation[0]?.getAttribute('values') || '').split(';');
            const scaleVals = (targetAnimation[1]?.getAttribute('values') || '').split(';');
            const rotateVals = (targetAnimation[2]?.getAttribute('values') || '').split(';');

            // Assume all arrays have the same length, based on the first one
            const valuesLength = translateVals.length;
            if (valuesLength === 0 || scaleVals.length !== valuesLength || rotateVals.length !== valuesLength) {
                console.warn('animateTransform value arrays have mismatched lengths or are empty.');
                // Continue anyway, but might produce incomplete results
            }

            const combinedValues = [];
            for (let i = 0; i < valuesLength; i++) {
                // Combine corresponding values with ';'
                const combined = `${translateVals[i] || ''};${scaleVals[i] || ''};${rotateVals[i] || ''}`;
                combinedValues.push(combined);
            }

            // Map unique combined values to representative letters (a-z, A-Z)
            const valueMap = new Map();
            const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
            let charIndex = 0;
            const representativeLetters = [];

            for (const value of combinedValues) {
                let letter = valueMap.get(value);
                if (!letter) {
                    // Assign a new letter if this value hasn't been seen
                    if (charIndex < alphabet.length) {
                        letter = alphabet[charIndex];
                        charIndex++;
                    } else {
                        // Fallback if we run out of letters (more than 52 unique steps)
                        letter = `?${charIndex - alphabet.length + 1}`;
                    }
                    valueMap.set(value, letter);
                }
                representativeLetters.push(letter);
            }

            // Create and append spans for each representative letter
            for (const letter of representativeLetters) {
                const newSpan = createEl('span', 'etaniAVItem', letter);
                valuesElement.appendChild(newSpan);
            }
            break;

        case 'animate':
            // targetAnimation is a single Element
            const values = targetAnimation.getAttribute('values');
            if (values) {
                const valueArray = values.split(';');
                // Create a span for each value
                for (const val of valueArray) {
                    if (val.trim() !== '') { // Avoid creating spans for empty values (e.g., from "a;;b")
                        const newSpan = createEl('span', 'etaniAVItem', val);
                        valuesElement.appendChild(newSpan);
                    }
                }
            }
            break;

        case 'animateMotion':
            // targetAnimation is a single Element
            const pathValue = targetAnimation.getAttribute('path');
            let motionValue = '';

            if (pathValue) {
                // Use 'path' attribute if it exists
                motionValue = pathValue;
            } else {
                // Otherwise, find the <mpath> element and use its 'href'
                const mpathElement = targetAnimation.querySelector('mpath');
                if (mpathElement) {
                    motionValue = mpathElement.getAttribute('href') || '';
                }
            }
            
            const motionSpan = createEl('span', 'etaniAVItem', motionValue);
            valuesElement.appendChild(motionSpan);
            break;

        case 'set':
            // targetAnimation is a single Element
            const toValue = targetAnimation.getAttribute('to') || '';
            
            // Create a span for the 'to' attribute value
            const setSpan = createEl('span', 'etaniAVItem', toValue);
            valuesElement.appendChild(setSpan);
            break;

        default:
            // Handle unknown tagname
            console.warn(`Unhandled animation tag: ${tagname}`);
    }
}

// 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);
});
接下來的事項:
  1. 加值之後的加dur、改dur
  2. 改值
  3. 刪除、複製、移動
  4. 加所有動畫到Right
  5. AllAppend
  6. Update
  7. Center
  8. Value function
  9. Undo, Redo
  10. ReadFile
  11. repeat - freeze
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4360
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 170 次
被圈友点赞: 197 次
联系:

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

帖子 ejsoon »

於是我自己加了一個function,還用了不少時間。
當前代碼:

代码: 全选

// 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 = 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;
}
.etaniAppendSet.active {
  background-color: #008CBA;
  color: white;
  border-color: #008CBA;
}
.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;
}
textarea.etaniHTMLTextarea {
  width: calc(100vw - 72px);
  height: calc(50vh - 24px);
  resize: none;
  border: 1px solid #ccc;
  font-size: 12px;
  box-sizing: border-box;
}
.etaniWindowRow {
  padding-top: 12px;
  text-align: center;
}
.etaniWindowRow button {
  margin: 0 12px;
}
.editDurInput {
  width: 36px;
}
.etaniEditDur {
  text-align: center;
}
    `;

    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';
    etaniContentHTML.addEventListener('click', handleContentHTMLClick);
    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 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 = 'etanidrop';
            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');
        etaniItemId.addEventListener('click', function() {
            etaniAppendAnimateWindow(itemId, dataType);
        });
        etaniItemLeft.appendChild(etaniItemId);

        // Create etaniItemName span
        const itemName = itemId === 'etanidrop' ? 'board' : itemId;
        const etaniItemName = createEl('span', 'etaniItemName', itemName);
        etaniItemId.appendChild(etaniItemName);

        // Create etaniItemPlus img
        const etaniItemPlus = createEl('span', 'etaniItemPlus');
        etaniItemPlus.innerHTML = '<svg width="12" height="12" fill="none" stroke-width="1" stroke="darkgreen"><line x1="6" y1="0" x2="6" y2="12"></line><line x1="0" y1="6" x2="12" y2="6"></line></svg>';
        etaniItemId.appendChild(etaniItemPlus);

        // Create etaniItemRight div
        const etaniItemRight = createEl('div', '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.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;
}

// Handle the HTML popup window
function handleContentHTMLClick() {
    if (!etani) return;
    const textarea = createEl('textarea', 'etaniHTMLTextarea');
    textarea.value = new XMLSerializer().serializeToString(etani);
    etaniWindow(textarea);
}

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

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

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

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

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

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

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

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

// Open a generic window
function etaniWindow(content, confirm_function = null) {
    // 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 = createEl('div', 'etaniWindow');
    
    // Append custom content to the window
    if (typeof content === 'string') {
        windowDiv.innerHTML = content;
    } else if (content instanceof Element) {
        windowDiv.appendChild(content);
    } else {
        console.error('Invalid content type provided.');
        return;
    }
    
    // Add confirm and cancel buttons if not already in content
    let btnConfirm = windowDiv.querySelector('button.confirm'); // Assume class or id for identification
    let btnCancel = windowDiv.querySelector('button.cancel');
    if (!btnConfirm || !btnCancel) {
        const rowButtons = createEl('div', 'etaniWindowRow');
        btnConfirm = createEl('button', 'confirm', 'Confirm');
        btnCancel = createEl('button', 'cancel', 'Cancel');
        rowButtons.appendChild(btnConfirm);
        rowButtons.appendChild(btnCancel);
        windowDiv.appendChild(rowButtons);
    }
    
    // Append the windowDiv to .etaniinner
    inner.appendChild(windowDiv);
    
    // Add event listeners
    if (btnConfirm) {
        btnConfirm.addEventListener('click', function() {
            if (typeof confirm_function === 'function') {
                confirm_function();
            }
            windowDiv.remove();
        });
    }
    if (btnCancel) {
        btnCancel.addEventListener('click', function() {
            windowDiv.remove();
        });
    }
}

// Open append animate window
function etaniAppendAnimateWindow(id, dataType) {
    const appendAnimateWindow = createEl('div', 'appendAnimateWindow');
    // First row
    const row1 = createEl('div', 'etaniAppendRow');
    const strong = createEl('strong', null, 'id: ' + id);
    row1.appendChild(strong);
    appendAnimateWindow.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);
    appendAnimateWindow.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);
    appendAnimateWindow.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);
    appendAnimateWindow.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);
    appendAnimateWindow.appendChild(row5);
    etaniWindow(appendAnimateWindow);

    // Add click events to all specified elements
    // For a elements with href="javascript:;"
    aTransform.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, "animateTransform", "transform");
        document.querySelector('.etaniWindow').remove();
    });
    aMotion.addEventListener('click', function() {
        // TODO: Implement Motion click functionality
    });
    aOpacity.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, "animate", "opacity", 1);
        document.querySelector('.etaniWindow').remove();
    });
    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() {
        etaniAppendAnimate(id, dataType, "animate", "stroke-width", 2);
        document.querySelector('.etaniWindow').remove();
    });
    // For span.etaniAppendSet
    spanSet.addEventListener('click', function() {
        if (spanSet.classList.contains('active')) {
            this.classList.remove('active');
            aTransform.style.pointerEvents = 'auto';
            aTransform.style.opacity = '1'; // Add disabled CSS state
            aMotion.style.pointerEvents = 'auto';
            aMotion.style.opacity = '1'; // Add disabled CSS state
        } else {
            this.classList.add('active');
            aTransform.style.pointerEvents = 'none';
            aTransform.style.opacity = '0.5'; // Add disabled CSS state
            aMotion.style.pointerEvents = 'none';
            aMotion.style.opacity = '0.5'; // Add disabled CSS state
        }
    });
}

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

// 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,
                dur: "1s",
                type: "translate",
                values: transformValues.translate
            });
            targetSVGParent.appendChild(elTranslate);

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

            // Create and append <animateTransform> for rotate
            const elRotate = createSVGElement('animateTransform', {
                ...baseAttrs,
                dur: "1s",
                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;
    }
}

// get target animate element by etaniAVCtrl button
function getTargetAnimation(valueBtn) {
    // --- Step 1: Get .etaniAnimate parent data ---
    const animateParent = valueBtn.closest('.etaniAnimate');
    if (!animateParent) {
        throw new Error('Could not find parent .etaniAnimate');
    }

    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;

    // Get the index of this element among its siblings
    const parentChildren = Array.from(animateParent.parentNode.children);
    const animateIndex = parentChildren.indexOf(animateParent);

    if (animateIndex === -1) {
         throw new Error('Could not determine animateIndex');
    }

    // --- Step 2: Get .etaniItem parent data ---
    const itemParent = valueBtn.closest('.etaniItem');
    if (!itemParent) {
        throw new Error('Could not find parent .etaniItem');
    }

    const dataId = itemParent.dataset.id;
    const dataType = itemParent.dataset.type;
    const hrefId = `#${dataId}`; // Prepare for attribute selector

    // --- Step 3: Find target animation elements ---
    const animationTags = 'animateTransform, animate, animateMotion, set';
    let animations = [];

    if (dataType === 'tile') {
        const useElement = etani.querySelector(`.etdrop > use[href="${hrefId}"]`);
        if (useElement) {
            // Find all animation elements inside the <use> tag
            animations = Array.from(useElement.querySelectorAll(animationTags));
        }
    } else if (dataType === 'board') {
        // Find all direct children of #etani matching the tags and href
        const selector = `:scope > animateTransform[href="${hrefId}"], 
                        :scope > animate[href="${hrefId}"], 
                        :scope > animateMotion[href="${hrefId}"], 
                        :scope > set[href="${hrefId}"]`;
        animations = Array.from(etani.querySelectorAll(selector));
    }

    // Group consecutive animateTransforms (3 at a time)
    const groupedAnimations = [];
    for (let i = 0; i < animations.length; ) {
        const currentAnim = animations[i];
        if (currentAnim.tagName === 'animateTransform') {
            // Assume 3 consecutive animateTransforms
            if (i + 2 < animations.length &&
                animations[i+1].tagName === 'animateTransform' &&
                animations[i+2].tagName === 'animateTransform') 
            {
                groupedAnimations.push([animations[i], animations[i+1], animations[i+2]]);
                i += 3;
            } else {
                // Handle incomplete groups: log warning and skip this element
                console.warn('Incomplete or non-consecutive animateTransform group found.', currentAnim);
                i++; // Skip this one to avoid infinite loop
            }
        } else {
            // Add other animation types as single items
            groupedAnimations.push(currentAnim);
            i++;
        }
    }

    // Get the specific target animation (group) using the index
    const targetAnimation = groupedAnimations[animateIndex];
    if (!targetAnimation) {
        throw new Error(`No animation element found at index ${animateIndex}`);
    }
    return targetAnimation;
}

/**
 * Appends a value to an element's 'values' attribute, separated by semicolons.
 * @param {Element} element - The animation element.
 * @param {string|number} value - The value to append.
 */
function appendValueToAttribute(element, value) {
    // Do not append if element or value is invalid
    if (!element || value === null || typeof value === 'undefined') return;
    
    let currentValues = element.getAttribute('values');
    const stringValue = String(value); // Ensure value is a string

    if (currentValues && currentValues.trim() !== '') {
        // Add with a semicolon if values already exist
        element.setAttribute('values', currentValues + ';' + stringValue);
    } else {
        // Set as the first value
        element.setAttribute('values', stringValue);
    }
}

/**
 * Extracts a specific transform function's value (e.g., "10 20" from "translate(10 20)")
 * @param {string} type - The transform type (e.g., 'translate', 'scale').
 * @param {string} transformString - The full transform attribute string.
 * @returns {string|null} The extracted value or null if not found.
 */
function getTransformValue(type, transformString) {
    if (!transformString) return null;
    // Regex to find the type and capture the content inside the parentheses
    const regex = new RegExp(`${type}\\(([^)]+)\\)`);
    const match = transformString.match(regex);
    return match ? match[1] : null; // Return the captured group (the values)
}

// Add animate value
function etaniAVClick(etaniAVAdd) {
    // --- Step 1-3: get targetAnimation element(s) ---
    let targetAnimation = getTargetAnimation(etaniAVAdd);
    // --- Step 4: Insert values into target element ---
    let defaultValue; // This will be used for both Step 4 and 5
    const animateParent = etaniAVAdd.closest('.etaniAnimate');
    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;
    const itemParent = etaniAVAdd.closest('.etaniItem');
    const dataId = itemParent.dataset.id;
    const hrefId = `#${dataId}`; // Prepare for attribute selector

    if (tagName === 'animateTransform') {
        // Check if targetAnimation is valid (an array of 3)
        if (Array.isArray(targetAnimation) && targetAnimation.length === 3) {
            const mainUseElement = etmain.querySelector(`.etdrop > use[href="${hrefId}"]`);
            
            if (mainUseElement) {
                const transformString = mainUseElement.getAttribute('transform');
                
                // Extract values
                const translateVal = getTransformValue('translate', transformString);
                const scaleVal = getTransformValue('scale', transformString);
                const rotateVal = getTransformValue('rotate', transformString);

                // Add values to the 'values' attribute of each corresponding animation
                // Assuming order: [0] = translate, [1] = scale, [2] = rotate
                appendValueToAttribute(targetAnimation[0], translateVal);
                appendValueToAttribute(targetAnimation[1], scaleVal);
                appendValueToAttribute(targetAnimation[2], rotateVal);
            }
        } else {
             console.warn('Expected targetAnimation to be a group of 3, but it was not.', targetAnimation);
        }
    } else if (tagName === 'animate') {
        // Check if targetAnimation is a single element
        if (targetAnimation && !Array.isArray(targetAnimation)) {
            // Determine default value based on animateType
            if (animateType === 'opacity') {
                defaultValue = 1;
            } else if (animateType === 'stroke-width') {
                defaultValue = 2;
            } else {
                defaultValue = 0;
            }
            
            // Add to values attribute
            appendValueToAttribute(targetAnimation, defaultValue);
        } else {
            console.warn('Expected targetAnimation to be a single element, but it was not.', targetAnimation);
        }
    }

    // --- Step 5: Add <span> to .etaniAV ---
    const avParent = etaniAVAdd.closest('.etaniAnimateValue');
    const etaniAV = avParent.querySelector('.etaniAV');
    setUIAnimateValues(tagName, targetAnimation, etaniAV);
    updateEtaniResult();
}

// 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');
    elAnimateDur.addEventListener('click', editAnimateAttribute);

    const elAnimateAttrAdd = createEl('span', 'etaniAnimateAttrAdd', '+');
    
    elAnimateAttr.appendChild(elAnimateName);
    elAnimateAttr.appendChild(elAnimateDur);
    elAnimateAttr.appendChild(elAnimateAttrAdd);

    // 2. Build .etaniAnimateValue
    const elAnimateValue = createEl('div', 'etaniAnimateValue');
    elAnimateValue.dataset.mode = 'edit';
    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);
    etaniAVAdd.addEventListener('click', () => {
        const active = elAVCtrl.querySelector('.active');
        if (active) active.classList.remove('active');
        elAnimateValue.dataset.mode = 'edit';
        etaniAVClick(etaniAVAdd);
    });

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

    // convert animateName
    let cName;
    if (animatetype === 'stroke-width') {
        cName = 'width';
    } else {
        cName = animatetype;
    }

    // 3. Apply logic based on tagname
    switch (tagname) {
        case 'animateTransform':
            elAnimateName.textContent = 'transform';
            elAnimateDur.textContent = 'dur = 1s';
            elAVLabel.textContent = 'values = ';
            elAVItem.textContent = 'a'; // Default placeholder
            elAnimateValue.appendChild(elAVCtrl);
            break;
            
        case 'animate':
            elAnimateName.textContent = cName;
            elAnimateDur.textContent = 'dur = 1s';
            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 = cName;
            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);
}

/**
 * Populates a UI element with spans representing animation values,
 * based on the type of animation tag.
 *
 * @param {string} tagname - The tag name of the animation element 
 * (e.g., 'animateTransform', 'animate', 'animateMotion', 'set').
 * @param {Element|Element[]} targetAnimation - The target animation element(s).
 * @param {Element} valuesElement - The container element to append value spans to.
 */
function setUIAnimateValues(tagname, targetAnimation, valuesElement) {
    // Clear the target container first
    valuesElement.innerHTML = '';

    switch (tagname) {
        case 'animateTransform':
            // targetAnimation is an array [translate, scale, rotate]
            if (!Array.isArray(targetAnimation) || targetAnimation.length < 3) {
                console.error('animateTransform expects an array of 3 elements.');
                return;
            }

            // Get values from all three transform elements
            const translateVals = (targetAnimation[0]?.getAttribute('values') || '').split(';');
            const scaleVals = (targetAnimation[1]?.getAttribute('values') || '').split(';');
            const rotateVals = (targetAnimation[2]?.getAttribute('values') || '').split(';');

            // Assume all arrays have the same length, based on the first one
            const valuesLength = translateVals.length;
            if (valuesLength === 0 || scaleVals.length !== valuesLength || rotateVals.length !== valuesLength) {
                console.warn('animateTransform value arrays have mismatched lengths or are empty.');
                // Continue anyway, but might produce incomplete results
            }

            const combinedValues = [];
            for (let i = 0; i < valuesLength; i++) {
                // Combine corresponding values with ';'
                const combined = `${translateVals[i] || ''};${scaleVals[i] || ''};${rotateVals[i] || ''}`;
                combinedValues.push(combined);
            }

            // Map unique combined values to representative letters (a-z, A-Z)
            const valueMap = new Map();
            const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
            let charIndex = 0;
            const representativeLetters = [];

            for (const value of combinedValues) {
                let letter = valueMap.get(value);
                if (!letter) {
                    // Assign a new letter if this value hasn't been seen
                    if (charIndex < alphabet.length) {
                        letter = alphabet[charIndex];
                        charIndex++;
                    } else {
                        // Fallback if we run out of letters (more than 52 unique steps)
                        letter = `?${charIndex - alphabet.length + 1}`;
                    }
                    valueMap.set(value, letter);
                }
                representativeLetters.push(letter);
            }

            // Create and append spans for each representative letter
            for (const letter of representativeLetters) {
                const newSpan = createEl('span', 'etaniAVItem', letter);
                valuesElement.appendChild(newSpan);
            }
            break;

        case 'animate':
            // targetAnimation is a single Element
            const values = targetAnimation.getAttribute('values');
            if (values) {
                const valueArray = values.split(';');
                // Create a span for each value
                for (const val of valueArray) {
                    if (val.trim() !== '') { // Avoid creating spans for empty values (e.g., from "a;;b")
                        const newSpan = createEl('span', 'etaniAVItem', val);
                        valuesElement.appendChild(newSpan);
                    }
                }
            }
            break;

        case 'animateMotion':
            // targetAnimation is a single Element
            const pathValue = targetAnimation.getAttribute('path');
            let motionValue = '';

            if (pathValue) {
                // Use 'path' attribute if it exists
                motionValue = pathValue;
            } else {
                // Otherwise, find the <mpath> element and use its 'href'
                const mpathElement = targetAnimation.querySelector('mpath');
                if (mpathElement) {
                    motionValue = mpathElement.getAttribute('href') || '';
                }
            }
            
            const motionSpan = createEl('span', 'etaniAVItem', motionValue);
            valuesElement.appendChild(motionSpan);
            break;

        case 'set':
            // targetAnimation is a single Element
            const toValue = targetAnimation.getAttribute('to') || '';
            
            // Create a span for the 'to' attribute value
            const setSpan = createEl('span', 'etaniAVItem', toValue);
            valuesElement.appendChild(setSpan);
            break;

        default:
            // Handle unknown tagname
            console.warn(`Unhandled animation tag: ${tagname}`);
    }
}

// edit Animate Attribute
function editAnimateAttribute() {
	const targetAnimation = getTargetAnimation(this);
	let editDurValue;
	if (Array.isArray(targetAnimation)) {
		editDurValue = targetAnimation[0].getAttribute('dur');
	} else {
		editDurValue = targetAnimation.getAttribute('dur');
	}
	editDurValue = editDurValue.replace('s', '');

	const editDur = createEl('div', 'etaniEditDur');
	const editDurLabel = createEl('label', 'editDurLabel', 'dur = ');
	const editDurInput = createEl('input', 'editDurInput');
	editDurInput.type = 'text';
	editDurInput.value = editDurValue;
	const editDurSpan = createEl('span', 'editDurSpan', ' s');
	editDurLabel.appendChild(editDurInput);
	editDurLabel.appendChild(editDurSpan);
	editDur.appendChild(editDurLabel);
	etaniWindow(editDur, () => {
		let setDurValue = document.querySelector('.editDurInput').value;
		setDurValue = setDurValue + 's';
		if (Array.isArray(targetAnimation)) {
			targetAnimation[0].setAttribute('dur', setDurValue);
			targetAnimation[1].setAttribute('dur', setDurValue);
			targetAnimation[2].setAttribute('dur', setDurValue);
		} else {
			targetAnimation.setAttribute('dur', setDurValue);
		}
		this.textContent = 'dur = ' + setDurValue;
		updateEtaniResult();
	});
}

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

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

帖子 ejsoon »

接下來的事項:
  1. 加值之後的加dur、改dur
  2. 改值
  3. 刪除、複製、移動
  4. 加屬性,改屬性
  5. 加所有動畫到Right
  6. AllAppend
  7. Update
  8. Center
  9. Value function
  10. Undo, Redo
  11. ReadFile
  12. repeat - freeze
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4360
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 170 次
被圈友点赞: 197 次
联系:

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 = 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;
}
.etaniAppendSet.active {
  background-color: #008CBA;
  color: white;
  border-color: #008CBA;
}
.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;
}
textarea.etaniHTMLTextarea {
  width: calc(100vw - 72px);
  height: calc(50vh - 24px);
  resize: none;
  border: 1px solid #ccc;
  font-size: 12px;
  box-sizing: border-box;
}
.etaniWindowRow {
  padding-top: 12px;
  text-align: center;
}
.etaniWindowRow button {
  margin: 0 12px;
}
.editDurInput {
  width: 36px;
}
.etaniEditDur {
  text-align: center;
}
    `;

    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';
    etaniContentHTML.addEventListener('click', handleContentHTMLClick);
    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 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 = 'etanidrop';
            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');
        etaniItemId.addEventListener('click', function() {
            etaniAppendAnimateWindow(itemId, dataType);
        });
        etaniItemLeft.appendChild(etaniItemId);

        // Create etaniItemName span
        const itemName = itemId === 'etanidrop' ? 'board' : itemId;
        const etaniItemName = createEl('span', 'etaniItemName', itemName);
        etaniItemId.appendChild(etaniItemName);

        // Create etaniItemPlus img
        const etaniItemPlus = createEl('span', 'etaniItemPlus');
        etaniItemPlus.innerHTML = '<svg width="12" height="12" fill="none" stroke-width="1" stroke="darkgreen"><line x1="6" y1="0" x2="6" y2="12"></line><line x1="0" y1="6" x2="12" y2="6"></line></svg>';
        etaniItemId.appendChild(etaniItemPlus);

        // Create etaniItemRight div
        const etaniItemRight = createEl('div', '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.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;
}

// Handle the HTML popup window
function handleContentHTMLClick() {
    if (!etani) return;
    const textarea = createEl('textarea', 'etaniHTMLTextarea');
    textarea.value = new XMLSerializer().serializeToString(etani);
    etaniWindow(textarea);
}

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

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

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

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

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

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

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

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

// Open a generic window
function etaniWindow(content, confirm_function = null) {
    // 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 = createEl('div', 'etaniWindow');
    
    // Append custom content to the window
    if (typeof content === 'string') {
        windowDiv.innerHTML = content;
    } else if (content instanceof Element) {
        windowDiv.appendChild(content);
    } else {
        console.error('Invalid content type provided.');
        return;
    }
    
    // Add confirm and cancel buttons if not already in content
    let btnConfirm = windowDiv.querySelector('button.confirm'); // Assume class or id for identification
    let btnCancel = windowDiv.querySelector('button.cancel');
    if (!btnConfirm || !btnCancel) {
        const rowButtons = createEl('div', 'etaniWindowRow');
        btnConfirm = createEl('button', 'confirm', 'Confirm');
        btnCancel = createEl('button', 'cancel', 'Cancel');
        rowButtons.appendChild(btnConfirm);
        rowButtons.appendChild(btnCancel);
        windowDiv.appendChild(rowButtons);
    }
    
    // Append the windowDiv to .etaniinner
    inner.appendChild(windowDiv);
    
    // Add event listeners
    if (btnConfirm) {
        btnConfirm.addEventListener('click', function() {
            if (typeof confirm_function === 'function') {
                confirm_function();
            }
            windowDiv.remove();
        });
    }
    if (btnCancel) {
        btnCancel.addEventListener('click', function() {
            windowDiv.remove();
        });
    }
}

// Open append animate window
function etaniAppendAnimateWindow(id, dataType) {
    const appendAnimateWindow = createEl('div', 'appendAnimateWindow');
    // First row
    const row1 = createEl('div', 'etaniAppendRow');
    const strong = createEl('strong', null, 'id: ' + id);
    row1.appendChild(strong);
    appendAnimateWindow.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);
    appendAnimateWindow.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);
    appendAnimateWindow.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);
    appendAnimateWindow.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);
    appendAnimateWindow.appendChild(row5);
    etaniWindow(appendAnimateWindow);

    // 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() {
        etaniAppendAnimate(id, dataType, 'animateMotion', 'motion', 'M 0,0 H 120 V 120 Z');
    });
    aOpacity.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animate', 'opacity', 1);
    });
    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() {
        etaniAppendAnimate(id, dataType, "animate", "stroke-width", 2);
        document.querySelector('.etaniWindow').remove();
    });
    // For span.etaniAppendSet
    spanSet.addEventListener('click', function() {
        if (spanSet.classList.contains('active')) {
            this.classList.remove('active');
            aTransform.style.pointerEvents = 'auto';
            aTransform.style.opacity = '1'; // Add disabled CSS state
            aMotion.style.pointerEvents = 'auto';
            aMotion.style.opacity = '1'; // Add disabled CSS state
        } else {
            this.classList.add('active');
            aTransform.style.pointerEvents = 'none';
            aTransform.style.opacity = '0.5'; // Add disabled CSS state
            aMotion.style.pointerEvents = 'none';
            aMotion.style.opacity = '0.5'; // Add disabled CSS state
        }
    });
}

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

// 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,
                dur: "1s",
                type: "translate",
                values: transformValues.translate
            });
            targetSVGParent.appendChild(elTranslate);

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

            // Create and append <animateTransform> for rotate
            const elRotate = createSVGElement('animateTransform', {
                ...baseAttrs,
                dur: "1s",
                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;
    }
}

// get target animate element by etaniAVCtrl button
function getTargetAnimation(valueBtn) {
    // --- Step 1: Get .etaniAnimate parent data ---
    const animateParent = valueBtn.closest('.etaniAnimate');
    if (!animateParent) {
        throw new Error('Could not find parent .etaniAnimate');
    }

    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;

    // Get the index of this element among its siblings
    const parentChildren = Array.from(animateParent.parentNode.children);
    const animateIndex = parentChildren.indexOf(animateParent);

    if (animateIndex === -1) {
         throw new Error('Could not determine animateIndex');
    }

    // --- Step 2: Get .etaniItem parent data ---
    const itemParent = valueBtn.closest('.etaniItem');
    if (!itemParent) {
        throw new Error('Could not find parent .etaniItem');
    }

    const dataId = itemParent.dataset.id;
    const dataType = itemParent.dataset.type;
    const hrefId = `#${dataId}`; // Prepare for attribute selector

    // --- Step 3: Find target animation elements ---
    const animationTags = 'animateTransform, animate, animateMotion, set';
    let animations = [];

    if (dataType === 'tile') {
        const useElement = etani.querySelector(`.etdrop > use[href="${hrefId}"]`);
        if (useElement) {
            // Find all animation elements inside the <use> tag
            animations = Array.from(useElement.querySelectorAll(animationTags));
        }
    } else if (dataType === 'board') {
        // Find all direct children of #etani matching the tags and href
        const selector = `:scope > animateTransform[href="${hrefId}"], 
                        :scope > animate[href="${hrefId}"], 
                        :scope > animateMotion[href="${hrefId}"], 
                        :scope > set[href="${hrefId}"]`;
        animations = Array.from(etani.querySelectorAll(selector));
    }

    // Group consecutive animateTransforms (3 at a time)
    const groupedAnimations = [];
    for (let i = 0; i < animations.length; ) {
        const currentAnim = animations[i];
        if (currentAnim.tagName === 'animateTransform') {
            // Assume 3 consecutive animateTransforms
            if (i + 2 < animations.length &&
                animations[i+1].tagName === 'animateTransform' &&
                animations[i+2].tagName === 'animateTransform') 
            {
                groupedAnimations.push([animations[i], animations[i+1], animations[i+2]]);
                i += 3;
            } else {
                // Handle incomplete groups: log warning and skip this element
                console.warn('Incomplete or non-consecutive animateTransform group found.', currentAnim);
                i++; // Skip this one to avoid infinite loop
            }
        } else {
            // Add other animation types as single items
            groupedAnimations.push(currentAnim);
            i++;
        }
    }

    // Get the specific target animation (group) using the index
    const targetAnimation = groupedAnimations[animateIndex];
    if (!targetAnimation) {
        throw new Error(`No animation element found at index ${animateIndex}`);
    }
    return targetAnimation;
}

/**
 * Appends a value to an element's 'values' attribute, separated by semicolons.
 * @param {Element} element - The animation element.
 * @param {string|number} value - The value to append.
 */
function appendValueToAttribute(element, value) {
    // Do not append if element or value is invalid
    if (!element || value === null || typeof value === 'undefined') return;
    
    let currentValues = element.getAttribute('values');
    const stringValue = String(value); // Ensure value is a string

    if (currentValues && currentValues.trim() !== '') {
        // Add with a semicolon if values already exist
        element.setAttribute('values', currentValues + ';' + stringValue);
    } else {
        // Set as the first value
        element.setAttribute('values', stringValue);
    }
}

/**
 * Extracts a specific transform function's value (e.g., "10 20" from "translate(10 20)")
 * @param {string} type - The transform type (e.g., 'translate', 'scale').
 * @param {string} transformString - The full transform attribute string.
 * @returns {string|null} The extracted value or null if not found.
 */
function getTransformValue(type, transformString) {
    if (!transformString) return null;
    // Regex to find the type and capture the content inside the parentheses
    const regex = new RegExp(`${type}\\(([^)]+)\\)`);
    const match = transformString.match(regex);
    return match ? match[1] : null; // Return the captured group (the values)
}

// Add animate value
function etaniAVClick(etaniAVAdd) {
    // --- Step 1-3: get targetAnimation element(s) ---
    let targetAnimation = getTargetAnimation(etaniAVAdd);
    // --- Step 4: Insert values into target element ---
    let defaultValue; // This will be used for both Step 4 and 5
    const animateParent = etaniAVAdd.closest('.etaniAnimate');
    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;
    const itemParent = etaniAVAdd.closest('.etaniItem');
    const dataId = itemParent.dataset.id;
    const hrefId = `#${dataId}`; // Prepare for attribute selector

    if (tagName === 'animateTransform') {
        // Check if targetAnimation is valid (an array of 3)
        if (Array.isArray(targetAnimation) && targetAnimation.length === 3) {
            const mainUseElement = etmain.querySelector(`.etdrop > use[href="${hrefId}"]`);
            
            if (mainUseElement) {
                const transformString = mainUseElement.getAttribute('transform');
                
                // Extract values
                const translateVal = getTransformValue('translate', transformString);
                const scaleVal = getTransformValue('scale', transformString);
                const rotateVal = getTransformValue('rotate', transformString);

                // Add values to the 'values' attribute of each corresponding animation
                // Assuming order: [0] = translate, [1] = scale, [2] = rotate
                appendValueToAttribute(targetAnimation[0], translateVal);
                appendValueToAttribute(targetAnimation[1], scaleVal);
                appendValueToAttribute(targetAnimation[2], rotateVal);
            }
        } else {
             console.warn('Expected targetAnimation to be a group of 3, but it was not.', targetAnimation);
        }
    } else if (tagName === 'animate') {
        // Check if targetAnimation is a single element
        if (targetAnimation && !Array.isArray(targetAnimation)) {
            // Determine default value based on animateType
            if (animateType === 'opacity') {
                defaultValue = 1;
            } else if (animateType === 'stroke-width') {
                defaultValue = 2;
            } else {
                defaultValue = 0;
            }
            
            // Add to values attribute
            appendValueToAttribute(targetAnimation, defaultValue);
        } else {
            console.warn('Expected targetAnimation to be a single element, but it was not.', targetAnimation);
        }
    }

    // --- Step 5: Add <span> to .etaniAV ---
    const avParent = etaniAVAdd.closest('.etaniAnimateValue');
    const etaniAV = avParent.querySelector('.etaniAV');
    setUIAnimateValues(tagName, targetAnimation, etaniAV);
    updateEtaniResult();
}

// 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');
    elAnimateDur.addEventListener('click', editAnimateAttribute);

    const elAnimateAttrAdd = createEl('span', 'etaniAnimateAttrAdd', '+');
    
    elAnimateAttr.appendChild(elAnimateName);
    elAnimateAttr.appendChild(elAnimateDur);
    elAnimateAttr.appendChild(elAnimateAttrAdd);

    // 2. Build .etaniAnimateValue
    const elAnimateValue = createEl('div', 'etaniAnimateValue');
    elAnimateValue.dataset.mode = 'edit';
    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);
    etaniAVAdd.addEventListener('click', () => {
        const active = elAVCtrl.querySelector('.active');
        if (active) active.classList.remove('active');
        elAnimateValue.dataset.mode = 'edit';
        etaniAVClick(etaniAVAdd);
    });

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

    // convert animateName
    let cName;
    if (animatetype === 'stroke-width') {
        cName = 'width';
    } else {
        cName = animatetype;
    }

    // 3. Apply logic based on tagname
    switch (tagname) {
        case 'animateTransform':
            elAnimateName.textContent = 'transform';
            elAnimateDur.textContent = 'dur = 1s';
            elAVLabel.textContent = 'values = ';
            elAVItem.textContent = 'a'; // Default placeholder
            elAnimateValue.appendChild(elAVCtrl);
            break;
            
        case 'animate':
            elAnimateName.textContent = cName;
            elAnimateDur.textContent = 'dur = 1s';
            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 = cName;
            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) {
    appendAnimateToResult(id, elementtype, tagname, animatetype, defaultvalue);
    appendAnimateToItemRight(id, elementtype, tagname, animatetype, defaultvalue);
    updateEtaniResult();
    if (document.querySelector('.etaniWindow')) {
        document.querySelector('.etaniWindow').remove();
    }
}

/**
 * Populates a UI element with spans representing animation values,
 * based on the type of animation tag.
 *
 * @param {string} tagname - The tag name of the animation element 
 * (e.g., 'animateTransform', 'animate', 'animateMotion', 'set').
 * @param {Element|Element[]} targetAnimation - The target animation element(s).
 * @param {Element} valuesElement - The container element to append value spans to.
 */
function setUIAnimateValues(tagname, targetAnimation, valuesElement) {
    // Clear the target container first
    valuesElement.innerHTML = '';

    switch (tagname) {
        case 'animateTransform':
            // targetAnimation is an array [translate, scale, rotate]
            if (!Array.isArray(targetAnimation) || targetAnimation.length < 3) {
                console.error('animateTransform expects an array of 3 elements.');
                return;
            }

            // Get values from all three transform elements
            const translateVals = (targetAnimation[0]?.getAttribute('values') || '').split(';');
            const scaleVals = (targetAnimation[1]?.getAttribute('values') || '').split(';');
            const rotateVals = (targetAnimation[2]?.getAttribute('values') || '').split(';');

            // Assume all arrays have the same length, based on the first one
            const valuesLength = translateVals.length;
            if (valuesLength === 0 || scaleVals.length !== valuesLength || rotateVals.length !== valuesLength) {
                console.warn('animateTransform value arrays have mismatched lengths or are empty.');
                // Continue anyway, but might produce incomplete results
            }

            const combinedValues = [];
            for (let i = 0; i < valuesLength; i++) {
                // Combine corresponding values with ';'
                const combined = `${translateVals[i] || ''};${scaleVals[i] || ''};${rotateVals[i] || ''}`;
                combinedValues.push(combined);
            }

            // Map unique combined values to representative letters (a-z, A-Z)
            const valueMap = new Map();
            const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
            let charIndex = 0;
            const representativeLetters = [];

            for (const value of combinedValues) {
                let letter = valueMap.get(value);
                if (!letter) {
                    // Assign a new letter if this value hasn't been seen
                    if (charIndex < alphabet.length) {
                        letter = alphabet[charIndex];
                        charIndex++;
                    } else {
                        // Fallback if we run out of letters (more than 52 unique steps)
                        letter = `?${charIndex - alphabet.length + 1}`;
                    }
                    valueMap.set(value, letter);
                }
                representativeLetters.push(letter);
            }

            // Create and append spans for each representative letter
            for (const letter of representativeLetters) {
                const newSpan = createEl('span', 'etaniAVItem', letter);
                valuesElement.appendChild(newSpan);
            }
            break;

        case 'animate':
            // targetAnimation is a single Element
            const values = targetAnimation.getAttribute('values');
            if (values) {
                const valueArray = values.split(';');
                // Create a span for each value
                for (const val of valueArray) {
                    if (val.trim() !== '') { // Avoid creating spans for empty values (e.g., from "a;;b")
                        const newSpan = createEl('span', 'etaniAVItem', val);
                        valuesElement.appendChild(newSpan);
                    }
                }
            }
            break;

        case 'animateMotion':
            // targetAnimation is a single Element
            const pathValue = targetAnimation.getAttribute('path');
            let motionValue = '';

            if (pathValue) {
                // Use 'path' attribute if it exists
                motionValue = pathValue;
            } else {
                // Otherwise, find the <mpath> element and use its 'href'
                const mpathElement = targetAnimation.querySelector('mpath');
                if (mpathElement) {
                    motionValue = mpathElement.getAttribute('href') || '';
                }
            }
            
            const motionSpan = createEl('span', 'etaniAVItem', motionValue);
            valuesElement.appendChild(motionSpan);
            break;

        case 'set':
            // targetAnimation is a single Element
            const toValue = targetAnimation.getAttribute('to') || '';
            
            // Create a span for the 'to' attribute value
            const setSpan = createEl('span', 'etaniAVItem', toValue);
            valuesElement.appendChild(setSpan);
            break;

        default:
            // Handle unknown tagname
            console.warn(`Unhandled animation tag: ${tagname}`);
    }
}

// edit Animate Attribute
function editAnimateAttribute() {
    const targetAnimation = getTargetAnimation(this);
    let editDurValue;
    if (Array.isArray(targetAnimation)) {
        editDurValue = targetAnimation[0].getAttribute('dur');
    } else {
        editDurValue = targetAnimation.getAttribute('dur');
    }
    editDurValue = editDurValue.replace('s', '');

    const editDur = createEl('div', 'etaniEditDur');
    const editDurLabel = createEl('label', 'editDurLabel', 'dur = ');
    const editDurInput = createEl('input', 'editDurInput');
    editDurInput.type = 'text';
    editDurInput.value = editDurValue;
    const editDurSpan = createEl('span', 'editDurSpan', ' s');
    editDurLabel.appendChild(editDurInput);
    editDurLabel.appendChild(editDurSpan);
    editDur.appendChild(editDurLabel);
    etaniWindow(editDur, () => {
        let setDurValue = document.querySelector('.editDurInput').value;
        setDurValue = setDurValue + 's';
        if (Array.isArray(targetAnimation)) {
            targetAnimation[0].setAttribute('dur', setDurValue);
            targetAnimation[1].setAttribute('dur', setDurValue);
            targetAnimation[2].setAttribute('dur', setDurValue);
        } else {
            targetAnimation.setAttribute('dur', setDurValue);
        }
        this.textContent = 'dur = ' + setDurValue;
        updateEtaniResult();
    });
}

// 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);
});
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复

在线用户

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