skip to main content

Jixun's Blog 填坑还是开坑,这是个好问题。

Draw with canvas in React JS

Preface #

Initially I did not plan to use canvas for this task, but to use a div with block of span with solid colours.

However, for some bazaar reasons, that would cause the content to offset 1 pixel towards right and bottom.

Since React works directly with DOM elements, I would need to get the canvas context to draw.

The code #

Firstly, is the base class CanvasComponent, where all drawing component will derive from here.

Where except the data property, other props will be passed straightaway to the underlaying canvas element.

// 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;

Then the actual component to implement the draw method, for example the StorageBar component to illustrate the storage usage:

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;

Finally, use it elsewhere like so:

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

How it looks #

Actual Screenshot

You can also see it in action at stats.jixun.uk.