Skip to content

Instantly share code, notes, and snippets.

@sbrl
Last active August 30, 2019 03:50
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sbrl/69a8fa588865cacef9c0 to your computer and use it in GitHub Desktop.
Save sbrl/69a8fa588865cacef9c0 to your computer and use it in GitHub Desktop.
[Vector.mjs] A simple ES6 Vector class. #microlibrary
"use strict";
/******************************************************
************** Simple ES6 Vector2 Class **************
******************************************************
* Author: Starbeamrainbowlabs
* Twitter: @SBRLabs
* Email: feedback at starbeamrainbowlabs dot com
*
* From https://gist.github.com/sbrl/69a8fa588865cacef9c0
******************************************************
* Originally written for my 2D Graphics ACW at Hull
* University.
******************************************************
* Changelog
******************************************************
* 19th December 2015:
* Added this changelog.
* 28th December 2015:
* Rewrite tests with klud.js + Node.js
* 30th January 2016:
* Tweak angleFrom function to make it work properly.
* 31st January 2016:
* Add the moveTowards function.
* Add the minComponent getter.
* Add the maxComponent getter.
* Add the equalTo function.
* Tests still need to be written for all of the above.
* 19th September 2016:
* Added Vector2 support to the multiply method.
* 10th June 2017:
* Fixed a grammatical mistake in a comment.
* Added Vector2.fromBearing static method.
* 21st October 2017:
* Converted to ES6 module.
* Added Vector2.zero and Vector2.one constants. Remember to clone them!
* 4th August 2018: (#LOWREZJAM!)
* Optimised equalTo()
* 6th August 2018: (#LOWREZJAM again!)
* Added round(), floor(), and ceil()
* 7th August 2018: (moar #LOWREZJAM :D)
* Added area() and snapTo(grid_size)
* 10th August 2018: (even more #LOWREZJAM!)
* Added Vector2 support to divide()
* 12th June 2019:
* Fixed limitTo() behaviour
* Added setTo() that uses the old limitTo() behaviour
* Squash a nasty bug in angleFrom()
* Fix & update the test suite to cover new and bugfixed functionality
* Squash another nasty bug in .minComponent and .maxComponent involving negative numbers
*/
class Vector2 {
// Constructor
constructor(inX, inY) {
if(typeof inX != "number")
throw new Error("Invalid x value.");
if(typeof inY != "number")
throw new Error("Invalid y value.");
// Store the (x, y) coordinates
this.x = inX;
this.y = inY;
}
/**
* Add another Vector2 to this Vector2.
* @param {Vector2} v The Vector2 to add.
* @return {Vector2} The current Vector2. useful for daisy-chaining calls.
*/
add(v) {
this.x += v.x;
this.y += v.y;
return this;
}
/**
* Take another Vector2 from this Vector2.
* @param {Vector2} v The Vector2 to subtrace from this one.
* @return {Vector2} The current Vector2. useful for daisy-chaining calls.
*/
subtract(v) {
this.x -= v.x;
this.y -= v.y;
return this;
}
/**
* Divide the current Vector2 by a given value.
* @param {Number|Vector2} value The number (or Vector2) to divide by.
* @return {Vector2} The current Vector2. Useful for daisy-chaining calls.
*/
divide(value) {
if(value instanceof Vector2)
{
this.x /= value.x;
this.y /= value.y;
}
else if(typeof value == "number")
{
this.x /= value;
this.y /= value;
}
else
throw new Error("Can't divide by non-number value.");
return this;
}
/**
* Multiply the current Vector2 by a given value.
* @param {Number|Vector2} value The number (or Vector2) to multiply the current Vector2 by.
* @return {Vector2} The current Vector2. useful for daisy-chaining calls.
*/
multiply(value) {
if(value instanceof Vector2)
{
this.x *= value.x;
this.y *= value.y;
}
else if(typeof value == "number")
{
this.x *= value;
this.y *= value;
}
else
throw new Error("Can't multiply by non-number value.");
return this;
}
/**
* Move the Vector2 towards the given Vector2 by the given amount.
* @param {Vector2} v The Vector2 to move towards.
* @param {Number} amount The distance to move towards the given Vector2.
*/
moveTowards(v, amount)
{
// From http://stackoverflow.com/a/2625107/1460422
var dir = new Vector2(
v.x - this.x,
v.y - this.y
).limitTo(amount);
this.x += dir.x;
this.y += dir.y;
return this;
}
/**
* Rounds the x and y components of this Vector2 down to the next integer.
* @return {Vector2} This Vector2 - useful for diasy-chaining.
*/
floor() {
this.x = Math.floor(this.x);
this.y = Math.floor(this.y);
return this;
}
/**
* Rounds the x and y components of this Vector2 up to the next integer.
* @return {Vector2} This Vector2 - useful for diasy-chaining.
*/
ceil() {
this.x = Math.ceil(this.x);
this.y = Math.ceil(this.y);
return this;
}
/**
* Rounds the x and y components of this Vector2 to the nearest integer.
* @return {Vector2} This Vector2 - useful for diasy-chaining.
*/
round() {
this.x = Math.round(this.x);
this.y = Math.round(this.y);
return this;
}
/**
* Calculates the 'area' of this Vector2 and returns the result.
* In other words, returns x * y. Useful if you're using a Vector2 to store
* a size.
* @return {Number} The 'area' of this Vector2.
*/
area() {
return this.x * this.y;
}
/**
* Snaps this Vector2 to an imaginary square grid with the specified sized
* squares.
* @param {Number} grid_size The size of the squares on the imaginary grid to which to snap.
* @return {Vector2} The current Vector2 - useful for daisy-chaining.
*/
snapTo(grid_size) {
this.x = Math.floor(this.x / grid_size) * grid_size;
this.y = Math.floor(this.y / grid_size) * grid_size;
return this;
}
/**
* Limit the length of the current Vector2 to value without changing the
* direction in which the Vector2 is pointing.
* @param {Number} value The number to limit the current Vector2's length to.
* @return {Vector2} The current Vector2. Useful for daisy-chaining calls.
*/
limitTo(value) {
if(typeof value != "number")
throw new Error("Can't limit to non-number value.");
if(this.length > value) {
this.divide(this.length);
this.multiply(value);
}
return this;
}
/**
* Like limitTo(), but explicitly sets the length of the Vector2 without changing the direction.
* In other words, it acts like limitTo, but also scales up small Vector2s to match the specified length.
* @param {Number} value The length to set the Vector2 to.
*/
setTo(value) {
if(typeof value != "number")
throw new Error("Can't limit to non-number value.");
this.divide(this.length);
this.multiply(value);
return this;
}
/**
* Return the dot product of the current Vector2 and another Vector2.
* @param {Vector2} v The other Vector2 we should calculate the dot product with.
* @return {Vector2} The current Vector2. Useful for daisy-chaining calls.
*/
dotProduct(v) {
return (this.x * v.x) + (this.y * v.y);
}
/**
* Calculate the angle, in radians, from north to another Vector2.
* @param {Vector2} v The other Vector2 to which to calculate the angle.
* @return {Vector2} The current Vector2. Useful for daisy-chaining calls.
*/
angleFrom(v) {
// From http://stackoverflow.com/a/16340752/1460422
var angle = Math.atan2(v.y - this.y, v.x - this.x);
angle += Math.PI / 2;
if(angle < 0) angle += Math.PI * 2;
return angle;
}
/**
* Clones the current Vector2.
* @return {Vector2} A clone of the current Vector2. Very useful for passing around copies of a Vector2 if you don't want the original to be altered.
*/
clone() {
return new Vector2(this.x, this.y);
}
/*
* Returns a representation of the current Vector2 as a string.
* @returns {string} A representation of the current Vector2 as a string.
*/
toString() {
return `(${this.x}, ${this.y})`;
}
/**
* Whether the Vector2 is equal to another Vector2.
* @param {Vector2} v The Vector2 to compare to.
* @return {boolean} Whether the current Vector2 is equal to the given Vector2.
*/
equalTo(v)
{
return this.x == v.x && this.y == v.y;
}
/**
* Get the unit Vector2 of the current Vector2 - that is a Vector2 poiting in the same direction with a length of 1. Note that this does *not* alter the original Vector2.
* @return {Vector2} The current Vector2's unit form.
*/
get unitVector2() {
var length = this.length;
return new Vector2(
this.x / length,
this.y / length);
}
/**
* Get the length of the current Vector2.
* @return {Number} The length of the current Vector2.
*/
get length() {
return Math.sqrt((this.x * this.x) + (this.y * this.y));
}
/**
* Get the value of the minimum component of the Vector2.
* @return {Number} The minimum component of the Vector2.
*/
get minComponent() {
if(Math.abs(this.x) < Math.abs(this.y))
return this.x;
return this.y;
}
/**
* Get the value of the maximum component of the Vector2.
* @return {Number} The maximum component of the Vector2.
*/
get maxComponent() {
if(Math.abs(this.x) > Math.abs(this.y))
return this.x;
return this.y;
}
}
/**
* Returns a new Vector2 based on an angle and a length.
* @param {Number} angle The angle, in radians.
* @param {Number} length The length.
* @return {Vector2} A new Vector2 that represents the (x, y) of the specified angle and length.
*/
Vector2.fromBearing = function(angle, length) {
return new Vector2(
length * Math.cos(angle),
length * Math.sin(angle)
);
}
Vector2.zero = new Vector2(0, 0);
Vector2.one = new Vector2(1, 1);
export default Vector2;
"use strict";
import Vector2 from './Vector2.mjs';
// klud.js - from https://bitbucket.org/zserge/klud.js/
global.klud = !function(n){"use strict";function t(n,e){if(typeof n!=typeof e)return!1;if(n instanceof Function)return n.toString()===e.toString();if(n===e||n.valueOf()===e.valueOf())return!0;if(!(n instanceof Object))return!1;var r=Object.keys(n);if(r.length!=Object.keys(e).length)return!1;for(var i in e)if(e.hasOwnProperty(i)){if(-1===r.indexOf(i))return!1;if(!t(n[i],e[i]))return!1}return!0}function e(n){var t=function(){var e;if(t.called=t.called||[],t.thrown=t.thrown||[],n)try{e=n.apply(n["this"],arguments),t.thrown.push(void 0)}catch(r){t.thrown.push(r)}return t.called.push(arguments),e};return t}var r=[],i=function(){r.length>0?r[0](i):f("finalize")},o=n,f=function(){};n.test=function(u,c,s){if("function"==typeof u)return f=u,void(o=c||n);var a=function(n){var i={ok:o.ok,spy:o.spy,eq:o.eq},a=f,p=function(){o.ok=i.ok,o.spy=i.spy,o.eq=i.eq,a("end",u),r.shift(),n&&n()};o.eq=t,o.spy=e,o.ok=function(n,t){n=!!n,n?a("pass",u,t):a("fail",u,t)},a("begin",u);try{c(p)}catch(h){a("except",u,h)}s||(a("end",u),o.ok=i.ok,o.spy=i.spy,o.eq=i.eq)};s?(r.push(a),1==r.length&&i()):a()}}(global);
var counters = {
passed: 0,
failed: 0,
errors: 0
};
test(function(e, test, msg) {
switch (e) {
case 'begin':
console.log(`\u001b[1m> ${test}\u001b[0m\n`);
break;
case 'end':
console.log();
break;
case 'pass':
counters.passed++;
console.log(" \u001b[32m✓\u001b[0m " + msg);
break;
case 'fail':
counters.failed++;
console.log(" \u001b[31m✗\u001b[0m " + msg);
break;
case 'except':
counters.errors++;
console.log(" \u001b[31m\u001b[1m!!\u001b[0m " + msg);
break;
}
});
test("Creation", function() {
var v1 = new Vector2(10, 10);
ok(v1.x === 10, "Small creation - x");
ok(v1.y === 10, "Small creation - y");
var v2 = new Vector2(572, 672651);
ok(v2.x === 572, "Large creation - x");
ok(v2.y === 672651, "Large creation - y");
});
test("Invalid Creation", function() {
var result1 = spy(function() {
var v1 = new Vector2();
});
result1();
ok(result1.thrown.length == 1 && typeof result1.thrown[0] !== "undefined", "No arguments");
var result2 = spy(function() {
var v2 = new Vector2(34);
});
result2();
ok(result2.thrown.length == 1 && typeof result2.thrown[0] !== "undefined", "1 Argument");
var result3 = spy(function() {
var v3 = new Vector2("cheese");
});
result3();
ok(result3.thrown.length == 1 && typeof result3.thrown[0] !== "undefined", "Single invalid argument");
var result4 = spy(function() {
var v4 = new Vector2("cheese", "rocket");
});
result4();
ok(result4.thrown.length == 1 && typeof result4.thrown[0] !== "undefined", "2 Invalid arguments");
});
test("Addition", function() {
var v1 = new Vector2(10, 10);
v1.add(new Vector2(20, 20));
ok(v1.x == 30, "Simple adding - x");
ok(v1.y == 30, "Simple adding - y");
var v = new Vector2(4, 6);
v.add(new Vector2(5, 2));
ok(v.x == 9, "More simple adding - x");
ok(v.y == 8, "More simple adding - y");
});
test("Subtraction", function() {
var v = new Vector2(10, 10);
v.subtract(new Vector2(20, 20));
ok(v.x == -10, "Subtracting - x");
ok(v.y == -10, "Subtracting - y");
});
test("Division", function() {
var v = new Vector2(10, 10);
v.divide(20);
ok(v.x == 0.5, "Dividing - x");
ok(v.y == 0.5, "Dividing - y");
var result = spy(function() {
v.divide("porcupine");
});
result();
ok(result.thrown.length == 1 &&
typeof result.thrown[0] != "undefined",
"Invalid argument");
});
test("Multiplication", function() {
var v = new Vector2(3, 4);
v.multiply(11);
ok(v.x == 33, "Multiplying number - x");
ok(v.y == 44, "Multiplying number - y");
var result = spy(function() {
v.multiply("cake");
});
result();
ok(result.thrown.length == 1 &&
typeof result.thrown[0] != "undefined",
"Invalid argument");
var v2 = new Vector2(4, 5),
v3 = new Vector2(2, 3);
v2.multiply(v3);
ok(v2.x == 8 && v3.x == 2, "Multiplying Vector2 - x");
ok(v2.y == 15 && v3.y == 3, "Multiplying Vector2 - x");
});
test("Dot Product", function() {
var v1 = new Vector2(3, 4),
v2 = new Vector2(4, 3),
result = v1.dotProduct(v2);
ok(result === 24, "Dot product");
});
test("Clone Creation", function() {
var v1 = new Vector2(3, 4),
v2 = v1.clone();
ok(v1.x === v2.x, "Clone creation - x");
ok(v1.y === v2.y, "Clone creation - y");
});
test("Clone Alteration", function() {
var v1 = new Vector2(3, 4),
v2 = v1.clone();
v2.x = 63;
v2.y = 75;
ok(v1.x === 3, "Clone alteration - x1");
ok(v1.y === 4, "Clone alteration - y1");
ok(v2.x === 63, "Clone alteration - x2");
ok(v2.y === 75, "Clone alteration - y2");
});
test("Length", function() {
var v = new Vector2(3, 4);
ok(v.length === 5, "Length");
});
test("Unit Vector2", function() {
var v = new Vector2(3, 4),
uv = v.unitVector2;
ok(uv.x == 0.6, "Unit Vector2 - x");
ok(uv.y == 0.8, "Unit Vector2 - y");
});
test("Limit To", function() {
var v = new Vector2(3, 4),
uv = v.limitTo(1);
ok(uv.x == 0.6, "Limit to - basic - x");
ok(uv.y == 0.8, "Limit to - basic - y");
v = new Vector2(4, 5);
uv = v.limitTo(10, 10);
ok(uv.x == 4, "Limit to - small - x");
ok(uv.y == 5, "Limit to - small - y");
var result = spy(function() {
v.limitTo("cheese");
});
result();
ok(result.thrown.length == 1 && typeof result.thrown[0] !== "undefined", "Invalid argument");
});
test("Set To", function() {
var v = new Vector2(3, 4),
uv = v.setTo(1);
ok(uv.x == 0.6, "Set to - basic - x");
ok(uv.y == 0.8, "Set to - basic - y");
v = new Vector2(3, 4);
uv = v.setTo(13);
ok(Math.abs(uv.x - 7.8) < 0.0001, "Set to - small - x");
ok(Math.abs(uv.y - 10.4) < 0.0001, "Set to - small - y");
var result = spy(function() {
v.limitTo("cheese");
});
result();
ok(result.thrown.length == 1 && typeof result.thrown[0] !== "undefined", "Invalid argument");
});
test("Angle From", function() {
var v1 = new Vector2(3, 4),
v2 = new Vector2(3, 8),
angle = v2.angleFrom(v1),
expected = 0;
ok(Math.abs(angle - expected) < 0.0001, "Angle from - noon");
v1 = new Vector2(3, 4),
v2 = new Vector2(8, 4),
angle = v1.angleFrom(v2),
expected = Math.PI / 2;
ok(Math.abs(angle - expected) < 0.0001, "Angle from - 3 o'clock");
v1 = new Vector2(-4, 3),
v2 = new Vector2(3, 3),
angle = v1.angleFrom(v2),
expected = Math.PI / 2;
ok(Math.abs(angle - expected) < 0.0001, "Angle from - 3 o'clock - negative");
v1 = new Vector2(3, 4),
v2 = new Vector2(3, 8),
angle = v1.angleFrom(v2),
expected = Math.PI;
ok(Math.abs(angle - expected) < 0.0001, "Angle from - 6 o'clock");
v1 = new Vector2(-3, -4),
v2 = new Vector2(-3, 8),
angle = v1.angleFrom(v2),
expected = Math.PI;
ok(Math.abs(angle - expected) < 0.0001, "Angle from - 6 o'clock - negative");
});
test("minComponent", function() {
var v = new Vector2(5, 8),
expected = 5;
ok(v.minComponent == expected, "minComponent");
v = new Vector2(5, -8),
expected = 5;
ok(v.minComponent == expected, "minComponent - negative A");
v = new Vector2(-5, 8),
expected = -5;
ok(v.minComponent == expected, "minComponent - negative B");
v = new Vector2(-5, -8),
expected = -5;
ok(v.minComponent == expected, "minComponent - negative C");
});
test("maxComponent", function() {
var v = new Vector2(5, 8),
expected = 8;
ok(v.maxComponent == expected, "maxComponent");
v = new Vector2(5, -8),
expected = -8;
ok(v.maxComponent == expected, "maxComponent - negative A");
v = new Vector2(-5, 8),
expected = 8;
ok(v.maxComponent == expected, "maxComponent - negative B");
v = new Vector2(-5, -8),
expected = -8;
ok(v.maxComponent == expected, "maxComponent - negative C");
});
test("To String", function() {
var v = new Vector2(5, 8),
expected = "(5, 8)";
ok(v.toString() == expected, "To string");
});
process.on("exit", function () {
console.log(`Passed: ${counters.passed} Failed: ${counters.failed} Errors: ${counters.errors}`)
});
@Ziao
Copy link

