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