A Scalable, Draggable, Anchored Triangle in kinetic.js

10:31 PM , 5 Comments


I don't do a whole bunch on the front end, which is probably my only (weak) explanation as to why I am just starting to play with HTML5 canvas.  However, for the past three nights, I've found myself playing with it and creating stuff that would have blown my (tiny) mind just a few years back. After years of producing rigid web pages of columns and rows, there is nothing quite as liberating as seeing a scalene triangle glide across a web page.  So, you can imagine my glee when I found out that accomplishing that was a simple as:
<html>
  <head>
    <script src="http://www.html5canvastutorials.com/libraries/kinetic-v3.10.0.js"></script>
<script type="text/javascript">
window.onload = function() {
var stage = new Kinetic.Stage({
container : "container",
height : 200
});

var layer = new Kinetic.Layer();

var triangle = new Kinetic.Polygon({
stroke : "red",
strokeWidth : 4,
points : [ 60, 100, 90, 100, 90, 140 ],
draggable : true
});

layer.add(triangle);
stage.add(layer);
}
</script>
<style>
#container {
border: 1px solid black;
}
</style>
</head>
<body onmousedown="return false;">
<div id="container">
</div>
</body>
</html>
So, after about 60 seconds of sliding my triangle across the page (and unsuccessfully trying to get my seven-year-old to appreciate its utter awesomeness), I decided that maybe it wouldn't be so hard to create a much more dynamic triangle that could be resized and reshaped as well.

Draggable Vertices

Turns out that there was a simple tutorial online for reshapable polygons. I simply adapted that code for my triangle. The basic approach is to create three draggable anchor points that are placed in the same layer as the polygon and then override the draw function for that layer. Each time there is a redraw, the vertices of the triangle are reassessed in accordance with the position of the three anchor points. The challenge here was making it draggable as well as anchorable. With movable anchors as vertices, the shape of the triangle is dependent on the position of the anchors. Once it is draggable, the dependency is reversed, and the anchors are dependent on the position of the triangle. To get this right we need to do three things. First, we need to be able to differentiate the two events. KineticJs provides support for dragstart, dragend, and dragmove as bindable events, but for now, it is easy enough for us to differentiate between draggable and non-draggable paint scenarios with the isDragging method:
if ( polygon.isDragging() ) {
    // move the anchor points
    moveAnchorPoints();
  } else {
    // move the triangle to match the anchor points
    moveTriangle();
  }
Easy enough. Second, we need to implement moveTriangle. KineticJs gives us the setPoints method, so the api answers the call again:
function moveTriangle(triangle, points) {
    // voila!
    triangle.setPoints(points);
  }
Third, we need to implement moveAnchorPoints(). This is where life gets trickier. One might think that calling triangle.getPoints and setting the anchors to those would work, but this is not so. While I don't yet understand the reasons, getPoints does not get updated before the drawTriangle execution on a drag event. This means that using getPoints as a reference will simply leave the points in the dust as the triangle is dragged out from underneath them. It turns out that getX and getY are updated before drawTriangle is drawn. Sweet. The problem is that this is a single point in the upper-left-hand corner of the polygon, so it is not representative of all three points. It turns out that this is enough, though, since we can use that point to detect how much the triangle has moved. Dragging affects all the vertices equally, so if we keep track of where the polygon's (x,y) before the drag event began and where it is at the time of the draw method, then we can just change the anchor points relative to that change. The calculations are simple. I add a custom property to my polygon that tracks the last known position of the triangle prior to the event:
triangle.was = { x : 0, y : 0 };
and then account for the movement in moveAnchorPoints():
function moveAnchorPoints(layer) {
    var anchors = layer.get(".anchor");
    var triangle = layer.get(".triangle")[0];

    for ( var i = 0; i < anchors.length; i ++ ) {
      // the difference between the current position of triangle and the last known position is also the same amount that each anchor needs to move
      anchors[i].setX(anchors[i].getX() + (triangle.getX() - triangle.was.x));
      anchors[i].setY(anchors[i].getY() + (triangle.getY() - triangle.was.y));
    }

    // record the new known location
    triangle.was.x = triangle.getX();
    triangle.was.y = triangle.getY();
  }
