Monday, September 24, 2012

Aligning DOM Elements around a Circle

I've been working on a project that requires elements to be positioned relative to an imaginary circle.  I need to be able to support two different alignment scenarios.  Both of these are illustrated below:

                   

I've added the imaginary circles as a reference for the alignment:

                   
I decided to start by figuring out where I needed to align my elements.  We can any point on the circle using the following formula:




(x',y')=(cx+r*cos(α),cy+r*sin(α))


Where cx and cy are the center of the circle, r is the radius, and α is the angle of the point, in radians, relative to the center of the circle.

In my example, I want the elements to be evenly distributed around the circle.  I have five elements so each element should be located 1/5 the way around the circle.  360 / 5 = 72 so the first element will be place at the point 72 degrees around the circle, the second element at 144 degree around, and so on.  If I plug my numbers into the above equation and set the top/left of each element, I get the following:



That's a good start but the elements are suppose to be on the perimeter of the circle and aligned to their middle/bottom point.  If we back to the original reference and look at Item 2 which positioned 72 degrees around the circle moving clockwise from the top, you can see that rotating the element by 72 degrees will make it tangent to the circle at the same point:



In jQuery, I can set this via the css() function:

          $el.css({
                transformOrigin: 'top left',
                transform: 'rotate(72deg)'
             });


If you noticed, I also set transform-origin to "top left" to ensure the visual top/left corner of the element shared the top/left corner in the DOM.  This is important so aligning does not become tedious.  Now we have the elements rotated but not positioned on the circle at the bottom/middle point:



We need to perform one more transform to move the element's bottom/middle point to the top/left point.  Again, we can use CSS3 transform functions:


$el.css({
                transformOrigin: 'top left',
                transform: 'rotate(72deg) translate(-40px, -50px)'
             });


The translation assumes my element is 80px width and 50px tall.  The middle is at 40px and the bottom is at 50px relative to the top/left.  By combining the two transform functions, we get the final alignment:



The second alignment scenario is a little bit trickier.  Its not as easy as finding the point on the circle, aligning the top/left, and performing a transform on the element.  Instead, as each element is placed around the circle, the point touching the circle is different and the distance from the center of the circle to the top/left is different.  While a mathematical solution may be possible, I chose a slightly different approach



If you draw a path through the top/left points of each element, you will notice they form a rounded box.  In fact, it is simply a rectangle with a width of 2*r+w and a height of 2*r+h with a corner radius of r (r is the radius of the circle, w/h is the width/height of the element).  If you align this rounded rectangle so the bottom/right aligns with the bottom/right of the circle, you can align the element's top/left point to points along the rectangle's perimeter and the elements will line up properly:



I can find a point on the rectangle by moving along its perimeter clockwise the same relative distance I would position the element along the circle.  To do this, I use the expected angle divided by 360 degrees to find the percentage of distance along that path.  I can then find that point on the rectangle and position the element appropriately.  I actually used a library I built called PathJS to locate the correct point.  PathJS can generate interpolated points along any number of paths.  The points are sequential so you can actually step through them to do things like path animations or, in this case, estimate the position of a point on the perimeter.

I created a small demo in my sandbox that allows you to toggle parts of the alignment on and off.  It also demonstrates how the elements can be further animated along the path or aligned in several different configurations.