正在做etani,ejtile的動畫套件

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

正在做etani,ejtile的動畫套件

帖子 ejsoon »

已經想做很久了,現在想用日拱一卒的方式推進。感謝gemini,以及grok等各種免費AI。
以下圈友为 ejsoon 的帖子点了赞:
BobMaster
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3805
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 157 次
被圈友点赞: 188 次
联系:

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

帖子 ejsoon »

代码: 全选

做一個js,功能描述如下:

一,在window.addEventListener(onload)之時,在div#etmainouter的下方增加
div.etaniouter>button#etanibutton{Animate it}

二,當按下etanibutton時,在etaniouter內部的最後增加:

div.etaniinner>(div.etaniCtrl+div.etaniCol+div.etaniResult)

三,同時,etaniCol內也增加多個div.etaniItem,它的數量等同於在.etdrop內<use/>的數量。

四,每個etaniItem內,包含了。etaniItemLeft和。etaniItemRight,etaniItemLeft的寬度固定為60px,其餘寬度給etaniItemRight佔滿。etaniItemLeft最小高度為48px。

五,etaniItemLeft的右邊有border 1 px,整個etaniItem也有border 1px,boxsizing要包含邊框。

六,所有html元素的增加,包括css的增加,都要用createElement的方式增加,而不是用文本方式增加。

只需要提供js,注釋全部使用英文。

代码: 全选

