正在做etani,ejtile的動畫套件

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

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

帖子 ejsoon »

當前代碼:

代码: 全选

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

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

    const styleSheet = createEl('style');
    styleSheet.id = 'dynamic-et-styles'; 
    styleSheet.textContent = `
.etaniinner {
  margin-top: 10px;
}
.etaniCtrl {
  margin-bottom: 10px;
  clear: both;
  padding: 5px;
  border: 1px solid #c0c0c0;
  text-align: center;
}
.etaniCtrl > div {
  display: inline-block;
  vertical-align: top;
  padding: 5px;
  border: 1px solid #ccc;
  margin: 0 5px 5px 5px;
  text-align: left;
}
.etaniCtrl > div > a, .etaniCtrl > div > span {
  display: inline-block;
  text-decoration: none;
  padding: 2px 8px;
  font-size: 14px;
  margin: 0 2px;
  cursor: pointer;
  user-select: none;
}
.etaniCtrl > div > span {
  border: 1px solid #888;
  background-color: #eee;
  color: #333;
}
.etaniCtrl > div > span.active {
  background-color: #008CBA;
  color: white;
  border-color: #008CBA;
}
.etaniContentHTML {
  border: 1px solid #db3a32;
  color: #db3a32;
}
.etaniUpdateTiles {
  border: 1px solid #008CBA;
  color: #008CBA;
}
.etaniCenter {
  border: 1px solid green;
  color: green;
}
.etaniAllAppendTransform {
  border: 1px solid #2e36b9;
  color: #2e36b9;
}
.etaniAllAppendOpacity {
  border: 1px solid #b68942;
  color: #b68942;
}
.etaniValueIncrease {
  border: 1px solid purple;
  color: purple;
}
.etaniCol {
  border: 1px solid #aaa;
  padding: 5px;
  margin-bottom: 10px;
  clear: both;
}
.etaniItem {
  min-height: 48px;
  border: 1px solid #ccc;
  box-sizing: border-box;
  width: 100%;
  margin-bottom: -1px;
  background-color: lightyellow;
  display: inline-block;
}
.etaniItemLeft {
  float: left;
  width: 60px;
  min-height: 48px;
  padding: 2px 0;
  text-align: center;
}
.etaniItemImageOuter {
  width: 40px;
  height: 40px;
  margin: 0 auto;
}
.etaniItemImage {
  width: 100%;
  height: 100%;
  display: block;
}
.etaniItemId {
  text-align: center;
  font-size: 12px;
  word-break: break-all;
  margin-top: 2px;
  cursor: pointer;
}
.etaniItemPlus {
  width: 12px;
  height: 12px;
  display: inline-block;
  margin-left: 2px;
}
.etaniItemRight {
  margin-left: 60px;
  padding: 7px;
  min-height: 64px;
  background-color: #fff;
}
.etaniWindow {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: white;
  padding: 24px;
  border: 1px solid #ccc;
  box-shadow: 0 4px 8px rgba(0,0,0,0.2);
  z-index: 777;
  width: auto;
  display: inline-block;
}
.etaniAppendRow {
  margin-bottom: 8px;
}
.etaniAppendRow a, .etaniAppendRow span {
  display: inline-block;
  padding: 4px 8px;
  margin-right: 5px;
  border: 1px solid #888;
  background-color: #eee;
  color: #333;
  cursor: pointer;
  text-decoration: none;
  font-size: 12px;
}
.etaniAppendSet.active {
  background-color: #008CBA;
  color: white;
  border-color: #008CBA;
}
.etaniWindow label {
  display: inline;
  margin-bottom: 10px;
}
.etaniAppendSpecify {
  display: inline;
  width: 120px;
  box-sizing: border-box;
  padding: 4px;
  font-size: 12px;
}
.etaniWindow button {
  margin-right: 10px;
  padding: 5px 10px;
}
.etaniResult {
  text-align: center;
  margin-bottom: 10px;
  padding: 10px;
  border: 1px solid #bbb;
  box-sizing: border-box;
}
.etaniResultImage {
  display: block;
  max-width: 480px;
  width: 100%;
  height: auto;
  margin: 0 auto 10px auto;
  border: 1px solid #000;
  box-sizing: border-box;
}
.etaniResultDR {
  text-align: center;
  margin-bottom: 10px;
}
.etaniResultDownload, .etaniResultRename {
  display: inline-block;
  margin-right: 15px;
  text-decoration: none;
  padding: 5px 10px;
  font-size: 16px;
}
.etaniResultDownload {
  border: 1px solid blue;
  color: blue;
}
.etaniResultRename {
  border: 1px solid brown;
  color: brown;
}
.etaniResultSize {
  display: inline-block;
  margin-left: 10px;
  font-size: 12px;
  color: #555;
}
.etaniAnimate {
  border: 1px solid #999;
  padding: 5px;
  margin-bottom: 5px;
}
.etaniAnimateAttr {
  margin-bottom: 5px;
}
.etaniAnimateAttr > span {
  cursor: pointer;
  display: inline-block;
  padding: 2px 5px;
  font-size: 12px;
  box-sizing: border-box;
  border-width: 1px;
  border-style: solid;
}
.etaniAnimateName {
  background-color: #555;
  border-color: #555;
  color: white;
  margin-right: 10px;
}
.etaniAnimateAttr > span:not(.etaniAnimateName) {
  margin-right: 7px;
}
.etaniAnimateDur {
  border-color: blue;
  color: blue;
}
.etaniAnimateFR {
  border-color: #78229f;
  color: #78229f;
}
.etaniAnimateAttrAdd {
  border-color: #2c8c12;
  color: #2c8c12;
}
.etaniAVCtrl {
  display: inline-block;
  vertical-align: top;
  margin-right: 5px;
  margin-bottom: 3px;
}
.etaniAVCtrl > span {
  display: inline-block;
  width: 24px;
  height: 24px;
  cursor: pointer;
  vertical-align: top;
  margin-right: 3px;
  box-sizing: border-box;
}
.etaniAVLabel {
  font-size: 14px;
  margin-right: 5px;
}
.etaniAV {
  display: inline-block;
  vertical-align: top;
}
.etaniAVItem {
  display: inline-block;
  height: 24px;
  background-color: #ff9933;
  border: 1px dashed #00bfff;
  margin: 0 5px 3px;
  padding: 0 5px;
  box-sizing: border-box;
  cursor: pointer;
  position: relative;
  text-align: center;
  line-height: 24px;
  font-size: 12px;
  color: #333;
}
.etaniAVAdd {
  background-color: #a7fca7;
  border: 1px solid #71c371;
}
.etaniAVDelete {
  background-color: #ffcccc;
  border: 1px solid #cc3333;
}
.etaniAVCopy {
  background-color: #ccccff;
  border: 1px solid #6666cc;
}
.etaniAVMove {
  background-color: #ffcc99;
  border: 1px solid #cc9966;
}
.etaniAVDelete.active {
  background-color: #cc3333;
  color: white;
}
.etaniAVCopy.active {
  background-color: #6666cc;
  color: white;
}
.etaniAVMove.active {
  background-color: #cc9966;
  color: white;
}
.etaniAVCtrl > span > svg {
  margin-left: -1px;
  margin-top: -1px;
}
textarea.etaniHTMLTextarea {
  width: calc(100vw - 72px);
  height: calc(50vh - 24px);
  resize: none;
  border: 1px solid #ccc;
  font-size: 12px;
  box-sizing: border-box;
}
.etaniWindowRow {
  padding-top: 12px;
  text-align: center;
}
.etaniWindowRow button {
  margin: 0 12px;
}
.editDurInput {
  width: 36px;
}
.etaniEditDur {
  text-align: center;
}
.editInput {
  width: 120px;
}
    `;

    document.head.appendChild(styleSheet);
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        // Create etaniCtrl div
        addEtaniCtrlElements(etaniinner);

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

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

        etaniinner.appendChild(etaniResult);

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

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

        // list etani elements
        listEtaniItems();

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

        // update result
        updateEtaniResult();
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// Open append animate window
function etaniAppendAnimateWindow(id, dataType) {
    const appendAnimateWindow = createEl('div', 'appendAnimateWindow');
    // First row
    const row1 = createEl('div', 'etaniAppendRow');
    const strong = createEl('strong', null, 'id: ' + id);
    row1.appendChild(strong);
    appendAnimateWindow.appendChild(row1);

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

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

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

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

    // Add click events to all specified elements
    // For a elements with href='javascript:;'
    aTransform.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animateTransform', 'transform');
    });
    aMotion.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animateMotion', 'motion', 'M 0,0 H 120 V 120 Z');
    });
    aOpacity.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animate', 'opacity', 1);
    });
    aWriting.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animate', 'stroke-dasharray', 1);
    });
    aFill.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animate', 'fill', '#f758b8');
    });
    aStroke.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animate', 'stroke', '#7786ce');
    });
    aWidth.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, "animate", "stroke-width", 2);
    });
    // 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' };
        }
    }

    // Special attribute
    let targetEl;
    if (elementtype === 'tile') {
        targetEl = etani.querySelector(`.etdrop > use[href="#${id}"]`);
    } else {
        targetEl = etani.getElementById(id);
    }
    // Writting animation
    if (animatetype === 'stroke-dasharray') {
        // get the writing length
        let tPath = etani.getElementById(id);
        tPath = elementtype === 'tile' ? tPath.firstChild : tPath;
        let targetLength = Math.round(tPath.getTotalLength());
        // set dashoffset
        targetEl.setAttribute('stroke-dashoffset', targetLength);
        // set defaultvalue
        defaultvalue = targetLength + ';' + (targetLength * 2);
    } else if (animatetype === 'fill') {
        // get the original color
        let targetElFill = targetEl.getAttribute('fill');
        if (targetElFill) defaultvalue = targetElFill;
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    // 2. Build .etaniAnimateValue
    const elAnimateValue = createEl('div', 'etaniAnimateValue');
    elAnimateValue.dataset.mode = 'edit';
    const elAVLabel = createEl('span', 'etaniAVLabel');
    const elAV = createEl('div', 'etaniAV');
    const elAVItem = createEl('span', 'etaniAVItem');
    elAVItem.addEventListener('click', etaniAVItemClick);
    
    elAV.appendChild(elAVItem);

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

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

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

    // cname
    let cName;
    if (animatetype === 'stroke-width') {
        cName = 'width';
    } else if (animatetype === 'stroke-dasharray') {
        cName = 'writing';
        let tPath = etani.getElementById(id);
        tPath = elementtype === 'tile' ? tPath.firstChild : tPath;
        defaultvalue = Math.round(tPath.getTotalLength());
    } else {
        cName = animatetype;
    }

    // Special attribute and convert animateName
    let targetEl;
    if (elementtype === 'tile') {
        targetEl = etani.querySelector(`.etdrop > use[href="#${id}"]`);
    } else {
        targetEl = etani.getElementById(id);
    }
    if (animatetype === 'fill') {
        // get the original color
        let targetElFill = targetEl.getAttribute('fill');
        if (targetElFill) defaultvalue = targetElFill;
    }

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

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

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

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

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

    // if writing
    if (animatetype === 'stroke-dasharray') {
        elAV.appendChild(createEl('span', 'etaniAVItem', defaultvalue * 2));
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// Values item click event
function etaniAVItemClick () {
    // get standard data
    const targetAnimation = getTargetAnimation(this);
    const animateValue = this.closest('.etaniAnimateValue');
    const ctrlMode = animateValue.dataset.mode;
    const animateParent = this.closest('.etaniAnimate');
    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;
    const dataId = this.closest('.etaniItem').dataset.id;
    const dataType = this.closest('.etaniItem').dataset.type;
    const etaniAV = animateParent.querySelector('.etaniAV');
    // Get the index of this element among its siblings
    const itemIndex = Array.from(this.parentNode.children).indexOf(this);
    const values = targetAnimation.getAttribute('values');
    const valueArray = values.split(';');
    const targetValue = valueArray[itemIndex];

    if (tagName === 'animate') {
        const editDiv = createEl('div', 'etaniEditDiv');
        const editLabel = createEl('label', 'editLabel', animateType + ' = ');
        const editInput = createEl('input', 'editInput');
        editInput.type = 'input';
        editInput.value = targetValue;
        editLabel.appendChild(editInput);
        editDiv.appendChild(editLabel);
        etaniWindow(editDiv, () => {
            // set the animation value
            valueArray.splice(itemIndex, 1, editInput.value);
            targetAnimation.setAttribute('values', valueArray.join(';'));
            // update UI
            setUIAnimateValues(tagName, targetAnimation, etaniAV);
            // update result
            updateEtaniResult();
        });
    }
}

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

    // insert dynamic style
    addEtaniStyles();

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

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

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

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

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

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

帖子 ejsoon »

要開始提高一下效率啦,完善了values的增刪複移。
當前代碼:

代码: 全选

// 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;
}
.etaniAVItem.selected {
  background-color: #779933;
}
.etaniAVAdd {
  background-color: #a7fca7;
  border: 1px solid #71c371;
}
.etaniAVDelete {
  background-color: #ffcccc;
  border: 1px solid #cc3333;
}
.etaniAVCopy {
  background-color: #ccccff;
  border: 1px solid #6666cc;
}
.etaniAVMove {
  background-color: #ffcc99;
  border: 1px solid #cc9966;
}
.etaniAVDelete.active {
  background-color: #cc3333;
  color: white;
}
.etaniAVCopy.active {
  background-color: #6666cc;
  color: white;
}
.etaniAVMove.active {
  background-color: #cc9966;
  color: white;
}
.etaniAVCtrl > span > svg {
  margin-left: -1px;
  margin-top: -1px;
}
textarea.etaniHTMLTextarea {
  width: calc(100vw - 72px);
  height: calc(50vh - 24px);
  resize: none;
  border: 1px solid #ccc;
  font-size: 12px;
  box-sizing: border-box;
}
.etaniWindowRow {
  padding-top: 12px;
  text-align: center;
}
.etaniWindowRow button {
  margin: 0 12px;
}
.editDurInput {
  width: 36px;
}
.etaniEditDur {
  text-align: center;
}
.editInput {
  width: 120px;
}
    `;

    document.head.appendChild(styleSheet);
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        // Create etaniCtrl div
        addEtaniCtrlElements(etaniinner);

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

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

        etaniinner.appendChild(etaniResult);

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

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

        // list etani elements
        listEtaniItems();

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

        // update result
        updateEtaniResult();
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// Open append animate window
function etaniAppendAnimateWindow(id, dataType) {
    const appendAnimateWindow = createEl('div', 'appendAnimateWindow');
    // First row
    const row1 = createEl('div', 'etaniAppendRow');
    const strong = createEl('strong', null, 'id: ' + id);
    row1.appendChild(strong);
    appendAnimateWindow.appendChild(row1);

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

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

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

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

    // Add click events to all specified elements
    // For a elements with href='javascript:;'
    aTransform.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animateTransform', 'transform');
    });
    aMotion.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animateMotion', 'motion', 'M 0,0 H 120 V 120 Z');
    });
    aOpacity.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animate', 'opacity', 1);
    });
    aWriting.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animate', 'stroke-dasharray', 1);
    });
    aFill.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animate', 'fill', '#f758b8');
    });
    aStroke.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animate', 'stroke', '#7786ce');
    });
    aWidth.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, "animate", "stroke-width", 2);
    });
    // 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' };
        }
    }

    // Special attribute
    let targetEl;
    if (elementtype === 'tile') {
        targetEl = etani.querySelector(`.etdrop > use[href="#${id}"]`);
    } else {
        targetEl = etani.getElementById(id);
    }
    // Writting animation
    if (animatetype === 'stroke-dasharray') {
        // get the writing length
        let tPath = etani.getElementById(id);
        tPath = elementtype === 'tile' ? tPath.firstChild : tPath;
        let targetLength = Math.round(tPath.getTotalLength());
        // set dashoffset
        targetEl.setAttribute('stroke-dashoffset', targetLength);
        // set defaultvalue
        defaultvalue = targetLength + ';' + (targetLength * 2);
    } else if (animatetype === 'fill') {
        // get the original color
        let targetElFill = targetEl.getAttribute('fill');
        if (targetElFill) defaultvalue = targetElFill;
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    // Get the specific target animation (group) using the index
    const targetAnimation = groupedAnimations[animateIndex];
    if (!targetAnimation) {
        throw new Error(`No animation element found at index ${animateIndex}`);
    }
    return targetAnimation;
}
/**
 * Set the values to the animateTransform
 * @param {array} element - The three transform animation elements.
 * @param {array} string - The values, combined by semicolons.
 */
function setTransformValues(elements, values) {
    // Do not append if element or value is invalid
    let translateValues = '', scaleValues = '', rotateValues = '';
    for (let i = 0; i < values.length; i++) {
        translateValues += values[i].split(';')[0];
        translateValues += i < values.length - 1 ? ';' : '';
        scaleValues += values[i].split(';')[1];
        scaleValues += i < values.length - 1 ? ';' : '';
        rotateValues += values[i].split(';')[2];
        rotateValues += i < values.length - 1 ? ';' : '';
    }
    elements[0].setAttribute('values', translateValues)
    elements[1].setAttribute('values', scaleValues)
    elements[2].setAttribute('values', rotateValues)
}

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

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

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

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

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

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

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

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

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

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

    // 2. Build .etaniAnimateValue
    const elAnimateValue = createEl('div', 'etaniAnimateValue');
    elAnimateValue.dataset.mode = 'edit';
    const elAVLabel = createEl('span', 'etaniAVLabel');
    const elAV = createEl('div', 'etaniAV');
    const elAVItem = createEl('span', 'etaniAVItem');
    elAVItem.addEventListener('click', etaniAVItemClick);
    
    elAV.appendChild(elAVItem);

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

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

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

    // cname
    let cName;
    if (animatetype === 'stroke-width') {
        cName = 'width';
    } else if (animatetype === 'stroke-dasharray') {
        cName = 'writing';
        let tPath = etani.getElementById(id);
        tPath = elementtype === 'tile' ? tPath.firstChild : tPath;
        defaultvalue = Math.round(tPath.getTotalLength());
    } else {
        cName = animatetype;
    }

    // Special attribute and convert animateName
    let targetEl;
    if (elementtype === 'tile') {
        targetEl = etani.querySelector(`.etdrop > use[href="#${id}"]`);
    } else {
        targetEl = etani.getElementById(id);
    }
    if (animatetype === 'fill') {
        // get the original color
        let targetElFill = targetEl.getAttribute('fill');
        if (targetElFill) defaultvalue = targetElFill;
    }

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

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

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

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

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

    // if writing
    if (animatetype === 'stroke-dasharray') {
        elAV.appendChild(createEl('span', 'etaniAVItem', defaultvalue * 2));
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// get values from targetAnimation
function getValues(targetAnimation) {
    if (!Array.isArray(targetAnimation) || targetAnimation.length < 3) {
        return targetAnimation.getAttribute('values').split(';');
    }
    // Get values from all three transform elements
    const translateVals = (targetAnimation[0]?.getAttribute('values') || '').split(';');
    const scaleVals = (targetAnimation[1]?.getAttribute('values') || '').split(';');
    const rotateVals = (targetAnimation[2]?.getAttribute('values') || '').split(';');

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

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

// Values item click event
function etaniAVItemClick () {
    // get standard data
    const targetAnimation = getTargetAnimation(this);
    const animateValue = this.closest('.etaniAnimateValue');
    const ctrlMode = animateValue.dataset.mode;
    const animateParent = this.closest('.etaniAnimate');
    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;
    const dataId = this.closest('.etaniItem').dataset.id;
    const dataType = this.closest('.etaniItem').dataset.type;
    const etaniAV = animateParent.querySelector('.etaniAV');
    // Get the index of this element among its siblings
    const itemParent = this.parentNode;
    const itemIndex = Array.from(itemParent.children).indexOf(this);
    const valueArray = getValues(targetAnimation);
    const targetValue = valueArray[itemIndex];

    if (ctrlMode === 'delete') {
        valueArray.splice(itemIndex, 1);
        if (tagName === 'animate') {
            targetAnimation.setAttribute('values', valueArray.join(';'));
        } else {
            setTransformValues(targetAnimation, valueArray);
        }
        setUIAnimateValues(tagName, targetAnimation, etaniAV);
        updateEtaniResult();
    } else if (ctrlMode === 'copy') {
        valueArray.splice(itemIndex, 0, targetValue);
        if (tagName === 'animate') {
            targetAnimation.setAttribute('values', valueArray.join(';'));
        } else {
            setTransformValues(targetAnimation, valueArray);
        }
        setUIAnimateValues(tagName, targetAnimation, etaniAV);
        updateEtaniResult();
    } else if (ctrlMode === 'move') {
        let sItem = itemParent.querySelector('.selected');
        if (this === sItem) {
            this.classList.remove('selected');
        } else if (sItem) {
            const sIndex = Array.from(itemParent.children).indexOf(sItem);
            const sValue = valueArray[sIndex];
            if (sIndex < itemIndex) {
                if (this.nextSibling) {
                    itemParent.insertBefore(sItem, this.nextSibling);
                } else {
                    itemParent.appendChild(sItem);
                }
                valueArray.splice(itemIndex + 1, 0, sValue);
                valueArray.splice(sIndex, 1);
            } else {
                itemParent.insertBefore(sItem, this);
                valueArray.splice(sIndex, 1);
                valueArray.splice(itemIndex, 0, sValue);
            }
            if (tagName === 'animate') {
                targetAnimation.setAttribute('values', valueArray.join(';'));
            } else {
                setTransformValues(targetAnimation, valueArray);
            }
            setUIAnimateValues(tagName, targetAnimation, etaniAV);
            updateEtaniResult();
        } else {
            this.classList.add('selected');
        }
    } else if (tagName === 'animate') {
        const editDiv = createEl('div', 'etaniEditDiv');
        const editLabel = createEl('label', 'editLabel', animateType + ' = ');
        const editInput = createEl('input', 'editInput');
        editInput.type = 'input';
        editInput.value = targetValue;
        editLabel.appendChild(editInput);
        editDiv.appendChild(editLabel);
        etaniWindow(editDiv, () => {
            // set the animation value
            valueArray.splice(itemIndex, 1, editInput.value);
            targetAnimation.setAttribute('values', valueArray.join(';'));
            // update UI
            setUIAnimateValues(tagName, targetAnimation, etaniAV);
            // update result
            updateEtaniResult();
        });
    }
}

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

    // insert dynamic style
    addEtaniStyles();

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

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

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

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

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

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

帖子 ejsoon »

進一步完善「增刪複移改」values。

代码: 全选

// 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;
}
.etaniAVItem.selected {
  background-color: #779933;
}
.etaniAVAdd {
  background-color: #a7fca7;
  border: 1px solid #71c371;
}
.etaniAVDelete {
  background-color: #ffcccc;
  border: 1px solid #cc3333;
}
.etaniAVCopy {
  background-color: #ccccff;
  border: 1px solid #6666cc;
}
.etaniAVMove {
  background-color: #ffcc99;
  border: 1px solid #cc9966;
}
.etaniAVDelete.active {
  background-color: #cc3333;
  color: white;
}
.etaniAVCopy.active {
  background-color: #6666cc;
  color: white;
}
.etaniAVMove.active {
  background-color: #cc9966;
  color: white;
}
.etaniAVCtrl > span > svg {
  margin-left: -1px;
  margin-top: -1px;
}
textarea.etaniHTMLTextarea {
  width: calc(100vw - 72px);
  height: calc(50vh - 24px);
  resize: none;
  border: 1px solid #ccc;
  font-size: 12px;
  box-sizing: border-box;
}
.etaniWindowRow {
  padding-top: 12px;
  text-align: center;
}
.etaniWindowRow button {
  margin: 0 12px;
}
.editDurInput {
  width: 36px;
}
.etaniEditDur {
  text-align: center;
}
.editInput {
  width: 120px;
}
    `;

    document.head.appendChild(styleSheet);
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        // Create etaniCtrl div
        addEtaniCtrlElements(etaniinner);

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

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

        etaniinner.appendChild(etaniResult);

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

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

        // list etani elements
        listEtaniItems();

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

        // update result
        updateEtaniResult();
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// Open append animate window
function etaniAppendAnimateWindow(id, dataType) {
    const appendAnimateWindow = createEl('div', 'appendAnimateWindow');
    // First row
    const row1 = createEl('div', 'etaniAppendRow');
    const strong = createEl('strong', null, 'id: ' + id);
    row1.appendChild(strong);
    appendAnimateWindow.appendChild(row1);

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

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

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

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

    // Add click events to all specified elements
    // For a elements with href='javascript:;'
    aTransform.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animateTransform', 'transform');
    });
    aMotion.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animateMotion', 'motion', 'M 0,0 H 120 V 120 Z');
    });
    aOpacity.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animate', 'opacity', 1);
    });
    aWriting.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animate', 'stroke-dasharray', 1);
    });
    aFill.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animate', 'fill', '#f758b8');
    });
    aStroke.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animate', 'stroke', '#7786ce');
    });
    aWidth.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, "animate", "stroke-width", 2);
    });
    // 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' };
        }
    }

    // Special attribute
    let targetEl;
    if (elementtype === 'tile') {
        targetEl = etani.querySelector(`.etdrop > use[href="#${id}"]`);
    } else {
        targetEl = etani.getElementById(id);
    }
    // Writting animation
    if (animatetype === 'stroke-dasharray') {
        // get the writing length
        let tPath = etani.getElementById(id);
        tPath = elementtype === 'tile' ? tPath.firstChild : tPath;
        let targetLength = Math.round(tPath.getTotalLength());
        // set dashoffset
        targetEl.setAttribute('stroke-dashoffset', targetLength);
        // set defaultvalue
        defaultvalue = targetLength + ';' + (targetLength * 2);
    } else if (animatetype === 'fill') {
        // get the original color
        let targetElFill = targetEl.getAttribute('fill');
        if (targetElFill) defaultvalue = targetElFill;
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    // Get the specific target animation (group) using the index
    const targetAnimation = groupedAnimations[animateIndex];
    if (!targetAnimation) {
        throw new Error(`No animation element found at index ${animateIndex}`);
    }
    return targetAnimation;
}
/**
 * Set the values to the animation
 * @param {tagName} string - The values, combined by semicolons.
 * @param {targetAnimation} element - The animation elements.
 * @param {valueArray} array - The values, combined by semicolons.
 * @param {etaniAV} element - The values, combined by semicolons.
 */
function setValues(tagName, targetAnimation, valueArray, etaniAV) {
    if (tagName === 'animate') {
        targetAnimation.setAttribute('values', valueArray.join(';'));
    } else if (tagName === 'animateTransform') {
        // Do not append if element or value is invalid
        let translateValues = '', scaleValues = '', rotateValues = '';
        for (let i = 0; i < valueArray.length; i++) {
            translateValues += valueArray[i].split(';')[0];
            translateValues += i < valueArray.length - 1 ? ';' : '';
            scaleValues += valueArray[i].split(';')[1];
            scaleValues += i < valueArray.length - 1 ? ';' : '';
            rotateValues += valueArray[i].split(';')[2];
            rotateValues += i < valueArray.length - 1 ? ';' : '';
        }
        targetAnimation[0].setAttribute('values', translateValues)
        targetAnimation[1].setAttribute('values', scaleValues)
        targetAnimation[2].setAttribute('values', rotateValues)
    }
    setUIAnimateValues(tagName, targetAnimation, etaniAV);
    updateEtaniResult();
}

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

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

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

