width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const rect1 = new Konva.Rect({
x: 20,
y: 20,
width: 100,
height: 50,
fill: 'green',
stroke: 'black',
strokeWidth: 4
});
layer.add(rect1);
const rect2 = new Konva.Rect({
x: 150,
y: 40,
width: 100,
height: 50,
fill: 'red',
shadowBlur: 10,
cornerRadius: 10
});
layer.add(rect2);
const rect3 = new Konva.Rect({
x: 50,
y: 120,
width: 100,
height: 100,
fill: 'blue',
cornerRadius: [0, 10, 20, 30]
});
layer.add(rect3);
stage.add(layer);
```
```js
import { Stage, Layer, Rect } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
```js
```
**Next steps:**
- [Add events to shapes →](/docs/events/Binding_Events.html)
- [Make shapes draggable →](/docs/drag_and_drop/Drag_and_Drop.html)
- [Add resize and rotate handles →](/docs/select_and_transform/Basic_demo.html)
- [Animate shape properties →](/docs/animations/Create_an_Animation.html)
- [Konva.Rect API Reference →](/api/Konva.Rect.html)
---
# HTML5 Canvas Shape Events
> Learn how to bind event listeners to HTML5 Canvas shapes with Konva.js. Handle click, dblclick, mouseover, mouseout, mousemove, and more.
Source: https://konvajs.org/docs/events/Binding_Events.html
To detect shape events with Konva, we can use the `on()` method to bind event handlers to a node.
The `on()` method requires an event type and a function to be executed when the event occurs.
Mouse events: `mouseover`, `mouseout`, `mouseenter`, `mouseleave`, `mousemove`, `mousedown`, `mouseup`, `wheel`, `click`, `dblclick`.
Touch events: `touchstart`, `touchmove`, `touchend`, `tap`, `dbltap`.
Pointer events: `pointerdown`, `pointermove`, `pointereup`, `pointercancel`, `pointerover`, `pointerenter`, `pointerout`,`pointerleave`, `pointerclick`, `pointerdblclick`.
Drag events: `dragstart`, `dragmove`, and `dragend`.
Transform events: `transformstart`, `transform`, `transformend`.
**Instructions: Mouseover and mouseout of the triangle, and mouseover, mouseout, mousedown, and mouseup over the circle.**
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
const text = new Konva.Text({
x: 10,
y: 10,
fontFamily: 'Calibri',
fontSize: 24,
text: '',
fill: 'black',
});
const triangle = new Konva.RegularPolygon({
x: 80,
y: 120,
sides: 3,
radius: 80,
fill: '#00D2FF',
stroke: 'black',
strokeWidth: 4,
});
const circle = new Konva.Circle({
x: 230,
y: 100,
radius: 60,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
function writeMessage(message) {
text.text(message);
}
triangle.on('mouseout', () => {
writeMessage('Mouseout triangle');
});
triangle.on('mousemove', () => {
const mousePos = stage.getPointerPosition();
writeMessage('x: ' + mousePos.x + ', y: ' + mousePos.y);
});
circle.on('mouseover', () => {
writeMessage('Mouseover circle');
});
circle.on('mouseout', () => {
writeMessage('Mouseout circle');
});
circle.on('mousedown', () => {
writeMessage('Mousedown circle');
});
circle.on('mouseup', () => {
writeMessage('Mouseup circle');
});
layer.add(triangle);
layer.add(circle);
layer.add(text);
stage.add(layer);
````
```jsx
import { Stage, Layer, RegularPolygon, Circle, Text } from 'react-konva';
import { useRef, useState } from 'react';
const App = () => {
const [message, setMessage] = useState('');
const stageRef = useRef();
const writeMessage = (text) => {
setMessage(text);
};
return (
writeMessage('Mouseout triangle')}
onMousemove={() => {
const mousePos = stageRef.current.getPointerPosition();
writeMessage('x: ' + mousePos.x + ', y: ' + mousePos.y);
}}
/>
writeMessage('Mouseover circle')}
onMouseout={() => writeMessage('Mouseout circle')}
onMousedown={() => writeMessage('Mousedown circle')}
onMouseup={() => writeMessage('Mouseup circle')}
/>
);
};
export default App;
````
```html
```
---
# HTML5 Canvas Drag and Drop Tutorial
> Learn how to add drag and drop to HTML5 Canvas shapes with Konva.js. Make any shape draggable with a single property and handle drag events.
Source: https://konvajs.org/docs/drag_and_drop/Drag_and_Drop.html
To drag and drop shapes with Konva, we can set the `draggable` property
to true when we instantiate a shape, or we can use the `draggable()` method.
The `draggable()` method enables drag and drop for both desktop and mobile
applications automatically.
To detect drag and drop events with Konva, we can use the `on()` method to
bind `dragstart`, `dragmove`, or `dragend` events to a node.
The `on()` method requires an event type and a function to be executed when the event occurs.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const circle = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
draggable: true,
});
// add cursor styling
circle.on('mouseover', function () {
document.body.style.cursor = 'pointer';
});
circle.on('mouseout', function () {
document.body.style.cursor = 'default';
});
layer.add(circle);
```
```jsx
import { Stage, Layer, Circle } from 'react-konva';
import { useState } from 'react';
const App = () => {
const [position, setPosition] = useState({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
return (
{
document.body.style.cursor = 'pointer';
}}
onMouseLeave={(e) => {
document.body.style.cursor = 'default';
}}
onDragEnd={(e) => {
setPosition({
x: e.target.x(),
y: e.target.y()
});
}}
/>
);
};
export default App;
```
```vue
```
---
# HTML5 Canvas Konva Animation Tutorial
> Learn how to create animations on HTML5 Canvas with Konva.js. Use Konva.Animation for frame-based animations and Konva.Tween for property transitions.
Source: https://konvajs.org/docs/animations/Create_an_Animation.html
To create custom animations with Konva, we can use the `Konva.Animation`
constructor which takes two arguments, the required update function and
an optional layer, or array of layers, that will be updated with each animation frame.
The animation function is passed a `frame` object which contains a `time` property which is the number
of milliseconds that the animation has been running, a `timeDiff` property which
is the number of milliseconds that have passed since the last frame,
and a `frameRate` property which is the current frame rate in frames per second.
The update function should never redraw the stage or a layer because the animation
engine will intelligently handle that for us.
The update function should only contain logic that updates Node properties,
such as `position`, `rotation`, `scale`, `width`, `height`, `radius`, `colors`, etc.
Once the animation has been created, we can start it at anytime with the `start()` method.
For a full list of attributes and methods, check out the [Konva.Animation documentation](/api/Konva.Animation.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const rect = new Konva.Rect({
x: 50,
y: 50,
width: 50,
height: 50,
fill: 'green',
});
layer.add(rect);
const anim = new Konva.Animation(function(frame) {
const time = frame.time;
const timeDiff = frame.timeDiff;
const frameRate = frame.frameRate;
// Example: move rectangle in a circle
const radius = 50;
const x = radius * Math.cos(frame.time * 2 * Math.PI / 2000) + 100;
const y = radius * Math.sin(frame.time * 2 * Math.PI / 2000) + 100;
rect.position({ x, y });
}, layer);
anim.start();
````
```js
import { Stage, Layer, Rect } from 'react-konva';
import { useEffect, useRef } from 'react';
const App = () => {
const rectRef = useRef(null);
useEffect(() => {
const anim = new Konva.Animation((frame) => {
const time = frame.time;
const timeDiff = frame.timeDiff;
const frameRate = frame.frameRate;
// Example: move rectangle in a circle
const radius = 50;
const x = radius * Math.cos(frame.time * 2 * Math.PI / 2000) + 100;
const y = radius * Math.sin(frame.time * 2 * Math.PI / 2000) + 100;
rectRef.current.position({ x, y });
}, rectRef.current.getLayer());
anim.start();
return () => {
anim.stop();
};
}, []);
return (
);
};
export default App;
````
```js
```
---
# HTML5 Canvas Blur Image Filter Tutorial
> Learn how to apply a blur filter to images on HTML5 Canvas using Konva.js with adjustable blurRadius property.
Source: https://konvajs.org/docs/filters/Blur.html
To apply filter to an `Konva.Image`, we have to cache it first with `cache()`
function. Then apply filter with `filters()` function.
To blur an image with Konva, we can use the `Konva.Filters.Blur` filter
and set the blur amount with the `blurRadius` property.
**Instructions**: Slide the control to adjust the blur radius.
For all available filters go to [Filters Documentation](/api/Konva.Filters.html).
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
const imageObj = new Image();
imageObj.onload = () => {
const image = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
draggable: true,
});
layer.add(image);
image.cache();
image.filters([Konva.Filters.Blur]);
image.blurRadius(10);
const slider = document.createElement('input');
slider.type = 'range';
slider.min = '0';
slider.max = '40';
slider.value = image.blurRadius();
slider.style.position = 'absolute';
slider.style.top = '20px';
slider.style.left = '20px';
slider.addEventListener('input', (e) => {
const value = parseInt(e.target.value);
image.blurRadius(value);
});
document.body.appendChild(slider);
};
imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg';
imageObj.crossOrigin = 'anonymous';
```
```js
import Konva from 'konva';
import { Stage, Layer, Image } from 'react-konva';
import { useState, useEffect, useRef } from 'react';
import useImage from 'use-image';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
const [blurRadius, setBlurRadius] = useState(10);
const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous');
const imageRef = useRef(null);
useEffect(() => {
if (image && imageRef.current) {
imageRef.current.cache();
}
}, [image]);
return (
<>
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
filters={[Konva.Filters.Blur]}
blurRadius={blurRadius}
/>
setBlurRadius(parseInt(e.target.value))}
style={{ position: 'absolute', top: '20px', left: '20px' }}
/>
>
);
};
export default App;
```
```js
```
---
# HTML5 Canvas All Konva performance tips list
> All Konva.js performance optimization tips: layer management, shape caching, listening false, batch draw, and more. Render thousands of shapes efficiently on HTML5 Canvas.
Source: https://konvajs.org/docs/performance/All_Performance_Tips.html
Don't want to spend your time with performance issues? Request a [performance review](https://lavrton.com/consulting/).
### Why this is important
The HTML5 canvas is efficient at what it does and internally `Konva` has many features that aim to provide great performance. However, when your project starts to grow in complexity, or when you just have a lot of shapes on the stage, there must inevitably be some negative performance impact.
### Optimization targets
The optimizations here focus on two general rules:
* Compute as little as possible: all computation takes time to complete. Each individual computation may run in a tiny fraction of a second, but the thousands or millions of computations caused by your code, Konva, JavaScript, and the layers below that, will add up to something more observable by the human eye if that super-slick animation or effect is, in fact, jerky.
* **Draw as little as possible**: this is important because all drawing has a performance cost. There are two categories of cost - firstly the computation of the drawing which we covered in the point above, and then the movement of the drawing from memory to the screen. Depending on the case, there may also be intermediate off-screen compositing or per-pixel processing. The rule is therefore do as little drawing as possible.
### The Stage
1. Optimise stage size - following the rule of 'draw as little as possible', try to avoid creating a large stage because moving all those bytes from memory to screen is going to have a negative impact. There are some tips [here](/docs/sandbox/Canvas_Scrolling.html) that offer alternative approaches to the mega-stage problem!
2. Set a viewport on mobile - Scaling images is a significant performance hit, so for mobile applications set viewport: `
` which will avoid unnecessary scaling of your Konva output.
3. Use `Konva.pixelRatio = 1` on retina devices - Konva automatically handles pixel ratio adjustments in order to render crisp drawings on all devices. But, just in case you have bad performance on retina devices, set `Konva.pixelRatio = 1` to reduce the scaling work Konva has to do. This setting might affect the output in some cases, so make sure that quality of the result is ok for you.
### Layers
1. [Layer Management](/docs/performance/Layer_Management.html) - under the hood, each Konva layer is a separate HTML5 canvas element which gives some useful capabilities, including the ability to refresh only a layer that changed and so avoid the performance cost of refreshing the entire stage. But with great power great responsibility comes, and each layer has an incremental performance overhead so we should keep the number of layers to a minimum.
2. Use `layer.listening(false)` - Konva gives us mouse and touch event listeners on all the shapes we draw. But there is a performance cost for each one, and for a layer with many shapes Konva has to expend many cycles checking which listeners might be triggered. If you have a layer on which none of the shapes need to react to events, take this burden away by setting `layer.listening(false)`. See [Demo](/docs/sandbox/Animation_Stress_Test.html). There is a similar point in the shapes section.
3. Optimise dragging costs - while you drag a shape across a layer that layer must be redrawn per cycle of the move event listener. To avoid this performance cost, move the shape to a dedicated layer while dragging, then move it back to original layer at drag end. See [Demo](/docs/sandbox/Drag_and_Drop_Stress_Test.html)
### Shapes
1. [Shape Caching](/docs/performance/Shape_Caching.html) - internally Konva makes an image of your shape and uses that when the shape has to be drawn. Drawing images avoids the overhead of composing the shape from its drawing instructions, and can increase performance impressively for complex shapes and groups.
2. Keep the shapes tidy - each shape in your stage has a cost just to exist. To optimise performance, hide or remove from the layer any objects that become invisible / opacity = 0, or objects that go out of view.
3. Use `shape.listening(false)` - as with layers (see point 7 above), Konva looks out for when events should be triggered for shapes, which has a performance cost. Telling a shape to stop listening for events reduces this cost, as explained at [Listening false](/docs/performance/Listening_False.html).
4. Switch off perfect drawing - In some cases the result of drawing with the HTML5 canvas is not what you might have expected - see the demo for an example [Disable Perfect Drawing](/docs/performance/Disable_Perfect_Draw.html). Konva does extra work via its perfect drawing feature to put that right, but this comes with a performance cost. By setting `shape.perfectDrawEnabled(false)` this cost can be avoided, with no reduction in output quality, when a shape has fill, stroke and opacity.
5. [Optimize Stroke Drawing](/docs/performance/Optimize_Strokes.html) - To achieve drawing results that look as expected, Konva makes an extra internal drawing when a shape has both stroke and shadow. Avoid this performance burden by switching off the shadow that Konva adds for the stroke.
### Animations
1. [Optimize Animation](/docs/performance/Optimize_Animation.html) - Avoiding unnecessary redraw costs for animation steps that fall between visual changes.
### Memory
1. [Avoid Memory Leaks](/docs/performance/Avoid_Memory_Leaks.html) - Konva looks after a lot of cases where you might make memory leaks, but bringing shapes and tweens into the world and managing their exit is an area where you can help.
2. **Know what a layer costs.** Every layer allocates two canvases: a scene canvas at the device pixel ratio, and a hit canvas always at ratio 1. On a 1920 × 1080 stage on a retina screen that is roughly 33 MB plus 8 MB, so about 41 MB per layer before you draw anything. This is why Konva warns above five layers — at that point you are near 200 MB of canvas memory on nothing but empty layers.
3. **Mobile Safari enforces a hard ceiling.** Past it you get `Total canvas memory use exceeds the maximum limit`, reported as 256 MB on some devices and 384 MB on others, and the canvas goes blank rather than degrading. The levers, in order of effect: fewer layers, a smaller stage, and `Konva.pixelRatio = 1` — which quarters the scene canvas on a 2× screen. Caches count towards the same budget, so release them with `node.clearCache()` when a node is no longer visible.
### Very large scenes
1. **Cull what is off screen.** Konva draws every node on a layer whether or not it lands inside the stage. For a scene much larger than the viewport, hide the nodes outside it — `visible(false)` skips both drawing and hit testing, and is far cheaper than removing and re-adding nodes.
```js
function cull() {
const view = {
x: -stage.x() / stage.scaleX(),
y: -stage.y() / stage.scaleY(),
width: stage.width() / stage.scaleX(),
height: stage.height() / stage.scaleY(),
};
layer.children.forEach((node) => {
node.visible(Konva.Util.haveIntersection(view, node.getClientRect()));
});
}
```
Call it when the view changes — after a pan or a zoom — not on every frame. `getClientRect()` is not free, so for tens of thousands of nodes keep your own index of positions and test against that instead of asking each node.
2. **Skip hit detection during a drag.** While a node is being dragged Konva does not run hit detection, which keeps dragging cheap. If you need to know what is under the pointer mid-drag — highlighting a drop target, for example — turn it back on and accept the cost:
```js
Konva.hitOnDragEnabled = true; // default is false
```
3. **Reach for one shape instead of many.** A thousand nodes each with their own attributes, transform and hit region cost far more than one custom shape that draws a thousand things in a single `sceneFunc`. You lose per-item events and dragging, so this is a trade, not a free win — see [Custom Shape](/docs/shapes/Custom.html).
Below is a demo showing some of these performance tips in action:
```js
import Konva from 'konva';
// Create stage with good performance settings
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
// Create layers with performance optimizations
const backgroundLayer = new Konva.Layer({ listening: false });
const mainLayer = new Konva.Layer();
const dragLayer = new Konva.Layer();
stage.add(backgroundLayer);
stage.add(mainLayer);
stage.add(dragLayer);
// Create a shape with caching
const star = new Konva.Star({
x: 200,
y: 200,
numPoints: 6,
innerRadius: 40,
outerRadius: 70,
fill: 'yellow',
stroke: 'black',
strokeWidth: 4,
draggable: true,
perfectDrawEnabled: false, // performance optimization
});
// Cache the shape for better performance
star.cache();
// Optimize dragging performance
star.on('dragstart', () => {
star.moveTo(dragLayer);
});
star.on('dragend', () => {
star.moveTo(mainLayer);
});
// Create background with listening disabled
const rect = new Konva.Rect({
x: 0,
y: 0,
width: stage.width(),
height: stage.height(),
fill: 'lightgray',
listening: false,
});
backgroundLayer.add(rect);
mainLayer.add(star);
```
```js
import { Stage, Layer, Star, Rect } from 'react-konva';
import { Portal } from 'react-konva-utils';
import { useState, useRef, useEffect } from 'react';
const App = () => {
const [isDragging, setIsDragging] = useState(false);
const [position, setPosition] = useState({ x: 200, y: 200 });
const starRef = useRef(null);
useEffect(() => {
// Cache the shape for better performance
if (starRef.current) {
starRef.current.cache();
}
}, []);
const handleDragStart = () => {
setIsDragging(true);
};
const handleDragEnd = (event) => {
setPosition(event.target.position());
setIsDragging(false);
};
return (
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Stage Serialization Tutorial
> Learn how to serialize and save HTML5 Canvas state as JSON with Konva.js. Use stage.toJSON() to export and Konva.Node.create() to restore canvas content.
Source: https://konvajs.org/docs/data_and_serialization/Serialize_a_Stage.html
To serialize a stage with Konva, we can use the `toJSON()` method.
The `toJSON()` method will return a JSON string that contains all of the node's attributes.
Note that event handlers and images are not serializable.
```js
import Konva from 'konva';
// Create wrapper with relative positioning
const stage = new Konva.Stage({
container: 'container',
width: 400,
height: 400
});
const layer = new Konva.Layer();
stage.add(layer);
const circle = new Konva.Circle({
x: 100,
y: 100,
radius: 50,
fill: 'red',
stroke: 'black',
strokeWidth: 3
});
layer.add(circle);
// Add button on top of stage
const button = document.createElement('button');
button.textContent = 'Serialize Stage';
button.style.position = 'absolute';
button.style.top = '10px';
button.style.left = '10px';
document.body.appendChild(button);
button.addEventListener('click', () => {
const json = stage.toJSON();
console.log(json);
alert('Stage serialized! Check the console for the JSON string.');
});
```
**Note:** While directly serializing the stage works in React, it's generally considered an anti-pattern. In React applications, you should manage your application state separately and serialize that state instead of the stage.
```js
import { Stage, Layer, Circle } from 'react-konva';
import { useRef, useState } from 'react';
const App = () => {
const stageRef = useRef(null);
const [circle, setCircle] = useState({
x: 100,
y: 100,
radius: 50,
fill: 'red',
stroke: 'black',
strokeWidth: 3
});
const handleSerialize = () => {
// In a real app, prefer saving app state, not stage JSON
const json = JSON.stringify({ shapes: [circle] });
console.log('Serialized state:', json);
alert('State serialized! Check the console for the JSON string.');
};
return (
Serialize
{
setCircle({
...circle,
x: e.target.x(),
y: e.target.y()
});
}}
/>
);
};
export default App;
```
**Note:** While directly serializing the stage works in Vue, it's generally considered an anti-pattern. In Vue applications, you should manage your application state with reactive data and serialize that state instead of the stage.
```js
Serialize
```
---
# HTML5 Canvas Shape select, resize and rotate
> Learn how to select, resize, and rotate shapes on HTML5 Canvas with Konva.js Transformer. Add interactive handles for resizing and rotating any shape.
Source: https://konvajs.org/docs/select_and_transform/Basic_demo.html
`Transformer` is a special kind of `Konva.Group`. It allows you easily resize and rotate any node or set of nodes.
To enable it you need to:
1. Create new instance with `new Konva.Transformer()`
2. Add it to layer
3. attach to node with `transformer.nodes([shape]);`
_Note:_ Transforming tool is not changing `width` and `height` properties of nodes when you resize them. Instead it changes `scaleX` and `scaleY` properties.
**Instructions: Try to resize and rotate shapes. Click on empty area to remove selection. Use SHIFT or CTRL to add/remove shapes into/from selection. Try to select area on a canvas.**
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
// create rectangle
const rect1 = new Konva.Rect({
x: 60,
y: 60,
width: 100,
height: 90,
fill: 'red',
name: 'rect',
draggable: true,
});
layer.add(rect1);
const rect2 = new Konva.Rect({
x: 250,
y: 100,
width: 150,
height: 90,
fill: 'green',
name: 'rect',
draggable: true,
});
layer.add(rect2);
// create transformer
const tr = new Konva.Transformer();
layer.add(tr);
// add a new feature, lets add ability to draw selection rectangle
let selectionRectangle = new Konva.Rect({
fill: 'rgba(0,0,255,0.5)',
visible: false,
});
layer.add(selectionRectangle);
let x1, y1, x2, y2;
stage.on('mousedown touchstart', (e) => {
// do nothing if we mousedown on any shape
if (e.target !== stage) {
return;
}
x1 = stage.getPointerPosition().x;
y1 = stage.getPointerPosition().y;
x2 = stage.getPointerPosition().x;
y2 = stage.getPointerPosition().y;
selectionRectangle.setAttrs({
x: x1,
y: y1,
width: 0,
height: 0,
visible: true,
});
});
stage.on('mousemove touchmove', () => {
// do nothing if we didn't start selection
if (!selectionRectangle.visible()) {
return;
}
x2 = stage.getPointerPosition().x;
y2 = stage.getPointerPosition().y;
selectionRectangle.setAttrs({
x: Math.min(x1, x2),
y: Math.min(y1, y2),
width: Math.abs(x2 - x1),
height: Math.abs(y2 - y1),
});
});
stage.on('mouseup touchend', () => {
// do nothing if we didn't start selection
if (!selectionRectangle.visible()) {
return;
}
// update visibility in timeout, so we can check it in click event
setTimeout(() => {
selectionRectangle.visible(false);
});
var shapes = stage.find('.rect');
var box = selectionRectangle.getClientRect();
var selected = shapes.filter((shape) =>
Konva.Util.haveIntersection(box, shape.getClientRect())
);
tr.nodes(selected);
});
// clicks should select/deselect shapes
stage.on('click tap', function (e) {
// if we are selecting with rect, do nothing
if (selectionRectangle.visible() && selectionRectangle.width() > 0 && selectionRectangle.height() > 0) {
return;
}
// if click on empty area - remove all selections
if (e.target === stage) {
tr.nodes([]);
return;
}
// do nothing if clicked NOT on our rectangles
if (!e.target.hasName('rect')) {
return;
}
// do we pressed shift or ctrl?
const metaPressed = e.evt.shiftKey || e.evt.ctrlKey || e.evt.metaKey;
const isSelected = tr.nodes().indexOf(e.target) >= 0;
if (!metaPressed && !isSelected) {
// if no key pressed and the node is not selected
// select just one
tr.nodes([e.target]);
} else if (metaPressed && isSelected) {
// if we pressed keys and node was selected
// we need to remove it from selection:
const nodes = tr.nodes().slice(); // use slice to have new copy of array
// remove node from array
nodes.splice(nodes.indexOf(e.target), 1);
tr.nodes(nodes);
} else if (metaPressed && !isSelected) {
// add the node into selection
const nodes = tr.nodes().concat([e.target]);
tr.nodes(nodes);
}
});
````
```js
import { Stage, Layer, Rect, Transformer } from 'react-konva';
import { useState, useEffect, useRef } from 'react';
const initialRectangles = [
{
x: 60,
y: 60,
width: 100,
height: 90,
fill: 'red',
id: 'rect1',
name: 'rect',
rotation: 0,
},
{
x: 250,
y: 100,
width: 150,
height: 90,
fill: 'green',
id: 'rect2',
name: 'rect',
rotation: 0,
},
];
// Helper functions for calculating bounding boxes of rotated rectangles
const degToRad = (angle) => (angle / 180) * Math.PI;
const getCorner = (pivotX, pivotY, diffX, diffY, angle) => {
const distance = Math.sqrt(diffX * diffX + diffY * diffY);
angle += Math.atan2(diffY, diffX);
const x = pivotX + distance * Math.cos(angle);
const y = pivotY + distance * Math.sin(angle);
return { x, y };
};
const getClientRect = (element) => {
const { x, y, width, height, rotation = 0 } = element;
const rad = degToRad(rotation);
const p1 = getCorner(x, y, 0, 0, rad);
const p2 = getCorner(x, y, width, 0, rad);
const p3 = getCorner(x, y, width, height, rad);
const p4 = getCorner(x, y, 0, height, rad);
const minX = Math.min(p1.x, p2.x, p3.x, p4.x);
const minY = Math.min(p1.y, p2.y, p3.y, p4.y);
const maxX = Math.max(p1.x, p2.x, p3.x, p4.x);
const maxY = Math.max(p1.y, p2.y, p3.y, p4.y);
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
};
const App = () => {
const [rectangles, setRectangles] = useState(initialRectangles);
const [selectedIds, setSelectedIds] = useState([]);
const [selectionRectangle, setSelectionRectangle] = useState({
visible: false,
x1: 0,
y1: 0,
x2: 0,
y2: 0,
});
const isSelecting = useRef(false);
const transformerRef = useRef();
const rectRefs = useRef(new Map());
// Update transformer when selection changes
useEffect(() => {
if (selectedIds.length && transformerRef.current) {
// Get the nodes from the refs Map
const nodes = selectedIds
.map(id => rectRefs.current.get(id))
.filter(node => node);
transformerRef.current.nodes(nodes);
} else if (transformerRef.current) {
// Clear selection
transformerRef.current.nodes([]);
}
}, [selectedIds]);
// Click handler for stage
const handleStageClick = (e) => {
// If we are selecting with rect, do nothing
// But allow point clicks through (when width/height are 0)
const selWidth = Math.abs(selectionRectangle.x2 - selectionRectangle.x1);
const selHeight = Math.abs(selectionRectangle.y2 - selectionRectangle.y1);
if (selectionRectangle.visible && selWidth > 0 && selHeight > 0) {
return;
}
// If click on empty area - remove all selections
if (e.target === e.target.getStage()) {
setSelectedIds([]);
return;
}
// Do nothing if clicked NOT on our rectangles
if (!e.target.hasName('rect')) {
return;
}
const clickedId = e.target.id();
// Do we pressed shift or ctrl?
const metaPressed = e.evt.shiftKey || e.evt.ctrlKey || e.evt.metaKey;
const isSelected = selectedIds.includes(clickedId);
if (!metaPressed && !isSelected) {
// If no key pressed and the node is not selected
// select just one
setSelectedIds([clickedId]);
} else if (metaPressed && isSelected) {
// If we pressed keys and node was selected
// we need to remove it from selection
setSelectedIds(selectedIds.filter(id => id !== clickedId));
} else if (metaPressed && !isSelected) {
// Add the node into selection
setSelectedIds([...selectedIds, clickedId]);
}
};
const handleMouseDown = (e) => {
// Do nothing if we mousedown on any shape
if (e.target !== e.target.getStage()) {
return;
}
// Start selection rectangle
isSelecting.current = true;
const pos = e.target.getStage().getPointerPosition();
setSelectionRectangle({
visible: true,
x1: pos.x,
y1: pos.y,
x2: pos.x,
y2: pos.y,
});
};
const handleMouseMove = (e) => {
// Do nothing if we didn't start selection
if (!isSelecting.current) {
return;
}
const pos = e.target.getStage().getPointerPosition();
setSelectionRectangle({
...selectionRectangle,
x2: pos.x,
y2: pos.y,
});
};
const handleMouseUp = () => {
// Do nothing if we didn't start selection
if (!isSelecting.current) {
return;
}
isSelecting.current = false;
// Update visibility in timeout, so we can check it in click event
setTimeout(() => {
setSelectionRectangle({
...selectionRectangle,
visible: false,
});
});
const selBox = {
x: Math.min(selectionRectangle.x1, selectionRectangle.x2),
y: Math.min(selectionRectangle.y1, selectionRectangle.y2),
width: Math.abs(selectionRectangle.x2 - selectionRectangle.x1),
height: Math.abs(selectionRectangle.y2 - selectionRectangle.y1),
};
// Only select shapes if selection box has actual size (not just a point click)
if (selBox.width > 0 && selBox.height > 0) {
const selected = rectangles.filter(rect => {
// Check if rectangle intersects with selection box
return Konva.Util.haveIntersection(selBox, getClientRect(rect));
});
setSelectedIds(selected.map(rect => rect.id));
}
};
const handleDragEnd = (e) => {
const id = e.target.id();
setRectangles(prevRects => {
const newRects = [...prevRects];
const index = newRects.findIndex(r => r.id === id);
if (index !== -1) {
newRects[index] = {
...newRects[index],
x: e.target.x(),
y: e.target.y()
};
}
return newRects;
});
};
const handleTransformEnd = (e) => {
// Find which rectangle(s) were transformed
const id = e.target.id();
const node = e.target;
setRectangles(prevRects => {
const newRects = [...prevRects];
// Update each transformed node
const index = newRects.findIndex(r => r.id === id);
if (index !== -1) {
const scaleX = node.scaleX();
const scaleY = node.scaleY();
// Reset scale
node.scaleX(1);
node.scaleY(1);
// Update the state with new values
newRects[index] = {
...newRects[index],
x: node.x(),
y: node.y(),
width: Math.max(5, node.width() * scaleX),
height: Math.max(5, node.height() * scaleY),
rotation: node.rotation(),
};
}
return newRects;
});
};
return (
{/* Render rectangles directly */}
{rectangles.map(rect => (
{
if (node) {
rectRefs.current.set(rect.id, node);
}
}}
onDragEnd={handleDragEnd}
onTransformEnd={handleTransformEnd}
/>
))}
{/* Single transformer for all selected shapes */}
{
// Limit resize
if (newBox.width < 5 || newBox.height < 5) {
return oldBox;
}
return newBox;
}}
/>
{/* Selection rectangle */}
{selectionRectangle.visible && (
)}
);
};
export default App;
```
```js
handleDragEnd(e, i)"
@transformend="(e) => handleTransformEnd(e, i)"
ref="rectRefs"
/>
```
## What Transformer does not do
`Transformer` draws the handles and applies the scale. Snapping to other
objects, alignment guides, a shared bounding box for a multi-selection, and
per-shape aspect rules are all yours to build — see
[objects snapping](/docs/sandbox/Objects_Snapping.html) for one approach.
A production editor also needs text editing, templates, and export around the
Transformer. If you would rather not build those, [Polotno](https://polotno.com/?utm_source=konvajs&utm_medium=docs&utm_content=transformer-basic) is a commercial
design editor SDK built on Konva by the Konva maintainers that ships them.
---
# Node.js Setup
> Set up Konva for server-side rendering in Node.js with canvas or skia-canvas backends for image generation and batch processing.
Source: https://konvajs.org/docs/nodejs/nodejs-setup/index.html
Konva can be used in Node.js environments for server-side rendering, image processing, and canvas operations. This guide will help you set up Konva in your Node.js project.
## Konva Version 10+
Konva v10+ dropped default support for Node.js environment. You now need to explicitly import a canvas backend.
### Installation
Konva v10+ offers two backend options for Node.js:
**node-canvas Backend**:
```bash
npm install konva canvas
```
**Skia Backend (Better Performance)**:
```bash
npm install konva skia-canvas
```
### Usage
Import Konva and your chosen backend:
**Canvas Backend**:
```js
import Konva from 'konva';
import 'konva/canvas-backend';
```
**Skia Backend**:
```js
import Konva from 'konva';
import 'konva/skia-backend';
```
**Complete Example**:
```js
import Konva from 'konva';
import 'konva/canvas-backend'; // or 'konva/skia-backend'
// Create a stage
const stage = new Konva.Stage({
container: 'container', // This will be ignored in Node.js
width: 800,
height: 600
});
// ... the rest of your konva code
// Export as data URL
const dataURL = stage.toDataURL();
```
## Konva Version ≤ 9 (Legacy)
For older versions of Konva, the setup was simpler:
### Installation
```bash
npm install konva
```
### Setup
```js
const Konva = require('konva');
// Create a stage
const stage = new Konva.Stage({
container: 'container', // This will be ignored in Node.js
width: 800,
height: 600
});
// ... the rest of your konva code
// Export as data URL
const dataURL = stage.toDataURL();
```
## Server-Side Rendering Considerations
When using Konva in Node.js, keep in mind:
1. **No DOM**: Konva doesn't require a DOM, making it perfect for server-side rendering
2. **Canvas Export**: Use `stage.toDataURL()` to export your canvas as an image
3. **Memory Management**: Be mindful of memory usage when processing multiple canvases
4. **Performance**: Konva performs well in Node.js environments for batch operations
5. **SSR Frameworks**: For Next.js and other SSR frameworks, consider using client-side only rendering for canvas content
## Common Use Cases
- **Image Generation**: Create dynamic images for emails, reports, or social media
- **Chart Generation**: Generate charts and graphs server-side
- **Document Processing**: Add graphics to PDFs or other documents
- **Batch Processing**: Process multiple images or graphics in parallel
---
# AI Tools for Konva.js Development
> Use AI to build Konva.js apps faster. Chat with an AI bot trained on Konva docs, or connect the Konva MCP server to Cursor, Claude Desktop, Windsurf, and other AI coding tools.
Source: https://konvajs.org/docs/ai_tools.html
## Coding with Konva and AI
We have several AI tools to help you build Konva apps faster. All of them are powered by [CrawlChat](https://www.crawlchat.com/).
The AI agent uses Konva docs extensively to answer your questions. Please remember it is an LLM and as any modern LLM it may give wrong answers.
## AI Chat Bot
Click the "Ask AI" button on any page to ask a question about Konva.
You can also join the [Konva Discord community](https://discord.gg/8FqZwVT) and ask `@AiBot-CrawlChat` there.
## MCP (Model Context Protocol)
MCP is a standard protocol that connects AI coding tools to external documentation. With the Konva MCP server, tools like Cursor, Claude Desktop, and Windsurf can access Konva documentation directly when helping you write code.
### Cursor
Add the following to your Cursor MCP settings:
**Important: Cursor uses MCP only in "Agent" mode. "Ask" and other modes will not use it.**
```json
"konva-documentation": {
"command": "npx",
"args": [
"crawl-chat-mcp",
"--id=67d221efb4b9de65095a2579",
"--name=konva_documentation"
]
}
```
### Claude Desktop
Add the following to your Claude Desktop config file (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"konva-documentation": {
"command": "npx",
"args": [
"crawl-chat-mcp",
"--id=67d221efb4b9de65095a2579",
"--name=konva_documentation"
]
}
}
}
```
On macOS, the config file is at `~/Library/Application Support/Claude/claude_desktop_config.json`. On Windows, it's at `%APPDATA%\Claude\claude_desktop_config.json`.
### Windsurf
Add the following to your Windsurf MCP configuration:
```json
"konva-documentation": {
"command": "npx",
"args": [
"crawl-chat-mcp",
"--id=67d221efb4b9de65095a2579",
"--name=konva_documentation"
]
}
```
### Generic MCP Command
For any MCP-compatible tool, use:
```
npx crawl-chat-mcp --id=67d221efb4b9de65095a2579 --name=konva_documentation
```
## LLM-Readable Documentation
Konva provides machine-readable documentation files for AI tools:
- [`/llms.txt`](/llms.txt) — Concise summary of Konva with key documentation links (follows the [llmstxt.org](https://llmstxt.org/) standard)
- [`/llms-full.txt`](pathname:///llms-full.txt) — The same index followed by the full text of every documentation page, for tools that ingest one file
- [`/llms-small.txt`](pathname:///llms-small.txt) — The same without the sandbox demos, small enough for a single model context
- [`
.md`](pathname:///docs/overview.md) — Every documentation page is also served as plain markdown at the same path with a `.md` extension
These files help AI assistants give accurate answers about Konva.
If you are building a design editor with [Polotno](https://polotno.com/?utm_source=konvajs&utm_medium=docs&utm_content=ai-tools), the commercial design editor SDK built on Konva by the Konva maintainers, its [build with AI](https://polotno.com/docs/build-with-ai?utm_source=konvajs&utm_medium=docs&utm_content=ai-tools) page lists the equivalent files and MCP setup.
## Tips for Using AI with Konva
When asking AI tools about Konva, you'll get better results if you:
- Mention "Konva" or "react-konva" explicitly in your prompt
- Reference specific Konva features (e.g., "Transformer", "Layer", "toDataURL")
- Ask about one task at a time rather than combining multiple questions
- Verify AI-generated code against the [Konva docs](https://konvajs.org/docs/overview.html) and [API reference](https://konvajs.org/api/Konva.html)
---
# Need help with Konva library?
> Get help with Konva via StackOverflow, GitHub Issues, Discord chat, consulting, and other community resources.
Source: https://konvajs.org/docs/support.html
## Looking for a help with Konva framework?
Here is what you should do:
1. First try to find solutions online. Try to search your question. You can use google, or built-in search on top of that page.
2. The best place to ask questions is [StackOverflow](https://stackoverflow.com/questions/tagged/konvajs). You will have more chances to have a good answer if you create a high quality question with online demo, code samples, correct tags, etc.
3. If you found a bug or you want to request a feature go to [Issues Page](https://github.com/konvajs/konva/issues).
4. If you just want to discuss Konva you can join [discord Chat](https://discord.gg/8FqZwVT)
5. If you have something interesting to share use Twitter `#konvajs` hashtag
6. Visit [Changelog](https://github.com/konvajs/konva/blob/master/CHANGELOG.md) to see what is changing.
7. Need a consulting or strategy review? Go to the [consulting page](https://lavrton.com/consulting/).
8. Building a full design editor rather than a custom canvas? [Polotno](https://polotno.com/?utm_source=konvajs&utm_medium=docs&utm_content=support) is a commercial design editor SDK built on Konva by the Konva maintainers, with its own documentation and support.
---
# Angular Konva Cache Tutorial
> Learn how to cache Konva shapes in Angular using ng2-konva to improve canvas rendering performance.
Source: https://konvajs.org/docs/angular/Cache.html
Caching rasterizes a node into an internal image, which can improve performance and is required for some effects such as filters.
This example accesses the underlying Konva node after render and calls `cache()` manually.
**Instructions**: The demo shows a rectangle with shadow styling that is cached after the view initializes.
For more details, see the [Node API Reference](/api/Konva.Node.html) and [`cache()` documentation](/api/Konva.Node.html#cache).
## Cache Example
```js
import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { StageConfig } from 'konva/lib/Stage';
import { RectConfig } from 'konva/lib/shapes/Rect';
import {
CoreShapeComponent,
StageComponent,
} from 'ng2-konva';
@Component({
selector: 'app-root',
standalone: true,
template: `
`,
imports: [StageComponent, CoreShapeComponent],
})
export default class App implements AfterViewInit {
@ViewChild('rect') rect!: any;
public configStage: StageConfig = {
width: window.innerWidth,
height: window.innerHeight,
};
public configRect: RectConfig = {
x: 50,
y: 50,
width: 100,
height: 100,
fill: 'red',
shadowBlur: 10,
shadowColor: 'black',
shadowOffsetX: 5,
shadowOffsetY: 5
};
ngAfterViewInit() {
if (this.rect) {
this.rect.getNode().cache();
}
}
}
```
---
# Angular Konva Custom Shape Tutorial
> Learn how to draw custom shapes on canvas in Angular using ng2-konva with the sceneFunc drawing function.
Source: https://konvajs.org/docs/angular/Custom_Shape.html
To create a custom shape with `ng2-konva`, use the `ko-shape` component and provide a `sceneFunc` drawing function.
Inside `sceneFunc`, you can use the native canvas context together with Konva helpers such as `context.fillStrokeShape(shape)` to apply fill, stroke, and shadow styles correctly.
**Instructions**: The demo draws a custom triangle-like shape with canvas path commands.
For more details, see the [Shape API Reference](/api/Konva.Shape.html).
## Custom Shape Example
```js
import { Component } from '@angular/core';
import { StageConfig } from 'konva/lib/Stage';
import { ShapeConfig } from 'konva/lib/shapes/Shape';
import {
CoreShapeComponent,
StageComponent,
} from 'ng2-konva';
@Component({
selector: 'app-root',
standalone: true,
template: `
`,
imports: [StageComponent, CoreShapeComponent],
})
export default class App {
public configStage: StageConfig = {
width: window.innerWidth,
height: window.innerHeight,
};
public configShape: ShapeConfig = {
x: 100,
y: 100,
fill: 'red',
stroke: 'black',
strokeWidth: 2,
sceneFunc: (context: any, shape: any) => {
context.beginPath();
context.moveTo(0, 0);
context.lineTo(100, 0);
context.lineTo(50, 100);
context.closePath();
context.fillStrokeShape(shape);
}
};
}
```
---
# Angular Konva Drag and Drop Tutorial
> Learn how to implement drag and drop for canvas shapes in Angular using ng2-konva with event handlers.
Source: https://konvajs.org/docs/angular/Drag_And_Drop.html
To make a shape draggable, set `draggable: true` in its config and listen to drag events on the node.
This example moves the dragged circle to the top of its layer when dragging starts.
**Instructions**: Drag the circle around the stage and notice that it is brought to the top when you start dragging.
For more details, see the [Node API Reference](/api/Konva.Node.html) and the [Circle API Reference](/api/Konva.Circle.html).
## Drag and Drop Example
```js
import { Component } from '@angular/core';
import { StageConfig } from 'konva/lib/Stage';
import { CircleConfig } from 'konva/lib/shapes/Circle';
import {
CoreShapeComponent,
StageComponent,
} from 'ng2-konva';
@Component({
selector: 'app-root',
standalone: true,
template: `
`,
imports: [StageComponent, CoreShapeComponent],
})
export default class App {
public configStage: StageConfig = {
width: window.innerWidth,
height: window.innerHeight,
};
public configCircle: CircleConfig = {
x: 100,
y: 100,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
draggable: true,
};
public handleDragstart(event: any): void {
event.target.moveToTop();
}
}
```
---
# Angular Konva Events Tutorial
> Learn how to handle mouse, touch, and pointer events on canvas shapes in Angular using ng2-konva event bindings.
Source: https://konvajs.org/docs/angular/Events.html
To handle pointer events in Angular, attach listeners such as `(mousemove)` and `(mouseout)` directly to `ng2-konva` components.
This demo updates a text label from stage pointer coordinates while the mouse moves over the triangle.
**Instructions**: Move your mouse over the triangle to update the coordinate label, then move out to reset the text.
For more details, see the [Node API Reference](/api/Konva.Node.html) and the [Stage API Reference](/api/Konva.Stage.html).
## Events Example
```js
import { Component } from '@angular/core';
import { StageConfig } from 'konva/lib/Stage';
import { RegularPolygonConfig } from 'konva/lib/shapes/RegularPolygon';
import { TextConfig } from 'konva/lib/shapes/Text';
import {
CoreShapeComponent,
StageComponent,
} from 'ng2-konva';
@Component({
selector: 'app-root',
standalone: true,
template: `
`,
imports: [StageComponent, CoreShapeComponent],
})
export default class App {
public configStage: StageConfig = {
width: window.innerWidth,
height: window.innerHeight,
};
public configTriangle: RegularPolygonConfig = {
x: 80,
y: 120,
sides: 3,
radius: 80,
fill: '#00D2FF',
stroke: 'black',
strokeWidth: 4
};
public configText: TextConfig = {
x: 10,
y: 10,
fontFamily: 'Calibri',
fontSize: 24,
text: 'hello',
fill: 'black'
};
public handleMouseMove(event: any): void {
const mousePos = event.target.getStage().getPointerPosition();
const x = mousePos.x - 190;
const y = mousePos.y - 40;
this.configText = { ...this.configText, text: 'x: ' + x + ', y: ' + y };
}
public handleMouseOut(): void {
this.configText = { ...this.configText, text: 'Mouseout triangle' };
}
}
```
---
# Angular Konva Filters Tutorial
> Learn how to apply visual filters like blur to canvas shapes in Angular using ng2-konva with caching.
Source: https://konvajs.org/docs/angular/Filters.html
To apply filters in Konva, set the `filters` property and cache the node after it is mounted.
This demo uses a blur filter on a circle and calls `cache()` once the underlying Konva node is available.
**Instructions**: The demo renders a blurred red circle. Remove `cache()` in the editor to see why caching is required for filters.
For more details, see the [Filters API Reference](/api/Konva.Filters.html) and [`cache()` documentation](/api/Konva.Node.html#cache).
## Filters Example
```js
import { Component, ViewChild } from '@angular/core';
import { StageConfig } from 'konva/lib/Stage';
import { CircleConfig } from 'konva/lib/shapes/Circle';
import Konva from 'konva';
import {
CoreShapeComponent,
StageComponent,
} from 'ng2-konva';
@Component({
selector: 'app-root',
standalone: true,
template: `
`,
imports: [StageComponent, CoreShapeComponent],
})
export default class App {
@ViewChild('circle') circle!: any;
public configStage: StageConfig = {
width: window.innerWidth,
height: window.innerHeight,
};
public configCircle: CircleConfig = {
x: 150,
y: 150,
radius: 50,
fill: 'red',
filters: [Konva.Filters.Blur],
blurRadius: 15
};
ngAfterViewInit() {
this.circle.getNode().cache({ offset: 10 });
}
}
```
---
# Angular Konva Images Tutorial
> Learn how to load and display images on an HTML5 canvas in Angular using ng2-konva Image component.
Source: https://konvajs.org/docs/angular/Images.html
To display images with `Konva` in Angular, use `ko-image` and assign a loaded `HTMLImageElement` to the `image` property.
For Angular 21 apps, signals are the simplest way to update image config from async callbacks such as `Image.onload`.
**Instructions**: The demo loads an external image and renders it after the browser finishes loading it.
For more details, see the [Image API Reference](/api/Konva.Image.html).
## Images Example
```js
import { Component, OnInit, signal } from '@angular/core';
import { StageConfig } from 'konva/lib/Stage';
import { ImageConfig } from 'konva/lib/shapes/Image';
import {
CoreShapeComponent,
StageComponent,
} from 'ng2-konva';
@Component({
selector: 'app-root',
standalone: true,
template: `
`,
imports: [StageComponent, CoreShapeComponent],
})
export default class App implements OnInit {
public configStage: StageConfig = {
width: window.innerWidth,
height: window.innerHeight,
};
public configImage = signal({
x: 50,
y: 50,
image: null,
width: 100,
height: 100
});
ngOnInit() {
const imageObj = new Image();
imageObj.onload = () => {
this.configImage.update((config) => ({
...config,
image: imageObj
}));
};
imageObj.src = 'https://konvajs.org/assets/yoda.jpg';
}
}
```
---
# Angular Konva Shapes Tutorial
> Learn how to draw rectangles, circles, lines, and text on canvas in Angular using ng2-konva shape components.
Source: https://konvajs.org/docs/angular/Shapes.html
All `ng2-konva` shape components map to Konva shapes with the `ko-` prefix, so you can pass any regular Konva shape settings through the `config` object.
This example shows several common shapes with different styling options such as shadows, gradients, and line tension.
**Instructions**: The demo renders text, a rectangle, a circle, and a closed line with a gradient fill.
For more details, see the [Konva API Reference](/api/Konva.html) and the [Line API Reference](/api/Konva.Line.html).
## Shapes Example
```js
import { Component } from '@angular/core';
import { StageConfig } from 'konva/lib/Stage';
import { TextConfig } from 'konva/lib/shapes/Text';
import { RectConfig } from 'konva/lib/shapes/Rect';
import { CircleConfig } from 'konva/lib/shapes/Circle';
import { LineConfig } from 'konva/lib/shapes/Line';
import {
CoreShapeComponent,
StageComponent,
} from 'ng2-konva';
@Component({
selector: 'app-root',
standalone: true,
template: `
`,
imports: [StageComponent, CoreShapeComponent],
})
export default class App {
public configStage: StageConfig = {
width: window.innerWidth,
height: window.innerHeight,
};
public configText: TextConfig = {
text: 'Some text on canvas',
fontSize: 15
};
public configRect: RectConfig = {
x: 20,
y: 50,
width: 100,
height: 100,
fill: 'red',
shadowBlur: 10
};
public configCircle: CircleConfig = {
x: 200,
y: 100,
radius: 50,
fill: 'green'
};
public configLine: LineConfig = {
x: 20,
y: 200,
points: [0, 0, 100, 0, 100, 100],
tension: 0.5,
closed: true,
stroke: 'black',
fillLinearGradientStartPoint: { x: -50, y: -50 },
fillLinearGradientEndPoint: { x: 50, y: 50 },
fillLinearGradientColorStops: [0, 'red', 1, 'yellow']
};
}
```
---
# Angular Konva Simple Animations Tutorial
> Learn how to create simple canvas animations in Angular using ng2-konva with the Konva.Animation class.
Source: https://konvajs.org/docs/angular/Simple_Animations.html
Konva provides two main animation tools: `node.to()` for simple transitions and `Konva.Animation` for frame-by-frame updates.
This example uses `Konva.Animation` directly to move a circle in a sine wave after the view initializes.
**Instructions**: The demo continuously animates the red circle left and right.
For more details, see the [Animation docs](/docs/animations/Rotation.html) and the [Node API Reference](/api/Konva.Node.html).
## Simple Animation Example
```js
import { Component, ViewChild, OnInit, OnDestroy } from '@angular/core';
import { StageConfig } from 'konva/lib/Stage';
import { CircleConfig } from 'konva/lib/shapes/Circle';
import Konva from 'konva';
import {
CoreShapeComponent,
StageComponent,
} from 'ng2-konva';
@Component({
selector: 'app-root',
standalone: true,
template: `
`,
imports: [StageComponent, CoreShapeComponent],
})
export default class App implements OnInit, OnDestroy {
@ViewChild('circle') circle!: any;
private animation: any = null;
public configStage: StageConfig = {
width: window.innerWidth,
height: window.innerHeight,
};
public configCircle: CircleConfig = {
x: 100,
y: 100,
radius: 50,
fill: 'red',
stroke: 'black',
strokeWidth: 4
};
ngAfterViewInit() {
const circle = this.circle.getNode();
this.animation = new Konva.Animation((frame: any) => {
const time = frame.time;
const x = 100 + Math.sin(time / 1000) * 100;
circle.x(x);
}, circle.getLayer());
this.animation.start();
}
ngOnDestroy() {
if (this.animation) {
this.animation.stop();
}
}
}
```
---
# Angular Konva Transformer Tutorial
> Learn how to resize and rotate canvas shapes in Angular using ng2-konva Transformer for interactive selection.
Source: https://konvajs.org/docs/angular/Transformer.html
The Transformer tool is attached to nodes imperatively. In Angular, that means creating a `ko-transformer` and connecting it to the selected shape after the view is ready.
**Instructions**: Click the rectangle to select it, drag it to move it, and click on the empty stage to clear the selection.
For more details, see the [Transformer API Reference](/api/Konva.Transformer.html).
## Transformer Example
```js
import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { StageConfig } from 'konva/lib/Stage';
import { RectConfig } from 'konva/lib/shapes/Rect';
import {
CoreShapeComponent,
StageComponent,
} from 'ng2-konva';
@Component({
selector: 'app-root',
standalone: true,
template: `
`,
imports: [StageComponent, CoreShapeComponent],
})
export default class App implements AfterViewInit {
@ViewChild('rect') rect!: any;
@ViewChild('transformer') transformer!: any;
public configStage: StageConfig = {
width: window.innerWidth,
height: window.innerHeight,
};
public configRect: RectConfig = {
x: 100,
y: 100,
width: 100,
height: 100,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
draggable: true
};
ngAfterViewInit() {
this.transformer.getNode().nodes([this.rect.getNode()]);
}
public handleStageClick(event: any): void {
if (event?.target === event.target.getStage()) {
this.transformer.getNode().nodes([]);
}
}
public handleShapeClick(): void {
this.transformer.getNode().nodes([this.rect.getNode()]);
}
}
```
---
# Angular Konva Undo-Redo Tutorial
> Learn how to implement undo and redo for canvas interactions in Angular using ng2-konva with state history tracking.
Source: https://konvajs.org/docs/angular/Undo-Redo.html
Undo and redo work best when canvas state is stored in plain data objects instead of reading values back from the canvas on every render.
This example stores rectangle positions in a small history stack and restores previous snapshots when you click Undo or Redo.
**Instructions**: Drag the rectangle to create history entries, then click Undo and Redo to move through the saved states.
For more details, see the [Rect API Reference](/api/Konva.Rect.html).
## Undo-Redo Example
```js
import { Component } from '@angular/core';
import { StageConfig } from 'konva/lib/Stage';
import { RectConfig } from 'konva/lib/shapes/Rect';
import {
CoreShapeComponent,
StageComponent,
} from 'ng2-konva';
@Component({
selector: 'app-root',
standalone: true,
template: `
Undo
Redo
`,
imports: [StageComponent, CoreShapeComponent],
})
export default class App {
private history: RectConfig[] = [];
private currentIndex: number = -1;
public configStage: StageConfig = {
width: window.innerWidth,
height: window.innerHeight,
};
public configRect: RectConfig = {
x: 100,
y: 100,
width: 100,
height: 100,
fill: 'red',
draggable: true
};
constructor() {
this.saveState();
}
private saveState(): void {
// Remove any states after current index
this.history = this.history.slice(0, this.currentIndex + 1);
// Add current state
this.history.push({ ...this.configRect });
this.currentIndex++;
}
public handleDragEnd(event: any): void {
this.configRect = {
...this.configRect,
x: event.target.x(),
y: event.target.y()
};
this.saveState();
}
public undo(): void {
if (this.canUndo()) {
this.currentIndex--;
this.configRect = { ...this.history[this.currentIndex] };
}
}
public redo(): void {
if (this.canRedo()) {
this.currentIndex++;
this.configRect = { ...this.history[this.currentIndex] };
}
}
public canUndo(): boolean {
return this.currentIndex > 0;
}
public canRedo(): boolean {
return this.currentIndex < this.history.length - 1;
}
}
```
## Where a hand-built history stops
The history above records one value per step. A production editor has to record
grouped operations, so that a multi-select drag undoes as a single step, plus
transforms and images that finish loading after the action. That state machine
usually grows larger than the drawing code, so plan the history around document
operations rather than around raw node state.
---
# Angular Konva Z-Index Tutorial
> Learn how to control shape stacking order and z-index in Angular using ng2-konva by managing the data array.
Source: https://konvajs.org/docs/angular/zIndex.html
To control shape stacking order in Angular, update the order of the data array that renders your shapes.
The demo shows how to:
1. Create an array of circle shapes with random positions and colors
2. Handle drag events to update the visual order of shapes
3. Maintain the correct stacking order by manipulating the array order
4. Keep rendering driven by Angular state instead of imperative `zIndex()` calls
**Instructions**: Try to drag a circle. When you start dragging, it will automatically move to the top of the stack. This is achieved by manipulating the array of circles in our data, not by manually changing zIndex.
```js
import { Component, OnInit } from '@angular/core';
import { StageConfig } from 'konva/lib/Stage';
import { CircleConfig } from 'konva/lib/shapes/Circle';
import {
CoreShapeComponent,
StageComponent,
} from 'ng2-konva';
@Component({
selector: 'app-root',
standalone: true,
template: `
@for (item of items; track trackById($index, item)) {
}
`,
imports: [StageComponent, CoreShapeComponent],
})
export default class App implements OnInit {
public configStage: StageConfig = {
width: window.innerWidth,
height: window.innerHeight,
};
public items: CircleConfig[] = [];
private dragItemId: string | null = null;
ngOnInit() {
this.generateItems();
}
private generateItems(): void {
const newItems: CircleConfig[] = [];
for (let i = 0; i < 10; i++) {
newItems.push({
x: Math.random() * this.configStage.width!,
y: Math.random() * this.configStage.height!,
radius: 50,
id: "node-" + i,
fill: this.getRandomColor(),
draggable: true
});
}
this.items = newItems;
}
private getRandomColor(): string {
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9'];
return colors[Math.floor(Math.random() * colors.length)];
}
public handleDragstart(event: any): void {
this.dragItemId = event.target.id();
const item = this.items.find(i => i.id === this.dragItemId);
if (item) {
this.items = [
...this.items.filter((i) => i.id !== this.dragItemId),
item
];
}
}
public handleDragend(): void {
this.dragItemId = null;
}
public trackById(index: number, item: CircleConfig): string {
return item.id as string;
}
}
```
---
# Animate Position Tutorial
> Learn how to animate shape position on HTML5 Canvas with Konva.js. Move shapes smoothly using Konva.Animation for frame-based updates.
Source: https://konvajs.org/docs/animations/Moving.html
To animate a shape's position with Konva, we can create a new animation with `Konva.Animation`
which modifies the shape's position with each animation frame.
For a full list of attributes and methods, check out the [Konva.Animation documentation](/api/Konva.Animation.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const circle = new Konva.Circle({
x: 50,
y: window.innerHeight / 2,
radius: 30,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
layer.add(circle);
const amplitude = 100;
const period = 2000; // in milliseconds
const anim = new Konva.Animation(function(frame) {
circle.x(
amplitude * Math.sin((frame.time * 2 * Math.PI) / period) +
window.innerWidth / 2
);
}, layer);
anim.start();
````
```js
import { Stage, Layer, Circle } from 'react-konva';
import { useEffect, useRef } from 'react';
const App = () => {
const circleRef = useRef(null);
useEffect(() => {
const amplitude = 100;
const period = 2000; // in milliseconds
const anim = new Konva.Animation((frame) => {
circleRef.current.x(
amplitude * Math.sin((frame.time * 2 * Math.PI) / period) +
window.innerWidth / 2
);
}, circleRef.current.getLayer());
anim.start();
return () => {
anim.stop();
};
}, []);
return (
);
};
export default App;
````
```js
```
---
# Rotation Animation tutorial
> Learn how to animate shape rotation on HTML5 Canvas with Konva.js. Create smooth rotation animations using Konva.Animation.
Source: https://konvajs.org/docs/animations/Rotation.html
To animate a shape's rotation with Konva, we can create a new animation with
`Konva.Animation`, and define a function which modifies the shape's rotation with each animation frame.
In this tutorial, we'll rotate a blue rectangle about the top left corner,
a yellow rectangle about its center, and a red rectangle about an outside point.
For a full list of attributes and methods, check out the [Konva.Animation documentation](/api/Konva.Animation.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// blue rectangle - rotate around top-left corner
const blueRect = new Konva.Rect({
x: 50,
y: 50,
width: 100,
height: 50,
fill: '#00D2FF',
stroke: 'black',
strokeWidth: 4,
offset: {
x: 0,
y: 0,
},
});
// yellow rectangle - rotate around center
const yellowRect = new Konva.Rect({
x: 200,
y: 50,
width: 100,
height: 50,
fill: 'yellow',
stroke: 'black',
strokeWidth: 4,
offset: {
x: 50,
y: 25,
},
});
// red rectangle - rotate around point outside shape
const redRect = new Konva.Rect({
x: 350,
y: 50,
width: 100,
height: 50,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
offset: {
x: -50,
y: 25,
},
});
layer.add(blueRect);
layer.add(yellowRect);
layer.add(redRect);
const angularSpeed = 90;
const anim = new Konva.Animation(function(frame) {
const angleDiff = (frame.timeDiff * angularSpeed) / 1000;
blueRect.rotate(angleDiff);
yellowRect.rotate(angleDiff);
redRect.rotate(angleDiff);
}, layer);
anim.start();
````
```js
import { Stage, Layer, Rect } from 'react-konva';
import { useEffect, useRef } from 'react';
const App = () => {
const blueRectRef = useRef(null);
const yellowRectRef = useRef(null);
const redRectRef = useRef(null);
useEffect(() => {
const angularSpeed = 90;
const anim = new Konva.Animation((frame) => {
const angleDiff = (frame.timeDiff * angularSpeed) / 1000;
blueRectRef.current.rotate(angleDiff);
yellowRectRef.current.rotate(angleDiff);
redRectRef.current.rotate(angleDiff);
}, blueRectRef.current.getLayer());
anim.start();
return () => {
anim.stop();
};
}, []);
return (
);
};
export default App;
````
```js
```
---
# HTML5 Canvas Konva Scale Animation Tutorial
> Learn how to animate shape scaling on the HTML5 canvas using Konva.Animation with scaleX and scaleY properties.
Source: https://konvajs.org/docs/animations/Scaling.html
To animate a shape's scale with Konva, we can create a new animation with
`Konva.Animation`, and define a function which modifies the shape's scale with each animation frame.
In this tutorial, we'll scale the x and y component of a blue hexagon, the y component
of a yellow hexagon, and the x component of a red hexagon about an axis positioned on the right side of the shape.
**Instructions:** drag and drop the hexagons as they animate
For a full list of attributes and methods, check out the [Konva.Animation documentation](/api/Konva.Animation.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// blue hexagon - scale x and y
const blueHex = new Konva.RegularPolygon({
x: 50,
y: 50,
sides: 6,
radius: 20,
fill: '#00D2FF',
stroke: 'black',
strokeWidth: 4,
draggable: true
});
// yellow hexagon - scale y only
const yellowHex = new Konva.RegularPolygon({
x: 150,
y: 50,
sides: 6,
radius: 20,
fill: 'yellow',
stroke: 'black',
strokeWidth: 4,
draggable: true
});
// red hexagon - scale x only
const redHex = new Konva.RegularPolygon({
x: 250,
y: 50,
sides: 6,
radius: 20,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
draggable: true
});
layer.add(blueHex);
layer.add(yellowHex);
layer.add(redHex);
const period = 2000;
const anim = new Konva.Animation(function(frame) {
const scale = Math.sin(frame.time * 2 * Math.PI / period) + 2;
// blue hex - scale x and y
blueHex.scale({ x: scale, y: scale });
// yellow hex - scale y only
yellowHex.scaleY(scale);
// red hex - scale x only
redHex.scaleX(scale);
}, layer);
anim.start();
````
```js
import { Stage, Layer, RegularPolygon } from 'react-konva';
import { useEffect, useRef, useState } from 'react';
const App = () => {
const blueHexRef = useRef(null);
const yellowHexRef = useRef(null);
const redHexRef = useRef(null);
const [positions, setPositions] = useState({
blue: { x: 50, y: 50 },
yellow: { x: 150, y: 50 },
red: { x: 250, y: 50 }
});
useEffect(() => {
const period = 2000;
const anim = new Konva.Animation((frame) => {
const scale = Math.sin(frame.time * 2 * Math.PI / period) + 2;
// blue hex - scale x and y
blueHexRef.current.scale({ x: scale, y: scale });
// yellow hex - scale y only
yellowHexRef.current.scaleY(scale);
// red hex - scale x only
redHexRef.current.scaleX(scale);
}, blueHexRef.current.getLayer());
anim.start();
return () => {
anim.stop();
};
}, []);
const handleDragEnd = (e, color) => {
setPositions(prev => ({
...prev,
[color]: { x: e.target.x(), y: e.target.y() }
}));
};
return (
handleDragEnd(e, 'blue')}
/>
handleDragEnd(e, 'yellow')}
/>
handleDragEnd(e, 'red')}
/>
);
};
export default App;
````
```js
```
---
# HTML5 Canvas Konva Stop Animation Tutorial
> Learn how to start and stop canvas animations in Konva using the start() and stop() methods on Konva.Animation.
Source: https://konvajs.org/docs/animations/Stop_Animation.html
To stop an animation with Konva, we can use the `stop()` method.
To restart the animation, we can again call the `start()`.
**Instructions:** Click on "Start" to start the animation and "Stop" to stop the animation.
For a full list of attributes and methods, check out the [Konva.Animation documentation](/api/Konva.Animation.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const circle = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 30,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
layer.add(circle);
// add buttons
const container = document.createElement('div');
document.body.appendChild(container);
container.style.position = 'absolute';
container.style.top = '0px';
container.style.left = '0px';
const startBtn = document.createElement('button');
startBtn.textContent = 'Start Animation';
container.appendChild(startBtn);
const stopBtn = document.createElement('button');
stopBtn.textContent = 'Stop Animation';
container.appendChild(stopBtn);
const anim = new Konva.Animation(function(frame) {
circle.x(
amplitude * Math.sin((frame.time * 2 * Math.PI) / period) +
stage.width() / 2
);
}, layer);
const amplitude = 100;
const period = 2000;
startBtn.addEventListener('click', () => anim.start());
stopBtn.addEventListener('click', () => anim.stop());
````
```js
import { Stage, Layer, Circle } from 'react-konva';
import { useEffect, useRef, useState } from 'react';
const App = () => {
const circleRef = useRef(null);
const [isAnimating, setIsAnimating] = useState(false);
const animRef = useRef(null);
useEffect(() => {
const amplitude = 100;
const period = 2000;
animRef.current = new Konva.Animation((frame) => {
circleRef.current.x(
amplitude * Math.sin((frame.time * 2 * Math.PI) / period) +
window.innerWidth / 2
);
}, circleRef.current.getLayer());
return () => {
if (animRef.current) {
animRef.current.stop();
}
};
}, []);
const handleStart = () => {
animRef.current.start();
setIsAnimating(true);
};
const handleStop = () => {
animRef.current.stop();
setIsAnimating(false);
};
return (
Start Animation
Stop Animation
);
};
export default App;
````
```js
Start Animation
Stop Animation
```
---
# Text Animations Tutorial
> Learn how to create character-by-character text animations in Konva using the charRenderFunc property for per-character rendering effects.
Source: https://konvajs.org/docs/animations/Text_Animations.html
**Note: this feature is only available from Konva v10.0.0.**
Konva provides powerful text animation capabilities through the `charRenderFunc` property. This function allows you to customize how each character is rendered, enabling character-by-character animations and effects.
```js
var text = new Konva.Text({
x: 10,
y: 10,
text: 'AB',
fontSize: 20,
charRenderFunc: function ({ context, index }) {
if (index === 1) {
// shift only the second character
context.translate(0, 10);
}
},
});
```
The `charRenderFunc` receives a context object with the following parameters:
- **`char`** - The actual character string being rendered
- **`index`** - Zero-based index of the character in the entire text
- **`x`** - X position where the character will be rendered
- **`y`** - Y position where the character will be rendered
- **`lineIndex`** - Zero-based index of the line containing this character
- **`column`** - Zero-based column position within the current line
- **`isLastInLine`** - Boolean indicating if this is the last character in its line
- **`width`** - Width of the character
- **`context`** - Canvas 2D rendering context for applying transformations, opacity, colors, etc.
This allows you to apply transformations, opacity changes, or other effects to individual characters based on their position and properties.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// we will store the opacity of each character in an array
const charOpacities = [];
const textNode = new Konva.Text({
x: window.innerWidth / 2 - 100,
y: window.innerHeight / 2 - 20,
text: 'ANIMATION',
fontSize: 40,
fontFamily: 'Arial',
fill: '#333',
charRenderFunc: function ({ context, index }) {
context.globalAlpha = charOpacities[index];
},
});
layer.add(textNode);
const anim = new Konva.Animation(function(frame) {
const time = frame.time;
const cycleDuration = 4000; // 4 seconds total cycle
const fadeInDuration = 1500; // 1.5 seconds to fade in all
const holdDuration = 1000; // 1 second hold
const fadeOutDuration = 1500; // 1.5 seconds to fade out all
const cycleTime = time % cycleDuration;
for (let i = 0; i < textNode.text().length; i++) {
const charDelay = i * 150; // 150ms delay between characters
if (cycleTime < fadeInDuration) {
// Fade in phase
const charStartTime = charDelay;
const charFadeTime = Math.max(0, cycleTime - charStartTime);
charOpacities[i] = Math.min(1, charFadeTime / 300);
} else if (cycleTime < fadeInDuration + holdDuration) {
// Hold phase - all characters visible
charOpacities[i] = 1;
} else {
// Fade out phase
const fadeOutStart = fadeInDuration + holdDuration;
const charFadeOutDelay = i * 150; // Same order as fade in
const charFadeOutTime = Math.max(0, cycleTime - fadeOutStart - charFadeOutDelay);
charOpacities[i] = Math.max(0, 1 - charFadeOutTime / 300);
}
}
}, layer);
anim.start();
```
```js
import { Stage, Layer, Text } from 'react-konva';
import { useEffect, useRef } from 'react';
const App = () => {
const textRef = useRef(null);
const layerRef = useRef(null);
const charOpacitiesRef = useRef([]);
useEffect(() => {
const anim = new Konva.Animation((frame) => {
const time = frame.time;
const cycleDuration = 4000; // 4 seconds total cycle
const fadeInDuration = 1500; // 1.5 seconds to fade in all
const holdDuration = 1000; // 1 second hold
const fadeOutDuration = 1500; // 1.5 seconds to fade out all
const cycleTime = time % cycleDuration;
for (let i = 0; i < textRef.current.text().length; i++) {
const charDelay = i * 150; // 150ms delay between characters
if (cycleTime < fadeInDuration) {
// Fade in phase
const charStartTime = charDelay;
const charFadeTime = Math.max(0, cycleTime - charStartTime);
charOpacitiesRef.current[i] = Math.min(1, charFadeTime / 300);
} else if (cycleTime < fadeInDuration + holdDuration) {
// Hold phase - all characters visible
charOpacitiesRef.current[i] = 1;
} else {
// Fade out phase
const fadeOutStart = fadeInDuration + holdDuration;
const charFadeOutDelay = i * 150; // Same order as fade in
const charFadeOutTime = Math.max(0, cycleTime - fadeOutStart - charFadeOutDelay);
charOpacitiesRef.current[i] = Math.max(0, 1 - charFadeOutTime / 300);
}
}
}, layerRef.current);
anim.start();
return () => {
anim.stop();
};
}, []);
return (
{
context.globalAlpha = charOpacitiesRef.current[index] || 0;
}}
/>
);
};
export default App;
```
```js
```
---
# Clipping Functions Tutorial
> Learn how to use custom clipping functions with clipFunc to create complex clipping regions for groups and layers in Konva.
Source: https://konvajs.org/docs/clipping/Clipping_Function.html
## How to clip nodes in the layer?
To draw things inside of complex clipping regions with Konva, we can set the `clipFunc`
property of a group, a layer.
In this tutorial, we'll draw blobs inside of a two circles clipping region applied to a group.
```js
import Konva from 'konva';
// First we need to create stage
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
// Then create layer
const layer = new Konva.Layer();
const group = new Konva.Group({
clipFunc: function (ctx) {
ctx.beginPath();
ctx.arc(200, 120, 50, 0, Math.PI * 2, false);
ctx.arc(280, 120, 50, 0, Math.PI * 2, false);
},
});
for (let i = 0; i < 20; i++) {
const blob = new Konva.Circle({
x: Math.random() * stage.width(),
y: Math.random() * stage.height(),
radius: Math.random() * 50,
fill: 'green',
opacity: 0.8,
});
group.add(blob);
}
// add the shape to the layer
layer.add(group);
// add the layer to the stage
stage.add(layer);
```
```js
import { Stage, Layer, Group, Circle } from 'react-konva';
const App = () => {
const blobs = Array.from({ length: 20 }, (_, i) => ({
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
radius: Math.random() * 50,
}));
return (
{
ctx.beginPath();
ctx.arc(200, 120, 50, 0, Math.PI * 2, false);
ctx.arc(280, 120, 50, 0, Math.PI * 2, false);
}}
>
{blobs.map((blob, i) => (
))}
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Simple Clipping tutorial
> Learn how to apply simple rectangular clipping regions to groups and layers in Konva using the clip property.
Source: https://konvajs.org/docs/clipping/Clipping_Regions.html
To draw things inside of clipping regions with Konva, we can set the `clip`
property of a group or a layer.
Clipping regions are defined by an `x`, `y`, `width`, and `height`. In this tutorial,
we'll draw blobs inside of a rectangular clipping region applied to a group.
For more complex cases take a look into clipping function. [Clipping Function](/docs/clipping/Clipping_Function.html)
```js
import Konva from 'konva';
// First we need to create stage
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
// Then create layer
const layer = new Konva.Layer();
const group = new Konva.Group({
clip: {
x: 100,
y: 20,
width: 200,
height: 200,
},
});
for (let i = 0; i < 20; i++) {
const blob = new Konva.Circle({
x: Math.random() * stage.width(),
y: Math.random() * stage.height(),
radius: Math.random() * 50,
fill: 'green',
opacity: 0.8,
});
group.add(blob);
}
// add the shape to the layer
layer.add(group);
// add the layer to the stage
stage.add(layer);
```
```js
import { Stage, Layer, Group, Circle } from 'react-konva';
const App = () => {
const blobs = Array.from({ length: 20 }, (_, i) => ({
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
radius: Math.random() * 50,
}));
return (
{blobs.map((blob, i) => (
))}
);
};
export default App;
```
```js
```
---
# Save and Load HTML5 Canvas Stage Best Practices
> Best practices for saving and loading HTML5 Canvas state with Konva.js. Tips for serialization, data management, and state persistence.
Source: https://konvajs.org/docs/data_and_serialization/Best_Practices.html
## What is the best way to save/load full stage content and how to implement undo/redo?
If you want to save/load simple canvas content you can use the built-in `Konva` methods: `node.toJSON()` and `Node.create(json)`.
See [simple](/docs/data_and_serialization/Simple_Load.html) and [complex](/docs/data_and_serialization/Complex_Load.html) demos.
But those methods are useful only in very small apps. In bigger apps it is VERY hard to use those methods. Why? Because the tree structure is usually very complex in larger apps, you may have a lot of event listeners, images, filters, etc. That data is not serializable into JSON (or it is very hard to do that).
Also it is very common that nodes in a tree have a lot information that is not directly related to the state of your app, but just used to describe visual view of your app.
For instance, let's think we have a game, that draws several balls in canvas. The balls are not just circles, but the complex visual groups of objects with shadows and texts inside them (like "Made in China"). Now let's think you want to serialize state of your app and use it somewhere else. Like send to another computer or implement undo/redo. Almost all the visual information (shadows, texts, sizes) is not critical and may be you don't need to save it. Because all balls have the same shadows, sizes, etc. But what is critical? In that case it is just a number of balls and their coordinates. You need to save/load only that information. It will be just a simple array:
```javascript
var state = [{x: 10, y: 10}, { x: 160, y: 1041}]
```
Now when you have that information, you need to have a function, that can create the whole canvas structure.
If you want to update your canvas, for instance, you want to create a new ball, you don't need to create a new canvas node directly (like creating new instance of `Konva.Circle`), you just need to push a new object into a state and update (or recreate) canvas.
In that case you don't need to care about image loading, filters, event listeners, etc in saving/loading phases. Because you do all these actions in your `create` or `update` functions.
You would better understand what I am talking about if you know how many modern frameworks work (like `React`, `Vue`, `Angular` and many other).
Also take a look into these demos to have a better idea:
1. [Undo/redo with react](/docs/react/Undo-Redo.html)
1. [Save/load with Vue](/docs/vue/Save-Load.html)
How to implement that `create` and `update` functions? It depends. From my point of view it will be easier to use frameworks that can do that job for you, like [react-konva](/docs/react/index.html).
If you don't want to use such frameworks you need to think in terms of your own app. Here I will try to make a small demo to give you an idea.
The super naive method is to implement just one function `create(state)` that will do all the complex job of loading.
If you have some changes in your app you just need to destroy the canvas and create a new one. But the drawback of such approach is possibly a bad performance.
A bit smarter implementation is to create two functions `create(state)` and `update(state)`. `create` will make instances of all required objects, attach events and load images. `update` will update properties of nodes. If number of objects is changed - destroy all and create from scratch. If only some properties changed - call `update`.
**Instructions:** In that demo we will have a bunch of images with filters, and you can add more, move them, apply a new filter by clicking on images and use undo/redo.
```js
import Konva from 'konva';
// Initial state
let state = {
images: [
{ x: 50, y: 50, filter: 'none' },
{ x: 150, y: 50, filter: 'blur' }
]
};
// History for undo/redo
const history = [JSON.stringify(state)];
let historyStep = 0;
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// Create container
const container = document.createElement('div');
container.style.position = 'relative';
document.body.appendChild(container);
// Create button container
const buttonContainer = document.createElement('div');
buttonContainer.style.position = 'absolute';
buttonContainer.style.top = '10px';
buttonContainer.style.left = '10px';
buttonContainer.style.zIndex = '10';
container.appendChild(buttonContainer);
// Create UI buttons
const addButton = document.createElement('button');
addButton.textContent = 'Add Image';
addButton.style.margin = '0 5px';
buttonContainer.appendChild(addButton);
const undoButton = document.createElement('button');
undoButton.textContent = 'Undo';
undoButton.style.margin = '0 5px';
buttonContainer.appendChild(undoButton);
const redoButton = document.createElement('button');
redoButton.textContent = 'Redo';
redoButton.style.margin = '0 5px';
buttonContainer.appendChild(redoButton);
// Move stage container into our container
const stageContainer = document.getElementById('container');
container.appendChild(stageContainer);
stageContainer.style.position = 'absolute';
stageContainer.style.top = '0';
stageContainer.style.left = '0';
// Load image
const imageObj = new Image();
imageObj.src = 'https://konvajs.org/assets/lion.png';
function createImage(imageConfig) {
const image = new Konva.Image({
image: imageObj,
x: imageConfig.x,
y: imageConfig.y,
width: 100,
height: 100,
draggable: true
});
if (imageConfig.filter === 'blur') {
image.cache();
image.filters([Konva.Filters.Blur]);
image.blurRadius(10);
}
return image;
}
function create(state) {
layer.destroyChildren();
state.images.forEach(imgConfig => {
const image = createImage(imgConfig);
image.on('dragend', () => {
const pos = image.position();
const index = layer.children.indexOf(image);
state.images[index] = {
...state.images[index],
x: pos.x,
y: pos.y
};
saveHistory();
});
image.on('click', () => {
const index = layer.children.indexOf(image);
state.images[index] = {
...state.images[index],
filter: state.images[index].filter === 'none' ? 'blur' : 'none'
};
saveHistory();
create(state);
});
layer.add(image);
});
}
function saveHistory() {
historyStep++;
history.length = historyStep;
history.push(JSON.stringify(state));
}
// Add event listeners
addButton.addEventListener('click', () => {
state.images.push({
x: Math.random() * stage.width(),
y: Math.random() * stage.height(),
filter: 'none'
});
saveHistory();
create(state);
});
undoButton.addEventListener('click', () => {
if (historyStep === 0) return;
historyStep--;
state = JSON.parse(history[historyStep]);
create(state);
});
redoButton.addEventListener('click', () => {
if (historyStep === history.length - 1) return;
historyStep++;
state = JSON.parse(history[historyStep]);
create(state);
});
imageObj.onload = () => {
create(state);
};
```
```js
import Konva from 'konva';
import { Stage, Layer, Image } from 'react-konva';
import { useState, useEffect, useRef } from 'react';
import useImage from 'use-image';
const FilteredImage = ({ filter, ...props }) => {
const imageRef = useRef();
useEffect(() => {
imageRef.current?.cache();
}, [props.image]);
return (
);
};
const App = () => {
const [images, setImages] = useState([
{ x: 50, y: 50, filter: 'none' },
{ x: 150, y: 50, filter: 'blur' }
]);
const [history, setHistory] = useState([]);
const [historyStep, setHistoryStep] = useState(0);
const [lionImage] = useImage('https://konvajs.org/assets/lion.png', 'anonymous');
useEffect(() => {
if (lionImage) {
setHistory([JSON.stringify(images)]);
}
}, [lionImage]);
const handleDragEnd = (index, e) => {
const newImages = [...images];
newImages[index] = {
...newImages[index],
x: e.target.x(),
y: e.target.y()
};
setImages(newImages);
saveHistory(newImages);
};
const handleClick = (index) => {
const newImages = [...images];
newImages[index] = {
...newImages[index],
filter: newImages[index].filter === 'none' ? 'blur' : 'none'
};
setImages(newImages);
saveHistory(newImages);
};
const saveHistory = (newImages) => {
const newHistory = history.slice(0, historyStep + 1);
newHistory.push(JSON.stringify(newImages));
setHistory(newHistory);
setHistoryStep(newHistory.length - 1);
};
const handleAdd = () => {
const newImages = [...images, {
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
filter: 'none'
}];
setImages(newImages);
saveHistory(newImages);
};
const handleUndo = () => {
if (historyStep === 0) return;
const newStep = historyStep - 1;
setHistoryStep(newStep);
setImages(JSON.parse(history[newStep]));
};
const handleRedo = () => {
if (historyStep === history.length - 1) return;
const newStep = historyStep + 1;
setHistoryStep(newStep);
setImages(JSON.parse(history[newStep]));
};
return (
Add Image
Undo
Redo
{lionImage && images.map((img, i) => (
handleDragEnd(i, e)}
onClick={() => handleClick(i)}
/>
))}
);
};
export default App;
```
```js
```
---
# Load HTML5 Canvas Stage from JSON Tutorial
> Load a Konva stage from JSON with images and event bindings using Konva.Node.create and find selectors.
Source: https://konvajs.org/docs/data_and_serialization/Complex_Load.html
To load a complex stage that originally contained images and event bindings using Konva,
we need to create a stage node using `Konva.Node.create()`, and then set the
images and event handlers with the help of selectors using the `find()` method.
Images and event handlers must be manually set because they aren't serializable.
**That methods works for small apps. For more complex cases take a look into [Best Practices](/docs/data_and_serialization/Best_Practices.html)**
```js
import Konva from 'konva';
// JSON string from a previous save
const json = '{"attrs":{"width":578,"height":200},"className":"Stage","children":[{"attrs":{},"className":"Layer","children":[{"attrs":{"x":100,"y":100,"sides":6,"radius":70,"fill":"red","stroke":"black","strokeWidth":4},"className":"RegularPolygon"}]}]}';
// create node using json string
const stage = Konva.Node.create(json, 'container');
// get reference to the hexagon
const hexagon = stage.findOne('RegularPolygon');
// bind events
hexagon.on('click', () => {
hexagon.fill(Konva.Util.getRandomColor());
});
```
**Note:** Using `Konva.Node.create()` directly in React is an anti-pattern. In React applications, we should manage state separately from the view. Instead of deserializing entire node structures, we should load the data that defines our shapes and let React components handle the rendering. The example below demonstrates how to load shape data as state in React:
```js
import { Stage, Layer, RegularPolygon } from 'react-konva';
import { useState, useEffect } from 'react';
import Konva from 'konva';
const App = () => {
const [shapeData, setShapeData] = useState(null);
useEffect(() => {
// Simulating loading JSON data from storage or API
const loadData = () => {
// This would typically come from localStorage, API, etc.
const jsonString = '{"hexagon":{"x":100,"y":100,"sides":6,"radius":70,"fill":"red","stroke":"black","strokeWidth":4}}';
try {
// Parse the JSON into a JavaScript object
const data = JSON.parse(jsonString);
setShapeData(data);
} catch (error) {
console.error('Error parsing JSON:', error);
}
};
loadData();
}, []);
const handleClick = () => {
if (shapeData) {
setShapeData({
...shapeData,
hexagon: {
...shapeData.hexagon,
fill: Konva.Util.getRandomColor()
}
});
}
};
// Don't render until we have data
if (!shapeData) return Loading...
;
return (
);
};
export default App;
```
**Note:** Using `Konva.Node.create()` directly in Vue is an anti-pattern. In Vue applications, we should manage state with reactive data separately from the view. Instead of deserializing entire node structures, we should load the data that defines our shapes and let Vue components handle the rendering. The example below demonstrates how to load shape data as reactive state in Vue:
```js
```
---
# HTML5 Canvas Export to High Quality Image Tutorial
> Learn how to export HTML5 Canvas to high-quality PNG or JPEG images with Konva.js. Use stage.toDataURL() with pixelRatio for retina-quality exports.
Source: https://konvajs.org/docs/data_and_serialization/High-Quality-Export.html
If you need to export a stage as an image or as base64 then you can use the `stage.toDataURL()` or `stage.toImage()` methods.
By default in `Konva`, exported images have the `pixelRatio` attribute set to `1`. This means that if you export a stage with a size of `500x500`, then the exported image will have the same size of `500x500`.
In some cases you may want to export an image that is more suited to higher (or even smaller) resolutions. For instance, you may wish to export something as an image and then use that image on a canvas on HDPI devices (with a high pixel ratio, like a retina display). Another scenario may be that you need to export a user's drawing onto a computer running a high resolution.
If you were to do this with the default settings, then you would see a blurred image. You can read more about the global `pixelRatio` attribute here [MDN - devicePixelRatio](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio).
For both of these use cases, you can use:
```javascript
stage.toDataURL({
pixelRatio: 2 // or other value you need
})
```
Now, a stage with a size of `500x500` would be exported as an image with a size of `1000x1000`. Almost all nodes in `Konva` are stored as vector data, apart from bitmap images and cached nodes. This results in a high quality exported image.
**Instructions:** try to save stage as an image. You will see that it has a high resolution.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: 400,
height: 400
});
const layer = new Konva.Layer();
stage.add(layer);
// create some shapes
const circle = new Konva.Circle({
x: 200,
y: 200,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4
});
const text = new Konva.Text({
x: 150,
y: 190,
text: 'High Quality Export',
fontSize: 20,
fill: 'white'
});
layer.add(circle);
layer.add(text);
// add button
const button = document.createElement('button');
button.textContent = 'Save as High Quality Image';
document.body.appendChild(button);
button.addEventListener('click', () => {
// save stage as a high quality image
const dataURL = stage.toDataURL({
pixelRatio: 2 // double resolution
});
// create link to download
const link = document.createElement('a');
link.download = 'stage.png';
link.href = dataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
```
```js
import { Stage, Layer, Circle, Text } from 'react-konva';
import { useRef } from 'react';
const App = () => {
const stageRef = useRef(null);
const handleExport = () => {
const dataURL = stageRef.current.toDataURL({
pixelRatio: 2 // double resolution
});
const link = document.createElement('a');
link.download = 'stage.png';
link.href = dataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
return (
Save as High Quality Image
);
};
export default App;
```
```js
Save as High Quality Image
```
## The browser size limit
Every browser caps canvas width, height, and total area, and the cap is lower on
mobile. Past that point a large `pixelRatio` fails quietly: the call returns a
blank or truncated image instead of raising an error. Large-format output has to
be tiled, or rendered outside the browser.
---
# Load Simple HTML5 Canvas Stage from JSON Tutorial
> Learn how to load an HTML5 Canvas stage from JSON with Konva.js. Restore saved canvas state using Konva.Node.create().
Source: https://konvajs.org/docs/data_and_serialization/Simple_Load.html
To load a simple stage from JSON with Konva, we can use the `Konva.Node.create()` method.
The `create()` method accepts a JSON string and container id as arguments.
```js
import Konva from 'konva';
// JSON string from a previous save
const json = '{"attrs":{"width":400,"height":400},"className":"Stage","children":[{"attrs":{},"className":"Layer","children":[{"attrs":{"x":100,"y":100,"radius":50,"fill":"red","stroke":"black","strokeWidth":3},"className":"Circle"}]}]}';
// create node using json string
const stage = Konva.Node.create(json, 'container');
// you can keep adding events, etc
const circle = stage.findOne('Circle');
circle.on('click', () => {
circle.fill(Konva.Util.getRandomColor());
});
```
**Note:** Using `Konva.Node.create()` directly in React or Vue is an anti-pattern. In these frameworks, we should manage state (data) separately from the view (components). Instead of serializing and loading entire node structures, we should save and load the data that defines our shapes, then let the framework components handle rendering. This approach is more aligned with React and Vue's declarative, state-driven patterns and provides better control over component lifecycle and events.
```js
import { Stage, Layer, Circle } from 'react-konva';
import { useState, useEffect } from 'react';
import Konva from 'konva';
const App = () => {
// In React, we store shape data as state instead of using Konva.Node.create()
const [shapeData, setShapeData] = useState(null);
useEffect(() => {
// Simulating loading JSON data from storage or API
const savedShapeData = {
circle: {
x: 100,
y: 100,
radius: 50,
fill: 'red',
stroke: 'black',
strokeWidth: 3
},
// We could have more shapes here
};
// In a real app, this might be:
// fetch('/api/shapes').then(response => response.json()).then(setShapeData)
setShapeData(savedShapeData);
}, []);
const handleCircleClick = () => {
setShapeData({
...shapeData,
circle: {
...shapeData.circle,
fill: Konva.Util.getRandomColor()
}
});
};
// Don't render until we have data
if (!shapeData) return Loading...
;
return (
);
};
export default App;
```
**Note:** Using `Konva.Node.create()` directly in React or Vue is an anti-pattern. In these frameworks, we should manage state (data) separately from the view (components). Instead of serializing and loading entire node structures, we should save and load the data that defines our shapes, then let the framework components handle rendering. This approach is more aligned with React and Vue's declarative, state-driven patterns and provides better control over component lifecycle and events.
```js
Loading...
```
---
# Canvas Screenshot — Export HTML5 Canvas as Image with JavaScript
> Take a screenshot of your HTML5 Canvas and export it as PNG or JPEG with JavaScript. Use Konva.js toDataURL() to capture canvas content as a base64 image or downloadable file.
Source: https://konvajs.org/docs/data_and_serialization/Stage_Data_URL.html
To take a screenshot of your canvas and export it as an image with `Konva`, use the `toDataURL()`
method. It returns the data URL directly for every node type, including `Stage`.
You can pass in a mime type such as image/jpeg and a quality value that ranges between 0 and 1.
You can also capture screenshots of specific nodes, including layers, groups, and shapes.
*Note: The `toDataURL()` method requires that any images drawn onto the canvas
are hosted on a web server with the same domain as the code executing it.
If this condition is not met, a SECURITY_ERR exception is thrown.*
**Instructions:** Drag and drop the rectangle and then click on the save button to get the composite data url and open the resulting image in a new window.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: 400,
height: 400
});
const layer = new Konva.Layer();
stage.add(layer);
// create draggable rectangle
const rect = new Konva.Rect({
x: 100,
y: 100,
width: 100,
height: 100,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
draggable: true
});
layer.add(rect);
// add button
const button = document.createElement('button');
button.textContent = 'Save as Image';
document.body.appendChild(button);
button.addEventListener('click', () => {
// get data URL with default settings
const dataURL = stage.toDataURL();
// open in new window
const win = window.open();
win.document.write(` `);
// you can also save with different settings
const jpegURL = stage.toDataURL({
mimeType: 'image/jpeg',
quality: 0.8
});
console.log('JPEG URL:', jpegURL);
});
```
```js
import { Stage, Layer, Rect } from 'react-konva';
import { useRef, useState } from 'react';
const App = () => {
const stageRef = useRef(null);
const [position, setPosition] = useState({ x: 100, y: 100 });
const handleExport = () => {
// get data URL with default settings
const dataURL = stageRef.current.toDataURL();
// open in new window
const win = window.open();
win.document.write(` `);
// you can also save with different settings
const jpegURL = stageRef.current.toDataURL({
mimeType: 'image/jpeg',
quality: 0.8
});
console.log('JPEG URL:', jpegURL);
};
const handleDragEnd = (e) => {
setPosition({
x: e.target.x(),
y: e.target.y()
});
};
return (
Save as Image
);
};
export default App;
```
```js
Save as Image
```
---
# How to support and donate to Konva project?
> Support Konva development through Patreon, Open Collective, or GitHub Sponsors to help maintain the framework.
Source: https://konvajs.org/docs/donate.html
Hello, my name is Anton. I am core maintainer of `Konva` framework.
**If you want to support development of `Konva` and all its ecosystem tools like `react-konva` and `vue-konva` you can use:**
- [Patreon](https://www.patreon.com/lavrton)
- [Open Collective](https://opencollective.com/konva)
- [GitHub Sponsor](https://github.com/sponsors/lavrton)
I am spending a large amount of time to support `Konva` users and develop new versions with bugs fixes and new features.
If you are making money, by using `Konva` in your project, it makes sense to support `Konva` development. By doing this you will make sure that you have a good quality and maintained framework.
Even if you are not making money from your project but `Konva` saved you a lot of time, it will be very kind to support it.
### Your company is using Konva?
It may be hard for many developers to make a financial donation. But if you are using `konva` as part of your work in the company, talk to your managers to support the project. Well-supported project is a good value for the company.
---
# HTML5 Canvas Complex Drag and Drop Bounds
> Learn how to constrain drag and drop movement to custom boundaries and regions in Konva using the dragmove event.
Source: https://konvajs.org/docs/drag_and_drop/Complex_Drag_and_Drop.html
To bound the movement of nodes being dragged and dropped inside regions with
Konva, we can use the `dragmove` event to define boundaries that the node cannot cross.
_Tip: you can use `shape.absolutePosition()` method to get/set absolute position of a node, instead of relative `x` and `y`._
**Instructions:** Drag and drop the light blue rectangle and observe that it
is bound below an imaginary boundary at y = 50. Drag and drop the yellow
rectangle and observe that it is bound inside of an imaginary circle.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
const blueGroup = new Konva.Group({
x: 30,
y: 70,
draggable: true,
});
// bound below y=50
blueGroup.on('dragmove', () => {
blueGroup.y(Math.max(blueGroup.y(), 50));
});
// bound inside a circle
const yellowGroup = new Konva.Group({
x: stage.width() / 2,
y: 70,
draggable: true,
});
yellowGroup.on('dragmove', () => {
const x = stage.width() / 2;
const y = 70;
const radius = 50;
const pos = yellowGroup.absolutePosition();
const scale = radius / Math.sqrt(Math.pow(pos.x - x, 2) + Math.pow(pos.y - y, 2));
if (scale < 1) {
yellowGroup.x(Math.round((pos.x - x) * scale + x));
yellowGroup.y(Math.round((pos.y - y) * scale + y));
}
});
const blueText = new Konva.Text({
fontSize: 26,
fontFamily: 'Calibri',
text: 'bound below',
fill: 'black',
padding: 10,
width: 150,
align: 'center',
});
const blueRect = new Konva.Rect({
width: 150,
height: 72,
fill: '#aaf',
stroke: 'black',
strokeWidth: 4,
});
const yellowText = new Konva.Text({
fontSize: 26,
fontFamily: 'Calibri',
text: 'bound in circle',
fill: 'black',
padding: 10,
width: 150,
align: 'center',
});
const yellowRect = new Konva.Rect({
width: 150,
height:72,
fill: 'yellow',
stroke: 'black',
strokeWidth: 4,
});
blueGroup.add(blueRect).add(blueText);
yellowGroup.add(yellowRect).add(yellowText);
layer.add(blueGroup);
layer.add(yellowGroup);
stage.add(layer);
````
```jsx
import { Stage, Layer, Group, Rect, Text } from 'react-konva';
import { useState } from 'react';
const App = () => {
const [bluePosition, setBluePosition] = useState({ x: 30, y: 70 });
const [yellowPosition, setYellowPosition] = useState({
x: window.innerWidth / 2,
y: 70,
});
const handleBlueDragMove = (e) => {
setBluePosition({
x: e.target.x(),
y: Math.max(e.target.y(), 50),
});
};
const handleYellowDragMove = (e) => {
const x = window.innerWidth / 2;
const y = 70;
const radius = 50;
const position = e.target.absolutePosition();
const scale =
radius /
Math.sqrt(
Math.pow(position.x - x, 2) + Math.pow(position.y - y, 2)
);
if (scale < 1) {
setYellowPosition({
x: Math.round((position.x - x) * scale + x),
y: Math.round((position.y - y) * scale + y),
});
} else {
setYellowPosition(position);
}
};
return (
);
};
export default App;
````
```vue
```
---
# HTML5 Canvas Drag and Drop Events
> Learn how to handle drag events on HTML5 Canvas with Konva.js. Use dragstart, dragmove, and dragend events to respond to shape dragging.
Source: https://konvajs.org/docs/drag_and_drop/Drag_Events.html
To detect drag and drop events with Konva, we can use the `on()` method to
bind `dragstart`, `dragmove`, or `dragend` events to a node.
The `on()` method requires an event type and a function to be executed when the event occurs.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const text = new Konva.Text({
x: 40,
y: 40,
text: 'Draggable Text',
fontSize: 20,
draggable: true,
width: 200,
});
layer.add(text);
const status = new Konva.Text({
x: 40,
y: 100,
text: '',
fontSize: 16,
width: 200,
});
layer.add(status);
text.on('dragstart', () => {
status.text('drag started');
});
text.on('dragend', () => {
status.text('drag ended');
});
text.on('dragmove', () => {
status.text('dragging');
});
```
```jsx
import { Stage, Layer, Text } from 'react-konva';
import { useState } from 'react';
const App = () => {
const [position, setPosition] = useState({ x: 40, y: 40 });
const [status, setStatus] = useState('');
const handleDrag = (e, nextStatus) => {
setPosition({ x: e.target.x(), y: e.target.y() });
setStatus(nextStatus);
};
return (
setStatus('drag started')}
onDragEnd={(e) => handleDrag(e, 'drag ended')}
onDragMove={(e) => handleDrag(e, 'dragging')}
/>
);
};
export default App;
```
```vue
```
---
# HTML5 Canvas Drag and Drop a Group Tutorial
> Learn how to drag and drop a group of shapes together on the HTML5 canvas using Konva's draggable property.
Source: https://konvajs.org/docs/drag_and_drop/Drag_a_Group.html
To drag and drop groups with Konva, we can set the `draggable` property
of the config object to `true` when the group is instantiated, or we can use the `draggable()` method.
Note: remember, dragging a group, do not change the `x` and `y` properties of any of the children nodes. Instead properties of group itself are changed.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const group = new Konva.Group({
draggable: true,
});
layer.add(group);
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'purple'];
for (let i = 0; i < 6; i++) {
const box = new Konva.Rect({
x: i * 30 + 10,
y: i * 18 + 40,
width: 100,
height: 50,
name: colors[i],
fill: colors[i],
stroke: 'black',
strokeWidth: 4,
});
group.add(box);
}
group.on('mouseover', function () {
document.body.style.cursor = 'move';
});
group.on('mouseout', function () {
document.body.style.cursor = 'default';
});
```
```jsx
import { Stage, Layer, Group, Rect, Text } from 'react-konva';
const App = () => {
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'purple'];
const handleMouseOver = () => {
document.body.style.cursor = 'move';
};
const handleMouseOut = () => {
document.body.style.cursor = 'default';
};
return (
{colors.map((color, i) => (
))}
);
};
export default App;
```
```vue
```
---
# HTML5 Canvas Drag and Drop a Line
> Learn how to drag and drop a line on the HTML5 canvas with Konva by setting the draggable property to true.
Source: https://konvajs.org/docs/drag_and_drop/Drag_a_Line.html
To drag and drop a line with Konva, we can set the `draggable` property
of the config object to `true` when the line is instantiated, or we can use the `draggable()` method.
**Note: (!) dragging a line will NOT change the `points` property. Instead `x` and `y` properties of the line are changed.**
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
const redLine = new Konva.Line({
x: 50,
y: 50,
points: [0, 0, 150, 0],
stroke: 'red',
strokeWidth: 15,
lineCap: 'round',
lineJoin: 'round',
draggable: true,
});
// add cursor styling
redLine.on('mouseover', function () {
document.body.style.cursor = 'pointer';
});
redLine.on('mouseout', function () {
document.body.style.cursor = 'default';
});
layer.add(redLine);
stage.add(layer);
```
```jsx
import { Stage, Layer, Line } from 'react-konva';
import { useState } from 'react';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
return (
{
setPosition({
x: e.target.x(),
y: e.target.y(),
});
}}
onMouseEnter={(e) => {
document.body.style.cursor = 'pointer';
}}
onMouseLeave={(e) => {
document.body.style.cursor = 'default';
}}
/>
);
};
export default App;
```
```vue
```
---
# HTML5 Canvas Drag and Drop the Stage
> Learn how to make the entire Konva stage draggable, allowing users to pan the canvas by dragging any area.
Source: https://konvajs.org/docs/drag_and_drop/Drag_a_Stage.html
To drag and drop a stage with Konva, we can set the `draggable` property
of the config object to `true` when the group is instantiated, or we can use the `draggable()` method.
Unlike drag and drop for other nodes, such as shapes, groups, and layers,
we can drag the entire stage by dragging any portion of the stage.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
draggable: true
});
const layer = new Konva.Layer();
stage.add(layer);
// create circle
const circle = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4
});
// create text
const text = new Konva.Text({
x: 10,
y: 10,
text: 'Drag the stage anywhere',
fontSize: 20,
fontFamily: 'Calibri',
fill: 'black'
});
layer.add(circle);
layer.add(text);
```
```jsx
import { Stage, Layer, Circle, Text } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
```vue
```
---
# HTML5 Canvas Drag and Drop an Image
> Learn how to drag and drop an image on the HTML5 canvas with Konva by setting the draggable property to true.
Source: https://konvajs.org/docs/drag_and_drop/Drag_an_Image.html
To drag and drop an image with Konva, we can set the `draggable` property
to true when we instantiate a shape, or we can use the `draggable()` method.
The `draggable()` method enables drag and drop for both desktop and mobile
applications automatically.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
const imageObj = new Image();
imageObj.onload = () => {
const yoda = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
width: 106,
height: 118,
draggable: true,
});
// add cursor styling
yoda.on('mouseover', function () {
document.body.style.cursor = 'pointer';
});
yoda.on('mouseout', function () {
document.body.style.cursor = 'default';
});
layer.add(yoda);
};
imageObj.src = 'https://konvajs.org/assets/yoda.jpg';
stage.add(layer);
```
```jsx
import { Stage, Layer, Image } from 'react-konva';
import { useState } from 'react';
import useImage from 'use-image';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
const [yodaImage] = useImage('https://konvajs.org/assets/yoda.jpg');
return (
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
onMouseEnter={(e) => {
document.body.style.cursor = 'pointer';
}}
onMouseLeave={(e) => {
document.body.style.cursor = 'default';
}}
/>
);
};
export default App;
```
```vue
```
---
# HTML5 Canvas Drop Events
> Learn how to implement drop events on HTML5 Canvas with Konva.js. Detect when a dragged shape is dropped onto a target zone.
Source: https://konvajs.org/docs/drag_and_drop/Drop_Events.html
Konva does not support drop events. But you can write your own drop events detections.
To detect drop target shape you have to move dragging object into another layer.
In this example you can see implementation of `drop`, `dragenter`, `dragleave`, `dragover` events.
**Instructions:** drag one shape over another. Or drag and drop one shape into another.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
const tempLayer = new Konva.Layer();
stage.add(layer);
stage.add(tempLayer);
const text = new Konva.Text({
fill: 'black',
});
layer.add(text);
let previousShape;
// create multiple stars
for (let i = 0; i < 10; i++) {
const star = new Konva.Star({
x: stage.width() * Math.random(),
y: stage.height() * Math.random(),
fill: 'blue',
numPoints: 10,
innerRadius: 20,
outerRadius: 25,
draggable: true,
name: 'star ' + i,
shadowOffsetX: 5,
shadowOffsetY: 5,
});
star.on('dragstart', () => {
star.moveTo(tempLayer);
text.text('Moving ' + star.name());
});
star.on('dragmove', (e) => {
const pos = stage.getPointerPosition();
const shape = layer.getIntersection(pos);
if (previousShape && shape) {
if (previousShape !== shape) {
// leave from old target
previousShape.fire('dragleave', { evt: e.evt }, true);
// enter new target
shape.fire('dragenter', { evt: e.evt }, true);
previousShape = shape;
} else {
previousShape.fire('dragover', { evt: e.evt }, true);
}
} else if (!previousShape && shape) {
previousShape = shape;
shape.fire('dragenter', { evt: e.evt }, true);
} else if (previousShape && !shape) {
previousShape.fire('dragleave', { evt: e.evt }, true);
previousShape = undefined;
}
});
star.on('dragend', (e) => {
const pos = stage.getPointerPosition();
const shape = layer.getIntersection(pos);
if (previousShape && previousShape !== shape) {
previousShape.fire('dragleave', { evt: e.evt }, true);
}
if (shape) {
shape.fire('drop', { evt: e.evt }, true);
}
previousShape = undefined;
star.moveTo(layer);
});
star.on('dragenter', () => {
star.fill('green');
text.text('dragenter ' + star.name());
});
star.on('dragleave', () => {
star.fill('blue');
text.text('dragleave ' + star.name());
});
star.on('dragover', () => {
text.text('dragover ' + star.name());
});
star.on('drop', () => {
star.fill('red');
text.text('drop ' + star.name());
});
layer.add(star);
}
```
```jsx
import { Stage, Layer, Text, Star } from 'react-konva';
import { useState, useRef } from 'react';
const App = () => {
const [stars, setStars] = useState(() =>
Array.from({ length: 10 }, (_, i) => ({
id: i,
x: window.innerWidth * Math.random(),
y: window.innerHeight * Math.random(),
fill: 'blue',
name: `star ${i}`,
}))
);
const [message, setMessage] = useState('');
const previousShapeRef = useRef(null);
const mainLayerRef = useRef(null);
const tempLayerRef = useRef(null);
const updateStar = (id, attrs) => {
setStars((currentStars) =>
currentStars.map((star) =>
star.id === id ? { ...star, ...attrs } : star
)
);
};
const handleDragStart = (id, e) => {
const shape = e.target;
updateStar(id, { x: shape.x(), y: shape.y() });
shape.moveTo(tempLayerRef.current);
setMessage('Moving ' + shape.name());
};
const handleDragMove = (id, e) => {
const stage = e.target.getStage();
const pos = stage.getPointerPosition();
const shape = mainLayerRef.current.getIntersection(pos);
updateStar(id, { x: e.target.x(), y: e.target.y() });
if (previousShapeRef.current && shape) {
if (previousShapeRef.current !== shape) {
// leave from old target
previousShapeRef.current.fire('dragleave', { evt: e.evt }, true);
// enter new target
shape.fire('dragenter', { evt: e.evt }, true);
previousShapeRef.current = shape;
} else {
previousShapeRef.current.fire('dragover', { evt: e.evt }, true);
}
} else if (!previousShapeRef.current && shape) {
previousShapeRef.current = shape;
shape.fire('dragenter', { evt: e.evt }, true);
} else if (previousShapeRef.current && !shape) {
previousShapeRef.current.fire('dragleave', { evt: e.evt }, true);
previousShapeRef.current = undefined;
}
};
const handleDragEnd = (id, e) => {
const shape = e.target;
const stage = e.target.getStage();
const pos = stage.getPointerPosition();
const dropShape = mainLayerRef.current.getIntersection(pos);
if (
previousShapeRef.current &&
previousShapeRef.current !== dropShape
) {
previousShapeRef.current.fire('dragleave', { evt: e.evt }, true);
}
if (dropShape) {
dropShape.fire('drop', { evt: e.evt }, true);
}
updateStar(id, { x: shape.x(), y: shape.y() });
shape.moveTo(mainLayerRef.current);
previousShapeRef.current = undefined;
};
const handleDragEnter = (id, e) => {
updateStar(id, { fill: 'green' });
setMessage('dragenter ' + e.target.name());
};
const handleDragLeave = (id, e) => {
updateStar(id, { fill: 'blue' });
setMessage('dragleave ' + e.target.name());
};
const handleDragOver = (e) => {
setMessage('dragover ' + e.target.name());
};
const handleDrop = (id, e) => {
updateStar(id, { fill: 'red' });
setMessage('drop ' + e.target.name());
};
return (
{stars.map((star) => (
handleDragStart(star.id, e)}
onDragMove={(e) => handleDragMove(star.id, e)}
onDragEnd={(e) => handleDragEnd(star.id, e)}
onDragEnter={(e) => handleDragEnter(star.id, e)}
onDragLeave={(e) => handleDragLeave(star.id, e)}
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(star.id, e)}
/>
))}
);
};
export default App;
```
```vue
```
---
# HTML5 Canvas Simple Drag Bounds Tutorial
> Learn how to constrain drag movement on HTML5 Canvas with Konva.js. Set drag boundaries to limit where shapes can be dragged.
Source: https://konvajs.org/docs/drag_and_drop/Simple_Drag_Bounds.html
To restrict the movement of shapes being dragged and dropped with Konva,
we can use the `dragmove` event and overrides the drag and drop position inside of it.
This event can be used to constrain the drag and drop movement in all kinds of ways, such as constraining the motion horizontally, vertically, diagonally, or radially, or even constrain the node
to stay inside of a box, circle, or any other path.
```js
shape.on('dragmove', () => {
// lock position of the shape on x axis
// keep y position as is
shape.x(0);
});
```
_Tip: you can use `shape.absolutePosition()` method to get/set absolute position of a node, instead of relative `x` and `y`._
**Instructions:** Drag and drop the the horizontal text and observe that it can only
move horizontally. Drag and drop the vertical text and observe that it can only move vertically.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const horizontalText = new Konva.Text({
x: 50,
y: 50,
text: 'Drag me horizontally',
fontSize: 16,
draggable: true,
fill: 'black',
});
horizontalText.on('dragmove', function () {
// horizontal only
this.y(50);
});
const verticalText = new Konva.Text({
x: 200,
y: 50,
text: 'Drag me vertically',
fontSize: 16,
draggable: true,
fill: 'black',
});
verticalText.on('dragmove', function () {
// vertical only
this.x(200);
});
layer.add(horizontalText);
layer.add(verticalText);
```
```jsx
import { Stage, Layer, Text } from 'react-konva';
import { useState } from 'react';
const App = () => {
const [horizontalPosition, setHorizontalPosition] = useState({ x: 50, y: 50 });
const [verticalPosition, setVerticalPosition] = useState({ x: 200, y: 50 });
const handleHorizontalDragMove = (e) => {
setHorizontalPosition({ x: e.target.x(), y: 50 });
};
const handleVerticalDragMove = (e) => {
setVerticalPosition({ x: 200, y: e.target.y() });
};
return (
);
};
export default App;
```
```vue
```
---
# HTML5 Canvas Cancel Event Bubble Propagation with Konva
> Learn how to cancel event bubble propagation in Konva by setting cancelBubble to true on the event object.
Source: https://konvajs.org/docs/events/Cancel_Propagation.html
To cancel event bubble propagation with Konva, we can set the `cancelBubble`
property of the Event object to true.
**Instructions: Click on the circle to observe that only the circle event binding
is handled because the event propagation was canceled when the circle event was triggered,
therefore preventing the event object from bubbling upwards.**
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const circle = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
circle.on('click', function (evt) {
alert('You clicked on the circle');
// stop event bubble
evt.cancelBubble = true;
});
layer.on('click', function () {
alert('You clicked on the layer');
});
layer.add(circle);
```
```jsx
import { Stage, Layer, Circle } from 'react-konva';
const App = () => {
const handleCircleClick = (e) => {
alert('You clicked on the circle');
// stop event bubble
e.cancelBubble = true;
};
const handleLayerClick = () => {
alert('You clicked on the layer');
};
return (
);
};
export default App;
```
```html
```
---
# HTML5 Canvas Custom Hit Detection Function Tutorial
> Learn how to define custom hit detection regions for HTML5 Canvas shapes with Konva.js. Override default hit areas with hitFunc for precise click detection.
Source: https://konvajs.org/docs/events/Custom_Hit_Region.html
There are two ways to change hit region of the shape: `hitFunc` and `hitStrokeWidth` properties.
## 1. What is `hitFunc`?
To create a custom hit draw function for a shape with Konva, we can set
the `hitFunc` property. A hit draw function is the function that Konva
will use to draw a region used for hit detection. Using a custom draw hit
function can have several benefits, such as making the hit region larger
so that it's easier for users to interact with a shape, making some portions
of a shape detectable and others not, or simplifying the hit draw function
in order to improve rendering performance.
Also take a look into some [best practices](/docs/shapes/Custom.html) of writing custom `sceneFunc` that can be used for `hitFunc` too.
`hitFunc` is a function with two arguments: [Konva.Context](/api/Konva.Context.html) renderer and a shape instance.
## 2. What is `hitStrokeWidth`?
For some shapes, like `Konva.Line` it is too hard to overwrite `hitFunc`. In some cases you just want to make it thicker for events. In this case it is better to use `hitStrokeWidth` property with a large value.
**Instructions: Mouseover, mouseout, mousedown, and mouseup over the star and
observe that the hit region is an over sized circle encompassing the shape. Also try the same for a line.
Also you can toggle hit canvas to see how it looks. It may be useful for debugging.**
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const text = new Konva.Text({
x: 10,
y: 10,
text: '',
fontSize: 24,
});
layer.add(text);
const star = new Konva.Star({
x: stage.width() / 4,
y: stage.height() / 2,
numPoints: 5,
innerRadius: 40,
outerRadius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
// custom hit function
star.hitFunc(function (context) {
context.beginPath();
context.arc(0, 0, 70, 0, Math.PI * 2, true);
context.closePath();
context.fillStrokeShape(this);
});
const line = new Konva.Line({
x: stage.width() * 0.6,
y: stage.height() / 2,
points: [-50, -50, 50, 50],
stroke: 'black',
strokeWidth: 2,
hitStrokeWidth: 20,
});
const button = document.createElement('button');
button.innerHTML = 'Toggle hit canvas';
document.body.appendChild(button);
let showHit = false;
button.addEventListener('click', () => {
showHit = !showHit;
if (showHit) {
stage.container().style.border = '2px solid black';
stage.container().style.height = stage.height() + 'px';
stage.container().appendChild(layer.hitCanvas._canvas);
layer.hitCanvas._canvas.style.position = 'absolute';
layer.hitCanvas._canvas.style.top = 0;
layer.hitCanvas._canvas.style.left = 0;
} else {
layer.hitCanvas._canvas.remove();
}
});
function writeMessage(message) {
text.text(message);
}
star.on('mouseover mouseout mousedown mouseup', function (evt) {
writeMessage(evt.type + ' star');
});
line.on('mouseover mouseout mousedown mouseup', function (evt) {
writeMessage(evt.type + ' line');
});
layer.add(star);
layer.add(line);
```
```jsx
import { Stage, Layer, Star, Line, Text } from 'react-konva';
import { useState, useEffect } from 'react';
const App = () => {
const [message, setMessage] = useState('');
const [showHit, setShowHit] = useState(false);
const handleStarEvent = (evt) => {
setMessage(evt.type + ' star');
};
const handleLineEvent = (evt) => {
setMessage(evt.type + ' line');
};
useEffect(() => {
const stage = document.querySelector('.konvajs-content');
if (showHit) {
const hitCanvas = stage.querySelector('canvas:last-child');
stage.style.border = '2px solid black';
hitCanvas.style.position = 'absolute';
hitCanvas.style.top = '0';
hitCanvas.style.left = '0';
}
}, [showHit]);
return (
<>
setShowHit(!showHit)}>Toggle hit canvas
{
context.beginPath();
context.arc(0, 0, 70, 0, Math.PI * 2, true);
context.closePath();
context.fillStrokeShape(shape);
}}
onMouseover={handleStarEvent}
onMouseout={handleStarEvent}
onMousedown={handleStarEvent}
onMouseup={handleStarEvent}
/>
>
);
};
export default App;
```
```html
Toggle hit canvas
```
---
# HTML5 Canvas Desktop and Mobile Events Support Tutorial
> Learn how to handle both desktop and mobile events in Konva using paired event bindings like mousedown/touchstart and mouseup/touchend.
Source: https://konvajs.org/docs/events/Desktop_and_Mobile.html
_Note: this demo may be outdate, because modern browsers support pointer events. And you can use pointer events in Konva too. See [Pointer Events Demo](/docs/events/Pointer_Events.html). But if you prefer not to use pointer events, keep reading..._
To add event handlers to shapes that work for both desktop and mobile applications with Konva, we can use the `on()` method and pass in paired events.
For example, in order for the `mousedown` event to be triggered on desktop and mobile applications, we can use the `"mousedown touchstart"` event pair to cover both mediums.
In order for the `mouseup` event to be triggered on both desktop and mobile applications, we can use the `"mouseup touchend"` event pair.
We can also use the `"dblclick dbltap"` event pair to bind a double click event that works for both desktop and mobile devices.
**Instructions: Mousedown, mouseup, touchstart, or touchend the circle on either a desktop or mobile device to observe the same functionality.**
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
const text = new Konva.Text({
x: 10,
y: 10,
fontFamily: 'Calibri',
fontSize: 24,
text: '',
fill: 'black',
});
const circle = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
function writeMessage(message) {
text.text(message);
}
// desktop and mobile events
circle.on('mousedown touchstart', function () {
writeMessage('Mousedown or touchstart');
});
circle.on('mouseup touchend', function () {
writeMessage('Mouseup or touchend');
});
layer.add(circle);
layer.add(text);
stage.add(layer);
```
```jsx
import { Stage, Layer, Circle, Text } from 'react-konva';
import { useState } from 'react';
const App = () => {
const [message, setMessage] = useState('');
return (
setMessage('Mousedown or touchstart')}
onTouchstart={() => setMessage('Mousedown or touchstart')}
onMouseup={() => setMessage('Mouseup or touchend')}
onTouchend={() => setMessage('Mouseup or touchend')}
/>
);
};
export default App;
```
```html
```
---
# HTML5 Canvas Event Delegation with Konva
> Learn how to use event delegation on HTML5 Canvas with Konva.js. Listen for events on layers or groups instead of individual shapes for cleaner code.
Source: https://konvajs.org/docs/events/Event_Delegation.html
To get the event target with Konva, we can access the `target` property
of the Event object. This is particularly useful when using event delegation,
in which we can bind an event handler to a parent node, and listen to events
that occur on its children.
**Instructions: Click on the star and observe that the layer event binding
correctly identifies the shape that was clicked on.**
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const text = new Konva.Text({
x: 10,
y: 10,
fontFamily: 'Calibri',
fontSize: 24,
text: '',
fill: 'black',
});
layer.add(text);
const star = new Konva.Star({
x: stage.width() / 2,
y: stage.height() / 2,
numPoints: 5,
innerRadius: 40,
outerRadius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
layer.add(star);
// add event delegation
layer.on('click', function (evt) {
const shape = evt.target;
text.text('click on ' + shape.getClassName());
});
```
```jsx
import { Stage, Layer, Star, Text } from 'react-konva';
import { useState } from 'react';
const App = () => {
const [message, setMessage] = useState('');
const handleLayerClick = (e) => {
const shape = e.target;
setMessage('click on ' + shape.getClassName());
};
return (
);
};
export default App;
```
```html
```
---
# HTML5 Canvas Fire Event with Konva
> Learn how to programmatically fire events on shapes in Konva using the fire() method, including custom events.
Source: https://konvajs.org/docs/events/Fire_Events.html
To fire events with Konva, we can use the `fire()` method.
This enables us to programmatically fire events like `click`, `mouseover`,
`mousemove`, etc., and also fire custom events, like foo and bar.
> **Note**: While custom events are possible, it's generally better to use built-in interaction events like `click`, `mouseover`, `mousemove`, etc. Custom events can make code harder to maintain and debug.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const circle = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
// add shape event listener
circle.on('customEvent', function (evt) {
alert('custom event fired');
});
// add button to trigger custom event
const button = document.createElement('button');
button.innerHTML = 'Fire Custom Event';
button.style.position = 'absolute';
button.style.top = '10px';
button.style.left = '10px';
button.style.zIndex = '1';
document.body.appendChild(button);
button.addEventListener('click', () => {
// fire custom event
circle.fire('customEvent', {
bubbles: true,
});
});
layer.add(circle);
```
```jsx
import { Stage, Layer, Circle } from 'react-konva';
import { useRef } from 'react';
const App = () => {
const circleRef = useRef();
const handleCustomEvent = () => {
alert('custom event fired');
};
const fireCustomEvent = () => {
circleRef.current.fire('customevent', {
bubbles: true,
});
};
return (
<>
Fire Custom Event
>
);
};
export default App;
```
```html
Fire Custom Event
```
---
# HTML5 Canvas Image Events with Konva
> Learn how to handle events on images in HTML5 Canvas with Konva.js. Bind click, mouseover, and touch events to Konva.Image shapes.
Source: https://konvajs.org/docs/events/Image_Events.html
To only detect events for non transparent pixels in an image with Konva, we can use the `drawHitFromCache()` method to generate a more precise image hit region.
By default, events can be triggered for any pixel inside of an image, even if it's transparent. The `drawHitFromCache()` method also accepts an optional callback method to be executed whenever the image hit region has been created.
*Note: The `drawHitFromCache()` method requires that the image is hosted on a web server with the same domain as the code executing it.*
**Instructions: Mouse over the monkey and the lion and observe the mouseover event bindings. Notice that the event is triggered for the monkey if you mouseover any portion of the image, including transparent pixels. Since we created an image hit region for the lion, transparent pixels are ignored, which enables more precise event detection.**
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const text = new Konva.Text({
x: 10,
y: 10,
fontFamily: 'Calibri',
fontSize: 24,
text: '',
fill: 'black',
});
layer.add(text);
function writeMessage(message) {
text.text(message);
}
const imageObj1 = new Image();
imageObj1.onload = () => {
const monkey = new Konva.Image({
x: 120,
y: 50,
image: imageObj1,
width: 200,
height: 200,
});
monkey.on('mouseover', function () {
writeMessage('mouseover monkey (regular image)');
});
monkey.on('mouseout', function () {
writeMessage('');
});
layer.add(monkey);
};
imageObj1.crossOrigin = 'Anonymous';
imageObj1.src = 'https://konvajs.org/assets/monkey.png';
const imageObj2 = new Image();
imageObj2.onload = () => {
const lion = new Konva.Image({
x: 320,
y: 50,
image: imageObj2,
width: 200,
height: 200,
});
// override color detection region
lion.on('mouseover', function () {
writeMessage('mouseover lion (with transparent pixels detection)');
});
lion.on('mouseout', function () {
writeMessage('');
});
layer.add(lion);
lion.cache();
lion.drawHitFromCache();
};
imageObj2.crossOrigin = 'Anonymous';
imageObj2.src = 'https://konvajs.org/assets/lion.png';
```
```jsx
import { Stage, Layer, Image, Text } from 'react-konva';
import { useState, useEffect, useRef } from 'react';
import useImage from 'use-image';
const App = () => {
const [message, setMessage] = useState('');
const lionRef = useRef();
const [monkeyImage] = useImage('https://konvajs.org/assets/monkey.png', 'anonymous');
const [lionImage] = useImage('https://konvajs.org/assets/lion.png', 'anonymous');
useEffect(() => {
if (lionImage) {
lionRef.current.cache();
lionRef.current.drawHitFromCache();
}
}, [lionImage]);
return (
{monkeyImage && (
setMessage('mouseover monkey (regular image)')}
onMouseout={() => setMessage('')}
/>
)}
{lionImage && (
setMessage('mouseover lion (with transparent pixels detection)')
}
onMouseout={() => setMessage('')}
/>
)}
);
};
export default App;
```
```html
```
---
# HTML5 Canvas Keyboard events with Konva
> Learn how to handle keyboard events on HTML5 Canvas shapes with Konva.js. Implement keyboard shortcuts, arrow key movement, and key-based interactions.
Source: https://konvajs.org/docs/events/Keyboard_Events.html
There are no built-in keyboard events like `keydown` or `keyup` in Konva.
### But how to listen keydown or keyup events on canvas?
You can easily add them by two ways:
1. Listen global events on `window` object
2. Or make stage container focusable with `tabIndex` property and listen events on it.
**Instructions: click on stage to focus it, move a shape with arrows**
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const circle = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 50,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
layer.add(circle);
// make stage container focusable
stage.container().tabIndex = 1;
// focus it
// also stage will be in focus on its click
stage.container().focus();
const DELTA = 4;
// add keyboard events
stage.container().addEventListener('keydown', (e) => {
if (e.keyCode === 37) {
circle.x(circle.x() - DELTA);
} else if (e.keyCode === 38) {
circle.y(circle.y() - DELTA);
} else if (e.keyCode === 39) {
circle.x(circle.x() + DELTA);
} else if (e.keyCode === 40) {
circle.y(circle.y() + DELTA);
} else {
return;
}
e.preventDefault();
});
```
```jsx
import { Stage, Layer, Circle } from 'react-konva';
import { useRef, useEffect, useState } from 'react';
const App = () => {
const stageRef = useRef();
const containerRef = useRef();
const [position, setPosition] = useState({
x: window.innerWidth / 2,
y: window.innerHeight / 2,
});
useEffect(() => {
// focus the div on mount
containerRef.current.focus();
}, []);
const handleKeyDown = (e) => {
const DELTA = 4;
switch (e.keyCode) {
case 37: // left
setPosition(pos => ({ ...pos, x: pos.x - DELTA }));
break;
case 38: // up
setPosition(pos => ({ ...pos, y: pos.y - DELTA }));
break;
case 39: // right
setPosition(pos => ({ ...pos, x: pos.x + DELTA }));
break;
case 40: // down
setPosition(pos => ({ ...pos, y: pos.y + DELTA }));
break;
default:
return;
}
e.preventDefault();
};
return (
);
};
export default App;
```
```html
```
---
# HTML5 Canvas Listen or Don't Listen to Events with Konva
> Learn how to enable or disable event listening on shapes in Konva using the listening property and setListening() method.
Source: https://konvajs.org/docs/events/Listen_for_Events.html
To listen or don't listen to events with Konva, we can set the listening
property of the config object to true or false when a shape is instantiated,
or we can set the listening property with the `setListening()` method.
Once we've set the listening property for one or more nodes, we'll also need
to redraw the hit graph for each affected layer with the `drawHit()` method.
**Instructions: Mouseover the oval to observe that the event handler is not executed.
Click on "Listen" to start listening for events and observe that the event handler is now executed.**
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const text = new Konva.Text({
x: 10,
y: 10,
fontFamily: 'Calibri',
fontSize: 24,
text: '',
fill: 'black',
});
layer.add(text);
const oval = new Konva.Ellipse({
x: stage.width() / 2,
y: stage.height() / 2,
radiusX: 100,
radiusY: 50,
fill: 'yellow',
stroke: 'black',
strokeWidth: 4,
listening: false,
});
oval.on('mouseover', function () {
writeMessage('Mouseover oval');
});
oval.on('mouseout', function () {
writeMessage('');
});
function writeMessage(message) {
text.text(message);
}
layer.add(oval);
// add button to toggle listening
const button = document.createElement('button');
button.innerHTML = 'Listen';
document.body.appendChild(button);
button.addEventListener('click', () => {
const listening = !oval.listening();
oval.listening(listening);
button.innerHTML = listening ? 'Stop listening' : 'Listen';
layer.drawHit();
});
```
```jsx
import { Stage, Layer, Ellipse, Text } from 'react-konva';
import { useState } from 'react';
const App = () => {
const [message, setMessage] = useState('');
const [listening, setListening] = useState(false);
return (
<>
setListening(!listening)}>
{listening ? 'Stop listening' : 'Listen'}
setMessage('Mouseover oval')}
onMouseout={() => setMessage('')}
/>
>
);
};
export default App;
```
```html
{{ listening ? 'Stop listening' : 'Listen' }}
```
---
# HTML5 Canvas Mobile Touch Events Tutorial
> Learn how to handle touch events on HTML5 Canvas with Konva.js on mobile devices. Support tap, touchstart, touchmove, touchend, and multi-touch gestures.
Source: https://konvajs.org/docs/events/Mobile_Events.html
To bind event handlers to shapes on a mobile device with Konva, we can use the `on()` method.
The `on()` method requires an event type and a function to be executed when the event occurs.
Konva supports `touchstart`, `touchmove`, `touchend`, `tap`, `dbltap`, `dragstart`, `dragmove`, and `dragend` mobile events.
For more complex gestures like `rotate` take a look into [Gestures Demo](/docs/sandbox/Gestures.html).
If you are looking for pan and zoom logic for the whole stage take a look into [Multi-touch scale Stage demo](/docs/sandbox/Multi-touch_Scale_Stage.html).
*Note: This example only works on mobile devices because it makes use of touch events rather than mouse events.*
**Instructions: move your finger across the triangle to see touch coordinates and touch start and touch end the circle.**
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
const text = new Konva.Text({
x: 10,
y: 10,
fontFamily: 'Calibri',
fontSize: 24,
text: '',
fill: 'black',
});
const triangle = new Konva.RegularPolygon({
x: 80,
y: 120,
sides: 3,
radius: 80,
fill: '#00D2FF',
stroke: 'black',
strokeWidth: 4,
});
const circle = new Konva.Circle({
x: 230,
y: 100,
radius: 60,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
function writeMessage(message) {
text.text(message);
}
triangle.on('touchmove', function () {
const touchPos = stage.getPointerPosition();
const x = touchPos.x;
const y = touchPos.y;
writeMessage('x: ' + x + ', y: ' + y);
});
circle.on('touchstart', function () {
writeMessage('touchstart circle');
});
circle.on('touchend', function () {
writeMessage('touchend circle');
});
layer.add(triangle);
layer.add(circle);
layer.add(text);
stage.add(layer);
```
```jsx
import { Stage, Layer, RegularPolygon, Circle, Text } from 'react-konva';
import { useState, useRef } from 'react';
const App = () => {
const [message, setMessage] = useState('');
const stageRef = useRef();
const handleTriangleTouch = () => {
const touchPos = stageRef.current.getPointerPosition();
setMessage(`x: ${touchPos.x}, y: ${touchPos.y}`);
};
return (
setMessage('touchstart circle')}
onTouchend={() => setMessage('touchend circle')}
/>
);
};
export default App;
```
```html
```
---
# HTML5 Canvas Mobile Scrolling and Native Events with Konva
> Learn how to control mobile scrolling behavior on Konva stages using the preventDefault property on shapes.
Source: https://konvajs.org/docs/events/Mobile_Scrolling.html
By default `Konva` will prevent default behaviour of all pointer interactions with a stage.
That will prevent unexpected scrolling of a page when you are trying to drag&drop a shape on a mobile device.
But in some cases you may want to keep default behaviour of browser events. In that case you may set `preventDefault` property of a shape to `false`.
**Instructions: if you are on mobile device try to scroll a page by each rectangle.
Green - should prevent default behaviour (no page scrolling).
Red - will keep default behaviour (scrolling should work).**
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// green rectangle - will prevent scrolling
const greenRect = new Konva.Rect({
x: 50,
y: 50,
width: 100,
height: 600,
fill: 'green',
stroke: 'black',
strokeWidth: 4,
});
layer.add(greenRect);
// red rectangle - will NOT prevent scrolling
const redRect = new Konva.Rect({
x: 200,
y: 50,
width: 100,
height: 600,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
preventDefault: false,
});
layer.add(redRect);
```
```jsx
import { Stage, Layer, Rect } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
```html
```
---
# Why Your Handler Fires Twice on Mobile, and Click After Drag
> A single tap can fire tap, pointerclick and click in Konva, so a handler bound to 'click tap' runs twice on touch devices. How to bind one event family, and why click already does not fire after a drag.
Source: https://konvajs.org/docs/events/Mobile_Tap_And_Click.html
Two things surprise people about pointer input on canvas. One is a real problem
you have to handle. The other is already handled, and knowing that saves you a
workaround you do not need.
## A tap can fire three events
Konva listens to all three DOM input families — mouse, touch, and pointer — so
that a handler works everywhere without you choosing in advance. It does not
merge them.
On a touchscreen the browser sends a `touchstart`/`touchend` pair, a
`pointerdown`/`pointerup` pair, and then **synthesises** `mousedown`/`mouseup`
for compatibility with pages written before touch existed. Konva turns each of
those into its own event. One tap produces:
| Konva event | Comes from |
| --- | --- |
| `tap` | the touch pair |
| `pointerclick` | the pointer pair |
| `click` | the synthesised mouse pair |
So this runs your handler **twice** on a phone and once on a desktop:
```js
// Fires for `click` and again for `tap` on a touch device.
shape.on('click tap', handleSelect);
```
### Bind one family
The simplest fix is to stop pairing events and use the pointer family, which
already covers mouse, touch, and pen:
```js
shape.on('pointerclick', handleSelect); // once, on every device
```
`pointerdown`, `pointermove` and `pointerup` work the same way. Pointer events
are on by default (`Konva.pointerEventsEnabled`), and there is more in
[Pointer Events](/docs/events/Pointer_Events.html).
If you need to support a browser without pointer events, or you are maintaining
code that already pairs `click tap`, guard the handler instead:
```js
let lastHandled = 0;
function handleSelect(e) {
// A synthesised mouse event follows its touch within about 300ms.
const now = Date.now();
if (now - lastHandled < 400) return;
lastHandled = now;
// ...
}
```
Prefer binding one family. The timer is a workaround, and it will swallow a
genuine second tap from a fast user.
## Click already does not fire after a drag
This one is worth knowing because the usual workaround is unnecessary.
When a drag actually starts, Konva clears the flag that allows a click to be
dispatched, so `dragend` is **not** followed by `click`:
```js
shape.on('dragend', () => console.log('dragged'));
shape.on('click', () => console.log('clicked'));
// Drag the shape: you get 'dragged' only.
// Press and release without moving: you get 'clicked' only.
```
If you are seeing a click after a drag anyway, it is usually one of these.
**You are listening to `mouseup`, not `click`.** `mouseup` is a raw pointer
event and always fires, drag or not. `click` is the one Konva suppresses.
**The pointer never moved far enough to count as a drag.** `Konva.dragDistance`
defaults to `3` pixels. A press that wanders one or two pixels — which is normal
on a touchscreen — is treated as a click, and `dragstart` never fires either.
That is intended. Raise it if your users have shaky hands on a small target:
```js
Konva.dragDistance = 8;
```
**You set `dragDistance` to `0`.** Then every press is a drag, which suppresses
the click you wanted.
## Preventing the browser's own gestures
Separately from Konva, a touch on a canvas may scroll the page or trigger a
double-tap zoom. That is the browser, and it is stopped in CSS:
```css
#container {
touch-action: none;
}
```
Use `touch-action: pan-y` instead if the page should still scroll vertically
while the canvas handles horizontal drags. See
[Mobile Scrolling](/docs/events/Mobile_Scrolling.html) for the full picture.
## Related
- [Pointer Events](/docs/events/Pointer_Events.html) — one family for every device
- [Mobile Events](/docs/events/Mobile_Events.html) — the touch event list
- [Desktop and Mobile](/docs/events/Desktop_and_Mobile.html) — the older paired-event approach
---
# HTML5 Canvas Canvas Multi-Event Binding Tutorial
> Learn how to bind multiple events to a single handler in Konva using a space-delimited string with the on() method.
Source: https://konvajs.org/docs/events/Multi_Event.html
To bind multiple events to a single handler with Konva, we can use the `on()` method and pass in a space delimited string containing multiple event types.
```js
shape.on('mouseover mousedown mouseup', function (e) {
console.log('events: ' + e.type);
});
```
**Instructions: Mouseover, mousedown, and mouseup over the circle to observe that the function bound to the circle is executed for each event.**
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const text = new Konva.Text({
x: 10,
y: 10,
fontFamily: 'Calibri',
fontSize: 24,
text: '',
fill: 'black',
});
layer.add(text);
const circle = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
function writeMessage(message) {
text.text(message);
}
circle.on('mouseover mousedown mouseup', function (evt) {
writeMessage('event: ' + evt.type);
});
layer.add(circle);
```
```jsx
import { Stage, Layer, Circle, Text } from 'react-konva';
import { useState } from 'react';
const App = () => {
const [message, setMessage] = useState('');
const handleMultiEvent = (e) => {
setMessage('event: ' + e.type);
};
return (
);
};
export default App;
```
```html
```
---
# HTML5 Canvas Pointer Events Tutorial
> Learn how to use pointer events in Konva to handle both mouse and touch input with a single event handler.
Source: https://konvajs.org/docs/events/Pointer_Events.html
Pointer events can be useful to handle both mobile and desktop events with one handler.
To bind pointer event handlers to shapes with Konva, we can use the `on()` method.
The `on()` method requires an event type and a function to be executed when the event occurs.
Konva supports `pointerdown`, `pointermove`, `pointerup`, `pointercancel`, `pointerover`, `pointerenter`, `pointerout`, `pointerleave`, `pointerclick`, `pointerdblclick` events.
_Note: This example works on both mobile and desktop devices._
**Instructions: move your mouse/finger across the triangle to see pointer coordinates.**
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
const text = new Konva.Text({
x: 10,
y: 10,
fontFamily: 'Calibri',
fontSize: 24,
text: '',
fill: 'black',
});
const triangle = new Konva.RegularPolygon({
x: stage.width() / 2,
y: stage.height() / 2,
sides: 3,
radius: 80,
fill: '#00D2FF',
stroke: 'black',
strokeWidth: 4,
});
function writeMessage(message) {
text.text(message);
}
triangle.on('pointermove', function () {
const pos = stage.getPointerPosition();
writeMessage('x: ' + pos.x + ', y: ' + pos.y);
});
triangle.on('pointerout', function () {
writeMessage('');
});
layer.add(triangle);
layer.add(text);
stage.add(layer);
```
```jsx
import { Stage, Layer, RegularPolygon, Text } from 'react-konva';
import { useState, useRef } from 'react';
const App = () => {
const [message, setMessage] = useState('');
const stageRef = useRef();
const handlePointerMove = () => {
const pos = stageRef.current.getPointerPosition();
setMessage(`x: ${pos.x}, y: ${pos.y}`);
};
return (
setMessage('')}
/>
);
};
export default App;
```
```html
```
---
# HTML5 Canvas Remove Event Listener with Konva
> Learn how to remove event listeners from shapes in Konva using the off() method.
Source: https://konvajs.org/docs/events/Remove_Event.html
To remove an event listener with Konva, we can use the `off()` method of
a shape object which requires an event type such as click or mousedown.
**Instructions: Click on the circle to see an alert triggered from the onclick
event binding. Remove the event listener by clicking on the button and again
click on the circle to observe that the event binding has been removed.**
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const circle = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
// add click listener
circle.on('click', function () {
alert('you clicked the circle');
});
layer.add(circle);
// add button to remove listener
const button = document.createElement('button');
button.style.position = 'absolute';
button.style.top = '10px';
button.style.left = '10px';
button.innerHTML = 'Remove click listener';
document.body.appendChild(button);
button.addEventListener('click', () => {
// remove click listener
circle.off('click');
});
```
```jsx
import { Stage, Layer, Circle } from 'react-konva';
import { useState } from 'react';
const App = () => {
const [hasListener, setHasListener] = useState(true);
return (
<>
setHasListener(false)}>
Remove click listener
alert('you clicked the circle') : null}
/>
>
);
};
export default App;
```
```html
Remove click listener
```
---
# HTML5 Canvas Remove Event Listener by Name with Konva
> Learn how to remove specific event listeners by namespace in Konva using the on() and off() methods with named events.
Source: https://konvajs.org/docs/events/Remove_by_Name.html
Konva event namespaces identify related listeners in imperative code. Add the
namespace after the event type, such as `click.menu`. Then pass the same name to
`off()` to remove that listener.
**Instructions:** Select the circle to run two listeners. Use each button to
remove one listener. Then select the circle again.
`react-konva` does not expose Konva event namespaces through React event props.
React has one prop for each event type, such as `onClick`. Store enabled states
in React, and conditionally pass one dispatcher to that prop.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const circle = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
// add click listeners
circle.on('click.event1', function () {
alert('first click listener');
});
circle.on('click.event2', function () {
alert('second click listener');
});
layer.add(circle);
// add buttons to remove listeners
const button1 = document.createElement('button');
button1.innerHTML = 'Remove first listener';
button1.style.position = 'absolute';
button1.style.top = '0';
button1.style.left = '0';
button1.onclick = function() {
circle.off('click.event1');
};
document.getElementById('container').appendChild(button1);
const button2 = document.createElement('button');
button2.innerHTML = 'Remove second listener';
button2.style.position = 'absolute';
button2.style.top = '30px';
button2.style.left = '0';
button2.onclick = function() {
circle.off('click.event2');
};
document.getElementById('container').appendChild(button2);
```
```jsx
import { useState } from 'react';
import { Stage, Layer, Circle, Text } from 'react-konva';
const App = () => {
const [firstEnabled, setFirstEnabled] = useState(true);
const [secondEnabled, setSecondEnabled] = useState(true);
const [message, setMessage] = useState('Select the circle');
const handleClick =
firstEnabled || secondEnabled
? () => {
const messages = [];
if (firstEnabled) {
messages.push('first listener');
}
if (secondEnabled) {
messages.push('second listener');
}
setMessage(messages.join(' + '));
}
: undefined;
return (
<>
setFirstEnabled(false)}>
Remove first listener
setSecondEnabled(false)}>
Remove second listener
>
);
};
export default App;
```
---
# HTML5 Canvas Special Stage Events Konva
> Learn about special stage-level events in Konva.js: contentClick, contentMousemove, and other events that fire on the Stage container.
Source: https://konvajs.org/docs/events/Stage_Events.html
All events are started from Shapes. So if you click on an empty space within a canvas, a `click` event will not trigger on `Layer` but it will trigger on the `Stage` object instead.
**Instructions: Click on empty space and on shapes to see different event behaviors.**
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const text = new Konva.Text({
x: 10,
y: 10,
fontFamily: 'Calibri',
fontSize: 24,
text: '',
fill: 'black',
});
layer.add(text);
const circle = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
function writeMessage(message) {
text.text(message);
}
// handle stage click
stage.on('click', function (e) {
if (e.target === stage) {
writeMessage('clicked on stage');
return;
}
writeMessage('clicked on ' + e.target.name());
});
// add shape
circle.name('circle');
layer.add(circle);
```
```jsx
import { Stage, Layer, Circle, Text } from 'react-konva';
import { useState } from 'react';
const App = () => {
const [message, setMessage] = useState('');
const handleStageClick = (e) => {
if (e.target === e.target.getStage()) {
setMessage('clicked on stage');
return;
}
setMessage('clicked on ' + e.target.name());
};
return (
);
};
export default App;
```
```html
```
---
# HTML5 Canvas Brighten Image Filter Tutorial
> Deprecated Brighten filter tutorial for Konva.js. Use the newer Brightness filter instead for adjusting image brightness.
Source: https://konvajs.org/docs/filters/Brighten.html
**Note**: This filter is deprecated and will be removed in the future. Use the `Brightness` filter instead.
To apply filter to an `Konva.Image`, we have to cache it first with `cache()`
function. Then apply filter with `filters()` function.
To brighten or darken an image with Konva, we can use the `Konva.Filters.Brighten`
filter and set the brightness amount with the `brightness` property.
The `brightness` property can be set to any number between -1 and 1.
Negative values darken the image, and positive values brighten the image.
**Instructions**: Slide the control to adjust the brightness
For all available filters go to [Filters Documentation](/api/Konva.Filters.html).
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
const imageObj = new Image();
imageObj.onload = () => {
const image = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
draggable: true,
});
layer.add(image);
image.cache();
image.filters([Konva.Filters.Brighten]);
image.brightness(0.3);
const slider = document.createElement('input');
slider.type = 'range';
slider.min = '-1';
slider.max = '1';
slider.step = '0.1';
slider.value = image.brightness();
slider.style.position = 'absolute';
slider.style.top = '20px';
slider.style.left = '20px';
slider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
image.brightness(value);
});
document.body.appendChild(slider);
};
imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg';
imageObj.crossOrigin = 'anonymous';
```
```js
import Konva from 'konva';
import { Stage, Layer, Image } from 'react-konva';
import { useState, useEffect, useRef } from 'react';
import useImage from 'use-image';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
const [brightness, setBrightness] = useState(0.3);
const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous');
const imageRef = useRef(null);
useEffect(() => {
if (image && imageRef.current) {
imageRef.current.cache();
}
}, [image]);
return (
<>
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
filters={[Konva.Filters.Brighten]}
brightness={brightness}
/>
setBrightness(parseFloat(e.target.value))}
style={{ position: 'absolute', top: '20px', left: '20px' }}
/>
>
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Brightness Image Filter Tutorial
> Learn how to adjust image brightness on HTML5 Canvas using the Konva.js Brightness filter, similar to CSS filter brightness.
Source: https://konvajs.org/docs/filters/Brightness.html
**Note**: This filter was introduced in Konva 10.0.0 to replace the `Brighten` filter. The `Brighten` filter is still available for backward compatibility, but it is deprecated and will be removed in the future.
New filter renders closer to CSS `filter: brightness(0.5);`
To apply filter to an `Konva.Image`, we have to cache it first with `cache()`
function. Then apply filter with `filters()` function.
To brighten or darken an image with Konva, we can use the `Konva.Filters.Brightness`
filter and set the brightness amount with the `brightness` property.
The `brightness` property can be set to any number from 0 to 2, where:
- 0 creates a completely black image
- 1 is the original image (no change)
- Values greater than 1 brighten the image
- 2 creates a very bright image
**Instructions**: Slide the control to adjust the brightness
For all available filters go to [Filters Documentation](/api/Konva.Filters.html).
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
const imageObj = new Image();
imageObj.onload = () => {
const image = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
draggable: true,
});
layer.add(image);
image.cache();
image.filters([Konva.Filters.Brightness]);
image.brightness(1.5);
const slider = document.createElement('input');
slider.type = 'range';
slider.min = '0';
slider.max = '2';
slider.step = '0.1';
slider.value = image.brightness();
slider.style.position = 'absolute';
slider.style.top = '20px';
slider.style.left = '20px';
slider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
image.brightness(value);
});
document.body.appendChild(slider);
};
imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg';
imageObj.crossOrigin = 'anonymous';
```
```js
import Konva from 'konva';
import { Stage, Layer, Image } from 'react-konva';
import { useState, useEffect, useRef } from 'react';
import useImage from 'use-image';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
const [brightness, setBrightness] = useState(1.5);
const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous');
const imageRef = useRef(null);
useEffect(() => {
if (image && imageRef.current) {
imageRef.current.cache();
}
}, [image]);
return (
<>
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
filters={[Konva.Filters.Brightness]}
brightness={brightness}
/>
setBrightness(parseFloat(e.target.value))}
style={{ position: 'absolute', top: '20px', left: '20px' }}
/>
>
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Contrast filter Image Tutorial
> Learn how to adjust image contrast on HTML5 Canvas using the Konva.js Contrast filter with an interactive slider demo.
Source: https://konvajs.org/docs/filters/Contrast.html
To apply filter to an `Konva.Node`, we have to cache it first with `cache()`
function. Then apply filter with `filters()` function.
To change contrast of an image with Konva, we can use the `Konva.Filters.Contrast`.
**Instructions**: Slide the control to change contrast value.
For all available filters go to [Filters Documentation](/api/Konva.Filters.html).
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
const imageObj = new Image();
imageObj.onload = () => {
const image = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
draggable: true,
});
layer.add(image);
image.cache();
image.filters([Konva.Filters.Contrast]);
image.contrast(30);
const slider = document.createElement('input');
slider.type = 'range';
slider.min = '-100';
slider.max = '100';
slider.value = image.contrast();
slider.style.position = 'absolute';
slider.style.top = '20px';
slider.style.left = '20px';
slider.addEventListener('input', (e) => {
const value = parseInt(e.target.value);
image.contrast(value);
});
document.body.appendChild(slider);
};
imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg';
imageObj.crossOrigin = 'anonymous';
```
```js
import Konva from 'konva';
import { Stage, Layer, Image } from 'react-konva';
import { useState, useEffect, useRef } from 'react';
import useImage from 'use-image';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
const [contrast, setContrast] = useState(30);
const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous');
const imageRef = useRef(null);
useEffect(() => {
if (image && imageRef.current) {
imageRef.current.cache();
}
}, [image]);
return (
<>
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
filters={[Konva.Filters.Contrast]}
contrast={contrast}
/>
setContrast(parseInt(e.target.value))}
style={{ position: 'absolute', top: '20px', left: '20px' }}
/>
>
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Custom Filter Tutorial
> Learn how to create and apply custom image filters in Konva.js by manipulating canvas ImageData pixels directly.
Source: https://konvajs.org/docs/filters/Custom_Filter.html
## How apply custom filter for Konva nodes?
This demo demonstrate how to use custom filters with `Konva` framework.
`Filter` is a function that have canvas ImageData as input and it should mutate it.
```javascript
function Filter(imageData) {
// do something with image data
imageData.data[0] = 0;
}
```
For all available filters go to [Filters Documentation](/api/Konva.Filters.html).
Also take a look into [Image Border Demo](/docs/sandbox/Image_Border.html) for custom filter example.
**In this demo we will remove all transparency from the image.**
```js
import Konva from 'konva';
// create our custom filter
Konva.Filters.RemoveAlpha = function (imageData) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
data[i + 3] = 255; // set alpha to 1
}
};
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const imageObj = new Image();
imageObj.onload = () => {
const image = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
draggable: true,
});
layer.add(image);
image.cache();
image.filters([Konva.Filters.RemoveAlpha]);
};
imageObj.src = 'https://konvajs.org/assets/lion.png';
imageObj.crossOrigin = 'anonymous';
```
```js
import Konva from 'konva';
import { Stage, Layer, Image } from 'react-konva';
import { useState, useEffect, useRef } from 'react';
import useImage from 'use-image';
// create our custom filter
Konva.Filters.RemoveAlpha = function (imageData) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
data[i + 3] = 255; // set alpha to 1
}
};
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
const [image] = useImage('https://konvajs.org/assets/lion.png', 'anonymous');
const imageRef = useRef(null);
useEffect(() => {
if (image) {
imageRef.current.cache();
}
}, [image]);
return (
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
filters={[Konva.Filters.RemoveAlpha]}
/>
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Emboss filter Image Tutorial
> Learn how to apply an emboss filter to images on HTML5 Canvas using Konva.js with adjustable strength, white level, and blend controls.
Source: https://konvajs.org/docs/filters/Emboss.html
To apply filter to an `Konva.Image`, we have to cache it first with `cache()`
function. Then apply filter with `filters()` function.
**Instructions**: Slide the controls to change emboss values.
For all available filters go to [Filters Documentation](/api/Konva.Filters.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const imageObj = new Image();
imageObj.onload = () => {
const image = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
draggable: true,
});
layer.add(image);
image.cache();
image.filters([Konva.Filters.Emboss]);
image.embossStrength(0.5);
image.embossWhiteLevel(0.5);
image.embossDirection('top-left');
image.embossBlend(0.5);
// create sliders
const strengthSlider = document.createElement('input');
strengthSlider.type = 'range';
strengthSlider.min = '0';
strengthSlider.max = '1';
strengthSlider.step = '0.1';
strengthSlider.value = image.embossStrength();
strengthSlider.style.position = 'absolute';
strengthSlider.style.top = '20px';
strengthSlider.style.left = '20px';
const whiteLevelSlider = document.createElement('input');
whiteLevelSlider.type = 'range';
whiteLevelSlider.min = '0';
whiteLevelSlider.max = '1';
whiteLevelSlider.step = '0.1';
whiteLevelSlider.value = image.embossWhiteLevel();
whiteLevelSlider.style.position = 'absolute';
whiteLevelSlider.style.top = '45px';
whiteLevelSlider.style.left = '20px';
const blendSlider = document.createElement('input');
blendSlider.type = 'range';
blendSlider.min = '0';
blendSlider.max = '1';
blendSlider.step = '0.1';
blendSlider.value = image.embossBlend();
blendSlider.style.position = 'absolute';
blendSlider.style.top = '70px';
blendSlider.style.left = '20px';
// add labels
const strengthLabel = document.createElement('div');
strengthLabel.textContent = 'Strength';
strengthLabel.style.position = 'absolute';
strengthLabel.style.top = '20px';
strengthLabel.style.left = '200px';
const whiteLevelLabel = document.createElement('div');
whiteLevelLabel.textContent = 'White Level';
whiteLevelLabel.style.position = 'absolute';
whiteLevelLabel.style.top = '45px';
whiteLevelLabel.style.left = '200px';
const blendLabel = document.createElement('div');
blendLabel.textContent = 'Blend';
blendLabel.style.position = 'absolute';
blendLabel.style.top = '70px';
blendLabel.style.left = '200px';
// add event listeners
strengthSlider.addEventListener('input', (e) => {
image.embossStrength(parseFloat(e.target.value));
});
whiteLevelSlider.addEventListener('input', (e) => {
image.embossWhiteLevel(parseFloat(e.target.value));
});
blendSlider.addEventListener('input', (e) => {
image.embossBlend(parseFloat(e.target.value));
});
// add elements to the page
document.body.appendChild(strengthSlider);
document.body.appendChild(whiteLevelSlider);
document.body.appendChild(blendSlider);
document.body.appendChild(strengthLabel);
document.body.appendChild(whiteLevelLabel);
document.body.appendChild(blendLabel);
};
imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg';
imageObj.crossOrigin = 'anonymous';
```
```js
import Konva from 'konva';
import { Stage, Layer, Image } from 'react-konva';
import { useState, useEffect, useRef } from 'react';
import useImage from 'use-image';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
const [strength, setStrength] = useState(0.5);
const [whiteLevel, setWhiteLevel] = useState(0.5);
const [blend, setBlend] = useState(0.5);
const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous');
const imageRef = useRef(null);
useEffect(() => {
if (image) {
imageRef.current.cache();
}
}, [image]);
return (
<>
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
filters={[Konva.Filters.Emboss]}
embossStrength={strength}
embossWhiteLevel={whiteLevel}
embossDirection="top-left"
embossBlend={blend}
/>
>
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Enhance Image Filter Tutorial
> Learn how to enhance images on HTML5 Canvas using the Konva.js Enhance filter with adjustable enhancement levels.
Source: https://konvajs.org/docs/filters/Enhance.html
To apply filter to an `Konva.Image`, we have to cache it first with `cache()`
function. Then apply filter with `filters()` function.
To enhance an image with Konva, we can use the `Konva.Filters.Enhance` filter
and set the enhance amount with the `enhance` property.
**Instructions**: Slide the control to adjust the enhance value.
For all available filters go to [Filters Documentation](/api/Konva.Filters.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const imageObj = new Image();
imageObj.onload = () => {
const image = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
draggable: true,
});
layer.add(image);
image.cache();
image.filters([Konva.Filters.Enhance]);
image.enhance(0.4);
const slider = document.createElement('input');
slider.type = 'range';
slider.min = '-1';
slider.max = '1';
slider.step = '0.1';
slider.value = image.enhance();
slider.style.position = 'absolute';
slider.style.top = '20px';
slider.style.left = '20px';
slider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
image.enhance(value);
});
document.body.appendChild(slider);
};
imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg';
imageObj.crossOrigin = 'anonymous';
```
```js
import Konva from 'konva';
import { Stage, Layer, Image } from 'react-konva';
import { useState, useEffect, useRef } from 'react';
import useImage from 'use-image';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
const [enhance, setEnhance] = useState(0.4);
const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous');
const imageRef = useRef(null);
useEffect(() => {
if (image) {
imageRef.current.cache();
}
}, [image]);
return (
<>
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
filters={[Konva.Filters.Enhance]}
enhance={enhance}
/>
setEnhance(parseFloat(e.target.value))}
style={{ position: 'absolute', top: '20px', left: '20px' }}
/>
>
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Grayscale Image Filter Tutorial
> Learn how to convert images to grayscale on HTML5 Canvas using the Konva.js Grayscale filter.
Source: https://konvajs.org/docs/filters/Grayscale.html
To apply filter to an `Konva.Image`, we have to cache it first with `cache()` function. Then apply filter with `filters()` function.
To invert the colors of an image with Konva, we can use the
`Konva.Filters.Grayscale` filter.
For all available filters go to [Filters Documentation](/api/Konva.Filters.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const imageObj = new Image();
imageObj.onload = () => {
const image = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
draggable: true,
});
layer.add(image);
image.cache();
image.filters([Konva.Filters.Grayscale]);
};
imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg';
imageObj.crossOrigin = 'anonymous';
```
```js
import Konva from 'konva';
import { Stage, Layer, Image } from 'react-konva';
import { useState, useEffect, useRef } from 'react';
import useImage from 'use-image';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous');
const imageRef = useRef(null);
useEffect(() => {
if (image) {
imageRef.current.cache();
}
}, [image]);
return (
{image && (
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
filters={[Konva.Filters.Grayscale]}
/>
)}
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Hue, Saturation and Luminance filter Image Tutorial
> Learn how to adjust hue, saturation, and luminance of images on HTML5 Canvas using the Konva.js HSL filter.
Source: https://konvajs.org/docs/filters/HSL.html
To apply filter to an `Konva.Node`, we have to cache it first with `cache()` function. Then apply filter with `filters()` function.
To change hue, saturation and luminance components of an image with Konva, we can use the `Konva.Filters.HSL`.
**Instructions**: Slide the controls to change HSL values.
For all available filters go to [Filters Documentation](/api/Konva.Filters.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const imageObj = new Image();
imageObj.onload = () => {
const image = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
draggable: true,
});
layer.add(image);
image.cache();
image.filters([Konva.Filters.HSL]);
// create sliders
const createSlider = (label, min, max, defaultValue, property) => {
const container = document.createElement('div');
container.style.position = 'absolute';
container.style.left = '20px';
const text = document.createElement('span');
text.textContent = `${label}: `;
container.appendChild(text);
const slider = document.createElement('input');
slider.type = 'range';
slider.min = min;
slider.max = max;
slider.step = '0.1';
slider.value = defaultValue;
slider.style.width = '200px';
slider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
image[property](value);
});
container.appendChild(slider);
return container;
};
const hueSlider = createSlider('Hue', -180, 180, 0, 'hue');
hueSlider.style.top = '20px';
document.body.appendChild(hueSlider);
const saturationSlider = createSlider('Saturation', -2, 10, 0, 'saturation');
saturationSlider.style.top = '45px';
document.body.appendChild(saturationSlider);
const luminanceSlider = createSlider('Luminance', -2, 2, 0, 'luminance');
luminanceSlider.style.top = '70px';
document.body.appendChild(luminanceSlider);
};
imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg';
imageObj.crossOrigin = 'anonymous';
```
```js
import Konva from 'konva';
import { Stage, Layer, Image } from 'react-konva';
import { useState, useEffect, useRef } from 'react';
import useImage from 'use-image';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
const [hue, setHue] = useState(0);
const [saturation, setSaturation] = useState(0);
const [luminance, setLuminance] = useState(0);
const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous');
const imageRef = useRef(null);
useEffect(() => {
if (image && imageRef.current) {
imageRef.current.cache();
}
}, [image]);
return (
<>
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
filters={[Konva.Filters.HSL]}
hue={hue}
saturation={saturation}
luminance={luminance}
/>
>
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Hue, Saturation and Value filter Image Tutorial
> Learn how to adjust hue, saturation, and value of images on HTML5 Canvas using the Konva.js HSV filter.
Source: https://konvajs.org/docs/filters/HSV.html
To apply filter to an `Konva.Node`, we have to cache it first with `cache()` function. Then apply filter with `filters()` function.
To change hue, saturation and value components of an image with Konva, we can use the `Konva.Filters.HSV`.
**Instructions**: Slide the controls to change HSV values.
For all available filters go to [Filters Documentation](/api/Konva.Filters.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const imageObj = new Image();
imageObj.onload = () => {
const image = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
draggable: true,
});
layer.add(image);
image.cache();
image.filters([Konva.Filters.HSV]);
// create sliders
const createSlider = (label, min, max, defaultValue, property) => {
const container = document.createElement('div');
container.style.position = 'absolute';
container.style.left = '20px';
const text = document.createElement('span');
text.textContent = `${label}: `;
container.appendChild(text);
const slider = document.createElement('input');
slider.type = 'range';
slider.min = min;
slider.max = max;
slider.step = '0.1';
slider.value = defaultValue;
slider.style.width = '200px';
slider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
image[property](value);
});
container.appendChild(slider);
return container;
};
const hueSlider = createSlider('Hue', -180, 180, 0, 'hue');
hueSlider.style.top = '20px';
document.body.appendChild(hueSlider);
const saturationSlider = createSlider('Saturation', -2, 10, 0, 'saturation');
saturationSlider.style.top = '45px';
document.body.appendChild(saturationSlider);
const value = createSlider('Value', -2, 2, 0, 'value');
value.style.top = '70px';
document.body.appendChild(value);
};
imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg';
imageObj.crossOrigin = 'anonymous';
```
```js
import Konva from 'konva';
import { Stage, Layer, Image } from 'react-konva';
import { useState, useEffect, useRef } from 'react';
import useImage from 'use-image';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
const [hue, setHue] = useState(0);
const [saturation, setSaturation] = useState(0);
const [value, setValue] = useState(0);
const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous');
const imageRef = useRef(null);
useEffect(() => {
if (image && imageRef.current) {
imageRef.current.cache();
}
}, [image]);
return (
<>
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
filters={[Konva.Filters.HSV]}
hue={hue}
saturation={saturation}
value={value}
/>
>
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Invert Image Filter Tutorial
> Learn how to invert image colors on HTML5 Canvas using the Konva.js Invert filter for a negative photo effect.
Source: https://konvajs.org/docs/filters/Invert.html
To apply filter to an `Konva.Image`, we have to cache it first with `cache()` function. Then apply filter with `filters()` function.
To invert the colors of an image with Konva, we can use the
`Konva.Filters.Invert` filter.
For all available filters go to [Filters Documentation](/api/Konva.Filters.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const imageObj = new Image();
imageObj.onload = () => {
const image = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
draggable: true,
});
layer.add(image);
image.cache();
image.filters([Konva.Filters.Invert]);
};
imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg';
imageObj.crossOrigin = 'anonymous';
```
```js
import Konva from 'konva';
import { Stage, Layer, Image } from 'react-konva';
import { useState, useRef, useEffect } from 'react';
import useImage from 'use-image';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous');
const imageRef = useRef(null);
useEffect(() => {
if (image && imageRef.current) {
imageRef.current.cache();
}
}, [image]);
return (
{image && (
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
filters={[Konva.Filters.Invert]}
/>
)}
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Kaleidoscope Image Filter Tutorial
> Learn how to create a kaleidoscope effect on images using the Konva.js Kaleidoscope filter with adjustable power and angle.
Source: https://konvajs.org/docs/filters/Kaleidoscope.html
To apply filter to an `Konva.Image`, we have to cache it first with `cache()`
function. Then apply filter with `filters()` function.
To create a kaleidoscope with Konva, we can use the `Konva.Filters.Kaleidoscope`
filter and set the `kaleidoscopePower` and `kaleidoscopeAngle` properties.
**Instructions**: Slide the controls to adjust the kaleidoscope power and angle.
For all available filters go to [Filters Documentation](/api/Konva.Filters.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const imageObj = new Image();
imageObj.onload = () => {
const image = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
draggable: true,
});
layer.add(image);
image.cache();
image.filters([Konva.Filters.Kaleidoscope]);
image.kaleidoscopePower(3);
image.kaleidoscopeAngle(0);
// create sliders
const createSlider = (label, min, max, defaultValue, property) => {
const container = document.createElement('div');
container.style.position = 'absolute';
container.style.left = '20px';
const text = document.createElement('span');
text.textContent = `${label}: `;
container.appendChild(text);
const slider = document.createElement('input');
slider.type = 'range';
slider.min = min;
slider.max = max;
slider.step = property === 'kaleidoscopePower' ? '1' : '0.1';
slider.value = defaultValue;
slider.style.width = '200px';
slider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
image[property](value);
});
container.appendChild(slider);
return container;
};
const powerSlider = createSlider('Power', 2, 8, 3, 'kaleidoscopePower');
powerSlider.style.top = '20px';
document.body.appendChild(powerSlider);
const angleSlider = createSlider('Angle', 0, 360, 0, 'kaleidoscopeAngle');
angleSlider.style.top = '45px';
document.body.appendChild(angleSlider);
};
imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg';
imageObj.crossOrigin = 'anonymous';
```
```js
import Konva from 'konva';
import { Stage, Layer, Image } from 'react-konva';
import { useState, useEffect, useRef } from 'react';
import useImage from 'use-image';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
const [angle, setAngle] = useState(0);
const [power, setPower] = useState(3);
const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous');
const imageRef = useRef(null);
useEffect(() => {
if (image && imageRef.current) {
imageRef.current.cache();
}
}, [image]);
return (
<>
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
filters={[Konva.Filters.Kaleidoscope]}
kaleidoscopePower={power}
kaleidoscopeAngle={angle}
/>
>
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Mask Image Filter Tutorial
> Learn how to remove image backgrounds on HTML5 Canvas using the Konva.js Mask filter with adjustable threshold.
Source: https://konvajs.org/docs/filters/Mask.html
To apply filter to an `Konva.Image`, we have to cache it first with `cache()` function. Then apply filter with `filters()` function.
To mask the colors of an image with Konva, we can use the
`Konva.Filters.Mask` filter.
The `Konva.Filters.Mask` filter attempts to remove the background from an image. It works by:
1. Sampling the color of the four corners of the image.
2. If the corner colors are similar (within the `threshold`), it assumes this color represents the background.
3. It then creates a mask where pixels similar to the identified background color become transparent, and other pixels remain opaque.
4. This mask is refined using image processing techniques (like erosion and dilation) to remove noise and smooth edges.
5. Finally, the refined mask is applied to the image's alpha channel.
The `threshold` property (ranging from 0 to 255) controls how similar a pixel's color must be to the background color to be masked out. A lower threshold means only colors very close to the background will be removed, while a higher threshold will remove a wider range of colors.
**Instructions**: Slide the control to adjust the mask threshold.
For all available filters go to [Filters Documentation](/api/Konva.Filters.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const imageObj = new Image();
imageObj.onload = () => {
const image = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
draggable: true,
});
layer.add(image);
image.cache();
image.filters([Konva.Filters.Mask]);
image.threshold(10);
const slider = document.createElement('input');
slider.type = 'range';
slider.min = '0';
slider.max = '255';
slider.value = image.threshold();
slider.style.position = 'absolute';
slider.style.top = '20px';
slider.style.left = '20px';
slider.addEventListener('input', (e) => {
const value = parseInt(e.target.value);
image.threshold(value);
});
document.body.appendChild(slider);
};
imageObj.src = 'https://konvajs.org/assets/space.jpg';
imageObj.crossOrigin = 'anonymous';
```
```js
import Konva from 'konva';
import { Stage, Layer, Image } from 'react-konva';
import { useState, useEffect, useRef } from 'react';
import useImage from 'use-image';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
const [threshold, setThreshold] = useState(10);
const [image] = useImage('https://konvajs.org/assets/space.jpg', 'anonymous');
const imageRef = useRef(null);
useEffect(() => {
if (image) {
imageRef.current.cache();
}
}, [image]);
return (
<>
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
filters={[Konva.Filters.Mask]}
threshold={threshold}
/>
setThreshold(parseInt(e.target.value))}
style={{ position: 'absolute', top: '20px', left: '20px' }}
/>
>
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Multiple Filters Tutorial
> Learn how to apply multiple filters like blur, brightness, and contrast simultaneously to images using Konva.js.
Source: https://konvajs.org/docs/filters/Multiple_Filters.html
To apply multiple filters to an `Konva.Image`, we have to cache it first with `cache()`
function. Then apply filters with `filters()` function.
**Instructions**: Use the checkboxes to toggle different filters and adjust their values with the sliders.
For all available filters go to [Filters Documentation](/api/Konva.Filters.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const imageObj = new Image();
imageObj.onload = () => {
const image = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
draggable: true,
});
layer.add(image);
image.cache();
// Create controls container
const container = document.createElement('div');
container.style.position = 'absolute';
container.style.top = '20px';
container.style.left = '20px';
document.body.appendChild(container);
// Filter states
const filterStates = {
blur: false,
brightness: false,
contrast: false,
};
const filterValues = {
blur: 10,
brightness: 1.3,
contrast: 50,
};
// Create filter controls
const createFilterControl = (name, min, max, step, defaultValue) => {
const div = document.createElement('div');
div.style.marginBottom = '10px';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = name;
checkbox.checked = filterStates[name];
const label = document.createElement('label');
label.htmlFor = name;
label.textContent = ` ${name.charAt(0).toUpperCase() + name.slice(1)}: `;
const slider = document.createElement('input');
slider.type = 'range';
slider.min = min;
slider.max = max;
slider.step = step;
slider.value = defaultValue;
slider.style.width = '200px';
slider.disabled = !filterStates[name];
div.appendChild(checkbox);
div.appendChild(label);
div.appendChild(slider);
checkbox.addEventListener('change', (e) => {
filterStates[name] = e.target.checked;
slider.disabled = !e.target.checked;
updateFilters();
});
slider.addEventListener('input', (e) => {
filterValues[name] = parseFloat(e.target.value);
updateFilters();
});
return div;
};
// Add controls
container.appendChild(createFilterControl('blur', 0, 40, 1, filterValues.blur));
container.appendChild(createFilterControl('brightness', 0, 2, 0.1, filterValues.brightness));
container.appendChild(createFilterControl('contrast', -100, 100, 1, filterValues.contrast));
function updateFilters() {
const activeFilters = [];
if (filterStates.blur) {
activeFilters.push(Konva.Filters.Blur);
image.blurRadius(filterValues.blur);
}
if (filterStates.brightness) {
activeFilters.push(Konva.Filters.Brightness);
image.brightness(filterValues.brightness);
}
if (filterStates.contrast) {
activeFilters.push(Konva.Filters.Contrast);
image.contrast(filterValues.contrast);
}
image.filters(activeFilters);
}
};
imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg';
imageObj.crossOrigin = 'anonymous';
```
```js
import Konva from 'konva';
import { Stage, Layer, Image } from 'react-konva';
import { useState, useEffect, useRef } from 'react';
import useImage from 'use-image';
const FilterControl = ({ name, min, max, step, filters, setFilters }) => {
const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1);
return (
{
setFilters({
...filters,
[name]: { ...filters[name], active: e.target.checked },
});
}}
/>
{capitalizedName}:
{
setFilters({
...filters,
[name]: { ...filters[name], value: parseFloat(e.target.value) },
});
}}
style={{ width: '200px' }}
/>
);
};
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
const [filters, setFilters] = useState({
blur: { active: false, value: 10 },
brightness: { active: false, value: 1.3 },
contrast: { active: false, value: 50 },
});
const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous');
const imageRef = useRef(null);
useEffect(() => {
if (image && imageRef.current) {
imageRef.current.cache();
}
}, [image]);
const activeFilters = [];
if (filters.blur.active) activeFilters.push(Konva.Filters.Blur);
if (filters.brightness.active) activeFilters.push(Konva.Filters.Brightness);
if (filters.contrast.active) activeFilters.push(Konva.Filters.Contrast);
return (
<>
{image && (
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
filters={activeFilters}
blurRadius={filters.blur.value}
brightness={filters.brightness.value}
contrast={filters.contrast.value}
/>
)}
>
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Noise filter Image Tutorial
> Learn how to add a noise effect to images on HTML5 Canvas using the Konva.js Noise filter with adjustable intensity.
Source: https://konvajs.org/docs/filters/Noise.html
To apply filter to an `Konva.Node`, we have to cache it first with `cache()`
function. Then apply filter with `filters()` function.
To change noise of an image with Konva, we can use the `Konva.Filters.Noise`.
**Instructions**: Slide the control to change noise value.
For all available filters go to [Filters Documentation](/api/Konva.Filters.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const imageObj = new Image();
imageObj.onload = () => {
const image = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
draggable: true,
});
layer.add(image);
image.cache();
image.filters([Konva.Filters.Noise]);
image.noise(0.3);
// create slider
const container = document.createElement('div');
container.style.position = 'absolute';
container.style.top = '20px';
container.style.left = '20px';
const text = document.createElement('span');
text.textContent = 'Noise: ';
container.appendChild(text);
const slider = document.createElement('input');
slider.type = 'range';
slider.min = '0';
slider.max = '1';
slider.step = '0.1';
slider.value = image.noise();
slider.style.width = '200px';
slider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
image.noise(value);
});
container.appendChild(slider);
document.body.appendChild(container);
};
imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg';
imageObj.crossOrigin = 'anonymous';
```
```js
import Konva from 'konva';
import { Stage, Layer, Image } from 'react-konva';
import { useState, useEffect, useRef } from 'react';
import useImage from 'use-image';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
const [noise, setNoise] = useState(0.3);
const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous');
const imageRef = useRef(null);
useEffect(() => {
if (image && imageRef.current) {
imageRef.current.cache();
}
}, [image]);
return (
<>
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
filters={[Konva.Filters.Noise]}
noise={noise}
/>
setNoise(parseFloat(e.target.value))}
style={{ position: 'absolute', top: '20px', left: '20px' }}
/>
>
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Pixelate filter Image Tutorial
> Learn how to pixelate images on HTML5 Canvas using the Konva.js Pixelate filter with adjustable pixel size.
Source: https://konvajs.org/docs/filters/Pixelate.html
To apply filter to an `Konva.Node`, we have to cache it first with `cache()`
function. Then apply filter with `filters()` function.
To change pixelate effect of an image with Konva, we can use the `Konva.Filters.Pixelate`.
**Instructions**: Slide the control to change pixel size value.
For all available filters go to [Filters Documentation](/api/Konva.Filters.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const imageObj = new Image();
imageObj.onload = () => {
const image = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
draggable: true,
});
layer.add(image);
image.cache();
image.filters([Konva.Filters.Pixelate]);
image.pixelSize(8);
// create slider
const container = document.createElement('div');
container.style.position = 'absolute';
container.style.top = '20px';
container.style.left = '20px';
const text = document.createElement('span');
text.textContent = 'Pixel Size: ';
container.appendChild(text);
const slider = document.createElement('input');
slider.type = 'range';
slider.min = '2';
slider.max = '32';
slider.step = '1';
slider.value = image.pixelSize();
slider.style.width = '200px';
slider.addEventListener('input', (e) => {
const value = parseInt(e.target.value);
image.pixelSize(value);
});
container.appendChild(slider);
document.body.appendChild(container);
};
imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg';
imageObj.crossOrigin = 'anonymous';
```
```js
import Konva from 'konva';
import { Stage, Layer, Image } from 'react-konva';
import { useState, useEffect, useRef } from 'react';
import useImage from 'use-image';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
const [pixelSize, setPixelSize] = useState(8);
const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous');
const imageRef = useRef(null);
useEffect(() => {
if (image && imageRef.current) {
imageRef.current.cache();
}
}, [image]);
return (
<>
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
filters={[Konva.Filters.Pixelate]}
pixelSize={pixelSize}
/>
setPixelSize(parseInt(e.target.value))}
style={{ position: 'absolute', top: '20px', left: '20px' }}
/>
>
);
};
export default App;
```
```js
```
---
# HTML5 Canvas RGB filter Image Tutorial
> Learn how to adjust red, green, and blue color channels of images on HTML5 Canvas using the Konva.js RGB filter.
Source: https://konvajs.org/docs/filters/RGB.html
To apply filter to an `Konva.Image`, we have to cache it first with `cache()`
function. Then apply filter with `filters()` function.
To change rgb components of an image with Konva, we can use the `Konva.Filters.RGB`.
**Instructions**: Slide the controls to change RGB values.
For all available filters go to [Filters Documentation](/api/Konva.Filters.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const imageObj = new Image();
imageObj.onload = () => {
const image = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
draggable: true,
});
layer.add(image);
image.cache();
image.filters([Konva.Filters.RGB]);
image.red(100);
image.green(100);
image.blue(100);
// create sliders
const createSlider = (label, property) => {
const container = document.createElement('div');
container.style.position = 'absolute';
container.style.left = '20px';
const text = document.createElement('span');
text.textContent = `${label}: `;
container.appendChild(text);
const slider = document.createElement('input');
slider.type = 'range';
slider.min = '0';
slider.max = '255';
slider.step = '1';
slider.value = image[property]();
slider.style.width = '200px';
slider.addEventListener('input', (e) => {
const value = parseInt(e.target.value);
image[property](value);
});
container.appendChild(slider);
return container;
};
const redSlider = createSlider('Red', 'red');
redSlider.style.top = '20px';
document.body.appendChild(redSlider);
const greenSlider = createSlider('Green', 'green');
greenSlider.style.top = '45px';
document.body.appendChild(greenSlider);
const blueSlider = createSlider('Blue', 'blue');
blueSlider.style.top = '70px';
document.body.appendChild(blueSlider);
};
imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg';
imageObj.crossOrigin = 'anonymous';
```
```js
import Konva from 'konva';
import { Stage, Layer, Image } from 'react-konva';
import { useState, useEffect, useRef } from 'react';
import useImage from 'use-image';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
const [red, setRed] = useState(100);
const [green, setGreen] = useState(100);
const [blue, setBlue] = useState(100);
const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous');
const imageRef = useRef(null);
useEffect(() => {
if (image && imageRef.current) {
imageRef.current.cache();
}
}, [image]);
return (
<>
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
filters={[Konva.Filters.RGB]}
red={red}
green={green}
blue={blue}
/>
>
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Sepia filter Image Tutorial
> Learn how to apply a sepia tone effect to images on HTML5 Canvas using the Konva.js Sepia filter.
Source: https://konvajs.org/docs/filters/Sepia.html
To apply filter to an `Konva.Image`, we have to cache it first with `cache()`
function. Then apply filter with `filters()` function.
To apply a sepia effect to an image with Konva, we can use the `Konva.Filters.Sepia`.
For all available filters go to [Filters Documentation](/api/Konva.Filters.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const imageObj = new Image();
imageObj.onload = () => {
const image = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
draggable: true,
});
layer.add(image);
// Apply Sepia filter
image.cache();
image.filters([Konva.Filters.Sepia]);
};
imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg';
imageObj.crossOrigin = 'anonymous';
```
```js
import Konva from 'konva';
import { Stage, Layer, Image } from 'react-konva';
import { useState, useRef, useEffect } from 'react';
import useImage from 'use-image';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous');
const imageRef = useRef(null);
useEffect(() => {
if (image && imageRef.current) {
imageRef.current.cache();
}
}, [image]);
return (
{image && (
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
filters={[Konva.Filters.Sepia]}
/>
)}
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Solarize filter Image Tutorial
> Learn how to apply a solarize effect to images on HTML5 Canvas using the Konva.js Solarize filter with adjustable threshold.
Source: https://konvajs.org/docs/filters/Solarize.html
To apply filter to an `Konva.Image`, we have to cache it first with `cache()`
function. Then apply filter with `filters()` function.
To apply a solarize effect to an image with Konva, we can use the `Konva.Filters.Solarize`.
**Instructions**: Slide the control to change threshold value.
For all available filters go to [Filters Documentation](/api/Konva.Filters.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const imageObj = new Image();
imageObj.onload = () => {
const image = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
draggable: true,
});
layer.add(image);
image.cache();
image.filters([Konva.Filters.Solarize]);
image.threshold(0.5);
// create slider
const container = document.createElement('div');
container.style.position = 'absolute';
container.style.top = '20px';
container.style.left = '20px';
const text = document.createElement('span');
text.textContent = 'Threshold: ';
container.appendChild(text);
const slider = document.createElement('input');
slider.type = 'range';
slider.min = '0';
slider.max = '1';
slider.step = '0.1';
slider.value = image.threshold();
slider.style.width = '200px';
slider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
image.threshold(value);
});
container.appendChild(slider);
document.body.appendChild(container);
};
imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg';
imageObj.crossOrigin = 'anonymous';
```
```js
import Konva from 'konva';
import { Stage, Layer, Image } from 'react-konva';
import { useState, useEffect, useRef } from 'react';
import useImage from 'use-image';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
const [threshold, setThreshold] = useState(0.5);
const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous');
const imageRef = useRef(null);
useEffect(() => {
if (image && imageRef.current) {
imageRef.current.cache();
}
}, [image]);
return (
<>
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
filters={[Konva.Filters.Solarize]}
threshold={threshold}
/>
setThreshold(parseFloat(e.target.value))}
style={{ position: 'absolute', top: '20px', left: '20px' }}
/>
>
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Threshold filter Image Tutorial
> Learn how to convert images to black and white using the Konva.js Threshold filter with an adjustable threshold value.
Source: https://konvajs.org/docs/filters/Threshold.html
To apply filter to an `Konva.Image`, we have to cache it first with `cache()`
function. Then apply filter with `filters()` function.
To apply a threshold effect to an image with Konva, we can use the `Konva.Filters.Threshold`.
The threshold filter converts the image into a black and white image where all pixels above the threshold value become white and all pixels below become black.
**Instructions**: Slide the control to change threshold value.
For all available filters go to [Filters Documentation](/api/Konva.Filters.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const imageObj = new Image();
imageObj.onload = () => {
const image = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
draggable: true,
});
layer.add(image);
image.cache();
image.filters([Konva.Filters.Threshold]);
image.threshold(0.5);
// create slider
const container = document.createElement('div');
container.style.position = 'absolute';
container.style.top = '20px';
container.style.left = '20px';
const text = document.createElement('span');
text.textContent = 'Threshold: ';
container.appendChild(text);
const slider = document.createElement('input');
slider.type = 'range';
slider.min = '0';
slider.max = '1';
slider.step = '0.1';
slider.value = image.threshold();
slider.style.width = '200px';
slider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
image.threshold(value);
});
container.appendChild(slider);
document.body.appendChild(container);
};
imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg';
imageObj.crossOrigin = 'anonymous';
```
```js
import Konva from 'konva';
import { Stage, Layer, Image } from 'react-konva';
import { useState, useEffect, useRef } from 'react';
import useImage from 'use-image';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
const [threshold, setThreshold] = useState(0.5);
const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous');
const imageRef = useRef(null);
useEffect(() => {
if (image && imageRef.current) {
imageRef.current.cache();
}
}, [image]);
return (
<>
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
filters={[Konva.Filters.Threshold]}
threshold={threshold}
/>
setThreshold(parseFloat(e.target.value))}
style={{ position: 'absolute', top: '20px', left: '20px' }}
/>
>
);
};
export default App;
```
```js
```
---
# Move Shape to Another Container
> Learn how to move shapes between groups, layers, and containers in Konva using the moveTo() method.
Source: https://konvajs.org/docs/groups_and_layers/Change_Containers.html
To move a shape from one container into another with Konva, we can use the `moveTo()` method which requires a container as a parameter.
A container can be another stage, a layer, or a group. You can also move groups into other groups and layers, or shapes from groups directly into other layers.
**Instructions: Drag and drop the groups and observe that the red rectangle is bound to either the yellow group or the blue group. Use the buttons on the left to move the box from one group into another.**
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
// yellow group
const group1 = new Konva.Group({
x: 50,
y: 50,
draggable: true,
});
const yellow = new Konva.Rect({
width: 100,
height: 100,
fill: 'yellow',
stroke: 'black',
strokeWidth: 4,
});
group1.add(yellow);
// blue group
const group2 = new Konva.Group({
x: 200,
y: 50,
draggable: true,
});
const blue = new Konva.Rect({
width: 100,
height: 100,
fill: 'blue',
stroke: 'black',
strokeWidth: 4,
});
group2.add(blue);
// red box
const redBox = new Konva.Rect({
x: 10,
y: 10,
width: 30,
height: 30,
fill: 'red',
});
group1.add(redBox);
layer.add(group1);
layer.add(group2);
stage.add(layer);
// create buttons
const moveToGroup1Btn = document.createElement('button');
moveToGroup1Btn.textContent = 'Move to yellow group';
moveToGroup1Btn.addEventListener('click', () => {
redBox.moveTo(group1);
});
const moveToGroup2Btn = document.createElement('button');
moveToGroup2Btn.textContent = 'Move to blue group';
moveToGroup2Btn.addEventListener('click', () => {
redBox.moveTo(group2);
});
document.body.appendChild(moveToGroup1Btn);
document.body.appendChild(moveToGroup2Btn);
````
```js
import { Stage, Layer, Rect, Group } from 'react-konva';
import { useState } from 'react';
const App = () => {
const [redBoxGroup, setRedBoxGroup] = useState('yellow');
const [groupPositions, setGroupPositions] = useState({
yellow: { x: 50, y: 50 },
blue: { x: 200, y: 50 },
});
const handleDragEnd = (group, e) => {
setGroupPositions((positions) => ({
...positions,
[group]: { x: e.target.x(), y: e.target.y() },
}));
};
return (
<>
setRedBoxGroup('yellow')}>
Move to yellow group
setRedBoxGroup('blue')}>
Move to blue group
handleDragEnd('yellow', e)}
>
{redBoxGroup === 'yellow' && (
)}
handleDragEnd('blue', e)}
>
{redBoxGroup === 'blue' && (
)}
>
);
};
export default App;
````
```js
Move to yellow group
Move to blue group
```
---
# Shape Groups
> Learn how to group multiple shapes together with Konva.Group to move, rotate, and scale them as a single unit.
Source: https://konvajs.org/docs/groups_and_layers/Groups.html
To group multiple shapes together with Konva, we can instantiate a `Konva.Group()` object and then add shapes to it with the `add()` method.
Grouping shapes together is really handy when we want to transform multiple shapes together, e.g. if we want to move, rotate, or scale multiple shapes at once.
Groups can also be added to other groups to create more complex Node trees.
For a full list of attributes and methods, check out the [Konva.Group documentation](/api/Konva.Group.html).
**Instructions: Try to drag the group. Notice how all shapes move together.**
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
const group = new Konva.Group({
x: 50,
y: 50,
draggable: true,
});
const circle = new Konva.Circle({
x: 40,
y: 40,
radius: 30,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
const rect = new Konva.Rect({
x: 80,
y: 20,
width: 100,
height: 50,
fill: 'green',
stroke: 'black',
strokeWidth: 4,
});
group.add(circle);
group.add(rect);
layer.add(group);
stage.add(layer);
````
```js
import { Stage, Layer, Group, Circle, Rect } from 'react-konva';
import { useState } from 'react';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
return (
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
>
);
};
export default App;
````
```js
```
---
# Shape Layering
> Learn how to reorder shapes on the canvas using moveToTop, moveToBottom, moveUp, and moveDown methods in Konva.
Source: https://konvajs.org/docs/groups_and_layers/Layering.html
To layer shapes with Konva, we can use one of the following layering methods:
`moveToTop()`, `moveToBottom()`, `moveUp()`, `moveDown()`, or `zIndex()`.
You can also layer groups and layers.
**Instructions: Drag and drop the boxes to move them around, and then use the buttons on the left to reorder the yellow box.**
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
const yellowBox = new Konva.Rect({
x: 50,
y: 50,
width: 100,
height: 100,
fill: 'yellow',
stroke: 'black',
strokeWidth: 4,
draggable: true,
});
const redBox = new Konva.Rect({
x: 100,
y: 100,
width: 100,
height: 100,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
draggable: true,
});
layer.add(yellowBox);
layer.add(redBox);
stage.add(layer);
// create buttons
const toTopBtn = document.createElement('button');
toTopBtn.textContent = 'Move yellow box to top';
toTopBtn.addEventListener('click', () => {
yellowBox.moveToTop();
});
const toBottomBtn = document.createElement('button');
toBottomBtn.textContent = 'Move yellow box to bottom';
toBottomBtn.addEventListener('click', () => {
yellowBox.moveToBottom();
});
document.body.prepend(toTopBtn);
document.body.prepend(toBottomBtn);
````
```js
import { Stage, Layer, Rect } from 'react-konva';
import { useRef, useState } from 'react';
const App = () => {
const yellowRef = useRef();
const [positions, setPositions] = useState({
yellow: { x: 50, y: 50 },
red: { x: 100, y: 100 },
});
const handleDragEnd = (color, e) => {
setPositions((currentPositions) => ({
...currentPositions,
[color]: { x: e.target.x(), y: e.target.y() },
}));
};
return (
<>
yellowRef.current.moveToTop()}>
Move yellow box to top
yellowRef.current.moveToBottom()}>
Move yellow box to bottom
handleDragEnd('yellow', e)}
/>
handleDragEnd('red', e)}
/>
>
);
};
export default App;
````
```js
Move yellow box to top
Move yellow box to bottom
```
---
# Understanding Node zIndex
> Understand how zIndex works in Konva as the index of a node within its parent's children array, and how it differs from CSS z-index.
Source: https://konvajs.org/docs/groups_and_layers/zIndex.html
## What is zIndex of a node?
You can get/set zIndex of a node in this way:
```javascript
// get
const zIndex = shape.zIndex();
// set
shape.zIndex(1);
```
zIndex is just the index of a node in its parent's children array. Please don't confuse `zIndex` in Konva with `z-index` in CSS.
```javascript
const group = new Konva.Group();
const circle = new Konva.Circle({});
group.add(circle);
// it will log 0
console.log(circle.zIndex());
// the next line will not work because the group has only one child
circle.zIndex(1);
// still logs 0
console.log(circle.zIndex());
// for any node this equation will be true:
console.log(circle.zIndex() === circle.getParent().children.indexOf(circle));
```
You can't use `zIndex` to set absolute position of the node, like we do in CSS.
Konva draws nodes in the strict order as they are defined in the nodes tree.
**Instructions: Try to change zIndex of shapes using the buttons. Notice how the order of shapes changes.**
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
// first group
const group1 = new Konva.Group();
layer.add(group1);
const blackRect = new Konva.Rect({
x: 10,
y: 10,
width: 100,
height: 100,
fill: 'black',
});
group1.add(blackRect);
const redCircle = new Konva.Circle({
x: 80,
y: 80,
radius: 40,
fill: 'red',
});
group1.add(redCircle);
// second group
const group2 = new Konva.Group();
layer.add(group2);
const greenRect = new Konva.Rect({
x: 50,
y: 50,
width: 100,
height: 100,
fill: 'green',
});
group2.add(greenRect);
stage.add(layer);
// create buttons
const btn1 = document.createElement('button');
btn1.textContent = 'Move red circle to group2';
btn1.addEventListener('click', () => {
redCircle.moveTo(group2);
});
const btn2 = document.createElement('button');
btn2.textContent = 'Move red circle to group1';
btn2.addEventListener('click', () => {
redCircle.moveTo(group1);
});
document.body.appendChild(btn1);
document.body.appendChild(btn2);
````
```js
import { Stage, Layer, Group, Rect, Circle } from 'react-konva';
import { useState } from 'react';
const App = () => {
const [redCircleGroup, setRedCircleGroup] = useState('group1');
return (
<>
setRedCircleGroup('group2')}>
Move red circle to group2
setRedCircleGroup('group1')}>
Move red circle to group1
{redCircleGroup === 'group1' && (
)}
{redCircleGroup === 'group2' && (
)}
>
);
};
export default App;
````
```js
Move red circle to group2
Move red circle to group1
```
---
# HTML5 Canvas How to avoid Memory leaks Tip
> Prevent memory leaks in HTML5 Canvas applications with Konva.js. Learn how to properly destroy shapes, detach events, and manage canvas resources.
Source: https://konvajs.org/docs/performance/Avoid_Memory_Leaks.html
### Deleting shapes
There are two very close methods `remove()` and `destroy()`. If you need to completely delete a node you should `destroy()` it. The `destroy()` method deletes all references to node from the KonvaJS engine. If you are going to reuse a node you should `remove()` it then later you can add it again to any container.
### Tweening
When you are using `Konva.Tween` instance you have to destroy it after usage.
Here's a demo showing proper memory management:
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// Create circle
const circle = new Konva.Circle({
x: 100,
y: 100,
radius: 30,
fill: 'red'
});
layer.add(circle);
// Add buttons
const addButton = document.createElement('button');
addButton.textContent = 'Add Circle';
document.body.appendChild(addButton);
const removeButton = document.createElement('button');
removeButton.textContent = 'Remove Circle';
document.body.appendChild(removeButton);
const animateButton = document.createElement('button');
animateButton.textContent = 'Animate';
document.body.appendChild(animateButton);
// Handle adding/removing
addButton.addEventListener('click', () => {
layer.add(circle);
});
removeButton.addEventListener('click', () => {
// Just remove from layer, can be added back
circle.remove();
});
animateButton.addEventListener('click', () => {
// Using to() method which auto-destroys the tween
circle.to({
x: Math.random() * stage.width(),
y: Math.random() * stage.height(),
duration: 1
});
// If using Tween directly, make sure to destroy it
const tween = new Konva.Tween({
node: circle,
rotation: 360,
duration: 1,
onFinish: function() {
// Clean up the tween
tween.destroy();
}
}).play();
});
```
```js
import { Stage, Layer, Circle } from 'react-konva';
import { useState } from 'react';
const App = () => {
const [isVisible, setIsVisible] = useState(true);
const [position, setPosition] = useState({ x: 100, y: 100 });
const [rotation, setRotation] = useState(0);
const handleAdd = () => {
setIsVisible(true);
};
const handleRemove = () => {
setIsVisible(false);
};
const handleAnimate = () => {
// Update position
setPosition({
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight
});
// Update rotation
setRotation(rotation + 360);
};
return (
Add Circle
Remove Circle
Animate
{isVisible && (
)}
);
};
export default App;
```
```js
Add Circle
Remove Circle
Animate
```
---
# Automatic Redraws — Do You Need draw() or batchDraw()?
> Konva redraws automatically since version 8, so layer.draw() and layer.batchDraw() are no longer needed after changing a shape. When a manual redraw is still required, and how to turn batching off.
Source: https://konvajs.org/docs/performance/Batch_Draw.html
**Short answer: no. Since Konva 8, you do not need to call `draw()` or
`batchDraw()` after changing a shape.**
```js
// This is enough. Konva schedules the redraw itself.
rect.fill('red');
rect.x(120);
// Both of these are redundant.
layer.draw();
layer.batchDraw();
```
Setting any attribute marks the layer as dirty, and Konva redraws it once on
the next animation frame. Changing ten attributes in a row still produces one
redraw, so calling `batchDraw()` yourself adds nothing — the batching already
happened.
You will still see `layer.draw()` in older tutorials, in answers written before
2021, and in code that AI assistants generate, because that is what the corpus
is full of. It is harmless, just unnecessary. Deleting those calls will not
change what you see on screen.
## When you do still need a manual redraw
Auto-draw reacts to *Konva* changes. If something changes outside Konva's
knowledge, nothing marks the layer dirty and you have to say so yourself.
The usual case is a `Konva.Image` backed by a source that mutates on its own —
a `` element, an animated GIF, or a raw canvas another library draws
into:
```js
// An external library drew a new GIF frame into our canvas.
// Konva has no attribute change to notice, so ask for the redraw.
function onDrawFrame(ctx, frame) {
ctx.drawImage(frame.buffer, 0, 0);
layer.draw();
}
```
For video, prefer [`Konva.Animation`](/docs/animations/Create_an_Animation.html),
which redraws every frame for you. There is a worked example in
[Video on canvas](/docs/sandbox/Video_On_Canvas.html) and
[GIF on canvas](/docs/sandbox/GIF_On_Canvas.html).
## Turning batching off
`Konva.autoDrawEnabled = false` restores the old behaviour, where nothing is
drawn until you ask. It is worth it only if you are driving the render loop
yourself and want exact control over when frames happen. With it off,
`batchDraw()` becomes useful again: it collapses many calls into one redraw per
frame, instead of redrawing on every one.
The demo below turns auto-draw off so `batchDraw()` has something to do. The
rectangle rotates on `mousemove`, an event that can fire far more often than the
screen refreshes.
**Instructions:** Move your mouse over the stage to spin the rectangle
```js
import Konva from 'konva';
// Turned off on purpose, so batchDraw has a job to do
Konva.autoDrawEnabled = false;
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const rect = new Konva.Rect({
x: stage.width() / 2 - 50,
y: stage.height() / 2 - 25,
width: 100,
height: 50,
fill: 'green',
stroke: 'black',
strokeWidth: 4,
});
layer.add(rect);
stage.on('mousemove', () => {
// rotate rectangle on mouse move
rect.rotate(5);
// auto-draw is off, so ask for a batched redraw
layer.batchDraw();
});
```
```js
import { Stage, Layer, Rect } from 'react-konva';
import { useRef, useEffect } from 'react';
// Turned off on purpose, so batchDraw has a job to do
Konva.autoDrawEnabled = false;
const App = () => {
const rectRef = useRef(null);
const layerRef = useRef(null);
useEffect(() => {
const stage = rectRef.current.getStage();
stage.on('mousemove', () => {
// rotate rectangle on mouse move
rectRef.current.rotate(5);
// auto-draw is off, so ask for a batched redraw
layerRef.current.getLayer().batchDraw();
});
}, []);
return (
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Disable Perfect Drawing Tip
> Improve Konva canvas performance by disabling perfectDrawEnabled to skip buffer canvas rendering for shapes with opacity.
Source: https://konvajs.org/docs/performance/Disable_Perfect_Draw.html
In some cases drawing on canvas has unexpected results.
For example, let's draw a shape with fill, stroke and opacity.
As strokes are drawn on top of fill, there's a line of half the size of the stroke inside the shape which is darker
because it's the intersection of the fill and the stroke.
Probably that is not expected for you. So `Konva` fixes such behavior with using buffer canvas.
In this case `Konva` is doing these steps:
1. Draw shape on buffer canvas
2. Fill and stroke it WITHOUT opacity
3. Apply opacity on layer's canvas
4. Then draw on layer canvas result from buffer
But using buffer canvas might drop performance. So you can disable such fixing:
```javascript
shape.perfectDrawEnabled(false);
```
See difference here:
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// With perfect drawing (default)
const perfectCircle = new Konva.Circle({
x: 100,
y: 100,
radius: 50,
fill: 'red',
stroke: 'black',
strokeWidth: 10,
opacity: 0.5,
});
// Without perfect drawing
const nonPerfectCircle = new Konva.Circle({
x: 250,
y: 100,
radius: 50,
fill: 'red',
stroke: 'black',
strokeWidth: 10,
opacity: 0.5,
perfectDrawEnabled: false,
});
// Add labels
const perfectLabel = new Konva.Text({
x: 50,
y: 170,
text: 'Perfect Drawing',
fontSize: 16,
});
const nonPerfectLabel = new Konva.Text({
x: 200,
y: 170,
text: 'Perfect Drawing Disabled',
fontSize: 16,
});
layer.add(perfectCircle);
layer.add(nonPerfectCircle);
layer.add(perfectLabel);
layer.add(nonPerfectLabel);
```
```js
import { Stage, Layer, Circle, Text } from 'react-konva';
const App = () => {
return (
{/* With perfect drawing (default) */}
{/* Without perfect drawing */}
{/* Labels */}
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Layer Management Performance Tip
> Improve HTML5 Canvas performance with Konva.js multi-layer architecture. Separate static and dynamic content across layers to avoid unnecessary redraws.
Source: https://konvajs.org/docs/performance/Layer_Management.html
When creating Konva applications, the most important thing to consider,
in regards to performance, is layer management. One of the things that makes
Konva stand out from other canvas libraries is that it enables us to create
individual layers, each with their own canvas elements. This means that we can
animate, transition, or update some stage elements, while not redrawing others.
If we inspect the DOM of a Konva stage, we'll see that there is actually one
canvas element per layer.
This tutorial has two layers, one layer that's animated, and another static layer
that contains text. Since there's no reason to continually redraw the text, it's placed in its own layer.
**Note: Do not create too many layers. Usually 3-5 is max.**
Below is a demo showing efficient layer management:
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
// Static layer for text
const textLayer = new Konva.Layer();
stage.add(textLayer);
// Animated layer for shapes
const animLayer = new Konva.Layer();
stage.add(animLayer);
// Add static text
const text = new Konva.Text({
x: 20,
y: 20,
text: 'This text is in a static layer.\nThe circle below is in an animated layer.',
fontSize: 16,
fill: 'black'
});
textLayer.add(text);
// Add animated circle
const circle = new Konva.Circle({
x: 100,
y: 100,
radius: 30,
fill: 'red',
});
animLayer.add(circle);
// Create animation
const anim = new Konva.Animation((frame) => {
// Move circle in a figure-8 pattern
const scale = 100;
const centerX = stage.width() / 2;
const centerY = stage.height() / 2;
circle.x(centerX + Math.sin(frame.time / 1000) * scale);
circle.y(centerY + Math.sin(frame.time / 2000) * scale);
}, animLayer);
anim.start();
```
```js
import { Stage, Layer, Text, Circle } from 'react-konva';
import { useEffect, useRef } from 'react';
const App = () => {
const circleRef = useRef(null);
useEffect(() => {
const anim = new Konva.Animation((frame) => {
const scale = 100;
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
circleRef.current.x(centerX + Math.sin(frame.time / 1000) * scale);
circleRef.current.y(centerY + Math.sin(frame.time / 2000) * scale);
}, circleRef.current.getLayer());
anim.start();
return () => anim.stop();
}, []);
return (
{/* Static layer for text */}
{/* Animated layer for shapes */}
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Listening False Performance Tip
> Boost HTML5 Canvas performance with Konva.js by disabling event listening on shapes that don't need interaction. Set listening:false to skip hit detection.
Source: https://konvajs.org/docs/performance/Listening_False.html
If you have a lot of shapes on the canvas and you don't need to detect events for some of them,
you can set `listening` to `false` to improve performance.
When `listening = false`, the shape will be ignored from event detection (like mouseover, drag and drop, click, etc).
This can significantly improve performance for complex applications.
Below is a demo showing the performance difference between listening and non-listening shapes:
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// Create many circles with listening enabled
for (let i = 0; i < 100; i++) {
const circle = new Konva.Circle({
x: Math.random() * stage.width(),
y: Math.random() * stage.height(),
radius: 20,
fill: 'blue',
opacity: 0.5,
// Enable event detection (default)
listening: true,
});
// Add hover effect
circle.on('mouseover', function() {
this.fill('red');
});
circle.on('mouseout', function() {
this.fill('blue');
});
layer.add(circle);
}
// Create many circles with listening disabled
for (let i = 0; i < 1000; i++) {
const circle = new Konva.Circle({
x: Math.random() * stage.width(),
y: Math.random() * stage.height(),
radius: 20,
fill: 'green',
opacity: 0.5,
// Disable event detection for better performance
listening: false,
});
layer.add(circle);
}
// Add text explanation
const text = new Konva.Text({
x: 10,
y: 10,
text: 'Blue circles (100) have event listeners (hover them)\nGreen circles (1000) have no listeners (better performance)',
fontSize: 16,
fill: 'black',
});
layer.add(text);
```
```js
import { Stage, Layer, Circle, Text } from 'react-konva';
import { useState } from 'react';
// Generate circles data
const listeningCircles = Array.from({ length: 100 }, (_, i) => ({
id: i,
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
}));
const nonListeningCircles = Array.from({ length: 1000 }, (_, i) => ({
id: i + 100,
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
}));
const App = () => {
const [hoveredId, setHoveredId] = useState(null);
return (
{/* Circles with event listeners */}
{listeningCircles.map((circle) => (
setHoveredId(circle.id)}
onMouseLeave={() => setHoveredId(null)}
/>
))}
{/* Circles without event listeners */}
{nonListeningCircles.map((circle) => (
))}
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Optimize Animation Performance Tip
> Boost Konva animation performance with shape caching, Konva.Animation, and minimizing animated nodes.
Source: https://konvajs.org/docs/performance/Optimize_Animation.html
When creating animations with Konva, it's important to optimize them for better performance.
Here are some key tips:
1. Use `Konva.Animation` instead of `requestAnimationFrame` directly
2. Only animate properties that need to change
3. Consider using shape caching for complex shapes
4. Minimize the number of nodes being animated
Below is a demo showing optimized animation techniques:
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// Create a complex star shape
const star = new Konva.Star({
x: stage.width() / 2,
y: stage.height() / 2,
numPoints: 6,
innerRadius: 40,
outerRadius: 70,
fill: 'yellow',
stroke: 'black',
strokeWidth: 4,
});
// Cache the shape for better performance
star.cache();
layer.add(star);
// Create simple circle that doesn't need caching
const circle = new Konva.Circle({
x: 100,
y: 100,
radius: 20,
fill: 'red',
});
layer.add(circle);
// Create optimized animation
const anim = new Konva.Animation((frame) => {
// Rotate star (cached shape)
star.rotation(frame.time * 0.1);
// Move circle in a circle pattern
circle.x(100 + Math.cos(frame.time * 0.002) * 50);
circle.y(100 + Math.sin(frame.time * 0.002) * 50);
}, layer);
// Add start/stop button
const button = document.createElement('button');
button.textContent = 'Toggle Animation';
button.style.position = 'absolute';
button.style.top = '10px';
button.style.left = '10px';
document.body.appendChild(button);
let isPlaying = true;
button.addEventListener('click', () => {
if (isPlaying) {
anim.stop();
button.textContent = 'Start Animation';
} else {
anim.start();
button.textContent = 'Stop Animation';
}
isPlaying = !isPlaying;
});
anim.start();
```
```js
import { Stage, Layer, Star, Circle } from 'react-konva';
import { useState, useEffect, useRef } from 'react';
const App = () => {
const [isPlaying, setIsPlaying] = useState(true);
const starRef = useRef(null);
const circleRef = useRef(null);
const animRef = useRef(null);
useEffect(() => {
// Cache the star shape for better performance
if (starRef.current) {
starRef.current.cache();
}
// Create animation
const anim = new Konva.Animation((frame) => {
// Rotate star (cached shape)
starRef.current.rotation(frame.time * 0.1);
// Move circle in a circle pattern
circleRef.current.x(100 + Math.cos(frame.time * 0.002) * 50);
circleRef.current.y(100 + Math.sin(frame.time * 0.002) * 50);
}, starRef.current.getLayer());
animRef.current = anim;
anim.start();
return () => anim.stop();
}, []);
const toggleAnimation = () => {
if (isPlaying) {
animRef.current.stop();
} else {
animRef.current.start();
}
setIsPlaying(!isPlaying);
};
return (
{isPlaying ? 'Stop Animation' : 'Start Animation'}
);
};
export default App;
```
```js
{{ isPlaying ? 'Stop Animation' : 'Start Animation' }}
```
---
# HTML5 Canvas Optimize Strokes Performance Tip
> Optimize Konva stroke rendering performance by disabling shadowForStrokeEnabled to skip extra drawing passes.
Source: https://konvajs.org/docs/performance/Optimize_Strokes.html
When drawing shapes with strokes and shadows in Konva, there's an extra internal drawing step that occurs.
This is because Konva needs to ensure that the stroke's shadow is drawn correctly.
However, this can impact performance, especially when dealing with many shapes.
To optimize performance, you can disable the stroke shadow by setting `shadowForStrokeEnabled(false)`.
This is particularly useful when you don't need the stroke to cast a shadow.
Below is a demo showing the performance difference with and without stroke shadows:
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// Create shape with shadow for stroke (default)
const circleWithShadow = new Konva.Circle({
x: 100,
y: 100,
radius: 50,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
shadowColor: 'black',
shadowBlur: 10,
shadowOffset: { x: 5, y: 5 },
shadowOpacity: 0.5,
});
// Create shape without shadow for stroke (optimized)
const circleOptimized = new Konva.Circle({
x: 250,
y: 100,
radius: 50,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
shadowColor: 'black',
shadowBlur: 10,
shadowOffset: { x: 5, y: 5 },
shadowOpacity: 0.5,
shadowForStrokeEnabled: false,
});
// Add labels
const defaultLabel = new Konva.Text({
x: 50,
y: 170,
text: 'With Stroke Shadow',
fontSize: 16,
});
const optimizedLabel = new Konva.Text({
x: 200,
y: 170,
text: 'Without Stroke Shadow\n(Better Performance)',
fontSize: 16,
});
// Add FPS counter
const fpsText = new Konva.Text({
x: 10,
y: 10,
text: 'FPS: 0',
fontSize: 16,
});
layer.add(circleWithShadow);
layer.add(circleOptimized);
layer.add(defaultLabel);
layer.add(optimizedLabel);
layer.add(fpsText);
// Create animation to demonstrate performance
const anim = new Konva.Animation((frame) => {
circleWithShadow.rotation(frame.time * 0.1);
circleOptimized.rotation(frame.time * 0.1);
// Update FPS counter
fpsText.text('FPS: ' + frame.frameRate.toFixed(1));
}, layer);
anim.start();
```
```js
import { Stage, Layer, Circle, Text } from 'react-konva';
import { useEffect, useRef } from 'react';
const App = () => {
const circleWithShadowRef = useRef(null);
const circleOptimizedRef = useRef(null);
const fpsTextRef = useRef(null);
useEffect(() => {
const anim = new Konva.Animation((frame) => {
// Rotate circles
circleWithShadowRef.current.rotation(frame.time * 0.1);
circleOptimizedRef.current.rotation(frame.time * 0.1);
// Update FPS counter
fpsTextRef.current.text('FPS: ' + frame.frameRate.toFixed(1));
}, circleWithShadowRef.current.getLayer());
anim.start();
return () => anim.stop();
}, []);
return (
{/* Circle with shadow for stroke (default) */}
{/* Circle without shadow for stroke (optimized) */}
{/* Labels */}
{/* FPS counter */}
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Shape Caching Performance Tip
> Speed up HTML5 Canvas rendering with Konva.js shape caching. Cache complex shapes as images for dramatically better performance.
Source: https://konvajs.org/docs/performance/Shape_Caching.html
If you have a complex shape with many drawing operations, or if you're applying filters,
you can improve performance by caching the shape. When you cache a shape, Konva will draw
it onto an internal canvas buffer. After that, instead of redrawing the shape every time,
Konva will simply use the cached version.
This is particularly useful for:
1. Complex shapes with many drawing operations
2. Shapes with filters
3. Shapes that don't change often but need to be redrawn frequently
To cache a shape, simply call the `cache()` method. You can clear the cache with `clearCache()`.
## How caching works?
When you call the `cache()` method on a shape, Konva:
1. Creates an internal canvas buffer
2. Draws the shape onto this buffer
3. Stores this buffer for future use
After caching, instead of redrawing the shape every time it needs to be displayed, Konva simply uses the cached version from the buffer. This is much faster than redrawing the shape repeatedly.
## Guidelines
1. Don't cache simple shapes without filters. It may be faster to render it dirrectly, than from cached version.
2. Every cached node creates several canvas buffers. So don't overuse it, as it will consume a lot of memory.
3. It is better to cache groups of shapes, then to cache each shape individually.
4. Remember to always measure performance with and without caching to see the actual difference.
Below is a demo showing the performance difference between cached and non-cached complex shapes:
**Instructions:**
- Click anywhere on the stage to add 1000 more circles
- Toggle the checkbox to enable/disable caching
- Watch the FPS counter to see the performance difference
- The group of circles rotates continuously
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// Create a group of circles
const group = new Konva.Group({
x: stage.width() / 2,
y: stage.height() / 2,
});
layer.add(group);
// Add initial circles
const addCircles = (count) => {
const radius = 300;
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const distance = Math.random() * radius;
const x = Math.cos(angle) * distance;
const y = Math.sin(angle) * distance;
const circle = new Konva.Circle({
x,
y,
radius: 5 + Math.random() * 10,
fill: Konva.Util.getRandomColor(),
shadowColor: 'black',
shadowBlur: 10,
shadowOpacity: 0.5,
shadowOffset: { x: 2, y: 2 },
listening: false,
});
group.add(circle);
}
};
// Add initial circles
addCircles(5000);
// Add FPS counter
const fpsText = new Konva.Text({
x: 10,
y: 10,
text: 'FPS: 0',
fontSize: 16,
fill: 'white',
shadowColor: 'black',
shadowBlur: 5,
shadowOffset: { x: 1, y: 1 }
});
layer.add(fpsText);
// Add circle count text
const countText = new Konva.Text({
x: 10,
y: 40,
text: 'Circles: 1000',
fontSize: 16,
fill: 'white',
shadowColor: 'black',
shadowBlur: 5,
shadowOffset: { x: 1, y: 1 }
});
layer.add(countText);
// Create animation
const anim = new Konva.Animation((frame) => {
group.rotation(frame.time * 0.05);
// Update FPS counter
fpsText.text('FPS: ' + frame.frameRate.toFixed(1));
}, layer);
// Add click handler to add more circles
stage.on('click', () => {
addCircles(1000);
countText.text('Circles: ' + group.children.length);
});
// Add DOM checkbox
const container = stage.container();
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = 'cache-toggle';
checkbox.style.position = 'absolute';
checkbox.style.top = '70px';
checkbox.style.left = '10px';
checkbox.style.zIndex = '100';
container.appendChild(checkbox);
const label = document.createElement('label');
label.htmlFor = 'cache-toggle';
label.textContent = 'Enable Caching';
label.style.position = 'absolute';
label.style.top = '70px';
label.style.left = '30px';
label.style.color = 'white';
label.style.textShadow = '0 0 5px black';
label.style.zIndex = '100';
container.appendChild(label);
// Toggle caching
checkbox.addEventListener('change', () => {
if (checkbox.checked) {
group.cache();
} else {
group.clearCache();
}
});
anim.start();
```
```js
import { Stage, Layer, Circle, Text, Group } from 'react-konva';
import { useEffect, useRef, useState } from 'react';
import Konva from 'konva';
const App = () => {
const [circles, setCircles] = useState([]);
const [isCached, setIsCached] = useState(false);
const fpsTextRef = useRef(null);
const groupRef = useRef(null);
useEffect(() => {
// Add initial circles
addCircles(5000);
// Setup animation
const anim = new Konva.Animation((frame) => {
if (groupRef.current) {
groupRef.current.rotation(frame.time * 0.05);
}
// Update FPS counter
fpsTextRef.current.text('FPS: ' + frame.frameRate.toFixed(1));
}, fpsTextRef.current.getLayer());
anim.start();
return () => anim.stop();
}, []);
// Toggle caching
useEffect(() => {
if (groupRef.current) {
if (isCached) {
groupRef.current.cache();
} else {
groupRef.current.clearCache();
}
}
}, [isCached]);
// Add circles
const addCircles = (count) => {
const newCircles = [];
const radius = 300;
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const distance = Math.random() * radius;
const x = Math.cos(angle) * distance;
const y = Math.sin(angle) * distance;
newCircles.push({
id: circles.length + i,
x,
y,
radius: 5 + Math.random() * 10,
fill: Konva.Util.getRandomColor(),
shadowColor: 'black',
shadowBlur: 10,
shadowOpacity: 0.5,
shadowOffset: { x: 2, y: 2 },
listening: false
});
}
setCircles(prev => [...prev, ...newCircles]);
};
return (
<>
addCircles(1000)}
>
{circles.map((circle) => (
))}
setIsCached(e.target.checked)}
/>
Enable Caching
>
);
};
export default App;
```
```js
Enable Caching
```
---
# Why Your HTML5 Canvas Looks Blurry, and How to Fix It
> Fix a blurry HTML5 canvas on retina and high-DPI screens. How devicePixelRatio works, why Konva already handles it, and why the common double-scaling fix makes it worse.
Source: https://konvajs.org/docs/posts/Canvas_Blurry.html
A canvas has two sizes, and they are easy to confuse:
- **The display size** — how large the element is on the page, in CSS pixels.
- **The bitmap size** — how many real pixels the browser allocates to draw into.
When the bitmap is smaller than the display size, the browser stretches the
result and the drawing looks soft. On a retina screen the display size is
already two or three device pixels wide for every CSS pixel, so a canvas that
allocates one bitmap pixel per CSS pixel is stretched by default.
## Konva already handles this
This is the part most advice gets wrong, so it is worth stating plainly.
`Konva.pixelRatio` defaults to `window.devicePixelRatio`, and every canvas Konva
creates allocates its bitmap at that ratio. **You give Konva sizes in CSS
pixels and it sizes the bitmap for you.**
```js
// This is correct on a retina screen. Nothing else is needed.
const stage = new Konva.Stage({
container: 'container',
width: 600, // CSS pixels
height: 400, // CSS pixels
});
```
On a 2× screen that stage draws into a 1200 × 800 bitmap and displays at
600 × 400. Text and strokes are crisp.
## The fix that makes it worse
Search for "blurry canvas" and you will find this pattern, which is correct for
a raw `` and wrong for Konva:
```js
// DO NOT do this in Konva
const dpr = window.devicePixelRatio;
stage.width(container.clientWidth * dpr);
stage.height(container.clientHeight * dpr);
stage.scale({ x: dpr, y: dpr });
```
It multiplies by the device pixel ratio twice: once here, and once inside Konva.
On a 2× screen the stage ends up **twice as large as its container** and renders
into a bitmap four times the area it needs. The drawing is sharp, so the mistake
is easy to miss — what you notice is that the scene is too big and the memory
use is high.
The advice is not wrong in general. It is what you would write against a bare
canvas context, where nothing sets the bitmap size for you. Konva is not a bare
context.
## When it really is blurry
If a Konva scene still looks soft, the cause is usually one of these.
### The container is scaled with CSS
A `transform: scale()` on the container, or a `width` in percent that does not
match the stage size, stretches the finished bitmap. Konva cannot see that.
Size the stage to the container instead — see
[Responsive Canvas](/docs/posts/responsive-canvas.html).
### pixelRatio was turned off
`Konva.pixelRatio = 1` is a real performance tip, and it costs sharpness on
retina screens. It is worth it for heavy scenes and not worth it for text.
If someone set it globally, that is your answer.
```js
Konva.pixelRatio = 1; // faster, and soft on retina
```
### A cached node was cached at the wrong ratio
`node.cache()` renders the node into its own bitmap. That bitmap is fixed at the
moment you cache it, so a node cached before a zoom is upscaled afterwards.
Cache at the ratio you will display at, or re-cache after the scale changes.
```js
node.cache({ pixelRatio: 2 });
```
### An exported image is blurry
Export is a separate setting. `toDataURL()` defaults to `pixelRatio: 1`
regardless of the screen, so an export looks softer than the canvas it came
from. Ask for more:
```js
stage.toDataURL({ pixelRatio: 2 });
```
There is more on this in
[High quality export](/docs/data_and_serialization/High-Quality-Export.html).
## Thin lines look soft, not blurry
A one-pixel stroke drawn at a whole coordinate straddles two pixels, because the
line is centred on the path. Half of it lands in each, and the browser blends
both. This is a different problem from device pixel ratio and it happens at any
zoom level.
Offset by half a pixel so the stroke fills one row:
```js
const line = new Konva.Line({
points: [10, 20.5, 200, 20.5], // .5 puts the 1px stroke inside one pixel row
stroke: 'black',
strokeWidth: 1,
});
```
The demo below draws the same horizontal line twice. The top one sits on a whole
coordinate and looks grey; the bottom one is offset by half a pixel and looks
black.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: 400,
height: 140,
});
const layer = new Konva.Layer();
stage.add(layer);
// On a whole coordinate: the 1px stroke straddles two pixel rows.
layer.add(
new Konva.Line({
points: [20, 40, 380, 40],
stroke: 'black',
strokeWidth: 1,
})
);
layer.add(
new Konva.Text({ x: 20, y: 48, text: 'y = 40 — blended across two rows', fontSize: 13 })
);
// Offset by half a pixel: the stroke fills a single row.
layer.add(
new Konva.Line({
points: [20, 100.5, 380, 100.5],
stroke: 'black',
strokeWidth: 1,
})
);
layer.add(
new Konva.Text({ x: 20, y: 108, text: 'y = 100.5 — inside one row', fontSize: 13 })
);
```
Konva can do this for you on shapes with a stroke. `strokeScaleEnabled` and
`perfectDrawEnabled` control related behaviour, and for crisp axis-aligned
strokes the half-pixel offset is the reliable answer.
## Reading the actual numbers
When you are not sure which size is wrong, print both:
```js
const canvas = stage.container().querySelector('canvas');
console.log('CSS size ', canvas.clientWidth, canvas.clientHeight);
console.log('bitmap size', canvas.width, canvas.height);
console.log('ratio ', canvas.width / canvas.clientWidth);
console.log('devicePixelRatio', window.devicePixelRatio);
```
If the ratio matches `devicePixelRatio`, the canvas is correct and the softness
is coming from CSS, from a cache, or from an export. If the ratio is `1` on a
retina screen, `pixelRatio` was turned off somewhere.
---
# What is the difference between position and offset in Konva
> Understand the difference between position (x, y) and offset (offsetX, offsetY) in Konva and how they affect shape origin and rotation.
Source: https://konvajs.org/docs/posts/Position_vs_Offset.html
There are several properties in Konva that looks similar and may lead some confusion but have a different effect and purpose.
In the post I will explain the difference between position (x and y coordinates) and offset (offsetX and offsetY).
So x and y properties define position of Node on canvas. If you set `draggable = true` property and start dragging, Konva will change x and y properties of the node. That logic will be applied for all nodes (even Konva.Line).
Position of a rectangle shape defines its top-left point. Position of circle defines its center.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
var layer = new Konva.Layer();
var rect = new Konva.Rect({
x: 20,
y: 20,
width: 100,
height: 100,
fill: 'yellow',
stroke: 'black',
strokeWidth: 4,
draggable: true,
});
// add the shape to the layer
layer.add(rect);
var circle = new Konva.Circle({
x: 150,
y: 120,
radius: 50,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
draggable: true,
});
// add the shape to the layer
layer.add(circle);
var text = new Konva.Text();
layer.add(text);
// add the layer to the stage
stage.add(layer);
function updateText(e) {
text.text('Position: x = ' + e.target.x() + ' y = ' + e.target.y());
}
rect.on('dragmove', updateText);
circle.on('dragmove', updateText);
```
## Why do we need an `offset` property?
When you are changing the offset property it may looks like you are changing position of the node. But actually not. You are changing ORIGIN of the shape.
What is it origin? You may think of it as "point from where we start drawing of a shape" or "center of the shape" or "the point around which we rotating a shape".
Just a small note, long time ago offset property was called "center" in Konva codebase (when it was KineticJS project). But later it was refactored to "offset".
Take a look into this demo. All rectangles here have the same `y` position, but a different `offset` property.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const centerY = stage.height() / 2;
// rectangle with no offset
const rect1 = new Konva.Rect({
x: 50,
y: centerY,
width: 100,
height: 100,
fill: 'red',
stroke: 'black',
strokeWidth: 2,
});
layer.add(rect1);
// rectangle with offset at center
const rect2 = new Konva.Rect({
x: 200,
y: centerY,
width: 100,
height: 100,
fill: 'green',
stroke: 'black',
strokeWidth: 2,
offsetX: 50,
offsetY: 50,
rotation: 45
});
layer.add(rect2);
// rectangle with custom offset
const rect3 = new Konva.Rect({
x: 350,
y: centerY,
width: 100,
height: 100,
fill: 'blue',
stroke: 'black',
strokeWidth: 2,
offsetX: 100,
offsetY: 0,
rotation: 45
});
layer.add(rect3);
```
And you should understand that Konva has two main methods to define origin of the shape.
So "circle-like" shapes have origin at actual center of the shape (Circle, Ellipse, Wedge, Star, Ring ,etc).
When you set `{x, y}` of a circle you are defining "where will be the center of the circle".
And "rectangle-like" shapes has origin at TOP LEFT (Rectangle, Sprite, Text, Image, etc)
When you set `{x, y}` of a rectangle you are defining "where will be the top-left point of the rect".
So a shape will be rotated around its origin point (around its "center"). So if you set rotation 45 deg of a star it will be rotated around its actual center.
But if you set rotation 45 deg of rectangle, it will be rotated around top-left. But in some cases it is not convenient. Sometimes you may want to rotate the shape around its center.
In this case you can set `offset` property. By using it we will tell konva: "Hey, use this point as the new origin of the shape".
## How to set rotation point of a shape?
Now let's think you are placed a 100x100 rectangle in `x = 0, y = 0`, and now you want to rotate it around its center.
If you are not using offset you have to recalculate position of it't top left edge (recall you trigonometry lessons from the school).
You can do this by using something like this:
```javascript
const rotatePoint = ({ x, y }, rad) => {
const rcos = Math.cos(rad);
const rsin = Math.sin(rad);
return { x: x * rcos - y * rsin, y: y * rcos + x * rsin };
};
// will work for shapes with top-left origin, like rectangle
function rotateAroundCenter(node, rotation) {
//current rotation origin (0, 0) relative to desired origin - center (node.width()/2, node.height()/2)
const topLeft = { x: -node.width() / 2, y: -node.height() / 2 };
const current = rotatePoint(topLeft, Konva.getAngle(node.rotation()));
const rotated = rotatePoint(topLeft, Konva.getAngle(rotation));
const dx = rotated.x - current.x,
dy = rotated.y - current.y;
node.rotation(rotation);
node.x(node.x() + dx);
node.y(node.y() + dy);
}
// then use it
rotateAroundCenter(rect, 180);
```
Or you can set `offsetX = width / 2` and `offsetY = height / 2`. But the rectangle will be moved on the canvas (since you change its origin). So you will need to adjust the position.
Still have a question? Ask in comments.
---
# Resolving "Tainted canvases may not be exported" with Konva
> Fix the 'Tainted canvases may not be exported' CORS error when exporting or filtering cross-origin images in Konva.
Source: https://konvajs.org/docs/posts/Tainted_Canvas.html
When you are trying to export a canvas you may have an error like:
> Unable to get data URL. Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.
> Unable to get image data from canvas because the canvas has been tainted by cross-origin data.
Or when you apply filters you can have this error:
> Unable to apply filter. Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data.
> Unable to apply filter. The operation is insecure.
## Why do we have that insecure error?
That is a [CORS error](https://developer.mozilla.org/en-US/docs/Web/HTML/How_to/CORS_enabled_image). For security reasons, a browser can mark a canvas as tainted when you load images from another domain. In that case the browser blocks canvas exporting into `dataURL` or `imageData` (that is what we are doing on export or when filters are used).
## How to fix CORS issue?
First you may try to set `crossOrigin = Anonymous` attribute of the loading image. This approach will work only if requested domain has an `Access-Control-Allow-Origin` headers that allow shared requests.
```js
import Konva from 'konva';
// Method 1: native image loading
const imageObj = new Image();
imageObj.onload = () => {
const image = new Konva.Image({
x: 50,
y: 50,
image: imageObj
});
layer.add(image);
};
imageObj.crossOrigin = 'Anonymous';
imageObj.src = url;
// Method 2: using Konva helper method
// crossOrigin is set automatically to Anonymous
Konva.Image.fromURL(url, (image) => {
image.setAttrs({
x: 50,
y: 50
});
layer.add(image);
});
```
```js
import { Stage, Layer, Image } from 'react-konva';
import useImage from 'use-image';
const MyImage = ({ url }) => {
// useImage hook handles crossOrigin automatically
const [image] = useImage(url, 'Anonymous');
return (
);
}
const App = () => {
return (
);
};
```
```js
```
### What if it doesn't work?
**It may still not work for all cases. If it doesn't work, then you have to configure your server in a different way (it is out of Konva scope) or you can try to store images somewhere else where CORS requests are supported.**
---
# Canvas Drag and Drop with JavaScript and Konva
> Implement canvas drag and drop with pointer events, hit detection, and Konva draggable nodes in JavaScript or React.
Source: https://konvajs.org/docs/posts/canvas-drag-and-drop.html
The Canvas API does not provide drag and drop for drawn shapes. A raw Canvas
application must implement four parts:
1. Store each shape and its position.
2. Detect the shape under the pointer.
3. Convert browser coordinates to canvas coordinates.
4. Update the shape and redraw the scene during a drag.
Pointer events support a mouse, pen, and touch input through one event model.
Call `setPointerCapture()` after `pointerdown` in a raw Canvas implementation.
The capture keeps move events active outside the canvas.
## Drag a Konva node
Konva implements hit detection and pointer tracking. Set `draggable` on a node.
Then store its final position in application state.
```jsx
import { useState } from 'react';
import { Stage, Layer, Rect, Text } from 'react-konva';
const App = () => {
const [position, setPosition] = useState({ x: 80, y: 100 });
const [dragging, setDragging] = useState(false);
return (
setDragging(true)}
onDragMove={(event) => {
setPosition({ x: event.target.x(), y: event.target.y() });
}}
onDragEnd={(event) => {
setDragging(false);
setPosition({ x: event.target.x(), y: event.target.y() });
}}
/>
);
};
export default App;
```
Konva changes the node position during a drag. The example also writes that
position to React state. This state keeps the application model synchronized.
For large scenes, do not update unrelated React state on each `dragmove` event.
Update the Konva node during the drag. Save the final application state on
`dragend` when other components do not need live coordinates.
Canvas shapes are not keyboard controls. Provide an HTML control for each
essential drag action. The control can move the same shape in fixed steps.
See the [drag and drop guide](/docs/drag_and_drop/Drag_and_Drop.html) for Vanilla,
React, Vue, Svelte, and Angular examples.
---
# How to Export an HTML5 Canvas as an Image
> Export an HTML5 canvas as PNG or JPEG with Konva, control output resolution, and avoid cross-origin image errors.
Source: https://konvajs.org/docs/posts/canvas-export-image.html
The browser can encode a canvas as a data URL or a `Blob`. Konva provides
`toDataURL()` and `toBlob()` on stages and nodes.
Use `toBlob()` for large exports when the browser supports it. A `Blob` does not
create the large encoded string that a data URL creates.
## Export a Konva stage from React
This example exports the stage as a PNG. The `pixelRatio` value doubles each
output dimension.
```jsx
import { useRef } from 'react';
import { Stage, Layer, Rect, Circle, Text } from 'react-konva';
const App = () => {
const stageRef = useRef(null);
const downloadImage = () => {
const dataUrl = stageRef.current.toDataURL({ pixelRatio: 2 });
const link = document.createElement('a');
link.download = 'konva-scene.png';
link.href = dataUrl;
document.body.appendChild(link);
link.click();
link.remove();
};
return (
<>
Download PNG
>
);
};
export default App;
```
The export includes only canvas content. HTML controls above the stage are not
part of the image.
## Set the file type and area
Pass `mimeType: 'image/jpeg'` for JPEG output. JPEG has no transparency, so add
a background shape before the other scene content.
Pass `x`, `y`, `width`, and `height` to export part of a stage. Pass
`pixelRatio` to increase the resolution without changing scene coordinates.
High pixel ratios use more memory. A 2× ratio creates four times as many output
pixels. Use the smallest ratio that meets the output requirement.
## Prevent a tainted canvas
The browser blocks export after Canvas draws an image without permitted
cross-origin access. The image server must return a suitable
`Access-Control-Allow-Origin` header. Load the image with `crossOrigin` set
before its `src` value.
Read the [tainted canvas guide](/docs/posts/Tainted_Canvas.html) for complete
image-loading examples. Read [high-quality export](/docs/data_and_serialization/High-Quality-Export.html)
for more resolution details.
---
# HTML5 Canvas Performance with Konva
> Improve HTML5 canvas performance with fewer redraws, separate Konva layers, disabled hit detection, and measured optimizations.
Source: https://konvajs.org/docs/posts/canvas-performance.html
Canvas performance depends on scene complexity, redraw frequency, pixel count,
and hit detection. Optimize the measured limit, not the node count alone.
Use browser performance tools on a representative device. Record frame time,
memory use, and interaction delay for the real scene.
## High-value Konva changes
- Put static and changing content on separate layers.
- Set `listening(false)` on nodes or layers that do not receive events.
- Hide nodes that are outside the visible area when the scene is very large.
- Cache a complex, stable node after measurement shows a rendering benefit.
- Reduce shadows, filters, and large transparent areas when they dominate frame time.
- Draw only after a data change. Konva batches layer draws automatically.
Do not cache every node. Each cache uses memory and creates extra work when its
content changes.
## Separate the interactive layer
This example puts 2,000 noninteractive circles on one layer. The layer skips
the hit canvas because `listening` is false. A second layer contains one
draggable shape.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: 420,
});
const backgroundLayer = new Konva.Layer({ listening: false });
const interactionLayer = new Konva.Layer();
stage.add(backgroundLayer, interactionLayer);
for (let index = 0; index < 2000; index += 1) {
backgroundLayer.add(
new Konva.Circle({
x: Math.random() * stage.width(),
y: Math.random() * stage.height(),
radius: 2 + Math.random() * 4,
fill: Konva.Util.getRandomColor(),
opacity: 0.55,
perfectDrawEnabled: false,
})
);
}
const handle = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 36,
fill: '#ff922b',
stroke: '#7c2d12',
strokeWidth: 3,
draggable: true,
});
const label = new Konva.Text({
x: 16,
y: 16,
text: 'The orange circle stays interactive',
fontSize: 18,
fill: '#111827',
});
interactionLayer.add(handle, label);
```
Layer separation has a cost because each layer creates scene and hit canvases.
Use a small number of layers with clear update patterns.
Read [all Konva performance tips](/docs/performance/All_Performance_Tips.html)
for animation, caching, pixel ratio, and shape-specific guidance.
---
# Canvas Undo and Redo with React and Konva
> Add undo and redo to a canvas editor with immutable application state, a history index, React, and Konva.
Source: https://konvajs.org/docs/posts/canvas-undo-redo.html
Canvas pixels do not contain the application history. Store history in the data
model that produces the canvas scene.
For small editors, store an immutable snapshot after each complete user action.
Keep a current history index. Remove future snapshots after a new edit that
follows an undo.
Do not store Konva node instances in history. Store plain application data such
as positions, colors, text, and stable identifiers.
## Snapshot history in React
Drag the rectangle. Each completed drag adds one snapshot. The Undo and Redo
buttons change the current history index.
```jsx
import { useRef, useState } from 'react';
import { Stage, Layer, Rect, Text } from 'react-konva';
const initialRectangle = {
id: 'rectangle-1',
x: 80,
y: 100,
width: 170,
height: 110,
fill: '#4dabf7',
};
const App = () => {
const history = useRef([initialRectangle]);
const historyIndex = useRef(0);
const [rectangle, setRectangle] = useState(initialRectangle);
const commit = (nextRectangle) => {
const previousSnapshots = history.current.slice(0, historyIndex.current + 1);
history.current = [...previousSnapshots, nextRectangle];
historyIndex.current = history.current.length - 1;
setRectangle(nextRectangle);
};
const undo = () => {
if (historyIndex.current === 0) {
return;
}
historyIndex.current -= 1;
setRectangle(history.current[historyIndex.current]);
};
const redo = () => {
if (historyIndex.current === history.current.length - 1) {
return;
}
historyIndex.current += 1;
setRectangle(history.current[historyIndex.current]);
};
return (
<>
Undo
Redo
{
commit({
...rectangle,
x: event.target.x(),
y: event.target.y(),
});
}}
/>
>
);
};
export default App;
```
Commit one history entry for one user action. Do not add an entry for each
pointer move. This rule keeps undo behavior predictable and limits memory use.
Large documents can make full snapshots expensive. In that case, store commands
or patches with enough data to apply and reverse each change. This model is more
complex, so use it only after measurement shows a snapshot limit.
Define a history limit for a long editor session. Remove the oldest snapshots
after the limit is reached. Keep saved document versions separate from the local
undo history.
See the focused [React undo and redo example](/docs/react/Undo-Redo.html) for a
smaller implementation.
---
# Canvas vs SVG for Interactive Graphics
> Compare Canvas and SVG for interactive web graphics, and learn when the Konva canvas object model is a good fit.
Source: https://konvajs.org/docs/posts/canvas-vs-svg.html
Canvas and SVG use different rendering models. This difference affects
performance, interaction code, accessibility, and export behavior.
SVG keeps each shape as a DOM element. Browser tools and CSS can inspect each
element. This model works well for diagrams with a small number of shapes.
Canvas draws pixels into one bitmap. The browser does not keep the drawn shapes.
Canvas often works better for scenes with many shapes or frequent redraws.
| Requirement | Usual choice |
| --- | --- |
| Accessible DOM elements for each shape | SVG |
| CSS selectors and DOM events on each shape | SVG |
| Thousands of frequently changed shapes | Canvas |
| Pixel editing or image filters | Canvas |
| A retained shape model on Canvas | Konva |
These choices are not absolute. Measure the real scene on the devices that your
users have.
## The raw Canvas tradeoff
The Canvas API does not remember this rectangle after `fillRect()` returns:
```js
const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');
context.fillStyle = '#4dabf7';
context.fillRect(40, 40, 120, 80);
```
Your application must store the rectangle data. It must also redraw the scene
and detect pointer hits.
## Add an object model with Konva
Konva stores nodes in a scene graph. Each node has properties, events, and
methods. Konva also manages hit detection and redraws.
Drag the rectangle in this example:
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: 360,
});
const layer = new Konva.Layer();
stage.add(layer);
const rectangle = new Konva.Rect({
x: 40,
y: 60,
width: 160,
height: 100,
fill: '#4dabf7',
stroke: '#1c7ed6',
strokeWidth: 3,
cornerRadius: 8,
draggable: true,
});
const label = new Konva.Text({
x: 40,
y: 20,
text: 'Drag the rectangle',
fontSize: 18,
fill: '#212529',
});
rectangle.on('dragmove', () => {
label.text(`Position: ${Math.round(rectangle.x())}, ${Math.round(rectangle.y())}`);
});
layer.add(label, rectangle);
```
Konva does not create accessible DOM elements for canvas shapes. Keep essential
controls and content in HTML. Add keyboard controls and accessible labels to
those HTML elements.
Use SVG when each graphic element must participate in the DOM. Use Konva when
the scene needs Canvas performance and an interactive object model.
---
# How to Make a Responsive Canvas with Konva
> Make a responsive HTML5 canvas with Konva, ResizeObserver, and a fixed virtual scene size that preserves its aspect ratio.
Source: https://konvajs.org/docs/posts/responsive-canvas.html
A canvas has a display size and a drawing size. A CSS resize changes only the
display size. This change can stretch the rendered pixels.
For a responsive Konva scene, define a fixed virtual size. Then scale the stage
from the available container width.
The scale formula is:
```text
scale = containerWidth / virtualWidth
```
The displayed height is `virtualHeight * scale`. All node coordinates stay in
the virtual coordinate system.
## Responsive React example
This example uses `ResizeObserver`. The observer responds when the container
changes size, not only when the browser window changes size.
```jsx
import { useEffect, useRef, useState } from 'react';
import { Stage, Layer, Rect, Circle, Text } from 'react-konva';
const VIRTUAL_WIDTH = 900;
const VIRTUAL_HEIGHT = 420;
const App = () => {
const containerRef = useRef(null);
const [containerWidth, setContainerWidth] = useState(VIRTUAL_WIDTH);
const [circlePosition, setCirclePosition] = useState({
x: VIRTUAL_WIDTH / 2,
y: VIRTUAL_HEIGHT / 2,
});
const [rectanglePosition, setRectanglePosition] = useState({
x: VIRTUAL_WIDTH - 190,
y: VIRTUAL_HEIGHT - 130,
});
useEffect(() => {
const container = containerRef.current;
const observer = new ResizeObserver(([entry]) => {
setContainerWidth(entry.contentRect.width);
});
observer.observe(container);
return () => observer.disconnect();
}, []);
const scale = containerWidth / VIRTUAL_WIDTH;
return (
{
setCirclePosition(event.target.position());
}}
/>
{
setRectanglePosition(event.target.position());
}}
/>
);
};
export default App;
```
Do not store scaled coordinates in application state. Store virtual coordinates.
Konva applies the stage scale during rendering and pointer conversion.
For high pixel density, Konva uses the device pixel ratio by default. Do not
multiply the stage size by `devicePixelRatio` unless the application manages
the backing store itself.
For more stage-resize details, see the [responsive sandbox demo](/docs/sandbox/Responsive_Canvas.html).
---
# How to access Konva nodes from react-konva?
> Learn how to access Konva nodes from react-konva using refs and event callbacks with code examples.
Source: https://konvajs.org/docs/react/Access_Konva_Nodes.html
In some cases you may need to use the `Konva` API directly. For example for exporting canvases or animations.
There are two ways to access Konva nodes/shapes from `react-konva`.
## Using the `refs` API.
You can use the [React refs API](https://react.dev/learn/manipulating-the-dom-with-refs) to access a Konva node.
```js
import React from 'react';
import { Stage, Layer, Circle } from 'react-konva';
const App = () => {
const shapeRef = React.useRef(null);
React.useEffect(() => {
// it will log `Konva.Circle` instance
console.log(shapeRef.current);
});
return (
);
};
export default App;
```
## Using an event object inside of the event callback
Another common way to access a Konva node is to just use an event object that you have as an argument in any event:
```js
import { Stage, Layer, Circle } from 'react-konva';
const App = () => {
const handleClick = (e) => {
// logs clicked Konva.Circle instance
console.log(e.target);
};
return (
);
};
export default App;
```
---
# How to export a canvas into an image from react-konva?
> Learn how to export canvas to an image in React with react-konva. Use stage.toDataURL() to save canvas as PNG or JPEG.
Source: https://konvajs.org/docs/react/Canvas_Export.html
## How to save a drawing from react-konva?
To export any `Konva` node into an image you can either use the [node.toDataURL()](/api/Konva.Node.html#toDataURL) or the [node.toImage()](https://konvajs.org/api/Konva.Node.html#toImage) API. Take a look into the [vanilla Konva image export demo](https://konvajs.org/docs/data_and_serialization/Stage_Data_URL.html).
You will need to use the [Refs API](/docs/react/Access_Konva_Nodes.html) to access a `Konva` node directly in order to call these methods.
```js
import React, { Fragment } from 'react';
import { Stage, Layer, Rect } from 'react-konva';
// function from https://stackoverflow.com/a/15832662/512042
function downloadURI(uri, name) {
var link = document.createElement('a');
link.download = name;
link.href = uri;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
const App = () => {
const width = window.innerWidth;
const height = window.innerHeight;
const stageRef = React.useRef(null);
const handleExport = () => {
const uri = stageRef.current.toDataURL();
console.log(uri);
// we also can save uri as file
downloadURI(uri, 'stage.png');
};
return (
Click here to export stage as image
);
};
export default App;
```
## Before you raise the pixel ratio
Browsers cap canvas width, height, and total area, so a high `pixelRatio` can
return a blank image rather than an error. See
[high quality export](/docs/data_and_serialization/High-Quality-Export.html) for
the limits and the ways around them.
---
# How to use portals in react-konva?
> Learn how to use canvas portals in react-konva to move nodes between layers for drag overlays and z-ordering.
Source: https://konvajs.org/docs/react/Canvas_Portal.html
## How does react-konva control the zIndex?
`react-konva` strictly follows the order of elements in the way that you define them in your render. For more info take a look into the [zIndex demo](/docs/react/zIndex.html).
## Is it possible to move a node into another container with `react-konva`?
Currently `react-konva` doesn't support the `React.createPortal` API.
But we can use ` ` component from [react-konva-utils package](https://github.com/konvajs/react-konva-utils)
Such a portal can be useful when you want to temporarily move a node into another container. The common use cases are:
1. Move a dragging shape into another layer for better performance
2. Show an element on top of other elements, but still keep it deep down in the components tree
**Instructions: try to drag a rectangle. You will see that it is visible on top, but in render it is still the first element.**
```js
import React from 'react';
import { Stage, Layer, Rect, Text, Circle, Line } from 'react-konva';
import { Portal } from 'react-konva-utils';
const App = () => {
const [isDragging, setDragging] = React.useState(false);
const [rectanglePosition, setRectanglePosition] = React.useState({
x: 20,
y: 50,
});
const [linePosition, setLinePosition] = React.useState({ x: 20, y: 200 });
return (
{
setDragging(true);
}}
onDragEnd={(event) => {
setRectanglePosition(event.target.position());
setDragging(false);
}}
/>
{
setLinePosition(event.target.position());
}}
/>
);
};
export default App;
```
---
# How to build complex canvas animations with React and react-spring?
> Learn how to create complex canvas animations in React with react-konva and react-spring. Animate colors, size, position, and shadows smoothly.
Source: https://konvajs.org/docs/react/Complex_Animations.html
For complex animations and high performance updates you can use the [react-spring library](https://github.com/pmndrs/react-spring).
```js
import React, { useState } from 'react';
import { Stage, Layer, Text } from 'react-konva';
import { Spring, animated } from '@react-spring/konva';
const ColoredRect = () => {
const [flag, setFlag] = useState(false);
const handleClick = () => setFlag((prev) => !prev);
return (
{(props) => (
)}
);
};
const App = () => {
return (
);
};
export default App;
```
[Open the interactive demo](https://codesandbox.io/embed/github/konvajs/site/tree/master/react-demos/complex_animations?hidenavigation=1&view=split&fontsize=10)
---
# How to draw custom shapes with React?
> Learn how to draw custom shapes with react-konva using the Shape component and HTML5 Canvas context.
Source: https://konvajs.org/docs/react/Custom_Shape.html
To create a custom shape with `react-konva`, we should use the `Shape` component.
When creating a custom shape, we need to define a drawing function that is passed to a Konva.Canvas renderer.
We can use the renderer to access the HTML5 Canvas context, and to use special methods like `context.fillStrokeShape(shape)` which automatically handle filling, stroking, and applying shadows.
```js
import React from 'react';
import { Stage, Layer, Shape } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
---
# Render DOM elements inside a canvas stage
> Learn how to render DOM elements like inputs and divs inside a Konva canvas stage using react-konva-utils Html component.
Source: https://konvajs.org/docs/react/DOM_Portal.html
## How to put DOM elements (like inputs or divs) inside of a Konva stage?
If you want to have some DOM nodes as part of your canvas tree you can use ` ` component from [react-konva-utils package](https://github.com/konvajs/react-konva-utils).
Remember that DOM nodes are not direct children of Konva containers. ` ` is just a wrapper to work with a Portal-like API. HTML content will be not visible if you try to export canvas as image.
```js
import React from 'react';
import { Stage, Layer, Rect } from 'react-konva';
import { Html } from 'react-konva-utils';
const App = () => {
return (
);
};
export default App;
```
---
# Drag and drop canvas shapes
> Learn how to implement drag and drop on canvas in React with react-konva. Make shapes draggable with the draggable prop and handle drag events.
Source: https://konvajs.org/docs/react/Drag_And_Drop.html
To enable drag&drop for any node on the canvas you just need to pass the `draggable` property into the component.
When you drag&drop a shape it is recommended to save its position into your app store. You can use the `onDragMove` and `onDragEnd` events for that purpose.
```js
import React from 'react';
import { Stage, Layer, Text } from 'react-konva';
const App = () => {
const [isDragging, setIsDragging] = React.useState(false);
const [position, setPosition] = React.useState({
x: 50,
y: 50,
});
return (
{
setIsDragging(true);
}}
onDragEnd={(e) => {
setIsDragging(false);
setPosition({
x: e.target.x(),
y: e.target.y(),
});
}}
/>
);
};
export default App;
```
---
# How to drop image elements into a canvas with React?
> Learn how to drag and drop images from the page into a Konva canvas using HTML5 drag and drop with React.
Source: https://konvajs.org/docs/react/Drop_Image.html
You can use HTML drag-and-drop events to add images or other elements to a canvas.
```js
import React from 'react';
import { Stage, Layer, Image } from 'react-konva';
import useImage from 'use-image';
const URLImage = ({ image, onChange }) => {
const [img] = useImage(image.src);
return (
{
onChange({ ...image, ...event.target.position() });
}}
/>
);
};
const App = () => {
const dragUrl = React.useRef();
const stageRef = React.useRef();
const nextImageId = React.useRef(0);
const [images, setImages] = React.useState([]);
return (
Try to drag and drop the image into the stage:
{
dragUrl.current = e.target.src;
}}
/>
{
e.preventDefault();
// register event position
stageRef.current.setPointersPositions(e);
// add image
setImages((currentImages) =>
currentImages.concat([
{
id: nextImageId.current++,
...stageRef.current.getPointerPosition(),
src: dragUrl.current,
},
])
);
}}
onDragOver={(e) => e.preventDefault()}
>
{images.map((image) => {
return (
{
setImages((currentImages) =>
currentImages.map((currentImage) =>
currentImage.id === newAttributes.id
? newAttributes
: currentImage
)
);
}}
/>
);
})}
);
};
export default App;
```
---
# How to listen to an event on a canvas shape with React and Konva?
> Learn how to handle canvas events in React with react-konva. Use onClick, onMouseEnter, onDragEnd, and other event props on Konva components.
Source: https://konvajs.org/docs/react/Events.html
With `react-konva` you can attach any events that `Konva` supports to canvas nodes.
To do that you can use the `onEventName` scheme, like `onMouseDown` for `mousedown`, `onDragEnd` for `dragend`, etc.
For the full list of events take a look into the [on() method documentation](/api/Konva.Node.html).
In this demo you can see how we are using `dragstart` and `dragend` events to create an interactive star field:
```js
import React from 'react';
import { Stage, Layer, Star, Text } from 'react-konva';
function generateShapes() {
return [...Array(10)].map((_, i) => ({
id: i.toString(),
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
rotation: Math.random() * 180,
isDragging: false,
}));
}
const App = () => {
const [stars, setStars] = React.useState(generateShapes());
const handleDragStart = (e) => {
const id = e.target.id();
setStars((currentStars) =>
currentStars.map((star) => {
return {
...star,
isDragging: star.id === id,
};
})
);
};
const handleDragEnd = (e) => {
const id = e.target.id();
const position = e.target.position();
setStars((currentStars) =>
currentStars.map((star) => {
return {
...star,
isDragging: false,
...(star.id === id ? position : {}),
};
})
);
};
return (
{stars.map((star) => (
))}
);
};
export default App;
```
---
# How to apply canvas filters with React and Konva?
> Learn how to apply canvas filters like blur and noise to shapes and images using react-konva with caching.
Source: https://konvajs.org/docs/react/Filters.html
To apply filters to a Konva node, you need to:
1. Cache the node using `node.cache()`
2. Apply filters using the `filters` prop
3. Re-cache the node when its properties change
```js
import React from 'react';
import Konva from 'konva';
import { Stage, Layer, Rect, Image } from 'react-konva';
import useImage from 'use-image';
// Example of functional component with image filter
const FilterImage = () => {
const [image] = useImage('https://konvajs.org/assets/lion.png', 'anonymous');
const imageRef = React.useRef();
// when image is loaded we need to cache the shape
React.useEffect(() => {
if (image) {
// you many need to reapply cache on some props changes like shadow, stroke, etc.
imageRef.current.cache();
}
}, [image]);
return (
);
};
// Example of class component with noise filter
// Try to click on rect to see color updates
const FilterRect = () => {
const [color, setColor] = React.useState('green');
const rectRef = React.useRef();
React.useEffect(() => {
if (rectRef.current) {
rectRef.current.cache();
}
}, []);
const handleClick = () => {
setColor(Konva.Util.getRandomColor());
// recache shape when we updated it
rectRef.current.cache();
};
return (
);
};
const App = () => {
return (
);
};
export default App;
```
---
# How to implement free drawing on canvas with React?
> Learn how to implement free drawing (whiteboard) on canvas in React with react-konva. Track mouse/touch events to draw lines freehand.
Source: https://konvajs.org/docs/react/Free_Drawing.html
This demo shows how to implement a free drawing app the "React way" with full vector representation.
Such an implementation should work well for many whiteboard apps. It allows you to simply add [undo/redo functions](/docs/react/Undo-Redo.html) and save the full state to the backend.
Note: It will get slower if you have too many lines in the state. So you will have to do some extra optimizations if you want to enable drawings of hundreds or thousands of lines.
The demo shows how to:
1. Track drawing state using `React.useRef` for performance
2. Store lines as vector data in React state
3. Handle mouse/touch events for drawing
4. Implement both pen and eraser tools using `globalCompositeOperation`
5. Create smooth lines with rounded caps and tension
```js
import React from 'react';
import { Stage, Layer, Line, Text } from 'react-konva';
const App = () => {
const [tool, setTool] = React.useState('pen');
const [lines, setLines] = React.useState([]);
const isDrawing = React.useRef(false);
const handleMouseDown = (e) => {
isDrawing.current = true;
const pos = e.target.getStage().getPointerPosition();
setLines([...lines, { tool, points: [pos.x, pos.y] }]);
};
const handleMouseMove = (e) => {
// no drawing - skipping
if (!isDrawing.current) {
return;
}
const stage = e.target.getStage();
const point = stage.getPointerPosition();
let lastLine = lines[lines.length - 1];
// add point
lastLine.points = lastLine.points.concat([point.x, point.y]);
// replace last
lines.splice(lines.length - 1, 1, lastLine);
setLines(lines.concat());
};
const handleMouseUp = () => {
isDrawing.current = false;
};
return (
{
setTool(e.target.value);
}}
>
Pen
Eraser
{lines.map((line, i) => (
))}
);
};
export default App;
```
---
# How to draw images on canvas with React?
> Learn how to draw images on canvas in React with react-konva. Load and display images using the Image component with useImage hook.
Source: https://konvajs.org/docs/react/Images.html
To render image on canvas with react you can use the [use-image](https://github.com/konvajs/use-image) hook.
```js
import React from 'react';
import { Stage, Layer, Image } from 'react-konva';
import useImage from 'use-image';
const URLImage = ({ src, ...rest }) => {
const [image] = useImage(src, 'anonymous');
return ;
};
const App = () => {
return (
);
};
export default App;
```
---
# Drawing canvas shapes with React
> Learn how to draw shapes on canvas with React using react-konva. Use Rect, Circle, Line, Text, and other Konva components as React elements.
Source: https://konvajs.org/docs/react/Shapes.html
All `react-konva` components correspond to `Konva` components of the same name.
All the parameters available for `Konva` objects are valid props for
corresponding `react-konva` components, unless noted otherwise.
Core shapes are: `Rect`, `Circle`, `Ellipse`, `Line`, `Image`, `Text`, `TextPath`, `Star`,
`Label`, `SVG Path`, `RegularPolygon`. You can also create [custom shapes](/docs/react/Custom_Shape.html).
Here is a demo showing some of the basic shapes with various styling options:
```js
import React from 'react';
import { Stage, Layer, Rect, Text, Circle, Line } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
---
# How to animate canvas shapes with React and Konva?
> Learn how to animate canvas shapes in React with react-konva. Use React state, requestAnimationFrame, and Konva.Tween for smooth animations.
Source: https://konvajs.org/docs/react/Simple_Animations.html
Konva itself has two methods for animations: [Tween](/docs/tweens/Linear_Easing.html) and [Animation](/docs/animations/Rotation.html). You can apply both of them to nodes manually.
For simple use cases we recommend using the `node.to()` method. For more complex animations take a look at the [Complex react-konva animation demo](/docs/react/Complex_Animations.html).
The demo is using the [refs API](/docs/react/Access_Konva_Nodes.html) to access shape instances directly.
Instructions: Try to drag the rectangle to see it animate.
```js
import React, { useRef } from 'react';
import { Stage, Layer, Rect } from 'react-konva';
const MyRect = () => {
const rectRef = useRef(null);
const changeSize = () => {
// to() is a method of `Konva.Node` instances
rectRef.current.to({
scaleX: Math.random() + 0.8,
scaleY: Math.random() + 0.8,
duration: 0.2,
});
};
return (
);
};
const App = () => {
return (
);
};
export default App;
```
---
# How to test react-konva components
> Test react-konva components in a real browser instead of jsdom, where canvas is stubbed and tests pass while asserting nothing. Getting a stage handle, asserting on the scene graph, simulating drags, and checking for leaks.
Source: https://konvajs.org/docs/react/Testing.html
Testing canvas is not like testing DOM. Your shapes are not elements, so
Testing Library queries never find them, and the environment most React projects
test in cannot draw.
## Run canvas tests in a real browser
Use jsdom for your store and your logic. Use a real browser for anything that
touches a `Stage`.
This matters more than it sounds, because **jsdom does not fail loudly**. There
is no canvas implementation in it, so the usual fix is a mock that returns empty
values from every context call. Nothing throws. Your test goes green. What you
are actually asserting is:
| What you test | What jsdom gives you |
| --- | --- |
| `stage.getIntersection(pos)` | `null`, whatever is under the point |
| `text.width()` after measuring | a value from a font that was never loaded |
| `stage.toDataURL()` | a stub string, not an image |
| pixel readback | fully transparent |
Only the parts that are pure JavaScript — `find()`, attributes, the scene graph
— behave correctly. So a hit-testing test passes without testing hit testing.
That is the one failure a coding agent cannot catch for you: it writes the test,
the test is green, and the loop ends.
`react-konva` itself is tested this way. Its config is short:
```js
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import { playwright } from '@vitest/browser-playwright';
export default defineConfig({
test: {
browser: {
enabled: true,
provider: playwright(),
headless: process.env.HEADLESS !== 'false',
instances: [{ browser: 'chromium' }],
},
globals: true,
},
});
```
If you have existing jsdom tests, keep them. Split the run into two projects —
one jsdom for logic, one browser for canvas — rather than moving everything.
## Getting hold of the stage
A `ref` is the cleanest route:
```jsx
const stageRef = React.useRef(null);
render(… );
stageRef.current.find('#target');
```
When the component under test owns its own stage and gives you no ref, Konva
keeps a registry:
```js
import Konva from 'konva';
const stage = Konva.stages[Konva.stages.length - 1];
```
**`data-testid` on `` does not work.** The component renders a container
`div` and forwards only `id`, `accessKey`, `className`, `role`, `style`,
`tabIndex` and `title`. Anything else, including `data-*`, is dropped. Use `id`
if you want to query the container, and remember the container is a `div` — the
`` elements live inside it.
## Assert on the scene graph, not the DOM
Shapes are Konva nodes. Query them the way Konva does:
```js
const rect = stage.findOne('#card');
expect(rect.width()).toBe(120);
expect(stage.find('Circle')).toHaveLength(3);
expect(stage.find('.selected')).toHaveLength(1); // .name, not a CSS class
```
`find('Rect')` matches by node type, `find('.name')` by the `name` attribute,
and `findOne('#id')` by `id`. None of this needs the browser to have painted
anything, so these assertions are the fast, reliable core of a canvas test suite.
For anything about *appearance* — did this actually render, is it the right
colour — you need real pixels, which is the other reason for the browser:
```js
const ctx = layer.getContext();
const ratio = layer.getCanvas().getPixelRatio();
const { data } = ctx.getImageData(x * ratio, y * ratio, 1, 1);
expect([data[0], data[1], data[2]]).toEqual([255, 0, 0]);
```
Scale the coordinates by the layer's pixel ratio, or you sample the wrong pixel
on a retina screen. See [Blurry canvas](/docs/posts/Canvas_Blurry.html) for why.
## Simulating a drag
Konva does not listen for `mousemove` and `mouseup` on the shape, or even on the
stage container. It attaches them to `window` when a drag begins, so the drag
survives the pointer leaving the canvas. A synthetic drag has to follow that:
```js
const container = stage.container();
container.dispatchEvent(new MouseEvent('mousedown', { clientX: 20, clientY: 20, bubbles: true }));
window.dispatchEvent(new MouseEvent('mousemove', { clientX: 90, clientY: 60, bubbles: true }));
window.dispatchEvent(new MouseEvent('mouseup', { clientX: 90, clientY: 60, bubbles: true }));
```
Two things to know:
- **Dispatch `MouseEvent`, not only `PointerEvent`.** Konva's drag handling is
driven by the mouse family, so a pointer-only sequence can be ignored and the
drag becomes a silent no-op. Dispatching both is the safe option.
- **`Konva.dragDistance` defaults to 3 pixels.** A move of one or two pixels is
treated as a click and `dragstart` never fires. Move further than that, or set
the threshold in the test.
`react-konva` flushes React updates after each Konva event handler. You do not
need an extra microtask between events. Wait only if your own handler starts
asynchronous work.
## Clean up, and check nothing leaked
Every stage registers itself in `Konva.stages`. If a test unmounts and a stage
survives, later tests inherit it and start failing in confusing ways. Assert on
that rather than hoping:
```js
afterEach(async () => {
cleanup();
// Stage.destroy() is deferred a tick so a StrictMode remount can reuse it.
await new Promise((r) => setTimeout(r, 0));
const leaked = Konva.stages.length;
[...Konva.stages].forEach((s) => s.destroy());
expect(leaked).toBe(0);
});
```
## End-to-end tests
For a full application, drive a real browser with Playwright and skip synthetic
events entirely — real input reaches `window` naturally, so the trap above does
not apply:
```js
const start = await page.evaluate(() => {
const stage = window.Konva.stages[0];
return stage.findOne('#card').position();
});
await page.mouse.move(120, 120); // A point inside #card.
await page.mouse.down();
await page.mouse.move(260, 200);
await page.mouse.up();
const pos = await page.evaluate(() => {
const stage = window.Konva.stages[0];
return stage.findOne('#card').position();
});
expect(pos).toEqual({ x: start.x + 140, y: start.y + 80 });
```
Konva preserves the pointer offset inside the node. It checks `dragDistance`
against the total displacement from the start, so one move of more than three
pixels can start a drag.
## Plain Konva without React
Konva runs in Node with a backend module, which is a different setup from
everything above. See [Node.js](/docs/nodejs/nodejs-setup) — and note that Konva
prints the exact install and import lines if you forget them.
---
# How to resize and rotate canvas shapes with React and Konva?
> Learn how to resize and rotate canvas shapes in React with react-konva Transformer. Add interactive selection handles to shapes.
Source: https://konvajs.org/docs/react/Transformer.html
Currently there is no good, pure declarative "react-way" to use the Transformer tool.
But you can still use it with some small manual requests to the Konva nodes.
And it will work just fine.
The idea: you need to create a `Konva.Transformer` node, and attach it to the required node manually.
Instructions: Click on one of the rectangles to select it. Then you can:
- Drag it to move
- Use handles to resize
- Click outside to deselect
```js
import React from 'react';
import { Stage, Layer, Rect, Transformer } from 'react-konva';
const Rectangle = ({ shapeProps, isSelected, onSelect, onChange }) => {
const shapeRef = React.useRef();
const trRef = React.useRef();
React.useEffect(() => {
if (isSelected) {
// we need to attach transformer manually
trRef.current.nodes([shapeRef.current]);
}
}, [isSelected]);
return (
{
onChange({
...shapeProps,
x: e.target.x(),
y: e.target.y(),
});
}}
onTransformEnd={(e) => {
// transformer is changing scale of the node
// and NOT its width or height
// but in the store we have only width and height
// to match the data better we will reset scale on transform end
const node = shapeRef.current;
const scaleX = node.scaleX();
const scaleY = node.scaleY();
// we will reset it back
node.scaleX(1);
node.scaleY(1);
onChange({
...shapeProps,
x: node.x(),
y: node.y(),
// set minimal value
width: Math.max(5, node.width() * scaleX),
height: Math.max(5, node.height() * scaleY),
});
}}
/>
{isSelected && (
{
// limit resize
if (Math.abs(newBox.width) < 5 || Math.abs(newBox.height) < 5) {
return oldBox;
}
return newBox;
}}
/>
)}
);
};
const initialRectangles = [
{
x: 10,
y: 10,
width: 100,
height: 100,
fill: 'red',
id: 'rect1',
},
{
x: 150,
y: 150,
width: 100,
height: 100,
fill: 'green',
id: 'rect2',
},
];
const App = () => {
const [rectangles, setRectangles] = React.useState(initialRectangles);
const [selectedId, selectShape] = React.useState(null);
const checkDeselect = (e) => {
// deselect when clicked on empty area
const clickedOnEmpty = e.target === e.target.getStage();
if (clickedOnEmpty) {
selectShape(null);
}
};
return (
{rectangles.map((rect, i) => {
return (
{
selectShape(rect.id);
}}
onChange={(newAttrs) => {
const rects = rectangles.slice();
rects[i] = newAttrs;
setRectangles(rects);
}}
/>
);
})}
);
};
export default App;
```
## What Transformer does not do
`Transformer` draws the handles and applies the scale. Snapping to other
objects, alignment guides, a shared bounding box for a multi-selection, and
per-shape aspect rules are all yours to build — see
[objects snapping](/docs/sandbox/Objects_Snapping.html) for one approach.
A production editor also needs text editing, templates, and export around the
Transformer. If you would rather not build those, [Polotno](https://polotno.com/?utm_source=konvajs&utm_medium=docs&utm_content=react-transformer) is a commercial
design editor SDK built on Konva by the Konva maintainers that ships them.
---
# How to implement undo/redo on canvas with React?
> Learn how to implement undo and redo functionality for canvas interactions using React state history management.
Source: https://konvajs.org/docs/react/Undo-Redo.html
To implement undo/redo functionality with React you don't need to use Konva's serialization and deserialization methods.
You just need to save a history of all the state changes within your app. There are many ways to do this. It may be simpler to do that if you use immutable structures.
Instructions: Try to move the square by dragging it. Then use the "undo" and "redo" buttons to revert or replay your actions.
```js
import React, { Component } from 'react';
import { Stage, Layer, Rect, Text } from 'react-konva';
const App = () => {
const [position, setPosition] = React.useState({ x: 20, y: 20 });
// We use refs to keep history to avoid unnecessary re-renders
const history = React.useRef([{ x: 20, y: 20 }]);
const historyStep = React.useRef(0);
const handleUndo = () => {
if (historyStep.current === 0) {
return;
}
historyStep.current -= 1;
const previous = history.current[historyStep.current];
setPosition(previous);
};
const handleRedo = () => {
if (historyStep.current === history.current.length - 1) {
return;
}
historyStep.current += 1;
const next = history.current[historyStep.current];
setPosition(next);
};
const handleDragEnd = (e) => {
// Remove all states after current step
history.current = history.current.slice(0, historyStep.current + 1);
const pos = {
x: e.target.x(),
y: e.target.y(),
};
// Push the new state
history.current = history.current.concat([pos]);
historyStep.current += 1;
setPosition(pos);
};
return (
);
};
export default App;
```
## Where a hand-built history stops
The history above records one value per step. A production editor has to record
grouped operations, so that a multi-select drag undoes as a single step, plus
transforms and images that finish loading after the action. That state machine
usually grows larger than the drawing code, so plan the history around document
operations rather than around raw node state.
---
# How to change the zIndex of nodes with React?
> Learn how to control zIndex and reorder shapes in react-konva by managing component state instead of manual z-ordering.
Source: https://konvajs.org/docs/react/zIndex.html
## How to change the zIndex and reorder components in `react-konva`?
When you are working with `Konva` directly you have many methods to change the order of nodes like `node.zIndex(5)`, `node.moveToTop()`, etc. [Tutorial](/docs/groups_and_layers/Layering.html).
But it is not recommended to use these methods when you are working with the React framework.
`react-konva` is strictly follows the order of the nodes exactly as you described them in your component. So instead of changing the zIndex manually, you just need to update the state of the app correctly.
Don't use the `zIndex` for your canvas components.
If you want to temporarily move a node into another container, for example when you want to show an overlay, take a look into the [Canvas Portal demo](/docs/react/Canvas_Portal.html).
**Instructions: Try to drag a circle. See how it goes to the top. We are doing this by manipulating the state so that componet render returns the correct order.**
```js
import React, { Component } from 'react';
import Konva from 'konva';
import { Stage, Layer, Circle } from 'react-konva';
function generateItems() {
const items = [];
for (let i = 0; i < 10; i++) {
items.push({
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
id: 'node-' + i,
color: Konva.Util.getRandomColor(),
});
}
return items;
}
const App = () => {
const [items, setItems] = React.useState(generateItems());
const handleDragStart = (e) => {
const id = e.target.name();
const itemsCopy = items.slice();
const item = itemsCopy.find((i) => i.id === id);
const index = itemsCopy.indexOf(item);
// remove from the list:
itemsCopy.splice(index, 1);
// add to the top
itemsCopy.push(item);
setItems(itemsCopy);
};
const onDragEnd = (e) => {
const id = e.target.name();
const itemsCopy = items.slice();
const item = items.find((i) => i.id === id);
const index = items.indexOf(item);
// update item position
itemsCopy[index] = {
...item,
x: e.target.x(),
y: e.target.y(),
};
setItems(itemsCopy);
};
return (
{items.map((item) => (
))}
);
};
export default App;
```
---
# 10,000 Shapes with Tooltips Stress Test with Konva
> Stress test rendering 10,000 circles on canvas with interactive tooltips showing shape info on hover using Konva.
Source: https://konvajs.org/docs/sandbox/10000_Shapes_with_Tooltip.html
This demo shows how to handle a large number of shapes (10,000 circles) with tooltips efficiently. When you hover over any circle, a tooltip will show its index and color.
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const circlesLayer = new Konva.Layer();
const tooltipLayer = new Konva.Layer();
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'cyan', 'purple'];
let colorIndex = 0;
for (let i = 0; i < 10000; i++) {
const color = colors[colorIndex++];
if (colorIndex >= colors.length) {
colorIndex = 0;
}
const randX = Math.random() * stage.width();
const randY = Math.random() * stage.height();
const circle = new Konva.Circle({
x: randX,
y: randY,
radius: 3,
fill: color,
name: i.toString(),
});
circlesLayer.add(circle);
}
const tooltip = new Konva.Text({
text: '',
fontFamily: 'Calibri',
fontSize: 12,
padding: 5,
visible: false,
fill: 'black',
opacity: 0.75,
});
tooltipLayer.add(tooltip);
stage.add(circlesLayer);
stage.add(tooltipLayer);
circlesLayer.on('mousemove', (e) => {
const mousePos = stage.getPointerPosition();
tooltip.position({
x: mousePos.x + 5,
y: mousePos.y + 5,
});
tooltip.text('node: ' + e.target.name() + ', color: ' + e.target.fill());
tooltip.show();
});
circlesLayer.on('mouseout', () => {
tooltip.hide();
});
```
```js
import React from 'react';
import { Stage, Layer, Circle, Text } from 'react-konva';
const App = () => {
const [tooltipProps, setTooltipProps] = React.useState({
text: '',
visible: false,
x: 0,
y: 0
});
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'cyan', 'purple'];
const circles = React.useMemo(() => {
const items = [];
let colorIndex = 0;
for (let i = 0; i < 10000; i++) {
const color = colors[colorIndex++];
if (colorIndex >= colors.length) {
colorIndex = 0;
}
items.push({
id: i,
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
color
});
}
return items;
}, []);
const handleMouseMove = (e) => {
const mousePos = e.target.getStage().getPointerPosition();
setTooltipProps({
text: `node: ${e.target.name()}, color: ${e.target.attrs.fill}`,
visible: true,
x: mousePos.x + 5,
y: mousePos.y + 5
});
};
const handleMouseOut = () => {
setTooltipProps(prev => ({ ...prev, visible: false }));
};
return (
{circles.map(({ id, x, y, color }) => (
))}
);
};
export default App;
```
```js
```
---
# Interactive Scatter Plot with 20,000 Nodes
> Demo rendering 20,000 draggable circles with tooltips and event delegation to showcase Konva performance.
Source: https://konvajs.org/docs/sandbox/20000_Nodes.html
The purpose of this lab is to demonstrate the sheer number of nodes that Konva can handle by rendering 20,000 circles. Each circle is sensitive to mouseover events, and can be drag and dropped. This lab is also a great demonstration of event delegation, in which a single event handler attached to the stage handles the circle events.
**Instructions:** Mouse over the nodes to see more information, and then drag and drop them around the stage.
```js
import Konva from 'konva';
// create stage
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
// function to add a node to layer
function addNode(obj, layer) {
const node = new Konva.Circle({
x: obj.x,
y: obj.y,
radius: 4,
fill: obj.color,
id: obj.id,
});
layer.add(node);
}
// Create a single layer for all circles
const circlesLayer = new Konva.Layer();
const tooltipLayer = new Konva.Layer();
const dragLayer = new Konva.Layer();
// create tooltip
const tooltip = new Konva.Label({
opacity: 0.75,
visible: false,
listening: false,
});
tooltip.add(
new Konva.Tag({
fill: 'black',
pointerDirection: 'down',
pointerWidth: 10,
pointerHeight: 10,
lineJoin: 'round',
shadowColor: 'black',
shadowBlur: 10,
shadowOffsetX: 10,
shadowOffsetY: 10,
shadowOpacity: 0.2,
})
);
tooltip.add(
new Konva.Text({
text: '',
fontFamily: 'Calibri',
fontSize: 18,
padding: 5,
fill: 'white',
})
);
tooltipLayer.add(tooltip);
// build data
const data = [];
const colors = ['red', 'orange', 'cyan', 'green', 'blue', 'purple'];
for (let n = 0; n < 20000; n++) {
const x = Math.random() * width;
const y = height + Math.random() * 200 - 100 + (height / width) * -1 * x;
data.push({
x: x,
y: y,
id: n.toString(),
color: colors[Math.round(Math.random() * 5)],
});
}
// Add all nodes to a single layer
for (let n = 0; n < data.length; n++) {
addNode(data[n], circlesLayer);
}
// Add all layers to stage
stage.add(circlesLayer);
stage.add(dragLayer);
stage.add(tooltipLayer);
// handle events
let originalLayer;
stage.on('mouseover mousemove dragmove', function (evt) {
const node = evt.target;
if (node === stage) {
return;
}
if (node) {
// update tooltip
const mousePos = node.getStage().getPointerPosition();
tooltip.position({
x: mousePos.x,
y: mousePos.y - 5,
});
tooltip
.getText()
.text('node: ' + node.id() + ', color: ' + node.fill());
tooltip.show();
}
});
stage.on('mouseout', function (evt) {
tooltip.hide();
});
stage.on('mousedown', function (evt) {
const shape = evt.target;
if (shape) {
originalLayer = shape.getLayer();
shape.moveTo(dragLayer);
// manually trigger drag and drop
shape.startDrag();
}
});
stage.on('mouseup', function (evt) {
const shape = evt.target;
if (shape) {
shape.moveTo(originalLayer);
}
});
```
```js
import { useState, useEffect, useRef, useCallback, memo } from 'react';
import { Stage, Layer, Circle, Label, Tag, Text } from 'react-konva';
const CirclesLayer = ({ nodes, onMouseOver, onMouseMove, onMouseOut, onMouseDown, onMouseUp, onDragEnd }) => {
// Only re-render when the nodes array reference changes
return (
{nodes.map(node => (
onMouseOver(e, node)}
onMouseMove={onMouseMove}
onMouseOut={onMouseOut}
onDragMove={onMouseMove}
onMouseDown={e => onMouseDown(e, node)}
onMouseUp={e => onMouseUp(e, node)}
onDragEnd={e => onDragEnd(e, node.id)}
draggable
/>
))}
);
};
// Memoize the CirclesLayer component to prevent unnecessary re-renders
const MemoizedCirclesLayer = memo(CirclesLayer);
const TooltipLayer = ({ tooltip }) => (
);
// Memoize the TooltipLayer to only re-render when tooltip props change
const MemoizedTooltipLayer = memo(TooltipLayer);
const App = () => {
const width = window.innerWidth;
const height = window.innerHeight;
// Create refs for the layers
const dragLayerRef = useRef(null);
// State for tooltip
const [tooltip, setTooltip] = useState({
visible: false,
x: 0,
y: 0,
text: ''
});
// Keep positions in state, but update only after a drag for this large scene.
const [nodes, setNodes] = useState(() => {
const colors = ['red', 'orange', 'cyan', 'green', 'blue', 'purple'];
const data = [];
for (let n = 0; n < 20000; n++) {
const x = Math.random() * width;
const y = height + Math.random() * 200 - 100 + (height / width) * -1 * x;
data.push({
x,
y,
id: n,
color: colors[Math.round(Math.random() * 5)],
});
}
return data;
});
// Event handlers - wrap in useCallback to prevent recreating functions on each render
const handleMouseOver = useCallback((e, node) => {
const stage = e.target.getStage();
const pos = stage.getPointerPosition();
setTooltip({
visible: true,
x: pos.x,
y: pos.y - 5,
text: `node: ${node.id}, color: ${node.color}`
});
}, []);
const handleMouseMove = useCallback((e) => {
const stage = e.target.getStage();
const pos = stage.getPointerPosition();
setTooltip(prev => ({
...prev,
x: pos.x,
y: pos.y - 5
}));
}, []);
const handleMouseOut = useCallback(() => {
setTooltip(prev => ({
...prev,
visible: false
}));
}, []);
const handleMouseDown = useCallback((e, node) => {
// For drag handling if needed
}, []);
const handleMouseUp = useCallback((e, node) => {
// For drag handling if needed
}, []);
const handleDragEnd = useCallback((e, id) => {
const { x, y } = e.target.position();
setNodes(current =>
current.map(node => (node.id === id ? { ...node, x, y } : node))
);
}, []);
return (
{/* Render single layer for all circles */}
{/* Drag layer - if needed */}
{/* Tooltip layer */}
);
};
export default App;
```
```js
```
---
# Animals on the Beach Game
> Interactive drag-and-drop game where you match animal images to their silhouettes on a beach using Konva.
Source: https://konvajs.org/docs/sandbox/Animals_on_the_Beach_Game.html
```js
import Konva from 'konva';
// create stage
const stage = new Konva.Stage({
container: 'container',
width: 578,
height: 530,
});
const background = new Konva.Layer();
const animalLayer = new Konva.Layer();
const animalShapes = [];
let score = 0;
// create and load background image
const backgroundImage = new Image();
backgroundImage.onload = function() {
const backgroundKonvaImage = new Konva.Image({
image: backgroundImage,
x: 0,
y: 0,
width: stage.width(),
height: stage.height(),
});
background.add(backgroundKonvaImage);
backgroundKonvaImage.moveToBottom();
};
backgroundImage.src = 'https://konvajs.org/assets/beach.png';
// image positions
const animals = {
snake: {
x: 10,
y: 70,
},
giraffe: {
x: 90,
y: 70,
},
monkey: {
x: 275,
y: 70,
},
lion: {
x: 400,
y: 70,
},
};
const outlines = {
snake_black: {
x: 275,
y: 350,
},
giraffe_black: {
x: 390,
y: 250,
},
monkey_black: {
x: 300,
y: 420,
},
lion_black: {
x: 100,
y: 390,
},
};
function isNearOutline(animal, outline) {
const a = animal;
const o = outline;
const ax = a.x();
const ay = a.y();
if (ax > o.x - 20 && ax < o.x + 20 && ay > o.y - 20 && ay < o.y + 20) {
return true;
} else {
return false;
}
}
// create message text
const messageText = new Konva.Text({
text: 'Ahoy! Put the animals on the beach!',
x: stage.width() / 2,
y: 40,
fontSize: 20,
fontFamily: 'Calibri',
fill: 'white',
align: 'center',
// for center align we need to set offset
offsetX: 200,
});
background.add(messageText);
function updateMessage(text) {
messageText.text(text);
}
function loadImages(sources, callback) {
const assetDir = 'https://konvajs.org/assets/';
const images = {};
let loadedImages = 0;
let numImages = 0;
for (const src in sources) {
numImages++;
}
for (const src in sources) {
images[src] = new Image();
images[src].onload = function () {
if (++loadedImages >= numImages) {
callback(images);
}
};
images[src].src = assetDir + sources[src];
}
}
function initStage(images) {
// create draggable animals
for (const key in animals) {
// anonymous function to induce scope
(function () {
const privKey = key;
const anim = animals[key];
const animal = new Konva.Image({
image: images[key],
x: anim.x,
y: anim.y,
draggable: true,
});
animal.on('dragstart', function () {
this.moveToTop();
});
/*
* check if animal is in the right spot and
* snap into place if it is
*/
animal.on('dragend', function () {
const outline = outlines[privKey + '_black'];
if (!animal.inRightPlace && isNearOutline(animal, outline)) {
animal.position({
x: outline.x,
y: outline.y,
});
animal.inRightPlace = true;
if (++score >= 4) {
const text = 'You win! Enjoy your booty!';
updateMessage(text);
}
// disable drag and drop
setTimeout(function () {
animal.draggable(false);
}, 50);
}
});
// make animal glow on mouseover
animal.on('mouseover', function () {
animal.image(images[privKey + '_glow']);
document.body.style.cursor = 'pointer';
});
// return animal on mouseout
animal.on('mouseout', function () {
animal.image(images[privKey]);
document.body.style.cursor = 'default';
});
animal.on('dragmove', function () {
document.body.style.cursor = 'pointer';
});
animalLayer.add(animal);
animalShapes.push(animal);
})();
}
// create animal outlines
for (const key in outlines) {
// anonymous function to induce scope
(function () {
const imageObj = images[key];
const out = outlines[key];
const outline = new Konva.Image({
image: imageObj,
x: out.x,
y: out.y,
});
animalLayer.add(outline);
})();
}
stage.add(background);
stage.add(animalLayer);
updateMessage(
'Ahoy! Put the animals on the beach!'
);
}
const sources = {
beach: 'beach.png',
snake: 'snake.png',
snake_glow: 'snake-glow.png',
snake_black: 'snake-black.png',
lion: 'lion.png',
lion_glow: 'lion-glow.png',
lion_black: 'lion-black.png',
monkey: 'monkey.png',
monkey_glow: 'monkey-glow.png',
monkey_black: 'monkey-black.png',
giraffe: 'giraffe.png',
giraffe_glow: 'giraffe-glow.png',
giraffe_black: 'giraffe-black.png',
};
// Demo warning: You'll need local image files for this demo to work
loadImages(sources, initStage);
```
```jsx
import { useState } from 'react';
import { Stage, Layer, Image, Text } from 'react-konva';
import useImage from 'use-image';
const Animal = ({ name, startX, startY, outline, onScore }) => {
const [pos, setPos] = useState({ x: startX, y: startY });
const [isDraggable, setIsDraggable] = useState(true);
const [inRightPlace, setInRightPlace] = useState(false);
const [image] = useImage(`https://konvajs.org/assets/${name}.png`);
const [glowImage] = useImage(`https://konvajs.org/assets/${name}-glow.png`);
const isNearOutline = (pos, outline) => {
const { x, y } = pos;
return (
x > outline.x - 20 &&
x < outline.x + 20 &&
y > outline.y - 20 &&
y < outline.y + 20
);
};
if (!image || !glowImage) return null;
return (
e.target.moveToTop()}
onDragEnd={(e) => {
const newPos = { x: e.target.x(), y: e.target.y() };
setPos(newPos);
if (!inRightPlace && isNearOutline(newPos, outline)) {
setPos({ x: outline.x, y: outline.y });
setInRightPlace(true);
setIsDraggable(false);
onScore();
}
}}
onMouseOver={(e) => {
e.target.image(glowImage);
const stage = e.target.getStage();
stage.container().style.cursor = 'pointer';
}}
onMouseOut={(e) => {
e.target.image(image);
const stage = e.target.getStage();
stage.container().style.cursor = 'default';
}}
onDragMove={(e) => {
const stage = e.target.getStage();
stage.container().style.cursor = 'pointer';
}}
/>
);
};
const AnimalOutline = ({ name, x, y }) => {
const [image] = useImage(`https://konvajs.org/assets/${name}-black.png`);
return image ? : null;
};
const Background = () => {
const [image] = useImage('https://konvajs.org/assets/beach.png');
return image ? : null;
};
const App = () => {
const [score, setScore] = useState(0);
const animals = {
snake: { x: 10, y: 70, outline: { x: 275, y: 350 } },
giraffe: { x: 90, y: 70, outline: { x: 390, y: 250 } },
monkey: { x: 275, y: 70, outline: { x: 300, y: 420 } },
lion: { x: 400, y: 70, outline: { x: 100, y: 390 } }
};
return (
= 4 ? 'You win! Enjoy your booty!' : 'Ahoy! Put the animals on the beach!'}
x={578 / 2}
y={40}
fontSize={20}
fontFamily="Calibri"
fill="white"
align="center"
offsetX={200}
/>
{Object.entries(animals).map(([name, pos]) => (
))}
{Object.entries(animals).map(([name, pos]) => (
setScore(s => s + 1)}
/>
))}
);
};
export default App;
```
```vue
```
**Instructions:** Drag and drop the animals onto their silhouettes on the beach. When all animals are correctly placed, you win!
---
# Animation Stress Test
> Stress test animating 300 rotating rectangles on canvas with optimized performance using Konva.
Source: https://konvajs.org/docs/sandbox/Animation_Stress_Test.html
This demo creates 300 rectangles with random sizes, positions, and colors, then animates them by rotating each rectangle. The animation performance is optimized by setting the `listening` property of the layer to `false`, which improves drawing performance as the rectangles won't be drawn onto the hit graph.
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
function update(layer, frame) {
const angularSpeed = 100;
const angularDiff = (angularSpeed * frame.timeDiff) / 1000;
const shapes = layer.getChildren();
for (let n = 0; n < shapes.length; n++) {
const shape = shapes[n];
shape.rotate(angularDiff);
}
}
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
/*
* setting the listening property to false will improve
* drawing performance because the rectangles won't have to be
* drawn onto the hit graph
*/
const layer = new Konva.Layer({
listening: false,
});
const colors = [
'red',
'orange',
'yellow',
'green',
'blue',
'cyan',
'purple',
];
let colorIndex = 0;
for (let i = 0; i < 300; i++) {
const color = colors[colorIndex++];
if (colorIndex >= colors.length) {
colorIndex = 0;
}
const randWidth = Math.random() * 100 + 20;
const randHeight = Math.random() * 100 + 20;
const randX = Math.random() * stage.width() - 20;
const randY = Math.random() * stage.height() - 20;
const box = new Konva.Rect({
x: randX,
y: randY,
offset: {
x: randWidth / 2,
y: randHeight / 2,
},
width: randWidth,
height: randHeight,
fill: color,
stroke: 'black',
strokeWidth: 4,
});
layer.add(box);
}
stage.add(layer);
const anim = new Konva.Animation(function (frame) {
update(layer, frame);
}, layer);
anim.start();
```
```js
import { useState, useEffect, useRef } from 'react';
import { Stage, Layer, Rect } from 'react-konva';
import Konva from 'konva';
const App = () => {
const width = window.innerWidth;
const height = window.innerHeight;
// State for rectangles
const [boxes, setBoxes] = useState([]);
// Animation ref to keep track of the animation
const animRef = useRef(null);
const layerRef = useRef(null);
// Generate random boxes
useEffect(() => {
const colors = [
'red',
'orange',
'yellow',
'green',
'blue',
'cyan',
'purple',
];
const newBoxes = [];
let colorIndex = 0;
for (let i = 0; i < 300; i++) {
const color = colors[colorIndex++];
if (colorIndex >= colors.length) {
colorIndex = 0;
}
const randWidth = Math.random() * 100 + 20;
const randHeight = Math.random() * 100 + 20;
const randX = Math.random() * width - 20;
const randY = Math.random() * height - 20;
newBoxes.push({
id: i,
x: randX,
y: randY,
width: randWidth,
height: randHeight,
offsetX: randWidth / 2,
offsetY: randHeight / 2,
fill: color,
stroke: 'black',
strokeWidth: 4,
rotation: 0, // Initial rotation
});
}
setBoxes(newBoxes);
}, [width, height]);
// Setup animation on mount
useEffect(() => {
if (layerRef.current && boxes.length > 0) {
// Create animation
const angularSpeed = 100;
animRef.current = new Konva.Animation((frame) => {
const angularDiff = (angularSpeed * frame.timeDiff) / 1000;
setBoxes(prevBoxes =>
prevBoxes.map(box => ({
...box,
rotation: box.rotation + angularDiff
}))
);
}, layerRef.current.getLayer());
// Start animation
animRef.current.start();
// Cleanup on unmount
return () => {
if (animRef.current) {
animRef.current.stop();
}
};
}
}, [boxes.length]);
return (
{boxes.map((box) => (
))}
);
};
export default App;
```
```js
```
**Instructions:** This demo shows the animation capability of Konva by rotating 300 rectangles simultaneously. Watch as the shapes rotate smoothly across the screen.
---
# How to Build a Name Tag and Badge Maker with JavaScript Canvas
> Create custom name tags, badges, and labels with JavaScript and HTML5 Canvas using Konva.js. Drag-and-drop text, shapes, and icons with live preview and PNG export.
Source: https://konvajs.org/docs/sandbox/Badge_Maker.html
Name tags, badges and labels are used everywhere — from conferences and events to product packaging and school projects. With Konva you can build a fully interactive badge maker with draggable text, shape templates, color customization and one-click export.
**Instructions:** Click a badge template to start. Edit the text fields, drag elements to reposition, change colors, and click "Export as PNG" to save your badge.
```js
import Konva from 'konva';
// --- Controls ---
const controls = document.createElement('div');
controls.style.cssText = 'display:flex;gap:8px;align-items:center;margin-bottom:4px;flex-wrap:wrap;';
const bgLabel = document.createElement('label');
bgLabel.textContent = 'Badge Color: ';
const bgColor = document.createElement('input');
bgColor.type = 'color';
bgColor.value = '#3b82f6';
bgLabel.appendChild(bgColor);
const textColorLabel = document.createElement('label');
textColorLabel.textContent = 'Text Color: ';
const textColor = document.createElement('input');
textColor.type = 'color';
textColor.value = '#ffffff';
textColorLabel.appendChild(textColor);
const exportBtn = document.createElement('button');
exportBtn.textContent = 'Export as PNG';
const clearBtn = document.createElement('button');
clearBtn.textContent = 'Reset';
controls.appendChild(bgLabel);
controls.appendChild(textColorLabel);
controls.appendChild(exportBtn);
controls.appendChild(clearBtn);
const container = document.getElementById('container');
container.parentNode.insertBefore(controls, container);
// --- Stage ---
const stageWidth = window.innerWidth;
const stageHeight = window.innerHeight - 40;
const stage = new Konva.Stage({
container: 'container',
width: stageWidth,
height: stageHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// Badge dimensions
const badgeW = 340;
const badgeH = 220;
const badgeX = (stageWidth - badgeW) / 2;
const badgeY = (stageHeight - badgeH) / 2;
// Badge background
const badge = new Konva.Rect({
x: badgeX,
y: badgeY,
width: badgeW,
height: badgeH,
fill: bgColor.value,
cornerRadius: 16,
shadowColor: 'rgba(0,0,0,0.15)',
shadowBlur: 12,
shadowOffset: { x: 0, y: 4 },
});
layer.add(badge);
// Decorative top stripe
const stripe = new Konva.Rect({
x: badgeX,
y: badgeY,
width: badgeW,
height: 50,
fill: 'rgba(0,0,0,0.15)',
cornerRadius: [16, 16, 0, 0],
});
layer.add(stripe);
// "HELLO" header
const helloText = new Konva.Text({
x: badgeX,
y: badgeY + 8,
width: badgeW,
text: 'HELLO',
fontSize: 14,
fontFamily: 'Arial',
fontStyle: 'bold',
fill: 'rgba(255,255,255,0.8)',
align: 'center',
letterSpacing: 4,
});
layer.add(helloText);
// "my name is" subtitle
const subtitleText = new Konva.Text({
x: badgeX,
y: badgeY + 26,
width: badgeW,
text: 'my name is',
fontSize: 12,
fontFamily: 'Arial',
fill: 'rgba(255,255,255,0.6)',
align: 'center',
});
layer.add(subtitleText);
// Editable name
const nameText = new Konva.Text({
x: badgeX + 20,
y: badgeY + 70,
width: badgeW - 40,
text: 'Your Name',
fontSize: 36,
fontFamily: 'Arial',
fontStyle: 'bold',
fill: textColor.value,
align: 'center',
draggable: true,
});
layer.add(nameText);
// Editable title/role
const roleText = new Konva.Text({
x: badgeX + 20,
y: badgeY + 120,
width: badgeW - 40,
text: 'Job Title',
fontSize: 18,
fontFamily: 'Arial',
fill: 'rgba(255,255,255,0.75)',
align: 'center',
draggable: true,
});
layer.add(roleText);
// Editable company
const companyText = new Konva.Text({
x: badgeX + 20,
y: badgeY + 150,
width: badgeW - 40,
text: 'Company',
fontSize: 16,
fontFamily: 'Arial',
fill: 'rgba(255,255,255,0.55)',
align: 'center',
draggable: true,
});
layer.add(companyText);
// Small circle decoration
const circle = new Konva.Circle({
x: badgeX + badgeW - 30,
y: badgeY + badgeH - 30,
radius: 14,
fill: 'rgba(255,255,255,0.2)',
});
layer.add(circle);
// Transformer for selection
const tr = new Konva.Transformer({
rotateEnabled: false,
borderStrokeWidth: 1,
anchorSize: 8,
});
layer.add(tr);
// Select on click
stage.on('click tap', function (e) {
const target = e.target;
if (target === stage || target === badge || target === stripe || target === circle) {
tr.nodes([]);
return;
}
if (target.draggable()) {
tr.nodes([target]);
}
});
// Double-click to edit text
function enableTextEdit(textNode) {
textNode.on('dblclick dbltap', () => {
textNode.hide();
tr.hide();
const textPosition = textNode.absolutePosition();
const stageBox = stage.container().getBoundingClientRect();
const input = document.createElement('input');
input.type = 'text';
input.value = textNode.text();
input.style.position = 'absolute';
input.style.top = stageBox.top + textPosition.y + 'px';
input.style.left = stageBox.left + textPosition.x + 'px';
input.style.width = textNode.width() + 'px';
input.style.fontSize = textNode.fontSize() + 'px';
input.style.fontFamily = textNode.fontFamily();
input.style.textAlign = textNode.align();
input.style.border = '2px solid #3b82f6';
input.style.borderRadius = '4px';
input.style.padding = '2px 4px';
input.style.outline = 'none';
input.style.background = '#fff';
input.style.zIndex = '1000';
document.body.appendChild(input);
input.focus();
input.select();
function finish() {
textNode.text(input.value);
textNode.show();
tr.show();
tr.forceUpdate();
document.body.removeChild(input);
}
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') finish();
});
input.addEventListener('blur', finish);
});
}
enableTextEdit(nameText);
enableTextEdit(roleText);
enableTextEdit(companyText);
// Controls
bgColor.addEventListener('input', () => {
badge.fill(bgColor.value);
});
textColor.addEventListener('input', () => {
nameText.fill(textColor.value);
});
exportBtn.addEventListener('click', () => {
// hide transformer for clean export
tr.nodes([]);
const dataURL = stage.toDataURL({
x: badgeX - 4,
y: badgeY - 4,
width: badgeW + 8,
height: badgeH + 8,
pixelRatio: 3,
});
const link = document.createElement('a');
link.download = 'badge.png';
link.href = dataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
clearBtn.addEventListener('click', () => {
nameText.text('Your Name');
roleText.text('Job Title');
companyText.text('Company');
bgColor.value = '#3b82f6';
badge.fill('#3b82f6');
textColor.value = '#ffffff';
nameText.fill('#ffffff');
tr.nodes([]);
});
```
```js
import React from 'react';
import { Stage, Layer, Rect, Text, Circle, Transformer } from 'react-konva';
const App = () => {
const [bgFill, setBgFill] = React.useState('#3b82f6');
const [textFill, setTextFill] = React.useState('#ffffff');
const [name, setName] = React.useState('Your Name');
const [role, setRole] = React.useState('Job Title');
const [company, setCompany] = React.useState('Company');
const [selected, setSelected] = React.useState(null);
const stageRef = React.useRef(null);
const trRef = React.useRef(null);
const nameRef = React.useRef(null);
const roleRef = React.useRef(null);
const companyRef = React.useRef(null);
const W = window.innerWidth;
const H = window.innerHeight - 60;
const bW = 340, bH = 220;
const bX = (W - bW) / 2, bY = (H - bH) / 2;
const initialTransforms = {
name: { x: bX + 20, y: bY + 70, scaleX: 1, scaleY: 1 },
role: { x: bX + 20, y: bY + 120, scaleX: 1, scaleY: 1 },
company: { x: bX + 20, y: bY + 150, scaleX: 1, scaleY: 1 },
};
const [transforms, setTransforms] = React.useState(initialTransforms);
React.useEffect(() => {
if (trRef.current && selected) {
trRef.current.nodes([selected]);
trRef.current.getLayer().batchDraw();
}
}, [selected]);
const handleStageClick = (e) => {
const target = e.target;
if (target === e.target.getStage() || !target.draggable()) {
setSelected(null);
if (trRef.current) trRef.current.nodes([]);
return;
}
setSelected(target);
};
const handleDblClick = (e, setText) => {
const textNode = e.target;
textNode.hide();
if (trRef.current) trRef.current.hide();
const pos = textNode.absolutePosition();
const scale = textNode.getAbsoluteScale();
const box = stageRef.current.container().getBoundingClientRect();
const input = document.createElement('input');
input.type = 'text';
input.value = textNode.text();
input.style.cssText = `position:absolute;top:${box.top+pos.y}px;left:${box.left+pos.x}px;width:${textNode.width()*Math.abs(scale.x)}px;font-size:${textNode.fontSize()*Math.abs(scale.y)}px;font-family:${textNode.fontFamily()};text-align:${textNode.align()};border:2px solid #3b82f6;border-radius:4px;padding:2px 4px;box-sizing:border-box;outline:none;z-index:1000;background:#fff;`;
document.body.appendChild(input);
input.focus();
input.select();
const finish = () => {
setText(input.value);
textNode.show();
if (trRef.current) trRef.current.show();
if (document.body.contains(input)) document.body.removeChild(input);
};
input.addEventListener('keydown', (ev) => { if (ev.key === 'Enter') finish(); });
input.addEventListener('blur', finish);
};
const handleDragEnd = (id, e) => {
setTransforms((current) => ({
...current,
[id]: { ...current[id], ...e.target.position() },
}));
};
const handleTransformEnd = (id, e) => {
const node = e.target;
setTransforms((current) => ({
...current,
[id]: {
x: node.x(),
y: node.y(),
scaleX: node.scaleX(),
scaleY: node.scaleY(),
},
}));
};
const handleExport = () => {
setSelected(null);
if (trRef.current) trRef.current.nodes([]);
setTimeout(() => {
const dataURL = stageRef.current.toDataURL({ x: bX-4, y: bY-4, width: bW+8, height: bH+8, pixelRatio: 3 });
const link = document.createElement('a');
link.download = 'badge.png';
link.href = dataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}, 50);
};
return (
<>
Badge Color: setBgFill(e.target.value)} />
Text Color: setTextFill(e.target.value)} />
Export as PNG
{ setName('Your Name'); setRole('Job Title'); setCompany('Company'); setBgFill('#3b82f6'); setTextFill('#ffffff'); setTransforms(initialTransforms); }}>Reset
handleDragEnd('name',e)} onTransformEnd={e=>handleTransformEnd('name',e)} onDblClick={e=>handleDblClick(e,setName)} onDblTap={e=>handleDblClick(e,setName)} />
handleDragEnd('role',e)} onTransformEnd={e=>handleTransformEnd('role',e)} onDblClick={e=>handleDblClick(e,setRole)} onDblTap={e=>handleDblClick(e,setRole)} />
handleDragEnd('company',e)} onTransformEnd={e=>handleTransformEnd('company',e)} onDblClick={e=>handleDblClick(e,setCompany)} onDblTap={e=>handleDblClick(e,setCompany)} />
>
);
};
export default App;
```
```js
```
---
# How to add background to canvas?
> Learn two ways to add a background to your Konva canvas: using a Rect shape or CSS styles on the container.
Source: https://konvajs.org/docs/sandbox/Canvas_Background.html
## How add background to Konva stage?
There are two ways to add a background.
### 1. Adding background with `Konva.Rect` shape.
The Konva-way to add a background to your canvas is just by drawing `Konva.Rect` shape with the size of a stage on the bottom of your scene. You can style that rectangle as you want with [solid color, gradient or pattern image](/docs/styling/Fill.html).
*The only thing that you should be careful about here is the rectangle's position and size. If you are transforming any parent of background shape (such as stage or layer) by moving it, or applying scale you should "reset" background shape position/size to fill whole Stage area.*
### 2. Adding background with CSS
The other solution to add background to your canvas is just use CSS styles to stage container DOM element. That solution is simpler than the first approach, because you don't need to track position, size changes. It also has **a bit** better performance, because you don't need to draw any additional shapes.
**But it has one drawback. The CSS background will be not visible on export when you use methods like `stage.toImage()` and `stage.toDataURL()`.**
**Instructions:** On the demo below, the green solid background is made with CSS. Yellow-blue gradient will be done with `Konva.Rect` instance. Try to drag the stage. You will see that gradient stays in place.
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
draggable: true,
});
const layer = new Konva.Layer();
stage.add(layer);
// there are two ways to add background to the stage.
// the simplest solution is to just using CSS
stage.container().style.backgroundColor = 'green';
// another solution is to use rectangle shape
const background = new Konva.Rect({
x: 0,
y: 0,
width: stage.width(),
height: stage.height(),
fillLinearGradientStartPoint: { x: 0, y: 0 },
fillLinearGradientEndPoint: { x: stage.width(), y: stage.height() },
// gradient into transparent color, so we can see CSS styles
fillLinearGradientColorStops: [
0,
'yellow',
0.5,
'blue',
0.6,
'rgba(0, 0, 0, 0)',
],
// remove background from hit graph for better perf
// because we don't need any events on the background
listening: false,
});
layer.add(background);
// the stage is draggable
// that means absolute position of background may change
// so we need to reset it back to {0, 0}
stage.on('dragmove', () => {
background.absolutePosition({ x: 0, y: 0 });
});
// add demo shape
const circle = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 100,
fill: 'red',
});
layer.add(circle);
```
```js
import { useState, useRef, useEffect } from 'react';
import { Stage, Layer, Rect, Circle } from 'react-konva';
const App = () => {
const width = window.innerWidth;
const height = window.innerHeight;
const stageRef = useRef(null);
const backgroundRef = useRef(null);
// Set CSS background when component mounts
useEffect(() => {
if (stageRef.current) {
// Apply CSS background to stage container
const container = stageRef.current.container();
container.style.backgroundColor = 'green';
}
}, []);
// Handler to reset background position on stage drag
const handleDragMove = () => {
if (backgroundRef.current) {
backgroundRef.current.absolutePosition({ x: 0, y: 0 });
}
};
return (
{/* Gradient background */}
{/* Demo shape */}
);
};
export default App;
```
```js
```
---
# Canvas Right-Click Context Menu — Custom Menu for Canvas Shapes
> Add a custom right-click context menu to HTML5 Canvas shapes with JavaScript using Konva.js. Includes pulse animation and delete actions on canvas objects.
Source: https://konvajs.org/docs/sandbox/Canvas_Context_Menu.html
## Do you want to show a context menu for a canvas shape?
To show a context menu we have to:
1. Listen to `contextmenu` event on canvas container (stage)
2. Prevent default browser behavior, so we don't see native context menu
3. Create our own context menu with `Konva` tools or regular html
**Instructions: double click on the stage to create a circle. Try right click (context menu) on shapes for a menu.**
```js
import Konva from 'konva';
// Create a div to use as a context menu
const menuNode = document.createElement('div');
menuNode.id = 'menu';
menuNode.style.display = 'none';
menuNode.style.position = 'fixed';
menuNode.style.width = '60px';
menuNode.style.backgroundColor = 'white';
menuNode.style.boxShadow = '0 0 5px grey';
menuNode.style.borderRadius = '3px';
// Create buttons for the menu
const pulseButton = document.createElement('button');
pulseButton.textContent = 'Pulse';
pulseButton.style.width = '100%';
pulseButton.style.backgroundColor = 'white';
pulseButton.style.border = 'none';
pulseButton.style.margin = '0';
pulseButton.style.padding = '10px';
const deleteButton = document.createElement('button');
deleteButton.textContent = 'Delete';
deleteButton.style.width = '100%';
deleteButton.style.backgroundColor = 'white';
deleteButton.style.border = 'none';
deleteButton.style.margin = '0';
deleteButton.style.padding = '10px';
// Add hover effects
pulseButton.addEventListener('mouseover', () => {
pulseButton.style.backgroundColor = 'lightgray';
});
pulseButton.addEventListener('mouseout', () => {
pulseButton.style.backgroundColor = 'white';
});
deleteButton.addEventListener('mouseover', () => {
deleteButton.style.backgroundColor = 'lightgray';
});
deleteButton.addEventListener('mouseout', () => {
deleteButton.style.backgroundColor = 'white';
});
// Add buttons to menu
menuNode.appendChild(pulseButton);
menuNode.appendChild(deleteButton);
document.body.appendChild(menuNode);
// Set up the stage
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
// add default shape
const shape = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 50,
fill: 'red',
shadowBlur: 10,
});
layer.add(shape);
let currentShape;
// Setup the menu functionality
pulseButton.addEventListener('click', () => {
currentShape.to({
scaleX: 2,
scaleY: 2,
onFinish: () => {
currentShape.to({ scaleX: 1, scaleY: 1 });
},
});
});
deleteButton.addEventListener('click', () => {
currentShape.destroy();
});
// Hide menu on document click
window.addEventListener('click', () => {
menuNode.style.display = 'none';
});
// Add double click event to create new shapes
stage.on('dblclick dbltap', function () {
// add a new shape
const newShape = new Konva.Circle({
x: stage.getPointerPosition().x,
y: stage.getPointerPosition().y,
radius: 10 + Math.random() * 30,
fill: Konva.Util.getRandomColor(),
shadowBlur: 10,
});
layer.add(newShape);
});
// Add context menu event
stage.on('contextmenu', function (e) {
// prevent default behavior
e.evt.preventDefault();
if (e.target === stage) {
// if we are on empty place of the stage we will do nothing
return;
}
currentShape = e.target;
// show menu
menuNode.style.display = 'initial';
const containerRect = stage.container().getBoundingClientRect();
menuNode.style.top =
containerRect.top + stage.getPointerPosition().y + 4 + 'px';
menuNode.style.left =
containerRect.left + stage.getPointerPosition().x + 4 + 'px';
});
```
```js
import { useState, useRef, useEffect } from 'react';
import { Stage, Layer, Circle } from 'react-konva';
const App = () => {
const [circles, setCircles] = useState([
{
id: 'initial-circle',
x: window.innerWidth / 2,
y: window.innerHeight / 2,
radius: 50,
fill: 'red',
shadowBlur: 10
}
]);
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
const [showMenu, setShowMenu] = useState(false);
const [selectedId, setSelectedId] = useState(null);
const stageRef = useRef(null);
const width = window.innerWidth;
const height = window.innerHeight;
// Create and cleanup context menu
useEffect(() => {
// Hide menu on window click
const handleWindowClick = () => {
setShowMenu(false);
};
window.addEventListener('click', handleWindowClick);
return () => {
window.removeEventListener('click', handleWindowClick);
};
}, []);
// Handle double click to create a new circle
const handleDblClick = (e) => {
const stage = e.target.getStage();
const pointerPosition = stage.getPointerPosition();
const newCircle = {
id: Date.now().toString(),
x: pointerPosition.x,
y: pointerPosition.y,
radius: 10 + Math.random() * 30,
fill: getRandomColor(),
shadowBlur: 10
};
setCircles([...circles, newCircle]);
};
// Handle context menu for circles
const handleContextMenu = (e) => {
e.evt.preventDefault();
if (e.target === e.target.getStage()) {
return;
}
const stage = e.target.getStage();
const containerRect = stage.container().getBoundingClientRect();
const pointerPosition = stage.getPointerPosition();
setMenuPosition({
x: containerRect.left + pointerPosition.x + 4,
y: containerRect.top + pointerPosition.y + 4
});
setShowMenu(true);
setSelectedId(e.target.id());
e.cancelBubble = true;
};
// Menu action handlers
const handlePulse = () => {
const newCircles = circles.map(circle => {
if (circle.id === selectedId) {
return {
...circle,
scaleX: 2,
scaleY: 2,
animation: 'pulse'
};
}
return circle;
});
setCircles(newCircles);
// Reset scale after animation
setTimeout(() => {
const resetCircles = circles.map(circle => {
if (circle.id === selectedId) {
return {
...circle,
scaleX: 1,
scaleY: 1,
animation: null
};
}
return circle;
});
setCircles(resetCircles);
}, 300);
};
const handleDelete = () => {
const newCircles = circles.filter(circle => circle.id !== selectedId);
setCircles(newCircles);
setShowMenu(false);
};
// Utility function for random color
const getRandomColor = () => {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
};
return (
{circles.map((circle) => (
))}
{/* Context Menu */}
{showMenu && (
e.stopPropagation()}
>
e.target.style.backgroundColor = 'lightgray'}
onMouseOut={(e) => e.target.style.backgroundColor = 'white'}
onClick={handlePulse}
>
Pulse
e.target.style.backgroundColor = 'lightgray'}
onMouseOut={(e) => e.target.style.backgroundColor = 'white'}
onClick={handleDelete}
>
Delete
)}
);
};
export default App;
```
```js
e.target.style.backgroundColor = 'lightgray'"
@mouseout="e => e.target.style.backgroundColor = 'white'"
@click="handlePulse"
>
Pulse
e.target.style.backgroundColor = 'lightgray'"
@mouseout="e => e.target.style.backgroundColor = 'white'"
@click="handleDelete"
>
Delete
```
---
# Canvas Overlay — Add Color, Text, and Gradient Overlays with JavaScript
> Add overlays to images on HTML5 Canvas with JavaScript. Create color tints, text captions, and gradient overlays using Konva.js. Interactive demo with opacity controls.
Source: https://konvajs.org/docs/sandbox/Canvas_Overlay.html
Layer visual effects on top of an image — color tints, text captions, and gradient vignettes. Toggle each overlay on and off and control the opacity.
**Instructions:** Click the buttons to toggle overlays. Adjust the opacity slider.
```js
import Konva from 'konva';
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
var layer = new Konva.Layer();
stage.add(layer);
var container = document.getElementById('container');
var controls = document.createElement('div');
controls.style.cssText = 'margin-bottom: 8px; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; font: 13px Arial, sans-serif;';
function makeBtn(label) {
var btn = document.createElement('button');
btn.textContent = label;
btn.dataset.active = 'false';
btn.style.cssText = 'padding: 6px 12px; border: 1px solid #ddd; border-radius: 4px; background: #f5f5f5; cursor: pointer; font-size: 13px;';
return btn;
}
var colorBtn = makeBtn('Color');
var textBtn = makeBtn('Text');
var gradientBtn = makeBtn('Gradient');
var opLabel = document.createElement('label');
opLabel.style.cssText = 'display: flex; align-items: center; gap: 6px; font-size: 13px;';
opLabel.appendChild(document.createTextNode('Opacity:'));
var opSlider = document.createElement('input');
opSlider.type = 'range';
opSlider.min = '0';
opSlider.max = '1';
opSlider.step = '0.1';
opSlider.value = '0.5';
opSlider.style.width = '80px';
opLabel.appendChild(opSlider);
controls.appendChild(colorBtn);
controls.appendChild(textBtn);
controls.appendChild(gradientBtn);
controls.appendChild(opLabel);
container.parentNode.insertBefore(controls, container);
var colorOverlay = null;
var textOverlay = null;
var gradientOverlay = null;
var currentOpacity = 0.5;
var imageObj = new Image();
imageObj.onload = function() {
layer.add(new Konva.Image({ image: imageObj, width: width, height: height }));
colorOverlay = new Konva.Rect({
width: width, height: height,
fill: '#0066ff', opacity: currentOpacity, visible: false,
});
layer.add(colorOverlay);
textOverlay = new Konva.Text({
x: 20, y: height - 50,
text: 'Sunset Valley', fontSize: 26,
fontFamily: 'Arial', fill: 'white',
opacity: currentOpacity, visible: false,
shadowColor: 'rgba(0,0,0,0.6)', shadowBlur: 6,
});
layer.add(textOverlay);
gradientOverlay = new Konva.Rect({
width: width, height: height,
fillLinearGradientStartPoint: { x: 0, y: 0 },
fillLinearGradientEndPoint: { x: 0, y: height },
fillLinearGradientColorStops: [0, 'rgba(0,0,0,0)', 0.5, 'rgba(0,0,0,0)', 1, 'rgba(0,0,0,0.7)'],
opacity: currentOpacity, visible: false,
});
layer.add(gradientOverlay);
};
imageObj.src = 'https://konvajs.org/assets/landscape.jpg';
function toggleBtn(btn, overlay) {
if (!overlay) return;
var active = btn.dataset.active === 'true';
btn.dataset.active = active ? 'false' : 'true';
overlay.visible(!active);
btn.style.background = !active ? '#0066ff' : '#f5f5f5';
btn.style.color = !active ? 'white' : 'black';
}
colorBtn.onclick = function() { toggleBtn(colorBtn, colorOverlay); };
textBtn.onclick = function() { toggleBtn(textBtn, textOverlay); };
gradientBtn.onclick = function() { toggleBtn(gradientBtn, gradientOverlay); };
opSlider.oninput = function() {
currentOpacity = parseFloat(this.value);
if (colorOverlay) colorOverlay.opacity(currentOpacity);
if (textOverlay) textOverlay.opacity(currentOpacity);
if (gradientOverlay) gradientOverlay.opacity(currentOpacity);
};
```
```js
import { Stage, Layer, Image as KonvaImage, Rect, Text } from 'react-konva';
import { useState, useEffect } from 'react';
var App = function() {
var [image, setImage] = useState(null);
var [colorOn, setColorOn] = useState(false);
var [textOn, setTextOn] = useState(false);
var [gradientOn, setGradientOn] = useState(false);
var [opacity, setOpacity] = useState(0.5);
var width = window.innerWidth;
var height = window.innerHeight;
useEffect(function() {
var img = new window.Image();
img.onload = function() { setImage(img); };
img.src = 'https://konvajs.org/assets/landscape.jpg';
}, []);
function btnStyle(active) {
return { padding: '6px 12px', border: '1px solid #ddd', borderRadius: '4px', background: active ? '#0066ff' : '#f5f5f5', color: active ? 'white' : 'black', cursor: 'pointer', fontSize: '13px' };
}
return (
);
};
export default App;
```
```js
```
---
# HTML5 Large Canvas Scrolling Demo
> Four approaches to scroll and navigate a large canvas: native scrollbars, drag-and-drop, custom scrollbars, and CSS transforms.
Source: https://konvajs.org/docs/sandbox/Canvas_Scrolling.html
Imagine we have this scenario. There are a very large stage 3000x3000 with many nodes inside.
User wants to take a look into all nodes, but they are not visible at once.
## How to display and scroll a very big html5 canvas?
Lets think you have a very large canvas and you want to add ability to navigate on it.
I will show your 4 different approaches to achieve that:
### 1. Just make large stage
This is the simplest approach. But it is very slow, because large canvases are slow.
User will be able to scroll with native scrollbars.
Pros:
* Simple implementation
Cons:
* Slow
```js
import Konva from 'konva';
const WIDTH = 3000;
const HEIGHT = 3000;
const NUMBER = 200;
const stage = new Konva.Stage({
container: 'container',
width: WIDTH,
height: HEIGHT,
});
const layer = new Konva.Layer();
stage.add(layer);
function generateNode() {
return new Konva.Circle({
x: WIDTH * Math.random(),
y: HEIGHT * Math.random(),
radius: 50,
fill: 'red',
stroke: 'black',
});
}
for (let i = 0; i < NUMBER; i++) {
layer.add(generateNode());
}
```
```js
import React from 'react';
import { Stage, Layer, Circle } from 'react-konva';
const WIDTH = 3000;
const HEIGHT = 3000;
const NUMBER = 200;
const App = () => {
const [nodes] = React.useState(() =>
Array(NUMBER).fill().map(() => ({
x: WIDTH * Math.random(),
y: HEIGHT * Math.random(),
}))
);
return (
{nodes.map((node, i) => (
))}
);
};
export default App;
```
```js
```
### 2. Make stage draggable (navigate with drag&drop)
That one is better because stage is much smaller.
Pros:
* Simple implementation
* Fast
Cons:
* Sometimes drag&drop navigation is not the best UX
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
draggable: true,
});
const layer = new Konva.Layer();
stage.add(layer);
const WIDTH = 3000;
const HEIGHT = 3000;
const NUMBER = 200;
function generateNode() {
return new Konva.Circle({
x: WIDTH * Math.random(),
y: HEIGHT * Math.random(),
radius: 50,
fill: 'red',
stroke: 'black',
});
}
for (let i = 0; i < NUMBER; i++) {
layer.add(generateNode());
}
```
```js
import React from 'react';
import { Stage, Layer, Circle } from 'react-konva';
const WIDTH = 3000;
const HEIGHT = 3000;
const NUMBER = 200;
const App = () => {
const [nodes] = React.useState(() =>
Array(NUMBER).fill().map(() => ({
x: WIDTH * Math.random(),
y: HEIGHT * Math.random(),
}))
);
return (
{nodes.map((node, i) => (
))}
);
};
export default App;
```
```js
```
### 3. Emulate scrollbars
You will have to draw them manually and implement all moving functionality.
That is quite a lot of work. But works good for many apps.
**Instructions**: try to scroll with bars.
Pros:
- Works ok
- Intuitive scroll
- Fast
Cons:
- Scrollbars are not native, so you have to implement many things manually (like scroll with keyboard)
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
const WIDTH = 3000;
const HEIGHT = 3000;
const NUMBER = 200;
function generateNode() {
return new Konva.Circle({
x: WIDTH * Math.random(),
y: HEIGHT * Math.random(),
radius: 50,
fill: 'red',
stroke: 'black',
});
}
for (let i = 0; i < NUMBER; i++) {
layer.add(generateNode());
}
// now draw our bars
const scrollLayers = new Konva.Layer();
stage.add(scrollLayers);
const PADDING = 5;
const verticalBar = new Konva.Rect({
width: 10,
height: 100,
fill: 'grey',
opacity: 0.8,
x: stage.width() - PADDING - 10,
y: PADDING,
draggable: true,
dragBoundFunc: function (pos) {
pos.x = stage.width() - PADDING - 10;
pos.y = Math.max(
Math.min(pos.y, stage.height() - this.height() - PADDING),
PADDING
);
return pos;
},
});
scrollLayers.add(verticalBar);
verticalBar.on('dragmove', function () {
// delta in %
const availableHeight =
stage.height() - PADDING * 2 - verticalBar.height();
const delta = (verticalBar.y() - PADDING) / availableHeight;
layer.y(-(HEIGHT - stage.height()) * delta);
});
const horizontalBar = new Konva.Rect({
width: 100,
height: 10,
fill: 'grey',
opacity: 0.8,
x: PADDING,
y: stage.height() - PADDING - 10,
draggable: true,
dragBoundFunc: function (pos) {
pos.x = Math.max(
Math.min(pos.x, stage.width() - this.width() - PADDING),
PADDING
);
pos.y = stage.height() - PADDING - 10;
return pos;
},
});
scrollLayers.add(horizontalBar);
horizontalBar.on('dragmove', function () {
// delta in %
const availableWidth =
stage.width() - PADDING * 2 - horizontalBar.width();
const delta = (horizontalBar.x() - PADDING) / availableWidth;
layer.x(-(WIDTH - stage.width()) * delta);
});
stage.on('wheel', function (e) {
// prevent parent scrolling
e.evt.preventDefault();
const dx = e.evt.deltaX;
const dy = e.evt.deltaY;
const minX = -(WIDTH - stage.width());
const maxX = 0;
const x = Math.max(minX, Math.min(layer.x() - dx, maxX));
const minY = -(HEIGHT - stage.height());
const maxY = 0;
const y = Math.max(minY, Math.min(layer.y() - dy, maxY));
layer.position({ x, y });
const availableHeight =
stage.height() - PADDING * 2 - verticalBar.height();
const vy =
(layer.y() / (-HEIGHT + stage.height())) * availableHeight + PADDING;
verticalBar.y(vy);
const availableWidth =
stage.width() - PADDING * 2 - horizontalBar.width();
const hx =
(layer.x() / (-WIDTH + stage.width())) * availableWidth + PADDING;
horizontalBar.x(hx);
});
```
```js
import React from 'react';
import { Stage, Layer, Circle, Rect } from 'react-konva';
const WIDTH = 3000;
const HEIGHT = 3000;
const NUMBER = 200;
const PADDING = 5;
const App = () => {
const [nodes] = React.useState(() =>
Array(NUMBER).fill().map(() => ({
x: WIDTH * Math.random(),
y: HEIGHT * Math.random(),
}))
);
const [position, setPosition] = React.useState({ x: 0, y: 0 });
const [scrollBars, setScrollBars] = React.useState(() => ({
vertical: { x: window.innerWidth - PADDING - 10, y: PADDING },
horizontal: { x: PADDING, y: window.innerHeight - PADDING - 10 }
}));
const handleVerticalDrag = (e) => {
const pos = e.target.position();
const availableHeight = window.innerHeight - PADDING * 2 - 100;
const delta = (pos.y - PADDING) / availableHeight;
setPosition(prev => ({ ...prev, y: -(HEIGHT - window.innerHeight) * delta }));
setScrollBars(prev => ({ ...prev, vertical: pos }));
};
const handleHorizontalDrag = (e) => {
const pos = e.target.position();
const availableWidth = window.innerWidth - PADDING * 2 - 100;
const delta = (pos.x - PADDING) / availableWidth;
setPosition(prev => ({ ...prev, x: -(WIDTH - window.innerWidth) * delta }));
setScrollBars(prev => ({ ...prev, horizontal: pos }));
};
const handleWheel = (e) => {
e.evt.preventDefault();
const dx = e.evt.deltaX;
const dy = e.evt.deltaY;
const minX = -(WIDTH - window.innerWidth);
const maxX = 0;
const x = Math.max(minX, Math.min(position.x - dx, maxX));
const minY = -(HEIGHT - window.innerHeight);
const maxY = 0;
const y = Math.max(minY, Math.min(position.y - dy, maxY));
setPosition({ x, y });
const availableHeight = window.innerHeight - PADDING * 2 - 100;
const vy = (y / (-HEIGHT + window.innerHeight)) * availableHeight + PADDING;
const availableWidth = window.innerWidth - PADDING * 2 - 100;
const hx = (x / (-WIDTH + window.innerWidth)) * availableWidth + PADDING;
setScrollBars({
vertical: { x: window.innerWidth - PADDING - 10, y: vy },
horizontal: { x: hx, y: window.innerHeight - PADDING - 10 }
});
};
return (
{nodes.map((node, i) => (
))}
({
x: window.innerWidth - PADDING - 10,
y: Math.max(
Math.min(pos.y, window.innerHeight - 100 - PADDING),
PADDING
),
})}
/>
({
x: Math.max(
Math.min(pos.x, window.innerWidth - 100 - PADDING),
PADDING
),
y: window.innerHeight - PADDING - 10,
})}
/>
);
};
export default App;
```
```js
```
### 4. Emulate screen moving with transform
That demo works really good, but it may be tricky.
The idea is:
- We will use small canvas with the size of the screen
- We will create container with required size (3000x3000), so native scrollbars will be visible
- When user is trying to scroll, we will apply css transform for the stage container so it will be still in the center of user's screen
- We will move all nodes so it looks like you scroll (by changing stage position)
Props:
- Works perfect and fast
- Native scrolling
Cons:
- You have to understand what is going on.
**Instructions**: try to scroll with native bars.
```js
import Konva from 'konva';
// First we need to add required CSS
const style = document.createElement('style');
style.textContent = `
#large-container {
width: 3000px;
height: 3000px;
overflow: hidden;
}
#scroll-container {
width: calc(100% - 22px);
height: calc(100vh - 22px);
overflow: auto;
margin: 10px;
border: 1px solid grey;
}
`;
document.head.appendChild(style);
// Then create required DOM structure
const scrollContainer = document.createElement('div');
scrollContainer.id = 'scroll-container';
const largeContainer = document.createElement('div');
largeContainer.id = 'large-container';
const container = document.createElement('div');
container.id = 'stage-container';
scrollContainer.appendChild(largeContainer);
largeContainer.appendChild(container);
document.body.appendChild(scrollContainer);
const WIDTH = 3000;
const HEIGHT = 3000;
const NUMBER = 200;
// padding will increase the size of stage
// so scrolling will look smoother
const PADDING = 200;
const stage = new Konva.Stage({
container: 'stage-container',
width: window.innerWidth + PADDING * 2,
height: window.innerHeight + PADDING * 2,
});
const layer = new Konva.Layer();
stage.add(layer);
function generateNode() {
return new Konva.Circle({
x: WIDTH * Math.random(),
y: HEIGHT * Math.random(),
radius: 50,
fill: 'red',
stroke: 'black',
});
}
for (let i = 0; i < NUMBER; i++) {
layer.add(generateNode());
}
function repositionStage() {
const dx = scrollContainer.scrollLeft - PADDING;
const dy = scrollContainer.scrollTop - PADDING;
stage.container().style.transform =
'translate(' + dx + 'px, ' + dy + 'px)';
stage.x(-dx);
stage.y(-dy);
}
scrollContainer.addEventListener('scroll', repositionStage);
repositionStage();
```
```js
import React from 'react';
import { Stage, Layer, Circle } from 'react-konva';
const WIDTH = 3000;
const HEIGHT = 3000;
const NUMBER = 200;
const PADDING = 500;
const App = () => {
const [nodes] = React.useState(() =>
Array(NUMBER).fill().map(() => ({
x: WIDTH * Math.random(),
y: HEIGHT * Math.random(),
}))
);
const [position, setPosition] = React.useState({ x: 0, y: 0 });
const scrollContainerRef = React.useRef(null);
const containerRef = React.useRef(null);
React.useEffect(() => {
const scrollContainer = scrollContainerRef.current;
if (!scrollContainer) return;
function repositionStage() {
const dx = scrollContainer.scrollLeft - PADDING;
const dy = scrollContainer.scrollTop - PADDING;
if (containerRef.current) {
containerRef.current.style.transform = `translate(${dx}px, ${dy}px)`;
}
setPosition({ x: -dx, y: -dy });
}
scrollContainer.addEventListener('scroll', repositionStage);
repositionStage();
return () => {
scrollContainer.removeEventListener('scroll', repositionStage);
};
}, []);
return (
{nodes.map((node, i) => (
))}
);
};
export default App;
```
```js
```
---
# Canvas Sticker — Add Drag and Drop Stickers to Images with JavaScript
> Add stickers to images on HTML5 Canvas with JavaScript. Drag, drop, resize, and rotate decorative shapes on photos using Konva.js. Interactive sticker demo with export.
Source: https://konvajs.org/docs/sandbox/Canvas_Sticker.html
Place stickers on an image — stars, hearts, badges, and arrows. Click to add, drag to move, use handles to resize and rotate. Export the result as PNG.
**Instructions:** Click a sticker button to add it. Click a sticker to select and transform it. Click the image to deselect.
```js
import Konva from 'konva';
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
var layer = new Konva.Layer();
stage.add(layer);
var container = document.getElementById('container');
var controls = document.createElement('div');
controls.style.cssText = 'display:flex;gap:8px;margin-bottom:8px;flex-wrap:wrap;align-items:center;';
var stickerTypes = [
{ name: 'Star', emoji: '⭐' },
{ name: 'Heart', emoji: '❤️' },
{ name: 'Badge', emoji: '🔴' },
{ name: 'Arrow', emoji: '➡️' },
];
stickerTypes.forEach(function(s) {
var btn = document.createElement('button');
btn.textContent = s.emoji + ' ' + s.name;
btn.style.cssText = 'padding:6px 12px;cursor:pointer;border:1px solid #ddd;border-radius:4px;background:#f8f9fa;font-size:13px;';
btn.onclick = function() { addSticker(s.name); };
controls.appendChild(btn);
});
var exportBtn = document.createElement('button');
exportBtn.textContent = 'Export PNG';
exportBtn.style.cssText = 'padding:6px 12px;cursor:pointer;border:none;border-radius:4px;background:#333;color:white;font-size:13px;font-weight:600;';
exportBtn.onclick = function() {
transformer.nodes([]);
var link = document.createElement('a');
link.href = stage.toDataURL({ pixelRatio: 2 });
link.download = 'sticker-canvas.png';
link.click();
};
controls.appendChild(exportBtn);
container.parentNode.insertBefore(controls, container);
var imageObj = new Image();
imageObj.onload = function() {
layer.add(new Konva.Image({ image: imageObj, width: width, height: height }));
layer.add(transformer);
};
imageObj.src = 'https://konvajs.org/assets/landscape.jpg';
var transformer = new Konva.Transformer();
function addSticker(type) {
var shape;
var x = 100 + Math.random() * (width - 200);
var y = 80 + Math.random() * (height - 160);
if (type === 'Star') {
shape = new Konva.Star({ x: x, y: y, numPoints: 5, innerRadius: 15, outerRadius: 35, fill: '#FFD700', stroke: '#FFA500', strokeWidth: 2, draggable: true });
} else if (type === 'Heart') {
shape = new Konva.Path({ x: x, y: y, data: 'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z', scaleX: 2, scaleY: 2, fill: '#FF69B4', draggable: true });
} else if (type === 'Badge') {
shape = new Konva.Circle({ x: x, y: y, radius: 25, fill: '#FF4444', stroke: '#fff', strokeWidth: 3, draggable: true });
} else if (type === 'Arrow') {
shape = new Konva.Arrow({ x: x, y: y, points: [0, 0, 60, 0], fill: '#20C997', stroke: '#20C997', strokeWidth: 3, pointerLength: 12, pointerWidth: 12, draggable: true });
}
if (shape) {
layer.add(shape);
shape.moveToTop();
transformer.moveToTop();
transformer.nodes([shape]);
}
}
stage.on('click tap', function(e) {
if (e.target === stage || e.target.getClassName() === 'Image') {
transformer.nodes([]);
return;
}
if (e.target.getParent() && e.target.getParent().getClassName() === 'Transformer') return;
transformer.nodes([e.target]);
});
```
```js
import { Stage, Layer, Image as KonvaImage, Star, Circle, Arrow, Path, Transformer } from 'react-konva';
import { useRef, useState, useEffect } from 'react';
var App = function() {
var width = window.innerWidth;
var height = window.innerHeight;
var stageRef = useRef(null);
var trRef = useRef(null);
var [selectedId, setSelectedId] = useState(null);
var [image, setImage] = useState(null);
var [shapes, setShapes] = useState([]);
var shapeRefs = useRef({});
var nextId = useRef(1);
useEffect(function() {
var img = new window.Image();
img.onload = function() { setImage(img); };
img.src = 'https://konvajs.org/assets/landscape.jpg';
}, []);
useEffect(function() {
if (selectedId && trRef.current && shapeRefs.current[selectedId]) {
trRef.current.nodes([shapeRefs.current[selectedId]]);
trRef.current.getLayer().batchDraw();
} else if (trRef.current) {
trRef.current.nodes([]);
trRef.current.getLayer().batchDraw();
}
}, [selectedId]);
var addSticker = function(type) {
var id = 'shape_' + nextId.current++;
var initialScale = type === 'heart' ? 2 : 1;
setShapes(function(prev) {
return prev.concat([{
id: id,
type: type,
x: 100 + Math.random() * (width - 200),
y: 80 + Math.random() * (height - 160),
rotation: 0,
scaleX: initialScale,
scaleY: initialScale,
}]);
});
setSelectedId(id);
};
var updateShape = function(id, patch) {
setShapes(function(currentShapes) {
return currentShapes.map(function(shape) {
return shape.id === id ? Object.assign({}, shape, patch) : shape;
});
});
};
var handleExport = function() {
setSelectedId(null);
setTimeout(function() {
var link = document.createElement('a');
link.href = stageRef.current.toDataURL({ pixelRatio: 2 });
link.download = 'sticker-canvas.png';
link.click();
}, 100);
};
var handleStageClick = function(e) {
if (e.target === e.target.getStage() || e.target.getClassName() === 'Image') {
setSelectedId(null);
}
};
var stickerButtons = [
{ emoji: '⭐', name: 'Star', type: 'star' },
{ emoji: '❤️', name: 'Heart', type: 'heart' },
{ emoji: '🔴', name: 'Badge', type: 'badge' },
{ emoji: '➡️', name: 'Arrow', type: 'arrow' },
];
return (
{stickerButtons.map(function(s) {
return {s.emoji} {s.name} ;
})}
Export PNG
{image && }
{shapes.map(function(item) {
var common = {
ref: function(node) { shapeRefs.current[item.id] = node; },
x: item.x,
y: item.y,
rotation: item.rotation,
scaleX: item.scaleX,
scaleY: item.scaleY,
draggable: true,
onClick: function() { setSelectedId(item.id); },
onTap: function() { setSelectedId(item.id); },
onDragEnd: function(e) {
updateShape(item.id, e.target.position());
},
onTransformEnd: function(e) {
updateShape(item.id, {
x: e.target.x(),
y: e.target.y(),
rotation: e.target.rotation(),
scaleX: e.target.scaleX(),
scaleY: e.target.scaleY(),
});
},
};
if (item.type === 'star') return ;
if (item.type === 'heart') return ;
if (item.type === 'badge') return ;
if (item.type === 'arrow') return ;
return null;
})}
);
};
export default App;
```
```js
{{ s.emoji }} {{ s.name }}
Export PNG
```
---
# Canvas Watermark — Add Text Watermark to Images with JavaScript
> Add text watermarks to images on HTML5 Canvas with JavaScript. Customize watermark text, opacity, size, and rotation using Konva.js. Export watermarked images as PNG.
Source: https://konvajs.org/docs/sandbox/Canvas_Watermark.html
Add a repeating diagonal watermark to any image. Type custom text, adjust opacity and font size, then export as PNG.
**Instructions:** Edit the text field, adjust sliders, click Export to download.
```js
import Konva from 'konva';
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
var layer = new Konva.Layer();
stage.add(layer);
var container = document.getElementById('container');
var controls = document.createElement('div');
controls.style.cssText = 'display:flex;gap:8px;align-items:center;margin-bottom:6px;flex-wrap:wrap;font:12px Arial,sans-serif;';
controls.appendChild(document.createTextNode('Text:'));
var textInput = document.createElement('input');
textInput.type = 'text';
textInput.value = 'SAMPLE';
textInput.style.cssText = 'padding:3px 5px;width:90px;font-size:12px;';
controls.appendChild(textInput);
var opacityLabel = document.createElement('label');
opacityLabel.style.cssText = 'display:flex;align-items:center;gap:4px;';
opacityLabel.appendChild(document.createTextNode('Opacity:'));
var opacitySlider = document.createElement('input');
opacitySlider.type = 'range';
opacitySlider.min = '0.05';
opacitySlider.max = '1';
opacitySlider.step = '0.05';
opacitySlider.value = '0.3';
opacitySlider.style.width = '70px';
opacityLabel.appendChild(opacitySlider);
var opacityVal = document.createElement('span');
opacityVal.textContent = '0.3';
opacityVal.style.minWidth = '24px';
opacityLabel.appendChild(opacityVal);
controls.appendChild(opacityLabel);
var sizeLabel = document.createElement('label');
sizeLabel.style.cssText = 'display:flex;align-items:center;gap:4px;';
sizeLabel.appendChild(document.createTextNode('Size:'));
var sizeSlider = document.createElement('input');
sizeSlider.type = 'range';
sizeSlider.min = '14';
sizeSlider.max = '60';
sizeSlider.step = '2';
sizeSlider.value = '28';
sizeSlider.style.width = '70px';
sizeLabel.appendChild(sizeSlider);
var sizeVal = document.createElement('span');
sizeVal.textContent = '28';
sizeVal.style.minWidth = '20px';
sizeLabel.appendChild(sizeVal);
controls.appendChild(sizeLabel);
var exportBtn = document.createElement('button');
exportBtn.textContent = 'Export PNG';
exportBtn.style.cssText = 'padding:4px 10px;cursor:pointer;font-size:12px;background:#333;color:white;border:none;border-radius:3px;';
controls.appendChild(exportBtn);
container.parentNode.insertBefore(controls, container);
var watermarkGroup = new Konva.Group();
var imageObj = new Image();
imageObj.onload = function() {
var bgImage = new Konva.Image({
image: imageObj,
width: width,
height: height,
});
layer.add(bgImage);
layer.add(watermarkGroup);
drawWatermarks();
};
imageObj.src = 'https://konvajs.org/assets/landscape.jpg';
function drawWatermarks() {
watermarkGroup.destroyChildren();
var text = textInput.value || 'SAMPLE';
var opacity = parseFloat(opacitySlider.value);
var size = parseInt(sizeSlider.value);
for (var x = -width; x < width * 2; x += 180) {
for (var y = -height; y < height * 2; y += 120) {
watermarkGroup.add(new Konva.Text({
x: x, y: y,
text: text,
fontSize: size,
fontFamily: 'Arial',
fill: 'white',
opacity: opacity,
rotation: -30,
}));
}
}
}
textInput.addEventListener('input', drawWatermarks);
opacitySlider.addEventListener('input', function() {
opacityVal.textContent = parseFloat(this.value).toFixed(2);
drawWatermarks();
});
sizeSlider.addEventListener('input', function() {
sizeVal.textContent = this.value;
drawWatermarks();
});
exportBtn.addEventListener('click', function() {
var link = document.createElement('a');
link.href = stage.toDataURL({ pixelRatio: 2 });
link.download = 'watermarked.png';
link.click();
});
```
```js
import { Stage, Layer, Image, Text, Group } from 'react-konva';
import { useState, useRef, useEffect } from 'react';
var App = function() {
var stageRef = useRef(null);
var [text, setText] = useState('SAMPLE');
var [opacity, setOpacity] = useState(0.3);
var [fontSize, setFontSize] = useState(28);
var [image, setImage] = useState(null);
var width = window.innerWidth;
var height = window.innerHeight;
useEffect(function() {
var img = new window.Image();
img.onload = function() { setImage(img); };
img.src = 'https://konvajs.org/assets/landscape.jpg';
}, []);
var positions = [];
for (var x = -width; x < width * 2; x += 180) {
for (var y = -height; y < height * 2; y += 120) {
positions.push({ x: x, y: y });
}
}
var handleExport = function() {
var link = document.createElement('a');
link.href = stageRef.current.toDataURL({ pixelRatio: 2 });
link.download = 'watermarked.png';
link.click();
};
return (
);
};
export default App;
```
```js
```
---
# Canvas to PDF — Export HTML5 Canvas as PDF with JavaScript
> Export HTML5 Canvas to PDF with JavaScript using Konva.js and jsPDF. Save canvas drawings as high-quality PDF files with selectable text. Full working demo and source code.
Source: https://konvajs.org/docs/sandbox/Canvas_to_PDF.html
Export any HTML5 Canvas content to a PDF document using JavaScript. This demo shows how to convert a Konva stage into a downloadable PDF file using [jsPDF](https://parall.ax/products/jspdf), with support for high-quality rendering and selectable text.
The approach works in four steps: generate your canvas content, export the canvas as an image, insert that image into a PDF document, and save. Two tips to get the best results:
**High-quality export:** Use the `pixelRatio` attribute when converting canvas to an image — see the [High Quality Export guide](/docs/data_and_serialization/High-Quality-Export.html) for details.
**Selectable text in PDF:** Even though the canvas is added as an image, you can insert text nodes manually into the PDF layer beneath it. The text won't be visible (it sits behind the image) but remains selectable and searchable. Text rendering in PDF differs from Konva's, so complex styles may need adjustment.
**Instructions: view the canvas below, then click the button to save it as a PDF.**
```js
import Konva from 'konva';
import { jsPDF } from 'jspdf';
// Create a button for PDF export
const saveButton = document.createElement('button');
saveButton.textContent = 'Save as PDF';
saveButton.style.position = 'absolute';
saveButton.style.top = '5px';
saveButton.style.left = '5px';
document.body.appendChild(saveButton);
// Create a stage
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
// Add background
const back = new Konva.Rect({
width: stage.width(),
height: stage.height(),
fill: 'rgba(200, 200, 200)',
});
layer.add(back);
// Add text with blur effect
const text = new Konva.Text({
text: 'This is the Darth Vader',
x: 15,
y: 40,
rotation: -10,
filters: [Konva.Filters.Blur],
blurRadius: 4,
fontSize: 18,
});
text.cache();
layer.add(text);
// Add arrow
const arrow = new Konva.Arrow({
points: [70, 50, 100, 80, 150, 100, 190, 100],
tension: 0.5,
stroke: 'black',
fill: 'black',
});
layer.add(arrow);
// Add image
const imageUrl = 'https://konvajs.org/assets/darth-vader.jpg';
Konva.Image.fromURL(
imageUrl,
function (darthNode) {
darthNode.setAttrs({
x: 200,
y: 50,
scaleX: 0.5,
scaleY: 0.5,
});
layer.add(darthNode);
},
function () {
console.error('Failed to load image');
}
);
// Handle PDF export
saveButton.addEventListener('click', function () {
const pdf = new jsPDF({
orientation: 'landscape',
unit: 'px',
format: [stage.width(), stage.height()],
hotfixes: ['px_scaling'],
});
pdf.setTextColor('#000000');
// First add texts
stage.find('Text').forEach((text) => {
const size = text.fontSize() * 0.75; // convert pixels to points
pdf.setFontSize(size);
pdf.text(text.text(), text.x(), text.y(), {
baseline: 'top',
angle: -text.getAbsoluteRotation(),
});
});
// Then put image on top of texts (so texts are not visible)
pdf.addImage(
stage.toDataURL({ pixelRatio: 2 }),
0,
0,
stage.width(),
stage.height()
);
pdf.save('canvas.pdf');
});
```
```js
import Konva from 'konva';
import { useEffect, useRef } from 'react';
import { Stage, Layer, Rect, Text, Arrow, Image } from 'react-konva';
import useImage from 'use-image';
import { jsPDF } from 'jspdf';
const App = () => {
const stageRef = useRef(null);
const textRef = useRef(null);
const [darthVaderImage] = useImage('https://konvajs.org/assets/darth-vader.jpg', 'anonymous');
const width = window.innerWidth;
const height = window.innerHeight;
useEffect(() => {
textRef.current?.cache();
}, []);
// Handle PDF export
const handleExport = () => {
if (stageRef.current) {
const stage = stageRef.current;
const pdf = new jsPDF({
orientation: 'landscape',
unit: 'px',
format: [width, height],
hotfixes: ['px_scaling'],
});
pdf.setTextColor('#000000');
// First add texts
stage.find('Text').forEach((text) => {
const size = text.fontSize() * 0.75; // convert pixels to points
pdf.setFontSize(size);
pdf.text(text.text(), text.x(), text.y(), {
baseline: 'top',
angle: -text.getAbsoluteRotation(),
});
});
// Then put image on top of texts (so texts are not visible)
pdf.addImage(
stage.toDataURL({ pixelRatio: 2 }),
0,
0,
width,
height
);
pdf.save('canvas.pdf');
} else {
console.error('Stage is not available');
}
};
return (
Save as PDF
{darthVaderImage && (
)}
);
};
export default App;
```
```js
Save as PDF
```
## Print output
The method above places a raster image into an RGB document. A commercial
printer usually asks for CMYK separation, PDF/X compliance, bleed, and crop
marks, and a browser canvas produces none of them — that needs a render step
outside the browser.
[Polotno](https://polotno.com/?utm_source=konvajs&utm_medium=docs&utm_content=canvas-to-pdf) is a commercial design editor SDK built with
Konva that covers the editor and its export pipeline.
---
# Canvas Collision Detection — Detect Shape Overlaps with JavaScript
> Implement collision detection on HTML5 Canvas with JavaScript using Konva.js. Detect overlapping shapes with bounding box checks on draggable rectangles.
Source: https://konvajs.org/docs/sandbox/Collision_Detection.html
## How to find overlapping objects on the canvas?
In this demo we will use simple collision detection to highlight intersected objects.
For simplicity we will use just bounding boxes to detect collision.
Red borders are used to show bounding boxes.
```js
import Konva from 'konva';
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
var layer = new Konva.Layer();
stage.add(layer);
function createShape() {
var group = new Konva.Group({
x: Math.random() * width,
y: Math.random() * height,
draggable: true,
});
var shape = new Konva.Rect({
width: 30 + Math.random() * 30,
height: 30 + Math.random() * 30,
fill: 'grey',
rotation: 360 * Math.random(),
name: 'fillShape',
});
group.add(shape);
var boundingBox = shape.getClientRect({ relativeTo: group });
var box = new Konva.Rect({
x: boundingBox.x,
y: boundingBox.y,
width: boundingBox.width,
height: boundingBox.height,
stroke: 'red',
strokeWidth: 1,
});
group.add(box);
return group;
}
for (var i = 0; i < 10; i++) {
layer.add(createShape());
}
layer.on('dragmove', function (e) {
var target = e.target;
var targetRect = e.target.getClientRect();
layer.children.forEach(function (group) {
// do not check intersection with itself
if (group === target) {
return;
}
if (haveIntersection(group.getClientRect(), targetRect)) {
group.findOne('.fillShape').fill('red');
} else {
group.findOne('.fillShape').fill('grey');
}
});
});
function haveIntersection(r1, r2) {
return !(
r2.x > r1.x + r1.width ||
r2.x + r2.width < r1.x ||
r2.y > r1.y + r1.height ||
r2.y + r2.height < r1.y
);
}
```
```js
import { Stage, Layer, Group, Rect } from 'react-konva';
import { useState } from 'react';
const createInitialShapes = () => {
const shapes = [];
for (let i = 0; i < 10; i++) {
const width = 30 + Math.random() * 30;
const height = 30 + Math.random() * 30;
const rotation = 360 * Math.random();
// calculate bounding box for rotated rectangle
const radians = (rotation * Math.PI) / 180;
const cos = Math.cos(radians);
const sin = Math.sin(radians);
// calculate corners of the rectangle
const corners = [
{ x: 0, y: 0 },
{ x: width, y: 0 },
{ x: width, y: height },
{ x: 0, y: height }
].map(point => ({
x: point.x * cos - point.y * sin,
y: point.x * sin + point.y * cos
}));
// find bounding box dimensions
const minX = Math.min(...corners.map(p => p.x));
const maxX = Math.max(...corners.map(p => p.x));
const minY = Math.min(...corners.map(p => p.y));
const maxY = Math.max(...corners.map(p => p.y));
shapes.push({
id: i,
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
rotation,
width,
height,
fill: 'grey',
box: {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY
}
});
}
return shapes;
};
const haveIntersection = (r1, r2) => {
return !(
r2.x > r1.x + r1.width ||
r2.x + r2.width < r1.x ||
r2.y > r1.y + r1.height ||
r2.y + r2.height < r1.y
);
};
const App = () => {
const [shapes, setShapes] = useState(createInitialShapes());
const handleDragMove = (e, id) => {
const target = e.target;
const targetRect = target.getClientRect();
setShapes(currentShapes => currentShapes.map(shape => {
if (shape.id === id) {
return {
...shape,
x: target.x(),
y: target.y()
};
}
const shapeGroup = target.parent.parent.findOne(`#group-${shape.id}`);
if (!shapeGroup) return shape;
const isIntersecting = haveIntersection(
shapeGroup.getClientRect(),
targetRect
);
return {
...shape,
fill: isIntersecting ? 'red' : 'grey'
};
}));
};
const handleDragEnd = (e, id) => {
setShapes(currentShapes => currentShapes.map(shape =>
shape.id === id
? { ...shape, x: e.target.x(), y: e.target.y() }
: shape
));
};
return (
{shapes.map((shape) => (
handleDragMove(e, shape.id)}
onDragEnd={(e) => handleDragEnd(e, shape.id)}
>
))}
);
};
export default App;
```
```js
handleDragMove(e, shape.id)"
@dragend="(e) => handleDragEnd(e, shape.id)"
>
```
---
# Keep Labels and Handles the Same Size While Zooming a Canvas
> Keep labels, handles and HUD elements a constant screen size while the Konva stage zooms. Counter-scale a node by 1/scale, or map a whole layer to screen space.
Source: https://konvajs.org/docs/sandbox/Constant_Screen_Size.html
When you scale a stage, everything scales — including the parts you meant as
interface. Labels become unreadable at 20% and enormous at 400%. Drag handles
grow until they cover the shape they are meant to resize.
The fix is to scale those nodes by the inverse of the stage scale, so the two
cancel out.
```js
const s = stage.scaleX();
label.scale({ x: 1 / s, y: 1 / s });
```
The node keeps its position in scene coordinates, so it still travels with the
shape it belongs to. Only its size stops changing.
## When to recompute it
Do it in the same handler that changes the zoom. There is no event called
`scale change` — Konva names attribute events `Change`, so the real one
is `scaleXChange`:
```js
// Correct: fires when scaleX is set.
stage.on('scaleXChange', updateLabels);
// Not a real event. This handler will never run, silently.
stage.on('scale change', updateLabels);
```
Listening to `scaleXChange` works, but calling your update function directly
from the wheel handler is usually simpler, and it keeps the redraw in one place.
## Labels pinned to shapes
Scroll to zoom. The labels stay readable at every level, while the shapes they
belong to scale normally.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: 500,
height: 300,
draggable: true,
});
const layer = new Konva.Layer();
stage.add(layer);
const shapes = [
{ x: 90, y: 90, radius: 45, fill: '#2f6df6', name: 'Alpha' },
{ x: 250, y: 170, radius: 35, fill: '#10784f', name: 'Beta' },
{ x: 390, y: 100, radius: 40, fill: '#d6336c', name: 'Gamma' },
];
const labels = [];
shapes.forEach((s) => {
layer.add(new Konva.Circle({ x: s.x, y: s.y, radius: s.radius, fill: s.fill }));
// The label sits in scene coordinates, so it moves with its circle.
const label = new Konva.Text({
x: s.x,
y: s.y + s.radius + 6,
text: s.name,
fontSize: 14,
fill: '#333',
});
label.offsetX(label.width() / 2);
labels.push(label);
layer.add(label);
});
// Undo the stage scale on every label.
function keepLabelsReadable() {
const s = stage.scaleX();
labels.forEach((label) => label.scale({ x: 1 / s, y: 1 / s }));
}
const hint = new Konva.Text({
x: 10,
y: 10,
text: 'scroll to zoom, drag to pan',
fontSize: 12,
fill: '#888',
});
layer.add(hint);
stage.on('wheel', (e) => {
e.evt.preventDefault();
const oldScale = stage.scaleX();
const pointer = stage.getPointerPosition();
const pointTo = {
x: (pointer.x - stage.x()) / oldScale,
y: (pointer.y - stage.y()) / oldScale,
};
const direction = e.evt.deltaY > 0 ? -1 : 1;
const newScale = Math.max(0.2, Math.min(5, oldScale * (direction > 0 ? 1.08 : 1 / 1.08)));
stage.scale({ x: newScale, y: newScale });
stage.position({
x: pointer.x - pointTo.x * newScale,
y: pointer.y - pointTo.y * newScale,
});
keepLabelsReadable();
});
```
```js
import React, { useState } from 'react';
import { Stage, Layer, Circle, Text } from 'react-konva';
const SHAPES = [
{ x: 90, y: 90, radius: 45, fill: '#2f6df6', name: 'Alpha' },
{ x: 250, y: 170, radius: 35, fill: '#10784f', name: 'Beta' },
{ x: 390, y: 100, radius: 40, fill: '#d6336c', name: 'Gamma' },
];
const App = () => {
const [view, setView] = useState({ scale: 1, x: 0, y: 0 });
const handleWheel = (e) => {
e.evt.preventDefault();
const stage = e.target.getStage();
const oldScale = stage.scaleX();
const pointer = stage.getPointerPosition();
const pointTo = {
x: (pointer.x - stage.x()) / oldScale,
y: (pointer.y - stage.y()) / oldScale,
};
const direction = e.evt.deltaY > 0 ? -1 : 1;
const scale = Math.max(
0.2,
Math.min(5, oldScale * (direction > 0 ? 1.08 : 1 / 1.08))
);
setView({
scale,
x: pointer.x - pointTo.x * scale,
y: pointer.y - pointTo.y * scale,
});
};
const handleDragMove = (e) => {
const { x, y } = e.target.position();
setView((current) => ({ ...current, x, y }));
};
return (
{SHAPES.map((s) => (
))}
{SHAPES.map((s) => (
))}
);
};
export default App;
```
## Stroke width is a special case
A stroke has its own switch, so you do not need to counter-scale for it:
```js
shape.strokeScaleEnabled(false); // the stroke keeps its pixel width when zoomed
```
`Konva.Transformer` anchors are ordinary shapes, so they do scale with the
stage. To keep them a usable size while zooming, drive `anchorSize` from the
scale instead of counter-scaling the transformer:
```js
tr.anchorSize(10 / stage.scaleX());
```
## A layer that ignores the zoom entirely
Counter-scaling keeps a node's *size* fixed while its *position* still follows
the scene. For a HUD — a toolbar, a scale bar, a coordinate readout — you want
both fixed, in screen space.
Every layer is a child of the stage, so it inherits the stage transform. Cancel
the whole transform rather than just the scale:
```js
function pinLayerToScreen(layer) {
const s = stage.scaleX();
layer.scale({ x: 1 / s, y: 1 / s });
layer.position({ x: -stage.x() / s, y: -stage.y() / s });
}
```
That maps layer coordinates back onto screen coordinates: a node at `(10, 10)`
on that layer stays 10 pixels from the top left however the stage is zoomed or
panned. Call it from the same place you call the counter-scale.
Set `listening: false` on a HUD layer if it is purely decorative, so it does not
absorb clicks meant for the scene below.
## Related
- [Zoom relative to pointer](/docs/sandbox/Zooming_Relative_To_Pointer.html) —
the zoom maths used above
- [Infinite canvas](/docs/sandbox/Infinite_Canvas.html) — pan and zoom over an
unbounded scene
- [Canvas minimap](/docs/sandbox/Stage_Preview.html) — a second view of a large
stage
---
# Custom Fonts on HTML5 Canvas — Load Web Fonts and Measure Text Width
> Load custom web fonts on HTML5 canvas with Konva. Fix text measured at the wrong width before the font loads, using the Font Loading API and a cache reset.
Source: https://konvajs.org/docs/sandbox/Custom_Font.html
## How to draw external font on html5 canvas?
If you want to use custom font for `Konva.Text` you just need to:
1. Add font style to your page
2. Set `fontFamily` attribute to required font-face **when font is loaded**
But there is one important thing here. When you set font for DOM elements (like `div` or `span`) browsers will automatically update that elements when font is loaded. But it doesn't work the same for canvas text. You need to redraw canvas again.
**Note:** For older browsers that don't support the native Font Loading API, you can use a width measurement approach - measure text width with fallback font, then periodically check if the custom font width differs (indicating it's loaded).
### Loading font
You may use this async function that combines the native Font Loading API with a reliable width-measurement fallback and timing guard:
```javascript
const loadedFonts = {};
function measureFont(fontName, fallbackFont, fontStyle = 'normal', fontWeight = '400') {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const sampleText = 'The quick brown fox 0123456789';
ctx.font = `${fontStyle} ${fontWeight} 16px '${fontName}', ${fallbackFont}`;
return ctx.measureText(sampleText).width;
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function loadFont(fontName, fontStyle = 'normal', fontWeight = '400') {
if (loadedFonts[fontName]) return;
const hasFontsLoadSupport = !!(document.fonts && document.fonts.load);
const arialWidth = measureFont('Arial', 'Arial', fontStyle, fontWeight);
if (hasFontsLoadSupport) {
try {
await document.fonts.load(`${fontStyle} ${fontWeight} 16px '${fontName}'`);
const newWidth = measureFont(fontName, 'Arial', fontStyle, fontWeight);
const shouldTrustChanges = arialWidth !== newWidth;
if (shouldTrustChanges) {
// Small guard delay to avoid rare race when metrics are not ready yet
await delay(60);
loadedFonts[fontName] = true;
return;
}
} catch (e) {
// ignore and fallback to polling
}
}
const timesWidth = measureFont('Times', 'Times', fontStyle, fontWeight);
const lastWidth = measureFont(fontName, 'Arial', fontStyle, fontWeight);
const waitTime = 60;
const timeout = 6000; // do not wait more than 6 seconds
const attemptsNumber = Math.ceil(timeout / waitTime);
for (let i = 0; i < attemptsNumber; i++) {
const newWidthArial = measureFont(fontName, 'Arial', fontStyle, fontWeight);
const newWidthTimes = measureFont(fontName, 'Times', fontStyle, fontWeight);
const somethingChanged =
newWidthArial !== lastWidth ||
newWidthArial !== arialWidth ||
newWidthTimes !== timesWidth;
if (somethingChanged) {
await delay(60);
loadedFonts[fontName] = true;
return;
}
await delay(waitTime);
}
console.warn(
`Timeout for loading font "${fontName}". Is it a correct font family?`
);
}
```
```js
import Konva from 'konva';
const loadedFonts = {};
function measureFont(fontName, fallbackFont, fontStyle = 'normal', fontWeight = '400') {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const sampleText = 'The quick brown fox 0123456789';
ctx.font = `${fontStyle} ${fontWeight} 16px '${fontName}', ${fallbackFont}`;
return ctx.measureText(sampleText).width;
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function loadFont(fontName, fontStyle = 'normal', fontWeight = '400') {
if (loadedFonts[fontName]) return;
const hasFontsLoadSupport = !!(document.fonts && document.fonts.load);
const arialWidth = measureFont('Arial', 'Arial', fontStyle, fontWeight);
if (hasFontsLoadSupport) {
try {
await document.fonts.load(`${fontStyle} ${fontWeight} 16px '${fontName}'`);
const newWidth = measureFont(fontName, 'Arial', fontStyle, fontWeight);
const shouldTrustChanges = arialWidth !== newWidth;
if (shouldTrustChanges) {
await delay(60);
loadedFonts[fontName] = true;
return;
}
} catch (e) {}
}
const timesWidth = measureFont('Times', 'Times', fontStyle, fontWeight);
const lastWidth = measureFont(fontName, 'Arial', fontStyle, fontWeight);
const waitTime = 60;
const timeout = 6000;
const attemptsNumber = Math.ceil(timeout / waitTime);
for (let i = 0; i < attemptsNumber; i++) {
const newWidthArial = measureFont(fontName, 'Arial', fontStyle, fontWeight);
const newWidthTimes = measureFont(fontName, 'Times', fontStyle, fontWeight);
const somethingChanged =
newWidthArial !== lastWidth ||
newWidthArial !== arialWidth ||
newWidthTimes !== timesWidth;
if (somethingChanged) {
await delay(60);
loadedFonts[fontName] = true;
return;
}
await delay(waitTime);
}
console.warn(`Timeout for loading font "${fontName}".`);
}
// Load the font using a stylesheet link
const fontLink = document.createElement('link');
fontLink.href = 'https://fonts.googleapis.com/css2?family=Kavivanar&display=swap';
fontLink.rel = 'stylesheet';
document.head.appendChild(fontLink);
// Build stage immediately with fallback font
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
var layer = new Konva.Layer();
stage.add(layer);
var text = new Konva.Text({
x: 50,
y: 50,
fontSize: 40,
text: 'A text with custom font.',
width: 250,
fontFamily: 'Arial'
});
layer.add(text);
// Then wait for font to load and apply it
loadFont('Kavivanar', 'normal', '400').then(() => {
text.fontFamily('Kavivanar');
});
```
```js
import { Stage, Layer, Text } from 'react-konva';
import { useState, useEffect } from 'react';
const loadedFonts = {};
function measureFont(fontName, fallbackFont, fontStyle = 'normal', fontWeight = '400') {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const sampleText = 'The quick brown fox 0123456789';
ctx.font = `${fontStyle} ${fontWeight} 16px '${fontName}', ${fallbackFont}`;
return ctx.measureText(sampleText).width;
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function loadFont(fontName, fontStyle = 'normal', fontWeight = '400') {
if (loadedFonts[fontName]) return;
const hasFontsLoadSupport = !!(document.fonts && document.fonts.load);
const arialWidth = measureFont('Arial', 'Arial', fontStyle, fontWeight);
if (hasFontsLoadSupport) {
try {
await document.fonts.load(`${fontStyle} ${fontWeight} 16px '${fontName}'`);
const newWidth = measureFont(fontName, 'Arial', fontStyle, fontWeight);
const shouldTrustChanges = arialWidth !== newWidth;
if (shouldTrustChanges) {
await delay(60);
loadedFonts[fontName] = true;
return;
}
} catch (e) {}
}
const timesWidth = measureFont('Times', 'Times', fontStyle, fontWeight);
const lastWidth = measureFont(fontName, 'Arial', fontStyle, fontWeight);
const waitTime = 60;
const timeout = 6000;
const attemptsNumber = Math.ceil(timeout / waitTime);
for (let i = 0; i < attemptsNumber; i++) {
const newWidthArial = measureFont(fontName, 'Arial', fontStyle, fontWeight);
const newWidthTimes = measureFont(fontName, 'Times', fontStyle, fontWeight);
const somethingChanged =
newWidthArial !== lastWidth ||
newWidthArial !== arialWidth ||
newWidthTimes !== timesWidth;
if (somethingChanged) {
await delay(60);
loadedFonts[fontName] = true;
return;
}
await delay(waitTime);
}
console.warn(`Timeout for loading font "${fontName}".`);
}
const App = () => {
const [fontLoaded, setFontLoaded] = useState(false);
useEffect(() => {
// Load the font using a stylesheet link
const fontLink = document.createElement('link');
fontLink.href = 'https://fonts.googleapis.com/css2?family=Kavivanar&display=swap';
fontLink.rel = 'stylesheet';
document.head.appendChild(fontLink);
// Wait for font to load using combined approach
loadFont('Kavivanar', 'normal', '400').then(() => {
setFontLoaded(true);
});
}, []);
return (
);
};
export default App;
```
```js
```
---
# Drag and Drop Multiple Shapes
> Drag and drop multiple colored rectangles on canvas with double-click to remove shapes using Konva.
Source: https://konvajs.org/docs/sandbox/Drag_and_Drop_Multiple_Shapes.html
**Instructions:** Drag and drop the shapes or remove them by double clicking or double tapping.
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'purple'];
for (let i = 0; i < 6; i++) {
const box = new Konva.Rect({
x: i * 30 + 50,
y: i * 18 + 40,
fill: colors[i],
stroke: 'black',
strokeWidth: 4,
draggable: true,
width: 100,
height: 50,
});
box.on('dragstart', function () {
this.moveToTop();
});
box.on('dragmove', function () {
document.body.style.cursor = 'pointer';
});
// dblclick to remove box for desktop app
// and dbltap to remove box for mobile app
box.on('dblclick dbltap', function () {
this.destroy();
});
box.on('mouseover', function () {
document.body.style.cursor = 'pointer';
});
box.on('mouseout', function () {
document.body.style.cursor = 'default';
});
layer.add(box);
}
// add the layer to the stage
stage.add(layer);
```
```js
import { useState } from 'react';
import { Stage, Layer, Rect } from 'react-konva';
const App = () => {
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'purple'];
// Initialize boxes with proper IDs and positions
const initialBoxes = colors.map((color, i) => ({
id: i.toString(),
x: i * 30 + 50,
y: i * 18 + 40,
width: 100,
height: 50,
fill: color,
stroke: 'black',
strokeWidth: 4
}));
const [boxes, setBoxes] = useState(initialBoxes);
const handleDragStart = (e) => {
// Move the dragged box to the end of the array to simulate moveToTop
const id = e.target.id();
setBoxes(currentBoxes => {
const box = currentBoxes.find(b => b.id === id);
if (!box) return currentBoxes;
return [...currentBoxes.filter(b => b.id !== id), box];
});
};
const handleDragMove = (e) => {
// Update the position of the box
const id = e.target.id();
setBoxes(currentBoxes => currentBoxes.map(box =>
box.id === id
? { ...box, x: e.target.x(), y: e.target.y() }
: box
));
};
const handleDoubleClick = (id) => {
// Remove the box on double click
setBoxes(currentBoxes => currentBoxes.filter(box => box.id !== id));
};
return (
{boxes.map((box) => (
handleDoubleClick(box.id)}
onDblTap={() => handleDoubleClick(box.id)}
onMouseOver={(e) => {
document.body.style.cursor = 'pointer';
}}
onMouseOut={(e) => {
document.body.style.cursor = 'default';
}}
/>
))}
);
};
export default App;
```
```js
```
---
# Drag and Drop Stress Test with 10,000 Shapes
> Stress test dragging and dropping 10,000 shapes using a separate drag layer for smooth performance in Konva.
Source: https://konvajs.org/docs/sandbox/Drag_and_Drop_Stress_Test.html
This example demonstrates a stress test with 10,000 shapes. For simplicity, we're using just two layers - one main layer for all shapes and one dedicated drag layer. When we drag a shape, it's moved to the separate drag layer to ensure smooth movement.
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
// Create stage
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
// Create main layer for all shapes
const mainLayer = new Konva.Layer();
// Create a dedicated layer for dragging
const dragLayer = new Konva.Layer();
// Define colors for random shapes
const colors = [
'red',
'orange',
'yellow',
'green',
'blue',
'cyan',
'purple',
];
let colorIndex = 0;
// Helper function to add a circle to a layer
function addCircle(layer) {
const color = colors[colorIndex++];
if (colorIndex >= colors.length) {
colorIndex = 0;
}
const randX = Math.random() * stage.width();
const randY = Math.random() * stage.height();
const circle = new Konva.Circle({
x: randX,
y: randY,
radius: 6,
fill: color,
});
layer.add(circle);
}
// Create 10,000 circles on the main layer
for (let n = 0; n < 10000; n++) {
addCircle(mainLayer);
}
// Add the main layer and drag layer to the stage
stage.add(mainLayer);
stage.add(dragLayer);
// Setup drag and drop behavior
stage.on('mousedown', function (evt) {
const circle = evt.target;
// Only handle circle shapes (ignore clicks on empty space)
if (!circle || circle.getClassName() !== 'Circle') {
return;
}
// Move the circle to the drag layer
circle.moveTo(dragLayer);
circle.startDrag();
});
// When dragging stops, move the circle back to the main layer
stage.on('mouseup', function (evt) {
const circle = evt.target;
// Only handle circle shapes
if (!circle || circle.getClassName() !== 'Circle') {
return;
}
// Move the circle back to the main layer
circle.moveTo(mainLayer);
});
```
```js
import { useState, useEffect, useRef } from 'react';
import { Stage, Layer, Circle } from 'react-konva';
const COLORS = ['red', 'orange', 'yellow', 'green', 'blue', 'cyan', 'purple'];
const SHAPE_COUNT = 10000;
const App = () => {
// State to hold all the circles data
const [circles, setCircles] = useState([]);
// Refs to layers
const mainLayerRef = useRef(null);
const dragLayerRef = useRef(null);
// Initialize circles data
useEffect(() => {
const circlesData = [];
// Create 10,000 circles
for (let i = 0; i < SHAPE_COUNT; i++) {
circlesData.push({
id: i,
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
radius: 6,
fill: COLORS[i % COLORS.length]
});
}
setCircles(circlesData);
}, []);
// This is not the typical "React way" of managing components.
// In a more React-friendly approach, we would update state and let React handle the DOM.
// However, for this performance demo, we're directly manipulating the nodes
// to match the vanilla JS implementation.
const handleDragStart = (e) => {
const target = e.target;
// Move the circle to the drag layer
target.moveTo(dragLayerRef.current);
};
const handleDragEnd = (e) => {
const target = e.target;
const id = target.id();
const { x, y } = target.position();
// Move the circle back to the main layer
target.moveTo(mainLayerRef.current);
// Store the final position without re-rendering 10,000 nodes on each move.
setCircles(currentCircles => currentCircles.map(circle =>
String(circle.id) === String(id) ? { ...circle, x, y } : circle
));
};
return (
{/* Main layer for all circles */}
{circles.map(circle => (
))}
{/* Empty drag layer that will receive circles during drag */}
);
};
export default App;
```
```js
```
---
# How to drag and drop DOM image into the canvas
> Drag and drop external DOM images onto an HTML5 canvas stage using HTML5 drag-and-drop API with Konva.
Source: https://konvajs.org/docs/sandbox/Drop_DOM_Element.html
In this demo we will demonstrate how drop DOM element that is placed outside of canvas into the stage.
The first image is a DOM image. We can use [HTML drag and drop](https://web.dev/articles/drag-and-drop) to make it draggable.
You will need some extra step if you need to enable drag&drop for DOM element on touch devices. You can read [here](https://mobiforge.com/design-development/touch-friendly-drag-and-drop) for more info.
**Instructions:** drag&drop yoda into the canvas.
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
// Add DOM elements to render outside the container
document.getElementById('container').insertAdjacentHTML(
'beforebegin',
`
Drag&drop yoda into the grey area.
`
);
// Style the container with grey background
document.getElementById('container').style.backgroundColor = 'rgba(0, 0, 0, 0.1)';
// Create stage and layer
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height - 150, // leave space for the DOM elements
});
const layer = new Konva.Layer();
stage.add(layer);
// Track URL of the dragging element
let itemURL = '';
document
.getElementById('drag-items')
.addEventListener('dragstart', function (e) {
itemURL = e.target.src;
});
// Handle dragover on container
const container = stage.container();
container.addEventListener('dragover', function (e) {
e.preventDefault(); // important - must prevent default behavior
});
// Handle drop on container
container.addEventListener('drop', function (e) {
e.preventDefault();
// Register the pointer position manually since this is a DOM event
stage.setPointersPositions(e);
// Load the image and add it to the layer
Konva.Image.fromURL(itemURL, function (image) {
// Calculate appropriate size based on image dimensions
const img = image.image();
const maxDimension = 100;
let width = img.width;
let height = img.height;
if (width > height) {
height = (height / width) * maxDimension;
width = maxDimension;
} else {
width = (width / height) * maxDimension;
height = maxDimension;
}
image.size({
width: width,
height: height
});
layer.add(image);
image.position(stage.getPointerPosition());
image.draggable(true);
});
});
```
```js
import { useState, useRef, useEffect } from 'react';
import { Stage, Layer, Image } from 'react-konva';
import { useImage } from 'react-konva-utils';
const DragItem = ({ src, onDragStart, onDragEnd }) => {
return (
onDragStart(src)}
onDragEnd={onDragEnd}
/>
);
};
const App = () => {
const [images, setImages] = useState([]);
const stageRef = useRef(null);
const dragImageSrc = useRef('');
const nextImageId = useRef(1);
const handleDragStart = (src) => {
dragImageSrc.current = src;
};
const handleDragSourceEnd = () => {
dragImageSrc.current = '';
};
const handleDragOver = (e) => {
e.preventDefault(); // prevent default behavior
};
const handleDrop = (e) => {
e.preventDefault();
const src = dragImageSrc.current;
dragImageSrc.current = '';
if (!src || !stageRef.current) return;
// Get stage and pointer position
const stage = stageRef.current;
// Register the pointer position manually since this is a DOM event
stage.setPointersPositions(e);
const position = stage.getPointerPosition();
const id = `image-${nextImageId.current++}`;
// Add new image to the list
setImages(currentImages => [
...currentImages,
{
src,
x: position.x,
y: position.y,
id
}
]);
};
const handleImageDragEnd = (id, e) => {
const { x, y } = e.target.position();
setImages(currentImages => currentImages.map(img =>
img.id === id ? { ...img, x, y } : img
));
};
return (
Drag&drop yoda into the grey area.
{images.map((img) => (
handleImageDragEnd(img.id, e)}
/>
))}
);
};
// Separate component for Konva Image with proper loading
const KonvaImage = ({ src, x, y, draggable, onDragEnd }) => {
const [image] = useImage(src);
if (!image) return null;
// Calculate appropriate size
const maxDimension = 100;
let width = image.width;
let height = image.height;
if (width > height) {
height = (height / width) * maxDimension;
width = maxDimension;
} else {
width = (width / height) * maxDimension;
height = maxDimension;
}
return (
);
};
export default App;
```
```js
Drag&drop yoda into the grey area.
```
---
# Text editing in HTML5 canvas with Konva
> Double-click to edit text on HTML5 canvas using a textarea overlay with transform support in Konva.
Source: https://konvajs.org/docs/sandbox/Editable_Text.html
Users cannot edit `Konva.Text` content directly for [many reasons](https://html.spec.whatwg.org/multipage/canvas.html#best-practices). The Canvas API is not designed for text editing.
It is possible to emulate text editing on canvas (by drawing blinking cursor, emulate selection, etc).
Konva has not support for such case. We recommend to edit the user input outside of your canvas with native DOM elements such as `input` or `textarea`.
If you want to enable full rich text editing features see [Rich Text Demo](/docs/sandbox/Rich_Text.html).
**Instructions: Double click on text to edit it. Type something. Press Enter or click outside to save changes.**
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const textNode = new Konva.Text({
text: 'Some text here',
x: 50,
y: 80,
fontSize: 20,
draggable: true,
width: 200,
});
layer.add(textNode);
const tr = new Konva.Transformer({
nodes: [textNode],
enabledAnchors: ['middle-left', 'middle-right'],
boundBoxFunc: function (oldBox, newBox) {
newBox.width = Math.max(30, newBox.width);
return newBox;
},
});
textNode.on('transform', function () {
textNode.setAttrs({
width: textNode.width() * textNode.scaleX(),
scaleX: 1,
});
});
layer.add(tr);
textNode.on('dblclick dbltap', () => {
textNode.hide();
tr.hide();
const textPosition = textNode.absolutePosition();
const stageBox = stage.container().getBoundingClientRect();
const areaPosition = {
x: stageBox.left + textPosition.x,
y: stageBox.top + textPosition.y,
};
const textarea = document.createElement('textarea');
document.body.appendChild(textarea);
textarea.value = textNode.text();
textarea.style.position = 'absolute';
textarea.style.top = areaPosition.y + 'px';
textarea.style.left = areaPosition.x + 'px';
textarea.style.width = textNode.width() - textNode.padding() * 2 + 'px';
textarea.style.height = textNode.height() - textNode.padding() * 2 + 5 + 'px';
textarea.style.fontSize = textNode.fontSize() + 'px';
textarea.style.border = 'none';
textarea.style.padding = '0px';
textarea.style.margin = '0px';
textarea.style.overflow = 'hidden';
textarea.style.background = 'none';
textarea.style.outline = 'none';
textarea.style.resize = 'none';
textarea.style.lineHeight = textNode.lineHeight().toString();
textarea.style.fontFamily = textNode.fontFamily();
textarea.style.transformOrigin = 'left top';
textarea.style.textAlign = textNode.align();
textarea.style.color = textNode.fill().toString();
const rotation = textNode.rotation();
let transform = '';
if (rotation) {
transform += 'rotateZ(' + rotation + 'deg)';
}
transform += 'translateY(-' + 2 + 'px)';
textarea.style.transform = transform;
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 3 + 'px';
textarea.focus();
function removeTextarea() {
textarea.parentNode.removeChild(textarea);
window.removeEventListener('click', handleOutsideClick);
window.removeEventListener('touchstart', handleOutsideClick);
textNode.show();
tr.show();
tr.forceUpdate();
}
function setTextareaWidth(newWidth = 0) {
if (!newWidth) {
newWidth = textNode.placeholder.length * textNode.fontSize();
}
textarea.style.width = newWidth + 'px';
}
textarea.addEventListener('keydown', function (e) {
if (e.key === 'Enter' && !e.shiftKey) {
textNode.text(textarea.value);
removeTextarea();
}
if (e.key === 'Escape') {
removeTextarea();
}
});
textarea.addEventListener('keydown', function () {
const scale = textNode.getAbsoluteScale().x;
setTextareaWidth(textNode.width() * scale);
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + textNode.fontSize() + 'px';
});
function handleOutsideClick(e) {
if (e.target !== textarea) {
textNode.text(textarea.value);
removeTextarea();
}
}
setTimeout(() => {
window.addEventListener('click', handleOutsideClick);
});
});
```
```jsx
import { Stage, Layer, Text, Transformer } from "react-konva";
import { Html } from "react-konva-utils";
import { useEffect, useRef, useState, useCallback } from "react";
const TextArea = ({ textNode, onClose, onChange }) => {
const textareaRef = useRef(null);
useEffect(() => {
if (!textareaRef.current) return;
const textarea = textareaRef.current;
const stage = textNode.getStage();
const textPosition = textNode.position();
const stageBox = stage.container().getBoundingClientRect();
const areaPosition = {
x: textPosition.x,
y: textPosition.y,
};
// Match styles with the text node
textarea.value = textNode.text();
textarea.style.position = "absolute";
textarea.style.top = `${areaPosition.y}px`;
textarea.style.left = `${areaPosition.x}px`;
textarea.style.width = `${textNode.width() - textNode.padding() * 2}px`;
textarea.style.height = `${
textNode.height() - textNode.padding() * 2 + 5
}px`;
textarea.style.fontSize = `${textNode.fontSize()}px`;
textarea.style.border = "none";
textarea.style.padding = "0px";
textarea.style.margin = "0px";
textarea.style.overflow = "hidden";
textarea.style.background = "none";
textarea.style.outline = "none";
textarea.style.resize = "none";
textarea.style.lineHeight = textNode.lineHeight();
textarea.style.fontFamily = textNode.fontFamily();
textarea.style.transformOrigin = "left top";
textarea.style.textAlign = textNode.align();
textarea.style.color = textNode.fill();
const rotation = textNode.rotation();
let transform = "";
if (rotation) {
transform += `rotateZ(${rotation}deg)`;
}
textarea.style.transform = transform;
textarea.style.height = "auto";
textarea.style.height = `${textarea.scrollHeight + 3}px`;
textarea.focus();
const handleOutsideClick = (e) => {
if (e.target !== textarea) {
onChange(textarea.value);
onClose();
}
};
// Add event listeners
const handleKeyDown = (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
onChange(textarea.value);
onClose();
}
if (e.key === "Escape") {
onClose();
}
};
const handleInput = () => {
const scale = textNode.getAbsoluteScale().x;
textarea.style.width = `${textNode.width() * scale}px`;
textarea.style.height = "auto";
textarea.style.height = `${
textarea.scrollHeight + textNode.fontSize()
}px`;
};
textarea.addEventListener("keydown", handleKeyDown);
textarea.addEventListener("input", handleInput);
setTimeout(() => {
window.addEventListener("click", handleOutsideClick);
});
return () => {
textarea.removeEventListener("keydown", handleKeyDown);
textarea.removeEventListener("input", handleInput);
window.removeEventListener("click", handleOutsideClick);
};
}, [textNode, onChange, onClose]);
return (
);
};
const TextEditor = (props) => {
return (
);
};
const EditableText = () => {
const [text, setText] = useState("Some text here");
const [isEditing, setIsEditing] = useState(false);
const [textWidth, setTextWidth] = useState(200);
const textRef = useRef();
const trRef = useRef();
useEffect(() => {
if (trRef.current && textRef.current) {
trRef.current.nodes([textRef.current]);
}
}, [isEditing]);
const handleTextDblClick = useCallback(() => {
setIsEditing(true);
}, []);
const handleTextChange = useCallback((newText) => {
setText(newText);
}, []);
const handleTransform = useCallback((e) => {
const node = textRef.current;
const scaleX = node.scaleX();
const newWidth = node.width() * scaleX;
setTextWidth(newWidth);
node.setAttrs({
width: newWidth,
scaleX: 1,
});
}, []);
return (
{isEditing && (
setIsEditing(false)}
/>
)}
{!isEditing && (
({
...newBox,
width: Math.max(30, newBox.width),
})}
/>
)}
);
};
export default EditableText;
```
```js
```
## What a textarea overlay does not cover
The method above works well when the visitor edits one field at a time. It stops
being sufficient as soon as the text has to behave like a document:
- **Reflow.** The overlay does not re-wrap while the box is resized, so the
editing view and the drawn view disagree during a transform.
- **Font metrics.** A web font that loads after the first paint changes the line
height, so cached measurements and the caret position go stale.
- **Per-character styling.** `Konva.Text` applies one style to the whole node.
Mixed bold, colour, or size inside one paragraph needs several nodes and your
own layout pass.
- **History.** Each keystroke has to be folded into the same undo stack as the
canvas operations, or undo will jump between two separate timelines.
- **Export fidelity.** The exported image is drawn by the canvas, not by the
textarea, so any difference between the two shows up in the output.
At that point you are writing a text engine rather than an editor feature. If
you would rather not,
[Polotno](https://polotno.com/?utm_source=konvajs&utm_medium=docs&utm_content=editable-text)
is a commercial design editor SDK built with Konva that already solves this.
---
# Elastic Stars
> Drag and drop stars with an elastic bounce-back animation on release using Konva tweens and easing.
Source: https://konvajs.org/docs/sandbox/Elastic_Stars.html
**Instructions:** Drag and drop the stars and observe the elastic drop on dragend. Refresh the page to randomize the stars again.
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
let tween = null;
function addStar(layer, stage) {
const scale = Math.random();
const star = new Konva.Star({
x: Math.random() * stage.width(),
y: Math.random() * stage.height(),
numPoints: 5,
innerRadius: 30,
outerRadius: 50,
fill: '#89b717',
opacity: 0.8,
draggable: true,
scale: {
x: scale,
y: scale,
},
rotation: Math.random() * 180,
shadowColor: 'black',
shadowBlur: 10,
shadowOffset: {
x: 5,
y: 5,
},
shadowOpacity: 0.6,
startScale: scale,
});
layer.add(star);
}
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
for (let n = 0; n < 10; n++) {
addStar(layer, stage);
}
stage.add(layer);
stage.on('dragstart', function (evt) {
const shape = evt.target;
if (tween) {
tween.pause();
}
shape.setAttrs({
shadowOffset: {
x: 15,
y: 15,
},
scale: {
x: shape.getAttr('startScale') * 1.2,
y: shape.getAttr('startScale') * 1.2,
},
});
});
stage.on('dragend', function (evt) {
const shape = evt.target;
tween = new Konva.Tween({
node: shape,
duration: 0.5,
easing: Konva.Easings.ElasticEaseOut,
scaleX: shape.getAttr('startScale'),
scaleY: shape.getAttr('startScale'),
shadowOffsetX: 5,
shadowOffsetY: 5,
});
tween.play();
});
```
```js
import { useState, useEffect } from 'react';
import { Stage, Layer, Star } from 'react-konva';
const App = () => {
const [stars, setStars] = useState([]);
// Generate initial stars
useEffect(() => {
const initialStars = [];
for (let n = 0; n < 10; n++) {
const scale = Math.random();
initialStars.push({
id: n.toString(),
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
numPoints: 5,
innerRadius: 30,
outerRadius: 50,
fill: '#89b717',
opacity: 0.8,
rotation: Math.random() * 180,
shadowColor: 'black',
shadowBlur: 10,
shadowOffset: {
x: 5,
y: 5,
},
shadowOpacity: 0.6,
scale: {
x: scale,
y: scale,
},
startScale: scale
});
}
setStars(initialStars);
}, []);
const handleDragStart = (e) => {
const id = e.target.id();
setStars(currentStars => currentStars.map(star =>
star.id === id
? {
...star,
shadowOffset: { x: 15, y: 15 },
scale: {
x: star.startScale * 1.2,
y: star.startScale * 1.2,
},
}
: star
));
};
const handleDragEnd = (e) => {
const id = e.target.id();
const { x, y } = e.target.position();
setStars(currentStars => currentStars.map(star =>
star.id === id
? {
...star,
x,
y,
shadowOffset: { x: 5, y: 5 },
scale: { x: star.startScale, y: star.startScale },
}
: star
));
};
return (
{stars.map(star => (
))}
);
};
export default App;
```
```js
```
---
# Expand Image on Hover
> Scale up images smoothly on mouse hover with draggable support using Konva event delegation.
Source: https://konvajs.org/docs/sandbox/Expand_Images_on_Hover.html
This demo shows how to create an effect where images expand when the mouse hovers over them. The images are also draggable.
**Instructions:** Hover your mouse over the images to see them expand.
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
// Create Darth Vader image
const darthVaderImg = new Konva.Image({
x: 110,
y: 88,
width: 200,
height: 137,
offset: {
x: 100,
y: 68,
},
draggable: true,
});
layer.add(darthVaderImg);
// Create Yoda image
const yodaImg = new Konva.Image({
x: 290,
y: 70,
width: 93,
height: 104,
offset: {
x: 46,
y: 52,
},
draggable: true,
});
layer.add(yodaImg);
// Load Darth Vader image
const imageObj1 = new Image();
imageObj1.onload = function () {
darthVaderImg.image(imageObj1);
};
imageObj1.src = 'https://konvajs.org/assets/darth-vader.jpg';
// Load Yoda image
const imageObj2 = new Image();
imageObj2.onload = function () {
yodaImg.image(imageObj2);
};
imageObj2.src = 'https://konvajs.org/assets/yoda.jpg';
// Use event delegation to update pointer style and apply scaling
layer.on('mouseover', function (evt) {
const shape = evt.target;
document.body.style.cursor = 'pointer';
// Scale up the image on hover
shape.to({
scaleX: 1.2,
scaleY: 1.2,
duration: 0.2,
});
});
layer.on('mouseout', function (evt) {
const shape = evt.target;
document.body.style.cursor = 'default';
// Scale back to normal when mouse leaves
shape.to({
scaleX: 1,
scaleY: 1,
duration: 0.2,
});
});
```
```js
import { useState, useEffect } from 'react';
import { Stage, Layer, Image } from 'react-konva';
import { useImage } from 'react-konva-utils';
const ImageWithHover = ({ src, x, y, width, height, offsetX, offsetY }) => {
const [image] = useImage(src);
const [isHovered, setIsHovered] = useState(false);
const [position, setPosition] = useState({ x, y });
const scale = isHovered ? 1.2 : 1;
return (
setPosition(e.target.position())}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
/>
);
};
const App = () => {
return (
);
};
export default App;
```
```js
```
---
# How to animate GIF on Canvas
> Display and animate GIF images on HTML5 canvas by parsing frames with the gifler library and rendering via Konva.
Source: https://konvajs.org/docs/sandbox/GIF_On_Canvas.html
## How to show animated GIF on canvas?
You can't directly insert GIF image into the canvas. But we can use external library to parse the gif and then we can draw it into the layer as `Konva.Image` shape.
This demo uses [gifler](https://themadcreator.github.io/gifler/) to parse and draw the GIF. You can use a different library.
```js
import Konva from 'konva';
// Load gifler library
const script = document.createElement('script');
script.src = 'https://unpkg.com/gifler@0.1.0/gifler.min.js';
document.head.appendChild(script);
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const canvas = document.createElement('canvas');
// use external library to parse and draw gif animation
function onDrawFrame(ctx, frame) {
// update canvas size
canvas.width = frame.width;
canvas.height = frame.height;
// update canvas that we are using for Konva.Image
ctx.drawImage(frame.buffer, 0, 0);
// redraw the layer
layer.draw();
}
script.onload = () => {
gifler('https://konvajs.org/assets/yoda.gif').frames(canvas, onDrawFrame);
};
// draw resulted canvas into the stage as Konva.Image
const image = new Konva.Image({
image: canvas,
});
layer.add(image);
```
```js
import React from 'react';
import { Stage, Layer, Image } from 'react-konva';
const GifImage = () => {
const imageRef = React.useRef(null);
const canvasRef = React.useRef(document.createElement('canvas'));
React.useEffect(() => {
// Load gifler library
const script = document.createElement('script');
script.src = 'https://unpkg.com/gifler@0.1.0/gifler.min.js';
script.onload = () => {
// use external library to parse and draw gif animation
function onDrawFrame(ctx, frame) {
// update canvas size
canvasRef.current.width = frame.width;
canvasRef.current.height = frame.height;
// update canvas that we are using for Konva.Image
ctx.drawImage(frame.buffer, 0, 0);
// update Konva.Image
imageRef.current?.getLayer()?.batchDraw();
}
gifler('https://konvajs.org/assets/yoda.gif').frames(canvasRef.current, onDrawFrame);
};
document.head.appendChild(script);
return () => script.remove();
}, []);
return (
);
};
const App = () => {
return (
);
};
export default App;
```
```js
```
Instructions: The demo shows an animated GIF rendered on canvas using the gifler library. The GIF is parsed and each frame is drawn onto a canvas, which is then used as the source for a Konva.Image shape.
---
# Gesture Events on Canvas Shapes
> Learn how to handle swipe, pinch zoom, rotate, and other multi-touch gesture events on Konva canvas shapes using Hammer.js.
Source: https://konvajs.org/docs/sandbox/Gestures.html
## How to listen to swipe, pinch zoom, rotate and other multi-touch gesture events on canvas shapes?
By default `Konva` supports only basic touch events such as `touchstart`, `touchmove`, `touchend`.
You have to implement gesture events manually from that touch events.
If you are looking for pan and zoom logic for the whole stage take a look into [Multi-touch scale Stage demo](/docs/sandbox/Multi-touch_Scale_Stage.html).
But I was able to slightly change [Hammer.js](https://hammerjs.github.io/) to make it work with Konva!
You can find modified [hammer.js source code here](/js/hammer-konva.js).
**Instructions: you can try different gestures on the rectangle such as swipe, rotate, zoom, drag&drop, press. For desktop browsers you can hold `Shift` key to emulate touch events.**
```js
import Konva from 'konva';
// Load required scripts
const loadScript = (src) => {
return new Promise((resolve, reject) => {
if (document.querySelector(`script[src="${src}"]`)) {
resolve();
return;
}
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
};
// Initialize the demo after loading dependencies
async function initDemo() {
try {
await loadScript('https://cdn.jsdelivr.net/gh/hammerjs/touchemulator@eed95bf676877a1394c4538d849802061156681a/touch-emulator.js');
await loadScript('https://konvajs.org/js/hammer-konva.js');
// emulate touches on desktop
TouchEmulator();
Konva.hitOnDragEnabled = true;
Konva.capturePointerEventsEnabled = true;
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const originalAttrs = {
x: stage.width() / 2,
y: stage.height() / 2,
scaleX: 1,
scaleY: 1,
draggable: true,
rotation: 0,
};
const group = new Konva.Group(originalAttrs);
layer.add(group);
const size = 200;
const rect = new Konva.Rect({
width: size,
height: size,
fill: 'yellow',
offsetX: size / 2,
offsetY: size / 2,
cornerRadius: 5,
shadowBlur: 10,
shadowColor: 'grey',
});
group.add(rect);
const defaultText = 'Try\ndrag, swipe, pinch zoom, rotate, press...';
const text = new Konva.Text({
text: defaultText,
x: -size / 2,
width: size,
align: 'center',
});
group.add(text);
// attach modified version of Hammer.js
// "domEvents" property will allow triggering events on group
// instead of "hammertime" instance
const hammertime = new Hammer(group, { domEvents: true });
// add rotate gesture
hammertime.get('rotate').set({ enable: true });
// now attach all possible events
group.on('swipe', function (ev) {
text.text('swiping');
group.to({
x: group.x() + ev.evt.gesture.deltaX,
y: group.y() + ev.evt.gesture.deltaY,
onFinish: function () {
group.to(Object.assign({}, originalAttrs));
text.text(defaultText);
},
});
});
group.on('press', function (ev) {
text.text('Under press');
rect.to({
fill: 'green',
});
});
group.on('touchend', function (ev) {
rect.to({
fill: 'yellow',
});
setTimeout(() => {
text.text(defaultText);
}, 300);
});
group.on('dragend', () => {
group.to(Object.assign({}, originalAttrs));
});
let oldRotation = 0;
let startScale = 0;
group.on('rotatestart', function (ev) {
oldRotation = ev.evt.gesture.rotation;
startScale = rect.scaleX();
group.stopDrag();
group.draggable(false);
text.text('rotating...');
});
group.on('rotate', function (ev) {
const delta = oldRotation - ev.evt.gesture.rotation;
group.rotate(-delta);
oldRotation = ev.evt.gesture.rotation;
group.scaleX(startScale * ev.evt.gesture.scale);
group.scaleY(startScale * ev.evt.gesture.scale);
});
group.on('rotateend rotatecancel', function (ev) {
group.to(Object.assign({}, originalAttrs));
text.text(defaultText);
group.draggable(true);
});
} catch (error) {
console.error('Failed to initialize demo:', error);
}
}
// Start the demo
initDemo();
```
---
# Heatmap Generator — Build Interactive Heatmaps with JavaScript Canvas
> Generate interactive heatmaps on HTML5 Canvas with JavaScript using Konva.js. Click or move your mouse to add data points with adjustable radius and intensity. Export as PNG.
Source: https://konvajs.org/docs/sandbox/Heatmap_Generator.html
Create interactive heatmaps by clicking or dragging on the canvas. Adjust the radius and intensity sliders to control how heat points spread and blend together, then export your heatmap as a PNG image.
**Instructions:** Click on the canvas to add heat points, or click and drag to paint continuously. Use the radius and intensity sliders to fine-tune the heatmap appearance. Click "Clear" to reset and "Export PNG" to download your heatmap.
```js
import Konva from 'konva';
// --- Controls ---
const controls = document.createElement('div');
controls.style.cssText = 'display:flex;gap:10px;align-items:center;margin-bottom:4px;flex-wrap:wrap;font-size:13px;';
const radiusLabel = document.createElement('label');
radiusLabel.textContent = 'Radius: ';
const radiusSlider = document.createElement('input');
radiusSlider.type = 'range';
radiusSlider.min = '10';
radiusSlider.max = '80';
radiusSlider.value = '40';
radiusSlider.style.width = '80px';
const radiusVal = document.createElement('span');
radiusVal.textContent = '40px';
radiusLabel.appendChild(radiusSlider);
radiusLabel.appendChild(radiusVal);
const intensityLabel = document.createElement('label');
intensityLabel.textContent = 'Intensity: ';
const intensitySlider = document.createElement('input');
intensitySlider.type = 'range';
intensitySlider.min = '1';
intensitySlider.max = '10';
intensitySlider.value = '5';
intensitySlider.style.width = '80px';
const intensityVal = document.createElement('span');
intensityVal.textContent = '0.5';
intensityLabel.appendChild(intensitySlider);
intensityLabel.appendChild(intensityVal);
const clearBtn = document.createElement('button');
clearBtn.textContent = 'Clear';
const exportBtn = document.createElement('button');
exportBtn.textContent = 'Export PNG';
controls.appendChild(radiusLabel);
controls.appendChild(intensityLabel);
controls.appendChild(clearBtn);
controls.appendChild(exportBtn);
const container = document.getElementById('container');
container.parentNode.insertBefore(controls, container);
radiusSlider.addEventListener('input', () => { radiusVal.textContent = radiusSlider.value + 'px'; });
intensitySlider.addEventListener('input', () => { intensityVal.textContent = (intensitySlider.value / 10).toFixed(1); });
// --- Stage ---
const width = window.innerWidth;
const height = window.innerHeight - 40;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
// Dark background
const bg = new Konva.Rect({ x: 0, y: 0, width, height, fill: '#1a1a2e' });
layer.add(bg);
// Offscreen canvas for heatmap rendering
const shadowCanvas = document.createElement('canvas');
shadowCanvas.width = width;
shadowCanvas.height = height;
const shadowCtx = shadowCanvas.getContext('2d');
const heatPoints = [];
let heatImage = null;
let isDrawing = false;
function drawHeatPoint(ctx, x, y, radius, intensity) {
// Use additive blending with colored gradients — no pixel loop needed
ctx.globalCompositeOperation = 'lighter';
const grad = ctx.createRadialGradient(x, y, 0, x, y, radius);
// Center: warm red/orange, edges: cool blue, fading to transparent
grad.addColorStop(0, 'rgba(255, 80, 0, ' + intensity + ')');
grad.addColorStop(0.3, 'rgba(255, 200, 0, ' + (intensity * 0.7) + ')');
grad.addColorStop(0.6, 'rgba(0, 200, 100, ' + (intensity * 0.3) + ')');
grad.addColorStop(0.85, 'rgba(0, 100, 255, ' + (intensity * 0.15) + ')');
grad.addColorStop(1, 'rgba(0, 0, 100, 0)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
}
function renderHeatmap() {
shadowCtx.globalCompositeOperation = 'source-over';
shadowCtx.clearRect(0, 0, shadowCanvas.width, shadowCanvas.height);
heatPoints.forEach(function(p) {
drawHeatPoint(shadowCtx, p.x, p.y, p.r, p.i);
});
if (heatImage) {
heatImage.destroy();
}
heatImage = new Konva.Image({
image: shadowCanvas,
x: 0,
y: 0,
listening: false,
});
layer.add(heatImage);
bg.moveToBottom();
}
function addPoint(pos) {
heatPoints.push({
x: pos.x,
y: pos.y,
r: parseInt(radiusSlider.value),
i: parseInt(intensitySlider.value) / 10,
});
renderHeatmap();
}
stage.on('mousedown touchstart', function(e) {
isDrawing = true;
addPoint(stage.getPointerPosition());
});
stage.on('mousemove touchmove', function() {
if (!isDrawing) return;
addPoint(stage.getPointerPosition());
});
stage.on('mouseup touchend mouseleave', function() {
isDrawing = false;
});
clearBtn.addEventListener('click', function() {
heatPoints.length = 0;
renderHeatmap();
});
exportBtn.addEventListener('click', function() {
const dataURL = stage.toDataURL({ pixelRatio: 2 });
const link = document.createElement('a');
link.download = 'heatmap.png';
link.href = dataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
```
```js
import React from 'react';
import { Stage, Layer, Rect, Image } from 'react-konva';
const App = () => {
const stageRef = React.useRef(null);
const shadowRef = React.useRef(null);
const heatImageRef = React.useRef(null);
const pointsRef = React.useRef([]);
const drawingRef = React.useRef(false);
const radiusRef = React.useRef(40);
const intensityRef = React.useRef(5);
const [radius, setRadius] = React.useState(40);
const [intensity, setIntensity] = React.useState(5);
const W = window.innerWidth;
const H = window.innerHeight - 60;
React.useEffect(() => {
const c = document.createElement('canvas');
c.width = W;
c.height = H;
shadowRef.current = c;
}, []);
function drawHeatPoint(ctx, x, y, r, inten) {
ctx.globalCompositeOperation = 'lighter';
const grad = ctx.createRadialGradient(x, y, 0, x, y, r);
grad.addColorStop(0, 'rgba(255, 80, 0, ' + inten + ')');
grad.addColorStop(0.3, 'rgba(255, 200, 0, ' + (inten * 0.7) + ')');
grad.addColorStop(0.6, 'rgba(0, 200, 100, ' + (inten * 0.3) + ')');
grad.addColorStop(0.85, 'rgba(0, 100, 255, ' + (inten * 0.15) + ')');
grad.addColorStop(1, 'rgba(0, 0, 100, 0)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fill();
}
function renderHeatmap() {
var c = shadowRef.current;
if (!c || !heatImageRef.current) return;
var ctx = c.getContext('2d');
ctx.globalCompositeOperation = 'source-over';
ctx.clearRect(0, 0, c.width, c.height);
pointsRef.current.forEach(function(p) {
drawHeatPoint(ctx, p.x, p.y, p.r, p.i);
});
// Update Konva node directly — no state change, no flicker
var node = heatImageRef.current;
node.image(c);
node.getLayer().batchDraw();
}
function addPoint(pos) {
pointsRef.current.push({ x: pos.x, y: pos.y, r: radiusRef.current, i: intensityRef.current / 10 });
renderHeatmap();
}
var handleDown = function() {
drawingRef.current = true;
addPoint(stageRef.current.getPointerPosition());
};
var handleMove = function() {
if (!drawingRef.current) return;
addPoint(stageRef.current.getPointerPosition());
};
var handleUp = function() {
drawingRef.current = false;
};
var handleClear = function() {
pointsRef.current = [];
renderHeatmap();
};
var handleExport = function() {
var dataURL = stageRef.current.toDataURL({ pixelRatio: 2 });
var link = document.createElement('a');
link.download = 'heatmap.png';
link.href = dataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
return (
<>
Radius: {radius}px
Intensity: {(intensity/10).toFixed(1)}
Clear
Export PNG
>
);
};
export default App;
```
```js
```
---
# Border for Image around Non Transparent parts
> Draw a stroke border around the non-transparent parts of an image using a custom Konva filter with shadow-based contour detection.
Source: https://konvajs.org/docs/sandbox/Image_Border.html
## How to draw a stroke around image with alpha channel?
This demo demonstrates how to use custom filters with the Konva framework to create a border that follows the contour of an image with an alpha channel.
Since following a contour precisely is a complex task, we'll use a technique with blurred shadow as a border foundation. The filter replaces transparent/blurred pixels with our solid color that we want for the border.
**Instructions:** Observe the image with a custom border that follows its non-transparent parts.
```js
import Konva from 'konva';
// Create stage
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// Define variables for our custom filter
let canvas = document.createElement('canvas');
let tempCanvas = document.createElement('canvas');
// Make all pixels opaque 100% (except pixels that are 100% transparent)
function removeTransparency(canvas) {
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const nPixels = imageData.data.length;
for (let i = 3; i < nPixels; i += 4) {
if (imageData.data[i] > 0) {
imageData.data[i] = 255;
}
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.putImageData(imageData, 0, 0);
return canvas;
}
// Define our custom Border filter
function Border(imageData) {
const nPixels = imageData.data.length;
const size = this.getAttr('borderSize') || 0;
// Set correct dimensions for canvases
canvas.width = imageData.width;
canvas.height = imageData.height;
tempCanvas.width = imageData.width;
tempCanvas.height = imageData.height;
// Draw original shape into temp canvas
tempCanvas.getContext('2d').putImageData(imageData, 0, 0);
// Remove alpha channel because it will affect shadow (transparent shapes have smaller shadow)
removeTransparency(tempCanvas);
const ctx = canvas.getContext('2d');
const color = this.getAttr('borderColor') || 'black';
// Use shadow as border
ctx.save();
ctx.shadowColor = color;
ctx.shadowBlur = size;
ctx.drawImage(tempCanvas, 0, 0);
ctx.restore();
// Get image data of [original image + shadow]
const tempImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const SMOOTH_MIN_THRESHOLD = 3;
const SMOOTH_MAX_THRESHOLD = 10;
let val, hasValue;
const offset = 3;
for (let i = 3; i < nPixels; i += 4) {
// Skip opaque pixels
if (imageData.data[i] === 255) {
continue;
}
val = tempImageData.data[i];
hasValue = val !== 0;
if (!hasValue) {
continue;
}
if (val > SMOOTH_MAX_THRESHOLD) {
val = 255;
} else if (val < SMOOTH_MIN_THRESHOLD) {
val = 0;
} else {
val = ((val - SMOOTH_MIN_THRESHOLD) / (SMOOTH_MAX_THRESHOLD - SMOOTH_MIN_THRESHOLD)) * 255;
}
tempImageData.data[i] = val;
}
// Draw resulting image (original + shadow without opacity) into canvas
ctx.putImageData(tempImageData, 0, 0);
// Fill whole image with color (after that shadow is colored)
ctx.save();
ctx.globalCompositeOperation = 'source-in';
ctx.fillStyle = color;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
// Copy colored shadow into original imageData
const newImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const indexesToProcess = [];
for (let i = 3; i < nPixels; i += 4) {
const hasTransparentOnTop = imageData.data[i - imageData.width * 4 * offset] === 0;
const hasTransparentOnTopRight = imageData.data[i - (imageData.width * 4 + 4) * offset] === 0;
const hasTransparentOnTopLeft = imageData.data[i - (imageData.width * 4 - 4) * offset] === 0;
const hasTransparentOnRight = imageData.data[i + 4 * offset] === 0;
const hasTransparentOnLeft = imageData.data[i - 4 * offset] === 0;
const hasTransparentOnBottom = imageData.data[i + imageData.width * 4 * offset] === 0;
const hasTransparentOnBottomRight = imageData.data[i + (imageData.width * 4 + 4) * offset] === 0;
const hasTransparentOnBottomLeft = imageData.data[i + (imageData.width * 4 - 4) * offset] === 0;
const hasTransparentAround =
hasTransparentOnTop ||
hasTransparentOnRight ||
hasTransparentOnLeft ||
hasTransparentOnBottom ||
hasTransparentOnTopRight ||
hasTransparentOnTopLeft ||
hasTransparentOnBottomRight ||
hasTransparentOnBottomLeft;
// Skip pixels presented in original image
if (imageData.data[i] === 255 || (imageData.data[i] && !hasTransparentAround)) {
continue;
}
if (!newImageData.data[i]) {
// Skip transparent pixels
continue;
}
indexesToProcess.push(i);
}
for (let index = 0; index < indexesToProcess.length; index += 1) {
const i = indexesToProcess[index];
const alpha = imageData.data[i] / 255;
imageData.data[i] = newImageData.data[i];
imageData.data[i - 1] = newImageData.data[i - 1] * (1 - alpha) + imageData.data[i - 1] * alpha;
imageData.data[i - 2] = newImageData.data[i - 2] * (1 - alpha) + imageData.data[i - 2] * alpha;
imageData.data[i - 3] = newImageData.data[i - 3] * (1 - alpha) + imageData.data[i - 3] * alpha;
}
}
// Load image and apply filter
Konva.Image.fromURL('https://konvajs.org/assets/lion.png', function (image) {
layer.add(image);
image.setAttrs({
x: 80,
y: 30,
borderSize: 5,
borderColor: 'red',
});
image.filters([Border]);
image.cache();
});
```
---
# Drag and Drop Multiple Images with Border Highlighting
> Drag and drop multiple images with border highlighting that toggles on hover using Konva event delegation.
Source: https://konvajs.org/docs/sandbox/Image_Border_Highlighting.html
This demo shows how to implement highlighting effects with images. When hovering over an image, the border disappears, and it reappears when you move the mouse away.
**Instructions:** Hover over the images to hide their borders and drag them around the stage.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// Create Darth Vader image
const darthVaderImg = new Konva.Image({
x: 20,
y: 20,
width: 200,
height: 137,
stroke: 'red',
strokeWidth: 10,
draggable: true,
});
layer.add(darthVaderImg);
// Create Yoda image
const yodaImg = new Konva.Image({
x: 240,
y: 20,
width: 93,
height: 104,
draggable: true,
stroke: 'red',
strokeWidth: 10,
});
layer.add(yodaImg);
// Load Darth Vader image
const imageObj1 = new Image();
imageObj1.onload = function () {
darthVaderImg.image(imageObj1);
};
imageObj1.src = 'https://konvajs.org/assets/darth-vader.jpg';
// Load Yoda image
const imageObj2 = new Image();
imageObj2.onload = function () {
yodaImg.image(imageObj2);
};
imageObj2.src = 'https://konvajs.org/assets/yoda.jpg';
// Use event delegation to update pointer style and borders
layer.on('mouseover', function (evt) {
const shape = evt.target;
document.body.style.cursor = 'pointer';
shape.strokeEnabled(false);
});
layer.on('mouseout', function (evt) {
const shape = evt.target;
document.body.style.cursor = 'default';
shape.strokeEnabled(true);
});
```
```js
import { useState } from 'react';
import { Stage, Layer, Image } from 'react-konva';
import { useImage } from 'react-konva-utils';
const DraggableImage = ({ src, x, y, width, height }) => {
const [image] = useImage(src);
const [isHovered, setIsHovered] = useState(false);
const [position, setPosition] = useState({ x, y });
return (
setPosition(e.target.position())}
onMouseEnter={() => {
setIsHovered(true);
document.body.style.cursor = 'pointer';
}}
onMouseLeave={() => {
setIsHovered(false);
document.body.style.cursor = 'default';
}}
/>
);
};
const App = () => {
return (
);
};
export default App;
```
```js
```
---
# Canvas Resize Image — Drag, Drop, and Resize Images with JavaScript
> Learn how to resize images on HTML5 Canvas with JavaScript. Drag, drop, and scale images using corner anchor points with Konva.js. Interactive demo with source code.
Source: https://konvajs.org/docs/sandbox/Image_Resize.html
This demo shows how to resize images on canvas by implementing draggable corner anchors. The images can be both dragged and resized — a common pattern for canvas image editing.
Note: We also have a built-in method for such cases with the special `Konva.Transformer` node. Take a look at the [Select and Transform demo](/docs/select_and_transform/Basic_demo.html) for an easier approach.
**Instructions:** Drag the images to move them. Click and drag the corner anchors to resize.
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
// Function to update image size based on anchor movement
function update(activeAnchor) {
const group = activeAnchor.getParent();
const topLeft = group.findOne('.topLeft');
const topRight = group.findOne('.topRight');
const bottomRight = group.findOne('.bottomRight');
const bottomLeft = group.findOne('.bottomLeft');
const image = group.findOne('Image');
const anchorX = activeAnchor.x();
const anchorY = activeAnchor.y();
// Update anchor positions based on which anchor was moved
switch (activeAnchor.getName()) {
case 'topLeft':
topRight.y(anchorY);
bottomLeft.x(anchorX);
break;
case 'topRight':
topLeft.y(anchorY);
bottomRight.x(anchorX);
break;
case 'bottomRight':
bottomLeft.y(anchorY);
topRight.x(anchorX);
break;
case 'bottomLeft':
bottomRight.y(anchorY);
topLeft.x(anchorX);
break;
}
// Position image at top-left corner
image.position(topLeft.position());
// Update image dimensions
const width = topRight.x() - topLeft.x();
const height = bottomLeft.y() - topLeft.y();
if (width && height) {
image.width(width);
image.height(height);
}
}
// Function to add resize anchors to a group
function addAnchor(group, x, y, name) {
const anchor = new Konva.Circle({
x: x,
y: y,
stroke: '#666',
fill: '#ddd',
strokeWidth: 2,
radius: 8,
name: name,
draggable: true,
dragOnTop: false,
});
// Add event listeners for resize behavior
anchor.on('dragmove', function () {
update(this);
});
anchor.on('mousedown touchstart', function () {
group.draggable(false);
this.moveToTop();
});
anchor.on('dragend', function () {
group.draggable(true);
});
// Add hover styling
anchor.on('mouseover', function () {
document.body.style.cursor = 'pointer';
this.strokeWidth(4);
});
anchor.on('mouseout', function () {
document.body.style.cursor = 'default';
this.strokeWidth(2);
});
group.add(anchor);
}
// Create Darth Vader Group with Image and anchors
const darthVaderImg = new Konva.Image({
width: 200,
height: 137,
});
const darthVaderGroup = new Konva.Group({
x: 180,
y: 50,
draggable: true,
});
layer.add(darthVaderGroup);
darthVaderGroup.add(darthVaderImg);
// Add anchors at the corners
addAnchor(darthVaderGroup, 0, 0, 'topLeft');
addAnchor(darthVaderGroup, 200, 0, 'topRight');
addAnchor(darthVaderGroup, 200, 137, 'bottomRight');
addAnchor(darthVaderGroup, 0, 137, 'bottomLeft');
// Create Yoda Group with Image and anchors
const yodaImg = new Konva.Image({
width: 93,
height: 104,
});
const yodaGroup = new Konva.Group({
x: 20,
y: 110,
draggable: true,
});
layer.add(yodaGroup);
yodaGroup.add(yodaImg);
// Add anchors at the corners
addAnchor(yodaGroup, 0, 0, 'topLeft');
addAnchor(yodaGroup, 93, 0, 'topRight');
addAnchor(yodaGroup, 93, 104, 'bottomRight');
addAnchor(yodaGroup, 0, 104, 'bottomLeft');
// Load the images
const imageObj1 = new Image();
imageObj1.onload = function () {
darthVaderImg.image(imageObj1);
};
imageObj1.src = 'https://konvajs.org/assets/darth-vader.jpg';
const imageObj2 = new Image();
imageObj2.onload = function () {
yodaImg.image(imageObj2);
};
imageObj2.src = 'https://konvajs.org/assets/yoda.jpg';
```
---
# Jumping Bunnies Performance Stress Test
> Bunnymark performance stress test for Konva, animating hundreds of bouncing bunny sprites on an HTML5 canvas.
Source: https://konvajs.org/docs/sandbox/Jumping_Bunnies.html
## Performance stress test with bouncing bunnies
This demo showcases the performance of moving many `Konva.Image` objects at the same time. It's adapted from the [Bunnymark demo](https://www.goodboydigital.com/pixijs/bunnymark/) of the [PixiJS framework](https://pixijs.com/).
Note: You may notice that the `Konva` version is slower than the original `PixiJS` version. This is because PixiJS is highly optimized for WebGL rendering and this specific type of animation. While Konva continues to optimize its internals, remember that this demo doesn't represent the performance of typical applications made with Konva.
For applications with a very large number of animated objects, you might consider using [Native Canvas Access](/docs/sandbox/Native_Context_Access.html) or even a different framework. Choose the right tool for your specific application needs.
**Instructions:** Click or touch the canvas to add more bunnies. The counter will show how many bunnies are currently animating.
```js
import Konva from 'konva';
// Set up stage and layer
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer({ listening: false });
stage.add(layer);
// Create stats and counter display
const counterDiv = document.createElement('div');
counterDiv.style.position = 'absolute';
counterDiv.style.top = '50px';
counterDiv.style.backgroundColor = 'white';
counterDiv.style.fontSize = '12px';
counterDiv.style.padding = '5px';
counterDiv.innerHTML = '0 BUNNIES';
document.getElementById('container').appendChild(counterDiv);
// Define variables
const bunnys = [];
const GRAVITY = 0.75;
const maxX = width;
const minX = 0;
const maxY = height;
const minY = 0;
const startBunnyCount = 100; // Starting with fewer bunnies for better initial performance
const amount = 10; // Add this many bunnies at a time
let isAdding = false;
let count = 0;
let wabbitTexture;
// Load the bunny image
wabbitTexture = new Image();
wabbitTexture.onload = function() {
addBunnies(startBunnyCount);
counterDiv.innerHTML = startBunnyCount + ' BUNNIES';
count = startBunnyCount;
// Start animation loop
requestAnimationFrame(update);
};
wabbitTexture.src = 'https://konvajs.org/assets/bunny.png';
// Add event listeners
stage.on('mousedown touchstart', function() {
isAdding = true;
});
stage.on('mouseup touchend', function() {
isAdding = false;
});
// Function to add bunnies
function addBunnies(num) {
for (let i = 0; i < num; i++) {
const bunny = new Konva.Image({
image: wabbitTexture,
transformsEnabled: 'position',
perfectDrawEnabled: false,
x: Math.random() * width,
y: Math.random() * height,
});
bunny.speedX = Math.random() * 10;
bunny.speedY = Math.random() * 10 - 5;
bunnys.push(bunny);
layer.add(bunny);
}
}
// Animation update function
function update() {
// Add more bunnies if mouse is down
if (isAdding) {
addBunnies(amount);
count += amount;
counterDiv.innerHTML = count + ' BUNNIES';
}
// Update all bunnies
for (let i = 0; i < bunnys.length; i++) {
const bunny = bunnys[i];
let x = bunny.x();
let y = bunny.y();
x += bunny.speedX;
y += bunny.speedY;
bunny.speedY += GRAVITY;
// Bounce off the edges
if (x > maxX - wabbitTexture.width) {
bunny.speedX *= -1;
x = maxX - wabbitTexture.width;
} else if (x < minX) {
bunny.speedX *= -1;
x = minX;
}
if (y > maxY - wabbitTexture.height) {
bunny.speedY *= -0.85;
y = maxY - wabbitTexture.height;
if (Math.random() > 0.5) {
bunny.speedY -= Math.random() * 6;
}
} else if (y < minY) {
bunny.speedY = 0;
y = minY;
}
bunny.position({ x, y });
}
layer.batchDraw();
requestAnimationFrame(update);
}
```
```js
import { useState, useEffect, useRef } from 'react';
import { Stage, Layer, Image } from 'react-konva';
import { useImage } from 'react-konva-utils';
const BunnyMark = () => {
// Constants
const width = window.innerWidth;
const height = window.innerHeight;
const GRAVITY = 0.75;
const START_COUNT = 100;
const ADD_AMOUNT = 10;
// State and refs
const [count, setCount] = useState(0);
const [isAdding, setIsAdding] = useState(false);
const layerRef = useRef(null);
const bunniesRef = useRef([]);
const bunnyNodesRef = useRef([]); // Store references to the actual Konva nodes
const [bunnyImage] = useImage('https://konvajs.org/assets/bunny.png');
// Create a bunny with position and velocity
const createBunny = (x, y) => ({
x,
y,
speedX: Math.random() * 10,
speedY: Math.random() * 10 - 5
});
// Store references to Konva image nodes
const storeNodeRef = (index, node) => {
if (node) {
bunnyNodesRef.current[index] = node;
}
};
// Initialize bunnies when image loads
useEffect(() => {
if (!bunnyImage) return;
const initialBunnies = Array(START_COUNT).fill(0).map(() => createBunny(
Math.random() * width,
Math.random() * height
));
bunniesRef.current = initialBunnies;
bunnyNodesRef.current = new Array(START_COUNT);
setCount(START_COUNT);
}, [bunnyImage]);
// Animation loop
useEffect(() => {
if (!bunnyImage) return;
let animationFrameId;
const update = () => {
// Add more bunnies if needed
if (isAdding) {
const currentLength = bunniesRef.current.length;
const newBunnies = Array(ADD_AMOUNT).fill(0).map(() =>
createBunny(
Math.random() * width,
Math.random() * height
)
);
bunniesRef.current = [...bunniesRef.current, ...newBunnies];
// Extend the nodes array to accommodate new bunnies
bunnyNodesRef.current = [...bunnyNodesRef.current, ...new Array(ADD_AMOUNT)];
setCount(prevCount => prevCount + ADD_AMOUNT);
}
// Update all bunnies - DIRECT NODE MANIPULATION FOR PERFORMANCE
// This avoids expensive React re-renders for position updates
bunniesRef.current.forEach((bunny, i) => {
// Update data model
bunny.x += bunny.speedX;
bunny.y += bunny.speedY;
bunny.speedY += GRAVITY;
// Bounce off edges
if (bunny.x > width - bunnyImage.width) {
bunny.speedX *= -1;
bunny.x = width - bunnyImage.width;
} else if (bunny.x < 0) {
bunny.speedX *= -1;
bunny.x = 0;
}
if (bunny.y > height - bunnyImage.height) {
bunny.speedY *= -0.85;
bunny.y = height - bunnyImage.height;
if (Math.random() > 0.5) {
bunny.speedY -= Math.random() * 6;
}
} else if (bunny.y < 0) {
bunny.speedY = 0;
bunny.y = 0;
}
// Direct node update if we have a reference (much faster than React updates)
const node = bunnyNodesRef.current[i];
if (node) {
node.x(bunny.x);
node.y(bunny.y);
}
});
// Batch draw the layer once instead of updating each node individually
if (layerRef.current) {
layerRef.current.getLayer().batchDraw();
}
animationFrameId = requestAnimationFrame(update);
};
update();
return () => {
cancelAnimationFrame(animationFrameId);
};
}, [isAdding, bunnyImage]);
// Handle mouse/touch events
const handleDown = () => setIsAdding(true);
const handleUp = () => setIsAdding(false);
if (!bunnyImage) return Loading bunny image...
;
return (
<>
{bunniesRef.current.map((bunny, i) => (
storeNodeRef(i, node)}
image={bunnyImage}
x={bunny.x}
y={bunny.y}
transformsEnabled="position"
perfectDrawEnabled={false}
/>
))}
{count} BUNNIES
>
);
};
export default BunnyMark;
```
```js
```
---
# How to Limit Dragging and Resizing of Shapes by Canvas Stage
> Restrict dragging and resizing of Konva shapes to stay within the canvas stage boundaries using custom boundary functions.
Source: https://konvajs.org/docs/sandbox/Limited_Drag_And_Resize.html
This demo demonstrates how to restrict dragging and resizing of shapes to stay within the boundaries of the canvas stage. By implementing custom boundary functions, we can prevent shapes from being moved or resized outside the visible area.
The implementation combines techniques from the [Drag Limit Demo](https://konvajs.org/docs/drag_and_drop/Simple_Drag_Bounds.html) and [Resize Limit Demo](https://konvajs.org/docs/select_and_transform/Resize_Limits.html) to add restrictions to user interactions.
**Instructions:** Try to rotate, drag, or resize the shapes. Notice how they are constrained to stay within the canvas boundaries.
```js
import Konva from 'konva';
// Helper functions for calculating bounding boxes
function getCorner(pivotX, pivotY, diffX, diffY, angle) {
const distance = Math.sqrt(diffX * diffX + diffY * diffY);
// Find angle from pivot to corner
angle += Math.atan2(diffY, diffX);
// Get new x and y coordinates
const x = pivotX + distance * Math.cos(angle);
const y = pivotY + distance * Math.sin(angle);
return { x, y };
}
// Calculate client rect accounting for rotation
function getClientRect(rotatedBox) {
const { x, y, width, height } = rotatedBox;
const rad = rotatedBox.rotation;
const p1 = getCorner(x, y, 0, 0, rad);
const p2 = getCorner(x, y, width, 0, rad);
const p3 = getCorner(x, y, width, height, rad);
const p4 = getCorner(x, y, 0, height, rad);
const minX = Math.min(p1.x, p2.x, p3.x, p4.x);
const minY = Math.min(p1.y, p2.y, p3.y, p4.y);
const maxX = Math.max(p1.x, p2.x, p3.x, p4.x);
const maxY = Math.max(p1.y, p2.y, p3.y, p4.y);
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
}
// Calculate total bounding box of multiple shapes
function getTotalBox(boxes) {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
boxes.forEach((box) => {
minX = Math.min(minX, box.x);
minY = Math.min(minY, box.y);
maxX = Math.max(maxX, box.x + box.width);
maxY = Math.max(maxY, box.y + box.height);
});
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
}
// Set up the stage
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// Create first shape (red rectangle)
const shape1 = new Konva.Rect({
x: stage.width() / 2 - 60,
y: stage.height() / 2 - 60,
width: 50,
height: 50,
fill: 'red',
draggable: true,
});
layer.add(shape1);
// Create second shape (green rectangle)
const shape2 = shape1.clone({
x: stage.width() / 2 + 10,
y: stage.height() / 2 + 10,
fill: 'green',
});
layer.add(shape2);
// Add transformer that includes both shapes
const tr = new Konva.Transformer({
nodes: [shape1, shape2],
// Set boundary function for resize operations
boundBoxFunc: (oldBox, newBox) => {
// Calculate the actual bounding box of the transformed shape
const box = getClientRect(newBox);
// Check if the new box is outside the stage boundaries
const isOut =
box.x < 0 ||
box.y < 0 ||
box.x + box.width > stage.width() ||
box.y + box.height > stage.height();
// If outside boundaries, keep the old box
if (isOut) {
return oldBox;
}
// If within boundaries, allow the transformation
return newBox;
},
});
layer.add(tr);
// Handle drag events to keep shapes within the stage
tr.on('dragmove', () => {
// Get client rects for all selected nodes
const boxes = tr.nodes().map((node) => node.getClientRect());
// Get the total bounding box of all shapes
const box = getTotalBox(boxes);
// Keep shapes within stage boundaries
tr.nodes().forEach((shape) => {
const absPos = shape.getAbsolutePosition();
// Calculate shape position relative to group bounding box
const offsetX = box.x - absPos.x;
const offsetY = box.y - absPos.y;
// Adjust position if outside boundaries
const newAbsPos = { ...absPos };
if (box.x < 0) {
newAbsPos.x = -offsetX;
}
if (box.y < 0) {
newAbsPos.y = -offsetY;
}
if (box.x + box.width > stage.width()) {
newAbsPos.x = stage.width() - box.width - offsetX;
}
if (box.y + box.height > stage.height()) {
newAbsPos.y = stage.height() - box.height - offsetY;
}
shape.setAbsolutePosition(newAbsPos);
});
});
```
```js
import { useState, useEffect, useRef } from 'react';
import { Stage, Layer, Rect, Transformer } from 'react-konva';
// Helper functions for calculating bounding boxes
const getCorner = (pivotX, pivotY, diffX, diffY, angle) => {
const distance = Math.sqrt(diffX * diffX + diffY * diffY);
angle += Math.atan2(diffY, diffX);
const x = pivotX + distance * Math.cos(angle);
const y = pivotY + distance * Math.sin(angle);
return { x, y };
};
const getClientRect = (rotatedBox) => {
const { x, y, width, height } = rotatedBox;
const rad = rotatedBox.rotation;
const p1 = getCorner(x, y, 0, 0, rad);
const p2 = getCorner(x, y, width, 0, rad);
const p3 = getCorner(x, y, width, height, rad);
const p4 = getCorner(x, y, 0, height, rad);
const minX = Math.min(p1.x, p2.x, p3.x, p4.x);
const minY = Math.min(p1.y, p2.y, p3.y, p4.y);
const maxX = Math.max(p1.x, p2.x, p3.x, p4.x);
const maxY = Math.max(p1.y, p2.y, p3.y, p4.y);
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
};
const getTotalBox = (boxes) => {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
boxes.forEach((box) => {
minX = Math.min(minX, box.x);
minY = Math.min(minY, box.y);
maxX = Math.max(maxX, box.x + box.width);
maxY = Math.max(maxY, box.y + box.height);
});
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
};
const LimitedDragAndResize = () => {
const [stageSize, setStageSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
const [shapes, setShapes] = useState([
{
id: 'rect1',
x: window.innerWidth / 2 - 60,
y: window.innerHeight / 2 - 60,
width: 50,
height: 50,
fill: 'red',
rotation: 0,
scaleX: 1,
scaleY: 1,
},
{
id: 'rect2',
x: window.innerWidth / 2 + 10,
y: window.innerHeight / 2 + 10,
width: 50,
height: 50,
fill: 'green',
rotation: 0,
scaleX: 1,
scaleY: 1,
}
]);
const shapeRefs = useRef(new Map());
const trRef = useRef(null);
// Set up Transformer after the layer mounts
useEffect(() => {
if (trRef.current) {
const nodes = shapes.map(shape => shapeRefs.current.get(shape.id));
trRef.current.nodes(nodes);
}
}, [shapes]);
// Handle window resize
useEffect(() => {
const handleResize = () => {
setStageSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// Boundary function for Transformer
const boundBoxFunc = (oldBox, newBox) => {
const box = getClientRect(newBox);
const isOut =
box.x < 0 ||
box.y < 0 ||
box.x + box.width > stageSize.width ||
box.y + box.height > stageSize.height;
if (isOut) {
return oldBox;
}
return newBox;
};
const syncShapeState = () => {
setShapes(currentShapes => currentShapes.map(shape => {
const node = shapeRefs.current.get(shape.id);
if (!node) return shape;
return {
...shape,
x: node.x(),
y: node.y(),
rotation: node.rotation(),
scaleX: node.scaleX(),
scaleY: node.scaleY(),
};
}));
};
// Handle drag for transformer group
const handleTransformerDrag = (e) => {
if (!trRef.current) return;
const nodes = trRef.current.nodes();
if (nodes.length === 0) return;
const boxes = nodes.map(node => node.getClientRect());
const box = getTotalBox(boxes);
nodes.forEach(shape => {
const absPos = shape.getAbsolutePosition();
const offsetX = box.x - absPos.x;
const offsetY = box.y - absPos.y;
const newAbsPos = { ...absPos };
if (box.x < 0) {
newAbsPos.x = -offsetX;
}
if (box.y < 0) {
newAbsPos.y = -offsetY;
}
if (box.x + box.width > stageSize.width) {
newAbsPos.x = stageSize.width - box.width - offsetX;
}
if (box.y + box.height > stageSize.height) {
newAbsPos.y = stageSize.height - box.height - offsetY;
}
shape.setAbsolutePosition(newAbsPos);
});
syncShapeState();
};
return (
{shapes.map(shape => (
{
if (node) shapeRefs.current.set(shape.id, node);
}}
x={shape.x}
y={shape.y}
width={shape.width}
height={shape.height}
fill={shape.fill}
rotation={shape.rotation}
scaleX={shape.scaleX}
scaleY={shape.scaleY}
draggable
onDragMove={handleTransformerDrag}
onDragEnd={syncShapeState}
/>
))}
);
};
export default LimitedDragAndResize;
```
```js
```
---
# Canvas Flip Image — Mirror and Flip Shapes on HTML5 Canvas
> Flip and mirror images or shapes on HTML5 Canvas with JavaScript. Learn how to flip horizontally, vertically, and rotate using Konva.js with interactive examples.
Source: https://konvajs.org/docs/sandbox/Mirror_Canvas_Shape.html
Flip any image or shape on an HTML5 Canvas — horizontally, vertically, or both. This technique is essential for building image editors, design tools, and canvas-based applications where users need to mirror content.
To flip any node with `Konva` you can use negative `scaleX` to flip it horizontally or `scaleY` to flip it vertically. The `scale` properties work relative to the origin of a node. For a rectangle that's the top-left corner; for a circle it's the center. You can change the origin with `offsetX` and `offsetY` — see the [Position vs Offset guide](/docs/posts/Position_vs_Offset.html) for details.
Depending on your use case, you may need to adjust `{x, y}` to keep the node in its original position after flipping.
**Instructions: click the flip buttons to see shapes mirror horizontally and vertically.**
```js
import Konva from 'konva';
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
var layer = new Konva.Layer();
stage.add(layer);
var text1 = new Konva.Text({
x: 180,
y: 50,
text: 'Default text with no offset. Its origin is in top left corner.',
align: 'center',
width: 200,
});
layer.add(text1);
var text2 = new Konva.Text({
text: 'Text with the origin in its center',
width: 200,
align: 'center',
y: 100,
x: 270,
});
layer.add(text2);
// set horizontal origin in the center of the text
text2.offsetX(text2.width() / 2);
var button = document.createElement('button');
button.innerText = 'Flip horizontally';
button.style.position = 'absolute';
button.style.top = '5px';
button.style.left = '5px';
document.body.appendChild(button);
button.addEventListener('click', () => {
layer.find('Text').forEach((text) => {
text.to({
scaleX: -text.scaleX(),
});
});
});
```
```js
import { Stage, Layer, Text } from 'react-konva';
import { useState, useRef } from 'react';
const App = () => {
const [texts, setTexts] = useState([
{
id: 1,
text: 'Default text with no offset. Its origin is in top left corner.',
x: 180,
y: 50,
width: 200,
align: 'center',
scaleX: 1,
offsetX: 0
},
{
id: 2,
text: 'Text with the origin in its center',
x: 270,
y: 100,
width: 200,
align: 'center',
scaleX: 1,
offsetX: 100 // Half of the width to center it
}
]);
const handleFlip = () => {
setTexts(texts.map(text => ({
...text,
scaleX: -text.scaleX
})));
};
return (
{texts.map((text) => (
))}
Flip horizontally
);
};
export default App;
```
```js
Flip horizontally
```
---
# Modify Curves with Anchor Points
> Interactively edit quadratic and Bezier curves by dragging anchor control points on a Konva canvas.
Source: https://konvajs.org/docs/sandbox/Modify_Curves_with_Anchor_Points.html
# How to modify line points with anchors?
This demo shows how to create interactive curves (quadratic and Bezier) that can be modified by dragging their anchor points. This technique is commonly used in vector graphic editors and gives users the ability to create and adjust custom curves.
**Instructions:** Use your mouse or finger to drag and drop the anchor points to modify the curvature of the quadratic curve (red) and the Bezier curve (blue).
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
// Create stage and layer
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
// Function to build anchor point
function buildAnchor(x, y) {
const anchor = new Konva.Circle({
x: x,
y: y,
radius: 20,
stroke: '#666',
fill: '#ddd',
strokeWidth: 2,
draggable: true,
});
layer.add(anchor);
// Add hover styling
anchor.on('mouseover', function () {
document.body.style.cursor = 'pointer';
this.strokeWidth(4);
});
anchor.on('mouseout', function () {
document.body.style.cursor = 'default';
this.strokeWidth(2);
});
// Update curves when anchor is moved
anchor.on('dragmove', function () {
updateDottedLines();
});
return anchor;
}
// Function to update dashed line points (showing control points)
function updateDottedLines() {
const q = quad;
const b = bezier;
const quadLinePath = layer.findOne('#quadLinePath');
const bezierLinePath = layer.findOne('#bezierLinePath');
// Update control point lines for quadratic curve
quadLinePath.points([
q.start.x(),
q.start.y(),
q.control.x(),
q.control.y(),
q.end.x(),
q.end.y(),
]);
// Update control point lines for bezier curve
bezierLinePath.points([
b.start.x(),
b.start.y(),
b.control1.x(),
b.control1.y(),
b.control2.x(),
b.control2.y(),
b.end.x(),
b.end.y(),
]);
}
// Create quadratic curve with custom shape
const quadraticLine = new Konva.Shape({
stroke: 'red',
strokeWidth: 4,
sceneFunc: (ctx, shape) => {
ctx.beginPath();
ctx.moveTo(quad.start.x(), quad.start.y());
ctx.quadraticCurveTo(
quad.control.x(),
quad.control.y(),
quad.end.x(),
quad.end.y()
);
ctx.fillStrokeShape(shape);
},
});
layer.add(quadraticLine);
// Create bezier curve with custom shape
const bezierLine = new Konva.Shape({
stroke: 'blue',
strokeWidth: 5,
sceneFunc: (ctx, shape) => {
ctx.beginPath();
ctx.moveTo(bezier.start.x(), bezier.start.y());
ctx.bezierCurveTo(
bezier.control1.x(),
bezier.control1.y(),
bezier.control2.x(),
bezier.control2.y(),
bezier.end.x(),
bezier.end.y()
);
ctx.fillStrokeShape(shape);
},
});
layer.add(bezierLine);
// Create dashed line to show control points for quadratic curve
const quadLinePath = new Konva.Line({
dash: [10, 10, 0, 10],
strokeWidth: 3,
stroke: 'black',
lineCap: 'round',
id: 'quadLinePath',
opacity: 0.3,
points: [0, 0],
});
layer.add(quadLinePath);
// Create dashed line to show control points for bezier curve
const bezierLinePath = new Konva.Line({
dash: [10, 10, 0, 10],
strokeWidth: 3,
stroke: 'black',
lineCap: 'round',
id: 'bezierLinePath',
opacity: 0.3,
points: [0, 0],
});
layer.add(bezierLinePath);
// Create anchor points for the quadratic curve
const quad = {
start: buildAnchor(60, 30),
control: buildAnchor(240, 110),
end: buildAnchor(80, 160),
};
// Create anchor points for the bezier curve
const bezier = {
start: buildAnchor(280, 20),
control1: buildAnchor(530, 40),
control2: buildAnchor(480, 150),
end: buildAnchor(300, 150),
};
// Update the control point lines
updateDottedLines();
```
```js
import React from 'react';
import { Stage, Layer, Circle, Line, Shape } from 'react-konva';
const ModifyCurvesDemo = () => {
const width = window.innerWidth;
const height = window.innerHeight;
const [quadPoints, setQuadPoints] = React.useState({
start: { x: 60, y: 30 },
control: { x: 240, y: 110 },
end: { x: 80, y: 160 },
});
const [bezierPoints, setBezierPoints] = React.useState({
start: { x: 280, y: 20 },
control1: { x: 530, y: 40 },
control2: { x: 480, y: 150 },
end: { x: 300, y: 150 },
});
const [hoveredAnchor, setHoveredAnchor] = React.useState(null);
const handleDragMove = (e, points, setPoints, pointName) => {
setPoints({
...points,
[pointName]: { x: e.target.x(), y: e.target.y() }
});
};
const handleCursor = (e, pointId, isEnter) => {
const stage = e.target.getStage();
stage.container().style.cursor = isEnter ? 'pointer' : 'default';
setHoveredAnchor(isEnter ? pointId : null);
};
const renderAnchor = (point, pointName, prefix, onDragMove) => (
handleCursor(e, prefix + pointName, true)}
onMouseLeave={e => handleCursor(e, prefix + pointName, false)}
/>
);
const quadAnchors = Object.entries(quadPoints).map(([name, point]) =>
renderAnchor(point, name, 'quad-', e => handleDragMove(e, quadPoints, setQuadPoints, name))
);
const bezierAnchors = Object.entries(bezierPoints).map(([name, point]) =>
renderAnchor(point, name, 'bezier-', e => handleDragMove(e, bezierPoints, setBezierPoints, name))
);
return (
{
ctx.beginPath();
ctx.moveTo(quadPoints.start.x, quadPoints.start.y);
ctx.quadraticCurveTo(
quadPoints.control.x, quadPoints.control.y,
quadPoints.end.x, quadPoints.end.y
);
ctx.fillStrokeShape(shape);
}}
stroke="red"
strokeWidth={4}
/>
{
ctx.beginPath();
ctx.moveTo(bezierPoints.start.x, bezierPoints.start.y);
ctx.bezierCurveTo(
bezierPoints.control1.x, bezierPoints.control1.y,
bezierPoints.control2.x, bezierPoints.control2.y,
bezierPoints.end.x, bezierPoints.end.y
);
ctx.fillStrokeShape(shape);
}}
stroke="blue"
strokeWidth={5}
/>
{quadAnchors}
{bezierAnchors}
);
};
export default ModifyCurvesDemo;
```
```js
handleQuadDragMove(e, name)"
@mouseenter="handleMouseEnter(`quad-${name}`, e)"
@mouseleave="handleMouseLeave(`quad-${name}`, e)"
/>
handleBezierDragMove(e, name)"
@mouseenter="handleMouseEnter(`bezier-${name}`, e)"
@mouseleave="handleMouseLeave(`bezier-${name}`, e)"
/>
```
---
# Modify Shape Color on Click
> Click on Konva canvas shapes to dynamically change their fill color using click event handlers.
Source: https://konvajs.org/docs/sandbox/Modify_Shape_Color_on_Click.html
**Instructions: Click on a shape to change its color**
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
const triangle = new Konva.RegularPolygon({
x: 80,
y: 120,
sides: 3,
radius: 50,
fill: '#00D2FF',
stroke: 'black',
strokeWidth: 4,
});
triangle.on('click', function () {
const fill = this.fill() === 'yellow' ? '#00D2FF' : 'yellow';
this.fill(fill);
});
layer.add(triangle);
const circle = new Konva.Circle({
x: 180,
y: 120,
radius: 50,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
circle.on('click', function () {
const fill = this.fill() === 'red' ? '#00d00f' : 'red';
this.fill(fill);
});
layer.add(circle);
stage.add(layer);
```
```js
import { Stage, Layer, RegularPolygon, Circle } from 'react-konva';
import { useState } from 'react';
const App = () => {
const [triangleColor, setTriangleColor] = useState('#00D2FF');
const [circleColor, setCircleColor] = useState('red');
const handleTriangleClick = () => {
setTriangleColor(triangleColor === 'yellow' ? '#00D2FF' : 'yellow');
};
const handleCircleClick = () => {
setCircleColor(circleColor === 'red' ? '#00d00f' : 'red');
};
return (
);
};
export default App;
```
```js
```
---
# Multi-touch Scale Shape Tutorial
> Scale individual Konva shapes with multi-touch pinch zoom gestures on mobile devices.
Source: https://konvajs.org/docs/sandbox/Multi-touch_Scale_Shape.html
Note: This lab only works on devices that support multi-touch gestures such as iOS because it makes use of multiple touch events.
**Instructions:** Using a mobile device that supports multi-touch gestures such as iOS, drag and drop a shape by touching it and then dragging your finger across the screen, activate a shape by tapping on it, and scale an active shape by pinching the screen.
```js
import Konva from 'konva';
// by default Konva prevent some events when node is dragging
// it improve the performance and work well for 95% of cases
// we need to enable all events on Konva, even when we are dragging a node
// so it triggers touchmove correctly
Konva.hitOnDragEnabled = true;
const width = window.innerWidth;
const height = window.innerHeight;
let lastDist = 0;
let startScale = 1;
let activeShape = null;
function getDistance(p1, p2) {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
}
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
draggable: true,
x: width / 2,
y: height / 2,
offset: {
x: width / 2,
y: height / 2,
},
});
const layer = new Konva.Layer();
const triangle = new Konva.RegularPolygon({
x: 190,
y: stage.height() / 2,
sides: 3,
radius: 80,
fill: 'green',
stroke: 'black',
strokeWidth: 4,
draggable: true,
name: 'triangle',
});
const circle = new Konva.Circle({
x: 380,
y: stage.height() / 2,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
draggable: true,
name: 'circle',
});
stage.on('tap', function (evt) {
// set active shape
const shape = evt.target;
activeShape =
activeShape && activeShape.getName() === shape.getName()
? null
: shape;
// sync scene graph
triangle.setAttrs({
fill:
activeShape && activeShape.getName() === triangle.getName()
? '#78E7FF'
: 'green',
stroke:
activeShape && activeShape.getName() === triangle.getName()
? 'blue'
: 'black',
});
circle.setAttrs({
fill:
activeShape && activeShape.getName() === circle.getName()
? '#78E7FF'
: 'red',
stroke:
activeShape && activeShape.getName() === circle.getName()
? 'blue'
: 'black',
});
});
stage.getContent().addEventListener(
'touchmove',
function (evt) {
const touch1 = evt.touches[0];
const touch2 = evt.touches[1];
if (touch1 && touch2 && activeShape) {
const dist = getDistance(
{
x: touch1.clientX,
y: touch1.clientY,
},
{
x: touch2.clientX,
y: touch2.clientY,
}
);
if (!lastDist) {
lastDist = dist;
}
const scale = (activeShape.scaleX() * dist) / lastDist;
activeShape.scaleX(scale);
activeShape.scaleY(scale);
lastDist = dist;
}
},
false
);
stage.getContent().addEventListener(
'touchend',
function () {
lastDist = 0;
},
false
);
layer.add(triangle);
layer.add(circle);
stage.add(layer);
```
```js
import { Stage, Layer, RegularPolygon, Circle } from 'react-konva';
import { useState, useEffect, useRef } from 'react';
import Konva from 'konva';
const getDistance = (p1, p2) => {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
};
const App = () => {
const stageRef = useRef(null);
const activeShape = useRef(null);
const lastDist = useRef(0);
const [shapes, setShapes] = useState({
triangle: {
x: 190,
y: window.innerHeight / 2,
scaleX: 1,
scaleY: 1,
fill: 'green',
stroke: 'black'
},
circle: {
x: 380,
y: window.innerHeight / 2,
scaleX: 1,
scaleY: 1,
fill: 'red',
stroke: 'black'
}
});
const handleTap = (shapeName) => {
const nextActiveShape = activeShape.current === shapeName ? null : shapeName;
activeShape.current = nextActiveShape;
setShapes(prev => ({
...prev,
triangle: {
...prev.triangle,
fill: nextActiveShape === 'triangle' ? '#78E7FF' : 'green',
stroke: nextActiveShape === 'triangle' ? 'blue' : 'black'
},
circle: {
...prev.circle,
fill: nextActiveShape === 'circle' ? '#78E7FF' : 'red',
stroke: nextActiveShape === 'circle' ? 'blue' : 'black'
}
}));
};
const handleDragMove = (shapeName, e) => {
const { x, y } = e.target.position();
setShapes(prev => ({
...prev,
[shapeName]: { ...prev[shapeName], x, y }
}));
};
useEffect(() => {
const previousHitOnDragEnabled = Konva.hitOnDragEnabled;
Konva.hitOnDragEnabled = true;
const content = stageRef.current.container();
const handleTouchMove = (evt) => {
const touch1 = evt.touches[0];
const touch2 = evt.touches[1];
const shapeName = activeShape.current;
if (touch1 && touch2 && shapeName) {
const dist = getDistance(
{
x: touch1.clientX,
y: touch1.clientY,
},
{
x: touch2.clientX,
y: touch2.clientY,
}
);
if (!lastDist.current) {
lastDist.current = dist;
return;
}
const scaleBy = dist / lastDist.current;
setShapes(prev => ({
...prev,
[shapeName]: {
...prev[shapeName],
scaleX: prev[shapeName].scaleX * scaleBy,
scaleY: prev[shapeName].scaleY * scaleBy
}
}));
lastDist.current = dist;
}
};
const handleTouchEnd = () => {
lastDist.current = 0;
};
content.addEventListener('touchmove', handleTouchMove, false);
content.addEventListener('touchend', handleTouchEnd, false);
content.addEventListener('touchcancel', handleTouchEnd, false);
return () => {
content.removeEventListener('touchmove', handleTouchMove);
content.removeEventListener('touchend', handleTouchEnd);
content.removeEventListener('touchcancel', handleTouchEnd);
Konva.hitOnDragEnabled = previousHitOnDragEnabled;
};
}, []);
return (
handleDragMove('triangle', e)}
onTap={() => handleTap('triangle')}
/>
handleDragMove('circle', e)}
onTap={() => handleTap('circle')}
/>
);
};
export default App;
```
```js
```
---
# Multi-touch Canvas scale with pinch zoom
> Enable pan and pinch-to-zoom on the entire Konva stage using multi-touch gestures on mobile devices.
Source: https://konvajs.org/docs/sandbox/Multi-touch_Scale_Stage.html
## How to enable pan and pinch zoom for canvas stage?
Inside `touchmove` callback we can get access to all native properties of touch events with `e.evt.touches`. So we just need to manually calculate position and scale properties of the stage, when two pointers are used in `touchmove`.
Note: This lab only works on devices that support multi-touch gestures because it makes use of multiple touch events.
**Instructions:** Using a mobile device that supports multi-touch gestures, use two fingers to zoom in or out of the stage.
```js
import Konva from 'konva';
// by default Konva prevent some events when node is dragging
// it improve the performance and work well for 95% of cases
// we need to enable all events on Konva, even when we are dragging a node
// so it triggers touchmove correctly
Konva.hitOnDragEnabled = true;
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
draggable: true,
});
const layer = new Konva.Layer();
const triangle = new Konva.RegularPolygon({
x: 190,
y: stage.height() / 2,
sides: 3,
radius: 80,
fill: 'green',
stroke: 'black',
strokeWidth: 4,
});
const circle = new Konva.Circle({
x: 380,
y: stage.height() / 2,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
function getDistance(p1, p2) {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
}
function getCenter(p1, p2) {
return {
x: (p1.x + p2.x) / 2,
y: (p1.y + p2.y) / 2,
};
}
let lastCenter = null;
let lastDist = 0;
let dragStopped = false;
stage.on('touchmove', function (e) {
e.evt.preventDefault();
const touch1 = e.evt.touches[0];
const touch2 = e.evt.touches[1];
// we need to restore dragging, if it was cancelled by multi-touch
if (touch1 && !touch2 && !stage.isDragging() && dragStopped) {
stage.startDrag();
dragStopped = false;
}
if (touch1 && touch2) {
// if the stage was under Konva's drag&drop
// we need to stop it, and implement our own pan logic with two pointers
if (stage.isDragging()) {
dragStopped = true;
stage.stopDrag();
}
const rect = stage.container().getBoundingClientRect();
const p1 = {
x: touch1.clientX - rect.left,
y: touch1.clientY - rect.top,
};
const p2 = {
x: touch2.clientX - rect.left,
y: touch2.clientY - rect.top,
};
if (!lastCenter) {
lastCenter = getCenter(p1, p2);
return;
}
const newCenter = getCenter(p1, p2);
const dist = getDistance(p1, p2);
if (!lastDist) {
lastDist = dist;
}
// local coordinates of center point
const pointTo = {
x: (newCenter.x - stage.x()) / stage.scaleX(),
y: (newCenter.y - stage.y()) / stage.scaleX(),
};
const scale = stage.scaleX() * (dist / lastDist);
stage.scaleX(scale);
stage.scaleY(scale);
// calculate new position of the stage
const dx = newCenter.x - lastCenter.x;
const dy = newCenter.y - lastCenter.y;
const newPos = {
x: newCenter.x - pointTo.x * scale + dx,
y: newCenter.y - pointTo.y * scale + dy,
};
stage.position(newPos);
lastDist = dist;
lastCenter = newCenter;
}
});
stage.on('touchend', function () {
lastDist = 0;
lastCenter = null;
});
layer.add(triangle);
layer.add(circle);
stage.add(layer);
```
```js
import { Stage, Layer, RegularPolygon, Circle } from 'react-konva';
import { useState, useEffect, useCallback } from 'react';
import Konva from 'konva';
const App = () => {
const [stagePos, setStagePos] = useState({ x: 0, y: 0 });
const [stageScale, setStageScale] = useState({ x: 1, y: 1 });
const [lastCenter, setLastCenter] = useState(null);
const [lastDist, setLastDist] = useState(0);
const [dragStopped, setDragStopped] = useState(false);
const getDistance = (p1, p2) => {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
};
const getCenter = (p1, p2) => {
return {
x: (p1.x + p2.x) / 2,
y: (p1.y + p2.y) / 2,
};
};
useEffect(() => {
const previousHitOnDragEnabled = Konva.hitOnDragEnabled;
Konva.hitOnDragEnabled = true;
return () => {
Konva.hitOnDragEnabled = previousHitOnDragEnabled;
};
}, []);
const handleTouchMove = useCallback((e) => {
e.evt.preventDefault();
const touch1 = e.evt.touches[0];
const touch2 = e.evt.touches[1];
const stage = e.target.getStage();
// we need to restore dragging, if it was cancelled by multi-touch
if (touch1 && !touch2 && !stage.isDragging() && dragStopped) {
stage.startDrag();
setDragStopped(false);
}
if (touch1 && touch2) {
// if the stage was under Konva's drag&drop
// we need to stop it, and implement our own pan logic with two pointers
if (stage.isDragging()) {
stage.stopDrag();
setDragStopped(true);
}
const rect = stage.container().getBoundingClientRect();
const p1 = {
x: touch1.clientX - rect.left,
y: touch1.clientY - rect.top,
};
const p2 = {
x: touch2.clientX - rect.left,
y: touch2.clientY - rect.top,
};
if (!lastCenter) {
setLastCenter(getCenter(p1, p2));
return;
}
const newCenter = getCenter(p1, p2);
const dist = getDistance(p1, p2);
if (!lastDist) {
setLastDist(dist);
return;
}
// local coordinates of center point
const pointTo = {
x: (newCenter.x - stagePos.x) / stageScale.x,
y: (newCenter.y - stagePos.y) / stageScale.x,
};
const scale = stageScale.x * (dist / lastDist);
setStageScale({ x: scale, y: scale });
// calculate new position of the stage
const dx = newCenter.x - lastCenter.x;
const dy = newCenter.y - lastCenter.y;
setStagePos({
x: newCenter.x - pointTo.x * scale + dx,
y: newCenter.y - pointTo.y * scale + dy,
});
setLastDist(dist);
setLastCenter(newCenter);
}
}, [dragStopped, lastCenter, lastDist, stagePos, stageScale]);
const handleTouchEnd = () => {
setLastDist(0);
setLastCenter(null);
};
const handleDragEnd = (e) => {
setDragStopped(false);
// Ensure stage position is synchronized with our reactive state
const stage = e.target.getStage();
setStagePos({ x: stage.x(), y: stage.y() });
};
return (
);
};
export default App;
```
```js
```
---
# How to access native 2d context
> Access the native HTML5 Canvas 2D context from Konva to perform custom manual drawing and use it as a Konva.Image.
Source: https://konvajs.org/docs/sandbox/Native_Context_Access.html
## How to access the native 2D canvas context from Konva
Konva gives you an object model for drawing shapes on a canvas. An application starts with a Stage in a `div`. The Stage contains one or more Layers. Each Layer uses Canvas elements.
You can access the internal Canvas context and draw without Konva shapes. This method is not safe. Konva controls Layer drawing, so it can erase the manual drawing. Exports such as `stage.toDataURL()` can also omit it.
Use one of these methods for manual drawing:
1. [Use a custom shape](/docs/shapes/Custom.html).
2. Create a Canvas element and use it as the source of a `Konva.Image`.
Both methods keep the drawing in Konva's scene graph and export process.
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
// if you want to make something with native 2d canvas
// we can create it and use it for Konva.Image
const canvas = document.createElement('canvas');
canvas.width = 200;
canvas.height = 150;
const ctx = canvas.getContext('2d');
const image = new Konva.Image({
x: 50,
y: 50,
image: canvas,
draggable: true,
});
layer.add(image);
// make manual drawings
ctx.fillStyle = 'blue';
ctx.fillRect(5, 5, canvas.width - 10, canvas.height / 2);
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.arc(100, 75, 50, 0, 2 * Math.PI);
ctx.fill();
// such as canvas is updated we need to redraw the layer
layer.batchDraw();
```
```js
import { Stage, Layer, Image } from 'react-konva';
import { useMemo, useState } from 'react';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
const canvas = useMemo(() => {
const canvas = document.createElement('canvas');
canvas.width = 200;
canvas.height = 150;
const ctx = canvas.getContext('2d');
// make manual drawings
ctx.fillStyle = 'blue';
ctx.fillRect(5, 5, canvas.width - 10, canvas.height / 2);
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.arc(100, 75, 50, 0, 2 * Math.PI);
ctx.fill();
return canvas;
}, []);
return (
{
setPosition({
x: e.target.x(),
y: e.target.y(),
});
}}
/>
);
};
export default App;
```
```js
```
---
# Canvas Snapping and Alignment Guides — Snap Shapes While Dragging
> Snap draggable shapes to edges and centers of other objects and stage boundaries with guide lines in Konva.
Source: https://konvajs.org/docs/sandbox/Objects_Snapping.html
## How to snap draggable shapes to each other
This demo snaps a dragged shape to the stage and other shapes. Each shape can
snap at its left, center, right, top, middle, and bottom edges.
These external examples show grid snapping:
1. [Snap to grid post](https://medium.com/@pierrebleroux/snap-to-grid-with-konvajs-c41eae97c13f)
2. [Snap to grid demo](https://codepen.io/pierrebleroux/pen/gGpvxJ)
**Instructions:** Drag a rectangle near another rectangle or a stage edge.
The blue guides show the active snap positions.
```js
import Konva from 'konva';
var width = window.innerWidth;
var height = window.innerHeight;
var GUIDELINE_OFFSET = 5;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
var layer = new Konva.Layer();
stage.add(layer);
// first generate random rectangles
for (var i = 0; i < 5; i++) {
layer.add(
new Konva.Rect({
x: Math.random() * stage.width(),
y: Math.random() * stage.height(),
width: 50 + Math.random() * 50,
height: 50 + Math.random() * 50,
fill: Konva.Util.getRandomColor(),
rotation: Math.random() * 360,
draggable: true,
name: 'object',
})
);
}
// were can we snap our objects?
function getLineGuideStops(skipShape) {
// we can snap to stage borders and the center of the stage
var vertical = [0, stage.width() / 2, stage.width()];
var horizontal = [0, stage.height() / 2, stage.height()];
// and we snap over edges and center of each object on the canvas
stage.find('.object').forEach((guideItem) => {
if (guideItem === skipShape) {
return;
}
var box = guideItem.getClientRect();
// and we can snap to all edges of shapes
vertical.push([box.x, box.x + box.width, box.x + box.width / 2]);
horizontal.push([box.y, box.y + box.height, box.y + box.height / 2]);
});
return {
vertical: vertical.flat(),
horizontal: horizontal.flat(),
};
}
// what points of the object will trigger to snapping?
// it can be just center of the object
// but we will enable all edges and center
function getObjectSnappingEdges(node) {
var box = node.getClientRect();
var absPos = node.absolutePosition();
return {
vertical: [
{
guide: Math.round(box.x),
offset: Math.round(absPos.x - box.x),
snap: 'start',
},
{
guide: Math.round(box.x + box.width / 2),
offset: Math.round(absPos.x - box.x - box.width / 2),
snap: 'center',
},
{
guide: Math.round(box.x + box.width),
offset: Math.round(absPos.x - box.x - box.width),
snap: 'end',
},
],
horizontal: [
{
guide: Math.round(box.y),
offset: Math.round(absPos.y - box.y),
snap: 'start',
},
{
guide: Math.round(box.y + box.height / 2),
offset: Math.round(absPos.y - box.y - box.height / 2),
snap: 'center',
},
{
guide: Math.round(box.y + box.height),
offset: Math.round(absPos.y - box.y - box.height),
snap: 'end',
},
],
};
}
// find all snapping possibilities
function getGuides(lineGuideStops, itemBounds) {
var resultV = [];
var resultH = [];
lineGuideStops.vertical.forEach((lineGuide) => {
itemBounds.vertical.forEach((itemBound) => {
var diff = Math.abs(lineGuide - itemBound.guide);
// if the distance between guild line and object snap point is close we can consider this for snapping
if (diff < GUIDELINE_OFFSET) {
resultV.push({
lineGuide: lineGuide,
diff: diff,
snap: itemBound.snap,
offset: itemBound.offset,
});
}
});
});
lineGuideStops.horizontal.forEach((lineGuide) => {
itemBounds.horizontal.forEach((itemBound) => {
var diff = Math.abs(lineGuide - itemBound.guide);
if (diff < GUIDELINE_OFFSET) {
resultH.push({
lineGuide: lineGuide,
diff: diff,
snap: itemBound.snap,
offset: itemBound.offset,
});
}
});
});
var guides = [];
// find closest snap
var minV = resultV.sort((a, b) => a.diff - b.diff)[0];
var minH = resultH.sort((a, b) => a.diff - b.diff)[0];
if (minV) {
guides.push({
lineGuide: minV.lineGuide,
offset: minV.offset,
orientation: 'V',
snap: minV.snap,
});
}
if (minH) {
guides.push({
lineGuide: minH.lineGuide,
offset: minH.offset,
orientation: 'H',
snap: minH.snap,
});
}
return guides;
}
function drawGuides(guides) {
guides.forEach((lg) => {
if (lg.orientation === 'H') {
var line = new Konva.Line({
points: [-6000, 0, 6000, 0],
stroke: 'rgb(0, 161, 255)',
strokeWidth: 1,
name: 'guid-line',
dash: [4, 6],
});
layer.add(line);
line.absolutePosition({
x: 0,
y: lg.lineGuide,
});
} else if (lg.orientation === 'V') {
var line = new Konva.Line({
points: [0, -6000, 0, 6000],
stroke: 'rgb(0, 161, 255)',
strokeWidth: 1,
name: 'guid-line',
dash: [4, 6],
});
layer.add(line);
line.absolutePosition({
x: lg.lineGuide,
y: 0,
});
}
});
}
layer.on('dragmove', function (e) {
// clear all previous lines on the screen
layer.find('.guid-line').forEach((l) => l.destroy());
// find possible snapping lines
var lineGuideStops = getLineGuideStops(e.target);
// find snapping points of current object
var itemBounds = getObjectSnappingEdges(e.target);
// now find where can we snap current object
var guides = getGuides(lineGuideStops, itemBounds);
// do nothing of no snapping
if (!guides.length) {
return;
}
drawGuides(guides);
var absPos = e.target.absolutePosition();
// now force object position
guides.forEach((lg) => {
switch (lg.orientation) {
case 'V': {
absPos.x = lg.lineGuide + lg.offset;
break;
}
case 'H': {
absPos.y = lg.lineGuide + lg.offset;
break;
}
}
});
e.target.absolutePosition(absPos);
});
layer.on('dragend', function (e) {
// clear all previous lines on the screen
layer.find('.guid-line').forEach((l) => l.destroy());
});
```
```jsx
import { useState } from 'react';
import { Stage, Layer, Rect, Line } from 'react-konva';
const GUIDELINE_OFFSET = 5;
const WIDTH = window.innerWidth;
const HEIGHT = 500;
const initialShapes = [
{ id: 'one', x: 80, y: 90, width: 90, height: 70, fill: '#ff6b6b', rotation: 8 },
{ id: 'two', x: 300, y: 230, width: 110, height: 80, fill: '#4dabf7', rotation: -6 },
{ id: 'three', x: 560, y: 100, width: 80, height: 100, fill: '#69db7c', rotation: 4 },
];
function getLineGuideStops(stage, skipShape) {
const vertical = [0, stage.width() / 2, stage.width()];
const horizontal = [0, stage.height() / 2, stage.height()];
stage.find('.object').forEach((guideItem) => {
if (guideItem === skipShape) {
return;
}
const box = guideItem.getClientRect();
vertical.push(box.x, box.x + box.width / 2, box.x + box.width);
horizontal.push(box.y, box.y + box.height / 2, box.y + box.height);
});
return { vertical, horizontal };
}
function getObjectSnappingEdges(node) {
const box = node.getClientRect();
const absolutePosition = node.absolutePosition();
return {
vertical: [
{
guide: Math.round(box.x),
offset: Math.round(absolutePosition.x - box.x),
},
{
guide: Math.round(box.x + box.width / 2),
offset: Math.round(absolutePosition.x - box.x - box.width / 2),
},
{
guide: Math.round(box.x + box.width),
offset: Math.round(absolutePosition.x - box.x - box.width),
},
],
horizontal: [
{
guide: Math.round(box.y),
offset: Math.round(absolutePosition.y - box.y),
},
{
guide: Math.round(box.y + box.height / 2),
offset: Math.round(absolutePosition.y - box.y - box.height / 2),
},
{
guide: Math.round(box.y + box.height),
offset: Math.round(absolutePosition.y - box.y - box.height),
},
],
};
}
function getGuides(lineGuideStops, itemBounds) {
const verticalMatches = [];
const horizontalMatches = [];
lineGuideStops.vertical.forEach((lineGuide) => {
itemBounds.vertical.forEach((itemBound) => {
const diff = Math.abs(lineGuide - itemBound.guide);
if (diff < GUIDELINE_OFFSET) {
verticalMatches.push({
lineGuide,
diff,
offset: itemBound.offset,
orientation: 'V',
});
}
});
});
lineGuideStops.horizontal.forEach((lineGuide) => {
itemBounds.horizontal.forEach((itemBound) => {
const diff = Math.abs(lineGuide - itemBound.guide);
if (diff < GUIDELINE_OFFSET) {
horizontalMatches.push({
lineGuide,
diff,
offset: itemBound.offset,
orientation: 'H',
});
}
});
});
const closestVertical = verticalMatches.sort((a, b) => a.diff - b.diff)[0];
const closestHorizontal = horizontalMatches.sort((a, b) => a.diff - b.diff)[0];
return [closestVertical, closestHorizontal].filter(Boolean);
}
const App = () => {
const [shapes, setShapes] = useState(initialShapes);
const [guides, setGuides] = useState([]);
const handleDragMove = (event, id) => {
const node = event.target;
const stage = node.getStage();
const lineGuideStops = getLineGuideStops(stage, node);
const itemBounds = getObjectSnappingEdges(node);
const nextGuides = getGuides(lineGuideStops, itemBounds);
setGuides(nextGuides);
if (nextGuides.length) {
const absolutePosition = node.absolutePosition();
nextGuides.forEach((guide) => {
if (guide.orientation === 'V') {
absolutePosition.x = guide.lineGuide + guide.offset;
} else {
absolutePosition.y = guide.lineGuide + guide.offset;
}
});
node.absolutePosition(absolutePosition);
}
const { x, y } = node.position();
setShapes((currentShapes) =>
currentShapes.map((shape) =>
shape.id === id ? { ...shape, x, y } : shape
)
);
};
const handleDragEnd = (event, id) => {
setGuides([]);
setShapes((currentShapes) =>
currentShapes.map((shape) =>
shape.id === id
? { ...shape, x: event.target.x(), y: event.target.y() }
: shape
)
);
};
return (
{shapes.map((shape) => (
handleDragMove(event, shape.id)}
onDragEnd={(event) => handleDragEnd(event, shape.id)}
/>
))}
{guides.map((guide, index) =>
guide.orientation === 'H' ? (
) : (
)
)}
);
};
export default App;
```
---
# Physics Simulator with Curve Detection
> Simulate physics with gravity, wall bouncing, and Bezier curve collision detection on an HTML5 canvas using Konva.
Source: https://konvajs.org/docs/sandbox/Physics_Simulator.html
**Instructions: Throw the ball around with your cursor.**
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
/*
* Vector math functions
*/
function dot(a, b) {
return a.x * b.x + a.y * b.y;
}
function magnitude(a) {
return Math.sqrt(a.x * a.x + a.y * a.y);
}
function normalize(a) {
var mag = magnitude(a);
if (mag === 0) {
return {
x: 0,
y: 0,
};
} else {
return {
x: a.x / mag,
y: a.y / mag,
};
}
}
function add(a, b) {
return {
x: a.x + b.x,
y: a.y + b.y,
};
}
function angleBetween(a, b) {
return Math.acos(dot(a, b) / (magnitude(a) * magnitude(b)));
}
function rotate(a, angle) {
var ca = Math.cos(angle);
var sa = Math.sin(angle);
var rx = a.x * ca - a.y * sa;
var ry = a.x * sa + a.y * ca;
return {
x: rx * -1,
y: ry * -1,
};
}
function invert(a) {
return {
x: a.x * -1,
y: a.y * -1,
};
}
/*
* this cross product function has been simplified by
* setting x and y to zero because vectors a and b
* lie in the canvas plane
*/
function cross(a, b) {
return {
x: 0,
y: 0,
z: a.x * b.y - b.x * a.y,
};
}
function getNormal(curve, ball) {
var curveLayer = curve.getLayer();
var context = curveLayer.getContext();
var testRadius = 20;
// pixels
var totalX = 0;
var totalY = 0;
var x = ball.x();
var y = ball.y();
/*
* check various points around the center point
* to determine the normal vector
*/
for (var n = 0; n < 20; n++) {
var angle = (n * 2 * Math.PI) / 20;
var offsetX = testRadius * Math.cos(angle);
var offsetY = testRadius * Math.sin(angle);
var testX = x + offsetX;
var testY = y + offsetY;
if (!context._context.isPointInPath(testX, testY)) {
totalX += offsetX;
totalY += offsetY;
}
}
var normal;
if (totalX === 0 && totalY === 0) {
normal = {
x: 0,
y: -1,
};
} else {
normal = {
x: totalX,
y: totalY,
};
}
return normalize(normal);
}
function handleCurveCollision(ball, curve) {
var curveLayer = curve.getLayer();
var x = ball.x();
var y = ball.y();
var curveDamper = 0.05;
// 5% energy loss
if (curveLayer.getIntersection({ x: x, y: y })) {
var normal = getNormal(curve, ball);
if (normal !== null) {
var angleToNormal = angleBetween(normal, invert(ball.velocity));
var crossProduct = cross(normal, ball.velocity);
var polarity = crossProduct.z > 0 ? 1 : -1;
var collisonAngle = polarity * angleToNormal * 2;
var collisionVector = rotate(ball.velocity, collisonAngle);
ball.velocity.x = collisionVector.x;
ball.velocity.y = collisionVector.y;
ball.velocity.x *= 1 - curveDamper;
ball.velocity.y *= 1 - curveDamper;
x += normal.x;
if (ball.velocity.y > 0.1) {
y += normal.y;
} else {
y += normal.y / 10;
}
ball.x(x).y(y);
}
tween.finish();
}
}
function updateBall(frame) {
var timeDiff = frame.timeDiff;
var stage = ball.getStage();
var height = stage.height();
var width = stage.width();
var x = ball.x();
var y = ball.y();
var radius = ball.radius();
tween.reverse();
// physics variables
var gravity = 10;
// px / second^2
var speedIncrementFromGravityEachFrame = (gravity * timeDiff) / 1000;
var collisionDamper = 0.2;
// 20% energy loss
var floorFriction = 5;
// px / second^2
var floorFrictionSpeedReduction = (floorFriction * timeDiff) / 1000;
// if ball is being dragged and dropped
if (ball.isDragging()) {
var mousePos = stage.getPointerPosition();
if (mousePos) {
var mouseX = mousePos.x;
var mouseY = mousePos.y;
var c = 0.06 * timeDiff;
ball.velocity = {
x: c * (mouseX - ball.lastMouseX),
y: c * (mouseY - ball.lastMouseY),
};
ball.lastMouseX = mouseX;
ball.lastMouseY = mouseY;
}
} else {
// gravity
ball.velocity.y += speedIncrementFromGravityEachFrame;
x += ball.velocity.x;
y += ball.velocity.y;
// ceiling condition
if (y < radius) {
y = radius;
ball.velocity.y *= -1;
ball.velocity.y *= 1 - collisionDamper;
}
// floor condition
if (y > height - radius) {
y = height - radius;
ball.velocity.y *= -1;
ball.velocity.y *= 1 - collisionDamper;
}
// floor friction
if (y == height - radius) {
if (ball.velocity.x > 0.1) {
ball.velocity.y -= floorFrictionSpeedReduction;
} else if (ball.velocity.x < -0.1) {
ball.velocity.x += floorFrictionSpeedReduction;
} else {
ball.velocity.x = 0;
}
}
// right wall condition
if (x > width - radius) {
x = width - radius;
ball.velocity.x *= -1;
ball.velocity.x *= 1 - collisionDamper;
}
// left wall condition
if (x < radius) {
x = radius;
ball.velocity.x *= -1;
ball.velocity.x *= 1 - collisionDamper;
}
ball.position({ x: x, y: y });
/*
* if the ball comes into contact with the
* curve, then bounce it in the direction of the
* curve's surface normal
*/
var collision = handleCurveCollision(ball, curve);
}
}
// create stage
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
// create separate layers for curve and ball
const curveLayer = new Konva.Layer();
const ballLayer = new Konva.Layer();
// create curve with original bezier curve
const curve = new Konva.Shape({
sceneFunc: function (context) {
context.beginPath();
context.moveTo(40, height);
context.bezierCurveTo(
width * 0.2,
-1 * height * 0.5,
width * 0.7,
height * 1.3,
width,
height * 0.5
);
context.lineTo(width, height);
context.lineTo(40, height);
context.closePath();
context.fillShape(this);
},
fill: '#8dbdff',
});
curveLayer.add(curve);
// create ball with original styling
const ball = new Konva.Circle({
x: 190,
y: 20,
radius: 20,
fill: 'blue',
draggable: true,
opacity: 0.8,
});
ball.velocity = {
x: 0,
y: 0,
};
// add original event handlers
ball.on('dragstart', function () {
ball.velocity = {
x: 0,
y: 0,
};
anim.start();
});
ball.on('mousedown', function () {
anim.stop();
});
ball.on('mouseover', function () {
document.body.style.cursor = 'pointer';
});
ball.on('mouseout', function () {
document.body.style.cursor = 'default';
});
ballLayer.add(ball);
// add layers to stage in correct order
stage.add(curveLayer);
stage.add(ballLayer);
// add tween with original styling
const tween = new Konva.Tween({
node: ball,
fill: 'red',
duration: 0.3,
easing: Konva.Easings.EaseOut,
});
// add animation
const anim = new Konva.Animation(function (frame) {
updateBall(frame);
}, ballLayer);
anim.start();
```
---
# Planets Image Map
> Create an interactive image map of planets with hover tooltips and a toggleable overlay using Konva.
Source: https://konvajs.org/docs/sandbox/Planets_Image_Map.html
**Instructions:** Mouse over the planets to see their names and use the check box to show and hide the map overlay.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const planetsLayer = new Konva.Layer();
const circlesLayer = new Konva.Layer();
const messageLayer = new Konva.Layer();
stage.add(planetsLayer);
stage.add(circlesLayer);
stage.add(messageLayer);
const text = new Konva.Text({
x: 10,
y: 10,
fontFamily: 'Calibri',
fontSize: 24,
text: '',
fill: 'white',
});
messageLayer.add(text);
function writeMessage(message) {
text.text(message);
}
const planets = {
Mercury: {
x: 46,
y: 126,
radius: 32,
},
Venus: {
x: 179,
y: 126,
radius: 79,
},
Earth: {
x: 366,
y: 127,
radius: 85,
},
Mars: {
x: 515,
y: 127,
radius: 45,
},
};
// create checkbox
const container = document.createElement('div');
container.style.position = 'absolute';
container.style.top = '10px';
container.style.left = '10px';
container.style.zIndex = '99999';
container.innerHTML = `
Show map overlay
`;
document.body.appendChild(container);
// draw shape overlays
for (const key in planets) {
const planet = planets[key];
const planetOverlay = new Konva.Circle({
x: planet.x,
y: planet.y,
radius: planet.radius,
});
planetOverlay.on('mouseover', () => {
writeMessage(key);
});
planetOverlay.on('mouseout', () => {
writeMessage('');
});
circlesLayer.add(planetOverlay);
}
const checkbox = document.getElementById('checkbox');
checkbox.addEventListener('click', () => {
const shapes = circlesLayer.getChildren();
shapes.forEach(shape => {
const f = shape.fill();
shape.fill(f === 'red' ? null : 'red');
});
});
// load the image
Konva.Image.fromURL('https://konvajs.org/assets/planets.png', (planetsImage) => {
planetsLayer.add(planetsImage);
});
````
```js
import { Stage, Layer, Image, Circle, Text } from 'react-konva';
import { useState } from 'react';
import useImage from 'use-image';
const planets = {
Mercury: {
x: 46,
y: 126,
radius: 32,
},
Venus: {
x: 179,
y: 126,
radius: 79,
},
Earth: {
x: 366,
y: 127,
radius: 85,
},
Mars: {
x: 515,
y: 127,
radius: 45,
},
};
const CheckboxStyles = {
container: {
position: 'absolute',
left: '10px',
top: '10px',
zIndex: 99999,
},
label: {
color: 'white',
display: 'flex',
alignItems: 'center',
gap: '5px',
cursor: 'pointer',
},
input: {
cursor: 'pointer',
},
};
const App = () => {
const [message, setMessage] = useState('');
const [showOverlay, setShowOverlay] = useState(false);
const [planetsImage] = useImage('https://konvajs.org/assets/planets.png');
return (
<>
setShowOverlay(e.target.checked)}
/>
Show map overlay
{planetsImage && }
{Object.entries(planets).map(([name, planet]) => (
setMessage(name)}
onMouseLeave={() => setMessage('')}
/>
))}
>
);
};
export default App;
````
```js
```
---
# Oscillating Blobs
> Animate colorful oscillating blobs with dynamic tension on an HTML5 canvas using Konva line shapes.
Source: https://konvajs.org/docs/sandbox/Quantum_Squiggle.html
**Instructions: Refresh the page to generate new blobs. You can also drag and drop the blobs as they animate.**
```js
import Konva from 'konva';
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
var layer = new Konva.Layer();
var colors = ['red', 'orange', 'yellow', 'green', 'blue', 'purple'];
var blobs = [];
// create 6 blobs
for (var n = 0; n < 6; n++) {
// build array of random points
var points = [];
for (var i = 0; i < 5; i++) {
points.push(stage.width() * Math.random());
points.push(height * Math.random());
}
var blob = new Konva.Line({
points: points,
fill: colors[n],
stroke: 'black',
strokeWidth: 2,
tension: 0,
opacity: Math.random(),
draggable: true,
closed: true,
});
layer.add(blob);
blobs.push(blob);
}
stage.add(layer);
var period = 2000;
var centerTension = 0;
var amplitude = 1;
var anim = new Konva.Animation(function (frame) {
for (var n = 0; n < blobs.length; n++) {
blobs[n].tension(
amplitude * Math.sin((frame.time * 2 * Math.PI) / period) +
centerTension
);
}
}, layer);
anim.start();
```
```js
import { Stage, Layer, Line } from 'react-konva';
import { useState, useEffect } from 'react';
const COLORS = ['red', 'orange', 'yellow', 'green', 'blue', 'purple'];
const App = () => {
const [blobs, setBlobs] = useState([]);
const [tension, setTension] = useState(0);
useEffect(() => {
// Generate initial blobs
const newBlobs = COLORS.map((color) => {
const points = [];
for (let i = 0; i < 5; i++) {
points.push(window.innerWidth * Math.random());
points.push(window.innerHeight * Math.random());
}
return {
points,
fill: color,
opacity: Math.random(),
x: 0,
y: 0
};
});
setBlobs(newBlobs);
}, []);
useEffect(() => {
const period = 2000;
const centerTension = 0;
const amplitude = 1;
const interval = setInterval(() => {
const time = new Date().getTime();
setTension(
amplitude * Math.sin((time * 2 * Math.PI) / period) + centerTension
);
}, 1000 / 60);
return () => clearInterval(interval);
}, []);
const handleDragEnd = (e, index) => {
const newBlobs = [...blobs];
newBlobs[index] = {
...newBlobs[index],
x: e.target.x(),
y: e.target.y()
};
setBlobs(newBlobs);
};
return (
{blobs.map((blob, i) => (
handleDragEnd(e, i)}
/>
))}
);
};
export default App;
```
```js
```
---
# How to find relative mouse position?
> Find the relative mouse pointer position inside transformed and nested Konva nodes using inverted absolute transforms.
Source: https://konvajs.org/docs/sandbox/Relative_Pointer_Position.html
In some cases you may need to find position of a point relative to a node. For purpose we can use mathematical `Konva.Transform` methods.
In this demo we have deep nesting transformed nodes: moved stage, scaled layer, rotated group.
Now we want to add circles into the group on click. But how to find position of that circles?
We can't use `stage.getPointerPosition()` directly because that is position relative to top-left corner of the stage.
The idea is simple. We just need to use inverted absolute transform.
```js
import Konva from 'konva';
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
x: 20,
y: 50,
});
var layer = new Konva.Layer({
scaleX: 1.2,
scaleY: 0.8,
rotation: 5,
});
stage.add(layer);
var group = new Konva.Group({
x: 30,
rotation: 10,
scaleX: 1.5,
});
layer.add(group);
var text = new Konva.Text({
text: 'Click on the canvas to draw a circle',
fontSize: 20,
});
group.add(text);
stage.on('click', function () {
var pos = group.getRelativePointerPosition();
var shape = new Konva.Circle({
x: pos.x,
y: pos.y,
fill: 'red',
radius: 20,
});
group.add(shape);
});
```
```js
import { useState } from 'react';
import { Stage, Layer, Group, Text, Circle } from 'react-konva';
const App = () => {
const [circles, setCircles] = useState([]);
const handleStageClick = (e) => {
// Get the group reference from konva tree
const group = e.target.getStage().findOne('Group');
if (!group) return;
// Get position relative to the group
const pos = group.getRelativePointerPosition();
// Add new circle
setCircles([
...circles,
{
x: pos.x,
y: pos.y,
radius: 20,
fill: 'red',
id: Date.now().toString()
}
]);
};
return (
{circles.map((circle) => (
))}
);
};
export default App;
```
```js
```
---
# Resizing Stress Test with Konva
> Stress test demo for selecting and resizing thousands of shapes at once using layers and caching for performance.
Source: https://konvajs.org/docs/sandbox/Resizing_Stress_Test.html
This is a stress test demo to select and resize many shapes at the same time.
The demo is using two core `Konva` features to boost the performance:
### 1. Layers
Resizing shapes are moved into another layer (another canvas element). So while you resize selected shapes, we don't need to redraw other shapes.
### 2. Caching
On `select`, I am moving all selected shapes into a group and cache that group. The cache action will convert group into bitmap image. It is mush faster to redraw such group on the screen.
**Instructions: try to select several shapes and resize/rotate them.**
```js
import Konva from 'konva';
// first we need to create a stage
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
// layer for all shapes
var layer = new Konva.Layer();
stage.add(layer);
for (var i = 0; i < 10000; i++) {
var shape = new Konva.Circle({
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
radius: 10,
name: 'shape',
fill: Konva.Util.getRandomColor(),
});
layer.add(shape);
}
// top layer for transforming group
var topLayer = new Konva.Layer();
stage.add(topLayer);
var group = new Konva.Group({
draggable: true,
});
topLayer.add(group);
var tr = new Konva.Transformer();
topLayer.add(tr);
// add a new feature, lets add ability to draw selection rectangle
var selectionRectangle = new Konva.Rect({
fill: 'rgba(0,0,255,0.5)',
visible: false,
});
topLayer.add(selectionRectangle);
var x1, y1, x2, y2;
stage.on('mousedown touchstart', (e) => {
// do nothing if we mousedown on the transformer
if (e.target.getParent() === tr) {
return;
}
// do nothing if we mousedown on the group
if (e.target.parent === group) {
return;
}
x1 = stage.getPointerPosition().x;
y1 = stage.getPointerPosition().y;
x2 = stage.getPointerPosition().x;
y2 = stage.getPointerPosition().y;
selectionRectangle.setAttrs({
x: x1,
y: y1,
width: 0,
height: 0,
visible: true,
});
// move old selection back to original layer
group.children.slice().forEach((shape) => {
const transform = shape.getAbsoluteTransform();
shape.moveTo(layer);
shape.setAttrs(transform.decompose());
});
// reset group transforms
group.setAttrs({
x: 0,
y: 0,
scaleX: 1,
scaleY: 1,
rotation: 0,
});
group.clearCache();
});
stage.on('mousemove touchmove', () => {
// do nothing if we didn't start selection
if (!selectionRectangle.visible()) {
return;
}
x2 = stage.getPointerPosition().x;
y2 = stage.getPointerPosition().y;
selectionRectangle.setAttrs({
x: Math.min(x1, x2),
y: Math.min(y1, y2),
width: Math.abs(x2 - x1),
height: Math.abs(y2 - y1),
});
});
stage.on('mouseup touchend', () => {
// no nothing if we didn't start selection
if (!selectionRectangle.visible()) {
return;
}
// update visibility in timeout, so we can check it in click event
setTimeout(() => {
selectionRectangle.visible(false);
});
var shapes = stage.find('.shape');
var box = selectionRectangle.getClientRect();
// remove all children for better performance
layer.removeChildren();
// then check intersections and add all shape into correct container
shapes.forEach((shape) => {
var intersected = Konva.Util.haveIntersection(
box,
shape.getClientRect()
);
if (intersected) {
group.add(shape);
shape.stroke('blue');
} else {
layer.add(shape);
shape.stroke(null);
}
});
if (group.children.length) {
tr.nodes([group]);
group.cache();
} else {
tr.nodes([]);
group.clearCache();
}
});
// clicks should select/deselect shapes
stage.on('click tap', function (e) {
// if we are selecting with rect, do nothing
if (selectionRectangle.visible()) {
return;
}
// if click on empty area - remove all selections
if (e.target === stage) {
tr.nodes([]);
return;
}
});
```
```js
import { useState, useRef, useEffect } from 'react';
import { Stage, Layer, Circle, Group, Transformer, Rect } from 'react-konva';
import Konva from 'konva';
const App = () => {
const [shapes, setShapes] = useState([]);
const [selectedIds, setSelectedIds] = useState([]);
const [selectionRect, setSelectionRect] = useState({
visible: false,
x1: 0,
y1: 0,
x2: 0,
y2: 0,
});
const [groupKey, setGroupKey] = useState(0);
const groupRef = useRef();
const trRef = useRef();
const selectionRectRef = useRef();
// Generate 10k shapes once
useEffect(() => {
const items = [];
for (let i = 0; i < 10000; i++) {
items.push({
id: i,
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
radius: 10,
fill: Konva.Util.getRandomColor(),
});
}
setShapes(items);
}, []);
// Attach/Detach transformer & cache
useEffect(() => {
if (selectedIds.length && groupRef.current) {
trRef.current.nodes([groupRef.current]);
groupRef.current.cache();
} else {
trRef.current.nodes([]);
if (groupRef.current) groupRef.current.clearCache();
}
}, [selectedIds]);
// Utility to apply group transform to shapes and commit into React state
const applyGroupTransform = () => {
if (!selectedIds.length || !groupRef.current) return;
const transform = groupRef.current.getAbsoluteTransform();
const { scaleX } = transform.decompose();
setShapes((prev) =>
prev.map((shape) => {
if (!selectedIds.includes(shape.id)) return shape;
const pos = transform.point({ x: shape.x, y: shape.y });
return {
...shape,
x: pos.x,
y: pos.y,
radius: shape.radius * scaleX,
};
})
);
};
const pointerPos = (e) => e.target.getStage().getPointerPosition();
const handleMouseDown = (e) => {
// ignore click on transformer or group
if (e.target.getParent() === trRef.current || e.target.parent === groupRef.current) {
return;
}
// finalise previous selection (if any)
applyGroupTransform();
if (selectedIds.length) {
setSelectedIds([]);
setGroupKey((k) => k + 1); // reset group for fresh transform
}
const p = pointerPos(e);
setSelectionRect({ visible: true, x1: p.x, y1: p.y, x2: p.x, y2: p.y });
};
const handleMouseMove = (e) => {
if (!selectionRect.visible) return;
const p = pointerPos(e);
setSelectionRect((prev) => ({ ...prev, x2: p.x, y2: p.y }));
};
const handleMouseUp = (e) => {
if (!selectionRect.visible) return;
setTimeout(() => setSelectionRect((prev) => ({ ...prev, visible: false })), 0);
const stage = e.target.getStage();
const nodes = stage.find('.shape');
const box = {
x: Math.min(selectionRect.x1, selectionRect.x2),
y: Math.min(selectionRect.y1, selectionRect.y2),
width: Math.abs(selectionRect.x2 - selectionRect.x1),
height: Math.abs(selectionRect.y2 - selectionRect.y1),
};
const ids = [];
nodes.forEach((node) => {
if (Konva.Util.haveIntersection(box, node.getClientRect())) {
ids.push(Number(node.id()));
}
});
setSelectedIds(ids);
};
const handleStageClick = (e) => {
// ignore clicks that are part of selection rectangle drawing
if (selectionRect.visible) return;
if (e.target === e.target.getStage()) {
// clicked on empty area: apply transform and clear selection
applyGroupTransform();
if (selectedIds.length) {
setSelectedIds([]);
setGroupKey((k) => k + 1);
}
}
};
const selectionRectProps = {
fill: 'rgba(0,0,255,0.5)',
visible: selectionRect.visible,
x: Math.min(selectionRect.x1, selectionRect.x2),
y: Math.min(selectionRect.y1, selectionRect.y2),
width: Math.abs(selectionRect.x2 - selectionRect.x1),
height: Math.abs(selectionRect.y2 - selectionRect.y1),
ref: selectionRectRef,
};
return (
{shapes
.filter((s) => !selectedIds.includes(s.id))
.map((shape) => (
))}
{shapes
.filter((s) => selectedIds.includes(s.id))
.map((shape) => (
))}
);
};
export default App;
```
---
# Responsive Canvas Stage Demo
> Learn how to make a responsive Konva canvas stage that scales to fit any browser window size.
Source: https://konvajs.org/docs/sandbox/Responsive_Canvas.html
## Do you need responsive/adaptive canvas for your desktop and mobile applications?
There are many ways to make your canvas stage "responsive", and you may need different behavior for different applications.
This demo shows the simplest solution: fitting a canvas stage into the user's window with scaling. In this example, we'll focus on adjusting the stage WIDTH. You can add extra logic if you need to fit height too.
**Instructions:** Try resizing your browser window and see how the canvas adapts.
```js
import Konva from 'konva';
// Define virtual size for our scene
// The real size will be different to fit user's page
const sceneWidth = 1000;
const sceneHeight = 1000;
// Create stage with initial size
const stage = new Konva.Stage({
container: 'container',
width: sceneWidth,
height: sceneHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// Add circle in the center
const circle = new Konva.Circle({
radius: 50,
fill: 'red',
x: stage.width() / 2,
y: stage.height() / 2,
});
layer.add(circle);
// Add rectangle in bottom right of the stage
const rect = new Konva.Rect({
fill: 'green',
x: stage.width() - 100,
y: stage.height() - 100,
width: 100,
height: 100,
});
layer.add(rect);
// Add some text
const text = new Konva.Text({
x: 20,
y: 20,
text: 'Try resizing your browser window',
fontSize: 20,
fontFamily: 'Arial',
fill: 'black',
});
layer.add(text);
// Function to make the stage responsive
function fitStageIntoParentContainer() {
// Get the container element
const container = document.getElementById('container');
// Make the container take up the full width
container.style.width = '100%';
// Get current container width
const containerWidth = container.offsetWidth;
// Calculate scale based on virtual width vs actual width
const scale = containerWidth / sceneWidth;
// Set stage dimensions and scale
stage.width(sceneWidth * scale);
stage.height(sceneHeight * scale);
stage.scale({ x: scale, y: scale });
}
// Initial fit
fitStageIntoParentContainer();
// Adapt the stage on window resize
window.addEventListener('resize', fitStageIntoParentContainer);
```
```js
import { useState, useEffect, useRef } from 'react';
import { Stage, Layer, Circle, Rect, Text } from 'react-konva';
const App = () => {
// Define virtual size for our scene
const sceneWidth = 1000;
const sceneHeight = 1000;
// State to track current scale and dimensions
const [stageSize, setStageSize] = useState({
width: sceneWidth,
height: sceneHeight,
scale: 1
});
// Reference to parent container
const containerRef = useRef(null);
// Function to handle resize
const updateSize = () => {
if (!containerRef.current) return;
// Get container width
const containerWidth = containerRef.current.offsetWidth;
// Calculate scale
const scale = containerWidth / sceneWidth;
// Update state with new dimensions
setStageSize({
width: sceneWidth * scale,
height: sceneHeight * scale,
scale: scale
});
};
// Update on mount and when window resizes
useEffect(() => {
updateSize();
window.addEventListener('resize', updateSize);
return () => {
window.removeEventListener('resize', updateSize);
};
}, []);
return (
);
};
export default App;
```
```js
```
---
# How to show rich html on canvas with Konva
> Display rich HTML text with bold, italic, and color styles on canvas using Konva and render-tag.
Source: https://konvajs.org/docs/sandbox/Rich_Text.html
## How to show complex styles (like bold) and enable rich text editing features?
Canvas's text API is very limited. [Konva.Text](/docs/shapes/Text.html) allows you to add many different styles, support multiline text, etc. But at the current moment it has limitations. You can't use different styles for different parts of `Konva.Text`. For that case you have to use several `Konva.Text` instances.
If you want to show complex styles on canvas, you can use [render-tag](https://polotno.com/render-tag/?utm_source=konvajs&utm_medium=docs&utm_content=rich-text) — a library that renders HTML + CSS directly onto canvas using the 2D API. No SVG, no `foreignObject`, fully synchronous.
The idea is:
1. Create a custom `Konva.Shape` with an `html` property
2. Use `render-tag` to compute layout and draw styled text onto the canvas context
3. The shape auto-sizes its height based on the HTML content
Instructions: Try to type and format text in the editor. The formatted text will be rendered on the canvas below it. You can drag the rendered text around.
```js
import Konva from 'konva';
import { Factory } from 'konva/lib/Factory';
// render-tag: renders HTML+CSS onto canvas via pure 2D API
// loaded from CDN to avoid Sandpack transpilation issues
var computeLayout, drawLayout;
// Create a custom Konva shape that renders HTML via render-tag
// Use Reflect.construct to extend ES6 class from transpiled code
function RichText(config) {
var instance = Reflect.construct(Konva.Shape, [config], RichText);
instance._layoutResult = null;
instance.on('htmlChange widthChange', function () {
this._recomputeLayout();
});
instance._recomputeLayout();
return instance;
}
RichText.prototype = Object.create(Konva.Shape.prototype);
RichText.prototype.constructor = RichText;
RichText.prototype.className = 'RichText';
RichText.prototype._recomputeLayout = function () {
var html = this.html();
var width = this.width() || 200;
if (!html) {
this._layoutResult = null;
return;
}
this._layoutResult = computeLayout({ html: html, width: width });
};
RichText.prototype._sceneFunc = function (context) {
if (!this._layoutResult) return;
var width = this.width() || 200;
drawLayout({
layout: this._layoutResult,
width: width,
ctx: context._context,
pixelRatio: 1,
});
};
RichText.prototype._hitFunc = function (context) {
var width = this.width() || 200;
var height = this.height() || (this._layoutResult ? this._layoutResult.height : 0);
context.beginPath();
context.rect(0, 0, width, height);
context.closePath();
context.fillStrokeShape(this);
};
Factory.addGetterSetter(RichText, 'html', '');
Factory.addGetterSetter(RichText, 'width', 200);
Factory.addGetterSetter(RichText, 'height', 0);
// --- Toolbar + contenteditable editor ---
function execCmd(cmd, val) {
document.execCommand(cmd, false, val || null);
editor.focus();
}
var toolbar = document.createElement('div');
toolbar.innerHTML = [
'B ',
'I ',
'U ',
'H1 ',
'H2 ',
'A ',
].join('');
toolbar.style.cssText = 'display:flex;gap:4px;margin-bottom:4px;';
toolbar.querySelectorAll('button').forEach(function (btn) {
btn.style.cssText = 'padding:2px 8px;cursor:pointer;';
btn.addEventListener('mousedown', function (e) {
e.preventDefault();
execCmd(btn.dataset.cmd, btn.dataset.val);
});
});
document.body.prepend(toolbar);
var editor = document.createElement('div');
editor.contentEditable = true;
editor.style.cssText = 'border:1px solid #ccc;padding:8px;min-height:60px;margin-bottom:8px;';
editor.innerHTML =
'That is some styled text on canvas !' +
'What do you think about it? ';
var container = document.getElementById('container');
document.body.insertBefore(editor, container);
// --- Load render-tag and set up canvas ---
var loadScript = function (src) {
return new Promise(function (resolve, reject) {
var s = document.createElement('script');
s.src = src;
s.onload = resolve;
s.onerror = reject;
document.head.appendChild(s);
});
};
loadScript('https://cdn.jsdelivr.net/npm/render-tag/lib/render-tag.umd.js').then(function () {
computeLayout = RenderTag.layout;
drawLayout = RenderTag.drawLayout;
var stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: 200,
});
var layer = new Konva.Layer();
stage.add(layer);
var shape = new RichText({
x: 10,
y: 10,
width: 400,
draggable: true,
html: editor.innerHTML,
});
layer.add(shape);
editor.addEventListener('input', function () {
shape.html(editor.innerHTML);
});
});
```
---
# How to Rotate and Flip Images Online with JavaScript Canvas
> Rotate and flip images online with JavaScript and HTML5 Canvas using Konva.js. Upload any image, rotate 90 degrees, flip horizontally or vertically, and export the result as PNG.
Source: https://konvajs.org/docs/sandbox/Rotate_Flip_Image.html
A simple image rotation and flip tool is one of the most common canvas utilities. With Konva you can load any image, rotate it by 90-degree steps, flip it horizontally or vertically, and export the result — all in the browser with no server needed.
**Instructions:** Click "Load Image" to upload a photo (or use the default). Use the buttons to rotate 90° clockwise/counter-clockwise, flip horizontally or vertically. Click "Save as PNG" to download the result.
```js
import Konva from 'konva';
// --- Controls ---
const controls = document.createElement('div');
controls.style.cssText = 'display:flex;gap:6px;align-items:center;margin-bottom:4px;flex-wrap:wrap;';
const loadBtn = document.createElement('button');
loadBtn.textContent = 'Load Image';
const rotateCW = document.createElement('button');
rotateCW.textContent = 'Rotate 90° →';
const rotateCCW = document.createElement('button');
rotateCCW.textContent = '← Rotate 90°';
const flipH = document.createElement('button');
flipH.textContent = 'Flip Horizontal';
const flipV = document.createElement('button');
flipV.textContent = 'Flip Vertical';
const saveBtn = document.createElement('button');
saveBtn.textContent = 'Save as PNG';
const resetBtn = document.createElement('button');
resetBtn.textContent = 'Reset';
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
[loadBtn, rotateCCW, rotateCW, flipH, flipV, saveBtn, resetBtn].forEach(b => controls.appendChild(b));
controls.appendChild(fileInput);
const container = document.getElementById('container');
container.parentNode.insertBefore(controls, container);
// --- Stage ---
const stageWidth = window.innerWidth;
const stageHeight = window.innerHeight - 40;
const stage = new Konva.Stage({
container: 'container',
width: stageWidth,
height: stageHeight,
});
const bgLayer = new Konva.Layer();
stage.add(bgLayer);
// checkerboard background to show transparency
const gridSize = 20;
for (let x = 0; x < stageWidth; x += gridSize) {
for (let y = 0; y < stageHeight; y += gridSize) {
const isEven = ((x / gridSize) + (y / gridSize)) % 2 === 0;
if (!isEven) {
bgLayer.add(new Konva.Rect({
x, y, width: gridSize, height: gridSize,
fill: '#f0f0f0', listening: false,
}));
}
}
}
const layer = new Konva.Layer();
stage.add(layer);
let konvaImage = null;
let currentRotation = 0;
let currentScaleX = 1;
let currentScaleY = 1;
function loadImage(src) {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function () {
if (konvaImage) konvaImage.destroy();
// fit image to stage
const ratio = Math.min(
(stageWidth - 40) / img.width,
(stageHeight - 40) / img.height,
1
);
const w = img.width * ratio;
const h = img.height * ratio;
currentRotation = 0;
currentScaleX = 1;
currentScaleY = 1;
konvaImage = new Konva.Image({
image: img,
x: stageWidth / 2,
y: stageHeight / 2,
width: w,
height: h,
offsetX: w / 2,
offsetY: h / 2,
rotation: 0,
});
layer.add(konvaImage);
};
img.src = src;
}
// Load default image
loadImage('https://konvajs.org/assets/darth-vader.jpg');
loadBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => loadImage(ev.target.result);
reader.readAsDataURL(file);
});
rotateCW.addEventListener('click', () => {
if (!konvaImage) return;
currentRotation += 90;
konvaImage.rotation(currentRotation);
});
rotateCCW.addEventListener('click', () => {
if (!konvaImage) return;
currentRotation -= 90;
konvaImage.rotation(currentRotation);
});
flipH.addEventListener('click', () => {
if (!konvaImage) return;
currentScaleX *= -1;
konvaImage.scaleX(currentScaleX);
});
flipV.addEventListener('click', () => {
if (!konvaImage) return;
currentScaleY *= -1;
konvaImage.scaleY(currentScaleY);
});
resetBtn.addEventListener('click', () => {
if (!konvaImage) return;
currentRotation = 0;
currentScaleX = 1;
currentScaleY = 1;
konvaImage.rotation(0);
konvaImage.scaleX(1);
konvaImage.scaleY(1);
});
saveBtn.addEventListener('click', () => {
if (!konvaImage) return;
// hide checkerboard and export only the image area
bgLayer.hide();
const rect = konvaImage.getClientRect();
const dataURL = stage.toDataURL({
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
pixelRatio: 2,
});
const link = document.createElement('a');
link.download = 'rotated-image.png';
link.href = dataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
bgLayer.show();
});
```
```js
import React from 'react';
import { Stage, Layer, Image as KonvaImage, Rect } from 'react-konva';
const App = () => {
const [image, setImage] = React.useState(null);
const [rotation, setRotation] = React.useState(0);
const [scaleX, setScaleX] = React.useState(1);
const [scaleY, setScaleY] = React.useState(1);
const [imgSize, setImgSize] = React.useState({ w: 200, h: 137 });
const stageRef = React.useRef(null);
const bgLayerRef = React.useRef(null);
const fileRef = React.useRef(null);
const W = window.innerWidth;
const H = window.innerHeight - 50;
React.useEffect(() => {
const img = new window.Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const ratio = Math.min((W - 40) / img.width, (H - 40) / img.height, 1);
setImgSize({ w: img.width * ratio, h: img.height * ratio });
setImage(img);
};
img.src = 'https://konvajs.org/assets/darth-vader.jpg';
}, []);
const loadFile = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
const img = new window.Image();
img.onload = () => {
const ratio = Math.min((W - 40) / img.width, (H - 40) / img.height, 1);
setImgSize({ w: img.width * ratio, h: img.height * ratio });
setImage(img);
setRotation(0);
setScaleX(1);
setScaleY(1);
};
img.src = ev.target.result;
};
reader.readAsDataURL(file);
};
const imgRef = React.useRef(null);
const handleSave = () => {
// hide checkerboard and export only the image area
bgLayerRef.current.hide();
const rect = imgRef.current.getClientRect();
const dataURL = stageRef.current.toDataURL({
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
pixelRatio: 2,
});
const link = document.createElement('a');
link.download = 'rotated-image.png';
link.href = dataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
bgLayerRef.current.show();
};
// checkerboard squares
const gridSize = 20;
const checkers = [];
for (let x = 0; x < W; x += gridSize) {
for (let y = 0; y < H; y += gridSize) {
if (((x / gridSize) + (y / gridSize)) % 2 !== 0) {
checkers.push({ x, y, key: `${x}-${y}` });
}
}
}
return (
<>
fileRef.current.click()}>Load Image
setRotation(r => r - 90)}>← Rotate 90°
setRotation(r => r + 90)}>Rotate 90° →
setScaleX(s => s * -1)}>Flip Horizontal
setScaleY(s => s * -1)}>Flip Vertical
Save as PNG
{ setRotation(0); setScaleX(1); setScaleY(1); }}>Reset
{checkers.map(c => (
))}
{image && (
)}
>
);
};
export default App;
```
```js
```
---
# How to draw SVG image on canvas with Konva
> Render SVG images on HTML5 canvas using Konva.Image, Konva.Path, or the canvg library.
Source: https://konvajs.org/docs/sandbox/SVG_On_Canvas.html
## How to show SVG image on canvas?
It has not always been possible for browsers to draw `*.svg` images onto the canvas. However, the situation has improved and you currently have several options available if you want to render a vector image with `Konva`:
### Option 1: Use Konva.Image
In most of the cases you can use `*.svg` image the same way as any other image such as `*.png` or `*.jpg`. You can use [Konva.Image](/docs/shapes/Image.html) shape.
```js
Konva.Image.fromURL('/image.svg', (image) => {
layer.add(image);
});
```
This method works well in many cases, but is not fully cross-compatible. For example, some SVG may not be visible in the Firefox browser ([there is a workaround for that case](https://github.com/konvajs/konva/issues/677#issuecomment-504596837)).
### Option 2: Use Konva.Path
Use [Konva.Path](/docs/shapes/Path.html). This method is good for simple path shapes. If you have a large SVG with many paths you, you may need to split it manually into several `Konva.Path` shapes.
### Option 3: Use an external library to render SVG to canvas
Use an external library (for example, [canvg](https://github.com/canvg/canvg)) to draw the SVG into the `` element. And then use that canvas for [Konva.Image](/docs/shapes/Image.html).
This method has been tested in at least one large production app, with proven reliability and rendering accuracy.
### Demo
Below is a demo that shows drawing natively and with a library.
```js
import Konva from 'konva';
import { Canvg } from 'canvg';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const SOURCE = 'https://konvajs.org/assets/tiger.svg';
// try to draw SVG natively
Konva.Image.fromURL(SOURCE, (imageNode) => {
layer.add(imageNode);
imageNode.setAttrs({
width: 150,
height: 150,
});
});
// draw svg with external library
async function renderWithCanvg() {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const renderer = await Canvg.from(context, SOURCE);
await renderer.render();
const image = new Konva.Image({
image: canvas,
x: 200,
width: 150,
height: 150,
});
layer.add(image);
}
renderWithCanvg().catch((error) => {
console.error('Failed to render SVG:', error);
});
```
```js
import React from 'react';
import { Stage, Layer, Image } from 'react-konva';
import { useImage } from 'react-konva-utils';
import { Canvg } from 'canvg';
const SOURCE = 'https://konvajs.org/assets/tiger.svg';
const App = () => {
const [nativeImage] = useImage(SOURCE);
const [canvgImage, setCanvgImage] = React.useState(null);
React.useEffect(() => {
let active = true;
const renderWithCanvg = async () => {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const renderer = await Canvg.from(context, SOURCE);
await renderer.render();
if (active) setCanvgImage(canvas);
};
renderWithCanvg().catch((error) => {
console.error('Failed to render SVG:', error);
});
return () => {
active = false;
};
}, []);
return (
{nativeImage && (
)}
{canvgImage && (
)}
);
};
export default App;
```
```js
```
Instructions: The demo shows two SVG images rendered in different ways:
1. Left image: Native SVG rendering using Konva.Image
2. Right image: SVG rendered using canvg library
---
# Scaling image to fit a fixed area on canvas
> Scale and crop images to fit a fixed area without stretching, emulating CSS object-fit cover with Konva.
Source: https://konvajs.org/docs/sandbox/Scale_Image_To_Fit.html
## How to scale image to fit available area without its stretching?
The demo demonstrates how to use [crop](/api/Konva.Image.html#crop) property of `Konva.Image` to emulate `object-fit: cover` of CSS.
The [crop](https://konvajs.org/api/Konva.Image.html#crop) property allows you to use only specified area of source image to draw into the canvas. If you do the correct calculations, then the resulting image can be drawn without any stretching.
**Instructions: Try to resize the image or change the crop strategy using the dropdown menu at the top. The image will maintain its aspect ratio while fitting into the specified dimensions.**
```js
import Konva from 'konva';
// Create select element for crop position
const select = document.createElement('select');
select.style.position = 'absolute';
select.style.top = '4px';
select.style.left = '4px';
const positions = [
'left-top', 'center-top', 'right-top', '--',
'left-middle', 'center-middle', 'right-middle', '--',
'left-bottom', 'center-bottom', 'right-bottom'
];
positions.forEach(pos => {
const option = document.createElement('option');
option.value = pos;
option.text = pos;
if (pos === 'center-middle') option.selected = true;
select.appendChild(option);
});
document.body.appendChild(select);
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// function to calculate crop values from source image, its visible size and a crop strategy
function getCrop(image, size, clipPosition = 'center-middle') {
const width = size.width;
const height = size.height;
const aspectRatio = width / height;
let newWidth;
let newHeight;
const imageRatio = image.width / image.height;
if (aspectRatio >= imageRatio) {
newWidth = image.width;
newHeight = image.width / aspectRatio;
} else {
newWidth = image.height * aspectRatio;
newHeight = image.height;
}
let x = 0;
let y = 0;
if (clipPosition === 'left-top') {
x = 0;
y = 0;
} else if (clipPosition === 'left-middle') {
x = 0;
y = (image.height - newHeight) / 2;
} else if (clipPosition === 'left-bottom') {
x = 0;
y = image.height - newHeight;
} else if (clipPosition === 'center-top') {
x = (image.width - newWidth) / 2;
y = 0;
} else if (clipPosition === 'center-middle') {
x = (image.width - newWidth) / 2;
y = (image.height - newHeight) / 2;
} else if (clipPosition === 'center-bottom') {
x = (image.width - newWidth) / 2;
y = image.height - newHeight;
} else if (clipPosition === 'right-top') {
x = image.width - newWidth;
y = 0;
} else if (clipPosition === 'right-middle') {
x = image.width - newWidth;
y = (image.height - newHeight) / 2;
} else if (clipPosition === 'right-bottom') {
x = image.width - newWidth;
y = image.height - newHeight;
}
return {
cropX: x,
cropY: y,
cropWidth: newWidth,
cropHeight: newHeight,
};
}
// function to apply crop
function applyCrop(img, pos) {
img.setAttr('lastCropUsed', pos);
const crop = getCrop(
img.image(),
{ width: img.width(), height: img.height() },
pos
);
img.setAttrs(crop);
}
Konva.Image.fromURL('https://konvajs.org/assets/darth-vader.jpg', (img) => {
img.setAttrs({
width: 300,
height: 100,
x: 80,
y: 100,
name: 'image',
draggable: true,
});
layer.add(img);
// apply default center-middle crop
applyCrop(img, 'center-middle');
const tr = new Konva.Transformer({
nodes: [img],
keepRatio: false,
flipEnabled: false,
boundBoxFunc: (oldBox, newBox) => {
if (Math.abs(newBox.width) < 10 || Math.abs(newBox.height) < 10) {
return oldBox;
}
return newBox;
},
});
layer.add(tr);
img.on('transform', () => {
// reset scale on transform
img.setAttrs({
scaleX: 1,
scaleY: 1,
width: img.width() * img.scaleX(),
height: img.height() * img.scaleY(),
});
applyCrop(img, img.getAttr('lastCropUsed'));
});
});
select.addEventListener('change', (e) => {
const img = layer.findOne('.image');
applyCrop(img, e.target.value);
});
```
```js
import React from 'react';
import { Stage, Layer, Image, Transformer } from 'react-konva';
import { useImage } from 'react-konva-utils';
const positions = [
'left-top', 'center-top', 'right-top', '--',
'left-middle', 'center-middle', 'right-middle', '--',
'left-bottom', 'center-bottom', 'right-bottom'
];
function getCrop(image, size, clipPosition = 'center-middle') {
const width = size.width;
const height = size.height;
const aspectRatio = width / height;
let newWidth;
let newHeight;
const imageRatio = image.width / image.height;
if (aspectRatio >= imageRatio) {
newWidth = image.width;
newHeight = image.width / aspectRatio;
} else {
newWidth = image.height * aspectRatio;
newHeight = image.height;
}
let x = 0;
let y = 0;
if (clipPosition === 'left-top') {
x = 0;
y = 0;
} else if (clipPosition === 'left-middle') {
x = 0;
y = (image.height - newHeight) / 2;
} else if (clipPosition === 'left-bottom') {
x = 0;
y = image.height - newHeight;
} else if (clipPosition === 'center-top') {
x = (image.width - newWidth) / 2;
y = 0;
} else if (clipPosition === 'center-middle') {
x = (image.width - newWidth) / 2;
y = (image.height - newHeight) / 2;
} else if (clipPosition === 'center-bottom') {
x = (image.width - newWidth) / 2;
y = image.height - newHeight;
} else if (clipPosition === 'right-top') {
x = image.width - newWidth;
y = 0;
} else if (clipPosition === 'right-middle') {
x = image.width - newWidth;
y = (image.height - newHeight) / 2;
} else if (clipPosition === 'right-bottom') {
x = image.width - newWidth;
y = image.height - newHeight;
}
return {
cropX: x,
cropY: y,
cropWidth: newWidth,
cropHeight: newHeight,
};
}
const App = () => {
const [position, setPosition] = React.useState('center-middle');
const [imageAttrs, setImageAttrs] = React.useState({
x: 80,
y: 100,
width: 300,
height: 100,
});
const imageRef = React.useRef(null);
const trRef = React.useRef(null);
const [image] = useImage('https://konvajs.org/assets/darth-vader.jpg');
const handleTransform = () => {
const node = imageRef.current;
const scaleX = node.scaleX();
const scaleY = node.scaleY();
node.scaleX(1);
node.scaleY(1);
setImageAttrs({
x: node.x(),
y: node.y(),
width: Math.max(5, node.width() * scaleX),
height: Math.max(5, node.height() * scaleY),
});
};
const crop = React.useMemo(() => {
if (!image) return null;
return getCrop(
image,
{ width: imageAttrs.width, height: imageAttrs.height },
position
);
}, [image, imageAttrs.width, imageAttrs.height, position]);
React.useEffect(() => {
if (image && imageRef.current && trRef.current) {
trRef.current.nodes([imageRef.current]);
}
}, [image]);
return (
<>
{image && (
{
const { x, y } = e.target.position();
setImageAttrs((current) => ({ ...current, x, y }));
}}
onTransform={handleTransform}
/>
)}
{
if (Math.abs(newBox.width) < 10 || Math.abs(newBox.height) < 10) {
return oldBox;
}
return newBox;
}}
/>
setPosition(e.target.value)}
>
{positions.map((pos) => (
{pos}
))}
>
);
};
export default App;
```
```js
{{ pos }}
```
---
# How to automatically scroll stage by edge drag?
> Auto-scroll the Konva stage when dragging shapes near the edge of the viewport for infinite panning.
Source: https://konvajs.org/docs/sandbox/Scroll_By_Edge_Drag.html
## How to automatically scroll stage by edge drag?
If you're looking to enhance your Konva.js application's user experience, implementing an auto-scroll feature is a great way to go. This functionality is especially useful in interactive UIs where users need to drag items or navigate large canvases. By enabling the scroll to automatically move when a user drags an item to the bottom or right edge of the viewport, you create a smoother and more intuitive interaction.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const NUMBER = 100;
function generateNode() {
return new Konva.Circle({
x: stage.width() * (Math.random() * 2 - 1),
y: stage.height() * (Math.random() * 2 - 1),
radius: 40,
fill: 'red',
stroke: 'black',
draggable: true,
});
}
for (let i = 0; i < NUMBER; i++) {
layer.add(generateNode());
}
let scrollInterval = null;
stage.on('dragstart', (e) => {
const duration = 1000 / 60;
scrollInterval = setInterval(() => {
const pos = stage.getPointerPosition();
const offset = 100;
const isNearLeft = pos.x < offset;
if (isNearLeft) {
stage.x(stage.x() + 2);
e.target.x(e.target.x() - 2);
}
const isNearRight = pos.x > stage.width() - offset;
if (isNearRight) {
stage.x(stage.x() - 2);
e.target.x(e.target.x() + 2);
}
const isNearTop = pos.y < offset;
if (isNearTop) {
stage.y(stage.y() + 2);
e.target.y(e.target.y() - 2);
}
const isNearBottom = pos.y > stage.height() - offset;
if (isNearBottom) {
stage.y(stage.y() - 2);
e.target.y(e.target.y() + 2);
}
}, duration);
});
stage.on('dragend', () => {
clearInterval(scrollInterval);
});
```
```jsx
import React from 'react';
import { Stage, Layer, Circle } from 'react-konva';
const NUMBER = 100;
const generateNodes = (width, height) => {
return Array.from({ length: NUMBER }, (_, id) => ({
id,
x: width * (Math.random() * 2 - 1),
y: height * (Math.random() * 2 - 1),
}));
};
const App = () => {
const [stagePos, setStagePos] = React.useState({ x: 0, y: 0 });
const [nodes, setNodes] = React.useState([]);
const scrollInterval = React.useRef(null);
const stageRef = React.useRef(null);
const draggedNodeRef = React.useRef(null);
React.useEffect(() => {
setNodes(generateNodes(window.innerWidth, window.innerHeight));
}, []);
const updateNodePosition = React.useCallback((id, position) => {
setNodes((currentNodes) => currentNodes.map((node) =>
node.id === id ? { ...node, ...position } : node
));
}, []);
const handleDragStart = React.useCallback((e, id) => {
draggedNodeRef.current = { node: e.target, id };
const duration = 1000 / 60;
scrollInterval.current = setInterval(() => {
const stage = stageRef.current;
const dragged = draggedNodeRef.current;
if (!stage || !dragged) return;
const pos = stage.getPointerPosition();
if (!pos) return;
const offset = 100;
let newX = stage.x();
let newY = stage.y();
let moved = false;
if (pos.x < offset) {
newX += 2;
dragged.node.x(dragged.node.x() - 2);
moved = true;
} else if (pos.x > stage.width() - offset) {
newX -= 2;
dragged.node.x(dragged.node.x() + 2);
moved = true;
}
if (pos.y < offset) {
newY += 2;
dragged.node.y(dragged.node.y() - 2);
moved = true;
} else if (pos.y > stage.height() - offset) {
newY -= 2;
dragged.node.y(dragged.node.y() + 2);
moved = true;
}
if (moved) {
stage.position({ x: newX, y: newY });
setStagePos({ x: newX, y: newY });
updateNodePosition(dragged.id, dragged.node.position());
}
}, duration);
}, [updateNodePosition]);
const handleDragMove = React.useCallback((e, id) => {
updateNodePosition(id, e.target.position());
}, [updateNodePosition]);
const handleDragEnd = React.useCallback((e, id) => {
updateNodePosition(id, e.target.position());
draggedNodeRef.current = null;
if (scrollInterval.current) {
clearInterval(scrollInterval.current);
scrollInterval.current = null;
}
}, [updateNodePosition]);
React.useEffect(() => {
return () => {
if (scrollInterval.current) {
clearInterval(scrollInterval.current);
}
};
}, []);
return (
{nodes.map((node) => (
handleDragStart(e, node.id)}
onDragMove={(e) => handleDragMove(e, node.id)}
onDragEnd={(e) => handleDragEnd(e, node.id)}
/>
))}
);
};
export default App;
```
```vue
```
**Instructions:** Start dragging any shape. When you drag it near the edge of the stage, the stage will automatically scroll in that direction. This creates a smooth, infinite scrolling experience.
---
# HTML5 Canvas Shape Tango with Konva
> Animated shapes that dance across the canvas with random positions, rotations, and colors using Konva tweens.
Source: https://konvajs.org/docs/sandbox/Shape_Tango.html
## HTML5 Canvas Shape Tango with Konva
This demo shows how to create animated shapes that dance around the canvas when triggered. It demonstrates:
1. Creating random shapes with different properties
2. Using Konva's tweening system for smooth animations
3. Handling user interactions (drag and drop, button clicks)
4. Managing multiple animations simultaneously
**Instructions:** Drag and drop the shapes to position them, then click the "Tango!" button to make them dance around the canvas. Each shape will move to a random position, rotate, change size, and color. Refresh the page to generate new random shapes.
```js
import Konva from 'konva';
// Create button
const button = document.createElement('button');
button.textContent = 'Tango!';
button.style.position = 'absolute';
button.style.top = '10px';
button.style.left = '10px';
button.style.padding = '10px';
document.body.appendChild(button);
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'purple'];
function getRandomColor() {
return colors[Math.floor(Math.random() * colors.length)];
}
function tango(layer) {
layer.getChildren().forEach((shape) => {
const radius = Math.random() * 100 + 20;
new Konva.Tween({
node: shape,
duration: 1,
x: Math.random() * stage.width(),
y: Math.random() * stage.height(),
rotation: Math.random() * 360,
radius: radius,
opacity: (radius - 20) / 100,
easing: Konva.Easings.EaseInOut,
fill: getRandomColor(),
}).play();
});
}
// Create initial shapes
for (let n = 0; n < 10; n++) {
const radius = Math.random() * 100 + 20;
const shape = new Konva.RegularPolygon({
x: Math.random() * stage.width(),
y: Math.random() * stage.height(),
sides: Math.ceil(Math.random() * 5 + 3),
radius: radius,
fill: getRandomColor(),
opacity: (radius - 20) / 100,
draggable: true,
});
layer.add(shape);
}
button.addEventListener('click', () => tango(layer));
```
```js
import React from 'react';
import Konva from 'konva';
import { Stage, Layer, RegularPolygon } from 'react-konva';
const COLORS = ['red', 'orange', 'yellow', 'green', 'blue', 'purple'];
const NUM_SHAPES = 10;
const getRandomColor = () => COLORS[Math.floor(Math.random() * COLORS.length)];
const getRandomShapeProps = (id, width, height) => {
const radius = Math.random() * 100 + 20;
return {
id,
x: Math.random() * width,
y: Math.random() * height,
sides: Math.ceil(Math.random() * 5 + 3),
radius,
fill: getRandomColor(),
opacity: (radius - 20) / 100,
};
};
const App = () => {
const [shapes, setShapes] = React.useState([]);
const [isAnimating, setIsAnimating] = React.useState(false);
const stageRef = React.useRef();
const isAnimatingRef = React.useRef(false);
const tweensRef = React.useRef([]);
React.useEffect(() => {
const initialShapes = Array.from({ length: NUM_SHAPES }, (_, index) =>
getRandomShapeProps(`shape-${index}`, window.innerWidth, window.innerHeight)
);
setShapes(initialShapes);
return () => {
tweensRef.current.forEach((tween) => tween.destroy());
tweensRef.current = [];
isAnimatingRef.current = false;
};
}, []);
const handleDragEnd = (e, id) => {
const { x, y } = e.target.position();
setShapes(currentShapes => currentShapes.map(shape =>
shape.id === id ? { ...shape, x, y } : shape
));
};
const handleTango = () => {
if (isAnimatingRef.current) return;
const layer = stageRef.current.findOne('Layer');
const shapeNodes = layer.find('RegularPolygon');
if (!shapeNodes.length) return;
isAnimatingRef.current = true;
setIsAnimating(true);
const nextShapes = new Map();
let remaining = shapeNodes.length;
shapeNodes.forEach((node) => {
const id = node.id();
const radius = Math.random() * 100 + 20;
const nextShape = {
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
rotation: Math.random() * 360,
radius: radius,
opacity: (radius - 20) / 100,
fill: getRandomColor(),
};
nextShapes.set(id, nextShape);
const tween = new Konva.Tween({
node,
...nextShape,
duration: 1,
easing: Konva.Easings.EaseInOut,
onFinish: () => {
tween.destroy();
tweensRef.current = tweensRef.current.filter(
(activeTween) => activeTween !== tween
);
remaining -= 1;
if (remaining > 0) return;
setShapes(currentShapes => currentShapes.map(shape => ({
...shape,
...nextShapes.get(shape.id),
})));
isAnimatingRef.current = false;
setIsAnimating(false);
},
});
tweensRef.current.push(tween);
tween.play();
});
};
return (
<>
{shapes.map((shape) => (
handleDragEnd(e, shape.id)}
/>
))}
{isAnimating ? 'Tangoing…' : 'Tango!'}
>
);
};
export default App;
```
```js
handleDragEnd(e, i)"
/>
Tango!
```
---
# Shape Tooltips
> Create tooltips that follow the mouse cursor when hovering over canvas shapes with Konva.
Source: https://konvajs.org/docs/sandbox/Shape_Tooltips.html
## HTML5 Canvas Shape Tooltips with Konva
This demo shows how to create tooltips that follow the mouse cursor when hovering over shapes. It demonstrates:
1. Creating custom shapes using the sceneFunc
2. Handling mouse events (mousemove, mouseout)
3. Using multiple layers for better organization
4. Creating dynamic tooltips that follow the cursor
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const shapesLayer = new Konva.Layer();
const tooltipLayer = new Konva.Layer();
// Create custom triangle shape
const triangle = new Konva.Shape({
stroke: 'black',
fill: '#00D2FF',
strokeWidth: 1,
sceneFunc: function (context) {
context.beginPath();
context.moveTo(120, 50);
context.lineTo(250, 80);
context.lineTo(150, 170);
context.closePath();
context.fillStrokeShape(this);
},
});
// Create circle
const circle = new Konva.Circle({
x: 250,
y: stage.height() / 2,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
radius: 70,
});
// Create tooltip
const tooltip = new Konva.Text({
text: '',
fontFamily: 'Calibri',
fontSize: 12,
padding: 5,
textFill: 'white',
fill: 'black',
alpha: 0.75,
visible: false,
});
// Add event listeners for triangle
triangle.on('mousemove', () => {
const mousePos = stage.getPointerPosition();
tooltip.position({
x: mousePos.x + 5,
y: mousePos.y + 5,
});
tooltip.text('Cyan Triangle');
tooltip.show();
});
triangle.on('mouseout', () => {
tooltip.hide();
});
// Add event listeners for circle
circle.on('mousemove', () => {
const mousePos = stage.getPointerPosition();
tooltip.position({
x: mousePos.x + 5,
y: mousePos.y + 5,
});
tooltip.text('Red Circle');
tooltip.show();
});
circle.on('mouseout', () => {
tooltip.hide();
});
// Add shapes and tooltip to layers
shapesLayer.add(triangle);
shapesLayer.add(circle);
tooltipLayer.add(tooltip);
// Add layers to stage
stage.add(shapesLayer);
stage.add(tooltipLayer);
```
```js
import React from 'react';
import { Stage, Layer, Shape, Circle, Text } from 'react-konva';
const CustomShape = ({ onMouseMove, onMouseOut }) => {
return (
{
context.beginPath();
context.moveTo(120, 50);
context.lineTo(250, 80);
context.lineTo(150, 170);
context.closePath();
context.fillStrokeShape(shape);
}}
onMouseMove={onMouseMove}
onMouseOut={onMouseOut}
/>
);
};
const App = () => {
const [tooltipPos, setTooltipPos] = React.useState({ x: 0, y: 0 });
const [tooltipText, setTooltipText] = React.useState('');
const [isTooltipVisible, setTooltipVisible] = React.useState(false);
const handleMouseMove = (e, text) => {
const stage = e.target.getStage();
const pos = stage.getPointerPosition();
setTooltipPos({
x: pos.x + 5,
y: pos.y + 5,
});
setTooltipText(text);
setTooltipVisible(true);
};
const handleMouseOut = () => {
setTooltipVisible(false);
};
return (
handleMouseMove(e, 'Cyan Triangle')}
onMouseOut={handleMouseOut}
/>
handleMouseMove(e, 'Red Circle')}
onMouseOut={handleMouseOut}
/>
);
};
export default App;
```
```js
handleMouseMove(e, 'Cyan Triangle')"
@mouseout="handleMouseOut"
/>
handleMouseMove(e, 'Red Circle')"
@mouseout="handleMouseOut"
/>
```
Instructions: Move your mouse over the shapes to see tooltips appear. The tooltips will follow your cursor and display information about each shape.
---
# How to Build a Signature Pad with JavaScript Canvas
> Build a smooth digital signature pad with JavaScript and HTML5 Canvas using Konva.js. Capture signatures with variable stroke width, customize pen color, and export as PNG or data URL.
Source: https://konvajs.org/docs/sandbox/Signature_Pad.html
A signature pad is a common UI component for capturing digital signatures in web forms, contracts, and e-signing workflows. Konva makes it easy to build one with smooth line drawing, customizable pen settings, and one-click export to PNG.
**Instructions:** Draw your signature below. Use the controls to change pen color, clear the pad, or save/export the signature as a PNG image.
```js
import Konva from 'konva';
// --- UI Controls ---
const controls = document.createElement('div');
controls.style.cssText = 'display:flex;gap:8px;align-items:center;margin-bottom:4px;flex-wrap:wrap;';
const colorLabel = document.createElement('label');
colorLabel.textContent = 'Pen Color: ';
const colorInput = document.createElement('input');
colorInput.type = 'color';
colorInput.value = '#000000';
colorLabel.appendChild(colorInput);
const widthLabel = document.createElement('label');
widthLabel.textContent = 'Width: ';
const widthSelect = document.createElement('select');
[2, 3, 4, 5, 6].forEach((w) => {
const opt = document.createElement('option');
opt.value = w;
opt.textContent = w + 'px';
if (w === 3) opt.selected = true;
widthSelect.appendChild(opt);
});
widthLabel.appendChild(widthSelect);
const clearBtn = document.createElement('button');
clearBtn.textContent = 'Clear';
const saveBtn = document.createElement('button');
saveBtn.textContent = 'Save as PNG';
controls.appendChild(colorLabel);
controls.appendChild(widthLabel);
controls.appendChild(clearBtn);
controls.appendChild(saveBtn);
const container = document.getElementById('container');
container.parentNode.insertBefore(controls, container);
// --- Stage Setup ---
const width = window.innerWidth;
const height = window.innerHeight - 40;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const bgLayer = new Konva.Layer();
stage.add(bgLayer);
// signature line at bottom
const signLine = new Konva.Line({
points: [40, height - 60, width - 40, height - 60],
stroke: '#ccc',
strokeWidth: 1,
dash: [6, 4],
});
bgLayer.add(signLine);
const signLabel = new Konva.Text({
x: 40,
y: height - 50,
text: 'Sign here',
fontSize: 14,
fill: '#aaa',
fontFamily: 'Arial',
});
bgLayer.add(signLabel);
const drawLayer = new Konva.Layer();
stage.add(drawLayer);
let isPaint = false;
let lastLine;
let lastPointerPosition;
stage.on('mousedown touchstart', function (e) {
isPaint = true;
lastPointerPosition = stage.getPointerPosition();
lastLine = new Konva.Line({
stroke: colorInput.value,
strokeWidth: parseInt(widthSelect.value),
lineCap: 'round',
lineJoin: 'round',
tension: 0.3,
points: [lastPointerPosition.x, lastPointerPosition.y],
});
drawLayer.add(lastLine);
});
stage.on('mouseup touchend', function () {
isPaint = false;
});
stage.on('mousemove touchmove', function (e) {
if (!isPaint) return;
e.evt.preventDefault();
const pos = stage.getPointerPosition();
const newPoints = lastLine.points().concat([pos.x, pos.y]);
lastLine.points(newPoints);
lastPointerPosition = pos;
});
clearBtn.addEventListener('click', function () {
drawLayer.destroyChildren();
});
saveBtn.addEventListener('click', function () {
// temporarily hide background elements for clean export
bgLayer.hide();
const dataURL = stage.toDataURL({ pixelRatio: 2 });
bgLayer.show();
const link = document.createElement('a');
link.download = 'signature.png';
link.href = dataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
```
```js
import React from 'react';
import { Stage, Layer, Line, Text } from 'react-konva';
const App = () => {
const [lines, setLines] = React.useState([]);
const [color, setColor] = React.useState('#000000');
const [strokeWidth, setStrokeWidth] = React.useState(3);
const isDrawing = React.useRef(false);
const stageRef = React.useRef(null);
const bgLayerRef = React.useRef(null);
const stageWidth = window.innerWidth;
const stageHeight = window.innerHeight - 50;
const handleMouseDown = (e) => {
isDrawing.current = true;
const pos = e.target.getStage().getPointerPosition();
setLines([
...lines,
{ color, strokeWidth, points: [pos.x, pos.y] },
]);
};
const handleMouseMove = (e) => {
if (!isDrawing.current) return;
e.evt.preventDefault();
const stage = e.target.getStage();
const point = stage.getPointerPosition();
const lastLine = lines[lines.length - 1];
lastLine.points = lastLine.points.concat([point.x, point.y]);
lines.splice(lines.length - 1, 1, lastLine);
setLines(lines.concat());
};
const handleMouseUp = () => {
isDrawing.current = false;
};
const handleClear = () => setLines([]);
const handleSave = () => {
const stage = stageRef.current;
bgLayerRef.current.hide();
const dataURL = stage.toDataURL({ pixelRatio: 2 });
bgLayerRef.current.show();
const link = document.createElement('a');
link.download = 'signature.png';
link.href = dataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
return (
<>
Pen Color:{' '}
setColor(e.target.value)}
/>
Width:{' '}
setStrokeWidth(Number(e.target.value))}
>
{[2, 3, 4, 5, 6].map((w) => (
{w}px
))}
Clear
Save as PNG
{lines.map((line, i) => (
))}
>
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Simple Window Designer
> Draw a simple window frame on canvas with adjustable width and height dimensions using Konva.
Source: https://konvajs.org/docs/sandbox/Simple_Window_Frame.html
That is a very simple demo that draws a window frame.
**Instructions:** You can change its width and height
```js
import Konva from 'konva';
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
var layer = new Konva.Layer();
stage.add(layer);
var widthInput = document.createElement('input');
widthInput.type = 'number';
widthInput.value = '1000';
var widthLabel = document.createElement('span');
widthLabel.innerText = 'Width: ';
var widthContainer = document.createElement('div');
widthContainer.style.float = 'left';
widthContainer.style.padding = '10px';
widthContainer.appendChild(widthLabel);
widthContainer.appendChild(widthInput);
var heightInput = document.createElement('input');
heightInput.type = 'number';
heightInput.value = '2000';
var heightLabel = document.createElement('span');
heightLabel.innerText = 'Height: ';
var heightContainer = document.createElement('div');
heightContainer.style.float = 'left';
heightContainer.style.padding = '10px';
heightContainer.appendChild(heightLabel);
heightContainer.appendChild(heightInput);
var controls = document.createElement('div');
controls.style.position = 'absolute';
controls.style.top = '4px';
controls.style.left = '4px';
controls.appendChild(widthContainer);
controls.appendChild(heightContainer);
document.body.appendChild(controls);
function createFrame(frameWidth, frameHeight) {
var padding = 70;
var group = new Konva.Group();
var top = new Konva.Line({
points: [
0,
0,
frameWidth,
0,
frameWidth - padding,
padding,
padding,
padding,
],
fill: 'white',
});
var left = new Konva.Line({
points: [
0,
0,
padding,
padding,
padding,
frameHeight - padding,
0,
frameHeight,
],
fill: 'white',
});
var bottom = new Konva.Line({
points: [
0,
frameHeight,
padding,
frameHeight - padding,
frameWidth - padding,
frameHeight - padding,
frameWidth,
frameHeight,
],
fill: 'white',
});
var right = new Konva.Line({
points: [
frameWidth,
0,
frameWidth,
frameHeight,
frameWidth - padding,
frameHeight - padding,
frameWidth - padding,
padding,
],
fill: 'white',
});
var glass = new Konva.Rect({
x: padding,
y: padding,
width: frameWidth - padding * 2,
height: frameHeight - padding * 2,
fill: 'lightblue',
});
group.add(glass, top, left, bottom, right);
group.find('Line').forEach((line) => {
line.closed(true);
line.stroke('black');
line.strokeWidth(1);
});
return group;
}
function createInfo(frameWidth, frameHeight) {
var offset = 20;
var arrowOffset = offset / 2;
var arrowSize = 5;
var group = new Konva.Group();
var lines = new Konva.Shape({
sceneFunc: function (ctx) {
ctx.fillStyle = 'grey';
ctx.lineWidth = 0.5;
ctx.moveTo(0, 0);
ctx.lineTo(-offset, 0);
ctx.moveTo(0, frameHeight);
ctx.lineTo(-offset, frameHeight);
ctx.moveTo(0, frameHeight);
ctx.lineTo(0, frameHeight + offset);
ctx.moveTo(frameWidth, frameHeight);
ctx.lineTo(frameWidth, frameHeight + offset);
ctx.stroke();
},
});
var leftArrow = new Konva.Shape({
sceneFunc: function (ctx) {
// top pointer
ctx.moveTo(-arrowOffset - arrowSize, arrowSize);
ctx.lineTo(-arrowOffset, 0);
ctx.lineTo(-arrowOffset + arrowSize, arrowSize);
// line
ctx.moveTo(-arrowOffset, 0);
ctx.lineTo(-arrowOffset, frameHeight);
// bottom pointer
ctx.moveTo(-arrowOffset - arrowSize, frameHeight - arrowSize);
ctx.lineTo(-arrowOffset, frameHeight);
ctx.lineTo(-arrowOffset + arrowSize, frameHeight - arrowSize);
ctx.strokeShape(this);
},
stroke: 'grey',
strokeWidth: 0.5,
});
var bottomArrow = new Konva.Shape({
sceneFunc: function (ctx) {
// top pointer
ctx.translate(0, frameHeight + arrowOffset);
ctx.moveTo(arrowSize, -arrowSize);
ctx.lineTo(0, 0);
ctx.lineTo(arrowSize, arrowSize);
// line
ctx.moveTo(0, 0);
ctx.lineTo(frameWidth, 0);
// bottom pointer
ctx.moveTo(frameWidth - arrowSize, -arrowSize);
ctx.lineTo(frameWidth, 0);
ctx.lineTo(frameWidth - arrowSize, arrowSize);
ctx.strokeShape(this);
},
stroke: 'grey',
strokeWidth: 0.5,
});
// left text
var leftLabel = new Konva.Label();
leftLabel.add(
new Konva.Tag({
fill: 'white',
stroke: 'grey',
})
);
var leftText = new Konva.Text({
text: heightInput.value + 'mm',
padding: 2,
fill: 'black',
});
leftLabel.add(leftText);
leftLabel.position({
x: -arrowOffset - leftText.width(),
y: frameHeight / 2 - leftText.height() / 2,
});
leftLabel.on('click tap', function () {
createInput('height', this.getAbsolutePosition(), leftText.size());
});
// bottom text
var bottomLabel = new Konva.Label();
bottomLabel.add(
new Konva.Tag({
fill: 'white',
stroke: 'grey',
})
);
var bottomText = new Konva.Text({
text: widthInput.value + 'mm',
padding: 2,
fill: 'black',
});
bottomLabel.add(bottomText);
bottomLabel.position({
x: frameWidth / 2 - bottomText.width() / 2,
y: frameHeight + arrowOffset,
});
bottomLabel.on('click tap', function () {
createInput('width', this.getAbsolutePosition(), bottomText.size());
});
group.add(lines, leftArrow, bottomArrow, leftLabel, bottomLabel);
return group;
}
function createInput(metric, pos, size) {
var wrap = document.createElement('div');
wrap.style.position = 'absolute';
wrap.style.backgroundColor = 'rgba(0,0,0,0.1)';
wrap.style.top = 0;
wrap.style.left = 0;
wrap.style.width = '100%';
wrap.style.height = '100%';
document.body.appendChild(wrap);
var input = document.createElement('input');
input.type = 'number';
var similarInput = metric === 'width' ? widthInput : heightInput;
input.value = similarInput.value;
input.style.position = 'absolute';
input.style.top = pos.y + 3 + 'px';
input.style.left = pos.x + 'px';
input.style.height = size.height + 3 + 'px';
input.style.width = size.width + 3 + 'px';
wrap.appendChild(input);
input.addEventListener('change', function () {
similarInput.value = input.value;
updateCanvas();
});
input.addEventListener('input', function () {
similarInput.value = input.value;
updateCanvas();
});
wrap.addEventListener('click', function (e) {
if (e.target === wrap) {
document.body.removeChild(wrap);
}
});
input.addEventListener('keyup', function (e) {
if (e.key === 'Enter') {
document.body.removeChild(wrap);
}
});
}
function updateCanvas() {
layer.children.forEach((child) => child.destroy());
var frameWidth = parseInt(widthInput.value, 10);
var frameHeight = parseInt(heightInput.value, 10);
var wr = stage.width() / frameWidth;
var hr = stage.height() / frameHeight;
var ratio = Math.min(wr, hr) * 0.8;
var frameOnScreenWidth = frameWidth * ratio;
var frameOnScreenHeight = frameHeight * ratio;
var group = new Konva.Group({});
group.x(Math.round(stage.width() / 2 - frameOnScreenWidth / 2) + 0.5);
group.y(Math.round(stage.height() / 2 - frameOnScreenHeight / 2) + 0.5);
layer.add(group);
var frameGroup = createFrame(frameWidth, frameHeight);
frameGroup.scale({ x: ratio, y: ratio });
group.add(frameGroup);
var infoGroup = createInfo(frameOnScreenWidth, frameOnScreenHeight);
group.add(infoGroup);
}
widthInput.addEventListener('change', updateCanvas);
widthInput.addEventListener('input', updateCanvas);
heightInput.addEventListener('change', updateCanvas);
heightInput.addEventListener('input', updateCanvas);
updateCanvas();
```
```js
import { Stage, Layer, Group, Line, Rect, Shape, Label, Tag, Text } from 'react-konva';
import { useState, useEffect, useRef, useCallback, useMemo, useReducer } from 'react';
// Constants
const MIN_DIMENSION = 100;
const MAX_DIMENSION = 5000;
const DEFAULT_WIDTH = 1000;
const DEFAULT_HEIGHT = 2000;
const PADDING = 70;
// Reducer for dimensions state
const dimensionsReducer = (state, action) => {
switch (action.type) {
case 'SET_WIDTH':
return { ...state, width: Math.min(Math.max(parseInt(action.payload, 10) || MIN_DIMENSION, MIN_DIMENSION), MAX_DIMENSION) };
case 'SET_HEIGHT':
return { ...state, height: Math.min(Math.max(parseInt(action.payload, 10) || MIN_DIMENSION, MIN_DIMENSION), MAX_DIMENSION) };
default:
return state;
}
};
// Custom hook for window size
const useWindowSize = () => {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
};
// Custom hook for overlay input
const useInputOverlay = (dimensions, dispatch) => {
const [overlay, setOverlay] = useState(null);
// Handle creating overlay
const createOverlay = useCallback((metric, position, size) => {
setOverlay({ metric, position, size });
}, []);
// Handle closing overlay
const closeOverlay = useCallback(() => {
setOverlay(null);
}, []);
// Handle overlay effects
useEffect(() => {
if (!overlay) return;
// Create overlay elements
const wrap = document.createElement('div');
wrap.style.position = 'absolute';
wrap.style.backgroundColor = 'rgba(0,0,0,0.1)';
wrap.style.top = 0;
wrap.style.left = 0;
wrap.style.width = '100%';
wrap.style.height = '100%';
wrap.style.zIndex = 999;
wrap.setAttribute('aria-modal', 'true');
wrap.setAttribute('role', 'dialog');
const input = document.createElement('input');
input.type = 'number';
input.min = MIN_DIMENSION;
input.max = MAX_DIMENSION;
input.value = overlay.metric === 'width' ? dimensions.width : dimensions.height;
input.style.position = 'absolute';
input.style.top = `${overlay.position.y + 3}px`;
input.style.left = `${overlay.position.x}px`;
input.style.width = `${overlay.size.width + 3}px`;
input.style.height = `${overlay.size.height + 3}px`;
input.setAttribute('aria-label', `Edit ${overlay.metric}`);
wrap.appendChild(input);
document.body.appendChild(wrap);
// Handle input changes
const handleChange = () => {
const value = input.value;
dispatch({
type: overlay.metric === 'width' ? 'SET_WIDTH' : 'SET_HEIGHT',
payload: value
});
};
// Handle click outside
const handleWrapClick = (e) => {
if (e.target === wrap) {
closeOverlay();
document.body.removeChild(wrap);
}
};
// Handle keyboard events
const handleKeyUp = (e) => {
if (e.key === 'Enter' || e.key === 'Escape') {
closeOverlay();
document.body.removeChild(wrap);
}
};
input.addEventListener('change', handleChange);
input.addEventListener('input', handleChange);
wrap.addEventListener('click', handleWrapClick);
input.addEventListener('keyup', handleKeyUp);
window.addEventListener('keyup', handleKeyUp);
// Focus the input
input.focus();
// Cleanup
return () => {
input.removeEventListener('change', handleChange);
input.removeEventListener('input', handleChange);
wrap.removeEventListener('click', handleWrapClick);
input.removeEventListener('keyup', handleKeyUp);
window.removeEventListener('keyup', handleKeyUp);
if (document.body.contains(wrap)) {
document.body.removeChild(wrap);
}
};
}, [overlay, dimensions, dispatch, closeOverlay]);
return { createOverlay, closeOverlay };
};
// WindowFrame component for the actual frame rendering
const WindowFrame = ({ width, height }) => {
// Generate the points for each side of the frame
const framePoints = useMemo(() => ({
top: [0, 0, width, 0, width - PADDING, PADDING, PADDING, PADDING],
left: [0, 0, PADDING, PADDING, PADDING, height - PADDING, 0, height],
bottom: [0, height, PADDING, height - PADDING, width - PADDING, height - PADDING, width, height],
right: [width, 0, width, height, width - PADDING, height - PADDING, width - PADDING, PADDING]
}), [width, height]);
return (
{/* Glass panel */}
{/* Frame sides */}
{Object.entries(framePoints).map(([key, points]) => (
))}
);
};
// MeasurementInfo component for dimensions and arrows
const MeasurementInfo = ({ width, height, dimensions, createOverlay }) => {
const offset = 20;
const arrowOffset = offset / 2;
const arrowSize = 5;
// Handle label clicks
const handleLabelClick = useCallback((metric, e) => {
const pos = e.target.getAbsolutePosition();
const size = e.target.getSize();
createOverlay(metric, pos, size);
}, [createOverlay]);
return (
{/* Guide lines */}
{
ctx.fillStyle = 'grey';
ctx.lineWidth = 0.5;
ctx.moveTo(0, 0);
ctx.lineTo(-offset, 0);
ctx.moveTo(0, height);
ctx.lineTo(-offset, height);
ctx.moveTo(0, height);
ctx.lineTo(0, height + offset);
ctx.moveTo(width, height);
ctx.lineTo(width, height + offset);
ctx.stroke();
}}
/>
{/* Left arrow (height) */}
{
// top pointer
ctx.moveTo(-arrowOffset - arrowSize, arrowSize);
ctx.lineTo(-arrowOffset, 0);
ctx.lineTo(-arrowOffset + arrowSize, arrowSize);
// line
ctx.moveTo(-arrowOffset, 0);
ctx.lineTo(-arrowOffset, height);
// bottom pointer
ctx.moveTo(-arrowOffset - arrowSize, height - arrowSize);
ctx.lineTo(-arrowOffset, height);
ctx.lineTo(-arrowOffset + arrowSize, height - arrowSize);
ctx.strokeShape(shape);
}}
stroke="grey"
strokeWidth={0.5}
/>
{/* Bottom arrow (width) */}
{
// translate for bottom arrow
ctx.translate(0, height + arrowOffset);
// left pointer
ctx.moveTo(arrowSize, -arrowSize);
ctx.lineTo(0, 0);
ctx.lineTo(arrowSize, arrowSize);
// line
ctx.moveTo(0, 0);
ctx.lineTo(width, 0);
// right pointer
ctx.moveTo(width - arrowSize, -arrowSize);
ctx.lineTo(width, 0);
ctx.lineTo(width - arrowSize, arrowSize);
ctx.strokeShape(shape);
}}
stroke="grey"
strokeWidth={0.5}
/>
{/* Height label */}
handleLabelClick('height', e)}
>
{/* Width label */}
handleLabelClick('width', e)}
>
);
};
// DimensionControls component for input fields
const DimensionControls = ({ dimensions, dispatch }) => {
// Styles
const inputStyle = {
float: 'left',
padding: '10px'
};
const controlsStyle = {
position: 'absolute',
top: '4px',
left: '4px'
};
// Handle input changes
const handleInputChange = useCallback((e, type) => {
dispatch({
type: type === 'width' ? 'SET_WIDTH' : 'SET_HEIGHT',
payload: e.target.value
});
}, [dispatch]);
return (
);
};
// Main App component
const App = () => {
// State
const [dimensions, dispatch] = useReducer(dimensionsReducer, {
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT
});
// Get window size
const windowSize = useWindowSize();
// Setup overlay management
const { createOverlay } = useInputOverlay(dimensions, dispatch);
// Calculate frame positioning
const frameCalculation = useMemo(() => {
const { width, height } = dimensions;
const wr = windowSize.width / width;
const hr = windowSize.height / height;
const ratio = Math.min(wr, hr) * 0.8;
const frameOnScreenWidth = width * ratio;
const frameOnScreenHeight = height * ratio;
const x = Math.round(windowSize.width / 2 - frameOnScreenWidth / 2) + 0.5;
const y = Math.round(windowSize.height / 2 - frameOnScreenHeight / 2) + 0.5;
return {
scale: ratio,
position: { x, y },
screenWidth: frameOnScreenWidth,
screenHeight: frameOnScreenHeight
};
}, [dimensions, windowSize]);
return (
{/* Canvas */}
{/* Scaled window frame */}
{/* Measurement info */}
{/* Input controls */}
);
};
export default App;
```
```js
```
---
# Canvas Minimap — Preview a Large Konva Stage
> Generate a small minimap preview of a large Konva canvas using cloned nodes or image export.
Source: https://konvajs.org/docs/sandbox/Stage_Preview.html
## Need to generate a small preview of the canvas?
There are many ways to generate small preview. `Konva` doesn't provide any methods to do this automatically.
But we can use `Konva` methods to generate preview area manually.
We will show two options - cloning and using images. In large applications it is better to generate preview from the state of the app.
### Clone nodes from the main stage
So we can just clone the stage or the layer and update its internal nodes from the state of the main canvas area.
Also it will make sense to simplify shapes on the preview. Like hide texts, remove strokes and shadows, etc.
Instructions: Try to drag circles and double-click to add new ones. The preview updates while you drag or after you add a shape.
```js
import Konva from 'konva';
// Create preview container
const preview = document.createElement('div');
preview.id = 'preview';
preview.style.position = 'absolute';
preview.style.top = '2px';
preview.style.right = '2px';
preview.style.border = '1px solid grey';
preview.style.backgroundColor = 'lightgrey';
document.body.appendChild(preview);
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// generate random shapes
for (let i = 0; i < 10; i++) {
const shape = new Konva.Circle({
x: Math.random() * stage.width(),
y: Math.random() * stage.height(),
radius: Math.random() * 30 + 5,
fill: Konva.Util.getRandomColor(),
draggable: true,
// each shape MUST have unique name
// so we can easily update the preview clone by name
name: 'shape-' + i,
});
layer.add(shape);
}
// create smaller preview stage
const previewStage = new Konva.Stage({
container: 'preview',
width: window.innerWidth / 4,
height: window.innerHeight / 4,
scaleX: 1 / 4,
scaleY: 1 / 4,
});
// clone original layer, and disable all events on it
let previewLayer = layer.clone({ listening: false });
previewStage.add(previewLayer);
function updatePreview() {
// we just need to update ALL nodes in the preview
layer.children.forEach((shape) => {
// find cloned node
const clone = previewLayer.findOne('.' + shape.name());
// update its position from the original
clone.position(shape.position());
});
}
stage.on('dragmove', updatePreview);
// add new shapes on double click or double tap
stage.on('dblclick dbltap', () => {
const shape = new Konva.Circle({
x: stage.getPointerPosition().x,
y: stage.getPointerPosition().y,
radius: Math.random() * 30 + 5,
fill: Konva.Util.getRandomColor(),
draggable: true,
name: 'shape-' + layer.children.length,
});
layer.add(shape);
// remove all layer
previewLayer.destroy();
// generate new one
previewLayer = layer.clone({ listening: false });
previewStage.add(previewLayer);
});
```
```js
import React from 'react';
import { Stage, Layer, Circle } from 'react-konva';
const getRandomColor = () => {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
};
const App = () => {
const [shapes, setShapes] = React.useState(() =>
Array.from({ length: 10 }, (_, i) => ({
id: i,
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
radius: Math.random() * 30 + 5,
fill: getRandomColor(),
}))
);
const handleDragMove = (e, id) => {
const { x, y } = e.target.position();
setShapes(shapes.map(shape =>
shape.id === id ? { ...shape, x, y } : shape
));
};
const handleDblClick = (e) => {
const stage = e.target.getStage();
const pos = stage.getPointerPosition();
const newShape = {
id: shapes.length,
x: pos.x,
y: pos.y,
radius: Math.random() * 30 + 5,
fill: getRandomColor(),
};
setShapes([...shapes, newShape]);
};
return (
{shapes.map(shape => (
handleDragMove(e, shape.id)}
/>
))}
{shapes.map(shape => (
))}
);
};
export default App;
```
```js
handleDragMove(e, shape.id)"
/>
```
### Use image preview
Or we can export the stage to an image and use it as a preview.
For performance reasons we are not updating the preview on every `dragmove` events.
```js
import Konva from 'konva';
// Create preview container
const preview = document.createElement('img');
preview.id = 'preview';
preview.style.position = 'absolute';
preview.style.top = '2px';
preview.style.right = '2px';
preview.style.border = '1px solid grey';
preview.style.backgroundColor = 'lightgrey';
document.body.appendChild(preview);
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// generate random shapes
for (let i = 0; i < 10; i++) {
const shape = new Konva.Circle({
x: Math.random() * stage.width(),
y: Math.random() * stage.height(),
radius: Math.random() * 30 + 5,
fill: Konva.Util.getRandomColor(),
draggable: true,
name: 'shape-' + i,
});
layer.add(shape);
}
function updatePreview() {
const scale = 1 / 4;
// use pixelRatio to generate smaller preview
const url = stage.toDataURL({ pixelRatio: scale });
preview.src = url;
}
// update preview only on dragend for performance
stage.on('dragend', updatePreview);
// add new shapes on double click or double tap
stage.on('dblclick dbltap', () => {
const shape = new Konva.Circle({
x: stage.getPointerPosition().x,
y: stage.getPointerPosition().y,
radius: Math.random() * 30 + 5,
fill: Konva.Util.getRandomColor(),
draggable: true,
name: 'shape-' + layer.children.length,
});
layer.add(shape);
updatePreview();
});
// show initial preview
updatePreview();
```
```js
import React from 'react';
import { Stage, Layer, Circle } from 'react-konva';
const getRandomColor = () => {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
};
const App = () => {
const [shapes, setShapes] = React.useState(() =>
Array.from({ length: 10 }, (_, i) => ({
id: i,
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
radius: Math.random() * 30 + 5,
fill: getRandomColor(),
}))
);
const [previewUrl, setPreviewUrl] = React.useState('');
const stageRef = React.useRef(null);
const updatePreview = React.useCallback(() => {
if (!stageRef.current) return;
const scale = 1 / 4;
const url = stageRef.current.toDataURL({ pixelRatio: scale });
setPreviewUrl(url);
}, []);
React.useEffect(() => {
updatePreview();
}, [shapes, updatePreview]);
const handleDragEnd = (e, id) => {
const { x, y } = e.target.position();
setShapes(shapes.map(shape =>
shape.id === id ? { ...shape, x, y } : shape
));
};
const handleDblClick = (e) => {
const stage = e.target.getStage();
const pos = stage.getPointerPosition();
const newShape = {
id: shapes.length,
x: pos.x,
y: pos.y,
radius: Math.random() * 30 + 5,
fill: getRandomColor(),
};
setShapes([...shapes, newShape]);
};
return (
{shapes.map(shape => (
handleDragEnd(e, shape.id)}
/>
))}
);
};
export default App;
```
```js
handleDragEnd(e, shape.id)"
/>
```
---
# Star Spinner
> Interactive star shape that spins with angular velocity and friction, controlled by mouse drag.
Source: https://konvajs.org/docs/sandbox/Star_Spinner.html
**Instructions: Spin the star with your mouse.**
```js
import Konva from 'konva';
// disable degree mode to use radians
Konva.angleDeg = false;
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const animatedLayer = new Konva.Layer();
const star = new Konva.Star({
x: stage.width() / 2,
y: stage.height() / 2,
outerRadius: 80,
innerRadius: 40,
stroke: '#005500',
fill: '#b5ff88',
strokeWidth: 4,
numPoints: 5,
lineJoin: 'round',
shadowOffsetX: 5,
shadowOffsetY: 5,
shadowBlur: 10,
shadowColor: 'black',
shadowOpacity: 0.5,
opacity: 0.8,
});
// custom properties
star.lastRotation = 0;
star.angularVelocity = 6;
star.controlled = false;
star.on('mousedown touchstart', function () {
this.angularVelocity = 0;
this.controlled = true;
});
animatedLayer.add(star);
// add center point
const center = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 3,
fill: '#555',
});
animatedLayer.add(center);
// add listeners to container
stage.on('mouseup touchend', function () {
star.controlled = false;
});
stage.on('mousemove touchmove', function () {
if (star.controlled) {
const mousePos = stage.getPointerPosition();
const x = star.x() - mousePos.x;
const y = star.y() - mousePos.y;
star.rotation(0.5 * Math.PI + Math.atan(y / x));
if (mousePos.x <= stage.width() / 2) {
star.rotate(Math.PI);
}
}
});
stage.add(animatedLayer);
// animation
function animate(frame) {
// 20% slow down per second
const angularFriction = 0.2;
const angularVelocityChange =
(star.angularVelocity * frame.timeDiff * (1 - angularFriction)) / 1000;
star.angularVelocity -= angularVelocityChange;
if (star.controlled) {
star.angularVelocity =
((star.rotation() - star.lastRotation) * 1000) / frame.timeDiff;
} else {
star.rotate((frame.timeDiff * star.angularVelocity) / 1000);
}
star.lastRotation = star.rotation();
}
const anim = new Konva.Animation(animate, animatedLayer);
// wait one second and then spin the star
setTimeout(function () {
anim.start();
}, 1000);
```
```jsx
import { Stage, Layer, Star, Circle } from 'react-konva';
import { useEffect, useRef } from 'react';
import Konva from 'konva';
const App = () => {
const controlledRef = useRef(false);
const starRef = useRef(null);
const animRef = useRef(null);
const lastRotationRef = useRef(0);
const angularVelocityRef = useRef(6);
useEffect(() => {
// disable degree mode to use radians
const previousAngleMode = Konva.angleDeg;
Konva.angleDeg = false;
// start animation after 1 second
const timeout = setTimeout(() => {
if (!starRef.current) return;
const layer = starRef.current.getLayer();
animRef.current = new Konva.Animation((frame) => {
const star = starRef.current;
if (!star) return;
// 20% slow down per second
const angularFriction = 0.2;
const angularVelocityChange =
(angularVelocityRef.current * frame.timeDiff * (1 - angularFriction)) / 1000;
angularVelocityRef.current -= angularVelocityChange;
if (controlledRef.current) {
const rotation = star.rotation();
angularVelocityRef.current =
((rotation - lastRotationRef.current) * 1000) / frame.timeDiff;
lastRotationRef.current = rotation;
} else {
star.rotate((frame.timeDiff * angularVelocityRef.current) / 1000);
lastRotationRef.current = star.rotation();
}
}, layer);
animRef.current.start();
}, 1000);
return () => {
clearTimeout(timeout);
if (animRef.current) {
animRef.current.stop();
}
Konva.angleDeg = previousAngleMode;
};
}, []);
const handleMouseDown = () => {
if (!starRef.current) return;
angularVelocityRef.current = 0;
lastRotationRef.current = starRef.current.rotation();
controlledRef.current = true;
};
const handleMouseUp = () => {
if (!starRef.current) return;
lastRotationRef.current = starRef.current.rotation();
controlledRef.current = false;
};
const handleMouseMove = (e) => {
if (!controlledRef.current || !starRef.current) return;
const stage = e.target.getStage();
const mousePos = stage.getPointerPosition();
const star = starRef.current;
const x = star.x() - mousePos.x;
const y = star.y() - mousePos.y;
star.rotation(0.5 * Math.PI + Math.atan(y / x));
if (mousePos.x <= stage.width() / 2) {
star.rotate(Math.PI);
}
};
return (
);
};
export default App;
```
```js
```
---
# How to apply transparency for several shapes at once?
> Apply uniform transparency to a group of overlapping shapes using Konva group caching.
Source: https://konvajs.org/docs/sandbox/Transparent_Group.html
# How to apply transparency for several shapes at once?
## Is it possible to use opacity for several shapes at the same time?
You can use the `opacity` attribute to change the alpha channel of any `Konva` node. Due to how canvas works, all shapes have their own independent opacity values.
That means if you have a group with several shapes inside and that group has `group.opacity(0.5)`, it will look exactly the same as if each shape inside the group has `shape.opacity(0.5)` and the group has `group.opacity(1)`. This means you will see overlapping areas of those shapes.
### What if we don't want to see overlapping areas of transparent shapes?
There is a way to fix such default behavior. You just need to cache the group with `group.cache()`. Caching the group will convert it into a bitmap and draw it into an external canvas. On the next draw call, `Konva` will use that resulted canvas to draw the whole group with opacity applied to the whole image.
So while `Konva` is making a bitmap cache for such group, it will draw internal shapes ignoring transparency of the group.
**Remember that if a group is cached, it has some limitations of cached nodes. If you are doing any internal changes (like changing shapes attributes), you have to recache the group. This is an expensive operation, so it is not recommended to do it frequently like inside animations or on every mousemove.**
In the demo below, on the left you see the default behavior, on the right you see the fixed behavior with a cached group.
Try dragging both groups to see the difference in how transparency is applied. The left group shows the default behavior with visible overlapping areas, while the right group shows the cached behavior where the entire group is treated as a single transparent unit.
```js
import Konva from 'konva';
// Stage setup
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
// lets create default group with two overlapping shapes
const group1 = new Konva.Group({
opacity: 0.5,
x: 50,
y: 50,
draggable: true,
});
group1.add(
new Konva.Rect({
width: 100,
height: 100,
fill: 'red',
})
);
group1.add(
new Konva.Circle({
x: 100,
y: 100,
radius: 70,
fill: 'green',
})
);
layer.add(group1);
// lets create the second group
const group2 = group1.clone({ x: 250 });
layer.add(group2);
// to change opacity behavior we have to cache whole group
group2.cache();
```
```js
import { Stage, Layer, Group, Rect, Circle } from 'react-konva';
import { useEffect, useRef, useState } from 'react';
const App = () => {
const group2Ref = useRef(null);
const [groups, setGroups] = useState({
default: { x: 50, y: 50 },
cached: { x: 250, y: 50 },
});
useEffect(() => {
if (group2Ref.current) {
// Cache the second group to change opacity behavior
group2Ref.current.cache();
}
}, []);
const sharedGroupProps = {
opacity: 0.5,
draggable: true,
};
const handleDragEnd = (id, e) => {
setGroups((current) => ({
...current,
[id]: e.target.position(),
}));
};
const renderGroup = (id, ref = null) => (
handleDragEnd(id, e)}
>
);
return (
{/* Default group with overlapping shapes */}
{renderGroup('default')}
{/* Cached group with fixed opacity behavior */}
{renderGroup('cached', group2Ref)}
);
};
export default App;
```
```js
```
---
# How to display video on Canvas
> Play and display video on HTML5 canvas with Konva, including play/pause controls and drag support.
Source: https://konvajs.org/docs/sandbox/Video_On_Canvas.html
# How to display video on Canvas
> Also take a look at this post for additional information: [Case Study: Video Editor for Stream](https://lavrton.com/case-study-video-editor-for-stream/)
The demo below shows how to display a video on canvas with play/pause controls. You can also drag and drop the video around the canvas.
```js
import Konva from 'konva';
// create buttons
const playButton = document.createElement('button');
playButton.textContent = 'Play';
playButton.id = 'play';
document.body.appendChild(playButton);
const pauseButton = document.createElement('button');
pauseButton.textContent = 'Pause';
pauseButton.id = 'pause';
document.body.appendChild(pauseButton);
const width = window.innerWidth;
const height = 300;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
const video = document.createElement('video');
const image = new Konva.Image({
image: video,
draggable: true,
x: 50,
y: 20,
});
layer.add(image);
const text = new Konva.Text({
text: 'Loading video...',
width: stage.width(),
height: stage.height(),
align: 'center',
verticalAlign: 'middle',
});
layer.add(text);
const anim = new Konva.Animation(function () {
// do nothing, animation just needs to update the layer
}, layer);
// update Konva.Image size when meta is loaded
video.addEventListener('loadedmetadata', function () {
text.text('Press PLAY...');
image.width(video.videoWidth);
image.height(video.videoHeight);
});
video.src =
'https://upload.wikimedia.org/wikipedia/commons/transcoded/c/c4/Physicsworks.ogv/Physicsworks.ogv.240p.vp9.webm';
document.getElementById('play').addEventListener('click', function () {
text.destroy();
video.play();
anim.start();
});
document.getElementById('pause').addEventListener('click', function () {
video.pause();
anim.stop();
});
```
```js
import Konva from 'konva';
import { Stage, Layer, Image, Text } from 'react-konva';
import { useEffect, useRef, useState } from 'react';
const App = () => {
const [dimensions, setDimensions] = useState({
width: window.innerWidth,
height: 400,
});
const [videoElement] = useState(() => document.createElement('video'));
const [videoSize, setVideoSize] = useState({ width: 0, height: 0 });
const [videoPosition, setVideoPosition] = useState({ x: 50, y: 20 });
const [status, setStatus] = useState('Loading video...');
const animationRef = useRef(null);
const layerRef = useRef(null);
useEffect(() => {
const handleResize = () => {
setDimensions({
width: window.innerWidth,
height: 400,
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
useEffect(() => {
const handleMetadata = () => {
setStatus('Press PLAY...');
setVideoSize({
width: videoElement.videoWidth,
height: videoElement.videoHeight,
});
};
videoElement.addEventListener('loadedmetadata', handleMetadata);
videoElement.src =
'https://upload.wikimedia.org/wikipedia/commons/transcoded/c/c4/Physicsworks.ogv/Physicsworks.ogv.240p.vp9.webm';
if (videoElement.readyState >= 1) handleMetadata();
return () => videoElement.removeEventListener('loadedmetadata', handleMetadata);
}, [videoElement]);
useEffect(() => {
if (!layerRef.current) return;
const animation = new Konva.Animation(() => {}, layerRef.current);
animationRef.current = animation;
return () => {
animation.stop();
videoElement.pause();
animationRef.current = null;
};
}, [videoElement]);
const handlePlay = () => {
setStatus('');
videoElement.play();
animationRef.current?.start();
};
const handlePause = () => {
videoElement.pause();
if (animationRef.current) {
animationRef.current.stop();
}
};
return (
Play
Pause
setVideoPosition(event.target.position())}
/>
{status && (
)}
);
};
export default App;
```
```js
Play
Pause
```
The demo shows how to:
1. Create a video element and use it as the source for a Konva.Image
2. Implement play/pause controls for the video
3. Use Konva.Animation to continuously update the layer while the video is playing
4. Make the video draggable on the canvas
5. Display loading and play status messages
6. Handle video metadata to set the correct dimensions
Try playing the video and dragging it around the canvas. The video will continue playing while you move it.
---
# Offscreen canvas inside Web Worker
> Run Konva rendering inside a Web Worker using OffscreenCanvas for off-main-thread performance.
Source: https://konvajs.org/docs/sandbox/Web_Worker.html
## How to run Konva in a Web Worker?
**Warning! This demo is VERY EXPERIMENTAL! It may not work in many browsers.** Check [Offscreen canvas capability tabletv](https://caniuse.com/#feat=offscreencanvas).
With some extra work we can render `Konva` stage inside a [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Worker) using [Offscreen Canvas](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas) for performance reasons or for some crazy ideas.
You can use a webworker to make some visualizations with `Konva`.
But one of the main features of `Konva` is its interactivity (full events support for canvas shapes). And there are no DOM events inside a web worker. So we have to write some kind of "proxy" to pass all DOM events inside Konva engine. That way we can have interactive objects inside web worker too.
This demo is adopted from [Jumping bunnies](/docs/sandbox/Jumping_Bunnies.html) performance stress test.
You may need to write more code to cover more functions and different edge cases (such as HDPI screen support).
**Instructions: there are two interactive objects on the stage. "Add buttons" and a draggable red circle. Try to add more bunnies or drag the circle.**
All you see on that screen is **rendered inside another javascript thread**!. So it should not block main JS thread of the current page.
```js
// main.js
const workerCode = `
// load konva framework
importScripts('https://unpkg.com/konva@10/konva.min.js');
// monkeypatch Konva for offscreen canvas usage
Konva.Util.createCanvasElement = () => {
const canvas = new OffscreenCanvas(1, 1);
canvas.style = {};
return canvas;
};
// now we can create our canvas content
var stage = new Konva.Stage({
width: 200,
height: 200,
});
var layer = new Konva.Layer();
stage.add(layer);
var topGroup = new Konva.Group();
layer.add(topGroup);
// counter will show number of bunnies
var counter = new Konva.Text({
x: 5,
y: 35,
});
topGroup.add(counter);
// "add more bunnies" button
var button = new Konva.Label({
x: 5,
y: 5,
opacity: 0.75,
});
topGroup.add(button);
button.add(
new Konva.Tag({
fill: 'black',
})
);
button.add(
new Konva.Text({
text: 'Push me to add bunnies',
fontFamily: 'Calibri',
fontSize: 18,
padding: 5,
fill: 'white',
})
);
// draggable circle to show interactivity
var circle = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 20,
fill: 'red',
draggable: true,
});
topGroup.add(circle);
self.onmessage = function (evt) {
// when canvas is passes we can start our worker
if (evt.data.canvas) {
var canvas = evt.data.canvas;
stage.setSize({
width: canvas.width,
height: canvas.height,
});
const ctx = canvas.getContext('2d');
layer.on('draw', () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(layer.getCanvas()._canvas, 0, 0);
});
}
// emulate some drag&drop events
if (evt.data.eventName === 'mouseup') {
Konva.DD._endDragBefore(evt.data.event);
}
if (evt.data.eventName === 'touchend') {
Konva.DD._endDragBefore(evt.data.event);
}
if (evt.data.eventName === 'mousemove') {
Konva.DD._drag(evt.data.event);
}
if (evt.data.eventName === 'touchmove') {
Konva.DD._drag(evt.data.event);
}
if (evt.data.eventName === 'mouseup') {
Konva.DD._endDragAfter(evt.data.event);
}
if (evt.data.eventName === 'touchend') {
Konva.DD._endDragAfter(evt.data.event);
}
// pass incoming events into the stage
if (evt.data.eventName) {
const event = evt.data.eventName.replace('mouse', 'pointer');
stage['_' + event](evt.data.event);
}
};
function requestAnimationFrame(cb) {
setTimeout(cb, 16);
}
async function runBunnies() {
const imgBlob = await fetch('https://konvajs.org/assets/bunny.png').then(
(r) => r.blob()
);
const img = await createImageBitmap(imgBlob);
var bunnys = [];
var gravity = 0.75;
var startBunnyCount = 100;
var isAdding = false;
var count = 0;
var amount = 10;
button.on('mousedown', function () {
isAdding = true;
});
button.on('mouseup', function () {
isAdding = false;
});
for (var i = 0; i < startBunnyCount; i++) {
var bunny = new Konva.Image({
image: img,
transformsEnabled: 'position',
x: 10,
y: 10,
listening: false,
});
bunny.speedX = Math.random() * 10;
bunny.speedY = Math.random() * 10 - 5;
bunnys.push(bunny);
counter.text('Bunnies number: ' + bunnys.length);
layer.add(bunny);
}
topGroup.moveToTop();
function update() {
var maxX = stage.width() - 10;
var minX = 0;
var maxY = stage.height() - 10;
var minY = 0;
if (isAdding) {
for (var i = 0; i < amount; i++) {
var bunny = new Konva.Image({
image: img,
transformsEnabled: 'position',
x: 0,
y: 0,
listening: false,
});
bunny.speedX = Math.random() * 10;
bunny.speedY = Math.random() * 10 - 5;
bunnys.push(bunny);
layer.add(bunny);
counter.text('Bunnies number: ' + bunnys.length);
count++;
}
topGroup.moveToTop();
}
for (var i = 0; i < bunnys.length; i++) {
var bunny = bunnys[i];
bunny.setX(bunny.getX() + bunny.speedX);
bunny.setY(bunny.getY() + bunny.speedY);
bunny.speedY += gravity;
if (bunny.getX() > maxX - img.width) {
bunny.speedX *= -1;
bunny.setX(maxX - img.width);
} else if (bunny.getX() < minX) {
bunny.speedX *= -1;
bunny.setX(minX);
}
if (bunny.getY() > maxY - img.height) {
bunny.speedY *= -0.85;
bunny.setY(maxY - img.height);
if (Math.random() > 0.5) {
bunny.speedY -= Math.random() * 6;
}
} else if (bunny.getY() < minY) {
bunny.speedY = 0;
bunny.setY(minY);
}
}
layer.drawScene();
requestAnimationFrame(update);
}
update();
}
runBunnies();
`;
// Create a blob from the worker code
const blob = new Blob([workerCode], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
canvas.style.border = '1px solid black';
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// transfer control of the canvas to the worker
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);
// proxy all events
const events = [
'mousedown',
'mouseup',
'mousemove',
'mouseenter',
'mouseleave',
// 'click',
// 'dblclick',
'touchstart',
'touchend',
'touchmove',
];
events.forEach((eventName) => {
canvas.addEventListener(eventName, (event) => {
worker.postMessage({
eventName,
event: {
clientX: event.clientX,
clientY: event.clientY,
type: event.type,
button: event.button,
},
});
});
});
```
---
# Wheel of Fortune HTML5 Canvas Game
> Build an interactive Wheel of Fortune game with spin physics and prize detection using Konva.
Source: https://konvajs.org/docs/sandbox/Wheel_of_Fortune.html
This demo shows how to create an interactive Wheel of Fortune game using Konva. The wheel can be spun with mouse or touch input, and it will gradually slow down due to angular friction. When it stops, it will show your prize!
```js
import Konva from 'konva';
Konva.angleDeg = false;
let angularVelocity = 6;
const angularVelocities = [];
let lastRotation = 0;
let controlled = false;
const numWedges = 25;
const angularFriction = 0.2;
let target, activeWedge, stage, layer, wheel, pointer;
let finished = false;
function getAverageAngularVelocity() {
const total = angularVelocities.reduce((sum, vel) => sum + vel, 0);
return angularVelocities.length ? total / angularVelocities.length : 0;
}
function purifyColor(color) {
const randIndex = Math.round(Math.random() * 3);
color[randIndex] = 0;
return color;
}
function getRandomColor() {
const r = 100 + Math.round(Math.random() * 55);
const g = 100 + Math.round(Math.random() * 55);
const b = 100 + Math.round(Math.random() * 55);
return purifyColor([r, g, b]);
}
function getRandomReward() {
const mainDigit = Math.round(Math.random() * 9);
return mainDigit + '\n0\n0';
}
function addWedge(n) {
const s = getRandomColor();
const reward = getRandomReward();
const [r, g, b] = s;
const angle = (2 * Math.PI) / numWedges;
const endColor = `rgb(${r},${g},${b})`;
const startColor = `rgb(${r + 100},${g + 100},${b + 100})`;
const wedge = new Konva.Group({
rotation: (2 * n * Math.PI) / numWedges,
});
const wedgeBackground = new Konva.Wedge({
radius: 400,
angle: angle,
fillRadialGradientStartPoint: 0,
fillRadialGradientStartRadius: 0,
fillRadialGradientEndPoint: 0,
fillRadialGradientEndRadius: 400,
fillRadialGradientColorStops: [0, startColor, 1, endColor],
fill: '#64e9f8',
fillPriority: 'radial-gradient',
stroke: '#ccc',
strokeWidth: 2,
});
wedge.add(wedgeBackground);
const text = new Konva.Text({
text: reward,
fontFamily: 'Calibri',
fontSize: 50,
fill: 'white',
align: 'center',
stroke: 'yellow',
strokeWidth: 1,
rotation: (Math.PI + angle) / 2,
x: 380,
y: 30,
listening: false,
});
wedge.add(text);
text.cache();
wedge.startRotation = wedge.rotation();
wheel.add(wedge);
}
function animate(frame) {
// handle wheel spin
const angularVelocityChange =
(angularVelocity * frame.timeDiff * (1 - angularFriction)) / 1000;
angularVelocity -= angularVelocityChange;
// activate / deactivate wedges based on point intersection
const shape = stage.getIntersection({
x: stage.width() / 2,
y: 100,
});
if (controlled) {
if (angularVelocities.length > 10) {
angularVelocities.shift();
}
angularVelocities.push(
((wheel.rotation() - lastRotation) * 1000) / frame.timeDiff
);
} else {
const diff = (frame.timeDiff * angularVelocity) / 1000;
if (diff > 0.0001) {
wheel.rotate(diff);
} else if (!finished && !controlled) {
if (shape) {
const text = shape.getParent().findOne('Text').text();
const price = text.split('\n').join('');
alert('Your price is ' + price);
}
finished = true;
}
}
lastRotation = wheel.rotation();
if (shape && (!activeWedge || shape._id !== activeWedge._id)) {
pointer.y(20);
new Konva.Tween({
node: pointer,
duration: 0.3,
y: 30,
easing: Konva.Easings.ElasticEaseOut,
}).play();
if (activeWedge) {
activeWedge.fillPriority('radial-gradient');
}
shape.fillPriority('fill');
activeWedge = shape;
}
}
stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: 400,
});
layer = new Konva.Layer();
wheel = new Konva.Group({
x: stage.width() / 2,
y: 410,
});
for (let n = 0; n < numWedges; n++) {
addWedge(n);
}
pointer = new Konva.Wedge({
fillRadialGradientStartPoint: 0,
fillRadialGradientStartRadius: 0,
fillRadialGradientEndPoint: 0,
fillRadialGradientEndRadius: 30,
fillRadialGradientColorStops: [0, 'white', 1, 'red'],
stroke: 'white',
strokeWidth: 2,
lineJoin: 'round',
angle: 1,
radius: 30,
x: stage.width() / 2,
y: 33,
rotation: -90,
shadowColor: 'black',
shadowOffsetX: 3,
shadowOffsetY: 3,
shadowBlur: 2,
shadowOpacity: 0.5,
});
// add components to the stage
layer.add(wheel);
layer.add(pointer);
stage.add(layer);
// bind events
wheel.on('mousedown touchstart', function (evt) {
angularVelocity = 0;
controlled = true;
target = evt.target;
finished = false;
});
stage.on('mouseup touchend', function () {
controlled = false;
angularVelocity = getAverageAngularVelocity() * 5;
if (angularVelocity > 20) {
angularVelocity = 20;
} else if (angularVelocity < -20) {
angularVelocity = -20;
}
angularVelocities.length = 0;
});
stage.on('mousemove touchmove', function () {
const mousePos = stage.getPointerPosition();
if (controlled && mousePos && target) {
const x = mousePos.x - wheel.getX();
const y = mousePos.y - wheel.getY();
const atan = Math.atan(y / x);
const rotation = x >= 0 ? atan : atan + Math.PI;
wheel.rotation(rotation);
}
});
// create animation
const anim = new Konva.Animation(animate, layer);
anim.start();
```
---
# How to Build a Window Frame Configurator with JavaScript Canvas
> Build a CAD-style window frame configurator with JavaScript and HTML5 Canvas using Konva.js. Interactive demo of a product configurator with section splitting and sash type selection.
Source: https://konvajs.org/docs/sandbox/Window_Frame_Designer.html
# HTML5 Canvas Window Frame Designer
This is a demo of a window frame constructor, created as a prototype of a large CAD system. The demo showcases how to build a complex interactive application using React, Konva, and react-konva.
If your company needs a similar product, [get in touch](https://lavrton.com/consulting/).
## Features
- Select sections of the window frame
- Split sections into multiple child sections
- Set different sash types for each section
- Interactive visual design
- Real-time updates
## Instructions
1. Click on a section to select it
2. Use the controls to split the selected section horizontally or vertically
3. Choose different sash types for each section
4. Experiment with different window frame designs
## Demo
[Open the interactive demo](https://codesandbox.io/embed/github/konvajs/site/tree/master/react-demos/window-frame-design-app?hidenavigation=1&view=split&fontsize=10)
## Implementation Details
This demo is built using:
- React for UI components and state management
- Konva for canvas rendering
- react-konva for integrating Konva with React
The application demonstrates several advanced concepts:
1. Tree-like data structures for representing window sections
2. Complex mouse interactions for selection
3. Dynamic shape rendering based on section properties
4. Responsive layout calculations
5. Component composition for reusable UI elements
The complete source code is available in the CodeSandbox demo above. You can explore it to learn how to:
- Structure a complex React application with Konva
- Handle nested interactive shapes
- Manage state for a tree-like structure
- Implement undo/redo functionality
- Create a responsive canvas application
This demo shows how Konva can be used to build sophisticated CAD-like applications with smooth interactions and real-time updates.
---
# Zoom Image on Hover
> Zoom into an image on mouse hover by scaling the Konva layer and following the cursor position.
Source: https://konvajs.org/docs/sandbox/Zoom_Layer_On_hover.html
**Instructions: Hover over an Image.**
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
const imageObj = new Image();
imageObj.onload = function () {
const backgroundImage = new Konva.Image({
x: 0,
y: 0,
width: width,
height: height,
image: imageObj,
});
layer.add(backgroundImage);
};
imageObj.src = 'https://konvajs.org/assets/space.jpg';
const zoomLevel = 2;
layer.on('mouseenter', function () {
layer.scale({
x: zoomLevel,
y: zoomLevel,
});
});
layer.on('mousemove', function (e) {
const pos = stage.getPointerPosition();
layer.x(-pos.x);
layer.y(-pos.y);
});
layer.on('mouseleave', function () {
layer.x(0);
layer.y(0);
layer.scale({
x: 1,
y: 1,
});
});
```
```js
import { Stage, Layer, Image } from 'react-konva';
import { useEffect, useState } from 'react';
import useImage from 'use-image';
const App = () => {
const [image] = useImage('https://konvajs.org/assets/space.jpg');
const [scale, setScale] = useState(1);
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseEnter = () => {
setScale(2);
};
const handleMouseMove = (e) => {
const stage = e.target.getStage();
const pos = stage.getPointerPosition();
setPosition({
x: -pos.x,
y: -pos.y,
});
};
const handleMouseLeave = () => {
setScale(1);
setPosition({ x: 0, y: 0 });
};
return (
{image && (
)}
);
};
export default App;
```
```js
```
---
# Canvas Zoom and Pan — Zoom Relative to Pointer Position
> Implement canvas zoom and pan with JavaScript. Zoom in and out relative to mouse pointer position using scroll wheel on a Konva stage.
Source: https://konvajs.org/docs/sandbox/Zooming_Relative_To_Pointer.html
# Zooming stage relative to pointer position
This demo shows how to implement zooming that is relative to the mouse pointer position. This creates a more natural zooming experience where the content scales around the mouse cursor.
**Instructions:** Use your mouse wheel or trackpad to zoom in and out. Notice how the content scales around the position of your cursor, rather than the center of the stage.
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
const circle = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 50,
fill: 'green',
});
layer.add(circle);
const scaleBy = 1.01;
stage.on('wheel', (e) => {
// stop default scrolling
e.evt.preventDefault();
const oldScale = stage.scaleX();
const pointer = stage.getPointerPosition();
const mousePointTo = {
x: (pointer.x - stage.x()) / oldScale,
y: (pointer.y - stage.y()) / oldScale,
};
// how to scale? Zoom in? Or zoom out?
let direction = e.evt.deltaY > 0 ? 1 : -1;
// when we zoom on trackpad, e.evt.ctrlKey is true
// in that case lets revert direction
if (e.evt.ctrlKey) {
direction = -direction;
}
const newScale = direction > 0 ? oldScale * scaleBy : oldScale / scaleBy;
stage.scale({ x: newScale, y: newScale });
const newPos = {
x: pointer.x - mousePointTo.x * newScale,
y: pointer.y - mousePointTo.y * newScale,
};
stage.position(newPos);
});
```
```js
import { Stage, Layer, Circle } from 'react-konva';
import { useRef } from 'react';
const App = () => {
const width = window.innerWidth;
const height = window.innerHeight;
const stageRef = useRef(null);
const handleWheel = (e) => {
e.evt.preventDefault();
const stage = stageRef.current;
const oldScale = stage.scaleX();
const pointer = stage.getPointerPosition();
const mousePointTo = {
x: (pointer.x - stage.x()) / oldScale,
y: (pointer.y - stage.y()) / oldScale,
};
// how to scale? Zoom in? Or zoom out?
let direction = e.evt.deltaY > 0 ? 1 : -1;
// when we zoom on trackpad, e.evt.ctrlKey is true
// in that case lets revert direction
if (e.evt.ctrlKey) {
direction = -direction;
}
const scaleBy = 1.01;
const newScale = direction > 0 ? oldScale * scaleBy : oldScale / scaleBy;
stage.scale({ x: newScale, y: newScale });
const newPos = {
x: pointer.x - mousePointTo.x * newScale,
y: pointer.y - mousePointTo.y * newScale,
};
stage.position(newPos);
};
return (
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Shape Resize Relative to Center
> Learn how to resize shapes from the center using Konva Transformer's centeredScaling property or ALT key.
Source: https://konvajs.org/docs/select_and_transform/Centered_Scaling.html
To resize a node into both sides at the same time you can set `centeredScaling` to true or hold `ALT` key while moving an anchor (even if `centeredScaling` is false).
**Instructions: Try to resize texts.**
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
const text = new Konva.Text({
x: 50,
y: 80,
text: 'Simple text',
fontSize: 30,
draggable: true,
width: 200,
});
layer.add(text);
const text2 = new Konva.Text({
x: 50,
y: 180,
text: 'Simple text',
fontSize: 30,
draggable: true,
width: 200,
});
layer.add(text2);
const tr = new Konva.Transformer({
nodes: [text],
centeredScaling: true,
});
layer.add(tr);
const tr2 = new Konva.Transformer({
nodes: [text2],
});
layer.add(tr2);
````
```js
import { useRef, useEffect, useState } from 'react'
import { Stage, Layer, Text, Transformer } from 'react-konva';
const App = () => {
const [text1Position, setText1Position] = useState({ x: 50, y: 80 });
const [text2Position, setText2Position] = useState({ x: 50, y: 180 });
const text1Ref = useRef()
const text2Ref = useRef()
const tr1Ref = useRef()
const tr2Ref = useRef()
useEffect(() => {
tr1Ref.current.nodes([text1Ref.current]);
tr2Ref.current.nodes([text2Ref.current]);
}, []);
return (
setText1Position(e.target.position())}
onTransformEnd={(e) => setText1Position(e.target.position())}
/>
setText2Position(e.target.position())}
onTransformEnd={(e) => setText2Position(e.target.position())}
/>
);
};
export default App;
````
```js
```
---
# HTML5 Canvas Force Update Tutorial
> Manually refresh Konva Transformer with forceUpdate when it cannot automatically detect deep changes in groups.
Source: https://konvajs.org/docs/select_and_transform/Force_Update.html
`Konva.Transformer` automatically tracks properties of attached nodes.
So it will adopt its own properties automatically.
But in some cases `Konva.Transformer` can't do this. Currently `Konva.Transformer` can not track deep changes inside `Konva.Group` node. In this case you will need to use `forceUpdate` method to reset transforming tools.
**Instructions: Click the button. See how transformer is changed.**
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
const group = new Konva.Group({
x: 50,
y: 50,
draggable: true,
});
layer.add(group);
const text = new Konva.Text({
text: 'Some text here',
fontSize: 24,
});
group.add(text);
const rect = new Konva.Rect({
width: text.width(),
height: text.height(),
fill: 'yellow',
});
group.add(rect);
// add the shape to the layer
rect.moveToBottom();
const tr = new Konva.Transformer({
nodes: [group],
padding: 5,
// enable only one anchor
enabledAnchors: ['middle-left', 'middle-right'],
});
layer.add(tr);
const button = document.createElement('button');
button.innerHTML = 'Change text';
document.body.appendChild(button);
button.addEventListener('click', () => {
text.text('Something else is here');
rect.width(text.width());
// we need to update transformer manually
tr.forceUpdate();
});
````
```js
import { Stage, Layer, Text, Rect, Group, Transformer } from 'react-konva';
import { useState, useRef, useLayoutEffect } from 'react';
const App = () => {
const [text, setText] = useState('Some text here');
const [groupPosition, setGroupPosition] = useState({ x: 50, y: 50 });
const groupRef = useRef();
const textRef = useRef();
const rectRef = useRef();
const trRef = useRef();
const handleClick = () => {
setText('Something else is here');
};
useLayoutEffect(() => {
const transformer = trRef.current;
const label = textRef.current;
const background = rectRef.current;
if (!transformer || !label || !background) return;
background.size({ width: label.width(), height: label.height() });
transformer.nodes([groupRef.current]);
transformer.forceUpdate();
}, [text]);
return (
<>
Change text
setGroupPosition(event.target.position())}
onTransformEnd={(event) => setGroupPosition(event.target.position())}
>
>
);
};
export default App;
````
```js
Change text
```
---
# How to resize shape on canvas without changing its stroke size?
> Prevent stroke width from scaling during shape resize with Konva Transformer using ignoreStroke and strokeScaleEnabled.
Source: https://konvajs.org/docs/select_and_transform/Ignore_Stroke_On_Transform.html
Remember, that `Konva.Transformer` is changing `scaleX` and `scaleY` properties of a node.
By default, if you are transforming a shape, its stroke will be scaled too. In some cases that is not a good behavior.
There are two ways to prevent stroke scaling:
1. Reset scale of a shape
2. Use `shape.strokeScaleEnabled(false)` and `transformer.ignoreStroke(true)`
**Instructions: there are two rectangles to resize. The green one will reset its scale. The red one will just disable stroke scaling.**
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
// first way - reset scale on transform end
const rect1 = new Konva.Rect({
x: 50,
y: 50,
width: 100,
height: 100,
fill: '#00ff00',
stroke: 'black',
strokeWidth: 5,
draggable: true,
});
layer.add(rect1);
const tr1 = new Konva.Transformer({
nodes: [rect1],
});
layer.add(tr1);
rect1.on('transformend', () => {
const scaleX = rect1.scaleX();
const scaleY = rect1.scaleY();
// apply the scale to the size, then reset it
rect1.scaleX(1);
rect1.scaleY(1);
rect1.width(Math.max(5, rect1.width() * scaleX));
rect1.height(Math.max(5, rect1.height() * scaleY));
});
// second way - disable stroke scaling
const rect2 = new Konva.Rect({
x: 200,
y: 50,
width: 100,
height: 100,
fill: '#ff0000',
stroke: 'black',
strokeWidth: 5,
draggable: true,
strokeScaleEnabled: false,
});
layer.add(rect2);
const tr2 = new Konva.Transformer({
nodes: [rect2],
ignoreStroke: true,
});
layer.add(tr2);
````
```js
import { Stage, Layer, Rect, Transformer } from 'react-konva';
import { useRef, useEffect, useState } from 'react';
const App = () => {
const [rect1Attrs, setRect1Attrs] = useState({
x: 50,
y: 50,
width: 100,
height: 100,
});
const [rect2Position, setRect2Position] = useState({ x: 200, y: 50 });
const rect1Ref = useRef();
const rect2Ref = useRef();
const tr1Ref = useRef();
const tr2Ref = useRef();
useEffect(() => {
tr1Ref.current.nodes([rect1Ref.current]);
tr2Ref.current.nodes([rect2Ref.current]);
}, []);
return (
{
setRect1Attrs((attrs) => ({
...attrs,
...e.target.position(),
}));
}}
onTransformEnd={(e) => {
const node = rect1Ref.current;
const nextAttrs = {
x: node.x(),
y: node.y(),
width: node.width() * node.scaleX(),
height: node.height() * node.scaleY(),
};
node.scaleX(1);
node.scaleY(1);
setRect1Attrs(nextAttrs);
}}
/>
setRect2Position(e.target.position())}
onTransformEnd={(e) => setRect2Position(e.target.position())}
/>
);
};
export default App;
````
```js
```
---
# HTML5 Canvas Shape Resize With Ratio Preserved
> Learn how to preserve aspect ratio when resizing shapes with Konva Transformer using keepRatio and SHIFT key.
Source: https://konvajs.org/docs/select_and_transform/Keep_Ratio.html
By default when you resize with corner anchors (`top-left`, `top-right`, `bottom-left` or `bottom-right`) Transformer will save ratio of a node.
You can set `keepRatio` to `false` if you don't need that behavior.
Even if you set `keepRatio` to `false` you can hold `SHIFT` to still keep ratio.
**Instructions: Try to resize texts.**
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
const text = new Konva.Text({
x: 50,
y: 50,
text: 'keep ratio by default',
fontSize: 20,
draggable: true,
width: 200,
});
layer.add(text);
const text2 = new Konva.Text({
x: 50,
y: 150,
text: 'no ratio, but hold shift to keep ratio',
fontSize: 20,
draggable: true,
width: 200,
});
layer.add(text2);
const tr = new Konva.Transformer({
nodes: [text],
});
layer.add(tr);
const tr2 = new Konva.Transformer({
nodes: [text2],
keepRatio: false,
});
layer.add(tr2);
````
```js
import { Stage, Layer, Text, Transformer } from 'react-konva';
import { useRef, useEffect, useState } from 'react';
const App = () => {
const [text1Position, setText1Position] = useState({ x: 50, y: 50 });
const [text2Position, setText2Position] = useState({ x: 50, y: 150 });
const text1Ref = useRef();
const text2Ref = useRef();
const tr1Ref = useRef();
const tr2Ref = useRef();
useEffect(() => {
tr1Ref.current.nodes([text1Ref.current]);
tr2Ref.current.nodes([text2Ref.current]);
}, []);
return (
setText1Position(e.target.position())}
onTransformEnd={(e) => setText1Position(e.target.position())}
/>
setText2Position(e.target.position())}
onTransformEnd={(e) => setText2Position(e.target.position())}
/>
);
};
export default App;
````
```js
```
---
# HTML5 Canvas Shape Resize and Transform Limits
> Learn how to set minimum and maximum size limits when resizing shapes on HTML5 Canvas with Konva.js Transformer.
Source: https://konvajs.org/docs/select_and_transform/Resize_Limits.html
To limit or change resize and transform behavior you can use `boundBoxFunc` property.
It works a bit similar to [dragBoundFunc](/docs/drag_and_drop/Simple_Drag_Bounds.html).
**Instructions: Try to resize a shape. You will see that its width is limited to 200.**
The demo clamps the new bounding box to the limit instead of rejecting it. If you simply `return oldBox` when the size is over the limit, a fast mouse move overshoots the boundary in one event, and the shape gets stuck below the limit. Interpolating between `oldBox` and `newBox` lands exactly on the limit and keeps the opposite side of the shape pinned for every anchor.
Also you can control movement of every anchors individually. See [Resize Snap Demo](https://konvajs.org/docs/select_and_transform/Resize_Snaps.html).
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
const rect = new Konva.Rect({
x: 50,
y: 50,
width: 100,
height: 100,
fill: 'yellow',
stroke: 'black',
draggable: true,
});
layer.add(rect);
const tr = new Konva.Transformer({
nodes: [rect],
boundBoxFunc: (oldBox, newBox) => {
// limit resize to a maximum width of 200.
// clamp the box between oldBox and newBox instead of
// rejecting it, so a fast drag still lands exactly on the limit
if (newBox.width > 200) {
const t = (200 - oldBox.width) / (newBox.width - oldBox.width);
return {
x: oldBox.x + t * (newBox.x - oldBox.x),
y: oldBox.y + t * (newBox.y - oldBox.y),
width: 200,
height: oldBox.height + t * (newBox.height - oldBox.height),
rotation: newBox.rotation,
};
}
return newBox;
},
});
layer.add(tr);
````
```js
import { Stage, Layer, Rect, Transformer } from 'react-konva';
import { useRef, useEffect, useState } from 'react';
const App = () => {
const [rectPosition, setRectPosition] = useState({ x: 50, y: 50 });
const rectRef = useRef();
const trRef = useRef();
useEffect(() => {
trRef.current.nodes([rectRef.current]);
}, []);
return (
setRectPosition(e.target.position())}
onTransformEnd={(e) => setRectPosition(e.target.position())}
/>
{
// limit resize to a maximum width of 200.
// clamp the box between oldBox and newBox instead of
// rejecting it, so a fast drag still lands exactly on the limit
if (newBox.width > 200) {
const t = (200 - oldBox.width) / (newBox.width - oldBox.width);
return {
x: oldBox.x + t * (newBox.x - oldBox.x),
y: oldBox.y + t * (newBox.y - oldBox.y),
width: 200,
height: oldBox.height + t * (newBox.height - oldBox.height),
rotation: newBox.rotation,
};
}
return newBox;
}}
/>
);
};
export default App;
````
```js
```
---
# HTML5 Canvas Shape Resize Snapping
> Snap shape resize anchors to guide lines using anchorDragBoundFunc in Konva Transformer.
Source: https://konvajs.org/docs/select_and_transform/Resize_Snaps.html
In some applications, you may want to snap resizing near some values. Snapping makes a shape "sticky" near provided values and works like rounding.
You can control anchor position behavior with the [anchorDragBoundFunc](/api/Konva.Transformer.html#anchorDragBoundFunc) method.
```js
transformer.anchorDragBoundFunc(function (oldAbsPos, newAbsPos, event) {
// limit any another position on the x axis
return {
x: 0,
y: newAbsolutePosition.y,
};
});
```
**Instructions: Try to resize a shape. You will see how transformer is trying to snap to guide lines.**
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
// create guides
const horizontalLine = new Konva.Line({
points: [0, height / 2, width, height / 2],
stroke: '#000',
strokeWidth: 1,
dash: [4, 4],
});
layer.add(horizontalLine);
const verticalLine = new Konva.Line({
points: [width / 2, 0, width / 2, height],
stroke: '#000',
strokeWidth: 1,
dash: [4, 4],
});
layer.add(verticalLine);
const rect = new Konva.Rect({
x: 60,
y: 60,
width: 100,
height: 100,
fill: 'red',
draggable: true,
});
layer.add(rect);
const tr = new Konva.Transformer({
nodes: [rect],
anchorDragBoundFunc: function (oldPos, newPos) {
const dist = Math.sqrt(Math.pow(newPos.x - width / 2, 2));
if (dist < 10) {
return {
...newPos,
x: width / 2,
};
}
return newPos;
},
});
layer.add(tr);
````
```js
import { Stage, Layer, Line, Rect, Transformer } from 'react-konva';
import { useRef, useEffect, useState } from 'react';
const App = () => {
const [rectPosition, setRectPosition] = useState({ x: 60, y: 60 });
const rectRef = useRef();
const trRef = useRef();
useEffect(() => {
trRef.current.nodes([rectRef.current]);
}, []);
return (
setRectPosition(e.target.position())}
onTransformEnd={(e) => setRectPosition(e.target.position())}
/>
{
const dist = Math.sqrt(Math.pow(newPos.x - window.innerWidth / 2, 2));
if (dist < 10) {
return {
...newPos,
x: window.innerWidth / 2,
};
}
return newPos;
}}
/>
);
};
export default App;
````
```js
```
---
# How to resize text on canvas?
> Learn how to resize text on HTML5 Canvas with Konva.js. Change font size when the Transformer is used to scale a Text shape.
Source: https://konvajs.org/docs/select_and_transform/Resize_Text.html
Remember, that `Konva.Transformer` is changing `scaleX` and `scaleY` properties of a node.
If you want to change width of the text, without changing its size, you should reset scale of a text back to 1 and adjust `width` accordingly.
You can use `transform` event to update text's properties as you need it.
**Instructions: Try to resize a text.**
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
const text = new Konva.Text({
x: 50,
y: 50,
text: 'Hello from Konva! Try to resize me.',
fontSize: 24,
draggable: true,
width: 200,
});
layer.add(text);
const tr = new Konva.Transformer({
nodes: [text],
// enable only one anchor to see better what is happening
// we are changing width only
enabledAnchors: ['middle-left', 'middle-right'],
});
layer.add(tr);
text.on('transform', function () {
// reset scale on transform
text.setAttrs({
width: text.width() * text.scaleX(),
scaleX: 1,
});
});
````
```js
import { Stage, Layer, Text, Transformer } from 'react-konva';
import { useRef, useEffect, useState } from 'react';
const App = () => {
const [textAttrs, setTextAttrs] = useState({
x: 50,
y: 50,
width: 200,
});
const textRef = useRef();
const trRef = useRef();
useEffect(() => {
trRef.current.nodes([textRef.current]);
}, []);
return (
{
setTextAttrs((attrs) => ({
...attrs,
...e.target.position(),
}));
}}
onTransform={() => {
const node = textRef.current;
const nextWidth = node.width() * node.scaleX();
node.scaleX(1);
setTextAttrs({
x: node.x(),
y: node.y(),
width: nextWidth,
});
}}
/>
);
};
export default App;
````
```js
```
---
# HTML5 Canvas Shape Snap Rotation
> Snap shape rotation to specific angles like 0, 90, 180, and 270 degrees using Konva Transformer rotationSnaps.
Source: https://konvajs.org/docs/select_and_transform/Rotation_Snaps.html
In some applications, you may want to snap rotation near some values. Snapping makes a shape "sticky" near provided values and works like rounding.
Most common snaps are 0, 45, 90, 135, 180, etc degrees. Snaps allow simpler setting of rotation to exactly these values.
For instance, if you have snap point at 45 deg, a user will not be able to set rotation to 43 deg. It will be rounded to 45 deg. But a user still will be able to set rotation to 35 deg, as it is too far from 45 so it will not be snapped.
**Instructions: Try to rotate a shape. See snapping at 0, 90, 180 and 270 deg.**
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
const rect = new Konva.Rect({
x: 50,
y: 50,
width: 100,
height: 50,
fill: 'yellow',
stroke: 'black',
draggable: true,
});
layer.add(rect);
const tr = new Konva.Transformer({
nodes: [rect],
rotationSnaps: [0, 90, 180, 270],
rotationSnapTolerance: 30,
});
layer.add(tr);
````
```js
import { Stage, Layer, Rect, Transformer } from 'react-konva';
import { useRef, useEffect, useState } from 'react';
const App = () => {
const [rectPosition, setRectPosition] = useState({ x: 50, y: 50 });
const rectRef = useRef();
const trRef = useRef();
useEffect(() => {
trRef.current.nodes([rectRef.current]);
}, []);
return (
setRectPosition(e.target.position())}
onTransformEnd={(e) => setRectPosition(e.target.position())}
/>
);
};
export default App;
````
```js
```
---
# HTML5 Canvas Stop Shape Transform
> Programmatically stop an active transform using the stopTransform method on Konva Transformer.
Source: https://konvajs.org/docs/select_and_transform/Stop_Transform.html
If you need to stop transforming immediately you can use `stopTransform` method of `Konva.Transformer` instance.
**Instructions: Try to resize a shape. If width of the shape is bigger than 200 transforming will be stopped.**
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
const rect = new Konva.Rect({
x: 50,
y: 50,
width: 100,
height: 100,
fill: 'yellow',
stroke: 'black',
draggable: true,
});
layer.add(rect);
const tr = new Konva.Transformer({
nodes: [rect],
});
layer.add(tr);
rect.on('transform', function () {
const width = rect.width() * rect.scaleX();
if (width > 200) {
tr.stopTransform();
}
});
````
```js
import { Stage, Layer, Rect, Transformer } from 'react-konva';
import { useRef, useEffect, useState } from 'react';
const App = () => {
const [rectPosition, setRectPosition] = useState({ x: 50, y: 50 });
const rectRef = useRef();
const trRef = useRef();
useEffect(() => {
trRef.current.nodes([rectRef.current]);
}, []);
return (
setRectPosition(e.target.position())}
onTransform={() => {
const node = rectRef.current;
const width = node.width() * node.scaleX();
if (width > 200) {
trRef.current.stopTransform();
}
}}
onTransformEnd={(e) => setRectPosition(e.target.position())}
/>
);
};
export default App;
````
```js
```
---
# HTML5 Canvas Transform and Resize events
> Learn how to listen to transform and resize events on HTML5 Canvas with Konva.js Transformer. Handle transformstart, transform, and transformend.
Source: https://konvajs.org/docs/select_and_transform/Transform_Events.html
`Konva.Transformer` object has special transform events that you can use in your app: `transformstart`, `transform` and `transformend`.
These events also will be triggered on attached node.
**Instructions: Open console, try to transform, see logs**
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
const rect = new Konva.Rect({
x: 50,
y: 50,
width: 100,
height: 100,
fill: 'yellow',
stroke: 'black',
draggable: true,
});
layer.add(rect);
const tr = new Konva.Transformer({
nodes: [rect],
});
layer.add(tr);
tr.on('transformstart', () => {
console.log('transform start');
});
tr.on('transform', () => {
console.log('transforming');
});
tr.on('transformend', () => {
console.log('transform end');
});
rect.on('transformstart', () => {
console.log('rect transform start');
});
rect.on('transform', () => {
console.log('rect transforming');
});
rect.on('transformend', () => {
console.log('rect transform end');
});
````
```js
import { Stage, Layer, Rect, Transformer } from 'react-konva';
import { useRef, useEffect, useState } from 'react';
const App = () => {
const [rectPosition, setRectPosition] = useState({ x: 50, y: 50 });
const rectRef = useRef();
const trRef = useRef();
useEffect(() => {
trRef.current.nodes([rectRef.current]);
}, []);
return (
setRectPosition(e.target.position())}
onTransformStart={() => console.log('rect transform start')}
onTransform={() => console.log('rect transforming')}
onTransformEnd={(e) => {
console.log('rect transform end');
setRectPosition(e.target.position());
}}
/>
console.log('transform start')}
onTransform={() => console.log('transforming')}
onTransformEnd={() => console.log('transform end')}
/>
);
};
export default App;
````
```js
console.log('rect transform start')"
@transform="() => console.log('rect transforming')"
@transformend="() => console.log('rect transform end')"
/>
console.log('transform start')"
@transform="() => console.log('transforming')"
@transformend="() => console.log('transform end')"
/>
```
---
# Deep Style Konva Transformer
> Use anchorStyleFunc for advanced per-anchor styling of Konva Transformer, including custom sizes and visibility.
Source: https://konvajs.org/docs/select_and_transform/Transformer_Complex_Styling.html
You can use `anchorStyleFunc` property of `Konva.Transformer` to have deeper control on styling of anchors.
Also take a look into [Transformer Styling](/docs/select_and_transform/Transformer_Styling.html) for simpler use cases.
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
const rect = new Konva.Rect({
x: 50,
y: 50,
width: 100,
height: 100,
fill: 'yellow',
stroke: 'black',
draggable: true,
});
layer.add(rect);
const tr = new Konva.Transformer({
nodes: [rect],
anchorStyleFunc: (anchor) => {
// make all anchors circles
anchor.cornerRadius(50);
// make all anchors red
anchor.fill('red');
// make right-middle bigger
if (anchor.hasName('middle-right')) {
anchor.scale({ x: 2, y: 2 });
}
// make top-left invisible
if (anchor.hasName('top-left')) {
anchor.scale({ x: 0, y: 0 });
}
},
});
layer.add(tr);
````
```js
import { Stage, Layer, Rect, Transformer } from 'react-konva';
import { useRef, useEffect, useState } from 'react';
const App = () => {
const [rectPosition, setRectPosition] = useState({ x: 50, y: 50 });
const rectRef = useRef();
const trRef = useRef();
useEffect(() => {
trRef.current.nodes([rectRef.current]);
}, []);
return (
setRectPosition(e.target.position())}
onTransformEnd={(e) => setRectPosition(e.target.position())}
/>
{
// make all anchors circles
anchor.cornerRadius(50);
// make all anchors red
anchor.fill('red');
// make right-middle bigger
if (anchor.hasName('middle-right')) {
anchor.scale({ x: 2, y: 2 });
}
// make top-left invisible
if (anchor.hasName('top-left')) {
anchor.scale({ x: 0, y: 0 });
}
}}
/>
);
};
export default App;
````
```js
```
---
# Style Konva Transformer
> Customize Konva Transformer appearance by changing anchor fill, stroke, size, border color, and corner radius.
Source: https://konvajs.org/docs/select_and_transform/Transformer_Styling.html
You can adjust styles of `Konva.Transformer` for your web app. You can change stroke, size and fill of all anchors.
Also you can change stroke color and size of border.
Also take a look into [Complex Transformer Styling](/docs/select_and_transform/Transformer_Complex_Styling.html) for fine tuning.
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
stage.add(layer);
const rect = new Konva.Rect({
x: 50,
y: 50,
width: 100,
height: 100,
fill: 'yellow',
stroke: 'black',
draggable: true,
});
layer.add(rect);
const tr = new Konva.Transformer({
nodes: [rect],
// add border
borderStroke: '#000',
borderStrokeWidth: 3,
// add anchors
anchorFill: '#fff',
anchorStroke: '#000',
anchorStrokeWidth: 2,
anchorSize: 20,
// make all anchors look like circles
anchorCornerRadius: 50,
});
layer.add(tr);
````
```js
import { Stage, Layer, Rect, Transformer } from 'react-konva';
import { useRef, useEffect, useState } from 'react';
const App = () => {
const [rectPosition, setRectPosition] = useState({ x: 50, y: 50 });
const rectRef = useRef();
const trRef = useRef();
useEffect(() => {
trRef.current.nodes([rectRef.current]);
}, []);
return (
setRectPosition(e.target.position())}
onTransformEnd={(e) => setRectPosition(e.target.position())}
/>
);
};
export default App;
````
```js
```
---
# HTML5 Canvas Select Shape by Name Tutorial
> Learn how to select and find shapes by name in Konva using the find() method with the dot (.) selector.
Source: https://konvajs.org/docs/selectors/Select_by_Name.html
To select shapes by name with Konva, we can use the `find()` method using the `.` selector.
The `find()` method returns an array of nodes that match the selector string.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// create shapes with names
const circle1 = new Konva.Circle({
x: 50,
y: stage.height() / 2,
radius: 30,
fill: 'red',
name: 'myCircle'
});
const circle2 = new Konva.Circle({
x: 150,
y: stage.height() / 2,
radius: 30,
fill: 'green',
name: 'myCircle'
});
const rect = new Konva.Rect({
x: 250,
y: stage.height() / 2 - 25,
width: 50,
height: 50,
fill: 'blue',
name: 'myRect'
});
layer.add(circle1);
layer.add(circle2);
layer.add(rect);
// find all circles by name
const circles = layer.find('.myCircle');
circles.forEach(circle => {
// add animation to circles only
circle.to({
duration: 1,
rotation: 360,
easing: Konva.Easings.EaseInOut
});
});
```
```js
import { Stage, Layer, Circle, Rect } from 'react-konva';
import { useEffect, useRef } from 'react';
const App = () => {
const layerRef = useRef(null);
useEffect(() => {
// find all circles by name and animate them
const circles = layerRef.current.find('.myCircle');
circles.forEach(circle => {
circle.to({
duration: 1,
rotation: 360,
easing: Konva.Easings.EaseInOut
});
});
}, []);
return (
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Select Shape by Type Tutorial
> Learn how to select and find shapes by their type or class name in Konva using the find() method.
Source: https://konvajs.org/docs/selectors/Select_by_Type.html
To select shapes by type with Konva, we can use the `find()` method with the name of the type or class name.
The `find()` method returns an array of nodes that match the selector string.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// create shapes of different types
const circle1 = new Konva.Circle({
x: 50,
y: stage.height() / 2,
radius: 30,
fill: 'red'
});
const circle2 = new Konva.Circle({
x: 150,
y: stage.height() / 2,
radius: 30,
fill: 'green'
});
const rect = new Konva.Rect({
x: 250,
y: stage.height() / 2 - 25,
width: 50,
height: 50,
fill: 'blue'
});
layer.add(circle1);
layer.add(circle2);
layer.add(rect);
// find all circles by type
const circles = layer.find('Circle');
circles.forEach(circle => {
// add animation to circles only
circle.to({
duration: 1,
scale: { x: 1.5, y: 1.5 },
easing: Konva.Easings.EaseInOut
});
});
```
```js
import { Stage, Layer, Circle, Rect } from 'react-konva';
import { useEffect, useRef } from 'react';
const App = () => {
const layerRef = useRef(null);
useEffect(() => {
// find all circles by type and animate them
const circles = layerRef.current.find('Circle');
circles.forEach(circle => {
circle.to({
duration: 1,
scale: { x: 1.5, y: 1.5 },
easing: Konva.Easings.EaseInOut
});
});
}, []);
return (
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Select Shape by id Tutorial
> Learn how to select and find shapes by id in Konva using the find() and findOne() methods with the hash (#) selector.
Source: https://konvajs.org/docs/selectors/Select_by_id.html
To select a shape by id with Konva, we can use the `find()` method using the # selector.
The `find()` method always returns an array of elements, even if we are expecting it to return one element.
if you need only one element you can use `findOne()` method.
The `find()` method works for any node, including the stage, layers, groups, and shapes.
**Instructions:** press the "Activate Rectangle" button to select the rectangle by id and perform a transition. You can also drag and drop the rectangle.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
// create a rectangle with id
const rect = new Konva.Rect({
x: stage.width() / 2 - 25,
y: stage.height() / 2 - 25,
width: 50,
height: 50,
fill: 'red',
id: 'myRect',
draggable: true
});
layer.add(rect);
// add button
const button = document.createElement('button');
button.textContent = 'Activate Rectangle';
document.body.appendChild(button);
button.addEventListener('click', () => {
// find rectangle by id and animate it
const rectangle = layer.findOne('#myRect');
rectangle.to({
duration: 1,
rotation: 360,
fill: 'blue',
easing: Konva.Easings.EaseInOut
});
});
```
```js
import { Stage, Layer, Rect } from 'react-konva';
import { useRef, useState } from 'react';
const App = () => {
const [position, setPosition] = useState({
x: window.innerWidth / 2 - 25,
y: window.innerHeight / 2 - 25
});
const layerRef = useRef(null);
const handleClick = () => {
// find rectangle by id and animate it
const rectangle = layerRef.current.findOne('#myRect');
rectangle.to({
duration: 1,
rotation: 360,
fill: 'blue',
easing: Konva.Easings.EaseInOut
});
};
const handleDragEnd = (e) => {
setPosition({
x: e.target.x(),
y: e.target.y()
});
};
return (
Activate Rectangle
);
};
export default App;
```
```js
Activate Rectangle
```
---
# HTML5 canvas Arc Tutorial
> Learn how to draw arcs on HTML5 Canvas with Konva.js. Set inner/outer radius, angle, and clockwise direction with the Konva.Arc shape.
Source: https://konvajs.org/docs/shapes/Arc.html
To create an arc shape with `Konva`, we can instantiate a `Konva.Arc()` object.
For full list of properties and methods, see the [Arc API Reference](/api/Konva.Arc.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const arc = new Konva.Arc({
x: stage.width() / 2,
y: stage.height() / 2,
innerRadius: 40,
outerRadius: 70,
angle: 60,
fill: 'yellow',
stroke: 'black',
strokeWidth: 4
});
layer.add(arc);
```
```js
import { Stage, Layer, Arc } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
```js
```
```js
```
```js
import { Component } from '@angular/core';
import { StageConfig } from 'konva/lib/Stage';
import { ArcConfig } from 'konva/lib/shapes/Arc';
import {
CoreShapeComponent,
// NgKonvaEventObject,
StageComponent,
} from 'ng2-konva';
@Component({
selector: 'app-root',
template: `
`,
imports: [StageComponent, CoreShapeComponent],
})
export default class App {
public configStage: StageConfig = {
width: window.innerWidth,
height: window.innerHeight,
};
public configArc: ArcConfig = {
x: window.innerWidth / 2,
y: window.innerHeight / 2,
innerRadius: 40,
outerRadius: 70,
angle: 60,
fill: 'yellow',
stroke: 'black',
strokeWidth: 4,
};
}
```
---
# HTML5 canvas Arrow Tutorial
> Learn how to draw arrows on HTML5 Canvas with Konva.js. Create arrows with customizable pointers, stroke, and fill using the Konva.Arrow shape.
Source: https://konvajs.org/docs/shapes/Arrow.html
To create an arrow shape with `Konva`, we can instantiate a `Konva.Arrow()` object.
For full list of properties and methods, see the [Arrow API Reference](/api/Konva.Arrow.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const arrow = new Konva.Arrow({
x: stage.width() / 4,
y: stage.height() / 4,
points: [0, 0, 100, 100],
pointerLength: 20,
pointerWidth: 20,
fill: 'black',
stroke: 'black',
strokeWidth: 4
});
layer.add(arrow);
```
```js
import { Stage, Layer, Arrow } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
```js
```
---
# HTML5 canvas Circle Tutorial
> Learn how to draw circles on HTML5 Canvas with Konva.js. Set radius, fill, stroke, opacity, and shadows with the Konva.Circle shape.
Source: https://konvajs.org/docs/shapes/Circle.html
To create a circle shape with `Konva`, we can instantiate a `Konva.Circle()` object.
For full list of properties and methods, see the [Circle API Reference](/api/Konva.Circle.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const circle = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4
});
layer.add(circle);
```
```js
import { Stage, Layer, Circle } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
```js
```
---
# HTML5 canvas Custom Shape Tutorial
> Learn how to draw custom shapes on HTML5 Canvas with Konva.js. Use the Konva.Shape sceneFunc to create any shape with the native Canvas 2D context.
Source: https://konvajs.org/docs/shapes/Custom.html
To create a custom shape with `Konva`, you can use the `Konva.Shape()` object and define a custom drawing function.
When creating a custom shape, you need to define a drawing function that is passed a [Konva.Context](/api/Konva.Context.html) renderer and a shape instance. Here's a simple rectangle example:
```javascript
const rect = new Konva.Shape({
x: 10,
y: 20,
fill: '#00D2FF',
width: 100,
height: 50,
sceneFunc: function (context, shape) {
context.beginPath();
// don't need to set position of rect, Konva will handle it
context.rect(0, 0, shape.getAttr('width'), shape.getAttr('height'));
// (!) Konva specific method, it is very important
// it will apply all required styles
context.fillStrokeShape(shape);
}
});
```
`Konva.Context` is a wrapper around native 2d canvas context that has the same properties and methods with some additional API.
There are two properties that can be used for drawing custom shapes:
- `sceneFunc` - defines visual appearance of a shape
- `hitFunc` - optional function to define custom hit region for events (see [Custom Hit Region demo](/docs/events/Custom_Hit_Region.html))
### Best practices for writing `sceneFunc` and `hitFunc`:
1. Optimize the function as it can be called many times per second. Avoid creating images or large objects.
2. The function should not have side effects like moving shapes, attaching events or changing app state.
3. Define custom `hitFunc` when applying complex styles or drawing images.
4. Don't manually apply position and scaling in `sceneFunc`. Let Konva handle it through shape properties.
5. Avoid manual styling in `sceneFunc`. Use `context.fillStrokeShape(shape)` for styling.
6. Reference [Konva core shapes implementations](https://github.com/konvajs/konva/tree/master/src/shapes) for more examples.
For full list of properties and methods, see the [Shape API Reference](/api/Konva.Shape.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const triangle = new Konva.Shape({
sceneFunc: function (context, shape) {
context.beginPath();
context.moveTo(20, 50);
context.lineTo(220, 80);
context.lineTo(100, 150);
context.closePath();
context.fillStrokeShape(shape);
},
fill: '#00D2FF',
stroke: 'black',
strokeWidth: 4
});
layer.add(triangle);
```
```js
import { Stage, Layer, Shape } from 'react-konva';
const App = () => {
return (
{
context.beginPath();
context.moveTo(20, 50);
context.lineTo(220, 80);
context.lineTo(100, 150);
context.closePath();
context.fillStrokeShape(shape);
}}
fill="#00D2FF"
stroke="black"
strokeWidth={4}
/>
);
};
export default App;
```
```js
```
---
# HTML5 canvas Ellipse Tutorial
> Learn how to draw ellipses on HTML5 Canvas with Konva.js. Set radiusX, radiusY, fill, stroke, and more with the Konva.Ellipse shape.
Source: https://konvajs.org/docs/shapes/Ellipse.html
To create an ellipse shape with `Konva`, we can instantiate a `Konva.Ellipse()` object.
For full list of properties and methods, see the [Ellipse API Reference](/api/Konva.Ellipse.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const ellipse = new Konva.Ellipse({
x: stage.width() / 2,
y: stage.height() / 2,
radiusX: 100,
radiusY: 50,
fill: 'yellow',
stroke: 'black',
strokeWidth: 4
});
layer.add(ellipse);
```
```js
import { Stage, Layer, Ellipse } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
```js
```
---
# HTML5 canvas Group Tutorial
> Learn how to group shapes on HTML5 Canvas with Konva.js. Use Konva.Group to move, rotate, and scale multiple shapes together.
Source: https://konvajs.org/docs/shapes/Group.html
To create a group of shapes with `Konva`, we can instantiate a `Konva.Group()` object.
For full list of properties and methods, see the [Group API Reference](/api/Konva.Group.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const group = new Konva.Group({
x: 50,
y: 50,
draggable: true
});
const circle = new Konva.Circle({
x: 0,
y: 0,
radius: 30,
fill: 'red'
});
const rect = new Konva.Rect({
x: 20,
y: 20,
width: 100,
height: 50,
fill: 'green'
});
group.add(circle);
group.add(rect);
layer.add(group);
```
```js
import { Stage, Layer, Group, Circle, Rect } from 'react-konva';
import { useState } from 'react';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
return (
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
>
);
};
export default App;
```
```js
```
---
# HTML5 canvas Image Tutorial
> Learn how to draw images on HTML5 Canvas with Konva.js. Load images from URLs, crop, resize, and apply filters with the Konva.Image shape.
Source: https://konvajs.org/docs/shapes/Image.html
To create an image with `Konva`, you can use the `Konva.Image()` object.
For full list of properties and methods, see the [Image API Reference](/api/Konva.Image.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
// main API:
const imageObj = new Image();
imageObj.onload = function () {
const yoda = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
width: 106,
height: 118
});
layer.add(yoda);
};
imageObj.src = 'https://konvajs.org/assets/yoda.jpg';
// alternative API:
Konva.Image.fromURL('https://konvajs.org/assets/darth-vader.jpg', function (darthNode) {
darthNode.setAttrs({
x: 200,
y: 50,
scaleX: 0.5,
scaleY: 0.5,
cornerRadius: 20
});
layer.add(darthNode);
});
```
```js
import { Stage, Layer, Image } from 'react-konva';
import { useEffect, useState } from 'react';
import useImage from 'use-image';
const App = () => {
const [yodaImage] = useImage('https://konvajs.org/assets/yoda.jpg');
const [vaderImage] = useImage('https://konvajs.org/assets/darth-vader.jpg');
return (
);
};
export default App;
```
```js
```
---
# HTML5 canvas Label Tutorial
> Learn how to draw labels with text and tags on HTML5 Canvas with Konva.js. Create tooltips, callouts, and annotations using the Konva.Label shape.
Source: https://konvajs.org/docs/shapes/Label.html
To create a label with `Konva`, you can use the `Konva.Label()` object.
For full list of properties and methods, see the [Label API Reference](/api/Konva.Label.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
// tooltip pointing down
const tooltip = new Konva.Label({
x: 170,
y: 75,
opacity: 0.75
});
tooltip.add(
new Konva.Tag({
fill: 'black',
pointerDirection: 'down',
pointerWidth: 10,
pointerHeight: 10,
lineJoin: 'round',
shadowColor: 'black',
shadowBlur: 10,
shadowOffsetX: 10,
shadowOffsetY: 10,
shadowOpacity: 0.5
})
);
tooltip.add(
new Konva.Text({
text: 'Tooltip pointing down',
fontFamily: 'Calibri',
fontSize: 18,
padding: 5,
fill: 'white'
})
);
// label pointing left
const labelLeft = new Konva.Label({
x: 20,
y: 130,
opacity: 0.75
});
labelLeft.add(
new Konva.Tag({
fill: 'green',
pointerDirection: 'left',
pointerWidth: 20,
pointerHeight: 28,
lineJoin: 'round'
})
);
labelLeft.add(
new Konva.Text({
text: 'Label pointing left',
fontFamily: 'Calibri',
fontSize: 18,
padding: 5,
fill: 'white'
})
);
// simple label
const simpleLabel = new Konva.Label({
x: 180,
y: 150,
opacity: 0.75
});
simpleLabel.add(
new Konva.Tag({
fill: 'yellow'
})
);
simpleLabel.add(
new Konva.Text({
text: 'Simple label',
fontFamily: 'Calibri',
fontSize: 18,
padding: 5,
fill: 'black'
})
);
layer.add(tooltip).add(labelLeft).add(simpleLabel);
stage.add(layer);
```
```js
import { Stage, Layer, Label, Tag, Text } from 'react-konva';
const App = () => {
return (
{/* tooltip pointing down */}
{/* label pointing left */}
{/* simple label */}
);
};
export default App;
```
```js
```
---
# HTML5 canvas Line Tutorial
> Learn how to draw lines on HTML5 Canvas with Konva.js. Create simple lines, splines, polygons, and blobs with the Konva.Line shape.
Source: https://konvajs.org/docs/shapes/Line.html
To create line shapes with `Konva`, we can instantiate a `Konva.Line()` object. Lines can be configured in different ways to create various shapes like simple lines, splines, blobs, and polygons.
For full list of properties and methods, see the [Line API Reference](/api/Konva.Line.html).
## Simple Line
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const redLine = new Konva.Line({
points: [5, 70, 140, 23, 250, 60, 300, 20],
stroke: 'red',
strokeWidth: 15,
lineCap: 'round',
lineJoin: 'round'
});
// dashed line
const greenLine = new Konva.Line({
points: [5, 70, 140, 23, 250, 60, 300, 20],
stroke: 'green',
strokeWidth: 2,
lineJoin: 'round',
dash: [33, 10]
});
greenLine.y(50);
layer.add(redLine, greenLine);
```
```js
import { Stage, Layer, Line } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
```js
```
## Spline (Curved Line)
To create a curved line, add the `tension` property:
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const line = new Konva.Line({
points: [5, 70, 140, 23, 250, 60, 300, 20],
stroke: 'red',
strokeWidth: 15,
lineCap: 'round',
lineJoin: 'round',
tension: 1
});
layer.add(line);
```
```js
import { Stage, Layer, Line } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
```js
```
## Polygon
To create a polygon, set the `closed` property to true:
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const polygon = new Konva.Line({
points: [73, 192, 73, 160, 340, 23, 500, 109, 499, 139, 342, 93],
fill: '#00D2FF',
stroke: 'black',
strokeWidth: 5,
closed: true
});
layer.add(polygon);
```
```js
import { Stage, Layer, Line } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
```js
```
## Blob
To create a blob, combine `closed` and `tension` properties:
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const blob = new Konva.Line({
points: [23, 20, 23, 160, 70, 93, 150, 109, 290, 139, 270, 93],
fill: '#00D2FF',
stroke: 'black',
strokeWidth: 5,
closed: true,
tension: 0.3
});
layer.add(blob);
```
```js
import { Stage, Layer, Line } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
```js
```
---
# HTML5 canvas Blob Tutorial
> Learn how to draw blob shapes on the HTML5 canvas with Konva using Konva.Line with closed and tension properties.
Source: https://konvajs.org/docs/shapes/Line_-_Blob.html
To create a blob with `Konva`, we can instantiate a `Konva.Line()` object with both `closed` and `tension` properties.
For full list of properties and methods, see the [Line API Reference](/api/Konva.Line.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const blob = new Konva.Line({
points: [23, 20, 23, 160, 70, 93, 150, 109, 290, 139, 270, 93],
fill: '#00D2FF',
stroke: 'black',
strokeWidth: 5,
closed: true,
tension: 0.3
});
layer.add(blob);
```
```js
import { Stage, Layer, Line } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
```js
```
---
# HTML5 canvas Polygon Tutorial
> Learn how to draw polygon shapes on the HTML5 canvas with Konva using Konva.Line with the closed property set to true.
Source: https://konvajs.org/docs/shapes/Line_-_Polygon.html
To create a polygon with `Konva`, we can instantiate a `Konva.Line()` object with the `closed` property set to `true`.
For full list of properties and methods, see the [Line API Reference](/api/Konva.Line.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const polygon = new Konva.Line({
points: [73, 192, 73, 160, 340, 23, 500, 109, 499, 139, 342, 93],
fill: '#00D2FF',
stroke: 'black',
strokeWidth: 5,
closed: true
});
layer.add(polygon);
```
```js
import { Stage, Layer, Line } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Simple, Dashed and Dotted Lines
> Learn how to draw simple lines, dashed lines, and dotted lines on the HTML5 canvas using Konva.Line.
Source: https://konvajs.org/docs/shapes/Line_-_Simple_Line.html
To create a simple line with `Konva`, we can instantiate a `Konva.Line()` object.
For full list of properties and methods, see the [Line API Reference](/api/Konva.Line.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const redLine = new Konva.Line({
points: [5, 70, 140, 23, 250, 60, 300, 20],
stroke: 'red',
strokeWidth: 15,
lineCap: 'round',
lineJoin: 'round'
});
// line segments with a length of 33px with a gap of 10px
const greenLine = new Konva.Line({
points: [5, 70, 140, 23, 250, 60, 300, 20],
stroke: 'green',
strokeWidth: 2,
lineJoin: 'round',
dash: [33, 10]
});
// line segments with a length of 29px with a gap of 20px
// followed by a dot (0.001px) and another gap of 20px
const blueLine = new Konva.Line({
points: [5, 70, 140, 23, 250, 60, 300, 20],
stroke: 'blue',
strokeWidth: 10,
lineCap: 'round',
lineJoin: 'round',
dash: [29, 20, 0.001, 20]
});
redLine.move({ x: 0, y: 5 });
greenLine.move({ x: 0, y: 55 });
blueLine.move({ x: 0, y: 105 });
layer.add(redLine, greenLine, blueLine);
```
```js
import { Stage, Layer, Line } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
```js
```
---
# HTML5 canvas Spline Tutorial
> Learn how to draw smooth curved spline lines on the HTML5 canvas with Konva using Konva.Line with the tension property.
Source: https://konvajs.org/docs/shapes/Line_-_Spline.html
To create a spline (curved line) with `Konva`, we can instantiate a `Konva.Line()` object with the `tension` property.
For full list of properties and methods, see the [Line API Reference](/api/Konva.Line.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const line = new Konva.Line({
points: [5, 70, 140, 23, 250, 60, 300, 20],
stroke: 'red',
strokeWidth: 15,
lineCap: 'round',
lineJoin: 'round',
tension: 1
});
layer.add(line);
```
```js
import { Stage, Layer, Line } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
```js
```
---
# HTML5 canvas Path Tutorial
> Learn how to draw custom paths on HTML5 Canvas with Konva.js using SVG path data. Create complex shapes with the Konva.Path shape.
Source: https://konvajs.org/docs/shapes/Path.html
To create a custom path shape with `Konva`, we can instantiate a `Konva.Path()` object.
For full list of properties and methods, see the [Path API Reference](/api/Konva.Path.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const path = new Konva.Path({
x: 50,
y: 50,
data: 'M12.582,9.551C3.251,16.237,0.921,29.021,7.08,38.564l-2.36,1.689l4.893,2.262l4.893,2.262l-0.568-5.36l-0.567-5.359l-2.365,1.694c-4.657-7.375-2.83-17.185,4.352-22.33c7.451-5.338,17.817-3.625,23.156,3.824c5.337,7.449,3.625,17.813-3.821,23.152l2.857,3.988c9.617-6.893,11.827-20.277,4.935-29.896C35.591,4.87,22.204,2.658,12.582,9.551z',
fill: 'green',
scale: {
x: 2,
y: 2
}
});
layer.add(path);
```
```js
import { Stage, Layer, Path } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
```js
```
---
# HTML5 canvas Regular Polygon Tutorial
> Learn how to draw regular polygons (triangles, pentagons, hexagons) on HTML5 Canvas with Konva.js using the Konva.RegularPolygon shape.
Source: https://konvajs.org/docs/shapes/RegularPolygon.html
To create a regular polygon shape with `Konva`, we can instantiate a `Konva.RegularPolygon()` object.
For full list of properties and methods, see the [RegularPolygon API Reference](/api/Konva.RegularPolygon.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const hexagon = new Konva.RegularPolygon({
x: stage.width() / 2,
y: stage.height() / 2,
sides: 6,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4
});
layer.add(hexagon);
```
```js
import { Stage, Layer, RegularPolygon } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
```js
```
---
# HTML5 canvas Ring Tutorial
> Learn how to draw rings (donuts) on HTML5 Canvas with Konva.js. Set inner and outer radius with the Konva.Ring shape.
Source: https://konvajs.org/docs/shapes/Ring.html
To create a ring with `Konva`, you can use the `Konva.Ring()` object.
For full list of properties and methods, see the [Ring API Reference](/api/Konva.Ring.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const ring = new Konva.Ring({
x: stage.width() / 2,
y: stage.height() / 2,
innerRadius: 40,
outerRadius: 70,
fill: 'yellow',
stroke: 'black',
strokeWidth: 4
});
layer.add(ring);
```
```js
import { Stage, Layer, Ring } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
```js
```
---
# HTML5 canvas Sprite Tutorial
> Learn how to use sprite sheets for animation on HTML5 Canvas with Konva.js. Play frame-based animations using the Konva.Sprite shape.
Source: https://konvajs.org/docs/shapes/Sprite.html
To create an animated sprite with `Konva`, we can instantiate a `Konva.Sprite()` object.
For full list of properties and methods, see the [Sprite API Reference](/api/Konva.Sprite.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const animations = {
idle: [
2, 2, 70, 119, // frame 1
71, 2, 74, 119, // frame 2
146, 2, 81, 119, // frame 3
226, 2, 76, 119, // frame 4
],
punch: [
2, 138, 74, 122, // frame 1
76, 138, 84, 122, // frame 2
346, 138, 120, 122, // frame 3
],
};
const imageObj = new Image();
imageObj.onload = function() {
const sprite = new Konva.Sprite({
x: 50,
y: 50,
image: imageObj,
animation: 'idle',
animations: animations,
frameRate: 7,
frameIndex: 0
});
layer.add(sprite);
sprite.start();
// Add punch button functionality
const button = document.createElement('button');
button.textContent = 'Punch';
button.style.position = 'absolute';
button.style.top = '0';
button.style.left = '0';
document.body.appendChild(button);
button.addEventListener('click', () => {
sprite.animation('punch');
sprite.on('frameIndexChange.button', function() {
if (this.frameIndex() === 2) {
setTimeout(() => {
sprite.animation('idle');
sprite.off('.button');
}, 1000 / sprite.frameRate());
}
});
});
};
imageObj.src = 'https://konvajs.org/assets/blob-sprite.png';
```
```js
import { Stage, Layer, Sprite } from 'react-konva';
import { useEffect, useRef } from 'react';
import useImage from 'use-image';
const App = () => {
const spriteRef = useRef(null);
const [image] = useImage('https://konvajs.org/assets/blob-sprite.png');
const animations = {
idle: [
2, 2, 70, 119, // frame 1
71, 2, 74, 119, // frame 2
146, 2, 81, 119, // frame 3
226, 2, 76, 119, // frame 4
],
punch: [
2, 138, 74, 122, // frame 1
76, 138, 84, 122, // frame 2
346, 138, 120, 122, // frame 3
],
};
useEffect(() => {
if (spriteRef.current) {
spriteRef.current.start();
}
}, [image]);
const handlePunch = () => {
if (spriteRef.current) {
const sprite = spriteRef.current;
sprite.animation('punch');
sprite.on('frameIndexChange.button', function() {
if (this.frameIndex() === 2) {
setTimeout(() => {
sprite.animation('idle');
sprite.off('.button');
}, 1000 / sprite.frameRate());
}
});
}
};
return (
<>
Punch
>
);
};
export default App;
```
```js
Punch
```
---
# HTML5 canvas Star Tutorial
> Learn how to draw stars on HTML5 Canvas with Konva.js. Set inner/outer radius, number of points, fill, and stroke with the Konva.Star shape.
Source: https://konvajs.org/docs/shapes/Star.html
To create a star with `Konva`, you can use the `Konva.Star()` object.
For full list of properties and methods, see the [Star API Reference](/api/Konva.Star.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const star = new Konva.Star({
x: stage.width() / 2,
y: stage.height() / 2,
numPoints: 5,
innerRadius: 30,
outerRadius: 70,
fill: 'yellow',
stroke: 'black',
strokeWidth: 4
});
layer.add(star);
```
```js
import { Stage, Layer, Star } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
```js
```
---
# HTML5 canvas Text Tutorial
> Learn how to draw text on HTML5 Canvas with Konva.js. Set font family, size, style, alignment, wrapping, padding, and decoration with the Konva.Text shape.
Source: https://konvajs.org/docs/shapes/Text.html
Canvas has no text elements. The browser gives you `fillText()`, which draws a
string at a point and forgets it — no wrapping, no alignment, no way to ask how
wide it was. `Konva.Text` is a shape, so the text stays a node you can move,
style, measure, and hit-test.
```js
const text = new Konva.Text({
x: 20,
y: 20,
text: 'Hello Konva',
fontSize: 24,
fontFamily: 'Arial',
fill: 'black',
});
```
Set `width` and the text wraps to it. Leave `width` unset and the shape sizes
itself to the content.
For the full list of properties and methods, see the
[Text API Reference](/api/Konva.Text.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
// Simple text
const simpleText = new Konva.Text({
x: stage.width() / 2,
y: 15,
text: 'Simple Text',
fontSize: 30,
fontFamily: 'Calibri',
fill: 'green'
});
simpleText.offsetX(simpleText.width() / 2);
// Complex text with background
const complexText = new Konva.Text({
x: 20,
y: 60,
text: "COMPLEX TEXT\n\nAll the world's a stage, and all the men and women merely players. They have their exits and their entrances.",
fontSize: 18,
fontFamily: 'Calibri',
fill: '#555',
width: 300,
padding: 20,
align: 'center'
});
const rect = new Konva.Rect({
x: 20,
y: 60,
stroke: '#555',
strokeWidth: 5,
fill: '#ddd',
width: 300,
height: complexText.height(),
shadowColor: 'black',
shadowBlur: 10,
shadowOffsetX: 10,
shadowOffsetY: 10,
shadowOpacity: 0.2,
cornerRadius: 10
});
layer.add(rect);
layer.add(simpleText);
layer.add(complexText);
```
```js
import { Stage, Layer, Text, Rect } from 'react-konva';
const text = `COMPLEX TEXT
All the world's a stage, and all the men and women merely players. They have their exits and their entrances.`;
const App = () => {
return (
);
};
export default App;
```
```js
```
## Measuring text
A `Konva.Text` measures itself as soon as it exists, so you can read its size
before it is on a layer.
```js
const text = new Konva.Text({ text: 'Hello Konva', fontSize: 24 });
text.width(); // full shape width, including padding
text.height(); // full shape height, all lines, including padding
text.getTextWidth(); // width of the widest line, excluding padding
text.fontSize(); // height of a single line
```
Use these instead of estimating. Centring by hand is the usual reason people
guess:
```js
text.offsetX(text.width() / 2); // exact
```
Two things to know:
- **`getTextHeight()` is deprecated.** It warns in the console. Use `height()`
for the whole shape and `fontSize()` for one line.
- **`measureSize(string)` measures a string you have not drawn**, using this
shape's font. It is useful for sizing something before you commit to it, and
it cannot handle multiline text.
```js
const { width } = text.measureSize('Some other string');
```
In React and Vue, read the same values through a ref to the node rather than
approximating them — or avoid measurement entirely by giving the text a `width`
and an `align`, as the demo above does.
## Wrapping and ellipsis
Text wraps only when it has a `width`.
```js
new Konva.Text({
text: 'A long line that will not fit on one row',
width: 200, // wrapping requires this
wrap: 'word', // 'word' (default), 'char', or 'none'
ellipsis: true, // needs a height too, or there is nothing to overflow
});
```
`ellipsis` truncates with `…` when the text does not fit the box. It only has an
effect when both `width` and `height` are set, since without a height the shape
grows to fit and nothing ever overflows.
```js
import Konva from 'konva';
const stage = new Konva.Stage({ container: 'container', width: 500, height: 260 });
const layer = new Konva.Layer();
stage.add(layer);
const sample = 'Konva.Text wraps to the width you give it, and can truncate when it runs out of room.';
[
{ y: 10, label: "wrap: 'word' (default)", config: { width: 220 } },
{ y: 95, label: "wrap: 'char'", config: { width: 220, wrap: 'char' } },
{ y: 180, label: 'ellipsis with a fixed height', config: { width: 220, height: 44, ellipsis: true } },
].forEach(({ y, label, config }) => {
layer.add(new Konva.Text({ x: 250, y, text: label, fontSize: 13, fill: '#666' }));
layer.add(
new Konva.Rect({ x: 20, y, width: 220, height: config.height || 70, stroke: '#ddd' })
);
layer.add(new Konva.Text({ x: 20, y, text: sample, fontSize: 14, padding: 4, ...config }));
});
```
## Text and web fonts
This is the most common text bug, and it is not a Konva bug.
A DOM element re-lays-out by itself when a web font finishes loading. Canvas
does not. If the text was created before the font arrived, it was measured with
the fallback font, and that stale measurement is what wrapping, centring, and
`width()` are still based on. The text usually looks right and sits in the
wrong place.
Wait for the font, then force a re-measure:
```js
await document.fonts.load('16px "Roboto"');
// Re-setting any measured attribute re-runs the measurement.
text.fontFamily('Roboto');
```
Konva re-measures whenever one of its text-affecting attributes changes:
`text`, `fontFamily`, `fontSize`, `fontStyle`, `fontVariant`, `lineHeight`,
`letterSpacing`, `align`, `verticalAlign`, `padding`, `width`, `height`,
`wrap`, `ellipsis`, and `direction`. Setting one of those to the value it
already has is enough.
There is a complete example, including loading the font itself, in
[Custom Font](/docs/sandbox/Custom_Font.html).
---
# HTML5 canvas TextPath Tutorial
> Learn how to draw text along a path on HTML5 Canvas with Konva.js. Render curved or circular text using the Konva.TextPath shape.
Source: https://konvajs.org/docs/shapes/TextPath.html
To create text that follows a path with `Konva`, we can instantiate a `Konva.TextPath()` object.
For full list of properties and methods, see the [TextPath API Reference](/api/Konva.TextPath.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const textPath = new Konva.TextPath({
x: 0,
y: 50,
fill: '#333',
fontSize: 16,
fontFamily: 'Arial',
text: 'All the world\'s a stage, and all the men and women merely players.',
data: 'M10,10 C0,0 10,150 100,100 S300,150 400,50',
});
layer.add(textPath);
```
```js
import { Stage, Layer, TextPath } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
```js
```
---
# HTML5 canvas Wedge Tutorial
> Learn how to draw wedges (pie slices) on HTML5 Canvas with Konva.js. Set angle, radius, and rotation with the Konva.Wedge shape.
Source: https://konvajs.org/docs/shapes/Wedge.html
To create a wedge (pie piece) shape with `Konva`, we can instantiate a `Konva.Wedge()` object.
For full list of properties and methods, see the [Wedge API Reference](/api/Konva.Wedge.html).
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const wedge = new Konva.Wedge({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 70,
angle: 60,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
rotation: -120
});
layer.add(wedge);
```
```js
import { Stage, Layer, Wedge } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Blend mode with globalCompositeOperation Tutorial
> Learn how to use globalCompositeOperation for blend modes like XOR on HTML5 Canvas shapes using Konva.js.
Source: https://konvajs.org/docs/styling/Blend_Mode.html
[globalCompositeOperation Documentation](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation).
With Konva framework you can set globalCompositeOperation or blending mode operations with `globalCompositeOperation` property.
**Instructions**: Drag the red rectangle over the green text to see the XOR blending effect.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
const text = new Konva.Text({
text: 'Text Shadow!',
fontFamily: 'Calibri',
fontSize: 40,
x: 20,
y: 20,
fill: 'green',
shadowColor: 'white',
shadowOffset: { x: 10, y: 10 }
});
layer.add(text);
const rect = new Konva.Rect({
x: 50,
y: 50,
width: 100,
height: 100,
fill: 'red',
draggable: true,
globalCompositeOperation: 'xor'
});
layer.add(rect);
stage.add(layer);
```
```js
import { Stage, Layer, Text, Rect } from 'react-konva';
import { useState } from 'react';
const App = () => {
const [position, setPosition] = useState({ x: 50, y: 50 });
return (
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
globalCompositeOperation="xor"
/>
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Set Fill Tutorial
> Learn how to fill shapes with colors, patterns, linear gradients, and radial gradients using Konva.js on HTML5 Canvas.
Source: https://konvajs.org/docs/styling/Fill.html
To fill a shape with Konva, we can set the fill property when we instantiate a shape, or we can use the `fill()` method.
Konva supports colors, patterns, linear gradients, and radial gradients.
Instructions: Mouseover each pentagon to change its fill. You can also drag and drop the shapes.
```js
import Konva from 'konva';
function loadImages(sources, callback) {
var images = {};
var loadedImages = 0;
var numImages = 0;
// get num of sources
for (var src in sources) {
numImages++;
}
for (var src in sources) {
images[src] = new Image();
images[src].onload = function () {
if (++loadedImages >= numImages) {
callback(images);
}
};
images[src].src = sources[src];
}
}
function draw(images) {
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
var layer = new Konva.Layer();
var colorPentagon = new Konva.RegularPolygon({
x: 80,
y: stage.height() / 2,
sides: 5,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
draggable: true,
});
var patternPentagon = new Konva.RegularPolygon({
x: 220,
y: stage.height() / 2,
sides: 5,
radius: 70,
fillPatternImage: images.darthVader,
fillPatternOffset: { x: -220, y: 70 },
stroke: 'black',
strokeWidth: 4,
draggable: true,
});
var linearGradPentagon = new Konva.RegularPolygon({
x: 360,
y: stage.height() / 2,
sides: 5,
radius: 70,
fillLinearGradientStartPoint: { x: -50, y: -50 },
fillLinearGradientEndPoint: { x: 50, y: 50 },
fillLinearGradientColorStops: [0, 'red', 1, 'yellow'],
stroke: 'black',
strokeWidth: 4,
draggable: true,
});
var radialGradPentagon = new Konva.RegularPolygon({
x: 500,
y: stage.height() / 2,
sides: 5,
radius: 70,
fillRadialGradientStartPoint: { x: 0, y: 0 },
fillRadialGradientStartRadius: 0,
fillRadialGradientEndPoint: { x: 0, y: 0 },
fillRadialGradientEndRadius: 70,
fillRadialGradientColorStops: [0, 'red', 0.5, 'yellow', 1, 'blue'],
stroke: 'black',
strokeWidth: 4,
draggable: true,
});
/*
* bind listeners
*/
colorPentagon.on('mouseover touchstart', function () {
this.fill('blue');
});
colorPentagon.on('mouseout touchend', function () {
this.fill('red');
});
patternPentagon.on('mouseover touchstart', function () {
this.fillPatternImage(images.yoda);
this.fillPatternOffset({ x: -100, y: 70 });
});
patternPentagon.on('mouseout touchend', function () {
this.fillPatternImage(images.darthVader);
this.fillPatternOffset({ x: -220, y: 70 });
});
linearGradPentagon.on('mouseover touchstart', function () {
this.fillLinearGradientStartPoint({ x: -50 });
this.fillLinearGradientEndPoint({ x: 50 });
this.fillLinearGradientColorStops([0, 'green', 1, 'yellow']);
});
linearGradPentagon.on('mouseout touchend', function () {
// set multiple properties at once with setAttrs
this.setAttrs({
fillLinearGradientStartPoint: { x: -50, y: -50 },
fillLinearGradientEndPoint: { x: 50, y: 50 },
fillLinearGradientColorStops: [0, 'red', 1, 'yellow'],
});
});
radialGradPentagon.on('mouseover touchstart', function () {
this.fillRadialGradientColorStops([
0,
'red',
0.5,
'yellow',
1,
'green',
]);
});
radialGradPentagon.on('mouseout touchend', function () {
// set multiple properties at once with setAttrs
this.setAttrs({
fillRadialGradientStartPoint: 0,
fillRadialGradientStartRadius: 0,
fillRadialGradientEndPoint: 0,
fillRadialGradientEndRadius: 70,
fillRadialGradientColorStops: [0, 'red', 0.5, 'yellow', 1, 'blue'],
});
});
layer.add(colorPentagon);
layer.add(patternPentagon);
layer.add(linearGradPentagon);
layer.add(radialGradPentagon);
stage.add(layer);
}
var sources = {
darthVader: 'https://konvajs.org/assets/darth-vader.jpg',
yoda: 'https://konvajs.org/assets/yoda.jpg',
};
loadImages(sources, function (images) {
draw(images);
});
```
```jsx
import React from 'react';
import { Stage, Layer, RegularPolygon } from 'react-konva';
import useImage from 'use-image';
const commonProps = {
sides: 5,
radius: 70,
stroke: 'black',
strokeWidth: 4,
draggable: true,
};
const ColorPolygon = () => {
const [fill, setFill] = React.useState('red');
const [position, setPosition] = React.useState({
x: 80,
y: window.innerHeight / 2,
});
const handleEnter = (e) => {
setFill('blue');
e.target.getStage().container().style.cursor = 'pointer';
};
const handleLeave = (e) => {
setFill('red');
e.target.getStage().container().style.cursor = 'default';
};
return (
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
/>
);
};
const PatternPolygon = () => {
const [darthVader] = useImage('https://konvajs.org/assets/darth-vader.jpg');
const [yoda] = useImage('https://konvajs.org/assets/yoda.jpg');
const [image, setImage] = React.useState(null);
const [offset, setOffset] = React.useState({ x: -220, y: 70 });
const [position, setPosition] = React.useState({
x: 220,
y: window.innerHeight / 2,
});
React.useEffect(() => {
if (darthVader) {
setImage(darthVader);
}
}, [darthVader]);
const handleEnter = (e) => {
setImage(yoda);
setOffset({ x: -100, y: 70 });
e.target.getStage().container().style.cursor = 'pointer';
};
const handleLeave = (e) => {
setImage(darthVader);
setOffset({ x: -220, y: 70 });
e.target.getStage().container().style.cursor = 'default';
};
return (
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
/>
);
};
const LinearGradientPolygon = () => {
const [colorStops, setColorStops] = React.useState([0, 'red', 1, 'yellow']);
const [position, setPosition] = React.useState({
x: 360,
y: window.innerHeight / 2,
});
const handleEnter = (e) => {
setColorStops([0, 'green', 1, 'yellow']);
e.target.getStage().container().style.cursor = 'pointer';
};
const handleLeave = (e) => {
setColorStops([0, 'red', 1, 'yellow']);
e.target.getStage().container().style.cursor = 'default';
};
return (
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
/>
);
};
const RadialGradientPolygon = () => {
const [colorStops, setColorStops] = React.useState([0, 'red', 0.5, 'yellow', 1, 'blue']);
const [position, setPosition] = React.useState({
x: 500,
y: window.innerHeight / 2,
});
const handleEnter = (e) => {
setColorStops([0, 'red', 0.5, 'yellow', 1, 'green']);
e.target.getStage().container().style.cursor = 'pointer';
};
const handleLeave = (e) => {
setColorStops([0, 'red', 0.5, 'yellow', 1, 'blue']);
e.target.getStage().container().style.cursor = 'default';
};
return (
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
/>
);
};
const App = () => {
return (
);
};
export default App;
```
```html
```
---
# Fill and stroke order demo
> Learn how to control fill and stroke rendering order in Konva.js using the fillAfterStrokeEnabled property.
Source: https://konvajs.org/docs/styling/Fill_Stroke_Order.html
If a shape has both fill and stroke, by default, `Konva` will draw filling first then stroke on top of it. That is the best behavior for most of the applications.
## How to draw fill part on top of the stroke?
In some rare cases you may need a shape that has stroke first, then a fill on top of it. For that use case you may use [fillAfterStrokeEnabled](https://konvajs.org/api/Konva.Shape.html#fillAfterStrokeEnabled) property.
```js
shape.fillAfterStrokeEnabled(true);
```
**Instructions:** Take a look into two examples of different fill&stroke order.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const text1 = new Konva.Text({
text: 'Default shape rendering.\nfillAfterStrokeEnabled = false',
x: 50,
y: 50,
fontSize: 40,
stroke: 'green',
fill: 'yellow',
strokeWidth: 3,
});
layer.add(text1);
const text2 = new Konva.Text({
text: 'Reversed rendering order.\nfillAfterStrokeEnabled = true',
x: 50,
y: 150,
fontSize: 40,
stroke: 'green',
fill: 'yellow',
strokeWidth: 3,
fillAfterStrokeEnabled: true,
});
layer.add(text2);
```
```js
import { Stage, Layer, Text } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Hide and Show Shape Tutorial
> Learn how to toggle shape visibility on HTML5 Canvas using the Konva.js hide() and show() methods.
Source: https://konvajs.org/docs/styling/Hide_and_Show.html
To hide and show a shape with Konva, we can set the visible property when we instantiate a shape, or we can use the `hide()` and `show()` methods.
**Instructions:** Click on the buttons to show and hide the shape.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const rect = new Konva.Rect({
x: stage.width() / 2 - 50,
y: stage.height() / 2 - 25,
width: 100,
height: 50,
fill: 'green',
stroke: 'black',
strokeWidth: 4,
});
layer.add(rect);
// update button creation and styling
const buttonContainer = document.createElement('div');
buttonContainer.style.position = 'absolute';
buttonContainer.style.zIndex = 1;
buttonContainer.style.padding = '10px';
buttonContainer.style.top = '0px';
buttonContainer.style.left = '0px';
const showBtn = document.createElement('button');
showBtn.textContent = 'Show';
showBtn.onclick = () => rect.show();
buttonContainer.appendChild(showBtn);
const hideBtn = document.createElement('button');
hideBtn.textContent = 'Hide';
hideBtn.onclick = () => rect.hide();
buttonContainer.appendChild(hideBtn);
document.body.appendChild(buttonContainer);
```
```jsx
import React, { useState } from 'react';
import { Stage, Layer, Rect } from 'react-konva';
function App() {
const [visible, setVisible] = useState(true);
return (
setVisible(true)}>Show
setVisible(false)}>Hide
);
}
export default App;
```
```vue
```
---
# HTML5 Canvas Line Join Tutorial
> Learn how to set the line join style (miter, bevel, round) for shapes on HTML5 Canvas using Konva.js.
Source: https://konvajs.org/docs/styling/Line_Join.html
To set the line join for a shape with Konva, we can set the `lineJoin` property when we instantiate a shape, or we can use the `lineJoin()` method.
The `lineJoin` property can be set to `miter`, `bevel`, or `round`. Unless otherwise specified, the default line join is `miter`.
**Instructions:** Mouseover the triangle to change the line join style.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const triangle = new Konva.RegularPolygon({
x: stage.width() / 2,
y: stage.height() / 2,
sides: 3,
radius: 70,
fill: '#00D2FF',
stroke: 'black',
strokeWidth: 20,
lineJoin: 'miter'
});
layer.add(triangle);
triangle.on('mouseenter', function() {
const lineJoins = ['miter', 'bevel', 'round'];
const index = lineJoins.indexOf(triangle.lineJoin());
const nextIndex = (index + 1) % lineJoins.length;
triangle.lineJoin(lineJoins[nextIndex]);
});
```
```js
import { Stage, Layer, RegularPolygon } from 'react-konva';
import { useState } from 'react';
const App = () => {
const [lineJoin, setLineJoin] = useState('miter');
const handleMouseEnter = () => {
const lineJoins = ['miter', 'bevel', 'round'];
const index = lineJoins.indexOf(lineJoin);
const nextIndex = (index + 1) % lineJoins.length;
setLineJoin(lineJoins[nextIndex]);
};
return (
);
};
export default App;
```
```js
```
---
# Canvas Cursor Style — Change Mouse Cursor on HTML5 Canvas Shapes
> Change the mouse cursor style when hovering over HTML5 Canvas shapes. Set pointer, crosshair, grab, or custom cursors on canvas objects using Konva.js.
Source: https://konvajs.org/docs/styling/Mouse_Cursor.html
Change the mouse cursor when users hover over shapes on an HTML5 Canvas. Konva lets you listen for mouse events on individual shapes and apply any CSS cursor style to the stage container — pointer, crosshair, grab, move, or any custom cursor.
**Instructions:** Hover over each pentagon to see the cursor change.
```js
import Konva from 'konva';
const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight,
});
const layer = new Konva.Layer();
stage.add(layer);
const pentagon1 = new Konva.RegularPolygon({
x: 80,
y: stage.height() / 2,
sides: 5,
radius: 30,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
pentagon1.on('mouseover', function (e) {
e.target.getStage().container().style.cursor = 'pointer';
});
pentagon1.on('mouseout', function (e) {
e.target.getStage().container().style.cursor = 'default';
});
const pentagon2 = new Konva.RegularPolygon({
x: 180,
y: stage.height() / 2,
sides: 5,
radius: 30,
fill: 'green',
stroke: 'black',
strokeWidth: 4,
});
pentagon2.on('mouseover', function (e) {
e.target.getStage().container().style.cursor = 'crosshair';
});
pentagon2.on('mouseout', function (e) {
e.target.getStage().container().style.cursor = 'default';
});
const pentagon3 = new Konva.RegularPolygon({
x: 280,
y: stage.height() / 2,
sides: 5,
radius: 30,
fill: 'blue',
stroke: 'black',
strokeWidth: 4,
});
pentagon3.on('mouseover', function (e) {
e.target.getStage().container().style.cursor = 'move';
});
pentagon3.on('mouseout', function (e) {
e.target.getStage().container().style.cursor = 'default';
});
layer.add(pentagon1);
layer.add(pentagon2);
layer.add(pentagon3);
```
```js
import { Stage, Layer, RegularPolygon } from 'react-konva';
import { useState } from 'react';
// Separate component for polygon that changes cursor directly
const SpecialPolygon = ({ x, y }) => {
// We use e.target approach here because this component doesn't have
// access to the Stage's cursor state from the parent component
const handleMouseOver = (e) => {
e.target.getStage().container().style.cursor = 'pointer';
};
const handleMouseOut = (e) => {
e.target.getStage().container().style.cursor = 'default';
};
return (
);
};
const App = () => {
const [cursor, setCursor] = useState('default');
return (
setCursor('crosshair')}
onMouseOut={() => setCursor('default')}
/>
setCursor('move')}
onMouseOut={() => setCursor('default')}
/>
);
};
export default App;
```
```js
```
---
# HTML5 Canvas Set Shape Opacity Tutorial
> Learn how to set and change shape opacity (transparency) on HTML5 Canvas using Konva.js with values from 0 to 1.
Source: https://konvajs.org/docs/styling/Opacity.html
To set a shape opacity with Konva, we can set the `opacity` property when we instantiate the node, or we can use the `opacity()` method.
Shapes can have an opacity value between 0 and 1, where 0 is fully transparent, and 1 is fully opaque. Unless otherwise specified, all shapes are defaulted with an opacity value of 1.
If you want to apply transparency for several shapes without seen overlapping areas, take a look into [Transparent Group Demo](/docs/sandbox/Transparent_Group.html)
Instructions: Mouseover the pentagon to change its opacity.
```js
import Konva from 'konva';
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
var layer = new Konva.Layer();
var pentagon = new Konva.RegularPolygon({
x: stage.width() / 2,
y: stage.height() / 2,
sides: 5,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
opacity: 0.5,
});
pentagon.on('mouseover', function () {
this.opacity(1);
});
pentagon.on('mouseout', function () {
this.opacity(0.5);
});
// add the shape to the layer
layer.add(pentagon);
// add the layer to the stage
stage.add(layer);
```
```jsx
import React, { useState } from 'react';
import { Stage, Layer, RegularPolygon } from 'react-konva';
const App = () => {
const [opacity, setOpacity] = useState(0.5);
const [cursor, setCursor] = useState('default');
const handleMouseEnter = () => {
setOpacity(1);
setCursor('pointer');
};
const handleMouseLeave = () => {
setOpacity(0.5);
setCursor('default');
};
return (
);
};
export default App;
```
```vue
```
---
# HTML5 Canvas Shadows Tutorial
> Learn how to add shadows to shapes on HTML5 Canvas using Konva.js with shadowColor, shadowOffset, shadowBlur, and shadowOpacity.
Source: https://konvajs.org/docs/styling/Shadow.html
To apply shadows with Konva, we can set the `shadowColor`, `shadowOffset`, `shadowBlur`, and `shadowOpacity` properties when we instantiate a shape.
We can adjust the shadow properties after instantiation by using the `shadowColor()`, `shadowOffset()`, `shadowBlur()`, and `shadowOpacity()` methods.
Instructions: Mouseover the star to change its shadow properties.
```js
import Konva from 'konva';
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
var layer = new Konva.Layer();
var text = new Konva.Text({
text: 'Text Shadow!',
fontFamily: 'Calibri',
fontSize: 40,
x: 20,
y: 20,
stroke: 'red',
strokeWidth: 2,
shadowColor: 'black',
shadowBlur: 0,
shadowOffset: { x: 10, y: 10 },
shadowOpacity: 0.5,
});
var line = new Konva.Line({
stroke: 'green',
strokeWidth: 10,
lineJoin: 'round',
lineCap: 'round',
points: [50, 140, 250, 160],
shadowColor: 'black',
shadowBlur: 10,
shadowOffset: { x: 10, y: 10 },
shadowOpacity: 0.5,
});
var rect = new Konva.Rect({
x: 100,
y: 120,
width: 100,
height: 50,
fill: '#00D2FF',
stroke: 'black',
strokeWidth: 4,
shadowColor: 'black',
shadowBlur: 10,
shadowOffset: { x: 10, y: 10 },
shadowOpacity: 0.5,
});
layer.add(text);
layer.add(line);
layer.add(rect);
stage.add(layer);
```
```jsx
import React from 'react';
import { Stage, Layer, Text, Line, Rect } from 'react-konva';
const App = () => {
return (
);
};
export default App;
```
```vue
```
---
# HTML5 Canvas Set Shape Stroke Color and Width Tutorial
> Learn how to set and dynamically change stroke color and width on HTML5 Canvas shapes using Konva.js.
Source: https://konvajs.org/docs/styling/Stroke.html
To set a shape stroke and stroke width with Konva, we can set the `stroke` and `strokeWidth` properties when we instantiate a shape, or we can use the `stroke()` and `strokeWidth()` methods.
Instructions: Mouseover the pentagon to change its stroke color and width.
```js
import Konva from 'konva';
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
var layer = new Konva.Layer();
var pentagon = new Konva.RegularPolygon({
x: stage.width() / 2,
y: stage.height() / 2,
sides: 5,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
pentagon.on('mouseover', function () {
this.stroke('blue');
this.strokeWidth(20);
});
pentagon.on('mouseout', function () {
this.stroke('black');
this.strokeWidth(4);
});
// add the shape to the layer
layer.add(pentagon);
// add the layer to the stage
stage.add(layer);
```
```jsx
import React, { useState } from 'react';
import { Stage, Layer, RegularPolygon } from 'react-konva';
const App = () => {
const [stroke, setStroke] = useState('black');
const [strokeWidth, setStrokeWidth] = useState(4);
const [cursor, setCursor] = useState('default');
const handleMouseEnter = () => {
setStroke('blue');
setStrokeWidth(20);
setCursor('pointer');
};
const handleMouseLeave = () => {
setStroke('black');
setStrokeWidth(4);
setCursor('default');
};
return (
);
};
export default App;
```
```vue
```
---
# Binding the config prop
> Learn how svelte-konva keeps config props in sync with Konva node state after drag and transform events.
Source: https://konvajs.org/docs/svelte/Bindings.html
Svelte-Konva is able to keep certain props in sync with the internal state of Konva (position, rotation, scale, ...) after `dragend` and `transformend` events in case the prop is bound.
### Disabling automatic syncing
In most cases this default behavior of svelte-konva listening to the `dragend` and `transformend` events is what you want. In some cases this might not be beneficial though (mainly for performance reasons). In such cases you can opt out of this behavior by passing the `staticConfig` prop to the component, in which case svelte-konva will not listen to those events and update the bound props:
```
```
Keep in mind that svelte-konva will evaluate the `staticConfig` prop only once during component initialization. Changing the `staticConfig` prop after the component has been initialized will not have any effect.
Drag the different rings and observe the reactive changes triggered by Svelte. Note that only the bound ring (yellow) changes the coordinates on `dragend` automatically.
[Open the interactive demo](https://codesandbox.io/p/sandbox/github/konvajs/site/tree/master/svelte-demos/bindings?file=/src/App.svelte)
---
# How to cache canvas shapes with Svelte
> Learn how to cache canvas shapes in Svelte with svelte-konva for improved rendering performance using node.cache().
Source: https://konvajs.org/docs/svelte/Cache.html
If you want to cache a node in a Svelte app, you need to have an access to Konva node and use `node.cache()` function.
To get access to a node you can use the component instance's `node` property. See [Konva Node](/docs/svelte/Konva_Node.html) for more information.
**Instruction: try to drag whole stage. Then try again with cached group.**
You should see much better performance.
[Open the interactive demo](https://codesandbox.io/p/sandbox/github/konvajs/site/tree/master/svelte-demos/cache?file=/src/App.svelte)
---
# How to draw a custom canvas shape with Svelte?
> Learn how to draw custom canvas shapes in Svelte using the svelte-konva Shape component and canvas drawing functions.
Source: https://konvajs.org/docs/svelte/Custom_Shape.html
To create a custom shape with `svelte-konva`, you should use the `Shape` component.
When creating a custom shape, you need to define a drawing function that is passed a `Konva.Canvas` renderer.
You can then use the renderer to access the HTML5 Canvas context, and to use special methods like `context.fillStrokeShape(shape)` which automatically handles filling, stroking, and applying shadows.
[Open the interactive demo](https://codesandbox.io/p/sandbox/github/konvajs/site/tree/master/svelte-demos/custom_shape?file=/src/App.svelte)
---
# Drag and drop canvas shapes with Svelte
> Learn how to enable drag and drop for canvas shapes in Svelte with svelte-konva using the draggable prop and bindings.
Source: https://konvajs.org/docs/svelte/Drag_And_Drop.html
To enable drag&drop for any node on canvas you just need to pass the `draggable=true` prop to the component.
svelte-konva is able to automatically keep affected props (x, y) in sync with the Konva node on `dragend`. See the [bindings](/docs/svelte/Bindings.html) doc page for more details.
[Open the interactive demo](https://codesandbox.io/p/sandbox/github/konvajs/site/tree/master/svelte-demos/drag_and_drop?file=/src/App.svelte)
---
# How to listen to an event on a canvas shape with Svelte and Konva?
> Learn how to handle click, mouse, touch, and drag events on canvas shapes with Svelte and Konva.
Source: https://konvajs.org/docs/svelte/Events.html
With `svelte-konva` you can easily listen to user input events (`click`, `dblclick`, `mouseover`, `tap`, `dbltap`, `touchstart`, etc...) and drag&drop events (`dragstart`, `dragmove`, `dragend`). For this you can pass a callback function to the prop named `on` which is then called by svelte-konva every time the event is fired. You can also access the Konva event payload object inside the callback function as its argument.
```js
```
For the full list of events take a look into [on() method documentation](/api/Konva.Node.html).
## Bubbling
Konva events bubble up by default. To prevent this you can set the `cancelBubble` property of the Konva event to `true`:
```js
function handleClick(e) {
// Cancel bubbling
e.cancelBubble = true;
}
```
[Open the interactive demo](https://codesandbox.io/p/sandbox/github/konvajs/site/tree/master/svelte-demos/events?file=/src/App.svelte)
---
# How to apply canvas filters with Svelte and Konva?
> Learn how to apply canvas filters to shapes in Svelte with Konva using manual caching in onMount and afterUpdate.
Source: https://konvajs.org/docs/svelte/Filters.html
To apply filters you need to cache `Konva.Node` manually. You can do this initially in the `onMount()` method.
In case you dynamically change the style of the nodes you need to recache them manually for the changes to take effect on the canvas. This can be done by calling the `cache()` method on the affected nodes directly after a change (like in the demo) or in the `afterUpdate()` method to automatically recache the node on each state change in the component.
Instructions: hover over the rectangle to see the changes
[Open the interactive demo](https://codesandbox.io/p/sandbox/github/konvajs/site/tree/master/svelte-demos/filters?file=/src/App.svelte)
---
# How to draw an image on canvas with Svelte?
> Learn how to load and display images on an HTML5 Canvas with Svelte using the svelte-konva Image component.
Source: https://konvajs.org/docs/svelte/Images.html
For images you need to manually create a native window.Image instance or `canvas` element and use it as image attribute of `Image` component.
[Open the interactive demo](https://codesandbox.io/p/sandbox/github/konvajs/site/tree/master/svelte-demos/images?file=/src/App.svelte)
---
# Accessing the Konva node
> Learn how to access the underlying Konva node from svelte-konva components via the node property or event payloads.
Source: https://konvajs.org/docs/svelte/Konva_Node.html
In some cases you might need to access the underlying Konva node of the svelte-konva component directly. You can do this by accessing the `node` property of the corresponding component instance or by accessing it in the payload of a Konva event.
[Open the interactive demo](https://codesandbox.io/p/sandbox/github/konvajs/site/tree/master/svelte-demos/konva_node?file=/src/App.svelte)
---
# Using labels with Svelte
> Learn how to create labels and tooltips on canvas with Svelte using svelte-konva Label, Tag, and Text components.
Source: https://konvajs.org/docs/svelte/Labels.html
Creating a label is a multi-step process in Konva, as a Label instance needs to contain a Tag and Text instance to function. In svelte-konva the Tag and Text components can be easily nested inside the Label component to automatically create a correct Label without having to wire things up manually.
Hover over the circles to show the tooltips:
[Open the interactive demo](https://codesandbox.io/p/sandbox/github/konvajs/site/tree/master/svelte-demos/labels?file=/src/App.svelte)
---
# Saving and loading canvas with Svelte and Konva
> Learn how to save and load canvas state in Svelte with Konva by serializing your app state instead of Konva internals.
Source: https://konvajs.org/docs/svelte/Save_Load.html
Native Konva can serialize a node tree and its serializable attributes with `node.toJSON()`. It can restore them with `Konva.Node.create(json)` [(see demo)](/docs/data_and_serialization/Simple_Load.html). Restore images, event handlers, and custom drawing functions separately.
With svelte-konva, save the application state instead. The state must contain the data that the stage needs. Do not save Konva internals and nodes.
The demo saves and retrieves JSON data from `localStorage`. You can use a different storage method.
[Open the interactive demo](https://codesandbox.io/p/sandbox/github/konvajs/site/tree/master/svelte-demos/save_load?file=/src/App.svelte)
---
# Drawing canvas shapes with Svelte
> Learn how to draw canvas shapes like Rect, Circle, Line, Star, and more in Svelte using svelte-konva components.
Source: https://konvajs.org/docs/svelte/Shapes.html
All `svelte-konva` components correspond to `Konva` components of the same name. All the parameters available for `Konva` objects are valid props for corresponding `svelte-konva` components, unless noted otherwise.
Core shapes are: Rect, Circle, Ellipse, Line, Image, Text, TextPath, Star, Label, SVG Path, RegularPolygon. You can also create custom shapes.
To get more info about Konva you can read the [Konva Overview](/docs/overview.html).
[Open the interactive demo](https://codesandbox.io/p/sandbox/github/konvajs/site/tree/master/svelte-demos/shapes?file=/src/App.svelte)
---
# How to apply canvas animations with Svelte and Konva?
> Learn how to animate canvas shapes in Svelte using Konva Tweens, node.to() method, and Konva.Animation.
Source: https://konvajs.org/docs/svelte/Simple_Animations.html
Konva itself has two methods for animations [Tween](/docs/tweens/Linear_Easing.html) and [Animation](/docs/animations/Rotation.html). You can apply both of them to nodes manually.
For simple use cases we recommend to use `node.to()` method.
Instructions: Try to move a rectangle.
[Open the interactive demo](https://codesandbox.io/p/sandbox/github/konvajs/site/tree/master/svelte-demos/simple_animations?file=/src/App.svelte)
---
# How to use svelte-konva with SvelteKit?
> Learn how to use svelte-konva with SvelteKit SSR and prerendering using browser checks or dynamic imports.
Source: https://konvajs.org/docs/svelte/SvelteKit.html
Generally, svelte-konva is a client-side only library. When using SvelteKit, special care needs to be taken if svelte-konva/Konva functionality is used on prerendered and server side rendered (SSR) components. Prerendering and SSR happens in a Node.js environment. In case you use any svelte-konva functionality in such a context it will throw an error on the server:
> Error: svelte-konva: Library can only be used in a browser context but is currently used in a server environment.
There are multiple solutions to this problem:
### Wrap your svelte-konva Components into browser checks
A rudimental solution is to wrap all your svelte-konva code into SvelteKit browser checks. This is only recommended in case your project is small as all the if-blocks can get messy quickly. For larger projects use dynamic imports outlined below.
```html
{#if browser}
{/if}
```
### Dynamically import your svelte-konva stage:
A better approach is to dynamically import your svelte-konva canvas on the client-side only. Suppose you have a Svelte component containing your stage with various svelte-konva components:
_MyCanvas.svelte_
```html
```
To use this component inside a SvelteKit prerendered/SSR page you can dynamically import it inside `onMount()` and render it once it becomes defined:
_+page.svelte_
```html
This is my fancy server side rendered (or prerendered) page.
{#await MyCanvas}
Loading...
{:then Component}
{:catch error}
Something went wrong: {error.message}
{/await}
```
Instructions: Each page available in this SvelteKit App is rendered differently containing a `svelte-konva` canvas. Both dynamic import approaches are shown. Client-side only use of the canvas using SvelteKit browser checks on the prerendered page and dynamic importing of the svelte-konva canvas on the SSR page. Try to inspect the network requests made on each navigation to understand the different approaches of rendering in SvelteKit.
[Open the interactive demo](https://codesandbox.io/p/sandbox/github/konvajs/site/tree/master/svelte-demos/sveltekit?file=/src/routes/%2Bpage.svelte)
---
# How to resize and rotate canvas shapes with Svelte and Konva?
> Learn how to resize and rotate canvas shapes in Svelte using the svelte-konva Transformer component with select support.
Source: https://konvajs.org/docs/svelte/Transformer.html
You can use the transformer tool by using the svelte-konva transformer component. Generally this approach requires some interfacing with the native Konva API. You can attach shapes to the transformer by manually attaching their handles to the transformer using the `nodes()` function. svelte-konva also automatically keeps the relevant component props in sync with the Konva node on `transformend` if bound. See the [bindings](/docs/svelte/Bindings.html) doc page for more details.
For a more detailed example with select & transform functionality see the [example](https://github.com/konvajs/svelte-konva/blob/master/src/routes/examples/transform/Transform.svelte) in the svelte-konva repo.
Instructions: click on shape to select it.
[Open the interactive demo](https://codesandbox.io/p/sandbox/github/konvajs/site/tree/master/svelte-demos/transformer?file=/src/App.svelte)
---
# How to change the zIndex of nodes with svelte-konva?
> Learn how to manage zIndex and reorder canvas shapes in svelte-konva using native Konva methods like moveToTop().
Source: https://konvajs.org/docs/svelte/zIndex.html
When working with other Konva-Wrappers like `vue-konva` or `react-konva` you might be used to the data order representing the drawing order of the components on the canvas. In svelte-konva such a functionality is currently not implemented.
Instead you should use the Konva native functions to perform dynamic reordering of components on the canvas like `node.zIndex(5)`, `node.moveToTop()`, etc. [Tutorial](/docs/groups_and_layers/Layering.html).
### Using if-blocks
svelte-konva will follow the initial ordering of the components to draw the shapes on the canvas. This works fine in cases where you do not need to change the ordering dynamically during runtime. When using Svelte if-blocks to show/hide certain components you should know the following caveat. Consider the following example:
```
{#if showRing}
{/if}
```
Based on the ordering one would expect to see the circle drawn on the top of the canvas, followed by the ring and then the rect shapes. However, due to the if-block the ring might end up at the top of the canvas depending on the initial value and changes of `showRing`. This is caused by Svelte mounting/unmounting components inside if-blocks and svelte-konva drawing the shapes during mounting at the top of the canvas. If you want to avoid this behavior you should avoid Svelte if-blocks and use the `visible` prop to control whether a shape is visible or not. This way the component is not mounted/unmounted and maintains its initial drawing order on the canvas.
Instructions: Try to drag a circle. See how it goes to the top. This is done by calling `moveToTop()` on the dragged shape handle.
[Open the interactive demo](https://codesandbox.io/p/sandbox/github/konvajs/site/tree/master/svelte-demos/zIndex?file=/src/App.svelte)
---
# Konva.js Tools and Plugins
> Explore the official Konva integrations for React, Vue, Svelte, and Angular.
Source: https://konvajs.org/docs/tools.html
- [Konva + React](https://github.com/konvajs/react-konva/)
- [Konva + Vue](https://github.com/konvajs/vue-konva)
- [Konva + Svelte](https://github.com/konvajs/svelte-konva)
- [Konva + Angular](https://github.com/konvajs/ng2-konva)
## Debugging
- [konva-devtool](https://github.com/konvajs/konva-devtool) — official browser extension that shows the Konva scene graph and node attributes, instead of the single `` element the built-in inspector sees
- [konva-inspector](https://github.com/maitrungduc1410/konva-inspector) — community extension with a profiler and a `$konva` console handle; supports Konva 9 and 10
---
# All Tween Controls Tutorial
> Learn how to control Konva.js tweens with play, pause, reverse, reset, finish, and seek methods.
Source: https://konvajs.org/docs/tweens/All_Controls.html
To control tweens with Konva, we can use the following methods:
- `play()` - Start or resume the tween
- `pause()` - Pause the tween
- `reverse()` - Reverse the tween direction
- `reset()` - Reset to initial state
- `finish()` - Jump to final state
- `seek()` - Jump to specific position
**Instructions: Use the buttons to control the tween animation of the circle.**
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
const circle = new Konva.Circle({
x: 100,
y: height / 2,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
layer.add(circle);
stage.add(layer);
const tween = new Konva.Tween({
node: circle,
duration: 2,
x: width - 100,
easing: Konva.Easings.EaseInOut,
});
// create buttons
const controls = ['play', 'pause', 'reverse', 'reset', 'finish'];
controls.forEach(control => {
const button = document.createElement('button');
button.textContent = control;
button.addEventListener('click', () => {
tween[control]();
});
document.body.appendChild(button);
});
// seek control
const seekBtn = document.createElement('button');
seekBtn.textContent = 'Seek to 50%';
seekBtn.addEventListener('click', () => {
tween.seek(1); // seek to 1 second
});
document.body.appendChild(seekBtn);
````
```js
import Konva from 'konva';
import { Stage, Layer, Circle } from 'react-konva';
import { useEffect, useRef } from 'react';
const App = () => {
const circleRef = useRef();
const tweenRef = useRef();
useEffect(() => {
if (!circleRef.current) return;
const tween = new Konva.Tween({
node: circleRef.current,
duration: 2,
x: window.innerWidth - 100,
easing: Konva.Easings.EaseInOut,
});
tweenRef.current = tween;
return () => tween.destroy();
}, []);
const controls = ['play', 'pause', 'reverse', 'reset', 'finish'];
return (
<>
{controls.map(control => (
tweenRef.current?.[control]()}
>
{control}
))}
tweenRef.current?.seek(1)}>
Seek to 50%
>
);
};
export default App;
````
```js
{{ control }}
Seek to 50%
```
---
# More Easing Functions Tutorial
> See all Konva.js easing functions in action: Linear, Ease, Back, Elastic, Bounce, and Strong with interactive demos.
Source: https://konvajs.org/docs/tweens/All_Easings.html
This tutorial demonstrates all of the easing function sets provided by Konva, including:
- `Linear`
- `Ease`
- `Back`
- `Elastic`
- `Bounce`
- `Strong`
For all available easings go to [Easings Documentation](/api/Konva.Easings.html).
**Instructions: Press "Play" to transition all of the text nodes with different easing functions.**
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
const easings = [
'Linear',
'EaseIn',
'EaseOut',
'EaseInOut',
'BackEaseIn',
'BackEaseOut',
'BackEaseInOut',
'ElasticEaseIn',
'ElasticEaseOut',
'ElasticEaseInOut',
'BounceEaseIn',
'BounceEaseOut',
'BounceEaseInOut',
'StrongEaseIn',
'StrongEaseOut',
'StrongEaseInOut',
];
const tweens = [];
easings.forEach((easing, i) => {
const text = new Konva.Text({
x: 50,
y: 30 + i * 25,
text: easing,
fontSize: 16,
fontFamily: 'Calibri',
fill: 'black',
});
layer.add(text);
tweens.push(
new Konva.Tween({
node: text,
duration: 2,
x: width - 200,
easing: Konva.Easings[easing],
})
);
});
stage.add(layer);
// create button
const button = document.createElement('button');
button.textContent = 'Play';
button.style.position = 'absolute';
button.style.top = '0px';
button.style.left = '0px';
button.addEventListener('click', () => {
tweens.forEach((tween) => {
tween.reset();
tween.play();
});
});
document.body.appendChild(button);
````
```js
import Konva from 'konva';
import { Stage, Layer, Text } from 'react-konva';
import { useEffect, useRef } from 'react';
const App = () => {
const tweensRef = useRef([]);
const textsRef = useRef([]);
const easings = [
'Linear',
'EaseIn',
'EaseOut',
'EaseInOut',
'BackEaseIn',
'BackEaseOut',
'BackEaseInOut',
'ElasticEaseIn',
'ElasticEaseOut',
'ElasticEaseInOut',
'BounceEaseIn',
'BounceEaseOut',
'BounceEaseInOut',
'StrongEaseIn',
'StrongEaseOut',
'StrongEaseInOut',
];
useEffect(() => {
tweensRef.current = textsRef.current.map((text, i) => {
return new Konva.Tween({
node: text,
duration: 2,
x: window.innerWidth - 200,
easing: Konva.Easings[easings[i]],
});
});
return () => {
tweensRef.current.forEach((tween) => tween.destroy());
};
}, []);
const handlePlay = () => {
tweensRef.current.forEach((tween) => {
tween.reset();
tween.play();
});
};
return (
<>
Play
{easings.map((easing, i) => (
{
textsRef.current[i] = node;
}}
x={50}
y={30 + i * 25}
text={easing}
fontSize={16}
fontFamily="Calibri"
fill="black"
/>
))}
>
);
};
export default App;
````
```js
Play
```
---
# Simple Easings Tutorial
> Learn how to use common easing functions like EaseIn, EaseOut, and EaseInOut for smooth Konva.js tween animations.
Source: https://konvajs.org/docs/tweens/Common_Easings.html
To create a non-linear easing tween with Konva, we can set the `easing` property to an easing function. Other than `Konva.Easings.Linear`, the other most common easings are:
- `Konva.Easings.EaseIn`
- `Konva.Easings.EaseInOut`
- `Konva.Easings.EaseOut`
For all available easings go to [Easings Documentation](/api/Konva.Easings.html).
**Instructions: Mouseover or touchstart the boxes to tween them with different easing functions.**
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
const easings = ['Linear', 'EaseIn', 'EaseOut', 'EaseInOut'];
const boxes = [];
easings.forEach((easing, i) => {
const box = new Konva.Rect({
x: 50,
y: 50 + i * 80,
width: 100,
height: 50,
fill: '#00D2FF',
stroke: 'black',
strokeWidth: 4,
});
layer.add(box);
boxes.push(box);
const text = new Konva.Text({
x: 160,
y: 65 + i * 80,
text: easing,
fontSize: 16,
fontFamily: 'Calibri',
fill: 'black',
});
layer.add(text);
box.on('mouseenter touchstart', () => {
const tween = new Konva.Tween({
node: box,
duration: 1,
x: width - 150,
easing: Konva.Easings[easing],
}).play();
});
box.on('mouseleave touchend', () => {
const tween = new Konva.Tween({
node: box,
duration: 1,
x: 50,
easing: Konva.Easings[easing],
}).play();
});
});
stage.add(layer);
````
```js
import Konva from 'konva';
import { Stage, Layer, Rect, Text } from 'react-konva';
import { useRef } from 'react';
const Box = ({ easing, y }) => {
const boxRef = useRef();
const handleMouseEnter = () => {
boxRef.current.to({
duration: 1,
x: window.innerWidth - 150,
easing: Konva.Easings[easing],
});
};
const handleMouseLeave = () => {
boxRef.current.to({
duration: 1,
x: 50,
easing: Konva.Easings[easing],
});
};
return (
<>
>
);
};
const App = () => {
const easings = ['Linear', 'EaseIn', 'EaseOut', 'EaseInOut'];
return (
{easings.map((easing, i) => (
))}
);
};
export default App;
````
```js
```
---
# Complex Tweening Tutorial
> Learn how to create complex tween animations in Konva.js including gradient transitions and chained tweens.
Source: https://konvajs.org/docs/tweens/Complex_Tweening.html
This demo combines chained `Konva.Tween` instances with a `Konva.Animation`.
The tweens change the circle scale. The animation changes the
`fillLinearGradientColorStops` property.
**Instructions: Click the shape to start the complex animation with gradient changes.**
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
const circle = new Konva.Circle({
x: width / 2,
y: height / 2,
radius: 70,
fillLinearGradientStartPoint: { x: -50, y: -50 },
fillLinearGradientEndPoint: { x: 50, y: 50 },
fillLinearGradientColorStops: [0, 'red', 1, 'yellow'],
stroke: 'black',
strokeWidth: 4,
draggable: true,
});
layer.add(circle);
stage.add(layer);
let scaleUpTween;
let scaleDownTween;
let gradientAnimation;
let gradientTimer;
const stopAnimation = () => {
scaleUpTween?.destroy();
scaleDownTween?.destroy();
gradientAnimation?.stop();
clearTimeout(gradientTimer);
scaleUpTween = undefined;
scaleDownTween = undefined;
gradientAnimation = undefined;
gradientTimer = undefined;
};
circle.on('click tap', () => {
stopAnimation();
// using regular Konva tween
scaleUpTween = new Konva.Tween({
node: circle,
duration: 1,
scaleX: 1.5,
scaleY: 1.5,
easing: Konva.Easings.EaseInOut,
onFinish: () => {
scaleUpTween.destroy();
scaleUpTween = undefined;
// scale back with another tween
scaleDownTween = new Konva.Tween({
node: circle,
duration: 1,
scaleX: 1,
scaleY: 1,
easing: Konva.Easings.BounceEaseOut,
onFinish: () => {
scaleDownTween.destroy();
scaleDownTween = undefined;
},
});
scaleDownTween.play();
},
});
scaleUpTween.play();
// manually update gradient
let ratio = 0;
gradientAnimation = new Konva.Animation((frame) => {
ratio += frame.timeDiff / 1000;
if (ratio > 1) {
ratio = 0;
}
circle.fillLinearGradientColorStops([
0,
'red',
ratio,
'yellow',
1,
'blue',
]);
}, layer);
gradientAnimation.start();
gradientTimer = setTimeout(() => {
gradientAnimation.stop();
gradientAnimation = undefined;
gradientTimer = undefined;
}, 2000);
});
````
```js
import Konva from 'konva';
import { Stage, Layer, Circle } from 'react-konva';
import { useEffect, useRef, useState } from 'react';
const App = () => {
const circleRef = useRef();
const scaleUpTweenRef = useRef();
const scaleDownTweenRef = useRef();
const gradientAnimationRef = useRef();
const gradientTimerRef = useRef();
const [position, setPosition] = useState({
x: window.innerWidth / 2,
y: window.innerHeight / 2,
});
const stopAnimation = () => {
scaleUpTweenRef.current?.destroy();
scaleDownTweenRef.current?.destroy();
gradientAnimationRef.current?.stop();
clearTimeout(gradientTimerRef.current);
scaleUpTweenRef.current = undefined;
scaleDownTweenRef.current = undefined;
gradientAnimationRef.current = undefined;
gradientTimerRef.current = undefined;
};
useEffect(() => stopAnimation, []);
const handleClick = () => {
const circle = circleRef.current;
stopAnimation();
// using regular Konva tween
scaleUpTweenRef.current = new Konva.Tween({
node: circle,
duration: 1,
scaleX: 1.5,
scaleY: 1.5,
easing: Konva.Easings.EaseInOut,
onFinish: () => {
scaleUpTweenRef.current.destroy();
scaleUpTweenRef.current = undefined;
// scale back with another tween
scaleDownTweenRef.current = new Konva.Tween({
node: circle,
duration: 1,
scaleX: 1,
scaleY: 1,
easing: Konva.Easings.BounceEaseOut,
onFinish: () => {
scaleDownTweenRef.current.destroy();
scaleDownTweenRef.current = undefined;
},
});
scaleDownTweenRef.current.play();
},
});
scaleUpTweenRef.current.play();
// manually update gradient
let ratio = 0;
gradientAnimationRef.current = new Konva.Animation((frame) => {
ratio += frame.timeDiff / 1000;
if (ratio > 1) {
ratio = 0;
}
circle.fillLinearGradientColorStops([
0,
'red',
ratio,
'yellow',
1,
'blue',
]);
}, circle.getLayer());
gradientAnimationRef.current.start();
gradientTimerRef.current = setTimeout(() => {
gradientAnimationRef.current.stop();
gradientAnimationRef.current = undefined;
gradientTimerRef.current = undefined;
}, 2000);
};
return (
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
onClick={handleClick}
onTap={handleClick}
/>
);
};
export default App;
````
```js
```
---
# HTML5 Canvas Tween Finish Event Tutorial
> Learn how to trigger a callback function when a Konva.js tween animation finishes using the onFinish property.
Source: https://konvajs.org/docs/tweens/Finish_Event/index.html
To trigger a user defined function when the tween finishes with Konva, we can set the `onFinish` property.
---
# Basic Tweening Tutorial
> Learn how to create basic linear tween animations in Konva.js to transition shape properties like position, scale, and opacity.
Source: https://konvajs.org/docs/tweens/Linear_Easing.html
To tween properties with Konva, we can instantiate a `Konva.Tween` object and then start the tween by calling `play()`.
Any numeric property of a `Shape`, `Group`, `Layer`, or `Stage` can be transitioned, such as:
- `x`, `y` (position)
- `rotation`
- `width`, `height`, `radius`
- `strokeWidth`
- `opacity`
- `scaleX`, `scaleY`
- `offsetX`, `offsetY`
For a full list of attributes and methods, check out the [Konva.Tween documentation](/api/Konva.Tween.html).
**Instructions: Click the circle to start a simple linear animation.**
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
const circle = new Konva.Circle({
x: 100,
y: height / 2,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
layer.add(circle);
stage.add(layer);
circle.on('click tap', () => {
// simple tween
const tween = new Konva.Tween({
node: circle,
duration: 1,
x: width - 100,
easing: Konva.Easings.Linear,
});
tween.play();
});
````
```js
import Konva from 'konva';
import { Stage, Layer, Circle } from 'react-konva';
import { useEffect, useRef } from 'react';
const App = () => {
const circleRef = useRef();
const tweenRef = useRef();
useEffect(() => {
return () => tweenRef.current?.destroy();
}, []);
const handleClick = () => {
tweenRef.current?.destroy();
tweenRef.current = new Konva.Tween({
node: circleRef.current,
duration: 1,
x: window.innerWidth - 100,
easing: Konva.Easings.Linear,
});
tweenRef.current.play();
};
return (
);
};
export default App;
````
```js
```
---
# Tween Blur Filter Tutorial
> Learn how to animate filter properties like blurRadius using Konva.js tweens for smooth filter transitions on images.
Source: https://konvajs.org/docs/tweens/Tween_Filter.html
To tween a filter using Konva, we can simply tween the properties associated with the filter.
In this tutorial, we'll tween the `blurRadius` property, which controls the amount of blur applied to the image.
**Instructions: Mouseover or touch the image to focus it (reduce blur).**
```js
import Konva from 'konva';
const width = window.innerWidth;
const height = window.innerHeight;
const stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
const layer = new Konva.Layer();
// create image
const imageObj = new Image();
imageObj.onload = () => {
const lion = new Konva.Image({
x: 50,
y: 50,
image: imageObj,
draggable: true,
});
layer.add(lion);
// add blur filter
lion.cache();
lion.filters([Konva.Filters.Blur]);
lion.blurRadius(10);
// create blur tween
const tween = new Konva.Tween({
node: lion,
duration: 0.5,
blurRadius: 0,
easing: Konva.Easings.EaseInOut,
});
// bind events
lion.on('mouseenter touchstart', () => {
tween.play();
});
lion.on('mouseleave touchend', () => {
tween.reverse();
});
};
imageObj.src = '/assets/lion.png';
imageObj.crossOrigin = 'anonymous';
stage.add(layer);
````
```js
import Konva from 'konva';
import { Stage, Layer, Image } from 'react-konva';
import { useEffect, useRef, useState } from 'react';
import useImage from 'use-image';
const App = () => {
const imageRef = useRef();
const [image] = useImage('/assets/lion.png');
const tweenRef = useRef();
const [position, setPosition] = useState({ x: 50, y: 50 });
useEffect(() => {
if (!image || !imageRef.current) return;
const node = imageRef.current;
node.cache();
node.filters([Konva.Filters.Blur]);
node.blurRadius(10);
const tween = new Konva.Tween({
node: node,
duration: 0.5,
blurRadius: 0,
easing: Konva.Easings.EaseInOut,
});
tweenRef.current = tween;
return () => {
tween.destroy();
if (tweenRef.current === tween) tweenRef.current = null;
};
}, [image]);
const handleMouseEnter = () => {
tweenRef.current?.play();
};
const handleMouseLeave = () => {
tweenRef.current?.reverse();
};
return (
{
setPosition({ x: e.target.x(), y: e.target.y() });
}}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onTouchStart={handleMouseEnter}
onTouchEnd={handleMouseLeave}
/>
);
};
export default App;
````
```js
```
---
# How to cache canvas shapes with Vue?
> Learn how to cache canvas shapes in Vue with vue-konva for improved rendering performance using node.cache().
Source: https://konvajs.org/docs/vue/Cache.html
If you want to cache a node in a Vue app, you need to have access to the Konva node and use the `node.cache()` function.
To get access to a node you can use references and the `component.getNode()` method:
```javascript
// in template:
// later in the code:
this.$refs.group.getNode().cache();
```
Instructions: Try to drag the whole stage. Then enable caching and try again. You should see much better performance with caching enabled.
```js
```
---
# How to draw custom canvas shape with Vue?
> Learn how to draw custom canvas shapes in Vue using the v-shape component and HTML5 Canvas drawing functions.
Source: https://konvajs.org/docs/vue/Custom_Shape.html
To create a custom shape with `vue-konva`, we should use the `v-shape` component.
When creating a custom shape, we need to define a drawing function that is passed a Konva.Canvas renderer.
We can use the renderer to access the HTML5 Canvas context, and to use special methods like `context.fillStrokeShape(shape)` which automatically handles filling, stroking, and applying shadows.
Instructions: The demo shows a custom shape drawn using canvas drawing commands.
```js
```
---
# How to implement drag and drop for canvas shapes with Vue?
> Learn how to add drag and drop to canvas shapes in Vue with vue-konva using the draggable property and drag events.
Source: https://konvajs.org/docs/vue/Drag_And_Drop.html
To enable drag&drop for any node on canvas you just need to pass `draggable: true` property into the component.
When you drag&drop a shape, it is recommended to save its position in your app store. You can use `dragstart` and `dragend` events for that purpose.
Instructions: Try to drag the text. Notice how it changes color while being dragged.
```js
```
---
# How to listen to events on canvas shapes with Vue and Konva?
> Learn how to listen to mouse, touch, and drag events on canvas shapes with Vue and Konva.
Source: https://konvajs.org/docs/vue/Events.html
With `vue-konva` you can easily listen to user input events (`click`, `dblclick`, `mouseover`, `tap`, `dbltap`, `touchstart`, etc...) and drag&drop events (`dragstart`, `dragmove`, `dragend`).
For the full list of events take a look at the [on() method documentation](/api/Konva.Node.html).
Instructions: Move your mouse over the triangle to see coordinates. Move the mouse out to see the mouseout event.
```js
```
---
# How to apply canvas filters with Vue and Konva?
> Learn how to apply canvas filters like noise and blur to shapes in Vue with Konva using caching and recaching.
Source: https://konvajs.org/docs/vue/Filters.html
To apply filters you need to cache `Konva.Node` manually. You can do it in the `mounted()` hook.
You will need to recache nodes every time you update their styles.
Instructions: Move your mouse over the rectangle to see color changes with noise filter applied.
```js
```
---
# How to draw images on canvas with Vue?
> Learn how to load and display images on an HTML5 Canvas with Vue using the vue-konva useImage hook.
Source: https://konvajs.org/docs/vue/Images.html
For images, you can use the `useImage` hook from `vue-konva` to easily load and handle images in your components.
Instructions: The demo shows how to load and display multiple images on the canvas.
```js
```
---
# How to save and load canvas with Vue and Konva?
> Learn how to save and load canvas state in Vue with Konva by serializing your app state to localStorage.
Source: https://konvajs.org/docs/vue/Save-Load.html
## How to serialize and deserialize Konva stage with Vue?
Pure Konva can serialize a node tree and its serializable attributes with `node.toJSON()`. It can restore them with `Konva.Node.create(json)`. Restore images, event handlers, and custom drawing functions separately.
[See demo](/docs/data_and_serialization/Simple_Load.html).
With `vue-konva`, define the application state in your Vue components. The state maps to nodes through templates. Save and load the application state instead of Konva internals and nodes.
Instructions: Click on the canvas to create circles. Reload the page - the circles should persist.
```js
Click on canvas to create a circle.
Reload the page . Circles should stay here.
```
---
# Drawing canvas shapes with Vue
> Learn how to draw canvas shapes like rectangles, circles, lines, and more in Vue using vue-konva components.
Source: https://konvajs.org/docs/vue/Shapes.html
All `vue-konva` components correspond to Konva components of the same name with the prefix 'v'. All the parameters available for Konva objects can be added as config in the prop for corresponding `vue-konva` components.
Core shapes are: `v-rect`, `v-circle`, `v-ellipse`, `v-line`, `v-image`, `v-text`, `v-text-path`, `v-star`, `v-label`, `v-path`, `v-regular-polygon`. You can also create a [custom shape](/docs/vue/Custom_Shape.html).
To get more info about Konva you can read [Konva Overview](/docs/overview.html).
Instructions: The demo shows various shapes with different styles and configurations.
```js
```
---
# How to apply canvas animations with Vue and Konva?
> Learn how to animate canvas shapes in Vue using Konva Tweens, node.to() method, and Konva.Animation for continuous motion.
Source: https://konvajs.org/docs/vue/Simple_Animations.html
Konva provides two methods for animations: [Tween](/docs/tweens/Linear_Easing.html) and [Animation](/docs/animations/Rotation.html). For simple use cases, we recommend using the `node.to()` method, which is a simplified version of Tween.
Instructions: Try to drag the green rectangle to see it scale randomly, and observe the red hexagon moving in a sine wave pattern.
```js
```
The demo above shows two types of animations:
1. Using `node.to()` method (Tween) to animate the green rectangle's scale when dragged
2. Using `Konva.Animation` to create a continuous sine wave movement for the red hexagon
The `node.to()` method is perfect for simple transitions, while `Konva.Animation` is better for complex, continuous animations that need to run on each frame.
---
# How to resize and rotate canvas shapes with Vue and Konva?
> Learn how to resize and rotate canvas shapes in Vue using the Konva Transformer component with click-to-select.
Source: https://konvajs.org/docs/vue/Transformer.html
Currently, there is no pure declarative "Vue-way" to use the Transformer tool.
However, you can still use it effectively by manually attaching it to Konva nodes.
The idea is to create a `v-transformer` component and manually attach it to the required node when selected.
Instructions: Click on a shape to select it. You can then resize and rotate it using the transformer handles. The shape will change color after transformation.
```js
```
## What Transformer does not do
`Transformer` draws the handles and applies the scale. Snapping to other
objects, alignment guides, a shared bounding box for a multi-selection, and
per-shape aspect rules are all yours to build — see
[objects snapping](/docs/sandbox/Objects_Snapping.html) for one approach.
A production editor also needs text editing, templates, and export around the
Transformer. If you would rather not build those, [Polotno](https://polotno.com/?utm_source=konvajs&utm_medium=docs&utm_content=vue-transformer) is a commercial
design editor SDK built on Konva by the Konva maintainers that ships them.
---
# How to implement undo/redo on canvas with Vue?
> Learn how to implement undo and redo for canvas shapes in Vue by tracking state history with Vue reactivity.
Source: https://konvajs.org/docs/vue/Undo-Redo.html
To implement undo/redo functionality with Vue, you don't need to use Konva's serialization and deserialization methods.
You just need to save a history of all the state changes within your app. There are many ways to do this. It may be simpler to do that if you use immutable structures.
Instructions: Try to move the square by dragging it. Then use the "undo" and "redo" buttons to revert or replay your actions.
```js
```
The demo shows how to:
1. Keep track of position history using Vue's reactivity system
2. Implement undo/redo functionality by navigating through the history
3. Update the history when dragging ends
4. Use `reactive` for the current position and `ref` for the history to maintain reactivity
Note that we're using Vue's reactivity system to manage the state, but we're careful to avoid unnecessary re-renders by keeping the history in a `ref`.
## Where a hand-built history stops
The history above records one value per step. A production editor has to record
grouped operations, so that a multi-select drag undoes as a single step, plus
transforms and images that finish loading after the action. That state machine
usually grows larger than the drawing code, so plan the history around document
operations rather than around raw node state.
---
# How to change the zIndex of nodes with Vue?
> Learn how to change zIndex and reorder canvas shapes in Vue by manipulating data array order instead of using zIndex.
Source: https://konvajs.org/docs/vue/zIndex.html
## How to change the zIndex and reorder components in `vue-konva`?
When you are working with `Konva` directly, you have many methods to change the order of nodes like `node.zIndex(5)`, `node.moveToTop()`, etc. See the [Layering Tutorial](/docs/groups_and_layers/Layering.html) for more details.
However, when working with Vue, it's recommended to follow Vue's declarative approach instead of using these imperative methods.
`vue-konva` follows the order of the nodes exactly as you describe them in your ``. Instead of changing the `zIndex` manually, you should update your app's data so that the components in your `` maintain the correct order.
The demo shows how to:
1. Create an array of circle shapes with random positions and colors
2. Handle drag events to update the visual order of shapes
3. Maintain the correct stacking order by manipulating the array order
4. Follow Vue's reactivity system for state management
Remember: Don't use the `zIndex` property for your canvas components. Instead, rely on the order of elements in your template and data structures.
Instructions: Try to drag a circle. When you start dragging, it will automatically move to the top of the stack. This is achieved by manipulating the array of circles in our data, not by manually changing zIndex.
```js
```