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