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() {