@alepot55/chessboardjs 2.2.1 → 2.3.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.
Files changed (74) hide show
  1. package/README.md +125 -401
  2. package/assets/themes/alepot/theme.json +42 -0
  3. package/assets/themes/default/theme.json +42 -0
  4. package/dist/chessboard.cjs.js +11785 -0
  5. package/dist/chessboard.css +243 -0
  6. package/dist/chessboard.esm.js +11716 -0
  7. package/dist/chessboard.iife.js +11791 -0
  8. package/dist/chessboard.umd.js +11791 -0
  9. package/package.json +33 -3
  10. package/{chessboard.move.js → src/components/Move.js} +3 -3
  11. package/src/components/Piece.js +771 -0
  12. package/{chessboard.square.js → src/components/Square.js} +61 -8
  13. package/src/constants/index.js +15 -0
  14. package/src/constants/positions.js +62 -0
  15. package/src/core/Chessboard.js +2346 -0
  16. package/src/core/ChessboardConfig.js +707 -0
  17. package/src/core/ChessboardFactory.js +385 -0
  18. package/src/core/index.js +141 -0
  19. package/src/errors/ChessboardError.js +133 -0
  20. package/src/errors/index.js +15 -0
  21. package/src/errors/messages.js +189 -0
  22. package/src/index.js +103 -0
  23. package/src/services/AnimationService.js +180 -0
  24. package/src/services/BoardService.js +156 -0
  25. package/src/services/CoordinateService.js +355 -0
  26. package/src/services/EventService.js +955 -0
  27. package/src/services/MoveService.js +567 -0
  28. package/src/services/PieceService.js +339 -0
  29. package/src/services/PositionService.js +237 -0
  30. package/src/services/ValidationService.js +673 -0
  31. package/src/services/index.js +14 -0
  32. package/src/styles/animations.css +46 -0
  33. package/{chessboard.css → src/styles/board.css} +30 -7
  34. package/src/styles/index.css +4 -0
  35. package/src/styles/pieces.css +70 -0
  36. package/src/utils/animations.js +37 -0
  37. package/{chess.js → src/utils/chess.js} +16 -16
  38. package/src/utils/coordinates.js +62 -0
  39. package/src/utils/cross-browser.js +150 -0
  40. package/src/utils/logger.js +422 -0
  41. package/src/utils/performance.js +311 -0
  42. package/src/utils/validation.js +458 -0
  43. package/.babelrc +0 -4
  44. package/chessboard.bundle.js +0 -3422
  45. package/chessboard.config.js +0 -147
  46. package/chessboard.js +0 -979
  47. package/chessboard.piece.js +0 -115
  48. package/jest.config.js +0 -7
  49. package/rollup.config.js +0 -11
  50. package/test/chessboard.test.js +0 -128
  51. /package/{alepot_theme → assets/themes/alepot}/bb.svg +0 -0
  52. /package/{alepot_theme → assets/themes/alepot}/bw.svg +0 -0
  53. /package/{alepot_theme → assets/themes/alepot}/kb.svg +0 -0
  54. /package/{alepot_theme → assets/themes/alepot}/kw.svg +0 -0
  55. /package/{alepot_theme → assets/themes/alepot}/nb.svg +0 -0
  56. /package/{alepot_theme → assets/themes/alepot}/nw.svg +0 -0
  57. /package/{alepot_theme → assets/themes/alepot}/pb.svg +0 -0
  58. /package/{alepot_theme → assets/themes/alepot}/pw.svg +0 -0
  59. /package/{alepot_theme → assets/themes/alepot}/qb.svg +0 -0
  60. /package/{alepot_theme → assets/themes/alepot}/qw.svg +0 -0
  61. /package/{alepot_theme → assets/themes/alepot}/rb.svg +0 -0
  62. /package/{alepot_theme → assets/themes/alepot}/rw.svg +0 -0
  63. /package/{default_pieces → assets/themes/default}/bb.svg +0 -0
  64. /package/{default_pieces → assets/themes/default}/bw.svg +0 -0
  65. /package/{default_pieces → assets/themes/default}/kb.svg +0 -0
  66. /package/{default_pieces → assets/themes/default}/kw.svg +0 -0
  67. /package/{default_pieces → assets/themes/default}/nb.svg +0 -0
  68. /package/{default_pieces → assets/themes/default}/nw.svg +0 -0
  69. /package/{default_pieces → assets/themes/default}/pb.svg +0 -0
  70. /package/{default_pieces → assets/themes/default}/pw.svg +0 -0
  71. /package/{default_pieces → assets/themes/default}/qb.svg +0 -0
  72. /package/{default_pieces → assets/themes/default}/qw.svg +0 -0
  73. /package/{default_pieces → assets/themes/default}/rb.svg +0 -0
  74. /package/{default_pieces → assets/themes/default}/rw.svg +0 -0