// Function to create and append the initial animation controls structure
window.addEventListener('load', () => {
    // I. Add div.etaniouter>button#etanibutton{Animate it} below div#etmainouter
    const etmainouter = document.getElementById('etmainouter');
    if (etmainouter) {
        // Create div.etaniouter
        const etaniouter = document.createElement('div');
        etaniouter.className = 'etaniouter';

        // Create button#etanibutton{Animate it}
        const etanibutton = document.createElement('button');
        etanibutton.id = 'etanibutton';
        etanibutton.textContent = 'Animate it';

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

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

        // II. Add click listener to etanibutton
        etanibutton.addEventListener('click', () => {
            // Check if the inner structure already exists to prevent duplication
            if (etaniouter.querySelector('.etaniinner')) {
                console.log('Animation controls already created.');
                return;
            }

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

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

            // Create div.etaniCol
            const etaniCol = document.createElement('div');
            etaniCol.className = 'etaniCol';
            // Set styles for etaniCol to accommodate children and borders (part of V)
            Object.assign(etaniCol.style, {
                display: 'flex',
                flexDirection: 'column',
                gap: '5px', // Optional: space between items
                padding: '5px' // Optional: internal padding
            });

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

            // Append children to etaniinner
            etaniinner.appendChild(etaniCtrl);
            etaniinner.appendChild(etaniCol);
            etaniinner.appendChild(etaniResult);

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

            // III. Populate etaniCol with div.etaniItem based on the count of <use/> in .etdrop
            const etdropElements = document.querySelectorAll('.etdrop use');
            const itemCount = etdropElements.length;

            for (let i = 0; i < itemCount; i++) {
                // Create div.etaniItem (Part V: border 1px, box-sizing: border-box)
                const etaniItem = document.createElement('div');
                etaniItem.className = 'etaniItem';
                Object.assign(etaniItem.style, {
                    display: 'flex',
                    border: '1px solid #ccc', // V. border 1px
                    boxSizing: 'border-box', // V. box-sizing: border-box
                    width: '100%',
                    minHeight: '48px' // IV. Minimum height for item
                });

                // Create div.etaniItemLeft (IV. width: 60px, IV. min-height: 48px, V. right border 1px)
                const etaniItemLeft = document.createElement('div');
                etaniItemLeft.className = 'etaniItemLeft';
                Object.assign(etaniItemLeft.style, {
                    width: '60px', // IV. width: 60px
                    minHeight: '48px', // IV. min-height: 48px
                    borderRight: '1px solid #ccc', // V. right border 1px
                    // Optional: Center content and set background for visibility
                    display: 'flex',
                    alignItems: 'center',
                    justifyContent: 'center',
                    backgroundColor: '#f9f9f9'
                });
                etaniItemLeft.textContent = `L-${i+1}`; // Example content

                // Create div.etaniItemRight (IV. fill remaining width)
                const etaniItemRight = document.createElement('div');
                etaniItemRight.className = 'etaniItemRight';
                Object.assign(etaniItemRight.style, {
                    flexGrow: '1', // IV. fill remaining width
                    minHeight: '48px', // Ensures right side matches min height
                    padding: '5px' // Optional padding
                });
                etaniItemRight.textContent = `R-${i+1}`; // Example content

                // Append Left and Right to Item
                etaniItem.appendChild(etaniItemLeft);
                etaniItem.appendChild(etaniItemRight);

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

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

帖子 ejsoon »

代码: 全选

// Function to dynamically add a <style> element with all necessary CSS rules
function addDynamicStyles() {
    // Check if styles are already present to avoid duplication
    if (document.getElementById('dynamic-et-styles')) {
        return;
    }

    const styleSheet = document.createElement('style');
    styleSheet.id = 'dynamic-et-styles'; // Give it an ID for easy lookup and prevention of duplication
    styleSheet.textContent = `
        /* I. Styles for etaniouter and button */
        .etaniouter {
            margin-top: 10px; /* Example: add some separation from the element above */
            padding: 10px;
            border: 1px solid #ddd;
            background-color: #f5f5f5;
        }

        /* II. Styles for etaniinner, etaniCtrl, etaniCol, etaniResult */
        .etaniinner {
            display: flex;
            flex-direction: column;
            margin-top: 10px;
            gap: 10px;
        }
        
        .etaniCol {
            display: flex;
            flex-direction: column;
            gap: 5px; /* Optional: space between items */
            padding: 5px; /* Optional: internal padding */
            border: 1px solid #aaa; /* Optional: outline for the column */
        }
        
        /* III, IV, V. Styles for etaniItem, etaniItemLeft, etaniItemRight */
        .etaniItem {
            display: flex;
            border: 1px solid #ccc; /* V. border 1px for the entire item */
            box-sizing: border-box; /* V. box-sizing: border-box */
            width: 100%;
            min-height: 48px; /* IV. Minimum height for item */
        }

        .etaniItemLeft {
            width: 60px; /* IV. fixed width: 60px */
            min-height: 48px; /* IV. min-height: 48px */
            border-right: 1px solid #ccc; /* V. right border 1px */
            
            /* Optional: Center content and set background for visibility */
            display: flex;
            align-items: center;
            justify-content: center;
            background-color: #e8e8e8;
        }

        .etaniItemRight {
            flex-grow: 1; /* IV. fill remaining width */
            min-height: 48px; /* Ensures right side matches min height */
            padding: 5px; /* Optional padding */
            background-color: #fff;
        }
    `;

    // Append the <style> element to the document's <head>
    document.head.appendChild(styleSheet);
}


// Main function to create and append the initial animation controls structure
window.addEventListener('load', () => {
    
    // Call the function to add all necessary CSS dynamically
    addDynamicStyles();

    // I. Add div.etaniouter>button#etanibutton{Animate it} below div#etmainouter
    const etmainouter = document.getElementById('etmainouter');
    if (etmainouter) {
        // Create div.etaniouter
        const etaniouter = document.createElement('div');
        etaniouter.className = 'etaniouter'; // Apply CSS class

        // Create button#etanibutton{Animate it}
        const etanibutton = document.createElement('button');
        etanibutton.id = 'etanibutton';
        etanibutton.textContent = 'Animate it';

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

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

        // II. Add click listener to etanibutton
        etanibutton.addEventListener('click', () => {
            // Check if the inner structure already exists to prevent duplication
            if (etaniouter.querySelector('.etaniinner')) {
                console.log('Animation controls already created.');
                return;
            }

            // Create div.etaniinner
            const etaniinner = document.createElement('div');
            etaniinner.className = 'etaniinner'; // Apply CSS class

            // Create div.etaniCtrl
            const etaniCtrl = document.createElement('div');
            etaniCtrl.className = 'etaniCtrl'; // Apply CSS class

            // Create div.etaniCol
            const etaniCol = document.createElement('div');
            etaniCol.className = 'etaniCol'; // Apply CSS class

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

            // Append children to etaniinner
            etaniinner.appendChild(etaniCtrl);
            etaniinner.appendChild(etaniCol);
            etaniinner.appendChild(etaniResult);

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

            // III. Populate etaniCol with div.etaniItem based on the count of <use/> in .etdrop
            const etdropElements = document.querySelectorAll('.etdrop use');
            const itemCount = etdropElements.length;

            for (let i = 0; i < itemCount; i++) {
                // Create div.etaniItem
                const etaniItem = document.createElement('div');
                etaniItem.className = 'etaniItem'; // Apply CSS class

                // Create div.etaniItemLeft
                const etaniItemLeft = document.createElement('div');
                etaniItemLeft.className = 'etaniItemLeft'; // Apply CSS class
                etaniItemLeft.textContent = `L-${i+1}`; // Example content

                // Create div.etaniItemRight
                const etaniItemRight = document.createElement('div');
                etaniItemRight.className = 'etaniItemRight'; // Apply CSS class
                etaniItemRight.textContent = `R-${i+1}`; // Example content

                // Append Left and Right to Item
                etaniItem.appendChild(etaniItemLeft);
                etaniItem.appendChild(etaniItemRight);

                // Append Item to etaniCol
                etaniCol.appendChild(etaniItem);
            }
        });
    } else {
        console.error('Div with ID "etmainouter" not found.');
    }
});
css改為動態添加。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3805
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 157 次
被圈友点赞: 188 次
联系:

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

帖子 ejsoon »

代码: 全选

增加功能:

一,當etanibutton被按下時,它的innerHTML將改為「Animate finished」。

如果「Animate finished」不符合英文表達習慣,請幫我換個詞。

二,當再次按下etanibutton時,etaniinner將被刪除,裡面如果含有事件綁定也被解除綁定。

三,所有的css都不要用到flex,也不要用到table,改為其它方法。比如float:left和margin-left。

四,提取出.etdrop內每一個<use/>的href,賦值至tileid。

五,找到.etwait的內部的每一個<g>中的 <use href="tileid">,這些<use href="tileid">的transform都會含有translate、scale和rotate。將這些<use href="tileid">的transform賦值至etwaittransform,再把etwaittransform中的translate改為translate(20,20),scale和rotate保持不變。同時把<use href="tileid">的fill屬性賦值至etwaitfill。

六,再找到<defs>內部的每一個<g id="tileid">,克隆為tileclone,為tileclone加上transform=etwaittransform和fill=etwaitfill,去掉id,再套上一個svg的殼(長寬40,不要有viewbox,要有version、xmlns和xmlns:svg),加上.etanitileimg類名,之後放到每一個.etaniItemLeft中,並居中顯示。

七,在etanitileimg下方增加div.tileid,它的內容即是tileid。同樣居中顯示。

請用繁體中文回覆。

代码: 全选

// Function to dynamically add a <style> element with all necessary CSS rules
function addDynamicStyles() {
    // Check if styles are already present to avoid duplication
    if (document.getElementById('dynamic-et-styles')) {
        return;
    }

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

        /* II. Styles for etaniinner, etaniCtrl, etaniCol, etaniResult */
        .etaniinner {
            margin-top: 10px;
            /* Use margin to create vertical spacing instead of flex gap */
        }
        
        .etaniCtrl, .etaniCol, .etaniResult {
            /* Since we are not using flex, these will stack naturally */
            margin-bottom: 10px; 
            clear: both; /* Ensure they start on a new line after floats if necessary */
        }
        
        .etaniCol {
            border: 1px solid #aaa;
            padding: 5px;
            overflow: auto; /* To contain floated etaniItemLeft and etaniItemRight */
        }
        
        /* III, IV, V. Styles for etaniItem, etaniItemLeft, etaniItemRight */
        .etaniItem {
            min-height: 48px;
            border: 1px solid #ccc; /* V. border 1px for the entire item */
            box-sizing: border-box; /* V. box-sizing: border-box */
            width: 100%;
            margin-bottom: -1px; /* Overlap borders to prevent double thickness */
            overflow: auto; /* To contain floated elements */
        }
        
        .etaniItemLeft {
            float: left; /* III. Use float instead of flex */
            width: 60px; /* IV. fixed width: 60px */
            min-height: 48px; /* IV. min-height: 48px */
            border-right: 1px solid #ccc; /* V. right border 1px */
            
            /* Center content vertically and horizontally */
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            
            background-color: #e8e8e8;
            padding: 2px 0; /* Add a little internal padding */
        }
        
        .etaniItemRight {
            /* III. Use margin-left to fill remaining width */
            margin-left: 60px; /* Width of etaniItemLeft */
            min-height: 48px; 
            padding: 5px;
            background-color: #fff;
        }

        /* VI. Styles for the cloned SVG tile */
        .etanitileimg {
            display: block; /* Ensure it takes up its own line */
            margin: 0 auto; /* Center the SVG */
        }

        /* VII. Styles for tileid text */
        .tileid {
            text-align: center; /* Center the text */
            font-size: 10px;
            word-break: break-all;
            padding-top: 2px;
        }
    `;

    // Append the <style> element to the document's <head>
    document.head.appendChild(styleSheet);
}

// Function to handle the creation and population of etaniinner
function createEtaniInner(etaniouter) {
    // 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';

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

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

    // 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');
    
    // Get all <g> elements within .etwait
    const etwaitGroups = document.querySelectorAll('.etwait g');

    etdropUses.forEach((useElement, i) => {
        // IV. Get tileid
        const tileid = useElement.getAttribute('href').substring(1); // Remove the '#' prefix
        
        // Find the corresponding <use> element inside .etwait for this tileid
        let targetUse = null;
        let groupElement = null;
        
        // V. Search for the <use> element inside all <g> elements in .etwait
        for (const group of etwaitGroups) {
            const useInGroup = group.querySelector(`use[href="#${tileid}"]`);
            if (useInGroup) {
                targetUse = useInGroup;
                groupElement = group; // Record the parent group
                break;
            }
        }
        
        let etwaittransform = '';
        let etwaitfill = '';

        if (targetUse) {
            // V. Get the original transform and fill
            etwaittransform = targetUse.getAttribute('transform') || '';
            etwaitfill = targetUse.getAttribute('fill') || 'none';

            // V. Modify the transform: translate(20,20), keep scale and rotate
            // Find scale(X) and rotate(Y) parts
            const scaleMatch = etwaittransform.match(/scale\(([^)]+)\)/);
            const rotateMatch = etwaittransform.match(/rotate\(([^)]+)\)/);
            
            const scalePart = scaleMatch ? scaleMatch[0] : '';
            const rotatePart = rotateMatch ? rotateMatch[0] : '';
            
            // Reconstruct transform: new translate + old scale + old rotate
            etwaittransform = `translate(20,20) ${scalePart} ${rotatePart}`.trim().replace(/\s+/g, ' '); // Clean up spaces
        }
        
        // VI. Find the original <g> in <defs> and clone it
        const originalTile = document.querySelector(`defs g#${tileid}`);
        
        // Create div.etaniItem
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';

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

        // Create div.etaniItemRight
        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        etaniItemRight.textContent = `Tile ID: ${tileid}, Transform: ${etwaittransform}`; // Example content

        // VI. Clone, modify, wrap, and append SVG
        if (originalTile) {
            const tileclone = originalTile.cloneNode(true);
            tileclone.removeAttribute('id'); // Remove id
            
            // Set the new transform and fill
            if (etwaittransform) {
                tileclone.setAttribute('transform', etwaittransform);
            }
            if (etwaitfill) {
                // Apply fill to the group clone
                tileclone.setAttribute('fill', etwaitfill); 
                // Note: The fill should ideally be applied to paths/shapes inside the group, 
                // but setting on the <g> often works or is a starting point.
            }
            
            // Create the SVG wrapper
            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'; // Add CSS class
            
            svgWrapper.appendChild(tileclone);
            etaniItemLeft.appendChild(svgWrapper);
        }

        // VII. Add div.tileid below the SVG wrapper
        const tileidDiv = document.createElement('div');
        tileidDiv.className = 'tileid';
        tileidDiv.textContent = tileid;
        etaniItemLeft.appendChild(tileidDiv);

        // Append Left and Right to Item
        etaniItem.appendChild(etaniItemLeft);
        
        // Apply margin-left offset for etaniItemRight
        etaniItemRight.style.marginLeft = '60px'; // Redundant if using CSS class, but safer here if class is not loaded
        
        etaniItem.appendChild(etaniItemRight);

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

// Function to handle button click: toggle animation controls
function toggleAnimation(event) {
    const etanibutton = event.currentTarget;
    const etaniouter = etanibutton.parentNode;
    const etaniinner = document.getElementById('etaniinner'); // Find the inner container

    // I. Check current state and toggle
    if (etanibutton.textContent === 'Animate it') {
        // State: Animation is OFF -> Turn ON
        
        // 1. Change button text
        etanibutton.textContent = 'Stop Animation'; 

        // 2. Create and insert etaniinner
        createEtaniInner(etaniouter);
        
    } else if (etanibutton.textContent === 'Stop Animation') {
        // State: Animation is ON -> Turn OFF (II. etaniinner will be removed)

        // 1. Change button text
        etanibutton.textContent = 'Animate it';
        
        // 2. Remove etaniinner and all its contents (II. Event listeners inside are automatically unbound)
        if (etaniinner) {
            etaniinner.remove();
        }
    }
}


// Main function on window load
window.addEventListener('load', () => {
    
    // III. Call the function to add all necessary CSS dynamically
    addDynamicStyles();

    // I. Add div.etaniouter>button#etanibutton{Animate it} below div#etmainouter
    const etmainouter = document.getElementById('etmainouter');
    if (etmainouter) {
        // Create div.etaniouter
        const etaniouter = document.createElement('div');
        etaniouter.className = 'etaniouter';

        // Create button#etanibutton{Animate it}
        const etanibutton = document.createElement('button');
        etanibutton.id = 'etanibutton';
        etanibutton.textContent = 'Animate it';

        // II. Add click listener using the new toggle function
        etanibutton.addEventListener('click', toggleAnimation);

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

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

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

帖子 ejsoon »

代码: 全选

更改:

把<use href="tileid">的stroke和stroke-width屬性也跟fill屬性一樣處理。

用繁體中文回覆。

代码: 全选

// Function to dynamically add a <style> element with all necessary CSS rules
function addDynamicStyles() {
    // Check if styles are already present to avoid duplication
    if (document.getElementById('dynamic-et-styles')) {
        return;
    }

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

        /* II. Styles for etaniinner, etaniCtrl, etaniCol, etaniResult */
        .etaniinner {
            margin-top: 10px;
            /* Use margin to create vertical spacing instead of flex gap */
        }
        
        .etaniCtrl, .etaniCol, .etaniResult {
            /* Since we are not using flex, these will stack naturally */
            margin-bottom: 10px; 
            clear: both; /* Ensure they start on a new line after floats if necessary */
        }
        
        .etaniCol {
            border: 1px solid #aaa;
            padding: 5px;
            overflow: auto; /* To contain floated etaniItemLeft and etaniItemRight */
        }
        
        /* III, IV, V. Styles for etaniItem, etaniItemLeft, etaniItemRight */
        .etaniItem {
            min-height: 48px;
            border: 1px solid #ccc; /* V. border 1px for the entire item */
            box-sizing: border-box; /* V. box-sizing: border-box */
            width: 100%;
            margin-bottom: -1px; /* Overlap borders to prevent double thickness */
            overflow: auto; /* To contain floated elements */
        }
        
        .etaniItemLeft {
            float: left; /* III. Use float instead of flex */
            width: 60px; /* IV. fixed width: 60px */
            min-height: 48px; /* IV. min-height: 48px */
            border-right: 1px solid #ccc; /* V. right border 1px */
            
            /* Center content vertically and horizontally */
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            
            background-color: #e8e8e8;
            padding: 2px 0; /* Add a little internal padding */
        }
        
        .etaniItemRight {
            /* III. Use margin-left to fill remaining width */
            margin-left: 60px; /* Width of etaniItemLeft */
            min-height: 48px; 
            padding: 5px;
            background-color: #fff;
        }

        /* VI. Styles for the cloned SVG tile */
        .etanitileimg {
            display: block; /* Ensure it takes up its own line */
            margin: 0 auto; /* Center the SVG */
        }

        /* VII. Styles for tileid text */
        .tileid {
            text-align: center; /* Center the text */
            font-size: 10px;
            word-break: break-all;
            padding-top: 2px;
        }
    `;

    // Append the <style> element to the document's <head>
    document.head.appendChild(styleSheet);
}

// Function to handle the creation and population of etaniinner
function createEtaniInner(etaniouter) {
    // 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';

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

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

    // 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');
    
    // Get all <g> elements within .etwait
    const etwaitGroups = document.querySelectorAll('.etwait g');

    etdropUses.forEach((useElement, i) => {
        // IV. Get tileid
        const tileid = useElement.getAttribute('href').substring(1); // Remove the '#' prefix
        
        // Find the corresponding <use> element inside .etwait for this tileid
        let targetUse = null;
        
        // V. Search for the <use> element inside all <g> elements in .etwait
        for (const group of etwaitGroups) {
            const useInGroup = group.querySelector(`use[href="#${tileid}"]`);
            if (useInGroup) {
                targetUse = useInGroup;
                break;
            }
        }
        
        let etwaittransform = '';
        let etwaitfill = '';
        let etwaitstroke = ''; // New variable for stroke
        let etwaitstrokeWidth = ''; // New variable for stroke-width

        if (targetUse) {
            // V. Get the original transform, fill, stroke, and stroke-width
            etwaittransform = targetUse.getAttribute('transform') || '';
            etwaitfill = targetUse.getAttribute('fill') || '';
            etwaitstroke = targetUse.getAttribute('stroke') || ''; // Get stroke
            etwaitstrokeWidth = targetUse.getAttribute('stroke-width') || ''; // Get stroke-width

            // V. Modify the transform: translate(20,20), keep scale and rotate
            // Find scale(X) and rotate(Y) parts
            const scaleMatch = etwaittransform.match(/scale\(([^)]+)\)/);
            const rotateMatch = etwaittransform.match(/rotate\(([^)]+)\)/);
            
            const scalePart = scaleMatch ? scaleMatch[0] : '';
            const rotatePart = rotateMatch ? rotateMatch[0] : '';
            
            // Reconstruct transform: new translate + old scale + old rotate
            etwaittransform = `translate(20,20) ${scalePart} ${rotatePart}`.trim().replace(/\s+/g, ' '); // Clean up spaces
        }
        
        // VI. Find the original <g> in <defs> and clone it
        const originalTile = document.querySelector(`defs g#${tileid}`);
        
        // Create div.etaniItem
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';

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

        // Create div.etaniItemRight
        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        etaniItemRight.textContent = `Tile ID: ${tileid}, Transform: ${etwaittransform}`; // Example content

        // VI. Clone, modify, wrap, and append SVG
        if (originalTile) {
            const tileclone = originalTile.cloneNode(true);
            tileclone.removeAttribute('id'); // Remove id
            
            // Set the new transform
            if (etwaittransform) {
                tileclone.setAttribute('transform', etwaittransform);
            }
            
            // Set the fill, stroke, and stroke-width (處理新增的屬性)
            if (etwaitfill) {
                tileclone.setAttribute('fill', etwaitfill);
            }
            if (etwaitstroke) {
                tileclone.setAttribute('stroke', etwaitstroke);
            }
            if (etwaitstrokeWidth) {
                tileclone.setAttribute('stroke-width', etwaitstrokeWidth);
            }
            
            // Create the SVG wrapper
            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'; // Add CSS class
            
            svgWrapper.appendChild(tileclone);
            etaniItemLeft.appendChild(svgWrapper);
        }

        // VII. Add div.tileid below the SVG wrapper
        const tileidDiv = document.createElement('div');
        tileidDiv.className = 'tileid';
        tileidDiv.textContent = tileid;
        etaniItemLeft.appendChild(tileidDiv);

        // Append Left and Right to Item
        etaniItem.appendChild(etaniItemLeft);
        
        // Apply margin-left offset for etaniItemRight
        etaniItemRight.style.marginLeft = '60px'; 
        
        etaniItem.appendChild(etaniItemRight);

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

// Function to handle button click: toggle animation controls
function toggleAnimation(event) {
    const etanibutton = event.currentTarget;
    const etaniouter = etanibutton.parentNode;
    const etaniinner = document.getElementById('etaniinner'); // Find the inner container

    // I. Check current state and toggle
    if (etanibutton.textContent === 'Animate it') {
        // State: Animation is OFF -> Turn ON
        
        // 1. Change button text
        etanibutton.textContent = 'Stop Animation'; 

        // 2. Create and insert etaniinner
        createEtaniInner(etaniouter);
        
    } else if (etanibutton.textContent === 'Stop Animation') {
        // State: Animation is ON -> Turn OFF (II. etaniinner will be removed)

        // 1. Change button text
        etanibutton.textContent = 'Animate it';
        
        // 2. Remove etaniinner and all its contents (II. Event listeners inside are automatically unbound)
        if (etaniinner) {
            etaniinner.remove();
        }
    }
}


// Main function on window load
window.addEventListener('load', () => {
    
    // III. Call the function to add all necessary CSS dynamically
    addDynamicStyles();

    // I. Add div.etaniouter>button#etanibutton{Animate it} below div#etmainouter
    const etmainouter = document.getElementById('etmainouter');
    if (etmainouter) {
        // Create div.etaniouter
        const etaniouter = document.createElement('div');
        etaniouter.className = 'etaniouter';

        // Create button#etanibutton{Animate it}
        const etanibutton = document.createElement('button');
        etanibutton.id = 'etanibutton';
        etanibutton.textContent = 'Animate it';

        // II. Add click listener using the new toggle function
        etanibutton.addEventListener('click', toggleAnimation);

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

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

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

帖子 ejsoon »

代码: 全选

        etaniItemRight.style.marginLeft = '60px'; 

四,在div.etaniResult內含有:

img.etaniResultImage

a.etaniResultDownload

a.etaniResultRename

span.etaniResultSize

五,當點擊button#etanistart,js將執行以下操作:

5.1把當前svg#etmain的clone賦值給變量etani_clone。

5.2把etani_clone的.etwait賦值給etani_wait_clone,並把.etwait從etani_clone中刪掉。

5.3把etani_clone的.etdrop的transform設置為translate(240,240) scale(1,1),之後以base64的方式顯示至.etaniResultImage,同時span.etaniResultSize也顯示它的大小(byte),a.etaniResultDownload點擊就可以下載,a.etaniResultRename點擊就重命名所下載的文件名(點認文件名為ejtileAnimation_yyyymmdd_hhiiss.svg)。往後只要etani_clone更改,這些都要即時更新。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3805
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 157 次
被圈友点赞: 188 次
联系:

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

帖子 ejsoon »

代码: 全选

改動:

一,把「etaniItemRight.style.marginLeft = '60px'; 」去掉。

二,etaniItemRight的內容清空。

三,把「Stop Animation」改為「Close Ejtile Ani」。

四,在div.etaniResult內增加:

img.etaniResultImage+a.etaniResultDownload+a.etaniResultRename+span.etaniResultSize

其中etaniResultImage單獨一行居中,最大寬度為480px,當屏幕寬度小於480px,它就跟屏幕一樣寬。css不要用flex。

五,當按下etanibutton時,js將執行以下操作:

5.1把當前svg#etmain的clone賦值給變量etani_clone。

5.2把etani_clone中的.etwait刪掉。

5.3把etani_clone的id改為"etmainani",之後以base64的方式顯示至.etaniResultImage,同時span.etaniResultSize也顯示它的大小(byte),a.etaniResultDownload點擊就可以下載,a.etaniResultRename點擊就重命名所下載的文件名(點認文件名為ejtileAnimation_yyyymmdd_hhiiss.svg)。

往後只要etani_clone更改,這些都要即時更新。

用繁體中文回覆。

代码: 全选

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

// Function to dynamically add a <style> element with all necessary CSS rules
function addDynamicStyles() {
    // Check if styles are already present to avoid duplication
    if (document.getElementById('dynamic-et-styles')) {
        return;
    }

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

        /* II. Styles for etaniinner, etaniCtrl, etaniCol, etaniResult */
        .etaniinner {
            margin-top: 10px;
        }
        
        .etaniCtrl, .etaniCol {
            margin-bottom: 10px; 
            clear: both;
        }
        
        .etaniCol {
            border: 1px solid #aaa;
            padding: 5px;
            overflow: auto;
        }

        /* IV. Styles for etaniResult container and its elements */
        .etaniResult {
            text-align: center; /* Center content like image and buttons */
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #bbb;
            overflow: auto;
        }
        
        /* IV. etaniResultImage CSS: centered, max-width 480px, responsive */
        .etaniResultImage {
            display: block; /* Ensure it takes up its own line for centering */
            max-width: 480px; /* Maximum width */
            width: 100%; /* Becomes same width as screen if screen < 480px */
            height: auto;
            margin: 0 auto 10px auto; /* Center it, add bottom margin */
            border: 1px solid #000; /* Optional: for visibility */
        }
        
        .etaniResultDownload, .etaniResultRename {
            margin: 0 5px;
            display: inline-block;
        }
        
        .etaniResultSize {
            display: inline-block;
            margin-left: 10px;
            font-size: 0.8em;
            color: #555;
        }

        /* III, IV, V. Styles for etaniItem, etaniItemLeft, etaniItemRight */
        .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 {
            /* III. Now rely solely on float clearance and margin-left offset for content flow */
            min-height: 48px; 
            background-color: #fff;
            
            /* The element will naturally take up the remaining space if not floated, 
               but we use min-height and proper float management for the container. */
        }

        /* VI. Styles for the cloned SVG tile */
        .etanitileimg {
            display: block;
            margin: 0 auto;
        }

        /* VII. Styles for tileid text */
        .tileid {
            text-align: center;
            font-size: 10px;
            word-break: break-all;
            padding-top: 2px;
        }
    `;

    // Append the <style> element to the document's <head>
    document.head.appendChild(styleSheet);
}

// Function to encode SVG string to Base64 data URL
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 to format byte size (for span.etaniResultSize)
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 to update the etaniResult section with the current etaniClone state
function updateEtaniResult() {
    if (!etaniClone) return;

    // Get the SVG string and its size
    const svgString = new XMLSerializer().serializeToString(etaniClone);
    const sizeInBytes = new Blob([svgString]).size;
    const base64Url = svgToBase64(svgString);
    
    // Format the date for the download filename
    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`;

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

    if (imgElement && downloadElement && sizeElement) {
        // 5.3 Update Image
        imgElement.src = base64Url;

        // 5.3 Update Size
        sizeElement.textContent = `Size: ${formatBytes(sizeInBytes)}`;
        
        // 5.3 Update Download Link
        downloadElement.href = base64Url;
        downloadElement.download = defaultFilename;
        
        // 5.3 Rename Link (Handles renaming logic)
        renameElement.onclick = (e) => {
            e.preventDefault();
            let currentDownloadName = downloadElement.download;
            let newFilename = prompt("Enter new filename:", currentDownloadName);
            if (newFilename) {
                // Ensure it ends with .svg if not specified
                if (!newFilename.toLowerCase().endsWith('.svg')) {
                    newFilename += '.svg';
                }
                downloadElement.download = newFilename;
                alert(`Filename changed to: ${newFilename}`);
            }
        };
    }
}


// Function to handle the creation and population of etaniinner
function createEtaniInner(etaniouter) {
    // 5.1 Clone the original SVG
    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etaniClone = originalSvg.cloneNode(true);
    
    // 5.2 Remove .etwait from the clone
    const etwaitElement = etaniClone.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    
    // 5.3 Update clone ID
    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';

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

    // Create div.etaniResult (IV. Add result elements)
    const etaniResult = document.createElement('div');
    etaniResult.className = 'etaniResult';
    
    // IV. Add img, a, span elements
    const resultImg = document.createElement('img');
    resultImg.className = 'etaniResultImage';
    resultImg.alt = 'Rendered Ejtile Animation SVG';

    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';

    etaniResult.appendChild(resultImg);
    etaniResult.appendChild(document.createElement('br')); // Force buttons/span to new line
    etaniResult.appendChild(downloadLink);
    etaniResult.appendChild(renameLink);
    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) => {
        // IV. Get tileid
        const tileid = useElement.getAttribute('href').substring(1);
        
        // Find the corresponding <use> element inside .etwait for this tileid
        let targetUse = null;
        for (const group of etwaitGroups) {
            const useInGroup = group.querySelector(`use[href="#${tileid}"]`);
            if (useInGroup) {
                targetUse = useInGroup;
                break;
            }
        }
        
        let etwaittransform = '';
        let etwaitfill = '';
        let etwaitstroke = '';
        let etwaitstrokeWidth = '';

        if (targetUse) {
            // V. Get the original transform, fill, stroke, and stroke-width
            etwaittransform = targetUse.getAttribute('transform') || '';
            etwaitfill = targetUse.getAttribute('fill') || '';
            etwaitstroke = targetUse.getAttribute('stroke') || '';
            etwaitstrokeWidth = targetUse.getAttribute('stroke-width') || '';

            // V. Modify the transform
            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, ' ');
        }
        
        // VI. Find the original <g> in <defs> and clone it
        const originalTile = document.querySelector(`defs g#${tileid}`);
        
        // Create div.etaniItem
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';

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

        // Create div.etaniItemRight (II. Content is cleared)
        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        // etaniItemRight.textContent = ''; // Already empty
        
        // VI. Clone, modify, wrap, and append SVG
        if (originalTile) {
            const tileclone = originalTile.cloneNode(true);
            tileclone.removeAttribute('id');
            
            // Set the new transform and attributes
            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);
            
            // Create the SVG wrapper
            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);
        }

        // VII. Add div.tileid below the SVG wrapper
        const tileidDiv = document.createElement('div');
        tileidDiv.className = 'tileid';
        tileidDiv.textContent = tileid;
        etaniItemLeft.appendChild(tileidDiv);

        // Append Left and Right to Item
        etaniItem.appendChild(etaniItemLeft);
        
        // I. etaniItemRight.style.marginLeft = '60px'; is removed
        
        etaniItem.appendChild(etaniItemRight);

        // Append Item to etaniCol
        etaniCol.appendChild(etaniItem);
    });
    
    // 5. Update the result section immediately after etaniinner is fully created
    updateEtaniResult();
}

// Function to handle button click: toggle animation controls
function toggleAnimation(event) {
    const etanibutton = event.currentTarget;
    const etaniouter = etanibutton.parentNode;
    const etaniinner = document.getElementById('etaniinner'); // Find the inner container

    // I, III. Check current state and toggle
    if (etanibutton.textContent === 'Animate it') {
        // State: Animation is OFF -> Turn ON
        
        // 1. Change button text
        etanibutton.textContent = 'Close Ejtile Ani'; 

        // 2. Create and insert etaniinner
        createEtaniInner(etaniouter);
        
    } else if (etanibutton.textContent === 'Close Ejtile Ani') {
        // State: Animation is ON -> Turn OFF (II. etaniinner will be removed)

        // 1. Change button text
        etanibutton.textContent = 'Animate it';
        
        // 2. Remove etaniinner and all its contents (II. Event listeners inside are automatically unbound)
        if (etaniinner) {
            etaniinner.remove();
        }
        
        // Reset global clone variable
        etaniClone = null;
    }
}


// Main function on window load
window.addEventListener('load', () => {
    
    // Call the function to add all necessary CSS dynamically
    addDynamicStyles();

    // I. Add div.etaniouter>button#etanibutton{Animate it} below div#etmainouter
    const etmainouter = document.getElementById('etmainouter');
    if (etmainouter) {
        // Create div.etaniouter
        const etaniouter = document.createElement('div');
        etaniouter.className = 'etaniouter';

        // Create button#etanibutton{Animate it}
        const etanibutton = document.createElement('button');
        etanibutton.id = 'etanibutton';
        etanibutton.textContent = 'Animate it';

        // II. Add click listener
        etanibutton.addEventListener('click', toggleAnimation);

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

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

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

帖子 ejsoon »

代码: 全选

.etaniResultDownload, .etaniResultRename {
  display: inline-block;
  margin-right: 15px;
  text-decoration: none;
  padding: 5px 10px;
  border: 1px solid blue;
  color: blue;
}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3805
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 157 次
被圈友点赞: 188 次
联系:

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

帖子 ejsoon »

代码: 全选

改動:

一,css加上:
.etaniResultDownload, .etaniResultRename {
  display: inline-block;
  margin-right: 15px;
  text-decoration: none;
  padding: 5px 10px;
  border: 1px solid blue;
  color: blue;
}

同時把這兩個元素放進一個父元素div.etaniResultDR中,使它們水平居中。

二,為etaniItemRight加上margin-left: 60,以及padding:7px。

用繁體中文回覆。

代码: 全选

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

// Function to dynamically add a <style> element with all necessary CSS rules
function addDynamicStyles() {
    // Check if styles are already present to avoid duplication
    if (document.getElementById('dynamic-et-styles')) {
        return;
    }

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

        /* II. Styles for etaniinner, etaniCtrl, etaniCol, etaniResult */
        .etaniinner {
            margin-top: 10px;
        }
        
        .etaniCtrl, .etaniCol {
            margin-bottom: 10px; 
            clear: both;
        }
        
        .etaniCol {
            border: 1px solid #aaa;
            padding: 5px;
            overflow: auto;
        }

        /* IV. Styles for etaniResult container and its elements */
        .etaniResult {
            text-align: center; /* Center content like image and buttons */
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #bbb;
            overflow: auto;
        }
        
        /* New: Container for Download/Rename links for horizontal centering */
        .etaniResultDR {
            text-align: center;
            margin-bottom: 10px;
        }

        /* New: Styles for Download and Rename links (要求一) */
        .etaniResultDownload, .etaniResultRename {
            display: inline-block;
            margin-right: 15px;
            text-decoration: none;
            padding: 5px 10px;
            border: 1px solid blue;
            color: blue;
        }
        
        .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: 0.8em;
            color: #555;
        }

        /* III, IV, V. Styles for etaniItem, etaniItemLeft, etaniItemRight */
        .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 和 padding */
            margin-left: 60px; 
            padding: 7px;
            min-height: 48px; 
            background-color: #fff;
        }

        /* VI. Styles for the cloned SVG tile */
        .etanitileimg {
            display: block;
            margin: 0 auto;
        }

        /* VII. Styles for tileid text */
        .tileid {
            text-align: center;
            font-size: 10px;
            word-break: break-all;
            padding-top: 2px;
        }
    `;

    // Append the <style> element to the document's <head>
    document.head.appendChild(styleSheet);
}

// Function to encode SVG string to Base64 data URL
function svgToBase64(svgString) {
    const encoder = new TextEncoder();
    const svgBytes = encoder.encode(svgString);
    // Use btoa for Base64 encoding
    const byteString = String.fromCharCode.apply(null, svgBytes);
    const base64 = btoa(byteString);
    return `data:image/svg+xml;base64,${base64}`;
}

// Function to format byte size (for span.etaniResultSize)
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 to update the etaniResult section with the current etaniClone state
function updateEtaniResult() {
    if (!etaniClone) return;

    // Get the SVG string and its size
    const svgString = new XMLSerializer().serializeToString(etaniClone);
    const sizeInBytes = new Blob([svgString]).size;
    const base64Url = svgToBase64(svgString);
    
    // Format the date for the download filename
    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`;

    // Get result elements
    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) {
        // 5.3 Update Image
        imgElement.src = base64Url;

        // 5.3 Update Size
        sizeElement.textContent = `Size: ${formatBytes(sizeInBytes)}`;
        
        // 5.3 Update Download Link
        downloadElement.href = base64Url;
        downloadElement.download = defaultFilename;
        
        // 5.3 Rename Link (Handles renaming logic)
        renameElement.onclick = (e) => {
            e.preventDefault();
            let currentDownloadName = downloadElement.download;
            let newFilename = prompt("Enter new filename:", currentDownloadName);
            if (newFilename) {
                // Ensure it ends with .svg if not specified
                if (!newFilename.toLowerCase().endsWith('.svg')) {
                    newFilename += '.svg';
                }
                downloadElement.download = newFilename;
                alert(`Filename changed to: ${newFilename}`);
            }
        };
    }
}


// Function to handle the creation and population of etaniinner
function createEtaniInner(etaniouter) {
    // 5.1 Clone the original SVG
    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etaniClone = originalSvg.cloneNode(true);
    
    // 5.2 Remove .etwait from the clone
    const etwaitElement = etaniClone.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    
    // 5.3 Update clone ID
    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';

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

    // Create div.etaniResult
    const etaniResult = document.createElement('div');
    etaniResult.className = 'etaniResult';
    
    // IV. Add img element
    const resultImg = document.createElement('img');
    resultImg.className = 'etaniResultImage';
    resultImg.alt = 'Rendered Ejtile Animation SVG';

    // 要求一:創建父元素 div.etaniResultDR
    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 內容
    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) => {
        // IV. Get tileid
        const tileid = useElement.getAttribute('href').substring(1);
        
        // Find the corresponding <use> element inside .etwait for this tileid
        let targetUse = null;
        for (const group of etwaitGroups) {
            const useInGroup = group.querySelector(`use[href="#${tileid}"]`);
            if (useInGroup) {
                targetUse = useInGroup;
                break;
            }
        }
        
        let etwaittransform = '';
        let etwaitfill = '';
        let etwaitstroke = '';
        let etwaitstrokeWidth = '';

        if (targetUse) {
            // V. Get the original transform, fill, stroke, and stroke-width
            etwaittransform = targetUse.getAttribute('transform') || '';
            etwaitfill = targetUse.getAttribute('fill') || '';
            etwaitstroke = targetUse.getAttribute('stroke') || '';
            etwaitstrokeWidth = targetUse.getAttribute('stroke-width') || '';

            // V. Modify the transform
            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, ' ');
        }
        
        // VI. Find the original <g> in <defs> and clone it
        const originalTile = document.querySelector(`defs g#${tileid}`);
        
        // Create div.etaniItem
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';

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

        // Create div.etaniItemRight (II. Content is cleared)
        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        
        // VI. Clone, modify, wrap, and append SVG
        if (originalTile) {
            const tileclone = originalTile.cloneNode(true);
            tileclone.removeAttribute('id');
            
            // Set the new transform and attributes
            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);
            
            // Create the SVG wrapper
            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);
        }

        // VII. Add div.tileid below the SVG wrapper
        const tileidDiv = document.createElement('div');
        tileidDiv.className = 'tileid';
        tileidDiv.textContent = tileid;
        etaniItemLeft.appendChild(tileidDiv);

        // Append Left and Right to Item
        etaniItem.appendChild(etaniItemLeft);
        etaniItem.appendChild(etaniItemRight);

        // Append Item to etaniCol
        etaniCol.appendChild(etaniItem);
    });
    
    // 5. Update the result section immediately after etaniinner is fully created
    updateEtaniResult();
}

// Function to handle button click: toggle animation controls
function toggleAnimation(event) {
    const etanibutton = event.currentTarget;
    const etaniouter = etanibutton.parentNode;
    const etaniinner = document.getElementById('etaniinner'); // Find the inner container

    // I, III. Check current state and toggle
    if (etanibutton.textContent === 'Animate it') {
        // State: Animation is OFF -> Turn ON
        
        // 1. Change button text
        etanibutton.textContent = 'Close Ejtile Ani'; 

        // 2. Create and insert etaniinner
        createEtaniInner(etaniouter);
        
    } else if (etanibutton.textContent === 'Close Ejtile Ani') {
        // State: Animation is ON -> Turn OFF (II. etaniinner will be removed)

        // 1. Change button text
        etanibutton.textContent = 'Animate it';
        
        // 2. Remove etaniinner and all its contents (II. Event listeners inside are automatically unbound)
        if (etaniinner) {
            etaniinner.remove();
        }
        
        // Reset global clone variable
        etaniClone = null;
    }
}


// Main function on window load
window.addEventListener('load', () => {
    
    // Call the function to add all necessary CSS dynamically
    addDynamicStyles();

    // I. Add div.etaniouter>button#etanibutton{Animate it} below div#etmainouter
    const etmainouter = document.getElementById('etmainouter');
    if (etmainouter) {
        // Create div.etaniouter
        const etaniouter = document.createElement('div');
        etaniouter.className = 'etaniouter';

        // Create button#etanibutton{Animate it}
        const etanibutton = document.createElement('button');
        etanibutton.id = 'etanibutton';
        etanibutton.textContent = 'Animate it';

        // II. Add click listener
        etanibutton.addEventListener('click', toggleAnimation);

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

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

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

帖子 ejsoon »

代码: 全选

改動:

一,把.etaniResultDownload和.etaniResultRename的字號改為16px,把.etaniResultRename的顏色改為brown(包括字體顏色和邊框)。

二,把.etaniResultSize和.tileid的字號改為12px。往後最小字號只能是12px,字號的單位只能用px。

用繁體中文回覆。

代码: 全选

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

// Function to dynamically add a <style> element with all necessary CSS rules
function addDynamicStyles() {
    // Check if styles are already present to avoid duplication
    if (document.getElementById('dynamic-et-styles')) {
        return;
    }

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

        /* II. Styles for etaniinner, etaniCtrl, etaniCol, etaniResult */
        .etaniinner {
            margin-top: 10px;
        }
        
        .etaniCtrl, .etaniCol {
            margin-bottom: 10px; 
            clear: both;
        }
        
        .etaniCol {
            border: 1px solid #aaa;
            padding: 5px;
            overflow: auto;
        }

        /* IV. Styles for etaniResult container and its elements */
        .etaniResult {
            text-align: center; /* Center content like image and buttons */
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #bbb;
            overflow: auto;
        }
        
        /* Container for Download/Rename links for horizontal centering */
        .etaniResultDR {
            text-align: center;
            margin-bottom: 10px;
        }

        /* New: Styles for Download and Rename links (要求一:字號16px) */
        .etaniResultDownload, .etaniResultRename {
            display: inline-block;
            margin-right: 15px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 16px; /* 已修改為 16px */
        }
        
        /* New: .etaniResultDownload default style */
        .etaniResultDownload {
            border: 1px solid blue;
            color: blue;
        }

        /* New: .etaniResultRename style (要求一:顏色改為 brown) */
        .etaniResultRename {
            border: 1px solid brown; /* 邊框顏色改為 brown */
            color: brown; /* 字體顏色改為 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; /* 已修改為 12px */
            color: #555;
        }

        /* III, IV, V. Styles for etaniItem, etaniItemLeft, etaniItemRight */
        .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;
        }

        /* VI. Styles for the cloned SVG tile */
        .etanitileimg {
            display: block;
            margin: 0 auto;
        }

        /* VII. Styles for tileid text */
        .tileid {
            text-align: center;
            font-size: 12px; /* 已修改為 12px */
            word-break: break-all;
            padding-top: 2px;
        }
    `;

    // Append the <style> element to the document's <head>
    document.head.appendChild(styleSheet);
}

// Function to encode SVG string to Base64 data URL
function svgToBase64(svgString) {
    const encoder = new TextEncoder();
    const svgBytes = encoder.encode(svgString);
    // Use btoa for Base64 encoding
    const byteString = String.fromCharCode.apply(null, svgBytes);
    const base64 = btoa(byteString);
    return `data:image/svg+xml;base64,${base64}`;
}

// Function to format byte size (for span.etaniResultSize)
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 to update the etaniResult section with the current etaniClone state
function updateEtaniResult() {
    if (!etaniClone) return;

    // Get the SVG string and its size
    const svgString = new XMLSerializer().serializeToString(etaniClone);
    const sizeInBytes = new Blob([svgString]).size;
    const base64Url = svgToBase64(svgString);
    
    // Format the date for the download filename
    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`;

    // Get result elements
    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) {
        // 5.3 Update Image
        imgElement.src = base64Url;

        // 5.3 Update Size
        sizeElement.textContent = `Size: ${formatBytes(sizeInBytes)}`;
        
        // 5.3 Update Download Link
        downloadElement.href = base64Url;
        downloadElement.download = defaultFilename;
        
        // 5.3 Rename Link (Handles renaming logic)
        renameElement.onclick = (e) => {
            e.preventDefault();
            let currentDownloadName = downloadElement.download;
            let newFilename = prompt("Enter new filename:", currentDownloadName);
            if (newFilename) {
                // Ensure it ends with .svg if not specified
                if (!newFilename.toLowerCase().endsWith('.svg')) {
                    newFilename += '.svg';
                }
                downloadElement.download = newFilename;
                alert(`Filename changed to: ${newFilename}`);
            }
        };
    }
}


// Function to handle the creation and population of etaniinner
function createEtaniInner(etaniouter) {
    // 5.1 Clone the original SVG
    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etaniClone = originalSvg.cloneNode(true);
    
    // 5.2 Remove .etwait from the clone
    const etwaitElement = etaniClone.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    
    // 5.3 Update clone ID
    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';

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

    // Create div.etaniResult
    const etaniResult = document.createElement('div');
    etaniResult.className = 'etaniResult';
    
    // IV. Add img element
    const resultImg = document.createElement('img');
    resultImg.className = 'etaniResultImage';
    resultImg.alt = 'Rendered Ejtile Animation SVG';

    // 要求:創建父元素 div.etaniResultDR
    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 內容
    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) => {
        // IV. Get tileid
        const tileid = useElement.getAttribute('href').substring(1);
        
        // Find the corresponding <use> element inside .etwait for this tileid
        let targetUse = null;
        for (const group of etwaitGroups) {
            const useInGroup = group.querySelector(`use[href="#${tileid}"]`);
            if (useInGroup) {
                targetUse = useInGroup;
                break;
            }
        }
        
        let etwaittransform = '';
        let etwaitfill = '';
        let etwaitstroke = '';
        let etwaitstrokeWidth = '';

        if (targetUse) {
            // V. Get the original transform, fill, stroke, and stroke-width
            etwaittransform = targetUse.getAttribute('transform') || '';
            etwaitfill = targetUse.getAttribute('fill') || '';
            etwaitstroke = targetUse.getAttribute('stroke') || '';
            etwaitstrokeWidth = targetUse.getAttribute('stroke-width') || '';

            // V. Modify the transform
            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, ' ');
        }
        
        // VI. Find the original <g> in <defs> and clone it
        const originalTile = document.querySelector(`defs g#${tileid}`);
        
        // Create div.etaniItem
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';

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

        // Create div.etaniItemRight (II. Content is cleared)
        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        
        // VI. Clone, modify, wrap, and append SVG
        if (originalTile) {
            const tileclone = originalTile.cloneNode(true);
            tileclone.removeAttribute('id');
            
            // Set the new transform and attributes
            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);
            
            // Create the SVG wrapper
            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);
        }

        // VII. Add div.tileid below the SVG wrapper
        const tileidDiv = document.createElement('div');
        tileidDiv.className = 'tileid';
        tileidDiv.textContent = tileid;
        etaniItemLeft.appendChild(tileidDiv);

        // Append Left and Right to Item
        etaniItem.appendChild(etaniItemLeft);
        etaniItem.appendChild(etaniItemRight);

        // Append Item to etaniCol
        etaniCol.appendChild(etaniItem);
    });
    
    // 5. Update the result section immediately after etaniinner is fully created
    updateEtaniResult();
}

