chess2img 0.2.2 → 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 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
- highlightSquares: ["e2", "e4"],
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(squares: Square[]): void`
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
- - `highlightSquares`: array of algebraic squares such as `["e4", "d5"]`
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
 
@@ -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
- for (const square of SQUARES) {
625
- const squareGeometry = geometry.squares[square];
626
- context.fillStyle = isDarkSquare(square) ? request.colors.darkSquare : request.colors.lightSquare;
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
- highlightSquares: normalizeHighlights(options.highlightSquares ?? []),
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(squares) {
914
- this.highlights = normalizeHighlights(squares);
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
- highlightSquares: this.highlights
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.highlightSquares,
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.highlightSquares,
1110
+ highlights: normalized.highlights,
975
1111
  size: normalized.size,
976
1112
  padding: normalized.padding,
977
1113
  borderSize: normalized.borderSize,