08
transformation

changing a to b

when it's numbers we call it math, and when it's images we call it transformation, but it's the same basic operations in both cases—taking one range of values and mapping it to another. this is a fundamental theme in graphics programming.

interval notation

for starters, we have to be able to communicate exactly what numbers a range may include or exclude. fortunately, mathematicians have worked this out for us. they call it interval notation. it uses brackets ([ ]), parentheses (( )) and two numbers separated by a comma.

the numbers are the edges of the range (a.k.a. interval), and the smaller one comes first. if it's part of the range, it is enclosed in a square bracket ([—inclusive). if the range starts immediately after the number, it is enclosed in a parenthesis ((—exclusive). the same goes for the larger number, if it's included in the range, it is enclosed in a square bracket (]—inclusive). if the range ends immediately before the second number, it is enclosed in a parenthesis ()—exclusive). some examples:

also, we can say a variable is in a range with the ‘element of’ operator: , e.g.: x ∈ [0,1).

transforming numbers

so how can we map one range to another? it all boils down to a combination of sliding the brackets along the number line with the + or - operators, and changing the distance between the brackets with the * and / operators.

even though there are an infinite number of values contained in any given range, by the very definition of the number line we know that all those values will always be between the ends of the range. so if we just concentrate on operations that put the ends where we need them, we can rest assured that the values in between will follow. let's look at some examples:

[5,10] → [7,12]

x       // x  [5,10]
x += 2; // x  [7,12]
[5,10] → [25,45]

x       // x  [5,10]
x *= 4; // x  [20,40]
x += 5; // x  [25,45]
[50,99] → [-5,3]

x        // x  [50,99]
x -= 50; // x  [0,49]
x /= 49; // x  [0,1]
x *= 8;  // x  [0,8]
x -= 5;  // x  [-5,3]

normalization

the range [0,1] is a special and very handy one. because of the unique multiplicative properties of zero, the low end becomes an anchor point for multiplication and division operations. only the high end bracket moves. this makes it the perfect intermediate range, as it can easily be stretched to the correct magnitude, and then slid to the correct position on the numberline.

by mastering simple transformations of the numberline, we gain control over visual transformations as well.

 

transforming imagery

by employing the same number moving techniques to the endpoints of line segments, we can start moving shapes.

translation

some initial setup
import flash.display.Graphics;

var P1:Point = new Point(.4, .2);
var P2:Point = new Point(.4, .8);
var shift:Number = .15;

function drawLine(g:Graphics, p1:Point, p2:Point, weight:Number=1, alpha:Number=1, color:uint=0x000000):void {
   var w:Number = stage.stageWidth;
   var h:Number = stage.stageHeight;
   g.lineStyle(weight, color, alpha);
   g.moveTo(P1.x*w, P1.y*h);
   g.lineTo(P2.x*w, P2.y*h);
}

// original
drawLine(graphics, P1, P2, 4, .3);
one coordinate of one endpoint
// shift P1 x
P1.x += shift;
drawLine(graphics, P1, P2, 4, .6);
same coordinate of both endpoints
// P1 x and P2 x both shifted
P2.x += shift;
drawLine(graphics, P1, P2, 4, 1);

scale

remember that multiplication by a value less than one is similar to division by a value greater than one, multiplication by zero is always zero, and the origin is coordinate (0, 0).
(x * .5) == (x / 2)
(x *  0) == 0
some initial setup (set your canvas to a square dimension to make the results more intuitive)
import flash.display.Graphics;

var P1:Point = new Point(.2, .6);
var P2:Point = new Point(.6, .2);
var scale:Number;

function drawLine(g:Graphics, p1:Point, p2:Point, weight:Number=1, alpha:Number=1, color:uint=0x000000):void {
   var w:Number = stage.stageWidth;
   var h:Number = stage.stageHeight;
   g.lineStyle(weight, color, alpha);
   g.moveTo(P1.x*w, P1.y*h);
   g.lineTo(P2.x*w, P2.y*h);
}

