@bucky24/react-canvas

A library of components that can be used to manipulate a canvas using JSX embedded in react.

Usage no npm install needed!

<script type="module">
  import bucky24ReactCanvas from 'https://cdn.skypack.dev/@bucky24/react-canvas';
</script>

README

React-Canvas

This module is intended to be a library that allows an HTML5 <canvas> element to exist seamlessly with a React application, using React-like components to allow drawing onto the canvas using JSX.

Installation

With NPM

npm install --save @bucky24/react-canvas

With Yarn

yarn add @bucky24/react-canvas

ReactCanvas has a peer dependency on react and prop-types but should be able to use whatever you have installed for your main project. It does require the use of the child context apis, so React 16 is recommended at minimum.

Usage

The module exports a series of components that can be used in JSX. To use, declare a canvas of specific width and height inside your React app, then use the same nesting syntax as the rest of React to draw various elements. The components register and behave (with some exceptions) as normal React components, so most standard behaviors with components should work for them.

Warning

These modules must be inside a Canvas object to work properly, and normal HTML tags cannot be used inside them. So you can't nest ReactCanvas tags, then HTML tags, then more ReactCanvas tags.

Available Elements

Canvas

The root level element. This will actually create a <canvas> canvas tag on the page. However, it will also pass the 2D context object into React context of all its children. This allows the children to draw to the canvas object.

Parameters

Parameter Description
width The width, in pixels, of the resulting canvas
height The height, in pixels, of the resulting canvas
captureAllKeyEvents If this is set to false, any events not originating from the body (which is the default when no input field is selected) will be ignored. If this is set to true (default), all key events will be captured. You should set this to false if you have any dom elements outside of the canvas that need to handle key events.
Children

Accepts multiple children. Children should be valid ReactCanvas elements.

Example
return (<div>
    <Canvas
        width={300}
        height={300}
    >
        {reactCanvasElements}
    </Canvas>
</div>);

Shape

The Shape element is simple-it draws a shape centered around a given point. Note that there have to be at least 3 entries in the points array

Parameters
Parameter Description
x The origin x coord of the shape
y The origin y coord of the shape
points An array containing objects with x and y parameters. This will form the body of the shape. Note that all coords in this array are relative to the x and y given as top level params.
color A hex color code, which determines the color of the shape
fill a boolean value, which determines if the shape is drawn as an outline (false) or a filled in shape (true)
close A boolean value (default true) which determines if the shape's path is closed before drawing, or left empty. Closing the path means a line will be drawn from the last point to the first point.
Example
<Canvas
    width={300}
    height={300}
>
    <Shape
        x={40}
        y={0}
        points={[
            { x: 10, y: 10},
            { x: 100, y: 10 },
            { x: 10, y: 100}
        ]}
        color="#f00"
        fill={true}
    />
</Canvas>

Text

Draws text to the screen at the given coordinates.

Parameters
Parameter Description
x The origin x coord of the text
y The origin y coord of the text
color the color for the text (default black)
font the font for the text (default 12px Arial)
Children

Accepts a single child, which is the text to be displayed

Example
<Canvas
    width={300}
    height={300}
>
    <Text
        x={5}
        y={60}
    >
        Some text here
    </Text>
    <Text
        x={5}
        y={80}
        color="#00f"
        font="18px Times New Roman"
    >
        Blue Text
    </Text>
</Canvas>

Image

The Image element takes care of loading and displaying an image asset to the canvas.

Images are cached after first load, so re-using the same src will not cause the image to be loaded a second time.

Parameters
Parameter Description
x The origin x coord to draw the image at
y The origin y coord to draw the image at
src The URL that the image can be found at. This can also be base-64 encoded image data
width The width to draw the image at
height The height to draw the image at
clip See Image Clipping below
rot Rotation angle in degrees
onLoad Function that is called when the image loads. If the image is already loaded, the function will not be called (note, using this function can cause optimization issues)
Example
<Canvas
    width={300}
    height={300}
>
    <Image
        src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png"
        x={40}
        y={50}
        width={50}
        height={50}
    />
</Canvas>
Base64 Encoded Example
const imageData = "data:image/png;base64,<some base 64 encoded data here>";
return <Canvas
    width={300}
    height={300}
>
    <Image
        src={imageData}
        x={40}
        y={50}
        width={50}
        height={50}
    />
</Canvas>;
Image Clipping

