The Math and ActionScript of Curves: Gradients and Normals
We've tackled drawing curves, and finding their quadratic and cubic roots, as well as handy applications for using quadratic roots within games. Now, as promised, we'll look at applications for finding cubic roots, as well as curves' gradients and normals, like making objects bounce off curved surfaces. Let's go!
Example
Let's take a look one practical use of this math:
In this demo, the "ship" bounces off the edges of the SWF and the curve. The yellow dot represents the closest point to the ship that lies on the curve. You can adjust the shape of the curve by dragging the red dots, and adjust the movement of the ship using the arrow keys.
Step 1: Shortest Distance to a Curve
Let's consider the scenario where a point is located near a quadratic curve. How do you calculate the shortest distance between the point and the curve?

Well, let's start with Pythagoras's Theorem.
\[
Let\ the\ point\ be\ (x_p,\ y_p)\\
and\ call\ the\ closest\ point\ on\ the\ curve\ (x_c,\ y_c)\\
Then:\\
z^2 = x^2 + y^2\\
z^2 = (x_c-x_p)^2 + (y_c-y_p)^2\\
Given\ y_c=ax_c^2 + bx_c + c,\\
z^2 = (x_c-x_p)^2 + [(ax_c^2 + bx_c + c) -y_p]^2
\]
You can see that we have substituted \(y_c\) with the quadratic equation. At a glance, we can see the highest power is 4. Thus, we have a quartic equation. All we need to do is to find a minimum point in this equation to give us the shortest distance from a point to a quadratic curve.
But before that, we'll need to understand gradients on a curve...
Step 2: Gradient of a Curve
Before we look at the problem of minimizing a quartic equation, let's try to understand gradients of a curve. A straight line has only one gradient. But a quadratic curve's gradient depends on which point on the curve we refer to. Check out the Flash presentation below:
Drag the red dots around to change the quadratic curve. You can also play with the slider's handle to change the position of blue dot along x. As the blue dot changes, so will the gradient drawn.
Step 3: Gradient Through Calculus
This is where calculus will come in handy. You may have guessed that differentiating a quadratic equation would give you the gradient of the curve.
\[
f(x) = ax^2+bx+c\\
\frac{df(x)}{dx} = 2ax+b
\]
So \(\frac{df(x)}{dx}\) is the gradient of a quadratic curve, and it's dependant on the \(x\) coordinate. Well, good thing we've got a method to handle this: diff1(x:Number)
will return the value after a single differentiation.
To draw the gradient, we'll need an equation to represent the line, \(y=mx+c\). The coordinate of the blue point \((x_p, y_p)\) will be substituted into the \(x\) and \(y\), and the gradient of the line found through differentiation will go into \(m\). Thus the y-intercept of line, \(c\) can be calculated through some algebra work.
Check out the AS3:
1 |
|
2 |
var x:Number = s.value |
3 |
var y:Number = quadratic_equation.fx_of(s.value) |
4 |
point.x = x; |
5 |
point.y = y; |
6 |
|
7 |
/**
|
8 |
* y = mx + c;
|
9 |
* c = y - mx; <== use this to find c
|
10 |
*/
|
11 |
var m:Number = quadratic_equation.diff1(x); |
12 |
var c:Number = y - m * x; |
13 |
|
14 |
graphics.clear(); |
15 |
graphics.lineStyle(1, 0xff0000); |
16 |
graphics.moveTo(0, c); |
17 |
graphics.lineTo(stage.stageWidth, m * stage.stageWidth + c); |
Step 4: Coordinate Systems
Always bear in mind of the inverted y-axis of Flash coordinate space as shown in the image below. At first glance, the diagram on right may seem like a negative gradient - but due to the inverted y-axis, it's actually a positive gradient.

The same goes for the minimum point as indicated below. Because of the inverted y-axis, the minimum point in Cartesian coordinate space (at (0,0)) looks like a maximum in Flash coordinate space. But by referring to the location of origin in Flash coordinate space relative to the quadratic curve, it's actually a minimum point.



Step 5: Rate of Change for Gradient
Now let's say I'm interested in finding the lowest point on a curve - how do I proceed? Check out the image below (both figures are in the same coordinate space).



In order to get the minimum point, we'll just equate \(\frac{df(x)}{dx} = 0\), since by definition we're looking for the point where the gradient is zero. But as shown above, it turns out that the maximum point on a curve also satisfies this condition. So how do we discriminate between these two cases?
Let's try differentiation of the second degree. It'll give us the rate of change of the gradient.
\[
\frac{df(x)}{dx} = 2ax+b\\
\frac{df^2(x)}{dx^2} = 2a
\]
I'll explain with reference to the image below (drawn in Cartesian coordinate space). We can see that, as we increment along the x-axis, the gradient changes from negative to positive. So the rate of change should be a positive value.
We can also see that when \(\frac{df^2(x)}{dx^2}\) is positive, there's a minimum point on the curve. Conversely if the rate is negative, a maximum point is present.



