/**
* @file
*
* Summary.
*
* <p>The SimpleRotator class implements an <a href="/cwdc/13-webgl/extras/doc/Arcball.pdf">ArcBall</a> like interface.</p>
* Created by {@link https://dl.acm.org/profile/81100026146 Ken Shoemake} in 1992,
* it is the de facto <a href="/cwdc/13-webgl/extras/doc/shoemake92-arcball.pdf">standard</a>
* for interactive 3D model manipulation and visualization.
* <p>The class defines the following methods for an object of type SimpleRotator:</p>
* <ul>
* <li>{@link SimpleRotator#setView}(viewDirectionVector, viewUpVector, viewDistance) <br>
* set up the view, where the parameters are optional and are used in the
* same way as the corresponding parameters in the constructor;</li>
* <li>{@link SimpleRotator#setViewDistance}(viewDistance) <br>
* sets the distance of the viewer from the origin without
* changing the direction of view;</li>
* <li>{@link SimpleRotator#getViewDistance}() <br>
* returns the viewDistance;</li>
* <li>{@link SimpleRotator#setViewMatrix}(matrix) <br>
* Sets the view matrix.
* <li>{@link SimpleRotator#getViewMatrix}() <br>
* returns a {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Float32Array Float32Array}
* representing the viewing transformation matrix for the current view, suitable for using with
* {@link https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/uniformMatrix gl.uniformMatrix4fv} <br>
* or for further transformation with the {@link https://glmatrix.net/docs/ glmatrix} library
* {@link https://glmatrix.net/docs/module-mat4.html mat4} class;</li>
* <li>{@link SimpleRotator#getViewMatrixArray}() <br>
* returns the view transformation matrix as a regular JavaScript array,
* but still represents as a 1D array of 16 elements, in column-major order.</li>
* </ul>
*
* @since 22/01/2016
* @author {@link https://math.hws.edu/eck/ David J. Eck}
* @author modified by Paulo Roma
* @see <a href="/cwdc/13-webgl/lib/simple-rotator.js">source</a>
* @see {@link https://math.hws.edu/eck/cs424/notes2013/webgl/skybox-and-reflection/simple-rotator.js hws source}
* @see {@link https://math.hws.edu/eck/cs424/notes2013/webgl/cube-with-rotator.html A Cube With Rotator}
* @see {@link https://math.hws.edu/eck/cs424/notes2013/webgl/skybox-and-reflection/ Skybox and Reflection}
* @see {@link https://en.wikibooks.org/wiki/OpenGL_Programming/Modern_OpenGL_Tutorial_Arcball Modern OpenGL Tutorial Arcball}
* @see {@link https://braintrekking.wordpress.com/2012/08/21/tutorial-of-arcball-without-quaternions/ Tutorial of Arcball without quaternions}
* @see <img src="/cwdc/13-webgl/lib/arcball4.png" width="256">
*/
/**
* <p>An object of type SimpleRotator can be used to implement a trackball-like mouse rotation
* of a WebGL scene about the origin.</p>
*
* Only the first parameter of the constructor is required. <br>
* When an object is created, mouse event handlers are set up on the canvas to respond to rotation.<br>
* It will also work with a touchscreen.
*/
class SimpleRotator {
/**
* <p>Constructor of SimpleRotator.</p>
* @constructs SimpleRotator
* @param {HTMLCanvasElement} canvas the HTML canvas element used for WebGL drawing. <br>
* The user will rotate the scene by dragging the mouse on this canvas. <br>
* This parameter is required.
* @param {Function} callback if present must be a function, which is called whenever the rotation changes. <br>
* It is typically the function that draws the scene
* @param {Array<Number>} viewDirectionVector if present must be an array of three numbers, not all zero. <br>
* The view is from the direction of this vector towards the origin (0,0,0). <br>
* If not present, the value [0,0,10] is used.
* @param {Array<Number>} viewUpVector if present must be an array of three numbers. <br>
* Gives a vector that will be seen as pointing upwards in the view. <br>
* If not present, the value is [0,1,0].
* @param {Number} viewDistance if present must be a positive number. <br>
* Gives the distance of the viewer from the origin. <br>
* If not present, the length of viewDirectionVector is used.
*/
constructor(
canvas,
callback,
viewDirectionVector,
viewUpVector,
viewDistance,
) {
var unitx = new Array(3);
var unity = new Array(3);
var unitz = new Array(3);
var viewZ;
var centerX = canvas.width / 2;
var centerY = canvas.height / 2;
var radius = Math.min(centerX, centerY);
var radius2 = radius * radius;
var prevx, prevy;
var dragging = false;
var touchStarted = false;
/**
* Set up the view, where the parameters are optional,
* and are used in the same way, <br>
* as the corresponding parameters in the constructor.
* @param {Array<Number>} viewDirectionVector if present must be an array of three numbers, not all zero. <br>
* The view is from the direction of this vector towards the origin (0,0,0). <br>
* If not present, the value [0,0,10] is used.
* @param {Array<Number>} viewUpVector if present must be an array of three numbers. <br>
* Gives a vector that will be seen as pointing upwards in the view. <br>
* If not present, the value is [0,1,0].
* @param {Number} viewDistance if present must be a positive number. <br>
* Gives the distance of the viewer from the origin. <br>
* If not present, the length of viewDirectionVector is used.
*/
this.setView = function (viewDirectionVector, viewUpVector, viewDistance) {
var viewpoint = viewDirectionVector || [0, 0, 10];
var viewup = viewUpVector || [0, 1, 0];
if (viewDistance && typeof viewDistance == "number") viewZ = viewDistance;
else viewZ = length(viewpoint);
copy(unitz, viewpoint);
normalize(unitz, unitz);
copy(unity, unitz);
scale(unity, unity, dot(unitz, viewup));
subtract(unity, viewup, unity);
normalize(unity, unity);
cross(unitx, unity, unitz);
};
/**
* Returns an array representing the viewing transformation matrix for the current view, <br>
* suitable for using with
* {@link https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/uniformMatrix gl.uniformMatrix4fv}
* or, for further transformation, with the glmatrix library {@link https://glmatrix.net/docs/module-mat4.html mat4} class.
* @return {Float32Array} view matrix.
*/
this.getViewMatrix = function () {
return new Float32Array(this.getViewMatrixArray());
};
/**
* Sets the view matrix.
* @param {Float32Array} matrix view matrix.
*/
this.setViewMatrix = function (matrix) {
unitx[0] = matrix[0];
unity[0] = matrix[1];
unitz[0] = matrix[2];
unitx[1] = matrix[4];
unity[1] = matrix[5];
unitz[1] = matrix[6];
unitx[2] = matrix[8];
unity[2] = matrix[9];
unitz[2] = matrix[10];
};
/**
* Returns the view transformation matrix as a regular JavaScript array, <br>
* but still represents as a 1D array of 16 elements, in column-major order.
* @returns {Array<Number>} view matrix.
*/
this.getViewMatrixArray = function () {
return [
unitx[0],
unity[0],
unitz[0],
0,
unitx[1],
unity[1],
unitz[1],
0,
unitx[2],
unity[2],
unitz[2],
0,
0,
0,
-viewZ,
1,
];
};
/**
* Returns the viewDistance.
* @return {Number} view distance.
*/
this.getViewDistance = function () {
return viewZ;
};
/**
* Sets the distance of the viewer from the origin without
* changing the direction of view. <br>
* If not present, the length of viewDirectionVector is used.
* @param {Number} viewDistance view distance.
*/
this.setViewDistance = function (viewDistance) {
viewZ = viewDistance;
};
// ------------------ private functions --------------------
function applyTransvection(e1, e2) {
// rotate vector e1 onto e2
function reflectInAxis(axis, source, destination) {
var s =
2 * (axis[0] * source[0] + axis[1] * source[1] + axis[2] * source[2]);
destination[0] = s * axis[0] - source[0];
destination[1] = s * axis[1] - source[1];
destination[2] = s * axis[2] - source[2];
}
normalize(e1, e1);
normalize(e2, e2);
var e = [0, 0, 0];
add(e, e1, e2);
normalize(e, e);
var temp = [0, 0, 0];
reflectInAxis(e, unitz, temp);
reflectInAxis(e1, temp, unitz);
reflectInAxis(e, unitx, temp);
reflectInAxis(e1, temp, unitx);
reflectInAxis(e, unity, temp);
reflectInAxis(e1, temp, unity);
}
function doMouseDown(evt) {
if (dragging) return;
dragging = true;
document.addEventListener("mousemove", doMouseDrag, false);
document.addEventListener("mouseup", doMouseUp, false);
var box = canvas.getBoundingClientRect();
prevx = window.pageXOffset + evt.clientX - box.left;
prevy = window.pageYOffset + evt.clientY - box.top;
}
function doMouseDrag(evt) {
if (!dragging) return;
var box = canvas.getBoundingClientRect();
var x = window.pageXOffset + evt.clientX - box.left;
var y = window.pageYOffset + evt.clientY - box.top;
var ray1 = toRay(prevx, prevy);
var ray2 = toRay(x, y);
applyTransvection(ray1, ray2);
prevx = x;
prevy = y;
if (callback) {
callback();
}
}
function doMouseUp(evt) {
if (dragging) {
document.removeEventListener("mousemove", doMouseDrag, false);
document.removeEventListener("mouseup", doMouseUp, false);
dragging = false;
}
}
function doTouchStart(evt) {
if (evt.touches.length != 1) {
doTouchCancel();
return;
}
evt.preventDefault();
var r = canvas.getBoundingClientRect();
prevx = evt.touches[0].clientX - r.left;
prevy = evt.touches[0].clientY - r.top;
canvas.addEventListener("touchmove", doTouchMove, false);
canvas.addEventListener("touchend", doTouchEnd, false);
canvas.addEventListener("touchcancel", doTouchCancel, false);
touchStarted = true;
centerX = canvas.width / 2;
centerY = canvas.height / 2;
var radius = Math.min(centerX, centerY);
radius2 = radius * radius;
}
function doTouchMove(evt) {
if (evt.touches.length != 1 || !touchStarted) {
doTouchCancel();
return;
}
evt.preventDefault();
var r = canvas.getBoundingClientRect();
var x = evt.touches[0].clientX - r.left;
var y = evt.touches[0].clientY - r.top;
var ray1 = toRay(prevx, prevy);
var ray2 = toRay(x, y);
applyTransvection(ray1, ray2);
prevx = x;
prevy = y;
if (callback) {
callback();
}
}
function doTouchEnd(evt) {
doTouchCancel();
}
function doTouchCancel() {
if (touchStarted) {
touchStarted = false;
canvas.removeEventListener("touchmove", doTouchMove, false);
canvas.removeEventListener("touchend", doTouchEnd, false);
canvas.removeEventListener("touchcancel", doTouchCancel, false);
}
}
function toRay(x, y) {
var dx = x - centerX;
var dy = centerY - y;
var vx = dx * unitx[0] + dy * unity[0]; // The mouse point as a vector in the image plane.
var vy = dx * unitx[1] + dy * unity[1];
var vz = dx * unitx[2] + dy * unity[2];
var dist2 = vx * vx + vy * vy + vz * vz;
if (dist2 > radius2) {
return [vx, vy, vz];
} else {
var z = Math.sqrt(radius2 - dist2);
return [vx + z * unitz[0], vy + z * unitz[1], vz + z * unitz[2]];
}
}
function dot(v, w) {
return v[0] * w[0] + v[1] * w[1] + v[2] * w[2];
}
function length(v) {
return Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
}
function normalize(v, w) {
var d = length(w);
v[0] = w[0] / d;
v[1] = w[1] / d;
v[2] = w[2] / d;
}
function copy(v, w) {
v[0] = w[0];
v[1] = w[1];
v[2] = w[2];
}
function add(sum, v, w) {
sum[0] = v[0] + w[0];
sum[1] = v[1] + w[1];
sum[2] = v[2] + w[2];
}
function subtract(dif, v, w) {
dif[0] = v[0] - w[0];
dif[1] = v[1] - w[1];
dif[2] = v[2] - w[2];
}
function scale(ans, v, num) {
ans[0] = v[0] * num;
ans[1] = v[1] * num;
ans[2] = v[2] * num;
}
function cross(c, v, w) {
var x = v[1] * w[2] - v[2] * w[1];
var y = v[2] * w[0] - v[0] * w[2];
var z = v[0] * w[1] - v[1] * w[0];
c[0] = x;
c[1] = y;
c[2] = z;
}
this.setView(viewDirectionVector, viewUpVector, viewDistance);
canvas.addEventListener("mousedown", doMouseDown, false);
canvas.addEventListener("touchstart", doTouchStart, false);
}
}