The clip parameter allows drawing of only part of the image. It takes in the following parameters:

Parameter Description
x X coord of where to start the clip
y Y coord of where to start the clip
width The width of the clip
height The height of the clip

Note that these parameters are all translated into image space from draw space. So for example, if I had an image that was 50x50, but I was drawing it like this:

    <Image
        src={src}
        x={40}
        y={50}
        width={200}
        height={200}
        clip={{
            x: 100,
            y: 100,
            width: 50,
            height: 50,
        }}
    />
</Canvas>;

The x coordinate is 50% of the final width of the image, meaning that when it is translated, it translates into half of the image width, so it becomes 25. This is to avoid the developer needing to know the size of the image before it's loaded (and becomes important because the Image component does not expose the image data). So when using clip, just use the width and height that you're passing into the Image component to make your calculations about how much to clip.

You can also observe the imageExample in the source to see how this is used.

Images

The Images element draws multiple images to the screen. This can be helpful when you need to draw a lot of images but don't want to have the overhead of a lot of React components. In general this component behaves exactly like the Image component.

Parameters
Parameter Description
images A list containing objects that conform to the parameters for the Image component
onLoad Function that is called when any images load. If all images are already loaded, the function will not be called. The callback function will be given a single param: the src of the image that was loaded. If the same image is listed multiple times in 'images', the callback will be called multiple times for the same src. Like Image, using this can cause slowness.
Example
<Canvas
    width={300}
    height={300}
>
    <Images
        images={[
            {
                src:"https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png",
                x:40,
                y:50,
                width:50,
                height:50,
            },
        ]}
    />
</Canvas>

Line

The Line element draws a line of a specific color between two given points.

Parameters
Parameter Description
x The origin x coord of the line
y The origin y coord of the line
x2 The destination x coord of the line
y2 The destination y coord of the line
color A hex color code, which determines the color of the line
Example
<Canvas
    width={300}
    height={300}
>
    <Line
        x={40}
        y={0}
        x2={100}
        y2={150}
        color="#f00"
    />
</Canvas>

Rect

The Rect element is just a wrapper around Shape that returns a rectangle drawn between two points.

Parameters
Parameter Description
x The first x coord of the rectangle
y The second y coord of the rectangle
x2 The second x coord of the rectangle
y2 The second y coord of the rectangle
color A hex color code, which determines the color of the rectangle
fill Boolean, indicates if the rectangle should be filled or an outline
Example
<Canvas
    width={300}
    height={300}
>
    <Rect
        x={40}
        y={0}
        x2={100}
        y2={150}
        color="#f00"
        fill={false}
    />
</Canvas>

Circle

Draws a circle with a specific position and radius.

Parameters
Parameter Description
x The x position of the circle center
y The y position of the circle center
radius The radius of the circle
color A hex color code, which determines the color of the circle
fill Boolean, indicates if the circle should be filled or an outline
Example
<Canvas
    width={300}
    height={300}
>
    <Circle
        x={40}
        y={0}
        radius={10}
        color="#f00"
        fill={false}
    />
</Canvas>

Arc

Draws a semi-circle between two angles with a specific position and radius.

Parameters
Parameter Description
x The x position of the arc center
y The y position of the arc center
radius The radius of the arc
startAngle The start angle in radians of the arc
endAngle The eng angle in radians of the arc
sector Boolean, indicates if the drawn shape should be a slice of pie (true) or just the outer part of the circle (false)
color A hex color code, which determines the color of the arc
fill Boolean, indicates if the arc should be filled or an outline
closed Boolean, determines if the arc should close its path (draw a line back to the start) or not
Example
<Canvas
    width={300}
    height={300}
>
    <Arc
        x={40}
        y={0}
        radius={10}
        startAngle={0}
        endAngle={Math.PI}
        color="#f00"
        fill={false}
        sector={false}
    />
</Canvas>

Raw

Sometimes the basic elements contained in react-canvas aren't enough. For example, if you need to do a very complex shape with semi-circles and lines, currently there's no way to to do that, except creating a custom component with access to the raw canvas context.

That's where the Raw component comes in. It takes in a single prop, which is a callback it will call with the context as the first parameter, allowing you to access any low level functions or complex operations you need to.

Parameters
Parameter Description
drawFn A callback that will be called on-render with a single param, which will be the context of the canvas. Note this is not a React context object. but rather an instance of CanvasRenderingContext2D
Example
<Canvas
    width={300}
    height={300}
