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. | |
| // So determine size/layout, 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. | |
| export class Canvas extends React.Component { | |
| constructor(props) { | |
| super(props); | |
| this.fitToZoomAndPixelRatio = this.fitToZoomAndPixelRatio.bind(this); | |
| this.draw = this.draw.bind(this); | |
| this.state = { | |
| width: 1, | |
| height: 1, | |
| ratio: window.devicePixelRatio || 1, | |
| 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 canvas. | |
| this.setResize = debounce(resize, 200); | |
| } | |
| componentDidMount() { | |
| this.fitToZoomAndPixelRatio(); | |
| window.addEventListener('resize', this.setResize); | |
| } | |
| componentWillUnmount() { | |
| window.removeEventListener('resize', this.setResize); | |
| } | |
| componentDidUpdate(prevProps, prevState) { | |
| // if resizing was not true both before the update, | |
| // but is now, then the canvas has been unmounted | |
| if (!prevState.resizing && this.state.resizing) { | |
| this.fitToZoomAndPixelRatio(); | |
| } | |
| if (!this.state.resizing && !prevProps.loop) { | |
| this.draw(); | |
| } | |
| } | |
| // 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 | |
| fitToZoomAndPixelRatio() { | |
| 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 }); | |
| } | |
| // Relies on a ref to a DOM element, so only call | |
| // when canvas element has been rendered! | |
| draw() { | |
| const { width, height, ratio, resizing } = this.state; | |
| if (!resizing) { | |
| 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() { | |
| // Canvas layouting is a bit buggy and inconsistent | |
| // across browsers. So, to make it dependent on the | |
| // layout of the parent container, we must completely | |
| // remove the node when resizing. After calculations | |
| // are done, we mount it again at the proper dimensions | |
| let canvas = null; | |
| if (!this.state.resizing) { | |
| canvas = (<canvas | |
| ref='canvas' | |
| width={this.state.width} | |
| height={this.state.height} | |
| style={{ | |
| width: '100%', | |
| height: '100%', | |
| }} /> | |
| ); | |
| } | |
| return ( | |
| <div | |
| ref='view' | |
| className={this.props.className} | |
| style={this.props.style}> | |
| {canvas} | |
| </div> | |
| ); | |
| } | |
| } | |
| 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?