diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..fbeb128 --- /dev/null +++ b/.cursorignore @@ -0,0 +1,4 @@ +public/ +third_party/ +assets/ +dist/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0eb8a1e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 iFwu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5a26e29 --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# 象棋棋盘识别与分析系统 + +这是一个在线象棋棋盘识别与分析系统,可以从上传的图片中识别出象棋棋局,并提供最佳走法分析。 + +## 在线演示 + +您可以在这里体验本系统:[象棋棋盘识别与分析](https://ifwu.github.io/xiangqi-analysis/) + +## 功能特点 + +- 棋盘识别:从上传的图片中自动识别象棋棋盘和棋子位置 +- FEN 生成:根据识别结果生成 FEN(Forsyth–Edwards Notation)字符串 +- 最佳走法分析:使用象棋引擎分析当前局面,给出最佳走法建议 +- 走棋模拟:可以按照建议的最佳走法进行模拟,查看局面变化 +- 中文走法表示:将走法转换为中文表示,方便理解 +- 响应式设计:适配不同尺寸的设备屏幕 + +## 技术栈 + +- 前端框架:Preact +- 图像处理:OpenCV.js +- 象棋引擎:Pikafish(WebAssembly 版本) +- 构建工具:Vite +- 包管理器:pnpm +- 部署:GitHub Pages + +## 本地开发 + +1. 克隆仓库: + +``` +git clone https://github.com/your-username/xiangqi-analysis.git +cd xiangqi-analysis +``` + +2. 安装依赖: + +``` +pnpm install +``` + +3. 启动开发服务器: + +``` +pnpm dev +``` + +4. 在浏览器中打开 `http://localhost:5173` 查看应用 + +## 构建和部署 + +1. 构建项目: + +``` +pnpm build +``` + +2. 部署到 GitHub Pages: + +``` +pnpm deploy +``` + +注意:本项目使用 GitHub Actions 进行自动部署。每次推送到主分支时,都会触发构建和部署流程。 + +## 项目结构 + +- `src/`: 源代码目录 + - `components/`: React 组件 + - `chessboard/`: 棋盘识别和分析相关的模块 +- `public/`: 静态资源目录 +- `vite.config.ts`: Vite 配置文件 +- `.github/workflows/`: GitHub Actions 工作流配置 + +## 贡献 + +欢迎提交 Issues 和 Pull Requests 来改进这个项目! + +## 许可证声明 + +本项目使用 MIT 许可证。 + +本项目使用了 [Pikafish](https://github.com/official-pikafish/Pikafish) 象棋引擎,该引擎采用 GNU General Public License v3.0 (GPL-3.0) 许可证。Pikafish 的版权归其原作者所有。 + +本项目仅调用 Pikafish 引擎,并未对其进行修改。使用本项目不要求您的应用程序遵守 GPL-3.0 许可证。但是,如果您计划重新分发包含 Pikafish 的完整应用程序,请确保遵守 GPL-3.0 许可证的相关规定,包括提供源代码访问和适当的许可声明。 + +完整的 GPL-3.0 许可证文本可在 [此处](https://www.gnu.org/licenses/gpl-3.0.en.html) 查看。 + +使用本项目时,请确保理解并遵守相关的许可条款。如有疑问,建议咨询法律专业人士。 diff --git a/index.html b/index.html index 9a5f6e8..d7ee4e1 100644 --- a/index.html +++ b/index.html @@ -1,11 +1,18 @@ - + - - + + + + + + + + + %VITE_UMAMI_SCRIPT% - 象棋分析 + 象棋棋盘识别与分析系统 | 在线象棋分析工具
diff --git a/package.json b/package.json index b8b322e..988ab53 100644 --- a/package.json +++ b/package.json @@ -22,5 +22,6 @@ "vite": "^5.4.8" }, "packageManager": "pnpm@9.12.0", - "homepage": "https://ifwu.github.io/xiangqi-analysis/" + "homepage": "https://ifwu.github.io/xiangqi-analysis/", + "license": "MIT" } diff --git a/src/App.tsx b/src/App.tsx index 0d14d57..db3b7f7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -48,7 +48,7 @@ export function App() { height: number; }>(); // 从 localStorage 加载初始深度值 - const initialDepth = Number(localStorage.getItem('depth')) || 15; + const initialDepth = Number(localStorage.getItem('depth')) || 14; const [depth, setDepth] = useState(initialDepth); // Initialize OpenCV and Chess Engine @@ -295,7 +295,11 @@ export function App() { ); diff --git a/src/app.css b/src/app.css index 23adb82..7968c01 100644 --- a/src/app.css +++ b/src/app.css @@ -1,72 +1,41 @@ +/* 全局样式 */ :root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } + --font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + --text-color: #333; + --background-color: #f4f4f4; + --primary-color: #3498db; + --primary-hover-color: #2980b9; + --header-footer-bg: #2c3e50; + --header-footer-color: #ecf0f1; } body { + font-family: var(--font-family); + line-height: 1.6; + color: var(--text-color); + background-color: var(--background-color); margin: 0; padding: 0; min-width: 100%; min-height: 100vh; - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - line-height: 1.6; - color: #333; - background-color: #f4f4f4; - margin: 0; - padding: 0; display: flex; flex-direction: column; - width: 100%; overflow-x: hidden; } +h1 { + font-size: 2.5rem; + line-height: 1.1; + margin: 0; +} + +h2 { + color: #2c3e50; + margin-top: 0; + font-size: 1.5rem; +} + +/* 布局 */ .app-container { flex: 1; display: flex; @@ -78,10 +47,27 @@ body { box-sizing: border-box; } +.content-wrapper { + display: flex; + flex-wrap: wrap; + gap: 2rem; + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; + box-sizing: border-box; +} + +.left-column, +.right-column { + width: 100%; +} + +/* 头部和底部 */ header, footer { width: 100%; - background-color: #2c3e50; - color: #ecf0f1; + background-color: var(--header-footer-bg); + color: var(--header-footer-color); text-align: center; padding: 1rem 0; } @@ -90,11 +76,17 @@ header { margin-bottom: 1rem; } -h1 { - margin: 0; - font-size: 2.5rem; +footer a { + color: var(--primary-color); + text-decoration: none; + transition: color 0.3s ease; +} + +footer a:hover { + color: var(--primary-hover-color); } +/* 主要内容区 */ main { flex: 1; display: flex; @@ -106,22 +98,7 @@ main { box-sizing: border-box; } -.content-wrapper { - display: flex; - flex-wrap: wrap; - gap: 2rem; - width: 100%; - max-width: 1200px; - margin: 0 auto; - padding: 0 1rem; - box-sizing: border-box; -} - -.left-column, -.right-column { - width: 100%; -} - +/* 组件样式 */ .solution-section, .upload-section, .board-result-section, @@ -134,42 +111,59 @@ main { margin-bottom: 1rem; } -.solution-column, -.image-column { - flex: 1; +/* 标题左对齐 */ +.solution-section h2, +.upload-section h2, +.board-result-section h2, +.fen-section h2, +.depth-control-section h2 { + text-align: left; +} + +/* 内容居中 */ +.solution-section > *:not(h2), +.upload-section > *:not(h2), +.board-result-section > *:not(h2), +.fen-section > *:not(h2), +.depth-control-section > *:not(h2) { + text-align: center; +} + +/* 解法控制按钮样式 */ +.solution-controls { display: flex; - flex-direction: column; + justify-content: center; gap: 1rem; + margin-top: 1rem; } -.solution-section, -.upload-section, -.board-result-section, -.depth-control-section, -.fen-section { - background-color: #fff; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - padding: 1.5rem; - margin-bottom: 1rem; +/* 按钮样式 */ +button { + border-radius: 4px; + border: none; + padding: 0.5rem 1rem; + font-size: 1em; + font-weight: 500; + background-color: var(--primary-color); + color: #fff; + cursor: pointer; + transition: background-color 0.3s, opacity 0.3s; + display: inline-block; /* 确保按钮可以被居中 */ } -.solution-section { - flex: 1; - background-color: #fff; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - padding: 1.5rem; - box-sizing: border-box; +button:hover { + background-color: var(--primary-hover-color); } -h2 { - color: #2c3e50; - margin-top: 0; - font-size: 1.5rem; +button:disabled { + background-color: #95a5a6; + cursor: not-allowed; + opacity: 0.7; } -input[type="file"] { +/* 输入框样式 */ +input[type="file"], +.fen-container input { display: block; margin: 1rem 0; padding: 0.5rem; @@ -180,53 +174,119 @@ input[type="file"] { box-sizing: border-box; } +/* FEN容器 */ .fen-container { display: flex; gap: 1rem; + justify-content: center; + align-items: center; /* 添加这行以垂直居中对齐项目 */ } +/* 输入框样式 */ .fen-container input { - flex: 1; - padding: 0.5rem; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 1rem; + flex: 1; /* 让输入框占据剩余空间 */ + margin: 0; /* 移除之前的 margin */ } -.fen-container button, .solution-controls button { - padding: 0.5rem 1rem; - background-color: #3498db; - color: #fff; - border: none; - border-radius: 4px; - cursor: pointer; - transition: background-color 0.3s, opacity 0.3s; +/* FEN 复制按钮样式 */ +.fen-container button { + flex-shrink: 0; /* 防止按钮被压缩 */ + white-space: nowrap; /* 防止文本换行 */ } -.fen-container button:hover, .solution-controls button:hover { - background-color: #2980b9; +/* 深度滑块 */ +.depth-slider-container { + display: flex; + flex-direction: column; + gap: 0.5rem; } -#solutionCanvas { - display: block; - margin: 0 auto; - border: 1px solid #ddd; +.depth-slider-container label { + font-weight: bold; } -.solution-controls { +#depth-slider { + width: 100%; +} + +/* 图像容器 */ +.image-container { + position: relative; + width: 100%; + overflow: hidden; + margin-top: 1rem; +} + +.overlay-image { + position: absolute; + top: 0; +} + +/* 棋盘样式 */ +.chessboard { + display: inline-block; + border: 2px solid #000; + background-color: #f0d9b5; +} + +.board-row { + display: flex; +} + +.board-cell { + width: 40px; + height: 40px; display: flex; justify-content: center; - gap: 1rem; - margin-top: 1rem; + align-items: center; + border: 1px solid #000; } -footer { - text-align: center; - padding: 1rem 0; - background-color: #2c3e50; - color: #ecf0f1; +.piece { + font-size: 24px; + font-weight: bold; +} + +.piece.red { + color: #ff0000; +} + +.piece.black { + color: #000000; +} + +/* 棋盘和按钮之间的间距 */ +.chessboard-display { + margin-bottom: 1rem; +} + +/* 走棋历史样式 */ +.move-list { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + justify-content: flex-start; +} + +.move-item { + background-color: #f0f0f0; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.9em; + display: inline-block; + margin-bottom: 0.25rem; } +/* 为红方和黑方的移动添加不同的样式 */ +.move-item:nth-child(odd) { + background-color: #ffebee; /* 浅红色背景 */ +} + +.move-item:nth-child(even) { + background-color: #e3f2fd; /* 浅蓝色背景 */ +} + +/* 媒体查询 */ @media (max-width: 768px) { .content-wrapper { flex-direction: column; @@ -238,25 +298,11 @@ footer { width: 100%; } - .solution-section { - order: 1; - } - - .upload-section { - order: 0; - } - - .board-result-section { - order: 2; - } - - .fen-section { - order: 3; - } - - .depth-control-section { - order: 4; - } + .solution-section { order: 1; } + .upload-section { order: 0; } + .board-result-section { order: 2; } + .fen-section { order: 3; } + .depth-control-section { order: 4; } } @media (min-width: 769px) { @@ -279,6 +325,7 @@ footer { .solution-section { height: calc(100vh - 2rem); overflow-y: auto; + order: -1; } .solution-column, @@ -286,60 +333,17 @@ footer { width: 50%; max-width: 600px; } - .solution-section { - order: -1; - } - -} - -.chessboard { - display: inline-block; - border: 2px solid #000; - background-color: #f0d9b5; -} - -.board-row { - display: flex; -} - -.board-cell { - width: 40px; - height: 40px; - display: flex; - justify-content: center; - align-items: center; - border: 1px solid #000; -} - -.piece { - font-size: 24px; - font-weight: bold; -} - -.piece.red { - color: #ff0000; -} - -.piece.black { - color: #000000; } -.solution-controls button:disabled { - background-color: #95a5a6; - cursor: not-allowed; - opacity: 0.7; -} +/* ... 其他样式保持不变 ... */ -.depth-slider-container { - display: flex; - flex-direction: column; - gap: 0.5rem; +.solution-debug { + margin: 1rem 0; + font-size: 1.1em; } -.depth-slider-container label { - font-weight: bold; +.solution-debug p { + margin: 0.5rem 0; } -#depth-slider { - width: 100%; -} \ No newline at end of file +/* ... 其他样式保持不变 ... */ \ No newline at end of file diff --git a/src/components/BoardResult.tsx b/src/components/BoardResult.tsx index 0611f7a..0b96dbd 100644 --- a/src/components/BoardResult.tsx +++ b/src/components/BoardResult.tsx @@ -1,5 +1,3 @@ -import './ChessboardOverlay.css'; - interface BoardResultProps { overlayImageSrc: string; chessboardRect?: { x: number; y: number; width: number; height: number }; diff --git a/src/components/ChessboardOverlay.css b/src/components/ChessboardOverlay.css deleted file mode 100644 index 27c9ce0..0000000 --- a/src/components/ChessboardOverlay.css +++ /dev/null @@ -1,11 +0,0 @@ -.image-container { - position: relative; - width: 100%; - overflow: hidden; - margin-top: 1rem; -} - -.overlay-image { - position: absolute; - top: 0; -} \ No newline at end of file diff --git a/src/components/ImageUploader.tsx b/src/components/ImageUploader.tsx index c772d05..e021237 100644 --- a/src/components/ImageUploader.tsx +++ b/src/components/ImageUploader.tsx @@ -1,8 +1,12 @@ +import { useRef } from 'preact/hooks'; + interface ImageUploaderProps { onImageUpload: (img: HTMLImageElement) => void; } export function ImageUploader({ onImageUpload }: ImageUploaderProps) { + const fileInputRef = useRef(null); + const handleImageUpload = (event: Event) => { const file = (event.target as HTMLInputElement).files?.[0]; if (file) { @@ -16,10 +20,21 @@ export function ImageUploader({ onImageUpload }: ImageUploaderProps) { } }; + const handleButtonClick = () => { + fileInputRef.current?.click(); + }; + return (

