The C-Files: Art & Interfaces

Sean Donohue
The Startup
Published in
8 min readDec 8, 2020

--

Building a Free-Draw interface in React with React Konva

I recently built a React app that features canvas. For any reader who is not familiar, a “canvas” is a type of HTML element that is extremely useful for rendering graphics and animations. Its corresponding API makes it possible for users to create a wide variety of images, and there are many existing libraries out there that simplify and build features on top of the core API.

In this tutorial, I will be focusing on one particular library, React-Konva, which is part of the larger Konva Framework.

React-Konva allows you to interact with a canvas element in a way that is cleaner and more intuitive to developers who are used to using React. I for one was struggling to design an interface with the vanilla canvas API that would allow the sort of flexibility that I was looking for, and this ended up being a fantastic tool for my case. The really awesome thing about using Konva, is that the components you can import from their library can take props, which will allow a degree of customization with much less than code than with other canvas libraries.

Before we get started building, I’ll go over the components you need in order to setup for free-drawing. First, we’ll need to create a react app in a fresh directory, then we need to install the library by using this command.

npm install react-konva konva — save

We’ll need to import the following from ‘react-konva’ in our App.js file.

  1. Stage
  2. Layer
  3. Rect
  4. Line

So now our basic setup will look something like this…

But what is the use of each of these and how do they interact with the Canvas API?

A Stage is Konva’s abstraction of an HTML canvas element. It is in effect, the canvas that we will be working with, and it functions in a similar way to the base Canvas API.

A Layer is what functions as a “rendering context” for the stage. All canvas elements require a rendering context in order for a specific event to actually trigger visible changes on the canvas itself.

A Rect is a graphic that is a 2d rectangle. In our case, it used to setup a white backdrop in order for us to see changes against a different colored background style.

A Line is the component that makes a free-drawing app possible. Line’s in Konva require an array of points that are registered like

[x1, y1, x2, y2, x3, y3…]

…and lines require an attribute called tension, which controls how curvy the connection between points looks. The value will be between zero and one; a value closer to one results in a more curvy look, and a value closer to zero results in a more discrete look. Between two sets of coordinates, these changes will not be visible, but when plotting many tens or hundreds points in sequence, you will begin to see what a difference.

We are also going to utilize the useRef hook so that we can toggle drawing based on mouse events. We do not want to be using the useState hook for toggling here, because unlike the values for the state hook, the values that we store as refs will not change or be set to default across renders. AND when we change the value assigned to a useRef hook, it will not cause a page re-render. This is very important because drawing on the Stage is controlled by refs changing on mouse events. You probably should not use the state hook for this purpose because it will cause more re-renders than is necessary and we want to minimize this for performance.

Ok, so now that we have the let’s get started!

In order to get setup, we need to need make sure our components are rendered in the following order, before we start adding other features. The way Konva Stage and Layer components work is by wrapping other components. So the Stage will wrap a Layer (the canvas will grab its rendering context) and Layer will wrap the Rect and all of the Lines (the rendering context renders the canvas objects). Our stage will work like a regular canvas element, in that we need to give it width and height attributes. Since we are working against a darker backdrop, we are going to import and use a Rect component inside the layer that will fill close to the same dimensions as the canvas. This all happening inside the render portion of the App component.

Render

We will then set the ‘fill’ attribute on it to ‘white’ so that we have a white backdrop.

Localhost

Now we need an interface for the lines. Luckily for us, the Konva documentation has a fantastic example for how to do this here. This interface responds to mousedown, mouseover, and mouseup events. We will be using their demonstration to build an interface.

On mousedown, we will be changing the ref we declared to “true”. The method getContext allows us to target the canvas, and getMousePointer position allows us to get the x and y coordinates of where the mouse is currently in that canvas. That initial point is the starting position of what will become a new Line component. When we set lines here, we are adding that line to the array of already existing lines held in component state. It is in this line object, defined by the points array, that we can also add other attributes that Line components can take. We should do that here, because when we build out features (changing color, brush width, etc.) changes to any value in state only effect the newly created line, but doesn’t effect any of the other lines.

onMouseDown for Stage

On mousemove, we also need to grab the context and position of the mouse cursor. As the mouse moves, we will grab the line that was made when we performed a mouse down event. Since as we mentioned earlier that lines are defined by a series of points, we will add each new pair of coordinates to that line object. As we draw, that line object is growing as more and more coordinates are being added to its point object. We can see these changes as if in real-time. Yet surprisingly, that line in state is still just that object with the one set of coordinates that we made on the mouse down event. We need to replace it with the copy of the same line that we added more coordinate pairs too. Then when setLines triggers a re-render, that modified line will be added to state and will be shown on the re-render. And on mouseup, we will set the useRef to false which stops rendering on the canvas.

onMouseMove and onMouseUp for Stage

Pretty cool, stuff from the Konva contributors! But now that we have a good interface for drawing, how can we add settings so that we aren’t just drawing black lines?

Adding some features

Let’s add a few inputs that will change values that we hold in state. Remember though, that as we change values that are held in state, we trigger a re-render of the component. We don’t want to hold values in component state that we are going to pass as props to the Line. If we did that, and we wanted to change something like color, every Line’s color would be changed.

Remember that one very useful feature of the useState hook over class-based state, is that you can more or less, hold many different data types as a value. When we are calling setLines here, what we are doing is adding an object with points which has the starting points for the new Line created on mouse down.

In f()mouseDown

WE ARE NOT RENDERING THE ACTUAL LINE COMPONENT YET! Instead, we hold all the data we want for a Line component to have in an object (stroke, strokeWidth, tension, ….) When it comes to time to render, we map over the array of objects and add Line Components inside the Layer Component. If we want to make dynamic changes, we target the values INSIDE each line object. Why? Every Line component has its data stored as an object, which are all held in an array (in state). If we were to change state for something like color, that state value is stored inside the object on creation and stays there after. So we can use this object when making our free draw to then pass props to a corresponding Line component.

In f()mouseDown

Each component from React-Konva can accept certain props for rendering. The API docs have all the info for which components accept what. In our case, Line components need tension and points as props, and they can accept many others as well. We use the values we store in the objects to set props with values. Since each line object holds data about itself, we do not have to worry about triggering re-renders when state values update. All the coordinates are still saved in the object.

In Render

If we wanted to do this with class-based state, we would essentially have to recreate the entire state object, just to update one key with a nested object. Depending on how many features you want to add, this could get messy very quickly. With useState, you just have to worry about recreating whatever values like arrays and objects are being held.

If we now add an JSX input element with a type attribute of ‘color’ we can spice things up a bit

Change Line Color

Phew! That was a lot! I hope this tutorial serves you well if you wish to dive into Konva or Canvas. I want to extend my thanks over to the talented contributors working on Konva and Anton Lavrenov, who is actively maintaining the Konva framework. Happy creating!

--

--

Sean Donohue
The Startup

Aspiring web developer…who knows a lot about wine