react-native-chess-kit 0.1.0 → 0.2.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/lib/commonjs/board-annotations.js +131 -0
- package/lib/commonjs/board-annotations.js.map +1 -0
- package/lib/commonjs/board-arrows.js +164 -0
- package/lib/commonjs/board-arrows.js.map +1 -0
- package/lib/commonjs/board-highlights.js +212 -0
- package/lib/commonjs/board-highlights.js.map +1 -0
- package/lib/commonjs/board-piece.js +41 -12
- package/lib/commonjs/board-piece.js.map +1 -1
- package/lib/commonjs/board-pieces.js +2 -0
- package/lib/commonjs/board-pieces.js.map +1 -1
- package/lib/commonjs/board.js +392 -42
- package/lib/commonjs/board.js.map +1 -1
- package/lib/commonjs/constants.js +104 -0
- package/lib/commonjs/constants.js.map +1 -0
- package/lib/commonjs/index.js +128 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/pieces/default-pieces.js +536 -0
- package/lib/commonjs/pieces/default-pieces.js.map +1 -0
- package/lib/commonjs/pieces/index.js +13 -0
- package/lib/commonjs/pieces/index.js.map +1 -0
- package/lib/commonjs/promotion-picker.js +129 -0
- package/lib/commonjs/promotion-picker.js.map +1 -0
- package/lib/commonjs/static-board.js +150 -0
- package/lib/commonjs/static-board.js.map +1 -0
- package/lib/commonjs/themes.js +175 -0
- package/lib/commonjs/themes.js.map +1 -0
- package/lib/commonjs/use-board-gesture.js +184 -11
- package/lib/commonjs/use-board-gesture.js.map +1 -1
- package/lib/commonjs/use-premove.js +44 -0
- package/lib/commonjs/use-premove.js.map +1 -0
- package/lib/module/board-annotations.js +126 -0
- package/lib/module/board-annotations.js.map +1 -0
- package/lib/module/board-arrows.js +161 -0
- package/lib/module/board-arrows.js.map +1 -0
- package/lib/module/board-highlights.js +206 -0
- package/lib/module/board-highlights.js.map +1 -0
- package/lib/module/board-piece.js +42 -13
- package/lib/module/board-piece.js.map +1 -1
- package/lib/module/board-pieces.js +2 -0
- package/lib/module/board-pieces.js.map +1 -1
- package/lib/module/board.js +395 -44
- package/lib/module/board.js.map +1 -1
- package/lib/module/constants.js +100 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/index.js +29 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/pieces/default-pieces.js +530 -0
- package/lib/module/pieces/default-pieces.js.map +1 -0
- package/lib/module/pieces/index.js +4 -0
- package/lib/module/pieces/index.js.map +1 -0
- package/lib/module/promotion-picker.js +124 -0
- package/lib/module/promotion-picker.js.map +1 -0
- package/lib/module/static-board.js +146 -0
- package/lib/module/static-board.js.map +1 -0
- package/lib/module/themes.js +171 -0
- package/lib/module/themes.js.map +1 -0
- package/lib/module/use-board-gesture.js +185 -11
- package/lib/module/use-board-gesture.js.map +1 -1
- package/lib/module/use-premove.js +40 -0
- package/lib/module/use-premove.js.map +1 -0
- package/lib/typescript/board-annotations.d.ts +30 -0
- package/lib/typescript/board-annotations.d.ts.map +1 -0
- package/lib/typescript/board-arrows.d.ts +27 -0
- package/lib/typescript/board-arrows.d.ts.map +1 -0
- package/lib/typescript/board-highlights.d.ts +65 -0
- package/lib/typescript/board-highlights.d.ts.map +1 -0
- package/lib/typescript/board-piece.d.ts +10 -4
- package/lib/typescript/board-piece.d.ts.map +1 -1
- package/lib/typescript/board-pieces.d.ts +2 -1
- package/lib/typescript/board-pieces.d.ts.map +1 -1
- package/lib/typescript/board.d.ts +11 -2
- package/lib/typescript/board.d.ts.map +1 -1
- package/lib/typescript/constants.d.ts +54 -0
- package/lib/typescript/constants.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +9 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/pieces/default-pieces.d.ts +3 -0
- package/lib/typescript/pieces/default-pieces.d.ts.map +1 -0
- package/lib/typescript/pieces/index.d.ts +2 -0
- package/lib/typescript/pieces/index.d.ts.map +1 -0
- package/lib/typescript/promotion-picker.d.ts +30 -0
- package/lib/typescript/promotion-picker.d.ts.map +1 -0
- package/lib/typescript/static-board.d.ts +12 -0
- package/lib/typescript/static-board.d.ts.map +1 -0
- package/lib/typescript/themes.d.ts +15 -0
- package/lib/typescript/themes.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +194 -24
- package/lib/typescript/types.d.ts.map +1 -1
- package/lib/typescript/use-board-gesture.d.ts +28 -2
- package/lib/typescript/use-board-gesture.d.ts.map +1 -1
- package/lib/typescript/use-premove.d.ts +31 -0
- package/lib/typescript/use-premove.d.ts.map +1 -0
- package/package.json +4 -2
- package/src/board-annotations.tsx +147 -0
- package/src/board-arrows.tsx +197 -0
- package/src/board-highlights.tsx +226 -0
- package/src/board-piece.tsx +50 -12
- package/src/board-pieces.tsx +4 -1
- package/src/board.tsx +462 -46
- package/src/constants.ts +100 -0
- package/src/index.ts +62 -1
- package/src/pieces/default-pieces.tsx +383 -0
- package/src/pieces/index.ts +1 -0
- package/src/promotion-picker.tsx +147 -0
- package/src/static-board.tsx +150 -0
- package/src/themes.ts +129 -0
- package/src/types.ts +251 -25
- package/src/use-board-gesture.ts +219 -8
- package/src/use-premove.ts +59 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { ChessColor, AnnotationData } from './types';
|
|
5
|
+
import { squareToXY } from './use-board-pieces';
|
|
6
|
+
import { DEFAULT_ANNOTATION_BG, DEFAULT_ANNOTATION_TEXT, ANNOTATION_SCALE } from './constants';
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Props
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
type BoardAnnotationsProps = {
|
|
13
|
+
boardSize: number;
|
|
14
|
+
orientation: ChessColor;
|
|
15
|
+
squareSize: number;
|
|
16
|
+
annotations: AnnotationData[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Annotations layer
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Text annotation badges on chess squares (!, ?, !!, ??, etc.).
|
|
25
|
+
*
|
|
26
|
+
* Each annotation appears as a small colored badge at the top-right
|
|
27
|
+
* corner of the target square. Positioned absolutely, pointer-events none.
|
|
28
|
+
*/
|
|
29
|
+
export const BoardAnnotations = React.memo(function BoardAnnotations({
|
|
30
|
+
boardSize,
|
|
31
|
+
orientation,
|
|
32
|
+
squareSize,
|
|
33
|
+
annotations,
|
|
34
|
+
}: BoardAnnotationsProps) {
|
|
35
|
+
if (annotations.length === 0) return null;
|
|
36
|
+
|
|
37
|
+
const badgeSize = squareSize * ANNOTATION_SCALE;
|
|
38
|
+
const fontSize = badgeSize * 0.65;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<View
|
|
42
|
+
style={{ position: 'absolute', width: boardSize, height: boardSize }}
|
|
43
|
+
pointerEvents="none"
|
|
44
|
+
>
|
|
45
|
+
{annotations.map((ann, i) => {
|
|
46
|
+
const { x, y } = squareToXY(ann.square, squareSize, orientation);
|
|
47
|
+
const bgColor = ann.backgroundColor ?? DEFAULT_ANNOTATION_BG;
|
|
48
|
+
const textColor = ann.color ?? DEFAULT_ANNOTATION_TEXT;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<View
|
|
52
|
+
key={`ann-${ann.square}-${i}`}
|
|
53
|
+
style={{
|
|
54
|
+
position: 'absolute',
|
|
55
|
+
// Position at top-right corner of the square
|
|
56
|
+
left: x + squareSize - badgeSize - 1,
|
|
57
|
+
top: y + 1,
|
|
58
|
+
minWidth: badgeSize,
|
|
59
|
+
height: badgeSize,
|
|
60
|
+
borderRadius: badgeSize / 2,
|
|
61
|
+
backgroundColor: bgColor,
|
|
62
|
+
alignItems: 'center',
|
|
63
|
+
justifyContent: 'center',
|
|
64
|
+
paddingHorizontal: 2,
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<Text
|
|
68
|
+
style={{
|
|
69
|
+
color: textColor,
|
|
70
|
+
fontSize,
|
|
71
|
+
fontWeight: '700',
|
|
72
|
+
lineHeight: badgeSize * 0.85,
|
|
73
|
+
textAlign: 'center',
|
|
74
|
+
}}
|
|
75
|
+
numberOfLines={1}
|
|
76
|
+
>
|
|
77
|
+
{ann.text}
|
|
78
|
+
</Text>
|
|
79
|
+
</View>
|
|
80
|
+
);
|
|
81
|
+
})}
|
|
82
|
+
</View>
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Standalone export for advanced consumers
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
type AnnotationBadgeProps = {
|
|
91
|
+
square: string;
|
|
92
|
+
text: string;
|
|
93
|
+
squareSize: number;
|
|
94
|
+
orientation: ChessColor;
|
|
95
|
+
color?: string;
|
|
96
|
+
backgroundColor?: string;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Single annotation badge component.
|
|
101
|
+
* Exported for consumers who build their own overlay layers.
|
|
102
|
+
*/
|
|
103
|
+
export const Annotation = React.memo(function Annotation({
|
|
104
|
+
square,
|
|
105
|
+
text,
|
|
106
|
+
squareSize,
|
|
107
|
+
orientation,
|
|
108
|
+
color,
|
|
109
|
+
backgroundColor,
|
|
110
|
+
}: AnnotationBadgeProps) {
|
|
111
|
+
const { x, y } = squareToXY(square, squareSize, orientation);
|
|
112
|
+
const badgeSize = squareSize * ANNOTATION_SCALE;
|
|
113
|
+
const fontSize = badgeSize * 0.65;
|
|
114
|
+
const bgColor = backgroundColor ?? DEFAULT_ANNOTATION_BG;
|
|
115
|
+
const textColor = color ?? DEFAULT_ANNOTATION_TEXT;
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<View
|
|
119
|
+
style={{
|
|
120
|
+
position: 'absolute',
|
|
121
|
+
left: x + squareSize - badgeSize - 1,
|
|
122
|
+
top: y + 1,
|
|
123
|
+
minWidth: badgeSize,
|
|
124
|
+
height: badgeSize,
|
|
125
|
+
borderRadius: badgeSize / 2,
|
|
126
|
+
backgroundColor: bgColor,
|
|
127
|
+
alignItems: 'center',
|
|
128
|
+
justifyContent: 'center',
|
|
129
|
+
paddingHorizontal: 2,
|
|
130
|
+
}}
|
|
131
|
+
pointerEvents="none"
|
|
132
|
+
>
|
|
133
|
+
<Text
|
|
134
|
+
style={{
|
|
135
|
+
color: textColor,
|
|
136
|
+
fontSize,
|
|
137
|
+
fontWeight: '700',
|
|
138
|
+
lineHeight: badgeSize * 0.85,
|
|
139
|
+
textAlign: 'center',
|
|
140
|
+
}}
|
|
141
|
+
numberOfLines={1}
|
|
142
|
+
>
|
|
143
|
+
{text}
|
|
144
|
+
</Text>
|
|
145
|
+
</View>
|
|
146
|
+
);
|
|
147
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import Svg, { Line, Polygon, Circle as SvgCircle } from 'react-native-svg';
|
|
4
|
+
|
|
5
|
+
import type { ChessColor, ArrowData, ShapeData } from './types';
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_ARROW_COLOR,
|
|
8
|
+
DEFAULT_SHAPE_COLOR,
|
|
9
|
+
ARROW_STROKE_WIDTH,
|
|
10
|
+
ARROW_HEAD_SIZE,
|
|
11
|
+
ARROW_SHORTEN_BY,
|
|
12
|
+
} from './constants';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Coordinate utilities (percentage-based viewBox 0-100)
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
function squareToViewBoxCenter(
|
|
19
|
+
square: string,
|
|
20
|
+
orientation: ChessColor,
|
|
21
|
+
): { x: number; y: number } {
|
|
22
|
+
const fileIdx = square.charCodeAt(0) - 97;
|
|
23
|
+
const rankIdx = parseInt(square[1], 10) - 1;
|
|
24
|
+
const col = orientation === 'white' ? fileIdx : 7 - fileIdx;
|
|
25
|
+
const row = orientation === 'white' ? 7 - rankIdx : rankIdx;
|
|
26
|
+
return {
|
|
27
|
+
x: (col + 0.5) * 12.5,
|
|
28
|
+
y: (row + 0.5) * 12.5,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function calculateArrowPath(
|
|
33
|
+
from: { x: number; y: number },
|
|
34
|
+
to: { x: number; y: number },
|
|
35
|
+
shortenBy: number,
|
|
36
|
+
): { x1: number; y1: number; x2: number; y2: number } {
|
|
37
|
+
const dx = to.x - from.x;
|
|
38
|
+
const dy = to.y - from.y;
|
|
39
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
40
|
+
if (len === 0) return { x1: from.x, y1: from.y, x2: to.x, y2: to.y };
|
|
41
|
+
|
|
42
|
+
const ux = dx / len;
|
|
43
|
+
const uy = dy / len;
|
|
44
|
+
return {
|
|
45
|
+
x1: from.x + ux * shortenBy,
|
|
46
|
+
y1: from.y + uy * shortenBy,
|
|
47
|
+
x2: to.x - ux * shortenBy,
|
|
48
|
+
y2: to.y - uy * shortenBy,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Props
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
type BoardArrowsProps = {
|
|
57
|
+
boardSize: number;
|
|
58
|
+
orientation: ChessColor;
|
|
59
|
+
arrows?: ArrowData[];
|
|
60
|
+
shapes?: ShapeData[];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Combined arrows + shapes SVG layer
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* SVG overlay for arrows and shapes drawn on the board.
|
|
69
|
+
*
|
|
70
|
+
* Uses a percentage-based 100x100 viewBox so coordinates map cleanly
|
|
71
|
+
* to the 8x8 grid (each square = 12.5 x 12.5 units).
|
|
72
|
+
*
|
|
73
|
+
* Renders above the pieces layer so arrows are always visible.
|
|
74
|
+
*/
|
|
75
|
+
export const BoardArrows = React.memo(function BoardArrows({
|
|
76
|
+
boardSize,
|
|
77
|
+
orientation,
|
|
78
|
+
arrows,
|
|
79
|
+
shapes,
|
|
80
|
+
}: BoardArrowsProps) {
|
|
81
|
+
const hasArrows = arrows && arrows.length > 0;
|
|
82
|
+
const hasShapes = shapes && shapes.length > 0;
|
|
83
|
+
|
|
84
|
+
if (!hasArrows && !hasShapes) return null;
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<View
|
|
88
|
+
style={{ position: 'absolute', width: boardSize, height: boardSize }}
|
|
89
|
+
pointerEvents="none"
|
|
90
|
+
>
|
|
91
|
+
<Svg viewBox="0 0 100 100" width={boardSize} height={boardSize}>
|
|
92
|
+
{/* Shapes (circles) -- drawn under arrows */}
|
|
93
|
+
{hasShapes &&
|
|
94
|
+
shapes.map((shape, i) => {
|
|
95
|
+
if (shape.type === 'circle') {
|
|
96
|
+
const center = squareToViewBoxCenter(shape.square, orientation);
|
|
97
|
+
const color = shape.color ?? DEFAULT_SHAPE_COLOR;
|
|
98
|
+
return (
|
|
99
|
+
<SvgCircle
|
|
100
|
+
key={`circle-${shape.square}-${i}`}
|
|
101
|
+
cx={center.x}
|
|
102
|
+
cy={center.y}
|
|
103
|
+
r={5.5}
|
|
104
|
+
fill="none"
|
|
105
|
+
stroke={color}
|
|
106
|
+
strokeWidth={1.2}
|
|
107
|
+
opacity={0.85}
|
|
108
|
+
/>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
})}
|
|
113
|
+
|
|
114
|
+
{/* Arrows */}
|
|
115
|
+
{hasArrows &&
|
|
116
|
+
arrows.map((arrow, i) => (
|
|
117
|
+
<ArrowSvg
|
|
118
|
+
key={`arrow-${arrow.from}-${arrow.to}-${i}`}
|
|
119
|
+
from={arrow.from}
|
|
120
|
+
to={arrow.to}
|
|
121
|
+
color={arrow.color ?? DEFAULT_ARROW_COLOR}
|
|
122
|
+
width={arrow.width ?? ARROW_STROKE_WIDTH}
|
|
123
|
+
orientation={orientation}
|
|
124
|
+
/>
|
|
125
|
+
))}
|
|
126
|
+
</Svg>
|
|
127
|
+
</View>
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Single arrow SVG element
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
type ArrowSvgProps = {
|
|
136
|
+
from: string;
|
|
137
|
+
to: string;
|
|
138
|
+
color: string;
|
|
139
|
+
width: number;
|
|
140
|
+
orientation: ChessColor;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const ArrowSvg = React.memo(function ArrowSvg({
|
|
144
|
+
from,
|
|
145
|
+
to,
|
|
146
|
+
color,
|
|
147
|
+
width,
|
|
148
|
+
orientation,
|
|
149
|
+
}: ArrowSvgProps) {
|
|
150
|
+
const fromCoord = squareToViewBoxCenter(from, orientation);
|
|
151
|
+
const toCoord = squareToViewBoxCenter(to, orientation);
|
|
152
|
+
const path = calculateArrowPath(fromCoord, toCoord, ARROW_SHORTEN_BY);
|
|
153
|
+
|
|
154
|
+
const dx = path.x2 - path.x1;
|
|
155
|
+
const dy = path.y2 - path.y1;
|
|
156
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
157
|
+
|
|
158
|
+
if (len === 0) return null;
|
|
159
|
+
|
|
160
|
+
const ux = dx / len;
|
|
161
|
+
const uy = dy / len;
|
|
162
|
+
const headSize = ARROW_HEAD_SIZE;
|
|
163
|
+
|
|
164
|
+
const tip = { x: path.x2, y: path.y2 };
|
|
165
|
+
const baseLeft = {
|
|
166
|
+
x: path.x2 - ux * headSize * 2 + uy * headSize,
|
|
167
|
+
y: path.y2 - uy * headSize * 2 - ux * headSize,
|
|
168
|
+
};
|
|
169
|
+
const baseRight = {
|
|
170
|
+
x: path.x2 - ux * headSize * 2 - uy * headSize,
|
|
171
|
+
y: path.y2 - uy * headSize * 2 + ux * headSize,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const arrowPoints = `${tip.x},${tip.y} ${baseLeft.x},${baseLeft.y} ${baseRight.x},${baseRight.y}`;
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<>
|
|
178
|
+
<Line
|
|
179
|
+
x1={path.x1}
|
|
180
|
+
y1={path.y1}
|
|
181
|
+
x2={path.x2}
|
|
182
|
+
y2={path.y2}
|
|
183
|
+
stroke={color}
|
|
184
|
+
strokeWidth={width}
|
|
185
|
+
strokeLinecap="round"
|
|
186
|
+
opacity={0.85}
|
|
187
|
+
/>
|
|
188
|
+
<Polygon points={arrowPoints} fill={color} opacity={0.85} />
|
|
189
|
+
</>
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Standalone exports for advanced consumers
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
export { ArrowSvg as Arrow };
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
type SharedValue,
|
|
6
|
+
} from 'react-native-reanimated';
|
|
7
|
+
|
|
8
|
+
import type { ChessColor, HighlightData } from './types';
|
|
9
|
+
import { squareToXY } from './use-board-pieces';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Props
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
type BoardHighlightsProps = {
|
|
16
|
+
boardSize: number;
|
|
17
|
+
orientation: ChessColor;
|
|
18
|
+
squareSize: number;
|
|
19
|
+
/** Last move squares to highlight */
|
|
20
|
+
lastMove?: { from: string; to: string } | null;
|
|
21
|
+
lastMoveColor: string;
|
|
22
|
+
/** Check highlight on king square (auto-detected) */
|
|
23
|
+
checkSquare?: string | null;
|
|
24
|
+
checkColor: string;
|
|
25
|
+
/** Selected piece square */
|
|
26
|
+
selectedSquare?: string | null;
|
|
27
|
+
selectedColor: string;
|
|
28
|
+
/** Premove squares */
|
|
29
|
+
premoveSquares?: { from: string; to: string } | null;
|
|
30
|
+
premoveColor: string;
|
|
31
|
+
/** Custom highlights from consumer */
|
|
32
|
+
highlights?: HighlightData[];
|
|
33
|
+
/** Imperative highlights (from ref.highlight()) */
|
|
34
|
+
imperativeHighlights?: HighlightData[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type DragTargetHighlightProps = {
|
|
38
|
+
squareSize: number;
|
|
39
|
+
orientation: ChessColor;
|
|
40
|
+
dragTargetSquare: SharedValue<string | null>;
|
|
41
|
+
color: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Static highlights (View-based, no animation needed)
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Renders all board square highlights as a single layer.
|
|
50
|
+
*
|
|
51
|
+
* Combines multiple highlight sources (last move, check, selected piece,
|
|
52
|
+
* premoves, custom, and imperative) into one flat list of colored rectangles.
|
|
53
|
+
* Rendered between the background and pieces layers.
|
|
54
|
+
*
|
|
55
|
+
* Uses plain Views (not Animated) because highlights change discretely
|
|
56
|
+
* on move events, not during animation frames.
|
|
57
|
+
*/
|
|
58
|
+
export const BoardHighlights = React.memo(function BoardHighlights({
|
|
59
|
+
boardSize,
|
|
60
|
+
orientation,
|
|
61
|
+
squareSize,
|
|
62
|
+
lastMove,
|
|
63
|
+
lastMoveColor,
|
|
64
|
+
checkSquare,
|
|
65
|
+
checkColor,
|
|
66
|
+
selectedSquare,
|
|
67
|
+
selectedColor,
|
|
68
|
+
premoveSquares,
|
|
69
|
+
premoveColor,
|
|
70
|
+
highlights,
|
|
71
|
+
imperativeHighlights,
|
|
72
|
+
}: BoardHighlightsProps) {
|
|
73
|
+
// Collect all highlights into a flat array (ordered by visual priority: bottom to top)
|
|
74
|
+
const allHighlights: HighlightData[] = [];
|
|
75
|
+
|
|
76
|
+
// Last move (lowest priority -- drawn first, underneath everything)
|
|
77
|
+
if (lastMove) {
|
|
78
|
+
allHighlights.push({ square: lastMove.from, color: lastMoveColor });
|
|
79
|
+
allHighlights.push({ square: lastMove.to, color: lastMoveColor });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Premove highlights
|
|
83
|
+
if (premoveSquares) {
|
|
84
|
+
allHighlights.push({ square: premoveSquares.from, color: premoveColor });
|
|
85
|
+
allHighlights.push({ square: premoveSquares.to, color: premoveColor });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Selected piece square
|
|
89
|
+
if (selectedSquare) {
|
|
90
|
+
allHighlights.push({ square: selectedSquare, color: selectedColor });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check indicator (king square)
|
|
94
|
+
if (checkSquare) {
|
|
95
|
+
allHighlights.push({ square: checkSquare, color: checkColor });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Consumer-provided custom highlights
|
|
99
|
+
if (highlights) {
|
|
100
|
+
for (const h of highlights) {
|
|
101
|
+
allHighlights.push(h);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Imperative highlights (from ref.highlight() calls)
|
|
106
|
+
if (imperativeHighlights) {
|
|
107
|
+
for (const h of imperativeHighlights) {
|
|
108
|
+
allHighlights.push(h);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (allHighlights.length === 0) return null;
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<View
|
|
116
|
+
style={{ position: 'absolute', width: boardSize, height: boardSize }}
|
|
117
|
+
pointerEvents="none"
|
|
118
|
+
>
|
|
119
|
+
{allHighlights.map((h, i) => {
|
|
120
|
+
const { x, y } = squareToXY(h.square, squareSize, orientation);
|
|
121
|
+
return (
|
|
122
|
+
<View
|
|
123
|
+
key={`${h.square}-${h.color}-${i}`}
|
|
124
|
+
style={{
|
|
125
|
+
position: 'absolute',
|
|
126
|
+
left: x,
|
|
127
|
+
top: y,
|
|
128
|
+
width: squareSize,
|
|
129
|
+
height: squareSize,
|
|
130
|
+
backgroundColor: h.color,
|
|
131
|
+
}}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
})}
|
|
135
|
+
</View>
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Drag target highlight (Animated, updates on UI thread during drag)
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Highlight on the square currently under the dragged piece.
|
|
145
|
+
* Uses Reanimated shared values for zero-JS-bridge updates during drag.
|
|
146
|
+
*/
|
|
147
|
+
export const DragTargetHighlight = React.memo(function DragTargetHighlight({
|
|
148
|
+
squareSize,
|
|
149
|
+
orientation,
|
|
150
|
+
dragTargetSquare,
|
|
151
|
+
color,
|
|
152
|
+
}: DragTargetHighlightProps) {
|
|
153
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
154
|
+
const square = dragTargetSquare.value;
|
|
155
|
+
if (!square) {
|
|
156
|
+
return { opacity: 0, transform: [{ translateX: 0 }, { translateY: 0 }] };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Inline coordinate calculation (worklet-safe, no function call)
|
|
160
|
+
const fileIdx = square.charCodeAt(0) - 97;
|
|
161
|
+
const rankIdx = parseInt(square[1], 10) - 1;
|
|
162
|
+
const col = orientation === 'white' ? fileIdx : 7 - fileIdx;
|
|
163
|
+
const row = orientation === 'white' ? 7 - rankIdx : rankIdx;
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
opacity: 1,
|
|
167
|
+
transform: [
|
|
168
|
+
{ translateX: col * squareSize },
|
|
169
|
+
{ translateY: row * squareSize },
|
|
170
|
+
],
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<Animated.View
|
|
176
|
+
style={[
|
|
177
|
+
{
|
|
178
|
+
position: 'absolute',
|
|
179
|
+
width: squareSize,
|
|
180
|
+
height: squareSize,
|
|
181
|
+
backgroundColor: color,
|
|
182
|
+
},
|
|
183
|
+
animatedStyle,
|
|
184
|
+
]}
|
|
185
|
+
pointerEvents="none"
|
|
186
|
+
/>
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Standalone SquareHighlight (exported for advanced consumers)
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
type SquareHighlightProps = {
|
|
195
|
+
square: string;
|
|
196
|
+
color: string;
|
|
197
|
+
squareSize: number;
|
|
198
|
+
orientation: ChessColor;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Single square highlight component.
|
|
203
|
+
* Exported for consumers who build their own overlay layers.
|
|
204
|
+
*/
|
|
205
|
+
export const SquareHighlight = React.memo(function SquareHighlight({
|
|
206
|
+
square,
|
|
207
|
+
color,
|
|
208
|
+
squareSize,
|
|
209
|
+
orientation,
|
|
210
|
+
}: SquareHighlightProps) {
|
|
211
|
+
const { x, y } = squareToXY(square, squareSize, orientation);
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<View
|
|
215
|
+
style={{
|
|
216
|
+
position: 'absolute',
|
|
217
|
+
left: x,
|
|
218
|
+
top: y,
|
|
219
|
+
width: squareSize,
|
|
220
|
+
height: squareSize,
|
|
221
|
+
backgroundColor: color,
|
|
222
|
+
}}
|
|
223
|
+
pointerEvents="none"
|
|
224
|
+
/>
|
|
225
|
+
);
|
|
226
|
+
});
|
package/src/board-piece.tsx
CHANGED
|
@@ -3,17 +3,24 @@ import Animated, {
|
|
|
3
3
|
useAnimatedStyle,
|
|
4
4
|
useSharedValue,
|
|
5
5
|
withTiming,
|
|
6
|
+
withSpring,
|
|
7
|
+
FadeOut,
|
|
6
8
|
type SharedValue,
|
|
7
9
|
} from 'react-native-reanimated';
|
|
8
10
|
|
|
11
|
+
import type { AnimationConfig } from './types';
|
|
12
|
+
import { DEFAULT_MOVE_DURATION, CAPTURE_FADE_DURATION } from './constants';
|
|
13
|
+
|
|
9
14
|
type BoardPieceProps = {
|
|
10
15
|
/** Target pixel position (top-left of destination square) */
|
|
11
16
|
targetX: number;
|
|
12
17
|
targetY: number;
|
|
13
18
|
/** Square size in pixels */
|
|
14
19
|
squareSize: number;
|
|
15
|
-
/**
|
|
16
|
-
|
|
20
|
+
/** Animation config for piece movement (timing or spring) */
|
|
21
|
+
animationConfig?: AnimationConfig;
|
|
22
|
+
/** Fallback move duration if animationConfig not provided */
|
|
23
|
+
moveDuration?: number;
|
|
17
24
|
/** The piece visual (rendered by parent via renderPiece) */
|
|
18
25
|
children: React.ReactElement;
|
|
19
26
|
/** Gesture state: is this piece currently being dragged? */
|
|
@@ -23,6 +30,34 @@ type BoardPieceProps = {
|
|
|
23
30
|
square: string;
|
|
24
31
|
};
|
|
25
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Animate a shared value using the provided AnimationConfig.
|
|
35
|
+
* Falls back to withTiming with moveDuration for backwards compatibility.
|
|
36
|
+
*/
|
|
37
|
+
function animateValue(
|
|
38
|
+
target: number,
|
|
39
|
+
config?: AnimationConfig,
|
|
40
|
+
moveDuration?: number,
|
|
41
|
+
): number {
|
|
42
|
+
if (config) {
|
|
43
|
+
if (config.type === 'spring') {
|
|
44
|
+
return withSpring(target, {
|
|
45
|
+
damping: config.damping ?? 15,
|
|
46
|
+
stiffness: config.stiffness ?? 200,
|
|
47
|
+
mass: config.mass ?? 1,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
// timing
|
|
51
|
+
return withTiming(target, {
|
|
52
|
+
duration: config.duration ?? DEFAULT_MOVE_DURATION,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const duration = moveDuration ?? DEFAULT_MOVE_DURATION;
|
|
57
|
+
if (duration <= 0) return target;
|
|
58
|
+
return withTiming(target, { duration });
|
|
59
|
+
}
|
|
60
|
+
|
|
26
61
|
/**
|
|
27
62
|
* A single animated chess piece.
|
|
28
63
|
*
|
|
@@ -35,14 +70,18 @@ type BoardPieceProps = {
|
|
|
35
70
|
* - No position changes on the original piece during drag
|
|
36
71
|
*
|
|
37
72
|
* After a move:
|
|
38
|
-
* - Snaps to new position via withTiming on translateX/translateY
|
|
39
|
-
* -
|
|
73
|
+
* - Snaps to new position via withTiming/withSpring on translateX/translateY
|
|
74
|
+
* - Animation style controlled by user's animationConfig or moveDuration
|
|
75
|
+
*
|
|
76
|
+
* On capture (unmount):
|
|
77
|
+
* - Fades out via Reanimated's `exiting` prop (FadeOut)
|
|
40
78
|
*/
|
|
41
79
|
export const BoardPieceView = React.memo(
|
|
42
80
|
function BoardPieceView({
|
|
43
81
|
targetX,
|
|
44
82
|
targetY,
|
|
45
83
|
squareSize,
|
|
84
|
+
animationConfig,
|
|
46
85
|
moveDuration,
|
|
47
86
|
children,
|
|
48
87
|
activeSquare,
|
|
@@ -57,13 +96,9 @@ export const BoardPieceView = React.memo(
|
|
|
57
96
|
// useEffect is the correct pattern for reacting to JS prop changes —
|
|
58
97
|
// useDerivedValue is meant for shared-value-to-shared-value derivation.
|
|
59
98
|
useEffect(() => {
|
|
60
|
-
currentX.value =
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
currentY.value = moveDuration > 0
|
|
64
|
-
? withTiming(targetY, { duration: moveDuration })
|
|
65
|
-
: targetY;
|
|
66
|
-
}, [targetX, targetY, moveDuration, currentX, currentY]);
|
|
99
|
+
currentX.value = animateValue(targetX, animationConfig, moveDuration);
|
|
100
|
+
currentY.value = animateValue(targetY, animationConfig, moveDuration);
|
|
101
|
+
}, [targetX, targetY, animationConfig, moveDuration, currentX, currentY]);
|
|
67
102
|
|
|
68
103
|
const animatedStyle = useAnimatedStyle(() => {
|
|
69
104
|
const isBeingDragged = isDragging.value && activeSquare.value === square;
|
|
@@ -88,17 +123,20 @@ export const BoardPieceView = React.memo(
|
|
|
88
123
|
},
|
|
89
124
|
animatedStyle,
|
|
90
125
|
]}
|
|
126
|
+
// Fade out when this piece is captured (removed from the piece list)
|
|
127
|
+
exiting={FadeOut.duration(CAPTURE_FADE_DURATION)}
|
|
91
128
|
>
|
|
92
129
|
{children}
|
|
93
130
|
</Animated.View>
|
|
94
131
|
);
|
|
95
132
|
},
|
|
96
|
-
// Custom comparator: only re-render when position or
|
|
133
|
+
// Custom comparator: only re-render when position, square, or animation config changes
|
|
97
134
|
(prev, next) =>
|
|
98
135
|
prev.targetX === next.targetX &&
|
|
99
136
|
prev.targetY === next.targetY &&
|
|
100
137
|
prev.square === next.square &&
|
|
101
138
|
prev.squareSize === next.squareSize &&
|
|
102
139
|
prev.moveDuration === next.moveDuration &&
|
|
140
|
+
prev.animationConfig === next.animationConfig &&
|
|
103
141
|
prev.children === next.children,
|
|
104
142
|
);
|