Category Archives: HTML5

Propagating events through element layers as rendered in page

I am currently working on a project involving Mapbox and Leaflet.  As part of a performance enhancement feature I was implementing I came across an interesting issue on event handling in the map layers.

I am using D3 to implement a light-weight custom Leaflet layer to better handle large FeatureCollections.  From the server I receive a GeoJSON FeatureCollection of features that includes shapes, markers, and text that I need to render to the map.  In my custom layer the different feature types in the collection need to be placed into different map panes (overlay, markers, and popup panes) to maintain the proper layering.  These “panes” are implemented as sibling DIVs as oppose to being in a tree layout.

Depending on the purpose of the map, any one of these layers can be enabled to accept click events (and possibly more than one).  The issue I encountered was that only the top layer would actually accept the click — this layer typically being the marker or text.  If that layer was deemed “un-clickable” then nothing would happen because the event propagated up the DOM tree and not to the sibling “panes.”

What I need to handle is passing this event to all of the sibling panes to see who could handle it.  But I cannot just broadcast it!  I need to pass it to the panes in the order they are rendered on the page and only to each pane’s feature located at the exact mouse-click position.

Some background…

In mapbox maps there is an overlay pane for adding new overlay shapes, like polygons.  There is also the marker pane, for things like “pins” on point locations.  There are still other panes, like “popups” for additional items.  Because these panes are sibling divs and not hierarchical event handling can be tricky.

The FeatureCollection I receive can contain features mapping to 1..n of these panes.  So what I can end up with is multiple overlapping features on a map — e.g., a shape, a marker (or more) on that shape, and a text label on that marker.  These features know nothing about their layer or other features, but do contain information in their properties so  I can determine if the feature is clickable.

Because the layers are actually laid out in the DOM as absolute-positioned sibling ‘div’ elements I cannot use the browser’s event propagation.   What I need for my situation is information about the stack of elements as layered on the page by the renderer at the X/Y point of the mouse click.  An example of what i mean by the “layers” of the DOM elements is nicely displayed in this picture:

Layers on the google home page.
Element layers as rendered on the google home page.

I took this screen shot using a cool firefox plugin called Tilt, although I found out after I installed it that the firefox dev tools already has it built in as “3D View.”   Because everyone is familiar with it, I used Google’s homepage to illustrate stacked DOM element layers as I am working with such layers on the map.  The actual map did not illustrate this as well since the Tilt plugin was not splitting out SVG trees into layers.

To propagate the event the way  I need to do it, I need to dig through these layers top-to-bottom at the coordinates of the mouse click regardless of how these layers are represented in the DOM tree — CSS can move them anywhere.  Its like driving a skewer through the page at the point of click and then peeling back the shish kabob of features until i get to the one that can process the event.

A solution emerges…

It turns out there is a very useful document function for this called document.elementFromPoint(x,y).  This function will take an X/Y coordinate and return the element first encountered at those coordinates.  This is great, but I already know the first encountered element… it’s the one in the event that is triggered when the mouse click occurs.  So this alone does not help much, especially since it is only available on document and not sub-elements in order to process sub-trees.  But, it is a start…

So digging deeper I found that I could manipulate the results of the elementFromPoint function using very simple CSS!  When an element has the style “pointer-events: none,” click events pass through the element to what ever is next behind it.  This style alone could solve this issue sometimes if you are able to throw it around multiple places, but in the case of creating a custom layer I need to be more surgical so i don’t affect other layers that may be added to the map.  When this style is on an element, it is also skipped by the elementFromPoint function!  

Aside: Another page on quirksmode says you can do this with “display: none” as well, but that would change the page visually…  

A happy merging of these two ideas and a dose of recursion solves the problem.  All of the features I add to the page have a click handler that routes the click to a special function that does the following:

  1. Get the elementFromPoint from the X/Y coordinates in the mouse event.
  2. Check if this element is
    1. one of my features, which means it contains data (i.e., a “__data__” property) and has a key/value attribute I put on the element specifically for matching.
    2. is clickable (a separate function to determine this based on whatever…)
    3. is a child of some baseline element that I know all of my elements are inside.  This is to fail faster instead of parsing a lot of the page I don’t care about.  The base element for me, inside the mapbox map, is an element classed with ‘.leaflet-objects-pane’.
  3. If it all tests in #2 pass, return this element and pass it and it’s data (remember in D3 this is the element.__data__) off to the real click handler.
  4. If the tests in #2 fail, either return null (if i’m outside of my base element) or add the “pointer-events: none” style to the current element and call this function again recursively.   The current element is added to an array of “passed over elements” so that we can come back later and remove this style.  In order to not break the page, maintain the prior value of this style if there was one.

