chess2img 0.2.0 → 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/dist/index.d.cts CHANGED
@@ -51,10 +51,13 @@ interface BoardColors {
51
51
  darkSquare?: string;
52
52
  highlight?: string;
53
53
  }
54
+ type CoordinatesPosition = "border" | "inside";
54
55
  interface CoordinatesOptions {
55
56
  enabled?: boolean;
57
+ position?: CoordinatesPosition;
56
58
  color?: string;
57
59
  }
60
+ type CoordinatesInput = boolean | CoordinatesPosition | CoordinatesOptions;
58
61
  interface RenderOptions {
59
62
  size?: number;
60
63
  padding?: Padding;
@@ -64,7 +67,7 @@ interface RenderOptions {
64
67
  theme?: string | ThemeDefinition;
65
68
  highlightSquares?: Square[];
66
69
  colors?: BoardColors;
67
- coordinates?: boolean | CoordinatesOptions;
70
+ coordinates?: CoordinatesInput;
68
71
  }
69
72
  interface ChessImageGeneratorOptions extends RenderOptions {
70
73
  }
@@ -75,7 +78,8 @@ interface ResolvedColors {
75
78
  }
76
79
  interface ResolvedCoordinates {
77
80
  enabled: boolean;
78
- color: string;
81
+ position: CoordinatesPosition;
82
+ color?: string;
79
83
  }
80
84
  interface ResolvedRenderOptions {
81
85
  size: number;
@@ -122,4 +126,4 @@ declare function renderChess(options: RenderChessOptions): Promise<Buffer>;
122
126
 
123
127
  declare function registerTheme(theme: ThemeDefinition): ThemeDefinition;
124
128
 
125
- export { type BoardArray, type BoardCell, type BoardColors, ChessImageGenerator, type ChessImageGeneratorOptions, type CoordinatesOptions, IOError, type Padding, ParseError, type PieceKey, type PieceStyle, type PngAssetSource, type RenderChessOptions, RenderError, type RenderOptions, type ResolvedColors, type ResolvedCoordinates, type ResolvedRenderOptions, type Square, type SvgAssetSource, type ThemeAssetSource, type ThemeDefinition, ThemeError, ValidationError, registerTheme, renderChess };
129
+ export { type BoardArray, type BoardCell, type BoardColors, ChessImageGenerator, type ChessImageGeneratorOptions, type CoordinatesInput, type CoordinatesOptions, type CoordinatesPosition, IOError, type Padding, ParseError, type PieceKey, type PieceStyle, type PngAssetSource, type RenderChessOptions, RenderError, type RenderOptions, type ResolvedColors, type ResolvedCoordinates, type ResolvedRenderOptions, type Square, type SvgAssetSource, type ThemeAssetSource, type ThemeDefinition, ThemeError, ValidationError, registerTheme, renderChess };
package/dist/index.d.ts CHANGED
@@ -51,10 +51,13 @@ interface BoardColors {
51
51
  darkSquare?: string;
52
52
  highlight?: string;
53
53
  }
54
+ type CoordinatesPosition = "border" | "inside";
54
55
  interface CoordinatesOptions {
55
56
  enabled?: boolean;
57
+ position?: CoordinatesPosition;
56
58
  color?: string;
57
59
  }
60
+ type CoordinatesInput = boolean | CoordinatesPosition | CoordinatesOptions;
58
61
  interface RenderOptions {
59
62
  size?: number;
60
63
  padding?: Padding;
@@ -64,7 +67,7 @@ interface RenderOptions {
64
67
  theme?: string | ThemeDefinition;
65
68
  highlightSquares?: Square[];
66
69
  colors?: BoardColors;
67
- coordinates?: boolean | CoordinatesOptions;
70
+ coordinates?: CoordinatesInput;
68
71
  }
69
72
  interface ChessImageGeneratorOptions extends RenderOptions {
70
73
  }
@@ -75,7 +78,8 @@ interface ResolvedColors {
75
78
  }
76
79
  interface ResolvedCoordinates {
77
80
  enabled: boolean;
78
- color: string;
81
+ position: CoordinatesPosition;
82
+ color?: string;
79
83
  }
80
84
  interface ResolvedRenderOptions {
81
85
  size: number;
@@ -122,4 +126,4 @@ declare function renderChess(options: RenderChessOptions): Promise<Buffer>;
122
126
 
123
127
  declare function registerTheme(theme: ThemeDefinition): ThemeDefinition;
124
128
 
125
- export { type BoardArray, type BoardCell, type BoardColors, ChessImageGenerator, type ChessImageGeneratorOptions, type CoordinatesOptions, IOError, type Padding, ParseError, type PieceKey, type PieceStyle, type PngAssetSource, type RenderChessOptions, RenderError, type RenderOptions, type ResolvedColors, type ResolvedCoordinates, type ResolvedRenderOptions, type Square, type SvgAssetSource, type ThemeAssetSource, type ThemeDefinition, ThemeError, ValidationError, registerTheme, renderChess };
129
+ export { type BoardArray, type BoardCell, type BoardColors, ChessImageGenerator, type ChessImageGeneratorOptions, type CoordinatesInput, type CoordinatesOptions, type CoordinatesPosition, IOError, type Padding, ParseError, type PieceKey, type PieceStyle, type PngAssetSource, type RenderChessOptions, RenderError, type RenderOptions, type ResolvedColors, type ResolvedCoordinates, type ResolvedRenderOptions, type Square, type SvgAssetSource, type ThemeAssetSource, type ThemeDefinition, ThemeError, ValidationError, registerTheme, renderChess };
package/dist/index.js CHANGED
@@ -158,16 +158,37 @@ function validateBoardColors(colors) {
158
158
  function isCoordinatesOptions(value) {
159
159
  return typeof value === "object" && value !== null && !Array.isArray(value);
160
160
  }
161
- function validateCoordinatesOption(coordinates) {
161
+ function isCoordinatesPosition(value) {
162
+ return value === "border" || value === "inside";
163
+ }
164
+ function validateCoordinatesOption(coordinates, borderSize) {
162
165
  if (coordinates === void 0 || typeof coordinates === "boolean") {
163
166
  return;
164
167
  }
168
+ if (isCoordinatesPosition(coordinates)) {
169
+ if (coordinates === "border" && borderSize === 0) {
170
+ throw new ValidationError(
171
+ "coordinates position 'border' requires borderSize > 0"
172
+ );
173
+ }
174
+ return;
175
+ }
165
176
  if (!isCoordinatesOptions(coordinates)) {
166
- throw new ValidationError("coordinates must be a boolean or an options object");
177
+ throw new ValidationError(
178
+ "coordinates must be false, true, 'border', 'inside', or an options object"
179
+ );
167
180
  }
168
181
  if (coordinates.enabled !== void 0 && typeof coordinates.enabled !== "boolean") {
169
182
  throw new ValidationError("coordinates.enabled must be a boolean");
170
183
  }
184
+ if (coordinates.position !== void 0 && !isCoordinatesPosition(coordinates.position)) {
185
+ throw new ValidationError("coordinates.position must be 'border' or 'inside'");
186
+ }
187
+ if (coordinates.enabled !== false && coordinates.position === "border" && borderSize === 0) {
188
+ throw new ValidationError(
189
+ "coordinates position 'border' requires borderSize > 0"
190
+ );
191
+ }
171
192
  if (coordinates.color !== void 0) {
172
193
  validateColorString(coordinates.color, "coordinates.color");
173
194
  }
@@ -269,16 +290,54 @@ function createBoardGeometry({
269
290
  );
270
291
  const displayedFiles = flipped ? [...FILES].reverse() : [...FILES];
271
292
  const displayedRanks = flipped ? [...RANKS].reverse() : [...RANKS];
272
- const fileLabels = borderSize ? displayedFiles.map((file, fileIndex) => ({
293
+ const bottomEdgeRank = flipped ? "8" : "1";
294
+ const leftEdgeFile = flipped ? "h" : "a";
295
+ const borderFileLabels = borderSize ? displayedFiles.map((file, fileIndex) => ({
273
296
  text: file,
274
297
  x: boardX + fileIndex * squareSize + squareSize / 2,
275
- y: boardOuterY + boardOuterSize - borderSize / 2
298
+ y: boardOuterY + boardOuterSize - borderSize / 2,
299
+ square: `${file}${bottomEdgeRank}`,
300
+ textAlign: "center",
301
+ textBaseline: "middle"
276
302
  })) : [];
277
- const rankLabels = borderSize ? displayedRanks.map((rank, rankIndex) => ({
303
+ const borderRankLabels = borderSize ? displayedRanks.map((rank, rankIndex) => ({
278
304
  text: rank,
279
305
  x: boardOuterX + borderSize / 2,
280
- y: boardY + rankIndex * squareSize + squareSize / 2
306
+ y: boardY + rankIndex * squareSize + squareSize / 2,
307
+ square: `${leftEdgeFile}${rank}`,
308
+ textAlign: "center",
309
+ textBaseline: "middle"
281
310
  })) : [];
311
+ const insideFileInsetX = squareSize * 0.14;
312
+ const insideFileInsetY = squareSize * 0.1;
313
+ const insideRankInsetX = squareSize * 0.14;
314
+ const insideRankInsetY = squareSize * 0.1;
315
+ const insideLabelMaxWidth = squareSize * 0.26;
316
+ const insideLabelMaxHeight = squareSize * 0.24;
317
+ const insideFileLabels = displayedFiles.map((file) => {
318
+ const square = `${file}${bottomEdgeRank}`;
319
+ const squareGeometry = squares[square];
320
+ return {
321
+ text: file,
322
+ x: squareGeometry.x + insideFileInsetX,
323
+ y: squareGeometry.y + squareGeometry.size - insideFileInsetY,
324
+ square,
325
+ textAlign: "left",
326
+ textBaseline: "bottom"
327
+ };
328
+ });
329
+ const insideRankLabels = displayedRanks.map((rank) => {
330
+ const square = `${leftEdgeFile}${rank}`;
331
+ const squareGeometry = squares[square];
332
+ return {
333
+ text: rank,
334
+ x: squareGeometry.x + insideRankInsetX,
335
+ y: squareGeometry.y + insideRankInsetY,
336
+ square,
337
+ textAlign: "left",
338
+ textBaseline: "top"
339
+ };
340
+ });
282
341
  return {
283
342
  imageWidth: left + size + right,
284
343
  imageHeight: top + size + bottom,
@@ -290,8 +349,16 @@ function createBoardGeometry({
290
349
  boardX,
291
350
  boardY,
292
351
  boardSize,
293
- fileLabels,
294
- rankLabels,
352
+ borderFileLabels,
353
+ borderRankLabels,
354
+ insideFileLabels,
355
+ insideRankLabels,
356
+ insideFileInsetX,
357
+ insideFileInsetY,
358
+ insideRankInsetX,
359
+ insideRankInsetY,
360
+ insideLabelMaxWidth,
361
+ insideLabelMaxHeight,
295
362
  squares
296
363
  };
297
364
  }
@@ -395,6 +462,9 @@ var MIN_COORDINATE_FONT_SIZE = 8;
395
462
  var MAX_FILE_LABEL_WIDTH_RATIO = 0.75;
396
463
  var MAX_RANK_LABEL_WIDTH_RATIO = 0.7;
397
464
  var MAX_LABEL_HEIGHT_RATIO = 0.7;
465
+ var INSIDE_COORDINATE_MAX_FONT_RATIO = 0.34;
466
+ var INSIDE_LIGHT_LABEL_COLOR = "rgba(255,255,255,0.6)";
467
+ var INSIDE_DARK_LABEL_COLOR = "rgba(0,0,0,0.45)";
398
468
  function isDarkSquare(square) {
399
469
  const fileIndex = square.charCodeAt(0) - 97;
400
470
  const rankNumber = Number(square[1]);
@@ -410,22 +480,25 @@ async function getPieceRaster(themeName, pieceKey, asset, squareSize) {
410
480
  pieceRasterCache.set(cacheKey, raster);
411
481
  return raster;
412
482
  }
413
- function drawCoordinates(context, request, geometry) {
414
- if (!request.coordinates.enabled || geometry.borderSize === 0) {
415
- return;
483
+ function resolveInsideLabelColor(request, square) {
484
+ if (request.coordinates.color) {
485
+ return request.coordinates.color;
416
486
  }
487
+ return isDarkSquare(square) ? INSIDE_LIGHT_LABEL_COLOR : INSIDE_DARK_LABEL_COLOR;
488
+ }
489
+ function resolveBorderCoordinateFontSize(context, geometry) {
417
490
  const maxFontSize = Math.floor(
418
491
  Math.min(geometry.squareSize * 0.6, geometry.borderSize * 0.65)
419
492
  );
420
493
  let fontSize = null;
421
494
  for (let candidate = maxFontSize; candidate >= MIN_COORDINATE_FONT_SIZE; candidate -= 1) {
422
495
  context.font = `${candidate}px sans-serif`;
423
- const filesFit = geometry.fileLabels.every((label) => {
496
+ const filesFit = geometry.borderFileLabels.every((label) => {
424
497
  const metrics = context.measureText(label.text);
425
498
  const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
426
499
  return metrics.width <= geometry.squareSize * MAX_FILE_LABEL_WIDTH_RATIO && textHeight <= geometry.borderSize * MAX_LABEL_HEIGHT_RATIO;
427
500
  });
428
- const ranksFit = geometry.rankLabels.every((label) => {
501
+ const ranksFit = geometry.borderRankLabels.every((label) => {
429
502
  const metrics = context.measureText(label.text);
430
503
  const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
431
504
  return metrics.width <= geometry.borderSize * MAX_RANK_LABEL_WIDTH_RATIO && textHeight <= geometry.squareSize * MAX_LABEL_HEIGHT_RATIO;
@@ -435,20 +508,80 @@ function drawCoordinates(context, request, geometry) {
435
508
  break;
436
509
  }
437
510
  }
511
+ return fontSize;
512
+ }
513
+ function resolveInsideCoordinateFontSize(context, geometry) {
514
+ const maxFontSize = Math.floor(
515
+ geometry.squareSize * INSIDE_COORDINATE_MAX_FONT_RATIO
516
+ );
517
+ let fontSize = null;
518
+ for (let candidate = maxFontSize; candidate >= MIN_COORDINATE_FONT_SIZE; candidate -= 1) {
519
+ context.font = `${candidate}px sans-serif`;
520
+ const filesFit = geometry.insideFileLabels.every((label) => {
521
+ const metrics = context.measureText(label.text);
522
+ const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
523
+ return metrics.width <= geometry.insideLabelMaxWidth && textHeight <= geometry.insideLabelMaxHeight;
524
+ });
525
+ const ranksFit = geometry.insideRankLabels.every((label) => {
526
+ const metrics = context.measureText(label.text);
527
+ const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
528
+ return metrics.width <= geometry.insideLabelMaxWidth && textHeight <= geometry.insideLabelMaxHeight;
529
+ });
530
+ if (filesFit && ranksFit) {
531
+ fontSize = candidate;
532
+ break;
533
+ }
534
+ }
535
+ return fontSize;
536
+ }
537
+ function drawBorderCoordinates(context, request, geometry) {
538
+ if (geometry.borderSize === 0) {
539
+ return;
540
+ }
541
+ const fontSize = resolveBorderCoordinateFontSize(context, geometry);
438
542
  if (fontSize === null) {
439
543
  return;
440
544
  }
441
- context.fillStyle = request.coordinates.color;
545
+ context.fillStyle = request.coordinates.color ?? "#333";
442
546
  context.font = `${fontSize}px sans-serif`;
443
547
  context.textAlign = "center";
444
548
  context.textBaseline = "middle";
445
- for (const label of geometry.fileLabels) {
549
+ for (const label of geometry.borderFileLabels) {
446
550
  context.fillText(label.text, label.x, label.y);
447
551
  }
448
- for (const label of geometry.rankLabels) {
552
+ for (const label of geometry.borderRankLabels) {
449
553
  context.fillText(label.text, label.x, label.y);
450
554
  }
451
555
  }
556
+ function drawInsideCoordinates(context, request, geometry) {
557
+ const fontSize = resolveInsideCoordinateFontSize(context, geometry);
558
+ if (fontSize === null) {
559
+ return;
560
+ }
561
+ context.font = `${fontSize}px sans-serif`;
562
+ for (const label of geometry.insideFileLabels) {
563
+ context.fillStyle = resolveInsideLabelColor(request, label.square);
564
+ context.textAlign = label.textAlign;
565
+ context.textBaseline = label.textBaseline;
566
+ context.fillText(label.text, label.x, label.y);
567
+ }
568
+ for (const label of geometry.insideRankLabels) {
569
+ context.fillStyle = resolveInsideLabelColor(request, label.square);
570
+ context.textAlign = label.textAlign;
571
+ context.textBaseline = label.textBaseline;
572
+ context.fillText(label.text, label.x, label.y);
573
+ }
574
+ }
575
+ function drawCoordinates(context, request, geometry) {
576
+ if (!request.coordinates.enabled) {
577
+ return;
578
+ }
579
+ if (request.coordinates.position === "border") {
580
+ drawBorderCoordinates(context, request, geometry);
581
+ return;
582
+ }
583
+ drawInsideCoordinates(context, request, geometry);
584
+ }
452
585
  var CanvasPngRenderer = class {
453
586
  async render(request) {
454
587
  try {
@@ -669,7 +802,7 @@ var DEFAULT_COLORS = {
669
802
  };
670
803
  var DEFAULT_COORDINATES = {
671
804
  enabled: false,
672
- color: "#333"
805
+ position: "inside"
673
806
  };
674
807
  function normalizeColors(colors) {
675
808
  return {
@@ -678,19 +811,30 @@ function normalizeColors(colors) {
678
811
  highlight: colors?.highlight ?? DEFAULT_COLORS.highlight
679
812
  };
680
813
  }
681
- function normalizeCoordinates(coordinates) {
814
+ function normalizeCoordinates(coordinates, borderSize) {
682
815
  if (coordinates === void 0 || coordinates === false) {
683
816
  return { ...DEFAULT_COORDINATES };
684
817
  }
818
+ const defaultPosition = borderSize > 0 ? "border" : "inside";
685
819
  if (coordinates === true) {
686
820
  return {
687
821
  enabled: true,
688
- color: DEFAULT_COORDINATES.color
822
+ position: defaultPosition,
823
+ color: defaultPosition === "border" ? "#333" : void 0
824
+ };
825
+ }
826
+ if (coordinates === "border" || coordinates === "inside") {
827
+ return {
828
+ enabled: true,
829
+ position: coordinates,
830
+ color: coordinates === "border" ? "#333" : void 0
689
831
  };
690
832
  }
833
+ const position = coordinates.position ?? defaultPosition;
691
834
  return {
692
835
  enabled: coordinates.enabled ?? true,
693
- color: coordinates.color ?? DEFAULT_COORDINATES.color
836
+ position,
837
+ color: coordinates.color ?? (position === "border" ? "#333" : void 0)
694
838
  };
695
839
  }
696
840
  function normalizeRenderInputs(options) {
@@ -700,7 +844,7 @@ function normalizeRenderInputs(options) {
700
844
  size
701
845
  );
702
846
  validateBoardColors(options.colors);
703
- validateCoordinatesOption(options.coordinates);
847
+ validateCoordinatesOption(options.coordinates, borderSize);
704
848
  return {
705
849
  size,
706
850
  padding: normalizePadding(options.padding ?? DEFAULT_PADDING),
@@ -712,7 +856,7 @@ function normalizeRenderInputs(options) {
712
856
  }),
713
857
  highlightSquares: normalizeHighlights(options.highlightSquares ?? []),
714
858
  colors: normalizeColors(options.colors),
715
- coordinates: normalizeCoordinates(options.coordinates)
859
+ coordinates: normalizeCoordinates(options.coordinates, borderSize)
716
860
  };
717
861
  }
718
862