@chessviewer-org/chess-viewer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/LICENSE +661 -0
- package/README.md +494 -0
- package/dist/index.cjs +1196 -0
- package/dist/index.d.cts +369 -0
- package/dist/index.d.ts +369 -0
- package/dist/index.js +1091 -0
- package/package.json +83 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
BOARD_THEMES: () => BOARD_THEMES,
|
|
24
|
+
DEFAULT_DARK_SQUARE: () => DEFAULT_DARK_SQUARE,
|
|
25
|
+
DEFAULT_LIGHT_SQUARE: () => DEFAULT_LIGHT_SQUARE,
|
|
26
|
+
EMPTY_FEN: () => EMPTY_FEN,
|
|
27
|
+
FENParseError: () => FENParseError,
|
|
28
|
+
MAX_FEN_LENGTH: () => MAX_FEN_LENGTH,
|
|
29
|
+
PIECES: () => PIECES,
|
|
30
|
+
PIECE_MAP: () => PIECE_MAP,
|
|
31
|
+
PIECE_SETS: () => PIECE_SETS,
|
|
32
|
+
PIECE_SET_POPULARITY: () => PIECE_SET_POPULARITY,
|
|
33
|
+
QUALITY_PRESETS: () => QUALITY_PRESETS,
|
|
34
|
+
STARTING_FEN: () => STARTING_FEN,
|
|
35
|
+
applyFilters: () => applyFilters,
|
|
36
|
+
bestTextColor: () => bestTextColor,
|
|
37
|
+
boardToFEN: () => boardToFEN,
|
|
38
|
+
buildFENRecord: () => buildFENRecord,
|
|
39
|
+
calculateStatus: () => calculateStatus,
|
|
40
|
+
changeDPI: () => changeDPI,
|
|
41
|
+
cloneBoard: () => cloneBoard,
|
|
42
|
+
contrastRatio: () => contrastRatio,
|
|
43
|
+
convertToArchivedEntry: () => convertToArchivedEntry,
|
|
44
|
+
countPieces: () => countPieces,
|
|
45
|
+
createEmptyBoard: () => createEmptyBoard,
|
|
46
|
+
createHistoryEntry: () => createHistoryEntry,
|
|
47
|
+
describeBoardPosition: () => describeBoardPosition,
|
|
48
|
+
fenPlacementField: () => fenPlacementField,
|
|
49
|
+
findKing: () => findKing,
|
|
50
|
+
flipBoard: () => flipBoard,
|
|
51
|
+
generateDiagram: () => generateDiagram,
|
|
52
|
+
getBoardTheme: () => getBoardTheme,
|
|
53
|
+
getCoordinateParams: () => getCoordinateParams,
|
|
54
|
+
getDisplayCoordinates: () => getDisplayCoordinates,
|
|
55
|
+
getFENValidationError: () => getFENValidationError,
|
|
56
|
+
getPieceAt: () => getPieceAt,
|
|
57
|
+
getPieceSVG: () => getPieceSVG,
|
|
58
|
+
getPieceSet: () => getPieceSet,
|
|
59
|
+
getQualityPreset: () => getQualityPreset,
|
|
60
|
+
getSquareBounds: () => getSquareBounds,
|
|
61
|
+
hasBothKings: () => hasBothKings,
|
|
62
|
+
hexToHsv: () => hexToHsv,
|
|
63
|
+
hexToRgb: () => hexToRgb,
|
|
64
|
+
hsvToHex: () => hsvToHex,
|
|
65
|
+
hsvToRgb: () => hsvToRgb,
|
|
66
|
+
indicesToSquare: () => indicesToSquare,
|
|
67
|
+
isBoardEmpty: () => isBoardEmpty,
|
|
68
|
+
isLightSquare: () => isLightSquare,
|
|
69
|
+
isRecord: () => isRecord,
|
|
70
|
+
isValidHexColor: () => isValidHexColor2,
|
|
71
|
+
listPieces: () => listPieces,
|
|
72
|
+
listThemeIds: () => listThemeIds,
|
|
73
|
+
materialBalance: () => materialBalance,
|
|
74
|
+
mergeById: () => mergeById,
|
|
75
|
+
movePiece: () => movePiece,
|
|
76
|
+
normalizeFEN: () => normalizeFEN,
|
|
77
|
+
parseFEN: () => parseFEN,
|
|
78
|
+
parseFENRecord: () => parseFENRecord,
|
|
79
|
+
partitionByArchiveStatus: () => partitionByArchiveStatus,
|
|
80
|
+
physicalSize: () => physicalSize,
|
|
81
|
+
pieceSetsByPopularity: () => pieceSetsByPopularity,
|
|
82
|
+
pieceToName: () => pieceToName,
|
|
83
|
+
readImageDimensions: () => readImageDimensions,
|
|
84
|
+
relativeLuminance: () => relativeLuminance,
|
|
85
|
+
removePieceAt: () => removePieceAt,
|
|
86
|
+
rgbToHex: () => rgbToHex,
|
|
87
|
+
rgbToHsv: () => rgbToHsv,
|
|
88
|
+
safeJSONParse: () => safeJSONParse,
|
|
89
|
+
sanitizeFileName: () => sanitizeFileName,
|
|
90
|
+
sanitizeHexColor: () => sanitizeHexColor,
|
|
91
|
+
sanitizeInput: () => sanitizeInput,
|
|
92
|
+
setPieceAt: () => setPieceAt,
|
|
93
|
+
sortArchivedByArchiveDate: () => sortArchivedByArchiveDate,
|
|
94
|
+
sortByMostRecent: () => sortByMostRecent,
|
|
95
|
+
squareToIndices: () => squareToIndices,
|
|
96
|
+
themeContrast: () => themeContrast,
|
|
97
|
+
themeCoordinateColor: () => themeCoordinateColor,
|
|
98
|
+
toggleActiveColor: () => toggleActiveColor,
|
|
99
|
+
touchEntry: () => touchEntry,
|
|
100
|
+
validateFEN: () => validateFEN,
|
|
101
|
+
validateFENDetailed: () => validateFENDetailed
|
|
102
|
+
});
|
|
103
|
+
module.exports = __toCommonJS(index_exports);
|
|
104
|
+
|
|
105
|
+
// src/fen.ts
|
|
106
|
+
var MAX_FEN_LENGTH = 93;
|
|
107
|
+
var VALID_PIECES = /* @__PURE__ */ new Set(["p", "n", "b", "r", "q", "k", "P", "N", "B", "R", "Q", "K"]);
|
|
108
|
+
var VALID_DIGITS = /* @__PURE__ */ new Set(["1", "2", "3", "4", "5", "6", "7", "8"]);
|
|
109
|
+
function isPieceSymbol(char) {
|
|
110
|
+
return VALID_PIECES.has(char);
|
|
111
|
+
}
|
|
112
|
+
var FENParseError = class extends Error {
|
|
113
|
+
constructor(message) {
|
|
114
|
+
super(message);
|
|
115
|
+
this.name = "FENParseError";
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
function parseFEN(fenString) {
|
|
119
|
+
if (!fenString || typeof fenString !== "string")
|
|
120
|
+
throw new FENParseError("Invalid FEN string");
|
|
121
|
+
if (fenString.length > MAX_FEN_LENGTH)
|
|
122
|
+
throw new FENParseError("FEN string exceeds maximum length");
|
|
123
|
+
const trimmed = fenString.trim();
|
|
124
|
+
if (trimmed.length === 0) throw new FENParseError("FEN string is empty");
|
|
125
|
+
const position = trimmed.split(/\s+/)[0] ?? "";
|
|
126
|
+
const rows = position.split("/");
|
|
127
|
+
if (rows.length !== 8)
|
|
128
|
+
throw new FENParseError(`Invalid FEN: expected 8 ranks, got ${rows.length}`);
|
|
129
|
+
const board = [];
|
|
130
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
|
|
131
|
+
const row = rows[rowIndex];
|
|
132
|
+
if (row === void 0) continue;
|
|
133
|
+
const boardRow = [];
|
|
134
|
+
let squareCount = 0;
|
|
135
|
+
for (const char of row) {
|
|
136
|
+
if (VALID_DIGITS.has(char)) {
|
|
137
|
+
const count = parseInt(char, 10);
|
|
138
|
+
squareCount += count;
|
|
139
|
+
for (let i = 0; i < count; i++) boardRow.push("");
|
|
140
|
+
} else {
|
|
141
|
+
if (!isPieceSymbol(char))
|
|
142
|
+
throw new FENParseError(`Invalid piece character '${char}' in rank ${rowIndex + 1}`);
|
|
143
|
+
squareCount++;
|
|
144
|
+
boardRow.push(char);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (squareCount !== 8)
|
|
148
|
+
throw new FENParseError(`Rank ${rowIndex + 1} has ${squareCount} squares instead of 8`);
|
|
149
|
+
board.push(boardRow);
|
|
150
|
+
}
|
|
151
|
+
if (board.length !== 8)
|
|
152
|
+
throw new FENParseError(`Invalid board structure: ${board.length} ranks`);
|
|
153
|
+
return board;
|
|
154
|
+
}
|
|
155
|
+
function validateFEN(fen) {
|
|
156
|
+
return getFENValidationError(fen) === "";
|
|
157
|
+
}
|
|
158
|
+
function getFENValidationError(fen) {
|
|
159
|
+
try {
|
|
160
|
+
if (!fen || typeof fen !== "string") return "FEN is empty";
|
|
161
|
+
if (fen.length > MAX_FEN_LENGTH) return "FEN string is too long";
|
|
162
|
+
const position = fen.trim().split(/\s+/)[0] ?? "";
|
|
163
|
+
const rows = position.split("/");
|
|
164
|
+
if (rows.length !== 8) return "Board must have 8 ranks";
|
|
165
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
|
|
166
|
+
const row = rows[rowIndex];
|
|
167
|
+
if (row === void 0) continue;
|
|
168
|
+
let count = 0;
|
|
169
|
+
for (const char of row) {
|
|
170
|
+
if (VALID_DIGITS.has(char)) {
|
|
171
|
+
count += parseInt(char, 10);
|
|
172
|
+
} else if (VALID_PIECES.has(char)) {
|
|
173
|
+
count++;
|
|
174
|
+
} else {
|
|
175
|
+
return `Invalid piece character: ${char}`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (count !== 8) return `Rank ${rowIndex + 1} has ${count} squares`;
|
|
179
|
+
}
|
|
180
|
+
return "";
|
|
181
|
+
} catch {
|
|
182
|
+
return "Invalid FEN";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function validateFENDetailed(fen) {
|
|
186
|
+
if (!fen || typeof fen !== "string") {
|
|
187
|
+
return { isValid: false, errorMessage: "Error: FEN string is empty or has an invalid format." };
|
|
188
|
+
}
|
|
189
|
+
if (fen.length > MAX_FEN_LENGTH) {
|
|
190
|
+
return { isValid: false, errorMessage: "Error: FEN string is too long." };
|
|
191
|
+
}
|
|
192
|
+
const trimmed = fen.trim();
|
|
193
|
+
const parts = trimmed.split(/\s+/);
|
|
194
|
+
if (parts.length !== 6) {
|
|
195
|
+
return {
|
|
196
|
+
isValid: false,
|
|
197
|
+
errorMessage: `Error: A valid FEN must have exactly 6 parts. You provided ${parts.length}.`
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const [position, activeColor, castling, enPassant, halfmove, fullmove] = parts;
|
|
201
|
+
if (!position || !activeColor || !castling || !enPassant || !halfmove || !fullmove) {
|
|
202
|
+
return { isValid: false, errorMessage: "Error: Missing FEN parts." };
|
|
203
|
+
}
|
|
204
|
+
const rows = position.split("/");
|
|
205
|
+
if (rows.length !== 8) {
|
|
206
|
+
return {
|
|
207
|
+
isValid: false,
|
|
208
|
+
errorMessage: `Error: The board must have 8 ranks, but yours has ${rows.length}.`
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
|
|
212
|
+
const row = rows[rowIndex];
|
|
213
|
+
if (row === void 0) continue;
|
|
214
|
+
let squareCount = 0;
|
|
215
|
+
for (const char of row) {
|
|
216
|
+
if (VALID_DIGITS.has(char)) {
|
|
217
|
+
squareCount += parseInt(char, 10);
|
|
218
|
+
} else if (VALID_PIECES.has(char)) {
|
|
219
|
+
squareCount++;
|
|
220
|
+
} else {
|
|
221
|
+
return {
|
|
222
|
+
isValid: false,
|
|
223
|
+
errorMessage: `Error: Invalid character '${char}' in the piece placement field.`
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (squareCount !== 8) {
|
|
228
|
+
return {
|
|
229
|
+
isValid: false,
|
|
230
|
+
errorMessage: `Error: Rank ${rowIndex + 1} has ${squareCount} squares instead of 8.`
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (activeColor !== "w" && activeColor !== "b") {
|
|
235
|
+
return { isValid: false, errorMessage: "Error: Active color must be 'w' (white) or 'b' (black)." };
|
|
236
|
+
}
|
|
237
|
+
if (castling !== "-") {
|
|
238
|
+
if (!/^[KQkq]{1,4}$/.test(castling)) {
|
|
239
|
+
return { isValid: false, errorMessage: "Error: Castling field is invalid." };
|
|
240
|
+
}
|
|
241
|
+
const unique = new Set(castling);
|
|
242
|
+
if (unique.size !== castling.length) {
|
|
243
|
+
return { isValid: false, errorMessage: "Error: Castling field contains duplicate characters." };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (enPassant !== "-" && !/^[a-h][36]$/.test(enPassant)) {
|
|
247
|
+
return {
|
|
248
|
+
isValid: false,
|
|
249
|
+
errorMessage: "Error: En passant square is invalid (must be a file a-h on rank 3 or 6)."
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (!/^\d+$/.test(halfmove) || !/^\d+$/.test(fullmove)) {
|
|
253
|
+
return {
|
|
254
|
+
isValid: false,
|
|
255
|
+
errorMessage: "Error: Halfmove clock and fullmove number must be non-negative integers."
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (parseInt(fullmove, 10) < 1) {
|
|
259
|
+
return { isValid: false, errorMessage: "Error: Fullmove number must be at least 1." };
|
|
260
|
+
}
|
|
261
|
+
return { isValid: true, errorMessage: null };
|
|
262
|
+
}
|
|
263
|
+
function createEmptyBoard() {
|
|
264
|
+
return Array(8).fill(null).map(() => Array(8).fill(""));
|
|
265
|
+
}
|
|
266
|
+
function boardToFEN(board) {
|
|
267
|
+
const rows = [];
|
|
268
|
+
for (let r = 0; r < 8; r++) {
|
|
269
|
+
const row = board[r];
|
|
270
|
+
if (!row) continue;
|
|
271
|
+
let fenRow = "";
|
|
272
|
+
let emptyCount = 0;
|
|
273
|
+
for (let c = 0; c < 8; c++) {
|
|
274
|
+
const piece = row[c];
|
|
275
|
+
if (piece === "") {
|
|
276
|
+
emptyCount++;
|
|
277
|
+
} else {
|
|
278
|
+
if (emptyCount > 0) {
|
|
279
|
+
fenRow += emptyCount.toString();
|
|
280
|
+
emptyCount = 0;
|
|
281
|
+
}
|
|
282
|
+
fenRow += piece;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (emptyCount > 0) fenRow += emptyCount.toString();
|
|
286
|
+
rows.push(fenRow);
|
|
287
|
+
}
|
|
288
|
+
return rows.join("/");
|
|
289
|
+
}
|
|
290
|
+
function isBoardEmpty(board) {
|
|
291
|
+
return board.every((row) => row.every((piece) => piece === ""));
|
|
292
|
+
}
|
|
293
|
+
var PIECE_NAMES = {
|
|
294
|
+
K: "white king",
|
|
295
|
+
Q: "white queen",
|
|
296
|
+
R: "white rook",
|
|
297
|
+
B: "white bishop",
|
|
298
|
+
N: "white knight",
|
|
299
|
+
P: "white pawn",
|
|
300
|
+
k: "black king",
|
|
301
|
+
q: "black queen",
|
|
302
|
+
r: "black rook",
|
|
303
|
+
b: "black bishop",
|
|
304
|
+
n: "black knight",
|
|
305
|
+
p: "black pawn"
|
|
306
|
+
};
|
|
307
|
+
function pieceToName(piece) {
|
|
308
|
+
return PIECE_NAMES[piece] ?? piece;
|
|
309
|
+
}
|
|
310
|
+
function describeBoardPosition(board, flipped = false) {
|
|
311
|
+
const white = [];
|
|
312
|
+
const black = [];
|
|
313
|
+
const files = "abcdefgh";
|
|
314
|
+
for (let r = 0; r < 8; r++) {
|
|
315
|
+
for (let c = 0; c < 8; c++) {
|
|
316
|
+
const piece = board[r]?.[c];
|
|
317
|
+
if (!piece) continue;
|
|
318
|
+
const displayRow = flipped ? r : 7 - r;
|
|
319
|
+
const displayCol = flipped ? 7 - c : c;
|
|
320
|
+
const square = `${files[displayCol] ?? c}${displayRow + 1}`;
|
|
321
|
+
const name = pieceToName(piece);
|
|
322
|
+
if (piece === piece.toUpperCase()) white.push(`${name} ${square}`);
|
|
323
|
+
else black.push(`${name} ${square}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (white.length === 0 && black.length === 0) return "Empty board";
|
|
327
|
+
const parts = [];
|
|
328
|
+
if (white.length > 0) parts.push(`White: ${white.join(", ")}`);
|
|
329
|
+
if (black.length > 0) parts.push(`Black: ${black.join(", ")}`);
|
|
330
|
+
return parts.join(". ");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/fen-record.ts
|
|
334
|
+
var CASTLING_RE = /^(-|[KQkq]{1,4})$/;
|
|
335
|
+
var EN_PASSANT_RE = /^(-|[a-h][36])$/;
|
|
336
|
+
function parseFENRecord(fen) {
|
|
337
|
+
if (!fen || typeof fen !== "string") throw new FENParseError("Invalid FEN string");
|
|
338
|
+
const parts = fen.trim().split(/\s+/);
|
|
339
|
+
const [
|
|
340
|
+
placement,
|
|
341
|
+
activeColor = "w",
|
|
342
|
+
castling = "-",
|
|
343
|
+
enPassant = "-",
|
|
344
|
+
halfmoveRaw = "0",
|
|
345
|
+
fullmoveRaw = "1"
|
|
346
|
+
] = parts;
|
|
347
|
+
const board = parseFEN(placement ?? "");
|
|
348
|
+
if (activeColor !== "w" && activeColor !== "b")
|
|
349
|
+
throw new FENParseError(`Invalid active color '${activeColor}'`);
|
|
350
|
+
if (!CASTLING_RE.test(castling))
|
|
351
|
+
throw new FENParseError(`Invalid castling field '${castling}'`);
|
|
352
|
+
if (castling !== "-" && new Set(castling).size !== castling.length)
|
|
353
|
+
throw new FENParseError("Castling field contains duplicate characters");
|
|
354
|
+
if (!EN_PASSANT_RE.test(enPassant))
|
|
355
|
+
throw new FENParseError(`Invalid en passant square '${enPassant}'`);
|
|
356
|
+
if (!/^\d+$/.test(halfmoveRaw))
|
|
357
|
+
throw new FENParseError(`Invalid halfmove clock '${halfmoveRaw}'`);
|
|
358
|
+
if (!/^\d+$/.test(fullmoveRaw))
|
|
359
|
+
throw new FENParseError(`Invalid fullmove number '${fullmoveRaw}'`);
|
|
360
|
+
const halfmove = parseInt(halfmoveRaw, 10);
|
|
361
|
+
const fullmove = parseInt(fullmoveRaw, 10);
|
|
362
|
+
if (fullmove < 1) throw new FENParseError("Fullmove number must be at least 1");
|
|
363
|
+
return { board, activeColor, castling, enPassant, halfmove, fullmove };
|
|
364
|
+
}
|
|
365
|
+
function buildFENRecord(record) {
|
|
366
|
+
const {
|
|
367
|
+
board,
|
|
368
|
+
activeColor = "w",
|
|
369
|
+
castling = "-",
|
|
370
|
+
enPassant = "-",
|
|
371
|
+
halfmove = 0,
|
|
372
|
+
fullmove = 1
|
|
373
|
+
} = record;
|
|
374
|
+
return `${boardToFEN(board)} ${activeColor} ${castling} ${enPassant} ${halfmove} ${fullmove}`;
|
|
375
|
+
}
|
|
376
|
+
function toggleActiveColor(record) {
|
|
377
|
+
return { ...record, activeColor: record.activeColor === "w" ? "b" : "w" };
|
|
378
|
+
}
|
|
379
|
+
function fenPlacementField(fen) {
|
|
380
|
+
return (fen ?? "").trim().split(/\s+/)[0] ?? "";
|
|
381
|
+
}
|
|
382
|
+
function normalizeFEN(fen) {
|
|
383
|
+
return buildFENRecord(parseFENRecord(fen));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/coordinates.ts
|
|
387
|
+
function getCoordinateParams(boardSize) {
|
|
388
|
+
const borderSize = Math.round(Math.max(18, Math.min(800, boardSize * 0.05)));
|
|
389
|
+
const fontSize = Math.round(Math.max(10, Math.min(480, borderSize * 0.72)));
|
|
390
|
+
return { fontSize, borderSize, fontWeight: 600, offset: Math.round(borderSize / 2) };
|
|
391
|
+
}
|
|
392
|
+
function getSquareBounds(rowIndex, colIndex, squareSize, offsetX = 0, offsetY = 0) {
|
|
393
|
+
const x0 = Math.round(offsetX + colIndex * squareSize);
|
|
394
|
+
const x1 = Math.round(offsetX + (colIndex + 1) * squareSize);
|
|
395
|
+
const y0 = Math.round(offsetY + rowIndex * squareSize);
|
|
396
|
+
const y1 = Math.round(offsetY + (rowIndex + 1) * squareSize);
|
|
397
|
+
return { x: x0, y: y0, width: x1 - x0, height: y1 - y0, centerX: Math.round((x0 + x1) / 2), centerY: Math.round((y0 + y1) / 2) };
|
|
398
|
+
}
|
|
399
|
+
function isLightSquare(row, col) {
|
|
400
|
+
return (row + col) % 2 === 0;
|
|
401
|
+
}
|
|
402
|
+
function getDisplayCoordinates(row, col, flipped) {
|
|
403
|
+
return [flipped ? 7 - row : row, flipped ? 7 - col : col];
|
|
404
|
+
}
|
|
405
|
+
function squareToIndices(square) {
|
|
406
|
+
if (square.length !== 2) return null;
|
|
407
|
+
const file = square.charCodeAt(0) - 97;
|
|
408
|
+
const rank = parseInt(square[1] ?? "", 10);
|
|
409
|
+
if (file < 0 || file > 7 || isNaN(rank) || rank < 1 || rank > 8) return null;
|
|
410
|
+
return [8 - rank, file];
|
|
411
|
+
}
|
|
412
|
+
function indicesToSquare(row, col) {
|
|
413
|
+
const file = String.fromCharCode(97 + col);
|
|
414
|
+
const rank = 8 - row;
|
|
415
|
+
return `${file}${rank}`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/board.ts
|
|
419
|
+
function resolve(square) {
|
|
420
|
+
if (typeof square === "string") return squareToIndices(square);
|
|
421
|
+
const [row, col] = square;
|
|
422
|
+
if (row < 0 || row > 7 || col < 0 || col > 7) return null;
|
|
423
|
+
return [row, col];
|
|
424
|
+
}
|
|
425
|
+
function cloneBoard(board) {
|
|
426
|
+
return board.map((row) => [...row]);
|
|
427
|
+
}
|
|
428
|
+
function getPieceAt(board, square) {
|
|
429
|
+
const idx = resolve(square);
|
|
430
|
+
if (!idx) return null;
|
|
431
|
+
return board[idx[0]]?.[idx[1]] ?? null;
|
|
432
|
+
}
|
|
433
|
+
function setPieceAt(board, square, piece) {
|
|
434
|
+
const idx = resolve(square);
|
|
435
|
+
if (!idx) return board;
|
|
436
|
+
const next = cloneBoard(board);
|
|
437
|
+
const row = next[idx[0]];
|
|
438
|
+
if (row) row[idx[1]] = piece;
|
|
439
|
+
return next;
|
|
440
|
+
}
|
|
441
|
+
function removePieceAt(board, square) {
|
|
442
|
+
return setPieceAt(board, square, "");
|
|
443
|
+
}
|
|
444
|
+
function movePiece(board, from, to) {
|
|
445
|
+
const piece = getPieceAt(board, from);
|
|
446
|
+
if (!piece) return board;
|
|
447
|
+
const cleared = removePieceAt(board, from);
|
|
448
|
+
return setPieceAt(cleared, to, piece);
|
|
449
|
+
}
|
|
450
|
+
function flipBoard(board) {
|
|
451
|
+
return board.map((row) => [...row].reverse()).reverse();
|
|
452
|
+
}
|
|
453
|
+
function listPieces(board) {
|
|
454
|
+
const out = [];
|
|
455
|
+
for (let row = 0; row < 8; row++) {
|
|
456
|
+
for (let col = 0; col < 8; col++) {
|
|
457
|
+
const piece = board[row]?.[col];
|
|
458
|
+
if (piece) out.push({ square: indicesToSquare(row, col), piece });
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return out;
|
|
462
|
+
}
|
|
463
|
+
function countPieces(board) {
|
|
464
|
+
const counts = {};
|
|
465
|
+
for (const row of board) {
|
|
466
|
+
for (const piece of row) {
|
|
467
|
+
if (piece) counts[piece] = (counts[piece] ?? 0) + 1;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return counts;
|
|
471
|
+
}
|
|
472
|
+
var MATERIAL_VALUES = {
|
|
473
|
+
p: 1,
|
|
474
|
+
n: 3,
|
|
475
|
+
b: 3,
|
|
476
|
+
r: 5,
|
|
477
|
+
q: 9,
|
|
478
|
+
k: 0
|
|
479
|
+
};
|
|
480
|
+
function materialBalance(board) {
|
|
481
|
+
let balance = 0;
|
|
482
|
+
for (const row of board) {
|
|
483
|
+
for (const piece of row) {
|
|
484
|
+
if (!piece) continue;
|
|
485
|
+
const value = MATERIAL_VALUES[piece.toLowerCase()] ?? 0;
|
|
486
|
+
balance += piece === piece.toUpperCase() ? value : -value;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return balance;
|
|
490
|
+
}
|
|
491
|
+
function findKing(board, color) {
|
|
492
|
+
const target = color === "w" ? "K" : "k";
|
|
493
|
+
for (let row = 0; row < 8; row++) {
|
|
494
|
+
for (let col = 0; col < 8; col++) {
|
|
495
|
+
if (board[row]?.[col] === target) return indicesToSquare(row, col);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
function hasBothKings(board) {
|
|
501
|
+
const counts = countPieces(board);
|
|
502
|
+
return counts["K"] === 1 && counts["k"] === 1;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/pieces.ts
|
|
506
|
+
var PIECES = {
|
|
507
|
+
wK: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45"><g fill="none" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"><path stroke-linejoin="miter" d="M22.5 11.63V6M20 8h5"/><path fill="#fff" stroke-linecap="butt" stroke-linejoin="miter" d="M22.5 25s4.5-7.5 3-10.5c0 0-1-2.5-3-2.5s-3 2.5-3 2.5c-1.5 3 3 10.5 3 10.5"/><path fill="#fff" d="M11.5 37c5.5 3.5 15.5 3.5 21 0v-7s9-4.5 6-10.5c-4-6.5-13.5-3.5-16 4V27v-3.5c-3.5-7.5-13-10.5-16-4-3 6 5 10 5 10z"/><path d="M11.5 30c5.5-3 15.5-3 21 0m-21 3.5c5.5-3 15.5-3 21 0m-21 3.5c5.5-3 15.5-3 21 0"/></g></svg>`,
|
|
508
|
+
wQ: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45"><g fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"><path d="M8 12a2 2 0 1 1-4 0 2 2 0 1 1 4 0m16.5-4.5a2 2 0 1 1-4 0 2 2 0 1 1 4 0M41 12a2 2 0 1 1-4 0 2 2 0 1 1 4 0M16 8.5a2 2 0 1 1-4 0 2 2 0 1 1 4 0M33 9a2 2 0 1 1-4 0 2 2 0 1 1 4 0"/><path stroke-linecap="butt" d="M9 26c8.5-1.5 21-1.5 27 0l2-12-7 11V11l-5.5 13.5-3-15-3 15-5.5-14V25L7 14z"/><path stroke-linecap="butt" d="M9 26c0 2 1.5 2 2.5 4 1 1.5 1 1 .5 3.5-1.5 1-1.5 2.5-1.5 2.5-1.5 1.5.5 2.5.5 2.5 6.5 1 16.5 1 23 0 0 0 1.5-1 0-2.5 0 0 .5-1.5-1-2.5-.5-2.5-.5-2 .5-3.5 1-2 2.5-2 2.5-4-8.5-1.5-18.5-1.5-27 0z"/><path fill="none" d="M11.5 30c3.5-1 18.5-1 22 0M12 33.5c6-1 15-1 21 0"/></g></svg>`,
|
|
509
|
+
wR: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45"><g fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"><path stroke-linecap="butt" d="M9 39h27v-3H9zm3-3v-4h21v4zm-1-22V9h4v2h5V9h5v2h5V9h4v5"/><path d="m34 14-3 3H14l-3-3"/><path stroke-linecap="butt" stroke-linejoin="miter" d="M31 17v12.5H14V17"/><path d="m31 29.5 1.5 2.5h-20l1.5-2.5"/><path fill="none" stroke-linejoin="miter" d="M11 14h23"/></g></svg>`,
|
|
510
|
+
wB: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45"><g fill="none" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"><g fill="#fff" stroke-linecap="butt"><path d="M9 36c3.39-.97 10.11.43 13.5-2 3.39 2.43 10.11 1.03 13.5 2 0 0 1.65.54 3 2-.68.97-1.65.99-3 .5-3.39-.97-10.11.46-13.5-1-3.39 1.46-10.11.03-13.5 1-1.35.49-2.32.47-3-.5 1.35-1.94 3-2 3-2z"/><path d="M15 32c2.5 2.5 12.5 2.5 15 0 .5-1.5 0-2 0-2 0-2.5-2.5-4-2.5-4 5.5-1.5 6-11.5-5-15.5-11 4-10.5 14-5 15.5 0 0-2.5 1.5-2.5 4 0 0-.5.5 0 2z"/><path d="M25 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 1 1 5 0z"/></g><path stroke-linejoin="miter" d="M17.5 26h10M15 30h15m-7.5-14.5v5M20 18h5"/></g></svg>`,
|
|
511
|
+
wN: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45"><g fill="none" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"><path fill="#fff" d="M22 10c10.5 1 16.5 8 16 29H15c0-9 10-6.5 8-21"/><path fill="#fff" d="M24 18c.38 2.91-5.55 7.37-8 9-3 2-2.82 4.34-5 4-1.042-.94 1.41-3.04 0-3-1 0 .19 1.23-1 2-1 0-4.003 1-4-4 0-2 6-12 6-12s1.89-1.9 2-3.5c-.73-.994-.5-2-.5-3 1-1 3 2.5 3 2.5h2s.78-1.992 2.5-3c1 0 1 3 1 3"/><path fill="#000" d="M9.5 25.5a.5.5 0 1 1-1 0 .5.5 0 1 1 1 0m5.433-9.75a.5 1.5 30 1 1-.866-.5.5 1.5 30 1 1 .866.5"/></g></svg>`,
|
|
512
|
+
wP: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45"><path fill="#fff" stroke="#000" stroke-linecap="round" stroke-width="1.5" d="M22.5 9c-2.21 0-4 1.79-4 4 0 .89.29 1.71.78 2.38C17.33 16.5 16 18.59 16 21c0 2.03.94 3.84 2.41 5.03-3 1.06-7.41 5.55-7.41 13.47h23c0-7.92-4.41-12.41-7.41-13.47 1.47-1.19 2.41-3 2.41-5.03 0-2.41-1.33-4.5-3.28-5.62.49-.67.78-1.49.78-2.38 0-2.21-1.79-4-4-4z"/></svg>`,
|
|
513
|
+
bK: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45"><g fill="none" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"><path stroke-linejoin="miter" d="M22.5 11.6V6"/><path fill="#000" stroke-linecap="butt" stroke-linejoin="miter" d="M22.5 25s4.5-7.5 3-10.5c0 0-1-2.5-3-2.5s-3 2.5-3 2.5c-1.5 3 3 10.5 3 10.5"/><path fill="#000" d="M11.5 37a22.3 22.3 0 0 0 21 0v-7s9-4.5 6-10.5c-4-6.5-13.5-3.5-16 4V27v-3.5c-3.5-7.5-13-10.5-16-4-3 6 5 10 5 10z"/><path stroke-linejoin="miter" d="M20 8h5"/><path stroke="#ececec" d="M32 29.5s8.5-4 6-9.7C34.1 14 25 18 22.5 24.6v2.1-2.1C20 18 9.9 14 7 19.9c-2.5 5.6 4.8 9 4.8 9"/><path stroke="#ececec" d="M11.5 30c5.5-3 15.5-3 21 0m-21 3.5c5.5-3 15.5-3 21 0m-21 3.5c5.5-3 15.5-3 21 0"/></g></svg>`,
|
|
514
|
+
bQ: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45"><g fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"><g stroke="none"><circle cx="6" cy="12" r="2.75"/><circle cx="14" cy="9" r="2.75"/><circle cx="22.5" cy="8" r="2.75"/><circle cx="31" cy="9" r="2.75"/><circle cx="39" cy="12" r="2.75"/></g><path stroke-linecap="butt" d="M9 26c8.5-1.5 21-1.5 27 0l2.5-12.5L31 25l-.3-14.1-5.2 13.6-3-14.5-3 14.5-5.2-13.6L14 25 6.5 13.5z"/><path stroke-linecap="butt" d="M9 26c0 2 1.5 2 2.5 4 1 1.5 1 1 .5 3.5-1.5 1-1.5 2.5-1.5 2.5-1.5 1.5.5 2.5.5 2.5 6.5 1 16.5 1 23 0 0 0 1.5-1 0-2.5 0 0 .5-1.5-1-2.5-.5-2.5-.5-2 .5-3.5 1-2 2.5-2 2.5-4-8.5-1.5-18.5-1.5-27 0z"/><path fill="none" stroke-linecap="butt" d="M11 38.5a35 35 1 0 0 23 0"/><path fill="none" stroke="#ececec" d="M11 29a35 35 1 0 1 23 0m-21.5 2.5h20m-21 3a35 35 1 0 0 22 0m-23 3a35 35 1 0 0 24 0"/></g></svg>`,
|
|
515
|
+
bR: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45"><g fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"><path stroke-linecap="butt" d="M9 39h27v-3H9zm3.5-7 1.5-2.5h17l1.5 2.5zm-.5 4v-4h21v4z"/><path stroke-linecap="butt" stroke-linejoin="miter" d="M14 29.5v-13h17v13z"/><path stroke-linecap="butt" d="M14 16.5 11 14h23l-3 2.5zM11 14V9h4v2h5V9h5v2h5V9h4v5z"/><path fill="none" stroke="#ececec" stroke-linejoin="miter" stroke-width="1" d="M12 35.5h21m-20-4h19m-18-2h17m-17-13h17M11 14h23"/></g></svg>`,
|
|
516
|
+
bB: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45"><g fill="none" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"><g fill="#000" stroke-linecap="butt"><path d="M9 36c3.4-1 10.1.4 13.5-2 3.4 2.4 10.1 1 13.5 2 0 0 1.6.5 3 2-.7 1-1.6 1-3 .5-3.4-1-10.1.5-13.5-1-3.4 1.5-10.1 0-13.5 1-1.4.5-2.3.5-3-.5 1.4-2 3-2 3-2z"/><path d="M15 32c2.5 2.5 12.5 2.5 15 0 .5-1.5 0-2 0-2 0-2.5-2.5-4-2.5-4 5.5-1.5 6-11.5-5-15.5-11 4-10.5 14-5 15.5 0 0-2.5 1.5-2.5 4 0 0-.5.5 0 2z"/><path d="M25 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 1 1 5 0z"/></g><path stroke="#ececec" stroke-linejoin="miter" d="M17.5 26h10M15 30h15m-7.5-14.5v5M20 18h5"/></g></svg>`,
|
|
517
|
+
bN: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45"><g fill="none" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"><path fill="#000" d="M22 10c10.5 1 16.5 8 16 29H15c0-9 10-6.5 8-21"/><path fill="#000" d="M24 18c.38 2.91-5.55 7.37-8 9-3 2-2.82 4.34-5 4-1.04-.94 1.41-3.04 0-3-1 0 .19 1.23-1 2-1 0-4 1-4-4 0-2 6-12 6-12s1.89-1.9 2-3.5c-.73-1-.5-2-.5-3 1-1 3 2.5 3 2.5h2s.78-2 2.5-3c1 0 1 3 1 3"/><path fill="#ececec" stroke="#ececec" d="M9.5 25.5a.5.5 0 1 1-1 0 .5.5 0 1 1 1 0m5.43-9.75a.5 1.5 30 1 1-.86-.5.5 1.5 30 1 1 .86.5"/><path fill="#ececec" stroke="none" d="m24.55 10.4-.45 1.45.5.15c3.15 1 5.65 2.49 7.9 6.75S35.75 29.06 35.25 39l-.05.5h2.25l.05-.5c.5-10.06-.88-16.85-3.25-21.34s-5.79-6.64-9.19-7.16z"/></g></svg>`,
|
|
518
|
+
bP: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45"><path stroke="#000" stroke-linecap="round" stroke-width="1.5" d="M22.5 9a4 4 0 0 0-3.22 6.38 6.48 6.48 0 0 0-.87 10.65c-3 1.06-7.41 5.55-7.41 13.47h23c0-7.92-4.41-12.41-7.41-13.47a6.46 6.46 0 0 0-.87-10.65A4.01 4.01 0 0 0 22.5 9z"/></svg>`
|
|
519
|
+
};
|
|
520
|
+
function getPieceSVG(fenChar) {
|
|
521
|
+
const isWhite = fenChar === fenChar.toUpperCase();
|
|
522
|
+
const type = fenChar.toUpperCase();
|
|
523
|
+
const key = (isWhite ? "w" : "b") + type;
|
|
524
|
+
return PIECES[key] ?? null;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// src/svg.ts
|
|
528
|
+
function escapeXml(value) {
|
|
529
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
530
|
+
}
|
|
531
|
+
function isValidHexColor(color) {
|
|
532
|
+
return /^#[0-9A-Fa-f]{6}$/.test(color);
|
|
533
|
+
}
|
|
534
|
+
function safeColor(color, fallback) {
|
|
535
|
+
if (color && isValidHexColor(color)) return color;
|
|
536
|
+
return fallback;
|
|
537
|
+
}
|
|
538
|
+
function resolveCoordColor(color, fallback) {
|
|
539
|
+
if (color === "white") return "#ffffff";
|
|
540
|
+
if (color === "black") return "#000000";
|
|
541
|
+
return safeColor(color, fallback);
|
|
542
|
+
}
|
|
543
|
+
function generateDiagram(options) {
|
|
544
|
+
const {
|
|
545
|
+
fen,
|
|
546
|
+
size = 400,
|
|
547
|
+
showCoords = false,
|
|
548
|
+
flipped = false,
|
|
549
|
+
showFrame = false,
|
|
550
|
+
label = "Chess position"
|
|
551
|
+
} = options;
|
|
552
|
+
const lightSquare = safeColor(options.lightSquare, "#f0d9b5");
|
|
553
|
+
const darkSquare = safeColor(options.darkSquare, "#b58863");
|
|
554
|
+
const coordColor = resolveCoordColor(options.coordColor, "#000000");
|
|
555
|
+
const board = parseFEN(fen);
|
|
556
|
+
const COORD_RATIO = 0.05;
|
|
557
|
+
const coordBorder = showCoords ? Math.round(Math.max(18, size * COORD_RATIO)) : 0;
|
|
558
|
+
const frameThickness = showFrame ? Math.max(2, Math.round(size * 3e-3)) : 0;
|
|
559
|
+
const framePadding = showFrame ? frameThickness * 2 : 0;
|
|
560
|
+
const totalWidth = coordBorder + size + framePadding;
|
|
561
|
+
const totalHeight = size + coordBorder + framePadding;
|
|
562
|
+
const boardX = coordBorder + (showFrame ? frameThickness : 0);
|
|
563
|
+
const boardY = showFrame ? frameThickness : 0;
|
|
564
|
+
const squareSize = size / 8;
|
|
565
|
+
const parts = [];
|
|
566
|
+
parts.push(
|
|
567
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${totalWidth} ${totalHeight}" width="${totalWidth}" height="${totalHeight}" role="img" aria-label="${escapeXml(label)}">`
|
|
568
|
+
);
|
|
569
|
+
parts.push(`<title>${escapeXml(label)}</title>`);
|
|
570
|
+
if (showFrame) {
|
|
571
|
+
const f = frameThickness;
|
|
572
|
+
parts.push(
|
|
573
|
+
`<rect x="0" y="0" width="${totalWidth}" height="${f}" fill="#333333"/>`,
|
|
574
|
+
`<rect x="0" y="${totalHeight - f}" width="${totalWidth}" height="${f}" fill="#333333"/>`,
|
|
575
|
+
`<rect x="0" y="0" width="${f}" height="${totalHeight}" fill="#333333"/>`,
|
|
576
|
+
`<rect x="${totalWidth - f}" y="0" width="${f}" height="${totalHeight}" fill="#333333"/>`
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
const strokeW = Math.max(1, Math.round(size * 2e-3));
|
|
580
|
+
const half = strokeW / 2;
|
|
581
|
+
parts.push(
|
|
582
|
+
`<rect x="${boardX - half}" y="${boardY - half}" width="${size + strokeW}" height="${size + strokeW}" fill="none" stroke="#000000" stroke-width="${strokeW}"/>`
|
|
583
|
+
);
|
|
584
|
+
for (let row = 0; row < 8; row++) {
|
|
585
|
+
for (let col = 0; col < 8; col++) {
|
|
586
|
+
const visRow = flipped ? 7 - row : row;
|
|
587
|
+
const visCol = flipped ? 7 - col : col;
|
|
588
|
+
const color = (row + col) % 2 === 0 ? lightSquare : darkSquare;
|
|
589
|
+
const x = boardX + visCol * squareSize;
|
|
590
|
+
const y = boardY + visRow * squareSize;
|
|
591
|
+
parts.push(
|
|
592
|
+
`<rect x="${x}" y="${y}" width="${squareSize}" height="${squareSize}" fill="${escapeXml(color)}"/>`
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
for (let row = 0; row < 8; row++) {
|
|
597
|
+
for (let col = 0; col < 8; col++) {
|
|
598
|
+
const fenPiece = board[row]?.[col];
|
|
599
|
+
if (!fenPiece) continue;
|
|
600
|
+
const pieceSvg = getPieceSVG(fenPiece);
|
|
601
|
+
if (!pieceSvg) continue;
|
|
602
|
+
const visRow = flipped ? 7 - row : row;
|
|
603
|
+
const visCol = flipped ? 7 - col : col;
|
|
604
|
+
const x = boardX + visCol * squareSize;
|
|
605
|
+
const y = boardY + visRow * squareSize;
|
|
606
|
+
const innerContent = pieceSvg.replace(/^<svg[^>]*>/, "").replace(/<\/svg>$/, "");
|
|
607
|
+
parts.push(
|
|
608
|
+
`<g transform="translate(${x},${y}) scale(${squareSize / 45})">${innerContent}</g>`
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (showCoords) {
|
|
613
|
+
const files = flipped ? ["h", "g", "f", "e", "d", "c", "b", "a"] : ["a", "b", "c", "d", "e", "f", "g", "h"];
|
|
614
|
+
const ranks = flipped ? ["1", "2", "3", "4", "5", "6", "7", "8"] : ["8", "7", "6", "5", "4", "3", "2", "1"];
|
|
615
|
+
const fontSize = Math.round(Math.max(10, coordBorder * 0.72));
|
|
616
|
+
const fontFamily = "'Inter', system-ui, sans-serif";
|
|
617
|
+
const textAttrs = `font-family="${escapeXml(fontFamily)}" font-size="${fontSize}" font-weight="600" fill="${escapeXml(coordColor)}" text-anchor="middle"`;
|
|
618
|
+
for (let col = 0; col < 8; col++) {
|
|
619
|
+
const x = boardX + col * squareSize + squareSize / 2;
|
|
620
|
+
const y = boardY + size + coordBorder * 0.7;
|
|
621
|
+
parts.push(`<text x="${x}" y="${y}" ${textAttrs}>${files[col]}</text>`);
|
|
622
|
+
}
|
|
623
|
+
for (let row = 0; row < 8; row++) {
|
|
624
|
+
const frameOffset = showFrame ? frameThickness : 0;
|
|
625
|
+
const x = frameOffset + coordBorder * 0.5;
|
|
626
|
+
const y = boardY + row * squareSize + squareSize / 2 + fontSize * 0.35;
|
|
627
|
+
parts.push(`<text x="${x}" y="${y}" ${textAttrs}>${ranks[row]}</text>`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
parts.push("</svg>");
|
|
631
|
+
return parts.join("\n");
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/colors.ts
|
|
635
|
+
function hexToRgb(hex) {
|
|
636
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
637
|
+
if (!result) return { r: 0, g: 0, b: 0 };
|
|
638
|
+
const r = result[1], g = result[2], b = result[3];
|
|
639
|
+
if (r === void 0 || g === void 0 || b === void 0) return { r: 0, g: 0, b: 0 };
|
|
640
|
+
return { r: parseInt(r, 16), g: parseInt(g, 16), b: parseInt(b, 16) };
|
|
641
|
+
}
|
|
642
|
+
function rgbToHex(r, g, b) {
|
|
643
|
+
return "#" + [r, g, b].map((x) => {
|
|
644
|
+
const hex = x.toString(16);
|
|
645
|
+
return hex.length === 1 ? "0" + hex : hex;
|
|
646
|
+
}).join("");
|
|
647
|
+
}
|
|
648
|
+
function rgbToHsv(r, g, b) {
|
|
649
|
+
const rn = r / 255, gn = g / 255, bn = b / 255;
|
|
650
|
+
const max = Math.max(rn, gn, bn), min = Math.min(rn, gn, bn);
|
|
651
|
+
const d = max - min;
|
|
652
|
+
let h = 0;
|
|
653
|
+
const s = max === 0 ? 0 : d / max;
|
|
654
|
+
const v = max;
|
|
655
|
+
if (max !== min) {
|
|
656
|
+
switch (max) {
|
|
657
|
+
case rn:
|
|
658
|
+
h = (gn - bn) / d + (gn < bn ? 6 : 0);
|
|
659
|
+
break;
|
|
660
|
+
case gn:
|
|
661
|
+
h = (bn - rn) / d + 2;
|
|
662
|
+
break;
|
|
663
|
+
case bn:
|
|
664
|
+
h = (rn - gn) / d + 4;
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
h /= 6;
|
|
668
|
+
}
|
|
669
|
+
return { h, s, v };
|
|
670
|
+
}
|
|
671
|
+
function hsvToRgb(h, s, v) {
|
|
672
|
+
let r = 0, g = 0, b = 0;
|
|
673
|
+
const i = Math.floor(h * 6);
|
|
674
|
+
const f = h * 6 - i, p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s);
|
|
675
|
+
switch (i % 6) {
|
|
676
|
+
case 0:
|
|
677
|
+
r = v;
|
|
678
|
+
g = t;
|
|
679
|
+
b = p;
|
|
680
|
+
break;
|
|
681
|
+
case 1:
|
|
682
|
+
r = q;
|
|
683
|
+
g = v;
|
|
684
|
+
b = p;
|
|
685
|
+
break;
|
|
686
|
+
case 2:
|
|
687
|
+
r = p;
|
|
688
|
+
g = v;
|
|
689
|
+
b = t;
|
|
690
|
+
break;
|
|
691
|
+
case 3:
|
|
692
|
+
r = p;
|
|
693
|
+
g = q;
|
|
694
|
+
b = v;
|
|
695
|
+
break;
|
|
696
|
+
case 4:
|
|
697
|
+
r = t;
|
|
698
|
+
g = p;
|
|
699
|
+
b = v;
|
|
700
|
+
break;
|
|
701
|
+
case 5:
|
|
702
|
+
r = v;
|
|
703
|
+
g = p;
|
|
704
|
+
b = q;
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) };
|
|
708
|
+
}
|
|
709
|
+
function hexToHsv(hex) {
|
|
710
|
+
const { r, g, b } = hexToRgb(hex);
|
|
711
|
+
return rgbToHsv(r, g, b);
|
|
712
|
+
}
|
|
713
|
+
function hsvToHex(h, s, v) {
|
|
714
|
+
const { r, g, b } = hsvToRgb(h, s, v);
|
|
715
|
+
return rgbToHex(r, g, b);
|
|
716
|
+
}
|
|
717
|
+
function relativeLuminance(hex) {
|
|
718
|
+
const { r, g, b } = hexToRgb(hex);
|
|
719
|
+
const toLinear = (c) => {
|
|
720
|
+
const s = c / 255;
|
|
721
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
722
|
+
};
|
|
723
|
+
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
|
|
724
|
+
}
|
|
725
|
+
function contrastRatio(hex1, hex2) {
|
|
726
|
+
const l1 = relativeLuminance(hex1);
|
|
727
|
+
const l2 = relativeLuminance(hex2);
|
|
728
|
+
const lighter = Math.max(l1, l2);
|
|
729
|
+
const darker = Math.min(l1, l2);
|
|
730
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
731
|
+
}
|
|
732
|
+
function bestTextColor(backgroundHex) {
|
|
733
|
+
const contrastWithWhite = contrastRatio(backgroundHex, "#ffffff");
|
|
734
|
+
const contrastWithBlack = contrastRatio(backgroundHex, "#000000");
|
|
735
|
+
return contrastWithWhite >= contrastWithBlack ? "white" : "black";
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/validation.ts
|
|
739
|
+
var PROTOTYPE_POISON_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
740
|
+
function safeJSONParse(jsonString, fallback) {
|
|
741
|
+
if (!jsonString || typeof jsonString !== "string") return fallback;
|
|
742
|
+
try {
|
|
743
|
+
const parsed = JSON.parse(jsonString, (key, value) => {
|
|
744
|
+
if (key !== "" && PROTOTYPE_POISON_KEYS.has(key)) return void 0;
|
|
745
|
+
return value;
|
|
746
|
+
});
|
|
747
|
+
return parsed !== null && parsed !== void 0 ? parsed : fallback;
|
|
748
|
+
} catch {
|
|
749
|
+
return fallback;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
function sanitizeFileName(fileName) {
|
|
753
|
+
if (!fileName || typeof fileName !== "string") return "chess-position";
|
|
754
|
+
let s = fileName.replace(/[\\/:*?"<>|&]/g, "-");
|
|
755
|
+
s = s.replace(/\s+/g, "_");
|
|
756
|
+
s = s.replace(/^\.+/, "").replace(/\.+$/, "").trim();
|
|
757
|
+
if (s.length > 100) s = s.substring(0, 100);
|
|
758
|
+
return s || "chess-position";
|
|
759
|
+
}
|
|
760
|
+
function isValidHexColor2(color) {
|
|
761
|
+
if (!color || typeof color !== "string") return false;
|
|
762
|
+
return /^#[0-9A-Fa-f]{6}$/.test(color);
|
|
763
|
+
}
|
|
764
|
+
function sanitizeHexColor(color, fallback = "#ffffff") {
|
|
765
|
+
return isValidHexColor2(color) ? color : fallback;
|
|
766
|
+
}
|
|
767
|
+
function sanitizeInput(input, maxLength = 500) {
|
|
768
|
+
if (!input || typeof input !== "string") return "";
|
|
769
|
+
let s = input.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'").replace(/\//g, "/");
|
|
770
|
+
s = s.trim();
|
|
771
|
+
return s.length > maxLength ? s.substring(0, maxLength) : s;
|
|
772
|
+
}
|
|
773
|
+
function isRecord(val) {
|
|
774
|
+
return typeof val === "object" && val !== null && !Array.isArray(val);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// src/constants.ts
|
|
778
|
+
var DEFAULT_LIGHT_SQUARE = "#f0d9b5";
|
|
779
|
+
var DEFAULT_DARK_SQUARE = "#b58863";
|
|
780
|
+
var STARTING_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
|
|
781
|
+
var EMPTY_FEN = "8/8/8/8/8/8/8/8 w - - 0 1";
|
|
782
|
+
var PIECE_SETS = [
|
|
783
|
+
{ id: "alpha", name: "Alpha" },
|
|
784
|
+
{ id: "cardinal", name: "Cardinal" },
|
|
785
|
+
{ id: "california", name: "California" },
|
|
786
|
+
{ id: "cburnett", name: "Classic (CBurnett)" },
|
|
787
|
+
{ id: "companion", name: "Companion" },
|
|
788
|
+
{ id: "dubrovny", name: "Dubrovny" },
|
|
789
|
+
{ id: "fantasy", name: "Fantasy" },
|
|
790
|
+
{ id: "fresca", name: "Fresca" },
|
|
791
|
+
{ id: "gioco", name: "Gioco" },
|
|
792
|
+
{ id: "governor", name: "Governor" },
|
|
793
|
+
{ id: "horsey", name: "Horsey" },
|
|
794
|
+
{ id: "icpieces", name: "IC Pieces" },
|
|
795
|
+
{ id: "kosal", name: "Kosal" },
|
|
796
|
+
{ id: "leipzig", name: "Leipzig" },
|
|
797
|
+
{ id: "merida", name: "Merida" },
|
|
798
|
+
{ id: "maestro", name: "Maestro" },
|
|
799
|
+
{ id: "pirouetti", name: "Pirouetti" },
|
|
800
|
+
{ id: "pixel", name: "Pixel" },
|
|
801
|
+
{ id: "reillycraig", name: "Reilly Craig" },
|
|
802
|
+
{ id: "riohacha", name: "Riohacha" },
|
|
803
|
+
{ id: "spatial", name: "Spatial" },
|
|
804
|
+
{ id: "staunty", name: "Staunty" },
|
|
805
|
+
{ id: "tatiana", name: "Tatiana" }
|
|
806
|
+
];
|
|
807
|
+
var PIECE_SET_POPULARITY = [
|
|
808
|
+
"cburnett",
|
|
809
|
+
"merida",
|
|
810
|
+
"alpha",
|
|
811
|
+
"staunty",
|
|
812
|
+
"maestro",
|
|
813
|
+
"horsey",
|
|
814
|
+
"fantasy",
|
|
815
|
+
"leipzig",
|
|
816
|
+
"pixel",
|
|
817
|
+
"gioco",
|
|
818
|
+
"governor",
|
|
819
|
+
"tatiana",
|
|
820
|
+
"dubrovny",
|
|
821
|
+
"fresca",
|
|
822
|
+
"cardinal",
|
|
823
|
+
"icpieces",
|
|
824
|
+
"companion",
|
|
825
|
+
"california",
|
|
826
|
+
"pirouetti",
|
|
827
|
+
"kosal",
|
|
828
|
+
"reillycraig",
|
|
829
|
+
"spatial",
|
|
830
|
+
"riohacha"
|
|
831
|
+
];
|
|
832
|
+
var BOARD_THEMES = {
|
|
833
|
+
classic: { name: "Classic", light: "#f0d9b5", dark: "#b58863" },
|
|
834
|
+
brown: { name: "Brown", light: "#f0d9b5", dark: "#946f51" },
|
|
835
|
+
wood: { name: "Wood", light: "#d4af7a", dark: "#8b4513" },
|
|
836
|
+
sand: { name: "Sand", light: "#f5deb3", dark: "#d2b48c" },
|
|
837
|
+
slate: { name: "Slate", light: "#d0d0d0", dark: "#4a4a4a" },
|
|
838
|
+
marble: { name: "Marble", light: "#e3e6e8", dark: "#6e7a8a" },
|
|
839
|
+
blue: { name: "Blue", light: "#dee3e6", dark: "#8ca2ad" },
|
|
840
|
+
ocean: { name: "Ocean", light: "#c9e4f5", dark: "#4a90a4" },
|
|
841
|
+
green: { name: "Green", light: "#ffffdd", dark: "#86a666" },
|
|
842
|
+
forest: { name: "Forest", light: "#d4e8d4", dark: "#2d6930" },
|
|
843
|
+
mint: { name: "Mint", light: "#e0f5e9", dark: "#6fb98f" },
|
|
844
|
+
purple: { name: "Purple", light: "#e8d5c7", dark: "#9f7ab9" },
|
|
845
|
+
lavender: { name: "Lavender", light: "#e6e6fa", dark: "#9370db" },
|
|
846
|
+
red: { name: "Red", light: "#ffe0c5", dark: "#c97866" },
|
|
847
|
+
coral: { name: "Coral", light: "#ffebcd", dark: "#ff7f50" },
|
|
848
|
+
sunset: { name: "Sunset", light: "#ffe4b5", dark: "#ff8c42" },
|
|
849
|
+
pink: { name: "Pink", light: "#ffd7e0", dark: "#d87093" },
|
|
850
|
+
burgundy: { name: "Burgundy", light: "#e8d0d0", dark: "#8b3a3a" },
|
|
851
|
+
navy: { name: "Navy", light: "#d9e3f0", dark: "#405d7f" },
|
|
852
|
+
ice: { name: "Ice", light: "#e8f4f8", dark: "#7eb8da" }
|
|
853
|
+
};
|
|
854
|
+
var QUALITY_PRESETS = [
|
|
855
|
+
{ value: 1, label: "Print 1\xD7 (300 DPI)", description: "Standard print resolution \u2014 300 DPI at physical size", mode: "print", forceCoordinateBorder: false, estimatedSize: "10-110 KB" },
|
|
856
|
+
{ value: 2, label: "Print 2\xD7 (600 DPI)", description: "High print resolution \u2014 600 DPI at physical size", mode: "print", forceCoordinateBorder: false, estimatedSize: "50-440 KB" },
|
|
857
|
+
{ value: 3, label: "Social 3\xD7 (900 DPI)", description: "Keeps board size, higher zoom quality \u2014 900 DPI", mode: "social", forceCoordinateBorder: true, estimatedSize: "170KB-1.5MB" },
|
|
858
|
+
{ value: 4, label: "Max 4\xD7 (1200 DPI)", description: "Keeps board size, maximum zoom quality \u2014 1200 DPI", mode: "social", forceCoordinateBorder: true, estimatedSize: "300KB-2.7MB" }
|
|
859
|
+
];
|
|
860
|
+
var PIECE_MAP = {
|
|
861
|
+
wK: "wK",
|
|
862
|
+
wQ: "wQ",
|
|
863
|
+
wR: "wR",
|
|
864
|
+
wB: "wB",
|
|
865
|
+
wN: "wN",
|
|
866
|
+
wP: "wP",
|
|
867
|
+
bK: "bK",
|
|
868
|
+
bQ: "bQ",
|
|
869
|
+
bR: "bR",
|
|
870
|
+
bB: "bB",
|
|
871
|
+
bN: "bN",
|
|
872
|
+
bP: "bP"
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
// src/history.ts
|
|
876
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
877
|
+
var SEVEN_DAYS_MS = 7 * DAY_MS;
|
|
878
|
+
var THIRTY_DAYS_MS = 30 * DAY_MS;
|
|
879
|
+
var NINETY_DAYS_MS = 90 * DAY_MS;
|
|
880
|
+
function calculateStatus(lastActiveAt) {
|
|
881
|
+
const age = Date.now() - lastActiveAt;
|
|
882
|
+
if (age < SEVEN_DAYS_MS) return "green";
|
|
883
|
+
if (age < THIRTY_DAYS_MS) return "yellow";
|
|
884
|
+
return "red";
|
|
885
|
+
}
|
|
886
|
+
function createHistoryEntry(fen, source, dragSessionId = null) {
|
|
887
|
+
const now = Date.now();
|
|
888
|
+
return {
|
|
889
|
+
id: now,
|
|
890
|
+
fen,
|
|
891
|
+
createdAt: now,
|
|
892
|
+
lastActiveAt: now,
|
|
893
|
+
source,
|
|
894
|
+
isFavorite: false,
|
|
895
|
+
...dragSessionId ? { dragSessionId } : {}
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
function touchEntry(entry) {
|
|
899
|
+
return { ...entry, lastActiveAt: Date.now() };
|
|
900
|
+
}
|
|
901
|
+
function sortByMostRecent(entries) {
|
|
902
|
+
return [...entries].sort((a, b) => b.lastActiveAt - a.lastActiveAt);
|
|
903
|
+
}
|
|
904
|
+
function sortArchivedByArchiveDate(entries) {
|
|
905
|
+
return [...entries].sort((a, b) => b.archivedAt - a.archivedAt);
|
|
906
|
+
}
|
|
907
|
+
function mergeById(primary, secondary) {
|
|
908
|
+
const byId = /* @__PURE__ */ new Map();
|
|
909
|
+
for (const entry of secondary) byId.set(entry.id, entry);
|
|
910
|
+
for (const entry of primary) byId.set(entry.id, entry);
|
|
911
|
+
return [...byId.values()];
|
|
912
|
+
}
|
|
913
|
+
function applyFilters(entries, filters) {
|
|
914
|
+
if (!filters || Object.keys(filters).length === 0) return entries;
|
|
915
|
+
return entries.filter((entry) => {
|
|
916
|
+
if (filters.fenSearch && !entry.fen.toLowerCase().includes(filters.fenSearch.toLowerCase())) return false;
|
|
917
|
+
if (filters.dateFrom && entry.createdAt < filters.dateFrom) return false;
|
|
918
|
+
if (filters.dateTo && entry.createdAt > filters.dateTo) return false;
|
|
919
|
+
if (filters.status && calculateStatus(entry.lastActiveAt) !== filters.status) return false;
|
|
920
|
+
if (filters.source && entry.source !== filters.source) return false;
|
|
921
|
+
if (filters.favoritesOnly && !entry.isFavorite) return false;
|
|
922
|
+
return true;
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
function partitionByArchiveStatus(entries) {
|
|
926
|
+
const active = [];
|
|
927
|
+
const toArchive = [];
|
|
928
|
+
for (const entry of entries) {
|
|
929
|
+
if (entry.isFavorite || Date.now() - entry.lastActiveAt < NINETY_DAYS_MS) {
|
|
930
|
+
active.push(entry);
|
|
931
|
+
} else {
|
|
932
|
+
toArchive.push(entry);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return { active, toArchive };
|
|
936
|
+
}
|
|
937
|
+
function convertToArchivedEntry(entry, archiveSource = "auto") {
|
|
938
|
+
return {
|
|
939
|
+
id: entry.id,
|
|
940
|
+
fen: entry.fen,
|
|
941
|
+
createdAt: entry.createdAt,
|
|
942
|
+
lastActiveAt: entry.lastActiveAt,
|
|
943
|
+
archivedAt: Date.now(),
|
|
944
|
+
source: entry.source,
|
|
945
|
+
archiveSource,
|
|
946
|
+
isFavorite: entry.isFavorite
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// src/dpi.ts
|
|
951
|
+
async function changeDPI(blob, dpi, format) {
|
|
952
|
+
return format === "png" ? changePngDPI(blob, dpi) : changeJpegDPI(blob, dpi);
|
|
953
|
+
}
|
|
954
|
+
var crcTable = (() => {
|
|
955
|
+
const t = new Uint32Array(256);
|
|
956
|
+
for (let n = 0; n < 256; n++) {
|
|
957
|
+
let c = n;
|
|
958
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
959
|
+
t[n] = c;
|
|
960
|
+
}
|
|
961
|
+
return t;
|
|
962
|
+
})();
|
|
963
|
+
async function changePngDPI(blob, dpi) {
|
|
964
|
+
const bytes = new Uint8Array(await blob.arrayBuffer());
|
|
965
|
+
const ppm = Math.round(dpi * 39.3701);
|
|
966
|
+
const phys = new Uint8Array(21);
|
|
967
|
+
phys.set([0, 0, 0, 9, 112, 72, 89, 115]);
|
|
968
|
+
const dv = new DataView(phys.buffer);
|
|
969
|
+
dv.setUint32(8, ppm);
|
|
970
|
+
dv.setUint32(12, ppm);
|
|
971
|
+
phys[16] = 1;
|
|
972
|
+
let crc = 4294967295;
|
|
973
|
+
for (let i = 4; i < 17; i++) crc = crcTable[(crc ^ phys[i]) & 255] ^ crc >>> 8;
|
|
974
|
+
dv.setUint32(17, crc ^ 4294967295);
|
|
975
|
+
const chunks = [bytes.slice(0, 8)];
|
|
976
|
+
let pos = 8, inserted = false;
|
|
977
|
+
while (pos < bytes.length) {
|
|
978
|
+
const length = new DataView(bytes.buffer).getUint32(pos);
|
|
979
|
+
const type = String.fromCharCode(...Array.from(bytes.slice(pos + 4, pos + 8)));
|
|
980
|
+
if (!inserted && (type === "IDAT" || type === "PLTE")) {
|
|
981
|
+
chunks.push(phys);
|
|
982
|
+
inserted = true;
|
|
983
|
+
}
|
|
984
|
+
if (type !== "pHYs") chunks.push(bytes.slice(pos, pos + 12 + length));
|
|
985
|
+
pos += 12 + length;
|
|
986
|
+
}
|
|
987
|
+
if (!inserted) chunks.splice(chunks.length - 1, 0, phys);
|
|
988
|
+
return new Blob(chunks, { type: "image/png" });
|
|
989
|
+
}
|
|
990
|
+
async function changeJpegDPI(blob, dpi) {
|
|
991
|
+
dpi = Math.min(Math.max(Math.round(dpi), 1), 65535);
|
|
992
|
+
const bytes = new Uint8Array(await blob.arrayBuffer());
|
|
993
|
+
if (bytes[0] !== 255 || bytes[1] !== 216) return blob;
|
|
994
|
+
let pos = 2;
|
|
995
|
+
while (pos < bytes.length) {
|
|
996
|
+
if (bytes[pos] !== 255) break;
|
|
997
|
+
const marker = bytes[pos + 1];
|
|
998
|
+
const l1 = bytes[pos + 2], l2 = bytes[pos + 3];
|
|
999
|
+
if (l1 === void 0 || l2 === void 0) break;
|
|
1000
|
+
const length = (l1 << 8) + l2;
|
|
1001
|
+
if (marker === 224 && length >= 16) {
|
|
1002
|
+
const jfif = bytes.slice(pos, pos + 2 + length);
|
|
1003
|
+
jfif[13] = 1;
|
|
1004
|
+
jfif[14] = dpi >> 8 & 255;
|
|
1005
|
+
jfif[15] = dpi & 255;
|
|
1006
|
+
jfif[16] = dpi >> 8 & 255;
|
|
1007
|
+
jfif[17] = dpi & 255;
|
|
1008
|
+
const out2 = new Uint8Array(bytes.length);
|
|
1009
|
+
out2.set(bytes.slice(0, pos));
|
|
1010
|
+
out2.set(jfif, pos);
|
|
1011
|
+
out2.set(bytes.slice(pos + 2 + length), pos + 2 + length);
|
|
1012
|
+
return new Blob([out2], { type: "image/jpeg" });
|
|
1013
|
+
}
|
|
1014
|
+
if (marker === 218) break;
|
|
1015
|
+
pos += 2 + length;
|
|
1016
|
+
}
|
|
1017
|
+
const header = new Uint8Array([
|
|
1018
|
+
255,
|
|
1019
|
+
224,
|
|
1020
|
+
0,
|
|
1021
|
+
16,
|
|
1022
|
+
74,
|
|
1023
|
+
70,
|
|
1024
|
+
73,
|
|
1025
|
+
70,
|
|
1026
|
+
0,
|
|
1027
|
+
1,
|
|
1028
|
+
1,
|
|
1029
|
+
1,
|
|
1030
|
+
dpi >> 8 & 255,
|
|
1031
|
+
dpi & 255,
|
|
1032
|
+
dpi >> 8 & 255,
|
|
1033
|
+
dpi & 255,
|
|
1034
|
+
0,
|
|
1035
|
+
0
|
|
1036
|
+
]);
|
|
1037
|
+
const out = new Uint8Array(bytes.length + header.length);
|
|
1038
|
+
out.set(bytes.slice(0, 2));
|
|
1039
|
+
out.set(header, 2);
|
|
1040
|
+
out.set(bytes.slice(2), 2 + header.length);
|
|
1041
|
+
return new Blob([out], { type: "image/jpeg" });
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// src/themes.ts
|
|
1045
|
+
function getBoardTheme(id) {
|
|
1046
|
+
return BOARD_THEMES[id] ?? null;
|
|
1047
|
+
}
|
|
1048
|
+
function listThemeIds() {
|
|
1049
|
+
return Object.keys(BOARD_THEMES);
|
|
1050
|
+
}
|
|
1051
|
+
function getPieceSet(id) {
|
|
1052
|
+
return PIECE_SETS.find((set) => set.id === id) ?? null;
|
|
1053
|
+
}
|
|
1054
|
+
function pieceSetsByPopularity() {
|
|
1055
|
+
const rank = new Map(PIECE_SET_POPULARITY.map((id, i) => [id, i]));
|
|
1056
|
+
return [...PIECE_SETS].sort((a, b) => {
|
|
1057
|
+
const ra = rank.get(a.id) ?? Number.MAX_SAFE_INTEGER;
|
|
1058
|
+
const rb = rank.get(b.id) ?? Number.MAX_SAFE_INTEGER;
|
|
1059
|
+
return ra - rb;
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
function getQualityPreset(value) {
|
|
1063
|
+
return QUALITY_PRESETS.find((preset) => preset.value === value) ?? null;
|
|
1064
|
+
}
|
|
1065
|
+
function themeContrast(theme) {
|
|
1066
|
+
return contrastRatio(theme.light, theme.dark);
|
|
1067
|
+
}
|
|
1068
|
+
function themeCoordinateColor(theme) {
|
|
1069
|
+
return bestTextColor(theme.dark);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// src/image.ts
|
|
1073
|
+
function readImageDimensions(data) {
|
|
1074
|
+
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
1075
|
+
return readPng(bytes) ?? readJpeg(bytes);
|
|
1076
|
+
}
|
|
1077
|
+
function readPng(bytes) {
|
|
1078
|
+
if (bytes.length < 24) return null;
|
|
1079
|
+
const sig = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
1080
|
+
for (let i = 0; i < sig.length; i++) if (bytes[i] !== sig[i]) return null;
|
|
1081
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1082
|
+
return { width: dv.getUint32(16), height: dv.getUint32(20) };
|
|
1083
|
+
}
|
|
1084
|
+
function readJpeg(bytes) {
|
|
1085
|
+
if (bytes[0] !== 255 || bytes[1] !== 216) return null;
|
|
1086
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1087
|
+
let pos = 2;
|
|
1088
|
+
while (pos + 4 < bytes.length) {
|
|
1089
|
+
if (bytes[pos] !== 255) {
|
|
1090
|
+
pos++;
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
const marker = bytes[pos + 1];
|
|
1094
|
+
if (marker === void 0) break;
|
|
1095
|
+
const isSOF = marker >= 192 && marker <= 195 || marker >= 197 && marker <= 199 || marker >= 201 && marker <= 203 || marker >= 205 && marker <= 207;
|
|
1096
|
+
if (isSOF) {
|
|
1097
|
+
if (pos + 9 > bytes.length) return null;
|
|
1098
|
+
return { width: dv.getUint16(pos + 7), height: dv.getUint16(pos + 5) };
|
|
1099
|
+
}
|
|
1100
|
+
if (marker === 216 || marker === 217 || marker >= 208 && marker <= 215) {
|
|
1101
|
+
pos += 2;
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
const length = dv.getUint16(pos + 2);
|
|
1105
|
+
if (length < 2) return null;
|
|
1106
|
+
pos += 2 + length;
|
|
1107
|
+
}
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
function physicalSize(pixels, dpi) {
|
|
1111
|
+
if (dpi <= 0) return { inches: 0, mm: 0 };
|
|
1112
|
+
const inches = pixels / dpi;
|
|
1113
|
+
return { inches, mm: inches * 25.4 };
|
|
1114
|
+
}
|
|
1115
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1116
|
+
0 && (module.exports = {
|
|
1117
|
+
BOARD_THEMES,
|
|
1118
|
+
DEFAULT_DARK_SQUARE,
|
|
1119
|
+
DEFAULT_LIGHT_SQUARE,
|
|
1120
|
+
EMPTY_FEN,
|
|
1121
|
+
FENParseError,
|
|
1122
|
+
MAX_FEN_LENGTH,
|
|
1123
|
+
PIECES,
|
|
1124
|
+
PIECE_MAP,
|
|
1125
|
+
PIECE_SETS,
|
|
1126
|
+
PIECE_SET_POPULARITY,
|
|
1127
|
+
QUALITY_PRESETS,
|
|
1128
|
+
STARTING_FEN,
|
|
1129
|
+
applyFilters,
|
|
1130
|
+
bestTextColor,
|
|
1131
|
+
boardToFEN,
|
|
1132
|
+
buildFENRecord,
|
|
1133
|
+
calculateStatus,
|
|
1134
|
+
changeDPI,
|
|
1135
|
+
cloneBoard,
|
|
1136
|
+
contrastRatio,
|
|
1137
|
+
convertToArchivedEntry,
|
|
1138
|
+
countPieces,
|
|
1139
|
+
createEmptyBoard,
|
|
1140
|
+
createHistoryEntry,
|
|
1141
|
+
describeBoardPosition,
|
|
1142
|
+
fenPlacementField,
|
|
1143
|
+
findKing,
|
|
1144
|
+
flipBoard,
|
|
1145
|
+
generateDiagram,
|
|
1146
|
+
getBoardTheme,
|
|
1147
|
+
getCoordinateParams,
|
|
1148
|
+
getDisplayCoordinates,
|
|
1149
|
+
getFENValidationError,
|
|
1150
|
+
getPieceAt,
|
|
1151
|
+
getPieceSVG,
|
|
1152
|
+
getPieceSet,
|
|
1153
|
+
getQualityPreset,
|
|
1154
|
+
getSquareBounds,
|
|
1155
|
+
hasBothKings,
|
|
1156
|
+
hexToHsv,
|
|
1157
|
+
hexToRgb,
|
|
1158
|
+
hsvToHex,
|
|
1159
|
+
hsvToRgb,
|
|
1160
|
+
indicesToSquare,
|
|
1161
|
+
isBoardEmpty,
|
|
1162
|
+
isLightSquare,
|
|
1163
|
+
isRecord,
|
|
1164
|
+
isValidHexColor,
|
|
1165
|
+
listPieces,
|
|
1166
|
+
listThemeIds,
|
|
1167
|
+
materialBalance,
|
|
1168
|
+
mergeById,
|
|
1169
|
+
movePiece,
|
|
1170
|
+
normalizeFEN,
|
|
1171
|
+
parseFEN,
|
|
1172
|
+
parseFENRecord,
|
|
1173
|
+
partitionByArchiveStatus,
|
|
1174
|
+
physicalSize,
|
|
1175
|
+
pieceSetsByPopularity,
|
|
1176
|
+
pieceToName,
|
|
1177
|
+
readImageDimensions,
|
|
1178
|
+
relativeLuminance,
|
|
1179
|
+
removePieceAt,
|
|
1180
|
+
rgbToHex,
|
|
1181
|
+
rgbToHsv,
|
|
1182
|
+
safeJSONParse,
|
|
1183
|
+
sanitizeFileName,
|
|
1184
|
+
sanitizeHexColor,
|
|
1185
|
+
sanitizeInput,
|
|
1186
|
+
setPieceAt,
|
|
1187
|
+
sortArchivedByArchiveDate,
|
|
1188
|
+
sortByMostRecent,
|
|
1189
|
+
squareToIndices,
|
|
1190
|
+
themeContrast,
|
|
1191
|
+
themeCoordinateColor,
|
|
1192
|
+
toggleActiveColor,
|
|
1193
|
+
touchEntry,
|
|
1194
|
+
validateFEN,
|
|
1195
|
+
validateFENDetailed
|
|
1196
|
+
});
|