Posted on Tuesday 1 November 2005
I was asked recently how to graph a gaussian (you know, the bell curve, ) in Flash. Of course you can graph the function by sampling it at different intervals and placing lines at the right places. But I always find that it looks a bit jagged and it's a lot cooler to use curveTo. I explained how to do this a few years back in a tutorial for actionscript.org.
Basically it's fairly simple: control points in quad curves are really the intersect of the tangent at the two extremities. So instead of sampling at say 100 points and making lines, you sample your function at say 10 points along with the derivatives at these points, you get the tangent equations from the derivatives and then you intersect the derivatives, then you render. All very simple except:
- You need to know the derivative at every point
- You need to do a fair bit of analysis
- It does not work well when there are singular points or discontinuities in the derivatives
I whipped up an elegant solution. First of all, calculate derivatives numerically using a second order difference equation, , eliminating the need to find the derivative explicitly. Secondly, wrap all the analysis inside a class that does all the hard thinking for you. Thirdly, use an adaptive interval scheme where you specify the minimum and maximum number of curves you want in the graph and whenever the engine runs into a singularity, it will add more points to the graph until it reaches a maximum number of points. This last point I'm particularly fond of, as it's a really simple algorithm, yet it does the job quite well. Here's the class:
/**
* A class to draw graphs. Use like so:
*
function gaussian(x:Number)
{
return Math.exp(-x*x);
}
var graph:Graph = new Graph();
graph.setFunction(gaussian);
graph.setRange(-3, 3, 0, 1);
graph.setResolutionRange(7, 500);
var curves:Array = graph.getCurves();
graph.renderCurves(curves, _root, 550, 400);
trace('Rendered with ' + (curves.length - 1)/2 + ' curves');
*
* You are free to use this function for any use you wish as long as you
* don't attribute it to yourself. Links are appreciated, not mandatory
* @author Patrick Mineault
*/
import flash.geom.Point;
class clib.graph.Graph
{
private var start:Number = -10;
private var end:Number = 10;
private var startY:Number = -10;
private var endY:Number = -10;
private var minCurves:Number = 10;
private var maxCurves:Number = 1000;
private var func:Function;
private var extraArgs:Array;
private var epsilon:Number = 0.00001;
private var resDivisor:Number = 4;
private var resMultiplier:Number = 2;
function Graph()
{
}
/**
* Sets the range of the resulting graph
*/
public function setRange(start:Number, end:Number, startY:Number, endY:Number):Void
{
this.start = start;
this.end = end;
this.startY = startY;
this.endY = endY;
}
/**
* Sets the resolution range in the resulting graph
*/
public function setResolutionRange(minCurves:Number, maxCurves:Number):Void
{
this.minCurves = minCurves;
this.maxCurves = maxCurves;
}
/**
* Sets the function to use for rendering
*/
public function setFunction(func:Function, extraArgs:Array):Void
{
this.func = func;
this.extraArgs = extraArgs;
}
/**
* Sets the epsilon for the numeric derivative function
*/
public function setEpsilon(epsilon:Number):Void
{
this.epsilon = epsilon;
}
/**
* Gets the curves for an arbitrary function in range startX..endX, with numCurves curves
*/
public function getCurves():Array
{
var minRes:Number = (end - start)/minCurves;
var maxRes:Number = (end - start)/maxCurves;
var oldX:Number = start;
var oldY:Number = func(start, extraArgs);
var currX:Number;
var currY:Number;
var currRes:Number = minRes;
var intersect:Point;
var points:Array = new Array;
points.push(new Point(oldX, oldY));
var oldD:Number = 0.5/epsilon*(func(oldX + epsilon, extraArgs) - func(oldX - epsilon, extraArgs));
var currD:Number = 0;
while(oldX < end)
{
currX = oldX + currRes;
currY = func(currX, extraArgs);
var currD = 0.5/epsilon*(func(currX + epsilon, extraArgs) - func(currX - epsilon, extraArgs));
intersect = getCurveIntersect(oldD, currD, oldX, oldY, currX, currY);
if(intersect.x > oldX && intersect.x < currX)
{
//Point is ok
points.push(intersect);
points.push(new Point(currX, currY));
oldX = currX;
oldY = currY;
oldD = currD;
currRes = Math.min(resMultiplier*currRes, minRes);
}
else
{
if(Math.abs(currRes - maxRes) < 0.0001 )
{
//Give up, push a line
points.push(new Point(currX, currY));
points.push(new Point(currX, currY));
oldX = currX;
oldY = currY;
oldD = currD;
}
else
{
//Make current resolution higher
currRes = Math.max(currRes/resDivisor, maxRes);
}
}
}
return points;
}
public function renderCurves(curves:Array, mc:MovieClip, width:Number, height:Number):Void
{
var scaleX:Number = width/(end - start);
var scaleY:Number = height/(endY - startY);
var numCurves = (curves.length - 1)/2;
mc.lineStyle(1, 0x000000);
mc.moveTo((curves[0].x - start)*scaleX, (endY - curves[0].y)*scaleY);
for(var i = 0; i < numCurves; i++)
{
var cp:Point = curves[i*2+1];
var p2:Point = curves[i*2+2];
mc.curveTo((cp.x - start)*scaleX,
(endY - cp.y)*scaleY,
(p2.x - start)*scaleX,
(endY - p2.y)*scaleY);
}
}
/**
* Gets an intersect
*/
private function getCurveIntersect(d1:Number, d2:Number, x1:Number, y1:Number, x2:Number, y2:Number):Point
{
var intersectX = (-x2*d2 + y2 + x1*d1 - y1)/(d1 - d2);
var intersectY = d1*(intersectX - x1) + y1;
return new Point(intersectX, intersectY);
}
}
You can actually get a pretty good looking gaussian at 550x440 with a measly 7 curves! You can download a sample fla with a few examples here. With a math expression parser and a axes renderer, this could be something really cool. Try it with sin(1/x) to see the behaviour at singularities.
Final note: Looking at the timestamps, I created the project folder at 5:46 and last exported the fla at 6:52. It compiled the first time too (and I was coding in SEPY, not FDT either). Flash is really becoming second nature... Scary.


