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.
4346 lines
116 KiB
JavaScript
4346 lines
116 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 === 'rectangle' && !(arguments.length === 1 || arguments.length === 5)) {
|
|
throw new TypeError('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 anymation would be the
|
|
* sketch framerate divided by 4 (60fps = 15fps)
|
|
*
|
|
* @property frameDelay
|
|
* @type {Number}
|
|
* @default 2
|
|
*/
|
|
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);
|
|
} 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);
|
|
};
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
}
|
|
};
|
|
|
|
}));
|