chess2img 0.1.4 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -1
- package/dist/index.cjs +327 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +325 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
- Render PNG chessboard images from `fen`, `pgn`, or `board` input.
|
|
21
21
|
- Use five bundled built-in themes: `merida`, `alpha`, `cburnett`, `cheq`, and `leipzig`.
|
|
22
22
|
- Highlight squares such as the last move or key tactical ideas.
|
|
23
|
-
- Customize board colors, size, padding, and board orientation.
|
|
23
|
+
- Customize board colors, size, padding, border sizing, coordinates, and board orientation.
|
|
24
24
|
- Use either the functional `renderChess(...)` API or the `ChessImageGenerator` class API.
|
|
25
25
|
- Register custom themes globally or pass a theme inline for one-off rendering.
|
|
26
26
|
- Consume the package from both JavaScript and TypeScript projects.
|
|
@@ -89,6 +89,61 @@ const png = await renderChess({
|
|
|
89
89
|
await writeFile("board.png", png);
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
### Automatic Coordinates
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { writeFile } from "node:fs/promises";
|
|
96
|
+
import { renderChess } from "chess2img";
|
|
97
|
+
|
|
98
|
+
const png = await renderChess({
|
|
99
|
+
fen: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
|
|
100
|
+
size: 480,
|
|
101
|
+
style: "cburnett",
|
|
102
|
+
coordinates: true,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await writeFile("board-with-auto-coordinates.png", png);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`coordinates: true` chooses `border` mode when `borderSize > 0`, otherwise it falls back to `inside` mode.
|
|
109
|
+
|
|
110
|
+
### Border Coordinates
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { writeFile } from "node:fs/promises";
|
|
114
|
+
import { renderChess } from "chess2img";
|
|
115
|
+
|
|
116
|
+
const png = await renderChess({
|
|
117
|
+
fen: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
|
|
118
|
+
size: 480,
|
|
119
|
+
style: "cburnett",
|
|
120
|
+
borderSize: 24,
|
|
121
|
+
coordinates: {
|
|
122
|
+
enabled: true,
|
|
123
|
+
position: "border",
|
|
124
|
+
color: "#333",
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await writeFile("board-with-border-coordinates.png", png);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Inside Coordinates
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { writeFile } from "node:fs/promises";
|
|
135
|
+
import { renderChess } from "chess2img";
|
|
136
|
+
|
|
137
|
+
const png = await renderChess({
|
|
138
|
+
fen: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
|
|
139
|
+
size: 480,
|
|
140
|
+
style: "cburnett",
|
|
141
|
+
coordinates: "inside",
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await writeFile("board-with-inside-coordinates.png", png);
|
|
145
|
+
```
|
|
146
|
+
|
|
92
147
|
### Class API
|
|
93
148
|
|
|
94
149
|
```ts
|
|
@@ -202,14 +257,22 @@ Semantics:
|
|
|
202
257
|
|
|
203
258
|
- `size`: board size in pixels
|
|
204
259
|
- `padding`: `[top, right, bottom, left]`
|
|
260
|
+
- `borderSize`: inner border size in pixels, from `0` up to `Math.floor(size / 8)`
|
|
205
261
|
- `flipped`: render from black's perspective when `true`
|
|
206
262
|
- `style`: built-in theme alias
|
|
207
263
|
- `theme`: built-in theme name, registered custom theme name, or inline `ThemeDefinition`
|
|
208
264
|
- `highlightSquares`: array of algebraic squares such as `["e4", "d5"]`
|
|
265
|
+
- `coordinates`: `boolean`, `"border"`, `"inside"`, or `{ enabled?: boolean; position?: "border" | "inside"; color?: string }`
|
|
209
266
|
- `colors.lightSquare`
|
|
210
267
|
- `colors.darkSquare`
|
|
211
268
|
- `colors.highlight`
|
|
212
269
|
|
|
270
|
+
`coordinates: false` or omitting the option disables labels. `coordinates: true` enables labels and chooses `border` mode when `borderSize > 0`, otherwise `inside` mode. Explicit `coordinates: "inside"` is always valid. Explicit `coordinates: "border"` requires `borderSize > 0` and throws `ValidationError` otherwise.
|
|
271
|
+
|
|
272
|
+
Inside coordinates use automatic light/dark contrast by default, similar to chess.com. If `coordinates.color` is provided, that exact color is used instead. Border coordinates keep a single-color label style with `#333` as the default.
|
|
273
|
+
|
|
274
|
+
At very small valid sizes, the renderer suppresses coordinates when they cannot fit legibly in the available border band or edge-square area.
|
|
275
|
+
|
|
213
276
|
### Errors
|
|
214
277
|
|
|
215
278
|
The library exports:
|
package/dist/index.cjs
CHANGED
|
@@ -81,8 +81,10 @@ function createEmptyBoardPosition() {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
// src/core/validators.ts
|
|
84
|
+
var import_canvas = require("canvas");
|
|
84
85
|
var SQUARE_PATTERN = /^[a-h][1-8]$/;
|
|
85
86
|
var PIECE_PATTERN = /^[prnbqkPRNBQK]$/;
|
|
87
|
+
var colorValidationContext = (0, import_canvas.createCanvas)(1, 1).getContext("2d");
|
|
86
88
|
function validateSize(size) {
|
|
87
89
|
if (!Number.isFinite(size) || size <= 0) {
|
|
88
90
|
throw new ValidationError(`Invalid board size: ${size}`);
|
|
@@ -135,6 +137,88 @@ function validateThemeName(name) {
|
|
|
135
137
|
}
|
|
136
138
|
return normalized;
|
|
137
139
|
}
|
|
140
|
+
function validateBorderSize(borderSize, size) {
|
|
141
|
+
const normalizedSize = validateSize(size);
|
|
142
|
+
const normalizedBorderSize = Math.round(borderSize);
|
|
143
|
+
const maxBorderSize = Math.floor(normalizedSize / 8);
|
|
144
|
+
if (!Number.isFinite(borderSize) || normalizedBorderSize < 0) {
|
|
145
|
+
throw new ValidationError(`Invalid borderSize: ${borderSize}`);
|
|
146
|
+
}
|
|
147
|
+
if (normalizedBorderSize > maxBorderSize) {
|
|
148
|
+
throw new ValidationError(
|
|
149
|
+
`Invalid borderSize: ${borderSize}. Maximum allowed for size ${normalizedSize} is ${maxBorderSize}`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return normalizedBorderSize;
|
|
153
|
+
}
|
|
154
|
+
function validateColorString(color, label = "color") {
|
|
155
|
+
if (typeof color !== "string" || color.trim() === "") {
|
|
156
|
+
throw new ValidationError(`Invalid ${label}: ${String(color)}`);
|
|
157
|
+
}
|
|
158
|
+
const normalized = color.trim();
|
|
159
|
+
colorValidationContext.fillStyle = "#010203";
|
|
160
|
+
colorValidationContext.fillStyle = normalized;
|
|
161
|
+
const firstPass = String(colorValidationContext.fillStyle);
|
|
162
|
+
colorValidationContext.fillStyle = "#fefefe";
|
|
163
|
+
colorValidationContext.fillStyle = normalized;
|
|
164
|
+
const secondPass = String(colorValidationContext.fillStyle);
|
|
165
|
+
if (firstPass !== secondPass) {
|
|
166
|
+
throw new ValidationError(`Invalid ${label}: ${color}`);
|
|
167
|
+
}
|
|
168
|
+
return normalized;
|
|
169
|
+
}
|
|
170
|
+
function validateBoardColors(colors) {
|
|
171
|
+
if (!colors) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (colors.lightSquare !== void 0) {
|
|
175
|
+
validateColorString(colors.lightSquare, "lightSquare color");
|
|
176
|
+
}
|
|
177
|
+
if (colors.darkSquare !== void 0) {
|
|
178
|
+
validateColorString(colors.darkSquare, "darkSquare color");
|
|
179
|
+
}
|
|
180
|
+
if (colors.highlight !== void 0) {
|
|
181
|
+
validateColorString(colors.highlight, "highlight color");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function isCoordinatesOptions(value) {
|
|
185
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
186
|
+
}
|
|
187
|
+
function isCoordinatesPosition(value) {
|
|
188
|
+
return value === "border" || value === "inside";
|
|
189
|
+
}
|
|
190
|
+
function validateCoordinatesOption(coordinates, borderSize) {
|
|
191
|
+
if (coordinates === void 0 || typeof coordinates === "boolean") {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (isCoordinatesPosition(coordinates)) {
|
|
195
|
+
if (coordinates === "border" && borderSize === 0) {
|
|
196
|
+
throw new ValidationError(
|
|
197
|
+
"coordinates position 'border' requires borderSize > 0"
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (!isCoordinatesOptions(coordinates)) {
|
|
203
|
+
throw new ValidationError(
|
|
204
|
+
"coordinates must be false, true, 'border', 'inside', or an options object"
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (coordinates.enabled !== void 0 && typeof coordinates.enabled !== "boolean") {
|
|
208
|
+
throw new ValidationError("coordinates.enabled must be a boolean");
|
|
209
|
+
}
|
|
210
|
+
if (coordinates.position !== void 0 && !isCoordinatesPosition(coordinates.position)) {
|
|
211
|
+
throw new ValidationError("coordinates.position must be 'border' or 'inside'");
|
|
212
|
+
}
|
|
213
|
+
if (coordinates.enabled !== false && coordinates.position === "border" && borderSize === 0) {
|
|
214
|
+
throw new ValidationError(
|
|
215
|
+
"coordinates position 'border' requires borderSize > 0"
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (coordinates.color !== void 0) {
|
|
219
|
+
validateColorString(coordinates.color, "coordinates.color");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
138
222
|
|
|
139
223
|
// src/core/parsers.ts
|
|
140
224
|
var PIECE_SYMBOL_TO_KEY = {
|
|
@@ -204,32 +288,103 @@ function normalizeHighlights(input) {
|
|
|
204
288
|
}
|
|
205
289
|
|
|
206
290
|
// src/render/canvas-renderer.ts
|
|
207
|
-
var
|
|
291
|
+
var import_canvas3 = require("canvas");
|
|
208
292
|
|
|
209
293
|
// src/core/geometry.ts
|
|
210
294
|
function createBoardGeometry({
|
|
211
295
|
size,
|
|
212
296
|
padding,
|
|
297
|
+
borderSize,
|
|
213
298
|
flipped
|
|
214
299
|
}) {
|
|
215
300
|
const [top, right, bottom, left] = padding;
|
|
216
|
-
const
|
|
301
|
+
const boardOuterSize = size;
|
|
302
|
+
const boardOuterX = left;
|
|
303
|
+
const boardOuterY = top;
|
|
304
|
+
const boardX = left + borderSize;
|
|
305
|
+
const boardY = top + borderSize;
|
|
306
|
+
const boardSize = size - borderSize * 2;
|
|
307
|
+
const squareSize = boardSize / 8;
|
|
217
308
|
const squares = Object.fromEntries(
|
|
218
309
|
SQUARES.map((square, index) => {
|
|
219
310
|
const fileIndex = index % 8;
|
|
220
311
|
const rankIndex = Math.floor(index / 8);
|
|
221
|
-
const x =
|
|
222
|
-
const y =
|
|
312
|
+
const x = boardX + (flipped ? 7 - fileIndex : fileIndex) * squareSize;
|
|
313
|
+
const y = boardY + (flipped ? 7 - rankIndex : rankIndex) * squareSize;
|
|
223
314
|
return [square, { x, y, size: squareSize }];
|
|
224
315
|
})
|
|
225
316
|
);
|
|
317
|
+
const displayedFiles = flipped ? [...FILES].reverse() : [...FILES];
|
|
318
|
+
const displayedRanks = flipped ? [...RANKS].reverse() : [...RANKS];
|
|
319
|
+
const bottomEdgeRank = flipped ? "8" : "1";
|
|
320
|
+
const leftEdgeFile = flipped ? "h" : "a";
|
|
321
|
+
const borderFileLabels = borderSize ? displayedFiles.map((file, fileIndex) => ({
|
|
322
|
+
text: file,
|
|
323
|
+
x: boardX + fileIndex * squareSize + squareSize / 2,
|
|
324
|
+
y: boardOuterY + boardOuterSize - borderSize / 2,
|
|
325
|
+
square: `${file}${bottomEdgeRank}`,
|
|
326
|
+
textAlign: "center",
|
|
327
|
+
textBaseline: "middle"
|
|
328
|
+
})) : [];
|
|
329
|
+
const borderRankLabels = borderSize ? displayedRanks.map((rank, rankIndex) => ({
|
|
330
|
+
text: rank,
|
|
331
|
+
x: boardOuterX + borderSize / 2,
|
|
332
|
+
y: boardY + rankIndex * squareSize + squareSize / 2,
|
|
333
|
+
square: `${leftEdgeFile}${rank}`,
|
|
334
|
+
textAlign: "center",
|
|
335
|
+
textBaseline: "middle"
|
|
336
|
+
})) : [];
|
|
337
|
+
const insideFileInsetX = squareSize * 0.14;
|
|
338
|
+
const insideFileInsetY = squareSize * 0.1;
|
|
339
|
+
const insideRankInsetX = squareSize * 0.14;
|
|
340
|
+
const insideRankInsetY = squareSize * 0.1;
|
|
341
|
+
const insideLabelMaxWidth = squareSize * 0.26;
|
|
342
|
+
const insideLabelMaxHeight = squareSize * 0.24;
|
|
343
|
+
const insideFileLabels = displayedFiles.map((file) => {
|
|
344
|
+
const square = `${file}${bottomEdgeRank}`;
|
|
345
|
+
const squareGeometry = squares[square];
|
|
346
|
+
return {
|
|
347
|
+
text: file,
|
|
348
|
+
x: squareGeometry.x + insideFileInsetX,
|
|
349
|
+
y: squareGeometry.y + squareGeometry.size - insideFileInsetY,
|
|
350
|
+
square,
|
|
351
|
+
textAlign: "left",
|
|
352
|
+
textBaseline: "bottom"
|
|
353
|
+
};
|
|
354
|
+
});
|
|
355
|
+
const insideRankLabels = displayedRanks.map((rank) => {
|
|
356
|
+
const square = `${leftEdgeFile}${rank}`;
|
|
357
|
+
const squareGeometry = squares[square];
|
|
358
|
+
return {
|
|
359
|
+
text: rank,
|
|
360
|
+
x: squareGeometry.x + insideRankInsetX,
|
|
361
|
+
y: squareGeometry.y + insideRankInsetY,
|
|
362
|
+
square,
|
|
363
|
+
textAlign: "left",
|
|
364
|
+
textBaseline: "top"
|
|
365
|
+
};
|
|
366
|
+
});
|
|
226
367
|
return {
|
|
227
368
|
imageWidth: left + size + right,
|
|
228
369
|
imageHeight: top + size + bottom,
|
|
229
370
|
squareSize,
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
371
|
+
borderSize,
|
|
372
|
+
boardOuterX,
|
|
373
|
+
boardOuterY,
|
|
374
|
+
boardOuterSize,
|
|
375
|
+
boardX,
|
|
376
|
+
boardY,
|
|
377
|
+
boardSize,
|
|
378
|
+
borderFileLabels,
|
|
379
|
+
borderRankLabels,
|
|
380
|
+
insideFileLabels,
|
|
381
|
+
insideRankLabels,
|
|
382
|
+
insideFileInsetX,
|
|
383
|
+
insideFileInsetY,
|
|
384
|
+
insideRankInsetX,
|
|
385
|
+
insideRankInsetY,
|
|
386
|
+
insideLabelMaxWidth,
|
|
387
|
+
insideLabelMaxHeight,
|
|
233
388
|
squares
|
|
234
389
|
};
|
|
235
390
|
}
|
|
@@ -259,7 +414,7 @@ var SourceAssetCache = class {
|
|
|
259
414
|
|
|
260
415
|
// src/render/rasterizer.ts
|
|
261
416
|
var import_promises = require("fs/promises");
|
|
262
|
-
var
|
|
417
|
+
var import_canvas2 = require("canvas");
|
|
263
418
|
var import_resvg_js = require("@resvg/resvg-js");
|
|
264
419
|
var svgSourceCache = new SourceAssetCache();
|
|
265
420
|
var imageBufferCache = new SourceAssetCache();
|
|
@@ -299,8 +454,8 @@ async function rasterizeSvgAsset(filePath, squareSize) {
|
|
|
299
454
|
}
|
|
300
455
|
});
|
|
301
456
|
const pngBuffer = resvg.render().asPng();
|
|
302
|
-
const image = await (0,
|
|
303
|
-
const canvas = (0,
|
|
457
|
+
const image = await (0, import_canvas2.loadImage)(pngBuffer);
|
|
458
|
+
const canvas = (0, import_canvas2.createCanvas)(squareSize, squareSize);
|
|
304
459
|
const context = canvas.getContext("2d");
|
|
305
460
|
context.drawImage(image, 0, 0, squareSize, squareSize);
|
|
306
461
|
return canvas;
|
|
@@ -311,8 +466,8 @@ async function rasterizeSvgAsset(filePath, squareSize) {
|
|
|
311
466
|
async function rasterizePngAsset(filePath, squareSize) {
|
|
312
467
|
try {
|
|
313
468
|
const pngSource = await readBinaryAsset(filePath);
|
|
314
|
-
const image = await (0,
|
|
315
|
-
const canvas = (0,
|
|
469
|
+
const image = await (0, import_canvas2.loadImage)(pngSource);
|
|
470
|
+
const canvas = (0, import_canvas2.createCanvas)(squareSize, squareSize);
|
|
316
471
|
const context = canvas.getContext("2d");
|
|
317
472
|
context.drawImage(image, 0, 0, squareSize, squareSize);
|
|
318
473
|
return canvas;
|
|
@@ -329,6 +484,13 @@ async function rasterizeThemeAsset(asset, squareSize) {
|
|
|
329
484
|
|
|
330
485
|
// src/render/canvas-renderer.ts
|
|
331
486
|
var pieceRasterCache = new RasterAssetCache();
|
|
487
|
+
var MIN_COORDINATE_FONT_SIZE = 8;
|
|
488
|
+
var MAX_FILE_LABEL_WIDTH_RATIO = 0.75;
|
|
489
|
+
var MAX_RANK_LABEL_WIDTH_RATIO = 0.7;
|
|
490
|
+
var MAX_LABEL_HEIGHT_RATIO = 0.7;
|
|
491
|
+
var INSIDE_COORDINATE_MAX_FONT_RATIO = 0.34;
|
|
492
|
+
var INSIDE_LIGHT_LABEL_COLOR = "rgba(255,255,255,0.6)";
|
|
493
|
+
var INSIDE_DARK_LABEL_COLOR = "rgba(0,0,0,0.45)";
|
|
332
494
|
function isDarkSquare(square) {
|
|
333
495
|
const fileIndex = square.charCodeAt(0) - 97;
|
|
334
496
|
const rankNumber = Number(square[1]);
|
|
@@ -344,15 +506,118 @@ async function getPieceRaster(themeName, pieceKey, asset, squareSize) {
|
|
|
344
506
|
pieceRasterCache.set(cacheKey, raster);
|
|
345
507
|
return raster;
|
|
346
508
|
}
|
|
509
|
+
function resolveInsideLabelColor(request, square) {
|
|
510
|
+
if (request.coordinates.color) {
|
|
511
|
+
return request.coordinates.color;
|
|
512
|
+
}
|
|
513
|
+
return isDarkSquare(square) ? INSIDE_LIGHT_LABEL_COLOR : INSIDE_DARK_LABEL_COLOR;
|
|
514
|
+
}
|
|
515
|
+
function resolveBorderCoordinateFontSize(context, geometry) {
|
|
516
|
+
const maxFontSize = Math.floor(
|
|
517
|
+
Math.min(geometry.squareSize * 0.6, geometry.borderSize * 0.65)
|
|
518
|
+
);
|
|
519
|
+
let fontSize = null;
|
|
520
|
+
for (let candidate = maxFontSize; candidate >= MIN_COORDINATE_FONT_SIZE; candidate -= 1) {
|
|
521
|
+
context.font = `${candidate}px sans-serif`;
|
|
522
|
+
const filesFit = geometry.borderFileLabels.every((label) => {
|
|
523
|
+
const metrics = context.measureText(label.text);
|
|
524
|
+
const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
|
|
525
|
+
return metrics.width <= geometry.squareSize * MAX_FILE_LABEL_WIDTH_RATIO && textHeight <= geometry.borderSize * MAX_LABEL_HEIGHT_RATIO;
|
|
526
|
+
});
|
|
527
|
+
const ranksFit = geometry.borderRankLabels.every((label) => {
|
|
528
|
+
const metrics = context.measureText(label.text);
|
|
529
|
+
const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
|
|
530
|
+
return metrics.width <= geometry.borderSize * MAX_RANK_LABEL_WIDTH_RATIO && textHeight <= geometry.squareSize * MAX_LABEL_HEIGHT_RATIO;
|
|
531
|
+
});
|
|
532
|
+
if (filesFit && ranksFit) {
|
|
533
|
+
fontSize = candidate;
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return fontSize;
|
|
538
|
+
}
|
|
539
|
+
function resolveInsideCoordinateFontSize(context, geometry) {
|
|
540
|
+
const maxFontSize = Math.floor(
|
|
541
|
+
geometry.squareSize * INSIDE_COORDINATE_MAX_FONT_RATIO
|
|
542
|
+
);
|
|
543
|
+
let fontSize = null;
|
|
544
|
+
for (let candidate = maxFontSize; candidate >= MIN_COORDINATE_FONT_SIZE; candidate -= 1) {
|
|
545
|
+
context.font = `${candidate}px sans-serif`;
|
|
546
|
+
const filesFit = geometry.insideFileLabels.every((label) => {
|
|
547
|
+
const metrics = context.measureText(label.text);
|
|
548
|
+
const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
|
|
549
|
+
return metrics.width <= geometry.insideLabelMaxWidth && textHeight <= geometry.insideLabelMaxHeight;
|
|
550
|
+
});
|
|
551
|
+
const ranksFit = geometry.insideRankLabels.every((label) => {
|
|
552
|
+
const metrics = context.measureText(label.text);
|
|
553
|
+
const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
|
|
554
|
+
return metrics.width <= geometry.insideLabelMaxWidth && textHeight <= geometry.insideLabelMaxHeight;
|
|
555
|
+
});
|
|
556
|
+
if (filesFit && ranksFit) {
|
|
557
|
+
fontSize = candidate;
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return fontSize;
|
|
562
|
+
}
|
|
563
|
+
function drawBorderCoordinates(context, request, geometry) {
|
|
564
|
+
if (geometry.borderSize === 0) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
const fontSize = resolveBorderCoordinateFontSize(context, geometry);
|
|
568
|
+
if (fontSize === null) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
context.fillStyle = request.coordinates.color ?? "#333";
|
|
572
|
+
context.font = `${fontSize}px sans-serif`;
|
|
573
|
+
context.textAlign = "center";
|
|
574
|
+
context.textBaseline = "middle";
|
|
575
|
+
for (const label of geometry.borderFileLabels) {
|
|
576
|
+
context.fillText(label.text, label.x, label.y);
|
|
577
|
+
}
|
|
578
|
+
for (const label of geometry.borderRankLabels) {
|
|
579
|
+
context.fillText(label.text, label.x, label.y);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function drawInsideCoordinates(context, request, geometry) {
|
|
583
|
+
const fontSize = resolveInsideCoordinateFontSize(context, geometry);
|
|
584
|
+
if (fontSize === null) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
context.font = `${fontSize}px sans-serif`;
|
|
588
|
+
for (const label of geometry.insideFileLabels) {
|
|
589
|
+
context.fillStyle = resolveInsideLabelColor(request, label.square);
|
|
590
|
+
context.textAlign = label.textAlign;
|
|
591
|
+
context.textBaseline = label.textBaseline;
|
|
592
|
+
context.fillText(label.text, label.x, label.y);
|
|
593
|
+
}
|
|
594
|
+
for (const label of geometry.insideRankLabels) {
|
|
595
|
+
context.fillStyle = resolveInsideLabelColor(request, label.square);
|
|
596
|
+
context.textAlign = label.textAlign;
|
|
597
|
+
context.textBaseline = label.textBaseline;
|
|
598
|
+
context.fillText(label.text, label.x, label.y);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function drawCoordinates(context, request, geometry) {
|
|
602
|
+
if (!request.coordinates.enabled) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (request.coordinates.position === "border") {
|
|
606
|
+
drawBorderCoordinates(context, request, geometry);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
drawInsideCoordinates(context, request, geometry);
|
|
610
|
+
}
|
|
347
611
|
var CanvasPngRenderer = class {
|
|
348
612
|
async render(request) {
|
|
349
613
|
try {
|
|
350
614
|
const geometry = createBoardGeometry({
|
|
351
615
|
size: request.size,
|
|
352
616
|
padding: request.padding,
|
|
617
|
+
borderSize: request.borderSize,
|
|
353
618
|
flipped: request.flipped
|
|
354
619
|
});
|
|
355
|
-
const canvas = (0,
|
|
620
|
+
const canvas = (0, import_canvas3.createCanvas)(geometry.imageWidth, geometry.imageHeight);
|
|
356
621
|
const context = canvas.getContext("2d");
|
|
357
622
|
context.fillStyle = request.colors.lightSquare;
|
|
358
623
|
context.fillRect(0, 0, geometry.imageWidth, geometry.imageHeight);
|
|
@@ -392,6 +657,7 @@ var CanvasPngRenderer = class {
|
|
|
392
657
|
squareGeometry.size
|
|
393
658
|
);
|
|
394
659
|
}
|
|
660
|
+
drawCoordinates(context, request, geometry);
|
|
395
661
|
return canvas.toBuffer("image/png");
|
|
396
662
|
} catch (error) {
|
|
397
663
|
if (error instanceof RenderError) {
|
|
@@ -554,11 +820,16 @@ function resolveTheme({ theme, style }) {
|
|
|
554
820
|
// src/utils/normalization.ts
|
|
555
821
|
var DEFAULT_SIZE = 480;
|
|
556
822
|
var DEFAULT_PADDING = [0, 0, 0, 0];
|
|
823
|
+
var DEFAULT_BORDER_SIZE = 0;
|
|
557
824
|
var DEFAULT_COLORS = {
|
|
558
825
|
lightSquare: "#f0d9b5",
|
|
559
826
|
darkSquare: "#b58863",
|
|
560
827
|
highlight: "rgba(255, 206, 0, 0.45)"
|
|
561
828
|
};
|
|
829
|
+
var DEFAULT_COORDINATES = {
|
|
830
|
+
enabled: false,
|
|
831
|
+
position: "inside"
|
|
832
|
+
};
|
|
562
833
|
function normalizeColors(colors) {
|
|
563
834
|
return {
|
|
564
835
|
lightSquare: colors?.lightSquare ?? DEFAULT_COLORS.lightSquare,
|
|
@@ -566,17 +837,52 @@ function normalizeColors(colors) {
|
|
|
566
837
|
highlight: colors?.highlight ?? DEFAULT_COLORS.highlight
|
|
567
838
|
};
|
|
568
839
|
}
|
|
840
|
+
function normalizeCoordinates(coordinates, borderSize) {
|
|
841
|
+
if (coordinates === void 0 || coordinates === false) {
|
|
842
|
+
return { ...DEFAULT_COORDINATES };
|
|
843
|
+
}
|
|
844
|
+
const defaultPosition = borderSize > 0 ? "border" : "inside";
|
|
845
|
+
if (coordinates === true) {
|
|
846
|
+
return {
|
|
847
|
+
enabled: true,
|
|
848
|
+
position: defaultPosition,
|
|
849
|
+
color: defaultPosition === "border" ? "#333" : void 0
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
if (coordinates === "border" || coordinates === "inside") {
|
|
853
|
+
return {
|
|
854
|
+
enabled: true,
|
|
855
|
+
position: coordinates,
|
|
856
|
+
color: coordinates === "border" ? "#333" : void 0
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
const position = coordinates.position ?? defaultPosition;
|
|
860
|
+
return {
|
|
861
|
+
enabled: coordinates.enabled ?? true,
|
|
862
|
+
position,
|
|
863
|
+
color: coordinates.color ?? (position === "border" ? "#333" : void 0)
|
|
864
|
+
};
|
|
865
|
+
}
|
|
569
866
|
function normalizeRenderInputs(options) {
|
|
867
|
+
const size = validateSize(options.size ?? DEFAULT_SIZE);
|
|
868
|
+
const borderSize = validateBorderSize(
|
|
869
|
+
options.borderSize ?? DEFAULT_BORDER_SIZE,
|
|
870
|
+
size
|
|
871
|
+
);
|
|
872
|
+
validateBoardColors(options.colors);
|
|
873
|
+
validateCoordinatesOption(options.coordinates, borderSize);
|
|
570
874
|
return {
|
|
571
|
-
size
|
|
875
|
+
size,
|
|
572
876
|
padding: normalizePadding(options.padding ?? DEFAULT_PADDING),
|
|
877
|
+
borderSize,
|
|
573
878
|
flipped: options.flipped ?? false,
|
|
574
879
|
theme: resolveTheme({
|
|
575
880
|
theme: options.theme,
|
|
576
881
|
style: options.style
|
|
577
882
|
}),
|
|
578
883
|
highlightSquares: normalizeHighlights(options.highlightSquares ?? []),
|
|
579
|
-
colors: normalizeColors(options.colors)
|
|
884
|
+
colors: normalizeColors(options.colors),
|
|
885
|
+
coordinates: normalizeCoordinates(options.coordinates, borderSize)
|
|
580
886
|
};
|
|
581
887
|
}
|
|
582
888
|
|
|
@@ -625,8 +931,10 @@ var ChessImageGenerator = class {
|
|
|
625
931
|
highlights: normalized.highlightSquares,
|
|
626
932
|
size: normalized.size,
|
|
627
933
|
padding: normalized.padding,
|
|
934
|
+
borderSize: normalized.borderSize,
|
|
628
935
|
flipped: normalized.flipped,
|
|
629
|
-
colors: normalized.colors
|
|
936
|
+
colors: normalized.colors,
|
|
937
|
+
coordinates: normalized.coordinates
|
|
630
938
|
});
|
|
631
939
|
}
|
|
632
940
|
async toFile(filePath) {
|
|
@@ -666,8 +974,10 @@ async function renderChess(options) {
|
|
|
666
974
|
highlights: normalized.highlightSquares,
|
|
667
975
|
size: normalized.size,
|
|
668
976
|
padding: normalized.padding,
|
|
977
|
+
borderSize: normalized.borderSize,
|
|
669
978
|
flipped: normalized.flipped,
|
|
670
|
-
colors: normalized.colors
|
|
979
|
+
colors: normalized.colors,
|
|
980
|
+
coordinates: normalized.coordinates
|
|
671
981
|
});
|
|
672
982
|
}
|
|
673
983
|
// Annotate the CommonJS export names for ESM import in node:
|