正在做etani,ejtile的動畫套件

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

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

帖子 ejsoon »

做個js函式etaniAllPattern():

拿到etaniInput的value,它只能由[a-zA-Z]或[01]組成。

一,如果是由[a-zA-Z]組成:

1.1,首先拿到etaniWindow中的「.allSpan」,如果它含有.selected,則targetAnimate就是etani.querySelectorAll('.etdrop > use > animateTransform')。

如果它不含,etaniWindow.selectAll「.tilesSpan」中的innerHTML就是tileId(可能有多個),則targetAnimate就是etani.querySelectorAll('.etdrop > use[href=#tileId] > animateTransform')。

1.2,再把etaniWindow中的「.allId.selected」(可能有多個)的innerHTML作為animateId,往targetAnimate加上etani.querySelector(':scope > [id=animateId]')。

如果etani.querySelector(':scope > [id=animateId]')的tagName是animateTransform,則它後面的兩個animateTransform也要加入targetAnimate。

1.3,etaniInput的每個字母意義是:以[a-zA-Z]的順序,每個字母代表一個index。比如a代表0,c代表2,y代表24,B代表27。

1.4,etaniInput的字母組合就是為了給targetAnimate的values排列組合,比如「bbd」是指取到兩個index為1和一個index為3的values。

1.5,首先要先確定每一個targetAnimate中的values.split(;)的項數-1是否不小於etaniInput最大的字母所對應的index。只要有一個小於,則返回,如果都不小於,則執行排列組合。

二,如果etaniInput的值只存在0和1:

