react-native-chess-kit 0.5.2 → 0.5.3

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.
Files changed (106) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +187 -168
  3. package/lib/commonjs/board-annotations.js +8 -8
  4. package/lib/commonjs/board-annotations.js.map +1 -1
  5. package/lib/commonjs/board-arrows.js.map +1 -1
  6. package/lib/commonjs/board-background.js +5 -5
  7. package/lib/commonjs/board-background.js.map +1 -1
  8. package/lib/commonjs/board-coordinates.js +8 -8
  9. package/lib/commonjs/board-coordinates.js.map +1 -1
  10. package/lib/commonjs/board-drag-ghost.js +10 -10
  11. package/lib/commonjs/board-drag-ghost.js.map +1 -1
  12. package/lib/commonjs/board-highlights.js +15 -15
  13. package/lib/commonjs/board-highlights.js.map +1 -1
  14. package/lib/commonjs/board-legal-dots.js +5 -5
  15. package/lib/commonjs/board-legal-dots.js.map +1 -1
  16. package/lib/commonjs/board-piece.js +25 -25
  17. package/lib/commonjs/board-piece.js.map +1 -1
  18. package/lib/commonjs/board-pieces.js +6 -6
  19. package/lib/commonjs/board-pieces.js.map +1 -1
  20. package/lib/commonjs/board.js +68 -63
  21. package/lib/commonjs/board.js.map +1 -1
  22. package/lib/commonjs/constants.js.map +1 -1
  23. package/lib/commonjs/index.js.map +1 -1
  24. package/lib/commonjs/pieces/default-pieces.js.map +1 -1
  25. package/lib/commonjs/pieces/index.js.map +1 -1
  26. package/lib/commonjs/promotion-picker.js +9 -9
  27. package/lib/commonjs/promotion-picker.js.map +1 -1
  28. package/lib/commonjs/static-board.js +7 -7
  29. package/lib/commonjs/static-board.js.map +1 -1
  30. package/lib/commonjs/themes.js.map +1 -1
  31. package/lib/commonjs/types.js.map +1 -1
  32. package/lib/commonjs/use-board-gesture.js +26 -26
  33. package/lib/commonjs/use-board-gesture.js.map +1 -1
  34. package/lib/commonjs/use-board-pieces.js +15 -15
  35. package/lib/commonjs/use-board-pieces.js.map +1 -1
  36. package/lib/commonjs/use-board-state.js +21 -12
  37. package/lib/commonjs/use-board-state.js.map +1 -1
  38. package/lib/commonjs/use-premove.js +12 -12
  39. package/lib/commonjs/use-premove.js.map +1 -1
  40. package/lib/module/board-annotations.js +8 -8
  41. package/lib/module/board-annotations.js.map +1 -1
  42. package/lib/module/board-arrows.js.map +1 -1
  43. package/lib/module/board-background.js +5 -5
  44. package/lib/module/board-background.js.map +1 -1
  45. package/lib/module/board-coordinates.js +8 -8
  46. package/lib/module/board-coordinates.js.map +1 -1
  47. package/lib/module/board-drag-ghost.js +10 -10
  48. package/lib/module/board-drag-ghost.js.map +1 -1
  49. package/lib/module/board-highlights.js +15 -15
  50. package/lib/module/board-highlights.js.map +1 -1
  51. package/lib/module/board-legal-dots.js +5 -5
  52. package/lib/module/board-legal-dots.js.map +1 -1
  53. package/lib/module/board-piece.js +25 -25
  54. package/lib/module/board-piece.js.map +1 -1
  55. package/lib/module/board-pieces.js +6 -6
  56. package/lib/module/board-pieces.js.map +1 -1
  57. package/lib/module/board.js +68 -63
  58. package/lib/module/board.js.map +1 -1
  59. package/lib/module/constants.js.map +1 -1
  60. package/lib/module/index.js.map +1 -1
  61. package/lib/module/pieces/default-pieces.js.map +1 -1
  62. package/lib/module/pieces/index.js.map +1 -1
  63. package/lib/module/promotion-picker.js +9 -9
  64. package/lib/module/promotion-picker.js.map +1 -1
  65. package/lib/module/static-board.js +7 -7
  66. package/lib/module/static-board.js.map +1 -1
  67. package/lib/module/themes.js.map +1 -1
  68. package/lib/module/types.js.map +1 -1
  69. package/lib/module/use-board-gesture.js +26 -26
  70. package/lib/module/use-board-gesture.js.map +1 -1
  71. package/lib/module/use-board-pieces.js +15 -15
  72. package/lib/module/use-board-pieces.js.map +1 -1
  73. package/lib/module/use-board-state.js +21 -12
  74. package/lib/module/use-board-state.js.map +1 -1
  75. package/lib/module/use-premove.js +12 -12
  76. package/lib/module/use-premove.js.map +1 -1
  77. package/lib/typescript/board-coordinates.d.ts.map +1 -1
  78. package/lib/typescript/board.d.ts.map +1 -1
  79. package/lib/typescript/promotion-picker.d.ts.map +1 -1
  80. package/lib/typescript/static-board.d.ts.map +1 -1
  81. package/lib/typescript/types.d.ts +12 -2
  82. package/lib/typescript/types.d.ts.map +1 -1
  83. package/lib/typescript/use-board-gesture.d.ts.map +1 -1
  84. package/lib/typescript/use-board-state.d.ts.map +1 -1
  85. package/package.json +23 -3
  86. package/src/board-annotations.tsx +147 -147
  87. package/src/board-background.tsx +46 -46
  88. package/src/board-coordinates.tsx +192 -192
  89. package/src/board-drag-ghost.tsx +132 -132
  90. package/src/board-highlights.tsx +226 -226
  91. package/src/board-legal-dots.tsx +73 -73
  92. package/src/board-piece.tsx +160 -160
  93. package/src/board-pieces.tsx +63 -63
  94. package/src/board.tsx +688 -685
  95. package/src/constants.ts +103 -103
  96. package/src/index.ts +101 -101
  97. package/src/pieces/default-pieces.tsx +383 -383
  98. package/src/pieces/index.ts +1 -1
  99. package/src/promotion-picker.tsx +147 -147
  100. package/src/static-board.tsx +186 -187
  101. package/src/themes.ts +129 -129
  102. package/src/types.ts +394 -373
  103. package/src/use-board-gesture.ts +459 -429
  104. package/src/use-board-pieces.ts +158 -158
  105. package/src/use-board-state.ts +120 -111
  106. 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 } 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,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'];