Step 6: Back to the Problem
Now we are ready to solve the problem presented in Step 1. Recall the quartic equation (where the highest degree is 4) we arrived at:
\[
z^2 = (x_c-x_p)^2 + [(ax_c^2 + bx_c + c) -y_p]^2
\]



The same quartic equation, plotted
Remember, we are interested to find the minimum point on this curve, because the corresponding point on the original quadratic curve will be the point that's at the minimum distance from the red dot.

So, let's differentiate the quartic function to get to gradient of this curve, and then equate the gradient of this quartic function to zero. You will see that the gradient is actually a cubic function. I'll refer interested readers to Wolfram's page; for this tutorial, I'll just pluck the result of their algebra workings:
\[
\frac{d(z^2)}{dx}=
2(x_c-x_p) + 2(ax_c^2 + bx_c + c - y_p)(2ax_c+b)\\
\frac{d(z^2)}{dx}= 2a^2(x_c)^3+3ab(x_c)^2+(b^2+2ac-2ay_p+1)(x_c)+(bc-by_p-x_p)\\
Equate\ gradient\ to\ 0\\
\frac{d(z^2)}{dx}=0\\
2a^2(x_c)^3+3ab(x_c)^2+(b^2+2ac-2ay_p+1)(x_c)+(bc-by_p-x_p)=0\\
Compare\ with\ cubic\ equation\\
Ax^3+Bx^2+Cx+D=0\\
A = 2a^2\\
B=3ab\\
C=b^2+2ac-2ay_p+1\\
D=bc-by_p-x_p
\]
Solve for the roots of this (rather messy) cubic function and we'll arrive at the coordinates of the three blue points as indicated above.
Next, how do we filter our results for the minimum point? Recall from the previous step that a minimum point has a rate of change that's positive. To get this rate of change, differentiate the cubic function that represents gradient. If the rate of change for the given blue point is positive, it's one of the minimum points. To get the minimum point, the one that we're interested in, choose the point with the highest rate of change.
Step 7: Sample of Output
So here's a sample implementation of the idea explained above. You can drag the red dots around to customise your quadratic curve. The blue dot can also be dragged. As you move the blue dot, the yellow one will be repositioned so that the distance between the blue and yellow dots will be minimum among all points on the curve.
As you interact with the Flash presentation, there may be times where three yellow dots appear all at once. Two of these, faded out, refer to the roots obtained from the calculation but rejected because they are not the closest points on the curve to the blue dot.
Step 8: ActionScript Implementation
So here's the ActionScript implementation of the above. You can find the full script in Demo2.as
.
First of all, we'll have to draw the quadratic curve. Note that the matrix m2
will be referred to for further calculation.
1 |
|
2 |
private function redraw_quadratic_curve():void |
3 |
{
|
4 |
var cmd:Vector.<int> = new Vector.<int>; |
5 |
var coord:Vector.<Number> = new Vector.<Number>; |
6 |
|
7 |
//redraw curve;
|
8 |
m1 = new Matrix3d( |
9 |
curve_points[0].x * curve_points[0].x, curve_points[0].x, 1, 0, |
10 |
curve_points[1].x * curve_points[1].x, curve_points[1].x, 1, 0, |
11 |
curve_points[2].x * curve_points[2].x, curve_points[2].x, 1, 0, |
12 |
0,0,0,1 |
13 |
); |
14 |
|
15 |
m2 = new Matrix3d( |
16 |
curve_points[0].y, 0, 0, 0, |
17 |
curve_points[1].y, 0, 0, 0, |
18 |
curve_points[2].y, 0, 0, 0, |
19 |
0,0,0,1 |
20 |
)
|
21 |
|
22 |
m1.invert(); |
23 |
m2.append(m1); |
24 |
quadratic_equation.define(m2.n11, m2.n21, m2.n31); |
25 |
|
26 |
for (var i:int = 0; i < stage.stageWidth; i+=2) |
27 |
{
|
28 |
if (i == 0) cmd.push(1); |
29 |
else cmd.push(2); |
30 |
|
31 |
coord.push(i, quadratic_equation.fx_of(i)); |
32 |
}
|
33 |
|
34 |
graphics.clear(); |
35 |
graphics.lineStyle(1); |
36 |
graphics.drawPath(cmd, coord); |
37 |
}
|
And here's the one that implements the mathematical concept explained. c1
refers to a point randomly positioned on stage.
1 |
|
2 |
private function recalculate_distance():void |
3 |
{
|
4 |
var a:Number = m2.n11; |
5 |
var b:Number = m2.n21; |
6 |
var c:Number = m2.n31; |
7 |
|
8 |
/*f(x) = Ax^3 + Bx^2 +Cx + D
|
9 |
*/
|
10 |
var A:Number = 2*a*a |
11 |
var B:Number = 3*b*a |
12 |
var C:Number = b*b + 2*c*a - 2*a*c1.y +1 |
13 |
var D:Number = c * b - b * c1.y - c1.x |
14 |
|
15 |
quartic_gradient = new EqCubic(); |
16 |
quartic_gradient.define(A, B, C, D); |
17 |
quartic_gradient.calcRoots(); |
18 |
|
19 |
roots = quartic_gradient.roots_R; |
20 |
var chosen:Number = roots[0]; |
21 |
|
22 |
if (!isNaN(roots[1]) && !isNaN(roots[2])) { |
23 |
|
24 |
//calculate gradient and rate of gradient of all real roots
|
25 |
var quartic_rate:Vector.<Number> = new Vector.<Number>; |
26 |
|
27 |
for (var i:int = 0; i < roots.length; i++) |
28 |
{
|
29 |
if (!isNaN(roots[i])) quartic_rate.push(quartic_gradient.diff1(roots[i])); |
30 |
else roots.splice(i, 1); |
31 |
}
|
32 |
|
33 |
//select the root that will produce the shortest distance
|
34 |
for (var j:int = 1; j < roots.length; j++) |
35 |
{
|
36 |
//the rate that corresponds with the root must be the highest positive value
|
37 |
//because that will correspond with the minimum point
|
38 |
if (quartic_rate[j] > quartic_rate[j - 1]) { |
39 |
chosen = roots[j]; |
40 |
}
|
41 |
}
|
42 |
|
43 |
//position the extra roots in demo
|
44 |
position_extras(); |
45 |
}
|
46 |
else { |
47 |
|
48 |
//remove the extra roots in demo
|
49 |
kill_extras(); |
50 |
}
|
51 |
intersec_points[0].x = chosen |
52 |
intersec_points[0].y = quadratic_equation.fx_of(chosen); |
53 |
}
|
Step 9: Example: Collision Detection
Let's make use of this concept to detect the overlap between a circle and a curve.
The idea is simple: if the distance between the the blue dot and the yellow dot is less than blue dot's radius, we have a collision. Check out the demo below. The interactive items are the red dots (to control the curve) and the blue dot. If the blue dot is colliding with the curve, it will fade out a little.
Step 10: ActionScript Implementation
Well, the code is quite simple. Check out the full source in CollisionDetection.as
.
1 |
|
2 |
graphics.moveTo(intersec_points[0].x, intersec_points[0].y); |
3 |
graphics.lineTo(c1.x, c1.y); |
4 |
|
5 |
var distance:Number= Math2.Pythagoras( |
6 |
intersec_points[0].x, |
7 |
intersec_points[0].y, |
8 |
c1.x, |
9 |
c1.y) |
10 |
|
11 |
if (distance < c1.radius) c1.alpha = 0.5; |
12 |
else c1.alpha = 1.0; |
13 |
|
14 |
t.text = distance.toPrecision(3); |
Step 11: Bouncing Off the Curve
So now that we know when collision will occur, let's try to program some collision response. How about bouncing off the surface? Check out the Flash presentation below.
You can see the ship (triangle shape), is surrounded by a circle (translucent blue). Once the circle collides with the curve, the ship will bounce off the surface.
Step 12: Controlling the Ship
Here's the ActionScript to control the ship.
1 |
|
2 |
public function CollisionDetection2() |
3 |
{
|
4 |
/**
|
5 |
* Instantiation of ship & its blue-ish circular area
|
6 |
*/
|
7 |
ship = new Triangle(); addChild(ship); |
8 |
ship.x = Math.random() * stage.stageWidth; |
9 |
ship.y = stage.stageHeight * 0.8; |
10 |
|
11 |
c1 = new Circle(0x0000ff, 15); addChild(c1); |
12 |
c1.alpha = 0.2; |
13 |
|
14 |
/**
|
15 |
* Ship's velocity
|
16 |
*/
|
17 |
velo = new Vector2D(0, -1); |
18 |
updateShip(); |
19 |
|
20 |
stage.addEventListener(KeyboardEvent.KEY_DOWN, handleKey); |
21 |
stage.addEventListener(KeyboardEvent.KEY_UP, handleKey); |
22 |
stage.addEventListener(Event.EXIT_FRAME, handleEnterFrame); |
23 |
|
24 |
/**
|
25 |
* The curve and the calculations
|
26 |
*/
|
27 |
quadratic_equation = new EqQuadratic(); |
28 |
|
29 |
curve_points = new Vector.<Circle>; |
30 |
populate(curve_points, 0xff0000, 3); |
31 |
|
32 |
intersec_points = new Vector.<Circle>; |
33 |
populate(intersec_points, 0xffff00, 3, false); |
34 |
|
35 |
redraw_quadratic_curve(); |
36 |
}
|
37 |
|
38 |
private function handleKey(e:KeyboardEvent):void |
39 |
{
|
40 |
if (e.type == "keyDown") { |
41 |
if (e.keyCode == Keyboard.UP) isUp = true; |
42 |
else if (e.keyCode == Keyboard.DOWN) isDown = true; |
43 |
if (e.keyCode == Keyboard.LEFT) isLeft = true; |
44 |
else if (e.keyCode == Keyboard.RIGHT) isRight = true; |
45 |
}
|
46 |
if (e.type == "keyUp") { |
47 |
if (e.keyCode == Keyboard.UP) isUp = false; |
48 |
else if (e.keyCode == Keyboard.DOWN) isDown = false; |
49 |
if (e.keyCode == Keyboard.LEFT) isLeft = false; |
50 |
else if (e.keyCode == Keyboard.RIGHT) isRight = false; |
51 |
}
|
52 |
}
|
53 |
|
54 |
private function handleEnterFrame(e:Event):void |
55 |
{
|
56 |
/**
|
57 |
* Control the magnitude
|
58 |
*/
|
59 |
if (isUp) velo.setMagnitude(Math.min(velo.getMagnitude()+0.2, 3)); |
60 |
else if(isDown) velo.setMagnitude(Math.max(velo.getMagnitude()-0.2, 1)); |
61 |
|
62 |
/**
|
63 |
* Control the direction
|
64 |
*/
|
65 |
if (isRight) velo.setAngle(velo.getAngle() + 0.03); |
66 |
else if (isLeft) velo.setAngle(velo.getAngle() - 0.03); |
67 |
|
68 |
recalculate_distance(); |
69 |
|
70 |
if (distance < c1.radius) bounce(); |
71 |
updateShip(); |
72 |
}
|
73 |
|
74 |
/**
|
75 |
* Update ship's position, orientation and it's area (the blue-ish circle)
|
76 |
*/
|
77 |
private function updateShip():void { |
78 |
ship.x += velo.x; |
79 |
ship.y += velo.y; |
80 |
ship.rotation = Math2.degreeOf(velo.getAngle()); |
81 |
|
82 |
c1.x = ship.x; |
83 |
c1.y = ship.y; |
84 |
|
85 |
if (ship.x > stage.stageWidth || ship.x < 0) velo.x *= -1; |
86 |
if (ship.y > stage.stageHeight || ship.y < 0) velo.y *= -1; |
87 |
}
|
You can see that the keyboard controls are updating flags to indicate whether the left, up, right, or down keys are being pressed. These flags will be captured by the enterframe event handler and update the magnitude and direction of the ship.
Step 13: Calculating the Reflection Vector
I've already covered the vector calculation of reflection vector in this post. Here, I shall just cover how to obtain the normal vector from gradient.
\[
\frac{df(x)}{dx}=gradient\\
line\ gradient=\frac{y}{x}\\
Assume\ gradient=0.5\\
y=0.5\\
x=1\\
Vector\ of\ left\ normal=
\begin{bmatrix}-1 \\0.5\end{bmatrix}\\
Vector\ of\ right\ normal=
\begin{bmatrix}1 \\-0.5\end{bmatrix}
\]
Step 14: ActionScript Implementation
So the ActionScript below will implement the mathematical concept explained in the previous step. Check out the highlighted lines:
1 |
|
2 |
private function bounce():void |
3 |
{
|
4 |
var gradient:Number = quadratic_equation.diff1(intersec_points[0].x); |
5 |
var grad_vec:Vector2D = new Vector2D(1, gradient); |
6 |
|
7 |
var left_norm:Vector2D = grad_vec.getNormal(false); |
8 |
var right_norm:Vector2D = grad_vec.getNormal(); |
9 |
var chosen_vec:Vector2D; |
10 |
|
11 |
if (velo.dotProduct(left_norm) > 0) chosen_vec = left_norm |
12 |
else chosen_vec = right_norm |
13 |
|
14 |
var chosen_unit:Vector2D = chosen_vec.normalise(); |
15 |
var proj:Number = velo.dotProduct(chosen_unit); |
16 |
|
17 |
chosen_unit.scale(-2*proj); |
18 |
|
19 |
velo = velo.add(chosen_unit); |
20 |
}
|
Conclusion
Well, thanks for your time! If you've found this useful, or have any questions, do leave some comments.