import { forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from 'react'; import type { ChessiroCanvasProps, ChessiroCanvasRef, BoardTheme } from './types'; import { INITIAL_FEN, readFen } from './utils/fen'; import { useBoardSize } from './hooks/useBoardSize'; import { useInteraction } from './interaction/useInteraction'; import { useKeyboard } from './interaction/useKeyboard'; import { Squares } from './render/Squares'; import { PiecesLayer } from './render/Pieces'; import { ArrowsLayer } from './render/Arrows'; import { Notation } from './render/Notation'; import { PromotionDialog } from './render/Promotion'; import { DragGhost } from './render/DragGhost'; import { Badge } from './render/Badge'; import { OverlaysLayer } from './render/Overlays'; const DEFAULT_THEME: BoardTheme = { id: 'Chessiro', name: 'Chessiro', darkSquare: '#785E45', lightSquare: '#DFC29A', margin: '#66503B', lastMoveHighlight: '#DFAA4E', selectedPiece: '#B57340', }; const EMPTY_ARRAY: any[] = []; const EMPTY_OBJECT: any = {}; function hasActiveRadius(radius: string | number): boolean { if (typeof radius === 'number') return radius > 0; const trimmed = radius.trim(); if (trimmed.length === 0) return false; const parsed = Number.parseFloat(trimmed); if (Number.isFinite(parsed)) return parsed > 0; return true; } export const ChessiroCanvas = forwardRef( function ChessiroCanvas(props, ref) { const { position = INITIAL_FEN, orientation = 'white', interactive = true, turnColor, movableColor, onMove, lastMove, dests, premovable, arrows = EMPTY_ARRAY, onArrowsChange, arrowBrushes, snapArrowsToValidMoves = true, markedSquares, onMarkedSquaresChange, plyIndex, plyArrows, onPlyArrowsChange, plyMarks, onPlyMarksChange, theme = DEFAULT_THEME, pieceSet, flipPieces = false, showMargin = true, marginThickness = 24, marginRadius = 4, boardRadius = 0, showNotation = true, highlightedSquares = EMPTY_OBJECT, squareVisuals, arrowVisuals, notationVisuals, promotionVisuals, overlayVisuals, check, moveQualityBadge, allowDragging = true, allowDrawingArrows = true, animationDurationMs = 200, showAnimations = true, blockTouchScroll = false, selectedPieceScale, dragScale = 1, touchDragScale = 1.9, dragLiftSquares = 0, touchDragLiftSquares = 0.6, onPrevious, onNext, onFirst, onLast, onFlipBoard, onShowThreat, onDeselect, onSquareClick, onClearOverlays, overlays = EMPTY_ARRAY, overlayRenderer, pieces: customPieces, className, style, } = props; const boardRef = useRef(null); const { bounds, getFreshBounds } = useBoardSize(boardRef); const piecesMap = useMemo(() => readFen(position || INITIAL_FEN), [position]); const boundsToDomRect = useCallback((b: { left: number; top: number; width: number; height: number }): DOMRect => { return { left: b.left, top: b.top, width: b.width, height: b.height, right: b.left + b.width, bottom: b.top + b.height, x: b.left, y: b.top, toJSON: () => ({}), } as DOMRect; }, []); const boardDomRect = useMemo(() => { if (!bounds) return null; return boundsToDomRect(bounds); }, [bounds, boundsToDomRect]); const getFreshDomRect = useCallback((): DOMRect | null => { const fresh = getFreshBounds(); return fresh ? boundsToDomRect(fresh) : null; }, [getFreshBounds, boundsToDomRect]); const interaction = useInteraction({ position, pieces: piecesMap, orientation, interactive, allowDragging, allowDrawingArrows, boardRef, boardBounds: boardDomRect, onMove, dests, turnColor, movableColor, premovable, arrows, onArrowsChange, arrowBrushes, snapArrowsToValidMoves, markedSquares, onMarkedSquaresChange, plyIndex, plyArrows, onPlyArrowsChange, plyMarks, onPlyMarksChange, onSquareClick, onClearOverlays, blockTouchScroll, getFreshBounds: getFreshDomRect, }); const occupiedSquares = useMemo(() => { if (interaction.legalSquares.length === 0 && interaction.premoveSquares.length === 0) { return undefined; } const set = new Set(); for (const sq of piecesMap.keys()) set.add(sq); return set; }, [piecesMap, interaction.legalSquares, interaction.premoveSquares]); const handleDeselect = useCallback(() => { interaction.clearSelection(); onDeselect?.(); }, [interaction.clearSelection, onDeselect]); useKeyboard({ onPrevious, onNext, onFirst, onLast, onFlipBoard, onShowThreat, onDeselect: handleDeselect, }); useImperativeHandle(ref, () => ({ getSquareRect(square: string) { if (!bounds) return null; const file = square.charCodeAt(0) - 97; const rank = parseInt(square[1]) - 1; const asWhite = orientation === 'white'; const col = asWhite ? file : 7 - file; const row = asWhite ? 7 - rank : rank; const sqW = bounds.width / 8; const sqH = bounds.height / 8; return new DOMRect(bounds.left + col * sqW, bounds.top + row * sqH, sqW, sqH); }, getBoardRect() { return boardRef.current?.getBoundingClientRect() ?? null; }, }), [bounds, orientation]); const hasValidSize = bounds && bounds.width > 0; const boardWidth = bounds?.width ?? 0; const boardHeight = bounds?.height ?? 0; const squareSize = boardWidth / 8; // Cursor: grab when hovering pieces, grabbing while dragging const isDragging = !!interaction.drag; const cursor = !interactive ? 'default' : isDragging ? 'grabbing' : allowDragging ? 'grab' : 'pointer'; const marginPx = showMargin ? marginThickness : 0; const clipBoardContent = hasActiveRadius(boardRadius); return (
{/* Margin frame */} {marginPx > 0 && (
)}
{hasValidSize && ( <>
{moveQualityBadge && ( )} {overlays.length > 0 && ( )} {interaction.pendingPromotion && ( )}
{showNotation && ( )} )}
{interaction.drag && ( )}
); }, );