2.1,首先拿到etaniWindow中的「.allSpan」,如果它含有.selected,則targetAnimate就是etani.querySelectorAll('.etdrop > use > animate[attributeName=opacity])。

如果它不含,etaniWindow.selectAll「.tilesSpan」中的innerHTML就是tileId(可能有多個),則targetAnimate就是etani.querySelectorAll('.etdrop > use[href=#tileId] > animate[attributeName=opacity]')。

2.2,再把etaniWindow中的「.allId.selected」(可能有多個)的innerHTML作為animateId,往targetAnimate加上etani.querySelectorAll(':scope > animate[attributeName=opacity][id=animateId]')。

2.3,etaniInput的數字用;分隔之後,直接作為values替換掉targetAnimate的values。

etaniWindow是一個element變量,可直接用.querySelector。

用繁體中文回覆。所有注釋都要是英文。

代码: 全选

/**
 * Gets the value from etaniInput and applies it as a pattern to SVG animations
 * within the 'etani' element, based on UI selections in 'etaniWindow'.
 *
 * Assumes 'etani' (SVG element) and 'etaniWindow' (a DOM element, not a window)
 * are available in the scope where this function is called.
 */
function etaniAllPattern() {
    // Find the input element, assuming it's in the main document.
    const etaniInput = document.getElementById('etaniInput');

    if (!etaniInput) {
        console.error('etaniInput element not found.');
        return;
    }

    const inputValue = etaniInput.value;

    /**
     * Helper function for (1.3)
     * Converts a letter [a-zA-Z] to its corresponding index (a=0, z=25, A=26, Z=51).
     * @param {string} char - The character to convert.
     * @returns {number} - The calculated index, or -1 if invalid.
     */
    function getIndexFromChar(char) {
        const code = char.charCodeAt(0);
        if (code >= 97 && code <= 122) { // a-z
            return code - 97;
        }
        if (code >= 65 && code <= 90) { // A-Z
            return code - 65 + 26;
        }
        return -1; // Invalid character
    }

    // Use a Set to store unique animation elements
    const finalTargets = new Set();

    // --- Branch 1: Alphabetical Input [a-zA-Z] ---
    if (/^[a-zA-Z]+$/.test(inputValue)) {
        const allSpan = etaniWindow.querySelector('.allSpan');

        // 1.1: Determine base target elements (animateTransform)
        if (allSpan && allSpan.classList.contains('selected')) {
            // If .allSpan is selected, target all animateTransforms
            const nodes = etani.querySelectorAll('.etdrop > use > animateTransform');
            nodes.forEach(n => finalTargets.add(n));
        } else {
            // If .allSpan is not selected, target based on all .tilesSpan
            const tilesSpans = etaniWindow.querySelectorAll('.tilesSpan');
            
            if (tilesSpans && tilesSpans.length > 0) {
                const selectors = Array.from(tilesSpans).map(span => {
                    const tileId = span.innerHTML.trim();
                    // Ensure tileId is not empty
                    if (tileId) {
                        return `.etdrop > use[href="#${tileId}"] > animateTransform`;
                    }
                    return null;
                }).filter(Boolean); // Filter out any null entries

                if (selectors.length > 0) {
                    const nodes = etani.querySelectorAll(selectors.join(', '));
                    nodes.forEach(n => finalTargets.add(n));
                }
            }
        }

        // 1.2: Add specific animations by ID
        const selectedIds = etaniWindow.querySelectorAll('.allId.selected');
        selectedIds.forEach(idSpan => {
            const animateId = idSpan.innerHTML.trim();
            if (!animateId) return;

            // Find the element by ID within the 'etani' scope (using querySelector)
            const animElement = etani.querySelector(`:scope > [id="${animateId}"]`);

            if (animElement) {
                finalTargets.add(animElement);

                // If it's animateTransform, add the next two animateTransforms
                if (animElement.tagName.toLowerCase() === 'animatetransform') {
                    let next = animElement.nextElementSibling;
                    // Check if next sibling exists and is an animateTransform
                    if (next && next.tagName.toLowerCase() === 'animatetransform') {
                        finalTargets.add(next);
                        let nextNext = next.nextElementSibling;
                        // Check if next-next sibling exists and is an animateTransform
                        if (nextNext && nextNext.tagName.toLowerCase() === 'animatetransform') {
                            finalTargets.add(nextNext);
                        }
                    }
                }
            }
        });

        // 1.3 & 1.4 (Preparation): Map input string to indices
        const indices = inputValue.split('').map(getIndexFromChar);
        // Check if getIndexFromChar returned -1 for any invalid char
        if (indices.includes(-1)) {
            console.error('Invalid characters in input. Must be [a-zA-Z].');
            return;
        }
        const maxIndex = Math.max(...indices);

        // 1.5: Validate values length before applying
        const allValuesArrays = new Map();
        let validationFailed = false;

        for (const anim of finalTargets) {
            const valuesStr = anim.getAttribute('values');
            if (!valuesStr) {
                console.warn(`Element ${anim.id || '(no id)'} has no 'values' attribute.`);
                validationFailed = true;
                break;
            }
            const valuesList = valuesStr.split(';');
            allValuesArrays.set(anim, valuesList); // Store for later use

            // Check: (count - 1) must be >= maxIndex
            if (valuesList.length - 1 < maxIndex) {
                console.error(`Validation failed: Element ${anim.id || '(no id)'} has ${valuesList.length} values, but needs at least ${maxIndex + 1} (for index ${maxIndex}).`);
                validationFailed = true;
                break;
            }
        }

        if (validationFailed) {
            console.warn('Aborting pattern application due to validation failure.');
            return;
        }

        // 1.4 (Execution): Apply permutation
        for (const anim of finalTargets) {
            const valuesList = allValuesArrays.get(anim);
            if (!valuesList) continue; // Should not happen if validation passed

            // Build the new values string based on the indices
            const newValues = indices.map(index => valuesList[index]).join(';');
            anim.setAttribute('values', newValues);
            
            // Optional: Restart animation if necessary
            // if (typeof anim.beginElement === 'function') {
            //     anim.beginElement();
            // }
        }

    // --- Branch 2: Binary Input [01] ---
    } else if (/^[01]+$/.test(inputValue)) {
        const allSpan = etaniWindow.querySelector('.allSpan');

        // 2.1: Determine base target elements (animate attributeName=opacity)
        if (allSpan && allSpan.classList.contains('selected')) {
            const nodes = etani.querySelectorAll('.etdrop > use > animate[attributeName=opacity]');
            nodes.forEach(n => finalTargets.add(n));
        } else {
            // If not .allSpan, use .tilesSpan
            const tilesSpans = etaniWindow.querySelectorAll('.tilesSpan');
            
            if (tilesSpans && tilesSpans.length > 0) {
                const selectors = Array.from(tilesSpans).map(span => {
                    const tileId = span.innerHTML.trim();
                    if (tileId) {
                        return `.etdrop > use[href="#${tileId}"] > animate[attributeName=opacity]`;
                    }
                    return null;
                }).filter(Boolean);

                if (selectors.length > 0) {
                     const nodes = etani.querySelectorAll(selectors.join(', '));
                    nodes.forEach(n => finalTargets.add(n));
                }
            }
        }

        // 2.2: Add specific animations by ID (using querySelectorAll as requested)
        const selectedIds = etaniWindow.querySelectorAll('.allId.selected');
        selectedIds.forEach(idSpan => {
            const animateId = idSpan.innerHTML.trim();
            if (!animateId) return;

            // Use querySelectorAll as specified in 2.2
            const animElements = etani.querySelectorAll(`:scope > animate[attributeName=opacity][id="${animateId}"]`);
            animElements.forEach(anim => finalTargets.add(anim));
        });

        // 2.3: Apply values. Input "010" becomes "0;1;0" values.
        const newValues = inputValue.split('').join(';');

        for (const anim of finalTargets) {
            anim.setAttribute('values', newValues);
            
            // Optional: Restart animation if necessary
            // if (typeof anim.beginElement === 'function') {
            //     anim.beginElement();
            // }
        }

    } else {
        // Handle invalid (non-empty) input
        if (inputValue.length > 0) {
            console.warn(`Invalid input: "${inputValue}". Input must be only [a-zA-Z] or only [01].`);
        }
        // If input is empty, do nothing silently.
    }
}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4041
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 191 次
联系:

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

帖子 ejsoon »

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

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

帖子 ejsoon »

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

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

帖子 ejsoon »

做多個js函式etaniUndo()、etaniRedo()、etaniCapture()、etaniState():

etani是一個已定義的element全局變量。

etaniCapture()記錄的是當前etani的值。最大記錄數是36個,或不大於24m。

etaniUndo()、etaniRedo()是etani的回退和重做。

etaniState()是三個函式都會調用的,用於確定#undo和#redo是否處於能按狀態。

用繁體中文回覆。所有注釋都要是英文。

在前面的基礎上:
調用一個etaniSize以獲得當前etani的大小,之後加上24m限制的功能。

代码: 全选

// --- Etani Undo/Redo System ---
// This system manages the undo/redo history for the global 'etani' element,
// enforcing both a maximum item count and a total size limit.

// --- History Storage ---

// Global history stack
// Stores objects: { value: "...", size: 123 }
let etaniHistory = [];

// Global history pointer
// Stores the current index within the etaniHistory array.
let etaniHistoryIndex = -1;

// Global history total size
// Stores the sum of all 'size' properties in etaniHistory.
let etaniHistoryTotalSize = 0;

// --- Limits ---

// Maximum number of history states to store.
const ETANI_MAX_HISTORY = 36;

// Maximum total size (in units provided by etaniSize()) for the entire history.
// 24m = 24,000,000
const ETANI_MAX_SIZE = 24000000;


// --- Mock etaniSize() Function ---
// This is a *placeholder* function.
// You MUST replace this with your actual etaniSize() function.
// This example implementation calculates the UTF-8 byte length of etani.value.
function etaniSize() {
    // Assuming 'etani' is a <textarea> or <input>
    if (typeof etani === 'undefined' || typeof etani.value === 'undefined') {
        return 0;
    }
    // Use TextEncoder to get a realistic byte size (UTF-8).
    // string.length is NOT accurate for memory size.
    try {
        return new TextEncoder().encode(etani.value).length;
    } catch (e) {
        // Fallback for older environments
        return etani.value.length;
    }
}
// ---------------------------------


// --- Core Functions ---

/**
 * Captures the current state of the 'etani' element and saves it to history.
 *
 * IMPORTANT: This function assumes 'etani' is an element with a '.value' property.
 * If 'etani' is a contenteditable <div>, change 'etani.value' to 'etani.innerHTML'
 * in this function AND in the etaniSize() function.
 */
function etaniCapture() {
    const currentValue = etani.value;
    // Get the size of the new state by calling the provided etaniSize()
    const currentSize = etaniSize();

    // --- 1. Handle "future" (redo) states ---
    // If we have undone, we must truncate the "future" states.
    if (etaniHistoryIndex < etaniHistory.length - 1) {
        etaniHistory = etaniHistory.slice(0, etaniHistoryIndex + 1);
        
        // Recalculate the total size after truncation.
        // This is safer than trying to subtract the removed items.
        etaniHistoryTotalSize = etaniHistory.reduce((total, state) => total + state.size, 0);
    }

    // --- 2. Enforce limits (pruning old states) ---
    // Remove old states from the beginning *until* the new state can fit
    // without violating either the COUNT limit or the SIZE limit.
    while (etaniHistory.length > 0 && (
           // Condition 1: We're at or over the max item count (36)
           (etaniHistory.length >= ETANI_MAX_HISTORY) ||
           // Condition 2: Adding the new item would exceed the max size (24m)
           (etaniHistoryTotalSize + currentSize > ETANI_MAX_SIZE)
          )) {
        
        // Remove the oldest state (from the front of the array).
        const removedState = etaniHistory.shift();
        
        // Update the total size.
        etaniHistoryTotalSize -= removedState.size;
        
        // IMPORTANT: We must also decrement the index, as the array has shifted.
        etaniHistoryIndex--;
    }
    
    // Safety check: If the index became negative, reset it.
    if (etaniHistoryIndex < -1) {
        etaniHistoryIndex = -1;
    }

    // --- 3. Add the new state ---
    const newState = {
        value: currentValue,
        size: currentSize
    };

    etaniHistory.push(newState);
    etaniHistoryTotalSize += currentSize;

    // After capturing, the current index is always the last item.
    etaniHistoryIndex = etaniHistory.length - 1;

    // --- 4. Update button states ---
    etaniState();
}

/**
 * Reverts the 'etani' element to the previous state in history (Undo).
 */
function etaniUndo() {
    // Check if we can undo (i.e., we are not at the very first item).
    if (etaniHistoryIndex > 0) {
        // Move the pointer back one step.
        etaniHistoryIndex--;

        // Update the element's value from the history object.
        etani.value = etaniHistory[etaniHistoryIndex].value;

        // Update the button states.
        etaniState();
    }
}

/**
 * Re-applies a state that was previously undone (Redo).
 */
function etaniRedo() {
    // Check if we can redo (i.e., we are not at the very last item).
    if (etaniHistoryIndex < etaniHistory.length - 1) {
        // Move the pointer forward one step.
        etaniHistoryIndex++;

        // Update the element's value from the history object.
        etani.value = etaniHistory[etaniHistoryIndex].value;

        // Update the button states.
        etaniState();
    }
}

/**
 * Updates the disabled state of the #undo and #redo buttons.
 * This function's logic does not need to change.
 */
function etaniState() {
    const undoButton = document.getElementById('undo');
    const redoButton = document.getElementById('redo');

    if (undoButton) {
        // Can undo if the index is greater than 0.
        undoButton.disabled = (etaniHistoryIndex <= 0);
    }

    if (redoButton) {
        // Can redo if the index is not at the very end.
        redoButton.disabled = (etaniHistoryIndex >= etaniHistory.length - 1);
    }
}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4041
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 191 次
联系:

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

帖子 ejsoon »

把outhtml轉成dom element:

代码: 全选

要將 HTML 或 SVG 的 outerHTML 字符串轉換回一個 DOM 元素,你可以使用 JavaScript 的 `DOMParser` API。這是一個內建的瀏覽器 API,能夠解析字符串為 DOM 結構。以下是步驟和範例代碼,我會分別說明 HTML 和 SVG 的處理方式。

### 1. **處理 HTML 字符串**
   - 使用 `DOMParser.parseFromString()` 解析字符串,指定 MIME 類型為 `'text/html'`。
   - 解析後,從結果的 `body` 中取出第一個子元素(假設字符串只包含單一根元素)。
   - 注意:如果字符串包含多個元素,你可能需要取出整個 `body.children` 或調整邏輯。

   **範例代碼:**
   ```javascript
   // 假設你的 outerHTML 字符串
   const htmlString = '<div class="example">Hello World</div>';

   // 創建 DOMParser 實例
   const parser = new DOMParser();

   // 解析字符串為 DOM 文檔
   const doc = parser.parseFromString(htmlString, 'text/html');

   // 取出元素(假設是單一元素)
   const element = doc.body.firstChild;

   // 現在你可以將它添加到頁面 DOM 中
   document.body.appendChild(element);

   console.log(element); // 輸出: <div class="example">Hello World</div>
   ```

### 2. **處理 SVG 字符串**
   - SVG 需要指定 MIME 類型為 `'image/svg+xml'`,因為 SVG 是 XML 基底的。
   - 解析後,直接從 `documentElement` 取出根元素(通常是 `<svg>`)。

   **範例代碼:**
   ```javascript
   // 假設你的 outerHTML 字符串(SVG)
   const svgString = '<svg width="100" height="100"><circle cx="50" cy="50" r="40" fill="red" /></svg>';

   // 創建 DOMParser 實例
   const parser = new DOMParser();

   // 解析字符串為 DOM 文檔
   const svgDoc = parser.parseFromString(svgString, 'image/svg+xml');

   // 取出 SVG 根元素
   const svgElement = svgDoc.documentElement;

   // 現在你可以將它添加到頁面 DOM 中
   document.body.appendChild(svgElement);

   console.log(svgElement); // 輸出: <svg width="100" height="100">...</svg>
   ```

### 注意事項:
- **瀏覽器相容性**:`DOMParser` 在現代瀏覽器(如 Chrome、Firefox、Safari)中廣泛支援,但如果在舊版 IE 中,可能需要 polyfill。
- **錯誤處理**:如果字符串無效(例如語法錯誤),解析可能會失敗。你可以檢查 `svgDoc.querySelector('parsererror')` 是否存在來偵測錯誤。
- **安全性**:如果字符串來自不可信任來源,避免直接解析並插入 DOM,以防 XSS 攻擊。建議先 sanitizing 字符串。
- **替代方法**:如果你只處理簡單元素,可以使用 `document.createElement('div')` 並設定 `innerHTML`,但對於完整 outerHTML,`DOMParser` 更可靠。對於 SVG,也可以使用 `document.createElementNS('http://www.w3.org/2000/svg', 'svg')` 來手動創建,但解析字符串時 `DOMParser` 更方便。

如果你的字符串更複雜或有特定情境,提供更多細節我可以調整範例!
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4041
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 191 次
联系:

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

帖子 ejsoon »

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

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

帖子 ejsoon »

做readfile, download, convert to ejtile:

代码: 全选

<div class="etaniEditDiv"><textarea class="etaniHTMLTextarea"></textarea><div class="etaniEditDiv"><a class="etaniReadfile" href="javascript:;">read file</a><span class="etaniFileName"></span><span class="etaniFileSize"></span></div><div class="etaniEditDiv"><a class="etaniEjtile" href="javascript:;">convert to ejtile</a><a class="ejtileDownload hide"></a><span class="etaniEjtileSize"></span></div></div>
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4041
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 191 次
联系:

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

帖子 ejsoon »

做一個js函式etaniReadfileClick(),它是綁定在一個按鈕上的事件。

當它觸發時,讀取一個文件,之後判斷文件是否為svg,且這個svg的id屬性為etani。

如果以上判定通過,則「.etaniHTMLTextarea」將顯示這個文件的文本內容。之後元素readfileName和rwadfileSize將顯示它的文件名和大小(byte)。


再做一個js函式etaniEjtileClick(),它也 是綁定在一個按鈕上的事件。

當它觸發時,將進行以下操作:

一,對元素etaniCovert的id改為etmain。把etaniCovert內的.etdrop的id去掉。

二,把etaniCovert中的「.etdrop > use > animate、.etdrop > use > animateTransform、.etdrop > use > animateMotion、.etdrop > use > set」刪除。

三,把etaniCovert的「:scope > animate、:scope > animateTransform、:scope > animateMotion、:scope > set」刪除。

四,把etaniCovert的outerHTML顯示到.etaniHTMLTextarea中。

五,在已有的.ejtileDownload弄成下載這個etaniCovert的內容。

六,在etaniEjtileSize將顯示這個文件的大小(byte)。


請注意:讀取文件的input[type=file]並不存在,需要實時生成,當讀到文件內容後,這個input[type=file]應當被刪除。

用繁體中文回覆。所有注釋都要是英文。

代码: 全选

/**
 * 綁定到按鈕的事件,用於讀取和驗證 SVG 文件。
 * 動態創建一個 <input type="file"> 元素,並在完成後將其刪除。
 */
function etaniReadfileClick() {
    // 1. Create a file input element dynamically
    const fileInput = document.createElement('input');
    fileInput.type = 'file';
    fileInput.accept = 'image/svg+xml'; // Only allow SVG files
    fileInput.style.display = 'none'; // Hide the element from view

    // 2. Add an event listener for when a file is selected
    fileInput.addEventListener('change', (event) => {
        const file = event.target.files[0];

        // Cleanup function to remove the input
        const cleanup = () => {
            document.body.removeChild(fileInput);
        };

        if (!file) {
            // No file selected, just clean up
            cleanup();
            return;
        }

        const reader = new FileReader();

        // 3. Define what happens when the file is successfully read
        reader.onload = (e) => {
            const content = e.target.result;
            let isValid = false;

            try {
                // 4. Parse the file content as XML/SVG
                const parser = new DOMParser();
                const doc = parser.parseFromString(content, 'image/svg+xml');

                // Check for parser errors (e.g., if it's not valid XML/SVG)
                const parserError = doc.querySelector('parsererror');
                if (parserError) {
                    console.error('Error parsing SVG:', parserError.textContent);
                } else {
                    // 5. Validate if the root element is <svg> and its id is 'etani'
                    const svgElement = doc.documentElement;
                    if (svgElement && svgElement.tagName.toLowerCase() === 'svg' && svgElement.id === 'etani') {
                        isValid = true;
                    }
                }
            } catch (error) {
                console.error('Error processing file:', error);
            }

            // 6. If validation passes, update the UI
            if (isValid) {
                // Display the file's text content
                const textarea = document.querySelector('.etaniHTMLTextarea');
                if (textarea) {
                    textarea.value = content;
                }

                // Display file name (assuming 'readfileName' is an ID)
                const fileNameElement = document.querySelector('#readfileName');
                if (fileNameElement) {
                    fileNameElement.textContent = file.name;
                }

                // Display file size (assuming 'rwadfileSize' is an ID)
                const fileSizeElement = document.querySelector('#rwadfileSize');
                if (fileSizeElement) {
                    fileSizeElement.textContent = `${file.size} bytes`;
                }
            } else {
                alert('文件不是 ID 為 "etani" 的有效 SVG,或者文件已損壞。');
            }
            
            // 7. Clean up the dynamically created input
            cleanup();
        };

        // Define error handling
        reader.onerror = () => {
            console.error('Error reading file.');
            alert('讀取文件時發生錯誤。');
            cleanup();
        };

        // Start reading the file as text
        reader.readAsText(file);
    });

    // 8. Append the input to the body and click it to open the file dialog
    document.body.appendChild(fileInput);
    fileInput.click();
}

/**
 * 綁定到按鈕的事件,用於處理、清理和導出 #etaniCovert 元素。
 */
function etaniEjtileClick() {
    // Find the original element to process
    const originalElement = document.querySelector('#etaniCovert');

    if (!originalElement) {
        console.error('Element with ID "etaniCovert" not found.');
        alert('找不到 "etaniCovert" 元素。');
        return;
    }

    // Create a deep clone to modify, preserving the original element on the page
    const clone = originalElement.cloneNode(true);

    // --- Operation 1 ---
    // Change the ID of the clone to 'etmain'
    clone.id = 'etmain';
    
    // Find the .etdrop element inside the clone and remove its ID
    const innerDrop = clone.querySelector('.etdrop');
    if (innerDrop) {
        innerDrop.removeAttribute('id');
    }

    // --- Operation 2 ---
    // Remove specified animation elements within .etdrop > use
    const animationsToDrop = clone.querySelectorAll(
        '.etdrop > use > animate, .etdrop > use > animateTransform, .etdrop > use > animateMotion, .etdrop > use > set'
    );
    animationsToDrop.forEach(anim => anim.remove());

    // --- Operation 3 ---
    // Remove specified animation elements that are direct children (:scope > ...)
    const directChildTags = ['animate', 'animateTransform', 'animateMotion', 'set'];
    // Filter only direct children of the clone
    const childAnimations = Array.from(clone.children).filter(child =>
        directChildTags.includes(child.tagName.toLowerCase())
    );
    childAnimations.forEach(anim => anim.remove());

    // --- Operation 4 ---
    // Get the outerHTML of the modified clone
    const processedHTML = clone.outerHTML;

    // Display the processed HTML in the textarea
    const textarea = document.querySelector('.etaniHTMLTextarea');
    if (textarea) {
        textarea.value = processedHTML;
    }

    // --- Operation 5 & 6 ---
    // Create a Blob from the processed HTML
    const blob = new Blob([processedHTML], { type: 'image/svg+xml' });

    // --- Operation 5: Setup download link ---
    const downloadLink = document.querySelector('.ejtileDownload'); // Assuming class
    if (downloadLink) {
        // Create an object URL for the Blob
        const url = URL.createObjectURL(blob);
        
        // Set the link's href to the new URL
        downloadLink.href = url;
        
        // Set the default filename for the download
        downloadLink.download = 'etmain.svg'; 
    }
    
    // --- Operation 6: Display file size ---
    const sizeElement = document.querySelector('#etaniEjtileSize'); // Assuming ID
    if (sizeElement) {
        sizeElement.textContent = `${blob.size} bytes`;
    }
}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4041
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 191 次
联系:

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

帖子 ejsoon »

在寫readfile,現在需要把repeat/freeze寫到svg屬性上。

當前代碼:
ejtileanimate202511121600.7z
(22.24 KiB) 尚未被下载
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4041
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 191 次
联系:

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

帖子 ejsoon »

上次由 ejsoon 在 2025年 11月 13日 09:34,总共编辑 1 次。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4041
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 191 次
联系:

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

帖子 ejsoon »

chmod +x make_archive.sh # 給執行權限(只需一次)
./make_archive.sh

代码: 全选

#!/bin/bash

# 設定要執行的目錄(請替換成你的實際路徑)
TARGET_DIR="/path/to/your/directory"

# 產生檔名中的時間戳:YYYYMMDDHHMM(24小時制)
TIMESTAMP=$(date '+%Y%m%d%H%M')

# 完整的 7z 壓縮檔名
ARCHIVE_NAME="ejtileanimate${TIMESTAMP}"

# 切換到目標目錄並執行 7z 壓縮
cd "$TARGET_DIR" && 7z a "${ARCHIVE_NAME}" ejtileanimate.js
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4041
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 191 次
联系:

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

帖子 ejsoon »

在下面的代碼中,當原來的defs中的g[id]比clone多時,會在clone加上,但是如果少時則不會。現在希望加上,當少時就刪除,包括刪除defs中的g[id]以及.etdrop中的use。

只需要提供更改之處,用繁體中文回答。所有注釋都要是英文。

代码: 全选

// Handle click event for the update button
function etaniUpdateTilesClick() {
    const originalSvg = document.getElementById('etmain');
    if (!originalSvg) return;

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

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

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

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

    // update board
    const originalBoard = originalSvg.querySelector('.etdrop > .etboard');
    const cloneBoard = etani.querySelector('.etdrop > .etboard');
    if (originalBoard && cloneBoard) {
        cloneBoard.replaceWith(originalBoard.cloneNode(true));
    }

    // update UI and result
    updateAllAnimateUI();
    updateEtaniResult();
}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4041
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 191 次
联系:

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

帖子 ejsoon »

代码: 全选

// ... (緊接在 'const cloneUsesMap = ...' 之後)

    // Create a Set of original tile IDs for efficient lookup during deletion check
    const originalTileIds = new Set(Array.from(originalTiles).map(g => g.id));

// ... (您原有的 'originalTiles.forEach(...){ ... }' 迴圈保持不變)
    originalTiles.forEach(originalTileG => {
        // ... (您原有的更新/添加邏輯)
    });

// --- ADD THIS NEW SECTION ---
    // Loop 2: Remove tiles that are in clone but not in original
    cloneTilesMap.forEach((cloneTileG, tileId) => {
        // Check if the tileId from clone exists in the original set
        if (!originalTileIds.has(tileId)) {
            // If not, remove the tile definition from clone defs
            cloneTileG.remove();
            
            // Also remove the corresponding <use> element from clone drop
            const cloneUse = cloneUsesMap.get(tileId);
            if (cloneUse) {
                cloneUse.remove();
            }
        }
    });
// --- END NEW SECTION ---

    // update board
    // ... (函數的其餘部分)
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4041
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 191 次
联系:

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

帖子 ejsoon »

寫一個js函式etaniItemImageClick():

代码: 全选

寫一個js函式etaniItemImageClick():

設
etaniCol = this.closest('.etaniCol');
thisItem = this.closest('.etaniItem');
selectedItem = etaniCol.querySelector('.etaniItem.selected');
thisItemType = thisItem.dataset.type;
thisItemId = thisItem.dataset.id;
selectedItemId = selectedItem.dataset.id

如果thisItemType !== 'tile',則結束。

如果selectedItem不存在,則this的class加上'selected',結束。

如果selectedItem存在,並且this.classList包含'selected',則this.classlist.remove('selected'),結束。

如果selectedItem存在,並且this.classList不包含'selected':

一,首先獲得thisItem、selectedItem在etaniCol中的index,設為thisItemIndex,selectedItemIndex。

二,etani是一個已定義的element,設:

thisUse = etani.querySelector('.etdrop > use[href=#thisItemId]')
selectedUse = etani.querySelector('.etdrop > use[href=#selectedItemId]')

三,如果thisItemIndex小於selectedItemIndex,則在etani的.etdrop中把selectedUse移到thisUse的前面。
比如「01234」,thisItemIndex是2,selectedItemIndex是4,則最後排列的結果是「01423」。

四,如果thisItemIndex大於selectedItemIndex,則在etani的.etdrop中把selectedUse移到thisUse的後面。
比如「01234」,thisItemIndex是1,selectedItemIndex是4,則最後排列的結果是「02341」。

用繁體中文回覆,注釋都要是英文。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
圈圈精英
圈圈精英
帖子: 4041
注册时间: 2022年 11月 18日 17:36
为圈友点赞: 159 次
被圈友点赞: 191 次
联系:

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

帖子 ejsoon »

代码: 全选

/**
 * Handles the click event on an .etaniItem image.
 * Manages item selection and reorders corresponding SVG <use> elements.
 * Assumes 'etani' is a defined element variable in the accessible scope.
 * * @param {Event} event - The click event (this function is intended to be used as an event handler).
 */
function etaniItemImageClick() {
    // 'this' refers to the element that was clicked (e.g., the image)
    
    // Find the parent column
    const etaniCol = this.closest('.etaniCol');
    if (!etaniCol) {
        console.error('etaniItemImageClick: Could not find .etaniCol');
        return; // Exit if column not found
    }

    // Find the item container for the clicked element
    const thisItem = this.closest('.etaniItem');
    if (!thisItem) {
        console.error('etaniItemImageClick: Could not find .etaniItem');
        return; // Exit if item not found
    }

    // Find the currently selected item in this column
    const selectedItem = etaniCol.querySelector('.etaniItem.selected');

    // Get data attributes from the clicked item
    const thisItemType = thisItem.dataset.type;
    const thisItemId = thisItem.dataset.id;

    // --- Condition 1: Check item type ---
    // If thisItemType is not 'tile', end.
    if (thisItemType !== 'tile') {
        return;
    }

    // --- Condition 2: No item is selected ---
    // If selectedItem does not exist, add 'selected' to thisItem and end.
    if (!selectedItem) {
        thisItem.classList.add('selected');
        return;
    }

    // --- Condition 3: Clicking the already selected item ---
    // If selectedItem exists, and thisItem has 'selected' class (is the selected item)
    // Remove 'selected' class and end.
    if (thisItem.classList.contains('selected')) {
        thisItem.classList.remove('selected');
        return;
    }

    // --- Condition 4: Clicking a different item when one is already selected ---
    // If selectedItem exists, and thisItem does not have 'selected' class.

    // Get the ID of the selected item
    const selectedItemId = selectedItem.dataset.id;

    // --- Part 4.1: Get Indexes ---
    // Get all items in the column to determine their indices
    const allItems = Array.from(etaniCol.querySelectorAll('.etaniItem'));
    const thisItemIndex = allItems.indexOf(thisItem);
    const selectedItemIndex = allItems.indexOf(selectedItem);

    // --- Part 4.2: Get <use> elements ---
    // 'etani' is assumed to be a pre-defined element variable
    const etdrop = etani.querySelector('.etdrop');
    if (!etdrop) {
        console.error('etaniItemImageClick: Could not find .etdrop within etani element');
        return;
    }

    // Find the <use> elements corresponding to the item IDs
    // Note: We use template literals to build the href selector
    const thisUse = etdrop.querySelector(`use[href="#${thisItemId}"]`);
    const selectedUse = etdrop.querySelector(`use[href="#${selectedItemId}"]`);

    // Check if both <use> elements were found
    if (!thisUse || !selectedUse) {
        console.error('etaniItemImageClick: Could not find one or both <use> elements.');
        return;
    }

    // --- Part 4.3: Reorder if thisItem is before selectedItem ---
    if (thisItemIndex < selectedItemIndex) {
        // Move selectedUse to be immediately before thisUse
        // Example: 01234, this=2, selected=4. Result: 01423
        thisUse.parentElement.insertBefore(selectedUse, thisUse);
    }
    // --- Part 4.4: Reorder if thisItem is after selectedItem ---
    else if (thisItemIndex > selectedItemIndex) {
        // Move selectedUse to be immediately after thisUse
        // We use .nextSibling to insert *after*
        // Example: 01234, this=4, selected=1. Result: 02341
        thisUse.parentElement.insertBefore(selectedUse, thisUse.nextSibling);
    }

    // --- Final Step: Update selection state ---
    // Remove 'selected' from the previously selected item
    selectedItem.classList.remove('selected');
    // Add 'selected' to the newly clicked item
    thisItem.classList.add('selected');
}
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复

在线用户

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