Imported Upstream version 2.81
[platform/upstream/libbullet.git] / Demos / NativeClient / bin_html / trackball.js
1 // Copyright (c) 2011 The Native Client 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 /**
6  * @fileoverview  Implement a virtual trackball in the tumbler.Trackball
7  * class.  This class maps 2D mouse events to 3D rotations by simulating a
8  * trackball that you roll by dragging the mouse.  There are two principle
9  * methods in the class: startAtPointInFrame which you use to begin a trackball
10  * simulation and rollToPoint, which you use while dragging the mouse.  The
11  * rollToPoint method returns a rotation expressed as a quaternion.
12  */
13
14
15 // Requires tumbler.Application
16 // Requires tumbler.DragEvent
17 // Requires tumbler.Vector3
18
19 /**
20  * Constructor for the Trackball object.  This class maps 2D mouse drag events
21  * into 3D rotations by simulating a trackball.  The idea is to simulate
22  * clicking on the trackball, and then rolling it as you drag the mouse.
23  * The math behind the trackball is simple: start with a vector from the first
24  * mouse-click on the ball to the center of the 3D view.  At the same time, set
25  * the radius  of the ball to be the smaller dimension of the 3D view.  As you
26  * drag the mouse around in the 3D view, a second vector is computed from the
27  * surface of the ball to the center.  The axis of rotation is the cross
28  * product of these two vectors, and the angle of rotation is the angle between
29  * the two vectors.
30  * @constructor
31  */
32 tumbler.Trackball = function() {
33   /**
34    * The square of the trackball's radius.  The math never looks at the radius,
35    * but looks at the radius squared.
36    * @type {number}
37    * @private
38    */
39   this.sqrRadius_ = 0;
40
41   /**
42    * The 3D vector representing the point on the trackball where the mouse
43    * was clicked.  Default is pointing stright through the center of the ball.
44    * @type {Object}
45    * @private
46    */
47   this.rollStart_ = new tumbler.Vector3(0, 0, 1);
48
49   /**
50    * The 2D center of the frame that encloses the trackball.
51    * @type {!Object}
52    * @private
53    */
54   this.center_ = { x: 0, y: 0 };
55
56   /**
57    * Cached camera orientation.  When a drag START event happens this is set to
58    * the current orientation in the calling view's plugin.  The default is the
59    * identity quaternion.
60    * @type {Array.<number>}
61    * @private
62    */
63   this.cameraOrientation_ = [0, 0, 0, 1];
64 };
65
66 /**
67  * Compute the dimensions of the virtual trackball to fit inside |frameSize|.
68  * The radius of the trackball is set to be 1/2 of the smaller of the two frame
69  * dimensions, the center point is at the midpoint of each side.
70  * @param {!goog.math.Size} frameSize 2D-point representing the size of the
71  *     element that encloses the virtual trackball.
72  * @private
73  */
74 tumbler.Trackball.prototype.initInFrame_ = function(frameSize) {
75   // Compute the radius of the virtual trackball.  This is 1/2 of the smaller
76   // of the frame's width and height.
77   var halfFrameSize = 0.5 * Math.min(frameSize.width, frameSize.height);
78   // Cache the square of the trackball's radius.
79   this.sqrRadius_ = halfFrameSize * halfFrameSize;
80   // Figure the center of the view.
81   this.center_.x = frameSize.width * 0.5;
82   this.center_.y = frameSize.height * 0.5;
83 };
84
85 /**
86  * Method to convert (by translation) a 2D client point from a coordinate space
87  * with origin in the lower-left corner of the client view to a space with
88  * origin in the center of the client view.  Use this method before mapping the
89  * 2D point to he 3D tackball point (see also the projectOnTrackball_() method).
90  * Call the startAtPointInFrame before calling this method so that the
91  * |center_| property is correctly initialized.
92  * @param {!Object} clientPoint map this point to the coordinate space with
93  *     origin in thecenter of the client view.
94  * @return {Object} the converted point.
95  * @private
96  */
97 tumbler.Trackball.prototype.convertClientPoint_ = function(clientPoint) {
98   var difference = { x: clientPoint.x - this.center_.x,
99                      y: clientPoint.y - this.center_.y }
100   return difference;
101 };
102
103 /**
104  * Method to map a 2D point to a 3D point on the virtual trackball that was set
105  * up using the startAtPointInFrame method.  If the point lies outside of the
106  * radius of the virtual trackball, then the z-coordinate of the 3D point
107  * is set to 0.
108  * @param {!Object.<x, y>} point 2D-point in the coordinate space with origin
109  *     in the center of the client view.
110  * @return {tumbler.Vector3} the 3D point on the virtual trackball.
111  * @private
112  */
113 tumbler.Trackball.prototype.projectOnTrackball_ = function(point) {
114   var sqrRadius2D = point.x * point.x + point.y * point.y;
115   var zValue;
116   if (sqrRadius2D > this.sqrRadius_) {
117     // |point| lies outside the virtual trackball's sphere, so use a virtual
118     // z-value of 0.  This is equivalent to clicking on the horizontal equator
119     // of the trackball.
120     zValue = 0;
121   } else {
122     // A sphere can be defined as: r^2 = x^2 + y^2 + z^2, so z =
123     // sqrt(r^2 - (x^2 + y^2)).
124     zValue = Math.sqrt(this.sqrRadius_ - sqrRadius2D);
125   }
126   var trackballPoint = new tumbler.Vector3(point.x, point.y, zValue);
127   return trackballPoint;
128 };
129
130 /**
131  * Method to start up the trackball.  The trackball works by pretending that a
132  * ball encloses the 3D view.  You roll this pretend ball with the mouse.  For
133  * example, if you click on the center of the ball and move the mouse straight
134  * to the right, you roll the ball around its Y-axis.  This produces a Y-axis
135  * rotation.  You can click on the "edge" of the ball and roll it around
136  * in a circle to get a Z-axis rotation.
137  * @param {!Object.<x, y>} startPoint 2D-point, usually the mouse-down
138  *     point.
139  * @param {!Object.<width, height>} frameSize 2D-point representing the size of
140  *     the element that encloses the virtual trackball.
141  */
142 tumbler.Trackball.prototype.startAtPointInFrame =
143     function(startPoint, frameSize) {
144   this.initInFrame_(frameSize);
145   // Compute the starting vector from the surface of the ball to its center.
146   this.rollStart_ = this.projectOnTrackball_(
147       this.convertClientPoint_(startPoint));
148 };
149
150 /**
151  * Method to roll the virtual trackball; call this in response to a mouseDrag
152  * event.  Takes |dragPoint| and projects it from 2D mouse coordinates onto the
153  * virtual track ball that was set up in startAtPointInFrame method.
154  * Returns a quaternion that represents the rotation from |rollStart_| to
155  * |rollEnd_|.
156  * @param {!Object.<x, y>} dragPoint 2D-point representing the
157  *     destination mouse point.
158  * @return {Array.<number>} a quaternion that represents the rotation from
159  *     the point wnere the mouse was clicked on the trackball to this point.
160  *     The quaternion looks like this: [[v], cos(angle/2)], where [v] is the
161  *     imaginary part of the quaternion and is computed as [x, y, z] *
162  *     sin(angle/2).
163  */
164 tumbler.Trackball.prototype.rollToPoint = function(dragPoint) {
165   var rollTo = this.convertClientPoint_(dragPoint);
166   if ((Math.abs(this.rollStart_.x - rollTo.x) <
167                tumbler.Trackball.DOUBLE_EPSILON) &&
168       (Math.abs(this.rollStart_.y, rollTo.y) <
169                tumbler.Trackball.DOUBLE_EPSILON)) {
170     // Not enough change in the vectors to roll the ball, return the identity
171     // quaternion.
172     return [0, 0, 0, 1];
173   }
174
175   // Compute the ending vector from the surface of the ball to its center.
176   var rollEnd = this.projectOnTrackball_(rollTo);
177
178   // Take the cross product of the two vectors. r = s X e
179   var rollVector = this.rollStart_.cross(rollEnd);
180   var invStartMag = 1.0 / this.rollStart_.magnitude();
181   var invEndMag = 1.0 / rollEnd.magnitude();
182
183   // cos(a) = (s . e) / (||s|| ||e||)
184   var cosAng = this.rollStart_.dot(rollEnd) * invStartMag * invEndMag;
185   // sin(a) = ||(s X e)|| / (||s|| ||e||)
186   var sinAng = rollVector.magnitude() * invStartMag * invEndMag;
187   // Build a quaternion that represents the rotation about |rollVector|.
188   // Use atan2 for a better angle.  If you use only cos or sin, you only get
189   // half the possible angles, and you can end up with rotations that flip
190   // around near the poles.
191   var rollHalfAngle = Math.atan2(sinAng, cosAng) * 0.5;
192   rollVector.normalize();
193   // The quaternion looks like this: [[v], cos(angle/2)], where [v] is the
194   // imaginary part of the quaternion and is computed as [x, y, z] *
195   // sin(angle/2).
196   rollVector.scale(Math.sin(rollHalfAngle));
197   var ballQuaternion = [rollVector.x,
198                         rollVector.y,
199                         rollVector.z,
200                         Math.cos(rollHalfAngle)];
201   return ballQuaternion;
202 };
203
204 /**
205  * Handle the drag START event: grab the current camera orientation from the
206  * sending view and set up the virtual trackball.
207  * @param {!tumbler.Application} view The view controller that called this
208  *     method.
209  * @param {!tumbler.DragEvent} dragStartEvent The DRAG_START event that
210  *     triggered this handler.
211  */
212 tumbler.Trackball.prototype.handleStartDrag =
213     function(controller, dragStartEvent) {
214   // Cache the camera orientation.  The orientations from the trackball as it
215   // rolls are concatenated to this orientation and pushed back into the
216   // plugin on the other side of the JavaScript bridge.
217   controller.setCameraOrientation(this.cameraOrientation_);
218   // Invert the y-coordinate for the trackball computations.
219   var frameSize = { width: controller.offsetWidth,
220                     height: controller.offsetHeight };
221   var flippedY = { x: dragStartEvent.clientX,
222                    y: frameSize.height - dragStartEvent.clientY };
223   this.startAtPointInFrame(flippedY, frameSize);
224 };
225
226 /**
227  * Handle the drag DRAG event: concatenate the current orientation to the
228  * cached orientation.  Send this final value through to the GSPlugin via the
229  * setValueForKey() method.
230  * @param {!tumbler.Application} view The view controller that called this
231  *     method.
232  * @param {!tumbler.DragEvent} dragEvent The DRAG event that triggered this
233  *     handler.
234  */
235 tumbler.Trackball.prototype.handleDrag =
236     function(controller, dragEvent) {
237   // Flip the y-coordinate so that the 2D origin is in the lower-left corner.
238   var frameSize = { width: controller.offsetWidth,
239                     height: controller.offsetHeight };
240   var flippedY = { x: dragEvent.clientX,
241                    y: frameSize.height - dragEvent.clientY };
242   controller.setCameraOrientation(
243       tumbler.multQuaternions(this.rollToPoint(flippedY),
244                               this.cameraOrientation_));
245 };
246
247 /**
248  * Handle the drag END event: get the final orientation and concatenate it to
249  * the cached orientation.
250  * @param {!tumbler.Application} view The view controller that called this
251  *     method.
252  * @param {!tumbler.DragEvent} dragEndEvent The DRAG_END event that triggered
253  *     this handler.
254  */
255 tumbler.Trackball.prototype.handleEndDrag =
256     function(controller, dragEndEvent) {
257   // Flip the y-coordinate so that the 2D origin is in the lower-left corner.
258   var frameSize = { width: controller.offsetWidth,
259                     height: controller.offsetHeight };
260   var flippedY = { x: dragEndEvent.clientX,
261                    y: frameSize.height - dragEndEvent.clientY };
262   this.cameraOrientation_ = tumbler.multQuaternions(this.rollToPoint(flippedY),
263                                                     this.cameraOrientation_);
264   controller.setCameraOrientation(this.cameraOrientation_);
265 };
266
267 /**
268  * A utility function to multiply two quaterions.  Returns the product q0 * q1.
269  * This is effectively the same thing as concatenating the two rotations
270  * represented in each quaternion together. Note that quaternion multiplication
271  * is NOT commutative: q0 * q1 != q1 * q0.
272  * @param {!Array.<number>} q0 A 4-element array representing the first
273  *     quaternion.
274  * @param {!Array.<number>} q1 A 4-element array representing the second
275  *     quaternion.
276  * @return {Array.<number>} A 4-element array representing the product q0 * q1.
277  */
278 tumbler.multQuaternions = function(q0, q1) {
279   // Return q0 * q1 (note the order).
280   var qMult = [
281       q0[3] * q1[0] + q0[0] * q1[3] + q0[1] * q1[2] - q0[2] * q1[1],
282       q0[3] * q1[1] - q0[0] * q1[2] + q0[1] * q1[3] + q0[2] * q1[0],
283       q0[3] * q1[2] + q0[0] * q1[1] - q0[1] * q1[0] + q0[2] * q1[3],
284       q0[3] * q1[3] - q0[0] * q1[0] - q0[1] * q1[1] - q0[2] * q1[2]
285   ];
286   return qMult;
287 };
288
289 /**
290  * Real numbers that are less than this distance apart are considered
291  * equivalent.
292  * TODO(dspringer): It seems as though there should be a const like this
293  * in Closure somewhere (goog.math?).
294  * @type {number}
295  */
296 tumbler.Trackball.DOUBLE_EPSILON = 1.0e-16;