Click here to Skip to main content
Click here to Skip to main content

Selecting Multiple Objects with KineticJS

, 21 Nov 2013
Rate this:
Please Sign up or sign in to vote.
The KineticJS libraries make using the HTML5 canvas tag easy and straightforward. This example shows how to

Sample Image - maximum width is 600 pixels

Introduction

The KineticJS libraries make using the HTML5 canvas tag easy and straightforward. This example shows how to use the KineticJS library to create objects on the screen that can be selected by dragging the mouse to create a selection box.

Background

If you are new to the HTML5 canvas tag, please visit HTML5CanvasTutorials.com. If you are new to the KineticJS libraries, click here to learn more about them and how they can make graphics for web browsers a whole new experience. This sample works against version 4.7.4 of the KineticJS library, which can be found here. This article is the first in a series of two that use the KineticJS library to select and manipulate HTML5 canvas objects.

Using the Code

In this example, I am creating three boxes on an HTML5 canvas that I will be able to select and align. As always, KineticJS projects begin by creating a div container:

<div id="container" style="position:absolute;left:0;top:0"></div>

Next, a stage and a layer are created - again this is standard KineticJS practice:

var stage = new Kinetic.Stage({
	container: 'container',
	width: 300,
	height: 500
});
var layer = new Kinetic.Layer();

The first trick to getting this to work is to add a transparent background rectangle. This will allow us to catch the clicks that occur on the background, while still allowing our objects to be dragged and dropped as normal when they are clicked on.

var rectBackground = new Kinetic.Rect({
	x: 0,
	y: 0,
	height: stage.attrs.height,
	width: stage.attrs.width,
	fill: 'transparent',
	draggable: false,
	name: 'rectBackground'
});
layer.add(rectBackground);

Now we will add the blocks to the canvas:

DrawBlocks();
function DrawBlocks()
{
	var x, y, height;
	x = 90;
	y = 10;
	size = 40;
	CreateBlock(x, y, size, size, "green");

	x = 150;
	y = 80;
	CreateBlock(x, y, size + 20, size + 60, "red");

	x = 110;
	y = 170;
	CreateBlock(x, y, size, size, "blue");
	layer.draw();
}

function CreateBlock(x, y, height, width, color)
{
	var grpBlk = new Kinetic.Group({
		x: x,
		y: y,
		height: height,
		width: width,
		name: color,
		draggable: true
	});

	var blk = new Kinetic.Rect({
		x: x,
		y: y,
		height: height,
		width: width,
		fill: color,
		name: color + ' block'
	});
	grpBlk.add(blk);
	blk.setAbsolutePosition(x, y);
	grpBlk.setAbsolutePosition(x, y);
	layer.add(grpBlk);
	return grpBlk;
}

This sample allows you to create a selection box by dragging the mouse across an area of the canvas. To do this, we need a few variables:

var arSelected = new Array();
var bDragging = false;
var bHaveSelBox = false;
var rectSel = null;
var initX = 0;
var initY = 0;
  • arSelected will hold the names of the blocks that have been selected by the user.
  • bDragging prevents re-entrant code while we calculate the size of the box as the mouse is still moving.bHaveSelBox keeps us from drawing the box more than once
  • rectSel is a global to hold the selection box so that we don't have to keep looking it up (faster performance)
  • initX and initY contain the initial position at the moment the mouse is pushed down. This allows us to calculate how big the selection rectangle should be as it is dragged.

Now we need to introduce some event handlers. The first thing we need to capture is a click on our transparent background so that we know to start drawing a selection box.

rectBackground.on("mousedown", function (evt)
{
	bDragging = true;
});

Next, we need to redraw the selection box as the mouse is dragged.

stage.getContent().addEventListener('mousemove', function (e)
{
	if (bDragging)
	{
		SetSelRectPosition(e);
	}
});

var bInHere = false;  //prevents re-entrance in event driven code
function SetSelRectPosition(e)
{
	if (bDragging && !bInHere)
	{
		bInHere = true;
		var canvas = layer.getCanvas();

		var mousepos = stage.getPointerPosition();
		var x = mousepos.x;
		var y = mousepos.y;

		if (!bHaveSelBox)
		{
			initX = x;
			initY = y;
			
			//create the selection rectangle
			rectSel = new Kinetic.Rect({
				x: initX,
				y: initY,
				height: 1,
				width: 1,
				fill: 'transparent',
				stroke: 'black',
				strokeWidth: 1
			});
			layer.add(rectSel);
			layer.draw();
			bHaveSelBox = true;
		}
		else
		{
			var height = 0;
			var width = 0;
			var newX = 0;
			var newY = 0;

			if (x > initX)
				newX = initX;
			else
				newX = x;

			if (y > initY)
				newY = initY;
			else
				newY = y;

			height = Math.abs(Math.abs(y) - Math.abs(initY));
			width = Math.abs(Math.abs(x) - Math.abs(initX));

			rectSel.setHeight(height);
			rectSel.setWidth(width);
			rectSel.setX(newX);
			rectSel.setY(newY);
			layer.draw();
		}
	}
	bInHere = false;
}

When the user lets the mouse up, we need to figure out which items were selected and place their names in the arSelected array so that we can have access to them later. We also need to highlight our boxes so that we can have a visual indication of which items were selected.