>
    <Raw drawFn={(context) => {
        // do any low level canvas code here
    }} />
</Canvas>

Clip

Clip takes in parameters that define a rectangle, and a list of children. It will use the canvas context clip method to ensure that any part of those children that falls outside the rectangle is not drawn. This is different from the clip in Image, which determines the section of the source image to display.

Clip uses a render method that bypasses React's render system. This is because the children have to finish rendering before Clip finishes rendering. React can sortof do this, but if any OTHER component renders during that time it will also be clipped, which we don't want (and this happens pretty frequently).

At this point, Clip should be able to handle anything a Canvas element can. This includes custom components. Because it uses the same custom rendering method that Canvas does when doing direct rendering, it is not recommended to use class components when using Clip (the direct rendering method does not handle them in a performant way). If you get any errors using Clip, then make a bug report-it's a bit fragile.

Children

Takes multiple children, see above for limitations and notes on this.

Parameters
Parameter Description
x The start x of the clip rect
y The start y of the clip rect
width The width of the clip rect
height The height of the clip rect
Example
<Canvas
    width={300}
    height={300}
>
    <Clip
        x={100}
        y={100}
        width={100}
        height={100}
    >
        <Text
            x={150}
            y={130}
        >
            Clipped text
        </Text>
    </Clip>
</Canvas>

Container

The Container element acts as a collector. Older versions of React did not allow returning an array of elements from the render function. Because of this, always returning a singular <div> tag was a standard practice. Container takes the place of the div tag for ReactCanvas elements.

For newer versions of React, returning an array or a React Fragment is perfectly acceptable and should work as expected. This component exists for backwards compatability only.

Children

Takes multiple children, must be ReactCanvas elements.

Example
    return <Container>
        <Text
            x={5}
            y={60}
        >
            Blah
        </Text>
        <Shape
            points={[
                { x: 10, y: 10},
                { x: 100, y: 10 },
                { x: 10, y: 100}
            ]}
            color="#f00"
            fill={true}
        />
    </Container>;

Pattern

The Pattern element allows drawing an image in a repeated pattern in a rectangle.

Parameters
Parameter Description
x X position to start the draw at
y Y position to start the draw at
width Width of the rectangle
height Height of the rectangle
src Similar to image, a url, or raw image data to tile
Example
<Canvas
    width={300}
    height={300}
>
    <Pattern
        x={100}
        y={100}
        width={200}
        height={200}
        src="https://website.com/path/to/image"
    />
</Canvas>

Events

Event List

The following events are available:

Event Params
onMouseMove Coords
onMouseDown CoordsWithButton
onMouseUp CoordsWithButton
onKeyDown KeyData
onKeyUp KeyData
onWheel CoordsWithDirection

Coords

Coords is simply an object containing x and y as integers.

CoordsWithButton

CoordsWithButton contains x and y coordinates, as well as a button field that is one of the following:

| Type | | -- | | ButtonTypes.LEFT | | ButtonTypes.MIDDLE | | ButtonTypes.RIGHT |

KeyData

KeyData is an object containing char which is the character of the key pressed, and code which is the key code.

CoordsWithDirection

CoordsWithDirection contains x and y coordinates, as well as an up field, which indicates if the scroll wheel was moved up (true) or down (false)

Handling on the Canvas

The quickest way to handle canvas events is via event handlers directly on the Canvas object. Any component containing a Canvas can listen for any events it emits via callbacks.

class MyComponent extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
        return (<div className={styles.appRoot}>
            <Canvas
                width={300}
                height={300}
                onMove={({ x, y, button }) => {
                    console.log("mouse moved to", data.x, data.y);
                }}
                onMouseDown={({ x, y, button }) => {
                
                }}
                onMouseUp={({ x, y, button }) => {
                
                }}
            >
                { /* some things here */ }
            </Canvas>
        </div>);
    }
};

CanvasComponent

If your components extend CanvasComponent (see below sections), you can handle events as native functions inside the component as React lifecycle methods.

If bounds is set on the child object (containing x, y, width, and height), the second parameter of the callback for all mouse events will be a boolean indicating if the operation took place within those bounds. If no bounds are set, then this boolean is always false.

Note that if you make a component a CanvasComponent but it is not nested inside a Canvas element, it will throw an error to your console because the child context doesn't exist.

