From bed16bc1f1d62f8ae5eadb3a75d8c8d31ebf8e57 Mon Sep 17 00:00:00 2001 From: iFwu Date: Tue, 8 Oct 2024 21:41:31 +0800 Subject: [PATCH] Improve chessboard display with custom font and DPI scaling - Add LiSu font for Chinese characters on the chessboard - Implement device pixel ratio (DPI) scaling for better rendering on high-resolution displays - Adjust text positioning and styling for better visual appearance - Include .woff2 files in Vite asset processing --- CHANGELOG.md | 3 ++ assets/LiSu.woff2 | Bin 0 -> 4660 bytes src/app.css | 9 +++- src/components/ChessboardDisplay.tsx | 63 +++++++++++++++++++-------- vite.config.ts | 1 + 5 files changed, 57 insertions(+), 19 deletions(-) create mode 100644 assets/LiSu.woff2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d874f7..073154a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ### 2024-10-08 - 增加 更新日志 显示 +- 棋盘显示优化 + - 棋子字体使用隶书 + - 提升高清屏渲染质量 ### 2024-10-07 - 优化 **OpenCV.js** 库加载、使用 `cdn.jsdmirror.com` 替换 `jsdelivr.net` diff --git a/assets/LiSu.woff2 b/assets/LiSu.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..2d8287702cbb953cbb2df026fded6f5dc89846af GIT binary patch literal 4660 zcmV-463gv(Pew8T0RR9101`9+5C8xG03mDu01?&z0RR9100000000000000000000 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[]); const canvasRef = useRef(null); const [scale, setScale] = useState(1); + const [fontLoaded, setFontLoaded] = useState(false); + + useEffect(() => { + const font = new FontFace('LiSu', `url(${LiSuFontUrl})`); + font.load().then(() => { + document.fonts.add(font); + return document.fonts.ready; + }).then(() => { + setFontLoaded(true); + }).catch(error => console.error('Failed to load font:', error)); + }, []); useEffect(() => { const newBoard = parseFEN(fen); @@ -38,13 +50,13 @@ export function ChessboardDisplay({ fen, bestMove }: ChessboardDisplayProps) { }, [fen]); useEffect(() => { - if (canvasRef.current) { + if (canvasRef.current && fontLoaded) { drawChessboard(); if (bestMove) { drawArrow(bestMove); } } - }, [board, bestMove]); + }, [board, bestMove, fontLoaded]); useEffect(() => { function handleResize() { @@ -98,12 +110,17 @@ export function ChessboardDisplay({ fen, bestMove }: ChessboardDisplayProps) { const boardWidth = 8 * cellSize + 2 * margin; const boardHeight = 9 * cellSize + 2 * margin; - // Set canvas size based on scale - canvas.width = boardWidth * scale; - canvas.height = boardHeight * scale; + const dpr = window.devicePixelRatio; + // 设置 Canvas 大小,考虑设备像素比 + canvas.width = boardWidth * scale * dpr; + canvas.height = boardHeight * scale * dpr; - // Scale the context - ctx.scale(scale, scale); + // 设置 Canvas 的 CSS 大小 + canvas.style.width = `${boardWidth * scale}px`; + canvas.style.height = `${boardHeight * scale}px`; + + // 缩放绘图上下文以匹配设备像素比 + ctx.scale(dpr * scale, dpr * scale); // 绘制棋盘背景 ctx.fillStyle = '#f0d9b5'; @@ -146,12 +163,12 @@ export function ChessboardDisplay({ fen, bestMove }: ChessboardDisplayProps) { ctx.stroke(); // 绘制楚河汉界 - ctx.font = '18px "KaiTi", "楷体", sans-serif'; - ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + ctx.font = '20px "LiSu", sans-serif'; + ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - ctx.fillText('楚 河', 2 * cellSize + margin, 4.5 * cellSize + margin); - ctx.fillText('漢 界', 6 * cellSize + margin, 4.5 * cellSize + margin); + ctx.fillText('楚 河', 2 * cellSize + margin, 4.6 * cellSize + margin); + ctx.fillText('汉 界', 6 * cellSize + margin, 4.6 * cellSize + margin); // 绘制九宫格斜线 ctx.beginPath(); @@ -171,7 +188,7 @@ export function ChessboardDisplay({ fen, bestMove }: ChessboardDisplayProps) { function drawCrosshair(x: number, y: number, isEdge: boolean = false) { if (!ctx) return; const size = cellSize * 0.15; - const offset = -cellSize * 0.25; // 移动距离 + const offset = -cellSize * 0.23; // 移动距离 ctx.strokeStyle = 'rgba(0, 0, 0, 0.4)'; ctx.lineWidth = 1; @@ -222,7 +239,7 @@ export function ChessboardDisplay({ fen, bestMove }: ChessboardDisplayProps) { } // 绘制坐标标记 - ctx.font = '14px Arial'; + ctx.font = '11px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; @@ -265,16 +282,26 @@ export function ChessboardDisplay({ fen, bestMove }: ChessboardDisplayProps) { // 绘制棋子文字 ctx.fillStyle = piece.color === 'red' ? '#c00000' : '#000000'; - ctx.font = - 'bold 25px "LiSu", "隶书", "STKaiti", "楷体", "KaiTi", "SimKai", sans-serif'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; + ctx.font = 'bold 22px "LiSu", sans-serif'; const pieceChar = piece.type.toUpperCase(); const pieceSymbol = piece.color === 'red' ? pieceMap[pieceChar] : pieceMap[pieceChar.toLowerCase()]; - ctx.fillText(pieceSymbol, centerX, centerY - radius * 0.12); + + // 保存当前的绘图状态 + ctx.save(); + + // 应用变换:水平拉伸 1.2 倍,垂直不变 + ctx.setTransform(1 * dpr * scale, 0, 0, 1.25 * dpr * scale, centerX * dpr * scale, centerY * dpr * scale); + + // 绘制文字,注意坐标现在是相对于变换后的原点 + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(pieceSymbol, radius * 0.05, radius * 0.2); + + // 恢复原始绘图状态 + ctx.restore(); } }); }); diff --git a/vite.config.ts b/vite.config.ts index 19755b4..7b7e9bd 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -62,4 +62,5 @@ export default defineConfig({ define: { 'import.meta.env.VITE_GIT_COMMIT_HASH': JSON.stringify(process.env.VITE_GIT_COMMIT_HASH), }, + assetsInclude: ['**/*.woff2'], }); \ No newline at end of file