@lichess-org/chessground 9.2.3
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/LICENSE +674 -0
- package/README.md +110 -0
- package/assets/chessground.base.css +224 -0
- package/assets/chessground.brown.css +62 -0
- package/assets/chessground.cburnett.css +37 -0
- package/dist/anim.d.ts +17 -0
- package/dist/anim.js +99 -0
- package/dist/anim.js.map +1 -0
- package/dist/api.d.ts +28 -0
- package/dist/api.js +98 -0
- package/dist/api.js.map +1 -0
- package/dist/autoPieces.d.ts +3 -0
- package/dist/autoPieces.js +38 -0
- package/dist/autoPieces.js.map +1 -0
- package/dist/board.d.ts +25 -0
- package/dist/board.js +331 -0
- package/dist/board.js.map +1 -0
- package/dist/chessground.d.ts +7 -0
- package/dist/chessground.js +63 -0
- package/dist/chessground.js.map +1 -0
- package/dist/chessground.min.js +1 -0
- package/dist/config.d.ts +87 -0
- package/dist/config.js +64 -0
- package/dist/config.js.map +1 -0
- package/dist/drag.d.ts +20 -0
- package/dist/drag.js +208 -0
- package/dist/drag.js.map +1 -0
- package/dist/draw.d.ts +65 -0
- package/dist/draw.js +90 -0
- package/dist/draw.js.map +1 -0
- package/dist/drop.d.ts +5 -0
- package/dist/drop.js +31 -0
- package/dist/drop.js.map +1 -0
- package/dist/events.d.ts +4 -0
- package/dist/events.js +72 -0
- package/dist/events.js.map +1 -0
- package/dist/explosion.d.ts +3 -0
- package/dist/explosion.js +18 -0
- package/dist/explosion.js.map +1 -0
- package/dist/fen.d.ts +4 -0
- package/dist/fen.js +79 -0
- package/dist/fen.js.map +1 -0
- package/dist/premove.d.ts +6 -0
- package/dist/premove.js +57 -0
- package/dist/premove.js.map +1 -0
- package/dist/render.d.ts +4 -0
- package/dist/render.js +235 -0
- package/dist/render.js.map +1 -0
- package/dist/state.d.ts +100 -0
- package/dist/state.js +92 -0
- package/dist/state.js.map +1 -0
- package/dist/svg.d.ts +8 -0
- package/dist/svg.js +348 -0
- package/dist/svg.js.map +1 -0
- package/dist/sync.d.ts +8 -0
- package/dist/sync.js +27 -0
- package/dist/sync.js.map +1 -0
- package/dist/types.d.ts +94 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +20 -0
- package/dist/util.js +89 -0
- package/dist/util.js.map +1 -0
- package/dist/wrap.d.ts +3 -0
- package/dist/wrap.js +90 -0
- package/dist/wrap.js.map +1 -0
- package/package.json +58 -0
- package/src/anim.ts +139 -0
- package/src/api.ts +187 -0
- package/src/autoPieces.ts +47 -0
- package/src/board.ts +371 -0
- package/src/chessground.ts +67 -0
- package/src/config.ts +165 -0
- package/src/drag.ts +223 -0
- package/src/draw.ts +154 -0
- package/src/drop.ts +36 -0
- package/src/events.ts +86 -0
- package/src/explosion.ts +19 -0
- package/src/fen.ts +78 -0
- package/src/premove.ts +76 -0
- package/src/render.ts +262 -0
- package/src/state.ts +199 -0
- package/src/svg.ts +441 -0
- package/src/sync.ts +36 -0
- package/src/types.ts +110 -0
- package/src/util.ts +105 -0
- package/src/wrap.ts +111 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { HeadlessState } from './state.js';
|
|
2
|
+
import { setCheck, setSelected } from './board.js';
|
|
3
|
+
import { read as fenRead } from './fen.js';
|
|
4
|
+
import { DrawShape, DrawBrushes } from './draw.js';
|
|
5
|
+
import * as cg from './types.js';
|
|
6
|
+
|
|
7
|
+
export interface Config {
|
|
8
|
+
fen?: cg.FEN; // chess position in Forsyth notation
|
|
9
|
+
orientation?: cg.Color; // board orientation. white | black
|
|
10
|
+
turnColor?: cg.Color; // turn to play. white | black
|
|
11
|
+
check?: cg.Color | boolean; // true for current color, false to unset
|
|
12
|
+
lastMove?: cg.Key[]; // squares part of the last move ["c3", "c4"]
|
|
13
|
+
selected?: cg.Key; // square currently selected "a1"
|
|
14
|
+
coordinates?: boolean; // include coords attributes
|
|
15
|
+
coordinatesOnSquares?: boolean; // include coords attributes on every square
|
|
16
|
+
autoCastle?: boolean; // immediately complete the castle by moving the rook after king move
|
|
17
|
+
viewOnly?: boolean; // don't bind events: the user will never be able to move pieces around
|
|
18
|
+
disableContextMenu?: boolean; // because who needs a context menu on a chessboard
|
|
19
|
+
addPieceZIndex?: boolean; // adds z-index values to pieces (for 3D)
|
|
20
|
+
addDimensionsCssVarsTo?: HTMLElement; // add ---cg-width and ---cg-height CSS vars containing the board's dimensions to this element
|
|
21
|
+
blockTouchScroll?: boolean; // block scrolling via touch dragging on the board, e.g. for coordinate training
|
|
22
|
+
// pieceKey: boolean; // add a data-key attribute to piece elements
|
|
23
|
+
trustAllEvents?: boolean; // disable checking for human only input (e.isTrusted)
|
|
24
|
+
highlight?: {
|
|
25
|
+
lastMove?: boolean; // add last-move class to squares
|
|
26
|
+
check?: boolean; // add check class to squares
|
|
27
|
+
custom?: cg.SquareClasses; // add custom classes to custom squares
|
|
28
|
+
};
|
|
29
|
+
animation?: {
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
duration?: number;
|
|
32
|
+
};
|
|
33
|
+
movable?: {
|
|
34
|
+
free?: boolean; // all moves are valid - board editor
|
|
35
|
+
color?: cg.Color | 'both'; // color that can move. white | black | both | undefined
|
|
36
|
+
dests?: cg.Dests; // valid moves. {"a2" ["a3" "a4"] "b1" ["a3" "c3"]}
|
|
37
|
+
showDests?: boolean; // whether to add the move-dest class on squares
|
|
38
|
+
events?: {
|
|
39
|
+
after?: (orig: cg.Key, dest: cg.Key, metadata: cg.MoveMetadata) => void; // called after the move has been played
|
|
40
|
+
afterNewPiece?: (role: cg.Role, key: cg.Key, metadata: cg.MoveMetadata) => void; // called after a new piece is dropped on the board
|
|
41
|
+
};
|
|
42
|
+
rookCastle?: boolean; // castle by moving the king to the rook
|
|
43
|
+
};
|
|
44
|
+
premovable?: {
|
|
45
|
+
enabled?: boolean; // allow premoves for color that can not move
|
|
46
|
+
showDests?: boolean; // whether to add the premove-dest class on squares
|
|
47
|
+
castle?: boolean; // whether to allow king castle premoves
|
|
48
|
+
dests?: cg.Key[]; // premove destinations for the current selection
|
|
49
|
+
customDests?: cg.Dests; // use custom valid premoves. {"a2" ["a3" "a4"] "b1" ["a3" "c3"]}
|
|
50
|
+
events?: {
|
|
51
|
+
set?: (orig: cg.Key, dest: cg.Key, metadata?: cg.SetPremoveMetadata) => void; // called after the premove has been set
|
|
52
|
+
unset?: () => void; // called after the premove has been unset
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
predroppable?: {
|
|
56
|
+
enabled?: boolean; // allow predrops for color that can not move
|
|
57
|
+
events?: {
|
|
58
|
+
set?: (role: cg.Role, key: cg.Key) => void; // called after the predrop has been set
|
|
59
|
+
unset?: () => void; // called after the predrop has been unset
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
draggable?: {
|
|
63
|
+
enabled?: boolean; // allow moves & premoves to use drag'n drop
|
|
64
|
+
distance?: number; // minimum distance to initiate a drag; in pixels
|
|
65
|
+
autoDistance?: boolean; // lets chessground set distance to zero when user drags pieces
|
|
66
|
+
showGhost?: boolean; // show ghost of piece being dragged
|
|
67
|
+
deleteOnDropOff?: boolean; // delete a piece when it is dropped off the board
|
|
68
|
+
};
|
|
69
|
+
selectable?: {
|
|
70
|
+
// disable to enforce dragging over click-click move
|
|
71
|
+
enabled?: boolean;
|
|
72
|
+
};
|
|
73
|
+
events?: {
|
|
74
|
+
change?: () => void; // called after the situation changes on the board
|
|
75
|
+
// called after a piece has been moved.
|
|
76
|
+
// capturedPiece is undefined or like {color: 'white'; 'role': 'queen'}
|
|
77
|
+
move?: (orig: cg.Key, dest: cg.Key, capturedPiece?: cg.Piece) => void;
|
|
78
|
+
dropNewPiece?: (piece: cg.Piece, key: cg.Key) => void;
|
|
79
|
+
select?: (key: cg.Key) => void; // called when a square is selected
|
|
80
|
+
insert?: (elements: cg.Elements) => void; // when the board DOM has been (re)inserted
|
|
81
|
+
};
|
|
82
|
+
drawable?: {
|
|
83
|
+
enabled?: boolean; // can draw
|
|
84
|
+
visible?: boolean; // can view
|
|
85
|
+
defaultSnapToValidMove?: boolean;
|
|
86
|
+
// false to keep the drawing if a movable piece is clicked.
|
|
87
|
+
// Clicking an empty square or immovable piece will clear the drawing regardless.
|
|
88
|
+
eraseOnClick?: boolean;
|
|
89
|
+
shapes?: DrawShape[];
|
|
90
|
+
autoShapes?: DrawShape[];
|
|
91
|
+
brushes?: DrawBrushes;
|
|
92
|
+
onChange?: (shapes: DrawShape[]) => void; // called after drawable shapes change
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function applyAnimation(state: HeadlessState, config: Config): void {
|
|
97
|
+
if (config.animation) {
|
|
98
|
+
deepMerge(state.animation, config.animation);
|
|
99
|
+
// no need for such short animations
|
|
100
|
+
if ((state.animation.duration || 0) < 70) state.animation.enabled = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function configure(state: HeadlessState, config: Config): void {
|
|
105
|
+
// don't merge destinations and autoShapes. Just override.
|
|
106
|
+
if (config.movable?.dests) state.movable.dests = undefined;
|
|
107
|
+
if (config.drawable?.autoShapes) state.drawable.autoShapes = [];
|
|
108
|
+
|
|
109
|
+
deepMerge(state, config);
|
|
110
|
+
|
|
111
|
+
// if a fen was provided, replace the pieces
|
|
112
|
+
if (config.fen) {
|
|
113
|
+
state.pieces = fenRead(config.fen);
|
|
114
|
+
state.drawable.shapes = config.drawable?.shapes || [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// apply config values that could be undefined yet meaningful
|
|
118
|
+
if ('check' in config) setCheck(state, config.check || false);
|
|
119
|
+
if ('lastMove' in config && !config.lastMove) state.lastMove = undefined;
|
|
120
|
+
// in case of ZH drop last move, there's a single square.
|
|
121
|
+
// if the previous last move had two squares,
|
|
122
|
+
// the merge algorithm will incorrectly keep the second square.
|
|
123
|
+
else if (config.lastMove) state.lastMove = config.lastMove;
|
|
124
|
+
|
|
125
|
+
// fix move/premove dests
|
|
126
|
+
if (state.selected) setSelected(state, state.selected);
|
|
127
|
+
|
|
128
|
+
applyAnimation(state, config);
|
|
129
|
+
|
|
130
|
+
if (!state.movable.rookCastle && state.movable.dests) {
|
|
131
|
+
const rank = state.movable.color === 'white' ? '1' : '8',
|
|
132
|
+
kingStartPos = ('e' + rank) as cg.Key,
|
|
133
|
+
dests = state.movable.dests.get(kingStartPos),
|
|
134
|
+
king = state.pieces.get(kingStartPos);
|
|
135
|
+
if (!dests || !king || king.role !== 'king') return;
|
|
136
|
+
state.movable.dests.set(
|
|
137
|
+
kingStartPos,
|
|
138
|
+
dests.filter(
|
|
139
|
+
d =>
|
|
140
|
+
!(d === 'a' + rank && dests.includes(('c' + rank) as cg.Key)) &&
|
|
141
|
+
!(d === 'h' + rank && dests.includes(('g' + rank) as cg.Key)),
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function deepMerge(base: any, extend: any): void {
|
|
148
|
+
for (const key in extend) {
|
|
149
|
+
if (key === '__proto__' || key === 'constructor' || !Object.prototype.hasOwnProperty.call(extend, key))
|
|
150
|
+
continue;
|
|
151
|
+
if (
|
|
152
|
+
Object.prototype.hasOwnProperty.call(base, key) &&
|
|
153
|
+
isPlainObject(base[key]) &&
|
|
154
|
+
isPlainObject(extend[key])
|
|
155
|
+
)
|
|
156
|
+
deepMerge(base[key], extend[key]);
|
|
157
|
+
else base[key] = extend[key];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isPlainObject(o: unknown): boolean {
|
|
162
|
+
if (typeof o !== 'object' || o === null) return false;
|
|
163
|
+
const proto = Object.getPrototypeOf(o);
|
|
164
|
+
return proto === Object.prototype || proto === null;
|
|
165
|
+
}
|
package/src/drag.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { State } from './state.js';
|
|
2
|
+
import * as board from './board.js';
|
|
3
|
+
import * as util from './util.js';
|
|
4
|
+
import { clear as drawClear } from './draw.js';
|
|
5
|
+
import * as cg from './types.js';
|
|
6
|
+
import { anim } from './anim.js';
|
|
7
|
+
|
|
8
|
+
export interface DragCurrent {
|
|
9
|
+
orig: cg.Key; // orig key of dragging piece
|
|
10
|
+
piece: cg.Piece;
|
|
11
|
+
origPos: cg.NumberPair; // first event position
|
|
12
|
+
pos: cg.NumberPair; // latest event position
|
|
13
|
+
started: boolean; // whether the drag has started; as per the distance setting
|
|
14
|
+
element: cg.PieceNode | (() => cg.PieceNode | undefined);
|
|
15
|
+
newPiece?: boolean; // it it a new piece from outside the board
|
|
16
|
+
force?: boolean; // can the new piece replace an existing one (editor)
|
|
17
|
+
previouslySelected?: cg.Key;
|
|
18
|
+
originTarget: EventTarget | null;
|
|
19
|
+
keyHasChanged: boolean; // whether the drag has left the orig key
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function start(s: State, e: cg.MouchEvent): void {
|
|
23
|
+
if (!(s.trustAllEvents || e.isTrusted)) return; // only trust when trustAllEvents is enabled
|
|
24
|
+
if (e.buttons !== undefined && e.buttons > 1) return; // only touch or left click
|
|
25
|
+
if (e.touches && e.touches.length > 1) return; // support one finger touch only
|
|
26
|
+
const bounds = s.dom.bounds(),
|
|
27
|
+
position = util.eventPosition(e)!,
|
|
28
|
+
orig = board.getKeyAtDomPos(position, board.whitePov(s), bounds);
|
|
29
|
+
if (!orig) return;
|
|
30
|
+
const piece = s.pieces.get(orig);
|
|
31
|
+
const previouslySelected = s.selected;
|
|
32
|
+
if (
|
|
33
|
+
!previouslySelected &&
|
|
34
|
+
s.drawable.enabled &&
|
|
35
|
+
(s.drawable.eraseOnClick || !piece || piece.color !== s.turnColor)
|
|
36
|
+
)
|
|
37
|
+
drawClear(s);
|
|
38
|
+
// Prevent touch scroll and create no corresponding mouse event, if there
|
|
39
|
+
// is an intent to interact with the board.
|
|
40
|
+
if (
|
|
41
|
+
e.cancelable !== false &&
|
|
42
|
+
(!e.touches || s.blockTouchScroll || piece || previouslySelected || pieceCloseTo(s, position))
|
|
43
|
+
)
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
else if (e.touches) return; // Handle only corresponding mouse event https://github.com/lichess-org/chessground/pull/268
|
|
46
|
+
|
|
47
|
+
const hadPremove = !!s.premovable.current;
|
|
48
|
+
const hadPredrop = !!s.predroppable.current;
|
|
49
|
+
s.stats.ctrlKey = e.ctrlKey;
|
|
50
|
+
if (s.selected && board.canMove(s, s.selected, orig)) {
|
|
51
|
+
anim(state => board.selectSquare(state, orig), s);
|
|
52
|
+
} else {
|
|
53
|
+
board.selectSquare(s, orig);
|
|
54
|
+
}
|
|
55
|
+
const stillSelected = s.selected === orig;
|
|
56
|
+
const element = pieceElementByKey(s, orig);
|
|
57
|
+
if (piece && element && stillSelected && board.isDraggable(s, orig)) {
|
|
58
|
+
s.draggable.current = {
|
|
59
|
+
orig,
|
|
60
|
+
piece,
|
|
61
|
+
origPos: position,
|
|
62
|
+
pos: position,
|
|
63
|
+
started: s.draggable.autoDistance && s.stats.dragged,
|
|
64
|
+
element,
|
|
65
|
+
previouslySelected,
|
|
66
|
+
originTarget: e.target,
|
|
67
|
+
keyHasChanged: false,
|
|
68
|
+
};
|
|
69
|
+
element.cgDragging = true;
|
|
70
|
+
element.classList.add('dragging');
|
|
71
|
+
// place ghost
|
|
72
|
+
const ghost = s.dom.elements.ghost;
|
|
73
|
+
if (ghost) {
|
|
74
|
+
ghost.className = `ghost ${piece.color} ${piece.role}`;
|
|
75
|
+
util.translate(ghost, util.posToTranslate(bounds)(util.key2pos(orig), board.whitePov(s)));
|
|
76
|
+
util.setVisible(ghost, true);
|
|
77
|
+
}
|
|
78
|
+
processDrag(s);
|
|
79
|
+
} else {
|
|
80
|
+
if (hadPremove) board.unsetPremove(s);
|
|
81
|
+
if (hadPredrop) board.unsetPredrop(s);
|
|
82
|
+
}
|
|
83
|
+
s.dom.redraw();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function pieceCloseTo(s: State, pos: cg.NumberPair): boolean {
|
|
87
|
+
const asWhite = board.whitePov(s),
|
|
88
|
+
bounds = s.dom.bounds(),
|
|
89
|
+
radiusSq = Math.pow(bounds.width / 8, 2);
|
|
90
|
+
for (const key of s.pieces.keys()) {
|
|
91
|
+
const center = util.computeSquareCenter(key, asWhite, bounds);
|
|
92
|
+
if (util.distanceSq(center, pos) <= radiusSq) return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function dragNewPiece(s: State, piece: cg.Piece, e: cg.MouchEvent, force?: boolean): void {
|
|
98
|
+
const key: cg.Key = 'a0';
|
|
99
|
+
s.pieces.set(key, piece);
|
|
100
|
+
s.dom.redraw();
|
|
101
|
+
|
|
102
|
+
const position = util.eventPosition(e)!;
|
|
103
|
+
|
|
104
|
+
s.draggable.current = {
|
|
105
|
+
orig: key,
|
|
106
|
+
piece,
|
|
107
|
+
origPos: position,
|
|
108
|
+
pos: position,
|
|
109
|
+
started: true,
|
|
110
|
+
element: () => pieceElementByKey(s, key),
|
|
111
|
+
originTarget: e.target,
|
|
112
|
+
newPiece: true,
|
|
113
|
+
force: !!force,
|
|
114
|
+
keyHasChanged: false,
|
|
115
|
+
};
|
|
116
|
+
processDrag(s);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function processDrag(s: State): void {
|
|
120
|
+
requestAnimationFrame(() => {
|
|
121
|
+
const cur = s.draggable.current;
|
|
122
|
+
if (!cur) return;
|
|
123
|
+
// cancel animations while dragging
|
|
124
|
+
if (s.animation.current?.plan.anims.has(cur.orig)) s.animation.current = undefined;
|
|
125
|
+
// if moving piece is gone, cancel
|
|
126
|
+
const origPiece = s.pieces.get(cur.orig);
|
|
127
|
+
if (!origPiece || !util.samePiece(origPiece, cur.piece)) cancel(s);
|
|
128
|
+
else {
|
|
129
|
+
if (!cur.started && util.distanceSq(cur.pos, cur.origPos) >= Math.pow(s.draggable.distance, 2))
|
|
130
|
+
cur.started = true;
|
|
131
|
+
if (cur.started) {
|
|
132
|
+
// support lazy elements
|
|
133
|
+
if (typeof cur.element === 'function') {
|
|
134
|
+
const found = cur.element();
|
|
135
|
+
if (!found) return;
|
|
136
|
+
found.cgDragging = true;
|
|
137
|
+
found.classList.add('dragging');
|
|
138
|
+
cur.element = found;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const bounds = s.dom.bounds();
|
|
142
|
+
util.translate(cur.element, [
|
|
143
|
+
cur.pos[0] - bounds.left - bounds.width / 16,
|
|
144
|
+
cur.pos[1] - bounds.top - bounds.height / 16,
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
cur.keyHasChanged ||= cur.orig !== board.getKeyAtDomPos(cur.pos, board.whitePov(s), bounds);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
processDrag(s);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function move(s: State, e: cg.MouchEvent): void {
|
|
155
|
+
// support one finger touch only
|
|
156
|
+
if (s.draggable.current && (!e.touches || e.touches.length < 2)) {
|
|
157
|
+
s.draggable.current.pos = util.eventPosition(e)!;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function end(s: State, e: cg.MouchEvent): void {
|
|
162
|
+
const cur = s.draggable.current;
|
|
163
|
+
if (!cur) return;
|
|
164
|
+
// create no corresponding mouse event
|
|
165
|
+
if (e.type === 'touchend' && e.cancelable !== false) e.preventDefault();
|
|
166
|
+
// comparing with the origin target is an easy way to test that the end event
|
|
167
|
+
// has the same touch origin
|
|
168
|
+
if (e.type === 'touchend' && cur.originTarget !== e.target && !cur.newPiece) {
|
|
169
|
+
s.draggable.current = undefined;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
board.unsetPremove(s);
|
|
173
|
+
board.unsetPredrop(s);
|
|
174
|
+
// touchend has no position; so use the last touchmove position instead
|
|
175
|
+
const eventPos = util.eventPosition(e) || cur.pos;
|
|
176
|
+
const dest = board.getKeyAtDomPos(eventPos, board.whitePov(s), s.dom.bounds());
|
|
177
|
+
if (dest && cur.started && cur.orig !== dest) {
|
|
178
|
+
if (cur.newPiece) board.dropNewPiece(s, cur.orig, dest, cur.force);
|
|
179
|
+
else {
|
|
180
|
+
s.stats.ctrlKey = e.ctrlKey;
|
|
181
|
+
if (board.userMove(s, cur.orig, dest)) s.stats.dragged = true;
|
|
182
|
+
}
|
|
183
|
+
} else if (cur.newPiece) {
|
|
184
|
+
s.pieces.delete(cur.orig);
|
|
185
|
+
} else if (s.draggable.deleteOnDropOff && !dest) {
|
|
186
|
+
s.pieces.delete(cur.orig);
|
|
187
|
+
board.callUserFunction(s.events.change);
|
|
188
|
+
}
|
|
189
|
+
if ((cur.orig === cur.previouslySelected || cur.keyHasChanged) && (cur.orig === dest || !dest))
|
|
190
|
+
board.unselect(s);
|
|
191
|
+
else if (!s.selectable.enabled) board.unselect(s);
|
|
192
|
+
|
|
193
|
+
removeDragElements(s);
|
|
194
|
+
|
|
195
|
+
s.draggable.current = undefined;
|
|
196
|
+
s.dom.redraw();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function cancel(s: State): void {
|
|
200
|
+
const cur = s.draggable.current;
|
|
201
|
+
if (cur) {
|
|
202
|
+
if (cur.newPiece) s.pieces.delete(cur.orig);
|
|
203
|
+
s.draggable.current = undefined;
|
|
204
|
+
board.unselect(s);
|
|
205
|
+
removeDragElements(s);
|
|
206
|
+
s.dom.redraw();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function removeDragElements(s: State): void {
|
|
211
|
+
const e = s.dom.elements;
|
|
212
|
+
if (e.ghost) util.setVisible(e.ghost, false);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function pieceElementByKey(s: State, key: cg.Key): cg.PieceNode | undefined {
|
|
216
|
+
let el = s.dom.elements.board.firstChild;
|
|
217
|
+
while (el) {
|
|
218
|
+
if ((el as cg.KeyedNode).cgKey === key && (el as cg.KeyedNode).tagName === 'PIECE')
|
|
219
|
+
return el as cg.PieceNode;
|
|
220
|
+
el = el.nextSibling;
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
package/src/draw.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { State } from './state.js';
|
|
2
|
+
import { unselect, cancelMove, getKeyAtDomPos, getSnappedKeyAtDomPos, whitePov } from './board.js';
|
|
3
|
+
import { eventPosition, isRightButton } from './util.js';
|
|
4
|
+
import * as cg from './types.js';
|
|
5
|
+
|
|
6
|
+
export interface DrawShape {
|
|
7
|
+
orig: cg.Key;
|
|
8
|
+
dest?: cg.Key;
|
|
9
|
+
brush?: string; // if no brush, no shape. label moved to top right of square
|
|
10
|
+
modifiers?: DrawModifiers;
|
|
11
|
+
piece?: DrawShapePiece;
|
|
12
|
+
customSvg?: { html: string; center?: 'orig' | 'dest' | 'label' }; // 100 x 100 viewbox cenetered at [50,50]
|
|
13
|
+
label?: { text: string; fill?: string }; // fill is in '#rrggbb' format
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DrawModifiers {
|
|
17
|
+
lineWidth?: number;
|
|
18
|
+
hilite?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DrawShapePiece {
|
|
22
|
+
role: cg.Role;
|
|
23
|
+
color: cg.Color;
|
|
24
|
+
scale?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DrawBrush {
|
|
28
|
+
key: string;
|
|
29
|
+
color: string;
|
|
30
|
+
opacity: number;
|
|
31
|
+
lineWidth: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DrawBrushes {
|
|
35
|
+
green: DrawBrush;
|
|
36
|
+
red: DrawBrush;
|
|
37
|
+
blue: DrawBrush;
|
|
38
|
+
yellow: DrawBrush;
|
|
39
|
+
[color: string]: DrawBrush;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface Drawable {
|
|
43
|
+
enabled: boolean; // can draw
|
|
44
|
+
visible: boolean; // can view
|
|
45
|
+
defaultSnapToValidMove: boolean;
|
|
46
|
+
eraseOnClick: boolean;
|
|
47
|
+
onChange?: (shapes: DrawShape[]) => void;
|
|
48
|
+
shapes: DrawShape[]; // user shapes
|
|
49
|
+
autoShapes: DrawShape[]; // computer shapes
|
|
50
|
+
current?: DrawCurrent;
|
|
51
|
+
brushes: DrawBrushes;
|
|
52
|
+
prevSvgHash: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface DrawCurrent {
|
|
56
|
+
orig: cg.Key; // orig key of drawing
|
|
57
|
+
dest?: cg.Key; // shape dest, or undefined for circle
|
|
58
|
+
mouseSq?: cg.Key; // square being moused over
|
|
59
|
+
pos: cg.NumberPair; // relative current position
|
|
60
|
+
brush: cg.BrushColor; // brush name for shape
|
|
61
|
+
snapToValidMove: boolean; // whether to snap to valid piece moves
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const brushes: cg.BrushColor[] = ['green', 'red', 'blue', 'yellow'];
|
|
65
|
+
|
|
66
|
+
export function start(state: State, e: cg.MouchEvent): void {
|
|
67
|
+
// support one finger touch only
|
|
68
|
+
if (e.touches && e.touches.length > 1) return;
|
|
69
|
+
e.stopPropagation();
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
e.ctrlKey ? unselect(state) : cancelMove(state);
|
|
72
|
+
const pos = eventPosition(e)!,
|
|
73
|
+
orig = getKeyAtDomPos(pos, whitePov(state), state.dom.bounds());
|
|
74
|
+
if (!orig) return;
|
|
75
|
+
state.drawable.current = {
|
|
76
|
+
orig,
|
|
77
|
+
pos,
|
|
78
|
+
brush: eventBrush(e),
|
|
79
|
+
snapToValidMove: state.drawable.defaultSnapToValidMove,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
processDraw(state);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function processDraw(state: State): void {
|
|
86
|
+
requestAnimationFrame(() => {
|
|
87
|
+
const cur = state.drawable.current;
|
|
88
|
+
if (cur) {
|
|
89
|
+
const keyAtDomPos = getKeyAtDomPos(cur.pos, whitePov(state), state.dom.bounds());
|
|
90
|
+
if (!keyAtDomPos) {
|
|
91
|
+
cur.snapToValidMove = false;
|
|
92
|
+
}
|
|
93
|
+
const mouseSq = cur.snapToValidMove
|
|
94
|
+
? getSnappedKeyAtDomPos(cur.orig, cur.pos, whitePov(state), state.dom.bounds())
|
|
95
|
+
: keyAtDomPos;
|
|
96
|
+
if (mouseSq !== cur.mouseSq) {
|
|
97
|
+
cur.mouseSq = mouseSq;
|
|
98
|
+
cur.dest = mouseSq !== cur.orig ? mouseSq : undefined;
|
|
99
|
+
state.dom.redrawNow();
|
|
100
|
+
}
|
|
101
|
+
processDraw(state);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function move(state: State, e: cg.MouchEvent): void {
|
|
107
|
+
if (state.drawable.current) state.drawable.current.pos = eventPosition(e)!;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function end(state: State): void {
|
|
111
|
+
const cur = state.drawable.current;
|
|
112
|
+
if (cur) {
|
|
113
|
+
if (cur.mouseSq) addShape(state.drawable, cur);
|
|
114
|
+
cancel(state);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function cancel(state: State): void {
|
|
119
|
+
if (state.drawable.current) {
|
|
120
|
+
state.drawable.current = undefined;
|
|
121
|
+
state.dom.redraw();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function clear(state: State): void {
|
|
126
|
+
if (state.drawable.shapes.length) {
|
|
127
|
+
state.drawable.shapes = [];
|
|
128
|
+
state.dom.redraw();
|
|
129
|
+
onChange(state.drawable);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function eventBrush(e: cg.MouchEvent): cg.BrushColor {
|
|
134
|
+
const modA = (e.shiftKey || e.ctrlKey) && isRightButton(e);
|
|
135
|
+
const modB = e.altKey || e.metaKey || e.getModifierState?.('AltGraph');
|
|
136
|
+
return brushes[(modA ? 1 : 0) + (modB ? 2 : 0)];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function addShape(drawable: Drawable, cur: DrawCurrent): void {
|
|
140
|
+
const sameShape = (s: DrawShape) => s.orig === cur.orig && s.dest === cur.dest;
|
|
141
|
+
const similar = drawable.shapes.find(sameShape);
|
|
142
|
+
if (similar) drawable.shapes = drawable.shapes.filter(s => !sameShape(s));
|
|
143
|
+
if (!similar || similar.brush !== cur.brush)
|
|
144
|
+
drawable.shapes.push({
|
|
145
|
+
orig: cur.orig,
|
|
146
|
+
dest: cur.dest,
|
|
147
|
+
brush: cur.brush,
|
|
148
|
+
});
|
|
149
|
+
onChange(drawable);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function onChange(drawable: Drawable): void {
|
|
153
|
+
if (drawable.onChange) drawable.onChange(drawable.shapes);
|
|
154
|
+
}
|
package/src/drop.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { State } from './state.js';
|
|
2
|
+
import * as cg from './types.js';
|
|
3
|
+
import * as board from './board.js';
|
|
4
|
+
import * as util from './util.js';
|
|
5
|
+
import { cancel as dragCancel } from './drag.js';
|
|
6
|
+
|
|
7
|
+
export function setDropMode(s: State, piece?: cg.Piece): void {
|
|
8
|
+
s.dropmode = {
|
|
9
|
+
active: true,
|
|
10
|
+
piece,
|
|
11
|
+
};
|
|
12
|
+
dragCancel(s);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function cancelDropMode(s: State): void {
|
|
16
|
+
s.dropmode = {
|
|
17
|
+
active: false,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function drop(s: State, e: cg.MouchEvent): void {
|
|
22
|
+
if (!s.dropmode.active) return;
|
|
23
|
+
|
|
24
|
+
board.unsetPremove(s);
|
|
25
|
+
board.unsetPredrop(s);
|
|
26
|
+
|
|
27
|
+
const piece = s.dropmode.piece;
|
|
28
|
+
|
|
29
|
+
if (piece) {
|
|
30
|
+
s.pieces.set('a0', piece);
|
|
31
|
+
const position = util.eventPosition(e);
|
|
32
|
+
const dest = position && board.getKeyAtDomPos(position, board.whitePov(s), s.dom.bounds());
|
|
33
|
+
if (dest) board.dropNewPiece(s, 'a0', dest);
|
|
34
|
+
}
|
|
35
|
+
s.dom.redraw();
|
|
36
|
+
}
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { State } from './state.js';
|
|
2
|
+
import * as drag from './drag.js';
|
|
3
|
+
import * as draw from './draw.js';
|
|
4
|
+
import { drop } from './drop.js';
|
|
5
|
+
import { isRightButton } from './util.js';
|
|
6
|
+
import * as cg from './types.js';
|
|
7
|
+
|
|
8
|
+
type MouchBind = (e: cg.MouchEvent) => void;
|
|
9
|
+
type StateMouchBind = (d: State, e: cg.MouchEvent) => void;
|
|
10
|
+
|
|
11
|
+
export function bindBoard(s: State, onResize: () => void): void {
|
|
12
|
+
const boardEl = s.dom.elements.board;
|
|
13
|
+
|
|
14
|
+
if ('ResizeObserver' in window) new ResizeObserver(onResize).observe(s.dom.elements.wrap);
|
|
15
|
+
|
|
16
|
+
if (s.disableContextMenu || s.drawable.enabled) {
|
|
17
|
+
boardEl.addEventListener('contextmenu', e => e.preventDefault());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (s.viewOnly) return;
|
|
21
|
+
|
|
22
|
+
// Cannot be passive, because we prevent touch scrolling and dragging of
|
|
23
|
+
// selected elements.
|
|
24
|
+
const onStart = startDragOrDraw(s);
|
|
25
|
+
boardEl.addEventListener('touchstart', onStart as EventListener, {
|
|
26
|
+
passive: false,
|
|
27
|
+
});
|
|
28
|
+
boardEl.addEventListener('mousedown', onStart as EventListener, {
|
|
29
|
+
passive: false,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// returns the unbind function
|
|
34
|
+
export function bindDocument(s: State, onResize: () => void): cg.Unbind {
|
|
35
|
+
const unbinds: cg.Unbind[] = [];
|
|
36
|
+
|
|
37
|
+
// Old versions of Edge and Safari do not support ResizeObserver. Send
|
|
38
|
+
// chessground.resize if a user action has changed the bounds of the board.
|
|
39
|
+
if (!('ResizeObserver' in window)) unbinds.push(unbindable(document.body, 'chessground.resize', onResize));
|
|
40
|
+
|
|
41
|
+
if (!s.viewOnly) {
|
|
42
|
+
const onmove = dragOrDraw(s, drag.move, draw.move);
|
|
43
|
+
const onend = dragOrDraw(s, drag.end, draw.end);
|
|
44
|
+
|
|
45
|
+
for (const ev of ['touchmove', 'mousemove'])
|
|
46
|
+
unbinds.push(unbindable(document, ev, onmove as EventListener));
|
|
47
|
+
for (const ev of ['touchend', 'mouseup']) unbinds.push(unbindable(document, ev, onend as EventListener));
|
|
48
|
+
|
|
49
|
+
const onScroll = () => s.dom.bounds.clear();
|
|
50
|
+
unbinds.push(unbindable(document, 'scroll', onScroll, { capture: true, passive: true }));
|
|
51
|
+
unbinds.push(unbindable(window, 'resize', onScroll, { passive: true }));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return () => unbinds.forEach(f => f());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function unbindable(
|
|
58
|
+
el: EventTarget,
|
|
59
|
+
eventName: string,
|
|
60
|
+
callback: EventListener,
|
|
61
|
+
options?: AddEventListenerOptions,
|
|
62
|
+
): cg.Unbind {
|
|
63
|
+
el.addEventListener(eventName, callback, options);
|
|
64
|
+
return () => el.removeEventListener(eventName, callback, options);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const startDragOrDraw =
|
|
68
|
+
(s: State): MouchBind =>
|
|
69
|
+
e => {
|
|
70
|
+
if (s.draggable.current) drag.cancel(s);
|
|
71
|
+
else if (s.drawable.current) draw.cancel(s);
|
|
72
|
+
else if (e.shiftKey || isRightButton(e)) {
|
|
73
|
+
if (s.drawable.enabled) draw.start(s, e);
|
|
74
|
+
} else if (!s.viewOnly) {
|
|
75
|
+
if (s.dropmode.active) drop(s, e);
|
|
76
|
+
else drag.start(s, e);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const dragOrDraw =
|
|
81
|
+
(s: State, withDrag: StateMouchBind, withDraw: StateMouchBind): MouchBind =>
|
|
82
|
+
e => {
|
|
83
|
+
if (s.drawable.current) {
|
|
84
|
+
if (s.drawable.enabled) withDraw(s, e);
|
|
85
|
+
} else if (!s.viewOnly) withDrag(s, e);
|
|
86
|
+
};
|
package/src/explosion.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { State } from './state.js';
|
|
2
|
+
import { Key } from './types.js';
|
|
3
|
+
|
|
4
|
+
export function explosion(state: State, keys: Key[]): void {
|
|
5
|
+
state.exploding = { stage: 1, keys };
|
|
6
|
+
state.dom.redraw();
|
|
7
|
+
setTimeout(() => {
|
|
8
|
+
setStage(state, 2);
|
|
9
|
+
setTimeout(() => setStage(state, undefined), 120);
|
|
10
|
+
}, 120);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function setStage(state: State, stage: number | undefined): void {
|
|
14
|
+
if (state.exploding) {
|
|
15
|
+
if (stage) state.exploding.stage = stage;
|
|
16
|
+
else state.exploding = undefined;
|
|
17
|
+
state.dom.redraw();
|
|
18
|
+
}
|
|
19
|
+
}
|