chess2img 0.2.1 → 0.3.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/README.md +38 -3
- package/dist/index.cjs +189 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -4
- package/dist/index.d.ts +21 -4
- package/dist/index.js +189 -53
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ const png = await renderChess({
|
|
|
50
50
|
darkSquare: "#769656",
|
|
51
51
|
highlight: "rgba(246, 246, 105, 0.6)",
|
|
52
52
|
},
|
|
53
|
-
|
|
53
|
+
highlights: ["e2", "e4"],
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
await writeFile("board.png", png);
|
|
@@ -144,6 +144,38 @@ const png = await renderChess({
|
|
|
144
144
|
await writeFile("board-with-inside-coordinates.png", png);
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
+
### Circle Highlights
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { writeFile } from "node:fs/promises";
|
|
151
|
+
import { renderChess } from "chess2img";
|
|
152
|
+
|
|
153
|
+
const png = await renderChess({
|
|
154
|
+
fen: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
|
|
155
|
+
size: 480,
|
|
156
|
+
style: "cburnett",
|
|
157
|
+
highlights: [{ square: "e4", style: "circle" }],
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await writeFile("board-with-circle-highlight.png", png);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Combined Fill And Circle Highlights
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { writeFile } from "node:fs/promises";
|
|
167
|
+
import { renderChess } from "chess2img";
|
|
168
|
+
|
|
169
|
+
const png = await renderChess({
|
|
170
|
+
fen: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
|
|
171
|
+
size: 480,
|
|
172
|
+
style: "cburnett",
|
|
173
|
+
highlights: ["e4", { square: "e4", style: "circle" }],
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await writeFile("board-with-fill-and-circle-highlights.png", png);
|
|
177
|
+
```
|
|
178
|
+
|
|
147
179
|
### Class API
|
|
148
180
|
|
|
149
181
|
```ts
|
|
@@ -241,7 +273,7 @@ Methods:
|
|
|
241
273
|
- `loadFEN(fen: string): Promise<void>`
|
|
242
274
|
- `loadPGN(pgn: string): Promise<void>`
|
|
243
275
|
- `loadBoard(board: BoardArray): Promise<void>`
|
|
244
|
-
- `setHighlights(
|
|
276
|
+
- `setHighlights(highlights: HighlightInput[]): void`
|
|
245
277
|
- `clearHighlights(): void`
|
|
246
278
|
- `toBuffer(): Promise<Buffer>`
|
|
247
279
|
- `toFile(filePath: string): Promise<void>`
|
|
@@ -261,7 +293,8 @@ Semantics:
|
|
|
261
293
|
- `flipped`: render from black's perspective when `true`
|
|
262
294
|
- `style`: built-in theme alias
|
|
263
295
|
- `theme`: built-in theme name, registered custom theme name, or inline `ThemeDefinition`
|
|
264
|
-
- `
|
|
296
|
+
- `highlights`: array of square strings or highlight objects such as `["e4", { square: "d5", style: "circle" }]`
|
|
297
|
+
- `highlightSquares`: compatibility alias for `highlights`
|
|
265
298
|
- `coordinates`: `boolean`, `"border"`, `"inside"`, or `{ enabled?: boolean; position?: "border" | "inside"; color?: string }`
|
|
266
299
|
- `colors.lightSquare`
|
|
267
300
|
- `colors.darkSquare`
|
|
@@ -269,6 +302,8 @@ Semantics:
|
|
|
269
302
|
|
|
270
303
|
`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
304
|
|
|
305
|
+
`highlights` is the preferred API. Each entry may be a square string for a filled highlight, or an object with `square`, `style`, `color`, `opacity`, and `lineWidth`. `highlightSquares` remains available for backward compatibility, but should not be used together with `highlights` in the same call.
|
|
306
|
+
|
|
272
307
|
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
308
|
|
|
274
309
|
At very small valid sizes, the renderer suppresses coordinates when they cannot fit legibly in the available border band or edge-square area.
|
package/dist/index.cjs
CHANGED
|
@@ -187,6 +187,12 @@ function isCoordinatesOptions(value) {
|
|
|
187
187
|
function isCoordinatesPosition(value) {
|
|
188
188
|
return value === "border" || value === "inside";
|
|
189
189
|
}
|
|
190
|
+
function isHighlightOptions(value) {
|
|
191
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
192
|
+
}
|
|
193
|
+
function isHighlightStyle(value) {
|
|
194
|
+
return value === "fill" || value === "circle";
|
|
195
|
+
}
|
|
190
196
|
function validateCoordinatesOption(coordinates, borderSize) {
|
|
191
197
|
if (coordinates === void 0 || typeof coordinates === "boolean") {
|
|
192
198
|
return;
|
|
@@ -219,6 +225,49 @@ function validateCoordinatesOption(coordinates, borderSize) {
|
|
|
219
225
|
validateColorString(coordinates.color, "coordinates.color");
|
|
220
226
|
}
|
|
221
227
|
}
|
|
228
|
+
function validateHighlightEntry(entry) {
|
|
229
|
+
if (typeof entry === "string") {
|
|
230
|
+
validateSquare(entry);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (!isHighlightOptions(entry)) {
|
|
234
|
+
throw new ValidationError("highlights entries must be square strings or highlight objects");
|
|
235
|
+
}
|
|
236
|
+
if (typeof entry.square !== "string") {
|
|
237
|
+
throw new ValidationError("highlight.square must be a valid algebraic square");
|
|
238
|
+
}
|
|
239
|
+
validateSquare(entry.square);
|
|
240
|
+
if (entry.style !== void 0 && !isHighlightStyle(entry.style)) {
|
|
241
|
+
throw new ValidationError("highlight.style must be 'fill' or 'circle'");
|
|
242
|
+
}
|
|
243
|
+
if (entry.color !== void 0) {
|
|
244
|
+
validateColorString(entry.color, "highlight.color");
|
|
245
|
+
}
|
|
246
|
+
if (entry.opacity !== void 0 && (!Number.isFinite(entry.opacity) || entry.opacity < 0 || entry.opacity > 1)) {
|
|
247
|
+
throw new ValidationError("highlight.opacity must be a finite number between 0 and 1");
|
|
248
|
+
}
|
|
249
|
+
if (entry.lineWidth !== void 0 && (!Number.isFinite(entry.lineWidth) || entry.lineWidth <= 0)) {
|
|
250
|
+
throw new ValidationError("highlight.lineWidth must be a finite number greater than 0");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function validateHighlightOptions(highlights) {
|
|
254
|
+
if (highlights === void 0) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (!Array.isArray(highlights)) {
|
|
258
|
+
throw new ValidationError("highlights must be an array");
|
|
259
|
+
}
|
|
260
|
+
for (const entry of highlights) {
|
|
261
|
+
validateHighlightEntry(entry);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function validateHighlightsInput(highlights, highlightSquares) {
|
|
265
|
+
if (highlights !== void 0 && highlightSquares !== void 0) {
|
|
266
|
+
throw new ValidationError("Use either highlights or highlightSquares, not both");
|
|
267
|
+
}
|
|
268
|
+
validateHighlightOptions(highlights);
|
|
269
|
+
validateHighlightOptions(highlightSquares);
|
|
270
|
+
}
|
|
222
271
|
|
|
223
272
|
// src/core/parsers.ts
|
|
224
273
|
var PIECE_SYMBOL_TO_KEY = {
|
|
@@ -282,11 +331,6 @@ function parseBoardArray(board) {
|
|
|
282
331
|
return position;
|
|
283
332
|
}
|
|
284
333
|
|
|
285
|
-
// src/core/highlights.ts
|
|
286
|
-
function normalizeHighlights(input) {
|
|
287
|
-
return [...new Set(input.map(validateSquare))].sort();
|
|
288
|
-
}
|
|
289
|
-
|
|
290
334
|
// src/render/canvas-renderer.ts
|
|
291
335
|
var import_canvas3 = require("canvas");
|
|
292
336
|
|
|
@@ -345,10 +389,10 @@ function createBoardGeometry({
|
|
|
345
389
|
const squareGeometry = squares[square];
|
|
346
390
|
return {
|
|
347
391
|
text: file,
|
|
348
|
-
x: squareGeometry.x + insideFileInsetX,
|
|
392
|
+
x: squareGeometry.x + squareGeometry.size - insideFileInsetX,
|
|
349
393
|
y: squareGeometry.y + squareGeometry.size - insideFileInsetY,
|
|
350
394
|
square,
|
|
351
|
-
textAlign: "
|
|
395
|
+
textAlign: "right",
|
|
352
396
|
textBaseline: "bottom"
|
|
353
397
|
};
|
|
354
398
|
});
|
|
@@ -512,6 +556,19 @@ function resolveInsideLabelColor(request, square) {
|
|
|
512
556
|
}
|
|
513
557
|
return isDarkSquare(square) ? INSIDE_LIGHT_LABEL_COLOR : INSIDE_DARK_LABEL_COLOR;
|
|
514
558
|
}
|
|
559
|
+
function resolveHighlightOpacity(style, color, opacity) {
|
|
560
|
+
if (opacity !== void 0) {
|
|
561
|
+
return opacity;
|
|
562
|
+
}
|
|
563
|
+
if (style === "circle" || color !== void 0) {
|
|
564
|
+
return 0.9;
|
|
565
|
+
}
|
|
566
|
+
return 1;
|
|
567
|
+
}
|
|
568
|
+
function resolveCircleLineWidth(squareSize, lineWidth) {
|
|
569
|
+
const candidate = lineWidth ?? squareSize * 0.08;
|
|
570
|
+
return Math.max(2, Math.min(8, candidate));
|
|
571
|
+
}
|
|
515
572
|
function resolveBorderCoordinateFontSize(context, geometry) {
|
|
516
573
|
const maxFontSize = Math.floor(
|
|
517
574
|
Math.min(geometry.squareSize * 0.6, geometry.borderSize * 0.65)
|
|
@@ -608,6 +665,93 @@ function drawCoordinates(context, request, geometry) {
|
|
|
608
665
|
}
|
|
609
666
|
drawInsideCoordinates(context, request, geometry);
|
|
610
667
|
}
|
|
668
|
+
function drawBoardSquares(context, request, geometry) {
|
|
669
|
+
for (const square of SQUARES) {
|
|
670
|
+
const squareGeometry = geometry.squares[square];
|
|
671
|
+
context.fillStyle = isDarkSquare(square) ? request.colors.darkSquare : request.colors.lightSquare;
|
|
672
|
+
context.fillRect(
|
|
673
|
+
squareGeometry.x,
|
|
674
|
+
squareGeometry.y,
|
|
675
|
+
squareGeometry.size,
|
|
676
|
+
squareGeometry.size
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function drawFillHighlights(context, request, geometry) {
|
|
681
|
+
for (const highlight of request.highlights) {
|
|
682
|
+
if (highlight.style !== "fill") {
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
const squareGeometry = geometry.squares[highlight.square];
|
|
686
|
+
context.save();
|
|
687
|
+
context.globalAlpha = resolveHighlightOpacity(
|
|
688
|
+
highlight.style,
|
|
689
|
+
highlight.color,
|
|
690
|
+
highlight.opacity
|
|
691
|
+
);
|
|
692
|
+
context.fillStyle = highlight.color ?? request.colors.highlight;
|
|
693
|
+
context.fillRect(
|
|
694
|
+
squareGeometry.x,
|
|
695
|
+
squareGeometry.y,
|
|
696
|
+
squareGeometry.size,
|
|
697
|
+
squareGeometry.size
|
|
698
|
+
);
|
|
699
|
+
context.restore();
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
function drawCircleHighlights(context, request, geometry) {
|
|
703
|
+
for (const highlight of request.highlights) {
|
|
704
|
+
if (highlight.style !== "circle") {
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
const squareGeometry = geometry.squares[highlight.square];
|
|
708
|
+
const centerX = squareGeometry.x + squareGeometry.size / 2;
|
|
709
|
+
const centerY = squareGeometry.y + squareGeometry.size / 2;
|
|
710
|
+
context.save();
|
|
711
|
+
context.globalAlpha = resolveHighlightOpacity(
|
|
712
|
+
highlight.style,
|
|
713
|
+
highlight.color,
|
|
714
|
+
highlight.opacity
|
|
715
|
+
);
|
|
716
|
+
context.strokeStyle = highlight.color ?? "#ffcc00";
|
|
717
|
+
context.lineWidth = resolveCircleLineWidth(
|
|
718
|
+
squareGeometry.size,
|
|
719
|
+
highlight.lineWidth
|
|
720
|
+
);
|
|
721
|
+
context.beginPath();
|
|
722
|
+
context.arc(
|
|
723
|
+
centerX,
|
|
724
|
+
centerY,
|
|
725
|
+
squareGeometry.size * 0.32,
|
|
726
|
+
0,
|
|
727
|
+
Math.PI * 2
|
|
728
|
+
);
|
|
729
|
+
context.stroke();
|
|
730
|
+
context.restore();
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
async function drawPieces(context, request, geometry) {
|
|
734
|
+
for (const square of SQUARES) {
|
|
735
|
+
const squareGeometry = geometry.squares[square];
|
|
736
|
+
const pieceKey = request.board.squares[square];
|
|
737
|
+
if (!pieceKey) {
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
const raster = await getPieceRaster(
|
|
741
|
+
request.theme.name,
|
|
742
|
+
pieceKey,
|
|
743
|
+
request.theme.pieces[pieceKey],
|
|
744
|
+
Math.round(geometry.squareSize)
|
|
745
|
+
);
|
|
746
|
+
context.drawImage(
|
|
747
|
+
raster,
|
|
748
|
+
squareGeometry.x,
|
|
749
|
+
squareGeometry.y,
|
|
750
|
+
squareGeometry.size,
|
|
751
|
+
squareGeometry.size
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
611
755
|
var CanvasPngRenderer = class {
|
|
612
756
|
async render(request) {
|
|
613
757
|
try {
|
|
@@ -621,43 +765,11 @@ var CanvasPngRenderer = class {
|
|
|
621
765
|
const context = canvas.getContext("2d");
|
|
622
766
|
context.fillStyle = request.colors.lightSquare;
|
|
623
767
|
context.fillRect(0, 0, geometry.imageWidth, geometry.imageHeight);
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
context.fillRect(
|
|
628
|
-
squareGeometry.x,
|
|
629
|
-
squareGeometry.y,
|
|
630
|
-
squareGeometry.size,
|
|
631
|
-
squareGeometry.size
|
|
632
|
-
);
|
|
633
|
-
if (request.highlights.includes(square)) {
|
|
634
|
-
context.fillStyle = request.colors.highlight;
|
|
635
|
-
context.fillRect(
|
|
636
|
-
squareGeometry.x,
|
|
637
|
-
squareGeometry.y,
|
|
638
|
-
squareGeometry.size,
|
|
639
|
-
squareGeometry.size
|
|
640
|
-
);
|
|
641
|
-
}
|
|
642
|
-
const pieceKey = request.board.squares[square];
|
|
643
|
-
if (!pieceKey) {
|
|
644
|
-
continue;
|
|
645
|
-
}
|
|
646
|
-
const raster = await getPieceRaster(
|
|
647
|
-
request.theme.name,
|
|
648
|
-
pieceKey,
|
|
649
|
-
request.theme.pieces[pieceKey],
|
|
650
|
-
Math.round(geometry.squareSize)
|
|
651
|
-
);
|
|
652
|
-
context.drawImage(
|
|
653
|
-
raster,
|
|
654
|
-
squareGeometry.x,
|
|
655
|
-
squareGeometry.y,
|
|
656
|
-
squareGeometry.size,
|
|
657
|
-
squareGeometry.size
|
|
658
|
-
);
|
|
659
|
-
}
|
|
768
|
+
drawBoardSquares(context, request, geometry);
|
|
769
|
+
drawFillHighlights(context, request, geometry);
|
|
770
|
+
drawCircleHighlights(context, request, geometry);
|
|
660
771
|
drawCoordinates(context, request, geometry);
|
|
772
|
+
await drawPieces(context, request, geometry);
|
|
661
773
|
return canvas.toBuffer("image/png");
|
|
662
774
|
} catch (error) {
|
|
663
775
|
if (error instanceof RenderError) {
|
|
@@ -678,6 +790,26 @@ async function writeBufferToFile(filePath, buffer) {
|
|
|
678
790
|
}
|
|
679
791
|
}
|
|
680
792
|
|
|
793
|
+
// src/core/highlights.ts
|
|
794
|
+
function normalizeHighlightEntries(input) {
|
|
795
|
+
return input.map((entry) => {
|
|
796
|
+
if (typeof entry === "string") {
|
|
797
|
+
return {
|
|
798
|
+
square: validateSquare(entry),
|
|
799
|
+
style: "fill"
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
const style = entry.style ?? "fill";
|
|
803
|
+
return {
|
|
804
|
+
square: validateSquare(entry.square),
|
|
805
|
+
style,
|
|
806
|
+
color: entry.color ?? (style === "circle" ? "#ffcc00" : void 0),
|
|
807
|
+
opacity: entry.opacity ?? (style === "circle" ? 0.9 : void 0),
|
|
808
|
+
lineWidth: entry.lineWidth
|
|
809
|
+
};
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
681
813
|
// src/themes/builtins.ts
|
|
682
814
|
var import_node_fs = require("fs");
|
|
683
815
|
var import_node_path = require("path");
|
|
@@ -863,6 +995,9 @@ function normalizeCoordinates(coordinates, borderSize) {
|
|
|
863
995
|
color: coordinates.color ?? (position === "border" ? "#333" : void 0)
|
|
864
996
|
};
|
|
865
997
|
}
|
|
998
|
+
function normalizeHighlightEntries2(highlights) {
|
|
999
|
+
return normalizeHighlightEntries(highlights ?? []);
|
|
1000
|
+
}
|
|
866
1001
|
function normalizeRenderInputs(options) {
|
|
867
1002
|
const size = validateSize(options.size ?? DEFAULT_SIZE);
|
|
868
1003
|
const borderSize = validateBorderSize(
|
|
@@ -871,6 +1006,8 @@ function normalizeRenderInputs(options) {
|
|
|
871
1006
|
);
|
|
872
1007
|
validateBoardColors(options.colors);
|
|
873
1008
|
validateCoordinatesOption(options.coordinates, borderSize);
|
|
1009
|
+
validateHighlightsInput(options.highlights, options.highlightSquares);
|
|
1010
|
+
const highlightInput = options.highlights ?? options.highlightSquares;
|
|
874
1011
|
return {
|
|
875
1012
|
size,
|
|
876
1013
|
padding: normalizePadding(options.padding ?? DEFAULT_PADDING),
|
|
@@ -880,7 +1017,7 @@ function normalizeRenderInputs(options) {
|
|
|
880
1017
|
theme: options.theme,
|
|
881
1018
|
style: options.style
|
|
882
1019
|
}),
|
|
883
|
-
|
|
1020
|
+
highlights: normalizeHighlightEntries2(highlightInput),
|
|
884
1021
|
colors: normalizeColors(options.colors),
|
|
885
1022
|
coordinates: normalizeCoordinates(options.coordinates, borderSize)
|
|
886
1023
|
};
|
|
@@ -893,10 +1030,7 @@ var ChessImageGenerator = class {
|
|
|
893
1030
|
highlights = [];
|
|
894
1031
|
constructor(options = {}) {
|
|
895
1032
|
this.defaults = { ...options };
|
|
896
|
-
normalizeRenderInputs(
|
|
897
|
-
...this.defaults,
|
|
898
|
-
highlightSquares: []
|
|
899
|
-
});
|
|
1033
|
+
normalizeRenderInputs(this.defaults);
|
|
900
1034
|
}
|
|
901
1035
|
async loadFEN(fen) {
|
|
902
1036
|
this.position = parseFEN(fen);
|
|
@@ -910,8 +1044,9 @@ var ChessImageGenerator = class {
|
|
|
910
1044
|
this.position = parseBoardArray(board);
|
|
911
1045
|
this.clearHighlights();
|
|
912
1046
|
}
|
|
913
|
-
setHighlights(
|
|
914
|
-
|
|
1047
|
+
setHighlights(highlights) {
|
|
1048
|
+
validateHighlightsInput(highlights, void 0);
|
|
1049
|
+
this.highlights = [...highlights];
|
|
915
1050
|
}
|
|
916
1051
|
clearHighlights() {
|
|
917
1052
|
this.highlights = [];
|
|
@@ -923,12 +1058,13 @@ var ChessImageGenerator = class {
|
|
|
923
1058
|
const renderer = new CanvasPngRenderer();
|
|
924
1059
|
const normalized = normalizeRenderInputs({
|
|
925
1060
|
...this.defaults,
|
|
926
|
-
|
|
1061
|
+
highlights: this.highlights,
|
|
1062
|
+
highlightSquares: void 0
|
|
927
1063
|
});
|
|
928
1064
|
return renderer.render({
|
|
929
1065
|
board: this.position,
|
|
930
1066
|
theme: normalized.theme,
|
|
931
|
-
highlights: normalized.
|
|
1067
|
+
highlights: normalized.highlights,
|
|
932
1068
|
size: normalized.size,
|
|
933
1069
|
padding: normalized.padding,
|
|
934
1070
|
borderSize: normalized.borderSize,
|
|
@@ -971,7 +1107,7 @@ async function renderChess(options) {
|
|
|
971
1107
|
return renderer.render({
|
|
972
1108
|
board: position,
|
|
973
1109
|
theme: normalized.theme,
|
|
974
|
-
highlights: normalized.
|
|
1110
|
+
highlights: normalized.highlights,
|
|
975
1111
|
size: normalized.size,
|
|
976
1112
|
padding: normalized.padding,
|
|
977
1113
|
borderSize: normalized.borderSize,
|