chess2img 0.2.2 → 0.3.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 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,52 @@ 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
+ if (entry.radius !== void 0 && (!Number.isFinite(entry.radius) || entry.radius <= 0 || entry.radius > 0.5)) {
253
+ throw new ValidationError("highlight.radius must be a finite number greater than 0 and at most 0.5");
254
+ }
255
+ }
256
+ function validateHighlightOptions(highlights) {
257
+ if (highlights === void 0) {
258
+ return;
259
+ }
260
+ if (!Array.isArray(highlights)) {
261
+ throw new ValidationError("highlights must be an array");
262
+ }
263
+ for (const entry of highlights) {
264
+ validateHighlightEntry(entry);
265
+ }
266
+ }
267
+ function validateHighlightsInput(highlights, highlightSquares) {
268
+ if (highlights !== void 0 && highlightSquares !== void 0) {
269
+ throw new ValidationError("Use either highlights or highlightSquares, not both");
270
+ }
271
+ validateHighlightOptions(highlights);
272
+ validateHighlightOptions(highlightSquares);
273
+ }
222
274
 
223
275
  // src/core/parsers.ts
224
276
  var PIECE_SYMBOL_TO_KEY = {
@@ -282,11 +334,6 @@ function parseBoardArray(board) {
282
334
  return position;
283
335
  }
284
336
 
285
- // src/core/highlights.ts
286
- function normalizeHighlights(input) {
287
- return [...new Set(input.map(validateSquare))].sort();
288
- }
289
-
290
337
  // src/render/canvas-renderer.ts
291
338
  var import_canvas3 = require("canvas");
292
339
 
@@ -512,6 +559,23 @@ function resolveInsideLabelColor(request, square) {
512
559
  }
513
560
  return isDarkSquare(square) ? INSIDE_LIGHT_LABEL_COLOR : INSIDE_DARK_LABEL_COLOR;
514
561
  }
