@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/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
- if (fading && elPieceName === pieceNameOf(fading)) {
80
- el.classList.add('fading');
81
- el.cgFading = true;
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 dests = s.movable.dests?.get(s.selected);
225
- if (dests)
226
- for (const k of dests) {
227
- addSquare(squares, k, 'move-dest' + (s.pieces.has(k) ? ' oc' : ''));
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"; "c4"]
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
- type AngleSlots = Set<number>; // arrow angle slots for label positioning
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
- function pieceHash(piece: DrawShapePiece): Hash {
181
- return [piece.color, piece.role, piece.scale].filter(x => x).join(',');
182
- }
184
+ const pieceHash = (piece: DrawShapePiece): Hash =>
185
+ [piece.color, piece.role, piece.scale].filter(x => x).join(',');
183
186
 
184
- function modifiersHash(m: DrawModifiers): Hash {
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
- function orient(pos: cg.Pos, color: cg.Color): cg.Pos {
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
- function isShort(dest: cg.Key | undefined, dests: ArrowDests) {
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
- function createElement(tagName: string): SVGElement {
362
- return document.createElementNS('http://www.w3.org/2000/svg', tagName);
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
- function makeCustomBrush(base: DrawBrush, modifiers: DrawModifiers | undefined): DrawBrush {
373
- return !modifiers
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
- function circleWidth(): [number, number] {
384
- return [3 / 64, 4 / 64];
385
- }
389
+ const circleWidth = (): [number, number] => [3 / 64, 4 / 64];
386
390
 
387
- function lineWidth(brush: DrawBrush, current: boolean): number {
388
- return ((brush.lineWidth || 10) * (current ? 0.85 : 1)) / 64;
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
- function opacity(brush: DrawBrush, current: boolean, pendingErase: boolean): number {
397
- return (brush.opacity || 1) * (pendingErase ? 0.6 : current ? 0.9 : 1);
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
- function arrowMargin(shorten: boolean): number {
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
- function moveAngle(from: cg.NumberPair, to: cg.NumberPair, asSlot = true) {
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
- function dist(from: cg.NumberPair, to: cg.NumberPair): number {
433
- return Math.sqrt([from[0] - to[0], from[1] - to[1]].reduce((acc, x) => acc + x * x, 0));
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, false);
447
+ const angle = moveAngle(from, to);
449
448
  if (slots) {
450
449
  mag -= 33 / 64; // reduce by arrowhead length
451
- if (slots.size > 1) {
450
+ if (anyTwoCloserThan90Degrees(slots)) {
452
451
  mag -= 10 / 64; // reduce by shortening factor
453
- const slot = moveAngle(from, to);
454
- if (slots.has((slot + 1) % 16) || slots.has((slot + 15) % 16)) {
455
- if (slot & 1) mag -= 0.4;
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(renderCoords(ranks, 'ranks' + orientClass + ranksPositionClass));
72
- container.appendChild(renderCoords(files, 'files' + orientClass));
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
- for (const elem of elems) {
101
- f = createEl('coord');
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
  }