分页: 13 / 13

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

发表于 : 2025年 10月 29日 12:42
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();
    });
}

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

发表于 : 2025年 10月 29日 12:48
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
}

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

发表于 : 2025年 10月 29日 13:20
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);
});

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

发表于 : 2025年 10月 29日 13:29
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);
});