Now, we have a draggable and adjustable triangle:
<html>
  <head>
    <script src="http://www.html5canvastutorials.com/libraries/kinetic-v3.10.0.js"></script>
    <script type="text/javascript">
      // the circle anchor points
      function buildAnchor(layer, x, y, name) {
        var anchor = new Kinetic.Circle({
          x: x,
          y: y,
          radius: 8,
          stroke: "#666",
          fill: "#ddd",
          strokeWidth: 2,
          draggable: true,
          name : name
        });

        // add hover styling
        anchor.on("mouseover", function() {
          document.body.style.cursor = "pointer";
          this.setStrokeWidth(4);
          layer.draw();
        });
        anchor.on("mouseout", function() {
          document.body.style.cursor = "default";
          this.setStrokeWidth(2);
          layer.draw();
        });

        layer.add(anchor);
        return anchor;
      }

      function buildTriangle(layer, points, name) {
        var triangle = new Kinetic.Polygon({
          stroke : "red",
          strokeWidth : 4,
          name : name,
          draggable : true
        });
 
        triangle.a = buildAnchor(layer, points[0], points[1], "anchor");
        triangle.b = buildAnchor(layer, points[2], points[3], "anchor");
        triangle.c = buildAnchor(layer, points[4], points[5], "anchor");
 triangle.was = { x : 0, y : 0 };

        layer.add(triangle);
        return triangle;
      }

      function drawTriangle() {
        var triangle = this.get(".triangle")[0];

        if ( !triangle.isDragging() ) { 
          triangle.setPoints([ triangle.a.attrs.x - triangle.was.x, 
                               triangle.a.attrs.y - triangle.was.y,
                               triangle.b.attrs.x - triangle.was.x,
                               triangle.b.attrs.y - triangle.was.y,
                               triangle.c.attrs.x - triangle.was.x,
                               triangle.c.attrs.y - triangle.was.y ]);
        } else {
          var anchors = this.get(".anchor");

          for ( var i = 0; i < anchors.length; i ++ ) {
            anchors[i].setX(anchors[i].getX() + (triangle.getX() - triangle.was.x));
            anchors[i].setY(anchors[i].getY() + (triangle.getY() - triangle.was.y));
          }

          triangle.was.x = triangle.getX();
          triangle.was.y = triangle.getY();
        }
      }

      window.onload = function() {
        var stage = new Kinetic.Stage({
          container: "container",
          height: 200
        });

        var layer = new Kinetic.Layer({
          drawFunc : drawTriangle
        });

        var triangle = buildTriangle(layer, [60, 100, 90, 100, 90, 140], "triangle");
        triangle.moveToBottom();

        // add the layer to the stage
        stage.add(layer);
      }
    </script>
    <style>
      #container {
        border: 1px solid black;
      }
    </style>
  </head>
  <body onmousedown="return false;">
    <div id="container">
</div>
</body>
</html>
  

Scalable Polygons

Last is making it scalable. I wanted to make this work with the scroll wheel, but haven't had the time, yet, to figure that out. So, I am controlling it via the 'q' and 'w' keys. Again, the calculations are pretty simple. Scaling a convex polygon is about finding the in-center of the polygon and moving each vertex out by the appropriate factor away from the in-center. Moving it away amounts to simply moving it along the line defined between the vertex and the in-center (think y = mx + b). Calculating the in-center is just taking the average x and average y of the three vertices:
triangle.calculateCenter = function() {
    this.center.x = ( this.a.attrs.x + this.b.attrs.x + this.c.attrs.x ) / 3;
    this.center.y = ( this.a.attrs.y + this.b.attrs.y + this.c.attrs.y ) / 3;
  };
And moving it along the line is pretty easy, too. If we treat the center as an offset from (0,0), than we can just multiply the point less the offset:
triangle.scaleAnchor = function(anchor, factor) {
    anchor.attrs.x = ( anchor.attrs.x - this.center.x ) * factor + this.center.x;
    anchor.attrs.y = ( anchor.attrs.y - this.center.y ) * factor + this.center.y;
  };