// Function to handle button click: toggle animation controls
function toggleAnimation(event) {
    const etanibutton = event.currentTarget;
    const etaniouter = etanibutton.parentNode;
    const etaniinner = document.getElementById('etaniinner'); // Find the inner container

    // I, III. Check current state and toggle
    if (etanibutton.textContent === 'Animate it') {
        // State: Animation is OFF -> Turn ON
        
        // 1. Change button text
        etanibutton.textContent = 'Close Ejtile Ani'; 

        // 2. Create and insert etaniinner
        createEtaniInner(etaniouter);
        
    } else if (etanibutton.textContent === 'Close Ejtile Ani') {
        // State: Animation is ON -> Turn OFF (II. etaniinner will be removed)

        // 1. Change button text
        etanibutton.textContent = 'Animate it';
        
        // 2. Remove etaniinner and all its contents (II. Event listeners inside are automatically unbound)
        if (etaniinner) {
            etaniinner.remove();
        }
        
        // Reset global clone variable
        etaniClone = null;
    }
}


// Main function on window load
window.addEventListener('load', () => {
    
    // Call the function to add all necessary CSS dynamically
    addDynamicStyles();

    // I. Add div.etaniouter>button#etanibutton{Animate it} below div#etmainouter
    const etmainouter = document.getElementById('etmainouter');
    if (etmainouter) {
        // Create div.etaniouter
        const etaniouter = document.createElement('div');
        etaniouter.className = 'etaniouter';

        // Create button#etanibutton{Animate it}
        const etanibutton = document.createElement('button');
        etanibutton.id = 'etanibutton';
        etanibutton.textContent = 'Animate it';

        // II. Add click listener
        etanibutton.addEventListener('click', toggleAnimation);

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

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

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

帖子 ejsoon »

代码: 全选

改動:

一,在div.etaniCtrl中增加.etaniContent

二,在.etaniContent中增加a.etaniCenter{Center},當點擊它時,etani_clone中的.etdrop的transform就會改為translate(240,240) scale(1,1)。

用繁體中文回覆。

代码: 全选

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

// Function to dynamically add a <style> element with all necessary CSS rules
function addDynamicStyles() {
    // Check if styles are already present to avoid duplication
    if (document.getElementById('dynamic-et-styles')) {
        return;
    }

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

        /* II. Styles for etaniinner, etaniCtrl, etaniCol, etaniResult */
        .etaniinner {
            margin-top: 10px;
        }
        
        .etaniCtrl {
            margin-bottom: 10px; 
            clear: both;
            padding: 5px;
            border: 1px solid #c0c0c0; /* Optional: to frame the control area */
        }
        
        /* New: Styles for etaniContent (Flex is replaced by text-align for centering) */
        .etaniContent {
            text-align: center;
            padding: 5px 0;
        }

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

        /* IV. Styles for etaniResult container and its elements */
        .etaniResult {
            text-align: center;
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #bbb;
            overflow: auto;
        }
        
        /* Container for Download/Rename links for horizontal centering */
        .etaniResultDR {
            text-align: center;
            margin-bottom: 10px;
        }

        /* Styles for control links (like Center button) */
        .etaniCenter {
            display: inline-block;
            margin: 0 5px;
            text-decoration: none;
            padding: 5px 10px;
            border: 1px solid green;
            color: green;
            font-size: 14px; /* Default size for controls */
        }

        /* Styles for Download and Rename links (要求一:字號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;
        }
        
        .etaniResultSize {
            display: inline-block;
            margin-left: 10px;
            font-size: 12px; /* 最小字號 12px */
            color: #555;
        }

        /* III, IV, V. Styles for etaniItem, etaniItemLeft, etaniItemRight */
        .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;
        }

        /* VI. Styles for the cloned SVG tile */
        .etanitileimg {
            display: block;
            margin: 0 auto;
        }

        /* VII. Styles for tileid text */
        .tileid {
            text-align: center;
            font-size: 12px; /* 最小字號 12px */
            word-break: break-all;
            padding-top: 2px;
        }
    `;

    // Append the <style> element to the document's <head>
    document.head.appendChild(styleSheet);
}

// Function to encode SVG string to Base64 data URL
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 to format byte size (for span.etaniResultSize)
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 to update the etaniResult section with the current etaniClone state
function updateEtaniResult() {
    if (!etaniClone) return;

    // Get the SVG string and its size
    const svgString = new XMLSerializer().serializeToString(etaniClone);
    const sizeInBytes = new Blob([svgString]).size;
    const base64Url = svgToBase64(svgString);
    
    // Format the date for the download filename
    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`;

    // Get result elements
    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) {
        // 5.3 Update Image
        imgElement.src = base64Url;

        // 5.3 Update Size
        sizeElement.textContent = `Size: ${formatBytes(sizeInBytes)}`;
        
        // 5.3 Update Download Link
        downloadElement.href = base64Url;
        downloadElement.download = defaultFilename;
        
        // 5.3 Rename Link (Handles renaming logic)
        renameElement.onclick = (e) => {
            e.preventDefault();
            let currentDownloadName = downloadElement.download;
            let newFilename = prompt("Enter new filename:", currentDownloadName);
            if (newFilename) {
                // Ensure it ends with .svg if not specified
                if (!newFilename.toLowerCase().endsWith('.svg')) {
                    newFilename += '.svg';
                }
                downloadElement.download = newFilename;
                alert(`Filename changed to: ${newFilename}`);
            }
        };
    }
}


