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.

Files changed (56) hide show
  1. ankigammon/__init__.py +7 -0
  2. ankigammon/__main__.py +6 -0
  3. ankigammon/analysis/__init__.py +13 -0
  4. ankigammon/analysis/score_matrix.py +373 -0
  5. ankigammon/anki/__init__.py +6 -0
  6. ankigammon/anki/ankiconnect.py +224 -0
  7. ankigammon/anki/apkg_exporter.py +123 -0
  8. ankigammon/anki/card_generator.py +1307 -0
  9. ankigammon/anki/card_styles.py +1034 -0
  10. ankigammon/gui/__init__.py +8 -0
  11. ankigammon/gui/app.py +209 -0
  12. ankigammon/gui/dialogs/__init__.py +10 -0
  13. ankigammon/gui/dialogs/export_dialog.py +597 -0
  14. ankigammon/gui/dialogs/import_options_dialog.py +163 -0
  15. ankigammon/gui/dialogs/input_dialog.py +776 -0
  16. ankigammon/gui/dialogs/note_dialog.py +93 -0
  17. ankigammon/gui/dialogs/settings_dialog.py +384 -0
  18. ankigammon/gui/format_detector.py +292 -0
  19. ankigammon/gui/main_window.py +1071 -0
  20. ankigammon/gui/resources/icon.icns +0 -0
  21. ankigammon/gui/resources/icon.ico +0 -0
  22. ankigammon/gui/resources/icon.png +0 -0
  23. ankigammon/gui/resources/style.qss +394 -0
  24. ankigammon/gui/resources.py +26 -0
  25. ankigammon/gui/widgets/__init__.py +8 -0
  26. ankigammon/gui/widgets/position_list.py +193 -0
  27. ankigammon/gui/widgets/smart_input.py +268 -0
  28. ankigammon/models.py +322 -0
  29. ankigammon/parsers/__init__.py +7 -0
  30. ankigammon/parsers/gnubg_parser.py +454 -0
  31. ankigammon/parsers/xg_binary_parser.py +870 -0
  32. ankigammon/parsers/xg_text_parser.py +729 -0
  33. ankigammon/renderer/__init__.py +5 -0
  34. ankigammon/renderer/animation_controller.py +406 -0
  35. ankigammon/renderer/animation_helper.py +221 -0
  36. ankigammon/renderer/color_schemes.py +145 -0
  37. ankigammon/renderer/svg_board_renderer.py +824 -0
  38. ankigammon/settings.py +239 -0
  39. ankigammon/thirdparty/__init__.py +7 -0
  40. ankigammon/thirdparty/xgdatatools/__init__.py +17 -0
  41. ankigammon/thirdparty/xgdatatools/xgimport.py +160 -0
  42. ankigammon/thirdparty/xgdatatools/xgstruct.py +1032 -0
  43. ankigammon/thirdparty/xgdatatools/xgutils.py +118 -0
  44. ankigammon/thirdparty/xgdatatools/xgzarc.py +260 -0
  45. ankigammon/utils/__init__.py +13 -0
  46. ankigammon/utils/gnubg_analyzer.py +431 -0
  47. ankigammon/utils/gnuid.py +622 -0
  48. ankigammon/utils/move_parser.py +239 -0
  49. ankigammon/utils/ogid.py +335 -0
  50. ankigammon/utils/xgid.py +419 -0
  51. ankigammon-1.0.0.dist-info/METADATA +370 -0
  52. ankigammon-1.0.0.dist-info/RECORD +56 -0
  53. ankigammon-1.0.0.dist-info/WHEEL +5 -0
  54. ankigammon-1.0.0.dist-info/entry_points.txt +2 -0
  55. ankigammon-1.0.0.dist-info/licenses/LICENSE +21 -0
  56. ankigammon-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,5 @@
1
+ """Board rendering utilities."""
2
+
3
+ from ankigammon.renderer.svg_board_renderer import SVGBoardRenderer
4
+
5
+ __all__ = ["SVGBoardRenderer"]
@@ -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
+ """