This loop continues until we either find an element that will handle the event or we are outside of the base element.

The last important step before returning from this function is to iterate over the “passed over elements” array and revert the “pointer-events: none” style.

Implementation
Here is the implementation as a member in the custom layer class, a little generalized for clarity.

        _digForElements: function keepDigging(evt, passedOverElementsFromLast) {
            var layer = this,
                  jMapPane = jQuery('.leaflet-objects-pane');
            if (!jMapPane) return null;

            var baseElement = jMapPane[0]
                attr = 'data-d3layerkey',
                attrVal = layer._layerId,
                passedOverElements = passedOverElementsFromLast || [],
                firstFound,
                element,
                jElement,
                existingCssValue;

            element = document.elementFromPoint(evt.x, evt.y);
            if (element && jQuery.contains(baseElement, element)) {
                jElement = jQuery(element);

                if (jElement.attr(attr) && jElement.attr(attr) === attrVal &&
                    _.isObject(element.__data__) && layer.options.isSeriesClickable(element.__data__)) {
                    firstFound = element;
                } else {
                    existingCssValue = jElement.css('pointer-events');
                    passedOverElements.push({
                        element: element,
                        previousCSSValue: existingCssValue === 'auto' ? '' : existingCssValue;
                    });

                    // Add style to it so that elementFromPoint bypasses it next time.
                    jElement.css({
                        'pointer-events': 'none'
                    });

                    firstFound = keepDigging.call(this, evt, passedOverElements);
                }
            }

            // clean up all elements we added classes to.  Also, end a sentence with a preposition.  done.
            var info;
            while (passedOverElements.length > 0) {
                // It did not have it before or it would not have come up in the search...
                // so remove it again.
                info = passedOverElements.pop();
                jQuery(info.element).css({
                    'pointer-events': info.previousCSSValue
                });
            }

            return firstFound;
        },
Share

HTML5 Canvas: scrolling background and selecting/dragging shapes

I have been wanting to do this for a while and just had a chance to mess around with it this week.  I also had more motivation after spending Saturday in an “Designing (for) Interactions” workshop by Dan Mall, and hosted by RefreshPGH.  The key concepts were HTML5, CSS3, some jQuery, and generally getting some good experience on how people in the real world design web sites.  ( I really liked the boilerplate website, font squirrel, and of course A List Apart articles! )

Anyway, the thing I have been wanting to mess around with is the canvas.  I have recently been working on some mobile application prototypes and I am getting familiar with some of the components that exist there.  I wanted to see if I could put together a canvas demo to replicate a ScrollView, as well as being able to throw stuff in and manipulate it.  So here’s my example,with a little explanation.  It is a little hacked together because I put it together pretty quickly, so don’t grade on neatness and optimization —  many of the functions and comments are still very much what Simon Sarris wrote and I pieced things in around his code.  There are also bits and pieces of example code used from multiple places, like here.

The Details…

I started out with the code that Simon Sarris wrote for selectable shapes in a canvas.  After looking and understanding what he was doing, I refactored the code to be object oriented so it was more expandable.  My Goal: Understand the code well enough that I can edit it, make it expandable, and experiment with creating the “ScrollView” effect that I see on mobile devices.

Here is the canvas tag I used:

<canvas id="canvas_clickAndDragOO" width="400" height="300" style="border: 1px black solid;">
   This text is displayed if your browser does not support HTML5 Canvas.
</canvas>

The manipulation of the shapes is exactly how Simon wrote it, so check out his page for that explanation.  The main thing I added was the scrollable content.

