Re: 正在做etani,ejtile的動畫套件
发表于 : 2025年 10月 17日 02:51
代码: 全选
對以下js代碼:
```
// Global variable to hold the SVG clone and ensure state is maintained
let etaniClone = null;
// --- CSS STYLES ---
function addDynamicStyles() {
if (document.getElementById('dynamic-et-styles')) {
return;
}
const styleSheet = document.createElement('style');
styleSheet.id = 'dynamic-et-styles';
styleSheet.textContent = `
/* I. Styles for etaniouter and button */
.etaniouter {
margin-top: 10px;
padding: 10px;
border: 1px solid #ddd;
background-color: #f5f5f5;
overflow: auto;
}
/* II. Styles for etaniinner, etaniCtrl, etaniCol, etaniResult */
.etaniinner {
margin-top: 10px;
}
.etaniCtrl {
margin-bottom: 10px;
clear: both;
padding: 5px;
border: 1px solid #c0c0c0;
text-align: center; /* To center etaniContent, etaniSetting, etaniAllAppend */
}
/* 要求一:etaniContent, etaniSetting, etaniAllAppend 樣式 */
.etaniContent, .etaniSetting, .etaniAllAppend {
display: inline-block;
vertical-align: top;
padding: 5px;
border: 1px solid #ccc; /* 灰色邊框 */
margin: 0 5px 5px 5px;
text-align: left;
}
.etaniCol {
border: 1px solid #aaa;
padding: 5px;
overflow: auto;
margin-bottom: 10px;
clear: both;
}
/* IV. Styles for etaniResult */
.etaniResult {
text-align: center;
margin-bottom: 10px;
padding: 10px;
border: 1px solid #bbb;
overflow: auto;
}
/* ------------------ CONTROL & BUTTON STYLES ------------------ */
/* Styles for control links (Center button) */
.etaniContent a {
display: inline-block;
margin: 0 5px;
text-decoration: none;
padding: 5px 10px;
font-size: 14px;
}
.etaniCenter {
border: 1px solid green;
color: green;
}
.etaniContentHTML {
border: 1px solid #0099ff;
color: #0099ff;
margin-right: 15px;
}
/* 要求三:etaniAllAppend buttons */
.etaniAllAppend button {
padding: 5px 10px;
font-size: 16px;
margin: 0 5px;
cursor: pointer;
border: 1px solid #333;
background-color: #fff;
}
/* 要求二:自定義 Radio 按鈕樣式 */
.etaniSettingMode {
display: inline-block;
cursor: pointer;
padding: 4px 8px;
margin: 0 3px;
font-size: 14px;
border: 1px solid #888;
background-color: #eee;
color: #333;
user-select: none;
}
.etaniSettingMode.active {
background-color: #008CBA;
color: white;
border-color: #008CBA;
}
/* ------------------ RESULT & ANIMAION ITEM STYLES ------------------ */
.etaniResultDR {
text-align: center;
margin-bottom: 10px;
}
/* 要求一:下載和重命名連結字號16px */
.etaniResultDownload, .etaniResultRename {
display: inline-block;
margin-right: 15px;
text-decoration: none;
padding: 5px 10px;
font-size: 16px;
}
/* .etaniResultDownload default style */
.etaniResultDownload {
border: 1px solid blue;
color: blue;
}
/* .etaniResultRename style (要求一:顏色改為 brown) */
.etaniResultRename {
border: 1px solid brown;
color: brown;
}
.etaniResultImage {
display: block;
max-width: 480px;
width: 100%;
height: auto;
margin: 0 auto 10px auto;
border: 1px solid #000;
}
/* 要求二:最小字號 12px */
.etaniResultSize {
display: inline-block;
margin-left: 10px;
font-size: 12px;
color: #555;
}
/* etaniItem structure (Float layout) */
.etaniItem {
min-height: 48px;
border: 1px solid #ccc;
box-sizing: border-box;
width: 100%;
margin-bottom: -1px;
overflow: auto;
}
.etaniItemLeft {
float: left;
width: 60px;
min-height: 48px;
border-right: 1px solid #ccc;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #e8e8e8;
padding: 2px 0;
}
.etaniItemRight {
margin-left: 60px;
padding: 7px;
min-height: 48px;
background-color: #fff;
}
/* 要求二:最小字號 12px */
.tileid {
text-align: center;
font-size: 12px;
word-break: break-all;
padding-top: 2px;
}
/* ------------------ ANIMATE CONTROLS (4.1) ------------------ */
.etaniAnimate {
border: 1px solid #999;
padding: 5px;
margin-bottom: 5px;
}
.etaniAnimateName {
display: inline-block;
padding: 2px 5px;
background-color: #555; /* 灰底 */
color: white; /* 白字 */
margin-right: 10px;
font-size: 12px; /* 最小字號 12px */
}
.etaniAnimateDur {
display: inline-block;
margin-right: 10px;
font-size: 14px;
}
.etaniAnimateValue {
margin-top: 5px;
overflow: auto;
}
.etaniAVAdd {
display: inline-block;
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
background-color: #a7fca7; /* 淺綠色 */
border: 1px solid #71c371; /* 細線近淺綠色 */
margin-right: 5px;
cursor: pointer;
box-sizing: border-box;
font-size: 20px; /* For plus sign SVG */
}
.etaniAVLabel {
font-size: 14px;
margin-right: 5px;
}
.etaniAV {
display: inline-block;
vertical-align: top;
}
.etaniAVItem {
display: inline-block;
width: 24px;
height: 24px;
background-color: #ff9933; /* 暗橙色 */
border: 1px dashed #00bfff; /* 虛線天藍色 */
margin: 0 5px;
box-sizing: border-box;
cursor: pointer;
}
/* ------------------ MODAL/POPUP STYLES ------------------ */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.modal-content {
position: fixed;
width: 98%;
height: 48%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 1001;
box-sizing: border-box;
}
.modal-content textarea {
width: 100%;
height: calc(100% - 30px); /* Adjust based on button/header height */
resize: none;
border: 1px solid #ccc;
font-size: 12px;
box-sizing: border-box;
}
.modal-close {
position: absolute;
top: 5px;
right: 10px;
font-size: 24px;
cursor: pointer;
color: #333;
}
`;
document.head.appendChild(styleSheet);
}
// --- UTILITY FUNCTIONS ---
function svgToBase64(svgString) {
const encoder = new TextEncoder();
const svgBytes = encoder.encode(svgString);
const byteString = String.fromCharCode.apply(null, svgBytes);
const base64 = btoa(byteString);
return `data:image/svg+xml;base64,${base64}`;
}
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function updateEtaniResult() {
// ... (保持不變的 updateEtaniResult 函數) ...
if (!etaniClone) return;
const svgString = new XMLSerializer().serializeToString(etaniClone);
const sizeInBytes = new Blob([svgString]).size;
const base64Url = svgToBase64(svgString);
const now = new Date();
const yyyy = now.getFullYear();
const mm = String(now.getMonth() + 1).padStart(2, '0');
const dd = String(now.getDate()).padStart(2, '0');
const hh = String(now.getHours()).padStart(2, '0');
const mi = String(now.getMinutes()).padStart(2, '0');
const ss = String(now.getSeconds()).padStart(2, '0');
const defaultFilename = `ejtileAnimation_${yyyy}${mm}${dd}_${hh}${mi}${ss}.svg`;
const imgElement = document.querySelector('.etaniResultImage');
const downloadElement = document.querySelector('.etaniResultDownload');
const renameElement = document.querySelector('.etaniResultRename');
const sizeElement = document.querySelector('.etaniResultSize');
if (imgElement && downloadElement && renameElement && sizeElement) {
imgElement.src = base64Url;
sizeElement.textContent = `Size: ${formatBytes(sizeInBytes)}`;
downloadElement.href = base64Url;
downloadElement.download = defaultFilename;
renameElement.onclick = (e) => {
e.preventDefault();
let currentDownloadName = downloadElement.download;
let newFilename = prompt("Enter new filename:", currentDownloadName);
if (newFilename) {
if (!newFilename.toLowerCase().endsWith('.svg')) {
newFilename += '.svg';
}
downloadElement.download = newFilename;
alert(`Filename changed to: ${newFilename}`);
}
};
}
}
function handleCenterClick(e) {
e.preventDefault();
if (etaniClone) {
const etdrop = etaniClone.querySelector('.etdrop');
if (etdrop) {
etdrop.setAttribute('transform', 'translate(240,240) scale(1,1)');
updateEtaniResult();
}
}
}
// --- NEW HANDLERS ---
/**
* 處理「+」按鈕點擊事件 (要求六)
* 增加一個新的 animateTransform value,使用 svg#etmain 中對應 <use/> 的當前 transform 值。
*/
function handleAVAddClick(e, itemIndex, useElementId) {
e.preventDefault();
if (!etaniClone) return;
// 1. 找到 svg#etmain 中對應的 <use> 元素,獲取其 transform 值 (作為新的 value)
const originalUseElement = document.querySelector(`.etdrop use[href="#${useElementId}"]`);
const newTransformValue = originalUseElement ? originalUseElement.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)' : 'translate(0,0) scale(1,1) rotate(0)';
// 從 transform 字符串中提取 translate, scale, rotate 的值
const getTransformValue = (type) => {
const match = newTransformValue.match(new RegExp(`${type}\\(([^)]+)\\)`, 'i'));
return match ? match[1].split(/[,\s]+/).join(',') : (type === 'scale' ? '1,1' : '0');
};
const translateValue = getTransformValue('translate');
const scaleValue = getTransformValue('scale');
const rotateValue = getTransformValue('rotate');
// 2. 找到 etani_clone 中對應的 <use> 元素
const cloneUseElement = etaniClone.querySelector(`use[href="#${useElementId}"]`);
if (cloneUseElement) {
// 3. 更新三個 animateTransform 標籤的 values
const animates = cloneUseElement.querySelectorAll('animateTransform');
animates.forEach(animate => {
const type = animate.getAttribute('type').toLowerCase();
let currentValue = animate.getAttribute('values') || '';
let newValue = '';
if (type === 'translate') {
newValue = translateValue;
} else if (type === 'scale') {
newValue = scaleValue;
} else if (type === 'rotate') {
newValue = rotateValue;
}
// Append the new value to the existing string, separated by a semicolon
animate.setAttribute('values', (currentValue ? currentValue + ';' : '') + newValue);
});
// 4. 在 .etaniItemRight 中增加一個新的 .etaniAVItem
const etaniItemRight = e.currentTarget.closest('.etaniItemRight');
const etaniAV = etaniItemRight.querySelector('.etaniAV');
if (etaniAV) {
const newAVItem = document.createElement('span');
newAVItem.className = 'etaniAVItem';
etaniAV.appendChild(newAVItem);
}
// 5. 即時更新 SVG
updateEtaniResult();
}
}
/**
* 處理「Transform」按鈕點擊事件 (要求四)
* 向所有 <use/> 元素添加三個 animateTransform 標籤,並在 .etaniItemRight 中增加控制項。
*/
function handleAllAppendTransformClick() {
if (!etaniClone) return;
const etdropUses = etaniClone.querySelectorAll('.etdrop use');
const etaniItemRights = document.querySelectorAll('.etaniItemRight');
etdropUses.forEach((useElement, i) => {
const useId = useElement.getAttribute('href').substring(1);
const itemRight = etaniItemRights[i];
if (!itemRight || itemRight.querySelector('.etaniAnimate')) return; // 避免重複添加
// 4.2:為 <use/> 添加 animateTransform 標籤
const baseAnimate = (type, values) => {
const animate = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
animate.setAttribute('attributeName', 'transform');
animate.setAttribute('attributeType', 'XML');
animate.setAttribute('type', type);
animate.setAttribute('values', values);
animate.setAttribute('dur', '1s');
animate.setAttribute('fill', 'freeze');
animate.setAttribute('additive', 'sum');
return animate;
};
// 初始值設置為 '0',因為 additive="sum",表示相對位移/縮放/旋轉
useElement.appendChild(baseAnimate('translate', '0,0'));
useElement.appendChild(baseAnimate('scale', '1')); // scale 初始值應為 1 (不變)
useElement.appendChild(baseAnimate('rotate', '0'));
// 4.1:為 .etaniItemRight 增加 .etaniAnimate 控制項
// 外部 wrapper
const etaniAnimate = document.createElement('div');
etaniAnimate.className = 'etaniAnimate';
// Animate Name (灰色背景白色字體)
const nameSpan = document.createElement('span');
nameSpan.className = 'etaniAnimateName';
nameSpan.textContent = 'transform';
etaniAnimate.appendChild(nameSpan);
// Animate Dur (dur: n)
const durSpan = document.createElement('span');
durSpan.className = 'etaniAnimateDur';
durSpan.textContent = 'dur: 1s'; // 初始值 1s
etaniAnimate.appendChild(durSpan);
// Animate Value container
const valueDiv = document.createElement('div');
valueDiv.className = 'etaniAnimateValue';
// AV Add (加號 SVG)
const avAddSpan = document.createElement('span');
avAddSpan.className = 'etaniAVAdd';
avAddSpan.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>`;
avAddSpan.title = 'Add Transform Value';
// 綁定事件:傳入當前的 item 索引和 use 元素的 ID
avAddSpan.addEventListener('click', (e) => handleAVAddClick(e, i, useId));
// AV Label
const avLabelSpan = document.createElement('span');
avLabelSpan.className = 'etaniAVLabel';
avLabelSpan.textContent = 'values : ';
// AV Container
const avDiv = document.createElement('div');
avDiv.className = 'etaniAV';
// AV Item (最初只含一個)
const avItemSpan = document.createElement('span');
avItemSpan.className = 'etaniAVItem';
avDiv.appendChild(avItemSpan);
valueDiv.appendChild(avAddSpan);
valueDiv.appendChild(avLabelSpan);
valueDiv.appendChild(avDiv);
etaniAnimate.appendChild(valueDiv);
itemRight.appendChild(etaniAnimate);
});
// 即時更新 SVG
updateEtaniResult();
}
/**
* 處理 Setting Mode (Freeze/Repeat) 切換 (要求五)
*/
function handleSettingModeChange(mode) {
if (!etaniClone) return;
const animates = etaniClone.querySelectorAll('animateTransform');
const isRepeat = mode === 'repeat';
animates.forEach(animate => {
if (isRepeat) {
animate.removeAttribute('fill');
animate.setAttribute('repeatCount', 'indefinite');
} else {
animate.removeAttribute('repeatCount');
animate.setAttribute('fill', 'freeze');
}
});
// 更新 active 狀態
document.querySelector('.etaniSettingFreeze').classList.toggle('active', !isRepeat);
document.querySelector('.etaniSettingRepeat').classList.toggle('active', isRepeat);
updateEtaniResult();
}
/**
* 處理 HTML 彈出視窗 (要求七)
*/
function handleContentHTMLClick(e) {
e.preventDefault();
if (!etaniClone) return;
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
const content = document.createElement('div');
content.className = 'modal-content';
const close = document.createElement('span');
close.className = 'modal-close';
close.innerHTML = '×'; // 'x' icon
const textarea = document.createElement('textarea');
textarea.value = etaniClone.outerHTML; // 輸出 etani_clone 的 outerHTML
// 關閉邏輯
const closeModal = () => {
document.body.removeChild(overlay);
document.body.removeChild(content);
};
close.onclick = closeModal;
overlay.onclick = closeModal; // 點擊背景也關閉
content.appendChild(close);
content.appendChild(textarea);
document.body.appendChild(overlay);
document.body.appendChild(content);
}
// --- MAIN STRUCTURE CREATION ---
function createEtaniInner(etaniouter) {
// 5.1, 5.2, 5.3 Clone, remove .etwait, update ID
const originalSvg = document.getElementById('etmain');
if (!originalSvg) {
console.error('SVG with ID "etmain" not found.');
return;
}
etaniClone = originalSvg.cloneNode(true);
const etwaitElement = etaniClone.querySelector('.etwait');
if (etwaitElement) {
etwaitElement.remove();
}
etaniClone.id = 'etmainani';
// Create div.etaniinner
const etaniinner = document.createElement('div');
etaniinner.className = 'etaniinner';
etaniinner.id = 'etaniinner';
// Create div.etaniCtrl
const etaniCtrl = document.createElement('div');
etaniCtrl.className = 'etaniCtrl';
// 要求七:a.etaniContentHTML
const etaniContent = document.createElement('div');
etaniContent.className = 'etaniContent';
const contentHTMLLink = document.createElement('a');
contentHTMLLink.className = 'etaniContentHTML';
contentHTMLLink.textContent = 'Show HTML';
contentHTMLLink.href = '#';
contentHTMLLink.addEventListener('click', handleContentHTMLClick);
etaniContent.appendChild(contentHTMLLink);
// a.etaniCenter
const centerLink = document.createElement('a');
centerLink.className = 'etaniCenter';
centerLink.textContent = 'Center';
centerLink.href = '#';
centerLink.addEventListener('click', handleCenterClick);
etaniContent.appendChild(centerLink);
etaniCtrl.appendChild(etaniContent);
// 要求一、二:div.etaniSetting (Radio Buttons)
const etaniSetting = document.createElement('div');
etaniSetting.className = 'etaniSetting';
const freezeRadio = document.createElement('span');
freezeRadio.className = 'etaniSettingMode etaniSettingFreeze active';
freezeRadio.textContent = 'Freeze';
freezeRadio.setAttribute('data-mode', 'freeze');
freezeRadio.addEventListener('click', () => handleSettingModeChange('freeze'));
const repeatRadio = document.createElement('span');
repeatRadio.className = 'etaniSettingMode etaniSettingRepeat';
repeatRadio.textContent = 'Repeat';
repeatRadio.setAttribute('data-mode', 'repeat');
repeatRadio.addEventListener('click', () => handleSettingModeChange('repeat'));
etaniSetting.appendChild(freezeRadio);
etaniSetting.appendChild(repeatRadio);
etaniCtrl.appendChild(etaniSetting);
// 要求一、三:div.etaniAllAppend (Buttons)
const etaniAllAppend = document.createElement('div');
etaniAllAppend.className = 'etaniAllAppend';
const transformButton = document.createElement('button');
transformButton.className = 'etaniAllAppendTransform';
transformButton.textContent = 'transform';
transformButton.addEventListener('click', handleAllAppendTransformClick);
const opacityButton = document.createElement('button');
opacityButton.className = 'etaniAllAppendOpacity';
opacityButton.textContent = 'opacity';
// opacityButton.addEventListener('click', handleAllAppendOpacityClick); // Placeholder
etaniAllAppend.appendChild(transformButton);
etaniAllAppend.appendChild(opacityButton);
etaniCtrl.appendChild(etaniAllAppend);
// Create div.etaniCol (Tile list)
const etaniCol = document.createElement('div');
etaniCol.className = 'etaniCol';
// Create div.etaniResult
const etaniResult = document.createElement('div');
etaniResult.className = 'etaniResult';
// Result elements (img, download, rename, size) ... (略,與前次相同)
const resultImg = document.createElement('img');
resultImg.className = 'etaniResultImage';
resultImg.alt = 'Rendered Ejtile Animation SVG';
const resultDRDiv = document.createElement('div');
resultDRDiv.className = 'etaniResultDR';
const downloadLink = document.createElement('a');
downloadLink.className = 'etaniResultDownload';
downloadLink.textContent = 'Download SVG';
downloadLink.href = '#';
const renameLink = document.createElement('a');
renameLink.className = 'etaniResultRename';
renameLink.textContent = 'Rename File';
renameLink.href = '#';
const sizeSpan = document.createElement('span');
sizeSpan.className = 'etaniResultSize';
resultDRDiv.appendChild(downloadLink);
resultDRDiv.appendChild(renameLink);
etaniResult.appendChild(resultImg);
etaniResult.appendChild(resultDRDiv);
etaniResult.appendChild(sizeSpan);
// Append children to etaniinner
etaniinner.appendChild(etaniCtrl);
etaniinner.appendChild(etaniCol);
etaniinner.appendChild(etaniResult);
// Append etaniinner to etaniouter
etaniouter.appendChild(etaniinner);
// III, IV, V, VI, VII. Populate etaniCol
const etdropUses = document.querySelectorAll('.etdrop use');
const etwaitGroups = document.querySelectorAll('.etwait g');
etdropUses.forEach((useElement, i) => {
// (Tile processing logic - 保持不變)
const tileid = useElement.getAttribute('href').substring(1);
let targetUse = null;
for (const group of etwaitGroups) {
const useInGroup = group.querySelector(`use[href="#${tileid}"]`);
if (useInGroup) {
targetUse = useInGroup;
break;
}
}
let etwaittransform = '', etwaitfill = '', etwaitstroke = '', etwaitstrokeWidth = '';
if (targetUse) {
etwaittransform = targetUse.getAttribute('transform') || '';
etwaitfill = targetUse.getAttribute('fill') || '';
etwaitstroke = targetUse.getAttribute('stroke') || '';
etwaitstrokeWidth = targetUse.getAttribute('stroke-width') || '';
const scaleMatch = etwaittransform.match(/scale\(([^)]+)\)/);
const rotateMatch = etwaittransform.match(/rotate\(([^)]+)\)/);
const scalePart = scaleMatch ? scaleMatch[0] : '';
const rotatePart = rotateMatch ? rotateMatch[0] : '';
etwaittransform = `translate(20,20) ${scalePart} ${rotatePart}`.trim().replace(/\s+/g, ' ');
}
const originalTile = document.querySelector(`defs g#${tileid}`);
const etaniItem = document.createElement('div');
etaniItem.className = 'etaniItem';
const etaniItemLeft = document.createElement('div');
etaniItemLeft.className = 'etaniItemLeft';
const etaniItemRight = document.createElement('div');
etaniItemRight.className = 'etaniItemRight';
if (originalTile) {
const tileclone = originalTile.cloneNode(true);
tileclone.removeAttribute('id');
if (etwaittransform) tileclone.setAttribute('transform', etwaittransform);
if (etwaitfill) tileclone.setAttribute('fill', etwaitfill);
if (etwaitstroke) tileclone.setAttribute('stroke', etwaitstroke);
if (etwaitstrokeWidth) tileclone.setAttribute('stroke-width', etwaitstrokeWidth);
const svgWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svgWrapper.setAttribute('width', '40');
svgWrapper.setAttribute('height', '40');
svgWrapper.setAttribute('version', '1.1');
svgWrapper.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
svgWrapper.setAttribute('xmlns:svg', 'http://www.w3.org/2000/svg');
svgWrapper.className = 'etanitileimg';
svgWrapper.appendChild(tileclone);
etaniItemLeft.appendChild(svgWrapper);
}
const tileidDiv = document.createElement('div');
tileidDiv.className = 'tileid';
tileidDiv.textContent = tileid;
etaniItemLeft.appendChild(tileidDiv);
etaniItem.appendChild(etaniItemLeft);
etaniItem.appendChild(etaniItemRight);
etaniCol.appendChild(etaniItem);
});
// 5. Update the result section immediately
updateEtaniResult();
}
// --- INITIALIZATION ---
function toggleAnimation(event) {
const etanibutton = event.currentTarget;
const etaniouter = etanibutton.parentNode;
const etaniinner = document.getElementById('etaniinner');
if (etanibutton.textContent === 'Animate it') {
etanibutton.textContent = 'Close Ejtile Ani';
createEtaniInner(etaniouter);
} else if (etanibutton.textContent === 'Close Ejtile Ani') {
etanibutton.textContent = 'Animate it';
if (etaniinner) {
etaniinner.remove();
}
etaniClone = null;
}
}
window.addEventListener('load', () => {
addDynamicStyles();
const etmainouter = document.getElementById('etmainouter');
if (etmainouter) {
const etaniouter = document.createElement('div');
etaniouter.className = 'etaniouter';
const etanibutton = document.createElement('button');
etanibutton.id = 'etanibutton';
etanibutton.textContent = 'Animate it';
etanibutton.addEventListener('click', toggleAnimation);
etaniouter.appendChild(etanibutton);
etmainouter.parentNode.insertBefore(etaniouter, etmainouter.nextSibling);
} else {
console.error('Div with ID "etmainouter" not found.');
}
});
```
改動:
一,前面的代碼中,etani_clone被寫成etaniClone,現在要改回etani_clone。
二,當新增animateTransform的時候,這三個標籤的初始值,是
translate(0,0)
scale(1,1)
rotate(0)
三,當在animateTransform增加value時,設它的三個標籤的值的計算結果為translate(v1) scale(v2) rotate(v3)。它的計算方法是:
首先取出它在svg#etmain中的.etdrop中的<use/>的transform="translate(cv1) scale(cv2) rotate(cv3)"的三個值,之後分別減去在etani_clone中的.etdrop中的<use/>的transform="translate(ov1) scale(ov2) rotate(ov3)"的三個值。
v1=cv1-ov1
v2=cv2/ov2
v3=cv3-ov3
四,當新增animateTransform時,程式要在etani_clone中的.etdrop中的每一個<use/>的外部增加一個<g></g>,animateTransform的translate改為放在它裡面。
五,modal-close的字體大小設為48px,line-height設為1,top為0。
用繁體中文回覆。在代碼中所有注釋都要用英文。
代码: 全选
// Global variable to hold the SVG clone and ensure state is maintained
let etani_clone = null;
// --- CSS STYLES ---
function addDynamicStyles() {
if (document.getElementById('dynamic-et-styles')) {
return;
}
const styleSheet = document.createElement('style');
styleSheet.id = 'dynamic-et-styles';
styleSheet.textContent = `
/* I. Styles for etaniouter and button */
.etaniouter {
margin-top: 10px;
padding: 10px;
border: 1px solid #ddd;
background-color: #f5f5f5;
overflow: auto;
}
/* II. Styles for etaniinner, etaniCtrl, etaniCol, etaniResult */
.etaniinner {
margin-top: 10px;
}
.etaniCtrl {
margin-bottom: 10px;
clear: both;
padding: 5px;
border: 1px solid #c0c0c0;
text-align: center; /* To center etaniContent, etaniSetting, etaniAllAppend */
}
/* Requirement 1: etaniContent, etaniSetting, etaniAllAppend styles */
.etaniContent, .etaniSetting, .etaniAllAppend {
display: inline-block;
vertical-align: top;
padding: 5px;
border: 1px solid #ccc; /* Grey border */
margin: 0 5px 5px 5px;
text-align: left;
}
.etaniCol {
border: 1px solid #aaa;
padding: 5px;
overflow: auto;
margin-bottom: 10px;
clear: both;
}
/* IV. Styles for etaniResult */
.etaniResult {
text-align: center;
margin-bottom: 10px;
padding: 10px;
border: 1px solid #bbb;
overflow: auto;
}
/* ------------------ CONTROL & BUTTON STYLES ------------------ */
/* Styles for control links (Center button) */
.etaniContent a {
display: inline-block;
margin: 0 5px;
text-decoration: none;
padding: 5px 10px;
font-size: 14px;
}
.etaniCenter {
border: 1px solid green;
color: green;
}
.etaniContentHTML {
border: 1px solid #0099ff;
color: #0099ff;
margin-right: 15px;
}
/* Requirement 3: etaniAllAppend buttons */
.etaniAllAppend button {
padding: 5px 10px;
font-size: 16px;
margin: 0 5px;
cursor: pointer;
border: 1px solid #333;
background-color: #fff;
}
/* Requirement 2: Custom Radio button styles */
.etaniSettingMode {
display: inline-block;
cursor: pointer;
padding: 4px 8px;
margin: 0 3px;
font-size: 14px;
border: 1px solid #888;
background-color: #eee;
color: #333;
user-select: none;
}
.etaniSettingMode.active {
background-color: #008CBA;
color: white;
border-color: #008CBA;
}
/* ------------------ RESULT & ANIMAION ITEM STYLES ------------------ */
.etaniResultDR {
text-align: center;
margin-bottom: 10px;
}
/* Requirement 1: Download and rename link font size 16px */
.etaniResultDownload, .etaniResultRename {
display: inline-block;
margin-right: 15px;
text-decoration: none;
padding: 5px 10px;
font-size: 16px;
}
/* .etaniResultDownload default style */
.etaniResultDownload {
border: 1px solid blue;
color: blue;
}
/* .etaniResultRename style (Requirement 1: color changed to brown) */
.etaniResultRename {
border: 1px solid brown;
color: brown;
}
.etaniResultImage {
display: block;
max-width: 480px;
width: 100%;
height: auto;
margin: 0 auto 10px auto;
border: 1px solid #000;
}
/* Requirement 2: Minimum font size 12px */
.etaniResultSize {
display: inline-block;
margin-left: 10px;
font-size: 12px;
color: #555;
}
/* etaniItem structure (Float layout) */
.etaniItem {
min-height: 48px;
border: 1px solid #ccc;
box-sizing: border-box;
width: 100%;
margin-bottom: -1px;
overflow: auto;
}
.etaniItemLeft {
float: left;
width: 60px;
min-height: 48px;
border-right: 1px solid #ccc;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #e8e8e8;
padding: 2px 0;
}
.etaniItemRight {
margin-left: 60px;
padding: 7px;
min-height: 48px;
background-color: #fff;
}
/* Requirement 2: Minimum font size 12px */
.tileid {
text-align: center;
font-size: 12px;
word-break: break-all;
padding-top: 2px;
}
/* ------------------ ANIMATE CONTROLS (4.1) ------------------ */
.etaniAnimate {
border: 1px solid #999;
padding: 5px;
margin-bottom: 5px;
}
.etaniAnimateName {
display: inline-block;
padding: 2px 5px;
background-color: #555; /* Dark grey background */
color: white; /* White text */
margin-right: 10px;
font-size: 12px; /* Minimum font size 12px */
}
.etaniAnimateDur {
display: inline-block;
margin-right: 10px;
font-size: 14px;
}
.etaniAnimateValue {
margin-top: 5px;
overflow: auto;
}
.etaniAVAdd {
display: inline-block;
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
background-color: #a7fca7; /* Light green */
border: 1px solid #71c371; /* Thin line near light green */
margin-right: 5px;
cursor: pointer;
box-sizing: border-box;
font-size: 20px; /* For plus sign SVG */
}
.etaniAVLabel {
font-size: 14px;
margin-right: 5px;
}
.etaniAV {
display: inline-block;
vertical-align: top;
}
.etaniAVItem {
display: inline-block;
width: 24px;
height: 24px;
background-color: #ff9933; /* Dark orange */
border: 1px dashed #00bfff; /* Dashed sky blue */
margin: 0 5px;
box-sizing: border-box;
cursor: pointer;
}
/* ------------------ MODAL/POPUP STYLES ------------------ */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.modal-content {
position: fixed;
width: 98%;
height: 48%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 1001;
box-sizing: border-box;
}
.modal-content textarea {
width: 100%;
height: calc(100% - 50px); /* Adjusted for larger close button */
resize: none;
border: 1px solid #ccc;
font-size: 12px;
box-sizing: border-box;
}
/* Requirement 5: modal-close font size 48px, line-height 1, top 0 */
.modal-close {
position: absolute;
top: 0;
right: 10px;
font-size: 48px;
line-height: 1;
cursor: pointer;
color: #333;
}
`;
document.head.appendChild(styleSheet);
}
// --- UTILITY FUNCTIONS ---
/**
* Converts an SVG string to a Base64 data URL.
* @param {string} svgString The SVG XML string.
* @returns {string} The Base64 data URL.
*/
function svgToBase64(svgString) {
// Note: Using encodeURIComponent and btoa for broader compatibility,
// although TextEncoder/fromCharCode is often faster.
const base64 = btoa(unescape(encodeURIComponent(svgString)));
return `data:image/svg+xml;base64,${base64}`;
}
/**
* Formats byte size into human-readable string (e.g., KB, MB).
* @param {number} bytes The size in bytes.
* @returns {string} The formatted string.
*/
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* Parses transform string to get individual transform values.
* @param {string} transformString The transform attribute string.
* @returns {{translate: string, scale: string, rotate: string}} Parsed values.
*/
function parseTransform(transformString) {
const defaultTransform = { translate: '0,0', scale: '1,1', rotate: '0' };
if (!transformString) return defaultTransform;
const transform = {};
const getMatch = (type) => {
const match = transformString.match(new RegExp(`${type}\\(([^)]+)\\)`, 'i'));
return match ? match[1].split(/[,\s]+/).join(',') : null;
};
transform.translate = getMatch('translate') || defaultTransform.translate;
transform.scale = getMatch('scale') || defaultTransform.scale;
transform.rotate = getMatch('rotate') || defaultTransform.rotate;
return transform;
}
/**
* Updates the result section with the current state of etani_clone.
*/
function updateEtaniResult() {
if (!etani_clone) return;
const svgString = new XMLSerializer().serializeToString(etani_clone);
const sizeInBytes = new Blob([svgString]).size;
const base64Url = svgToBase64(svgString);
const now = new Date();
const yyyy = now.getFullYear();
const mm = String(now.getMonth() + 1).padStart(2, '0');
const dd = String(now.getDate()).padStart(2, '0');
const hh = String(now.getHours()).padStart(2, '0');
const mi = String(now.getMinutes()).padStart(2, '0');
const ss = String(now.getSeconds()).padStart(2, '0');
const defaultFilename = `ejtileAnimation_${yyyy}${mm}${dd}_${hh}${mi}${ss}.svg`;
const imgElement = document.querySelector('.etaniResultImage');
const downloadElement = document.querySelector('.etaniResultDownload');
const renameElement = document.querySelector('.etaniResultRename');
const sizeElement = document.querySelector('.etaniResultSize');
if (imgElement && downloadElement && renameElement && sizeElement) {
imgElement.src = base64Url;
sizeElement.textContent = `Size: ${formatBytes(sizeInBytes)}`;
downloadElement.href = base64Url;
downloadElement.download = defaultFilename;
renameElement.onclick = (e) => {
e.preventDefault();
let currentDownloadName = downloadElement.download;
let newFilename = prompt("Enter new filename:", currentDownloadName);
if (newFilename) {
if (!newFilename.toLowerCase().endsWith('.svg')) {
newFilename += '.svg';
}
downloadElement.download = newFilename;
alert(`Filename changed to: ${newFilename}`);
}
};
}
}
/**
* Handler for the 'Center' button click. Resets the .etdrop transform.
* @param {Event} e The click event.
*/
function handleCenterClick(e) {
e.preventDefault();
if (etani_clone) {
const etdrop = etani_clone.querySelector('.etdrop');
if (etdrop) {
etdrop.setAttribute('transform', 'translate(240,240) scale(1,1)');
updateEtaniResult();
}
}
}
// --- NEW HANDLERS ---
/**
* Calculates the required *additive* transform value for a given type.
* @param {string} type The transform type ('translate', 'scale', 'rotate').
* @param {string} originalValue The transform value from the original <use/> (cv).
* @param {string} currentBaseValue The current base transform value from the etani_clone <use/> (ov).
* @returns {string} The new additive value (v).
*/
function calculateAdditiveValue(type, originalValue, currentBaseValue) {
if (type === 'translate') {
const ovCoords = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
const cvCoords = originalValue.split(',').map(c => parseFloat(c.trim()));
// v1 = cv1 - ov1, v2 = cv2 - ov2
return `${cvCoords[0] - ovCoords[0]},${cvCoords[1] - ovCoords[1]}`;
} else if (type === 'scale') {
const ovScales = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
const cvScales = originalValue.split(',').map(c => parseFloat(c.trim()));
// v1 = cv1 / ov1, v2 = cv2 / ov2. Note: scale needs to handle single vs double value.
const v1 = (cvScales[0] / ovScales[0]).toFixed(4);
const v2 = (cvScales.length > 1 ? (cvScales[1] / ovScales[1]) : v1).toFixed(4);
return v1 === v2 ? v1 : `${v1},${v2}`;
} else if (type === 'rotate') {
const ovAngle = parseFloat(currentBaseValue.trim());
const cvAngle = parseFloat(originalValue.trim());
// v3 = cv3 - ov3
return (cvAngle - ovAngle).toFixed(4);
}
return '';
}
/**
* Handles the click event for the '+' button (Requirement 6).
* Adds a new animateTransform value using the current transform of the corresponding <use/> in svg#etmain.
* @param {Event} e The click event.
* @param {number} itemIndex The index of the item (unused in this implementation but kept for context).
* @param {string} useElementId The ID of the original <g> element referenced by the <use/>.
*/
function handleAVAddClick(e, itemIndex, useElementId) {
e.preventDefault();
if (!etani_clone) return;
// 1. Get the current transform of the original <use/> element in svg#etmain
const originalUseElement = document.querySelector(`.etdrop use[href="#${useElementId}"]`);
if (!originalUseElement) return;
const originalTransformString = originalUseElement.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)';
const originalTransforms = parseTransform(originalTransformString);
// 2. Find the corresponding <g> wrapper in etani_clone
const cloneGWrapper = etani_clone.querySelector(`.etdrop > g[data-use-href="#${useElementId}"]`);
if (!cloneGWrapper) return;
// 3. Get the *base* transform of the <use> element inside the wrapper (which holds the initial values for the animation).
const cloneUseElement = cloneGWrapper.querySelector('use');
const currentBaseTransformString = cloneUseElement.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)';
const currentBaseTransforms = parseTransform(currentBaseTransformString);
// 4. Update the three animateTransform tags' values
const animates = cloneGWrapper.querySelectorAll('animateTransform');
animates.forEach(animate => {
const type = animate.getAttribute('type').toLowerCase();
let currentValueString = animate.getAttribute('values') || '';
let newValue = '';
// Calculate the *additive* difference based on the requirement: v = c - o (or c/o for scale)
if (type === 'translate') {
newValue = calculateAdditiveValue('translate', originalTransforms.translate, currentBaseTransforms.translate);
} else if (type === 'scale') {
newValue = calculateAdditiveValue('scale', originalTransforms.scale, currentBaseTransforms.scale);
} else if (type === 'rotate') {
newValue = calculateAdditiveValue('rotate', originalTransforms.rotate, currentBaseTransforms.rotate);
}
// Append the new value to the existing string, separated by a semicolon
animate.setAttribute('values', (currentValueString ? currentValueString + ';' : '') + newValue);
});
// 5. Add a new .etaniAVItem in .etaniItemRight
const etaniItemRight = e.currentTarget.closest('.etaniItemRight');
const etaniAV = etaniItemRight.querySelector('.etaniAV');
if (etaniAV) {
const newAVItem = document.createElement('span');
newAVItem.className = 'etaniAVItem';
etaniAV.appendChild(newAVItem);
}
// 6. Update the SVG result
updateEtaniResult();
}
/**
* Handles the click event for the 'Transform' button (Requirement 4).
* Adds three animateTransform tags to all <use/> elements and adds controls to .etaniItemRight.
*/
function handleAllAppendTransformClick() {
if (!etani_clone) return;
const etdropUses = document.querySelectorAll('#etmain .etdrop use'); // Select original <use> elements for their href
const etaniItemRights = document.querySelectorAll('.etaniItemRight');
etdropUses.forEach((originalUseElement, i) => {
const useId = originalUseElement.getAttribute('href').substring(1);
const itemRight = etaniItemRights[i];
if (!itemRight) return;
// 1. Find the corresponding <use> element in etani_clone
let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return;
// Check if transform controls already exist to prevent duplication
if (itemRight.querySelector('.etaniAnimate')) return;
// 2. Wrap the <use/> element in a <g> tag (Requirement 4)
const gWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
gWrapper.setAttribute('data-use-href', `#${useId}`); // Use a data attribute to link it back
// Preserve the base transform on the <use> element for additive calculation
const baseTransform = cloneUseElement.getAttribute('transform') || 'translate(0,0) scale(1,1) rotate(0)';
cloneUseElement.setAttribute('transform', baseTransform);
cloneUseElement.parentNode.insertBefore(gWrapper, cloneUseElement);
gWrapper.appendChild(cloneUseElement);
// 3. Add animateTransform tags to the <g> wrapper
const baseAnimate = (type, initialValue) => {
const animate = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
animate.setAttribute('attributeName', 'transform');
animate.setAttribute('attributeType', 'XML');
animate.setAttribute('type', type);
// Initial values: translate(0,0), scale(1) or scale(1,1), rotate(0) (Requirement 2 & 4.2)
animate.setAttribute('values', initialValue);
animate.setAttribute('dur', '1s');
animate.setAttribute('fill', 'freeze');
animate.setAttribute('additive', 'sum'); // Important for relative transforms
return animate;
};
// For additive="sum", the initial value in 'values' represents the *difference* from the base.
// If we want no initial difference, the value is the identity transformation:
gWrapper.appendChild(baseAnimate('translate', '0,0'));
gWrapper.appendChild(baseAnimate('scale', '1')); // scale '1' is identity (no change)
gWrapper.appendChild(baseAnimate('rotate', '0'));
// 4. Add .etaniAnimate controls to .etaniItemRight (4.1)
const etaniAnimate = document.createElement('div');
etaniAnimate.className = 'etaniAnimate';
// Animate Name (Dark grey background, white text)
const nameSpan = document.createElement('span');
nameSpan.className = 'etaniAnimateName';
nameSpan.textContent = 'transform';
etaniAnimate.appendChild(nameSpan);
// Animate Dur (dur: n)
const durSpan = document.createElement('span');
durSpan.className = 'etaniAnimateDur';
durSpan.textContent = 'dur: 1s'; // Initial value 1s
etaniAnimate.appendChild(durSpan);
// Animate Value container
const valueDiv = document.createElement('div');
valueDiv.className = 'etaniAnimateValue';
// AV Add (Plus sign SVG)
const avAddSpan = document.createElement('span');
avAddSpan.className = 'etaniAVAdd';
avAddSpan.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>`;
avAddSpan.title = 'Add Transform Value';
// Event binding: Pass the current item index and the original <use> element's ID
avAddSpan.addEventListener('click', (e) => handleAVAddClick(e, i, useId));
// AV Label
const avLabelSpan = document.createElement('span');
avLabelSpan.className = 'etaniAVLabel';
avLabelSpan.textContent = 'values : ';
// AV Container
const avDiv = document.createElement('div');
avDiv.className = 'etaniAV';
// AV Item (Initially contains one)
const avItemSpan = document.createElement('span');
avItemSpan.className = 'etaniAVItem';
avDiv.appendChild(avItemSpan);
valueDiv.appendChild(avAddSpan);
valueDiv.appendChild(avLabelSpan);
valueDiv.appendChild(avDiv);
etaniAnimate.appendChild(valueDiv);
itemRight.appendChild(etaniAnimate);
});
// 5. Update the SVG result immediately
updateEtaniResult();
}
/**
* Handles the Setting Mode switch (Freeze/Repeat) (Requirement 5).
* @param {string} mode The animation end mode ('freeze' or 'repeat').
*/
function handleSettingModeChange(mode) {
if (!etani_clone) return;
// We select animateTransform inside <g> wrappers
const animates = etani_clone.querySelectorAll('.etdrop > g > animateTransform');
const isRepeat = mode === 'repeat';
animates.forEach(animate => {
if (isRepeat) {
animate.removeAttribute('fill');
animate.setAttribute('repeatCount', 'indefinite');
} else {
animate.removeAttribute('repeatCount');
animate.setAttribute('fill', 'freeze');
}
});
// Update active state
document.querySelector('.etaniSettingFreeze').classList.toggle('active', !isRepeat);
document.querySelector('.etaniSettingRepeat').classList.toggle('active', isRepeat);
updateEtaniResult();
}
/**
* Handles the HTML popup window (Requirement 7).
* @param {Event} e The click event.
*/
function handleContentHTMLClick(e) {
e.preventDefault();
if (!etani_clone) return;
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
const content = document.createElement('div');
content.className = 'modal-content';
const close = document.createElement('span');
close.className = 'modal-close';
close.innerHTML = '×'; // 'x' icon
const textarea = document.createElement('textarea');
textarea.value = etani_clone.outerHTML; // Output etani_clone's outerHTML
// Close logic
const closeModal = () => {
if (document.body.contains(overlay)) document.body.removeChild(overlay);
if (document.body.contains(content)) document.body.removeChild(content);
};
close.onclick = closeModal;
overlay.onclick = closeModal; // Clicking background also closes
content.appendChild(close);
content.appendChild(textarea);
document.body.appendChild(overlay);
document.body.appendChild(content);
}
// --- MAIN STRUCTURE CREATION ---
/**
* Creates the internal animation control structure.
* @param {HTMLElement} etaniouter The outer container element.
*/
function createEtaniInner(etaniouter) {
// 5.1, 5.2, 5.3 Clone, remove .etwait, update ID
const originalSvg = document.getElementById('etmain');
if (!originalSvg) {
console.error('SVG with ID "etmain" not found.');
return;
}
etani_clone = originalSvg.cloneNode(true);
const etwaitElement = etani_clone.querySelector('.etwait');
if (etwaitElement) {
etwaitElement.remove();
}
etani_clone.id = 'etmainani';
// Create div.etaniinner
const etaniinner = document.createElement('div');
etaniinner.className = 'etaniinner';
etaniinner.id = 'etaniinner';
// Create div.etaniCtrl
const etaniCtrl = document.createElement('div');
etaniCtrl.className = 'etaniCtrl';
// Requirement 7: a.etaniContentHTML
const etaniContent = document.createElement('div');
etaniContent.className = 'etaniContent';
const contentHTMLLink = document.createElement('a');
contentHTMLLink.className = 'etaniContentHTML';
contentHTMLLink.textContent = 'Show HTML';
contentHTMLLink.href = '#';
contentHTMLLink.addEventListener('click', handleContentHTMLClick);
etaniContent.appendChild(contentHTMLLink);
// a.etaniCenter
const centerLink = document.createElement('a');
centerLink.className = 'etaniCenter';
centerLink.textContent = 'Center';
centerLink.href = '#';
centerLink.addEventListener('click', handleCenterClick);
etaniContent.appendChild(centerLink);
etaniCtrl.appendChild(etaniContent);
// Requirement 1, 2: div.etaniSetting (Radio Buttons)
const etaniSetting = document.createElement('div');
etaniSetting.className = 'etaniSetting';
const freezeRadio = document.createElement('span');
freezeRadio.className = 'etaniSettingMode etaniSettingFreeze active';
freezeRadio.textContent = 'Freeze';
freezeRadio.setAttribute('data-mode', 'freeze');
freezeRadio.addEventListener('click', () => handleSettingModeChange('freeze'));
const repeatRadio = document.createElement('span');
repeatRadio.className = 'etaniSettingMode etaniSettingRepeat';
repeatRadio.textContent = 'Repeat';
repeatRadio.setAttribute('data-mode', 'repeat');
repeatRadio.addEventListener('click', () => handleSettingModeChange('repeat'));
etaniSetting.appendChild(freezeRadio);
etaniSetting.appendChild(repeatRadio);
etaniCtrl.appendChild(etaniSetting);
// Requirement 1, 3: div.etaniAllAppend (Buttons)
const etaniAllAppend = document.createElement('div');
etaniAllAppend.className = 'etaniAllAppend';
const transformButton = document.createElement('button');
transformButton.className = 'etaniAllAppendTransform';
transformButton.textContent = 'transform';
transformButton.addEventListener('click', handleAllAppendTransformClick);
const opacityButton = document.createElement('button');
opacityButton.className = 'etaniAllAppendOpacity';
opacityButton.textContent = 'opacity';
// opacityButton.addEventListener('click', handleAllAppendOpacityClick); // Placeholder
etaniAllAppend.appendChild(transformButton);
etaniAllAppend.appendChild(opacityButton);
etaniCtrl.appendChild(etaniAllAppend);
// Create div.etaniCol (Tile list)
const etaniCol = document.createElement('div');
etaniCol.className = 'etaniCol';
// Create div.etaniResult
const etaniResult = document.createElement('div');
etaniResult.className = 'etaniResult';
// Result elements (img, download, rename, size)
const resultImg = document.createElement('img');
resultImg.className = 'etaniResultImage';
resultImg.alt = 'Rendered Ejtile Animation SVG';
const resultDRDiv = document.createElement('div');
resultDRDiv.className = 'etaniResultDR';
const downloadLink = document.createElement('a');
downloadLink.className = 'etaniResultDownload';
downloadLink.textContent = 'Download SVG';
downloadLink.href = '#';
const renameLink = document.createElement('a');
renameLink.className = 'etaniResultRename';
renameLink.textContent = 'Rename File';
renameLink.href = '#';
const sizeSpan = document.createElement('span');
sizeSpan.className = 'etaniResultSize';
resultDRDiv.appendChild(downloadLink);
resultDRDiv.appendChild(renameLink);
etaniResult.appendChild(resultImg);
etaniResult.appendChild(resultDRDiv);
etaniResult.appendChild(sizeSpan);
// Append children to etaniinner
etaniinner.appendChild(etaniCtrl);
etaniinner.appendChild(etaniCol);
etaniinner.appendChild(etaniResult);
// Append etaniinner to etaniouter
etaniouter.appendChild(etaniinner);
// III, IV, V, VI, VII. Populate etaniCol
const etdropUses = document.querySelectorAll('.etdrop use');
const etwaitGroups = document.querySelectorAll('.etwait g');
etdropUses.forEach((useElement, i) => {
// (Tile processing logic - Unchanged)
const tileid = useElement.getAttribute('href').substring(1);
let targetUse = null;
for (const group of etwaitGroups) {
const useInGroup = group.querySelector(`use[href="#${tileid}"]`);
if (useInGroup) {
targetUse = useInGroup;
break;
}
}
let etwaittransform = '', etwaitfill = '', etwaitstroke = '', etwaitstrokeWidth = '';
if (targetUse) {
etwaittransform = targetUse.getAttribute('transform') || '';
etwaitfill = targetUse.getAttribute('fill') || '';
etwaitstroke = targetUse.getAttribute('stroke') || '';
etwaitstrokeWidth = targetUse.getAttribute('stroke-width') || '';
const scaleMatch = etwaittransform.match(/scale\(([^)]+)\)/);
const rotateMatch = etwaittransform.match(/rotate\(([^)]+)\)/);
const scalePart = scaleMatch ? scaleMatch[0] : '';
const rotatePart = rotateMatch ? rotateMatch[0] : '';
etwaittransform = `translate(20,20) ${scalePart} ${rotatePart}`.trim().replace(/\s+/g, ' ');
}
const originalTile = document.querySelector(`defs g#${tileid}`);
const etaniItem = document.createElement('div');
etaniItem.className = 'etaniItem';
const etaniItemLeft = document.createElement('div');
etaniItemLeft.className = 'etaniItemLeft';
const etaniItemRight = document.createElement('div');
etaniItemRight.className = 'etaniItemRight';
if (originalTile) {
const tileclone = originalTile.cloneNode(true);
tileclone.removeAttribute('id');
if (etwaittransform) tileclone.setAttribute('transform', etwaittransform);
if (etwaitfill) tileclone.setAttribute('fill', etwaitfill);
if (etwaitstroke) tileclone.setAttribute('stroke', etwaitstroke);
if (etwaitstrokeWidth) tileclone.setAttribute('stroke-width', etwaitstrokeWidth);
const svgWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svgWrapper.setAttribute('width', '40');
svgWrapper.setAttribute('height', '40');
svgWrapper.setAttribute('version', '1.1');
svgWrapper.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
svgWrapper.setAttribute('xmlns:svg', 'http://www.w3.org/2000/svg');
svgWrapper.className = 'etanitileimg';
svgWrapper.appendChild(tileclone);
etaniItemLeft.appendChild(svgWrapper);
}
const tileidDiv = document.createElement('div');
tileidDiv.className = 'tileid';
tileidDiv.textContent = tileid;
etaniItemLeft.appendChild(tileidDiv);
etaniItem.appendChild(etaniItemLeft);
etaniItem.appendChild(etaniItemRight);
etaniCol.appendChild(etaniItem);
});
// 5. Update the result section immediately
updateEtaniResult();
}
// --- INITIALIZATION ---
/**
* Toggles the visibility and content of the animation control panel.
* @param {Event} event The click event.
*/
function toggleAnimation(event) {
const etanibutton = event.currentTarget;
const etaniouter = etanibutton.parentNode;
const etaniinner = document.getElementById('etaniinner');
if (etanibutton.textContent === 'Animate it') {
etanibutton.textContent = 'Close Ejtile Ani';
createEtaniInner(etaniouter);
} else if (etanibutton.textContent === 'Close Ejtile Ani') {
etanibutton.textContent = 'Animate it';
if (etaniinner) {
etaniinner.remove();
}
etani_clone = null;
}
}
window.addEventListener('load', () => {
addDynamicStyles();
const etmainouter = document.getElementById('etmainouter');
if (etmainouter) {
const etaniouter = document.createElement('div');
etaniouter.className = 'etaniouter';
const etanibutton = document.createElement('button');
etanibutton.id = 'etanibutton';
etanibutton.textContent = 'Animate it';
etanibutton.addEventListener('click', toggleAnimation);
etaniouter.appendChild(etanibutton);
etmainouter.parentNode.insertBefore(etaniouter, etmainouter.nextSibling);
} else {
console.error('Div with ID "etmainouter" not found.');
}
});