Canvasser

One of the fiddly things about using Html5 canvas is dragging and dropping. It's something I needed to do a fair bit, so I thought I'd write a configurable function to do it. You can use it with Vanilla JavaScript web apps, with Apps Script HTMLService in an Add-on or as a webapp - click here to execute that, or as in the demo in this page, a Google Gadget.

The first app I've written using an early version of this is the Color Arranger Apps Script Add-on, but I'm expecting to be able to use this in a few more things I have in the pipeline.

You can find the code for the Apps Script version, plain JavaScript, or as a gadget here on github.

Here's how it works.

Canvasser

This is used to manage canvas operations for a collection of shapes. It provides default mouse, dragging and animation functions and events (all of which can be customized) for dealing with the dragging, dropping and painting of shapes on a canvas. This first version only supports rectangular shapes, but I'll be adding more stuff as I use it for more apps.

In addition to x,y co-ordinates relative to the canvas, it also supports a z level for layers (what goes in front).

The purpose is to manage all the mouse and shape coordination to generate a smooth drag and drop experience. Below is an apps script webapp using canvasser with some shapes to play around with.

Let's work through the example.

Initialization

It needs the element containing the canvas it's supposed to work on.

var cv = new Canvasser (document.getElementById("canvas"));

Adding shapes

Canvasser keeps track of all the shapes plotted on it with CanvasserShape objects. A new, default shape is created like this.

cv.addShape();

The default shape template object looks like this. Each shape that is added will use these values as the default. An example is the black shape in the top left corner of the demo.

ITEM: {

shape: {

x:0,

y:0,

z:self.ENUMS.LAYERS.ZINDEX,

height: 50,

width: 50,

color: 'black',

border: {

width: 2,

color: 'gray'

},

text: {

content:'',

font: '10px sans-serif',

textAlign:'center',

textBaseline:'alphabetic',

direction:'inherit',

fillStyle:'white'

},

type:self.ENUMS.TYPES.RECTANGLE, // for now only do rectangles .. will add an enum for later

center:false, // whether the co-ords apply to the center (normally top left) TODO

visible:true, // whether to display TODO

draggable:true,// whether item should be considered for dragging TODO

mode:self.ENUMS.MODES.PLACE, // how to handle a dragged item

dragz:self.ENUMS.LAYERS.ONTOP

},

data:{ // this can be used to carry around any user required properties

}

Shape parameters

Any of the above can be changed by passing an example object to the CanvasserShape constructor. Here's the green and yellow shapes in the demo.

// some modifications

cv.addShape({shape:{ x:20, y:10 , width:20, color:'green', z:150 }});

cv.addShape({shape:{ x:40, y:60 , width:30, color:'yellow', z:50 }});

Events

There are a number of specific events that can be monitored. Many events can be global (apply to all shapes), or can be tailored for specific shapes. We'll dig into that a little more later, but for now, let's say I want to provoke a callback when the mouse enters a new shape, or exits an old one.

In this case I want to turn an element on my web page 'block' , to the same color as the shape I'm entering, or gray if I'm leaving.

var block = document.getElementById("block");

// when we are not over any shape, turn them all gray

cv.onShapeExit = function(cs) {

block.style.background = 'gray' ;

};

// when we enter a shape, turn box the same color as the shape

cv.onShapeEnter = function(cs) {

block.style.background = cs.item.shape.color ;

};

Try on the demo moving the mouse over various shapes

Copying properties from other shapes

You may want to use one shape as the template for another. In this case, I'm creating the blue shape and copying it to make the pink shape

// some data in here

var blue = cv.addShape({

shape:{x:90, y:30,width:60,height:60,color:'blue',z:50},

data: {block:'blueblock',something:'blue data'}

});

// can copy another

var pink = cv.addShape(blue.item.shape);

and then make a few minor adjustments directly

// can tweak it directly

pink.item.shape.x += pink.item.shape.width;

pink.item.shape.color = "pink";

// can assign the data directly

pink.item.data.block = 'pinkblock';

pink.item.data.something = 'pink data';

The data property

In the blue/pink example you'll notice that the CanvasserShape is initialized with a shape property, plus a data property. The shape property describes the shape's characteristics, but the data property is provided as a convenient container for whatever data you want to associate with a shape. This means that when you get a callback where the CanvasserShape is passed to you, any data that it carries will be accessible too.

onDragEnd event

Each shape can have its own personal callback for a dragEnd (or dragStart) that can override the default dragEnd behavior. In this case we want to fill a couple of elements (defined earlier in the data property of the pink and blue shapes) with some data associated with each shape.

onDragEnd callback is called with 2 arguments.

shape - this is the CanvasserShape that's being dragged

isOver - this is the CanvassertShape it was positioned over when it was dropped. It may be null if there was no shape in the drop position.

// exchange data when drag ends

blue.onDragEnd = function (shape, isOver) {

// the element for the dragged item

var block = document.getElementById(shape.item.data.block);

block.innerHTML = shape.item.data.something;

if (isOver && isOver.item.data) {

//then we are positioned over another shape

var blockOver = document.getElementById(isOver.item.data.block);

if(blockOver) {

blockOver.innerHTML = isOver.item.data.something;

block.innerHTML += "/" + isOver.item.data.something;

}

}

};

// pink will use the same

pink.onDragEnd = blue.onDragEnd;

In this example both the pink and blue shapes now have the same customized onDragEnd event, whereas the other shapes do not. Play around with dragging the pink and blue shapes in the demo below

Placing in new position

When a shape is dropped in a new position, there are a number of positioning modes. These have been identified so far

PLACE - the item is dropped where it lands, and no other shape is affected

SWAP - the item swaps position with the one it's now over

SHUFFLE - the item is inserted in the drop position and the subsequent items are all pushed down to make room - this is not yet implemented, but is in progress

By default, items use the PLACE mode. Here's an example of selecting the SWAP mode

// how to do text, and to use the swap position rather than place

cv.addShape({shape:{x:100,y:100,color:"indigo",width:60,height:40,mode:cv.ENUMS.MODES.SWAP,text:{content:'i will swap'}}});

Text

There is a primitive text capability, which allows you to label shapes. See the default item earlier for the properties supported

Z position of dragged items

By default a dragged items Z position will be changed when dropped to put it on the top layer. However you can configure a shape to retain its original position after being dragged as in the lemonchiffon example above, or you can specify a specific Z position.

Retain current Z position - dragz : cv.ENUMS.LAYERS.UNCHANGED

Move on top - dragz : cv.ENUMS.LAYERS.ONTOP

Use a specific z position - dragz : 23

Play around with the lemonchiffon and indigo shapes to see the SWAP and UNCHANGED behaviors in action.