A userscript library for seamlessly adding chess move arrows to game boards on popular platforms like Chess.com and Lichess.org
สคริปต์นี้ไม่ควรถูกติดตั้งโดยตรง มันเป็นคลังสำหรับสคริปต์อื่น ๆ เพื่อบรรจุด้วยคำสั่งเมทา // @require https://update.greasyfork.org/scripts/470417/1455862/UniversalBoardDrawerjs.js
/* UniversalBoardDrawer.js - Version: 1.3.4 - Author: Haka - Description: A userscript library for seamlessly adding chess move arrows to game boards on popular platforms like Chess.com and Lichess.org - GitHub: https://github.com/Hakorr/UniversalBoardDrawer */ class UniversalBoardDrawer { constructor(boardElem, config) { this.boardElem = boardElem; this.window = config?.window || window; this.document = this.window?.document; this.parentElem = config?.parentElem || this.document.body; this.boardDimensions = { 'width': config?.boardDimensions?.[0] || 8, 'height': config?.boardDimensions?.[1] || 8 }; this.adjustSizeByDimensions = config?.adjustSizeByDimensions || false; this.adjustSizeConfig = config?.adjustSizeConfig; this.orientation = config?.orientation || 'w'; this.zIndex = config?.zIndex || 1000; // container z-index this.usePrepend = config?.prepend || false; this.debugMode = config?.debugMode || false; this.ignoreBodyRectLeft = config?.ignoreBodyRectLeft || false; this.boardContainerElem = null; this.singleSquareSize = null; this.lastInputPositionStr = null; this.lastInputPosition = null; this.addedShapes = []; this.squareSvgCoordinates = []; this.observers = []; this.customActivityListeners = []; this.defaultFillColor = 'mediumseagreen'; this.defaultOpacity = 0.8; this.updateInterval = 100; this.isInputDown = false; this.terminated = false; if(!this.document) { if(this.debugMode) console.error(`Inputted document element doesn't exist!`); return; } if(!this.boardElem) { if(this.debugMode) console.error(`Inputted board element doesn't exist!`); return; } if(typeof this.boardDimensions != 'object') { if(this.debugMode) console.error(`Invalid board dimensions value, please use array! (e.g. [8, 8])`); return; } this.createOverlaySVG(); const handleMouseMove = e => { if (this.terminated) { this.document.removeEventListener('mousemove', handleMouseMove); return; } this.handleMouseEvent.bind(this)(e); }; const handleTouchStart = e => { if (this.terminated) { this.document.removeEventListener('touchstart', handleTouchStart); return; } this.handleMouseEvent.bind(this)(e); }; const handleMouseDown = () => { if (this.terminated) { this.document.removeEventListener('mousedown', handleMouseDown); return; } this.isInputDown = true; }; const handleMouseUp = () => { if (this.terminated) { this.document.removeEventListener('mouseup', handleMouseUp); return; } this.isInputDown = false; }; this.document.addEventListener('mousemove', handleMouseMove); this.document.addEventListener('touchstart', handleTouchStart); this.document.addEventListener('mousedown', handleMouseDown); this.document.addEventListener('mouseup', handleMouseUp); } setOrientation(orientation) { this.orientation = orientation; this.updateDimensions(); } setBoardDimensions(dimensionArr) { const [width, height] = dimensionArr || [8, 8]; this.boardDimensions = { width, height }; this.updateDimensions(); } setAdjustSizeByDimensions(boolean) { this.adjustSizeByDimensions = boolean; this.updateDimensions(); } createArrowBetweenPositions(from, to, config) { const fromCoordinateObj = this.squareSvgCoordinates.find(x => this.coordinateToFen(x.coordinates) == from); const toCoordinateObj = this.squareSvgCoordinates.find(x => this.coordinateToFen(x.coordinates) == to); if(!fromCoordinateObj || !toCoordinateObj) { if(this.debugMode) console.error('Coordinates', from, to, 'do not exist. Possibly out of bounds?'); return; } const [fromX, fromY] = fromCoordinateObj?.positions; const [toX, toY] = toCoordinateObj?.positions; const distance = Math.sqrt(Math.pow(fromX - toX, 2) + Math.pow(fromY - toY, 2)); const angle = Math.atan2(fromY - toY, fromX - toX); const scale = this.singleSquareSize / 100; const lineWidth = (config?.lineWidth || 15) * scale; const arrowheadWidth = (config?.arrowheadWidth || 55) * scale; const arrowheadHeight = (config?.arrowheadHeight || 45) * scale; const startOffset = (config?.startOffset || 20) * scale; const existingArrowElem = config?.existingElem; const arrowElem = typeof existingArrowElem === 'object' ? existingArrowElem : this.document.createElementNS('http://www.w3.org/2000/svg', 'polygon'); arrowElem.setAttribute('transform', `rotate(${angle * (180 / Math.PI) - 90} ${fromX} ${fromY})`); const arrowPoints = [ { x: fromX - lineWidth / 2, y: fromY - startOffset }, { x: fromX - lineWidth / 2, y: fromY - distance + arrowheadHeight }, { x: fromX - arrowheadWidth / 2, y: fromY - distance + arrowheadHeight }, { x: fromX, y: fromY - distance }, { x: fromX + arrowheadWidth / 2, y: fromY - distance + arrowheadHeight }, { x: fromX + lineWidth / 2, y: fromY - distance + arrowheadHeight }, { x: fromX + lineWidth / 2, y: fromY - startOffset } ]; const pointsString = arrowPoints.map(point => `${point.x},${point.y}`).join(' '); arrowElem.setAttribute('points', pointsString); arrowElem.style.fill = this.defaultFillColor; arrowElem.style.opacity = this.defaultOpacity; const style = config?.style; if(style) arrowElem.setAttribute('style', style); return arrowElem; } createDotOnSVG(x, y) { const dot = this.document.createElementNS('http://www.w3.org/2000/svg', 'circle'); dot.setAttribute('cx', x); dot.setAttribute('cy', y); dot.setAttribute('r', '1'); dot.setAttribute('fill', 'black'); this.addedShapes.push({ type: 'debugDot', 'element': dot }); this.boardContainerElem.appendChild(dot); } removeAllExistingShapes() { this.addedShapes .forEach(shapeObj => { shapeObj.element?.remove(); }); } removeAllDebugDots() { this.addedShapes .filter(shapeObj => shapeObj.type == 'debugDot') .forEach(debugDotObj => { debugDotObj.element?.remove(); }); } updateShapes() { if(this.debugMode) { this.removeAllDebugDots(); this.squareSvgCoordinates.forEach(x => this.createDotOnSVG(...x.positions)); } this.addedShapes .filter(shapeObj => shapeObj.type != 'debugDot') .forEach(shapeObj => { const newShapeElem = this.createArrowBetweenPositions(...shapeObj.positions, shapeObj.config); this.transferAttributes(newShapeElem, shapeObj.element); }); } coordinateToFen(coordinates) { let [x, y] = coordinates; x = this.orientation == 'w' ? x : this.boardDimensions.width - x + 1; y = this.orientation == 'b' ? y : this.boardDimensions.height - y + 1; const getCharacter = num => String.fromCharCode(96 + num); const file = getCharacter(x); const rank = y; return file + rank; } updateCoords() { this.squareSvgCoordinates = []; // reset coordinate array // calculate every square center point coordinates relative to the svg for(let y = 0; this.boardDimensions.height > y; y++) { for(let x = 0; this.boardDimensions.width > x; x++) { this.squareSvgCoordinates.push({ coordinates: [x + 1, y + 1], positions: [this.squareWidth / 2 + (this.squareWidth * x), this.squareHeight / 2 + (this.squareHeight * y)] }); } } } transferAttributes(fromElem, toElem) { if(fromElem && fromElem?.attributes && toElem) { [...fromElem.attributes].forEach(attr => toElem.setAttribute(attr.name, attr.value)); } } createShape(type, positions, config) { if(this.terminated) { if(this.debugMode) console.warn('Failed to create shape! Tried to create shape after termination!'); return false; } if(!this.boardContainerElem) { if(this.debugMode) console.warn(`Failed to create shape! Board SVG doesn't exist yet! (createOverlaySVG() failed?)`); return false; } switch(type) { case 'arrow': const element = this.createArrowBetweenPositions(...positions, config); if(element) { this.addedShapes.push({ type, positions, config, element }); if(this.usePrepend) { this.boardContainerElem.prepend(element); } else { this.boardContainerElem.appendChild(element); } return element; } break; } return null; } updateDimensions() { const boardRect = this.boardElem.getBoundingClientRect(), bodyRect = this.document.body.getBoundingClientRect(); // https://stackoverflow.com/a/62106310 let boardWidth = boardRect.width, boardHeight = boardRect.height; let boardPositionTop = boardRect.top - bodyRect.top, boardPositionLeft = boardRect.left - (this.ignoreBodyRectLeft ? 0 : bodyRect.left); if(this.adjustSizeByDimensions) { if(this.boardDimensions.width > this.boardDimensions.height) { const multiplier = this.boardDimensions.height / this.boardDimensions.width, newHeight = boardWidth * multiplier; if(boardHeight !== newHeight) { if(!this.adjustSizeConfig?.noTopAdjustment) boardPositionTop += (boardHeight - newHeight) / 2; boardHeight = newHeight; } } else { const multiplier = this.boardDimensions.width / this.boardDimensions.height, newWidth = boardWidth * multiplier; if(boardWidth !== newWidth) { if(!this.adjustSizeConfig?.noLeftAdjustment) boardPositionLeft += (boardWidth - newWidth) / 2; boardWidth = newWidth; } } } this.boardContainerElem.style.width = boardWidth + 'px'; this.boardContainerElem.style.height = boardHeight + 'px'; this.boardContainerElem.style.left = boardPositionLeft + 'px'; this.boardContainerElem.style.top = boardPositionTop + 'px'; const squareWidth = boardWidth / this.boardDimensions.width; const squareHeight = boardHeight / this.boardDimensions.height; this.singleSquareSize = squareWidth; this.squareWidth = squareWidth; this.squareHeight = squareHeight; this.updateCoords(); this.updateShapes(); } createOverlaySVG() { const svg = this.document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.style.position = 'absolute'; svg.style.pointerEvents = 'none'; svg.style['z-index'] = this.zIndex; this.boardContainerElem = svg; this.updateDimensions(); this.parentElem.appendChild(this.boardContainerElem); const rObs = new ResizeObserver(this.updateDimensions.bind(this)); rObs.observe(this.boardElem); rObs.observe(this.document.body); this.observers.push(rObs); let oldBoardRect = JSON.stringify(this.boardElem.getBoundingClientRect()); const additionalCheckLoop = setInterval(() => { if(this.terminated) { clearInterval(additionalCheckLoop); return; } const boardRect = JSON.stringify(this.boardElem.getBoundingClientRect()); if(boardRect !== oldBoardRect) { oldBoardRect = boardRect; this.updateDimensions(); } }, this.updateInterval); } getCoordinatesFromInputPosition(e) { const boardRect = this.boardElem.getBoundingClientRect(); const { clientX, clientY } = e.touches ? e.touches[0] : e; const isOutOfBounds = clientX < boardRect.left || clientX > boardRect.right || clientY < boardRect.top || clientY > boardRect.bottom; const relativeX = clientX - boardRect.left; const relativeY = clientY - boardRect.top; return isOutOfBounds ? [null, null] : [Math.floor(relativeX / this.squareWidth) + 1, Math.floor(relativeY / this.squareHeight) + 1]; } handleMouseEvent(e) { if(this.isInputDown) return; const position = this.getCoordinatesFromInputPosition(e), positionStr = position?.toString(); if(positionStr != this.lastInputPositionStr) { const enteredSquareListeners = this.customActivityListeners.filter(obj => obj.square == this.coordinateToFen(position)); enteredSquareListeners.forEach(obj => obj.cb('enter')); if(this.lastInputPosition && this.lastInputPosition[0] != null) { const leftSquareListeners = this.customActivityListeners.filter(obj => obj.square == this.coordinateToFen(this.lastInputPosition)); leftSquareListeners.forEach(obj => obj.cb('leave')); } this.lastInputPositionStr = positionStr; this.lastInputPosition = position; } } addSquareListener(square, cb) { this.customActivityListeners.push({ square, cb }); return { remove: () => { this.customActivityListeners = this.customActivityListeners.filter(obj => obj.square != square && obj.cb != cb); }}; } terminate() { this.terminated = true; this.observers.forEach(observer => observer.disconnect()); this.boardContainerElem.remove(); } }