@@ -0,0 +1,771 @@
1
+ class Piece {
2
+ constructor(color, type, src, opacity = 1) {
3
+ this.color = color;
4
+ this.type = type;
5
+ this.id = this.getId();
6
+ this.src = src;
7
+ this.element = this.createElement(src, opacity);
8
+ console.debug(`[Piece] Constructed: ${this.id}`);
9
+ this.check();
10
+ }
11
+
12
+ getId() { return this.type + this.color }
13
+
14
+ createElement(src, opacity = 1) {
15
+ let element = document.createElement("img");
16
+ element.classList.add("piece");
17
+ element.id = this.id;
18
+ element.src = src || this.src;
19
+ element.style.opacity = opacity;
20
+
21
+ // Ensure the image loads properly
22
+ element.onerror = () => {
23
+ console.warn('Failed to load piece image:', element.src);
24
+ };
25
+
26
+ return element;
27
+ }
28
+
29
+ visible() { if (this.element) { this.element.style.opacity = 1; console.debug(`[Piece] visible: ${this.id}`); } }
30
+
31
+ invisible() { if (this.element) { this.element.style.opacity = 0; console.debug(`[Piece] invisible: ${this.id}`); } }
32
+
33
+ /**
34
+ * Updates the piece image source
35
+ * @param {string} newSrc - New image source
36
+ */
37
+ updateSrc(newSrc) {
38
+ this.src = newSrc;
39
+ if (this.element) {
40
+ this.element.src = newSrc;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Transforms the piece to a new type with smooth animation
46
+ * @param {string} newType - New piece type
47
+ * @param {string} newSrc - New image source
48
+ * @param {number} [duration=200] - Animation duration in milliseconds
49
+ * @param {Function} [callback] - Callback when transformation is complete
50
+ */
51
+ transformTo(newType, newSrc, duration = 200, callback = null) {
52
+ if (!this.element) { console.debug(`[Piece] transformTo: ${this.id} - element is null`); if (callback) callback(); return; }
53
+ const element = this.element;
54
+ const oldSrc = element.src;
55
+
56
+ // Add transformation class to disable all transitions temporarily
57
+ element.classList.add('transforming');
58
+
59
+ // Create a smooth scale animation for the transformation
60
+ const scaleDown = [
61
+ { transform: 'scale(1)', opacity: '1' },
62
+ { transform: 'scale(0.8)', opacity: '0.7' }
63
+ ];
64
+
65
+ const scaleUp = [
66
+ { transform: 'scale(0.8)', opacity: '0.7' },
67
+ { transform: 'scale(1)', opacity: '1' }
68
+ ];
69
+
70
+ const halfDuration = duration / 2;
71
+
72
+ // First animation: scale down
73
+ if (element.animate) {
74
+ const scaleDownAnimation = element.animate(scaleDown, {
75
+ duration: halfDuration,
76
+ easing: 'ease-in',
77
+ fill: 'forwards'
78
+ });
79
+
80
+ scaleDownAnimation.onfinish = () => {
81
+ if (!this.element) { console.debug(`[Piece] transformTo.scaleDown.onfinish: ${this.id} - element is null`); if (callback) callback(); return; }
82
+ // Change the piece type and source at the smallest scale
83
+ this.type = newType;
84
+ this.id = this.getId();
85
+ this.src = newSrc;
86
+ element.src = newSrc;
87
+ element.id = this.id;
88
+
89
+ // Second animation: scale back up
90
+ const scaleUpAnimation = element.animate(scaleUp, {
91
+ duration: halfDuration,
92
+ easing: 'ease-out',
93
+ fill: 'forwards'
94
+ });
95
+
96
+ scaleUpAnimation.onfinish = () => {
97
+ if (!this.element) { console.debug(`[Piece] transformTo.scaleUp.onfinish: ${this.id} - element is null`); if (callback) callback(); return; }
98
+ // Reset transform and remove transformation class
99
+ element.style.transform = '';
100
+ element.style.opacity = '';
101
+ element.classList.remove('transforming');
102
+
103
+ // Add a subtle bounce effect
104
+ element.classList.add('transform-complete');
105
+
106
+ // Remove bounce class after animation
107
+ setTimeout(() => {
108
+ if (!this.element) return;
109
+ element.classList.remove('transform-complete');
110
+ }, 400);
111
+
112
+ console.debug(`[Piece] transformTo complete: ${this.id}`);
113
+ if (callback) callback();
114
+ };
115
+ };
116
+ } else {
117
+ // Fallback for browsers without Web Animations API
118
+ element.style.transition = `transform ${halfDuration}ms ease-in, opacity ${halfDuration}ms ease-in`;
119
+ element.style.transform = 'scale(0.8)';
120
+ element.style.opacity = '0.7';
121
+
122
+ setTimeout(() => {
123
+ if (!this.element) { console.debug(`[Piece] transformTo (fallback): ${this.id} - element is null`); if (callback) callback(); return; }
124
+ // Change the piece
125
+ this.type = newType;
126
+ this.id = this.getId();
127
+ this.src = newSrc;
128
+ element.src = newSrc;
129
+ element.id = this.id;
130
+
131
+ // Scale back up
132
+ element.style.transition = `transform ${halfDuration}ms ease-out, opacity ${halfDuration}ms ease-out`;
133
+ element.style.transform = 'scale(1)';
134
+ element.style.opacity = '1';
135
+
136
+ setTimeout(() => {
137
+ if (!this.element) { console.debug(`[Piece] transformTo (fallback, cleanup): ${this.id} - element is null`); if (callback) callback(); return; }
138
+ // Clean up
139
+ element.style.transition = '';
140
+ element.style.transform = '';
141
+ element.style.opacity = '';
142
+ element.classList.remove('transforming');
143
+
144
+ // Add bounce effect
145
+ element.classList.add('transform-complete');
146
+ setTimeout(() => {
147
+ if (!this.element) return;
148
+ element.classList.remove('transform-complete');
149
+ }, 400);
150
+
151
+ console.debug(`[Piece] transformTo complete (fallback): ${this.id}`);
152
+ if (callback) callback();
153
+ }, halfDuration);
154
+ }, halfDuration);
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Animate piece appearance with configurable style
160
+ * @param {string} style - Appearance style: 'fade', 'pulse', 'pop', 'drop', 'instant'
161
+ * @param {number} duration - Animation duration in ms
162
+ * @param {Function} callback - Callback when complete
163
+ */
164
+ appearAnimate(style, duration, callback) {
165
+ if (!this.element) {
166
+ if (callback) callback();
167
+ return;
168
+ }
169
+
170
+ const element = this.element;
171
+ const cleanup = () => {
172
+ if (element) {
173
+ element.style.opacity = '1';
174
+ element.style.transform = '';
175
+ }
176
+ if (callback) callback();
177
+ };
178
+
179
+ const smoothDecel = 'cubic-bezier(0.33, 1, 0.68, 1)';
180
+ const springOvershoot = 'cubic-bezier(0.34, 1.56, 0.64, 1)';
181
+
182
+ switch (style) {
183
+ case 'instant':
184
+ element.style.opacity = '1';
185
+ cleanup();
186
+ break;
187
+
188
+ case 'fade':
189
+ if (element.animate) {
190
+ element.style.opacity = '0';
191
+ const anim = element.animate([
192
+ { opacity: 0, transform: 'scale(0.95)' },
193
+ { opacity: 1, transform: 'scale(1)' }
194
+ ], { duration, easing: smoothDecel, fill: 'forwards' });
195
+ anim.onfinish = () => {
196
+ anim.cancel();
197
+ cleanup();
198
+ };
199
+ } else {
200
+ element.style.opacity = '0';
201
+ element.style.transform = 'scale(0.95)';
202
+ setTimeout(cleanup, duration);
203
+ }
204
+ break;
205
+
206
+ case 'pulse':
207
+ if (element.animate) {
208
+ element.style.opacity = '0';
209
+ const anim = element.animate([
210
+ { opacity: 0, transform: 'scale(0.6)', offset: 0 },
211
+ { opacity: 1, transform: 'scale(1.12)', offset: 0.3 },
212
+ { opacity: 1, transform: 'scale(0.92)', offset: 0.55 },
213
+ { opacity: 1, transform: 'scale(1.06)', offset: 0.8 },
214
+ { opacity: 1, transform: 'scale(1)', offset: 1 }
215
+ ], { duration, easing: smoothDecel, fill: 'forwards' });
216
+ anim.onfinish = () => {
217
+ anim.cancel();
218
+ cleanup();
219
+ };
220
+ } else {
221
+ element.style.opacity = '0';
222
+ element.style.transform = 'scale(0.6)';
223
+ setTimeout(cleanup, duration);
224
+ }
225
+ break;
226
+
227
+ case 'pop':
228
+ if (element.animate) {
229
+ element.style.opacity = '0';
230
+ const anim = element.animate([
231
+ { opacity: 0, transform: 'scale(0)', offset: 0 },
232
+ { opacity: 1, transform: 'scale(1.15)', offset: 0.6 },
233
+ { opacity: 1, transform: 'scale(1)', offset: 1 }
234
+ ], { duration, easing: springOvershoot, fill: 'forwards' });
235
+ anim.onfinish = () => {
236
+ anim.cancel();
237
+ cleanup();
238
+ };
239
+ } else {
240
+ element.style.opacity = '0';
241
+ element.style.transform = 'scale(0)';
242
+ setTimeout(cleanup, duration);
243
+ }
244
+ break;
245
+
246
+ case 'drop':
247
+ if (element.animate) {
248
+ element.style.opacity = '0';
249
+ const anim = element.animate([
250
+ { opacity: 0, transform: 'translateY(-15px) scale(0.95)', offset: 0 },
251
+ { opacity: 1, transform: 'translateY(3px) scale(1.02)', offset: 0.5 },
252
+ { opacity: 1, transform: 'translateY(-1px) scale(1)', offset: 0.75 },
253
+ { opacity: 1, transform: 'translateY(0) scale(1)', offset: 1 }
254
+ ], { duration, easing: smoothDecel, fill: 'forwards' });
255
+ anim.onfinish = () => {
256
+ anim.cancel();
257
+ cleanup();
258
+ };
259
+ } else {
260
+ element.style.opacity = '0';
261
+ element.style.transform = 'translateY(-15px) scale(0.95)';
262
+ setTimeout(cleanup, duration);
263
+ }
264
+ break;
265
+
266
+ default:
267
+ this.appearAnimate('fade', duration, callback);
268
+ return;
269
+ }
270
+ }
271
+
272
+ /** @deprecated Use appearAnimate() instead */
273
+ fadeIn(duration, speed, transition_f, callback) {
274
+ let start = performance.now();
275
+ let opacity = 0;
276
+ let piece = this;
277
+ let fade = function () {
278
+ if (!piece.element) { console.debug(`[Piece] fadeIn: ${piece.id} - element is null`); if (callback) callback(); return; }
279
+ let elapsed = performance.now() - start;
280
+ opacity = transition_f(elapsed, duration, speed);
281
+ piece.element.style.opacity = opacity;
282
+ if (elapsed < duration) {
283
+ requestAnimationFrame(fade);
284
+ } else {
285
+ if (!piece.element) { console.debug(`[Piece] fadeIn: ${piece.id} - element is null (end)`); if (callback) callback(); return; }
286
+ piece.element.style.opacity = 1;
287
+ console.debug(`[Piece] fadeIn complete: ${piece.id}`);
288
+ if (callback) callback();
289
+ }
290
+ }
291
+ fade();
292
+ }
293
+
294
+ fadeOut(duration, speed, transition_f, callback) {
295
+ let start = performance.now();
296
+ let opacity = 1;
297
+ let piece = this;
298
+ let fade = function () {
299
+ if (!piece.element) { console.debug(`[Piece] fadeOut: ${piece.id} - element is null`); if (callback) callback(); return; }
300
+ let elapsed = performance.now() - start;
301
+ opacity = 1 - transition_f(elapsed, duration, speed);
302
+ piece.element.style.opacity = opacity;
303
+ if (elapsed < duration) {
304
+ requestAnimationFrame(fade);
305
+ } else {
306
+ if (!piece.element) { if (callback) callback(); return; }
307
+ piece.element.style.opacity = 0;
308
+ // Remove element from DOM after fade completes
309
+ if (piece.element.parentNode) {
310
+ piece.element.parentNode.removeChild(piece.element);
311
+ }
312
+ if (callback) callback();
313
+ }
314
+ }
315
+ fade();
316
+ }
317
+
318
+ /**
319
+ * Animate piece capture with configurable style
320
+ * Uses fluid easing for smooth, connected animations
321
+ * @param {string} style - Capture style: 'fade', 'shrink', 'instant', 'explode'
322
+ * @param {number} duration - Animation duration in ms
323
+ * @param {Function} callback - Callback when complete
324
+ */
325
+ captureAnimate(style, duration, callback) {
326
+ if (!this.element) {
327
+ if (callback) callback();
328
+ return;
329
+ }
330
+
331
+ const element = this.element;
332
+ const cleanup = () => {
333
+ if (element && element.parentNode) {
334
+ element.parentNode.removeChild(element);
335
+ }
336
+ if (callback) callback();
337
+ };
338
+
339
+ // Fluid easing functions
340
+ const smoothDecel = 'cubic-bezier(0.33, 1, 0.68, 1)';
341
+ const smoothAccel = 'cubic-bezier(0.32, 0, 0.67, 0)';
342
+
343
+ switch (style) {
344
+ case 'instant':
345
+ cleanup();
346
+ break;
347
+
348
+ case 'fade':
349
+ if (element.animate) {
350
+ // Smooth fade with subtle scale down
351
+ const anim = element.animate([
352
+ { opacity: 1, transform: 'scale(1)' },
353
+ { opacity: 0, transform: 'scale(0.9)' }
354
+ ], { duration, easing: smoothDecel, fill: 'forwards' });
355
+ anim.onfinish = cleanup;
356
+ } else {
357
+ element.style.transition = `opacity ${duration}ms ease-out, transform ${duration}ms ease-out`;
358
+ element.style.opacity = '0';
359
+ element.style.transform = 'scale(0.9)';
360
+ setTimeout(cleanup, duration);
361
+ }
362
+ break;
363
+
364
+ case 'shrink':
365
+ if (element.animate) {
366
+ // Smooth shrink with accelerating easing for "sucked in" feel
367
+ const anim = element.animate([
368
+ { transform: 'scale(1)', opacity: 1, offset: 0 },
369
+ { transform: 'scale(0.7)', opacity: 0.6, offset: 0.6 },
370
+ { transform: 'scale(0)', opacity: 0, offset: 1 }
371
+ ], { duration, easing: smoothAccel, fill: 'forwards' });
372
+ anim.onfinish = cleanup;
373
+ } else {
374
+ element.style.transition = `transform ${duration}ms ease-in, opacity ${duration}ms ease-in`;
375
+ element.style.transform = 'scale(0)';
376
+ element.style.opacity = '0';
377
+ setTimeout(cleanup, duration);
378
+ }
379
+ break;
380
+
381
+ case 'explode':
382
+ if (element.animate) {
383
+ // Subtle expand with smooth deceleration - less dramatic
384
+ const anim = element.animate([
385
+ { transform: 'scale(1)', opacity: 1, offset: 0 },
386
+ { transform: 'scale(1.15)', opacity: 0.5, offset: 0.5 },
387
+ { transform: 'scale(1.25)', opacity: 0, offset: 1 }
388
+ ], { duration, easing: smoothDecel, fill: 'forwards' });
389
+ anim.onfinish = cleanup;
390
+ } else {
391
+ element.style.transition = `transform ${duration}ms ease-out, opacity ${duration}ms ease-out`;
392
+ element.style.transform = 'scale(1.25)';
393
+ element.style.opacity = '0';
394
+ setTimeout(cleanup, duration);
395
+ }
396
+ break;
397
+
398
+ default:
399
+ // Default to fade
400
+ this.captureAnimate('fade', duration, callback);
401
+ return;
402
+ }
403
+ }
404
+
405
+ setDrag(f) {
406
+ if (!this.element) { console.debug(`[Piece] setDrag: ${this.id} - element is null`); return; }
407
+
408
+ // Remove any existing drag handlers first
409
+ if (this._dragHandler) {
410
+ this.element.removeEventListener('mousedown', this._dragHandler);
411
+ }
412
+
413
+ this.element.ondragstart = (e) => { e.preventDefault() };
414
+
415
+ // Use the drag function directly without timeout
416
+ this._dragHandler = f;
417
+ this.element.addEventListener('mousedown', this._dragHandler);
418
+ console.debug(`[Piece] setDrag: ${this.id}`);
419
+ }
420
+
421
+ destroy() {
422
+ console.debug(`[Piece] Destroy: ${this.id}`);
423
+
424
+ // Remove all event listeners
425
+ if (this.element) {
426
+ if (this._dragHandler) {
427
+ this.element.removeEventListener('mousedown', this._dragHandler);
428
+ this._dragHandler = null;
429
+ }
430
+
431
+ this.element.onmousedown = null;
432
+ this.element.ondragstart = null;
433
+
434
+ // Remove from DOM
435
+ if (this.element.parentNode) {
436
+ this.element.parentNode.removeChild(this.element);
437
+ }
438
+
439
+ // Clear references
440
+ this.element = null;
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Translate piece to target position with configurable movement style
446
+ * Uses Chessground-style cubic easing for fluid, natural movement
447
+ * @param {HTMLElement} to - Target element
448
+ * @param {number} duration - Animation duration in ms
449
+ * @param {Function} transition_f - Transition function (unused, for compatibility)
450
+ * @param {number} speed - Speed factor (unused, for compatibility)
451
+ * @param {Function} callback - Callback when complete
452
+ * @param {Object} options - Movement options
453
+ * @param {string} options.style - Movement style: 'slide', 'arc', 'hop', 'teleport', 'fade'
454
+ * @param {string} options.easing - CSS easing function
455
+ * @param {number} options.arcHeight - Arc height ratio (for arc/hop styles)
456
+ * @param {string} options.landingEffect - Landing effect: 'none', 'bounce', 'pulse', 'settle'
457
+ * @param {number} options.landingDuration - Landing effect duration in ms
458
+ */
459
+ translate(to, duration, transition_f, speed, callback = null, options = {}) {
460
+ if (!this.element) {
461
+ console.debug(`[Piece] translate: ${this.id} - element is null`);
462
+ if (callback) callback();
463
+ return;
464
+ }
465
+
466
+ const style = options.style || 'slide';
467
+ const arcHeight = options.arcHeight || 0.3;
468
+ const landingEffect = options.landingEffect || 'none';
469
+ const landingDuration = options.landingDuration || 150;
470
+
471
+ // Map easing names to Chessground-style fluid cubic-bezier curves
472
+ // Default: smooth deceleration like Lichess/Chessground
473
+ const easingMap = {
474
+ 'ease': 'cubic-bezier(0.33, 1, 0.68, 1)', // Chessground default - smooth decel
475
+ 'linear': 'linear',
476
+ 'ease-in': 'cubic-bezier(0.32, 0, 0.67, 0)', // Smooth acceleration
477
+ 'ease-out': 'cubic-bezier(0.33, 1, 0.68, 1)', // Smooth deceleration (same as default)
478
+ 'ease-in-out': 'cubic-bezier(0.65, 0, 0.35, 1)' // Smooth both ways
479
+ };
480
+ const easing = easingMap[options.easing] || easingMap['ease'];
481
+
482
+ // Handle teleport (instant)
483
+ if (style === 'teleport' || duration === 0) {
484
+ if (callback) callback();
485
+ return;
486
+ }
487
+
488
+ // Handle fade style
489
+ if (style === 'fade') {
490
+ this._translateFade(to, duration, easing, landingEffect, landingDuration, callback);
491
+ return;
492
+ }
493
+
494
+ // Calculate movement vectors
495
+ const sourceRect = this.element.getBoundingClientRect();
496
+ const targetRect = to.getBoundingClientRect();
497
+ const dx = (targetRect.left + targetRect.width / 2) - (sourceRect.left + sourceRect.width / 2);
498
+ const dy = (targetRect.top + targetRect.height / 2) - (sourceRect.top + sourceRect.height / 2);
499
+ const distance = Math.sqrt(dx * dx + dy * dy);
500
+
501
+ // Build keyframes based on style
502
+ let keyframes;
503
+ let animationEasing = easing;
504
+
505
+ switch (style) {
506
+ case 'arc':
507
+ keyframes = this._buildArcKeyframes(dx, dy, distance, arcHeight);
508
+ // Arc uses linear easing because the curve is in the keyframes
509
+ animationEasing = 'linear';
510
+ break;
511
+ case 'hop':
512
+ keyframes = this._buildHopKeyframes(dx, dy, distance, arcHeight);
513
+ // Hop uses ease-out for natural landing feel
514
+ animationEasing = 'cubic-bezier(0.33, 1, 0.68, 1)';
515
+ break;
516
+ case 'slide':
517
+ default:
518
+ keyframes = [
519
+ { transform: 'translate(0, 0)' },
520
+ { transform: `translate(${dx}px, ${dy}px)` }
521
+ ];
522
+ }
523
+
524
+ // Animate
525
+ if (this.element.animate) {
526
+ const animation = this.element.animate(keyframes, {
527
+ duration: duration,
528
+ easing: animationEasing,
529
+ fill: 'forwards'
530
+ });
531
+
532
+ animation.onfinish = () => {
533
+ if (!this.element) {
534
+ if (callback) callback();
535
+ return;
536
+ }
537
+
538
+ // Cancel animation and move piece in DOM first
539
+ animation.cancel();
540
+ if (this.element) this.element.style = '';
541
+
542
+ // Callback moves piece to new square in DOM
543
+ if (callback) callback();
544
+
545
+ // Apply landing effect AFTER piece is in new position
546
+ if (landingEffect !== 'none') {
547
+ // Small delay to ensure DOM is updated
548
+ requestAnimationFrame(() => {
549
+ this._applyLandingEffect(landingEffect, landingDuration);
550
+ });
551
+ }
552
+ };
553
+ } else {
554
+ // Fallback for browsers without Web Animations API
555
+ this.element.style.transition = `transform ${duration}ms ${animationEasing}`;
556
+ this.element.style.transform = `translate(${dx}px, ${dy}px)`;
557
+ setTimeout(() => {
558
+ if (!this.element) {
559
+ if (callback) callback();
560
+ return;
561
+ }
562
+ if (this.element) this.element.style = '';
563
+ if (callback) callback();
564
+
565
+ // Apply landing effect after DOM update
566
+ if (landingEffect !== 'none') {
567
+ requestAnimationFrame(() => {
568
+ this._applyLandingEffect(landingEffect, landingDuration);
569
+ });
570
+ }
571
+ }, duration);
572
+ }
573
+ }
574
+
575
+ /**
576
+ * Build arc-shaped keyframes (smooth parabolic curve)
577
+ * Uses many keyframes for fluid motion without relying on easing
578
+ * @private
579
+ */
580
+ _buildArcKeyframes(dx, dy, distance, arcHeight) {
581
+ const lift = distance * arcHeight;
582
+ const steps = 10;
583
+ const keyframes = [];
584
+
585
+ for (let i = 0; i <= steps; i++) {
586
+ const t = i / steps;
587
+ // Smooth easing for horizontal movement (Chessground-style cubic)
588
+ const easedT = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
589
+ // Parabolic arc for vertical lift: peaks at t=0.5
590
+ const arcT = 4 * t * (1 - t); // Parabola: 0 at t=0, 1 at t=0.5, 0 at t=1
591
+
592
+ const x = dx * easedT;
593
+ const y = dy * easedT - lift * arcT;
594
+
595
+ keyframes.push({
596
+ transform: `translate(${x}px, ${y}px)`,
597
+ offset: t
598
+ });
599
+ }
600
+ return keyframes;
601
+ }
602
+
603
+ /**
604
+ * Build hop-shaped keyframes (knight-like jump with subtle scale)
605
+ * More aggressive vertical movement, subtle scale for emphasis
606
+ * @private
607
+ */
608
+ _buildHopKeyframes(dx, dy, distance, arcHeight) {
609
+ const lift = distance * arcHeight * 1.2;
610
+ const steps = 12;
611
+ const keyframes = [];
612
+
613
+ for (let i = 0; i <= steps; i++) {
614
+ const t = i / steps;
615
+ // Chessground-style cubic easing for smooth acceleration/deceleration
616
+ const easedT = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
617
+ // Sharp parabolic hop - peaks earlier for snappier feel
618
+ const hopT = Math.sin(t * Math.PI); // Sine for smooth hop curve
619
+ // Subtle scale: peaks at 1.03 at the top of the hop
620
+ const scale = 1 + 0.03 * hopT;
621
+
622
+ const x = dx * easedT;
623
+ const y = dy * easedT - lift * hopT;
624
+
625
+ keyframes.push({
626
+ transform: `translate(${x}px, ${y}px) scale(${scale})`,
627
+ offset: t
628
+ });
629
+ }
630
+ return keyframes;
631
+ }
632
+
633
+ /**
634
+ * Translate using fade effect (smooth crossfade with scale)
635
+ * @private
636
+ */
637
+ _translateFade(to, duration, easing, landingEffect, landingDuration, callback) {
638
+ const halfDuration = duration / 2;
639
+ // Use smooth deceleration for fade
640
+ const fadeEasing = 'cubic-bezier(0.33, 1, 0.68, 1)';
641
+
642
+ // Fade out with subtle scale down
643
+ if (this.element.animate) {
644
+ const fadeOut = this.element.animate([
645
+ { opacity: 1, transform: 'scale(1)' },
646
+ { opacity: 0, transform: 'scale(0.95)' }
647
+ ], { duration: halfDuration, easing: fadeEasing, fill: 'forwards' });
648
+
649
+ fadeOut.onfinish = () => {
650
+ if (!this.element) {
651
+ if (callback) callback();
652
+ return;
653
+ }
654
+ // Move instantly (hidden)
655
+ fadeOut.cancel();
656
+ this.element.style.opacity = '0';
657
+ this.element.style.transform = 'scale(0.95)';
658
+
659
+ // Let parent move the piece in DOM, then fade in
660
+ if (callback) callback();
661
+
662
+ // Fade in at new position with subtle scale up
663
+ requestAnimationFrame(() => {
664
+ if (!this.element) return;
665
+ const fadeIn = this.element.animate([
666
+ { opacity: 0, transform: 'scale(0.95)' },
667
+ { opacity: 1, transform: 'scale(1)' }
668
+ ], { duration: halfDuration, easing: fadeEasing, fill: 'forwards' });
669
+
670
+ fadeIn.onfinish = () => {
671
+ if (this.element) {
672
+ fadeIn.cancel();
673
+ this.element.style.opacity = '';
674
+ this.element.style.transform = '';
675
+ this._applyLandingEffect(landingEffect, landingDuration);
676
+ }
677
+ };
678
+ });
679
+ };
680
+ } else {
681
+ // Fallback
682
+ this.element.style.transition = `opacity ${halfDuration}ms ease, transform ${halfDuration}ms ease`;
683
+ this.element.style.opacity = '0';
684
+ this.element.style.transform = 'scale(0.95)';
685
+ setTimeout(() => {
686
+ if (callback) callback();
687
+ if (this.element) {
688
+ this.element.style.opacity = '1';
689
+ this.element.style.transform = 'scale(1)';
690
+ setTimeout(() => {
691
+ if (this.element) this.element.style = '';
692
+ }, halfDuration);
693
+ }
694
+ }, halfDuration);
695
+ }
696
+ }
697
+
698
+ /**
699
+ * Apply landing effect after movement completes
700
+ * Uses spring-like overshoot easing for natural, connected feel
701
+ * @private
702
+ */
703
+ _applyLandingEffect(effect, duration, callback) {
704
+ if (!this.element || effect === 'none') {
705
+ if (callback) callback();
706
+ return;
707
+ }
708
+
709
+ let keyframes;
710
+ // Overshoot easing for spring-like natural feel
711
+ let effectEasing = 'cubic-bezier(0.34, 1.56, 0.64, 1)';
712
+
713
+ switch (effect) {
714
+ case 'bounce':
715
+ // Subtle bounce using spring easing - single smooth bounce
716
+ keyframes = [
717
+ { transform: 'translateY(0)', offset: 0 },
718
+ { transform: 'translateY(-5px)', offset: 0.4 },
719
+ { transform: 'translateY(0)', offset: 1 }
720
+ ];
721
+ // Use ease-out for natural deceleration
722
+ effectEasing = 'cubic-bezier(0.33, 1, 0.68, 1)';
723
+ break;
724
+ case 'pulse':
725
+ // Subtle scale pulse - less aggressive, more refined
726
+ keyframes = [
727
+ { transform: 'scale(1)', offset: 0 },
728
+ { transform: 'scale(1.08)', offset: 0.5 },
729
+ { transform: 'scale(1)', offset: 1 }
730
+ ];
731
+ break;
732
+ case 'settle':
733
+ // Minimal settle - piece "clicks" into place
734
+ keyframes = [
735
+ { transform: 'scale(1.02)', offset: 0 },
736
+ { transform: 'scale(1)', offset: 1 }
737
+ ];
738
+ effectEasing = 'cubic-bezier(0.33, 1, 0.68, 1)';
739
+ break;
740
+ default:
741
+ if (callback) callback();
742
+ return;
743
+ }
744
+
745
+ if (this.element.animate) {
746
+ const animation = this.element.animate(keyframes, {
747
+ duration: duration,
748
+ easing: effectEasing,
749
+ fill: 'forwards'
750
+ });
751
+ animation.onfinish = () => {
752
+ if (this.element) animation.cancel();
753
+ if (callback) callback();
754
+ };
755
+ } else {
756
+ if (callback) callback();
757
+ }
758
+ }
759
+
760
+ check() {
761
+ if (['p', 'r', 'n', 'b', 'q', 'k'].indexOf(this.type) === -1) {
762
+ throw new Error("Invalid piece type");
763
+ }
764
+
765
+ if (['w', 'b'].indexOf(this.color) === -1) {
766
+ throw new Error("Invalid piece color");
767
+ }
768
+ }
769
+ }
770
+
771
+ export default Piece;