// Add animate value
function etaniAVAddClick() {
    // Change mode to default 'edit'
    const elAVCtrl = this.closest('.etaniAVCtrl');
    const active = elAVCtrl.querySelector('.active');
    if (active) active.classList.remove('active');
    const elAnimateValue = this.closest('.etaniAnimateValue');
    elAnimateValue.dataset.mode = 'edit';
    // --- Step 1-3: get targetAnimation element(s) ---
    let targetAnimation = getTargetAnimation(this);
    // --- Step 4: Insert values into target element ---
    let defaultValue; // This will be used for both Step 4 and 5
    const animateParent = this.closest('.etaniAnimate');
    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;
    const itemParent = this.closest('.etaniItem');
    const dataId = itemParent.dataset.id;
    const hrefId = `#${dataId}`; // Prepare for attribute selector
    if (tagName === 'animateTransform') {
        // Check if targetAnimation is valid (an array of 3)
        if (Array.isArray(targetAnimation) && targetAnimation.length === 3) {
            const mainUseElement = etmain.querySelector(`.etdrop > use[href="${hrefId}"]`);
            
            if (mainUseElement) {
                const transformString = mainUseElement.getAttribute('transform');
                
                // Extract values
                const translateVal = getTransformValue('translate', transformString);
                const scaleVal = getTransformValue('scale', transformString);
                const rotateVal = getTransformValue('rotate', transformString);

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

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

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

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

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

    // 2. Build .etaniAnimateValue
    const elAnimateValue = createEl('div', 'etaniAnimateValue');
    elAnimateValue.dataset.mode = 'edit';
    const elAVLabel = createEl('span', 'etaniAVLabel');
    const elAV = createEl('div', 'etaniAV');
    const elAVItem = createEl('span', 'etaniAVItem');
    elAVItem.addEventListener('click', etaniAVItemClick);
    
    elAV.appendChild(elAVItem);

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

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

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

    // cname
    let cName;
    if (animatetype === 'stroke-width') {
        cName = 'width';
    } else if (animatetype === 'stroke-dasharray') {
        cName = 'writing';
        let tPath = etani.getElementById(id);
        tPath = elementtype === 'tile' ? tPath.firstChild : tPath;
        defaultvalue = Math.round(tPath.getTotalLength());
    } else {
        cName = animatetype;
    }

    // Special attribute and convert animateName
    let targetEl;
    if (elementtype === 'tile') {
        targetEl = etani.querySelector(`.etdrop > use[href="#${id}"]`);
    } else {
        targetEl = etani.getElementById(id);
    }
    if (animatetype === 'fill') {
        // get the original color
        let targetElFill = targetEl.getAttribute('fill');
        if (targetElFill) defaultvalue = targetElFill;
    }

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

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

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

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

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

    // if writing
    if (animatetype === 'stroke-dasharray') {
        elAV.appendChild(createEl('span', 'etaniAVItem', defaultvalue * 2));
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// get values from targetAnimation
function getValues(targetAnimation) {
    if (!Array.isArray(targetAnimation) || targetAnimation.length < 3) {
        return targetAnimation.getAttribute('values').split(';');
    }
    // Get values from all three transform elements
    const translateVals = (targetAnimation[0]?.getAttribute('values') || '').split(';');
    const scaleVals = (targetAnimation[1]?.getAttribute('values') || '').split(';');
    const rotateVals = (targetAnimation[2]?.getAttribute('values') || '').split(';');

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

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

// Values item click event
function etaniAVItemClick () {
    // get standard data
    const targetAnimation = getTargetAnimation(this);
    const animateValue = this.closest('.etaniAnimateValue');
    const ctrlMode = animateValue.dataset.mode;
    const animateParent = this.closest('.etaniAnimate');
    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;
    const dataId = this.closest('.etaniItem').dataset.id;
    const dataType = this.closest('.etaniItem').dataset.type;
    const etaniAV = animateParent.querySelector('.etaniAV');
    // Get the index of this element among its siblings
    const itemParent = this.parentNode;
    const itemIndex = Array.from(itemParent.children).indexOf(this);
    const valueArray = getValues(targetAnimation);
    const targetValue = valueArray[itemIndex];

    if (ctrlMode === 'delete') {
        valueArray.splice(itemIndex, 1);
        setValues(tagName, targetAnimation, valueArray, etaniAV);
    } else if (ctrlMode === 'copy') {
        valueArray.splice(itemIndex, 0, targetValue);
        setValues(tagName, targetAnimation, valueArray, etaniAV);
    } else if (ctrlMode === 'move') {
        let sItem = itemParent.querySelector('.selected');
        if (this === sItem) {
            this.classList.remove('selected');
        } else if (sItem) {
            const sIndex = Array.from(itemParent.children).indexOf(sItem);
            const sValue = valueArray[sIndex];
            if (sIndex < itemIndex) {
                if (this.nextSibling) {
                    itemParent.insertBefore(sItem, this.nextSibling);
                } else {
                    itemParent.appendChild(sItem);
                }
                valueArray.splice(itemIndex + 1, 0, sValue);
                valueArray.splice(sIndex, 1);
            } else {
                itemParent.insertBefore(sItem, this);
                valueArray.splice(sIndex, 1);
                valueArray.splice(itemIndex, 0, sValue);
            }
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        } else {
            this.classList.add('selected');
        }
    } else if (tagName === 'animate') {
        const editDiv = createEl('div', 'etaniEditDiv');
        const editLabel = createEl('label', 'editLabel', animateType + ' = ');
        const editInput = createEl('input', 'editInput');
        editInput.type = 'input';
        editInput.value = targetValue;
        editLabel.appendChild(editInput);
        editDiv.appendChild(editLabel);
        etaniWindow(editDiv, () => {
            valueArray.splice(itemIndex, 1, editInput.value);
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        });
    } else if (tagName === 'animateTransform') {
        const editDiv = createEl('div', 'etaniEditDiv');
        // translate
        const translateRow = createEl('div', 'etaniAppendRow');
        const translateLabel = createEl('label', 'editLabel', 'translate = ');
        const translateInput = createEl('input', 'editInput');
        translateInput.type = 'input';
        translateInput.value = targetValue.split(';')[0];
        translateRow.appendChild(translateLabel);
        translateRow.appendChild(translateInput);
        editDiv.appendChild(translateRow);
        // scale
        const scaleRow = createEl('div', 'etaniAppendRow');
        const scaleLabel = createEl('label', 'editLabel', 'scale = ');
        const scaleInput = createEl('input', 'editInput');
        scaleInput.type = 'input';
        scaleInput.value = targetValue.split(';')[1];
        scaleRow.appendChild(scaleLabel);
        scaleRow.appendChild(scaleInput);
        editDiv.appendChild(scaleRow);
        // rotate
        const rotateRow = createEl('div', 'etaniAppendRow');
        const rotateLabel = createEl('label', 'editLabel', 'rotate = ');
        const rotateInput = createEl('input', 'editInput');
        rotateInput.type = 'input';
        rotateInput.value = targetValue.split(';')[2];
        rotateRow.appendChild(rotateLabel);
        rotateRow.appendChild(rotateInput);
        editDiv.appendChild(rotateRow);

        etaniWindow(editDiv, () => {
            let combineValue = translateInput.value + ';' + 
                scaleInput.value + ';' + rotateInput.value;
            valueArray.splice(itemIndex, 1, combineValue);
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        });
    }
}

// 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);
});
下一步就是motion和set,然後是attr。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4001
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 190 次
联系:

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

帖子 ejsoon »

做好motion。
當前代碼:

代码: 全选

// 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;
}
.etaniAVItem.selected {
  background-color: #779933;
}
.etaniAVAdd {
  background-color: #a7fca7;
  border: 1px solid #71c371;
}
.etaniAVDelete {
  background-color: #ffcccc;
  border: 1px solid #cc3333;
}
.etaniAVCopy {
  background-color: #ccccff;
  border: 1px solid #6666cc;
}
.etaniAVMove {
  background-color: #ffcc99;
  border: 1px solid #cc9966;
}
.etaniAVDelete.active {
  background-color: #cc3333;
  color: white;
}
.etaniAVCopy.active {
  background-color: #6666cc;
  color: white;
}
.etaniAVMove.active {
  background-color: #cc9966;
  color: white;
}
.etaniAVCtrl > span > svg {
  margin-left: -1px;
  margin-top: -1px;
}
textarea.etaniHTMLTextarea {
  width: calc(100vw - 72px);
  height: calc(50vh - 24px);
  resize: none;
  border: 1px solid #ccc;
  font-size: 12px;
  box-sizing: border-box;
}
.etaniWindowRow {
  padding-top: 12px;
  text-align: center;
}
.etaniWindowRow button {
  margin: 0 12px;
}
.editDurInput {
  width: 36px;
}
.etaniEditDur {
  text-align: center;
}
.editInput {
  width: 120px;
}
    `;

    document.head.appendChild(styleSheet);
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        // Create etaniCtrl div
        addEtaniCtrlElements(etaniinner);

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

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

        etaniinner.appendChild(etaniResult);

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

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

        // list etani elements
        listEtaniItems();

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

        // update result
        updateEtaniResult();
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// Open append animate window
function etaniAppendAnimateWindow(id, dataType) {
    const appendAnimateWindow = createEl('div', 'appendAnimateWindow');
    // First row
    const row1 = createEl('div', 'etaniAppendRow');
    const strong = createEl('strong', null, 'id: ' + id);
    row1.appendChild(strong);
    appendAnimateWindow.appendChild(row1);

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

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

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

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

    // Add click events to all specified elements
    // For a elements with href='javascript:;'
    aTransform.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animateTransform', 'transform');
    });
    aMotion.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animateMotion', 'motion', 'M 0,0 H 120 V 120 Z');
    });
    aOpacity.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animate', 'opacity', 1);
    });
    aWriting.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animate', 'stroke-dasharray', 1);
    });
    aFill.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animate', 'fill', '#f758b8');
    });
    aStroke.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, 'animate', 'stroke', '#7786ce');
    });
    aWidth.addEventListener('click', function() {
        etaniAppendAnimate(id, dataType, "animate", "stroke-width", 2);
    });
    // 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' };
        }
    }

    // Special attribute
    let targetEl;
    if (elementtype === 'tile') {
        targetEl = etani.querySelector(`.etdrop > use[href="#${id}"]`);
    } else {
        targetEl = etani.getElementById(id);
    }
    // Writting animation
    if (animatetype === 'stroke-dasharray') {
        // get the writing length
        let tPath = etani.getElementById(id);
        tPath = elementtype === 'tile' ? tPath.firstChild : tPath;
        let targetLength = Math.round(tPath.getTotalLength());
        // set dashoffset
        targetEl.setAttribute('stroke-dashoffset', targetLength);
        // set defaultvalue
        defaultvalue = targetLength + ';' + (targetLength * 2);
    } else if (animatetype === 'fill') {
        // get the original color
        let targetElFill = targetEl.getAttribute('fill');
        if (targetElFill) defaultvalue = targetElFill;
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    // Get the specific target animation (group) using the index
    const targetAnimation = groupedAnimations[animateIndex];
    if (!targetAnimation) {
        throw new Error(`No animation element found at index ${animateIndex}`);
    }
    return targetAnimation;
}
/**
 * Set the values to the animation
 * @param {tagName} string - The values, combined by semicolons.
 * @param {targetAnimation} element - The animation elements.
 * @param {valueArray} array - The values, combined by semicolons.
 * @param {etaniAV} element - The values, combined by semicolons.
 */
function setValues(tagName, targetAnimation, valueArray, etaniAV) {
    if (tagName === 'animate') {
        targetAnimation.setAttribute('values', valueArray.join(';'));
    } else if (tagName === 'animateTransform') {
        // Do not append if element or value is invalid
        let translateValues = '', scaleValues = '', rotateValues = '';
        for (let i = 0; i < valueArray.length; i++) {
            translateValues += valueArray[i].split(';')[0];
            translateValues += i < valueArray.length - 1 ? ';' : '';
            scaleValues += valueArray[i].split(';')[1];
            scaleValues += i < valueArray.length - 1 ? ';' : '';
            rotateValues += valueArray[i].split(';')[2];
            rotateValues += i < valueArray.length - 1 ? ';' : '';
        }
        targetAnimation[0].setAttribute('values', translateValues)
        targetAnimation[1].setAttribute('values', scaleValues)
        targetAnimation[2].setAttribute('values', rotateValues)
    } else if (tagName === 'animateMotion') {
        let editInputValue = valueArray[0];
        if (targetAnimation.querySelector('mpath')) {
            targetAnimation.querySelector('mpath').remove();
        }
        if (editInputValue.substring(0, 1) === '#') {
            let mpath = createSVGElement('mpath', {href : editInputValue});
            targetAnimation.appendChild(mpath);
            if (targetAnimation.hasAttribute('path')) {
                targetAnimation.removeAttribute('path');
            }
        } else {
            targetAnimation.setAttribute('path', editInputValue);
        }
    }
    setUIAnimateValues(tagName, targetAnimation, etaniAV);
    updateEtaniResult();
}

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

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

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

// Add animate value
function etaniAVAddClick() {
    // Change mode to default 'edit'
    const elAVCtrl = this.closest('.etaniAVCtrl');
    const active = elAVCtrl.querySelector('.active');
    if (active) active.classList.remove('active');
    const elAnimateValue = this.closest('.etaniAnimateValue');
    elAnimateValue.dataset.mode = 'edit';
    // --- Step 1-3: get targetAnimation element(s) ---
    let targetAnimation = getTargetAnimation(this);
    // --- Step 4: Insert values into target element ---
    let defaultValue; // This will be used for both Step 4 and 5
    const animateParent = this.closest('.etaniAnimate');
    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;
    const itemParent = this.closest('.etaniItem');
    const dataId = itemParent.dataset.id;
    const hrefId = `#${dataId}`; // Prepare for attribute selector
    if (tagName === 'animateTransform') {
        // Check if targetAnimation is valid (an array of 3)
        if (Array.isArray(targetAnimation) && targetAnimation.length === 3) {
            const mainUseElement = etmain.querySelector(`.etdrop > use[href="${hrefId}"]`);
            
            if (mainUseElement) {
                const transformString = mainUseElement.getAttribute('transform');
                
                // Extract values
                const translateVal = getTransformValue('translate', transformString);
                const scaleVal = getTransformValue('scale', transformString);
                const rotateVal = getTransformValue('rotate', transformString);

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

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

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

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

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

    // 2. Build .etaniAnimateValue
    const elAnimateValue = createEl('div', 'etaniAnimateValue');
    elAnimateValue.dataset.mode = 'edit';
    const elAVLabel = createEl('span', 'etaniAVLabel');
    const elAV = createEl('div', 'etaniAV');
    const elAVItem = createEl('span', 'etaniAVItem');
    elAVItem.addEventListener('click', etaniAVItemClick);
    
    elAV.appendChild(elAVItem);

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

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

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

    // cname
    let cName;
    if (animatetype === 'stroke-width') {
        cName = 'width';
    } else if (animatetype === 'stroke-dasharray') {
        cName = 'writing';
        let tPath = etani.getElementById(id);
        tPath = elementtype === 'tile' ? tPath.firstChild : tPath;
        defaultvalue = Math.round(tPath.getTotalLength());
    } else {
        cName = animatetype;
    }

    // Special attribute and convert animateName
    let targetEl;
    if (elementtype === 'tile') {
        targetEl = etani.querySelector(`.etdrop > use[href="#${id}"]`);
    } else {
        targetEl = etani.getElementById(id);
    }
    if (animatetype === 'fill') {
        // get the original color
        let targetElFill = targetEl.getAttribute('fill');
        if (targetElFill) defaultvalue = targetElFill;
    }

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

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

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

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

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

    // if writing
    if (animatetype === 'stroke-dasharray') {
        elAV.appendChild(createEl('span', 'etaniAVItem', defaultvalue * 2));
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// get values from targetAnimation
function getValues(targetAnimation) {
    if (!Array.isArray(targetAnimation)) {
        if (targetAnimation.tagName === 'animate') {
            return targetAnimation.getAttribute('values').split(';');
        } else if (targetAnimation.tagName === 'animateMotion') {
            if (targetAnimation.hasAttribute('path')) {
                return [targetAnimation.getAttribute('path')];
            } else if (targetAnimation.querySelector('mpath')) {
                return [targetAnimation.querySelector('mpath').getAttribute('href')];
            }
        } else if (targetAnimation.tagName === 'set') {
            return [targetAnimation.getAttribute('to')];
        }
    }
    // Get values from all three transform elements
    const translateVals = (targetAnimation[0]?.getAttribute('values') || '').split(';');
    const scaleVals = (targetAnimation[1]?.getAttribute('values') || '').split(';');
    const rotateVals = (targetAnimation[2]?.getAttribute('values') || '').split(';');

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

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

// Values item click event
function etaniAVItemClick () {
    // get standard data
    const targetAnimation = getTargetAnimation(this);
    const animateValue = this.closest('.etaniAnimateValue');
    const ctrlMode = animateValue.dataset.mode;
    const animateParent = this.closest('.etaniAnimate');
    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;
    const dataId = this.closest('.etaniItem').dataset.id;
    const dataType = this.closest('.etaniItem').dataset.type;
    const etaniAV = animateParent.querySelector('.etaniAV');
    // Get the index of this element among its siblings
    const itemParent = this.parentNode;
    const itemIndex = Array.from(itemParent.children).indexOf(this);
    const valueArray = getValues(targetAnimation);
    const targetValue = valueArray[itemIndex];

    if (ctrlMode === 'delete') {
        valueArray.splice(itemIndex, 1);
        setValues(tagName, targetAnimation, valueArray, etaniAV);
    } else if (ctrlMode === 'copy') {
        valueArray.splice(itemIndex, 0, targetValue);
        setValues(tagName, targetAnimation, valueArray, etaniAV);
    } else if (ctrlMode === 'move') {
        let sItem = itemParent.querySelector('.selected');
        if (this === sItem) {
            this.classList.remove('selected');
        } else if (sItem) {
            const sIndex = Array.from(itemParent.children).indexOf(sItem);
            const sValue = valueArray[sIndex];
            if (sIndex < itemIndex) {
                if (this.nextSibling) {
                    itemParent.insertBefore(sItem, this.nextSibling);
                } else {
                    itemParent.appendChild(sItem);
                }
                valueArray.splice(itemIndex + 1, 0, sValue);
                valueArray.splice(sIndex, 1);
            } else {
                itemParent.insertBefore(sItem, this);
                valueArray.splice(sIndex, 1);
                valueArray.splice(itemIndex, 0, sValue);
            }
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        } else {
            this.classList.add('selected');
        }
    } else if (tagName === 'animate') {
        const editDiv = createEl('div', 'etaniEditDiv');
        const editLabel = createEl('label', 'editLabel', animateType + ' = ');
        const editInput = createEl('input', 'editInput');
        editInput.type = 'input';
        editInput.value = targetValue;
        editLabel.appendChild(editInput);
        editDiv.appendChild(editLabel);
        etaniWindow(editDiv, () => {
            valueArray.splice(itemIndex, 1, editInput.value);
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        });
    } else if (tagName === 'animateTransform') {
        const editDiv = createEl('div', 'etaniEditDiv');
        // translate
        const translateRow = createEl('div', 'etaniAppendRow');
        const translateLabel = createEl('label', 'editLabel', 'translate = ');
        const translateInput = createEl('input', 'editInput');
        translateInput.type = 'input';
        translateInput.value = targetValue.split(';')[0];
        translateRow.appendChild(translateLabel);
        translateRow.appendChild(translateInput);
        editDiv.appendChild(translateRow);
        // scale
        const scaleRow = createEl('div', 'etaniAppendRow');
        const scaleLabel = createEl('label', 'editLabel', 'scale = ');
        const scaleInput = createEl('input', 'editInput');
        scaleInput.type = 'input';
        scaleInput.value = targetValue.split(';')[1];
        scaleRow.appendChild(scaleLabel);
        scaleRow.appendChild(scaleInput);
        editDiv.appendChild(scaleRow);
        // rotate
        const rotateRow = createEl('div', 'etaniAppendRow');
        const rotateLabel = createEl('label', 'editLabel', 'rotate = ');
        const rotateInput = createEl('input', 'editInput');
        rotateInput.type = 'input';
        rotateInput.value = targetValue.split(';')[2];
        rotateRow.appendChild(rotateLabel);
        rotateRow.appendChild(rotateInput);
        editDiv.appendChild(rotateRow);

        etaniWindow(editDiv, () => {
            let combineValue = translateInput.value + ';' + 
                scaleInput.value + ';' + rotateInput.value;
            valueArray.splice(itemIndex, 1, combineValue);
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        });
    } else if (tagName === 'animateMotion') {
        const editDiv = createEl('div', 'etaniEditDiv');
        const editLabel = createEl('label', 'editLabel', 'path = ');
        const editInput = createEl('input', 'editInput');
        editInput.type = 'input';
        editInput.value = targetValue;
        editLabel.appendChild(editInput);
        editDiv.appendChild(editLabel);

        etaniWindow(editDiv, () => {
            setValues(tagName, targetAnimation, [editInput.value], etaniAV);
        });
    }
}

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

    // insert dynamic style
    addEtaniStyles();

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

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

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

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

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

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

帖子 ejsoon »

代码: 全选

// 假设 'this' 是一个 DOM 元素(如在類別方法中),並且 'targetAnimation' 是一個物件或元素
function getRectHeightAndCheckAnimation() {
  // 拿到 this 的 rect 高 (使用 getBoundingClientRect())
  const rectHeight = this.getBoundingClientRect().height;
  console.log('Rect height:', rectHeight);

  // 拿到 targetAnimation 的所有屬性 (使用 Object.keys())
  const animationProps = Object.keys(targetAnimation);

  // 檢查是否有 'id' 屬性
  if (!animationProps.includes('id')) {
    // TODO: 處理沒有 id 的情況,例如拋出錯誤或添加預設值
    console.log('TODO: targetAnimation 缺少 id 屬性');
  }

  // 檢查是否有 'begin' 屬性
  if (!animationProps.includes('begin')) {
    // TODO: 處理沒有 begin 的情況,例如拋出錯誤或添加預設值
    console.log('TODO: targetAnimation 缺少 begin 屬性');
  }

  // 如果需要,返回結果或其他處理
  return { height: rectHeight, props: animationProps };
}

// 示例使用:需要定義 this 和 targetAnimation
// 例如,在一個類別中:
// class MyElement {
//   constructor() {
//     this.targetAnimation = { duration: '5s' }; // 示例物件,缺少 id 和 begin
//   }
//   check() {
//     getRectHeightAndCheckAnimation.call(document.getElementById('myElement'));
//   }
// }
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4001
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 190 次
联系:

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

帖子 ejsoon »

代码: 全选

const attrObj = Array.from(element.attributes).reduce((obj, attr) => {
  obj[attr.name] = attr.value;
  return obj;
}, {});

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

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

帖子 ejsoon »

做好了刪整個動畫,準備開始做attr的顯增刪改。

當前代碼:

代码: 全选

// 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;
  user-select: none;
}
.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 {
  position: relative;
  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;
}
.etaniAVItem.selected {
  background-color: #779933;
}
.etaniAVAdd {
  background-color: #a7fca7;
  border: 1px solid #71c371;
}
.etaniAVDelete {
  background-color: #ffcccc;
  border: 1px solid #cc3333;
}
.etaniAVCopy {
  background-color: #ccccff;
  border: 1px solid #6666cc;
}
.etaniAVMove {
  background-color: #ffcc99;
  border: 1px solid #cc9966;
}
.etaniAVDelete.active {
  background-color: #cc3333;
  color: white;
}
.etaniAVCopy.active {
  background-color: #6666cc;
  color: white;
}
.etaniAVMove.active {
  background-color: #cc9966;
  color: white;
}
.etaniAVCtrl > span > svg {
  margin-left: -1px;
  margin-top: -1px;
}
textarea.etaniHTMLTextarea {
  width: calc(100vw - 72px);
  height: calc(50vh - 24px);
  resize: none;
  border: 1px solid #ccc;
  font-size: 12px;
  box-sizing: border-box;
}
.etaniWindowRow {
  padding-top: 12px;
  text-align: center;
}
.etaniWindowRow button {
  margin: 0 12px;
}
.editDurInput {
  width: 36px;
}
.etaniEditDur {
  text-align: center;
}
.editInput {
  width: 120px;
}
.etaniAnimateAttrRemove {
  float: right;
  margin-right: 0 !important;
  border-color: #c33;
  color: #861616;
  background: #f6cdcd;
}
.etaniDropdown {
  position: absolute;
  left: 0px;
  background-color: #fff;
  border: 1px solid #ccc;
  box-shadow: 0 4px 8px rgba(0,0,0,0.1);
  z-index: 1200;
}
.etaniDropdownItem {
  color: black;
  padding: 8px 12px;
  cursor: pointer;
  font-size: 12px;
}
.etaniDropdownItem:hover {
  color: green;
  background-color: #f0f0f0;
}
    `;

    document.head.appendChild(styleSheet);
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        // Create etaniCtrl div
        addEtaniCtrlElements(etaniinner);

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

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

        etaniinner.appendChild(etaniResult);

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

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

        // list etani elements
        listEtaniItems();

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

        // update result
        updateEtaniResult();
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// Open append animate window
function etaniAppendAnimateWindow(id, dataType) {
    const appendAnimateWindow = createEl('div', 'appendAnimateWindow');
    appendAnimateWindow.dataset.id = id;
    appendAnimateWindow.dataset.type = dataType;
    // 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', etaniAppendAnimate);
    aMotion.addEventListener('click', etaniAppendAnimate);
    aOpacity.addEventListener('click', etaniAppendAnimate);
    aWriting.addEventListener('click', etaniAppendAnimate);
    aFill.addEventListener('click', etaniAppendAnimate);
    aStroke.addEventListener('click', etaniAppendAnimate);
    aWidth.addEventListener('click', etaniAppendAnimate);
    // 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;
    }

    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' };
        }
    }
    // if type is board, it must has the 'href' attribute
    let boardHref = {};
    if (elementtype === 'board') {
        boardHref = { href: `#${id}` };
    }

    // Special attribute
    let targetEl;
    if (elementtype === 'tile') {
        targetEl = etani.querySelector(`.etdrop > use[href="#${id}"]`);
    } else {
        targetEl = etani.getElementById(id);
    }
    // Writting animation
    if (animatetype === 'stroke-dasharray') {
        // get the writing length
        let tPath = etani.getElementById(id);
        tPath = elementtype === 'tile' ? tPath.firstChild : tPath;
        let targetLength = Math.round(tPath.getTotalLength());
        // set dashoffset
        targetEl.setAttribute('stroke-dashoffset', targetLength);
        // set defaultvalue
        defaultvalue = targetLength + ';' + (targetLength * 2);
    } else if (animatetype === 'fill') {
        // get the original color
        let targetElFill = targetEl.getAttribute('fill');
        if (targetElFill) defaultvalue = targetElFill;
    }

    // 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",
                ...boardHref,
                ...repeatOrFillAttrs // Add the repeat/fill logic
            };

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    // Get the specific target animation (group) using the index
    const targetAnimation = groupedAnimations[animateIndex];
    if (!targetAnimation) {
        throw new Error(`No animation element found at index ${animateIndex}`);
    }
    return targetAnimation;
}
/**
 * Set the values to the animation
 * @param {tagName} string - The values, combined by semicolons.
 * @param {targetAnimation} element - The animation elements.
 * @param {valueArray} array - The values, combined by semicolons.
 * @param {etaniAV} element - The values, combined by semicolons.
 */
function setValues(tagName, targetAnimation, valueArray, etaniAV) {
    if (tagName === 'animate') {
        targetAnimation.setAttribute('values', valueArray.join(';'));
    } else if (tagName === 'animateTransform') {
        // Do not append if element or value is invalid
        let translateValues = '', scaleValues = '', rotateValues = '';
        for (let i = 0; i < valueArray.length; i++) {
            translateValues += valueArray[i].split(';')[0];
            translateValues += i < valueArray.length - 1 ? ';' : '';
            scaleValues += valueArray[i].split(';')[1];
            scaleValues += i < valueArray.length - 1 ? ';' : '';
            rotateValues += valueArray[i].split(';')[2];
            rotateValues += i < valueArray.length - 1 ? ';' : '';
        }
        targetAnimation[0].setAttribute('values', translateValues)
        targetAnimation[1].setAttribute('values', scaleValues)
        targetAnimation[2].setAttribute('values', rotateValues)
    } else if (tagName === 'animateMotion') {
        let editInputValue = valueArray[0];
        if (targetAnimation.querySelector('mpath')) {
            targetAnimation.querySelector('mpath').remove();
        }
        if (editInputValue.substring(0, 1) === '#') {
            let mpath = createSVGElement('mpath', {href : editInputValue});
            targetAnimation.appendChild(mpath);
            if (targetAnimation.hasAttribute('path')) {
                targetAnimation.removeAttribute('path');
            }
        } else {
            targetAnimation.setAttribute('path', editInputValue);
        }
    } else if (tagName === 'set') {
        let editInputValue = valueArray[0];
        targetAnimation.setAttribute('to', valueArray[0]);
    }
    setUIAnimateValues(tagName, targetAnimation, etaniAV);
    updateEtaniResult();
}

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

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

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

// Add animate value
function etaniAVAddClick() {
    // Change mode to default 'edit'
    const elAVCtrl = this.closest('.etaniAVCtrl');
    const active = elAVCtrl.querySelector('.active');
    if (active) active.classList.remove('active');
    const elAnimateValue = this.closest('.etaniAnimateValue');
    elAnimateValue.dataset.mode = 'edit';
    // --- Step 1-3: get targetAnimation element(s) ---
    let targetAnimation = getTargetAnimation(this);
    // --- Step 4: Insert values into target element ---
    let defaultValue; // This will be used for both Step 4 and 5
    const animateParent = this.closest('.etaniAnimate');
    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;
    const itemParent = this.closest('.etaniItem');
    const dataId = itemParent.dataset.id;
    const hrefId = `#${dataId}`; // Prepare for attribute selector
    if (tagName === 'animateTransform') {
        // Check if targetAnimation is valid (an array of 3)
        if (Array.isArray(targetAnimation) && targetAnimation.length === 3) {
            const mainUseElement = etmain.querySelector(`.etdrop > use[href="${hrefId}"]`);
            
            if (mainUseElement) {
                const transformString = mainUseElement.getAttribute('transform');
                
                // Extract values
                const translateVal = getTransformValue('translate', transformString);
                const scaleVal = getTransformValue('scale', transformString);
                const rotateVal = getTransformValue('rotate', transformString);

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

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

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

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

    const elAnimateAttrAdd = createEl('span', 'etaniAnimateAttrAdd', '+');
    elAnimateAttrAdd.addEventListener('click', elAnimateAttrAddClick);
    const elAnimateAttrRemove = createEl('span', 'etaniAnimateAttrRemove', '×');
    elAnimateAttrRemove.addEventListener('click', elAnimateAttrRemoveClick);
    
    elAnimateAttr.appendChild(elAnimateAttrRemove);
    elAnimateAttr.appendChild(elAnimateName);
    elAnimateAttr.appendChild(elAnimateDur);
    elAnimateAttr.appendChild(elAnimateAttrAdd);

    // 2. Build .etaniAnimateValue
    const elAnimateValue = createEl('div', 'etaniAnimateValue');
    elAnimateValue.dataset.mode = 'edit';
    const elAVLabel = createEl('span', 'etaniAVLabel');
    const elAV = createEl('div', 'etaniAV');
    const elAVItem = createEl('span', 'etaniAVItem');
    elAVItem.addEventListener('click', etaniAVItemClick);
    
    elAV.appendChild(elAVItem);

    // create controls for this type
    const elAVCtrl = createEl('div', 'etaniAVCtrl');
    const etaniAVAdd = createEl('span', 'etaniAVAdd');
    etaniAVAdd.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>';
    const etaniAVDelete = createEl('span', 'etaniAVDelete');
    etaniAVDelete.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>';
    const etaniAVCopy = createEl('span', 'etaniAVCopy');
    etaniAVCopy.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
    const etaniAVMove = createEl('span', 'etaniAVMove');
    etaniAVMove.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M 7,4 L 3,8 L 7,12 M 3,8 L 18,8 M 17,12 L 21,16 L 17,20 M 21,16 L 6,16"></path></svg>';
    elAVCtrl.appendChild(etaniAVAdd);
    elAVCtrl.appendChild(etaniAVDelete);
    elAVCtrl.appendChild(etaniAVCopy);
    elAVCtrl.appendChild(etaniAVMove);
    etaniAVAdd.addEventListener('click', etaniAVAddClick);

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

    // cname
    let cName;
    if (animatetype === 'stroke-width') {
        cName = 'width';
    } else if (animatetype === 'stroke-dasharray') {
        cName = 'writing';
        let tPath = etani.getElementById(id);
        tPath = elementtype === 'tile' ? tPath.firstChild : tPath;
        defaultvalue = Math.round(tPath.getTotalLength());
    } else {
        cName = animatetype;
    }

    // Special attribute and convert animateName
    let targetEl;
    if (elementtype === 'tile') {
        targetEl = etani.querySelector(`.etdrop > use[href="#${id}"]`);
    } else {
        targetEl = etani.getElementById(id);
    }
    if (animatetype === 'fill') {
        // get the original color
        let targetElFill = targetEl.getAttribute('fill');
        if (targetElFill) defaultvalue = targetElFill;
    }

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

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

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

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

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

    // if writing
    if (animatetype === 'stroke-dasharray') {
        elAV.appendChild(createEl('span', 'etaniAVItem', defaultvalue * 2));
    }
}

// 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() {
    // let id, elementtype, tagname, animatetype, defaultvalue;
    let appendAnimateWindow = this.closest('.appendAnimateWindow');
    let id = appendAnimateWindow.dataset.id;
    let elementtype = appendAnimateWindow.dataset.type;
    let tagname = 'animate', animatetype = '', defaultvalue = 0;
    if (this.innerHTML === 'transform') {
        tagname = 'animateTransform';
        animatetype = 'transform';
    } else if (this.innerHTML === 'motion') {
        tagname = 'animateMotion';
        defaultvalue = 'M 0,0 H 120 V 120 Z';
    } else if (this.innerHTML === 'opacity') {
        animatetype = 'opacity';
        defaultvalue = 1;
    } else if (this.innerHTML === 'writing') {
        animatetype = 'stroke-dasharray';
        defaultvalue = 0;
    } else if (this.innerHTML === 'fill') {
        animatetype = 'fill';
        defaultvalue = '#f758b8';
    } else if (this.innerHTML === 'stroke') {
        animatetype = 'stroke';
        defaultvalue = '#7786ce';
    } else if (this.innerHTML === 'width') {
        animatetype = 'stroke-width';
        defaultvalue = 2;
    }
    let setSpan = appendAnimateWindow.querySelector('.etaniAppendSet');
    if (setSpan.classList.contains('active')) tagname = 'set';
    appendAnimateToResult(id, elementtype, tagname, animatetype, defaultvalue);
    appendAnimateToItemRight(id, elementtype, tagname, animatetype, defaultvalue);
    updateEtaniResult();
    if (document.querySelector('.etaniWindow')) {
        document.querySelector('.etaniWindow').remove();
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

// Add attribute to the target animation element
function elAnimateAttrAddClick(e) {
    if (this.querySelector('.etaniDropdown')) {
        if (e.target.innerHTML === 'id') {
            console.log('add id');
        } else if (e.target.innerHTML === 'begin') {
            console.log('add begin');
        } else if (e.target.innerHTML === 'other') {
            console.log('add other');
        }
        this.querySelector('.etaniDropdown').remove();
        return;
    }
    const etaniDropdown = createEl('div', 'etaniDropdown');
    etaniDropdown.style.top = this.getBoundingClientRect().height + 'px';
    let targetAni = getTargetAnimation(this);
    if (Array.isArray(targetAnimation)) targetAni = targetAnimation[0];
    const attrObj = Array.from(targetAni.attributes).reduce((obj, attr) => {
        obj[attr.name] = attr.value;
        return obj;
    }, {});
    const animationProps = Object.keys(attrObj);
    if (!animationProps.includes('id')) {
        const itemid = createEl('div', 'etaniDropdownItem', 'id');
        etaniDropdown.appendChild(itemid);
    }
    if (!animationProps.includes('begin')) {
        const itemBegin = createEl('div', 'etaniDropdownItem', 'begin');
        etaniDropdown.appendChild(itemBegin);
    }
    const itemOther = createEl('div', 'etaniDropdownItem', 'other');
    etaniDropdown.appendChild(itemOther);
    this.appendChild(etaniDropdown);
}

// remove the target animation element
function elAnimateAttrRemoveClick() {
    getTargetAnimation(this).remove();
    this.closest('.etaniAnimate').remove();
    updateEtaniResult();
}

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

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

// get values from targetAnimation
function getValues(targetAnimation) {
    if (!Array.isArray(targetAnimation)) {
        if (targetAnimation.tagName === 'animate') {
            return targetAnimation.getAttribute('values').split(';');
        } else if (targetAnimation.tagName === 'animateMotion') {
            if (targetAnimation.hasAttribute('path')) {
                return [targetAnimation.getAttribute('path')];
            } else if (targetAnimation.querySelector('mpath')) {
                return [targetAnimation.querySelector('mpath').getAttribute('href')];
            }
        } else if (targetAnimation.tagName === 'set') {
            return [targetAnimation.getAttribute('to')];
        }
    }
    // Get values from all three transform elements
    const translateVals = (targetAnimation[0]?.getAttribute('values') || '').split(';');
    const scaleVals = (targetAnimation[1]?.getAttribute('values') || '').split(';');
    const rotateVals = (targetAnimation[2]?.getAttribute('values') || '').split(';');

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

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

// Values item click event
function etaniAVItemClick () {
    // get standard data
    const targetAnimation = getTargetAnimation(this);
    const animateValue = this.closest('.etaniAnimateValue');
    const ctrlMode = animateValue.dataset.mode;
    const animateParent = this.closest('.etaniAnimate');
    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;
    const dataId = this.closest('.etaniItem').dataset.id;
    const dataType = this.closest('.etaniItem').dataset.type;
    const etaniAV = animateParent.querySelector('.etaniAV');
    // Get the index of this element among its siblings
    const itemParent = this.parentNode;
    const itemIndex = Array.from(itemParent.children).indexOf(this);
    const valueArray = getValues(targetAnimation);
    const targetValue = valueArray[itemIndex];

    if (ctrlMode === 'delete') {
        if (valueArray.length > 1) {
            valueArray.splice(itemIndex, 1);
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        }
    } else if (ctrlMode === 'copy') {
        valueArray.splice(itemIndex, 0, targetValue);
        setValues(tagName, targetAnimation, valueArray, etaniAV);
    } else if (ctrlMode === 'move') {
        let sItem = itemParent.querySelector('.selected');
        if (this === sItem) {
            this.classList.remove('selected');
        } else if (sItem) {
            const sIndex = Array.from(itemParent.children).indexOf(sItem);
            const sValue = valueArray[sIndex];
            if (sIndex < itemIndex) {
                if (this.nextSibling) {
                    itemParent.insertBefore(sItem, this.nextSibling);
                } else {
                    itemParent.appendChild(sItem);
                }
                valueArray.splice(itemIndex + 1, 0, sValue);
                valueArray.splice(sIndex, 1);
            } else {
                itemParent.insertBefore(sItem, this);
                valueArray.splice(sIndex, 1);
                valueArray.splice(itemIndex, 0, sValue);
            }
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        } else {
            this.classList.add('selected');
        }
    } else if (tagName === 'animate') {
        const editDiv = createEl('div', 'etaniEditDiv');
        const editLabel = createEl('label', 'editLabel', animateType + ' = ');
        const editInput = createEl('input', 'editInput');
        editInput.type = 'input';
        editInput.value = targetValue;
        editLabel.appendChild(editInput);
        editDiv.appendChild(editLabel);
        etaniWindow(editDiv, () => {
            valueArray.splice(itemIndex, 1, editInput.value);
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        });
    } else if (tagName === 'animateTransform') {
        const editDiv = createEl('div', 'etaniEditDiv');
        // translate
        const translateRow = createEl('div', 'etaniAppendRow');
        const translateLabel = createEl('label', 'editLabel', 'translate = ');
        const translateInput = createEl('input', 'editInput');
        translateInput.type = 'input';
        translateInput.value = targetValue.split(';')[0];
        translateRow.appendChild(translateLabel);
        translateRow.appendChild(translateInput);
        editDiv.appendChild(translateRow);
        // scale
        const scaleRow = createEl('div', 'etaniAppendRow');
        const scaleLabel = createEl('label', 'editLabel', 'scale = ');
        const scaleInput = createEl('input', 'editInput');
        scaleInput.type = 'input';
        scaleInput.value = targetValue.split(';')[1];
        scaleRow.appendChild(scaleLabel);
        scaleRow.appendChild(scaleInput);
        editDiv.appendChild(scaleRow);
        // rotate
        const rotateRow = createEl('div', 'etaniAppendRow');
        const rotateLabel = createEl('label', 'editLabel', 'rotate = ');
        const rotateInput = createEl('input', 'editInput');
        rotateInput.type = 'input';
        rotateInput.value = targetValue.split(';')[2];
        rotateRow.appendChild(rotateLabel);
        rotateRow.appendChild(rotateInput);
        editDiv.appendChild(rotateRow);

        etaniWindow(editDiv, () => {
            let combineValue = translateInput.value + ';' + 
                scaleInput.value + ';' + rotateInput.value;
            valueArray.splice(itemIndex, 1, combineValue);
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        });
    } else if (tagName === 'animateMotion') {
        const editDiv = createEl('div', 'etaniEditDiv');
        const editLabel = createEl('label', 'editLabel', 'path = ');
        const editInput = createEl('input', 'editInput');
        editInput.type = 'input';
        editInput.value = targetValue;
        editLabel.appendChild(editInput);
        editDiv.appendChild(editLabel);

        etaniWindow(editDiv, () => {
            setValues(tagName, targetAnimation, [editInput.value], etaniAV);
        });
    } else if (tagName === 'set') {
        const editDivTitle = createEl('div', 'etaniEditDiv');
        const editTitle = createEl('label', 'editLabel', 'set ' + animateType);
        const editDiv = createEl('div', 'etaniEditDiv');
        const editLabel = createEl('label', 'editLabel', 'to = ');
        const editInput = createEl('input', 'editInput');
        editInput.type = 'input';
        editInput.value = targetValue;
        editDivTitle.appendChild(editTitle);
        editLabel.appendChild(editInput);
        editDiv.appendChild(editDivTitle);
        editDiv.appendChild(editLabel);

        etaniWindow(editDiv, () => {
            setValues(tagName, targetAnimation, [editInput.value], etaniAV);
        });
    }
}

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

    // insert dynamic style
    addEtaniStyles();

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

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

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

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

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

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

帖子 ejsoon »

正在寫屬性的增改。
當前代碼:

代码: 全选

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

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

    const styleSheet = 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;
  user-select: none;
}
.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 {
  position: relative;
  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;
}
.etaniAVItem.selected {
  background-color: #779933;
}
.etaniAVAdd {
  background-color: #a7fca7;
  border: 1px solid #71c371;
}
.etaniAVDelete {
  background-color: #ffcccc;
  border: 1px solid #cc3333;
}
.etaniAVCopy {
  background-color: #ccccff;
  border: 1px solid #6666cc;
}
.etaniAVMove {
  background-color: #ffcc99;
  border: 1px solid #cc9966;
}
.etaniAVDelete.active {
  background-color: #cc3333;
  color: white;
}
.etaniAVCopy.active {
  background-color: #6666cc;
  color: white;
}
.etaniAVMove.active {
  background-color: #cc9966;
  color: white;
}
.etaniAVCtrl > span > svg {
  margin-left: -1px;
  margin-top: -1px;
}
textarea.etaniHTMLTextarea {
  width: calc(100vw - 72px);
  height: calc(50vh - 24px);
  resize: none;
  border: 1px solid #ccc;
  font-size: 12px;
  box-sizing: border-box;
}
.etaniWindowRow {
  padding-top: 12px;
  text-align: center;
}
.etaniWindowRow button {
  margin: 0 12px;
}
.editDurInput {
  width: 36px;
}
.etaniEditDur {
  text-align: center;
}
.editInput {
  width: 120px;
}
.etaniAnimateAttrRemove {
  float: right;
  margin-right: 0 !important;
  border-color: #c33;
  color: #861616;
  background: #f6cdcd;
}
.etaniDropdown {
  position: absolute;
  left: 0px;
  background-color: #fff;
  border: 1px solid #ccc;
  box-shadow: 0 4px 8px rgba(0,0,0,0.1);
  z-index: 1200;
}
.etaniDropdownItem {
  color: black;
  padding: 8px 12px;
  cursor: pointer;
  font-size: 12px;
}
.etaniDropdownItem:hover {
  color: green;
  background-color: #f0f0f0;
}
    `;

    document.head.appendChild(styleSheet);
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        // Create etaniCtrl div
        addEtaniCtrlElements(etaniinner);

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

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

        etaniinner.appendChild(etaniResult);

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

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

        // list etani elements
        listEtaniItems();

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

        // update result
        updateEtaniResult();
    }
}

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

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

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

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

// Handle the HTML popup window
function handleContentHTMLClick() {
    if (!etani) return;
    const textarea = createEl('textarea', 'etaniHTMLTextarea');
    textarea.value = new XMLSerializer().serializeToString(etani);
    etaniWindow(textarea, () => {
        const parser = new DOMParser();
        etani = parser.parseFromString(textarea.value, 'image/svg+xml');
        updateEtaniResult();
    });
}

// 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');
    appendAnimateWindow.dataset.id = id;
    appendAnimateWindow.dataset.type = dataType;
    // 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', etaniAppendAnimate);
    aMotion.addEventListener('click', etaniAppendAnimate);
    aOpacity.addEventListener('click', etaniAppendAnimate);
    aWriting.addEventListener('click', etaniAppendAnimate);
    aFill.addEventListener('click', etaniAppendAnimate);
    aStroke.addEventListener('click', etaniAppendAnimate);
    aWidth.addEventListener('click', etaniAppendAnimate);
    // 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;
    }

    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' };
        }
    }
    // if type is board, it must has the 'href' attribute
    let boardHref = {};
    if (elementtype === 'board') {
        boardHref = { href: `#${id}` };
    }

    // Special attribute
    let targetEl;
    if (elementtype === 'tile') {
        targetEl = etani.querySelector(`.etdrop > use[href="#${id}"]`);
    } else {
        targetEl = etani.getElementById(id);
    }
    // Writting animation
    if (animatetype === 'stroke-dasharray') {
        // get the writing length
        let tPath = etani.getElementById(id);
        tPath = elementtype === 'tile' ? tPath.firstChild : tPath;
        let targetLength = Math.round(tPath.getTotalLength());
        // set dashoffset
        targetEl.setAttribute('stroke-dashoffset', targetLength);
        // set defaultvalue
        defaultvalue = targetLength + ';' + (targetLength * 2);
    } else if (animatetype === 'fill') {
        // get the original color
        let targetElFill = targetEl.getAttribute('fill');
        if (targetElFill) defaultvalue = targetElFill;
    }

    // 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",
                ...boardHref,
                ...repeatOrFillAttrs // Add the repeat/fill logic
            };

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    // Get the specific target animation (group) using the index
    const targetAnimation = groupedAnimations[animateIndex];
    if (!targetAnimation) {
        throw new Error(`No animation element found at index ${animateIndex}`);
    }
    return targetAnimation;
}
/**
 * Set the values to the animation
 * @param {tagName} string - The values, combined by semicolons.
 * @param {targetAnimation} element - The animation elements.
 * @param {valueArray} array - The values, combined by semicolons.
 * @param {etaniAV} element - The values, combined by semicolons.
 */
function setValues(tagName, targetAnimation, valueArray, etaniAV) {
    if (tagName === 'animate') {
        targetAnimation.setAttribute('values', valueArray.join(';'));
    } else if (tagName === 'animateTransform') {
        // Do not append if element or value is invalid
        let translateValues = '', scaleValues = '', rotateValues = '';
        for (let i = 0; i < valueArray.length; i++) {
            translateValues += valueArray[i].split(';')[0];
            translateValues += i < valueArray.length - 1 ? ';' : '';
            scaleValues += valueArray[i].split(';')[1];
            scaleValues += i < valueArray.length - 1 ? ';' : '';
            rotateValues += valueArray[i].split(';')[2];
            rotateValues += i < valueArray.length - 1 ? ';' : '';
        }
        targetAnimation[0].setAttribute('values', translateValues)
        targetAnimation[1].setAttribute('values', scaleValues)
        targetAnimation[2].setAttribute('values', rotateValues)
    } else if (tagName === 'animateMotion') {
        let editInputValue = valueArray[0];
        if (targetAnimation.querySelector('mpath')) {
            targetAnimation.querySelector('mpath').remove();
        }
        if (editInputValue.substring(0, 1) === '#') {
            let mpath = createSVGElement('mpath', {href : editInputValue});
            targetAnimation.appendChild(mpath);
            if (targetAnimation.hasAttribute('path')) {
                targetAnimation.removeAttribute('path');
            }
        } else {
            targetAnimation.setAttribute('path', editInputValue);
        }
    } else if (tagName === 'set') {
        let editInputValue = valueArray[0];
        targetAnimation.setAttribute('to', valueArray[0]);
    }
    setUIAnimateValues(tagName, targetAnimation, etaniAV);
    updateEtaniResult();
}

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

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

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

// Add animate value
function etaniAVAddClick() {
    // Change mode to default 'edit'
    const elAVCtrl = this.closest('.etaniAVCtrl');
    const active = elAVCtrl.querySelector('.active');
    if (active) active.classList.remove('active');
    const elAnimateValue = this.closest('.etaniAnimateValue');
    elAnimateValue.dataset.mode = 'edit';
    // --- Step 1-3: get targetAnimation element(s) ---
    let targetAnimation = getTargetAnimation(this);
    // --- Step 4: Insert values into target element ---
    let defaultValue; // This will be used for both Step 4 and 5
    const animateParent = this.closest('.etaniAnimate');
    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;
    const itemParent = this.closest('.etaniItem');
    const dataId = itemParent.dataset.id;
    const hrefId = `#${dataId}`; // Prepare for attribute selector
    if (tagName === 'animateTransform') {
        // Check if targetAnimation is valid (an array of 3)
        if (Array.isArray(targetAnimation) && targetAnimation.length === 3) {
            const mainUseElement = etmain.querySelector(`.etdrop > use[href="${hrefId}"]`);
            
            if (mainUseElement) {
                const transformString = mainUseElement.getAttribute('transform');
                
                // Extract values
                const translateVal = getTransformValue('translate', transformString);
                const scaleVal = getTransformValue('scale', transformString);
                const rotateVal = getTransformValue('rotate', transformString);

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

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

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

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

    const elAnimateAttrAdd = createEl('span', 'etaniAnimateAttrAdd', '+');
    elAnimateAttrAdd.addEventListener('click', elAnimateAttrAddClick);
    const elAnimateAttrRemove = createEl('span', 'etaniAnimateAttrRemove', '×');
    elAnimateAttrRemove.addEventListener('click', elAnimateAttrRemoveClick);
    
    elAnimateAttr.appendChild(elAnimateAttrRemove);
    elAnimateAttr.appendChild(elAnimateName);
    elAnimateAttr.appendChild(elAnimateDur);
    elAnimateAttr.appendChild(elAnimateAttrAdd);

    // 2. Build .etaniAnimateValue
    const elAnimateValue = createEl('div', 'etaniAnimateValue');
    elAnimateValue.dataset.mode = 'edit';
    const elAVLabel = createEl('span', 'etaniAVLabel');
    const elAV = createEl('div', 'etaniAV');
    const elAVItem = createEl('span', 'etaniAVItem');
    elAVItem.addEventListener('click', etaniAVItemClick);
    
    elAV.appendChild(elAVItem);

    // create controls for this type
    const elAVCtrl = createEl('div', 'etaniAVCtrl');
    const etaniAVAdd = createEl('span', 'etaniAVAdd');
    etaniAVAdd.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>';
    const etaniAVDelete = createEl('span', 'etaniAVDelete');
    etaniAVDelete.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>';
    const etaniAVCopy = createEl('span', 'etaniAVCopy');
    etaniAVCopy.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
    const etaniAVMove = createEl('span', 'etaniAVMove');
    etaniAVMove.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M 7,4 L 3,8 L 7,12 M 3,8 L 18,8 M 17,12 L 21,16 L 17,20 M 21,16 L 6,16"></path></svg>';
    elAVCtrl.appendChild(etaniAVAdd);
    elAVCtrl.appendChild(etaniAVDelete);
    elAVCtrl.appendChild(etaniAVCopy);
    elAVCtrl.appendChild(etaniAVMove);
    etaniAVAdd.addEventListener('click', etaniAVAddClick);

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

    // cname
    let cName;
    if (animatetype === 'stroke-width') {
        cName = 'width';
    } else if (animatetype === 'stroke-dasharray') {
        cName = 'writing';
        let tPath = etani.getElementById(id);
        tPath = elementtype === 'tile' ? tPath.firstChild : tPath;
        defaultvalue = Math.round(tPath.getTotalLength());
    } else {
        cName = animatetype;
    }

    // Special attribute and convert animateName
    let targetEl;
    if (elementtype === 'tile') {
        targetEl = etani.querySelector(`.etdrop > use[href="#${id}"]`);
    } else {
        targetEl = etani.getElementById(id);
    }
    if (animatetype === 'fill') {
        // get the original color
        let targetElFill = targetEl.getAttribute('fill');
        if (targetElFill) defaultvalue = targetElFill;
    }

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

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

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

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

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

    // if writing
    if (animatetype === 'stroke-dasharray') {
        elAV.appendChild(createEl('span', 'etaniAVItem', defaultvalue * 2));
    }
}

// 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() {
    // let id, elementtype, tagname, animatetype, defaultvalue;
    let appendAnimateWindow = this.closest('.appendAnimateWindow');
    let id = appendAnimateWindow.dataset.id;
    let elementtype = appendAnimateWindow.dataset.type;
    let tagname = 'animate', animatetype = '', defaultvalue = 0;
    if (this.innerHTML === 'transform') {
        tagname = 'animateTransform';
        animatetype = 'transform';
    } else if (this.innerHTML === 'motion') {
        tagname = 'animateMotion';
        defaultvalue = 'M 0,0 H 120 V 120 Z';
    } else if (this.innerHTML === 'opacity') {
        animatetype = 'opacity';
        defaultvalue = 1;
    } else if (this.innerHTML === 'writing') {
        animatetype = 'stroke-dasharray';
        defaultvalue = 0;
    } else if (this.innerHTML === 'fill') {
        animatetype = 'fill';
        defaultvalue = '#f758b8';
    } else if (this.innerHTML === 'stroke') {
        animatetype = 'stroke';
        defaultvalue = '#7786ce';
    } else if (this.innerHTML === 'width') {
        animatetype = 'stroke-width';
        defaultvalue = 2;
    }
    let setSpan = appendAnimateWindow.querySelector('.etaniAppendSet');
    if (setSpan.classList.contains('active')) tagname = 'set';
    appendAnimateToResult(id, elementtype, tagname, animatetype, defaultvalue);
    appendAnimateToItemRight(id, elementtype, tagname, animatetype, defaultvalue);
    updateEtaniResult();
    if (document.querySelector('.etaniWindow')) {
        document.querySelector('.etaniWindow').remove();
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

// Add attribute to the target animation element
function elAnimateAttrAddClick(e) {
    if (this.querySelector('.etaniDropdown')) {
        if (e.target.innerHTML === 'id') {
            // Collect animate elements
            const cssSelector = `
                .etdrop > use > animate[id],
                .etdrop > use > animateTransform[id],
                .etdrop > use > animateMotion[id],
                .etdrop > use > set[id],
                :scope > animate[id],
                :scope > animateTransform[id],
                :scope > animateMotion[id],
                :scope > set[id]
            `;
            let hasIdEle = etani.querySelectorAll(cssSelector);
            console.log(hasIdEle);
            console.log('add id');
        } else if (e.target.innerHTML === 'begin') {
            console.log('add begin');
        } else if (e.target.innerHTML === 'other') {
            console.log('add other');
        }
        this.querySelector('.etaniDropdown').remove();
        return;
    }
    const etaniDropdown = createEl('div', 'etaniDropdown');
    etaniDropdown.style.top = this.getBoundingClientRect().height + 'px';
    let targetAni = getTargetAnimation(this);
    if (Array.isArray(targetAni)) targetAni = targetAni[0];
    const attrObj = Array.from(targetAni.attributes).reduce((obj, attr) => {
        obj[attr.name] = attr.value;
        return obj;
    }, {});
    const animationProps = Object.keys(attrObj);
    if (!animationProps.includes('id')) {
        const itemid = createEl('div', 'etaniDropdownItem', 'id');
        etaniDropdown.appendChild(itemid);
    }
    if (!animationProps.includes('begin')) {
        const itemBegin = createEl('div', 'etaniDropdownItem', 'begin');
        etaniDropdown.appendChild(itemBegin);
    }
    const itemOther = createEl('div', 'etaniDropdownItem', 'other');
    etaniDropdown.appendChild(itemOther);
    this.appendChild(etaniDropdown);
}

// remove the target animation element
function elAnimateAttrRemoveClick() {
    getTargetAnimation(this).remove();
    this.closest('.etaniAnimate').remove();
    updateEtaniResult();
}

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

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

// get values from targetAnimation
function getValues(targetAnimation) {
    if (!Array.isArray(targetAnimation)) {
        if (targetAnimation.tagName === 'animate') {
            return targetAnimation.getAttribute('values').split(';');
        } else if (targetAnimation.tagName === 'animateMotion') {
            if (targetAnimation.hasAttribute('path')) {
                return [targetAnimation.getAttribute('path')];
            } else if (targetAnimation.querySelector('mpath')) {
                return [targetAnimation.querySelector('mpath').getAttribute('href')];
            }
        } else if (targetAnimation.tagName === 'set') {
            return [targetAnimation.getAttribute('to')];
        }
    }
    // Get values from all three transform elements
    const translateVals = (targetAnimation[0]?.getAttribute('values') || '').split(';');
    const scaleVals = (targetAnimation[1]?.getAttribute('values') || '').split(';');
    const rotateVals = (targetAnimation[2]?.getAttribute('values') || '').split(';');

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

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

// Values item click event
function etaniAVItemClick () {
    // get standard data
    const targetAnimation = getTargetAnimation(this);
    const animateValue = this.closest('.etaniAnimateValue');
    const ctrlMode = animateValue.dataset.mode;
    const animateParent = this.closest('.etaniAnimate');
    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;
    const dataId = this.closest('.etaniItem').dataset.id;
    const dataType = this.closest('.etaniItem').dataset.type;
    const etaniAV = animateParent.querySelector('.etaniAV');
    // Get the index of this element among its siblings
    const itemParent = this.parentNode;
    const itemIndex = Array.from(itemParent.children).indexOf(this);
    const valueArray = getValues(targetAnimation);
    const targetValue = valueArray[itemIndex];

    if (ctrlMode === 'delete') {
        if (valueArray.length > 1) {
            valueArray.splice(itemIndex, 1);
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        }
    } else if (ctrlMode === 'copy') {
        valueArray.splice(itemIndex, 0, targetValue);
        setValues(tagName, targetAnimation, valueArray, etaniAV);
    } else if (ctrlMode === 'move') {
        let sItem = itemParent.querySelector('.selected');
        if (this === sItem) {
            this.classList.remove('selected');
        } else if (sItem) {
            const sIndex = Array.from(itemParent.children).indexOf(sItem);
            const sValue = valueArray[sIndex];
            if (sIndex < itemIndex) {
                if (this.nextSibling) {
                    itemParent.insertBefore(sItem, this.nextSibling);
                } else {
                    itemParent.appendChild(sItem);
                }
                valueArray.splice(itemIndex + 1, 0, sValue);
                valueArray.splice(sIndex, 1);
            } else {
                itemParent.insertBefore(sItem, this);
                valueArray.splice(sIndex, 1);
                valueArray.splice(itemIndex, 0, sValue);
            }
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        } else {
            this.classList.add('selected');
        }
    } else if (tagName === 'animate') {
        const editDiv = createEl('div', 'etaniEditDiv');
        const editLabel = createEl('label', 'editLabel', animateType + ' = ');
        const editInput = createEl('input', 'editInput');
        editInput.type = 'input';
        editInput.value = targetValue;
        editLabel.appendChild(editInput);
        editDiv.appendChild(editLabel);
        etaniWindow(editDiv, () => {
            valueArray.splice(itemIndex, 1, editInput.value);
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        });
    } else if (tagName === 'animateTransform') {
        const editDiv = createEl('div', 'etaniEditDiv');
        // translate
        const translateRow = createEl('div', 'etaniAppendRow');
        const translateLabel = createEl('label', 'editLabel', 'translate = ');
        const translateInput = createEl('input', 'editInput');
        translateInput.type = 'input';
        translateInput.value = targetValue.split(';')[0];
        translateRow.appendChild(translateLabel);
        translateRow.appendChild(translateInput);
        editDiv.appendChild(translateRow);
        // scale
        const scaleRow = createEl('div', 'etaniAppendRow');
        const scaleLabel = createEl('label', 'editLabel', 'scale = ');
        const scaleInput = createEl('input', 'editInput');
        scaleInput.type = 'input';
        scaleInput.value = targetValue.split(';')[1];
        scaleRow.appendChild(scaleLabel);
        scaleRow.appendChild(scaleInput);
        editDiv.appendChild(scaleRow);
        // rotate
        const rotateRow = createEl('div', 'etaniAppendRow');
        const rotateLabel = createEl('label', 'editLabel', 'rotate = ');
        const rotateInput = createEl('input', 'editInput');
        rotateInput.type = 'input';
        rotateInput.value = targetValue.split(';')[2];
        rotateRow.appendChild(rotateLabel);
        rotateRow.appendChild(rotateInput);
        editDiv.appendChild(rotateRow);

        etaniWindow(editDiv, () => {
            let combineValue = translateInput.value + ';' + 
                scaleInput.value + ';' + rotateInput.value;
            valueArray.splice(itemIndex, 1, combineValue);
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        });
    } else if (tagName === 'animateMotion') {
        const editDiv = createEl('div', 'etaniEditDiv');
        const editLabel = createEl('label', 'editLabel', 'path = ');
        const editInput = createEl('input', 'editInput');
        editInput.type = 'input';
        editInput.value = targetValue;
        editLabel.appendChild(editInput);
        editDiv.appendChild(editLabel);

        etaniWindow(editDiv, () => {
            setValues(tagName, targetAnimation, [editInput.value], etaniAV);
        });
    } else if (tagName === 'set') {
        const editDivTitle = createEl('div', 'etaniEditDiv');
        const editTitle = createEl('label', 'editLabel', 'set ' + animateType);
        const editDiv = createEl('div', 'etaniEditDiv');
        const editLabel = createEl('label', 'editLabel', 'to = ');
        const editInput = createEl('input', 'editInput');
        editInput.type = 'input';
        editInput.value = targetValue;
        editDivTitle.appendChild(editTitle);
        editLabel.appendChild(editInput);
        editDiv.appendChild(editDivTitle);
        editDiv.appendChild(editLabel);

        etaniWindow(editDiv, () => {
            setValues(tagName, targetAnimation, [editInput.value], etaniAV);
        });
    }
}

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

    // insert dynamic style
    addEtaniStyles();

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

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

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

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

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

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

帖子 ejsoon »

代码: 全选

以下是一段簡單的 JavaScript 程式碼,使用 `map()` 遍歷數組中的每一項,並將每項的 `id` 屬性提取出來存入一個新數組:

```javascript
// 假設有一個物件數組
const items = [
  { id: 1, name: 'Apple' },
  { id: 2, name: 'Banana' },
  { id: 3, name: 'Orange' }
];

// 提取所有 id 到新數組
const ids = items.map(item => item.id);

console.log(ids); // 輸出: [1, 2, 3]
```

### 說明:
- `items.map(item => item.id)`:遍歷 `items` 數組,每個 `item` 取出 `id` 屬性。
- 回傳一個新數組 `ids`,只包含 `id` 值。

---

### 如果你想用 `for...of` 寫法(更傳統):

```javascript
const items = [
  { id: 1, name: 'Apple' },
  { id: 2, name: 'Banana' },
  { id: 3, name: 'Orange' }
];

const ids = [];

for (const item of items) {
  ids.push(item.id);
}

console.log(ids); // [1, 2, 3]
```

兩種方式都可以,推薦使用 `map()`,因為簡潔且函數式風格更清晰。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4001
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 190 次
联系:

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

帖子 ejsoon »

代码: 全选

/**
 * 檢查字串是否可作為合法的 HTML id
 * 規則:
 * 1. 不能為空
 * 2. 不能包含空白字元
 * 3. 必須以字母開頭(a-z 或 A-Z)
 * 4. 後續只能包含:字母、數字、連字號 (-)、底線 (_)
 * 5. 不允許冒號 (:) 和句點 (.)
 *
 * @param {string} str - 要檢查的字串
 * @returns {boolean} - true 表示合法,false 表示不合法
 */
function isValidId(str) {
  // 1. 檢查是否為字串且不為空
  if (typeof str !== 'string' || str.trim() === '') {
    return false;
  }

  // 2. 檢查是否有空白字元(包含空格、換行、Tab 等)
  if (str.trim() !== str) {
    return false;
  }

  // 3. 必須以字母開頭
  if (!/^[a-zA-Z]/.test(str)) {
    return false;
  }

  // 4. 其餘字元只能是:字母、數字、-、_
  //    且明確排除 : 和 .
  if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(str)) {
    return false;
  }

  // 5. 額外檢查:明確不包含 : 或 .
  if (/[:.]/.test(str)) {
    return false;
  }

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

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

帖子 ejsoon »

已加入id的增加和編輯,以及updateAttr。

當前代碼:

代码: 全选

// 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;
  user-select: none;
}
.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 {
  position: relative;
  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;
}
.etaniAVItem.selected {
  background-color: #779933;
}
.etaniAVAdd {
  background-color: #a7fca7;
  border: 1px solid #71c371;
}
.etaniAVDelete {
  background-color: #ffcccc;
  border: 1px solid #cc3333;
}
.etaniAVCopy {
  background-color: #ccccff;
  border: 1px solid #6666cc;
}
.etaniAVMove {
  background-color: #ffcc99;
  border: 1px solid #cc9966;
}
.etaniAVDelete.active {
  background-color: #cc3333;
  color: white;
}
.etaniAVCopy.active {
  background-color: #6666cc;
  color: white;
}
.etaniAVMove.active {
  background-color: #cc9966;
  color: white;
}
.etaniAVCtrl > span > svg {
  margin-left: -1px;
  margin-top: -1px;
}
textarea.etaniHTMLTextarea {
  width: calc(100vw - 72px);
  height: calc(50vh - 24px);
  resize: none;
  border: 1px solid #ccc;
  font-size: 12px;
  box-sizing: border-box;
}
.etaniWindowRow {
  padding-top: 12px;
  text-align: center;
}
.etaniWindowRow button {
  margin: 0 12px;
}
.editDurInput {
  width: 36px;
}
.etaniEditDur {
  text-align: center;
}
.editInput {
  width: 120px;
}
.etaniAnimateAttrRemove {
  float: right;
  margin-right: 0 !important;
  border-color: #c33;
  color: #861616;
  background: #f6cdcd;
}
.etaniDropdown {
  position: absolute;
  left: 0px;
  background-color: #fff;
  border: 1px solid #ccc;
  box-shadow: 0 4px 8px rgba(0,0,0,0.1);
  z-index: 1200;
}
.etaniDropdownItem {
  color: black;
  padding: 8px 12px;
  cursor: pointer;
  font-size: 12px;
}
.etaniDropdownItem:hover {
  color: green;
  background-color: #f0f0f0;
}
.etaniIdListSpan {
  border: 1px solid #420664;
  padding: 2px 5px;
  font-size: 12px;
  color: #420664;
}
.etaniAnimateId {
  border-color: #420664;
  color: #420664;
}
    `;

    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.onclick = 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.onclick = 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.onclick = function() {
            // Remove active from all spans in etaniMode
            modeSpans.forEach(function(s) { s.classList.remove('active'); });
            // Add active to clicked span
            this.classList.add('active');
        };
    });

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        // Create etaniCtrl div
        addEtaniCtrlElements(etaniinner);

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

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

        etaniinner.appendChild(etaniResult);

        // Append etaniinner to etaniouter
        etaniouter.appendChild(etaniinner);

        // Change button text
        etaniStart.textContent = 'close ejtile animate';

        // list etani elements
        listEtaniItems();

        // update etani elements display by etaniFilter
        updateVisibility();

        // update result
        updateEtaniResult();
    }
}

// Generate base64 image for a tile
function generateTileImage(tileid) {
    const originalTile = document.querySelector(`#etmain > defs > g#${tileid}`);
    if (!originalTile) return null;

    const etdropUses = document.querySelectorAll('#etmain > .etdrop > use');
    const etwaitGroups = document.querySelectorAll('#etmain > .etwait g');
    
    let targetUse = null;
    for (const group of etwaitGroups) {
        const useInGroup = group.querySelector(`use[href="#${tileid}"]`);
        if (useInGroup) {
            targetUse = useInGroup;
            break;
        }
    }
    
    let etwaittransform = '', etwaitfill = '', etwaitstroke = '', etwaitstrokeWidth = '';
    if (targetUse) {
        etwaittransform = targetUse.getAttribute('transform') || '';
        etwaitfill = targetUse.getAttribute('fill') || '';
        etwaitstroke = targetUse.getAttribute('stroke') || '';
        etwaitstrokeWidth = targetUse.getAttribute('stroke-width') || '';

        const scaleMatch = etwaittransform.match(/scale\(([^)]+)\)/);
        const rotateMatch = etwaittransform.match(/rotate\(([^)]+)\)/);
        const scalePart = scaleMatch ? scaleMatch[0] : '';
        const rotatePart = rotateMatch ? rotateMatch[0] : '';
        etwaittransform = `translate(20,20) ${scalePart} ${rotatePart}`.trim().replace(/\s+/g, ' ');
    }
    
    const tileclone = originalTile.cloneNode(true);
    tileclone.removeAttribute('id');
    if (etwaittransform) tileclone.setAttribute('transform', etwaittransform);
    if (etwaitfill) tileclone.setAttribute('fill', etwaitfill);
    if (etwaitstroke) tileclone.setAttribute('stroke', etwaitstroke);
    if (etwaitstrokeWidth) tileclone.setAttribute('stroke-width', etwaitstrokeWidth);
    
    const svgWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svgWrapper.setAttribute('width', '40');
    svgWrapper.setAttribute('height', '40');
    svgWrapper.setAttribute('version', '1.1');
    svgWrapper.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
    svgWrapper.appendChild(tileclone);
    
    const svgString = new XMLSerializer().serializeToString(svgWrapper);
    return svgToBase64(svgString);
}

// generate hex color
function generateHexColor(seed) {
  let hash = 0;
  for (let i = 0; i < seed.length; i++) {
    hash = seed.charCodeAt(i) + ((hash << 5) - hash);
  }
  let color = (hash & 0x00FFFFFF).toString(16).toUpperCase();
  while (color.length < 6) {
    color = '0' + color;
  }
  return '#' + color;
}

// Handle the HTML popup window
function handleContentHTMLClick() {
    if (!etani) return;
    const textarea = createEl('textarea', 'etaniHTMLTextarea');
    textarea.value = new XMLSerializer().serializeToString(etani);
    etaniWindow(textarea, () => {
        const parser = new DOMParser();
        etani = parser.parseFromString(textarea.value, 'image/svg+xml');
        updateEtaniResult();
    });
}

// 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.onclick = function() {
            if (typeof confirm_function === 'function') {
                confirm_function();
            }
            windowDiv.remove();
        };
    }
    if (btnCancel) {
        btnCancel.onclick = function() {
            windowDiv.remove();
        };
    }
}

// Open append animate window
function etaniAppendAnimateWindow(id, dataType) {
    const appendAnimateWindow = createEl('div', 'appendAnimateWindow');
    appendAnimateWindow.dataset.id = id;
    appendAnimateWindow.dataset.type = dataType;
    // 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.onclick = etaniAppendAnimate;
    aMotion.onclick = etaniAppendAnimate;
    aOpacity.onclick = etaniAppendAnimate;
    aWriting.onclick = etaniAppendAnimate;
    aFill.onclick = etaniAppendAnimate;
    aStroke.onclick = etaniAppendAnimate;
    aWidth.onclick = etaniAppendAnimate;
    // For span.etaniAppendSet
    spanSet.onclick = 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;
    }

    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' };
        }
    }
    // if type is board, it must has the 'href' attribute
    let boardHref = {};
    if (elementtype === 'board') {
        boardHref = { href: `#${id}` };
    }

    // Special attribute
    let targetEl;
    if (elementtype === 'tile') {
        targetEl = etani.querySelector(`.etdrop > use[href="#${id}"]`);
    } else {
        targetEl = etani.getElementById(id);
    }
    // Writting animation
    if (animatetype === 'stroke-dasharray') {
        // get the writing length
        let tPath = etani.getElementById(id);
        tPath = elementtype === 'tile' ? tPath.firstChild : tPath;
        let targetLength = Math.round(tPath.getTotalLength());
        // set dashoffset
        targetEl.setAttribute('stroke-dashoffset', targetLength);
        // set defaultvalue
        defaultvalue = targetLength + ';' + (targetLength * 2);
    } else if (animatetype === 'fill') {
        // get the original color
        let targetElFill = targetEl.getAttribute('fill');
        if (targetElFill) defaultvalue = targetElFill;
    }

    // 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",
                ...boardHref,
                ...repeatOrFillAttrs // Add the repeat/fill logic
            };

            // Create and append <animateTransform> for translate
            const elTranslate = createSVGElement('animateTransform', {
                ...baseAttrs,
                dur: "1s",
                type: "translate",
                values: transformValues.translate
            });
            targetSVGParent.appendChild(elTranslate);

            // Create and append <animateTransform> for scale
            const elScale = createSVGElement('animateTransform', {
                ...baseAttrs,
                dur: "1s",
                type: "scale",
                values: transformValues.scale,
                additive: "sum"
            });
            targetSVGParent.appendChild(elScale);

            // Create and append <animateTransform> for rotate
            const elRotate = createSVGElement('animateTransform', {
                ...baseAttrs,
                dur: "1s",
                type: "rotate",
                values: transformValues.rotate,
                additive: "sum"
            });
            targetSVGParent.appendChild(elRotate);
            break;

        case 'animate':
            const elAnimateSVG = createSVGElement('animate', {
                attributeName: animatetype,
                dur: "1s",
                values: defaultvalue,
                ...boardHref,
                ...repeatOrFillAttrs
            });
            targetSVGParent.appendChild(elAnimateSVG);
            break;

        case 'animateMotion':
            const elMotion = createSVGElement('animateMotion', {
                dur: "1s",
                path: defaultvalue,
                ...boardHref,
                ...repeatOrFillAttrs
            });
            targetSVGParent.appendChild(elMotion);
            break;

        case 'set':
            const elSet = createSVGElement('set', {
                attributeName: animatetype,
                to: defaultvalue,
                dur: "1s",
                ...boardHref,
                ...repeatOrFillAttrs // Will be {fill: "freeze"} or {}
            });
            targetSVGParent.appendChild(elSet);
            break;
    }
}

