Playchilla logo

Vector 2d for AS3

Here is the source code for an AS3 vector 2d class I wrote (Vec2.as and Vec2Const.as), feel free to use it and link back if you like it. Here are some features:

  • Implemented for most operations, add, sub, div, mul, scale, dot, normalize, cross, length, distance, rotation, spinor, lerp, slerp, reflection etc
  • Separation of read only operations and write operations.
  • Most methods have support for self modification (postfix with Self, e.g. addSelf).
  • Most methods have support for component wise operations (postfix with XY, e.g. addXY and addXYSelf).

Over the years I’ve written a couple of vector classes for different languages this time I ported an old C++ class to AS3. Well I couldn’t exactly port it, I more or less had to rewrite it due to differences in the two languages. What I really like about C++ is it’s strict typing and how it allows (good) developers to create code that is more or less impossible to misuse by for example using ‘const’ keywords.

As I mentioned earlier in my post AS3 and constants AS3 is missing such a feature. Another feature it’s missing is operator overloading (ability to create your own += operators). Both of those features are highly desirable for vector implementations.

Operator overloading and naming convention

It’s not hard to see why we want operator overloading considering this c++ example

Vec2 v1(1, 2);
Vec2 v2(3, 4);
v1 += v2; // Operator overloading of +=

However, in AS3 we have to settle with something less nice -methods that do the same work. Many implementations (even Adobes Vector3d among others) name the += ‘operator’ methods to ‘incrementBy’ or ‘decrementBy’ while ‘add’ and ‘sub’ returns a copy (v1 + v2). As an API user I find this rather confusing – how do I know which is modifying itself and which is returning a copy per intuition?

I decided to postfix the name all methods that modify itself with ‘Self’, so for example ‘v1.addSelf(v2);’ means v1 adds v2 to itself. This can easily be applied on any method without having to break your brain to figure out a good descriptive name.

For example what would you name a normalize() method if you wanted one that modified itself and another that returned a normalized vector? This naming convention makes it easy to keep things consequent and pretty self explanatory.

Vec2 v1 = new Vec2(1, 2);
Vec2 v2 = new Vec2(3, 4);
v1.addSelf(v2); // Substitute for +=

Const, or the absense of const

Just to sketch out why we want const consider the following scenario. You have a bot class, the bot class has a position. Only the bot is allowed to modify it position. Everything outside the bot is only allowed to read the position, how do we solve that? There are several ways mentioned here. For this Vector2d class I decided to go with inheritance for performance reasons. Since the vector objects often are involved in many computations, performance is relevant.

In short, using an interface for read only methods would force us to always access the components (x and y) via an intermediate get/set accessor (much slower than direct access). By doing this by inheritance internal operations will always work directly on the components and it’s easy to convert (use precompile options) to make direct access externally.

The class that contains read only methods is called Vec2Const and read and write operations are in Vec2.

public function getPos():Vec2Const { return _pos; } // only expose readables
...
private const _pos:Vec2 = new Vec2;

If you don’t get the point of const you could just go with the Vec2 and you will be fine =)

Enough talking – just give me the code

Most of the code is self explanatory, however I am aware that some parts would require some documentation (spinors, crossDet, lerp and slerp among other). Alrighy!

Vec2Const.as

