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