Manipulating HTML5 Canvas Using Konva: Part 5, Events

If you have been following this series from the beginning, you should now be very comfortable with shapes, groups, and layers. You should also be able to easily draw some basic and complex shapes on the canvas using Konva. If you plan on using Konva to create some interactive app or games, learning how to bind events to different shapes on the stage is the next logical step.

Manipulating HTML5 Canvas Using Konva: Part 5, Events

In this tutorial, you will learn how to bind events to any shape using Konva. You will also learn about event delegation and propagation. Sometimes, you need might need to control the hit region of a shape as well as fire events programmatically. We will be discussing these two topics as well.

Binding Events to a Shape

You can bind different events to any shape created using Konva with the help of the on() method. All you have to do is pass the name of the event as the first parameter and the function to be executed when the event occurs as the second parameter. You can use Konva to detect mouseup, mousedown, mouseenter, mouseleave, mouseover, mousemove, click, and dblclick. Additionally, Konva allows you to detect wheel, dragstart, dragmove, and dragend events.

Here is an example that detects mousedown and mouseleave events on a regular polygon (hexagon). Similarly, the smaller circle is bound to the mouseover and mouseup events and the larger circle is bound to the mouseenter, mouseleave, and mousemove events.

var canvasWidth = 600;
var canvasHeight = 400;

var stage = new Konva.Stage({
  container: "example",
  width: canvasWidth,
  height: canvasHeight
});

var layerA = new Konva.Layer();

var polyA = new Konva.RegularPolygon({
  x: 125,
  y: 125,
  sides: 6,
  radius: 80,
  fill: "yellow",
  stroke: "black",
  strokeWidth: 5
});

var circA = new Konva.Circle({
  x: 275,
  y: 225,
  height: 100,
  fill: "orange",
  stroke: "black"
});

var circB = new Konva.Circle({
  x: 475,
  y: 275,
  radius: 100,
  fill: "red",
  stroke: "black"
});

layerA.add(polyA, circA, circB);

stage.add(layerA);

polyA.on("mousedown", function() {
  polyA.sides(polyA.sides() + 1);
  layerA.draw();
});

polyA.on("mouseleave", function() {
  var totalSides = polyA.sides();
  if(totalSides > 3) {
    polyA.sides(polyA.sides() - 1);
  }
  layerA.draw();
});

circA.on("mouseover", function() {
  circA.strokeWidth(10);
  layerA.draw();
});

circA.on("mouseup", function() {
  circA.strokeWidth(5);
  layerA.draw();
});

circB.on("mouseenter", function() {
  stage.container().style.cursor = "crosshair";
});

circB.on("mouseleave", function() {
  stage.container().style.cursor = "default";
});

circB.on("mousemove", function() {
  var pointerPos = stage.getPointerPosition();
  var r = pointerPos.x % 255;
  var g = pointerPos.y % 255;
  circB.fill("rgb(" + r + ", " + g + ", 100)");
  layerA.draw();
});

If a user presses any mouse button while the cursor is inside the regular polygon, we increase the number of sides of the polygon by 1. The sides() method can be used without a parameter to get the number of sides for a polygon or used with one parameter to set the number of sides for a polygon. You can also get the number of sides using getSides() and set the number of sides using setSides(). The sides of the polygon are reduced by one whenever the mouse cursor leaves the polygon.

For the smaller circle, the mouseover event is used to set the stroke width value to 10. The mouseup event changes the stroke width value to 5. Keep in mind that the mouseup event has to occur inside the circle itself. For example, the stroke width will not change to 5 if you press the mouse button inside the circle and then release it only after the cursor is outside the circle.

In the case of the larger circle, we are using the mousemove event to change its fill color. We are also changing the cursor of the larger circle using stage.container().style.cursor whenever the cursor moves in and out of the circle.

One important thing that you should keep in mind is that you have to call the draw() method on the respective layer if the event listeners for any shape resulted in a change of attributes like fill color, stroke width, etc. Otherwise, the changes won’t be reflected on the canvas.

You don’t have to bind one event at a time to a shape. You can also pass in a space delimited string containing multiple event types to the on() method. This will bind all the events listed in the string to that particular shape.

Konva also supports corresponding mobile versions of all these events. For example, you can register touchstart, touchmove, touchend, tap, dbltap, dragstart, dragmove, and dragend using Konva on mobile devices.

You can also fire any of these events for a particular shape or shapes using the fire() method. Similarly, Konva allows you to fire custom events like throwStones.

Removing Event Listeners

You can remove any event listeners attached to a shape with the help of the off() method in Konva. You just have to specify the event name that you don’t want to listen to.

You can also create multiple event bindings for a single shape. For example, let’s say you have a circle and you want to increase the radius of the circle every time the mouse cursor goes over it, up to a certain limit. You might also want to change the fill color of the circle on every mouseover event.