package shared.math
{
	/**
	 * A 2d Vector class to perform constant operations. Use this class to make sure that objects stay consts, e.g.
	 * public function getPos():Vec2Const { return _pos; } // pos is not allowed to change outside of bot.
	 *
	 * Many method has a postfix of XY - this allows you to operate on the components directly i.e.
	 * instead of writing add(new Vec2(1, 2)) you can directly write addXY(1, 2);
	 *
	 * For performance reasons I am not using an interface for read only specification since internally it should be possible
	 * to use direct access to x and y. Externally x and y is obtained via getters which are a bit slower than direct access of
	 * a public variable. I suggest you stick with this during development. If there is a bottleneck you can just remove the get
	 * accessors and directly expose _x and _y (rename it to x and replace all _x and _y to this.x, this.y internally).
	 *
	 * The class in not commented properly yet - just subdivided into logical chunks.
	 *
	 * @author playchilla.com
	 *
	 * License: Use it as you wish and if you like it - link back!
	 */
	public class Vec2Const
	{
		public function get x():Number { return _x; }
		public function get y():Number { return _y; }
		
		public function Vec2Const(x:Number = 0, y:Number = 0)
		{
			_x = x;
			_y = y;
		}
		
		public function clone():Vec2 { return new Vec2(_x, _y); }
		
		/**
		 * Add, sub, mul and div
		 */
		public function add(pos:Vec2Const):Vec2 { return new Vec2(_x + pos._x, _y + pos._y); }
		public function addXY(x:Number, y:Number):Vec2 { return new Vec2(_x + x, _y + y); }
		
		public function sub(pos:Vec2Const):Vec2 { return new Vec2(_x - pos._x, _y - pos._y); }
		public function subXY(x:Number, y:Number):Vec2 { return new Vec2(_x - x, _y - y); }
		
		public function mul(vec:Vec2Const):Vec2 { return new Vec2(_x * vec._x, _y * vec._y); }
		public function mulXY(x:Number, y:Number):Vec2 { return new Vec2(_x * x, _y * y); }
		
		public function div(vec:Vec2Const):Vec2 { return new Vec2(_x / vec._x, _y / vec._y); }
		public function divXY(x:Number, y:Number):Vec2 { return new Vec2(_x / x, _y / y); }
		
		/**
		 * Scale
		 */
		public function scale(s:Number):Vec2 { return new Vec2(_x * s, _y * s); }
		
		public function rescale(newLength:Number):Vec2
		{
			const nf:Number = newLength / Math.sqrt(_x * _x + _y * _y);
			return new Vec2(_x * nf, _y * nf);
		}

		/**
		 * Normalize
		 */
		public function normalize():Vec2
		{
			const nf:Number = 1 / Math.sqrt(_x * _x + _y * _y);
			return new Vec2(_x * nf, _y * nf);
		}

		/**
		 * Distance
		 */
		public function length():Number { return Math.sqrt(_x * _x + _y * _y); }
		public function lengthSqr():Number { return _x * _x + _y * _y; }
		public function distance(vec:Vec2Const):Number
		{
			const xd:Number = _x - vec._x;
			const yd:Number = _y - vec._y;
			return Math.sqrt(xd * xd + yd * yd);
		}
		public function distanceXY(x:Number, y:Number):Number
		{
			const xd:Number = _x - x;
			const yd:Number = _y - y;
			return Math.sqrt(xd * xd + yd * yd);
		}
		public function distanceSqr(vec:Vec2Const):Number
		{
			const xd:Number = _x - vec._x;
			const yd:Number = _y - vec._y;
			return xd * xd + yd * yd;
		}
		public function distanceXYSqr(x:Number, y:Number):Number
		{
			const xd:Number = _x - x;
			const yd:Number = _y - y;
			return xd * xd + yd * yd;
		}

		/**
		 * Queries.
		 */
		public function equals(vec:Vec2Const):Boolean { return _x == vec._x && _y == vec._y; }
		public function equalsXY(x:Number, y:Number):Boolean { return _x == x && _y == y; }
		public function isNormalized():Boolean { return Math.abs((_x * _x + _y * _y)-1) < Vec2.EpsilonSqr; }
		public function isZero():Boolean { return _x == 0 && _y == 0; }
		public function isNear(vec2:Vec2Const):Boolean { return distanceSqr(vec2) < Vec2.EpsilonSqr; }
		public function isNearXY(x:Number, y:Number):Boolean { return distanceXYSqr(x, y) < Vec2.EpsilonSqr; }
		public function isWithin(vec2:Vec2Const, epsilon:Number):Boolean { return distanceSqr(vec2) < epsilon*epsilon; }
		public function isWithinXY(x:Number, y:Number, epsilon:Number):Boolean { return distanceXYSqr(x, y) < epsilon*epsilon; }
		public function isValid():Boolean { return !isNaN(_x) && !isNaN(_y) && isFinite(_x) && isFinite(_y); }
		public function getDegrees():Number { return getRads() * _RadsToDeg; }
		public function getRads():Number { return Math.atan2(_y, _x); }
		public function getRadsBetween(vec:Vec2Const):Number { return Math.atan2(x - vec.x, y - vec.y); }		}
		
		/**
		 * Dot product
		 */
		public function dot(vec:Vec2Const):Number { return _x * vec._x + _y * vec._y; }
		public function dotXY(x:Number, y:Number):Number { return _x * x + _y * y; }
		
		/**
		 * Cross determinant
		 */
		public function crossDet(vec:Vec2Const):Number { return _x * vec._y - _y * vec._x; }
		public function crossDetXY(x:Number, y:Number):Number { return _x * y - _y * x; }

		/**
		 * Rotate
		 */
		public function rotate(rads:Number):Vec2
		{
			const s:Number = Math.sin(rads);
			const c:Number = Math.cos(rads);
			return new Vec2(_x * c - _y * s, _x * s + _y * c);
		}
		public function normalRight():Vec2 { return new Vec2(-_y, _x); }
		public function normalLeft():Vec2 { return new Vec2(_y, -_x); }
		public function negate():Vec2 { return new Vec2( -_x, -_y); }
		
		/**
		 * Spinor rotation
		 */
		public function rotateSpinorXY(x:Number, y:Number):Vec2 { return new Vec2(_x * x - _y * y, _x * y + _y * x); }
		public function rotateSpinor(vec:Vec2Const):Vec2 { return new Vec2(_x * vec._x - _y * vec._y, _x * vec._y + _y * vec._x); }
		public function spinorBetween(vec:Vec2Const):Vec2
		{
			const d:Number = lengthSqr();
			const r:Number = (vec._x * _x + vec._y * _y) / d;
			const i:Number = (vec._y * _x - vec._x * _y) / d;
			return new Vec2(r, i);
		}

		/**
		 * Lerp / slerp
		 * Note: Slerp is not well tested yet.
		 */
		public function lerp(to:Vec2Const, t:Number):Vec2 { return new Vec2(_x + t * (to._x - _x), _y + t * (to._y - _y)); }
		
		public function slerp(vec:Vec2Const, t:Number):Vec2
		{
			const cosTheta:Number = dot(vec);
			const theta:Number = Math.acos(cosTheta);
			const sinTheta:Number = Math.sin(theta);
			if (sinTheta <= Vec2.Epsilon)
				return vec.clone();
			const w1:Number = Math.sin((1 - t) * theta) / sinTheta;
			const w2:Number = Math.sin(t * theta) / sinTheta;
			return scale(w1).add(vec.scale(w2));
		}

		/**
		 * Reflect
		 */
		public function reflect(normal:Vec2Const):Vec2
		{
			const d:Number = 2 * (_x * normal._x + _y * normal._y);
			return new Vec2(_x - d * normal._x, _y - d * normal._y);
		}

		/**
		 * String
		 */
		public function toString():String { return "[" + _x + ", " + _y + "]"; }
		
		public function getMin(p:Vec2Const):Vec2 { return new Vec2(Math.min(p._x, _x), Math.min(p._y, _y)); }
		public function getMax(p:Vec2Const):Vec2 { return new Vec2(Math.max(p._x, _x), Math.max(p._y, _y)); }
		
		internal var _x:Number;
		internal var _y:Number;
		
		private static const _RadsToDeg:Number = 180 / Math.PI;
	}
}