Key Points:

  • I have two main things in the javascript:  a Canvas class and Shapes.  The shapes are pretty basic, so the Canvas class is the most interesting.  It maintains two canvas elements: one for the content and one for the view.  The content canvas is never added to the view and is only maintained behind the scenes as a javascript variable.  Everything is written to that content canvas and the context drawImage method is used to send a section of it to the viewable canvas.
  • To make the view ‘scroll,’ an X and Y value are maintained to specify the coordinates of the top left corner of the viewable canvas on the content canvas.  When the background of the canvas is clicked and dragged, this X and Y value are updated.  The draw method simply reacts to these new values and sends an new section of the content canvas out to the viewable canvas.  The viewable canvas is simply a window to a section of the content canvas.

The “content canvas” (hidden) is automatically created twice the size of the “viewable canvas” for this demo, but this and other things are configurable if you look at the Canvas object in the code.  Just include this in a webpage via a script tag and it should work with the canvas above.  (UPDATE: I pasted this here so it could be read and copied easier)

UPDATE 2:  This code doesn’t work on < IE8 using the excanvas.js library because that library does not support the getImageData function!  I would have to find a way around that in order for it to be backwards compatible.  Also, older IE browsers do not support the pageX and pageY properties on the mouse events. This issue can be solved quickly by looking here (search the page for “pageX”).

UPDATE 3: Here is a link to a zip file demo.  Unzip it and open index.html in your browser.  I’ll add it to run here at some point..

UPDATE 4:  Not sure why I did not do this earlier… but here is is now running

/*
 * My object oriented version of click and drag
 *
 */

function Canvas( data ) {
  data = data || {};
  var self = this;

  this.canvasID = data.canvasID || 'canvas';
  this.viewableCanvas = document.getElementById(this.canvasID);
  this.vctx = data.canvasContext || this.viewableCanvas.getContext('2d');
  // set our events. Up and down are for dragging,
  // double click is for making new boxes
  this.viewableCanvas.onmousedown = function(e){ Canvas.prototype.mouseDownAction.call( self, e ) };
  this.viewableCanvas.onmouseup = function(e){ Canvas.prototype.mouseUpAction.call( self, e ) };
  this.viewableCanvas.onmouseout = function(e){ Canvas.prototype.mouseUpAction.call( self, e ) };
  this.viewableCanvas.ondblclick = function(e){ Canvas.prototype.mouseDblClickAction.call( self, e ) };

  this.isValid = Canvas.IS_INVALID;

  this.canvasContentObjectsList = [];

  // we use a content canvas to draw individual shapes.  This is larger than the viewable canvas.  
  // only a section of this is passed to the viewable canvas
  var contentCanvasHeight = data.contentCanvasHeight || this.viewableCanvas.height * 2;
  if (contentCanvasHeight < this.viewableCanvas.height) { contentCanvasHeight = this.viewableCanvas.height }
  var contentCanvasWidth = data.contentCanvasWidth || this.viewableCanvas.width * 2;
  if (contentCanvasWidth < this.viewableCanvas.width) { contentCanvasWidth = this.viewableCanvas.width }
  this.contentCanvas = document.createElement('canvas');
  this.contentCanvas.height = contentCanvasHeight;
  this.contentCanvas.width = contentCanvasWidth;
  this.cctx = this.contentCanvas.getContext('2d'); // content context
  // These X and Y values are the top left corner of the "viewable window" of this content canvas
  this.ccViewX = Math.max( (contentCanvasWidth/2) - (this.viewableCanvas.width/2), 0 );
  this.ccViewY = Math.max( (contentCanvasHeight/2) - (this.viewableCanvas.height/2), 0 );

  // Max values for the ccView X and Y vals
  this.ccViewXMax = this.cctx.canvas.width - this.vctx.canvas.width;
  this.ccViewYMax = this.cctx.canvas.height - this.vctx.canvas.height;

  this.isContentObjectDragAction = false;
  this.isContentCanvasDragAction = false;

  // the X and Y coordinate for when the mousedown event is triggered
  this.contentCanvasDragStartX = 0;
  this.contentCanvasDragStartY = 0;

    //fixes a problem where double clicking causes text to get selected on the canvas
  this.viewableCanvas.onselectstart = function () { return false; }

  // fixes mouse co-ordinate problems when there's a border or padding
  // see getMouse for more detail
  this.stylePaddingLeft = 0;
  this.stylePaddingTop = 0;
  this.styleBorderLeft = 0;
  this.styleBorderTop = 0;
  if (document.defaultView && document.defaultView.getComputedStyle) {
    this.stylePaddingLeft = parseInt(document.defaultView.getComputedStyle(this.viewableCanvas, null)['paddingLeft'], 10)      || 0;
    this.stylePaddingTop  = parseInt(document.defaultView.getComputedStyle(this.viewableCanvas, null)['paddingTop'], 10)       || 0;
    this.styleBorderLeft  = parseInt(document.defaultView.getComputedStyle(this.viewableCanvas, null)['borderLeftWidth'], 10)  || 0;
    this.styleBorderTop   = parseInt(document.defaultView.getComputedStyle(this.viewableCanvas, null)['borderTopWidth'], 10)   || 0;
  }

// The selection color and width. Right now we have a red selection with a small width
  this.selectedColor = data.selectedColor || '#CC0000';
  this.selectedWidth = data.selectedwidth || 2;

  // since we can drag from anywhere in a node
  // instead of just its x/y corner, we need to save
  // the offset of the mouse when we start dragging.
  this.offsetx = this.ccViewX;
  this.offsety = this.ccViewY;

  this.currentlySelectedContentObject = null;

    // make draw() fire every INTERVAL milliseconds
  setInterval(function() { Canvas.prototype.draw.call( self ) }, 1000/(data.interval?data.interval:40) );
}
// static members
Canvas.IS_VALID = true;
Canvas.IS_INVALID = false;

