cm-chessboard 8.12.18 → 8.13.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 +6 -0
- package/examples/extensions/piece-rotation-extension.html +53 -0
- package/package.json +1 -1
- package/src/Chessboard.js +8 -0
- package/src/extensions/piece-rotation/PieceRotation.js +171 -0
- package/src/extensions/piece-rotation/README.md +39 -0
- package/src/view/VisualMoveInput.js +38 -1
- package/test/TestPieceRotation.js +138 -0
- package/test/TestVisualMoveInput.js +127 -0
- package/test/index.html +2 -0
package/README.md
CHANGED
|
@@ -247,6 +247,12 @@ chessboard.enableMoveInput((event) => {
|
|
|
247
247
|
|
|
248
248
|
Disables moves via user input.
|
|
249
249
|
|
|
250
|
+
### cancelMoveInput()
|
|
251
|
+
|
|
252
|
+
Cancels a move input that is currently in progress (a piece picked up or a square selected), without disabling move
|
|
253
|
+
input. Fires a `moveInputCanceled` event with `event.reason === "canceled"`. It is a no-op when no move input is in
|
|
254
|
+
progress. Useful to abort a move from your own UI, e.g. on an "escape" key press or a cancel button.
|
|
255
|
+
|
|
250
256
|
### isMoveInputEnabled()
|
|
251
257
|
|
|
252
258
|
Returns `true` if move input is currently enabled for white or black.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>cm-chessboard</title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1.0"/>
|
|
7
|
+
<link rel="stylesheet" href="../styles/examples.css"/>
|
|
8
|
+
<link rel="stylesheet" href="../../assets/chessboard.css"/>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<h1><a href="../../">cm-chessboard</a></h1>
|
|
12
|
+
<h2>Example: PieceRotation Extension</h2>
|
|
13
|
+
<p>Rotates the piece glyphs in place, by any angle, optionally animated — either all pieces or only those
|
|
14
|
+
of one color. Useful for over-the-board play on a tablet lying flat between two players. Moving pieces
|
|
15
|
+
stays enabled, the rotation survives moves and position changes.</p>
|
|
16
|
+
<div class="board" id="board"></div>
|
|
17
|
+
<div style="clear: both"></div>
|
|
18
|
+
<p>
|
|
19
|
+
<select id="color">
|
|
20
|
+
<option value="">all pieces</option>
|
|
21
|
+
<option value="w">white only</option>
|
|
22
|
+
<option value="b">black only</option>
|
|
23
|
+
</select>
|
|
24
|
+
<button onclick="window.rotate(0)">0°</button>
|
|
25
|
+
<button onclick="window.rotate(90)">90°</button>
|
|
26
|
+
<button onclick="window.rotate(180)">180°</button>
|
|
27
|
+
<button onclick="window.rotate(270)">270°</button>
|
|
28
|
+
<label><input type="checkbox" id="animated" checked> animated</label>
|
|
29
|
+
</p>
|
|
30
|
+
<script type="module">
|
|
31
|
+
import {Chessboard} from "../../src/Chessboard.js"
|
|
32
|
+
import {FEN} from "../../src/model/Position.js"
|
|
33
|
+
import {PieceRotation} from "../../src/extensions/piece-rotation/PieceRotation.js"
|
|
34
|
+
|
|
35
|
+
const board = new Chessboard(document.getElementById("board"), {
|
|
36
|
+
position: FEN.start,
|
|
37
|
+
assetsCache: false,
|
|
38
|
+
assetsUrl: "../../assets/",
|
|
39
|
+
style: {aspectRatio: 0.98},
|
|
40
|
+
extensions: [{class: PieceRotation}]
|
|
41
|
+
})
|
|
42
|
+
board.enableMoveInput(() => {
|
|
43
|
+
return true // allow all moves in this demo
|
|
44
|
+
})
|
|
45
|
+
window.rotate = (angle) => {
|
|
46
|
+
board.setPiecesRotation(angle, {
|
|
47
|
+
color: document.getElementById("color").value || undefined,
|
|
48
|
+
animated: document.getElementById("animated").checked
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
</body>
|
|
53
|
+
</html>
|
package/package.json
CHANGED
package/src/Chessboard.js
CHANGED
|
@@ -77,6 +77,7 @@ export class Chessboard {
|
|
|
77
77
|
async setPiece(square, piece, animated = false) {
|
|
78
78
|
const positionFrom = this.state.position.clone()
|
|
79
79
|
this.state.position.setPiece(square, piece)
|
|
80
|
+
this.view.visualMoveInput.positionChanged()
|
|
80
81
|
this.state.invokeExtensionPoints(EXTENSION_POINT.positionChanged)
|
|
81
82
|
return this.positionAnimationsQueue.enqueuePositionChange(positionFrom, this.state.position.clone(), animated)
|
|
82
83
|
}
|
|
@@ -84,6 +85,7 @@ export class Chessboard {
|
|
|
84
85
|
async movePiece(squareFrom, squareTo, animated = false) {
|
|
85
86
|
const positionFrom = this.state.position.clone()
|
|
86
87
|
this.state.position.movePiece(squareFrom, squareTo)
|
|
88
|
+
this.view.visualMoveInput.positionChanged()
|
|
87
89
|
this.state.invokeExtensionPoints(EXTENSION_POINT.positionChanged)
|
|
88
90
|
return this.positionAnimationsQueue.enqueuePositionChange(positionFrom, this.state.position.clone(), animated)
|
|
89
91
|
}
|
|
@@ -93,6 +95,7 @@ export class Chessboard {
|
|
|
93
95
|
const positionTo = new Position(fen)
|
|
94
96
|
if (positionFrom.getFen() !== positionTo.getFen()) {
|
|
95
97
|
this.state.position.setFen(fen)
|
|
98
|
+
this.view.visualMoveInput.positionChanged()
|
|
96
99
|
this.state.invokeExtensionPoints(EXTENSION_POINT.positionChanged)
|
|
97
100
|
}
|
|
98
101
|
return this.positionAnimationsQueue.enqueuePositionChange(positionFrom, this.state.position.clone(), animated)
|
|
@@ -131,6 +134,11 @@ export class Chessboard {
|
|
|
131
134
|
this.view.disableMoveInput()
|
|
132
135
|
}
|
|
133
136
|
|
|
137
|
+
// Cancel a move input that is currently in progress (leaves move input enabled).
|
|
138
|
+
cancelMoveInput() {
|
|
139
|
+
this.view.visualMoveInput.cancelMoveInput()
|
|
140
|
+
}
|
|
141
|
+
|
|
134
142
|
isMoveInputEnabled() {
|
|
135
143
|
return this.state.inputWhiteEnabled || this.state.inputBlackEnabled
|
|
136
144
|
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Author and copyright: Stefan Haack (https://shaack.com)
|
|
3
|
+
* Repository: https://github.com/shaack/cm-chessboard
|
|
4
|
+
* License: MIT, see file 'LICENSE'
|
|
5
|
+
*/
|
|
6
|
+
import {Extension, EXTENSION_POINT} from "../../model/Extension.js"
|
|
7
|
+
import {COLOR} from "../../Chessboard.js"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Rotates the piece glyphs in place, by any angle, optionally animated —
|
|
11
|
+
* either all pieces or only those of one color. The squares and the position
|
|
12
|
+
* stay untouched, only the glyphs turn around their own center. Useful for
|
|
13
|
+
* over-the-board play on a tablet lying flat between two players: rotate the
|
|
14
|
+
* pieces by 180 degrees towards the player to move.
|
|
15
|
+
*
|
|
16
|
+
* The rotation is implemented as an additional SVGTransform appended to each
|
|
17
|
+
* piece's `use` element, after the translate and scale the view sets there.
|
|
18
|
+
* Appended last, it rotates in sprite coordinates around the tile center, so
|
|
19
|
+
* it composes with any piece position and survives the move animations, which
|
|
20
|
+
* only touch the translate of the surrounding group. A MutationObserver
|
|
21
|
+
* re-applies the angles to freshly drawn pieces, so the rotation survives
|
|
22
|
+
* redraws and position changes from any code path.
|
|
23
|
+
*
|
|
24
|
+
* Registers two methods on the chessboard:
|
|
25
|
+
*
|
|
26
|
+
* // rotate all pieces, animated
|
|
27
|
+
* chessboard.setPiecesRotation(180)
|
|
28
|
+
* // rotate only the black pieces, not animated
|
|
29
|
+
* chessboard.setPiecesRotation(180, {color: COLOR.black, animated: false})
|
|
30
|
+
* // returns a Promise, resolved when the rotation is shown
|
|
31
|
+
* await chessboard.setPiecesRotation(0)
|
|
32
|
+
*
|
|
33
|
+
* chessboard.getPiecesRotation(COLOR.white) // current target angle
|
|
34
|
+
*/
|
|
35
|
+
export class PieceRotation extends Extension {
|
|
36
|
+
|
|
37
|
+
constructor(chessboard, props = {}) {
|
|
38
|
+
super(chessboard)
|
|
39
|
+
this.props = {
|
|
40
|
+
angle: 0, // the initial rotation angle in degrees, for all pieces
|
|
41
|
+
animationDuration: 300 // duration of an animated rotation in milliseconds
|
|
42
|
+
}
|
|
43
|
+
Object.assign(this.props, props)
|
|
44
|
+
this.angles = {w: this.props.angle, b: this.props.angle}
|
|
45
|
+
this.targetAngles = {w: this.props.angle, b: this.props.angle}
|
|
46
|
+
this.animations = {w: null, b: null}
|
|
47
|
+
|
|
48
|
+
// Redraws and animations replace the piece elements with fresh ones
|
|
49
|
+
// without the rotation, and they do so from several code paths — some
|
|
50
|
+
// of them after all extension points have fired (redrawPieces runs in
|
|
51
|
+
// the animation queue's callback). A MutationObserver on the pieces
|
|
52
|
+
// group catches every new element, no matter which path created it.
|
|
53
|
+
this.observer = new MutationObserver(() => {
|
|
54
|
+
this.applyAngles()
|
|
55
|
+
})
|
|
56
|
+
this.observer.observe(chessboard.view.piecesGroup, {childList: true, subtree: true})
|
|
57
|
+
this.registerExtensionPoint(EXTENSION_POINT.destroy, () => {
|
|
58
|
+
this.observer.disconnect()
|
|
59
|
+
this.cancelAnimation(COLOR.white)
|
|
60
|
+
this.cancelAnimation(COLOR.black)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
chessboard.setPiecesRotation = this.setPiecesRotation.bind(this)
|
|
64
|
+
chessboard.getPiecesRotation = this.getPiecesRotation.bind(this)
|
|
65
|
+
if (this.props.angle !== 0) {
|
|
66
|
+
this.applyAngles()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Rotate pieces to `angle` degrees. Returns a Promise which resolves when
|
|
72
|
+
* the rotation is shown, after the animation if animated.
|
|
73
|
+
*
|
|
74
|
+
* @param angle target angle in degrees
|
|
75
|
+
* @param props {color: COLOR.white|COLOR.black|undefined, animated: true}
|
|
76
|
+
* without `color` all pieces are rotated
|
|
77
|
+
*/
|
|
78
|
+
setPiecesRotation(angle, props = {}) {
|
|
79
|
+
const {color = undefined, animated = true} = props
|
|
80
|
+
const colors = color ? [color] : [COLOR.white, COLOR.black]
|
|
81
|
+
const promises = colors.map((rotatedColor) => this.rotateColor(rotatedColor, angle, animated))
|
|
82
|
+
return Promise.all(promises)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The current target angle of a color's pieces.
|
|
87
|
+
*/
|
|
88
|
+
getPiecesRotation(color = COLOR.white) {
|
|
89
|
+
return this.targetAngles[color]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// -- internal ------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
rotateColor(color, angle, animated) {
|
|
95
|
+
this.targetAngles[color] = angle
|
|
96
|
+
this.cancelAnimation(color)
|
|
97
|
+
if (!animated || this.props.animationDuration <= 0 || angle === this.angles[color]) {
|
|
98
|
+
this.angles[color] = angle
|
|
99
|
+
this.applyAngles()
|
|
100
|
+
return Promise.resolve()
|
|
101
|
+
}
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
const startAngle = this.angles[color]
|
|
104
|
+
let startTime = null
|
|
105
|
+
const animation = {resolve}
|
|
106
|
+
const step = (time) => {
|
|
107
|
+
if (!this.chessboard.state) { // board was destroyed
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
if (startTime === null) {
|
|
111
|
+
startTime = time
|
|
112
|
+
}
|
|
113
|
+
const t = Math.min(1, (time - startTime) / this.props.animationDuration)
|
|
114
|
+
const progress = t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t // easeInOut
|
|
115
|
+
this.angles[color] = startAngle + (angle - startAngle) * progress
|
|
116
|
+
this.applyAngles()
|
|
117
|
+
if (t < 1) {
|
|
118
|
+
animation.frameHandle = requestAnimationFrame(step)
|
|
119
|
+
} else {
|
|
120
|
+
this.animations[color] = null
|
|
121
|
+
resolve()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
animation.frameHandle = requestAnimationFrame(step)
|
|
125
|
+
this.animations[color] = animation
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* A superseded animation stops where it is, its promise resolves. The new
|
|
131
|
+
* rotation starts from the currently shown angle.
|
|
132
|
+
*/
|
|
133
|
+
cancelAnimation(color) {
|
|
134
|
+
const animation = this.animations[color]
|
|
135
|
+
if (animation) {
|
|
136
|
+
cancelAnimationFrame(animation.frameHandle)
|
|
137
|
+
animation.resolve()
|
|
138
|
+
this.animations[color] = null
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
applyAngles() {
|
|
143
|
+
const center = this.chessboard.props.style.pieces.tileSize / 2
|
|
144
|
+
const groups = this.chessboard.view.piecesGroup.querySelectorAll("g[data-piece]")
|
|
145
|
+
for (const group of groups) {
|
|
146
|
+
const color = group.getAttribute("data-piece").charAt(0)
|
|
147
|
+
const piece = group.querySelector("use.piece")
|
|
148
|
+
if (piece) {
|
|
149
|
+
this.rotateElement(piece, this.angles[color], center)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The rotation is the transform appended after the ones the view sets, its
|
|
156
|
+
* index in the list is stored on the element. The item is addressed by
|
|
157
|
+
* position, not by a retained object reference: WebKit returns fresh
|
|
158
|
+
* wrapper objects from `getItem`, an identity check would report the
|
|
159
|
+
* transform as lost on every call and stale rotations would pile up.
|
|
160
|
+
*/
|
|
161
|
+
rotateElement(element, angle, center) {
|
|
162
|
+
const list = element.transform.baseVal
|
|
163
|
+
let index = parseInt(element.getAttribute("data-rotation-index"), 10)
|
|
164
|
+
if (isNaN(index) || index >= list.numberOfItems) {
|
|
165
|
+
index = list.numberOfItems
|
|
166
|
+
list.appendItem(element.ownerSVGElement.createSVGTransform())
|
|
167
|
+
element.setAttribute("data-rotation-index", index)
|
|
168
|
+
}
|
|
169
|
+
list.getItem(index).setRotate(angle, center, center)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# PieceRotation extension
|
|
2
|
+
|
|
3
|
+
Rotates the piece glyphs in place, by any angle, optionally animated — either all pieces or only those of one color. The squares and the position stay untouched, only the glyphs turn around their own center.
|
|
4
|
+
|
|
5
|
+
The main use case is over-the-board play on a tablet lying flat between two players: rotate the pieces by 180 degrees towards the player to move, so both players see their pieces upright.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import {PieceRotation} from "cm-chessboard/src/extensions/piece-rotation/PieceRotation.js"
|
|
11
|
+
|
|
12
|
+
const board = new Chessboard(context, {
|
|
13
|
+
position: FEN.start,
|
|
14
|
+
assetsUrl: "./assets/",
|
|
15
|
+
extensions: [{class: PieceRotation, props: {animationDuration: 300}}]
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
// rotate all pieces, animated, returns a Promise
|
|
19
|
+
await board.setPiecesRotation(180)
|
|
20
|
+
|
|
21
|
+
// rotate only the black pieces, not animated
|
|
22
|
+
board.setPiecesRotation(180, {color: COLOR.black, animated: false})
|
|
23
|
+
|
|
24
|
+
// the current target angle of a color (default COLOR.white)
|
|
25
|
+
board.getPiecesRotation(COLOR.black)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Props
|
|
29
|
+
|
|
30
|
+
| Prop | Default | Description |
|
|
31
|
+
|---------------------|---------|--------------------------------------------------------|
|
|
32
|
+
| `angle` | `0` | Initial rotation angle in degrees, for all pieces |
|
|
33
|
+
| `animationDuration` | `300` | Duration of an animated rotation in milliseconds |
|
|
34
|
+
|
|
35
|
+
## How it works
|
|
36
|
+
|
|
37
|
+
The rotation is an additional `SVGTransform` appended to each piece's `use` element, after the translate and scale the view sets there. Appended last, it rotates in sprite coordinates around the tile center, so it composes with any piece position and survives the move animations, which only touch the translate of the surrounding group. A `MutationObserver` on the pieces group re-applies the angles to freshly drawn pieces, so the rotation survives redraws and position changes from any code path.
|
|
38
|
+
|
|
39
|
+
The rotation is independent of move input. The dragged ghost piece following the pointer is intentionally not rotated.
|
|
@@ -24,7 +24,9 @@ export const MOVE_CANCELED_REASON = {
|
|
|
24
24
|
movedOutOfBoard: "movedOutOfBoard",
|
|
25
25
|
draggedBack: "draggedBack", // dragged to the start square
|
|
26
26
|
clickedAnotherPiece: "clickedAnotherPiece", // of the same color
|
|
27
|
-
touchCanceled: "touchCanceled"
|
|
27
|
+
touchCanceled: "touchCanceled",
|
|
28
|
+
movedPieceChanged: "movedPieceChanged", // the held piece changed on the board, most likely got captured
|
|
29
|
+
canceled: "canceled" // cancelled programmatically via chessboard.cancelMoveInput()
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
const DRAG_THRESHOLD = 4
|
|
@@ -37,6 +39,7 @@ export class VisualMoveInput {
|
|
|
37
39
|
this.moveInputState = null
|
|
38
40
|
this.fromSquare = null
|
|
39
41
|
this.toSquare = null
|
|
42
|
+
this.movedPiece = null
|
|
40
43
|
|
|
41
44
|
this.setMoveInputState(MOVE_INPUT_STATE.waitForInputStart)
|
|
42
45
|
}
|
|
@@ -432,6 +435,40 @@ export class VisualMoveInput {
|
|
|
432
435
|
this.moveInputCanceledCallback(this.fromSquare, null, MOVE_CANCELED_REASON.secondaryClick)
|
|
433
436
|
}
|
|
434
437
|
|
|
438
|
+
// Cancel a move input that is currently in progress. No-op when idle.
|
|
439
|
+
cancelMoveInput() {
|
|
440
|
+
if (this.moveInputState !== MOVE_INPUT_STATE.waitForInputStart) {
|
|
441
|
+
const moveStartSquare = this.fromSquare
|
|
442
|
+
this.view.redrawPieces()
|
|
443
|
+
this.setMoveInputState(MOVE_INPUT_STATE.reset)
|
|
444
|
+
this.moveInputCanceledCallback(moveStartSquare, null, MOVE_CANCELED_REASON.canceled)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Called after the board position changed (setPosition/movePiece/setPiece).
|
|
449
|
+
// If the piece we are holding changed on its square — most likely captured
|
|
450
|
+
// by an external position update — cancel the in-progress move input.
|
|
451
|
+
positionChanged() {
|
|
452
|
+
// Only cancel while actually holding a piece. Never in moveDone/reset:
|
|
453
|
+
// completing a move itself goes through movePiece() and must not self-cancel.
|
|
454
|
+
const holdingStates = [
|
|
455
|
+
MOVE_INPUT_STATE.pieceClickedThreshold,
|
|
456
|
+
MOVE_INPUT_STATE.clickTo,
|
|
457
|
+
MOVE_INPUT_STATE.secondClickThreshold,
|
|
458
|
+
MOVE_INPUT_STATE.dragTo,
|
|
459
|
+
MOVE_INPUT_STATE.clickDragTo
|
|
460
|
+
]
|
|
461
|
+
if (this.fromSquare && holdingStates.includes(this.moveInputState) &&
|
|
462
|
+
this.chessboard.getPiece(this.fromSquare) !== this.movedPiece) {
|
|
463
|
+
const moveStartSquare = this.fromSquare
|
|
464
|
+
// drop the stale held piece so the reset branch does not stamp it
|
|
465
|
+
// back onto the square, overwriting the piece just placed there
|
|
466
|
+
this.movedPiece = null
|
|
467
|
+
this.setMoveInputState(MOVE_INPUT_STATE.reset)
|
|
468
|
+
this.moveInputCanceledCallback(moveStartSquare, null, MOVE_CANCELED_REASON.movedPieceChanged)
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
435
472
|
isDragging() {
|
|
436
473
|
return this.moveInputState === MOVE_INPUT_STATE.dragTo || this.moveInputState === MOVE_INPUT_STATE.clickDragTo
|
|
437
474
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Author and copyright: Stefan Haack (https://shaack.com)
|
|
3
|
+
* Repository: https://github.com/shaack/cm-chessboard
|
|
4
|
+
* License: MIT, see file 'LICENSE'
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {describe, it, assert} from "../node_modules/teevi/src/teevi.js"
|
|
8
|
+
import {Chessboard, COLOR} from "../src/Chessboard.js"
|
|
9
|
+
import {FEN} from "../src/model/Position.js"
|
|
10
|
+
import {PieceRotation} from "../src/extensions/piece-rotation/PieceRotation.js"
|
|
11
|
+
|
|
12
|
+
function makeBoard(props = {}) {
|
|
13
|
+
return new Chessboard(document.getElementById("TestPieceRotation"), {
|
|
14
|
+
position: FEN.start,
|
|
15
|
+
assetsUrl: "../assets/",
|
|
16
|
+
...props
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function rotationOf(useElement) {
|
|
21
|
+
// the appended rotation is the last transform in the list
|
|
22
|
+
const list = useElement.transform.baseVal
|
|
23
|
+
const item = list.getItem(list.numberOfItems - 1)
|
|
24
|
+
return item.type === SVGTransform.SVG_TRANSFORM_ROTATE ? item.angle : null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function piecesOf(chessboard, color) {
|
|
28
|
+
return chessboard.view.piecesGroup.querySelectorAll(`g[data-piece^="${color}"] use.piece`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The MutationObserver in the extension applies the angles to freshly drawn
|
|
32
|
+
// pieces in a microtask, wait one tick before asserting.
|
|
33
|
+
function nextTick() {
|
|
34
|
+
return new Promise((resolve) => setTimeout(resolve))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("TestPieceRotation", () => {
|
|
38
|
+
|
|
39
|
+
it("should rotate all pieces instantly and back", async () => {
|
|
40
|
+
const chessboard = makeBoard({extensions: [{class: PieceRotation}]})
|
|
41
|
+
assert.equal(chessboard.getPiecesRotation(), 0)
|
|
42
|
+
await chessboard.setPiecesRotation(180, {animated: false})
|
|
43
|
+
assert.equal(chessboard.getPiecesRotation(COLOR.white), 180)
|
|
44
|
+
assert.equal(chessboard.getPiecesRotation(COLOR.black), 180)
|
|
45
|
+
const pieces = chessboard.view.piecesGroup.querySelectorAll("use.piece")
|
|
46
|
+
assert.equal(pieces.length, 32)
|
|
47
|
+
for (const piece of pieces) {
|
|
48
|
+
assert.equal(rotationOf(piece), 180)
|
|
49
|
+
}
|
|
50
|
+
await chessboard.setPiecesRotation(0, {animated: false})
|
|
51
|
+
for (const piece of pieces) {
|
|
52
|
+
assert.equal(rotationOf(piece), 0)
|
|
53
|
+
}
|
|
54
|
+
chessboard.destroy()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it("should rotate only one color", async () => {
|
|
58
|
+
const chessboard = makeBoard({extensions: [{class: PieceRotation}]})
|
|
59
|
+
await chessboard.setPiecesRotation(180, {color: COLOR.black, animated: false})
|
|
60
|
+
assert.equal(chessboard.getPiecesRotation(COLOR.black), 180)
|
|
61
|
+
assert.equal(chessboard.getPiecesRotation(COLOR.white), 0)
|
|
62
|
+
for (const piece of piecesOf(chessboard, "b")) {
|
|
63
|
+
assert.equal(rotationOf(piece), 180)
|
|
64
|
+
}
|
|
65
|
+
for (const piece of piecesOf(chessboard, "w")) {
|
|
66
|
+
assert.equal(rotationOf(piece), 0)
|
|
67
|
+
}
|
|
68
|
+
chessboard.destroy()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it("should animate the rotation to the target angle", async () => {
|
|
72
|
+
const chessboard = makeBoard({
|
|
73
|
+
extensions: [{class: PieceRotation, props: {animationDuration: 50}}]
|
|
74
|
+
})
|
|
75
|
+
await chessboard.setPiecesRotation(90)
|
|
76
|
+
const pieces = chessboard.view.piecesGroup.querySelectorAll("use.piece")
|
|
77
|
+
assert.equal(pieces.length, 32)
|
|
78
|
+
for (const piece of pieces) {
|
|
79
|
+
assert.equal(rotationOf(piece), 90)
|
|
80
|
+
}
|
|
81
|
+
chessboard.destroy()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it("should keep the rotation on pieces after a position change", async () => {
|
|
85
|
+
const chessboard = makeBoard({extensions: [{class: PieceRotation}]})
|
|
86
|
+
await chessboard.setPiecesRotation(180, {animated: false})
|
|
87
|
+
await chessboard.movePiece("e2", "e4", false)
|
|
88
|
+
await nextTick()
|
|
89
|
+
const moved = chessboard.view.piecesGroup.querySelector('g[data-square="e4"] use.piece')
|
|
90
|
+
assert.true(moved !== null)
|
|
91
|
+
assert.equal(rotationOf(moved), 180)
|
|
92
|
+
// all the other pieces keep it too
|
|
93
|
+
const pieces = chessboard.view.piecesGroup.querySelectorAll("use.piece")
|
|
94
|
+
for (const piece of pieces) {
|
|
95
|
+
assert.equal(rotationOf(piece), 180)
|
|
96
|
+
}
|
|
97
|
+
chessboard.destroy()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it("should keep the rotation after an animated move", async () => {
|
|
101
|
+
const chessboard = makeBoard({
|
|
102
|
+
style: {animationDuration: 50},
|
|
103
|
+
extensions: [{class: PieceRotation}]
|
|
104
|
+
})
|
|
105
|
+
await chessboard.setPiecesRotation(180, {animated: false})
|
|
106
|
+
await chessboard.movePiece("b1", "c3", true)
|
|
107
|
+
await nextTick()
|
|
108
|
+
const moved = chessboard.view.piecesGroup.querySelector('g[data-square="c3"] use.piece')
|
|
109
|
+
assert.true(moved !== null)
|
|
110
|
+
assert.equal(rotationOf(moved), 180)
|
|
111
|
+
chessboard.destroy()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("should apply an initial angle from the props", async () => {
|
|
115
|
+
const chessboard = makeBoard({
|
|
116
|
+
extensions: [{class: PieceRotation, props: {angle: 180}}]
|
|
117
|
+
})
|
|
118
|
+
await nextTick()
|
|
119
|
+
for (const piece of chessboard.view.piecesGroup.querySelectorAll("use.piece")) {
|
|
120
|
+
assert.equal(rotationOf(piece), 180)
|
|
121
|
+
}
|
|
122
|
+
chessboard.destroy()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it("should supersede a running animation", async () => {
|
|
126
|
+
const chessboard = makeBoard({
|
|
127
|
+
extensions: [{class: PieceRotation, props: {animationDuration: 1000}}]
|
|
128
|
+
})
|
|
129
|
+
chessboard.setPiecesRotation(180) // not awaited, will be superseded
|
|
130
|
+
await chessboard.setPiecesRotation(45, {animated: false})
|
|
131
|
+
assert.equal(chessboard.getPiecesRotation(), 45)
|
|
132
|
+
const pieces = chessboard.view.piecesGroup.querySelectorAll("use.piece")
|
|
133
|
+
for (const piece of pieces) {
|
|
134
|
+
assert.equal(rotationOf(piece), 45)
|
|
135
|
+
}
|
|
136
|
+
chessboard.destroy()
|
|
137
|
+
})
|
|
138
|
+
})
|
|
@@ -231,4 +231,131 @@ describe("TestVisualMoveInput", () => {
|
|
|
231
231
|
board.destroy()
|
|
232
232
|
})
|
|
233
233
|
|
|
234
|
+
// Regression for https://github.com/shaack/cm-chessboard/pull/165
|
|
235
|
+
it("should cancel and keep the new position when the held piece is captured via setPosition", async () => {
|
|
236
|
+
const board = newBoard()
|
|
237
|
+
let canceled = null
|
|
238
|
+
board.enableMoveInput((e) => {
|
|
239
|
+
if (e.type === INPUT_EVENT_TYPE.moveInputCanceled) canceled = {square: e.squareFrom, reason: e.reason}
|
|
240
|
+
return true
|
|
241
|
+
}, COLOR.white)
|
|
242
|
+
const vmi = board.view.visualMoveInput
|
|
243
|
+
vmi.onPointerDown(mouseDown("e2"))
|
|
244
|
+
vmi.onPointerUp(mouseUp("e2"))
|
|
245
|
+
assert.equal(vmi.moveInputState, STATE_CLICK_TO)
|
|
246
|
+
|
|
247
|
+
// the opponent captures the held e2 pawn (e2 now holds a black knight)
|
|
248
|
+
const capturedFen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPnPPP/RNBQKBNR"
|
|
249
|
+
await board.setPosition(capturedFen)
|
|
250
|
+
|
|
251
|
+
assert.equal(canceled && canceled.reason, MOVE_CANCELED_REASON.movedPieceChanged)
|
|
252
|
+
assert.equal(canceled && canceled.square, "e2") // the real start square, not null
|
|
253
|
+
assert.equal(vmi.moveInputState, STATE_WAIT_FOR_INPUT_START)
|
|
254
|
+
// the position must not be corrupted back to the white pawn
|
|
255
|
+
assert.equal("" + board.getPosition(), capturedFen)
|
|
256
|
+
await tick()
|
|
257
|
+
board.destroy()
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it("should not cancel a normal move completion", async () => {
|
|
261
|
+
const board = newBoard()
|
|
262
|
+
let canceledReason
|
|
263
|
+
let finished = 0
|
|
264
|
+
board.enableMoveInput((e) => {
|
|
265
|
+
if (e.type === INPUT_EVENT_TYPE.moveInputCanceled) canceledReason = e.reason
|
|
266
|
+
if (e.type === INPUT_EVENT_TYPE.moveInputFinished) finished++
|
|
267
|
+
return true
|
|
268
|
+
}, COLOR.white)
|
|
269
|
+
const vmi = board.view.visualMoveInput
|
|
270
|
+
vmi.onPointerDown(mouseDown("e2"))
|
|
271
|
+
vmi.onPointerUp(mouseUp("e2"))
|
|
272
|
+
vmi.onPointerDown(mouseDown("e4")) // completes via moveDone -> movePiece()
|
|
273
|
+
await tick()
|
|
274
|
+
assert.equal(board.getPiece("e4"), "wp")
|
|
275
|
+
assert.equal(canceledReason, undefined) // the guard must prevent a spurious cancel
|
|
276
|
+
assert.equal(finished, 1)
|
|
277
|
+
board.destroy()
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it("should cancel when the held piece is captured via movePiece or setPiece", async () => {
|
|
281
|
+
// via setPiece
|
|
282
|
+
let board = newBoard()
|
|
283
|
+
let reason
|
|
284
|
+
board.enableMoveInput((e) => {
|
|
285
|
+
if (e.type === INPUT_EVENT_TYPE.moveInputCanceled) reason = e.reason
|
|
286
|
+
return true
|
|
287
|
+
}, COLOR.white)
|
|
288
|
+
let vmi = board.view.visualMoveInput
|
|
289
|
+
vmi.onPointerDown(mouseDown("e2"))
|
|
290
|
+
vmi.onPointerUp(mouseUp("e2"))
|
|
291
|
+
await board.setPiece("e2", "bn")
|
|
292
|
+
assert.equal(reason, MOVE_CANCELED_REASON.movedPieceChanged)
|
|
293
|
+
await tick()
|
|
294
|
+
board.destroy()
|
|
295
|
+
|
|
296
|
+
// via movePiece (a black pawn captures onto e2)
|
|
297
|
+
board = newBoard()
|
|
298
|
+
reason = undefined
|
|
299
|
+
board.enableMoveInput((e) => {
|
|
300
|
+
if (e.type === INPUT_EVENT_TYPE.moveInputCanceled) reason = e.reason
|
|
301
|
+
return true
|
|
302
|
+
}, COLOR.white)
|
|
303
|
+
vmi = board.view.visualMoveInput
|
|
304
|
+
vmi.onPointerDown(mouseDown("e2"))
|
|
305
|
+
vmi.onPointerUp(mouseUp("e2"))
|
|
306
|
+
await board.movePiece("d7", "e2")
|
|
307
|
+
assert.equal(reason, MOVE_CANCELED_REASON.movedPieceChanged)
|
|
308
|
+
await tick()
|
|
309
|
+
board.destroy()
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it("should cancel an in-progress click selection via cancelMoveInput()", async () => {
|
|
313
|
+
const board = newBoard()
|
|
314
|
+
let canceled = null
|
|
315
|
+
board.enableMoveInput((e) => {
|
|
316
|
+
if (e.type === INPUT_EVENT_TYPE.moveInputCanceled) canceled = {square: e.squareFrom, reason: e.reason}
|
|
317
|
+
return true
|
|
318
|
+
}, COLOR.white)
|
|
319
|
+
const vmi = board.view.visualMoveInput
|
|
320
|
+
vmi.onPointerDown(mouseDown("e2"))
|
|
321
|
+
vmi.onPointerUp(mouseUp("e2"))
|
|
322
|
+
assert.equal(vmi.moveInputState, STATE_CLICK_TO)
|
|
323
|
+
|
|
324
|
+
board.cancelMoveInput()
|
|
325
|
+
assert.equal(canceled && canceled.reason, MOVE_CANCELED_REASON.canceled)
|
|
326
|
+
assert.equal(canceled && canceled.square, "e2")
|
|
327
|
+
assert.equal(vmi.moveInputState, STATE_WAIT_FOR_INPUT_START)
|
|
328
|
+
await tick()
|
|
329
|
+
board.destroy()
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it("should cancel a drag in progress via cancelMoveInput() without moving the piece", async () => {
|
|
333
|
+
const board = newBoard()
|
|
334
|
+
board.enableMoveInput(() => true, COLOR.white)
|
|
335
|
+
const vmi = board.view.visualMoveInput
|
|
336
|
+
vmi.onPointerDown(mouseDown("e2", 100, 100))
|
|
337
|
+
vmi.onPointerMove(mouseMove("e4", 100, 140)) // -> dragTo
|
|
338
|
+
assert.equal(vmi.moveInputState, STATE_DRAG_TO)
|
|
339
|
+
|
|
340
|
+
board.cancelMoveInput()
|
|
341
|
+
assert.equal(vmi.moveInputState, STATE_WAIT_FOR_INPUT_START)
|
|
342
|
+
assert.equal(board.getPiece("e2"), "wp")
|
|
343
|
+
assert.equal(board.getPiece("e4"), null)
|
|
344
|
+
await tick()
|
|
345
|
+
board.destroy()
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it("should be a no-op when cancelMoveInput() is called with no move in progress", () => {
|
|
349
|
+
const board = newBoard()
|
|
350
|
+
let canceledCount = 0
|
|
351
|
+
board.enableMoveInput((e) => {
|
|
352
|
+
if (e.type === INPUT_EVENT_TYPE.moveInputCanceled) canceledCount++
|
|
353
|
+
return true
|
|
354
|
+
}, COLOR.white)
|
|
355
|
+
board.cancelMoveInput()
|
|
356
|
+
assert.equal(canceledCount, 0)
|
|
357
|
+
assert.equal(board.view.visualMoveInput.moveInputState, STATE_WAIT_FOR_INPUT_START)
|
|
358
|
+
board.destroy()
|
|
359
|
+
})
|
|
360
|
+
|
|
234
361
|
})
|
package/test/index.html
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
<div class="board" id="TestPosition"></div>
|
|
22
22
|
<div class="board" id="TestMarkers"></div>
|
|
23
23
|
<div class="board" id="TestArrows"></div>
|
|
24
|
+
<div class="board" id="TestPieceRotation"></div>
|
|
24
25
|
<script type="module">
|
|
25
26
|
import {teevi} from "../node_modules/teevi/src/teevi.js"
|
|
26
27
|
import "./TestChessboard.js"
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
import "./TestArrows.js"
|
|
30
31
|
import "./TestPosition.js"
|
|
31
32
|
import "./TestVisualMoveInput.js"
|
|
33
|
+
import "./TestPieceRotation.js"
|
|
32
34
|
teevi.run()
|
|
33
35
|
</script>
|
|
34
36
|
</body>
|