// original
drawLine(graphics, P1, P2, 4, .3);
scaling
// scale down (towards origin)
scale = .5;
P1.x *= scale;
P1.y *= scale;
P2.x *= scale;
P2.y *= scale;
drawLine(graphics, P1, P2, 4, .6);

// scale up (away from origin)
scale = 3;
P1.x *= scale;
P1.y *= scale;
P2.x *= scale;
P2.y *= scale;
drawLine(graphics, P1, P2, 4, 1);

rotation

there isn't a good analogy on the number line for rotation, since it's a two dimensional transformation, and the number line is one-dimensional. but intuitively, we can imagine sliding each endpoint along a circle that is centered at the origin and has a radius equal to the distance of the endpoint from the origin.

examples

the following examples make use of these initial statements:
import flash.display.Graphics;
import flash.geom.Point;

function drawLine(g:Graphics, p1:Point, p2:Point, weight:Number=3, alpha:Number=1, color:uint=0x000000):void {
   var w:Number = stage.stageWidth;
   var h:Number = stage.stageHeight;
   g.lineStyle(weight, color, alpha);
   g.moveTo(P1.x*w, P1.y*h);
   g.lineTo(P2.x*w, P2.y*h);
}

function drawRegion(g:Graphics, points:Array, weight:Number=3, alpha:Number=1, color:uint=0x000000):void {
   var w:Number = stage.stageWidth;
   var h:Number = stage.stageHeight;
   g.lineStyle(weight, color, alpha);
   g.moveTo(points[0].x*w, points[0].y*h);
   for (var i:Number = 1; i < points.length; i++) {
      g.lineTo(points[i].x*w, points[i].y*h);
   }
   g.lineTo(points[0].x*w, points[0].y*h);
}

function scale(P:Point, factor:Number, C:Point):void {
   P.offset(-C.x, -C.y);
   P.x *= factor;
   P.y *= factor;
   P.offset(C.x, C.y);
}

function rotate(P:Point, theta:Number, C:Point):void {
   var cosTheta:Number = Math.cos(theta);
   var sinTheta:Number = Math.sin(theta);
   var centered_x:Number = P.x - C.x;
   var centered_y:Number = P.y - C.y;
   P.x = (cosTheta*centered_x - sinTheta*centered_y) + C.x;
   P.y = (sinTheta*centered_x + cosTheta*centered_y) + C.y;
}

translate

line gradient
var P1:Point = new Point(0, 0);
var P2:Point = new Point(1, 0);
var shift:Number = .005;

while (P1.y <= 1) {
   drawLine(graphics, P1, P2);
   P1.offset(0, shift);
   P2.offset(0, shift);
   shift *= 1.25;
}
line mesh
var P1:Point = new Point(0, 0);
var P2:Point = new Point(0, 1);
var shift:Number = .05;

while (P1.y <= 1) {
   drawLine(graphics, P1, P2);
   P1.offset(0, shift);
   P2.offset(shift, 0);
}

scale

concentric shapes
var P1:Point = new Point(0, 0);
var P2:Point = new Point(1, 0);
var P3:Point = new Point(1, 1);
var P4:Point = new Point(0, 1);
var R:Array = [P1, P2, P3, P4];
var C:Point = new Point(.5, .5);
var factor:Number = .85;

while (P2.x - P1.x > .15) {
   drawRegion(graphics, R);
   for (var p:Number = 0; p < R.length; p++) {
      var point:Point = R[p];
      scale(point, factor, C);
   }
}

rotate

rotated shapes
var P1:Point = new Point(.25, .25);
var P2:Point = new Point(.75, .25);
var P3:Point = new Point(.75, .75);
var P4:Point = new Point(.25, .75);
var R:Array = [P1, P2, P3, P4];
var C:Point = new Point(.5, .5);
var angle:Number = .2;

for (var i:Number = 0; i < Math.PI/2/angle; i++) {
   drawRegion(graphics, R);
   for (var p:Number = 0; p < R.length; p++) {
      var point:Point = R[p];
      rotate(point, angle, C);
   }
}