// get target animate element by etaniAVCtrl button
function getTargetAnimation(valueBtn) {
    // --- Step 1: Get .etaniAnimate parent data ---
    const animateParent = valueBtn.closest('.etaniAnimate');
    if (!animateParent) {
        throw new Error('Could not find parent .etaniAnimate');
    }

    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;

    // Get the index of this element among its siblings
    const parentChildren = Array.from(animateParent.parentNode.children);
    const animateIndex = parentChildren.indexOf(animateParent);

    if (animateIndex === -1) {
         throw new Error('Could not determine animateIndex');
    }

    // --- Step 2: Get .etaniItem parent data ---
    const itemParent = valueBtn.closest('.etaniItem');
    if (!itemParent) {
        throw new Error('Could not find parent .etaniItem');
    }

    const dataId = itemParent.dataset.id;
    const dataType = itemParent.dataset.type;
    const hrefId = `#${dataId}`; // Prepare for attribute selector

    // --- Step 3: Find target animation elements ---
    const animationTags = 'animateTransform, animate, animateMotion, set';
    let animations = [];

    if (dataType === 'tile') {
        const useElement = etani.querySelector(`.etdrop > use[href="${hrefId}"]`);
        if (useElement) {
            // Find all animation elements inside the <use> tag
            animations = Array.from(useElement.querySelectorAll(animationTags));
        }
    } else if (dataType === 'board') {
        // Find all direct children of #etani matching the tags and href
        const selector = `:scope > animateTransform[href="${hrefId}"], 
                        :scope > animate[href="${hrefId}"], 
                        :scope > animateMotion[href="${hrefId}"], 
                        :scope > set[href="${hrefId}"]`;
        animations = Array.from(etani.querySelectorAll(selector));
    }

    // Group consecutive animateTransforms (3 at a time)
    const groupedAnimations = [];
    for (let i = 0; i < animations.length; ) {
        const currentAnim = animations[i];
        if (currentAnim.tagName === 'animateTransform') {
            // Assume 3 consecutive animateTransforms
            if (i + 2 < animations.length &&
                animations[i+1].tagName === 'animateTransform' &&
                animations[i+2].tagName === 'animateTransform') 
            {
                groupedAnimations.push([animations[i], animations[i+1], animations[i+2]]);
                i += 3;
            } else {
                // Handle incomplete groups: log warning and skip this element
                console.warn('Incomplete or non-consecutive animateTransform group found.', currentAnim);
                i++; // Skip this one to avoid infinite loop
            }
        } else {
            // Add other animation types as single items
            groupedAnimations.push(currentAnim);
            i++;
        }
    }

    // Get the specific target animation (group) using the index
    const targetAnimation = groupedAnimations[animateIndex];
    if (!targetAnimation) {
        throw new Error(`No animation element found at index ${animateIndex}`);
    }
    return targetAnimation;
}
/**
 * Set the values to the animation
 * @param {tagName} string - The values, combined by semicolons.
 * @param {targetAnimation} element - The animation elements.
 * @param {valueArray} array - The values, combined by semicolons.
 * @param {etaniAV} element - The values, combined by semicolons.
 */
