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
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> ); }
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(); }
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> ); }
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> ); }
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> ); }
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.
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> ); }
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.
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> ); }
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> ); }
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.
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;
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
.
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> ); }
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.
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowHandle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
Design React Native UIs that look great on any device by using adaptive layouts, responsive scaling, and platform-specific tools.
Angular’s two-way data binding has evolved with signals, offering improved performance, simpler syntax, and better type inference.
4 Replies to "Guide to canvas manipulation with React Konva"
how can i download the stage?
Saved Life, 🙂
Thanks!
Very useful! Thanks sr.