绘制中国象棋棋盘 - CSS Pseudo Elements 的使用
比起国际象棋:
象棋棋盘的绘制要稍微难一些:
主要原因在于,国际象棋的落子在格子中,象棋的落子在交叉点上。因此,国际象棋的布局可以使用 background-color: #fff
以及 background-color: #aaa
标记奇数和偶数的格子——这一点 CSS 也有提供 nth-child()
这个 pseudo class 可以方便且高效的完成任务,但是中国象棋的处理相对而言就稍微麻烦一些。
不过,在经过一番搜索后,发现有一个方法可以相对简单的实现棋盘的绘制,那就是通过 before
和 after
这两个 pseudo elements,最后的结果如下:
空白的棋盘如下:
具体的实现如下:
-
父组件管理棋盘数组
import { useState } from "react"; import Jiang from "../../types/pieces/Jiang"; import Piece from "../piece/Piece"; import styled from "styled-components"; const ChessboardUIWrapper = styled.div` border: 2px solid ${BASE_BLACK}; margin: 0 auto; padding: 2em; width: calc(${TILE_SIZE} * ${NUM_OF_COLS}); position: relative; @media (max-device-width: 1024px) { padding: 0.5em; } @media (max-width: 768px) { padding: 0.8em; width: 80%; } @media (max-device-width: 1024px) and (orientation: landscape) { max-width: calc(${NUM_OF_COLS} * ${TILE_SIZE_MOBILE_LANDSCAPE}); } `; const ChessPiecesUI = styled.div` display: grid; grid-template-columns: repeat(${NUM_OF_COLS}, ${TILE_SIZE}); @media (max-width: 768px) { grid-template-columns: repeat(${NUM_OF_COLS}, ${TILE_SIZE_MOBILE}); } @media (max-device-width: 1024px) and (orientation: landscape) { grid-template-columns: repeat( ${NUM_OF_COLS}, ${TILE_SIZE_MOBILE_LANDSCAPE} ); } `; const ChessboardUI = () => { const chessboard = new Chessboard(); // 9 * 10 const [board, setBoard] = useState(Array(90).fill("")); const renderPieces = () => { return ( <ChessPiecesUI> {board.map((piece, idx) => ( <Piece key={idx} idx={idx} piece={piece} /> ))} </ChessPiecesUI> ); }; return <ChessboardUIWrapper>{renderPieces()}</ChessboardUIWrapper>; };
-
子组件渲染网格
import { FC } from 'react'; import ChessPiece from '../../types/pieces/ChessPiece'; import blankPiece from '../../assets/imgs/blank.svg'; import styled from 'styled-components'; interface TileProps { idx: number; piece?: ChessPiece; } const Piece: FC<TileProps> = ({ idx, piece }) => { let hasBoderBtm = Math.floor(idx / 9) !== 9 && Math.floor(idx / 9) !== 4; // keep borders for river if ((idx % 9 === 0 || idx % 9 === 8) && Math.floor(idx / 9) !== 9) { hasBoderBtm = true; } const hasBorderRight = idx % 9 !== 8; const image = piece ? piece.getImages : blankPiece; const alt = piece ? piece.getTypes.toString() : 'blank piece'; return ( <ChessPieceUI hasBefore={hasBoderBtm} hasAfter={hasBorderRight}> <Img src={image} alt={alt} className="chess-piece__img" /> </ChessPieceUI> ); }; const before = ` &:before { content: ''; position: absolute; left: calc(50% - 1px); top: 50%; border-right: 1px solid black; height: 100%; }`; const after = ` &:after { content: ''; position: absolute; left: 50%; top: calc(50% - 1px); border-top: 1px solid black; width: 100%; }`; const ChessPieceUI = styled.div<{ hasBefore: boolean; hasAfter: boolean }>` position: relative; ${({ hasBefore }) => hasBefore && before} ${({ hasAfter }) => hasAfter && after} `; const Img = styled.img` max-width: 100%; position: relative; z-index: 1; `; export default Piece;
因为使用了 CSS in JS,所以代码量看起来就比较长。
具体逻辑分析如下:
父组件中生成 90 个格子(象棋可落子点为 90),然后将 棋子(piece) 的数据传递到子组件中。
子组件会通过 before
和 after
两个 pseudo selector 生成垂直和水平的边框,细节如下:
这里使用 border-top
, border-bottom
, border-right
, border-left
的区别不是很大,只要一个水平一个垂直即可,区别无非就是 position 的调整,以及最后决定需要隐藏的 rowNum 和 colNum。没有任何条件控制,before
和 after
全都渲染的棋盘如下:
在我这里需要隐藏的,就是位于棋盘最右侧的 border-top
,以及位于棋盘最下方的 border-right
。当然,因为中间还需要楚河汉界的空间,所以楚河汉界处,col = 0
与 col = 8
这两个的 border-right
也需要保留。
我这里使用了一维数组,条件的控制会稍微麻烦一些,如果是二维数组可以直接通过 row
和 col
进行比较即可。
总体来说这次无意中发现 pseudo elements 的这个用法也是打破固定思维了。之后如果有其他类似的需求,完全可以通过 pseudo elements,一来对于 event handler 的处理会方便很多(使用 canvas 需要 refs,),二来也少写很多的代码(听说 canvas 绘图其实还挺麻烦的),三来对于移动端的支持(对比使用图片切割,canvas 移动端这方便不是很了解,无法对比)也好了很多。
顺便附上几张移动端的效果,当然,网格的尺寸、padding 和 margin 之类的还需要调整一下,不过这也就是修改 CSS 或是 constant 中的两三行代码的事情了。
补一张完成图:
米字格为 SVG 制作完成。