Draw with canvas in React JS
- 中
- en
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
You can also see it in action at stats.jixun.uk.