Dynamic Drag Alignment Rulers {elegance sometimes happens}

In most cases when you work on a product, requests from ‘those who shall be obeyed’ and I don’t mean your mother, are fairly simple.  You are given a functional idea or a behavior to be implemented within a product and you say to yourself ‘yep, no problem’. In other words, not often is there a challenge. Then there are those rare cases where you come across a task where you need to think outside of the box and we all crave those tasks. Such is a task that I recently faced on a product I work on.  In essence think of the product as an online version of Microsoft PowerPoint, it’s not, but it’s the closest I can think of in the context of the post. The request was to display rulers top, bottom, center, middle, left and right on the selected object. As the object was dragged around the screen when any of these rulers came into alignment on the X or Y of any other object they were to be displayed.  A very similar UI behavior that you would see in PhotoShop, for example.  To make it even more difficult the rulers had to gradually get stronger as it got close to any alignment on any other element on the page on its 3 horizontal or 3 vertical axis!

So imagine the below scenario:

So in the above image there are three shapes.  The blue square has been selected and when any of the 3 horizontal rulers comes into contact with the top, middle or bottom of the other shapes they would need to appear.  Same goes for the vertical if the user where to drag the blue square to the right. So this means that for each element/shape on the page there are 3 vertical and 3 horizontal points to compare to the current points on the actively dragged element, in this case the blue box. Now consider you could have any number of shapes you need to align to! This leads one to the conclusion that any sort of heavy on the fly calculation while dragging will simply destroy performance.  So the question then becomes how can we do this with ZERO calculations/conditions while the selected element is being dragged.  The answer is YES we can and here is how. Just a quick note regarding some of the code, in order to preserve the privacy of the application I am not using the general namespace we use for our toolkit that provides DOM manipulation. However you can easily see how to do this in jQuery, ExtJS etc.  The point is the solution not so much the implementation.

Creating the Component

The first step is to create our basic component framework.  Each piece of functionality I call a component and all of its behavior is neatly encapsulated in the file for easy modification.  Since this component is a singleton we will use Crockford’s singleton patter.

