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.

Creative Commons Licence This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Comments