Vec2.as

package shared.math 
{
	/**
	 * A 2d Vector class to that is mutable.
	 * 
	 * Due to the lack of AS3 operator overloading most methods exists in different names,
	 * all methods that ends with Self actually modifies the object itself (including obvious ones copy, copyXY and zero).
	 * For example v1 += v2; is written as v1.addSelf(v2);
	 * 
	 * The class in not commented properly yet - just subdivided into logical chunks.
	 * 
	 * @author playchilla.com
	 *
	 * License: Use it as you wish and if you like it - link back!
	 */
	public class Vec2 extends Vec2Const
	{
		public static const Zero:Vec2Const = new Vec2Const;
		public static const Epsilon:Number = 0.0000001;
		public static const EpsilonSqr:Number = Epsilon * Epsilon;
		
		public static function createRandomDir():Vec2
		{
			const rads:Number = Math.random() * Math.PI * 2;
			return new Vec2(Math.cos(rads), Math.sin(rads));
		}
		
		public function Vec2(x:Number = 0, y:Number = 0) { super(x, y); }
		
		/**
		 * Copy / assignment
		 */
		public function set x(x:Number):void { _x = x; }
		public function set y(y:Number):void { _y = y; }

		public function copy(pos:Vec2Const):Vec2
		{
			_x = pos._x;
			_y = pos._y;
			return this;
		}
		public function copyXY(x:Number, y:Number):Vec2
		{
			_x = x;
			_y = y;
			return this;
		}
		public function zero():Vec2
		{
			_x = 0;
			_y = 0;
			return this;
		}

		/**
		 * Add
		 */
		public function addSelf(pos:Vec2Const):Vec2
		{
			_x += pos._x;
			_y += pos._y;
			return this;
		}		
		public function addXYSelf(x:Number, y:Number):Vec2
		{
			_x += x;
			_y += y;
			return this;
		}

		/**
		 * Sub
		 */		
		public function subSelf(pos:Vec2Const):Vec2
		{
			_x -= pos._x;
			_y -= pos._y;
			return this;
		}
		public function subXYSelf(x:Number, y:Number):Vec2
		{
			_x -= x;
			_y -= y;
			return this;
		}

		/**
		 * Mul
		 */
		public function mulSelf(vec:Vec2Const):Vec2
		{
			_x *= vec._x;
			_y *= vec._y;
			return this;
		}
		public function mulXYSelf(x:Number, y:Number):Vec2
		{
			_x *= x;
			_y *= y;
			return this;
		}

		/**
		 * Div
		 */
		public function divSelf(vec:Vec2Const):Vec2
		{
			_x /= vec._x;
			_y /= vec._y;
			return this;
		}
		public function divXYSelf(x:Number, y:Number):Vec2
		{
			_x /= x;
			_y /= y;
			return this;
		}

		/**
		 * Scale
		 */		
		public function scaleSelf(s:Number):Vec2
		{
			_x *= s;
			_y *= s;
			return this;
		}

		public function rescaleSelf(newLength:Number):Vec2
		{
			const nf:Number = newLength / Math.sqrt(_x * _x + _y * _y);
			_x *= nf;
			_y *= nf;
			return this;
		}
		
		/**
		 * Normalize
		 */
		public function normalizeSelf():Vec2
		{
			const nf:Number = 1 / Math.sqrt(_x * _x + _y * _y);
			_x *= nf;
			_y *= nf;
			return this;
		}

		/**
		 * Rotate
		 */
		public function rotateSelf(rads:Number):Vec2
		{
			const s:Number = Math.sin(rads);
			const c:Number = Math.cos(rads);
			const xr:Number = _x * c - _y * s;
			_y = _x * s + _y * c;
			_x = xr;
			return this;
		}
		public function normalRightSelf():Vec2
		{
			const xr:Number = _x;
			_x = -_y
			_y = xr;
			return this;
		}
		public function normalLeftSelf():Vec2
		{
			const xr:Number = _x;
			_x = _y
			_y = -xr;
			return this;
		}
		public function negateSelf():Vec2
		{
			_x = -_x;
			_y = -_y;
			return this;
		}
		
		/**
		 * Spinor
		 */
		public function rotateSpinorSelf(vec:Vec2Const):Vec2
		{
			const xr:Number = _x * vec._x - _y * vec._y;
			_y = _x * vec._y + _y * vec._x;
			_x = xr;
			return this;
		}
		
		/**
		 * lerp
		 */
		public function lerpSelf(to:Vec2Const, t:Number):Vec2
		{
			_x = _x + t * (to._x - _x);
			_y = _y + t * (to._y - _y);
			return this;
		}

		/**
		 * Helpers
		 */
		public static function swap(a:Vec2, b:Vec2):void
		{
			const x:Number = a._x;
			const y:Number = a._y;
			a._x = b._x;
			a._y = b._y;
			b._x = x;
			b._y = y;
		}
	}
}

