diff --git a/src/webgl/interaction.js b/src/webgl/interaction.js
index d9015b15f0..666e9a07be 100644
--- a/src/webgl/interaction.js
+++ b/src/webgl/interaction.js
@@ -29,6 +29,12 @@ import * as constants from '../core/constants';
* Setting this to true makes mobile interactions smoother by preventing
* accidental interactions with the page while orbiting. But if you're already
* doing it via css or want the default touch actions, consider setting it to false.
+ * freeRotation - Boolean, default value is false.
+ * By default, horizontal movement of the mouse or touch pointer rotates the camera
+ * around the y-axis, and vertical movement rotates the camera around the x-axis.
+ * But if setting this option to true, the camera always rotates in the direction
+ * the pointer is moving. For zoom and move, the behavior is the same regardless of
+ * true/false.
* @chainable
* @example
*
@@ -42,7 +48,12 @@ import * as constants from '../core/constants';
* }
* function draw() {
* background(200);
+ *
+ * // If you execute the line commented out instead of next line, the direction of rotation
+ * // will be the direction the mouse or touch pointer moves, not around the X or Y axis.
* orbitControl();
+ * // orbitControl(1, 1, 1, {freeRotation: true});
+ *
* rotateY(0.5);
* box(30, 50);
* }
@@ -105,12 +116,15 @@ p5.prototype.orbitControl = function(
this._setProperty('touchActionsDisabled', true);
}
+ // If option.freeRotation is true, the camera always rotates freely in the direction
+ // the pointer moves. default value is false (normal behavior)
+ const { freeRotation = false } = options;
+
// get moved touches.
const movedTouches = [];
- for (let i = 0; i < this.touches.length; i++) {
- const curTouch = this.touches[i];
- for (let k = 0; k < this._renderer.prevTouches.length; k++) {
- const prevTouch = this._renderer.prevTouches[k];
+
+ this.touches.forEach(curTouch => {
+ this._renderer.prevTouches.forEach(prevTouch => {
if (curTouch.id === prevTouch.id) {
const movedTouch = {
x: curTouch.x,
@@ -120,8 +134,9 @@ p5.prototype.orbitControl = function(
};
movedTouches.push(movedTouch);
}
- }
- }
+ });
+ });
+
this._renderer.prevTouches = this.touches;
// The idea of using damping is based on the following website. thank you.
@@ -232,7 +247,16 @@ p5.prototype.orbitControl = function(
this._renderer.zoomVelocity += deltaRadius;
}
if (Math.abs(this._renderer.zoomVelocity) > 0.001) {
- this._renderer._curCamera._orbit(0, 0, this._renderer.zoomVelocity);
+ // if freeRotation is true, we use _orbitFree() instead of _orbit()
+ if (freeRotation) {
+ this._renderer._curCamera._orbitFree(
+ 0, 0, this._renderer.zoomVelocity
+ );
+ } else {
+ this._renderer._curCamera._orbit(
+ 0, 0, this._renderer.zoomVelocity
+ );
+ }
// In orthogonal projection, the scale does not change even if
// the distance to the gaze point is changed, so the projection matrix
// needs to be modified.
@@ -256,16 +280,24 @@ p5.prototype.orbitControl = function(
// accelerate rotate velocity
this._renderer.rotateVelocity.add(
deltaTheta * rotateAccelerationFactor,
- deltaPhi * rotateAccelerationFactor,
- 0
+ deltaPhi * rotateAccelerationFactor
);
}
if (this._renderer.rotateVelocity.magSq() > 0.000001) {
- this._renderer._curCamera._orbit(
- this._renderer.rotateVelocity.x,
- this._renderer.rotateVelocity.y,
- 0
- );
+ // if freeRotation is true, the camera always rotates freely in the direction the pointer moves
+ if (freeRotation) {
+ this._renderer._curCamera._orbitFree(
+ -this._renderer.rotateVelocity.x,
+ this._renderer.rotateVelocity.y,
+ 0
+ );
+ } else {
+ this._renderer._curCamera._orbit(
+ this._renderer.rotateVelocity.x,
+ this._renderer.rotateVelocity.y,
+ 0
+ );
+ }
// damping
this._renderer.rotateVelocity.mult(damping);
} else {
diff --git a/src/webgl/p5.Camera.js b/src/webgl/p5.Camera.js
index 7a81de3172..c5a4a6cfef 100644
--- a/src/webgl/p5.Camera.js
+++ b/src/webgl/p5.Camera.js
@@ -1781,6 +1781,84 @@ p5.Camera = class Camera {
);
}
+ /**
+ * Orbits the camera about center point. For use with orbitControl().
+ * Unlike _orbit(), the direction of rotation always matches the direction of pointer movement.
+ * @method _orbitFree
+ * @private
+ * @param {Number} dx the x component of the rotation vector.
+ * @param {Number} dy the y component of the rotation vector.
+ * @param {Number} dRadius change in radius
+ */
+ _orbitFree(dx, dy, dRadius) {
+ // Calculate the vector and its magnitude from the center to the viewpoint
+ const diffX = this.eyeX - this.centerX;
+ const diffY = this.eyeY - this.centerY;
+ const diffZ = this.eyeZ - this.centerZ;
+ let camRadius = Math.hypot(diffX, diffY, diffZ);
+ // front vector. unit vector from center to eye.
+ const front = new p5.Vector(diffX, diffY, diffZ).normalize();
+ // up vector. camera's up vector.
+ const up = new p5.Vector(this.upX, this.upY, this.upZ);
+ // side vector. Right when viewed from the front. (like x-axis)
+ const side = new p5.Vector.cross(up, front).normalize();
+ // down vector. Bottom when viewed from the front. (like y-axis)
+ const down = new p5.Vector.cross(front, side);
+
+ // side vector and down vector are no longer used as-is.
+ // Create a vector representing the direction of rotation
+ // in the form cos(direction)*side + sin(direction)*down.
+ // Make the current side vector into this.
+ const directionAngle = Math.atan2(dy, dx);
+ down.mult(Math.sin(directionAngle));
+ side.mult(Math.cos(directionAngle)).add(down);
+ // The amount of rotation is the size of the vector (dx, dy).
+ const rotAngle = Math.sqrt(dx*dx + dy*dy);
+ // The vector that is orthogonal to both the front vector and
+ // the rotation direction vector is the rotation axis vector.
+ const axis = new p5.Vector.cross(front, side);
+
+ // update camRadius
+ camRadius *= Math.pow(10, dRadius);
+ // prevent zooming through the center:
+ if (camRadius < this.cameraNear) {
+ camRadius = this.cameraNear;
+ }
+ if (camRadius > this.cameraFar) {
+ camRadius = this.cameraFar;
+ }
+
+ // If the axis vector is likened to the z-axis, the front vector is
+ // the x-axis and the side vector is the y-axis. Rotate the up and front
+ // vectors respectively by thinking of them as rotations around the z-axis.
+
+ // Calculate the components by taking the dot product and
+ // calculate a rotation based on that.
+ const c = Math.cos(rotAngle);
+ const s = Math.sin(rotAngle);
+ const dotFront = up.dot(front);
+ const dotSide = up.dot(side);
+ const ux = dotFront * c + dotSide * s;
+ const uy = -dotFront * s + dotSide * c;
+ const uz = up.dot(axis);
+ up.x = ux * front.x + uy * side.x + uz * axis.x;
+ up.y = ux * front.y + uy * side.y + uz * axis.y;
+ up.z = ux * front.z + uy * side.z + uz * axis.z;
+ // We won't be using the side vector and the front vector anymore,
+ // so let's make the front vector into the vector from the center to the new eye.
+ side.mult(-s);
+ front.mult(c).add(side).mult(camRadius);
+
+ // it's complete. let's update camera.
+ this.camera(
+ front.x + this.centerX,
+ front.y + this.centerY,
+ front.z + this.centerZ,
+ this.centerX, this.centerY, this.centerZ,
+ up.x, up.y, up.z
+ );
+ }
+
/**
* Returns true if camera is currently attached to renderer.
* @method _isActive
diff --git a/test/unit/webgl/p5.Camera.js b/test/unit/webgl/p5.Camera.js
index 2d62fd1ec1..e9572c5419 100644
--- a/test/unit/webgl/p5.Camera.js
+++ b/test/unit/webgl/p5.Camera.js
@@ -548,6 +548,58 @@ suite('p5.Camera', function() {
myCam._orbit(0, 0, -250);
assert.deepEqual(myCam.cameraMatrix.mat4, myCamCopy.cameraMatrix.mat4, 'deep equal is failing');
});
+ test('_orbitFree(1,0,0) sets correct matrix', function() {
+ var expectedMatrix = new Float32Array([
+ 0.5403022766113281, 0, -0.8414709568023682, 0,
+ -0, 1, 0, 0,
+ 0.8414709568023682, 0, 0.5403022766113281, 0,
+ -8.216248374992574e-7, 0, -86.6025390625, 1
+ ]);
+
+ myCam._orbitFree(1, 0, 0);
+
+ assert.deepEqual(myCam.cameraMatrix.mat4, expectedMatrix);
+ });
+ test('_orbitFree(0,1,0) sets correct matrix', function() {
+ var expectedMatrix = new Float32Array([
+ 1, -2.8148363983860944e-17, -5.1525235865883254e-17, 0,
+ -2.8148363983860944e-17, 0.5403022766113281, -0.8414709568023682, 0,
+ 5.1525235865883254e-17, 0.8414709568023682, 0.5403022766113281, 0,
+ 1.8143673340160988e-22, -8.216248374992574e-7, -86.6025390625, 1
+ ]);
+
+ myCam._orbitFree(0, 1, 0);
+
+ assert.deepEqual(myCam.cameraMatrix.mat4, expectedMatrix);
+ });
+ test('_orbitFree(0,0,1) sets correct matrix', function() {
+ var expectedMatrix = new Float32Array([
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, -866.025390625, 1
+ ]);
+
+ myCam._orbitFree(0, 0, 1);
+
+ assert.deepEqual(myCam.cameraMatrix.mat4, expectedMatrix);
+ });
+ test('Rotate camera 360° with _orbitFree() returns it to its original position', function() {
+ // Rotate the camera 360 degrees in any direction using _orbitFree()
+ // and it will return to its original state.
+ myCam.camera(100, 100, 100, 0, 0, 0, 1, 2, 3);
+ var myCamCopy = myCam.copy();
+ // Performing 200 rotations of Math.PI*0.01 makes exactly one rotation.
+ // However, we test in a slightly slanted direction instead of parallel with axis.
+ for (let i = 0; i < 200; i++) {
+ myCamCopy._orbitFree(Math.PI * 0.006, Math.PI * 0.008, 0);
+ }
+ for (let i = 0; i < myCamCopy.cameraMatrix.mat4.length; i++) {
+ expect(
+ myCamCopy.cameraMatrix.mat4[i]).to.be.closeTo(
+ myCam.cameraMatrix.mat4[i], 0.001);
+ }
+ });
});
suite('Projection', function() {