// create a random number between two ints, inclusive
Canvas.randomFromTo = function( from, to ) {
  return Math.floor(Math.random() * (to - from + 1) + from);
}

//wipes the canvas context
Canvas.prototype.clear = function( ctx ) {
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}

// While draw is called as often as the INTERVAL variable demands,
// It only ever does something if the canvas gets invalidated by our code
Canvas.prototype.draw = function( ) {
  if ( !this.isValid && this.cctx ) {
    //Canvas.prototype.clear.call( this, ctx );
    this.clear( this.cctx );

    // Add stuff you want drawn in the background all the time here
    //Draw a grid to assist in seeing the background scroll
    this.drawGrid();

    // draw all boxes
    var l = this.canvasContentObjectsList.length;
    for (var i = 0; i < l; i++) {
      this.canvasContentObjectsList[i].drawshape({
        ctx: this.cctx
      });
    }

    // draw selection
    // right now this is just a stroke along the edge of the selected box
    var selectedObj = this.currentlySelectedContentObject;
    if (selectedObj != null) {
      selectedObj.highlightBorder({
        ctx: this.cctx,
        selectedColor: this.selectedColor,
        selectedWidth: this.selectedWidth
      });
    }

    // copy section of content canvas out to the viewable canvas
    try {
      this.clear( this.vctx );
      this.vctx.drawImage(
        this.cctx.canvas,
        this.ccViewX,
        this.ccViewY,
        this.viewableCanvas.width,
        this.viewableCanvas.height,
        0,
        0,
        this.viewableCanvas.width,
        this.viewableCanvas.height
      )
    } catch(e) {
      // Do nothing.  a draw error may occur because we are updating this canvas
      // before the cctx.canvas is done drawing.
    }

    // Add stuff you want drawn on top all the time here

    this.isValid = Canvas.IS_VALID;
  }
}

// draw grid lines, but only on the part visible
Canvas.prototype.drawGrid = function( data ) {

  // Draw gridlines to help with scrolling
  for (var x = (this.ccViewX + 0.5); x < (this.ccViewX + this.vctx.canvas.width); x += 40) {
    this.cctx.moveTo(x, 0);
    this.cctx.lineTo(x, this.ccViewY + this.vctx.canvas.height);
  }
  for (var y = (this.ccViewY + 0.5); y < (this.ccViewY + this.vctx.canvas.height); y += 40) {
    this.cctx.moveTo(0, y);
    this.cctx.lineTo(this.ccViewX + this.vctx.canvas.width, y);
  }
  this.cctx.strokeStyle = "#eee";
  this.cctx.stroke();
}

