UNPKG

shogiground

Version:
1,311 lines (1,300 loc) 358 kB
"use strict"; var Shogiground = (() => { var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { default: () => index_default }); // src/constants.ts var colors = ["sente", "gote"]; var files = [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16" ]; var ranks = [ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p" ]; var allKeys = Array.prototype.concat( ...ranks.map((r) => files.map((f) => f + r)) ); // src/util.ts var pos2key = (pos) => allKeys[pos[0] + 16 * pos[1]]; var key2pos = (k) => { if (k.length > 2) return [k.charCodeAt(1) - 39, k.charCodeAt(2) - 97]; else return [k.charCodeAt(0) - 49, k.charCodeAt(1) - 97]; }; function memo(f) { let v; const ret = () => { if (v === void 0) v = f(); return v; }; ret.clear = () => { v = void 0; }; return ret; } function callUserFunction(f, ...args) { if (f) setTimeout(() => f(...args), 1); } var opposite = (c) => c === "sente" ? "gote" : "sente"; var sentePov = (o) => o === "sente"; var distanceSq = (pos1, pos2) => { const dx = pos1[0] - pos2[0], dy = pos1[1] - pos2[1]; return dx * dx + dy * dy; }; var samePiece = (p1, p2) => p1.role === p2.role && p1.color === p2.color; var posToTranslateBase = (pos, dims, asSente, xFactor, yFactor) => [ (asSente ? dims.files - 1 - pos[0] : pos[0]) * xFactor, (asSente ? pos[1] : dims.ranks - 1 - pos[1]) * yFactor ]; var posToTranslateAbs = (dims, bounds) => { const xFactor = bounds.width / dims.files, yFactor = bounds.height / dims.ranks; return (pos, asSente) => posToTranslateBase(pos, dims, asSente, xFactor, yFactor); }; var posToTranslateRel = (dims) => (pos, asSente) => posToTranslateBase(pos, dims, asSente, 100, 100); var translateAbs = (el, pos, scale) => { el.style.transform = `translate(${pos[0]}px,${pos[1]}px) scale(${scale}`; }; var translateRel = (el, percents, scaleFactor, scale) => { el.style.transform = `translate(${percents[0] * scaleFactor}%,${percents[1] * scaleFactor}%) scale(${scale || scaleFactor})`; }; var setDisplay = (el, v) => { el.style.display = v ? "" : "none"; }; var eventPosition = (e) => { var _a; if (e.clientX || e.clientX === 0) return [e.clientX, e.clientY]; if ((_a = e.targetTouches) == null ? void 0 : _a[0]) return [e.targetTouches[0].clientX, e.targetTouches[0].clientY]; return; }; var isRightButton = (e) => e.buttons === 2 || e.button === 2; var isMiddleButton = (e) => e.buttons === 4 || e.button === 1; var createEl = (tagName, className) => { const el = document.createElement(tagName); if (className) el.className = className; return el; }; function pieceNameOf(piece) { return `${piece.color} ${piece.role}`; } function isPieceNode(el) { return el.tagName === "PIECE"; } function isSquareNode(el) { return el.tagName === "SQ"; } function computeSquareCenter(key, asSente, dims, bounds) { const pos = key2pos(key); if (asSente) { pos[0] = dims.files - 1 - pos[0]; pos[1] = dims.ranks - 1 - pos[1]; } return [ bounds.left + bounds.width * pos[0] / dims.files + bounds.width / (dims.files * 2), bounds.top + bounds.height * (dims.ranks - 1 - pos[1]) / dims.ranks + bounds.height / (dims.ranks * 2) ]; } function domSquareIndexOfKey(key, asSente, dims) { const pos = key2pos(key); let index = dims.files - 1 - pos[0] + pos[1] * dims.files; if (!asSente) index = dims.files * dims.ranks - 1 - index; return index; } function isInsideRect(rect, pos) { return rect.left <= pos[0] && rect.top <= pos[1] && rect.left + rect.width > pos[0] && rect.top + rect.height > pos[1]; } function getKeyAtDomPos(pos, asSente, dims, bounds) { let file = Math.floor(dims.files * (pos[0] - bounds.left) / bounds.width); if (asSente) file = dims.files - 1 - file; let rank = Math.floor(dims.ranks * (pos[1] - bounds.top) / bounds.height); if (!asSente) rank = dims.ranks - 1 - rank; return file >= 0 && file < dims.files && rank >= 0 && rank < dims.ranks ? pos2key([file, rank]) : void 0; } function getHandPieceAtDomPos(pos, roles, bounds) { for (const color of colors) { for (const role of roles) { const piece = { color, role }, pieceRect = bounds.get(pieceNameOf(piece)); if (pieceRect && isInsideRect(pieceRect, pos)) return piece; } } return; } function posOfOutsideEl(left, top, asSente, dims, boardBounds) { const sqW = boardBounds.width / dims.files, sqH = boardBounds.height / dims.ranks; if (!sqW || !sqH) return; let xOff = (left - boardBounds.left) / sqW; if (asSente) xOff = dims.files - 1 - xOff; let yOff = (top - boardBounds.top) / sqH; if (!asSente) yOff = dims.ranks - 1 - yOff; return [xOff, yOff]; } // src/hands.ts function addToHand(s, piece, cnt = 1) { const hand = s.hands.handMap.get(piece.color), role = (s.hands.roles.includes(piece.role) ? piece.role : s.promotion.unpromotesTo(piece.role)) || piece.role; if (hand && s.hands.roles.includes(role)) hand.set(role, (hand.get(role) || 0) + cnt); } function removeFromHand(s, piece, cnt = 1) { const hand = s.hands.handMap.get(piece.color), role = (s.hands.roles.includes(piece.role) ? piece.role : s.promotion.unpromotesTo(piece.role)) || piece.role, num = hand == null ? void 0 : hand.get(role); if (hand && num) hand.set(role, Math.max(num - cnt, 0)); } function renderHand(s, handEl) { var _a; handEl.classList.toggle("promotion", !!s.promotion.current); let wrapEl = handEl.firstElementChild; while (wrapEl) { const pieceEl = wrapEl.firstElementChild, piece = { role: pieceEl.sgRole, color: pieceEl.sgColor }, num = ((_a = s.hands.handMap.get(piece.color)) == null ? void 0 : _a.get(piece.role)) || 0, isSelected = !!s.selectedPiece && samePiece(piece, s.selectedPiece) && !s.droppable.spare; wrapEl.classList.toggle( "selected", (s.activeColor === "both" || s.activeColor === s.turnColor) && isSelected ); wrapEl.classList.toggle( "preselected", s.activeColor !== "both" && s.activeColor !== s.turnColor && isSelected ); wrapEl.classList.toggle( "last-dest", s.highlight.lastDests && !!s.lastPiece && samePiece(piece, s.lastPiece) ); wrapEl.classList.toggle("drawing", !!s.drawable.piece && samePiece(s.drawable.piece, piece)); wrapEl.classList.toggle( "current-pre", !!s.predroppable.current && samePiece(s.predroppable.current.piece, piece) ); wrapEl.dataset.nb = num.toString(); wrapEl = wrapEl.nextElementSibling; } } // src/board.ts function toggleOrientation(state) { state.orientation = opposite(state.orientation); state.animation.current = state.draggable.current = state.promotion.current = state.hovered = state.selected = state.selectedPiece = void 0; } function reset(state) { unselect(state); unsetPremove(state); unsetPredrop(state); cancelPromotion(state); state.animation.current = state.draggable.current = state.hovered = void 0; } function setPieces(state, pieces) { for (const [key, piece] of pieces) { if (piece) state.pieces.set(key, piece); else state.pieces.delete(key); } } function setChecks(state, checksValue) { if (Array.isArray(checksValue)) { state.checks = checksValue; } else { if (checksValue === true) checksValue = state.turnColor; if (checksValue) { const checks = []; for (const [k, p] of state.pieces) { if (state.highlight.checkRoles.includes(p.role) && p.color === checksValue) checks.push(k); } state.checks = checks; } else state.checks = void 0; } } function setPremove(state, orig, dest, prom) { unsetPredrop(state); state.premovable.current = { orig, dest, prom }; callUserFunction(state.premovable.events.set, orig, dest, prom); } function unsetPremove(state) { if (state.premovable.current) { state.premovable.current = void 0; callUserFunction(state.premovable.events.unset); } } function setPredrop(state, piece, key, prom) { unsetPremove(state); state.predroppable.current = { piece, key, prom }; callUserFunction(state.predroppable.events.set, piece, key, prom); } function unsetPredrop(state) { if (state.predroppable.current) { state.predroppable.current = void 0; callUserFunction(state.predroppable.events.unset); } } function baseMove(state, orig, dest, prom) { const origPiece = state.pieces.get(orig), destPiece = state.pieces.get(dest); if (orig === dest || !origPiece) return false; const captured = destPiece && destPiece.color !== origPiece.color ? destPiece : void 0, promPiece = prom && promotePiece(state, origPiece); if (dest === state.selected || orig === state.selected) unselect(state); state.pieces.set(dest, promPiece || origPiece); state.pieces.delete(orig); state.lastDests = [orig, dest]; state.lastPiece = void 0; state.checks = void 0; callUserFunction(state.events.move, orig, dest, prom, captured); callUserFunction(state.events.change); return captured || true; } function baseDrop(state, piece, key, prom) { var _a; const pieceCount = ((_a = state.hands.handMap.get(piece.color)) == null ? void 0 : _a.get(piece.role)) || 0; if (!pieceCount && !state.droppable.spare) return false; const promPiece = prom && promotePiece(state, piece); if (key === state.selected || !state.droppable.spare && pieceCount === 1 && state.selectedPiece && samePiece(state.selectedPiece, piece)) unselect(state); state.pieces.set(key, promPiece || piece); state.lastDests = [key]; state.lastPiece = piece; state.checks = void 0; if (!state.droppable.spare) removeFromHand(state, piece); callUserFunction(state.events.drop, piece, key, prom); callUserFunction(state.events.change); return true; } function baseUserMove(state, orig, dest, prom) { const result = baseMove(state, orig, dest, prom); if (result) { state.movable.dests = void 0; state.droppable.dests = void 0; state.turnColor = opposite(state.turnColor); state.animation.current = void 0; } return result; } function baseUserDrop(state, piece, key, prom) { const result = baseDrop(state, piece, key, prom); if (result) { state.movable.dests = void 0; state.droppable.dests = void 0; state.turnColor = opposite(state.turnColor); state.animation.current = void 0; } return result; } function userDrop(state, piece, key, prom) { const realProm = prom || state.promotion.forceDropPromotion(piece, key); if (canDrop(state, piece, key)) { const result = baseUserDrop(state, piece, key, realProm); if (result) { unselect(state); callUserFunction(state.droppable.events.after, piece, key, realProm, { premade: false }); return true; } } else if (canPredrop(state, piece, key)) { setPredrop(state, piece, key, realProm); unselect(state); return true; } unselect(state); return false; } function userMove(state, orig, dest, prom) { const realProm = prom || state.promotion.forceMovePromotion(orig, dest); if (canMove(state, orig, dest)) { const result = baseUserMove(state, orig, dest, realProm); if (result) { unselect(state); const metadata = { premade: false }; if (result !== true) metadata.captured = result; callUserFunction(state.movable.events.after, orig, dest, realProm, metadata); return true; } } else if (canPremove(state, orig, dest)) { setPremove(state, orig, dest, realProm); unselect(state); return true; } unselect(state); return false; } function basePromotionDialog(state, piece, key) { const promotedPiece = promotePiece(state, piece); if (state.viewOnly || state.promotion.current || !promotedPiece) return false; state.promotion.current = { piece, promotedPiece, key, dragged: !!state.draggable.current }; state.hovered = key; return true; } function promotionDialogDrop(state, piece, key) { if (canDropPromote(state, piece, key) && (canDrop(state, piece, key) || canPredrop(state, piece, key))) { if (basePromotionDialog(state, piece, key)) { callUserFunction(state.promotion.events.initiated); return true; } } return false; } function promotionDialogMove(state, orig, dest) { if (canMovePromote(state, orig, dest) && (canMove(state, orig, dest) || canPremove(state, orig, dest))) { const piece = state.pieces.get(orig); if (piece && basePromotionDialog(state, piece, dest)) { callUserFunction(state.promotion.events.initiated); return true; } } return false; } function promotePiece(s, piece) { const promRole = s.promotion.promotesTo(piece.role); return promRole !== void 0 ? { color: piece.color, role: promRole } : void 0; } function deletePiece(state, key) { if (state.pieces.delete(key)) callUserFunction(state.events.change); } function selectSquare(state, key, prom, force) { callUserFunction(state.events.select, key); if (!state.draggable.enabled && state.selected === key) { callUserFunction(state.events.unselect, key); unselect(state); return; } if (state.selectable.enabled || force || state.selectable.forceSpares && state.selectedPiece && state.droppable.spare) { if (state.selectedPiece && userDrop(state, state.selectedPiece, key, prom)) return; else if (state.selected && userMove(state, state.selected, key, prom)) return; } if ((state.selectable.enabled || state.draggable.enabled || force) && (isMovable(state, key) || isPremovable(state, key))) { setSelected(state, key); } } function selectPiece(state, piece, spare, force, api) { callUserFunction(state.events.pieceSelect, piece); if (state.selectable.addSparesToHand && state.droppable.spare && state.selectedPiece) { addToHand(state, { role: state.selectedPiece.role, color: piece.color }); callUserFunction(state.events.change); unselect(state); } else if (!api && !state.draggable.enabled && state.selectedPiece && samePiece(state.selectedPiece, piece)) { callUserFunction(state.events.pieceUnselect, piece); unselect(state); } else if ((state.selectable.enabled || state.draggable.enabled || force) && (isDroppable(state, piece, !!spare) || isPredroppable(state, piece))) { setSelectedPiece(state, piece); state.droppable.spare = !!spare; } else { unselect(state); } } function setSelected(state, key) { unselect(state); state.selected = key; setPreDests(state); } function setSelectedPiece(state, piece) { unselect(state); state.selectedPiece = piece; setPreDests(state); } function setPreDests(state) { state.premovable.dests = state.predroppable.dests = void 0; if (state.selected && isPremovable(state, state.selected) && state.premovable.generate) state.premovable.dests = state.premovable.generate(state.selected, state.pieces); else if (state.selectedPiece && isPredroppable(state, state.selectedPiece) && state.predroppable.generate) state.predroppable.dests = state.predroppable.generate(state.selectedPiece, state.pieces); } function unselect(state) { state.selected = state.selectedPiece = state.premovable.dests = state.predroppable.dests = state.promotion.current = void 0; state.droppable.spare = false; } function isMovable(state, orig) { const piece = state.pieces.get(orig); return !!piece && (state.activeColor === "both" || state.activeColor === piece.color && state.turnColor === piece.color); } function isDroppable(state, piece, spare) { var _a; return (spare || !!((_a = state.hands.handMap.get(piece.color)) == null ? void 0 : _a.get(piece.role))) && (state.activeColor === "both" || state.activeColor === piece.color && state.turnColor === piece.color); } function canMove(state, orig, dest) { var _a, _b; return orig !== dest && isMovable(state, orig) && (state.movable.free || !!((_b = (_a = state.movable.dests) == null ? void 0 : _a.get(orig)) == null ? void 0 : _b.includes(dest))); } function canDrop(state, piece, dest) { var _a, _b; return isDroppable(state, piece, state.droppable.spare) && (state.droppable.free || state.droppable.spare || !!((_b = (_a = state.droppable.dests) == null ? void 0 : _a.get(pieceNameOf(piece))) == null ? void 0 : _b.includes(dest))); } function canMovePromote(state, orig, dest) { const piece = state.pieces.get(orig); return !!piece && state.promotion.movePromotionDialog(orig, dest); } function canDropPromote(state, piece, key) { return !state.droppable.spare && state.promotion.dropPromotionDialog(piece, key); } function isPremovable(state, orig) { const piece = state.pieces.get(orig); return !!piece && state.premovable.enabled && state.activeColor === piece.color && state.turnColor !== piece.color; } function isPredroppable(state, piece) { var _a; return !!((_a = state.hands.handMap.get(piece.color)) == null ? void 0 : _a.get(piece.role)) && state.predroppable.enabled && state.activeColor === piece.color && state.turnColor !== piece.color; } function canPremove(state, orig, dest) { return orig !== dest && isPremovable(state, orig) && !!state.premovable.generate && state.premovable.generate(orig, state.pieces).includes(dest); } function canPredrop(state, piece, dest) { const destPiece = state.pieces.get(dest); return isPredroppable(state, piece) && (!destPiece || destPiece.color !== state.activeColor) && !!state.predroppable.generate && state.predroppable.generate(piece, state.pieces).includes(dest); } function isDraggable(state, piece) { return state.draggable.enabled && (state.activeColor === "both" || state.activeColor === piece.color && (state.turnColor === piece.color || state.premovable.enabled)); } function playPremove(state) { const move3 = state.premovable.current; if (!move3) return false; const orig = move3.orig, dest = move3.dest, prom = move3.prom; let success = false; if (canMove(state, orig, dest)) { const result = baseUserMove(state, orig, dest, prom); if (result) { const metadata = { premade: true }; if (result !== true) metadata.captured = result; callUserFunction(state.movable.events.after, orig, dest, prom, metadata); success = true; } } unsetPremove(state); return success; } function playPredrop(state) { const drop = state.predroppable.current; if (!drop) return false; const piece = drop.piece, key = drop.key, prom = drop.prom; let success = false; if (canDrop(state, piece, key)) { if (baseUserDrop(state, piece, key, prom)) { callUserFunction(state.droppable.events.after, piece, key, prom, { premade: true }); success = true; } } unsetPredrop(state); return success; } function cancelMoveOrDrop(state) { unsetPremove(state); unsetPredrop(state); unselect(state); } function cancelPromotion(state) { if (!state.promotion.current) return; unselect(state); state.promotion.current = void 0; state.hovered = void 0; callUserFunction(state.promotion.events.cancel); } function stop(state) { state.activeColor = state.movable.dests = state.droppable.dests = state.draggable.current = state.animation.current = state.promotion.current = state.hovered = void 0; cancelMoveOrDrop(state); } // src/sfen.ts function inferDimensions(boardSfen) { const ranks2 = boardSfen.split("/"), firstFile = ranks2[0].split(""); let filesCnt = 0, cnt = 0; for (const c of firstFile) { const nb = c.charCodeAt(0); if (nb < 58 && nb > 47) cnt = cnt * 10 + nb - 48; else if (c !== "+") { filesCnt += cnt + 1; cnt = 0; } } filesCnt += cnt; return { files: filesCnt, ranks: ranks2.length }; } function sfenToBoard(sfen, dims, fromForsyth) { const sfenParser = fromForsyth || standardFromForsyth, pieces = /* @__PURE__ */ new Map(); let x = dims.files - 1, y = 0; for (let i = 0; i < sfen.length; i++) { switch (sfen[i]) { case " ": case "_": return pieces; case "/": ++y; if (y > dims.ranks - 1) return pieces; x = dims.files - 1; break; default: { const nb1 = sfen[i].charCodeAt(0), nb2 = sfen[i + 1] && sfen[i + 1].charCodeAt(0); if (nb1 < 58 && nb1 > 47) { if (nb2 && nb2 < 58 && nb2 > 47) { x -= (nb1 - 48) * 10 + (nb2 - 48); i++; } else x -= nb1 - 48; } else { const roleStr = sfen[i] === "+" && sfen.length > i + 1 ? "+" + sfen[++i] : sfen[i], role = sfenParser(roleStr); if (x >= 0 && role) { const color = roleStr === roleStr.toLowerCase() ? "gote" : "sente"; pieces.set(pos2key([x, y]), { role, color }); } --x; } } } } return pieces; } function boardToSfen(pieces, dims, toForsyth) { const sfenRenderer = toForsyth || standardToForsyth, reversedFiles = files.slice(0, dims.files).reverse(); return ranks.slice(0, dims.ranks).map( (y) => reversedFiles.map((x) => { const piece = pieces.get(x + y), forsyth = piece && sfenRenderer(piece.role); if (forsyth) { return piece.color === "sente" ? forsyth.toUpperCase() : forsyth.toLowerCase(); } else return "1"; }).join("") ).join("/").replace(/1{2,}/g, (s) => s.length.toString()); } function sfenToHands(sfen, fromForsyth) { const sfenParser = fromForsyth || standardFromForsyth, sente = /* @__PURE__ */ new Map(), gote = /* @__PURE__ */ new Map(); let tmpNum = 0, num = 1; for (let i = 0; i < sfen.length; i++) { const nb = sfen[i].charCodeAt(0); if (nb < 58 && nb > 47) { tmpNum = tmpNum * 10 + nb - 48; num = tmpNum; } else { const roleStr = sfen[i] === "+" && sfen.length > i + 1 ? "+" + sfen[++i] : sfen[i], role = sfenParser(roleStr); if (role) { const color = roleStr === roleStr.toLowerCase() ? "gote" : "sente"; if (color === "sente") sente.set(role, (sente.get(role) || 0) + num); else gote.set(role, (gote.get(role) || 0) + num); } tmpNum = 0; num = 1; } } return /* @__PURE__ */ new Map([ ["sente", sente], ["gote", gote] ]); } function handsToSfen(hands, roles, toForsyth) { var _a, _b; const sfenRenderer = toForsyth || standardToForsyth; let senteHandStr = "", goteHandStr = ""; for (const role of roles) { const forsyth = sfenRenderer(role); if (forsyth) { const senteCnt = (_a = hands.get("sente")) == null ? void 0 : _a.get(role), goteCnt = (_b = hands.get("gote")) == null ? void 0 : _b.get(role); if (senteCnt) senteHandStr += senteCnt > 1 ? senteCnt.toString() + forsyth : forsyth; if (goteCnt) goteHandStr += goteCnt > 1 ? goteCnt.toString() + forsyth : forsyth; } } if (senteHandStr || goteHandStr) return senteHandStr.toUpperCase() + goteHandStr.toLowerCase(); else return "-"; } function standardFromForsyth(forsyth) { switch (forsyth.toLowerCase()) { case "p": return "pawn"; case "l": return "lance"; case "n": return "knight"; case "s": return "silver"; case "g": return "gold"; case "b": return "bishop"; case "r": return "rook"; case "+p": return "tokin"; case "+l": return "promotedlance"; case "+n": return "promotedknight"; case "+s": return "promotedsilver"; case "+b": return "horse"; case "+r": return "dragon"; case "k": return "king"; default: return; } } function standardToForsyth(role) { switch (role) { case "pawn": return "p"; case "lance": return "l"; case "knight": return "n"; case "silver": return "s"; case "gold": return "g"; case "bishop": return "b"; case "rook": return "r"; case "tokin": return "+p"; case "promotedlance": return "+l"; case "promotedknight": return "+n"; case "promotedsilver": return "+s"; case "horse": return "+b"; case "dragon": return "+r"; case "king": return "k"; default: return; } } // src/config.ts function applyAnimation(state, config) { if (config.animation) { deepMerge(state.animation, config.animation); if ((state.animation.duration || 0) < 70) state.animation.enabled = false; } } function configure(state, config) { var _a, _b, _c, _d, _e, _f, _g, _h, _i; if ((_a = config.movable) == null ? void 0 : _a.dests) state.movable.dests = void 0; if ((_b = config.droppable) == null ? void 0 : _b.dests) state.droppable.dests = void 0; if ((_c = config.drawable) == null ? void 0 : _c.shapes) state.drawable.shapes = []; if ((_d = config.drawable) == null ? void 0 : _d.autoShapes) state.drawable.autoShapes = []; if ((_e = config.drawable) == null ? void 0 : _e.squares) state.drawable.squares = []; if ((_f = config.hands) == null ? void 0 : _f.roles) state.hands.roles = []; deepMerge(state, config); if ((_g = config.sfen) == null ? void 0 : _g.board) { state.dimensions = inferDimensions(config.sfen.board); state.pieces = sfenToBoard(config.sfen.board, state.dimensions, state.forsyth.fromForsyth); state.drawable.shapes = ((_h = config.drawable) == null ? void 0 : _h.shapes) || []; } if ((_i = config.sfen) == null ? void 0 : _i.hands) { state.hands.handMap = sfenToHands(config.sfen.hands, state.forsyth.fromForsyth); } if ("checks" in config) setChecks(state, config.checks || false); if ("lastPiece" in config && !config.lastPiece) state.lastPiece = void 0; if ("lastDests" in config && !config.lastDests) state.lastDests = void 0; else if (config.lastDests) state.lastDests = config.lastDests; setPreDests(state); applyAnimation(state, config); } function deepMerge(base, extend) { for (const key in extend) { if (Object.prototype.hasOwnProperty.call(extend, key)) { if (Object.prototype.hasOwnProperty.call(base, key) && isPlainObject(base[key]) && isPlainObject(extend[key])) deepMerge(base[key], extend[key]); else base[key] = extend[key]; } } } function isPlainObject(o) { if (typeof o !== "object" || o === null) return false; const proto = Object.getPrototypeOf(o); return proto === Object.prototype || proto === null; } // src/anim.ts function anim(mutation, state) { return state.animation.enabled ? animate(mutation, state) : render(mutation, state); } function render(mutation, state) { const result = mutation(state); state.dom.redraw(); return result; } function makePiece(key, piece) { return { key, pos: key2pos(key), piece }; } function closer(piece, pieces) { return pieces.sort((p1, p2) => { return distanceSq(piece.pos, p1.pos) - distanceSq(piece.pos, p2.pos); })[0]; } function computePlan(prevPieces, prevHands, current) { const anims = /* @__PURE__ */ new Map(), animedOrigs = [], fadings = /* @__PURE__ */ new Map(), promotions = /* @__PURE__ */ new Map(), missings = [], news = [], prePieces = /* @__PURE__ */ new Map(); for (const [k, p] of prevPieces) { prePieces.set(k, makePiece(k, p)); } for (const key of allKeys) { const curP = current.pieces.get(key), preP = prePieces.get(key); if (curP) { if (preP) { if (!samePiece(curP, preP.piece)) { missings.push(preP); news.push(makePiece(key, curP)); } } else news.push(makePiece(key, curP)); } else if (preP) missings.push(preP); } if (current.animation.hands) { for (const color of colors) { const curH = current.hands.handMap.get(color), preH = prevHands.get(color); if (preH && curH) { for (const [role, n] of preH) { const piece = { role, color }, curN = curH.get(role) || 0; if (curN < n) { const handPieceOffset = current.dom.bounds.hands.pieceBounds().get(pieceNameOf(piece)), bounds = current.dom.bounds.board.bounds(), outPos = handPieceOffset && bounds ? posOfOutsideEl( handPieceOffset.left, handPieceOffset.top, sentePov(current.orientation), current.dimensions, bounds ) : void 0; if (outPos) missings.push({ pos: outPos, piece }); } } } } } for (const newP of news) { const preP = closer( newP, missings.filter((p) => { if (samePiece(newP.piece, p.piece)) return true; const pRole = current.promotion.promotesTo(p.piece.role), pPiece = pRole && { color: p.piece.color, role: pRole }; const nRole = current.promotion.promotesTo(newP.piece.role), nPiece = nRole && { color: newP.piece.color, role: nRole }; return !!pPiece && samePiece(newP.piece, pPiece) || !!nPiece && samePiece(nPiece, p.piece); }) ); if (preP) { const vector = [preP.pos[0] - newP.pos[0], preP.pos[1] - newP.pos[1]]; anims.set(newP.key, vector.concat(vector)); if (preP.key) animedOrigs.push(preP.key); if (!samePiece(newP.piece, preP.piece) && newP.key) promotions.set(newP.key, preP.piece); } } for (const p of missings) { if (p.key && !animedOrigs.includes(p.key)) fadings.set(p.key, p.piece); } return { anims, fadings, promotions }; } function step(state, now) { const cur = state.animation.current; if (cur === void 0) { if (!state.dom.destroyed) state.dom.redrawNow(); return; } const rest = 1 - (now - cur.start) * cur.frequency; if (rest <= 0) { state.animation.current = void 0; state.dom.redrawNow(); } else { const ease = easing(rest); for (const cfg of cur.plan.anims.values()) { cfg[2] = cfg[0] * ease; cfg[3] = cfg[1] * ease; } state.dom.redrawNow(true); requestAnimationFrame((now2 = performance.now()) => step(state, now2)); } } function animate(mutation, state) { var _a; const prevPieces = new Map(state.pieces), prevHands = /* @__PURE__ */ new Map([ ["sente", new Map(state.hands.handMap.get("sente"))], ["gote", new Map(state.hands.handMap.get("gote"))] ]); const result = mutation(state), plan = computePlan(prevPieces, prevHands, state); if (plan.anims.size || plan.fadings.size) { const alreadyRunning = ((_a = state.animation.current) == null ? void 0 : _a.start) !== void 0; state.animation.current = { start: performance.now(), frequency: 1 / Math.max(state.animation.duration, 1), plan }; if (!alreadyRunning) step(state, performance.now()); } else { state.dom.redraw(); } return result; } function easing(t) { return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1; } // src/shapes.ts function createSVGElement(tagName) { return document.createElementNS("http://www.w3.org/2000/svg", tagName); } var outsideArrowHash = "outsideArrow"; function renderShapes(state, svg, customSvg, freePieces) { const d = state.drawable, curD = d.current, cur = (curD == null ? void 0 : curD.dest) ? curD : void 0, outsideArrow = !!curD && !cur, arrowDests = /* @__PURE__ */ new Map(), pieceMap = /* @__PURE__ */ new Map(); const hashBounds = () => { 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; const defsEl = svg.querySelector("defs"), shapesEl = svg.querySelector("g"), customSvgsEl = customSvg.querySelector("g"); syncDefs(shapes, outsideArrow ? curD : void 0, 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 = void 0; 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); } } } function syncDefs(shapes, outsideShape, defsEl) { const brushes2 = /* @__PURE__ */ new Set(); for (const s of shapes) { if (!samePieceOrKey(s.shape.dest, s.shape.orig)) brushes2.add(s.shape.brush); } if (outsideShape) brushes2.add(outsideShape.brush); const keysInDom = /* @__PURE__ */ new Set(); let el = defsEl.firstElementChild; while (el) { keysInDom.add(el.getAttribute("sgKey")); el = el.nextElementSibling; } for (const key of brushes2) { const brush = key || "primary"; if (!keysInDom.has(brush)) defsEl.appendChild(renderMarker(brush)); } } function syncShapes(shapes, root, renderShape, outsideArrow) { const hashesInDom = /* @__PURE__ */ new Map(), 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"); if (hashesInDom.has(elHash)) hashesInDom.set(elHash, true); else toRemove.push(el); el = el.nextElementSibling; } for (const el2 of toRemove) root.removeChild(el2); 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) { 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); 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; 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 }); } 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; } function setAttributes(el, attrs) { for (const key in attrs) { if (Object.prototype.hasOwnProperty.call(attrs, key)) el.setAttribute(key, attrs[key]); } return el; } 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]]; } function isPiece(x) { return typeof x === "object"; } function samePieceOrKey(kp1, kp2) { return isPiece(kp1) && isPiece(kp2) && samePiece(kp1, kp2) || kp1 === kp2; } 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); } // src/draw.ts var brushes = ["primary", "alternative0", "alternative1", "alternative2"]; function start(state, e) { if (e.touches && e.touches.length > 1) return; e.stopPropagation(); e.preventDefault(); if (e.ctrlKey) unselect(state); else cancelMoveOrDrop(state); const pos = eventPosition(e), bounds = state.dom.bounds.board.bounds(), orig = pos && bounds && getKeyAtDomPos(pos, sentePov(state.orientation), state.dimensions, bounds), piece = state.drawable.piece; if (!orig) return; state.drawable.current = { orig, dest: void 0, pos, piece, brush: eventBrush(e, isRightButton(e) || state.drawable.forced) }; processDraw(state); } function startFromHand(state, piece, e) { if (e.touches && e.touches.length > 1) return; e.stopPropagation(); e.preventDefault(); if (e.ctrlKey) unselect(state); else cancelMoveOrDrop(state); const pos = eventPosition(e); if (!pos) return; state.drawable.current = { orig: piece, dest: void 0, pos, brush: eventBrush(e, isRightButton(e) || state.drawable.forced) }; processDraw(state); } function processDraw(state) { requestAnimationFrame(() => { const cur = state.drawable.current, bounds = state.dom.bounds.board.bounds(); if (cur && bounds) { const dest = getKeyAtDomPos(cur.pos, sentePov(state.orientation), state.dimensions, bounds) || getHandPieceAtDomPos(cur.pos, state.hands.roles, state.dom.bounds.hands.pieceBounds()); if (cur.dest !== dest && !(cur.dest && dest && samePieceOrKey(dest, cur.dest))) { cur.dest = dest; state.dom.redrawNow(); } const outPos = posOfOutsideEl( cur.pos[0], cur.pos[1], sentePov(state.orientation), state.dimensions, bounds ); if (!cur.dest && cur.arrow && outPos) { const dest2 = pos2user(outPos, state.orientation, state.dimensions, state.squareRatio); setAttributes(cur.arrow, { x2: dest2[0] - state.squareRatio[0] / 2, y2: dest2[1] - state.squareRatio[1] / 2 }); } processDraw(state); } }); } function move(state, e) { if (state.drawable.current) state.drawable.current.pos = eventPosition(e); } function end(state, _) { const cur = state.drawable.current; if (cur) { addShape(state.drawable, cur); cancel(state); } } function cancel(state) { if (state.drawable.current) { state.drawable.current = void 0; state.dom.redraw(); } } function clear(state) { const drawableLength = state.drawable.shapes.length; if (drawableLength || state.drawable.piece) { state.drawable.shapes = []; state.drawable.piece = void 0; state.dom.redraw(); if (drawableLength) onChange(state.drawable); } } function setDrawPiece(state, piece) { if (state.drawable.piece && samePiece(state.drawable.piece, piece)) state.drawable.piece = void 0; else state.drawable.piece = piece; state.dom.redraw(); } function eventBrush(e, allowFirstModifier) { var _a; const modA = allowFirstModifier && (e.shiftKey || e.ctrlKey), modB = e.altKey || e.metaKey || ((_a = e.getModifierState) == null ? void 0 : _a.call(e, "AltGraph")); return brushes[(modA ? 1 : 0) + (modB ? 2 : 0)]; } function addShape(drawable, cur) { if (!cur.dest) return; const similarShape = (s) => cur.dest && samePieceOrKey(cur.orig, s.orig) && samePieceOrKey(cur.dest, s.dest); const piece = cur.piece; cur.piece = void 0; const similar = drawable.shapes.find(similarShape), removePiece = drawable.shapes.find( (s) => similarShape(s) && piece && s.piece && samePiece(piece, s.piece) ), diffPiece = drawable.shapes.find( (s) => similarShape(s) && piece && s.piece && !samePiece(piece, s.piece) ); if (similar) drawable.shapes = drawable.shapes.filter((s) => !similarShape(s)); if (!isPiece(cur.orig) && piece && !removePiece) { drawable.shapes.push({ orig: cur.orig, dest: cur.orig, piece, brush: cur.brush }); if (!samePieceOrKey(cur.orig, cur.dest)) drawable.shapes.push({ orig: cur.orig, dest: cur.orig, brush: cur.brush }); } if (!similar || diffPiece || similar.brush !== cur.brush) drawable.shapes.push(cur); onChange(drawable); } function onChange(drawable) { if (drawable.onChange) drawable.onChange(drawable.shapes); } // src/drag.ts function start2(s, e) { var _a; const bounds = s.dom.bounds.board.bounds(), position = eventPosition(e), orig = bounds && position && getKeyAtDomPos(position, sentePov(s.orientation), s.dimensions, bounds); if (!orig) return; const piece = s.pieces.get(orig),