Drawing super ellipse rounded corners in ActionScript 3

Posted by Zeh Fernando on 16/September/2013 at 18:34

Super ellipses. If you’re of the mathematical type, they are a geometric figure defined in the Cartesian coordinate system as the set of all points (x, y) with (x/a)ⁿ + (y/b)ⁿ = 1 where n, a and b are positive numbers. If you’re everyone else, they are those fancy rounded corners used in icons for iOS 7.

Regardless of your inclination, the fact is that corners rounded with that formula do look great – especially on a screen, I’d say – and as such, I decided to roll them into the standard rounded box drawing classes that I use in pretty much everything I build. Since it is a well know geometric figure, Wikipedia comes to the rescue as usual, so implementation is trivial. This is a comparison of the result:

Superellipse example

In my case, the implementation just tries to mimic Flash’s own drawing APIs. So in my RoundedBox class, I normally had this:

graphics.drawRoundRectComplex(0, 0, _width, _height, _topLeftRadius, _topRightRadius, _bottomLeftRadius, _bottomRightRadius);

But I now have this:

drawRoundRectSuperEllipse(graphics, 0, 0, _width, _height, _topLeftRadius, _topRightRadius, _bottomLeftRadius, _bottomRightRadius);

Your mileage will vary depending on how you implement rounded corners. However, the equation is easy enough to allow it to be applied on every kind of case with relative ease. The code is fairly simple too, so I would assume it’d be easy to adapt it to other languages, especially JavaScript (using the Canvas drawing API).

This is the actual implementation of the function (well, functions):

private function drawRoundRectSuperEllipse(__target:Graphics, __x:Number, __y:Number, __width:Number, __height:Number, __topLeftRadius:Number, __topRightRadius:Number, __bottomLeftRadius:Number, __bottomRightRadius:Number):void {
	// Draws a normal rectangle, but with "super ellipse" corners
	// https://en.wikipedia.org/wiki/Superellipse

	// TL
	if (__topLeftRadius <= 0) {
		graphics.moveTo(__x, __y);
	} else {
		drawSuperEllipseCurve(__target, __x + __topLeftRadius, __y + __topLeftRadius, __topLeftRadius, __topLeftRadius, 180, 270, true);
	}

	// TR
	if (__topRightRadius <= 0) {
		graphics.lineTo(__x + __width, __y);
	} else {
		drawSuperEllipseCurve(__target, __x + __width - __topRightRadius, __y + __topRightRadius, __topRightRadius, __topRightRadius, 270, 360);
	}

	// BR
	if (__bottomRightRadius <= 0) {
		graphics.lineTo(__x + __width, __y + __height);
	} else {
		drawSuperEllipseCurve(__target, __x + __width - __bottomRightRadius, __y + __height - __bottomRightRadius, __bottomRightRadius, __bottomRightRadius, 0, 90);
	}

	// BL
	if (__bottomLeftRadius <= 0) {
		graphics.lineTo(__x, __y + __height);
	} else {
		drawSuperEllipseCurve(__target, __x + __bottomLeftRadius, __y + __height - __bottomLeftRadius, __bottomLeftRadius, __bottomLeftRadius, 90, 180);
	}
}

private function drawSuperEllipseCurve(__target:Graphics, __cx:Number, __cy:Number, __xRadius:Number, __yRadius:Number, __startAngleDegrees:Number, __endAngleDegrees:Number, __moveFirst:Boolean = false):void {
	// Draw a "super ellipse" curve
	// https://en.wikipedia.org/wiki/Superellipse

	const SEGMENT_SIZE:Number = 2; // In degrees.. more = more precise but may be slower if done repeatedly

	// Enforce always min->max
	while (__endAngleDegrees < __startAngleDegrees) __endAngleDegrees += 360;

	var p:Point;
	for (var angleDegrees:Number = __startAngleDegrees; angleDegrees < __endAngleDegrees; angleDegrees += SEGMENT_SIZE) {
		p = getSuperEllipsePointOnCurve(__cx, __cy, angleDegrees, __xRadius, __yRadius);
		if (angleDegrees == __startAngleDegrees && __moveFirst) {
			__target.moveTo(p.x, p.y);
		} else {
			__target.lineTo(p.x, p.y);
		}
	}
	// Last point
	p = getSuperEllipsePointOnCurve(__cx, __cy, __endAngleDegrees, __xRadius, __yRadius);
	__target.lineTo(p.x, p.y);

	return;
}

private function getSuperEllipsePointOnCurve(__cx:Number, __cy:Number, __angleDegrees:Number, __xRadius:Number, __yRadius:Number):Point {
	const N:Number = 5; // The n of the curve; 4 according to wikipedia, 5 for a stronger corner
	var cn:Number = 2 / N;
	var angle:Number = __angleDegrees / 180 * Math.PI;
	var ca:Number = Math.cos(angle);
	var sa:Number = Math.sin(angle);
	return new Point(
		Math.pow(Math.abs(ca), cn) * __xRadius * (ca < 0 ? -1 : 1) + __cx,
		Math.pow(Math.abs(sa), cn) * __yRadius * (sa < 0 ? -1 : 1) + __cy
	);
}

One last note: in my own code, I multiply the radius of every corner by 2 before drawing each corner, just so the super-ellipse corner looks roughly the same size as the original corner. The above code omits this tweak, but it’s something I’d really recommend if you just plan on replacing corner drawing functions with this curve.



Leave a comment