cm-chessboard 8.12.19 → 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.
|
@@ -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
|
@@ -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.
|
|
@@ -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
|
+
})
|
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>
|