@hackersheet/next-document-content-kifu 0.1.0-alpha.15 → 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/kifu/kifu.js +2 -2
- 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/{index.js → canvas-utils.js} +26 -16
- 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 +24 -3
- package/dist/cjs/components/shogi-player/moves-area.js +15 -20
- 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 +51 -41
- 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/kifu/kifu.mjs +1 -1
- 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 +24 -3
- package/dist/esm/components/shogi-player/moves-area.mjs +16 -17
- 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 +52 -42
- 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
- package/dist/cjs/components/shogi-player/index.d.ts +0 -2
- package/dist/esm/components/shogi-player/index.d.mts +0 -2
- package/dist/esm/components/shogi-player/index.mjs +0 -4
|
@@ -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
|
-
export {
|
|
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,29 +14,30 @@ 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 {
|
|
43
|
-
MovesArea
|
|
42
|
+
MovesArea as default
|
|
44
43
|
};
|
|
@@ -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 };
|
|
@@ -1,117 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { JKFPlayer } from "json-kifu-format";
|
|
3
2
|
import React from "react";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const cell = boardSize / 9;
|
|
13
|
-
return { margin, boardSize, cell };
|
|
14
|
-
};
|
|
15
|
-
const drawBackground = (ctx, size, boardColor) => {
|
|
16
|
-
ctx.fillStyle = boardColor;
|
|
17
|
-
ctx.fillRect(0, 0, size, size);
|
|
18
|
-
};
|
|
19
|
-
const drawBoard = (ctx, margin, boardSize, lineColor) => {
|
|
20
|
-
ctx.strokeStyle = lineColor;
|
|
21
|
-
ctx.lineWidth = 2;
|
|
22
|
-
ctx.strokeRect(margin, margin, boardSize, boardSize);
|
|
23
|
-
ctx.lineWidth = 1;
|
|
24
|
-
Array.from({ length: 8 }).forEach((_, i) => {
|
|
25
|
-
const offset = (i + 1) * (boardSize / 9);
|
|
26
|
-
ctx.beginPath();
|
|
27
|
-
ctx.moveTo(margin + offset, margin);
|
|
28
|
-
ctx.lineTo(margin + offset, margin + boardSize);
|
|
29
|
-
ctx.stroke();
|
|
30
|
-
ctx.beginPath();
|
|
31
|
-
ctx.moveTo(margin, margin + offset);
|
|
32
|
-
ctx.lineTo(margin + boardSize, margin + offset);
|
|
33
|
-
ctx.stroke();
|
|
34
|
-
});
|
|
35
|
-
};
|
|
36
|
-
const drawPieces = (ctx, pieces, margin, cell, fontFamily, fontSizeRatio, isSente) => {
|
|
37
|
-
ctx.textAlign = "center";
|
|
38
|
-
ctx.textBaseline = "middle";
|
|
39
|
-
pieces.forEach((row, rowIndex) => {
|
|
40
|
-
row.forEach((piece, colIndex) => {
|
|
41
|
-
if (!piece) return;
|
|
42
|
-
const x = isSente ? 8 - rowIndex : rowIndex;
|
|
43
|
-
const y = isSente ? colIndex : 8 - colIndex;
|
|
44
|
-
const px = margin + x * cell + cell / 2;
|
|
45
|
-
const py = margin + y * cell + cell / 2;
|
|
46
|
-
ctx.save();
|
|
47
|
-
ctx.translate(px, py);
|
|
48
|
-
if (isSente && piece.color === Color.White) {
|
|
49
|
-
ctx.rotate(Math.PI);
|
|
50
|
-
} else if (!isSente && piece.color === Color.Black) {
|
|
51
|
-
ctx.rotate(Math.PI);
|
|
52
|
-
}
|
|
53
|
-
const kan = JKFPlayer.kindToKan(piece.kind);
|
|
54
|
-
ctx.fillStyle = "#000";
|
|
55
|
-
if (kan.length === 2) {
|
|
56
|
-
const baseFontSize = cell * fontSizeRatio * 0.5;
|
|
57
|
-
ctx.font = `${baseFontSize}px ${fontFamily}`;
|
|
58
|
-
const scaleX = 2;
|
|
59
|
-
const scaleY = 1;
|
|
60
|
-
const offsetY = cell * 0.18;
|
|
61
|
-
ctx.save();
|
|
62
|
-
ctx.scale(scaleX, scaleY);
|
|
63
|
-
ctx.fillText(kan[0], 0 / scaleX, -offsetY / scaleY);
|
|
64
|
-
ctx.restore();
|
|
65
|
-
ctx.save();
|
|
66
|
-
ctx.scale(scaleX, scaleY);
|
|
67
|
-
ctx.fillText(kan[1], 0 / scaleX, offsetY / scaleY);
|
|
68
|
-
ctx.restore();
|
|
69
|
-
} else {
|
|
70
|
-
const fontSize = cell * fontSizeRatio;
|
|
71
|
-
ctx.font = `${fontSize}px ${fontFamily}`;
|
|
72
|
-
ctx.fillText(kan, 0, 0);
|
|
73
|
-
}
|
|
74
|
-
ctx.restore();
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
};
|
|
78
|
-
const drawCoordinates = (ctx, margin, boardSize, cell, fontFamily, isSente) => {
|
|
79
|
-
ctx.fillStyle = "#000";
|
|
80
|
-
ctx.font = `${cell * 0.35}px ${fontFamily}`;
|
|
81
|
-
ctx.textAlign = "center";
|
|
82
|
-
ctx.textBaseline = "middle";
|
|
83
|
-
Array.from({ length: 9 }).forEach((_, i) => {
|
|
84
|
-
const x = margin + i * cell + cell / 2;
|
|
85
|
-
const y = margin / 2;
|
|
86
|
-
const label = isSente ? JKFPlayer.numToZen(9 - i) : JKFPlayer.numToZen(i + 1);
|
|
87
|
-
ctx.fillText(label, x, y);
|
|
88
|
-
});
|
|
89
|
-
Array.from({ length: 9 }).forEach((_, i) => {
|
|
90
|
-
const x = margin + boardSize + margin / 2;
|
|
91
|
-
const y = margin + i * cell + cell / 2;
|
|
92
|
-
const label = isSente ? JKFPlayer.numToKan(i + 1) : JKFPlayer.numToKan(9 - i);
|
|
93
|
-
ctx.fillText(label, x, y);
|
|
94
|
-
});
|
|
95
|
-
};
|
|
96
|
-
const drawHighlightedCell = (ctx, margin, cell, isSente, currentMove) => {
|
|
97
|
-
if (!currentMove) return;
|
|
98
|
-
if (currentMove.to) {
|
|
99
|
-
const toRow = currentMove.to.x - 1;
|
|
100
|
-
const toCol = currentMove.to.y - 1;
|
|
101
|
-
const toX = isSente ? 8 - toRow : toRow;
|
|
102
|
-
const toY = isSente ? toCol : 8 - toCol;
|
|
103
|
-
ctx.fillStyle = "rgba(255,0,0,0.1)";
|
|
104
|
-
ctx.fillRect(margin + toX * cell, margin + toY * cell, cell, cell);
|
|
105
|
-
}
|
|
106
|
-
if (currentMove.from) {
|
|
107
|
-
const fromRow = currentMove.from.x - 1;
|
|
108
|
-
const fromCol = currentMove.from.y - 1;
|
|
109
|
-
const fromX = isSente ? 8 - fromRow : fromRow;
|
|
110
|
-
const fromY = isSente ? fromCol : 8 - fromCol;
|
|
111
|
-
ctx.fillStyle = "rgba(255,0,0,0.1)";
|
|
112
|
-
ctx.fillRect(margin + fromX * cell, margin + fromY * cell, cell, cell);
|
|
113
|
-
}
|
|
114
|
-
};
|
|
3
|
+
import {
|
|
4
|
+
drawBoardBackground,
|
|
5
|
+
drawBoardGrid,
|
|
6
|
+
drawBoardPieces,
|
|
7
|
+
drawBoardCoordinates,
|
|
8
|
+
drawHighlightedCell
|
|
9
|
+
} from "./board-renderer.mjs";
|
|
10
|
+
import { getCanvasDimensions, getBoardLayout } from "./canvas-utils.mjs";
|
|
115
11
|
const ShogiBoardCanvas = ({
|
|
116
12
|
size = 360,
|
|
117
13
|
boardColor = "#f9d27a",
|
|
@@ -136,11 +32,11 @@ const ShogiBoardCanvas = ({
|
|
|
136
32
|
ctx.scale(dpr, dpr);
|
|
137
33
|
ctx.clearRect(0, 0, size, size);
|
|
138
34
|
const { margin, boardSize, cell } = getBoardLayout(size);
|
|
139
|
-
|
|
140
|
-
|
|
35
|
+
drawBoardBackground(ctx, size, boardColor);
|
|
36
|
+
drawBoardGrid(ctx, margin, boardSize, lineColor);
|
|
141
37
|
drawHighlightedCell(ctx, margin, cell, isSente, currentMove);
|
|
142
|
-
|
|
143
|
-
|
|
38
|
+
drawBoardPieces(ctx, pieces, margin, cell, fontFamily, fontSizeRatio, isSente);
|
|
39
|
+
drawBoardCoordinates(ctx, margin, boardSize, cell, fontFamily, isSente);
|
|
144
40
|
}, [size, boardColor, lineColor, fontFamily, fontSizeRatio, pieces, isSente, currentMove]);
|
|
145
41
|
return /* @__PURE__ */ React.createElement("canvas", { ref: canvasRef, width: size, height: size });
|
|
146
42
|
};
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { ShogiHandsCanvasProps } from './types.mjs';
|
|
3
|
+
import 'shogi.js';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Canvas component for rendering captured pieces (hands)
|
|
7
|
+
*
|
|
8
|
+
* @component
|
|
9
|
+
* @param props - Component props
|
|
10
|
+
* @returns Canvas element displaying the hands (captured pieces area)
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* <ShogiHandsCanvas
|
|
15
|
+
* size={360}
|
|
16
|
+
* hands={capturedPieces}
|
|
17
|
+
* isSente={true}
|
|
18
|
+
* isTop={true}
|
|
19
|
+
* />
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
declare const ShogiHandsCanvas: React.FC<ShogiHandsCanvasProps>;
|
|
15
23
|
|
|
16
24
|
export { ShogiHandsCanvas as default };
|
|
@@ -1,78 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { JKFPlayer } from "json-kifu-format";
|
|
3
2
|
import React from "react";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
const dpr = window.devicePixelRatio || 1;
|
|
7
|
-
return { width: size * dpr, height: size * dpr, dpr };
|
|
8
|
-
};
|
|
9
|
-
const getHandsLayout = (size) => {
|
|
10
|
-
const margin = size * 0.06;
|
|
11
|
-
const boardSize = size - margin * 2;
|
|
12
|
-
const cellSize = boardSize / 9;
|
|
13
|
-
const handsHeight = cellSize + margin + 2;
|
|
14
|
-
return { margin, handsHeight, boardSize, cellSize };
|
|
15
|
-
};
|
|
16
|
-
const drawBackground = (ctx, width, height, color) => {
|
|
17
|
-
ctx.fillStyle = color;
|
|
18
|
-
ctx.fillRect(0, 0, width, height);
|
|
19
|
-
};
|
|
20
|
-
const drawHandsFrame = (ctx, x, y, width, height, lineColor, isTop) => {
|
|
21
|
-
ctx.strokeStyle = lineColor;
|
|
22
|
-
ctx.lineWidth = 2;
|
|
23
|
-
if (isTop) {
|
|
24
|
-
ctx.strokeRect(x, y, width, height);
|
|
25
|
-
} else {
|
|
26
|
-
ctx.strokeRect(x, 2, width, height);
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
const drawCoordinates = (ctx, fontSize, fontFamily) => {
|
|
30
|
-
ctx.fillStyle = "#000";
|
|
31
|
-
ctx.font = `${fontSize}px ${fontFamily}`;
|
|
32
|
-
ctx.textAlign = "center";
|
|
33
|
-
ctx.textBaseline = "middle";
|
|
34
|
-
};
|
|
35
|
-
const drawPieces = (ctx, hands, margin, boardSize, cell, fontFamily, fontSizeRatio, isSente, isTop) => {
|
|
36
|
-
ctx.textAlign = "center";
|
|
37
|
-
ctx.textBaseline = "middle";
|
|
38
|
-
const pieces = isSente && isTop || !isSente && !isTop ? hands[Color.White] : hands[Color.Black];
|
|
39
|
-
if (!pieces || pieces.length === 0) return;
|
|
40
|
-
const grouped = pieces.reduce(
|
|
41
|
-
(acc, piece) => {
|
|
42
|
-
if (!piece) return acc;
|
|
43
|
-
const key = piece.kind;
|
|
44
|
-
if (!acc[key]) acc[key] = { count: 0, color: piece.color };
|
|
45
|
-
acc[key].count += 1;
|
|
46
|
-
return acc;
|
|
47
|
-
},
|
|
48
|
-
{}
|
|
49
|
-
);
|
|
50
|
-
const order = ["OU", "HI", "KA", "KI", "GI", "KE", "KY", "FU"];
|
|
51
|
-
const kinds = order.filter((kind) => grouped[kind]);
|
|
52
|
-
kinds.forEach((kind, index) => {
|
|
53
|
-
const { count, color } = grouped[kind];
|
|
54
|
-
const px = isTop ? margin + boardSize - (index * cell + cell / 2) : margin + index * cell + cell / 2;
|
|
55
|
-
const py = isTop ? margin + cell / 2 : cell / 2 + 2;
|
|
56
|
-
ctx.save();
|
|
57
|
-
ctx.translate(px, py);
|
|
58
|
-
if (isSente && color === Color.White) {
|
|
59
|
-
ctx.rotate(Math.PI);
|
|
60
|
-
} else if (!isSente && color === Color.Black) {
|
|
61
|
-
ctx.rotate(Math.PI);
|
|
62
|
-
}
|
|
63
|
-
const fontSize = cell * fontSizeRatio;
|
|
64
|
-
const kan = JKFPlayer.kindToKan(kind);
|
|
65
|
-
ctx.font = `${fontSize}px ${fontFamily}`;
|
|
66
|
-
ctx.fillText(kan, 0, 0);
|
|
67
|
-
if (count > 1) {
|
|
68
|
-
ctx.font = `${fontSize * 0.5}px ${fontFamily}`;
|
|
69
|
-
const countOffsetX = cell * 0;
|
|
70
|
-
const countOffsetY = cell * 0.8;
|
|
71
|
-
ctx.fillText(String(count), countOffsetX, countOffsetY);
|
|
72
|
-
}
|
|
73
|
-
ctx.restore();
|
|
74
|
-
});
|
|
75
|
-
};
|
|
3
|
+
import { getCanvasDimensions, getHandsLayout } from "./canvas-utils.mjs";
|
|
4
|
+
import { drawHandsBackground, drawHandsFrame, drawHandsPieces } from "./hands-renderer.mjs";
|
|
76
5
|
const ShogiHandsCanvas = ({
|
|
77
6
|
size = 360,
|
|
78
7
|
boardColor = "#f9d27a",
|
|
@@ -97,10 +26,9 @@ const ShogiHandsCanvas = ({
|
|
|
97
26
|
canvas.style.height = `${handsHeight}px`;
|
|
98
27
|
ctx.scale(dpr, dpr);
|
|
99
28
|
ctx.clearRect(0, 0, size, handsHeight);
|
|
100
|
-
|
|
29
|
+
drawHandsBackground(ctx, size, handsHeight, boardColor);
|
|
101
30
|
drawHandsFrame(ctx, margin, margin, boardSize, cellSize, lineColor, isTop);
|
|
102
|
-
|
|
103
|
-
drawPieces(ctx, hands, margin, boardSize, cellSize, fontFamily, fontSizeRatio, isSente, isTop);
|
|
31
|
+
drawHandsPieces(ctx, hands, margin, boardSize, cellSize, fontFamily, fontSizeRatio, isSente, isTop);
|
|
104
32
|
}, [size, boardColor, lineColor, fontFamily, fontSizeRatio, isSente, hands, isTop]);
|
|
105
33
|
return /* @__PURE__ */ React.createElement("canvas", { ref: canvasRef });
|
|
106
34
|
};
|
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { KifuAdapterFactory } from './types.mjs';
|
|
3
|
+
import 'shogi.js';
|
|
2
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Props for the ShogiPlayer component
|
|
7
|
+
* @property kifuText - KIF format kifu (game record) text
|
|
8
|
+
* @property size - Canvas size in CSS pixels (default: 360)
|
|
9
|
+
* @property tesuu - Initial move number to display
|
|
10
|
+
* @property adapterFactory - Factory function for creating KifuAdapter (for DI/testing)
|
|
11
|
+
*/
|
|
3
12
|
type ShogiPlayerProps = {
|
|
4
13
|
kifuText: string;
|
|
5
14
|
size?: number;
|
|
6
15
|
tesuu?: number;
|
|
16
|
+
adapterFactory?: KifuAdapterFactory;
|
|
7
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* Shogi game viewer component with board, hands, and move navigation
|
|
20
|
+
*
|
|
21
|
+
* @component
|
|
22
|
+
* @param props - Component props
|
|
23
|
+
* @returns Interactive shogi player with keyboard and button navigation
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* <ShogiPlayer kifuText={kifString} tesuu={10} />
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
8
30
|
declare function ShogiPlayer(props: ShogiPlayerProps): React.JSX.Element;
|
|
9
31
|
|
|
10
32
|
export { type ShogiPlayerProps, ShogiPlayer as default };
|
|
@@ -1,46 +1,31 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
2
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { createKifuAdapter } from "./adapters/kifu-adapter.mjs";
|
|
4
4
|
import Button from "./button.mjs";
|
|
5
|
-
import
|
|
5
|
+
import MovesArea from "./moves-area.mjs";
|
|
6
6
|
import ShogiBoardCanvas from "./shogi-board-canvas.mjs";
|
|
7
7
|
import ShogiHandsCanvas from "./shogi-hands-canvas.mjs";
|
|
8
8
|
function ShogiPlayer(props) {
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const [
|
|
12
|
-
const [moves, setMoves] = useState(player.kifu.moves);
|
|
13
|
-
const [tesuu, setTesuu] = useState(player.tesuu);
|
|
14
|
-
const [currentMove, setCurrentMove] = useState(player.getMove());
|
|
9
|
+
const factory = props.adapterFactory ?? createKifuAdapter;
|
|
10
|
+
const adapterRef = useRef(factory(props.kifuText));
|
|
11
|
+
const [gameState, setGameState] = useState(() => adapterRef.current.getState());
|
|
15
12
|
const [isSente, setIsSente] = useState(true);
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
};
|
|
32
|
-
const handleBackward = () => {
|
|
33
|
-
player.backward();
|
|
34
|
-
updateState();
|
|
35
|
-
};
|
|
36
|
-
const handleGoto = (tesuu2) => {
|
|
37
|
-
player.goto(tesuu2);
|
|
38
|
-
updateState();
|
|
39
|
-
};
|
|
40
|
-
const handeleToggle = () => {
|
|
41
|
-
setIsSente(!isSente);
|
|
42
|
-
updateState();
|
|
43
|
-
};
|
|
13
|
+
const size = props.size ?? 360;
|
|
14
|
+
const handleForward = useCallback(() => {
|
|
15
|
+
const newState = adapterRef.current.forward();
|
|
16
|
+
setGameState(newState);
|
|
17
|
+
}, []);
|
|
18
|
+
const handleBackward = useCallback(() => {
|
|
19
|
+
const newState = adapterRef.current.backward();
|
|
20
|
+
setGameState(newState);
|
|
21
|
+
}, []);
|
|
22
|
+
const handleGoto = useCallback((tesuu) => {
|
|
23
|
+
const newState = adapterRef.current.goto(tesuu);
|
|
24
|
+
setGameState(newState);
|
|
25
|
+
}, []);
|
|
26
|
+
const handleToggle = useCallback(() => {
|
|
27
|
+
setIsSente((prev) => !prev);
|
|
28
|
+
}, []);
|
|
44
29
|
const handleKeydown = useCallback(
|
|
45
30
|
(event) => {
|
|
46
31
|
event.preventDefault();
|
|
@@ -52,7 +37,7 @@ function ShogiPlayer(props) {
|
|
|
52
37
|
break;
|
|
53
38
|
case "Down":
|
|
54
39
|
case "ArrowDown":
|
|
55
|
-
handleGoto(
|
|
40
|
+
handleGoto(gameState.maxMoveIndex);
|
|
56
41
|
break;
|
|
57
42
|
case "Left":
|
|
58
43
|
case "ArrowLeft":
|
|
@@ -64,18 +49,43 @@ function ShogiPlayer(props) {
|
|
|
64
49
|
handleForward();
|
|
65
50
|
break;
|
|
66
51
|
case "r":
|
|
67
|
-
|
|
52
|
+
handleToggle();
|
|
68
53
|
break;
|
|
69
54
|
}
|
|
70
55
|
},
|
|
71
|
-
[
|
|
56
|
+
[gameState.maxMoveIndex, handleGoto, handleBackward, handleForward, handleToggle]
|
|
72
57
|
);
|
|
73
58
|
useEffect(() => {
|
|
74
59
|
if (props.tesuu !== void 0) {
|
|
75
60
|
handleGoto(props.tesuu);
|
|
76
61
|
}
|
|
77
|
-
}, [props.tesuu]);
|
|
78
|
-
|
|
62
|
+
}, [props.tesuu, handleGoto]);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
return () => {
|
|
65
|
+
adapterRef.current.dispose();
|
|
66
|
+
};
|
|
67
|
+
}, []);
|
|
68
|
+
const topPlayerName = isSente ? `\u2616 ${gameState.header.goteName}` : `\u2617 ${gameState.header.senteName}`;
|
|
69
|
+
const bottomPlayerName = isSente ? `\u2617 ${gameState.header.senteName}` : `\u2616 ${gameState.header.goteName}`;
|
|
70
|
+
return /* @__PURE__ */ React.createElement(
|
|
71
|
+
"div",
|
|
72
|
+
{
|
|
73
|
+
className: "flex flex-col sm:flex-row w-fit",
|
|
74
|
+
tabIndex: 0,
|
|
75
|
+
role: "application",
|
|
76
|
+
"aria-label": "Shogi player - use arrow keys to navigate, space or right arrow to move forward, left arrow to move backward, 'r' to flip board",
|
|
77
|
+
onKeyDown: handleKeydown
|
|
78
|
+
},
|
|
79
|
+
/* @__PURE__ */ React.createElement("div", { className: "flex flex-col" }, /* @__PURE__ */ React.createElement("div", { className: "bg-[#f9d27a] text-black text-xs text-right p-1" }, topPlayerName), /* @__PURE__ */ React.createElement(ShogiHandsCanvas, { size, hands: gameState.hands, isSente, isTop: true }), /* @__PURE__ */ React.createElement(ShogiBoardCanvas, { size, pieces: gameState.board, isSente, currentMove: gameState.currentMove }), /* @__PURE__ */ React.createElement(ShogiHandsCanvas, { size, hands: gameState.hands, isSente, isTop: false }), /* @__PURE__ */ React.createElement("div", { className: "bg-[#f9d27a] text-black text-xs p-1" }, bottomPlayerName)),
|
|
80
|
+
/* @__PURE__ */ React.createElement("div", { className: "flex flex-col sm:w-fit bg-[#f9d27a] p-4 gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "hidden sm:block flex-1 relative w-full" }, /* @__PURE__ */ React.createElement(
|
|
81
|
+
MovesArea,
|
|
82
|
+
{
|
|
83
|
+
readableMoves: gameState.readableMoves,
|
|
84
|
+
tesuu: gameState.currentMoveIndex,
|
|
85
|
+
onTesuuChange: handleGoto
|
|
86
|
+
}
|
|
87
|
+
)), /* @__PURE__ */ React.createElement("div", { className: "hidden sm:block text-black border-black border-2 p-1 text-xs max-h-40 w-0 min-w-full overflow-auto" }, gameState.comments.map((comment, index) => /* @__PURE__ */ React.createElement("div", { key: index }, comment)), gameState.comments.length === 0 && gameState.currentMoveIndex !== 0 && /* @__PURE__ */ React.createElement("div", null, "\xA0"), gameState.currentMoveIndex === 0 && Object.entries(gameState.header).map(([key, value], i) => /* @__PURE__ */ React.createElement("div", { key: i, className: "whitespace-nowrap" }, key, ": ", value))), /* @__PURE__ */ React.createElement("div", { className: "flex gap-2" }, /* @__PURE__ */ React.createElement(Button, { onClick: () => handleGoto(0) }, "\u6700\u521D"), /* @__PURE__ */ React.createElement(Button, { onClick: handleBackward }, "\u524D"), /* @__PURE__ */ React.createElement(Button, { onClick: handleForward }, "\u6B21"), /* @__PURE__ */ React.createElement(Button, { onClick: () => handleGoto(gameState.maxMoveIndex) }, "\u6700\u5F8C"), /* @__PURE__ */ React.createElement(Button, { onClick: handleToggle }, "\u53CD\u8EE2")))
|
|
88
|
+
);
|
|
79
89
|
}
|
|
80
90
|
export {
|
|
81
91
|
ShogiPlayer as default
|