1<!DOCTYPE html> 2<!-- 3Copyright (c) 2014 The Chromium Authors. All rights reserved. 4Use of this source code is governed by a BSD-style license that can be 5found in the LICENSE file. 6--> 7 8<link rel="import" href="/tracing/base/settings.html"> 9<link rel="import" href="/tracing/ui/base/utils.html"> 10<link rel="import" href="/tracing/ui/base/ui.html"> 11 12<script> 13'use strict'; 14 15tr.exportTo('tr.ui.b', function() { 16 17 var constants = { 18 DEFAULT_SCALE: 0.5, 19 DEFAULT_EYE_DISTANCE: 10000, 20 MINIMUM_DISTANCE: 1000, 21 MAXIMUM_DISTANCE: 100000, 22 FOV: 15, 23 RESCALE_TIMEOUT_MS: 200, 24 MAXIMUM_TILT: 80, 25 SETTINGS_NAMESPACE: 'tr.ui_camera' 26 }; 27 28 29 var Camera = tr.ui.b.define('camera'); 30 31 Camera.prototype = { 32 __proto__: HTMLUnknownElement.prototype, 33 34 decorate: function(eventSource) { 35 this.eventSource_ = eventSource; 36 37 this.eventSource_.addEventListener('beginpan', 38 this.onPanBegin_.bind(this)); 39 this.eventSource_.addEventListener('updatepan', 40 this.onPanUpdate_.bind(this)); 41 this.eventSource_.addEventListener('endpan', 42 this.onPanEnd_.bind(this)); 43 44 this.eventSource_.addEventListener('beginzoom', 45 this.onZoomBegin_.bind(this)); 46 this.eventSource_.addEventListener('updatezoom', 47 this.onZoomUpdate_.bind(this)); 48 this.eventSource_.addEventListener('endzoom', 49 this.onZoomEnd_.bind(this)); 50 51 this.eventSource_.addEventListener('beginrotate', 52 this.onRotateBegin_.bind(this)); 53 this.eventSource_.addEventListener('updaterotate', 54 this.onRotateUpdate_.bind(this)); 55 this.eventSource_.addEventListener('endrotate', 56 this.onRotateEnd_.bind(this)); 57 58 this.eye_ = [0, 0, constants.DEFAULT_EYE_DISTANCE]; 59 this.gazeTarget_ = [0, 0, 0]; 60 this.rotation_ = [0, 0]; 61 62 this.pixelRatio_ = window.devicePixelRatio || 1; 63 }, 64 65 66 get modelViewMatrix() { 67 var mvMatrix = mat4.create(); 68 69 mat4.lookAt(mvMatrix, this.eye_, this.gazeTarget_, [0, 1, 0]); 70 return mvMatrix; 71 }, 72 73 get projectionMatrix() { 74 var rect = 75 tr.ui.b.windowRectForElement(this.canvas_). 76 scaleSize(this.pixelRatio_); 77 78 var aspectRatio = rect.width / rect.height; 79 var matrix = mat4.create(); 80 mat4.perspective( 81 matrix, tr.b.deg2rad(constants.FOV), aspectRatio, 1, 100000); 82 83 return matrix; 84 }, 85 86 set canvas(c) { 87 this.canvas_ = c; 88 }, 89 90 set deviceRect(rect) { 91 this.deviceRect_ = rect; 92 }, 93 94 get stackingDistanceDampening() { 95 var gazeVector = [ 96 this.gazeTarget_[0] - this.eye_[0], 97 this.gazeTarget_[1] - this.eye_[1], 98 this.gazeTarget_[2] - this.eye_[2]]; 99 vec3.normalize(gazeVector, gazeVector); 100 return 1 + gazeVector[2]; 101 }, 102 103 loadCameraFromSettings: function(settings) { 104 this.eye_ = settings.get( 105 'eye', this.eye_, constants.SETTINGS_NAMESPACE); 106 this.gazeTarget_ = settings.get( 107 'gaze_target', this.gazeTarget_, constants.SETTINGS_NAMESPACE); 108 this.rotation_ = settings.get( 109 'rotation', this.rotation_, constants.SETTINGS_NAMESPACE); 110 111 this.dispatchRenderEvent_(); 112 }, 113 114 saveCameraToSettings: function(settings) { 115 settings.set( 116 'eye', this.eye_, constants.SETTINGS_NAMESPACE); 117 settings.set( 118 'gaze_target', this.gazeTarget_, constants.SETTINGS_NAMESPACE); 119 settings.set( 120 'rotation', this.rotation_, constants.SETTINGS_NAMESPACE); 121 }, 122 123 resetCamera: function() { 124 this.eye_ = [0, 0, constants.DEFAULT_EYE_DISTANCE]; 125 this.gazeTarget_ = [0, 0, 0]; 126 this.rotation_ = [0, 0]; 127 128 var settings = tr.b.SessionSettings(); 129 var keys = settings.keys(constants.SETTINGS_NAMESPACE); 130 if (keys.length !== 0) { 131 this.loadCameraFromSettings(settings); 132 return; 133 } 134 135 if (this.deviceRect_) { 136 var rect = tr.ui.b.windowRectForElement(this.canvas_). 137 scaleSize(this.pixelRatio_); 138 139 this.eye_[0] = this.deviceRect_.width / 2; 140 this.eye_[1] = this.deviceRect_.height / 2; 141 142 this.gazeTarget_[0] = this.deviceRect_.width / 2; 143 this.gazeTarget_[1] = this.deviceRect_.height / 2; 144 } 145 146 this.saveCameraToSettings(settings); 147 this.dispatchRenderEvent_(); 148 }, 149 150 updatePanByDelta: function(delta) { 151 var rect = 152 tr.ui.b.windowRectForElement(this.canvas_). 153 scaleSize(this.pixelRatio_); 154 155 // Get the eye vector, since we'll be adjusting gazeTarget. 156 var eyeVector = [ 157 this.eye_[0] - this.gazeTarget_[0], 158 this.eye_[1] - this.gazeTarget_[1], 159 this.eye_[2] - this.gazeTarget_[2]]; 160 var length = vec3.length(eyeVector); 161 vec3.normalize(eyeVector, eyeVector); 162 163 var halfFov = constants.FOV / 2; 164 var multiplier = 165 2.0 * length * Math.tan(tr.b.deg2rad(halfFov)) / rect.height; 166 167 // Get the up and right vectors. 168 var up = [0, 1, 0]; 169 var rotMatrix = mat4.create(); 170 mat4.rotate( 171 rotMatrix, rotMatrix, tr.b.deg2rad(this.rotation_[1]), [0, 1, 0]); 172 mat4.rotate( 173 rotMatrix, rotMatrix, tr.b.deg2rad(this.rotation_[0]), [1, 0, 0]); 174 vec3.transformMat4(up, up, rotMatrix); 175 176 var right = [0, 0, 0]; 177 vec3.cross(right, eyeVector, up); 178 vec3.normalize(right, right); 179 180 // Update the gaze target. 181 for (var i = 0; i < 3; ++i) { 182 this.gazeTarget_[i] += 183 delta[0] * multiplier * right[i] - delta[1] * multiplier * up[i]; 184 185 this.eye_[i] = this.gazeTarget_[i] + length * eyeVector[i]; 186 } 187 188 // If we have some z offset, we need to reposition gazeTarget 189 // to be on the plane z = 0 with normal [0, 0, 1]. 190 if (Math.abs(this.gazeTarget_[2]) > 1e-6) { 191 var gazeVector = [-eyeVector[0], -eyeVector[1], -eyeVector[2]]; 192 var newLength = tr.b.clamp( 193 -this.eye_[2] / gazeVector[2], 194 constants.MINIMUM_DISTANCE, 195 constants.MAXIMUM_DISTANCE); 196 197 for (var i = 0; i < 3; ++i) 198 this.gazeTarget_[i] = this.eye_[i] + newLength * gazeVector[i]; 199 } 200 201 this.saveCameraToSettings(tr.b.SessionSettings()); 202 this.dispatchRenderEvent_(); 203 }, 204 205 updateZoomByDelta: function(delta) { 206 var deltaY = delta[1]; 207 deltaY = tr.b.clamp(deltaY, -50, 50); 208 var scale = 1.0 - deltaY / 100.0; 209 210 var eyeVector = [0, 0, 0]; 211 vec3.subtract(eyeVector, this.eye_, this.gazeTarget_); 212 213 var length = vec3.length(eyeVector); 214 215 // Clamp the length to allowed values by changing the scale. 216 if (length * scale < constants.MINIMUM_DISTANCE) 217 scale = constants.MINIMUM_DISTANCE / length; 218 else if (length * scale > constants.MAXIMUM_DISTANCE) 219 scale = constants.MAXIMUM_DISTANCE / length; 220 221 vec3.scale(eyeVector, eyeVector, scale); 222 vec3.add(this.eye_, this.gazeTarget_, eyeVector); 223 224 this.saveCameraToSettings(tr.b.SessionSettings()); 225 this.dispatchRenderEvent_(); 226 }, 227 228 updateRotateByDelta: function(delta) { 229 delta[0] *= 0.5; 230 delta[1] *= 0.5; 231 232 if (Math.abs(this.rotation_[0] + delta[1]) > constants.MAXIMUM_TILT) 233 return; 234 if (Math.abs(this.rotation_[1] - delta[0]) > constants.MAXIMUM_TILT) 235 return; 236 237 var eyeVector = [0, 0, 0, 0]; 238 vec3.subtract(eyeVector, this.eye_, this.gazeTarget_); 239 240 // Undo the current rotation. 241 var rotMatrix = mat4.create(); 242 mat4.rotate( 243 rotMatrix, rotMatrix, -tr.b.deg2rad(this.rotation_[0]), [1, 0, 0]); 244 mat4.rotate( 245 rotMatrix, rotMatrix, -tr.b.deg2rad(this.rotation_[1]), [0, 1, 0]); 246 vec4.transformMat4(eyeVector, eyeVector, rotMatrix); 247 248 // Update rotation values. 249 this.rotation_[0] += delta[1]; 250 this.rotation_[1] -= delta[0]; 251 252 // Redo the new rotation. 253 mat4.identity(rotMatrix); 254 mat4.rotate( 255 rotMatrix, rotMatrix, tr.b.deg2rad(this.rotation_[1]), [0, 1, 0]); 256 mat4.rotate( 257 rotMatrix, rotMatrix, tr.b.deg2rad(this.rotation_[0]), [1, 0, 0]); 258 vec4.transformMat4(eyeVector, eyeVector, rotMatrix); 259 260 vec3.add(this.eye_, this.gazeTarget_, eyeVector); 261 262 this.saveCameraToSettings(tr.b.SessionSettings()); 263 this.dispatchRenderEvent_(); 264 }, 265 266 267 // Event callbacks. 268 onPanBegin_: function(e) { 269 this.panning_ = true; 270 this.lastMousePosition_ = this.getMousePosition_(e); 271 }, 272 273 onPanUpdate_: function(e) { 274 if (!this.panning_) 275 return; 276 277 var delta = this.getMouseDelta_(e, this.lastMousePosition_); 278 this.lastMousePosition_ = this.getMousePosition_(e); 279 this.updatePanByDelta(delta); 280 }, 281 282 onPanEnd_: function(e) { 283 this.panning_ = false; 284 }, 285 286 onZoomBegin_: function(e) { 287 this.zooming_ = true; 288 289 var p = this.getMousePosition_(e); 290 291 this.lastMousePosition_ = p; 292 this.zoomPoint_ = p; 293 }, 294 295 onZoomUpdate_: function(e) { 296 if (!this.zooming_) 297 return; 298 299 var delta = this.getMouseDelta_(e, this.lastMousePosition_); 300 this.lastMousePosition_ = this.getMousePosition_(e); 301 this.updateZoomByDelta(delta); 302 }, 303 304 onZoomEnd_: function(e) { 305 this.zooming_ = false; 306 this.zoomPoint_ = undefined; 307 }, 308 309 onRotateBegin_: function(e) { 310 this.rotating_ = true; 311 this.lastMousePosition_ = this.getMousePosition_(e); 312 }, 313 314 onRotateUpdate_: function(e) { 315 if (!this.rotating_) 316 return; 317 318 var delta = this.getMouseDelta_(e, this.lastMousePosition_); 319 this.lastMousePosition_ = this.getMousePosition_(e); 320 this.updateRotateByDelta(delta); 321 }, 322 323 onRotateEnd_: function(e) { 324 this.rotating_ = false; 325 }, 326 327 328 // Misc helper functions. 329 getMousePosition_: function(e) { 330 var rect = tr.ui.b.windowRectForElement(this.canvas_); 331 return [(e.clientX - rect.x) * this.pixelRatio_, 332 (e.clientY - rect.y) * this.pixelRatio_]; 333 }, 334 335 getMouseDelta_: function(e, p) { 336 var newP = this.getMousePosition_(e); 337 return [newP[0] - p[0], newP[1] - p[1]]; 338 }, 339 340 dispatchRenderEvent_: function() { 341 tr.b.dispatchSimpleEvent(this, 'renderrequired', false, false); 342 } 343 }; 344 345 return { 346 Camera: Camera 347 }; 348}); 349</script> 350