// Sets mx,my to the mouse position relative to the canvas
// unfortunately this can be tricky, we have to worry about padding and borders
Canvas.prototype.getMouseCoordinates = function(e) {
      var element = this.viewableCanvas, offsetX = 0, offsetY = 0;

      if (element.offsetParent) {
        do {
          offsetX += element.offsetLeft;
          offsetY += element.offsetTop;
        } while ((element = element.offsetParent));
      }

      // Add padding and border style widths to offset
      offsetX += this.stylePaddingLeft;
      offsetY += this.stylePaddingTop;

      offsetX += this.styleBorderLeft;
      offsetY += this.styleBorderTop;

      var mx = e.pageX - offsetX + this.ccViewX;
      var my = e.pageY - offsetY + this.ccViewY;

      // These are the mouse coordinates on the VIEWABLE CANVAS
      return {
        mouseX: mx,
        mouseY: my
      };
}

// Happens when the mouse is clicked in the canvas
Canvas.prototype.mouseDownAction = function(e){

  var self = this;
  var mouseMoveFunction = function(e){ Canvas.prototype.mouseMoveAction.call( self, e ) };

  var mouseCoords = this.getMouseCoordinates(e);
  //Canvas.prototype.clear.call( this, ctx );
  this.clear( this.cctx );
  var l = this.canvasContentObjectsList.length;
  for (var i = l-1; i >= 0; i--) {
    // draw shape onto ghost context
      this.canvasContentObjectsList[i].drawshape({
        ctx: this.cctx,
        fill: 'black'
      });

    // get image data at the mouse x,y pixel
    var imageData = this.cctx.getImageData(mouseCoords.mouseX, mouseCoords.mouseY, 1, 1);
    //var index = (mouseCoords.mouseX + mouseCoords.mouseY * imageData.width) * 4;

    // if the mouse pixel exists, select and break
    if (imageData.data[3] > 0) {
      this.currentlySelectedContentObject = this.canvasContentObjectsList[i];
      this.offsetx = mouseCoords.mouseX - this.currentlySelectedContentObject.x;
      this.offsety = mouseCoords.mouseY - this.currentlySelectedContentObject.y;
      this.currentlySelectedContentObject.x = mouseCoords.mouseX - this.offsetx;
      this.currentlySelectedContentObject.y = mouseCoords.mouseY - this.offsety;
      this.isContentObjectDragAction = true;
      this.viewableCanvas.onmousemove = mouseMoveFunction;
      this.isValid = Canvas.IS_INVALID;
      //Canvas.prototype.clear.call( this, ctx );
      this.clear( this.cctx );
      return;
    }
  }
  // Register a drag action for the whole canvas
  if ( !this.isContentCanvasDragAction ) {
    this.contentCanvasDragStartX = mouseCoords.mouseX;
    this.contentCanvasDragStartY = mouseCoords.mouseY;
    this.originalCCViewX = this.ccViewX;
    this.originalCCViewY = this.ccViewY;
  }
  this.isContentCanvasDragAction = true;
  this.viewableCanvas.onmousemove = mouseMoveFunction;
  // havent returned means we have selected nothing
  this.currentlySelectedContentObject = null;
  // clear the ghost canvas for next time
  //Canvas.prototype.clear.call( this, ctx );
  this.clear( this.cctx );
  // invalidate because we might need the selection border to disappear
  this.isValid = Canvas.IS_INVALID;
}

// Happens when the mouse is moving inside the canvas
Canvas.prototype.mouseMoveAction = function(e){
  var mouseCoords = null;

  if ( this.isContentObjectDragAction ){
    mouseCoords = this.getMouseCoordinates(e);

    this.currentlySelectedContentObject.x = mouseCoords.mouseX - this.offsetx;
    this.currentlySelectedContentObject.y = mouseCoords.mouseY - this.offsety;   

    // something is changing position so we better invalidate the canvas!
    this.isValid = Canvas.IS_INVALID;
  }

  if ( this.isContentCanvasDragAction ) {
    mouseCoords = this.getMouseCoordinates(e);

    xChange = (mouseCoords.mouseX - this.contentCanvasDragStartX);
    yChange = (mouseCoords.mouseY - this.contentCanvasDragStartY);

    // Must move 30 pixels before scrolling.
    if (Math.abs(xChange) > 30 || Math.abs(yChange) > 30) {
      var newX = this.originalCCViewX - xChange;
      newX = newX < 0 ? 0 : newX;
      newX = newX > this.ccViewXMax ? this.ccViewXMax : newX;
      this.ccViewX = newX;

      var newY = this.originalCCViewY - yChange;
      newY = newY < 0 ? 0 : newY;
      newY = newY > this.ccViewYMax ? this.ccViewYMax : newY;
      this.ccViewY = newY;

      this.isValid = Canvas.IS_INVALID;
    }
  }
}

