Sunday, August 2, 2009

rounding cornders

Whenever possible, I draw with code instead of with Flash's drawing tools. I do this because (a) it allows me to make dynamic changes to shapes at run-time, and (b) because it keeps all the app-construction info in one place (in the code), not split between the code and the stage or library.

But I keep needing shapes with rounded corners. Actionscript has a built-in rounded-corner rectangle class (graphics.drawRoundedRect), but that's the only rounded-cornder method available. I often need rounded-corner triangles, rounded-corner tooltips and rounded-corner circles (just kidding on that last one). So I decided to create a method that allowed me to round the corners of any shape.

I knew I'd need math, which is not my strong point. So I ignored that problem and focused on how I'd create rounded corners with a Bezier-pen tool. Think of the top of a triangle: imaging making points on both of the lines that meet at the top (the apex). If those points are down a little from the top, they can act as start and end points for a Bezier curve. In the illustration, below, I've drawn the start and end points as red dots.

A point at the apex (blue dot) can act as a control point, giving you all the points you need to draw a curve:

Note: Actionscript's Bezier curves have just one control point. The curve is the widest-possible path that Flash can draw without it crossing outside the triangle formed by the start, end and control points.

With this approach, I could make a rounded-cornered shape by starting with a straight-cornered shape and calculating a Bezier curve for each corner.

The first step would be to figure out how to plot points that are a little ways in from the ends of all the lines, as shown by the red dots, above. Generalizing this, I needed a way to find a point on a line. Which is where math reared its ugly head. Luckily, google exists for moments like this. Before long, I found a formula and turned it into a nifty function:

function pointOnLine(t : Number, point1 : Point, point2 : Point ) : Point
var xResult : Number = point1.x + ( t * ( point2.x - point1.x ) );
var yResult : Number = point1.y + ( t * ( point2.y - point1.y ) );
return new Point( xResult, yResult );

You can use this function by first creating two points that make up a line...

import flash.geom.Point;

var start : Point = new Point( 100, 100 );
var end : Point = new Point( 100, 500 );

... and then handing the points to the pointOnLine function. You also have to hand it t, which is a number between 0 and 1. A t of zero returns the start-point of the line; a t of .5 returns the mid-point of the line. A t of 1 returns the end-point of the line. And so on. So to get a point close to the end of the line, I could call the function like this:

var nearEnd : Point = pointOnLine( .8, start, end );

That out of the way, I could now calculate all the start and end points I needed by calling pointOnLine() twice for each line in my shape, e.g.

//note that I'm using .2 and .8 for t
var nearStart : Point = pointOnLine( .2, start, end );
var nearEnd : Point = pointOnLine( .8, start, end );

Here's an illustration of nearStart and nearEnd for one lines on a triangle:

Now all I had to do was to loop through all the lines on the triangle to find the rest of the start and end points:

After that, it was easy to find the corner points, since they were used to define the triangle in the first place:

I then drew curves through each group of start-end-corner points with the built in curve-to function:

graphics.moveTo( start.x, start.y );
graphics.curveTo( control.x, control.y, end.x, end.y );

In the illustrations here, I've drawn the triangle so you can understand my thought process. In fact, my code never draws the straight-edge version. If it did, it would have to go back later and erase the non-curvy corners. Instead, it draws actual lines from the two inset points of each virtual line: e.g. lines from the .2 t to the .8 t of each line.

If I go through the same process with the t values adjusted to move the pointOnLines in a little, say .4 and .6, I get something like this:

I packaged up my code in a utility class called RoundedShapes. To use it, you call its draw() method as follows:

RounedShapes.draw( target, roundness, points );

The target parameter is the display object on which you want to draw. It can be any object with a graphics property: e.g. Sprite, MovieClip or Shape.

The roundness parameter is essentially t. Roundness must be a number between 0 and 1, 0 being the same as straight.

The points parameter is an array of Points, defining the straight-corner version of the shape.

To draw a rounded-corner triangle, you could use code like this:

import com.grumblebee.ui.drawing.RoundedShapes;

var sprite : Sprite = new Sprite();
var points : Array = [ new Point( 320, 10 ),
new Point( 420, 80 ),
new Point( 320, 160 ),
new Point( 320, 10 ) ];

var roundness : Number = .2; 2, 0x000000 ); 0xFF0000 );
RoundedShapes.draw( sprite, roundness, points );;
addChild( sprite );

Here's a swf with some example shapes drawn by RoundedShapes.draw(). Just for fun, I animated the roundness parameter of the star:

Here's a link to the RoundedShapes class and an example fla. Happy rounding!

No comments:

Post a Comment