在 React 利用 Canvas 进行绘图

前言

本来是利用 div 容器与 span 实体颜色方块的方式画图,然而在手机浏览器以及 Firefox 下显示会有错位的问题… 只好用 Canvas 画一份了。

因为 React 是直接作用于 DOM 元素,因此需要想办法获取上下文(context)来绘图。

代码

首先是基类 CanvasComponent,任何需要绘图的组件都衍生自此。

其中,除了 data 这个数据属性,其它作为属性(props)传进来的内容将原封不动的传递给 canvas 元素。

// CanvasComponent.tsx
import React, {CanvasHTMLAttributes, Component} from 'react';

export interface ICanvasComponentProps<T> extends CanvasHTMLAttributes<HTMLCanvasElement> {
  data: T;

  // must be present
  width: number;
  height: number;
}

abstract class CanvasComponent<P extends ICanvasComponentProps<T>, S, T> extends Component<P, S> {
  protected canvas?: HTMLCanvasElement;
  protected ctx?: CanvasRenderingContext2D;
  protected drawId: number = 0;

  private setupCanvas = (canvas: HTMLCanvasElement) => {
    this.canvas = canvas;
    if (canvas) {
      this.ctx = canvas.getContext('2d')!;
      this.dispatchDraw();
    }
  };

  render() {
    cancelAnimationFrame(this.drawId);
    this.drawId = requestAnimationFrame(this.dispatchDraw);

    const { data, ...props } = this.props;

    return (
      <canvas {...props} ref={this.setupCanvas} />
    );
  }

  abstract draw(ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement): void;
  private dispatchDraw = () => {
    if (this.ctx && this.canvas) {
      this.draw(this.ctx, this.canvas);
    }
  };
}

export default CanvasComponent;

然后就是制作一个具现组件(implementation),比如绘制占用储存空间的 StorageBar 组件:

import CanvasComponent, {ICanvasComponentProps} from "./CanvasComponent";

interface ICanvasData {
  used: number;
}

interface IPropsCanvas extends ICanvasComponentProps<ICanvasData> {

}

interface IState {

}

class StorageBar extends CanvasComponent<IPropsCanvas, IState, ICanvasData> {
  draw(ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement): void {
    const { data, width, height } = this.props;
    const {
      used
    } = data;

    const w = width - 2;
    const h = height - 2;

    ctx.strokeStyle = '1px solid #000';
    ctx.strokeRect(0, 0, width, height);

    ctx.fillStyle = '#fff';
    ctx.fillRect(1, 1, w, h);

    const usedBar = w * used;
    ctx.fillStyle = '#4d9510';
    ctx.fillRect(1, 1, usedBar, h);
  }
}

export default StorageBar;

最后,在页面调用储存条的显示:

<StorageBar
  className="storage-bar"
  data={{
    used: used / total
  }}
  width={100}
  height={16}
/>

具体效果

实际效果截图

这个效果也可以在 stats.jixun.uk 查看。

Jixun

Jixun

学习与游戏的旅途,各类杂谈。