Skip to content

Commit

Permalink
Fixed 3-D rotations
Browse files Browse the repository at this point in the history
  • Loading branch information
tinybike committed Nov 22, 2016
1 parent 9059d34 commit a18318a
Show file tree
Hide file tree
Showing 6 changed files with 1,272 additions and 219 deletions.
40 changes: 30 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Twirl
[![Coverage Status](https://coveralls.io/repos/tinybike/twirl/badge.svg?branch=master&service=github)](https://coveralls.io/github/tinybike/twirl?branch=master)
[![npm version](https://badge.fury.io/js/twirl.svg)](https://badge.fury.io/js/twirl)

A few vanilla JS methods to move/rotate/scale points in 2-D and 3-D.
Tools for zooming and rotating around arbitrary points in 2-D and 3-D. Twirl uses Tait-Bryan angles (i.e., roll, pitch, and yaw) for 3-D rotations.

Usage
-----
Expand All @@ -21,17 +21,37 @@ A minified, browserified file `dist/twirl.min.js` is included for use in the bro
<script src="dist/twirl.min.js" type="text/javascript"></script>
```

To use `twirl.twirl`, specify the rotation angle, the rotation center (the point around which your points will be rotated; if `null` the rotation will be around the origin), scaling factor (amount to zoom in/out), and a list of points as an array-of-arrays:
### 2-D Rotate/Zoom

Twirl has a combined 2-D rotate/zoom method called `rotateZoom`. To use this function, you will need to specify:
- the rotation angle (degrees counter-clockwise in a right-handed coordinate system)
- the rotation center (the point around which your points will be rotated; if `null` the rotation will be around the origin)
- zoom factor (amount to zoom in/out)
- list of coordinates you want to rotate/zoom (as an array-of-arrays)
```javascript
var angle = 90; // 90 degree rotation
var center = [1, 1]; // rotate around the point (1, 1)
var zoom = 2; // 2x zoom
var coords = [[3, 1], [1, 1], [0, 2]]; // (x, y) coordinates
twirl.rotateZoom(angle, center, zoom, coords);
// output: [[1, 5], [1, 1], [-1, -1]]
```
Twirl also has individual `rotate`, `zoom`, and `translate` methods. Please refer to the unit tests for details/usage.

### 3-D Rotate/Zoom

Twirl's combined 3-D rotate/zoom method is called `rotateZoom3D`. This function requires the same inputs as `rotateZoom`, except that it takes three input angles (roll, pitch, and yaw) instead of just one. 3-D rotations are defined by specifying their elemental rotations; `rotateZoom3D` follows the z-y'-x'' (yaw-pitch-roll) intrinsic rotation convention.
```javascript
var angle = 30; // 30 degree rotation (counter-clockwise in a right-handed coordinate system)
var rotationCenter = [8, 10]; // rotate around the point (8, 10)
var scalingFactor = 1.1; // zoom in 1.1x
var coords = [[4, 2], [1, 2], [0, 0]]; // three (x, y) coordinate pairs
var newCoords = twirl.twirl(angle, rotationCenter, scalingFactor, coords);
// newCoords: [8.58948822334847, 0.17897644669693946],
// [5.731604390859821, -1.471023553303061],
// [5.878976446696939, -3.926279441628827]
var roll = 90; // 90 degree rotation around the x-axis
var pitch = 90; // 90 degree rotation around the y-axis
var yaw = 90; // 90 degree rotation around the z-axis
var center = [1, 1, 1]; // rotate around the point (1, 1, 1)
var zoom = 2; // 2x zoom
var coords = [[1, 1, 1], [2, 3, 4]]; // (x, y, z) coordinates
twirl.rotateZoom3D(roll, pitch, yaw, center, zoom, coords);
// output: [[1, 1, 1], [7, -3, 3]]
```
Twirl has a `rotate3D` (3-D rotation around the origin) method, as well as elemental rotation methods (called `roll`, `pitch`, and `yaw`). Please refer to unit tests for details/usage.

Tests
-----
Expand Down
179 changes: 124 additions & 55 deletions dist/twirl.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,59 @@ global.twirl = twirl;
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"./":2}],2:[function(require,module,exports){
/**
* Twirl: move, rotate, and scale points in 2-D and 3-D.
* Twirl: zoom and rotate around arbitrary points in 2-D and 3-D.
* (Uses the z-y'-x'' intrinsic rotation convention for 3-D rotations.)
* @author Jack Peterson (jack@tinybike.net)
*/

"use strict";

module.exports = {
function degreesToRadians(degrees) {
return degrees * Math.PI / 180;
}

degreesToRadians: function (degrees) {
return degrees * Math.PI / 180;
},
module.exports = {

// Rotate and scale in 2-D
twirl: function (angle, rotationCenter, scalingFactor, coords) {
if (!coords || !coords[0] || !coords[0].length) {
/**
* 2-D rotate/zoom.
* @param {number} angle Rotation angle in degrees counter-clockwise.
* @param {array=} center Center of rotation as an array [x, y] (default: [0, 0]).
* @param {number=} scale Scaling (zoom) factor (default: 1).
* @param {array} coords Coordinates to rotate/zoom as an array-of-arrays.
* @return {array} Rotated/zoomed coordinates as an array-of-arrays.
*/
rotateZoom: function (angle, center, scale, coords) {
if (!coords || !coords[0] || coords[0].length !== 2) {
throw new Error("Expected nested array coords: [[1, 2], [3, 4], ...]");
}
if (scalingFactor === null || scalingFactor === undefined) {
scalingFactor = 1;
if (scale === null || scale === undefined) {
scale = 1;
}
if (!rotationCenter || !rotationCenter.length) {
rotationCenter = [0, 0];
if (!center || center.length !== 2) {
center = [0, 0];
}
if (!angle) {
return this.scale(scalingFactor, coords);
return this.zoom(scale, coords);
}
var newCoords = this.translate([-rotationCenter[0], -rotationCenter[1]], coords);
var rotatedCoords = this.rotate(angle, newCoords);
var rescaledRotatedCoords = this.scale(scalingFactor, rotatedCoords);
return this.translate(rotationCenter, rescaledRotatedCoords);
return this.translate(
center,
this.zoom(
scale,
this.rotate(angle,
this.translate([-center[0], -center[1]], coords)
)
)
);
},

// 2-D rotation
/**
* 2-D rotation around the origin.
* @param {number} angle Rotation angle in degrees counter-clockwise.
* @param {array} coords Coordinates to rotate as an array-of-arrays.
* @return {array} Rotated coordinates as an array-of-arrays.
*/
rotate: function (angle, coords) {
var radians = this.degreesToRadians(angle);
var radians = degreesToRadians(angle);
var numCoords = coords.length;
var newCoords = new Array(numCoords);
var cos = Math.cos(radians);
Expand All @@ -54,35 +72,58 @@ module.exports = {
return newCoords;
},

// Rotate and scale in 3-D
twirl3d: function (angles, rotationCenter, scalingFactor, coords) {
if (!coords || !coords[0] || !coords[0].length) {
/**
* 3-D rotate/zoom: roll, pitch, and yaw are principal axis rotations
* (Tait-Bryan angles).
* @param {number} roll Roll angle in degrees counter-clockwise.
* @param {number} pitch Pitch angle in degrees counter-clockwise.
* @param {number} yaw Yaw angle in degrees counter-clockwise.
* @param {array=} center Center of rotation as an array [x, y, z] (default: [0, 0, 0]).
* @param {number=} scale Scaling (zoom) factor (default: 1).
* @param {array} coords Coordinates to rotate/zoom as an array-of-arrays.
* @return {array} Rotated/zoomed coordinates as an array-of-arrays.
*/
rotateZoom3D: function (roll, pitch, yaw, center, scale, coords) {
if (!coords || !coords[0] || coords[0].length !== 3) {
throw new Error("Expected nested array coords: [[1, 2, 3], [4, 5, 6], ...]");
}
if (scalingFactor === null || scalingFactor === undefined) {
scalingFactor = 1;
if (scale === null || scale === undefined) {
scale = 1;
}
if (!rotationCenter || !rotationCenter.length) {
rotationCenter = [0, 0, 0];
if (!center || center.length !== 3) {
center = [0, 0, 0];
}
if (!angles) {
return this.scale(scalingFactor, coords);
if (!roll && !pitch && !yaw) {
return this.zoom(scale, coords);
}
var newCoords = this.translate([-rotationCenter[0], -rotationCenter[1], -rotationCenter[2]], coords);
var rotatedCoords = this.rotate3d(angles[0], angles[1], angles[2], newCoords);
var rescaledRotatedCoords = this.scale(scalingFactor, rotatedCoords);
return this.translate(rotationCenter, rescaledRotatedCoords);
return this.translate(
center,
this.zoom(
scale,
this.rotate3D(
roll,
pitch,
yaw,
this.translate([-center[0], -center[1], -center[2]], coords)
)
)
);
},

// x-axis rotation (3-D)
/**
* Rotation around the x-axis in 3-D.
* @param {number} angle Rotation angle in degrees counter-clockwise.
* @param {array} coords Coordinates to rotate as an array-of-arrays.
* @return {array} Rotated coordinates as an array-of-arrays.
*/
roll: function (angle, coords) {
var radians = this.degreesToRadians(angle);
var radians = degreesToRadians(angle);
var numCoords = coords.length;
var newCoords = new Array(numCoords);
var cos = Math.cos(radians);
var sin = Math.sin(radians);
for (var i = 0; i < numCoords; ++i) {
newCoords = [
newCoords[i] = [
coords[i][0],
cos*coords[i][1] - sin*coords[i][2],
sin*coords[i][1] + cos*coords[i][2]
Expand All @@ -91,15 +132,20 @@ module.exports = {
return newCoords;
},

// y-axis rotation (3-D)
/**
* Rotation around the y-axis in 3-D.
* @param {number} angle Rotation angle in degrees counter-clockwise.
* @param {array} coords Coordinates to rotate as an array-of-arrays.
* @return {array} Rotated coordinates as an array-of-arrays.
*/
pitch: function (angle, coords) {
var radians = this.degreesToRadians(angle);
var radians = degreesToRadians(angle);
var numCoords = coords.length;
var newCoords = new Array(numCoords);
var cos = Math.cos(radians);
var sin = Math.sin(radians);
for (var i = 0; i < numCoords; ++i) {
newCoords = [
newCoords[i] = [
cos*coords[i][0] + sin*coords[i][2],
coords[i][1],
-sin*coords[i][0] + cos*coords[i][2]
Expand All @@ -108,15 +154,20 @@ module.exports = {
return newCoords;
},

// z-axis rotation (3-D)
/**
* Rotation around the z-axis in 3-D.
* @param {number} angle Rotation angle in degrees counter-clockwise.
* @param {array} coords Coordinates to rotate as an array-of-arrays.
* @return {array} Rotated coordinates as an array-of-arrays.
*/
yaw: function (angle, coords) {
var radians = this.degreesToRadians(angle);
var radians = degreesToRadians(angle);
var numCoords = coords.length;
var newCoords = new Array(numCoords);
var cos = Math.cos(radians);
var sin = Math.sin(radians);
for (var i = 0; i < numCoords; ++i) {
newCoords = [
newCoords[i] = [
cos*coords[i][0] - sin*coords[i][1],
sin*coords[i][0] + cos*coords[i][1],
coords[i][2]
Expand All @@ -125,50 +176,68 @@ module.exports = {
return newCoords;
},

// 3-D rotation
rotate3d: function (x, y, z, coords) {
/**
* 3-D rotation around the origin. Elemental rotations are applied in
* the following order: yaw, pitch, roll.
* @param {number} roll Roll angle in degrees counter-clockwise.
* @param {number} pitch Pitch angle in degrees counter-clockwise.
* @param {number} yaw Yaw angle in degrees counter-clockwise.
* @param {array} coords Coordinates to rotate as an array-of-arrays.
* @return {array} Rotated coordinates as an array-of-arrays.
*/
rotate3D: function (roll, pitch, yaw, coords) {
var newCoords = coords;
if (coords && coords.length) {
if (x) {
newCoords = this.roll(x, newCoords);
if (yaw) {
newCoords = this.yaw(yaw, newCoords);
}
if (y) {
newCoords = this.pitch(y, newCoords);
if (pitch) {
newCoords = this.pitch(pitch, newCoords);
}
if (z) {
newCoords = this.yaw(z, newCoords);
if (roll) {
newCoords = this.roll(roll, newCoords);
}
}
return newCoords;
},

// move: [x, y] or [x, y, z]
translate: function (move, coords) {
/**
* Move a point (vector) without rotating or rescaling it.
* @param {array} translation Amounts to move ([x, y] or [x, y, z]).
* @param {array} coords Coordinates to rotate as an array-of-arrays.
* @return {array} Translated coordinates as an array-of-arrays.
*/
translate: function (translation, coords) {
var numCoords = coords.length;
if (numCoords && coords && coords.length && coords[0] && coords[0].length) {
var d = coords[0].length;
var newCoords = new Array(numCoords);
for (var i = 0; i < numCoords; ++i) {
newCoords[i] = new Array(d);
for (var j = 0; j < d; ++j) {
newCoords[i][j] = coords[i][j] + move[j];
newCoords[i][j] = coords[i][j] + translation[j];
}
}
return newCoords;
}
return coords;
},

// rescale: multiply all coordinates by a scaling factor
scale: function (scalingFactor, coords) {
/**
* Zoom (rescale): multiply coordinates by a constant scaling factor.
* @param {number=} scale Scaling (zoom) factor.
* @param {array} coords Coordinates to rotate as an array-of-arrays.
* @return {array} Rescaled coordinates as an array-of-arrays.
*/
zoom: function (scale, coords) {
var numCoords = coords.length;
if (numCoords && coords && coords.length && coords[0] && coords[0].length) {
var d = coords[0].length;
var newCoords = new Array(numCoords);
for (var i = 0; i < numCoords; ++i) {
newCoords[i] = new Array(d);
for (var j = 0; j < d; ++j) {
newCoords[i][j] = scalingFactor*coords[i][j];
newCoords[i][j] = scale*coords[i][j];
}
}
return newCoords;
Expand Down

0 comments on commit a18318a

Please sign in to comment.