// Function to handle the 'Center' button click
function handleCenterClick(e) {
    e.preventDefault();
    if (etaniClone) {
        // 獲取 etani_clone 中的 .etdrop 元素
        const etdrop = etaniClone.querySelector('.etdrop');
        if (etdrop) {
            // 設置新的 transform 屬性
            etdrop.setAttribute('transform', 'translate(240,240) scale(1,1)');
            console.log("etdrop transform updated to: translate(240,240) scale(1,1)");
            
            // 即時更新結果顯示
            updateEtaniResult();
        } else {
            console.warn('.etdrop not found in the cloned SVG.');
        }
    } else {
        console.warn('etaniClone is not initialized.');
    }
}


// Function to handle the creation and population of etaniinner
function createEtaniInner(etaniouter) {
    // 5.1 Clone the original SVG
    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etaniClone = originalSvg.cloneNode(true);
    
    // 5.2 Remove .etwait from the clone
    const etwaitElement = etaniClone.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    
    // 5.3 Update clone ID
    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';
    
    // 要求一:在 div.etaniCtrl 中增加 div.etaniContent
    const etaniContent = document.createElement('div');
    etaniContent.className = 'etaniContent';
    
    // 要求二:在 .etaniContent 中增加 a.etaniCenter{Center}
    const centerLink = document.createElement('a');
    centerLink.className = 'etaniCenter';
    centerLink.textContent = 'Center';
    centerLink.href = '#';
    centerLink.addEventListener('click', handleCenterClick);
    
    // 組合 etaniCtrl 內容
    etaniContent.appendChild(centerLink);
    etaniCtrl.appendChild(etaniContent);


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

    // Create div.etaniResult
    const etaniResult = document.createElement('div');
    etaniResult.className = 'etaniResult';
    
    // IV. Add img element
    const resultImg = document.createElement('img');
    resultImg.className = 'etaniResultImage';
    resultImg.alt = 'Rendered Ejtile Animation SVG';

    // 創建父元素 div.etaniResultDR
    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 內容
    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 (unchanged tile processing logic)
    const etdropUses = document.querySelectorAll('.etdrop use');
    const etwaitGroups = document.querySelectorAll('.etwait g');

    etdropUses.forEach((useElement, i) => {
        // IV. Get tileid
        const tileid = useElement.getAttribute('href').substring(1);
        
        // Find the corresponding <use> element inside .etwait for this tileid
        let targetUse = null;
        for (const group of etwaitGroups) {
            const useInGroup = group.querySelector(`use[href="#${tileid}"]`);
            if (useInGroup) {
                targetUse = useInGroup;
                break;
            }
        }
        
        let etwaittransform = '';
        let etwaitfill = '';
        let etwaitstroke = '';
        let etwaitstrokeWidth = '';

        if (targetUse) {
            // V. Get the original transform, fill, stroke, and stroke-width
            etwaittransform = targetUse.getAttribute('transform') || '';
            etwaitfill = targetUse.getAttribute('fill') || '';
            etwaitstroke = targetUse.getAttribute('stroke') || '';
            etwaitstrokeWidth = targetUse.getAttribute('stroke-width') || '';

            // V. Modify the transform
            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, ' ');
        }
        
        // VI. Find the original <g> in <defs> and clone it
        const originalTile = document.querySelector(`defs g#${tileid}`);
        
        // Create div.etaniItem
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';

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

        // Create div.etaniItemRight (II. Content is cleared)
        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        
        // VI. Clone, modify, wrap, and append SVG
        if (originalTile) {
            const tileclone = originalTile.cloneNode(true);
            tileclone.removeAttribute('id');
            
            // Set the new transform and attributes
            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);
            
            // Create the SVG wrapper
            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);
        }

        // VII. Add div.tileid below the SVG wrapper
        const tileidDiv = document.createElement('div');
        tileidDiv.className = 'tileid';
        tileidDiv.textContent = tileid;
        etaniItemLeft.appendChild(tileidDiv);

        // Append Left and Right to Item
        etaniItem.appendChild(etaniItemLeft);
        etaniItem.appendChild(etaniItemRight);

        // Append Item to etaniCol
        etaniCol.appendChild(etaniItem);
    });
    
    // 5. Update the result section immediately after etaniinner is fully created
    updateEtaniResult();
}