function setValues(tagName, targetAnimation, valueArray, etaniAV) {
    if (tagName === 'animate') {
        targetAnimation.setAttribute('values', valueArray.join(';'));
    } else if (tagName === 'animateTransform') {
        // Do not append if element or value is invalid
        let translateValues = '', scaleValues = '', rotateValues = '';
        for (let i = 0; i < valueArray.length; i++) {
            translateValues += valueArray[i].split(';')[0];
            translateValues += i < valueArray.length - 1 ? ';' : '';
            scaleValues += valueArray[i].split(';')[1];
            scaleValues += i < valueArray.length - 1 ? ';' : '';
            rotateValues += valueArray[i].split(';')[2];
            rotateValues += i < valueArray.length - 1 ? ';' : '';
        }
        targetAnimation[0].setAttribute('values', translateValues)
        targetAnimation[1].setAttribute('values', scaleValues)
        targetAnimation[2].setAttribute('values', rotateValues)
    } else if (tagName === 'animateMotion') {
        let editInputValue = valueArray[0];
        if (targetAnimation.querySelector('mpath')) {
            targetAnimation.querySelector('mpath').remove();
        }
        if (editInputValue.substring(0, 1) === '#') {
            let mpath = createSVGElement('mpath', {href : editInputValue});
            targetAnimation.appendChild(mpath);
            if (targetAnimation.hasAttribute('path')) {
                targetAnimation.removeAttribute('path');
            }
        } else {
            targetAnimation.setAttribute('path', editInputValue);
        }
    } else if (tagName === 'set') {
        let editInputValue = valueArray[0];
        targetAnimation.setAttribute('to', valueArray[0]);
    }
    setUIAnimateValues(tagName, targetAnimation, etaniAV);
    updateEtaniResult();
}

/**
 * Appends a value to an element's 'values' attribute, separated by semicolons.
 * @param {Element} element - The animation element.
 * @param {string|number} value - The value to append.
 */
function addValue(element, value) {
    // Do not append if element or value is invalid
    if (!element || value === null || typeof value === 'undefined') return;
    
    let currentValues = element.getAttribute('values');
    const stringValue = String(value); // Ensure value is a string

    if (currentValues && currentValues.trim() !== '') {
        // Add with a semicolon if values already exist
        element.setAttribute('values', currentValues + ';' + stringValue);
    } else {
        // Set as the first value
        element.setAttribute('values', stringValue);
    }
}

/**
 * Extracts a specific transform function's value (e.g., "10 20" from "translate(10 20)")
 * @param {string} type - The transform type (e.g., 'translate', 'scale').
 * @param {string} transformString - The full transform attribute string.
 * @returns {string|null} The extracted value or null if not found.
 */
function getTransformValue(type, transformString) {
    if (!transformString) return null;
    // Regex to find the type and capture the content inside the parentheses
    const regex = new RegExp(`${type}\\(([^)]+)\\)`);
    const match = transformString.match(regex);
    return match ? match[1] : null; // Return the captured group (the values)
}

// Add animate value
function etaniAVAddClick() {
    // Change mode to default 'edit'
    const elAVCtrl = this.closest('.etaniAVCtrl');
    const active = elAVCtrl.querySelector('.active');
    if (active) active.classList.remove('active');
    const elAnimateValue = this.closest('.etaniAnimateValue');
    elAnimateValue.dataset.mode = 'edit';
    // --- Step 1-3: get targetAnimation element(s) ---
    let targetAnimation = getTargetAnimation(this);
    // --- Step 4: Insert values into target element ---
    let defaultValue; // This will be used for both Step 4 and 5
    const animateParent = this.closest('.etaniAnimate');
    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;
    const itemParent = this.closest('.etaniItem');
    const dataId = itemParent.dataset.id;
    const hrefId = `#${dataId}`; // Prepare for attribute selector
    if (tagName === 'animateTransform') {
        // Check if targetAnimation is valid (an array of 3)
        if (Array.isArray(targetAnimation) && targetAnimation.length === 3) {
            const mainUseElement = etmain.querySelector(`.etdrop > use[href="${hrefId}"]`);
            
            if (mainUseElement) {
                const transformString = mainUseElement.getAttribute('transform');
                
                // Extract values
                const translateVal = getTransformValue('translate', transformString);
                const scaleVal = getTransformValue('scale', transformString);
                const rotateVal = getTransformValue('rotate', transformString);

                // Add values to the 'values' attribute of each corresponding animation
                // Assuming order: [0] = translate, [1] = scale, [2] = rotate
                addValue(targetAnimation[0], translateVal);
                addValue(targetAnimation[1], scaleVal);
                addValue(targetAnimation[2], rotateVal);
            }
        } else {
             console.warn('Expected targetAnimation to be a group of 3, but it was not.', targetAnimation);
        }
    } else if (tagName === 'animate') {
        // Check if targetAnimation is a single element
        if (targetAnimation && !Array.isArray(targetAnimation)) {
            // Determine default value based on animateType
            if (animateType === 'opacity') {
                defaultValue = 1;
            } else if (animateType === 'stroke-width') {
                defaultValue = 2;
            } else if (animateType === 'fill') {
                defaultValue = '#f758b8';
            } else if (animateType === 'stroke') {
                defaultValue = '#7786ce';
            } else {
                defaultValue = 0;
            }
            
            // Add to values attribute
            addValue(targetAnimation, defaultValue);
        } else {
            console.warn('Expected targetAnimation to be a single element, but it was not.', targetAnimation);
        }
    }

    // --- Step 5: Add <span> to .etaniAV ---
    const avParent = this.closest('.etaniAnimateValue');
    const etaniAV = avParent.querySelector('.etaniAV');
    setUIAnimateValues(tagName, targetAnimation, etaniAV);
    updateEtaniResult();
}

// append etaniAnimate to etaniItemRight
function appendAnimateToItemRight(id, elementtype, tagname, animatetype, defaultvalue = null) {
    // Find the UI target parent element (in the .etaniinner UI panel)
    const targetUIParent = document.querySelector(`.etaniinner .etaniItem[data-id="${id}"] > .etaniItemRight`);
    
    if (!targetUIParent) {
        console.error(`Error: UI target (.etaniinner .etaniItem[data-id="${id}"] > .etaniItemRight) not found.`);
        return; 
    }

    // Build the UI structure
    const elAnimate = createEl('div', 'etaniAnimate');
    elAnimate.dataset.tagname = tagname;
    elAnimate.dataset.animatetype = animatetype;
    
    // 1. Build .etaniAnimateAttr
    const elAnimateAttr = createEl('div', 'etaniAnimateAttr');
    const elAnimateName = createEl('span', 'etaniAnimateName');
    const elAnimateDur = createEl('span', 'etaniAnimateDur');
    elAnimateDur.onclick = editDurAttribute;

    const elAnimateAttrAdd = createEl('span', 'etaniAnimateAttrAdd', '+');
    elAnimateAttrAdd.onclick = elAnimateAttrAddClick;
    const elAnimateAttrRemove = createEl('span', 'etaniAnimateAttrRemove', '×');
    elAnimateAttrRemove.onclick = elAnimateAttrRemoveClick;
    
    elAnimateAttr.appendChild(elAnimateAttrRemove);
    elAnimateAttr.appendChild(elAnimateName);
    elAnimateAttr.appendChild(elAnimateDur);
    elAnimateAttr.appendChild(elAnimateAttrAdd);

    // 2. Build .etaniAnimateValue
    const elAnimateValue = createEl('div', 'etaniAnimateValue');
    elAnimateValue.dataset.mode = 'edit';
    const elAVLabel = createEl('span', 'etaniAVLabel');
    const elAV = createEl('div', 'etaniAV');
    const elAVItem = createEl('span', 'etaniAVItem');
    elAVItem.onclick = etaniAVItemClick;
    
    elAV.appendChild(elAVItem);

    // create controls for this type
    const elAVCtrl = createEl('div', 'etaniAVCtrl');
    const etaniAVAdd = createEl('span', 'etaniAVAdd');
    etaniAVAdd.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>';
    const etaniAVDelete = createEl('span', 'etaniAVDelete');
    etaniAVDelete.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>';
    const etaniAVCopy = createEl('span', 'etaniAVCopy');
    etaniAVCopy.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
    const etaniAVMove = createEl('span', 'etaniAVMove');
    etaniAVMove.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M 7,4 L 3,8 L 7,12 M 3,8 L 18,8 M 17,12 L 21,16 L 17,20 M 21,16 L 6,16"></path></svg>';
    elAVCtrl.appendChild(etaniAVAdd);
    elAVCtrl.appendChild(etaniAVDelete);
    elAVCtrl.appendChild(etaniAVCopy);
    elAVCtrl.appendChild(etaniAVMove);
    etaniAVAdd.onclick = etaniAVAddClick;

    const toggleButtons = {
        delete: { element: etaniAVDelete, mode: 'delete' },
        copy: { element: etaniAVCopy, mode: 'copy' },
        move: { element: etaniAVMove, mode: 'move' }
    };

    Object.values(toggleButtons).forEach(({ element, mode }) => {
        element.onclick = 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;
            }
        };
    });

    // cname
    let aName;
    if (animatetype === 'stroke-width') {
        aName = 'width';
    } else if (animatetype === 'stroke-dasharray') {
        aName = 'writing';
        let tPath = etani.getElementById(id);
        tPath = elementtype === 'tile' ? tPath.firstChild : tPath;
        defaultvalue = Math.round(tPath.getTotalLength());
    } else {
        aName = animatetype;
    }

    // Special attribute and convert animateName
    let targetEl;
    if (elementtype === 'tile') {
        targetEl = etani.querySelector(`.etdrop > use[href="#${id}"]`);
    } else {
        targetEl = etani.getElementById(id);
    }
    if (animatetype === 'fill') {
        // get the original color
        let targetElFill = targetEl.getAttribute('fill');
        if (targetElFill) defaultvalue = targetElFill;
    }

    // 3. Apply logic based on tagname
    switch (tagname) {
        case 'animateTransform':
            elAnimateName.textContent = 'transform';
            elAnimateDur.textContent = 'dur = 1s';
            elAVLabel.textContent = 'values = ';
            elAVItem.textContent = 'a'; // Default placeholder
            elAnimateValue.appendChild(elAVCtrl);
            break;
            
        case 'animate':
            elAnimateName.textContent = aName;
            elAnimateDur.textContent = 'dur = 1s';
            elAVLabel.textContent = 'values = ';
            elAVItem.textContent = defaultvalue;
            elAnimateValue.appendChild(elAVCtrl);
            break;

        case 'animateMotion':
            elAnimateName.textContent = 'motion';
            elAnimateDur.textContent = 'dur = 1s';
            elAVLabel.textContent = 'move to = ';
            elAVItem.textContent = defaultvalue;
            // No controls for this type
            break;

        case 'set':
            elAnimateName.textContent = aName;
            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);

    // if writing
    if (animatetype === 'stroke-dasharray') {
        elAV.appendChild(createEl('span', 'etaniAVItem', defaultvalue * 2));
    }
}

// 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() {
    // let id, elementtype, tagname, animatetype, defaultvalue;
    let appendAnimateWindow = this.closest('.appendAnimateWindow');
    let id = appendAnimateWindow.dataset.id;
    let elementtype = appendAnimateWindow.dataset.type;
    let tagname = 'animate', animatetype = '', defaultvalue = 0;
    if (this.innerHTML === 'transform') {
        tagname = 'animateTransform';
        animatetype = 'transform';
    } else if (this.innerHTML === 'motion') {
        tagname = 'animateMotion';
        defaultvalue = 'M 0,0 H 120 V 120 Z';
    } else if (this.innerHTML === 'opacity') {
        animatetype = 'opacity';
        defaultvalue = 1;
    } else if (this.innerHTML === 'writing') {
        animatetype = 'stroke-dasharray';
        defaultvalue = 0;
    } else if (this.innerHTML === 'fill') {
        animatetype = 'fill';
        defaultvalue = '#f758b8';
    } else if (this.innerHTML === 'stroke') {
        animatetype = 'stroke';
        defaultvalue = '#7786ce';
    } else if (this.innerHTML === 'width') {
        animatetype = 'stroke-width';
        defaultvalue = 2;
    }
    let setSpan = appendAnimateWindow.querySelector('.etaniAppendSet');
    if (setSpan.classList.contains('active')) tagname = 'set';
    appendAnimateToResult(id, elementtype, tagname, animatetype, defaultvalue);
    appendAnimateToItemRight(id, elementtype, tagname, animatetype, defaultvalue);
    updateEtaniResult();
    if (document.querySelector('.etaniWindow')) {
        document.querySelector('.etaniWindow').remove();
    }
}

/**
 * Populates a UI element with spans representing animation values,
 * based on the type of animation tag.
 *
 * @param {string} tagname - The tag name of the animation element 
 * (e.g., 'animateTransform', 'animate', 'animateMotion', 'set').
 * @param {Element|Element[]} targetAnimation - The target animation element(s).
 * @param {Element} valuesElement - The container element to append value spans to.
 */
function setUIAnimateValues(tagname, targetAnimation, valuesElement) {
    // Clear the target container first
    valuesElement.innerHTML = '';

    switch (tagname) {
        case 'animateTransform':
            // targetAnimation is an array [translate, scale, rotate]
            if (!Array.isArray(targetAnimation) || targetAnimation.length < 3) {
                console.error('animateTransform expects an array of 3 elements.');
                return;
            }

            // Get values from all three transform elements
            const translateVals = (targetAnimation[0]?.getAttribute('values') || '').split(';');
            const scaleVals = (targetAnimation[1]?.getAttribute('values') || '').split(';');
            const rotateVals = (targetAnimation[2]?.getAttribute('values') || '').split(';');

            // Assume all arrays have the same length, based on the first one
            const valuesLength = translateVals.length;
            if (valuesLength === 0 || scaleVals.length !== valuesLength || rotateVals.length !== valuesLength) {
                console.warn('animateTransform value arrays have mismatched lengths or are empty.');
                // Continue anyway, but might produce incomplete results
            }

            const combinedValues = [];
            for (let i = 0; i < valuesLength; i++) {
                // Combine corresponding values with ';'
                const combined = `${translateVals[i] || ''};${scaleVals[i] || ''};${rotateVals[i] || ''}`;
                combinedValues.push(combined);
            }

            // Map unique combined values to representative letters (a-z, A-Z)
            const valueMap = new Map();
            const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
            let charIndex = 0;
            const representativeLetters = [];

            for (const value of combinedValues) {
                let letter = valueMap.get(value);
                if (!letter) {
                    // Assign a new letter if this value hasn't been seen
                    if (charIndex < alphabet.length) {
                        letter = alphabet[charIndex];
                        charIndex++;
                    } else {
                        // Fallback if we run out of letters (more than 52 unique steps)
                        letter = `?${charIndex - alphabet.length + 1}`;
                    }
                    valueMap.set(value, letter);
                }
                representativeLetters.push(letter);
            }

            // Create and append spans for each representative letter
            for (const letter of representativeLetters) {
                const newSpan = createEl('span', 'etaniAVItem', letter);
                newSpan.onclick = etaniAVItemClick;
                valuesElement.appendChild(newSpan);
            }
            break;

        case 'animate':
            // targetAnimation is a single Element
            const values = targetAnimation.getAttribute('values');
            if (values) {
                const valueArray = values.split(';');
                // Create a span for each value
                for (const val of valueArray) {
                    if (val.trim() !== '') { // Avoid creating spans for empty values (e.g., from "a;;b")
                        const newSpan = createEl('span', 'etaniAVItem', val);
                        newSpan.onclick = etaniAVItemClick;
                        valuesElement.appendChild(newSpan);
                    }
                }
            }
            break;

        case 'animateMotion':
            // targetAnimation is a single Element
            const pathValue = targetAnimation.getAttribute('path');
            let motionValue = '';

            if (pathValue) {
                // Use 'path' attribute if it exists
                motionValue = pathValue;
            } else {
                // Otherwise, find the <mpath> element and use its 'href'
                const mpathElement = targetAnimation.querySelector('mpath');
                if (mpathElement) {
                    motionValue = mpathElement.getAttribute('href') || '';
                }
            }
            
            const motionSpan = createEl('span', 'etaniAVItem', motionValue);
            motionSpan.onclick = etaniAVItemClick;
            valuesElement.appendChild(motionSpan);
            break;

        case 'set':
            // targetAnimation is a single Element
            const toValue = targetAnimation.getAttribute('to') || '';
            
            // Create a span for the 'to' attribute value
            const setSpan = createEl('span', 'etaniAVItem', toValue);
            setSpan.onclick = etaniAVItemClick;
            valuesElement.appendChild(setSpan);
            break;

        default:
            // Handle unknown tagname
            console.warn(`Unhandled animation tag: ${tagname}`);
    }
}

// helper function, get all animate element that has id
function getAllId() {
    // Collect animate elements
    const cssSelector = `
        .etdrop > use > animate[id],
        .etdrop > use > animateTransform[id],
        .etdrop > use > animateMotion[id],
        .etdrop > use > set[id],
        :scope > animate[id],
        :scope > animateTransform[id],
        :scope > animateMotion[id],
        :scope > set[id]
    `;
    let hasIdEle = etani.querySelectorAll(cssSelector);
    return Array.from(hasIdEle).map(item => item.id);
}

// helper function, get animate name from animate element
function getAnimateName(element) {
    let targetAni = getTargetAnimation(element);
    if (Array.isArray(targetAni)) targetAni = targetAni[0];
    const attrName = targetAni.getAttribute('attributeName');
    let aName;
    if (attrName === 'stroke-width') {
        aName = 'width';
    } else if (attrName === 'stroke-dasharray') {
        aName = 'writing';
    } else {
        aName = attrName;
    }
    return aName;
}

// update animate UI attr
function updateAttr(element) {
    let targetAni = getTargetAnimation(element);
    if (Array.isArray(targetAni)) targetAni = targetAni[0];
    const etaniItem = element.closest('.etaniItem');
    const etaniOldAttr = etaniItem.querySelector('.etaniAnimateAttr');
    // Build .etaniAnimateAttr
    const elAnimateAttr = createEl('div', 'etaniAnimateAttr');
    // Build remove button
    const elAnimateAttrRemove = createEl('span', 'etaniAnimateAttrRemove', '×');
    elAnimateAttrRemove.onclick = elAnimateAttrRemoveClick;
    elAnimateAttr.appendChild(elAnimateAttrRemove);
    // Build Animate Name
    const aName = getAnimateName(element)
    const elAnimateName = createEl('span', 'etaniAnimateName', aName);
    elAnimateAttr.appendChild(elAnimateName);
    // Build id attribute
    if (targetAni.hasAttribute('id')) {
        const aId = 'id = ' + targetAni.id;
        const elAnimateId = createEl('span', 'etaniAnimateId', aId);
        // elAnimateId.onclick = editIdAttribute;
        elAnimateAttr.appendChild(elAnimateId);
    }
    // Build dur attribute
    const aDur = 'dur = ' + targetAni.getAttribute('dur') + 's';
    const elAnimateDur = createEl('span', 'etaniAnimateDur', aDur);
    elAnimateDur.onclick = editDurAttribute;
    elAnimateAttr.appendChild(elAnimateDur);
    // Build add button
    const elAnimateAttrAdd = createEl('span', 'etaniAnimateAttrAdd', '+');
    elAnimateAttrAdd.onclick = elAnimateAttrAddClick;
    elAnimateAttr.appendChild(elAnimateAttrAdd);
    etaniOldAttr.replaceWith(elAnimateAttr);
}

// check the id is valid
function isValidId(str) {
  if (typeof str !== 'string' || str.trim() === '') return false;
  if (str.trim() !== str) return false;
  if (!/^[a-zA-Z]/.test(str)) return false;
  if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(str)) return false;
  if (/[:.]/.test(str)) return false;
  return true;
}

// Add attribute to the target animation element
function elAnimateAttrAddClick(e) {
    // handle dropdown menu
    if (this.querySelector('.etaniDropdown')) {
        if (e.target.innerHTML === 'id') {
            // input id
            const editDiv = createEl('div', 'etaniEditDiv');
            const editLabel = createEl('label', 'editLabel', 'id = ');
            const editInput = createEl('input', 'editInput');
            editInput.type = 'input';
            editLabel.appendChild(editInput);
            editDiv.appendChild(editLabel);
            editDiv.appendChild(createEl('hr'));
            // id list
            const idListDiv = createEl('div', 'etaniEditDiv');
            const idListLabel = createEl('label', 'editLabel', 'id list:');
            const idListInner = createEl('div', 'etaniEditDiv');
            let allId = getAllId();
            for (let item of allId) {
                const spanSet = createEl('span', 'etaniIdListSpan', item);
                idListInner.appendChild(spanSet);
            }
            idListDiv.appendChild(idListLabel);
            idListDiv.appendChild(idListInner);
            editDiv.appendChild(idListDiv);
            etaniWindow(editDiv, () => {
                // check id input
                if (!isValidId(editInput.value)) return;
                let targetAni = getTargetAnimation(this);
                if (Array.isArray(targetAni)) targetAni = targetAni[0];
                targetAni.id = editInput.value;
                updateAttr(this);
            });
        } else if (e.target.innerHTML === 'begin') {
            console.log('add begin');
        } else if (e.target.innerHTML === 'other') {
            console.log('add other');
        }
        this.querySelector('.etaniDropdown').remove();
        return;
    }
    // open dropdown menu
    const etaniDropdown = createEl('div', 'etaniDropdown');
    etaniDropdown.style.top = this.getBoundingClientRect().height + 'px';
    let targetAni = getTargetAnimation(this);
    if (Array.isArray(targetAni)) targetAni = targetAni[0];
    const attrObj = Array.from(targetAni.attributes).reduce((obj, attr) => {
        obj[attr.name] = attr.value;
        return obj;
    }, {});
    const animationProps = Object.keys(attrObj);
    if (!animationProps.includes('id')) {
        const itemid = createEl('div', 'etaniDropdownItem', 'id');
        etaniDropdown.appendChild(itemid);
    }
    if (!animationProps.includes('begin')) {
        const itemBegin = createEl('div', 'etaniDropdownItem', 'begin');
        etaniDropdown.appendChild(itemBegin);
    }
    const itemOther = createEl('div', 'etaniDropdownItem', 'other');
    etaniDropdown.appendChild(itemOther);
    this.appendChild(etaniDropdown);
}

