绘制中国象棋棋盘 - CSS Pseudo Elements 的使用

绘制中国象棋棋盘 - CSS Pseudo Elements 的使用

比起国际象棋:

chess board

象棋棋盘的绘制要稍微难一些:

cn-chess

主要原因在于,国际象棋的落子在格子中,象棋的落子在交叉点上。因此,国际象棋的布局可以使用 background-color: #fff 以及 background-color: #aaa 标记奇数和偶数的格子——这一点 CSS 也有提供 nth-child() 这个 pseudo class 可以方便且高效的完成任务,但是中国象棋的处理相对而言就稍微麻烦一些。

不过,在经过一番搜索后,发现有一个方法可以相对简单的实现棋盘的绘制,那就是通过 beforeafter 这两个 pseudo elements,最后的结果如下:

res1

空白的棋盘如下:

res2

具体的实现如下:

  • 父组件管理棋盘数组

    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) 的数据传递到子组件中。

子组件会通过 beforeafter 两个 pseudo selector 生成垂直和水平的边框,细节如下:

detail

这里使用 border-top, border-bottom, border-right, border-left 的区别不是很大,只要一个水平一个垂直即可,区别无非就是 position 的调整,以及最后决定需要隐藏的 rowNum 和 colNum。没有任何条件控制,beforeafter 全都渲染的棋盘如下:

all rendered

在我这里需要隐藏的,就是位于棋盘最右侧的 border-top,以及位于棋盘最下方的 border-right。当然,因为中间还需要楚河汉界的空间,所以楚河汉界处,col = 0col = 8 这两个的 border-right 也需要保留。

我这里使用了一维数组,条件的控制会稍微麻烦一些,如果是二维数组可以直接通过 rowcol 进行比较即可。

总体来说这次无意中发现 pseudo elements 的这个用法也是打破固定思维了。之后如果有其他类似的需求,完全可以通过 pseudo elements,一来对于 event handler 的处理会方便很多(使用 canvas 需要 refs,),二来也少写很多的代码(听说 canvas 绘图其实还挺麻烦的),三来对于移动端的支持(对比使用图片切割,canvas 移动端这方便不是很了解,无法对比)也好了很多。

顺便附上几张移动端的效果,当然,网格的尺寸、padding 和 margin 之类的还需要调整一下,不过这也就是修改 CSS 或是 constant 中的两三行代码的事情了。

mobile1

mobile2

tablet1

tablet2

补一张完成图:

finished

米字格为 SVG 制作完成。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值