Canvas.prototype.mouseUpAction = function(){
  this.isContentObjectDragAction = false;
  this.isContentCanvasDragAction = false;
  this.viewableCanvas.onmousemove = null;
}

Canvas.prototype.addContentObject = function( obj ) {
  // Verify that this is the correct object type so we have X and Y coordinates

  this.canvasContentObjectsList.push( obj );
  this.isValid = Canvas.IS_INVALID;
}

// adds a new node
Canvas.prototype.mouseDblClickAction = function(e) {
  var mouseCoords = this.getMouseCoordinates(e);
  var randomColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16);

  switch( Canvas.randomFromTo( 1, 3 ) ) {
    case 1:
      this.addContentObject( new Circle({
          x: mouseCoords.mouseX,
          y: mouseCoords.mouseY,
          r: Canvas.randomFromTo( 20, 50 ),
          fill: randomColor
        })
      );
      break;
    case 2:
      this.addContentObject( new Rectangle({
          x: mouseCoords.mouseX,
          y: mouseCoords.mouseY,
          w: Canvas.randomFromTo( 20, 100 ),
          h: Canvas.randomFromTo( 20, 100 ),
          fill: randomColor
        })
      );
      break;
    case 3:
    default:
      this.addContentObject( new Triangle({
          x: mouseCoords.mouseX,
          y: mouseCoords.mouseY,
          w: Canvas.randomFromTo( 20, 100 ),
          h: Canvas.randomFromTo( 20, 100 ),
          fill: randomColor
        })
      );
      break;
  }
}

/***********************************************************
 * Shape super class
 *
 ***********************************************************/

function Shape( data ) {
  data = data || {};

  this.shape = data.shape || 'Shape';
  this.shapeID = data.id || 'Shape';

  this.fill = data.fill || '#444444';

  this.x = data.x;
  this.y = data.y;
}

// Draws a single shape to a single context
// draw() will call this with the normal canvas
// myDown will call this with the ghost canvas
Shape.prototype.drawshape = function( data ) {
    if (!data) return;

  // fill can be overridded if passed in
  data.ctx.fillStyle = data.fill || this.fill;

  // subclass specific
}

Shape.prototype.highlightBorder = function( data ) {
  // Handle the basic, standard highlight things here.
  if (!data) return;

  data.ctx.strokeStyle = data.selectedColor;
  data.ctx.lineWidth = data.selectedWidth;
}

/***********************************************************
 * Rectangle super class
 *
 ***********************************************************/

// Create subclass and reassign constructor to self.
Rectangle.prototype = new Shape();
Rectangle.prototype.constructor = Rectangle;
function Rectangle( data ) {
  data = data || {};

  Shape.call( this, data );

  this.w = data.w;
  this.h = data.h;
}

// Draws a single shape to a single context
// draw() will call this with the normal canvas
// myDown will call this with the ghost canvas
Rectangle.prototype.drawshape = function( data ) {
  Shape.prototype.drawshape.call(this, data); 

  // We can skip the drawing of elements that have moved off the screen:
  if (this.x > data.ctx.canvas.width || this.y > data.ctx.canvas.height) return;
  if (this.x + this.w < 0 || this.y + this.h < 0) return;

  data.ctx.fillRect(this.x,this.y,this.w,this.h);
}

Rectangle.prototype.highlightBorder = function( data ) {
  Shape.prototype.highlightBorder.call(this, data);
  data.ctx.strokeRect(this.x,this.y,this.w,this.h);
}

/***********************************************************
 * Circle super class
 *
 ***********************************************************/

// Create subclass and reassign constructor to self.
Circle.prototype = new Shape();
Circle.prototype.constructor = Circle;
function Circle( data ) {
  data = data || {};

  Shape.call( this, data );

  this.r = data.r; //radius
}