// remove the target animation element
function elAnimateAttrRemoveClick() {
    getTargetAnimation(this).remove();
    this.closest('.etaniAnimate').remove();
    updateEtaniResult();
}

// edit Animate Attribute
function editDurAttribute() {
    const targetAnimation = getTargetAnimation(this);
    let editDurValue;
    if (Array.isArray(targetAnimation)) {
        editDurValue = targetAnimation[0].getAttribute('dur');
    } else {
        editDurValue = targetAnimation.getAttribute('dur');
    }
    editDurValue = editDurValue.replace('s', '');

    const editDur = createEl('div', 'etaniEditDur');
    const editDurLabel = createEl('label', 'editDurLabel', 'dur = ');
    const editDurInput = createEl('input', 'editDurInput');
    editDurInput.type = 'text';
    editDurInput.value = editDurValue;
    const editDurSpan = createEl('span', 'editDurSpan', ' s');
    editDurLabel.appendChild(editDurInput);
    editDurLabel.appendChild(editDurSpan);
    editDur.appendChild(editDurLabel);
    etaniWindow(editDur, () => {
        let setDurValue = document.querySelector('.editDurInput').value;
        setDurValue = setDurValue + 's';
        if (Array.isArray(targetAnimation)) {
            targetAnimation[0].setAttribute('dur', setDurValue);
            targetAnimation[1].setAttribute('dur', setDurValue);
            targetAnimation[2].setAttribute('dur', setDurValue);
        } else {
            targetAnimation.setAttribute('dur', setDurValue);
        }
        this.textContent = 'dur = ' + setDurValue;
        updateEtaniResult();
    });
}

// get values from targetAnimation
function getValues(targetAnimation) {
    if (!Array.isArray(targetAnimation)) {
        if (targetAnimation.tagName === 'animate') {
            return targetAnimation.getAttribute('values').split(';');
        } else if (targetAnimation.tagName === 'animateMotion') {
            if (targetAnimation.hasAttribute('path')) {
                return [targetAnimation.getAttribute('path')];
            } else if (targetAnimation.querySelector('mpath')) {
                return [targetAnimation.querySelector('mpath').getAttribute('href')];
            }
        } else if (targetAnimation.tagName === 'set') {
            return [targetAnimation.getAttribute('to')];
        }
    }
    // Get values from all three transform elements
    const translateVals = (targetAnimation[0]?.getAttribute('values') || '').split(';');
    const scaleVals = (targetAnimation[1]?.getAttribute('values') || '').split(';');
    const rotateVals = (targetAnimation[2]?.getAttribute('values') || '').split(';');

    // Assume all arrays have the same length, based on the first one
    const valuesLength = translateVals.length;
    if (valuesLength === 0 || scaleVals.length !== valuesLength || rotateVals.length !== valuesLength) {
        console.warn('animateTransform value arrays have mismatched lengths or are empty.');
    }

    const combinedValues = [];
    for (let i = 0; i < valuesLength; i++) {
        // Combine corresponding values with ';'
        const combined = `${translateVals[i] || ''};${scaleVals[i] || ''};${rotateVals[i] || ''}`;
        combinedValues.push(combined);
    }
    return combinedValues;
}

// Values item click event
function etaniAVItemClick () {
    // get standard data
    const targetAnimation = getTargetAnimation(this);
    const animateValue = this.closest('.etaniAnimateValue');
    const ctrlMode = animateValue.dataset.mode;
    const animateParent = this.closest('.etaniAnimate');
    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;
    const dataId = this.closest('.etaniItem').dataset.id;
    const dataType = this.closest('.etaniItem').dataset.type;
    const etaniAV = animateParent.querySelector('.etaniAV');
    // Get the index of this element among its siblings
    const itemParent = this.parentNode;
    const itemIndex = Array.from(itemParent.children).indexOf(this);
    const valueArray = getValues(targetAnimation);
    const targetValue = valueArray[itemIndex];

    if (ctrlMode === 'delete') {
        if (valueArray.length > 1) {
            valueArray.splice(itemIndex, 1);
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        }
    } else if (ctrlMode === 'copy') {
        valueArray.splice(itemIndex, 0, targetValue);
        setValues(tagName, targetAnimation, valueArray, etaniAV);
    } else if (ctrlMode === 'move') {
        let sItem = itemParent.querySelector('.selected');
        if (this === sItem) {
            this.classList.remove('selected');
        } else if (sItem) {
            const sIndex = Array.from(itemParent.children).indexOf(sItem);
            const sValue = valueArray[sIndex];
            if (sIndex < itemIndex) {
                if (this.nextSibling) {
                    itemParent.insertBefore(sItem, this.nextSibling);
                } else {
                    itemParent.appendChild(sItem);
                }
                valueArray.splice(itemIndex + 1, 0, sValue);
                valueArray.splice(sIndex, 1);
            } else {
                itemParent.insertBefore(sItem, this);
                valueArray.splice(sIndex, 1);
                valueArray.splice(itemIndex, 0, sValue);
            }
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        } else {
            this.classList.add('selected');
        }
    } else if (tagName === 'animate') {
        const editDiv = createEl('div', 'etaniEditDiv');
        const editLabel = createEl('label', 'editLabel', animateType + ' = ');
        const editInput = createEl('input', 'editInput');
        editInput.type = 'input';
        editInput.value = targetValue;
        editLabel.appendChild(editInput);
        editDiv.appendChild(editLabel);
        etaniWindow(editDiv, () => {
            valueArray.splice(itemIndex, 1, editInput.value);
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        });
    } else if (tagName === 'animateTransform') {
        const editDiv = createEl('div', 'etaniEditDiv');
        // translate
        const translateRow = createEl('div', 'etaniAppendRow');
        const translateLabel = createEl('label', 'editLabel', 'translate = ');
        const translateInput = createEl('input', 'editInput');
        translateInput.type = 'input';
        translateInput.value = targetValue.split(';')[0];
        translateRow.appendChild(translateLabel);
        translateRow.appendChild(translateInput);
        editDiv.appendChild(translateRow);
        // scale
        const scaleRow = createEl('div', 'etaniAppendRow');
        const scaleLabel = createEl('label', 'editLabel', 'scale = ');
        const scaleInput = createEl('input', 'editInput');
        scaleInput.type = 'input';
        scaleInput.value = targetValue.split(';')[1];
        scaleRow.appendChild(scaleLabel);
        scaleRow.appendChild(scaleInput);
        editDiv.appendChild(scaleRow);
        // rotate
        const rotateRow = createEl('div', 'etaniAppendRow');
        const rotateLabel = createEl('label', 'editLabel', 'rotate = ');
        const rotateInput = createEl('input', 'editInput');
        rotateInput.type = 'input';
        rotateInput.value = targetValue.split(';')[2];
        rotateRow.appendChild(rotateLabel);
        rotateRow.appendChild(rotateInput);
        editDiv.appendChild(rotateRow);

        etaniWindow(editDiv, () => {
            let combineValue = translateInput.value + ';' + 
                scaleInput.value + ';' + rotateInput.value;
            valueArray.splice(itemIndex, 1, combineValue);
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        });
    } else if (tagName === 'animateMotion') {
        const editDiv = createEl('div', 'etaniEditDiv');
        const editLabel = createEl('label', 'editLabel', 'path = ');
        const editInput = createEl('input', 'editInput');
        editInput.type = 'input';
        editInput.value = targetValue;
        editLabel.appendChild(editInput);
        editDiv.appendChild(editLabel);

        etaniWindow(editDiv, () => {
            setValues(tagName, targetAnimation, [editInput.value], etaniAV);
        });
    } else if (tagName === 'set') {
        const editDivTitle = createEl('div', 'etaniEditDiv');
        const editTitle = createEl('label', 'editLabel', 'set ' + animateType);
        const editDiv = createEl('div', 'etaniEditDiv');
        const editLabel = createEl('label', 'editLabel', 'to = ');
        const editInput = createEl('input', 'editInput');
        editInput.type = 'input';
        editInput.value = targetValue;
        editDivTitle.appendChild(editTitle);
        editLabel.appendChild(editInput);
        editDiv.appendChild(editDivTitle);
        editDiv.appendChild(editLabel);

        etaniWindow(editDiv, () => {
            setValues(tagName, targetAnimation, [editInput.value], etaniAV);
        });
    }
}

// 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.onclick = etaniStartClick;
});
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4001
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 190 次
联系:

Re: 正在做etani,ejtile的動畫套件

帖子 ejsoon »

當前代碼:

代码: 全选

// Global variable to hold the SVG clone and ensure state is maintained
let etani = null;
let intervalMoving = null;

// Add dynamic CSS styles to the document
function addEtaniStyles() {
    if (document.getElementById('dynamic-et-styles')) {
        return;
    }

    const styleSheet = 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;
  user-select: none;
}
.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 {
  position: relative;
  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;
}
.etaniAVItem.selected {
  background-color: #779933;
}
.etaniAVAdd {
  background-color: #a7fca7;
  border: 1px solid #71c371;
}
.etaniAVDelete {
  background-color: #ffcccc;
  border: 1px solid #cc3333;
}
.etaniAVCopy {
  background-color: #ccccff;
  border: 1px solid #6666cc;
}
.etaniAVMove {
  background-color: #ffcc99;
  border: 1px solid #cc9966;
}
.etaniAVDelete.active {
  background-color: #cc3333;
  color: white;
}
.etaniAVCopy.active {
  background-color: #6666cc;
  color: white;
}
.etaniAVMove.active {
  background-color: #cc9966;
  color: white;
}
.etaniAVCtrl > span > svg {
  margin-left: -1px;
  margin-top: -1px;
}
textarea.etaniHTMLTextarea {
  width: calc(100vw - 72px);
  height: calc(50vh - 24px);
  resize: none;
  border: 1px solid #ccc;
  font-size: 12px;
  box-sizing: border-box;
}
.etaniWindowRow {
  padding-top: 12px;
  text-align: center;
}
.etaniWindowRow button {
  margin: 0 12px;
}
.editDurInput {
  width: 36px;
}
.etaniEditDur {
  text-align: center;
}
.editInput {
  width: 120px;
}
.etaniAnimateAttrRemove {
  float: right;
  margin-right: 0 !important;
  border-color: #c33;
  color: #861616;
  background: #f6cdcd;
}
.etaniDropdown {
  position: absolute;
  left: 0px;
  background-color: #fff;
  border: 1px solid #ccc;
  box-shadow: 0 4px 8px rgba(0,0,0,0.1);
  z-index: 1200;
}
.etaniDropdownItem {
  color: black;
  padding: 8px 12px;
  cursor: pointer;
  font-size: 12px;
}
.etaniDropdownItem:hover {
  color: green;
  background-color: #f0f0f0;
}
.etaniIdListSpan {
  border: 1px solid #420664;
  padding: 2px 5px;
  font-size: 12px;
  color: #420664;
  margin: 3px;
}
.etaniIdListSpan.selected {
  background: #420664;
  color: white;
}
.etaniIdListDiv {
  max-width: 360px;
}
.etaniAnimateId {
  border-color: #420664;
  color: #420664;
}
.etaniAttrRemove {
  cursor: pointer;
  padding: 2px 5px;
  font-size: 12px;
  box-sizing: border-box;
  border-width: 1px;
  border-style: solid;
  float: right;
  margin-right: 0 !important;
  border-color: #c33;
  color: #861616;
  background: #f6cdcd;
}
.editBeginStartInput {
  width: 36px;
}
.beginOption {
  border: 1px solid #777;
  background-color: #eee;
  color: #333;
  display: inline-block;
  text-decoration: none;
  padding: 2px 8px;
  font-size: 14px;
  margin: 0 2px;
  cursor: pointer;
  user-select: none;
}
.beginOption.selected {
  background-color: #008CBA;
  color: white;
  border-color: #008CBA;
}
.endOrBeginDiv {
  margin-top: 7px;
}
    `;

    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.onclick = 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.onclick = 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.onclick = function() {
            // Remove active from all spans in etaniMode
            modeSpans.forEach(function(s) { s.classList.remove('active'); });
            // Add active to clicked span
            this.classList.add('active');
        };
    });

    // Create etaniAllAppend div
    const etaniAllAppend = createEl('div', 'etaniAllAppend');

    // Create etaniAllAppendTransform a
    const etaniAllAppendTransform = createEl('a', 'etaniAllAppendTransform');
    etaniAllAppendTransform.href = 'javascript:;';
    etaniAllAppendTransform.textContent = 'transform';
    etaniAllAppend.appendChild(etaniAllAppendTransform);

    // Create etaniAllAppendOpacity a
    const etaniAllAppendOpacity = createEl('a', 'etaniAllAppendOpacity');
    etaniAllAppendOpacity.href = 'javascript:;';
    etaniAllAppendOpacity.textContent = 'opacity';
    etaniAllAppend.appendChild(etaniAllAppendOpacity);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniAllAppend);

    // Create etaniValue div
    const etaniValue = createEl('div', 'etaniValue');

    // Create etaniValueIncrease a
    const etaniValueIncrease = createEl('a', 'etaniValueIncrease');
    etaniValueIncrease.href = 'javascript:;';
    etaniValueIncrease.textContent = 'increase';
    etaniValue.appendChild(etaniValueIncrease);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniValue);
}

// Define updateMovingTiles
function updateMovingTiles() {
    const movingUse = document.querySelector('#etmain > .etdrop > use.tilemoving');
    const items = document.querySelectorAll('.etaniItem');
    items.forEach(function(item) {
        item.style.display = 'none';
    });
    if (movingUse) {
        const id = movingUse.getAttribute('href').slice(1);
        const item = document.querySelector('.etaniItem[data-id="' + id + '"]');
        if (item) {
            item.style.display = 'block';
        }
    }
}

// list Etani elements
function listEtaniItems() {
    // Get the etaniCol element
    let etaniCol = document.querySelector('.etaniCol');
    if (!etaniCol) return; // Exit if etaniCol not found

    // Clear existing content in etaniCol
    etaniCol.innerHTML = '';

    // Collect elements into etaniElementArray
    let uses = etani.querySelectorAll('.etdrop > use');
    let etanidrop = etani.getElementById('etanidrop');
    let boardElements = etani.querySelectorAll('.etdrop > .etboard [id]');
    let etaniElementArray = [...uses, etanidrop, ...boardElements];

    // Loop through etaniElementArray to create etaniItem elements
    etaniElementArray.forEach(function(element) {
        if (!element) return; // Skip if element is null

        // Determine itemId and dataType
        let itemId;
        let dataType;
        if (element.tagName === 'use') {
            itemId = element.getAttribute('href').slice(1);
            dataType = 'tile';
        } else if (element.id === 'etanidrop') {
            itemId = 'etanidrop';
            dataType = 'board';
        } else {
            itemId = element.id;
            dataType = 'board';
        }

        // Create etaniItem div
        const etaniItem = createEl('div', 'etaniItem');
        etaniItem.dataset.id = itemId;
        etaniItem.dataset.type = dataType;

        // Create etaniItemLeft div
        const etaniItemLeft = createEl('div', 'etaniItemLeft');
        etaniItem.appendChild(etaniItemLeft);

        // Create etaniItemImageOuter div
        const etaniItemImageOuter = createEl('div', 'etaniItemImageOuter');
        etaniItemLeft.appendChild(etaniItemImageOuter);

        if (dataType === 'tile') {
            // Create etaniItemImage img
            const etaniItemImage = createEl('img', 'etaniItemImage');
            etaniItemImage.src = generateTileImage(itemId);
            etaniItemImageOuter.appendChild(etaniItemImage);
        } else {
            // Generate background color
            etaniItemImageOuter.style.background = generateHexColor(itemId);
        }

        // Create etaniItemId div
        const etaniItemId = createEl('div', 'etaniItemId');
        etaniItemId.onclick = function() {
            etaniAppendAnimateWindow(itemId, dataType);
        };
        etaniItemLeft.appendChild(etaniItemId);

        // Create etaniItemName span
        const itemName = itemId === 'etanidrop' ? 'board' : itemId;
        const etaniItemName = createEl('span', 'etaniItemName', itemName);
        etaniItemId.appendChild(etaniItemName);

        // Create etaniItemPlus img
        const etaniItemPlus = createEl('span', 'etaniItemPlus');
        etaniItemPlus.innerHTML = '<svg width="12" height="12" fill="none" stroke-width="1" stroke="darkgreen"><line x1="6" y1="0" x2="6" y2="12"></line><line x1="0" y1="6" x2="12" y2="6"></line></svg>';
        etaniItemId.appendChild(etaniItemPlus);

        // Create etaniItemRight div
        const etaniItemRight = createEl('div', 'etaniItemRight');
        etaniItem.appendChild(etaniItemRight);

        // Append etaniItem to etaniCol
        etaniCol.appendChild(etaniItem);
    });
}

// Define updateVisibility
function updateVisibility() {
    const active = document.querySelector('.etaniFilter .active');
    if (!active) return;

    const activeClass = active.classList[0]; // e.g., 'etaniFilterTiles'
    const items = document.querySelectorAll('.etaniItem');
    items.forEach(function(item) {
        item.style.display = 'none';
    });

    if (activeClass === 'etaniFilterTiles') {
        document.querySelectorAll('.etaniItem[data-type="tile"]').forEach(function(item) {
            item.style.display = 'block';
        });
    } else if (activeClass === 'etaniFilterBoard') {
        document.querySelectorAll('.etaniItem[data-type="board"]').forEach(function(item) {
            item.style.display = 'block';
        });
    } else if (activeClass === 'etaniFilterMoving') {
        document.querySelectorAll('.etaniItem[data-type="board"]').forEach(function(item) {
            item.style.display = 'block';
        });
        // Tiles visibility handled by interval
    }
}

// Convert SVG string to a Base64 data URL
function svgToBase64(svgString) {
    const base64 = btoa(unescape(encodeURIComponent(svgString)));
    return `data:image/svg+xml;base64,${base64}`;
}

// Define start button click
function etaniStartClick() {
    const etaniouter = document.querySelector('.etaniouter');

    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etani = originalSvg.cloneNode(true);
    const etwaitElement = etani.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    etani.id = 'etani';
    const etdropClone = etani.querySelector('.etdrop');
    if (etdropClone) {
        etdropClone.id = 'etanidrop';
    }

    // Check if etaniinner already exists
    let etaniinner = etaniouter.querySelector('.etaniinner');
    if (etaniinner) {
        // Remove etaniinner
        etaniouter.removeChild(etaniinner);
        // Restore button text
        etaniStart.textContent = 'start ejtile animate';
    } else {
        // Create etaniinner div
        etaniinner = createEl('div', 'etaniinner');

        // Create etaniCtrl div
        addEtaniCtrlElements(etaniinner);

        // Create etaniCol div
        const etaniCol = createEl('div', 'etaniCol');
        etaniinner.appendChild(etaniCol);

        // Create etaniResult div
        const etaniResult = createEl('div', 'etaniResult');

        etaniinner.appendChild(etaniResult);

        // Append etaniinner to etaniouter
        etaniouter.appendChild(etaniinner);

        // Change button text
        etaniStart.textContent = 'close ejtile animate';

        // list etani elements
        listEtaniItems();

        // update etani elements display by etaniFilter
        updateVisibility();

        // update result
        updateEtaniResult();
    }
}

// Generate base64 image for a tile
function generateTileImage(tileid) {
    const originalTile = document.querySelector(`#etmain > defs > g#${tileid}`);
    if (!originalTile) return null;

    const etdropUses = document.querySelectorAll('#etmain > .etdrop > use');
    const etwaitGroups = document.querySelectorAll('#etmain > .etwait g');
    
    let targetUse = null;
    for (const group of etwaitGroups) {
        const useInGroup = group.querySelector(`use[href="#${tileid}"]`);
        if (useInGroup) {
            targetUse = useInGroup;
            break;
        }
    }
    
    let etwaittransform = '', etwaitfill = '', etwaitstroke = '', etwaitstrokeWidth = '';
    if (targetUse) {
        etwaittransform = targetUse.getAttribute('transform') || '';
        etwaitfill = targetUse.getAttribute('fill') || '';
        etwaitstroke = targetUse.getAttribute('stroke') || '';
        etwaitstrokeWidth = targetUse.getAttribute('stroke-width') || '';

        const scaleMatch = etwaittransform.match(/scale\(([^)]+)\)/);
        const rotateMatch = etwaittransform.match(/rotate\(([^)]+)\)/);
        const scalePart = scaleMatch ? scaleMatch[0] : '';
        const rotatePart = rotateMatch ? rotateMatch[0] : '';
        etwaittransform = `translate(20,20) ${scalePart} ${rotatePart}`.trim().replace(/\s+/g, ' ');
    }
    
    const tileclone = originalTile.cloneNode(true);
    tileclone.removeAttribute('id');
    if (etwaittransform) tileclone.setAttribute('transform', etwaittransform);
    if (etwaitfill) tileclone.setAttribute('fill', etwaitfill);
    if (etwaitstroke) tileclone.setAttribute('stroke', etwaitstroke);
    if (etwaitstrokeWidth) tileclone.setAttribute('stroke-width', etwaitstrokeWidth);
    
    const svgWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svgWrapper.setAttribute('width', '40');
    svgWrapper.setAttribute('height', '40');
    svgWrapper.setAttribute('version', '1.1');
    svgWrapper.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
    svgWrapper.appendChild(tileclone);
    
    const svgString = new XMLSerializer().serializeToString(svgWrapper);
    return svgToBase64(svgString);
}

// generate hex color
function generateHexColor(seed) {
  let hash = 0;
  for (let i = 0; i < seed.length; i++) {
    hash = seed.charCodeAt(i) + ((hash << 5) - hash);
  }
  let color = (hash & 0x00FFFFFF).toString(16).toUpperCase();
  while (color.length < 6) {
    color = '0' + color;
  }
  return '#' + color;
}

// Handle the HTML popup window
function handleContentHTMLClick() {
    if (!etani) return;
    const textarea = createEl('textarea', 'etaniHTMLTextarea');
    textarea.value = new XMLSerializer().serializeToString(etani);
    etaniWindow(textarea, () => {
        const parser = new DOMParser();
        etani = parser.parseFromString(textarea.value, 'image/svg+xml');
        updateEtaniResult();
    });
}

// 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.onclick = function() {
            if (typeof confirm_function === 'function') {
                confirm_function();
            }
            windowDiv.remove();
        };
    }
    if (btnCancel) {
        btnCancel.onclick = function() {
            windowDiv.remove();
        };
    }
}

// Open append animate window
function etaniAppendAnimateWindow(id, dataType) {
    const appendAnimateWindow = createEl('div', 'appendAnimateWindow');
    appendAnimateWindow.dataset.id = id;
    appendAnimateWindow.dataset.type = dataType;
    // 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.onclick = etaniAppendAnimate;
    aMotion.onclick = etaniAppendAnimate;
    aOpacity.onclick = etaniAppendAnimate;
    aWriting.onclick = etaniAppendAnimate;
    aFill.onclick = etaniAppendAnimate;
    aStroke.onclick = etaniAppendAnimate;
    aWidth.onclick = etaniAppendAnimate;
    // For span.etaniAppendSet
    spanSet.onclick = 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;
    }

    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' };
        }
    }
    // if type is board, it must has the 'href' attribute
    let boardHref = {};
    if (elementtype === 'board') {
        boardHref = { href: `#${id}` };
    }

    // Special attribute
    let targetEl;
    if (elementtype === 'tile') {
        targetEl = etani.querySelector(`.etdrop > use[href="#${id}"]`);
    } else {
        targetEl = etani.getElementById(id);
    }
    // Writting animation
    if (animatetype === 'stroke-dasharray') {
        // get the writing length
        let tPath = etani.getElementById(id);
        tPath = elementtype === 'tile' ? tPath.firstChild : tPath;
        let targetLength = Math.round(tPath.getTotalLength());
        // set dashoffset
        targetEl.setAttribute('stroke-dashoffset', targetLength);
        // set defaultvalue
        defaultvalue = targetLength + ';' + (targetLength * 2);
    } else if (animatetype === 'fill') {
        // get the original color
        let targetElFill = targetEl.getAttribute('fill');
        if (targetElFill) defaultvalue = targetElFill;
    }

    // 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",
                ...boardHref,
                ...repeatOrFillAttrs // Add the repeat/fill logic
            };

            // Create and append <animateTransform> for translate
            const elTranslate = createSVGElement('animateTransform', {
                ...baseAttrs,
                dur: "1s",
                type: "translate",
                values: transformValues.translate
            });
            targetSVGParent.appendChild(elTranslate);

            // Create and append <animateTransform> for scale
            const elScale = createSVGElement('animateTransform', {
                ...baseAttrs,
                dur: "1s",
                type: "scale",
                values: transformValues.scale,
                additive: "sum"
            });
            targetSVGParent.appendChild(elScale);

            // Create and append <animateTransform> for rotate
            const elRotate = createSVGElement('animateTransform', {
                ...baseAttrs,
                dur: "1s",
                type: "rotate",
                values: transformValues.rotate,
                additive: "sum"
            });
            targetSVGParent.appendChild(elRotate);
            break;

        case 'animate':
            const elAnimateSVG = createSVGElement('animate', {
                attributeName: animatetype,
                dur: "1s",
                values: defaultvalue,
                ...boardHref,
                ...repeatOrFillAttrs
            });
            targetSVGParent.appendChild(elAnimateSVG);
            break;

        case 'animateMotion':
            const elMotion = createSVGElement('animateMotion', {
                dur: "1s",
                path: defaultvalue,
                ...boardHref,
                ...repeatOrFillAttrs
            });
            targetSVGParent.appendChild(elMotion);
            break;

        case 'set':
            const elSet = createSVGElement('set', {
                attributeName: animatetype,
                to: defaultvalue,
                dur: "1s",
                ...boardHref,
                ...repeatOrFillAttrs // Will be {fill: "freeze"} or {}
            });
            targetSVGParent.appendChild(elSet);
            break;
    }
}

// get target animate element by etaniAVCtrl button
function getTargetAnimation(valueBtn) {
    // --- Step 1: Get .etaniAnimate parent data ---
    const animateParent = valueBtn.closest('.etaniAnimate');
    if (!animateParent) {
        throw new Error('Could not find parent .etaniAnimate');
    }

    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;

    // Get the index of this element among its siblings
    const parentChildren = Array.from(animateParent.parentNode.children);
    const animateIndex = parentChildren.indexOf(animateParent);

    if (animateIndex === -1) {
         throw new Error('Could not determine animateIndex');
    }

    // --- Step 2: Get .etaniItem parent data ---
    const itemParent = valueBtn.closest('.etaniItem');
    if (!itemParent) {
        throw new Error('Could not find parent .etaniItem');
    }

    const dataId = itemParent.dataset.id;
    const dataType = itemParent.dataset.type;
    const hrefId = `#${dataId}`; // Prepare for attribute selector

    // --- Step 3: Find target animation elements ---
    const animationTags = 'animateTransform, animate, animateMotion, set';
    let animations = [];

    if (dataType === 'tile') {
        const useElement = etani.querySelector(`.etdrop > use[href="${hrefId}"]`);
        if (useElement) {
            // Find all animation elements inside the <use> tag
            animations = Array.from(useElement.querySelectorAll(animationTags));
        }
    } else if (dataType === 'board') {
        // Find all direct children of #etani matching the tags and href
        const selector = `:scope > animateTransform[href="${hrefId}"], 
                        :scope > animate[href="${hrefId}"], 
                        :scope > animateMotion[href="${hrefId}"], 
                        :scope > set[href="${hrefId}"]`;
        animations = Array.from(etani.querySelectorAll(selector));
    }

    // Group consecutive animateTransforms (3 at a time)
    const groupedAnimations = [];
    for (let i = 0; i < animations.length; ) {
        const currentAnim = animations[i];
        if (currentAnim.tagName === 'animateTransform') {
            // Assume 3 consecutive animateTransforms
            if (i + 2 < animations.length &&
                animations[i+1].tagName === 'animateTransform' &&
                animations[i+2].tagName === 'animateTransform') 
            {
                groupedAnimations.push([animations[i], animations[i+1], animations[i+2]]);
                i += 3;
            } else {
                // Handle incomplete groups: log warning and skip this element
                console.warn('Incomplete or non-consecutive animateTransform group found.', currentAnim);
                i++; // Skip this one to avoid infinite loop
            }
        } else {
            // Add other animation types as single items
            groupedAnimations.push(currentAnim);
            i++;
        }
    }

    // Get the specific target animation (group) using the index
    const targetAnimation = groupedAnimations[animateIndex];
    if (!targetAnimation) {
        throw new Error(`No animation element found at index ${animateIndex}`);
    }
    return targetAnimation;
}
/**
 * Set the values to the animation
 * @param {tagName} string - The values, combined by semicolons.
 * @param {targetAnimation} element - The animation elements.
 * @param {valueArray} array - The values, combined by semicolons.
 * @param {etaniAV} element - The values, combined by semicolons.
 */
function setValues(tagName, targetAnimation, valueArray, etaniAV) {
    if (tagName === 'animate') {
        targetAnimation.setAttribute('values', valueArray.join(';'));
    } else if (tagName === 'animateTransform') {
        // Do not append if element or value is invalid
        let translateValues = '', scaleValues = '', rotateValues = '';
        for (let i = 0; i < valueArray.length; i++) {
            translateValues += valueArray[i].split(';')[0];
            translateValues += i < valueArray.length - 1 ? ';' : '';
            scaleValues += valueArray[i].split(';')[1];
            scaleValues += i < valueArray.length - 1 ? ';' : '';
            rotateValues += valueArray[i].split(';')[2];
            rotateValues += i < valueArray.length - 1 ? ';' : '';
        }
        targetAnimation[0].setAttribute('values', translateValues)
        targetAnimation[1].setAttribute('values', scaleValues)
        targetAnimation[2].setAttribute('values', rotateValues)
    } else if (tagName === 'animateMotion') {
        let editInputValue = valueArray[0];
        if (targetAnimation.querySelector('mpath')) {
            targetAnimation.querySelector('mpath').remove();
        }
        if (editInputValue.substring(0, 1) === '#') {
            let mpath = createSVGElement('mpath', {href : editInputValue});
            targetAnimation.appendChild(mpath);
            if (targetAnimation.hasAttribute('path')) {
                targetAnimation.removeAttribute('path');
            }
        } else {
            targetAnimation.setAttribute('path', editInputValue);
        }
    } else if (tagName === 'set') {
        let editInputValue = valueArray[0];
        targetAnimation.setAttribute('to', valueArray[0]);
    }
    setUIAnimateValues(tagName, targetAnimation, etaniAV);
    updateEtaniResult();
}

/**
 * Appends a value to an element's 'values' attribute, separated by semicolons.
 * @param {Element} element - The animation element.
 * @param {string|number} value - The value to append.
 */
function addValue(element, value) {
    // Do not append if element or value is invalid
    if (!element || value === null || typeof value === 'undefined') return;
    
    let currentValues = element.getAttribute('values');
    const stringValue = String(value); // Ensure value is a string

    if (currentValues && currentValues.trim() !== '') {
        // Add with a semicolon if values already exist
        element.setAttribute('values', currentValues + ';' + stringValue);
    } else {
        // Set as the first value
        element.setAttribute('values', stringValue);
    }
}

/**
 * Extracts a specific transform function's value (e.g., "10 20" from "translate(10 20)")
 * @param {string} type - The transform type (e.g., 'translate', 'scale').
 * @param {string} transformString - The full transform attribute string.
 * @returns {string|null} The extracted value or null if not found.
 */
