import Finger; import flash.geom.Point; import flash.geom.Matrix; import flash.geom.Rectangle; class RightTriangles { public static var out:TextField; public static var clip:MovieClip; public static var initialized:Boolean; public static var finger1:Finger; public static var finger2:Finger; public static var finger3:Finger; public static var box:MovieClip; public static var startMatrix:Matrix; public static var startFinger1:Point; public static var startFinger2:Point; public static var startFinger3:Point; public static var verbose:Boolean = false; public static function main(mc:MovieClip):Void { clip = mc; initialized = false; Stage.align = 'TL'; Stage.scaleMode = 'noScale'; Stage.addListener(RightTriangles); onResize(); } private static function onResize():Void { if(!initialized) init(); } public static function cleanUp():Void { clip.clear(); finger3._visible = false; } public static function drawLine(a:Point, b:Point, t:Number):Void { clip.lineStyle(0, 0x000000, t); clip.moveTo(a.x, a.y); clip.lineTo(b.x, b.y); clip.lineStyle(); } /** * Given two points, return a third point that forms an equilateral * right triangle, if the line between the two given points is the * hypotenuse. * * There's no particular reason this triangle has to be right or * equilateral - it just needs to be predictable, congruent, and * easy to eyeball for correctness. */ public static function deriveThirdPoint(f1:Point, f2:Point):Point { // a vector from finger 1 to finger 2 var vec:Point = f2.subtract(f1); var vecAngle:Number = Math.atan2(vec.y, vec.x); var vecLength:Number = vec.length; // two more vectors, one for each leg of the equilateral right triangle // (a triangle can be specified with the length of a side and two angles) var vec1:Point = Point.polar(vecLength, vecAngle + Math.PI/4); var vec2:Point = Point.polar(vecLength, vecAngle + 3*Math.PI/4); // slope and intercept for each line // intercept derived from http://mathworld.wolfram.com/Line.html (2) var slope1:Number = vec1.y / vec1.x; var intercept1:Number = f1.y - slope1 * f1.x; var slope2:Number = vec2.y / vec2.x; var intercept2:Number = f2.y - slope2 * f2.x; // solve for x and y of the mysterious third finger var f3:Point = new Point(0, 0); f3.x = (intercept2 - intercept1) / (slope1 - slope2); f3.y = slope1 * f3.x + intercept1; return f3; } /** * We know where two of the fingers are, but need to derive the position * of the third finger and draw a triangle on the base clip to show it. */ public static function nailDownFingers(verbose:Boolean):Void { clip.clear(); // points that correspond to the position of each finger... var f1:Point = new Point(finger1._x, finger1._y); var f2:Point = new Point(finger2._x, finger2._y); var f3:Point = deriveThirdPoint(f1, f2); if(verbose) log('Fingers '+f1.toString()+', '+f2.toString()+' -> '+f3.toString()); // draw the lines between them drawLine(f1, f2, 100); drawLine(f1, f3, 50); drawLine(f2, f3, 50); finger3._visible = true; finger3._x = f3.x; finger3._y = f3.y; } public static function deformBox(verbose:Boolean):Void { // first things first nailDownFingers(verbose); // figure out the transform from the beginning of the current movement to here var xt:Array = linearSolution(startFinger1.x, startFinger1.y, finger1._x, startFinger2.x, startFinger2.y, finger2._x, startFinger3.x, startFinger3.y, finger3._x); var yt:Array = linearSolution(startFinger1.x, startFinger1.y, finger1._y, startFinger2.x, startFinger2.y, finger2._y, startFinger3.x, startFinger3.y, finger3._y); /* Flash seems to switch the 'b' and 'c' arguments from what common sense dictates. According to the documentation, the arguments (a, b, c, d, tx, ty) correspond to this matrix: a b tx c d ty 0 0 1 However, tested behavior of flash.geom.Matrix seems to indicate that they really mean this matrix: a c tx b d ty 0 0 1 Thus, this turns out to be true of the transformation: x' = (a * x) + (c * y) + tx y' = (b * x) + (d * y) + ty So we're switching the arguments here to match, under protest. */ var a:Number = xt[0]; var b:Number = yt[0]; // ?!? var c:Number = xt[1]; // !!1! var d:Number = yt[1]; var tx:Number = xt[2]; var ty:Number = yt[2]; // transformation at point when the movement started var o:Matrix = startMatrix.clone(); // transformation over the course of the current movement var m:Matrix = new Matrix(a, b, c, d, tx, ty); // the two transformations combined o.concat(m); box.transform.matrix = o; box.blendMode = 'multiply'; if(verbose) log('Deformed: ' + box.transform.matrix.toString()); } public static function markStart():Void { // have to know where #3 is nailDownFingers(false); startMatrix = box.transform.matrix.clone(); startFinger1 = new Point(finger1._x, finger1._y); startFinger2 = new Point(finger2._x, finger2._y); startFinger3 = new Point(finger3._x, finger3._y); } public static function onFingerPressed(finger:Finger):Void { log('\nStart: ' + box.transform.matrix.toString()); markStart(); deformBox(verbose); } public static function onFingerMoved(finger:Finger):Void { deformBox(false); } public static function onFingerReleased(finger:Finger):Void { deformBox(verbose); cleanUp(); log('End: ' + box.transform.matrix.toString()); } /** * Solves a system of linear equations. * * z1 = (a * x1) + (b * y1) + c * z2 = (a * x2) + (b * y2) + c * z3 = (a * x3) + (b * y3) + c * * x1 - z3 are the known values. * a, b, c are the unknowns to be solved. * generates a function which will return z for any x, y. * ask mike about pages 78-79 of his notebook for more info. */ public static function linearSolution(x1:Number, y1:Number, z1:Number, x2:Number, y2:Number, z2:Number, x3:Number, y3:Number, z3:Number):Array { var a:Number = (((z2 - z3) * (y1 - y2)) - ((z1 - z2) * (y2 - y3))) / (((x2 - x3) * (y1 - y2)) - ((x1 - x2) * (y2 - y3))); var b:Number = (((z2 - z3) * (x1 - x2)) - ((z1 - z2) * (x2 - x3))) / (((y2 - y3) * (x1 - x2)) - ((y1 - y2) * (x2 - x3))); var c:Number = z1 - (x1 * a) - (y1 * b); return [a, b, c]; } public static function init():Void { initialized = true; // a place for debug info out = clip.createTextField('out', 1, 0, 0, 800, 600); out.text = 'Hello World (' + Stage.width + 'x' + Stage.height + ')'; out.selectable = false; out.textColor = 0xCCCCCC; out._visible = false; box = clip.createEmptyMovieClip('box', 2); // a rectangle for our photo box var r:Rectangle = new Rectangle(30, 30, 240, 180); box._x = r.left; box._y = r.top; box.loadMovie('http://mike.teczno.com/img/right-triangles/photo.jpg'); // add some fingers finger1 = Finger(clip.attachMovie(Finger.symbolName, 'finger1', 11)); finger1._x = r.left + (Math.random() * r.width/2); finger1._y = r.top + (Math.random() * r.height); finger2 = Finger(clip.attachMovie(Finger.symbolName, 'finger2', 12)); finger2._x = r.right - (Math.random() * r.width/2); finger2._y = r.top + (Math.random() * r.height); finger3 = Finger(clip.attachMovie(Finger.symbolName, 'finger3', 13)); finger3._visible = false; // so we know which is which finger1._rotation = 15; finger2._rotation = -15; finger3._alpha = 50; cleanUp(); } public static function log(msg:String):Void { out.text += '\n'; out.text += msg; out.scroll = out.maxscroll; } }