DEV Community

Cover image for Guide to canvas manipulation with React Konva
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Guide to canvas manipulation with React Konva

Written by John Au-Yeung✏️

React Konva is a tool that allows us to manipulate the canvas. It lets us easily create shapes without calculating where each point will be, and it has lots of built-in shapes and animation features we can use to create more interactive canvases.

React Konva is available in the form of a Node package. We can install it by running:

npm install react-konva konva --save
Enter fullscreen mode Exit fullscreen mode

Drawing basic shapes

With React Konva, we can create a canvas with its Stage component, which has one or more Layer components nested inside.

Inside each Layer, we can put in whatever shape we want. React Konva comes with shapes such as rectangles, circles, ellipses, lines, images, text, stars, labels, SVG, and polygons.

We can create a canvas and add a rectangle with a shadow as follows:

import React from "react";
import { Stage, Layer, Rect } from "react-konva";

export default function App() {
  return (
    <Stage width={window.innerWidth} height={window.innerHeight}>
      <Layer>
        <Rect
          x={20}
          y={50}
          width={100}
          height={100}
          fill="red"
          shadowBlur={5}
        />
      </Layer>
    </Stage>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the code above, we referenced the Stage component, which creates the canvas. The width is set to the browser tab width, and the height is set to the browser tab height.

Then we put the rectangle inside the canvas by using the Rect component that comes with React Konva. x and y sets the position of the top left corner; width and height set the dimensions; and fill sets the fill color. shadowBlur lets us adjust the shadow’s width in number of pixels.

In contrast, to create a rectangle with plain JavaScript, we have to do more work:

const canvas = document.querySelector("canvas");
canvas.style.width = '500';
canvas.style.height = '500';
if (canvas.getContext) {
  const ctx = canvas.getContext('2d');
  ctx.rect(20, 50, 100, 100);
  ctx.shadowColor = 'gray';
  ctx.shadowBlur = 10;
  ctx.shadowOffsetX = 10;
  ctx.shadowOffsetY = 10;
  ctx.fillStyle = "red";
  ctx.fill();
}
Enter fullscreen mode Exit fullscreen mode

We have to set the parameters for the shadow by setting each property individually rather than just passing in one prop. Also, the canvas context only comes with methods for drawing lines and rectangles. This means that any other shapes would be very difficult to draw without some library like React Konva.

Similarly, we can draw circles by replacing Rect with Circle. Here, the x and y props are the coordinates of the center of the circle. We can write the following to create a red circle with a shadow:

import React from "react";
import { Stage, Layer, Circle } from "react-konva";

export default function App() {
  return (
    <Stage width={window.innerWidth} height={window.innerHeight}>
      <Layer>
        <Circle
          x={100}
          y={100}
          width={100}
          height={100}
          fill="red"
          shadowBlur={5}
        />
      </Layer>
    </Stage>
  );
}
Enter fullscreen mode Exit fullscreen mode

Regular polygons are just as easy to draw with React Konva:

import React from "react";
import { Stage, Layer, RegularPolygon } from "react-konva";

export default function App() {
  return (
    <Stage width={window.innerWidth} height={window.innerHeight}>
      <Layer>
        <RegularPolygon
          sides={10}
          x={100}
          y={100}
          width={100}
          height={100}
          fill="red"
          shadowBlur={5}
        />
      </Layer>
    </Stage>
  );
}
Enter fullscreen mode Exit fullscreen mode

Drawing custom shapes

We can draw custom shapes by using the Shape component. Its sceneFunc prop takes a function with the canvas context object as the first parameter and the shape method as the second parameter. The shape object is a Konva-specific method used in the fillStrokeShape method.

For instance, we can draw our own shape as follows:

import React from "react";
import { Stage, Layer, Shape } from "react-konva";

export default function App() {
  return (
    <Stage width={window.innerWidth} height={window.innerHeight}>
      <Layer>
        <Shape
          sceneFunc={(context, shape) => {
            context.beginPath();
            context.moveTo(0, 50);
            context.bezierCurveTo(100, 200, 100, 400, 200, 0);
            context.closePath();
            context.fillStrokeShape(shape);
          }}
          fill="#00D2FF"
          stroke="black"
          strokeWidth={4}
        />
      </Layer>
    </Stage>
  );
}
Enter fullscreen mode Exit fullscreen mode

The sceneFunc props let us access the canvas directly, and we can draw whatever shape we want. Then, we call fillStrokeSgape on the canvas with the shape function passed in as a callback to fill the shape with the given color and draw the strokes. We can also pass in other props like fill and stroke to set the fill and stroke color.

LogRocket Free Trial Banner

Event handling

React Konva shapes can listen to events and then respond to the events accordingly. For instance, it’s very easy to make a shape draggable by adding the draggable prop to it and then attach event listeners for the dragstart and dragend events:

import React from "react";
import { Stage, Layer, Circle } from "react-konva";

export default function App() {
  const handleDragStart = e => {
    e.target.setAttrs({
      shadowOffset: {
        x: 15,
        y: 15
      },
      scaleX: 1.1,
      scaleY: 1.1
    });
  };
  const handleDragEnd = e => {
    e.target.to({
      duration: 0.5,
      easing: Konva.Easings.ElasticEaseOut,
      scaleX: 1,
      scaleY: 1,
      shadowOffsetX: 5,
      shadowOffsetY: 5
    });
  };

  return (
    <Stage width={window.innerWidth} height={window.innerHeight}>
      <Layer>
        <Circle
          x={100}
          y={100}
          width={100}
          height={100}
          fill="red"
          shadowBlur={5}
          draggable
          onDragStart={handleDragStart}
          onDragEnd={handleDragEnd}
        />
      </Layer>
    </Stage>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the code above, we have the handleDragStart function to set the shadow to a new color and offset as the original circle. We also expanded the shape slightly to indicate that it’s being dragged.

We pass the handleDragStart function into the onDragStart prop, and likewise, we defined the handleDragEnd function to the onDragEnd prop. In there, we move the shape that’s being dragged to the new position by calling the to method of the shape object that’s being dragged, which is the value of e.target.

We set the easing to the built-in Konva.Easings.ElasticEaseOut value, which makes it look bouncy as it’s being dragged and dropped. The easing changes the speed that the shape moves as a function of time. Without it, everything would move at constant speed, which is neither natural nor interesting.

Ease out means that it moves quickly at first and then slows down afterward.

Adding images

We can add an image to the canvas with React Konva’s Image component. To add an image, we create a window.Image instance, set the URL of our image to the src attribute, and then pass the image object as the value of the image prop to the Image component as follows:

import React, { useEffect, useState } from "react";
import { Stage, Layer, Image } from "react-konva";

export default function App() {
  const [image, setImage] = useState(new window.Image());

  useEffect(() => {
    const img = new window.Image();
    img.src =
      "https://images.unsplash.com/photo-1531804055935-76f44d7c3621?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=334&q=80";
    setImage(img);
  }, []);

  return (
    <Stage width={window.innerWidth} height={window.innerHeight}>
      <Layer>
        <Image x={100} y={200} image={image} />
      </Layer>
    </Stage>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the code above, we used the useEffect Hook to load the image when our app first loads by passing an empty array to the second argument of useEffect.

After adding the image, we can use some built-in effects from React Konva to add some effects to our images. For instance, we can use the built-in blur effect to blur the image that we loaded onto the canvas as follows:

import React, { useEffect, useState, useRef } from "react";
import { Stage, Layer, Image } from "react-konva";
import Konva from "konva";

export default function App() {
  const [image, setImage] = useState(new window.Image());
  const imageRef = useRef();

  useEffect(() => {
    const img = new window.Image();
    img.crossOrigin = "Anonymous";
    img.src =
      "https://images.unsplash.com/photo-1531804055935-76f44d7c3621?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=334&q=80";
    setImage(img);
  }, []);

  useEffect(() => {
    if (image) {
      imageRef.current.cache();
      imageRef.current.getLayer().batchDraw();
    }
  }, [image]);

  return (
    <Stage width={window.innerWidth} height={window.innerHeight}>
      <Layer>
        <Image
          blurRadius={10}
          filters={[Konva.Filters.Blur]}
          x={100}
          y={200}
          image={image}
          ref={imageRef}
        />
      </Layer>
    </Stage>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the code above, we loaded an image, then we added the filters prop with the Konva.Filters.Blur object to the array. It’s an array, which means we can apply more than one effect at once.

We need the second useEffect callback to apply the filter. It watches when the image loads, and then redraws the image with the filter applied when it’s loaded.

It references the imageRef that we set the image to. Then we call imageRef.current.cache(); to cache the original image, and imageRef.current.getLayer().batchDraw(); redraws the image with the filter applied.

Resizing images with Transformer

In React Konva, transformer objects let users resize images by adding handles to the shape being transformed and resizing as we do with most photo editing apps.

Adding the transformation feature is a bit complex. We’ve to create a component that has the React Konva shape, then make it draggable. Next, we have to add the onTransformationEnd prop with a function that scales the shape by the factor that the user transforms the shape to and set that as the width and height.

Besides the shape component, we have to add the Transformer component as a sibling to add the handles to let users transform the shape. For instance, we write the following to create two circles and then show the handles to let users stretch and shrink the shapes as follows:

import React from "react";
import { Stage, Layer, Circle, Transformer } from "react-konva";

const Circ = ({ shapeProps, isSelected, onSelect, onChange }) => {
  const shapeRef = React.useRef();
  const trRef = React.useRef();

  React.useEffect(() => {
    if (isSelected) {
      trRef.current.setNode(shapeRef.current);
      trRef.current.getLayer().batchDraw();
    }
  }, [isSelected]);

  return (
    <React.Fragment>
      <Circle
        onClick={onSelect}
        ref={shapeRef}
        {...shapeProps}
        draggable
        onDragEnd={e => {
          onChange({
            ...shapeProps,
            x: e.target.x(),
            y: e.target.y()
          });
        }}
        onTransformEnd={e => {
          const node = shapeRef.current;
          const scaleX = node.scaleX();
          const scaleY = node.scaleY();

          node.scaleX(1);
          node.scaleY(1);
          onChange({
            ...shapeProps,
            x: node.x(),
            y: node.y(),
            width: Math.max(5, node.width() * scaleX),
            height: Math.max(node.height() * scaleY)
          });
        }}
      />
      {isSelected && (
        <Transformer
          ref={trRef}
          boundBoxFunc={(oldBox, newBox) => {
            if (newBox.width < 5 || newBox.height < 5) {
              return oldBox;
            }
            return newBox;
          }}
        />
      )}
    </React.Fragment>
  );
};

const initialCircles = [
  {
    x: 100,
    y: 100,
    width: 100,
    height: 100,
    fill: "blue",
    id: "circ1"
  },
  {
    x: 150,
    y: 150,
    width: 100,
    height: 100,
    fill: "green",
    id: "circ2"
  }
];

const App = () => {
  const [circles, setCircles] = React.useState(initialCircles);
  const [selectedId, selectShape] = React.useState(null);

  return (
    <Stage
      width={window.innerWidth}
      height={window.innerHeight}
      onMouseDown={e => {
        const clickedOnEmpty = e.target === e.target.getStage();
        if (clickedOnEmpty) {
          selectShape(null);
        }
      }}
    >
      <Layer>
        {circles.map((circ, i) => {
          return (
            <Circ
              key={i}
              shapeProps={circ}
              isSelected={circ.id === selectedId}
              onSelect={() => {
                selectShape(circ.id);
              }}
              onChange={newAttrs => {
                const circs = circles.slice();
                circs[i] = newAttrs;
                setCircles(circs);
              }}
            />
          );
        })}
      </Layer>
    </Stage>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

In the code above, we have the Circ component that has the shape, the Circle, and the Transformer component as we described above.

The onDragEnd handler enables the dragging, as we did in the earlier drag and drop example. The onTransformEnd handler has the function to resize the shape to the new width and height after the user is done dragging.

The Transformer component has handles for resizing the circle, which is on when the isSelected prop is set to true. We set that when we click the shape in the onSelect handler of App. onSelect is run when a circle is clicked, which is then used to set isSelected for the circle that’s clicked to true.

Basic animation

We can add some basic animation effects for actions like dragging shapes around by scaling the dimensions by random factors.

For example, we can write the following code to do that:

import React from "react";
import { Stage, Layer, Circle } from "react-konva";

export default function App() {
  const circ = React.useRef();
  const changeSize = () => {
    circ.current.to({
      scaleX: Math.random() + 0.9,
      scaleY: Math.random() + 0.8,
      duration: 0.2
    });
  };

  return (
    <Stage width={window.innerWidth} height={window.innerHeight}>
      <Layer>
        <Circle
          x={100}
          y={100}
          width={100}
          height={100}
          fill="red"
          shadowBlur={5}
          draggable
          ref={circ}
          onDragStart={changeSize}
          onDragEnd={changeSize}
        />
      </Layer>
    </Stage>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the code above, we have the changeSize function, which is called when either the dragstart or dragend events are triggered.

Note that we have to use use the current property to get the DOM object to call the to method.

Conclusion

With React Konva, we can draw many kinds of shapes with much less effort than using plain JavaScript. We can also add features to let users transform shapes and drag shapes around with ease. This isn’t available in the standard JavaScript canvas library.

Even better, we can add Transformers to let users change the size of the shape like they do in photo editors. Adding images with effects is also easy with the built-in Image component and filters.


Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

Alt Text

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — start monitoring for free.


The post Guide to canvas manipulation with React Konva appeared first on LogRocket Blog.

Top comments (1)

Collapse
 
core2duo12 profile image
core_2_duo

Nice one. Love this