@lichess-org/chessground 9.8.5 → 9.10.1
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 +1 -1
- package/dist/chessground.min.js +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/premove.js +12 -139
- package/dist/premove.js.map +1 -1
- package/dist/render.js +22 -25
- package/dist/render.js.map +1 -1
- package/dist/state.d.ts +0 -1
- package/dist/state.js.map +1 -1
- package/dist/svg.d.ts +1 -1
- package/dist/svg.js +36 -56
- package/dist/svg.js.map +1 -1
- package/dist/types.d.ts +0 -2
- package/dist/wrap.js +8 -7
- package/dist/wrap.js.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +1 -1
- package/src/premove.ts +13 -185
- package/src/render.ts +23 -28
- package/src/state.ts +1 -2
- package/src/svg.ts +45 -48
- package/src/types.ts +0 -2
- package/src/wrap.ts +11 -7
package/src/render.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { State } from './state.js';
|
|
2
2
|
import { key2pos, createEl, posToTranslate as posToTranslateFromBounds, translate } from './util.js';
|
|
3
|
+
import * as util from './util.js';
|
|
3
4
|
import { whitePov } from './board.js';
|
|
4
5
|
import { AnimCurrent, AnimVectors, AnimVector, AnimFadings } from './anim.js';
|
|
5
6
|
import { DragCurrent } from './drag.js';
|
|
@@ -71,23 +72,15 @@ export function render(s: State): void {
|
|
|
71
72
|
if (s.addPieceZIndex) el.style.zIndex = posZIndex(key2pos(k), asWhite);
|
|
72
73
|
}
|
|
73
74
|
// same piece: flag as same
|
|
74
|
-
if (elPieceName === pieceNameOf(pieceAtKey) && (!fading || !el.cgFading))
|
|
75
|
-
samePieces.add(k);
|
|
76
|
-
}
|
|
75
|
+
if (elPieceName === pieceNameOf(pieceAtKey) && (!fading || !el.cgFading)) samePieces.add(k);
|
|
77
76
|
// different piece: flag as moved unless it is a fading piece
|
|
78
|
-
else {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
} else {
|
|
83
|
-
appendValue(movedPieces, elPieceName, el);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
77
|
+
else if (fading && elPieceName === pieceNameOf(fading)) {
|
|
78
|
+
el.classList.add('fading');
|
|
79
|
+
el.cgFading = true;
|
|
80
|
+
} else appendValue(movedPieces, elPieceName, el);
|
|
86
81
|
}
|
|
87
82
|
// no piece: flag as moved
|
|
88
|
-
else
|
|
89
|
-
appendValue(movedPieces, elPieceName, el);
|
|
90
|
-
}
|
|
83
|
+
else appendValue(movedPieces, elPieceName, el);
|
|
91
84
|
} else if (isSquareNode(el)) {
|
|
92
85
|
const cn = el.className;
|
|
93
86
|
if (squares.get(k) === cn) sameSquares.add(k);
|
|
@@ -205,32 +198,34 @@ function posZIndex(pos: cg.Pos, asWhite: boolean): string {
|
|
|
205
198
|
const minZ = 3;
|
|
206
199
|
const rank = pos[1];
|
|
207
200
|
const z = asWhite ? minZ + 7 - rank : minZ + rank;
|
|
208
|
-
|
|
209
201
|
return `${z}`;
|
|
210
202
|
}
|
|
211
203
|
|
|
212
204
|
const pieceNameOf = (piece: cg.Piece): string => `${piece.color} ${piece.role}`;
|
|
213
205
|
|
|
206
|
+
const normalizeLastMoveStandardRookCastle = (s: State, k: cg.Key): cg.Key =>
|
|
207
|
+
!!s.lastMove?.[1] &&
|
|
208
|
+
!s.pieces.has(s.lastMove[1]) &&
|
|
209
|
+
s.lastMove[0][0] === 'e' &&
|
|
210
|
+
['h', 'a'].includes(s.lastMove[1][0]) &&
|
|
211
|
+
s.lastMove[0][1] === s.lastMove[1][1] &&
|
|
212
|
+
util.squaresBetween(...key2pos(s.lastMove[0]), ...key2pos(s.lastMove[1])).some(sq => s.pieces.has(sq))
|
|
213
|
+
? (((k > s.lastMove[0] ? 'g' : 'c') + k[1]) as cg.Key)
|
|
214
|
+
: k;
|
|
215
|
+
|
|
214
216
|
function computeSquareClasses(s: State): cg.SquareClasses {
|
|
215
217
|
const squares: cg.SquareClasses = new Map();
|
|
216
218
|
if (s.lastMove && s.highlight.lastMove)
|
|
217
|
-
for (const k of s.lastMove)
|
|
218
|
-
addSquare(squares, k, 'last-move');
|
|
219
|
-
}
|
|
219
|
+
for (const [i, k] of s.lastMove.entries())
|
|
220
|
+
addSquare(squares, i === 1 ? normalizeLastMoveStandardRookCastle(s, k) : k, 'last-move');
|
|
220
221
|
if (s.check && s.highlight.check) addSquare(squares, s.check, 'check');
|
|
221
222
|
if (s.selected) {
|
|
222
223
|
addSquare(squares, s.selected, 'selected');
|
|
223
224
|
if (s.movable.showDests) {
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
const pDests = s.premovable.customDests?.get(s.selected) ?? s.premovable.dests;
|
|
230
|
-
if (pDests)
|
|
231
|
-
for (const k of pDests) {
|
|
232
|
-
addSquare(squares, k, 'premove-dest' + (s.pieces.has(k) ? ' oc' : ''));
|
|
233
|
-
}
|
|
225
|
+
for (const k of s.movable.dests?.get(s.selected) ?? [])
|
|
226
|
+
addSquare(squares, k, 'move-dest' + (s.pieces.has(k) ? ' oc' : ''));
|
|
227
|
+
for (const k of s.premovable.customDests?.get(s.selected) ?? s.premovable.dests ?? [])
|
|
228
|
+
addSquare(squares, k, 'premove-dest' + (s.pieces.has(k) ? ' oc' : ''));
|
|
234
229
|
}
|
|
235
230
|
}
|
|
236
231
|
const premove = s.premovable.current;
|
package/src/state.ts
CHANGED
|
@@ -10,7 +10,7 @@ export interface HeadlessState {
|
|
|
10
10
|
orientation: cg.Color; // board orientation. white | black
|
|
11
11
|
turnColor: cg.Color; // turn to play. white | black
|
|
12
12
|
check?: cg.Key; // square currently in check "a2"
|
|
13
|
-
lastMove?: cg.Key[]; // squares part of the last move ["c3"
|
|
13
|
+
lastMove?: cg.Key[]; // squares part of the last move ["c3", "c4"]
|
|
14
14
|
selected?: cg.Key; // square currently selected "a1"
|
|
15
15
|
coordinates: boolean; // include coords attributes
|
|
16
16
|
coordinatesOnSquares: boolean; // include coords attributes on every square
|
|
@@ -52,7 +52,6 @@ export interface HeadlessState {
|
|
|
52
52
|
dests?: cg.Key[]; // premove destinations for the current selection
|
|
53
53
|
customDests?: cg.Dests; // use custom valid premoves. {"a2" ["a3" "a4"] "b1" ["a3" "c3"]}
|
|
54
54
|
current?: cg.KeyPair; // keys of the current saved premove ["e2" "e4"]
|
|
55
|
-
unrestrictedPremoves?: boolean; // if falsy, the positions of friendly pieces will be used to trim premove options
|
|
56
55
|
additionalPremoveRequirements: cg.Mobility;
|
|
57
56
|
events: {
|
|
58
57
|
set?: (orig: cg.Key, dest: cg.Key, metadata?: cg.SetPremoveMetadata) => void; // called after the premove has been set
|
package/src/svg.ts
CHANGED
|
@@ -32,7 +32,9 @@ import * as cg from './types.js';
|
|
|
32
32
|
|
|
33
33
|
type CustomBrushes = Map<string, DrawBrush>; // by hash
|
|
34
34
|
type Svg = { el: SVGElement; isCustom?: boolean };
|
|
35
|
-
|
|
35
|
+
const angleSlotVals = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] as const;
|
|
36
|
+
type AngleSlot = (typeof angleSlotVals)[number];
|
|
37
|
+
type AngleSlots = Set<AngleSlot>; // arrow angle slots for label positioning
|
|
36
38
|
type ArrowDests = Map<cg.Key | undefined, AngleSlots>; // angle slots per dest
|
|
37
39
|
|
|
38
40
|
export { createElement, setAttributes };
|
|
@@ -58,7 +60,7 @@ export function renderSvg(state: State, els: cg.Elements): void {
|
|
|
58
60
|
const sources = dests.get(s.dest) ?? new Set(),
|
|
59
61
|
from = pos2user(orient(key2pos(s.orig), state.orientation), bounds),
|
|
60
62
|
to = pos2user(orient(key2pos(s.dest), state.orientation), bounds);
|
|
61
|
-
sources.add(moveAngle(from, to));
|
|
63
|
+
sources.add(angleToSlot(moveAngle(from, to)));
|
|
62
64
|
dests.set(s.dest, sources);
|
|
63
65
|
}
|
|
64
66
|
const shapes: SyncableShape[] = [];
|
|
@@ -71,14 +73,14 @@ export function renderSvg(state: State, els: cg.Elements): void {
|
|
|
71
73
|
shape: s,
|
|
72
74
|
current: false,
|
|
73
75
|
pendingErase: isPendingErase,
|
|
74
|
-
hash: shapeHash(s, isShort(s.dest, dests), false, bounds, isPendingErase),
|
|
76
|
+
hash: shapeHash(s, isShort(s.dest, dests), false, bounds, isPendingErase, angleCount(s.dest, dests)),
|
|
75
77
|
});
|
|
76
78
|
}
|
|
77
79
|
if (cur && pendingEraseIdx === -1)
|
|
78
80
|
shapes.push({
|
|
79
81
|
shape: cur,
|
|
80
82
|
current: true,
|
|
81
|
-
hash: shapeHash(cur, isShort(cur.dest, dests), true, bounds, false),
|
|
83
|
+
hash: shapeHash(cur, isShort(cur.dest, dests), true, bounds, false, angleCount(cur.dest, dests)),
|
|
82
84
|
pendingErase: false,
|
|
83
85
|
});
|
|
84
86
|
|
|
@@ -156,6 +158,7 @@ function shapeHash(
|
|
|
156
158
|
current: boolean,
|
|
157
159
|
bounds: DOMRectReadOnly,
|
|
158
160
|
pendingErase: boolean,
|
|
161
|
+
angleCountOfDest: number,
|
|
159
162
|
): Hash {
|
|
160
163
|
// a shape and an overlay svg share a lifetime and have the same cgHash attribute
|
|
161
164
|
return [
|
|
@@ -163,6 +166,7 @@ function shapeHash(
|
|
|
163
166
|
bounds.height,
|
|
164
167
|
current,
|
|
165
168
|
pendingErase && 'pendingErase',
|
|
169
|
+
angleCountOfDest,
|
|
166
170
|
orig,
|
|
167
171
|
dest,
|
|
168
172
|
brush,
|
|
@@ -177,13 +181,10 @@ function shapeHash(
|
|
|
177
181
|
.join(',');
|
|
178
182
|
}
|
|
179
183
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
184
|
+
const pieceHash = (piece: DrawShapePiece): Hash =>
|
|
185
|
+
[piece.color, piece.role, piece.scale].filter(x => x).join(',');
|
|
183
186
|
|
|
184
|
-
|
|
185
|
-
return [m.lineWidth, m.hilite].filter(x => x).join(',');
|
|
186
|
-
}
|
|
187
|
+
const modifiersHash = (m: DrawModifiers): Hash => [m.lineWidth, m.hilite].filter(x => x).join(',');
|
|
187
188
|
|
|
188
189
|
function textHash(s: string): Hash {
|
|
189
190
|
// Rolling hash with base 31 (cf. https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript)
|
|
@@ -350,17 +351,23 @@ function renderLabel(
|
|
|
350
351
|
return g;
|
|
351
352
|
}
|
|
352
353
|
|
|
353
|
-
|
|
354
|
-
return color === 'white' ? pos : [7 - pos[0], 7 - pos[1]];
|
|
355
|
-
}
|
|
354
|
+
const orient = (pos: cg.Pos, color: cg.Color): cg.Pos => (color === 'white' ? pos : [7 - pos[0], 7 - pos[1]]);
|
|
356
355
|
|
|
357
|
-
|
|
358
|
-
return true === (dest && dests.has(dest) && dests.get(dest)!.size > 1);
|
|
359
|
-
}
|
|
356
|
+
const mod = (n: number, m: number): number => ((n % m) + m) % m;
|
|
360
357
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
358
|
+
const rotateAngleSlot = (slot: AngleSlot, steps: number): AngleSlot => mod(slot + steps, 16) as AngleSlot;
|
|
359
|
+
|
|
360
|
+
const anyTwoCloserThan90Degrees = (slots: AngleSlots): boolean =>
|
|
361
|
+
[...slots].some(slot => [-3, -2, -1, 1, 2, 3].some(i => slots.has(rotateAngleSlot(slot, i))));
|
|
362
|
+
|
|
363
|
+
const isShort = (dest: cg.Key | undefined, dests: ArrowDests): boolean =>
|
|
364
|
+
!!dest && dests.has(dest) && anyTwoCloserThan90Degrees(dests.get(dest)!);
|
|
365
|
+
|
|
366
|
+
const createElement = (tagName: string): SVGElement =>
|
|
367
|
+
document.createElementNS('http://www.w3.org/2000/svg', tagName);
|
|
368
|
+
|
|
369
|
+
const angleCount = (dest: cg.Key | undefined, dests: ArrowDests): number =>
|
|
370
|
+
dest && dests.has(dest) ? dests.get(dest)!.size : 0;
|
|
364
371
|
|
|
365
372
|
function setAttributes(el: SVGElement, attrs: { [key: string]: any }): SVGElement {
|
|
366
373
|
for (const key in attrs) {
|
|
@@ -369,8 +376,8 @@ function setAttributes(el: SVGElement, attrs: { [key: string]: any }): SVGElemen
|
|
|
369
376
|
return el;
|
|
370
377
|
}
|
|
371
378
|
|
|
372
|
-
|
|
373
|
-
|
|
379
|
+
const makeCustomBrush = (base: DrawBrush, modifiers: DrawModifiers | undefined): DrawBrush =>
|
|
380
|
+
!modifiers
|
|
374
381
|
? base
|
|
375
382
|
: {
|
|
376
383
|
color: base.color,
|
|
@@ -378,28 +385,21 @@ function makeCustomBrush(base: DrawBrush, modifiers: DrawModifiers | undefined):
|
|
|
378
385
|
lineWidth: Math.round(modifiers.lineWidth || base.lineWidth),
|
|
379
386
|
key: [base.key, modifiers.lineWidth].filter(x => x).join(''),
|
|
380
387
|
};
|
|
381
|
-
}
|
|
382
388
|
|
|
383
|
-
|
|
384
|
-
return [3 / 64, 4 / 64];
|
|
385
|
-
}
|
|
389
|
+
const circleWidth = (): [number, number] => [3 / 64, 4 / 64];
|
|
386
390
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
391
|
+
const lineWidth = (brush: DrawBrush, current: boolean): number =>
|
|
392
|
+
((brush.lineWidth || 10) * (current ? 0.85 : 1)) / 64;
|
|
390
393
|
|
|
391
394
|
function hiliteOf(shape: DrawShape): { key?: string; color?: string } {
|
|
392
395
|
const hilite = shape.modifiers?.hilite;
|
|
393
396
|
return { key: hilite && `hilite-${hilite.replace('#', '')}`, color: hilite };
|
|
394
397
|
}
|
|
395
398
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
399
|
+
const opacity = (brush: DrawBrush, current: boolean, pendingErase: boolean): number =>
|
|
400
|
+
(brush.opacity || 1) * (pendingErase ? 0.6 : current ? 0.9 : 1);
|
|
399
401
|
|
|
400
|
-
|
|
401
|
-
return (shorten ? 20 : 10) / 64;
|
|
402
|
-
}
|
|
402
|
+
const arrowMargin = (shorten: boolean): number => (shorten ? 20 : 10) / 64;
|
|
403
403
|
|
|
404
404
|
function pos2user(pos: cg.Pos, bounds: DOMRectReadOnly): cg.NumberPair {
|
|
405
405
|
const xScale = Math.min(1, bounds.width / bounds.height);
|
|
@@ -424,14 +424,13 @@ function filterBox(from: cg.NumberPair, to: cg.NumberPair): SVGElement {
|
|
|
424
424
|
});
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
-
|
|
428
|
-
const angle = Math.atan2(to[1] - from[1], to[0] - from[0]) + Math.PI;
|
|
429
|
-
return asSlot ? (Math.round((angle * 8) / Math.PI) + 16) % 16 : angle;
|
|
430
|
-
}
|
|
427
|
+
const angleToSlot = (angle: number): AngleSlot => mod(Math.round((angle * 8) / Math.PI), 16) as AngleSlot;
|
|
431
428
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
429
|
+
const moveAngle = (from: cg.NumberPair, to: cg.NumberPair): number =>
|
|
430
|
+
Math.atan2(to[1] - from[1], to[0] - from[0]) + Math.PI;
|
|
431
|
+
|
|
432
|
+
const dist = (from: cg.NumberPair, to: cg.NumberPair): number =>
|
|
433
|
+
Math.sqrt([from[0] - to[0], from[1] - to[1]].reduce((acc, x) => acc + x * x, 0));
|
|
435
434
|
|
|
436
435
|
/*
|
|
437
436
|
try to place label at the junction of the destination shaft and arrowhead. if there's more than
|
|
@@ -445,16 +444,14 @@ function dist(from: cg.NumberPair, to: cg.NumberPair): number {
|
|
|
445
444
|
function labelCoords(from: cg.NumberPair, to: cg.NumberPair, slots?: AngleSlots): cg.NumberPair {
|
|
446
445
|
let mag = dist(from, to);
|
|
447
446
|
//if (mag === 0) return [from[0], from[1]];
|
|
448
|
-
const angle = moveAngle(from, to
|
|
447
|
+
const angle = moveAngle(from, to);
|
|
449
448
|
if (slots) {
|
|
450
449
|
mag -= 33 / 64; // reduce by arrowhead length
|
|
451
|
-
if (slots
|
|
450
|
+
if (anyTwoCloserThan90Degrees(slots)) {
|
|
452
451
|
mag -= 10 / 64; // reduce by shortening factor
|
|
453
|
-
const slot =
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
// and by label size for the knight if another arrow is within pi / 8.
|
|
457
|
-
}
|
|
452
|
+
const slot = angleToSlot(angle);
|
|
453
|
+
// reduce by label size for the knight if another arrow is within pi / 8:
|
|
454
|
+
if (slot & 1 && [-1, 1].some(s => slots.has(rotateAngleSlot(slot, s)))) mag -= 0.4;
|
|
458
455
|
}
|
|
459
456
|
}
|
|
460
457
|
return [from[0] - Math.cos(angle) * mag, from[1] - Math.sin(angle) * mag].map(
|
package/src/types.ts
CHANGED
|
@@ -124,9 +124,7 @@ export type MobilityContext = {
|
|
|
124
124
|
allPieces: Pieces;
|
|
125
125
|
friendlies: Pieces;
|
|
126
126
|
enemies: Pieces;
|
|
127
|
-
unrestrictedPremoves: boolean;
|
|
128
127
|
color: Color;
|
|
129
|
-
canCastle: boolean;
|
|
130
128
|
rookFilesFriendlies: number[];
|
|
131
129
|
lastMove: Key[] | undefined;
|
|
132
130
|
};
|
package/src/wrap.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HeadlessState } from './state.js';
|
|
2
2
|
import { setVisible, createEl } from './util.js';
|
|
3
|
-
import { colors, files, ranks, Elements } from './types.js';
|
|
3
|
+
import { colors, files, ranks, Elements, Color } from './types.js';
|
|
4
4
|
import { createElement as createSVG, setAttributes, createDefs } from './svg.js';
|
|
5
5
|
|
|
6
6
|
export function renderWrap(element: HTMLElement, s: HeadlessState): Elements {
|
|
@@ -64,12 +64,15 @@ export function renderWrap(element: HTMLElement, s: HeadlessState): Elements {
|
|
|
64
64
|
renderCoords(
|
|
65
65
|
ranks.map(r => f + r),
|
|
66
66
|
'squares rank' + rankN(i) + orientClass + ranksPositionClass,
|
|
67
|
+
i % 2 === 0 ? 'black' : 'white',
|
|
67
68
|
),
|
|
68
69
|
),
|
|
69
70
|
);
|
|
70
71
|
} else {
|
|
71
|
-
container.appendChild(
|
|
72
|
-
|
|
72
|
+
container.appendChild(
|
|
73
|
+
renderCoords(ranks, 'ranks' + ranksPositionClass, s.ranksPosition === 'right' ? 'white' : 'black'),
|
|
74
|
+
);
|
|
75
|
+
container.appendChild(renderCoords(files, 'files', 'black'));
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
|
|
@@ -94,13 +97,14 @@ function svgContainer(cls: string, isShapes: boolean) {
|
|
|
94
97
|
return svg;
|
|
95
98
|
}
|
|
96
99
|
|
|
97
|
-
function renderCoords(elems: readonly string[], className: string): HTMLElement {
|
|
100
|
+
function renderCoords(elems: readonly string[], className: string, firstColor: Color): HTMLElement {
|
|
98
101
|
const el = createEl('coords', className);
|
|
99
102
|
let f: HTMLElement;
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
elems.forEach((elem, i) => {
|
|
104
|
+
const light = i % 2 === (firstColor === 'white' ? 0 : 1);
|
|
105
|
+
f = createEl('coord', `coord-${light ? 'light' : 'dark'}`);
|
|
102
106
|
f.textContent = elem;
|
|
103
107
|
el.appendChild(f);
|
|
104
|
-
}
|
|
108
|
+
});
|
|
105
109
|
return el;
|
|
106
110
|
}
|