// Function to handle button click: toggle animation controls
function toggleAnimation(event) {
    const etanibutton = event.currentTarget;
    const etaniouter = etanibutton.parentNode;
    const etaniinner = document.getElementById('etaniinner'); // Find the inner container

    // I, III. Check current state and toggle
    if (etanibutton.textContent === 'Animate it') {
        // State: Animation is OFF -> Turn ON
        
        // 1. Change button text
        etanibutton.textContent = 'Close Ejtile Ani'; 

        // 2. Create and insert etaniinner
        createEtaniInner(etaniouter);
        
    } else if (etanibutton.textContent === 'Close Ejtile Ani') {
        // State: Animation is ON -> Turn OFF (II. etaniinner will be removed)

        // 1. Change button text
        etanibutton.textContent = 'Animate it';
        
        // 2. Remove etaniinner and all its contents (II. Event listeners inside are automatically unbound)
        if (etaniinner) {
            etaniinner.remove();
        }
        
        // Reset global clone variable
        etaniClone = null;
    }
}


// Main function on window load
window.addEventListener('load', () => {
    
    // Call the function to add all necessary CSS dynamically
    addDynamicStyles();

    // I. Add div.etaniouter>button#etanibutton{Animate it} below div#etmainouter
    const etmainouter = document.getElementById('etmainouter');
    if (etmainouter) {
        // Create div.etaniouter
        const etaniouter = document.createElement('div');
        etaniouter.className = 'etaniouter';

        // Create button#etanibutton{Animate it}
        const etanibutton = document.createElement('button');
        etanibutton.id = 'etanibutton';
        etanibutton.textContent = 'Animate it';

        // II. Add click listener
        etanibutton.addEventListener('click', toggleAnimation);

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

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

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

帖子 ejsoon »

代码: 全选

改動:

一,在div.etaniCtrl中增加.etaniContent

二,在.etaniContent中增加a.etaniCenter{Center},當點擊它時,etani_clone中的.etdrop的transform就會改為translate(240,240) scale(1,1)。

用繁體中文回覆。

代码: 全选

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

// Function to dynamically add a <style> element with all necessary CSS rules
function addDynamicStyles() {
    // Check if styles are already present to avoid duplication
    if (document.getElementById('dynamic-et-styles')) {
        return;
    }

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

        /* II. Styles for etaniinner, etaniCtrl, etaniCol, etaniResult */
        .etaniinner {
            margin-top: 10px;
        }
        
        .etaniCtrl {
            margin-bottom: 10px; 
            clear: both;
            padding: 5px;
            border: 1px solid #c0c0c0; /* Optional: to frame the control area */
        }
        
        /* New: Styles for etaniContent (Flex is replaced by text-align for centering) */
        .etaniContent {
            text-align: center;
            padding: 5px 0;
        }

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

        /* IV. Styles for etaniResult container and its elements */
        .etaniResult {
            text-align: center;
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #bbb;
            overflow: auto;
        }
        
        /* Container for Download/Rename links for horizontal centering */
        .etaniResultDR {
            text-align: center;
            margin-bottom: 10px;
        }

        /* Styles for control links (like Center button) */
        .etaniCenter {
            display: inline-block;
            margin: 0 5px;
            text-decoration: none;
            padding: 5px 10px;
            border: 1px solid green;
            color: green;
            font-size: 14px; /* Default size for controls */
        }

        /* Styles for Download and Rename links (要求一:字號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;
        }
        
        .etaniResultSize {
            display: inline-block;
            margin-left: 10px;
            font-size: 12px; /* 最小字號 12px */
            color: #555;
        }

        /* III, IV, V. Styles for etaniItem, etaniItemLeft, etaniItemRight */
        .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;
        }

        /* VI. Styles for the cloned SVG tile */
        .etanitileimg {
            display: block;
            margin: 0 auto;
        }

        /* VII. Styles for tileid text */
        .tileid {
            text-align: center;
            font-size: 12px; /* 最小字號 12px */
            word-break: break-all;
            padding-top: 2px;
        }
    `;

    // Append the <style> element to the document's <head>
    document.head.appendChild(styleSheet);
}

// Function to encode SVG string to Base64 data URL
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 to format byte size (for span.etaniResultSize)
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 to update the etaniResult section with the current etaniClone state
function updateEtaniResult() {
    if (!etaniClone) return;

    // Get the SVG string and its size
    const svgString = new XMLSerializer().serializeToString(etaniClone);
    const sizeInBytes = new Blob([svgString]).size;
    const base64Url = svgToBase64(svgString);
    
    // Format the date for the download filename
    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`;

    // Get result elements
    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) {
        // 5.3 Update Image
        imgElement.src = base64Url;

        // 5.3 Update Size
        sizeElement.textContent = `Size: ${formatBytes(sizeInBytes)}`;
        
        // 5.3 Update Download Link
        downloadElement.href = base64Url;
        downloadElement.download = defaultFilename;
        
        // 5.3 Rename Link (Handles renaming logic)
        renameElement.onclick = (e) => {
            e.preventDefault();
            let currentDownloadName = downloadElement.download;
            let newFilename = prompt("Enter new filename:", currentDownloadName);
            if (newFilename) {
                // Ensure it ends with .svg if not specified
                if (!newFilename.toLowerCase().endsWith('.svg')) {
                    newFilename += '.svg';
                }
                downloadElement.download = newFilename;
                alert(`Filename changed to: ${newFilename}`);
            }
        };
    }
}