Another thing of note: CanvasComponent consumes componentDidMount to do event handling. If you are using componentDidMount in your custom component, be sure to call super.componentDidMount() or else event handling will not work.

Example
import React from 'react';
import { CanvasComponent } from 'react-canvas';

class MyElement extends CanvasComponent {
    constructor(props) {
        super(props);
        
        this.bounds = {
            x: 100,
            y: 100,
            width: 20,
            height: 20
        };
    }
    onMouseMove({ x, y }, overMe) {
        // take some action
    }
    onMouseDown({ x, y, button }, overMe) {
        // take some action
    }
    onMouseUp({ x, y, button }, overMe) {
        // take some action
    }
    onKeyDown({ char, code }) {
        // do something
    }
    onKeyUp({ char, code }) {
        // do something
    }
    onWheel({ x, y, up }, overMe) {
        // take action
    }
    render() {
        // some rendering here
    }
}

export default MyElement;

Raw Event Handling

It is possible to hook into canvas events for your component by using the registerListener and unregisterListener functions on the Canvas context.

Both registerListener and unregisterListener take in two parameters: an EventType, and a closure that will be passed a data object when the event is triggered.

It is recommended that your component call registerListener only once, and that you call unregisterListener when the component is about to unmount, similar to any other listeners in react.

| Event Type | Triggered by | Contents of Data | | -- | -- | -- | | EventTypes.MOVE | The mouse moving across the canvas | An object containing x and y of the mouse on the canvas. May also contain a "touches" key if on a multi-touch device, which contains the x and y of all touches detected | | EventTypes.MOUSE_UP | The mouse being released | An object containing x and y of the mouse on the canvas and a button corresponding to a ButtonType above | | EventTypes.MOUSE_DOWN| The mouse being pressed | An object containing x and y of the mouse on the canvas and a button corresponding to a ButtonType above | | EventTypes.KEY_DOWN | A key being pressed or repeated | An object containing the char value of the key and the key code | | EventTypes.KEY_UP | A key being released | An object containing the char value of the key and the key code | | EventTypes.WHEEL | The scroll wheel being spun | An object containing x and y of the mouse on the canvas, and a boolean "up" which indicates if the wheel is spinning up or down |

Extending

You can easily create your own elements that have access to the canvas context.

Context

The following properties are available from the CanvasContext:

Name Purpose
context The raw canvas context
registerListener Function for event handling. See above for usage
unregisterListener Function for event handling. See above for usage
loadImage Function that takes in a src and a cb function. If the image is already loaded, it will return the img object. If not, the cb function is called when the image is loaded
loadPattern Function that takes in a src and a cb function. If the pattern is already loaded, it will return the canvas pattern object. If not, the cb function is called when the pattern is loaded
forceRenderCount This is the number of times the forceRerender function has been called. This can be used to determine if a render is taking place because an image has loaded (as image loads call forceRerender by default)
width The width of the Canvas
height The height of the Canvas
Example
import React, { useContext } from 'react';
import { CanvasContext } from 'react-canvas';

const MyElement = (props) => {
    const { context } = useContext(CanvasContext);
            
    if (!context) {
        return null;
    }
    context.save();

    // do context things here

    context.restore();
}

export default MyElement;

Rerendering

The canvas context also provides a function forceRerender which will essentially call this.forceUpdate() on the top level canvas. This can be useful in some situations to force the canvas to redraw.

Direct Render (Experimental)

In some cases, it's useful to completely bypass React's rendering (which, as described in the Z Index section, is not always great for Canvas). In order to do this, you can use the customRender flag on the Canvas object. This is experimental, and does not really work very well with Class components, but may be necessary for other experimental features.

    return <Canvas
        width={300}
        height={300}
        customRender={true}
    >
        { children }
    </Canvas>;

Z Index (Experimental)

Sometimes you can run into the situation where some parts of your canvas will redraw and others will not. For example, let's say you're drawing a map, but drawing parts of the UI on top of it. If you have the map in a specific CanvasComponent and UI pieces in other components, when your map redraws, you may find that your UI components do not. This is because React is trying to ease the load on the browser by only updating dom elements that have changed. While this works really well for a dom, where making sure dom elements are rerendered properly is taken care of by the browser, we're dealing with a canvas, where this is not the case.

In order to potentially fix this, you can use z-indexing on your components. What this will do is detect when a component has re-rendered, and at that point, force render every component with a higher z-index.

