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.</li< />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;
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;
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()
{
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();
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);
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);
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;
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