function getTransformValue(type, transformString) {
    if (!transformString) return null;
    // Regex to find the type and capture the content inside the parentheses
    const regex = new RegExp(`${type}\\(([^)]+)\\)`);
    const match = transformString.match(regex);
    return match ? match[1] : null; // Return the captured group (the values)
}

// Add animate value
function etaniAVAddClick() {
    // Change mode to default 'edit'
    const elAVCtrl = this.closest('.etaniAVCtrl');
    const active = elAVCtrl.querySelector('.active');
    if (active) active.classList.remove('active');
    const elAnimateValue = this.closest('.etaniAnimateValue');
    elAnimateValue.dataset.mode = 'edit';
    // --- Step 1-3: get targetAnimation element(s) ---
    let targetAnimation = getTargetAnimation(this);
    // --- Step 4: Insert values into target element ---
    let defaultValue; // This will be used for both Step 4 and 5
    const animateParent = this.closest('.etaniAnimate');
    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;
    const itemParent = this.closest('.etaniItem');
    const dataId = itemParent.dataset.id;
    const hrefId = `#${dataId}`; // Prepare for attribute selector
    if (tagName === 'animateTransform') {
        // Check if targetAnimation is valid (an array of 3)
        if (Array.isArray(targetAnimation) && targetAnimation.length === 3) {
            const mainUseElement = etmain.querySelector(`.etdrop > use[href="${hrefId}"]`);
            
            if (mainUseElement) {
                const transformString = mainUseElement.getAttribute('transform');
                
                // Extract values
                const translateVal = getTransformValue('translate', transformString);
                const scaleVal = getTransformValue('scale', transformString);
                const rotateVal = getTransformValue('rotate', transformString);

                // Add values to the 'values' attribute of each corresponding animation
                // Assuming order: [0] = translate, [1] = scale, [2] = rotate
                addValue(targetAnimation[0], translateVal);
                addValue(targetAnimation[1], scaleVal);
                addValue(targetAnimation[2], rotateVal);
            }
        } else {
             console.warn('Expected targetAnimation to be a group of 3, but it was not.', targetAnimation);
        }
    } else if (tagName === 'animate') {
        // Check if targetAnimation is a single element
        if (targetAnimation && !Array.isArray(targetAnimation)) {
            // Determine default value based on animateType
            if (animateType === 'opacity') {
                defaultValue = 1;
            } else if (animateType === 'stroke-width') {
                defaultValue = 2;
            } else if (animateType === 'fill') {
                defaultValue = '#f758b8';
            } else if (animateType === 'stroke') {
                defaultValue = '#7786ce';
            } else {
                defaultValue = 0;
            }
            
            // Add to values attribute
            addValue(targetAnimation, defaultValue);
        } else {
            console.warn('Expected targetAnimation to be a single element, but it was not.', targetAnimation);
        }
    }

    // --- Step 5: Add <span> to .etaniAV ---
    const avParent = this.closest('.etaniAnimateValue');
    const etaniAV = avParent.querySelector('.etaniAV');
    setUIAnimateValues(tagName, targetAnimation, etaniAV);
    updateEtaniResult();
}

// append etaniAnimate to etaniItemRight
function appendAnimateToItemRight(id, elementtype, tagname, animatetype, defaultvalue = null) {
    // Find the UI target parent element (in the .etaniinner UI panel)
    const targetUIParent = document.querySelector(`.etaniinner .etaniItem[data-id="${id}"] > .etaniItemRight`);
    
    if (!targetUIParent) {
        console.error(`Error: UI target (.etaniinner .etaniItem[data-id="${id}"] > .etaniItemRight) not found.`);
        return; 
    }

    // Build the UI structure
    const elAnimate = createEl('div', 'etaniAnimate');
    elAnimate.dataset.tagname = tagname;
    elAnimate.dataset.animatetype = animatetype;
    
    // 1. Build .etaniAnimateAttr
    const elAnimateAttr = createEl('div', 'etaniAnimateAttr');
    const elAnimateName = createEl('span', 'etaniAnimateName');
    const elAnimateDur = createEl('span', 'etaniAnimateDur');
    elAnimateDur.onclick = editDurAttribute;

    const elAnimateAttrAdd = createEl('span', 'etaniAnimateAttrAdd', '+');
    elAnimateAttrAdd.onclick = elAnimateAttrAddClick;
    const elAnimateAttrRemove = createEl('span', 'etaniAnimateAttrRemove', '×');
    elAnimateAttrRemove.onclick = elAnimateAttrRemoveClick;
    
    elAnimateAttr.appendChild(elAnimateAttrRemove);
    elAnimateAttr.appendChild(elAnimateName);
    elAnimateAttr.appendChild(elAnimateDur);
    elAnimateAttr.appendChild(elAnimateAttrAdd);

    // 2. Build .etaniAnimateValue
    const elAnimateValue = createEl('div', 'etaniAnimateValue');
    elAnimateValue.dataset.mode = 'edit';
    const elAVLabel = createEl('span', 'etaniAVLabel');
    const elAV = createEl('div', 'etaniAV');
    const elAVItem = createEl('span', 'etaniAVItem');
    elAVItem.onclick = etaniAVItemClick;
    
    elAV.appendChild(elAVItem);

    // create controls for this type
    const elAVCtrl = createEl('div', 'etaniAVCtrl');
    const etaniAVAdd = createEl('span', 'etaniAVAdd');
    etaniAVAdd.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>';
    const etaniAVDelete = createEl('span', 'etaniAVDelete');
    etaniAVDelete.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>';
    const etaniAVCopy = createEl('span', 'etaniAVCopy');
    etaniAVCopy.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
    const etaniAVMove = createEl('span', 'etaniAVMove');
    etaniAVMove.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M 7,4 L 3,8 L 7,12 M 3,8 L 18,8 M 17,12 L 21,16 L 17,20 M 21,16 L 6,16"></path></svg>';
    elAVCtrl.appendChild(etaniAVAdd);
    elAVCtrl.appendChild(etaniAVDelete);
    elAVCtrl.appendChild(etaniAVCopy);
    elAVCtrl.appendChild(etaniAVMove);
    etaniAVAdd.onclick = etaniAVAddClick;

    const toggleButtons = {
        delete: { element: etaniAVDelete, mode: 'delete' },
        copy: { element: etaniAVCopy, mode: 'copy' },
        move: { element: etaniAVMove, mode: 'move' }
    };

    Object.values(toggleButtons).forEach(({ element, mode }) => {
        element.onclick = 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;
            }
        };
    });

    // cname
    let aName;
    if (animatetype === 'stroke-width') {
        aName = 'width';
    } else if (animatetype === 'stroke-dasharray') {
        aName = 'writing';
        let tPath = etani.getElementById(id);
        tPath = elementtype === 'tile' ? tPath.firstChild : tPath;
        defaultvalue = Math.round(tPath.getTotalLength());
    } else {
        aName = animatetype;
    }

    // Special attribute and convert animateName
    let targetEl;
    if (elementtype === 'tile') {
        targetEl = etani.querySelector(`.etdrop > use[href="#${id}"]`);
    } else {
        targetEl = etani.getElementById(id);
    }
    if (animatetype === 'fill') {
        // get the original color
        let targetElFill = targetEl.getAttribute('fill');
        if (targetElFill) defaultvalue = targetElFill;
    }

    // 3. Apply logic based on tagname
    switch (tagname) {
        case 'animateTransform':
            elAnimateName.textContent = 'transform';
            elAnimateDur.textContent = 'dur = 1s';
            elAVLabel.textContent = 'values = ';
            elAVItem.textContent = 'a'; // Default placeholder
            elAnimateValue.appendChild(elAVCtrl);
            break;
            
        case 'animate':
            elAnimateName.textContent = aName;
            elAnimateDur.textContent = 'dur = 1s';
            elAVLabel.textContent = 'values = ';
            elAVItem.textContent = defaultvalue;
            elAnimateValue.appendChild(elAVCtrl);
            break;

        case 'animateMotion':
            elAnimateName.textContent = 'motion';
            elAnimateDur.textContent = 'dur = 1s';
            elAVLabel.textContent = 'move to = ';
            elAVItem.textContent = defaultvalue;
            // No controls for this type
            break;

        case 'set':
            elAnimateName.textContent = aName;
            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);

    // if writing
    if (animatetype === 'stroke-dasharray') {
        elAV.appendChild(createEl('span', 'etaniAVItem', defaultvalue * 2));
    }
}

// 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() {
    // let id, elementtype, tagname, animatetype, defaultvalue;
    let appendAnimateWindow = this.closest('.appendAnimateWindow');
    let id = appendAnimateWindow.dataset.id;
    let elementtype = appendAnimateWindow.dataset.type;
    let tagname = 'animate', animatetype = '', defaultvalue = 0;
    if (this.innerHTML === 'transform') {
        tagname = 'animateTransform';
        animatetype = 'transform';
    } else if (this.innerHTML === 'motion') {
        tagname = 'animateMotion';
        defaultvalue = 'M 0,0 H 120 V 120 Z';
    } else if (this.innerHTML === 'opacity') {
        animatetype = 'opacity';
        defaultvalue = 1;
    } else if (this.innerHTML === 'writing') {
        animatetype = 'stroke-dasharray';
        defaultvalue = 0;
    } else if (this.innerHTML === 'fill') {
        animatetype = 'fill';
        defaultvalue = '#f758b8';
    } else if (this.innerHTML === 'stroke') {
        animatetype = 'stroke';
        defaultvalue = '#7786ce';
    } else if (this.innerHTML === 'width') {
        animatetype = 'stroke-width';
        defaultvalue = 2;
    }
    let setSpan = appendAnimateWindow.querySelector('.etaniAppendSet');
    if (setSpan.classList.contains('active')) tagname = 'set';
    appendAnimateToResult(id, elementtype, tagname, animatetype, defaultvalue);
    appendAnimateToItemRight(id, elementtype, tagname, animatetype, defaultvalue);
    updateEtaniResult();
    if (document.querySelector('.etaniWindow')) {
        document.querySelector('.etaniWindow').remove();
    }
}

/**
 * Populates a UI element with spans representing animation values,
 * based on the type of animation tag.
 *
 * @param {string} tagname - The tag name of the animation element 
 * (e.g., 'animateTransform', 'animate', 'animateMotion', 'set').
 * @param {Element|Element[]} targetAnimation - The target animation element(s).
 * @param {Element} valuesElement - The container element to append value spans to.
 */
function setUIAnimateValues(tagname, targetAnimation, valuesElement) {
    // Clear the target container first
    valuesElement.innerHTML = '';

    switch (tagname) {
        case 'animateTransform':
            // targetAnimation is an array [translate, scale, rotate]
            if (!Array.isArray(targetAnimation) || targetAnimation.length < 3) {
                console.error('animateTransform expects an array of 3 elements.');
                return;
            }

            // Get values from all three transform elements
            const translateVals = (targetAnimation[0]?.getAttribute('values') || '').split(';');
            const scaleVals = (targetAnimation[1]?.getAttribute('values') || '').split(';');
            const rotateVals = (targetAnimation[2]?.getAttribute('values') || '').split(';');

            // Assume all arrays have the same length, based on the first one
            const valuesLength = translateVals.length;
            if (valuesLength === 0 || scaleVals.length !== valuesLength || rotateVals.length !== valuesLength) {
                console.warn('animateTransform value arrays have mismatched lengths or are empty.');
                // Continue anyway, but might produce incomplete results
            }

            const combinedValues = [];
            for (let i = 0; i < valuesLength; i++) {
                // Combine corresponding values with ';'
                const combined = `${translateVals[i] || ''};${scaleVals[i] || ''};${rotateVals[i] || ''}`;
                combinedValues.push(combined);
            }

            // Map unique combined values to representative letters (a-z, A-Z)
            const valueMap = new Map();
            const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
            let charIndex = 0;
            const representativeLetters = [];

            for (const value of combinedValues) {
                let letter = valueMap.get(value);
                if (!letter) {
                    // Assign a new letter if this value hasn't been seen
                    if (charIndex < alphabet.length) {
                        letter = alphabet[charIndex];
                        charIndex++;
                    } else {
                        // Fallback if we run out of letters (more than 52 unique steps)
                        letter = `?${charIndex - alphabet.length + 1}`;
                    }
                    valueMap.set(value, letter);
                }
                representativeLetters.push(letter);
            }

            // Create and append spans for each representative letter
            for (const letter of representativeLetters) {
                const newSpan = createEl('span', 'etaniAVItem', letter);
                newSpan.onclick = etaniAVItemClick;
                valuesElement.appendChild(newSpan);
            }
            break;

        case 'animate':
            // targetAnimation is a single Element
            const values = targetAnimation.getAttribute('values');
            if (values) {
                const valueArray = values.split(';');
                // Create a span for each value
                for (const val of valueArray) {
                    if (val.trim() !== '') { // Avoid creating spans for empty values (e.g., from "a;;b")
                        const newSpan = createEl('span', 'etaniAVItem', val);
                        newSpan.onclick = etaniAVItemClick;
                        valuesElement.appendChild(newSpan);
                    }
                }
            }
            break;

        case 'animateMotion':
            // targetAnimation is a single Element
            const pathValue = targetAnimation.getAttribute('path');
            let motionValue = '';

            if (pathValue) {
                // Use 'path' attribute if it exists
                motionValue = pathValue;
            } else {
                // Otherwise, find the <mpath> element and use its 'href'
                const mpathElement = targetAnimation.querySelector('mpath');
                if (mpathElement) {
                    motionValue = mpathElement.getAttribute('href') || '';
                }
            }
            
            const motionSpan = createEl('span', 'etaniAVItem', motionValue);
            motionSpan.onclick = etaniAVItemClick;
            valuesElement.appendChild(motionSpan);
            break;

        case 'set':
            // targetAnimation is a single Element
            const toValue = targetAnimation.getAttribute('to') || '';
            
            // Create a span for the 'to' attribute value
            const setSpan = createEl('span', 'etaniAVItem', toValue);
            setSpan.onclick = etaniAVItemClick;
            valuesElement.appendChild(setSpan);
            break;

        default:
            // Handle unknown tagname
            console.warn(`Unhandled animation tag: ${tagname}`);
    }
}

// helper function, get all animate element that has id
function getAllId() {
    // Collect animate elements
    const cssSelector = `
        .etdrop > use > animate[id],
        .etdrop > use > animateTransform[id],
        .etdrop > use > animateMotion[id],
        .etdrop > use > set[id],
        :scope > animate[id],
        :scope > animateTransform[id],
        :scope > animateMotion[id],
        :scope > set[id]
    `;
    let hasIdEle = etani.querySelectorAll(cssSelector);
    return Array.from(hasIdEle).map(item => item.id);
}

// helper function, get animate name from animate element
function getAnimateName(element) {
    let targetAni = getTargetAnimation(element);
    if (Array.isArray(targetAni)) targetAni = targetAni[0];
    const attrName = targetAni.getAttribute('attributeName');
    let aName;
    if (attrName === 'stroke-width') {
        aName = 'width';
    } else if (attrName === 'stroke-dasharray') {
        aName = 'writing';
    } else {
        aName = attrName;
    }
    return aName;
}

// edit begin attribute
function editBeginAttribute() {
    // get target animate element
    let targetAni = getTargetAnimation(this);
    if (Array.isArray(targetAni)) targetAni = targetAni[0];
    // input id
    const editDiv = createEl('div', 'etaniEditDiv');
    const editLabel = createEl('label', 'editLabel', 'begin = ');
    const editInput = createEl('input', 'editBeginStartInput');
    editInput.type = 'input';
    if (targetAni.hasAttribute('begin')) {
        editInput.value = targetAni.getAttribute('begin');
        const editRemove = createEl('span', 'etaniAttrRemove', '×');
        editRemove.onclick = () => {
            document.querySelector('.etaniWindow').remove();
            targetAni.removeAttribute('begin');
            updateAttr(this);
        };
        editDiv.appendChild(editRemove);
    }
    editLabel.appendChild(editInput);
    const editBeginStartSpan = createEl('span', 'editBeginSpan', ' s;');
    editLabel.appendChild(editBeginStartSpan);
    editDiv.appendChild(editLabel);
    // id list
    const idListDiv = createEl('div', 'etaniIdListDiv');
    const idListInner = createEl('div', 'etaniEditDiv');
    let allId = getAllId();
    for (let item of allId) {
        const spanIdItem = createEl('span', 'etaniIdListSpan', item);
        // TODO if (item === targetAni.id) spanIdItem.classList.add('selected');
        idListInner.appendChild(spanIdItem);
    }
    idListDiv.appendChild(idListInner);
    editDiv.appendChild(idListDiv);
    // end or begin, plus
    const endOrBeginDiv = createEl('div', 'endOrBeginDiv');
    const optionEnd = createEl('span', 'beginOption', 'end');
    optionEnd.classList.add('selected');
    const optionBegin = createEl('span', 'beginOption', 'begin');
    const beginPlus = createEl('span', 'beginPlusSpan', ' + ');
    const beginPlusInput = createEl('input', 'editBeginStartInput');
    beginPlusInput.type = 'input';
    const beginPlusUnit = createEl('span', 'beginPlusUnit', ' s');
    editInput.type = 'input';
    endOrBeginDiv.appendChild(optionEnd);
    endOrBeginDiv.appendChild(optionBegin);
    endOrBeginDiv.appendChild(beginPlus);
    endOrBeginDiv.appendChild(beginPlusInput);
    endOrBeginDiv.appendChild(beginPlusUnit);
    editDiv.appendChild(endOrBeginDiv);
    etaniWindow(editDiv, () => {
        updateAttr(this);
    });
}

// edit id attribute
function editIdAttribute() {
    // get target animate element
    let targetAni = getTargetAnimation(this);
    if (Array.isArray(targetAni)) targetAni = targetAni[0];
    // input id
    const editDiv = createEl('div', 'etaniEditDiv');
    const editLabel = createEl('label', 'editLabel', 'id = ');
    const editInput = createEl('input', 'editInput');
    editInput.type = 'input';
    if (targetAni.hasAttribute('id')) {
        editInput.value = targetAni.id;
        const editRemove = createEl('span', 'etaniAttrRemove', '×');
        editRemove.onclick = () => {
            document.querySelector('.etaniWindow').remove();
            targetAni.removeAttribute('id');
            updateAttr(this);
        };
        editDiv.appendChild(editRemove);
    }
    editLabel.appendChild(editInput);
    editDiv.appendChild(editLabel);
    editDiv.appendChild(createEl('hr'));
    // id list
    const idListDiv = createEl('div', 'etaniIdListDiv');
    const idListLabel = createEl('label', 'editLabel', 'id list:');
    const idListInner = createEl('div', 'etaniEditDiv');
    let allId = getAllId();
    for (let item of allId) {
        const spanIdItem = createEl('span', 'etaniIdListSpan', item);
        if (item === targetAni.id) spanIdItem.classList.add('selected');
        idListInner.appendChild(spanIdItem);
    }
    idListDiv.appendChild(idListLabel);
    idListDiv.appendChild(idListInner);
    editDiv.appendChild(idListDiv);
    etaniWindow(editDiv, () => {
        // check id input
        if (!isValidId(editInput.value)) {
            alert('Not a valid id!');
            return;
        }
        if (allId.includes(editInput.value)) {
            alert('Dumplicate id!');
            return;
        }
        targetAni.id = editInput.value;
        updateAttr(this);
    });
}

// update animate UI attr
function updateAttr(element) {
    let targetAni = getTargetAnimation(element);
    if (Array.isArray(targetAni)) targetAni = targetAni[0];
    const etaniAnimate = element.closest('.etaniAnimate');
    const etaniOldAttr = etaniAnimate.querySelector('.etaniAnimateAttr');
    // Build .etaniAnimateAttr
    const elAnimateAttr = createEl('div', 'etaniAnimateAttr');
    // Build remove button
    const elAnimateAttrRemove = createEl('span', 'etaniAnimateAttrRemove', '×');
    elAnimateAttrRemove.onclick = elAnimateAttrRemoveClick;
    elAnimateAttr.appendChild(elAnimateAttrRemove);
    // Build Animate Name
    const aName = getAnimateName(element)
    const elAnimateName = createEl('span', 'etaniAnimateName', aName);
    elAnimateAttr.appendChild(elAnimateName);
    // Build id attribute
    if (targetAni.hasAttribute('id')) {
        const aId = 'id = ' + targetAni.id;
        const elAnimateId = createEl('span', 'etaniAnimateId', aId);
        elAnimateId.onclick = editIdAttribute;
        elAnimateAttr.appendChild(elAnimateId);
    }
    // Build dur attribute
    const aDur = 'dur = ' + targetAni.getAttribute('dur');
    const elAnimateDur = createEl('span', 'etaniAnimateDur', aDur);
    elAnimateDur.onclick = editDurAttribute;
    elAnimateAttr.appendChild(elAnimateDur);
    // Build add button
    const elAnimateAttrAdd = createEl('span', 'etaniAnimateAttrAdd', '+');
    elAnimateAttrAdd.onclick = elAnimateAttrAddClick;
    elAnimateAttr.appendChild(elAnimateAttrAdd);
    etaniOldAttr.replaceWith(elAnimateAttr);
}

// check the id is valid
function isValidId(str) {
  if (typeof str !== 'string' || str.trim() === '') return false;
  if (str.trim() !== str) return false;
  if (!/^[a-zA-Z]/.test(str)) return false;
  if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(str)) return false;
  if (/[:.]/.test(str)) return false;
  return true;
}

// Add attribute to the target animation element
function elAnimateAttrAddClick(e) {
    // handle dropdown menu
    if (this.querySelector('.etaniDropdown')) {
        if (e.target.innerHTML === 'id') {
            editIdAttribute.call(this);
        } else if (e.target.innerHTML === 'begin') {
            editBeginAttribute.call(this);
        } else if (e.target.innerHTML === 'other') {
            console.log('add other');
        }
        this.querySelector('.etaniDropdown').remove();
        return;
    }
    // open dropdown menu
    const etaniDropdown = createEl('div', 'etaniDropdown');
    etaniDropdown.style.top = this.getBoundingClientRect().height + 'px';
    let targetAni = getTargetAnimation(this);
    if (Array.isArray(targetAni)) targetAni = targetAni[0];
    const attrObj = Array.from(targetAni.attributes).reduce((obj, attr) => {
        obj[attr.name] = attr.value;
        return obj;
    }, {});
    const animationProps = Object.keys(attrObj);
    if (!animationProps.includes('id')) {
        const itemid = createEl('div', 'etaniDropdownItem', 'id');
        etaniDropdown.appendChild(itemid);
    }
    if (!animationProps.includes('begin')) {
        const itemBegin = createEl('div', 'etaniDropdownItem', 'begin');
        etaniDropdown.appendChild(itemBegin);
    }
    const itemOther = createEl('div', 'etaniDropdownItem', 'other');
    etaniDropdown.appendChild(itemOther);
    this.appendChild(etaniDropdown);
}

// remove the target animation element
function elAnimateAttrRemoveClick() {
    const targetAnimation = getTargetAnimation(this);
    if (Array.isArray(targetAnimation)) {
        targetAnimation[2].remove();
        targetAnimation[1].remove();
        targetAnimation[0].remove();
    } else {
        targetAnimation.remove();
    }
    this.closest('.etaniAnimate').remove();
    updateEtaniResult();
}

// edit Animate Attribute
function editDurAttribute() {
    const targetAnimation = getTargetAnimation(this);
    let editDurValue;
    if (Array.isArray(targetAnimation)) {
        editDurValue = targetAnimation[0].getAttribute('dur');
    } else {
        editDurValue = targetAnimation.getAttribute('dur');
    }
    editDurValue = editDurValue.replace('s', '');

    const editDur = createEl('div', 'etaniEditDur');
    const editDurLabel = createEl('label', 'editDurLabel', 'dur = ');
    const editDurInput = createEl('input', 'editDurInput');
    editDurInput.type = 'text';
    editDurInput.value = editDurValue;
    const editDurSpan = createEl('span', 'editDurSpan', ' s');
    editDurLabel.appendChild(editDurInput);
    editDurLabel.appendChild(editDurSpan);
    editDur.appendChild(editDurLabel);
    etaniWindow(editDur, () => {
        let setDurValue = document.querySelector('.editDurInput').value;
        setDurValue = setDurValue + 's';
        if (Array.isArray(targetAnimation)) {
            targetAnimation[0].setAttribute('dur', setDurValue);
            targetAnimation[1].setAttribute('dur', setDurValue);
            targetAnimation[2].setAttribute('dur', setDurValue);
        } else {
            targetAnimation.setAttribute('dur', setDurValue);
        }
        this.textContent = 'dur = ' + setDurValue;
        updateEtaniResult();
    });
}

// get values from targetAnimation
function getValues(targetAnimation) {
    if (!Array.isArray(targetAnimation)) {
        if (targetAnimation.tagName === 'animate') {
            return targetAnimation.getAttribute('values').split(';');
        } else if (targetAnimation.tagName === 'animateMotion') {
            if (targetAnimation.hasAttribute('path')) {
                return [targetAnimation.getAttribute('path')];
            } else if (targetAnimation.querySelector('mpath')) {
                return [targetAnimation.querySelector('mpath').getAttribute('href')];
            }
        } else if (targetAnimation.tagName === 'set') {
            return [targetAnimation.getAttribute('to')];
        }
    }
    // Get values from all three transform elements
    const translateVals = (targetAnimation[0]?.getAttribute('values') || '').split(';');
    const scaleVals = (targetAnimation[1]?.getAttribute('values') || '').split(';');
    const rotateVals = (targetAnimation[2]?.getAttribute('values') || '').split(';');

    // Assume all arrays have the same length, based on the first one
    const valuesLength = translateVals.length;
    if (valuesLength === 0 || scaleVals.length !== valuesLength || rotateVals.length !== valuesLength) {
        console.warn('animateTransform value arrays have mismatched lengths or are empty.');
    }

    const combinedValues = [];
    for (let i = 0; i < valuesLength; i++) {
        // Combine corresponding values with ';'
        const combined = `${translateVals[i] || ''};${scaleVals[i] || ''};${rotateVals[i] || ''}`;
        combinedValues.push(combined);
    }
    return combinedValues;
}

// Values item click event
function etaniAVItemClick () {
    // get standard data
    const targetAnimation = getTargetAnimation(this);
    const animateValue = this.closest('.etaniAnimateValue');
    const ctrlMode = animateValue.dataset.mode;
    const animateParent = this.closest('.etaniAnimate');
    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;
    const dataId = this.closest('.etaniItem').dataset.id;
    const dataType = this.closest('.etaniItem').dataset.type;
    const etaniAV = animateParent.querySelector('.etaniAV');
    // Get the index of this element among its siblings
    const itemParent = this.parentNode;
    const itemIndex = Array.from(itemParent.children).indexOf(this);
    const valueArray = getValues(targetAnimation);
    const targetValue = valueArray[itemIndex];

    if (ctrlMode === 'delete') {
        if (valueArray.length > 1) {
            valueArray.splice(itemIndex, 1);
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        }
    } else if (ctrlMode === 'copy') {
        valueArray.splice(itemIndex, 0, targetValue);
        setValues(tagName, targetAnimation, valueArray, etaniAV);
    } else if (ctrlMode === 'move') {
        let sItem = itemParent.querySelector('.selected');
        if (this === sItem) {
            this.classList.remove('selected');
        } else if (sItem) {
            const sIndex = Array.from(itemParent.children).indexOf(sItem);
            const sValue = valueArray[sIndex];
            if (sIndex < itemIndex) {
                if (this.nextSibling) {
                    itemParent.insertBefore(sItem, this.nextSibling);
                } else {
                    itemParent.appendChild(sItem);
                }
                valueArray.splice(itemIndex + 1, 0, sValue);
                valueArray.splice(sIndex, 1);
            } else {
                itemParent.insertBefore(sItem, this);
                valueArray.splice(sIndex, 1);
                valueArray.splice(itemIndex, 0, sValue);
            }
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        } else {
            this.classList.add('selected');
        }
    } else if (tagName === 'animate') {
        const editDiv = createEl('div', 'etaniEditDiv');
        const editLabel = createEl('label', 'editLabel', animateType + ' = ');
        const editInput = createEl('input', 'editInput');
        editInput.type = 'input';
        editInput.value = targetValue;
        editLabel.appendChild(editInput);
        editDiv.appendChild(editLabel);
        etaniWindow(editDiv, () => {
            valueArray.splice(itemIndex, 1, editInput.value);
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        });
    } else if (tagName === 'animateTransform') {
        const editDiv = createEl('div', 'etaniEditDiv');
        // translate
        const translateRow = createEl('div', 'etaniAppendRow');
        const translateLabel = createEl('label', 'editLabel', 'translate = ');
        const translateInput = createEl('input', 'editInput');
        translateInput.type = 'input';
        translateInput.value = targetValue.split(';')[0];
        translateRow.appendChild(translateLabel);
        translateRow.appendChild(translateInput);
        editDiv.appendChild(translateRow);
        // scale
        const scaleRow = createEl('div', 'etaniAppendRow');
        const scaleLabel = createEl('label', 'editLabel', 'scale = ');
        const scaleInput = createEl('input', 'editInput');
        scaleInput.type = 'input';
        scaleInput.value = targetValue.split(';')[1];
        scaleRow.appendChild(scaleLabel);
        scaleRow.appendChild(scaleInput);
        editDiv.appendChild(scaleRow);
        // rotate
        const rotateRow = createEl('div', 'etaniAppendRow');
        const rotateLabel = createEl('label', 'editLabel', 'rotate = ');
        const rotateInput = createEl('input', 'editInput');
        rotateInput.type = 'input';
        rotateInput.value = targetValue.split(';')[2];
        rotateRow.appendChild(rotateLabel);
        rotateRow.appendChild(rotateInput);
        editDiv.appendChild(rotateRow);

        etaniWindow(editDiv, () => {
            let combineValue = translateInput.value + ';' + 
                scaleInput.value + ';' + rotateInput.value;
            valueArray.splice(itemIndex, 1, combineValue);
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        });
    } else if (tagName === 'animateMotion') {
        const editDiv = createEl('div', 'etaniEditDiv');
        const editLabel = createEl('label', 'editLabel', 'path = ');
        const editInput = createEl('input', 'editInput');
        editInput.type = 'input';
        editInput.value = targetValue;
        editLabel.appendChild(editInput);
        editDiv.appendChild(editLabel);

        etaniWindow(editDiv, () => {
            setValues(tagName, targetAnimation, [editInput.value], etaniAV);
        });
    } else if (tagName === 'set') {
        const editDivTitle = createEl('div', 'etaniEditDiv');
        const editTitle = createEl('label', 'editLabel', 'set ' + animateType);
        const editDiv = createEl('div', 'etaniEditDiv');
        const editLabel = createEl('label', 'editLabel', 'to = ');
        const editInput = createEl('input', 'editInput');
        editInput.type = 'input';
        editInput.value = targetValue;
        editDivTitle.appendChild(editTitle);
        editLabel.appendChild(editInput);
        editDiv.appendChild(editDivTitle);
        editDiv.appendChild(editLabel);

        etaniWindow(editDiv, () => {
            setValues(tagName, targetAnimation, [editInput.value], etaniAV);
        });
    }
}

// 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.onclick = etaniStartClick;
});
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4001
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 190 次
联系:

Re: 正在做etani,ejtile的動畫套件

帖子 ejsoon »

改input樣式:

代码: 全选

瀏覽器預設的 `<input type="text">`(或 `input[type=input]`,通常指文字輸入框)樣式確實很樞紐、邊框單薄、圓角不夠、陰影缺失,視覺上不夠現代。要讓它「好看」,可以從 **一致性、層次感、互動反饋、圓角、字體** 幾個方向優化。

以下是一個 **現代、簡潔、美觀** 的範例 CSS,適合大多數網站風格:

