/**
* A 2D Particle class for all your physics needs
* @module BB.particle2D
*/
define(['./BB', './BB.Vector2'],
function( BB, Vector2){
'use strict';
BB.Vector2 = Vector2;
/**
* A 2D Particle class for all your physics needs
* @class BB.Particle2D
* @constructor
* @param {Object} [config] An optional config object to initialize
* Particle2D properties, including: position ( object with x and y ), mass
* ( defaults to 1 ), radius ( defaults to 0 ) and friction ( defaults to 1
* ).
*
* an initial velocity or acceleration can also be set by passing a
* BB.Vector2 to either of those properties ( ie. velocity or acceleration
* ). Or an alternative approach is to initialize with a heading property
* (radians) and speed property ( number ). If no velocity or acceleration
* or heading/speed is set, the default velocity is BB.Vector2(0,0).
*
* @example <code class="code prettyprint"> var WIDTH = window.innerWidth;<br>
* var HEIGHT = window.innerHeight;<br><br>
* var star = newBB.Particle2D({ <br>
* position: new BB.Vector2(WIDTH/2, HEIGHT/2 ),<br>
* mass: 20000 <br>
* }); <br><br>
* var planet = new BB.Particle2D({ <br>
* position: new BB.Vector2( WIDTH/2+200, HEIGHT/2),<br>
* heading: -Math.PI / 2, <br>
* speed: 10 <br>
* }); <br><br>
* var comet = new BB.Particle2D({<br>
* position: new BB.Vector2( <br>
* BB.MathUtils.randomInt(WIDTH), <br>
* BB.MathUtils.randomInt(HEIGHT) ), <br>
* velocity: new BB.Vector2( <br>
* BB.MathUtils.randomInt(10),<br>
* BB.MathUtils.randomInt(10)) <br>
* });
* </code>
*/
BB.Particle2D = function(config) {
// position -------------------------------------------------
var x = (config && typeof config.x === 'number') ? config.x : 0;
var y = (config && typeof config.y === 'number') ? config.y : 0;
this.position = (config && typeof config.position === 'object' && config.position instanceof BB.Vector2)
? config.position : new BB.Vector2(x, y);
/**
* the particle's velocity ( see acceleration also )
* @property velocity
* @type BB.Vector2
*/
if( typeof config.velocity !== "undefined" && typeof config.heading !== 'undefined' ||
typeof config.velocity !== "undefined" && typeof config.speed !== 'undefined' ){
throw new Error("BB.Particle2D: either use heading/speed or velocity (can't initialize with both)");
}
else if (typeof config.velocity !== 'undefined' && config.velocity instanceof BB.Vector2) {
this.velocity = config.velocity; // set velocity as per config vector
}
else if(typeof config.velocity !== 'undefined' && !(config.velocity instanceof BB.Vector2) ) {
throw new Error("BB.Particle2D: velocity must be an instance of BB.Vector2");
}
else if(typeof config.speed !== 'undefined' || typeof config.heading !== 'undefined'){
if(typeof config.speed !== 'undefined' && typeof config.speed !== 'number' ){
throw new Error("BB.Particle2D: speed must be a number");
}
else if(typeof config.heading !== 'undefined' && typeof config.heading !== 'number' ){
throw new Error("BB.Particle2D: heading must be a number in radians");
}
else if(typeof config.heading !== 'undefined' && typeof config.speed === 'undefined'){
throw new Error("BB.Particle2D: when setting a heading, a speed parameter is also required");
}
else if(typeof config.speed !== 'undefined' && typeof config.heading === 'undefined'){
throw new Error("BB.Particle2D: when setting a speed, a heading parameter is also required");
}
else {
// we've got both heading + speed, && their both numbers,
// so create velocity vector based on heading/speed
this.velocity = new BB.Vector2(0, 0);
this.velocity.x = Math.cos(config.heading) * config.speed;
this.velocity.y = Math.sin(config.heading) * config.speed;
}
}
else {
this.velocity = new BB.Vector2(0, 0); // default velocity vector
}
/**
* Usually used to accumulate forces to be added to velocity each frame
* @property acceleration
* @type BB.Vector2
*/
if( typeof config.acceleration !== "undefined" && typeof config.velocity !== "undefined" ||
typeof config.acceleration !== "undefined" && typeof config.heading !== "undefined" ||
typeof config.acceleration !== "undefined" && typeof config.speed !== "undefined"){
throw new Error("BB.Particle2D: acceleration shouldn't be initialized along with velocity or heading/speed, use one or the other");
} else {
this.acceleration = (config && typeof config.acceleration === 'object' && config.acceleration instanceof BB.Vector2)
? config.acceleration : new BB.Vector2(0, 0);
}
/**
* the particle's mass
* @property mass
* @type Number
* @default 1
*/
this.mass = (config && typeof config.mass === 'number') ? config.mass : 1;
/**
* the particle's radius, used for callculating collistions
* @property radius
* @type Number
* @default 0
*/
this.radius = (config && typeof config.radius === 'number') ? config.radius : 0;
/**
* the particle's friction ( not environment's friction ) multiplied by velocity each frame
* @property friction
* @type Number
* @default 1
*/
this.friction = (config && typeof config.friction === 'number') ? config.friction : 1;
/**
* how bouncy it is when it collides with an object
* @property elasticity
* @type Number
* @default 0.05
*/
this.elasticity = (config && typeof config.elasticity === 'number') ? config.elasticity : 0.05;
this.maxSpeed = (config && typeof config.maxSpeed === 'number') ? config.maxSpeed : 100;
this._springs = [];
this._colliders = []; // array of: other Particles ( x,y,r ) to collide against
this._world = {}; // object w/: left, right, top, bottom properties, "walls", ie. perimeter for colliding
this._gravitations = []; // array of: Vectors or Object{ position:..., mass:... }
};
/**
* the particle's "heading" expressed in radians, essentially: Math.atan2( velocity.y, velocity.x );
* @property heading
* @type Number
*/
Object.defineProperty(BB.Particle2D.prototype, 'heading', {
get: function() {
return Math.atan2(this.velocity.y, this.velocity.x);
},
set: function(heading) {
this.velocity.x = Math.cos(heading) * this.speed;
this.velocity.y = Math.sin(heading) * this.speed;
}
});
/**
* the particle's "speed", essentially: the square root of velocity.x² + velocity.y²
* @property speed
* @type Number
*/
Object.defineProperty(BB.Particle2D.prototype, 'speed', {
get: function() {
return Math.sqrt(this.velocity.x * this.velocity.x + this.velocity.y * this.velocity.y);
},
set: function(speed) {
this.velocity.x = Math.cos(this.heading) * speed;
this.velocity.y = Math.sin(this.heading) * speed;
}
});
/**
* identifies something to gravitate towards. the object of gravitation needs to
* have a position ( x, y ) and mass
*
* @method gravitate
*
* @param {Object} particle if passed as the only argument it should be an
* Object with a position.x, position.y and mass ( ie. an instance of
* BB.Particle2D ). Otherwise the first argument needs to be an Object with
* an x and y ( ie. instance of BB.Vector2 or at the very least { x: ..., y:
* ... } )
*
* alternatively, gravitate could also be passed an <b>array</b> of objects
* ( each with position and mass properties )
*
* @param {Number} [mass] when particle is not an instance of BB.Particle2D
* and is a Vector an additional argument for mass is required
*
* @example
* <code class="code prettyprint">
* // assuming star and planet are both instances of BB.Particle2D <br>
* planet.gravitate( star ); <br>
* // or <br>
* planet.gravitate( star.position, star.mass ); <br>
* // or <br>
* planet.gravitate( { x:WIDTH/2, y:HEIGHT/2 }, 20000 ); <br><br>
* // assuming stars is an array of BB.particle2D <br>
* planet.gravitate( stars );<br>
* </code>
*/
BB.Particle2D.prototype.gravitate = function( particle, mass ) {
var part;
// if array --------------------------------------------------------------------
if( particle instanceof Array ){
for (var i = 0; i < particle.length; i++) {
var p = particle[i];
if( typeof p === "undefined"){
throw new Error('BB.Particle2D: gravitate array is empty');
}
else if( p instanceof BB.Particle2D ){
this._gravitations.push({ position:p.position, mass:p.mass });
}
else if( p instanceof BB.Vector2 && typeof mass === "number" ){
part = { position:p };
this._gravitations.push({ position:part.position, mass:mass });
}
else if( p instanceof BB.Vector2 && typeof mass !== "number" ){
throw new Error('BB.Particle2D: gravitate array objects are missing a mass');
}
else if( !(p instanceof BB.Vector2) ){
if( typeof p.x === "undefined" || typeof p.y === "undefined" ){
throw new Error('BB.Particle2D: gravitate array items should be objects with x and y properties');
}
else if( typeof mass == "undefined"){
throw new Error('BB.Particle2D: gravitate array objects are missing a mass' );
}
else {
part = { position:{x:p.x, y:p.y } };
this._gravitations.push({ position:part.position, mass:mass });
}
}
}
}
// if single particle -----------------------------------------------------------
else {
if( typeof particle === "undefined"){
throw new Error('BB.Particle2D: gravitate is missing arguments');
}
else if( particle instanceof BB.Particle2D ){
this._gravitations.push({ position:particle.position, mass:particle.mass });
}
else if( particle instanceof BB.Vector2 && typeof mass === "number" ){
part = { position:particle };
this._gravitations.push({ position:part.position, mass:mass });
}
else if( particle instanceof BB.Vector2 && typeof mass !== "number" ){
throw new Error('BB.Particle2D: gravitate\'s second argument requires a number ( mass )');
}
else if( !(particle instanceof BB.Vector2) ){
if( typeof particle.x === "undefined" || typeof particle.y === "undefined" ){
throw new Error('BB.Particle2D: gravitate argument should be an object with an x and y property');
}
else if( typeof mass == "undefined"){
throw new Error('BB.Particle2D: gravitate\'s second argument requires a number ( mass )' );
}
else {
part = { position:{x:particle.x, y:particle.y } };
this._gravitations.push({ position:part.position, mass:mass });
}
}
}
};
/**
* identifies something to spring towards. the target needs to have an x,y
* position, a k value which is a constant factor characteristic of the spring
* ( ie. its stiffness, usually some decimal ), and a length.
*
* @method spring
*
* @param {Object} config object with properties for point ( vector with x,y ),
* k ( number ) and length ( number ).
*
* alternatively, spring could also be passed an <b>array</b> of config objects
*
* @example
* <code class="code prettyprint">
* // assuming ball is an instance of BB.Particle2D <br>
* // and center is an object with x,y positions <br>
* ball.spring({ <br>
* position: center.position,<br>
* k: 0.1,<br>
* length: 100<br>
* });<br>
* <br>
* // the ball will spring back and forth forever from the center position <br>
* // unless ball has friction value below the default of 1.0
* </code>
*/
BB.Particle2D.prototype.spring = function( config ) {
// if array --------------------------------------------------------------------
if( config instanceof Array ){
for (var i = 0; i < config.length; i++) {
var p = config[i];
if( typeof p === "undefined"){
throw new Error('BB.Particle2D: spring array is empty, expecting config objects');
}
else if( typeof p !== "object" || p.position === "undefined" ||
typeof p.k === "undefined" || typeof p.length === "undefined"){
throw new Error('BB.Particle2D: spring array expecting config objects, with properies for position, length and k');
}
else if( typeof p.position.x !== "number" || typeof p.position.y !== "number" ){
throw new Error('BB.Particle2D: spring array objects\' positions should have x and y properties ( numbers )');
}
else if( typeof p.k !== "number" ){
throw new Error('BB.Particle2D: spring array object\'s k properties should be numbers ( usually a float )');
}
else if( typeof p.length !== "number" ){
throw new Error('BB.Particle2D: spring array object\'s length properties should be numbers ( usually a integers ');
}
else {
this._springs.push({ position:p.position, k:p.k, length:p.length });
}
}
}
// if single target -----------------------------------------------------------
else {
if( typeof config === "undefined"){
throw new Error('BB.Particle2D: spring is missing arguments');
}
else if( typeof config !== "object" || config.position === "undefined" ||
typeof config.k === "undefined" || typeof config.length === "undefined"){
throw new Error('BB.Particle2D: spring expecting a config object, with properies for position, length and k');
}
else if( typeof config.position.x !== "number" || typeof config.position.y !== "number" ){
throw new Error('BB.Particle2D: config.position should have x and y properties ( numbers )');
}
else if( typeof config.k !== "number" ){
throw new Error('BB.Particle2D: config.k property should be a number ( usually a float )');
}
else if( typeof config.length !== "number" ){
throw new Error('BB.Particle2D: config.length property should be a number ( usually an integer )');
}
else {
this._springs.push( { position:config.position, k:config.k, length:config.length } );
}
}
};
/**
* tracks objects to collide against, this can be other particles ( objects with
* position vectors and a radius ) and/or a perimeter ( top, left, right, bottom )
*
* @method collide
*
* @param {Object} config object with properties for top, left, bottom, right ( all numbers ) and particles ( array of other
* particles or objects with position.x, positon.y and radius properties )
*
* @example
* <code class="code prettyprint">
* // assuming ball is an instance of BB.Particle2D <br>
* // assuming balls is an array of BB.Particle2D objects <br>
* ball.collide({ <br>
* top:0, <br>
* right: canvas.width, <br>
* bottom: canvas.height, <br>
* left: 0, <br>
* particles: balls <br>
* });<br>
* </code>
*/
BB.Particle2D.prototype.collide = function( config ) {
if( typeof config === "undefined" ){
throw new Error('BB.Particle2D: collide requires arguments to konw what to collide against');
}
// perimeter -----------------------------------------------
if( typeof config.dampen !== "undefined") this._world.dampen = config.dampen;
if( typeof config.left !== "undefined" ) this._world.left = config.left;
if( typeof config.right !== "undefined" ) this._world.right = config.right;
if( typeof config.top !== "undefined" ) this._world.top = config.top;
if( typeof config.bottom !== "undefined" ) this._world.bottom = config.bottom;
// other particles -----------------------------------------
var i = 0;
if( typeof config.particles !== "undefined" ){ // when sent along w/ above parameters
if( !(config.particles instanceof Array) ){
throw new Error('BB.Particle2D: collide: particles value expecting array of particles');
}
else {
for (i = 0; i < config.particles.length; i++) {
// if( !( config.particles[i] instanceof BB.Particle2D ) ){
if( typeof config.particles[i].position.x === "undefined" ) {
throw new Error('BB.Particle2D: collide: particles['+i+'] is missing a position.x');
}
if( typeof config.particles[i].position.y === "undefined" ) {
throw new Error('BB.Particle2D: collide: particles['+i+'] is missing a position.y');
}
if( typeof config.particles[i].radius === "undefined" ) {
throw new Error('BB.Particle2D: collide: particles['+i+'] is missing a radius');
}
this._colliders = config.particles;
}
}
}
};
/**
* Update the particle's internals and apply acceleration to veloicty.
* Called once per animation frame.
* @method update
*/
BB.Particle2D.prototype.update = function() {
var i = 0;
var accVector = new BB.Vector2();
var dx, dy, ax, ay, tx, ty,
dist, distSQ, distMin,
force, angle;
// apply gravitations ----------------------------------------
for (i = 0; i < this._gravitations.length; i++) {
var g = this._gravitations[i];
dx = g.position.x - this.position.x;
dy = g.position.y - this.position.y;
distSQ = dx * dx + dy * dy;
dist = Math.sqrt(distSQ);
force = g.mass / distSQ;
ax = dx / dist * force;
ay = dy / dist * force;
accVector.set( ax, ay );
this.applyForce( accVector );
// this.acceleration.add( new BB.Vector2(ax,ay) );
}
// apply springs ----------------------------------------
for (i = 0; i < this._springs.length; i++) {
var s = this._springs[i];
dx = s.position.x - this.position.x;
dy = s.position.y - this.position.y;
dist = Math.sqrt(dx * dx + dy * dy);
force = (dist - s.length || 0) * s.k;
ax = dx / dist * force;
ay = dy / dist * force;
accVector.set( ax, ay );
this.applyForce( accVector );
}
// apply collisions ----------------------------------------
for (i = 0; i < this._colliders.length; i++) {
var c = this._colliders[i];
if( c !== this ){
dx = c.position.x - this.position.x;
dy = c.position.y - this.position.y;
dist = Math.sqrt(dx*dx + dy*dy);
distMin = c.radius + this.radius;
if (dist < distMin) {
angle = Math.atan2(dy, dx);
tx = this.position.x + Math.cos(angle) * distMin;
ty = this.position.y + Math.sin(angle) * distMin;
ax = (tx - c.position.x) * this.elasticity;
ay = (ty - c.position.y) * this.elasticity;
accVector.set( -ax, -ay);
this.applyForce( accVector );
}
}
}
if( typeof this._world.left !== "undefined" ){
if( (this.position.x - this.radius) < this._world.left ){
this.position.x = this._world.left + this.radius;
this.velocity.x = -this.velocity.x;
this.velocity.x *= this._world.dampen || 0.7;
}
}
if( typeof this._world.right !== "undefined" ){
if( (this.position.x + this.radius) > this._world.right ){
this.position.x = this._world.right - this.radius;
this.velocity.x = -this.velocity.x;
this.velocity.x *= this._world.dampen || 0.7;
}
}
if( typeof this._world.top !== "undefined" ){
if( (this.position.y - this.radius) < this._world.top ) {
this.position.y = this._world.top + this.radius;
this.velocity.y = -this.velocity.y;
this.velocity.y *= this._world.dampen || 0.7;
}
}
if( typeof this._world.bottom !== "undefined" ){
if( (this.position.y + this.radius) > this._world.bottom ) {
this.position.y = this._world.bottom - this.radius;
this.velocity.y = -this.velocity.y;
this.velocity.y *= this._world.dampen || 0.7;
}
}
// this.acceleration.multiplyScalar(this.friction); // NOT WORKING?
this.velocity.multiplyScalar(this.friction); // APPLYING DIRECTLY TO VELOCITY INSTEAD
this.velocity.add(this.acceleration);
if (this.velocity.length() > this.maxSpeed) {
this.velocity.setLength(this.maxSpeed);
}
this.position.add(this.velocity);
this.acceleration.multiplyScalar(0);
this._gravitations = [];
this._springs = [];
this._colliders = [];
};
/**
* takes a force, divides it by particle's mass, and applies it to acceleration ( which is added to velocity each frame )
*
* @method applyForce
*
* @param {BB.Vector2} vector force to be applied
*/
BB.Particle2D.prototype.applyForce = function(force) {
if (typeof force !== 'object' || ! (force instanceof BB.Vector2)) {
throw new Error('BB.Particle2D.applyForce: force parameter must be present and an instance of BB.Vector2');
}
this.acceleration.add( force.clone().divideScalar(this.mass) );
};
return BB.Particle2D;
});