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