@hackersheet/next-document-content-kifu 0.1.0-alpha.16 → 0.1.0-alpha.17
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/cjs/components/shogi-player/adapters/kifu-adapter.d.ts +11 -0
- package/dist/cjs/components/shogi-player/adapters/kifu-adapter.js +104 -0
- package/dist/cjs/components/shogi-player/board-renderer.d.ts +60 -0
- package/dist/cjs/components/shogi-player/board-renderer.js +137 -0
- package/dist/cjs/components/shogi-player/button.d.ts +22 -1
- package/dist/cjs/components/shogi-player/button.js +2 -1
- package/dist/cjs/components/shogi-player/canvas-utils.d.ts +29 -0
- package/dist/cjs/components/shogi-player/canvas-utils.js +48 -0
- package/dist/cjs/components/shogi-player/hands-renderer.d.ts +42 -0
- package/dist/cjs/components/shogi-player/hands-renderer.js +86 -0
- package/dist/cjs/components/shogi-player/moves-area.d.ts +23 -2
- package/dist/cjs/components/shogi-player/moves-area.js +14 -15
- package/dist/cjs/components/shogi-player/shogi-board-canvas.d.ts +20 -13
- package/dist/cjs/components/shogi-player/shogi-board-canvas.js +9 -119
- package/dist/cjs/components/shogi-player/shogi-hands-canvas.d.ts +20 -12
- package/dist/cjs/components/shogi-player/shogi-hands-canvas.js +7 -79
- package/dist/cjs/components/shogi-player/shogi-player.d.ts +22 -0
- package/dist/cjs/components/shogi-player/shogi-player.js +50 -40
- package/dist/cjs/components/shogi-player/types.d.ts +169 -0
- package/dist/cjs/components/shogi-player/types.js +16 -0
- package/dist/esm/components/shogi-player/adapters/kifu-adapter.d.mts +11 -0
- package/dist/esm/components/shogi-player/adapters/kifu-adapter.mjs +80 -0
- package/dist/esm/components/shogi-player/board-renderer.d.mts +60 -0
- package/dist/esm/components/shogi-player/board-renderer.mjs +109 -0
- package/dist/esm/components/shogi-player/button.d.mts +22 -1
- package/dist/esm/components/shogi-player/button.mjs +2 -1
- package/dist/esm/components/shogi-player/canvas-utils.d.mts +29 -0
- package/dist/esm/components/shogi-player/canvas-utils.mjs +22 -0
- package/dist/esm/components/shogi-player/hands-renderer.d.mts +42 -0
- package/dist/esm/components/shogi-player/hands-renderer.mjs +60 -0
- package/dist/esm/components/shogi-player/moves-area.d.mts +23 -2
- package/dist/esm/components/shogi-player/moves-area.mjs +15 -16
- package/dist/esm/components/shogi-player/shogi-board-canvas.d.mts +20 -13
- package/dist/esm/components/shogi-player/shogi-board-canvas.mjs +12 -116
- package/dist/esm/components/shogi-player/shogi-hands-canvas.d.mts +20 -12
- package/dist/esm/components/shogi-player/shogi-hands-canvas.mjs +4 -76
- package/dist/esm/components/shogi-player/shogi-player.d.mts +22 -0
- package/dist/esm/components/shogi-player/shogi-player.mjs +51 -41
- package/dist/esm/components/shogi-player/types.d.mts +169 -0
- package/dist/esm/components/shogi-player/types.mjs +0 -0
- package/package.json +3 -3
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __copyProps = (to, from, except, desc) => {
|
|
7
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
8
|
+
for (let key of __getOwnPropNames(from))
|
|
9
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
10
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
11
|
+
}
|
|
12
|
+
return to;
|
|
13
|
+
};
|
|
14
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
15
|
+
var types_exports = {};
|
|
16
|
+
module.exports = __toCommonJS(types_exports);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { KifuAdapter } from '../types.mjs';
|
|
2
|
+
import 'shogi.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Factory function to create a KifuAdapter from KIF text
|
|
6
|
+
* @param kifuText - KIF format game record text
|
|
7
|
+
* @returns KifuAdapter instance
|
|
8
|
+
*/
|
|
9
|
+
declare function createKifuAdapter(kifuText: string): KifuAdapter;
|
|
10
|
+
|
|
11
|
+
export { createKifuAdapter };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { JKFPlayer } from "json-kifu-format";
|
|
2
|
+
function convertToMove(move) {
|
|
3
|
+
if (!move?.move) {
|
|
4
|
+
return void 0;
|
|
5
|
+
}
|
|
6
|
+
const m = move.move;
|
|
7
|
+
return {
|
|
8
|
+
from: m.from ? { x: m.from.x, y: m.from.y } : void 0,
|
|
9
|
+
to: m.to ? { x: m.to.x, y: m.to.y } : void 0,
|
|
10
|
+
color: m.color,
|
|
11
|
+
piece: m.piece,
|
|
12
|
+
same: m.same,
|
|
13
|
+
promote: m.promote,
|
|
14
|
+
capture: m.capture,
|
|
15
|
+
relative: m.relative
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
class JKFPlayerAdapter {
|
|
19
|
+
player;
|
|
20
|
+
readableMoves;
|
|
21
|
+
constructor(kifuText) {
|
|
22
|
+
this.player = JKFPlayer.parse(kifuText.trim());
|
|
23
|
+
this.readableMoves = this.buildReadableMoves();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Pre-compute readable move strings for all moves
|
|
27
|
+
*/
|
|
28
|
+
buildReadableMoves() {
|
|
29
|
+
const moves = this.player.kifu.moves;
|
|
30
|
+
return moves.map((move, index) => {
|
|
31
|
+
if (index === 0) {
|
|
32
|
+
return "\u958B\u59CB\u5C40\u9762";
|
|
33
|
+
}
|
|
34
|
+
return JKFPlayer.moveToReadableKifu(move);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Extract current game state from JKFPlayer
|
|
39
|
+
*/
|
|
40
|
+
extractState() {
|
|
41
|
+
const header = this.player.kifu.header;
|
|
42
|
+
return {
|
|
43
|
+
board: [...this.player.shogi.board],
|
|
44
|
+
hands: [...this.player.shogi.hands],
|
|
45
|
+
currentMoveIndex: this.player.tesuu,
|
|
46
|
+
maxMoveIndex: this.player.getMaxTesuu(),
|
|
47
|
+
currentMove: convertToMove(this.player.kifu.moves[this.player.tesuu]),
|
|
48
|
+
header: {
|
|
49
|
+
senteName: header["\u5148\u624B"] || header["\u4E0B\u624B"] || "",
|
|
50
|
+
goteName: header["\u5F8C\u624B"] || header["\u4E0A\u624B"] || "",
|
|
51
|
+
...header
|
|
52
|
+
},
|
|
53
|
+
comments: this.player.getComments(),
|
|
54
|
+
readableMoves: this.readableMoves
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
forward() {
|
|
58
|
+
this.player.forward();
|
|
59
|
+
return this.extractState();
|
|
60
|
+
}
|
|
61
|
+
backward() {
|
|
62
|
+
this.player.backward();
|
|
63
|
+
return this.extractState();
|
|
64
|
+
}
|
|
65
|
+
goto(moveIndex) {
|
|
66
|
+
this.player.goto(moveIndex);
|
|
67
|
+
return this.extractState();
|
|
68
|
+
}
|
|
69
|
+
getState() {
|
|
70
|
+
return this.extractState();
|
|
71
|
+
}
|
|
72
|
+
dispose() {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function createKifuAdapter(kifuText) {
|
|
76
|
+
return new JKFPlayerAdapter(kifuText);
|
|
77
|
+
}
|
|
78
|
+
export {
|
|
79
|
+
createKifuAdapter
|
|
80
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Piece } from 'shogi.js';
|
|
2
|
+
import { Move } from './types.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Draw the board background
|
|
6
|
+
* Fills the entire canvas with the board color
|
|
7
|
+
*
|
|
8
|
+
* @param ctx - Canvas rendering context
|
|
9
|
+
* @param size - Canvas size in CSS pixels
|
|
10
|
+
* @param boardColor - Background color hex code
|
|
11
|
+
*/
|
|
12
|
+
declare function drawBoardBackground(ctx: CanvasRenderingContext2D, size: number, boardColor: string): void;
|
|
13
|
+
/**
|
|
14
|
+
* Draw the board grid (lines and outer frame)
|
|
15
|
+
* Creates the 9x9 grid structure with outer border
|
|
16
|
+
*
|
|
17
|
+
* @param ctx - Canvas rendering context
|
|
18
|
+
* @param margin - Margin from canvas edge
|
|
19
|
+
* @param boardSize - Total size of the board area
|
|
20
|
+
* @param lineColor - Color of the lines
|
|
21
|
+
*/
|
|
22
|
+
declare function drawBoardGrid(ctx: CanvasRenderingContext2D, margin: number, boardSize: number, lineColor: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* Draw all pieces on the board
|
|
25
|
+
* Renders pieces with proper rotation based on perspective
|
|
26
|
+
*
|
|
27
|
+
* @param ctx - Canvas rendering context
|
|
28
|
+
* @param pieces - 2D array of pieces on the board
|
|
29
|
+
* @param margin - Margin from canvas edge
|
|
30
|
+
* @param cell - Size of a single cell
|
|
31
|
+
* @param fontFamily - Font family for piece characters
|
|
32
|
+
* @param fontSizeRatio - Font size ratio relative to cell size
|
|
33
|
+
* @param isSente - Whether rendering from black player's perspective
|
|
34
|
+
*/
|
|
35
|
+
declare function drawBoardPieces(ctx: CanvasRenderingContext2D, pieces: (Piece | null)[][], margin: number, cell: number, fontFamily: string, fontSizeRatio: number, isSente: boolean): void;
|
|
36
|
+
/**
|
|
37
|
+
* Draw board coordinates (row and column labels)
|
|
38
|
+
* Displays numbers and kanji labels around the board
|
|
39
|
+
*
|
|
40
|
+
* @param ctx - Canvas rendering context
|
|
41
|
+
* @param margin - Margin from canvas edge
|
|
42
|
+
* @param boardSize - Total size of the board area
|
|
43
|
+
* @param cell - Size of a single cell
|
|
44
|
+
* @param fontFamily - Font family for labels
|
|
45
|
+
* @param isSente - Whether rendering from black player's perspective
|
|
46
|
+
*/
|
|
47
|
+
declare function drawBoardCoordinates(ctx: CanvasRenderingContext2D, margin: number, boardSize: number, cell: number, fontFamily: string, isSente: boolean): void;
|
|
48
|
+
/**
|
|
49
|
+
* Highlight the destination and source cells of the current move
|
|
50
|
+
* Uses red semi-transparent rectangles to indicate move locations
|
|
51
|
+
*
|
|
52
|
+
* @param ctx - Canvas rendering context
|
|
53
|
+
* @param margin - Margin from canvas edge
|
|
54
|
+
* @param cell - Size of a single cell
|
|
55
|
+
* @param isSente - Whether rendering from black player's perspective
|
|
56
|
+
* @param currentMove - Current move data, undefined means no highlight
|
|
57
|
+
*/
|
|
58
|
+
declare function drawHighlightedCell(ctx: CanvasRenderingContext2D, margin: number, cell: number, isSente: boolean, currentMove?: Move): void;
|
|
59
|
+
|
|
60
|
+
export { drawBoardBackground, drawBoardCoordinates, drawBoardGrid, drawBoardPieces, drawHighlightedCell };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { JKFPlayer } from "json-kifu-format";
|
|
2
|
+
import { Color } from "shogi.js";
|
|
3
|
+
function drawBoardBackground(ctx, size, boardColor) {
|
|
4
|
+
ctx.fillStyle = boardColor;
|
|
5
|
+
ctx.fillRect(0, 0, size, size);
|
|
6
|
+
}
|
|
7
|
+
function drawBoardGrid(ctx, margin, boardSize, lineColor) {
|
|
8
|
+
ctx.strokeStyle = lineColor;
|
|
9
|
+
ctx.lineWidth = 2;
|
|
10
|
+
ctx.strokeRect(margin, margin, boardSize, boardSize);
|
|
11
|
+
ctx.lineWidth = 1;
|
|
12
|
+
Array.from({ length: 8 }).forEach((_, i) => {
|
|
13
|
+
const offset = (i + 1) * (boardSize / 9);
|
|
14
|
+
ctx.beginPath();
|
|
15
|
+
ctx.moveTo(margin + offset, margin);
|
|
16
|
+
ctx.lineTo(margin + offset, margin + boardSize);
|
|
17
|
+
ctx.stroke();
|
|
18
|
+
ctx.beginPath();
|
|
19
|
+
ctx.moveTo(margin, margin + offset);
|
|
20
|
+
ctx.lineTo(margin + boardSize, margin + offset);
|
|
21
|
+
ctx.stroke();
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function drawBoardPieces(ctx, pieces, margin, cell, fontFamily, fontSizeRatio, isSente) {
|
|
25
|
+
ctx.textAlign = "center";
|
|
26
|
+
ctx.textBaseline = "middle";
|
|
27
|
+
pieces.forEach((row, rowIndex) => {
|
|
28
|
+
row.forEach((piece, colIndex) => {
|
|
29
|
+
if (!piece) return;
|
|
30
|
+
const x = isSente ? 8 - rowIndex : rowIndex;
|
|
31
|
+
const y = isSente ? colIndex : 8 - colIndex;
|
|
32
|
+
const px = margin + x * cell + cell / 2;
|
|
33
|
+
const py = margin + y * cell + cell / 2;
|
|
34
|
+
ctx.save();
|
|
35
|
+
ctx.translate(px, py);
|
|
36
|
+
if (isSente && piece.color === Color.White) {
|
|
37
|
+
ctx.rotate(Math.PI);
|
|
38
|
+
} else if (!isSente && piece.color === Color.Black) {
|
|
39
|
+
ctx.rotate(Math.PI);
|
|
40
|
+
}
|
|
41
|
+
const kan = JKFPlayer.kindToKan(piece.kind);
|
|
42
|
+
ctx.fillStyle = "#000";
|
|
43
|
+
if (kan.length === 2) {
|
|
44
|
+
const baseFontSize = cell * fontSizeRatio * 0.5;
|
|
45
|
+
ctx.font = `${baseFontSize}px ${fontFamily}`;
|
|
46
|
+
const scaleX = 2;
|
|
47
|
+
const scaleY = 1;
|
|
48
|
+
const offsetY = cell * 0.18;
|
|
49
|
+
ctx.save();
|
|
50
|
+
ctx.scale(scaleX, scaleY);
|
|
51
|
+
ctx.fillText(kan[0], 0 / scaleX, -offsetY / scaleY);
|
|
52
|
+
ctx.restore();
|
|
53
|
+
ctx.save();
|
|
54
|
+
ctx.scale(scaleX, scaleY);
|
|
55
|
+
ctx.fillText(kan[1], 0 / scaleX, offsetY / scaleY);
|
|
56
|
+
ctx.restore();
|
|
57
|
+
} else {
|
|
58
|
+
const fontSize = cell * fontSizeRatio;
|
|
59
|
+
ctx.font = `${fontSize}px ${fontFamily}`;
|
|
60
|
+
ctx.fillText(kan, 0, 0);
|
|
61
|
+
}
|
|
62
|
+
ctx.restore();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function drawBoardCoordinates(ctx, margin, boardSize, cell, fontFamily, isSente) {
|
|
67
|
+
ctx.fillStyle = "#000";
|
|
68
|
+
ctx.font = `${cell * 0.35}px ${fontFamily}`;
|
|
69
|
+
ctx.textAlign = "center";
|
|
70
|
+
ctx.textBaseline = "middle";
|
|
71
|
+
Array.from({ length: 9 }).forEach((_, i) => {
|
|
72
|
+
const x = margin + i * cell + cell / 2;
|
|
73
|
+
const y = margin / 2;
|
|
74
|
+
const label = isSente ? JKFPlayer.numToZen(9 - i) : JKFPlayer.numToZen(i + 1);
|
|
75
|
+
ctx.fillText(label, x, y);
|
|
76
|
+
});
|
|
77
|
+
Array.from({ length: 9 }).forEach((_, i) => {
|
|
78
|
+
const x = margin + boardSize + margin / 2;
|
|
79
|
+
const y = margin + i * cell + cell / 2;
|
|
80
|
+
const label = isSente ? JKFPlayer.numToKan(i + 1) : JKFPlayer.numToKan(9 - i);
|
|
81
|
+
ctx.fillText(label, x, y);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function drawHighlightedCell(ctx, margin, cell, isSente, currentMove) {
|
|
85
|
+
if (!currentMove) return;
|
|
86
|
+
if (currentMove.to) {
|
|
87
|
+
const toRow = currentMove.to.x - 1;
|
|
88
|
+
const toCol = currentMove.to.y - 1;
|
|
89
|
+
const toX = isSente ? 8 - toRow : toRow;
|
|
90
|
+
const toY = isSente ? toCol : 8 - toCol;
|
|
91
|
+
ctx.fillStyle = "rgba(255,0,0,0.1)";
|
|
92
|
+
ctx.fillRect(margin + toX * cell, margin + toY * cell, cell, cell);
|
|
93
|
+
}
|
|
94
|
+
if (currentMove.from) {
|
|
95
|
+
const fromRow = currentMove.from.x - 1;
|
|
96
|
+
const fromCol = currentMove.from.y - 1;
|
|
97
|
+
const fromX = isSente ? 8 - fromRow : fromRow;
|
|
98
|
+
const fromY = isSente ? fromCol : 8 - fromCol;
|
|
99
|
+
ctx.fillStyle = "rgba(255,0,0,0.1)";
|
|
100
|
+
ctx.fillRect(margin + fromX * cell, margin + fromY * cell, cell, cell);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export {
|
|
104
|
+
drawBoardBackground,
|
|
105
|
+
drawBoardCoordinates,
|
|
106
|
+
drawBoardGrid,
|
|
107
|
+
drawBoardPieces,
|
|
108
|
+
drawHighlightedCell
|
|
109
|
+
};
|
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
import React, { MouseEventHandler, PropsWithChildren } from 'react';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Props for the Button component
|
|
5
|
+
* @property onClick - Click event handler
|
|
6
|
+
* @property type - HTML button type attribute
|
|
7
|
+
* @property children - Button content/label
|
|
8
|
+
*/
|
|
3
9
|
type ButtonProps = {
|
|
4
10
|
onClick?: MouseEventHandler<HTMLButtonElement>;
|
|
11
|
+
type?: 'button' | 'submit' | 'reset';
|
|
5
12
|
} & PropsWithChildren;
|
|
6
|
-
|
|
13
|
+
/**
|
|
14
|
+
* A styled button component for the shogi player interface
|
|
15
|
+
*
|
|
16
|
+
* @component
|
|
17
|
+
* @param props - Component props
|
|
18
|
+
* @returns A button element with consistent styling
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <Button onClick={handleClick} type="button">
|
|
23
|
+
* Click me
|
|
24
|
+
* </Button>
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
declare function Button({ children, onClick, type }: ButtonProps): React.JSX.Element;
|
|
7
28
|
|
|
8
29
|
export { type ButtonProps, Button as default };
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
function Button({ children, onClick }) {
|
|
2
|
+
function Button({ children, onClick, type = "button" }) {
|
|
3
3
|
return /* @__PURE__ */ React.createElement(
|
|
4
4
|
"button",
|
|
5
5
|
{
|
|
6
|
+
type,
|
|
6
7
|
className: "border-2 text-xs text-black p-2 border-black rounded-lg hover:bg-amber-100 cursor-pointer",
|
|
7
8
|
onClick
|
|
8
9
|
},
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { CanvasDimensions, BoardLayout, HandsLayout } from './types.mjs';
|
|
2
|
+
import 'shogi.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Calculate canvas dimensions based on device pixel ratio
|
|
6
|
+
* Adjusts canvas resolution for crisp rendering on HiDPI displays
|
|
7
|
+
*
|
|
8
|
+
* @param size - Base size in CSS pixels
|
|
9
|
+
* @returns Canvas dimensions with DPR applied for high-quality rendering
|
|
10
|
+
*/
|
|
11
|
+
declare function getCanvasDimensions(size: number): CanvasDimensions;
|
|
12
|
+
/**
|
|
13
|
+
* Calculate board layout dimensions
|
|
14
|
+
* Computes margins and cell size based on canvas size
|
|
15
|
+
*
|
|
16
|
+
* @param size - Base size in CSS pixels
|
|
17
|
+
* @returns Layout object containing margin, total board size, and individual cell size
|
|
18
|
+
*/
|
|
19
|
+
declare function getBoardLayout(size: number): BoardLayout;
|
|
20
|
+
/**
|
|
21
|
+
* Calculate hands (captured pieces) layout dimensions
|
|
22
|
+
* Computes layout for the area displaying captured pieces
|
|
23
|
+
*
|
|
24
|
+
* @param size - Base size in CSS pixels
|
|
25
|
+
* @returns Layout object with margins, height, and cell size for the hands area
|
|
26
|
+
*/
|
|
27
|
+
declare function getHandsLayout(size: number): HandsLayout;
|
|
28
|
+
|
|
29
|
+
export { getBoardLayout, getCanvasDimensions, getHandsLayout };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function getCanvasDimensions(size) {
|
|
2
|
+
const dpr = window.devicePixelRatio || 1;
|
|
3
|
+
return { width: size * dpr, height: size * dpr, dpr };
|
|
4
|
+
}
|
|
5
|
+
function getBoardLayout(size) {
|
|
6
|
+
const margin = size * 0.06;
|
|
7
|
+
const boardSize = size - margin * 2;
|
|
8
|
+
const cell = boardSize / 9;
|
|
9
|
+
return { margin, boardSize, cell };
|
|
10
|
+
}
|
|
11
|
+
function getHandsLayout(size) {
|
|
12
|
+
const margin = size * 0.06;
|
|
13
|
+
const boardSize = size - margin * 2;
|
|
14
|
+
const cellSize = boardSize / 9;
|
|
15
|
+
const handsHeight = cellSize + margin + 2;
|
|
16
|
+
return { margin, handsHeight, boardSize, cellSize };
|
|
17
|
+
}
|
|
18
|
+
export {
|
|
19
|
+
getBoardLayout,
|
|
20
|
+
getCanvasDimensions,
|
|
21
|
+
getHandsLayout
|
|
22
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Piece } from 'shogi.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Draw the background of the hands area
|
|
5
|
+
* Fills the canvas with a solid color
|
|
6
|
+
*
|
|
7
|
+
* @param ctx - Canvas rendering context
|
|
8
|
+
* @param width - Width of the canvas
|
|
9
|
+
* @param height - Height of the canvas
|
|
10
|
+
* @param color - Background color hex code
|
|
11
|
+
*/
|
|
12
|
+
declare function drawHandsBackground(ctx: CanvasRenderingContext2D, width: number, height: number, color: string): void;
|
|
13
|
+
/**
|
|
14
|
+
* Draw the frame border for the hands area
|
|
15
|
+
* Creates a bordered rectangle around the hands display
|
|
16
|
+
*
|
|
17
|
+
* @param ctx - Canvas rendering context
|
|
18
|
+
* @param x - X coordinate of the frame
|
|
19
|
+
* @param y - Y coordinate of the frame
|
|
20
|
+
* @param width - Width of the frame
|
|
21
|
+
* @param height - Height of the frame
|
|
22
|
+
* @param lineColor - Color of the border lines
|
|
23
|
+
* @param isTop - Whether this is the top hands area (affects positioning)
|
|
24
|
+
*/
|
|
25
|
+
declare function drawHandsFrame(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, lineColor: string, isTop: boolean): void;
|
|
26
|
+
/**
|
|
27
|
+
* Draw captured pieces (hands) on the canvas
|
|
28
|
+
* Displays pieces grouped by type with counts for multiple captures
|
|
29
|
+
*
|
|
30
|
+
* @param ctx - Canvas rendering context
|
|
31
|
+
* @param hands - 2D array of captured pieces
|
|
32
|
+
* @param margin - Margin from canvas edge
|
|
33
|
+
* @param boardSize - Width of the hands area
|
|
34
|
+
* @param cell - Size of a single cell
|
|
35
|
+
* @param fontFamily - Font family for piece names
|
|
36
|
+
* @param fontSizeRatio - Font size ratio relative to cell size
|
|
37
|
+
* @param isSente - Whether rendering from black player's perspective
|
|
38
|
+
* @param isTop - Whether this is the top hands area
|
|
39
|
+
*/
|
|
40
|
+
declare function drawHandsPieces(ctx: CanvasRenderingContext2D, hands: Piece[][], margin: number, boardSize: number, cell: number, fontFamily: string, fontSizeRatio: number, isSente: boolean, isTop: boolean): void;
|
|
41
|
+
|
|
42
|
+
export { drawHandsBackground, drawHandsFrame, drawHandsPieces };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { JKFPlayer } from "json-kifu-format";
|
|
2
|
+
import { Color } from "shogi.js";
|
|
3
|
+
function drawHandsBackground(ctx, width, height, color) {
|
|
4
|
+
ctx.fillStyle = color;
|
|
5
|
+
ctx.fillRect(0, 0, width, height);
|
|
6
|
+
}
|
|
7
|
+
function drawHandsFrame(ctx, x, y, width, height, lineColor, isTop) {
|
|
8
|
+
ctx.strokeStyle = lineColor;
|
|
9
|
+
ctx.lineWidth = 2;
|
|
10
|
+
if (isTop) {
|
|
11
|
+
ctx.strokeRect(x, y, width, height);
|
|
12
|
+
} else {
|
|
13
|
+
ctx.strokeRect(x, 2, width, height);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function drawHandsPieces(ctx, hands, margin, boardSize, cell, fontFamily, fontSizeRatio, isSente, isTop) {
|
|
17
|
+
ctx.textAlign = "center";
|
|
18
|
+
ctx.textBaseline = "middle";
|
|
19
|
+
const pieces = isSente && isTop || !isSente && !isTop ? hands[Color.White] : hands[Color.Black];
|
|
20
|
+
if (!pieces || pieces.length === 0) return;
|
|
21
|
+
const grouped = pieces.reduce(
|
|
22
|
+
(acc, piece) => {
|
|
23
|
+
if (!piece) return acc;
|
|
24
|
+
const key = piece.kind;
|
|
25
|
+
if (!acc[key]) acc[key] = { count: 0, color: piece.color };
|
|
26
|
+
acc[key].count += 1;
|
|
27
|
+
return acc;
|
|
28
|
+
},
|
|
29
|
+
{}
|
|
30
|
+
);
|
|
31
|
+
const order = ["OU", "HI", "KA", "KI", "GI", "KE", "KY", "FU"];
|
|
32
|
+
const kinds = order.filter((kind) => grouped[kind]);
|
|
33
|
+
kinds.forEach((kind, index) => {
|
|
34
|
+
const { count, color } = grouped[kind];
|
|
35
|
+
const px = isTop ? margin + boardSize - (index * cell + cell / 2) : margin + index * cell + cell / 2;
|
|
36
|
+
const py = isTop ? margin + cell / 2 : cell / 2 + 2;
|
|
37
|
+
ctx.save();
|
|
38
|
+
ctx.translate(px, py);
|
|
39
|
+
if (isSente && color === Color.White) {
|
|
40
|
+
ctx.rotate(Math.PI);
|
|
41
|
+
} else if (!isSente && color === Color.Black) {
|
|
42
|
+
ctx.rotate(Math.PI);
|
|
43
|
+
}
|
|
44
|
+
const fontSize = cell * fontSizeRatio;
|
|
45
|
+
const kan = JKFPlayer.kindToKan(kind);
|
|
46
|
+
ctx.font = `${fontSize}px ${fontFamily}`;
|
|
47
|
+
ctx.fillText(kan, 0, 0);
|
|
48
|
+
if (count > 1) {
|
|
49
|
+
ctx.font = `${fontSize * 0.5}px ${fontFamily}`;
|
|
50
|
+
const countOffsetY = cell * 0.8;
|
|
51
|
+
ctx.fillText(String(count), 0, countOffsetY);
|
|
52
|
+
}
|
|
53
|
+
ctx.restore();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
drawHandsBackground,
|
|
58
|
+
drawHandsFrame,
|
|
59
|
+
drawHandsPieces
|
|
60
|
+
};
|
|
@@ -1,11 +1,32 @@
|
|
|
1
|
-
import { IMoveFormat } from 'json-kifu-format/dist/src/Formats';
|
|
2
1
|
import React from 'react';
|
|
3
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Props for the MovesArea component
|
|
5
|
+
* @property readableMoves - Array of human-readable move strings (e.g., "☗7六歩")
|
|
6
|
+
* @property tesuu - Current move number
|
|
7
|
+
* @property onTesuuChange - Callback when the user selects a different move
|
|
8
|
+
*/
|
|
4
9
|
type MovesAreaProps = {
|
|
5
|
-
|
|
10
|
+
readableMoves: string[];
|
|
6
11
|
tesuu: number;
|
|
7
12
|
onTesuuChange?: (tesuu: number) => void;
|
|
8
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* Scrollable list of moves showing the game record with current position highlighting
|
|
16
|
+
*
|
|
17
|
+
* @component
|
|
18
|
+
* @param props - Component props
|
|
19
|
+
* @returns A scrollable moves list with keyboard navigation support
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* <MovesArea
|
|
24
|
+
* readableMoves={['開始局面', '☗7六歩', '☖3四歩']}
|
|
25
|
+
* tesuu={1}
|
|
26
|
+
* onTesuuChange={(move) => setCurrentMove(move)}
|
|
27
|
+
* />
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
9
30
|
declare function MovesArea(props: MovesAreaProps): React.JSX.Element;
|
|
10
31
|
|
|
11
32
|
export { type MovesAreaProps, MovesArea as default };
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
import React, { Fragment, useEffect, useRef } from "react";
|
|
2
|
+
import React, { useEffect, useRef } from "react";
|
|
4
3
|
function MovesArea(props) {
|
|
5
|
-
const moves = props.moves;
|
|
6
4
|
const scrollRef = useRef(null);
|
|
7
5
|
const containerRef = useRef(null);
|
|
8
6
|
useEffect(() => {
|
|
@@ -16,27 +14,28 @@ function MovesArea(props) {
|
|
|
16
14
|
});
|
|
17
15
|
}
|
|
18
16
|
}, [props.tesuu]);
|
|
19
|
-
const
|
|
17
|
+
const initialCurrent = 0 === props.tesuu ? " bg-amber-600" : "";
|
|
20
18
|
return /* @__PURE__ */ React.createElement("div", { className: "absolute overflow-y-auto h-full w-full border-2 border-black text-black", ref: containerRef }, /* @__PURE__ */ React.createElement("div", { className: "grid gap-0 text-xs" }, /* @__PURE__ */ React.createElement(
|
|
21
19
|
"div",
|
|
22
20
|
{
|
|
23
21
|
onClick: () => props.onTesuuChange && props.onTesuuChange(0),
|
|
24
|
-
className: "col-span-500 grid grid-cols-subgrid gap-2 py-1 px-2 cursor-pointer hover:bg-amber-100" +
|
|
22
|
+
className: "col-span-500 grid grid-cols-subgrid gap-2 py-1 px-2 cursor-pointer hover:bg-amber-100" + initialCurrent
|
|
25
23
|
},
|
|
26
|
-
/* @__PURE__ */ React.createElement("div", null, 0 === props.tesuu && /* @__PURE__ */ React.createElement("
|
|
27
|
-
/* @__PURE__ */ React.createElement("div", null,
|
|
28
|
-
),
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
return /* @__PURE__ */ React.createElement(
|
|
24
|
+
/* @__PURE__ */ React.createElement("div", null, 0 === props.tesuu && /* @__PURE__ */ React.createElement("span", { ref: scrollRef, className: "sr-only", "aria-hidden": "true" })),
|
|
25
|
+
/* @__PURE__ */ React.createElement("div", null, props.readableMoves[0])
|
|
26
|
+
), props.readableMoves.slice(1).map((moveText, index) => {
|
|
27
|
+
const moveIndex = index + 1;
|
|
28
|
+
const moveCurrent = moveIndex === props.tesuu ? " bg-amber-600" : "";
|
|
29
|
+
return /* @__PURE__ */ React.createElement(
|
|
32
30
|
"div",
|
|
33
31
|
{
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
key: moveIndex,
|
|
33
|
+
className: "col-span-500 grid grid-cols-subgrid border-black gap-2 border-t py-1 px-2 cursor-pointer hover:bg-amber-100" + moveCurrent,
|
|
34
|
+
onClick: () => props.onTesuuChange && props.onTesuuChange(moveIndex)
|
|
36
35
|
},
|
|
37
|
-
/* @__PURE__ */ React.createElement("div", { className: "flex" },
|
|
38
|
-
/* @__PURE__ */ React.createElement("div", null,
|
|
39
|
-
)
|
|
36
|
+
/* @__PURE__ */ React.createElement("div", { className: "flex" }, moveIndex === props.tesuu && /* @__PURE__ */ React.createElement("span", { ref: scrollRef, className: "sr-only", "aria-hidden": "true" }), /* @__PURE__ */ React.createElement("div", { className: "tabular-nums text-right flex-auto" }, moveIndex)),
|
|
37
|
+
/* @__PURE__ */ React.createElement("div", null, moveText)
|
|
38
|
+
);
|
|
40
39
|
})));
|
|
41
40
|
}
|
|
42
41
|
export {
|
|
@@ -1,17 +1,24 @@
|
|
|
1
|
-
import { IMoveMoveFormat } from 'json-kifu-format/dist/src/Formats';
|
|
2
1
|
import React from 'react';
|
|
3
|
-
import {
|
|
2
|
+
import { ShogiBoardCanvasProps } from './types.mjs';
|
|
3
|
+
import 'shogi.js';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Canvas component for rendering the shogi board with pieces
|
|
7
|
+
*
|
|
8
|
+
* @component
|
|
9
|
+
* @param props - Component props
|
|
10
|
+
* @returns Canvas element displaying the shogi board
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* <ShogiBoardCanvas
|
|
15
|
+
* size={360}
|
|
16
|
+
* pieces={boardState}
|
|
17
|
+
* isSente={true}
|
|
18
|
+
* currentMove={lastMove}
|
|
19
|
+
* />
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
declare const ShogiBoardCanvas: React.FC<ShogiBoardCanvasProps>;
|
|
16
23
|
|
|
17
24
|
export { ShogiBoardCanvas as default };
|