Here are the modifications to address your three requirements. All comments and code are in English.
Add these new styles to the <style> block in the <head> to handle the SVG preview correctly without white edges.
CSS
/* --- Add these styles for the SVG Export Preview --- */
.ejcees-export-modal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0,0,0,0.85);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
}
.ejcees-export-box {
position: relative;
background-color: #333;
padding: 35px 20px 20px;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
max-height: 90vh;
max-width: 90vw;
box-shadow: 0px 0px 25px rgba(0,0,0,0.5);
box-sizing: border-box;
}
.ejcees-export-preview {
width: auto;
height: 60vh;
max-width: 100%;
background-color: transparent;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
}
.ejcees-export-preview svg {
display: block;
height: 100%;
aspect-ratio: 432 / 480;
background-color: #f4f4f9;
}
Add the following functions right after document.addEventListener('DOMContentLoaded', () => { so they are accessible globally within the script.
JavaScript
// --- Helper functions for Base64 Unicode encoding/decoding ---
const b64EncodeUnicode = (str) => {
if (!str) return '';
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
return String.fromCharCode('0x' + p1);
}));
};
const b64DecodeUnicode = (str) => {
if (!str) return '';
return decodeURIComponent(Array.prototype.map.call(atob(str), function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
};
// --- Function to Import SVG and reconstruct Game History ---
function importExportedSVG(svgText) {
const parser = new DOMParser();
const svgDoc = parser.parseFromString(svgText, "image/svg+xml");
// 1. Get step0 for initial comment
const step0 = svgDoc.getElementById('step0');
const initialComment = step0 && step0.getAttribute('data-comment') ? b64DecodeUnicode(step0.getAttribute('data-comment')) : "";
// 2. Reconstruct Initial FEN from SVG DOM positions
const etboard = svgDoc.querySelector('.etboard');
const uses = etboard.querySelectorAll('use');
const boardMap = Array(10).fill(null).map(() => Array(9).fill(''));
let firstMovePieceId = null;
const step1 = svgDoc.getElementById('step1');
if (step1 && step1.parentElement) {
firstMovePieceId = step1.parentElement.getAttribute('id');
}
uses.forEach(use => {
const href = use.getAttribute('href') || use.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
if (/^#(tile([2-9]|1[0-5]))$/.test(href)) {
const transform = use.getAttribute('transform');
const match = transform && transform.match(/translate\(([^,]+),\s*([^)]+)\)/);
if (match) {
const px = parseFloat(match[1]);
const py = parseFloat(match[2]);
const x = Math.round((px - 24) / 48);
const y = Math.round((py - 24) / 48);
const id = use.getAttribute('id');
// Ignore cloned animation elements (e.g. ones with 'f' suffix)
if (id && !id.endsWith('f') && x >= 0 && x < 9 && y >= 0 && y < 10) {
boardMap[y][x] = id[0];
}
}
}
});
let initialFenBoard = "";
for (let y = 0; y < 10; y++) {
let emptyCount = 0;
for (let x = 0; x < 9; x++) {
if (boardMap[y][x] === '') {
emptyCount++;
} else {
if (emptyCount > 0) { initialFenBoard += emptyCount; emptyCount = 0; }
initialFenBoard += boardMap[y][x];
}
}
if (emptyCount > 0) initialFenBoard += emptyCount;
if (y < 9) initialFenBoard += "/";
}
// Determine whose turn it is based on the first move
let initialTurn = 'w';
if (firstMovePieceId && firstMovePieceId[0] === firstMovePieceId[0].toLowerCase()) {
initialTurn = 'b';
}
const baseFen = initialFenBoard + " " + initialTurn;
let newHistoryFEN = { fen: baseFen, move: null, lastMove: null, c: initialComment, v: [] };
let currentNode = newHistoryFEN;
let currentFen = baseFen;
let stepIndex = 1;
// Temporarily load FEN to populate `tileMap` correctly for `getMoveNotation` logic
loadFEN(baseFen);
// 3. Reconstruct moves by analyzing animateTransform values
while (true) {
const stepAnim = svgDoc.getElementById(`step${stepIndex}`);
if (!stepAnim) break;
const commentStr = stepAnim.getAttribute('data-comment');
const comment = commentStr ? b64DecodeUnicode(commentStr) : "";
const fromStr = stepAnim.getAttribute('from');
const toStr = stepAnim.getAttribute('to');
const pId = stepAnim.parentElement.getAttribute('id');
const fParts = fromStr.split(',');
const tParts = toStr.split(',');
const startX = Math.round((parseFloat(fParts[0]) - 24) / 48);
const startY = Math.round((parseFloat(fParts[1]) - 24) / 48);
const endX = Math.round((parseFloat(tParts[0]) - 24) / 48);
const endY = Math.round((parseFloat(tParts[1]) - 24) / 48);
const capturedId = tileMap.get(`${endX},${endY}`) || null;
const moveNotation = getMoveNotation(pId, startX, startY, endX, endY, capturedId);
const dc = { startX, startY, endX, endY };
const nextFen = simulateMove(currentFen, dc);
// Update logical board for next loop's `getMoveNotation`
tileMap.delete(`${startX},${startY}`);
tileMap.set(`${endX},${endY}`, pId);
let newNode = { fen: nextFen, move: moveNotation, lastMove: dc, c: comment, v: [] };
currentNode.v.push(newNode);
currentNode = newNode;
currentFen = nextFen;
stepIndex++;
}
historyFEN = newHistoryFEN;
initBranch();
currentStepIndex = 0;
renderRecordUI();
renderNoteUI();
jumpToStep(0);
saveStateToUndo();
}
Locate document.getElementById('file-input').addEventListener('change', ...) and modify it to handle SVG files:
JavaScript
document.getElementById('file-input').addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
const result = event.target.result;
// Route to SVG Import if it's an SVG file
if (file.name.toLowerCase().endsWith('.svg') || result.trim().startsWith('<svg')) {
importExportedSVG(result);
e.target.value = ''; // Reset input
return;
}
const data = JSON.parse(result);
// ... [Keep the rest of your original JSON parsing logic untouched here] ...
Locate document.getElementById('tool-exp-svg').addEventListener('click', ...) and modify it to append the base64 encoded comments to step0 and stepN:
JavaScript
document.getElementById('tool-exp-svg').addEventListener('click', () => {
if (isExportTextMode || isEditingComment || isEditMode) return;
let exportStart = 0;
let exportEnd = getPathDepth(historyFEN, currentBranch) - 1;
let isAnimation = true;
// 1. Determine export bounds based on the "Range Selection" (isRangeMode) logic
if (isRangeMode) {
if (rangeStart === null && rangeEnd === null) {
isAnimation = false;
exportStart = currentStepIndex;
exportEnd = currentStepIndex;
} else if (rangeStart !== null && rangeEnd !== null && rangeStart === rangeEnd) {
isAnimation = false;
exportStart = rangeStart;
exportEnd = rangeEnd;
} else if (rangeStart !== null && rangeEnd === null) {
exportStart = rangeStart;
} else if (rangeStart !== null && rangeEnd !== null && rangeStart !== rangeEnd) {
exportStart = Math.min(rangeStart, rangeEnd);
exportEnd = Math.max(rangeStart, rangeEnd);
}
isRangeMode = false;
clearRangeBorders();
renderNoteUI();
}
const savedStep = currentStepIndex;
jumpToStep(exportStart);
const cloneSvg = document.querySelector('.ejceespb').cloneNode(true);
// Get initial state comment
const startNodeComment = getNodeAtStep(exportStart).c || "";
if (isAnimation) {
const w2t = cloneSvg.querySelector('#whiteToTransparent');
if (w2t) w2t.remove();
const startDot = cloneSvg.querySelector('#ejceesstartdot');
if (startDot) startDot.remove();
cloneSvg.querySelectorAll('use').forEach(p => {
const href = p.getAttribute('href');
if (/^#(tile([2-9]|1[0-5]))$/.test(href)) {
p.removeAttribute('currentmove');
p.setAttribute('stroke', 'none');
p.removeAttribute('stroke-width');
}
});
} else {
// Export Static SVG Snapshot - Append a dummy step0 for the base64 comment
const dummyStep0 = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
dummyStep0.setAttribute('id', 'step0');
dummyStep0.setAttribute('data-comment', b64EncodeUnicode(startNodeComment));
cloneSvg.querySelector('.etboard').appendChild(dummyStep0);
const svgStringXML = new XMLSerializer().serializeToString(cloneSvg);
const svgString = svgStringXML.replace(/ xmlns:a.*?=.*?".*?"/g, '').replace(/ a[^t].*?=.*?".*?"/g, '').replace(/^\s+|\s+$/gm, '').replace(/[\r\n]/gm, '');
showSvgExportModal(svgString, false);
jumpToStep(savedStep);
return;
}
cloneSvg.setAttribute('class', 'ejceespbanimate');
const etboardClone = cloneSvg.querySelector('.etboard');
const etdropClone = cloneSvg.querySelector('.etdrop');
const movedPieceIds = new Set();
const initialCoords = {};
const boardPieces = etboardClone.querySelectorAll('use');
boardPieces.forEach(p => {
const href = p.getAttribute('href');
if (/^#(tile([2-9]|1[0-5]))$/.test(href)) {
const id = p.getAttribute('id');
const pf = p.cloneNode(true);
pf.setAttribute('id', id + 'f');
pf.setAttribute('opacity', '0');
etdropClone.appendChild(pf);
const transform = p.getAttribute('transform');
const match = transform.match(/translate\(([^,]+),\s*([^)]+)\)/);
if (match) {
initialCoords[id] = `${match[1]},${match[2]}`;
}
}
});
let pathNodes = [];
let curr = historyFEN;
let ptr = 0;
let tempStep = 0;
while (curr) {
if (tempStep > exportStart && tempStep <= exportEnd) {
pathNodes.push(curr);
}
if (tempStep === exportEnd) break;
const children = curr.v || [];
if (children.length === 0) break;
const choice = children.length > 1 ? (currentBranch[ptr++] || 0) : 0;
curr = children[choice];
tempStep++;
}
let simMap = new Map(tileMap);
const delayStr = moveInterval + 's';
pathNodes.forEach((node, i) => {
let stepNum = i + 1;
let moveData = node.lastMove;
if (!moveData) return;
let pieceId = simMap.get(`${moveData.startX},${moveData.startY}`);
let capturedId = simMap.get(`${moveData.endX},${moveData.endY}`);
if (!pieceId) return;
// Extract node comment
const nodeComment = node.c || "";
let visStart = getVisualCoords(moveData.startX, moveData.startY);
let visEnd = getVisualCoords(moveData.endX, moveData.endY);
let fromStr = `${visStart.x * 48 + 24},${visStart.y * 48 + 24}`;
let toStr = `${visEnd.x * 48 + 24},${visEnd.y * 48 + 24}`;
movedPieceIds.add(pieceId);
simMap.delete(`${moveData.startX},${moveData.startY}`);
if (capturedId) simMap.delete(`${moveData.endX},${moveData.endY}`);
simMap.set(`${moveData.endX},${moveData.endY}`, pieceId);
let beginStr = (stepNum === 1) ?
`${delayStr};step0.end+${delayStr}` :
`step${stepNum - 1}.begin+${delayStr}`;
const boardPiece = etboardClone.querySelector(`#${pieceId}`);
if (boardPiece) {
const animT = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
animT.setAttribute('id', `step${stepNum}`);
// Add base64 comment data
animT.setAttribute('data-comment', b64EncodeUnicode(nodeComment));
animT.setAttribute('begin', beginStr);
animT.setAttribute('attributeName', 'transform');
animT.setAttribute('attributeType', 'XML');
animT.setAttribute('type', 'translate');
animT.setAttribute('from', fromStr);
animT.setAttribute('to', toStr);
animT.setAttribute('dur', `${moveSpeed}s`);
animT.setAttribute('fill', 'freeze');
boardPiece.appendChild(animT);
}
const dropPiece = etdropClone.querySelector(`#${pieceId}f`);
if (dropPiece) {
const animT = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
animT.setAttribute('begin', `step${stepNum}.begin`);
animT.setAttribute('attributeName', 'transform');
animT.setAttribute('attributeType', 'XML');
animT.setAttribute('type', 'translate');
animT.setAttribute('from', fromStr);
animT.setAttribute('to', toStr);
animT.setAttribute('dur', `${moveSpeed}s`);
animT.setAttribute('fill', 'freeze');
dropPiece.appendChild(animT);
const animO = document.createElementNS('http://www.w3.org/2000/svg', 'set');
animO.setAttribute('begin', `step${stepNum}.begin`);
animO.setAttribute('end', `step${stepNum}.end`);
animO.setAttribute('attributeName', 'opacity');
animO.setAttribute('to', '1');
dropPiece.appendChild(animO);
}
if (capturedId) {
const capPiece = etboardClone.querySelector(`#${capturedId}`);
if (capPiece) {
const animCap = document.createElementNS('http://www.w3.org/2000/svg', 'set');
animCap.setAttribute('begin', `step${stepNum}.end`);
animCap.setAttribute('end', `step0.begin+${moveInterval}s`);
animCap.setAttribute('attributeName', 'opacity');
animCap.setAttribute('to', '0');
capPiece.appendChild(animCap);
}
}
});
if (pathNodes.length > 0) {
const finalStepNum = pathNodes.length;
const step0 = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
step0.setAttribute('id', 'step0');
// Add base64 initial comment data
step0.setAttribute('data-comment', b64EncodeUnicode(startNodeComment));
step0.setAttribute('begin', `step${finalStepNum}.end+${moveInterval * 2}s`);
step0.setAttribute('attributeName', 'opacity');
step0.setAttribute('values', '1;0;0;1');
step0.setAttribute('dur', `${moveInterval * 3}s`);
step0.setAttribute('fill', 'freeze');
etboardClone.appendChild(step0);
movedPieceIds.forEach(pid => {
const initialPos = initialCoords[pid];
if (!initialPos) return;
const bp = etboardClone.querySelector(`#${pid}`);
if (bp) {
const r = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
r.setAttribute('begin', `step0.begin+${moveInterval}s`);
r.setAttribute('dur', `${moveInterval}s`);
r.setAttribute('attributeName', 'transform');
r.setAttribute('attributeType', 'XML');
r.setAttribute('type', 'translate');
r.setAttribute('to', initialPos);
r.setAttribute('fill', 'freeze');
bp.appendChild(r);
}
const dp = etdropClone.querySelector(`#${pid}f`);
if (dp) {
const r = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
r.setAttribute('begin', `step0.begin+${moveInterval}s`);
r.setAttribute('dur', `${moveInterval}s`);
r.setAttribute('attributeName', 'transform');
r.setAttribute('attributeType', 'XML');
r.setAttribute('type', 'translate');
r.setAttribute('to', initialPos);
r.setAttribute('fill', 'freeze');
dp.appendChild(r);
}
});
}
const svgStringXML = new XMLSerializer().serializeToString(cloneSvg);
const svgString = svgStringXML.replace(/ xmlns:a.*?=.*?".*?"/g, '').replace(/ a[^t].*?=.*?".*?"/g, '').replace(/^\s+|\s+$/gm, '').replace(/[\r\n]/gm, '');
showSvgExportModal(svgString, true);
jumpToStep(savedStep);
});
Locate function showSvgExportModal(svgContent, isAnimation) { and modify it to use the new CSS classes to remove white edges and make it perfectly responsive:
JavaScript
// Step 9: Render Preview / Download UI Modal
function showSvgExportModal(svgContent, isAnimation) {
const blob = new Blob([svgContent], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const size = blob.size;
const now = new Date();
const timestamp = now.getFullYear().toString().padStart(4, '0') +
(now.getMonth() + 1).toString().padStart(2, '0') +
now.getDate().toString().padStart(2, '0') +
now.getHours().toString().padStart(2, '0') +
now.getMinutes().toString().padStart(2, '0') +
now.getSeconds().toString().padStart(2, '0');
const filename = `ejcees_${isAnimation ? 'animate' : 'static'}_${timestamp}.svg`;
// Modal Overlay - Using new CSS Class
const modal = document.createElement('div');
modal.className = 'ejcees-export-modal';
// Modal Box Container - Using new CSS Class
const box = document.createElement('div');
box.className = 'ejcees-export-box';
// Preview Container - Using new CSS Class
const preview = document.createElement('div');
preview.className = 'ejcees-export-preview';
preview.innerHTML = svgContent;
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.textContent = `Download (${size} bytes)`;
link.style.color = '#66b2ff';
link.style.textDecoration = 'none';
link.style.fontSize = '16px';
link.style.fontWeight = 'bold';
link.style.padding = '5px 15px';
link.style.border = '1px solid #66b2ff';
link.style.borderRadius = '4px';
const closeBtn = document.createElement('button');
closeBtn.innerHTML = '<svg viewBox="0 0 24 24" width="22" height="22"><path fill="#fff" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>';
closeBtn.style.position = 'absolute';
closeBtn.style.top = '10px';
closeBtn.style.right = '10px';
closeBtn.style.background = 'transparent';
closeBtn.style.border = 'none';
closeBtn.style.cursor = 'pointer';
closeBtn.onclick = () => {
document.body.removeChild(modal);
URL.revokeObjectURL(url);
};
box.appendChild(closeBtn);
box.appendChild(preview);
box.appendChild(link);
modal.appendChild(box);
document.body.appendChild(modal);
}