One option is to do both these tasks inside a single mouseover event listener and stop updating the radius later. Another option is to create two mouseover event listeners with different namespaces to identify them. This way, you will be able to increase the radius and change the fill color independently.

circA.on("mouseover.radius", function() {
  var curRadius = circA.radius();
  if(curRadius < 150) {
    circA.radius(curRadius + 5);
    layerA.draw();
  } else {
    circA.off('mouseover.radius');
  }
});

circA.on("mouseover.fillcolor", function() {
  var h = Math.floor(Math.random()*360);
  var color = "hsl(" + h + ", 60%, 60%)";
  circA.fill(color);
  layerA.draw();
});

You should note that I have added layerA.draw() inside both listeners. If you fail to add it inside the mouseover.fillcolor listener, the color will stop updating as soon as the radius becomes 150.

Instead of removing one event listener at a time, you can also stop listening to all events bound to a shape using the setListening() method. You can pass true and false to this method in order to turn event listeners on and off. Keep in mind that you will also have to redraw the hit graph of the affected layer by calling the drawHit() method right after you call setListening().

Event Delegation and Propagation

Instead of binding events directly to all the shapes present on a layer, you can also bind an event to the layer itself. After that, you can determine which shape triggered the event using the target property of the event object. This way, Konva allows you to effectively delegate events from the parent to its children.

Let’s say you are listening to click events on a circle drawn on a layer in Konva. The same click event propagates to the containing group as well as the containing layer. This may or may not be the intended behavior. If you want to prevent the event from bubbling up inside a shape to the containing layer, you can set the cancelBubble property for the event object to true.

var canvasWidth = 600;
var canvasHeight = 400;

var stage = new Konva.Stage({
  container: "example",
  width: canvasWidth,
  height: canvasHeight
});

var layerA = new Konva.Layer();

var circA = new Konva.Circle({
  x: 300,
  y: 200,
  height: 100,
  fill: "orange",
  stroke: "black",
  name: "Orange Circle"
});

var starA = new Konva.Star({
  x: 125,
  y: 125,
  innerRadius: 25,
  outerRadius: 75,
  rotation: 90,
  fill: "blue",
  stroke: "black",
  name: "Blue Star"
});

var ringA = new Konva.Ring({
  x: 475,
  y: 275,
  innerRadius: 25,
  outerRadius: 75,
  fill: "brown",
  stroke: "black",
  name: "Brown Ring"
});

var textA = new Konva.Text({
  text: "",
  fontFamily: "Calibri",
  fontSize: 24,
  fill: "black",
  x: 10,
  y: 10
});

layerA.add(circA, starA, ringA, textA);

stage.add(layerA);

layerA.on("click", function(e) {
  var shapeName = e.target.attrs.name;
  textA.setText(shapeName);
  layerA.draw();
});

I have used the name property to assign a name to each of our shapes. The setText() method is then used to change the text inside textA to the name of the shape we just clicked.

Custom Hit Regions

In the above example, the ring registered a click on it when the click occurred between the inner and outer circle. What if you wanted to register the click inside the smaller circle as well? Konva allows you to define custom hit regions using the hitFunc property. This property accepts a function as its value, and this function is used to draw the custom hit region.

The following example shows you how to create custom hit regions. You should now be able to click in the area between the star spikes and still register a click. With the help of custom hit regions, you can make sure that your users don’t have to click on exact locations to register a click event. This can result in a better user experience when dealing with smaller or more complex shapes.

var starA = new Konva.Star({
  x: 125,
  y: 125,
  innerRadius: 25,
  outerRadius: 75,
  rotation: 90,
  fill: "blue",
  stroke: "black",
  name: "Blue Star",
  hitFunc: function(context) {
    context.beginPath();
    context.arc(0, 0, this.getOuterRadius(), 0, Math.PI * 2, true);
    context.closePath();
    context.fillStrokeShape(this);
  }
});

var ringA = new Konva.Ring({
  x: 475,
  y: 275,
  innerRadius: 25,
  outerRadius: 75,
  fill: "brown",
  stroke: "black",
  name: "Brown Ring",
  hitFunc: function(context) {
    context.beginPath();
    context.arc(0, 0, this.getOuterRadius(), 0, Math.PI * 2, true);
    context.closePath();
    context.fillStrokeShape(this);
  }
});

Final Thoughts

In this tutorial, we have covered different mobile and desktop events that you can bind to any shape in Konva. You can attach these events one at a time or many of them at once. Konva also allows you to fire your own custom events programmatically using the fire() method. The last section of the tutorial showed you how to define your own hit regions in order to detect hits on an area that could be larger or smaller than the original shape.

Combining the knowledge of this tutorial with others in the series, you should now be able to draw a variety of shapes on the canvas and allow your users to interact with them.

If you have any questions related to this tutorial, feel free to let me know in the comments.