代码: 全选
// 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;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2px 0;
}
.etaniItemRight {
margin-left: 60px;
padding: 7px;
min-height: 48px;
background-color: #fff;
}
.tileid {
text-align: center;
font-size: 12px;
word-break: break-all;
padding-top: 2px;
}
/* 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, .etaniHistory {
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.className = 'etanitileimg';
svgWrapper.appendChild(tileclone);
etaniItemLeft.appendChild(svgWrapper);
}
const tileidDiv = document.createElement('div');
tileidDiv.className = 'tileid';
tileidDiv.textContent = tileid;
etaniItemLeft.appendChild(tileidDiv);
etaniItem.appendChild(etaniItemLeft);
etaniItem.appendChild(etaniItemRight);
etaniCol.appendChild(etaniItem);
});
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') {
existingIdsDiv = createIdsDisplay(existingIds, true, isEdit ? parseBeginValue(existingSpan.textContent) : []);
windowDiv.appendChild(existingIdsDiv);
historyDiv = document.createElement('div');
historyDiv.className = 'etaniHistory';
historyDiv = document.createElement('div');
historyDiv.className = 'etaniHistory';
historyDiv.textContent = 'History: ';
const historyIds = getHistoryIds();
historyIds.forEach(id => {
const span = document.createElement('span');
span.className = 'etaniIdItem';
span.textContent = id;
span.addEventListener('click', () => {
span.remove(); // Remove from history UI on click
});
historyDiv.appendChild(span);
});
windowDiv.appendChild(historyDiv);
} 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));
buttonsDiv.appendChild(confirmBtn);
if (isEdit) {
deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.addEventListener('click', () => handleDelete(animateType, useId, type, otherAttrName, existingSpan));
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', () => {
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 selected items
function parseBeginValue(beginStr) {
return beginStr.split(';').map(v => v.endsWith('.end') ? v.replace('.end', '') : v);
}
// Handle confirm button for adding/editing
function handleConfirm(type, animateType, useId, input1, input2, existingIdsDiv, existingSpan) {
let value, attrName;
if (type === 'id') {
value = input1.value.trim();
if (!value || getExistingIds(animateType).includes(value)) return; // Invalid or duplicate
attrName = 'id';
} else if (type === 'begin') {
const selectedZero = existingIdsDiv.querySelector('.zero-s.selected');
const selectedId = existingIdsDiv.querySelector('.etaniIdItem:not(.zero-s).selected');
const parts = [];
if (selectedZero) parts.push('0s');
if (selectedId) parts.push(`${selectedId.textContent}.end`);
value = parts.join(';');
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) {
const attrName = type === 'other' ? otherAttrName : type;
const animates = getAnimates(animateType, useId);
animates.forEach(animate => animate.removeAttribute(attrName));
existingSpan.remove();
updateEtaniResult();
document.body.removeChild(existingSpan.closest('.etaniWindow'));
}
// 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);
}
// Get unique history IDs from all begin attributes
function getHistoryIds() {
const allAnimates = etani_clone.querySelectorAll('animateTransform, animate[attributeName="opacity"]');
const historySet = new Set();
allAnimates.forEach(animate => {
const begin = animate.getAttribute('begin');
if (begin) {
begin.split(';').forEach(part => {
const trimmed = part.trim();
if (trimmed.endsWith('.end')) {
historySet.add(trimmed.replace('.end', ''));
} else if (trimmed !== '0s') {
historySet.add(trimmed);
}
});
}
});
return Array.from(historySet);
}
// 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.');
}
});