Home | History | Annotate | Download | only in ui
      1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 'use strict';
      6 
      7 base.exportTo('ui', function() {
      8 
      9   function lerp(a, b, interp) {
     10     return (a * (1 - interp)) +
     11         (b * interp);
     12   }
     13 
     14   /**
     15    * @constructor
     16    */
     17   function Camera(targetElement) {
     18     this.targetElement_ = targetElement;
     19 
     20     this.onMouseDown_ = this.onMouseDown_.bind(this);
     21     this.onMouseMove_ = this.onMouseMove_.bind(this);
     22     this.onMouseUp_ = this.onMouseUp_.bind(this);
     23 
     24     this.cameraStart_ = {x: 0, y: 0};
     25     this.rotations_ = {x: 0, y: 0};
     26     this.rotationStart_ = {x: 0, y: 0};
     27     this.matrixParameters_ = {
     28       thicknessRatio: 0.012, // Ratio of thickness to world size.
     29       strengthRatioX: 0.7, // Ratio of mousemove X pixels to degrees rotated.
     30       strengthRatioY: 0.25 // Ratio of mousemove Y pixels to degrees rotated.
     31     };
     32 
     33     this.targetElement_.addEventListener('mousedown', this.onMouseDown_);
     34     this.targetElement_.addEventListener('layersChange',
     35         this.scheduleRepaint.bind(this));
     36   }
     37 
     38   Camera.prototype = {
     39 
     40     scheduleRepaint: function() {
     41       if (this.repaintPending_)
     42         return;
     43       this.repaintPending_ = true;
     44       base.requestAnimationFrameInThisFrameIfPossible(
     45           this.repaint_, this);
     46     },
     47 
     48     /** Call only inside of a requestAnimationFrame. */
     49     repaint: function() {
     50       this.repaintPending_ = true;
     51       this.repaint_();
     52     },
     53 
     54     repaint_: function() {
     55       if (!this.repaintPending_)
     56         return;
     57 
     58       this.repaintPending_ = false;
     59       var layers = this.targetElement_.layers;
     60 
     61       if (!layers)
     62         return;
     63 
     64       var numLayers = layers.length;
     65 
     66       var vpThickness;
     67       if (this.targetElement_.viewport) {
     68         vpThickness = this.matrixParameters_.thicknessRatio *
     69             Math.min(this.targetElement_.viewport.worldRect.width,
     70                      this.targetElement_.viewport.worldRect.height);
     71       } else {
     72         vpThickness = 0;
     73       }
     74       vpThickness = Math.max(vpThickness, 15);
     75 
     76       // When viewing the stack head-on, we want no foreshortening effects. As
     77       // we move off axis, let the thickness grow as well as the amount of
     78       // perspective foreshortening.
     79       var maxRotation = Math.max(Math.abs(this.rotations_.x),
     80                                  Math.abs(this.rotations_.y));
     81       var clampLimit = 30;
     82       var clampedMaxRotation = Math.min(maxRotation, clampLimit);
     83       var percentToClampLimit = clampedMaxRotation / clampLimit;
     84       var persp = Math.pow(Math.E,
     85                            lerp(Math.log(5000), Math.log(500),
     86                                 percentToClampLimit));
     87       this.targetElement_.webkitPerspective = persp;
     88       var effectiveThickness = vpThickness * percentToClampLimit;
     89 
     90       // Set depth of each layer such that they center around 0.
     91       var deepestLayerZ = -effectiveThickness * 0.5;
     92       var depthIncreasePerLayer = effectiveThickness /
     93           Math.max(1, numLayers - 1);
     94       for (var i = 0; i < numLayers; i++) {
     95         var layer = layers[i];
     96         var newDepth = deepestLayerZ + i * depthIncreasePerLayer;
     97         layer.style.webkitTransform = 'translateZ(' + newDepth + 'px)';
     98       }
     99 
    100       // Set rotation matrix to whatever is stored.
    101       var transformString = '';
    102       transformString += 'rotateX(' + this.rotations_.x + 'deg)';
    103       transformString += ' rotateY(' + this.rotations_.y + 'deg)';
    104       var container = this.targetElement_.contentContainer;
    105       container.style.webkitTransform = transformString;
    106     },
    107 
    108     updateCameraStart_: function(x, y) {
    109       this.cameraStart_.x = x;
    110       this.cameraStart_.y = y;
    111       this.rotationStart_.x = this.rotations_.x;
    112       this.rotationStart_.y = this.rotations_.y;
    113     },
    114 
    115     updateCamera_: function(x, y) {
    116       var delta = {
    117         x: this.cameraStart_.x - x,
    118         y: this.cameraStart_.y - y
    119       };
    120       // update new rotation matrix (note the parameter swap)
    121       // "strength" is ration between mouse dist and rotation amount.
    122       this.rotations_.x = this.rotationStart_.x + delta.y *
    123           this.matrixParameters_.strengthRatioY;
    124       this.rotations_.y = this.rotationStart_.y + -delta.x *
    125           this.matrixParameters_.strengthRatioX;
    126       this.scheduleRepaint();
    127     },
    128 
    129     onMouseDown_: function(e) {
    130       this.updateCameraStart_(e.x, e.y);
    131       document.addEventListener('mousemove', this.onMouseMove_);
    132       document.addEventListener('mouseup', this.onMouseUp_);
    133       e.preventDefault();
    134       return true;
    135     },
    136 
    137     onMouseMove_: function(e) {
    138       this.updateCamera_(e.x, e.y);
    139     },
    140 
    141     onMouseUp_: function(e) {
    142       document.removeEventListener('mousemove', this.onMouseMove_);
    143       document.removeEventListener('mouseup', this.onMouseUp_);
    144       this.updateCamera_(e.x, e.y);
    145     },
    146 
    147   };
    148 
    149   return {
    150     Camera: Camera
    151   };
    152 });
    153