Here is the complete implementation and guide to creating the independent ejceesexpsvg function.
To make this function truly standalone and independent of the active browser DOM and global state tracking (tileMap, currentStepIndex, etc.), the logic has been decoupled.
This function takes your text notation, interval, and speed, and orchestrates the creation of the SVG string natively.
JavaScript
/**
* Standalone function to export an Ejcees SVG (Static or Animated)
* @param {string} txt - The text format containing FEN and moves (URL, Engine, CN, EN, JSON).
* @param {number} interval - Interval between moves in seconds.
* @param {number} speed - Animation movement speed in seconds.
* @returns {string} - The serialized SVG string.
*/
function ejceesexpsvg(txt, interval = 2, speed = 0.6) {
// 1. Parse the text into a linear array of move nodes.
// (Requires a decoupled version of the original parsing logic)
const pathNodes = headlessParseTextToPath(txt);
if (!pathNodes || pathNodes.length === 0) return "";
const startNode = pathNodes[0];
const isAnimation = pathNodes.length > 1;
// 2. Generate the base SVG template element programmatically from the starting FEN
// (Creates the raw `<svg>` wrapper with `<defs>`, `<g class="etboard">`, and `<g class="etdrop">`)
const cloneSvg = buildBaseSVGFromFEN(startNode.fen);
// 3. Extract view rotation and flip flags from the FEN string
const fenArr = startNode.fen.split(' ');
const isRotateEnabled = fenArr.length >= 4 && fenArr[2] === '1';
const isFlipEnabled = fenArr.length >= 4 && fenArr[3] === '1';
cloneSvg.setAttribute('ejceesrotate', isRotateEnabled ? '1' : '0');
cloneSvg.setAttribute('ejceesflip', isFlipEnabled ? '1' : '0');
// Local coordinate helper decoupled from the global state
const getVisCoords = (logX, logY) => {
let vX = logX;
let vY = logY;
if (isFlipEnabled) vX = 8 - vX;
if (isRotateEnabled) {
vX = 8 - vX;
vY = 9 - vY;
}
return { x: vX, y: vY };
};
const startNodeComment = startNode.c || "";
// --- 4A. Static SVG Generation ---
if (!isAnimation) {
cloneSvg.setAttribute('class', 'ejceespbstatic');
cloneSvg.setAttribute('data-comment', b64EncodeUnicode(startNodeComment));
// Clean up unneeded elements for the static view
const w2t = cloneSvg.querySelector('#whiteToTransparent');
if (w2t) w2t.remove();
const startDot = cloneSvg.querySelector('#ejceesstartdot');
if (startDot) startDot.remove();
return new XMLSerializer().serializeToString(cloneSvg)
.replace(/ xmlns:a.*?=.*?".*?"/g, '')
.replace(/ a[^t].*?=.*?".*?"/g, '')
.replace(/^\s+|\s+$/gm, '')
.replace(/[\r\n]/gm, '');
}
// --- 4B. Animated SVG Generation ---
cloneSvg.setAttribute('class', 'ejceespbanimate');
const etboardClone = cloneSvg.querySelector('.etboard');
let etdropClone = cloneSvg.querySelector('.etdrop');
// Ensure etdrop layer exists in the template
if (!etdropClone) {
etdropClone = document.createElementNS('http://www.w3.org/2000/svg', 'g');
etdropClone.setAttribute('class', 'etdrop');
cloneSvg.appendChild(etdropClone);
}
// Build a virtual map of pieces using the starting FEN
let simMap = buildVirtualMap(startNode.fen);
const movedPieceIds = new Set();
const initialCoords = {};
// Clone pieces to the `.etdrop` layer to handle animation layering smoothly
const boardPieces = etboardClone.querySelectorAll('use');
boardPieces.forEach(p => {
const href = p.getAttribute('href') || p.getAttributeNS('http://www.w3.org/1999/xlink', '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 && transform.match(/translate\(([^,]+),\s*([^)]+)\)/);
if (match) {
initialCoords[id] = `${match[1]},${match[2]}`;
}
}
});
const delayStr = interval + 's';
// Loop through the move path to construct `<animateTransform>` and `<set>` nodes
for (let i = 1; i < pathNodes.length; i++) {
const node = pathNodes[i];
const stepNum = i;
const moveData = node.lastMove;
if (!moveData) continue;
let pieceId = simMap.get(`${moveData.startX},${moveData.startY}`);
let capturedId = simMap.get(`${moveData.endX},${moveData.endY}`);
if (!pieceId) continue;
const nodeComment = node.c || "";
let visStart = getVisCoords(moveData.startX, moveData.startY);
let visEnd = getVisCoords(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);
// Update virtual tracking map
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}`;
// Add movement animation to the main board piece
const boardPiece = etboardClone.querySelector(`#${pieceId}`);
if (boardPiece) {
const animT = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
animT.setAttribute('id', `step${stepNum}`);
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', `${speed}s`);
animT.setAttribute('fill', 'freeze');
animT.setAttribute('data-comment', b64EncodeUnicode(nodeComment));
boardPiece.appendChild(animT);
}
// Apply identical animation to the ghost layer clone
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', `${speed}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);
}
// Hide captured pieces exactly when the moving piece lands
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+${interval}s`);
animCap.setAttribute('attributeName', 'opacity');
animCap.setAttribute('to', '0');
capPiece.appendChild(animCap);
}
}
}
// --- 5. Implement the Restart/Loop Reset mechanism ---
const finalStepNum = pathNodes.length - 1;
const step0 = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
step0.setAttribute('id', 'step0');
step0.setAttribute('data-comment', b64EncodeUnicode(startNodeComment));
step0.setAttribute('begin', `step${finalStepNum}.end+${interval * 2}s`);
step0.setAttribute('attributeName', 'opacity');
step0.setAttribute('values', '1;0;0;1');
step0.setAttribute('dur', `${interval * 3}s`);
step0.setAttribute('fill', 'freeze');
etboardClone.appendChild(step0);
// Add transforms to reset pieces to their original position instantly at the end of the loop
movedPieceIds.forEach(pid => {
const initialPos = initialCoords[pid];
if (!initialPos) return;
[etboardClone.querySelector(`#${pid}`), etdropClone.querySelector(`#${pid}f`)].forEach(pieceEl => {
if (pieceEl) {
const r = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
r.setAttribute('begin', `step0.begin+${interval}s`);
r.setAttribute('dur', `${interval}s`);
r.setAttribute('attributeName', 'transform');
r.setAttribute('attributeType', 'XML');
r.setAttribute('type', 'translate');
r.setAttribute('to', initialPos);
r.setAttribute('fill', 'freeze');
pieceEl.appendChild(r);
}
});
});
// Output strictly sanitized text
return new XMLSerializer().serializeToString(cloneSvg)
.replace(/ xmlns:a.*?=.*?".*?"/g, '')
.replace(/ a[^t].*?=.*?".*?"/g, '')
.replace(/^\s+|\s+$/gm, '')
.replace(/[\r\n]/gm, '');
}
You must retain these exact functions/objects from your original codebase:
To isolate the parsing and SVG-building logic from your web app UI, you need to add these two helper functions:
A. headlessParseTextToPath(text)
Your original importExportedText modifies active DOM nodes, UI sliders, and global properties (currentBranch, historyFEN). You need to clone that parsing logic but alter it so it strictly returns a 1-dimensional array representing the main path of the game, rather than mutating globals.
B. buildBaseSVGFromFEN(fen)
Your original script clones document.querySelector('.ejceespb').cloneNode(true). For an independent script (especially if running without the viewer HTML), this needs to be constructed programmatically.