@alepot55/chessboardjs 2.2.2 → 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.
- package/dist/chessboard.cjs.js +1477 -382
- package/dist/chessboard.css +22 -7
- package/dist/chessboard.esm.js +1477 -382
- package/dist/chessboard.iife.js +1477 -382
- package/dist/chessboard.umd.js +1477 -382
- package/package.json +18 -3
- package/src/components/Piece.js +509 -26
- package/src/components/Square.js +3 -3
- package/src/core/Chessboard.js +625 -218
- package/src/core/ChessboardConfig.js +257 -8
- package/src/services/MoveService.js +37 -99
- package/src/services/PieceService.js +51 -24
- package/src/styles/board.css +22 -3
- package/.eslintrc.json +0 -227
- package/chessboard.bundle.js +0 -4072
- package/config/.babelrc +0 -4
- package/config/jest.config.js +0 -15
- package/config/rollup.config.js +0 -36
- package/jest.config.js +0 -2
- package/rollup.config.js +0 -2
- package/tests/unit/chessboard-config-animations.test.js +0 -106
- package/tests/unit/chessboard-robust.test.js +0 -163
- package/tests/unit/chessboard.test.js +0 -183
package/package.json
CHANGED
|
@@ -3,9 +3,24 @@
|
|
|
3
3
|
"chess.js": "^1.0.0"
|
|
4
4
|
},
|
|
5
5
|
"name": "@alepot55/chessboardjs",
|
|
6
|
-
"version": "2.
|
|
7
|
-
"main": "
|
|
6
|
+
"version": "2.3.0",
|
|
7
|
+
"main": "dist/chessboard.cjs.js",
|
|
8
|
+
"module": "dist/chessboard.esm.js",
|
|
9
|
+
"browser": "dist/chessboard.iife.js",
|
|
8
10
|
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/chessboard.esm.js",
|
|
14
|
+
"require": "./dist/chessboard.cjs.js",
|
|
15
|
+
"default": "./dist/chessboard.esm.js"
|
|
16
|
+
},
|
|
17
|
+
"./dist/*": "./dist/*"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist/",
|
|
21
|
+
"src/",
|
|
22
|
+
"assets/"
|
|
23
|
+
],
|
|
9
24
|
"scripts": {
|
|
10
25
|
"dev": "rollup -c config/rollup.config.js --watch",
|
|
11
26
|
"build": "rollup -c config/rollup.config.js",
|
|
@@ -21,7 +36,7 @@
|
|
|
21
36
|
"lint:fix": "echo 'ESLint not configured yet'",
|
|
22
37
|
"docs": "echo 'JSDoc not configured yet'",
|
|
23
38
|
"examples": "echo 'Example server not configured yet'",
|
|
24
|
-
"prepublishOnly": "npm run build"
|
|
39
|
+
"prepublishOnly": "npm run test && npm run build"
|
|
25
40
|
},
|
|
26
41
|
"author": "alepot55",
|
|
27
42
|
"license": "ISC",
|
package/src/components/Piece.js
CHANGED
|
@@ -155,6 +155,121 @@ class Piece {
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
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 */
|
|
158
273
|
fadeIn(duration, speed, transition_f, callback) {
|
|
159
274
|
let start = performance.now();
|
|
160
275
|
let opacity = 0;
|
|
@@ -188,15 +303,105 @@ class Piece {
|
|
|
188
303
|
if (elapsed < duration) {
|
|
189
304
|
requestAnimationFrame(fade);
|
|
190
305
|
} else {
|
|
191
|
-
if (!piece.element) {
|
|
306
|
+
if (!piece.element) { if (callback) callback(); return; }
|
|
192
307
|
piece.element.style.opacity = 0;
|
|
193
|
-
|
|
308
|
+
// Remove element from DOM after fade completes
|
|
309
|
+
if (piece.element.parentNode) {
|
|
310
|
+
piece.element.parentNode.removeChild(piece.element);
|
|
311
|
+
}
|
|
194
312
|
if (callback) callback();
|
|
195
313
|
}
|
|
196
314
|
}
|
|
197
315
|
fade();
|
|
198
316
|
}
|
|
199
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
|
+
|
|
200
405
|
setDrag(f) {
|
|
201
406
|
if (!this.element) { console.debug(`[Piece] setDrag: ${this.id} - element is null`); return; }
|
|
202
407
|
|
|
@@ -236,41 +441,319 @@ class Piece {
|
|
|
236
441
|
}
|
|
237
442
|
}
|
|
238
443
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
+
}
|
|
254
523
|
|
|
524
|
+
// Animate
|
|
255
525
|
if (this.element.animate) {
|
|
256
|
-
|
|
526
|
+
const animation = this.element.animate(keyframes, {
|
|
257
527
|
duration: duration,
|
|
258
|
-
easing:
|
|
259
|
-
fill: '
|
|
528
|
+
easing: animationEasing,
|
|
529
|
+
fill: 'forwards'
|
|
260
530
|
});
|
|
261
531
|
|
|
262
532
|
animation.onfinish = () => {
|
|
263
|
-
if (!this.element) {
|
|
264
|
-
|
|
533
|
+
if (!this.element) {
|
|
534
|
+
if (callback) callback();
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Cancel animation and move piece in DOM first
|
|
539
|
+
animation.cancel();
|
|
265
540
|
if (this.element) this.element.style = '';
|
|
266
|
-
|
|
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
|
+
}
|
|
267
552
|
};
|
|
268
553
|
} else {
|
|
269
|
-
|
|
554
|
+
// Fallback for browsers without Web Animations API
|
|
555
|
+
this.element.style.transition = `transform ${duration}ms ${animationEasing}`;
|
|
270
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 {
|
|
271
756
|
if (callback) callback();
|
|
272
|
-
if (this.element) this.element.style = '';
|
|
273
|
-
console.debug(`[Piece] translate complete (no animate): ${this.id}`);
|
|
274
757
|
}
|
|
275
758
|
}
|
|
276
759
|
|
package/src/components/Square.js
CHANGED
|
@@ -106,9 +106,9 @@ class Square {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
putPiece(piece) {
|
|
109
|
-
// If there's already a piece,
|
|
109
|
+
// If there's already a piece, destroy it to avoid orphaned DOM elements
|
|
110
110
|
if (this.piece) {
|
|
111
|
-
this.removePiece(
|
|
111
|
+
this.removePiece(false);
|
|
112
112
|
}
|
|
113
113
|
this.piece = piece;
|
|
114
114
|
if (piece && piece.element) {
|
|
@@ -220,7 +220,7 @@ class Square {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
getColor() {
|
|
223
|
-
return this.piece.getColor();
|
|
223
|
+
return this.piece ? this.piece.getColor() : null;
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
check() {
|