Published
- 6 min read
How to Draw shapes on HTML canvas using Konvajs: Creating image annotation app
Overview
In this post, I will write a code to draw shapes using the Konvajs library and create an application where users can draw different shapes such as rectangles, arrows, and dots on the specific parts of an image. Each shape will also have a comment that will be used to identify the objects in the image. Konvajs makes HTML canvas API easier than using natively. This article will help you to learn the use of konvajs API in HTML by drawing some shapes such as rectangles, arrows and adding konvajs events.
Create HTML file
Create a basic html file with the following code. Add <script src=“https://unpkg.com/[email protected]/konva.min.js”></script> in the head section to add konva.js library. Here we have used <div id=‘container’></div> to render html canvas using konva.js library. This section will load an image and have drawing functions such as Add Rectangle, Add Arrow, Add Point. Finally <dialog id=‘dialog’> will show a dialog to write comments for each shape.
<!doctype html>
<html>
<head>
<script src="https://unpkg.com/[email protected]/konva.min.js"></script>
<meta charset="utf-8" />
<title>Image Annotation</title>
<style>
#dialog {
z-index: 100;
}
</style>
</head>
<body>
<div id="container"></div>
<button onclick="activateDrawMode()">Add Rectangle</button>
<button onclick="activateArrowMode()">Add Arrow</button>
<button onclick="activatePointMode()">Add Point</button>
<button onclick="activateMoveMode()">Move (Pan)</button>
<dialog id="dialog">
<p>Enter Comment</p>
<input type="text" id="dialog-input" />
<form method="dialog">
<button>OK</button>
</form>
</dialog>
</body>
<script>
// konva js code
</script>
</html>
Add Konva functionalities
Let’s write some konvajs code in the <script></script> tag to implement image loading, drawing objects, and event functionalities. I will break down this section into the following parts
Create Konva stage
Create konva stage to with fixed width and height for the canvas area.
const width = 700
const height = 500
var stage = new Konva.Stage({
container: 'container',
width,
height
})
var layer = new Konva.Layer()
stage.add(layer)
Add background image
Add a background image for the Konva stage. The shapes will be drawn over the image to add comments on the objects inside the image.
var backgroundImage = new Image()
backgroundImage.src = 'https://cdn.pixabay.com/photo/2016/02/17/15/37/laptop-1205256_1280.jpg' // URL of your background image
backgroundImage.onload = function () {
var background = new Konva.Image({
image: backgroundImage,
width,
height
})
layer.add(background)
layer.batchDraw()
}
Define variables
These variables will hold different values while the konva stage is rendered.
- isDrawing will hold a boolean value to specify whether the drawing mode is active or not
- currentShape : This variable will display the index of the current shape that has been drawn, the starting index is 1
- rect, arrow, point : these variables will hold the konva object values according to the name
- commentText will hold the comment value of the currently active shape
- canvasMode will store the current canvas modes of drawing such as free, rectangle, arrow, or point.
var isDrawing = false
var currentShape = 1
var rect, arrow, point
var commentText
var canvasMode = 'Free'
Add button functions
These are the button click events that active the mode of canvas according to the button clicked and clearObjects() is called to set the variable data to null so that we that shape is selected again, it will not redraw the old object.
function activateDrawMode() {
// Enable rectangle drawing mode
clearObjects() // Reset the comment text object
canvasMode = 'Rect'
}
function activateMoveMode() {
canvasMode = 'Move'
// Disable rectangle drawing mode
isDrawing = false
}
function activateArrowMode() {
clearObjects()
canvasMode = 'Arrow'
}
function activatePointMode() {
clearObjects()
canvasMode = 'Point'
}
function clearObjects() {
isDrawing = true
rect = null
commentText = null
arrow = null
}
Define Draw functions
These are the core Konva’s functions to define the shapes by using the konva api. I have used Konva.Circle to add a dot shape, Konva.Arrow to draw the arrow and Konva.Rect to draw the rectangle.
function addPoint(e) {
var touchPos = stage.getPointerPosition()
var circle = new Konva.Circle({
x: touchPos.x,
y: touchPos.y,
radius: 3,
fill: 'blue',
draggable: true
})
circle.setAttr('label', currentShape)
addLabel(currentShape, touchPos.x, touchPos.y - 10, 'new', 'blue')
layer.add(circle)
currentShape++
}
function addArrow(e) {
arrow = new Konva.Arrow({
points: [0, 0, 0, 0], // Initialize with zero-length arrow
pointerLength: 20,
pointerWidth: 20,
fill: 'green',
stroke: 'green',
strokeWidth: 2,
draggable: true
})
var touchPos = stage.getPointerPosition()
arrow.points([touchPos.x, touchPos.y, touchPos.x, touchPos.y])
addLabel(currentShape, touchPos.x, touchPos.y - 10, 'new', 'green')
arrow.setAttr('comment', 'comment')
arrow.setAttr('label', currentShape)
layer.add(arrow)
arrow.on('dblclick dbltap', (e) => showModel(e.target))
currentShape++
}
function addReactangle(e) {
const pos = stage.getPointerPosition()
rect = new Konva.Rect({
x: pos.x,
y: pos.y,
stroke: 'red',
strokeWidth: 2,
draggable: true
})
rect.setAttr('comment', 'Comment')
rect.setAttr('label', currentShape)
addLabel(currentShape, pos.x, pos.y - 10, 'new', 'red')
layer.add(rect)
currentShape++
rect.on('dblclick dbltap', (e) => showModel(e.target))
}
Add Draw functions with mouse events
The actual drawing functionality will be implemented by the following mouse events. When the events such as mousedown, dragmove, mousemove, mouseup events occurred. The Konva library will execute the drawing function for the shape according to the variable data being add for the currently active mode.
stage.on('mousedown', (e) => {
if (isDrawing) {
if (canvasMode == 'Rect') addReactangle(e)
else if (canvasMode == 'Arrow') addArrow(e)
else if (canvasMode == 'Point') addPoint(e)
layer.draw()
}
})
stage.on('dragmove', (e) => {
const cn = e.target.className
var touchPos = stage.getPointerPosition()
if (e.target.getAttr('label')) {
if (cn == 'Arrow')
addLabel(
e.target.getAttr('label'),
e.target.points()[0] + e.target.x(),
e.target.points()[1] - 10 + e.target.y(),
'update'
)
else addLabel(e.target.getAttr('label'), e.target.x(), e.target.y() - 10, 'update')
}
})
stage.on('mousemove', (e) => {
const pos = stage.getPointerPosition()
if (isDrawing) {
if (canvasMode == 'Rect' && rect) {
rect.width(pos.x - rect.x())
rect.height(pos.y - rect.y())
} else if (canvasMode == 'Arrow' && arrow) {
arrow.points([arrow.points()[0], arrow.points()[1], pos.x, pos.y])
}
layer.batchDraw()
}
})
stage.on('mouseup', (e) => {
if (isDrawing) {
if (rect) showModel(rect)
else if (arrow) showModel(arrow)
isDrawing = false
}
})
Show comment model
When The drawing events i.e. mouseup event is called. We need to display a dialog to enter the comment value so that will can write the label for each object. We will be using virtual DOM to show the dialog. addLable function adds the label using DOM. The appended child from this code will display the HTML code over the Konva stage by using absolute positioning.
function showModel(object) {
var paragraph = document.createElement('p')
var dialog = document.createElement('dialog')
var btn = document.createElement('button')
var input = document.createElement('input')
input.value = object.getAttr('comment') ?? ''
paragraph.textContent = 'Comment'
btn.textContent = 'Ok'
dialog.appendChild(btn)
dialog.insertBefore(paragraph, dialog.firstChild)
dialog.insertBefore(input, paragraph.nextSibling)
btn.addEventListener('click', function () {
object.setAttr('comment', input.value)
document.body.removeChild(dialog)
})
document.body.appendChild(dialog)
dialog.showModal()
}
function addLabel(label, left, top, type, color = 'red') {
var span = document.createElement('div')
if (type == 'update') {
var elm = document.querySelector('#label-' + label)
if (left && top) {
elm.style.left = left + 'px'
elm.style.top = top + 'px'
}
return
} else if (type == 'new') {
span.style.position = 'absolute'
span.style.left = left + 'px'
span.style.top = top + 'px'
span.style.color = color
span.id = 'label-' + label
span.innerHTML = label
document.body.appendChild(span)
}
}
Edit Comment
You can also edit the comments of each marked object on the image by adding a double-click functionality on the shape to edit the comment values. This function will create an input tag virtually temporarily and it will be removed from the DOM after saving the comment value.
function doubleClickRect(object, points) {
var input = document.createElement('input')
input.type = 'text'
input.style.position = 'absolute'
input.style.left = `${points ? points.x : object.x()}px`
input.style.top = `${points ? points.y : object.y()}px`
input.value = object.getAttr('comment')
document.body.appendChild(input)
input.focus()
input.addEventListener('blur', () => {
object.setAttr('comment', input.value)
try {
// input.removeEventListener('blur')
document.body.removeChild(input)
} catch (error) {
console.log()
}
layer.draw()
})
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
layer.draw()
try {
document.body.removeChild(input)
} catch (error) {
console.log()
}
}
})
}