Last active
July 29, 2023 01:44
-
-
Save JobLeonard/987731e86b473d42cd1885e70eed616a to your computer and use it in GitHub Desktop.
A react component that wraps and autosizes a canvas element
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, {PropTypes} from 'react'; | |
import { debounce } from 'lodash'; | |
// A simple helper component, wrapping retina logic for canvas and | |
// auto-resizing the canvas to fill its parent container. | |
// To determine size/layout, we just use CSS on the div containing | |
// the Canvas component (we're using this with flexbox, for example). | |
// Expects a "paint" function that takes a "context" to draw on | |
// Whenever this component updates it will call this paint function | |
// to draw on the canvas. For convenience, pixel dimensions are stored | |
// in context.width, context.height and contex.pixelRatio. | |
class CanvasEnhancer extends React.Component { | |
constructor(props) { | |
super(props); | |
this.draw = this.draw.bind(this); | |
// Attach helper functions to context prototype | |
let prototype = CanvasRenderingContext2D.prototype; | |
if (!prototype.circle) { | |
prototype.circle = function (x, y, radius) { | |
this.moveTo(x + radius, y); | |
this.arc(x, y, radius, 0, 2 * Math.PI); | |
}; | |
} | |
if (!prototype.textSize) { | |
prototype.textSize = function (size = 10) { | |
// will return an array with [ size, font ] as strings | |
const fontArgs = this.font.split(' '); | |
const font = fontArgs[fontArgs.length - 1]; | |
switch (typeof size) { | |
case 'number': | |
this.font = size + 'px ' + font; | |
break; | |
case 'string': | |
this.font = size + font; | |
break; | |
} | |
}; | |
} | |
if (!prototype.textStyle) { | |
prototype.textStyle = function (fill = 'black', stroke = 'white', lineWidth = 2) { | |
this.fillStyle = fill; | |
this.strokeStyle = stroke; | |
this.lineWidth = lineWidth; | |
}; | |
} | |
if (!prototype.drawText) { | |
prototype.drawText = function (text, x, y) { | |
this.strokeText(text, x, y); | |
this.fillText(text, x, y); | |
}; | |
} | |
} | |
// Make sure we get a sharp canvas on Retina displays | |
// as well as adjust the canvas on zoomed browsers | |
// Does NOT scale; painter functions decide how to handle | |
// window.devicePixelRatio on a case-by-case basis | |
componentDidMount() { | |
const view = this.refs.view; | |
const ratio = window.devicePixelRatio || 1; | |
const width = (view.clientWidth * ratio) | 0; | |
const height = (view.clientHeight * ratio) | 0; | |
const resizing = false; | |
this.setState({ width, height, ratio, resizing }); | |
} | |
componentDidUpdate(prevProps) { | |
if (!prevProps.loop) { | |
this.draw(); | |
} | |
} | |
// Relies on a ref to a DOM element, so only call | |
// when canvas element has been rendered! | |
draw() { | |
if (this.state) { | |
const { width, height, ratio } = this.state; | |
const canvas = this.refs.canvas; | |
let context = canvas.getContext('2d'); | |
// store width, height and ratio in context for paint functions | |
context.width = width; | |
context.height = height; | |
context.pixelRatio = ratio; | |
// should we clear the canvas every redraw? | |
if (this.props.clear) { context.clearRect(0, 0, canvas.width, canvas.height); } | |
this.props.paint(context); | |
} | |
// is the provided paint function an animation? (not entirely sure about this API) | |
if (this.props.loop) { | |
window.requestAnimationFrame(this.draw); | |
} | |
} | |
render() { | |
// The way canvas interacts with CSS layouting is a bit buggy | |
// and inconsistent across browsers. To make it dependent on | |
// the layout of the parent container, we only render it after | |
// mounting, after CSS layouting is done. | |
const canvas = this.state ? ( | |
<canvas | |
ref='canvas' | |
width={this.state.width} | |
height={this.state.height} | |
style={{ | |
width: '100%', | |
height: '100%', | |
}} /> | |
) : null; | |
return ( | |
<div | |
ref='view' | |
className={this.props.className ? this.props.className : 'view'} | |
style={this.props.style}> | |
{canvas} | |
</div> | |
); | |
} | |
} | |
CanvasEnhancer.propTypes = { | |
paint: PropTypes.func.isRequired, | |
clear: PropTypes.bool, | |
loop: PropTypes.bool, | |
className: PropTypes.string, | |
style: PropTypes.object, | |
}; | |
// This pattern turns out to be generic enough to | |
// warrant its own component | |
export class RemountOnResize extends React.Component { | |
constructor(props) { | |
super(props); | |
this.state = { resizing: true }; | |
const resize = () => { this.setState({ resizing: true }); }; | |
// Because the resize event can fire very often, we | |
// add a debouncer to minimise pointless | |
// (unmount, resize, remount)-ing of the child nodes. | |
this.setResize = debounce(resize, 500); | |
} | |
componentDidMount() { | |
window.addEventListener('resize', this.setResize); | |
this.setState({ resizing: false }); | |
} | |
componentWillUnmount() { | |
window.removeEventListener('resize', this.setResize); | |
} | |
componentDidUpdate(prevProps, prevState) { | |
if (!prevState.resizing && this.state.resizing) { | |
this.setState({ resizing: false }); | |
} | |
} | |
render() { | |
return this.state.resizing ? null : this.props.children; | |
} | |
} | |
RemountOnResize.propTypes = { | |
className: PropTypes.string, | |
style: PropTypes.object, | |
children: PropTypes.node, | |
}; | |
export const Canvas = function (props) { | |
return ( | |
<RemountOnResize | |
/* Since canvas interferes with CSS layouting, | |
we unmount and remount it on resize events */ | |
> | |
<CanvasEnhancer | |
paint={props.paint} | |
clear={props.clear} | |
loop={props.loop} | |
className={props.className} | |
style={props.style} | |
/> | |
</RemountOnResize> | |
); | |
}; | |
Canvas.propTypes = { | |
paint: PropTypes.func.isRequired, | |
clear: PropTypes.bool, | |
loop: PropTypes.bool, | |
className: PropTypes.string, | |
style: PropTypes.object, | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Will this work for drawing an image on the canvas? e.g. context.drawImage(base_image, 0, 0);
I don't understand why the prototype had to be extended with circle, testSize, etc. Does it need a drawImage?