```css
/* 基礎重置與美化 input[type=text], input[type=email], input[type=password] 等 */
input[type=text],
input[type=email],
input[type=password],
input[type=search],
input[type=number],
input[type=tel],
input[type=url] {
  /* 外觀 */
  -webkit-appearance: none; /* 移除 iOS 預設樣式 */
  -moz-appearance: none;
  appearance: none;

  /* 佈局 */
  width: 100%;
  padding: 12px 16px;
  font-size: 16px; /* 避免 iOS 縮放 */
  line-height: 1.5;
  border: 2px solid #e2e8f0;
  border-radius: 12px; /* 圓角更柔和 */
  background-color: #ffffff;
  color: #1a202c;

  /* 字體與排版 */
  font-family: inherit;
  font-weight: 500;

  /* 過渡動畫 */
  transition: all 0.2s ease;

  /* 聚焦時效果 */
  &:focus {
    outline: none;
    border-color: #3b82f6; /* 藍色焦點邊框 */
    box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.15); /* 柔和外陰影 */
    background-color: #f8faff;
  }

  /* 懸停效果 */
  &:hover {
    border-color: #94a3b8;
  }

  /* 禁用狀態 */
  &:disabled {
    background-color: #f1f5f9;
    color: #94a3b8;
    cursor: not-allowed;
    opacity: 0.7;
  }

  /* 錯誤狀態(可搭配 class="error" 使用) */
  &.error {
    border-color: #ef4444;
    background-color: #fef2f2;
  }

  &.error:focus {
    box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.15);
  }
}

/* 可選:加上 placeholder 樣式 */
input::placeholder {
  color: #94a3b8;
  opacity: 1;
  font-weight: 400;
}
```

---

### 完整 HTML 範例

```html
<!DOCTYPE html>
<html lang="zh-TW">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>美化 Input</title>
  <style>
    /* 貼上上面的 CSS */
    body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      padding: 40px;
      background: #f7fafc;
    }
    .form-group {
      margin-bottom: 20px;
      max-width: 400px;
    }
    label {
      display: block;
      margin-bottom: 8px;
      font-weight: 600;
      color: #2d3748;
    }
  </style>
</head>
<body>

  <div class="form-group">
    <label for="name">姓名</label>
    <input type="text" id="name" placeholder="請輸入姓名" />
  </div>

  <div class="form-group">
    <label for="email">Email</label>
    <input type="email" id="email" placeholder="example@domain.com" class="error" />
  </div>

</body>
</html>
```

---

### 設計要點總結(讓 input 更好看)

| 項目 | 建議 |
|------|------|
| **圓角** | `border-radius: 8px ~ 12px` 比預設的 0 或 4px 更柔和 |
| **邊框** | 用 `2px` 比 `1px` 更有存在感 |
| **內距** | `padding: 12px 16px` 讓點擊區域更大 |
| **字體** | `font-size: 16px` + `font-weight: 500` 更清晰 |
| **聚焦效果** | 邊框變色 + 外層 `box-shadow` 提示互動 |
| **陰影/背景** | 聚焦時微調背景色(如 `#f8faff`)增加層次 |
| **一致性** | 所有 input 統一樣式(text, email, search...) |

---

### 進階建議

- **暗黑模式支援**:
  ```css
  @media (prefers-color-scheme: dark) {
    input[type=text] { 
      background: #1a202c; 
      border-color: #4a5568; 
      color: #f7fafc; 
    }
    input:focus { 
      border-color: #63b3ed; 
      box-shadow: 0 0 0 4px rgba(99, 179, 237, 0.2); 
    }
  }
  ```

- **使用 CSS 變數**方便主題切換:
  ```css
  :root {
    --input-border: #e2e8f0;
    --input-focus: #3b82f6;
    --input-radius: 12px;
  }
  ```

---

照這樣改,你的 input 會從「樸素」變成「專業好看」,而且跨瀏覽器一致、行動裝置友善。

需要我幫你搭配 **按鈕、表單整體風格** 嗎?
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4001
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 190 次
联系:

Re: 正在做etani,ejtile的動畫套件

帖子 ejsoon »

美化了一下下input[type=text]
當前代碼:

代码: 全选

// 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;
}
.etaniinner input[type=text] {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  line-height: 1.5;
  border: 2px solid #e2e8f0;
  border-radius: 1px;
  background-color: #ffffff;
  color: #1a202c;
  font-family: inherit;
  font-weight: 500;
  transition: all 0.2s ease;
  &:focus {
    outline: none;
    border-color: #3b82f6;
    box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.15);
    background-color: #f8faff;
  }
  &:hover {
    border-color: #94a3b8;
  }
}
.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;
  user-select: none;
}
.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 {
  position: relative;
  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;
}
.etaniAVItem.selected {
  background-color: #779933;
}
.etaniAVAdd {
  background-color: #a7fca7;
  border: 1px solid #71c371;
}
.etaniAVDelete {
  background-color: #ffcccc;
  border: 1px solid #cc3333;
}
.etaniAVCopy {
  background-color: #ccccff;
  border: 1px solid #6666cc;
}
.etaniAVMove {
  background-color: #ffcc99;
  border: 1px solid #cc9966;
}
.etaniAVDelete.active {
  background-color: #cc3333;
  color: white;
}
.etaniAVCopy.active {
  background-color: #6666cc;
  color: white;
}
.etaniAVMove.active {
  background-color: #cc9966;
  color: white;
}
.etaniAVCtrl > span > svg {
  margin-left: -1px;
  margin-top: -1px;
}
textarea.etaniHTMLTextarea {
  width: calc(100vw - 72px);
  height: calc(50vh - 24px);
  resize: none;
  border: 1px solid #ccc;
  font-size: 12px;
  box-sizing: border-box;
}
.etaniWindowRow {
  padding-top: 12px;
  text-align: center;
}
.etaniWindowRow button {
  margin: 0 12px;
}
.editDurInput {
  width: 36px;
}
.etaniEditDur {
  text-align: center;
}
.editInput {
  width: 120px;
}
.etaniAnimateAttrRemove {
  float: right;
  margin-right: 0 !important;
  border-color: #c33;
  color: #861616;
  background: #f6cdcd;
}
.etaniDropdown {
  position: absolute;
  left: 0px;
  background-color: #fff;
  border: 1px solid #ccc;
  box-shadow: 0 4px 8px rgba(0,0,0,0.1);
  z-index: 1200;
}
.etaniDropdownItem {
  color: black;
  padding: 8px 12px;
  cursor: pointer;
  font-size: 12px;
}
.etaniDropdownItem:hover {
  color: green;
  background-color: #f0f0f0;
}
.etaniIdListSpan {
  border: 1px solid #420664;
  padding: 2px 5px;
  font-size: 12px;
  color: #420664;
  margin: 3px;
}
.etaniIdListSpan.selected {
  background: #420664;
  color: white;
}
.etaniIdListDiv {
  max-width: 360px;
  margin-top: 7px;
}
.etaniAnimateId {
  border-color: #420664;
  color: #420664;
}
.etaniAttrRemove {
  cursor: pointer;
  padding: 2px 5px;
  font-size: 12px;
  box-sizing: border-box;
  border-width: 1px;
  border-style: solid;
  float: right;
  margin-right: 0 !important;
  border-color: #c33;
  color: #861616;
  background: #f6cdcd;
}
.editBeginStartInput {
  width: 36px;
}
.beginOption {
  border: 1px solid #777;
  background-color: #eee;
  color: #333;
  display: inline-block;
  text-decoration: none;
  padding: 2px 8px;
  font-size: 14px;
  margin: 0 2px;
  cursor: pointer;
  user-select: none;
}
.beginOption.selected {
  background-color: #008CBA;
  color: white;
  border-color: #008CBA;
}
.endOrBeginDiv {
  margin-top: 7px;
}
    `;

    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.onclick = 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.onclick = 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.onclick = function() {
            // Remove active from all spans in etaniMode
            modeSpans.forEach(function(s) { s.classList.remove('active'); });
            // Add active to clicked span
            this.classList.add('active');
        };
    });

    // Create etaniAllAppend div
    const etaniAllAppend = createEl('div', 'etaniAllAppend');

    // Create etaniAllAppendTransform a
    const etaniAllAppendTransform = createEl('a', 'etaniAllAppendTransform');
    etaniAllAppendTransform.href = 'javascript:;';
    etaniAllAppendTransform.textContent = 'transform';
    etaniAllAppend.appendChild(etaniAllAppendTransform);

    // Create etaniAllAppendOpacity a
    const etaniAllAppendOpacity = createEl('a', 'etaniAllAppendOpacity');
    etaniAllAppendOpacity.href = 'javascript:;';
    etaniAllAppendOpacity.textContent = 'opacity';
    etaniAllAppend.appendChild(etaniAllAppendOpacity);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniAllAppend);

    // Create etaniValue div
    const etaniValue = createEl('div', 'etaniValue');

    // Create etaniValueIncrease a
    const etaniValueIncrease = createEl('a', 'etaniValueIncrease');
    etaniValueIncrease.href = 'javascript:;';
    etaniValueIncrease.textContent = 'increase';
    etaniValue.appendChild(etaniValueIncrease);

    // Append to etaniCtrl
    etaniCtrl.appendChild(etaniValue);
}

// Define updateMovingTiles
function updateMovingTiles() {
    const movingUse = document.querySelector('#etmain > .etdrop > use.tilemoving');
    const items = document.querySelectorAll('.etaniItem');
    items.forEach(function(item) {
        item.style.display = 'none';
    });
    if (movingUse) {
        const id = movingUse.getAttribute('href').slice(1);
        const item = document.querySelector('.etaniItem[data-id="' + id + '"]');
        if (item) {
            item.style.display = 'block';
        }
    }
}

// list Etani elements
function listEtaniItems() {
    // Get the etaniCol element
    let etaniCol = document.querySelector('.etaniCol');
    if (!etaniCol) return; // Exit if etaniCol not found

    // Clear existing content in etaniCol
    etaniCol.innerHTML = '';

    // Collect elements into etaniElementArray
    let uses = etani.querySelectorAll('.etdrop > use');
    let etanidrop = etani.getElementById('etanidrop');
    let boardElements = etani.querySelectorAll('.etdrop > .etboard [id]');
    let etaniElementArray = [...uses, etanidrop, ...boardElements];

    // Loop through etaniElementArray to create etaniItem elements
    etaniElementArray.forEach(function(element) {
        if (!element) return; // Skip if element is null

        // Determine itemId and dataType
        let itemId;
        let dataType;
        if (element.tagName === 'use') {
            itemId = element.getAttribute('href').slice(1);
            dataType = 'tile';
        } else if (element.id === 'etanidrop') {
            itemId = 'etanidrop';
            dataType = 'board';
        } else {
            itemId = element.id;
            dataType = 'board';
        }

        // Create etaniItem div
        const etaniItem = createEl('div', 'etaniItem');
        etaniItem.dataset.id = itemId;
        etaniItem.dataset.type = dataType;

        // Create etaniItemLeft div
        const etaniItemLeft = createEl('div', 'etaniItemLeft');
        etaniItem.appendChild(etaniItemLeft);

        // Create etaniItemImageOuter div
        const etaniItemImageOuter = createEl('div', 'etaniItemImageOuter');
        etaniItemLeft.appendChild(etaniItemImageOuter);

        if (dataType === 'tile') {
            // Create etaniItemImage img
            const etaniItemImage = createEl('img', 'etaniItemImage');
            etaniItemImage.src = generateTileImage(itemId);
            etaniItemImageOuter.appendChild(etaniItemImage);
        } else {
            // Generate background color
            etaniItemImageOuter.style.background = generateHexColor(itemId);
        }

        // Create etaniItemId div
        const etaniItemId = createEl('div', 'etaniItemId');
        etaniItemId.onclick = function() {
            etaniAppendAnimateWindow(itemId, dataType);
        };
        etaniItemLeft.appendChild(etaniItemId);

        // Create etaniItemName span
        const itemName = itemId === 'etanidrop' ? 'board' : itemId;
        const etaniItemName = createEl('span', 'etaniItemName', itemName);
        etaniItemId.appendChild(etaniItemName);

        // Create etaniItemPlus img
        const etaniItemPlus = createEl('span', 'etaniItemPlus');
        etaniItemPlus.innerHTML = '<svg width="12" height="12" fill="none" stroke-width="1" stroke="darkgreen"><line x1="6" y1="0" x2="6" y2="12"></line><line x1="0" y1="6" x2="12" y2="6"></line></svg>';
        etaniItemId.appendChild(etaniItemPlus);

        // Create etaniItemRight div
        const etaniItemRight = createEl('div', 'etaniItemRight');
        etaniItem.appendChild(etaniItemRight);

        // Append etaniItem to etaniCol
        etaniCol.appendChild(etaniItem);
    });
}

// Define updateVisibility
function updateVisibility() {
    const active = document.querySelector('.etaniFilter .active');
    if (!active) return;

    const activeClass = active.classList[0]; // e.g., 'etaniFilterTiles'
    const items = document.querySelectorAll('.etaniItem');
    items.forEach(function(item) {
        item.style.display = 'none';
    });

    if (activeClass === 'etaniFilterTiles') {
        document.querySelectorAll('.etaniItem[data-type="tile"]').forEach(function(item) {
            item.style.display = 'block';
        });
    } else if (activeClass === 'etaniFilterBoard') {
        document.querySelectorAll('.etaniItem[data-type="board"]').forEach(function(item) {
            item.style.display = 'block';
        });
    } else if (activeClass === 'etaniFilterMoving') {
        document.querySelectorAll('.etaniItem[data-type="board"]').forEach(function(item) {
            item.style.display = 'block';
        });
        // Tiles visibility handled by interval
    }
}

// Convert SVG string to a Base64 data URL
function svgToBase64(svgString) {
    const base64 = btoa(unescape(encodeURIComponent(svgString)));
    return `data:image/svg+xml;base64,${base64}`;
}

// Define start button click
function etaniStartClick() {
    const etaniouter = document.querySelector('.etaniouter');

    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etani = originalSvg.cloneNode(true);
    const etwaitElement = etani.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    etani.id = 'etani';
    const etdropClone = etani.querySelector('.etdrop');
    if (etdropClone) {
        etdropClone.id = 'etanidrop';
    }

    // Check if etaniinner already exists
    let etaniinner = etaniouter.querySelector('.etaniinner');
    if (etaniinner) {
        // Remove etaniinner
        etaniouter.removeChild(etaniinner);
        // Restore button text
        etaniStart.textContent = 'start ejtile animate';
    } else {
        // Create etaniinner div
        etaniinner = createEl('div', 'etaniinner');

        // Create etaniCtrl div
        addEtaniCtrlElements(etaniinner);

        // Create etaniCol div
        const etaniCol = createEl('div', 'etaniCol');
        etaniinner.appendChild(etaniCol);

        // Create etaniResult div
        const etaniResult = createEl('div', 'etaniResult');

        etaniinner.appendChild(etaniResult);

        // Append etaniinner to etaniouter
        etaniouter.appendChild(etaniinner);

        // Change button text
        etaniStart.textContent = 'close ejtile animate';

        // list etani elements
        listEtaniItems();

        // update etani elements display by etaniFilter
        updateVisibility();

        // update result
        updateEtaniResult();
    }
}

// Generate base64 image for a tile
function generateTileImage(tileid) {
    const originalTile = document.querySelector(`#etmain > defs > g#${tileid}`);
    if (!originalTile) return null;

    const etdropUses = document.querySelectorAll('#etmain > .etdrop > use');
    const etwaitGroups = document.querySelectorAll('#etmain > .etwait g');
    
    let targetUse = null;
    for (const group of etwaitGroups) {
        const useInGroup = group.querySelector(`use[href="#${tileid}"]`);
        if (useInGroup) {
            targetUse = useInGroup;
            break;
        }
    }
    
    let etwaittransform = '', etwaitfill = '', etwaitstroke = '', etwaitstrokeWidth = '';
    if (targetUse) {
        etwaittransform = targetUse.getAttribute('transform') || '';
        etwaitfill = targetUse.getAttribute('fill') || '';
        etwaitstroke = targetUse.getAttribute('stroke') || '';
        etwaitstrokeWidth = targetUse.getAttribute('stroke-width') || '';

        const scaleMatch = etwaittransform.match(/scale\(([^)]+)\)/);
        const rotateMatch = etwaittransform.match(/rotate\(([^)]+)\)/);
        const scalePart = scaleMatch ? scaleMatch[0] : '';
        const rotatePart = rotateMatch ? rotateMatch[0] : '';
        etwaittransform = `translate(20,20) ${scalePart} ${rotatePart}`.trim().replace(/\s+/g, ' ');
    }
    
    const tileclone = originalTile.cloneNode(true);
    tileclone.removeAttribute('id');
    if (etwaittransform) tileclone.setAttribute('transform', etwaittransform);
    if (etwaitfill) tileclone.setAttribute('fill', etwaitfill);
    if (etwaitstroke) tileclone.setAttribute('stroke', etwaitstroke);
    if (etwaitstrokeWidth) tileclone.setAttribute('stroke-width', etwaitstrokeWidth);
    
    const svgWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svgWrapper.setAttribute('width', '40');
    svgWrapper.setAttribute('height', '40');
    svgWrapper.setAttribute('version', '1.1');
    svgWrapper.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
    svgWrapper.appendChild(tileclone);
    
    const svgString = new XMLSerializer().serializeToString(svgWrapper);
    return svgToBase64(svgString);
}

// generate hex color
function generateHexColor(seed) {
  let hash = 0;
  for (let i = 0; i < seed.length; i++) {
    hash = seed.charCodeAt(i) + ((hash << 5) - hash);
  }
  let color = (hash & 0x00FFFFFF).toString(16).toUpperCase();
  while (color.length < 6) {
    color = '0' + color;
  }
  return '#' + color;
}

// Handle the HTML popup window
function handleContentHTMLClick() {
    if (!etani) return;
    const textarea = createEl('textarea', 'etaniHTMLTextarea');
    textarea.value = new XMLSerializer().serializeToString(etani);
    etaniWindow(textarea, () => {
        const parser = new DOMParser();
        etani = parser.parseFromString(textarea.value, 'image/svg+xml');
        updateEtaniResult();
    });
}

// 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.onclick = function() {
            if (typeof confirm_function === 'function') {
                confirm_function();
            }
            windowDiv.remove();
        };
    }
    if (btnCancel) {
        btnCancel.onclick = function() {
            windowDiv.remove();
        };
    }
}

// Open append animate window
function etaniAppendAnimateWindow(id, dataType) {
    const appendAnimateWindow = createEl('div', 'appendAnimateWindow');
    appendAnimateWindow.dataset.id = id;
    appendAnimateWindow.dataset.type = dataType;
    // 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.onclick = etaniAppendAnimate;
    aMotion.onclick = etaniAppendAnimate;
    aOpacity.onclick = etaniAppendAnimate;
    aWriting.onclick = etaniAppendAnimate;
    aFill.onclick = etaniAppendAnimate;
    aStroke.onclick = etaniAppendAnimate;
    aWidth.onclick = etaniAppendAnimate;
    // For span.etaniAppendSet
    spanSet.onclick = 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;
    }

    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' };
        }
    }
    // if type is board, it must has the 'href' attribute
    let boardHref = {};
    if (elementtype === 'board') {
        boardHref = { href: `#${id}` };
    }

    // Special attribute
    let targetEl;
    if (elementtype === 'tile') {
        targetEl = etani.querySelector(`.etdrop > use[href="#${id}"]`);
    } else {
        targetEl = etani.getElementById(id);
    }
    // Writting animation
    if (animatetype === 'stroke-dasharray') {
        // get the writing length
        let tPath = etani.getElementById(id);
        tPath = elementtype === 'tile' ? tPath.firstChild : tPath;
        let targetLength = Math.round(tPath.getTotalLength());
        // set dashoffset
        targetEl.setAttribute('stroke-dashoffset', targetLength);
        // set defaultvalue
        defaultvalue = targetLength + ';' + (targetLength * 2);
    } else if (animatetype === 'fill') {
        // get the original color
        let targetElFill = targetEl.getAttribute('fill');
        if (targetElFill) defaultvalue = targetElFill;
    }

    // 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",
                ...boardHref,
                ...repeatOrFillAttrs // Add the repeat/fill logic
            };

            // Create and append <animateTransform> for translate
            const elTranslate = createSVGElement('animateTransform', {
                ...baseAttrs,
                dur: "1s",
                type: "translate",
                values: transformValues.translate
            });
            targetSVGParent.appendChild(elTranslate);

            // Create and append <animateTransform> for scale
            const elScale = createSVGElement('animateTransform', {
                ...baseAttrs,
                dur: "1s",
                type: "scale",
                values: transformValues.scale,
                additive: "sum"
            });
            targetSVGParent.appendChild(elScale);

            // Create and append <animateTransform> for rotate
            const elRotate = createSVGElement('animateTransform', {
                ...baseAttrs,
                dur: "1s",
                type: "rotate",
                values: transformValues.rotate,
                additive: "sum"
            });
            targetSVGParent.appendChild(elRotate);
            break;

        case 'animate':
            const elAnimateSVG = createSVGElement('animate', {
                attributeName: animatetype,
                dur: "1s",
                values: defaultvalue,
                ...boardHref,
                ...repeatOrFillAttrs
            });
            targetSVGParent.appendChild(elAnimateSVG);
            break;

        case 'animateMotion':
            const elMotion = createSVGElement('animateMotion', {
                dur: "1s",
                path: defaultvalue,
                ...boardHref,
                ...repeatOrFillAttrs
            });
            targetSVGParent.appendChild(elMotion);
            break;

        case 'set':
            const elSet = createSVGElement('set', {
                attributeName: animatetype,
                to: defaultvalue,
                dur: "1s",
                ...boardHref,
                ...repeatOrFillAttrs // Will be {fill: "freeze"} or {}
            });
            targetSVGParent.appendChild(elSet);
            break;
    }
}

// get target animate element by etaniAVCtrl button
function getTargetAnimation(valueBtn) {
    // --- Step 1: Get .etaniAnimate parent data ---
    const animateParent = valueBtn.closest('.etaniAnimate');
    if (!animateParent) {
        throw new Error('Could not find parent .etaniAnimate');
    }

    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;

    // Get the index of this element among its siblings
    const parentChildren = Array.from(animateParent.parentNode.children);
    const animateIndex = parentChildren.indexOf(animateParent);

    if (animateIndex === -1) {
         throw new Error('Could not determine animateIndex');
    }

    // --- Step 2: Get .etaniItem parent data ---
    const itemParent = valueBtn.closest('.etaniItem');
    if (!itemParent) {
        throw new Error('Could not find parent .etaniItem');
    }

    const dataId = itemParent.dataset.id;
    const dataType = itemParent.dataset.type;
    const hrefId = `#${dataId}`; // Prepare for attribute selector

    // --- Step 3: Find target animation elements ---
    const animationTags = 'animateTransform, animate, animateMotion, set';
    let animations = [];

    if (dataType === 'tile') {
        const useElement = etani.querySelector(`.etdrop > use[href="${hrefId}"]`);
        if (useElement) {
            // Find all animation elements inside the <use> tag
            animations = Array.from(useElement.querySelectorAll(animationTags));
        }
    } else if (dataType === 'board') {
        // Find all direct children of #etani matching the tags and href
        const selector = `:scope > animateTransform[href="${hrefId}"], 
                        :scope > animate[href="${hrefId}"], 
                        :scope > animateMotion[href="${hrefId}"], 
                        :scope > set[href="${hrefId}"]`;
        animations = Array.from(etani.querySelectorAll(selector));
    }

    // Group consecutive animateTransforms (3 at a time)
    const groupedAnimations = [];
    for (let i = 0; i < animations.length; ) {
        const currentAnim = animations[i];
        if (currentAnim.tagName === 'animateTransform') {
            // Assume 3 consecutive animateTransforms
            if (i + 2 < animations.length &&
                animations[i+1].tagName === 'animateTransform' &&
                animations[i+2].tagName === 'animateTransform') 
            {
                groupedAnimations.push([animations[i], animations[i+1], animations[i+2]]);
                i += 3;
            } else {
                // Handle incomplete groups: log warning and skip this element
                console.warn('Incomplete or non-consecutive animateTransform group found.', currentAnim);
                i++; // Skip this one to avoid infinite loop
            }
        } else {
            // Add other animation types as single items
            groupedAnimations.push(currentAnim);
            i++;
        }
    }

    // Get the specific target animation (group) using the index
    const targetAnimation = groupedAnimations[animateIndex];
    if (!targetAnimation) {
        throw new Error(`No animation element found at index ${animateIndex}`);
    }
    return targetAnimation;
}
/**
 * Set the values to the animation
 * @param {tagName} string - The values, combined by semicolons.
 * @param {targetAnimation} element - The animation elements.
 * @param {valueArray} array - The values, combined by semicolons.
 * @param {etaniAV} element - The values, combined by semicolons.
 */
function setValues(tagName, targetAnimation, valueArray, etaniAV) {
    if (tagName === 'animate') {
        targetAnimation.setAttribute('values', valueArray.join(';'));
    } else if (tagName === 'animateTransform') {
        // Do not append if element or value is invalid
        let translateValues = '', scaleValues = '', rotateValues = '';
        for (let i = 0; i < valueArray.length; i++) {
            translateValues += valueArray[i].split(';')[0];
            translateValues += i < valueArray.length - 1 ? ';' : '';
            scaleValues += valueArray[i].split(';')[1];
            scaleValues += i < valueArray.length - 1 ? ';' : '';
            rotateValues += valueArray[i].split(';')[2];
            rotateValues += i < valueArray.length - 1 ? ';' : '';
        }
        targetAnimation[0].setAttribute('values', translateValues)
        targetAnimation[1].setAttribute('values', scaleValues)
        targetAnimation[2].setAttribute('values', rotateValues)
    } else if (tagName === 'animateMotion') {
        let editInputValue = valueArray[0];
        if (targetAnimation.querySelector('mpath')) {
            targetAnimation.querySelector('mpath').remove();
        }
        if (editInputValue.substring(0, 1) === '#') {
            let mpath = createSVGElement('mpath', {href : editInputValue});
            targetAnimation.appendChild(mpath);
            if (targetAnimation.hasAttribute('path')) {
                targetAnimation.removeAttribute('path');
            }
        } else {
            targetAnimation.setAttribute('path', editInputValue);
        }
    } else if (tagName === 'set') {
        let editInputValue = valueArray[0];
        targetAnimation.setAttribute('to', valueArray[0]);
    }
    setUIAnimateValues(tagName, targetAnimation, etaniAV);
    updateEtaniResult();
}

/**
 * Appends a value to an element's 'values' attribute, separated by semicolons.
 * @param {Element} element - The animation element.
 * @param {string|number} value - The value to append.
 */
function addValue(element, value) {
    // Do not append if element or value is invalid
    if (!element || value === null || typeof value === 'undefined') return;
    
    let currentValues = element.getAttribute('values');
    const stringValue = String(value); // Ensure value is a string

    if (currentValues && currentValues.trim() !== '') {
        // Add with a semicolon if values already exist
        element.setAttribute('values', currentValues + ';' + stringValue);
    } else {
        // Set as the first value
        element.setAttribute('values', stringValue);
    }
}

/**
 * Extracts a specific transform function's value (e.g., "10 20" from "translate(10 20)")
 * @param {string} type - The transform type (e.g., 'translate', 'scale').
 * @param {string} transformString - The full transform attribute string.
 * @returns {string|null} The extracted value or null if not found.
 */
function getTransformValue(type, transformString) {
    if (!transformString) return null;
    // Regex to find the type and capture the content inside the parentheses
    const regex = new RegExp(`${type}\\(([^)]+)\\)`);
    const match = transformString.match(regex);
    return match ? match[1] : null; // Return the captured group (the values)
}

// Add animate value
function etaniAVAddClick() {
    // Change mode to default 'edit'
    const elAVCtrl = this.closest('.etaniAVCtrl');
    const active = elAVCtrl.querySelector('.active');
    if (active) active.classList.remove('active');
    const elAnimateValue = this.closest('.etaniAnimateValue');
    elAnimateValue.dataset.mode = 'edit';
    // --- Step 1-3: get targetAnimation element(s) ---
    let targetAnimation = getTargetAnimation(this);
    // --- Step 4: Insert values into target element ---
    let defaultValue; // This will be used for both Step 4 and 5
    const animateParent = this.closest('.etaniAnimate');
    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;
    const itemParent = this.closest('.etaniItem');
    const dataId = itemParent.dataset.id;
    const hrefId = `#${dataId}`; // Prepare for attribute selector
    if (tagName === 'animateTransform') {
        // Check if targetAnimation is valid (an array of 3)
        if (Array.isArray(targetAnimation) && targetAnimation.length === 3) {
            const mainUseElement = etmain.querySelector(`.etdrop > use[href="${hrefId}"]`);
            
            if (mainUseElement) {
                const transformString = mainUseElement.getAttribute('transform');
                
                // Extract values
                const translateVal = getTransformValue('translate', transformString);
                const scaleVal = getTransformValue('scale', transformString);
                const rotateVal = getTransformValue('rotate', transformString);

                // Add values to the 'values' attribute of each corresponding animation
                // Assuming order: [0] = translate, [1] = scale, [2] = rotate
                addValue(targetAnimation[0], translateVal);
                addValue(targetAnimation[1], scaleVal);
                addValue(targetAnimation[2], rotateVal);
            }
        } else {
             console.warn('Expected targetAnimation to be a group of 3, but it was not.', targetAnimation);
        }
    } else if (tagName === 'animate') {
        // Check if targetAnimation is a single element
        if (targetAnimation && !Array.isArray(targetAnimation)) {
            // Determine default value based on animateType
            if (animateType === 'opacity') {
                defaultValue = 1;
            } else if (animateType === 'stroke-width') {
                defaultValue = 2;
            } else if (animateType === 'fill') {
                defaultValue = '#f758b8';
            } else if (animateType === 'stroke') {
                defaultValue = '#7786ce';
            } else {
                defaultValue = 0;
            }
            
            // Add to values attribute
            addValue(targetAnimation, defaultValue);
        } else {
            console.warn('Expected targetAnimation to be a single element, but it was not.', targetAnimation);
        }
    }

    // --- Step 5: Add <span> to .etaniAV ---
    const avParent = this.closest('.etaniAnimateValue');
    const etaniAV = avParent.querySelector('.etaniAV');
    setUIAnimateValues(tagName, targetAnimation, etaniAV);
    updateEtaniResult();
}

