You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

4222 lines
124 KiB
JavaScript

/*
p5.play
by Paolo Pedercini/molleindustria, 2015
http://molleindustria.org/
*/
(function (root, factory) {
if (typeof define === 'function' && define.amd)
define('p5.play', ['p5'], function (p5) { (factory(p5)); });
else if (typeof exports === 'object')
factory(require('../p5'));
else
factory(root.p5);
}(this, function (p5) {
/**
* p5.play is a library for p5.js to facilitate the creation of games and gamelike
* projects.
*
* It provides a flexible Sprite class to manage visual objects in 2D space
* and features such as animation support, basic collision detection
* and resolution, mouse and keyboard interactions, and a virtual camera.
*
* p5.play is not a box2D-derived physics engine, it doesn't use events, and it's
* designed to be understood and possibly modified by intermediate programmers.
*
* See the examples folder for more info on how to use this library.
*
* @module p5.play
* @submodule p5.play
* @for p5.play
* @main
*/
// =============================================================================
// initialization
// =============================================================================
// This is the new way to initialize custom p5 properties for any p5 instance.
// The goal is to migrate lazy P5 properties over to this method.
// @see https://github.com/molleindustria/p5.play/issues/46
p5.prototype.registerMethod('init', function p5PlayInit() {
/**
* The sketch camera automatically created at the beginning of a sketch.
* A camera facilitates scrolling and zooming for scenes extending beyond
* the canvas. A camera has a position, a zoom factor, and the mouse
* coordinates relative to the view.
*
* In p5.js terms the camera wraps the whole drawing cycle in a
* transformation matrix but it can be disabled anytime during the draw
* cycle, for example to draw interface elements in an absolute position.
*
* @property camera
* @type {camera}
*/
this.camera = new Camera(this, 0, 0, 1);
this.camera.init = false;
});
// This provides a way for us to lazily define properties that
// are global to p5 instances.
//
// Note that this isn't just an optimization: p5 currently provides no
// way for add-ons to be notified when new p5 instances are created, so
// lazily creating these properties is the *only* mechanism available
// to us. For more information, see:
//
// https://github.com/processing/p5.js/issues/1263
function defineLazyP5Property(name, getter) {
Object.defineProperty(p5.prototype, name, {
configurable: true,
enumerable: true,
get: function () {
var context = (this instanceof p5 && !this._isGlobal) ? this : window;
if (typeof (context._p5PlayProperties) === 'undefined') {
context._p5PlayProperties = {};
}
if (!(name in context._p5PlayProperties)) {
context._p5PlayProperties[name] = getter.call(context);
}
return context._p5PlayProperties[name];
}
});
}
// This returns a factory function, suitable for passing to
// defineLazyP5Property, that returns a subclass of the given
// constructor that is always bound to a particular p5 instance.
function boundConstructorFactory(constructor) {
if (typeof (constructor) !== 'function')
throw new Error('constructor must be a function');
return function createBoundConstructor() {
var pInst = this;
function F() {
var args = Array.prototype.slice.call(arguments);
return constructor.apply(this, [pInst].concat(args));
}
F.prototype = constructor.prototype;
return F;
};
}
// This is a utility that makes it easy to define convenient aliases to
// pre-bound p5 instance methods.
//
// For example:
//
// var pInstBind = createPInstBinder(pInst);
//
// var createVector = pInstBind('createVector');
// var loadImage = pInstBind('loadImage');
//
// The above will create functions createVector and loadImage, which can be
// used similar to p5 global mode--however, they're bound to specific p5
// instances, and can thus be used outside of global mode.
function createPInstBinder(pInst) {
return function pInstBind(methodName) {
var method = pInst[methodName];
if (typeof (method) !== 'function')
throw new Error('"' + methodName + '" is not a p5 method');
return method.bind(pInst);
};
}
// These are utility p5 functions that don't depend on p5 instance state in
// order to work properly, so we'll go ahead and make them easy to
// access without needing to bind them to a p5 instance.
var abs = p5.prototype.abs;
var radians = p5.prototype.radians;
var dist = p5.prototype.dist;
var degrees = p5.prototype.degrees;
var pow = p5.prototype.pow;
var round = p5.prototype.round;
// =============================================================================
// p5 additions
// =============================================================================
/**
* A Group containing all the sprites in the sketch.
*
* @property allSprites
* @type {Group}
*/
defineLazyP5Property('allSprites', function () {
return new p5.prototype.Group();
});
p5.prototype.spriteUpdate = true;
/**
* A Sprite is the main building block of p5.play:
* an element able to store images or animations with a set of
* properties such as position and visibility.
* A Sprite can have a collider that defines the active area to detect
* collisions or overlappings with other sprites and mouse interactions.
*
* Sprites created using createSprite (the preferred way) are added to the
* allSprites group and given a depth value that puts it in front of all
* other sprites.
*
* @method createSprite
* @param {Number} x Initial x coordinate
* @param {Number} y Initial y coordinate
* @param {Number} width Width of the placeholder rectangle and of the
* collider until an image or new collider are set
* @param {Number} height Height of the placeholder rectangle and of the
* collider until an image or new collider are set
* @return {Object} The new sprite instance
*/
p5.prototype.createSprite = function (x, y, width, height) {
var s = new Sprite(this, x, y, width, height);
s.depth = this.allSprites.maxDepth() + 1;
this.allSprites.add(s);
return s;
};
/**
* Removes a Sprite from the sketch.
* The removed Sprite won't be drawn or updated anymore.
* Equivalent to Sprite.remove()
*
* @method removeSprite
* @param {Object} sprite Sprite to be removed
*/
p5.prototype.removeSprite = function (sprite) {
sprite.remove();
};
/**
* Updates all the sprites in the sketch (position, animation...)
* it's called automatically at every draw().
* It can be paused by passing a parameter true or false;
* Note: it does not render the sprites.
*
* @method updateSprites
* @param {Boolean} updating false to pause the update, true to resume
*/
p5.prototype.updateSprites = function (upd) {
if (upd === false)
this.spriteUpdate = false;
if (upd === true)
this.spriteUpdate = true;
if (this.spriteUpdate)
for (var i = 0; i < this.allSprites.size(); i++) {
this.allSprites.get(i).update();
}
};
/**
* Returns all the sprites in the sketch as an array
*
* @method getSprites
* @return {Array} Array of Sprites
*/
p5.prototype.getSprites = function () {
//draw everything
if (arguments.length === 0) {
return this.allSprites.toArray();
}
else {
var arr = [];
//for every tag
for (var j = 0; j < arguments.length; j++) {
for (var i = 0; i < this.allSprites.size(); i++) {
if (this.allSprites.get(i).isTagged(arguments[j]))
arr.push(this.allSprites.get(i));
}
}
return arr;
}
};
/**
* Displays a Group of sprites.
* If no parameter is specified, draws all sprites in the
* sketch.
* The drawing order is determined by the Sprite property "depth"
*
* @method drawSprites
* @param {Group} [group] Group of Sprites to be displayed
*/
p5.prototype.drawSprites = function (group) {
// If no group is provided, draw the allSprites group.
group = group || this.allSprites;
if (typeof group.draw !== 'function') {
throw ('Error: with drawSprites you can only draw all sprites or a group');
}
group.draw();
};
/**
* Displays a Sprite.
* To be typically used in the main draw function.
*
* @method drawSprite
* @param {Sprite} sprite Sprite to be displayed
*/
p5.prototype.drawSprite = function (sprite) {
if (sprite)
sprite.display();
};
/**
* Loads an animation.
* To be typically used in the preload() function of the sketch.
*
* @method loadAnimation
* @param {Sprite} sprite Sprite to be displayed
*/
p5.prototype.loadAnimation = function () {
return construct(this.Animation, arguments);
};
/**
* Loads a Sprite Sheet.
* To be typically used in the preload() function of the sketch.
*
* @method loadSpriteSheet
*/
p5.prototype.loadSpriteSheet = function () {
return construct(this.SpriteSheet, arguments);
};
/**
* Displays an animation.
*
* @method animation
* @param {Animation} anim Animation to be displayed
* @param {Number} x X coordinate
* @param {Number} y Y coordinate
*
*/
p5.prototype.animation = function (anim, x, y) {
anim.draw(x, y);
};
//variable to detect instant presses
defineLazyP5Property('_p5play', function () {
return {
keyStates: {},
mouseStates: {}
};
});
var KEY_IS_UP = 0;
var KEY_WENT_DOWN = 1;
var KEY_IS_DOWN = 2;
var KEY_WENT_UP = 3;
/**
* Detects if a key was pressed during the last cycle.
* It can be used to trigger events once, when a key is pressed or released.
* Example: Super Mario jumping.
*
* @method keyWentDown
* @param {Number|String} key Key code or character
* @return {Boolean} True if the key was pressed
*/
p5.prototype.keyWentDown = function (key) {
return this._isKeyInState(key, KEY_WENT_DOWN);
};
/**
* Detects if a key was released during the last cycle.
* It can be used to trigger events once, when a key is pressed or released.
* Example: Spaceship shooting.
*
* @method keyWentUp
* @param {Number|String} key Key code or character
* @return {Boolean} True if the key was released
*/
p5.prototype.keyWentUp = function (key) {
return this._isKeyInState(key, KEY_WENT_UP);
};
/**
* Detects if a key is currently pressed
* Like p5 keyIsDown but accepts strings and codes
*
* @method keyDown
* @param {Number|String} key Key code or character
* @return {Boolean} True if the key is down
*/
p5.prototype.keyDown = function (key) {
return this._isKeyInState(key, KEY_IS_DOWN);
};
/**
* Detects if a key is in the given state during the last cycle.
* Helper method encapsulating common key state logic; it may be preferable
* to call keyDown or other methods directly.
*
* @private
* @method _isKeyInState
* @param {Number|String} key Key code or character
* @param {Number} state Key state to check against
* @return {Boolean} True if the key is in the given state
*/
p5.prototype._isKeyInState = function (key, state) {
var keyCode;
var keyStates = this._p5play.keyStates;
if (typeof key === 'string') {
keyCode = this._keyCodeFromAlias(key);
}
else {
keyCode = key;
}
//if undefined start checking it
if (keyStates[keyCode] === undefined) {
if (this.keyIsDown(keyCode))
keyStates[keyCode] = KEY_IS_DOWN;
else
keyStates[keyCode] = KEY_IS_UP;
}
return (keyStates[keyCode] === state);
};
/**
* Detects if a mouse button is currently down
* Combines mouseIsPressed and mouseButton of p5
*
* @method mouseDown
* @param {Number} [buttonCode] Mouse button constant LEFT, RIGHT or CENTER
* @return {Boolean} True if the button is down
*/
p5.prototype.mouseDown = function (buttonCode) {
return this._isMouseButtonInState(buttonCode, KEY_IS_DOWN);
};
/**
* Detects if a mouse button is currently up
* Combines mouseIsPressed and mouseButton of p5
*
* @method mouseUp
* @param {Number} [buttonCode] Mouse button constant LEFT, RIGHT or CENTER
* @return {Boolean} True if the button is up
*/
p5.prototype.mouseUp = function (buttonCode) {
return this._isMouseButtonInState(buttonCode, KEY_IS_UP);
};
/**
* Detects if a mouse button was released during the last cycle.
* It can be used to trigger events once, to be checked in the draw cycle
*
* @method mouseWentUp
* @param {Number} [buttonCode] Mouse button constant LEFT, RIGHT or CENTER
* @return {Boolean} True if the button was just released
*/
p5.prototype.mouseWentUp = function (buttonCode) {
return this._isMouseButtonInState(buttonCode, KEY_WENT_UP);
};
/**
* Detects if a mouse button was pressed during the last cycle.
* It can be used to trigger events once, to be checked in the draw cycle
*
* @method mouseWentDown
* @param {Number} [buttonCode] Mouse button constant LEFT, RIGHT or CENTER
* @return {Boolean} True if the button was just pressed
*/
p5.prototype.mouseWentDown = function (buttonCode) {
return this._isMouseButtonInState(buttonCode, KEY_WENT_DOWN);
};
/**
* Detects if a mouse button is in the given state during the last cycle.
* Helper method encapsulating common mouse button state logic; it may be
* preferable to call mouseWentUp, etc, directly.
*
* @private
* @method _isMouseButtonInState
* @param {Number} [buttonCode] Mouse button constant LEFT, RIGHT or CENTER
* @param {Number} state
* @return {boolean} True if the button was in the given state
*/
p5.prototype._isMouseButtonInState = function (buttonCode, state) {
var mouseStates = this._p5play.mouseStates;
if (buttonCode === undefined)
buttonCode = this.LEFT;
//undefined = not tracked yet, start tracking
if (mouseStates[buttonCode] === undefined) {
if (this.mouseIsPressed && this.mouseButton === buttonCode)
mouseStates[buttonCode] = KEY_IS_DOWN;
else
mouseStates[buttonCode] = KEY_IS_UP;
}
return (mouseStates[buttonCode] === state);
};
/**
* An object storing all useful keys for easy access
* Key.tab = 9
*
* @private
* @property KEY
* @type {Object}
*/
p5.prototype.KEY = {
'BACKSPACE': 8,
'TAB': 9,
'ENTER': 13,
'SHIFT': 16,
'CTRL': 17,
'ALT': 18,
'PAUSE': 19,
'CAPS_LOCK': 20,
'ESC': 27,
'SPACE': 32,
' ': 32,
'PAGE_UP': 33,
'PAGE_DOWN': 34,
'END': 35,
'HOME': 36,
'LEFT_ARROW': 37,
'LEFT': 37,
'UP_ARROW': 38,
'UP': 38,
'RIGHT_ARROW': 39,
'RIGHT': 39,
'DOWN_ARROW': 40,
'DOWN': 40,
'INSERT': 45,
'DELETE': 46,
'0': 48,
'1': 49,
'2': 50,
'3': 51,
'4': 52,
'5': 53,
'6': 54,
'7': 55,
'8': 56,
'9': 57,
'A': 65,
'B': 66,
'C': 67,
'D': 68,
'E': 69,
'F': 70,
'G': 71,
'H': 72,
'I': 73,
'J': 74,
'K': 75,
'L': 76,
'M': 77,
'N': 78,
'O': 79,
'P': 80,
'Q': 81,
'R': 82,
'S': 83,
'T': 84,
'U': 85,
'V': 86,
'W': 87,
'X': 88,
'Y': 89,
'Z': 90,
'0NUMPAD': 96,
'1NUMPAD': 97,
'2NUMPAD': 98,
'3NUMPAD': 99,
'4NUMPAD': 100,
'5NUMPAD': 101,
'6NUMPAD': 102,
'7NUMPAD': 103,
'8NUMPAD': 104,
'9NUMPAD': 105,
'MULTIPLY': 106,
'PLUS': 107,
'MINUS': 109,
'DOT': 110,
'SLASH1': 111,
'F1': 112,
'F2': 113,
'F3': 114,
'F4': 115,
'F5': 116,
'F6': 117,
'F7': 118,
'F8': 119,
'F9': 120,
'F10': 121,
'F11': 122,
'F12': 123,
'EQUAL': 187,
'COMMA': 188,
'SLASH': 191,
'BACKSLASH': 220
};
/**
* An object storing deprecated key aliases, which we still support but
* should be mapped to valid aliases and generate warnings.
*
* @private
* @property KEY_DEPRECATIONS
* @type {Object}
*/
p5.prototype.KEY_DEPRECATIONS = {
'MINUT': 'MINUS',
'COMA': 'COMMA'
};
/**
* Given a string key alias (as defined in the KEY property above), look up
* and return the numeric JavaScript key code for that key. If a deprecated
* alias is passed (as defined in the KEY_DEPRECATIONS property) it will be
* mapped to a valid key code, but will also generate a warning about use
* of the deprecated alias.
*
* @private
* @method _keyCodeFromAlias
* @param {!string} alias - a case-insensitive key alias
* @return {number|undefined} a numeric JavaScript key code, or undefined
* if no key code matching the given alias is found.
*/
p5.prototype._keyCodeFromAlias = function (alias) {
alias = alias.toUpperCase();
if (this.KEY_DEPRECATIONS[alias]) {
this._warn('Key literal "' + alias + '" is deprecated and may be removed ' +
'in a future version of p5.play. ' +
'Please use "' + this.KEY_DEPRECATIONS[alias] + '" instead.');
alias = this.KEY_DEPRECATIONS[alias];
}
return this.KEY[alias];
};
//pre draw: detect keyStates
p5.prototype.readPresses = function () {
var keyStates = this._p5play.keyStates;
var mouseStates = this._p5play.mouseStates;
for (var key in keyStates) {
if (this.keyIsDown(key)) //if is down
{
if (keyStates[key] === KEY_IS_UP)//and was up
keyStates[key] = KEY_WENT_DOWN;
else
keyStates[key] = KEY_IS_DOWN; //now is simply down
}
else //if it's up
{
if (keyStates[key] === KEY_IS_DOWN)//and was up
keyStates[key] = KEY_WENT_UP;
else
keyStates[key] = KEY_IS_UP; //now is simply down
}
}
//mouse
for (var btn in mouseStates) {
if (this.mouseIsPressed && this.mouseButton === btn) //if is down
{
if (mouseStates[btn] === KEY_IS_UP)//and was up
mouseStates[btn] = KEY_WENT_DOWN;
else
mouseStates[btn] = KEY_IS_DOWN; //now is simply down
}
else //if it's up
{
if (mouseStates[btn] === KEY_IS_DOWN)//and was up
mouseStates[btn] = KEY_WENT_UP;
else
mouseStates[btn] = KEY_IS_UP; //now is simply down
}
}
};
/**
* Turns the quadTree on or off.
* A quadtree is a data structure used to optimize collision detection.
* It can improve performance when there is a large number of Sprites to be
* checked continuously for overlapping.
*
* p5.play will create and update a quadtree automatically.
*
* @method useQuadTree
* @param {Boolean} use Pass true to enable, false to disable
*/
p5.prototype.useQuadTree = function (use) {
if (this.quadTree !== undefined) {
if (use === undefined)
return this.quadTree.active;
else if (use)
this.quadTree.active = true;
else
this.quadTree.active = false;
}
else
return false;
};
//the actual quadTree
defineLazyP5Property('quadTree', function () {
return new Quadtree({
x: 0,
y: 0,
width: 0,
height: 0
}, 4);
});
/*
//framerate independent delta, doesn't really work
p5.prototype.deltaTime = 1;
var now = Date.now();
var then = Date.now();
var INTERVAL_60 = 0.0166666; //60 fps
function updateDelta() {
then = now;
now = Date.now();
deltaTime = ((now - then) / 1000)/INTERVAL_60; // seconds since last frame
}
*/
/**
* A Sprite is the main building block of p5.play:
* an element able to store images or animations with a set of
* properties such as position and visibility.
* A Sprite can have a collider that defines the active area to detect
* collisions or overlappings with other sprites and mouse interactions.
*
* To create a Sprite, use
* {{#crossLink "p5.play/createSprite:method"}}{{/crossLink}}.
*
* @class Sprite
*/
// For details on why these docs aren't in a YUIDoc comment block, see:
//
// https://github.com/molleindustria/p5.play/pull/67
//
// @param {Number} x Initial x coordinate
// @param {Number} y Initial y coordinate
// @param {Number} width Width of the placeholder rectangle and of the
// collider until an image or new collider are set
// @param {Number} height Height of the placeholder rectangle and of the
// collider until an image or new collider are set
function Sprite(pInst, _x, _y, _w, _h) {
var pInstBind = createPInstBinder(pInst);
var createVector = pInstBind('createVector');
var color = pInstBind('color');
var random = pInstBind('random');
var print = pInstBind('print');
var push = pInstBind('push');
var pop = pInstBind('pop');
var colorMode = pInstBind('colorMode');
var noStroke = pInstBind('noStroke');
var rectMode = pInstBind('rectMode');
var ellipseMode = pInstBind('ellipseMode');
var imageMode = pInstBind('imageMode');
var translate = pInstBind('translate');
var scale = pInstBind('scale');
var rotate = pInstBind('rotate');
var stroke = pInstBind('stroke');
var strokeWeight = pInstBind('strokeWeight');
var line = pInstBind('line');
var noFill = pInstBind('noFill');
var fill = pInstBind('fill');
var textAlign = pInstBind('textAlign');
var textSize = pInstBind('textSize');
var text = pInstBind('text');
var rect = pInstBind('rect');
var cos = pInstBind('cos');
var sin = pInstBind('sin');
var atan2 = pInstBind('atan2');
var quadTree = pInst.quadTree;
var camera = pInst.camera;
// These are p5 constants that we'd like easy access to.
var RGB = p5.prototype.RGB;
var CENTER = p5.prototype.CENTER;
var LEFT = p5.prototype.LEFT;
var BOTTOM = p5.prototype.BOTTOM;
/**
* The sprite's position of the sprite as a vector (x,y).
* @property position
* @type {p5.Vector}
*/
this.position = createVector(_x, _y);
/**
* The sprite's position at the beginning of the last update as a vector (x,y).
* @property previousPosition
* @type {p5.Vector}
*/
this.previousPosition = createVector(_x, _y);
/*
The sprite's position at the end of the last update as a vector (x,y).
Note: this will differ from position whenever the position is changed
directly by assignment.
*/
this.newPosition = createVector(_x, _y);
//Position displacement on the x coordinate since the last update
this.deltaX = 0;
this.deltaY = 0;
/**
* The sprite's velocity as a vector (x,y)
* Velocity is speed broken down to its vertical and horizontal components.
*
* @property velocity
* @type {p5.Vector}
*/
this.velocity = createVector(0, 0);
/**
* Set a limit to the sprite's scalar speed regardless of the direction.
* The value can only be positive. If set to -1, there's no limit.
*
* @property maxSpeed
* @type {Number}
* @default -1
*/
this.maxSpeed = -1;
/**
* Friction factor, reduces the sprite's velocity.
* The friction should be close to 0 (eg. 0.01)
* 0: no friction
* 1: full friction
*
* @property friction
* @type {Number}
* @default 0
*/
this.friction = 0;
/**
* The sprite's current collider.
* It can either be an Axis Aligned Bounding Box (a non-rotated rectangle)
* or a circular collider.
* If the sprite is checked for collision, bounce, overlapping or mouse events the
* collider is automatically created from the width and height
* of the sprite or from the image dimension in case of animate sprites
*
* You can set a custom collider with Sprite.setCollider
*
* @property collider
* @type {Object}
*/
this.collider = undefined;
//internal use
//"default" - no image or custom collider is specified, use the shape width / height
//"custom" - specified with setCollider
//"image" - no collider is set with setCollider and an image is added
this.colliderType = 'none';
/**
* Object containing information about the most recent collision/overlapping
* To be typically used in combination with Sprite.overlap or Sprite.collide
* functions.
* The properties are touching.left, touching.right, touching.top,
* touching.bottom and are either true or false depending on the side of the
* collider.
*
* @property touching
* @type {Object}
*/
this.touching = {};
this.touching.left = false;
this.touching.right = false;
this.touching.top = false;
this.touching.bottom = false;
/**
* The mass determines the velocity transfer when sprites bounce
* against each other. See Sprite.bounce
* The higher the mass the least the sprite will be affected by collisions.
*
* @property mass
* @type {Number}
* @default 1
*/
this.mass = 1;
/**
* If set to true the sprite won't bounce or be displaced by collisions
* Simulates an infinite mass or an anchored object.
*
* @property immovable
* @type {Boolean}
* @default false
*/
this.immovable = false;
//Coefficient of restitution - velocity lost in the bouncing
//0 perfectly inelastic , 1 elastic, > 1 hyper elastic
/**
* Coefficient of restitution. The velocity lost after bouncing.
* 1: perfectly elastic, no energy is lost
* 0: perfectly inelastic, no bouncing
* less than 1: inelastic, this is the most common in nature
* greater than 1: hyper elastic, energy is increased like in a pinball bumper
*
* @property restitution
* @type {Number}
* @default 1
*/
this.restitution = 1;
/**
* Rotation in degrees of the visual element (image or animation)
* Note: this is not the movement's direction, see getDirection.
*
* @property rotation
* @type {Number}
* @default 0
*/
Object.defineProperty(this, 'rotation', {
enumerable: true,
get: function () {
return this._rotation;
},
set: function (value) {
this._rotation = value;
if (this.rotateToDirection) {
this.setSpeed(this.getSpeed(), value);
}
}
});
/**
* Internal rotation variable (expressed in degrees).
* Note: external callers access this through the rotation property above.
*
* @private
* @property _rotation
* @type {Number}
* @default 0
*/
this._rotation = 0;
/**
* Rotation change in degrees per frame of thevisual element (image or animation)
* Note: this is not the movement's direction, see getDirection.
*
* @property rotationSpeed
* @type {Number}
* @default 0
*/
this.rotationSpeed = 0;
/**
* Automatically lock the rotation property of the visual element
* (image or animation) to the sprite's movement direction and vice versa.
*
* @property rotateToDirection
* @type {Boolean}
* @default false
*/
this.rotateToDirection = false;
/**
* Determines the rendering order within a group: a sprite with
* lower depth will appear below the ones with higher depth.
*
* Note: drawing a group before another with drawSprites will make
* its members appear below the second one, like in normal p5 canvas
* drawing.
*
* @property depth
* @type {Number}
* @default One more than the greatest existing sprite depth, when calling
* createSprite(). When calling new Sprite() directly, depth will
* initialize to 0 (not recommended).
*/
this.depth = 0;
/**
* Determines the sprite's scale.
* Example: 2 will be twice the native size of the visuals,
* 0.5 will be half. Scaling up may make images blurry.
*
* @property scale
* @type {Number}
* @default 1
*/
this.scale = 1;
var dirX = 1;
var dirY = 1;
/**
* The sprite's visibility.
*
* @property visible
* @type {Boolean}
* @default true
*/
this.visible = true;
/**
* If set to true sprite will track its mouse state.
* the properties mouseIsPressed and mouseIsOver will be updated.
* Note: automatically set to true if the functions
* onMouseReleased or onMousePressed are set.
*
* @property mouseActive
* @type {Boolean}
* @default false
*/
this.mouseActive = false;
/**
* True if mouse is on the sprite's collider.
* Read only.
*
* @property mouseIsOver
* @type {Boolean}
*/
this.mouseIsOver = false;
/**
* True if mouse is pressed on the sprite's collider.
* Read only.
*
* @property mouseIsPressed
* @type {Boolean}
*/
this.mouseIsPressed = false;
/*
* Width of the sprite's current image.
* If no images or animations are set it's the width of the
* placeholder rectangle.
* Used internally to make calculations and draw the sprite.
*
* @private
* @property _internalWidth
* @type {Number}
* @default 100
*/
this._internalWidth = _w;
/*
* Height of the sprite's current image.
* If no images or animations are set it's the height of the
* placeholder rectangle.
* Used internally to make calculations and draw the sprite.
*
* @private
* @property _internalHeight
* @type {Number}
* @default 100
*/
this._internalHeight = _h;
/*
* _internalWidth and _internalHeight are used for all p5.play
* calculations, but width and height can be extended. For example,
* you may want users to always get and set a scaled width:
Object.defineProperty(this, 'width', {
enumerable: true,
configurable: true,
get: function() {
return this._internalWidth * this.scale;
},
set: function(value) {
this._internalWidth = value / this.scale;
}
});
*/
/**
* Width of the sprite's current image.
* If no images or animations are set it's the width of the
* placeholder rectangle.
*
* @property width
* @type {Number}
* @default 100
*/
Object.defineProperty(this, 'width', {
enumerable: true,
configurable: true,
get: function () {
return this._internalWidth;
},
set: function (value) {
this._internalWidth = value;
}
});
if (_w === undefined)
this.width = 100;
else
this.width = _w;
/**
* Height of the sprite's current image.
* If no images or animations are set it's the height of the
* placeholder rectangle.
*
* @property height
* @type {Number}
* @default 100
*/
Object.defineProperty(this, 'height', {
enumerable: true,
configurable: true,
get: function () {
return this._internalHeight;
},
set: function (value) {
this._internalHeight = value;
}
});
if (_h === undefined)
this.height = 100;
else
this.height = _h;
/**
* Unscaled width of the sprite
* If no images or animations are set it's the width of the
* placeholder rectangle.
*
* @property originalWidth
* @type {Number}
* @default 100
*/
this.originalWidth = this._internalWidth;
/**
* Unscaled height of the sprite
* If no images or animations are set it's the height of the
* placeholder rectangle.
*
* @property originalHeight
* @type {Number}
* @default 100
*/
this.originalHeight = this._internalHeight;
/**
* True if the sprite has been removed.
*
* @property removed
* @type {Boolean}
*/
this.removed = false;
/**
* Cycles before self removal.
* Set it to initiate a countdown, every draw cycle the property is
* reduced by 1 unit. At 0 it will call a sprite.remove()
* Disabled if set to -1.
*
* @property life
* @type {Number}
* @default -1
*/
this.life = -1;
/**
* If set to true, draws an outline of the collider, the depth, and center.
*
* @property debug
* @type {Boolean}
* @default false
*/
this.debug = false;
/**
* If no image or animations are set this is color of the
* placeholder rectangle
*
* @property shapeColor
* @type {color}
*/
this.shapeColor = color(random(255), random(255), random(255));
/**
* Groups the sprite belongs to, including allSprites
*
* @property groups
* @type {Array}
*/
this.groups = [];
var animations = {};
//The current animation's label.
var currentAnimation = '';
/**
* Reference to the current animation.
*
* @property animation
* @type {Animation}
*/
this.animation = undefined;
/*
* @private
* Keep animation properties in sync with how the animation changes.
*/
this._syncAnimationSizes = function () {
//has an animation but the collider is still default
//the animation wasn't loaded. if the animation is not a 1x1 image
//it means it just finished loading
if (this.colliderType === 'default' &&
animations[currentAnimation].getWidth() !== 1 && animations[currentAnimation].getHeight() !== 1) {
this.collider = this.getBoundingBox();
this.colliderType = 'image';
this._internalWidth = animations[currentAnimation].getWidth() * abs(this._getScaleX());
this._internalHeight = animations[currentAnimation].getHeight() * abs(this._getScaleY());
//quadTree.insert(this);
}
//update size and collider
if (animations[currentAnimation].frameChanged || this.width === undefined || this.height === undefined) {
//this.collider = this.getBoundingBox();
this._internalWidth = animations[currentAnimation].getWidth() * abs(this._getScaleX());
this._internalHeight = animations[currentAnimation].getHeight() * abs(this._getScaleY());
}
};
/**
* Updates the sprite.
* Called automatically at the beginning of the draw cycle.
*
* @method update
*/
this.update = function () {
if (!this.removed) {
//if there has been a change somewhere after the last update
//the old position is the last position registered in the update
if (this.newPosition !== this.position)
this.previousPosition = createVector(this.newPosition.x, this.newPosition.y);
else
this.previousPosition = createVector(this.position.x, this.position.y);
this.velocity.x *= 1 - this.friction;
this.velocity.y *= 1 - this.friction;
if (this.maxSpeed !== -1)
this.limitSpeed(this.maxSpeed);
if (this.rotateToDirection && this.velocity.mag() > 0)
this._rotation = this.getDirection();
this.rotation += this.rotationSpeed;
this.position.x += this.velocity.x;
this.position.y += this.velocity.y;
this.newPosition = createVector(this.position.x, this.position.y);
this.deltaX = this.position.x - this.previousPosition.x;
this.deltaY = this.position.y - this.previousPosition.y;
//if there is an animation
if (animations[currentAnimation]) {
//update it
animations[currentAnimation].update();
this._syncAnimationSizes();
}
//a collider is created either manually with setCollider or
//when I check this sprite for collisions or overlaps
if (this.collider) {
if (this.collider instanceof AABB) {
//scale / rotate collider
var t;
if (pInst._angleMode === pInst.RADIANS) {
t = radians(this.rotation);
} else {
t = this.rotation;
}
if (this.colliderType === 'custom') {
this.collider.extents.x = this.collider.originalExtents.x * abs(this._getScaleX()) * abs(cos(t)) +
this.collider.originalExtents.y * abs(this._getScaleY()) * abs(sin(t));
this.collider.extents.y = this.collider.originalExtents.x * abs(this._getScaleX()) * abs(sin(t)) +
this.collider.originalExtents.y * abs(this._getScaleY()) * abs(cos(t));
}
else if (this.colliderType === 'default') {
this.collider.extents.x = this._internalWidth * abs(this._getScaleX()) * abs(cos(t)) +
this._internalHeight * abs(this._getScaleY()) * abs(sin(t));
this.collider.extents.y = this._internalWidth * abs(this._getScaleX()) * abs(sin(t)) +
this._internalHeight * abs(this._getScaleY()) * abs(cos(t));
}
else if (this.colliderType === 'image') {
this.collider.extents.x = this._internalWidth * abs(cos(t)) +
this._internalHeight * abs(sin(t));
this.collider.extents.y = this._internalWidth * abs(sin(t)) +
this._internalHeight * abs(cos(t));
}
}
if (this.collider instanceof CircleCollider) {
//print(this.scale);
this.collider.radius = this.collider.originalRadius * abs(this.scale);
}
}//end collider != null
//mouse actions
if (this.mouseActive) {
//if no collider set it
if (!this.collider)
this.setDefaultCollider();
this.mouseUpdate();
}
else {
if (typeof (this.onMouseOver) === 'function' ||
typeof (this.onMouseOut) === 'function' ||
typeof (this.onMousePressed) === 'function' ||
typeof (this.onMouseReleased) === 'function') {
//if a mouse function is set
//it's implied we want to have it mouse active so
//we do this automatically
this.mouseActive = true;
//if no collider set it
if (!this.collider)
this.setDefaultCollider();
this.mouseUpdate();
}
}
//self destruction countdown
if (this.life > 0)
this.life--;
if (this.life === 0)
this.remove();
}
};//end update
/**
* Creates a default collider matching the size of the
* placeholder rectangle or the bounding box of the image.
*
* @method setDefaultCollider
*/
this.setDefaultCollider = function () {
//if has animation get the animation bounding box
//working only for preloaded images
if (animations[currentAnimation] && (animations[currentAnimation].getWidth() !== 1 && animations[currentAnimation].getHeight() !== 1)) {
this.collider = this.getBoundingBox();
this._internalWidth = animations[currentAnimation].getWidth() * abs(this._getScaleX());
this._internalHeight = animations[currentAnimation].getHeight() * abs(this._getScaleY());
//quadTree.insert(this);
this.colliderType = 'image';
//print("IMAGE COLLIDER ADDED");
}
else if (animations[currentAnimation] && animations[currentAnimation].getWidth() === 1 && animations[currentAnimation].getHeight() === 1) {
//animation is still loading
//print("wait");
}
else //get the with and height defined at the creation
{
this.collider = new AABB(pInst, this.position, createVector(this._internalWidth, this._internalHeight));
//quadTree.insert(this);
this.colliderType = 'default';
}
pInst.quadTree.insert(this);
};
/**
* Updates the sprite mouse states and triggers the mouse events:
* onMouseOver, onMouseOut, onMousePressed, onMouseReleased
*
* @method mouseUpdate
*/
this.mouseUpdate = function () {
var mouseWasOver = this.mouseIsOver;
var mouseWasPressed = this.mouseIsPressed;
this.mouseIsOver = false;
this.mouseIsPressed = false;
var mousePosition;
if (camera.active)
mousePosition = createVector(camera.mouseX, camera.mouseY);
else
mousePosition = createVector(pInst.mouseX, pInst.mouseY);
//rollover
if (this.collider) {
if (this.collider instanceof CircleCollider) {
if (dist(mousePosition.x, mousePosition.y, this.collider.center.x, this.collider.center.y) < this.collider.radius)
this.mouseIsOver = true;
} else if (this.collider instanceof AABB) {
if (mousePosition.x > this.collider.left() &&
mousePosition.y > this.collider.top() &&
mousePosition.x < this.collider.right() &&
mousePosition.y < this.collider.bottom()) {
this.mouseIsOver = true;
}
}
//global p5 var
if (this.mouseIsOver && pInst.mouseIsPressed)
this.mouseIsPressed = true;
//event change - call functions
if (!mouseWasOver && this.mouseIsOver && this.onMouseOver !== undefined)
if (typeof (this.onMouseOver) === 'function')
this.onMouseOver.call(this, this);
else
print('Warning: onMouseOver should be a function');
if (mouseWasOver && !this.mouseIsOver && this.onMouseOut !== undefined)
if (typeof (this.onMouseOut) === 'function')
this.onMouseOut.call(this, this);
else
print('Warning: onMouseOut should be a function');
if (!mouseWasPressed && this.mouseIsPressed && this.onMousePressed !== undefined)
if (typeof (this.onMousePressed) === 'function')
this.onMousePressed.call(this, this);
else
print('Warning: onMousePressed should be a function');
if (mouseWasPressed && !pInst.mouseIsPressed && !this.mouseIsPressed && this.onMouseReleased !== undefined)
if (typeof (this.onMouseReleased) === 'function')
this.onMouseReleased.call(this, this);
else
print('Warning: onMouseReleased should be a function');
}
};
/**
* Sets a collider for the sprite.
*
* In p5.play a Collider is an invisible circle or rectangle
* that can have any size or position relative to the sprite and which
* will be used to detect collisions and overlapping with other sprites,
* or the mouse cursor.
*
* If the sprite is checked for collision, bounce, overlapping or mouse events
* a collider is automatically created from the width and height parameter
* passed at the creation of the sprite or the from the image dimension in case
* of animated sprites.
*
* Often the image bounding box is not appropriate as the active area for
* collision detection so you can set a circular or rectangular sprite with
* different dimensions and offset from the sprite's center.
*
* There are four ways to call this method:
*
* 1. setCollider("rectangle")
* 2. setCollider("rectangle", offsetX, offsetY, width, height)
* 3. setCollider("circle")
* 4. setCollider("circle", offsetX, offsetY, radius)
*
* @method setCollider
* @param {String} type Either "rectangle" or "circle"
* @param {Number} offsetX Collider x position from the center of the sprite
* @param {Number} offsetY Collider y position from the center of the sprite
* @param {Number} width Collider width or radius
* @param {Number} height Collider height
* @throws {TypeError} if given invalid parameters.
*/
this.setCollider = function (type, offsetX, offsetY, width, height) {
if (!(type === 'rectangle' || type === 'circle')) {
throw new TypeError('setCollider expects the first argument to be either "circle" or "rectangle"');
} else if (type === 'circle' && arguments.length > 1 && arguments.length < 4) {
throw new TypeError('Usage: setCollider("circle") or setCollider("circle", offsetX, offsetY, radius)');
} else if (type === 'circle' && arguments.length > 4) {
pInst._warn('Extra parameters to setCollider were ignored. Usage: setCollider("circle") or setCollider("circle", offsetX, offsetY, radius)');
} else if (type === 'rectangle' && arguments.length > 1 && arguments.length < 5) {
throw new TypeError('Usage: setCollider("rectangle") or setCollider("rectangle", offsetX, offsetY, width, height)');
} else if (type === 'rectangle' && arguments.length > 5) {
pInst._warn('Extra parameters to setCollider were ignored. Usage: setCollider("rectangle") or setCollider("rectangle", offsetX, offsetY, width, height)');
}
this.colliderType = 'custom';
var v = createVector(offsetX, offsetY);
if (type === 'rectangle' && arguments.length === 1) {
this.collider = new AABB(pInst, this.position, createVector(this.width, this.height));
} else if (type === 'rectangle' && arguments.length >= 5) {
this.collider = new AABB(pInst, this.position, createVector(width, height), v);
} else if (type === 'circle' && arguments.length === 1) {
this.collider = new CircleCollider(pInst, this.position, Math.floor(Math.max(this.width, this.height) / 2));
} else if (type === 'circle' && arguments.length >= 4) {
this.collider = new CircleCollider(pInst, this.position, width, v);
}
quadTree.insert(this);
};
/**
* Returns a the bounding box of the current image
* @method getBoundingBox
*/
this.getBoundingBox = function () {
var w = animations[currentAnimation].getWidth() * abs(this._getScaleX());
var h = animations[currentAnimation].getHeight() * abs(this._getScaleY());
//if the bounding box is 1x1 the image is not loaded
//potential issue with actual 1x1 images
if (w === 1 && h === 1) {
//not loaded yet
return new AABB(pInst, this.position, createVector(w, h));
}
else {
return new AABB(pInst, this.position, createVector(w, h));
}
};
/**
* Sets the sprite's horizontal mirroring.
* If 1 the images displayed normally
* If -1 the images are flipped horizontally
* If no argument returns the current x mirroring
*
* @method mirrorX
* @param {Number} dir Either 1 or -1
* @return {Number} Current mirroring if no parameter is specified
*/
this.mirrorX = function (dir) {
if (dir === 1 || dir === -1)
dirX = dir;
else
return dirX;
};
/**
* Sets the sprite's vertical mirroring.
* If 1 the images displayed normally
* If -1 the images are flipped vertically
* If no argument returns the current y mirroring
*
* @method mirrorY
* @param {Number} dir Either 1 or -1
* @return {Number} Current mirroring if no parameter is specified
*/
this.mirrorY = function (dir) {
if (dir === 1 || dir === -1)
dirY = dir;
else
return dirY;
};
/*
* Returns the value the sprite should be scaled in the X direction.
* Used to calculate rendering and collisions.
* @private
*/
this._getScaleX = function () {
return this.scale;
};
/*
* Returns the value the sprite should be scaled in the Y direction.
* Used to calculate rendering and collisions.
* @private
*/
this._getScaleY = function () {
return this.scale;
};
/**
* Manages the positioning, scale and rotation of the sprite
* Called automatically, it should not be overridden
* @private
* @final
* @method display
*/
this.display = function () {
if (this.visible && !this.removed) {
push();
colorMode(RGB);
noStroke();
rectMode(CENTER);
ellipseMode(CENTER);
imageMode(CENTER);
translate(this.position.x, this.position.y);
scale(this._getScaleX() * dirX, this._getScaleY() * dirY);
if (pInst._angleMode === pInst.RADIANS) {
rotate(radians(this.rotation));
} else {
rotate(this.rotation);
}
this.draw();
//draw debug info
pop();
if (this.debug) {
push();
//draw the anchor point
stroke(0, 255, 0);
strokeWeight(1);
line(this.position.x - 10, this.position.y, this.position.x + 10, this.position.y);
line(this.position.x, this.position.y - 10, this.position.x, this.position.y + 10);
noFill();
//depth number
noStroke();
fill(0, 255, 0);
textAlign(LEFT, BOTTOM);
textSize(16);
text(this.depth + '', this.position.x + 4, this.position.y - 2);
noFill();
stroke(0, 255, 0);
//bounding box
if (this.collider !== undefined) {
this.collider.draw();
}
pop();
}
}
};
/**
* Manages the visuals of the sprite.
* It can be overridden with a custom drawing function.
* The 0,0 point will be the center of the sprite.
* Example:
* sprite.draw = function() { ellipse(0,0,10,10) }
* Will display the sprite as circle.
*
* @method draw
*/
this.draw = function () {
if (currentAnimation !== '' && animations) {
if (animations[currentAnimation])
animations[currentAnimation].draw(0, 0, 0);
}
else {
noStroke();
fill(this.shapeColor);
rect(0, 0, this._internalWidth, this._internalHeight);
}
};
/**
* Removes the Sprite from the sketch.
* The removed Sprite won't be drawn or updated anymore.
*
* @method remove
*/
this.remove = function () {
this.removed = true;
quadTree.removeObject(this);
//when removed from the "scene" also remove all the references in all the groups
while (this.groups.length > 0) {
this.groups[0].remove(this);
}
};
/**
* Sets the velocity vector.
*
* @method setVelocity
* @param {Number} x X component
* @param {Number} y Y component
*/
this.setVelocity = function (x, y) {
this.velocity.x = x;
this.velocity.y = y;
};
/**
* Calculates the scalar speed.
*
* @method getSpeed
* @return {Number} Scalar speed
*/
this.getSpeed = function () {
return this.velocity.mag();
};
/**
* Calculates the movement's direction in degrees.
*
* @method getDirection
* @return {Number} Angle in degrees
*/
this.getDirection = function () {
var direction = atan2(this.velocity.y, this.velocity.x);
if (isNaN(direction))
direction = 0;
// Unlike Math.atan2, the atan2 method above will return degrees if
// the current p5 angleMode is DEGREES, and radians if the p5 angleMode is
// RADIANS. This method should always return degrees (for now).
// See https://github.com/molleindustria/p5.play/issues/94
if (pInst._angleMode === pInst.RADIANS) {
direction = degrees(direction);
}
return direction;
};
/**
* Adds the sprite to an existing group
*
* @method addToGroup
* @param {Object} group
*/
this.addToGroup = function (group) {
if (group instanceof Array)
group.add(this);
else
print('addToGroup error: ' + group + ' is not a group');
};
/**
* Limits the scalar speed.
*
* @method limitSpeed
* @param {Number} max Max speed: positive number
*/
this.limitSpeed = function (max) {
//update linear speed
var speed = this.getSpeed();
if (abs(speed) > max) {
//find reduction factor
var k = max / abs(speed);
this.velocity.x *= k;
this.velocity.y *= k;
}
};
/**
* Set the speed and direction of the sprite.
* The action overwrites the current velocity.
* If direction is not supplied, the current direction is maintained.
* If direction is not supplied and there is no current velocity, the current
* rotation angle used for the direction.
*
* @method setSpeed
* @param {Number} speed Scalar speed
* @param {Number} [angle] Direction in degrees
*/
this.setSpeed = function (speed, angle) {
var a;
if (typeof angle === 'undefined') {
if (this.velocity.x !== 0 || this.velocity.y !== 0) {
a = pInst.atan2(this.velocity.y, this.velocity.x);
} else {
if (pInst._angleMode === pInst.RADIANS) {
a = radians(this._rotation);
} else {
a = this._rotation;
}
}
} else {
if (pInst._angleMode === pInst.RADIANS) {
a = radians(angle);
} else {
a = angle;
}
}
this.velocity.x = cos(a) * speed;
this.velocity.y = sin(a) * speed;
};
/**
* Pushes the sprite in a direction defined by an angle.
* The force is added to the current velocity.
*
* @method addSpeed
* @param {Number} speed Scalar speed to add
* @param {Number} angle Direction in degrees
*/
this.addSpeed = function (speed, angle) {
var a;
if (pInst._angleMode === pInst.RADIANS) {
a = radians(angle);
} else {
a = angle;
}
this.velocity.x += cos(a) * speed;
this.velocity.y += sin(a) * speed;
};
/**
* Pushes the sprite toward a point.
* The force is added to the current velocity.
*
* @method attractionPoint
* @param {Number} magnitude Scalar speed to add
* @param {Number} pointX Direction x coordinate
* @param {Number} pointY Direction y coordinate
*/
this.attractionPoint = function (magnitude, pointX, pointY) {
var angle = atan2(pointY - this.position.y, pointX - this.position.x);
this.velocity.x += cos(angle) * magnitude;
this.velocity.y += sin(angle) * magnitude;
};
/**
* Adds an image to the sprite.
* An image will be considered a one-frame animation.
* The image should be preloaded in the preload() function using p5 loadImage.
* Animations require a identifying label (string) to change them.
* The image is stored in the sprite but not necessarily displayed
* until Sprite.changeAnimation(label) is called
*
* Usages:
* - sprite.addImage(label, image);
* - sprite.addImage(image);
*
* If only an image is passed no label is specified
*
* @method addImage
* @param {String|p5.Image} label Label or image
* @param {p5.Image} [img] Image
*/
this.addImage = function () {
if (typeof arguments[0] === 'string' && arguments[1] instanceof p5.Image)
this.addAnimation(arguments[0], arguments[1]);
else if (arguments[0] instanceof p5.Image)
this.addAnimation('normal', arguments[0]);
else
throw ('addImage error: allowed usages are <image> or <label>, <image>');
};
/**
* Adds an animation to the sprite.
* The animation should be preloaded in the preload() function
* using loadAnimation.
* Animations require a identifying label (string) to change them.
* Animations are stored in the sprite but not necessarily displayed
* until Sprite.changeAnimation(label) is called.
*
* Usage:
* - sprite.addAnimation(label, animation);
*
* Alternative usages. See Animation for more information on file sequences:
* - sprite.addAnimation(label, firstFrame, lastFrame);
* - sprite.addAnimation(label, frame1, frame2, frame3...);
*
* @method addAnimation
* @param {String} label Animation identifier
* @param {Animation} animation The preloaded animation
*/
this.addAnimation = function (label) {
var anim;
if (typeof label !== 'string') {
print('Sprite.addAnimation error: the first argument must be a label (String)');
return -1;
}
else if (arguments.length < 2) {
print('addAnimation error: you must specify a label and n frame images');
return -1;
}
else if (arguments[1] instanceof Animation) {
var sourceAnimation = arguments[1];
var newAnimation = sourceAnimation.clone();
animations[label] = newAnimation;
if (currentAnimation === '') {
currentAnimation = label;
this.animation = newAnimation;
}
newAnimation.isSpriteAnimation = true;
this._internalWidth = newAnimation.getWidth() * abs(this._getScaleX());
this._internalHeight = newAnimation.getHeight() * abs(this._getScaleY());
return newAnimation;
}
else {
var animFrames = [];
for (var i = 1; i < arguments.length; i++)
animFrames.push(arguments[i]);
anim = construct(pInst.Animation, animFrames);
animations[label] = anim;
if (currentAnimation === '') {
currentAnimation = label;
this.animation = anim;
}
anim.isSpriteAnimation = true;
this._internalWidth = anim.getWidth() * abs(this._getScaleX());
this._internalHeight = anim.getHeight() * abs(this._getScaleY());
return anim;
}
};
/**
* Changes the displayed image/animation.
* Equivalent to changeAnimation
*
* @method changeImage
* @param {String} label Image/Animation identifier
*/
this.changeImage = function (label) {
this.changeAnimation(label);
};
/**
* Returns the label of the current animation
*
* @method getAnimationLabel
* @return {String} label Image/Animation identifier
*/
this.getAnimationLabel = function () {
return currentAnimation;
};
/**
* Changes the displayed animation.
* See Animation for more control over the sequence.
*
* @method changeAnimation
* @param {String} label Animation identifier
*/
this.changeAnimation = function (label) {
if (!animations[label])
print('changeAnimation error: no animation labeled ' + label);
else {
currentAnimation = label;
this.animation = animations[label];
}
};
/**
* Checks if the given point corresponds to a transparent pixel
* in the sprite's current image. It can be used to check a point collision
* against only the visible part of the sprite.
*
* @method overlapPixel
* @param {Number} pointX x coordinate of the point to check
* @param {Number} pointY y coordinate of the point to check
* @return {Boolean} result True if non-transparent
*/
this.overlapPixel = function (pointX, pointY) {
var point = createVector(pointX, pointY);
var img = this.animation.getFrameImage();
//convert point to img relative position
point.x -= this.position.x - img.width / 2;
point.y -= this.position.y - img.height / 2;
//out of the image entirely
if (point.x < 0 || point.x > img.width || point.y < 0 || point.y > img.height)
return false;
else if (this.rotation === 0 && this.scale === 1) {
//true if full opacity
var values = img.get(point.x, point.y);
return values[3] === 255;
}
else {
print('Error: overlapPixel doesn\'t work with scaled or rotated sprites yet');
//offscreen printing to be implemented bleurch
return false;
}
};
/**
* Checks if the given point is inside the sprite's collider.
*
* @method overlapPoint
* @param {Number} pointX x coordinate of the point to check
* @param {Number} pointY y coordinate of the point to check
* @return {Boolean} result True if inside
*/
this.overlapPoint = function (pointX, pointY) {
var point = createVector(pointX, pointY);
if (!this.collider)
this.setDefaultCollider();
if (this.collider !== undefined) {
if (this.collider instanceof AABB)
return (point.x > this.collider.left() && point.x < this.collider.right() && point.y > this.collider.top() && point.y < this.collider.bottom());
if (this.collider instanceof CircleCollider) {
var sqRadius = this.collider.radius * this.collider.radius;
var sqDist = pow(this.collider.center.x - point.x, 2) + pow(this.collider.center.y - point.y, 2);
return sqDist < sqRadius;
}
else
return false;
}
else
return false;
};
/**
* Checks if the the sprite is overlapping another sprite or a group.
* The check is performed using the colliders. If colliders are not set
* they will be created automatically from the image/animation bounding box.
*
* A callback function can be specified to perform additional operations
* when the overlap occours.
* If the target is a group the function will be called for each single
* sprite overlapping. The parameter of the function are respectively the
* current sprite and the colliding sprite.
*
* @example
* sprite.overlap(otherSprite, explosion);
*
* function explosion(spriteA, spriteB) {
* spriteA.remove();
* spriteB.score++;
* }
*
* @method overlap
* @param {Object} target Sprite or group to check against the current one
* @param {Function} [callback] The function to be called if overlap is positive
* @return {Boolean} True if overlapping
*/
this.overlap = function (target, callback) {
//if(this.collider instanceof AABB && target.collider instanceof AABB)
return this.AABBops('overlap', target, callback);
};
/**
* Checks if the the sprite is overlapping another sprite or a group.
* If the overlap is positive the current sprite will be displace by
* the colliding one in the closest non-overlapping position.
*
* The check is performed using the colliders. If colliders are not set
* they will be created automatically from the image/animation bounding box.
*
* A callback function can be specified to perform additional operations
* when the collision occours.
* If the target is a group the function will be called for each single
* sprite colliding. The parameter of the function are respectively the
* current sprite and the colliding sprite.
*
* @example
* sprite.collide(otherSprite, explosion);
*
* function explosion(spriteA, spriteB) {
* spriteA.remove();
* spriteB.score++;
* }
*
* @method collide
* @param {Object} target Sprite or group to check against the current one
* @param {Function} [callback] The function to be called if overlap is positive
* @return {Boolean} True if overlapping
*/
this.collide = function (target, callback) {
//if(this.collider instanceof AABB && target.collider instanceof AABB)
return this.AABBops('collide', target, callback);
};
/**
* Checks if the the sprite is overlapping another sprite or a group.
* If the overlap is positive the current sprite will displace
* the colliding one to the closest non-overlapping position.
*
* The check is performed using the colliders. If colliders are not set
* they will be created automatically from the image/animation bounding box.
*
* A callback function can be specified to perform additional operations
* when the collision occours.
* If the target is a group the function will be called for each single
* sprite colliding. The parameter of the function are respectively the
* current sprite and the colliding sprite.
*
* @example
* sprite.displace(otherSprite, explosion);
*
* function explosion(spriteA, spriteB) {
* spriteA.remove();
* spriteB.score++;
* }
*
* @method displace
* @param {Object} target Sprite or group to check against the current one
* @param {Function} [callback] The function to be called if overlap is positive
* @return {Boolean} True if overlapping
*/
this.displace = function (target, callback) {
return this.AABBops('displace', target, callback);
};
/**
* Checks if the the sprite is overlapping another sprite or a group.
* If the overlap is positive the sprites will bounce affecting each
* other's trajectories depending on their .velocity, .mass and .restitution
*
* The check is performed using the colliders. If colliders are not set
* they will be created automatically from the image/animation bounding box.
*
* A callback function can be specified to perform additional operations
* when the collision occours.
* If the target is a group the function will be called for each single
* sprite colliding. The parameter of the function are respectively the
* current sprite and the colliding sprite.
*
* @example
* sprite.bounce(otherSprite, explosion);
*
* function explosion(spriteA, spriteB) {
* spriteA.remove();
* spriteB.score++;
* }
*
* @method bounce
* @param {Object} target Sprite or group to check against the current one
* @param {Function} [callback] The function to be called if overlap is positive
* @return {Boolean} True if overlapping
*/
this.bounce = function (target, callback) {
return this.AABBops('bounce', target, callback);
};
// Internal collision detection function. Do not use directly.
this.AABBops = function (type, target, callback) {
this.touching.left = false;
this.touching.right = false;
this.touching.top = false;
this.touching.bottom = false;
var result = false;
//if single sprite turn into array anyway
var others = [];
if (target instanceof Sprite)
others.push(target);
else if (target instanceof Array) {
if (quadTree !== undefined && quadTree.active)
others = quadTree.retrieveFromGroup(this, target);
if (others.length === 0)
others = target;
}
else
throw ('Error: overlap can only be checked between sprites or groups');
for (var i = 0; i < others.length; i++)
if (this !== others[i] && !this.removed) //you can check collisions within the same group but not on itself
{
var displacement;
var other = others[i];
if (this.collider === undefined)
this.setDefaultCollider();
if (other.collider === undefined)
other.setDefaultCollider();
/*
if(this.colliderType=="default" && animations[currentAnimation]!=null)
{
print("busted");
return false;
}*/
if (this.collider !== undefined && other.collider !== undefined) {
if (type === 'overlap') {
var over;
//if the other is a circle I calculate the displacement from here
if (this.collider instanceof CircleCollider)
over = other.collider.overlap(this.collider);
else
over = this.collider.overlap(other.collider);
if (over) {
result = true;
if (callback !== undefined && typeof callback === 'function')
callback.call(this, this, other);
}
}
else if (type === 'collide' || type === 'displace' || type === 'bounce') {
displacement = createVector(0, 0);
//if the sum of the speed is more than the collider i may
//have a tunnelling problem
var tunnelX = abs(this.velocity.x - other.velocity.x) >= other.collider.extents.x / 2 && round(this.deltaX - this.velocity.x) === 0;
var tunnelY = abs(this.velocity.y - other.velocity.y) >= other.collider.size().y / 2 && round(this.deltaY - this.velocity.y) === 0;
if (tunnelX || tunnelY) {
//instead of using the colliders I use the bounding box
//around the previous position and current position
//this is regardless of the collider type
//the center is the average of the coll centers
var c = createVector(
(this.position.x + this.previousPosition.x) / 2,
(this.position.y + this.previousPosition.y) / 2);
//the extents are the distance between the coll centers
//plus the extents of both
var e = createVector(
abs(this.position.x - this.previousPosition.x) + this.collider.extents.x,
abs(this.position.y - this.previousPosition.y) + this.collider.extents.y);
var bbox = new AABB(pInst, c, e, this.collider.offset);
//bbox.draw();
if (bbox.overlap(other.collider)) {
if (tunnelX) {
//entering from the right
if (this.velocity.x < 0)
displacement.x = other.collider.right() - this.collider.left() + 1;
else if (this.velocity.x > 0)
displacement.x = other.collider.left() - this.collider.right() - 1;
}
if (tunnelY) {
//from top
if (this.velocity.y > 0)
displacement.y = other.collider.top() - this.collider.bottom() - 1;
else if (this.velocity.y < 0)
displacement.y = other.collider.bottom() - this.collider.top() + 1;
}
}//end overlap
}
else //non tunnel overlap
{
//if the other is a circle I calculate the displacement from here
//and reverse it
if (this.collider instanceof CircleCollider) {
displacement = other.collider.collide(this.collider).mult(-1);
}
else
displacement = this.collider.collide(other.collider);
}
if (displacement.x !== 0 || displacement.y !== 0) {
var newVelX1, newVelY1, newVelX2, newVelY2;
if (type === 'displace' && !other.immovable) {
other.position.sub(displacement);
} else if ((type === 'collide' || type === 'bounce') && !this.immovable) {
this.position.add(displacement);
this.previousPosition = createVector(this.position.x, this.position.y);
this.newPosition = createVector(this.position.x, this.position.y);
}
if (displacement.x > 0)
this.touching.left = true;
if (displacement.x < 0)
this.touching.right = true;
if (displacement.y < 0)
this.touching.bottom = true;
if (displacement.y > 0)
this.touching.top = true;
if (type === 'bounce') {
if (this.collider instanceof CircleCollider && other.collider instanceof CircleCollider) {
var dx1 = p5.Vector.sub(this.position, other.position);
var dx2 = p5.Vector.sub(other.position, this.position);
var magnitude = dx1.magSq();
var totalMass = this.mass + other.mass;
var m1 = 0, m2 = 0;
if (this.immovable) {
m2 = 2;
} else if (other.immovable) {
m1 = 2;
} else {
m1 = 2 * other.mass / totalMass;
m2 = 2 * this.mass / totalMass;
}
var newVel1 = dx1.mult(m1 * p5.Vector.sub(this.velocity, other.velocity).dot(dx1) / magnitude);
var newVel2 = dx2.mult(m2 * p5.Vector.sub(other.velocity, this.velocity).dot(dx2) / magnitude);
this.velocity.sub(newVel1.mult(this.restitution));
other.velocity.sub(newVel2.mult(other.restitution));
}
else {
if (other.immovable) {
newVelX1 = -this.velocity.x + other.velocity.x;
newVelY1 = -this.velocity.y + other.velocity.y;
}
else {
newVelX1 = (this.velocity.x * (this.mass - other.mass) + (2 * other.mass * other.velocity.x)) / (this.mass + other.mass);
newVelY1 = (this.velocity.y * (this.mass - other.mass) + (2 * other.mass * other.velocity.y)) / (this.mass + other.mass);
newVelX2 = (other.velocity.x * (other.mass - this.mass) + (2 * this.mass * this.velocity.x)) / (this.mass + other.mass);
newVelY2 = (other.velocity.y * (other.mass - this.mass) + (2 * this.mass * this.velocity.y)) / (this.mass + other.mass);
}
//var bothCircles = (this.collider instanceof CircleCollider &&
// other.collider instanceof CircleCollider);
//if(this.touching.left || this.touching.right || this.collider instanceof CircleCollider)
//print(displacement);
if (abs(displacement.x) > abs(displacement.y)) {
if (!this.immovable) {
this.velocity.x = newVelX1 * this.restitution;
}
if (!other.immovable)
other.velocity.x = newVelX2 * other.restitution;
}
//if(this.touching.top || this.touching.bottom || this.collider instanceof CircleCollider)
if (abs(displacement.x) < abs(displacement.y)) {
if (!this.immovable)
this.velocity.y = newVelY1 * this.restitution;
if (!other.immovable)
other.velocity.y = newVelY2 * other.restitution;
}
}
}
//else if(type == "collide")
//this.velocity = createVector(0,0);
if (callback !== undefined && typeof callback === 'function')
callback.call(this, this, other);
result = true;
}
}
}//end collider exists
}
return result;
};
} //end Sprite class
defineLazyP5Property('Sprite', boundConstructorFactory(Sprite));
/**
* A camera facilitates scrolling and zooming for scenes extending beyond
* the canvas. A camera has a position, a zoom factor, and the mouse
* coordinates relative to the view.
* The camera is automatically created on the first draw cycle.
*
* In p5.js terms the camera wraps the whole drawing cycle in a
* transformation matrix but it can be disable anytime during the draw
* cycle for example to draw interface elements in an absolute position.
*
* @class Camera
* @constructor
* @param {Number} x Initial x coordinate
* @param {Number} y Initial y coordinate
* @param {Number} zoom magnification
**/
function Camera(pInst, x, y, zoom) {
/**
* Camera position. Defines the global offset of the sketch.
*
* @property position
* @type {p5.Vector}
*/
this.position = pInst.createVector(x, y);
/**
* Camera zoom. Defines the global scale of the sketch.
* A scale of 1 will be the normal size. Setting it to 2 will make everything
* twice the size. .5 will make everything half size.
*
* @property zoom
* @type {Number}
*/
this.zoom = zoom;
/**
* MouseX translated to the camera view.
* Offsetting and scaling the canvas will not change the sprites' position
* nor the mouseX and mouseY variables. Use this property to read the mouse
* position if the camera moved or zoomed.
*
* @property mouseX
* @type {Number}
*/
this.mouseX = pInst.mouseX;
/**
* MouseY translated to the camera view.
* Offsetting and scaling the canvas will not change the sprites' position
* nor the mouseX and mouseY variables. Use this property to read the mouse
* position if the camera moved or zoomed.
*
* @property mouseY
* @type {Number}
*/
this.mouseY = pInst.mouseY;
/**
* True if the camera is active.
* Read only property. Use the methods Camera.on() and Camera.off()
* to enable or disable the camera.
*
* @property active
* @type {Boolean}
*/
this.active = false;
/**
* Activates the camera.
* The canvas will be drawn according to the camera position and scale until
* Camera.off() is called
*
* @method on
*/
this.on = function () {
if (!this.active) {
cameraPush.call(pInst);
this.active = true;
}
};
/**
* Deactivates the camera.
* The canvas will be drawn normally, ignoring the camera's position
* and scale until Camera.on() is called
*
* @method off
*/
this.off = function () {
if (this.active) {
cameraPop.call(pInst);
this.active = false;
}
};
} //end camera class
defineLazyP5Property('Camera', boundConstructorFactory(Camera));
//called pre draw by default
function cameraPush() {
var pInst = this;
var camera = pInst.camera;
//awkward but necessary in order to have the camera at the center
//of the canvas by default
if (!camera.init && camera.position.x === 0 && camera.position.y === 0) {
camera.position.x = pInst.width / 2;
camera.position.y = pInst.height / 2;
camera.init = true;
}
camera.mouseX = pInst.mouseX + camera.position.x - pInst.width / 2;
camera.mouseY = pInst.mouseY + camera.position.y - pInst.height / 2;
if (!camera.active) {
camera.active = true;
pInst.push();
pInst.scale(camera.zoom);
pInst.translate(-camera.position.x + pInst.width / 2 / camera.zoom, -camera.position.y + pInst.height / 2 / camera.zoom);
}
}
//called postdraw by default
function cameraPop() {
var pInst = this;
if (pInst.camera.active) {
pInst.pop();
pInst.camera.active = false;
}
}
/**
* In p5.play groups are collections of sprites with similar behavior.
* For example a group may contain all the sprites in the background
* or all the sprites that "kill" the player.
*
* Groups are "extended" arrays and inherit all their properties
* e.g. group.length
*
* Since groups contain only references, a sprite can be in multiple
* groups and deleting a group doesn't affect the sprites themselves.
*
* Sprite.remove() will also remove the sprite from all the groups
* it belongs to.
*
* @class Group
* @constructor
*/
function Group() {
//basically extending the array
var array = [];
/**
* Gets the member at index i.
*
* @method get
* @param {Number} i The index of the object to retrieve
*/
array.get = function (i) {
return array[i];
};
/**
* Checks if the group contains a sprite.
*
* @method contains
* @param {Sprite} sprite The sprite to search
* @return {Number} Index or -1 if not found
*/
array.contains = function (sprite) {
return this.indexOf(sprite) > -1;
};
/**
* Same as Group.contains
* @method indexOf
*/
array.indexOf = function (item) {
for (var i = 0, len = array.length; i < len; ++i) {
if (virtEquals(item, array[i])) {
return i;
}
}
return -1;
};
/**
* Adds a sprite to the group.
*
* @method add
* @param {Sprite} s The sprite to be added
*/
array.add = function (s) {
if (!(s instanceof Sprite)) {
throw ('Error: you can only add sprites to a group');
}
if (-1 === this.indexOf(s)) {
array.push(s);
s.groups.push(this);
}
};
/**
* Same as group.length
* @method size
*/
array.size = function () {
return array.length;
};
/**
* Removes all the sprites in the group
* from the scene.
*
* @method removeSprites
*/
array.removeSprites = function () {
while (array.length > 0) {
array[0].remove();
}
};
/**
* Removes all references to the group.
* Does not remove the actual sprites.
*
* @method clear
*/
array.clear = function () {
array.length = 0;
};
/**
* Removes a sprite from the group.
* Does not remove the actual sprite, only the affiliation (reference).
*
* @method remove
* @param {Sprite} item The sprite to be removed
* @return {Boolean} True if sprite was found and removed
*/
array.remove = function (item) {
if (!(item instanceof Sprite)) {
throw ('Error: you can only remove sprites from a group');
}
var i, removed = false;
for (i = array.length - 1; i >= 0; i--) {
if (array[i] === item) {
array.splice(i, 1);
removed = true;
}
}
if (removed) {
for (i = item.groups.length - 1; i >= 0; i--) {
if (item.groups[i] === this) {
item.groups.splice(i, 1);
}
}
}
return removed;
};
/**
* Returns a copy of the group as standard array.
* @method toArray
*/
array.toArray = function () {
return array.slice(0);
};
/**
* Returns the highest depth in a group
*
* @method maxDepth
* @return {Number} The depth of the sprite drawn on the top
*/
array.maxDepth = function () {
if (array.length === 0) {
return 0;
}
return array.reduce(function (maxDepth, sprite) {
return Math.max(maxDepth, sprite.depth);
}, -Infinity);
};
/**
* Returns the lowest depth in a group
*
* @method minDepth
* @return {Number} The depth of the sprite drawn on the bottom
*/
array.minDepth = function () {
if (array.length === 0) {
return 99999;
}
return array.reduce(function (minDepth, sprite) {
return Math.min(minDepth, sprite.depth);
}, Infinity);
};
/**
* Draws all the sprites in the group.
*
* @method draw
*/
array.draw = function () {
//sort by depth
this.sort(function (a, b) {
return a.depth - b.depth;
});
for (var i = 0; i < this.size(); i++) {
this.get(i).display();
}
};
//internal use
function virtEquals(obj, other) {
if (obj === null || other === null) {
return (obj === null) && (other === null);
}
if (typeof (obj) === 'string') {
return obj === other;
}
if (typeof (obj) !== 'object') {
return obj === other;
}
if (obj.equals instanceof Function) {
return obj.equals(other);
}
return obj === other;
}
/**
* Collide each member of group against the target using the given collision
* type. Return true if any collision occurred.
* Internal use
*
* @private
* @method _groupCollide
* @param {!string} type one of 'overlap', 'collide', 'displace', 'bounce'
* @param {Object} target Group or Sprite
* @param {Function} [callback] on collision.
* @return {boolean} True if any collision/overlap occurred
*/
function _groupCollide(type, target, callback) {
var didCollide = false;
for (var i = 0; i < this.size(); i++)
didCollide = this.get(i).AABBops(type, target, callback) || didCollide;
return didCollide;
}
/**
* Checks if the the group is overlapping another group or sprite.
* The check is performed using the colliders. If colliders are not set
* they will be created automatically from the image/animation bounding box.
*
* A callback function can be specified to perform additional operations
* when the overlap occurs.
* The function will be called for each single sprite overlapping.
* The parameter of the function are respectively the
* member of the current group and the other sprite passed as parameter.
*
* @example
* group.overlap(otherSprite, explosion);
*
* function explosion(spriteA, spriteB) {
* spriteA.remove();
* spriteB.score++;
* }
*
* @method overlap
* @param {Object} target Group or Sprite to check against the current one
* @param {Function} [callback] The function to be called if overlap is positive
* @return {Boolean} True if overlapping
*/
array.overlap = _groupCollide.bind(array, 'overlap');
/**
* Checks if the the group is overlapping another group or sprite.
* If the overlap is positive the sprites in the group will be displaced
* by the colliding one to the closest non-overlapping positions.
*
* The check is performed using the colliders. If colliders are not set
* they will be created automatically from the image/animation bounding box.
*
* A callback function can be specified to perform additional operations
* when the overlap occours.
* The function will be called for each single sprite overlapping.
* The parameter of the function are respectively the
* member of the current group and the other sprite passed as parameter.
*
* @example
* group.collide(otherSprite, explosion);
*
* function explosion(spriteA, spriteB) {
* spriteA.remove();
* spriteB.score++;
* }
*
* @method collide
* @param {Object} target Group or Sprite to check against the current one
* @param {Function} [callback] The function to be called if overlap is positive
* @return {Boolean} True if overlapping
*/
array.collide = _groupCollide.bind(array, 'collide');
/**
* Checks if the the group is overlapping another group or sprite.
* If the overlap is positive the sprites in the group will displace
* the colliding ones to the closest non-overlapping positions.
*
* The check is performed using the colliders. If colliders are not set
* they will be created automatically from the image/animation bounding box.
*
* A callback function can be specified to perform additional operations
* when the overlap occurs.
* The function will be called for each single sprite overlapping.
* The parameter of the function are respectively the
* member of the current group and the other sprite passed as parameter.
*
* @example
* group.displace(otherSprite, explosion);
*
* function explosion(spriteA, spriteB) {
* spriteA.remove();
* spriteB.score++;
* }
*
* @method displace
* @param {Object} target Group or Sprite to check against the current one
* @param {Function} [callback] The function to be called if overlap is positive
* @return {Boolean} True if overlapping
*/
array.displace = _groupCollide.bind(array, 'displace');
/**
* Checks if the the group is overlapping another group or sprite.
* If the overlap is positive the sprites will bounce affecting each
* other's trajectories depending on their .velocity, .mass and .restitution.
*
* The check is performed using the colliders. If colliders are not set
* they will be created automatically from the image/animation bounding box.
*
* A callback function can be specified to perform additional operations
* when the overlap occours.
* The function will be called for each single sprite overlapping.
* The parameter of the function are respectively the
* member of the current group and the other sprite passed as parameter.
*
* @example
* group.bounce(otherSprite, explosion);
*
* function explosion(spriteA, spriteB) {
* spriteA.remove();
* spriteB.score++;
* }
*
* @method bounce
* @param {Object} target Group or Sprite to check against the current one
* @param {Function} [callback] The function to be called if overlap is positive
* @return {Boolean} True if overlapping
*/
array.bounce = _groupCollide.bind(array, 'bounce');
return array;
}
p5.prototype.Group = Group;
//circle collider - used internally
function CircleCollider(pInst, _center, _radius, _offset) {
var pInstBind = createPInstBinder(pInst);
var createVector = pInstBind('createVector');
var CENTER = p5.prototype.CENTER;
this.center = _center;
this.radius = _radius;
this.originalRadius = _radius;
if (_offset === undefined)
this.offset = createVector(0, 0);
else
this.offset = _offset;
this.extents = createVector(_radius * 2, _radius * 2);
this.draw = function () {
pInst.noFill();
pInst.stroke(0, 255, 0);
pInst.rectMode(CENTER);
pInst.ellipse(this.center.x + this.offset.x, this.center.y + this.offset.y, this.radius * 2, this.radius * 2);
};
//should be called only for circle vs circle
this.overlap = function (other) {
//square dist
var r = this.radius + other.radius;
r *= r;
var thisCenterX = this.center.x + this.offset.x;
var thisCenterY = this.center.y + this.offset.y;
var otherCenterX = other.center.x + other.offset.x;
var otherCenterY = other.center.y + other.offset.y;
var sqDist = pow(thisCenterX - otherCenterX, 2) + pow(thisCenterY - otherCenterY, 2);
return r > sqDist;
};
//should be called only for circle vs circle
this.collide = function (other) {
if (this.overlap(other)) {
var thisCenterX = this.center.x + this.offset.x;
var thisCenterY = this.center.y + this.offset.y;
var otherCenterX = other.center.x + other.offset.x;
var otherCenterY = other.center.y + other.offset.y;
var a = pInst.atan2(thisCenterY - otherCenterY, thisCenterX - otherCenterX);
var radii = this.radius + other.radius;
var intersection = abs(radii - dist(thisCenterX, thisCenterY, otherCenterX, otherCenterY));
var displacement = createVector(pInst.cos(a) * intersection, pInst.sin(a) * intersection);
return displacement;
} else {
return createVector(0, 0);
}
};
this.size = function () {
return createVector(this.radius * 2, this.radius * 2);
};
this.left = function () {
return this.center.x + this.offset.x - this.radius;
};
this.right = function () {
return this.center.x + this.offset.x + this.radius;
};
this.top = function () {
return this.center.y + this.offset.y - this.radius;
};
this.bottom = function () {
return this.center.y + this.offset.y + this.radius;
};
}
defineLazyP5Property('CircleCollider', boundConstructorFactory(CircleCollider));
//axis aligned bounding box - extents are the half sizes - used internally
function AABB(pInst, _center, _extents, _offset) {
var pInstBind = createPInstBinder(pInst);
var createVector = pInstBind('createVector');
var CENTER = p5.prototype.CENTER;
var PI = p5.prototype.PI;
this.center = _center;
this.extents = _extents;
this.originalExtents = _extents.copy();
if (_offset === undefined)
this.offset = createVector(0, 0);
else
this.offset = _offset;
this.min = function () {
return createVector(this.center.x + this.offset.x - this.extents.x, this.center.y + this.offset.y - this.extents.y);
};
this.max = function () {
return createVector(this.center.x + this.offset.x + this.extents.x, this.center.y + this.offset.y + this.extents.y);
};
this.right = function () {
return this.center.x + this.offset.x + this.extents.x / 2;
};
this.left = function () {
return this.center.x + this.offset.x - this.extents.x / 2;
};
this.top = function () {
return this.center.y + this.offset.y - this.extents.y / 2;
};
this.bottom = function () {
return this.center.y + this.offset.y + this.extents.y / 2;
};
this.size = function () {
return createVector(this.extents.x * 2, this.extents.y * 2);
};
this.rotate = function (r) {
//rotate the bbox
var t;
if (pInst._angleMode === pInst.RADIANS) {
t = radians(r);
} else {
t = r;
}
var w2 = this.extents.x * abs(pInst.cos(t)) + this.extents.y * abs(pInst.sin(t));
var h2 = this.extents.x * abs(pInst.sin(t)) + this.extents.y * abs(pInst.cos(t));
this.extents.x = w2;
this.extents.y = h2;
};
this.draw = function () {
//fill(col);
pInst.noFill();
pInst.stroke(0, 255, 0);
pInst.rectMode(CENTER);
pInst.rect(this.center.x + this.offset.x, this.center.y + this.offset.y, this.size().x / 2, this.size().y / 2);
};
this.overlap = function (other) {
//box vs box
if (other instanceof AABB) {
var md = other.minkowskiDifference(this);
if (md.min().x <= 0 &&
md.max().x >= 0 &&
md.min().y <= 0 &&
md.max().y >= 0) {
return true;
}
else
return false;
}
//box vs circle
else if (other instanceof CircleCollider) {
//find closest point to the circle on the box
var pt = createVector(other.center.x, other.center.y);
//I don't know what's going o try to trace a line from centers to see
if (other.center.x < this.left())
pt.x = this.left();
else if (other.center.x > this.right())
pt.x = this.right();
if (other.center.y < this.top())
pt.y = this.top();
else if (other.center.y > this.bottom())
pt.y = this.bottom();
var distance = pt.dist(other.center);
return distance < other.radius;
}
};
this.collide = function (other) {
if (other instanceof AABB) {
var md = other.minkowskiDifference(this);
if (md.min().x <= 0 &&
md.max().x >= 0 &&
md.min().y <= 0 &&
md.max().y >= 0) {
var boundsPoint = md.closestPointOnBoundsToPoint(createVector(0, 0));
return boundsPoint;
}
else
return createVector(0, 0);
}
//box vs circle
else if (other instanceof CircleCollider) {
//find closest point to the circle on the box
var pt = createVector(other.center.x, other.center.y);
//I don't know what's going o try to trace a line from centers to see
if (other.center.x < this.left())
pt.x = this.left();
else if (other.center.x > this.right())
pt.x = this.right();
if (other.center.y < this.top())
pt.y = this.top();
else if (other.center.y > this.bottom())
pt.y = this.bottom();
var distance = pt.dist(other.center);
var a;
if (distance < other.radius) {
//reclamp point
if (pt.x === other.center.x && pt.y === other.center.y) {
var xOverlap = pt.x - this.center.x;
var yOverlap = pt.y - this.center.y;
if (abs(xOverlap) < abs(yOverlap)) {
if (xOverlap > 0)
pt.x = this.right();
else
pt.x = this.left();
}
else {
if (yOverlap < 0)
pt.y = this.top();
else
pt.y = this.bottom();
}
a = pInst.atan2(other.center.y - pt.y, other.center.x - pt.x);
//fix exceptions
if (a === 0) {
if (pt.x === this.right()) a = PI;
if (pt.y === this.top()) a = PI / 2;
if (pt.y === this.bottom()) a = -PI / 2;
}
}
else {
//angle bw point and center
a = pInst.atan2(pt.y - other.center.y, pt.x - other.center.x);
//project the normal (line between pt and center) onto the circle
}
var d = createVector(pt.x - other.center.x, pt.y - other.center.y);
var displacement = createVector(pInst.cos(a) * other.radius - d.x, pInst.sin(a) * other.radius - d.y);
//if(pt.x === other.center.x && pt.y === other.center.y)
//displacement = displacement.mult(-1);
return displacement;
//return createVector(0,0);
}
else
return createVector(0, 0);
}
};
this.minkowskiDifference = function (other) {
var topLeft = this.min().sub(other.max());
var fullSize = this.size().add(other.size());
return new AABB(pInst, topLeft.add(fullSize.div(2)), fullSize.div(2));
};
this.closestPointOnBoundsToPoint = function (point) {
// test x first
var minDist = abs(point.x - this.min().x);
var boundsPoint = createVector(this.min().x, point.y);
if (abs(this.max().x - point.x) < minDist) {
minDist = abs(this.max().x - point.x);
boundsPoint = createVector(this.max().x, point.y);
}
if (abs(this.max().y - point.y) < minDist) {
minDist = abs(this.max().y - point.y);
boundsPoint = createVector(point.x, this.max().y);
}
if (abs(this.min().y - point.y) < minDist) {
minDist = abs(this.min.y - point.y);
boundsPoint = createVector(point.x, this.min().y);
}
return boundsPoint;
};
}//end AABB
defineLazyP5Property('AABB', boundConstructorFactory(AABB));
/**
* An Animation object contains a series of images (p5.Image) that
* can be displayed sequentially.
*
* All files must be png images. You must include the directory from the sketch root,
* and the extension .png
*
* A sprite can have multiple labeled animations, see Sprite.addAnimation
* and Sprite.changeAnimation, however an animation can be used independently.
*
* An animation can be created either by passing a series of file names,
* no matter how many or by passing the first and the last file name
* of a numbered sequence.
* p5.play will try to detect the sequence pattern.
*
* For example if the given filenames are
* "data/file0001.png" and "data/file0005.png" the images
* "data/file0003.png" and "data/file0004.png" will be loaded as well.
*
* @example
* var sequenceAnimation;
* var glitch;
*
* function preload() {
* sequenceAnimation = loadAnimation("data/walking0001.png", "data/walking0005.png");
* glitch = loadAnimation("data/dog.png", "data/horse.png", "data/cat.png", "data/snake.png");
* }
*
* function setup() {
* createCanvas(800, 600);
* }
*
* function draw() {
* background(0);
* animation(sequenceAnimation, 100, 100);
* animation(glitch, 200, 100);
* }
*
* @class Animation
* @constructor
* @param {String} fileName1 First file in a sequence OR first image file
* @param {String} fileName2 Last file in a sequence OR second image file
* @param {String} [...fileNameN] Any number of image files after the first two
*/
function Animation(pInst) {
var frameArguments = Array.prototype.slice.call(arguments, 1);
var i;
var CENTER = p5.prototype.CENTER;
/**
* Array of frames (p5.Image)
*
* @property images
* @type {Array}
*/
this.images = [];
var frame = 0;
var cycles = 0;
var targetFrame = -1;
this.offX = 0;
this.offY = 0;
/**
* Delay between frames in number of draw cycles.
* If set to 4 the framerate of the animation would be the
* sketch framerate divided by 4 (60fps = 15fps)
*
* @property frameDelay
* @type {Number}
* @default 4
*/
this.frameDelay = 4;
/**
* True if the animation is currently playing.
*
* @property playing
* @type {Boolean}
* @default true
*/
this.playing = true;
/**
* Animation visibility.
*
* @property visible
* @type {Boolean}
* @default true
*/
this.visible = true;
/**
* If set to false the animation will stop after reaching the last frame
*
* @property looping
* @type {Boolean}
* @default true
*/
this.looping = true;
/**
* True if frame changed during the last draw cycle
*
* @property frameChanged
* @type {Boolean}
*/
this.frameChanged = false;
//is the collider defined manually or defined
//by the current frame size
this.imageCollider = false;
//sequence mode
if (frameArguments.length === 2 && typeof frameArguments[0] === 'string' && typeof frameArguments[1] === 'string') {
var from = frameArguments[0];
var to = frameArguments[1];
//print("sequence mode "+from+" -> "+to);
//make sure the extensions are fine
var ext1 = from.substring(from.length - 4, from.length);
if (ext1 !== '.png') {
pInst.print('Animation error: you need to use .png files (filename ' + from + ')');
from = -1;
}
var ext2 = to.substring(to.length - 4, to.length);
if (ext2 !== '.png') {
pInst.print('Animation error: you need to use .png files (filename ' + to + ')');
to = -1;
}
//extensions are fine
if (from !== -1 && to !== -1) {
var digits1 = 0;
var digits2 = 0;
//skip extension work backwards to find the numbers
for (i = from.length - 5; i >= 0; i--) {
if (from.charAt(i) >= '0' && from.charAt(i) <= '9')
digits1++;
}
for (i = to.length - 5; i >= 0; i--) {
if (to.charAt(i) >= '0' && to.charAt(i) <= '9')
digits2++;
}
var prefix1 = from.substring(0, from.length - (4 + digits1));
var prefix2 = to.substring(0, to.length - (4 + digits2));
// Our numbers likely have leading zeroes, which means that some
// browsers (e.g., PhantomJS) will interpret them as base 8 (octal)
// instead of decimal. To fix this, we'll explicity tell parseInt to
// use a base of 10 (decimal). For more details on this issue, see
// http://stackoverflow.com/a/8763427/2422398.
var number1 = parseInt(from.substring(from.length - (4 + digits1), from.length - 4), 10);
var number2 = parseInt(to.substring(to.length - (4 + digits2), to.length - 4), 10);
//swap if inverted
if (number2 < number1) {
var t = number2;
number2 = number1;
number1 = t;
}
//two different frames
if (prefix1 !== prefix2) {
//print("2 separate images");
this.images.push(pInst.loadImage(from));
this.images.push(pInst.loadImage(to));
}
//same digits: case img0001, img0002
else {
var fileName;
if (digits1 === digits2) {
//load all images
for (i = number1; i <= number2; i++) {
// Use nf() to number format 'i' into four digits
fileName = prefix1 + pInst.nf(i, digits1) + '.png';
this.images.push(pInst.loadImage(fileName));
}
}
else //case: case img1, img2
{
//print("from "+prefix1+" "+number1 +" to "+number2);
for (i = number1; i <= number2; i++) {
// Use nf() to number format 'i' into four digits
fileName = prefix1 + i + '.png';
this.images.push(pInst.loadImage(fileName));
}
}
}
}//end no ext error
}//end sequence mode
// Sprite sheet mode
else if (frameArguments.length === 1 && (frameArguments[0] instanceof SpriteSheet)) {
this.spriteSheet = frameArguments[0];
this.images = this.spriteSheet.frames;
}
else if (frameArguments.length !== 0)//arbitrary list of images
{
//print("Animation arbitrary mode");
for (i = 0; i < frameArguments.length; i++) {
//print("loading "+fileNames[i]);
if (frameArguments[i] instanceof p5.Image)
this.images.push(frameArguments[i]);
else
this.images.push(pInst.loadImage(frameArguments[i]));
}
}
/**
* Objects are passed by reference so to have different sprites
* using the same animation you need to clone it.
*
* @method clone
* @return {Animation} A clone of the current animation
*/
this.clone = function () {
var myClone = new Animation(pInst); //empty
myClone.images = [];
if (this.spriteSheet) {
myClone.spriteSheet = this.spriteSheet.clone();
}
myClone.images = this.images.slice();
myClone.offX = this.offX;
myClone.offY = this.offY;
myClone.frameDelay = this.frameDelay;
myClone.playing = this.playing;
myClone.looping = this.looping;
return myClone;
};
/**
* Draws the animation at coordinate x and y.
* Updates the frames automatically.
*
* @method draw
* @param {Number} x x coordinate
* @param {Number} y y coordinate
* @param {Number} [r=0] rotation
*/
this.draw = function (x, y, r) {
this.xpos = x;
this.ypos = y;
this.rotation = r || 0;
if (this.visible) {
//only connection with the sprite class
//if animation is used independently draw and update are the sam
if (!this.isSpriteAnimation)
this.update();
//this.currentImageMode = g.imageMode;
pInst.push();
pInst.imageMode(CENTER);
pInst.translate(this.xpos, this.ypos);
if (pInst._angleMode === pInst.RADIANS) {
pInst.rotate(radians(this.rotation));
} else {
pInst.rotate(this.rotation);
}
if (this.images[frame] !== undefined) {
if (this.spriteSheet) {
var frame_info = this.images[frame].frame;
//pInst.image(this.spriteSheet.image, frame_info.x, frame_info.y, frame_info.width,
// frame_info.height, this.offX, this.offY, frame_info.width, frame_info.height);
pInst.image(this.spriteSheet.image, this.offX, this.offY, frame_info.width, frame_info.height, frame_info.x, frame_info.y, frame_info.width, frame_info.height);
} else {
pInst.image(this.images[frame], this.offX, this.offY);
}
}
else {
pInst.print('Warning undefined frame ' + frame);
//this.isActive = false;
}
pInst.pop();
}
};
//called by draw
this.update = function () {
cycles++;
var previousFrame = frame;
this.frameChanged = false;
//go to frame
if (this.images.length === 1) {
this.playing = false;
frame = 0;
}
if (this.playing && cycles % this.frameDelay === 0) {
//going to target frame up
if (targetFrame > frame && targetFrame !== -1) {
frame++;
}
//going to taget frame down
else if (targetFrame < frame && targetFrame !== -1) {
frame--;
}
else if (targetFrame === frame && targetFrame !== -1) {
this.playing = false;
}
else if (this.looping) //advance frame
{
//if next frame is too high
if (frame >= this.images.length - 1)
frame = 0;
else
frame++;
} else {
//if next frame is too high
if (frame < this.images.length - 1)
frame++;
}
}
if (previousFrame !== frame)
this.frameChanged = true;
};//end update
/**
* Plays the animation.
*
* @method play
*/
this.play = function () {
this.playing = true;
targetFrame = -1;
};
/**
* Stops the animation.
*
* @method stop
*/
this.stop = function () {
this.playing = false;
};
/**
* Rewinds the animation to the first frame.
*
* @method rewind
*/
this.rewind = function () {
frame = 0;
};
/**
* Changes the current frame.
*
* @method changeFrame
* @param {Number} frame Frame number (starts from 0).
*/
this.changeFrame = function (f) {
if (f < this.images.length)
frame = f;
else
frame = this.images.length - 1;
targetFrame = -1;
//this.playing = false;
};
/**
* Goes to the next frame and stops.
*
* @method nextFrame
*/
this.nextFrame = function () {
if (frame < this.images.length - 1)
frame = frame + 1;
else if (this.looping)
frame = 0;
targetFrame = -1;
this.playing = false;
};
/**
* Goes to the previous frame and stops.
*
* @method previousFrame
*/
this.previousFrame = function () {
if (frame > 0)
frame = frame - 1;
else if (this.looping)
frame = this.images.length - 1;
targetFrame = -1;
this.playing = false;
};
/**
* Plays the animation forward or backward toward a target frame.
*
* @method goToFrame
* @param {Number} toFrame Frame number destination (starts from 0)
*/
this.goToFrame = function (toFrame) {
if (toFrame < 0 || toFrame >= this.images.length) {
return;
}
// targetFrame gets used by the update() method to decide what frame to
// select next. When it's not being used it gets set to -1.
targetFrame = toFrame;
if (targetFrame !== frame) {
this.playing = true;
}
};
/**
* Returns the current frame number.
*
* @method getFrame
* @return {Number} Current frame (starts from 0)
*/
this.getFrame = function () {
return frame;
};
/**
* Returns the last frame number.
*
* @method getLastFrame
* @return {Number} Last frame number (starts from 0)
*/
this.getLastFrame = function () {
return this.images.length - 1;
};
/**
* Returns the current frame image as p5.Image.
*
* @method getFrameImage
* @return {p5.Image} Current frame image
*/
this.getFrameImage = function () {
return this.images[frame];
};
/**
* Returns the frame image at the specified frame number.
*
* @method getImageAt
* @param {Number} frame Frame number
* @return {p5.Image} Frame image
*/
this.getImageAt = function (f) {
return this.images[f];
};
/**
* Returns the current frame width in pixels.
* If there is no image loaded, returns 1.
*
* @method getWidth
* @return {Number} Frame width
*/
this.getWidth = function () {
if (this.images[frame] instanceof p5.Image) {
return this.images[frame].width;
} else if (this.images[frame]) {
// Special case: Animation-from-spritesheet treats its images array differently.
return this.images[frame].frame.width;
} else {
return 1;
}
};
/**
* Returns the current frame height in pixels.
* If there is no image loaded, returns 1.
*
* @method getHeight
* @return {Number} Frame height
*/
this.getHeight = function () {
if (this.images[frame] instanceof p5.Image) {
return this.images[frame].height;
} else if (this.images[frame]) {
// Special case: Animation-from-spritesheet treats its images array differently.
return this.images[frame].frame.height;
} else {
return 1;
}
};
}
defineLazyP5Property('Animation', boundConstructorFactory(Animation));
/**
* Represents a sprite sheet and all it's frames. To be used with Animation,
* or static drawing single frames.
*
* There are two different ways to load a SpriteSheet
*
* 1. Given width, height that will be used for every frame and the
* number of frames to cycle through. The sprite sheet must have a
* uniform grid with consistent rows and columns.
*
* 2. Given an array of frame objects that define the position and
* dimensions of each frame. This is Flexible because you can use
* sprite sheets that don't have uniform rows and columns.
*
* @example
* // Method 1 - Using width, height for each frame and number of frames
* explode_sprite_sheet = loadSpriteSheet('assets/explode_sprite_sheet.png', 171, 158, 11);
*
* // Method 2 - Using an array of objects that define each frame
* var player_frames = loadJSON('assets/tiles.json');
* player_sprite_sheet = loadSpriteSheet('assets/player_spritesheet.png', player_frames);
*
* @class SpriteSheet
* @constructor
* @param image String image path or p5.Image object
*/
function SpriteSheet(pInst) {
var spriteSheetArgs = Array.prototype.slice.call(arguments, 1);
this.image = null;
this.frames = [];
this.frame_width = 0;
this.frame_height = 0;
this.num_frames = 0;
/**
* Generate the frames data for this sprite sheet baesd on user params
* @private
* @method _generateSheetFrames
*/
this._generateSheetFrames = function () {
var sX = 0, sY = 0;
for (var i = 0; i < this.num_frames; i++) {
this.frames.push(
{
'name': i,
'frame': {
'x': sX,
'y': sY,
'width': this.frame_width,
'height': this.frame_height
}
});
sX += this.frame_width;
if (sX >= this.image.width) {
sX = 0;
sY += this.frame_height;
if (sY >= this.image.height) {
sY = 0;
}
}
}
};
if (spriteSheetArgs.length === 2 && Array.isArray(spriteSheetArgs[1])) {
this.frames = spriteSheetArgs[1];
this.num_frames = this.frames.length;
} else if (spriteSheetArgs.length === 4 &&
(typeof spriteSheetArgs[1] === 'number') &&
(typeof spriteSheetArgs[2] === 'number') &&
(typeof spriteSheetArgs[3] === 'number')) {
this.frame_width = spriteSheetArgs[1];
this.frame_height = spriteSheetArgs[2];
this.num_frames = spriteSheetArgs[3];
}
if (spriteSheetArgs[0] instanceof p5.Image) {
this.image = spriteSheetArgs[0];
if (spriteSheetArgs.length === 4) {
this._generateSheetFrames();
}
} else {
if (spriteSheetArgs.length === 2) {
this.image = pInst.loadImage(spriteSheetArgs[0]);
} else if (spriteSheetArgs.length === 4) {
this.image = pInst.loadImage(spriteSheetArgs[0], this._generateSheetFrames.bind(this));
}
}
/**
* Draws a specific frame to the canvas.
* @param frame_name Can either be a string name, or a numeric index.
* @param x x position to draw the frame at
* @param y y position to draw the frame at
* @param [width] optional width to draw the frame
* @param [height] optional height to draw the frame
* @method drawFrame
*/
this.drawFrame = function (frame_name, x, y, width, height) {
var frameToDraw;
if (typeof frame_name === 'number') {
frameToDraw = this.frames[frame_name].frame;
} else {
for (var i = 0; i < this.frames.length; i++) {
if (this.frames[i].name === frame_name) {
frameToDraw = this.frames[i].frame;
break;
}
}
}
var dWidth = width || frameToDraw.width;
var dHeight = height || frameToDraw.height;
pInst.image(this.image, frameToDraw.x, frameToDraw.y,
frameToDraw.width, frameToDraw.height, x, y, dWidth, dHeight);
//pInst.image(this.image, x, y,
// frameToDraw.width, frameToDraw.height - 1, frameToDraw.x, frameToDraw.y, dWidth, dHeight);
};
/**
* Objects are passed by reference so to have different sprites
* using the same animation you need to clone it.
*
* @method clone
* @return {SpriteSheet} A clone of the current SpriteSheet
*/
this.clone = function () {
var myClone = new SpriteSheet(pInst); //empty
// Deep clone the frames by value not reference
for (var i = 0; i < this.frames.length; i++) {
var frame = this.frames[i].frame;
var cloneFrame = {
'name': frame.name,
'frame': {
'x': frame.x,
'y': frame.y,
'width': frame.width,
'height': frame.height
}
};
myClone.frames.push(cloneFrame);
}
// clone other fields
myClone.image = this.image;
myClone.frame_width = this.frame_width;
myClone.frame_height = this.frame_height;
myClone.num_frames = this.num_frames;
return myClone;
};
}
defineLazyP5Property('SpriteSheet', boundConstructorFactory(SpriteSheet));
//general constructor to be able to feed arguments as array
function construct(constructor, args) {
function F() {
return constructor.apply(this, args);
}
F.prototype = constructor.prototype;
return new F();
}
/*
* Javascript Quadtree
* based on
* https://github.com/timohausmann/quadtree-js/
* Copyright © 2012 Timo Hausmann
*/
function Quadtree(bounds, max_objects, max_levels, level) {
this.active = true;
this.max_objects = max_objects || 10;
this.max_levels = max_levels || 4;
this.level = level || 0;
this.bounds = bounds;
this.objects = [];
this.object_refs = [];
this.nodes = [];
}
Quadtree.prototype.updateBounds = function () {
//find maximum area
var objects = this.getAll();
var x = 10000;
var y = 10000;
var w = -10000;
var h = -10000;
for (var i = 0; i < objects.length; i++) {
if (objects[i].position.x < x)
x = objects[i].position.x;
if (objects[i].position.y < y)
y = objects[i].position.y;
if (objects[i].position.x > w)
w = objects[i].position.x;
if (objects[i].position.y > h)
h = objects[i].position.y;
}
this.bounds = {
x: x,
y: y,
width: w,
height: h
};
//print(this.bounds);
};
/*
* Split the node into 4 subnodes
*/
Quadtree.prototype.split = function () {
var nextLevel = this.level + 1,
subWidth = Math.round(this.bounds.width / 2),
subHeight = Math.round(this.bounds.height / 2),
x = Math.round(this.bounds.x),
y = Math.round(this.bounds.y);
//top right node
this.nodes[0] = new Quadtree({
x: x + subWidth,
y: y,
width: subWidth,
height: subHeight
}, this.max_objects, this.max_levels, nextLevel);
//top left node
this.nodes[1] = new Quadtree({
x: x,
y: y,
width: subWidth,
height: subHeight
}, this.max_objects, this.max_levels, nextLevel);
//bottom left node
this.nodes[2] = new Quadtree({
x: x,
y: y + subHeight,
width: subWidth,
height: subHeight
}, this.max_objects, this.max_levels, nextLevel);
//bottom right node
this.nodes[3] = new Quadtree({
x: x + subWidth,
y: y + subHeight,
width: subWidth,
height: subHeight
}, this.max_objects, this.max_levels, nextLevel);
};
/*
* Determine the quadtrant for an area in this node
*/
Quadtree.prototype.getIndex = function (pRect) {
if (!pRect.collider)
return -1;
else {
var index = -1,
verticalMidpoint = this.bounds.x + (this.bounds.width / 2),
horizontalMidpoint = this.bounds.y + (this.bounds.height / 2),
//pRect can completely fit within the top quadrants
topQuadrant = (pRect.collider.top() < horizontalMidpoint && pRect.collider.top() + pRect.collider.size().y < horizontalMidpoint),
//pRect can completely fit within the bottom quadrants
bottomQuadrant = (pRect.collider.top() > horizontalMidpoint);
//pRect can completely fit within the left quadrants
if (pRect.collider.left() < verticalMidpoint && pRect.collider.left() + pRect.collider.size().x < verticalMidpoint) {
if (topQuadrant) {
index = 1;
} else if (bottomQuadrant) {
index = 2;
}
//pRect can completely fit within the right quadrants
} else if (pRect.collider.left() > verticalMidpoint) {
if (topQuadrant) {
index = 0;
} else if (bottomQuadrant) {
index = 3;
}
}
return index;
}
};
/*
* Insert an object into the node. If the node
* exceeds the capacity, it will split and add all
* objects to their corresponding subnodes.
*/
Quadtree.prototype.insert = function (obj) {
//avoid double insertion
if (this.objects.indexOf(obj) === -1) {
var i = 0,
index;
//if we have subnodes ...
if (typeof this.nodes[0] !== 'undefined') {
index = this.getIndex(obj);
if (index !== -1) {
this.nodes[index].insert(obj);
return;
}
}
this.objects.push(obj);
if (this.objects.length > this.max_objects && this.level < this.max_levels) {
//split if we don't already have subnodes
if (typeof this.nodes[0] === 'undefined') {
this.split();
}
//add all objects to there corresponding subnodes
while (i < this.objects.length) {
index = this.getIndex(this.objects[i]);
if (index !== -1) {
this.nodes[index].insert(this.objects.splice(i, 1)[0]);
} else {
i = i + 1;
}
}
}
}
};
/*
* Return all objects that could collide with a given area
*/
Quadtree.prototype.retrieve = function (pRect) {
var index = this.getIndex(pRect),
returnObjects = this.objects;
//if we have subnodes ...
if (typeof this.nodes[0] !== 'undefined') {
//if pRect fits into a subnode ..
if (index !== -1) {
returnObjects = returnObjects.concat(this.nodes[index].retrieve(pRect));
//if pRect does not fit into a subnode, check it against all subnodes
} else {
for (var i = 0; i < this.nodes.length; i = i + 1) {
returnObjects = returnObjects.concat(this.nodes[i].retrieve(pRect));
}
}
}
return returnObjects;
};
Quadtree.prototype.retrieveFromGroup = function (pRect, group) {
var results = [];
var candidates = this.retrieve(pRect);
for (var i = 0; i < candidates.length; i++)
if (group.contains(candidates[i]))
results.push(candidates[i]);
return results;
};
/*
* Get all objects stored in the quadtree
*/
Quadtree.prototype.getAll = function () {
var objects = this.objects;
for (var i = 0; i < this.nodes.length; i = i + 1) {
objects = objects.concat(this.nodes[i].getAll());
}
return objects;
};
/*
* Get the node in which a certain object is stored
*/
Quadtree.prototype.getObjectNode = function (obj) {
var index;
//if there are no subnodes, object must be here
if (!this.nodes.length) {
return this;
} else {
index = this.getIndex(obj);
//if the object does not fit into a subnode, it must be here
if (index === -1) {
return this;
//if it fits into a subnode, continue deeper search there
} else {
var node = this.nodes[index].getObjectNode(obj);
if (node) return node;
}
}
return false;
};
/*
* Removes a specific object from the quadtree
* Does not delete empty subnodes. See cleanup-function
*/
Quadtree.prototype.removeObject = function (obj) {
var node = this.getObjectNode(obj),
index = node.objects.indexOf(obj);
if (index === -1) return false;
node.objects.splice(index, 1);
};
/*
* Clear the quadtree and delete all objects
*/
Quadtree.prototype.clear = function () {
this.objects = [];
if (!this.nodes.length) return;
for (var i = 0; i < this.nodes.length; i = i + 1) {
this.nodes[i].clear();
}
this.nodes = [];
};
/*
* Clean up the quadtree
* Like clear, but objects won't be deleted but re-inserted
*/
Quadtree.prototype.cleanup = function () {
var objects = this.getAll();
this.clear();
for (var i = 0; i < objects.length; i++) {
this.insert(objects[i]);
}
};
function updateTree() {
if (this.quadTree.active) {
this.quadTree.updateBounds();
this.quadTree.cleanup();
}
}
//keyboard input
p5.prototype.registerMethod('pre', p5.prototype.readPresses);
//automatic sprite update
p5.prototype.registerMethod('pre', p5.prototype.updateSprites);
//quadtree update
p5.prototype.registerMethod('post', updateTree);
//camera push and pop
p5.prototype.registerMethod('pre', cameraPush);
p5.prototype.registerMethod('post', cameraPop);
//deltaTime
//p5.prototype.registerMethod('pre', updateDelta);
/**
* Log a warning message to the host console, using native `console.warn`
* if it is available but falling back on `console.log` if not. If no
* console is available, this method will fail silently.
* @method _warn
* @param {!string} message
* @private
*/
p5.prototype._warn = function (message) {
var console = window.console;
if (console) {
if ('function' === typeof console.warn) {
console.warn(message);
}
else if ('function' === typeof console.log) {
console.log('Warning: ' + message);
}
}
};
}));