react-native-chess-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/lib/commonjs/board-background.js +49 -0
- package/lib/commonjs/board-background.js.map +1 -0
- package/lib/commonjs/board-coordinates.js +78 -0
- package/lib/commonjs/board-coordinates.js.map +1 -0
- package/lib/commonjs/board-drag-ghost.js +110 -0
- package/lib/commonjs/board-drag-ghost.js.map +1 -0
- package/lib/commonjs/board-legal-dots.js +67 -0
- package/lib/commonjs/board-legal-dots.js.map +1 -0
- package/lib/commonjs/board-piece.js +74 -0
- package/lib/commonjs/board-piece.js.map +1 -0
- package/lib/commonjs/board-pieces.js +47 -0
- package/lib/commonjs/board-pieces.js.map +1 -0
- package/lib/commonjs/board.js +188 -0
- package/lib/commonjs/board.js.map +1 -0
- package/lib/commonjs/index.js +26 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/types.js +6 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/use-board-gesture.js +158 -0
- package/lib/commonjs/use-board-gesture.js.map +1 -0
- package/lib/commonjs/use-board-pieces.js +195 -0
- package/lib/commonjs/use-board-pieces.js.map +1 -0
- package/lib/commonjs/use-board-state.js +78 -0
- package/lib/commonjs/use-board-state.js.map +1 -0
- package/lib/module/board-background.js +44 -0
- package/lib/module/board-background.js.map +1 -0
- package/lib/module/board-coordinates.js +73 -0
- package/lib/module/board-coordinates.js.map +1 -0
- package/lib/module/board-drag-ghost.js +104 -0
- package/lib/module/board-drag-ghost.js.map +1 -0
- package/lib/module/board-legal-dots.js +62 -0
- package/lib/module/board-legal-dots.js.map +1 -0
- package/lib/module/board-piece.js +69 -0
- package/lib/module/board-piece.js.map +1 -0
- package/lib/module/board-pieces.js +42 -0
- package/lib/module/board-pieces.js.map +1 -0
- package/lib/module/board.js +184 -0
- package/lib/module/board.js.map +1 -0
- package/lib/module/index.js +21 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/use-board-gesture.js +154 -0
- package/lib/module/use-board-gesture.js.map +1 -0
- package/lib/module/use-board-pieces.js +189 -0
- package/lib/module/use-board-pieces.js.map +1 -0
- package/lib/module/use-board-state.js +74 -0
- package/lib/module/use-board-state.js.map +1 -0
- package/lib/typescript/board-background.d.ts +15 -0
- package/lib/typescript/board-background.d.ts.map +1 -0
- package/lib/typescript/board-coordinates.d.ts +20 -0
- package/lib/typescript/board-coordinates.d.ts.map +1 -0
- package/lib/typescript/board-drag-ghost.d.ts +21 -0
- package/lib/typescript/board-drag-ghost.d.ts.map +1 -0
- package/lib/typescript/board-legal-dots.d.ts +16 -0
- package/lib/typescript/board-legal-dots.d.ts.map +1 -0
- package/lib/typescript/board-piece.d.ts +36 -0
- package/lib/typescript/board-piece.d.ts.map +1 -0
- package/lib/typescript/board-pieces.d.ts +22 -0
- package/lib/typescript/board-pieces.d.ts.map +1 -0
- package/lib/typescript/board.d.ts +17 -0
- package/lib/typescript/board.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +4 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +88 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/lib/typescript/use-board-gesture.d.ts +46 -0
- package/lib/typescript/use-board-gesture.d.ts.map +1 -0
- package/lib/typescript/use-board-pieces.d.ts +23 -0
- package/lib/typescript/use-board-pieces.d.ts.map +1 -0
- package/lib/typescript/use-board-state.d.ts +35 -0
- package/lib/typescript/use-board-state.d.ts.map +1 -0
- package/package.json +73 -0
- package/src/board-background.tsx +46 -0
- package/src/board-coordinates.tsx +98 -0
- package/src/board-drag-ghost.tsx +132 -0
- package/src/board-legal-dots.tsx +73 -0
- package/src/board-piece.tsx +104 -0
- package/src/board-pieces.tsx +56 -0
- package/src/board.tsx +203 -0
- package/src/index.ts +39 -0
- package/src/types.ts +114 -0
- package/src/use-board-gesture.ts +201 -0
- package/src/use-board-pieces.ts +158 -0
- package/src/use-board-state.ts +104 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Animated, {
|
|
3
|
+
useAnimatedStyle,
|
|
4
|
+
type SharedValue,
|
|
5
|
+
} from 'react-native-reanimated';
|
|
6
|
+
|
|
7
|
+
type BoardDragGhostProps = {
|
|
8
|
+
squareSize: number;
|
|
9
|
+
isDragging: SharedValue<boolean>;
|
|
10
|
+
dragX: SharedValue<number>;
|
|
11
|
+
dragY: SharedValue<number>;
|
|
12
|
+
dragPieceCode: SharedValue<string | null>;
|
|
13
|
+
/** Render the piece image for a given piece code */
|
|
14
|
+
renderPiece: (code: string, size: number) => React.ReactElement;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Floating piece that follows the user's finger during drag.
|
|
19
|
+
*
|
|
20
|
+
* Only ONE instance exists — not one per piece. It reads drag position
|
|
21
|
+
* from shared values on the UI thread, so zero JS bridge calls and
|
|
22
|
+
* zero re-renders while dragging.
|
|
23
|
+
*/
|
|
24
|
+
export const BoardDragGhost = React.memo(function BoardDragGhost({
|
|
25
|
+
squareSize,
|
|
26
|
+
isDragging,
|
|
27
|
+
dragX,
|
|
28
|
+
dragY,
|
|
29
|
+
dragPieceCode,
|
|
30
|
+
renderPiece,
|
|
31
|
+
}: BoardDragGhostProps) {
|
|
32
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
33
|
+
if (!isDragging.value || !dragPieceCode.value) {
|
|
34
|
+
return { opacity: 0, transform: [{ translateX: 0 }, { translateY: 0 }] };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
opacity: 1,
|
|
39
|
+
// Center the piece on the finger, slightly above for visibility
|
|
40
|
+
transform: [
|
|
41
|
+
{ translateX: dragX.value - squareSize / 2 },
|
|
42
|
+
{ translateY: dragY.value - squareSize },
|
|
43
|
+
{ scale: 1.1 },
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Animated.View
|
|
50
|
+
style={[
|
|
51
|
+
{
|
|
52
|
+
position: 'absolute',
|
|
53
|
+
width: squareSize,
|
|
54
|
+
height: squareSize,
|
|
55
|
+
zIndex: 100,
|
|
56
|
+
},
|
|
57
|
+
animatedStyle,
|
|
58
|
+
]}
|
|
59
|
+
pointerEvents="none"
|
|
60
|
+
>
|
|
61
|
+
<DragGhostContent
|
|
62
|
+
renderPiece={renderPiece}
|
|
63
|
+
squareSize={squareSize}
|
|
64
|
+
dragPieceCode={dragPieceCode}
|
|
65
|
+
/>
|
|
66
|
+
</Animated.View>
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Inner content that renders the actual piece image.
|
|
72
|
+
* Separate component so the Animated.View wrapper doesn't need
|
|
73
|
+
* to re-render when the piece code changes — it uses shared value.
|
|
74
|
+
*/
|
|
75
|
+
const DragGhostContent = React.memo(function DragGhostContent({
|
|
76
|
+
renderPiece,
|
|
77
|
+
squareSize,
|
|
78
|
+
dragPieceCode,
|
|
79
|
+
}: {
|
|
80
|
+
renderPiece: (code: string, size: number) => React.ReactElement;
|
|
81
|
+
squareSize: number;
|
|
82
|
+
dragPieceCode: SharedValue<string | null>;
|
|
83
|
+
}) {
|
|
84
|
+
// We render all 12 possible piece types and show/hide based on dragPieceCode.
|
|
85
|
+
// This avoids re-mounting the Image component during drag.
|
|
86
|
+
// Only the opacity changes — pure worklet animation.
|
|
87
|
+
const codes = PIECE_CODES;
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<>
|
|
91
|
+
{codes.map((code) => (
|
|
92
|
+
<GhostPieceSlot
|
|
93
|
+
key={code}
|
|
94
|
+
code={code}
|
|
95
|
+
squareSize={squareSize}
|
|
96
|
+
dragPieceCode={dragPieceCode}
|
|
97
|
+
renderPiece={renderPiece}
|
|
98
|
+
/>
|
|
99
|
+
))}
|
|
100
|
+
</>
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const GhostPieceSlot = React.memo(function GhostPieceSlot({
|
|
105
|
+
code,
|
|
106
|
+
squareSize,
|
|
107
|
+
dragPieceCode,
|
|
108
|
+
renderPiece,
|
|
109
|
+
}: {
|
|
110
|
+
code: string;
|
|
111
|
+
squareSize: number;
|
|
112
|
+
dragPieceCode: SharedValue<string | null>;
|
|
113
|
+
renderPiece: (code: string, size: number) => React.ReactElement;
|
|
114
|
+
}) {
|
|
115
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
116
|
+
opacity: dragPieceCode.value === code ? 1 : 0,
|
|
117
|
+
}));
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<Animated.View
|
|
121
|
+
style={[
|
|
122
|
+
{ position: 'absolute', width: squareSize, height: squareSize },
|
|
123
|
+
animatedStyle,
|
|
124
|
+
]}
|
|
125
|
+
>
|
|
126
|
+
{renderPiece(code, squareSize)}
|
|
127
|
+
</Animated.View>
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// All 12 piece codes — pre-computed to avoid allocation
|
|
132
|
+
const PIECE_CODES = ['wp', 'wn', 'wb', 'wr', 'wq', 'wk', 'bp', 'bn', 'bb', 'br', 'bq', 'bk'];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { ChessColor, LegalMoveTarget } from './types';
|
|
5
|
+
import { squareToXY } from './use-board-pieces';
|
|
6
|
+
|
|
7
|
+
type BoardLegalDotsProps = {
|
|
8
|
+
legalMoves: LegalMoveTarget[];
|
|
9
|
+
squareSize: number;
|
|
10
|
+
orientation: ChessColor;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Legal move indicator dots, rendered only when a piece is selected.
|
|
15
|
+
*
|
|
16
|
+
* Dots are CONDITIONALLY rendered (only for actual legal move squares,
|
|
17
|
+
* typically 5-15) instead of always mounting 64 invisible dot views.
|
|
18
|
+
*/
|
|
19
|
+
export const BoardLegalDots = React.memo(function BoardLegalDots({
|
|
20
|
+
legalMoves,
|
|
21
|
+
squareSize,
|
|
22
|
+
orientation,
|
|
23
|
+
}: BoardLegalDotsProps) {
|
|
24
|
+
if (legalMoves.length === 0) return null;
|
|
25
|
+
|
|
26
|
+
const dotSize = squareSize * 0.28;
|
|
27
|
+
const ringSize = squareSize * 0.85;
|
|
28
|
+
const borderWidth = squareSize * 0.08;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<View
|
|
32
|
+
style={{ position: 'absolute', width: squareSize * 8, height: squareSize * 8 }}
|
|
33
|
+
pointerEvents="none"
|
|
34
|
+
>
|
|
35
|
+
{legalMoves.map((move) => {
|
|
36
|
+
const { x, y } = squareToXY(move.square, squareSize, orientation);
|
|
37
|
+
|
|
38
|
+
if (move.isCapture) {
|
|
39
|
+
return (
|
|
40
|
+
<View
|
|
41
|
+
key={move.square}
|
|
42
|
+
style={{
|
|
43
|
+
position: 'absolute',
|
|
44
|
+
left: x + (squareSize - ringSize) / 2,
|
|
45
|
+
top: y + (squareSize - ringSize) / 2,
|
|
46
|
+
width: ringSize,
|
|
47
|
+
height: ringSize,
|
|
48
|
+
borderRadius: ringSize / 2,
|
|
49
|
+
borderWidth,
|
|
50
|
+
borderColor: 'rgba(0, 0, 0, 0.25)',
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<View
|
|
58
|
+
key={move.square}
|
|
59
|
+
style={{
|
|
60
|
+
position: 'absolute',
|
|
61
|
+
left: x + (squareSize - dotSize) / 2,
|
|
62
|
+
top: y + (squareSize - dotSize) / 2,
|
|
63
|
+
width: dotSize,
|
|
64
|
+
height: dotSize,
|
|
65
|
+
borderRadius: dotSize / 2,
|
|
66
|
+
backgroundColor: 'rgba(0, 0, 0, 0.25)',
|
|
67
|
+
}}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
})}
|
|
71
|
+
</View>
|
|
72
|
+
);
|
|
73
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import Animated, {
|
|
3
|
+
useAnimatedStyle,
|
|
4
|
+
useSharedValue,
|
|
5
|
+
withTiming,
|
|
6
|
+
type SharedValue,
|
|
7
|
+
} from 'react-native-reanimated';
|
|
8
|
+
|
|
9
|
+
type BoardPieceProps = {
|
|
10
|
+
/** Target pixel position (top-left of destination square) */
|
|
11
|
+
targetX: number;
|
|
12
|
+
targetY: number;
|
|
13
|
+
/** Square size in pixels */
|
|
14
|
+
squareSize: number;
|
|
15
|
+
/** Move animation duration in ms */
|
|
16
|
+
moveDuration: number;
|
|
17
|
+
/** The piece visual (rendered by parent via renderPiece) */
|
|
18
|
+
children: React.ReactElement;
|
|
19
|
+
/** Gesture state: is this piece currently being dragged? */
|
|
20
|
+
activeSquare: SharedValue<string | null>;
|
|
21
|
+
isDragging: SharedValue<boolean>;
|
|
22
|
+
/** This piece's current square */
|
|
23
|
+
square: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A single animated chess piece.
|
|
28
|
+
*
|
|
29
|
+
* Animates ONLY `transform` and `opacity` — Reanimated's fast path on Android.
|
|
30
|
+
* No layout properties (top/left/width/height) are animated, avoiding costly
|
|
31
|
+
* layout recalculations on low-end devices.
|
|
32
|
+
*
|
|
33
|
+
* During drag:
|
|
34
|
+
* - Original piece hides (opacity: 0) — the drag ghost shows instead
|
|
35
|
+
* - No position changes on the original piece during drag
|
|
36
|
+
*
|
|
37
|
+
* After a move:
|
|
38
|
+
* - Snaps to new position via withTiming on translateX/translateY
|
|
39
|
+
* - Duration controlled by user's animation speed setting
|
|
40
|
+
*/
|
|
41
|
+
export const BoardPieceView = React.memo(
|
|
42
|
+
function BoardPieceView({
|
|
43
|
+
targetX,
|
|
44
|
+
targetY,
|
|
45
|
+
squareSize,
|
|
46
|
+
moveDuration,
|
|
47
|
+
children,
|
|
48
|
+
activeSquare,
|
|
49
|
+
isDragging,
|
|
50
|
+
square,
|
|
51
|
+
}: BoardPieceProps) {
|
|
52
|
+
// Shared values for smooth animated position — written from JS, read on UI thread
|
|
53
|
+
const currentX = useSharedValue(targetX);
|
|
54
|
+
const currentY = useSharedValue(targetY);
|
|
55
|
+
|
|
56
|
+
// When target position changes (piece moved), animate to the new square.
|
|
57
|
+
// useEffect is the correct pattern for reacting to JS prop changes —
|
|
58
|
+
// useDerivedValue is meant for shared-value-to-shared-value derivation.
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
currentX.value = moveDuration > 0
|
|
61
|
+
? withTiming(targetX, { duration: moveDuration })
|
|
62
|
+
: targetX;
|
|
63
|
+
currentY.value = moveDuration > 0
|
|
64
|
+
? withTiming(targetY, { duration: moveDuration })
|
|
65
|
+
: targetY;
|
|
66
|
+
}, [targetX, targetY, moveDuration, currentX, currentY]);
|
|
67
|
+
|
|
68
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
69
|
+
const isBeingDragged = isDragging.value && activeSquare.value === square;
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
transform: [
|
|
73
|
+
{ translateX: currentX.value },
|
|
74
|
+
{ translateY: currentY.value },
|
|
75
|
+
],
|
|
76
|
+
// Hide original piece during drag — drag ghost renders on top
|
|
77
|
+
opacity: isBeingDragged ? 0 : 1,
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Animated.View
|
|
83
|
+
style={[
|
|
84
|
+
{
|
|
85
|
+
position: 'absolute',
|
|
86
|
+
width: squareSize,
|
|
87
|
+
height: squareSize,
|
|
88
|
+
},
|
|
89
|
+
animatedStyle,
|
|
90
|
+
]}
|
|
91
|
+
>
|
|
92
|
+
{children}
|
|
93
|
+
</Animated.View>
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
// Custom comparator: only re-render when position or square changes
|
|
97
|
+
(prev, next) =>
|
|
98
|
+
prev.targetX === next.targetX &&
|
|
99
|
+
prev.targetY === next.targetY &&
|
|
100
|
+
prev.square === next.square &&
|
|
101
|
+
prev.squareSize === next.squareSize &&
|
|
102
|
+
prev.moveDuration === next.moveDuration &&
|
|
103
|
+
prev.children === next.children,
|
|
104
|
+
);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { SharedValue } from 'react-native-reanimated';
|
|
3
|
+
|
|
4
|
+
import type { ChessColor, BoardPiece } from './types';
|
|
5
|
+
import { BoardPieceView } from './board-piece';
|
|
6
|
+
import { squareToXY } from './use-board-pieces';
|
|
7
|
+
|
|
8
|
+
type BoardPiecesProps = {
|
|
9
|
+
pieces: BoardPiece[];
|
|
10
|
+
squareSize: number;
|
|
11
|
+
orientation: ChessColor;
|
|
12
|
+
moveDuration: number;
|
|
13
|
+
renderPiece: (code: string, size: number) => React.ReactElement;
|
|
14
|
+
activeSquare: SharedValue<string | null>;
|
|
15
|
+
isDragging: SharedValue<boolean>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Renders all pieces on the board.
|
|
20
|
+
*
|
|
21
|
+
* Each piece gets a stable key (from useBoardPieces) so React doesn't
|
|
22
|
+
* unmount/remount pieces that moved — it updates their position props
|
|
23
|
+
* and the BoardPieceView animates the transition.
|
|
24
|
+
*/
|
|
25
|
+
export const BoardPiecesLayer = React.memo(function BoardPiecesLayer({
|
|
26
|
+
pieces,
|
|
27
|
+
squareSize,
|
|
28
|
+
orientation,
|
|
29
|
+
moveDuration,
|
|
30
|
+
renderPiece,
|
|
31
|
+
activeSquare,
|
|
32
|
+
isDragging,
|
|
33
|
+
}: BoardPiecesProps) {
|
|
34
|
+
return (
|
|
35
|
+
<>
|
|
36
|
+
{pieces.map((piece) => {
|
|
37
|
+
const { x, y } = squareToXY(piece.square, squareSize, orientation);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<BoardPieceView
|
|
41
|
+
key={piece.id}
|
|
42
|
+
targetX={x}
|
|
43
|
+
targetY={y}
|
|
44
|
+
squareSize={squareSize}
|
|
45
|
+
moveDuration={moveDuration}
|
|
46
|
+
activeSquare={activeSquare}
|
|
47
|
+
isDragging={isDragging}
|
|
48
|
+
square={piece.square}
|
|
49
|
+
>
|
|
50
|
+
{renderPiece(piece.code, squareSize)}
|
|
51
|
+
</BoardPieceView>
|
|
52
|
+
);
|
|
53
|
+
})}
|
|
54
|
+
</>
|
|
55
|
+
);
|
|
56
|
+
});
|
package/src/board.tsx
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import React, { forwardRef, useState, useCallback, useImperativeHandle, useEffect } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { GestureDetector } from 'react-native-gesture-handler';
|
|
4
|
+
|
|
5
|
+
import type { BoardRef, BoardProps, LegalMoveTarget } from './types';
|
|
6
|
+
import { useBoardPieces } from './use-board-pieces';
|
|
7
|
+
import { useBoardState } from './use-board-state';
|
|
8
|
+
import { useBoardGesture } from './use-board-gesture';
|
|
9
|
+
import { BoardBackground } from './board-background';
|
|
10
|
+
import { BoardCoordinates } from './board-coordinates';
|
|
11
|
+
import { BoardLegalDots } from './board-legal-dots';
|
|
12
|
+
import { BoardPiecesLayer } from './board-pieces';
|
|
13
|
+
import { BoardDragGhost } from './board-drag-ghost';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* High-performance custom chess board built on Reanimated + Gesture Handler.
|
|
17
|
+
*
|
|
18
|
+
* Architecture:
|
|
19
|
+
* - 1 gesture handler (vs 32 in typical implementations)
|
|
20
|
+
* - ~40 components mounted (vs ~281)
|
|
21
|
+
* - ~75 native views (vs ~470)
|
|
22
|
+
* - 0 React Context providers
|
|
23
|
+
* - 0 re-renders during drag (pure worklet — only 2 shared value writes per frame)
|
|
24
|
+
*
|
|
25
|
+
* Follows chess.com/lichess pattern: single gesture receiver on the board,
|
|
26
|
+
* coordinate math to determine touched piece, shared values for drag state.
|
|
27
|
+
*/
|
|
28
|
+
export const Board = forwardRef<BoardRef, BoardProps>(function Board(
|
|
29
|
+
{
|
|
30
|
+
fen,
|
|
31
|
+
orientation,
|
|
32
|
+
boardSize,
|
|
33
|
+
gestureEnabled,
|
|
34
|
+
player,
|
|
35
|
+
onMove,
|
|
36
|
+
colors,
|
|
37
|
+
moveDuration,
|
|
38
|
+
withLetters,
|
|
39
|
+
withNumbers,
|
|
40
|
+
renderPiece,
|
|
41
|
+
showLegalMoves,
|
|
42
|
+
moveMethod,
|
|
43
|
+
},
|
|
44
|
+
ref,
|
|
45
|
+
) {
|
|
46
|
+
const squareSize = boardSize / 8;
|
|
47
|
+
|
|
48
|
+
// --- Piece data from FEN ---
|
|
49
|
+
const pieces = useBoardPieces(fen);
|
|
50
|
+
|
|
51
|
+
// --- Chess.js for legal move validation ---
|
|
52
|
+
const boardState = useBoardState(fen);
|
|
53
|
+
|
|
54
|
+
// Sync internal chess.js when parent changes FEN (puzzle reset, opponent move, etc.)
|
|
55
|
+
// Must be in useEffect — side effects during render violate React's rules
|
|
56
|
+
// and can fire multiple times in concurrent mode.
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
boardState.loadFen(fen);
|
|
59
|
+
}, [fen, boardState]);
|
|
60
|
+
|
|
61
|
+
// --- Selection state (triggers legal dots display) ---
|
|
62
|
+
const [selectedSquare, setSelectedSquare] = useState<string | null>(null);
|
|
63
|
+
const [legalMoves, setLegalMoves] = useState<LegalMoveTarget[]>([]);
|
|
64
|
+
|
|
65
|
+
// Default piece renderer (no-op if parent provides renderPiece)
|
|
66
|
+
const defaultRenderPiece = useCallback(
|
|
67
|
+
(code: string, size: number) => (
|
|
68
|
+
<View style={{ width: size, height: size, backgroundColor: 'rgba(0,0,0,0.3)' }} />
|
|
69
|
+
),
|
|
70
|
+
[],
|
|
71
|
+
);
|
|
72
|
+
const pieceRenderer = renderPiece ?? defaultRenderPiece;
|
|
73
|
+
|
|
74
|
+
// --- Gesture callbacks ---
|
|
75
|
+
const handlePieceSelected = useCallback(
|
|
76
|
+
(square: string) => {
|
|
77
|
+
setSelectedSquare(square);
|
|
78
|
+
if (showLegalMoves) {
|
|
79
|
+
setLegalMoves(boardState.getLegalMoves(square));
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
[showLegalMoves, boardState],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const handleSelectionCleared = useCallback(() => {
|
|
86
|
+
setSelectedSquare(null);
|
|
87
|
+
setLegalMoves([]);
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
const handlePieceMoved = useCallback(
|
|
91
|
+
(from: string, to: string) => {
|
|
92
|
+
// Clear selection and legal dots
|
|
93
|
+
setSelectedSquare(null);
|
|
94
|
+
setLegalMoves([]);
|
|
95
|
+
|
|
96
|
+
// Notify parent — parent decides whether to accept/reject
|
|
97
|
+
onMove?.({ from, to });
|
|
98
|
+
},
|
|
99
|
+
[onMove],
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// --- Single centralized gesture ---
|
|
103
|
+
const { gesture, gestureState } = useBoardGesture({
|
|
104
|
+
squareSize,
|
|
105
|
+
orientation,
|
|
106
|
+
gestureEnabled,
|
|
107
|
+
player,
|
|
108
|
+
moveMethod,
|
|
109
|
+
pieces,
|
|
110
|
+
callbacks: {
|
|
111
|
+
onPieceSelected: handlePieceSelected,
|
|
112
|
+
onPieceMoved: handlePieceMoved,
|
|
113
|
+
onSelectionCleared: handleSelectionCleared,
|
|
114
|
+
},
|
|
115
|
+
selectedSquare,
|
|
116
|
+
legalMoves,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// --- Imperative ref for parent (move, highlight, reset, undo) ---
|
|
120
|
+
useImperativeHandle(ref, () => ({
|
|
121
|
+
move: (move) => {
|
|
122
|
+
// Pre-apply to internal chess.js so subsequent getLegalMoves calls
|
|
123
|
+
// reflect the new position. The parent will also update the FEN prop,
|
|
124
|
+
// which triggers useBoardPieces -> piece position animates via shared values.
|
|
125
|
+
boardState.applyMove(move.from, move.to);
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
highlight: (_square, _color) => {
|
|
129
|
+
// Highlights are handled by overlay layers in the consuming app,
|
|
130
|
+
// not internally — this is a no-op stub for API compatibility.
|
|
131
|
+
// Use the Board's overlay API or render your own highlight layer.
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
clearHighlights: () => {
|
|
135
|
+
// Same as highlight — handled by overlay layer
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
resetBoard: (newFen) => {
|
|
139
|
+
boardState.loadFen(newFen);
|
|
140
|
+
setSelectedSquare(null);
|
|
141
|
+
setLegalMoves([]);
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
undo: () => {
|
|
145
|
+
boardState.undoMove();
|
|
146
|
+
setSelectedSquare(null);
|
|
147
|
+
setLegalMoves([]);
|
|
148
|
+
},
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<GestureDetector gesture={gesture}>
|
|
153
|
+
<View style={{ width: boardSize, height: boardSize }}>
|
|
154
|
+
{/* Layer 1: Board background (64 colored squares) */}
|
|
155
|
+
<BoardBackground
|
|
156
|
+
boardSize={boardSize}
|
|
157
|
+
lightColor={colors.light}
|
|
158
|
+
darkColor={colors.dark}
|
|
159
|
+
/>
|
|
160
|
+
|
|
161
|
+
{/* Layer 2: Coordinate labels (a-h, 1-8) */}
|
|
162
|
+
<BoardCoordinates
|
|
163
|
+
boardSize={boardSize}
|
|
164
|
+
orientation={orientation}
|
|
165
|
+
lightColor={colors.light}
|
|
166
|
+
darkColor={colors.dark}
|
|
167
|
+
withLetters={withLetters}
|
|
168
|
+
withNumbers={withNumbers}
|
|
169
|
+
/>
|
|
170
|
+
|
|
171
|
+
{/* Layer 3: Legal move dots (only when a piece is selected) */}
|
|
172
|
+
{showLegalMoves && (
|
|
173
|
+
<BoardLegalDots
|
|
174
|
+
legalMoves={legalMoves}
|
|
175
|
+
squareSize={squareSize}
|
|
176
|
+
orientation={orientation}
|
|
177
|
+
/>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
{/* Layer 4: Pieces */}
|
|
181
|
+
<BoardPiecesLayer
|
|
182
|
+
pieces={pieces}
|
|
183
|
+
squareSize={squareSize}
|
|
184
|
+
orientation={orientation}
|
|
185
|
+
moveDuration={moveDuration}
|
|
186
|
+
renderPiece={pieceRenderer}
|
|
187
|
+
activeSquare={gestureState.activeSquare}
|
|
188
|
+
isDragging={gestureState.isDragging}
|
|
189
|
+
/>
|
|
190
|
+
|
|
191
|
+
{/* Layer 5: Drag ghost (single floating piece) */}
|
|
192
|
+
<BoardDragGhost
|
|
193
|
+
squareSize={squareSize}
|
|
194
|
+
isDragging={gestureState.isDragging}
|
|
195
|
+
dragX={gestureState.dragX}
|
|
196
|
+
dragY={gestureState.dragY}
|
|
197
|
+
dragPieceCode={gestureState.dragPieceCode}
|
|
198
|
+
renderPiece={pieceRenderer}
|
|
199
|
+
/>
|
|
200
|
+
</View>
|
|
201
|
+
</GestureDetector>
|
|
202
|
+
);
|
|
203
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// react-native-chess-kit
|
|
2
|
+
// High-performance chess board for React Native
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Main component
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
export { Board } from './board';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Types
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export type {
|
|
15
|
+
// Core chess types
|
|
16
|
+
ChessColor,
|
|
17
|
+
MoveMethod,
|
|
18
|
+
|
|
19
|
+
// Board component API
|
|
20
|
+
BoardRef,
|
|
21
|
+
BoardProps,
|
|
22
|
+
BoardColors,
|
|
23
|
+
|
|
24
|
+
// Piece data (useful for custom piece renderers)
|
|
25
|
+
BoardPiece,
|
|
26
|
+
ParsedPiece,
|
|
27
|
+
|
|
28
|
+
// Gesture state (useful for advanced overlays)
|
|
29
|
+
GestureState,
|
|
30
|
+
|
|
31
|
+
// Legal move dots (useful if building custom dot rendering)
|
|
32
|
+
LegalMoveTarget,
|
|
33
|
+
} from './types';
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Utility functions (useful for overlay positioning)
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
export { squareToXY, xyToSquare } from './use-board-pieces';
|