react-native-chess-kit 0.5.0 → 0.5.2
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 -21
- package/README.md +168 -168
- package/lib/commonjs/board-annotations.js +8 -8
- package/lib/commonjs/board-arrows.js +2 -2
- package/lib/commonjs/board-arrows.js.map +1 -1
- package/lib/commonjs/board-background.js +5 -5
- package/lib/commonjs/board-coordinates.js +8 -8
- package/lib/commonjs/board-drag-ghost.js +10 -10
- package/lib/commonjs/board-highlights.js +15 -15
- package/lib/commonjs/board-legal-dots.js +5 -5
- package/lib/commonjs/board-piece.js +25 -25
- package/lib/commonjs/board-pieces.js +6 -6
- package/lib/commonjs/board.js +24 -24
- package/lib/commonjs/promotion-picker.js +8 -8
- package/lib/commonjs/static-board.js +7 -7
- package/lib/commonjs/use-board-gesture.js +52 -33
- package/lib/commonjs/use-board-gesture.js.map +1 -1
- package/lib/commonjs/use-board-pieces.js +15 -15
- package/lib/commonjs/use-board-state.js +8 -8
- package/lib/commonjs/use-premove.js +12 -12
- package/lib/module/board-annotations.js +8 -8
- package/lib/module/board-arrows.js +2 -2
- package/lib/module/board-arrows.js.map +1 -1
- package/lib/module/board-background.js +5 -5
- package/lib/module/board-coordinates.js +8 -8
- package/lib/module/board-drag-ghost.js +10 -10
- package/lib/module/board-highlights.js +15 -15
- package/lib/module/board-legal-dots.js +5 -5
- package/lib/module/board-piece.js +25 -25
- package/lib/module/board-pieces.js +6 -6
- package/lib/module/board.js +24 -24
- package/lib/module/promotion-picker.js +8 -8
- package/lib/module/static-board.js +7 -7
- package/lib/module/use-board-gesture.js +52 -33
- package/lib/module/use-board-gesture.js.map +1 -1
- package/lib/module/use-board-pieces.js +15 -15
- package/lib/module/use-board-state.js +8 -8
- package/lib/module/use-premove.js +12 -12
- package/lib/typescript/use-board-gesture.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/board-annotations.tsx +147 -147
- package/src/board-arrows.tsx +2 -2
- package/src/board-background.tsx +46 -46
- package/src/board-coordinates.tsx +192 -192
- package/src/board-drag-ghost.tsx +132 -132
- package/src/board-highlights.tsx +226 -226
- package/src/board-legal-dots.tsx +73 -73
- package/src/board-piece.tsx +160 -160
- package/src/board-pieces.tsx +63 -63
- package/src/board.tsx +685 -685
- package/src/constants.ts +103 -103
- package/src/index.ts +101 -101
- package/src/pieces/default-pieces.tsx +383 -383
- package/src/pieces/index.ts +1 -1
- package/src/promotion-picker.tsx +147 -147
- package/src/static-board.tsx +187 -187
- package/src/themes.ts +129 -129
- package/src/types.ts +373 -373
- package/src/use-board-gesture.ts +429 -412
- package/src/use-board-pieces.ts +158 -158
- package/src/use-board-state.ts +111 -111
- package/src/use-premove.ts +59 -59
|
@@ -1,192 +1,192 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { View, Text } from 'react-native';
|
|
3
|
-
|
|
4
|
-
import type { ChessColor, CoordinatePosition } from './types';
|
|
5
|
-
|
|
6
|
-
type BoardCoordinatesProps = {
|
|
7
|
-
boardSize: number;
|
|
8
|
-
orientation: ChessColor;
|
|
9
|
-
lightColor: string;
|
|
10
|
-
darkColor: string;
|
|
11
|
-
withLetters: boolean;
|
|
12
|
-
withNumbers: boolean;
|
|
13
|
-
/** 'inside' overlays on edge squares, 'outside' renders in a gutter area. */
|
|
14
|
-
position?: 'inside' | 'outside';
|
|
15
|
-
/** Gutter width in pixels (only used when position='outside'). */
|
|
16
|
-
gutterWidth?: number;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const FILES_WHITE = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
|
|
20
|
-
const FILES_BLACK = ['h', 'g', 'f', 'e', 'd', 'c', 'b', 'a'];
|
|
21
|
-
const RANKS_WHITE = ['8', '7', '6', '5', '4', '3', '2', '1'];
|
|
22
|
-
const RANKS_BLACK = ['1', '2', '3', '4', '5', '6', '7', '8'];
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* File letters (a-h) and rank numbers (1-8) drawn on or around the board.
|
|
26
|
-
*
|
|
27
|
-
* Two modes:
|
|
28
|
-
* - **inside** (default): absolute-positioned inside each edge square, colors
|
|
29
|
-
* alternate to contrast with the square behind them.
|
|
30
|
-
* - **outside**: rendered in a gutter area around the board. Rank numbers to
|
|
31
|
-
* the left, file letters along the bottom. Uses the dark square color.
|
|
32
|
-
*/
|
|
33
|
-
export const BoardCoordinates = React.memo(function BoardCoordinates({
|
|
34
|
-
boardSize,
|
|
35
|
-
orientation,
|
|
36
|
-
lightColor,
|
|
37
|
-
darkColor,
|
|
38
|
-
withLetters,
|
|
39
|
-
withNumbers,
|
|
40
|
-
position = 'inside',
|
|
41
|
-
gutterWidth = 0,
|
|
42
|
-
}: BoardCoordinatesProps) {
|
|
43
|
-
if (!withLetters && !withNumbers) return null;
|
|
44
|
-
|
|
45
|
-
const squareSize = boardSize / 8;
|
|
46
|
-
const files = orientation === 'white' ? FILES_WHITE : FILES_BLACK;
|
|
47
|
-
const ranks = orientation === 'white' ? RANKS_WHITE : RANKS_BLACK;
|
|
48
|
-
|
|
49
|
-
// ── Outside mode: labels in gutter area around the board ──
|
|
50
|
-
if (position === 'outside') {
|
|
51
|
-
const fontSize = gutterWidth * 0.65;
|
|
52
|
-
const textColor = darkColor;
|
|
53
|
-
|
|
54
|
-
return (
|
|
55
|
-
<>
|
|
56
|
-
{/* Rank numbers — left gutter, vertically centered on each row */}
|
|
57
|
-
{withNumbers && (
|
|
58
|
-
<View
|
|
59
|
-
style={{
|
|
60
|
-
position: 'absolute',
|
|
61
|
-
left: 0,
|
|
62
|
-
top: 0,
|
|
63
|
-
width: gutterWidth,
|
|
64
|
-
height: boardSize,
|
|
65
|
-
}}
|
|
66
|
-
pointerEvents="none"
|
|
67
|
-
>
|
|
68
|
-
{ranks.map((rank, row) => (
|
|
69
|
-
<View
|
|
70
|
-
key={`r-${rank}`}
|
|
71
|
-
style={{
|
|
72
|
-
position: 'absolute',
|
|
73
|
-
top: row * squareSize,
|
|
74
|
-
width: gutterWidth,
|
|
75
|
-
height: squareSize,
|
|
76
|
-
alignItems: 'center',
|
|
77
|
-
justifyContent: 'center',
|
|
78
|
-
}}
|
|
79
|
-
>
|
|
80
|
-
<Text
|
|
81
|
-
style={{
|
|
82
|
-
fontSize,
|
|
83
|
-
fontWeight: '600',
|
|
84
|
-
color: textColor,
|
|
85
|
-
}}
|
|
86
|
-
>
|
|
87
|
-
{rank}
|
|
88
|
-
</Text>
|
|
89
|
-
</View>
|
|
90
|
-
))}
|
|
91
|
-
</View>
|
|
92
|
-
)}
|
|
93
|
-
|
|
94
|
-
{/* File letters — bottom gutter, horizontally centered on each column */}
|
|
95
|
-
{withLetters && (
|
|
96
|
-
<View
|
|
97
|
-
style={{
|
|
98
|
-
position: 'absolute',
|
|
99
|
-
left: withNumbers ? gutterWidth : 0,
|
|
100
|
-
bottom: 0,
|
|
101
|
-
width: boardSize,
|
|
102
|
-
height: gutterWidth,
|
|
103
|
-
}}
|
|
104
|
-
pointerEvents="none"
|
|
105
|
-
>
|
|
106
|
-
{files.map((file, col) => (
|
|
107
|
-
<View
|
|
108
|
-
key={`f-${file}`}
|
|
109
|
-
style={{
|
|
110
|
-
position: 'absolute',
|
|
111
|
-
left: col * squareSize,
|
|
112
|
-
width: squareSize,
|
|
113
|
-
height: gutterWidth,
|
|
114
|
-
alignItems: 'center',
|
|
115
|
-
justifyContent: 'center',
|
|
116
|
-
}}
|
|
117
|
-
>
|
|
118
|
-
<Text
|
|
119
|
-
style={{
|
|
120
|
-
fontSize,
|
|
121
|
-
fontWeight: '600',
|
|
122
|
-
color: textColor,
|
|
123
|
-
}}
|
|
124
|
-
>
|
|
125
|
-
{file}
|
|
126
|
-
</Text>
|
|
127
|
-
</View>
|
|
128
|
-
))}
|
|
129
|
-
</View>
|
|
130
|
-
)}
|
|
131
|
-
</>
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ── Inside mode (default): absolute-positioned inside edge squares ──
|
|
136
|
-
const fontSize = squareSize * 0.22;
|
|
137
|
-
const padding = squareSize * 0.06;
|
|
138
|
-
|
|
139
|
-
return (
|
|
140
|
-
<View
|
|
141
|
-
style={{ position: 'absolute', width: boardSize, height: boardSize }}
|
|
142
|
-
pointerEvents="none"
|
|
143
|
-
>
|
|
144
|
-
{/* Rank numbers along left edge (inside each row's first square) */}
|
|
145
|
-
{withNumbers &&
|
|
146
|
-
ranks.map((rank, row) => {
|
|
147
|
-
const isLight = row % 2 === 0;
|
|
148
|
-
const textColor = isLight ? darkColor : lightColor;
|
|
149
|
-
|
|
150
|
-
return (
|
|
151
|
-
<Text
|
|
152
|
-
key={`r-${rank}`}
|
|
153
|
-
style={{
|
|
154
|
-
position: 'absolute',
|
|
155
|
-
left: padding,
|
|
156
|
-
top: row * squareSize + padding,
|
|
157
|
-
fontSize,
|
|
158
|
-
fontWeight: '700',
|
|
159
|
-
color: textColor,
|
|
160
|
-
}}
|
|
161
|
-
>
|
|
162
|
-
{rank}
|
|
163
|
-
</Text>
|
|
164
|
-
);
|
|
165
|
-
})}
|
|
166
|
-
|
|
167
|
-
{/* File letters along bottom edge (inside each column's last square) */}
|
|
168
|
-
{withLetters &&
|
|
169
|
-
files.map((file, col) => {
|
|
170
|
-
const isLight = (7 + col) % 2 === 0;
|
|
171
|
-
const textColor = isLight ? darkColor : lightColor;
|
|
172
|
-
|
|
173
|
-
return (
|
|
174
|
-
<Text
|
|
175
|
-
key={`f-${file}`}
|
|
176
|
-
style={{
|
|
177
|
-
position: 'absolute',
|
|
178
|
-
right: (7 - col) * squareSize + padding,
|
|
179
|
-
bottom: padding,
|
|
180
|
-
fontSize,
|
|
181
|
-
fontWeight: '700',
|
|
182
|
-
color: textColor,
|
|
183
|
-
textAlign: 'right',
|
|
184
|
-
}}
|
|
185
|
-
>
|
|
186
|
-
{file}
|
|
187
|
-
</Text>
|
|
188
|
-
);
|
|
189
|
-
})}
|
|
190
|
-
</View>
|
|
191
|
-
);
|
|
192
|
-
});
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { ChessColor, CoordinatePosition } from './types';
|
|
5
|
+
|
|
6
|
+
type BoardCoordinatesProps = {
|
|
7
|
+
boardSize: number;
|
|
8
|
+
orientation: ChessColor;
|
|
9
|
+
lightColor: string;
|
|
10
|
+
darkColor: string;
|
|
11
|
+
withLetters: boolean;
|
|
12
|
+
withNumbers: boolean;
|
|
13
|
+
/** 'inside' overlays on edge squares, 'outside' renders in a gutter area. */
|
|
14
|
+
position?: 'inside' | 'outside';
|
|
15
|
+
/** Gutter width in pixels (only used when position='outside'). */
|
|
16
|
+
gutterWidth?: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const FILES_WHITE = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
|
|
20
|
+
const FILES_BLACK = ['h', 'g', 'f', 'e', 'd', 'c', 'b', 'a'];
|
|
21
|
+
const RANKS_WHITE = ['8', '7', '6', '5', '4', '3', '2', '1'];
|
|
22
|
+
const RANKS_BLACK = ['1', '2', '3', '4', '5', '6', '7', '8'];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* File letters (a-h) and rank numbers (1-8) drawn on or around the board.
|
|
26
|
+
*
|
|
27
|
+
* Two modes:
|
|
28
|
+
* - **inside** (default): absolute-positioned inside each edge square, colors
|
|
29
|
+
* alternate to contrast with the square behind them.
|
|
30
|
+
* - **outside**: rendered in a gutter area around the board. Rank numbers to
|
|
31
|
+
* the left, file letters along the bottom. Uses the dark square color.
|
|
32
|
+
*/
|
|
33
|
+
export const BoardCoordinates = React.memo(function BoardCoordinates({
|
|
34
|
+
boardSize,
|
|
35
|
+
orientation,
|
|
36
|
+
lightColor,
|
|
37
|
+
darkColor,
|
|
38
|
+
withLetters,
|
|
39
|
+
withNumbers,
|
|
40
|
+
position = 'inside',
|
|
41
|
+
gutterWidth = 0,
|
|
42
|
+
}: BoardCoordinatesProps) {
|
|
43
|
+
if (!withLetters && !withNumbers) return null;
|
|
44
|
+
|
|
45
|
+
const squareSize = boardSize / 8;
|
|
46
|
+
const files = orientation === 'white' ? FILES_WHITE : FILES_BLACK;
|
|
47
|
+
const ranks = orientation === 'white' ? RANKS_WHITE : RANKS_BLACK;
|
|
48
|
+
|
|
49
|
+
// ── Outside mode: labels in gutter area around the board ──
|
|
50
|
+
if (position === 'outside') {
|
|
51
|
+
const fontSize = gutterWidth * 0.65;
|
|
52
|
+
const textColor = darkColor;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<>
|
|
56
|
+
{/* Rank numbers — left gutter, vertically centered on each row */}
|
|
57
|
+
{withNumbers && (
|
|
58
|
+
<View
|
|
59
|
+
style={{
|
|
60
|
+
position: 'absolute',
|
|
61
|
+
left: 0,
|
|
62
|
+
top: 0,
|
|
63
|
+
width: gutterWidth,
|
|
64
|
+
height: boardSize,
|
|
65
|
+
}}
|
|
66
|
+
pointerEvents="none"
|
|
67
|
+
>
|
|
68
|
+
{ranks.map((rank, row) => (
|
|
69
|
+
<View
|
|
70
|
+
key={`r-${rank}`}
|
|
71
|
+
style={{
|
|
72
|
+
position: 'absolute',
|
|
73
|
+
top: row * squareSize,
|
|
74
|
+
width: gutterWidth,
|
|
75
|
+
height: squareSize,
|
|
76
|
+
alignItems: 'center',
|
|
77
|
+
justifyContent: 'center',
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
<Text
|
|
81
|
+
style={{
|
|
82
|
+
fontSize,
|
|
83
|
+
fontWeight: '600',
|
|
84
|
+
color: textColor,
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
{rank}
|
|
88
|
+
</Text>
|
|
89
|
+
</View>
|
|
90
|
+
))}
|
|
91
|
+
</View>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
{/* File letters — bottom gutter, horizontally centered on each column */}
|
|
95
|
+
{withLetters && (
|
|
96
|
+
<View
|
|
97
|
+
style={{
|
|
98
|
+
position: 'absolute',
|
|
99
|
+
left: withNumbers ? gutterWidth : 0,
|
|
100
|
+
bottom: 0,
|
|
101
|
+
width: boardSize,
|
|
102
|
+
height: gutterWidth,
|
|
103
|
+
}}
|
|
104
|
+
pointerEvents="none"
|
|
105
|
+
>
|
|
106
|
+
{files.map((file, col) => (
|
|
107
|
+
<View
|
|
108
|
+
key={`f-${file}`}
|
|
109
|
+
style={{
|
|
110
|
+
position: 'absolute',
|
|
111
|
+
left: col * squareSize,
|
|
112
|
+
width: squareSize,
|
|
113
|
+
height: gutterWidth,
|
|
114
|
+
alignItems: 'center',
|
|
115
|
+
justifyContent: 'center',
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
<Text
|
|
119
|
+
style={{
|
|
120
|
+
fontSize,
|
|
121
|
+
fontWeight: '600',
|
|
122
|
+
color: textColor,
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
{file}
|
|
126
|
+
</Text>
|
|
127
|
+
</View>
|
|
128
|
+
))}
|
|
129
|
+
</View>
|
|
130
|
+
)}
|
|
131
|
+
</>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Inside mode (default): absolute-positioned inside edge squares ──
|
|
136
|
+
const fontSize = squareSize * 0.22;
|
|
137
|
+
const padding = squareSize * 0.06;
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<View
|
|
141
|
+
style={{ position: 'absolute', width: boardSize, height: boardSize }}
|
|
142
|
+
pointerEvents="none"
|
|
143
|
+
>
|
|
144
|
+
{/* Rank numbers along left edge (inside each row's first square) */}
|
|
145
|
+
{withNumbers &&
|
|
146
|
+
ranks.map((rank, row) => {
|
|
147
|
+
const isLight = row % 2 === 0;
|
|
148
|
+
const textColor = isLight ? darkColor : lightColor;
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<Text
|
|
152
|
+
key={`r-${rank}`}
|
|
153
|
+
style={{
|
|
154
|
+
position: 'absolute',
|
|
155
|
+
left: padding,
|
|
156
|
+
top: row * squareSize + padding,
|
|
157
|
+
fontSize,
|
|
158
|
+
fontWeight: '700',
|
|
159
|
+
color: textColor,
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
{rank}
|
|
163
|
+
</Text>
|
|
164
|
+
);
|
|
165
|
+
})}
|
|
166
|
+
|
|
167
|
+
{/* File letters along bottom edge (inside each column's last square) */}
|
|
168
|
+
{withLetters &&
|
|
169
|
+
files.map((file, col) => {
|
|
170
|
+
const isLight = (7 + col) % 2 === 0;
|
|
171
|
+
const textColor = isLight ? darkColor : lightColor;
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<Text
|
|
175
|
+
key={`f-${file}`}
|
|
176
|
+
style={{
|
|
177
|
+
position: 'absolute',
|
|
178
|
+
right: (7 - col) * squareSize + padding,
|
|
179
|
+
bottom: padding,
|
|
180
|
+
fontSize,
|
|
181
|
+
fontWeight: '700',
|
|
182
|
+
color: textColor,
|
|
183
|
+
textAlign: 'right',
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
{file}
|
|
187
|
+
</Text>
|
|
188
|
+
);
|
|
189
|
+
})}
|
|
190
|
+
</View>
|
|
191
|
+
);
|
|
192
|
+
});
|
package/src/board-drag-ghost.tsx
CHANGED
|
@@ -1,132 +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'];
|
|
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'];
|