Vec2Test.as

Finally some unit test so you can see how it's used.

		private function _testBasic():void
		{
			var v1:Vec2 = new Vec2;
			Debug.assert(v1.isZero());
			
			// add sub mul div
			v1.addSelf(new Vec2(1, 2)).subSelf(new Vec2(3, 4)).mulSelf(new Vec2(2, -3)).divSelf(new Vec2(2, 3));
			Debug.assert(v1.isNearXY( -2, 2));

			v1 = new Vec2().addXY(1, 2).subXY(3, 4).mulXY(2, -3).divXY(2, 3);
			Debug.assert(v1.isNearXY( -2, 2));

			v1 = new Vec2().add(new Vec2(1, 2)).sub(new Vec2(3, 4)).mul(new Vec2(2, -3)).div(new Vec2(2, 3));
			Debug.assert(v1.isNearXY( -2, 2));
			
			// scale
			Debug.assert(v1.copyXY(1, 2).scaleSelf(3).equalsXY(3, 6));
			Debug.assert(v1.copyXY(1, 2).scale(3).equalsXY(3, 6));
			
			// normalize
			Debug.assert(v1.copyXY(10, 0).normalizeSelf().equalsXY(1, 0));
			Debug.assert(v1.copyXY(0, 10).normalizeSelf().equalsXY(0, 1));
			Debug.assert(v1.copyXY(1, 1).normalizeSelf().isWithinXY(0.7, 0.7, 0.1));
			Debug.assert(v1.isNormalized());
			Debug.assert(v1.copyXY(1, 1).normalize().isWithinXY(0.7, 0.7, 0.1));
			Debug.assert(!v1.isNormalized());
			
			// rotate
			Debug.assert(v1.copyXY(1, 0).normalLeft().equalsXY(0, -1));
			Debug.assert(v1.copyXY(1, 0).normalRight().equalsXY(0, 1));
			Debug.assert(v1.copyXY(1, 0).normalLeftSelf().equalsXY(0, -1));
			Debug.assert(v1.copyXY(1, 0).normalRightSelf().equalsXY(0, 1));
			Debug.assert(v1.copyXY(-13, 3).negate().equalsXY(13, -3));
			Debug.assert(v1.copyXY( -13, 3).negateSelf().equalsXY(13, -3));
			Debug.assert(v1.copyXY(1, 0).rotate(Math.PI * 0.5).isNearXY(0, 1));
			Debug.assert(v1.copyXY(1, 0).rotateSelf(Math.PI * 0.5).isNearXY(0, 1));
			Debug.assert(Near.isNear(v1.getRads(), Math.PI * 0.5));
			Debug.assert(Near.isNear(v1.getDegrees(), 90));
			
			// swap
			var v2:Vec2 = new Vec2(3, 4);
			Vec2.swap(v1.copyXY(12, 13), v2);
			Debug.assert(v1.equalsXY(3, 4));
			Debug.assert(v2.equalsXY(12, 13));
			
			// distance
			Debug.assert(Near.isNear(v1.copyXY(3, 4).length(), 5));
			Debug.assert(v1.lengthSqr() == 25);
			Debug.assert(v1.distance(new Vec2(3, 4)) == 0);
			Debug.assert(v1.distance(new Vec2(3, 0)) == 4);
			Debug.assert(v1.distanceXY(3, 0) == 4);
			
			// dot
			Debug.assert(v1.copyXY(1, 0).dotXY(1, 0) == 1);
			Debug.assert(v1.copyXY(1, 0).dotXY( -1, 0) == -1);
			Debug.assert(v1.copyXY(1, 0).dotXY(0, 1) == 0);
			Debug.assert(v1.copyXY(1, 1).normalize().dot(v1.normalLeft()) == 0);

			// cross
			Debug.assert(v1.copyXY(1, 0).crossDetXY(1, 0) == 0)
			Debug.assert(v1.copyXY(1, 0).crossDetXY(0, -1) == -1)
			Debug.assert(v1.copyXY(1, 0).crossDetXY(0, 1) == 1)
			Debug.assert(v1.copyXY(1, 0).crossDet(new Vec2(1, 0)) == 0)
			Debug.assert(v1.copyXY(1, 0).crossDet(new Vec2(0, -1)) == -1)
			Debug.assert(v1.copyXY(1, 0).crossDet(new Vec2(0, 1)) == 1)
			
			// lerp
			Debug.assert(v1.copyXY(1, 0).lerp(new Vec2(0, -1), 0.5).isWithinXY(0.5, -0.5, 0.01));
			Debug.assert(v1.copyXY(1, 0).lerp(new Vec2(-1, 0), 0.5).isWithinXY(0, 0, 0.01));
			Debug.assert(v1.copyXY(1, 0).lerpSelf(new Vec2(0, -1), 0.5).isWithinXY(0.5, -0.5, 0.01));
			Debug.assert(v1.copyXY(1, 0).lerpSelf(new Vec2(-1, 0), 0.5).isWithinXY(0, 0, 0.01));

			// slerp (need more testing)
			Debug.assert(v1.copyXY(1, 0).slerp(new Vec2(0, -1), 0.5).isWithinXY(0.7, -0.7, 0.1));
		}

