正在做etani,ejtile的動畫套件
Re: 正在做etani,ejtile的動畫套件
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 正在做etani,ejtile的動畫套件
當前代碼:
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 正在做etani,ejtile的動畫套件
在執行了etaniMatchTiles之後,如果數量有變化,.etaniCol中的.etaniItem也要相應的增加或減少。
只需給出要修改的代碼和位置,以及所要替換的內容。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 正在做etani,ejtile的動畫套件
目前国外比较牛的OpenAI、Google、xAI、Anthropic Claude。一个到上限了切另一个。
其中Google推荐用aistudio,一般可以体验最新的模型。
人生如音乐,欢乐且自由
Re: 正在做etani,ejtile的動畫套件
當.etaniButton按下時,.etaniCtrl將增加div.etaniUpdate>(label{update: }+a.etaniUpdateTiles{tiles}+a.etaniUpdateBoard{board})。
當點擊etaniUpdateTiles時,把#etmain中的<defs>的每一個子元素<g id="id">的outerHTML覆蓋至etani_clone中的<defs>的每一個子元素<g id="id">,再把#etmain中的.etdrop中的每一個<use href="#id">的transform屬性
覆蓋至etani_clone中的.etdrop中的每一個<use href="#id">的transform屬性。
如果#etmain中的<defs>的子元素<g id="id">數量大於etani_clone,則把這些多出來的<g id="id">子元素加至etani_clone,之後再把#etmain的.etdrop中對應的<use href="#id">增加至etani_clone的.etdrop中,並在etaniCol中新增etaniItem。
當點擊etaniUpdateBoard時,把#etmain中的.etdrop中的.etboard的outHTML覆蓋至etani_clone中的.etdrop中的.etboard
為.etaniUpdate、.etaniUpdateTiles和.etaniUpdateBoard增加css。
請用繁體中文回答。只需告知所要更改的代碼和位置,以及要替換的內容。代碼的注釋都要是英文。
上次由 ejsoon 在 2025年 10月 22日 14:49,总共编辑 1 次。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 正在做etani,ejtile的動畫套件
代码: 全选
// Global variable to hold the SVG clone and ensure state is maintained
let etani_clone = null;
// Track copy, move, and delete mode states, selected item, and last click time for double-click detection
let isCopyMode = false;
let isMoveMode = false;
let isDeleteMode = false;
let selectedMoveItem = null;
let lastClickTime = 0;
let lastClickedItem = null;
// Add dynamic CSS styles to the document
function addDynamicStyles() {
if (document.getElementById('dynamic-et-styles')) {
return;
}
const styleSheet = document.createElement('style');
styleSheet.id = 'dynamic-et-styles';
styleSheet.textContent = `
/* Styles for etaniouter and button */
.etaniouter {
margin-top: 10px;
padding: 10px;
border: 1px solid #ddd;
background-color: #f5f5f5;
}
/* Styles for etaniinner, etaniCtrl, etaniCol, etaniResult */
.etaniinner {
margin-top: 10px;
}
.etaniCtrl {
margin-bottom: 10px;
clear: both;
padding: 5px;
border: 1px solid #c0c0c0;
text-align: center;
}
/* Styles for etaniContent, etaniMode, etaniAllAppend, etaniValueCtrl */
.etaniContent, .etaniMode, .etaniAllAppend, .etaniValueCtrl {
display: inline-block;
vertical-align: top;
padding: 5px;
border: 1px solid #ccc;
margin: 0 5px 5px 5px;
text-align: left;
}
.etaniCol {
border: 1px solid #aaa;
padding: 5px;
margin-bottom: 10px;
clear: both;
}
/* Styles for etaniResult */
.etaniResult {
text-align: center;
margin-bottom: 10px;
padding: 10px;
border: 1px solid #bbb;
box-sizing: border-box;
}
/* Control and button styles */
.etaniContent a, .etaniValueCtrl a {
display: inline-block;
margin: 0 5px;
text-decoration: none;
padding: 5px 10px;
font-size: 14px;
}
.etaniCenter {
border: 1px solid green;
color: green;
}
.etaniContentHTML {
border: 1px solid #0099ff;
color: #0099ff;
margin-right: 15px;
}
.etaniValueCtrlUp {
border: 1px solid purple;
color: purple;
}
/* Styles for etaniAllAppend buttons */
.etaniAllAppend button {
padding: 5px 10px;
font-size: 16px;
margin: 0 5px;
cursor: pointer;
border: 1px solid #333;
background-color: #fff;
}
/* Custom radio button styles */
.etaniMode span {
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;
}
.etaniMode span.active {
background-color: #008CBA;
color: white;
border-color: #008CBA;
}
/* Result and animation item styles */
.etaniResultDR {
text-align: center;
margin-bottom: 10px;
}
.etaniResultDownload, .etaniResultRename {
display: inline-block;
margin-right: 15px;
text-decoration: none;
padding: 5px 10px;
font-size: 16px;
}
.etaniResultDownload {
border: 1px solid blue;
color: blue;
}
.etaniResultRename {
border: 1px solid brown;
color: brown;
}
.etaniResultImage {
display: block;
max-width: 480px;
width: 100%;
height: auto;
margin: 0 auto 10px auto;
border: 1px solid #000;
box-sizing: border-box;
}
.etaniResultSize {
display: inline-block;
margin-left: 10px;
font-size: 12px;
color: #555;
}
/* etaniItem structure */
.etaniItem {
min-height: 48px;
border: 1px solid #ccc;
box-sizing: border-box;
width: 100%;
margin-bottom: -1px;
background-color: lightyellow;
}
.etaniItemLeft {
float: left;
width: 60px;
min-height: 48px;
padding: 2px 0;
text-align: center;
}
.etaniItemImageOuter {
width: 40px;
height: 40px;
margin: 0 auto;
}
.etaniItemImage {
width: 100%;
height: 100%;
display: block;
}
.etaniItemRight {
margin-left: 60px;
padding: 7px;
min-height: 48px;
background-color: #fff;
}
.tileid {
text-align: center;
font-size: 12px;
word-break: break-all;
padding-top: 2px;
}
/* Animation controls */
.etaniAnimate {
border: 1px solid #999;
padding: 5px;
margin-bottom: 5px;
}
.etaniAnimateAttr {
margin-bottom: 5px;
}
.etaniAnimateName {
display: inline-block;
padding: 2px 5px;
background-color: #555;
color: white;
margin-right: 10px;
font-size: 12px;
}
.etaniAnimateAttr > span {
cursor: pointer;
}
.etaniAnimateAttr > span:not(.etaniAnimateName) {
display: inline-block;
padding: 2px 5px;
font-size: 12px;
margin-right: 7px;
box-sizing: border-box;
border-width: 1px;
border-style: solid;
}
.etaniAnimateAttrAdd {
border-color: #2c8c12;
color: #2c8c12;
}
.etaniAnimateId {
border-color: #742bcc;
color: #742bcc;
}
.etaniAnimateBegin {
border-color: #b6533c;
color: #b6533c;
}
.etaniAnimateOther {
border-color: #1cadca;
color: #1cadca;
}
/* Dropdown menu styles */
.etaniDropdown {
position: absolute;
background-color: #fff;
border: 1px solid #ccc;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
z-index: 1002;
}
.etaniDropdownItem {
padding: 8px 12px;
cursor: pointer;
font-size: 12px;
}
.etaniDropdownItem:hover {
background-color: #f0f0f0;
}
/* Window modal styles */
.etaniWindow {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 15px;
border: 1px solid #ccc;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
z-index: 1003;
width: 300px;
}
.etaniWindow label, .etaniWindow input {
display: block;
margin-bottom: 10px;
}
.etaniWindow button {
margin-right: 10px;
padding: 5px 10px;
}
.etaniExistingIds, .etaniAnimateBeginPlusOuter {
margin-bottom: 10px;
font-size: 12px;
}
.etaniIdItem {
cursor: pointer;
padding: 2px 5px;
margin-right: 5px;
border: 1px solid #ddd;
display: inline-block;
}
.etaniIdItem.selected {
background-color: #008CBA;
color: white;
}
.etaniIdItem.zero-s.selected {
color: white;
}
.etaniIdItem.zero-s {
color: #888;
}
.etaniAnimateDur {
border-color: blue;
color: blue;
}
.etaniAnimateFR {
border-color: #78229f;
color: #78229f;
}
.etaniAnimateValue {
margin-top: 5px;
}
.etaniAVCtrl {
display: inline-block;
vertical-align: top;
margin-right: 5px;
margin-bottom: 3px;
}
.etaniAVCtrl svg {
margin-left: -1px;
margin-top: -1px;
}
.etaniAVCtrl span {
display: inline-block;
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
cursor: pointer;
box-sizing: border-box;
vertical-align: top;
margin-right: 3px;
}
.etaniAVAdd {
background-color: #a7fca7;
border: 1px solid #71c371;
}
.etaniAVDelete {
background-color: #ffcccc;
border: 1px solid #cc3333;
}
.etaniAVDelete.deleting {
background-color: #cc3333;
color: white;
}
.etaniAVCopy {
background-color: #ccccff;
border: 1px solid #6666cc;
}
.etaniAVCopy.copying {
background-color: #6666cc;
color: white;
}
.etaniAVMove {
background-color: #ffcc99;
border: 1px solid #cc9966;
}
.etaniAVMove.moving {
background-color: #cc9966;
color: white;
}
.etaniAVLabel {
font-size: 14px;
margin-right: 5px;
}
.etaniAV {
display: inline-block;
vertical-align: top;
}
.etaniAVItem {
display: inline-block;
height: 24px;
background-color: #ff9933;
border: 1px dashed #00bfff;
margin: 0 5px 3px;
padding: 0 5px;
box-sizing: border-box;
cursor: pointer;
position: relative;
text-align: center;
line-height: 24px;
font-size: 12px;
color: #333;
}
.etaniAVItem.deleting-target, .etaniAVItem.copying-target, .etaniAVItem.moving-target {
background-color: #ff4d4d;
border: 2px solid red;
}
.etaniAVItem.selected-move {
background-color: #66ccff;
border: 2px solid #0066cc;
}
/* Modal/popup styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.modal-content {
position: fixed;
width: 98%;
height: 48%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 1001;
box-sizing: border-box;
}
.modal-content textarea {
width: 100%;
height: calc(100% - 50px);
resize: none;
border: 1px solid #ccc;
font-size: 12px;
box-sizing: border-box;
}
.modal-close {
position: absolute;
top: 0;
right: 10px;
font-size: 48px;
line-height: 1;
cursor: pointer;
color: #333;
}
`;
document.head.appendChild(styleSheet);
}
// Find the first missing letter in the sequence starting from 'a'
function findFirstMissingLetter(existingLetters) {
let letter = 'a';
while (existingLetters.includes(letter)) {
const code = letter.charCodeAt(0);
if (code >= 97 && code < 122) { // lowercase a-y
letter = String.fromCharCode(code + 1);
} else if (code === 122) { // z -> A
letter = 'A';
} else if (code >= 65 && code < 90) { // uppercase A-Y
letter = String.fromCharCode(code + 1);
} else if (code === 90) { // Z -> a (loop back)
letter = 'a';
}
}
return letter;
}
// Extract a specific transform function and its value from a transform string
function extractTransformPart(transformString, type) {
const regex = new RegExp(`(${type})\\(([^)]+)\\)`, 'i');
const match = transformString.match(regex);
if (match) {
return { func: match[1], value: match[2].trim() };
}
return { func: '', value: '' };
}
// Parse transform string to get individual transform values
function parseTransform(transformString) {
const defaultTransform = { translate: '0,0', scale: '1,1', rotate: '0' };
if (!transformString) return defaultTransform;
const transform = {};
const getMatch = (type) => {
const part = extractTransformPart(transformString, type).value;
if (!part) return null;
return part.split(/[,\s]+/).join(',');
};
transform.translate = getMatch('translate') || defaultTransform.translate;
transform.scale = getMatch('scale') || defaultTransform.scale;
if (transform.scale.split(',').length === 1) {
transform.scale += `,${transform.scale}`;
}
transform.rotate = getMatch('rotate') || defaultTransform.rotate;
return transform;
}
// Convert SVG string to a Base64 data URL
function svgToBase64(svgString) {
const base64 = btoa(unescape(encodeURIComponent(svgString)));
return `data:image/svg+xml;base64,${base64}`;
}
// Format byte size into human-readable string
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Update the result section with the current state of etani_clone
function updateEtaniResult() {
if (!etani_clone) return;
const svgString = new XMLSerializer().serializeToString(etani_clone);
const sizeInBytes = new Blob([svgString]).size;
const base64Url = svgToBase64(svgString);
const now = new Date();
const yyyy = now.getFullYear();
const mm = String(now.getMonth() + 1).padStart(2, '0');
const dd = String(now.getDate()).padStart(2, '0');
const hh = String(now.getHours()).padStart(2, '0');
const mi = String(now.getMinutes()).padStart(2, '0');
const ss = String(now.getSeconds()).padStart(2, '0');
const defaultFilename = `ejtileAnimation_${yyyy}${mm}${dd}_${hh}${mi}${ss}.svg`;
const imgElement = document.querySelector('.etaniResultImage');
const downloadElement = document.querySelector('.etaniResultDownload');
const renameElement = document.querySelector('.etaniResultRename');
const sizeElement = document.querySelector('.etaniResultSize');
if (imgElement && downloadElement && renameElement && sizeElement) {
imgElement.src = base64Url;
sizeElement.textContent = `Size: ${formatBytes(sizeInBytes)}`;
downloadElement.href = base64Url;
downloadElement.download = defaultFilename;
renameElement.onclick = (e) => {
e.preventDefault();
let currentDownloadName = downloadElement.download;
let newFilename = prompt("Enter new filename:", currentDownloadName);
if (newFilename) {
if (!newFilename.toLowerCase().endsWith('.svg')) {
newFilename += '.svg';
}
downloadElement.download = newFilename;
alert(`Filename changed to: ${newFilename}`);
}
};
}
}
// Handle the 'Center' button click to reset the .etdrop transform
function handleCenterClick(e) {
e.preventDefault();
if (etani_clone) {
const etdrop = etani_clone.querySelector('.etdrop');
if (etdrop) {
etdrop.setAttribute('transform', 'translate(240,240) scale(1,1)');
updateEtaniResult();
}
}
}
// Handle the HTML popup window
function handleContentHTMLClick(e) {
e.preventDefault();
if (!etani_clone) return;
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
const content = document.createElement('div');
content.className = 'modal-content';
const close = document.createElement('span');
close.className = 'modal-close';
close.innerHTML = '×';
const textarea = document.createElement('textarea');
textarea.value = new XMLSerializer().serializeToString(etani_clone);
const closeModal = () => {
if (document.body.contains(overlay)) document.body.removeChild(overlay);
if (document.body.contains(content)) document.body.removeChild(content);
};
close.onclick = closeModal;
overlay.onclick = closeModal;
content.appendChild(close);
content.appendChild(textarea);
document.body.appendChild(overlay);
document.body.appendChild(content);
}
// Calculate additive transform or opacity value for a given type
function calculateAdditiveValue(type, originalValue, currentBaseValue, scaleValue = '1,1') {
const roundToThreeDecimals = (value) => {
return Math.round(value * 1000) / 1000;
};
if (type === 'rotate') {
const ovAngle = parseFloat(currentBaseValue.trim());
const cvAngle = parseFloat(originalValue.trim());
return roundToThreeDecimals(cvAngle - ovAngle);
} else if (type === 'scale') {
const ovScales = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
const cvScales = originalValue.split(',').map(c => parseFloat(c.trim()));
const v1 = roundToThreeDecimals(cvScales[0] / ovScales[0]);
const v2 = roundToThreeDecimals(cvScales.length > 1 ? (cvScales[1] / ovScales[1]) : (cvScales[0] / ovScales[0]));
return v1 === v2 ? `${v1}` : `${v1},${v2}`;
} else if (type === 'translate') {
const ovCoords = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
const cvCoords = originalValue.split(',').map(c => parseFloat(c.trim()));
const scaleFactors = scaleValue.split(',').map(c => parseFloat(c.trim()));
const v1 = roundToThreeDecimals(cvCoords[0] - (ovCoords[0] * scaleFactors[0]));
const v2 = roundToThreeDecimals(cvCoords[1] - (ovCoords[1] * (scaleFactors.length > 1 ? scaleFactors[1] : scaleFactors[0])));
return `${v1},${v2}`;
} else if (type === 'opacity') {
return originalValue;
}
return '';
}
// Update duration based on the number of values
function updateDuration(animate, etaniAnimate, newValuesLength) {
const currentDur = parseFloat(etaniAnimate.querySelector('.etaniAnimateDur').textContent.replace('dur: ', '').replace('s', ''));
const isIntegerDur = Number.isInteger(currentDur);
const newDur = isIntegerDur ? (newValuesLength > 1 ? newValuesLength - 1 : 0) : currentDur;
if (newDur > 0) {
animate.setAttribute('dur', `${newDur}s`);
} else {
animate.removeAttribute('dur');
}
etaniAnimate.querySelector('.etaniAnimateDur').textContent = `dur: ${newDur}s`;
}
// Handle click event for the '+' button for transform or opacity
function handleAVAddClick(e, useElementId, animateType = 'transform') {
e.preventDefault();
if (!etani_clone) return;
// Reset all modes before adding a new value
resetModes(animateType);
const etaniItemRight = e.currentTarget.closest('.etaniItemRight');
const etaniAV = etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"] .etaniAV`);
const etaniAnimate = etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`);
if (animateType === 'transform') {
// Get the original <use> element from #etmain to read its current transform
const originalUseElement = document.querySelector(`#etmain .etdrop use[href="#${useElementId}"]`);
if (!originalUseElement) return;
const originalTransformString = originalUseElement.getAttribute('transform') || '';
const originalTransforms = parseTransform(originalTransformString);
// Get the corresponding <use> element in the clone
const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useElementId}"]`);
if (!cloneUseElement) return;
// Find all animateTransform elements inside the cloned <use>
const animates = cloneUseElement.querySelectorAll('animateTransform');
animates.forEach(animate => {
const type = animate.getAttribute('type').toLowerCase();
let currentValueString = animate.getAttribute('values') || '';
let newValue = '';
// Get the new value directly from the parsed original transform
if (type === 'translate') {
newValue = originalTransforms.translate;
} else if (type === 'scale') {
newValue = originalTransforms.scale;
} else if (type === 'rotate') {
newValue = originalTransforms.rotate;
}
const newValueString = (currentValueString ? currentValueString + ';' : '') + newValue;
animate.setAttribute('values', newValueString);
updateDuration(animate, etaniAnimate, newValueString.split(';').length);
});
} else if (animateType === 'opacity') {
const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useElementId}"]`);
if (!cloneUseElement) return;
const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
if (!animateOpacity) return;
const currentValueString = animateOpacity.getAttribute('values') || '';
const values = currentValueString ? currentValueString.split(';') : [];
const newValue = values.length > 0 ? values[values.length - 1] : '1';
const newValueString = (currentValueString ? currentValueString + ';' : '') + newValue;
animateOpacity.setAttribute('values', newValueString);
updateDuration(animateOpacity, etaniAnimate, newValueString.split(';').length);
}
// Add new etaniAVItem with appropriate label
if (etaniAV && etaniAnimate) {
const newAVItem = document.createElement('span');
newAVItem.className = 'etaniAVItem';
newAVItem.addEventListener('click', (e) => handleAVItemClick(e, animateType));
const existingItems = etaniAV.querySelectorAll('.etaniAVItem');
if (animateType === 'transform') {
const existingLetters = Array.from(existingItems).map(item => item.textContent);
newAVItem.textContent = findFirstMissingLetter(existingLetters);
} else if (animateType === 'opacity') {
const values = animateType === 'opacity' ?
etaniAnimate.querySelector('animate[attributeName="opacity"]')?.getAttribute('values')?.split(';') || ['1'] :
['1'];
newAVItem.textContent = values[values.length - 1];
}
etaniAV.appendChild(newAVItem);
}
updateEtaniResult();
}
// Reset all mode states for a specific animateType
function resetModes(animateType, excludeMode = null) {
const etaniCol = document.querySelector('.etaniCol');
const avItems = document.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
if (excludeMode !== 'delete') {
isDeleteMode = false;
etaniCol.classList.remove(`deleting-mode-${animateType}`);
document.querySelectorAll(`.etaniAVDelete[data-type="${animateType}"]`).forEach(btn => {
btn.classList.remove('deleting');
btn.title = 'Delete Value';
});
}
if (excludeMode !== 'copy') {
isCopyMode = false;
etaniCol.classList.remove(`copying-mode-${animateType}`);
document.querySelectorAll(`.etaniAVCopy[data-type="${animateType}"]`).forEach(btn => {
btn.classList.remove('copying');
btn.title = 'Copy Value';
});
}
if (excludeMode !== 'move') {
isMoveMode = false;
selectedMoveItem = null;
etaniCol.classList.remove(`moving-mode-${animateType}`);
document.querySelectorAll(`.etaniAVMove[data-type="${animateType}"]`).forEach(btn => {
btn.classList.remove('moving');
btn.title = 'Move Value';
});
}
avItems.forEach(item => {
item.classList.remove('deleting-target', 'copying-target', 'moving-target', 'selected-move');
});
}
// Handle click event for the '-' button to toggle deletion mode
function handleAVDeleteToggle(e, animateType = 'transform') {
e.preventDefault();
const deleteButton = e.currentTarget;
resetModes(animateType, 'delete');
isDeleteMode = !isDeleteMode;
deleteButton.classList.toggle('deleting', isDeleteMode);
const etaniItemRight = deleteButton.closest('.etaniItemRight');
const etaniCol = deleteButton.closest('.etaniCol');
const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
avItems.forEach(item => {
item.classList.toggle('deleting-target', isDeleteMode);
});
etaniCol.classList.toggle(`deleting-mode-${animateType}`, isDeleteMode);
if (isDeleteMode) {
deleteButton.title = "Click value item to delete (Click again to cancel)";
} else {
deleteButton.title = "Delete Value";
avItems.forEach(item => {
item.classList.remove('deleting-target');
});
}
}
// Handle click event for the 'Copy' button to toggle copy mode
function handleAVCopyToggle(e, animateType) {
e.preventDefault();
const copyButton = e.currentTarget;
resetModes(animateType, 'copy');
isCopyMode = !isCopyMode;
copyButton.classList.toggle('copying', isCopyMode);
const etaniItemRight = copyButton.closest('.etaniItemRight');
const etaniCol = copyButton.closest('.etaniCol');
const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
avItems.forEach(item => {
item.classList.toggle('copying-target', isCopyMode);
});
etaniCol.classList.toggle(`copying-mode-${animateType}`, isCopyMode);
if (isCopyMode) {
copyButton.title = "Click value item to copy (Click again to cancel)";
} else {
copyButton.title = "Copy Value";
avItems.forEach(item => {
item.classList.remove('copying-target');
});
}
}
// Handle click event for the 'Move' button to toggle move mode
function handleAVMoveToggle(e, animateType) {
e.preventDefault();
const moveButton = e.currentTarget;
resetModes(animateType, 'move');
isMoveMode = !isMoveMode;
moveButton.classList.toggle('moving', isMoveMode);
const etaniItemRight = moveButton.closest('.etaniItemRight');
const etaniCol = moveButton.closest('.etaniCol');
const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
avItems.forEach(item => {
item.classList.toggle('moving-target', isMoveMode);
});
etaniCol.classList.toggle(`moving-mode-${animateType}`, isMoveMode);
if (isMoveMode) {
moveButton.title = "Click value item to select, then click another to move (Click again to cancel)";
} else {
moveButton.title = "Move Value";
selectedMoveItem = null;
avItems.forEach(item => {
item.classList.remove('moving-target');
item.classList.remove('selected-move');
});
}
}
// Handle click event for an .etaniAVItem in deletion, copy, or move mode
function handleAVItemClick(e, animateType = 'transform') {
const item = e.currentTarget;
const itemIndex = Array.from(item.parentNode.children).indexOf(item);
const etaniItemRight = item.closest('.etaniItemRight');
const etaniCol = item.closest('.etaniCol');
const useId = etaniItemRight.closest('.etaniItem').querySelector('.tileid').textContent;
const currentTime = Date.now();
const isDoubleClick = (item === lastClickedItem && (currentTime - lastClickTime) < 2000);
lastClickTime = currentTime;
lastClickedItem = item;
if (etaniCol.classList.contains(`moving-mode-${animateType}`)) {
const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
if (!selectedMoveItem) {
selectedMoveItem = item;
item.classList.add('selected-move');
return;
} else if (selectedMoveItem === item) {
selectedMoveItem.classList.remove('selected-move');
selectedMoveItem = null;
return;
} else {
const targetIndex = itemIndex;
const sourceIndex = Array.from(item.parentNode.children).indexOf(selectedMoveItem);
const parent = item.parentNode;
if (sourceIndex < targetIndex) {
if (item.nextSibling) {
parent.insertBefore(selectedMoveItem, item.nextSibling);
} else {
parent.appendChild(selectedMoveItem);
}
} else {
parent.insertBefore(selectedMoveItem, item);
}
if (animateType === 'transform') {
const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return;
const allAnimates = cloneUseElement.querySelectorAll('animateTransform');
allAnimates.forEach(animate => {
const values = animate.getAttribute('values').split(';');
const valueToMove = values[sourceIndex];
values.splice(sourceIndex, 1);
if (sourceIndex < targetIndex) {
values.splice(targetIndex, 0, valueToMove);
} else {
values.splice(targetIndex, 0, valueToMove);
}
animate.setAttribute('values', values.join(';'));
});
} else if (animateType === 'opacity') {
const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return;
const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
if (!animateOpacity) return;
const values = animateOpacity.getAttribute('values').split(';');
const valueToMove = values[sourceIndex];
values.splice(sourceIndex, 1);
if (sourceIndex < targetIndex) {
values.splice(targetIndex, 0, valueToMove);
} else {
values.splice(targetIndex, 0, valueToMove);
}
animateOpacity.setAttribute('values', values.join(';'));
}
selectedMoveItem.classList.remove('selected-move');
selectedMoveItem = null;
updateEtaniResult();
return;
}
}
if (etaniCol.classList.contains(`copying-mode-${animateType}`)) {
const etaniAV = item.parentNode;
const newAVItem = document.createElement('span');
newAVItem.className = 'etaniAVItem';
newAVItem.textContent = item.textContent;
newAVItem.addEventListener('click', (e) => handleAVItemClick(e, animateType));
etaniAV.insertBefore(newAVItem, item.nextSibling);
if (animateType === 'transform') {
const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return;
const allAnimates = cloneUseElement.querySelectorAll('animateTransform');
allAnimates.forEach(animate => {
const values = animate.getAttribute('values').split(';');
if (itemIndex < values.length) {
values.splice(itemIndex + 1, 0, values[itemIndex]);
animate.setAttribute('values', values.join(';'));
updateDuration(animate, etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`), values.length);
}
});
} else if (animateType === 'opacity') {
const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return;
const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
if (!animateOpacity) return;
const values = animateOpacity.getAttribute('values').split(';');
if (itemIndex < values.length) {
values.splice(itemIndex + 1, 0, values[itemIndex]);
animateOpacity.setAttribute('values', values.join(';'));
updateDuration(animateOpacity, etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`), values.length);
}
}
updateEtaniResult();
return;
}
if (etaniCol.classList.contains(`deleting-mode-${animateType}`)) {
const deleteButton = etaniItemRight.querySelector(`.etaniAVDelete[data-type="${animateType}"]`);
const etaniAnimate = etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`);
if (animateType === 'transform') {
const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return;
const allAnimates = cloneUseElement.querySelectorAll('animateTransform');
let newValuesLength = 0;
allAnimates.forEach((animate, index) => {
const values = animate.getAttribute('values').split(';');
if (itemIndex < values.length) {
values.splice(itemIndex, 1);
newValuesLength = values.length; // All animations will have the same new length
if (newValuesLength > 0) {
animate.setAttribute('values', values.join(';'));
// Only update duration on the first animate to avoid multiple calls
if (index === 0) {
updateDuration(animate, etaniAnimate, newValuesLength);
}
}
}
});
// If all values were removed, clean up
if (newValuesLength === 0 && allAnimates.length > 0) {
allAnimates.forEach(anim => anim.remove());
etaniAnimate.remove();
}
} else if (animateType === 'opacity') {
const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return;
const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
if (!animateOpacity) return;
const values = animateOpacity.getAttribute('values').split(';');
if (itemIndex < values.length) {
values.splice(itemIndex, 1);
if (values.length === 0) {
// Remove the opacity animation
animateOpacity.remove();
etaniAnimate.remove();
} else {
animateOpacity.setAttribute('values', values.join(';'));
updateDuration(animateOpacity, etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`), values.length);
}
}
}
item.remove();
const remainingItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
if (remainingItems.length === 0) {
// The .etaniAnimate element (containing the deleteButton) was just removed
// because it was the last item.
// We cannot .click() the button, as it's no longer in the DOM.
// Instead, we must manually reset the delete mode globally.
resetModes(animateType);
} else {
// If items remain, just re-apply the 'deleting-target' class to them.
remainingItems.forEach(item => {
item.classList.add('deleting-target');
});
}
updateEtaniResult();
} else if (animateType === 'opacity' && !etaniCol.classList.contains(`deleting-mode-${animateType}`) && !etaniCol.classList.contains(`copying-mode-${animateType}`)) {
const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return;
const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
if (!animateOpacity) return;
const values = animateOpacity.getAttribute('values').split(';');
if (itemIndex >= values.length) return;
if (isDoubleClick) {
const newValue = prompt("Enter opacity value (0 to 1):", values[itemIndex]);
if (newValue !== null && !isNaN(newValue) && newValue >= 0 && newValue <= 1) {
values[itemIndex] = newValue;
item.textContent = newValue;
animateOpacity.setAttribute('values', values.join(';'));
updateEtaniResult();
}
} else {
values[itemIndex] = values[itemIndex] === '0' ? '1' : '0';
item.textContent = values[itemIndex];
animateOpacity.setAttribute('values', values.join(';'));
updateEtaniResult();
}
}
}
// Handle click event for the 'Transform' button
function handleAllAppendTransformClick() {
if (!etani_clone) return;
const etdropUses = document.querySelectorAll('#etmain .etdrop use');
const etaniItemRights = document.querySelectorAll('.etaniItemRight');
const isRepeat = document.querySelector('.etaniModeRepeat.active');
etdropUses.forEach((originalUseElement, i) => {
const useId = originalUseElement.getAttribute('href').substring(1);
const itemRight = etaniItemRights[i];
if (!itemRight) return;
// Check if transform animation already exists
if (itemRight.querySelector('.etaniAnimate[data-type="transform"]')) return;
let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return;
// Get the original transform values from the #etmain SVG
const originalTransformString = originalUseElement.getAttribute('transform') || '';
const originalTransforms = parseTransform(originalTransformString);
const baseAnimate = (type, initialValue) => {
const animate = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
animate.setAttribute('attributeName', 'transform');
animate.setAttribute('attributeType', 'XML');
animate.setAttribute('type', type);
animate.setAttribute('values', initialValue);
animate.setAttribute('fill', isRepeat ? 'remove' : 'freeze');
if (isRepeat) animate.setAttribute('repeatCount', 'indefinite');
// Add additive="sum" for scale and rotate, but not for translate
if (type === 'scale' || type === 'rotate') {
animate.setAttribute('additive', 'sum');
}
return animate;
};
// Add all three animations directly to the <use> element
// Order: translate, scale, rotate
cloneUseElement.appendChild(baseAnimate('translate', originalTransforms.translate));
cloneUseElement.appendChild(baseAnimate('scale', originalTransforms.scale));
cloneUseElement.appendChild(baseAnimate('rotate', originalTransforms.rotate));
// --- Start of HTML control creation ---
const etaniAnimate = document.createElement('div');
etaniAnimate.className = 'etaniAnimate';
etaniAnimate.setAttribute('data-type', 'transform');
const nameSpan = document.createElement('span');
nameSpan.className = 'etaniAnimateName';
nameSpan.textContent = 'transform';
const durSpan = document.createElement('span');
durSpan.className = 'etaniAnimateDur';
durSpan.textContent = 'dur: 0s';
durSpan.addEventListener('click', () => {
const currentDur = parseFloat(durSpan.textContent.replace('dur: ', '').replace('s', ''));
const newDur = prompt('Enter duration in seconds:', currentDur);
if (newDur !== null && !isNaN(newDur) && newDur >= 0) {
// Find animates inside the <use> element
const animates = etani_clone.querySelectorAll(`.etdrop use[href="#${useId}"] animateTransform`);
animates.forEach(animate => {
if (newDur > 0) {
animate.setAttribute('dur', `${newDur}s`);
if (isRepeat) {
animate.removeAttribute('fill');
animate.setAttribute('repeatCount', 'indefinite');
} else {
animate.removeAttribute('repeatCount');
animate.setAttribute('fill', 'freeze');
}
} else {
animate.removeAttribute('dur');
animate.removeAttribute('fill');
animate.removeAttribute('repeatCount');
}
});
durSpan.textContent = `dur: ${newDur}s`;
updateEtaniResult();
}
});
const etaniAnimateAttr = document.createElement('div');
etaniAnimateAttr.className = 'etaniAnimateAttr';
etaniAnimateAttr.appendChild(nameSpan);
etaniAnimateAttr.appendChild(durSpan);
etaniAnimate.appendChild(etaniAnimateAttr);
const frSpan = document.createElement('span');
frSpan.className = 'etaniAnimateFR';
frSpan.textContent = document.querySelector('.etaniModeMixed.active') ? 'freeze' : 'repeat';
frSpan.style.display = document.querySelector('.etaniModeMixed.active') ? 'inline-block' : 'none';
frSpan.addEventListener('click', () => {
const currentValue = frSpan.textContent;
const newValue = currentValue === 'freeze' ? 'repeat' : 'freeze';
frSpan.textContent = newValue;
const useId = itemRight.closest('.etaniItem').querySelector('.tileid').textContent;
// Find animates inside the <use> element
const animates = etani_clone.querySelectorAll(`.etdrop use[href="#${useId}"] animateTransform`);
animates.forEach(animate => {
if (animate.hasAttribute('fill') || animate.hasAttribute('repeatCount')) {
if (newValue === 'repeat') {
animate.removeAttribute('fill');
animate.setAttribute('repeatCount', 'indefinite');
} else {
animate.removeAttribute('repeatCount');
animate.setAttribute('fill', 'freeze');
}
}
});
updateEtaniResult();
});
etaniAnimateAttr.appendChild(frSpan);
// Add the attribute add button
const animateType = 'transform';
const attrAddSpan = document.createElement('span');
attrAddSpan.className = 'etaniAnimateAttrAdd';
attrAddSpan.textContent = '+';
attrAddSpan.addEventListener('click', (e) => showDropdown(e, animateType, useId));
etaniAnimateAttr.appendChild(attrAddSpan);
// Handle clicks on existing attribute spans
etaniAnimateAttr.addEventListener('click', (e) => {
if (e.target.classList.contains('etaniAnimateId')) {
editAttribute(e.target, animateType, useId, 'id');
} else if (e.target.classList.contains('etaniAnimateBegin')) {
editAttribute(e.target, animateType, useId, 'begin');
} else if (e.target.classList.contains('etaniAnimateOther')) {
editAttribute(e.target, animateType, useId, 'other', e.target.textContent.split('=')[0]);
}
});
const valueDiv = document.createElement('div');
valueDiv.className = 'etaniAnimateValue';
const avCtrlDiv = createControlButtons('transform', useId);
const avLabelSpan = document.createElement('span');
avLabelSpan.className = 'etaniAVLabel';
avLabelSpan.textContent = 'values : ';
const avDiv = document.createElement('div');
avDiv.className = 'etaniAV';
const avItemSpan = document.createElement('span');
avItemSpan.className = 'etaniAVItem';
avItemSpan.textContent = 'a';
avItemSpan.addEventListener('click', (e) => handleAVItemClick(e, 'transform'));
avDiv.appendChild(avItemSpan);
valueDiv.appendChild(avCtrlDiv);
valueDiv.appendChild(avLabelSpan);
valueDiv.appendChild(avDiv);
etaniAnimate.appendChild(valueDiv);
itemRight.appendChild(etaniAnimate);
});
updateEtaniResult();
}
// Handle click event for the 'Opacity' button
function handleAllAppendOpacityClick() {
if (!etani_clone) return;
const etdropUses = document.querySelectorAll('#etmain .etdrop use');
const etaniItemRights = document.querySelectorAll('.etaniItemRight');
const isRepeat = document.querySelector('.etaniModeRepeat.active');
etdropUses.forEach((originalUseElement, i) => {
const useId = originalUseElement.getAttribute('href').substring(1);
const itemRight = etaniItemRights[i];
if (!itemRight) return;
if (itemRight.querySelector('.etaniAnimate[data-type="opacity"]')) return;
let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return;
const animateOpacity = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
animateOpacity.setAttribute('attributeName', 'opacity');
animateOpacity.setAttribute('values', '1');
if (isRepeat) {
animateOpacity.setAttribute('repeatCount', 'indefinite');
} else {
animateOpacity.setAttribute('fill', 'freeze');
}
cloneUseElement.appendChild(animateOpacity);
const etaniAnimate = document.createElement('div');
etaniAnimate.className = 'etaniAnimate';
etaniAnimate.setAttribute('data-type', 'opacity');
const nameSpan = document.createElement('span');
nameSpan.className = 'etaniAnimateName';
nameSpan.textContent = 'opacity';
const durSpan = document.createElement('span');
durSpan.className = 'etaniAnimateDur';
durSpan.textContent = 'dur: 0s';
durSpan.addEventListener('click', () => {
const currentDur = parseFloat(durSpan.textContent.replace('dur: ', '').replace('s', ''));
const newDur = prompt('Enter duration in seconds:', currentDur);
if (newDur !== null && !isNaN(newDur) && newDur >= 0) {
const animateOpacity = etani_clone.querySelector(`.etdrop use[href="#${useId}"] animate[attributeName="opacity"]`);
if (animateOpacity) {
if (newDur > 0) {
animateOpacity.setAttribute('dur', `${newDur}s`);
if (isRepeat) {
animateOpacity.removeAttribute('fill');
animateOpacity.setAttribute('repeatCount', 'indefinite');
} else {
animateOpacity.removeAttribute('repeatCount');
animateOpacity.setAttribute('fill', 'freeze');
}
} else {
animateOpacity.removeAttribute('dur');
animateOpacity.removeAttribute('fill');
animateOpacity.removeAttribute('repeatCount');
}
}
durSpan.textContent = `dur: ${newDur}s`;
updateEtaniResult();
}
});
const etaniAnimateAttr = document.createElement('div');
etaniAnimateAttr.className = 'etaniAnimateAttr';
etaniAnimateAttr.appendChild(nameSpan);
etaniAnimateAttr.appendChild(durSpan);
etaniAnimate.appendChild(etaniAnimateAttr);
const frSpan = document.createElement('span');
frSpan.className = 'etaniAnimateFR';
frSpan.textContent = document.querySelector('.etaniModeMixed.active') ? 'freeze' : 'repeat';
frSpan.style.display = document.querySelector('.etaniModeMixed.active') ? 'inline-block' : 'none';
frSpan.addEventListener('click', () => {
const currentValue = frSpan.textContent;
const newValue = currentValue === 'freeze' ? 'repeat' : 'freeze';
frSpan.textContent = newValue;
const useId = itemRight.closest('.etaniItem').querySelector('.tileid').textContent;
const animateOpacity = etani_clone.querySelector(`.etdrop use[href="#${useId}"] animate[attributeName="opacity"]`);
if (animateOpacity &&
(animateOpacity.hasAttribute('fill') || animateOpacity.hasAttribute('repeatCount'))) {
if (newValue === 'repeat') {
animateOpacity.removeAttribute('fill');
animateOpacity.setAttribute('repeatCount', 'indefinite');
} else {
animateOpacity.removeAttribute('repeatCount');
animateOpacity.setAttribute('fill', 'freeze');
}
}
updateEtaniResult();
});
etaniAnimateAttr.appendChild(frSpan);
// Add the attribute add button
const animateType = 'opacity';
const attrAddSpan = document.createElement('span');
attrAddSpan.className = 'etaniAnimateAttrAdd';
attrAddSpan.textContent = '+';
attrAddSpan.addEventListener('click', (e) => showDropdown(e, animateType, useId));
etaniAnimateAttr.appendChild(attrAddSpan);
// Handle clicks on existing attribute spans
etaniAnimateAttr.addEventListener('click', (e) => {
if (e.target.classList.contains('etaniAnimateId')) {
editAttribute(e.target, animateType, useId, 'id');
} else if (e.target.classList.contains('etaniAnimateBegin')) {
editAttribute(e.target, animateType, useId, 'begin');
} else if (e.target.classList.contains('etaniAnimateOther')) {
editAttribute(e.target, animateType, useId, 'other', e.target.textContent.split('=')[0]);
}
});
const valueDiv = document.createElement('div');
valueDiv.className = 'etaniAnimateValue';
const avCtrlDiv = createControlButtons('opacity', useId);
const avLabelSpan = document.createElement('span');
avLabelSpan.className = 'etaniAVLabel';
avLabelSpan.textContent = 'values : ';
const avDiv = document.createElement('div');
avDiv.className = 'etaniAV';
const avItemSpan = document.createElement('span');
avItemSpan.className = 'etaniAVItem';
avItemSpan.textContent = '1';
avItemSpan.addEventListener('click', (e) => handleAVItemClick(e, 'opacity'));
avDiv.appendChild(avItemSpan);
valueDiv.appendChild(avCtrlDiv);
valueDiv.appendChild(avLabelSpan);
valueDiv.appendChild(avDiv);
etaniAnimate.appendChild(valueDiv);
itemRight.appendChild(etaniAnimate);
});
updateEtaniResult();
}
// Handle click event for the 'Fill up values' button
function handleValueCtrlUpClick() {
if (!etani_clone) return;
const allAnimates = etani_clone.querySelectorAll('animateTransform, animate[attributeName="opacity"]');
let maxValuesLength = 0;
allAnimates.forEach(animate => {
const values = animate.getAttribute('values')?.split(';') || [];
maxValuesLength = Math.max(maxValuesLength, values.length);
});
allAnimates.forEach(animate => {
const values = animate.getAttribute('values')?.split(';') || [];
if (values.length < maxValuesLength && values.length > 0) {
const lastValue = values[values.length - 1];
while (values.length < maxValuesLength) {
values.push(lastValue);
}
animate.setAttribute('values', values.join(';'));
const useId = animate.closest('use')?.getAttribute('href')?.substring(1) ||
animate.closest('g[data-use-href]')?.getAttribute('data-use-href')?.substring(1);
if (useId) {
const etaniAnimate = document.querySelector(`.etaniItem:has(.tileid:where(:text("${useId}"))) .etaniAnimate[data-type="${animate.getAttribute('attributeName') === 'opacity' ? 'opacity' : 'transform'}"]`);
if (etaniAnimate) {
updateDuration(animate, etaniAnimate, values.length);
}
}
}
});
const etaniItemRights = document.querySelectorAll('.etaniItemRight');
etaniItemRights.forEach(itemRight => {
const useId = itemRight.closest('.etaniItem').querySelector('.tileid').textContent;
['transform', 'opacity'].forEach(animateType => {
const etaniAV = itemRight.querySelector(`.etaniAnimate[data-type="${animateType}"] .etaniAV`);
if (!etaniAV) return;
const currentItems = etaniAV.querySelectorAll('.etaniAVItem');
const currentValuesLength = currentItems.length;
if (currentValuesLength >= maxValuesLength) return;
const lastItem = currentItems[currentItems.length - 1];
let lastValue = lastItem.textContent;
for (let i = currentValuesLength; i < maxValuesLength; i++) {
const newAVItem = document.createElement('span');
newAVItem.className = 'etaniAVItem';
newAVItem.addEventListener('click', (e) => handleAVItemClick(e, animateType));
if (animateType === 'transform') {
newAVItem.textContent = lastValue; // Use the last letter instead of incrementing
} else if (animateType === 'opacity') {
newAVItem.textContent = lastValue;
}
etaniAV.appendChild(newAVItem);
}
});
});
updateEtaniResult();
}
// Handle setting mode switch (Repeat/Freeze)
function handleModeChange(mode) {
if (!etani_clone) return;
const animates = etani_clone.querySelectorAll('animateTransform, animate[attributeName="opacity"]');
const isRepeat = mode === 'repeat';
const isFreeze = mode === 'freeze';
const isMixed = mode === 'mixed';
if (!isMixed) {
animates.forEach(animate => {
if (animate.hasAttribute('fill') || animate.hasAttribute('repeatCount')) {
if (isRepeat) {
animate.removeAttribute('fill');
animate.setAttribute('repeatCount', 'indefinite');
} else if (isFreeze) {
animate.removeAttribute('repeatCount');
animate.setAttribute('fill', 'freeze');
}
}
});
}
document.querySelectorAll('.etaniMode span').forEach(span => {
span.classList.remove('active');
});
document.querySelector(`.etaniMode${mode.charAt(0).toUpperCase() + mode.slice(1)}`).classList.add('active');
const frElements = document.querySelectorAll('.etaniAnimateFR');
frElements.forEach(fr => {
fr.style.display = isMixed ? 'inline-block' : 'none';
});
updateEtaniResult();
}
// Create the internal animation control structure
function createEtaniInner(etaniouter) {
const originalSvg = document.getElementById('etmain');
if (!originalSvg) {
console.error('SVG with ID "etmain" not found.');
return;
}
etani_clone = originalSvg.cloneNode(true);
const etwaitElement = etani_clone.querySelector('.etwait');
if (etwaitElement) {
etwaitElement.remove();
}
etani_clone.id = 'etmainani';
const etaniinner = document.createElement('div');
etaniinner.className = 'etaniinner';
etaniinner.id = 'etaniinner';
const etaniCtrl = document.createElement('div');
etaniCtrl.className = 'etaniCtrl';
const etaniContent = document.createElement('div');
etaniContent.className = 'etaniContent';
const contentHTMLLink = document.createElement('a');
contentHTMLLink.className = 'etaniContentHTML';
contentHTMLLink.textContent = 'Show HTML';
contentHTMLLink.href = 'javascript:;';
contentHTMLLink.addEventListener('click', handleContentHTMLClick);
etaniContent.appendChild(contentHTMLLink);
const centerLink = document.createElement('a');
centerLink.className = 'etaniCenter';
centerLink.textContent = 'Center';
centerLink.href = 'javascript:;';
centerLink.addEventListener('click', handleCenterClick);
etaniContent.appendChild(centerLink);
etaniCtrl.appendChild(etaniContent);
const etaniMode = document.createElement('div');
etaniMode.className = 'etaniMode';
const repeatRadio = document.createElement('span');
repeatRadio.className = 'etaniModeRepeat active';
repeatRadio.textContent = 'Repeat';
repeatRadio.setAttribute('data-mode', 'repeat');
repeatRadio.addEventListener('click', () => handleModeChange('repeat'));
const freezeRadio = document.createElement('span');
freezeRadio.className = 'etaniModeFreeze';
freezeRadio.textContent = 'Freeze';
freezeRadio.setAttribute('data-mode', 'freeze');
freezeRadio.addEventListener('click', () => handleModeChange('freeze'));
const mixedRadio = document.createElement('span');
mixedRadio.className = 'etaniModeMixed';
mixedRadio.textContent = 'Mixed';
mixedRadio.setAttribute('data-mode', 'mixed');
mixedRadio.addEventListener('click', () => handleModeChange('mixed'));
etaniMode.appendChild(repeatRadio);
etaniMode.appendChild(freezeRadio);
etaniMode.appendChild(mixedRadio);
etaniCtrl.appendChild(etaniMode);
const etaniAllAppend = document.createElement('div');
etaniAllAppend.className = 'etaniAllAppend';
const transformButton = document.createElement('button');
transformButton.className = 'etaniAllAppendTransform';
transformButton.textContent = 'transform';
transformButton.addEventListener('click', handleAllAppendTransformClick);
const opacityButton = document.createElement('button');
opacityButton.className = 'etaniAllAppendOpacity';
opacityButton.textContent = 'opacity';
opacityButton.addEventListener('click', handleAllAppendOpacityClick);
etaniAllAppend.appendChild(transformButton);
etaniAllAppend.appendChild(opacityButton);
etaniCtrl.appendChild(etaniAllAppend);
const etaniValueCtrl = document.createElement('div');
etaniValueCtrl.className = 'etaniValueCtrl';
const valueCtrlUpLink = document.createElement('a');
valueCtrlUpLink.className = 'etaniValueCtrlUp';
valueCtrlUpLink.textContent = 'fill up values';
valueCtrlUpLink.href = 'javascript:;';
valueCtrlUpLink.addEventListener('click', handleValueCtrlUpClick);
etaniValueCtrl.appendChild(valueCtrlUpLink);
etaniCtrl.appendChild(etaniValueCtrl);
const etaniCol = document.createElement('div');
etaniCol.className = 'etaniCol';
const etaniResult = document.createElement('div');
etaniResult.className = 'etaniResult';
const resultImg = document.createElement('img');
resultImg.className = 'etaniResultImage';
resultImg.alt = 'Rendered Ejtile Animation SVG';
const resultDRDiv = document.createElement('div');
resultDRDiv.className = 'etaniResultDR';
const downloadLink = document.createElement('a');
downloadLink.className = 'etaniResultDownload';
downloadLink.textContent = 'Download SVG';
downloadLink.href = 'javascript:;';
const renameLink = document.createElement('a');
renameLink.className = 'etaniResultRename';
renameLink.textContent = 'Rename File';
renameLink.href = 'javascript:;';
const sizeSpan = document.createElement('span');
sizeSpan.className = 'etaniResultSize';
resultDRDiv.appendChild(downloadLink);
resultDRDiv.appendChild(renameLink);
etaniResult.appendChild(resultImg);
etaniResult.appendChild(resultDRDiv);
etaniResult.appendChild(sizeSpan);
etaniinner.appendChild(etaniCtrl);
etaniinner.appendChild(etaniCol);
etaniinner.appendChild(etaniResult);
etaniouter.appendChild(etaniinner);
const etdropUses = document.querySelectorAll('.etdrop use');
const etwaitGroups = document.querySelectorAll('.etwait g');
etdropUses.forEach((useElement, i) => {
const tileid = useElement.getAttribute('href').substring(1);
let targetUse = null;
for (const group of etwaitGroups) {
const useInGroup = group.querySelector(`use[href="#${tileid}"]`);
if (useInGroup) {
targetUse = useInGroup;
break;
}
}
let etwaittransform = '', etwaitfill = '', etwaitstroke = '', etwaitstrokeWidth = '';
if (targetUse) {
etwaittransform = targetUse.getAttribute('transform') || '';
etwaitfill = targetUse.getAttribute('fill') || '';
etwaitstroke = targetUse.getAttribute('stroke') || '';
etwaitstrokeWidth = targetUse.getAttribute('stroke-width') || '';
const scaleMatch = etwaittransform.match(/scale\(([^)]+)\)/);
const rotateMatch = etwaittransform.match(/rotate\(([^)]+)\)/);
const scalePart = scaleMatch ? scaleMatch[0] : '';
const rotatePart = rotateMatch ? rotateMatch[0] : '';
etwaittransform = `translate(20,20) ${scalePart} ${rotatePart}`.trim().replace(/\s+/g, ' ');
}
const originalTile = document.querySelector(`defs g#${tileid}`);
const etaniItem = document.createElement('div');
etaniItem.className = 'etaniItem';
etaniItem.setAttribute('data-use-id', tileid);
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.appendChild(tileclone);
// Convert SVG to base64 and use img element
const svgString = new XMLSerializer().serializeToString(svgWrapper);
const base64Url = svgToBase64(svgString);
const imageOuter = document.createElement('div');
imageOuter.className = 'etaniItemImageOuter';
const img = document.createElement('img');
img.className = 'etaniItemImage';
img.src = base64Url;
img.alt = tileid;
imageOuter.appendChild(img);
etaniItemLeft.appendChild(imageOuter);
}
const tileidDiv = document.createElement('div');
tileidDiv.className = 'tileid';
tileidDiv.textContent = tileid;
etaniItemLeft.appendChild(tileidDiv);
etaniItem.appendChild(etaniItemLeft);
etaniItem.appendChild(etaniItemRight);
etaniCol.appendChild(etaniItem);
});
updateEtaniResult();
}
// Toggle the visibility and content of the animation control panel
function toggleAnimation(event) {
const etanibutton = event.currentTarget;
const etaniouter = etanibutton.parentNode;
const etaniinner = document.getElementById('etaniinner');
if (etanibutton.textContent === 'Animate it') {
etanibutton.textContent = 'Close Ejtile Ani';
createEtaniInner(etaniouter);
} else if (etanibutton.textContent === 'Close Ejtile Ani') {
etanibutton.textContent = 'Animate it';
if (etaniinner) {
etaniinner.remove();
}
etani_clone = null;
isCopyMode = false;
isMoveMode = false;
isDeleteMode = false;
selectedMoveItem = null;
lastClickTime = 0;
lastClickedItem = null;
}
}
// Create control buttons when animate is appending
function createControlButtons(animateType, useId) {
const avCtrlDiv = document.createElement('div');
avCtrlDiv.className = 'etaniAVCtrl';
const buttons = [
{
className: 'etaniAVAdd',
title: `Add ${animateType.charAt(0).toUpperCase() + animateType.slice(1)} Value`,
svg: {
lines: [
{ x1: '12', y1: '5', x2: '12', y2: '19' },
{ x1: '5', y1: '12', x2: '19', y2: '12' }
]
},
handler: (e) => handleAVAddClick(e, useId, animateType)
},
{
className: 'etaniAVDelete',
title: 'Delete Value',
svg: {
lines: [
{ x1: '5', y1: '12', x2: '19', y2: '12' }
]
},
handler: (e) => handleAVDeleteToggle(e, animateType)
},
{
className: 'etaniAVCopy',
title: 'Copy Value',
svg: {
rect: { x: '9', y: '9', width: '13', height: '13', rx: '2', ry: '2' },
path: { d: 'M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1' }
},
handler: (e) => handleAVCopyToggle(e, animateType)
},
{
className: 'etaniAVMove',
title: 'Move Value',
svg: {
path: { d: 'M 7,4 L 3,8 L 7,12 M 3,8 L 18,8 M 17,12 L 21,16 L 17,20 M 21,16 L 6,16' }
},
handler: (e) => handleAVMoveToggle(e, animateType)
}
];
buttons.forEach(button => {
const span = document.createElement('span');
span.className = button.className;
span.title = button.title;
span.setAttribute('data-type', animateType);
span.addEventListener('click', button.handler);
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '24');
svg.setAttribute('height', '24');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('fill', 'none');
svg.setAttribute('stroke', 'currentColor');
svg.setAttribute('stroke-width', '1');
svg.setAttribute('stroke-linecap', 'round');
svg.setAttribute('stroke-linejoin', 'round');
if (button.svg.lines) {
button.svg.lines.forEach(line => {
const lineElement = document.createElementNS('http://www.w3.org/2000/svg', 'line');
Object.entries(line).forEach(([key, value]) => lineElement.setAttribute(key, value));
svg.appendChild(lineElement);
});
}
if (button.svg.rect) {
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
Object.entries(button.svg.rect).forEach(([key, value]) => rect.setAttribute(key, value));
svg.appendChild(rect);
}
if (button.svg.path) {
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
Object.entries(button.svg.path).forEach(([key, value]) => path.setAttribute(key, value));
svg.appendChild(path);
}
span.appendChild(svg);
avCtrlDiv.appendChild(span);
});
return avCtrlDiv;
}
// Show dropdown menu for adding attributes
function showDropdown(e, animateType, useId) {
const attrAddSpan = e.target;
const existingDropdown = attrAddSpan.parentNode.querySelector('.etaniDropdown');
if (existingDropdown) {
// Toggle: if already open, remove it
existingDropdown.remove();
return;
}
const dropdown = document.createElement('div');
dropdown.className = 'etaniDropdown';
// Dynamically determine items based on existing attributes
const attrParent = attrAddSpan.parentNode;
const hasId = attrParent.querySelector('.etaniAnimateId');
const hasBegin = attrParent.querySelector('.etaniAnimateBegin');
const items = [];
if (!hasId) items.push('id');
if (!hasBegin) items.push('begin');
items.push('other');
items.forEach(item => {
const div = document.createElement('div');
div.className = 'etaniDropdownItem';
div.textContent = item;
div.addEventListener('click', () => {
showWindow(item, animateType, useId);
dropdown.remove(); // Remove after selecting item
});
dropdown.appendChild(div);
});
attrAddSpan.parentNode.style.position = 'relative'; // Ensure parent is relative for absolute positioning
attrAddSpan.parentNode.appendChild(dropdown);
dropdown.style.left = `${attrAddSpan.offsetLeft}px`;
dropdown.style.top = `${attrAddSpan.offsetTop + attrAddSpan.offsetHeight}px`;
}
// Show window for adding/editing attributes
function showWindow(type, animateType, useId, existingSpan = null, otherAttrName = null) {
const windowDiv = document.createElement('div');
windowDiv.className = 'etaniWindow';
const existingIds = getExistingIds();
const isEdit = !!existingSpan;
let input1, input2, existingIdsDiv, historyDiv, confirmBtn, deleteBtn, cancelBtn;
if (type === 'id') {
const label = document.createElement('label');
label.textContent = 'Enter ID:';
input1 = document.createElement('input');
input1.type = 'text';
if (isEdit) input1.value = existingSpan.textContent.replace('id=', '');
windowDiv.appendChild(label);
windowDiv.appendChild(input1);
existingIdsDiv = createIdsDisplay(existingIds, false);
windowDiv.appendChild(existingIdsDiv);
} else if (type === 'begin') {
const parsed = isEdit ? parseBeginValue(existingSpan.textContent) : { selected: [], plus: null, time: null };
existingIdsDiv = createIdsDisplay(existingIds, true, parsed.selected);
windowDiv.appendChild(existingIdsDiv);
// Create a div wrapper for plus input with text
const plusDiv = document.createElement('div');
plusDiv.className = 'etaniAnimateBeginPlusOuter';
plusDiv.textContent = 'plus: ';
const plusInput = document.createElement('input');
plusInput.type = 'number';
plusInput.min = '0';
plusInput.step = '0.1';
plusInput.className = 'etaniAnimateBeginPlus';
plusInput.style.width = '60px';
plusInput.style.display = 'inline';
if (isEdit) {
const parsed = parseBeginValue(existingSpan.textContent);
plusInput.value = parsed.plus || parsed.time || '';
}
plusDiv.appendChild(plusInput);
const sText = document.createTextNode(' s');
plusDiv.appendChild(sText);
windowDiv.appendChild(plusDiv);
} else if (type === 'other') {
const label1 = document.createElement('label');
label1.textContent = 'Attribute Name:';
input1 = document.createElement('input');
input1.type = 'text';
if (isEdit) input1.value = otherAttrName;
const label2 = document.createElement('label');
label2.textContent = 'Attribute Value:';
input2 = document.createElement('input');
input2.type = 'text';
if (isEdit) input2.value = existingSpan.textContent.split('=')[1];
windowDiv.appendChild(label1);
windowDiv.appendChild(input1);
windowDiv.appendChild(label2);
windowDiv.appendChild(input2);
}
const buttonsDiv = document.createElement('div');
confirmBtn = document.createElement('button');
confirmBtn.textContent = 'Confirm';
confirmBtn.addEventListener('click', () => handleConfirm(type, animateType, useId, input1, input2, existingIdsDiv, existingSpan, windowDiv));
buttonsDiv.appendChild(confirmBtn);
if (isEdit) {
deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.addEventListener('click', () => handleDelete(animateType, useId, type, otherAttrName, existingSpan, windowDiv));
buttonsDiv.appendChild(deleteBtn);
}
cancelBtn = document.createElement('button');
cancelBtn.textContent = 'Cancel';
cancelBtn.addEventListener('click', () => document.body.removeChild(windowDiv));
buttonsDiv.appendChild(cancelBtn);
windowDiv.appendChild(buttonsDiv);
document.body.appendChild(windowDiv);
}
// Create display for existing IDs with selection option
function createIdsDisplay(existingIds, selectable, selected = []) {
const div = document.createElement('div');
div.className = 'etaniExistingIds';
div.textContent = 'Existing IDs: ';
if (selectable) {
const zeroS = document.createElement('span');
zeroS.className = 'etaniIdItem zero-s';
zeroS.textContent = '0s';
zeroS.classList.toggle('selected', selected.includes('0s'));
zeroS.addEventListener('click', () => zeroS.classList.toggle('selected'));
div.appendChild(zeroS);
}
existingIds.forEach(id => {
const span = document.createElement('span');
span.className = 'etaniIdItem';
span.textContent = id;
if (selectable) {
span.addEventListener('click', () => {
if (span.classList.contains('selected')) {
// If already selected, deselect it
span.classList.remove('selected');
} else {
// Otherwise, deselect all other IDs and select this one
div.querySelectorAll('.etaniIdItem:not(.zero-s)').forEach(s => s.classList.remove('selected'));
span.classList.add('selected');
}
});
if (selected.includes(id)) span.classList.add('selected');
}
div.appendChild(span);
});
return div;
}
// Get all existing animation IDs globally, shared across types
function getExistingIds() {
const animates = etani_clone.querySelectorAll('animateTransform, animate[attributeName="opacity"]');
return Array.from(animates)
.map(animate => animate.getAttribute('id'))
.filter(id => id);
}
// Parse begin value to extract zeros, id, plus, and time
function parseBeginValue(beginStr) {
const selected = [];
let plus = null;
let time = null;
let id = null;
// Remove 'begin=' prefix if present
beginStr = beginStr.replace('begin=', '');
// Case 1: "0s"
if (beginStr === '0s') {
selected.push('0s');
}
// Case 2: "3s" (or any number s)
else if (/^\d+(\.\d+)?s$/.test(beginStr)) {
time = parseFloat(beginStr.replace('s', ''));
}
// Case 3: "0s;id.end"
else if (/^0s;[^.]+.end$/.test(beginStr)) {
const parts = beginStr.split(';');
selected.push('0s');
id = parts[1].replace('.end', '');
selected.push(id);
}
// Case 4: "id.end"
else if (/^[^.]+.end$/.test(beginStr)) {
id = beginStr.replace('.end', '');
selected.push(id);
}
// Case 6: "0s;id.end+3s"
else if (/^0s;[^.]+.end\+\d+(\.\d+)?s$/.test(beginStr)) {
const parts = beginStr.split(';');
selected.push('0s');
const [idPart, plusPart] = parts[1].split('+');
id = idPart.replace('.end', '');
selected.push(id);
plus = parseFloat(plusPart.replace('s', ''));
}
// Case 5: "id.end+3s"
else if (/^[^.]+.end\+\d+(\.\d+)?s$/.test(beginStr)) {
const [idPart, plusPart] = beginStr.split('+');
id = idPart.replace('.end', '');
selected.push(id);
plus = parseFloat(plusPart.replace('s', ''));
}
return { selected, plus, time };
}
// Handle confirm button for adding/editing
function handleConfirm(type, animateType, useId, input1, input2, existingIdsDiv, existingSpan, windowDiv) {
let value, attrName;
if (type === 'id') {
value = input1.value.trim();
// Validate: only allow alphanumeric and underscore, no spaces
if (!value || !/^[a-zA-Z0-9_]+$/.test(value) || getExistingIds().includes(value)) {
alert('ID must contain only letters, numbers, and underscores (no spaces)');
return;
}
attrName = 'id';
} else if (type === 'begin') {
const selectedZero = existingIdsDiv.querySelector('.zero-s.selected');
const selectedId = existingIdsDiv.querySelector('.etaniIdItem:not(.zero-s).selected');
const plusInput = windowDiv.querySelector('.etaniAnimateBeginPlus');
const plusValue = plusInput && plusInput.value ? parseFloat(plusInput.value) : null;
let parts = [];
if (selectedZero && selectedId) {
parts.push('0s');
} // Only push '0s' if there is a selectedId
let idPart = selectedId ? `${selectedId.textContent}.end` : null;
if (plusValue > 0) {
if (idPart) {
idPart += `+${plusValue}s`;
} else {
idPart = `${plusValue}s`; // If no id, use Xs directly, ignore 0s
}
}
if (idPart) {
parts.push(idPart);
}
value = parts.join(';');
// Handle case where only '0s' is selected without id or plus
if (!value && selectedZero) {
value = '0s';
}
if (!value) return; // No selection
attrName = 'begin';
} else if (type === 'other') {
attrName = input1.value.trim();
value = input2.value.trim();
if (!attrName || !value) return; // Empty
// Check duplicate attributes (simplified, assume no duplicates for same attrName)
}
const animates = getAnimates(animateType, useId, attrName);
animates.forEach(animate => animate.setAttribute(attrName, value));
const attrAddSpan = existingSpan ? existingSpan.parentNode.querySelector('.etaniAnimateAttrAdd') : document.querySelector(`.etaniItem[data-use-id="${useId}"] .etaniAnimate[data-type="${animateType}"] .etaniAnimateAttrAdd`);
const className = `etaniAnimate${type.charAt(0).toUpperCase() + type.slice(1)}`;
const newSpan = existingSpan || document.createElement('span');
newSpan.className = className;
newSpan.textContent = type === 'other' ? `${attrName}=${value}` : `${attrName}=${value}`;
if (!existingSpan) attrAddSpan.parentNode.insertBefore(newSpan, attrAddSpan);
updateEtaniResult();
document.body.removeChild(existingIdsDiv.closest('.etaniWindow'));
}
// Handle delete button
function handleDelete(animateType, useId, type, otherAttrName, existingSpan, windowDiv) {
const attrName = type === 'other' ? otherAttrName : type;
const animates = getAnimates(animateType, useId);
animates.forEach(animate => animate.removeAttribute(attrName));
existingSpan.remove();
updateEtaniResult();
if (document.body.contains(windowDiv)) {
document.body.removeChild(windowDiv);
}
}
// Get animate elements for the specific useId and type
function getAnimates(animateType, useId, attrType = null) {
if (animateType === 'transform') {
const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return [];
if (attrType === 'id') {
// Per original logic and example, 'id' applies only to the rotate animation
return cloneUseElement.querySelectorAll('animateTransform[type="rotate"]') || [];
} else {
// Other attributes (like 'begin') apply to all three
return cloneUseElement.querySelectorAll('animateTransform') || [];
}
} else {
const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
return cloneUseElement?.querySelectorAll('animate[attributeName="opacity"]') || [];
}
}
// Edit existing attribute
function editAttribute(span, animateType, useId, type, otherAttrName) {
showWindow(type, animateType, useId, span, otherAttrName);
}
// Generate base64 image for a tile
function generateTileImage(tileid) {
const originalTile = document.querySelector(`defs g#${tileid}`);
if (!originalTile) return null;
const etdropUses = document.querySelectorAll('.etdrop use');
const etwaitGroups = document.querySelectorAll('.etwait g');
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 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.appendChild(tileclone);
const svgString = new XMLSerializer().serializeToString(svgWrapper);
return svgToBase64(svgString);
}
// Update all tile images in .etaniItemLeft
function updateAllTileImages() {
const etaniItems = document.querySelectorAll('.etaniItem');
etaniItems.forEach(item => {
const tileid = item.querySelector('.tileid').textContent;
const imageOuter = item.querySelector('.etaniItemImageOuter');
const img = imageOuter ? imageOuter.querySelector('.etaniItemImage') : null;
if (img) {
const base64Url = generateTileImage(tileid);
if (base64Url) {
img.src = base64Url;
}
}
});
}
// Initialize the animation control panel on window load
window.addEventListener('load', () => {
addDynamicStyles();
const etmainouter = document.getElementById('etmainouter');
if (etmainouter) {
const etaniouter = document.createElement('div');
etaniouter.className = 'etaniouter';
const etanibutton = document.createElement('button');
etanibutton.id = 'etanibutton';
etanibutton.textContent = 'Animate it';
etanibutton.addEventListener('click', toggleAnimation);
etaniouter.appendChild(etanibutton);
etmainouter.parentNode.insertBefore(etaniouter, etmainouter.nextSibling);
} else {
console.error('Div with ID "etmainouter" not found.');
}
});
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 正在做etani,ejtile的動畫套件
代码: 全选
好的,這是在 `ejtileani202510221458.js` 檔案中進行所需更改的說明。
-----
### 更改 1:增加 CSS 樣式
**位置:** `addDynamicStyles` 函數內部,在 `/* Styles for etaniContent, etaniMode, etaniAllAppend, etaniValueCtrl */` 區塊的結尾處(大約在第 70 行之後)。
**替換/新增內容:**
在 `.etaniContent, .etaniMode, .etaniAllAppend, .etaniValueCtrl` 的樣式規則之後,但在 `.etaniCol` 的樣式規則之前,插入以下 CSS:
```javascript
.etaniUpdate {
display: inline-block;
vertical-align: top;
padding: 5px;
border: 1px solid #ccc;
margin: 0 5px 5px 5px;
text-align: left;
font-size: 14px;
}
.etaniUpdate label {
margin-right: 5px;
}
.etaniUpdate a {
display: inline-block;
text-decoration: none;
padding: 2px 8px;
font-size: 14px;
margin: 0 2px;
cursor: pointer;
}
.etaniUpdateTiles {
border: 1px solid #008CBA;
color: #008CBA;
}
.etaniUpdateBoard {
border: 1px solid #c06;
color: #c06;
}
```
-----
### 更改 2:新增 `createEtaniItem` 輔助函數
**位置:** 在 `handleModeChange` 函數之後,`createEtaniInner` 函數之前(大約在第 1540 行)。
**替換/新增內容:**
插入以下新函數。此函數是從 `createEtaniInner` 中提取的,用於創建單個的 `etaniItem`:
```javascript
// Create a single etaniItem in the UI
function createEtaniItem(tileid) {
const etaniCol = document.querySelector('.etaniCol');
if (!etaniCol) return;
// Check if item already exists
if (etaniCol.querySelector(`.etaniItem[data-use-id="${tileid}"]`)) {
return;
}
const etaniItem = document.createElement('div');
etaniItem.className = 'etaniItem';
etaniItem.setAttribute('data-use-id', tileid);
const etaniItemLeft = document.createElement('div');
etaniItemLeft.className = 'etaniItemLeft';
const etaniItemRight = document.createElement('div');
etaniItemRight.className = 'etaniItemRight';
// Use generateTileImage to get the thumbnail
const base64Url = generateTileImage(tileid);
if (base64Url) {
const imageOuter = document.createElement('div');
imageOuter.className = 'etaniItemImageOuter';
const img = document.createElement('img');
img.className = 'etaniItemImage';
img.src = base64Url;
img.alt = tileid;
imageOuter.appendChild(img);
etaniItemLeft.appendChild(imageOuter);
}
const tileidDiv = document.createElement('div');
tileidDiv.className = 'tileid';
tileidDiv.textContent = tileid;
etaniItemLeft.appendChild(tileidDiv);
etaniItem.appendChild(etaniItemLeft);
etaniItem.appendChild(etaniItemRight);
etaniCol.appendChild(etaniItem);
}
```
-----
### 更改 3:新增 `handleUpdateTilesClick` 和 `handleUpdateBoardClick` 處理函數
**位置:** 在 `createEtaniItem` 函數(上一步新增的)之後,`createEtaniInner` 函數之前(大約在第 1588 行)。
**替換/新增內容:**
插入以下兩個新的事件處理函數:
```javascript
// Handle click event for the 'tiles' update button
function handleUpdateTilesClick(e) {
e.preventDefault();
if (!etani_clone) return;
const originalSvg = document.getElementById('etmain');
if (!originalSvg) return;
const originalDefs = originalSvg.querySelector('defs');
const cloneDefs = etani_clone.querySelector('defs');
const originalDrop = originalSvg.querySelector('.etdrop');
const cloneDrop = etani_clone.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));
}
// Add new etaniItem to UI
createEtaniItem(tileId);
}
});
// Refresh all tile images as defs might have changed
updateAllTileImages();
updateEtaniResult();
}
// Handle click event for the 'board' update button
function handleUpdateBoardClick(e) {
e.preventDefault();
if (!etani_clone) return;
const originalSvg = document.getElementById('etmain');
if (!originalSvg) return;
const originalBoard = originalSvg.querySelector('.etdrop .etboard');
const cloneBoard = etani_clone.querySelector('.etdrop .etboard');
if (originalBoard && cloneBoard) {
cloneBoard.replaceWith(originalBoard.cloneNode(true));
updateEtaniResult();
}
}
```
-----
### 更改 4:修改 `createEtaniInner` 函數
**位置:** `createEtaniInner` 函數(大約在第 1590 行開始)。
**替換/新增內容 1:**
在 `etaniCtrl.appendChild(etaniContent);` 這一行(大約在第 1629 行)之後,插入以下代碼以新增 "update" UI:
```javascript
const etaniUpdate = document.createElement('div');
etaniUpdate.className = 'etaniUpdate';
const updateLabel = document.createElement('label');
updateLabel.textContent = 'update: ';
etaniUpdate.appendChild(updateLabel);
const updateTilesLink = document.createElement('a');
updateTilesLink.className = 'etaniUpdateTiles';
updateTilesLink.textContent = 'tiles';
updateTilesLink.href = 'javascript:;';
updateTilesLink.addEventListener('click', handleUpdateTilesClick);
etaniUpdate.appendChild(updateTilesLink);
const updateBoardLink = document.createElement('a');
updateBoardLink.className = 'etaniUpdateBoard';
updateBoardLink.textContent = 'board';
updateBoardLink.href = 'javascript:;';
updateBoardLink.addEventListener('click', handleUpdateBoardClick);
etaniUpdate.appendChild(updateBoardLink);
etaniCtrl.appendChild(etaniUpdate);
```
**替換/新增內容 2:**
找到 `etdropUses.forEach((useElement, i) => { ... });` 這個循環(大約在第 1709 行),將其**整個** `forEach` 循環替換為以下內容,以使用新的 `createEtaniItem` 函數:
```javascript
const etdropUses = document.querySelectorAll('#etmain .etdrop use');
etdropUses.forEach((useElement) => {
const tileid = useElement.getAttribute('href').substring(1);
createEtaniItem(tileid);
});
```https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 正在做etani,ejtile的動畫套件
當用戶點擊btndelete、btnnew、btnup、btndown、btncopy時,<defs>中相應的<g id="id">會刪除,新增,上移,下移,複製。
當刪除時,直接把#etmain中的.etdrop中對應的<use href="#id">刪除,並改變餘下的<use>的id。比如當刪除id="tile3"的時候,tile4和tile5會變成tile3和tile4。
新增和複製時,要用以下代碼:
代码: 全选
// change etdrop tiles
function modeInit () {
mode = getMode();
if (!mode) return;
if (tiledropped.length) return;
// i為新增或複製的id,是當前所有id的最後一個
let thisdotmax = dotmax[i];
createTile(
i,
thisdotmax + rotatepadding + 80,
thisdotmax + rotatepadding,
1, 0, getTileFill(i), colordrop, strokewidthdrop
).classList.add("tiledropped");
};當上移或下移時,其實是id的交換,也要更改#etmain中的.etdrop中對應的<use href="#id">。
請用繁體中文回答。只需告知所要更改的代碼和位置,以及要替換的內容。代碼的注釋都要是英文。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 正在做etani,ejtile的動畫套件
代码: 全选
I can see the JavaScript code contains variables and elements related to the functionality you're requesting changes for. To address your request, here's the plan for modifying the code:
1. **Delete Operation (`btndelete`)**:
* Remove the corresponding `<g id="id">` inside `<defs>`.
* Also, delete the `<use href="#id">` in `#etmain .etdrop` and adjust the `id` of the remaining `<use>` elements accordingly.
2. **Add Operation (`btnnew`)**:
* Use the `modeInit` function to create new tiles with the appropriate `id`.
3. **Move Up and Move Down (`btnup`, `btndown`)**:
* Swap the `id` of the corresponding tiles and update the `<use href="#id">` elements.
4. **Copy Operation (`btncopy`)**:
* Use the `modeInit` function to create a copy of the tile, as per the provided code.
I'll proceed with suggesting the relevant code changes and replacements. Here's what needs to be updated:
---
### 1. Delete Operation (`btndelete`)
Find the code where the delete operation is handled and add the following:
```javascript
// delete the corresponding <g id="id"> from <defs>
function deleteTile(id) {
// Remove the <g id="id"> from <defs>
const tileGroup = document.querySelector(`#defs g#${id}`);
if (tileGroup) tileGroup.remove();
// Remove <use href="#id"> from #etmain .etdrop
const useElement = document.querySelector(`#etmain .etdrop use[href="#${id}"]`);
if (useElement) {
useElement.remove();
}
// Update remaining use elements' ids
let allUses = document.querySelectorAll("#etmain .etdrop use");
allUses.forEach((use, index) => {
use.setAttribute("id", `tile${index + 1}`);
});
}
```
---
### 2. Add Operation (`btnnew`)
You should update the function to include the creation of a new tile with the last available ID:
```javascript
// create a new tile with the appropriate id
function addNewTile() {
let lastId = getLastId(); // Implement this to find the last id number.
let newId = lastId + 1; // Increment the ID for the new tile
modeInit(newId); // This function already adds the tile as per your provided code
}
```
And in the `modeInit` function, ensure `i` is passed as the `id`:
```javascript
function modeInit(i) {
mode = getMode();
if (!mode) return;
if (tiledropped.length) return;
let thisdotmax = dotmax[i];
createTile(
i,
thisdotmax + rotatepadding + 80,
thisdotmax + rotatepadding,
1, 0, getTileFill(i), colordrop, strokewidthdrop
).classList.add("tiledropped");
}
```
---
### 3. Move Up and Move Down (`btnup`, `btndown`)
For the move up and move down functionality, swap the IDs of the tiles and update the `href` of the corresponding `<use>` elements:
```javascript
// Move tile up
function moveTileUp(id) {
let currentTile = document.querySelector(`#defs g#${id}`);
let prevTile = currentTile.previousElementSibling;
if (prevTile) {
// Swap the IDs
let tempId = currentTile.id;
currentTile.id = prevTile.id;
prevTile.id = tempId;
// Update the <use href="#id"> in #etmain .etdrop
let currentUse = document.querySelector(`#etmain .etdrop use[href="#${id}"]`);
let prevUse = document.querySelector(`#etmain .etdrop use[href="#${prevTile.id}"]`);
if (currentUse && prevUse) {
currentUse.setAttribute("href", `#${prevTile.id}`);
prevUse.setAttribute("href", `#${currentTile.id}`);
}
}
}
// Move tile down
function moveTileDown(id) {
let currentTile = document.querySelector(`#defs g#${id}`);
let nextTile = currentTile.nextElementSibling;
if (nextTile) {
// Swap the IDs
let tempId = currentTile.id;
currentTile.id = nextTile.id;
nextTile.id = tempId;
// Update the <use href="#id"> in #etmain .etdrop
let currentUse = document.querySelector(`#etmain .etdrop use[href="#${id}"]`);
let nextUse = document.querySelector(`#etmain .etdrop use[href="#${nextTile.id}"]`);
if (currentUse && nextUse) {
currentUse.setAttribute("href", `#${nextTile.id}`);
nextUse.setAttribute("href", `#${currentTile.id}`);
}
}
}
```
---
### 4. Copy Operation (`btncopy`)
For the copy functionality, you can reuse the `modeInit` function to create a duplicate tile:
```javascript
// Copy the tile with a new id
function copyTile(id) {
let newId = getLastId() + 1; // Increment for the new tile
modeInit(newId); // Create a new tile with the new id
}
```
Make sure `getLastId()` returns the current highest tile id for proper incrementing.
---
These code snippets should be added in the appropriate places of your existing JavaScript code to implement the changes. If you need further assistance with integrating these into your existing code or have any more specific requirements, feel free to ask!
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 正在做etani,ejtile的動畫套件
id是從0開始的遞增整數,起初會給出id的總數。比如id有「tile0、tile1、tile2」,總數是3。
當刪除某id時,其後方的id會遞減。
當新增時,會加到最後。
上移和下移,是兩個id之間的交換。比如id=tile5上移,那等於是tile5和tile4交換
寫一個全局變量record_id_change和一個函數recordIdChange(),記錄以上的操作。
之後再寫一個函數changeTheTilesId(),對於#etmain中的.etdrop中的<use href="#id">,改變那些由於刪除和移動所改變的id。對於新增的id要用以下代碼:
代码: 全选
// change etdrop tiles
function addTilesFromRecord () {
mode = getMode();
if (!mode) return;
if (tiledropped.length) return;
// 這裏應寫一個while循環,i為新增的id
let thisdotmax = dotmax[i];
createTile(
i,
thisdotmax + rotatepadding + 80,
thisdotmax + rotatepadding,
1, 0, getTileFill(i), colordrop, strokewidthdrop
).classList.add("tiledropped");
};
請用繁體中文回答。代碼的注釋都要是英文。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 正在做etani,ejtile的動畫套件
代码: 全选
let record_id_change = [];
function recordIdChange(type, id) {
// Record the operation with type and affected id
record_id_change.push({ type, id });
}
function changeTheTilesId() {
// Get current mode
const mode = getMode();
if (!mode) return;
// Process each change in order
for (const change of record_id_change) {
const { type, id: k } = change;
if (type === 'new') {
// Add the new tile for the recorded id
let thisdotmax = dotmax[k];
createTile(
k,
thisdotmax + rotatepadding + 80,
thisdotmax + rotatepadding,
1, 0, getTileFill(k), colordrop, strokewidthdrop
).classList.add("tiledropped");
} else {
// Update href for delete, up, down
const uses = document.querySelectorAll('#etmain .etdrop use');
for (const use of uses) {
const href = use.getAttribute('href');
if (href && href.startsWith('#tile')) {
const m = parseInt(href.slice(5));
if (isNaN(m)) continue;
let newM;
switch (type) {
case 'delete':
if (m === k) {
// Remove the etdrop for deleted id
use.closest('.etdrop')?.remove();
} else if (m > k) {
newM = m - 1;
}
break;
case 'up':
if (m === k) {
newM = k - 1;
} else if (m === k - 1) {
newM = k;
}
break;
case 'down':
if (m === k) {
newM = k + 1;
} else if (m === k + 1) {
newM = k;
}
break;
}
if (newM !== undefined) {
use.setAttribute('href', `#tile${newM}`);
}
}
}
}
}
// Clear the record after processing
record_id_change = [];
}代码: 全选
case 'delete':
if (m === k) {
// Remove only the use element for deleted id
use.remove();
} else if (m > k) {
newM = m - 1;
}
break;https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 正在做etani,ejtile的動畫套件
.etaniItemRight {min-height: 64px;}
.etaniItem {width: 100%; display: inline-block;}
把.etaniAllAppendTransform和.etaniAllAppendOpacity改成<a href="javascript:;">
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 正在做etani,ejtile的動畫套件
代码: 全选
// Global variable to hold the SVG clone and ensure state is maintained
let etani_clone = null;
// Track copy, move, and delete mode states, selected item, and last click time for double-click detection
let isCopyMode = false;
let isMoveMode = false;
let isDeleteMode = false;
let selectedMoveItem = null;
let lastClickTime = 0;
let lastClickedItem = null;
// Add dynamic CSS styles to the document
function addDynamicStyles() {
if (document.getElementById('dynamic-et-styles')) {
return;
}
const styleSheet = document.createElement('style');
styleSheet.id = 'dynamic-et-styles';
styleSheet.textContent = `
/* Styles for etaniouter and button */
.etaniouter {
margin-top: 10px;
padding: 10px;
border: 1px solid #ddd;
background-color: #f5f5f5;
}
/* Styles for etaniinner, etaniCtrl, etaniCol, etaniResult */
.etaniinner {
margin-top: 10px;
}
.etaniCtrl {
margin-bottom: 10px;
clear: both;
padding: 5px;
border: 1px solid #c0c0c0;
text-align: center;
}
/* Styles for etaniContent, etaniMode, etaniAllAppend, etaniValueCtrl */
.etaniContent, .etaniMode, .etaniAllAppend, .etaniValueCtrl {
display: inline-block;
vertical-align: top;
padding: 5px;
border: 1px solid #ccc;
margin: 0 5px 5px 5px;
text-align: left;
}
.etaniUpdate {
display: inline-block;
vertical-align: top;
padding: 5px;
border: 1px solid #ccc;
margin: 0 5px 5px 5px;
text-align: left;
font-size: 14px;
}
.etaniUpdate label {
margin-right: 5px;
}
.etaniUpdate a {
display: inline-block;
text-decoration: none;
padding: 2px 8px;
font-size: 14px;
margin: 0 2px;
cursor: pointer;
}
.etaniUpdateTiles {
border: 1px solid #008CBA;
color: #008CBA;
}
.etaniUpdateBoard {
border: 1px solid #c06;
color: #c06;
}
.etaniCol {
border: 1px solid #aaa;
padding: 5px;
margin-bottom: 10px;
clear: both;
}
/* Styles for etaniResult */
.etaniResult {
text-align: center;
margin-bottom: 10px;
padding: 10px;
border: 1px solid #bbb;
box-sizing: border-box;
}
/* Control and button styles */
.etaniContent a, .etaniValueCtrl a {
display: inline-block;
margin: 0 5px;
text-decoration: none;
padding: 5px 10px;
font-size: 14px;
}
.etaniCenter {
border: 1px solid green;
color: green;
}
.etaniContentHTML {
border: 1px solid #0099ff;
color: #0099ff;
margin-right: 15px;
}
.etaniValueCtrlUp {
border: 1px solid purple;
color: purple;
}
/* Styles for etaniAllAppend buttons */
.etaniAllAppend button {
padding: 5px 10px;
font-size: 16px;
margin: 0 5px;
cursor: pointer;
border: 1px solid #333;
background-color: #fff;
}
/* Custom radio button styles */
.etaniMode span {
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;
}
.etaniMode span.active {
background-color: #008CBA;
color: white;
border-color: #008CBA;
}
/* Result and animation item styles */
.etaniResultDR {
text-align: center;
margin-bottom: 10px;
}
.etaniResultDownload, .etaniResultRename {
display: inline-block;
margin-right: 15px;
text-decoration: none;
padding: 5px 10px;
font-size: 16px;
}
.etaniResultDownload {
border: 1px solid blue;
color: blue;
}
.etaniResultRename {
border: 1px solid brown;
color: brown;
}
.etaniResultImage {
display: block;
max-width: 480px;
width: 100%;
height: auto;
margin: 0 auto 10px auto;
border: 1px solid #000;
box-sizing: border-box;
}
.etaniResultSize {
display: inline-block;
margin-left: 10px;
font-size: 12px;
color: #555;
}
/* etaniItem structure */
.etaniItem {
min-height: 48px;
border: 1px solid #ccc;
box-sizing: border-box;
width: 100%;
margin-bottom: -1px;
background-color: lightyellow;
}
.etaniItemLeft {
float: left;
width: 60px;
min-height: 48px;
padding: 2px 0;
text-align: center;
}
.etaniItemImageOuter {
width: 40px;
height: 40px;
margin: 0 auto;
}
.etaniItemImage {
width: 100%;
height: 100%;
display: block;
}
.etaniItemRight {
margin-left: 60px;
padding: 7px;
min-height: 48px;
background-color: #fff;
}
.tileid {
text-align: center;
font-size: 12px;
word-break: break-all;
padding-top: 2px;
}
/* Animation controls */
.etaniAnimate {
border: 1px solid #999;
padding: 5px;
margin-bottom: 5px;
}
.etaniAnimateAttr {
margin-bottom: 5px;
}
.etaniAnimateName {
display: inline-block;
padding: 2px 5px;
background-color: #555;
color: white;
margin-right: 10px;
font-size: 12px;
}
.etaniAnimateAttr > span {
cursor: pointer;
}
.etaniAnimateAttr > span:not(.etaniAnimateName) {
display: inline-block;
padding: 2px 5px;
font-size: 12px;
margin-right: 7px;
box-sizing: border-box;
border-width: 1px;
border-style: solid;
}
.etaniAnimateAttrAdd {
border-color: #2c8c12;
color: #2c8c12;
}
.etaniAnimateId {
border-color: #742bcc;
color: #742bcc;
}
.etaniAnimateBegin {
border-color: #b6533c;
color: #b6533c;
}
.etaniAnimateOther {
border-color: #1cadca;
color: #1cadca;
}
/* Dropdown menu styles */
.etaniDropdown {
position: absolute;
background-color: #fff;
border: 1px solid #ccc;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
z-index: 1002;
}
.etaniDropdownItem {
padding: 8px 12px;
cursor: pointer;
font-size: 12px;
}
.etaniDropdownItem:hover {
background-color: #f0f0f0;
}
/* Window modal styles */
.etaniWindow {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 15px;
border: 1px solid #ccc;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
z-index: 1003;
width: 300px;
}
.etaniWindow label, .etaniWindow input {
display: block;
margin-bottom: 10px;
}
.etaniWindow button {
margin-right: 10px;
padding: 5px 10px;
}
.etaniExistingIds, .etaniAnimateBeginPlusOuter {
margin-bottom: 10px;
font-size: 12px;
}
.etaniIdItem {
cursor: pointer;
padding: 2px 5px;
margin-right: 5px;
border: 1px solid #ddd;
display: inline-block;
}
.etaniIdItem.selected {
background-color: #008CBA;
color: white;
}
.etaniIdItem.zero-s.selected {
color: white;
}
.etaniIdItem.zero-s {
color: #888;
}
.etaniAnimateDur {
border-color: blue;
color: blue;
}
.etaniAnimateFR {
border-color: #78229f;
color: #78229f;
}
.etaniAnimateValue {
margin-top: 5px;
}
.etaniAVCtrl {
display: inline-block;
vertical-align: top;
margin-right: 5px;
margin-bottom: 3px;
}
.etaniAVCtrl svg {
margin-left: -1px;
margin-top: -1px;
}
.etaniAVCtrl span {
display: inline-block;
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
cursor: pointer;
box-sizing: border-box;
vertical-align: top;
margin-right: 3px;
}
.etaniAVAdd {
background-color: #a7fca7;
border: 1px solid #71c371;
}
.etaniAVDelete {
background-color: #ffcccc;
border: 1px solid #cc3333;
}
.etaniAVDelete.deleting {
background-color: #cc3333;
color: white;
}
.etaniAVCopy {
background-color: #ccccff;
border: 1px solid #6666cc;
}
.etaniAVCopy.copying {
background-color: #6666cc;
color: white;
}
.etaniAVMove {
background-color: #ffcc99;
border: 1px solid #cc9966;
}
.etaniAVMove.moving {
background-color: #cc9966;
color: white;
}
.etaniAVLabel {
font-size: 14px;
margin-right: 5px;
}
.etaniAV {
display: inline-block;
vertical-align: top;
}
.etaniAVItem {
display: inline-block;
height: 24px;
background-color: #ff9933;
border: 1px dashed #00bfff;
margin: 0 5px 3px;
padding: 0 5px;
box-sizing: border-box;
cursor: pointer;
position: relative;
text-align: center;
line-height: 24px;
font-size: 12px;
color: #333;
}
.etaniAVItem.deleting-target, .etaniAVItem.copying-target, .etaniAVItem.moving-target {
background-color: #ff4d4d;
border: 2px solid red;
}
.etaniAVItem.selected-move {
background-color: #66ccff;
border: 2px solid #0066cc;
}
/* Modal/popup styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.modal-content {
position: fixed;
width: 98%;
height: 48%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 1001;
box-sizing: border-box;
}
.modal-content textarea {
width: 100%;
height: calc(100% - 50px);
resize: none;
border: 1px solid #ccc;
font-size: 12px;
box-sizing: border-box;
}
.modal-close {
position: absolute;
top: 0;
right: 10px;
font-size: 48px;
line-height: 1;
cursor: pointer;
color: #333;
}
`;
document.head.appendChild(styleSheet);
}
// Find the first missing letter in the sequence starting from 'a'
function findFirstMissingLetter(existingLetters) {
let letter = 'a';
while (existingLetters.includes(letter)) {
const code = letter.charCodeAt(0);
if (code >= 97 && code < 122) { // lowercase a-y
letter = String.fromCharCode(code + 1);
} else if (code === 122) { // z -> A
letter = 'A';
} else if (code >= 65 && code < 90) { // uppercase A-Y
letter = String.fromCharCode(code + 1);
} else if (code === 90) { // Z -> a (loop back)
letter = 'a';
}
}
return letter;
}
// Extract a specific transform function and its value from a transform string
function extractTransformPart(transformString, type) {
const regex = new RegExp(`(${type})\\(([^)]+)\\)`, 'i');
const match = transformString.match(regex);
if (match) {
return { func: match[1], value: match[2].trim() };
}
return { func: '', value: '' };
}
// Parse transform string to get individual transform values
function parseTransform(transformString) {
const defaultTransform = { translate: '0,0', scale: '1,1', rotate: '0' };
if (!transformString) return defaultTransform;
const transform = {};
const getMatch = (type) => {
const part = extractTransformPart(transformString, type).value;
if (!part) return null;
return part.split(/[,\s]+/).join(',');
};
transform.translate = getMatch('translate') || defaultTransform.translate;
transform.scale = getMatch('scale') || defaultTransform.scale;
if (transform.scale.split(',').length === 1) {
transform.scale += `,${transform.scale}`;
}
transform.rotate = getMatch('rotate') || defaultTransform.rotate;
return transform;
}
// Convert SVG string to a Base64 data URL
function svgToBase64(svgString) {
const base64 = btoa(unescape(encodeURIComponent(svgString)));
return `data:image/svg+xml;base64,${base64}`;
}
// Format byte size into human-readable string
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Update the result section with the current state of etani_clone
function updateEtaniResult() {
if (!etani_clone) return;
const svgString = new XMLSerializer().serializeToString(etani_clone);
const sizeInBytes = new Blob([svgString]).size;
const base64Url = svgToBase64(svgString);
const now = new Date();
const yyyy = now.getFullYear();
const mm = String(now.getMonth() + 1).padStart(2, '0');
const dd = String(now.getDate()).padStart(2, '0');
const hh = String(now.getHours()).padStart(2, '0');
const mi = String(now.getMinutes()).padStart(2, '0');
const ss = String(now.getSeconds()).padStart(2, '0');
const defaultFilename = `ejtileAnimation_${yyyy}${mm}${dd}_${hh}${mi}${ss}.svg`;
const imgElement = document.querySelector('.etaniResultImage');
const downloadElement = document.querySelector('.etaniResultDownload');
const renameElement = document.querySelector('.etaniResultRename');
const sizeElement = document.querySelector('.etaniResultSize');
if (imgElement && downloadElement && renameElement && sizeElement) {
imgElement.src = base64Url;
sizeElement.textContent = `Size: ${formatBytes(sizeInBytes)}`;
downloadElement.href = base64Url;
downloadElement.download = defaultFilename;
renameElement.onclick = (e) => {
e.preventDefault();
let currentDownloadName = downloadElement.download;
let newFilename = prompt("Enter new filename:", currentDownloadName);
if (newFilename) {
if (!newFilename.toLowerCase().endsWith('.svg')) {
newFilename += '.svg';
}
downloadElement.download = newFilename;
alert(`Filename changed to: ${newFilename}`);
}
};
}
}
// Handle the 'Center' button click to reset the .etdrop transform
function handleCenterClick(e) {
e.preventDefault();
if (etani_clone) {
const etdrop = etani_clone.querySelector('.etdrop');
if (etdrop) {
etdrop.setAttribute('transform', 'translate(240,240) scale(1,1)');
updateEtaniResult();
}
}
}
// Handle the HTML popup window
function handleContentHTMLClick(e) {
e.preventDefault();
if (!etani_clone) return;
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
const content = document.createElement('div');
content.className = 'modal-content';
const close = document.createElement('span');
close.className = 'modal-close';
close.innerHTML = '×';
const textarea = document.createElement('textarea');
textarea.value = new XMLSerializer().serializeToString(etani_clone);
const closeModal = () => {
if (document.body.contains(overlay)) document.body.removeChild(overlay);
if (document.body.contains(content)) document.body.removeChild(content);
};
close.onclick = closeModal;
overlay.onclick = closeModal;
content.appendChild(close);
content.appendChild(textarea);
document.body.appendChild(overlay);
document.body.appendChild(content);
}
// Calculate additive transform or opacity value for a given type
function calculateAdditiveValue(type, originalValue, currentBaseValue, scaleValue = '1,1') {
const roundToThreeDecimals = (value) => {
return Math.round(value * 1000) / 1000;
};
if (type === 'rotate') {
const ovAngle = parseFloat(currentBaseValue.trim());
const cvAngle = parseFloat(originalValue.trim());
return roundToThreeDecimals(cvAngle - ovAngle);
} else if (type === 'scale') {
const ovScales = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
const cvScales = originalValue.split(',').map(c => parseFloat(c.trim()));
const v1 = roundToThreeDecimals(cvScales[0] / ovScales[0]);
const v2 = roundToThreeDecimals(cvScales.length > 1 ? (cvScales[1] / ovScales[1]) : (cvScales[0] / ovScales[0]));
return v1 === v2 ? `${v1}` : `${v1},${v2}`;
} else if (type === 'translate') {
const ovCoords = currentBaseValue.split(',').map(c => parseFloat(c.trim()));
const cvCoords = originalValue.split(',').map(c => parseFloat(c.trim()));
const scaleFactors = scaleValue.split(',').map(c => parseFloat(c.trim()));
const v1 = roundToThreeDecimals(cvCoords[0] - (ovCoords[0] * scaleFactors[0]));
const v2 = roundToThreeDecimals(cvCoords[1] - (ovCoords[1] * (scaleFactors.length > 1 ? scaleFactors[1] : scaleFactors[0])));
return `${v1},${v2}`;
} else if (type === 'opacity') {
return originalValue;
}
return '';
}
// Update duration based on the number of values
function updateDuration(animate, etaniAnimate, newValuesLength) {
const currentDur = parseFloat(etaniAnimate.querySelector('.etaniAnimateDur').textContent.replace('dur: ', '').replace('s', ''));
const isIntegerDur = Number.isInteger(currentDur);
const newDur = isIntegerDur ? (newValuesLength > 1 ? newValuesLength - 1 : 0) : currentDur;
if (newDur > 0) {
animate.setAttribute('dur', `${newDur}s`);
} else {
animate.removeAttribute('dur');
}
etaniAnimate.querySelector('.etaniAnimateDur').textContent = `dur: ${newDur}s`;
}
// Handle click event for the '+' button for transform or opacity
function handleAVAddClick(e, useElementId, animateType = 'transform') {
e.preventDefault();
if (!etani_clone) return;
// Reset all modes before adding a new value
resetModes(animateType);
const etaniItemRight = e.currentTarget.closest('.etaniItemRight');
const etaniAV = etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"] .etaniAV`);
const etaniAnimate = etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`);
if (animateType === 'transform') {
// Get the original <use> element from #etmain to read its current transform
const originalUseElement = document.querySelector(`#etmain .etdrop use[href="#${useElementId}"]`);
if (!originalUseElement) return;
const originalTransformString = originalUseElement.getAttribute('transform') || '';
const originalTransforms = parseTransform(originalTransformString);
// Get the corresponding <use> element in the clone
const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useElementId}"]`);
if (!cloneUseElement) return;
// Find all animateTransform elements inside the cloned <use>
const animates = cloneUseElement.querySelectorAll('animateTransform');
animates.forEach(animate => {
const type = animate.getAttribute('type').toLowerCase();
let currentValueString = animate.getAttribute('values') || '';
let newValue = '';
// Get the new value directly from the parsed original transform
if (type === 'translate') {
newValue = originalTransforms.translate;
} else if (type === 'scale') {
newValue = originalTransforms.scale;
} else if (type === 'rotate') {
newValue = originalTransforms.rotate;
}
const newValueString = (currentValueString ? currentValueString + ';' : '') + newValue;
animate.setAttribute('values', newValueString);
updateDuration(animate, etaniAnimate, newValueString.split(';').length);
});
} else if (animateType === 'opacity') {
const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useElementId}"]`);
if (!cloneUseElement) return;
const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
if (!animateOpacity) return;
const currentValueString = animateOpacity.getAttribute('values') || '';
const values = currentValueString ? currentValueString.split(';') : [];
const newValue = values.length > 0 ? values[values.length - 1] : '1';
const newValueString = (currentValueString ? currentValueString + ';' : '') + newValue;
animateOpacity.setAttribute('values', newValueString);
updateDuration(animateOpacity, etaniAnimate, newValueString.split(';').length);
}
// Add new etaniAVItem with appropriate label
if (etaniAV && etaniAnimate) {
const newAVItem = document.createElement('span');
newAVItem.className = 'etaniAVItem';
newAVItem.addEventListener('click', (e) => handleAVItemClick(e, animateType));
const existingItems = etaniAV.querySelectorAll('.etaniAVItem');
if (animateType === 'transform') {
const existingLetters = Array.from(existingItems).map(item => item.textContent);
newAVItem.textContent = findFirstMissingLetter(existingLetters);
} else if (animateType === 'opacity') {
const values = animateType === 'opacity' ?
etaniAnimate.querySelector('animate[attributeName="opacity"]')?.getAttribute('values')?.split(';') || ['1'] :
['1'];
newAVItem.textContent = values[values.length - 1];
}
etaniAV.appendChild(newAVItem);
}
updateEtaniResult();
}
// Reset all mode states for a specific animateType
function resetModes(animateType, excludeMode = null) {
const etaniCol = document.querySelector('.etaniCol');
const avItems = document.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
if (excludeMode !== 'delete') {
isDeleteMode = false;
etaniCol.classList.remove(`deleting-mode-${animateType}`);
document.querySelectorAll(`.etaniAVDelete[data-type="${animateType}"]`).forEach(btn => {
btn.classList.remove('deleting');
btn.title = 'Delete Value';
});
}
if (excludeMode !== 'copy') {
isCopyMode = false;
etaniCol.classList.remove(`copying-mode-${animateType}`);
document.querySelectorAll(`.etaniAVCopy[data-type="${animateType}"]`).forEach(btn => {
btn.classList.remove('copying');
btn.title = 'Copy Value';
});
}
if (excludeMode !== 'move') {
isMoveMode = false;
selectedMoveItem = null;
etaniCol.classList.remove(`moving-mode-${animateType}`);
document.querySelectorAll(`.etaniAVMove[data-type="${animateType}"]`).forEach(btn => {
btn.classList.remove('moving');
btn.title = 'Move Value';
});
}
avItems.forEach(item => {
item.classList.remove('deleting-target', 'copying-target', 'moving-target', 'selected-move');
});
}
// Handle click event for the '-' button to toggle deletion mode
function handleAVDeleteToggle(e, animateType = 'transform') {
e.preventDefault();
const deleteButton = e.currentTarget;
resetModes(animateType, 'delete');
isDeleteMode = !isDeleteMode;
deleteButton.classList.toggle('deleting', isDeleteMode);
const etaniItemRight = deleteButton.closest('.etaniItemRight');
const etaniCol = deleteButton.closest('.etaniCol');
const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
avItems.forEach(item => {
item.classList.toggle('deleting-target', isDeleteMode);
});
etaniCol.classList.toggle(`deleting-mode-${animateType}`, isDeleteMode);
if (isDeleteMode) {
deleteButton.title = "Click value item to delete (Click again to cancel)";
} else {
deleteButton.title = "Delete Value";
avItems.forEach(item => {
item.classList.remove('deleting-target');
});
}
}
// Handle click event for the 'Copy' button to toggle copy mode
function handleAVCopyToggle(e, animateType) {
e.preventDefault();
const copyButton = e.currentTarget;
resetModes(animateType, 'copy');
isCopyMode = !isCopyMode;
copyButton.classList.toggle('copying', isCopyMode);
const etaniItemRight = copyButton.closest('.etaniItemRight');
const etaniCol = copyButton.closest('.etaniCol');
const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
avItems.forEach(item => {
item.classList.toggle('copying-target', isCopyMode);
});
etaniCol.classList.toggle(`copying-mode-${animateType}`, isCopyMode);
if (isCopyMode) {
copyButton.title = "Click value item to copy (Click again to cancel)";
} else {
copyButton.title = "Copy Value";
avItems.forEach(item => {
item.classList.remove('copying-target');
});
}
}
// Handle click event for the 'Move' button to toggle move mode
function handleAVMoveToggle(e, animateType) {
e.preventDefault();
const moveButton = e.currentTarget;
resetModes(animateType, 'move');
isMoveMode = !isMoveMode;
moveButton.classList.toggle('moving', isMoveMode);
const etaniItemRight = moveButton.closest('.etaniItemRight');
const etaniCol = moveButton.closest('.etaniCol');
const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
avItems.forEach(item => {
item.classList.toggle('moving-target', isMoveMode);
});
etaniCol.classList.toggle(`moving-mode-${animateType}`, isMoveMode);
if (isMoveMode) {
moveButton.title = "Click value item to select, then click another to move (Click again to cancel)";
} else {
moveButton.title = "Move Value";
selectedMoveItem = null;
avItems.forEach(item => {
item.classList.remove('moving-target');
item.classList.remove('selected-move');
});
}
}
// Handle click event for an .etaniAVItem in deletion, copy, or move mode
function handleAVItemClick(e, animateType = 'transform') {
const item = e.currentTarget;
const itemIndex = Array.from(item.parentNode.children).indexOf(item);
const etaniItemRight = item.closest('.etaniItemRight');
const etaniCol = item.closest('.etaniCol');
const useId = etaniItemRight.closest('.etaniItem').querySelector('.tileid').textContent;
const currentTime = Date.now();
const isDoubleClick = (item === lastClickedItem && (currentTime - lastClickTime) < 2000);
lastClickTime = currentTime;
lastClickedItem = item;
if (etaniCol.classList.contains(`moving-mode-${animateType}`)) {
const avItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
if (!selectedMoveItem) {
selectedMoveItem = item;
item.classList.add('selected-move');
return;
} else if (selectedMoveItem === item) {
selectedMoveItem.classList.remove('selected-move');
selectedMoveItem = null;
return;
} else {
const targetIndex = itemIndex;
const sourceIndex = Array.from(item.parentNode.children).indexOf(selectedMoveItem);
const parent = item.parentNode;
if (sourceIndex < targetIndex) {
if (item.nextSibling) {
parent.insertBefore(selectedMoveItem, item.nextSibling);
} else {
parent.appendChild(selectedMoveItem);
}
} else {
parent.insertBefore(selectedMoveItem, item);
}
if (animateType === 'transform') {
const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return;
const allAnimates = cloneUseElement.querySelectorAll('animateTransform');
allAnimates.forEach(animate => {
const values = animate.getAttribute('values').split(';');
const valueToMove = values[sourceIndex];
values.splice(sourceIndex, 1);
if (sourceIndex < targetIndex) {
values.splice(targetIndex, 0, valueToMove);
} else {
values.splice(targetIndex, 0, valueToMove);
}
animate.setAttribute('values', values.join(';'));
});
} else if (animateType === 'opacity') {
const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return;
const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
if (!animateOpacity) return;
const values = animateOpacity.getAttribute('values').split(';');
const valueToMove = values[sourceIndex];
values.splice(sourceIndex, 1);
if (sourceIndex < targetIndex) {
values.splice(targetIndex, 0, valueToMove);
} else {
values.splice(targetIndex, 0, valueToMove);
}
animateOpacity.setAttribute('values', values.join(';'));
}
selectedMoveItem.classList.remove('selected-move');
selectedMoveItem = null;
updateEtaniResult();
return;
}
}
if (etaniCol.classList.contains(`copying-mode-${animateType}`)) {
const etaniAV = item.parentNode;
const newAVItem = document.createElement('span');
newAVItem.className = 'etaniAVItem';
newAVItem.textContent = item.textContent;
newAVItem.addEventListener('click', (e) => handleAVItemClick(e, animateType));
etaniAV.insertBefore(newAVItem, item.nextSibling);
if (animateType === 'transform') {
const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return;
const allAnimates = cloneUseElement.querySelectorAll('animateTransform');
allAnimates.forEach(animate => {
const values = animate.getAttribute('values').split(';');
if (itemIndex < values.length) {
values.splice(itemIndex + 1, 0, values[itemIndex]);
animate.setAttribute('values', values.join(';'));
updateDuration(animate, etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`), values.length);
}
});
} else if (animateType === 'opacity') {
const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return;
const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
if (!animateOpacity) return;
const values = animateOpacity.getAttribute('values').split(';');
if (itemIndex < values.length) {
values.splice(itemIndex + 1, 0, values[itemIndex]);
animateOpacity.setAttribute('values', values.join(';'));
updateDuration(animateOpacity, etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`), values.length);
}
}
updateEtaniResult();
return;
}
if (etaniCol.classList.contains(`deleting-mode-${animateType}`)) {
const deleteButton = etaniItemRight.querySelector(`.etaniAVDelete[data-type="${animateType}"]`);
const etaniAnimate = etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`);
if (animateType === 'transform') {
const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return;
const allAnimates = cloneUseElement.querySelectorAll('animateTransform');
let newValuesLength = 0;
allAnimates.forEach((animate, index) => {
const values = animate.getAttribute('values').split(';');
if (itemIndex < values.length) {
values.splice(itemIndex, 1);
newValuesLength = values.length; // All animations will have the same new length
if (newValuesLength > 0) {
animate.setAttribute('values', values.join(';'));
// Only update duration on the first animate to avoid multiple calls
if (index === 0) {
updateDuration(animate, etaniAnimate, newValuesLength);
}
}
}
});
// If all values were removed, clean up
if (newValuesLength === 0 && allAnimates.length > 0) {
allAnimates.forEach(anim => anim.remove());
etaniAnimate.remove();
}
} else if (animateType === 'opacity') {
const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return;
const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
if (!animateOpacity) return;
const values = animateOpacity.getAttribute('values').split(';');
if (itemIndex < values.length) {
values.splice(itemIndex, 1);
if (values.length === 0) {
// Remove the opacity animation
animateOpacity.remove();
etaniAnimate.remove();
} else {
animateOpacity.setAttribute('values', values.join(';'));
updateDuration(animateOpacity, etaniItemRight.querySelector(`.etaniAnimate[data-type="${animateType}"]`), values.length);
}
}
}
item.remove();
const remainingItems = etaniItemRight.querySelectorAll(`.etaniAnimate[data-type="${animateType}"] .etaniAVItem`);
if (remainingItems.length === 0) {
// The .etaniAnimate element (containing the deleteButton) was just removed
// because it was the last item.
// We cannot .click() the button, as it's no longer in the DOM.
// Instead, we must manually reset the delete mode globally.
resetModes(animateType);
} else {
// If items remain, just re-apply the 'deleting-target' class to them.
remainingItems.forEach(item => {
item.classList.add('deleting-target');
});
}
updateEtaniResult();
} else if (animateType === 'opacity' && !etaniCol.classList.contains(`deleting-mode-${animateType}`) && !etaniCol.classList.contains(`copying-mode-${animateType}`)) {
const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return;
const animateOpacity = cloneUseElement.querySelector('animate[attributeName="opacity"]');
if (!animateOpacity) return;
const values = animateOpacity.getAttribute('values').split(';');
if (itemIndex >= values.length) return;
if (isDoubleClick) {
const newValue = prompt("Enter opacity value (0 to 1):", values[itemIndex]);
if (newValue !== null && !isNaN(newValue) && newValue >= 0 && newValue <= 1) {
values[itemIndex] = newValue;
item.textContent = newValue;
animateOpacity.setAttribute('values', values.join(';'));
updateEtaniResult();
}
} else {
values[itemIndex] = values[itemIndex] === '0' ? '1' : '0';
item.textContent = values[itemIndex];
animateOpacity.setAttribute('values', values.join(';'));
updateEtaniResult();
}
}
}
// Handle click event for the 'Transform' button
function handleAllAppendTransformClick() {
if (!etani_clone) return;
const etdropUses = document.querySelectorAll('#etmain .etdrop use');
const etaniItemRights = document.querySelectorAll('.etaniItemRight');
const isRepeat = document.querySelector('.etaniModeRepeat.active');
etdropUses.forEach((originalUseElement, i) => {
const useId = originalUseElement.getAttribute('href').substring(1);
const itemRight = etaniItemRights[i];
if (!itemRight) return;
// Check if transform animation already exists
if (itemRight.querySelector('.etaniAnimate[data-type="transform"]')) return;
let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return;
// Get the original transform values from the #etmain SVG
const originalTransformString = originalUseElement.getAttribute('transform') || '';
const originalTransforms = parseTransform(originalTransformString);
const baseAnimate = (type, initialValue) => {
const animate = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
animate.setAttribute('attributeName', 'transform');
animate.setAttribute('attributeType', 'XML');
animate.setAttribute('type', type);
animate.setAttribute('values', initialValue);
animate.setAttribute('fill', isRepeat ? 'remove' : 'freeze');
if (isRepeat) animate.setAttribute('repeatCount', 'indefinite');
// Add additive="sum" for scale and rotate, but not for translate
if (type === 'scale' || type === 'rotate') {
animate.setAttribute('additive', 'sum');
}
return animate;
};
// Add all three animations directly to the <use> element
// Order: translate, scale, rotate
cloneUseElement.appendChild(baseAnimate('translate', originalTransforms.translate));
cloneUseElement.appendChild(baseAnimate('scale', originalTransforms.scale));
cloneUseElement.appendChild(baseAnimate('rotate', originalTransforms.rotate));
// --- Start of HTML control creation ---
const etaniAnimate = document.createElement('div');
etaniAnimate.className = 'etaniAnimate';
etaniAnimate.setAttribute('data-type', 'transform');
const nameSpan = document.createElement('span');
nameSpan.className = 'etaniAnimateName';
nameSpan.textContent = 'transform';
const durSpan = document.createElement('span');
durSpan.className = 'etaniAnimateDur';
durSpan.textContent = 'dur: 0s';
durSpan.addEventListener('click', () => {
const currentDur = parseFloat(durSpan.textContent.replace('dur: ', '').replace('s', ''));
const newDur = prompt('Enter duration in seconds:', currentDur);
if (newDur !== null && !isNaN(newDur) && newDur >= 0) {
// Find animates inside the <use> element
const animates = etani_clone.querySelectorAll(`.etdrop use[href="#${useId}"] animateTransform`);
animates.forEach(animate => {
if (newDur > 0) {
animate.setAttribute('dur', `${newDur}s`);
if (isRepeat) {
animate.removeAttribute('fill');
animate.setAttribute('repeatCount', 'indefinite');
} else {
animate.removeAttribute('repeatCount');
animate.setAttribute('fill', 'freeze');
}
} else {
animate.removeAttribute('dur');
animate.removeAttribute('fill');
animate.removeAttribute('repeatCount');
}
});
durSpan.textContent = `dur: ${newDur}s`;
updateEtaniResult();
}
});
const etaniAnimateAttr = document.createElement('div');
etaniAnimateAttr.className = 'etaniAnimateAttr';
etaniAnimateAttr.appendChild(nameSpan);
etaniAnimateAttr.appendChild(durSpan);
etaniAnimate.appendChild(etaniAnimateAttr);
const frSpan = document.createElement('span');
frSpan.className = 'etaniAnimateFR';
frSpan.textContent = document.querySelector('.etaniModeMixed.active') ? 'freeze' : 'repeat';
frSpan.style.display = document.querySelector('.etaniModeMixed.active') ? 'inline-block' : 'none';
frSpan.addEventListener('click', () => {
const currentValue = frSpan.textContent;
const newValue = currentValue === 'freeze' ? 'repeat' : 'freeze';
frSpan.textContent = newValue;
const useId = itemRight.closest('.etaniItem').querySelector('.tileid').textContent;
// Find animates inside the <use> element
const animates = etani_clone.querySelectorAll(`.etdrop use[href="#${useId}"] animateTransform`);
animates.forEach(animate => {
if (animate.hasAttribute('fill') || animate.hasAttribute('repeatCount')) {
if (newValue === 'repeat') {
animate.removeAttribute('fill');
animate.setAttribute('repeatCount', 'indefinite');
} else {
animate.removeAttribute('repeatCount');
animate.setAttribute('fill', 'freeze');
}
}
});
updateEtaniResult();
});
etaniAnimateAttr.appendChild(frSpan);
// Add the attribute add button
const animateType = 'transform';
const attrAddSpan = document.createElement('span');
attrAddSpan.className = 'etaniAnimateAttrAdd';
attrAddSpan.textContent = '+';
attrAddSpan.addEventListener('click', (e) => showDropdown(e, animateType, useId));
etaniAnimateAttr.appendChild(attrAddSpan);
// Handle clicks on existing attribute spans
etaniAnimateAttr.addEventListener('click', (e) => {
if (e.target.classList.contains('etaniAnimateId')) {
editAttribute(e.target, animateType, useId, 'id');
} else if (e.target.classList.contains('etaniAnimateBegin')) {
editAttribute(e.target, animateType, useId, 'begin');
} else if (e.target.classList.contains('etaniAnimateOther')) {
editAttribute(e.target, animateType, useId, 'other', e.target.textContent.split('=')[0]);
}
});
const valueDiv = document.createElement('div');
valueDiv.className = 'etaniAnimateValue';
const avCtrlDiv = createControlButtons('transform', useId);
const avLabelSpan = document.createElement('span');
avLabelSpan.className = 'etaniAVLabel';
avLabelSpan.textContent = 'values : ';
const avDiv = document.createElement('div');
avDiv.className = 'etaniAV';
const avItemSpan = document.createElement('span');
avItemSpan.className = 'etaniAVItem';
avItemSpan.textContent = 'a';
avItemSpan.addEventListener('click', (e) => handleAVItemClick(e, 'transform'));
avDiv.appendChild(avItemSpan);
valueDiv.appendChild(avCtrlDiv);
valueDiv.appendChild(avLabelSpan);
valueDiv.appendChild(avDiv);
etaniAnimate.appendChild(valueDiv);
itemRight.appendChild(etaniAnimate);
});
updateEtaniResult();
}
// Handle click event for the 'Opacity' button
function handleAllAppendOpacityClick() {
if (!etani_clone) return;
const etdropUses = document.querySelectorAll('#etmain .etdrop use');
const etaniItemRights = document.querySelectorAll('.etaniItemRight');
const isRepeat = document.querySelector('.etaniModeRepeat.active');
etdropUses.forEach((originalUseElement, i) => {
const useId = originalUseElement.getAttribute('href').substring(1);
const itemRight = etaniItemRights[i];
if (!itemRight) return;
if (itemRight.querySelector('.etaniAnimate[data-type="opacity"]')) return;
let cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return;
const animateOpacity = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
animateOpacity.setAttribute('attributeName', 'opacity');
animateOpacity.setAttribute('values', '1');
if (isRepeat) {
animateOpacity.setAttribute('repeatCount', 'indefinite');
} else {
animateOpacity.setAttribute('fill', 'freeze');
}
cloneUseElement.appendChild(animateOpacity);
const etaniAnimate = document.createElement('div');
etaniAnimate.className = 'etaniAnimate';
etaniAnimate.setAttribute('data-type', 'opacity');
const nameSpan = document.createElement('span');
nameSpan.className = 'etaniAnimateName';
nameSpan.textContent = 'opacity';
const durSpan = document.createElement('span');
durSpan.className = 'etaniAnimateDur';
durSpan.textContent = 'dur: 0s';
durSpan.addEventListener('click', () => {
const currentDur = parseFloat(durSpan.textContent.replace('dur: ', '').replace('s', ''));
const newDur = prompt('Enter duration in seconds:', currentDur);
if (newDur !== null && !isNaN(newDur) && newDur >= 0) {
const animateOpacity = etani_clone.querySelector(`.etdrop use[href="#${useId}"] animate[attributeName="opacity"]`);
if (animateOpacity) {
if (newDur > 0) {
animateOpacity.setAttribute('dur', `${newDur}s`);
if (isRepeat) {
animateOpacity.removeAttribute('fill');
animateOpacity.setAttribute('repeatCount', 'indefinite');
} else {
animateOpacity.removeAttribute('repeatCount');
animateOpacity.setAttribute('fill', 'freeze');
}
} else {
animateOpacity.removeAttribute('dur');
animateOpacity.removeAttribute('fill');
animateOpacity.removeAttribute('repeatCount');
}
}
durSpan.textContent = `dur: ${newDur}s`;
updateEtaniResult();
}
});
const etaniAnimateAttr = document.createElement('div');
etaniAnimateAttr.className = 'etaniAnimateAttr';
etaniAnimateAttr.appendChild(nameSpan);
etaniAnimateAttr.appendChild(durSpan);
etaniAnimate.appendChild(etaniAnimateAttr);
const frSpan = document.createElement('span');
frSpan.className = 'etaniAnimateFR';
frSpan.textContent = document.querySelector('.etaniModeMixed.active') ? 'freeze' : 'repeat';
frSpan.style.display = document.querySelector('.etaniModeMixed.active') ? 'inline-block' : 'none';
frSpan.addEventListener('click', () => {
const currentValue = frSpan.textContent;
const newValue = currentValue === 'freeze' ? 'repeat' : 'freeze';
frSpan.textContent = newValue;
const useId = itemRight.closest('.etaniItem').querySelector('.tileid').textContent;
const animateOpacity = etani_clone.querySelector(`.etdrop use[href="#${useId}"] animate[attributeName="opacity"]`);
if (animateOpacity &&
(animateOpacity.hasAttribute('fill') || animateOpacity.hasAttribute('repeatCount'))) {
if (newValue === 'repeat') {
animateOpacity.removeAttribute('fill');
animateOpacity.setAttribute('repeatCount', 'indefinite');
} else {
animateOpacity.removeAttribute('repeatCount');
animateOpacity.setAttribute('fill', 'freeze');
}
}
updateEtaniResult();
});
etaniAnimateAttr.appendChild(frSpan);
// Add the attribute add button
const animateType = 'opacity';
const attrAddSpan = document.createElement('span');
attrAddSpan.className = 'etaniAnimateAttrAdd';
attrAddSpan.textContent = '+';
attrAddSpan.addEventListener('click', (e) => showDropdown(e, animateType, useId));
etaniAnimateAttr.appendChild(attrAddSpan);
// Handle clicks on existing attribute spans
etaniAnimateAttr.addEventListener('click', (e) => {
if (e.target.classList.contains('etaniAnimateId')) {
editAttribute(e.target, animateType, useId, 'id');
} else if (e.target.classList.contains('etaniAnimateBegin')) {
editAttribute(e.target, animateType, useId, 'begin');
} else if (e.target.classList.contains('etaniAnimateOther')) {
editAttribute(e.target, animateType, useId, 'other', e.target.textContent.split('=')[0]);
}
});
const valueDiv = document.createElement('div');
valueDiv.className = 'etaniAnimateValue';
const avCtrlDiv = createControlButtons('opacity', useId);
const avLabelSpan = document.createElement('span');
avLabelSpan.className = 'etaniAVLabel';
avLabelSpan.textContent = 'values : ';
const avDiv = document.createElement('div');
avDiv.className = 'etaniAV';
const avItemSpan = document.createElement('span');
avItemSpan.className = 'etaniAVItem';
avItemSpan.textContent = '1';
avItemSpan.addEventListener('click', (e) => handleAVItemClick(e, 'opacity'));
avDiv.appendChild(avItemSpan);
valueDiv.appendChild(avCtrlDiv);
valueDiv.appendChild(avLabelSpan);
valueDiv.appendChild(avDiv);
etaniAnimate.appendChild(valueDiv);
itemRight.appendChild(etaniAnimate);
});
updateEtaniResult();
}
// Handle click event for the 'Fill up values' button
function handleValueCtrlUpClick() {
if (!etani_clone) return;
const allAnimates = etani_clone.querySelectorAll('animateTransform, animate[attributeName="opacity"]');
let maxValuesLength = 0;
allAnimates.forEach(animate => {
const values = animate.getAttribute('values')?.split(';') || [];
maxValuesLength = Math.max(maxValuesLength, values.length);
});
allAnimates.forEach(animate => {
const values = animate.getAttribute('values')?.split(';') || [];
if (values.length < maxValuesLength && values.length > 0) {
const lastValue = values[values.length - 1];
while (values.length < maxValuesLength) {
values.push(lastValue);
}
animate.setAttribute('values', values.join(';'));
const useId = animate.closest('use')?.getAttribute('href')?.substring(1) ||
animate.closest('g[data-use-href]')?.getAttribute('data-use-href')?.substring(1);
if (useId) {
const etaniAnimate = document.querySelector(`.etaniItem:has(.tileid:where(:text("${useId}"))) .etaniAnimate[data-type="${animate.getAttribute('attributeName') === 'opacity' ? 'opacity' : 'transform'}"]`);
if (etaniAnimate) {
updateDuration(animate, etaniAnimate, values.length);
}
}
}
});
const etaniItemRights = document.querySelectorAll('.etaniItemRight');
etaniItemRights.forEach(itemRight => {
const useId = itemRight.closest('.etaniItem').querySelector('.tileid').textContent;
['transform', 'opacity'].forEach(animateType => {
const etaniAV = itemRight.querySelector(`.etaniAnimate[data-type="${animateType}"] .etaniAV`);
if (!etaniAV) return;
const currentItems = etaniAV.querySelectorAll('.etaniAVItem');
const currentValuesLength = currentItems.length;
if (currentValuesLength >= maxValuesLength) return;
const lastItem = currentItems[currentItems.length - 1];
let lastValue = lastItem.textContent;
for (let i = currentValuesLength; i < maxValuesLength; i++) {
const newAVItem = document.createElement('span');
newAVItem.className = 'etaniAVItem';
newAVItem.addEventListener('click', (e) => handleAVItemClick(e, animateType));
if (animateType === 'transform') {
newAVItem.textContent = lastValue; // Use the last letter instead of incrementing
} else if (animateType === 'opacity') {
newAVItem.textContent = lastValue;
}
etaniAV.appendChild(newAVItem);
}
});
});
updateEtaniResult();
}
// Handle setting mode switch (Repeat/Freeze)
function handleModeChange(mode) {
if (!etani_clone) return;
const animates = etani_clone.querySelectorAll('animateTransform, animate[attributeName="opacity"]');
const isRepeat = mode === 'repeat';
const isFreeze = mode === 'freeze';
const isMixed = mode === 'mixed';
if (!isMixed) {
animates.forEach(animate => {
if (animate.hasAttribute('fill') || animate.hasAttribute('repeatCount')) {
if (isRepeat) {
animate.removeAttribute('fill');
animate.setAttribute('repeatCount', 'indefinite');
} else if (isFreeze) {
animate.removeAttribute('repeatCount');
animate.setAttribute('fill', 'freeze');
}
}
});
}
document.querySelectorAll('.etaniMode span').forEach(span => {
span.classList.remove('active');
});
document.querySelector(`.etaniMode${mode.charAt(0).toUpperCase() + mode.slice(1)}`).classList.add('active');
const frElements = document.querySelectorAll('.etaniAnimateFR');
frElements.forEach(fr => {
fr.style.display = isMixed ? 'inline-block' : 'none';
});
updateEtaniResult();
}
// Create a single etaniItem in the UI
function createEtaniItem(tileid) {
const etaniCol = document.querySelector('.etaniCol');
if (!etaniCol) return;
// Check if item already exists
if (etaniCol.querySelector(`.etaniItem[data-use-id="${tileid}"]`)) {
return;
}
const etaniItem = document.createElement('div');
etaniItem.className = 'etaniItem';
etaniItem.setAttribute('data-use-id', tileid);
const etaniItemLeft = document.createElement('div');
etaniItemLeft.className = 'etaniItemLeft';
const etaniItemRight = document.createElement('div');
etaniItemRight.className = 'etaniItemRight';
// Use generateTileImage to get the thumbnail
const base64Url = generateTileImage(tileid);
if (base64Url) {
const imageOuter = document.createElement('div');
imageOuter.className = 'etaniItemImageOuter';
const img = document.createElement('img');
img.className = 'etaniItemImage';
img.src = base64Url;
img.alt = tileid;
imageOuter.appendChild(img);
etaniItemLeft.appendChild(imageOuter);
}
const tileidDiv = document.createElement('div');
tileidDiv.className = 'tileid';
tileidDiv.textContent = tileid;
etaniItemLeft.appendChild(tileidDiv);
etaniItem.appendChild(etaniItemLeft);
etaniItem.appendChild(etaniItemRight);
etaniCol.appendChild(etaniItem);
}
// Handle click event for the 'tiles' update button
function handleUpdateTilesClick(e) {
e.preventDefault();
if (!etani_clone) return;
const originalSvg = document.getElementById('etmain');
if (!originalSvg) return;
const originalDefs = originalSvg.querySelector('defs');
const cloneDefs = etani_clone.querySelector('defs');
const originalDrop = originalSvg.querySelector('.etdrop');
const cloneDrop = etani_clone.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));
}
// Add new etaniItem to UI
createEtaniItem(tileId);
}
});
// Refresh all tile images as defs might have changed
updateAllTileImages();
updateEtaniResult();
}
// Handle click event for the 'board' update button
function handleUpdateBoardClick(e) {
e.preventDefault();
if (!etani_clone) return;
const originalSvg = document.getElementById('etmain');
if (!originalSvg) return;
const originalBoard = originalSvg.querySelector('.etdrop .etboard');
const cloneBoard = etani_clone.querySelector('.etdrop .etboard');
if (originalBoard && cloneBoard) {
cloneBoard.replaceWith(originalBoard.cloneNode(true));
updateEtaniResult();
}
}
// Create the internal animation control structure
function createEtaniInner(etaniouter) {
const originalSvg = document.getElementById('etmain');
if (!originalSvg) {
console.error('SVG with ID "etmain" not found.');
return;
}
etani_clone = originalSvg.cloneNode(true);
const etwaitElement = etani_clone.querySelector('.etwait');
if (etwaitElement) {
etwaitElement.remove();
}
etani_clone.id = 'etmainani';
const etaniinner = document.createElement('div');
etaniinner.className = 'etaniinner';
etaniinner.id = 'etaniinner';
const etaniCtrl = document.createElement('div');
etaniCtrl.className = 'etaniCtrl';
const etaniContent = document.createElement('div');
etaniContent.className = 'etaniContent';
const contentHTMLLink = document.createElement('a');
contentHTMLLink.className = 'etaniContentHTML';
contentHTMLLink.textContent = 'Show HTML';
contentHTMLLink.href = 'javascript:;';
contentHTMLLink.addEventListener('click', handleContentHTMLClick);
etaniContent.appendChild(contentHTMLLink);
const centerLink = document.createElement('a');
centerLink.className = 'etaniCenter';
centerLink.textContent = 'Center';
centerLink.href = 'javascript:;';
centerLink.addEventListener('click', handleCenterClick);
etaniContent.appendChild(centerLink);
etaniCtrl.appendChild(etaniContent);
const etaniUpdate = document.createElement('div');
etaniUpdate.className = 'etaniUpdate';
const updateLabel = document.createElement('label');
updateLabel.textContent = 'update: ';
etaniUpdate.appendChild(updateLabel);
const updateTilesLink = document.createElement('a');
updateTilesLink.className = 'etaniUpdateTiles';
updateTilesLink.textContent = 'tiles';
updateTilesLink.href = 'javascript:;';
updateTilesLink.addEventListener('click', handleUpdateTilesClick);
etaniUpdate.appendChild(updateTilesLink);
const updateBoardLink = document.createElement('a');
updateBoardLink.className = 'etaniUpdateBoard';
updateBoardLink.textContent = 'board';
updateBoardLink.href = 'javascript:;';
updateBoardLink.addEventListener('click', handleUpdateBoardClick);
etaniUpdate.appendChild(updateBoardLink);
etaniCtrl.appendChild(etaniUpdate);
const etaniMode = document.createElement('div');
etaniMode.className = 'etaniMode';
const repeatRadio = document.createElement('span');
repeatRadio.className = 'etaniModeRepeat active';
repeatRadio.textContent = 'Repeat';
repeatRadio.setAttribute('data-mode', 'repeat');
repeatRadio.addEventListener('click', () => handleModeChange('repeat'));
const freezeRadio = document.createElement('span');
freezeRadio.className = 'etaniModeFreeze';
freezeRadio.textContent = 'Freeze';
freezeRadio.setAttribute('data-mode', 'freeze');
freezeRadio.addEventListener('click', () => handleModeChange('freeze'));
const mixedRadio = document.createElement('span');
mixedRadio.className = 'etaniModeMixed';
mixedRadio.textContent = 'Mixed';
mixedRadio.setAttribute('data-mode', 'mixed');
mixedRadio.addEventListener('click', () => handleModeChange('mixed'));
etaniMode.appendChild(repeatRadio);
etaniMode.appendChild(freezeRadio);
etaniMode.appendChild(mixedRadio);
etaniCtrl.appendChild(etaniMode);
const etaniAllAppend = document.createElement('div');
etaniAllAppend.className = 'etaniAllAppend';
const transformButton = document.createElement('button');
transformButton.className = 'etaniAllAppendTransform';
transformButton.textContent = 'transform';
transformButton.addEventListener('click', handleAllAppendTransformClick);
const opacityButton = document.createElement('button');
opacityButton.className = 'etaniAllAppendOpacity';
opacityButton.textContent = 'opacity';
opacityButton.addEventListener('click', handleAllAppendOpacityClick);
etaniAllAppend.appendChild(transformButton);
etaniAllAppend.appendChild(opacityButton);
etaniCtrl.appendChild(etaniAllAppend);
const etaniValueCtrl = document.createElement('div');
etaniValueCtrl.className = 'etaniValueCtrl';
const valueCtrlUpLink = document.createElement('a');
valueCtrlUpLink.className = 'etaniValueCtrlUp';
valueCtrlUpLink.textContent = 'fill up values';
valueCtrlUpLink.href = 'javascript:;';
valueCtrlUpLink.addEventListener('click', handleValueCtrlUpClick);
etaniValueCtrl.appendChild(valueCtrlUpLink);
etaniCtrl.appendChild(etaniValueCtrl);
const etaniCol = document.createElement('div');
etaniCol.className = 'etaniCol';
const etaniResult = document.createElement('div');
etaniResult.className = 'etaniResult';
const resultImg = document.createElement('img');
resultImg.className = 'etaniResultImage';
resultImg.alt = 'Rendered Ejtile Animation SVG';
const resultDRDiv = document.createElement('div');
resultDRDiv.className = 'etaniResultDR';
const downloadLink = document.createElement('a');
downloadLink.className = 'etaniResultDownload';
downloadLink.textContent = 'Download SVG';
downloadLink.href = 'javascript:;';
const renameLink = document.createElement('a');
renameLink.className = 'etaniResultRename';
renameLink.textContent = 'Rename File';
renameLink.href = 'javascript:;';
const sizeSpan = document.createElement('span');
sizeSpan.className = 'etaniResultSize';
resultDRDiv.appendChild(downloadLink);
resultDRDiv.appendChild(renameLink);
etaniResult.appendChild(resultImg);
etaniResult.appendChild(resultDRDiv);
etaniResult.appendChild(sizeSpan);
etaniinner.appendChild(etaniCtrl);
etaniinner.appendChild(etaniCol);
etaniinner.appendChild(etaniResult);
etaniouter.appendChild(etaniinner);
const etdropUses = document.querySelectorAll('.etdrop use');
etdropUses.forEach((useElement) => {
const tileid = useElement.getAttribute('href').substring(1);
createEtaniItem(tileid);
});
updateEtaniResult();
}
// Toggle the visibility and content of the animation control panel
function toggleAnimation(event) {
const etanibutton = event.currentTarget;
const etaniouter = etanibutton.parentNode;
const etaniinner = document.getElementById('etaniinner');
if (etanibutton.textContent === 'Animate it') {
etanibutton.textContent = 'Close Ejtile Ani';
createEtaniInner(etaniouter);
} else if (etanibutton.textContent === 'Close Ejtile Ani') {
etanibutton.textContent = 'Animate it';
if (etaniinner) {
etaniinner.remove();
}
etani_clone = null;
isCopyMode = false;
isMoveMode = false;
isDeleteMode = false;
selectedMoveItem = null;
lastClickTime = 0;
lastClickedItem = null;
}
}
// Create control buttons when animate is appending
function createControlButtons(animateType, useId) {
const avCtrlDiv = document.createElement('div');
avCtrlDiv.className = 'etaniAVCtrl';
const buttons = [
{
className: 'etaniAVAdd',
title: `Add ${animateType.charAt(0).toUpperCase() + animateType.slice(1)} Value`,
svg: {
lines: [
{ x1: '12', y1: '5', x2: '12', y2: '19' },
{ x1: '5', y1: '12', x2: '19', y2: '12' }
]
},
handler: (e) => handleAVAddClick(e, useId, animateType)
},
{
className: 'etaniAVDelete',
title: 'Delete Value',
svg: {
lines: [
{ x1: '5', y1: '12', x2: '19', y2: '12' }
]
},
handler: (e) => handleAVDeleteToggle(e, animateType)
},
{
className: 'etaniAVCopy',
title: 'Copy Value',
svg: {
rect: { x: '9', y: '9', width: '13', height: '13', rx: '2', ry: '2' },
path: { d: 'M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1' }
},
handler: (e) => handleAVCopyToggle(e, animateType)
},
{
className: 'etaniAVMove',
title: 'Move Value',
svg: {
path: { d: 'M 7,4 L 3,8 L 7,12 M 3,8 L 18,8 M 17,12 L 21,16 L 17,20 M 21,16 L 6,16' }
},
handler: (e) => handleAVMoveToggle(e, animateType)
}
];
buttons.forEach(button => {
const span = document.createElement('span');
span.className = button.className;
span.title = button.title;
span.setAttribute('data-type', animateType);
span.addEventListener('click', button.handler);
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '24');
svg.setAttribute('height', '24');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('fill', 'none');
svg.setAttribute('stroke', 'currentColor');
svg.setAttribute('stroke-width', '1');
svg.setAttribute('stroke-linecap', 'round');
svg.setAttribute('stroke-linejoin', 'round');
if (button.svg.lines) {
button.svg.lines.forEach(line => {
const lineElement = document.createElementNS('http://www.w3.org/2000/svg', 'line');
Object.entries(line).forEach(([key, value]) => lineElement.setAttribute(key, value));
svg.appendChild(lineElement);
});
}
if (button.svg.rect) {
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
Object.entries(button.svg.rect).forEach(([key, value]) => rect.setAttribute(key, value));
svg.appendChild(rect);
}
if (button.svg.path) {
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
Object.entries(button.svg.path).forEach(([key, value]) => path.setAttribute(key, value));
svg.appendChild(path);
}
span.appendChild(svg);
avCtrlDiv.appendChild(span);
});
return avCtrlDiv;
}
// Show dropdown menu for adding attributes
function showDropdown(e, animateType, useId) {
const attrAddSpan = e.target;
const existingDropdown = attrAddSpan.parentNode.querySelector('.etaniDropdown');
if (existingDropdown) {
// Toggle: if already open, remove it
existingDropdown.remove();
return;
}
const dropdown = document.createElement('div');
dropdown.className = 'etaniDropdown';
// Dynamically determine items based on existing attributes
const attrParent = attrAddSpan.parentNode;
const hasId = attrParent.querySelector('.etaniAnimateId');
const hasBegin = attrParent.querySelector('.etaniAnimateBegin');
const items = [];
if (!hasId) items.push('id');
if (!hasBegin) items.push('begin');
items.push('other');
items.forEach(item => {
const div = document.createElement('div');
div.className = 'etaniDropdownItem';
div.textContent = item;
div.addEventListener('click', () => {
showWindow(item, animateType, useId);
dropdown.remove(); // Remove after selecting item
});
dropdown.appendChild(div);
});
attrAddSpan.parentNode.style.position = 'relative'; // Ensure parent is relative for absolute positioning
attrAddSpan.parentNode.appendChild(dropdown);
dropdown.style.left = `${attrAddSpan.offsetLeft}px`;
dropdown.style.top = `${attrAddSpan.offsetTop + attrAddSpan.offsetHeight}px`;
}
// Show window for adding/editing attributes
function showWindow(type, animateType, useId, existingSpan = null, otherAttrName = null) {
const windowDiv = document.createElement('div');
windowDiv.className = 'etaniWindow';
const existingIds = getExistingIds();
const isEdit = !!existingSpan;
let input1, input2, existingIdsDiv, historyDiv, confirmBtn, deleteBtn, cancelBtn;
if (type === 'id') {
const label = document.createElement('label');
label.textContent = 'Enter ID:';
input1 = document.createElement('input');
input1.type = 'text';
if (isEdit) input1.value = existingSpan.textContent.replace('id=', '');
windowDiv.appendChild(label);
windowDiv.appendChild(input1);
existingIdsDiv = createIdsDisplay(existingIds, false);
windowDiv.appendChild(existingIdsDiv);
} else if (type === 'begin') {
const parsed = isEdit ? parseBeginValue(existingSpan.textContent) : { selected: [], plus: null, time: null };
existingIdsDiv = createIdsDisplay(existingIds, true, parsed.selected);
windowDiv.appendChild(existingIdsDiv);
// Create a div wrapper for plus input with text
const plusDiv = document.createElement('div');
plusDiv.className = 'etaniAnimateBeginPlusOuter';
plusDiv.textContent = 'plus: ';
const plusInput = document.createElement('input');
plusInput.type = 'number';
plusInput.min = '0';
plusInput.step = '0.1';
plusInput.className = 'etaniAnimateBeginPlus';
plusInput.style.width = '60px';
plusInput.style.display = 'inline';
if (isEdit) {
const parsed = parseBeginValue(existingSpan.textContent);
plusInput.value = parsed.plus || parsed.time || '';
}
plusDiv.appendChild(plusInput);
const sText = document.createTextNode(' s');
plusDiv.appendChild(sText);
windowDiv.appendChild(plusDiv);
} else if (type === 'other') {
const label1 = document.createElement('label');
label1.textContent = 'Attribute Name:';
input1 = document.createElement('input');
input1.type = 'text';
if (isEdit) input1.value = otherAttrName;
const label2 = document.createElement('label');
label2.textContent = 'Attribute Value:';
input2 = document.createElement('input');
input2.type = 'text';
if (isEdit) input2.value = existingSpan.textContent.split('=')[1];
windowDiv.appendChild(label1);
windowDiv.appendChild(input1);
windowDiv.appendChild(label2);
windowDiv.appendChild(input2);
}
const buttonsDiv = document.createElement('div');
confirmBtn = document.createElement('button');
confirmBtn.textContent = 'Confirm';
confirmBtn.addEventListener('click', () => handleConfirm(type, animateType, useId, input1, input2, existingIdsDiv, existingSpan, windowDiv));
buttonsDiv.appendChild(confirmBtn);
if (isEdit) {
deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.addEventListener('click', () => handleDelete(animateType, useId, type, otherAttrName, existingSpan, windowDiv));
buttonsDiv.appendChild(deleteBtn);
}
cancelBtn = document.createElement('button');
cancelBtn.textContent = 'Cancel';
cancelBtn.addEventListener('click', () => document.body.removeChild(windowDiv));
buttonsDiv.appendChild(cancelBtn);
windowDiv.appendChild(buttonsDiv);
document.body.appendChild(windowDiv);
}
// Create display for existing IDs with selection option
function createIdsDisplay(existingIds, selectable, selected = []) {
const div = document.createElement('div');
div.className = 'etaniExistingIds';
div.textContent = 'Existing IDs: ';
if (selectable) {
const zeroS = document.createElement('span');
zeroS.className = 'etaniIdItem zero-s';
zeroS.textContent = '0s';
zeroS.classList.toggle('selected', selected.includes('0s'));
zeroS.addEventListener('click', () => zeroS.classList.toggle('selected'));
div.appendChild(zeroS);
}
existingIds.forEach(id => {
const span = document.createElement('span');
span.className = 'etaniIdItem';
span.textContent = id;
if (selectable) {
span.addEventListener('click', () => {
if (span.classList.contains('selected')) {
// If already selected, deselect it
span.classList.remove('selected');
} else {
// Otherwise, deselect all other IDs and select this one
div.querySelectorAll('.etaniIdItem:not(.zero-s)').forEach(s => s.classList.remove('selected'));
span.classList.add('selected');
}
});
if (selected.includes(id)) span.classList.add('selected');
}
div.appendChild(span);
});
return div;
}
// Get all existing animation IDs globally, shared across types
function getExistingIds() {
const animates = etani_clone.querySelectorAll('animateTransform, animate[attributeName="opacity"]');
return Array.from(animates)
.map(animate => animate.getAttribute('id'))
.filter(id => id);
}
// Parse begin value to extract zeros, id, plus, and time
function parseBeginValue(beginStr) {
const selected = [];
let plus = null;
let time = null;
let id = null;
// Remove 'begin=' prefix if present
beginStr = beginStr.replace('begin=', '');
// Case 1: "0s"
if (beginStr === '0s') {
selected.push('0s');
}
// Case 2: "3s" (or any number s)
else if (/^\d+(\.\d+)?s$/.test(beginStr)) {
time = parseFloat(beginStr.replace('s', ''));
}
// Case 3: "0s;id.end"
else if (/^0s;[^.]+.end$/.test(beginStr)) {
const parts = beginStr.split(';');
selected.push('0s');
id = parts[1].replace('.end', '');
selected.push(id);
}
// Case 4: "id.end"
else if (/^[^.]+.end$/.test(beginStr)) {
id = beginStr.replace('.end', '');
selected.push(id);
}
// Case 6: "0s;id.end+3s"
else if (/^0s;[^.]+.end\+\d+(\.\d+)?s$/.test(beginStr)) {
const parts = beginStr.split(';');
selected.push('0s');
const [idPart, plusPart] = parts[1].split('+');
id = idPart.replace('.end', '');
selected.push(id);
plus = parseFloat(plusPart.replace('s', ''));
}
// Case 5: "id.end+3s"
else if (/^[^.]+.end\+\d+(\.\d+)?s$/.test(beginStr)) {
const [idPart, plusPart] = beginStr.split('+');
id = idPart.replace('.end', '');
selected.push(id);
plus = parseFloat(plusPart.replace('s', ''));
}
return { selected, plus, time };
}
// Handle confirm button for adding/editing
function handleConfirm(type, animateType, useId, input1, input2, existingIdsDiv, existingSpan, windowDiv) {
let value, attrName;
if (type === 'id') {
value = input1.value.trim();
// Validate: only allow alphanumeric and underscore, no spaces
if (!value || !/^[a-zA-Z0-9_]+$/.test(value) || getExistingIds().includes(value)) {
alert('ID must contain only letters, numbers, and underscores (no spaces)');
return;
}
attrName = 'id';
} else if (type === 'begin') {
const selectedZero = existingIdsDiv.querySelector('.zero-s.selected');
const selectedId = existingIdsDiv.querySelector('.etaniIdItem:not(.zero-s).selected');
const plusInput = windowDiv.querySelector('.etaniAnimateBeginPlus');
const plusValue = plusInput && plusInput.value ? parseFloat(plusInput.value) : null;
let parts = [];
if (selectedZero && selectedId) {
parts.push('0s');
} // Only push '0s' if there is a selectedId
let idPart = selectedId ? `${selectedId.textContent}.end` : null;
if (plusValue > 0) {
if (idPart) {
idPart += `+${plusValue}s`;
} else {
idPart = `${plusValue}s`; // If no id, use Xs directly, ignore 0s
}
}
if (idPart) {
parts.push(idPart);
}
value = parts.join(';');
// Handle case where only '0s' is selected without id or plus
if (!value && selectedZero) {
value = '0s';
}
if (!value) return; // No selection
attrName = 'begin';
} else if (type === 'other') {
attrName = input1.value.trim();
value = input2.value.trim();
if (!attrName || !value) return; // Empty
// Check duplicate attributes (simplified, assume no duplicates for same attrName)
}
const animates = getAnimates(animateType, useId, attrName);
animates.forEach(animate => animate.setAttribute(attrName, value));
const attrAddSpan = existingSpan ? existingSpan.parentNode.querySelector('.etaniAnimateAttrAdd') : document.querySelector(`.etaniItem[data-use-id="${useId}"] .etaniAnimate[data-type="${animateType}"] .etaniAnimateAttrAdd`);
const className = `etaniAnimate${type.charAt(0).toUpperCase() + type.slice(1)}`;
const newSpan = existingSpan || document.createElement('span');
newSpan.className = className;
newSpan.textContent = type === 'other' ? `${attrName}=${value}` : `${attrName}=${value}`;
if (!existingSpan) attrAddSpan.parentNode.insertBefore(newSpan, attrAddSpan);
updateEtaniResult();
document.body.removeChild(existingIdsDiv.closest('.etaniWindow'));
}
// Handle delete button
function handleDelete(animateType, useId, type, otherAttrName, existingSpan, windowDiv) {
const attrName = type === 'other' ? otherAttrName : type;
const animates = getAnimates(animateType, useId);
animates.forEach(animate => animate.removeAttribute(attrName));
existingSpan.remove();
updateEtaniResult();
if (document.body.contains(windowDiv)) {
document.body.removeChild(windowDiv);
}
}
// Get animate elements for the specific useId and type
function getAnimates(animateType, useId, attrType = null) {
if (animateType === 'transform') {
const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
if (!cloneUseElement) return [];
if (attrType === 'id') {
// Per original logic and example, 'id' applies only to the rotate animation
return cloneUseElement.querySelectorAll('animateTransform[type="rotate"]') || [];
} else {
// Other attributes (like 'begin') apply to all three
return cloneUseElement.querySelectorAll('animateTransform') || [];
}
} else {
const cloneUseElement = etani_clone.querySelector(`.etdrop use[href="#${useId}"]`);
return cloneUseElement?.querySelectorAll('animate[attributeName="opacity"]') || [];
}
}
// Edit existing attribute
function editAttribute(span, animateType, useId, type, otherAttrName) {
showWindow(type, animateType, useId, span, otherAttrName);
}
// Generate base64 image for a tile
function generateTileImage(tileid) {
const originalTile = document.querySelector(`defs g#${tileid}`);
if (!originalTile) return null;
const etdropUses = document.querySelectorAll('.etdrop use');
const etwaitGroups = document.querySelectorAll('.etwait g');
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 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.appendChild(tileclone);
const svgString = new XMLSerializer().serializeToString(svgWrapper);
return svgToBase64(svgString);
}
// Update all tile images in .etaniItemLeft
function updateAllTileImages() {
const etaniItems = document.querySelectorAll('.etaniItem');
etaniItems.forEach(item => {
const tileid = item.querySelector('.tileid').textContent;
const imageOuter = item.querySelector('.etaniItemImageOuter');
const img = imageOuter ? imageOuter.querySelector('.etaniItemImage') : null;
if (img) {
const base64Url = generateTileImage(tileid);
if (base64Url) {
img.src = base64Url;
}
}
});
}
// Initialize the animation control panel on window load
window.addEventListener('load', () => {
addDynamicStyles();
const etmainouter = document.getElementById('etmainouter');
if (etmainouter) {
const etaniouter = document.createElement('div');
etaniouter.className = 'etaniouter';
const etanibutton = document.createElement('button');
etanibutton.id = 'etanibutton';
etanibutton.textContent = 'Animate it';
etanibutton.addEventListener('click', toggleAnimation);
etaniouter.appendChild(etanibutton);
etmainouter.parentNode.insertBefore(etaniouter, etmainouter.nextSibling);
} else {
console.error('Div with ID "etmainouter" not found.');
}
});
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
Re: 正在做etani,ejtile的動畫套件
一,去掉.etaniUpdate內的label及其css。
二,把.etaniUpdate的css合並到.etaniMode等。
三,把點擊.etaniUpdateBoard的功能合並到.etaniUpdateTiles中,去掉.etaniUpdateBoard及相應的css,.etaniUpdateTiles的innerHTML改為「update」。
四,把.etaniCenter移到.etaniUpdate內部。
五,改css:
.etaniItemRight {min-height: 64px;}
.etaniItem {width: 100%; display: inline-block;}
六,把.etaniAllAppendTransform和.etaniAllAppendOpacity由button改成<a href="javascript:;">
請用繁體中文回答。只需告知所要更改的代碼和位置,以及要替換的內容。代碼的注釋都要是英文。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
弈趣極光:享受思維樂趣
在线用户
正浏览此版面之用户: Google [Bot] 和 1 访客