// Function to handle the 'Center' button click
function handleCenterClick(e) {
    e.preventDefault();
    if (etaniClone) {
        // 獲取 etani_clone 中的 .etdrop 元素
        const etdrop = etaniClone.querySelector('.etdrop');
        if (etdrop) {
            // 設置新的 transform 屬性
            etdrop.setAttribute('transform', 'translate(240,240) scale(1,1)');
            console.log("etdrop transform updated to: translate(240,240) scale(1,1)");
            
            // 即時更新結果顯示
            updateEtaniResult();
        } else {
            console.warn('.etdrop not found in the cloned SVG.');
        }
    } else {
        console.warn('etaniClone is not initialized.');
    }
}


// Function to handle the creation and population of etaniinner
function createEtaniInner(etaniouter) {
    // 5.1 Clone the original SVG
    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    etaniClone = originalSvg.cloneNode(true);
    
    // 5.2 Remove .etwait from the clone
    const etwaitElement = etaniClone.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    
    // 5.3 Update clone ID
    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';
    
    // 要求一:在 div.etaniCtrl 中增加 div.etaniContent
    const etaniContent = document.createElement('div');
    etaniContent.className = 'etaniContent';
    
    // 要求二:在 .etaniContent 中增加 a.etaniCenter{Center}
    const centerLink = document.createElement('a');
    centerLink.className = 'etaniCenter';
    centerLink.textContent = 'Center';
    centerLink.href = '#';
    centerLink.addEventListener('click', handleCenterClick);
    
    // 組合 etaniCtrl 內容
    etaniContent.appendChild(centerLink);
    etaniCtrl.appendChild(etaniContent);


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

    // Create div.etaniResult
    const etaniResult = document.createElement('div');
    etaniResult.className = 'etaniResult';
    
    // IV. Add img element
    const resultImg = document.createElement('img');
    resultImg.className = 'etaniResultImage';
    resultImg.alt = 'Rendered Ejtile Animation SVG';

    // 創建父元素 div.etaniResultDR
    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 內容
    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 (unchanged tile processing logic)
    const etdropUses = document.querySelectorAll('.etdrop use');
    const etwaitGroups = document.querySelectorAll('.etwait g');

    etdropUses.forEach((useElement, i) => {
        // IV. Get tileid
        const tileid = useElement.getAttribute('href').substring(1);
        
        // Find the corresponding <use> element inside .etwait for this tileid
        let targetUse = null;
        for (const group of etwaitGroups) {
            const useInGroup = group.querySelector(`use[href="#${tileid}"]`);
            if (useInGroup) {
                targetUse = useInGroup;
                break;
            }
        }
        
        let etwaittransform = '';
        let etwaitfill = '';
        let etwaitstroke = '';
        let etwaitstrokeWidth = '';

        if (targetUse) {
            // V. Get the original transform, fill, stroke, and stroke-width
            etwaittransform = targetUse.getAttribute('transform') || '';
            etwaitfill = targetUse.getAttribute('fill') || '';
            etwaitstroke = targetUse.getAttribute('stroke') || '';
            etwaitstrokeWidth = targetUse.getAttribute('stroke-width') || '';

            // V. Modify the transform
            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, ' ');
        }
        
        // VI. Find the original <g> in <defs> and clone it
        const originalTile = document.querySelector(`defs g#${tileid}`);
        
        // Create div.etaniItem
        const etaniItem = document.createElement('div');
        etaniItem.className = 'etaniItem';

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

        // Create div.etaniItemRight (II. Content is cleared)
        const etaniItemRight = document.createElement('div');
        etaniItemRight.className = 'etaniItemRight';
        
        // VI. Clone, modify, wrap, and append SVG
        if (originalTile) {
            const tileclone = originalTile.cloneNode(true);
            tileclone.removeAttribute('id');
            
            // Set the new transform and attributes
            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);
            
            // Create the SVG wrapper
            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);
        }

        // VII. Add div.tileid below the SVG wrapper
        const tileidDiv = document.createElement('div');
        tileidDiv.className = 'tileid';
        tileidDiv.textContent = tileid;
        etaniItemLeft.appendChild(tileidDiv);

        // Append Left and Right to Item
        etaniItem.appendChild(etaniItemLeft);
        etaniItem.appendChild(etaniItemRight);

        // Append Item to etaniCol
        etaniCol.appendChild(etaniItem);
    });
    
    // 5. Update the result section immediately after etaniinner is fully created
    updateEtaniResult();
}

