代码: 全选
// Global variable to hold the SVG clone and ensure state is maintained
let etani = null;
let intervalMoving = null;
// Add dynamic CSS styles to the document
function addEtaniStyles() {
if (document.getElementById('dynamic-et-styles')) {
return;
}
const styleSheet = createEl('style');
styleSheet.id = 'dynamic-et-styles';
styleSheet.textContent = `
.etaniinner {
margin-top: 10px;
}
.etaniinner input[type=text] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
line-height: 1.5;
border: 2px solid #e2e8f0;
border-radius: 1px;
background-color: #ffffff;
color: #1a202c;
font-family: inherit;
font-weight: 500;
transition: all 0.2s ease;
&:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.15);
background-color: #f8faff;
}
&:hover {
border-color: #94a3b8;
}
}
.etaniCtrl {
margin-bottom: 10px;
clear: both;
padding: 5px;
border: 1px solid #c0c0c0;
text-align: center;
}
.etaniCtrl > div {
display: inline-block;
vertical-align: top;
padding: 5px;
border: 1px solid #ccc;
margin: 0 5px 5px 5px;
text-align: left;
}
.etaniCtrl > div > a, .etaniCtrl > div > span {
display: inline-block;
text-decoration: none;
padding: 2px 8px;
font-size: 14px;
margin: 0 2px;
cursor: pointer;
user-select: none;
}
.etaniCtrl > div > span {
border: 1px solid #888;
background-color: #eee;
color: #333;
}
.etaniCtrl > div > span.active {
background-color: #008CBA;
color: white;
border-color: #008CBA;
}
.etaniContentHTML {
border: 1px solid #db3a32;
color: #db3a32;
}
.etaniUpdateTiles {
border: 1px solid #008CBA;
color: #008CBA;
}
.etaniCenter {
border: 1px solid green;
color: green;
}
.etaniAllAppendTransform {
border: 1px solid #2e36b9;
color: #2e36b9;
}
.etaniAllAppendOpacity {
border: 1px solid #b68942;
color: #b68942;
}
.etaniValueIncrease {
border: 1px solid purple;
color: purple;
}
.etaniCol {
border: 1px solid #aaa;
padding: 5px;
margin-bottom: 10px;
clear: both;
user-select: none;
}
.etaniItem {
min-height: 48px;
border: 1px solid #ccc;
box-sizing: border-box;
width: 100%;
margin-bottom: -1px;
background-color: lightyellow;
display: inline-block;
}
.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;
}
.etaniItemId {
text-align: center;
font-size: 12px;
word-break: break-all;
margin-top: 2px;
cursor: pointer;
}
.etaniItemPlus {
width: 12px;
height: 12px;
display: inline-block;
margin-left: 2px;
}
.etaniItemRight {
margin-left: 60px;
padding: 7px;
min-height: 64px;
background-color: #fff;
}
.etaniWindow {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 24px;
border: 1px solid #ccc;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
z-index: 777;
width: auto;
display: inline-block;
}
.etaniAppendRow {
margin-bottom: 8px;
}
.etaniAppendRow a, .etaniAppendRow span {
display: inline-block;
padding: 4px 8px;
margin-right: 5px;
border: 1px solid #888;
background-color: #eee;
color: #333;
cursor: pointer;
text-decoration: none;
font-size: 12px;
}
.etaniAppendSet.active {
background-color: #008CBA;
color: white;
border-color: #008CBA;
}
.etaniWindow label {
display: inline;
margin-bottom: 10px;
}
.etaniAppendSpecify {
display: inline;
width: 120px;
box-sizing: border-box;
padding: 4px;
font-size: 12px;
}
.etaniWindow button {
margin-right: 10px;
padding: 5px 10px;
}
.etaniResult {
text-align: center;
margin-bottom: 10px;
padding: 10px;
border: 1px solid #bbb;
box-sizing: border-box;
}
.etaniResultImage {
display: block;
max-width: 480px;
width: 100%;
height: auto;
margin: 0 auto 10px auto;
border: 1px solid #000;
box-sizing: border-box;
}
.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;
}
.etaniResultSize {
display: inline-block;
margin-left: 10px;
font-size: 12px;
color: #555;
}
.etaniAnimate {
border: 1px solid #999;
padding: 5px;
margin-bottom: 5px;
}
.etaniAnimateAttr {
margin-bottom: 5px;
}
.etaniAnimateAttr > span {
cursor: pointer;
display: inline-block;
padding: 2px 5px;
font-size: 12px;
box-sizing: border-box;
border-width: 1px;
border-style: solid;
}
.etaniAnimateName {
background-color: #555;
border-color: #555;
color: white;
margin-right: 10px;
}
.etaniAnimateAttr > span:not(.etaniAnimateName) {
margin-right: 7px;
}
.etaniAnimateDur {
border-color: blue;
color: blue;
}
.etaniAnimateFR {
border-color: #78229f;
color: #78229f;
}
.etaniAnimateAttrAdd {
position: relative;
border-color: #2c8c12;
color: #2c8c12;
}
.etaniAVCtrl {
display: inline-block;
vertical-align: top;
margin-right: 5px;
margin-bottom: 3px;
}
.etaniAVCtrl > span {
display: inline-block;
width: 24px;
height: 24px;
cursor: pointer;
vertical-align: top;
margin-right: 3px;
box-sizing: border-box;
}
.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.selected {
background-color: #779933;
}
.etaniAVAdd {
background-color: #a7fca7;
border: 1px solid #71c371;
}
.etaniAVDelete {
background-color: #ffcccc;
border: 1px solid #cc3333;
}
.etaniAVCopy {
background-color: #ccccff;
border: 1px solid #6666cc;
}
.etaniAVMove {
background-color: #ffcc99;
border: 1px solid #cc9966;
}
.etaniAVDelete.active {
background-color: #cc3333;
color: white;
}
.etaniAVCopy.active {
background-color: #6666cc;
color: white;
}
.etaniAVMove.active {
background-color: #cc9966;
color: white;
}
.etaniAVCtrl > span > svg {
margin-left: -1px;
margin-top: -1px;
}
textarea.etaniHTMLTextarea {
width: calc(100vw - 72px);
height: calc(50vh - 24px);
resize: none;
border: 1px solid #ccc;
font-size: 12px;
box-sizing: border-box;
}
.etaniWindowRow {
padding-top: 12px;
text-align: center;
}
.etaniWindowRow button {
margin: 0 12px;
}
.editDurInput {
width: 36px;
}
.etaniEditDur {
text-align: center;
}
.editInput {
width: 120px;
}
.etaniAnimateAttrRemove {
float: right;
margin-right: 0 !important;
border-color: #c33;
color: #861616;
background: #f6cdcd;
}
.etaniDropdown {
position: absolute;
left: 0px;
background-color: #fff;
border: 1px solid #ccc;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
z-index: 1200;
}
.etaniDropdownItem {
color: black;
padding: 8px 12px;
cursor: pointer;
font-size: 12px;
}
.etaniDropdownItem:hover {
color: green;
background-color: #f0f0f0;
}
.etaniIdListSpan {
cursor: pointer;
border: 1px solid #420664;
padding: 2px 5px;
font-size: 12px;
color: #420664;
margin: 3px;
}
.etaniIdListSpan.selected {
background: #420664;
color: white;
}
.etaniIdListDiv {
max-width: 360px;
margin-top: 7px;
}
.etaniAnimateId {
border-color: #420664;
color: #420664;
}
.etaniAttrRemove {
cursor: pointer;
padding: 2px 5px;
font-size: 12px;
box-sizing: border-box;
border-width: 1px;
border-style: solid;
float: right;
margin-right: 0 !important;
border-color: #c33;
color: #861616;
background: #f6cdcd;
}
.editBeginStartInput {
width: 36px;
}
.beginOption {
border: 1px solid #777;
background-color: #eee;
color: #333;
display: inline-block;
text-decoration: none;
padding: 2px 8px;
font-size: 14px;
margin: 0 2px;
cursor: pointer;
user-select: none;
}
.beginOption.selected {
background-color: #008CBA;
color: white;
border-color: #008CBA;
}
.endOrBeginDiv {
margin-top: 7px;
}
`;
document.head.appendChild(styleSheet);
}
// Append ctrl elements to etaniCtrl
function addEtaniCtrlElements(etaniinner) {
// Create the etaniCtrl element
const etaniCtrl = createEl('div', 'etaniCtrl');
etaniinner.appendChild(etaniCtrl);
// Create etaniContent div
const etaniContent = createEl('div', 'etaniContent');
// Create etaniContentHTML a
const etaniContentHTML = createEl('a', 'etaniContentHTML');
etaniContentHTML.href = 'javascript:;';
etaniContentHTML.textContent = 'HTML';
etaniContentHTML.onclick = handleContentHTMLClick;
etaniContent.appendChild(etaniContentHTML);
// Append to etaniCtrl
etaniCtrl.appendChild(etaniContent);
// Create etaniUpdate div
const etaniUpdate = createEl('div', 'etaniUpdate');
// Create etaniUpdateTiles a
const etaniUpdateTiles = createEl('a', 'etaniUpdateTiles');
etaniUpdateTiles.href = 'javascript:;';
etaniUpdateTiles.textContent = 'update';
etaniUpdate.appendChild(etaniUpdateTiles);
// Create etaniCenter a
const etaniCenter = createEl('a', 'etaniCenter');
etaniCenter.href = 'javascript:;';
etaniCenter.textContent = 'Center';
etaniUpdate.appendChild(etaniCenter);
// Append to etaniCtrl
etaniCtrl.appendChild(etaniUpdate);
// Create etaniFilter div
const etaniFilter = createEl('div', 'etaniFilter');
// Create etaniFilterTiles span
const etaniFilterTiles = createEl('span', 'etaniFilterTiles active', 'tiles');
etaniFilter.appendChild(etaniFilterTiles);
// Create etaniFilterMoving span
const etaniFilterMoving = createEl('span', 'etaniFilterMoving', 'moving');
etaniFilter.appendChild(etaniFilterMoving);
// Create etaniFilterBoard span
const etaniFilterBoard = createEl('span', 'etaniFilterBoard', 'board');
etaniFilter.appendChild(etaniFilterBoard);
// Append to etaniCtrl
etaniCtrl.appendChild(etaniFilter);
// Add click event listeners for etaniFilter spans
const filterSpans = etaniFilter.querySelectorAll('span');
filterSpans.forEach(function(span) {
span.onclick = function() {
// Remove active from all spans in etaniFilter
filterSpans.forEach(function(s) { s.classList.remove('active'); });
// Add active to clicked span
this.classList.add('active');
// Update visibility after filter change
updateVisibility();
const active = document.querySelector('.etaniFilter .active');
if (active && active.classList[0] === 'etaniFilterMoving') {
if (intervalMoving) clearInterval(intervalMoving);
intervalMoving = setInterval(updateMovingTiles, 1000);
// Call immediately
updateMovingTiles();
} else {
if (intervalMoving) clearInterval(intervalMoving);
intervalMoving = null;
}
};
});
// Create etaniMode div
const etaniMode = createEl('div', 'etaniMode');
// Create etaniModeRepeat span
const etaniModeRepeat = createEl('span', 'etaniModeRepeat active', 'repeat');
etaniMode.appendChild(etaniModeRepeat);
// Create etaniModeFreeze span
const etaniModeFreeze = createEl('span', 'etaniModeFreeze', 'freeze');
etaniMode.appendChild(etaniModeFreeze);
// Create etaniModeMixed span
const etaniModeMixed = createEl('span', 'etaniModeMixed', 'mixed');
etaniMode.appendChild(etaniModeMixed);
// Append to etaniCtrl
etaniCtrl.appendChild(etaniMode);
// Add click event listeners for etaniMode spans
const modeSpans = etaniMode.querySelectorAll('span');
modeSpans.forEach(function(span) {
span.onclick = function() {
// Remove active from all spans in etaniMode
modeSpans.forEach(function(s) { s.classList.remove('active'); });
// Add active to clicked span
this.classList.add('active');
};
});
// Create etaniAllAppend div
const etaniAllAppend = createEl('div', 'etaniAllAppend');
// Create etaniAllAppendTransform a
const etaniAllAppendTransform = createEl('a', 'etaniAllAppendTransform');
etaniAllAppendTransform.href = 'javascript:;';
etaniAllAppendTransform.textContent = 'transform';
etaniAllAppend.appendChild(etaniAllAppendTransform);
// Create etaniAllAppendOpacity a
const etaniAllAppendOpacity = createEl('a', 'etaniAllAppendOpacity');
etaniAllAppendOpacity.href = 'javascript:;';
etaniAllAppendOpacity.textContent = 'opacity';
etaniAllAppend.appendChild(etaniAllAppendOpacity);
// Append to etaniCtrl
etaniCtrl.appendChild(etaniAllAppend);
// Create etaniValue div
const etaniValue = createEl('div', 'etaniValue');
// Create etaniValueIncrease a
const etaniValueIncrease = createEl('a', 'etaniValueIncrease');
etaniValueIncrease.href = 'javascript:;';
etaniValueIncrease.textContent = 'increase';
etaniValue.appendChild(etaniValueIncrease);
// Append to etaniCtrl
etaniCtrl.appendChild(etaniValue);
}
// Define updateMovingTiles
function updateMovingTiles() {
const movingUse = document.querySelector('#etmain > .etdrop > use.tilemoving');
const items = document.querySelectorAll('.etaniItem');
items.forEach(function(item) {
item.style.display = 'none';
});
if (movingUse) {
const id = movingUse.getAttribute('href').slice(1);
const item = document.querySelector('.etaniItem[data-id="' + id + '"]');
if (item) {
item.style.display = 'block';
}
}
}
// list Etani elements
function listEtaniItems() {
// Get the etaniCol element
let etaniCol = document.querySelector('.etaniCol');
if (!etaniCol) return; // Exit if etaniCol not found
// Clear existing content in etaniCol
etaniCol.innerHTML = '';
// Collect elements into etaniElementArray
let uses = etani.querySelectorAll('.etdrop > use');
let etanidrop = etani.getElementById('etanidrop');
let boardElements = etani.querySelectorAll('.etdrop > .etboard [id]');
let etaniElementArray = [...uses, etanidrop, ...boardElements];
// Loop through etaniElementArray to create etaniItem elements
etaniElementArray.forEach(function(element) {
if (!element) return; // Skip if element is null
// Determine itemId and dataType
let itemId;
let dataType;
if (element.tagName === 'use') {
itemId = element.getAttribute('href').slice(1);
dataType = 'tile';
} else if (element.id === 'etanidrop') {
itemId = 'etanidrop';
dataType = 'board';
} else {
itemId = element.id;
dataType = 'board';
}
// Create etaniItem div
const etaniItem = createEl('div', 'etaniItem');
etaniItem.dataset.id = itemId;
etaniItem.dataset.type = dataType;
// Create etaniItemLeft div
const etaniItemLeft = createEl('div', 'etaniItemLeft');
etaniItem.appendChild(etaniItemLeft);
// Create etaniItemImageOuter div
const etaniItemImageOuter = createEl('div', 'etaniItemImageOuter');
etaniItemLeft.appendChild(etaniItemImageOuter);
if (dataType === 'tile') {
// Create etaniItemImage img
const etaniItemImage = createEl('img', 'etaniItemImage');
etaniItemImage.src = generateTileImage(itemId);
etaniItemImageOuter.appendChild(etaniItemImage);
} else {
// Generate background color
etaniItemImageOuter.style.background = generateHexColor(itemId);
}
// Create etaniItemId div
const etaniItemId = createEl('div', 'etaniItemId');
etaniItemId.onclick = function() {
etaniAppendAnimateWindow(itemId, dataType);
};
etaniItemLeft.appendChild(etaniItemId);
// Create etaniItemName span
const itemName = itemId === 'etanidrop' ? 'board' : itemId;
const etaniItemName = createEl('span', 'etaniItemName', itemName);
etaniItemId.appendChild(etaniItemName);
// Create etaniItemPlus img
const etaniItemPlus = createEl('span', 'etaniItemPlus');
etaniItemPlus.innerHTML = '<svg width="12" height="12" fill="none" stroke-width="1" stroke="darkgreen"><line x1="6" y1="0" x2="6" y2="12"></line><line x1="0" y1="6" x2="12" y2="6"></line></svg>';
etaniItemId.appendChild(etaniItemPlus);
// Create etaniItemRight div
const etaniItemRight = createEl('div', 'etaniItemRight');
etaniItem.appendChild(etaniItemRight);
// Append etaniItem to etaniCol
etaniCol.appendChild(etaniItem);
});
}
// Define updateVisibility
function updateVisibility() {
const active = document.querySelector('.etaniFilter .active');
if (!active) return;
const activeClass = active.classList[0]; // e.g., 'etaniFilterTiles'
const items = document.querySelectorAll('.etaniItem');
items.forEach(function(item) {
item.style.display = 'none';
});
if (activeClass === 'etaniFilterTiles') {
document.querySelectorAll('.etaniItem[data-type="tile"]').forEach(function(item) {
item.style.display = 'block';
});
} else if (activeClass === 'etaniFilterBoard') {
document.querySelectorAll('.etaniItem[data-type="board"]').forEach(function(item) {
item.style.display = 'block';
});
} else if (activeClass === 'etaniFilterMoving') {
document.querySelectorAll('.etaniItem[data-type="board"]').forEach(function(item) {
item.style.display = 'block';
});
// Tiles visibility handled by interval
}
}
// Convert SVG string to a Base64 data URL
function svgToBase64(svgString) {
const base64 = btoa(unescape(encodeURIComponent(svgString)));
return `data:image/svg+xml;base64,${base64}`;
}
// Define start button click
function etaniStartClick() {
const etaniouter = document.querySelector('.etaniouter');
const originalSvg = document.getElementById('etmain');
if (!originalSvg) {
console.error('SVG with ID "etmain" not found.');
return;
}
etani = originalSvg.cloneNode(true);
const etwaitElement = etani.querySelector('.etwait');
if (etwaitElement) {
etwaitElement.remove();
}
etani.id = 'etani';
const etdropClone = etani.querySelector('.etdrop');
if (etdropClone) {
etdropClone.id = 'etanidrop';
}
// Check if etaniinner already exists
let etaniinner = etaniouter.querySelector('.etaniinner');
if (etaniinner) {
// Remove etaniinner
etaniouter.removeChild(etaniinner);
// Restore button text
etaniStart.textContent = 'start ejtile animate';
} else {
// Create etaniinner div
etaniinner = createEl('div', 'etaniinner');
// Create etaniCtrl div
addEtaniCtrlElements(etaniinner);
// Create etaniCol div
const etaniCol = createEl('div', 'etaniCol');
etaniinner.appendChild(etaniCol);
// Create etaniResult div
const etaniResult = createEl('div', 'etaniResult');
etaniinner.appendChild(etaniResult);
// Append etaniinner to etaniouter
etaniouter.appendChild(etaniinner);
// Change button text
etaniStart.textContent = 'close ejtile animate';
// list etani elements
listEtaniItems();
// update etani elements display by etaniFilter
updateVisibility();
// update result
updateEtaniResult();
}
}
// Generate base64 image for a tile
function generateTileImage(tileid) {
const originalTile = document.querySelector(`#etmain > defs > g#${tileid}`);
if (!originalTile) return null;
const etdropUses = document.querySelectorAll('#etmain > .etdrop > use');
const etwaitGroups = document.querySelectorAll('#etmain > .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.appendChild(tileclone);
const svgString = new XMLSerializer().serializeToString(svgWrapper);
return svgToBase64(svgString);
}
// generate hex color
function generateHexColor(seed) {
let hash = 0;
for (let i = 0; i < seed.length; i++) {
hash = seed.charCodeAt(i) + ((hash << 5) - hash);
}
let color = (hash & 0x00FFFFFF).toString(16).toUpperCase();
while (color.length < 6) {
color = '0' + color;
}
return '#' + color;
}
// Handle the HTML popup window
function handleContentHTMLClick() {
if (!etani) return;
const textarea = createEl('textarea', 'etaniHTMLTextarea');
textarea.value = new XMLSerializer().serializeToString(etani);
etaniWindow(textarea, () => {
const parser = new DOMParser();
etani = parser.parseFromString(textarea.value, 'image/svg+xml');
updateEtaniResult();
});
}
// TODO
// Handle click event for the update button
function handleUpdateTilesClick(e) {
e.preventDefault();
if (!etani) 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);
}
});
// Logic from handleUpdateBoardClick (merging board update) (Inst 3)
const originalSvg_board = document.getElementById('etmain');
if (originalSvg_board) {
const originalBoard = originalSvg_board.querySelector('.etdrop .etboard');
const cloneBoard = etani_clone.querySelector('.etdrop .etboard');
if (originalBoard && cloneBoard) {
cloneBoard.replaceWith(originalBoard.cloneNode(true));
}
}
// End of merged logic
// Refresh all tile images as defs might have changed
updateAllTileImages();
updateEtaniResult();
}
// Open a generic window
function etaniWindow(content, confirm_function = null) {
// Get the .etaniinner element
const inner = document.querySelector('.etaniinner');
if (!inner) {
console.error('Element with class .etaniinner not found.');
return;
}
let windowDiv = inner.querySelector('.etaniWindow');
if (windowDiv) return;
// Create the main div.etaniWindow
windowDiv = createEl('div', 'etaniWindow');
// Append custom content to the window
if (typeof content === 'string') {
windowDiv.innerHTML = content;
} else if (content instanceof Element) {
windowDiv.appendChild(content);
} else {
console.error('Invalid content type provided.');
return;
}
// Add confirm and cancel buttons if not already in content
let btnConfirm = windowDiv.querySelector('button.confirm'); // Assume class or id for identification
let btnCancel = windowDiv.querySelector('button.cancel');
if (!btnConfirm || !btnCancel) {
const rowButtons = createEl('div', 'etaniWindowRow');
btnConfirm = createEl('button', 'confirm', 'Confirm');
btnCancel = createEl('button', 'cancel', 'Cancel');
rowButtons.appendChild(btnConfirm);
rowButtons.appendChild(btnCancel);
windowDiv.appendChild(rowButtons);
}
// Append the windowDiv to .etaniinner
inner.appendChild(windowDiv);
// Add event listeners
if (btnConfirm) {
btnConfirm.onclick = function() {
if (typeof confirm_function === 'function') {
confirm_function();
}
windowDiv.remove();
};
}
if (btnCancel) {
btnCancel.onclick = function() {
windowDiv.remove();
};
}
}
// Open append animate window
function etaniAppendAnimateWindow(id, dataType) {
const appendAnimateWindow = createEl('div', 'appendAnimateWindow');
appendAnimateWindow.dataset.id = id;
appendAnimateWindow.dataset.type = dataType;
// First row
const row1 = createEl('div', 'etaniAppendRow');
const strong = createEl('strong', null, 'id: ' + id);
row1.appendChild(strong);
appendAnimateWindow.appendChild(row1);
// Second row
const row2 = createEl('div', 'etaniAppendRow');
const aTransform = createEl('a', 'etaniAppendTransform', 'transform');
aTransform.href = 'javascript:;';
const aMotion = createEl('a', 'etaniAppendMotion', 'motion');
aMotion.href = 'javascript:;';
const spanSet = createEl('span', 'etaniAppendSet', 'set');
row2.appendChild(aTransform);
row2.appendChild(aMotion);
row2.appendChild(spanSet);
appendAnimateWindow.appendChild(row2);
// Third row
const row3 = createEl('div', 'etaniAppendRow');
const aOpacity = createEl('a', 'etaniAppendOpacity', 'opacity');
aOpacity.href = 'javascript:;';
const aWriting = createEl('a', 'etaniAppendWriting', 'writing');
aWriting.href = 'javascript:;';
row3.appendChild(aOpacity);
row3.appendChild(aWriting);
appendAnimateWindow.appendChild(row3);
// Fourth row
const row4 = createEl('div', 'etaniAppendRow');
const aFill = createEl('a', 'etaniAppendFill', 'fill');
aFill.href = 'javascript:;';
const aStroke = createEl('a', 'etaniAppendStroke', 'stroke');
aStroke.href = 'javascript:;';
const aWidth = createEl('a', 'etaniAppendWidth', 'width');
aWidth.href = 'javascript:;';
row4.appendChild(aFill);
row4.appendChild(aStroke);
row4.appendChild(aWidth);
appendAnimateWindow.appendChild(row4);
// Fifth row
const row5 = createEl('div', 'etaniAppendRow');
const label = createEl('label', null, 'specify: ');
const input = createEl('input', 'etaniAppendSpecify');
input.type = 'text';
row5.appendChild(label);
row5.appendChild(input);
appendAnimateWindow.appendChild(row5);
etaniWindow(appendAnimateWindow);
// Add click events to all specified elements
// For a elements with href='javascript:;'
aTransform.onclick = etaniAppendAnimate;
aMotion.onclick = etaniAppendAnimate;
aOpacity.onclick = etaniAppendAnimate;
aWriting.onclick = etaniAppendAnimate;
aFill.onclick = etaniAppendAnimate;
aStroke.onclick = etaniAppendAnimate;
aWidth.onclick = etaniAppendAnimate;
// For span.etaniAppendSet
spanSet.onclick = function() {
if (spanSet.classList.contains('active')) {
this.classList.remove('active');
aTransform.style.pointerEvents = 'auto';
aTransform.style.opacity = '1'; // Add disabled CSS state
aMotion.style.pointerEvents = 'auto';
aMotion.style.opacity = '1'; // Add disabled CSS state
} else {
this.classList.add('active');
aTransform.style.pointerEvents = 'none';
aTransform.style.opacity = '0.5'; // Add disabled CSS state
aMotion.style.pointerEvents = 'none';
aMotion.style.opacity = '0.5'; // Add disabled CSS state
}
};
}
// Update the result section with the current state of etani
function updateEtaniResult() {
if (!etani) return;
let etaniResult = document.querySelector('.etaniResult');
if (!etaniResult) return;
const svgString = new XMLSerializer().serializeToString(etani);
const sizeInBytes = new Blob([svgString]).size;
const base64Url = svgToBase64(svgString);
// Generate default filename with current date and time
const now = new Date();
const defaultFilename = `ejtileAnimation_${now.toISOString().replace(/[-:T]/g, '').slice(0, 15)}.svg`;
let imgElement = document.querySelector('.etaniResultImage');
let downloadElementOuter = document.querySelector('.etaniResultDR');
let downloadElement = document.querySelector('.etaniResultDownload');
let renameElement = document.querySelector('.etaniResultRename');
let sizeElement = document.querySelector('.etaniResultSize');
if (!imgElement) {
imgElement = createEl('img', 'etaniResultImage');
imgElement.alt = 'Rendered Ejtile Animation SVG';
etaniResult.appendChild(imgElement);
}
if (!downloadElementOuter) {
downloadElementOuter = createEl('div', 'etaniResultDR');
}
if (!downloadElement) {
downloadElement = createEl('a', 'etaniResultDownload', 'Download SVG');
downloadElement.href = 'javascript:;';
downloadElementOuter.appendChild(downloadElement);
etaniResult.appendChild(downloadElementOuter);
}
if (!renameElement) {
renameElement = createEl('a', 'etaniResultRename', 'Rename File');
renameElement.href = 'javascript:;';
downloadElementOuter.appendChild(renameElement);
etaniResult.appendChild(downloadElementOuter);
}
if (!sizeElement) {
sizeElement = createEl('span', 'etaniResultSize');
etaniResult.appendChild(sizeElement);
}
if (imgElement && downloadElement && renameElement && sizeElement) {
imgElement.src = base64Url;
sizeElement.textContent = `Size: ${sizeInBytes} byte`;
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}`);
}
};
}
}
// create element and set className
function createEl(tag, className, textContent) {
const el = document.createElement(tag);
if (className) {
el.className = className;
}
if (textContent) {
el.textContent = textContent;
}
return el;
}
// parse transform values
function parseTransformValues(transformStr) {
const transforms = {
translate: '0,0',
scale: '1,1',
rotate: '0'
};
if (!transformStr) {
return transforms;
}
// Extract translate values
const translateMatch = /translate\(([^)]+)\)/.exec(transformStr);
if (translateMatch) {
transforms.translate = translateMatch[1].trim();
}
// Extract scale values
const scaleMatch = /scale\(([^)]+)\)/.exec(transformStr);
if (scaleMatch) {
transforms.scale = scaleMatch[1].trim();
}
// Extract rotate values
const rotateMatch = /rotate\(([^)]+)\)/.exec(transformStr);
if (rotateMatch) {
transforms.rotate = rotateMatch[1].trim();
}
return transforms;
}
// append etaniAnimate to etaniItemRight
function appendAnimateToResult(id, elementtype, tagname, animatetype, defaultvalue = null) {
// 1. Find the SVG insertion target using the 'etani' global variable
let targetSVGParent;
if (elementtype === 'tile') {
// For 'tile', the target is the <use> element in .etdrop
targetSVGParent = etani.querySelector(`.etdrop > use[href="#${id}"]`);
} else if (elementtype === 'board') {
// For 'board', the target is the element (e.g., <g>) with the matching id
targetSVGParent = etani;
}
if (!targetSVGParent) {
console.error(`Error: SVG target parent (href="#${id}" or "#${id}") not found in 'etani' variable.`);
return;
}
// 2. Determine repeat or fill attributes from the UI state
const repeatModeActive = document.querySelector('.etaniModeRepeat.active');
let repeatOrFillAttrs = {};
if (tagname === 'set') {
// 'set' never has repeatCount
if (!repeatModeActive) {
repeatOrFillAttrs = { fill: 'freeze' };
}
} else {
// Other animation types
if (repeatModeActive) {
repeatOrFillAttrs = { repeatCount: 'indefinite' };
} else {
repeatOrFillAttrs = { fill: 'freeze' };
}
}
// if type is board, it must has the 'href' attribute
let boardHref = {};
if (elementtype === 'board') {
boardHref = { href: `#${id}` };
}
// Special attribute
let targetEl;
if (elementtype === 'tile') {
targetEl = etani.querySelector(`.etdrop > use[href="#${id}"]`);
} else {
targetEl = etani.getElementById(id);
}
// Writting animation
if (animatetype === 'stroke-dasharray') {
// get the writing length
let tPath = etani.getElementById(id);
tPath = elementtype === 'tile' ? tPath.firstChild : tPath;
let targetLength = Math.round(tPath.getTotalLength());
// set dashoffset
targetEl.setAttribute('stroke-dashoffset', targetLength);
// set defaultvalue
defaultvalue = targetLength + ';' + (targetLength * 2);
} else if (animatetype === 'fill') {
// get the original color
let targetElFill = targetEl.getAttribute('fill');
if (targetElFill) defaultvalue = targetElFill;
}
// 3. Create and append SVG elements
switch (tagname) {
case 'animateTransform':
// Find the source <use> element in #etmain to read the transform from
const sourceElement = document.querySelector(`#etmain .etdrop > use[href="#${id}"]`);
const transformString = sourceElement ? sourceElement.getAttribute('transform') : '';
// Parse the existing transform values
const transformValues = parseTransformValues(transformString);
// Define base attributes for all 3 transform animations
const baseAttrs = {
attributeName: "transform",
attributeType: "XML",
...boardHref,
...repeatOrFillAttrs // Add the repeat/fill logic
};
// Create and append <animateTransform> for translate
const elTranslate = createSVGElement('animateTransform', {
...baseAttrs,
dur: "1s",
type: "translate",
values: transformValues.translate
});
targetSVGParent.appendChild(elTranslate);
// Create and append <animateTransform> for scale
const elScale = createSVGElement('animateTransform', {
...baseAttrs,
dur: "1s",
type: "scale",
values: transformValues.scale,
additive: "sum"
});
targetSVGParent.appendChild(elScale);
// Create and append <animateTransform> for rotate
const elRotate = createSVGElement('animateTransform', {
...baseAttrs,
dur: "1s",
type: "rotate",
values: transformValues.rotate,
additive: "sum"
});
targetSVGParent.appendChild(elRotate);
break;
case 'animate':
const elAnimateSVG = createSVGElement('animate', {
attributeName: animatetype,
dur: "1s",
values: defaultvalue,
...boardHref,
...repeatOrFillAttrs
});
targetSVGParent.appendChild(elAnimateSVG);
break;
case 'animateMotion':
const elMotion = createSVGElement('animateMotion', {
dur: "1s",
path: defaultvalue,
...boardHref,
...repeatOrFillAttrs
});
targetSVGParent.appendChild(elMotion);
break;
case 'set':
const elSet = createSVGElement('set', {
attributeName: animatetype,
to: defaultvalue,
dur: "1s",
...boardHref,
...repeatOrFillAttrs // Will be {fill: "freeze"} or {}
});
targetSVGParent.appendChild(elSet);
break;
}
}
// get target animate element by etaniAVCtrl button
function getTargetAnimation(valueBtn) {
// --- Step 1: Get .etaniAnimate parent data ---
const animateParent = valueBtn.closest('.etaniAnimate');
if (!animateParent) {
throw new Error('Could not find parent .etaniAnimate');
}
const animateType = animateParent.dataset.animatetype;
const tagName = animateParent.dataset.tagname;
// Get the index of this element among its siblings
const parentChildren = Array.from(animateParent.parentNode.children);
const animateIndex = parentChildren.indexOf(animateParent);
if (animateIndex === -1) {
throw new Error('Could not determine animateIndex');
}
// --- Step 2: Get .etaniItem parent data ---
const itemParent = valueBtn.closest('.etaniItem');
if (!itemParent) {
throw new Error('Could not find parent .etaniItem');
}
const dataId = itemParent.dataset.id;
const dataType = itemParent.dataset.type;
const hrefId = `#${dataId}`; // Prepare for attribute selector
// --- Step 3: Find target animation elements ---
const animationTags = 'animateTransform, animate, animateMotion, set';
let animations = [];
if (dataType === 'tile') {
const useElement = etani.querySelector(`.etdrop > use[href="${hrefId}"]`);
if (useElement) {
// Find all animation elements inside the <use> tag
animations = Array.from(useElement.querySelectorAll(animationTags));
}
} else if (dataType === 'board') {
// Find all direct children of #etani matching the tags and href
const selector = `:scope > animateTransform[href="${hrefId}"],
:scope > animate[href="${hrefId}"],
:scope > animateMotion[href="${hrefId}"],
:scope > set[href="${hrefId}"]`;
animations = Array.from(etani.querySelectorAll(selector));
}
// Group consecutive animateTransforms (3 at a time)
const groupedAnimations = [];
for (let i = 0; i < animations.length; ) {
const currentAnim = animations[i];
if (currentAnim.tagName === 'animateTransform') {
// Assume 3 consecutive animateTransforms
if (i + 2 < animations.length &&
animations[i+1].tagName === 'animateTransform' &&
animations[i+2].tagName === 'animateTransform')
{
groupedAnimations.push([animations[i], animations[i+1], animations[i+2]]);
i += 3;
} else {
// Handle incomplete groups: log warning and skip this element
console.warn('Incomplete or non-consecutive animateTransform group found.', currentAnim);
i++; // Skip this one to avoid infinite loop
}
} else {
// Add other animation types as single items
groupedAnimations.push(currentAnim);
i++;
}
}
// Get the specific target animation (group) using the index
const targetAnimation = groupedAnimations[animateIndex];
if (!targetAnimation) {
throw new Error(`No animation element found at index ${animateIndex}`);
}
return targetAnimation;
}
/**
* Set the values to the animation
* @param {tagName} string - The values, combined by semicolons.
* @param {targetAnimation} element - The animation elements.
* @param {valueArray} array - The values, combined by semicolons.
* @param {etaniAV} element - The values, combined by semicolons.
*/
function setValues(tagName, targetAnimation, valueArray, etaniAV) {
if (tagName === 'animate') {
targetAnimation.setAttribute('values', valueArray.join(';'));
} else if (tagName === 'animateTransform') {
// Do not append if element or value is invalid
let translateValues = '', scaleValues = '', rotateValues = '';
for (let i = 0; i < valueArray.length; i++) {
translateValues += valueArray[i].split(';')[0];
translateValues += i < valueArray.length - 1 ? ';' : '';
scaleValues += valueArray[i].split(';')[1];
scaleValues += i < valueArray.length - 1 ? ';' : '';
rotateValues += valueArray[i].split(';')[2];
rotateValues += i < valueArray.length - 1 ? ';' : '';
}
targetAnimation[0].setAttribute('values', translateValues)
targetAnimation[1].setAttribute('values', scaleValues)
targetAnimation[2].setAttribute('values', rotateValues)
} else if (tagName === 'animateMotion') {
let editInputValue = valueArray[0];
if (targetAnimation.querySelector('mpath')) {
targetAnimation.querySelector('mpath').remove();
}
if (editInputValue.substring(0, 1) === '#') {
let mpath = createSVGElement('mpath', {href : editInputValue});
targetAnimation.appendChild(mpath);
if (targetAnimation.hasAttribute('path')) {
targetAnimation.removeAttribute('path');
}
} else {
targetAnimation.setAttribute('path', editInputValue);
}
} else if (tagName === 'set') {
let editInputValue = valueArray[0];
targetAnimation.setAttribute('to', valueArray[0]);
}
setUIAnimateValues(tagName, targetAnimation, etaniAV);
updateEtaniResult();
}
/**
* Appends a value to an element's 'values' attribute, separated by semicolons.
* @param {Element} element - The animation element.
* @param {string|number} value - The value to append.
*/
function addValue(element, value) {
// Do not append if element or value is invalid
if (!element || value === null || typeof value === 'undefined') return;
let currentValues = element.getAttribute('values');
const stringValue = String(value); // Ensure value is a string
if (currentValues && currentValues.trim() !== '') {
// Add with a semicolon if values already exist
element.setAttribute('values', currentValues + ';' + stringValue);
} else {
// Set as the first value
element.setAttribute('values', stringValue);
}
}
/**
* Extracts a specific transform function's value (e.g., "10 20" from "translate(10 20)")
* @param {string} type - The transform type (e.g., 'translate', 'scale').
* @param {string} transformString - The full transform attribute string.
* @returns {string|null} The extracted value or null if not found.
*/
function getTransformValue(type, transformString) {
if (!transformString) return null;
// Regex to find the type and capture the content inside the parentheses
const regex = new RegExp(`${type}\\(([^)]+)\\)`);
const match = transformString.match(regex);
return match ? match[1] : null; // Return the captured group (the values)
}
// Add animate value
function etaniAVAddClick() {
// Change mode to default 'edit'
const elAVCtrl = this.closest('.etaniAVCtrl');
const active = elAVCtrl.querySelector('.active');
if (active) active.classList.remove('active');
const elAnimateValue = this.closest('.etaniAnimateValue');
elAnimateValue.dataset.mode = 'edit';
// --- Step 1-3: get targetAnimation element(s) ---
let targetAnimation = getTargetAnimation(this);
// --- Step 4: Insert values into target element ---
let defaultValue; // This will be used for both Step 4 and 5
const animateParent = this.closest('.etaniAnimate');
const animateType = animateParent.dataset.animatetype;
const tagName = animateParent.dataset.tagname;
const itemParent = this.closest('.etaniItem');
const dataId = itemParent.dataset.id;
const hrefId = `#${dataId}`; // Prepare for attribute selector
if (tagName === 'animateTransform') {
// Check if targetAnimation is valid (an array of 3)
if (Array.isArray(targetAnimation) && targetAnimation.length === 3) {
const mainUseElement = etmain.querySelector(`.etdrop > use[href="${hrefId}"]`);
if (mainUseElement) {
const transformString = mainUseElement.getAttribute('transform');
// Extract values
const translateVal = getTransformValue('translate', transformString);
const scaleVal = getTransformValue('scale', transformString);
const rotateVal = getTransformValue('rotate', transformString);
// Add values to the 'values' attribute of each corresponding animation
// Assuming order: [0] = translate, [1] = scale, [2] = rotate
addValue(targetAnimation[0], translateVal);
addValue(targetAnimation[1], scaleVal);
addValue(targetAnimation[2], rotateVal);
}
} else {
console.warn('Expected targetAnimation to be a group of 3, but it was not.', targetAnimation);
}
} else if (tagName === 'animate') {
// Check if targetAnimation is a single element
if (targetAnimation && !Array.isArray(targetAnimation)) {
// Determine default value based on animateType
if (animateType === 'opacity') {
defaultValue = 1;
} else if (animateType === 'stroke-width') {
defaultValue = 2;
} else if (animateType === 'fill') {
defaultValue = '#f758b8';
} else if (animateType === 'stroke') {
defaultValue = '#7786ce';
} else {
defaultValue = 0;
}
// Add to values attribute
addValue(targetAnimation, defaultValue);
} else {
console.warn('Expected targetAnimation to be a single element, but it was not.', targetAnimation);
}
}
// --- Step 5: Add <span> to .etaniAV ---
const avParent = this.closest('.etaniAnimateValue');
const etaniAV = avParent.querySelector('.etaniAV');
setUIAnimateValues(tagName, targetAnimation, etaniAV);
updateEtaniResult();
}
// append etaniAnimate to etaniItemRight
function appendAnimateToItemRight(id, elementtype, tagname, animatetype, defaultvalue = null) {
// Find the UI target parent element (in the .etaniinner UI panel)
const targetUIParent = document.querySelector(`.etaniinner .etaniItem[data-id="${id}"] > .etaniItemRight`);
if (!targetUIParent) {
console.error(`Error: UI target (.etaniinner .etaniItem[data-id="${id}"] > .etaniItemRight) not found.`);
return;
}
// Build the UI structure
const elAnimate = createEl('div', 'etaniAnimate');
elAnimate.dataset.tagname = tagname;
elAnimate.dataset.animatetype = animatetype;
// 1. Build .etaniAnimateAttr
const elAnimateAttr = createEl('div', 'etaniAnimateAttr');
const elAnimateName = createEl('span', 'etaniAnimateName');
const elAnimateDur = createEl('span', 'etaniAnimateDur');
elAnimateDur.onclick = editDurAttribute;
const elAnimateAttrAdd = createEl('span', 'etaniAnimateAttrAdd', '+');
elAnimateAttrAdd.onclick = elAnimateAttrAddClick;
const elAnimateAttrRemove = createEl('span', 'etaniAnimateAttrRemove', '×');
elAnimateAttrRemove.onclick = elAnimateAttrRemoveClick;
elAnimateAttr.appendChild(elAnimateAttrRemove);
elAnimateAttr.appendChild(elAnimateName);
elAnimateAttr.appendChild(elAnimateDur);
elAnimateAttr.appendChild(elAnimateAttrAdd);
// 2. Build .etaniAnimateValue
const elAnimateValue = createEl('div', 'etaniAnimateValue');
elAnimateValue.dataset.mode = 'edit';
const elAVLabel = createEl('span', 'etaniAVLabel');
const elAV = createEl('div', 'etaniAV');
const elAVItem = createEl('span', 'etaniAVItem');
elAVItem.onclick = etaniAVItemClick;
elAV.appendChild(elAVItem);
// create controls for this type
const elAVCtrl = createEl('div', 'etaniAVCtrl');
const etaniAVAdd = createEl('span', 'etaniAVAdd');
etaniAVAdd.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>';
const etaniAVDelete = createEl('span', 'etaniAVDelete');
etaniAVDelete.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>';
const etaniAVCopy = createEl('span', 'etaniAVCopy');
etaniAVCopy.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
const etaniAVMove = createEl('span', 'etaniAVMove');
etaniAVMove.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><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"></path></svg>';
elAVCtrl.appendChild(etaniAVAdd);
elAVCtrl.appendChild(etaniAVDelete);
elAVCtrl.appendChild(etaniAVCopy);
elAVCtrl.appendChild(etaniAVMove);
etaniAVAdd.onclick = etaniAVAddClick;
const toggleButtons = {
delete: { element: etaniAVDelete, mode: 'delete' },
copy: { element: etaniAVCopy, mode: 'copy' },
move: { element: etaniAVMove, mode: 'move' }
};
Object.values(toggleButtons).forEach(({ element, mode }) => {
element.onclick = function () {
if (elAnimateValue.dataset.mode === mode) {
this.classList.remove('active');
elAnimateValue.dataset.mode = 'edit';
} else {
const active = elAnimateValue.querySelector('.active');
if (active) active.classList.remove('active');
this.classList.add('active');
elAnimateValue.dataset.mode = mode;
}
};
});
// cname
let aName;
if (animatetype === 'stroke-width') {
aName = 'width';
} else if (animatetype === 'stroke-dasharray') {
aName = 'writing';
let tPath = etani.getElementById(id);
tPath = elementtype === 'tile' ? tPath.firstChild : tPath;
defaultvalue = Math.round(tPath.getTotalLength());
} else {
aName = animatetype;
}
// Special attribute and convert animateName
let targetEl;
if (elementtype === 'tile') {
targetEl = etani.querySelector(`.etdrop > use[href="#${id}"]`);
} else {
targetEl = etani.getElementById(id);
}
if (animatetype === 'fill') {
// get the original color
let targetElFill = targetEl.getAttribute('fill');
if (targetElFill) defaultvalue = targetElFill;
}
// 3. Apply logic based on tagname
switch (tagname) {
case 'animateTransform':
elAnimateName.textContent = 'transform';
elAnimateDur.textContent = 'dur = 1s';
elAVLabel.textContent = 'values = ';
elAVItem.textContent = 'a'; // Default placeholder
elAnimateValue.appendChild(elAVCtrl);
break;
case 'animate':
elAnimateName.textContent = aName;
elAnimateDur.textContent = 'dur = 1s';
elAVLabel.textContent = 'values = ';
elAVItem.textContent = defaultvalue;
elAnimateValue.appendChild(elAVCtrl);
break;
case 'animateMotion':
elAnimateName.textContent = 'motion';
elAnimateDur.textContent = 'dur = 1s';
elAVLabel.textContent = 'move to = ';
elAVItem.textContent = defaultvalue;
// No controls for this type
break;
case 'set':
elAnimateName.textContent = aName;
elAnimateDur.textContent = 'dur = 1s';
elAVLabel.textContent = 'set to = ';
elAVItem.textContent = defaultvalue;
// No controls for this type
break;
default:
console.error(`Error: Unknown tagname "${tagname}" for UI.`);
return;
}
// 4. Assemble the .etaniAnimateValue children
elAnimateValue.appendChild(elAVLabel);
elAnimateValue.appendChild(elAV);
// 5. Assemble the final element
elAnimate.appendChild(elAnimateAttr);
elAnimate.appendChild(elAnimateValue);
// 6. Append the fully constructed UI element to the DOM
targetUIParent.appendChild(elAnimate);
// if writing
if (animatetype === 'stroke-dasharray') {
elAV.appendChild(createEl('span', 'etaniAVItem', defaultvalue * 2));
}
}
// create SVG element
function createSVGElement(name, attrs) {
const el = document.createElementNS('http://www.w3.org/2000/svg', name);
for (const key in attrs) {
el.setAttribute(key, attrs[key]);
}
return el;
}
// Appends animation UI controls and the corresponding SVG animation element.
function etaniAppendAnimate() {
// let id, elementtype, tagname, animatetype, defaultvalue;
let appendAnimateWindow = this.closest('.appendAnimateWindow');
let id = appendAnimateWindow.dataset.id;
let elementtype = appendAnimateWindow.dataset.type;
let tagname = 'animate', animatetype = '', defaultvalue = 0;
if (this.innerHTML === 'transform') {
tagname = 'animateTransform';
animatetype = 'transform';
} else if (this.innerHTML === 'motion') {
tagname = 'animateMotion';
defaultvalue = 'M 0,0 H 120 V 120 Z';
} else if (this.innerHTML === 'opacity') {
animatetype = 'opacity';
defaultvalue = 1;
} else if (this.innerHTML === 'writing') {
animatetype = 'stroke-dasharray';
defaultvalue = 0;
} else if (this.innerHTML === 'fill') {
animatetype = 'fill';
defaultvalue = '#f758b8';
} else if (this.innerHTML === 'stroke') {
animatetype = 'stroke';
defaultvalue = '#7786ce';
} else if (this.innerHTML === 'width') {
animatetype = 'stroke-width';
defaultvalue = 2;
}
let setSpan = appendAnimateWindow.querySelector('.etaniAppendSet');
if (setSpan.classList.contains('active')) tagname = 'set';
appendAnimateToResult(id, elementtype, tagname, animatetype, defaultvalue);
appendAnimateToItemRight(id, elementtype, tagname, animatetype, defaultvalue);
updateEtaniResult();
if (document.querySelector('.etaniWindow')) {
document.querySelector('.etaniWindow').remove();
}
}
/**
* Populates a UI element with spans representing animation values,
* based on the type of animation tag.
*
* @param {string} tagname - The tag name of the animation element
* (e.g., 'animateTransform', 'animate', 'animateMotion', 'set').
* @param {Element|Element[]} targetAnimation - The target animation element(s).
* @param {Element} valuesElement - The container element to append value spans to.
*/
function setUIAnimateValues(tagname, targetAnimation, valuesElement) {
// Clear the target container first
valuesElement.innerHTML = '';
switch (tagname) {
case 'animateTransform':
// targetAnimation is an array [translate, scale, rotate]
if (!Array.isArray(targetAnimation) || targetAnimation.length < 3) {
console.error('animateTransform expects an array of 3 elements.');
return;
}
// Get values from all three transform elements
const translateVals = (targetAnimation[0]?.getAttribute('values') || '').split(';');
const scaleVals = (targetAnimation[1]?.getAttribute('values') || '').split(';');
const rotateVals = (targetAnimation[2]?.getAttribute('values') || '').split(';');
// Assume all arrays have the same length, based on the first one
const valuesLength = translateVals.length;
if (valuesLength === 0 || scaleVals.length !== valuesLength || rotateVals.length !== valuesLength) {
console.warn('animateTransform value arrays have mismatched lengths or are empty.');
// Continue anyway, but might produce incomplete results
}
const combinedValues = [];
for (let i = 0; i < valuesLength; i++) {
// Combine corresponding values with ';'
const combined = `${translateVals[i] || ''};${scaleVals[i] || ''};${rotateVals[i] || ''}`;
combinedValues.push(combined);
}
// Map unique combined values to representative letters (a-z, A-Z)
const valueMap = new Map();
const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
let charIndex = 0;
const representativeLetters = [];
for (const value of combinedValues) {
let letter = valueMap.get(value);
if (!letter) {
// Assign a new letter if this value hasn't been seen
if (charIndex < alphabet.length) {
letter = alphabet[charIndex];
charIndex++;
} else {
// Fallback if we run out of letters (more than 52 unique steps)
letter = `?${charIndex - alphabet.length + 1}`;
}
valueMap.set(value, letter);
}
representativeLetters.push(letter);
}
// Create and append spans for each representative letter
for (const letter of representativeLetters) {
const newSpan = createEl('span', 'etaniAVItem', letter);
newSpan.onclick = etaniAVItemClick;
valuesElement.appendChild(newSpan);
}
break;
case 'animate':
// targetAnimation is a single Element
const values = targetAnimation.getAttribute('values');
if (values) {
const valueArray = values.split(';');
// Create a span for each value
for (const val of valueArray) {
if (val.trim() !== '') { // Avoid creating spans for empty values (e.g., from "a;;b")
const newSpan = createEl('span', 'etaniAVItem', val);
newSpan.onclick = etaniAVItemClick;
valuesElement.appendChild(newSpan);
}
}
}
break;
case 'animateMotion':
// targetAnimation is a single Element
const pathValue = targetAnimation.getAttribute('path');
let motionValue = '';
if (pathValue) {
// Use 'path' attribute if it exists
motionValue = pathValue;
} else {
// Otherwise, find the <mpath> element and use its 'href'
const mpathElement = targetAnimation.querySelector('mpath');
if (mpathElement) {
motionValue = mpathElement.getAttribute('href') || '';
}
}
const motionSpan = createEl('span', 'etaniAVItem', motionValue);
motionSpan.onclick = etaniAVItemClick;
valuesElement.appendChild(motionSpan);
break;
case 'set':
// targetAnimation is a single Element
const toValue = targetAnimation.getAttribute('to') || '';
// Create a span for the 'to' attribute value
const setSpan = createEl('span', 'etaniAVItem', toValue);
setSpan.onclick = etaniAVItemClick;
valuesElement.appendChild(setSpan);
break;
default:
// Handle unknown tagname
console.warn(`Unhandled animation tag: ${tagname}`);
}
}
// helper function, get all animate element that has id
function getAllId() {
// Collect animate elements
const cssSelector = `
.etdrop > use > animate[id],
.etdrop > use > animateTransform[id],
.etdrop > use > animateMotion[id],
.etdrop > use > set[id],
:scope > animate[id],
:scope > animateTransform[id],
:scope > animateMotion[id],
:scope > set[id]
`;
let hasIdEle = etani.querySelectorAll(cssSelector);
return Array.from(hasIdEle).map(item => item.id);
}
// helper function, get animate name from animate element
function getAnimateName(element) {
let targetAni = getTargetAnimation(element);
if (Array.isArray(targetAni)) targetAni = targetAni[0];
const attrName = targetAni.getAttribute('attributeName');
let aName;
if (attrName === 'stroke-width') {
aName = 'width';
} else if (attrName === 'stroke-dasharray') {
aName = 'writing';
} else {
aName = attrName;
}
return aName;
}
// begin Id Click
function beginIdClick() {
if (this.classList.contains('selected')) {
this.classList.remove('selected');
} else {
let selected = this.parentNode.querySelector('.selected');
if (selected) {
selected.classList.remove('selected');
}
this.classList.add('selected');
}
}
/**
* Helper function to remove a trailing 's' if it exists.
* @param {string} str - The input string.
* @returns {string} - The string without the trailing 's'.
*/
const removeTrailingS = (str) => {
return str.endsWith('s') ? str.slice(0, -1) : str;
};
//Parses a specific string format into an object.
function generateBeginObject(str) {
// Initialize the result object with default empty values.
const obj = {
begin: '',
id: '',
endorbegin: '',
plus: ''
};
// Case 1: Simple format (no .end or .begin)
if (!str.includes('.end') && !str.includes('.begin')) {
obj.begin = removeTrailingS(str);
return obj;
}
// Case 2: Complex format (contains .end or .begin)
let mainPart = str; // This will hold the part before any '+'
let plusPart = '';
// 1. Check for 'plus' (+)
if (str.includes('+')) {
const plusIndex = str.lastIndexOf('+');
mainPart = str.substring(0, plusIndex);
plusPart = str.substring(plusIndex + 1);
obj.plus = removeTrailingS(plusPart);
}
// 2. Find 'endorbegin' (.end or .begin)
// We search in 'mainPart'
if (mainPart.includes('.end')) {
obj.endorbegin = '.end';
} else if (mainPart.includes('.begin')) {
obj.endorbegin = '.begin';
}
// 3. Find 'begin' and 'id'
// We need the part of 'mainPart' *before* the 'endorbegin' string
const endorbeginIndex = mainPart.lastIndexOf(obj.endorbegin);
const beforePart = mainPart.substring(0, endorbeginIndex);
if (beforePart.includes(';')) {
// If ';' exists
const semicolonIndex = beforePart.indexOf(';');
obj.begin = removeTrailingS(beforePart.substring(0, semicolonIndex));
obj.id = beforePart.substring(semicolonIndex + 1);
} else {
// If no ';' exists
obj.begin = ''; // As specified
obj.id = beforePart;
}
return obj;
}
//Generates a string from a specific object format.
function generateBeginString(obj) {
if (!obj.id) {
return obj.begin + 's';
}
let result = '';
if (obj.begin) result += obj.begin + 's;';
result += obj.id + obj.endorbegin;
if (obj.plus) result += '+' + obj.plus + 's';
return result;
}
// edit begin attribute
function editBeginAttribute() {
// get target animate element
let targetAnimation = getTargetAnimation(this);
let targetAni = targetAnimation;
if (Array.isArray(targetAni)) targetAni = targetAnimation[0];
// input begin
const editDiv = createEl('div', 'etaniEditDiv');
const editLabel = createEl('label', 'editLabel', 'begin = ');
const editInput = createEl('input', 'editBeginStartInput');
editInput.type = 'text';
let bObject = {
begin: '',
id: '',
endorbegin: '',
plus: '',
};
let bId = '', bBegin = '', bEndOrBegin = '', bPlus = '';
if (targetAni.hasAttribute('begin')) {
let thisBegin = targetAni.getAttribute('begin');
bObject = generateBeginObject(thisBegin);
editInput.value = bObject.begin;
const editRemove = createEl('span', 'etaniAttrRemove', '×');
editRemove.onclick = () => {
document.querySelector('.etaniWindow').remove();
if (Array.isArray(targetAnimation)) {
targetAnimation[2].removeAttribute('begin');
targetAnimation[1].removeAttribute('begin');
targetAnimation[0].removeAttribute('begin');
} else {
targetAnimation.removeAttribute('begin');
}
updateAttr(this);
};
editDiv.appendChild(editRemove);
}
editLabel.appendChild(editInput);
const editBeginStartSpan = createEl('span', 'editBeginSpan', ' s;');
editLabel.appendChild(editBeginStartSpan);
editDiv.appendChild(editLabel);
// id list
const idListDiv = createEl('div', 'etaniIdListDiv');
let allId = getAllId();
for (let item of allId) {
const spanIdItem = createEl('span', 'etaniIdListSpan', item);
spanIdItem.onclick = beginIdClick;
if (item === bObject.id) spanIdItem.classList.add('selected');
idListDiv.appendChild(spanIdItem);
}
editDiv.appendChild(idListDiv);
// end or begin, plus
const endOrBeginDiv = createEl('div', 'endOrBeginDiv');
const optionEnd = createEl('span', 'beginOption', '.end');
optionEnd.onclick = beginIdClick;
const optionBegin = createEl('span', 'beginOption', '.begin');
optionBegin.onclick = beginIdClick;
if (bObject.endorbegin === '' || bObject.endorbegin === '.end') {
optionEnd.classList.add('selected');
} else {
optionBegin.classList.add('selected');
}
const beginPlus = createEl('span', 'beginPlusSpan', ' + ');
const beginPlusInput = createEl('input', 'editBeginStartInput');
beginPlusInput.type = 'text';
beginPlusInput.value = bObject.plus;
const beginPlusUnit = createEl('span', 'beginPlusUnit', ' s');
endOrBeginDiv.appendChild(optionEnd);
endOrBeginDiv.appendChild(optionBegin);
endOrBeginDiv.appendChild(beginPlus);
endOrBeginDiv.appendChild(beginPlusInput);
endOrBeginDiv.appendChild(beginPlusUnit);
editDiv.appendChild(endOrBeginDiv);
etaniWindow(editDiv, () => {
let obj = {
begin: editInput.value,
id: idListDiv.querySelector('.selected')?.innerHTML,
endorbegin: endOrBeginDiv.querySelector('.selected')?.innerHTML,
plus: beginPlusInput.value,
};
let beginString = generateBeginString(obj);
if (Array.isArray(targetAnimation)) {
targetAnimation[2].setAttribute('begin', beginString);
targetAnimation[1].setAttribute('begin', beginString);
targetAnimation[0].setAttribute('begin', beginString);
} else {
targetAnimation.setAttribute('begin', beginString);
}
updateAttr(this);
});
}
// edit id attribute
function editIdAttribute() {
// get target animate element
let targetAni = getTargetAnimation(this);
if (Array.isArray(targetAni)) targetAni = targetAni[0];
// input id
const editDiv = createEl('div', 'etaniEditDiv');
const editLabel = createEl('label', 'editLabel', 'id = ');
const editInput = createEl('input', 'editInput');
editInput.type = 'text';
if (targetAni.hasAttribute('id')) {
editInput.value = targetAni.id;
const editRemove = createEl('span', 'etaniAttrRemove', '×');
editRemove.onclick = () => {
document.querySelector('.etaniWindow').remove();
targetAni.removeAttribute('id');
updateAttr(this);
};
editDiv.appendChild(editRemove);
}
editLabel.appendChild(editInput);
editDiv.appendChild(editLabel);
editDiv.appendChild(createEl('hr'));
// id list
const idListDiv = createEl('div', 'etaniIdListDiv');
let allId = getAllId();
for (let item of allId) {
const spanIdItem = createEl('span', 'etaniIdListSpan', item);
if (item === targetAni.id) spanIdItem.classList.add('selected');
idListDiv.appendChild(spanIdItem);
}
editDiv.appendChild(idListDiv);
etaniWindow(editDiv, () => {
// check id input
if (!isValidId(editInput.value)) {
alert('Not a valid id!');
return;
}
if (allId.includes(editInput.value)) {
alert('Dumplicate id!');
return;
}
targetAni.id = editInput.value;
updateAttr(this);
});
}
// update animate UI attr
function updateAttr(element) {
let targetAni = getTargetAnimation(element);
if (Array.isArray(targetAni)) targetAni = targetAni[0];
const etaniAnimate = element.closest('.etaniAnimate');
const etaniOldAttr = etaniAnimate.querySelector('.etaniAnimateAttr');
// Build .etaniAnimateAttr
const elAnimateAttr = createEl('div', 'etaniAnimateAttr');
// Build remove button
const elAnimateAttrRemove = createEl('span', 'etaniAnimateAttrRemove', '×');
elAnimateAttrRemove.onclick = elAnimateAttrRemoveClick;
elAnimateAttr.appendChild(elAnimateAttrRemove);
// Build Animate Name
const aName = getAnimateName(element)
const elAnimateName = createEl('span', 'etaniAnimateName', aName);
elAnimateAttr.appendChild(elAnimateName);
// Build id attribute
if (targetAni.hasAttribute('id')) {
const aId = 'id = ' + targetAni.id;
const elAnimateId = createEl('span', 'etaniAnimateId', aId);
elAnimateId.onclick = editIdAttribute;
elAnimateAttr.appendChild(elAnimateId);
}
// Build begin attribute
if (targetAni.hasAttribute('begin')) {
const aBegin = 'begin = ' + targetAni.getAttribute('begin');
const elAnimateBegin = createEl('span', 'etaniAnimateBegin', aBegin);
elAnimateBegin.onclick = editBeginAttribute;
elAnimateAttr.appendChild(elAnimateBegin);
}
// Build dur attribute
const aDur = 'dur = ' + targetAni.getAttribute('dur');
const elAnimateDur = createEl('span', 'etaniAnimateDur', aDur);
elAnimateDur.onclick = editDurAttribute;
elAnimateAttr.appendChild(elAnimateDur);
// Build add button
const elAnimateAttrAdd = createEl('span', 'etaniAnimateAttrAdd', '+');
elAnimateAttrAdd.onclick = elAnimateAttrAddClick;
elAnimateAttr.appendChild(elAnimateAttrAdd);
etaniOldAttr.replaceWith(elAnimateAttr);
}
// check the id is valid
function isValidId(str) {
if (typeof str !== 'string' || str.trim() === '') return false;
if (str.trim() !== str) return false;
if (!/^[a-zA-Z]/.test(str)) return false;
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(str)) return false;
if (/[:.]/.test(str)) return false;
return true;
}
// Add attribute to the target animation element
function elAnimateAttrAddClick(e) {
// handle dropdown menu
if (this.querySelector('.etaniDropdown')) {
if (e.target.innerHTML === 'id') {
editIdAttribute.call(this);
} else if (e.target.innerHTML === 'begin') {
editBeginAttribute.call(this);
} else if (e.target.innerHTML === 'other') {
console.log('add other');
}
this.querySelector('.etaniDropdown').remove();
return;
}
// open dropdown menu
const etaniDropdown = createEl('div', 'etaniDropdown');
etaniDropdown.style.top = this.getBoundingClientRect().height + 'px';
let targetAni = getTargetAnimation(this);
if (Array.isArray(targetAni)) targetAni = targetAni[0];
const attrObj = Array.from(targetAni.attributes).reduce((obj, attr) => {
obj[attr.name] = attr.value;
return obj;
}, {});
const animationProps = Object.keys(attrObj);
if (!animationProps.includes('id')) {
const itemid = createEl('div', 'etaniDropdownItem', 'id');
etaniDropdown.appendChild(itemid);
}
if (!animationProps.includes('begin')) {
const itemBegin = createEl('div', 'etaniDropdownItem', 'begin');
etaniDropdown.appendChild(itemBegin);
}
const itemOther = createEl('div', 'etaniDropdownItem', 'other');
etaniDropdown.appendChild(itemOther);
this.appendChild(etaniDropdown);
}
// remove the target animation element
function elAnimateAttrRemoveClick() {
const targetAnimation = getTargetAnimation(this);
if (Array.isArray(targetAnimation)) {
targetAnimation[2].remove();
targetAnimation[1].remove();
targetAnimation[0].remove();
} else {
targetAnimation.remove();
}
this.closest('.etaniAnimate').remove();
updateEtaniResult();
}
// edit Animate Attribute
function editDurAttribute() {
const targetAnimation = getTargetAnimation(this);
let editDurValue;
if (Array.isArray(targetAnimation)) {
editDurValue = targetAnimation[0].getAttribute('dur');
} else {
editDurValue = targetAnimation.getAttribute('dur');
}
editDurValue = editDurValue.replace('s', '');
const editDur = createEl('div', 'etaniEditDur');
const editDurLabel = createEl('label', 'editDurLabel', 'dur = ');
const editDurInput = createEl('input', 'editDurInput');
editDurInput.type = 'text';
editDurInput.value = editDurValue;
const editDurSpan = createEl('span', 'editDurSpan', ' s');
editDurLabel.appendChild(editDurInput);
editDurLabel.appendChild(editDurSpan);
editDur.appendChild(editDurLabel);
etaniWindow(editDur, () => {
let setDurValue = document.querySelector('.editDurInput').value;
setDurValue = setDurValue + 's';
if (Array.isArray(targetAnimation)) {
targetAnimation[0].setAttribute('dur', setDurValue);
targetAnimation[1].setAttribute('dur', setDurValue);
targetAnimation[2].setAttribute('dur', setDurValue);
} else {
targetAnimation.setAttribute('dur', setDurValue);
}
this.textContent = 'dur = ' + setDurValue;
updateEtaniResult();
});
}
// get values from targetAnimation
function getValues(targetAnimation) {
if (!Array.isArray(targetAnimation)) {
if (targetAnimation.tagName === 'animate') {
return targetAnimation.getAttribute('values').split(';');
} else if (targetAnimation.tagName === 'animateMotion') {
if (targetAnimation.hasAttribute('path')) {
return [targetAnimation.getAttribute('path')];
} else if (targetAnimation.querySelector('mpath')) {
return [targetAnimation.querySelector('mpath').getAttribute('href')];
}
} else if (targetAnimation.tagName === 'set') {
return [targetAnimation.getAttribute('to')];
}
}
// Get values from all three transform elements
const translateVals = (targetAnimation[0]?.getAttribute('values') || '').split(';');
const scaleVals = (targetAnimation[1]?.getAttribute('values') || '').split(';');
const rotateVals = (targetAnimation[2]?.getAttribute('values') || '').split(';');
// Assume all arrays have the same length, based on the first one
const valuesLength = translateVals.length;
if (valuesLength === 0 || scaleVals.length !== valuesLength || rotateVals.length !== valuesLength) {
console.warn('animateTransform value arrays have mismatched lengths or are empty.');
}
const combinedValues = [];
for (let i = 0; i < valuesLength; i++) {
// Combine corresponding values with ';'
const combined = `${translateVals[i] || ''};${scaleVals[i] || ''};${rotateVals[i] || ''}`;
combinedValues.push(combined);
}
return combinedValues;
}
// Values item click event
function etaniAVItemClick () {
// get standard data
const targetAnimation = getTargetAnimation(this);
const animateValue = this.closest('.etaniAnimateValue');
const ctrlMode = animateValue.dataset.mode;
const animateParent = this.closest('.etaniAnimate');
const animateType = animateParent.dataset.animatetype;
const tagName = animateParent.dataset.tagname;
const dataId = this.closest('.etaniItem').dataset.id;
const dataType = this.closest('.etaniItem').dataset.type;
const etaniAV = animateParent.querySelector('.etaniAV');
// Get the index of this element among its siblings
const itemParent = this.parentNode;
const itemIndex = Array.from(itemParent.children).indexOf(this);
const valueArray = getValues(targetAnimation);
const targetValue = valueArray[itemIndex];
if (ctrlMode === 'delete') {
if (valueArray.length > 1) {
valueArray.splice(itemIndex, 1);
setValues(tagName, targetAnimation, valueArray, etaniAV);
}
} else if (ctrlMode === 'copy') {
valueArray.splice(itemIndex, 0, targetValue);
setValues(tagName, targetAnimation, valueArray, etaniAV);
} else if (ctrlMode === 'move') {
let sItem = itemParent.querySelector('.selected');
if (this === sItem) {
this.classList.remove('selected');
} else if (sItem) {
const sIndex = Array.from(itemParent.children).indexOf(sItem);
const sValue = valueArray[sIndex];
if (sIndex < itemIndex) {
if (this.nextSibling) {
itemParent.insertBefore(sItem, this.nextSibling);
} else {
itemParent.appendChild(sItem);
}
valueArray.splice(itemIndex + 1, 0, sValue);
valueArray.splice(sIndex, 1);
} else {
itemParent.insertBefore(sItem, this);
valueArray.splice(sIndex, 1);
valueArray.splice(itemIndex, 0, sValue);
}
setValues(tagName, targetAnimation, valueArray, etaniAV);
} else {
this.classList.add('selected');
}
} else if (tagName === 'animate') {
const editDiv = createEl('div', 'etaniEditDiv');
const editLabel = createEl('label', 'editLabel', animateType + ' = ');
const editInput = createEl('input', 'editInput');
editInput.type = 'text';
editInput.value = targetValue;
editLabel.appendChild(editInput);
editDiv.appendChild(editLabel);
etaniWindow(editDiv, () => {
valueArray.splice(itemIndex, 1, editInput.value);
setValues(tagName, targetAnimation, valueArray, etaniAV);
});
} else if (tagName === 'animateTransform') {
const editDiv = createEl('div', 'etaniEditDiv');
// translate
const translateRow = createEl('div', 'etaniAppendRow');
const translateLabel = createEl('label', 'editLabel', 'translate = ');
const translateInput = createEl('input', 'editInput');
translateInput.type = 'text';
translateInput.value = targetValue.split(';')[0];
translateRow.appendChild(translateLabel);
translateRow.appendChild(translateInput);
editDiv.appendChild(translateRow);
// scale
const scaleRow = createEl('div', 'etaniAppendRow');
const scaleLabel = createEl('label', 'editLabel', 'scale = ');
const scaleInput = createEl('input', 'editInput');
scaleInput.type = 'text';
scaleInput.value = targetValue.split(';')[1];
scaleRow.appendChild(scaleLabel);
scaleRow.appendChild(scaleInput);
editDiv.appendChild(scaleRow);
// rotate
const rotateRow = createEl('div', 'etaniAppendRow');
const rotateLabel = createEl('label', 'editLabel', 'rotate = ');
const rotateInput = createEl('input', 'editInput');
rotateInput.type = 'text';
rotateInput.value = targetValue.split(';')[2];
rotateRow.appendChild(rotateLabel);
rotateRow.appendChild(rotateInput);
editDiv.appendChild(rotateRow);
etaniWindow(editDiv, () => {
let combineValue = translateInput.value + ';' +
scaleInput.value + ';' + rotateInput.value;
valueArray.splice(itemIndex, 1, combineValue);
setValues(tagName, targetAnimation, valueArray, etaniAV);
});
} else if (tagName === 'animateMotion') {
const editDiv = createEl('div', 'etaniEditDiv');
const editLabel = createEl('label', 'editLabel', 'path = ');
const editInput = createEl('input', 'editInput');
editInput.type = 'text';
editInput.value = targetValue;
editLabel.appendChild(editInput);
editDiv.appendChild(editLabel);
etaniWindow(editDiv, () => {
setValues(tagName, targetAnimation, [editInput.value], etaniAV);
});
} else if (tagName === 'set') {
const editDivTitle = createEl('div', 'etaniEditDiv');
const editTitle = createEl('label', 'editLabel', 'set ' + animateType);
const editDiv = createEl('div', 'etaniEditDiv');
const editLabel = createEl('label', 'editLabel', 'to = ');
const editInput = createEl('input', 'editInput');
editInput.type = 'text';
editInput.value = targetValue;
editDivTitle.appendChild(editTitle);
editLabel.appendChild(editInput);
editDiv.appendChild(editDivTitle);
editDiv.appendChild(editLabel);
etaniWindow(editDiv, () => {
setValues(tagName, targetAnimation, [editInput.value], etaniAV);
});
}
}
// Initialize the animation control panel on window load
window.addEventListener('load', function() {
// Get the etmainouter element
const etmainouter = document.getElementById('etmainouter');
if (!etmainouter) return; // Exit if etmainouter not found
// insert dynamic style
addEtaniStyles();
// Create etaniouter div
const etaniouter = createEl('div', 'etaniouter');
// Create etaniStart button
const etaniStart = createEl('button', null, 'start ejtile animate');
etaniStart.id = 'etaniStart';
// Append button to etaniouter
etaniouter.appendChild(etaniStart);
// Insert etaniouter after etmainouter
etmainouter.parentNode.insertBefore(etaniouter, etmainouter.nextSibling);
// Add click event listener to etaniStart
etaniStart.onclick = etaniStartClick;
});