// append etaniAnimate to etaniItemRight
function appendAnimateToItemRight(id, elementtype, tagname, animatetype, defaultvalue = null) {
    // Find the UI target parent element (in the .etaniinner UI panel)
    const targetUIParent = document.querySelector(`.etaniinner .etaniItem[data-id="${id}"] > .etaniItemRight`);
    
    if (!targetUIParent) {
        console.error(`Error: UI target (.etaniinner .etaniItem[data-id="${id}"] > .etaniItemRight) not found.`);
        return; 
    }

    // Build the UI structure
    const elAnimate = createEl('div', 'etaniAnimate');
    elAnimate.dataset.tagname = tagname;
    elAnimate.dataset.animatetype = animatetype;
    
    // 1. Build .etaniAnimateAttr
    const elAnimateAttr = createEl('div', 'etaniAnimateAttr');
    const elAnimateName = createEl('span', 'etaniAnimateName');
    const elAnimateDur = createEl('span', 'etaniAnimateDur');
    elAnimateDur.onclick = editDurAttribute;

    const elAnimateAttrAdd = createEl('span', 'etaniAnimateAttrAdd', '+');
    elAnimateAttrAdd.onclick = elAnimateAttrAddClick;
    const elAnimateAttrRemove = createEl('span', 'etaniAnimateAttrRemove', '×');
    elAnimateAttrRemove.onclick = elAnimateAttrRemoveClick;
    
    elAnimateAttr.appendChild(elAnimateAttrRemove);
    elAnimateAttr.appendChild(elAnimateName);
    elAnimateAttr.appendChild(elAnimateDur);
    elAnimateAttr.appendChild(elAnimateAttrAdd);

    // 2. Build .etaniAnimateValue
    const elAnimateValue = createEl('div', 'etaniAnimateValue');
    elAnimateValue.dataset.mode = 'edit';
    const elAVLabel = createEl('span', 'etaniAVLabel');
    const elAV = createEl('div', 'etaniAV');
    const elAVItem = createEl('span', 'etaniAVItem');
    elAVItem.onclick = etaniAVItemClick;
    
    elAV.appendChild(elAVItem);

    // create controls for this type
    const elAVCtrl = createEl('div', 'etaniAVCtrl');
    const etaniAVAdd = createEl('span', 'etaniAVAdd');
    etaniAVAdd.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>';
    const etaniAVDelete = createEl('span', 'etaniAVDelete');
    etaniAVDelete.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>';
    const etaniAVCopy = createEl('span', 'etaniAVCopy');
    etaniAVCopy.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
    const etaniAVMove = createEl('span', 'etaniAVMove');
    etaniAVMove.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M 7,4 L 3,8 L 7,12 M 3,8 L 18,8 M 17,12 L 21,16 L 17,20 M 21,16 L 6,16"></path></svg>';
    elAVCtrl.appendChild(etaniAVAdd);
    elAVCtrl.appendChild(etaniAVDelete);
    elAVCtrl.appendChild(etaniAVCopy);
    elAVCtrl.appendChild(etaniAVMove);
    etaniAVAdd.onclick = etaniAVAddClick;

    const toggleButtons = {
        delete: { element: etaniAVDelete, mode: 'delete' },
        copy: { element: etaniAVCopy, mode: 'copy' },
        move: { element: etaniAVMove, mode: 'move' }
    };

    Object.values(toggleButtons).forEach(({ element, mode }) => {
        element.onclick = 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;
            }
        };
    });

    // cname
    let aName;
    if (animatetype === 'stroke-width') {
        aName = 'width';
    } else if (animatetype === 'stroke-dasharray') {
        aName = 'writing';
        let tPath = etani.getElementById(id);
        tPath = elementtype === 'tile' ? tPath.firstChild : tPath;
        defaultvalue = Math.round(tPath.getTotalLength());
    } else {
        aName = animatetype;
    }

    // Special attribute and convert animateName
    let targetEl;
    if (elementtype === 'tile') {
        targetEl = etani.querySelector(`.etdrop > use[href="#${id}"]`);
    } else {
        targetEl = etani.getElementById(id);
    }
    if (animatetype === 'fill') {
        // get the original color
        let targetElFill = targetEl.getAttribute('fill');
        if (targetElFill) defaultvalue = targetElFill;
    }

    // 3. Apply logic based on tagname
    switch (tagname) {
        case 'animateTransform':
            elAnimateName.textContent = 'transform';
            elAnimateDur.textContent = 'dur = 1s';
            elAVLabel.textContent = 'values = ';
            elAVItem.textContent = 'a'; // Default placeholder
            elAnimateValue.appendChild(elAVCtrl);
            break;
            
        case 'animate':
            elAnimateName.textContent = aName;
            elAnimateDur.textContent = 'dur = 1s';
            elAVLabel.textContent = 'values = ';
            elAVItem.textContent = defaultvalue;
            elAnimateValue.appendChild(elAVCtrl);
            break;

        case 'animateMotion':
            elAnimateName.textContent = 'motion';
            elAnimateDur.textContent = 'dur = 1s';
            elAVLabel.textContent = 'move to = ';
            elAVItem.textContent = defaultvalue;
            // No controls for this type
            break;

        case 'set':
            elAnimateName.textContent = aName;
            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);

    // if writing
    if (animatetype === 'stroke-dasharray') {
        elAV.appendChild(createEl('span', 'etaniAVItem', defaultvalue * 2));
    }
}

// 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() {
    // let id, elementtype, tagname, animatetype, defaultvalue;
    let appendAnimateWindow = this.closest('.appendAnimateWindow');
    let id = appendAnimateWindow.dataset.id;
    let elementtype = appendAnimateWindow.dataset.type;
    let tagname = 'animate', animatetype = '', defaultvalue = 0;
    if (this.innerHTML === 'transform') {
        tagname = 'animateTransform';
        animatetype = 'transform';
    } else if (this.innerHTML === 'motion') {
        tagname = 'animateMotion';
        defaultvalue = 'M 0,0 H 120 V 120 Z';
    } else if (this.innerHTML === 'opacity') {
        animatetype = 'opacity';
        defaultvalue = 1;
    } else if (this.innerHTML === 'writing') {
        animatetype = 'stroke-dasharray';
        defaultvalue = 0;
    } else if (this.innerHTML === 'fill') {
        animatetype = 'fill';
        defaultvalue = '#f758b8';
    } else if (this.innerHTML === 'stroke') {
        animatetype = 'stroke';
        defaultvalue = '#7786ce';
    } else if (this.innerHTML === 'width') {
        animatetype = 'stroke-width';
        defaultvalue = 2;
    }
    let setSpan = appendAnimateWindow.querySelector('.etaniAppendSet');
    if (setSpan.classList.contains('active')) tagname = 'set';
    appendAnimateToResult(id, elementtype, tagname, animatetype, defaultvalue);
    appendAnimateToItemRight(id, elementtype, tagname, animatetype, defaultvalue);
    updateEtaniResult();
    if (document.querySelector('.etaniWindow')) {
        document.querySelector('.etaniWindow').remove();
    }
}

/**
 * Populates a UI element with spans representing animation values,
 * based on the type of animation tag.
 *
 * @param {string} tagname - The tag name of the animation element 
 * (e.g., 'animateTransform', 'animate', 'animateMotion', 'set').
 * @param {Element|Element[]} targetAnimation - The target animation element(s).
 * @param {Element} valuesElement - The container element to append value spans to.
 */
function setUIAnimateValues(tagname, targetAnimation, valuesElement) {
    // Clear the target container first
    valuesElement.innerHTML = '';

    switch (tagname) {
        case 'animateTransform':
            // targetAnimation is an array [translate, scale, rotate]
            if (!Array.isArray(targetAnimation) || targetAnimation.length < 3) {
                console.error('animateTransform expects an array of 3 elements.');
                return;
            }

            // Get values from all three transform elements
            const translateVals = (targetAnimation[0]?.getAttribute('values') || '').split(';');
            const scaleVals = (targetAnimation[1]?.getAttribute('values') || '').split(';');
            const rotateVals = (targetAnimation[2]?.getAttribute('values') || '').split(';');

            // Assume all arrays have the same length, based on the first one
            const valuesLength = translateVals.length;
            if (valuesLength === 0 || scaleVals.length !== valuesLength || rotateVals.length !== valuesLength) {
                console.warn('animateTransform value arrays have mismatched lengths or are empty.');
                // Continue anyway, but might produce incomplete results
            }

            const combinedValues = [];
            for (let i = 0; i < valuesLength; i++) {
                // Combine corresponding values with ';'
                const combined = `${translateVals[i] || ''};${scaleVals[i] || ''};${rotateVals[i] || ''}`;
                combinedValues.push(combined);
            }

            // Map unique combined values to representative letters (a-z, A-Z)
            const valueMap = new Map();
            const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
            let charIndex = 0;
            const representativeLetters = [];

            for (const value of combinedValues) {
                let letter = valueMap.get(value);
                if (!letter) {
                    // Assign a new letter if this value hasn't been seen
                    if (charIndex < alphabet.length) {
                        letter = alphabet[charIndex];
                        charIndex++;
                    } else {
                        // Fallback if we run out of letters (more than 52 unique steps)
                        letter = `?${charIndex - alphabet.length + 1}`;
                    }
                    valueMap.set(value, letter);
                }
                representativeLetters.push(letter);
            }

            // Create and append spans for each representative letter
            for (const letter of representativeLetters) {
                const newSpan = createEl('span', 'etaniAVItem', letter);
                newSpan.onclick = etaniAVItemClick;
                valuesElement.appendChild(newSpan);
            }
            break;

        case 'animate':
            // targetAnimation is a single Element
            const values = targetAnimation.getAttribute('values');
            if (values) {
                const valueArray = values.split(';');
                // Create a span for each value
                for (const val of valueArray) {
                    if (val.trim() !== '') { // Avoid creating spans for empty values (e.g., from "a;;b")
                        const newSpan = createEl('span', 'etaniAVItem', val);
                        newSpan.onclick = etaniAVItemClick;
                        valuesElement.appendChild(newSpan);
                    }
                }
            }
            break;

        case 'animateMotion':
            // targetAnimation is a single Element
            const pathValue = targetAnimation.getAttribute('path');
            let motionValue = '';

            if (pathValue) {
                // Use 'path' attribute if it exists
                motionValue = pathValue;
            } else {
                // Otherwise, find the <mpath> element and use its 'href'
                const mpathElement = targetAnimation.querySelector('mpath');
                if (mpathElement) {
                    motionValue = mpathElement.getAttribute('href') || '';
                }
            }
            
            const motionSpan = createEl('span', 'etaniAVItem', motionValue);
            motionSpan.onclick = etaniAVItemClick;
            valuesElement.appendChild(motionSpan);
            break;

        case 'set':
            // targetAnimation is a single Element
            const toValue = targetAnimation.getAttribute('to') || '';
            
            // Create a span for the 'to' attribute value
            const setSpan = createEl('span', 'etaniAVItem', toValue);
            setSpan.onclick = etaniAVItemClick;
            valuesElement.appendChild(setSpan);
            break;

        default:
            // Handle unknown tagname
            console.warn(`Unhandled animation tag: ${tagname}`);
    }
}

// helper function, get all animate element that has id
function getAllId() {
    // Collect animate elements
    const cssSelector = `
        .etdrop > use > animate[id],
        .etdrop > use > animateTransform[id],
        .etdrop > use > animateMotion[id],
        .etdrop > use > set[id],
        :scope > animate[id],
        :scope > animateTransform[id],
        :scope > animateMotion[id],
        :scope > set[id]
    `;
    let hasIdEle = etani.querySelectorAll(cssSelector);
    return Array.from(hasIdEle).map(item => item.id);
}

// helper function, get animate name from animate element
function getAnimateName(element) {
    let targetAni = getTargetAnimation(element);
    if (Array.isArray(targetAni)) targetAni = targetAni[0];
    const attrName = targetAni.getAttribute('attributeName');
    let aName;
    if (attrName === 'stroke-width') {
        aName = 'width';
    } else if (attrName === 'stroke-dasharray') {
        aName = 'writing';
    } else {
        aName = attrName;
    }
    return aName;
}

// edit begin attribute
function editBeginAttribute() {
    // get target animate element
    let targetAni = getTargetAnimation(this);
    if (Array.isArray(targetAni)) targetAni = targetAni[0];
    // input id
    const editDiv = createEl('div', 'etaniEditDiv');
    const editLabel = createEl('label', 'editLabel', 'begin = ');
    const editInput = createEl('input', 'editBeginStartInput');
    editInput.type = 'text';
    if (targetAni.hasAttribute('begin')) {
        editInput.value = targetAni.getAttribute('begin');
        const editRemove = createEl('span', 'etaniAttrRemove', '×');
        editRemove.onclick = () => {
            document.querySelector('.etaniWindow').remove();
            targetAni.removeAttribute('begin');
            updateAttr(this);
        };
        editDiv.appendChild(editRemove);
    }
    editLabel.appendChild(editInput);
    const editBeginStartSpan = createEl('span', 'editBeginSpan', ' s;');
    editLabel.appendChild(editBeginStartSpan);
    editDiv.appendChild(editLabel);
    // id list
    const idListDiv = createEl('div', 'etaniIdListDiv');
    const idListInner = createEl('div', 'etaniEditDiv');
    let allId = getAllId();
    for (let item of allId) {
        const spanIdItem = createEl('span', 'etaniIdListSpan', item);
        // TODO if (item === targetAni.id) spanIdItem.classList.add('selected');
        idListInner.appendChild(spanIdItem);
    }
    idListDiv.appendChild(idListInner);
    editDiv.appendChild(idListDiv);
    // end or begin, plus
    const endOrBeginDiv = createEl('div', 'endOrBeginDiv');
    const optionEnd = createEl('span', 'beginOption', 'end');
    optionEnd.classList.add('selected');
    const optionBegin = createEl('span', 'beginOption', 'begin');
    const beginPlus = createEl('span', 'beginPlusSpan', ' + ');
    const beginPlusInput = createEl('input', 'editBeginStartInput');
    beginPlusInput.type = 'text';
    const beginPlusUnit = createEl('span', 'beginPlusUnit', ' s');
    editInput.type = 'text';
    endOrBeginDiv.appendChild(optionEnd);
    endOrBeginDiv.appendChild(optionBegin);
    endOrBeginDiv.appendChild(beginPlus);
    endOrBeginDiv.appendChild(beginPlusInput);
    endOrBeginDiv.appendChild(beginPlusUnit);
    editDiv.appendChild(endOrBeginDiv);
    etaniWindow(editDiv, () => {
        updateAttr(this);
    });
}

// edit id attribute
function editIdAttribute() {
    // get target animate element
    let targetAni = getTargetAnimation(this);
    if (Array.isArray(targetAni)) targetAni = targetAni[0];
    // input id
    const editDiv = createEl('div', 'etaniEditDiv');
    const editLabel = createEl('label', 'editLabel', 'id = ');
    const editInput = createEl('input', 'editInput');
    editInput.type = 'text';
    if (targetAni.hasAttribute('id')) {
        editInput.value = targetAni.id;
        const editRemove = createEl('span', 'etaniAttrRemove', '×');
        editRemove.onclick = () => {
            document.querySelector('.etaniWindow').remove();
            targetAni.removeAttribute('id');
            updateAttr(this);
        };
        editDiv.appendChild(editRemove);
    }
    editLabel.appendChild(editInput);
    editDiv.appendChild(editLabel);
    editDiv.appendChild(createEl('hr'));
    // id list
    const idListDiv = createEl('div', 'etaniIdListDiv');
    let allId = getAllId();
    for (let item of allId) {
        const spanIdItem = createEl('span', 'etaniIdListSpan', item);
        if (item === targetAni.id) spanIdItem.classList.add('selected');
        idListDiv.appendChild(spanIdItem);
    }
    editDiv.appendChild(idListDiv);
    etaniWindow(editDiv, () => {
        // check id input
        if (!isValidId(editInput.value)) {
            alert('Not a valid id!');
            return;
        }
        if (allId.includes(editInput.value)) {
            alert('Dumplicate id!');
            return;
        }
        targetAni.id = editInput.value;
        updateAttr(this);
    });
}

// update animate UI attr
function updateAttr(element) {
    let targetAni = getTargetAnimation(element);
    if (Array.isArray(targetAni)) targetAni = targetAni[0];
    const etaniAnimate = element.closest('.etaniAnimate');
    const etaniOldAttr = etaniAnimate.querySelector('.etaniAnimateAttr');
    // Build .etaniAnimateAttr
    const elAnimateAttr = createEl('div', 'etaniAnimateAttr');
    // Build remove button
    const elAnimateAttrRemove = createEl('span', 'etaniAnimateAttrRemove', '×');
    elAnimateAttrRemove.onclick = elAnimateAttrRemoveClick;
    elAnimateAttr.appendChild(elAnimateAttrRemove);
    // Build Animate Name
    const aName = getAnimateName(element)
    const elAnimateName = createEl('span', 'etaniAnimateName', aName);
    elAnimateAttr.appendChild(elAnimateName);
    // Build id attribute
    if (targetAni.hasAttribute('id')) {
        const aId = 'id = ' + targetAni.id;
        const elAnimateId = createEl('span', 'etaniAnimateId', aId);
        elAnimateId.onclick = editIdAttribute;
        elAnimateAttr.appendChild(elAnimateId);
    }
    // Build dur attribute
    const aDur = 'dur = ' + targetAni.getAttribute('dur');
    const elAnimateDur = createEl('span', 'etaniAnimateDur', aDur);
    elAnimateDur.onclick = editDurAttribute;
    elAnimateAttr.appendChild(elAnimateDur);
    // Build add button
    const elAnimateAttrAdd = createEl('span', 'etaniAnimateAttrAdd', '+');
    elAnimateAttrAdd.onclick = elAnimateAttrAddClick;
    elAnimateAttr.appendChild(elAnimateAttrAdd);
    etaniOldAttr.replaceWith(elAnimateAttr);
}

// check the id is valid
function isValidId(str) {
  if (typeof str !== 'string' || str.trim() === '') return false;
  if (str.trim() !== str) return false;
  if (!/^[a-zA-Z]/.test(str)) return false;
  if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(str)) return false;
  if (/[:.]/.test(str)) return false;
  return true;
}

// Add attribute to the target animation element
function elAnimateAttrAddClick(e) {
    // handle dropdown menu
    if (this.querySelector('.etaniDropdown')) {
        if (e.target.innerHTML === 'id') {
            editIdAttribute.call(this);
        } else if (e.target.innerHTML === 'begin') {
            editBeginAttribute.call(this);
        } else if (e.target.innerHTML === 'other') {
            console.log('add other');
        }
        this.querySelector('.etaniDropdown').remove();
        return;
    }
    // open dropdown menu
    const etaniDropdown = createEl('div', 'etaniDropdown');
    etaniDropdown.style.top = this.getBoundingClientRect().height + 'px';
    let targetAni = getTargetAnimation(this);
    if (Array.isArray(targetAni)) targetAni = targetAni[0];
    const attrObj = Array.from(targetAni.attributes).reduce((obj, attr) => {
        obj[attr.name] = attr.value;
        return obj;
    }, {});
    const animationProps = Object.keys(attrObj);
    if (!animationProps.includes('id')) {
        const itemid = createEl('div', 'etaniDropdownItem', 'id');
        etaniDropdown.appendChild(itemid);
    }
    if (!animationProps.includes('begin')) {
        const itemBegin = createEl('div', 'etaniDropdownItem', 'begin');
        etaniDropdown.appendChild(itemBegin);
    }
    const itemOther = createEl('div', 'etaniDropdownItem', 'other');
    etaniDropdown.appendChild(itemOther);
    this.appendChild(etaniDropdown);
}

// remove the target animation element
function elAnimateAttrRemoveClick() {
    const targetAnimation = getTargetAnimation(this);
    if (Array.isArray(targetAnimation)) {
        targetAnimation[2].remove();
        targetAnimation[1].remove();
        targetAnimation[0].remove();
    } else {
        targetAnimation.remove();
    }
    this.closest('.etaniAnimate').remove();
    updateEtaniResult();
}

// edit Animate Attribute
function editDurAttribute() {
    const targetAnimation = getTargetAnimation(this);
    let editDurValue;
    if (Array.isArray(targetAnimation)) {
        editDurValue = targetAnimation[0].getAttribute('dur');
    } else {
        editDurValue = targetAnimation.getAttribute('dur');
    }
    editDurValue = editDurValue.replace('s', '');

    const editDur = createEl('div', 'etaniEditDur');
    const editDurLabel = createEl('label', 'editDurLabel', 'dur = ');
    const editDurInput = createEl('input', 'editDurInput');
    editDurInput.type = 'text';
    editDurInput.value = editDurValue;
    const editDurSpan = createEl('span', 'editDurSpan', ' s');
    editDurLabel.appendChild(editDurInput);
    editDurLabel.appendChild(editDurSpan);
    editDur.appendChild(editDurLabel);
    etaniWindow(editDur, () => {
        let setDurValue = document.querySelector('.editDurInput').value;
        setDurValue = setDurValue + 's';
        if (Array.isArray(targetAnimation)) {
            targetAnimation[0].setAttribute('dur', setDurValue);
            targetAnimation[1].setAttribute('dur', setDurValue);
            targetAnimation[2].setAttribute('dur', setDurValue);
        } else {
            targetAnimation.setAttribute('dur', setDurValue);
        }
        this.textContent = 'dur = ' + setDurValue;
        updateEtaniResult();
    });
}

// get values from targetAnimation
function getValues(targetAnimation) {
    if (!Array.isArray(targetAnimation)) {
        if (targetAnimation.tagName === 'animate') {
            return targetAnimation.getAttribute('values').split(';');
        } else if (targetAnimation.tagName === 'animateMotion') {
            if (targetAnimation.hasAttribute('path')) {
                return [targetAnimation.getAttribute('path')];
            } else if (targetAnimation.querySelector('mpath')) {
                return [targetAnimation.querySelector('mpath').getAttribute('href')];
            }
        } else if (targetAnimation.tagName === 'set') {
            return [targetAnimation.getAttribute('to')];
        }
    }
    // Get values from all three transform elements
    const translateVals = (targetAnimation[0]?.getAttribute('values') || '').split(';');
    const scaleVals = (targetAnimation[1]?.getAttribute('values') || '').split(';');
    const rotateVals = (targetAnimation[2]?.getAttribute('values') || '').split(';');

    // Assume all arrays have the same length, based on the first one
    const valuesLength = translateVals.length;
    if (valuesLength === 0 || scaleVals.length !== valuesLength || rotateVals.length !== valuesLength) {
        console.warn('animateTransform value arrays have mismatched lengths or are empty.');
    }

    const combinedValues = [];
    for (let i = 0; i < valuesLength; i++) {
        // Combine corresponding values with ';'
        const combined = `${translateVals[i] || ''};${scaleVals[i] || ''};${rotateVals[i] || ''}`;
        combinedValues.push(combined);
    }
    return combinedValues;
}

// Values item click event
function etaniAVItemClick () {
    // get standard data
    const targetAnimation = getTargetAnimation(this);
    const animateValue = this.closest('.etaniAnimateValue');
    const ctrlMode = animateValue.dataset.mode;
    const animateParent = this.closest('.etaniAnimate');
    const animateType = animateParent.dataset.animatetype;
    const tagName = animateParent.dataset.tagname;
    const dataId = this.closest('.etaniItem').dataset.id;
    const dataType = this.closest('.etaniItem').dataset.type;
    const etaniAV = animateParent.querySelector('.etaniAV');
    // Get the index of this element among its siblings
    const itemParent = this.parentNode;
    const itemIndex = Array.from(itemParent.children).indexOf(this);
    const valueArray = getValues(targetAnimation);
    const targetValue = valueArray[itemIndex];

    if (ctrlMode === 'delete') {
        if (valueArray.length > 1) {
            valueArray.splice(itemIndex, 1);
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        }
    } else if (ctrlMode === 'copy') {
        valueArray.splice(itemIndex, 0, targetValue);
        setValues(tagName, targetAnimation, valueArray, etaniAV);
    } else if (ctrlMode === 'move') {
        let sItem = itemParent.querySelector('.selected');
        if (this === sItem) {
            this.classList.remove('selected');
        } else if (sItem) {
            const sIndex = Array.from(itemParent.children).indexOf(sItem);
            const sValue = valueArray[sIndex];
            if (sIndex < itemIndex) {
                if (this.nextSibling) {
                    itemParent.insertBefore(sItem, this.nextSibling);
                } else {
                    itemParent.appendChild(sItem);
                }
                valueArray.splice(itemIndex + 1, 0, sValue);
                valueArray.splice(sIndex, 1);
            } else {
                itemParent.insertBefore(sItem, this);
                valueArray.splice(sIndex, 1);
                valueArray.splice(itemIndex, 0, sValue);
            }
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        } else {
            this.classList.add('selected');
        }
    } else if (tagName === 'animate') {
        const editDiv = createEl('div', 'etaniEditDiv');
        const editLabel = createEl('label', 'editLabel', animateType + ' = ');
        const editInput = createEl('input', 'editInput');
        editInput.type = 'text';
        editInput.value = targetValue;
        editLabel.appendChild(editInput);
        editDiv.appendChild(editLabel);
        etaniWindow(editDiv, () => {
            valueArray.splice(itemIndex, 1, editInput.value);
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        });
    } else if (tagName === 'animateTransform') {
        const editDiv = createEl('div', 'etaniEditDiv');
        // translate
        const translateRow = createEl('div', 'etaniAppendRow');
        const translateLabel = createEl('label', 'editLabel', 'translate = ');
        const translateInput = createEl('input', 'editInput');
        translateInput.type = 'text';
        translateInput.value = targetValue.split(';')[0];
        translateRow.appendChild(translateLabel);
        translateRow.appendChild(translateInput);
        editDiv.appendChild(translateRow);
        // scale
        const scaleRow = createEl('div', 'etaniAppendRow');
        const scaleLabel = createEl('label', 'editLabel', 'scale = ');
        const scaleInput = createEl('input', 'editInput');
        scaleInput.type = 'text';
        scaleInput.value = targetValue.split(';')[1];
        scaleRow.appendChild(scaleLabel);
        scaleRow.appendChild(scaleInput);
        editDiv.appendChild(scaleRow);
        // rotate
        const rotateRow = createEl('div', 'etaniAppendRow');
        const rotateLabel = createEl('label', 'editLabel', 'rotate = ');
        const rotateInput = createEl('input', 'editInput');
        rotateInput.type = 'text';
        rotateInput.value = targetValue.split(';')[2];
        rotateRow.appendChild(rotateLabel);
        rotateRow.appendChild(rotateInput);
        editDiv.appendChild(rotateRow);

        etaniWindow(editDiv, () => {
            let combineValue = translateInput.value + ';' + 
                scaleInput.value + ';' + rotateInput.value;
            valueArray.splice(itemIndex, 1, combineValue);
            setValues(tagName, targetAnimation, valueArray, etaniAV);
        });
    } else if (tagName === 'animateMotion') {
        const editDiv = createEl('div', 'etaniEditDiv');
        const editLabel = createEl('label', 'editLabel', 'path = ');
        const editInput = createEl('input', 'editInput');
        editInput.type = 'text';
        editInput.value = targetValue;
        editLabel.appendChild(editInput);
        editDiv.appendChild(editLabel);

        etaniWindow(editDiv, () => {
            setValues(tagName, targetAnimation, [editInput.value], etaniAV);
        });
    } else if (tagName === 'set') {
        const editDivTitle = createEl('div', 'etaniEditDiv');
        const editTitle = createEl('label', 'editLabel', 'set ' + animateType);
        const editDiv = createEl('div', 'etaniEditDiv');
        const editLabel = createEl('label', 'editLabel', 'to = ');
        const editInput = createEl('input', 'editInput');
        editInput.type = 'text';
        editInput.value = targetValue;
        editDivTitle.appendChild(editTitle);
        editLabel.appendChild(editInput);
        editDiv.appendChild(editDivTitle);
        editDiv.appendChild(editLabel);

        etaniWindow(editDiv, () => {
            setValues(tagName, targetAnimation, [editInput.value], etaniAV);
        });
    }
}

// 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.onclick = etaniStartClick;
});
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4001
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 190 次
联系:

Re: 正在做etani,ejtile的動畫套件

帖子 ejsoon »

gemini2.5pro

代码: 全选

/**
 * Helper function to remove a trailing 's' if it exists.
 * @param {string} str - The input string.
 * @returns {string} - The string without the trailing 's'.
 */
const removeTrailingS = (str) => {
    return str.endsWith('s') ? str.slice(0, -1) : str;
};

/**
 * Parses a specific string format into an object.
 * @param {string} str - The input string (e.g., '0s;haha.end+2s' or '10s').
 * @returns {object} - An object { begin, id, endorbegin, plus }.
 */
function generateBeginObject(str) {
    // Initialize the result object with default empty values.
    const obj = {
        begin: '',
        id: '',
        endorbegin: '',
        plus: ''
    };

    // Case 1: Simple format (no .end or .begin)
    if (!str.includes('.end') && !str.includes('.begin')) {
        obj.begin = removeTrailingS(str);
        return obj;
    }

    // Case 2: Complex format (contains .end or .begin)

    let mainPart = str; // This will hold the part before any '+'
    let plusPart = '';

    // 1. Check for 'plus' (+)
    if (str.includes('+')) {
        const plusIndex = str.lastIndexOf('+');
        mainPart = str.substring(0, plusIndex);
        plusPart = str.substring(plusIndex + 1);
        obj.plus = removeTrailingS(plusPart);
    }

    // 2. Find 'endorbegin' (.end or .begin)
    // We search in 'mainPart'
    if (mainPart.includes('.end')) {
        obj.endorbegin = '.end';
    } else if (mainPart.includes('.begin')) {
        obj.endorbegin = '.begin';
    }

    // 3. Find 'begin' and 'id'
    // We need the part of 'mainPart' *before* the 'endorbegin' string
    const endorbeginIndex = mainPart.lastIndexOf(obj.endorbegin);
    const beforePart = mainPart.substring(0, endorbeginIndex);

    if (beforePart.includes(';')) {
        // If ';' exists
        const semicolonIndex = beforePart.indexOf(';');
        obj.begin = removeTrailingS(beforePart.substring(0, semicolonIndex));
        obj.id = beforePart.substring(semicolonIndex + 1);
    } else {
        // If no ';' exists
        obj.begin = ''; // As specified
        obj.id = beforePart;
    }

    return obj;
}

/**
 * Generates a string from a specific object format.
 * @param {object} obj - The input object (e.g., { begin, id, endorbegin, plus }).
 * @returns {string} - The formatted string.
 */
function generateBeginString(obj) {
    // Rule: If 'id' is empty, it's the simple format.
    // 'endorbegin' and 'plus' are ignored.
    if (!obj.id) {
        return obj.begin + 's';
    }

    // --- Complex format (id exists) ---

    let result = '';

    // 1. Handle 'begin' and ';'
    // If 'begin' has a value, it must have come from a string with ';'.
    if (obj.begin) {
        result += obj.begin + 's;';
    }

    // 2. Handle 'id' and 'endorbegin'
    // Rule: If 'id' exists, 'endorbegin' must also exist.
    result += obj.id + obj.endorbegin;

    // 3. Handle 'plus'
    if (obj.plus) {
        result += '+' + obj.plus + 's';
    }

    return result;
}
test

代码: 全选

// --- Test generateBeginObject ---

console.log("--- Testing generateBeginObject ---");

// 範例 1 (來自您的要求)
const str1 = '0s;haha.end+2s';
console.log(`Input: '${str1}'`);
console.log(generateBeginObject(str1));
// 預期輸出: { begin: '0', id: 'haha', endorbegin: '.end', plus: '2' }

// 範例 2 (沒有 'begin' 部分)
const str2 = 'ha.begin+5s';
console.log(`Input: '${str2}'`);
console.log(generateBeginObject(str2));
// 預期輸出: { begin: '', id: 'ha', endorbegin: '.begin', plus: '5' }

// 範例 3 (沒有 'plus' 部分)
const str3 = '1s;id_only.begin';
console.log(`Input: '${str3}'`);
console.log(generateBeginObject(str3));
// 預期輸出: { begin: '1', id: 'id_only', endorbegin: '.begin', plus: '' }

// 範例 4 (簡單模式)
const str4 = '20s';
console.log(`Input: '${str4}'`);
console.log(generateBeginObject(str4));
// 預期輸出: { begin: '20', id: '', endorbegin: '', plus: '' }


// --- Test generateBeginString ---

console.log("\n--- Testing generateBeginString ---");

// 範例 1 (來自您的要求)
const obj1 = {
    begin: '',
    id: 'ha',
    endorbegin: '.begin',
    plus: '5'
};
console.log("Input:", obj1);
console.log(`Output: '${generateBeginString(obj1)}'`); // 預期: 'ha.begin+5s'

// 範例 2 (完整範例)
const obj2 = {
    begin: '0',
    id: 'haha',
    endorbegin: '.end',
    plus: '2'
};
console.log("Input:", obj2);
console.log(`Output: '${generateBeginString(obj2)}'`); // 預期: '0s;haha.end+2s'

// 範例 3 (沒有 'plus')
const obj3 = {
    begin: '1',
    id: 'id_only',
    endorbegin: '.begin',
    plus: ''
};
console.log("Input:", obj3);
console.log(`Output: '${generateBeginString(obj3)}'`); // 預期: '1s;id_only.begin'

// 範例 4 (簡單模式)
const obj4 = {
    begin: '20',
    id: '',
    endorbegin: '', // 即使有值也會被忽略
    plus: ''        // 即使有值也會被忽略
};
console.log("Input:", obj4);
console.log(`Output: '${generateBeginString(obj4)}'`); // 預期: '20s'
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复

在线用户

正浏览此版面之用户: Amazon [Bot] 和 1 访客