text: /*global define*/
  define([
          '../Core/Cartesian3',
          '../Core/Cartographic',
          '../Core/defined',
          '../Core/destroyObject',
          '../Core/DeveloperError',
          '../Core/EasingFunction',
          '../Core/Math',
          '../Core/Matrix4',
          '../Core/Ray',
          '../Core/ScreenSpaceEventHandler',
          '../Core/ScreenSpaceEventType',
          '../Core/Transforms',
          './Camera',
          './OrthographicFrustum',
          './OrthographicOffCenterFrustum',
          './PerspectiveFrustum',
          './SceneMode'
      ], function(
          Cartesian3,
          Cartographic,
          defined,
          destroyObject,
          DeveloperError,
          EasingFunction,
          CesiumMath,
          Matrix4,
          Ray,
          ScreenSpaceEventHandler,
          ScreenSpaceEventType,
          Transforms,
          Camera,
          OrthographicFrustum,
          OrthographicOffCenterFrustum,
          PerspectiveFrustum,
          SceneMode) {
      'use strict';
  
      /**
       * @private
       */
      function SceneTransitioner(scene) {
          //>>includeStart('debug', pragmas.debug);
          if (!defined(scene)) {
              throw new DeveloperError('scene is required.');
          }
          //>>includeEnd('debug');
  
          this._scene = scene;
          this._currentTweens = [];
          this._morphHandler = undefined;
          this._morphCancelled = false;
          this._completeMorph = undefined;
          this._morphToOrthographic = false;
      }
  
      SceneTransitioner.prototype.completeMorph = function() {
          if (defined(this._completeMorph)) {
              this._completeMorph();
          }
      };
  
      SceneTransitioner.prototype.morphTo2D = function(duration, ellipsoid) {
          if (defined(this._completeMorph)) {
              this._completeMorph();
          }
  
          var scene = this._scene;
          this._previousMode = scene.mode;
          this._morphToOrthographic = scene.camera.frustum instanceof OrthographicFrustum;
  
          if (this._previousMode === SceneMode.SCENE2D || this._previousMode === SceneMode.MORPHING) {
              return;
          }
          this._scene.morphStart.raiseEvent(this, this._previousMode, SceneMode.SCENE2D, true);
  
          scene._mode = SceneMode.MORPHING;
          scene.camera._setTransform(Matrix4.IDENTITY);
  
          if (this._previousMode === SceneMode.COLUMBUS_VIEW) {
              morphFromColumbusViewTo2D(this, duration);
          } else {
              morphFrom3DTo2D(this, duration, ellipsoid);
          }
  
          if (duration === 0.0 && defined(this._completeMorph)) {
              this._completeMorph();
          }
      };
  
      var scratchToCVPosition = new Cartesian3();
      var scratchToCVDirection = new Cartesian3();
      var scratchToCVUp = new Cartesian3();
      var scratchToCVPosition2D = new Cartesian3();
      var scratchToCVDirection2D = new Cartesian3();
      var scratchToCVUp2D = new Cartesian3();
      var scratchToCVSurfacePosition = new Cartesian3();
      var scratchToCVCartographic = new Cartographic();
      var scratchToCVToENU = new Matrix4();
      var scratchToCVFrustumPerspective = new PerspectiveFrustum();
      var scratchToCVFrustumOrthographic = new OrthographicFrustum();
      var scratchToCVCamera = {
          position : undefined,
          direction : undefined,
          up : undefined,
          position2D : undefined,
          direction2D : undefined,
          up2D : undefined,
          frustum : undefined
      };
  
      SceneTransitioner.prototype.morphToColumbusView = function(duration, ellipsoid) {
          if (defined(this._completeMorph)) {
              this._completeMorph();
          }
  
          var scene = this._scene;
          this._previousMode = scene.mode;
  
          if (this._previousMode === SceneMode.COLUMBUS_VIEW || this._previousMode === SceneMode.MORPHING) {
              return;
          }
          this._scene.morphStart.raiseEvent(this, this._previousMode, SceneMode.COLUMBUS_VIEW, true);
  
          scene.camera._setTransform(Matrix4.IDENTITY);
  
          var position = scratchToCVPosition;
          var direction = scratchToCVDirection;
          var up = scratchToCVUp;
  
          if (duration > 0.0) {
              position.x = 0.0;
              position.y = -1.0;
              position.z = 1.0;
              position = Cartesian3.multiplyByScalar(Cartesian3.normalize(position, position), 5.0 * ellipsoid.maximumRadius, position);
  
              Cartesian3.negate(Cartesian3.normalize(position, direction), direction);
              Cartesian3.cross(Cartesian3.UNIT_X, direction, up);
          } else {
              var camera = scene.camera;
              if (this._previousMode === SceneMode.SCENE2D) {
                  Cartesian3.clone(camera.position, position);
                  position.z = camera.frustum.right - camera.frustum.left;
                  Cartesian3.negate(Cartesian3.UNIT_Z, direction);
                  Cartesian3.clone(Cartesian3.UNIT_Y, up);
              } else {
                  Cartesian3.clone(camera.positionWC, position);
                  Cartesian3.clone(camera.directionWC, direction);
                  Cartesian3.clone(camera.upWC, up);
  
                  var surfacePoint = ellipsoid.scaleToGeodeticSurface(position, scratchToCVSurfacePosition);
                  var toENU = Transforms.eastNorthUpToFixedFrame(surfacePoint, ellipsoid, scratchToCVToENU);
                  Matrix4.inverseTransformation(toENU, toENU);
  
                  scene.mapProjection.project(ellipsoid.cartesianToCartographic(position, scratchToCVCartographic), position);
                  Matrix4.multiplyByPointAsVector(toENU, direction, direction);
                  Matrix4.multiplyByPointAsVector(toENU, up, up);
              }
          }
  
          var frustum;
          if (this._morphToOrthographic) {
              frustum = scratchToCVFrustumOrthographic;
              frustum.width = scene.camera.frustum.right - scene.camera.frustum.left;
              frustum.aspectRatio = scene.drawingBufferWidth / scene.drawingBufferHeight;
          } else {
              frustum = scratchToCVFrustumPerspective;
              frustum.aspectRatio = scene.drawingBufferWidth / scene.drawingBufferHeight;
              frustum.fov = CesiumMath.toRadians(60.0);
          }
  
          var cameraCV = scratchToCVCamera;
          cameraCV.position = position;
          cameraCV.direction = direction;
          cameraCV.up = up;
          cameraCV.frustum = frustum;
  
          var complete = completeColumbusViewCallback(cameraCV);
          createMorphHandler(this, complete);
  
          if (this._previousMode === SceneMode.SCENE2D) {
              morphFrom2DToColumbusView(this, duration, cameraCV, complete);
          } else {
              cameraCV.position2D = Matrix4.multiplyByPoint(Camera.TRANSFORM_2D, position, scratchToCVPosition2D);
              cameraCV.direction2D = Matrix4.multiplyByPointAsVector(Camera.TRANSFORM_2D, direction, scratchToCVDirection2D);
              cameraCV.up2D = Matrix4.multiplyByPointAsVector(Camera.TRANSFORM_2D, up, scratchToCVUp2D);
  
              scene._mode = SceneMode.MORPHING;
              morphFrom3DToColumbusView(this, duration, cameraCV, complete);
          }
  
          if (duration === 0.0 && defined(this._completeMorph)) {
              this._completeMorph();
          }
      };
  
      SceneTransitioner.prototype.morphTo3D = function(duration, ellipsoid) {
          if (defined(this._completeMorph)) {
              this._completeMorph();
          }
  
          var scene = this._scene;
          this._previousMode = scene.mode;
  
          if (this._previousMode === SceneMode.SCENE3D || this._previousMode === SceneMode.MORPHING) {
              return;
          }
          this._scene.morphStart.raiseEvent(this, this._previousMode, SceneMode.SCENE3D, true);
  
          scene._mode = SceneMode.MORPHING;
          scene.camera._setTransform(Matrix4.IDENTITY);
  
          if (this._previousMode === SceneMode.SCENE2D) {
              morphFrom2DTo3D(this, duration, ellipsoid);
          } else {
              var camera3D;
              if (duration > 0.0) {
                  camera3D = scratchCVTo3DCamera;
                  Cartesian3.fromDegrees(0.0, 0.0, 5.0 * ellipsoid.maximumRadius, ellipsoid, camera3D.position);
                  Cartesian3.negate(camera3D.position, camera3D.direction);
                  Cartesian3.normalize(camera3D.direction, camera3D.direction);
                  Cartesian3.clone(Cartesian3.UNIT_Z, camera3D.up);
              } else {
                  camera3D = getColumbusViewTo3DCamera(this, ellipsoid);
  
                  var frustum;
                  if (this._morphToOrthographic) {
                      var camera = scene.camera;
                      frustum = scratch2DTo3DFrustumOrtho;
                      frustum.aspectRatio = scene.drawingBufferWidth / scene.drawingBufferHeight;
                      frustum.width = camera.frustum.right - camera.frustum.left;
                  } else {
                      frustum = scratch2DTo3DFrustumPersp;
                      frustum.aspectRatio = scene.drawingBufferWidth / scene.drawingBufferHeight;
                      frustum.fov = CesiumMath.toRadians(60.0);
                  }
                  camera3D.frustum = frustum;
              }
              var complete = complete3DCallback(camera3D);
              createMorphHandler(this, complete);
  
              morphFromColumbusViewTo3D(this, duration, camera3D, complete);
          }
  
          if (duration === 0.0 && defined(this._completeMorph)) {
              this._completeMorph();
          }
      };
  
      /**
       * Returns true if this object was destroyed; otherwise, false.
       * <br /><br />
       * If this object was destroyed, it should not be used; calling any function other than
       * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
       *
       * @returns {Boolean} <code>true</code> if this object was destroyed; otherwise, <code>false</code>.
       */
      SceneTransitioner.prototype.isDestroyed = function() {
          return false;
      };
  
      /**
       * Once an object is destroyed, it should not be used; calling any function other than
       * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.  Therefore,
       * assign the return value (<code>undefined</code>) to the object as done in the example.
       *
       * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
       *
       * @example
       * transitioner = transitioner && transitioner.destroy();
       */
      SceneTransitioner.prototype.destroy = function() {
          destroyMorphHandler(this);
          return destroyObject(this);
      };
  
      function createMorphHandler(transitioner, completeMorphFunction) {
          if (transitioner._scene.completeMorphOnUserInput) {
              transitioner._morphHandler = new ScreenSpaceEventHandler(transitioner._scene.canvas, false);
  
              var completeMorph = function() {
                  transitioner._morphCancelled = true;
                  completeMorphFunction(transitioner);
              };
              transitioner._completeMorph = completeMorph;
              transitioner._morphHandler.setInputAction(completeMorph, ScreenSpaceEventType.LEFT_DOWN);
              transitioner._morphHandler.setInputAction(completeMorph, ScreenSpaceEventType.MIDDLE_DOWN);
              transitioner._morphHandler.setInputAction(completeMorph, ScreenSpaceEventType.RIGHT_DOWN);
              transitioner._morphHandler.setInputAction(completeMorph, ScreenSpaceEventType.WHEEL);
          }
      }
  
      function destroyMorphHandler(transitioner) {
          var tweens = transitioner._currentTweens;
          for ( var i = 0; i < tweens.length; ++i) {
              tweens[i].cancelTween();
          }
          transitioner._currentTweens.length = 0;
          transitioner._morphHandler = transitioner._morphHandler && transitioner._morphHandler.destroy();
      }
  
      var scratchCVTo3DCartographic = new Cartographic();
      var scratchCVTo3DSurfacePoint = new Cartesian3();
      var scratchCVTo3DFromENU = new Matrix4();
      var scratchCVTo3DCamera = {
          position : new Cartesian3(),
          direction : new Cartesian3(),
          up : new Cartesian3(),
          frustum : undefined
      };
  
      function getColumbusViewTo3DCamera(transitioner, ellipsoid) {
          var scene = transitioner._scene;
          var camera = scene.camera;
  
          var camera3D = scratchCVTo3DCamera;
          var position = camera3D.position;
          var direction = camera3D.direction;
          var up = camera3D.up;
  
          var positionCarto = scene.mapProjection.unproject(camera.position, scratchCVTo3DCartographic);
          ellipsoid.cartographicToCartesian(positionCarto, position);
          var surfacePoint = ellipsoid.scaleToGeodeticSurface(position, scratchCVTo3DSurfacePoint);
  
          var fromENU = Transforms.eastNorthUpToFixedFrame(surfacePoint, ellipsoid, scratchCVTo3DFromENU);
  
          Matrix4.multiplyByPointAsVector(fromENU, camera.direction, direction);
          Matrix4.multiplyByPointAsVector(fromENU, camera.up, up);
  
          return camera3D;
      }
  
      var scratchCVTo3DStartPos = new Cartesian3();
      var scratchCVTo3DStartDir = new Cartesian3();
      var scratchCVTo3DStartUp = new Cartesian3();
      var scratchCVTo3DEndPos = new Cartesian3();
      var scratchCVTo3DEndDir = new Cartesian3();
      var scratchCVTo3DEndUp = new Cartesian3();
  
      function morphFromColumbusViewTo3D(transitioner, duration, endCamera, complete) {
          duration *= 0.5;
  
          var scene = transitioner._scene;
          var camera = scene.camera;
  
          var startPos = Cartesian3.clone(camera.position, scratchCVTo3DStartPos);
          var startDir = Cartesian3.clone(camera.direction, scratchCVTo3DStartDir);
          var startUp = Cartesian3.clone(camera.up, scratchCVTo3DStartUp);
  
          var endPos = Matrix4.multiplyByPoint(Camera.TRANSFORM_2D_INVERSE, endCamera.position, scratchCVTo3DEndPos);
          var endDir = Matrix4.multiplyByPointAsVector(Camera.TRANSFORM_2D_INVERSE, endCamera.direction, scratchCVTo3DEndDir);
          var endUp = Matrix4.multiplyByPointAsVector(Camera.TRANSFORM_2D_INVERSE, endCamera.up, scratchCVTo3DEndUp);
  
          function update(value) {
              columbusViewMorph(startPos, endPos, value.time, camera.position);
              columbusViewMorph(startDir, endDir, value.time, camera.direction);
              columbusViewMorph(startUp, endUp, value.time, camera.up);
              Cartesian3.cross(camera.direction, camera.up, camera.right);
              Cartesian3.normalize(camera.right, camera.right);
          }
  
          var tween = scene.tweens.add({
              duration : duration,
              easingFunction : EasingFunction.QUARTIC_OUT,
              startObject : {
                  time : 0.0
              },
              stopObject : {
                  time : 1.0
              },
              update : update,
              complete : function() {
                  addMorphTimeAnimations(transitioner, scene, 0.0, 1.0, duration, complete);
              }
          });
          transitioner._currentTweens.push(tween);
      }
  
      var scratch2DTo3DFrustumPersp = new PerspectiveFrustum();
      var scratch2DTo3DFrustumOrtho = new OrthographicFrustum();
  
      function morphFrom2DTo3D(transitioner, duration, ellipsoid) {
          duration /= 3.0;
  
          var scene = transitioner._scene;
          var camera = scene.camera;
  
          var camera3D;
          if (duration > 0.0) {
              camera3D = scratchCVTo3DCamera;
              Cartesian3.fromDegrees(0.0, 0.0, 5.0 * ellipsoid.maximumRadius, ellipsoid, camera3D.position);
              Cartesian3.negate(camera3D.position, camera3D.direction);
              Cartesian3.normalize(camera3D.direction, camera3D.direction);
              Cartesian3.clone(Cartesian3.UNIT_Z, camera3D.up);
          } else {
              camera.position.z = camera.frustum.right - camera.frustum.left;
  
              camera3D = getColumbusViewTo3DCamera(transitioner, ellipsoid);
          }
  
          var frustum;
          if (transitioner._morphToOrthographic) {
              frustum = scratch2DTo3DFrustumOrtho;
              frustum.aspectRatio = scene.drawingBufferWidth / scene.drawingBufferHeight;
              frustum.width = camera.frustum.right - camera.frustum.left;
          } else {
              frustum = scratch2DTo3DFrustumPersp;
              frustum.aspectRatio = scene.drawingBufferWidth / scene.drawingBufferHeight;
              frustum.fov = CesiumMath.toRadians(60.0);
          }
  
          camera3D.frustum = frustum;
  
          var complete = complete3DCallback(camera3D);
          createMorphHandler(transitioner, complete);
  
          var startPos = Cartesian3.clone(camera.position, scratch3DToCVStartPos);
          var startDir = Cartesian3.clone(camera.direction, scratch3DToCVStartDir);
          var startUp = Cartesian3.clone(camera.up, scratch3DToCVStartUp);
  
          var endPos = Cartesian3.fromElements(0.0, 0.0, 5.0 * ellipsoid.maximumRadius, scratch3DToCVEndPos);
          var endDir = Cartesian3.negate(Cartesian3.UNIT_Z, scratch3DToCVEndDir);
          var endUp = Cartesian3.clone(Cartesian3.UNIT_Y, scratch3DToCVEndUp);
  
          var startRight = camera.frustum.right;
          var endRight = endPos.z * 0.5;
  
          function update(value) {
              columbusViewMorph(startPos, endPos, value.time, camera.position);
              columbusViewMorph(startDir, endDir, value.time, camera.direction);
              columbusViewMorph(startUp, endUp, value.time, camera.up);
              Cartesian3.cross(camera.direction, camera.up, camera.right);
              Cartesian3.normalize(camera.right, camera.right);
  
              var frustum = camera.frustum;
              frustum.right = CesiumMath.lerp(startRight, endRight, value.time);
              frustum.left = -frustum.right;
              frustum.top = frustum.right * (scene.drawingBufferHeight / scene.drawingBufferWidth);
              frustum.bottom = -frustum.top;
  
              camera.position.z = 2.0 * scene.mapProjection.ellipsoid.maximumRadius;
          }
  
          var morph;
          if (transitioner._morphToOrthographic) {
              morph = function() {
                  morphFromColumbusViewTo3D(transitioner, duration, camera3D, complete);
              };
          } else {
              morph = function() {
                  morphOrthographicToPerspective(transitioner, duration, camera3D, function() {
                      morphFromColumbusViewTo3D(transitioner, duration, camera3D, complete);
                  });
              };
          }
  
          if (duration > 0.0) {
              var tween = scene.tweens.add({
                  duration : duration,
                  easingFunction : EasingFunction.QUARTIC_OUT,
                  startObject : {
                      time : 0.0
                  },
                  stopObject : {
                      time : 1.0
                  },
                  update : update,
                  complete : function() {
                      scene._mode = SceneMode.MORPHING;
                      morph();
                  }
              });
              transitioner._currentTweens.push(tween);
          } else {
              morph();
          }
      }
  
      function columbusViewMorph(startPosition, endPosition, time, result) {
          // Just linear for now.
          return Cartesian3.lerp(startPosition, endPosition, time, result);
      }
  
      function morphPerspectiveToOrthographic(transitioner, duration, endCamera, updateHeight, complete) {
          var scene = transitioner._scene;
          var camera = scene.camera;
  
          if (camera.frustum instanceof OrthographicFrustum) {
              return;
          }
  
          var startFOV = camera.frustum.fov;
          var endFOV = CesiumMath.RADIANS_PER_DEGREE * 0.5;
          var d = endCamera.position.z * Math.tan(startFOV * 0.5);
          camera.frustum.far = d / Math.tan(endFOV * 0.5) + 10000000.0;
  
          function update(value) {
              camera.frustum.fov = CesiumMath.lerp(startFOV, endFOV, value.time);
              var height = d / Math.tan(camera.frustum.fov * 0.5);
              updateHeight(camera, height);
          }
          var tween = scene.tweens.add({
              duration : duration,
              easingFunction : EasingFunction.QUARTIC_OUT,
              startObject : {
                  time : 0.0
              },
              stopObject : {
                  time : 1.0
              },
              update : update,
              complete : function() {
                  camera.frustum = endCamera.frustum.clone();
                  complete(transitioner);
              }
          });
          transitioner._currentTweens.push(tween);
      }
  
      var scratchCVTo2DStartPos = new Cartesian3();
      var scratchCVTo2DStartDir = new Cartesian3();
      var scratchCVTo2DStartUp = new Cartesian3();
      var scratchCVTo2DEndPos = new Cartesian3();
      var scratchCVTo2DEndDir = new Cartesian3();
      var scratchCVTo2DEndUp = new Cartesian3();
      var scratchCVTo2DFrustum = new OrthographicOffCenterFrustum();
      var scratchCVTo2DRay = new Ray();
      var scratchCVTo2DPickPos = new Cartesian3();
      var scratchCVTo2DCamera = {
          position : undefined,
          direction : undefined,
          up : undefined,
          frustum : undefined
      };
  
      function morphFromColumbusViewTo2D(transitioner, duration) {
          duration *= 0.5;
  
          var scene = transitioner._scene;
          var camera = scene.camera;
  
          var startPos = Cartesian3.clone(camera.position, scratchCVTo2DStartPos);
          var startDir = Cartesian3.clone(camera.direction, scratchCVTo2DStartDir);
          var startUp = Cartesian3.clone(camera.up, scratchCVTo2DStartUp);
  
          var endDir = Cartesian3.negate(Cartesian3.UNIT_Z, scratchCVTo2DEndDir);
          var endUp = Cartesian3.clone(Cartesian3.UNIT_Y, scratchCVTo2DEndUp);
  
          var endPos = scratchCVTo2DEndPos;
  
          if (duration > 0.0) {
              Cartesian3.clone(Cartesian3.ZERO, scratchCVTo2DEndPos);
              endPos.z = 5.0 * scene.mapProjection.ellipsoid.maximumRadius;
          } else {
              Cartesian3.clone(startPos, scratchCVTo2DEndPos);
  
              var ray = scratchCVTo2DRay;
              Matrix4.multiplyByPoint(Camera.TRANSFORM_2D, startPos, ray.origin);
              Matrix4.multiplyByPointAsVector(Camera.TRANSFORM_2D, startDir, ray.direction);
  
              var globe = scene.globe;
              if (defined(globe)) {
                  var pickPos = globe.pick(ray, scene, scratchCVTo2DPickPos);
                  if (defined(pickPos)) {
                      Matrix4.multiplyByPoint(Camera.TRANSFORM_2D_INVERSE, pickPos, endPos);
                      endPos.z += Cartesian3.distance(startPos, endPos);
                  }
              }
          }
  
          var frustum = scratchCVTo2DFrustum;
          frustum.right = endPos.z * 0.5;
          frustum.left = -frustum.right;
          frustum.top = frustum.right * (scene.drawingBufferHeight / scene.drawingBufferWidth);
          frustum.bottom = -frustum.top;
  
          var camera2D = scratchCVTo2DCamera;
          camera2D.position = endPos;
          camera2D.direction = endDir;
          camera2D.up = endUp;
          camera2D.frustum = frustum;
  
          var complete = complete2DCallback(camera2D);
          createMorphHandler(transitioner, complete);
  
          function updateCV(value) {
              columbusViewMorph(startPos, endPos, value.time, camera.position);
              columbusViewMorph(startDir, endDir, value.time, camera.direction);
              columbusViewMorph(startUp, endUp, value.time, camera.up);
              Cartesian3.cross(camera.direction, camera.up, camera.right);
              Cartesian3.normalize(camera.right, camera.right);
              camera._adjustOrthographicFrustum(true);
          }
  
          function updateHeight(camera, height) {
              camera.position.z = height;
          }
  
          var tween = scene.tweens.add({
              duration : duration,
              easingFunction : EasingFunction.QUARTIC_OUT,
              startObject : {
                  time : 0.0
              },
              stopObject : {
                  time : 1.0
              },
              update : updateCV,
              complete : function() {
                  morphPerspectiveToOrthographic(transitioner, duration, camera2D, updateHeight, complete);
              }
          });
          transitioner._currentTweens.push(tween);
      }
  
      var scratch3DTo2DCartographic = new Cartographic();
      var scratch3DTo2DCamera = {
          position : new Cartesian3(),
          direction : new Cartesian3(),
          up : new Cartesian3(),
          position2D : new Cartesian3(),
          direction2D : new Cartesian3(),
          up2D : new Cartesian3(),
          frustum : new OrthographicOffCenterFrustum()
      };
      var scratch3DTo2DEndCamera = {
          position : new Cartesian3(),
          direction : new Cartesian3(),
          up : new Cartesian3(),
          frustum : undefined
      };
      var scratch3DTo2DPickPosition = new Cartesian3();
      var scratch3DTo2DRay = new Ray();
      var scratch3DTo2DToENU = new Matrix4();
      var scratch3DTo2DSurfacePoint = new Cartesian3();
  
      function morphFrom3DTo2D(transitioner, duration, ellipsoid) {
          duration *= 0.5;
  
          var scene = transitioner._scene;
          var camera = scene.camera;
          var camera2D = scratch3DTo2DCamera;
  
          if (duration > 0.0) {
              Cartesian3.clone(Cartesian3.ZERO, camera2D.position);
              camera2D.position.z = 5.0 * ellipsoid.maximumRadius;
              Cartesian3.negate(Cartesian3.UNIT_Z, camera2D.direction);
              Cartesian3.clone(Cartesian3.UNIT_Y, camera2D.up);
          } else {
              ellipsoid.cartesianToCartographic(camera.positionWC, scratch3DTo2DCartographic);
              scene.mapProjection.project(scratch3DTo2DCartographic, camera2D.position);
  
              Cartesian3.negate(Cartesian3.UNIT_Z, camera2D.direction);
              Cartesian3.clone(Cartesian3.UNIT_Y, camera2D.up);
  
              var ray = scratch3DTo2DRay;
              Cartesian3.clone(camera2D.position2D, ray.origin);
              var rayDirection = Cartesian3.clone(camera.directionWC, ray.direction);
              var surfacePoint = ellipsoid.scaleToGeodeticSurface(camera.positionWC, scratch3DTo2DSurfacePoint);
              var toENU = Transforms.eastNorthUpToFixedFrame(surfacePoint, ellipsoid, scratch3DTo2DToENU);
              Matrix4.inverseTransformation(toENU, toENU);
              Matrix4.multiplyByPointAsVector(toENU, rayDirection, rayDirection);
              Matrix4.multiplyByPointAsVector(Camera.TRANSFORM_2D, rayDirection, rayDirection);
  
              var globe = scene.globe;
              if (defined(globe)) {
                  var pickedPos = globe.pick(ray, scene, scratch3DTo2DPickPosition);
                  if (defined(pickedPos)) {
                      var height = Cartesian3.distance(camera2D.position2D, pickedPos);
                      pickedPos.x += height;
                      Cartesian3.clone(pickedPos, camera2D.position2D);
                  }
              }
          }
  
          function updateHeight(camera, height) {
              camera.position.x = height;
          }
  
          Matrix4.multiplyByPoint(Camera.TRANSFORM_2D, camera2D.position, camera2D.position2D);
          Matrix4.multiplyByPointAsVector(Camera.TRANSFORM_2D, camera2D.direction, camera2D.direction2D);
          Matrix4.multiplyByPointAsVector(Camera.TRANSFORM_2D, camera2D.up, camera2D.up2D);
  
          var frustum = camera2D.frustum;
          frustum.right = camera2D.position.z * 0.5;
          frustum.left = -frustum.right;
          frustum.top = frustum.right * (scene.drawingBufferHeight / scene.drawingBufferWidth);
          frustum.bottom = -frustum.top;
  
          var endCamera = scratch3DTo2DEndCamera;
          Matrix4.multiplyByPoint(Camera.TRANSFORM_2D_INVERSE, camera2D.position2D, endCamera.position);
          Cartesian3.clone(camera2D.direction, endCamera.direction);
          Cartesian3.clone(camera2D.up, endCamera.up);
          endCamera.frustum = frustum;
  
          var complete = complete2DCallback(endCamera);
          createMorphHandler(transitioner, complete);
  
          function completeCallback() {
              morphPerspectiveToOrthographic(transitioner, duration, camera2D, updateHeight, complete);
          }
          morphFrom3DToColumbusView(transitioner, duration, camera2D, completeCallback);
      }
  
      function morphOrthographicToPerspective(transitioner, duration, cameraCV, complete) {
          var scene = transitioner._scene;
          var camera = scene.camera;
  
          var height = camera.frustum.right - camera.frustum.left;
          camera.frustum = cameraCV.frustum.clone();
  
          var endFOV = camera.frustum.fov;
          var startFOV = CesiumMath.RADIANS_PER_DEGREE * 0.5;
          var d = height * Math.tan(endFOV * 0.5);
          camera.frustum.far = d / Math.tan(startFOV * 0.5) + 10000000.0;
          camera.frustum.fov = startFOV;
  
          function update(value) {
              camera.frustum.fov = CesiumMath.lerp(startFOV, endFOV, value.time);
              camera.position.z = d / Math.tan(camera.frustum.fov * 0.5);
          }
          var tween = scene.tweens.add({
              duration : duration,
              easingFunction : EasingFunction.QUARTIC_OUT,
              startObject : {
                  time : 0.0
              },
              stopObject : {
                  time : 1.0
              },
              update : update,
              complete : function() {
                  complete(transitioner);
              }
          });
          transitioner._currentTweens.push(tween);
      }
  
      function morphFrom2DToColumbusView(transitioner, duration, cameraCV, complete) {
          duration *= 0.5;
  
          var scene = transitioner._scene;
          var camera = scene.camera;
  
          var endPos = Cartesian3.clone(cameraCV.position, scratch3DToCVEndPos);
          var endDir = Cartesian3.clone(cameraCV.direction, scratch3DToCVEndDir);
          var endUp = Cartesian3.clone(cameraCV.up, scratch3DToCVEndUp);
  
          scene._mode = SceneMode.MORPHING;
  
          function morph() {
              camera.frustum = cameraCV.frustum.clone();
  
              var startPos = Cartesian3.clone(camera.position, scratch3DToCVStartPos);
              var startDir = Cartesian3.clone(camera.direction, scratch3DToCVStartDir);
              var startUp = Cartesian3.clone(camera.up, scratch3DToCVStartUp);
              startPos.z = endPos.z;
  
              function update(value) {
                  columbusViewMorph(startPos, endPos, value.time, camera.position);
                  columbusViewMorph(startDir, endDir, value.time, camera.direction);
                  columbusViewMorph(startUp, endUp, value.time, camera.up);
                  Cartesian3.cross(camera.direction, camera.up, camera.right);
                  Cartesian3.normalize(camera.right, camera.right);
              }
              var tween = scene.tweens.add({
                  duration : duration,
                  easingFunction : EasingFunction.QUARTIC_OUT,
                  startObject : {
                      time : 0.0
                  },
                  stopObject : {
                      time : 1.0
                  },
                  update : update,
                  complete : function() {
                      complete(transitioner);
                  }
              });
              transitioner._currentTweens.push(tween);
          }
  
          if (transitioner._morphToOrthographic) {
              morph();
          } else {
              morphOrthographicToPerspective(transitioner, 0.0, cameraCV, morph);
          }
      }
  
      var scratch3DToCVStartPos = new Cartesian3();
      var scratch3DToCVStartDir = new Cartesian3();
      var scratch3DToCVStartUp = new Cartesian3();
      var scratch3DToCVEndPos = new Cartesian3();
      var scratch3DToCVEndDir = new Cartesian3();
      var scratch3DToCVEndUp = new Cartesian3();
  
      function morphFrom3DToColumbusView(transitioner, duration, endCamera, complete) {
          var scene = transitioner._scene;
          var camera = scene.camera;
  
          var startPos = Cartesian3.clone(camera.position, scratch3DToCVStartPos);
          var startDir = Cartesian3.clone(camera.direction, scratch3DToCVStartDir);
          var startUp = Cartesian3.clone(camera.up, scratch3DToCVStartUp);
  
          var endPos = Cartesian3.clone(endCamera.position2D, scratch3DToCVEndPos);
          var endDir = Cartesian3.clone(endCamera.direction2D, scratch3DToCVEndDir);
          var endUp = Cartesian3.clone(endCamera.up2D, scratch3DToCVEndUp);
  
          function update(value) {
              columbusViewMorph(startPos, endPos, value.time, camera.position);
              columbusViewMorph(startDir, endDir, value.time, camera.direction);
              columbusViewMorph(startUp, endUp, value.time, camera.up);
              Cartesian3.cross(camera.direction, camera.up, camera.right);
              Cartesian3.normalize(camera.right, camera.right);
              camera._adjustOrthographicFrustum(true);
          }
          var tween = scene.tweens.add({
              duration : duration,
              easingFunction : EasingFunction.QUARTIC_OUT,
              startObject : {
                  time : 0.0
              },
              stopObject : {
                  time : 1.0
              },
              update : update,
              complete : function() {
                  addMorphTimeAnimations(transitioner, scene, 1.0, 0.0, duration, complete);
              }
          });
          transitioner._currentTweens.push(tween);
      }
  
      function addMorphTimeAnimations(transitioner, scene, start, stop, duration, complete) {
          // Later, this will be linear and each object will adjust, if desired, in its vertex shader.
          var options = {
              object : scene,
              property : 'morphTime',
              startValue : start,
              stopValue : stop,
              duration : duration,
              easingFunction : EasingFunction.QUARTIC_OUT
          };
  
          if (defined(complete)) {
              options.complete = function() {
                  complete(transitioner);
              };
          }
  
          var tween = scene.tweens.addProperty(options);
          transitioner._currentTweens.push(tween);
      }
  
      function complete3DCallback(camera3D) {
          return function(transitioner) {
              var scene = transitioner._scene;
              scene._mode = SceneMode.SCENE3D;
              scene.morphTime = SceneMode.getMorphTime(SceneMode.SCENE3D);
  
              destroyMorphHandler(transitioner);
  
              if (transitioner._previousMode !== SceneMode.MORPHING || transitioner._morphCancelled) {
                  transitioner._morphCancelled = false;
  
                  var camera = scene.camera;
                  Cartesian3.clone(camera3D.position, camera.position);
                  Cartesian3.clone(camera3D.direction, camera.direction);
                  Cartesian3.clone(camera3D.up, camera.up);
                  Cartesian3.cross(camera.direction, camera.up, camera.right);
                  Cartesian3.normalize(camera.right, camera.right);
  
                  camera.frustum = camera3D.frustum.clone();
              }
  
              var wasMorphing = defined(transitioner._completeMorph);
              transitioner._completeMorph = undefined;
              scene.camera.update(scene.mode);
              transitioner._scene.morphComplete.raiseEvent(transitioner, transitioner._previousMode, SceneMode.SCENE3D, wasMorphing);
          };
      }
  
      function complete2DCallback(camera2D) {
          return function(transitioner) {
              var scene = transitioner._scene;
  
              scene._mode = SceneMode.SCENE2D;
              scene.morphTime = SceneMode.getMorphTime(SceneMode.SCENE2D);
  
              destroyMorphHandler(transitioner);
  
              var camera = scene.camera;
              Cartesian3.clone(camera2D.position, camera.position);
              camera.position.z = scene.mapProjection.ellipsoid.maximumRadius * 2.0;
              Cartesian3.clone(camera2D.direction, camera.direction);
              Cartesian3.clone(camera2D.up, camera.up);
              Cartesian3.cross(camera.direction, camera.up, camera.right);
              Cartesian3.normalize(camera.right, camera.right);
              camera.frustum = camera2D.frustum.clone();
  
              var wasMorphing = defined(transitioner._completeMorph);
              transitioner._completeMorph = undefined;
              scene.camera.update(scene.mode);
              transitioner._scene.morphComplete.raiseEvent(transitioner, transitioner._previousMode, SceneMode.SCENE2D, wasMorphing);
          };
      }
  
      function completeColumbusViewCallback(cameraCV) {
          return function(transitioner) {
              var scene = transitioner._scene;
              scene._mode = SceneMode.COLUMBUS_VIEW;
              scene.morphTime = SceneMode.getMorphTime(SceneMode.COLUMBUS_VIEW);
  
              destroyMorphHandler(transitioner);
  
              if (transitioner._previousModeMode !== SceneMode.MORPHING || transitioner._morphCancelled) {
                  transitioner._morphCancelled = false;
  
                  var camera = scene.camera;
                  Cartesian3.clone(cameraCV.position, camera.position);
                  Cartesian3.clone(cameraCV.direction, camera.direction);
                  Cartesian3.clone(cameraCV.up, camera.up);
                  Cartesian3.cross(camera.direction, camera.up, camera.right);
                  Cartesian3.normalize(camera.right, camera.right);
              }
  
              var wasMorphing = defined(transitioner._completeMorph);
              transitioner._completeMorph = undefined;
              scene.camera.update(scene.mode);
              transitioner._scene.morphComplete.raiseEvent(transitioner, transitioner._previousMode, SceneMode.COLUMBUS_VIEW, wasMorphing);
          };
      }
  
      return SceneTransitioner;
  });
  