UNPKG

shogiground

Version:
334 lines 13.4 kB
import { createEl, key2pos, pieceNameOf, posToTranslateRel, samePiece, translateRel, posOfOutsideEl, sentePov, } from './util.js'; export function createSVGElement(tagName) { return document.createElementNS('http://www.w3.org/2000/svg', tagName); } const outsideArrowHash = 'outsideArrow'; export function renderShapes(state, svg, customSvg, freePieces) { const d = state.drawable, curD = d.current, cur = (curD === null || curD === void 0 ? void 0 : curD.dest) ? curD : undefined, outsideArrow = !!curD && !cur, arrowDests = new Map(), pieceMap = new Map(); const hashBounds = () => { // todo also possible piece bounds const bounds = state.dom.bounds.board.bounds(); return (bounds && bounds.width.toString() + bounds.height) || ''; }; for (const s of d.shapes.concat(d.autoShapes).concat(cur ? [cur] : [])) { const destName = isPiece(s.dest) ? pieceNameOf(s.dest) : s.dest; if (!samePieceOrKey(s.dest, s.orig)) arrowDests.set(destName, (arrowDests.get(destName) || 0) + 1); } for (const s of d.shapes.concat(cur ? [cur] : []).concat(d.autoShapes)) { if (s.piece && !isPiece(s.orig)) pieceMap.set(s.orig, s); } const pieceShapes = [...pieceMap.values()].map((s) => { return { shape: s, hash: shapeHash(s, arrowDests, false, hashBounds), }; }); const shapes = d.shapes.concat(d.autoShapes).map((s) => { return { shape: s, hash: shapeHash(s, arrowDests, false, hashBounds), }; }); if (cur) shapes.push({ shape: cur, hash: shapeHash(cur, arrowDests, true, hashBounds), current: true, }); const fullHash = shapes.map((sc) => sc.hash).join(';') + (outsideArrow ? outsideArrowHash : ''); if (fullHash === state.drawable.prevSvgHash) return; state.drawable.prevSvgHash = fullHash; /* -- DOM hierarchy -- <svg class="sg-shapes"> (<= svg) <defs> ...(for brushes)... </defs> <g> ...(for arrows and circles)... </g> </svg> <svg class="sg-custom-svgs"> (<= customSvg) <g> ...(for custom svgs)... </g> <sg-free-pieces> (<= freePieces) ...(for pieces)... </sg-free-pieces> </svg> */ const defsEl = svg.querySelector('defs'), shapesEl = svg.querySelector('g'), customSvgsEl = customSvg.querySelector('g'); syncDefs(shapes, outsideArrow ? curD : undefined, defsEl); syncShapes(shapes.filter((s) => !s.shape.customSvg && (!s.shape.piece || s.current)), shapesEl, (shape) => renderSVGShape(state, shape, arrowDests), outsideArrow); syncShapes(shapes.filter((s) => s.shape.customSvg), customSvgsEl, (shape) => renderSVGShape(state, shape, arrowDests)); syncShapes(pieceShapes, freePieces, (shape) => renderPiece(state, shape)); if (!outsideArrow && curD) curD.arrow = undefined; if (outsideArrow && !curD.arrow) { const orig = pieceOrKeyToPos(curD.orig, state); if (orig) { const g = setAttributes(createSVGElement('g'), { class: shapeClass(curD.brush, true, true), sgHash: outsideArrowHash, }), el = renderArrow(curD.brush, orig, orig, state.squareRatio, true, false); g.appendChild(el); curD.arrow = el; shapesEl.appendChild(g); } } } // append only. Don't try to update/remove. function syncDefs(shapes, outsideShape, defsEl) { const brushes = new Set(); for (const s of shapes) { if (!samePieceOrKey(s.shape.dest, s.shape.orig)) brushes.add(s.shape.brush); } if (outsideShape) brushes.add(outsideShape.brush); const keysInDom = new Set(); let el = defsEl.firstElementChild; while (el) { keysInDom.add(el.getAttribute('sgKey')); el = el.nextElementSibling; } for (const key of brushes) { const brush = key || 'primary'; if (!keysInDom.has(brush)) defsEl.appendChild(renderMarker(brush)); } } // append and remove only. No updates. export function syncShapes(shapes, root, renderShape, outsideArrow) { const hashesInDom = new Map(), // by hash toRemove = []; for (const sc of shapes) hashesInDom.set(sc.hash, false); if (outsideArrow) hashesInDom.set(outsideArrowHash, true); let el = root.firstElementChild, elHash; while (el) { elHash = el.getAttribute('sgHash'); // found a shape element that's here to stay if (hashesInDom.has(elHash)) hashesInDom.set(elHash, true); // or remove it else toRemove.push(el); el = el.nextElementSibling; } // remove old shapes for (const el of toRemove) root.removeChild(el); // insert shapes that are not yet in dom for (const sc of shapes) { if (!hashesInDom.get(sc.hash)) { const shapeEl = renderShape(sc); if (shapeEl) root.appendChild(shapeEl); } } } function shapeHash({ orig, dest, brush, piece, customSvg, description }, arrowDests, current, boundHash) { return [ current, (isPiece(orig) || isPiece(dest)) && boundHash(), isPiece(orig) ? pieceHash(orig) : orig, isPiece(dest) ? pieceHash(dest) : dest, brush, (arrowDests.get(isPiece(dest) ? pieceNameOf(dest) : dest) || 0) > 1, piece && pieceHash(piece), customSvg && customSvgHash(customSvg), description, ] .filter((x) => x) .join(','); } function pieceHash(piece) { return [piece.color, piece.role, piece.scale].filter((x) => x).join(','); } function customSvgHash(s) { // Rolling hash with base 31 (cf. https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript) let h = 0; for (let i = 0; i < s.length; i++) { h = ((h << 5) - h + s.charCodeAt(i)) >>> 0; } return 'custom-' + h.toString(); } function renderSVGShape(state, { shape, current, hash }, arrowDests) { const orig = pieceOrKeyToPos(shape.orig, state); if (!orig) return; if (shape.customSvg) { return renderCustomSvg(shape.brush, shape.customSvg, orig, state.squareRatio); } else { let el; const dest = !samePieceOrKey(shape.orig, shape.dest) && pieceOrKeyToPos(shape.dest, state); if (dest) { el = renderArrow(shape.brush, orig, dest, state.squareRatio, !!current, (arrowDests.get(isPiece(shape.dest) ? pieceNameOf(shape.dest) : shape.dest) || 0) > 1); } else if (samePieceOrKey(shape.dest, shape.orig)) { let ratio = state.squareRatio; if (isPiece(shape.orig)) { const pieceBounds = state.dom.bounds.hands.pieceBounds().get(pieceNameOf(shape.orig)), bounds = state.dom.bounds.board.bounds(); if (pieceBounds && bounds) { const heightBase = pieceBounds.height / (bounds.height / state.dimensions.ranks); // we want to keep the ratio that is on the board ratio = [heightBase * state.squareRatio[0], heightBase * state.squareRatio[1]]; } } el = renderEllipse(orig, ratio, !!current); } if (el) { const g = setAttributes(createSVGElement('g'), { class: shapeClass(shape.brush, !!current, false), sgHash: hash, }); g.appendChild(el); const descEl = shape.description && renderDescription(state, shape, arrowDests); if (descEl) g.appendChild(descEl); return g; } else return; } } function renderCustomSvg(brush, customSvg, pos, ratio) { const [x, y] = pos; // Translate to top-left of `orig` square const g = setAttributes(createSVGElement('g'), { transform: `translate(${x},${y})` }); const svg = setAttributes(createSVGElement('svg'), { class: brush, width: ratio[0], height: ratio[1], viewBox: `0 0 ${ratio[0] * 10} ${ratio[1] * 10}`, }); g.appendChild(svg); svg.innerHTML = customSvg; return g; } function renderEllipse(pos, ratio, current) { const o = pos, widths = ellipseWidth(ratio); return setAttributes(createSVGElement('ellipse'), { 'stroke-width': widths[current ? 0 : 1], fill: 'none', cx: o[0], cy: o[1], rx: ratio[0] / 2 - widths[1] / 2, ry: ratio[1] / 2 - widths[1] / 2, }); } function renderArrow(brush, orig, dest, ratio, current, shorten) { const m = arrowMargin(shorten && !current, ratio), a = orig, b = dest, dx = b[0] - a[0], dy = b[1] - a[1], angle = Math.atan2(dy, dx), xo = Math.cos(angle) * m, yo = Math.sin(angle) * m; return setAttributes(createSVGElement('line'), { 'stroke-width': lineWidth(current, ratio), 'stroke-linecap': 'round', 'marker-end': 'url(#arrowhead-' + (brush || 'primary') + ')', x1: a[0], y1: a[1], x2: b[0] - xo, y2: b[1] - yo, }); } export function renderPiece(state, { shape }) { if (!shape.piece || isPiece(shape.orig)) return; const orig = shape.orig, scale = (shape.piece.scale || 1) * (state.scaleDownPieces ? 0.5 : 1); const pieceEl = createEl('piece', pieceNameOf(shape.piece)); pieceEl.sgKey = orig; pieceEl.sgScale = scale; translateRel(pieceEl, posToTranslateRel(state.dimensions)(key2pos(orig), sentePov(state.orientation)), state.scaleDownPieces ? 0.5 : 1, scale); return pieceEl; } function renderDescription(state, shape, arrowDests) { const orig = pieceOrKeyToPos(shape.orig, state); if (!orig || !shape.description) return; const dest = !samePieceOrKey(shape.orig, shape.dest) && pieceOrKeyToPos(shape.dest, state), diff = dest ? [dest[0] - orig[0], dest[1] - orig[1]] : [0, 0], offset = (arrowDests.get(isPiece(shape.dest) ? pieceNameOf(shape.dest) : shape.dest) || 0) > 1 ? 0.3 : 0.15, close = (diff[0] === 0 || Math.abs(diff[0]) === state.squareRatio[0]) && (diff[1] === 0 || Math.abs(diff[1]) === state.squareRatio[1]), ratio = dest ? 0.55 - (close ? offset : 0) : 0, mid = [orig[0] + diff[0] * ratio, orig[1] + diff[1] * ratio], textLength = shape.description.length; const g = setAttributes(createSVGElement('g'), { class: 'description' }), circle = setAttributes(createSVGElement('ellipse'), { cx: mid[0], cy: mid[1], rx: textLength + 1.5, ry: 2.5, }), text = setAttributes(createSVGElement('text'), { x: mid[0], y: mid[1], 'text-anchor': 'middle', 'dominant-baseline': 'central', }); g.appendChild(circle); text.appendChild(document.createTextNode(shape.description)); g.appendChild(text); return g; } function renderMarker(brush) { const marker = setAttributes(createSVGElement('marker'), { id: 'arrowhead-' + brush, orient: 'auto', markerWidth: 4, markerHeight: 8, refX: 2.05, refY: 2.01, }); marker.appendChild(setAttributes(createSVGElement('path'), { d: 'M0,0 V4 L3,2 Z', })); marker.setAttribute('sgKey', brush); return marker; } export function setAttributes(el, attrs) { for (const key in attrs) { if (Object.prototype.hasOwnProperty.call(attrs, key)) el.setAttribute(key, attrs[key]); } return el; } export function pos2user(pos, color, dims, ratio) { return color === 'sente' ? [(dims.files - 1 - pos[0]) * ratio[0], pos[1] * ratio[1]] : [pos[0] * ratio[0], (dims.ranks - 1 - pos[1]) * ratio[1]]; } export function isPiece(x) { return typeof x === 'object'; } export function samePieceOrKey(kp1, kp2) { return (isPiece(kp1) && isPiece(kp2) && samePiece(kp1, kp2)) || kp1 === kp2; } export function usesBounds(shapes) { return shapes.some((s) => isPiece(s.orig) || isPiece(s.dest)); } function shapeClass(brush, current, outside) { return brush + (current ? ' current' : '') + (outside ? ' outside' : ''); } function ratioAverage(ratio) { return (ratio[0] + ratio[1]) / 2; } function ellipseWidth(ratio) { return [(3 / 64) * ratioAverage(ratio), (4 / 64) * ratioAverage(ratio)]; } function lineWidth(current, ratio) { return ((current ? 8.5 : 10) / 64) * ratioAverage(ratio); } function arrowMargin(shorten, ratio) { return ((shorten ? 20 : 10) / 64) * ratioAverage(ratio); } function pieceOrKeyToPos(kp, state) { if (isPiece(kp)) { const pieceBounds = state.dom.bounds.hands.pieceBounds().get(pieceNameOf(kp)), bounds = state.dom.bounds.board.bounds(), offset = sentePov(state.orientation) ? [0.5, -0.5] : [-0.5, 0.5], pos = pieceBounds && bounds && posOfOutsideEl(pieceBounds.left + pieceBounds.width / 2, pieceBounds.top + pieceBounds.height / 2, sentePov(state.orientation), state.dimensions, bounds); return (pos && pos2user([pos[0] + offset[0], pos[1] + offset[1]], state.orientation, state.dimensions, state.squareRatio)); } else return pos2user(key2pos(kp), state.orientation, state.dimensions, state.squareRatio); } //# sourceMappingURL=shapes.js.map