stage.getContent().addEventListener('mouseup', function (e)
{
	if (bDragging)
	{
		bDragging = false;

		GetOverlapped();

		if (rectSel != null)
			rectSel.remove();

		rectSel = null;
		bHaveSelBox = false;
		layer.draw();
	}
});

function GetOverlapped()
{
	//bail if there is no rectangle
	if (rectSel == null)
		return;

	var iHeight = 0;
	var iWidth = -1000;

	arSelected.length = 0;

	initX = 10;
	initY = 10;

	var arGroups = layer.getChildren();

	for (i = 0; i < arGroups.length; i++)
	{
		var grp = arGroups[i];

		if (grp.attrs.name != rectSel.attrs.name && 
		grp.attrs.name != rectBackground.attrs.name && grp.attrs.name != 'btn' &&
				grp.attrs.name != 'highlightBlock')
		{
			var pos = rectSel.getAbsolutePosition();

			//get the extents of the selection box
			var selRecXStart = parseInt(pos.x);
			var selRecXEnd = parseInt(pos.x) + parseInt(rectSel.attrs.width);
			var selRecYStart = parseInt(pos.y);
			var selRecYEnd = parseInt(pos.y) + parseInt(rectSel.attrs.height);

			//get the extents of the group to compare to
			var grpXStart = parseInt(grp.attrs.x);
			var grpXEnd = parseInt(grp.attrs.x) + parseInt(grp.attrs.width);
			var grpYStart = parseInt(grp.attrs.y);
			var grpYEnd = parseInt(grp.attrs.y) + parseInt(grp.attrs.height);

			//Are we inside the selction area?
			if ((selRecXStart <= grpXStart && selRecXEnd >= grpXEnd) && 
			(selRecYStart <= grpYStart && selRecYEnd >= grpYEnd))
			{
				if (arSelected.indexOf(grp.getName()) < 0)
				{
					arSelected.push(grp.getName());

					var tmpX = parseInt(grp.attrs.x);
					var tmpY = parseInt(grp.attrs.y);

					var rectHighlight = new Kinetic.Rect({
						x: tmpX,
						y: tmpY,
						height: grp.attrs.height,
						width: grp.attrs.width,
						fill: 'transparent',
						name: 'highlightBlock',
						stroke: '#41d6f3',
						strokeWidth: 3
					});

					layer.add(rectHighlight);
				}
			}
		}
	}
}

Finally, a user would expect that the items will become unselected when the background or an individual item is selected. To make this happen, we add the following:

stage.getContent().addEventListener('mousedown', function (e)
{   
    if(arSelected.length > 0)
    {
        var name = "";

        if (e.shape != undefined)
            name = e.shape.attrs.name;

        if(e.targetNode != undefined)
            name = e.targetNode.attrs.name;
        
		//we don't want to unselect if we are pushing the button
        if (name != 'btn')
        {
            RemoveHighlights();
        }
    }

});

function RemoveHighlights()
{
	var arHighlights = layer.get('.highlightBlock');
	while (arHighlights.length > 0)
	{
		arHighlights[0].remove();
		arHighlights = layer.get('.highlightBlock');
	}
	arSelected.length = 0;
}

At this point, you should be able to give this a run and you should be able to select the items.

If you would like to be able to get a list of the objects selected, add this button to the screen:

x = 85;
y = 250;
var grpGetSelectedButton = CreateButton(x, y, "Get Selected");
grpGetSelectedButton.on("click", function (evt) { ShowSelected(); });

function CreateButton(x, y, text)
{
	var grpButton = new Kinetic.Group({
		x: x,
		y: y,
		height: 30,
		width: 135,
		name: 'btn',
		draggable: true
	});

	var blkButton = new Kinetic.Rect({
		x: x,
		y: y,
		height: 30,
		width: 135,
		fill: 'Violet',
		name: 'btn'
	});

	var txtButton = new Kinetic.Text({
		x: x + 2,
		y: y + 2,
		fontFamily: 'Calibri',
		fontSize: 22,
		text: text,
		fill: 'black',
		name: 'btn'
	});

	grpButton.add(blkButton);
	grpButton.add(txtButton);
	grpButton.setAbsolutePosition(x, y);
	blkButton.setAbsolutePosition(x, y);
	txtButton.setAbsolutePosition(x + 2, y + 2);

	layer.add(grpButton);

	return grpButton;
}

function ShowSelected()
{
	var str = "";
	for (var i = 0; i < arSelected.length; i++)
	{
		str += arSelected[i] + ", ";
	}
	if (str != "")
		str = str.substring(0, str.length - 2);

	alert(str);
}

Points of Interest

It should be noted that the lines where setAbsolutePosition are being called are absolutely necessary. If you just set the x and y, the position will not be the same relative to the stage as the mouse clicks, which will cause you great pain when you are calculating which items fall within the selection box.

The second article in this series can be found here.

You can see a working demo of this project here.

History

  • 21st November, 2013: Initial version

License

This article, along with any associated source code and files, is licensed under The MIT License

About the Author

Michelle Higgins (Harmonia.com)
Team Leader Harmonia
United States United States
I have been coding for something like 20 years now. I worked on factory control systems, call center systems and medical research projects. I've coded in most languages, but am happiest in the .NET world, and even happier if it can be web based development.
Follow on   LinkedIn

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Mobile
Web02 | 2.8.140709.1 | Last Updated 21 Nov 2013
Article Copyright 2013 by Michelle Higgins (Harmonia.com)
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid