package { /** * * Petri Leskinen, Finland, Espoo, 7th June 2009 * leskinen[dot]petri[at]luukku[dot]com * pixelero.wordpress.com * */ import flash.display.BitmapData; import flash.display.Sprite; import flash.display.Stage; import flash.display.TriangleCulling; import flash.events.KeyboardEvent; import flash.events.MouseEvent; import flash.text.TextField; import flash.events.Event; import flash.geom.Matrix; import flash.geom.Matrix3D; import flash.geom.PerspectiveProjection; import flash.geom.Vector3D; import flash.geom.Utils3D; import flash.utils.getTimer; import GridItem; import LinkedGrid; public class GridMain extends Sprite { public var grid:LinkedGrid, bmd:BitmapData, viewMatrix:Matrix3D, light:Object, gravity:Number = -0.6; private var useMass:Boolean = true; internal var i:int, j:int, tmp:Number; public function GridMain() { x = stage.stageWidth / 2; y = stage.stageHeight / 2; var w:int = 30, h:int = 30, size:Number =1.0; grid = new LinkedGrid(w, h, -size * w / 2, -size * h / 2, size, size); randomFix(); // set the viewpoint var perspective:PerspectiveProjection = new PerspectiveProjection(); perspective.fieldOfView = 70.0; viewMatrix = perspective.toMatrix3D(); viewMatrix.prependTranslation(0, 0, -36); viewMatrix.prependRotation( -75, Vector3D.X_AXIS); light = { x:1, y:0.0, z:0.0 }; normalize(light); grid.bitmapData = defaultBitmapData(); enterFrame(); addEventListener(Event.ENTER_FRAME, enterFrame); stage.addEventListener(MouseEvent.CLICK, clicked); stage.addEventListener(MouseEvent.MOUSE_MOVE, moved); stage.addEventListener(KeyboardEvent.KEY_DOWN, keyPressed); } private var tim:Number = 0.0, counts:int =0; private function enterFrame(e:Event = null):void { tim -= getTimer(); for (var k:int = 0; k != 2;++k) { // initialize values for animation grid.forEach(function (item:GridItem):void { item.mass = item.az = 0.0 } ); // differences in z-dimension grid.forEach(updateItem2); // vertices on the edges need some special handling // because of having less neighbors var item:GridItem = grid.firstItem; // first item, factor=2 - normally 4 neighbors, at corner only 2 item.az *= 2.0; // and top row, factor=4/3 - normally 4 neighbors, at edges 3 while ((item = item.itemRight).itemRight) item.az *= 1.333333; // right top corner item.az *= 2.0; // turn down on right edge while ((item = item.itemBelow).itemBelow) item.az *= 1.333333; // right bottom corner item.az *= 2.0; // back to top left corner item = grid.firstItem; // moving down the left edge while ((item = item.itemBelow).itemBelow) item.az *= 1.333333; // left bottom corner item.az *= 2.0; // bottom row while ((item = item.itemRight).itemRight) item.az *= 1.333333; if (useMass) grid.forArea(updateItem3); // motion, move to new position grid.forEach(updateItem4); } tim += getTimer(); ++counts; //trace ("average time: " + (tim / counts).toString() + "ms"); // // fast version (not using item.mass) average time: 3.39ms // // smoother version with .mass average time: 5.77ms // smoother version with approximate mass: 7.36ms // render(); } private function updateItem1(item:GridItem):void { // initialize values for animation item.mass = item.az = 0.0; } private function updateItem2(item:GridItem):void { // this counts the differences in z-direction if (item.itemRight) { item.az -= item.itemRight.z; item.itemRight.az -= item.z; } if (item.itemBelow) { item.az -= item.itemBelow.z; item.itemBelow.az -= item.z; } } private function updateItem3(item:GridItem):void { var z0:Number = item.z, z1:Number = item.itemRight.z, z2:Number = item.itemBelow.z, z3:Number = item.itemBelow.itemRight.z, // area of triangle in 3D space by three vertices (0,0,z0) (1,0,z1) (0,1,z2) // is given by 0.5*sqrt(1+(z1-z0)² +(z2-z0)²) // // this approximates the area // vertices (0,0,z0) (1,0,z1) (0,1,z2) (1,1,z3), // – which may not be coplanar mass:Number = 0.5 * ( Math.sqrt(1.0 + (tmp = z1 - z0) * tmp + (tmp = z2 - z0) * tmp) + Math.sqrt(1.0 + (tmp = z3 - z1) * tmp + (tmp = z3 - z0) * tmp)); // this 'mass' is added to all four vertices around this block, item.mass += mass; item.itemBelow.mass += mass; item.itemBelow.itemRight.mass += mass; item.itemRight.mass += mass; } private function updateItem4(item:GridItem):void { if (!item.fixed) { // To fully understand the principle of this // google for differential equation for catenary // F = mg - Ty0 - Ty1 -> a = F/m // this is the new acceleration for this vertex, // 0.25*item.az is the effect given by nearby vertices // as calculated in updateItem3 item.az = item.z + 0.25 * item.az; // fast version, not using updateItem3 // item.az = gravity - 15 * item.az; // smoother version, 16.0 controls the acceleration item.az = gravity - 16.0 * item.az / item.mass; // time interval tmp = 1 / 16; // new velocity, 0.02 friction for old velocity item.vz += tmp * item.az -0.02 * item.vz; // new location item.z += tmp * item.vz; } } public function render(e:Event = null) :void { // handle the viewpoint viewMatrix.prependRotation( 4.0*mouseX/stage.stageWidth, new Vector3D(0, 0, 1)); // viewMatrix.prependRotation( (mouseX < 0 ? -0.4:0.4), new Vector3D(0, 0, 1)); // get it done grid.render(graphics, light, viewMatrix); } private function randomFix():void { // release all joints grid.forEach(function (item:GridItem):void { item.fixed = false; }); var item:GridItem; // randomly make some new, choose from three models switch(int(Math.random()*3)){ case 1: makePylon(grid.getItemAt(0.2 * grid.sizeX, 3)); makePylon(grid.getItemAt(0.5 * grid.sizeX, 3)); makePylon(grid.getItemAt(0.8 * grid.sizeX, 3)); makePylon(grid.getItemAt(0.8 * grid.sizeX, grid.sizeY - 5)); makePylon(grid.getItemAt(0.2 * grid.sizeX, grid.sizeY - 5)); makePylon(grid.getItemAt(0.5 * grid.sizeX, grid.sizeY - 5)); break; case 2: makePylon(grid.getItemAt(Math.random() * (grid.sizeX - 1), 0)); makePylon(grid.getItemAt(Math.random() * (grid.sizeX - 1), grid.sizeY - 2)); makePylon(grid.getItemAt(0, Math.random() * (grid.sizeY - 1))); makePylon(grid.getItemAt(grid.sizeX - 2, Math.random() * (grid.sizeY - 1))); break; default: item = grid.getItemAt(Math.random() * grid.sizeX, Math.random() * (grid.sizeY-1)); while ((item = item.itemRight) && (Math.random() < 0.95)) item.fixed = item.itemBelow.fixed = true; item = grid.getItemAt(Math.random() * (grid.sizeX-1),Math.random() * grid.sizeY); while ((item = item.itemBelow) && (Math.random() < 0.95)) item.fixed =item.itemRight.fixed = true; item = grid.getItemAt(Math.random() * grid.sizeX, Math.random() * (grid.sizeY-1)); while ((item = item.itemRight) && (Math.random() < 0.95)) item.fixed = item.itemBelow.fixed=true; } } private function defaultBitmapData():BitmapData { // although it looks metallic, must say these color codes // were taken from a picture of beer ! // color from the darkest to lightest var colors:Vector. = Vector.( [ 0x563333, 0x663933, 0xB75E02, 0xE87701, 0xE2C287, 0xF2E4C0, 0xF6F2E7 , 0xF6F2E7 , 0xFFA72C ]); // notice the bitmapdata is only one pixel high, // the shading is done by counting the u-coordinate by light direction, var bmd:BitmapData = new BitmapData(colors.length, 1, false ); bmd.setVector(bmd.rect, colors); return bmd; } private function clicked(e:MouseEvent):void { detectMouse(e, 4, true); // 4= the strength of given impulse } private function moved(e:MouseEvent):void { gravity = -0.25*mouseY / stage.stageHeight; if (gravity < -0.5) gravity = -0.5; detectMouse(e, -1); } private function keyPressed(e:KeyboardEvent):void { // txt.text = "" + e.keyCode; // enter =13 // pressing esc or delete if (e.keyCode == 13 || e.keyCode == 32) randomFix(); } private function detectMouse(e:Event = null, bounce:Number = 2.0, strong:Boolean=false) :void { // how to find out of 5000 triangles the ones under the mouse ? // some checkup vectors for faster performance // checkX = array of booleans if point's x is smaller/larger than mouseX // checkY likewise for mouseY var checkX:Vector. = new Vector.(grid.gridSize), checkY:Vector. = new Vector.(grid.gridSize); // remember that projectedVerts consists of x-y-pairs grid.forEach(function (item:GridItem):void { checkX[item.id] = (grid.vertices2D[item.id << 1] > mouseX); checkY[item.id] = (grid.vertices2D[(item.id << 1)+1] > mouseY); }); // filters out the triangles completely on the left or right side of the mouse // Out of a spherical mesh something like 50-70 triangles will pass var tr:GridTriangle; for (var i:int = grid.triangles.length; --i != -1; ){ //for each (var tr:GridTriangle in grid.triangles ) { // check if x passes // Out of a spherical mesh something like 50-70 triangles will pass tr = grid.triangles[i]; if (!(checkX[tr.id0] == checkX[tr.id1] && checkX[tr.id0] == checkX[tr.id2])) { // check if y passes // now the number of triangles is reduced to ~5-10 if (!(checkY[tr.id0] == checkY[tr.id1] && checkY[tr.id0] == checkY[tr.id2])) { // give that point some vertical velocity tr.point0.bounce(bounce); if (strong) tr.point1.bounce(bounce); if (strong) tr.point2.bounce(bounce); // for performance, exits this loop as soon as the first // possible triangle is found i = 0; } } } } // fixes four points internal function makePylon (item:GridItem):void { item.fixed = true; if (item.itemBelow) item.itemBelow.fixed = true; if (item.itemRight) { item.itemRight.fixed = true; if (item.itemRight.itemBelow) item.itemRight.itemBelow.fixed = true; } } /* public function testArea(item:GridItem):void { graphics.beginFill(0x123456, 1); item.moveTo(graphics); item.itemRight.lineTo(graphics); item.itemRight.itemBelow.lineTo(graphics); item.itemBelow.lineTo(graphics); graphics.endFill(); } public function testFunction(item:GridItem, _color:uint=0x00, _width:Number = 3.0):void { graphics.lineStyle(_width, _color, 1); if (item.itemRight) { graphics.moveTo(item.x, item.y); graphics.lineTo(item.itemRight.x, item.itemRight.y); } if (item.itemBelow) { graphics.moveTo(item.x, item.y); graphics.lineTo(item.itemBelow.x, item.itemBelow.y); } } */ private function normalize(o:*):void { tmp = 1.0 / Math.sqrt(o.x * o.x + o.y * o.y + o.z * o.z); o.x *= tmp; o.y *= tmp; o.z *= tmp; } /* // Code for running the tests: import flash.utils.getTimer; public function GridTest() { var txt:TextField = new TextField(); addChild(txt); txt.width = 200; var w:int = 45, h:int = 45, size:Number =1.0; grid = new LinkedGrid(w, h, -size * w / 2, -size * h / 2, size, size); var tim:Number = -getTimer(); tim = -getTimer(); for (var k:int = 0; k != 6000;k++) { grid.items[int(grid.sizeX*Math.random())][int(grid.sizeY*Math.random())].z=1.0; } tim += getTimer(); txt.htmlText = "Vector[][] : " + tim.toString() + "ms"; trace("Vector[][] : " + tim.toString() + "ms"); tim = -getTimer(); for (k = 0; k != 6000; k++) { grid.getItemAt(int(grid.sizeX*Math.random()),int(grid.sizeY*Math.random())).z=1.0; // grid.items[int(grid.sizeX*Math.random())][int(grid.sizeY*Math.random())].z=1.0; } tim += getTimer(); txt.htmlText = "getItemAt() : " + tim.toString() + "ms"; trace("getItemAt() : " + tim.toString() + "ms"); } public function GridTestLooping() :void { var txt:TextField = new TextField(); addChild(txt); txt.width = 200; var w:int = 45, h:int = 45, size:Number =1.0; grid = new LinkedGrid(w, h, -size * w / 2, -size * h / 2, size, size); var tim:Number = -getTimer(); tim = -getTimer(); for (var k:int = 0; k != 2000;k++) { grid.forEach(function (item:GridItem):void { item.normalx = item.normaly = 0.0; item.normalz = 1.0; }); } tim += getTimer(); txt.htmlText = "inline function : " + tim.toString() + "ms"; trace("inline function : " + tim.toString() + "ms"); tim = -getTimer(); for (k = 0; k != 2000;k++) { grid.forEach(ff); } tim += getTimer(); trace("function : " + tim.toString()+"ms"); txt.htmlText += "\nfunction : " + tim.toString() + "ms" tim = -getTimer(); for (k = 0; k != 2000;k++) { grid.forEach(GridItem.resetNormal); } tim += getTimer(); trace("static function : " + tim.toString()+"ms"); txt.htmlText += "\nstatic function : " + tim.toString() + "ms" tim = -getTimer(); for (k = 0; k != 2000;k++) { var itemX:GridItem = grid.firstItem, itemY:GridItem = itemX; do { do { itemX.normalx = itemX.normaly = 0.0; itemX.normalz = 1.0; } while (itemY = itemY.itemBelow) } while (itemY = itemX = itemX.itemRight) } tim += getTimer(); trace("do-while : " + tim.toString()+"ms"); txt.htmlText += "\ndo-while : " + tim.toString() + "ms" } private function ff(item:GridItem) :void { item.normalx = item.normaly = 0.0; item.normalz = 1.0; } */ } }