The rest is pretty basic. I tie the keypress event to the document and call scaleAnchor accordingly. In the draw handler, I recalculate the center point whenever a drag event or an anchor adjustment occurs:
<html>
  <head>
    <script src="http://www.html5canvastutorials.com/libraries/kinetic-v3.10.0.js"></script>
    <script type="text/javascript">
      function buildAnchor(layer, x, y, name) {
        var anchor = new Kinetic.Circle({
          x: x,
          y: y,
          radius: 8,
          stroke: "#666",
          fill: "#ddd",
          strokeWidth: 2,
          draggable: true,
          name : name
        });

        // add hover styling
        anchor.on("mouseover", function() {
          document.body.style.cursor = "pointer";
          this.setStrokeWidth(4);
          layer.draw();
        });
        anchor.on("mouseout", function() {
          document.body.style.cursor = "default";
          this.setStrokeWidth(2);
          layer.draw();
        });

        layer.add(anchor);
        return anchor;
      }

      function buildTriangle(layer, points, name) {
        var triangle = new Kinetic.Polygon({
          stroke : "red",
          strokeWidth : 4,
          name : name,
          draggable : true
        });

        triangle.scaleAnchor = function(anchor, factor) {
            anchor.attrs.x = ( anchor.attrs.x - this.center.x ) * factor + this.center.x;
     anchor.attrs.y = ( anchor.attrs.y - this.center.y ) * factor + this.center.y;
        };
        triangle.calculateCenter = function() {
            this.center.x = ( this.a.attrs.x + this.b.attrs.x + this.c.attrs.x ) / 3;
            this.center.y = ( this.a.attrs.y + this.b.attrs.y + this.c.attrs.y ) / 3;
        };
 
        triangle.a = buildAnchor(layer, points[0], points[1], "anchor");
        triangle.b = buildAnchor(layer, points[2], points[3], "anchor");
        triangle.c = buildAnchor(layer, points[4], points[5], "anchor");
 triangle.was = { x : 0, y : 0 };
        triangle.center = { x : 0, y : 0 };

        layer.add(triangle);
        return triangle;
      }

      function drawTriangle() {
        var triangle = this.get(".triangle")[0];

        if ( !triangle.isDragging() ) { 
          triangle.setPoints([ triangle.a.attrs.x - triangle.was.x, 
                               triangle.a.attrs.y - triangle.was.y,
                               triangle.b.attrs.x - triangle.was.x,
                               triangle.b.attrs.y - triangle.was.y,
                               triangle.c.attrs.x - triangle.was.x,
                               triangle.c.attrs.y - triangle.was.y ]);
        } else {
          var anchors = this.get(".anchor");

          for ( var i = 0; i < anchors.length; i ++ ) {
            anchors[i].setX(anchors[i].getX() + (triangle.getX() - triangle.was.x));
            anchors[i].setY(anchors[i].getY() + (triangle.getY() - triangle.was.y));
          }

          triangle.was.x = triangle.getX();
          triangle.was.y = triangle.getY();
        }

        triangle.calculateCenter.apply(triangle);
      }

      window.onload = function() {
        var stage = new Kinetic.Stage({
          container: "container",
          width: 578,
          height: 200
        });

        var layer = new Kinetic.Layer({
          drawFunc : drawTriangle
        });

        var triangle = buildTriangle(layer, [60, 100, 90, 100, 90, 140], "triangle");
        triangle.moveToBottom();
 
 document.onkeypress = function(e) {
   if ( e.keyCode == 113 ) {
            triangle.scaleAnchor.apply(triangle, [triangle.a, 1.05]);
            triangle.scaleAnchor.apply(triangle, [triangle.b, 1.05]);
            triangle.scaleAnchor.apply(triangle, [triangle.c, 1.05]);
     layer.draw();
   } else if ( e.keyCode == 119 ) {
            triangle.scaleAnchor.apply(triangle, [triangle.a, .95]);
            triangle.scaleAnchor.apply(triangle, [triangle.b, .95]);
            triangle.scaleAnchor.apply(triangle, [triangle.c, .95]);
     layer.draw();
   }
 };

        // add the layer to the stage
        stage.add(layer);
      }
    </script>
    <style>
      #container {
        border: 1px solid black;
      }
    </stylegt;
  </head>
  <body onmousedown="return false;">
    <div id="container">
</div>
</body>
</html>
  

Viola! A scalable, draggable, adjustable triangle using HTML5 Canvas and KineticJS!

Josh Cummings

"I love to teach, as a painter loves to paint, as a singer loves to sing, as a musician loves to play" - William Lyon Phelps

5 comments:

  1. Dear JOSH CUMMINGS,

    I am very appreciable to your code, thanks a lot. I too required exactly the same code in my project but I want for rectangle in the place of triangle.I tried a lot by modifying the code but I didn't. So, Could you please send me the code for A Scalable, Draggable, Anchored Rectangle in kinetic.js. I am very thankful to you in this regard.

    Thanking You,

    ReplyDelete
  2. Josh,

    This code is great. I learned an enormous amount about kinetic.js from this tutorial. However, simply changing out the library version seems to break it. Using version 4.3.2.min.js (the current version)seems to cause the point to detach from the lines as you drag. When you release the point, the lines snap into place. Do you have any idea why this might happen? I needed to upgrade to 4.3.2.min.js because I wanted text support that was not available in the 3.0.0 version.

    ReplyDelete
  3. I'll take a look, David; if I were to take a stab, I would say that the extra subtraction that I'm doing when the triangle is being dragged is no longer necessary. I believe that was a bug in the earlier version that was corrected.

    ReplyDelete
  4. Very nice example but unfortunately I can't get it working in my project . Would you be able to publish html page with working example please? I'm sure many folks would find it very useful. Thanks

    ReplyDelete
    Replies
    1. Thanks, Greg! Better late than never: http://tech.joshuacummings.com/p/kineticjs-triangle-examples.html

      Note that this still uses kinetic 3.10.0; I will update it to kinetic 5.2 when I get a moment.

      Delete