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
7base.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