@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/svg.ts
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { State } from './state.js';
|
|
2
|
+
import { key2pos } from './util.js';
|
|
3
|
+
import { Drawable, DrawShape, DrawShapePiece, DrawBrush, DrawBrushes, DrawModifiers } from './draw.js';
|
|
4
|
+
import { SyncableShape, Hash } from './sync.js';
|
|
5
|
+
import * as cg from './types.js';
|
|
6
|
+
|
|
7
|
+
type CustomBrushes = Map<string, DrawBrush>; // by hash
|
|
8
|
+
type Svg = { el: SVGElement; isCustom?: boolean };
|
|
9
|
+
type AngleSlots = Set<number>; // arrow angle slots for label positioning
|
|
10
|
+
type ArrowDests = Map<cg.Key | undefined, AngleSlots>; // angle slots per dest
|
|
11
|
+
|
|
12
|
+
const hilites: { [name: string]: DrawBrush } = {
|
|
13
|
+
hilitePrimary: { key: 'hilitePrimary', color: '#3291ff', opacity: 1, lineWidth: 1 },
|
|
14
|
+
hiliteWhite: { key: 'hiliteWhite', color: '#ffffff', opacity: 1, lineWidth: 1 },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export { createElement, setAttributes };
|
|
18
|
+
|
|
19
|
+
export function createDefs(): Element {
|
|
20
|
+
const defs = createElement('defs');
|
|
21
|
+
const filter = setAttributes(createElement('filter'), { id: 'cg-filter-blur' });
|
|
22
|
+
filter.appendChild(setAttributes(createElement('feGaussianBlur'), { stdDeviation: '0.019' }));
|
|
23
|
+
defs.appendChild(filter);
|
|
24
|
+
return defs;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function renderSvg(state: State, shapesEl: SVGElement, customsEl: SVGElement): void {
|
|
28
|
+
const d = state.drawable,
|
|
29
|
+
curD = d.current,
|
|
30
|
+
cur = curD && curD.mouseSq ? (curD as DrawShape) : undefined,
|
|
31
|
+
dests: ArrowDests = new Map(),
|
|
32
|
+
bounds = state.dom.bounds(),
|
|
33
|
+
nonPieceAutoShapes = d.autoShapes.filter(autoShape => !autoShape.piece);
|
|
34
|
+
|
|
35
|
+
for (const s of d.shapes.concat(nonPieceAutoShapes).concat(cur ? [cur] : [])) {
|
|
36
|
+
if (!s.dest) continue;
|
|
37
|
+
const sources = dests.get(s.dest) ?? new Set(),
|
|
38
|
+
from = pos2user(orient(key2pos(s.orig), state.orientation), bounds),
|
|
39
|
+
to = pos2user(orient(key2pos(s.dest), state.orientation), bounds);
|
|
40
|
+
sources.add(moveAngle(from, to));
|
|
41
|
+
dests.set(s.dest, sources);
|
|
42
|
+
}
|
|
43
|
+
const shapes: SyncableShape[] = d.shapes.concat(nonPieceAutoShapes).map((s: DrawShape) => {
|
|
44
|
+
return {
|
|
45
|
+
shape: s,
|
|
46
|
+
current: false,
|
|
47
|
+
hash: shapeHash(s, isShort(s.dest, dests), false, bounds),
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
if (cur)
|
|
51
|
+
shapes.push({
|
|
52
|
+
shape: cur,
|
|
53
|
+
current: true,
|
|
54
|
+
hash: shapeHash(cur, isShort(cur.dest, dests), true, bounds),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const fullHash = shapes.map(sc => sc.hash).join(';');
|
|
58
|
+
if (fullHash === state.drawable.prevSvgHash) return;
|
|
59
|
+
state.drawable.prevSvgHash = fullHash;
|
|
60
|
+
|
|
61
|
+
/*
|
|
62
|
+
-- DOM hierarchy --
|
|
63
|
+
<svg class="cg-shapes"> (<= svg)
|
|
64
|
+
<defs>
|
|
65
|
+
...(for brushes)...
|
|
66
|
+
</defs>
|
|
67
|
+
<g>
|
|
68
|
+
...(for arrows and circles)...
|
|
69
|
+
</g>
|
|
70
|
+
</svg>
|
|
71
|
+
<svg class="cg-custom-svgs"> (<= customSvg)
|
|
72
|
+
<g>
|
|
73
|
+
...(for custom svgs)...
|
|
74
|
+
</g>
|
|
75
|
+
</svg>
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
const defsEl = shapesEl.querySelector('defs') as SVGElement;
|
|
79
|
+
|
|
80
|
+
syncDefs(d, shapes, defsEl);
|
|
81
|
+
syncShapes(shapes, shapesEl.querySelector('g')!, customsEl.querySelector('g')!, s =>
|
|
82
|
+
renderShape(state, s, d.brushes, dests, bounds),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// append only. Don't try to update/remove.
|
|
87
|
+
function syncDefs(d: Drawable, shapes: SyncableShape[], defsEl: SVGElement) {
|
|
88
|
+
const brushes: CustomBrushes = new Map();
|
|
89
|
+
let brush: DrawBrush;
|
|
90
|
+
for (const s of shapes.filter(s => s.shape.dest && s.shape.brush)) {
|
|
91
|
+
brush = makeCustomBrush(d.brushes[s.shape.brush!], s.shape.modifiers);
|
|
92
|
+
if (s.shape.modifiers?.hilite) brushes.set(hilite(brush).key, hilite(brush));
|
|
93
|
+
brushes.set(brush.key, brush);
|
|
94
|
+
}
|
|
95
|
+
const keysInDom = new Set();
|
|
96
|
+
let el: SVGElement | undefined = defsEl.firstElementChild as SVGElement;
|
|
97
|
+
while (el) {
|
|
98
|
+
keysInDom.add(el.getAttribute('cgKey'));
|
|
99
|
+
el = el.nextElementSibling as SVGElement | undefined;
|
|
100
|
+
}
|
|
101
|
+
for (const [key, brush] of brushes.entries()) {
|
|
102
|
+
if (!keysInDom.has(key)) defsEl.appendChild(renderMarker(brush));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function syncShapes(
|
|
107
|
+
syncables: SyncableShape[],
|
|
108
|
+
shapes: Element,
|
|
109
|
+
customs: Element,
|
|
110
|
+
renderShape: (shape: SyncableShape) => Svg[],
|
|
111
|
+
): void {
|
|
112
|
+
const hashesInDom = new Map();
|
|
113
|
+
|
|
114
|
+
for (const sc of syncables) hashesInDom.set(sc.hash, false);
|
|
115
|
+
for (const root of [shapes, customs]) {
|
|
116
|
+
const toRemove: SVGElement[] = [];
|
|
117
|
+
let el: SVGElement | undefined = root.firstElementChild as SVGElement,
|
|
118
|
+
elHash: Hash | null;
|
|
119
|
+
while (el) {
|
|
120
|
+
elHash = el.getAttribute('cgHash') as Hash;
|
|
121
|
+
if (hashesInDom.has(elHash)) hashesInDom.set(elHash, true);
|
|
122
|
+
else toRemove.push(el);
|
|
123
|
+
el = el.nextElementSibling as SVGElement | undefined;
|
|
124
|
+
}
|
|
125
|
+
for (const el of toRemove) root.removeChild(el);
|
|
126
|
+
}
|
|
127
|
+
// insert shapes that are not yet in dom
|
|
128
|
+
for (const sc of syncables.filter(s => !hashesInDom.get(s.hash))) {
|
|
129
|
+
for (const svg of renderShape(sc)) {
|
|
130
|
+
if (svg.isCustom) customs.appendChild(svg.el);
|
|
131
|
+
else shapes.appendChild(svg.el);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function shapeHash(
|
|
137
|
+
{ orig, dest, brush, piece, modifiers, customSvg, label }: DrawShape,
|
|
138
|
+
shorten: boolean,
|
|
139
|
+
current: boolean,
|
|
140
|
+
bounds: DOMRectReadOnly,
|
|
141
|
+
): Hash {
|
|
142
|
+
// a shape and an overlay svg share a lifetime and have the same cgHash attribute
|
|
143
|
+
return [
|
|
144
|
+
bounds.width,
|
|
145
|
+
bounds.height,
|
|
146
|
+
current,
|
|
147
|
+
orig,
|
|
148
|
+
dest,
|
|
149
|
+
brush,
|
|
150
|
+
shorten && '-',
|
|
151
|
+
piece && pieceHash(piece),
|
|
152
|
+
modifiers && modifiersHash(modifiers),
|
|
153
|
+
customSvg && `custom-${textHash(customSvg.html)},${customSvg.center?.[0] ?? 'o'}`,
|
|
154
|
+
label && `label-${textHash(label.text)}`,
|
|
155
|
+
]
|
|
156
|
+
.filter(x => x)
|
|
157
|
+
.join(',');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function pieceHash(piece: DrawShapePiece): Hash {
|
|
161
|
+
return [piece.color, piece.role, piece.scale].filter(x => x).join(',');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function modifiersHash(m: DrawModifiers): Hash {
|
|
165
|
+
return [m.lineWidth, m.hilite && '*'].filter(x => x).join(',');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function textHash(s: string): Hash {
|
|
169
|
+
// Rolling hash with base 31 (cf. https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript)
|
|
170
|
+
let h = 0;
|
|
171
|
+
for (let i = 0; i < s.length; i++) {
|
|
172
|
+
h = ((h << 5) - h + s.charCodeAt(i)) >>> 0;
|
|
173
|
+
}
|
|
174
|
+
return h.toString();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function renderShape(
|
|
178
|
+
state: State,
|
|
179
|
+
{ shape, current, hash }: SyncableShape,
|
|
180
|
+
brushes: DrawBrushes,
|
|
181
|
+
dests: ArrowDests,
|
|
182
|
+
bounds: DOMRectReadOnly,
|
|
183
|
+
): Svg[] {
|
|
184
|
+
const from = pos2user(orient(key2pos(shape.orig), state.orientation), bounds),
|
|
185
|
+
to = shape.dest ? pos2user(orient(key2pos(shape.dest), state.orientation), bounds) : from,
|
|
186
|
+
brush = shape.brush && makeCustomBrush(brushes[shape.brush], shape.modifiers),
|
|
187
|
+
slots = dests.get(shape.dest),
|
|
188
|
+
svgs: Svg[] = [];
|
|
189
|
+
|
|
190
|
+
if (brush) {
|
|
191
|
+
const el = setAttributes(createElement('g'), { cgHash: hash });
|
|
192
|
+
svgs.push({ el });
|
|
193
|
+
|
|
194
|
+
if (from[0] !== to[0] || from[1] !== to[1])
|
|
195
|
+
el.appendChild(renderArrow(shape, brush, from, to, current, isShort(shape.dest, dests)));
|
|
196
|
+
else el.appendChild(renderCircle(brushes[shape.brush!], from, current, bounds));
|
|
197
|
+
}
|
|
198
|
+
if (shape.label) {
|
|
199
|
+
const label = shape.label;
|
|
200
|
+
label.fill ??= shape.brush && brushes[shape.brush].color;
|
|
201
|
+
const corner = shape.brush ? undefined : 'tr';
|
|
202
|
+
svgs.push({ el: renderLabel(label, hash, from, to, slots, corner), isCustom: true });
|
|
203
|
+
}
|
|
204
|
+
if (shape.customSvg) {
|
|
205
|
+
const on = shape.customSvg.center ?? 'orig';
|
|
206
|
+
const [x, y] =
|
|
207
|
+
on === 'label' ? labelCoords(from, to, slots).map(c => c - 0.5) : on === 'dest' ? to : from;
|
|
208
|
+
const el = setAttributes(createElement('g'), { transform: `translate(${x},${y})`, cgHash: hash });
|
|
209
|
+
el.innerHTML = `<svg width="1" height="1" viewBox="0 0 100 100">${shape.customSvg.html}</svg>`;
|
|
210
|
+
svgs.push({ el, isCustom: true });
|
|
211
|
+
}
|
|
212
|
+
return svgs;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function renderCircle(
|
|
216
|
+
brush: DrawBrush,
|
|
217
|
+
at: cg.NumberPair,
|
|
218
|
+
current: boolean,
|
|
219
|
+
bounds: DOMRectReadOnly,
|
|
220
|
+
): SVGElement {
|
|
221
|
+
const widths = circleWidth(),
|
|
222
|
+
radius = (bounds.width + bounds.height) / (4 * Math.max(bounds.width, bounds.height));
|
|
223
|
+
return setAttributes(createElement('circle'), {
|
|
224
|
+
stroke: brush.color,
|
|
225
|
+
'stroke-width': widths[current ? 0 : 1],
|
|
226
|
+
fill: 'none',
|
|
227
|
+
opacity: opacity(brush, current),
|
|
228
|
+
cx: at[0],
|
|
229
|
+
cy: at[1],
|
|
230
|
+
r: radius - widths[1] / 2,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function hilite(brush: DrawBrush): DrawBrush {
|
|
235
|
+
return ['#ffffff', '#fff', 'white'].includes(brush.color)
|
|
236
|
+
? hilites['hilitePrimary']
|
|
237
|
+
: hilites['hiliteWhite'];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function renderArrow(
|
|
241
|
+
s: DrawShape,
|
|
242
|
+
brush: DrawBrush,
|
|
243
|
+
from: cg.NumberPair,
|
|
244
|
+
to: cg.NumberPair,
|
|
245
|
+
current: boolean,
|
|
246
|
+
shorten: boolean,
|
|
247
|
+
): SVGElement {
|
|
248
|
+
function renderLine(isHilite: boolean) {
|
|
249
|
+
const m = arrowMargin(shorten && !current),
|
|
250
|
+
dx = to[0] - from[0],
|
|
251
|
+
dy = to[1] - from[1],
|
|
252
|
+
angle = Math.atan2(dy, dx),
|
|
253
|
+
xo = Math.cos(angle) * m,
|
|
254
|
+
yo = Math.sin(angle) * m;
|
|
255
|
+
return setAttributes(createElement('line'), {
|
|
256
|
+
stroke: isHilite ? hilite(brush).color : brush.color,
|
|
257
|
+
'stroke-width': lineWidth(brush, current) + (isHilite ? 0.04 : 0),
|
|
258
|
+
'stroke-linecap': 'round',
|
|
259
|
+
'marker-end': `url(#arrowhead-${isHilite ? hilite(brush).key : brush.key})`,
|
|
260
|
+
opacity: s.modifiers?.hilite ? 1 : opacity(brush, current),
|
|
261
|
+
x1: from[0],
|
|
262
|
+
y1: from[1],
|
|
263
|
+
x2: to[0] - xo,
|
|
264
|
+
y2: to[1] - yo,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (!s.modifiers?.hilite) return renderLine(false);
|
|
268
|
+
|
|
269
|
+
const g = createElement('g');
|
|
270
|
+
const blurred = setAttributes(createElement('g'), { filter: 'url(#cg-filter-blur)' });
|
|
271
|
+
blurred.appendChild(filterBox(from, to));
|
|
272
|
+
blurred.appendChild(renderLine(true));
|
|
273
|
+
g.appendChild(blurred);
|
|
274
|
+
g.appendChild(renderLine(false));
|
|
275
|
+
return g;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function renderMarker(brush: DrawBrush): SVGElement {
|
|
279
|
+
const marker = setAttributes(createElement('marker'), {
|
|
280
|
+
id: 'arrowhead-' + brush.key,
|
|
281
|
+
orient: 'auto',
|
|
282
|
+
overflow: 'visible',
|
|
283
|
+
markerWidth: 4,
|
|
284
|
+
markerHeight: 4,
|
|
285
|
+
refX: brush.key.startsWith('hilite') ? 1.86 : 2.05,
|
|
286
|
+
refY: 2,
|
|
287
|
+
});
|
|
288
|
+
marker.appendChild(
|
|
289
|
+
setAttributes(createElement('path'), {
|
|
290
|
+
d: 'M0,0 V4 L3,2 Z',
|
|
291
|
+
fill: brush.color,
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
marker.setAttribute('cgKey', brush.key);
|
|
295
|
+
return marker;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function renderLabel(
|
|
299
|
+
label: { text: string; fill?: string },
|
|
300
|
+
hash: string,
|
|
301
|
+
from: cg.NumberPair,
|
|
302
|
+
to: cg.NumberPair,
|
|
303
|
+
slots?: AngleSlots,
|
|
304
|
+
corner?: 'tr',
|
|
305
|
+
): SVGElement {
|
|
306
|
+
const labelSize = 0.4,
|
|
307
|
+
fontSize = labelSize * 0.75 ** label.text.length,
|
|
308
|
+
at = labelCoords(from, to, slots),
|
|
309
|
+
cornerOff = corner === 'tr' ? 0.4 : 0,
|
|
310
|
+
g = setAttributes(createElement('g'), {
|
|
311
|
+
transform: `translate(${at[0] + cornerOff},${at[1] - cornerOff})`,
|
|
312
|
+
cgHash: hash,
|
|
313
|
+
});
|
|
314
|
+
g.appendChild(
|
|
315
|
+
setAttributes(createElement('circle'), {
|
|
316
|
+
r: labelSize / 2,
|
|
317
|
+
'fill-opacity': corner ? 1.0 : 0.8,
|
|
318
|
+
'stroke-opacity': corner ? 1.0 : 0.7,
|
|
319
|
+
'stroke-width': 0.03,
|
|
320
|
+
fill: label.fill ?? '#666',
|
|
321
|
+
stroke: 'white',
|
|
322
|
+
}),
|
|
323
|
+
);
|
|
324
|
+
const labelEl = setAttributes(createElement('text'), {
|
|
325
|
+
'font-size': fontSize,
|
|
326
|
+
'font-family': 'Noto Sans',
|
|
327
|
+
'text-anchor': 'middle',
|
|
328
|
+
fill: 'white',
|
|
329
|
+
y: 0.13 * 0.75 ** label.text.length,
|
|
330
|
+
});
|
|
331
|
+
labelEl.innerHTML = label.text;
|
|
332
|
+
g.appendChild(labelEl);
|
|
333
|
+
return g;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function orient(pos: cg.Pos, color: cg.Color): cg.Pos {
|
|
337
|
+
return color === 'white' ? pos : [7 - pos[0], 7 - pos[1]];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function isShort(dest: cg.Key | undefined, dests: ArrowDests) {
|
|
341
|
+
return true === (dest && dests.has(dest) && dests.get(dest)!.size > 1);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function createElement(tagName: string): SVGElement {
|
|
345
|
+
return document.createElementNS('http://www.w3.org/2000/svg', tagName);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function setAttributes(el: SVGElement, attrs: { [key: string]: any }): SVGElement {
|
|
349
|
+
for (const key in attrs) {
|
|
350
|
+
if (Object.prototype.hasOwnProperty.call(attrs, key)) el.setAttribute(key, attrs[key]);
|
|
351
|
+
}
|
|
352
|
+
return el;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function makeCustomBrush(base: DrawBrush, modifiers: DrawModifiers | undefined): DrawBrush {
|
|
356
|
+
return !modifiers
|
|
357
|
+
? base
|
|
358
|
+
: {
|
|
359
|
+
color: base.color,
|
|
360
|
+
opacity: Math.round(base.opacity * 10) / 10,
|
|
361
|
+
lineWidth: Math.round(modifiers.lineWidth || base.lineWidth),
|
|
362
|
+
key: [base.key, modifiers.lineWidth].filter(x => x).join(''),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function circleWidth(): [number, number] {
|
|
367
|
+
return [3 / 64, 4 / 64];
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function lineWidth(brush: DrawBrush, current: boolean): number {
|
|
371
|
+
return ((brush.lineWidth || 10) * (current ? 0.85 : 1)) / 64;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function opacity(brush: DrawBrush, current: boolean): number {
|
|
375
|
+
return (brush.opacity || 1) * (current ? 0.9 : 1);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function arrowMargin(shorten: boolean): number {
|
|
379
|
+
return (shorten ? 20 : 10) / 64;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function pos2user(pos: cg.Pos, bounds: DOMRectReadOnly): cg.NumberPair {
|
|
383
|
+
const xScale = Math.min(1, bounds.width / bounds.height);
|
|
384
|
+
const yScale = Math.min(1, bounds.height / bounds.width);
|
|
385
|
+
return [(pos[0] - 3.5) * xScale, (3.5 - pos[1]) * yScale];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function filterBox(from: cg.NumberPair, to: cg.NumberPair): SVGElement {
|
|
389
|
+
// lines/arrows are considered to be one dimensional for the purposes of SVG filters,
|
|
390
|
+
// so we add a transparent bounding box to ensure they apply to the 2nd dimension
|
|
391
|
+
const box = {
|
|
392
|
+
from: [Math.floor(Math.min(from[0], to[0])), Math.floor(Math.min(from[1], to[1]))],
|
|
393
|
+
to: [Math.ceil(Math.max(from[0], to[0])), Math.ceil(Math.max(from[1], to[1]))],
|
|
394
|
+
};
|
|
395
|
+
return setAttributes(createElement('rect'), {
|
|
396
|
+
x: box.from[0],
|
|
397
|
+
y: box.from[1],
|
|
398
|
+
width: box.to[0] - box.from[0],
|
|
399
|
+
height: box.to[1] - box.from[1],
|
|
400
|
+
fill: 'none',
|
|
401
|
+
stroke: 'none',
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function moveAngle(from: cg.NumberPair, to: cg.NumberPair, asSlot = true) {
|
|
406
|
+
const angle = Math.atan2(to[1] - from[1], to[0] - from[0]) + Math.PI;
|
|
407
|
+
return asSlot ? (Math.round((angle * 8) / Math.PI) + 16) % 16 : angle;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function dist(from: cg.NumberPair, to: cg.NumberPair): number {
|
|
411
|
+
return Math.sqrt([from[0] - to[0], from[1] - to[1]].reduce((acc, x) => acc + x * x, 0));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/*
|
|
415
|
+
try to place label at the junction of the destination shaft and arrowhead. if there's more than
|
|
416
|
+
1 arrow pointing to a square, the arrow shortens by 10 / 64 units so the label must move as well.
|
|
417
|
+
|
|
418
|
+
if the angle between two incoming arrows is pi / 8, such as when an adjacent knight and bishop
|
|
419
|
+
attack the same square, the knight's label is slid further down the shaft by an amount equal to
|
|
420
|
+
our label size to avoid collision
|
|
421
|
+
*/
|
|
422
|
+
|
|
423
|
+
function labelCoords(from: cg.NumberPair, to: cg.NumberPair, slots?: AngleSlots): cg.NumberPair {
|
|
424
|
+
let mag = dist(from, to);
|
|
425
|
+
//if (mag === 0) return [from[0], from[1]];
|
|
426
|
+
const angle = moveAngle(from, to, false);
|
|
427
|
+
if (slots) {
|
|
428
|
+
mag -= 33 / 64; // reduce by arrowhead length
|
|
429
|
+
if (slots.size > 1) {
|
|
430
|
+
mag -= 10 / 64; // reduce by shortening factor
|
|
431
|
+
const slot = moveAngle(from, to);
|
|
432
|
+
if (slots.has((slot + 1) % 16) || slots.has((slot + 15) % 16)) {
|
|
433
|
+
if (slot & 1) mag -= 0.4;
|
|
434
|
+
// and by label size for the knight if another arrow is within pi / 8.
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return [from[0] - Math.cos(angle) * mag, from[1] - Math.sin(angle) * mag].map(
|
|
439
|
+
c => c + 0.5,
|
|
440
|
+
) as cg.NumberPair;
|
|
441
|
+
}
|
package/src/sync.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { DrawShape } from './draw';
|
|
2
|
+
|
|
3
|
+
export interface SyncableShape {
|
|
4
|
+
shape: DrawShape;
|
|
5
|
+
current: boolean;
|
|
6
|
+
hash: Hash;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type Hash = string;
|
|
10
|
+
|
|
11
|
+
// append and remove only. No updates.
|
|
12
|
+
export function syncShapes(
|
|
13
|
+
shapes: SyncableShape[],
|
|
14
|
+
root: HTMLElement | SVGElement,
|
|
15
|
+
renderShape: (shape: SyncableShape) => HTMLElement | SVGElement,
|
|
16
|
+
): void {
|
|
17
|
+
const hashesInDom = new Map(), // by hash
|
|
18
|
+
toRemove: SVGElement[] = [];
|
|
19
|
+
for (const sc of shapes) hashesInDom.set(sc.hash, false);
|
|
20
|
+
let el: SVGElement | undefined = root.firstElementChild as SVGElement,
|
|
21
|
+
elHash: Hash | null;
|
|
22
|
+
while (el) {
|
|
23
|
+
elHash = el.getAttribute('cgHash') as Hash;
|
|
24
|
+
// found a shape element that's here to stay
|
|
25
|
+
if (hashesInDom.has(elHash)) hashesInDom.set(elHash, true);
|
|
26
|
+
// or remove it
|
|
27
|
+
else toRemove.push(el);
|
|
28
|
+
el = el.nextElementSibling as SVGElement | undefined;
|
|
29
|
+
}
|
|
30
|
+
// remove old shapes
|
|
31
|
+
for (const el of toRemove) root.removeChild(el);
|
|
32
|
+
// insert shapes that are not yet in dom
|
|
33
|
+
for (const sc of shapes) {
|
|
34
|
+
if (!hashesInDom.get(sc.hash)) root.appendChild(renderShape(sc));
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export type Color = (typeof colors)[number];
|
|
2
|
+
export type Role = (typeof roles)[number];
|
|
3
|
+
export type File = (typeof files)[number];
|
|
4
|
+
export type Rank = (typeof ranks)[number];
|
|
5
|
+
export type Key = 'a0' | `${File}${Rank}`;
|
|
6
|
+
export type FEN = string;
|
|
7
|
+
export type Pos = [number, number];
|
|
8
|
+
export interface Piece {
|
|
9
|
+
role: Role;
|
|
10
|
+
color: Color;
|
|
11
|
+
promoted?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface Drop {
|
|
14
|
+
role: Role;
|
|
15
|
+
key: Key;
|
|
16
|
+
}
|
|
17
|
+
export type Pieces = Map<Key, Piece>;
|
|
18
|
+
export type PiecesDiff = Map<Key, Piece | undefined>;
|
|
19
|
+
|
|
20
|
+
export type KeyPair = [Key, Key];
|
|
21
|
+
|
|
22
|
+
export type NumberPair = [number, number];
|
|
23
|
+
|
|
24
|
+
export type NumberQuad = [number, number, number, number];
|
|
25
|
+
|
|
26
|
+
export interface Rect {
|
|
27
|
+
left: number;
|
|
28
|
+
top: number;
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type Dests = Map<Key, Key[]>;
|
|
34
|
+
|
|
35
|
+
export interface Elements {
|
|
36
|
+
board: HTMLElement;
|
|
37
|
+
wrap: HTMLElement;
|
|
38
|
+
container: HTMLElement;
|
|
39
|
+
ghost?: HTMLElement;
|
|
40
|
+
svg?: SVGElement;
|
|
41
|
+
customSvg?: SVGElement;
|
|
42
|
+
autoPieces?: HTMLElement;
|
|
43
|
+
}
|
|
44
|
+
export interface Dom {
|
|
45
|
+
elements: Elements;
|
|
46
|
+
bounds: Memo<DOMRectReadOnly>;
|
|
47
|
+
redraw: () => void;
|
|
48
|
+
redrawNow: (skipSvg?: boolean) => void;
|
|
49
|
+
unbind?: Unbind;
|
|
50
|
+
destroyed?: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface Exploding {
|
|
53
|
+
stage: number;
|
|
54
|
+
keys: readonly Key[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface MoveMetadata {
|
|
58
|
+
premove: boolean;
|
|
59
|
+
ctrlKey?: boolean;
|
|
60
|
+
holdTime?: number;
|
|
61
|
+
captured?: Piece;
|
|
62
|
+
predrop?: boolean;
|
|
63
|
+
}
|
|
64
|
+
export interface SetPremoveMetadata {
|
|
65
|
+
ctrlKey?: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type MouchEvent = Event & Partial<MouseEvent & TouchEvent>;
|
|
69
|
+
|
|
70
|
+
export interface KeyedNode extends HTMLElement {
|
|
71
|
+
cgKey: Key;
|
|
72
|
+
}
|
|
73
|
+
export interface PieceNode extends KeyedNode {
|
|
74
|
+
tagName: 'PIECE';
|
|
75
|
+
cgPiece: string;
|
|
76
|
+
cgAnimating?: boolean;
|
|
77
|
+
cgFading?: boolean;
|
|
78
|
+
cgDragging?: boolean;
|
|
79
|
+
cgScale?: number;
|
|
80
|
+
}
|
|
81
|
+
export interface SquareNode extends KeyedNode {
|
|
82
|
+
tagName: 'SQUARE';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface Memo<A> {
|
|
86
|
+
(): A;
|
|
87
|
+
clear: () => void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface Timer {
|
|
91
|
+
start: () => void;
|
|
92
|
+
cancel: () => void;
|
|
93
|
+
stop: () => number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type Redraw = () => void;
|
|
97
|
+
export type Unbind = () => void;
|
|
98
|
+
export type Milliseconds = number;
|
|
99
|
+
export type KHz = number;
|
|
100
|
+
|
|
101
|
+
export const colors = ['white', 'black'] as const;
|
|
102
|
+
export const roles = ['pawn', 'knight', 'bishop', 'rook', 'queen', 'king'] as const;
|
|
103
|
+
export const files = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] as const;
|
|
104
|
+
export const ranks = ['1', '2', '3', '4', '5', '6', '7', '8'] as const;
|
|
105
|
+
|
|
106
|
+
export type RanksPosition = 'left' | 'right';
|
|
107
|
+
|
|
108
|
+
export type BrushColor = 'green' | 'red' | 'blue' | 'yellow';
|
|
109
|
+
|
|
110
|
+
export type SquareClasses = Map<Key, string>;
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import * as cg from './types.js';
|
|
2
|
+
|
|
3
|
+
export const invRanks: readonly cg.Rank[] = [...cg.ranks].reverse();
|
|
4
|
+
|
|
5
|
+
export const allKeys: readonly cg.Key[] = Array.prototype.concat(
|
|
6
|
+
...cg.files.map(c => cg.ranks.map(r => c + r)),
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
export const pos2key = (pos: cg.Pos): cg.Key => allKeys[8 * pos[0] + pos[1]];
|
|
10
|
+
|
|
11
|
+
export const key2pos = (k: cg.Key): cg.Pos => [k.charCodeAt(0) - 97, k.charCodeAt(1) - 49];
|
|
12
|
+
|
|
13
|
+
export const uciToMove = (uci: string | undefined): cg.Key[] | undefined => {
|
|
14
|
+
if (!uci) return undefined;
|
|
15
|
+
if (uci[1] === '@') return [uci.slice(2, 4) as cg.Key];
|
|
16
|
+
return [uci.slice(0, 2), uci.slice(2, 4)] as cg.Key[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const allPos: readonly cg.Pos[] = allKeys.map(key2pos);
|
|
20
|
+
|
|
21
|
+
export function memo<A>(f: () => A): cg.Memo<A> {
|
|
22
|
+
let v: A | undefined;
|
|
23
|
+
const ret = (): A => {
|
|
24
|
+
if (v === undefined) v = f();
|
|
25
|
+
return v;
|
|
26
|
+
};
|
|
27
|
+
ret.clear = () => {
|
|
28
|
+
v = undefined;
|
|
29
|
+
};
|
|
30
|
+
return ret;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const timer = (): cg.Timer => {
|
|
34
|
+
let startAt: number | undefined;
|
|
35
|
+
return {
|
|
36
|
+
start() {
|
|
37
|
+
startAt = performance.now();
|
|
38
|
+
},
|
|
39
|
+
cancel() {
|
|
40
|
+
startAt = undefined;
|
|
41
|
+
},
|
|
42
|
+
stop() {
|
|
43
|
+
if (!startAt) return 0;
|
|
44
|
+
const time = performance.now() - startAt;
|
|
45
|
+
startAt = undefined;
|
|
46
|
+
return time;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const opposite = (c: cg.Color): cg.Color => (c === 'white' ? 'black' : 'white');
|
|
52
|
+
|
|
53
|
+
export const distanceSq = (pos1: cg.Pos, pos2: cg.Pos): number => {
|
|
54
|
+
const dx = pos1[0] - pos2[0],
|
|
55
|
+
dy = pos1[1] - pos2[1];
|
|
56
|
+
return dx * dx + dy * dy;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const samePiece = (p1: cg.Piece, p2: cg.Piece): boolean =>
|
|
60
|
+
p1.role === p2.role && p1.color === p2.color;
|
|
61
|
+
|
|
62
|
+
export const posToTranslate =
|
|
63
|
+
(bounds: DOMRectReadOnly): ((pos: cg.Pos, asWhite: boolean) => cg.NumberPair) =>
|
|
64
|
+
(pos, asWhite) => [
|
|
65
|
+
((asWhite ? pos[0] : 7 - pos[0]) * bounds.width) / 8,
|
|
66
|
+
((asWhite ? 7 - pos[1] : pos[1]) * bounds.height) / 8,
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
export const translate = (el: HTMLElement, pos: cg.NumberPair): void => {
|
|
70
|
+
el.style.transform = `translate(${pos[0]}px,${pos[1]}px)`;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const translateAndScale = (el: HTMLElement, pos: cg.NumberPair, scale = 1): void => {
|
|
74
|
+
el.style.transform = `translate(${pos[0]}px,${pos[1]}px) scale(${scale})`;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const setVisible = (el: HTMLElement, v: boolean): void => {
|
|
78
|
+
el.style.visibility = v ? 'visible' : 'hidden';
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const eventPosition = (e: cg.MouchEvent): cg.NumberPair | undefined => {
|
|
82
|
+
if (e.clientX || e.clientX === 0) return [e.clientX, e.clientY!];
|
|
83
|
+
if (e.targetTouches?.[0]) return [e.targetTouches[0].clientX, e.targetTouches[0].clientY];
|
|
84
|
+
return; // touchend has no position!
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const isRightButton = (e: cg.MouchEvent): boolean => e.button === 2;
|
|
88
|
+
|
|
89
|
+
export const createEl = (tagName: string, className?: string): HTMLElement => {
|
|
90
|
+
const el = document.createElement(tagName);
|
|
91
|
+
if (className) el.className = className;
|
|
92
|
+
return el;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export function computeSquareCenter(key: cg.Key, asWhite: boolean, bounds: DOMRectReadOnly): cg.NumberPair {
|
|
96
|
+
const pos = key2pos(key);
|
|
97
|
+
if (!asWhite) {
|
|
98
|
+
pos[0] = 7 - pos[0];
|
|
99
|
+
pos[1] = 7 - pos[1];
|
|
100
|
+
}
|
|
101
|
+
return [
|
|
102
|
+
bounds.left + (bounds.width * pos[0]) / 8 + bounds.width / 16,
|
|
103
|
+
bounds.top + (bounds.height * (7 - pos[1])) / 8 + bounds.height / 16,
|
|
104
|
+
];
|
|
105
|
+
}
|