上传图片

- + +
); } \ No newline at end of file diff --git a/src/components/SolutionDisplay.tsx b/src/components/SolutionDisplay.tsx index 913fbde..89a085e 100644 --- a/src/components/SolutionDisplay.tsx +++ b/src/components/SolutionDisplay.tsx @@ -23,8 +23,9 @@ export function SolutionDisplay({ fenHistory, }: SolutionDisplayProps) { const currentMoveColor = moveHistory.length % 2 === 0 ? '红方' : '黑方'; + const currentMoveNumber = moveHistory.length + 1; - // Map over moveHistory and use the correct FEN before each move + // Create move history items const moveItems = moveHistory.map((move, index) => { const fenBeforeMove = fenHistory[index]; // FEN before the move let notation = ''; @@ -35,9 +36,9 @@ export function SolutionDisplay({ console.error(`Error converting move to notation: ${err}`); } return ( -
  • - {index % 2 === 0 ? '红方' : '黑方'}: {notation} -
  • + + {notation} + ); }); @@ -49,14 +50,15 @@ export function SolutionDisplay({ ? '黑方胜' : ''; - // Calculate the number of moves for each side - const redMoves = Math.ceil(moveHistory.length / 2); - const blackMoves = Math.floor(moveHistory.length / 2); - return (

    解法展示

    - +
    + +
    -
    -

    - 走棋历史 (红方: {redMoves}步, 黑方: {blackMoves}步) -

    -
      {moveItems}
    -
    -
    -

    当前步: {currentMoveColor}

    -
    +
    {moveItems}
    ); }