Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 46 additions & 14 deletions src/webgl/interaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
* <div>
Expand All @@ -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);
* }
Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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 {
Expand Down
78 changes: 78 additions & 0 deletions src/webgl/p5.Camera.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 52 additions & 0 deletions test/unit/webgl/p5.Camera.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down