12 Comments

    Hey mate.

    Thanks very much for the vector class :)

    • No problem, let me know if you have any questions about it.

    Hi! I was looking on the internet for a long time when I stumbled upon this website. I am working on making a vector class from the http://www.tonypa.pri.ee/vectors/tut06.html because I think the code is not that good. The website shows some balls moving and bouncing of vector line, like walls. Could you point me in the direction of how to do that with our class. I am stuck with the tutorials and your class seems to have that and more!

    Best regards,

    Vince

    • Sorry I don’t have any good tutorials on that stuff, I would just start playing with it, often you come along way with Vec2 newPos=oldPos.add(delta); Good luck!

    Hey, nice package you have here! ;)

    Seriously, Point is disappoint for multiple reasons.
    Aaand I really like the way you thought it out!

    You’ve made the whole thing chainable (NICE copy().mul().normalize()), able to act on the same instance (…Self() methods), and exposing a bunch of useful functions (rads vs degs), along with keeping a Vec2Const class?! Nice job dude!

    Don’t know if you’re still improving this, maybe you’ve added to it and throwed in some funky stuff as quaternions or some other useful features? I could use those too, so let us know.

    Mind if I refactor it a bit and add some other methods (with credits pointing at you in the source code obviously)?

    • Thanks! I’ve updated the code above with the latest version, not sure what has changed though.

      Sure, you can do whatever you want with the code, I am happy you like it! Also let me know if you find any bugs.

      • Hey, I’ve added a handful of methods and fully documented the classes.
        You can find them here: https://github.com/azrafe7/vec2

        • It looks really good, well done. Ever since I created the Vec2Const class I have been wondering if it would have been better to just name it Vec2C. I can’t seem to decide whats nicer, what do you think?

          • Nah… I think I’m fine with Vec2Const (less cryptic than Vec2C).

            Also: added clamping and refactored things a bit.

    Mmmhh… here’s a little problem: I have a Vec2 called A, and I wish to calculate an orthogonal offset from it. I know I can easily do it by calculating normal vectors (left and right) and add them to A, but isn’t there an easier way to do it with spinors? Or am I misunderstanding what they should be used for?

    PS: I’m planning to implement some object-pooling into the class to contain memory usage, refactor it a bit to make it more similar to the AS3 Point class, and take inspiration from Nape’s Vec2 class for further additions/changes. I’ll probably upload it up on github as soon as I’ve some relevant code.

    Cheers,
    azrafe7

  • Thanks for a nice class! it inspired me to write my own take on vector 2d class:
    https://github.com/MindScriptAct/as3-utils/blob/master/src/mindscriptact/utils/geom/Vect2d.as

    I simplified it, dropped self functions. I will use static functions instead, or just clone original vector.

    I also dropped static x and y, to make it perform better(getting and setting it is about 4 times faster this way).
    But I agree with you wanting to have static properties, I use different approach – I use mvcExpress live testing feature to ensure that const vectors are never changed.
    My approach does not support constant vector.

    Thanks for your work again.

    PS : where mul() function is used? I know only scalar multiplication use in practice..

  • Hey, I found an error right after getRadsBetween in Vec2Const at line 113. There’s an extra close curly brace ( } ) that’s messing things up.

Leave a Reply