正在做etani,ejtile的動畫套件

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

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

帖子 ejsoon »

代码: 全选

對以下js代碼:
```
// Global variable to hold the SVG clone and ensure state is maintained
let etaniClone = null;

// --- CSS STYLES ---

function addDynamicStyles() {
    if (document.getElementById('dynamic-et-styles')) {
        return;
    }

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

        /* II. Styles for etaniinner, etaniCtrl, etaniCol, etaniResult */
        .etaniinner {
            margin-top: 10px;
        }
        
        .etaniCtrl {
            margin-bottom: 10px; 
            clear: both;
            padding: 5px;
            border: 1px solid #c0c0c0;
            text-align: center; /* To center etaniContent, etaniSetting, etaniAllAppend */
        }
        
        /* 要求一:etaniContent, etaniSetting, etaniAllAppend 樣式 */
        .etaniContent, .etaniSetting, .etaniAllAppend {
            display: inline-block;
            vertical-align: top;
            padding: 5px;
            border: 1px solid #ccc; /* 灰色邊框 */
            margin: 0 5px 5px 5px;
            text-align: left;
        }

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

        /* IV. Styles for etaniResult */
        .etaniResult {
            text-align: center;
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #bbb;
            overflow: auto;
        }
        
        /* ------------------ CONTROL & BUTTON STYLES ------------------ */

        /* Styles for control links (Center button) */
        .etaniContent a {
            display: inline-block;
            margin: 0 5px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 14px;
        }
        
        .etaniCenter {
            border: 1px solid green;
            color: green;
        }
        
        .etaniContentHTML {
            border: 1px solid #0099ff;
            color: #0099ff;
            margin-right: 15px;
        }

        /* 要求三:etaniAllAppend buttons */
        .etaniAllAppend button {
            padding: 5px 10px;
            font-size: 16px;
            margin: 0 5px;
            cursor: pointer;
            border: 1px solid #333;
            background-color: #fff;
        }
        
        /* 要求二:自定義 Radio 按鈕樣式 */
        .etaniSettingMode {
            display: inline-block;
            cursor: pointer;
            padding: 4px 8px;
            margin: 0 3px;
            font-size: 14px;
            border: 1px solid #888;
            background-color: #eee;
            color: #333;
            user-select: none;
        }

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

        /* ------------------ RESULT & ANIMAION ITEM STYLES ------------------ */

        .etaniResultDR {
            text-align: center;
            margin-bottom: 10px;
        }

        /* 要求一:下載和重命名連結字號16px */
        .etaniResultDownload, .etaniResultRename {
            display: inline-block;
            margin-right: 15px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 16px; 
        }
        
        /* .etaniResultDownload default style */
        .etaniResultDownload {
            border: 1px solid blue;
            color: blue;
        }

        /* .etaniResultRename style (要求一:顏色改為 brown) */
        .etaniResultRename {
            border: 1px solid brown;
            color: brown;
        }

        .etaniResultImage {
            display: block;
            max-width: 480px;
            width: 100%;
            height: auto;
            margin: 0 auto 10px auto;
            border: 1px solid #000;
        }
        
        /* 要求二:最小字號 12px */
        .etaniResultSize {
            display: inline-block;
            margin-left: 10px;
            font-size: 12px; 
            color: #555;
        }
        
        /* etaniItem structure (Float layout) */
        .etaniItem {
            min-height: 48px;
            border: 1px solid #ccc;
            box-sizing: border-box;
            width: 100%;
            margin-bottom: -1px;
            overflow: auto;
        }
        
        .etaniItemLeft {
            float: left; 
            width: 60px;
            min-height: 48px;
            border-right: 1px solid #ccc;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            background-color: #e8e8e8;
            padding: 2px 0;
        }
        
        .etaniItemRight {
            margin-left: 60px; 
            padding: 7px;
            min-height: 48px; 
            background-color: #fff;
        }

        /* 要求二:最小字號 12px */
        .tileid {
            text-align: center;
            font-size: 12px; 
            word-break: break-all;
            padding-top: 2px;
        }
        
        /* ------------------ ANIMATE CONTROLS (4.1) ------------------ */

        .etaniAnimate {
            border: 1px solid #999;
            padding: 5px;
            margin-bottom: 5px;
        }

        .etaniAnimateName {
            display: inline-block;
            padding: 2px 5px;
            background-color: #555; /* 灰底 */
            color: white; /* 白字 */
            margin-right: 10px;
            font-size: 12px; /* 最小字號 12px */
        }

        .etaniAnimateDur {
            display: inline-block;
            margin-right: 10px;
            font-size: 14px;
        }

        .etaniAnimateValue {
            margin-top: 5px;
            overflow: auto;
        }

        .etaniAVAdd {
            display: inline-block;
            width: 24px;
            height: 24px;
            line-height: 24px;
            text-align: center;
            background-color: #a7fca7; /* 淺綠色 */
            border: 1px solid #71c371; /* 細線近淺綠色 */
            margin-right: 5px;
            cursor: pointer;
            box-sizing: border-box;
            font-size: 20px; /* For plus sign SVG */
        }
        
        .etaniAVLabel {
            font-size: 14px;
            margin-right: 5px;
        }

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

        .etaniAVItem {
            display: inline-block;
            width: 24px;
            height: 24px;
            background-color: #ff9933; /* 暗橙色 */
            border: 1px dashed #00bfff; /* 虛線天藍色 */
            margin: 0 5px;
            box-sizing: border-box;
            cursor: pointer;
        }
        
        /* ------------------ MODAL/POPUP STYLES ------------------ */
        .modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.5);
            z-index: 1000;
        }
        .modal-content {
            position: fixed;
            width: 98%;
            height: 48%;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 10px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
            z-index: 1001;
            box-sizing: border-box;
        }
        .modal-content textarea {
            width: 100%;
            height: calc(100% - 30px); /* Adjust based on button/header height */
            resize: none;
            border: 1px solid #ccc;
            font-size: 12px;
            box-sizing: border-box;
        }
        .modal-close {
            position: absolute;
            top: 5px;
            right: 10px;
            font-size: 24px;
            cursor: pointer;
            color: #333;
        }
    `;

    document.head.appendChild(styleSheet);
}

// --- UTILITY FUNCTIONS ---

function svgToBase64(svgString) {
    const encoder = new TextEncoder();
    const svgBytes = encoder.encode(svgString);
    const byteString = String.fromCharCode.apply(null, svgBytes);
    const base64 = btoa(byteString);
    return `data:image/svg+xml;base64,${base64}`;
}

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

function updateEtaniResult() {
    // ... (保持不變的 updateEtaniResult 函數) ...
    if (!etaniClone) return;

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

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

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

function handleCenterClick(e) {
    e.preventDefault();
    if (etaniClone) {
        const etdrop = etaniClone.querySelector('.etdrop');
        if (etdrop) {
            etdrop.setAttribute('transform', 'translate(240,240) scale(1,1)');
            updateEtaniResult();
        }
    }
}

// --- NEW HANDLERS ---

/**
 * 處理「+」按鈕點擊事件 (要求六)
 * 增加一個新的 animateTransform value,使用 svg#etmain 中對應 <use/> 的當前 transform 值。
 */
function handleAVAddClick(e, itemIndex, useElementId) {
    e.preventDefault();
    if (!etaniClone) return;

    // 1. 找到 svg#etmain 中對應的 <use> 元素,獲取其 transform 值 (作為新的 value)
    const originalUseElement = document.querySelector(`.etdrop use[href="#${useElementId}"]`);
    const newTransformValue = originalUseElement ? originalUseElement.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)' : 'translate(0,0) scale(1,1) rotate(0)';
    
    // 從 transform 字符串中提取 translate, scale, rotate 的值
    const getTransformValue = (type) => {
        const match = newTransformValue.match(new RegExp(`${type}\\(([^)]+)\\)`, 'i'));
        return match ? match[1].split(/[,\s]+/).join(',') : (type === 'scale' ? '1,1' : '0');
    };
    
    const translateValue = getTransformValue('translate');
    const scaleValue = getTransformValue('scale');
    const rotateValue = getTransformValue('rotate');
    
    // 2. 找到 etani_clone 中對應的 <use> 元素
    const cloneUseElement = etaniClone.querySelector(`use[href="#${useElementId}"]`);

    if (cloneUseElement) {
        // 3. 更新三個 animateTransform 標籤的 values
        const animates = cloneUseElement.querySelectorAll('animateTransform');
        animates.forEach(animate => {
            const type = animate.getAttribute('type').toLowerCase();
            let currentValue = animate.getAttribute('values') || '';
            let newValue = '';
            
            if (type === 'translate') {
                newValue = translateValue;
            } else if (type === 'scale') {
                newValue = scaleValue;
            } else if (type === 'rotate') {
                newValue = rotateValue;
            }
            
            // Append the new value to the existing string, separated by a semicolon
            animate.setAttribute('values', (currentValue ? currentValue + ';' : '') + newValue);
        });

        // 4. 在 .etaniItemRight 中增加一個新的 .etaniAVItem
        const etaniItemRight = e.currentTarget.closest('.etaniItemRight');
        const etaniAV = etaniItemRight.querySelector('.etaniAV');
        if (etaniAV) {
            const newAVItem = document.createElement('span');
            newAVItem.className = 'etaniAVItem';
            etaniAV.appendChild(newAVItem);
        }

        // 5. 即時更新 SVG
        updateEtaniResult();
    }
}


/**
 * 處理「Transform」按鈕點擊事件 (要求四)
 * 向所有 <use/> 元素添加三個 animateTransform 標籤,並在 .etaniItemRight 中增加控制項。
 */
function handleAllAppendTransformClick() {
    if (!etaniClone) return;

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

    etdropUses.forEach((useElement, i) => {
        const useId = useElement.getAttribute('href').substring(1);
        const itemRight = etaniItemRights[i];
        
        if (!itemRight || itemRight.querySelector('.etaniAnimate')) return; // 避免重複添加

        // 4.2:為 <use/> 添加 animateTransform 標籤
        const baseAnimate = (type, values) => {
            const animate = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
            animate.setAttribute('attributeName', 'transform');
            animate.setAttribute('attributeType', 'XML');
            animate.setAttribute('type', type);
            animate.setAttribute('values', values);
            animate.setAttribute('dur', '1s');
            animate.setAttribute('fill', 'freeze');
            animate.setAttribute('additive', 'sum');
            return animate;
        };

        // 初始值設置為 '0',因為 additive="sum",表示相對位移/縮放/旋轉
        useElement.appendChild(baseAnimate('translate', '0,0'));
        useElement.appendChild(baseAnimate('scale', '1')); // scale 初始值應為 1 (不變)
        useElement.appendChild(baseAnimate('rotate', '0'));


        // 4.1:為 .etaniItemRight 增加 .etaniAnimate 控制項
        
        // 外部 wrapper
        const etaniAnimate = document.createElement('div');
        etaniAnimate.className = 'etaniAnimate';

        // Animate Name (灰色背景白色字體)
        const nameSpan = document.createElement('span');
        nameSpan.className = 'etaniAnimateName';
        nameSpan.textContent = 'transform';
        etaniAnimate.appendChild(nameSpan);

        // Animate Dur (dur: n)
        const durSpan = document.createElement('span');
        durSpan.className = 'etaniAnimateDur';
        durSpan.textContent = 'dur: 1s'; // 初始值 1s
        etaniAnimate.appendChild(durSpan);

        // Animate Value container
        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        // AV Add (加號 SVG)
        const avAddSpan = document.createElement('span');
        avAddSpan.className = 'etaniAVAdd';
        avAddSpan.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" 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>`;
        avAddSpan.title = 'Add Transform Value';
        
        // 綁定事件:傳入當前的 item 索引和 use 元素的 ID
        avAddSpan.addEventListener('click', (e) => handleAVAddClick(e, i, useId));

        // AV Label
        const avLabelSpan = document.createElement('span');
        avLabelSpan.className = 'etaniAVLabel';
        avLabelSpan.textContent = 'values : ';

        // AV Container
        const avDiv = document.createElement('div');
        avDiv.className = 'etaniAV';
        
        // AV Item (最初只含一個)
        const avItemSpan = document.createElement('span');
        avItemSpan.className = 'etaniAVItem';
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avAddSpan);
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

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

    // 即時更新 SVG
    updateEtaniResult();
}

/**
 * 處理 Setting Mode (Freeze/Repeat) 切換 (要求五)
 */
function handleSettingModeChange(mode) {
    if (!etaniClone) return;

    const animates = etaniClone.querySelectorAll('animateTransform');
    const isRepeat = mode === 'repeat';

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

    // 更新 active 狀態
    document.querySelector('.etaniSettingFreeze').classList.toggle('active', !isRepeat);
    document.querySelector('.etaniSettingRepeat').classList.toggle('active', isRepeat);

    updateEtaniResult();
}

/**
 * 處理 HTML 彈出視窗 (要求七)
 */
function handleContentHTMLClick(e) {
    e.preventDefault();
    if (!etaniClone) return;

    const overlay = document.createElement('div');
    overlay.className = 'modal-overlay';
    
    const content = document.createElement('div');
    content.className = 'modal-content';
    
    const close = document.createElement('span');
    close.className = 'modal-close';
    close.innerHTML = '&times;'; // 'x' icon
    
    const textarea = document.createElement('textarea');
    textarea.value = etaniClone.outerHTML; // 輸出 etani_clone 的 outerHTML

    // 關閉邏輯
    const closeModal = () => {
        document.body.removeChild(overlay);
        document.body.removeChild(content);
    };
    close.onclick = closeModal;
    overlay.onclick = closeModal; // 點擊背景也關閉

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


// --- MAIN STRUCTURE CREATION ---

function createEtaniInner(etaniouter) {
    // 5.1, 5.2, 5.3 Clone, remove .etwait, update ID
    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etaniClone = originalSvg.cloneNode(true);
    const etwaitElement = etaniClone.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    etaniClone.id = 'etmainani';


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

    // Create div.etaniCtrl
    const etaniCtrl = document.createElement('div');
    etaniCtrl.className = 'etaniCtrl';
    
    // 要求七:a.etaniContentHTML
    const etaniContent = document.createElement('div');
    etaniContent.className = 'etaniContent';
    
    const contentHTMLLink = document.createElement('a');
    contentHTMLLink.className = 'etaniContentHTML';
    contentHTMLLink.textContent = 'Show HTML';
    contentHTMLLink.href = '#';
    contentHTMLLink.addEventListener('click', handleContentHTMLClick);
    etaniContent.appendChild(contentHTMLLink);

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


    // 要求一、二:div.etaniSetting (Radio Buttons)
    const etaniSetting = document.createElement('div');
    etaniSetting.className = 'etaniSetting';
    
    const freezeRadio = document.createElement('span');
    freezeRadio.className = 'etaniSettingMode etaniSettingFreeze active';
    freezeRadio.textContent = 'Freeze';
    freezeRadio.setAttribute('data-mode', 'freeze');
    freezeRadio.addEventListener('click', () => handleSettingModeChange('freeze'));
    
    const repeatRadio = document.createElement('span');
    repeatRadio.className = 'etaniSettingMode etaniSettingRepeat';
    repeatRadio.textContent = 'Repeat';
    repeatRadio.setAttribute('data-mode', 'repeat');
    repeatRadio.addEventListener('click', () => handleSettingModeChange('repeat'));

    etaniSetting.appendChild(freezeRadio);
    etaniSetting.appendChild(repeatRadio);
    etaniCtrl.appendChild(etaniSetting);


    // 要求一、三:div.etaniAllAppend (Buttons)
    const etaniAllAppend = document.createElement('div');
    etaniAllAppend.className = 'etaniAllAppend';

    const transformButton = document.createElement('button');
    transformButton.className = 'etaniAllAppendTransform';
    transformButton.textContent = 'transform';
    transformButton.addEventListener('click', handleAllAppendTransformClick);
    
    const opacityButton = document.createElement('button');
    opacityButton.className = 'etaniAllAppendOpacity';
    opacityButton.textContent = 'opacity';
    // opacityButton.addEventListener('click', handleAllAppendOpacityClick); // Placeholder
    
    etaniAllAppend.appendChild(transformButton);
    etaniAllAppend.appendChild(opacityButton);
    etaniCtrl.appendChild(etaniAllAppend);
    

    // Create div.etaniCol (Tile list)
    const etaniCol = document.createElement('div');
    etaniCol.className = 'etaniCol';

    // Create div.etaniResult
    const etaniResult = document.createElement('div');
    etaniResult.className = 'etaniResult';
    
    // Result elements (img, download, rename, size) ... (略,與前次相同)
    const resultImg = document.createElement('img');
    resultImg.className = 'etaniResultImage';
    resultImg.alt = 'Rendered Ejtile Animation SVG';

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

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

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

    resultDRDiv.appendChild(downloadLink);
    resultDRDiv.appendChild(renameLink);
    
    etaniResult.appendChild(resultImg);
    etaniResult.appendChild(resultDRDiv);
    etaniResult.appendChild(sizeSpan);
    
    // Append children to etaniinner
    etaniinner.appendChild(etaniCtrl);
    etaniinner.appendChild(etaniCol);
    etaniinner.appendChild(etaniResult);

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

    // III, IV, V, VI, VII. Populate etaniCol
    const etdropUses = document.querySelectorAll('.etdrop use');
    const etwaitGroups = document.querySelectorAll('.etwait g');

    etdropUses.forEach((useElement, i) => {
        // (Tile processing logic - 保持不變)
        const tileid = useElement.getAttribute('href').substring(1);
        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 originalTile = document.querySelector(`defs g#${tileid}`);
        
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';

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

        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        
        if (originalTile) {
            const tileclone = originalTile.cloneNode(true);
            tileclone.removeAttribute('id');
            if (etwaittransform) tileclone.setAttribute('transform', etwaittransform);
            if (etwaitfill) tileclone.setAttribute('fill', etwaitfill);
            if (etwaitstroke) tileclone.setAttribute('stroke', etwaitstroke);
            if (etwaitstrokeWidth) tileclone.setAttribute('stroke-width', etwaitstrokeWidth);
            
            const svgWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            svgWrapper.setAttribute('width', '40');
            svgWrapper.setAttribute('height', '40');
            svgWrapper.setAttribute('version', '1.1');
            svgWrapper.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
            svgWrapper.setAttribute('xmlns:svg', 'http://www.w3.org/2000/svg');
            svgWrapper.className = 'etanitileimg';
            
            svgWrapper.appendChild(tileclone);
            etaniItemLeft.appendChild(svgWrapper);
        }

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

        etaniItem.appendChild(etaniItemLeft);
        etaniItem.appendChild(etaniItemRight);
        etaniCol.appendChild(etaniItem);
    });
    
    // 5. Update the result section immediately
    updateEtaniResult();
}

// --- INITIALIZATION ---

function toggleAnimation(event) {
    const etanibutton = event.currentTarget;
    const etaniouter = etanibutton.parentNode;
    const etaniinner = document.getElementById('etaniinner');

    if (etanibutton.textContent === 'Animate it') {
        etanibutton.textContent = 'Close Ejtile Ani';
        createEtaniInner(etaniouter);
        
    } else if (etanibutton.textContent === 'Close Ejtile Ani') {
        etanibutton.textContent = 'Animate it';
        if (etaniinner) {
            etaniinner.remove();
        }
        etaniClone = null;
    }
}


window.addEventListener('load', () => {
    addDynamicStyles();

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

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

        etaniouter.appendChild(etanibutton);
        etmainouter.parentNode.insertBefore(etaniouter, etmainouter.nextSibling);
        
    } else {
        console.error('Div with ID "etmainouter" not found.');
    }
});
```

改動:

一,前面的代碼中,etani_clone被寫成etaniClone,現在要改回etani_clone。

二,當新增animateTransform的時候,這三個標籤的初始值,是
translate(0,0)
scale(1,1)
rotate(0)

三,當在animateTransform增加value時,設它的三個標籤的值的計算結果為translate(v1) scale(v2) rotate(v3)。它的計算方法是:

首先取出它在svg#etmain中的.etdrop中的<use/>的transform="translate(cv1) scale(cv2) rotate(cv3)"的三個值,之後分別減去在etani_clone中的.etdrop中的<use/>的transform="translate(ov1) scale(ov2) rotate(ov3)"的三個值。

v1=cv1-ov1
v2=cv2/ov2
v3=cv3-ov3

四,當新增animateTransform時,程式要在etani_clone中的.etdrop中的每一個<use/>的外部增加一個<g></g>,animateTransform的translate改為放在它裡面。

五,modal-close的字體大小設為48px,line-height設為1,top為0。

用繁體中文回覆。在代碼中所有注釋都要用英文。

代码: 全选

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

// --- CSS STYLES ---

function addDynamicStyles() {
    if (document.getElementById('dynamic-et-styles')) {
        return;
    }

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

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

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

        /* IV. Styles for etaniResult */
        .etaniResult {
            text-align: center;
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #bbb;
            overflow: auto;
        }
        
        /* ------------------ CONTROL & BUTTON STYLES ------------------ */

        /* Styles for control links (Center button) */
        .etaniContent a {
            display: inline-block;
            margin: 0 5px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 14px;
        }
        
        .etaniCenter {
            border: 1px solid green;
            color: green;
        }
        
        .etaniContentHTML {
            border: 1px solid #0099ff;
            color: #0099ff;
            margin-right: 15px;
        }

        /* Requirement 3: etaniAllAppend buttons */
        .etaniAllAppend button {
            padding: 5px 10px;
            font-size: 16px;
            margin: 0 5px;
            cursor: pointer;
            border: 1px solid #333;
            background-color: #fff;
        }
        
        /* Requirement 2: Custom Radio button styles */
        .etaniSettingMode {
            display: inline-block;
            cursor: pointer;
            padding: 4px 8px;
            margin: 0 3px;
            font-size: 14px;
            border: 1px solid #888;
            background-color: #eee;
            color: #333;
            user-select: none;
        }

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

        /* ------------------ RESULT & ANIMAION ITEM STYLES ------------------ */

        .etaniResultDR {
            text-align: center;
            margin-bottom: 10px;
        }

        /* Requirement 1: Download and rename link font size 16px */
        .etaniResultDownload, .etaniResultRename {
            display: inline-block;
            margin-right: 15px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 16px; 
        }
        
        /* .etaniResultDownload default style */
        .etaniResultDownload {
            border: 1px solid blue;
            color: blue;
        }

        /* .etaniResultRename style (Requirement 1: color changed to brown) */
        .etaniResultRename {
            border: 1px solid brown;
            color: brown;
        }

        .etaniResultImage {
            display: block;
            max-width: 480px;
            width: 100%;
            height: auto;
            margin: 0 auto 10px auto;
            border: 1px solid #000;
        }
        
        /* Requirement 2: Minimum font size 12px */
        .etaniResultSize {
            display: inline-block;
            margin-left: 10px;
            font-size: 12px; 
            color: #555;
        }
        
        /* etaniItem structure (Float layout) */
        .etaniItem {
            min-height: 48px;
            border: 1px solid #ccc;
            box-sizing: border-box;
            width: 100%;
            margin-bottom: -1px;
            overflow: auto;
        }
        
        .etaniItemLeft {
            float: left; 
            width: 60px;
            min-height: 48px;
            border-right: 1px solid #ccc;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            background-color: #e8e8e8;
            padding: 2px 0;
        }
        
        .etaniItemRight {
            margin-left: 60px; 
            padding: 7px;
            min-height: 48px; 
            background-color: #fff;
        }

        /* Requirement 2: Minimum font size 12px */
        .tileid {
            text-align: center;
            font-size: 12px; 
            word-break: break-all;
            padding-top: 2px;
        }
        
        /* ------------------ ANIMATE CONTROLS (4.1) ------------------ */

        .etaniAnimate {
            border: 1px solid #999;
            padding: 5px;
            margin-bottom: 5px;
        }

        .etaniAnimateName {
            display: inline-block;
            padding: 2px 5px;
            background-color: #555; /* Dark grey background */
            color: white; /* White text */
            margin-right: 10px;
            font-size: 12px; /* Minimum font size 12px */
        }

        .etaniAnimateDur {
            display: inline-block;
            margin-right: 10px;
            font-size: 14px;
        }

        .etaniAnimateValue {
            margin-top: 5px;
            overflow: auto;
        }

        .etaniAVAdd {
            display: inline-block;
            width: 24px;
            height: 24px;
            line-height: 24px;
            text-align: center;
            background-color: #a7fca7; /* Light green */
            border: 1px solid #71c371; /* Thin line near light green */
            margin-right: 5px;
            cursor: pointer;
            box-sizing: border-box;
            font-size: 20px; /* For plus sign SVG */
        }
        
        .etaniAVLabel {
            font-size: 14px;
            margin-right: 5px;
        }

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

        .etaniAVItem {
            display: inline-block;
            width: 24px;
            height: 24px;
            background-color: #ff9933; /* Dark orange */
            border: 1px dashed #00bfff; /* Dashed sky blue */
            margin: 0 5px;
            box-sizing: border-box;
            cursor: pointer;
        }
        
        /* ------------------ MODAL/POPUP STYLES ------------------ */
        .modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.5);
            z-index: 1000;
        }
        .modal-content {
            position: fixed;
            width: 98%;
            height: 48%;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 10px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
            z-index: 1001;
            box-sizing: border-box;
        }
        .modal-content textarea {
            width: 100%;
            height: calc(100% - 50px); /* Adjusted for larger close button */
            resize: none;
            border: 1px solid #ccc;
            font-size: 12px;
            box-sizing: border-box;
        }
        /* Requirement 5: modal-close font size 48px, line-height 1, top 0 */
        .modal-close {
            position: absolute;
            top: 0;
            right: 10px;
            font-size: 48px;
            line-height: 1; 
            cursor: pointer;
            color: #333;
        }
    `;

    document.head.appendChild(styleSheet);
}

// --- UTILITY FUNCTIONS ---

/**
 * Converts an SVG string to a Base64 data URL.
 * @param {string} svgString The SVG XML string.
 * @returns {string} The Base64 data URL.
 */
function svgToBase64(svgString) {
    // Note: Using encodeURIComponent and btoa for broader compatibility, 
    // although TextEncoder/fromCharCode is often faster.
    const base64 = btoa(unescape(encodeURIComponent(svgString)));
    return `data:image/svg+xml;base64,${base64}`;
}

/**
 * Formats byte size into human-readable string (e.g., KB, MB).
 * @param {number} bytes The size in bytes.
 * @returns {string} The formatted string.
 */
function formatBytes(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

/**
 * Parses transform string to get individual transform values.
 * @param {string} transformString The transform attribute string.
 * @returns {{translate: string, scale: string, rotate: string}} Parsed values.
 */
function parseTransform(transformString) {
    const defaultTransform = { translate: '0,0', scale: '1,1', rotate: '0' };
    if (!transformString) return defaultTransform;

    const transform = {};

    const getMatch = (type) => {
        const match = transformString.match(new RegExp(`${type}\\(([^)]+)\\)`, 'i'));
        return match ? match[1].split(/[,\s]+/).join(',') : null;
    };

    transform.translate = getMatch('translate') || defaultTransform.translate;
    transform.scale = getMatch('scale') || defaultTransform.scale;
    transform.rotate = getMatch('rotate') || defaultTransform.rotate;

    return transform;
}


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

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

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

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

/**
 * Handler for the 'Center' button click. Resets the .etdrop transform.
 * @param {Event} e The click event.
 */
function handleCenterClick(e) {
    e.preventDefault();
    if (etani_clone) {
        const etdrop = etani_clone.querySelector('.etdrop');
        if (etdrop) {
            etdrop.setAttribute('transform', 'translate(240,240) scale(1,1)');
            updateEtaniResult();
        }
    }
}

// --- NEW HANDLERS ---

/**
 * Calculates the required *additive* transform value for a given type.
 * @param {string} type The transform type ('translate', 'scale', 'rotate').
 * @param {string} originalValue The transform value from the original <use/> (cv).
 * @param {string} currentBaseValue The current base transform value from the etani_clone <use/> (ov).
 * @returns {string} The new additive value (v).
 */
function calculateAdditiveValue(type, originalValue, currentBaseValue) {
    if (type === 'translate') {
        const ovCoords = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
        const cvCoords = originalValue.split(',').map(c => parseFloat(c.trim()));
        // v1 = cv1 - ov1, v2 = cv2 - ov2
        return `${cvCoords[0] - ovCoords[0]},${cvCoords[1] - ovCoords[1]}`;
    } else if (type === 'scale') {
        const ovScales = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
        const cvScales = originalValue.split(',').map(c => parseFloat(c.trim()));
        // v1 = cv1 / ov1, v2 = cv2 / ov2. Note: scale needs to handle single vs double value.
        const v1 = (cvScales[0] / ovScales[0]).toFixed(4);
        const v2 = (cvScales.length > 1 ? (cvScales[1] / ovScales[1]) : v1).toFixed(4);
        return v1 === v2 ? v1 : `${v1},${v2}`;
    } else if (type === 'rotate') {
        const ovAngle = parseFloat(currentBaseValue.trim());
        const cvAngle = parseFloat(originalValue.trim());
        // v3 = cv3 - ov3
        return (cvAngle - ovAngle).toFixed(4);
    }
    return '';
}

/**
 * Handles the click event for the '+' button (Requirement 6).
 * Adds a new animateTransform value using the current transform of the corresponding <use/> in svg#etmain.
 * @param {Event} e The click event.
 * @param {number} itemIndex The index of the item (unused in this implementation but kept for context).
 * @param {string} useElementId The ID of the original <g> element referenced by the <use/>.
 */
function handleAVAddClick(e, itemIndex, useElementId) {
    e.preventDefault();
    if (!etani_clone) return;

    // 1. Get the current transform of the original <use/> element in svg#etmain
    const originalUseElement = document.querySelector(`.etdrop use[href="#${useElementId}"]`);
    if (!originalUseElement) return;

    const originalTransformString = originalUseElement.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)';
    const originalTransforms = parseTransform(originalTransformString);
    
    // 2. Find the corresponding <g> wrapper in etani_clone
    const cloneGWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useElementId}"]`);
    if (!cloneGWrapper) return;
    
    // 3. Get the *base* transform of the <use> element inside the wrapper (which holds the initial values for the animation).
    const cloneUseElement = cloneGWrapper.querySelector('use');
    const currentBaseTransformString = cloneUseElement.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)';
    const currentBaseTransforms = parseTransform(currentBaseTransformString);

    // 4. Update the three animateTransform tags' values
    const animates = cloneGWrapper.querySelectorAll('animateTransform');
    animates.forEach(animate => {
        const type = animate.getAttribute('type').toLowerCase();
        let currentValueString = animate.getAttribute('values') || '';
        let newValue = '';
        
        // Calculate the *additive* difference based on the requirement: v = c - o (or c/o for scale)
        if (type === 'translate') {
            newValue = calculateAdditiveValue('translate', originalTransforms.translate, currentBaseTransforms.translate);
        } else if (type === 'scale') {
            newValue = calculateAdditiveValue('scale', originalTransforms.scale, currentBaseTransforms.scale);
        } else if (type === 'rotate') {
            newValue = calculateAdditiveValue('rotate', originalTransforms.rotate, currentBaseTransforms.rotate);
        }
        
        // Append the new value to the existing string, separated by a semicolon
        animate.setAttribute('values', (currentValueString ? currentValueString + ';' : '') + newValue);
    });

    // 5. Add a new .etaniAVItem in .etaniItemRight
    const etaniItemRight = e.currentTarget.closest('.etaniItemRight');
    const etaniAV = etaniItemRight.querySelector('.etaniAV');
    if (etaniAV) {
        const newAVItem = document.createElement('span');
        newAVItem.className = 'etaniAVItem';
        etaniAV.appendChild(newAVItem);
    }

    // 6. Update the SVG result
    updateEtaniResult();
}


/**
 * Handles the click event for the 'Transform' button (Requirement 4).
 * Adds three animateTransform tags to all <use/> elements and adds controls to .etaniItemRight.
 */
function handleAllAppendTransformClick() {
    if (!etani_clone) return;

    const etdropUses = document.querySelectorAll('#etmain .etdrop use'); // Select original <use> elements for their href
    const etaniItemRights = document.querySelectorAll('.etaniItemRight');

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

        // 1. Find the corresponding <use> element in etani_clone
        let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;
        
        // Check if transform controls already exist to prevent duplication
        if (itemRight.querySelector('.etaniAnimate')) return; 

        // 2. Wrap the <use/> element in a <g> tag (Requirement 4)
        const gWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        gWrapper.setAttribute('data-use-href', `#${useId}`); // Use a data attribute to link it back
        
        // Preserve the base transform on the <use> element for additive calculation
        const baseTransform = cloneUseElement.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)';
        cloneUseElement.setAttribute('transform', baseTransform);
        
        cloneUseElement.parentNode.insertBefore(gWrapper, cloneUseElement);
        gWrapper.appendChild(cloneUseElement);
        
        // 3. Add animateTransform tags to the <g> wrapper
        const baseAnimate = (type, initialValue) => {
            const animate = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
            animate.setAttribute('attributeName', 'transform');
            animate.setAttribute('attributeType', 'XML');
            animate.setAttribute('type', type);
            // Initial values: translate(0,0), scale(1) or scale(1,1), rotate(0) (Requirement 2 & 4.2)
            animate.setAttribute('values', initialValue); 
            animate.setAttribute('dur', '1s');
            animate.setAttribute('fill', 'freeze');
            animate.setAttribute('additive', 'sum'); // Important for relative transforms
            return animate;
        };

        // For additive="sum", the initial value in 'values' represents the *difference* from the base.
        // If we want no initial difference, the value is the identity transformation:
        gWrapper.appendChild(baseAnimate('translate', '0,0'));
        gWrapper.appendChild(baseAnimate('scale', '1')); // scale '1' is identity (no change)
        gWrapper.appendChild(baseAnimate('rotate', '0'));


        // 4. Add .etaniAnimate controls to .etaniItemRight (4.1)
        const etaniAnimate = document.createElement('div');
        etaniAnimate.className = 'etaniAnimate';

        // Animate Name (Dark grey background, white text)
        const nameSpan = document.createElement('span');
        nameSpan.className = 'etaniAnimateName';
        nameSpan.textContent = 'transform';
        etaniAnimate.appendChild(nameSpan);

        // Animate Dur (dur: n)
        const durSpan = document.createElement('span');
        durSpan.className = 'etaniAnimateDur';
        durSpan.textContent = 'dur: 1s'; // Initial value 1s
        etaniAnimate.appendChild(durSpan);

        // Animate Value container
        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        // AV Add (Plus sign SVG)
        const avAddSpan = document.createElement('span');
        avAddSpan.className = 'etaniAVAdd';
        avAddSpan.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" 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>`;
        avAddSpan.title = 'Add Transform Value';
        
        // Event binding: Pass the current item index and the original <use> element's ID
        avAddSpan.addEventListener('click', (e) => handleAVAddClick(e, i, useId));

        // AV Label
        const avLabelSpan = document.createElement('span');
        avLabelSpan.className = 'etaniAVLabel';
        avLabelSpan.textContent = 'values : ';

        // AV Container
        const avDiv = document.createElement('div');
        avDiv.className = 'etaniAV';
        
        // AV Item (Initially contains one)
        const avItemSpan = document.createElement('span');
        avItemSpan.className = 'etaniAVItem';
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avAddSpan);
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

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

    // 5. Update the SVG result immediately
    updateEtaniResult();
}

/**
 * Handles the Setting Mode switch (Freeze/Repeat) (Requirement 5).
 * @param {string} mode The animation end mode ('freeze' or 'repeat').
 */
function handleSettingModeChange(mode) {
    if (!etani_clone) return;

    // We select animateTransform inside <g> wrappers
    const animates = etani_clone.querySelectorAll('.etdrop > g > animateTransform');
    const isRepeat = mode === 'repeat';

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

    // Update active state
    document.querySelector('.etaniSettingFreeze').classList.toggle('active', !isRepeat);
    document.querySelector('.etaniSettingRepeat').classList.toggle('active', isRepeat);

    updateEtaniResult();
}

/**
 * Handles the HTML popup window (Requirement 7).
 * @param {Event} e The click event.
 */
function handleContentHTMLClick(e) {
    e.preventDefault();
    if (!etani_clone) return;

    const overlay = document.createElement('div');
    overlay.className = 'modal-overlay';
    
    const content = document.createElement('div');
    content.className = 'modal-content';
    
    const close = document.createElement('span');
    close.className = 'modal-close';
    close.innerHTML = '&times;'; // 'x' icon
    
    const textarea = document.createElement('textarea');
    textarea.value = etani_clone.outerHTML; // Output etani_clone's outerHTML

    // Close logic
    const closeModal = () => {
        if (document.body.contains(overlay)) document.body.removeChild(overlay);
        if (document.body.contains(content)) document.body.removeChild(content);
    };
    close.onclick = closeModal;
    overlay.onclick = closeModal; // Clicking background also closes

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


// --- MAIN STRUCTURE CREATION ---

/**
 * Creates the internal animation control structure.
 * @param {HTMLElement} etaniouter The outer container element.
 */
function createEtaniInner(etaniouter) {
    // 5.1, 5.2, 5.3 Clone, remove .etwait, update ID
    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etani_clone = originalSvg.cloneNode(true);
    const etwaitElement = etani_clone.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    etani_clone.id = 'etmainani';


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

    // Create div.etaniCtrl
    const etaniCtrl = document.createElement('div');
    etaniCtrl.className = 'etaniCtrl';
    
    // Requirement 7: a.etaniContentHTML
    const etaniContent = document.createElement('div');
    etaniContent.className = 'etaniContent';
    
    const contentHTMLLink = document.createElement('a');
    contentHTMLLink.className = 'etaniContentHTML';
    contentHTMLLink.textContent = 'Show HTML';
    contentHTMLLink.href = '#';
    contentHTMLLink.addEventListener('click', handleContentHTMLClick);
    etaniContent.appendChild(contentHTMLLink);

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


    // Requirement 1, 2: div.etaniSetting (Radio Buttons)
    const etaniSetting = document.createElement('div');
    etaniSetting.className = 'etaniSetting';
    
    const freezeRadio = document.createElement('span');
    freezeRadio.className = 'etaniSettingMode etaniSettingFreeze active';
    freezeRadio.textContent = 'Freeze';
    freezeRadio.setAttribute('data-mode', 'freeze');
    freezeRadio.addEventListener('click', () => handleSettingModeChange('freeze'));
    
    const repeatRadio = document.createElement('span');
    repeatRadio.className = 'etaniSettingMode etaniSettingRepeat';
    repeatRadio.textContent = 'Repeat';
    repeatRadio.setAttribute('data-mode', 'repeat');
    repeatRadio.addEventListener('click', () => handleSettingModeChange('repeat'));

    etaniSetting.appendChild(freezeRadio);
    etaniSetting.appendChild(repeatRadio);
    etaniCtrl.appendChild(etaniSetting);


    // Requirement 1, 3: div.etaniAllAppend (Buttons)
    const etaniAllAppend = document.createElement('div');
    etaniAllAppend.className = 'etaniAllAppend';

    const transformButton = document.createElement('button');
    transformButton.className = 'etaniAllAppendTransform';
    transformButton.textContent = 'transform';
    transformButton.addEventListener('click', handleAllAppendTransformClick);
    
    const opacityButton = document.createElement('button');
    opacityButton.className = 'etaniAllAppendOpacity';
    opacityButton.textContent = 'opacity';
    // opacityButton.addEventListener('click', handleAllAppendOpacityClick); // Placeholder
    
    etaniAllAppend.appendChild(transformButton);
    etaniAllAppend.appendChild(opacityButton);
    etaniCtrl.appendChild(etaniAllAppend);
    

    // Create div.etaniCol (Tile list)
    const etaniCol = document.createElement('div');
    etaniCol.className = 'etaniCol';

    // Create div.etaniResult
    const etaniResult = document.createElement('div');
    etaniResult.className = 'etaniResult';
    
    // Result elements (img, download, rename, size) 
    const resultImg = document.createElement('img');
    resultImg.className = 'etaniResultImage';
    resultImg.alt = 'Rendered Ejtile Animation SVG';

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

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

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

    resultDRDiv.appendChild(downloadLink);
    resultDRDiv.appendChild(renameLink);
    
    etaniResult.appendChild(resultImg);
    etaniResult.appendChild(resultDRDiv);
    etaniResult.appendChild(sizeSpan);
    
    // Append children to etaniinner
    etaniinner.appendChild(etaniCtrl);
    etaniinner.appendChild(etaniCol);
    etaniinner.appendChild(etaniResult);

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

    // III, IV, V, VI, VII. Populate etaniCol
    const etdropUses = document.querySelectorAll('.etdrop use');
    const etwaitGroups = document.querySelectorAll('.etwait g');

    etdropUses.forEach((useElement, i) => {
        // (Tile processing logic - Unchanged)
        const tileid = useElement.getAttribute('href').substring(1);
        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 originalTile = document.querySelector(`defs g#${tileid}`);
        
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';

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

        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        
        if (originalTile) {
            const tileclone = originalTile.cloneNode(true);
            tileclone.removeAttribute('id');
            if (etwaittransform) tileclone.setAttribute('transform', etwaittransform);
            if (etwaitfill) tileclone.setAttribute('fill', etwaitfill);
            if (etwaitstroke) tileclone.setAttribute('stroke', etwaitstroke);
            if (etwaitstrokeWidth) tileclone.setAttribute('stroke-width', etwaitstrokeWidth);
            
            const svgWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            svgWrapper.setAttribute('width', '40');
            svgWrapper.setAttribute('height', '40');
            svgWrapper.setAttribute('version', '1.1');
            svgWrapper.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
            svgWrapper.setAttribute('xmlns:svg', 'http://www.w3.org/2000/svg');
            svgWrapper.className = 'etanitileimg';
            
            svgWrapper.appendChild(tileclone);
            etaniItemLeft.appendChild(svgWrapper);
        }

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

        etaniItem.appendChild(etaniItemLeft);
        etaniItem.appendChild(etaniItemRight);
        etaniCol.appendChild(etaniItem);
    });
    
    // 5. Update the result section immediately
    updateEtaniResult();
}

// --- INITIALIZATION ---

/**
 * Toggles the visibility and content of the animation control panel.
 * @param {Event} event The click event.
 */
function toggleAnimation(event) {
    const etanibutton = event.currentTarget;
    const etaniouter = etanibutton.parentNode;
    const etaniinner = document.getElementById('etaniinner');

    if (etanibutton.textContent === 'Animate it') {
        etanibutton.textContent = 'Close Ejtile Ani';
        createEtaniInner(etaniouter);
        
    } else if (etanibutton.textContent === 'Close Ejtile Ani') {
        etanibutton.textContent = 'Animate it';
        if (etaniinner) {
            etaniinner.remove();
        }
        etani_clone = null;
    }
}


window.addEventListener('load', () => {
    addDynamicStyles();

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

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

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

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

帖子 ejsoon »

代码: 全选

改動:

一,在.etaniAVAdd內的svg,它長寬也應為24px。同時,更改它插入的方式,用createElement和appendChild。

二,當點擊.etaniAVAdd增加了一個value時,dur也要加1。

三,在.etaniAVAdd右方,增加一個.etaniAVDelete,其內部也是一個長寬都是24px的svg圖標。當點擊它時,它會處於刪除狀態,這時再點擊一個.etaniAVItem,則這個.etaniAVItem會被刪除,dur也會減一。再次點擊.etaniAVDelete,會撤銷刪除狀態。第一個.etaniAVItem不能刪。

用繁體中文回覆。在代碼中所有注釋都要用英文。

代码: 全选

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

// --- CSS STYLES ---

function addDynamicStyles() {
    if (document.getElementById('dynamic-et-styles')) {
        return;
    }

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

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

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

        /* IV. Styles for etaniResult */
        .etaniResult {
            text-align: center;
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #bbb;
            overflow: auto;
        }
        
        /* ------------------ CONTROL & BUTTON STYLES ------------------ */

        /* Styles for control links (Center button) */
        .etaniContent a {
            display: inline-block;
            margin: 0 5px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 14px;
        }
        
        .etaniCenter {
            border: 1px solid green;
            color: green;
        }
        
        .etaniContentHTML {
            border: 1px solid #0099ff;
            color: #0099ff;
            margin-right: 15px;
        }

        /* Requirement 3: etaniAllAppend buttons */
        .etaniAllAppend button {
            padding: 5px 10px;
            font-size: 16px;
            margin: 0 5px;
            cursor: pointer;
            border: 1px solid #333;
            background-color: #fff;
        }
        
        /* Requirement 2: Custom Radio button styles */
        .etaniSettingMode {
            display: inline-block;
            cursor: pointer;
            padding: 4px 8px;
            margin: 0 3px;
            font-size: 14px;
            border: 1px solid #888;
            background-color: #eee;
            color: #333;
            user-select: none;
        }

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

        /* ------------------ RESULT & ANIMAION ITEM STYLES ------------------ */

        .etaniResultDR {
            text-align: center;
            margin-bottom: 10px;
        }

        /* Requirement 1: Download and rename link font size 16px */
        .etaniResultDownload, .etaniResultRename {
            display: inline-block;
            margin-right: 15px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 16px; 
        }
        
        /* .etaniResultDownload default style */
        .etaniResultDownload {
            border: 1px solid blue;
            color: blue;
        }

        /* .etaniResultRename style (Requirement 1: color changed to brown) */
        .etaniResultRename {
            border: 1px solid brown;
            color: brown;
        }

        .etaniResultImage {
            display: block;
            max-width: 480px;
            width: 100%;
            height: auto;
            margin: 0 auto 10px auto;
            border: 1px solid #000;
        }
        
        /* Requirement 2: Minimum font size 12px */
        .etaniResultSize {
            display: inline-block;
            margin-left: 10px;
            font-size: 12px; 
            color: #555;
        }
        
        /* etaniItem structure (Float layout) */
        .etaniItem {
            min-height: 48px;
            border: 1px solid #ccc;
            box-sizing: border-box;
            width: 100%;
            margin-bottom: -1px;
            overflow: auto;
        }
        
        .etaniItemLeft {
            float: left; 
            width: 60px;
            min-height: 48px;
            border-right: 1px solid #ccc;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            background-color: #e8e8e8;
            padding: 2px 0;
        }
        
        .etaniItemRight {
            margin-left: 60px; 
            padding: 7px;
            min-height: 48px; 
            background-color: #fff;
        }

        /* Requirement 2: Minimum font size 12px */
        .tileid {
            text-align: center;
            font-size: 12px; 
            word-break: break-all;
            padding-top: 2px;
        }
        
        /* ------------------ ANIMATE CONTROLS (4.1) ------------------ */

        .etaniAnimate {
            border: 1px solid #999;
            padding: 5px;
            margin-bottom: 5px;
        }

        .etaniAnimateName {
            display: inline-block;
            padding: 2px 5px;
            background-color: #555; /* Dark grey background */
            color: white; /* White text */
            margin-right: 10px;
            font-size: 12px; /* Minimum font size 12px */
        }

        .etaniAnimateDur {
            display: inline-block;
            margin-right: 10px;
            font-size: 14px;
        }

        .etaniAnimateValue {
            margin-top: 5px;
            overflow: auto;
        }

        /* Shared style for control buttons */
        .etaniAVAdd, .etaniAVDelete {
            display: inline-block;
            width: 24px;
            height: 24px;
            line-height: 24px;
            text-align: center;
            margin-right: 5px;
            cursor: pointer;
            box-sizing: border-box;
            vertical-align: top;
        }

        .etaniAVAdd {
            background-color: #a7fca7; /* Light green */
            border: 1px solid #71c371; /* Thin line near light green */
        }
        
        .etaniAVDelete {
            background-color: #ffcccc; /* Light red */
            border: 1px solid #cc3333; /* Darker red */
        }

        .etaniAVDelete.deleting {
            background-color: #cc3333; /* Dark red when active */
        }
        
        .etaniAVLabel {
            font-size: 14px;
            margin-right: 5px;
        }

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

        .etaniAVItem {
            display: inline-block;
            width: 24px;
            height: 24px;
            background-color: #ff9933; /* Dark orange */
            border: 1px dashed #00bfff; /* Dashed sky blue */
            margin: 0 5px;
            box-sizing: border-box;
            cursor: pointer;
            position: relative;
        }

        .etaniAVItem.deleting-target {
            background-color: #ff4d4d; /* Reddish when targeted for deletion */
            border: 2px solid red;
        }
        
        /* ------------------ MODAL/POPUP STYLES ------------------ */
        .modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.5);
            z-index: 1000;
        }
        .modal-content {
            position: fixed;
            width: 98%;
            height: 48%;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 10px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
            z-index: 1001;
            box-sizing: border-box;
        }
        .modal-content textarea {
            width: 100%;
            height: calc(100% - 50px); /* Adjusted for larger close button */
            resize: none;
            border: 1px solid #ccc;
            font-size: 12px;
            box-sizing: border-box;
        }
        .modal-close {
            position: absolute;
            top: 0;
            right: 10px;
            font-size: 48px;
            line-height: 1; 
            cursor: pointer;
            color: #333;
        }
    `;

    document.head.appendChild(styleSheet);
}

// --- UTILITY FUNCTIONS ---

/**
 * Converts an SVG string to a Base64 data URL.
 * @param {string} svgString The SVG XML string.
 * @returns {string} The Base64 data URL.
 */
function svgToBase64(svgString) {
    const base64 = btoa(unescape(encodeURIComponent(svgString)));
    return `data:image/svg+xml;base64,${base64}`;
}

/**
 * Formats byte size into human-readable string (e.g., KB, MB).
 * @param {number} bytes The size in bytes.
 * @returns {string} The formatted string.
 */
function formatBytes(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

/**
 * Parses transform string to get individual transform values.
 * @param {string} transformString The transform attribute string.
 * @returns {{translate: string, scale: string, rotate: string}} Parsed values.
 */
function parseTransform(transformString) {
    const defaultTransform = { translate: '0,0', scale: '1,1', rotate: '0' };
    if (!transformString) return defaultTransform;

    const transform = {};

    const getMatch = (type) => {
        const match = transformString.match(new RegExp(`${type}\\(([^)]+)\\)`, 'i'));
        return match ? match[1].split(/[,\s]+/).join(',') : null;
    };

    // Need to handle missing transforms gracefully
    transform.translate = getMatch('translate') || defaultTransform.translate;
    transform.scale = getMatch('scale') || defaultTransform.scale;
    // Normalize scale: if only one value (e.g., '1'), ensure it's '1,1' for coordinate parsing
    if (transform.scale.split(',').length === 1) {
        transform.scale += `,${transform.scale}`;
    }
    transform.rotate = getMatch('rotate') || defaultTransform.rotate;

    return transform;
}


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

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

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

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

/**
 * Handler for the 'Center' button click. Resets the .etdrop transform.
 * @param {Event} e The click event.
 */
function handleCenterClick(e) {
    e.preventDefault();
    if (etani_clone) {
        const etdrop = etani_clone.querySelector('.etdrop');
        if (etdrop) {
            etdrop.setAttribute('transform', 'translate(240,240) scale(1,1)');
            updateEtaniResult();
        }
    }
}

// --- NEW HANDLERS ---

/**
 * Calculates the required *additive* transform value for a given type.
 * @param {string} type The transform type ('translate', 'scale', 'rotate').
 * @param {string} originalValue The transform value from the original <use/> (cv).
 * @param {string} currentBaseValue The current base transform value from the etani_clone <use/> (ov).
 * @returns {string} The new additive value (v).
 */
function calculateAdditiveValue(type, originalValue, currentBaseValue) {
    if (type === 'translate') {
        const ovCoords = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
        const cvCoords = originalValue.split(',').map(c => parseFloat(c.trim()));
        // v1 = cv1 - ov1, v2 = cv2 - ov2
        return `${(cvCoords[0] - ovCoords[0]).toFixed(4)},${(cvCoords[1] - ovCoords[1]).toFixed(4)}`;
    } else if (type === 'scale') {
        const ovScales = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
        const cvScales = originalValue.split(',').map(c => parseFloat(c.trim()));
        // v1 = cv1 / ov1, v2 = cv2 / ov2.
        const v1 = (cvScales[0] / ovScales[0]).toFixed(4);
        const v2 = (cvScales.length > 1 ? (cvScales[1] / ovScales[1]) : v1).toFixed(4);
        return v1 === v2 ? v1 : `${v1},${v2}`;
    } else if (type === 'rotate') {
        const ovAngle = parseFloat(currentBaseValue.trim());
        const cvAngle = parseFloat(originalValue.trim());
        // v3 = cv3 - ov3
        return (cvAngle - ovAngle).toFixed(4);
    }
    return '';
}

/**
 * Updates the 'dur' attribute of all animateTransform tags within a parent element.
 * @param {HTMLElement} parent The parent element (the <g> wrapper).
 * @param {number} newDur The new duration in seconds (integer).
 */
function updateAnimateDuration(parent, newDur) {
    const animates = parent.querySelectorAll('animateTransform');
    const durString = `${newDur}s`;
    animates.forEach(animate => {
        animate.setAttribute('dur', durString);
    });

    // Update the UI display
    const etaniAnimate = parent.closest('.etaniItemRight').querySelector('.etaniAnimate');
    if (etaniAnimate) {
        etaniAnimate.querySelector('.etaniAnimateDur').textContent = `dur: ${durString}`;
    }
}

/**
 * Handles the click event for the '+' button (Requirement 6, 8-2).
 * Adds a new animateTransform value.
 * @param {Event} e The click event.
 * @param {number} itemIndex The index of the item (unused in this implementation but kept for context).
 * @param {string} useElementId The ID of the original <g> element referenced by the <use/>.
 */
function handleAVAddClick(e, itemIndex, useElementId) {
    e.preventDefault();
    if (!etani_clone) return;

    // 1. Get current transform of the original <use/> element in svg#etmain (cv)
    const originalUseElement = document.querySelector(`.etdrop use[href="#${useElementId}"]`);
    if (!originalUseElement) return;

    const originalTransformString = originalUseElement.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)';
    const originalTransforms = parseTransform(originalTransformString);
    
    // 2. Find the corresponding <g> wrapper in etani_clone
    const cloneGWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useElementId}"]`);
    if (!cloneGWrapper) return;
    
    // 3. Get the *base* transform of the <use> element inside the wrapper (ov)
    const cloneUseElement = cloneGWrapper.querySelector('use');
    const currentBaseTransformString = cloneUseElement.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)';
    const currentBaseTransforms = parseTransform(currentBaseTransformString);

    // 4. Update the three animateTransform tags' values and duration (Requirement 8-2)
    const animates = cloneGWrapper.querySelectorAll('animateTransform');
    
    // Calculate new duration
    const currentDur = parseInt(animates[0].getAttribute('dur').replace('s', '')) || 1;
    const newDur = currentDur + 1;

    animates.forEach(animate => {
        const type = animate.getAttribute('type').toLowerCase();
        let currentValueString = animate.getAttribute('values') || '';
        let newValue = '';
        
        // Calculate the *additive* difference based on the requirement
        if (type === 'translate') {
            newValue = calculateAdditiveValue('translate', originalTransforms.translate, currentBaseTransforms.translate);
        } else if (type === 'scale') {
            newValue = calculateAdditiveValue('scale', originalTransforms.scale, currentBaseTransforms.scale);
        } else if (type === 'rotate') {
            newValue = calculateAdditiveValue('rotate', originalTransforms.rotate, currentBaseTransforms.rotate);
        }
        
        // Append the new value
        animate.setAttribute('values', (currentValueString ? currentValueString + ';' : '') + newValue);

        // Update duration (Requirement 8-2)
        animate.setAttribute('dur', `${newDur}s`);
    });

    // 5. Add a new .etaniAVItem in .etaniItemRight and update UI duration
    const etaniItemRight = e.currentTarget.closest('.etaniItemRight');
    const etaniAV = etaniItemRight.querySelector('.etaniAV');
    if (etaniAV) {
        const newAVItem = document.createElement('span');
        newAVItem.className = 'etaniAVItem';
        newAVItem.addEventListener('click', handleAVItemClick); // Bind deletion handler
        etaniAV.appendChild(newAVItem);
    }
    etaniItemRight.querySelector('.etaniAnimateDur').textContent = `dur: ${newDur}s`;

    // 6. Update the SVG result
    updateEtaniResult();
}

/**
 * Handles the click event for the '-' button (Requirement 8-3).
 * Toggles deletion mode.
 * @param {Event} e The click event.
 */
function handleAVDeleteToggle(e) {
    e.preventDefault();
    const deleteButton = e.currentTarget;
    const isDeleting = deleteButton.classList.toggle('deleting');
    const etaniItemRight = deleteButton.closest('.etaniItemRight');
    
    const avItems = etaniItemRight.querySelectorAll('.etaniAVItem');
    
    // Toggle visual state of all items (except the first one)
    avItems.forEach((item, index) => {
        if (index > 0) {
            item.classList.toggle('deleting-target', isDeleting);
        }
    });

    // Toggle deleting state for all items in this column
    const etaniCol = etaniItemRight.closest('.etaniCol');
    etaniCol.classList.toggle('deleting-mode', isDeleting);

    // Disable all other AVDelete buttons if entering deleting mode
    document.querySelectorAll('.etaniAVDelete').forEach(btn => {
        if (btn !== deleteButton) {
            btn.disabled = isDeleting;
        }
    });

    if (isDeleting) {
        deleteButton.title = "Cancel Deletion";
    } else {
        deleteButton.title = "Delete Last Value";
    }
}

/**
 * Handles the click event for an .etaniAVItem in deletion mode (Requirement 8-3).
 * @param {Event} e The click event.
 */
function handleAVItemClick(e) {
    const itemToDelete = e.currentTarget;
    const itemIndex = Array.from(itemToDelete.parentNode.children).indexOf(itemToDelete);
    
    // Check if deletion mode is active and it's not the first item
    if (itemToDelete.closest('.etaniCol').classList.contains('deleting-mode') && itemIndex > 0) {
        const etaniItemRight = itemToDelete.closest('.etaniItemRight');
        const deleteButton = etaniItemRight.querySelector('.etaniAVDelete');
        const useId = etaniItemRight.closest('.etaniItem').querySelector('.tileid').textContent;
        
        // 1. Find the corresponding <g> wrapper in etani_clone
        const cloneGWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useId}"]`);
        if (!cloneGWrapper) return;
        
        // 2. Update animateTransform values and duration
        const animates = cloneGWrapper.querySelectorAll('animateTransform');
        
        // Calculate new duration
        const currentDur = parseInt(animates[0].getAttribute('dur').replace('s', '')) || 1;
        const newDur = Math.max(1, currentDur - 1); // Duration cannot be less than 1
        
        animates.forEach(animate => {
            const values = animate.getAttribute('values').split(';');
            
            // Remove the value at the target index (itemIndex)
            if (itemIndex < values.length) {
                values.splice(itemIndex, 1);
            }
            animate.setAttribute('values', values.join(';'));
            
            // Update duration (Requirement 8-3)
            animate.setAttribute('dur', `${newDur}s`);
        });

        // 3. Remove the item from the UI
        itemToDelete.remove();

        // 4. Update UI duration display
        etaniItemRight.querySelector('.etaniAnimateDur').textContent = `dur: ${newDur}s`;

        // 5. Turn off deleting mode
        deleteButton.click(); // Programmatically click to toggle out of deleting mode

        // 6. Update the SVG result
        updateEtaniResult();
    } else if (itemIndex === 0 && itemToDelete.closest('.etaniCol').classList.contains('deleting-mode')) {
        alert("The first value cannot be deleted.");
    }
}


/**
 * Handles the click event for the 'Transform' button (Requirement 4).
 * Adds three animateTransform tags to all <use/> elements and adds controls to .etaniItemRight.
 */
function handleAllAppendTransformClick() {
    if (!etani_clone) return;

    const etdropUses = document.querySelectorAll('#etmain .etdrop use'); // Select original <use> elements for their href
    const etaniItemRights = document.querySelectorAll('.etaniItemRight');

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

        // 1. Find the corresponding <use> element in etani_clone
        let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;
        
        // Check if transform controls already exist to prevent duplication
        if (itemRight.querySelector('.etaniAnimate')) return; 

        // 2. Wrap the <use/> element in a <g> tag (Requirement 4)
        const gWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        gWrapper.setAttribute('data-use-href', `#${useId}`); // Use a data attribute to link it back
        
        // Preserve the base transform on the <use> element
        const baseTransform = cloneUseElement.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)';
        cloneUseElement.setAttribute('transform', baseTransform);
        
        cloneUseElement.parentNode.insertBefore(gWrapper, cloneUseElement);
        gWrapper.appendChild(cloneUseElement);
        
        // 3. Add animateTransform tags to the <g> wrapper
        const baseAnimate = (type, initialValue) => {
            const animate = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
            animate.setAttribute('attributeName', 'transform');
            animate.setAttribute('attributeType', 'XML');
            animate.setAttribute('type', type);
            // Initial values: translate(0,0), scale(1) or scale(1,1), rotate(0) 
            animate.setAttribute('values', initialValue); 
            animate.setAttribute('dur', '1s'); // Initial duration
            animate.setAttribute('fill', 'freeze');
            animate.setAttribute('additive', 'sum'); // Important for relative transforms
            return animate;
        };

        gWrapper.appendChild(baseAnimate('translate', '0,0'));
        gWrapper.appendChild(baseAnimate('scale', '1')); 
        gWrapper.appendChild(baseAnimate('rotate', '0'));


        // 4. Add .etaniAnimate controls to .etaniItemRight (4.1)
        const etaniAnimate = document.createElement('div');
        etaniAnimate.className = 'etaniAnimate';

        // Animate Name (Dark grey background, white text)
        const nameSpan = document.createElement('span');
        nameSpan.className = 'etaniAnimateName';
        nameSpan.textContent = 'transform';
        etaniAnimate.appendChild(nameSpan);

        // Animate Dur (dur: n)
        const durSpan = document.createElement('span');
        durSpan.className = 'etaniAnimateDur';
        durSpan.textContent = 'dur: 1s'; // Initial value 1s
        etaniAnimate.appendChild(durSpan);

        // Animate Value container
        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        // AV Add (Plus sign SVG) (Requirement 8-1)
        const avAddSpan = document.createElement('span');
        avAddSpan.className = 'etaniAVAdd';
        avAddSpan.title = 'Add Transform Value';
        avAddSpan.addEventListener('click', (e) => handleAVAddClick(e, i, useId));
        
        const plusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        plusSvg.setAttribute('width', '24');
        plusSvg.setAttribute('height', '24');
        plusSvg.setAttribute('viewBox', '0 0 24 24');
        plusSvg.setAttribute('fill', 'none');
        plusSvg.setAttribute('stroke', 'currentColor');
        plusSvg.setAttribute('stroke-width', '3');
        plusSvg.setAttribute('stroke-linecap', 'round');
        plusSvg.setAttribute('stroke-linejoin', 'round');
        plusSvg.innerHTML = '<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>';
        avAddSpan.appendChild(plusSvg);


        // AV Delete (Minus sign SVG) (Requirement 8-3)
        const avDeleteSpan = document.createElement('span');
        avDeleteSpan.className = 'etaniAVDelete';
        avDeleteSpan.title = 'Delete Last Value';
        avDeleteSpan.addEventListener('click', handleAVDeleteToggle);

        const minusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        minusSvg.setAttribute('width', '24');
        minusSvg.setAttribute('height', '24');
        minusSvg.setAttribute('viewBox', '0 0 24 24');
        minusSvg.setAttribute('fill', 'none');
        minusSvg.setAttribute('stroke', 'currentColor');
        minusSvg.setAttribute('stroke-width', '3');
        minusSvg.setAttribute('stroke-linecap', 'round');
        minusSvg.setAttribute('stroke-linejoin', 'round');
        minusSvg.innerHTML = '<line x1="5" y1="12" x2="19" y2="12"></line>';
        avDeleteSpan.appendChild(minusSvg);


        // AV Label
        const avLabelSpan = document.createElement('span');
        avLabelSpan.className = 'etaniAVLabel';
        avLabelSpan.textContent = 'values : ';

        // AV Container
        const avDiv = document.createElement('div');
        avDiv.className = 'etaniAV';
        
        // AV Item (Initially contains one)
        const avItemSpan = document.createElement('span');
        avItemSpan.className = 'etaniAVItem';
        avItemSpan.addEventListener('click', handleAVItemClick); // Bind deletion handler
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avAddSpan);
        valueDiv.appendChild(avDeleteSpan); // Add delete button next to add
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

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

    // 5. Update the SVG result immediately
    updateEtaniResult();
}

/**
 * Handles the Setting Mode switch (Freeze/Repeat) (Requirement 5).
 * @param {string} mode The animation end mode ('freeze' or 'repeat').
 */
function handleSettingModeChange(mode) {
    if (!etani_clone) return;

    // We select animateTransform inside <g> wrappers
    const animates = etani_clone.querySelectorAll('.etdrop > g > animateTransform');
    const isRepeat = mode === 'repeat';

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

    // Update active state
    document.querySelector('.etaniSettingFreeze').classList.toggle('active', !isRepeat);
    document.querySelector('.etaniSettingRepeat').classList.toggle('active', isRepeat);

    updateEtaniResult();
}

/**
 * Handles the HTML popup window (Requirement 7).
 * @param {Event} e The click event.
 */
function handleContentHTMLClick(e) {
    e.preventDefault();
    if (!etani_clone) return;

    const overlay = document.createElement('div');
    overlay.className = 'modal-overlay';
    
    const content = document.createElement('div');
    content.className = 'modal-content';
    
    const close = document.createElement('span');
    close.className = 'modal-close';
    close.innerHTML = '&times;'; // 'x' icon
    
    const textarea = document.createElement('textarea');
    textarea.value = etani_clone.outerHTML; // Output etani_clone's outerHTML

    // Close logic
    const closeModal = () => {
        if (document.body.contains(overlay)) document.body.removeChild(overlay);
        if (document.body.contains(content)) document.body.removeChild(content);
    };
    close.onclick = closeModal;
    overlay.onclick = closeModal; // Clicking background also closes

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


// --- MAIN STRUCTURE CREATION ---

/**
 * Creates the internal animation control structure.
 * @param {HTMLElement} etaniouter The outer container element.
 */
function createEtaniInner(etaniouter) {
    // 5.1, 5.2, 5.3 Clone, remove .etwait, update ID
    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etani_clone = originalSvg.cloneNode(true);
    const etwaitElement = etani_clone.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    etani_clone.id = 'etmainani';


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

    // Create div.etaniCtrl
    const etaniCtrl = document.createElement('div');
    etaniCtrl.className = 'etaniCtrl';
    
    // Requirement 7: a.etaniContentHTML
    const etaniContent = document.createElement('div');
    etaniContent.className = 'etaniContent';
    
    const contentHTMLLink = document.createElement('a');
    contentHTMLLink.className = 'etaniContentHTML';
    contentHTMLLink.textContent = 'Show HTML';
    contentHTMLLink.href = '#';
    contentHTMLLink.addEventListener('click', handleContentHTMLClick);
    etaniContent.appendChild(contentHTMLLink);

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


    // Requirement 1, 2: div.etaniSetting (Radio Buttons)
    const etaniSetting = document.createElement('div');
    etaniSetting.className = 'etaniSetting';
    
    const freezeRadio = document.createElement('span');
    freezeRadio.className = 'etaniSettingMode etaniSettingFreeze active';
    freezeRadio.textContent = 'Freeze';
    freezeRadio.setAttribute('data-mode', 'freeze');
    freezeRadio.addEventListener('click', () => handleSettingModeChange('freeze'));
    
    const repeatRadio = document.createElement('span');
    repeatRadio.className = 'etaniSettingMode etaniSettingRepeat';
    repeatRadio.textContent = 'Repeat';
    repeatRadio.setAttribute('data-mode', 'repeat');
    repeatRadio.addEventListener('click', () => handleSettingModeChange('repeat'));

    etaniSetting.appendChild(freezeRadio);
    etaniSetting.appendChild(repeatRadio);
    etaniCtrl.appendChild(etaniSetting);


    // Requirement 1, 3: div.etaniAllAppend (Buttons)
    const etaniAllAppend = document.createElement('div');
    etaniAllAppend.className = 'etaniAllAppend';

    const transformButton = document.createElement('button');
    transformButton.className = 'etaniAllAppendTransform';
    transformButton.textContent = 'transform';
    transformButton.addEventListener('click', handleAllAppendTransformClick);
    
    const opacityButton = document.createElement('button');
    opacityButton.className = 'etaniAllAppendOpacity';
    opacityButton.textContent = 'opacity';
    // opacityButton.addEventListener('click', handleAllAppendOpacityClick); // Placeholder
    
    etaniAllAppend.appendChild(transformButton);
    etaniAllAppend.appendChild(opacityButton);
    etaniCtrl.appendChild(etaniAllAppend);
    

    // Create div.etaniCol (Tile list)
    const etaniCol = document.createElement('div');
    etaniCol.className = 'etaniCol';

    // Create div.etaniResult
    const etaniResult = document.createElement('div');
    etaniResult.className = 'etaniResult';
    
    // Result elements (img, download, rename, size) 
    const resultImg = document.createElement('img');
    resultImg.className = 'etaniResultImage';
    resultImg.alt = 'Rendered Ejtile Animation SVG';

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

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

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

    resultDRDiv.appendChild(downloadLink);
    resultDRDiv.appendChild(renameLink);
    
    etaniResult.appendChild(resultImg);
    etaniResult.appendChild(resultDRDiv);
    etaniResult.appendChild(sizeSpan);
    
    // Append children to etaniinner
    etaniinner.appendChild(etaniCtrl);
    etaniinner.appendChild(etaniCol);
    etaniinner.appendChild(etaniResult);

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

    // III, IV, V, VI, VII. Populate etaniCol
    const etdropUses = document.querySelectorAll('.etdrop use');
    const etwaitGroups = document.querySelectorAll('.etwait g');

    etdropUses.forEach((useElement, i) => {
        // (Tile processing logic - Unchanged)
        const tileid = useElement.getAttribute('href').substring(1);
        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 originalTile = document.querySelector(`defs g#${tileid}`);
        
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';

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

        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        
        if (originalTile) {
            const tileclone = originalTile.cloneNode(true);
            tileclone.removeAttribute('id');
            if (etwaittransform) tileclone.setAttribute('transform', etwaittransform);
            if (etwaitfill) tileclone.setAttribute('fill', etwaitfill);
            if (etwaitstroke) tileclone.setAttribute('stroke', etwaitstroke);
            if (etwaitstrokeWidth) tileclone.setAttribute('stroke-width', etwaitstrokeWidth);
            
            const svgWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            svgWrapper.setAttribute('width', '40');
            svgWrapper.setAttribute('height', '40');
            svgWrapper.setAttribute('version', '1.1');
            svgWrapper.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
            svgWrapper.setAttribute('xmlns:svg', 'http://www.w3.org/2000/svg');
            svgWrapper.className = 'etanitileimg';
            
            svgWrapper.appendChild(tileclone);
            etaniItemLeft.appendChild(svgWrapper);
        }

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

        etaniItem.appendChild(etaniItemLeft);
        etaniItem.appendChild(etaniItemRight);
        etaniCol.appendChild(etaniItem);
    });
    
    // 5. Update the result section immediately
    updateEtaniResult();
}

// --- INITIALIZATION ---

/**
 * Toggles the visibility and content of the animation control panel.
 * @param {Event} event The click event.
 */
function toggleAnimation(event) {
    const etanibutton = event.currentTarget;
    const etaniouter = etanibutton.parentNode;
    const etaniinner = document.getElementById('etaniinner');

    if (etanibutton.textContent === 'Animate it') {
        etanibutton.textContent = 'Close Ejtile Ani';
        createEtaniInner(etaniouter);
        
    } else if (etanibutton.textContent === 'Close Ejtile Ani') {
        etanibutton.textContent = 'Animate it';
        if (etaniinner) {
            etaniinner.remove();
        }
        etani_clone = null;
    }
}


window.addEventListener('load', () => {
    addDynamicStyles();

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

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

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

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

帖子 ejsoon »

要把scale還有rotate放對:

代码: 全选

改動:

一,在處於刪除狀態時,可以連續刪除多個,只有再次點擊.etaniAVDelete,才會取消刪除狀態。

二,.etaniAVAdd和.etaniAVDelete內部的svg,它的stroke-width改為1。

三,把.etaniAVAdd和.etaniAVDelete放到父元素div.etaniAVCtrl中。

四,增加css規則:

.etaniAVCtrl svg {margin-left: -1px;margin-top: -1px;}

五,dur應從0s開始。

六,當往etani_clone的use中增加transform時,translate的位置仍然保持不變,但是scale和rotate應加到<use></use>的內部。

七,etani_clone的<use></use>的父元素不需有任何的屬性。

用繁體中文回覆。在代碼中所有注釋都要用英文。

代码: 全选

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

// --- CSS STYLES ---

function addDynamicStyles() {
    if (document.getElementById('dynamic-et-styles')) {
        return;
    }

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

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

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

        /* IV. Styles for etaniResult */
        .etaniResult {
            text-align: center;
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #bbb;
            overflow: auto;
        }
        
        /* ------------------ CONTROL & BUTTON STYLES ------------------ */

        /* Styles for control links (Center button) */
        .etaniContent a {
            display: inline-block;
            margin: 0 5px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 14px;
        }
        
        .etaniCenter {
            border: 1px solid green;
            color: green;
        }
        
        .etaniContentHTML {
            border: 1px solid #0099ff;
            color: #0099ff;
            margin-right: 15px;
        }

        /* Requirement 3: etaniAllAppend buttons */
        .etaniAllAppend button {
            padding: 5px 10px;
            font-size: 16px;
            margin: 0 5px;
            cursor: pointer;
            border: 1px solid #333;
            background-color: #fff;
        }
        
        /* Requirement 2: Custom Radio button styles */
        .etaniSettingMode {
            display: inline-block;
            cursor: pointer;
            padding: 4px 8px;
            margin: 0 3px;
            font-size: 14px;
            border: 1px solid #888;
            background-color: #eee;
            color: #333;
            user-select: none;
        }

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

        /* ------------------ RESULT & ANIMAION ITEM STYLES ------------------ */

        .etaniResultDR {
            text-align: center;
            margin-bottom: 10px;
        }

        /* Requirement 1: Download and rename link font size 16px */
        .etaniResultDownload, .etaniResultRename {
            display: inline-block;
            margin-right: 15px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 16px; 
        }
        
        /* .etaniResultDownload default style */
        .etaniResultDownload {
            border: 1px solid blue;
            color: blue;
        }

        /* .etaniResultRename style (Requirement 1: color changed to brown) */
        .etaniResultRename {
            border: 1px solid brown;
            color: brown;
        }

        .etaniResultImage {
            display: block;
            max-width: 480px;
            width: 100%;
            height: auto;
            margin: 0 auto 10px auto;
            border: 1px solid #000;
        }
        
        /* Requirement 2: Minimum font size 12px */
        .etaniResultSize {
            display: inline-block;
            margin-left: 10px;
            font-size: 12px; 
            color: #555;
        }
        
        /* etaniItem structure (Float layout) */
        .etaniItem {
            min-height: 48px;
            border: 1px solid #ccc;
            box-sizing: border-box;
            width: 100%;
            margin-bottom: -1px;
            overflow: auto;
        }
        
        .etaniItemLeft {
            float: left; 
            width: 60px;
            min-height: 48px;
            border-right: 1px solid #ccc;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            background-color: #e8e8e8;
            padding: 2px 0;
        }
        
        .etaniItemRight {
            margin-left: 60px; 
            padding: 7px;
            min-height: 48px; 
            background-color: #fff;
        }

        /* Requirement 2: Minimum font size 12px */
        .tileid {
            text-align: center;
            font-size: 12px; 
            word-break: break-all;
            padding-top: 2px;
        }
        
        /* ------------------ ANIMATE CONTROLS (4.1) ------------------ */

        .etaniAnimate {
            border: 1px solid #999;
            padding: 5px;
            margin-bottom: 5px;
        }

        .etaniAnimateName {
            display: inline-block;
            padding: 2px 5px;
            background-color: #555; /* Dark grey background */
            color: white; /* White text */
            margin-right: 10px;
            font-size: 12px; /* Minimum font size 12px */
        }

        .etaniAnimateDur {
            display: inline-block;
            margin-right: 10px;
            font-size: 14px;
        }

        .etaniAnimateValue {
            margin-top: 5px;
            overflow: auto;
        }

        /* New container for Add/Delete buttons (Requirement 3) */
        .etaniAVCtrl {
            display: inline-block;
            vertical-align: top;
            margin-right: 5px;
        }

        /* Shared style for control buttons */
        .etaniAVAdd, .etaniAVDelete {
            display: block; /* Stacked */
            width: 24px;
            height: 24px;
            line-height: 24px;
            text-align: center;
            cursor: pointer;
            box-sizing: border-box;
            vertical-align: top;
        }

        .etaniAVAdd {
            background-color: #a7fca7; /* Light green */
            border: 1px solid #71c371; /* Thin line near light green */
            margin-bottom: 3px;
        }
        
        .etaniAVDelete {
            background-color: #ffcccc; /* Light red */
            border: 1px solid #cc3333; /* Darker red */
        }

        .etaniAVDelete.deleting {
            background-color: #cc3333; /* Dark red when active */
        }
        
        /* New CSS rule for SVG alignment (Requirement 4) */
        .etaniAVCtrl svg {
            margin-left: -1px;
            margin-top: -1px;
        }

        .etaniAVLabel {
            font-size: 14px;
            margin-right: 5px;
        }

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

        .etaniAVItem {
            display: inline-block;
            width: 24px;
            height: 24px;
            background-color: #ff9933; /* Dark orange */
            border: 1px dashed #00bfff; /* Dashed sky blue */
            margin: 0 5px;
            box-sizing: border-box;
            cursor: pointer;
            position: relative;
        }

        .etaniAVItem.deleting-target {
            background-color: #ff4d4d; /* Reddish when targeted for deletion */
            border: 2px solid red;
        }
        
        /* ------------------ MODAL/POPUP STYLES ------------------ */
        .modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.5);
            z-index: 1000;
        }
        .modal-content {
            position: fixed;
            width: 98%;
            height: 48%;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 10px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
            z-index: 1001;
            box-sizing: border-box;
        }
        .modal-content textarea {
            width: 100%;
            height: calc(100% - 50px); /* Adjusted for larger close button */
            resize: none;
            border: 1px solid #ccc;
            font-size: 12px;
            box-sizing: border-box;
        }
        .modal-close {
            position: absolute;
            top: 0;
            right: 10px;
            font-size: 48px;
            line-height: 1; 
            cursor: pointer;
            color: #333;
        }
    `;

    document.head.appendChild(styleSheet);
}

// --- UTILITY FUNCTIONS ---

/**
 * Extracts a specific transform function and its value from a transform string.
 * @param {string} transformString The full transform string.
 * @param {string} type The transform type ('translate', 'scale', 'rotate').
 * @returns {{func: string, value: string}} The transform function and its value.
 */
function extractTransformPart(transformString, type) {
    const regex = new RegExp(`(${type})\\(([^)]+)\\)`, 'i');
    const match = transformString.match(regex);
    if (match) {
        return { func: match[1], value: match[2].trim() };
    }
    return { func: '', value: '' };
}

/**
 * Parses transform string to get individual transform values.
 * @param {string} transformString The transform attribute string.
 * @returns {{translate: string, scale: string, rotate: string}} Parsed values.
 */
function parseTransform(transformString) {
    const defaultTransform = { translate: '0,0', scale: '1,1', rotate: '0' };
    if (!transformString) return defaultTransform;

    const transform = {};

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

    transform.translate = getMatch('translate') || defaultTransform.translate;
    transform.scale = getMatch('scale') || defaultTransform.scale;
    // Normalize scale: if only one value (e.g., '1'), ensure it's '1,1' for coordinate parsing
    if (transform.scale.split(',').length === 1) {
        transform.scale += `,${transform.scale}`;
    }
    transform.rotate = getMatch('rotate') || defaultTransform.rotate;

    return transform;
}


// Other utility functions (svgToBase64, formatBytes, updateEtaniResult, handleCenterClick, handleContentHTMLClick) remain the same.

/**
 * Converts an SVG string to a Base64 data URL.
 * @param {string} svgString The SVG XML string.
 * @returns {string} The Base64 data URL.
 */
function svgToBase64(svgString) {
    const base64 = btoa(unescape(encodeURIComponent(svgString)));
    return `data:image/svg+xml;base64,${base64}`;
}

/**
 * Formats byte size into human-readable string (e.g., KB, MB).
 * @param {number} bytes The size in bytes.
 * @returns {string} The formatted string.
 */
function formatBytes(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

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

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

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

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

/**
 * Handler for the 'Center' button click. Resets the .etdrop transform.
 * @param {Event} e The click event.
 */
function handleCenterClick(e) {
    e.preventDefault();
    if (etani_clone) {
        const etdrop = etani_clone.querySelector('.etdrop');
        if (etdrop) {
            // Reset position to the original center for the entire group
            etdrop.setAttribute('transform', 'translate(240,240) scale(1,1)');
            updateEtaniResult();
        }
    }
}

/**
 * Handles the HTML popup window (Requirement 7 from previous iteration).
 * @param {Event} e The click event.
 */
function handleContentHTMLClick(e) {
    e.preventDefault();
    if (!etani_clone) return;

    const overlay = document.createElement('div');
    overlay.className = 'modal-overlay';
    
    const content = document.createElement('div');
    content.className = 'modal-content';
    
    const close = document.createElement('span');
    close.className = 'modal-close';
    close.innerHTML = '&times;'; // 'x' icon
    
    const textarea = document.createElement('textarea');
    textarea.value = etani_clone.outerHTML; // Output etani_clone's outerHTML

    // Close logic
    const closeModal = () => {
        if (document.body.contains(overlay)) document.body.removeChild(overlay);
        if (document.body.contains(content)) document.body.removeChild(content);
    };
    close.onclick = closeModal;
    overlay.onclick = closeModal; // Clicking background also closes

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

// --- NEW/MODIFIED HANDLERS ---

/**
 * Calculates the required *additive* transform value for a given type.
 * @param {string} type The transform type ('translate', 'scale', 'rotate').
 * @param {string} originalValue The transform value from the original <use/> (cv).
 * @param {string} currentBaseValue The current base transform value from the etani_clone <use/> or <g> (ov).
 * @returns {string} The new additive value (v).
 */
function calculateAdditiveValue(type, originalValue, currentBaseValue) {
    // Both values are normalized coordinate strings (e.g., "10,20" or "0" or "1.5,1.5")
    
    if (type === 'translate') {
        const ovCoords = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
        const cvCoords = originalValue.split(',').map(c => parseFloat(c.trim()));
        // v1 = cv1 - ov1, v2 = cv2 - ov2
        return `${(cvCoords[0] - ovCoords[0]).toFixed(4)},${(cvCoords[1] - ovCoords[1]).toFixed(4)}`;
    } else if (type === 'scale') {
        const ovScales = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
        const cvScales = originalValue.split(',').map(c => parseFloat(c.trim()));
        // v1 = cv1 / ov1, v2 = cv2 / ov2.
        const v1 = (cvScales[0] / ovScales[0]).toFixed(4);
        const v2 = (cvScales.length > 1 ? (cvScales[1] / ovScales[1]) : v1).toFixed(4);
        return v1 === v2 ? v1 : `${v1},${v2}`;
    } else if (type === 'rotate') {
        const ovAngle = parseFloat(currentBaseValue.trim());
        const cvAngle = parseFloat(originalValue.trim());
        // v3 = cv3 - ov3
        return (cvAngle - ovAngle).toFixed(4);
    }
    return '';
}

/**
 * Updates the 'dur' attribute of all animateTransform tags within a parent element.
 * @param {HTMLElement} parent The parent element (the <g> wrapper).
 * @param {number} newDur The new duration in seconds (integer).
 */
function updateAnimateDuration(parent, newDur) {
    const animates = parent.querySelectorAll('animateTransform');
    const durString = `${newDur}s`;
    animates.forEach(animate => {
        animate.setAttribute('dur', durString);
    });

    // Update the UI display
    const etaniAnimate = parent.closest('.etaniItemRight').querySelector('.etaniAnimate');
    if (etaniAnimate) {
        etaniAnimate.querySelector('.etaniAnimateDur').textContent = `dur: ${durString}`;
    }
}

/**
 * Handles the click event for the '+' button.
 * Adds a new animateTransform value.
 * @param {Event} e The click event.
 * @param {string} useElementId The ID of the original <g> element referenced by the <use/>.
 */
function handleAVAddClick(e, useElementId) {
    e.preventDefault();
    if (!etani_clone) return;

    // 1. Get current transform of the original <use/> element in svg#etmain (cv)
    const originalUseElement = document.querySelector(`.etdrop use[href="#${useElementId}"]`);
    if (!originalUseElement) return;

    const originalTransformString = originalUseElement.getAttribute('transform') || '';
    const originalTransforms = parseTransform(originalTransformString);
    
    // 2. Find the corresponding <g> wrapper in etani_clone
    const cloneGWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useElementId}"]`);
    const cloneUseElement = cloneGWrapper ? cloneGWrapper.querySelector('use') : null;
    if (!cloneGWrapper || !cloneUseElement) return;
    
    // 3. Get the *base* transform values from the clone's <use> (translate) and <g> (scale/rotate)
    const cloneTranslateString = cloneUseElement.getAttribute('transform') || 'translate(0,0)';
    const cloneTransformString = cloneGWrapper.getAttribute('transform') || 'scale(1,1) rotate(0)'; // <g> holds scale/rotate
    
    const currentBaseTranslate = parseTransform(cloneTranslateString).translate;
    const currentBaseScale = parseTransform(cloneTransformString).scale;
    const currentBaseRotate = parseTransform(cloneTransformString).rotate;


    // 4. Update the three animateTransform tags' values and duration 
    const animates = cloneGWrapper.querySelectorAll('animateTransform');
    
    // Calculate new duration (starts from 0s, adds 1s)
    const currentDur = parseInt((animates[0].getAttribute('dur') || '0s').replace('s', '')) || 0;
    const newDur = currentDur + 1;

    animates.forEach(animate => {
        const type = animate.getAttribute('type').toLowerCase();
        let currentValueString = animate.getAttribute('values') || '';
        let newValue = '';
        
        // Calculate the *additive* difference
        if (type === 'translate') {
            newValue = calculateAdditiveValue('translate', originalTransforms.translate, currentBaseTranslate);
        } else if (type === 'scale') {
            newValue = calculateAdditiveValue('scale', originalTransforms.scale, currentBaseScale);
        } else if (type === 'rotate') {
            newValue = calculateAdditiveValue('rotate', originalTransforms.rotate, currentBaseRotate);
        }
        
        // Append the new value
        animate.setAttribute('values', (currentValueString ? currentValueString + ';' : '') + newValue);

        // Update duration (Requirement 5)
        animate.setAttribute('dur', `${newDur}s`);
    });

    // 5. Add a new .etaniAVItem in .etaniItemRight and update UI duration
    const etaniItemRight = e.currentTarget.closest('.etaniItemRight');
    const etaniAV = etaniItemRight.querySelector('.etaniAV');
    if (etaniAV) {
        const newAVItem = document.createElement('span');
        newAVItem.className = 'etaniAVItem';
        newAVItem.addEventListener('click', handleAVItemClick); // Bind deletion handler
        etaniAV.appendChild(newAVItem);
    }
    etaniItemRight.querySelector('.etaniAnimateDur').textContent = `dur: ${newDur}s`;

    // 6. Update the SVG result
    updateEtaniResult();
}

/**
 * Handles the click event for the '-' button.
 * Toggles deletion mode, allowing for continuous deletion (Requirement 1).
 * @param {Event} e The click event.
 */
function handleAVDeleteToggle(e) {
    e.preventDefault();
    const deleteButton = e.currentTarget;
    const isDeleting = deleteButton.classList.toggle('deleting');
    const etaniItemRight = deleteButton.closest('.etaniItemRight');
    const etaniCol = deleteButton.closest('.etaniCol');
    
    const avItems = etaniItemRight.querySelectorAll('.etaniAVItem');
    
    // Toggle visual state of all items (except the first one)
    avItems.forEach((item, index) => {
        if (index > 0) {
            item.classList.toggle('deleting-target', isDeleting);
        }
    });

    // Toggle deleting state for this item's column
    etaniCol.classList.toggle('deleting-mode', isDeleting);

    // Disable all other AVDelete buttons if entering deleting mode
    // NOTE: This prevents simultaneous deletion in different items, which might be confusing.
    document.querySelectorAll('.etaniAVDelete').forEach(btn => {
        if (btn !== deleteButton) {
            btn.disabled = isDeleting;
        }
    });

    if (isDeleting) {
        deleteButton.title = "Click to Cancel Deletion";
        deleteButton.style.color = 'white'; // White icon for active state
    } else {
        deleteButton.title = "Delete Value";
        deleteButton.style.color = 'currentColor'; // Reset to default
        
        // Remove visual state from all items when cancelling
        avItems.forEach(item => {
            item.classList.remove('deleting-target');
        });
    }
}

/**
 * Handles the click event for an .etaniAVItem in deletion mode.
 * @param {Event} e The click event.
 */
function handleAVItemClick(e) {
    const itemToDelete = e.currentTarget;
    const itemIndex = Array.from(itemToDelete.parentNode.children).indexOf(itemToDelete);
    const etaniItemRight = itemToDelete.closest('.etaniItemRight');
    
    // Check if deletion mode is active and it's not the first item
    if (etaniItemRight.closest('.etaniCol').classList.contains('deleting-mode') && itemIndex > 0) {
        
        const deleteButton = etaniItemRight.querySelector('.etaniAVDelete');
        const useId = etaniItemRight.closest('.etaniItem').querySelector('.tileid').textContent;
        
        // 1. Find the corresponding <g> wrapper in etani_clone
        const cloneGWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useId}"]`);
        if (!cloneGWrapper) return;
        
        // 2. Update animateTransform values and duration
        const animates = cloneGWrapper.querySelectorAll('animateTransform');
        
        // Calculate new duration (starts from 0s)
        const currentDur = parseInt((animates[0].getAttribute('dur') || '0s').replace('s', '')) || 0;
        const newDur = Math.max(0, currentDur - 1); // Duration cannot be less than 0
        
        animates.forEach(animate => {
            const values = animate.getAttribute('values').split(';');
            
            // Remove the value at the target index (itemIndex)
            if (itemIndex < values.length) {
                values.splice(itemIndex, 1);
            }
            animate.setAttribute('values', values.join(';'));
            
            // Update duration (Requirement 5)
            animate.setAttribute('dur', `${newDur}s`);
        });

        // 3. Remove the item from the UI
        itemToDelete.remove();

        // 4. Update UI duration display
        etaniItemRight.querySelector('.etaniAnimateDur').textContent = `dur: ${newDur}s`;
        
        // 5. Update visual state of remaining items in deletion mode (Requirement 1)
        const remainingItems = etaniItemRight.querySelectorAll('.etaniAVItem');
        remainingItems.forEach((item, index) => {
             // Only apply deleting-target visual if deletion mode is still active
            item.classList.toggle('deleting-target', index > 0); 
        });

        // 6. Update the SVG result
        updateEtaniResult();
        
    } else if (itemIndex === 0 && etaniItemRight.closest('.etaniCol').classList.contains('deleting-mode')) {
        alert("The first value cannot be deleted.");
    }
}


/**
 * Handles the click event for the 'Transform' button.
 * Adds three animateTransform tags to all <use/> elements and adds controls to .etaniItemRight.
 */
function handleAllAppendTransformClick() {
    if (!etani_clone) return;

    const etdropUses = document.querySelectorAll('#etmain .etdrop use'); // Select original <use> elements for their href
    const etaniItemRights = document.querySelectorAll('.etaniItemRight');

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

        // Check if transform controls already exist to prevent duplication
        if (itemRight.querySelector('.etaniAnimate')) return; 

        // 1. Find the corresponding <use> element in etani_clone
        let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;

        // 2. Separate transform attributes based on Requirement 6
        const originalTransformString = cloneUseElement.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)';
        
        const translatePart = extractTransformPart(originalTransformString, 'translate');
        const scalePart = extractTransformPart(originalTransformString, 'scale');
        const rotatePart = extractTransformPart(originalTransformString, 'rotate');
        
        // 3. Wrap the <use/> element in a <g> tag (Requirement 7: No attributes except data-use-href)
        const gWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        gWrapper.setAttribute('data-use-href', `#${useId}`); // Use a data attribute to link it back
        
        // 4. Move transform parts: translate stays on <use>, scale/rotate move to <g> (Requirement 6)
        
        // a. <use> element: only translate remains
        if (translatePart.func) {
            cloneUseElement.setAttribute('transform', `${translatePart.func}(${translatePart.value})`);
        } else {
            // Ensure translate is explicit for the base
            cloneUseElement.setAttribute('transform', 'translate(0,0)');
        }
        
        // b. <g> element: receives scale and rotate
        let gTransformString = '';
        if (scalePart.func) gTransformString += `${scalePart.func}(${scalePart.value}) `;
        if (rotatePart.func) gTransformString += `${rotatePart.func}(${rotatePart.value}) `;
        
        // Default scale and rotate if missing
        if (!scalePart.func && !rotatePart.func) {
             gTransformString = 'scale(1,1) rotate(0)';
        } else if (!scalePart.func) {
             gTransformString = `scale(1,1) ${gTransformString}`;
        } else if (!rotatePart.func) {
             gTransformString = `${gTransformString} rotate(0)`;
        }
        
        gWrapper.setAttribute('transform', gTransformString.trim().replace(/\s+/g, ' '));
        
        // c. Perform the wrapping
        cloneUseElement.parentNode.insertBefore(gWrapper, cloneUseElement);
        gWrapper.appendChild(cloneUseElement);
        
        // 5. Add animateTransform tags to the <g> wrapper
        const baseAnimate = (type, initialValue) => {
            const animate = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
            animate.setAttribute('attributeName', 'transform');
            animate.setAttribute('attributeType', 'XML');
            animate.setAttribute('type', type);
            // Initial value is the base state's additive difference, which is always 0 or 1 for the first point
            animate.setAttribute('values', initialValue); 
            animate.setAttribute('dur', '0s'); // Initial duration (Requirement 5)
            animate.setAttribute('fill', 'freeze');
            animate.setAttribute('additive', 'sum'); // Important for relative transforms
            return animate;
        };

        // Note: translate must animate on the <use> element, scale/rotate on the <g> element for the separation to work.
        // However, since we use additive='sum', we can animate all types on the <g> for simplicity, 
        // as long as the base state (ov) is correctly retrieved during handleAVAddClick.
        // Let's stick to animating on <g> and retrieving base from both <use> and <g> in the click handler.
        
        gWrapper.appendChild(baseAnimate('translate', '0,0'));
        gWrapper.appendChild(baseAnimate('scale', '1')); 
        gWrapper.appendChild(baseAnimate('rotate', '0'));


        // 6. Add .etaniAnimate controls to .etaniItemRight
        const etaniAnimate = document.createElement('div');
        etaniAnimate.className = 'etaniAnimate';

        // Animate Name (Dark grey background, white text)
        const nameSpan = document.createElement('span');
        nameSpan.className = 'etaniAnimateName';
        nameSpan.textContent = 'transform';
        etaniAnimate.appendChild(nameSpan);

        // Animate Dur (dur: n)
        const durSpan = document.createElement('span');
        durSpan.className = 'etaniAnimateDur';
        durSpan.textContent = 'dur: 0s'; // Initial value 0s (Requirement 5)
        etaniAnimate.appendChild(durSpan);

        // Animate Value container
        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        // New container for buttons (Requirement 3)
        const avCtrlDiv = document.createElement('div');
        avCtrlDiv.className = 'etaniAVCtrl';

        // AV Add (Plus sign SVG)
        const avAddSpan = document.createElement('span');
        avAddSpan.className = 'etaniAVAdd';
        avAddSpan.title = 'Add Transform Value';
        avAddSpan.addEventListener('click', (e) => handleAVAddClick(e, useId));
        
        const plusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        plusSvg.setAttribute('width', '24');
        plusSvg.setAttribute('height', '24');
        plusSvg.setAttribute('viewBox', '0 0 24 24');
        plusSvg.setAttribute('fill', 'none');
        plusSvg.setAttribute('stroke', 'currentColor');
        plusSvg.setAttribute('stroke-width', '1'); // Requirement 2
        plusSvg.setAttribute('stroke-linecap', 'round');
        plusSvg.setAttribute('stroke-linejoin', 'round');
        plusSvg.innerHTML = '<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>';
        avAddSpan.appendChild(plusSvg);


        // AV Delete (Minus sign SVG)
        const avDeleteSpan = document.createElement('span');
        avDeleteSpan.className = 'etaniAVDelete';
        avDeleteSpan.title = 'Delete Value';
        avDeleteSpan.addEventListener('click', handleAVDeleteToggle);

        const minusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        minusSvg.setAttribute('width', '24');
        minusSvg.setAttribute('height', '24');
        minusSvg.setAttribute('viewBox', '0 0 24 24');
        minusSvg.setAttribute('fill', 'none');
        minusSvg.setAttribute('stroke', 'currentColor');
        minusSvg.setAttribute('stroke-width', '1'); // Requirement 2
        minusSvg.setAttribute('stroke-linecap', 'round');
        minusSvg.setAttribute('stroke-linejoin', 'round');
        minusSvg.innerHTML = '<line x1="5" y1="12" x2="19" y2="12"></line>';
        avDeleteSpan.appendChild(minusSvg);
        
        avCtrlDiv.appendChild(avAddSpan);
        avCtrlDiv.appendChild(avDeleteSpan);


        // AV Label
        const avLabelSpan = document.createElement('span');
        avLabelSpan.className = 'etaniAVLabel';
        avLabelSpan.textContent = 'values : ';

        // AV Container
        const avDiv = document.createElement('div');
        avDiv.className = 'etaniAV';
        
        // AV Item (Initially contains one)
        const avItemSpan = document.createElement('span');
        avItemSpan.className = 'etaniAVItem';
        avItemSpan.addEventListener('click', handleAVItemClick); // Bind deletion handler
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avCtrlDiv); // Append control buttons container (Requirement 3)
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

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

    // 7. Update the SVG result immediately
    updateEtaniResult();
}

/**
 * Handles the Setting Mode switch (Freeze/Repeat).
 * @param {string} mode The animation end mode ('freeze' or 'repeat').
 */
function handleSettingModeChange(mode) {
    if (!etani_clone) return;

    // We select animateTransform inside <g> wrappers
    const animates = etani_clone.querySelectorAll('.etdrop > g > animateTransform');
    const isRepeat = mode === 'repeat';

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

    // Update active state
    document.querySelector('.etaniSettingFreeze').classList.toggle('active', !isRepeat);
    document.querySelector('.etaniSettingRepeat').classList.toggle('active', isRepeat);

    updateEtaniResult();
}


// --- MAIN STRUCTURE CREATION ---

/**
 * Creates the internal animation control structure.
 * @param {HTMLElement} etaniouter The outer container element.
 */
function createEtaniInner(etaniouter) {
    // 5.1, 5.2, 5.3 Clone, remove .etwait, update ID
    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etani_clone = originalSvg.cloneNode(true);
    const etwaitElement = etani_clone.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    etani_clone.id = 'etmainani';


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

    // Create div.etaniCtrl
    const etaniCtrl = document.createElement('div');
    etaniCtrl.className = 'etaniCtrl';
    
    // Content and Center link
    const etaniContent = document.createElement('div');
    etaniContent.className = 'etaniContent';
    
    const contentHTMLLink = document.createElement('a');
    contentHTMLLink.className = 'etaniContentHTML';
    contentHTMLLink.textContent = 'Show HTML';
    contentHTMLLink.href = '#';
    contentHTMLLink.addEventListener('click', handleContentHTMLClick);
    etaniContent.appendChild(contentHTMLLink);

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


    // Setting Mode (Radio Buttons)
    const etaniSetting = document.createElement('div');
    etaniSetting.className = 'etaniSetting';
    
    const freezeRadio = document.createElement('span');
    freezeRadio.className = 'etaniSettingMode etaniSettingFreeze active';
    freezeRadio.textContent = 'Freeze';
    freezeRadio.setAttribute('data-mode', 'freeze');
    freezeRadio.addEventListener('click', () => handleSettingModeChange('freeze'));
    
    const repeatRadio = document.createElement('span');
    repeatRadio.className = 'etaniSettingMode etaniSettingRepeat';
    repeatRadio.textContent = 'Repeat';
    repeatRadio.setAttribute('data-mode', 'repeat');
    repeatRadio.addEventListener('click', () => handleSettingModeChange('repeat'));

    etaniSetting.appendChild(freezeRadio);
    etaniSetting.appendChild(repeatRadio);
    etaniCtrl.appendChild(etaniSetting);


    // All Append (Buttons)
    const etaniAllAppend = document.createElement('div');
    etaniAllAppend.className = 'etaniAllAppend';

    const transformButton = document.createElement('button');
    transformButton.className = 'etaniAllAppendTransform';
    transformButton.textContent = 'transform';
    transformButton.addEventListener('click', handleAllAppendTransformClick);
    
    const opacityButton = document.createElement('button');
    opacityButton.className = 'etaniAllAppendOpacity';
    opacityButton.textContent = 'opacity';
    // opacityButton.addEventListener('click', handleAllAppendOpacityClick); // Placeholder
    
    etaniAllAppend.appendChild(transformButton);
    etaniAllAppend.appendChild(opacityButton);
    etaniCtrl.appendChild(etaniAllAppend);
    

    // Create div.etaniCol (Tile list)
    const etaniCol = document.createElement('div');
    etaniCol.className = 'etaniCol';

    // Create div.etaniResult
    const etaniResult = document.createElement('div');
    etaniResult.className = 'etaniResult';
    
    // Result elements (img, download, rename, size) 
    const resultImg = document.createElement('img');
    resultImg.className = 'etaniResultImage';
    resultImg.alt = 'Rendered Ejtile Animation SVG';

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

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

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

    resultDRDiv.appendChild(downloadLink);
    resultDRDiv.appendChild(renameLink);
    
    etaniResult.appendChild(resultImg);
    etaniResult.appendChild(resultDRDiv);
    etaniResult.appendChild(sizeSpan);
    
    // Append children to etaniinner
    etaniinner.appendChild(etaniCtrl);
    etaniinner.appendChild(etaniCol);
    etaniinner.appendChild(etaniResult);

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

    // III, IV, V, VI, VII. Populate etaniCol (Tile list)
    const etdropUses = document.querySelectorAll('.etdrop use');
    const etwaitGroups = document.querySelectorAll('.etwait g');

    etdropUses.forEach((useElement, i) => {
        // (Tile processing logic - Unchanged)
        const tileid = useElement.getAttribute('href').substring(1);
        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 originalTile = document.querySelector(`defs g#${tileid}`);
        
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';

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

        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        
        if (originalTile) {
            const tileclone = originalTile.cloneNode(true);
            tileclone.removeAttribute('id');
            if (etwaittransform) tileclone.setAttribute('transform', etwaittransform);
            if (etwaitfill) tileclone.setAttribute('fill', etwaitfill);
            if (etwaitstroke) tileclone.setAttribute('stroke', etwaitstroke);
            if (etwaitstrokeWidth) tileclone.setAttribute('stroke-width', etwaitstrokeWidth);
            
            const svgWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            svgWrapper.setAttribute('width', '40');
            svgWrapper.setAttribute('height', '40');
            svgWrapper.setAttribute('version', '1.1');
            svgWrapper.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
            svgWrapper.setAttribute('xmlns:svg', 'http://www.w3.org/2000/svg');
            svgWrapper.className = 'etanitileimg';
            
            svgWrapper.appendChild(tileclone);
            etaniItemLeft.appendChild(svgWrapper);
        }

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

        etaniItem.appendChild(etaniItemLeft);
        etaniItem.appendChild(etaniItemRight);
        etaniCol.appendChild(etaniItem);
    });
    
    // 5. Update the result section immediately
    updateEtaniResult();
}

// --- INITIALIZATION ---

/**
 * Toggles the visibility and content of the animation control panel.
 * @param {Event} event The click event.
 */
function toggleAnimation(event) {
    const etanibutton = event.currentTarget;
    const etaniouter = etanibutton.parentNode;
    const etaniinner = document.getElementById('etaniinner');

    if (etanibutton.textContent === 'Animate it') {
        etanibutton.textContent = 'Close Ejtile Ani';
        createEtaniInner(etaniouter);
        
    } else if (etanibutton.textContent === 'Close Ejtile Ani') {
        etanibutton.textContent = 'Animate it';
        if (etaniinner) {
            etaniinner.remove();
        }
        etani_clone = null;
    }
}


window.addEventListener('load', () => {
    addDynamicStyles();

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

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

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

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

帖子 ejsoon »

這回是否放對了…

代码: 全选

改動:

一,在處於刪除狀態時,可以連續刪除多個,只有再次點擊.etaniAVDelete,才會取消刪除狀態。

二,.etaniAVAdd和.etaniAVDelete內部的svg,它的stroke-width改為1。

三,把.etaniAVAdd和.etaniAVDelete放到父元素div.etaniAVCtrl中。

四,增加css規則:

.etaniAVCtrl svg {margin-left: -1px;margin-top: -1px;}
.etaniAVCtrl span {
把先前.etaniAVAdd, .etaniAVDelete的樣式搬過來,並且display是inline-block,margin-right是3px
}

五,dur應從0s開始。

六,當往etani_clone的use中增加<animateTransform>時,translate仍然加到它的父元素,但是scale和rotate應加到<use></use>的內部。

用繁體中文回覆。在代碼中所有注釋都要用英文。

代码: 全选

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

// --- CSS STYLES ---

function addDynamicStyles() {
    if (document.getElementById('dynamic-et-styles')) {
        return;
    }

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

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

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

        /* IV. Styles for etaniResult */
        .etaniResult {
            text-align: center;
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #bbb;
            overflow: auto;
        }
        
        /* ------------------ CONTROL & BUTTON STYLES ------------------ */

        /* Styles for control links (Center button) */
        .etaniContent a {
            display: inline-block;
            margin: 0 5px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 14px;
        }
        
        .etaniCenter {
            border: 1px solid green;
            color: green;
        }
        
        .etaniContentHTML {
            border: 1px solid #0099ff;
            color: #0099ff;
            margin-right: 15px;
        }

        /* Requirement 3: etaniAllAppend buttons */
        .etaniAllAppend button {
            padding: 5px 10px;
            font-size: 16px;
            margin: 0 5px;
            cursor: pointer;
            border: 1px solid #333;
            background-color: #fff;
        }
        
        /* Requirement 2: Custom Radio button styles */
        .etaniSettingMode {
            display: inline-block;
            cursor: pointer;
            padding: 4px 8px;
            margin: 0 3px;
            font-size: 14px;
            border: 1px solid #888;
            background-color: #eee;
            color: #333;
            user-select: none;
        }

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

        /* ------------------ RESULT & ANIMAION ITEM STYLES ------------------ */

        .etaniResultDR {
            text-align: center;
            margin-bottom: 10px;
        }

        /* Requirement 1: Download and rename link font size 16px */
        .etaniResultDownload, .etaniResultRename {
            display: inline-block;
            margin-right: 15px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 16px; 
        }
        
        /* .etaniResultDownload default style */
        .etaniResultDownload {
            border: 1px solid blue;
            color: blue;
        }

        /* .etaniResultRename style (Requirement 1: color changed to brown) */
        .etaniResultRename {
            border: 1px solid brown;
            color: brown;
        }

        .etaniResultImage {
            display: block;
            max-width: 480px;
            width: 100%;
            height: auto;
            margin: 0 auto 10px auto;
            border: 1px solid #000;
        }
        
        /* Requirement 2: Minimum font size 12px */
        .etaniResultSize {
            display: inline-block;
            margin-left: 10px;
            font-size: 12px; 
            color: #555;
        }
        
        /* etaniItem structure (Float layout) */
        .etaniItem {
            min-height: 48px;
            border: 1px solid #ccc;
            box-sizing: border-box;
            width: 100%;
            margin-bottom: -1px;
            overflow: auto;
        }
        
        .etaniItemLeft {
            float: left; 
            width: 60px;
            min-height: 48px;
            border-right: 1px solid #ccc;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            background-color: #e8e8e8;
            padding: 2px 0;
        }
        
        .etaniItemRight {
            margin-left: 60px; 
            padding: 7px;
            min-height: 48px; 
            background-color: #fff;
        }

        /* Requirement 2: Minimum font size 12px */
        .tileid {
            text-align: center;
            font-size: 12px; 
            word-break: break-all;
            padding-top: 2px;
        }
        
        /* ------------------ ANIMATE CONTROLS (4.1) ------------------ */

        .etaniAnimate {
            border: 1px solid #999;
            padding: 5px;
            margin-bottom: 5px;
        }

        .etaniAnimateName {
            display: inline-block;
            padding: 2px 5px;
            background-color: #555; /* Dark grey background */
            color: white; /* White text */
            margin-right: 10px;
            font-size: 12px; /* Minimum font size 12px */
        }

        .etaniAnimateDur {
            display: inline-block;
            margin-right: 10px;
            font-size: 14px;
        }

        .etaniAnimateValue {
            margin-top: 5px;
            overflow: auto;
        }

        /* New container for Add/Delete buttons (Requirement 3) */
        .etaniAVCtrl {
            display: inline-block;
            vertical-align: top;
            margin-right: 5px;
        }
        
        /* Custom CSS rule for SVG alignment (Requirement 4) */
        .etaniAVCtrl svg {
            margin-left: -1px;
            margin-top: -1px;
        }

        /* Shared style for control buttons (Requirement 4) */
        .etaniAVCtrl span {
            display: inline-block;
            width: 24px;
            height: 24px;
            line-height: 24px;
            text-align: center;
            cursor: pointer;
            box-sizing: border-box;
            vertical-align: top;
            margin-right: 3px; /* New margin */
        }

        .etaniAVAdd {
            background-color: #a7fca7; /* Light green */
            border: 1px solid #71c371; /* Thin line near light green */
        }
        
        .etaniAVDelete {
            background-color: #ffcccc; /* Light red */
            border: 1px solid #cc3333; /* Darker red */
        }

        .etaniAVDelete.deleting {
            background-color: #cc3333; /* Dark red when active */
        }
        
        .etaniAVLabel {
            font-size: 14px;
            margin-right: 5px;
        }

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

        .etaniAVItem {
            display: inline-block;
            width: 24px;
            height: 24px;
            background-color: #ff9933; /* Dark orange */
            border: 1px dashed #00bfff; /* Dashed sky blue */
            margin: 0 5px;
            box-sizing: border-box;
            cursor: pointer;
            position: relative;
        }

        .etaniAVItem.deleting-target {
            background-color: #ff4d4d; /* Reddish when targeted for deletion */
            border: 2px solid red;
        }
        
        /* ------------------ MODAL/POPUP STYLES ------------------ */
        .modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.5);
            z-index: 1000;
        }
        .modal-content {
            position: fixed;
            width: 98%;
            height: 48%;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 10px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
            z-index: 1001;
            box-sizing: border-box;
        }
        .modal-content textarea {
            width: 100%;
            height: calc(100% - 50px); /* Adjusted for larger close button */
            resize: none;
            border: 1px solid #ccc;
            font-size: 12px;
            box-sizing: border-box;
        }
        .modal-close {
            position: absolute;
            top: 0;
            right: 10px;
            font-size: 48px;
            line-height: 1; 
            cursor: pointer;
            color: #333;
        }
    `;

    document.head.appendChild(styleSheet);
}

// --- UTILITY FUNCTIONS ---

/**
 * Extracts a specific transform function and its value from a transform string.
 * @param {string} transformString The full transform string.
 * @param {string} type The transform type ('translate', 'scale', 'rotate').
 * @returns {{func: string, value: string}} The transform function and its value.
 */
function extractTransformPart(transformString, type) {
    const regex = new RegExp(`(${type})\\(([^)]+)\\)`, 'i');
    const match = transformString.match(regex);
    if (match) {
        return { func: match[1], value: match[2].trim() };
    }
    return { func: '', value: '' };
}

/**
 * Parses transform string to get individual transform values.
 * @param {string} transformString The transform attribute string.
 * @returns {{translate: string, scale: string, rotate: string}} Parsed values.
 */
function parseTransform(transformString) {
    const defaultTransform = { translate: '0,0', scale: '1,1', rotate: '0' };
    if (!transformString) return defaultTransform;

    const transform = {};

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

    transform.translate = getMatch('translate') || defaultTransform.translate;
    transform.scale = getMatch('scale') || defaultTransform.scale;
    // Normalize scale: if only one value (e.g., '1'), ensure it's '1,1' for coordinate parsing
    if (transform.scale.split(',').length === 1) {
        transform.scale += `,${transform.scale}`;
    }
    transform.rotate = getMatch('rotate') || defaultTransform.rotate;

    return transform;
}


/**
 * Converts an SVG string to a Base64 data URL.
 * @param {string} svgString The SVG XML string.
 * @returns {string} The Base64 data URL.
 */
function svgToBase64(svgString) {
    const base64 = btoa(unescape(encodeURIComponent(svgString)));
    return `data:image/svg+xml;base64,${base64}`;
}

/**
 * Formats byte size into human-readable string (e.g., KB, MB).
 * @param {number} bytes The size in bytes.
 * @returns {string} The formatted string.
 */
function formatBytes(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

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

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

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

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

/**
 * Handler for the 'Center' button click. Resets the .etdrop transform.
 * @param {Event} e The click event.
 */
function handleCenterClick(e) {
    e.preventDefault();
    if (etani_clone) {
        const etdrop = etani_clone.querySelector('.etdrop');
        if (etdrop) {
            // Reset position to the original center for the entire group
            etdrop.setAttribute('transform', 'translate(240,240) scale(1,1)');
            updateEtaniResult();
        }
    }
}

/**
 * Handles the HTML popup window.
 * @param {Event} e The click event.
 */
function handleContentHTMLClick(e) {
    e.preventDefault();
    if (!etani_clone) return;

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

    // Close logic
    const closeModal = () => {
        if (document.body.contains(overlay)) document.body.removeChild(overlay);
        if (document.body.contains(content)) document.body.removeChild(content);
    };
    close.onclick = closeModal;
    overlay.onclick = closeModal; // Clicking background also closes

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

// --- NEW/MODIFIED HANDLERS ---

/**
 * Calculates the required *additive* transform value for a given type.
 * @param {string} type The transform type ('translate', 'scale', 'rotate').
 * @param {string} originalValue The transform value from the original <use/> (cv).
 * @param {string} currentBaseValue The current base transform value from the etani_clone <use/> or <g> (ov).
 * @returns {string} The new additive value (v).
 */
function calculateAdditiveValue(type, originalValue, currentBaseValue) {
    // Both values are normalized coordinate strings (e.g., "10,20" or "0" or "1.5,1.5")
    
    if (type === 'translate') {
        const ovCoords = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
        const cvCoords = originalValue.split(',').map(c => parseFloat(c.trim()));
        // v1 = cv1 - ov1, v2 = cv2 - ov2
        return `${(cvCoords[0] - ovCoords[0]).toFixed(4)},${(cvCoords[1] - ovCoords[1]).toFixed(4)}`;
    } else if (type === 'scale') {
        const ovScales = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
        const cvScales = originalValue.split(',').map(c => parseFloat(c.trim()));
        // v1 = cv1 / ov1, v2 = cv2 / ov2.
        const v1 = (cvScales[0] / ovScales[0]).toFixed(4);
        const v2 = (cvScales.length > 1 ? (cvScales[1] / ovScales[1]) : v1).toFixed(4);
        return v1 === v2 ? v1 : `${v1},${v2}`;
    } else if (type === 'rotate') {
        const ovAngle = parseFloat(currentBaseValue.trim());
        const cvAngle = parseFloat(originalValue.trim());
        // v3 = cv3 - ov3
        return (cvAngle - ovAngle).toFixed(4);
    }
    return '';
}

/**
 * Handles the click event for the '+' button.
 * Adds a new animateTransform value.
 * @param {Event} e The click event.
 * @param {string} useElementId The ID of the original <g> element referenced by the <use/>.
 */
function handleAVAddClick(e, useElementId) {
    e.preventDefault();
    if (!etani_clone) return;

    // 1. Get current transform of the original <use/> element in svg#etmain (cv)
    const originalUseElement = document.querySelector(`.etdrop use[href="#${useElementId}"]`);
    if (!originalUseElement) return;

    const originalTransformString = originalUseElement.getAttribute('transform') || '';
    const originalTransforms = parseTransform(originalTransformString);
    
    // 2. Find the corresponding <g> wrapper and <use> in etani_clone
    const cloneGWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useElementId}"]`);
    const cloneUseElement = cloneGWrapper ? cloneGWrapper.querySelector('use') : null;
    if (!cloneGWrapper || !cloneUseElement) return;
    
    // 3. Get the *base* transform values from the clone's <g> (translate) and <use> (scale/rotate)
    const cloneTranslateString = cloneGWrapper.getAttribute('transform') || 'translate(0,0)'; // <g> holds base translate
    const cloneSRString = cloneUseElement.getAttribute('transform') || 'scale(1,1) rotate(0)'; // <use> holds base scale/rotate
    
    const currentBaseTranslate = parseTransform(cloneTranslateString).translate;
    const currentBaseScale = parseTransform(cloneSRString).scale;
    const currentBaseRotate = parseTransform(cloneSRString).rotate;

    // 4. Update the animateTransform tags' values and duration 
    // Translate animates on G, Scale/Rotate animates on USE
    const animatesG = cloneGWrapper.querySelectorAll('animateTransform[type="translate"]');
    const animatesUse = cloneUseElement.querySelectorAll('animateTransform[type="scale"], animateTransform[type="rotate"]');
    
    const allAnimates = [...animatesG, ...animatesUse];
    
    // Calculate new duration (starts from 0s, adds 1s)
    const currentDur = parseInt((allAnimates[0].getAttribute('dur') || '0s').replace('s', '')) || 0;
    const newDur = currentDur + 1;

    allAnimates.forEach(animate => {
        const type = animate.getAttribute('type').toLowerCase();
        let currentValueString = animate.getAttribute('values') || '';
        let newValue = '';
        
        // Calculate the *additive* difference
        if (type === 'translate') {
            newValue = calculateAdditiveValue('translate', originalTransforms.translate, currentBaseTranslate);
        } else if (type === 'scale') {
            newValue = calculateAdditiveValue('scale', originalTransforms.scale, currentBaseScale);
        } else if (type === 'rotate') {
            newValue = calculateAdditiveValue('rotate', originalTransforms.rotate, currentBaseRotate);
        }
        
        // Append the new value
        animate.setAttribute('values', (currentValueString ? currentValueString + ';' : '') + newValue);

        // Update duration (Requirement 5)
        animate.setAttribute('dur', `${newDur}s`);
    });

    // 5. Add a new .etaniAVItem in .etaniItemRight and update UI duration
    const etaniItemRight = e.currentTarget.closest('.etaniItemRight');
    const etaniAV = etaniItemRight.querySelector('.etaniAV');
    if (etaniAV) {
        const newAVItem = document.createElement('span');
        newAVItem.className = 'etaniAVItem';
        newAVItem.addEventListener('click', handleAVItemClick); // Bind deletion handler
        etaniAV.appendChild(newAVItem);
    }
    etaniItemRight.querySelector('.etaniAnimateDur').textContent = `dur: ${newDur}s`;

    // 6. Update the SVG result
    updateEtaniResult();
}

/**
 * Handles the click event for the '-' button.
 * Toggles deletion mode, allowing for continuous deletion (Requirement 1).
 * @param {Event} e The click event.
 */
function handleAVDeleteToggle(e) {
    e.preventDefault();
    const deleteButton = e.currentTarget;
    const isDeleting = deleteButton.classList.toggle('deleting');
    const etaniItemRight = deleteButton.closest('.etaniItemRight');
    const etaniCol = deleteButton.closest('.etaniCol');
    
    const avItems = etaniItemRight.querySelectorAll('.etaniAVItem');
    
    // Toggle visual state of all items (except the first one)
    avItems.forEach((item, index) => {
        if (index > 0) {
            // Apply deleting-target class only if deleting mode is active
            item.classList.toggle('deleting-target', isDeleting);
        }
    });

    // Toggle deleting state for this item's column
    etaniCol.classList.toggle('deleting-mode', isDeleting);

    // Disable all other AVDelete buttons if entering deleting mode
    document.querySelectorAll('.etaniAVDelete').forEach(btn => {
        if (btn !== deleteButton) {
            btn.closest('.etaniAVCtrl').disabled = isDeleting;
            btn.style.opacity = isDeleting ? 0.5 : 1; // Visual feedback for disabled state
        }
    });

    if (isDeleting) {
        deleteButton.title = "Click value item to delete (Click again to cancel)";
        deleteButton.style.color = 'white'; // White icon for active state
    } else {
        deleteButton.title = "Delete Value";
        deleteButton.style.color = 'currentColor'; // Reset to default
        
        // Remove visual state from all items when cancelling
        avItems.forEach(item => {
            item.classList.remove('deleting-target');
        });
    }
}

/**
 * Handles the click event for an .etaniAVItem in deletion mode.
 * @param {Event} e The click event.
 */
function handleAVItemClick(e) {
    const itemToDelete = e.currentTarget;
    const itemIndex = Array.from(itemToDelete.parentNode.children).indexOf(itemToDelete);
    const etaniItemRight = itemToDelete.closest('.etaniItemRight');
    
    // Check if deletion mode is active and it's not the first item
    if (etaniItemRight.closest('.etaniCol').classList.contains('deleting-mode') && itemIndex > 0) {
        
        const deleteButton = etaniItemRight.querySelector('.etaniAVDelete');
        const useId = etaniItemRight.closest('.etaniItem').querySelector('.tileid').textContent;
        
        // 1. Find the corresponding <g> wrapper and <use> in etani_clone
        const cloneGWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useId}"]`);
        const cloneUseElement = cloneGWrapper.querySelector('use');
        if (!cloneGWrapper || !cloneUseElement) return;
        
        // 2. Update animateTransform values and duration
        const animatesG = cloneGWrapper.querySelectorAll('animateTransform[type="translate"]');
        const animatesUse = cloneUseElement.querySelectorAll('animateTransform[type="scale"], animateTransform[type="rotate"]');
        const allAnimates = [...animatesG, ...animatesUse];
        
        // Calculate new duration (starts from 0s)
        const currentDur = parseInt((allAnimates[0].getAttribute('dur') || '0s').replace('s', '')) || 0;
        const newDur = Math.max(0, currentDur - 1); // Duration cannot be less than 0
        
        allAnimates.forEach(animate => {
            const values = animate.getAttribute('values').split(';');
            
            // Remove the value at the target index (itemIndex)
            if (itemIndex < values.length) {
                values.splice(itemIndex, 1);
            }
            animate.setAttribute('values', values.join(';'));
            
            // Update duration (Requirement 5)
            animate.setAttribute('dur', `${newDur}s`);
        });

        // 3. Remove the item from the UI
        itemToDelete.remove();

        // 4. Update UI duration display
        etaniItemRight.querySelector('.etaniAnimateDur').textContent = `dur: ${newDur}s`;
        
        // 5. Update visual state of remaining items in deletion mode (Requirement 1)
        const remainingItems = etaniItemRight.querySelectorAll('.etaniAVItem');
        
        // If no values remain (only the base state item[0]), exit deletion mode automatically
        if (remainingItems.length === 1) {
            deleteButton.click(); // Programmatically click to toggle out of deleting mode
        } else {
            // Otherwise, keep deletion mode active but update visual state
            remainingItems.forEach((item, index) => {
                 item.classList.toggle('deleting-target', index > 0); 
            });
        }

        // 6. Update the SVG result
        updateEtaniResult();
        
    } else if (itemIndex === 0 && etaniItemRight.closest('.etaniCol').classList.contains('deleting-mode')) {
        alert("The first value cannot be deleted.");
    }
}


/**
 * Handles the click event for the 'Transform' button.
 * Adds three animateTransform tags to all <use/> elements and adds controls to .etaniItemRight.
 */
function handleAllAppendTransformClick() {
    if (!etani_clone) return;

    const etdropUses = document.querySelectorAll('#etmain .etdrop use'); // Select original <use> elements for their href
    const etaniItemRights = document.querySelectorAll('.etaniItemRight');

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

        // Check if transform controls already exist to prevent duplication
        if (itemRight.querySelector('.etaniAnimate')) return; 

        // 1. Find the corresponding <use> element in etani_clone
        let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;

        // 2. Separate transform attributes based on Requirement 6
        const originalTransformString = cloneUseElement.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)';
        
        const translatePart = extractTransformPart(originalTransformString, 'translate');
        const scalePart = extractTransformPart(originalTransformString, 'scale');
        const rotatePart = extractTransformPart(originalTransformString, 'rotate');
        
        // 3. Wrap the <use/> element in a <g> tag (Requirement 7 from previous: No attributes except data-use-href)
        const gWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        gWrapper.setAttribute('data-use-href', `#${useId}`); 
        
        // 4. Move transform parts and set base state (Requirement 6)
        
        // a. <g> element: receives translate base state.
        let gTransformString = '';
        if (translatePart.func) {
            gTransformString = `${translatePart.func}(${translatePart.value})`;
        } else {
            gTransformString = 'translate(0,0)';
        }
        gWrapper.setAttribute('transform', gTransformString.trim());
        
        // b. <use> element: receives scale and rotate base state.
        let useTransformString = '';
        if (scalePart.func) useTransformString += `${scalePart.func}(${scalePart.value}) `;
        if (rotatePart.func) useTransformString += `${rotatePart.func}(${rotatePart.value}) `;
        
        // Ensure scale and rotate are explicit for the base if missing
        if (!scalePart.func) {
             useTransformString = `scale(1,1) ${useTransformString}`;
        }
        if (!rotatePart.func) {
             useTransformString = `${useTransformString} rotate(0)`;
        }
        cloneUseElement.setAttribute('transform', useTransformString.trim().replace(/\s+/g, ' '));
        
        // Remove the original full transform attribute to avoid conflicts
        cloneUseElement.removeAttribute('transform'); 
        if (useTransformString.trim()) {
            cloneUseElement.setAttribute('transform', useTransformString.trim().replace(/\s+/g, ' '));
        }

        // c. Perform the wrapping
        cloneUseElement.parentNode.insertBefore(gWrapper, cloneUseElement);
        gWrapper.appendChild(cloneUseElement);
        
        // 5. Add animateTransform tags (Requirement 6: Placement changed)
        const baseAnimate = (type, targetElement, initialValue) => {
            const animate = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
            animate.setAttribute('attributeName', 'transform');
            animate.setAttribute('attributeType', 'XML');
            animate.setAttribute('type', type);
            // Initial value is the base state's additive difference, which is always 0 or 1 for the first point
            animate.setAttribute('values', initialValue); 
            animate.setAttribute('dur', '0s'); // Initial duration (Requirement 5)
            animate.setAttribute('fill', 'freeze');
            animate.setAttribute('additive', 'sum'); // Important for relative transforms
            targetElement.appendChild(animate);
        };

        // translate animates on the <g> wrapper
        baseAnimate('translate', gWrapper, '0,0');
        // scale and rotate animate on the <use> element
        baseAnimate('scale', cloneUseElement, '1'); 
        baseAnimate('rotate', cloneUseElement, '0');


        // 6. Add .etaniAnimate controls to .etaniItemRight
        const etaniAnimate = document.createElement('div');
        etaniAnimate.className = 'etaniAnimate';

        // Animate Name (Dark grey background, white text)
        const nameSpan = document.createElement('span');
        nameSpan.className = 'etaniAnimateName';
        nameSpan.textContent = 'transform';
        etaniAnimate.appendChild(nameSpan);

        // Animate Dur (dur: n)
        const durSpan = document.createElement('span');
        durSpan.className = 'etaniAnimateDur';
        durSpan.textContent = 'dur: 0s'; // Initial value 0s (Requirement 5)
        etaniAnimate.appendChild(durSpan);

        // Animate Value container
        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        // New container for buttons (Requirement 3)
        const avCtrlDiv = document.createElement('div');
        avCtrlDiv.className = 'etaniAVCtrl';

        // AV Add (Plus sign SVG)
        const avAddSpan = document.createElement('span');
        avAddSpan.className = 'etaniAVAdd';
        avAddSpan.title = 'Add Transform Value';
        avAddSpan.addEventListener('click', (e) => handleAVAddClick(e, useId));
        
        const plusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        plusSvg.setAttribute('width', '24');
        plusSvg.setAttribute('height', '24');
        plusSvg.setAttribute('viewBox', '0 0 24 24');
        plusSvg.setAttribute('fill', 'none');
        plusSvg.setAttribute('stroke', 'currentColor');
        plusSvg.setAttribute('stroke-width', '1'); // Requirement 2
        plusSvg.setAttribute('stroke-linecap', 'round');
        plusSvg.setAttribute('stroke-linejoin', 'round');
        plusSvg.innerHTML = '<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>';
        avAddSpan.appendChild(plusSvg);


        // AV Delete (Minus sign SVG)
        const avDeleteSpan = document.createElement('span');
        avDeleteSpan.className = 'etaniAVDelete';
        avDeleteSpan.title = 'Delete Value';
        avDeleteSpan.addEventListener('click', handleAVDeleteToggle);

        const minusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        minusSvg.setAttribute('width', '24');
        minusSvg.setAttribute('height', '24');
        minusSvg.setAttribute('viewBox', '0 0 24 24');
        minusSvg.setAttribute('fill', 'none');
        minusSvg.setAttribute('stroke', 'currentColor');
        minusSvg.setAttribute('stroke-width', '1'); // Requirement 2
        minusSvg.setAttribute('stroke-linecap', 'round');
        minusSvg.setAttribute('stroke-linejoin', 'round');
        minusSvg.innerHTML = '<line x1="5" y1="12" x2="19" y2="12"></line>';
        avDeleteSpan.appendChild(minusSvg);
        
        avCtrlDiv.appendChild(avAddSpan);
        avCtrlDiv.appendChild(avDeleteSpan);


        // AV Label
        const avLabelSpan = document.createElement('span');
        avLabelSpan.className = 'etaniAVLabel';
        avLabelSpan.textContent = 'values : ';

        // AV Container
        const avDiv = document.createElement('div');
        avDiv.className = 'etaniAV';
        
        // AV Item (Initially contains one)
        const avItemSpan = document.createElement('span');
        avItemSpan.className = 'etaniAVItem';
        avItemSpan.addEventListener('click', handleAVItemClick); // Bind deletion handler
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avCtrlDiv); // Append control buttons container (Requirement 3)
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

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

    // 7. Update the SVG result immediately
    updateEtaniResult();
}

/**
 * Handles the Setting Mode switch (Freeze/Repeat).
 * @param {string} mode The animation end mode ('freeze' or 'repeat').
 */
function handleSettingModeChange(mode) {
    if (!etani_clone) return;

    // We select animateTransform inside <g> wrappers and <use> elements
    const animatesG = etani_clone.querySelectorAll('.etdrop > g > animateTransform');
    const animatesUse = etani_clone.querySelectorAll('.etdrop > g > use > animateTransform');
    const allAnimates = [...animatesG, ...animatesUse];
    
    const isRepeat = mode === 'repeat';

    allAnimates.forEach(animate => {
        if (isRepeat) {
            animate.removeAttribute('fill');
            animate.setAttribute('repeatCount', 'indefinite');
        } else {
            animate.removeAttribute('repeatCount');
            animate.setAttribute('fill', 'freeze');
        }
    });

    // Update active state
    document.querySelector('.etaniSettingFreeze').classList.toggle('active', !isRepeat);
    document.querySelector('.etaniSettingRepeat').classList.toggle('active', isRepeat);

    updateEtaniResult();
}


// --- MAIN STRUCTURE CREATION ---

/**
 * Creates the internal animation control structure.
 * @param {HTMLElement} etaniouter The outer container element.
 */
function createEtaniInner(etaniouter) {
    // 5.1, 5.2, 5.3 Clone, remove .etwait, update ID
    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etani_clone = originalSvg.cloneNode(true);
    const etwaitElement = etani_clone.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    etani_clone.id = 'etmainani';


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

    // Create div.etaniCtrl
    const etaniCtrl = document.createElement('div');
    etaniCtrl.className = 'etaniCtrl';
    
    // Content and Center link
    const etaniContent = document.createElement('div');
    etaniContent.className = 'etaniContent';
    
    const contentHTMLLink = document.createElement('a');
    contentHTMLLink.className = 'etaniContentHTML';
    contentHTMLLink.textContent = 'Show HTML';
    contentHTMLLink.href = '#';
    contentHTMLLink.addEventListener('click', handleContentHTMLClick);
    etaniContent.appendChild(contentHTMLLink);

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


    // Setting Mode (Radio Buttons)
    const etaniSetting = document.createElement('div');
    etaniSetting.className = 'etaniSetting';
    
    const freezeRadio = document.createElement('span');
    freezeRadio.className = 'etaniSettingMode etaniSettingFreeze active';
    freezeRadio.textContent = 'Freeze';
    freezeRadio.setAttribute('data-mode', 'freeze');
    freezeRadio.addEventListener('click', () => handleSettingModeChange('freeze'));
    
    const repeatRadio = document.createElement('span');
    repeatRadio.className = 'etaniSettingMode etaniSettingRepeat';
    repeatRadio.textContent = 'Repeat';
    repeatRadio.setAttribute('data-mode', 'repeat');
    repeatRadio.addEventListener('click', () => handleSettingModeChange('repeat'));

    etaniSetting.appendChild(freezeRadio);
    etaniSetting.appendChild(repeatRadio);
    etaniCtrl.appendChild(etaniSetting);


    // All Append (Buttons)
    const etaniAllAppend = document.createElement('div');
    etaniAllAppend.className = 'etaniAllAppend';

    const transformButton = document.createElement('button');
    transformButton.className = 'etaniAllAppendTransform';
    transformButton.textContent = 'transform';
    transformButton.addEventListener('click', handleAllAppendTransformClick);
    
    const opacityButton = document.createElement('button');
    opacityButton.className = 'etaniAllAppendOpacity';
    opacityButton.textContent = 'opacity';
    // opacityButton.addEventListener('click', handleAllAppendOpacityClick); // Placeholder
    
    etaniAllAppend.appendChild(transformButton);
    etaniAllAppend.appendChild(opacityButton);
    etaniCtrl.appendChild(etaniAllAppend);
    

    // Create div.etaniCol (Tile list)
    const etaniCol = document.createElement('div');
    etaniCol.className = 'etaniCol';

    // Create div.etaniResult
    const etaniResult = document.createElement('div');
    etaniResult.className = 'etaniResult';
    
    // Result elements (img, download, rename, size) 
    const resultImg = document.createElement('img');
    resultImg.className = 'etaniResultImage';
    resultImg.alt = 'Rendered Ejtile Animation SVG';

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

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

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

    resultDRDiv.appendChild(downloadLink);
    resultDRDiv.appendChild(renameLink);
    
    etaniResult.appendChild(resultImg);
    etaniResult.appendChild(resultDRDiv);
    etaniResult.appendChild(sizeSpan);
    
    // Append children to etaniinner
    etaniinner.appendChild(etaniCtrl);
    etaniinner.appendChild(etaniCol);
    etaniinner.appendChild(etaniResult);

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

    // III, IV, V, VI, VII. Populate etaniCol (Tile list)
    const etdropUses = document.querySelectorAll('.etdrop use');
    const etwaitGroups = document.querySelectorAll('.etwait g');

    etdropUses.forEach((useElement, i) => {
        // (Tile processing logic - Unchanged)
        const tileid = useElement.getAttribute('href').substring(1);
        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 originalTile = document.querySelector(`defs g#${tileid}`);
        
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';

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

        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        
        if (originalTile) {
            const tileclone = originalTile.cloneNode(true);
            tileclone.removeAttribute('id');
            if (etwaittransform) tileclone.setAttribute('transform', etwaittransform);
            if (etwaitfill) tileclone.setAttribute('fill', etwaitfill);
            if (etwaitstroke) tileclone.setAttribute('stroke', etwaitstroke);
            if (etwaitstrokeWidth) tileclone.setAttribute('stroke-width', etwaitstrokeWidth);
            
            const svgWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            svgWrapper.setAttribute('width', '40');
            svgWrapper.setAttribute('height', '40');
            svgWrapper.setAttribute('version', '1.1');
            svgWrapper.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
            svgWrapper.setAttribute('xmlns:svg', 'http://www.w3.org/2000/svg');
            svgWrapper.className = 'etanitileimg';
            
            svgWrapper.appendChild(tileclone);
            etaniItemLeft.appendChild(svgWrapper);
        }

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

        etaniItem.appendChild(etaniItemLeft);
        etaniItem.appendChild(etaniItemRight);
        etaniCol.appendChild(etaniItem);
    });
    
    // 5. Update the result section immediately
    updateEtaniResult();
}

// --- INITIALIZATION ---

/**
 * Toggles the visibility and content of the animation control panel.
 * @param {Event} event The click event.
 */
function toggleAnimation(event) {
    const etanibutton = event.currentTarget;
    const etaniouter = etanibutton.parentNode;
    const etaniinner = document.getElementById('etaniinner');

    if (etanibutton.textContent === 'Animate it') {
        etanibutton.textContent = 'Close Ejtile Ani';
        createEtaniInner(etaniouter);
        
    } else if (etanibutton.textContent === 'Close Ejtile Ani') {
        etanibutton.textContent = 'Animate it';
        if (etaniinner) {
            etaniinner.remove();
        }
        etani_clone = null;
    }
}


window.addEventListener('load', () => {
    addDynamicStyles();

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

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

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

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

帖子 ejsoon »

代码: 全选

改動:

一,當按下etanibutton時,先在etani_clone中的.etdrop中的<use></use>內加入animateTransform[type=rotate][value=0]。

然後在其外加第一層<g>,把animateTransform[type=scale][value=1,1]加入。

之後再在其外加第二層<g>,把animateTransform[type=translate][value=0,0]加入。

不要改動etani_clone中的.etdrop中的<use>的transform屬性值,也不需要把這些屬性搬至父元素。

二,當點擊etaniAVAdd增加value時,設它的三個標籤的值的計算結果為translate(v1) scale(v2) rotate(v3)。它的計算方法是:

首先取出它在svg#etmain中的.etdrop中的<use/>的屬性transform="translate(cv1) scale(cv2) rotate(cv3)"的三個值。

之後找到在etani_clone中的.etdrop中的<use/>的屬性transform="translate(ov1) scale(ov2) rotate(ov3)"的三個值。

v1=cv1-ov1
v2=cv2/ov2
v3=cv3-ov3

注意:ov1、ov2、ov3一般而言是固定不變的,而不是上一個值。

計算時,要四捨五入,先*1000,再round,再/1000。

三,在增加animateTransform時,是加fill=freeze還是加repeatCount,要看etaniSettingMode的當前狀態。

四,當dur=0s時,不用加到etani_clone中,如果有的話就移除,僅當dur大於0時才加入。

用繁體中文回覆。在代碼中所有注釋都要用英文。

代码: 全选

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

// --- CSS STYLES (Kept from previous iteration as no changes were requested here) ---

function addDynamicStyles() {
    if (document.getElementById('dynamic-et-styles')) {
        return;
    }

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

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

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

        /* IV. Styles for etaniResult */
        .etaniResult {
            text-align: center;
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #bbb;
            overflow: auto;
        }
        
        /* ------------------ CONTROL & BUTTON STYLES ------------------ */

        /* Styles for control links (Center button) */
        .etaniContent a {
            display: inline-block;
            margin: 0 5px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 14px;
        }
        
        .etaniCenter {
            border: 1px solid green;
            color: green;
        }
        
        .etaniContentHTML {
            border: 1px solid #0099ff;
            color: #0099ff;
            margin-right: 15px;
        }

        /* Requirement 3: etaniAllAppend buttons */
        .etaniAllAppend button {
            padding: 5px 10px;
            font-size: 16px;
            margin: 0 5px;
            cursor: pointer;
            border: 1px solid #333;
            background-color: #fff;
        }
        
        /* Requirement 2: Custom Radio button styles */
        .etaniSettingMode {
            display: inline-block;
            cursor: pointer;
            padding: 4px 8px;
            margin: 0 3px;
            font-size: 14px;
            border: 1px solid #888;
            background-color: #eee;
            color: #333;
            user-select: none;
        }

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

        /* ------------------ RESULT & ANIMAION ITEM STYLES ------------------ */

        .etaniResultDR {
            text-align: center;
            margin-bottom: 10px;
        }

        /* Requirement 1: Download and rename link font size 16px */
        .etaniResultDownload, .etaniResultRename {
            display: inline-block;
            margin-right: 15px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 16px; 
        }
        
        /* .etaniResultDownload default style */
        .etaniResultDownload {
            border: 1px solid blue;
            color: blue;
        }

        /* .etaniResultRename style (Requirement 1: color changed to brown) */
        .etaniResultRename {
            border: 1px solid brown;
            color: brown;
        }

        .etaniResultImage {
            display: block;
            max-width: 480px;
            width: 100%;
            height: auto;
            margin: 0 auto 10px auto;
            border: 1px solid #000;
        }
        
        /* Requirement 2: Minimum font size 12px */
        .etaniResultSize {
            display: inline-block;
            margin-left: 10px;
            font-size: 12px; 
            color: #555;
        }
        
        /* etaniItem structure (Float layout) */
        .etaniItem {
            min-height: 48px;
            border: 1px solid #ccc;
            box-sizing: border-box;
            width: 100%;
            margin-bottom: -1px;
            overflow: auto;
        }
        
        .etaniItemLeft {
            float: left; 
            width: 60px;
            min-height: 48px;
            border-right: 1px solid #ccc;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            background-color: #e8e8e8;
            padding: 2px 0;
        }
        
        .etaniItemRight {
            margin-left: 60px; 
            padding: 7px;
            min-height: 48px; 
            background-color: #fff;
        }

        /* Requirement 2: Minimum font size 12px */
        .tileid {
            text-align: center;
            font-size: 12px; 
            word-break: break-all;
            padding-top: 2px;
        }
        
        /* ------------------ ANIMATE CONTROLS (4.1) ------------------ */

        .etaniAnimate {
            border: 1px solid #999;
            padding: 5px;
            margin-bottom: 5px;
        }

        .etaniAnimateName {
            display: inline-block;
            padding: 2px 5px;
            background-color: #555; /* Dark grey background */
            color: white; /* White text */
            margin-right: 10px;
            font-size: 12px; /* Minimum font size 12px */
        }

        .etaniAnimateDur {
            display: inline-block;
            margin-right: 10px;
            font-size: 14px;
        }

        .etaniAnimateValue {
            margin-top: 5px;
            overflow: auto;
        }

        /* New container for Add/Delete buttons (Requirement 3) */
        .etaniAVCtrl {
            display: inline-block;
            vertical-align: top;
            margin-right: 5px;
        }
        
        /* Custom CSS rule for SVG alignment (Requirement 4) */
        .etaniAVCtrl svg {
            margin-left: -1px;
            margin-top: -1px;
        }

        /* Shared style for control buttons (Requirement 4) */
        .etaniAVCtrl span {
            display: inline-block;
            width: 24px;
            height: 24px;
            line-height: 24px;
            text-align: center;
            cursor: pointer;
            box-sizing: border-box;
            vertical-align: top;
            margin-right: 3px; /* New margin */
        }

        .etaniAVAdd {
            background-color: #a7fca7; /* Light green */
            border: 1px solid #71c371; /* Thin line near light green */
        }
        
        .etaniAVDelete {
            background-color: #ffcccc; /* Light red */
            border: 1px solid #cc3333; /* Darker red */
        }

        .etaniAVDelete.deleting {
            background-color: #cc3333; /* Dark red when active */
        }
        
        .etaniAVLabel {
            font-size: 14px;
            margin-right: 5px;
        }

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

        .etaniAVItem {
            display: inline-block;
            width: 24px;
            height: 24px;
            background-color: #ff9933; /* Dark orange */
            border: 1px dashed #00bfff; /* Dashed sky blue */
            margin: 0 5px;
            box-sizing: border-box;
            cursor: pointer;
            position: relative;
        }

        .etaniAVItem.deleting-target {
            background-color: #ff4d4d; /* Reddish when targeted for deletion */
            border: 2px solid red;
        }
        
        /* ------------------ MODAL/POPUP STYLES ------------------ */
        .modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.5);
            z-index: 1000;
        }
        .modal-content {
            position: fixed;
            width: 98%;
            height: 48%;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 10px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
            z-index: 1001;
            box-sizing: border-box;
        }
        .modal-content textarea {
            width: 100%;
            height: calc(100% - 50px); /* Adjusted for larger close button */
            resize: none;
            border: 1px solid #ccc;
            font-size: 12px;
            box-sizing: border-box;
        }
        .modal-close {
            position: absolute;
            top: 0;
            right: 10px;
            font-size: 48px;
            line-height: 1; 
            cursor: pointer;
            color: #333;
        }
    `;

    document.head.appendChild(styleSheet);
}

// --- UTILITY FUNCTIONS ---

/**
 * Extracts a specific transform function and its value from a transform string.
 * @param {string} transformString The full transform string.
 * @param {string} type The transform type ('translate', 'scale', 'rotate').
 * @returns {{func: string, value: string}} The transform function and its value.
 */
function extractTransformPart(transformString, type) {
    const regex = new RegExp(`(${type})\\(([^)]+)\\)`, 'i');
    const match = transformString.match(regex);
    if (match) {
        return { func: match[1], value: match[2].trim() };
    }
    return { func: '', value: '' };
}

/**
 * Parses transform string to get individual transform values.
 * @param {string} transformString The transform attribute string.
 * @returns {{translate: string, scale: string, rotate: string}} Parsed values.
 */
function parseTransform(transformString) {
    const defaultTransform = { translate: '0,0', scale: '1,1', rotate: '0' };
    if (!transformString) return defaultTransform;

    const transform = {};

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

    transform.translate = getMatch('translate') || defaultTransform.translate;
    transform.scale = getMatch('scale') || defaultTransform.scale;
    // Normalize scale: if only one value (e.g., '1'), ensure it's '1,1' for coordinate parsing
    if (transform.scale.split(',').length === 1) {
        transform.scale += `,${transform.scale}`;
    }
    transform.rotate = getMatch('rotate') || defaultTransform.rotate;

    return transform;
}


/**
 * Converts an SVG string to a Base64 data URL.
 * @param {string} svgString The SVG XML string.
 * @returns {string} The Base64 data URL.
 */
function svgToBase64(svgString) {
    const base64 = btoa(unescape(encodeURIComponent(svgString)));
    return `data:image/svg+xml;base64,${base64}`;
}

/**
 * Formats byte size into human-readable string (e.g., KB, MB).
 * @param {number} bytes The size in bytes.
 * @returns {string} The formatted string.
 */
function formatBytes(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

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

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

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

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

/**
 * Handler for the 'Center' button click. Resets the .etdrop transform.
 * @param {Event} e The click event.
 */
function handleCenterClick(e) {
    e.preventDefault();
    if (etani_clone) {
        const etdrop = etani_clone.querySelector('.etdrop');
        if (etdrop) {
            // Reset position to the original center for the entire group
            etdrop.setAttribute('transform', 'translate(240,240) scale(1,1)');
            updateEtaniResult();
        }
    }
}

/**
 * Handles the HTML popup window.
 * @param {Event} e The click event.
 */
function handleContentHTMLClick(e) {
    e.preventDefault();
    if (!etani_clone) return;

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

    // Close logic
    const closeModal = () => {
        if (document.body.contains(overlay)) document.body.removeChild(overlay);
        if (document.body.contains(content)) document.body.removeChild(content);
    };
    close.onclick = closeModal;
    overlay.onclick = closeModal; // Clicking background also closes

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

/**
 * Calculates the required *additive* transform value for a given type, 
 * using the specified rounding rule.
 * @param {string} type The transform type ('translate', 'scale', 'rotate').
 * @param {string} originalValue The transform value from the original <use/> (cv).
 * @param {string} currentBaseValue The current base transform value from the etani_clone <use/> (ov).
 * @returns {string} The new additive value (v).
 */
function calculateAdditiveValue(type, originalValue, currentBaseValue) {
    const roundToThreeDecimals = (value) => {
        return Math.round(value * 1000) / 1000;
    };
    
    if (type === 'translate') {
        const ovCoords = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
        const cvCoords = originalValue.split(',').map(c => parseFloat(c.trim()));
        // v1 = cv1 - ov1, v2 = cv2 - ov2
        const v1 = roundToThreeDecimals(cvCoords[0] - ovCoords[0]);
        const v2 = roundToThreeDecimals(cvCoords[1] - ovCoords[1]);
        return `${v1},${v2}`;
    } else if (type === 'scale') {
        const ovScales = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
        const cvScales = originalValue.split(',').map(c => parseFloat(c.trim()));
        // v1 = cv1 / ov1, v2 = cv2 / ov2.
        const v1 = roundToThreeDecimals(cvScales[0] / ovScales[0]);
        const v2 = roundToThreeDecimals(cvScales.length > 1 ? (cvScales[1] / ovScales[1]) : (cvScales[0] / ovScales[0]));
        return v1 === v2 ? `${v1}` : `${v1},${v2}`;
    } else if (type === 'rotate') {
        const ovAngle = parseFloat(currentBaseValue.trim());
        const cvAngle = parseFloat(originalValue.trim());
        // v3 = cv3 - ov3
        return roundToThreeDecimals(cvAngle - ovAngle);
    }
    return '';
}

/**
 * Handles the click event for the '+' button.
 * Adds a new animateTransform value.
 * @param {Event} e The click event.
 * @param {string} useElementId The ID of the original <g> element referenced by the <use/>.
 */
function handleAVAddClick(e, useElementId) {
    e.preventDefault();
    if (!etani_clone) return;

    // 1. Get current transform of the original <use/> element in svg#etmain (cv)
    const originalUseElement = document.querySelector(`.etdrop use[href="#${useElementId}"]`);
    if (!originalUseElement) return;

    const originalTransformString = originalUseElement.getAttribute('transform') || '';
    const originalTransforms = parseTransform(originalTransformString);
    
    // 2. Find the corresponding elements in etani_clone
    // Second G (Ancestor) is the translate wrapper
    const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useElementId}"]`);
    if (!translateWrapper) return;
    
    // First G (Parent) is the scale wrapper
    const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useElementId}"]`);
    // Use element is the rotate target
    const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
    
    if (!scaleWrapper || !cloneUseElement) return;
    
    // 3. Get the *base* transform values from the clone's <use> (ov)
    // Based on Requirement 2, we use the <use> element's transform attribute as the baseline (ov).
    const cloneBaseTransformString = cloneUseElement.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)'; 
    const currentBaseTransforms = parseTransform(cloneBaseTransformString);
    
    const currentBaseTranslate = currentBaseTransforms.translate;
    const currentBaseScale = currentBaseTransforms.scale;
    const currentBaseRotate = currentBaseTransforms.rotate;

    // 4. Collect all relevant animateTransform tags
    const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
    const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
    const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
    
    const allAnimates = [...animatesTranslate, ...animatesScale, ...animatesRotate];
    
    // Calculate new duration (starts from 0s, adds 1s)
    const currentDur = parseInt((animatesRotate[0].getAttribute('dur') || '0s').replace('s', '')) || 0;
    const newDur = currentDur + 1;

    // Determine repeat/freeze state (Requirement 3)
    const isRepeat = document.querySelector('.etaniSettingRepeat.active');
    
    allAnimates.forEach(animate => {
        const type = animate.getAttribute('type').toLowerCase();
        let currentValueString = animate.getAttribute('values') || '';
        let newValue = '';
        
        // Calculate the *additive* difference based on new formula (Requirement 2)
        if (type === 'translate') {
            newValue = calculateAdditiveValue('translate', originalTransforms.translate, currentBaseTranslate);
        } else if (type === 'scale') {
            newValue = calculateAdditiveValue('scale', originalTransforms.scale, currentBaseScale);
        } else if (type === 'rotate') {
            newValue = calculateAdditiveValue('rotate', originalTransforms.rotate, currentBaseRotate);
        }
        
        // Append the new value
        animate.setAttribute('values', (currentValueString ? currentValueString + ';' : '') + newValue);

        // Update duration (Requirement 4)
        if (newDur > 0) {
            animate.setAttribute('dur', `${newDur}s`);
            // Set repeat/freeze state (Requirement 3)
            if (isRepeat) {
                animate.removeAttribute('fill');
                animate.setAttribute('repeatCount', 'indefinite');
            } else {
                animate.removeAttribute('repeatCount');
                animate.setAttribute('fill', 'freeze');
            }
        } else {
            animate.removeAttribute('dur');
            animate.removeAttribute('fill');
            animate.removeAttribute('repeatCount');
        }
    });

    // 5. Add a new .etaniAVItem in .etaniItemRight and update UI duration
    const etaniItemRight = e.currentTarget.closest('.etaniItemRight');
    const etaniAV = etaniItemRight.querySelector('.etaniAV');
    if (etaniAV) {
        const newAVItem = document.createElement('span');
        newAVItem.className = 'etaniAVItem';
        newAVItem.addEventListener('click', handleAVItemClick); // Bind deletion handler
        etaniAV.appendChild(newAVItem);
    }
    etaniItemRight.querySelector('.etaniAnimateDur').textContent = `dur: ${newDur}s`;

    // 6. Update the SVG result
    updateEtaniResult();
}

/**
 * Handles the click event for the '-' button.
 * Toggles deletion mode, allowing for continuous deletion (Requirement 1).
 * @param {Event} e The click event.
 */
function handleAVDeleteToggle(e) {
    e.preventDefault();
    const deleteButton = e.currentTarget;
    const isDeleting = deleteButton.classList.toggle('deleting');
    const etaniItemRight = deleteButton.closest('.etaniItemRight');
    const etaniCol = deleteButton.closest('.etaniCol');
    
    const avItems = etaniItemRight.querySelectorAll('.etaniAVItem');
    
    // Toggle visual state of all items (except the first one)
    avItems.forEach((item, index) => {
        if (index > 0) {
            // Apply deleting-target class only if deleting mode is active
            item.classList.toggle('deleting-target', isDeleting);
        }
    });

    // Toggle deleting state for this item's column
    etaniCol.classList.toggle('deleting-mode', isDeleting);

    // Disable all other AVDelete buttons if entering deleting mode
    document.querySelectorAll('.etaniAVDelete').forEach(btn => {
        if (btn !== deleteButton) {
            btn.closest('.etaniAVCtrl').disabled = isDeleting;
            btn.style.opacity = isDeleting ? 0.5 : 1; // Visual feedback for disabled state
        }
    });

    if (isDeleting) {
        deleteButton.title = "Click value item to delete (Click again to cancel)";
        deleteButton.style.color = 'white'; // White icon for active state
    } else {
        deleteButton.title = "Delete Value";
        deleteButton.style.color = 'currentColor'; // Reset to default
        
        // Remove visual state from all items when cancelling
        avItems.forEach(item => {
            item.classList.remove('deleting-target');
        });
    }
}

/**
 * Handles the click event for an .etaniAVItem in deletion mode.
 * @param {Event} e The click event.
 */
function handleAVItemClick(e) {
    const itemToDelete = e.currentTarget;
    const itemIndex = Array.from(itemToDelete.parentNode.children).indexOf(itemToDelete);
    const etaniItemRight = itemToDelete.closest('.etaniItemRight');
    
    // Check if deletion mode is active and it's not the first item
    if (etaniItemRight.closest('.etaniCol').classList.contains('deleting-mode') && itemIndex > 0) {
        
        const deleteButton = etaniItemRight.querySelector('.etaniAVDelete');
        const useId = etaniItemRight.closest('.etaniItem').querySelector('.tileid').textContent;
        
        // 1. Find the corresponding elements in etani_clone
        const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useId}"]`);
        if (!translateWrapper) return;
        
        const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useId}"]`);
        const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
        if (!scaleWrapper || !cloneUseElement) return;

        // 2. Collect all relevant animateTransform tags
        const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
        const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
        const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
        
        const allAnimates = [...animatesTranslate, ...animatesScale, ...animatesRotate];
        
        // Calculate new duration (starts from 0s)
        const currentDur = parseInt((animatesRotate[0].getAttribute('dur') || '0s').replace('s', '')) || 0;
        const newDur = Math.max(0, currentDur - 1); // Duration cannot be less than 0
        
        allAnimates.forEach(animate => {
            const values = animate.getAttribute('values').split(';');
            
            // Remove the value at the target index (itemIndex)
            if (itemIndex < values.length) {
                values.splice(itemIndex, 1);
            }
            animate.setAttribute('values', values.join(';'));
            
            // Update duration (Requirement 4)
            if (newDur > 0) {
                animate.setAttribute('dur', `${newDur}s`);
            } else {
                animate.removeAttribute('dur');
                animate.removeAttribute('fill');
                animate.removeAttribute('repeatCount');
            }
        });

        // 3. Remove the item from the UI
        itemToDelete.remove();

        // 4. Update UI duration display
        etaniItemRight.querySelector('.etaniAnimateDur').textContent = `dur: ${newDur}s`;
        
        // 5. Update visual state of remaining items in deletion mode (Requirement 1)
        const remainingItems = etaniItemRight.querySelectorAll('.etaniAVItem');
        
        // If no values remain (only the base state item[0]), exit deletion mode automatically
        if (remainingItems.length === 1) {
            deleteButton.click(); // Programmatically click to toggle out of deleting mode
        } else {
            // Otherwise, keep deletion mode active but update visual state
            remainingItems.forEach((item, index) => {
                 item.classList.toggle('deleting-target', index > 0); 
            });
        }

        // 6. Update the SVG result
        updateEtaniResult();
        
    } else if (itemIndex === 0 && etaniItemRight.closest('.etaniCol').classList.contains('deleting-mode')) {
        alert("The first value cannot be deleted.");
    }
}


/**
 * Handles the click event for the 'Transform' button.
 * Adds the required <g> wrappers and animateTransform tags.
 */
function handleAllAppendTransformClick() {
    if (!etani_clone) return;

    const etdropUses = document.querySelectorAll('#etmain .etdrop use'); // Select original <use> elements for their href
    const etaniItemRights = document.querySelectorAll('.etaniItemRight');

    // Determine repeat/freeze state (Requirement 3)
    const isRepeat = document.querySelector('.etaniSettingRepeat.active');

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

        // Check if transform controls already exist to prevent duplication
        if (itemRight.querySelector('.etaniAnimate')) return; 

        // 1. Find the corresponding <use> element in etani_clone
        let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;

        // 2. Define the base animate function
        const baseAnimate = (type, initialValue) => {
            const animate = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
            animate.setAttribute('attributeName', 'transform');
            animate.setAttribute('attributeType', 'XML');
            animate.setAttribute('type', type);
            // Initial value is the base state's additive difference, which is always 0 or 1 for the first point
            animate.setAttribute('values', initialValue); 
            
            // dur="0s" is not set, only added when > 0 (Requirement 4)
            
            animate.setAttribute('fill', isRepeat ? 'remove' : 'freeze'); // Requirement 3
            if (isRepeat) animate.setAttribute('repeatCount', 'indefinite'); // Requirement 3

            animate.setAttribute('additive', 'sum'); // Important for relative transforms
            return animate;
        };
        
        // --- Requirement 1: Constructing the nested G structure ---
        
        // a. Add animateTransform[type=rotate][value=0] to <use>
        cloneUseElement.appendChild(baseAnimate('rotate', '0'));
        
        // b. Create first G (Scale Wrapper) and add animateTransform[type=scale][value=1]
        const scaleWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        scaleWrapper.setAttribute('data-use-href-scale', `#${useId}`); // Use a specific data attribute for scale wrapper
        scaleWrapper.appendChild(baseAnimate('scale', '1'));
        
        // c. Create second G (Translate Wrapper) and add animateTransform[type=translate][value=0,0]
        const translateWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        translateWrapper.setAttribute('data-use-href', `#${useId}`); // Main ID link
        translateWrapper.appendChild(baseAnimate('translate', '0,0'));
        
        // d. Perform the wrapping: (G_Translate) -> (G_Scale) -> (USE + Rotate_Animate)
        
        // i. Insert G_Translate before the original <use>
        cloneUseElement.parentNode.insertBefore(translateWrapper, cloneUseElement);
        
        // ii. Wrap <use> with G_Scale
        cloneUseElement.parentNode.insertBefore(scaleWrapper, cloneUseElement);
        scaleWrapper.appendChild(cloneUseElement);
        
        // iii. Wrap G_Scale with G_Translate
        translateWrapper.appendChild(scaleWrapper);
        

        // --- UI Control Creation (Unchanged logic) ---

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

        // Animate Name (Dark grey background, white text)
        const nameSpan = document.createElement('span');
        nameSpan.className = 'etaniAnimateName';
        nameSpan.textContent = 'transform';
        etaniAnimate.appendChild(nameSpan);

        // Animate Dur (dur: n)
        const durSpan = document.createElement('span');
        durSpan.className = 'etaniAnimateDur';
        durSpan.textContent = 'dur: 0s'; // Initial value 0s (Requirement 5)
        etaniAnimate.appendChild(durSpan);

        // Animate Value container
        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        // New container for buttons (Requirement 3)
        const avCtrlDiv = document.createElement('div');
        avCtrlDiv.className = 'etaniAVCtrl';

        // AV Add (Plus sign SVG)
        const avAddSpan = document.createElement('span');
        avAddSpan.className = 'etaniAVAdd';
        avAddSpan.title = 'Add Transform Value';
        avAddSpan.addEventListener('click', (e) => handleAVAddClick(e, useId));
        
        const plusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        plusSvg.setAttribute('width', '24');
        plusSvg.setAttribute('height', '24');
        plusSvg.setAttribute('viewBox', '0 0 24 24');
        plusSvg.setAttribute('fill', 'none');
        plusSvg.setAttribute('stroke', 'currentColor');
        plusSvg.setAttribute('stroke-width', '1'); // Requirement 2
        plusSvg.setAttribute('stroke-linecap', 'round');
        plusSvg.setAttribute('stroke-linejoin', 'round');
        plusSvg.innerHTML = '<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>';
        avAddSpan.appendChild(plusSvg);


        // AV Delete (Minus sign SVG)
        const avDeleteSpan = document.createElement('span');
        avDeleteSpan.className = 'etaniAVDelete';
        avDeleteSpan.title = 'Delete Value';
        avDeleteSpan.addEventListener('click', handleAVDeleteToggle);

        const minusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        minusSvg.setAttribute('width', '24');
        minusSvg.setAttribute('height', '24');
        minusSvg.setAttribute('viewBox', '0 0 24 24');
        minusSvg.setAttribute('fill', 'none');
        minusSvg.setAttribute('stroke', 'currentColor');
        minusSvg.setAttribute('stroke-width', '1'); // Requirement 2
        minusSvg.setAttribute('stroke-linecap', 'round');
        minusSvg.setAttribute('stroke-linejoin', 'round');
        minusSvg.innerHTML = '<line x1="5" y1="12" x2="19" y2="12"></line>';
        avDeleteSpan.appendChild(minusSvg);
        
        avCtrlDiv.appendChild(avAddSpan);
        avCtrlDiv.appendChild(avDeleteSpan);


        // AV Label
        const avLabelSpan = document.createElement('span');
        avLabelSpan.className = 'etaniAVLabel';
        avLabelSpan.textContent = 'values : ';

        // AV Container
        const avDiv = document.createElement('div');
        avDiv.className = 'etaniAV';
        
        // AV Item (Initially contains one)
        const avItemSpan = document.createElement('span');
        avItemSpan.className = 'etaniAVItem';
        avItemSpan.addEventListener('click', handleAVItemClick); // Bind deletion handler
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avCtrlDiv); // Append control buttons container (Requirement 3)
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

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

    // 7. Update the SVG result immediately
    updateEtaniResult();
}

/**
 * Handles the Setting Mode switch (Freeze/Repeat).
 * @param {string} mode The animation end mode ('freeze' or 'repeat').
 */
function handleSettingModeChange(mode) {
    if (!etani_clone) return;

    // We select all animateTransform tags
    const animates = etani_clone.querySelectorAll('animateTransform');
    const isRepeat = mode === 'repeat';

    animates.forEach(animate => {
        // Only modify if dur is present (i.e., dur > 0s)
        if (animate.hasAttribute('dur')) {
            if (isRepeat) {
                animate.removeAttribute('fill');
                animate.setAttribute('repeatCount', 'indefinite');
            } else {
                animate.removeAttribute('repeatCount');
                animate.setAttribute('fill', 'freeze');
            }
        }
    });

    // Update active state
    document.querySelector('.etaniSettingFreeze').classList.toggle('active', !isRepeat);
    document.querySelector('.etaniSettingRepeat').classList.toggle('active', isRepeat);

    updateEtaniResult();
}


// --- MAIN STRUCTURE CREATION ---

/**
 * Creates the internal animation control structure.
 * @param {HTMLElement} etaniouter The outer container element.
 */
function createEtaniInner(etaniouter) {
    // 5.1, 5.2, 5.3 Clone, remove .etwait, update ID
    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etani_clone = originalSvg.cloneNode(true);
    const etwaitElement = etani_clone.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    etani_clone.id = 'etmainani';


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

    // Create div.etaniCtrl
    const etaniCtrl = document.createElement('div');
    etaniCtrl.className = 'etaniCtrl';
    
    // Content and Center link
    const etaniContent = document.createElement('div');
    etaniContent.className = 'etaniContent';
    
    const contentHTMLLink = document.createElement('a');
    contentHTMLLink.className = 'etaniContentHTML';
    contentHTMLLink.textContent = 'Show HTML';
    contentHTMLLink.href = '#';
    contentHTMLLink.addEventListener('click', handleContentHTMLClick);
    etaniContent.appendChild(contentHTMLLink);

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


    // Setting Mode (Radio Buttons)
    const etaniSetting = document.createElement('div');
    etaniSetting.className = 'etaniSetting';
    
    const freezeRadio = document.createElement('span');
    freezeRadio.className = 'etaniSettingMode etaniSettingFreeze active';
    freezeRadio.textContent = 'Freeze';
    freezeRadio.setAttribute('data-mode', 'freeze');
    freezeRadio.addEventListener('click', () => handleSettingModeChange('freeze'));
    
    const repeatRadio = document.createElement('span');
    repeatRadio.className = 'etaniSettingMode etaniSettingRepeat';
    repeatRadio.textContent = 'Repeat';
    repeatRadio.setAttribute('data-mode', 'repeat');
    repeatRadio.addEventListener('click', () => handleSettingModeChange('repeat'));

    etaniSetting.appendChild(freezeRadio);
    etaniSetting.appendChild(repeatRadio);
    etaniCtrl.appendChild(etaniSetting);


    // All Append (Buttons)
    const etaniAllAppend = document.createElement('div');
    etaniAllAppend.className = 'etaniAllAppend';

    const transformButton = document.createElement('button');
    transformButton.className = 'etaniAllAppendTransform';
    transformButton.textContent = 'transform';
    transformButton.addEventListener('click', handleAllAppendTransformClick);
    
    const opacityButton = document.createElement('button');
    opacityButton.className = 'etaniAllAppendOpacity';
    opacityButton.textContent = 'opacity';
    // opacityButton.addEventListener('click', handleAllAppendOpacityClick); // Placeholder
    
    etaniAllAppend.appendChild(transformButton);
    etaniAllAppend.appendChild(opacityButton);
    etaniCtrl.appendChild(etaniAllAppend);
    

    // Create div.etaniCol (Tile list)
    const etaniCol = document.createElement('div');
    etaniCol.className = 'etaniCol';

    // Create div.etaniResult
    const etaniResult = document.createElement('div');
    etaniResult.className = 'etaniResult';
    
    // Result elements (img, download, rename, size) 
    const resultImg = document.createElement('img');
    resultImg.className = 'etaniResultImage';
    resultImg.alt = 'Rendered Ejtile Animation SVG';

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

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

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

    resultDRDiv.appendChild(downloadLink);
    resultDRDiv.appendChild(renameLink);
    
    etaniResult.appendChild(resultImg);
    etaniResult.appendChild(resultDRDiv);
    etaniResult.appendChild(sizeSpan);
    
    // Append children to etaniinner
    etaniinner.appendChild(etaniCtrl);
    etaniinner.appendChild(etaniCol);
    etaniinner.appendChild(etaniResult);

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

    // III, IV, V, VI, VII. Populate etaniCol (Tile list)
    const etdropUses = document.querySelectorAll('.etdrop use');
    const etwaitGroups = document.querySelectorAll('.etwait g');

    etdropUses.forEach((useElement, i) => {
        // (Tile processing logic - Unchanged)
        const tileid = useElement.getAttribute('href').substring(1);
        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 originalTile = document.querySelector(`defs g#${tileid}`);
        
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';

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

        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        
        if (originalTile) {
            const tileclone = originalTile.cloneNode(true);
            tileclone.removeAttribute('id');
            if (etwaittransform) tileclone.setAttribute('transform', etwaittransform);
            if (etwaitfill) tileclone.setAttribute('fill', etwaitfill);
            if (etwaitstroke) tileclone.setAttribute('stroke', etwaitstroke);
            if (etwaitstrokeWidth) tileclone.setAttribute('stroke-width', etwaitstrokeWidth);
            
            const svgWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            svgWrapper.setAttribute('width', '40');
            svgWrapper.setAttribute('height', '40');
            svgWrapper.setAttribute('version', '1.1');
            svgWrapper.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
            svgWrapper.setAttribute('xmlns:svg', 'http://www.w3.org/2000/svg');
            svgWrapper.className = 'etanitileimg';
            
            svgWrapper.appendChild(tileclone);
            etaniItemLeft.appendChild(svgWrapper);
        }

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

        etaniItem.appendChild(etaniItemLeft);
        etaniItem.appendChild(etaniItemRight);
        etaniCol.appendChild(etaniItem);
    });
    
    // 5. Update the result section immediately
    updateEtaniResult();
}

// --- INITIALIZATION ---

/**
 * Toggles the visibility and content of the animation control panel.
 * @param {Event} event The click event.
 */
function toggleAnimation(event) {
    const etanibutton = event.currentTarget;
    const etaniouter = etanibutton.parentNode;
    const etaniinner = document.getElementById('etaniinner');

    if (etanibutton.textContent === 'Animate it') {
        etanibutton.textContent = 'Close Ejtile Ani';
        createEtaniInner(etaniouter);
        
    } else if (etanibutton.textContent === 'Close Ejtile Ani') {
        etanibutton.textContent = 'Animate it';
        if (etaniinner) {
            etaniinner.remove();
        }
        etani_clone = null;
    }
}


window.addEventListener('load', () => {
    addDynamicStyles();

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

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

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

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

帖子 ejsoon »

研究出v1=cv1-(ov1*v2)

代码: 全选

改動:

一,當按下etanibutton時,先在etani_clone中的.etdrop中的<use></use>內加入animateTransform[type=rotate][value=0]。

然後在其外加第一層<g>,把animateTransform[type=scale][value=1,1]加入。

之後再在其外加第二層<g>,把animateTransform[type=translate][value=0,0]加入。

不要改動etani_clone中的.etdrop中的<use>的transform屬性值,也不需要把這些屬性搬至父元素。

二,當點擊etaniAVAdd增加value時,設它的三個標籤的值的計算結果為translate(v1) scale(v2) rotate(v3),它們的計算方法是:

首先取出它在svg#etmain中的.etdrop中的<use/>的屬性transform="translate(cv1) scale(cv2) rotate(cv3)"的三個值。

之後找到在etani_clone中的.etdrop中的<use/>的屬性transform="translate(ov1) scale(ov2) rotate(ov3)"的三個值。

v3=cv3-ov3
v2=cv2/ov2
v1=cv1-(ov1*v2)

注意:ov1、ov2、ov3一般而言是固定不變的,而不是上一個值。

計算時,要四捨五入,先*1000,再round,再/1000。

三,在增加animateTransform時,是加fill=freeze還是加repeatCount,要看etaniSettingMode的當前狀態。

四,當dur=0s時,不用加到etani_clone中,如果有的話就移除,僅當dur大於0時才加入。

用繁體中文回覆。在代碼中所有注釋都要用英文。

代码: 全选

// Global variable to hold the SVG clone and ensure state is maintained
let etani_clone = null;
// Global variable to store the original <use> transforms for easy calculation
const originalTransforms = new Map();

// --- CSS STYLES ---

function addDynamicStyles() {
    if (document.getElementById('dynamic-et-styles')) {
        return;
    }

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

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

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

        /* IV. Styles for etaniResult */
        .etaniResult {
            text-align: center;
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #bbb;
            overflow: auto;
        }
        
        /* ------------------ CONTROL & BUTTON STYLES ------------------ */

        /* Styles for control links (Center button) */
        .etaniContent a {
            display: inline-block;
            margin: 0 5px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 14px;
        }
        
        .etaniCenter {
            border: 1px solid green;
            color: green;
        }
        
        .etaniContentHTML {
            border: 1px solid #0099ff;
            color: #0099ff;
            margin-right: 15px;
        }

        /* Requirement 3: etaniAllAppend buttons */
        .etaniAllAppend button {
            padding: 5px 10px;
            font-size: 16px;
            margin: 0 5px;
            cursor: pointer;
            border: 1px solid #333;
            background-color: #fff;
        }
        
        /* Requirement 2: Custom Radio button styles */
        .etaniSettingMode {
            display: inline-block;
            cursor: pointer;
            padding: 4px 8px;
            margin: 0 3px;
            font-size: 14px;
            border: 1px solid #888;
            background-color: #eee;
            color: #333;
            user-select: none;
        }

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

        /* ------------------ RESULT & ANIMAION ITEM STYLES ------------------ */

        .etaniResultDR {
            text-align: center;
            margin-bottom: 10px;
        }

        /* Requirement 1: Download and rename link font size 16px */
        .etaniResultDownload, .etaniResultRename {
            display: inline-block;
            margin-right: 15px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 16px; 
        }
        
        /* .etaniResultDownload default style */
        .etaniResultDownload {
            border: 1px solid blue;
            color: blue;
        }

        /* .etaniResultRename style (Requirement 1: color changed to brown) */
        .etaniResultRename {
            border: 1px solid brown;
            color: brown;
        }

        .etaniResultImage {
            display: block;
            max-width: 480px;
            width: 100%;
            height: auto;
            margin: 0 auto 10px auto;
            border: 1px solid #000;
        }
        
        /* Requirement 2: Minimum font size 12px */
        .etaniResultSize {
            display: inline-block;
            margin-left: 10px;
            font-size: 12px; 
            color: #555;
        }
        
        /* etaniItem structure (Float layout) */
        .etaniItem {
            min-height: 48px;
            border: 1px solid #ccc;
            box-sizing: border-box;
            width: 100%;
            margin-bottom: -1px;
            overflow: auto;
        }
        
        .etaniItemLeft {
            float: left; 
            width: 60px;
            min-height: 48px;
            border-right: 1px solid #ccc;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            background-color: #e8e8e8;
            padding: 2px 0;
        }
        
        .etaniItemRight {
            margin-left: 60px; 
            padding: 7px;
            min-height: 48px; 
            background-color: #fff;
        }

        /* Requirement 2: Minimum font size 12px */
        .tileid {
            text-align: center;
            font-size: 12px; 
            word-break: break-all;
            padding-top: 2px;
        }
        
        /* ------------------ ANIMATE CONTROLS (4.1) ------------------ */

        .etaniAnimate {
            border: 1px solid #999;
            padding: 5px;
            margin-bottom: 5px;
        }

        .etaniAnimateName {
            display: inline-block;
            padding: 2px 5px;
            background-color: #555; /* Dark grey background */
            color: white; /* White text */
            margin-right: 10px;
            font-size: 12px; /* Minimum font size 12px */
        }

        .etaniAnimateDur {
            display: inline-block;
            margin-right: 10px;
            font-size: 14px;
        }

        .etaniAnimateValue {
            margin-top: 5px;
            overflow: auto;
        }

        /* New container for Add/Delete buttons (Requirement 3) */
        .etaniAVCtrl {
            display: inline-block;
            vertical-align: top;
            margin-right: 5px;
        }
        
        /* Custom CSS rule for SVG alignment (Requirement 4) */
        .etaniAVCtrl svg {
            margin-left: -1px;
            margin-top: -1px;
        }

        /* Shared style for control buttons (Requirement 4) */
        .etaniAVCtrl span {
            display: inline-block;
            width: 24px;
            height: 24px;
            line-height: 24px;
            text-align: center;
            cursor: pointer;
            box-sizing: border-box;
            vertical-align: top;
            margin-right: 3px; /* New margin */
        }

        .etaniAVAdd {
            background-color: #a7fca7; /* Light green */
            border: 1px solid #71c371; /* Thin line near light green */
        }
        
        .etaniAVDelete {
            background-color: #ffcccc; /* Light red */
            border: 1px solid #cc3333; /* Darker red */
        }

        .etaniAVDelete.deleting {
            background-color: #cc3333; /* Dark red when active */
        }
        
        .etaniAVLabel {
            font-size: 14px;
            margin-right: 5px;
        }

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

        .etaniAVItem {
            display: inline-block;
            width: 24px;
            height: 24px;
            background-color: #ff9933; /* Dark orange */
            border: 1px dashed #00bfff; /* Dashed sky blue */
            margin: 0 5px;
            box-sizing: border-box;
            cursor: pointer;
            position: relative;
        }

        .etaniAVItem.deleting-target {
            background-color: #ff4d4d; /* Reddish when targeted for deletion */
            border: 2px solid red;
        }
        
        /* ------------------ MODAL/POPUP STYLES ------------------ */
        .modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.5);
            z-index: 1000;
        }
        .modal-content {
            position: fixed;
            width: 98%;
            height: 48%;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 10px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
            z-index: 1001;
            box-sizing: border-box;
        }
        .modal-content textarea {
            width: 100%;
            height: calc(100% - 50px); /* Adjusted for larger close button */
            resize: none;
            border: 1px solid #ccc;
            font-size: 12px;
            box-sizing: border-box;
        }
        .modal-close {
            position: absolute;
            top: 0;
            right: 10px;
            font-size: 48px;
            line-height: 1; 
            cursor: pointer;
            color: #333;
        }
    `;

    document.head.appendChild(styleSheet);
}

// --- UTILITY FUNCTIONS ---

/**
 * Extracts a specific transform function and its value from a transform string.
 * @param {string} transformString The full transform string.
 * @param {string} type The transform type ('translate', 'scale', 'rotate').
 * @returns {{func: string, value: string}} The transform function and its value.
 */
function extractTransformPart(transformString, type) {
    const regex = new RegExp(`(${type})\\(([^)]+)\\)`, 'i');
    const match = transformString.match(regex);
    if (match) {
        return { func: match[1], value: match[2].trim() };
    }
    return { func: '', value: '' };
}

/**
 * Parses transform string to get individual transform values.
 * @param {string} transformString The transform attribute string.
 * @returns {{translate: string, scale: string, rotate: string}} Parsed values.
 */
function parseTransform(transformString) {
    const defaultTransform = { translate: '0,0', scale: '1,1', rotate: '0' };
    if (!transformString) return defaultTransform;

    const transform = {};

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

    transform.translate = getMatch('translate') || defaultTransform.translate;
    transform.scale = getMatch('scale') || defaultTransform.scale;
    // Normalize scale: if only one value (e.g., '1'), ensure it's '1,1' for coordinate parsing
    if (transform.scale.split(',').length === 1) {
        transform.scale += `,${transform.scale}`;
    }
    transform.rotate = getMatch('rotate') || defaultTransform.rotate;

    return transform;
}


/**
 * Converts an SVG string to a Base64 data URL.
 * @param {string} svgString The SVG XML string.
 * @returns {string} The Base64 data URL.
 */
function svgToBase64(svgString) {
    const base64 = btoa(unescape(encodeURIComponent(svgString)));
    return `data:image/svg+xml;base64,${base64}`;
}

/**
 * Formats byte size into human-readable string (e.g., KB, MB).
 * @param {number} bytes The size in bytes.
 * @returns {string} The formatted string.
 */
function formatBytes(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

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

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

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

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

/**
 * Handler for the 'Center' button click. Resets the .etdrop transform.
 * @param {Event} e The click event.
 */
function handleCenterClick(e) {
    e.preventDefault();
    if (etani_clone) {
        const etdrop = etani_clone.querySelector('.etdrop');
        if (etdrop) {
            // Reset position to the original center for the entire group
            etdrop.setAttribute('transform', 'translate(240,240) scale(1,1)');
            updateEtaniResult();
        }
    }
}

/**
 * Handles the HTML popup window.
 * @param {Event} e The click event.
 */
function handleContentHTMLClick(e) {
    e.preventDefault();
    if (!etani_clone) return;

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

    // Close logic
    const closeModal = () => {
        if (document.body.contains(overlay)) document.body.removeChild(overlay);
        if (document.body.contains(content)) document.body.removeChild(content);
    };
    close.onclick = closeModal;
    overlay.onclick = closeModal; // Clicking background also closes

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

// --- NEW/MODIFIED HANDLERS ---

/**
 * Calculates the required *additive* transform value for a given type based on the new logic (Requirement 2).
 * @param {string} type The transform type ('translate', 'scale', 'rotate').
 * @param {{translate: string, scale: string, rotate: string}} cv The transforms from original SVG (<use>).
 * @param {{translate: string, scale: string, rotate: string}} ov The base transforms from cloned SVG (<use>).
 * @returns {string} The new additive value (v).
 */
function calculateAdditiveValue(type, cv, ov) {
    // Helper function for rounding to 3 decimal places
    const round3 = (num) => Math.round(num * 1000) / 1000;

    const ov1 = ov.translate.split(',').map(c => parseFloat(c.trim())); // [tx, ty]
    const ov2 = ov.scale.split(',').map(c => parseFloat(c.trim()));     // [sx, sy]
    const ov3 = parseFloat(ov.rotate.trim());                           // [r]
    
    const cv1 = cv.translate.split(',').map(c => parseFloat(c.trim())); // [TX, TY]
    const cv2 = cv.scale.split(',').map(c => parseFloat(c.trim()));     // [SX, SY]
    const cv3 = parseFloat(cv.rotate.trim());                           // [R]

    if (type === 'rotate') {
        // v3 = cv3 - ov3
        const v3 = cv3 - ov3;
        return round3(v3).toString();
    } 
    
    if (type === 'scale') {
        // v2 = cv2 / ov2. Calculate for x and y.
        const v2x = cv2[0] / ov2[0];
        const v2y = cv2[1] / ov2[1];
        
        const v2x_rounded = round3(v2x);
        const v2y_rounded = round3(v2y);
        
        // Return single value if x and y are the same (after rounding)
        return v2x_rounded === v2y_rounded ? v2x_rounded.toString() : `${v2x_rounded},${v2y_rounded}`;
    } 
    
    if (type === 'translate') {
        // v1 = cv1 - (ov1 * v2) -> This is vector subtraction and matrix multiplication.
        // Assuming translate(tx, ty) scale(sx, sy) order:
        // Final TX = (ov1x * v2x) + v1x
        // Final TY = (ov1y * v2y) + v1y
        // We need: v1x = cv1x - (ov1x * v2x)
        // We need: v1y = cv1y - (ov1y * v2y)
        
        // Recalculate the v2 used for the translate calculation based on the *original* cv/ov.
        const v2x = cv2[0] / ov2[0];
        const v2y = cv2[1] / ov2[1];

        const v1x = cv1[0] - (ov1[0] * v2x);
        const v1y = cv1[1] - (ov1[1] * v2y);
        
        return `${round3(v1x)},${round3(v1y)}`;
    }
    return '';
}

/**
 * Updates the 'dur' attribute of all animateTransform tags within a parent element.
 * @param {HTMLElement} gWrapper The second <g> wrapper.
 * @param {number} newDur The new duration in seconds (integer).
 */
function updateAnimateDuration(gWrapper, newDur) {
    const useElement = gWrapper.querySelector('use');
    const animates = [
        ...gWrapper.querySelectorAll('animateTransform[attributeName="transform"]'), 
        ...useElement.querySelectorAll('animateTransform[attributeName="transform"]')
    ];
    
    const durString = `${newDur}s`;
    
    animates.forEach(animate => {
        if (newDur > 0) {
            animate.setAttribute('dur', durString);
        } else {
            // Requirement 4: Remove dur attribute if duration is 0
            animate.removeAttribute('dur');
        }
    });
}

/**
 * Gets the current animation end mode ('freeze' or 'repeat').
 * @returns {{fill: string, repeat: string}} The attribute values to set.
 */
function getAnimationModeAttributes() {
    const isRepeat = document.querySelector('.etaniSettingRepeat').classList.contains('active');
    return isRepeat 
        ? { fill: null, repeat: 'indefinite' }
        : { fill: 'freeze', repeat: null };
}

/**
 * Handles the click event for the '+' button.
 * Adds a new animateTransform value.
 * @param {Event} e The click event.
 * @param {string} useElementId The ID of the original <g> element referenced by the <use/>.
 */
function handleAVAddClick(e, useElementId) {
    e.preventDefault();
    if (!etani_clone) return;

    // 1. Get original transforms (cv) from the globally stored map (Requirement 2)
    const cvTransforms = originalTransforms.get(useElementId);
    if (!cvTransforms) return;

    // 2. Find the corresponding <g> wrappers and <use> in etani_clone
    // Second G (outermost layer)
    const cloneG2Wrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useElementId}"]`); 
    if (!cloneG2Wrapper) return;
    
    // First G (inner layer) and Use
    const cloneG1Wrapper = cloneG2Wrapper.querySelector('g');
    const cloneUseElement = cloneG1Wrapper ? cloneG1Wrapper.querySelector('use') : null;
    if (!cloneG1Wrapper || !cloneUseElement) return;

    // 3. Get the *base* transform values (ov) from the clone's <use> element (Requirement 2)
    const ovTransformString = cloneUseElement.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)';
    const ovTransforms = parseTransform(ovTransformString);
    
    // 4. Find all animates and calculate values
    // Translate animates on G2, Scale animates on G1, Rotate animates on USE (Requirement 1)
    const animateT = cloneG2Wrapper.querySelector('animateTransform[type="translate"]');
    const animateS = cloneG1Wrapper.querySelector('animateTransform[type="scale"]');
    const animateR = cloneUseElement.querySelector('animateTransform[type="rotate"]');
    
    if (!animateT || !animateS || !animateR) return;

    const allAnimates = [animateT, animateS, animateR];
    
    // Calculate new duration (starts from 0s, adds 1s)
    const currentDurAttr = animateT.getAttribute('dur') || '0s';
    const currentDur = parseInt(currentDurAttr.replace('s', '')) || 0;
    const newDur = currentDur + 1;
    const mode = getAnimationModeAttributes(); // Get fill/repeat mode (Requirement 3)

    allAnimates.forEach(animate => {
        const type = animate.getAttribute('type').toLowerCase();
        let currentValueString = animate.getAttribute('values') || '';
        let newValue = calculateAdditiveValue(type, cvTransforms, ovTransforms); // New calculation
        
        // Append the new value
        animate.setAttribute('values', (currentValueString ? currentValueString + ';' : '') + newValue);

        // Update duration (Requirement 4: only add 'dur' if > 0)
        if (newDur > 0) {
            animate.setAttribute('dur', `${newDur}s`);
        } else {
            animate.removeAttribute('dur');
        }

        // Update fill/repeat (Requirement 3)
        if (mode.fill) {
            animate.setAttribute('fill', mode.fill);
            animate.removeAttribute('repeatCount');
        } else if (mode.repeat) {
            animate.setAttribute('repeatCount', mode.repeat);
            animate.removeAttribute('fill');
        }
    });

    // 5. Add a new .etaniAVItem in .etaniItemRight and update UI duration
    const etaniItemRight = e.currentTarget.closest('.etaniItemRight');
    const etaniAV = etaniItemRight.querySelector('.etaniAV');
    if (etaniAV) {
        const newAVItem = document.createElement('span');
        newAVItem.className = 'etaniAVItem';
        newAVItem.addEventListener('click', handleAVItemClick); // Bind deletion handler
        etaniAV.appendChild(newAVItem);
    }
    etaniItemRight.querySelector('.etaniAnimateDur').textContent = `dur: ${newDur}s`;

    // 6. Update the SVG result
    updateEtaniResult();
}

/**
 * Handles the click event for the '-' button.
 * Toggles deletion mode, allowing for continuous deletion (Requirement 1 of V2).
 * @param {Event} e The click event.
 */
function handleAVDeleteToggle(e) {
    e.preventDefault();
    const deleteButton = e.currentTarget;
    const isDeleting = deleteButton.classList.toggle('deleting');
    const etaniItemRight = deleteButton.closest('.etaniItemRight');
    const etaniCol = deleteButton.closest('.etaniCol');
    
    const avItems = etaniItemRight.querySelectorAll('.etaniAVItem');
    
    // Toggle visual state of all items (except the first one)
    avItems.forEach((item, index) => {
        if (index > 0) {
            // Apply deleting-target class only if deleting mode is active
            item.classList.toggle('deleting-target', isDeleting);
        }
    });

    // Toggle deleting state for this item's column
    etaniCol.classList.toggle('deleting-mode', isDeleting);

    // Disable all other AVDelete buttons if entering deleting mode
    document.querySelectorAll('.etaniAVDelete').forEach(btn => {
        if (btn !== deleteButton) {
            btn.closest('.etaniAVCtrl').disabled = isDeleting;
            btn.style.opacity = isDeleting ? 0.5 : 1; // Visual feedback for disabled state
        }
    });

    if (isDeleting) {
        deleteButton.title = "Click value item to delete (Click again to cancel)";
        deleteButton.style.color = 'white'; // White icon for active state
    } else {
        deleteButton.title = "Delete Value";
        deleteButton.style.color = 'currentColor'; // Reset to default
        
        // Remove visual state from all items when cancelling
        avItems.forEach(item => {
            item.classList.remove('deleting-target');
        });
    }
}

/**
 * Handles the click event for an .etaniAVItem in deletion mode.
 * @param {Event} e The click event.
 */
function handleAVItemClick(e) {
    const itemToDelete = e.currentTarget;
    const itemIndex = Array.from(itemToDelete.parentNode.children).indexOf(itemToDelete);
    const etaniItemRight = itemToDelete.closest('.etaniItemRight');
    
    // Check if deletion mode is active and it's not the first item
    if (etaniItemRight.closest('.etaniCol').classList.contains('deleting-mode') && itemIndex > 0) {
        
        const deleteButton = etaniItemRight.querySelector('.etaniAVDelete');
        const useId = etaniItemRight.closest('.etaniItem').querySelector('.tileid').textContent;
        
        // 1. Find the corresponding <g> wrappers and <use> in etani_clone
        const cloneG2Wrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useId}"]`);
        if (!cloneG2Wrapper) return;
        const cloneG1Wrapper = cloneG2Wrapper.querySelector('g');
        const cloneUseElement = cloneG1Wrapper ? cloneG1Wrapper.querySelector('use') : null;
        if (!cloneG1Wrapper || !cloneUseElement) return;

        // 2. Update animateTransform values and duration
        const animatesT = cloneG2Wrapper.querySelectorAll('animateTransform[type="translate"]');
        const animatesS = cloneG1Wrapper.querySelectorAll('animateTransform[type="scale"]');
        const animatesR = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
        const allAnimates = [...animatesT, ...animatesS, ...animatesR];
        
        // Calculate new duration
        const currentDurAttr = allAnimates[0].getAttribute('dur') || '0s';
        const currentDur = parseInt(currentDurAttr.replace('s', '')) || 0;
        const newDur = Math.max(0, currentDur - 1); 
        
        allAnimates.forEach(animate => {
            const values = animate.getAttribute('values').split(';');
            
            // Remove the value at the target index (itemIndex)
            if (itemIndex < values.length) {
                values.splice(itemIndex, 1);
            }
            animate.setAttribute('values', values.join(';'));
            
            // Update duration (Requirement 4: only add 'dur' if > 0, otherwise remove)
            if (newDur > 0) {
                 animate.setAttribute('dur', `${newDur}s`);
            } else {
                 animate.removeAttribute('dur');
            }
        });

        // 3. Remove the item from the UI
        itemToDelete.remove();

        // 4. Update UI duration display
        etaniItemRight.querySelector('.etaniAnimateDur').textContent = `dur: ${newDur}s`;
        
        // 5. Update visual state of remaining items in deletion mode (Requirement 1)
        const remainingItems = etaniItemRight.querySelectorAll('.etaniAVItem');
        
        // If no values remain (only the base state item[0]), exit deletion mode automatically
        if (remainingItems.length === 1) {
            deleteButton.click(); // Programmatically click to toggle out of deleting mode
        } else {
            // Otherwise, keep deletion mode active but update visual state
            remainingItems.forEach((item, index) => {
                 item.classList.toggle('deleting-target', index > 0); 
            });
        }

        // 6. Update the SVG result
        updateEtaniResult();
        
    } else if (itemIndex === 0 && etaniItemRight.closest('.etaniCol').classList.contains('deleting-mode')) {
        alert("The first value cannot be deleted.");
    }
}


/**
 * Handles the click event for the 'Transform' button.
 * Adds animateTransform tags to all <use/> elements and adds controls to .etaniItemRight.
 */
function handleAllAppendTransformClick() {
    if (!etani_clone) return;

    const etdropUses = document.querySelectorAll('#etmain .etdrop use'); // Select original <use> elements for their href
    const etaniItemRights = document.querySelectorAll('.etaniItemRight');
    const mode = getAnimationModeAttributes(); // Get fill/repeat mode (Requirement 3)

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

        // Check if transform controls already exist to prevent duplication
        if (itemRight.querySelector('.etaniAnimate')) return; 

        // 1. Find the corresponding <use> element in etani_clone
        let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;
        
        // 2. Perform the required multi-layer wrapping and initial animation setup (Requirement 1)
        
        const baseAnimate = (type, initialValue) => {
            const animate = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
            animate.setAttribute('attributeName', 'transform');
            animate.setAttribute('attributeType', 'XML');
            animate.setAttribute('type', type);
            animate.setAttribute('values', initialValue); 
            // dur is not set initially (Requirement 4)
            if (mode.fill) { animate.setAttribute('fill', mode.fill); } // Requirement 3
            if (mode.repeat) { animate.setAttribute('repeatCount', mode.repeat); } // Requirement 3
            animate.setAttribute('additive', 'sum'); 
            return animate;
        };

        // a. <use> element: Add rotate animation (Requirement 1)
        cloneUseElement.appendChild(baseAnimate('rotate', '0'));

        // b. Wrap in First G (scale animation) (Requirement 1)
        const g1Wrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        cloneUseElement.parentNode.insertBefore(g1Wrapper, cloneUseElement);
        g1Wrapper.appendChild(cloneUseElement);
        g1Wrapper.appendChild(baseAnimate('scale', '1'));

        // c. Wrap in Second G (translate animation) and link to UI (Requirement 1)
        const g2Wrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        g2Wrapper.setAttribute('data-use-href', `#${useId}`); 
        g1Wrapper.parentNode.insertBefore(g2Wrapper, g1Wrapper);
        g2Wrapper.appendChild(g1Wrapper);
        g2Wrapper.appendChild(baseAnimate('translate', '0,0'));


        // 3. Add .etaniAnimate controls to .etaniItemRight (Unchanged UI logic)
        const etaniAnimate = document.createElement('div');
        etaniAnimate.className = 'etaniAnimate';

        const nameSpan = document.createElement('span');
        nameSpan.className = 'etaniAnimateName';
        nameSpan.textContent = 'transform';
        etaniAnimate.appendChild(nameSpan);

        const durSpan = document.createElement('span');
        durSpan.className = 'etaniAnimateDur';
        durSpan.textContent = 'dur: 0s'; 
        etaniAnimate.appendChild(durSpan);

        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        const avCtrlDiv = document.createElement('div');
        avCtrlDiv.className = 'etaniAVCtrl';

        const avAddSpan = document.createElement('span');
        avAddSpan.className = 'etaniAVAdd';
        avAddSpan.title = 'Add Transform Value';
        avAddSpan.addEventListener('click', (e) => handleAVAddClick(e, useId));
        
        const plusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        plusSvg.setAttribute('width', '24');
        plusSvg.setAttribute('height', '24');
        plusSvg.setAttribute('viewBox', '0 0 24 24');
        plusSvg.setAttribute('fill', 'none');
        plusSvg.setAttribute('stroke', 'currentColor');
        plusSvg.setAttribute('stroke-width', '1'); 
        plusSvg.setAttribute('stroke-linecap', 'round');
        plusSvg.setAttribute('stroke-linejoin', 'round');
        plusSvg.innerHTML = '<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>';
        avAddSpan.appendChild(plusSvg);


        const avDeleteSpan = document.createElement('span');
        avDeleteSpan.className = 'etaniAVDelete';
        avDeleteSpan.title = 'Delete Value';
        avDeleteSpan.addEventListener('click', handleAVDeleteToggle);

        const minusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        minusSvg.setAttribute('width', '24');
        minusSvg.setAttribute('height', '24');
        minusSvg.setAttribute('viewBox', '0 0 24 24');
        minusSvg.setAttribute('fill', 'none');
        minusSvg.setAttribute('stroke', 'currentColor');
        minusSvg.setAttribute('stroke-width', '1'); 
        minusSvg.setAttribute('stroke-linecap', 'round');
        minusSvg.setAttribute('stroke-linejoin', 'round');
        minusSvg.innerHTML = '<line x1="5" y1="12" x2="19" y2="12"></line>';
        avDeleteSpan.appendChild(minusSvg);
        
        avCtrlDiv.appendChild(avAddSpan);
        avCtrlDiv.appendChild(avDeleteSpan);


        const avLabelSpan = document.createElement('span');
        avLabelSpan.className = 'etaniAVLabel';
        avLabelSpan.textContent = 'values : ';

        const avDiv = document.createElement('div');
        avDiv.className = 'etaniAV';
        
        const avItemSpan = document.createElement('span');
        avItemSpan.className = 'etaniAVItem';
        avItemSpan.addEventListener('click', handleAVItemClick); 
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avCtrlDiv); 
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

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

    // 4. Update the SVG result immediately
    updateEtaniResult();
}

/**
 * Handles the Setting Mode switch (Freeze/Repeat).
 * @param {string} mode The animation end mode ('freeze' or 'repeat').
 */
function handleSettingModeChange(mode) {
    if (!etani_clone) return;

    const modeAttrs = getAnimationModeAttributes(); 
    const isRepeat = mode === 'repeat';

    // Select all animateTransform elements within the animation structure
    const animates = etani_clone.querySelectorAll('.etdrop g[data-use-href] animateTransform, .etdrop g[data-use-href] use animateTransform');

    animates.forEach(animate => {
        // Apply fill/repeatCount based on mode (Requirement 3)
        if (modeAttrs.fill) {
            animate.setAttribute('fill', modeAttrs.fill);
            animate.removeAttribute('repeatCount');
        } else if (modeAttrs.repeat) {
            animate.setAttribute('repeatCount', modeAttrs.repeat);
            animate.removeAttribute('fill');
        }
    });

    // Update active state
    document.querySelector('.etaniSettingFreeze').classList.toggle('active', !isRepeat);
    document.querySelector('.etaniSettingRepeat').classList.toggle('active', isRepeat);

    updateEtaniResult();
}


// --- MAIN STRUCTURE CREATION ---

/**
 * Creates the internal animation control structure.
 * @param {HTMLElement} etaniouter The outer container element.
 */
function createEtaniInner(etaniouter) {
    // 5.1, 5.2, 5.3 Clone, remove .etwait, update ID
    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    
    // Clear global state first
    originalTransforms.clear();
    
    // Store original <use> transforms (cv) globally
    document.querySelectorAll('#etmain .etdrop use').forEach(originalUse => {
        const useId = originalUse.getAttribute('href').substring(1);
        const transformString = originalUse.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)';
        originalTransforms.set(useId, parseTransform(transformString));
    });
    
    etani_clone = originalSvg.cloneNode(true);
    const etwaitElement = etani_clone.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    etani_clone.id = 'etmainani';


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

    // Create div.etaniCtrl
    const etaniCtrl = document.createElement('div');
    etaniCtrl.className = 'etaniCtrl';
    
    // Content and Center link
    const etaniContent = document.createElement('div');
    etaniContent.className = 'etaniContent';
    
    const contentHTMLLink = document.createElement('a');
    contentHTMLLink.className = 'etaniContentHTML';
    contentHTMLLink.textContent = 'Show HTML';
    contentHTMLLink.href = '#';
    contentHTMLLink.addEventListener('click', handleContentHTMLClick);
    etaniContent.appendChild(contentHTMLLink);

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


    // Setting Mode (Radio Buttons)
    const etaniSetting = document.createElement('div');
    etaniSetting.className = 'etaniSetting';
    
    const freezeRadio = document.createElement('span');
    freezeRadio.className = 'etaniSettingMode etaniSettingFreeze active';
    freezeRadio.textContent = 'Freeze';
    freezeRadio.setAttribute('data-mode', 'freeze');
    freezeRadio.addEventListener('click', () => handleSettingModeChange('freeze'));
    
    const repeatRadio = document.createElement('span');
    repeatRadio.className = 'etaniSettingMode etaniSettingRepeat';
    repeatRadio.textContent = 'Repeat';
    repeatRadio.setAttribute('data-mode', 'repeat');
    repeatRadio.addEventListener('click', () => handleSettingModeChange('repeat'));

    etaniSetting.appendChild(freezeRadio);
    etaniSetting.appendChild(repeatRadio);
    etaniCtrl.appendChild(etaniSetting);


    // All Append (Buttons)
    const etaniAllAppend = document.createElement('div');
    etaniAllAppend.className = 'etaniAllAppend';

    const transformButton = document.createElement('button');
    transformButton.className = 'etaniAllAppendTransform';
    transformButton.textContent = 'transform';
    transformButton.addEventListener('click', handleAllAppendTransformClick);
    
    const opacityButton = document.createElement('button');
    opacityButton.className = 'etaniAllAppendOpacity';
    opacityButton.textContent = 'opacity';
    // opacityButton.addEventListener('click', handleAllAppendOpacityClick); // Placeholder
    
    etaniAllAppend.appendChild(transformButton);
    etaniAllAppend.appendChild(opacityButton);
    etaniCtrl.appendChild(etaniAllAppend);
    

    // Create div.etaniCol (Tile list)
    const etaniCol = document.createElement('div');
    etaniCol.className = 'etaniCol';

    // Create div.etaniResult
    const etaniResult = document.createElement('div');
    etaniResult.className = 'etaniResult';
    
    // Result elements (img, download, rename, size) 
    const resultImg = document.createElement('img');
    resultImg.className = 'etaniResultImage';
    resultImg.alt = 'Rendered Ejtile Animation SVG';

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

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

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

    resultDRDiv.appendChild(downloadLink);
    resultDRDiv.appendChild(renameLink);
    
    etaniResult.appendChild(resultImg);
    etaniResult.appendChild(resultDRDiv);
    etaniResult.appendChild(sizeSpan);
    
    // Append children to etaniinner
    etaniinner.appendChild(etaniCtrl);
    etaniinner.appendChild(etaniCol);
    etaniinner.appendChild(etaniResult);

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

    // III, IV, V, VI, VII. Populate etaniCol (Tile list)
    const etdropUses = document.querySelectorAll('.etdrop use');
    const etwaitGroups = document.querySelectorAll('.etwait g');

    etdropUses.forEach((useElement, i) => {
        // (Tile processing logic - Unchanged)
        const tileid = useElement.getAttribute('href').substring(1);
        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 originalTile = document.querySelector(`defs g#${tileid}`);
        
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';

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

        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        
        if (originalTile) {
            const tileclone = originalTile.cloneNode(true);
            tileclone.removeAttribute('id');
            if (etwaittransform) tileclone.setAttribute('transform', etwaittransform);
            if (etwaitfill) tileclone.setAttribute('fill', etwaitfill);
            if (etwaitstroke) tileclone.setAttribute('stroke', etwaitstroke);
            if (etwaitstrokeWidth) tileclone.setAttribute('stroke-width', etwaitstrokeWidth);
            
            const svgWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            svgWrapper.setAttribute('width', '40');
            svgWrapper.setAttribute('height', '40');
            svgWrapper.setAttribute('version', '1.1');
            svgWrapper.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
            svgWrapper.setAttribute('xmlns:svg', 'http://www.w3.org/2000/svg');
            svgWrapper.className = 'etanitileimg';
            
            svgWrapper.appendChild(tileclone);
            etaniItemLeft.appendChild(svgWrapper);
        }

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

        etaniItem.appendChild(etaniItemLeft);
        etaniItem.appendChild(etaniItemRight);
        etaniCol.appendChild(etaniItem);
    });
    
    // 5. Update the result section immediately
    updateEtaniResult();
}

// --- INITIALIZATION ---

/**
 * Toggles the visibility and content of the animation control panel.
 * @param {Event} event The click event.
 */
function toggleAnimation(event) {
    const etanibutton = event.currentTarget;
    const etaniouter = etanibutton.parentNode;
    const etaniinner = document.getElementById('etaniinner');

    if (etanibutton.textContent === 'Animate it') {
        etanibutton.textContent = 'Close Ejtile Ani';
        createEtaniInner(etaniouter);
        
    } else if (etanibutton.textContent === 'Close Ejtile Ani') {
        etanibutton.textContent = 'Animate it';
        if (etaniinner) {
            etaniinner.remove();
        }
        etani_clone = null;
        originalTransforms.clear();
    }
}


window.addEventListener('load', () => {
    addDynamicStyles();

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

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

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

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

帖子 ejsoon »

上一問最好的一個版本。

代码: 全选

更改下方的js代碼。

當點擊etaniAVAdd時,要先計算rotate,再計算scale,最後計算translate。

要更改translate的算法。

例如,之前的算法是:

v_translate_x = current_translate_x - original_translate_x,v_translate_y = current_translate_y - original_translate_y

現在要改成:

當scale計算結果是v_scale_x,v_scale_y時,v_translate_x = current_translate_x - (original_translate_x * v_scale_x),v_translate_y = current_translate_y - (original_translate_y * v_scale_y)。

代码: 全选

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

// --- CSS STYLES (Kept from previous iteration as no changes were requested here) ---

function addDynamicStyles() {
    if (document.getElementById('dynamic-et-styles')) {
        return;
    }

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

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

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

        /* IV. Styles for etaniResult */
        .etaniResult {
            text-align: center;
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #bbb;
            overflow: auto;
        }
        
        /* ------------------ CONTROL & BUTTON STYLES ------------------ */

        /* Styles for control links (Center button) */
        .etaniContent a {
            display: inline-block;
            margin: 0 5px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 14px;
        }
        
        .etaniCenter {
            border: 1px solid green;
            color: green;
        }
        
        .etaniContentHTML {
            border: 1px solid #0099ff;
            color: #0099ff;
            margin-right: 15px;
        }

        /* Requirement 3: etaniAllAppend buttons */
        .etaniAllAppend button {
            padding: 5px 10px;
            font-size: 16px;
            margin: 0 5px;
            cursor: pointer;
            border: 1px solid #333;
            background-color: #fff;
        }
        
        /* Requirement 2: Custom Radio button styles */
        .etaniSettingMode {
            display: inline-block;
            cursor: pointer;
            padding: 4px 8px;
            margin: 0 3px;
            font-size: 14px;
            border: 1px solid #888;
            background-color: #eee;
            color: #333;
            user-select: none;
        }

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

        /* ------------------ RESULT & ANIMAION ITEM STYLES ------------------ */

        .etaniResultDR {
            text-align: center;
            margin-bottom: 10px;
        }

        /* Requirement 1: Download and rename link font size 16px */
        .etaniResultDownload, .etaniResultRename {
            display: inline-block;
            margin-right: 15px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 16px; 
        }
        
        /* .etaniResultDownload default style */
        .etaniResultDownload {
            border: 1px solid blue;
            color: blue;
        }

        /* .etaniResultRename style (Requirement 1: color changed to brown) */
        .etaniResultRename {
            border: 1px solid brown;
            color: brown;
        }

        .etaniResultImage {
            display: block;
            max-width: 480px;
            width: 100%;
            height: auto;
            margin: 0 auto 10px auto;
            border: 1px solid #000;
        }
        
        /* Requirement 2: Minimum font size 12px */
        .etaniResultSize {
            display: inline-block;
            margin-left: 10px;
            font-size: 12px; 
            color: #555;
        }
        
        /* etaniItem structure (Float layout) */
        .etaniItem {
            min-height: 48px;
            border: 1px solid #ccc;
            box-sizing: border-box;
            width: 100%;
            margin-bottom: -1px;
            overflow: auto;
        }
        
        .etaniItemLeft {
            float: left; 
            width: 60px;
            min-height: 48px;
            border-right: 1px solid #ccc;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            background-color: #e8e8e8;
            padding: 2px 0;
        }
        
        .etaniItemRight {
            margin-left: 60px; 
            padding: 7px;
            min-height: 48px; 
            background-color: #fff;
        }

        /* Requirement 2: Minimum font size 12px */
        .tileid {
            text-align: center;
            font-size: 12px; 
            word-break: break-all;
            padding-top: 2px;
        }
        
        /* ------------------ ANIMATE CONTROLS (4.1) ------------------ */

        .etaniAnimate {
            border: 1px solid #999;
            padding: 5px;
            margin-bottom: 5px;
        }

        .etaniAnimateName {
            display: inline-block;
            padding: 2px 5px;
            background-color: #555; /* Dark grey background */
            color: white; /* White text */
            margin-right: 10px;
            font-size: 12px; /* Minimum font size 12px */
        }

        .etaniAnimateDur {
            display: inline-block;
            margin-right: 10px;
            font-size: 14px;
        }

        .etaniAnimateValue {
            margin-top: 5px;
            overflow: auto;
        }

        /* New container for Add/Delete buttons (Requirement 3) */
        .etaniAVCtrl {
            display: inline-block;
            vertical-align: top;
            margin-right: 5px;
        }
        
        /* Custom CSS rule for SVG alignment (Requirement 4) */
        .etaniAVCtrl svg {
            margin-left: -1px;
            margin-top: -1px;
        }

        /* Shared style for control buttons (Requirement 4) */
        .etaniAVCtrl span {
            display: inline-block;
            width: 24px;
            height: 24px;
            line-height: 24px;
            text-align: center;
            cursor: pointer;
            box-sizing: border-box;
            vertical-align: top;
            margin-right: 3px; /* New margin */
        }

        .etaniAVAdd {
            background-color: #a7fca7; /* Light green */
            border: 1px solid #71c371; /* Thin line near light green */
        }
        
        .etaniAVDelete {
            background-color: #ffcccc; /* Light red */
            border: 1px solid #cc3333; /* Darker red */
        }

        .etaniAVDelete.deleting {
            background-color: #cc3333; /* Dark red when active */
        }
        
        .etaniAVLabel {
            font-size: 14px;
            margin-right: 5px;
        }

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

        .etaniAVItem {
            display: inline-block;
            width: 24px;
            height: 24px;
            background-color: #ff9933; /* Dark orange */
            border: 1px dashed #00bfff; /* Dashed sky blue */
            margin: 0 5px;
            box-sizing: border-box;
            cursor: pointer;
            position: relative;
        }

        .etaniAVItem.deleting-target {
            background-color: #ff4d4d; /* Reddish when targeted for deletion */
            border: 2px solid red;
        }
        
        /* ------------------ MODAL/POPUP STYLES ------------------ */
        .modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.5);
            z-index: 1000;
        }
        .modal-content {
            position: fixed;
            width: 98%;
            height: 48%;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 10px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
            z-index: 1001;
            box-sizing: border-box;
        }
        .modal-content textarea {
            width: 100%;
            height: calc(100% - 50px); /* Adjusted for larger close button */
            resize: none;
            border: 1px solid #ccc;
            font-size: 12px;
            box-sizing: border-box;
        }
        .modal-close {
            position: absolute;
            top: 0;
            right: 10px;
            font-size: 48px;
            line-height: 1; 
            cursor: pointer;
            color: #333;
        }
    `;

    document.head.appendChild(styleSheet);
}

// --- UTILITY FUNCTIONS ---

/**
 * Extracts a specific transform function and its value from a transform string.
 * @param {string} transformString The full transform string.
 * @param {string} type The transform type ('translate', 'scale', 'rotate').
 * @returns {{func: string, value: string}} The transform function and its value.
 */
function extractTransformPart(transformString, type) {
    const regex = new RegExp(`(${type})\\(([^)]+)\\)`, 'i');
    const match = transformString.match(regex);
    if (match) {
        return { func: match[1], value: match[2].trim() };
    }
    return { func: '', value: '' };
}

/**
 * Parses transform string to get individual transform values.
 * @param {string} transformString The transform attribute string.
 * @returns {{translate: string, scale: string, rotate: string}} Parsed values.
 */
function parseTransform(transformString) {
    const defaultTransform = { translate: '0,0', scale: '1,1', rotate: '0' };
    if (!transformString) return defaultTransform;

    const transform = {};

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

    transform.translate = getMatch('translate') || defaultTransform.translate;
    transform.scale = getMatch('scale') || defaultTransform.scale;
    // Normalize scale: if only one value (e.g., '1'), ensure it's '1,1' for coordinate parsing
    if (transform.scale.split(',').length === 1) {
        transform.scale += `,${transform.scale}`;
    }
    transform.rotate = getMatch('rotate') || defaultTransform.rotate;

    return transform;
}


/**
 * Converts an SVG string to a Base64 data URL.
 * @param {string} svgString The SVG XML string.
 * @returns {string} The Base64 data URL.
 */
function svgToBase64(svgString) {
    const base64 = btoa(unescape(encodeURIComponent(svgString)));
    return `data:image/svg+xml;base64,${base64}`;
}

/**
 * Formats byte size into human-readable string (e.g., KB, MB).
 * @param {number} bytes The size in bytes.
 * @returns {string} The formatted string.
 */
function formatBytes(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

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

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

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

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

/**
 * Handler for the 'Center' button click. Resets the .etdrop transform.
 * @param {Event} e The click event.
 */
function handleCenterClick(e) {
    e.preventDefault();
    if (etani_clone) {
        const etdrop = etani_clone.querySelector('.etdrop');
        if (etdrop) {
            // Reset position to the original center for the entire group
            etdrop.setAttribute('transform', 'translate(240,240) scale(1,1)');
            updateEtaniResult();
        }
    }
}

/**
 * Handles the HTML popup window.
 * @param {Event} e The click event.
 */
function handleContentHTMLClick(e) {
    e.preventDefault();
    if (!etani_clone) return;

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

    // Close logic
    const closeModal = () => {
        if (document.body.contains(overlay)) document.body.removeChild(overlay);
        if (document.body.contains(content)) document.body.removeChild(content);
    };
    close.onclick = closeModal;
    overlay.onclick = closeModal; // Clicking background also closes

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

/**
 * Calculates the required *additive* transform value for a given type, 
 * using the specified rounding rule.
 * @param {string} type The transform type ('translate', 'scale', 'rotate').
 * @param {string} originalValue The transform value from the original <use/> (cv).
 * @param {string} currentBaseValue The current base transform value from the etani_clone <use/> (ov).
 * @returns {string} The new additive value (v).
 */
function calculateAdditiveValue(type, originalValue, currentBaseValue) {
    const roundToThreeDecimals = (value) => {
        return Math.round(value * 1000) / 1000;
    };
    
    if (type === 'translate') {
        const ovCoords = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
        const cvCoords = originalValue.split(',').map(c => parseFloat(c.trim()));
        // v1 = cv1 - ov1, v2 = cv2 - ov2
        const v1 = roundToThreeDecimals(cvCoords[0] - ovCoords[0]);
        const v2 = roundToThreeDecimals(cvCoords[1] - ovCoords[1]);
        return `${v1},${v2}`;
    } else if (type === 'scale') {
        const ovScales = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
        const cvScales = originalValue.split(',').map(c => parseFloat(c.trim()));
        // v1 = cv1 / ov1, v2 = cv2 / ov2.
        const v1 = roundToThreeDecimals(cvScales[0] / ovScales[0]);
        const v2 = roundToThreeDecimals(cvScales.length > 1 ? (cvScales[1] / ovScales[1]) : (cvScales[0] / ovScales[0]));
        return v1 === v2 ? `${v1}` : `${v1},${v2}`;
    } else if (type === 'rotate') {
        const ovAngle = parseFloat(currentBaseValue.trim());
        const cvAngle = parseFloat(originalValue.trim());
        // v3 = cv3 - ov3
        return roundToThreeDecimals(cvAngle - ovAngle);
    }
    return '';
}

/**
 * Handles the click event for the '+' button.
 * Adds a new animateTransform value.
 * @param {Event} e The click event.
 * @param {string} useElementId The ID of the original <g> element referenced by the <use/>.
 */
function handleAVAddClick(e, useElementId) {
    e.preventDefault();
    if (!etani_clone) return;

    // 1. Get current transform of the original <use/> element in svg#etmain (cv)
    const originalUseElement = document.querySelector(`.etdrop use[href="#${useElementId}"]`);
    if (!originalUseElement) return;

    const originalTransformString = originalUseElement.getAttribute('transform') || '';
    const originalTransforms = parseTransform(originalTransformString);
    
    // 2. Find the corresponding elements in etani_clone
    // Second G (Ancestor) is the translate wrapper
    const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useElementId}"]`);
    if (!translateWrapper) return;
    
    // First G (Parent) is the scale wrapper
    const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useElementId}"]`);
    // Use element is the rotate target
    const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
    
    if (!scaleWrapper || !cloneUseElement) return;
    
    // 3. Get the *base* transform values from the clone's <use> (ov)
    // Based on Requirement 2, we use the <use> element's transform attribute as the baseline (ov).
    const cloneBaseTransformString = cloneUseElement.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)'; 
    const currentBaseTransforms = parseTransform(cloneBaseTransformString);
    
    const currentBaseTranslate = currentBaseTransforms.translate;
    const currentBaseScale = currentBaseTransforms.scale;
    const currentBaseRotate = currentBaseTransforms.rotate;

    // 4. Collect all relevant animateTransform tags
    const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
    const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
    const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
    
    const allAnimates = [...animatesTranslate, ...animatesScale, ...animatesRotate];
    
    // Calculate new duration (starts from 0s, adds 1s)
    const currentDur = parseInt((animatesRotate[0].getAttribute('dur') || '0s').replace('s', '')) || 0;
    const newDur = currentDur + 1;

    // Determine repeat/freeze state (Requirement 3)
    const isRepeat = document.querySelector('.etaniSettingRepeat.active');
    
    allAnimates.forEach(animate => {
        const type = animate.getAttribute('type').toLowerCase();
        let currentValueString = animate.getAttribute('values') || '';
        let newValue = '';
        
        // Calculate the *additive* difference based on new formula (Requirement 2)
        if (type === 'translate') {
            newValue = calculateAdditiveValue('translate', originalTransforms.translate, currentBaseTranslate);
        } else if (type === 'scale') {
            newValue = calculateAdditiveValue('scale', originalTransforms.scale, currentBaseScale);
        } else if (type === 'rotate') {
            newValue = calculateAdditiveValue('rotate', originalTransforms.rotate, currentBaseRotate);
        }
        
        // Append the new value
        animate.setAttribute('values', (currentValueString ? currentValueString + ';' : '') + newValue);

        // Update duration (Requirement 4)
        if (newDur > 0) {
            animate.setAttribute('dur', `${newDur}s`);
            // Set repeat/freeze state (Requirement 3)
            if (isRepeat) {
                animate.removeAttribute('fill');
                animate.setAttribute('repeatCount', 'indefinite');
            } else {
                animate.removeAttribute('repeatCount');
                animate.setAttribute('fill', 'freeze');
            }
        } else {
            animate.removeAttribute('dur');
            animate.removeAttribute('fill');
            animate.removeAttribute('repeatCount');
        }
    });

    // 5. Add a new .etaniAVItem in .etaniItemRight and update UI duration
    const etaniItemRight = e.currentTarget.closest('.etaniItemRight');
    const etaniAV = etaniItemRight.querySelector('.etaniAV');
    if (etaniAV) {
        const newAVItem = document.createElement('span');
        newAVItem.className = 'etaniAVItem';
        newAVItem.addEventListener('click', handleAVItemClick); // Bind deletion handler
        etaniAV.appendChild(newAVItem);
    }
    etaniItemRight.querySelector('.etaniAnimateDur').textContent = `dur: ${newDur}s`;

    // 6. Update the SVG result
    updateEtaniResult();
}

/**
 * Handles the click event for the '-' button.
 * Toggles deletion mode, allowing for continuous deletion (Requirement 1).
 * @param {Event} e The click event.
 */
function handleAVDeleteToggle(e) {
    e.preventDefault();
    const deleteButton = e.currentTarget;
    const isDeleting = deleteButton.classList.toggle('deleting');
    const etaniItemRight = deleteButton.closest('.etaniItemRight');
    const etaniCol = deleteButton.closest('.etaniCol');
    
    const avItems = etaniItemRight.querySelectorAll('.etaniAVItem');
    
    // Toggle visual state of all items (except the first one)
    avItems.forEach((item, index) => {
        if (index > 0) {
            // Apply deleting-target class only if deleting mode is active
            item.classList.toggle('deleting-target', isDeleting);
        }
    });

    // Toggle deleting state for this item's column
    etaniCol.classList.toggle('deleting-mode', isDeleting);

    // Disable all other AVDelete buttons if entering deleting mode
    document.querySelectorAll('.etaniAVDelete').forEach(btn => {
        if (btn !== deleteButton) {
            btn.closest('.etaniAVCtrl').disabled = isDeleting;
            btn.style.opacity = isDeleting ? 0.5 : 1; // Visual feedback for disabled state
        }
    });

    if (isDeleting) {
        deleteButton.title = "Click value item to delete (Click again to cancel)";
        deleteButton.style.color = 'white'; // White icon for active state
    } else {
        deleteButton.title = "Delete Value";
        deleteButton.style.color = 'currentColor'; // Reset to default
        
        // Remove visual state from all items when cancelling
        avItems.forEach(item => {
            item.classList.remove('deleting-target');
        });
    }
}

/**
 * Handles the click event for an .etaniAVItem in deletion mode.
 * @param {Event} e The click event.
 */
function handleAVItemClick(e) {
    const itemToDelete = e.currentTarget;
    const itemIndex = Array.from(itemToDelete.parentNode.children).indexOf(itemToDelete);
    const etaniItemRight = itemToDelete.closest('.etaniItemRight');
    
    // Check if deletion mode is active and it's not the first item
    if (etaniItemRight.closest('.etaniCol').classList.contains('deleting-mode') && itemIndex > 0) {
        
        const deleteButton = etaniItemRight.querySelector('.etaniAVDelete');
        const useId = etaniItemRight.closest('.etaniItem').querySelector('.tileid').textContent;
        
        // 1. Find the corresponding elements in etani_clone
        const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useId}"]`);
        if (!translateWrapper) return;
        
        const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useId}"]`);
        const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
        if (!scaleWrapper || !cloneUseElement) return;

        // 2. Collect all relevant animateTransform tags
        const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
        const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
        const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
        
        const allAnimates = [...animatesTranslate, ...animatesScale, ...animatesRotate];
        
        // Calculate new duration (starts from 0s)
        const currentDur = parseInt((animatesRotate[0].getAttribute('dur') || '0s').replace('s', '')) || 0;
        const newDur = Math.max(0, currentDur - 1); // Duration cannot be less than 0
        
        allAnimates.forEach(animate => {
            const values = animate.getAttribute('values').split(';');
            
            // Remove the value at the target index (itemIndex)
            if (itemIndex < values.length) {
                values.splice(itemIndex, 1);
            }
            animate.setAttribute('values', values.join(';'));
            
            // Update duration (Requirement 4)
            if (newDur > 0) {
                animate.setAttribute('dur', `${newDur}s`);
            } else {
                animate.removeAttribute('dur');
                animate.removeAttribute('fill');
                animate.removeAttribute('repeatCount');
            }
        });

        // 3. Remove the item from the UI
        itemToDelete.remove();

        // 4. Update UI duration display
        etaniItemRight.querySelector('.etaniAnimateDur').textContent = `dur: ${newDur}s`;
        
        // 5. Update visual state of remaining items in deletion mode (Requirement 1)
        const remainingItems = etaniItemRight.querySelectorAll('.etaniAVItem');
        
        // If no values remain (only the base state item[0]), exit deletion mode automatically
        if (remainingItems.length === 1) {
            deleteButton.click(); // Programmatically click to toggle out of deleting mode
        } else {
            // Otherwise, keep deletion mode active but update visual state
            remainingItems.forEach((item, index) => {
                 item.classList.toggle('deleting-target', index > 0); 
            });
        }

        // 6. Update the SVG result
        updateEtaniResult();
        
    } else if (itemIndex === 0 && etaniItemRight.closest('.etaniCol').classList.contains('deleting-mode')) {
        alert("The first value cannot be deleted.");
    }
}


/**
 * Handles the click event for the 'Transform' button.
 * Adds the required <g> wrappers and animateTransform tags.
 */
function handleAllAppendTransformClick() {
    if (!etani_clone) return;

    const etdropUses = document.querySelectorAll('#etmain .etdrop use'); // Select original <use> elements for their href
    const etaniItemRights = document.querySelectorAll('.etaniItemRight');

    // Determine repeat/freeze state (Requirement 3)
    const isRepeat = document.querySelector('.etaniSettingRepeat.active');

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

        // Check if transform controls already exist to prevent duplication
        if (itemRight.querySelector('.etaniAnimate')) return; 

        // 1. Find the corresponding <use> element in etani_clone
        let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;

        // 2. Define the base animate function
        const baseAnimate = (type, initialValue) => {
            const animate = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
            animate.setAttribute('attributeName', 'transform');
            animate.setAttribute('attributeType', 'XML');
            animate.setAttribute('type', type);
            // Initial value is the base state's additive difference, which is always 0 or 1 for the first point
            animate.setAttribute('values', initialValue); 
            
            // dur="0s" is not set, only added when > 0 (Requirement 4)
            
            animate.setAttribute('fill', isRepeat ? 'remove' : 'freeze'); // Requirement 3
            if (isRepeat) animate.setAttribute('repeatCount', 'indefinite'); // Requirement 3

            animate.setAttribute('additive', 'sum'); // Important for relative transforms
            return animate;
        };
        
        // --- Requirement 1: Constructing the nested G structure ---
        
        // a. Add animateTransform[type=rotate][value=0] to <use>
        cloneUseElement.appendChild(baseAnimate('rotate', '0'));
        
        // b. Create first G (Scale Wrapper) and add animateTransform[type=scale][value=1]
        const scaleWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        scaleWrapper.setAttribute('data-use-href-scale', `#${useId}`); // Use a specific data attribute for scale wrapper
        scaleWrapper.appendChild(baseAnimate('scale', '1'));
        
        // c. Create second G (Translate Wrapper) and add animateTransform[type=translate][value=0,0]
        const translateWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        translateWrapper.setAttribute('data-use-href', `#${useId}`); // Main ID link
        translateWrapper.appendChild(baseAnimate('translate', '0,0'));
        
        // d. Perform the wrapping: (G_Translate) -> (G_Scale) -> (USE + Rotate_Animate)
        
        // i. Insert G_Translate before the original <use>
        cloneUseElement.parentNode.insertBefore(translateWrapper, cloneUseElement);
        
        // ii. Wrap <use> with G_Scale
        cloneUseElement.parentNode.insertBefore(scaleWrapper, cloneUseElement);
        scaleWrapper.appendChild(cloneUseElement);
        
        // iii. Wrap G_Scale with G_Translate
        translateWrapper.appendChild(scaleWrapper);
        

        // --- UI Control Creation (Unchanged logic) ---

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

        // Animate Name (Dark grey background, white text)
        const nameSpan = document.createElement('span');
        nameSpan.className = 'etaniAnimateName';
        nameSpan.textContent = 'transform';
        etaniAnimate.appendChild(nameSpan);

        // Animate Dur (dur: n)
        const durSpan = document.createElement('span');
        durSpan.className = 'etaniAnimateDur';
        durSpan.textContent = 'dur: 0s'; // Initial value 0s (Requirement 5)
        etaniAnimate.appendChild(durSpan);

        // Animate Value container
        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        // New container for buttons (Requirement 3)
        const avCtrlDiv = document.createElement('div');
        avCtrlDiv.className = 'etaniAVCtrl';

        // AV Add (Plus sign SVG)
        const avAddSpan = document.createElement('span');
        avAddSpan.className = 'etaniAVAdd';
        avAddSpan.title = 'Add Transform Value';
        avAddSpan.addEventListener('click', (e) => handleAVAddClick(e, useId));
        
        const plusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        plusSvg.setAttribute('width', '24');
        plusSvg.setAttribute('height', '24');
        plusSvg.setAttribute('viewBox', '0 0 24 24');
        plusSvg.setAttribute('fill', 'none');
        plusSvg.setAttribute('stroke', 'currentColor');
        plusSvg.setAttribute('stroke-width', '1'); // Requirement 2
        plusSvg.setAttribute('stroke-linecap', 'round');
        plusSvg.setAttribute('stroke-linejoin', 'round');
        plusSvg.innerHTML = '<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>';
        avAddSpan.appendChild(plusSvg);


        // AV Delete (Minus sign SVG)
        const avDeleteSpan = document.createElement('span');
        avDeleteSpan.className = 'etaniAVDelete';
        avDeleteSpan.title = 'Delete Value';
        avDeleteSpan.addEventListener('click', handleAVDeleteToggle);

        const minusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        minusSvg.setAttribute('width', '24');
        minusSvg.setAttribute('height', '24');
        minusSvg.setAttribute('viewBox', '0 0 24 24');
        minusSvg.setAttribute('fill', 'none');
        minusSvg.setAttribute('stroke', 'currentColor');
        minusSvg.setAttribute('stroke-width', '1'); // Requirement 2
        minusSvg.setAttribute('stroke-linecap', 'round');
        minusSvg.setAttribute('stroke-linejoin', 'round');
        minusSvg.innerHTML = '<line x1="5" y1="12" x2="19" y2="12"></line>';
        avDeleteSpan.appendChild(minusSvg);
        
        avCtrlDiv.appendChild(avAddSpan);
        avCtrlDiv.appendChild(avDeleteSpan);


        // AV Label
        const avLabelSpan = document.createElement('span');
        avLabelSpan.className = 'etaniAVLabel';
        avLabelSpan.textContent = 'values : ';

        // AV Container
        const avDiv = document.createElement('div');
        avDiv.className = 'etaniAV';
        
        // AV Item (Initially contains one)
        const avItemSpan = document.createElement('span');
        avItemSpan.className = 'etaniAVItem';
        avItemSpan.addEventListener('click', handleAVItemClick); // Bind deletion handler
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avCtrlDiv); // Append control buttons container (Requirement 3)
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

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

    // 7. Update the SVG result immediately
    updateEtaniResult();
}

/**
 * Handles the Setting Mode switch (Freeze/Repeat).
 * @param {string} mode The animation end mode ('freeze' or 'repeat').
 */
function handleSettingModeChange(mode) {
    if (!etani_clone) return;

    // We select all animateTransform tags
    const animates = etani_clone.querySelectorAll('animateTransform');
    const isRepeat = mode === 'repeat';

    animates.forEach(animate => {
        // Only modify if dur is present (i.e., dur > 0s)
        if (animate.hasAttribute('dur')) {
            if (isRepeat) {
                animate.removeAttribute('fill');
                animate.setAttribute('repeatCount', 'indefinite');
            } else {
                animate.removeAttribute('repeatCount');
                animate.setAttribute('fill', 'freeze');
            }
        }
    });

    // Update active state
    document.querySelector('.etaniSettingFreeze').classList.toggle('active', !isRepeat);
    document.querySelector('.etaniSettingRepeat').classList.toggle('active', isRepeat);

    updateEtaniResult();
}


// --- MAIN STRUCTURE CREATION ---

/**
 * Creates the internal animation control structure.
 * @param {HTMLElement} etaniouter The outer container element.
 */
function createEtaniInner(etaniouter) {
    // 5.1, 5.2, 5.3 Clone, remove .etwait, update ID
    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etani_clone = originalSvg.cloneNode(true);
    const etwaitElement = etani_clone.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    etani_clone.id = 'etmainani';


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

    // Create div.etaniCtrl
    const etaniCtrl = document.createElement('div');
    etaniCtrl.className = 'etaniCtrl';
    
    // Content and Center link
    const etaniContent = document.createElement('div');
    etaniContent.className = 'etaniContent';
    
    const contentHTMLLink = document.createElement('a');
    contentHTMLLink.className = 'etaniContentHTML';
    contentHTMLLink.textContent = 'Show HTML';
    contentHTMLLink.href = '#';
    contentHTMLLink.addEventListener('click', handleContentHTMLClick);
    etaniContent.appendChild(contentHTMLLink);

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


    // Setting Mode (Radio Buttons)
    const etaniSetting = document.createElement('div');
    etaniSetting.className = 'etaniSetting';
    
    const freezeRadio = document.createElement('span');
    freezeRadio.className = 'etaniSettingMode etaniSettingFreeze active';
    freezeRadio.textContent = 'Freeze';
    freezeRadio.setAttribute('data-mode', 'freeze');
    freezeRadio.addEventListener('click', () => handleSettingModeChange('freeze'));
    
    const repeatRadio = document.createElement('span');
    repeatRadio.className = 'etaniSettingMode etaniSettingRepeat';
    repeatRadio.textContent = 'Repeat';
    repeatRadio.setAttribute('data-mode', 'repeat');
    repeatRadio.addEventListener('click', () => handleSettingModeChange('repeat'));

    etaniSetting.appendChild(freezeRadio);
    etaniSetting.appendChild(repeatRadio);
    etaniCtrl.appendChild(etaniSetting);


    // All Append (Buttons)
    const etaniAllAppend = document.createElement('div');
    etaniAllAppend.className = 'etaniAllAppend';

    const transformButton = document.createElement('button');
    transformButton.className = 'etaniAllAppendTransform';
    transformButton.textContent = 'transform';
    transformButton.addEventListener('click', handleAllAppendTransformClick);
    
    const opacityButton = document.createElement('button');
    opacityButton.className = 'etaniAllAppendOpacity';
    opacityButton.textContent = 'opacity';
    // opacityButton.addEventListener('click', handleAllAppendOpacityClick); // Placeholder
    
    etaniAllAppend.appendChild(transformButton);
    etaniAllAppend.appendChild(opacityButton);
    etaniCtrl.appendChild(etaniAllAppend);
    

    // Create div.etaniCol (Tile list)
    const etaniCol = document.createElement('div');
    etaniCol.className = 'etaniCol';

    // Create div.etaniResult
    const etaniResult = document.createElement('div');
    etaniResult.className = 'etaniResult';
    
    // Result elements (img, download, rename, size) 
    const resultImg = document.createElement('img');
    resultImg.className = 'etaniResultImage';
    resultImg.alt = 'Rendered Ejtile Animation SVG';

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

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

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

    resultDRDiv.appendChild(downloadLink);
    resultDRDiv.appendChild(renameLink);
    
    etaniResult.appendChild(resultImg);
    etaniResult.appendChild(resultDRDiv);
    etaniResult.appendChild(sizeSpan);
    
    // Append children to etaniinner
    etaniinner.appendChild(etaniCtrl);
    etaniinner.appendChild(etaniCol);
    etaniinner.appendChild(etaniResult);

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

    // III, IV, V, VI, VII. Populate etaniCol (Tile list)
    const etdropUses = document.querySelectorAll('.etdrop use');
    const etwaitGroups = document.querySelectorAll('.etwait g');

    etdropUses.forEach((useElement, i) => {
        // (Tile processing logic - Unchanged)
        const tileid = useElement.getAttribute('href').substring(1);
        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 originalTile = document.querySelector(`defs g#${tileid}`);
        
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';

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

        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        
        if (originalTile) {
            const tileclone = originalTile.cloneNode(true);
            tileclone.removeAttribute('id');
            if (etwaittransform) tileclone.setAttribute('transform', etwaittransform);
            if (etwaitfill) tileclone.setAttribute('fill', etwaitfill);
            if (etwaitstroke) tileclone.setAttribute('stroke', etwaitstroke);
            if (etwaitstrokeWidth) tileclone.setAttribute('stroke-width', etwaitstrokeWidth);
            
            const svgWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            svgWrapper.setAttribute('width', '40');
            svgWrapper.setAttribute('height', '40');
            svgWrapper.setAttribute('version', '1.1');
            svgWrapper.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
            svgWrapper.setAttribute('xmlns:svg', 'http://www.w3.org/2000/svg');
            svgWrapper.className = 'etanitileimg';
            
            svgWrapper.appendChild(tileclone);
            etaniItemLeft.appendChild(svgWrapper);
        }

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

        etaniItem.appendChild(etaniItemLeft);
        etaniItem.appendChild(etaniItemRight);
        etaniCol.appendChild(etaniItem);
    });
    
    // 5. Update the result section immediately
    updateEtaniResult();
}

// --- INITIALIZATION ---

/**
 * Toggles the visibility and content of the animation control panel.
 * @param {Event} event The click event.
 */
function toggleAnimation(event) {
    const etanibutton = event.currentTarget;
    const etaniouter = etanibutton.parentNode;
    const etaniinner = document.getElementById('etaniinner');

    if (etanibutton.textContent === 'Animate it') {
        etanibutton.textContent = 'Close Ejtile Ani';
        createEtaniInner(etaniouter);
        
    } else if (etanibutton.textContent === 'Close Ejtile Ani') {
        etanibutton.textContent = 'Animate it';
        if (etaniinner) {
            etaniinner.remove();
        }
        etani_clone = null;
    }
}


window.addEventListener('load', () => {
    addDynamicStyles();

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

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

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

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

帖子 ejsoon »

gemini不行了,換grok:

代码: 全选

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

// --- CSS STYLES (Kept from previous iteration as no changes were requested here) ---

function addDynamicStyles() {
    if (document.getElementById('dynamic-et-styles')) {
        return;
    }

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

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

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

        /* IV. Styles for etaniResult */
        .etaniResult {
            text-align: center;
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #bbb;
            overflow: auto;
        }
        
        /* ------------------ CONTROL & BUTTON STYLES ------------------ */

        /* Styles for control links (Center button) */
        .etaniContent a {
            display: inline-block;
            margin: 0 5px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 14px;
        }
        
        .etaniCenter {
            border: 1px solid green;
            color: green;
        }
        
        .etaniContentHTML {
            border: 1px solid #0099ff;
            color: #0099ff;
            margin-right: 15px;
        }

        /* Requirement 3: etaniAllAppend buttons */
        .etaniAllAppend button {
            padding: 5px 10px;
            font-size: 16px;
            margin: 0 5px;
            cursor: pointer;
            border: 1px solid #333;
            background-color: #fff;
        }
        
        /* Requirement 2: Custom Radio button styles */
        .etaniSettingMode {
            display: inline-block;
            cursor: pointer;
            padding: 4px 8px;
            margin: 0 3px;
            font-size: 14px;
            border: 1px solid #888;
            background-color: #eee;
            color: #333;
            user-select: none;
        }

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

        /* ------------------ RESULT & ANIMAION ITEM STYLES ------------------ */

        .etaniResultDR {
            text-align: center;
            margin-bottom: 10px;
        }

        /* Requirement 1: Download and rename link font size 16px */
        .etaniResultDownload, .etaniResultRename {
            display: inline-block;
            margin-right: 15px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 16px; 
        }
        
        /* .etaniResultDownload default style */
        .etaniResultDownload {
            border: 1px solid blue;
            color: blue;
        }

        /* .etaniResultRename style (Requirement 1: color changed to brown) */
        .etaniResultRename {
            border: 1px solid brown;
            color: brown;
        }

        .etaniResultImage {
            display: block;
            max-width: 480px;
            width: 100%;
            height: auto;
            margin: 0 auto 10px auto;
            border: 1px solid #000;
        }
        
        /* Requirement 2: Minimum font size 12px */
        .etaniResultSize {
            display: inline-block;
            margin-left: 10px;
            font-size: 12px; 
            color: #555;
        }
        
        /* etaniItem structure (Float layout) */
        .etaniItem {
            min-height: 48px;
            border: 1px solid #ccc;
            box-sizing: border-box;
            width: 100%;
            margin-bottom: -1px;
            overflow: auto;
        }
        
        .etaniItemLeft {
            float: left; 
            width: 60px;
            min-height: 48px;
            border-right: 1px solid #ccc;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            background-color: #e8e8e8;
            padding: 2px 0;
        }
        
        .etaniItemRight {
            margin-left: 60px; 
            padding: 7px;
            min-height: 48px; 
            background-color: #fff;
        }

        /* Requirement 2: Minimum font size 12px */
        .tileid {
            text-align: center;
            font-size: 12px; 
            word-break: break-all;
            padding-top: 2px;
        }
        
        /* ------------------ ANIMATE CONTROLS (4.1) ------------------ */

        .etaniAnimate {
            border: 1px solid #999;
            padding: 5px;
            margin-bottom: 5px;
        }

        .etaniAnimateName {
            display: inline-block;
            padding: 2px 5px;
            background-color: #555; /* Dark grey background */
            color: white; /* White text */
            margin-right: 10px;
            font-size: 12px; /* Minimum font size 12px */
        }

        .etaniAnimateDur {
            display: inline-block;
            margin-right: 10px;
            font-size: 14px;
        }

        .etaniAnimateValue {
            margin-top: 5px;
            overflow: auto;
        }

        /* New container for Add/Delete buttons (Requirement 3) */
        .etaniAVCtrl {
            display: inline-block;
            vertical-align: top;
            margin-right: 5px;
        }
        
        /* Custom CSS rule for SVG alignment (Requirement 4) */
        .etaniAVCtrl svg {
            margin-left: -1px;
            margin-top: -1px;
        }

        /* Shared style for control buttons (Requirement 4) */
        .etaniAVCtrl span {
            display: inline-block;
            width: 24px;
            height: 24px;
            line-height: 24px;
            text-align: center;
            cursor: pointer;
            box-sizing: border-box;
            vertical-align: top;
            margin-right: 3px; /* New margin */
        }

        .etaniAVAdd {
            background-color: #a7fca7; /* Light green */
            border: 1px solid #71c371; /* Thin line near light green */
        }
        
        .etaniAVDelete {
            background-color: #ffcccc; /* Light red */
            border: 1px solid #cc3333; /* Darker red */
        }

        .etaniAVDelete.deleting {
            background-color: #cc3333; /* Dark red when active */
        }
        
        .etaniAVLabel {
            font-size: 14px;
            margin-right: 5px;
        }

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

        .etaniAVItem {
            display: inline-block;
            width: 24px;
            height: 24px;
            background-color: #ff9933; /* Dark orange */
            border: 1px dashed #00bfff; /* Dashed sky blue */
            margin: 0 5px;
            box-sizing: border-box;
            cursor: pointer;
            position: relative;
        }

        .etaniAVItem.deleting-target {
            background-color: #ff4d4d; /* Reddish when targeted for deletion */
            border: 2px solid red;
        }
        
        /* ------------------ MODAL/POPUP STYLES ------------------ */
        .modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.5);
            z-index: 1000;
        }
        .modal-content {
            position: fixed;
            width: 98%;
            height: 48%;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 10px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
            z-index: 1001;
            box-sizing: border-box;
        }
        .modal-content textarea {
            width: 100%;
            height: calc(100% - 50px); /* Adjusted for larger close button */
            resize: none;
            border: 1px solid #ccc;
            font-size: 12px;
            box-sizing: border-box;
        }
        .modal-close {
            position: absolute;
            top: 0;
            right: 10px;
            font-size: 48px;
            line-height: 1; 
            cursor: pointer;
            color: #333;
        }
    `;

    document.head.appendChild(styleSheet);
}

// --- UTILITY FUNCTIONS ---

/**
 * Extracts a specific transform function and its value from a transform string.
 * @param {string} transformString The full transform string.
 * @param {string} type The transform type ('translate', 'scale', 'rotate').
 * @returns {{func: string, value: string}} The transform function and its value.
 */
function extractTransformPart(transformString, type) {
    const regex = new RegExp(`(${type})\\(([^)]+)\\)`, 'i');
    const match = transformString.match(regex);
    if (match) {
        return { func: match[1], value: match[2].trim() };
    }
    return { func: '', value: '' };
}

/**
 * Parses transform string to get individual transform values.
 * @param {string} transformString The transform attribute string.
 * @returns {{translate: string, scale: string, rotate: string}} Parsed values.
 */
function parseTransform(transformString) {
    const defaultTransform = { translate: '0,0', scale: '1,1', rotate: '0' };
    if (!transformString) return defaultTransform;

    const transform = {};

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

    transform.translate = getMatch('translate') || defaultTransform.translate;
    transform.scale = getMatch('scale') || defaultTransform.scale;
    // Normalize scale: if only one value (e.g., '1'), ensure it's '1,1' for coordinate parsing
    if (transform.scale.split(',').length === 1) {
        transform.scale += `,${transform.scale}`;
    }
    transform.rotate = getMatch('rotate') || defaultTransform.rotate;

    return transform;
}

/**
 * Converts an SVG string to a Base64 data URL.
 * @param {string} svgString The SVG XML string.
 * @returns {string} The Base64 data URL.
 */
function svgToBase64(svgString) {
    const base64 = btoa(unescape(encodeURIComponent(svgString)));
    return `data:image/svg+xml;base64,${base64}`;
}

/**
 * Formats byte size into human-readable string (e.g., KB, MB).
 * @param {number} bytes The size in bytes.
 * @returns {string} The formatted string.
 */
function formatBytes(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

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

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

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

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

/**
 * Handler for the 'Center' button click. Resets the .etdrop transform.
 * @param {Event} e The click event.
 */
function handleCenterClick(e) {
    e.preventDefault();
    if (etani_clone) {
        const etdrop = etani_clone.querySelector('.etdrop');
        if (etdrop) {
            // Reset position to the original center for the entire group
            etdrop.setAttribute('transform', 'translate(240,240) scale(1,1)');
            updateEtaniResult();
        }
    }
}

/**
 * Handles the HTML popup window.
 * @param {Event} e The click event.
 */
function handleContentHTMLClick(e) {
    e.preventDefault();
    if (!etani_clone) return;

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

    // Close logic
    const closeModal = () => {
        if (document.body.contains(overlay)) document.body.removeChild(overlay);
        if (document.body.contains(content)) document.body.removeChild(content);
    };
    close.onclick = closeModal;
    overlay.onclick = closeModal; // Clicking background also closes

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

/**
 * Calculates the required *additive* transform value for a given type, 
 * using the specified rounding rule and updated translate algorithm.
 * @param {string} type The transform type ('translate', 'scale', 'rotate').
 * @param {string} originalValue The transform value from the original <use/> (cv).
 * @param {string} currentBaseValue The current base transform value from the etani_clone <use/> (ov).
 * @param {string} [scaleValue] The calculated scale value, used for translate calculations.
 * @returns {string} The new additive value (v).
 */
function calculateAdditiveValue(type, originalValue, currentBaseValue, scaleValue = '1,1') {
    const roundToThreeDecimals = (value) => {
        return Math.round(value * 1000) / 1000;
    };
    
    if (type === 'rotate') {
        const ovAngle = parseFloat(currentBaseValue.trim());
        const cvAngle = parseFloat(originalValue.trim());
        // v3 = cv3 - ov3
        return roundToThreeDecimals(cvAngle - ovAngle);
    } else if (type === 'scale') {
        const ovScales = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
        const cvScales = originalValue.split(',').map(c => parseFloat(c.trim()));
        // v1 = cv1 / ov1, v2 = cv2 / ov2
        const v1 = roundToThreeDecimals(cvScales[0] / ovScales[0]);
        const v2 = roundToThreeDecimals(cvScales.length > 1 ? (cvScales[1] / ovScales[1]) : (cvScales[0] / ovScales[0]));
        return v1 === v2 ? `${v1}` : `${v1},${v2}`;
    } else if (type === 'translate') {
        const ovCoords = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
        const cvCoords = originalValue.split(',').map(c => parseFloat(c.trim()));
        const scaleFactors = scaleValue.split(',').map(c => parseFloat(c.trim()));
        // New algorithm: v_translate_x = cv1 - (ov1 * v_scale_x), v_translate_y = cv2 - (ov2 * v_scale_y)
        const v1 = roundToThreeDecimals(cvCoords[0] - (ovCoords[0] * scaleFactors[0]));
        const v2 = roundToThreeDecimals(cvCoords[1] - (ovCoords[1] * (scaleFactors.length > 1 ? scaleFactors[1] : scaleFactors[0])));
        return `${v1},${v2}`;
    }
    return '';
}

/**
 * Handles the click event for the '+' button.
 * Adds a new animateTransform value in the order: rotate, scale, translate.
 * @param {Event} e The click event.
 * @param {string} useElementId The ID of the original <g> element referenced by the <use/>.
 */
function handleAVAddClick(e, useElementId) {
    e.preventDefault();
    if (!etani_clone) return;

    // 1. Get current transform of the original <use/> element in svg#etmain (cv)
    const originalUseElement = document.querySelector(`.etdrop use[href="#${useElementId}"]`);
    if (!originalUseElement) return;

    const originalTransformString = originalUseElement.getAttribute('transform') || '';
    const originalTransforms = parseTransform(originalTransformString);
    
    // 2. Find the corresponding elements in etani_clone
    // Second G (Ancestor) is the translate wrapper
    const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useElementId}"]`);
    if (!translateWrapper) return;
    
    // First G (Parent) is the scale wrapper
    const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useElementId}"]`);
    // Use element is the rotate target
    const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
    
    if (!scaleWrapper || !cloneUseElement) return;
    
    // 3. Get the *base* transform values from the clone's <use> (ov)
    const cloneBaseTransformString = cloneUseElement.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)'; 
    const currentBaseTransforms = parseTransform(cloneBaseTransformString);
    
    const currentBaseTranslate = currentBaseTransforms.translate;
    const currentBaseScale = currentBaseTransforms.scale;
    const currentBaseRotate = currentBaseTransforms.rotate;

    // 4. Collect all relevant animateTransform tags
    const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
    const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
    const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
    
    const allAnimates = [...animatesRotate, ...animatesScale, ...animatesTranslate];
    
    // Calculate new duration (starts from 0s, adds 1s)
    const currentDur = parseInt((animatesRotate[0]?.getAttribute('dur') || '0s').replace('s', '')) || 0;
    const newDur = currentDur + 1;

    // Determine repeat/freeze state
    const isRepeat = document.querySelector('.etaniSettingRepeat.active');
    
    // Calculate scale value first to use in translate calculation
    const scaleValue = calculateAdditiveValue('scale', originalTransforms.scale, currentBaseScale);
    
    allAnimates.forEach(animate => {
        const type = animate.getAttribute('type').toLowerCase();
        let currentValueString = animate.getAttribute('values') || '';
        let newValue = '';
        
        // Calculate the *additive* difference in specified order: rotate, scale, translate
        if (type === 'rotate') {
            newValue = calculateAdditiveValue('rotate', originalTransforms.rotate, currentBaseRotate);
        } else if (type === 'scale') {
            newValue = scaleValue;
        } else if (type === 'translate') {
            newValue = calculateAdditiveValue('translate', originalTransforms.translate, currentBaseTranslate, scaleValue);
        }
        
        // Append the new value
        animate.setAttribute('values', (currentValueString ? currentValueString + ';' : '') + newValue);

        // Update duration
        if (newDur > 0) {
            animate.setAttribute('dur', `${newDur}s`);
            // Set repeat/freeze state
            if (isRepeat) {
                animate.removeAttribute('fill');
                animate.setAttribute('repeatCount', 'indefinite');
            } else {
                animate.removeAttribute('repeatCount');
                animate.setAttribute('fill', 'freeze');
            }
        } else {
            animate.removeAttribute('dur');
            animate.removeAttribute('fill');
            animate.removeAttribute('repeatCount');
        }
    });

    // 5. Add a new .etaniAVItem in .etaniItemRight and update UI duration
    const etaniItemRight = e.currentTarget.closest('.etaniItemRight');
    const etaniAV = etaniItemRight.querySelector('.etaniAV');
    if (etaniAV) {
        const newAVItem = document.createElement('span');
        newAVItem.className = 'etaniAVItem';
        newAVItem.addEventListener('click', handleAVItemClick); // Bind deletion handler
        etaniAV.appendChild(newAVItem);
    }
    etaniItemRight.querySelector('.etaniAnimateDur').textContent = `dur: ${newDur}s`;

    // 6. Update the SVG result
    updateEtaniResult();
}

/**
 * Handles the click event for the '-' button.
 * Toggles deletion mode, allowing for continuous deletion (Requirement 1).
 * @param {Event} e The click event.
 */
function handleAVDeleteToggle(e) {
    e.preventDefault();
    const deleteButton = e.currentTarget;
    const isDeleting = deleteButton.classList.toggle('deleting');
    const etaniItemRight = deleteButton.closest('.etaniItemRight');
    const etaniCol = deleteButton.closest('.etaniCol');
    
    const avItems = etaniItemRight.querySelectorAll('.etaniAVItem');
    
    // Toggle visual state of all items (except the first one)
    avItems.forEach((item, index) => {
        if (index > 0) {
            // Apply deleting-target class only if deleting mode is active
            item.classList.toggle('deleting-target', isDeleting);
        }
    });

    // Toggle deleting state for this item's column
    etaniCol.classList.toggle('deleting-mode', isDeleting);

    // Disable all other AVDelete buttons if entering deleting mode
    document.querySelectorAll('.etaniAVDelete').forEach(btn => {
        if (btn !== deleteButton) {
            btn.closest('.etaniAVCtrl').disabled = isDeleting;
            btn.style.opacity = isDeleting ? 0.5 : 1; // Visual feedback for disabled state
        }
    });

    if (isDeleting) {
        deleteButton.title = "Click value item to delete (Click again to cancel)";
        deleteButton.style.color = 'white'; // White icon for active state
    } else {
        deleteButton.title = "Delete Value";
        deleteButton.style.color = 'currentColor'; // Reset to default
        
        // Remove visual state from all items when cancelling
        avItems.forEach(item => {
            item.classList.remove('deleting-target');
        });
    }
}

/**
 * Handles the click event for an .etaniAVItem in deletion mode.
 * @param {Event} e The click event.
 */
function handleAVItemClick(e) {
    const itemToDelete = e.currentTarget;
    const itemIndex = Array.from(itemToDelete.parentNode.children).indexOf(itemToDelete);
    const etaniItemRight = itemToDelete.closest('.etaniItemRight');
    
    // Check if deletion mode is active and it's not the first item
    if (etaniItemRight.closest('.etaniCol').classList.contains('deleting-mode') && itemIndex > 0) {
        
        const deleteButton = etaniItemRight.querySelector('.etaniAVDelete');
        const useId = etaniItemRight.closest('.etaniItem').querySelector('.tileid').textContent;
        
        // 1. Find the corresponding elements in etani_clone
        const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useId}"]`);
        if (!translateWrapper) return;
        
        const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useId}"]`);
        const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
        if (!scaleWrapper || !cloneUseElement) return;

        // 2. Collect all relevant animateTransform tags
        const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
        const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
        const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
        
        const allAnimates = [...animatesTranslate, ...animatesScale, ...animatesRotate];
        
        // Calculate new duration (starts from 0s)
        const currentDur = parseInt((animatesRotate[0].getAttribute('dur') || '0s').replace('s', '')) || 0;
        const newDur = Math.max(0, currentDur - 1); // Duration cannot be less than 0
        
        allAnimates.forEach(animate => {
            const values = animate.getAttribute('values').split(';');
            
            // Remove the value at the target index (itemIndex)
            if (itemIndex < values.length) {
                values.splice(itemIndex, 1);
            }
            animate.setAttribute('values', values.join(';'));
            
            // Update duration
            if (newDur > 0) {
                animate.setAttribute('dur', `${newDur}s`);
            } else {
                animate.removeAttribute('dur');
                animate.removeAttribute('fill');
                animate.removeAttribute('repeatCount');
            }
        });

        // 3. Remove the item from the UI
        itemToDelete.remove();

        // 4. Update UI duration display
        etaniItemRight.querySelector('.etaniAnimateDur').textContent = `dur: ${newDur}s`;
        
        // 5. Update visual state of remaining items in deletion mode
        const remainingItems = etaniItemRight.querySelectorAll('.etaniAVItem');
        
        // If no values remain (only the base state item[0]), exit deletion mode automatically
        if (remainingItems.length === 1) {
            deleteButton.click(); // Programmatically click to toggle out of deleting mode
        } else {
            // Otherwise, keep deletion mode active but update visual state
            remainingItems.forEach((item, index) => {
                 item.classList.toggle('deleting-target', index > 0); 
            });
        }

        // 6. Update the SVG result
        updateEtaniResult();
        
    } else if (itemIndex === 0 && etaniItemRight.closest('.etaniCol').classList.contains('deleting-mode')) {
        alert("The first value cannot be deleted.");
    }
}

/**
 * Handles the click event for the 'Transform' button.
 * Adds the required <g> wrappers and animateTransform tags.
 */
function handleAllAppendTransformClick() {
    if (!etani_clone) return;

    const etdropUses = document.querySelectorAll('#etmain .etdrop use'); // Select original <use> elements for their href
    const etaniItemRights = document.querySelectorAll('.etaniItemRight');

    // Determine repeat/freeze state
    const isRepeat = document.querySelector('.etaniSettingRepeat.active');

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

        // Check if transform controls already exist to prevent duplication
        if (itemRight.querySelector('.etaniAnimate')) return; 

        // 1. Find the corresponding <use> element in etani_clone
        let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;

        // 2. Define the base animate function
        const baseAnimate = (type, initialValue) => {
            const animate = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
            animate.setAttribute('attributeName', 'transform');
            animate.setAttribute('attributeType', 'XML');
            animate.setAttribute('type', type);
            // Initial value is the base state's additive difference, which is always 0 or 1 for the first point
            animate.setAttribute('values', initialValue); 
            
            // dur="0s" is not set, only added when > 0
            
            animate.setAttribute('fill', isRepeat ? 'remove' : 'freeze');
            if (isRepeat) animate.setAttribute('repeatCount', 'indefinite');

            animate.setAttribute('additive', 'sum'); // Important for relative transforms
            return animate;
        };
        
        // --- Constructing the nested G structure ---
        
        // a. Add animateTransform[type=rotate][value=0] to <use>
        cloneUseElement.appendChild(baseAnimate('rotate', '0'));
        
        // b. Create first G (Scale Wrapper) and add animateTransform[type=scale][value=1]
        const scaleWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        scaleWrapper.setAttribute('data-use-href-scale', `#${useId}`); // Use a specific data attribute for scale wrapper
        scaleWrapper.appendChild(baseAnimate('scale', '1'));
        
        // c. Create second G (Translate Wrapper) and add animateTransform[type=translate][value=0,0]
        const translateWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        translateWrapper.setAttribute('data-use-href', `#${useId}`); // Main ID link
        translateWrapper.appendChild(baseAnimate('translate', '0,0'));
        
        // d. Perform the wrapping: (G_Translate) -> (G_Scale) -> (USE + Rotate_Animate)
        
        // i. Insert G_Translate before the original <use>
        cloneUseElement.parentNode.insertBefore(translateWrapper, cloneUseElement);
        
        // ii. Wrap <use> with G_Scale
        cloneUseElement.parentNode.insertBefore(scaleWrapper, cloneUseElement);
        scaleWrapper.appendChild(cloneUseElement);
        
        // iii. Wrap G_Scale with G_Translate
        translateWrapper.appendChild(scaleWrapper);
        

        // --- UI Control Creation ---

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

        // Animate Name (Dark grey background, white text)
        const nameSpan = document.createElement('span');
        nameSpan.className = 'etaniAnimateName';
        nameSpan.textContent = 'transform';
        etaniAnimate.appendChild(nameSpan);

        // Animate Dur (dur: n)
        const durSpan = document.createElement('span');
        durSpan.className = 'etaniAnimateDur';
        durSpan.textContent = 'dur: 0s'; // Initial value 0s
        etaniAnimate.appendChild(durSpan);

        // Animate Value container
        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        // New container for buttons
        const avCtrlDiv = document.createElement('div');
        avCtrlDiv.className = 'etaniAVCtrl';

        // AV Add (Plus sign SVG)
        const avAddSpan = document.createElement('span');
        avAddSpan.className = 'etaniAVAdd';
        avAddSpan.title = 'Add Transform Value';
        avAddSpan.addEventListener('click', (e) => handleAVAddClick(e, useId));
        
        const plusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        plusSvg.setAttribute('width', '24');
        plusSvg.setAttribute('height', '24');
        plusSvg.setAttribute('viewBox', '0 0 24 24');
        plusSvg.setAttribute('fill', 'none');
        plusSvg.setAttribute('stroke', 'currentColor');
        plusSvg.setAttribute('stroke-width', '1');
        plusSvg.setAttribute('stroke-linecap', 'round');
        plusSvg.setAttribute('stroke-linejoin', 'round');
        plusSvg.innerHTML = '<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>';
        avAddSpan.appendChild(plusSvg);

        // AV Delete (Minus sign SVG)
        const avDeleteSpan = document.createElement('span');
        avDeleteSpan.className = 'etaniAVDelete';
        avDeleteSpan.title = 'Delete Value';
        avDeleteSpan.addEventListener('click', handleAVDeleteToggle);

        const minusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        minusSvg.setAttribute('width', '24');
        minusSvg.setAttribute('height', '24');
        minusSvg.setAttribute('viewBox', '0 0 24 24');
        minusSvg.setAttribute('stroke', 'currentColor');
        minusSvg.setAttribute('stroke-width', '1');
        minusSvg.setAttribute('stroke-linecap', 'round');
        minusSvg.setAttribute('stroke-linejoin', 'round');
        minusSvg.innerHTML = '<line x1="5" y1="12" x2="19" y2="12"></line>';
        avDeleteSpan.appendChild(minusSvg);
        
        avCtrlDiv.appendChild(avAddSpan);
        avCtrlDiv.appendChild(avDeleteSpan);

        // AV Label
        const avLabelSpan = document.createElement('span');
        avLabelSpan.className = 'etaniAVLabel';
        avLabelSpan.textContent = 'values : ';

        // AV Container
        const avDiv = document.createElement('div');
        avDiv.className = 'etaniAV';
        
        // AV Item (Initially contains one)
        const avItemSpan = document.createElement('span');
        avItemSpan.className = 'etaniAVItem';
        avItemSpan.addEventListener('click', handleAVItemClick); // Bind deletion handler
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avCtrlDiv);
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

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

    // 7. Update the SVG result immediately
    updateEtaniResult();
}

/**
 * Handles the Setting Mode switch (Freeze/Repeat).
 * @param {string} mode The animation end mode ('freeze' or 'repeat').
 */
function handleSettingModeChange(mode) {
    if (!etani_clone) return;

    // We select all animateTransform tags
    const animates = etani_clone.querySelectorAll('animateTransform');
    const isRepeat = mode === 'repeat';

    animates.forEach(animate => {
        // Only modify if dur is present (i.e., dur > 0s)
        if (animate.hasAttribute('dur')) {
            if (isRepeat) {
                animate.removeAttribute('fill');
                animate.setAttribute('repeatCount', 'indefinite');
            } else {
                animate.removeAttribute('repeatCount');
                animate.setAttribute('fill', 'freeze');
            }
        }
    });

    // Update active state
    document.querySelector('.etaniSettingFreeze').classList.toggle('active', !isRepeat);
    document.querySelector('.etaniSettingRepeat').classList.toggle('active', isRepeat);

    updateEtaniResult();
}

// --- MAIN STRUCTURE CREATION ---

/**
 * Creates the internal animation control structure.
 * @param {HTMLElement} etaniouter The outer container element.
 */
function createEtaniInner(etaniouter) {
    // 5.1, 5.2, 5.3 Clone, remove .etwait, update ID
    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etani_clone = originalSvg.cloneNode(true);
    const etwaitElement = etani_clone.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    etani_clone.id = 'etmainani';

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

    // Create div.etaniCtrl
    const etaniCtrl = document.createElement('div');
    etaniCtrl.className = 'etaniCtrl';
    
    // Content and Center link
    const etaniContent = document.createElement('div');
    etaniContent.className = 'etaniContent';
    
    const contentHTMLLink = document.createElement('a');
    contentHTMLLink.className = 'etaniContentHTML';
    contentHTMLLink.textContent = 'Show HTML';
    contentHTMLLink.href = '#';
    contentHTMLLink.addEventListener('click', handleContentHTMLClick);
    etaniContent.appendChild(contentHTMLLink);

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

    // Setting Mode (Radio Buttons)
    const etaniSetting = document.createElement('div');
    etaniSetting.className = 'etaniSetting';
    
    const freezeRadio = document.createElement('span');
    freezeRadio.className = 'etaniSettingMode etaniSettingFreeze active';
    freezeRadio.textContent = 'Freeze';
    freezeRadio.setAttribute('data-mode', 'freeze');
    freezeRadio.addEventListener('click', () => handleSettingModeChange('freeze'));
    
    const repeatRadio = document.createElement('span');
    repeatRadio.className = 'etaniSettingMode etaniSettingRepeat';
    repeatRadio.textContent = 'Repeat';
    repeatRadio.setAttribute('data-mode', 'repeat');
    repeatRadio.addEventListener('click', () => handleSettingModeChange('repeat'));

    etaniSetting.appendChild(freezeRadio);
    etaniSetting.appendChild(repeatRadio);
    etaniCtrl.appendChild(etaniSetting);

    // All Append (Buttons)
    const etaniAllAppend = document.createElement('div');
    etaniAllAppend.className = 'etaniAllAppend';

    const transformButton = document.createElement('button');
    transformButton.className = 'etaniAllAppendTransform';
    transformButton.textContent = 'transform';
    transformButton.addEventListener('click', handleAllAppendTransformClick);
    
    const opacityButton = document.createElement('button');
    opacityButton.className = 'etaniAllAppendOpacity';
    opacityButton.textContent = 'opacity';
    // opacityButton.addEventListener('click', handleAllAppendOpacityClick); // Placeholder
    
    etaniAllAppend.appendChild(transformButton);
    etaniAllAppend.appendChild(opacityButton);
    etaniCtrl.appendChild(etaniAllAppend);
    
    // Create div.etaniCol (Tile list)
    const etaniCol = document.createElement('div');
    etaniCol.className = 'etaniCol';

    // Create div.etaniResult
    const etaniResult = document.createElement('div');
    etaniResult.className = 'etaniResult';
    
    // Result elements (img, download, rename, size) 
    const resultImg = document.createElement('img');
    resultImg.className = 'etaniResultImage';
    resultImg.alt = 'Rendered Ejtile Animation SVG';

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

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

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

    resultDRDiv.appendChild(downloadLink);
    resultDRDiv.appendChild(renameLink);
    
    etaniResult.appendChild(resultImg);
    etaniResult.appendChild(resultDRDiv);
    etaniResult.appendChild(sizeSpan);
    
    // Append children to etaniinner
    etaniinner.appendChild(etaniCtrl);
    etaniinner.appendChild(etaniCol);
    etaniinner.appendChild(etaniResult);

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

    // III, IV, V, VI, VII. Populate etaniCol (Tile list)
    const etdropUses = document.querySelectorAll('.etdrop use');
    const etwaitGroups = document.querySelectorAll('.etwait g');

    etdropUses.forEach((useElement, i) => {
        // (Tile processing logic - Unchanged)
        const tileid = useElement.getAttribute('href').substring(1);
        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 originalTile = document.querySelector(`defs g#${tileid}`);
        
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';

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

        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        
        if (originalTile) {
            const tileclone = originalTile.cloneNode(true);
            tileclone.removeAttribute('id');
            if (etwaittransform) tileclone.setAttribute('transform', etwaittransform);
            if (etwaitfill) tileclone.setAttribute('fill', etwaitfill);
            if (etwaitstroke) tileclone.setAttribute('stroke', etwaitstroke);
            if (etwaitstrokeWidth) tileclone.setAttribute('stroke-width', etwaitstrokeWidth);
            
            const svgWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            svgWrapper.setAttribute('width', '40');
            svgWrapper.setAttribute('height', '40');
            svgWrapper.setAttribute('version', '1.1');
            svgWrapper.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
            svgWrapper.setAttribute('xmlns:svg', 'http://www.w3.org/2000/svg');
            svgWrapper.className = 'etanitileimg';
            
            svgWrapper.appendChild(tileclone);
            etaniItemLeft.appendChild(svgWrapper);
        }

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

        etaniItem.appendChild(etaniItemLeft);
        etaniItem.appendChild(etaniItemRight);
        etaniCol.appendChild(etaniItem);
    });
    
    // 5. Update the result section immediately
    updateEtaniResult();
}

// --- INITIALIZATION ---

/**
 * Toggles the visibility and content of the animation control panel.
 * @param {Event} event The click event.
 */
function toggleAnimation(event) {
    const etanibutton = event.currentTarget;
    const etaniouter = etanibutton.parentNode;
    const etaniinner = document.getElementById('etaniinner');

    if (etanibutton.textContent === 'Animate it') {
        etanibutton.textContent = 'Close Ejtile Ani';
        createEtaniInner(etaniouter);
        
    } else if (etanibutton.textContent === 'Close Ejtile Ani') {
        etanibutton.textContent = 'Animate it';
        if (etaniinner) {
            etaniinner.remove();
        }
        etani_clone = null;
    }
}

window.addEventListener('load', () => {
    addDynamicStyles();

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

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

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

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

帖子 ejsoon »

https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3807
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 157 次
被圈友点赞: 188 次
联系:

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

帖子 ejsoon »

增加opacity以及fill up values。

代码: 全选

改動:

一,當點擊etaniAllAppendOpacity時,就要往etani_clone中的.etdrop中的每一個<use>內插入一個動畫標籤:

<animate attributeName="opacity" values="1"/>

fill或repeatCount屬性根據當前的etaniSettingMode增加。

同時在etaniItemRight內也加上一個etaniAnimate,name為opacity。

二,etaniAVItem也要顯示內容,如果name是transform,則顯示的是「abcd…」遞增的小寫字母。假設超過二十六個,那就接上大寫字母。當按下etaniAVAdd時,它會根據最後一個的字母,生成下一個字母。

如果name是opacity,則顯示的是value。

三,在etaniCtrl內部的最後增加div.etaniValueCtrl,它裡面有a.etaniValueCtrlUp{fill up values}。當按下時,將找到所有的動畫(包括transform和opacity以及其它類型)的values最多的數量,則values達不到這個數量的動畫,將用其最後的值填充。

代碼中的注釋都要全部是英文。增加元素的方式只能是createElement和appendChild。

代码: 全选

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

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

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

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

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

        /* Styles for etaniResult */
        .etaniResult {
            text-align: center;
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #bbb;
            overflow: auto;
        }
        
        /* Control and button styles */
        .etaniContent a, .etaniValueCtrl a {
            display: inline-block;
            margin: 0 5px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 14px;
        }
        
        .etaniCenter {
            border: 1px solid green;
            color: green;
        }
        
        .etaniContentHTML {
            border: 1px solid #0099ff;
            color: #0099ff;
            margin-right: 15px;
        }

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

        /* Styles for etaniAllAppend buttons */
        .etaniAllAppend button {
            padding: 5px 10px;
            font-size: 16px;
            margin: 0 5px;
            cursor: pointer;
            border: 1px solid #333;
            background-color: #fff;
        }
        
        /* Custom radio button styles */
        .etaniSettingMode {
            display: inline-block;
            cursor: pointer;
            padding: 4px 8px;
            margin: 0 3px;
            font-size: 14px;
            border: 1px solid #888;
            background-color: #eee;
            color: #333;
            user-select: none;
        }

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

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

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

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

        .etaniResultImage {
            display: block;
            max-width: 480px;
            width: 100%;
            height: auto;
            margin: 0 auto 10px auto;
            border: 1px solid #000;
        }
        
        .etaniResultSize {
            display: inline-block;
            margin-left: 10px;
            font-size: 12px; 
            color: #555;
        }
        
        /* etaniItem structure */
        .etaniItem {
            min-height: 48px;
            border: 1px solid #ccc;
            box-sizing: border-box;
            width: 100%;
            margin-bottom: -1px;
            overflow: auto;
        }
        
        .etaniItemLeft {
            float: left; 
            width: 60px;
            min-height: 48px;
            border-right: 1px solid #ccc;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            background-color: #e8e8e8;
            padding: 2px 0;
        }
        
        .etaniItemRight {
            margin-left: 60px; 
            padding: 7px;
            min-height: 48px; 
            background-color: #fff;
        }

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

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

        .etaniAnimateDur {
            display: inline-block;
            margin-right: 10px;
            font-size: 14px;
        }

        .etaniAnimateValue {
            margin-top: 5px;
            overflow: auto;
        }

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

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

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

        .etaniAVDelete.deleting {
            background-color: #cc3333;
        }
        
        .etaniAVLabel {
            font-size: 14px;
            margin-right: 5px;
        }

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

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

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

    document.head.appendChild(styleSheet);
}

// Generate the next letter for transform etaniAVItem labels
function getNextLetter(currentLetter) {
    if (!currentLetter) return 'a';
    const code = currentLetter.charCodeAt(0);
    if (code >= 97 && code < 122) { // lowercase a-y
        return String.fromCharCode(code + 1);
    } else if (code === 122) { // z -> A
        return 'A';
    } else if (code >= 65 && code < 90) { // uppercase A-Y
        return String.fromCharCode(code + 1);
    } else if (code === 90) { // Z -> a (loop back)
        return 'a';
    }
    return 'a';
}

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

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

    const transform = {};

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

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

    return transform;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

    const originalUseElement = document.querySelector(`.etdrop use[href="#${useElementId}"]`);
    if (!originalUseElement) return;

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

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

        const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useElementId}"]`);
        if (!translateWrapper) return;

        const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useElementId}"]`);
        const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
        
        if (!scaleWrapper || !cloneUseElement) return;

        const cloneBaseTransformString = cloneUseElement.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)'; 
        const currentBaseTransforms = parseTransform(cloneBaseTransformString);
        
        const currentBaseTranslate = currentBaseTransforms.translate;
        const currentBaseScale = currentBaseTransforms.scale;
        const currentBaseRotate = currentBaseTransforms.rotate;

        const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
        const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
        const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
        
        const allAnimates = [...animatesRotate, ...animatesScale, ...animatesTranslate];
        
        const currentDur = parseInt((animatesRotate[0]?.getAttribute('dur') || '0s').replace('s', '')) || 0;
        const newDur = currentDur + 1;

        const isRepeat = document.querySelector('.etaniSettingRepeat.active');
        
        const scaleValue = calculateAdditiveValue('scale', originalTransforms.scale, currentBaseScale);
        
        allAnimates.forEach(animate => {
            const type = animate.getAttribute('type').toLowerCase();
            let currentValueString = animate.getAttribute('values') || '';
            let newValue = '';
            
            if (type === 'rotate') {
                newValue = calculateAdditiveValue('rotate', originalTransforms.rotate, currentBaseRotate);
            } else if (type === 'scale') {
                newValue = scaleValue;
            } else if (type === 'translate') {
                newValue = calculateAdditiveValue('translate', originalTransforms.translate, currentBaseTranslate, scaleValue);
            }
            
            animate.setAttribute('values', (currentValueString ? currentValueString + ';' : '') + newValue);

            if (newDur > 0) {
                animate.setAttribute('dur', `${newDur}s`);
                if (isRepeat) {
                    animate.removeAttribute('fill');
                    animate.setAttribute('repeatCount', 'indefinite');
                } else {
                    animate.removeAttribute('repeatCount');
                    animate.setAttribute('fill', 'freeze');
                }
            } else {
                animate.removeAttribute('dur');
                animate.removeAttribute('fill');
                animate.removeAttribute('repeatCount');
            }
        });

        etaniItemRight.querySelector(`.etaniAnimate[data-type="transform"] .etaniAnimateDur`).textContent = `dur: ${newDur}s`;
    } else if (animateType === 'opacity') {
        const currentOpacity = originalUseElement.getAttribute('opacity') || '1';
        const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useElementId}"]`);
        if (!cloneUseElement) return;

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

        const currentValueString = animateOpacity.getAttribute('values') || '';
        const newValue = calculateAdditiveValue('opacity', currentOpacity, '1');
        animateOpacity.setAttribute('values', (currentValueString ? currentValueString + ';' : '') + newValue);

        const currentDur = parseInt((animateOpacity.getAttribute('dur') || '0s').replace('s', '')) || 0;
        const newDur = currentDur + 1;
        const isRepeat = document.querySelector('.etaniSettingRepeat.active');

        if (newDur > 0) {
            animateOpacity.setAttribute('dur', `${newDur}s`);
            if (isRepeat) {
                animateOpacity.removeAttribute('fill');
                animateOpacity.setAttribute('repeatCount', 'indefinite');
            } else {
                animateOpacity.removeAttribute('repeatCount');
                animate.setAttribute('fill', 'freeze');
            }
        } else {
            animateOpacity.removeAttribute('dur');
            animateOpacity.removeAttribute('fill');
            animateOpacity.removeAttribute('repeatCount');
        }

        etaniItemRight.querySelector(`.etaniAnimate[data-type="opacity"] .etaniAnimateDur`).textContent = `dur: ${newDur}s`;
    }

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

        const existingItems = etaniAV.querySelectorAll('.etaniAVItem');
        if (animateType === 'transform') {
            const lastItem = existingItems[existingItems.length - 1];
            const lastLetter = lastItem ? lastItem.textContent : '';
            newAVItem.textContent = getNextLetter(lastLetter);
        } else if (animateType === 'opacity') {
            newAVItem.textContent = originalUseElement.getAttribute('opacity') || '1';
        }

        etaniAV.appendChild(newAVItem);
    }

    updateEtaniResult();
}

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

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

    document.querySelectorAll(`.etaniAVDelete[data-type="${animateType}"]`).forEach(btn => {
        if (btn !== deleteButton) {
            btn.closest('.etaniAVCtrl').disabled = isDeleting;
            btn.style.opacity = isDeleting ? 0.5 : 1;
        }
    });

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

// Handle click event for an .etaniAVItem in deletion mode
function handleAVItemClick(e, animateType = 'transform') {
    const itemToDelete = e.currentTarget;
    const itemIndex = Array.from(itemToDelete.parentNode.children).indexOf(itemToDelete);
    const etaniItemRight = itemToDelete.closest('.etaniItemRight');
    
    if (etaniItemRight.closest('.etaniCol').classList.contains(`deleting-mode-${animateType}`) && itemIndex > 0) {
        
        const deleteButton = etaniItemRight.querySelector(`.etaniAVDelete[data-type="${animateType}"]`);
        const useId = etaniItemRight.closest('.etaniItem').querySelector('.tileid').textContent;
        
        if (animateType === 'transform') {
            const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useId}"]`);
            if (!translateWrapper) return;
            
            const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useId}"]`);
            const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
            if (!scaleWrapper || !cloneUseElement) return;

            const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
            const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
            const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
            
            const allAnimates = [...animatesTranslate, ...animatesScale, ...animatesRotate];
            
            const currentDur = parseInt((animatesRotate[0].getAttribute('dur') || '0s').replace('s', '')) || 0;
            const newDur = Math.max(0, currentDur - 1);
            
            allAnimates.forEach(animate => {
                const values = animate.getAttribute('values').split(';');
                
                if (itemIndex < values.length) {
                    values.splice(itemIndex, 1);
                }
                animate.setAttribute('values', values.join(';'));
                
                if (newDur > 0) {
                    animate.setAttribute('dur', `${newDur}s`);
                } else {
                    animate.removeAttribute('dur');
                    animate.removeAttribute('fill');
                    animate.removeAttribute('repeatCount');
                }
            });

            etaniItemRight.querySelector(`.etaniAnimate[data-type="transform"] .etaniAnimateDur`).textContent = `dur: ${newDur}s`;
        } else if (animateType === 'opacity') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return;

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

            const values = animateOpacity.getAttribute('values').split(';');
            if (itemIndex < values.length) {
                values.splice(itemIndex, 1);
            }
            animateOpacity.setAttribute('values', values.join(';'));

            const currentDur = parseInt((animateOpacity.getAttribute('dur') || '0s').replace('s', '')) || 0;
            const newDur = Math.max(0, currentDur - 1);

            if (newDur > 0) {
                animateOpacity.setAttribute('dur', `${newDur}s`);
            } else {
                animateOpacity.removeAttribute('dur');
                animateOpacity.removeAttribute('fill');
                animateOpacity.removeAttribute('repeatCount');
            }

            etaniItemRight.querySelector(`.etaniAnimate[data-type="opacity"] .etaniAnimateDur`).textContent = `dur: ${newDur}s`;
        }

        itemToDelete.remove();

        const remainingItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
        
        if (remainingItems.length === 1) {
            deleteButton.click();
        } else {
            remainingItems.forEach((item, index) => {
                item.classList.toggle('deleting-target', index > 0); 
            });
        }

        // Re-label transform items to maintain sequential letters
        if (animateType === 'transform') {
            const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="transform"] .etaniAVItem`);
            let currentLetter = '';
            avItems.forEach((item, index) => {
                currentLetter = index === 0 ? 'a' : getNextLetter(currentLetter);
                item.textContent = currentLetter;
            });
        }

        updateEtaniResult();
        
    } else if (itemIndex === 0 && etaniItemRight.closest('.etaniCol').classList.contains(`deleting-mode-${animateType}`)) {
        alert("The first value cannot be deleted.");
    }
}

// Handle click event for the 'Transform' button
function handleAllAppendTransformClick() {
    if (!etani_clone) return;

    const etdropUses = document.querySelectorAll('#etmain .etdrop use');
    const etaniItemRights = document.querySelectorAll('.etaniItemRight');

    const isRepeat = document.querySelector('.etaniSettingRepeat.active');

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

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

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

        const baseAnimate = (type, initialValue) => {
            const animate = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
            animate.setAttribute('attributeName', 'transform');
            animate.setAttribute('attributeType', 'XML');
            animate.setAttribute('type', type);
            animate.setAttribute('values', initialValue);
            animate.setAttribute('fill', isRepeat ? 'remove' : 'freeze');
            if (isRepeat) animate.setAttribute('repeatCount', 'indefinite');
            animate.setAttribute('additive', 'sum');
            return animate;
        };
        
        cloneUseElement.appendChild(baseAnimate('rotate', '0'));
        
        const scaleWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        scaleWrapper.setAttribute('data-use-href-scale', `#${useId}`);
        scaleWrapper.appendChild(baseAnimate('scale', '1'));
        
        const translateWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        translateWrapper.setAttribute('data-use-href', `#${useId}`);
        translateWrapper.appendChild(baseAnimate('translate', '0,0'));
        
        cloneUseElement.parentNode.insertBefore(translateWrapper, cloneUseElement);
        cloneUseElement.parentNode.insertBefore(scaleWrapper, cloneUseElement);
        scaleWrapper.appendChild(cloneUseElement);
        translateWrapper.appendChild(scaleWrapper);

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

        const nameSpan = document.createElement('span');
        nameSpan.className = 'etaniAnimateName';
        nameSpan.textContent = 'transform';
        etaniAnimate.appendChild(nameSpan);

        const durSpan = document.createElement('span');
        durSpan.className = 'etaniAnimateDur';
        durSpan.textContent = 'dur: 0s';
        etaniAnimate.appendChild(durSpan);

        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        const avCtrlDiv = document.createElement('div');
        avCtrlDiv.className = 'etaniAVCtrl';

        const avAddSpan = document.createElement('span');
        avAddSpan.className = 'etaniAVAdd';
        avAddSpan.title = 'Add Transform Value';
        avAddSpan.setAttribute('data-type', 'transform');
        avAddSpan.addEventListener('click', (e) => handleAVAddClick(e, useId, 'transform'));
        
        const plusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        plusSvg.setAttribute('width', '24');
        plusSvg.setAttribute('height', '24');
        plusSvg.setAttribute('viewBox', '0 0 24 24');
        plusSvg.setAttribute('fill', 'none');
        plusSvg.setAttribute('stroke', 'currentColor');
        plusSvg.setAttribute('stroke-width', '1');
        plusSvg.setAttribute('stroke-linecap', 'round');
        plusSvg.setAttribute('stroke-linejoin', 'round');
        plusSvg.innerHTML = '<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>';
        avAddSpan.appendChild(plusSvg);

        const avDeleteSpan = document.createElement('span');
        avDeleteSpan.className = 'etaniAVDelete';
        avDeleteSpan.title = 'Delete Value';
        avDeleteSpan.setAttribute('data-type', 'transform');
        avDeleteSpan.addEventListener('click', (e) => handleAVDeleteToggle(e, 'transform'));

        const minusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        minusSvg.setAttribute('width', '24');
        minusSvg.setAttribute('height', '24');
        minusSvg.setAttribute('viewBox', '0 0 24 24');
        minusSvg.setAttribute('fill', 'none');
        minusSvg.setAttribute('stroke', 'currentColor');
        minusSvg.setAttribute('stroke-width', '1');
        minusSvg.setAttribute('stroke-linecap', 'round');
        minusSvg.setAttribute('stroke-linejoin', 'round');
        minusSvg.innerHTML = '<line x1="5" y1="12" x2="19" y2="12"></line>';
        avDeleteSpan.appendChild(minusSvg);
        
        avCtrlDiv.appendChild(avAddSpan);
        avCtrlDiv.appendChild(avDeleteSpan);

        const avLabelSpan = document.createElement('span');
        avLabelSpan.className = 'etaniAVLabel';
        avLabelSpan.textContent = 'values : ';

        const avDiv = document.createElement('div');
        avDiv.className = 'etaniAV';
        
        const avItemSpan = document.createElement('span');
        avItemSpan.className = 'etaniAVItem';
        avItemSpan.textContent = 'a';
        avItemSpan.addEventListener('click', (e) => handleAVItemClick(e, 'transform'));
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avCtrlDiv);
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

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

    updateEtaniResult();
}

// Handle click event for the 'Opacity' button
function handleAllAppendOpacityClick() {
    if (!etani_clone) return;

    const etdropUses = document.querySelectorAll('#etmain .etdrop use');
    const etaniItemRights = document.querySelectorAll('.etaniItemRight');

    const isRepeat = document.querySelector('.etaniSettingRepeat.active');

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

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

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

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

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

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

        const durSpan = document.createElement('span');
        durSpan.className = 'etaniAnimateDur';
        durSpan.textContent = 'dur: 0s';
        etaniAnimate.appendChild(durSpan);

        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        const avCtrlDiv = document.createElement('div');
        avCtrlDiv.className = 'etaniAVCtrl';

        const avAddSpan = document.createElement('span');
        avAddSpan.className = 'etaniAVAdd';
        avAddSpan.title = 'Add Opacity Value';
        avAddSpan.setAttribute('data-type', 'opacity');
        avAddSpan.addEventListener('click', (e) => handleAVAddClick(e, useId, 'opacity'));
        
        const plusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        plusSvg.setAttribute('width', '24');
        plusSvg.setAttribute('height', '24');
        plusSvg.setAttribute('viewBox', '0 0 24 24');
        plusSvg.setAttribute('fill', 'none');
        plusSvg.setAttribute('stroke', 'currentColor');
        plusSvg.setAttribute('stroke-width', '1');
        plusSvg.setAttribute('stroke-linecap', 'round');
        plusSvg.setAttribute('stroke-linejoin', 'round');
        plusSvg.innerHTML = '<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>';
        avAddSpan.appendChild(plusSvg);

        const avDeleteSpan = document.createElement('span');
        avDeleteSpan.className = 'etaniAVDelete';
        avDeleteSpan.title = 'Delete Value';
        avDeleteSpan.setAttribute('data-type', 'opacity');
        avDeleteSpan.addEventListener('click', (e) => handleAVDeleteToggle(e, 'opacity'));

        const minusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        minusSvg.setAttribute('width', '24');
        minusSvg.setAttribute('height', '24');
        minusSvg.setAttribute('viewBox', '0 0 24 24');
        minusSvg.setAttribute('fill', 'none');
        minusSvg.setAttribute('stroke', 'currentColor');
        minusSvg.setAttribute('stroke-width', '1');
        minusSvg.setAttribute('stroke-linecap', 'round');
        minusSvg.setAttribute('stroke-linejoin', 'round');
        minusSvg.innerHTML = '<line x1="5" y1="12" x2="19" y2="12"></line>';
        avDeleteSpan.appendChild(minusSvg);
        
        avCtrlDiv.appendChild(avAddSpan);
        avCtrlDiv.appendChild(avDeleteSpan);

        const avLabelSpan = document.createElement('span');
        avLabelSpan.className = 'etaniAVLabel';
        avLabelSpan.textContent = 'values : ';

        const avDiv = document.createElement('div');
        avDiv.className = 'etaniAV';
        
        const avItemSpan = document.createElement('span');
        avItemSpan.className = 'etaniAVItem';
        avItemSpan.textContent = '1';
        avItemSpan.addEventListener('click', (e) => handleAVItemClick(e, 'opacity'));
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avCtrlDiv);
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

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

    updateEtaniResult();
}

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

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

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

    // Fill up animations with fewer values
    allAnimates.forEach(animate => {
        const values = animate.getAttribute('values')?.split(';') || [];
        if (values.length < maxValuesLength && values.length > 0) {
            const lastValue = values[values.length - 1];
            while (values.length < maxValuesLength) {
                values.push(lastValue);
            }
            animate.setAttribute('values', values.join(';'));
        }
    });

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

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

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

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

    updateEtaniResult();
}

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

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

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

    document.querySelector('.etaniSettingFreeze').classList.toggle('active', !isRepeat);
    document.querySelector('.etaniSettingRepeat').classList.toggle('active', isRepeat);

    updateEtaniResult();
}

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

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

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

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

    const etaniSetting = document.createElement('div');
    etaniSetting.className = 'etaniSetting';
    
    const freezeRadio = document.createElement('span');
    freezeRadio.className = 'etaniSettingMode etaniSettingFreeze active';
    freezeRadio.textContent = 'Freeze';
    freezeRadio.setAttribute('data-mode', 'freeze');
    freezeRadio.addEventListener('click', () => handleSettingModeChange('freeze'));
    
    const repeatRadio = document.createElement('span');
    repeatRadio.className = 'etaniSettingMode etaniSettingRepeat';
    repeatRadio.textContent = 'Repeat';
    repeatRadio.setAttribute('data-mode', 'repeat');
    repeatRadio.addEventListener('click', () => handleSettingModeChange('repeat'));

    etaniSetting.appendChild(freezeRadio);
    etaniSetting.appendChild(repeatRadio);
    etaniCtrl.appendChild(etaniSetting);

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

    const transformButton = document.createElement('button');
    transformButton.className = 'etaniAllAppendTransform';
    transformButton.textContent = 'transform';
    transformButton.addEventListener('click', handleAllAppendTransformClick);
    
    const opacityButton = document.createElement('button');
    opacityButton.className = 'etaniAllAppendOpacity';
    opacityButton.textContent = 'opacity';
    opacityButton.addEventListener('click', handleAllAppendOpacityClick);
    
    etaniAllAppend.appendChild(transformButton);
    etaniAllAppend.appendChild(opacityButton);
    etaniCtrl.appendChild(etaniAllAppend);

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

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

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

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

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

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

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

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

    etaniouter.appendChild(etaniinner);

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

    etdropUses.forEach((useElement, i) => {
        const tileid = useElement.getAttribute('href').substring(1);
        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 originalTile = document.querySelector(`defs g#${tileid}`);
        
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';

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

        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        
        if (originalTile) {
            const tileclone = originalTile.cloneNode(true);
            tileclone.removeAttribute('id');
            if (etwaittransform) tileclone.setAttribute('transform', etwaittransform);
            if (etwaitfill) tileclone.setAttribute('fill', etwaitfill);
            if (etwaitstroke) tileclone.setAttribute('stroke', etwaitstroke);
            if (etwaitstrokeWidth) tileclone.setAttribute('stroke-width', etwaitstrokeWidth);
            
            const svgWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            svgWrapper.setAttribute('width', '40');
            svgWrapper.setAttribute('height', '40');
            svgWrapper.setAttribute('version', '1.1');
            svgWrapper.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
            svgWrapper.setAttribute('xmlns:svg', 'http://www.w3.org/2000/svg');
            svgWrapper.className = 'etanitileimg';
            
            svgWrapper.appendChild(tileclone);
            etaniItemLeft.appendChild(svgWrapper);
        }

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

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

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

    if (etanibutton.textContent === 'Animate it') {
        etanibutton.textContent = 'Close Ejtile Ani';
        createEtaniInner(etaniouter);
    } else if (etanibutton.textContent === 'Close Ejtile Ani') {
        etanibutton.textContent = 'Animate it';
        if (etaniinner) {
            etaniinner.remove();
        }
        etani_clone = null;
    }
}

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

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

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

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

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

帖子 ejsoon »

改動一些CSS,更正opacity不能加value。增加複製功能。

代码: 全选

改動:

一,把etaniCtrl內所有的<a>的href都改成「javascript:;」。

二,css更改:
.etaniAnimateValue的樣式中去掉overflow。
.etaniAVCtrl的樣式增加margin-bottom: 3px。
.etaniAVItem的樣式中的margin改成 0 5px 3px,同時去掉width,用padding: 0 5px取代。

三,在data-type為opacity的etaniAnimate內,按下etaniAVAdd時,它所增加的value跟最後一個value相同。當點擊它的.etaniAVItem時,它的值會由0變1或由非0變0。當在2秒中連按兩次同一個.etaniAVItem,則出現輸入框來輸入它的值。

四,在etaniAVCtrl內部的最後增加etaniAVCopy,當點擊第一次時,就會進入COPY狀態,這時再點擊任一個etaniAVItem,它都會複製一個並放在它的後面。如果是transform類型,它的字母不必發生變化。

代碼中的注釋都要全部是英文。

代码: 全选

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

// Track copy mode state and last click time for double-click detection
let isCopyMode = false;
let lastClickTime = 0;
let lastClickedItem = null;

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

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

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

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

        /* Styles for etaniResult */
        .etaniResult {
            text-align: center;
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #bbb;
            overflow: auto;
        }
        
        /* Control and button styles */
        .etaniContent a, .etaniValueCtrl a {
            display: inline-block;
            margin: 0 5px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 14px;
        }
        
        .etaniCenter {
            border: 1px solid green;
            color: green;
        }
        
        .etaniContentHTML {
            border: 1px solid #0099ff;
            color: #0099ff;
            margin-right: 15px;
        }

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

        /* Styles for etaniAllAppend buttons */
        .etaniAllAppend button {
            padding: 5px 10px;
            font-size: 16px;
            margin: 0 5px;
            cursor: pointer;
            border: 1px solid #333;
            background-color: #fff;
        }
        
        /* Custom radio button styles */
        .etaniSettingMode {
            display: inline-block;
            cursor: pointer;
            padding: 4px 8px;
            margin: 0 3px;
            font-size: 14px;
            border: 1px solid #888;
            background-color: #eee;
            color: #333;
            user-select: none;
        }

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

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

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

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

        .etaniResultImage {
            display: block;
            max-width: 480px;
            width: 100%;
            height: auto;
            margin: 0 auto 10px auto;
            border: 1px solid #000;
        }
        
        .etaniResultSize {
            display: inline-block;
            margin-left: 10px;
            font-size: 12px; 
            color: #555;
        }
        
        /* etaniItem structure */
        .etaniItem {
            min-height: 48px;
            border: 1px solid #ccc;
            box-sizing: border-box;
            width: 100%;
            margin-bottom: -1px;
            overflow: auto;
        }
        
        .etaniItemLeft {
            float: left; 
            width: 60px;
            min-height: 48px;
            border-right: 1px solid #ccc;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            background-color: #e8e8e8;
            padding: 2px 0;
        }
        
        .etaniItemRight {
            margin-left: 60px; 
            padding: 7px;
            min-height: 48px; 
            background-color: #fff;
        }

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

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

        .etaniAnimateDur {
            display: inline-block;
            margin-right: 10px;
            font-size: 14px;
        }

        .etaniAnimateValue {
            margin-top: 5px;
        }

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

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

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

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

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

        .etaniAVCopy.copying {
            background-color: #6666cc;
            color: white;
        }
        
        .etaniAVLabel {
            font-size: 14px;
            margin-right: 5px;
        }

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

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

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

    document.head.appendChild(styleSheet);
}

// Generate the next letter for transform etaniAVItem labels
function getNextLetter(currentLetter) {
    if (!currentLetter) return 'a';
    const code = currentLetter.charCodeAt(0);
    if (code >= 97 && code < 122) { // lowercase a-y
        return String.fromCharCode(code + 1);
    } else if (code === 122) { // z -> A
        return 'A';
    } else if (code >= 65 && code < 90) { // uppercase A-Y
        return String.fromCharCode(code + 1);
    } else if (code === 90) { // Z -> a (loop back)
        return 'a';
    }
    return 'a';
}

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

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

    const transform = {};

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

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

    return transform;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

    const originalUseElement = document.querySelector(`.etdrop use[href="#${useElementId}"]`);
    if (!originalUseElement) return;

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

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

        const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useElementId}"]`);
        if (!translateWrapper) return;

        const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useElementId}"]`);
        const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
        
        if (!scaleWrapper || !cloneUseElement) return;

        const cloneBaseTransformString = cloneUseElement.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)'; 
        const currentBaseTransforms = parseTransform(cloneBaseTransformString);
        
        const currentBaseTranslate = currentBaseTransforms.translate;
        const currentBaseScale = currentBaseTransforms.scale;
        const currentBaseRotate = currentBaseTransforms.rotate;

        const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
        const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
        const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
        
        const allAnimates = [...animatesRotate, ...animatesScale, ...animatesTranslate];
        
        const currentDur = parseInt((animatesRotate[0]?.getAttribute('dur') || '0s').replace('s', '')) || 0;
        const newDur = currentDur + 1;

        const isRepeat = document.querySelector('.etaniSettingRepeat.active');
        
        const scaleValue = calculateAdditiveValue('scale', originalTransforms.scale, currentBaseScale);
        
        allAnimates.forEach(animate => {
            const type = animate.getAttribute('type').toLowerCase();
            let currentValueString = animate.getAttribute('values') || '';
            let newValue = '';
            
            if (type === 'rotate') {
                newValue = calculateAdditiveValue('rotate', originalTransforms.rotate, currentBaseRotate);
            } else if (type === 'scale') {
                newValue = scaleValue;
            } else if (type === 'translate') {
                newValue = calculateAdditiveValue('translate', originalTransforms.translate, currentBaseTranslate, scaleValue);
            }
            
            animate.setAttribute('values', (currentValueString ? currentValueString + ';' : '') + newValue);

            if (newDur > 0) {
                animate.setAttribute('dur', `${newDur}s`);
                if (isRepeat) {
                    animate.removeAttribute('fill');
                    animate.setAttribute('repeatCount', 'indefinite');
                } else {
                    animate.removeAttribute('repeatCount');
                    animate.setAttribute('fill', 'freeze');
                }
            } else {
                animate.removeAttribute('dur');
                animate.removeAttribute('fill');
                animate.removeAttribute('repeatCount');
            }
        });

        etaniItemRight.querySelector(`.etaniAnimate[data-type="transform"] .etaniAnimateDur`).textContent = `dur: ${newDur}s`;
    } else if (animateType === 'opacity') {
        const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useElementId}"]`);
        if (!cloneUseElement) return;

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

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

        const currentDur = parseInt((animateOpacity.getAttribute('dur') || '0s').replace('s', '')) || 0;
        const newDur = currentDur + 1;
        const isRepeat = document.querySelector('.etaniSettingRepeat.active');

        if (newDur > 0) {
            animateOpacity.setAttribute('dur', `${newDur}s`);
            if (isRepeat) {
                animateOpacity.removeAttribute('fill');
                animateOpacity.setAttribute('repeatCount', 'indefinite');
            } else {
                animateOpacity.removeAttribute('repeatCount');
                animateOpacity.setAttribute('fill', 'freeze');
            }
        } else {
            animateOpacity.removeAttribute('dur');
            animateOpacity.removeAttribute('fill');
            animateOpacity.removeAttribute('repeatCount');
        }

        etaniItemRight.querySelector(`.etaniAnimate[data-type="opacity"] .etaniAnimateDur`).textContent = `dur: ${newDur}s`;
    }

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

        const existingItems = etaniAV.querySelectorAll('.etaniAVItem');
        if (animateType === 'transform') {
            const lastItem = existingItems[existingItems.length - 1];
            const lastLetter = lastItem ? lastItem.textContent : '';
            newAVItem.textContent = getNextLetter(lastLetter);
        } else if (animateType === 'opacity') {
            const values = etaniAnimate.querySelector('animate[attributeName="opacity"]')?.getAttribute('values')?.split(';') || ['1'];
            newAVItem.textContent = values[values.length - 1];
        }

        etaniAV.appendChild(newAVItem);
    }

    updateEtaniResult();
}

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

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

    document.querySelectorAll(`.etaniAVDelete[data-type="${animateType}"]`).forEach(btn => {
        if (btn !== deleteButton) {
            btn.closest('.etaniAVCtrl').disabled = isDeleting;
            btn.style.opacity = isDeleting ? 0.5 : 1;
        }
    });

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

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

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

    document.querySelectorAll(`.etaniAVCopy[data-type="${animateType}"]`).forEach(btn => {
        if (btn !== copyButton) {
            btn.closest('.etaniAVCtrl').disabled = isCopyMode;
            btn.style.opacity = isCopyMode ? 0.5 : 1;
        }
    });

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

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

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

    lastClickTime = currentTime;
    lastClickedItem = item;

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

        if (animateType === 'transform') {
            const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useId}"]`);
            if (!translateWrapper) return;
            
            const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useId}"]`);
            const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
            if (!scaleWrapper || !cloneUseElement) return;

            const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
            const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
            const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
            
            const allAnimates = [...animatesTranslate, ...animatesScale, ...animatesRotate];
            
            allAnimates.forEach(animate => {
                const values = animate.getAttribute('values').split(';');
                if (itemIndex < values.length) {
                    values.splice(itemIndex + 1, 0, values[itemIndex]);
                }
                animate.setAttribute('values', values.join(';'));
            });
        } else if (animateType === 'opacity') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return;

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

            const values = animateOpacity.getAttribute('values').split(';');
            if (itemIndex < values.length) {
                values.splice(itemIndex + 1, 0, values[itemIndex]);
            }
            animateOpacity.setAttribute('values', values.join(';'));
        }

        updateEtaniResult();
        return;
    }

    if (etaniCol.classList.contains(`deleting-mode-${animateType}`) && itemIndex > 0) {
        const deleteButton = etaniItemRight.querySelector(`.etaniAVDelete[data-type="${animateType}"]`);
        
        if (animateType === 'transform') {
            const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useId}"]`);
            if (!translateWrapper) return;
            
            const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useId}"]`);
            const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
            if (!scaleWrapper || !cloneUseElement) return;

            const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
            const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
            const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
            
            const allAnimates = [...animatesTranslate, ...animatesScale, ...animatesRotate];
            
            const currentDur = parseInt((animatesRotate[0].getAttribute('dur') || '0s').replace('s', '')) || 0;
            const newDur = Math.max(0, currentDur - 1);
            
            allAnimates.forEach(animate => {
                const values = animate.getAttribute('values').split(';');
                
                if (itemIndex < values.length) {
                    values.splice(itemIndex, 1);
                }
                animate.setAttribute('values', values.join(';'));
                
                if (newDur > 0) {
                    animate.setAttribute('dur', `${newDur}s`);
                } else {
                    animate.removeAttribute('dur');
                    animate.removeAttribute('fill');
                    animate.removeAttribute('repeatCount');
                }
            });

            etaniItemRight.querySelector(`.etaniAnimate[data-type="transform"] .etaniAnimateDur`).textContent = `dur: ${newDur}s`;
        } else if (animateType === 'opacity') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return;

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

            const values = animateOpacity.getAttribute('values').split(';');
            if (itemIndex < values.length) {
                values.splice(itemIndex, 1);
            }
            animateOpacity.setAttribute('values', values.join(';'));

            const currentDur = parseInt((animateOpacity.getAttribute('dur') || '0s').replace('s', '')) || 0;
            const newDur = Math.max(0, currentDur - 1);

            if (newDur > 0) {
                animateOpacity.setAttribute('dur', `${newDur}s`);
            } else {
                animateOpacity.removeAttribute('dur');
                animateOpacity.removeAttribute('fill');
                animateOpacity.removeAttribute('repeatCount');
            }

            etaniItemRight.querySelector(`.etaniAnimate[data-type="opacity"] .etaniAnimateDur`).textContent = `dur: ${newDur}s`;
        }

        item.remove();

        const remainingItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
        
        if (remainingItems.length === 1) {
            deleteButton.click();
        } else {
            remainingItems.forEach((item, index) => {
                item.classList.toggle('deleting-target', index > 0); 
            });
        }

        if (animateType === 'transform') {
            const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="transform"] .etaniAVItem`);
            let currentLetter = '';
            avItems.forEach((item, index) => {
                currentLetter = index === 0 ? 'a' : getNextLetter(currentLetter);
                item.textContent = currentLetter;
            });
        }

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

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

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

        if (isDoubleClick) {
            const newValue = prompt("Enter opacity value (0 to 1):", values[itemIndex]);
            if (newValue !== null && !isNaN(newValue) && newValue >= 0 && newValue <= 1) {
                values[itemIndex] = newValue;
                item.textContent = newValue;
                animateOpacity.setAttribute('values', values.join(';'));
                updateEtaniResult();
            }
        } else {
            values[itemIndex] = values[itemIndex] === '0' ? '1' : '0';
            item.textContent = values[itemIndex];
            animateOpacity.setAttribute('values', values.join(';'));
            updateEtaniResult();
        }
    } else if (itemIndex === 0 && etaniCol.classList.contains(`deleting-mode-${animateType}`)) {
        alert("The first value cannot be deleted.");
    }
}

// Handle click event for the 'Transform' button
function handleAllAppendTransformClick() {
    if (!etani_clone) return;

    const etdropUses = document.querySelectorAll('#etmain .etdrop use');
    const etaniItemRights = document.querySelectorAll('.etaniItemRight');

    const isRepeat = document.querySelector('.etaniSettingRepeat.active');

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

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

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

        const baseAnimate = (type, initialValue) => {
            const animate = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
            animate.setAttribute('attributeName', 'transform');
            animate.setAttribute('attributeType', 'XML');
            animate.setAttribute('type', type);
            animate.setAttribute('values', initialValue);
            animate.setAttribute('fill', isRepeat ? 'remove' : 'freeze');
            if (isRepeat) animate.setAttribute('repeatCount', 'indefinite');
            animate.setAttribute('additive', 'sum');
            return animate;
        };
        
        cloneUseElement.appendChild(baseAnimate('rotate', '0'));
        
        const scaleWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        scaleWrapper.setAttribute('data-use-href-scale', `#${useId}`);
        scaleWrapper.appendChild(baseAnimate('scale', '1'));
        
        const translateWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        translateWrapper.setAttribute('data-use-href', `#${useId}`);
        translateWrapper.appendChild(baseAnimate('translate', '0,0'));
        
        cloneUseElement.parentNode.insertBefore(translateWrapper, cloneUseElement);
        cloneUseElement.parentNode.insertBefore(scaleWrapper, cloneUseElement);
        scaleWrapper.appendChild(cloneUseElement);
        translateWrapper.appendChild(scaleWrapper);

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

        const nameSpan = document.createElement('span');
        nameSpan.className = 'etaniAnimateName';
        nameSpan.textContent = 'transform';
        etaniAnimate.appendChild(nameSpan);

        const durSpan = document.createElement('span');
        durSpan.className = 'etaniAnimateDur';
        durSpan.textContent = 'dur: 0s';
        etaniAnimate.appendChild(durSpan);

        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        const avCtrlDiv = document.createElement('div');
        avCtrlDiv.className = 'etaniAVCtrl';

        const avAddSpan = document.createElement('span');
        avAddSpan.className = 'etaniAVAdd';
        avAddSpan.title = 'Add Transform Value';
        avAddSpan.setAttribute('data-type', 'transform');
        avAddSpan.addEventListener('click', (e) => handleAVAddClick(e, useId, 'transform'));
        
        const plusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        plusSvg.setAttribute('width', '24');
        plusSvg.setAttribute('height', '24');
        plusSvg.setAttribute('viewBox', '0 0 24 24');
        plusSvg.setAttribute('fill', 'none');
        plusSvg.setAttribute('stroke', 'currentColor');
        plusSvg.setAttribute('stroke-width', '1');
        plusSvg.setAttribute('stroke-linecap', 'round');
        plusSvg.setAttribute('stroke-linejoin', 'round');
        plusSvg.innerHTML = '<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>';
        avAddSpan.appendChild(plusSvg);

        const avDeleteSpan = document.createElement('span');
        avDeleteSpan.className = 'etaniAVDelete';
        avDeleteSpan.title = 'Delete Value';
        avDeleteSpan.setAttribute('data-type', 'transform');
        avDeleteSpan.addEventListener('click', (e) => handleAVDeleteToggle(e, 'transform'));

        const minusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        minusSvg.setAttribute('width', '24');
        minusSvg.setAttribute('height', '24');
        minusSvg.setAttribute('viewBox', '0 0 24 24');
        minusSvg.setAttribute('fill', 'none');
        minusSvg.setAttribute('stroke', 'currentColor');
        minusSvg.setAttribute('stroke-width', '1');
        minusSvg.setAttribute('stroke-linecap', 'round');
        minusSvg.setAttribute('stroke-linejoin', 'round');
        minusSvg.innerHTML = '<line x1="5" y1="12" x2="19" y2="12"></line>';
        avDeleteSpan.appendChild(minusSvg);

        const avCopySpan = document.createElement('span');
        avCopySpan.className = 'etaniAVCopy';
        avCopySpan.title = 'Copy Value';
        avCopySpan.setAttribute('data-type', 'transform');
        avCopySpan.addEventListener('click', (e) => handleAVCopyToggle(e, 'transform'));

        const copySvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        copySvg.setAttribute('width', '24');
        copySvg.setAttribute('height', '24');
        copySvg.setAttribute('viewBox', '0 0 24 24');
        copySvg.setAttribute('fill', 'none');
        copySvg.setAttribute('stroke', 'currentColor');
        copySvg.setAttribute('stroke-width', '1');
        copySvg.setAttribute('stroke-linecap', 'round');
        copySvg.setAttribute('stroke-linejoin', 'round');
        copySvg.innerHTML = '<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>';
        avCopySpan.appendChild(copySvg);
        
        avCtrlDiv.appendChild(avAddSpan);
        avCtrlDiv.appendChild(avDeleteSpan);
        avCtrlDiv.appendChild(avCopySpan);

        const avLabelSpan = document.createElement('span');
        avLabelSpan.className = 'etaniAVLabel';
        avLabelSpan.textContent = 'values : ';

        const avDiv = document.createElement('div');
        avDiv.className = 'etaniAV';
        
        const avItemSpan = document.createElement('span');
        avItemSpan.className = 'etaniAVItem';
        avItemSpan.textContent = 'a';
        avItemSpan.addEventListener('click', (e) => handleAVItemClick(e, 'transform'));
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avCtrlDiv);
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

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

    updateEtaniResult();
}

// Handle click event for the 'Opacity' button
function handleAllAppendOpacityClick() {
    if (!etani_clone) return;

    const etdropUses = document.querySelectorAll('#etmain .etdrop use');
    const etaniItemRights = document.querySelectorAll('.etaniItemRight');

    const isRepeat = document.querySelector('.etaniSettingRepeat.active');

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

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

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

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

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

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

        const durSpan = document.createElement('span');
        durSpan.className = 'etaniAnimateDur';
        durSpan.textContent = 'dur: 0s';
        etaniAnimate.appendChild(durSpan);

        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        const avCtrlDiv = document.createElement('div');
        avCtrlDiv.className = 'etaniAVCtrl';

        const avAddSpan = document.createElement('span');
        avAddSpan.className = 'etaniAVAdd';
        avAddSpan.title = 'Add Opacity Value';
        avAddSpan.setAttribute('data-type', 'opacity');
        avAddSpan.addEventListener('click', (e) => handleAVAddClick(e, useId, 'opacity'));
        
        const plusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        plusSvg.setAttribute('width', '24');
        plusSvg.setAttribute('height', '24');
        plusSvg.setAttribute('viewBox', '0 0 24 24');
        plusSvg.setAttribute('fill', 'none');
        plusSvg.setAttribute('stroke', 'currentColor');
        plusSvg.setAttribute('stroke-width', '1');
        plusSvg.setAttribute('stroke-linecap', 'round');
        plusSvg.setAttribute('stroke-linejoin', 'round');
        plusSvg.innerHTML = '<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>';
        avAddSpan.appendChild(plusSvg);

        const avDeleteSpan = document.createElement('span');
        avDeleteSpan.className = 'etaniAVDelete';
        avDeleteSpan.title = 'Delete Value';
        avDeleteSpan.setAttribute('data-type', 'opacity');
        avDeleteSpan.addEventListener('click', (e) => handleAVDeleteToggle(e, 'opacity'));

        const minusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        minusSvg.setAttribute('width', '24');
        minusSvg.setAttribute('height', '24');
        minusSvg.setAttribute('viewBox', '0 0 24 24');
        minusSvg.setAttribute('fill', 'none');
        minusSvg.setAttribute('stroke', 'currentColor');
        minusSvg.setAttribute('stroke-width', '1');
        minusSvg.setAttribute('stroke-linecap', 'round');
        minusSvg.setAttribute('stroke-linejoin', 'round');
        minusSvg.innerHTML = '<line x1="5" y1="12" x2="19" y2="12"></line>';
        avDeleteSpan.appendChild(minusSvg);

        const avCopySpan = document.createElement('span');
        avCopySpan.className = 'etaniAVCopy';
        avCopySpan.title = 'Copy Value';
        avCopySpan.setAttribute('data-type', 'opacity');
        avCopySpan.addEventListener('click', (e) => handleAVCopyToggle(e, 'opacity'));

        const copySvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        copySvg.setAttribute('width', '24');
        copySvg.setAttribute('height', '24');
        copySvg.setAttribute('viewBox', '0 0 24 24');
        copySvg.setAttribute('fill', 'none');
        copySvg.setAttribute('stroke', 'currentColor');
        copySvg.setAttribute('stroke-width', '1');
        copySvg.setAttribute('stroke-linecap', 'round');
        copySvg.setAttribute('stroke-linejoin', 'round');
        copySvg.innerHTML = '<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>';
        avCopySpan.appendChild(copySvg);
        
        avCtrlDiv.appendChild(avAddSpan);
        avCtrlDiv.appendChild(avDeleteSpan);
        avCtrlDiv.appendChild(avCopySpan);

        const avLabelSpan = document.createElement('span');
        avLabelSpan.className = 'etaniAVLabel';
        avLabelSpan.textContent = 'values : ';

        const avDiv = document.createElement('div');
        avDiv.className = 'etaniAV';
        
        const avItemSpan = document.createElement('span');
        avItemSpan.className = 'etaniAVItem';
        avItemSpan.textContent = '1';
        avItemSpan.addEventListener('click', (e) => handleAVItemClick(e, 'opacity'));
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avCtrlDiv);
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

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

    updateEtaniResult();
}

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

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

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

    allAnimates.forEach(animate => {
        const values = animate.getAttribute('values')?.split(';') || [];
        if (values.length < maxValuesLength && values.length > 0) {
            const lastValue = values[values.length - 1];
            while (values.length < maxValuesLength) {
                values.push(lastValue);
            }
            animate.setAttribute('values', values.join(';'));
        }
    });

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

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

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

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

    updateEtaniResult();
}

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

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

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

    document.querySelector('.etaniSettingFreeze').classList.toggle('active', !isRepeat);
    document.querySelector('.etaniSettingRepeat').classList.toggle('active', isRepeat);

    updateEtaniResult();
}

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

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

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

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

    const etaniSetting = document.createElement('div');
    etaniSetting.className = 'etaniSetting';
    
    const freezeRadio = document.createElement('span');
    freezeRadio.className = 'etaniSettingMode etaniSettingFreeze active';
    freezeRadio.textContent = 'Freeze';
    freezeRadio.setAttribute('data-mode', 'freeze');
    freezeRadio.addEventListener('click', () => handleSettingModeChange('freeze'));
    
    const repeatRadio = document.createElement('span');
    repeatRadio.className = 'etaniSettingMode etaniSettingRepeat';
    repeatRadio.textContent = 'Repeat';
    repeatRadio.setAttribute('data-mode', 'repeat');
    repeatRadio.addEventListener('click', () => handleSettingModeChange('repeat'));

    etaniSetting.appendChild(freezeRadio);
    etaniSetting.appendChild(repeatRadio);
    etaniCtrl.appendChild(etaniSetting);

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

    const transformButton = document.createElement('button');
    transformButton.className = 'etaniAllAppendTransform';
    transformButton.textContent = 'transform';
    transformButton.addEventListener('click', handleAllAppendTransformClick);
    
    const opacityButton = document.createElement('button');
    opacityButton.className = 'etaniAllAppendOpacity';
    opacityButton.textContent = 'opacity';
    opacityButton.addEventListener('click', handleAllAppendOpacityClick);
    
    etaniAllAppend.appendChild(transformButton);
    etaniAllAppend.appendChild(opacityButton);
    etaniCtrl.appendChild(etaniAllAppend);

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

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

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

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

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

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

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

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

    etaniouter.appendChild(etaniinner);

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

    etdropUses.forEach((useElement, i) => {
        const tileid = useElement.getAttribute('href').substring(1);
        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 originalTile = document.querySelector(`defs g#${tileid}`);
        
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';

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

        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        
        if (originalTile) {
            const tileclone = originalTile.cloneNode(true);
            tileclone.removeAttribute('id');
            if (etwaittransform) tileclone.setAttribute('transform', etwaittransform);
            if (etwaitfill) tileclone.setAttribute('fill', etwaitfill);
            if (etwaitstroke) tileclone.setAttribute('stroke', etwaitstroke);
            if (etwaitstrokeWidth) tileclone.setAttribute('stroke-width', etwaitstrokeWidth);
            
            const svgWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            svgWrapper.setAttribute('width', '40');
            svgWrapper.setAttribute('height', '40');
            svgWrapper.setAttribute('version', '1.1');
            svgWrapper.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
            svgWrapper.setAttribute('xmlns:svg', 'http://www.w3.org/2000/svg');
            svgWrapper.className = 'etanitileimg';
            
            svgWrapper.appendChild(tileclone);
            etaniItemLeft.appendChild(svgWrapper);
        }

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

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

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

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

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

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

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

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

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

帖子 ejsoon »

首個value可以移複刪,增加移動VALUE。

代码: 全选

改動:

一,當進入刪除或複製狀態時,不需要更改其它圖標的透明度。

二,往etaniAVCtrl增加一個新功能etaniAVMove,它的功能是移動value。當點擊它時,進入移動狀態,當再次點擊它時,取消此狀態。

在移動狀態中,當第一次點擊某個etaniAVItem(a)時,它將被選中,之後點擊另一個etaniAVItem(b),如果a在b的後面,則a將被放在b的前面,如果a在b的前面,則a將被放在b的後面,選中效果消失。

例如abcdefg,c被選中,然後點擊b,則結果為acbdefg。如果d被選中,再點擊f,則結果為abcefdg。

如果被選中的etaniAVItem被點擊第二次,選中效果消失。

三,對於首個etaniAVItem,現在改為可以刪除、移動、複製。

四,在etaniSettingMode中,把repeat移到前面並默認選中。

代碼中的注釋都要全部是英文。

代码: 全选

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

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

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

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

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

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

        /* Styles for etaniResult */
        .etaniResult {
            text-align: center;
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #bbb;
            overflow: auto;
        }
        
        /* Control and button styles */
        .etaniContent a, .etaniValueCtrl a {
            display: inline-block;
            margin: 0 5px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 14px;
        }
        
        .etaniCenter {
            border: 1px solid green;
            color: green;
        }
        
        .etaniContentHTML {
            border: 1px solid #0099ff;
            color: #0099ff;
            margin-right: 15px;
        }

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

        /* Styles for etaniAllAppend buttons */
        .etaniAllAppend button {
            padding: 5px 10px;
            font-size: 16px;
            margin: 0 5px;
            cursor: pointer;
            border: 1px solid #333;
            background-color: #fff;
        }
        
        /* Custom radio button styles */
        .etaniSettingMode {
            display: inline-block;
            cursor: pointer;
            padding: 4px 8px;
            margin: 0 3px;
            font-size: 14px;
            border: 1px solid #888;
            background-color: #eee;
            color: #333;
            user-select: none;
        }

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

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

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

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

        .etaniResultImage {
            display: block;
            max-width: 480px;
            width: 100%;
            height: auto;
            margin: 0 auto 10px auto;
            border: 1px solid #000;
        }
        
        .etaniResultSize {
            display: inline-block;
            margin-left: 10px;
            font-size: 12px; 
            color: #555;
        }
        
        /* etaniItem structure */
        .etaniItem {
            min-height: 48px;
            border: 1px solid #ccc;
            box-sizing: border-box;
            width: 100%;
            margin-bottom: -1px;
            overflow: auto;
        }
        
        .etaniItemLeft {
            float: left; 
            width: 60px;
            min-height: 48px;
            border-right: 1px solid #ccc;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            background-color: #e8e8e8;
            padding: 2px 0;
        }
        
        .etaniItemRight {
            margin-left: 60px; 
            padding: 7px;
            min-height: 48px; 
            background-color: #fff;
        }

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

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

        .etaniAnimateDur {
            display: inline-block;
            margin-right: 10px;
            font-size: 14px;
        }

        .etaniAnimateValue {
            margin-top: 5px;
        }

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

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

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

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

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

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

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

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

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

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

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

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

    document.head.appendChild(styleSheet);
}

// Generate the next letter for transform etaniAVItem labels
function getNextLetter(currentLetter) {
    if (!currentLetter) return 'a';
    const code = currentLetter.charCodeAt(0);
    if (code >= 97 && code < 122) { // lowercase a-y
        return String.fromCharCode(code + 1);
    } else if (code === 122) { // z -> A
        return 'A';
    } else if (code >= 65 && code < 90) { // uppercase A-Y
        return String.fromCharCode(code + 1);
    } else if (code === 90) { // Z -> a (loop back)
        return 'a';
    }
    return 'a';
}

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

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

    const transform = {};

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

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

    return transform;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

    const originalUseElement = document.querySelector(`.etdrop use[href="#${useElementId}"]`);
    if (!originalUseElement) return;

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

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

        const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useElementId}"]`);
        if (!translateWrapper) return;

        const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useElementId}"]`);
        const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
        
        if (!scaleWrapper || !cloneUseElement) return;

        const cloneBaseTransformString = cloneUseElement.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)'; 
        const currentBaseTransforms = parseTransform(cloneBaseTransformString);
        
        const currentBaseTranslate = currentBaseTransforms.translate;
        const currentBaseScale = currentBaseTransforms.scale;
        const currentBaseRotate = currentBaseTransforms.rotate;

        const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
        const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
        const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
        
        const allAnimates = [...animatesRotate, ...animatesScale, ...animatesTranslate];
        
        const currentDur = parseInt((animatesRotate[0]?.getAttribute('dur') || '0s').replace('s', '')) || 0;
        const newDur = currentDur + 1;

        const isRepeat = document.querySelector('.etaniSettingRepeat.active');
        
        const scaleValue = calculateAdditiveValue('scale', originalTransforms.scale, currentBaseScale);
        
        allAnimates.forEach(animate => {
            const type = animate.getAttribute('type').toLowerCase();
            let currentValueString = animate.getAttribute('values') || '';
            let newValue = '';
            
            if (type === 'rotate') {
                newValue = calculateAdditiveValue('rotate', originalTransforms.rotate, currentBaseRotate);
            } else if (type === 'scale') {
                newValue = scaleValue;
            } else if (type === 'translate') {
                newValue = calculateAdditiveValue('translate', originalTransforms.translate, currentBaseTranslate, scaleValue);
            }
            
            animate.setAttribute('values', (currentValueString ? currentValueString + ';' : '') + newValue);

            if (newDur > 0) {
                animate.setAttribute('dur', `${newDur}s`);
                if (isRepeat) {
                    animate.removeAttribute('fill');
                    animate.setAttribute('repeatCount', 'indefinite');
                } else {
                    animate.removeAttribute('repeatCount');
                    animate.setAttribute('fill', 'freeze');
                }
            } else {
                animate.removeAttribute('dur');
                animate.removeAttribute('fill');
                animate.removeAttribute('repeatCount');
            }
        });

        etaniItemRight.querySelector(`.etaniAnimate[data-type="transform"] .etaniAnimateDur`).textContent = `dur: ${newDur}s`;
    } else if (animateType === 'opacity') {
        const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useElementId}"]`);
        if (!cloneUseElement) return;

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

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

        const currentDur = parseInt((animateOpacity.getAttribute('dur') || '0s').replace('s', '')) || 0;
        const newDur = currentDur + 1;
        const isRepeat = document.querySelector('.etaniSettingRepeat.active');

        if (newDur > 0) {
            animateOpacity.setAttribute('dur', `${newDur}s`);
            if (isRepeat) {
                animateOpacity.removeAttribute('fill');
                animateOpacity.setAttribute('repeatCount', 'indefinite');
            } else {
                animateOpacity.removeAttribute('repeatCount');
                animateOpacity.setAttribute('fill', 'freeze');
            }
        } else {
            animateOpacity.removeAttribute('dur');
            animateOpacity.removeAttribute('fill');
            animateOpacity.removeAttribute('repeatCount');
        }

        etaniItemRight.querySelector(`.etaniAnimate[data-type="opacity"] .etaniAnimateDur`).textContent = `dur: ${newDur}s`;
    }

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

        const existingItems = etaniAV.querySelectorAll('.etaniAVItem');
        if (animateType === 'transform') {
            const lastItem = existingItems[existingItems.length - 1];
            const lastLetter = lastItem ? lastItem.textContent : '';
            newAVItem.textContent = getNextLetter(lastLetter);
        } else if (animateType === 'opacity') {
            const values = animateType === 'opacity' ? 
                etaniAnimate.querySelector('animate[attributeName="opacity"]')?.getAttribute('values')?.split(';') || ['1'] :
                ['1'];
            newAVItem.textContent = values[values.length - 1];
        }

        etaniAV.appendChild(newAVItem);
    }

    updateEtaniResult();
}

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

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

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

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

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

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

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

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

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

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

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

    lastClickTime = currentTime;
    lastClickedItem = item;

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

            if (animateType === 'transform') {
                const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useId}"]`);
                if (!translateWrapper) return;
                
                const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useId}"]`);
                const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
                if (!scaleWrapper || !cloneUseElement) return;

                const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
                const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
                const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
                
                const allAnimates = [...animatesTranslate, ...animatesScale, ...animatesRotate];
                
                allAnimates.forEach(animate => {
                    const values = animate.getAttribute('values').split(';');
                    const valueToMove = values[sourceIndex];
                    values.splice(sourceIndex, 1);
                    if (sourceIndex < targetIndex) {
                        values.splice(targetIndex, 0, valueToMove);
                    } else {
                        values.splice(targetIndex, 0, valueToMove);
                    }
                    animate.setAttribute('values', values.join(';'));
                });

                const avItemsArray = Array.from(avItems);
                if (animateType === 'transform') {
                    let currentLetter = '';
                    avItemsArray.forEach((avItem, index) => {
                        currentLetter = index === 0 ? 'a' : getNextLetter(currentLetter);
                        avItem.textContent = currentLetter;
                    });
                }
            } else if (animateType === 'opacity') {
                const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
                if (!cloneUseElement) return;

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

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

                const avItemsArray = Array.from(avItems);
                avItemsArray.forEach((avItem, index) => {
                    avItem.textContent = values[index];
                });
            }

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

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

        if (animateType === 'transform') {
            const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useId}"]`);
            if (!translateWrapper) return;
            
            const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useId}"]`);
            const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
            if (!scaleWrapper || !cloneUseElement) return;

            const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
            const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
            const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
            
            const allAnimates = [...animatesTranslate, ...animatesScale, ...animatesRotate];
            
            allAnimates.forEach(animate => {
                const values = animate.getAttribute('values').split(';');
                if (itemIndex < values.length) {
                    values.splice(itemIndex + 1, 0, values[itemIndex]);
                }
                animate.setAttribute('values', values.join(';'));
            });
        } else if (animateType === 'opacity') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return;

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

            const values = animateOpacity.getAttribute('values').split(';');
            if (itemIndex < values.length) {
                values.splice(itemIndex + 1, 0, values[itemIndex]);
            }
            animateOpacity.setAttribute('values', values.join(';'));
        }

        updateEtaniResult();
        return;
    }

    if (etaniCol.classList.contains(`deleting-mode-${animateType}`)) {
        const deleteButton = etaniItemRight.querySelector(`.etaniAVDelete[data-type="${animateType}"]`);
        
        if (animateType === 'transform') {
            const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useId}"]`);
            if (!translateWrapper) return;
            
            const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useId}"]`);
            const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
            if (!scaleWrapper || !cloneUseElement) return;

            const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
            const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
            const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
            
            const allAnimates = [...animatesTranslate, ...animatesScale, ...animatesRotate];
            
            const currentDur = parseInt((animatesRotate[0].getAttribute('dur') || '0s').replace('s', '')) || 0;
            const newDur = Math.max(0, currentDur - 1);
            
            allAnimates.forEach(animate => {
                const values = animate.getAttribute('values').split(';');
                
                if (itemIndex < values.length) {
                    values.splice(itemIndex, 1);
                }
                animate.setAttribute('values', values.join(';'));
                
                if (newDur > 0) {
                    animate.setAttribute('dur', `${newDur}s`);
                } else {
                    animate.removeAttribute('dur');
                    animate.removeAttribute('fill');
                    animate.removeAttribute('repeatCount');
                }
            });

            etaniItemRight.querySelector(`.etaniAnimate[data-type="transform"] .etaniAnimateDur`).textContent = `dur: ${newDur}s`;
        } else if (animateType === 'opacity') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return;

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

            const values = animateOpacity.getAttribute('values').split(';');
            if (itemIndex < values.length) {
                values.splice(itemIndex, 1);
            }
            animateOpacity.setAttribute('values', values.join(';'));

            const currentDur = parseInt((animateOpacity.getAttribute('dur') || '0s').replace('s', '')) || 0;
            const newDur = Math.max(0, currentDur - 1);

            if (newDur > 0) {
                animateOpacity.setAttribute('dur', `${newDur}s`);
            } else {
                animateOpacity.removeAttribute('dur');
                animateOpacity.removeAttribute('fill');
                animateOpacity.removeAttribute('repeatCount');
            }

            etaniItemRight.querySelector(`.etaniAnimate[data-type="opacity"] .etaniAnimateDur`).textContent = `dur: ${newDur}s`;
        }

        item.remove();

        const remainingItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
        
        if (remainingItems.length === 0) {
            deleteButton.click();
        } else {
            remainingItems.forEach(item => {
                item.classList.add('deleting-target'); 
            });
        }

        if (animateType === 'transform') {
            const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="transform"] .etaniAVItem`);
            let currentLetter = '';
            avItems.forEach((item, index) => {
                currentLetter = index === 0 ? 'a' : getNextLetter(currentLetter);
                item.textContent = currentLetter;
            });
        } else if (animateType === 'opacity') {
            const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="opacity"] .etaniAVItem`);
            const values = etaniItemRight.querySelector('animate[attributeName="opacity"]')?.getAttribute('values')?.split(';') || [];
            avItems.forEach((item, index) => {
                item.textContent = values[index] || '1';
            });
        }

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

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

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

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

// Handle click event for the 'Transform' button
function handleAllAppendTransformClick() {
    if (!etani_clone) return;

    const etdropUses = document.querySelectorAll('#etmain .etdrop use');
    const etaniItemRights = document.querySelectorAll('.etaniItemRight');

    const isRepeat = document.querySelector('.etaniSettingRepeat.active');

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

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

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

        const baseAnimate = (type, initialValue) => {
            const animate = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
            animate.setAttribute('attributeName', 'transform');
            animate.setAttribute('attributeType', 'XML');
            animate.setAttribute('type', type);
            animate.setAttribute('values', initialValue);
            animate.setAttribute('fill', isRepeat ? 'remove' : 'freeze');
            if (isRepeat) animate.setAttribute('repeatCount', 'indefinite');
            animate.setAttribute('additive', 'sum');
            return animate;
        };
        
        cloneUseElement.appendChild(baseAnimate('rotate', '0'));
        
        const scaleWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        scaleWrapper.setAttribute('data-use-href-scale', `#${useId}`);
        scaleWrapper.appendChild(baseAnimate('scale', '1'));
        
        const translateWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        translateWrapper.setAttribute('data-use-href', `#${useId}`);
        translateWrapper.appendChild(baseAnimate('translate', '0,0'));
        
        cloneUseElement.parentNode.insertBefore(translateWrapper, cloneUseElement);
        cloneUseElement.parentNode.insertBefore(scaleWrapper, cloneUseElement);
        scaleWrapper.appendChild(cloneUseElement);
        translateWrapper.appendChild(scaleWrapper);

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

        const nameSpan = document.createElement('span');
        nameSpan.className = 'etaniAnimateName';
        nameSpan.textContent = 'transform';
        etaniAnimate.appendChild(nameSpan);

        const durSpan = document.createElement('span');
        durSpan.className = 'etaniAnimateDur';
        durSpan.textContent = 'dur: 0s';
        etaniAnimate.appendChild(durSpan);

        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        const avCtrlDiv = document.createElement('div');
        avCtrlDiv.className = 'etaniAVCtrl';

        const avAddSpan = document.createElement('span');
        avAddSpan.className = 'etaniAVAdd';
        avAddSpan.title = 'Add Transform Value';
        avAddSpan.setAttribute('data-type', 'transform');
        avAddSpan.addEventListener('click', (e) => handleAVAddClick(e, useId, 'transform'));
        
        const plusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        plusSvg.setAttribute('width', '24');
        plusSvg.setAttribute('height', '24');
        plusSvg.setAttribute('viewBox', '0 0 24 24');
        plusSvg.setAttribute('fill', 'none');
        plusSvg.setAttribute('stroke', 'currentColor');
        plusSvg.setAttribute('stroke-width', '1');
        plusSvg.setAttribute('stroke-linecap', 'round');
        plusSvg.setAttribute('stroke-linejoin', 'round');
        plusSvg.innerHTML = '<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>';
        avAddSpan.appendChild(plusSvg);

        const avDeleteSpan = document.createElement('span');
        avDeleteSpan.className = 'etaniAVDelete';
        avDeleteSpan.title = 'Delete Value';
        avDeleteSpan.setAttribute('data-type', 'transform');
        avDeleteSpan.addEventListener('click', (e) => handleAVDeleteToggle(e, 'transform'));

        const minusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        minusSvg.setAttribute('width', '24');
        minusSvg.setAttribute('height', '24');
        minusSvg.setAttribute('viewBox', '0 0 24 24');
        minusSvg.setAttribute('fill', 'none');
        minusSvg.setAttribute('stroke', 'currentColor');
        minusSvg.setAttribute('stroke-width', '1');
        minusSvg.setAttribute('stroke-linecap', 'round');
        minusSvg.setAttribute('stroke-linejoin', 'round');
        minusSvg.innerHTML = '<line x1="5" y1="12" x2="19" y2="12"></line>';
        avDeleteSpan.appendChild(minusSvg);

        const avCopySpan = document.createElement('span');
        avCopySpan.className = 'etaniAVCopy';
        avCopySpan.title = 'Copy Value';
        avCopySpan.setAttribute('data-type', 'transform');
        avCopySpan.addEventListener('click', (e) => handleAVCopyToggle(e, 'transform'));

        const copySvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        copySvg.setAttribute('width', '24');
        copySvg.setAttribute('height', '24');
        copySvg.setAttribute('viewBox', '0 0 24 24');
        copySvg.setAttribute('fill', 'none');
        copySvg.setAttribute('stroke', 'currentColor');
        copySvg.setAttribute('stroke-width', '1');
        copySvg.setAttribute('stroke-linecap', 'round');
        copySvg.setAttribute('stroke-linejoin', 'round');
        copySvg.innerHTML = '<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>';
        avCopySpan.appendChild(copySvg);

        const avMoveSpan = document.createElement('span');
        avMoveSpan.className = 'etaniAVMove';
        avMoveSpan.title = 'Move Value';
        avMoveSpan.setAttribute('data-type', 'transform');
        avMoveSpan.addEventListener('click', (e) => handleAVMoveToggle(e, 'transform'));

        const moveSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        moveSvg.setAttribute('width', '24');
        moveSvg.setAttribute('height', '24');
        moveSvg.setAttribute('viewBox', '0 0 24 24');
        moveSvg.setAttribute('fill', 'none');
        moveSvg.setAttribute('stroke', 'currentColor');
        moveSvg.setAttribute('stroke-width', '1');
        moveSvg.setAttribute('stroke-linecap', 'round');
        moveSvg.setAttribute('stroke-linejoin', 'round');
        moveSvg.innerHTML = '<polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" y1="3" x2="14" y2="10"></line><line x1="3" y1="21" x2="10" y2="14"></line>';
        avMoveSpan.appendChild(moveSvg);
        
        avCtrlDiv.appendChild(avAddSpan);
        avCtrlDiv.appendChild(avDeleteSpan);
        avCtrlDiv.appendChild(avCopySpan);
        avCtrlDiv.appendChild(avMoveSpan);

        const avLabelSpan = document.createElement('span');
        avLabelSpan.className = 'etaniAVLabel';
        avLabelSpan.textContent = 'values : ';

        const avDiv = document.createElement('div');
        avDiv.className = 'etaniAV';
        
        const avItemSpan = document.createElement('span');
        avItemSpan.className = 'etaniAVItem';
        avItemSpan.textContent = 'a';
        avItemSpan.addEventListener('click', (e) => handleAVItemClick(e, 'transform'));
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avCtrlDiv);
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

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

    updateEtaniResult();
}

// Handle click event for the 'Opacity' button
function handleAllAppendOpacityClick() {
    if (!etani_clone) return;

    const etdropUses = document.querySelectorAll('#etmain .etdrop use');
    const etaniItemRights = document.querySelectorAll('.etaniItemRight');

    const isRepeat = document.querySelector('.etaniSettingRepeat.active');

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

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

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

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

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

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

        const durSpan = document.createElement('span');
        durSpan.className = 'etaniAnimateDur';
        durSpan.textContent = 'dur: 0s';
        etaniAnimate.appendChild(durSpan);

        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        const avCtrlDiv = document.createElement('div');
        avCtrlDiv.className = 'etaniAVCtrl';

        const avAddSpan = document.createElement('span');
        avAddSpan.className = 'etaniAVAdd';
        avAddSpan.title = 'Add Opacity Value';
        avAddSpan.setAttribute('data-type', 'opacity');
        avAddSpan.addEventListener('click', (e) => handleAVAddClick(e, useId, 'opacity'));
        
        const plusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        plusSvg.setAttribute('width', '24');
        plusSvg.setAttribute('height', '24');
        plusSvg.setAttribute('viewBox', '0 0 24 24');
        plusSvg.setAttribute('fill', 'none');
        plusSvg.setAttribute('stroke', 'currentColor');
        plusSvg.setAttribute('stroke-width', '1');
        plusSvg.setAttribute('stroke-linecap', 'round');
        plusSvg.setAttribute('stroke-linejoin', 'round');
        plusSvg.innerHTML = '<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>';
        avAddSpan.appendChild(plusSvg);

        const avDeleteSpan = document.createElement('span');
        avDeleteSpan.className = 'etaniAVDelete';
        avDeleteSpan.title = 'Delete Value';
        avDeleteSpan.setAttribute('data-type', 'opacity');
        avDeleteSpan.addEventListener('click', (e) => handleAVDeleteToggle(e, 'opacity'));

        const minusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        minusSvg.setAttribute('width', '24');
        minusSvg.setAttribute('height', '24');
        minusSvg.setAttribute('viewBox', '0 0 24 24');
        minusSvg.setAttribute('fill', 'none');
        minusSvg.setAttribute('stroke', 'currentColor');
        minusSvg.setAttribute('stroke-width', '1');
        minusSvg.setAttribute('stroke-linecap', 'round');
        minusSvg.setAttribute('stroke-linejoin', 'round');
        minusSvg.innerHTML = '<line x1="5" y1="12" x2="19" y2="12"></line>';
        avDeleteSpan.appendChild(minusSvg);

        const avCopySpan = document.createElement('span');
        avCopySpan.className = 'etaniAVCopy';
        avCopySpan.title = 'Copy Value';
        avCopySpan.setAttribute('data-type', 'opacity');
        avCopySpan.addEventListener('click', (e) => handleAVCopyToggle(e, 'opacity'));

        const copySvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        copySvg.setAttribute('width', '24');
        copySvg.setAttribute('height', '24');
        copySvg.setAttribute('viewBox', '0 0 24 24');
        copySvg.setAttribute('fill', 'none');
        copySvg.setAttribute('stroke', 'currentColor');
        copySvg.setAttribute('stroke-width', '1');
        copySvg.setAttribute('stroke-linecap', 'round');
        copySvg.setAttribute('stroke-linejoin', 'round');
        copySvg.innerHTML = '<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>';
        avCopySpan.appendChild(copySvg);

        const avMoveSpan = document.createElement('span');
        avMoveSpan.className = 'etaniAVMove';
        avMoveSpan.title = 'Move Value';
        avMoveSpan.setAttribute('data-type', 'opacity');
        avMoveSpan.addEventListener('click', (e) => handleAVMoveToggle(e, 'opacity'));

        const moveSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        moveSvg.setAttribute('width', '24');
        moveSvg.setAttribute('height', '24');
        moveSvg.setAttribute('viewBox', '0 0 24 24');
        moveSvg.setAttribute('fill', 'none');
        moveSvg.setAttribute('stroke', 'currentColor');
        moveSvg.setAttribute('stroke-width', '1');
        moveSvg.setAttribute('stroke-linecap', 'round');
        moveSvg.setAttribute('stroke-linejoin', 'round');
        moveSvg.innerHTML = '<polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" y1="3" x2="14" y2="10"></line><line x1="3" y1="21" x2="10" y2="14"></line>';
        avMoveSpan.appendChild(moveSvg);
        
        avCtrlDiv.appendChild(avAddSpan);
        avCtrlDiv.appendChild(avDeleteSpan);
        avCtrlDiv.appendChild(avCopySpan);
        avCtrlDiv.appendChild(avMoveSpan);

        const avLabelSpan = document.createElement('span');
        avLabelSpan.className = 'etaniAVLabel';
        avLabelSpan.textContent = 'values : ';

        const avDiv = document.createElement('div');
        avDiv.className = 'etaniAV';
        
        const avItemSpan = document.createElement('span');
        avItemSpan.className = 'etaniAVItem';
        avItemSpan.textContent = '1';
        avItemSpan.addEventListener('click', (e) => handleAVItemClick(e, 'opacity'));
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avCtrlDiv);
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

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

    updateEtaniResult();
}

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

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

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

    allAnimates.forEach(animate => {
        const values = animate.getAttribute('values')?.split(';') || [];
        if (values.length < maxValuesLength && values.length > 0) {
            const lastValue = values[values.length - 1];
            while (values.length < maxValuesLength) {
                values.push(lastValue);
            }
            animate.setAttribute('values', values.join(';'));
        }
    });

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

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

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

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

    updateEtaniResult();
}

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

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

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

    document.querySelector('.etaniSettingFreeze').classList.toggle('active', !isRepeat);
    document.querySelector('.etaniSettingRepeat').classList.toggle('active', isRepeat);

    updateEtaniResult();
}

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

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

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

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

    const etaniSetting = document.createElement('div');
    etaniSetting.className = 'etaniSetting';
    
    const repeatRadio = document.createElement('span');
    repeatRadio.className = 'etaniSettingMode etaniSettingRepeat active';
    repeatRadio.textContent = 'Repeat';
    repeatRadio.setAttribute('data-mode', 'repeat');
    repeatRadio.addEventListener('click', () => handleSettingModeChange('repeat'));
    
    const freezeRadio = document.createElement('span');
    freezeRadio.className = 'etaniSettingMode etaniSettingFreeze';
    freezeRadio.textContent = 'Freeze';
    freezeRadio.setAttribute('data-mode', 'freeze');
    freezeRadio.addEventListener('click', () => handleSettingModeChange('freeze'));

    etaniSetting.appendChild(repeatRadio);
    etaniSetting.appendChild(freezeRadio);
    etaniCtrl.appendChild(etaniSetting);

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

    const transformButton = document.createElement('button');
    transformButton.className = 'etaniAllAppendTransform';
    transformButton.textContent = 'transform';
    transformButton.addEventListener('click', handleAllAppendTransformClick);
    
    const opacityButton = document.createElement('button');
    opacityButton.className = 'etaniAllAppendOpacity';
    opacityButton.textContent = 'opacity';
    opacityButton.addEventListener('click', handleAllAppendOpacityClick);
    
    etaniAllAppend.appendChild(transformButton);
    etaniAllAppend.appendChild(opacityButton);
    etaniCtrl.appendChild(etaniAllAppend);

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

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

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

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

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

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

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

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

    etaniouter.appendChild(etaniinner);

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

    etdropUses.forEach((useElement, i) => {
        const tileid = useElement.getAttribute('href').substring(1);
        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 originalTile = document.querySelector(`defs g#${tileid}`);
        
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';

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

        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        
        if (originalTile) {
            const tileclone = originalTile.cloneNode(true);
            tileclone.removeAttribute('id');
            if (etwaittransform) tileclone.setAttribute('transform', etwaittransform);
            if (etwaitfill) tileclone.setAttribute('fill', etwaitfill);
            if (etwaitstroke) tileclone.setAttribute('stroke', etwaitstroke);
            if (etwaitstrokeWidth) tileclone.setAttribute('stroke-width', etwaitstrokeWidth);
            
            const svgWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            svgWrapper.setAttribute('width', '40');
            svgWrapper.setAttribute('height', '40');
            svgWrapper.setAttribute('version', '1.1');
            svgWrapper.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
            svgWrapper.setAttribute('xmlns:svg', 'http://www.w3.org/2000/svg');
            svgWrapper.className = 'etanitileimg';
            
            svgWrapper.appendChild(tileclone);
            etaniItemLeft.appendChild(svgWrapper);
        }

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

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

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

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

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

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

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

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

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

帖子 ejsoon »

move換圖標,move不要改字母,move,delete,copy可直接切換,新增按字母序補缺。

代码: 全选

改動:

一,etaniAVMove換個圖標,圖案是上下兩個箭頭,上面的朝左,下面的朝右。

二,當複製或刪除使etaniAVItem的數量發生變化時,dur也要一起變化。

三,etaniAVMove在移動之後,不要使etaniAVItem的內容發生變化。

四,在data-type="transform"中的etaniAVItem的字母可能會因為複製、移動、刪除而變化,這時如果點擊etaniAVAdd,它所新增的etaniAVItem內的英文,是從a開始的字母序中第一個缺失的字母。

比如現在的etaniAVItem有ahbedg,則新增的etaniAVItem內的字母應為c,因為c是第一個缺失的字母。

五,在etaniAVAdd、etaniAVCopy、etaniAVMove中,如果進入了某個狀態,之後又點擊另一個,則直接切換到另一個狀態。

代碼中所有的注釋都要使用英文。

代码: 全选

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

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

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

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

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

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

        /* Styles for etaniResult */
        .etaniResult {
            text-align: center;
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #bbb;
            overflow: auto;
        }
        
        /* Control and button styles */
        .etaniContent a, .etaniValueCtrl a {
            display: inline-block;
            margin: 0 5px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 14px;
        }
        
        .etaniCenter {
            border: 1px solid green;
            color: green;
        }
        
        .etaniContentHTML {
            border: 1px solid #0099ff;
            color: #0099ff;
            margin-right: 15px;
        }

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

        /* Styles for etaniAllAppend buttons */
        .etaniAllAppend button {
            padding: 5px 10px;
            font-size: 16px;
            margin: 0 5px;
            cursor: pointer;
            border: 1px solid #333;
            background-color: #fff;
        }
        
        /* Custom radio button styles */
        .etaniSettingMode {
            display: inline-block;
            cursor: pointer;
            padding: 4px 8px;
            margin: 0 3px;
            font-size: 14px;
            border: 1px solid #888;
            background-color: #eee;
            color: #333;
            user-select: none;
        }

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

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

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

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

        .etaniResultImage {
            display: block;
            max-width: 480px;
            width: 100%;
            height: auto;
            margin: 0 auto 10px auto;
            border: 1px solid #000;
        }
        
        .etaniResultSize {
            display: inline-block;
            margin-left: 10px;
            font-size: 12px; 
            color: #555;
        }
        
        /* etaniItem structure */
        .etaniItem {
            min-height: 48px;
            border: 1px solid #ccc;
            box-sizing: border-box;
            width: 100%;
            margin-bottom: -1px;
            overflow: auto;
        }
        
        .etaniItemLeft {
            float: left; 
            width: 60px;
            min-height: 48px;
            border-right: 1px solid #ccc;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            background-color: #e8e8e8;
            padding: 2px 0;
        }
        
        .etaniItemRight {
            margin-left: 60px; 
            padding: 7px;
            min-height: 48px; 
            background-color: #fff;
        }

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

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

        .etaniAnimateDur {
            display: inline-block;
            margin-right: 10px;
            font-size: 14px;
        }

        .etaniAnimateValue {
            margin-top: 5px;
        }

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

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

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

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

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

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

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

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

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

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

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

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

    document.head.appendChild(styleSheet);
}

// Generate the next letter for transform etaniAVItem labels
function getNextLetter(currentLetter) {
    if (!currentLetter) return 'a';
    const code = currentLetter.charCodeAt(0);
    if (code >= 97 && code < 122) { // lowercase a-y
        return String.fromCharCode(code + 1);
    } else if (code === 122) { // z -> A
        return 'A';
    } else if (code >= 65 && code < 90) { // uppercase A-Y
        return String.fromCharCode(code + 1);
    } else if (code === 90) { // Z -> a (loop back)
        return 'a';
    }
    return 'a';
}

// Find the first missing letter in the alphabetical sequence
function findFirstMissingLetter(existingLetters) {
    let currentLetter = 'a';
    const lettersSet = new Set(existingLetters);
    
    while (true) {
        if (!lettersSet.has(currentLetter)) {
            return currentLetter;
        }
        currentLetter = getNextLetter(currentLetter);
        if (currentLetter === 'a') break; // Avoid infinite loop
    }
    return 'a'; // Fallback if all letters are used
}

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

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

    const transform = {};

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

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

    return transform;
}

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

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

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

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

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

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

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

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

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

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

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

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

// Update duration based on the number of values
function updateDuration(animate, etaniAnimate, valuesLength) {
    const isRepeat = document.querySelector('.etaniSettingRepeat.active');
    const newDur = valuesLength - 1;

    if (newDur > 0) {
        animate.setAttribute('dur', `${newDur}s`);
        if (isRepeat) {
            animate.removeAttribute('fill');
            animate.setAttribute('repeatCount', 'indefinite');
        } else {
            animate.removeAttribute('repeatCount');
            animate.setAttribute('fill', 'freeze');
        }
    } else {
        animate.removeAttribute('dur');
        animate.removeAttribute('fill');
        animate.removeAttribute('repeatCount');
    }

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

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

    // Exit other modes
    if (isCopyMode || isMoveMode || isDeleteMode) {
        const etaniItemRight = e.currentTarget.closest('.etaniItemRight');
        const etaniCol = e.currentTarget.closest('.etaniCol');
        if (isCopyMode) {
            const copyButton = etaniItemRight.querySelector(`.etaniAVCopy[data-type="${animateType}"]`);
            if (copyButton) copyButton.click();
        }
        if (isMoveMode) {
            const moveButton = etaniItemRight.querySelector(`.etaniAVMove[data-type="${animateType}"]`);
            if (moveButton) moveButton.click();
        }
        if (isDeleteMode) {
            const deleteButton = etaniItemRight.querySelector(`.etaniAVDelete[data-type="${animateType}"]`);
            if (deleteButton) deleteButton.click();
        }
    }

    const originalUseElement = document.querySelector(`.etdrop use[href="#${useElementId}"]`);
    if (!originalUseElement) return;

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

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

        const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useElementId}"]`);
        if (!translateWrapper) return;

        const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useElementId}"]`);
        const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
        
        if (!scaleWrapper || !cloneUseElement) return;

        const cloneBaseTransformString = cloneUseElement.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)'; 
        const currentBaseTransforms = parseTransform(cloneBaseTransformString);
        
        const currentBaseTranslate = currentBaseTransforms.translate;
        const currentBaseScale = currentBaseTransforms.scale;
        const currentBaseRotate = currentBaseTransforms.rotate;

        const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
        const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
        const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
        
        const allAnimates = [...animatesRotate, ...animatesScale, ...animatesTranslate];
        
        const scaleValue = calculateAdditiveValue('scale', originalTransforms.scale, currentBaseScale);
        
        allAnimates.forEach(animate => {
            const type = animate.getAttribute('type').toLowerCase();
            let currentValueString = animate.getAttribute('values') || '';
            let newValue = '';
            
            if (type === 'rotate') {
                newValue = calculateAdditiveValue('rotate', originalTransforms.rotate, currentBaseRotate);
            } else if (type === 'scale') {
                newValue = scaleValue;
            } else if (type === 'translate') {
                newValue = calculateAdditiveValue('translate', originalTransforms.translate, currentBaseTranslate, scaleValue);
            }
            
            animate.setAttribute('values', (currentValueString ? currentValueString + ';' : '') + newValue);
            updateDuration(animate, etaniAnimate, (currentValueString ? currentValueString.split(';').length + 1 : 1));
        });
    } else if (animateType === 'opacity') {
        const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useElementId}"]`);
        if (!cloneUseElement) return;

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

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

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

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

        etaniAV.appendChild(newAVItem);
    }

    updateEtaniResult();
}

// Handle click event for the '-' button to toggle deletion mode
function handleAVDeleteToggle(e, animateType = 'transform') {
    e.preventDefault();
    const deleteButton = e.currentTarget;
    const etaniItemRight = deleteButton.closest('.etaniItemRight');
    const etaniCol = deleteButton.closest('.etaniCol');

    // Exit other modes
    if (isCopyMode || isMoveMode) {
        if (isCopyMode) {
            const copyButton = etaniItemRight.querySelector(`.etaniAVCopy[data-type="${animateType}"]`);
            if (copyButton) copyButton.click();
        }
        if (isMoveMode) {
            const moveButton = etaniItemRight.querySelector(`.etaniAVMove[data-type="${animateType}"]`);
            if (moveButton) moveButton.click();
        }
    }

    isDeleteMode = !isDeleteMode;
    deleteButton.classList.toggle('deleting', isDeleteMode);
    
    const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
    
    avItems.forEach(item => {
        item.classList.toggle('deleting-target', isDeleteMode);
    });

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

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

// Handle click event for the 'Copy' button to toggle copy mode
function handleAVCopyToggle(e, animateType) {
    e.preventDefault();
    const copyButton = e.currentTarget;
    const etaniItemRight = copyButton.closest('.etaniItemRight');
    const etaniCol = copyButton.closest('.etaniCol');

    // Exit other modes
    if (isDeleteMode || isMoveMode) {
        if (isDeleteMode) {
            const deleteButton = etaniItemRight.querySelector(`.etaniAVDelete[data-type="${animateType}"]`);
            if (deleteButton) deleteButton.click();
        }
        if (isMoveMode) {
            const moveButton = etaniItemRight.querySelector(`.etaniAVMove[data-type="${animateType}"]`);
            if (moveButton) moveButton.click();
        }
    }

    isCopyMode = !isCopyMode;
    copyButton.classList.toggle('copying', isCopyMode);
    
    const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
    
    avItems.forEach(item => {
        item.classList.toggle('copying-target', isCopyMode);
    });

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

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

// Handle click event for the 'Move' button to toggle move mode
function handleAVMoveToggle(e, animateType) {
    e.preventDefault();
    const moveButton = e.currentTarget;
    const etaniItemRight = moveButton.closest('.etaniItemRight');
    const etaniCol = moveButton.closest('.etaniCol');

    // Exit other modes
    if (isDeleteMode || isCopyMode) {
        if (isDeleteMode) {
            const deleteButton = etaniItemRight.querySelector(`.etaniAVDelete[data-type="${animateType}"]`);
            if (deleteButton) deleteButton.click();
        }
        if (isCopyMode) {
            const copyButton = etaniItemRight.querySelector(`.etaniAVCopy[data-type="${animateType}"]`);
            if (copyButton) copyButton.click();
        }
    }

    isMoveMode = !isMoveMode;
    moveButton.classList.toggle('moving', isMoveMode);
    
    const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
    
    avItems.forEach(item => {
        item.classList.toggle('moving-target', isMoveMode);
    });

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

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

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

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

    lastClickTime = currentTime;
    lastClickedItem = item;

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

            if (animateType === 'transform') {
                const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useId}"]`);
                if (!translateWrapper) return;
                
                const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useId}"]`);
                const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
                if (!scaleWrapper || !cloneUseElement) return;

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

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

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

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

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

        if (animateType === 'transform') {
            const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useId}"]`);
            if (!translateWrapper) return;
            
            const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useId}"]`);
            const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
            if (!scaleWrapper || !cloneUseElement) return;

            const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
            const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
            const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
            
            const allAnimates = [...animatesTranslate, ...animatesScale, ...animatesRotate];
            
            allAnimates.forEach(animate => {
                const values = animate.getAttribute('values').split(';');
                if (itemIndex < values.length) {
                    values.splice(itemIndex + 1, 0, values[itemIndex]);
                }
                animate.setAttribute('values', values.join(';'));
                updateDuration(animate, etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`), values.length);
            });

            const existingItems = etaniAV.querySelectorAll('.etaniAVItem');
            const existingLetters = Array.from(existingItems).map(item => item.textContent);
            newAVItem.textContent = findFirstMissingLetter(existingLetters);
        } else if (animateType === 'opacity') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return;

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

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

        updateEtaniResult();
        return;
    }

    if (etaniCol.classList.contains(`deleting-mode-${animateType}`)) {
        const deleteButton = etaniItemRight.querySelector(`.etaniAVDelete[data-type="${animateType}"]`);
        
        if (animateType === 'transform') {
            const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useId}"]`);
            if (!translateWrapper) return;
            
            const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useId}"]`);
            const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
            if (!scaleWrapper || !cloneUseElement) return;

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

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

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

        item.remove();

        const remainingItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
        
        if (remainingItems.length === 0) {
            deleteButton.click();
        } else {
            remainingItems.forEach(item => {
                item.classList.add('deleting-target'); 
            });
        }

        if (animateType === 'transform') {
            const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="transform"] .etaniAVItem`);
            const existingLetters = Array.from(avItems).map(item => item.textContent);
            avItems.forEach((item, index) => {
                item.textContent = findFirstMissingLetter(existingLetters.slice(0, index));
            });
        } else if (animateType === 'opacity') {
            const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="opacity"] .etaniAVItem`);
            const values = etaniItemRight.querySelector('animate[attributeName="opacity"]')?.getAttribute('values')?.split(';') || [];
            avItems.forEach((item, index) => {
                item.textContent = values[index] || '1';
            });
        }

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

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

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

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

// Handle click event for the 'Transform' button
function handleAllAppendTransformClick() {
    if (!etani_clone) return;

    const etdropUses = document.querySelectorAll('#etmain .etdrop use');
    const etaniItemRights = document.querySelectorAll('.etaniItemRight');

    const isRepeat = document.querySelector('.etaniSettingRepeat.active');

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

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

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

        const baseAnimate = (type, initialValue) => {
            const animate = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
            animate.setAttribute('attributeName', 'transform');
            animate.setAttribute('attributeType', 'XML');
            animate.setAttribute('type', type);
            animate.setAttribute('values', initialValue);
            animate.setAttribute('fill', isRepeat ? 'remove' : 'freeze');
            if (isRepeat) animate.setAttribute('repeatCount', 'indefinite');
            animate.setAttribute('additive', 'sum');
            return animate;
        };
        
        cloneUseElement.appendChild(baseAnimate('rotate', '0'));
        
        const scaleWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        scaleWrapper.setAttribute('data-use-href-scale', `#${useId}`);
        scaleWrapper.appendChild(baseAnimate('scale', '1'));
        
        const translateWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        translateWrapper.setAttribute('data-use-href', `#${useId}`);
        translateWrapper.appendChild(baseAnimate('translate', '0,0'));
        
        cloneUseElement.parentNode.insertBefore(translateWrapper, cloneUseElement);
        cloneUseElement.parentNode.insertBefore(scaleWrapper, cloneUseElement);
        scaleWrapper.appendChild(cloneUseElement);
        translateWrapper.appendChild(scaleWrapper);

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

        const nameSpan = document.createElement('span');
        nameSpan.className = 'etaniAnimateName';
        nameSpan.textContent = 'transform';
        etaniAnimate.appendChild(nameSpan);

        const durSpan = document.createElement('span');
        durSpan.className = 'etaniAnimateDur';
        durSpan.textContent = 'dur: 0s';
        etaniAnimate.appendChild(durSpan);

        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        const avCtrlDiv = document.createElement('div');
        avCtrlDiv.className = 'etaniAVCtrl';

        const avAddSpan = document.createElement('span');
        avAddSpan.className = 'etaniAVAdd';
        avAddSpan.title = 'Add Transform Value';
        avAddSpan.setAttribute('data-type', 'transform');
        avAddSpan.addEventListener('click', (e) => handleAVAddClick(e, useId, 'transform'));
        
        const plusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        plusSvg.setAttribute('width', '24');
        plusSvg.setAttribute('height', '24');
        plusSvg.setAttribute('viewBox', '0 0 24 24');
        plusSvg.setAttribute('fill', 'none');
        plusSvg.setAttribute('stroke', 'currentColor');
        plusSvg.setAttribute('stroke-width', '1');
        plusSvg.setAttribute('stroke-linecap', 'round');
        plusSvg.setAttribute('stroke-linejoin', 'round');
        plusSvg.innerHTML = '<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>';
        avAddSpan.appendChild(plusSvg);

        const avDeleteSpan = document.createElement('span');
        avDeleteSpan.className = 'etaniAVDelete';
        avDeleteSpan.title = 'Delete Value';
        avDeleteSpan.setAttribute('data-type', 'transform');
        avDeleteSpan.addEventListener('click', (e) => handleAVDeleteToggle(e, 'transform'));

        const minusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        minusSvg.setAttribute('width', '24');
        minusSvg.setAttribute('height', '24');
        minusSvg.setAttribute('viewBox', '0 0 24 24');
        minusSvg.setAttribute('fill', 'none');
        minusSvg.setAttribute('stroke', 'currentColor');
        minusSvg.setAttribute('stroke-width', '1');
        minusSvg.setAttribute('stroke-linecap', 'round');
        minusSvg.setAttribute('stroke-linejoin', 'round');
        minusSvg.innerHTML = '<line x1="5" y1="12" x2="19" y2="12"></line>';
        avDeleteSpan.appendChild(minusSvg);

        const avCopySpan = document.createElement('span');
        avCopySpan.className = 'etaniAVCopy';
        avCopySpan.title = 'Copy Value';
        avCopySpan.setAttribute('data-type', 'transform');
        avCopySpan.addEventListener('click', (e) => handleAVCopyToggle(e, 'transform'));

        const copySvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        copySvg.setAttribute('width', '24');
        copySvg.setAttribute('height', '24');
        copySvg.setAttribute('viewBox', '0 0 24 24');
        copySvg.setAttribute('fill', 'none');
        copySvg.setAttribute('stroke', 'currentColor');
        copySvg.setAttribute('stroke-width', '1');
        copySvg.setAttribute('stroke-linecap', 'round');
        copySvg.setAttribute('stroke-linejoin', 'round');
        copySvg.innerHTML = '<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>';
        avCopySpan.appendChild(copySvg);

        const avMoveSpan = document.createElement('span');
        avMoveSpan.className = 'etaniAVMove';
        avMoveSpan.title = 'Move Value';
        avMoveSpan.setAttribute('data-type', 'transform');
        avMoveSpan.addEventListener('click', (e) => handleAVMoveToggle(e, 'transform'));

        const moveSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        moveSvg.setAttribute('width', '24');
        moveSvg.setAttribute('height', '24');
        moveSvg.setAttribute('viewBox', '0 0 24 24');
        moveSvg.setAttribute('fill', 'none');
        moveSvg.setAttribute('stroke', 'currentColor');
        moveSvg.setAttribute('stroke-width', '1');
        moveSvg.setAttribute('stroke-linecap', 'round');
        moveSvg.setAttribute('stroke-linejoin', 'round');
        moveSvg.innerHTML = '<polyline points="3 8 9 2 15 8"></polyline><polyline points="9 22 15 16 21 22"></polyline>';
        avMoveSpan.appendChild(moveSvg);
        
        avCtrlDiv.appendChild(avAddSpan);
        avCtrlDiv.appendChild(avDeleteSpan);
        avCtrlDiv.appendChild(avCopySpan);
        avCtrlDiv.appendChild(avMoveSpan);

        const avLabelSpan = document.createElement('span');
        avLabelSpan.className = 'etaniAVLabel';
        avLabelSpan.textContent = 'values : ';

        const avDiv = document.createElement('div');
        avDiv.className = 'etaniAV';
        
        const avItemSpan = document.createElement('span');
        avItemSpan.className = 'etaniAVItem';
        avItemSpan.textContent = 'a';
        avItemSpan.addEventListener('click', (e) => handleAVItemClick(e, 'transform'));
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avCtrlDiv);
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

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

    updateEtaniResult();
}

// Handle click event for the 'Opacity' button
function handleAllAppendOpacityClick() {
    if (!etani_clone) return;

    const etdropUses = document.querySelectorAll('#etmain .etdrop use');
    const etaniItemRights = document.querySelectorAll('.etaniItemRight');

    const isRepeat = document.querySelector('.etaniSettingRepeat.active');

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

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

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

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

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

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

        const durSpan = document.createElement('span');
        durSpan.className = 'etaniAnimateDur';
        durSpan.textContent = 'dur: 0s';
        etaniAnimate.appendChild(durSpan);

        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        const avCtrlDiv = document.createElement('div');
        avCtrlDiv.className = 'etaniAVCtrl';

        const avAddSpan = document.createElement('span');
        avAddSpan.className = 'etaniAVAdd';
        avAddSpan.title = 'Add Opacity Value';
        avAddSpan.setAttribute('data-type', 'opacity');
        avAddSpan.addEventListener('click', (e) => handleAVAddClick(e, useId, 'opacity'));
        
        const plusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        plusSvg.setAttribute('width', '24');
        plusSvg.setAttribute('height', '24');
        plusSvg.setAttribute('viewBox', '0 0 24 24');
        plusSvg.setAttribute('fill', 'none');
        plusSvg.setAttribute('stroke', 'currentColor');
        plusSvg.setAttribute('stroke-width', '1');
        plusSvg.setAttribute('stroke-linecap', 'round');
        plusSvg.setAttribute('stroke-linejoin', 'round');
        plusSvg.innerHTML = '<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>';
        avAddSpan.appendChild(plusSvg);

        const avDeleteSpan = document.createElement('span');
        avDeleteSpan.className = 'etaniAVDelete';
        avDeleteSpan.title = 'Delete Value';
        avDeleteSpan.setAttribute('data-type', 'opacity');
        avDeleteSpan.addEventListener('click', (e) => handleAVDeleteToggle(e, 'opacity'));

        const minusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        minusSvg.setAttribute('width', '24');
        minusSvg.setAttribute('height', '24');
        minusSvg.setAttribute('viewBox', '0 0 24 24');
        minusSvg.setAttribute('fill', 'none');
        minusSvg.setAttribute('stroke', 'currentColor');
        minusSvg.setAttribute('stroke-width', '1');
        minusSvg.setAttribute('stroke-linecap', 'round');
        minusSvg.setAttribute('stroke-linejoin', 'round');
        minusSvg.innerHTML = '<line x1="5" y1="12" x2="19" y2="12"></line>';
        avDeleteSpan.appendChild(minusSvg);

        const avCopySpan = document.createElement('span');
        avCopySpan.className = 'etaniAVCopy';
        avCopySpan.title = 'Copy Value';
        avCopySpan.setAttribute('data-type', 'opacity');
        avCopySpan.addEventListener('click', (e) => handleAVCopyToggle(e, 'opacity'));

        const copySvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        copySvg.setAttribute('width', '24');
        copySvg.setAttribute('height', '24');
        copySvg.setAttribute('viewBox', '0 0 24 22');
        copySvg.setAttribute('fill', 'none');
        copySvg.setAttribute('stroke', 'currentColor');
        copySvg.setAttribute('stroke-width', '1');
        copySvg.setAttribute('stroke-linecap', 'round');
        copySvg.setAttribute('stroke-linejoin', 'round');
        copySvg.innerHTML = '<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>';
        avCopySpan.appendChild(copySvg);

        const avMoveSpan = document.createElement('span');
        avMoveSpan.className = 'etaniAVMove';
        avMoveSpan.title = 'Move Value';
        avMoveSpan.setAttribute('data-type', 'opacity');
        avMoveSpan.addEventListener('click', (e) => handleAVMoveToggle(e, 'opacity'));

        const moveSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        moveSvg.setAttribute('width', '24');
        moveSvg.setAttribute('height', '24');
        moveSvg.setAttribute('viewBox', '0 0 24 24');
        moveSvg.setAttribute('fill', 'none');
        moveSvg.setAttribute('stroke', 'currentColor');
        moveSvg.setAttribute('stroke-width', '1');
        moveSvg.setAttribute('stroke-linecap', 'round');
        moveSvg.setAttribute('stroke-linejoin', 'round');
        moveSvg.innerHTML = '<polyline points="3 8 9 2 15 8"></polyline><polyline points="9 22 15 16 21 22"></polyline>';
        avMoveSpan.appendChild(moveSvg);
        
        avCtrlDiv.appendChild(avAddSpan);
        avCtrlDiv.appendChild(avDeleteSpan);
        avCtrlDiv.appendChild(avCopySpan);
        avCtrlDiv.appendChild(avMoveSpan);

        const avLabelSpan = document.createElement('span');
        avLabelSpan.className = 'etaniAVLabel';
        avLabelSpan.textContent = 'values : ';

        const avDiv = document.createElement('div');
        avDiv.className = 'etaniAV';
        
        const avItemSpan = document.createElement('span');
        avItemSpan.className = 'etaniAVItem';
        avItemSpan.textContent = '1';
        avItemSpan.addEventListener('click', (e) => handleAVItemClick(e, 'opacity'));
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avCtrlDiv);
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

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

    updateEtaniResult();
}

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

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

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

    allAnimates.forEach(animate => {
        const values = animate.getAttribute('values')?.split(';') || [];
        if (values.length < maxValuesLength && values.length > 0) {
            const lastValue = values[values.length - 1];
            while (values.length < maxValuesLength) {
                values.push(lastValue);
            }
            animate.setAttribute('values', values.join(';'));
        }
    });

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

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

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

            for (let i = currentValuesLength; i < maxValuesLength; i++) {
                const newAVItem = document.createElement('span');
                newAVItem.className = 'etaniAVItem';
                newAVItem.addEventListener('click', (e) => handleAVItemClick(e, animateType));
                if (animateType === 'transform') {
                    const existingLetters = Array.from(currentItems).map(item => item.textContent);
                    newAVItem.textContent = findFirstMissingLetter(existingLetters);
                } else if (animateType === 'opacity') {
                    newAVItem.textContent = lastValue;
                }
                etaniAV.appendChild(newAVItem);
            }
        });
    });

    updateEtaniResult();
}

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

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

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

    document.querySelector('.etaniSettingFreeze').classList.toggle('active', !isRepeat);
    document.querySelector('.etaniSettingRepeat').classList.toggle('active', isRepeat);

    updateEtaniResult();
}

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

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

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

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

    const etaniSetting = document.createElement('div');
    etaniSetting.className = 'etaniSetting';
    
    const repeatRadio = document.createElement('span');
    repeatRadio.className = 'etaniSettingMode etaniSettingRepeat active';
    repeatRadio.textContent = 'Repeat';
    repeatRadio.setAttribute('data-mode', 'repeat');
    repeatRadio.addEventListener('click', () => handleSettingModeChange('repeat'));
    
    const freezeRadio = document.createElement('span');
    freezeRadio.className = 'etaniSettingMode etaniSettingFreeze';
    freezeRadio.textContent = 'Freeze';
    freezeRadio.setAttribute('data-mode', 'freeze');
    freezeRadio.addEventListener('click', () => handleSettingModeChange('freeze'));

    etaniSetting.appendChild(repeatRadio);
    etaniSetting.appendChild(freezeRadio);
    etaniCtrl.appendChild(etaniSetting);

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

    const transformButton = document.createElement('button');
    transformButton.className = 'etaniAllAppendTransform';
    transformButton.textContent = 'transform';
    transformButton.addEventListener('click', handleAllAppendTransformClick);
    
    const opacityButton = document.createElement('button');
    opacityButton.className = 'etaniAllAppendOpacity';
    opacityButton.textContent = 'opacity';
    opacityButton.addEventListener('click', handleAllAppendOpacityClick);
    
    etaniAllAppend.appendChild(transformButton);
    etaniAllAppend.appendChild(opacityButton);
    etaniCtrl.appendChild(etaniAllAppend);

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

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

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

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

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

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

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

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

    etaniouter.appendChild(etaniinner);

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

    etdropUses.forEach((useElement, i) => {
        const tileid = useElement.getAttribute('href').substring(1);
        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 originalTile = document.querySelector(`defs g#${tileid}`);
        
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';

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

        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        
        if (originalTile) {
            const tileclone = originalTile.cloneNode(true);
            tileclone.removeAttribute('id');
            if (etwaittransform) tileclone.setAttribute('transform', etwaittransform);
            if (etwaitfill) tileclone.setAttribute('fill', etwaitfill);
            if (etwaitstroke) tileclone.setAttribute('stroke', etwaitstroke);
            if (etwaitstrokeWidth) tileclone.setAttribute('stroke-width', etwaitstrokeWidth);
            
            const svgWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            svgWrapper.setAttribute('width', '40');
            svgWrapper.setAttribute('height', '40');
            svgWrapper.setAttribute('version', '1.1');
            svgWrapper.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
            svgWrapper.setAttribute('xmlns:svg', 'http://www.w3.org/2000/svg');
            svgWrapper.className = 'etanitileimg';
            
            svgWrapper.appendChild(tileclone);
            etaniItemLeft.appendChild(svgWrapper);
        }

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

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

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

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

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

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

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

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

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

帖子 ejsoon »

續前:圖標不滿意,copy不用改字母

代码: 全选

改動:

一,etaniAVMove換個圖標,圖案是上下兩個箭頭,上面的橫向朝左,下面的橫向朝右。

二,當複製或刪除使etaniAVItem的數量發生變化時,dur也要一起變化。

三,etaniAVMove在移動之後,不要使etaniAVItem的內容發生變化。

四,在data-type="transform"中的etaniAVItem的字母可能會因為複製、移動、刪除而變化。

這時如果點擊etaniAVAdd,它所新增的etaniAVItem內的英文,是從a開始的字母序中第一個缺失的字母。

比如現在的etaniAVItem有「ahbedg」,則點擊etaniAVAdd後字母應為「ahbedgc」,因為c是第一個缺失的字母。

如果點擊etaniAVCopy,它會直接複製etaniAVItem的字母。比如現在的etaniAVItem有「ahbedg」,當處於複製狀態並點擊b時,就變成「ahbbedg」。

五,在etaniAVAdd、etaniAVCopy、etaniAVMove中,如果進入了某個狀態,之後又點擊另一個,則直接切換到另一個狀態。

代碼中所有的注釋都要使用英文。

代码: 全选

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

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

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

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

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

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

        /* Styles for etaniResult */
        .etaniResult {
            text-align: center;
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #bbb;
            overflow: auto;
        }
        
        /* Control and button styles */
        .etaniContent a, .etaniValueCtrl a {
            display: inline-block;
            margin: 0 5px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 14px;
        }
        
        .etaniCenter {
            border: 1px solid green;
            color: green;
        }
        
        .etaniContentHTML {
            border: 1px solid #0099ff;
            color: #0099ff;
            margin-right: 15px;
        }

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

        /* Styles for etaniAllAppend buttons */
        .etaniAllAppend button {
            padding: 5px 10px;
            font-size: 16px;
            margin: 0 5px;
            cursor: pointer;
            border: 1px solid #333;
            background-color: #fff;
        }
        
        /* Custom radio button styles */
        .etaniSettingMode {
            display: inline-block;
            cursor: pointer;
            padding: 4px 8px;
            margin: 0 3px;
            font-size: 14px;
            border: 1px solid #888;
            background-color: #eee;
            color: #333;
            user-select: none;
        }

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

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

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

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

        .etaniResultImage {
            display: block;
            max-width: 480px;
            width: 100%;
            height: auto;
            margin: 0 auto 10px auto;
            border: 1px solid #000;
        }
        
        .etaniResultSize {
            display: inline-block;
            margin-left: 10px;
            font-size: 12px; 
            color: #555;
        }
        
        /* etaniItem structure */
        .etaniItem {
            min-height: 48px;
            border: 1px solid #ccc;
            box-sizing: border-box;
            width: 100%;
            margin-bottom: -1px;
            overflow: auto;
        }
        
        .etaniItemLeft {
            float: left; 
            width: 60px;
            min-height: 48px;
            border-right: 1px solid #ccc;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            background-color: #e8e8e8;
            padding: 2px 0;
        }
        
        .etaniItemRight {
            margin-left: 60px; 
            padding: 7px;
            min-height: 48px; 
            background-color: #fff;
        }

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

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

        .etaniAnimateDur {
            display: inline-block;
            margin-right: 10px;
            font-size: 14px;
        }

        .etaniAnimateValue {
            margin-top: 5px;
        }

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

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

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

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

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

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

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

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

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

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

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

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

    document.head.appendChild(styleSheet);
}

// Generate the next letter for transform etaniAVItem labels
function getNextLetter(currentLetter) {
    if (!currentLetter) return 'a';
    const code = currentLetter.charCodeAt(0);
    if (code >= 97 && code < 122) { // lowercase a-y
        return String.fromCharCode(code + 1);
    } else if (code === 122) { // z -> A
        return 'A';
    } else if (code >= 65 && code < 90) { // uppercase A-Y
        return String.fromCharCode(code + 1);
    } else if (code === 90) { // Z -> a (loop back)
        return 'a';
    }
    return 'a';
}

// Find the first missing letter in the sequence for transform etaniAVItems
function findFirstMissingLetter(items) {
    const letters = Array.from(items).map(item => item.textContent);
    let currentLetter = 'a';
    while (letters.includes(currentLetter)) {
        currentLetter = getNextLetter(currentLetter);
    }
    return currentLetter;
}

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

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

    const transform = {};

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

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

    return transform;
}

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

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

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

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

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

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

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

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

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

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

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

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

// Update duration based on the number of etaniAVItems
function updateDuration(etaniItemRight, animateType, useId) {
    const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
    const newDur = avItems.length - 1;

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

        const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useId}"]`);
        const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
        if (!scaleWrapper || !cloneUseElement) return;

        const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
        const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
        const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
        
        const allAnimates = [...animatesTranslate, ...animatesScale, ...animatesRotate];
        
        const isRepeat = document.querySelector('.etaniSettingRepeat.active');
        
        allAnimates.forEach(animate => {
            if (newDur > 0) {
                animate.setAttribute('dur', `${newDur}s`);
                if (isRepeat) {
                    animate.removeAttribute('fill');
                    animate.setAttribute('repeatCount', 'indefinite');
                } else {
                    animate.removeAttribute('repeatCount');
                    animate.setAttribute('fill', 'freeze');
                }
            } else {
                animate.removeAttribute('dur');
                animate.removeAttribute('fill');
                animate.removeAttribute('repeatCount');
            }
        });
    } else if (animateType === 'opacity') {
        const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;

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

        const isRepeat = document.querySelector('.etaniSettingRepeat.active');

        if (newDur > 0) {
            animateOpacity.setAttribute('dur', `${newDur}s`);
            if (isRepeat) {
                animateOpacity.removeAttribute('fill');
                animateOpacity.setAttribute('repeatCount', 'indefinite');
            } else {
                animateOpacity.removeAttribute('repeatCount');
                animateOpacity.setAttribute('fill', 'freeze');
            }
        } else {
            animateOpacity.removeAttribute('dur');
            animateOpacity.removeAttribute('fill');
            animateOpacity.removeAttribute('repeatCount');
        }
    }

    etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"] .etaniAnimateDur`).textContent = `dur: ${newDur}s`;
}

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

    // Exit other modes
    if (isCopyMode || isMoveMode || isDeleteMode) {
        const etaniItemRight = e.currentTarget.closest('.etaniItemRight');
        const etaniCol = e.currentTarget.closest('.etaniCol');
        if (isCopyMode) {
            const copyButton = etaniItemRight.querySelector(`.etaniAVCopy[data-type="${animateType}"]`);
            if (copyButton) copyButton.click();
        }
        if (isMoveMode) {
            const moveButton = etaniItemRight.querySelector(`.etaniAVMove[data-type="${animateType}"]`);
            if (moveButton) moveButton.click();
        }
        if (isDeleteMode) {
            const deleteButton = etaniItemRight.querySelector(`.etaniAVDelete[data-type="${animateType}"]`);
            if (deleteButton) deleteButton.click();
        }
    }

    const originalUseElement = document.querySelector(`.etdrop use[href="#${useElementId}"]`);
    if (!originalUseElement) return;

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

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

        const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useElementId}"]`);
        if (!translateWrapper) return;

        const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useElementId}"]`);
        const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
        
        if (!scaleWrapper || !cloneUseElement) return;

        const cloneBaseTransformString = cloneUseElement.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)'; 
        const currentBaseTransforms = parseTransform(cloneBaseTransformString);
        
        const currentBaseTranslate = currentBaseTransforms.translate;
        const currentBaseScale = currentBaseTransforms.scale;
        const currentBaseRotate = currentBaseTransforms.rotate;

        const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
        const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
        const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
        
        const allAnimates = [...animatesRotate, ...animatesScale, ...animatesTranslate];
        
        const scaleValue = calculateAdditiveValue('scale', originalTransforms.scale, currentBaseScale);
        
        allAnimates.forEach(animate => {
            const type = animate.getAttribute('type').toLowerCase();
            let currentValueString = animate.getAttribute('values') || '';
            let newValue = '';
            
            if (type === 'rotate') {
                newValue = calculateAdditiveValue('rotate', originalTransforms.rotate, currentBaseRotate);
            } else if (type === 'scale') {
                newValue = scaleValue;
            } else if (type === 'translate') {
                newValue = calculateAdditiveValue('translate', originalTransforms.translate, currentBaseTranslate, scaleValue);
            }
            
            animate.setAttribute('values', (currentValueString ? currentValueString + ';' : '') + newValue);
        });
    } else if (animateType === 'opacity') {
        const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useElementId}"]`);
        if (!cloneUseElement) return;

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

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

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

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

        etaniAV.appendChild(newAVItem);
    }

    // Update duration based on new item count
    updateDuration(etaniItemRight, animateType, useElementId);

    updateEtaniResult();
}

// Handle click event for the '-' button to toggle deletion mode
function handleAVDeleteToggle(e, animateType = 'transform') {
    e.preventDefault();
    const deleteButton = e.currentTarget;
    const etaniItemRight = deleteButton.closest('.etaniItemRight');
    const etaniCol = deleteButton.closest('.etaniCol');

    // Exit other modes
    if (isCopyMode || isMoveMode) {
        if (isCopyMode) {
            const copyButton = etaniItemRight.querySelector(`.etaniAVCopy[data-type="${animateType}"]`);
            if (copyButton) copyButton.click();
        }
        if (isMoveMode) {
            const moveButton = etaniItemRight.querySelector(`.etaniAVMove[data-type="${animateType}"]`);
            if (moveButton) moveButton.click();
        }
    }

    isDeleteMode = !isDeleteMode;
    deleteButton.classList.toggle('deleting', isDeleteMode);
    
    const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
    
    avItems.forEach(item => {
        item.classList.toggle('deleting-target', isDeleteMode);
    });

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

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

// Handle click event for the 'Copy' button to toggle copy mode
function handleAVCopyToggle(e, animateType) {
    e.preventDefault();
    const copyButton = e.currentTarget;
    const etaniItemRight = copyButton.closest('.etaniItemRight');
    const etaniCol = copyButton.closest('.etaniCol');

    // Exit other modes
    if (isDeleteMode || isMoveMode) {
        if (isDeleteMode) {
            const deleteButton = etaniItemRight.querySelector(`.etaniAVDelete[data-type="${animateType}"]`);
            if (deleteButton) deleteButton.click();
        }
        if (isMoveMode) {
            const moveButton = etaniItemRight.querySelector(`.etaniAVMove[data-type="${animateType}"]`);
            if (moveButton) moveButton.click();
        }
    }

    isCopyMode = !isCopyMode;
    copyButton.classList.toggle('copying', isCopyMode);
    
    const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
    
    avItems.forEach(item => {
        item.classList.toggle('copying-target', isCopyMode);
    });

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

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

// Handle click event for the 'Move' button to toggle move mode
function handleAVMoveToggle(e, animateType) {
    e.preventDefault();
    const moveButton = e.currentTarget;
    const etaniItemRight = moveButton.closest('.etaniItemRight');
    const etaniCol = moveButton.closest('.etaniCol');

    // Exit other modes
    if (isDeleteMode || isCopyMode) {
        if (isDeleteMode) {
            const deleteButton = etaniItemRight.querySelector(`.etaniAVDelete[data-type="${animateType}"]`);
            if (deleteButton) deleteButton.click();
        }
        if (isCopyMode) {
            const copyButton = etaniItemRight.querySelector(`.etaniAVCopy[data-type="${animateType}"]`);
            if (copyButton) copyButton.click();
        }
    }

    isMoveMode = !isMoveMode;
    moveButton.classList.toggle('moving', isMoveMode);
    
    const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
    
    avItems.forEach(item => {
        item.classList.toggle('moving-target', isMoveMode);
    });

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

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

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

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

    lastClickTime = currentTime;
    lastClickedItem = item;

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

            if (animateType === 'transform') {
                const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useId}"]`);
                if (!translateWrapper) return;
                
                const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useId}"]`);
                const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
                if (!scaleWrapper || !cloneUseElement) return;

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

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

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

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

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

        if (animateType === 'transform') {
            const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useId}"]`);
            if (!translateWrapper) return;
            
            const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useId}"]`);
            const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
            if (!scaleWrapper || !cloneUseElement) return;

            const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
            const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
            const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
            
            const allAnimates = [...animatesTranslate, ...animatesScale, ...animatesRotate];
            
            allAnimates.forEach(animate => {
                const values = animate.getAttribute('values').split(';');
                if (itemIndex < values.length) {
                    values.splice(itemIndex + 1, 0, values[itemIndex]);
                }
                animate.setAttribute('values', values.join(';'));
            });
        } else if (animateType === 'opacity') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return;

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

            const values = animateOpacity.getAttribute('values').split(';');
            if (itemIndex < values.length) {
                values.splice(itemIndex + 1, 0, values[itemIndex]);
            }
            animateOpacity.setAttribute('values', values.join(';'));
        }

        updateDuration(etaniItemRight, animateType, useId);
        updateEtaniResult();
        return;
    }

    if (etaniCol.classList.contains(`deleting-mode-${animateType}`)) {
        const deleteButton = etaniItemRight.querySelector(`.etaniAVDelete[data-type="${animateType}"]`);
        
        if (animateType === 'transform') {
            const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useId}"]`);
            if (!translateWrapper) return;
            
            const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useId}"]`);
            const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
            if (!scaleWrapper || !cloneUseElement) return;

            const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
            const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
            const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
            
            const allAnimates = [...animatesTranslate, ...animatesScale, ...animatesRotate];
            
            allAnimates.forEach(animate => {
                const values = animate.getAttribute('values').split(';');
                
                if (itemIndex < values.length) {
                    values.splice(itemIndex, 1);
                }
                animate.setAttribute('values', values.join(';'));
            });
        } else if (animateType === 'opacity') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return;

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

            const values = animateOpacity.getAttribute('values').split(';');
            if (itemIndex < values.length) {
                values.splice(itemIndex, 1);
            }
            animateOpacity.setAttribute('values', values.join(';'));
        }

        item.remove();

        const remainingItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
        
        if (remainingItems.length === 0) {
            deleteButton.click();
        } else if (animateType === 'transform') {
            const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="transform"] .etaniAVItem`);
            avItems.forEach((item, index) => {
                item.textContent = findFirstMissingLetter(Array.from(avItems).slice(0, index));
            });
            remainingItems.forEach(item => {
                item.classList.add('deleting-target'); 
            });
        } else if (animateType === 'opacity') {
            const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="opacity"] .etaniAVItem`);
            const values = etaniItemRight.querySelector('animate[attributeName="opacity"]')?.getAttribute('values')?.split(';') || [];
            avItems.forEach((item, index) => {
                item.textContent = values[index] || '1';
            });
            remainingItems.forEach(item => {
                item.classList.add('deleting-target'); 
            });
        }

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

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

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

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

// Handle click event for the 'Transform' button
function handleAllAppendTransformClick() {
    if (!etani_clone) return;

    const etdropUses = document.querySelectorAll('#etmain .etdrop use');
    const etaniItemRights = document.querySelectorAll('.etaniItemRight');

    const isRepeat = document.querySelector('.etaniSettingRepeat.active');

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

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

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

        const baseAnimate = (type, initialValue) => {
            const animate = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
            animate.setAttribute('attributeName', 'transform');
            animate.setAttribute('attributeType', 'XML');
            animate.setAttribute('type', type);
            animate.setAttribute('values', initialValue);
            animate.setAttribute('fill', isRepeat ? 'remove' : 'freeze');
            if (isRepeat) animate.setAttribute('repeatCount', 'indefinite');
            animate.setAttribute('additive', 'sum');
            return animate;
        };
        
        cloneUseElement.appendChild(baseAnimate('rotate', '0'));
        
        const scaleWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        scaleWrapper.setAttribute('data-use-href-scale', `#${useId}`);
        scaleWrapper.appendChild(baseAnimate('scale', '1'));
        
        const translateWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        translateWrapper.setAttribute('data-use-href', `#${useId}`);
        translateWrapper.appendChild(baseAnimate('translate', '0,0'));
        
        cloneUseElement.parentNode.insertBefore(translateWrapper, cloneUseElement);
        cloneUseElement.parentNode.insertBefore(scaleWrapper, cloneUseElement);
        scaleWrapper.appendChild(cloneUseElement);
        translateWrapper.appendChild(scaleWrapper);

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

        const nameSpan = document.createElement('span');
        nameSpan.className = 'etaniAnimateName';
        nameSpan.textContent = 'transform';
        etaniAnimate.appendChild(nameSpan);

        const durSpan = document.createElement('span');
        durSpan.className = 'etaniAnimateDur';
        durSpan.textContent = 'dur: 0s';
        etaniAnimate.appendChild(durSpan);

        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        const avCtrlDiv = document.createElement('div');
        avCtrlDiv.className = 'etaniAVCtrl';

        const avAddSpan = document.createElement('span');
        avAddSpan.className = 'etaniAVAdd';
        avAddSpan.title = 'Add Transform Value';
        avAddSpan.setAttribute('data-type', 'transform');
        avAddSpan.addEventListener('click', (e) => handleAVAddClick(e, useId, 'transform'));
        
        const plusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        plusSvg.setAttribute('width', '24');
        plusSvg.setAttribute('height', '24');
        plusSvg.setAttribute('viewBox', '0 0 24 24');
        plusSvg.setAttribute('fill', 'none');
        plusSvg.setAttribute('stroke', 'currentColor');
        plusSvg.setAttribute('stroke-width', '1');
        plusSvg.setAttribute('stroke-linecap', 'round');
        plusSvg.setAttribute('stroke-linejoin', 'round');
        plusSvg.innerHTML = '<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>';
        avAddSpan.appendChild(plusSvg);

        const avDeleteSpan = document.createElement('span');
        avDeleteSpan.className = 'etaniAVDelete';
        avDeleteSpan.title = 'Delete Value';
        avDeleteSpan.setAttribute('data-type', 'transform');
        avDeleteSpan.addEventListener('click', (e) => handleAVDeleteToggle(e, 'transform'));

        const minusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        minusSvg.setAttribute('width', '24');
        minusSvg.setAttribute('height', '24');
        minusSvg.setAttribute('viewBox', '0 0 24 24');
        minusSvg.setAttribute('fill', 'none');
        minusSvg.setAttribute('stroke', 'currentColor');
        minusSvg.setAttribute('stroke-width', '1');
        minusSvg.setAttribute('stroke-linecap', 'round');
        minusSvg.setAttribute('stroke-linejoin', 'round');
        minusSvg.innerHTML = '<line x1="5" y1="12" x2="19" y2="12"></line>';
        avDeleteSpan.appendChild(minusSvg);

        const avCopySpan = document.createElement('span');
        avCopySpan.className = 'etaniAVCopy';
        avCopySpan.title = 'Copy Value';
        avCopySpan.setAttribute('data-type', 'transform');
        avCopySpan.addEventListener('click', (e) => handleAVCopyToggle(e, 'transform'));

        const copySvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        copySvg.setAttribute('width', '24');
        copySvg.setAttribute('height', '24');
        copySvg.setAttribute('viewBox', '0 0 24 24');
        copySvg.setAttribute('fill', 'none');
        copySvg.setAttribute('stroke', 'currentColor');
        copySvg.setAttribute('stroke-width', '1');
        copySvg.setAttribute('stroke-linecap', 'round');
        copySvg.setAttribute('stroke-linejoin', 'round');
        copySvg.innerHTML = '<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>';
        avCopySpan.appendChild(copySvg);

        const avMoveSpan = document.createElement('span');
        avMoveSpan.className = 'etaniAVMove';
        avMoveSpan.title = 'Move Value';
        avMoveSpan.setAttribute('data-type', 'transform');
        avMoveSpan.addEventListener('click', (e) => handleAVMoveToggle(e, 'transform'));

        const moveSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        moveSvg.setAttribute('width', '24');
        moveSvg.setAttribute('height', '24');
        moveSvg.setAttribute('viewBox', '0 0 24 24');
        moveSvg.setAttribute('fill', 'none');
        moveSvg.setAttribute('stroke', 'currentColor');
        moveSvg.setAttribute('stroke-width', '1');
        moveSvg.setAttribute('stroke-linecap', 'round');
        moveSvg.setAttribute('stroke-linejoin', 'round');
        moveSvg.innerHTML = '<polyline points="3 8 7 4 11 8"></polyline><polyline points="13 16 17 20 21 16"></polyline>';
        avMoveSpan.appendChild(moveSvg);
        
        avCtrlDiv.appendChild(avAddSpan);
        avCtrlDiv.appendChild(avDeleteSpan);
        avCtrlDiv.appendChild(avCopySpan);
        avCtrlDiv.appendChild(avMoveSpan);

        const avLabelSpan = document.createElement('span');
        avLabelSpan.className = 'etaniAVLabel';
        avLabelSpan.textContent = 'values : ';

        const avDiv = document.createElement('div');
        avDiv.className = 'etaniAV';
        
        const avItemSpan = document.createElement('span');
        avItemSpan.className = 'etaniAVItem';
        avItemSpan.textContent = 'a';
        avItemSpan.addEventListener('click', (e) => handleAVItemClick(e, 'transform'));
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avCtrlDiv);
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

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

    updateEtaniResult();
}

// Handle click event for the 'Opacity' button
function handleAllAppendOpacityClick() {
    if (!etani_clone) return;

    const etdropUses = document.querySelectorAll('#etmain .etdrop use');
    const etaniItemRights = document.querySelectorAll('.etaniItemRight');

    const isRepeat = document.querySelector('.etaniSettingRepeat.active');

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

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

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

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

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

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

        const durSpan = document.createElement('span');
        durSpan.className = 'etaniAnimateDur';
        durSpan.textContent = 'dur: 0s';
        etaniAnimate.appendChild(durSpan);

        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        const avCtrlDiv = document.createElement('div');
        avCtrlDiv.className = 'etaniAVCtrl';

        const avAddSpan = document.createElement('span');
        avAddSpan.className = 'etaniAVAdd';
        avAddSpan.title = 'Add Opacity Value';
        avAddSpan.setAttribute('data-type', 'opacity');
        avAddSpan.addEventListener('click', (e) => handleAVAddClick(e, useId, 'opacity'));
        
        const plusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        plusSvg.setAttribute('width', '24');
        plusSvg.setAttribute('height', '24');
        plusSvg.setAttribute('viewBox', '0 0 24 24');
        plusSvg.setAttribute('fill', 'none');
        plusSvg.setAttribute('stroke', 'currentColor');
        plusSvg.setAttribute('stroke-width', '1');
        plusSvg.setAttribute('stroke-linecap', 'round');
        plusSvg.setAttribute('stroke-linejoin', 'round');
        plusSvg.innerHTML = '<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>';
        avAddSpan.appendChild(plusSvg);

        const avDeleteSpan = document.createElement('span');
        avDeleteSpan.className = 'etaniAVDelete';
        avDeleteSpan.title = 'Delete Value';
        avDeleteSpan.setAttribute('data-type', 'opacity');
        avDeleteSpan.addEventListener('click', (e) => handleAVDeleteToggle(e, 'opacity'));

        const minusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        minusSvg.setAttribute('width', '24');
        minusSvg.setAttribute('height', '24');
        minusSvg.setAttribute('viewBox', '0 0 24 24');
        minusSvg.setAttribute('fill', 'none');
        minusSvg.setAttribute('stroke', 'currentColor');
        minusSvg.setAttribute('stroke-width', '1');
        minusSvg.setAttribute('stroke-linecap', 'round');
        minusSvg.setAttribute('stroke-linejoin', 'round');
        minusSvg.innerHTML = '<line x1="5" y1="12" x2="19" y2="12"></line>';
        avDeleteSpan.appendChild(minusSvg);

        const avCopySpan = document.createElement('span');
        avCopySpan.className = 'etaniAVCopy';
        avCopySpan.title = 'Copy Value';
        avCopySpan.setAttribute('data-type', 'opacity');
        avCopySpan.addEventListener('click', (e) => handleAVCopyToggle(e, 'opacity'));

        const copySvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        copySvg.setAttribute('width', '24');
        copySvg.setAttribute('height', '24');
        copySvg.setAttribute('viewBox', '0 0 24 5100
        copySvg.setAttribute('fill', 'none');
        copySvg.setAttribute('stroke', 'currentColor');
        copySvg.setAttribute('stroke-width', '1');
        copySvg.setAttribute('stroke-linecap', 'round');
        copySvg.setAttribute('stroke-linejoin', 'round');
        copySvg.innerHTML = '<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>';
        avCopySpan.appendChild(copySvg);

        const avMoveSpan = document.createElement('span');
        avMoveSpan.className = 'etaniAVMove';
        avMoveSpan.title = 'Move Value';
        avMoveSpan.setAttribute('data-type', 'opacity');
        avMoveSpan.addEventListener('click', (e) => handleAVMoveToggle(e, 'opacity'));

        const moveSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        moveSvg.setAttribute('width', '24');
        moveSvg.setAttribute('height', '24');
        moveSvg.setAttribute('viewBox', '0 0 24 24');
        moveSvg.setAttribute('fill', 'none');
        moveSvg.setAttribute('stroke', 'currentColor');
        moveSvg.setAttribute('stroke-width', '1');
        moveSvg.setAttribute('stroke-linecap', 'round');
        moveSvg.setAttribute('stroke-linejoin', 'round');
        moveSvg.innerHTML = '<polyline points="3 8 7 4 11 8"></polyline><polyline points="13 16 17 20 21 16"></polyline>';
        avMoveSpan.appendChild(moveSvg);
        
        avCtrlDiv.appendChild(avAddSpan);
        avCtrlDiv.appendChild(avDeleteSpan);
        avCtrlDiv.appendChild(avCopySpan);
        avCtrlDiv.appendChild(avMoveSpan);

        const avLabelSpan = document.createElement('span');
        avLabelSpan.className = 'etaniAVLabel';
        avLabelSpan.textContent = 'values : ';

        const avDiv = document.createElement('div');
        avDiv.className = 'etaniAV';
        
        const avItemSpan = document.createElement('span');
        avItemSpan.className = 'etaniAVItem';
        avItemSpan.textContent = '1';
        avItemSpan.addEventListener('click', (e) => handleAVItemClick(e, 'opacity'));
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avCtrlDiv);
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

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

    updateEtaniResult();
}

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

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

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

    allAnimates.forEach(animate => {
        const values = animate.getAttribute('values')?.split(';') || [];
        if (values.length < maxValuesLength && values.length > 0) {
            const lastValue = values[values.length - 1];
            while (values.length < maxValuesLength) {
                values.push(lastValue);
            }
            animate.setAttribute('values', values.join(';'));
        }
    });

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

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

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

            for (let i = currentValuesLength; i < maxValuesLength; i++) {
                const newAVItem = document.createElement('span');
                newAVItem.className = 'etaniAVItem';
                newAVItem.addEventListener('click', (e) => handleAVItemClick(e, animateType));
                if (animateType === 'transform') {
                    lastValue = findFirstMissingLetter(currentItems);
                    newAVItem.textContent = lastValue;
                } else if (animateType === 'opacity') {
                    newAVItem.textContent = lastValue;
                }
                etaniAV.appendChild(newAVItem);
            }

            updateDuration(itemRight, animateType, useId);
        });
    });

    updateEtaniResult();
}

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

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

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

    document.querySelector('.etaniSettingFreeze').classList.toggle('active', !isRepeat);
    document.querySelector('.etaniSettingRepeat').classList.toggle('active', isRepeat);

    updateEtaniResult();
}

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

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

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

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

    const etaniSetting = document.createElement('div');
    etaniSetting.className = 'etaniSetting';
    
    const repeatRadio = document.createElement('span');
    repeatRadio.className = 'etaniSettingMode etaniSettingRepeat active';
    repeatRadio.textContent = 'Repeat';
    repeatRadio.setAttribute('data-mode', 'repeat');
    repeatRadio.addEventListener('click', () => handleSettingModeChange('repeat'));
    
    const freezeRadio = document.createElement('span');
    freezeRadio.className = 'etaniSettingMode etaniSettingFreeze';
    freezeRadio.textContent = 'Freeze';
    freezeRadio.setAttribute('data-mode', 'freeze');
    freezeRadio.addEventListener('click', () => handleSettingModeChange('freeze'));

    etaniSetting.appendChild(repeatRadio);
    etaniSetting.appendChild(freezeRadio);
    etaniCtrl.appendChild(etaniSetting);

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

    const transformButton = document.createElement('button');
    transformButton.className = 'etaniAllAppendTransform';
    transformButton.textContent = 'transform';
    transformButton.addEventListener('click', handleAllAppendTransformClick);
    
    const opacityButton = document.createElement('button');
    opacityButton.className = 'etaniAllAppendOpacity';
    opacityButton.textContent = 'opacity';
    opacityButton.addEventListener('click', handleAllAppendOpacityClick);
    
    etaniAllAppend.appendChild(transformButton);
    etaniAllAppend.appendChild(opacityButton);
    etaniCtrl.appendChild(etaniAllAppend);

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

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

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

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

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

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

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

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

    etaniouter.appendChild(etaniinner);

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

    etdropUses.forEach((useElement, i) => {
        const tileid = useElement.getAttribute('href').substring(1);
        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 originalTile = document.querySelector(`defs g#${tileid}`);
        
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';

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

        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        
        if (originalTile) {
            const tileclone = originalTile.cloneNode(true);
            tileclone.removeAttribute('id');
            if (etwaittransform) tileclone.setAttribute('transform', etwaittransform);
            if (etwaitfill) tileclone.setAttribute('fill', etwaitfill);
            if (etwaitstroke) tileclone.setAttribute('stroke', etwaitstroke);
            if (etwaitstrokeWidth) tileclone.setAttribute('stroke-width', etwaitstrokeWidth);
            
            const svgWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            svgWrapper.setAttribute('width', '40');
            svgWrapper.setAttribute('height', '40');
            svgWrapper.setAttribute('version', '1.1');
            svgWrapper.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
            svgWrapper.setAttribute('xmlns:svg', 'http://www.w3.org/2000/svg');
            svgWrapper.className = 'etanitileimg';
            
            svgWrapper.appendChild(tileclone);
            etaniItemLeft.appendChild(svgWrapper);
        }

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

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

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

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

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

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

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

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

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

帖子 ejsoon »

還是我自己做個圖標吧。delete擅自改字母也不對。

代码: 全选

改動:

一,etaniAVMove換個圖標,其svg的path的d="M 7,2 L 3,6 L 7,10 M 3,6 L 18,6 M 17,14 L 21,18 L 17,22 M 21,18 L 6,18"。

二,當複製或刪除使etaniAVItem的數量發生變化時,dur也要一起變化。

三,etaniAVMove在移動之後,不要使etaniAVItem的內容發生變化。比如acebd,當點c再點b,結果就是aebcd。再如badecg,當點c再點a,結果就是bcadeg。

以及etaniAVDelete之後,也不要使etaniAVItem的內容發生變化。比如acebd,當刪除了c,結果就是aebd。

四,在data-type="transform"中的etaniAVItem的字母可能會因為複製、移動、刪除而變化。

這時如果點擊etaniAVAdd,它所新增的etaniAVItem內的英文,是從a開始的字母序中第一個缺失的字母。

比如現在的etaniAVItem有「ahbedg」,則點擊etaniAVAdd後字母應為「ahbedgc」,因為c是第一個缺失的字母。

如果點擊etaniAVCopy,它會直接複製etaniAVItem的字母。比如現在的etaniAVItem有「ahbedg」,當處於複製狀態並點擊b時,就變成「ahbbedg」。

五,在etaniAVAdd、etaniAVCopy、etaniAVMove中,如果進入了某個狀態,之後又點擊另一個,則直接切換到另一個狀態。

代碼中所有的注釋都要使用英文。

代码: 全选

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

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

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

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

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

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

        /* Styles for etaniResult */
        .etaniResult {
            text-align: center;
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #bbb;
            overflow: auto;
        }
        
        /* Control and button styles */
        .etaniContent a, .etaniValueCtrl a {
            display: inline-block;
            margin: 0 5px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 14px;
        }
        
        .etaniCenter {
            border: 1px solid green;
            color: green;
        }
        
        .etaniContentHTML {
            border: 1px solid #0099ff;
            color: #0099ff;
            margin-right: 15px;
        }

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

        /* Styles for etaniAllAppend buttons */
        .etaniAllAppend button {
            padding: 5px 10px;
            font-size: 16px;
            margin: 0 5px;
            cursor: pointer;
            border: 1px solid #333;
            background-color: #fff;
        }
        
        /* Custom radio button styles */
        .etaniSettingMode {
            display: inline-block;
            cursor: pointer;
            padding: 4px 8px;
            margin: 0 3px;
            font-size: 14px;
            border: 1px solid #888;
            background-color: #eee;
            color: #333;
            user-select: none;
        }

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

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

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

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

        .etaniResultImage {
            display: block;
            max-width: 480px;
            width: 100%;
            height: auto;
            margin: 0 auto 10px auto;
            border: 1px solid #000;
        }
        
        .etaniResultSize {
            display: inline-block;
            margin-left: 10px;
            font-size: 12px; 
            color: #555;
        }
        
        /* etaniItem structure */
        .etaniItem {
            min-height: 48px;
            border: 1px solid #ccc;
            box-sizing: border-box;
            width: 100%;
            margin-bottom: -1px;
            overflow: auto;
        }
        
        .etaniItemLeft {
            float: left; 
            width: 60px;
            min-height: 48px;
            border-right: 1px solid #ccc;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            background-color: #e8e8e8;
            padding: 2px 0;
        }
        
        .etaniItemRight {
            margin-left: 60px; 
            padding: 7px;
            min-height: 48px; 
            background-color: #fff;
        }

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

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

        .etaniAnimateDur {
            display: inline-block;
            margin-right: 10px;
            font-size: 14px;
        }

        .etaniAnimateValue {
            margin-top: 5px;
        }

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

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

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

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

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

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

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

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

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

        .etaniAVItem {
            display: inline-block;
            height: 24px;
            background-color: #ff9933;
            border: 1px dashed #00bfff;
            margin: 0 5px 3px;
            padding: 0 5px;
            box-sizing: border-box;
            cursor: pointer;
            position: relative;
            text-align: center;
            line-height: 24px;
            font-size: 12px;
            color: #333;
        }

        .etaniAVItem.deleting-target, .etaniAVItem.copying-target, .etaniAVItem.moving-target {
            background-color: #ff4d4d;
            border: 2px solid red;
        }

        .etaniAVItem.selected-move {
            background-color: #66ccff;
            border: 2px solid #0066cc;
        }
        
        /* Modal/popup styles */
        .modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.5);
            z-index: 1000;
        }
        .modal-content {
            position: fixed;
            width: 98%;
            height: 48%;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 10px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
            z-index: 1001;
            box-sizing: border-box;
        }
        .modal-content textarea {
            width: 100%;
            height: calc(100% - 50px);
            resize: none;
            border: 1px solid #ccc;
            font-size: 12px;
            box-sizing: border-box;
        }
        .modal-close {
            position: absolute;
            top: 0;
            right: 10px;
            font-size: 48px;
            line-height: 1; 
            cursor: pointer;
            color: #333;
        }
    `;

    document.head.appendChild(styleSheet);
}

// Find the first missing letter in the sequence starting from 'a'
function findFirstMissingLetter(existingLetters) {
    let letter = 'a';
    while (existingLetters.includes(letter)) {
        const code = letter.charCodeAt(0);
        if (code >= 97 && code < 122) { // lowercase a-y
            letter = String.fromCharCode(code + 1);
        } else if (code === 122) { // z -> A
            letter = 'A';
        } else if (code >= 65 && code < 90) { // uppercase A-Y
            letter = String.fromCharCode(code + 1);
        } else if (code === 90) { // Z -> a (loop back)
            letter = 'a';
        }
    }
    return letter;
}

// Extract a specific transform function and its value from a transform string
function extractTransformPart(transformString, type) {
    const regex = new RegExp(`(${type})\\(([^)]+)\\)`, 'i');
    const match = transformString.match(regex);
    if (match) {
        return { func: match[1], value: match[2].trim() };
    }
    return { func: '', value: '' };
}

// Parse transform string to get individual transform values
function parseTransform(transformString) {
    const defaultTransform = { translate: '0,0', scale: '1,1', rotate: '0' };
    if (!transformString) return defaultTransform;

    const transform = {};

    const getMatch = (type) => {
        const part = extractTransformPart(transformString, type).value;
        if (!part) return null;
        return part.split(/[,\s]+/).join(',');
    };

    transform.translate = getMatch('translate') || defaultTransform.translate;
    transform.scale = getMatch('scale') || defaultTransform.scale;
    if (transform.scale.split(',').length === 1) {
        transform.scale += `,${transform.scale}`;
    }
    transform.rotate = getMatch('rotate') || defaultTransform.rotate;

    return transform;
}

// Convert SVG string to a Base64 data URL
function svgToBase64(svgString) {
    const base64 = btoa(unescape(encodeURIComponent(svgString)));
    return `data:image/svg+xml;base64,${base64}`;
}

// Format byte size into human-readable string
function formatBytes(bytes) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

// Update the result section with the current state of etani_clone
function updateEtaniResult() {
    if (!etani_clone) return;

    const svgString = new XMLSerializer().serializeToString(etani_clone);
    const sizeInBytes = new Blob([svgString]).size;
    const base64Url = svgToBase64(svgString);
    
    const now = new Date();
    const yyyy = now.getFullYear();
    const mm = String(now.getMonth() + 1).padStart(2, '0');
    const dd = String(now.getDate()).padStart(2, '0');
    const hh = String(now.getHours()).padStart(2, '0');
    const mi = String(now.getMinutes()).padStart(2, '0');
    const ss = String(now.getSeconds()).padStart(2, '0');
    const defaultFilename = `ejtileAnimation_${yyyy}${mm}${dd}_${hh}${mi}${ss}.svg`;

    const imgElement = document.querySelector('.etaniResultImage');
    const downloadElement = document.querySelector('.etaniResultDownload');
    const renameElement = document.querySelector('.etaniResultRename');
    const sizeElement = document.querySelector('.etaniResultSize');

    if (imgElement && downloadElement && renameElement && sizeElement) {
        imgElement.src = base64Url;
        sizeElement.textContent = `Size: ${formatBytes(sizeInBytes)}`;
        downloadElement.href = base64Url;
        downloadElement.download = defaultFilename;
        
        renameElement.onclick = (e) => {
            e.preventDefault();
            let currentDownloadName = downloadElement.download;
            let newFilename = prompt("Enter new filename:", currentDownloadName);
            if (newFilename) {
                if (!newFilename.toLowerCase().endsWith('.svg')) {
                    newFilename += '.svg';
                }
                downloadElement.download = newFilename;
                alert(`Filename changed to: ${newFilename}`);
            }
        };
    }
}

// Handle the 'Center' button click to reset the .etdrop transform
function handleCenterClick(e) {
    e.preventDefault();
    if (etani_clone) {
        const etdrop = etani_clone.querySelector('.etdrop');
        if (etdrop) {
            etdrop.setAttribute('transform', 'translate(240,240) scale(1,1)');
            updateEtaniResult();
        }
    }
}

// Handle the HTML popup window
function handleContentHTMLClick(e) {
    e.preventDefault();
    if (!etani_clone) return;

    const overlay = document.createElement('div');
    overlay.className = 'modal-overlay';
    
    const content = document.createElement('div');
    content.className = 'modal-content';
    
    const close = document.createElement('span');
    close.className = 'modal-close';
    close.innerHTML = '&times;';
    
    const textarea = document.createElement('textarea');
    textarea.value = new XMLSerializer().serializeToString(etani_clone);

    const closeModal = () => {
        if (document.body.contains(overlay)) document.body.removeChild(overlay);
        if (document.body.contains(content)) document.body.removeChild(content);
    };
    close.onclick = closeModal;
    overlay.onclick = closeModal;

    content.appendChild(close);
    content.appendChild(textarea);
    
    document.body.appendChild(overlay);
    document.body.appendChild(content);
}

// Calculate additive transform or opacity value for a given type
function calculateAdditiveValue(type, originalValue, currentBaseValue, scaleValue = '1,1') {
    const roundToThreeDecimals = (value) => {
        return Math.round(value * 1000) / 1000;
    };
    
    if (type === 'rotate') {
        const ovAngle = parseFloat(currentBaseValue.trim());
        const cvAngle = parseFloat(originalValue.trim());
        return roundToThreeDecimals(cvAngle - ovAngle);
    } else if (type === 'scale') {
        const ovScales = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
        const cvScales = originalValue.split(',').map(c => parseFloat(c.trim()));
        const v1 = roundToThreeDecimals(cvScales[0] / ovScales[0]);
        const v2 = roundToThreeDecimals(cvScales.length > 1 ? (cvScales[1] / ovScales[1]) : (cvScales[0] / ovScales[0]));
        return v1 === v2 ? `${v1}` : `${v1},${v2}`;
    } else if (type === 'translate') {
        const ovCoords = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
        const cvCoords = originalValue.split(',').map(c => parseFloat(c.trim()));
        const scaleFactors = scaleValue.split(',').map(c => parseFloat(c.trim()));
        const v1 = roundToThreeDecimals(cvCoords[0] - (ovCoords[0] * scaleFactors[0]));
        const v2 = roundToThreeDecimals(cvCoords[1] - (ovCoords[1] * (scaleFactors.length > 1 ? scaleFactors[1] : scaleFactors[0])));
        return `${v1},${v2}`;
    } else if (type === 'opacity') {
        return originalValue;
    }
    return '';
}

// Update duration based on the number of values
function updateDuration(animate, etaniAnimate, newValuesLength) {
    const newDur = newValuesLength > 1 ? newValuesLength - 1 : 0;
    const isRepeat = document.querySelector('.etaniSettingRepeat.active');

    if (newDur > 0) {
        animate.setAttribute('dur', `${newDur}s`);
        if (isRepeat) {
            animate.removeAttribute('fill');
            animate.setAttribute('repeatCount', 'indefinite');
        } else {
            animate.removeAttribute('repeatCount');
            animate.setAttribute('fill', 'freeze');
        }
    } else {
        animate.removeAttribute('dur');
        animate.removeAttribute('fill');
        animate.removeAttribute('repeatCount');
    }

    etaniAnimate.querySelector('.etaniAnimateDur').textContent = `dur: ${newDur}s`;
}

// Handle click event for the '+' button for transform or opacity
function handleAVAddClick(e, useElementId, animateType = 'transform') {
    e.preventDefault();
    if (!etani_clone) return;

    const etaniItemRight = e.currentTarget.closest('.etaniItemRight');
    const etaniAV = etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"] .etaniAV`);
    const etaniAnimate = etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`);

    if (animateType === 'transform') {
        const originalUseElement = document.querySelector(`.etdrop use[href="#${useElementId}"]`);
        if (!originalUseElement) return;

        const originalTransformString = originalUseElement.getAttribute('transform') || '';
        const originalTransforms = parseTransform(originalTransformString);

        const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useElementId}"]`);
        if (!translateWrapper) return;

        const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useElementId}"]`);
        const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
        
        if (!scaleWrapper || !cloneUseElement) return;

        const cloneBaseTransformString = cloneUseElement.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)'; 
        const currentBaseTransforms = parseTransform(cloneBaseTransformString);
        
        const currentBaseTranslate = currentBaseTransforms.translate;
        const currentBaseScale = currentBaseTransforms.scale;
        const currentBaseRotate = currentBaseTransforms.rotate;

        const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
        const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
        const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
        
        const allAnimates = [...animatesRotate, ...animatesScale, ...animatesTranslate];
        
        const scaleValue = calculateAdditiveValue('scale', originalTransforms.scale, currentBaseScale);
        
        allAnimates.forEach(animate => {
            const type = animate.getAttribute('type').toLowerCase();
            let currentValueString = animate.getAttribute('values') || '';
            let newValue = '';
            
            if (type === 'rotate') {
                newValue = calculateAdditiveValue('rotate', originalTransforms.rotate, currentBaseRotate);
            } else if (type === 'scale') {
                newValue = scaleValue;
            } else if (type === 'translate') {
                newValue = calculateAdditiveValue('translate', originalTransforms.translate, currentBaseTranslate, scaleValue);
            }
            
            const newValueString = (currentValueString ? currentValueString + ';' : '') + newValue;
            animate.setAttribute('values', newValueString);
            updateDuration(animate, etaniAnimate, newValueString.split(';').length);
        });
    } else if (animateType === 'opacity') {
        const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useElementId}"]`);
        if (!cloneUseElement) return;

        const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
        if (!animateOpacity) return;

        const currentValueString = animateOpacity.getAttribute('values') || '';
        const values = currentValueString ? currentValueString.split(';') : [];
        const newValue = values.length > 0 ? values[values.length - 1] : '1';
        const newValueString = (currentValueString ? currentValueString + ';' : '') + newValue;
        animateOpacity.setAttribute('values', newValueString);
        updateDuration(animateOpacity, etaniAnimate, newValueString.split(';').length);
    }

    // Add new etaniAVItem with appropriate label
    if (etaniAV && etaniAnimate) {
        const newAVItem = document.createElement('span');
        newAVItem.className = 'etaniAVItem';
        newAVItem.addEventListener('click', (e) => handleAVItemClick(e, animateType));

        const existingItems = etaniAV.querySelectorAll('.etaniAVItem');
        if (animateType === 'transform') {
            const existingLetters = Array.from(existingItems).map(item => item.textContent);
            newAVItem.textContent = findFirstMissingLetter(existingLetters);
        } else if (animateType === 'opacity') {
            const values = animateType === 'opacity' ? 
                etaniAnimate.querySelector('animate[attributeName="opacity"]')?.getAttribute('values')?.split(';') || ['1'] :
                ['1'];
            newAVItem.textContent = values[values.length - 1];
        }

        etaniAV.appendChild(newAVItem);
    }

    updateEtaniResult();
}

// Reset all mode states for a specific animateType
function resetModes(animateType, excludeMode = null) {
    const etaniCol = document.querySelector('.etaniCol');
    const avItems = document.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);

    if (excludeMode !== 'delete') {
        isDeleteMode = false;
        etaniCol.classList.remove(`deleting-mode-${animateType}`);
        document.querySelectorAll(`.etaniAVDelete[data-type="${animateType}"]`).forEach(btn => {
            btn.classList.remove('deleting');
            btn.title = 'Delete Value';
        });
    }

    if (excludeMode !== 'copy') {
        isCopyMode = false;
        etaniCol.classList.remove(`copying-mode-${animateType}`);
        document.querySelectorAll(`.etaniAVCopy[data-type="${animateType}"]`).forEach(btn => {
            btn.classList.remove('copying');
            btn.title = 'Copy Value';
        });
    }

    if (excludeMode !== 'move') {
        isMoveMode = false;
        selectedMoveItem = null;
        etaniCol.classList.remove(`moving-mode-${animateType}`);
        document.querySelectorAll(`.etaniAVMove[data-type="${animateType}"]`).forEach(btn => {
            btn.classList.remove('moving');
            btn.title = 'Move Value';
        });
    }

    avItems.forEach(item => {
        item.classList.remove('deleting-target', 'copying-target', 'moving-target', 'selected-move');
    });
}

// Handle click event for the '-' button to toggle deletion mode
function handleAVDeleteToggle(e, animateType = 'transform') {
    e.preventDefault();
    const deleteButton = e.currentTarget;
    resetModes(animateType, 'delete');
    isDeleteMode = !isDeleteMode;
    deleteButton.classList.toggle('deleting', isDeleteMode);
    
    const etaniItemRight = deleteButton.closest('.etaniItemRight');
    const etaniCol = deleteButton.closest('.etaniCol');
    
    const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
    
    avItems.forEach(item => {
        item.classList.toggle('deleting-target', isDeleteMode);
    });

    etaniCol.classList.toggle(`deleting-mode-${animateType}`, isDeleteMode);

    if (isDeleteMode) {
        deleteButton.title = "Click value item to delete (Click again to cancel)";
    } else {
        deleteButton.title = "Delete Value";
        
        avItems.forEach(item => {
            item.classList.remove('deleting-target');
        });
    }
}

// Handle click event for the 'Copy' button to toggle copy mode
function handleAVCopyToggle(e, animateType) {
    e.preventDefault();
    const copyButton = e.currentTarget;
    resetModes(animateType, 'copy');
    isCopyMode = !isCopyMode;
    copyButton.classList.toggle('copying', isCopyMode);
    
    const etaniItemRight = copyButton.closest('.etaniItemRight');
    const etaniCol = copyButton.closest('.etaniCol');
    
    const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
    
    avItems.forEach(item => {
        item.classList.toggle('copying-target', isCopyMode);
    });

    etaniCol.classList.toggle(`copying-mode-${animateType}`, isCopyMode);

    if (isCopyMode) {
        copyButton.title = "Click value item to copy (Click again to cancel)";
    } else {
        copyButton.title = "Copy Value";
        
        avItems.forEach(item => {
            item.classList.remove('copying-target');
        });
    }
}

// Handle click event for the 'Move' button to toggle move mode
function handleAVMoveToggle(e, animateType) {
    e.preventDefault();
    const moveButton = e.currentTarget;
    resetModes(animateType, 'move');
    isMoveMode = !isMoveMode;
    moveButton.classList.toggle('moving', isMoveMode);
    
    const etaniItemRight = moveButton.closest('.etaniItemRight');
    const etaniCol = moveButton.closest('.etaniCol');
    
    const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
    
    avItems.forEach(item => {
        item.classList.toggle('moving-target', isMoveMode);
    });

    etaniCol.classList.toggle(`moving-mode-${animateType}`, isMoveMode);

    if (isMoveMode) {
        moveButton.title = "Click value item to select, then click another to move (Click again to cancel)";
    } else {
        moveButton.title = "Move Value";
        selectedMoveItem = null;
        
        avItems.forEach(item => {
            item.classList.remove('moving-target');
            item.classList.remove('selected-move');
        });
    }
}

// Handle click event for an .etaniAVItem in deletion, copy, or move mode
function handleAVItemClick(e, animateType = 'transform') {
    const item = e.currentTarget;
    const itemIndex = Array.from(item.parentNode.children).indexOf(item);
    const etaniItemRight = item.closest('.etaniItemRight');
    const etaniCol = item.closest('.etaniCol');
    const useId = etaniItemRight.closest('.etaniItem').querySelector('.tileid').textContent;

    const currentTime = Date.now();
    const isDoubleClick = (item === lastClickedItem && (currentTime - lastClickTime) < 2000);

    lastClickTime = currentTime;
    lastClickedItem = item;

    if (etaniCol.classList.contains(`moving-mode-${animateType}`)) {
        const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
        
        if (!selectedMoveItem) {
            selectedMoveItem = item;
            item.classList.add('selected-move');
            return;
        } else if (selectedMoveItem === item) {
            selectedMoveItem.classList.remove('selected-move');
            selectedMoveItem = null;
            return;
        } else {
            const targetIndex = itemIndex;
            const sourceIndex = Array.from(item.parentNode.children).indexOf(selectedMoveItem);
            
            const parent = item.parentNode;
            if (sourceIndex < targetIndex) {
                if (item.nextSibling) {
                    parent.insertBefore(selectedMoveItem, item.nextSibling);
                } else {
                    parent.appendChild(selectedMoveItem);
                }
            } else {
                parent.insertBefore(selectedMoveItem, item);
            }

            if (animateType === 'transform') {
                const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useId}"]`);
                if (!translateWrapper) return;
                
                const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useId}"]`);
                const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
                if (!scaleWrapper || !cloneUseElement) return;

                const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
                const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
                const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
                
                const allAnimates = [...animatesTranslate, ...animatesScale, ...animatesRotate];
                
                allAnimates.forEach(animate => {
                    const values = animate.getAttribute('values').split(';');
                    const valueToMove = values[sourceIndex];
                    values.splice(sourceIndex, 1);
                    if (sourceIndex < targetIndex) {
                        values.splice(targetIndex, 0, valueToMove);
                    } else {
                        values.splice(targetIndex, 0, valueToMove);
                    }
                    animate.setAttribute('values', values.join(';'));
                });
            } else if (animateType === 'opacity') {
                const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
                if (!cloneUseElement) return;

                const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
                if (!animateOpacity) return;

                const values = animateOpacity.getAttribute('values').split(';');
                const valueToMove = values[sourceIndex];
                values.splice(sourceIndex, 1);
                if (sourceIndex < targetIndex) {
                    values.splice(targetIndex, 0, valueToMove);
                } else {
                    values.splice(targetIndex, 0, valueToMove);
                }
                animateOpacity.setAttribute('values', values.join(';'));
            }

            selectedMoveItem.classList.remove('selected-move');
            selectedMoveItem = null;
            updateEtaniResult();
            return;
        }
    }

    if (etaniCol.classList.contains(`copying-mode-${animateType}`)) {
        const etaniAV = item.parentNode;
        const newAVItem = document.createElement('span');
        newAVItem.className = 'etaniAVItem';
        newAVItem.textContent = item.textContent;
        newAVItem.addEventListener('click', (e) => handleAVItemClick(e, animateType));
        etaniAV.insertBefore(newAVItem, item.nextSibling);

        if (animateType === 'transform') {
            const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useId}"]`);
            if (!translateWrapper) return;
            
            const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useId}"]`);
            const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
            if (!scaleWrapper || !cloneUseElement) return;

            const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
            const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
            const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
            
            const allAnimates = [...animatesTranslate, ...animatesScale, ...animatesRotate];
            
            allAnimates.forEach(animate => {
                const values = animate.getAttribute('values').split(';');
                if (itemIndex < values.length) {
                    values.splice(itemIndex + 1, 0, values[itemIndex]);
                    animate.setAttribute('values', values.join(';'));
                    updateDuration(animate, etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`), values.length);
                }
            });
        } else if (animateType === 'opacity') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return;

            const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
            if (!animateOpacity) return;

            const values = animateOpacity.getAttribute('values').split(';');
            if (itemIndex < values.length) {
                values.splice(itemIndex + 1, 0, values[itemIndex]);
                animateOpacity.setAttribute('values', values.join(';'));
                updateDuration(animateOpacity, etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`), values.length);
            }
        }

        updateEtaniResult();
        return;
    }

    if (etaniCol.classList.contains(`deleting-mode-${animateType}`)) {
        const deleteButton = etaniItemRight.querySelector(`.etaniAVDelete[data-type="${animateType}"]`);
        
        if (animateType === 'transform') {
            const translateWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useId}"]`);
            if (!translateWrapper) return;
            
            const scaleWrapper = translateWrapper.querySelector(`g[data-use-href-scale="#${useId}"]`);
            const cloneUseElement = scaleWrapper ? scaleWrapper.querySelector('use') : null;
            if (!scaleWrapper || !cloneUseElement) return;

            const animatesTranslate = translateWrapper.querySelectorAll('animateTransform[type="translate"]');
            const animatesScale = scaleWrapper.querySelectorAll('animateTransform[type="scale"]');
            const animatesRotate = cloneUseElement.querySelectorAll('animateTransform[type="rotate"]');
            
            const allAnimates = [...animatesTranslate, ...animatesScale, ...animatesRotate];
            
            allAnimates.forEach(animate => {
                const values = animate.getAttribute('values').split(';');
                
                if (itemIndex < values.length) {
                    values.splice(itemIndex, 1);
                    animate.setAttribute('values', values.join(';'));
                    updateDuration(animate, etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`), values.length);
                }
            });
        } else if (animateType === 'opacity') {
            const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
            if (!cloneUseElement) return;

            const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
            if (!animateOpacity) return;

            const values = animateOpacity.getAttribute('values').split(';');
            if (itemIndex < values.length) {
                values.splice(itemIndex, 1);
                animateOpacity.setAttribute('values', values.join(';'));
                updateDuration(animateOpacity, etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`), values.length);
            }
        }

        item.remove();

        const remainingItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
        
        if (remainingItems.length === 0) {
            deleteButton.click();
        } else {
            remainingItems.forEach(item => {
                item.classList.add('deleting-target'); 
            });
        }

        updateEtaniResult();
        
    } else if (animateType === 'opacity' && !etaniCol.classList.contains(`deleting-mode-${animateType}`) && !etaniCol.classList.contains(`copying-mode-${animateType}`)) {
        const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;

        const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
        if (!animateOpacity) return;

        const values = animateOpacity.getAttribute('values').split(';');
        if (itemIndex >= values.length) return;

        if (isDoubleClick) {
            const newValue = prompt("Enter opacity value (0 to 1):", values[itemIndex]);
            if (newValue !== null && !isNaN(newValue) && newValue >= 0 && newValue <= 1) {
                values[itemIndex] = newValue;
                item.textContent = newValue;
                animateOpacity.setAttribute('values', values.join(';'));
                updateEtaniResult();
            }
        } else {
            values[itemIndex] = values[itemIndex] === '0' ? '1' : '0';
            item.textContent = values[itemIndex];
            animateOpacity.setAttribute('values', values.join(';'));
            updateEtaniResult();
        }
    }
}

// Handle click event for the 'Transform' button
function handleAllAppendTransformClick() {
    if (!etani_clone) return;

    const etdropUses = document.querySelectorAll('#etmain .etdrop use');
    const etaniItemRights = document.querySelectorAll('.etaniItemRight');

    const isRepeat = document.querySelector('.etaniSettingRepeat.active');

    etdropUses.forEach((originalUseElement, i) => {
        const useId = originalUseElement.getAttribute('href').substring(1);
        const itemRight = etaniItemRights[i];
        
        if (!itemRight) return;

        if (itemRight.querySelector('.etaniAnimate[data-type="transform"]')) return;

        let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;

        const baseAnimate = (type, initialValue) => {
            const animate = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
            animate.setAttribute('attributeName', 'transform');
            animate.setAttribute('attributeType', 'XML');
            animate.setAttribute('type', type);
            animate.setAttribute('values', initialValue);
            animate.setAttribute('fill', isRepeat ? 'remove' : 'freeze');
            if (isRepeat) animate.setAttribute('repeatCount', 'indefinite');
            animate.setAttribute('additive', 'sum');
            return animate;
        };
        
        cloneUseElement.appendChild(baseAnimate('rotate', '0'));
        
        const scaleWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        scaleWrapper.setAttribute('data-use-href-scale', `#${useId}`);
        scaleWrapper.appendChild(baseAnimate('scale', '1'));
        
        const translateWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
        translateWrapper.setAttribute('data-use-href', `#${useId}`);
        translateWrapper.appendChild(baseAnimate('translate', '0,0'));
        
        cloneUseElement.parentNode.insertBefore(translateWrapper, cloneUseElement);
        cloneUseElement.parentNode.insertBefore(scaleWrapper, cloneUseElement);
        scaleWrapper.appendChild(cloneUseElement);
        translateWrapper.appendChild(scaleWrapper);

        const etaniAnimate = document.createElement('div');
        etaniAnimate.className = 'etaniAnimate';
        etaniAnimate.setAttribute('data-type', 'transform');

        const nameSpan = document.createElement('span');
        nameSpan.className = 'etaniAnimateName';
        nameSpan.textContent = 'transform';
        etaniAnimate.appendChild(nameSpan);

        const durSpan = document.createElement('span');
        durSpan.className = 'etaniAnimateDur';
        durSpan.textContent = 'dur: 0s';
        etaniAnimate.appendChild(durSpan);

        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        const avCtrlDiv = document.createElement('div');
        avCtrlDiv.className = 'etaniAVCtrl';

        const avAddSpan = document.createElement('span');
        avAddSpan.className = 'etaniAVAdd';
        avAddSpan.title = 'Add Transform Value';
        avAddSpan.setAttribute('data-type', 'transform');
        avAddSpan.addEventListener('click', (e) => handleAVAddClick(e, useId, 'transform'));
        
        const plusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        plusSvg.setAttribute('width', '24');
        plusSvg.setAttribute('height', '24');
        plusSvg.setAttribute('viewBox', '0 0 24 24');
        plusSvg.setAttribute('fill', 'none');
        plusSvg.setAttribute('stroke', 'currentColor');
        plusSvg.setAttribute('stroke-width', '1');
        plusSvg.setAttribute('stroke-linecap', 'round');
        plusSvg.setAttribute('stroke-linejoin', 'round');
        plusSvg.innerHTML = '<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>';
        avAddSpan.appendChild(plusSvg);

        const avDeleteSpan = document.createElement('span');
        avDeleteSpan.className = 'etaniAVDelete';
        avDeleteSpan.title = 'Delete Value';
        avDeleteSpan.setAttribute('data-type', 'transform');
        avDeleteSpan.addEventListener('click', (e) => handleAVDeleteToggle(e, 'transform'));

        const minusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        minusSvg.setAttribute('width', '24');
        minusSvg.setAttribute('height', '24');
        minusSvg.setAttribute('viewBox', '0 0 24 24');
        minusSvg.setAttribute('fill', 'none');
        minusSvg.setAttribute('stroke', 'currentColor');
        minusSvg.setAttribute('stroke-width', '1');
        minusSvg.setAttribute('stroke-linecap', 'round');
        minusSvg.setAttribute('stroke-linejoin', 'round');
        minusSvg.innerHTML = '<line x1="5" y1="12" x2="19" y2="12"></line>';
        avDeleteSpan.appendChild(minusSvg);

        const avCopySpan = document.createElement('span');
        avCopySpan.className = 'etaniAVCopy';
        avCopySpan.title = 'Copy Value';
        avCopySpan.setAttribute('data-type', 'transform');
        avCopySpan.addEventListener('click', (e) => handleAVCopyToggle(e, 'transform'));

        const copySvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        copySvg.setAttribute('width', '24');
        copySvg.setAttribute('height', '24');
        copySvg.setAttribute('viewBox', '0 0 24 24');
        copySvg.setAttribute('fill', 'none');
        copySvg.setAttribute('stroke', 'currentColor');
        copySvg.setAttribute('stroke-width', '1');
        copySvg.setAttribute('stroke-linecap', 'round');
        copySvg.setAttribute('stroke-linejoin', 'round');
        copySvg.innerHTML = '<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>';
        avCopySpan.appendChild(copySvg);

        const avMoveSpan = document.createElement('span');
        avMoveSpan.className = 'etaniAVMove';
        avMoveSpan.title = 'Move Value';
        avMoveSpan.setAttribute('data-type', 'transform');
        avMoveSpan.addEventListener('click', (e) => handleAVMoveToggle(e, 'transform'));

        const moveSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        moveSvg.setAttribute('width', '24');
        moveSvg.setAttribute('height', '24');
        moveSvg.setAttribute('viewBox', '0 0 24 24');
        moveSvg.setAttribute('fill', 'none');
        moveSvg.setAttribute('stroke', 'currentColor');
        moveSvg.setAttribute('stroke-width', '1');
        moveSvg.setAttribute('stroke-linecap', 'round');
        moveSvg.setAttribute('stroke-linejoin', 'round');
        moveSvg.innerHTML = '<path d="M 7,2 L 3,6 L 7,10 M 3,6 L 18,6 M 17,14 L 21,18 L 17,22 M 21,18 L 6,18"></path>';
        avMoveSpan.appendChild(moveSvg);
        
        avCtrlDiv.appendChild(avAddSpan);
        avCtrlDiv.appendChild(avDeleteSpan);
        avCtrlDiv.appendChild(avCopySpan);
        avCtrlDiv.appendChild(avMoveSpan);

        const avLabelSpan = document.createElement('span');
        avLabelSpan.className = 'etaniAVLabel';
        avLabelSpan.textContent = 'values : ';

        const avDiv = document.createElement('div');
        avDiv.className = 'etaniAV';
        
        const avItemSpan = document.createElement('span');
        avItemSpan.className = 'etaniAVItem';
        avItemSpan.textContent = 'a';
        avItemSpan.addEventListener('click', (e) => handleAVItemClick(e, 'transform'));
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avCtrlDiv);
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

        etaniAnimate.appendChild(valueDiv);
        itemRight.appendChild(etaniAnimate);
    });

    updateEtaniResult();
}

// Handle click event for the 'Opacity' button
function handleAllAppendOpacityClick() {
    if (!etani_clone) return;

    const etdropUses = document.querySelectorAll('#etmain .etdrop use');
    const etaniItemRights = document.querySelectorAll('.etaniItemRight');

    const isRepeat = document.querySelector('.etaniSettingRepeat.active');

    etdropUses.forEach((originalUseElement, i) => {
        const useId = originalUseElement.getAttribute('href').substring(1);
        const itemRight = etaniItemRights[i];
        
        if (!itemRight) return;

        if (itemRight.querySelector('.etaniAnimate[data-type="opacity"]')) return;

        let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
        if (!cloneUseElement) return;

        const animateOpacity = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
        animateOpacity.setAttribute('attributeName', 'opacity');
        animateOpacity.setAttribute('values', '1');
        animateOpacity.setAttribute('fill', isRepeat ? 'remove' : 'freeze');
        if (isRepeat) animateOpacity.setAttribute('repeatCount', 'indefinite');
        cloneUseElement.appendChild(animateOpacity);

        const etaniAnimate = document.createElement('div');
        etaniAnimate.className = 'etaniAnimate';
        etaniAnimate.setAttribute('data-type', 'opacity');

        const nameSpan = document.createElement('span');
        nameSpan.className = 'etaniAnimateName';
        nameSpan.textContent = 'opacity';
        etaniAnimate.appendChild(nameSpan);

        const durSpan = document.createElement('span');
        durSpan.className = 'etaniAnimateDur';
        durSpan.textContent = 'dur: 0s';
        etaniAnimate.appendChild(durSpan);

        const valueDiv = document.createElement('div');
        valueDiv.className = 'etaniAnimateValue';
        
        const avCtrlDiv = document.createElement('div');
        avCtrlDiv.className = 'etaniAVCtrl';

        const avAddSpan = document.createElement('span');
        avAddSpan.className = 'etaniAVAdd';
        avAddSpan.title = 'Add Opacity Value';
        avAddSpan.setAttribute('data-type', 'opacity');
        avAddSpan.addEventListener('click', (e) => handleAVAddClick(e, useId, 'opacity'));
        
        const plusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        plusSvg.setAttribute('width', '24');
        plusSvg.setAttribute('height', '24');
        plusSvg.setAttribute('viewBox', '0 0 24 24');
        plusSvg.setAttribute('fill', 'none');
        plusSvg.setAttribute('stroke', 'currentColor');
        plusSvg.setAttribute('stroke-width', '1');
        plusSvg.setAttribute('stroke-linecap', 'round');
        plusSvg.setAttribute('stroke-linejoin', 'round');
        plusSvg.innerHTML = '<line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line>';
        avAddSpan.appendChild(plusSvg);

        const avDeleteSpan = document.createElement('span');
        avDeleteSpan.className = 'etaniAVDelete';
        avDeleteSpan.title = 'Delete Value';
        avDeleteSpan.setAttribute('data-type', 'opacity');
        avDeleteSpan.addEventListener('click', (e) => handleAVDeleteToggle(e, 'opacity'));

        const minusSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        minusSvg.setAttribute('width', '24');
        minusSvg.setAttribute('height', '24');
        minusSvg.setAttribute('viewBox', '0 0 24 24');
        minusSvg.setAttribute('fill', 'none');
        minusSvg.setAttribute('stroke', 'currentColor');
        minusSvg.setAttribute('stroke-width', '1');
        minusSvg.setAttribute('stroke-linecap', 'round');
        minusSvg.setAttribute('stroke-linejoin', 'round');
        minusSvg.innerHTML = '<line x1="5" y1="12" x2="19" y2="12"></line>';
        avDeleteSpan.appendChild(minusSvg);

        const avCopySpan = document.createElement('span');
        avCopySpan.className = 'etaniAVCopy';
        avCopySpan.title = 'Copy Value';
        avCopySpan.setAttribute('data-type', 'opacity');
        avCopySpan.addEventListener('click', (e) => handleAVCopyToggle(e, 'opacity'));

        const copySvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        copySvg.setAttribute('width', '24');
        copySvg.setAttribute('height', '24');
        copySvg.setAttribute('viewBox', '0 0 24 24');
        copySvg.setAttribute('fill', 'none');
        copySvg.setAttribute('stroke', 'currentColor');
        copySvg.setAttribute('stroke-width', '1');
        copySvg.setAttribute('stroke-linecap', 'round');
        copySvg.setAttribute('stroke-linejoin', 'round');
        copySvg.innerHTML = '<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>';
        avCopySpan.appendChild(copySvg);

        const avMoveSpan = document.createElement('span');
        avMoveSpan.className = 'etaniAVMove';
        avMoveSpan.title = 'Move Value';
        avMoveSpan.setAttribute('data-type', 'opacity');
        avMoveSpan.addEventListener('click', (e) => handleAVMoveToggle(e, 'opacity'));

        const moveSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        moveSvg.setAttribute('width', '24');
        moveSvg.setAttribute('height', '24');
        moveSvg.setAttribute('viewBox', '0 0 24 24');
        moveSvg.setAttribute('fill', 'none');
        moveSvg.setAttribute('stroke', 'currentColor');
        moveSvg.setAttribute('stroke-width', '1');
        moveSvg.setAttribute('stroke-linecap', 'round');
        moveSvg.setAttribute('stroke-linejoin', 'round');
        moveSvg.innerHTML = '<path d="M 7,2 L 3,6 L 7,10 M 3,6 L 18,6 M 17,14 L 21,18 L 17,22 M 21,18 L 6,18"></path>';
        avMoveSpan.appendChild(moveSvg);
        
        avCtrlDiv.appendChild(avAddSpan);
        avCtrlDiv.appendChild(avDeleteSpan);
        avCtrlDiv.appendChild(avCopySpan);
        avCtrlDiv.appendChild(avMoveSpan);

        const avLabelSpan = document.createElement('span');
        avLabelSpan.className = 'etaniAVLabel';
        avLabelSpan.textContent = 'values : ';

        const avDiv = document.createElement('div');
        avDiv.className = 'etaniAV';
        
        const avItemSpan = document.createElement('span');
        avItemSpan.className = 'etaniAVItem';
        avItemSpan.textContent = '1';
        avItemSpan.addEventListener('click', (e) => handleAVItemClick(e, 'opacity'));
        
        avDiv.appendChild(avItemSpan);
        
        valueDiv.appendChild(avCtrlDiv);
        valueDiv.appendChild(avLabelSpan);
        valueDiv.appendChild(avDiv);

        etaniAnimate.appendChild(valueDiv);
        itemRight.appendChild(etaniAnimate);
    });

    updateEtaniResult();
}

// Handle click event for the 'Fill up values' button
function handleValueCtrlUpClick() {
    if (!etani_clone) return;

    const allAnimates = etani_clone.querySelectorAll('animateTransform, animate[attributeName="opacity"]');
    let maxValuesLength = 0;

    allAnimates.forEach(animate => {
        const values = animate.getAttribute('values')?.split(';') || [];
        maxValuesLength = Math.max(maxValuesLength, values.length);
    });

    allAnimates.forEach(animate => {
        const values = animate.getAttribute('values')?.split(';') || [];
        if (values.length < maxValuesLength && values.length > 0) {
            const lastValue = values[values.length - 1];
            while (values.length < maxValuesLength) {
                values.push(lastValue);
            }
            animate.setAttribute('values', values.join(';'));
            const etaniAnimate = document.querySelector(`.etaniAnimate:has([data-type="${animate.getAttribute('type') || 'opacity'}"])`);
            updateDuration(animate, etaniAnimate, values.length);
        }
    });

    const etaniItemRights = document.querySelectorAll('.etaniItemRight');
    etaniItemRights.forEach(itemRight => {
        const useId = itemRight.closest('.etaniItem').querySelector('.tileid').textContent;
        ['transform', 'opacity'].forEach(animateType => {
            const etaniAV = itemRight.querySelector(`.etaniAnimate[data-type="${animateType}"] .etaniAV`);
            if (!etaniAV) return;

            const currentItems = etaniAV.querySelectorAll('.etaniAVItem');
            const currentValuesLength = currentItems.length;
            if (currentValuesLength >= maxValuesLength) return;

            const lastItem = currentItems[currentItems.length - 1];
            let lastValue = lastItem.textContent;

            for (let i = currentValuesLength; i < maxValuesLength; i++) {
                const newAVItem = document.createElement('span');
                newAVItem.className = 'etaniAVItem';
                newAVItem.addEventListener('click', (e) => handleAVItemClick(e, animateType));
                if (animateType === 'transform') {
                    const existingLetters = Array.from(currentItems).map(item => item.textContent);
                    newAVItem.textContent = findFirstMissingLetter(existingLetters);
                    existingLetters.push(newAVItem.textContent);
                } else if (animateType === 'opacity') {
                    newAVItem.textContent = lastValue;
                }
                etaniAV.appendChild(newAVItem);
            }
        });
    });

    updateEtaniResult();
}

// Handle setting mode switch (Repeat/Freeze)
function handleSettingModeChange(mode) {
    if (!etani_clone) return;

    const animates = etani_clone.querySelectorAll('animateTransform, animate[attributeName="opacity"]');
    const isRepeat = mode === 'repeat';

    animates.forEach(animate => {
        if (animate.hasAttribute('dur')) {
            if (isRepeat) {
                animate.removeAttribute('fill');
                animate.setAttribute('repeatCount', 'indefinite');
            } else {
                animate.removeAttribute('repeatCount');
                animate.setAttribute('fill', 'freeze');
            }
        }
    });

    document.querySelector('.etaniSettingFreeze').classList.toggle('active', !isRepeat);
    document.querySelector('.etaniSettingRepeat').classList.toggle('active', isRepeat);

    updateEtaniResult();
}

// Create the internal animation control structure
function createEtaniInner(etaniouter) {
    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etani_clone = originalSvg.cloneNode(true);
    const etwaitElement = etani_clone.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    etani_clone.id = 'etmainani';

    const etaniinner = document.createElement('div');
    etaniinner.className = 'etaniinner';
    etaniinner.id = 'etaniinner';

    const etaniCtrl = document.createElement('div');
    etaniCtrl.className = 'etaniCtrl';
    
    const etaniContent = document.createElement('div');
    etaniContent.className = 'etaniContent';
    
    const contentHTMLLink = document.createElement('a');
    contentHTMLLink.className = 'etaniContentHTML';
    contentHTMLLink.textContent = 'Show HTML';
    contentHTMLLink.href = 'javascript:;';
    contentHTMLLink.addEventListener('click', handleContentHTMLClick);
    etaniContent.appendChild(contentHTMLLink);

    const centerLink = document.createElement('a');
    centerLink.className = 'etaniCenter';
    centerLink.textContent = 'Center';
    centerLink.href = 'javascript:;';
    centerLink.addEventListener('click', handleCenterClick);
    etaniContent.appendChild(centerLink);
    
    etaniCtrl.appendChild(etaniContent);

    const etaniSetting = document.createElement('div');
    etaniSetting.className = 'etaniSetting';
    
    const repeatRadio = document.createElement('span');
    repeatRadio.className = 'etaniSettingMode etaniSettingRepeat active';
    repeatRadio.textContent = 'Repeat';
    repeatRadio.setAttribute('data-mode', 'repeat');
    repeatRadio.addEventListener('click', () => handleSettingModeChange('repeat'));
    
    const freezeRadio = document.createElement('span');
    freezeRadio.className = 'etaniSettingMode etaniSettingFreeze';
    freezeRadio.textContent = 'Freeze';
    freezeRadio.setAttribute('data-mode', 'freeze');
    freezeRadio.addEventListener('click', () => handleSettingModeChange('freeze'));

    etaniSetting.appendChild(repeatRadio);
    etaniSetting.appendChild(freezeRadio);
    etaniCtrl.appendChild(etaniSetting);

    const etaniAllAppend = document.createElement('div');
    etaniAllAppend.className = 'etaniAllAppend';

    const transformButton = document.createElement('button');
    transformButton.className = 'etaniAllAppendTransform';
    transformButton.textContent = 'transform';
    transformButton.addEventListener('click', handleAllAppendTransformClick);
    
    const opacityButton = document.createElement('button');
    opacityButton.className = 'etaniAllAppendOpacity';
    opacityButton.textContent = 'opacity';
    opacityButton.addEventListener('click', handleAllAppendOpacityClick);
    
    etaniAllAppend.appendChild(transformButton);
    etaniAllAppend.appendChild(opacityButton);
    etaniCtrl.appendChild(etaniAllAppend);

    const etaniValueCtrl = document.createElement('div');
    etaniValueCtrl.className = 'etaniValueCtrl';

    const valueCtrlUpLink = document.createElement('a');
    valueCtrlUpLink.className = 'etaniValueCtrlUp';
    valueCtrlUpLink.textContent = 'fill up values';
    valueCtrlUpLink.href = 'javascript:;';
    valueCtrlUpLink.addEventListener('click', handleValueCtrlUpClick);
    etaniValueCtrl.appendChild(valueCtrlUpLink);
    etaniCtrl.appendChild(etaniValueCtrl);

    const etaniCol = document.createElement('div');
    etaniCol.className = 'etaniCol';

    const etaniResult = document.createElement('div');
    etaniResult.className = 'etaniResult';
    
    const resultImg = document.createElement('img');
    resultImg.className = 'etaniResultImage';
    resultImg.alt = 'Rendered Ejtile Animation SVG';

    const resultDRDiv = document.createElement('div');
    resultDRDiv.className = 'etaniResultDR';

    const downloadLink = document.createElement('a');
    downloadLink.className = 'etaniResultDownload';
    downloadLink.textContent = 'Download SVG';
    downloadLink.href = 'javascript:;';

    const renameLink = document.createElement('a');
    renameLink.className = 'etaniResultRename';
    renameLink.textContent = 'Rename File';
    renameLink.href = 'javascript:;';
    
    const sizeSpan = document.createElement('span');
    sizeSpan.className = 'etaniResultSize';

    resultDRDiv.appendChild(downloadLink);
    resultDRDiv.appendChild(renameLink);
    
    etaniResult.appendChild(resultImg);
    etaniResult.appendChild(resultDRDiv);
    etaniResult.appendChild(sizeSpan);
    
    etaniinner.appendChild(etaniCtrl);
    etaniinner.appendChild(etaniCol);
    etaniinner.appendChild(etaniResult);

    etaniouter.appendChild(etaniinner);

    const etdropUses = document.querySelectorAll('.etdrop use');
    const etwaitGroups = document.querySelectorAll('.etwait g');

    etdropUses.forEach((useElement, i) => {
        const tileid = useElement.getAttribute('href').substring(1);
        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 originalTile = document.querySelector(`defs g#${tileid}`);
        
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';

        const etaniItemLeft = document.createElement('div');
        etaniItemLeft.className = 'etaniItemLeft';

        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        
        if (originalTile) {
            const tileclone = originalTile.cloneNode(true);
            tileclone.removeAttribute('id');
            if (etwaittransform) tileclone.setAttribute('transform', etwaittransform);
            if (etwaitfill) tileclone.setAttribute('fill', etwaitfill);
            if (etwaitstroke) tileclone.setAttribute('stroke', etwaitstroke);
            if (etwaitstrokeWidth) tileclone.setAttribute('stroke-width', etwaitstrokeWidth);
            
            const svgWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            svgWrapper.setAttribute('width', '40');
            svgWrapper.setAttribute('height', '40');
            svgWrapper.setAttribute('version', '1.1');
            svgWrapper.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
            svgWrapper.setAttribute('xmlns:svg', 'http://www.w3.org/2000/svg');
            svgWrapper.className = 'etanitileimg';
            
            svgWrapper.appendChild(tileclone);
            etaniItemLeft.appendChild(svgWrapper);
        }

        const tileidDiv = document.createElement('div');
        tileidDiv.className = 'tileid';
        tileidDiv.textContent = tileid;
        etaniItemLeft.appendChild(tileidDiv);

        etaniItem.appendChild(etaniItemLeft);
        etaniItem.appendChild(etaniItemRight);
        etaniCol.appendChild(etaniItem);
    });
    
    updateEtaniResult();
}

// Toggle the visibility and content of the animation control panel
function toggleAnimation(event) {
    const etanibutton = event.currentTarget;
    const etaniouter = etanibutton.parentNode;
    const etaniinner = document.getElementById('etaniinner');

    if (etanibutton.textContent === 'Animate it') {
        etanibutton.textContent = 'Close Ejtile Ani';
        createEtaniInner(etaniouter);
    } else if (etanibutton.textContent === 'Close Ejtile Ani') {
        etanibutton.textContent = 'Animate it';
        if (etaniinner) {
            etaniinner.remove();
        }
        etani_clone = null;
        isCopyMode = false;
        isMoveMode = false;
        isDeleteMode = false;
        selectedMoveItem = null;
        lastClickTime = 0;
        lastClickedItem = null;
    }
}

// Initialize the animation control panel on window load
window.addEventListener('load', () => {
    addDynamicStyles();

    const etmainouter = document.getElementById('etmainouter');
    if (etmainouter) {
        const etaniouter = document.createElement('div');
        etaniouter.className = 'etaniouter';

        const etanibutton = document.createElement('button');
        etanibutton.id = 'etanibutton';
        etanibutton.textContent = 'Animate it';
        etanibutton.addEventListener('click', toggleAnimation);

        etaniouter.appendChild(etanibutton);
        etmainouter.parentNode.insertBefore(etaniouter, etmainouter.nextSibling);
    } else {
        console.error('Div with ID "etmainouter" not found.');
    }
});
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复

在线用户

正浏览此版面之用户: 没有注册用户 和 1 访客