Ziao commented Mar 31, 2017

Two quick things;

  • Negative numbers can cause some weirdness, for example minComponent() and maxComponent() use Math.min so will behave incorrectly.
  • limitTo doesn't limit, it sets the length. Vector(1, 0).limitTo(4) will become Vector(4,0)

Easily fixed, but in case someone thinks about using this.

@kreso22
Copy link

kreso22 commented Jan 29, 2019

Just a warning to anyone coming to copy paste some code (like myself)...

This code is good only for comparison. Not if you want to initialize a new zero Vector.
Vector.zero = new Vector(0, 0);
Vector.one = new Vector(1, 1);

If you do this and then lets imagine you have 2 variables

var position = Vector.zero;
var speed = Vector.zero;

Now change the position, and it will result in also changing the speed (because it's same reference). That's probably not what you want.

Actually,
you probably want all the operations to create a new vector instead of changing the current one. This could be very dangerous.

Example:

add(v) {
var x = this.x + v.x;
var y = this.y + v.y;
return new Vector2(x, y);
}

@sbrl
Copy link
Author

sbrl commented Jun 12, 2019

Hey! How did I not catch these comments?

@kreso22: Indeed, that can be an issue. I actually had it like that before, but changed it at some point. The intent is to avoid creating new objects unnecessarily, to avoid too many objects needing to be garbage collected. My general design pattern is like this:

let v = Vector2.zero.clone()
let w = v.clone().add(new Vector2(10, 20));

This makes it explicit that you're creating a new instance. I've been toying with the idea of adding a recycling system to this too, so you could do something like this:

let temp = new Vector2(30, 40);

// ....

Vector2.recycle(temp);

// ......

let another = Vector2.reuse(40, 50);

This would further speed things up, because JS garbage collection is very slow - especially when creating a large number of instances of something that then later need garbage collecting.

@Ziao: Thanks! I'll investigate fixing those ASAP.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment