拿到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.
}
}