@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/dist/chessboard.cjs.js
CHANGED
|
@@ -1072,11 +1072,84 @@ const TRANSITION_FUNCTIONS = Object.freeze({
|
|
|
1072
1072
|
* @constant
|
|
1073
1073
|
* @type {Object}
|
|
1074
1074
|
*/
|
|
1075
|
+
/**
|
|
1076
|
+
* Flip mode options
|
|
1077
|
+
* @constant
|
|
1078
|
+
* @type {Object}
|
|
1079
|
+
*/
|
|
1080
|
+
const FLIP_MODES = Object.freeze({
|
|
1081
|
+
visual: 'visual', // CSS flexbox visual flip (instant, no piece animation)
|
|
1082
|
+
animate: 'animate', // Animate pieces to mirrored positions (smooth animation)
|
|
1083
|
+
none: 'none' // No visual change, only internal orientation
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Movement style options - how pieces travel from source to destination
|
|
1088
|
+
* @constant
|
|
1089
|
+
* @type {Object}
|
|
1090
|
+
*/
|
|
1091
|
+
const MOVE_STYLES = Object.freeze({
|
|
1092
|
+
slide: 'slide', // Linear movement (default) - piece slides in straight line
|
|
1093
|
+
arc: 'arc', // Arc trajectory - piece lifts up then comes down
|
|
1094
|
+
hop: 'hop', // Parabolic jump - like a knight hopping
|
|
1095
|
+
teleport: 'teleport', // Instant - no animation, piece appears at destination
|
|
1096
|
+
fade: 'fade' // Crossfade - fades out at source, fades in at destination
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Capture animation options - what happens to captured pieces
|
|
1101
|
+
* @constant
|
|
1102
|
+
* @type {Object}
|
|
1103
|
+
*/
|
|
1104
|
+
const CAPTURE_STYLES = Object.freeze({
|
|
1105
|
+
fade: 'fade', // Fade out (default)
|
|
1106
|
+
shrink: 'shrink', // Shrink then fade
|
|
1107
|
+
instant: 'instant', // Disappears immediately
|
|
1108
|
+
explode: 'explode' // Scale up and fade (dramatic effect)
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* Appearance animation options - how new pieces appear on the board
|
|
1113
|
+
* @constant
|
|
1114
|
+
* @type {Object}
|
|
1115
|
+
*/
|
|
1116
|
+
const APPEARANCE_STYLES = Object.freeze({
|
|
1117
|
+
fade: 'fade', // Opacity + subtle scale (default)
|
|
1118
|
+
pulse: 'pulse', // Double pulse scale bounce
|
|
1119
|
+
pop: 'pop', // Scale 0→1.15→1 with spring easing
|
|
1120
|
+
drop: 'drop', // Drops from above with bounce
|
|
1121
|
+
instant: 'instant' // No animation
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Landing effect options - what happens when piece reaches destination
|
|
1126
|
+
* @constant
|
|
1127
|
+
* @type {Object}
|
|
1128
|
+
*/
|
|
1129
|
+
const LANDING_EFFECTS = Object.freeze({
|
|
1130
|
+
none: 'none', // No effect (default)
|
|
1131
|
+
bounce: 'bounce', // Slight bounce on landing
|
|
1132
|
+
pulse: 'pulse', // Quick scale pulse
|
|
1133
|
+
settle: 'settle' // Subtle settling animation
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* Drag style options - how pieces behave during drag
|
|
1138
|
+
* @constant
|
|
1139
|
+
* @type {Object}
|
|
1140
|
+
*/
|
|
1141
|
+
const DRAG_STYLES = Object.freeze({
|
|
1142
|
+
smooth: 'smooth', // Piece follows cursor smoothly (default)
|
|
1143
|
+
snap: 'snap', // Piece snaps to cursor position
|
|
1144
|
+
elastic: 'elastic' // Slight elastic lag behind cursor
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1075
1147
|
const DEFAULT_CONFIG$1 = Object.freeze({
|
|
1076
1148
|
id: 'board',
|
|
1077
1149
|
position: 'start',
|
|
1078
1150
|
orientation: 'w',
|
|
1079
1151
|
mode: 'normal',
|
|
1152
|
+
flipMode: 'visual', // 'visual', 'animate', or 'none'
|
|
1080
1153
|
size: 'auto',
|
|
1081
1154
|
draggable: true,
|
|
1082
1155
|
hints: true,
|
|
@@ -1084,15 +1157,45 @@ const DEFAULT_CONFIG$1 = Object.freeze({
|
|
|
1084
1157
|
movableColors: 'both',
|
|
1085
1158
|
moveHighlight: true,
|
|
1086
1159
|
overHighlight: true,
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1160
|
+
|
|
1161
|
+
// Movement configuration
|
|
1162
|
+
moveStyle: 'slide', // 'slide', 'arc', 'hop', 'teleport', 'fade'
|
|
1163
|
+
moveEasing: 'ease', // CSS easing function
|
|
1164
|
+
moveTime: 'fast', // Duration: 'instant', 'veryFast', 'fast', 'normal', 'slow', 'verySlow' or ms
|
|
1165
|
+
moveArcHeight: 0.3, // Arc height as ratio of distance (for 'arc' and 'hop' styles)
|
|
1166
|
+
|
|
1167
|
+
// Capture configuration
|
|
1168
|
+
captureStyle: 'fade', // 'fade', 'shrink', 'instant', 'explode'
|
|
1169
|
+
captureTime: 'fast', // Duration for capture animation
|
|
1170
|
+
|
|
1171
|
+
// Appearance configuration
|
|
1172
|
+
appearanceStyle: 'fade', // 'fade', 'pulse', 'pop', 'drop', 'instant'
|
|
1173
|
+
appearanceTime: 'fast', // Duration for appearance animation
|
|
1174
|
+
|
|
1175
|
+
// Landing effect configuration
|
|
1176
|
+
landingEffect: 'none', // 'none', 'bounce', 'pulse', 'settle'
|
|
1177
|
+
landingDuration: 150, // Duration for landing effect in ms
|
|
1178
|
+
|
|
1179
|
+
// Drag configuration
|
|
1180
|
+
dragStyle: 'smooth', // 'smooth', 'snap', 'elastic'
|
|
1181
|
+
dragScale: 1.05, // Scale factor while dragging (1.0 = no scale)
|
|
1182
|
+
dragOpacity: 0.9, // Opacity while dragging
|
|
1183
|
+
|
|
1184
|
+
// Snapback configuration
|
|
1090
1185
|
snapbackTime: 'fast',
|
|
1091
|
-
|
|
1186
|
+
snapbackEasing: 'ease',
|
|
1187
|
+
|
|
1188
|
+
// Other timing
|
|
1189
|
+
dropOffBoard: 'snapback',
|
|
1092
1190
|
dropCenterTime: 'veryFast',
|
|
1093
1191
|
dropCenterAnimation: 'ease',
|
|
1094
1192
|
fadeTime: 'fast',
|
|
1095
1193
|
fadeAnimation: 'ease',
|
|
1194
|
+
|
|
1195
|
+
// Legacy compatibility
|
|
1196
|
+
moveAnimation: 'ease',
|
|
1197
|
+
snapbackAnimation: 'ease',
|
|
1198
|
+
|
|
1096
1199
|
ratio: 0.9,
|
|
1097
1200
|
piecesPath: '../assets/themes/default',
|
|
1098
1201
|
animationStyle: 'simultaneous',
|
|
@@ -1187,6 +1290,7 @@ class ChessboardConfig {
|
|
|
1187
1290
|
this.position = config.position;
|
|
1188
1291
|
this.orientation = config.orientation;
|
|
1189
1292
|
this.mode = config.mode;
|
|
1293
|
+
this.flipMode = this._validateFlipMode(config.flipMode);
|
|
1190
1294
|
this.dropOffBoard = config.dropOffBoard;
|
|
1191
1295
|
this.size = config.size;
|
|
1192
1296
|
this.movableColors = config.movableColors;
|
|
@@ -1201,12 +1305,27 @@ class ChessboardConfig {
|
|
|
1201
1305
|
this.onDrop = this._validateCallback(config.onDrop);
|
|
1202
1306
|
this.onSnapbackEnd = this._validateCallback(config.onSnapbackEnd);
|
|
1203
1307
|
|
|
1204
|
-
// Animation properties
|
|
1308
|
+
// Animation properties (legacy)
|
|
1205
1309
|
this.moveAnimation = this._setTransitionFunction(config.moveAnimation);
|
|
1206
1310
|
this.snapbackAnimation = this._setTransitionFunction(config.snapbackAnimation);
|
|
1207
1311
|
this.dropCenterAnimation = this._setTransitionFunction(config.dropCenterAnimation);
|
|
1208
1312
|
this.fadeAnimation = this._setTransitionFunction(config.fadeAnimation);
|
|
1209
1313
|
|
|
1314
|
+
// Movement configuration (new system)
|
|
1315
|
+
this.moveStyle = this._validateMoveStyle(config.moveStyle);
|
|
1316
|
+
this.moveEasing = this._setTransitionFunction(config.moveEasing);
|
|
1317
|
+
this.moveArcHeight = this._validateNumber(config.moveArcHeight, 0, 1, 'moveArcHeight');
|
|
1318
|
+
this.captureStyle = this._validateCaptureStyle(config.captureStyle);
|
|
1319
|
+
this.captureTime = this._setTime(config.captureTime);
|
|
1320
|
+
this.appearanceStyle = this._validateAppearanceStyle(config.appearanceStyle);
|
|
1321
|
+
this.appearanceTime = this._setTime(config.appearanceTime);
|
|
1322
|
+
this.landingEffect = this._validateLandingEffect(config.landingEffect);
|
|
1323
|
+
this.landingDuration = this._validateNumber(config.landingDuration, 0, 2000, 'landingDuration');
|
|
1324
|
+
this.dragStyle = this._validateDragStyle(config.dragStyle);
|
|
1325
|
+
this.dragScale = this._validateNumber(config.dragScale, 0.5, 2, 'dragScale');
|
|
1326
|
+
this.dragOpacity = this._validateNumber(config.dragOpacity, 0.1, 1, 'dragOpacity');
|
|
1327
|
+
this.snapbackEasing = this._setTransitionFunction(config.snapbackEasing);
|
|
1328
|
+
|
|
1210
1329
|
// Boolean properties
|
|
1211
1330
|
this.hints = this._setBoolean(config.hints);
|
|
1212
1331
|
this.clickable = this._setBoolean(config.clickable);
|
|
@@ -1310,6 +1429,111 @@ class ChessboardConfig {
|
|
|
1310
1429
|
return delay;
|
|
1311
1430
|
}
|
|
1312
1431
|
|
|
1432
|
+
/**
|
|
1433
|
+
* Validates flip mode
|
|
1434
|
+
* @private
|
|
1435
|
+
* @param {string} mode - Flip mode
|
|
1436
|
+
* @returns {string} Validated mode
|
|
1437
|
+
* @throws {ConfigurationError} If mode is invalid
|
|
1438
|
+
*/
|
|
1439
|
+
_validateFlipMode(mode) {
|
|
1440
|
+
if (!mode || !FLIP_MODES[mode]) {
|
|
1441
|
+
throw new ConfigurationError(
|
|
1442
|
+
`Invalid flip mode: ${mode}. Valid options: ${Object.keys(FLIP_MODES).join(', ')}`,
|
|
1443
|
+
'flipMode',
|
|
1444
|
+
mode
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
return mode;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
/**
|
|
1451
|
+
* Validates move style
|
|
1452
|
+
* @private
|
|
1453
|
+
* @param {string} style - Move style
|
|
1454
|
+
* @returns {string} Validated style
|
|
1455
|
+
*/
|
|
1456
|
+
_validateMoveStyle(style) {
|
|
1457
|
+
if (!style || !MOVE_STYLES[style]) {
|
|
1458
|
+
console.warn(`Invalid move style: ${style}. Using 'slide'. Valid: ${Object.keys(MOVE_STYLES).join(', ')}`);
|
|
1459
|
+
return 'slide';
|
|
1460
|
+
}
|
|
1461
|
+
return style;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/**
|
|
1465
|
+
* Validates capture style
|
|
1466
|
+
* @private
|
|
1467
|
+
* @param {string} style - Capture style
|
|
1468
|
+
* @returns {string} Validated style
|
|
1469
|
+
*/
|
|
1470
|
+
_validateCaptureStyle(style) {
|
|
1471
|
+
if (!style || !CAPTURE_STYLES[style]) {
|
|
1472
|
+
console.warn(`Invalid capture style: ${style}. Using 'fade'. Valid: ${Object.keys(CAPTURE_STYLES).join(', ')}`);
|
|
1473
|
+
return 'fade';
|
|
1474
|
+
}
|
|
1475
|
+
return style;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
/**
|
|
1479
|
+
* Validates appearance style
|
|
1480
|
+
* @private
|
|
1481
|
+
* @param {string} style - Appearance style
|
|
1482
|
+
* @returns {string} Validated style
|
|
1483
|
+
*/
|
|
1484
|
+
_validateAppearanceStyle(style) {
|
|
1485
|
+
if (!style || !APPEARANCE_STYLES[style]) {
|
|
1486
|
+
console.warn(`Invalid appearance style: ${style}. Using 'fade'. Valid: ${Object.keys(APPEARANCE_STYLES).join(', ')}`);
|
|
1487
|
+
return 'fade';
|
|
1488
|
+
}
|
|
1489
|
+
return style;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/**
|
|
1493
|
+
* Validates landing effect
|
|
1494
|
+
* @private
|
|
1495
|
+
* @param {string} effect - Landing effect
|
|
1496
|
+
* @returns {string} Validated effect
|
|
1497
|
+
*/
|
|
1498
|
+
_validateLandingEffect(effect) {
|
|
1499
|
+
if (!effect || !LANDING_EFFECTS[effect]) {
|
|
1500
|
+
console.warn(`Invalid landing effect: ${effect}. Using 'none'. Valid: ${Object.keys(LANDING_EFFECTS).join(', ')}`);
|
|
1501
|
+
return 'none';
|
|
1502
|
+
}
|
|
1503
|
+
return effect;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
/**
|
|
1507
|
+
* Validates drag style
|
|
1508
|
+
* @private
|
|
1509
|
+
* @param {string} style - Drag style
|
|
1510
|
+
* @returns {string} Validated style
|
|
1511
|
+
*/
|
|
1512
|
+
_validateDragStyle(style) {
|
|
1513
|
+
if (!style || !DRAG_STYLES[style]) {
|
|
1514
|
+
console.warn(`Invalid drag style: ${style}. Using 'smooth'. Valid: ${Object.keys(DRAG_STYLES).join(', ')}`);
|
|
1515
|
+
return 'smooth';
|
|
1516
|
+
}
|
|
1517
|
+
return style;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
/**
|
|
1521
|
+
* Validates a number within a range
|
|
1522
|
+
* @private
|
|
1523
|
+
* @param {number} value - Value to validate
|
|
1524
|
+
* @param {number} min - Minimum value
|
|
1525
|
+
* @param {number} max - Maximum value
|
|
1526
|
+
* @param {string} name - Property name for error message
|
|
1527
|
+
* @returns {number} Validated value
|
|
1528
|
+
*/
|
|
1529
|
+
_validateNumber(value, min, max, name) {
|
|
1530
|
+
if (typeof value !== 'number' || value < min || value > max) {
|
|
1531
|
+
console.warn(`Invalid ${name}: ${value}. Must be between ${min} and ${max}.`);
|
|
1532
|
+
return (min + max) / 2; // Return middle value as default
|
|
1533
|
+
}
|
|
1534
|
+
return value;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1313
1537
|
/**
|
|
1314
1538
|
* Sets a CSS custom property
|
|
1315
1539
|
* @private
|
|
@@ -1415,6 +1639,7 @@ class ChessboardConfig {
|
|
|
1415
1639
|
position: this.position,
|
|
1416
1640
|
orientation: this.orientation,
|
|
1417
1641
|
mode: this.mode,
|
|
1642
|
+
flipMode: this.flipMode,
|
|
1418
1643
|
size: this.size,
|
|
1419
1644
|
draggable: this.draggable,
|
|
1420
1645
|
hints: this.hints,
|
|
@@ -1422,11 +1647,26 @@ class ChessboardConfig {
|
|
|
1422
1647
|
movableColors: this.movableColors,
|
|
1423
1648
|
moveHighlight: this.moveHighlight,
|
|
1424
1649
|
overHighlight: this.overHighlight,
|
|
1425
|
-
|
|
1650
|
+
// Movement configuration
|
|
1651
|
+
moveStyle: this.moveStyle,
|
|
1652
|
+
moveEasing: this.moveEasing,
|
|
1426
1653
|
moveTime: this.moveTime,
|
|
1427
|
-
|
|
1654
|
+
moveArcHeight: this.moveArcHeight,
|
|
1655
|
+
captureStyle: this.captureStyle,
|
|
1656
|
+
captureTime: this.captureTime,
|
|
1657
|
+
appearanceStyle: this.appearanceStyle,
|
|
1658
|
+
appearanceTime: this.appearanceTime,
|
|
1659
|
+
landingEffect: this.landingEffect,
|
|
1660
|
+
landingDuration: this.landingDuration,
|
|
1661
|
+
dragStyle: this.dragStyle,
|
|
1662
|
+
dragScale: this.dragScale,
|
|
1663
|
+
dragOpacity: this.dragOpacity,
|
|
1428
1664
|
snapbackTime: this.snapbackTime,
|
|
1665
|
+
snapbackEasing: this.snapbackEasing,
|
|
1666
|
+
// Legacy
|
|
1667
|
+
moveAnimation: this.moveAnimation,
|
|
1429
1668
|
snapbackAnimation: this.snapbackAnimation,
|
|
1669
|
+
dropOffBoard: this.dropOffBoard,
|
|
1430
1670
|
dropCenterTime: this.dropCenterTime,
|
|
1431
1671
|
dropCenterAnimation: this.dropCenterAnimation,
|
|
1432
1672
|
fadeTime: this.fadeTime,
|
|
@@ -1631,6 +1871,121 @@ class Piece {
|
|
|
1631
1871
|
}
|
|
1632
1872
|
}
|
|
1633
1873
|
|
|
1874
|
+
/**
|
|
1875
|
+
* Animate piece appearance with configurable style
|
|
1876
|
+
* @param {string} style - Appearance style: 'fade', 'pulse', 'pop', 'drop', 'instant'
|
|
1877
|
+
* @param {number} duration - Animation duration in ms
|
|
1878
|
+
* @param {Function} callback - Callback when complete
|
|
1879
|
+
*/
|
|
1880
|
+
appearAnimate(style, duration, callback) {
|
|
1881
|
+
if (!this.element) {
|
|
1882
|
+
if (callback) callback();
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
const element = this.element;
|
|
1887
|
+
const cleanup = () => {
|
|
1888
|
+
if (element) {
|
|
1889
|
+
element.style.opacity = '1';
|
|
1890
|
+
element.style.transform = '';
|
|
1891
|
+
}
|
|
1892
|
+
if (callback) callback();
|
|
1893
|
+
};
|
|
1894
|
+
|
|
1895
|
+
const smoothDecel = 'cubic-bezier(0.33, 1, 0.68, 1)';
|
|
1896
|
+
const springOvershoot = 'cubic-bezier(0.34, 1.56, 0.64, 1)';
|
|
1897
|
+
|
|
1898
|
+
switch (style) {
|
|
1899
|
+
case 'instant':
|
|
1900
|
+
element.style.opacity = '1';
|
|
1901
|
+
cleanup();
|
|
1902
|
+
break;
|
|
1903
|
+
|
|
1904
|
+
case 'fade':
|
|
1905
|
+
if (element.animate) {
|
|
1906
|
+
element.style.opacity = '0';
|
|
1907
|
+
const anim = element.animate([
|
|
1908
|
+
{ opacity: 0, transform: 'scale(0.95)' },
|
|
1909
|
+
{ opacity: 1, transform: 'scale(1)' }
|
|
1910
|
+
], { duration, easing: smoothDecel, fill: 'forwards' });
|
|
1911
|
+
anim.onfinish = () => {
|
|
1912
|
+
anim.cancel();
|
|
1913
|
+
cleanup();
|
|
1914
|
+
};
|
|
1915
|
+
} else {
|
|
1916
|
+
element.style.opacity = '0';
|
|
1917
|
+
element.style.transform = 'scale(0.95)';
|
|
1918
|
+
setTimeout(cleanup, duration);
|
|
1919
|
+
}
|
|
1920
|
+
break;
|
|
1921
|
+
|
|
1922
|
+
case 'pulse':
|
|
1923
|
+
if (element.animate) {
|
|
1924
|
+
element.style.opacity = '0';
|
|
1925
|
+
const anim = element.animate([
|
|
1926
|
+
{ opacity: 0, transform: 'scale(0.6)', offset: 0 },
|
|
1927
|
+
{ opacity: 1, transform: 'scale(1.12)', offset: 0.3 },
|
|
1928
|
+
{ opacity: 1, transform: 'scale(0.92)', offset: 0.55 },
|
|
1929
|
+
{ opacity: 1, transform: 'scale(1.06)', offset: 0.8 },
|
|
1930
|
+
{ opacity: 1, transform: 'scale(1)', offset: 1 }
|
|
1931
|
+
], { duration, easing: smoothDecel, fill: 'forwards' });
|
|
1932
|
+
anim.onfinish = () => {
|
|
1933
|
+
anim.cancel();
|
|
1934
|
+
cleanup();
|
|
1935
|
+
};
|
|
1936
|
+
} else {
|
|
1937
|
+
element.style.opacity = '0';
|
|
1938
|
+
element.style.transform = 'scale(0.6)';
|
|
1939
|
+
setTimeout(cleanup, duration);
|
|
1940
|
+
}
|
|
1941
|
+
break;
|
|
1942
|
+
|
|
1943
|
+
case 'pop':
|
|
1944
|
+
if (element.animate) {
|
|
1945
|
+
element.style.opacity = '0';
|
|
1946
|
+
const anim = element.animate([
|
|
1947
|
+
{ opacity: 0, transform: 'scale(0)', offset: 0 },
|
|
1948
|
+
{ opacity: 1, transform: 'scale(1.15)', offset: 0.6 },
|
|
1949
|
+
{ opacity: 1, transform: 'scale(1)', offset: 1 }
|
|
1950
|
+
], { duration, easing: springOvershoot, fill: 'forwards' });
|
|
1951
|
+
anim.onfinish = () => {
|
|
1952
|
+
anim.cancel();
|
|
1953
|
+
cleanup();
|
|
1954
|
+
};
|
|
1955
|
+
} else {
|
|
1956
|
+
element.style.opacity = '0';
|
|
1957
|
+
element.style.transform = 'scale(0)';
|
|
1958
|
+
setTimeout(cleanup, duration);
|
|
1959
|
+
}
|
|
1960
|
+
break;
|
|
1961
|
+
|
|
1962
|
+
case 'drop':
|
|
1963
|
+
if (element.animate) {
|
|
1964
|
+
element.style.opacity = '0';
|
|
1965
|
+
const anim = element.animate([
|
|
1966
|
+
{ opacity: 0, transform: 'translateY(-15px) scale(0.95)', offset: 0 },
|
|
1967
|
+
{ opacity: 1, transform: 'translateY(3px) scale(1.02)', offset: 0.5 },
|
|
1968
|
+
{ opacity: 1, transform: 'translateY(-1px) scale(1)', offset: 0.75 },
|
|
1969
|
+
{ opacity: 1, transform: 'translateY(0) scale(1)', offset: 1 }
|
|
1970
|
+
], { duration, easing: smoothDecel, fill: 'forwards' });
|
|
1971
|
+
anim.onfinish = () => {
|
|
1972
|
+
anim.cancel();
|
|
1973
|
+
cleanup();
|
|
1974
|
+
};
|
|
1975
|
+
} else {
|
|
1976
|
+
element.style.opacity = '0';
|
|
1977
|
+
element.style.transform = 'translateY(-15px) scale(0.95)';
|
|
1978
|
+
setTimeout(cleanup, duration);
|
|
1979
|
+
}
|
|
1980
|
+
break;
|
|
1981
|
+
|
|
1982
|
+
default:
|
|
1983
|
+
this.appearAnimate('fade', duration, callback);
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
/** @deprecated Use appearAnimate() instead */
|
|
1634
1989
|
fadeIn(duration, speed, transition_f, callback) {
|
|
1635
1990
|
let start = performance.now();
|
|
1636
1991
|
let opacity = 0;
|
|
@@ -1664,15 +2019,105 @@ class Piece {
|
|
|
1664
2019
|
if (elapsed < duration) {
|
|
1665
2020
|
requestAnimationFrame(fade);
|
|
1666
2021
|
} else {
|
|
1667
|
-
if (!piece.element) {
|
|
2022
|
+
if (!piece.element) { if (callback) callback(); return; }
|
|
1668
2023
|
piece.element.style.opacity = 0;
|
|
1669
|
-
|
|
2024
|
+
// Remove element from DOM after fade completes
|
|
2025
|
+
if (piece.element.parentNode) {
|
|
2026
|
+
piece.element.parentNode.removeChild(piece.element);
|
|
2027
|
+
}
|
|
1670
2028
|
if (callback) callback();
|
|
1671
2029
|
}
|
|
1672
2030
|
};
|
|
1673
2031
|
fade();
|
|
1674
2032
|
}
|
|
1675
2033
|
|
|
2034
|
+
/**
|
|
2035
|
+
* Animate piece capture with configurable style
|
|
2036
|
+
* Uses fluid easing for smooth, connected animations
|
|
2037
|
+
* @param {string} style - Capture style: 'fade', 'shrink', 'instant', 'explode'
|
|
2038
|
+
* @param {number} duration - Animation duration in ms
|
|
2039
|
+
* @param {Function} callback - Callback when complete
|
|
2040
|
+
*/
|
|
2041
|
+
captureAnimate(style, duration, callback) {
|
|
2042
|
+
if (!this.element) {
|
|
2043
|
+
if (callback) callback();
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
const element = this.element;
|
|
2048
|
+
const cleanup = () => {
|
|
2049
|
+
if (element && element.parentNode) {
|
|
2050
|
+
element.parentNode.removeChild(element);
|
|
2051
|
+
}
|
|
2052
|
+
if (callback) callback();
|
|
2053
|
+
};
|
|
2054
|
+
|
|
2055
|
+
// Fluid easing functions
|
|
2056
|
+
const smoothDecel = 'cubic-bezier(0.33, 1, 0.68, 1)';
|
|
2057
|
+
const smoothAccel = 'cubic-bezier(0.32, 0, 0.67, 0)';
|
|
2058
|
+
|
|
2059
|
+
switch (style) {
|
|
2060
|
+
case 'instant':
|
|
2061
|
+
cleanup();
|
|
2062
|
+
break;
|
|
2063
|
+
|
|
2064
|
+
case 'fade':
|
|
2065
|
+
if (element.animate) {
|
|
2066
|
+
// Smooth fade with subtle scale down
|
|
2067
|
+
const anim = element.animate([
|
|
2068
|
+
{ opacity: 1, transform: 'scale(1)' },
|
|
2069
|
+
{ opacity: 0, transform: 'scale(0.9)' }
|
|
2070
|
+
], { duration, easing: smoothDecel, fill: 'forwards' });
|
|
2071
|
+
anim.onfinish = cleanup;
|
|
2072
|
+
} else {
|
|
2073
|
+
element.style.transition = `opacity ${duration}ms ease-out, transform ${duration}ms ease-out`;
|
|
2074
|
+
element.style.opacity = '0';
|
|
2075
|
+
element.style.transform = 'scale(0.9)';
|
|
2076
|
+
setTimeout(cleanup, duration);
|
|
2077
|
+
}
|
|
2078
|
+
break;
|
|
2079
|
+
|
|
2080
|
+
case 'shrink':
|
|
2081
|
+
if (element.animate) {
|
|
2082
|
+
// Smooth shrink with accelerating easing for "sucked in" feel
|
|
2083
|
+
const anim = element.animate([
|
|
2084
|
+
{ transform: 'scale(1)', opacity: 1, offset: 0 },
|
|
2085
|
+
{ transform: 'scale(0.7)', opacity: 0.6, offset: 0.6 },
|
|
2086
|
+
{ transform: 'scale(0)', opacity: 0, offset: 1 }
|
|
2087
|
+
], { duration, easing: smoothAccel, fill: 'forwards' });
|
|
2088
|
+
anim.onfinish = cleanup;
|
|
2089
|
+
} else {
|
|
2090
|
+
element.style.transition = `transform ${duration}ms ease-in, opacity ${duration}ms ease-in`;
|
|
2091
|
+
element.style.transform = 'scale(0)';
|
|
2092
|
+
element.style.opacity = '0';
|
|
2093
|
+
setTimeout(cleanup, duration);
|
|
2094
|
+
}
|
|
2095
|
+
break;
|
|
2096
|
+
|
|
2097
|
+
case 'explode':
|
|
2098
|
+
if (element.animate) {
|
|
2099
|
+
// Subtle expand with smooth deceleration - less dramatic
|
|
2100
|
+
const anim = element.animate([
|
|
2101
|
+
{ transform: 'scale(1)', opacity: 1, offset: 0 },
|
|
2102
|
+
{ transform: 'scale(1.15)', opacity: 0.5, offset: 0.5 },
|
|
2103
|
+
{ transform: 'scale(1.25)', opacity: 0, offset: 1 }
|
|
2104
|
+
], { duration, easing: smoothDecel, fill: 'forwards' });
|
|
2105
|
+
anim.onfinish = cleanup;
|
|
2106
|
+
} else {
|
|
2107
|
+
element.style.transition = `transform ${duration}ms ease-out, opacity ${duration}ms ease-out`;
|
|
2108
|
+
element.style.transform = 'scale(1.25)';
|
|
2109
|
+
element.style.opacity = '0';
|
|
2110
|
+
setTimeout(cleanup, duration);
|
|
2111
|
+
}
|
|
2112
|
+
break;
|
|
2113
|
+
|
|
2114
|
+
default:
|
|
2115
|
+
// Default to fade
|
|
2116
|
+
this.captureAnimate('fade', duration, callback);
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
|
|
1676
2121
|
setDrag(f) {
|
|
1677
2122
|
if (!this.element) { console.debug(`[Piece] setDrag: ${this.id} - element is null`); return; }
|
|
1678
2123
|
|
|
@@ -1712,41 +2157,319 @@ class Piece {
|
|
|
1712
2157
|
}
|
|
1713
2158
|
}
|
|
1714
2159
|
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
2160
|
+
/**
|
|
2161
|
+
* Translate piece to target position with configurable movement style
|
|
2162
|
+
* Uses Chessground-style cubic easing for fluid, natural movement
|
|
2163
|
+
* @param {HTMLElement} to - Target element
|
|
2164
|
+
* @param {number} duration - Animation duration in ms
|
|
2165
|
+
* @param {Function} transition_f - Transition function (unused, for compatibility)
|
|
2166
|
+
* @param {number} speed - Speed factor (unused, for compatibility)
|
|
2167
|
+
* @param {Function} callback - Callback when complete
|
|
2168
|
+
* @param {Object} options - Movement options
|
|
2169
|
+
* @param {string} options.style - Movement style: 'slide', 'arc', 'hop', 'teleport', 'fade'
|
|
2170
|
+
* @param {string} options.easing - CSS easing function
|
|
2171
|
+
* @param {number} options.arcHeight - Arc height ratio (for arc/hop styles)
|
|
2172
|
+
* @param {string} options.landingEffect - Landing effect: 'none', 'bounce', 'pulse', 'settle'
|
|
2173
|
+
* @param {number} options.landingDuration - Landing effect duration in ms
|
|
2174
|
+
*/
|
|
2175
|
+
translate(to, duration, transition_f, speed, callback = null, options = {}) {
|
|
2176
|
+
if (!this.element) {
|
|
2177
|
+
console.debug(`[Piece] translate: ${this.id} - element is null`);
|
|
2178
|
+
if (callback) callback();
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
1725
2181
|
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
2182
|
+
const style = options.style || 'slide';
|
|
2183
|
+
const arcHeight = options.arcHeight || 0.3;
|
|
2184
|
+
const landingEffect = options.landingEffect || 'none';
|
|
2185
|
+
const landingDuration = options.landingDuration || 150;
|
|
2186
|
+
|
|
2187
|
+
// Map easing names to Chessground-style fluid cubic-bezier curves
|
|
2188
|
+
// Default: smooth deceleration like Lichess/Chessground
|
|
2189
|
+
const easingMap = {
|
|
2190
|
+
'ease': 'cubic-bezier(0.33, 1, 0.68, 1)', // Chessground default - smooth decel
|
|
2191
|
+
'linear': 'linear',
|
|
2192
|
+
'ease-in': 'cubic-bezier(0.32, 0, 0.67, 0)', // Smooth acceleration
|
|
2193
|
+
'ease-out': 'cubic-bezier(0.33, 1, 0.68, 1)', // Smooth deceleration (same as default)
|
|
2194
|
+
'ease-in-out': 'cubic-bezier(0.65, 0, 0.35, 1)' // Smooth both ways
|
|
2195
|
+
};
|
|
2196
|
+
const easing = easingMap[options.easing] || easingMap['ease'];
|
|
2197
|
+
|
|
2198
|
+
// Handle teleport (instant)
|
|
2199
|
+
if (style === 'teleport' || duration === 0) {
|
|
2200
|
+
if (callback) callback();
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
// Handle fade style
|
|
2205
|
+
if (style === 'fade') {
|
|
2206
|
+
this._translateFade(to, duration, easing, landingEffect, landingDuration, callback);
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
// Calculate movement vectors
|
|
2211
|
+
const sourceRect = this.element.getBoundingClientRect();
|
|
2212
|
+
const targetRect = to.getBoundingClientRect();
|
|
2213
|
+
const dx = (targetRect.left + targetRect.width / 2) - (sourceRect.left + sourceRect.width / 2);
|
|
2214
|
+
const dy = (targetRect.top + targetRect.height / 2) - (sourceRect.top + sourceRect.height / 2);
|
|
2215
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
2216
|
+
|
|
2217
|
+
// Build keyframes based on style
|
|
2218
|
+
let keyframes;
|
|
2219
|
+
let animationEasing = easing;
|
|
2220
|
+
|
|
2221
|
+
switch (style) {
|
|
2222
|
+
case 'arc':
|
|
2223
|
+
keyframes = this._buildArcKeyframes(dx, dy, distance, arcHeight);
|
|
2224
|
+
// Arc uses linear easing because the curve is in the keyframes
|
|
2225
|
+
animationEasing = 'linear';
|
|
2226
|
+
break;
|
|
2227
|
+
case 'hop':
|
|
2228
|
+
keyframes = this._buildHopKeyframes(dx, dy, distance, arcHeight);
|
|
2229
|
+
// Hop uses ease-out for natural landing feel
|
|
2230
|
+
animationEasing = 'cubic-bezier(0.33, 1, 0.68, 1)';
|
|
2231
|
+
break;
|
|
2232
|
+
case 'slide':
|
|
2233
|
+
default:
|
|
2234
|
+
keyframes = [
|
|
2235
|
+
{ transform: 'translate(0, 0)' },
|
|
2236
|
+
{ transform: `translate(${dx}px, ${dy}px)` }
|
|
2237
|
+
];
|
|
2238
|
+
}
|
|
1730
2239
|
|
|
2240
|
+
// Animate
|
|
1731
2241
|
if (this.element.animate) {
|
|
1732
|
-
|
|
2242
|
+
const animation = this.element.animate(keyframes, {
|
|
1733
2243
|
duration: duration,
|
|
1734
|
-
easing:
|
|
1735
|
-
fill: '
|
|
2244
|
+
easing: animationEasing,
|
|
2245
|
+
fill: 'forwards'
|
|
1736
2246
|
});
|
|
1737
2247
|
|
|
1738
2248
|
animation.onfinish = () => {
|
|
1739
|
-
if (!this.element) {
|
|
1740
|
-
|
|
2249
|
+
if (!this.element) {
|
|
2250
|
+
if (callback) callback();
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
// Cancel animation and move piece in DOM first
|
|
2255
|
+
animation.cancel();
|
|
1741
2256
|
if (this.element) this.element.style = '';
|
|
1742
|
-
|
|
2257
|
+
|
|
2258
|
+
// Callback moves piece to new square in DOM
|
|
2259
|
+
if (callback) callback();
|
|
2260
|
+
|
|
2261
|
+
// Apply landing effect AFTER piece is in new position
|
|
2262
|
+
if (landingEffect !== 'none') {
|
|
2263
|
+
// Small delay to ensure DOM is updated
|
|
2264
|
+
requestAnimationFrame(() => {
|
|
2265
|
+
this._applyLandingEffect(landingEffect, landingDuration);
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
1743
2268
|
};
|
|
1744
2269
|
} else {
|
|
1745
|
-
|
|
2270
|
+
// Fallback for browsers without Web Animations API
|
|
2271
|
+
this.element.style.transition = `transform ${duration}ms ${animationEasing}`;
|
|
1746
2272
|
this.element.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
2273
|
+
setTimeout(() => {
|
|
2274
|
+
if (!this.element) {
|
|
2275
|
+
if (callback) callback();
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
if (this.element) this.element.style = '';
|
|
2279
|
+
if (callback) callback();
|
|
2280
|
+
|
|
2281
|
+
// Apply landing effect after DOM update
|
|
2282
|
+
if (landingEffect !== 'none') {
|
|
2283
|
+
requestAnimationFrame(() => {
|
|
2284
|
+
this._applyLandingEffect(landingEffect, landingDuration);
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
}, duration);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
/**
|
|
2292
|
+
* Build arc-shaped keyframes (smooth parabolic curve)
|
|
2293
|
+
* Uses many keyframes for fluid motion without relying on easing
|
|
2294
|
+
* @private
|
|
2295
|
+
*/
|
|
2296
|
+
_buildArcKeyframes(dx, dy, distance, arcHeight) {
|
|
2297
|
+
const lift = distance * arcHeight;
|
|
2298
|
+
const steps = 10;
|
|
2299
|
+
const keyframes = [];
|
|
2300
|
+
|
|
2301
|
+
for (let i = 0; i <= steps; i++) {
|
|
2302
|
+
const t = i / steps;
|
|
2303
|
+
// Smooth easing for horizontal movement (Chessground-style cubic)
|
|
2304
|
+
const easedT = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
2305
|
+
// Parabolic arc for vertical lift: peaks at t=0.5
|
|
2306
|
+
const arcT = 4 * t * (1 - t); // Parabola: 0 at t=0, 1 at t=0.5, 0 at t=1
|
|
2307
|
+
|
|
2308
|
+
const x = dx * easedT;
|
|
2309
|
+
const y = dy * easedT - lift * arcT;
|
|
2310
|
+
|
|
2311
|
+
keyframes.push({
|
|
2312
|
+
transform: `translate(${x}px, ${y}px)`,
|
|
2313
|
+
offset: t
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
2316
|
+
return keyframes;
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
/**
|
|
2320
|
+
* Build hop-shaped keyframes (knight-like jump with subtle scale)
|
|
2321
|
+
* More aggressive vertical movement, subtle scale for emphasis
|
|
2322
|
+
* @private
|
|
2323
|
+
*/
|
|
2324
|
+
_buildHopKeyframes(dx, dy, distance, arcHeight) {
|
|
2325
|
+
const lift = distance * arcHeight * 1.2;
|
|
2326
|
+
const steps = 12;
|
|
2327
|
+
const keyframes = [];
|
|
2328
|
+
|
|
2329
|
+
for (let i = 0; i <= steps; i++) {
|
|
2330
|
+
const t = i / steps;
|
|
2331
|
+
// Chessground-style cubic easing for smooth acceleration/deceleration
|
|
2332
|
+
const easedT = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
2333
|
+
// Sharp parabolic hop - peaks earlier for snappier feel
|
|
2334
|
+
const hopT = Math.sin(t * Math.PI); // Sine for smooth hop curve
|
|
2335
|
+
// Subtle scale: peaks at 1.03 at the top of the hop
|
|
2336
|
+
const scale = 1 + 0.03 * hopT;
|
|
2337
|
+
|
|
2338
|
+
const x = dx * easedT;
|
|
2339
|
+
const y = dy * easedT - lift * hopT;
|
|
2340
|
+
|
|
2341
|
+
keyframes.push({
|
|
2342
|
+
transform: `translate(${x}px, ${y}px) scale(${scale})`,
|
|
2343
|
+
offset: t
|
|
2344
|
+
});
|
|
2345
|
+
}
|
|
2346
|
+
return keyframes;
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
/**
|
|
2350
|
+
* Translate using fade effect (smooth crossfade with scale)
|
|
2351
|
+
* @private
|
|
2352
|
+
*/
|
|
2353
|
+
_translateFade(to, duration, easing, landingEffect, landingDuration, callback) {
|
|
2354
|
+
const halfDuration = duration / 2;
|
|
2355
|
+
// Use smooth deceleration for fade
|
|
2356
|
+
const fadeEasing = 'cubic-bezier(0.33, 1, 0.68, 1)';
|
|
2357
|
+
|
|
2358
|
+
// Fade out with subtle scale down
|
|
2359
|
+
if (this.element.animate) {
|
|
2360
|
+
const fadeOut = this.element.animate([
|
|
2361
|
+
{ opacity: 1, transform: 'scale(1)' },
|
|
2362
|
+
{ opacity: 0, transform: 'scale(0.95)' }
|
|
2363
|
+
], { duration: halfDuration, easing: fadeEasing, fill: 'forwards' });
|
|
2364
|
+
|
|
2365
|
+
fadeOut.onfinish = () => {
|
|
2366
|
+
if (!this.element) {
|
|
2367
|
+
if (callback) callback();
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
// Move instantly (hidden)
|
|
2371
|
+
fadeOut.cancel();
|
|
2372
|
+
this.element.style.opacity = '0';
|
|
2373
|
+
this.element.style.transform = 'scale(0.95)';
|
|
2374
|
+
|
|
2375
|
+
// Let parent move the piece in DOM, then fade in
|
|
2376
|
+
if (callback) callback();
|
|
2377
|
+
|
|
2378
|
+
// Fade in at new position with subtle scale up
|
|
2379
|
+
requestAnimationFrame(() => {
|
|
2380
|
+
if (!this.element) return;
|
|
2381
|
+
const fadeIn = this.element.animate([
|
|
2382
|
+
{ opacity: 0, transform: 'scale(0.95)' },
|
|
2383
|
+
{ opacity: 1, transform: 'scale(1)' }
|
|
2384
|
+
], { duration: halfDuration, easing: fadeEasing, fill: 'forwards' });
|
|
2385
|
+
|
|
2386
|
+
fadeIn.onfinish = () => {
|
|
2387
|
+
if (this.element) {
|
|
2388
|
+
fadeIn.cancel();
|
|
2389
|
+
this.element.style.opacity = '';
|
|
2390
|
+
this.element.style.transform = '';
|
|
2391
|
+
this._applyLandingEffect(landingEffect, landingDuration);
|
|
2392
|
+
}
|
|
2393
|
+
};
|
|
2394
|
+
});
|
|
2395
|
+
};
|
|
2396
|
+
} else {
|
|
2397
|
+
// Fallback
|
|
2398
|
+
this.element.style.transition = `opacity ${halfDuration}ms ease, transform ${halfDuration}ms ease`;
|
|
2399
|
+
this.element.style.opacity = '0';
|
|
2400
|
+
this.element.style.transform = 'scale(0.95)';
|
|
2401
|
+
setTimeout(() => {
|
|
2402
|
+
if (callback) callback();
|
|
2403
|
+
if (this.element) {
|
|
2404
|
+
this.element.style.opacity = '1';
|
|
2405
|
+
this.element.style.transform = 'scale(1)';
|
|
2406
|
+
setTimeout(() => {
|
|
2407
|
+
if (this.element) this.element.style = '';
|
|
2408
|
+
}, halfDuration);
|
|
2409
|
+
}
|
|
2410
|
+
}, halfDuration);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
/**
|
|
2415
|
+
* Apply landing effect after movement completes
|
|
2416
|
+
* Uses spring-like overshoot easing for natural, connected feel
|
|
2417
|
+
* @private
|
|
2418
|
+
*/
|
|
2419
|
+
_applyLandingEffect(effect, duration, callback) {
|
|
2420
|
+
if (!this.element || effect === 'none') {
|
|
2421
|
+
if (callback) callback();
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
let keyframes;
|
|
2426
|
+
// Overshoot easing for spring-like natural feel
|
|
2427
|
+
let effectEasing = 'cubic-bezier(0.34, 1.56, 0.64, 1)';
|
|
2428
|
+
|
|
2429
|
+
switch (effect) {
|
|
2430
|
+
case 'bounce':
|
|
2431
|
+
// Subtle bounce using spring easing - single smooth bounce
|
|
2432
|
+
keyframes = [
|
|
2433
|
+
{ transform: 'translateY(0)', offset: 0 },
|
|
2434
|
+
{ transform: 'translateY(-5px)', offset: 0.4 },
|
|
2435
|
+
{ transform: 'translateY(0)', offset: 1 }
|
|
2436
|
+
];
|
|
2437
|
+
// Use ease-out for natural deceleration
|
|
2438
|
+
effectEasing = 'cubic-bezier(0.33, 1, 0.68, 1)';
|
|
2439
|
+
break;
|
|
2440
|
+
case 'pulse':
|
|
2441
|
+
// Subtle scale pulse - less aggressive, more refined
|
|
2442
|
+
keyframes = [
|
|
2443
|
+
{ transform: 'scale(1)', offset: 0 },
|
|
2444
|
+
{ transform: 'scale(1.08)', offset: 0.5 },
|
|
2445
|
+
{ transform: 'scale(1)', offset: 1 }
|
|
2446
|
+
];
|
|
2447
|
+
break;
|
|
2448
|
+
case 'settle':
|
|
2449
|
+
// Minimal settle - piece "clicks" into place
|
|
2450
|
+
keyframes = [
|
|
2451
|
+
{ transform: 'scale(1.02)', offset: 0 },
|
|
2452
|
+
{ transform: 'scale(1)', offset: 1 }
|
|
2453
|
+
];
|
|
2454
|
+
effectEasing = 'cubic-bezier(0.33, 1, 0.68, 1)';
|
|
2455
|
+
break;
|
|
2456
|
+
default:
|
|
2457
|
+
if (callback) callback();
|
|
2458
|
+
return;
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
if (this.element.animate) {
|
|
2462
|
+
const animation = this.element.animate(keyframes, {
|
|
2463
|
+
duration: duration,
|
|
2464
|
+
easing: effectEasing,
|
|
2465
|
+
fill: 'forwards'
|
|
2466
|
+
});
|
|
2467
|
+
animation.onfinish = () => {
|
|
2468
|
+
if (this.element) animation.cancel();
|
|
2469
|
+
if (callback) callback();
|
|
2470
|
+
};
|
|
2471
|
+
} else {
|
|
1747
2472
|
if (callback) callback();
|
|
1748
|
-
if (this.element) this.element.style = '';
|
|
1749
|
-
console.debug(`[Piece] translate complete (no animate): ${this.id}`);
|
|
1750
2473
|
}
|
|
1751
2474
|
}
|
|
1752
2475
|
|
|
@@ -1867,9 +2590,9 @@ class Square {
|
|
|
1867
2590
|
}
|
|
1868
2591
|
|
|
1869
2592
|
putPiece(piece) {
|
|
1870
|
-
// If there's already a piece,
|
|
2593
|
+
// If there's already a piece, destroy it to avoid orphaned DOM elements
|
|
1871
2594
|
if (this.piece) {
|
|
1872
|
-
this.removePiece(
|
|
2595
|
+
this.removePiece(false);
|
|
1873
2596
|
}
|
|
1874
2597
|
this.piece = piece;
|
|
1875
2598
|
if (piece && piece.element) {
|
|
@@ -1981,7 +2704,7 @@ class Square {
|
|
|
1981
2704
|
}
|
|
1982
2705
|
|
|
1983
2706
|
getColor() {
|
|
1984
|
-
return this.piece.getColor();
|
|
2707
|
+
return this.piece ? this.piece.getColor() : null;
|
|
1985
2708
|
}
|
|
1986
2709
|
|
|
1987
2710
|
check() {
|
|
@@ -4639,25 +5362,11 @@ class MoveService {
|
|
|
4639
5362
|
to: move.to.id
|
|
4640
5363
|
};
|
|
4641
5364
|
|
|
4642
|
-
console.log('executeMove - move.promotion:', move.promotion);
|
|
4643
|
-
console.log('executeMove - move.hasPromotion():', move.hasPromotion());
|
|
4644
|
-
|
|
4645
5365
|
if (move.hasPromotion()) {
|
|
4646
5366
|
moveOptions.promotion = move.promotion;
|
|
4647
5367
|
}
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
const result = game.move(moveOptions);
|
|
4652
|
-
console.log('executeMove - result:', result);
|
|
4653
|
-
|
|
4654
|
-
// Check what's actually on the board after the move
|
|
4655
|
-
if (result) {
|
|
4656
|
-
const pieceOnDestination = game.get(move.to.id);
|
|
4657
|
-
console.log('executeMove - piece on destination after move:', pieceOnDestination);
|
|
4658
|
-
}
|
|
4659
|
-
|
|
4660
|
-
return result;
|
|
5368
|
+
|
|
5369
|
+
return game.move(moveOptions);
|
|
4661
5370
|
}
|
|
4662
5371
|
|
|
4663
5372
|
/**
|
|
@@ -4667,27 +5376,19 @@ class MoveService {
|
|
|
4667
5376
|
*/
|
|
4668
5377
|
requiresPromotion(move) {
|
|
4669
5378
|
const moveKey = `${move.from.id}->${move.to.id}`;
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
// Get detailed stack trace
|
|
4673
|
-
const stack = new Error().stack.split('\n');
|
|
4674
|
-
console.log('Call stack:', stack[1]);
|
|
4675
|
-
console.log('Caller:', stack[2]);
|
|
4676
|
-
console.log('Caller 2:', stack[3]);
|
|
4677
|
-
console.log('Caller 3:', stack[4]);
|
|
4678
|
-
|
|
4679
|
-
// Track recently processed moves to prevent processing the same move multiple times
|
|
5379
|
+
|
|
5380
|
+
// Track recently processed moves - return cached result for duplicates
|
|
4680
5381
|
const now = Date.now();
|
|
4681
|
-
if (this._recentMoves.has(moveKey)
|
|
4682
|
-
|
|
4683
|
-
|
|
5382
|
+
if (this._recentMoves.has(moveKey) &&
|
|
5383
|
+
this._lastPromotionCheck === moveKey &&
|
|
5384
|
+
this._lastPromotionResult !== null) {
|
|
5385
|
+
return this._lastPromotionResult;
|
|
4684
5386
|
}
|
|
4685
|
-
|
|
5387
|
+
|
|
4686
5388
|
// Debounce identical promotion checks within 100ms
|
|
4687
|
-
if (this._lastPromotionCheck === moveKey &&
|
|
5389
|
+
if (this._lastPromotionCheck === moveKey &&
|
|
4688
5390
|
this._lastPromotionResult !== null &&
|
|
4689
5391
|
now - this._lastPromotionTime < 100) {
|
|
4690
|
-
console.log('Using cached promotion result:', this._lastPromotionResult);
|
|
4691
5392
|
return this._lastPromotionResult;
|
|
4692
5393
|
}
|
|
4693
5394
|
|
|
@@ -4728,53 +5429,41 @@ class MoveService {
|
|
|
4728
5429
|
*/
|
|
4729
5430
|
_doRequiresPromotion(move) {
|
|
4730
5431
|
if (!this.config.onlyLegalMoves) {
|
|
4731
|
-
console.log('Not in legal moves mode, no promotion required');
|
|
4732
5432
|
return false;
|
|
4733
5433
|
}
|
|
4734
|
-
|
|
5434
|
+
|
|
4735
5435
|
const game = this.positionService.getGame();
|
|
4736
5436
|
if (!game) {
|
|
4737
|
-
console.log('No game instance available');
|
|
4738
5437
|
return false;
|
|
4739
5438
|
}
|
|
4740
|
-
|
|
5439
|
+
|
|
4741
5440
|
const piece = game.get(move.from.id);
|
|
4742
5441
|
if (!piece || piece.type !== 'p') {
|
|
4743
|
-
console.log('Not a pawn move, no promotion required');
|
|
4744
5442
|
return false;
|
|
4745
5443
|
}
|
|
4746
|
-
|
|
5444
|
+
|
|
4747
5445
|
const targetRank = move.to.row;
|
|
4748
5446
|
if (targetRank !== 1 && targetRank !== 8) {
|
|
4749
|
-
console.log('Not reaching promotion rank, no promotion required');
|
|
4750
5447
|
return false;
|
|
4751
5448
|
}
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
// Additional validation: check if the pawn can actually reach this square
|
|
5449
|
+
|
|
5450
|
+
// Validate pawn move
|
|
4756
5451
|
if (!this._isPawnMoveValid(move.from, move.to, piece.color)) {
|
|
4757
|
-
console.log('Pawn move not valid, no promotion required');
|
|
4758
5452
|
return false;
|
|
4759
5453
|
}
|
|
4760
|
-
|
|
4761
|
-
//
|
|
4762
|
-
// Check if it's a legal move by using the game's moves() method
|
|
5454
|
+
|
|
5455
|
+
// Check if it's a legal move
|
|
4763
5456
|
const legalMoves = game.moves({ square: move.from.id, verbose: true });
|
|
4764
5457
|
const matchingMove = legalMoves.find(m => m.to === move.to.id);
|
|
4765
|
-
|
|
5458
|
+
|
|
4766
5459
|
if (!matchingMove) {
|
|
4767
|
-
console.log('Move not in legal moves list, no promotion required');
|
|
4768
5460
|
return false;
|
|
4769
5461
|
}
|
|
4770
|
-
|
|
4771
|
-
//
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
console.log('Promotion required:', requiresPromotion);
|
|
4777
|
-
return requiresPromotion;
|
|
5462
|
+
|
|
5463
|
+
// Promotion is required if move has promotion flag or reaches promotion rank
|
|
5464
|
+
return matchingMove.flags.includes('p') ||
|
|
5465
|
+
(piece.color === 'w' && targetRank === 8) ||
|
|
5466
|
+
(piece.color === 'b' && targetRank === 1);
|
|
4778
5467
|
}
|
|
4779
5468
|
|
|
4780
5469
|
/**
|
|
@@ -4790,43 +5479,34 @@ class MoveService {
|
|
|
4790
5479
|
const toRank = to.row;
|
|
4791
5480
|
const fromFile = from.col;
|
|
4792
5481
|
const toFile = to.col;
|
|
4793
|
-
|
|
4794
|
-
console.log(`Validating pawn move: ${from.id} -> ${to.id} (${color})`);
|
|
4795
|
-
console.log(`Ranks: ${fromRank} -> ${toRank}, Files: ${fromFile} -> ${toFile}`);
|
|
4796
|
-
|
|
4797
|
-
// Direction of pawn movement
|
|
5482
|
+
|
|
4798
5483
|
const direction = color === 'w' ? 1 : -1;
|
|
4799
5484
|
const rankDiff = toRank - fromRank;
|
|
4800
5485
|
const fileDiff = Math.abs(toFile - fromFile);
|
|
4801
|
-
|
|
5486
|
+
|
|
4802
5487
|
// Pawn can only move forward
|
|
4803
5488
|
if (rankDiff * direction <= 0) {
|
|
4804
|
-
console.log('Invalid: Pawn cannot move backward or stay in place');
|
|
4805
5489
|
return false;
|
|
4806
5490
|
}
|
|
4807
|
-
|
|
4808
|
-
// Pawn can only move 1
|
|
5491
|
+
|
|
5492
|
+
// Pawn can only move 1-2 ranks
|
|
4809
5493
|
if (Math.abs(rankDiff) > 2) {
|
|
4810
|
-
console.log('Invalid: Pawn cannot move more than 2 ranks');
|
|
4811
5494
|
return false;
|
|
4812
5495
|
}
|
|
4813
|
-
|
|
5496
|
+
|
|
4814
5497
|
// If moving 2 ranks, must be from starting position
|
|
4815
5498
|
if (Math.abs(rankDiff) === 2) {
|
|
4816
5499
|
const startingRank = color === 'w' ? 2 : 7;
|
|
4817
5500
|
if (fromRank !== startingRank) {
|
|
4818
|
-
console.log(`Invalid: Pawn cannot move 2 ranks from rank ${fromRank}`);
|
|
4819
5501
|
return false;
|
|
4820
5502
|
}
|
|
4821
5503
|
}
|
|
4822
|
-
|
|
4823
|
-
// Pawn can only move to adjacent files
|
|
5504
|
+
|
|
5505
|
+
// Pawn can only move to adjacent files or same file
|
|
4824
5506
|
if (fileDiff > 1) {
|
|
4825
|
-
console.log('Invalid: Pawn cannot move more than 1 file');
|
|
4826
5507
|
return false;
|
|
4827
5508
|
}
|
|
4828
|
-
|
|
4829
|
-
console.log('Pawn move validation passed');
|
|
5509
|
+
|
|
4830
5510
|
return true;
|
|
4831
5511
|
}
|
|
4832
5512
|
|
|
@@ -4867,24 +5547,17 @@ class MoveService {
|
|
|
4867
5547
|
* @private
|
|
4868
5548
|
*/
|
|
4869
5549
|
_showPromotionInColumn(targetSquare, piece, squares, onPromotionSelect, onPromotionCancel) {
|
|
4870
|
-
console.log('Setting up promotion for', targetSquare.id, 'piece color:', piece.color);
|
|
4871
|
-
|
|
4872
5550
|
// Set up promotion choices starting from border row
|
|
4873
5551
|
PROMOTION_PIECES.forEach((pieceType, index) => {
|
|
4874
5552
|
const choiceSquare = this._findPromotionSquare(targetSquare, index, squares);
|
|
4875
|
-
|
|
5553
|
+
|
|
4876
5554
|
if (choiceSquare) {
|
|
4877
5555
|
const pieceId = pieceType + piece.color;
|
|
4878
5556
|
const piecePath = this._getPiecePathForPromotion(pieceId);
|
|
4879
|
-
|
|
4880
|
-
console.log('Setting up promotion choice:', pieceType, 'on square:', choiceSquare.id);
|
|
4881
|
-
|
|
5557
|
+
|
|
4882
5558
|
choiceSquare.putPromotion(piecePath, () => {
|
|
4883
|
-
console.log('Promotion choice selected:', pieceType);
|
|
4884
5559
|
onPromotionSelect(pieceType);
|
|
4885
5560
|
});
|
|
4886
|
-
} else {
|
|
4887
|
-
console.log('Could not find square for promotion choice:', pieceType, 'index:', index);
|
|
4888
5561
|
}
|
|
4889
5562
|
});
|
|
4890
5563
|
|
|
@@ -4911,40 +5584,28 @@ class MoveService {
|
|
|
4911
5584
|
_findPromotionSquare(targetSquare, index, squares) {
|
|
4912
5585
|
const col = targetSquare.col;
|
|
4913
5586
|
const baseRow = targetSquare.row;
|
|
4914
|
-
|
|
4915
|
-
console.log('Looking for promotion square - target:', targetSquare.id, 'index:', index, 'col:', col, 'baseRow:', baseRow);
|
|
4916
|
-
|
|
5587
|
+
|
|
4917
5588
|
// Calculate row based on index and promotion direction
|
|
4918
|
-
// Start from the border row (1 or 8) and go inward
|
|
4919
5589
|
let row;
|
|
4920
5590
|
if (baseRow === 8) {
|
|
4921
|
-
// White promotion: start from row 8 and go down
|
|
4922
5591
|
row = 8 - index;
|
|
4923
5592
|
} else if (baseRow === 1) {
|
|
4924
|
-
// Black promotion: start from row 1 and go up
|
|
4925
5593
|
row = 1 + index;
|
|
4926
5594
|
} else {
|
|
4927
|
-
console.log('Invalid promotion row:', baseRow);
|
|
4928
5595
|
return null;
|
|
4929
5596
|
}
|
|
4930
|
-
|
|
4931
|
-
console.log('Calculated row:', row);
|
|
4932
|
-
|
|
4933
|
-
// Ensure row is within bounds
|
|
5597
|
+
|
|
4934
5598
|
if (row < 1 || row > 8) {
|
|
4935
|
-
console.log('Row out of bounds:', row);
|
|
4936
5599
|
return null;
|
|
4937
5600
|
}
|
|
4938
|
-
|
|
5601
|
+
|
|
4939
5602
|
// Find square by row/col
|
|
4940
5603
|
for (const square of Object.values(squares)) {
|
|
4941
5604
|
if (square.col === col && square.row === row) {
|
|
4942
|
-
console.log('Found promotion square:', square.id);
|
|
4943
5605
|
return square;
|
|
4944
5606
|
}
|
|
4945
5607
|
}
|
|
4946
|
-
|
|
4947
|
-
console.log('No square found for col:', col, 'row:', row);
|
|
5608
|
+
|
|
4948
5609
|
return null;
|
|
4949
5610
|
}
|
|
4950
5611
|
|
|
@@ -5180,50 +5841,61 @@ class PieceService {
|
|
|
5180
5841
|
piece.setDrag(dragFunction(square, piece));
|
|
5181
5842
|
}
|
|
5182
5843
|
|
|
5183
|
-
if (fade
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5844
|
+
if (fade) {
|
|
5845
|
+
const appearanceStyle = this.config.appearanceStyle || 'fade';
|
|
5846
|
+
const duration = this.config.appearanceTime || this.config.fadeTime || 200;
|
|
5847
|
+
|
|
5848
|
+
if (duration > 0 && appearanceStyle !== 'instant') {
|
|
5849
|
+
piece.appearAnimate(appearanceStyle, duration, () => {
|
|
5850
|
+
piece.visible();
|
|
5851
|
+
if (callback) callback();
|
|
5852
|
+
});
|
|
5853
|
+
} else {
|
|
5854
|
+
piece.visible();
|
|
5855
|
+
if (callback) callback();
|
|
5856
|
+
}
|
|
5190
5857
|
} else {
|
|
5858
|
+
piece.visible();
|
|
5191
5859
|
if (callback) callback();
|
|
5192
5860
|
}
|
|
5193
|
-
|
|
5194
|
-
piece.visible();
|
|
5195
5861
|
}
|
|
5196
5862
|
|
|
5197
5863
|
/**
|
|
5198
|
-
* Removes a piece from a square with
|
|
5864
|
+
* Removes a piece from a square with configurable capture animation
|
|
5199
5865
|
* @param {Square} square - Source square
|
|
5200
|
-
* @param {boolean} [
|
|
5866
|
+
* @param {boolean} [animate=true] - Whether to animate the removal
|
|
5201
5867
|
* @param {Function} [callback] - Callback when animation completes
|
|
5202
5868
|
* @returns {Piece} The removed piece
|
|
5203
5869
|
* @throws {PieceError} When square has no piece to remove
|
|
5204
5870
|
*/
|
|
5205
|
-
removePieceFromSquare(square,
|
|
5871
|
+
removePieceFromSquare(square, animate = true, callback) {
|
|
5206
5872
|
console.debug(`[PieceService] removePieceFromSquare: ${square.id}`);
|
|
5207
5873
|
square.check();
|
|
5208
5874
|
|
|
5209
5875
|
const piece = square.piece;
|
|
5210
5876
|
if (!piece) {
|
|
5211
5877
|
if (callback) callback();
|
|
5212
|
-
|
|
5878
|
+
return null;
|
|
5213
5879
|
}
|
|
5214
5880
|
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5881
|
+
// Always remove piece reference synchronously to prevent stale state
|
|
5882
|
+
// when a new update starts before the animation completes
|
|
5883
|
+
square.piece = null;
|
|
5884
|
+
|
|
5885
|
+
const captureStyle = this.config.captureStyle || 'fade';
|
|
5886
|
+
const duration = this.config.captureTime || this.config.fadeTime || 200;
|
|
5887
|
+
|
|
5888
|
+
if (animate && duration > 0) {
|
|
5889
|
+
// Animate visual element, then destroy
|
|
5890
|
+
piece.captureAnimate(captureStyle, duration, () => {
|
|
5891
|
+
piece.destroy();
|
|
5892
|
+
if (callback) callback();
|
|
5893
|
+
});
|
|
5222
5894
|
} else {
|
|
5895
|
+
piece.destroy();
|
|
5223
5896
|
if (callback) callback();
|
|
5224
5897
|
}
|
|
5225
5898
|
|
|
5226
|
-
square.removePiece();
|
|
5227
5899
|
return piece;
|
|
5228
5900
|
}
|
|
5229
5901
|
|
|
@@ -5242,12 +5914,22 @@ class PieceService {
|
|
|
5242
5914
|
return;
|
|
5243
5915
|
}
|
|
5244
5916
|
|
|
5917
|
+
// Build movement options from config
|
|
5918
|
+
const moveOptions = {
|
|
5919
|
+
style: this.config.moveStyle || 'slide',
|
|
5920
|
+
easing: this.config.moveEasing || this.config.moveAnimation || 'ease',
|
|
5921
|
+
arcHeight: this.config.moveArcHeight || 0.3,
|
|
5922
|
+
landingEffect: this.config.landingEffect || 'none',
|
|
5923
|
+
landingDuration: this.config.landingDuration || 150
|
|
5924
|
+
};
|
|
5925
|
+
|
|
5245
5926
|
piece.translate(
|
|
5246
5927
|
targetSquare,
|
|
5247
5928
|
duration,
|
|
5248
5929
|
this._getTransitionTimingFunction(),
|
|
5249
5930
|
this.config.moveAnimation,
|
|
5250
|
-
callback
|
|
5931
|
+
callback,
|
|
5932
|
+
moveOptions
|
|
5251
5933
|
);
|
|
5252
5934
|
}
|
|
5253
5935
|
|
|
@@ -5261,8 +5943,8 @@ class PieceService {
|
|
|
5261
5943
|
*/
|
|
5262
5944
|
translatePiece(move, removeTarget, animate, dragFunction = null, callback = null) {
|
|
5263
5945
|
console.debug(`[PieceService] translatePiece: ${move.piece.id} from ${move.from.id} to ${move.to.id}`);
|
|
5264
|
-
if (!move.piece) {
|
|
5265
|
-
console.warn('PieceService.translatePiece: move.piece is null, skipping translation');
|
|
5946
|
+
if (!move.piece || !move.piece.element) {
|
|
5947
|
+
console.warn('PieceService.translatePiece: move.piece or element is null, skipping translation');
|
|
5266
5948
|
if (callback) callback();
|
|
5267
5949
|
return;
|
|
5268
5950
|
}
|
|
@@ -5274,6 +5956,12 @@ class PieceService {
|
|
|
5274
5956
|
}
|
|
5275
5957
|
|
|
5276
5958
|
const changeSquareCallback = () => {
|
|
5959
|
+
// If piece element was destroyed (e.g., by a newer update), skip
|
|
5960
|
+
if (!move.piece.element || !move.piece.element.parentNode) {
|
|
5961
|
+
if (callback) callback();
|
|
5962
|
+
return;
|
|
5963
|
+
}
|
|
5964
|
+
|
|
5277
5965
|
// Check if piece still exists and is on the source square
|
|
5278
5966
|
if (move.from.piece === move.piece) {
|
|
5279
5967
|
move.from.removePiece(true); // Preserve the piece when moving
|
|
@@ -7652,7 +8340,8 @@ let Chessboard$1 = class Chessboard {
|
|
|
7652
8340
|
this._handleConstructorError(error);
|
|
7653
8341
|
}
|
|
7654
8342
|
this._undoneMoves = [];
|
|
7655
|
-
this.
|
|
8343
|
+
this._destroyed = false;
|
|
8344
|
+
this._animationTimeouts = [];
|
|
7656
8345
|
}
|
|
7657
8346
|
|
|
7658
8347
|
/**
|
|
@@ -7764,6 +8453,11 @@ let Chessboard$1 = class Chessboard {
|
|
|
7764
8453
|
this._buildSquares();
|
|
7765
8454
|
this._addListeners();
|
|
7766
8455
|
this._updateBoardPieces(true, true); // Initial position load
|
|
8456
|
+
|
|
8457
|
+
// Apply flipped class if initial orientation is black
|
|
8458
|
+
if (this.coordinateService.getOrientation() === 'b' && this.boardService.element) {
|
|
8459
|
+
this.boardService.element.classList.add('flipped');
|
|
8460
|
+
}
|
|
7767
8461
|
}
|
|
7768
8462
|
|
|
7769
8463
|
/**
|
|
@@ -7792,9 +8486,7 @@ let Chessboard$1 = class Chessboard {
|
|
|
7792
8486
|
* Best practice: always remove squares (destroy JS/DOM) before clearing the board container.
|
|
7793
8487
|
*/
|
|
7794
8488
|
_buildBoard() {
|
|
7795
|
-
console.log('CHIAMATO: _buildBoard');
|
|
7796
8489
|
if (this._isUndoRedo) {
|
|
7797
|
-
console.log('SKIP _buildBoard per undo/redo');
|
|
7798
8490
|
return;
|
|
7799
8491
|
}
|
|
7800
8492
|
// Forza la pulizia completa del contenitore board (DOM)
|
|
@@ -7814,9 +8506,7 @@ let Chessboard$1 = class Chessboard {
|
|
|
7814
8506
|
* @private
|
|
7815
8507
|
*/
|
|
7816
8508
|
_buildSquares() {
|
|
7817
|
-
console.log('CHIAMATO: _buildSquares');
|
|
7818
8509
|
if (this._isUndoRedo) {
|
|
7819
|
-
console.log('SKIP _buildSquares per undo/redo');
|
|
7820
8510
|
return;
|
|
7821
8511
|
}
|
|
7822
8512
|
if (this.boardService && this.boardService.removeSquares) {
|
|
@@ -8029,6 +8719,14 @@ let Chessboard$1 = class Chessboard {
|
|
|
8029
8719
|
const isEnPassant = this.moveService.isEnPassant(gameMove);
|
|
8030
8720
|
|
|
8031
8721
|
if (animate && move.from.piece) {
|
|
8722
|
+
// For simultaneous castle, start rook animation alongside the king
|
|
8723
|
+
const isSimultaneousCastle = isCastle && this.config.animationStyle === 'simultaneous';
|
|
8724
|
+
if (isSimultaneousCastle) {
|
|
8725
|
+
setTimeout(() => {
|
|
8726
|
+
this._handleCastleMove(gameMove, true);
|
|
8727
|
+
}, this.config.simultaneousAnimationDelay);
|
|
8728
|
+
}
|
|
8729
|
+
|
|
8032
8730
|
this.pieceService.translatePiece(
|
|
8033
8731
|
move,
|
|
8034
8732
|
!!move.to.piece, // was there a capture?
|
|
@@ -8036,24 +8734,22 @@ let Chessboard$1 = class Chessboard {
|
|
|
8036
8734
|
this._createDragFunction.bind(this),
|
|
8037
8735
|
() => {
|
|
8038
8736
|
// After the main piece animation completes...
|
|
8039
|
-
|
|
8737
|
+
// For sequential castle, animate rook AFTER king finishes
|
|
8738
|
+
// For simultaneous, rook was already animated above - don't animate again
|
|
8739
|
+
if (isCastle && !isSimultaneousCastle) {
|
|
8040
8740
|
this._handleSpecialMoveAnimation(gameMove);
|
|
8041
8741
|
} else if (isEnPassant) {
|
|
8042
8742
|
this._handleSpecialMoveAnimation(gameMove);
|
|
8043
8743
|
}
|
|
8044
8744
|
// Notify user that the move is fully complete
|
|
8045
8745
|
this.config.onMoveEnd(gameMove);
|
|
8046
|
-
//
|
|
8047
|
-
|
|
8746
|
+
// For simultaneous castle, the rook callback will handle the final sync
|
|
8747
|
+
// to avoid interfering with the ongoing rook animation
|
|
8748
|
+
if (!isSimultaneousCastle) {
|
|
8749
|
+
this._updateBoardPieces(false);
|
|
8750
|
+
}
|
|
8048
8751
|
}
|
|
8049
8752
|
);
|
|
8050
|
-
|
|
8051
|
-
// For simultaneous castle, animate the rook alongside the king
|
|
8052
|
-
if (isCastle && this.config.animationStyle === 'simultaneous') {
|
|
8053
|
-
setTimeout(() => {
|
|
8054
|
-
this._handleCastleMove(gameMove, true);
|
|
8055
|
-
}, this.config.simultaneousAnimationDelay);
|
|
8056
|
-
}
|
|
8057
8753
|
} else {
|
|
8058
8754
|
// If not animating, handle special moves immediately and update the board
|
|
8059
8755
|
if (isCastle) {
|
|
@@ -8106,12 +8802,9 @@ let Chessboard$1 = class Chessboard {
|
|
|
8106
8802
|
const rookToSquare = this.boardService.getSquare(rookMove.to);
|
|
8107
8803
|
|
|
8108
8804
|
if (!rookFromSquare || !rookToSquare || !rookFromSquare.piece) {
|
|
8109
|
-
console.warn('Castle rook move failed - squares or piece not found');
|
|
8110
8805
|
return;
|
|
8111
8806
|
}
|
|
8112
8807
|
|
|
8113
|
-
console.log(`Castle: moving rook from ${rookMove.from} to ${rookMove.to}`);
|
|
8114
|
-
|
|
8115
8808
|
if (animate) {
|
|
8116
8809
|
// Always use translatePiece for smooth sliding animation
|
|
8117
8810
|
const rookPiece = rookFromSquare.piece;
|
|
@@ -8143,12 +8836,9 @@ let Chessboard$1 = class Chessboard {
|
|
|
8143
8836
|
|
|
8144
8837
|
const capturedSquareObj = this.boardService.getSquare(capturedSquare);
|
|
8145
8838
|
if (!capturedSquareObj || !capturedSquareObj.piece) {
|
|
8146
|
-
console.warn('En passant captured square not found or empty');
|
|
8147
8839
|
return;
|
|
8148
8840
|
}
|
|
8149
8841
|
|
|
8150
|
-
console.log(`En passant: removing captured pawn from ${capturedSquare}`);
|
|
8151
|
-
|
|
8152
8842
|
if (animate) {
|
|
8153
8843
|
// Animate the captured pawn removal
|
|
8154
8844
|
this.pieceService.removePieceFromSquare(capturedSquareObj, true);
|
|
@@ -8169,10 +8859,9 @@ let Chessboard$1 = class Chessboard {
|
|
|
8169
8859
|
* @param {boolean} [isPositionLoad=false] - Whether this is a position load
|
|
8170
8860
|
*/
|
|
8171
8861
|
_updateBoardPieces(animation = false, isPositionLoad = false) {
|
|
8172
|
-
|
|
8862
|
+
if (this._destroyed) return;
|
|
8173
8863
|
// Check if services are available
|
|
8174
8864
|
if (!this.positionService || !this.moveService || !this.eventService) {
|
|
8175
|
-
console.log('Cannot update board pieces - services not available');
|
|
8176
8865
|
return;
|
|
8177
8866
|
}
|
|
8178
8867
|
|
|
@@ -8235,33 +8924,24 @@ let Chessboard$1 = class Chessboard {
|
|
|
8235
8924
|
* @param {boolean} [isPositionLoad=false] - Whether this is a position load (affects delay)
|
|
8236
8925
|
*/
|
|
8237
8926
|
_doUpdateBoardPieces(animation = false, isPositionLoad = false) {
|
|
8927
|
+
if (this._destroyed) return;
|
|
8238
8928
|
// Skip update if we're in the middle of a promotion
|
|
8239
8929
|
if (this._isPromoting) {
|
|
8240
|
-
console.log('Skipping board update during promotion');
|
|
8241
8930
|
return;
|
|
8242
8931
|
}
|
|
8243
8932
|
|
|
8244
8933
|
// Check if services are available
|
|
8245
8934
|
if (!this.positionService || !this.positionService.getGame()) {
|
|
8246
|
-
console.log('Cannot update board pieces - position service not available');
|
|
8247
8935
|
return;
|
|
8248
8936
|
}
|
|
8249
8937
|
|
|
8250
8938
|
const squares = this.boardService.getAllSquares();
|
|
8251
8939
|
const gameStateBefore = this.positionService.getGame().fen();
|
|
8252
|
-
|
|
8253
|
-
console.log('_doUpdateBoardPieces - current FEN:', gameStateBefore);
|
|
8254
|
-
console.log('_doUpdateBoardPieces - animation:', animation, 'style:', this.config.animationStyle, 'isPositionLoad:', isPositionLoad);
|
|
8255
|
-
|
|
8256
|
-
// Determine which animation style to use
|
|
8257
8940
|
const useSimultaneous = this.config.animationStyle === 'simultaneous';
|
|
8258
|
-
console.log('_doUpdateBoardPieces - useSimultaneous:', useSimultaneous);
|
|
8259
8941
|
|
|
8260
8942
|
if (useSimultaneous) {
|
|
8261
|
-
|
|
8262
|
-
this._doSimultaneousUpdate(squares, gameStateBefore, isPositionLoad);
|
|
8943
|
+
this._doSimultaneousUpdate(squares, gameStateBefore, isPositionLoad, animation);
|
|
8263
8944
|
} else {
|
|
8264
|
-
console.log('Using sequential animation');
|
|
8265
8945
|
this._doSequentialUpdate(squares, gameStateBefore, animation);
|
|
8266
8946
|
}
|
|
8267
8947
|
}
|
|
@@ -8274,7 +8954,23 @@ let Chessboard$1 = class Chessboard {
|
|
|
8274
8954
|
* @param {boolean} animation - Whether to animate
|
|
8275
8955
|
*/
|
|
8276
8956
|
_doSequentialUpdate(squares, gameStateBefore, animation) {
|
|
8277
|
-
//
|
|
8957
|
+
// Cancel running animations and clean orphaned elements
|
|
8958
|
+
Object.values(squares).forEach(square => {
|
|
8959
|
+
const imgs = square.element.querySelectorAll('img.piece');
|
|
8960
|
+
imgs.forEach(img => {
|
|
8961
|
+
if (img.getAnimations) {
|
|
8962
|
+
img.getAnimations().forEach(anim => anim.cancel());
|
|
8963
|
+
}
|
|
8964
|
+
if (!square.piece || img !== square.piece.element) {
|
|
8965
|
+
img.remove();
|
|
8966
|
+
}
|
|
8967
|
+
});
|
|
8968
|
+
if (square.piece && square.piece.element) {
|
|
8969
|
+
square.piece.element.style = '';
|
|
8970
|
+
square.piece.element.style.opacity = '1';
|
|
8971
|
+
}
|
|
8972
|
+
});
|
|
8973
|
+
|
|
8278
8974
|
const expectedMap = {};
|
|
8279
8975
|
Object.values(squares).forEach(square => {
|
|
8280
8976
|
expectedMap[square.id] = this.positionService.getGamePieceId(square.id);
|
|
@@ -8290,12 +8986,13 @@ let Chessboard$1 = class Chessboard {
|
|
|
8290
8986
|
return;
|
|
8291
8987
|
}
|
|
8292
8988
|
|
|
8293
|
-
//
|
|
8989
|
+
// Remove current piece if it doesn't match expected
|
|
8294
8990
|
if (currentPiece && currentPieceId !== expectedPieceId) {
|
|
8295
|
-
|
|
8991
|
+
// Always remove synchronously to avoid race condition with addition
|
|
8992
|
+
this.pieceService.removePieceFromSquare(square, false);
|
|
8296
8993
|
}
|
|
8297
8994
|
|
|
8298
|
-
//
|
|
8995
|
+
// Add expected piece if it doesn't match current
|
|
8299
8996
|
if (expectedPieceId && currentPieceId !== expectedPieceId) {
|
|
8300
8997
|
const newPiece = this.pieceService.convertPiece(expectedPieceId);
|
|
8301
8998
|
this.pieceService.addPieceOnSquare(
|
|
@@ -8320,9 +9017,43 @@ let Chessboard$1 = class Chessboard {
|
|
|
8320
9017
|
* @param {Object} squares - All squares
|
|
8321
9018
|
* @param {string} gameStateBefore - Game state before update
|
|
8322
9019
|
* @param {boolean} [isPositionLoad=false] - Whether this is a position load
|
|
9020
|
+
* @param {boolean} [animation=true] - Whether to animate
|
|
8323
9021
|
*/
|
|
8324
|
-
_doSimultaneousUpdate(squares, gameStateBefore, isPositionLoad = false) {
|
|
8325
|
-
//
|
|
9022
|
+
_doSimultaneousUpdate(squares, gameStateBefore, isPositionLoad = false, animation = true) {
|
|
9023
|
+
// Increment generation to invalidate stale animation callbacks
|
|
9024
|
+
this._updateGeneration = (this._updateGeneration || 0) + 1;
|
|
9025
|
+
const generation = this._updateGeneration;
|
|
9026
|
+
|
|
9027
|
+
// Cancel pending animation timeouts from previous update
|
|
9028
|
+
if (this._animationTimeouts) {
|
|
9029
|
+
this._animationTimeouts.forEach(tid => clearTimeout(tid));
|
|
9030
|
+
this._animationTimeouts = [];
|
|
9031
|
+
}
|
|
9032
|
+
|
|
9033
|
+
// Cancel all running animations and force-sync DOM state
|
|
9034
|
+
Object.values(squares).forEach(square => {
|
|
9035
|
+
const imgs = square.element.querySelectorAll('img.piece');
|
|
9036
|
+
imgs.forEach(img => {
|
|
9037
|
+
// Cancel all Web Animations on this element so onfinish callbacks don't fire
|
|
9038
|
+
if (img.getAnimations) {
|
|
9039
|
+
img.getAnimations().forEach(anim => anim.cancel());
|
|
9040
|
+
}
|
|
9041
|
+
// Remove orphaned images not matching current piece
|
|
9042
|
+
if (!square.piece || img !== square.piece.element) {
|
|
9043
|
+
img.remove();
|
|
9044
|
+
}
|
|
9045
|
+
});
|
|
9046
|
+
// Reset current piece element to clean state (remove animation artifacts)
|
|
9047
|
+
if (square.piece && square.piece.element) {
|
|
9048
|
+
square.piece.element.style = '';
|
|
9049
|
+
square.piece.element.style.opacity = '1';
|
|
9050
|
+
// Ensure element is attached to correct square
|
|
9051
|
+
if (!square.element.contains(square.piece.element)) {
|
|
9052
|
+
square.element.appendChild(square.piece.element);
|
|
9053
|
+
}
|
|
9054
|
+
}
|
|
9055
|
+
});
|
|
9056
|
+
|
|
8326
9057
|
const currentMap = {};
|
|
8327
9058
|
const expectedMap = {};
|
|
8328
9059
|
|
|
@@ -8348,35 +9079,16 @@ let Chessboard$1 = class Chessboard {
|
|
|
8348
9079
|
const animationDelay = isPositionLoad ? 0 : this.config.simultaneousAnimationDelay;
|
|
8349
9080
|
let animationIndex = 0;
|
|
8350
9081
|
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
if (totalAnimations === 0) {
|
|
8356
|
-
this._addListeners();
|
|
8357
|
-
const gameStateAfter = this.positionService.getGame().fen();
|
|
8358
|
-
if (gameStateBefore !== gameStateAfter) {
|
|
8359
|
-
this.config.onChange(gameStateAfter);
|
|
8360
|
-
}
|
|
8361
|
-
return;
|
|
8362
|
-
}
|
|
8363
|
-
|
|
8364
|
-
const onAnimationComplete = () => {
|
|
8365
|
-
animationsCompleted++;
|
|
8366
|
-
if (animationsCompleted === totalAnimations) {
|
|
8367
|
-
this._addListeners();
|
|
8368
|
-
const gameStateAfter = this.positionService.getGame().fen();
|
|
8369
|
-
if (gameStateBefore !== gameStateAfter) {
|
|
8370
|
-
this.config.onChange(gameStateAfter);
|
|
8371
|
-
}
|
|
8372
|
-
}
|
|
8373
|
-
};
|
|
9082
|
+
// First pass: compute matching for all piece types
|
|
9083
|
+
const allRemovals = [];
|
|
9084
|
+
const allAdditions = [];
|
|
9085
|
+
const allMoves = [];
|
|
8374
9086
|
|
|
8375
9087
|
Object.keys(expectedMap).forEach(key => {
|
|
8376
9088
|
const fromList = (currentMap[key] || []).slice();
|
|
8377
9089
|
const toList = expectedMap[key].slice();
|
|
8378
9090
|
|
|
8379
|
-
//
|
|
9091
|
+
// Build distance matrix
|
|
8380
9092
|
const distances = [];
|
|
8381
9093
|
for (let i = 0; i < fromList.length; i++) {
|
|
8382
9094
|
distances[i] = [];
|
|
@@ -8386,10 +9098,9 @@ let Chessboard$1 = class Chessboard {
|
|
|
8386
9098
|
}
|
|
8387
9099
|
}
|
|
8388
9100
|
|
|
8389
|
-
//
|
|
9101
|
+
// Greedy matching: pair closest pieces
|
|
8390
9102
|
const fromMatched = new Array(fromList.length).fill(false);
|
|
8391
9103
|
const toMatched = new Array(toList.length).fill(false);
|
|
8392
|
-
const moves = [];
|
|
8393
9104
|
|
|
8394
9105
|
while (true) {
|
|
8395
9106
|
let minDist = Infinity, minI = -1, minJ = -1;
|
|
@@ -8405,58 +9116,140 @@ let Chessboard$1 = class Chessboard {
|
|
|
8405
9116
|
}
|
|
8406
9117
|
}
|
|
8407
9118
|
if (minI === -1 || minJ === -1) break;
|
|
8408
|
-
|
|
9119
|
+
fromMatched[minI] = true;
|
|
9120
|
+
toMatched[minJ] = true;
|
|
9121
|
+
// Skip unchanged pieces (same square)
|
|
8409
9122
|
if (fromList[minI].square === toList[minJ].square) {
|
|
8410
|
-
fromMatched[minI] = true;
|
|
8411
|
-
toMatched[minJ] = true;
|
|
8412
9123
|
continue;
|
|
8413
9124
|
}
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
9125
|
+
allMoves.push({ from: fromList[minI].square, to: toList[minJ].square, piece: fromList[minI].square.piece });
|
|
9126
|
+
}
|
|
9127
|
+
|
|
9128
|
+
// Collect unmatched current pieces (to remove)
|
|
9129
|
+
for (let i = 0; i < fromList.length; i++) {
|
|
9130
|
+
if (!fromMatched[i]) {
|
|
9131
|
+
allRemovals.push(fromList[i].square);
|
|
9132
|
+
}
|
|
9133
|
+
}
|
|
9134
|
+
|
|
9135
|
+
// Collect unmatched expected pieces (to add)
|
|
9136
|
+
for (let j = 0; j < toList.length; j++) {
|
|
9137
|
+
if (!toMatched[j]) {
|
|
9138
|
+
allAdditions.push({ square: toList[j].square, key });
|
|
9139
|
+
}
|
|
9140
|
+
}
|
|
9141
|
+
});
|
|
9142
|
+
|
|
9143
|
+
// Also count removals for pieces whose type doesn't exist in expectedMap
|
|
9144
|
+
Object.keys(currentMap).forEach(key => {
|
|
9145
|
+
if (!expectedMap[key]) {
|
|
9146
|
+
currentMap[key].forEach(entry => {
|
|
9147
|
+
allRemovals.push(entry.square);
|
|
9148
|
+
});
|
|
9149
|
+
}
|
|
9150
|
+
});
|
|
9151
|
+
|
|
9152
|
+
// Count only actual animations
|
|
9153
|
+
totalAnimations = allRemovals.length + allAdditions.length + allMoves.length;
|
|
9154
|
+
|
|
9155
|
+
if (totalAnimations === 0) {
|
|
9156
|
+
this._addListeners();
|
|
9157
|
+
const gameStateAfter = this.positionService.getGame().fen();
|
|
9158
|
+
if (gameStateBefore !== gameStateAfter) {
|
|
9159
|
+
this.config.onChange(gameStateAfter);
|
|
8418
9160
|
}
|
|
9161
|
+
return;
|
|
9162
|
+
}
|
|
8419
9163
|
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
}, animationIndex * animationDelay);
|
|
8426
|
-
animationIndex++;
|
|
8427
|
-
}
|
|
9164
|
+
// Detach moving pieces from source squares BEFORE any removals/additions
|
|
9165
|
+
// This prevents additions to a move's source square from destroying the piece
|
|
9166
|
+
allMoves.forEach(move => {
|
|
9167
|
+
if (move.from.piece === move.piece) {
|
|
9168
|
+
move.from.removePiece(true); // preserve element, just detach reference
|
|
8428
9169
|
}
|
|
9170
|
+
});
|
|
8429
9171
|
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
9172
|
+
// No animation: apply all changes synchronously
|
|
9173
|
+
if (!animation) {
|
|
9174
|
+
allRemovals.forEach(square => {
|
|
9175
|
+
this.pieceService.removePieceFromSquare(square, false);
|
|
9176
|
+
});
|
|
9177
|
+
allMoves.forEach(move => {
|
|
9178
|
+
this.pieceService.translatePiece(
|
|
9179
|
+
move, false, false, this._createDragFunction.bind(this)
|
|
9180
|
+
);
|
|
9181
|
+
});
|
|
9182
|
+
allAdditions.forEach(({ square, key }) => {
|
|
9183
|
+
const newPiece = this.pieceService.convertPiece(key);
|
|
9184
|
+
this.pieceService.addPieceOnSquare(
|
|
9185
|
+
square, newPiece, false, this._createDragFunction.bind(this)
|
|
9186
|
+
);
|
|
9187
|
+
});
|
|
9188
|
+
this._addListeners();
|
|
9189
|
+
const gameStateAfter = this.positionService.getGame().fen();
|
|
9190
|
+
if (gameStateBefore !== gameStateAfter) {
|
|
9191
|
+
this.config.onChange(gameStateAfter);
|
|
9192
|
+
}
|
|
9193
|
+
return;
|
|
9194
|
+
}
|
|
9195
|
+
|
|
9196
|
+
// Animated path
|
|
9197
|
+
if (!this._animationTimeouts) this._animationTimeouts = [];
|
|
9198
|
+
|
|
9199
|
+
const onAnimationComplete = () => {
|
|
9200
|
+
// Ignore callbacks from stale/destroyed boards
|
|
9201
|
+
if (this._destroyed || this._updateGeneration !== generation) return;
|
|
9202
|
+
animationsCompleted++;
|
|
9203
|
+
if (animationsCompleted === totalAnimations) {
|
|
9204
|
+
this._addListeners();
|
|
9205
|
+
const gameStateAfter = this.positionService.getGame().fen();
|
|
9206
|
+
if (gameStateBefore !== gameStateAfter) {
|
|
9207
|
+
this.config.onChange(gameStateAfter);
|
|
8444
9208
|
}
|
|
8445
9209
|
}
|
|
9210
|
+
};
|
|
8446
9211
|
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
|
|
9212
|
+
// Dispatch moves first (pieces already detached from source)
|
|
9213
|
+
allMoves.forEach(move => {
|
|
9214
|
+
const tid = setTimeout(() => {
|
|
9215
|
+
if (this._destroyed || this._updateGeneration !== generation) return;
|
|
9216
|
+
this.pieceService.translatePiece(
|
|
9217
|
+
move,
|
|
9218
|
+
false,
|
|
9219
|
+
true,
|
|
9220
|
+
this._createDragFunction.bind(this),
|
|
9221
|
+
onAnimationComplete
|
|
9222
|
+
);
|
|
9223
|
+
}, animationIndex * animationDelay);
|
|
9224
|
+
this._animationTimeouts.push(tid);
|
|
9225
|
+
animationIndex++;
|
|
9226
|
+
});
|
|
9227
|
+
|
|
9228
|
+
// Dispatch removals
|
|
9229
|
+
allRemovals.forEach(square => {
|
|
9230
|
+
const tid = setTimeout(() => {
|
|
9231
|
+
if (this._destroyed || this._updateGeneration !== generation) return;
|
|
9232
|
+
this.pieceService.removePieceFromSquare(square, true, onAnimationComplete);
|
|
9233
|
+
}, animationIndex * animationDelay);
|
|
9234
|
+
this._animationTimeouts.push(tid);
|
|
9235
|
+
animationIndex++;
|
|
9236
|
+
});
|
|
9237
|
+
|
|
9238
|
+
// Dispatch additions
|
|
9239
|
+
allAdditions.forEach(({ square, key }) => {
|
|
9240
|
+
const tid = setTimeout(() => {
|
|
9241
|
+
if (this._destroyed || this._updateGeneration !== generation) return;
|
|
9242
|
+
const newPiece = this.pieceService.convertPiece(key);
|
|
9243
|
+
this.pieceService.addPieceOnSquare(
|
|
9244
|
+
square,
|
|
9245
|
+
newPiece,
|
|
9246
|
+
true,
|
|
9247
|
+
this._createDragFunction.bind(this),
|
|
9248
|
+
onAnimationComplete
|
|
9249
|
+
);
|
|
9250
|
+
}, animationIndex * animationDelay);
|
|
9251
|
+
this._animationTimeouts.push(tid);
|
|
9252
|
+
animationIndex++;
|
|
8460
9253
|
});
|
|
8461
9254
|
}
|
|
8462
9255
|
|
|
@@ -8484,10 +9277,6 @@ let Chessboard$1 = class Chessboard {
|
|
|
8484
9277
|
}
|
|
8485
9278
|
});
|
|
8486
9279
|
|
|
8487
|
-
console.log('Position Analysis:');
|
|
8488
|
-
console.log('Current pieces:', Array.from(currentPieces.entries()));
|
|
8489
|
-
console.log('Expected pieces:', Array.from(expectedPieces.entries()));
|
|
8490
|
-
|
|
8491
9280
|
// Identify different types of changes
|
|
8492
9281
|
const moves = []; // Pieces that can slide to new positions
|
|
8493
9282
|
const removes = []; // Pieces that need to be removed
|
|
@@ -8501,8 +9290,6 @@ let Chessboard$1 = class Chessboard {
|
|
|
8501
9290
|
const expectedPieceId = expectedPieces.get(square);
|
|
8502
9291
|
|
|
8503
9292
|
if (currentPieceId === expectedPieceId) {
|
|
8504
|
-
// Same piece type on same square - no movement needed
|
|
8505
|
-
console.log(`UNCHANGED: ${currentPieceId} stays on ${square}`);
|
|
8506
9293
|
unchanged.push({
|
|
8507
9294
|
piece: currentPieceId,
|
|
8508
9295
|
square: square
|
|
@@ -8524,7 +9311,6 @@ let Chessboard$1 = class Chessboard {
|
|
|
8524
9311
|
|
|
8525
9312
|
if (availableDestination) {
|
|
8526
9313
|
const [toSquare, expectedId] = availableDestination;
|
|
8527
|
-
console.log(`MOVE: ${currentPieceId} from ${fromSquare} to ${toSquare}`);
|
|
8528
9314
|
moves.push({
|
|
8529
9315
|
piece: currentPieceId,
|
|
8530
9316
|
from: fromSquare,
|
|
@@ -8534,8 +9320,6 @@ let Chessboard$1 = class Chessboard {
|
|
|
8534
9320
|
});
|
|
8535
9321
|
processedSquares.add(toSquare);
|
|
8536
9322
|
} else {
|
|
8537
|
-
// This piece needs to be removed
|
|
8538
|
-
console.log(`REMOVE: ${currentPieceId} from ${fromSquare}`);
|
|
8539
9323
|
removes.push({
|
|
8540
9324
|
piece: currentPieceId,
|
|
8541
9325
|
square: fromSquare,
|
|
@@ -8547,7 +9331,6 @@ let Chessboard$1 = class Chessboard {
|
|
|
8547
9331
|
// Third pass: handle pieces that need to be added
|
|
8548
9332
|
expectedPieces.forEach((expectedPieceId, toSquare) => {
|
|
8549
9333
|
if (!processedSquares.has(toSquare)) {
|
|
8550
|
-
console.log(`ADD: ${expectedPieceId} to ${toSquare}`);
|
|
8551
9334
|
adds.push({
|
|
8552
9335
|
piece: expectedPieceId,
|
|
8553
9336
|
square: toSquare,
|
|
@@ -8573,26 +9356,13 @@ let Chessboard$1 = class Chessboard {
|
|
|
8573
9356
|
* @param {boolean} [isPositionLoad=false] - Whether this is a position load
|
|
8574
9357
|
*/
|
|
8575
9358
|
_executeSimultaneousChanges(changeAnalysis, gameStateBefore, isPositionLoad = false) {
|
|
8576
|
-
const { moves, removes, adds
|
|
8577
|
-
|
|
8578
|
-
console.log(`Position changes analysis:`, {
|
|
8579
|
-
moves: moves.length,
|
|
8580
|
-
removes: removes.length,
|
|
8581
|
-
adds: adds.length,
|
|
8582
|
-
unchanged: unchanged.length
|
|
8583
|
-
});
|
|
8584
|
-
|
|
8585
|
-
// Log unchanged pieces for debugging
|
|
8586
|
-
if (unchanged.length > 0) {
|
|
8587
|
-
console.log('Pieces staying in place:', unchanged.map(u => `${u.piece} on ${u.square}`));
|
|
8588
|
-
}
|
|
9359
|
+
const { moves, removes, adds } = changeAnalysis;
|
|
8589
9360
|
|
|
8590
9361
|
let animationsCompleted = 0;
|
|
8591
9362
|
const totalAnimations = moves.length + removes.length + adds.length;
|
|
8592
9363
|
|
|
8593
9364
|
// If no animations are needed, complete immediately
|
|
8594
9365
|
if (totalAnimations === 0) {
|
|
8595
|
-
console.log('No animations needed, completing immediately');
|
|
8596
9366
|
this._addListeners();
|
|
8597
9367
|
|
|
8598
9368
|
// Trigger change event if position changed
|
|
@@ -8605,9 +9375,7 @@ let Chessboard$1 = class Chessboard {
|
|
|
8605
9375
|
|
|
8606
9376
|
const onAnimationComplete = () => {
|
|
8607
9377
|
animationsCompleted++;
|
|
8608
|
-
console.log(`Animation completed: ${animationsCompleted}/${totalAnimations}`);
|
|
8609
9378
|
if (animationsCompleted === totalAnimations) {
|
|
8610
|
-
console.log('All simultaneous animations completed');
|
|
8611
9379
|
this._addListeners();
|
|
8612
9380
|
|
|
8613
9381
|
// Trigger change event if position changed
|
|
@@ -8618,45 +9386,33 @@ let Chessboard$1 = class Chessboard {
|
|
|
8618
9386
|
}
|
|
8619
9387
|
};
|
|
8620
9388
|
|
|
8621
|
-
// Determine delay: 0 for position loads, configured delay for normal moves
|
|
8622
9389
|
const animationDelay = isPositionLoad ? 0 : this.config.simultaneousAnimationDelay;
|
|
8623
|
-
console.log(`Using animation delay: ${animationDelay}ms (position load: ${isPositionLoad})`);
|
|
8624
|
-
|
|
8625
9390
|
let animationIndex = 0;
|
|
8626
9391
|
|
|
8627
|
-
// Process moves
|
|
9392
|
+
// Process moves
|
|
8628
9393
|
moves.forEach(move => {
|
|
8629
9394
|
const delay = animationIndex * animationDelay;
|
|
8630
|
-
console.log(`Scheduling move ${move.piece} from ${move.from} to ${move.to} with delay ${delay}ms`);
|
|
8631
|
-
|
|
8632
9395
|
setTimeout(() => {
|
|
8633
9396
|
this._animatePieceMove(move, onAnimationComplete);
|
|
8634
9397
|
}, delay);
|
|
8635
|
-
|
|
8636
9398
|
animationIndex++;
|
|
8637
9399
|
});
|
|
8638
9400
|
|
|
8639
|
-
// Process removes
|
|
9401
|
+
// Process removes
|
|
8640
9402
|
removes.forEach(remove => {
|
|
8641
9403
|
const delay = animationIndex * animationDelay;
|
|
8642
|
-
console.log(`Scheduling removal of ${remove.piece} from ${remove.square} with delay ${delay}ms`);
|
|
8643
|
-
|
|
8644
9404
|
setTimeout(() => {
|
|
8645
9405
|
this._animatePieceRemoval(remove, onAnimationComplete);
|
|
8646
9406
|
}, delay);
|
|
8647
|
-
|
|
8648
9407
|
animationIndex++;
|
|
8649
9408
|
});
|
|
8650
9409
|
|
|
8651
|
-
// Process adds
|
|
9410
|
+
// Process adds
|
|
8652
9411
|
adds.forEach(add => {
|
|
8653
9412
|
const delay = animationIndex * animationDelay;
|
|
8654
|
-
console.log(`Scheduling addition of ${add.piece} to ${add.square} with delay ${delay}ms`);
|
|
8655
|
-
|
|
8656
9413
|
setTimeout(() => {
|
|
8657
9414
|
this._animatePieceAddition(add, onAnimationComplete);
|
|
8658
9415
|
}, delay);
|
|
8659
|
-
|
|
8660
9416
|
animationIndex++;
|
|
8661
9417
|
});
|
|
8662
9418
|
}
|
|
@@ -8672,23 +9428,16 @@ let Chessboard$1 = class Chessboard {
|
|
|
8672
9428
|
const piece = fromSquare.piece;
|
|
8673
9429
|
|
|
8674
9430
|
if (!piece) {
|
|
8675
|
-
console.warn(`No piece found on ${move.from} for move animation`);
|
|
8676
9431
|
onComplete();
|
|
8677
9432
|
return;
|
|
8678
9433
|
}
|
|
8679
9434
|
|
|
8680
|
-
console.log(`Animating piece move: ${move.piece} from ${move.from} to ${move.to}`);
|
|
8681
|
-
|
|
8682
|
-
// Use translatePiece for smooth sliding animation
|
|
8683
9435
|
this.pieceService.translatePiece(
|
|
8684
9436
|
{ from: fromSquare, to: toSquare, piece: piece },
|
|
8685
|
-
false,
|
|
8686
|
-
true,
|
|
9437
|
+
false,
|
|
9438
|
+
true,
|
|
8687
9439
|
this._createDragFunction.bind(this),
|
|
8688
|
-
|
|
8689
|
-
console.log(`Piece move animation completed: ${move.piece} to ${move.to}`);
|
|
8690
|
-
onComplete();
|
|
8691
|
-
}
|
|
9440
|
+
onComplete
|
|
8692
9441
|
);
|
|
8693
9442
|
}
|
|
8694
9443
|
|
|
@@ -8699,12 +9448,7 @@ let Chessboard$1 = class Chessboard {
|
|
|
8699
9448
|
* @param {Function} onComplete - Callback when animation completes
|
|
8700
9449
|
*/
|
|
8701
9450
|
_animatePieceRemoval(remove, onComplete) {
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
this.pieceService.removePieceFromSquare(remove.squareObj, true, () => {
|
|
8705
|
-
console.log(`Piece removal animation completed: ${remove.piece} from ${remove.square}`);
|
|
8706
|
-
onComplete();
|
|
8707
|
-
});
|
|
9451
|
+
this.pieceService.removePieceFromSquare(remove.squareObj, true, onComplete);
|
|
8708
9452
|
}
|
|
8709
9453
|
|
|
8710
9454
|
/**
|
|
@@ -8714,18 +9458,13 @@ let Chessboard$1 = class Chessboard {
|
|
|
8714
9458
|
* @param {Function} onComplete - Callback when animation completes
|
|
8715
9459
|
*/
|
|
8716
9460
|
_animatePieceAddition(add, onComplete) {
|
|
8717
|
-
console.log(`Animating piece addition: ${add.piece} to ${add.square}`);
|
|
8718
|
-
|
|
8719
9461
|
const newPiece = this.pieceService.convertPiece(add.piece);
|
|
8720
9462
|
this.pieceService.addPieceOnSquare(
|
|
8721
9463
|
add.squareObj,
|
|
8722
9464
|
newPiece,
|
|
8723
9465
|
true,
|
|
8724
9466
|
this._createDragFunction.bind(this),
|
|
8725
|
-
|
|
8726
|
-
console.log(`Piece addition animation completed: ${add.piece} to ${add.square}`);
|
|
8727
|
-
onComplete();
|
|
8728
|
-
}
|
|
9467
|
+
onComplete
|
|
8729
9468
|
);
|
|
8730
9469
|
}
|
|
8731
9470
|
|
|
@@ -8825,7 +9564,7 @@ let Chessboard$1 = class Chessboard {
|
|
|
8825
9564
|
const animate = opts.animate !== undefined ? opts.animate : true;
|
|
8826
9565
|
// Use the default starting position from config or fallback
|
|
8827
9566
|
const startPosition = this.config && this.config.position ? this.config.position : 'start';
|
|
8828
|
-
|
|
9567
|
+
// setPosition already calls _updateBoardPieces, don't call it twice
|
|
8829
9568
|
return this.setPosition(startPosition, { animate });
|
|
8830
9569
|
}
|
|
8831
9570
|
/**
|
|
@@ -8840,10 +9579,13 @@ let Chessboard$1 = class Chessboard {
|
|
|
8840
9579
|
return false;
|
|
8841
9580
|
}
|
|
8842
9581
|
if (this._clearVisualState) this._clearVisualState();
|
|
9582
|
+
|
|
9583
|
+
// Clear the game state
|
|
8843
9584
|
this.positionService.getGame().clear();
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
9585
|
+
|
|
9586
|
+
// Let _updateBoardPieces handle removal (no manual loop to avoid race conditions)
|
|
9587
|
+
this._updateBoardPieces(animate, true);
|
|
9588
|
+
|
|
8847
9589
|
return true;
|
|
8848
9590
|
}
|
|
8849
9591
|
|
|
@@ -8880,6 +9622,32 @@ let Chessboard$1 = class Chessboard {
|
|
|
8880
9622
|
}
|
|
8881
9623
|
return false;
|
|
8882
9624
|
}
|
|
9625
|
+
/**
|
|
9626
|
+
* Move a piece from one square to another
|
|
9627
|
+
* @param {string} moveStr - Move in format 'e2e4' or 'e7e8q' (with promotion)
|
|
9628
|
+
* @param {Object} [opts]
|
|
9629
|
+
* @param {boolean} [opts.animate=true]
|
|
9630
|
+
* @returns {Object|boolean} Move result or false if invalid
|
|
9631
|
+
*/
|
|
9632
|
+
movePiece(moveStr, opts = {}) {
|
|
9633
|
+
const animate = opts.animate !== false;
|
|
9634
|
+
if (typeof moveStr !== 'string' || moveStr.length < 4) {
|
|
9635
|
+
return false;
|
|
9636
|
+
}
|
|
9637
|
+
const from = moveStr.slice(0, 2);
|
|
9638
|
+
const to = moveStr.slice(2, 4);
|
|
9639
|
+
const promotion = moveStr.length > 4 ? moveStr[4].toLowerCase() : undefined;
|
|
9640
|
+
|
|
9641
|
+
const moveObj = { from, to };
|
|
9642
|
+
if (promotion) moveObj.promotion = promotion;
|
|
9643
|
+
|
|
9644
|
+
const result = this.positionService.getGame().move(moveObj);
|
|
9645
|
+
if (result) {
|
|
9646
|
+
this._updateBoardPieces(animate);
|
|
9647
|
+
}
|
|
9648
|
+
return result || false;
|
|
9649
|
+
}
|
|
9650
|
+
|
|
8883
9651
|
/**
|
|
8884
9652
|
* Get legal moves for a square
|
|
8885
9653
|
* @param {string} square
|
|
@@ -8894,10 +9662,10 @@ let Chessboard$1 = class Chessboard {
|
|
|
8894
9662
|
* @returns {string|null}
|
|
8895
9663
|
*/
|
|
8896
9664
|
getPiece(square) {
|
|
8897
|
-
//
|
|
8898
|
-
|
|
8899
|
-
if (!
|
|
8900
|
-
const piece =
|
|
9665
|
+
// Use game state as source of truth
|
|
9666
|
+
// Returns piece in format 'wq' (color + type)
|
|
9667
|
+
if (!this.positionService || !this.positionService.getGame()) return null;
|
|
9668
|
+
const piece = this.positionService.getGame().get(square);
|
|
8901
9669
|
if (!piece) return null;
|
|
8902
9670
|
return (piece.color + piece.type).toLowerCase();
|
|
8903
9671
|
}
|
|
@@ -8958,45 +9726,355 @@ let Chessboard$1 = class Chessboard {
|
|
|
8958
9726
|
if (!this.validationService.isValidSquare(square)) {
|
|
8959
9727
|
throw new Error(`[removePiece] Invalid square: ${square}`);
|
|
8960
9728
|
}
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
squareObj.piece = null;
|
|
9729
|
+
if (!this.positionService || !this.positionService.getGame()) {
|
|
9730
|
+
return false;
|
|
9731
|
+
}
|
|
8965
9732
|
const game = this.positionService.getGame();
|
|
8966
|
-
game
|
|
9733
|
+
// Remove from game state first (source of truth)
|
|
9734
|
+
const removed = game.remove(square);
|
|
9735
|
+
// Then update the board visually
|
|
9736
|
+
const squareObj = this.boardService.getSquare(square);
|
|
9737
|
+
if (squareObj) {
|
|
9738
|
+
squareObj.piece = null;
|
|
9739
|
+
}
|
|
8967
9740
|
this._updateBoardPieces(animate);
|
|
8968
|
-
return
|
|
9741
|
+
return removed !== null;
|
|
8969
9742
|
}
|
|
8970
9743
|
|
|
8971
9744
|
// --- BOARD CONTROL ---
|
|
8972
9745
|
/**
|
|
8973
9746
|
* Flip the board orientation
|
|
8974
9747
|
* @param {Object} [opts]
|
|
8975
|
-
* @param {boolean} [opts.animate=true]
|
|
9748
|
+
* @param {boolean} [opts.animate=true] - Enable animation (for 'animate' mode)
|
|
9749
|
+
* @param {string} [opts.mode] - Override flip mode ('visual', 'animate', 'none')
|
|
8976
9750
|
*/
|
|
8977
9751
|
flipBoard(opts = {}) {
|
|
9752
|
+
const flipMode = opts.mode || this.config.flipMode || 'visual';
|
|
9753
|
+
|
|
9754
|
+
// Update internal orientation state
|
|
8978
9755
|
if (this.coordinateService && this.coordinateService.flipOrientation) {
|
|
8979
9756
|
this.coordinateService.flipOrientation();
|
|
8980
9757
|
}
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
9758
|
+
|
|
9759
|
+
const boardElement = this.boardService.element;
|
|
9760
|
+
const isFlipped = this.coordinateService.getOrientation() === 'b';
|
|
9761
|
+
|
|
9762
|
+
switch (flipMode) {
|
|
9763
|
+
case 'visual':
|
|
9764
|
+
// CSS flexbox flip - instant, no piece animation needed
|
|
9765
|
+
this._flipVisual(boardElement, isFlipped);
|
|
9766
|
+
break;
|
|
9767
|
+
|
|
9768
|
+
case 'animate':
|
|
9769
|
+
// Animate pieces to mirrored positions
|
|
9770
|
+
this._flipAnimate(opts.animate !== false);
|
|
9771
|
+
break;
|
|
9772
|
+
|
|
9773
|
+
case 'none':
|
|
9774
|
+
// No visual change - only internal orientation updated
|
|
9775
|
+
// Useful for programmatic orientation without visual feedback
|
|
9776
|
+
break;
|
|
9777
|
+
|
|
9778
|
+
default:
|
|
9779
|
+
this._flipVisual(boardElement, isFlipped);
|
|
9780
|
+
}
|
|
9781
|
+
}
|
|
9782
|
+
|
|
9783
|
+
/**
|
|
9784
|
+
* Visual flip using CSS flexbox (instant)
|
|
9785
|
+
* @private
|
|
9786
|
+
* @param {HTMLElement} boardElement - Board DOM element
|
|
9787
|
+
* @param {boolean} isFlipped - Whether board should be flipped
|
|
9788
|
+
*/
|
|
9789
|
+
_flipVisual(boardElement, isFlipped) {
|
|
9790
|
+
if (!boardElement) return;
|
|
9791
|
+
|
|
9792
|
+
if (isFlipped) {
|
|
9793
|
+
boardElement.classList.add('flipped');
|
|
9794
|
+
} else {
|
|
9795
|
+
boardElement.classList.remove('flipped');
|
|
9796
|
+
}
|
|
9797
|
+
}
|
|
9798
|
+
|
|
9799
|
+
/**
|
|
9800
|
+
* Animate flip using FLIP technique (First-Last-Invert-Play)
|
|
9801
|
+
* Same end state as visual mode (CSS flip), but pieces animate smoothly.
|
|
9802
|
+
* @private
|
|
9803
|
+
* @param {boolean} animate - Whether to animate the movement
|
|
9804
|
+
*/
|
|
9805
|
+
_flipAnimate(animate) {
|
|
9806
|
+
const boardElement = this.boardService.element;
|
|
9807
|
+
if (!boardElement) return;
|
|
9808
|
+
|
|
9809
|
+
const squares = this.boardService.getAllSquares();
|
|
9810
|
+
|
|
9811
|
+
// FIRST: Record current visual position of every piece
|
|
9812
|
+
const pieceRects = {};
|
|
9813
|
+
for (const [id, square] of Object.entries(squares)) {
|
|
9814
|
+
if (square.piece && square.piece.element) {
|
|
9815
|
+
pieceRects[id] = square.piece.element.getBoundingClientRect();
|
|
9816
|
+
}
|
|
9817
|
+
}
|
|
9818
|
+
|
|
9819
|
+
// LAST: Apply CSS flip (instant) - same as visual mode
|
|
9820
|
+
const isFlipped = this.coordinateService.getOrientation() === 'b';
|
|
9821
|
+
this._flipVisual(boardElement, isFlipped);
|
|
9822
|
+
|
|
9823
|
+
if (!animate || Object.keys(pieceRects).length === 0) return;
|
|
9824
|
+
|
|
9825
|
+
// INVERT + PLAY: Animate each piece from old position to new
|
|
9826
|
+
const duration = this.config.moveTime || 200;
|
|
9827
|
+
const easing = 'cubic-bezier(0.33, 1, 0.68, 1)';
|
|
9828
|
+
|
|
9829
|
+
for (const [id, oldRect] of Object.entries(pieceRects)) {
|
|
9830
|
+
const square = squares[id];
|
|
9831
|
+
if (!square || !square.piece || !square.piece.element) continue;
|
|
9832
|
+
|
|
9833
|
+
const piece = square.piece;
|
|
9834
|
+
const newRect = piece.element.getBoundingClientRect();
|
|
9835
|
+
const dx = oldRect.left - newRect.left;
|
|
9836
|
+
const dy = oldRect.top - newRect.top;
|
|
9837
|
+
|
|
9838
|
+
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) continue;
|
|
9839
|
+
|
|
9840
|
+
if (piece.element.animate) {
|
|
9841
|
+
const anim = piece.element.animate([
|
|
9842
|
+
{ transform: `translate(${dx}px, ${dy}px)` },
|
|
9843
|
+
{ transform: 'translate(0, 0)' }
|
|
9844
|
+
], { duration, easing, fill: 'forwards' });
|
|
9845
|
+
anim.onfinish = () => {
|
|
9846
|
+
anim.cancel();
|
|
9847
|
+
if (piece.element) piece.element.style.transform = '';
|
|
9848
|
+
};
|
|
9849
|
+
} else {
|
|
9850
|
+
// setTimeout fallback for jsdom / older browsers
|
|
9851
|
+
piece.element.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
9852
|
+
setTimeout(() => {
|
|
9853
|
+
if (!piece.element) return;
|
|
9854
|
+
piece.element.style.transition = `transform ${duration}ms`;
|
|
9855
|
+
piece.element.style.transform = 'translate(0, 0)';
|
|
9856
|
+
setTimeout(() => {
|
|
9857
|
+
if (!piece.element) return;
|
|
9858
|
+
piece.element.style.transition = '';
|
|
9859
|
+
piece.element.style.transform = '';
|
|
9860
|
+
}, duration);
|
|
9861
|
+
}, 0);
|
|
9862
|
+
}
|
|
9863
|
+
}
|
|
9864
|
+
}
|
|
9865
|
+
|
|
9866
|
+
/**
|
|
9867
|
+
* Set the flip mode at runtime
|
|
9868
|
+
* @param {'visual'|'animate'|'none'} mode - The flip mode to use
|
|
9869
|
+
*/
|
|
9870
|
+
setFlipMode(mode) {
|
|
9871
|
+
const validModes = ['visual', 'animate', 'none'];
|
|
9872
|
+
if (!validModes.includes(mode)) {
|
|
9873
|
+
console.warn(`Invalid flip mode: ${mode}. Valid options: ${validModes.join(', ')}`);
|
|
9874
|
+
return;
|
|
9875
|
+
}
|
|
9876
|
+
this.config.flipMode = mode;
|
|
9877
|
+
}
|
|
9878
|
+
|
|
9879
|
+
/**
|
|
9880
|
+
* Get the current flip mode
|
|
9881
|
+
* @returns {string} Current flip mode
|
|
9882
|
+
*/
|
|
9883
|
+
getFlipMode() {
|
|
9884
|
+
return this.config.flipMode || 'visual';
|
|
9885
|
+
}
|
|
9886
|
+
|
|
9887
|
+
// --- MOVEMENT CONFIGURATION ---
|
|
9888
|
+
|
|
9889
|
+
/**
|
|
9890
|
+
* Set the movement style
|
|
9891
|
+
* @param {'slide'|'arc'|'hop'|'teleport'|'fade'} style - Movement style
|
|
9892
|
+
*/
|
|
9893
|
+
setMoveStyle(style) {
|
|
9894
|
+
const validStyles = ['slide', 'arc', 'hop', 'teleport', 'fade'];
|
|
9895
|
+
if (!validStyles.includes(style)) {
|
|
9896
|
+
console.warn(`Invalid move style: ${style}. Valid: ${validStyles.join(', ')}`);
|
|
9897
|
+
return;
|
|
9898
|
+
}
|
|
9899
|
+
this.config.moveStyle = style;
|
|
9900
|
+
}
|
|
9901
|
+
|
|
9902
|
+
/**
|
|
9903
|
+
* Get the current movement style
|
|
9904
|
+
* @returns {string} Current movement style
|
|
9905
|
+
*/
|
|
9906
|
+
getMoveStyle() {
|
|
9907
|
+
return this.config.moveStyle || 'slide';
|
|
9908
|
+
}
|
|
9909
|
+
|
|
9910
|
+
/**
|
|
9911
|
+
* Set the capture animation style
|
|
9912
|
+
* @param {'fade'|'shrink'|'instant'|'explode'} style - Capture style
|
|
9913
|
+
*/
|
|
9914
|
+
setCaptureStyle(style) {
|
|
9915
|
+
const validStyles = ['fade', 'shrink', 'instant', 'explode'];
|
|
9916
|
+
if (!validStyles.includes(style)) {
|
|
9917
|
+
console.warn(`Invalid capture style: ${style}. Valid: ${validStyles.join(', ')}`);
|
|
9918
|
+
return;
|
|
9919
|
+
}
|
|
9920
|
+
this.config.captureStyle = style;
|
|
9921
|
+
}
|
|
9922
|
+
|
|
9923
|
+
/**
|
|
9924
|
+
* Get the current capture style
|
|
9925
|
+
* @returns {string} Current capture style
|
|
9926
|
+
*/
|
|
9927
|
+
getCaptureStyle() {
|
|
9928
|
+
return this.config.captureStyle || 'fade';
|
|
9929
|
+
}
|
|
9930
|
+
|
|
9931
|
+
/**
|
|
9932
|
+
* Set the appearance animation style
|
|
9933
|
+
* @param {'fade'|'pulse'|'pop'|'drop'|'instant'} style - Appearance style
|
|
9934
|
+
*/
|
|
9935
|
+
setAppearanceStyle(style) {
|
|
9936
|
+
const validStyles = ['fade', 'pulse', 'pop', 'drop', 'instant'];
|
|
9937
|
+
if (!validStyles.includes(style)) {
|
|
9938
|
+
console.warn(`Invalid appearance style: ${style}. Valid: ${validStyles.join(', ')}`);
|
|
9939
|
+
return;
|
|
9940
|
+
}
|
|
9941
|
+
this.config.appearanceStyle = style;
|
|
9942
|
+
}
|
|
9943
|
+
|
|
9944
|
+
/**
|
|
9945
|
+
* Get the current appearance style
|
|
9946
|
+
* @returns {string} Current appearance style
|
|
9947
|
+
*/
|
|
9948
|
+
getAppearanceStyle() {
|
|
9949
|
+
return this.config.appearanceStyle || 'fade';
|
|
9950
|
+
}
|
|
9951
|
+
|
|
9952
|
+
/**
|
|
9953
|
+
* Set the landing effect
|
|
9954
|
+
* @param {'none'|'bounce'|'pulse'|'settle'} effect - Landing effect
|
|
9955
|
+
*/
|
|
9956
|
+
setLandingEffect(effect) {
|
|
9957
|
+
const validEffects = ['none', 'bounce', 'pulse', 'settle'];
|
|
9958
|
+
if (!validEffects.includes(effect)) {
|
|
9959
|
+
console.warn(`Invalid landing effect: ${effect}. Valid: ${validEffects.join(', ')}`);
|
|
9960
|
+
return;
|
|
9961
|
+
}
|
|
9962
|
+
this.config.landingEffect = effect;
|
|
9963
|
+
}
|
|
9964
|
+
|
|
9965
|
+
/**
|
|
9966
|
+
* Get the current landing effect
|
|
9967
|
+
* @returns {string} Current landing effect
|
|
9968
|
+
*/
|
|
9969
|
+
getLandingEffect() {
|
|
9970
|
+
return this.config.landingEffect || 'none';
|
|
8986
9971
|
}
|
|
9972
|
+
|
|
9973
|
+
/**
|
|
9974
|
+
* Set the movement duration
|
|
9975
|
+
* @param {number|string} duration - Duration in ms or preset name ('instant', 'veryFast', 'fast', 'normal', 'slow', 'verySlow')
|
|
9976
|
+
*/
|
|
9977
|
+
setMoveTime(duration) {
|
|
9978
|
+
const presets = { instant: 0, veryFast: 100, fast: 200, normal: 400, slow: 600, verySlow: 1000 };
|
|
9979
|
+
if (typeof duration === 'string' && presets[duration] !== undefined) {
|
|
9980
|
+
this.config.moveTime = presets[duration];
|
|
9981
|
+
} else if (typeof duration === 'number' && duration >= 0) {
|
|
9982
|
+
this.config.moveTime = duration;
|
|
9983
|
+
} else {
|
|
9984
|
+
console.warn(`Invalid move time: ${duration}`);
|
|
9985
|
+
}
|
|
9986
|
+
}
|
|
9987
|
+
|
|
9988
|
+
/**
|
|
9989
|
+
* Get the current movement duration
|
|
9990
|
+
* @returns {number} Duration in ms
|
|
9991
|
+
*/
|
|
9992
|
+
getMoveTime() {
|
|
9993
|
+
return this.config.moveTime;
|
|
9994
|
+
}
|
|
9995
|
+
|
|
9996
|
+
/**
|
|
9997
|
+
* Set the easing function for movements
|
|
9998
|
+
* @param {string} easing - CSS easing function
|
|
9999
|
+
*/
|
|
10000
|
+
setMoveEasing(easing) {
|
|
10001
|
+
const validEasings = ['ease', 'linear', 'ease-in', 'ease-out', 'ease-in-out'];
|
|
10002
|
+
if (!validEasings.includes(easing)) {
|
|
10003
|
+
console.warn(`Invalid easing: ${easing}. Valid: ${validEasings.join(', ')}`);
|
|
10004
|
+
return;
|
|
10005
|
+
}
|
|
10006
|
+
this.config.moveEasing = easing;
|
|
10007
|
+
}
|
|
10008
|
+
|
|
10009
|
+
/**
|
|
10010
|
+
* Configure multiple movement settings at once
|
|
10011
|
+
* @param {Object} options - Movement configuration
|
|
10012
|
+
* @param {string} [options.style] - Movement style
|
|
10013
|
+
* @param {string} [options.captureStyle] - Capture animation style
|
|
10014
|
+
* @param {string} [options.landingEffect] - Landing effect
|
|
10015
|
+
* @param {number|string} [options.duration] - Movement duration
|
|
10016
|
+
* @param {string} [options.easing] - Easing function
|
|
10017
|
+
* @param {number} [options.arcHeight] - Arc height for arc/hop styles (0-1)
|
|
10018
|
+
*/
|
|
10019
|
+
configureMovement(options) {
|
|
10020
|
+
if (options.style) this.setMoveStyle(options.style);
|
|
10021
|
+
if (options.captureStyle) this.setCaptureStyle(options.captureStyle);
|
|
10022
|
+
if (options.appearanceStyle) this.setAppearanceStyle(options.appearanceStyle);
|
|
10023
|
+
if (options.landingEffect) this.setLandingEffect(options.landingEffect);
|
|
10024
|
+
if (options.duration !== undefined) this.setMoveTime(options.duration);
|
|
10025
|
+
if (options.easing) this.setMoveEasing(options.easing);
|
|
10026
|
+
if (options.arcHeight !== undefined) {
|
|
10027
|
+
this.config.moveArcHeight = Math.max(0, Math.min(1, options.arcHeight));
|
|
10028
|
+
}
|
|
10029
|
+
}
|
|
10030
|
+
|
|
10031
|
+
/**
|
|
10032
|
+
* Get all movement configuration
|
|
10033
|
+
* @returns {Object} Current movement configuration
|
|
10034
|
+
*/
|
|
10035
|
+
getMovementConfig() {
|
|
10036
|
+
return {
|
|
10037
|
+
style: this.config.moveStyle || 'slide',
|
|
10038
|
+
captureStyle: this.config.captureStyle || 'fade',
|
|
10039
|
+
appearanceStyle: this.config.appearanceStyle || 'fade',
|
|
10040
|
+
landingEffect: this.config.landingEffect || 'none',
|
|
10041
|
+
duration: this.config.moveTime,
|
|
10042
|
+
easing: this.config.moveEasing || 'ease',
|
|
10043
|
+
arcHeight: this.config.moveArcHeight || 0.3
|
|
10044
|
+
};
|
|
10045
|
+
}
|
|
10046
|
+
|
|
8987
10047
|
/**
|
|
8988
10048
|
* Set the board orientation
|
|
8989
10049
|
* @param {'w'|'b'} color
|
|
8990
10050
|
* @param {Object} [opts]
|
|
8991
|
-
* @param {boolean} [opts.animate=true]
|
|
10051
|
+
* @param {boolean} [opts.animate=true] - Enable animation (for 'animate' mode)
|
|
10052
|
+
* @param {string} [opts.mode] - Override flip mode ('visual', 'animate', 'none')
|
|
8992
10053
|
*/
|
|
8993
10054
|
setOrientation(color, opts = {}) {
|
|
8994
10055
|
if (this.validationService.isValidOrientation(color)) {
|
|
8995
|
-
this.coordinateService.
|
|
8996
|
-
if (
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
10056
|
+
const currentOrientation = this.coordinateService.getOrientation();
|
|
10057
|
+
if (currentOrientation !== color) {
|
|
10058
|
+
this.coordinateService.setOrientation(color);
|
|
10059
|
+
|
|
10060
|
+
const flipMode = opts.mode || this.config.flipMode || 'visual';
|
|
10061
|
+
const boardElement = this.boardService.element;
|
|
10062
|
+
const isFlipped = color === 'b';
|
|
10063
|
+
|
|
10064
|
+
switch (flipMode) {
|
|
10065
|
+
case 'visual':
|
|
10066
|
+
this._flipVisual(boardElement, isFlipped);
|
|
10067
|
+
break;
|
|
10068
|
+
case 'animate':
|
|
10069
|
+
this._flipAnimate(opts.animate !== false);
|
|
10070
|
+
break;
|
|
10071
|
+
case 'none':
|
|
10072
|
+
// No visual change
|
|
10073
|
+
break;
|
|
10074
|
+
default:
|
|
10075
|
+
this._flipVisual(boardElement, isFlipped);
|
|
10076
|
+
}
|
|
10077
|
+
}
|
|
9000
10078
|
}
|
|
9001
10079
|
return this.coordinateService.getOrientation();
|
|
9002
10080
|
}
|
|
@@ -9114,7 +10192,40 @@ let Chessboard$1 = class Chessboard {
|
|
|
9114
10192
|
/**
|
|
9115
10193
|
* Destroy the board and cleanup
|
|
9116
10194
|
*/
|
|
9117
|
-
destroy() {
|
|
10195
|
+
destroy() {
|
|
10196
|
+
this._destroyed = true;
|
|
10197
|
+
|
|
10198
|
+
// Remove all event listeners
|
|
10199
|
+
if (this.eventService) {
|
|
10200
|
+
this.eventService.removeAllListeners();
|
|
10201
|
+
this.eventService.destroy();
|
|
10202
|
+
}
|
|
10203
|
+
|
|
10204
|
+
// Clear all timeouts
|
|
10205
|
+
if (this._updateTimeout) {
|
|
10206
|
+
clearTimeout(this._updateTimeout);
|
|
10207
|
+
this._updateTimeout = null;
|
|
10208
|
+
}
|
|
10209
|
+
|
|
10210
|
+
// Clear all animation timeouts
|
|
10211
|
+
if (this._animationTimeouts) {
|
|
10212
|
+
this._animationTimeouts.forEach(tid => clearTimeout(tid));
|
|
10213
|
+
this._animationTimeouts = [];
|
|
10214
|
+
}
|
|
10215
|
+
|
|
10216
|
+
// Destroy services
|
|
10217
|
+
if (this.moveService) this.moveService.destroy();
|
|
10218
|
+
if (this.animationService && this.animationService.destroy) this.animationService.destroy();
|
|
10219
|
+
if (this.pieceService && this.pieceService.destroy) this.pieceService.destroy();
|
|
10220
|
+
if (this.boardService && this.boardService.destroy) this.boardService.destroy();
|
|
10221
|
+
if (this.positionService && this.positionService.destroy) this.positionService.destroy();
|
|
10222
|
+
if (this.coordinateService && this.coordinateService.destroy) this.coordinateService.destroy();
|
|
10223
|
+
if (this.validationService) this.validationService.destroy();
|
|
10224
|
+
if (this.config && this.config.destroy) this.config.destroy();
|
|
10225
|
+
|
|
10226
|
+
// Clear references
|
|
10227
|
+
this._cleanup();
|
|
10228
|
+
}
|
|
9118
10229
|
/**
|
|
9119
10230
|
* Rebuild the board
|
|
9120
10231
|
*/
|
|
@@ -9130,7 +10241,11 @@ let Chessboard$1 = class Chessboard {
|
|
|
9130
10241
|
* Set new config
|
|
9131
10242
|
* @param {Object} newConfig
|
|
9132
10243
|
*/
|
|
9133
|
-
setConfig(newConfig) {
|
|
10244
|
+
setConfig(newConfig) {
|
|
10245
|
+
if (this.config && typeof this.config.update === 'function') {
|
|
10246
|
+
this.config.update(newConfig);
|
|
10247
|
+
}
|
|
10248
|
+
}
|
|
9134
10249
|
|
|
9135
10250
|
// --- ALIASES/DEPRECATED ---
|
|
9136
10251
|
/**
|
|
@@ -9165,17 +10280,10 @@ let Chessboard$1 = class Chessboard {
|
|
|
9165
10280
|
}
|
|
9166
10281
|
|
|
9167
10282
|
/**
|
|
9168
|
-
* Gets the current position
|
|
9169
|
-
* @
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
return this.positionService.getPosition();
|
|
9173
|
-
}
|
|
9174
|
-
|
|
9175
|
-
/**
|
|
9176
|
-
* Sets a new position
|
|
9177
|
-
* @param {string|Object} position - New position
|
|
9178
|
-
* @param {boolean} [animate=true] - Whether to animate
|
|
10283
|
+
* Gets or sets the current position
|
|
10284
|
+
* @param {string|Object} [position] - Position to set (FEN or object). If omitted, returns current position.
|
|
10285
|
+
* @param {boolean} [animate=true] - Whether to animate when setting
|
|
10286
|
+
* @returns {Object} Current position object (when getting)
|
|
9179
10287
|
*/
|
|
9180
10288
|
position(position, animate = true) {
|
|
9181
10289
|
if (position === undefined) {
|
|
@@ -9438,34 +10546,21 @@ let Chessboard$1 = class Chessboard {
|
|
|
9438
10546
|
// Ensure all public API methods from README are present and routed
|
|
9439
10547
|
insert(square, piece) { return this.putPiece(piece, square); }
|
|
9440
10548
|
get(square) { return this.getPiece(square); }
|
|
9441
|
-
position(
|
|
9442
|
-
if (color) this.setOrientation(color);
|
|
9443
|
-
return this.setPosition(position);
|
|
9444
|
-
}
|
|
9445
|
-
flip(animation = true) { return this.flipBoard({ animate: animation }); }
|
|
10549
|
+
// Note: position() is defined above at line ~1684 with getter/setter functionality
|
|
9446
10550
|
build() { return this._initialize(); }
|
|
9447
10551
|
resize(value) { return this.resizeBoard(value); }
|
|
9448
|
-
destroy() { return this._cleanup(); }
|
|
9449
10552
|
piece(square) { return this.getPiece(square); }
|
|
9450
|
-
highlight(square) { return true; }
|
|
9451
|
-
dehighlight(square) { return true; }
|
|
9452
|
-
turn() { return this.positionService.getGame().turn(); }
|
|
9453
10553
|
ascii() { return this.positionService.getGame().ascii(); }
|
|
9454
10554
|
board() { return this.positionService.getGame().board(); }
|
|
9455
10555
|
getCastlingRights(color) { return this.positionService.getGame().getCastlingRights(color); }
|
|
9456
10556
|
getComment() { return this.positionService.getGame().getComment(); }
|
|
9457
10557
|
getComments() { return this.positionService.getGame().getComments(); }
|
|
9458
|
-
history(options = {}) { return this.positionService.getGame().history(options); }
|
|
9459
10558
|
lastMove() { return this.positionService.getGame().lastMove(); }
|
|
9460
10559
|
moveNumber() { return this.positionService.getGame().moveNumber(); }
|
|
9461
10560
|
moves(options = {}) { return this.positionService.getGame().moves(options); }
|
|
9462
|
-
pgn(options = {}) { return this.positionService.getGame().pgn(options); }
|
|
9463
10561
|
squareColor(squareId) { return this.boardService.getSquare(squareId).isWhite() ? 'light' : 'dark'; }
|
|
9464
|
-
isCheckmate() { return this.positionService.getGame().isCheckmate(); }
|
|
9465
|
-
isDraw() { return this.positionService.getGame().isDraw(); }
|
|
9466
10562
|
isDrawByFiftyMoves() { return this.positionService.getGame().isDrawByFiftyMoves(); }
|
|
9467
10563
|
isInsufficientMaterial() { return this.positionService.getGame().isInsufficientMaterial(); }
|
|
9468
|
-
isGameOver() { return this.positionService.getGame().isGameOver(); }
|
|
9469
10564
|
isStalemate() { return this.positionService.getGame().isStalemate(); }
|
|
9470
10565
|
isThreefoldRepetition() { return this.positionService.getGame().isThreefoldRepetition(); }
|
|
9471
10566
|
load(fen, options = {}, animation = true) { return this.setPosition(fen, { ...options, animate: animation }); }
|