ankigammon 1.0.0__py3-none-any.whl
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.
Potentially problematic release.
This version of ankigammon might be problematic. Click here for more details.
- ankigammon/__init__.py +7 -0
- ankigammon/__main__.py +6 -0
- ankigammon/analysis/__init__.py +13 -0
- ankigammon/analysis/score_matrix.py +373 -0
- ankigammon/anki/__init__.py +6 -0
- ankigammon/anki/ankiconnect.py +224 -0
- ankigammon/anki/apkg_exporter.py +123 -0
- ankigammon/anki/card_generator.py +1307 -0
- ankigammon/anki/card_styles.py +1034 -0
- ankigammon/gui/__init__.py +8 -0
- ankigammon/gui/app.py +209 -0
- ankigammon/gui/dialogs/__init__.py +10 -0
- ankigammon/gui/dialogs/export_dialog.py +597 -0
- ankigammon/gui/dialogs/import_options_dialog.py +163 -0
- ankigammon/gui/dialogs/input_dialog.py +776 -0
- ankigammon/gui/dialogs/note_dialog.py +93 -0
- ankigammon/gui/dialogs/settings_dialog.py +384 -0
- ankigammon/gui/format_detector.py +292 -0
- ankigammon/gui/main_window.py +1071 -0
- ankigammon/gui/resources/icon.icns +0 -0
- ankigammon/gui/resources/icon.ico +0 -0
- ankigammon/gui/resources/icon.png +0 -0
- ankigammon/gui/resources/style.qss +394 -0
- ankigammon/gui/resources.py +26 -0
- ankigammon/gui/widgets/__init__.py +8 -0
- ankigammon/gui/widgets/position_list.py +193 -0
- ankigammon/gui/widgets/smart_input.py +268 -0
- ankigammon/models.py +322 -0
- ankigammon/parsers/__init__.py +7 -0
- ankigammon/parsers/gnubg_parser.py +454 -0
- ankigammon/parsers/xg_binary_parser.py +870 -0
- ankigammon/parsers/xg_text_parser.py +729 -0
- ankigammon/renderer/__init__.py +5 -0
- ankigammon/renderer/animation_controller.py +406 -0
- ankigammon/renderer/animation_helper.py +221 -0
- ankigammon/renderer/color_schemes.py +145 -0
- ankigammon/renderer/svg_board_renderer.py +824 -0
- ankigammon/settings.py +239 -0
- ankigammon/thirdparty/__init__.py +7 -0
- ankigammon/thirdparty/xgdatatools/__init__.py +17 -0
- ankigammon/thirdparty/xgdatatools/xgimport.py +160 -0
- ankigammon/thirdparty/xgdatatools/xgstruct.py +1032 -0
- ankigammon/thirdparty/xgdatatools/xgutils.py +118 -0
- ankigammon/thirdparty/xgdatatools/xgzarc.py +260 -0
- ankigammon/utils/__init__.py +13 -0
- ankigammon/utils/gnubg_analyzer.py +431 -0
- ankigammon/utils/gnuid.py +622 -0
- ankigammon/utils/move_parser.py +239 -0
- ankigammon/utils/ogid.py +335 -0
- ankigammon/utils/xgid.py +419 -0
- ankigammon-1.0.0.dist-info/METADATA +370 -0
- ankigammon-1.0.0.dist-info/RECORD +56 -0
- ankigammon-1.0.0.dist-info/WHEEL +5 -0
- ankigammon-1.0.0.dist-info/entry_points.txt +2 -0
- ankigammon-1.0.0.dist-info/licenses/LICENSE +21 -0
- ankigammon-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Animation controller for backgammon checker movements.
|
|
3
|
+
|
|
4
|
+
Generates JavaScript code for animating checker movements using various strategies:
|
|
5
|
+
- Cross-fade: Simple opacity transition between positions
|
|
6
|
+
- Arc movement: GSAP-based arc animation of individual checkers
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from typing import List, Tuple, Dict, Optional
|
|
11
|
+
from ankigammon.models import Position, Move, Player
|
|
12
|
+
from ankigammon.renderer.animation_helper import AnimationHelper
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AnimationController:
|
|
16
|
+
"""
|
|
17
|
+
Orchestrates checker movement animations for Anki cards.
|
|
18
|
+
|
|
19
|
+
This controller generates JavaScript code that animates transitions
|
|
20
|
+
between backgammon positions using either simple cross-fade or
|
|
21
|
+
sophisticated GSAP-based arc movements.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
board_width: int = 900,
|
|
27
|
+
board_height: int = 600,
|
|
28
|
+
point_height_ratio: float = 0.45
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
Initialize the animation controller.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
board_width: SVG viewBox width (must match renderer)
|
|
35
|
+
board_height: SVG viewBox height (must match renderer)
|
|
36
|
+
point_height_ratio: Height of points as ratio of board height
|
|
37
|
+
"""
|
|
38
|
+
self.board_width = board_width
|
|
39
|
+
self.board_height = board_height
|
|
40
|
+
self.point_height_ratio = point_height_ratio
|
|
41
|
+
|
|
42
|
+
# Calculate board dimensions (match SVGBoardRenderer calculations)
|
|
43
|
+
self.margin = 20
|
|
44
|
+
self.cube_area_width = 70
|
|
45
|
+
self.bearoff_area_width = 100
|
|
46
|
+
|
|
47
|
+
self.playing_width = (
|
|
48
|
+
self.board_width - 2 * self.margin -
|
|
49
|
+
self.cube_area_width - self.bearoff_area_width
|
|
50
|
+
)
|
|
51
|
+
self.board_height_inner = self.board_height - 2 * self.margin
|
|
52
|
+
|
|
53
|
+
self.bar_width = self.playing_width * 0.08
|
|
54
|
+
self.half_width = (self.playing_width - self.bar_width) / 2
|
|
55
|
+
self.point_width = self.half_width / 6
|
|
56
|
+
self.point_height = self.board_height_inner * point_height_ratio
|
|
57
|
+
|
|
58
|
+
self.checker_radius = min(self.point_width * 0.45, 25)
|
|
59
|
+
|
|
60
|
+
# Board origin (matches SVGBoardRenderer)
|
|
61
|
+
self.board_x = self.margin + self.cube_area_width
|
|
62
|
+
self.board_y = self.margin
|
|
63
|
+
|
|
64
|
+
def get_point_coordinates(self, point_num: int, checker_index: int = 0, player: Optional[Player] = None) -> Tuple[float, float]:
|
|
65
|
+
"""
|
|
66
|
+
Calculate SVG coordinates for a checker on a specific point.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
point_num: Point number (0=X bar, 1-24=board points, 25=O bar, -1=bear off)
|
|
70
|
+
checker_index: Index of checker in stack (0=bottom, 1=next up, etc.)
|
|
71
|
+
player: Player bearing off (required when point_num == -1)
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
(x, y) coordinates in SVG space
|
|
75
|
+
"""
|
|
76
|
+
# Handle bar positions
|
|
77
|
+
if point_num == 0: # X's bar (top)
|
|
78
|
+
bar_x = self.board_x + self.half_width
|
|
79
|
+
bar_center_x = bar_x + self.bar_width / 2
|
|
80
|
+
y = self.board_y + self.point_height + checker_index * (self.checker_radius * 2 + 2)
|
|
81
|
+
return (bar_center_x, y)
|
|
82
|
+
|
|
83
|
+
if point_num == 25: # O's bar (bottom)
|
|
84
|
+
bar_x = self.board_x + self.half_width
|
|
85
|
+
bar_center_x = bar_x + self.bar_width / 2
|
|
86
|
+
y = (self.board_y + self.board_height_inner - self.point_height -
|
|
87
|
+
checker_index * (self.checker_radius * 2 + 2))
|
|
88
|
+
return (bar_center_x, y)
|
|
89
|
+
|
|
90
|
+
# Handle bear-off (special marker -1)
|
|
91
|
+
if point_num == -1:
|
|
92
|
+
# Return off-board position (will be animated to bear-off tray)
|
|
93
|
+
# X-coordinate: center of bearoff area
|
|
94
|
+
bearoff_x = self.board_x + self.playing_width + 50
|
|
95
|
+
|
|
96
|
+
# Y-coordinate: bottom of appropriate tray where thin rectangles are drawn
|
|
97
|
+
# (matches SVGBoardRenderer._draw_bearoff checker placement)
|
|
98
|
+
checker_height = 50 # Must match SVGBoardRenderer
|
|
99
|
+
|
|
100
|
+
if player == Player.X:
|
|
101
|
+
# X uses top tray (not flipped)
|
|
102
|
+
tray_bottom = self.board_y + self.board_height_inner / 2 - 10
|
|
103
|
+
bearoff_y = tray_bottom - 10 - checker_height
|
|
104
|
+
else:
|
|
105
|
+
# O uses bottom tray (not flipped)
|
|
106
|
+
tray_bottom = self.board_y + self.board_height_inner - 10
|
|
107
|
+
bearoff_y = tray_bottom - 10 - checker_height
|
|
108
|
+
|
|
109
|
+
return (bearoff_x, bearoff_y)
|
|
110
|
+
|
|
111
|
+
# Handle regular board points (1-24)
|
|
112
|
+
if point_num < 1 or point_num > 24:
|
|
113
|
+
raise ValueError(f"Invalid point number: {point_num}")
|
|
114
|
+
|
|
115
|
+
x, y_base, is_top = self._get_point_position(point_num)
|
|
116
|
+
|
|
117
|
+
# Calculate checker position in stack
|
|
118
|
+
if is_top:
|
|
119
|
+
y = y_base + self.checker_radius + checker_index * (self.checker_radius * 2 + 2)
|
|
120
|
+
else:
|
|
121
|
+
y = y_base - self.checker_radius - checker_index * (self.checker_radius * 2 + 2)
|
|
122
|
+
|
|
123
|
+
cx = x + self.point_width / 2
|
|
124
|
+
|
|
125
|
+
return (cx, y)
|
|
126
|
+
|
|
127
|
+
def _get_point_position(self, point_idx: int) -> Tuple[float, float, bool]:
|
|
128
|
+
"""
|
|
129
|
+
Get the x, y position and orientation of a point.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
(x, y_base, is_top) where is_top indicates if point extends from top
|
|
133
|
+
"""
|
|
134
|
+
if point_idx <= 6:
|
|
135
|
+
x = self.board_x + self.half_width + self.bar_width + (6 - point_idx) * self.point_width
|
|
136
|
+
y_base = self.board_y + self.board_height_inner
|
|
137
|
+
is_top = False
|
|
138
|
+
elif point_idx <= 12:
|
|
139
|
+
x = self.board_x + (12 - point_idx) * self.point_width
|
|
140
|
+
y_base = self.board_y + self.board_height_inner
|
|
141
|
+
is_top = False
|
|
142
|
+
elif point_idx <= 18:
|
|
143
|
+
x = self.board_x + (point_idx - 13) * self.point_width
|
|
144
|
+
y_base = self.board_y
|
|
145
|
+
is_top = True
|
|
146
|
+
else:
|
|
147
|
+
x = self.board_x + self.half_width + self.bar_width + (point_idx - 19) * self.point_width
|
|
148
|
+
y_base = self.board_y
|
|
149
|
+
is_top = True
|
|
150
|
+
|
|
151
|
+
return x, y_base, is_top
|
|
152
|
+
|
|
153
|
+
def generate_animation_script(
|
|
154
|
+
self,
|
|
155
|
+
from_position: Position,
|
|
156
|
+
to_position: Position,
|
|
157
|
+
move_notation: str,
|
|
158
|
+
on_roll: Player,
|
|
159
|
+
animation_style: str = "arc",
|
|
160
|
+
duration: float = 0.8,
|
|
161
|
+
svg_id_original: str = "original-svg",
|
|
162
|
+
svg_id_result: str = "result-svg"
|
|
163
|
+
) -> str:
|
|
164
|
+
"""
|
|
165
|
+
Generate JavaScript for animating a move.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
from_position: Starting position
|
|
169
|
+
to_position: Ending position
|
|
170
|
+
move_notation: Move notation (e.g., "24/23 24/23")
|
|
171
|
+
on_roll: Player making the move
|
|
172
|
+
animation_style: 'fade', 'arc', or 'none'
|
|
173
|
+
duration: Animation duration in seconds
|
|
174
|
+
svg_id_original: ID of original position SVG container
|
|
175
|
+
svg_id_result: ID of result position SVG container
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
JavaScript code as string
|
|
179
|
+
"""
|
|
180
|
+
if animation_style == "none":
|
|
181
|
+
return ""
|
|
182
|
+
|
|
183
|
+
if animation_style == "fade":
|
|
184
|
+
return self._generate_fade_animation(
|
|
185
|
+
svg_id_original, svg_id_result, duration
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if animation_style == "arc":
|
|
189
|
+
return self._generate_arc_animation(
|
|
190
|
+
from_position, to_position, move_notation, on_roll,
|
|
191
|
+
duration, svg_id_original, svg_id_result
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return ""
|
|
195
|
+
|
|
196
|
+
def _generate_fade_animation(
|
|
197
|
+
self,
|
|
198
|
+
svg_id_original: str,
|
|
199
|
+
svg_id_result: str,
|
|
200
|
+
duration: float
|
|
201
|
+
) -> str:
|
|
202
|
+
"""Generate simple cross-fade animation JavaScript."""
|
|
203
|
+
return f"""
|
|
204
|
+
// Cross-fade animation
|
|
205
|
+
function animatePositionTransition() {{
|
|
206
|
+
const originalSvg = document.getElementById('{svg_id_original}');
|
|
207
|
+
const resultSvg = document.getElementById('{svg_id_result}');
|
|
208
|
+
|
|
209
|
+
if (!originalSvg || !resultSvg) return;
|
|
210
|
+
|
|
211
|
+
// Set up initial state
|
|
212
|
+
originalSvg.style.opacity = '1';
|
|
213
|
+
originalSvg.style.transition = 'opacity {duration}s ease-in-out';
|
|
214
|
+
resultSvg.style.opacity = '0';
|
|
215
|
+
resultSvg.style.transition = 'opacity {duration}s ease-in-out';
|
|
216
|
+
resultSvg.style.display = 'block';
|
|
217
|
+
|
|
218
|
+
// Trigger fade
|
|
219
|
+
setTimeout(() => {{
|
|
220
|
+
originalSvg.style.opacity = '0';
|
|
221
|
+
resultSvg.style.opacity = '1';
|
|
222
|
+
}}, 50);
|
|
223
|
+
|
|
224
|
+
// Clean up
|
|
225
|
+
setTimeout(() => {{
|
|
226
|
+
originalSvg.style.display = 'none';
|
|
227
|
+
}}, {duration * 1000 + 100});
|
|
228
|
+
}}
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
def _generate_arc_animation(
|
|
232
|
+
self,
|
|
233
|
+
from_position: Position,
|
|
234
|
+
to_position: Position,
|
|
235
|
+
move_notation: str,
|
|
236
|
+
on_roll: Player,
|
|
237
|
+
duration: float,
|
|
238
|
+
svg_id_original: str,
|
|
239
|
+
svg_id_result: str
|
|
240
|
+
) -> str:
|
|
241
|
+
"""Generate GSAP-based arc animation JavaScript."""
|
|
242
|
+
# Parse the move to get checker movements
|
|
243
|
+
moves = AnimationHelper.parse_move_notation(move_notation, on_roll)
|
|
244
|
+
|
|
245
|
+
if not moves:
|
|
246
|
+
# Fall back to fade if no moves can be parsed
|
|
247
|
+
return self._generate_fade_animation(svg_id_original, svg_id_result, duration)
|
|
248
|
+
|
|
249
|
+
# Calculate movements with coordinates
|
|
250
|
+
movements = []
|
|
251
|
+
for from_point, to_point in moves:
|
|
252
|
+
# Get checker stack index (always move top checker)
|
|
253
|
+
checker_index = 0
|
|
254
|
+
|
|
255
|
+
# Get start and end coordinates
|
|
256
|
+
start_x, start_y = self.get_point_coordinates(from_point, checker_index)
|
|
257
|
+
# For bearoff moves (to_point == -1), pass the player information
|
|
258
|
+
end_x, end_y = self.get_point_coordinates(to_point, checker_index, player=on_roll if to_point == -1 else None)
|
|
259
|
+
|
|
260
|
+
# Calculate control point for arc (parabolic curve)
|
|
261
|
+
mid_x = (start_x + end_x) / 2
|
|
262
|
+
mid_y = min(start_y, end_y) - 80 # Arc upward
|
|
263
|
+
|
|
264
|
+
movements.append({
|
|
265
|
+
'from_point': from_point,
|
|
266
|
+
'to_point': to_point,
|
|
267
|
+
'start': [start_x, start_y],
|
|
268
|
+
'control': [mid_x, mid_y],
|
|
269
|
+
'end': [end_x, end_y],
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
movements_json = json.dumps(movements)
|
|
273
|
+
stagger = 0.05 # Delay between multiple checker animations
|
|
274
|
+
|
|
275
|
+
return f"""
|
|
276
|
+
// GSAP arc animation
|
|
277
|
+
if (typeof gsap !== 'undefined') {{
|
|
278
|
+
gsap.registerPlugin(MotionPathPlugin);
|
|
279
|
+
|
|
280
|
+
const movements = {movements_json};
|
|
281
|
+
const duration = {duration};
|
|
282
|
+
const stagger = {stagger};
|
|
283
|
+
|
|
284
|
+
function animatePositionTransition() {{
|
|
285
|
+
const originalSvg = document.getElementById('{svg_id_original}');
|
|
286
|
+
const resultSvg = document.getElementById('{svg_id_result}');
|
|
287
|
+
|
|
288
|
+
if (!originalSvg || !resultSvg) return;
|
|
289
|
+
|
|
290
|
+
// Clone the original SVG for animation
|
|
291
|
+
const animSvg = originalSvg.cloneNode(true);
|
|
292
|
+
animSvg.id = 'animation-svg';
|
|
293
|
+
animSvg.style.position = 'absolute';
|
|
294
|
+
animSvg.style.top = '0';
|
|
295
|
+
animSvg.style.left = '0';
|
|
296
|
+
animSvg.style.width = '100%';
|
|
297
|
+
animSvg.style.height = '100%';
|
|
298
|
+
|
|
299
|
+
originalSvg.parentNode.style.position = 'relative';
|
|
300
|
+
originalSvg.parentNode.appendChild(animSvg);
|
|
301
|
+
|
|
302
|
+
// Hide result initially
|
|
303
|
+
resultSvg.style.opacity = '0';
|
|
304
|
+
resultSvg.style.display = 'block';
|
|
305
|
+
|
|
306
|
+
// Animate each checker movement
|
|
307
|
+
const timeline = gsap.timeline({{
|
|
308
|
+
onComplete: function() {{
|
|
309
|
+
// Show final position
|
|
310
|
+
resultSvg.style.transition = 'opacity 0.2s';
|
|
311
|
+
resultSvg.style.opacity = '1';
|
|
312
|
+
|
|
313
|
+
// Remove animation SVG
|
|
314
|
+
setTimeout(() => {{
|
|
315
|
+
if (animSvg.parentNode) {{
|
|
316
|
+
animSvg.parentNode.removeChild(animSvg);
|
|
317
|
+
}}
|
|
318
|
+
originalSvg.style.display = 'none';
|
|
319
|
+
}}, 200);
|
|
320
|
+
}}
|
|
321
|
+
}});
|
|
322
|
+
|
|
323
|
+
movements.forEach((movement, index) => {{
|
|
324
|
+
// Find checkers at from_point in the animation SVG
|
|
325
|
+
const checkers = animSvg.querySelectorAll('.checker[data-point="' + movement.from_point + '"]');
|
|
326
|
+
|
|
327
|
+
if (checkers.length > 0) {{
|
|
328
|
+
const checker = checkers[0]; // Animate first (top) checker
|
|
329
|
+
|
|
330
|
+
// Create path for arc movement
|
|
331
|
+
const path = [
|
|
332
|
+
{{ x: movement.start[0], y: movement.start[1] }},
|
|
333
|
+
{{ x: movement.control[0], y: movement.control[1] }},
|
|
334
|
+
{{ x: movement.end[0], y: movement.end[1] }}
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
timeline.to(checker, {{
|
|
338
|
+
duration: duration,
|
|
339
|
+
motionPath: {{
|
|
340
|
+
path: path,
|
|
341
|
+
type: 'soft',
|
|
342
|
+
autoRotate: false
|
|
343
|
+
}},
|
|
344
|
+
ease: 'power2.inOut'
|
|
345
|
+
}}, index * stagger);
|
|
346
|
+
}}
|
|
347
|
+
}});
|
|
348
|
+
}}
|
|
349
|
+
}} else {{
|
|
350
|
+
// GSAP not available, fall back to fade
|
|
351
|
+
function animatePositionTransition() {{
|
|
352
|
+
const originalSvg = document.getElementById('{svg_id_original}');
|
|
353
|
+
const resultSvg = document.getElementById('{svg_id_result}');
|
|
354
|
+
|
|
355
|
+
if (!originalSvg || !resultSvg) return;
|
|
356
|
+
|
|
357
|
+
originalSvg.style.transition = 'opacity {duration}s';
|
|
358
|
+
resultSvg.style.transition = 'opacity {duration}s';
|
|
359
|
+
resultSvg.style.display = 'block';
|
|
360
|
+
resultSvg.style.opacity = '0';
|
|
361
|
+
|
|
362
|
+
setTimeout(() => {{
|
|
363
|
+
originalSvg.style.opacity = '0';
|
|
364
|
+
resultSvg.style.opacity = '1';
|
|
365
|
+
}}, 50);
|
|
366
|
+
|
|
367
|
+
setTimeout(() => {{
|
|
368
|
+
originalSvg.style.display = 'none';
|
|
369
|
+
}}, {duration * 1000 + 100});
|
|
370
|
+
}}
|
|
371
|
+
}}
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
def get_gsap_cdn_urls(self) -> List[str]:
|
|
375
|
+
"""
|
|
376
|
+
Get GSAP CDN URLs for inclusion in card HTML.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
List of CDN URLs
|
|
380
|
+
"""
|
|
381
|
+
return [
|
|
382
|
+
"https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js",
|
|
383
|
+
"https://cdn.jsdelivr.net/npm/gsap@3/dist/MotionPathPlugin.min.js"
|
|
384
|
+
]
|
|
385
|
+
|
|
386
|
+
def generate_trigger_button_html(
|
|
387
|
+
self,
|
|
388
|
+
button_text: str = "▶ Animate Move",
|
|
389
|
+
button_class: str = "animate-btn"
|
|
390
|
+
) -> str:
|
|
391
|
+
"""
|
|
392
|
+
Generate HTML for animation trigger button.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
button_text: Text to display on button
|
|
396
|
+
button_class: CSS class for button
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
HTML string
|
|
400
|
+
"""
|
|
401
|
+
return f"""
|
|
402
|
+
<div class="animation-controls" style="text-align: center; margin: 10px 0;">
|
|
403
|
+
<button onclick="if(typeof animatePositionTransition === 'function') animatePositionTransition();"
|
|
404
|
+
class="{button_class}">{button_text}</button>
|
|
405
|
+
</div>
|
|
406
|
+
"""
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Helper for generating GSAP-based checker movement animations."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import List, Tuple, Dict, Optional
|
|
5
|
+
from ankigammon.models import Position, Move, Player
|
|
6
|
+
from ankigammon.utils.move_parser import MoveParser
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AnimationHelper:
|
|
10
|
+
"""Generates animation data and JavaScript for checker movements."""
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def parse_move_notation(notation: str, on_roll: Player) -> List[Tuple[int, int]]:
|
|
14
|
+
"""
|
|
15
|
+
Parse backgammon move notation into (from_point, to_point) pairs.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
notation: Move notation (e.g., "24/23 24/23" or "bar/24")
|
|
19
|
+
on_roll: Player making the move
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
List of (from_point, to_point) tuples (0-25, where 0=X bar, 25=O bar)
|
|
23
|
+
"""
|
|
24
|
+
moves = []
|
|
25
|
+
|
|
26
|
+
if not notation or notation == "Can't move":
|
|
27
|
+
return moves
|
|
28
|
+
|
|
29
|
+
# Split by spaces to get individual checker moves
|
|
30
|
+
parts = notation.strip().split()
|
|
31
|
+
|
|
32
|
+
for part in parts:
|
|
33
|
+
if '/' not in part:
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
# Check for repetition notation like "6/4(4)" meaning "move 4 checkers from 6 to 4"
|
|
37
|
+
repetition_count = 1
|
|
38
|
+
repetition_match = re.search(r'\((\d+)\)$', part)
|
|
39
|
+
if repetition_match:
|
|
40
|
+
repetition_count = int(repetition_match.group(1))
|
|
41
|
+
# Remove the repetition notation from the part
|
|
42
|
+
part = re.sub(r'\(\d+\)$', '', part)
|
|
43
|
+
|
|
44
|
+
# Handle compound notation like "6/5*/3" or "24/23/22"
|
|
45
|
+
# Split by '/' and remove asterisks to get all points in the sequence
|
|
46
|
+
segments = part.split('/')
|
|
47
|
+
|
|
48
|
+
# Remove asterisks from each segment
|
|
49
|
+
segments = [seg.rstrip('*') for seg in segments]
|
|
50
|
+
|
|
51
|
+
# If there are more than 2 segments, this is compound notation
|
|
52
|
+
# Convert it to consecutive moves: "6/5/3" -> [(6,5), (5,3)]
|
|
53
|
+
if len(segments) > 2:
|
|
54
|
+
for i in range(len(segments) - 1):
|
|
55
|
+
from_str = segments[i]
|
|
56
|
+
to_str = segments[i + 1]
|
|
57
|
+
|
|
58
|
+
# Parse "from" point
|
|
59
|
+
if from_str.lower() == 'bar':
|
|
60
|
+
from_point = 0 if on_roll == Player.X else 25
|
|
61
|
+
elif from_str.lower() == 'off':
|
|
62
|
+
continue
|
|
63
|
+
else:
|
|
64
|
+
try:
|
|
65
|
+
from_point = int(from_str)
|
|
66
|
+
except ValueError:
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
# Parse "to" point
|
|
70
|
+
if to_str.lower() == 'bar':
|
|
71
|
+
to_point = 25 if on_roll == Player.X else 0
|
|
72
|
+
elif to_str.lower() == 'off':
|
|
73
|
+
to_point = -1
|
|
74
|
+
else:
|
|
75
|
+
try:
|
|
76
|
+
to_point = int(to_str)
|
|
77
|
+
except ValueError:
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
# Add the move repetition_count times
|
|
81
|
+
for _ in range(repetition_count):
|
|
82
|
+
moves.append((from_point, to_point))
|
|
83
|
+
else:
|
|
84
|
+
# Simple notation like "6/5" or "bar/22"
|
|
85
|
+
from_str = segments[0]
|
|
86
|
+
to_str = segments[1] if len(segments) > 1 else ''
|
|
87
|
+
|
|
88
|
+
if not to_str:
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
# Parse "from" point
|
|
92
|
+
if from_str.lower() == 'bar':
|
|
93
|
+
# Bar position depends on player
|
|
94
|
+
from_point = 0 if on_roll == Player.X else 25
|
|
95
|
+
elif from_str.lower() == 'off':
|
|
96
|
+
# Bearing off - skip (no visual animation needed)
|
|
97
|
+
continue
|
|
98
|
+
else:
|
|
99
|
+
try:
|
|
100
|
+
from_point = int(from_str)
|
|
101
|
+
except ValueError:
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
# Parse "to" point
|
|
105
|
+
if to_str.lower() == 'bar':
|
|
106
|
+
# Opponent bar
|
|
107
|
+
to_point = 25 if on_roll == Player.X else 0
|
|
108
|
+
elif to_str.lower() == 'off':
|
|
109
|
+
# Bearing off - use special marker (-1)
|
|
110
|
+
to_point = -1
|
|
111
|
+
else:
|
|
112
|
+
try:
|
|
113
|
+
to_point = int(to_str)
|
|
114
|
+
except ValueError:
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
# Add the move repetition_count times (handles notation like "6/4(4)")
|
|
118
|
+
for _ in range(repetition_count):
|
|
119
|
+
moves.append((from_point, to_point))
|
|
120
|
+
|
|
121
|
+
return moves
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def generate_animation_javascript(
|
|
125
|
+
move_notation: str,
|
|
126
|
+
on_roll: Player,
|
|
127
|
+
duration: float = 0.8,
|
|
128
|
+
stagger: float = 0.05
|
|
129
|
+
) -> str:
|
|
130
|
+
"""
|
|
131
|
+
Generate JavaScript code for animating a backgammon move using GSAP.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
move_notation: Move notation string
|
|
135
|
+
on_roll: Player making the move
|
|
136
|
+
duration: Animation duration in seconds
|
|
137
|
+
stagger: Stagger time between checkers in seconds
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
JavaScript code string
|
|
141
|
+
"""
|
|
142
|
+
moves = AnimationHelper.parse_move_notation(move_notation, on_roll)
|
|
143
|
+
|
|
144
|
+
if not moves:
|
|
145
|
+
return ""
|
|
146
|
+
|
|
147
|
+
# Generate GSAP animation code
|
|
148
|
+
js_code = f"""
|
|
149
|
+
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
|
|
150
|
+
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/MotionPathPlugin.min.js"></script>
|
|
151
|
+
<script>
|
|
152
|
+
if (typeof gsap !== 'undefined') {{
|
|
153
|
+
gsap.registerPlugin(MotionPathPlugin);
|
|
154
|
+
|
|
155
|
+
// Animation data
|
|
156
|
+
const moves = {moves};
|
|
157
|
+
const duration = {duration};
|
|
158
|
+
const stagger = {stagger};
|
|
159
|
+
|
|
160
|
+
// Animate checkers
|
|
161
|
+
function animateMove() {{
|
|
162
|
+
moves.forEach((move, index) => {{
|
|
163
|
+
const [fromPoint, toPoint] = move;
|
|
164
|
+
|
|
165
|
+
// Find checker at fromPoint
|
|
166
|
+
const checkers = document.querySelectorAll('.checker[data-point="' + fromPoint + '"]');
|
|
167
|
+
|
|
168
|
+
if (checkers.length > 0) {{
|
|
169
|
+
const checker = checkers[0]; // Animate first checker at point
|
|
170
|
+
|
|
171
|
+
// Calculate target position (simplified - would need actual point coordinates)
|
|
172
|
+
// This is a placeholder - actual implementation would calculate SVG coordinates
|
|
173
|
+
|
|
174
|
+
gsap.to(checker, {{
|
|
175
|
+
duration: duration,
|
|
176
|
+
delay: index * stagger,
|
|
177
|
+
attr: {{
|
|
178
|
+
cx: '+=100', // Placeholder translation
|
|
179
|
+
cy: '+=0'
|
|
180
|
+
}},
|
|
181
|
+
ease: 'power2.inOut'
|
|
182
|
+
}});
|
|
183
|
+
}}
|
|
184
|
+
}});
|
|
185
|
+
}}
|
|
186
|
+
|
|
187
|
+
// Optionally auto-play animation or add button to trigger
|
|
188
|
+
// animateMove();
|
|
189
|
+
}}
|
|
190
|
+
</script>
|
|
191
|
+
"""
|
|
192
|
+
return js_code
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def get_gsap_cdn_scripts() -> str:
|
|
196
|
+
"""
|
|
197
|
+
Get GSAP CDN script tags.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
HTML script tags for GSAP and MotionPathPlugin
|
|
201
|
+
"""
|
|
202
|
+
return """
|
|
203
|
+
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
|
|
204
|
+
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/MotionPathPlugin.min.js"></script>
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
@staticmethod
|
|
208
|
+
def generate_animation_button() -> str:
|
|
209
|
+
"""
|
|
210
|
+
Generate HTML for an animation trigger button.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
HTML button element
|
|
214
|
+
"""
|
|
215
|
+
return """
|
|
216
|
+
<div style="text-align: center; margin: 10px 0;">
|
|
217
|
+
<button onclick="animateCheckers()" class="toggle-btn">
|
|
218
|
+
▶ Animate Move
|
|
219
|
+
</button>
|
|
220
|
+
</div>
|
|
221
|
+
"""
|