// Function to handle button click: toggle animation controls
function toggleAnimation(event) {
    const etanibutton = event.currentTarget;
    const etaniouter = etanibutton.parentNode;
    const etaniinner = document.getElementById('etaniinner'); // Find the inner container

    // I, III. Check current state and toggle
    if (etanibutton.textContent === 'Animate it') {
        // State: Animation is OFF -> Turn ON
        
        // 1. Change button text
        etanibutton.textContent = 'Close Ejtile Ani'; 

        // 2. Create and insert etaniinner
        createEtaniInner(etaniouter);
        
    } else if (etanibutton.textContent === 'Close Ejtile Ani') {
        // State: Animation is ON -> Turn OFF (II. etaniinner will be removed)

        // 1. Change button text
        etanibutton.textContent = 'Animate it';
        
        // 2. Remove etaniinner and all its contents (II. Event listeners inside are automatically unbound)
        if (etaniinner) {
            etaniinner.remove();
        }
        
        // Reset global clone variable
        etaniClone = null;
    }
}


// Main function on window load
window.addEventListener('load', () => {
    
    // Call the function to add all necessary CSS dynamically
    addDynamicStyles();

    // I. Add div.etaniouter>button#etanibutton{Animate it} below div#etmainouter
    const etmainouter = document.getElementById('etmainouter');
    if (etmainouter) {
        // Create div.etaniouter
        const etaniouter = document.createElement('div');
        etaniouter.className = 'etaniouter';

        // Create button#etanibutton{Animate it}
        const etanibutton = document.createElement('button');
        etanibutton.id = 'etanibutton';
        etanibutton.textContent = 'Animate it';

        // II. Add click listener
        etanibutton.addEventListener('click', toggleAnimation);

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

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

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

帖子 ejsoon »

代码: 全选

<animateTransform attributeName="transform" attributeType="XML" type="translate" values="0" dur="1s" fill="freeze" additive="sum"/>
<animateTransform attributeName="transform" attributeType="XML" type="scale" values="0" dur="1s" fill="freeze" additive="sum"/>
<animateTransform attributeName="transform" attributeType="XML" type="rotate" values="0" dur="1s" fill="freeze" additive="sum"/>

repeatCount="indefinite"
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 3805
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 157 次
被圈友点赞: 188 次
联系:

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

帖子 ejsoon »

代码: 全选

改動:

一,在div.etaniCtrl中增加.etaniSetting和.etaniAllAppend。它們和.etaniContent的css-display都是inline-block。帶灰色邊框。帶上label文字。

二,在.etaniSetting中增加radio:.etaniSettingMode.etaniSettingFreeze{freeze}和.etaniSettingMode.etaniSettingRepeat{repeat}。默認選freeze。樣式要美觀,不要用html原生的。

三,在.etaniAllAppend中增加兩個button:.etaniAllAppendTransform{transform},etaniAllAppendOpacity{opacity}。

四,當按下.etaniAllAppendTransform時,js將做以下操作:

4.1,每個.etaniItemRight都增加一個.etaniAnimate。(它將三個標籤合為一個.etaniAnimate來表示)

.etaniAnimate內包含:

span.etaniAnimateName+
span.etaniAnimateDur+
div.etaniAnimateValue

其中.etaniAnimateName是灰底白字,內容是「Transform」。

.etaniAnimateDur的innerHTML為「dur: n」,n指一個數字,也有可能是小數。

div.etaniAnimateValue的內含「span.etaniAVAdd+span.etaniAVLabel{values : }+div.etaniAV」。

div.etaniAV中可能含有一個或多個span.etaniAVItem,起初只含有一個。

每個span.etaniAVItem的長寬為24px,背景為暗橙色,邊框為虛線天藍色,左右相距5px。

span.etaniAVAdd的長寬為24px,背景為淺綠色,邊框為細線近淺綠色,與右相距5px,內容為一個加號的svg。

4.2,在etani_clone中,.etdrop裡面的<use/>內都要添加三個animateTransform標籤:

<animateTransform attributeName="transform" attributeType="XML" type="translate" values="" dur="1s" fill="freeze"/>
<animateTransform attributeName="transform" attributeType="XML" type="scale" values="" dur="1s" fill="freeze"/>
<animateTransform attributeName="transform" attributeType="XML" type="rotate" values="" dur="1s" fill="freeze"/>

它們values的初始值是當前的svg#etmain中的.etdrop中對應的<use/>的transform="translate(…) scale(…) rotate(…)"的值。

五,當.etaniSettingMode切換為repeat時,所有的fill="freeze"都替換為repeatCount="indefinite"。

六,當點擊etaniAVAdd時,etani_clone中所對應的<use/>中的三個<animateTransform>的values都增加一個value,這個value是當前的svg#etmain中對應的<use/>的transform的值。

七,在etaniContent中,在最前面新增a.etaniContentHTML,當點擊時,頁面將彈出一個居中的div,css為fixed,寬度是98%,高度是48%。內含一個textarea,內容是etani_clone的outerHTML。同時還包含關閉圖標在右上角。

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

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

代码: 全选

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

// Function to dynamically add a <style> element with all necessary CSS rules
function addDynamicStyles() {
    // Check if styles are already present to avoid duplication
    if (document.getElementById('dynamic-et-styles')) {
        return;
    }

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

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

        /* Control Area - New and Modified */
        .etaniCtrl {
            margin-bottom: 10px; 
            clear: both;
            padding: 5px;
            border: 1px solid #c0c0c0;
            text-align: left; /* Default alignment for blocks */
            overflow: auto;
        }
        
        .etaniContent, .etaniSetting, .etaniAllAppend {
            display: inline-block; /* 讓它們水平排列 */
            margin-right: 15px;
            vertical-align: top;
            padding: 5px 10px;
            border: 1px solid #ccc; /* 灰色邊框 */
        }
        
        .etaniCtrl label {
            display: block;
            font-weight: bold;
            margin-bottom: 5px;
            font-size: 14px;
        }

        /* II. Radio Button Styles (Custom look, not native) */
        .etaniSettingMode {
            display: inline-block;
            margin-right: 10px;
            cursor: pointer;
            font-size: 14px;
            padding: 3px 5px;
            border: 1px solid #ddd;
            background-color: #eee;
            user-select: none;
        }

        .etaniSettingMode.selected {
            background-color: #99ccff;
            border-color: #3399ff;
            font-weight: bold;
        }

        /* III. Append Buttons */
        .etaniAllAppend button {
            display: block; /* Stack vertically */
            width: 100%;
            margin-bottom: 5px;
            padding: 5px 10px;
            cursor: pointer;
            font-size: 14px;
        }

        /* IV. etaniAnimate Component Styles */
        .etaniAnimate {
            border: 1px solid #666;
            margin-top: 5px;
            padding: 5px;
            font-size: 12px; /* Base font size for component */
        }
        
        .etaniAnimateName {
            background-color: gray;
            color: white;
            padding: 2px 4px;
            margin-right: 5px;
            font-size: 12px;
        }

        .etaniAnimateDur {
            margin-right: 10px;
            font-size: 12px;
        }

        .etaniAnimateValue {
            margin-top: 5px;
            clear: both;
            overflow: auto;
        }
        
        .etaniAVLabel {
            margin-right: 5px;
            font-size: 12px;
        }

        .etaniAV {
            display: inline-block;
            vertical-align: middle;
        }
        
        /* IV. .etaniAVItem and .etaniAVAdd Styles */
        .etaniAVAdd, .etaniAVItem {
            display: inline-block;
            width: 24px;
            height: 24px;
            line-height: 24px; /* Center content vertically */
            text-align: center;
            cursor: pointer;
            vertical-align: middle;
            margin-right: 5px;
            box-sizing: border-box;
            position: relative;
        }

        .etaniAVItem {
            background-color: #ff8c00; /* Dark Orange */
            border: 1px dashed #00bfff; /* Dashed Sky Blue */
        }

        .etaniAVAdd {
            background-color: #90ee90; /* Light Green */
            border: 1px solid #66cdaa; /* Light Sea Green (close to light green) */
        }
        
        /* Plus SVG inside .etaniAVAdd */
        .etaniAVAdd svg {
            width: 100%;
            height: 100%;
            vertical-align: top;
        }

        /* Result Area */
        .etaniResult {
            text-align: center;
            margin-bottom: 10px;
            padding: 10px;
            border: 1px solid #bbb;
            overflow: auto;
        }
        
        .etaniResultDR {
            text-align: center;
            margin-bottom: 10px;
        }

        .etaniResultDownload, .etaniResultRename, .etaniCenter, .etaniContentHTML {
            display: inline-block;
            margin-right: 15px;
            text-decoration: none;
            padding: 5px 10px;
            font-size: 16px; 
            cursor: pointer;
        }
        
        .etaniResultDownload {
            border: 1px solid blue;
            color: blue;
        }

        .etaniResultRename {
            border: 1px solid brown;
            color: brown;
        }

        .etaniCenter {
            border: 1px solid green;
            color: green;
        }
        
        /* New: etaniContentHTML style */
        .etaniContentHTML {
            border: 1px solid purple;
            color: purple;
        }

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

        /* Item 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;
        }
        
        /* VII. Modal/Popup Styles */
        .etani-modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.5);
            z-index: 1000;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .etani-modal-content {
            background: white;
            width: 98%; 
            height: 48vh; /* 48% height of viewport */
            position: relative;
            padding: 10px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
            border: 1px solid #000;
            box-sizing: border-box;
        }

        .etani-modal-content textarea {
            width: 100%;
            height: calc(100% - 30px); /* Adjust height for button/padding */
            font-family: monospace;
            font-size: 12px;
            border: none;
            resize: none;
            box-sizing: border-box;
        }

        .etani-modal-close {
            position: absolute;
            top: 5px;
            right: 10px;
            font-size: 24px;
            cursor: pointer;
            color: #333;
        }
    `;

    // Append the <style> element to the document's <head>
    document.head.appendChild(styleSheet);
}

// Function to encode SVG string to Base64 data URL
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 to format byte size (for span.etaniResultSize)
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 to update the etaniResult section with the current etani_clone state
function updateEtaniResult() {
    if (!etani_clone) return;

    // Get the SVG string and its size
    const svgString = new XMLSerializer().serializeToString(etani_clone);
    const sizeInBytes = new Blob([svgString]).size;
    const base64Url = svgToBase64(svgString);
    
    // Format the date for the download filename
    const now = new Date();
    const yyyy = String(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`;

    // Get result elements
    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) {
        // 5.3 Update Image
        imgElement.src = base64Url;

        // 5.3 Update Size
        sizeElement.textContent = `Size: ${formatBytes(sizeInBytes)}`;
        
        // 5.3 Update Download Link
        downloadElement.href = base64Url;
        
        // Only update download name if it hasn't been renamed by user
        if (!downloadElement.download || downloadElement.download.startsWith('ejtileAnimation_')) {
             downloadElement.download = defaultFilename;
        }
        
        // 5.3 Rename Link (Handles renaming logic)
        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 to get initial transform values from the original SVG
function getInitialTransformValues(useElement) {
    const transform = useElement.getAttribute('transform') || '';
    
    // Default values if parts are missing
    let translateValue = 'translate(0,0)';
    let scaleValue = 'scale(1)';
    let rotateValue = 'rotate(0)';

    // Extract values
    const translateMatch = transform.match(/translate\(([^)]+)\)/);
    const scaleMatch = transform.match(/scale\(([^)]+)\)/);
    const rotateMatch = transform.match(/rotate\(([^)]+)\)/);
    
    if (translateMatch) {
        translateValue = `translate(${translateMatch[1]})`;
    }
    if (scaleMatch) {
        scaleValue = `scale(${scaleMatch[1]})`;
    }
    if (rotateMatch) {
        rotateValue = `rotate(${rotateMatch[1]})`;
    }

    return {
        translate: translateValue,
        scale: scaleValue,
        rotate: rotateValue,
        // The combined string for the full transform attribute value
        combined: `${translateValue} ${scaleValue} ${rotateValue}`.trim().replace(/\s+/g, ' ')
    };
}