This feature bypasses React rendering and as such is experimental and may not work as expected. Use at your own risk. Also the feature only pays attention to z-index on the element that caused the redraw and the direct children of the Canvas. So if you have a grandchild that is z-index 5, the child on the Canvas is z-index 2, and the element that triggered the redraw is z-index 4, then that grandchild will not be redrawn.

To use, you must make two changes. First, add a z-index to the components to indicate which ones should be redrawn, as well as set the enableExperimental flag on the Canvas object.

    return <Canvas
        width={300}
        height={300}
        enableExperimental={true}
    >
        <Text
            x={5}
            y={60}
            zIndex={3}
        >
            Blah
        </Text>
        <Shape
            zIndex={2}
            points={[
                { x: 10, y: 10},
                { x: 100, y: 10 },
                { x: 10, y: 100}
            ]}
            color="#f00"
            fill={true}
        />
    </Canvas>;

The following example sets up the Text to be redrawn whenever something with a z-index of less than 3 is redrawn, and the Shape to be redrawn whenever something with a z-index of less than 2 is redrawn. Note that the default z-index is 1

The second thing you must do is indicate which components should trigger a redraw. This can be done on CanvasComponents by calling super.render() inside the render function, and also by calling triggerRedraw on the CanvasContext object, if you have one.

See the overlapExample in the examples directory for more information on how to make this work.

Double Buffering (Experimental)

Double Buffering is the process of rendering to an off-screen canvas, then drawing that canvas as an image onto the main screen.

To use double buffering, pass the doubleBuffer flag to the Canvas object.

Note: Direct Rendering must be enabled for double buffering to have any effect.

renderToImage (Experimental)

React Canvas exports a method, renderToImage, that can take in a series of React Canvas components, and a width and height, then return a data string that represents the image from that canvas.

This bypasses React rendering, so it may not work as expected for some situations.

However, because of this, renderToImage does not need to be called from inside a component's render method.

context is not a required parameter, but it is recommended to pass in the context of the parent Cavas if possible. If you are trying to get the context from a top level component that exports the Canvas element, you can use a ref to the Canvas and call getMyContext to get the context object.

import React from 'react';
import {
    Line,
    renderToImage,
} from 'react-canvas';

const App() {
    const width = 300;
    const height = 100;

    const components = <>
        <Line
            x={10}
            y={10}
            x2={100}
            y2={10}
            color="#888"
        />
        <Line
            x={10}
            y={10}
            x2={10}
            y2={100}
            color="#888"
        />
    </>;

    const imageSource = renderToImage(components, width, height);

    return (<div>
        <img src={imageSource}>
    </div>);
};

export default App;

renderToCanvas (Experimental)

React Canvas exports a method, renderToCanvas, that does basically the same thing as renderToImage (and takes the same parameters), but instead of a base-64 data string, returns a canvas dom element that has had the given elements rendered to it. This is useful if you have images that are loaded from outside of your domain, as the browser will not allow these to be rendered to an image, but it will allow them to be rendered to a canvas.

This canvas can be passed into the src attribute of an Image element to render it.

Like the above method, this bypasses React rendering, so it may not work as expected for some situations.

blendImage

The blendImage function allows an existing image to be manipulated on the fly, generating a new image (as a Canvas) that can be drawn.

Parameters

| Name | Description | -- | -- | | src | The source (url or base64 string) to blend | | operations | An array of BlendOperations. If empty, the original src is returned |

Usage

blendImage(sampleImage, [
    {
        type: BLEND_TYPE.COLOR_SWAP,
        from: '#ffffff',
        to: '#0000ff',
    },
    {
        type: BLEND_TYPE.COLOR_SWAP,
        from: '#000000',
        to: '#00ff00',
    },
]).then((newImg) => {
    // newImg is a Canvas containing the rendered image, and can now be stored for drawing.
});

BlendOperation

Represents an operation that can be performed on an image

| Name | Type | Description | -- | -- | -- | | type | BLEND_TYPE | Describes what operation to perform | | * | * | Other params determined by BLEND_TYPE, see below |

BLEND_TYPE

COLOR_SWAP

BLEND_TYPE.COLOR_SWAP allows swapping one color in an image completely for another color. It expects the following parameers on the BlendOperation:

| Name | Type | Description | -- | -- | -- | | from | Hex Color | A string containing the RGB hex code to look for | | to | Hex Color | A stirng containing the RGB hex code to use as a replacement