/** * Handles orbiting around an target object using spherical math * see http://mathworld.wolfram.com/SphericalCoordinates.html for examples. * Note that Y is the central "north-south" axis for this code. * @author John Dyer (jdyer@dts.edu) */ package { import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import flash.events.TimerEvent; import flash.ui.Keyboard; import flash.utils.Timer; import org.papervision3d.core.Number3D; import org.papervision3d.core.proto.CameraObject3D; import flash.display.Stage; import flash.display.StageDisplayState; import org.papervision3d.objects.DisplayObject3D; public class MouseOrbiter { public var isActive :Boolean = true; public var zoomIncriment:Number = 1000; public static var phiMax:Number = Math.PI/2; public static var phiMin:Number = -Math.PI/2; private var target:DisplayObject3D; private var camera:CameraObject3D; private var container:Sprite; private var timer:Timer; private var lastTheta:Number =0; private var lastPhi:Number =0; private var lastRadius:Number =0; public function MouseOrbiter(cam:CameraObject3D, target:DisplayObject3D, container:Sprite, stage:Stage) { this.target = target; this.camera = cam; this.container = container; stage.addEventListener( MouseEvent.MOUSE_DOWN, mouseDownHandler ); stage.addEventListener( MouseEvent.MOUSE_UP, mouseUpHandler ); stage.addEventListener( MouseEvent.MOUSE_WHEEL, mouseWheelHandler); startTimer(); } /********************************* MOUSE FUNCTIONALITY: START *********************************/ private var mouseIsPressed:Boolean = false; private var mouseXAtPress:Number = 0; private var mouseYAtPress:Number = 0; private var cameraPhiAtPress:Number = 0; private var cameraThetaAtPress:Number = 0; private function mouseWheelHandler( event :MouseEvent ):void { if (isActive) adjustDistance(-event.delta*zoomIncriment); } private function mouseDownHandler( event :MouseEvent ):void { mouseIsPressed = true; mouseXAtPress = container.mouseX; mouseYAtPress = container.mouseY; cameraPhiAtPress = lastPhi; cameraThetaAtPress = lastTheta; } private function mouseUpHandler( event :MouseEvent ):void { mouseIsPressed = false; } /********************************* MOUSE FUNCTIONALITY: END *********************************/ /********************************* STATIC MATH FUNCTIONS: START *********************************/ public static function getOffsetPosition(obj:DisplayObject3D, target:DisplayObject3D = null):Number3D { var x:Number = obj.x; var y:Number = obj.y; var z:Number = obj.z; if (target != null) { x -= target.x; y -= target.y; z -= target.z; } return new Number3D(x,y,z); } public static function getAngularPosition(obj:DisplayObject3D, target:DisplayObject3D = null):Object { var offset:Number3D = getOffsetPosition(obj, target); // calculate current radius and angles // Y-UP var radius:Number = Math.sqrt(offset.x*offset.x + offset.y*offset.y + offset.z*offset.z); var theta:Number = Math.atan2(offset.z, offset.x); var phi:Number = Math.acos(offset.y/radius); return {theta: theta, phi: phi, radius: radius}; } public static function calculateAngularPosition(obj:DisplayObject3D, theta:Number, phi:Number, radius:Number, target:DisplayObject3D = null):Number3D { var offset:Number3D = getOffsetPosition(obj, target); // limit phi (normally -pi/2 - pi/2) if (phi < phiMin) { phi = phiMin; } else if (phi > phiMax) { phi = phiMax; } // adjust theta (0 - 2pi) if (theta < -Math.PI) { theta = Math.PI*2 + theta; } else if (theta > Math.PI) { theta = theta - Math.PI*2; } // note: Z and Y have been flipped to make Y be the central axis // see http://mathworld.wolfram.com/SphericalCoordinates.html for explanation var sinPhi:Number = Math.sin(phi); var newX:Number = radius*Math.cos(theta)*sinPhi; var newZ:Number = radius*Math.sin(theta)*sinPhi ; var newY:Number = radius*Math.cos(phi); // re-adjust to target if (target != null) { newX += target.x; newY += target.y; newZ += target.z; } return new Number3D(newX,newY,newZ); } public static function setAngularPosition(obj:DisplayObject3D, theta:Number, phi:Number, radius:Number, target:DisplayObject3D = null):void { var pos:Number3D = MouseOrbiter.calculateAngularPosition(obj, theta, phi, radius, target); obj.x = pos.x; obj.y = pos.y; obj.z = pos.z; } /********************************* STATIC MATH FUNCTIONS: END *********************************/ /********************************* INSTANCE METHODS: START *********************************/ public function changeTarget(newTarget:DisplayObject3D):void { this.target = (newTarget != null) ? newTarget : new DisplayObject3D(); goToAngle(lastTheta, lastPhi, lastRadius); } public function adjustAngle(dTheta:Number, dPhi:Number):void { var position:Object = MouseOrbiter.getAngularPosition(camera, target); // adjust angles position.theta += dTheta; position.phi += dPhi; // reposition obj goToAngle(position.theta, position.phi, position.radius); } public function goToAngle(theta:Number, phi:Number, radius:Number):void { // calculate and set new position var newPosition:Number3D = MouseOrbiter.calculateAngularPosition(camera,theta,phi,radius,target); camera.x = newPosition.x; camera.y = newPosition.y; camera.z = newPosition.z; lastTheta = theta; lastPhi = phi; lastRadius = radius; // look at target camera.lookAt(target); } public function adjustDistance(dR:Number):void { var position:Object = MouseOrbiter.getAngularPosition(camera, target); // adjust distance position.radius += dR; if (position.radius < 0) position.radius = -position.radius; goToAngle(position.theta, position.phi, position.radius); } public function goToDistance(newDistance:Number):void { var position:Object = MouseOrbiter.getAngularPosition(camera, target); // reposition obj at new distance goToAngle(position.theta, position.phi, newDistance); } /********************************* INSTANCE MATH: END *********************************/ private function startTimer():void { timer = new Timer(1000/60); // 60 frames per second timer.addEventListener(TimerEvent.TIMER, onTimer); timer.start(); } private function onTimer( event: Event ): void { if (isActive ) { updateCamera(); } } private function updateCamera( ): void { if (mouseIsPressed) { var dTheta:Number = 0; var dPhi:Number = 0; var offset:Number3D = MouseOrbiter.getOffsetPosition(camera,target); // calculate radius and current angles var radius:Number = Math.sqrt(offset.x*offset.x + offset.y*offset.y + offset.z*offset.z); var theta:Number = cameraThetaAtPress; var phi:Number = cameraPhiAtPress; if (isNaN(cameraPhiAtPress) || isNaN(cameraThetaAtPress)) { cameraThetaAtPress = theta = Math.atan2(offset.z, offset.x); cameraPhiAtPress = phi = Math.acos(offset.y/radius); } // adjust theta var dX:Number = container.mouseX - mouseXAtPress; dTheta = dX/Math.PI/80; theta = theta - dTheta; // adjust phi var dY:Number = container.mouseY - mouseYAtPress; dPhi = dY/Math.PI/80; phi = phi - dPhi; goToAngle(theta, phi, radius); } else { // make sure to position the camera again in case // the target is moving. goToAngle(lastTheta, lastPhi, lastRadius); } } } }