// Draws a single shape to a single context
// draw() will call this with the normal canvas
// myDown will call this with the ghost canvas
Circle.prototype.drawshape = function( data ) {
  Shape.prototype.drawshape.call(this, data); 

  // We can skip the drawing of elements that have moved off the screen:
  if (this.x > data.ctx.canvas.width || this.y > data.ctx.canvas.height) return;
  if (this.x + this.r < 0 || this.y + this.r < 0) return;

  this.drawCircle( data );
}

Circle.prototype.highlightBorder = function( data ) {
  Shape.prototype.highlightBorder.call(this, data); 

  data['borderOnly'] = true;
  this.drawCircle( data );
}

Circle.prototype.drawCircle = function( data ) {
    // Draw a circle using the arc function.
  data.ctx.beginPath();

  // Arguments: x, y, radius, start angle, end angle, anticlockwise
  data.ctx.arc(this.x, this.y, this.r, 0, 360, false);
  if ( !data['borderOnly'] ) {
    data.ctx.fill();
  } else {
    data.ctx.stroke();
  }

  data.ctx.closePath();
}

/***********************************************************
 * Triangle super class
 *
 ***********************************************************/

// Create subclass and reassign constructor to self.
Triangle.prototype = new Shape();
Triangle.prototype.constructor = Triangle;
function Triangle( data ) {
  data = data || {};

  Shape.call( this, data );

  this.w = data.w;
  this.h = data.h;
}

// Draws a single shape to a single context
// draw() will call this with the normal canvas
// myDown will call this with the ghost canvas
Triangle.prototype.drawshape = function( data ) {
  Shape.prototype.drawshape.call(this, data); 

  // We can skip the drawing of elements that have moved off the screen:
  if (this.x > data.ctx.canvas.width || this.y > data.ctx.canvas.height) return;
  if (this.x + this.w < 0 || this.y + this.h < 0) return;

  this.drawTriangle( data );
}

Triangle.prototype.highlightBorder = function( data ) {
  Shape.prototype.highlightBorder.call(this, data);
  data['borderOnly'] = true;
  this.drawTriangle( data );
}

Triangle.prototype.drawTriangle = function( data ) {
  data.ctx.beginPath();
  // Start from the top-left point.
  data.ctx.moveTo( this.x, this.y - (this.h / 2) ); // give the (x,y) coordinates
  data.ctx.lineTo( this.x + (this.w / 2), this.y + (this.h / 2) );
  data.ctx.lineTo( this.x - (this.w / 2), this.y + (this.h / 2) );
  data.ctx.lineTo( this.x, this.y - (this.h / 2) );

  // Done! Now fill the shape, and draw the stroke.
  // Note: your shape will not be visible until you call any of the two methods.
  if ( !data['borderOnly'] ) {
    data.ctx.fill();
  } else {
    data.ctx.stroke();
  }
  data.ctx.closePath();
}

// start it up!
window.onload = function(){
  var OO_canvas = new Canvas( { canvasID: 'canvas_clickAndDragOO' });

  var r1 = new Rectangle({
    x: 800,
    y: 700,
    w: 40,
    h: 70,
    fill: '#FFC02B'
  });
  OO_canvas.addContentObject( r1 );

  var r2 = new Rectangle({
    x: 780,
    y: 780,
    w: 70,
    h: 40,
    fill: '#2BB8FF'
  });
  OO_canvas.addContentObject( r2 );

  var t1 = new Triangle({
    x: 700,
    y: 600,
    w: 70,
    h: 40,
    fill: '#900'
  });
  OO_canvas.addContentObject( t1 );

  var t2 = new Triangle({
    x: 900,
    y: 600,
    w: 70,
    h: 40,
    fill: '#009'
  });
  OO_canvas.addContentObject( t2 );

  var c1 = new Circle({
    x: 675,
    y: 730,
    r: 30,
    fill: '#459'
  });
  OO_canvas.addContentObject( c1 );

  var c2 = new Circle({
    x: 950,
    y: 730,
    r: 30,
    fill: '#954'
  });
  OO_canvas.addContentObject( c2 );
};
Share