import type { Move, Square } from 'chess.js'; import React, { useCallback, useImperativeHandle } from 'react'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import Animated, { runOnJS, useAnimatedStyle, useDerivedValue, useSharedValue, withTiming, } from 'react-native-reanimated'; import { useChessboardProps } from '../../context/props-context/hooks'; import { useBoardOperations } from '../../context/board-operations-context/hooks'; import { useBoardPromotion } from '../../context/board-promotion-context/hooks'; import { usePieceRefs } from '../../context/board-refs-context/hooks'; import { useChessEngine } from '../../context/chess-engine-context/hooks'; import { useReversePiecePosition } from '../../notation'; import type { PieceType, Vector } from '../../types'; import { ChessPiece } from './visual-piece'; type PieceProps = { id: PieceType; startPosition: Vector; square: Square; size: number; }; export type ChessPieceRef = { moveTo: (square: Square) => Promise; enable: (activate: boolean) => void; }; const Piece = React.memo( React.forwardRef( ({ id, startPosition, square, size }, ref) => { const chess = useChessEngine(); const refs = usePieceRefs(); const pieceEnabled = useSharedValue(true); const { isPromoting } = useBoardPromotion(); const { onSelectPiece, onMove, selectedSquare, turn } = useBoardOperations(); const { durations: { move: moveDuration }, gestureEnabled: gestureEnabledFromChessboardProps, } = useChessboardProps(); const gestureEnabled = useDerivedValue( () => turn.value === id.charAt(0) && gestureEnabledFromChessboardProps, [id, gestureEnabledFromChessboardProps] ); const { toPosition, toTranslation } = useReversePiecePosition(); const isGestureActive = useSharedValue(false); const offsetX = useSharedValue(0); const offsetY = useSharedValue(0); const scale = useSharedValue(1); const translateX = useSharedValue(startPosition.x * size); const translateY = useSharedValue(startPosition.y * size); const validateMove = useCallback( (from: Square, to: Square) => { return chess .moves({ verbose: true }) .find((m) => m.from === from && m.to === to); }, [chess] ); const wrappedOnMoveForJSThread = useCallback( ({ move }: { move: Move }) => { onMove(move.from, move.to); }, [onMove] ); const moveTo = useCallback( (from: Square, to: Square) => { return new Promise((resolve) => { const move = validateMove(from, to); const { x, y } = toTranslation(move ? move.to : from); translateX.value = withTiming(x, { duration: moveDuration }, () => { offsetX.value = translateX.value; }); translateY.value = withTiming( y, { duration: moveDuration }, (isFinished) => { if (!isFinished) return; offsetY.value = translateY.value; isGestureActive.value = false; if (move) { runOnJS(wrappedOnMoveForJSThread)({ move }); // Ideally I must call the resolve method // inside the "wrappedOnMoveForJSThread" after // the "onMove" function. // Unfortunately I'm not able to pass a // function in the RunOnJS params runOnJS(resolve)(move); } else { runOnJS(resolve)(undefined); } } ); }); }, [ isGestureActive, moveDuration, offsetX, offsetY, toTranslation, translateX, translateY, validateMove, wrappedOnMoveForJSThread, ] ); const movePiece = useCallback( (to: Square) => { const from = toPosition({ x: offsetX.value, y: offsetY.value }); moveTo(from, to); }, [moveTo, offsetX.value, offsetY.value, toPosition] ); useImperativeHandle( ref, () => { return { moveTo: (to: Square) => { return moveTo(square, to); }, enable: (active: boolean) => { pieceEnabled.value = active; }, }; }, [moveTo, pieceEnabled, square] ); const onStartTap = useCallback( // eslint-disable-next-line no-shadow (square: Square) => { 'worklet'; if (!onSelectPiece) { return; } runOnJS(onSelectPiece)(square); }, [onSelectPiece] ); const globalMoveTo = useCallback( (move: Move) => { refs?.current?.[move.from].current.moveTo?.(move.to); }, [refs] ); const handleOnBegin = useCallback(() => { const currentSquare = toPosition({ x: translateX.value, y: translateY.value, }); const previousTappedSquare = selectedSquare.value; const move = previousTappedSquare && validateMove(previousTappedSquare, currentSquare); if (move) { runOnJS(globalMoveTo)(move); return; } if (!gestureEnabled.value) return; scale.value = withTiming(1.2); onStartTap(square); }, [ gestureEnabled.value, globalMoveTo, onStartTap, scale, selectedSquare.value, square, toPosition, translateX.value, translateY.value, validateMove, ]); const gesture = Gesture.Pan() .enabled(!isPromoting && pieceEnabled.value) .onBegin(() => { offsetX.value = translateX.value; offsetY.value = translateY.value; runOnJS(handleOnBegin)(); }) .onStart(() => { if (!gestureEnabled.value) return; isGestureActive.value = true; }) .onUpdate(({ translationX, translationY }) => { if (!gestureEnabled.value) return; translateX.value = offsetX.value + translationX; translateY.value = offsetY.value + translationY; }) .onEnd(() => { if (!gestureEnabled.value) return; runOnJS(movePiece)( toPosition({ x: translateX.value, y: translateY.value }) ); }) .onFinalize(() => { scale.value = withTiming(1); }); const style = useAnimatedStyle(() => { return { position: 'absolute', opacity: withTiming(pieceEnabled.value ? 1 : 0), zIndex: isGestureActive.value ? 100 : 10, transform: [ { translateX: translateX.value }, { translateY: translateY.value }, { scale: scale.value }, ], }; }); const underlay = useAnimatedStyle(() => { const position = toPosition({ x: translateX.value, y: translateY.value, }); const translation = toTranslation(position); return { position: 'absolute', width: size * 2, height: size * 2, borderRadius: size, zIndex: 0, backgroundColor: isGestureActive.value ? 'rgba(0, 0, 0, 0.1)' : 'transparent', transform: [ { translateX: translation.x - size / 2 }, { translateY: translation.y - size / 2 }, ], }; }, [size]); return ( <> ); } ), (prev, next) => prev.id === next.id && prev.size === next.size && prev.square === next.square ); export default Piece;