562
+ function resolveHighlightOpacity(style, color, opacity) {
563
+ if (opacity !== void 0) {
564
+ return opacity;
565
+ }
566
+ if (style === "circle" || color !== void 0) {
567
+ return 0.9;
568
+ }
569
+ return 1;
570
+ }
571
+ function resolveCircleLineWidth(squareSize, lineWidth) {
572
+ const candidate = lineWidth ?? squareSize * 0.08;
573
+ return Math.max(2, Math.min(8, candidate));
574
+ }
575
+ function resolveCircleRadius(squareSize, radius, lineWidth) {
576
+ const radiusPx = squareSize * (radius ?? 0.42);
577
+ return Math.max(0, radiusPx - lineWidth / 2);
578
+ }
515
579
  function resolveBorderCoordinateFontSize(context, geometry) {
516
580
  const maxFontSize = Math.floor(
517
581
  Math.min(geometry.squareSize * 0.6, geometry.borderSize * 0.65)
@@ -608,6 +672,98 @@ function drawCoordinates(context, request, geometry) {
608
672
  }
609
673
  drawInsideCoordinates(context, request, geometry);
610
674
  }
675
+ function drawBoardSquares(context, request, geometry) {
676
+ for (const square of SQUARES) {
677
+ const squareGeometry = geometry.squares[square];
678
+ context.fillStyle = isDarkSquare(square) ? request.colors.darkSquare : request.colors.lightSquare;
679
+ context.fillRect(
680
+ squareGeometry.x,
681
+ squareGeometry.y,
682
+ squareGeometry.size,
683
+ squareGeometry.size
684
+ );
685
+ }
686
+ }
687
+ function drawFillHighlights(context, request, geometry) {
688
+ for (const highlight of request.highlights) {
689
+ if (highlight.style !== "fill") {
690
+ continue;
691
+ }
692
+ const squareGeometry = geometry.squares[highlight.square];
693
+ context.save();
694
+ context.globalAlpha = resolveHighlightOpacity(
695
+ highlight.style,
696
+ highlight.color,
697
+ highlight.opacity
698
+ );
699
+ context.fillStyle = highlight.color ?? request.colors.highlight;
700
+ context.fillRect(
701
+ squareGeometry.x,
702
+ squareGeometry.y,
703
+ squareGeometry.size,
704
+ squareGeometry.size
705
+ );
706
+ context.restore();
707
+ }
708
+ }
709
+ function drawCircleHighlights(context, request, geometry) {
710
+ for (const highlight of request.highlights) {
711
+ if (highlight.style !== "circle") {
712
+ continue;
713
+ }
714
+ const squareGeometry = geometry.squares[highlight.square];
715
+ const centerX = squareGeometry.x + squareGeometry.size / 2;
716
+ const centerY = squareGeometry.y + squareGeometry.size / 2;
717
+ context.save();
718
+ context.globalAlpha = resolveHighlightOpacity(
719
+ highlight.style,
720
+ highlight.color,
721
+ highlight.opacity
722
+ );
723
+ context.strokeStyle = highlight.color ?? "#ffcc00";
724
+ context.lineWidth = resolveCircleLineWidth(
725
+ squareGeometry.size,
726
+ highlight.lineWidth
727
+ );
728
+ const radius = resolveCircleRadius(
729
+ squareGeometry.size,
730
+ highlight.radius,
731
+ context.lineWidth
732
+ );
733
+ context.beginPath();
734
+ context.arc(
735
+ centerX,
736
+ centerY,
737
+ radius,
738
+ 0,
739
+ Math.PI * 2
740
+ );
741
+ context.stroke();
742
+ context.restore();
743
+ }
744
+ }
745
+ async function drawPieces(context, request, geometry) {
746
+ for (const square of SQUARES) {
747
+ const squareGeometry = geometry.squares[square];
748
+ const pieceKey = request.board.squares[square];
749
+ if (!pieceKey) {
750
+ continue;
751
+ }
752
+ const raster = await getPieceRaster(
753
+ request.theme.name,
754
+ pieceKey,
755
+ request.theme.pieces[pieceKey],
756
+ Math.round(geometry.squareSize)
757
+ );
758
+ context.drawImage(
759
+ raster,
760
+ squareGeometry.x,
761
+ squareGeometry.y,
762
+ squareGeometry.size,
763
+ squareGeometry.size
764
+ );
765
+ }
766
+ }
611
767
  var CanvasPngRenderer = class {
612
768
  async render(request) {
613
769
  try {
@@ -621,43 +777,11 @@ var CanvasPngRenderer = class {
621
777
  const context = canvas.getContext("2d");
622
778
  context.fillStyle = request.colors.lightSquare;
623
779
  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
- }
780
+ drawBoardSquares(context, request, geometry);
781
+ drawFillHighlights(context, request, geometry);
782
+ drawCircleHighlights(context, request, geometry);
660
783
  drawCoordinates(context, request, geometry);
784
+ await drawPieces(context, request, geometry);
661
785
  return canvas.toBuffer("image/png");
662
786
  } catch (error) {
663
787
  if (error instanceof RenderError) {
@@ -678,6 +802,31 @@ async function writeBufferToFile(filePath, buffer) {
678
802
  }
679
803
  }
680
804
 
805
+ // src/core/highlights.ts
806
+ function normalizeHighlightEntries(input) {
807
+ return input.map((entry) => {
808
+ if (typeof entry === "string") {
809
+ return {
810
+ square: validateSquare(entry),
811
+ style: "fill",
812
+ color: void 0,
813
+ opacity: void 0,
814
+ lineWidth: void 0,
815
+ radius: void 0
816
+ };
817
+ }
818
+ const style = entry.style ?? "fill";
819
+ return {
820
+ square: validateSquare(entry.square),
821
+ style,
822
+ color: entry.color ?? (style === "circle" ? "#ffcc00" : void 0),
823
+ opacity: entry.opacity ?? (style === "circle" ? 0.9 : void 0),
824
+ lineWidth: entry.lineWidth,
825
+ radius: style === "circle" ? entry.radius ?? 0.42 : void 0
826
+ };
827
+ });
828
+ }
829
+
681
830
  // src/themes/builtins.ts
682
831
  var import_node_fs = require("fs");
683
832
  var import_node_path = require("path");
@@ -863,6 +1012,9 @@ function normalizeCoordinates(coordinates, borderSize) {
863
1012
  color: coordinates.color ?? (position === "border" ? "#333" : void 0)
864
1013
  };
865
1014
  }
1015
+ function normalizeHighlightEntries2(highlights) {
1016
+ return normalizeHighlightEntries(highlights ?? []);
1017
+ }
866
1018
  function normalizeRenderInputs(options) {
867
1019
  const size = validateSize(options.size ?? DEFAULT_SIZE);
868
1020
  const borderSize = validateBorderSize(
@@ -871,6 +1023,8 @@ function normalizeRenderInputs(options) {
871
1023
  );
872
1024
  validateBoardColors(options.colors);
873
1025
  validateCoordinatesOption(options.coordinates, borderSize);
1026
+ validateHighlightsInput(options.highlights, options.highlightSquares);
1027
+ const highlightInput = options.highlights ?? options.highlightSquares;
874
1028
  return {
875
1029
  size,
876
1030
  padding: normalizePadding(options.padding ?? DEFAULT_PADDING),
@@ -880,7 +1034,7 @@ function normalizeRenderInputs(options) {
880
1034
  theme: options.theme,
881
1035
  style: options.style
882
1036
  }),
883
- highlightSquares: normalizeHighlights(options.highlightSquares ?? []),
1037
+ highlights: normalizeHighlightEntries2(highlightInput),
884
1038
  colors: normalizeColors(options.colors),
885
1039
  coordinates: normalizeCoordinates(options.coordinates, borderSize)
886
1040
  };
@@ -893,10 +1047,7 @@ var ChessImageGenerator = class {
893
1047
  highlights = [];
894
1048
  constructor(options = {}) {
895
1049
  this.defaults = { ...options };
896
- normalizeRenderInputs({
897
- ...this.defaults,
898
- highlightSquares: []
899
- });
1050
+ normalizeRenderInputs(this.defaults);
900
1051
  }
901
1052
  async loadFEN(fen) {
902
1053
  this.position = parseFEN(fen);
@@ -910,8 +1061,9 @@ var ChessImageGenerator = class {
910
1061
  this.position = parseBoardArray(board);
911
1062
  this.clearHighlights();
912
1063
  }
913
- setHighlights(squares) {
914
- this.highlights = normalizeHighlights(squares);
1064
+ setHighlights(highlights) {
1065
+ validateHighlightsInput(highlights, void 0);
1066
+ this.highlights = [...highlights];
915
1067
  }
916
1068
  clearHighlights() {
917
1069
  this.highlights = [];
@@ -923,12 +1075,13 @@ var ChessImageGenerator = class {
923
1075
  const renderer = new CanvasPngRenderer();
924
1076
  const normalized = normalizeRenderInputs({
925
1077
  ...this.defaults,
926
- highlightSquares: this.highlights
1078
+ highlights: this.highlights,
1079
+ highlightSquares: void 0
927
1080
  });
928
1081
  return renderer.render({
929
1082
  board: this.position,
930
1083
  theme: normalized.theme,
931
- highlights: normalized.highlightSquares,
1084
+ highlights: normalized.highlights,
932
1085
  size: normalized.size,
933
1086
  padding: normalized.padding,
934
1087
  borderSize: normalized.borderSize,
@@ -971,7 +1124,7 @@ async function renderChess(options) {
971
1124
  return renderer.render({
972
1125
  board: position,
973
1126
  theme: normalized.theme,
974
- highlights: normalized.highlightSquares,
1127
+ highlights: normalized.highlights,
975
1128
  size: normalized.size,
976
1129
  padding: normalized.padding,
977
1130
  borderSize: normalized.borderSize,