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

  cv.addShape({shape:{x:150,y:110,z:10,color:"lemonchiffon",width:60,height:40,dragz:cv.ENUMS.LAYERS.UNCHANGED,
     text:{fillStyle:'black',content:'i keep z pos'}}});

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.

For more like this, see  Google Apps Scripts snippets. Why not join our forum,follow the blog or follow me on twitter to ensure you get updates when they are available. 

You want to learn Google Apps Script?

Learning Apps Script, (and transitioning from VBA) are covered comprehensively in my my book, Going Gas - from VBA to Apps script, available All formats are available now from O'Reilly,Amazon and all good bookshops. You can also read a preview on O'Reilly

If you prefer Video style learning I also have two courses available. also published by O'Reilly.
Google Apps Script for Developers and Google Apps Script for Beginners.





Comments