// Function to handle the 'Center' button click
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();
        }
    }
}

// Function to handle the 'Show HTML' button click (Requirement VII)
function handleShowHtmlClick(e) {
    e.preventDefault();
    if (!etani_clone) {
        alert("Animation is not active (etani_clone is null).");
        return;
    }
    
    const svgString = new XMLSerializer().serializeToString(etani_clone);
    
    // Create Modal/Popup structure
    const overlay = document.createElement('div');
    overlay.className = 'etani-modal-overlay';
    
    const modalContent = document.createElement('div');
    modalContent.className = 'etani-modal-content';
    
    const closeIcon = document.createElement('span');
    closeIcon.className = 'etani-modal-close';
    closeIcon.innerHTML = '&times;'; // 'x' symbol
    closeIcon.onclick = () => overlay.remove(); // Close modal on click

    const textarea = document.createElement('textarea');
    textarea.value = svgString;
    
    modalContent.appendChild(closeIcon);
    modalContent.appendChild(textarea);
    overlay.appendChild(modalContent);
    document.body.appendChild(overlay);
    
    textarea.focus();
}

// Function to handle the radio button change (Requirement V)
function handleModeChange(e) {
    const mode = e.currentTarget.textContent;
    const selectedClass = 'selected';
    
    // Update visual selection
    document.querySelectorAll('.etaniSettingMode').forEach(radio => {
        radio.classList.remove(selectedClass);
    });
    e.currentTarget.classList.add(selectedClass);
    
    if (etani_clone) {
        const animateTransforms = etani_clone.querySelectorAll('animateTransform');
        
        animateTransforms.forEach(animate => {
            if (mode === 'repeat') {
                // V. When repeat is selected, set repeatCount="indefinite" and remove fill="freeze"
                animate.setAttribute('repeatCount', 'indefinite');
                animate.removeAttribute('fill');
            } else if (mode === 'freeze') {
                // V. When freeze is selected, set fill="freeze" and remove repeatCount
                animate.setAttribute('fill', 'freeze');
                animate.removeAttribute('repeatCount');
            }
        });
        
        // Update the result display immediately
        updateEtaniResult();
    }
}

// Function to handle adding a new value (Requirement VI)
function handleAddValueClick(e) {
    e.preventDefault();
    
    const etaniItemRight = e.currentTarget.closest('.etaniItemRight');
    const itemIndex = Array.from(etaniItemRight.parentNode.parentNode.children).indexOf(etaniItemRight.parentNode);
    
    if (etani_clone) {
        // Find the corresponding original <use/> to get the current value
        const originalDropUses = document.querySelectorAll('.etdrop use');
        const currentOriginalUse = originalDropUses[itemIndex];

        if (currentOriginalUse) {
            // Get the full transform value from the original <use/> (V. initial values)
            const values = getInitialTransformValues(currentOriginalUse);
            const currentValue = values.combined;
            
            // Find the corresponding <use/> in the etani_clone
            const cloneDropUses = etani_clone.querySelectorAll('.etdrop use');
            const cloneUse = cloneDropUses[itemIndex];
            
            if (cloneUse) {
                // VI. Append the new value to the three animateTransform tags
                const animates = cloneUse.querySelectorAll('animateTransform');
                animates.forEach(animate => {
                    let currentValues = animate.getAttribute('values') || '';
                    if (currentValues) {
                        currentValues += `;${currentValue}`;
                    } else {
                        currentValues = currentValue; // Should not happen if transform is applied correctly
                    }
                    animate.setAttribute('values', currentValues);
                });
                
                // Add the visual indicator
                const etaniAV = e.currentTarget.closest('.etaniAV');
                const newItem = document.createElement('span');
                newItem.className = 'etaniAVItem';
                etaniAV.appendChild(newItem);
                
                // Update the result display immediately
                updateEtaniResult();
            }
        }
    }
}


// Function to create the .etaniAnimate structure (Requirement IV.1)
function createAnimateStructure() {
    const animateDiv = document.createElement('div');
    animateDiv.className = 'etaniAnimate';

    // Name
    const nameSpan = document.createElement('span');
    nameSpan.className = 'etaniAnimateName';
    nameSpan.textContent = 'Transform';

    // Duration (dur: n) - Placeholder for now
    const durSpan = document.createElement('span');
    durSpan.className = 'etaniAnimateDur';
    durSpan.innerHTML = 'dur: 1s';

    // Value Container
    const valueDiv = document.createElement('div');
    valueDiv.className = 'etaniAnimateValue';

    // Add button (IV.1)
    const addSpan = document.createElement('span');
    addSpan.className = 'etaniAVAdd';
    // Add plus SVG
    addSpan.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M19 11h-6V5h-2v6H5v2h6v6h2v-6h6z"/></svg>`;
    addSpan.addEventListener('click', handleAddValueClick); // VI. Bind click event

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

    // Values holder
    const avDiv = document.createElement('div');
    avDiv.className = 'etaniAV';
    
    // Initial Item (IV.1)
    const initialItem = document.createElement('span');
    initialItem.className = 'etaniAVItem';
    
    avDiv.appendChild(initialItem);
    
    valueDiv.appendChild(addSpan);
    valueDiv.appendChild(labelSpan);
    valueDiv.appendChild(avDiv);

    animateDiv.appendChild(nameSpan);
    animateDiv.appendChild(durSpan);
    animateDiv.appendChild(valueDiv);

    return animateDiv;
}

// Function to handle the 'Append Transform' button click (Requirement IV)
function handleAllAppendTransformClick(e) {
    e.preventDefault();
    
    // IV.1 Add visual structure to all .etaniItemRight
    document.querySelectorAll('.etaniItemRight').forEach(rightDiv => {
        // Prevent duplication
        if (!rightDiv.querySelector('.etaniAnimate')) {
            rightDiv.appendChild(createAnimateStructure());
        }
    });

    // IV.2 Add animateTransform tags to etani_clone
    if (etani_clone) {
        const cloneDropUses = etani_clone.querySelectorAll('.etdrop use');
        const originalDropUses = document.querySelectorAll('.etdrop use');
        
        cloneDropUses.forEach((cloneUse, i) => {
            const originalUse = originalDropUses[i];
            
            // Get initial values (V. initial values)
            const transforms = getInitialTransformValues(originalUse);
            const initialValue = transforms.combined;
            
            // Function to create and append <animateTransform>
            const createAnimate = (type) => {
                // Initial values attribute should contain the starting transform (itself)
                const values = 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', values); // Initial value
                animate.setAttribute('dur', '1s');
                animate.setAttribute('fill', 'freeze'); // Default mode
                return animate;
            };

            // Remove any existing transforms before appending
            cloneUse.querySelectorAll('animateTransform').forEach(anim => anim.remove());

            // Append the three animateTransform tags
            cloneUse.appendChild(createAnimate('translate'));
            cloneUse.appendChild(createAnimate('scale'));
            cloneUse.appendChild(createAnimate('rotate'));
        });
        
        // Update the result display immediately
        updateEtaniResult();
    }
}


// Function to handle the creation and population of etaniinner
function createEtaniInner(etaniouter) {
    // 5.1 Clone the original SVG
    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) {
        console.error('SVG with ID "etmain" not found.');
        return;
    }
    // VIII. Use etani_clone
    etani_clone = originalSvg.cloneNode(true); 
    
    // 5.2 Remove .etwait from the clone
    const etwaitElement = etani_clone.querySelector('.etwait');
    if (etwaitElement) {
        etwaitElement.remove();
    }
    
    // 5.3 Update clone ID
    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';
    
    // I. Create .etaniSetting and .etaniAllAppend
    const etaniSetting = document.createElement('div');
    etaniSetting.className = 'etaniSetting';
    etaniSetting.innerHTML = `<label>Animation Mode</label>`;

    const etaniAllAppend = document.createElement('div');
    etaniAllAppend.className = 'etaniAllAppend';
    etaniAllAppend.innerHTML = `<label>Batch Append</label>`;

    // I, II. Add radio buttons to .etaniSetting
    const freezeRadio = document.createElement('span');
    freezeRadio.className = 'etaniSettingMode etaniSettingFreeze selected'; // Default selected
    freezeRadio.textContent = 'freeze';
    freezeRadio.addEventListener('click', handleModeChange);

    const repeatRadio = document.createElement('span');
    repeatRadio.className = 'etaniSettingMode etaniSettingRepeat';
    repeatRadio.textContent = 'repeat';
    repeatRadio.addEventListener('click', handleModeChange);
    
    etaniSetting.appendChild(freezeRadio);
    etaniSetting.appendChild(repeatRadio);

    // I, III. Add buttons to .etaniAllAppend
    const transformButton = document.createElement('button');
    transformButton.className = 'etaniAllAppendTransform';
    transformButton.textContent = 'Transform';
    transformButton.addEventListener('click', handleAllAppendTransformClick); // IV. Bind click

    const opacityButton = document.createElement('button');
    opacityButton.className = 'etaniAllAppendOpacity';
    opacityButton.textContent = 'Opacity';

    etaniAllAppend.appendChild(transformButton);
    etaniAllAppend.appendChild(opacityButton);


    // I, VII. Create .etaniContent
    const etaniContent = document.createElement('div');
    etaniContent.className = 'etaniContent';
    etaniContent.innerHTML = `<label>Controls</label>`;
    
    // VII. Add a.etaniContentHTML
    const htmlLink = document.createElement('a');
    htmlLink.className = 'etaniContentHTML';
    htmlLink.textContent = 'Show HTML';
    htmlLink.href = '#';
    htmlLink.addEventListener('click', handleShowHtmlClick); // VII. Bind click
    
    // Combine etaniCtrl content (order: HTML, Center, Setting, Append)
    etaniContent.appendChild(htmlLink);
    
    // Center link logic (from previous requirement)
    const centerLink = document.createElement('a');
    centerLink.className = 'etaniCenter';
    centerLink.textContent = 'Center';
    centerLink.href = '#';
    centerLink.addEventListener('click', handleCenterClick);
    etaniContent.appendChild(centerLink);


    etaniCtrl.appendChild(etaniContent);
    etaniCtrl.appendChild(etaniSetting);
    etaniCtrl.appendChild(etaniAllAppend);


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

    // Create div.etaniResult
    const etaniResult = document.createElement('div');
    etaniResult.className = 'etaniResult';
    
    // Result elements (unchanged)
    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);

    // Populate etaniCol (tile structure)
    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 = '';
        let etwaitfill = '';
        let etwaitstroke = '';
        let 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);
    });
    
    // Update the result section immediately after etaniinner is fully created
    updateEtaniResult();
}

// Function to handle button click: toggle animation controls
function toggleAnimation(event) {
    const etanibutton = event.currentTarget;
    const etaniouter = etanibutton.parentNode;
    const etaniinner = document.getElementById('etaniinner'); // Find the inner container

    if (etanibutton.textContent === 'Animate it') {
        // State: Animation is OFF -> Turn ON
        
        etanibutton.textContent = 'Close Ejtile Ani'; 

        createEtaniInner(etaniouter);
        
    } else if (etanibutton.textContent === 'Close Ejtile Ani') {
        // State: Animation is ON -> Turn OFF

        etanibutton.textContent = 'Animate it';
        
        if (etaniinner) {
            etaniinner.remove();
        }
        
        // Reset global clone variable
        etani_clone = null;
    }
}


// Main function 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
圈圈精英
圈圈精英
帖子: 3805
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 157 次
被圈友点赞: 188 次
联系:

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

帖子 ejsoon »

前一問最好的版本:

代码: 全选

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

在线用户

正浏览此版面之用户: ejsoon 和 1 访客