(function(){

      // the canvas element we exist in 
      var canvas = me.element.get('canvas');

      // set up the necessary static values
      var config = {
         width : canvas.width(),
         height : canvas.height(),
         tolerance : Number(8),
         baseTolerance : ‘k’,
         maxToleranceRange = 'kjihgfedcbabcdefghijk',
         activeTolerance= (config.maxToleranceRange.substring(10-tolerance,9+tolerance)).split(''),
      },

      // object to hold the map cordinates      
      map = {
         x : null,
         y : null
      },

      // Small function to help with setting the colorScale object
      getIncrementStyle = function(){},

      // Object to hold dynamic ruler styles
      colorScale = {},

      // create our rulers initially 
      rulers = {
          top : canvas.append(me.create({cls:'ruler horizontal top'})),
          middle : canvas.append(me.create({cls:'ruler horizontal’})),
          bottom : canvas.append(me.create({cls:'ruler horizontal’})),
          left : canvas.append(me.create({cls:'ruler vertical’})),
          center : canvas.append(me.create({cls:'ruler vertical’})),
          right : canvas.append(me.create({cls:'ruler vertical’}))
      },

      createRulerMap = function(){},
      hideRulers = function(){},
      trackRulers = function(){};

      // Bind to the necessary system events
      me.subscribe(‘me.element.selected’,createRulerMap);
      me.subscribe(‘me.element.deselected’,hideRulers);
      me.subscribe(‘me.canvas.dragMoved’,trackRulers);
      me.subscribe(‘me.canvas.dragEnd’,hideRulers);  
      return {};
}())

So all we have done at this point is lay out our basic framework which grabs the width and height of our work area and creates the rulers and assigns them to the rulers object for quick reference later one.  I will discuss the tolerance, maxToleranceRange and baseTolerance a little later on. I have also bound to the basic events in the system. The first one is the element selected which gets fired when an element/shape is selected on the canvas.  This is when we will create our map of other elements. The other is the element deselected and canvas drag end which in both cases will hide the rulers.

Note that in the rulers object where I am created the rulers I am assigning several CSS classes to each.  Here is the basic CSS:

.ruler{
   background-color:transparent;
   border-top-width:1px;
   border-left-width:1px;
   position:absolute;
   display:none;
 }
. horizontal{
   height:0px;
   left:0px;
   border-bottom:none !important;
   border-left:none !important;
   border-right:none !important;
 }
. vertical{
   width:0px;
   top:0px;
   border-right:none !important;
   border-top:none !important;
   border-bottom:none !important;
 }

Now some may argue why create the rulers initially rather than on demand to save DOM weight.  My argument against that is speed.  For this specific behavior we have to ensure no lag when the user clicks on an element and begins to drag.  Otherwise I would create on demand then destroy when no longer needed.

Creating the X and Y Map

So based on our initial component layout when an element on our canvas is selected we call the createRulerMap function, this is where our config.baseTolerance comes in. First we need to create a string of the baseTolerance keys based on the config.width  and place in map.y and conversely for map.x.

map.x = (Array(Number(config.width)).join(baseTolerance).split(‘’);
map.y = (Array(Number(config.height)).join(baseTolerance).split(‘’);

In essence create an empty array for the necessary width or height then join it with the base tolerance value. So if our base tolerance is k and the width is 1200 we end up with a string of 1200 k’s which we then split back into an array each containing a k. In other words we have a shit load of K’s!

Now let’s assume for now that we have one other element on the canvas that is 100 by 100 pixels in size and is at position X 200 and Y 200. So for this element we need to gets its 3 vertical positions and 3 horizontal positions to align the rulers to by placing those coordinates in our map. Let’s assume we have gotten our element in question and assigned it to the variable element.

var coords = {
   left : Number(element.x()),
   top : Number(element.y()),
   right : Number(element.x()) + Number(element.width()),
   bottom : Number(element.y()) + Number(element.height()),
   center : 0,
   middle : 0
};
coords.center = coords.left + parseInt(cords.right/2,10);
coords.middle = coords.top + parseInt(coords.bottom/2,10);

So now we have a coords object with left:200, top:200, right:300, bottom:300, center:250 and middle 250.

Now we come back to the config.activeTolerance and config.maxTolerance values along with the config.tolerance level. If we look at our config.activeTolerance we get the following result hgfedcbabcdefgh, this is based on the config.tolerance level which goes either side of the center of the string for the value specific in config.tolerance. If we look at the map.x presently we have a bunch ok baseTolerance k’s and now we need to insert the active tolerance where A sits at the exact coordinate. Therefore assuming our first X coordinate, coords.left, we need to insert it at position 200 in the map.x string.  What we would want to see is in essence:

…..kkkkkkkkkhgefdcbabcdefghkkkkkkkkkk….

However we could have more than one element on the page that we have to account for. What happens if there is another element sitting at X205, we need to account for that. So let’s work on doing our enumeration:

createRulerMap = function(){

   // This grabs all of canvas elements with a class of moveableElement
   var elements = canvas.filter(‘moveableElement’),

   // get the Dom ID of the currently selected element
   currentElementId = me.currentElement.id(),

   setTolerance = function(startPoint,mapCoord){                       
       var startPoint -= config.tolerance;
       for(var point=0;point<(config.tolerance*2)-1;point++){
           map[mapCoord][(point+startPoint)]=activeTolerance[point]<map[mapCoord][(point+startPoint)]?activeTolerance[point]:map[mapCoord][(point+startPoint)];
       };
       return null;
    };

    for(var element = 0; element < elements.length; element++){
        if(elements[element].id()!=currentElementId){
            var coords = {
                left : Number(element.x()),
                top : Number(element.y()),
                right : Number(element.x()) + Number(element.width()),
                bottom : Number(element.y()) + Number(element.height()),
                center : 0,
                middle : 0
            };
            coords.center = coords.left + parseInt(cords.right/2,10);
            coords.middle = coords.top + parseInt(coords.bottom/2,10)
            setTolerance(coords.left,’x’);
            setTolerance(coords.right,’x’);
            setTolerance(coords.center,’x’);
            setTolerance(coords.top,’y’);
            setTolerance(coords.bottom,’y’);
            setTolerance(coords.middle,’y’);
        };
     };
     map.x=map.x.join(‘’);
     map.y=map.j.join(‘’);
 }

So I think I little explanation of the inline setTolerance may be in order at this point so I will lay it out here. Here it is broken down :

// @param : startPoint (integer) position of the value we wish to put the string at;
// @param : mapCoord (string) either the string x or y to indicate what map string we are testing against
function(startPoint,mapCoord){
     // The start point in the array that we wish to check against is the passed coordinate minus the actual tolerance, so if x is 200 it would be 200-8
     var startPoint -= config.tolerance;

     // now enumerate through the tolerance * 2 but minus 1 because there is only one center value of A
     for(var point=0;point<(config.tolerance*2)-1;point++){

        // set map[ x or y](the loop value plus the startPoint      
        map[mapCoord][(point+startPoint)] =

        // test if the present value in the tolerance string is less than the value presently in the array at point+startPoint. This test works because A is less than B and B is less than C etc.
        (activeTolerance[point]<map[mapCoord][(point+startPoint)])?

        // if less then put in the tolerance letter otherwise leave what is in there.
        activeTolerance[point]:map[mapCoord][(point+startPoint)];
     };
     return null;
 };

So in the case of have 2 objects at X 200 and X 205 you would end up with the following snippet in the map.x string:

…..kkkkkkkkkhgefdcbabccbabcdefghkkkkkkkk….

At the end of our function we merge the map.x and map.y arrays back into single strings.

Tracking the Drag and doing the Magic bits

Now that we have our X and Y map we need to start tracking the drag behavior and decide when it is appropriate to display the necessary ruler. Before we do that however, we need to create our set of style rules that indicate how to display a ruler. If you remember back to when we created the basic framework for the component I declared an empty object variable entitled colorMap in the global scope of the component. To define this object first I first create a simple function that creates a style based on an integer value passed in, this is placed about the colorScale definition.

getIncrementStyle = function(x){
  var opacity=(((tolerance-x)+1)/10);
  return ';display:block;border-width:'+x+'px;opacity:'+opacity+';filter:alpha(opacity='+(opacity*100)+')'
}

Now we define our coloScale:

colorScale = {
  k:'',
  j:'border-color:#f80708;border-style:solid'+getIncrementStyle(10),
  i:'border-color:#f80708;border-style:solid'+getIncrementStyle(9),
  h:'border-color:#f80708;border-style:solid'+getIncrementStyle(8),
  g:'border-color:#f80708;border-style:solid'+getIncrementStyle(7),
  f:'border-color:#f80708;border-style:solid'+getIncrementStyle(6),
  e:'border-color:#f80708;border-style:solid'+getIncrementStyle(5),
  d:'border-color:#f80708;border-style:solid'+getIncrementStyle(4),
  c:'border-color:#f80708;border-style:solid'+getIncrementStyle(3),
  b:'border-color:#f80708;border-style:solid'+getIncrementStyle(2),
  a:'border-color:#00fffc;border-style:solid;display:block'
}

Now the getIncrementStyle is completely optional but it simply resulted in less coding for me and allowed me to play with the style a little more easily.  Note the value for k does not specify display of block.  This is because it is our config.baseTolerance. Also remember that the default style for the rulers based on its CSS is rules is display of none.  So when the ruler K is assigned to a ruler it is not displayed. In this style implementation the opacity of the ruler and width increases as a user gets closer to A until we hit A then simple make the ruler a solid dark line.

Now the final method of trackRulers, which is called during the drag event on our canvas element. This now becomes very simple.

trackMove = function(){
   var currentElement = me.currentElement,
   left = currentElement.x(),
   top = currentElement.y(),
   center = left + parseInt(currentElement.width().2,10),
   middle = top + parseInt(currentElement.height()/2,10),
   right = ((center-left)*2)+left,
   bottom = currentElement.height()+top;

   // apply the styles based on the letter found in the map which provides us our color via the colorScale object
   rulers.top.attribute('style',colorScale[map.y.substr(top,1)]+';top:'+top+'px;width:'+config.width+'px');
   rulers.middle.attribute('style',colorScale[map.y.substr(middle,1)]+';top:'+center+'px;width:'+config.width+'px');
   rulers.bottom.attribute('style',colorScale[map.y.substr(bottom,1)]+';top:'+bottom+'px;width:'+config.width+'px');
   rulers.left.attribute('style',colorScale[map.x.substr(left,1)]+';left:'+left+'px;height:'+config.height+'px');
   rulers.center.attribute('style',colorScale[map.x.substr(center,1)]+';left:'+xCenter+'px;height:'+config.height+'px');
   rulers.right.attribute('style',colorScale[map.x.substr(right,1)]+';left:'+right+'px;height:'+config.height+'px');
   return null;
}

So as you can see there are NO calculations going on during the applying of the styles to each of the six rulers. In this implementation I set the map.x and map.y to strings and then used the substr method to get the appropriate letter to get the style.  I am not sure why I simply did not leave it as an array in the first place there by negating the need for a substr calculation. But as they say hind sight is 20/20. Note also when setting the style attribute of each ruler I set horizontal rulers width to the config.width and the vertical rulers to config.height.  Each of these config values where defined at component start up time to the height and width of our working canvas area.

I have uploaded a screen cast of the alignment rulers in action at http://screencast.com/t/MGjZ1mej3be. In my implementation the users have the ability to always have rulers on when they select an element which the video demonstrates.

{Elegance Sometimes Happens}

I added this to the title of this post because I consider this an elegant solution to what may first appear as a simple piece of functionality but when you dig a little deeper you realize it is not. It is often the case that elegance is driven by necessity. In this case I had to find a solution that required as small of a footprint as possible while an element was being dragged around the screen.  This was required in order to ensure there was no choppy behavior that would ruin the user experience. This is one of the key factors of elegance, provide an efficient and usable user experience.  Here are some other factors of code elegance:

  1. Encapsulated within a closure,
  2. Does not rely on other components and therefore stands alone,
  3. Easy to read,
  4. Small foot print,
  5. Simple and clean,
  6. Easily customizable,
  7. Ratio between size and complexity vs functional behavior and user experience is high
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s