Standalone SVG Export Function

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.

1. The Core Function: ejceesexpsvg

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, '');
}

2. Original Functions Needed

You must retain these exact functions/objects from your original codebase:

  1. NotationConverter object (for English/Chinese notation mappings).
  2. simulateMove(fen, derive_coords)
  3. deriveCoordsFromMove(fen, move, isRed)
  4. buildVirtualMap(fen)
  5. parseUrlHash(hashStr)
  6. b64EncodeUnicode(str)
  7. pieceToHrefMap object mapping letters to tile IDs.

3. Modifications Required to Support Independence

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.

  • Change: Remove all renderRecordUI(), renderNoteUI(), jumpToStep(), and saveStateToUndo() calls.
  • Return: Compute the main branch from root to leaf, accumulating nodes into an array [{ fen, move, lastMove, c }, ...], and return it.

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.

  • Implementation Note: Write a small function that uses DOMParser or standard createElementNS to assemble the <svg> shell, includes the <defs> object you noted can be omitted from here, and dynamically appends <use> tags corresponding to the piece positions extracted from the fen variable.