/*
p5.play
by Paolo Pedercini/molleindustria, 2015
http://molleindustria.org/
*/
(function(root, factory) {
if (typeof define === 'function' && define.amd)
define('p5.play', ['@code-dot-org/p5'], function(p5) { (factory(p5)); });
else if (typeof exports === 'object')
factory(require('@code-dot-org/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
// =============================================================================
var DEFAULT_FRAME_RATE = 30;
// 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.angleMode(this.DEGREES);
this.frameRate(DEFAULT_FRAME_RATE);
this._defaultCanvasSize = {
width: 400,
height: 400
};
var startDate = new Date();
this._startTime = startDate.getTime();
// Temporary canvas for supporting tint operations from image elements;
// see p5.prototype.imageElement()
this._tempCanvas = document.createElement('canvas');
});
// 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 degrees = p5.prototype.degrees;
// =============================================================================
// p5 overrides
// =============================================================================
// Make the fill color default to gray (127, 127, 127) each time a new canvas is
// created.
if (!p5.prototype.originalCreateCanvas_) {
p5.prototype.originalCreateCanvas_ = p5.prototype.createCanvas;
p5.prototype.createCanvas = function() {
var result = this.originalCreateCanvas_.apply(this, arguments);
this.fill(this.color(127, 127, 127));
return result;
};
}
// Make width and height optional for ellipse() - default to 50
// Save the original implementation to allow for optional parameters.
if (!p5.prototype.originalEllipse_) {
p5.prototype.originalEllipse_ = p5.prototype.ellipse;
p5.prototype.ellipse = function(x, y, w, h) {
w = (w === undefined) ? 50 : w;
h = (h === undefined) ? w : h;
this.originalEllipse_(x, y, w, h);
};
}
// Make width and height optional for rect() - default to 50
// Save the original implementation to allow for optional parameters.
if (!p5.prototype.originalRect_) {
p5.prototype.originalRect_ = p5.prototype.rect;
p5.prototype.rect = function(x, y, w, h, tl, tr, br, bl) {
w = (w === undefined) ? 50 : w;
h = (h === undefined) ? w : h;
this.originalRect_(x, y, w, h, tl, tr, br, bl);
};
}
// Modify p5 to ignore out-of-bounds positions before setting touchIsDown
p5.prototype._ontouchstart = function(e) {
if (!this._curElement) {
return;
}
var validTouch;
for (var i = 0; i < e.touches.length; i++) {
validTouch = getTouchInfo(this._curElement.elt, e, i);
if (validTouch) {
break;
}
}
if (!validTouch) {
// No in-bounds (valid) touches, return and ignore:
return;
}
var context = this._isGlobal ? window : this;
var executeDefault;
this._updateNextTouchCoords(e);
this._updateNextMouseCoords(e);
this._setProperty('touchIsDown', true);
if (typeof context.touchStarted === 'function') {
executeDefault = context.touchStarted(e);
if (executeDefault === false) {
e.preventDefault();
}
} else if (typeof context.mousePressed === 'function') {
executeDefault = context.mousePressed(e);
if (executeDefault === false) {
e.preventDefault();
}
//this._setMouseButton(e);
}
};
// Modify p5 to handle CSS transforms (scale) and ignore out-of-bounds
// positions before reporting touch coordinates
//
// NOTE: _updateNextTouchCoords() is nearly identical, but calls a modified
// getTouchInfo() function below that scales the touch postion with the play
// space and can return undefined
p5.prototype._updateNextTouchCoords = function(e) {
var x = this.touchX;
var y = this.touchY;
if (e.type === 'mousedown' || e.type === 'mousemove' ||
e.type === 'mouseup' || !e.touches) {
x = this.mouseX;
y = this.mouseY;
} else {
if (this._curElement !== null) {
var touchInfo = getTouchInfo(this._curElement.elt, e, 0);
if (touchInfo) {
x = touchInfo.x;
y = touchInfo.y;
}
var touches = [];
var touchIndex = 0;
for (var i = 0; i < e.touches.length; i++) {
// Only some touches are valid - only push valid touches into the
// array for the `touches` property.
touchInfo = getTouchInfo(this._curElement.elt, e, i);
if (touchInfo) {
touches[touchIndex] = touchInfo;
touchIndex++;
}
}
this._setProperty('touches', touches);
}
}
this._setProperty('touchX', x);
this._setProperty('touchY', y);
if (!this._hasTouchInteracted) {
// For first draw, make previous and next equal
this._updateTouchCoords();
this._setProperty('_hasTouchInteracted', true);
}
};
// NOTE: returns undefined if the position is outside of the valid range
function getTouchInfo(canvas, e, i) {
i = i || 0;
var rect = canvas.getBoundingClientRect();
var touch = e.touches[i] || e.changedTouches[i];
var xPos = touch.clientX - rect.left;
var yPos = touch.clientY - rect.top;
if (xPos >= 0 && xPos < rect.width && yPos >= 0 && yPos < rect.height) {
return {
x: Math.round(xPos * canvas.offsetWidth / rect.width),
y: Math.round(yPos * canvas.offsetHeight / rect.height),
id: touch.identifier
};
}
}
// Modify p5 to ignore out-of-bounds positions before setting mouseIsPressed
// and isMousePressed
p5.prototype._onmousedown = function(e) {
if (!this._curElement) {
return;
}
if (!getMousePos(this._curElement.elt, e)) {
// Not in-bounds, return and ignore:
return;
}
var context = this._isGlobal ? window : this;
var executeDefault;
this._setProperty('isMousePressed', true);
this._setProperty('mouseIsPressed', true);
this._setMouseButton(e);
this._updateNextMouseCoords(e);
this._updateNextTouchCoords(e);
if (typeof context.mousePressed === 'function') {
executeDefault = context.mousePressed(e);
if (executeDefault === false) {
e.preventDefault();
}
} else if (typeof context.touchStarted === 'function') {
executeDefault = context.touchStarted(e);
if (executeDefault === false) {
e.preventDefault();
}
}
};
// Modify p5 to handle CSS transforms (scale) and ignore out-of-bounds
// positions before reporting mouse coordinates
//
// NOTE: _updateNextMouseCoords() is nearly identical, but calls a modified
// getMousePos() function below that scales the mouse position with the play
// space and can return undefined.
p5.prototype._updateNextMouseCoords = function(e) {
var x = this.mouseX;
var y = this.mouseY;
if (e.type === 'touchstart' || e.type === 'touchmove' ||
e.type === 'touchend' || e.touches) {
x = this.touchX;
y = this.touchY;
} else if (this._curElement !== null) {
var mousePos = getMousePos(this._curElement.elt, e);
if (mousePos) {
x = mousePos.x;
y = mousePos.y;
}
}
this._setProperty('mouseX', x);
this._setProperty('mouseY', y);
this._setProperty('winMouseX', e.pageX);
this._setProperty('winMouseY', e.pageY);
if (!this._hasMouseInteracted) {
// For first draw, make previous and next equal
this._updateMouseCoords();
this._setProperty('_hasMouseInteracted', true);
}
};
// NOTE: returns undefined if the position is outside of the valid range
function getMousePos(canvas, evt) {
var rect = canvas.getBoundingClientRect();
var xPos = evt.clientX - rect.left;
var yPos = evt.clientY - rect.top;
if (xPos >= 0 && xPos < rect.width && yPos >= 0 && yPos < rect.height) {
return {
x: Math.round(xPos * canvas.offsetWidth / rect.width),
y: Math.round(yPos * canvas.offsetHeight / rect.height)
};
}
}
// =============================================================================
// p5 extensions
// eslint-disable-next-line no-warning-comments
// TODO: It'd be nice to get these accepted upstream in p5
// =============================================================================
/**
* Projects a vector onto the line parallel to a second vector, giving a third
* vector which is the orthogonal projection of that vector onto the line.
* @see https://en.wikipedia.org/wiki/Vector_projection
* @method project
* @for p5.Vector
* @static
* @param {p5.Vector} a - vector being projected
* @param {p5.Vector} b - vector defining the projection target line.
* @return {p5.Vector} projection of a onto the line parallel to b.
*/
p5.Vector.project = function(a, b) {
return p5.Vector.mult(b, p5.Vector.dot(a, b) / p5.Vector.dot(b, b));
};
/**
* Ask whether a vector is parallel to this one.
* @method isParallel
* @for p5.Vector
* @param {p5.Vector} v2
* @param {number} [tolerance] - margin of error for comparisons, comes into
* play when comparing rotated vectors. For example, we want
* <1, 0> to be parallel to <0, 1>.rot(Math.PI/2) but float imprecision
* can get in the way of that.
* @return {boolean}
*/
p5.Vector.prototype.isParallel = function(v2, tolerance) {
tolerance = typeof tolerance === 'number' ? tolerance : 1e-14;
return (
Math.abs(this.x) < tolerance && Math.abs(v2.x) < tolerance
) || (
Math.abs(this.y ) < tolerance && Math.abs(v2.y) < tolerance
) || (
Math.abs(this.x / v2.x - this.y / v2.y) < tolerance
);
};
// =============================================================================
// p5 additions
// =============================================================================
/**
* Loads an image from a path and creates an Image from it.
*
* The image may not be immediately available for rendering
* If you want to ensure that the image is ready before doing
* anything with it, place the loadImageElement() call in preload().
* You may also supply a callback function to handle the image when it's ready.
*
* The path to the image should be relative to the HTML file
* that links in your sketch. Loading an from a URL or other
* remote location may be blocked due to your browser's built-in
* security.
*
* @method loadImageElement
* @param {String} path Path of the image to be loaded
* @param {Function(Image)} [successCallback] Function to be called once
* the image is loaded. Will be passed the
* Image.
* @param {Function(Event)} [failureCallback] called with event error if
* the image fails to load.
* @return {Image} the Image object
*/
p5.prototype.loadImageElement = function(path, successCallback, failureCallback) {
var img = new Image();
var decrementPreload = p5._getDecrementPreload.apply(this, arguments);
img.onload = function() {
if (typeof successCallback === 'function') {
successCallback(img);
}
if (decrementPreload && (successCallback !== decrementPreload)) {
decrementPreload();
}
};
img.onerror = function(e) {
p5._friendlyFileLoadError(0, img.src);
// don't get failure callback mixed up with decrementPreload
if ((typeof failureCallback === 'function') &&
(failureCallback !== decrementPreload)) {
failureCallback(e);
}
};
//set crossOrigin in case image is served which CORS headers
//this will let us draw to canvas without tainting it.
//see https://developer.mozilla.org/en-US/docs/HTML/CORS_Enabled_Image
// When using data-uris the file will be loaded locally
// so we don't need to worry about crossOrigin with base64 file types
if(path.indexOf('data:image/') !== 0) {
img.crossOrigin = 'Anonymous';
}
//start loading the image
img.src = path;
return img;
};
/**
* Draw an image element to the main canvas of the p5js sketch
*
* @method imageElement
* @param {Image} imgEl the image to display
* @param {Number} [sx=0] The X coordinate of the top left corner of the
* sub-rectangle of the source image to draw into
* the destination canvas.
* @param {Number} [sy=0] The Y coordinate of the top left corner of the
* sub-rectangle of the source image to draw into
* the destination canvas.
* @param {Number} [sWidth=imgEl.width] The width of the sub-rectangle of the
* source image to draw into the destination
* canvas.
* @param {Number} [sHeight=imgEl.height] The height of the sub-rectangle of the
* source image to draw into the
* destination context.
* @param {Number} [dx=0] The X coordinate in the destination canvas at
* which to place the top-left corner of the
* source image.
* @param {Number} [dy=0] The Y coordinate in the destination canvas at
* which to place the top-left corner of the
* source image.
* @param {Number} [dWidth] The width to draw the image in the destination
* canvas. This allows scaling of the drawn image.
* @param {Number} [dHeight] The height to draw the image in the destination
* canvas. This allows scaling of the drawn image.
* @example
*
*
* function setup() {
* // here we use a callback to display the image after loading
* loadImageElement("assets/laDefense.jpg", function(imgEl) {
* imageElement(imgEl, 0, 0);
* });
* }
*
*
*
* @alt
* image of the underside of a white umbrella and grided ceiling above
* image of the underside of a white umbrella and grided ceiling above
*
*/
p5.prototype.imageElement = function(imgEl, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) {
/**
* Validates clipping params. Per drawImage spec sWidth and sHight cannot be
* negative or greater than image intrinsic width and height
* @private
* @param {Number} sVal
* @param {Number} iVal
* @returns {Number}
* @private
*/
function _sAssign(sVal, iVal) {
if (sVal > 0 && sVal < iVal) {
return sVal;
}
else {
return iVal;
}
}
function modeAdjust(a, b, c, d, mode) {
if (mode === p5.prototype.CORNER) {
return {x: a, y: b, w: c, h: d};
} else if (mode === p5.prototype.CORNERS) {
return {x: a, y: b, w: c-a, h: d-b};
} else if (mode === p5.prototype.RADIUS) {
return {x: a-c, y: b-d, w: 2*c, h: 2*d};
} else if (mode === p5.prototype.CENTER) {
return {x: a-c*0.5, y: b-d*0.5, w: c, h: d};
}
}
if (arguments.length <= 5) {
dx = sx || 0;
dy = sy || 0;
sx = 0;
sy = 0;
dWidth = sWidth || imgEl.width;
dHeight = sHeight || imgEl.height;
sWidth = imgEl.width;
sHeight = imgEl.height;
} else if (arguments.length === 9) {
sx = sx || 0;
sy = sy || 0;
sWidth = _sAssign(sWidth, imgEl.width);
sHeight = _sAssign(sHeight, imgEl.height);
dx = dx || 0;
dy = dy || 0;
dWidth = dWidth || imgEl.width;
dHeight = dHeight || imgEl.height;
} else {
throw 'Wrong number of arguments to imageElement()';
}
var vals = modeAdjust(dx, dy, dWidth, dHeight,
this._renderer._imageMode);
if (this._renderer._tint) {
// Just-in-time create/draw into a temp canvas so tinting can
// work within the renderer as it would for a p5.Image
// Only resize canvas if it's too small
var context = this._tempCanvas.getContext('2d');
if (this._tempCanvas.width < vals.w || this._tempCanvas.height < vals.h) {
this._tempCanvas.width = Math.max(this._tempCanvas.width, vals.w);
this._tempCanvas.height = Math.max(this._tempCanvas.height, vals.h);
} else {
context.clearRect(0, 0, vals.w, vals.h);
}
context.drawImage(imgEl,
sx, sy, sWidth, sHeight,
0, 0, vals.w, vals.h);
// Call the renderer's image() method with an object that contains the Image
// as an 'elt' property and the temp canvas as well (when needed):
this._renderer.image({canvas: this._tempCanvas},
0, 0, vals.w, vals.h,
vals.x, vals.y, vals.w, vals.h);
} else {
this._renderer.image({elt: imgEl},
sx, sy, sWidth, sHeight,
vals.x, vals.y, vals.w, vals.h);
}
};
/**
* A Group containing all the sprites in the sketch.
*
* @property allSprites
* @for p5.play
* @type {Group}
*/
defineLazyP5Property('allSprites', function() {
return new p5.prototype.Group();
});
p5.prototype._mouseButtonIsPressed = function(buttonCode) {
return (this.mouseIsPressed && this.mouseButton === buttonCode) ||
(this.touchIsDown && buttonCode === this.LEFT);
};
p5.prototype.mouseDidMove = function() {
return this.pmouseX !== this.mouseX || this.pmouseY !== this.mouseY;
};
p5.prototype.mouseIsOver = function(sprite) {
if (!sprite) {
return false;
}
if (!sprite.collider) {
sprite.setDefaultCollider();
}
var mousePosition;
if (this.camera.active) {
mousePosition = this.createVector(this.camera.mouseX, this.camera.mouseY);
} else {
mousePosition = this.createVector(this.mouseX, this.mouseY);
}
return sprite.collider.overlap(new window.p5.PointCollider(mousePosition));
};
p5.prototype.mousePressedOver = function(sprite) {
return (this.mouseIsPressed || this.touchIsDown) && this.mouseIsOver(sprite);
};
var styleEmpty = 'rgba(0,0,0,0)';
p5.Renderer2D.prototype.regularPolygon = function(x, y, sides, size, rotation) {
var ctx = this.drawingContext;
var doFill = this._doFill, doStroke = this._doStroke;
if (doFill && !doStroke) {
if (ctx.fillStyle === styleEmpty) {
return this;
}
} else if (!doFill && doStroke) {
if (ctx.strokeStyle === styleEmpty) {
return this;
}
}
if (sides < 3) {
return;
}
ctx.beginPath();
ctx.moveTo(x + size * Math.cos(rotation), y + size * Math.sin(rotation));
for (var i = 1; i < sides; i++) {
var angle = rotation + (i * 2 * Math.PI / sides);
ctx.lineTo(x + size * Math.cos(angle), y + size * Math.sin(angle));
}
ctx.closePath();
if (doFill) {
ctx.fill();
}
if (doStroke) {
ctx.stroke();
}
};
p5.prototype.regularPolygon = function(x, y, sides, size, rotation) {
if (!this._renderer._doStroke && !this._renderer._doFill) {
return this;
}
var args = new Array(arguments.length);
for (var i = 0; i < args.length; ++i) {
args[i] = arguments[i];
}
if (typeof rotation === 'undefined') {
rotation = -(Math.PI / 2);
if (0 === sides % 2) {
rotation += Math.PI / sides;
}
} else if (this._angleMode === this.DEGREES) {
rotation = this.radians(rotation);
}
// NOTE: only implemented for non-3D
if (!this._renderer.isP3D) {
this._validateParameters(
'regularPolygon',
args,
[
['Number', 'Number', 'Number', 'Number'],
['Number', 'Number', 'Number', 'Number', 'Number']
]
);
this._renderer.regularPolygon(
args[0],
args[1],
args[2],
args[3],
rotation
);
}
return this;
};
p5.Renderer2D.prototype.shape = function() {
var ctx = this.drawingContext;
var doFill = this._doFill, doStroke = this._doStroke;
if (doFill && !doStroke) {
if (ctx.fillStyle === styleEmpty) {
return this;
}
} else if (!doFill && doStroke) {
if (ctx.strokeStyle === styleEmpty) {
return this;
}
}
var numCoords = arguments.length / 2;
if (numCoords < 1) {
return;
}
ctx.beginPath();
ctx.moveTo(arguments[0], arguments[1]);
for (var i = 1; i < numCoords; i++) {
ctx.lineTo(arguments[i * 2], arguments[i * 2 + 1]);
}
ctx.closePath();
if (doFill) {
ctx.fill();
}
if (doStroke) {
ctx.stroke();
}
};
p5.prototype.shape = function() {
if (!this._renderer._doStroke && !this._renderer._doFill) {
return this;
}
// NOTE: only implemented for non-3D
if (!this._renderer.isP3D) {
// eslint-disable-next-line no-warning-comments
// TODO: call this._validateParameters, once it is working in p5.js and
// we understand if it can be used for var args functions like this
this._renderer.shape.apply(this._renderer, arguments);
}
return this;
};
p5.prototype.rgb = function(r, g, b, a) {
// convert a from 0 to 255 to 0 to 1
if (!a) {
a = 1;
}
a = a * 255;
return this.color(r, g, b, a);
};
p5.prototype.createGroup = function() {
return new this.Group();
};
defineLazyP5Property('World', function() {
var World = {
pInst: this
};
function createReadOnlyP5PropertyAlias(name) {
Object.defineProperty(World, name, {
enumerable: true,
get: function() {
return this.pInst[name];
}
});
}
createReadOnlyP5PropertyAlias('width');
createReadOnlyP5PropertyAlias('height');
createReadOnlyP5PropertyAlias('mouseX');
createReadOnlyP5PropertyAlias('mouseY');
createReadOnlyP5PropertyAlias('allSprites');
createReadOnlyP5PropertyAlias('frameCount');
Object.defineProperty(World, 'frameRate', {
enumerable: true,
get: function() {
return this.pInst.frameRate();
},
set: function(value) {
this.pInst.frameRate(value);
}
});
Object.defineProperty(World, 'seconds', {
enumerable: true,
get: function() {
var currentDate = new Date();
var currentTime = currentDate.getTime();
return Math.round((currentTime - this.pInst._startTime) / 1000);
}
});
return World;
});
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 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;
/**
* Represents the opacity of the sprite, on a scale from 0 to 1, with 1 being fully
* opaque. Default value is 1.
* Read only.
*
* @property alpha
* @type {Number}
* @default 1
*/
this.alpha = 1;
/*
* 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;
/*
* @type {number}
* @private
* _horizontalStretch is the value to scale animation sprites in the X direction
*/
this._horizontalStretch = 1;
/*
* @type {number}
* @private
* _verticalStretch is the value to scale animation sprites in the Y direction
*/
this._verticalStretch = 1;
/*
* _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() {
if (this._internalWidth === undefined) {
return 100;
} else if (this.animation && pInst._fixedSpriteAnimationFrameSizes) {
return this._internalWidth * this._horizontalStretch;
} else {
return this._internalWidth;
}
},
set: function(value) {
if (this.animation && pInst._fixedSpriteAnimationFrameSizes) {
this._horizontalStretch = value / this._internalWidth;
} else {
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() {
if (this._internalHeight === undefined) {
return 100;
} else if (this.animation && pInst._fixedSpriteAnimationFrameSizes) {
return this._internalHeight * this._verticalStretch;
} else {
return this._internalHeight;
}
},
set: function(value) {
if (this.animation && pInst._fixedSpriteAnimationFrameSizes) {
this._verticalStretch = value / this._internalHeight;
} else {
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;
/**
* Gets the scaled width of the sprite.
*
* @method getScaledWidth
* @return {Number} Scaled width
*/
this.getScaledWidth = function() {
return this.width * this.scale;
};
/**
* Gets the scaled height of the sprite.
*
* @method getScaledHeight
* @return {Number} Scaled height
*/
this.getScaledHeight = function() {
return this.height * this.scale;
};
/**
* 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 the color of the
* placeholder rectangle
*
* @property shapeColor
* @type {color}
*/
this.shapeColor = color(127, 127, 127);
/**
* 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;
/**
* Swept collider oriented along the current velocity vector, extending to
* cover the old and new positions of the sprite.
*
* The corners of the swept collider will extend beyond the actual swept
* shape, but it should be sufficient for broad-phase detection of collision
* candidates.
*
* Note that this collider will have no dimensions if the source sprite has no
* velocity.
*/
this._sweptCollider = undefined;
/**
* Sprite x position (alias to position.x).
*
* @property x
* @type {Number}
*/
Object.defineProperty(this, 'x', {
enumerable: true,
get: function() {
return this.position.x;
},
set: function(value) {
this.position.x = value;
}
});
/**
* Sprite y position (alias to position.y).
*
* @property y
* @type {Number}
*/
Object.defineProperty(this, 'y', {
enumerable: true,
get: function() {
return this.position.y;
},
set: function(value) {
this.position.y = value;
}
});
/**
* Sprite x velocity (alias to velocity.x).
*
* @property velocityX
* @type {Number}
*/
Object.defineProperty(this, 'velocityX', {
enumerable: true,
get: function() {
return this.velocity.x;
},
set: function(value) {
this.velocity.x = value;
}
});
/**
* Sprite y velocity (alias to velocity.y).
*
* @property velocityY
* @type {Number}
*/
Object.defineProperty(this, 'velocityY', {
enumerable: true,
get: function() {
return this.velocity.y;
},
set: function(value) {
this.velocity.y = value;
}
});
/**
* Sprite lifetime (alias to life).
*
* @property lifetime
* @type {Number}
*/
Object.defineProperty(this, 'lifetime', {
enumerable: true,
get: function() {
return this.life;
},
set: function(value) {
this.life = value;
}
});
/**
* Sprite bounciness (alias to restitution).
*
* @property bounciness
* @type {Number}
*/
Object.defineProperty(this, 'bounciness', {
enumerable: true,
get: function() {
return this.restitution;
},
set: function(value) {
this.restitution = value;
}
});
/**
* Sprite animation frame delay (alias to animation.frameDelay).
*
* @property frameDelay
* @type {Number}
*/
Object.defineProperty(this, 'frameDelay', {
enumerable: true,
get: function() {
return this.animation && this.animation.frameDelay;
},
set: function(value) {
if (this.animation) {
this.animation.frameDelay = value;
}
}
});
/**
* If the sprite is moving, use the swept collider. Otherwise use the actual
* collider.
*/
this._getBroadPhaseCollider = function() {
return (this.velocity.magSq() > 0) ? this._sweptCollider : this.collider;
};
/**
* Returns true if the two sprites crossed paths in the current frame,
* indicating a possible collision.
*/
this._doSweptCollidersOverlap = function(target) {
var displacement = this._getBroadPhaseCollider().collide(target._getBroadPhaseCollider());
return displacement.x !== 0 || displacement.y !== 0;
};
/*
* @private
* Keep animation properties in sync with how the animation changes.
*/
this._syncAnimationSizes = function(animations, currentAnimation) {
if (pInst._fixedSpriteAnimationFrameSizes) {
return;
}
if(animations[currentAnimation].frameChanged || this.width === undefined || this.height === undefined)
{
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 (this._sweptCollider && this.velocity.magSq() > 0) {
this._sweptCollider.updateSweptColliderFromSprite(this);
}
//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(animations, currentAnimation);
}
//a collider is created either manually with setCollider or
//when I check this sprite for collisions or overlaps
if (this.collider) {
this.collider.updateFromSprite(this);
}
//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(animations[currentAnimation] && animations[currentAnimation].getWidth() === 1 && animations[currentAnimation].getHeight() === 1) {
//animation is still loading
return;
}
this.setCollider('rectangle');
};
/**
* 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;
//rollover
if(this.collider) {
var mousePosition;
if(camera.active)
mousePosition = createVector(camera.mouseX, camera.mouseY);
else
mousePosition = createVector(pInst.mouseX, pInst.mouseY);
this.mouseIsOver = this.collider.overlap(new p5.PointCollider(mousePosition));
//global p5 var
if(this.mouseIsOver && (pInst.mouseIsPressed || pInst.touchIsDown))
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 rectangle 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 many ways to call this method. The first argument determines the
* type of collider you are creating, which in turn changes the remaining
* arguments. Valid collider types are:
*
* * `point` - A point collider with no dimensions, only a position.
*
* `setCollider("point"[, offsetX, offsetY])`
*
* * `circle` - A circular collider with a set radius.
*
* `setCollider("circle"[, offsetX, offsetY[, radius])`
*
* * `rectangle` - An alias for `aabb`, below.
*
* * `aabb` - An axis-aligned bounding box - has width and height but no rotation.
*
* `setCollider("aabb"[, offsetX, offsetY[, width, height]])`
*
* * `obb` - An oriented bounding box - has width, height, and rotation.
*
* `setCollider("obb"[, offsetX, offsetY[, width, height[, rotation]]])`
*
*
* @method setCollider
* @param {String} type One of "point", "circle", "rectangle", "aabb" or "obb"
* @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
* @param {Number} [rotation] Collider rotation in degrees
* @throws {TypeError} if given invalid parameters.
*/
this.setCollider = function(type, offsetX, offsetY, width, height, rotation) {
var _type = type ? type.toLowerCase() : '';
if (_type === 'rectangle') {
// Map 'rectangle' to AABB. Change this if you want it to default to OBB.
_type = 'obb';
}
// Check correct arguments, provide context-sensitive usage message if wrong.
if (!(_type === 'point' || _type === 'circle' || _type === 'obb' || _type === 'aabb')) {
throw new TypeError('setCollider expects the first argument to be one of "point", "circle", "rectangle", "aabb" or "obb"');
} else if (_type === 'point' && !(arguments.length === 1 || arguments.length === 3)) {
throw new TypeError('Usage: setCollider("' + type + '"[, offsetX, offsetY])');
} else if (_type === 'circle' && !(arguments.length === 1 || arguments.length === 3 || arguments.length === 4)) {
throw new TypeError('Usage: setCollider("' + type + '"[, offsetX, offsetY[, radius]])');
} else if (_type === 'aabb' && !(arguments.length === 1 || arguments.length === 3 || arguments.length === 5)) {
throw new TypeError('Usage: setCollider("' + type + '"[, offsetX, offsetY[, width, height]])');
} else if (_type === 'obb' && !(arguments.length === 1 || arguments.length === 3 || arguments.length === 5 || arguments.length === 6)) {
throw new TypeError('Usage: setCollider("' + type + '"[, offsetX, offsetY[, width, height[, rotation]]])');
}
//var center = this.position;
var offset = createVector(offsetX, offsetY);
if (_type === 'point') {
this.collider = p5.PointCollider.createFromSprite(this, offset);
} else if (_type === 'circle') {
this.collider = p5.CircleCollider.createFromSprite(this, offset, width);
} else if (_type === 'aabb') {
this.collider = p5.AxisAlignedBoundingBoxCollider.createFromSprite(this, offset, width, height);
} else if (_type === 'obb') {
this.collider = p5.OrientedBoundingBoxCollider.createFromSprite(this, offset, width, height, radians(rotation));
}
this._sweptCollider = new p5.OrientedBoundingBoxCollider();
// Disabled for Code.org, since perf seems better without the quadtree:
// quadTree.insert(this);
};
/**
* 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.
* When _fixedSpriteAnimationFrameSizes is set, the scale value should
* include the horizontal stretch for animations.
* @private
*/
this._getScaleX = function()
{
if (pInst._fixedSpriteAnimationFrameSizes) {
return this.scale * this._horizontalStretch;
}
return this.scale;
};
/*
* Returns the value the sprite should be scaled in the Y direction.
* Used to calculate rendering and collisions.
* When _fixedSpriteAnimationFrameSizes is set, the scale value should
* include the vertical stretch for animations.
* @private
*/
this._getScaleY = function()
{
if (pInst._fixedSpriteAnimationFrameSizes) {
return this.scale * this._verticalStretch;
}
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);
if (pInst._angleMode === pInst.RADIANS) {
rotate(radians(this.rotation));
} else {
rotate(this.rotation);
}
scale(this._getScaleX()*dirX, this._getScaleY()*dirY);
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);
// Draw collision shape
if (this.collider === undefined) {
this.setDefaultCollider();
}
if(this.collider) {
this.collider.draw(pInst);
}
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]) {
if(this.tint) {
push();
tint(this.tint);
}
if(this.alpha < 1) {
push();
alphaTint(this.alpha);
}
animations[currentAnimation].draw(0, 0, 0);
if(this.alpha < 1) {
pop();
}
if(this.tint) {
pop();
}
}
}
else
{
var fillColor = this.shapeColor;
if (this.tint) {
fillColor = lerpColor(color(fillColor), color(this.tint), 0.5);
}
noStroke();
fill(fillColor);
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);
}
};
/**
* Alias for remove()
*
* @method destroy
*/
this.destroy = this.remove;
/**
* 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;
};
/**
* Alias for setSpeed()
*
* @method setSpeedAndDirection
* @param {Number} speed Scalar speed
* @param {Number} [angle] Direction in degrees
*/
this.setSpeedAndDirection = this.setSpeed;
/**
* Alias for animation.changeFrame()
*
* @method setFrame
* @param {Number} frame Frame number (starts from 0).
*/
this.setFrame = function(f) {
if (this.animation) {
this.animation.changeFrame(f);
}
};
/**
* Alias for animation.nextFrame()
*
* @method nextFrame
*/
this.nextFrame = function() {
if (this.animation) {
this.animation.nextFrame();
}
};
/**
* Alias for animation.previousFrame()
*
* @method previousFrame
*/
this.previousFrame = function() {
if (this.animation) {
this.animation.previousFrame();
}
};
/**
* Alias for animation.stop()
*
* @method pause
*/
this.pause = function() {
if (this.animation) {
this.animation.stop();
}
};
/**
* Alias for animation.play() with extra logic
*
* Plays/resumes the sprite's current animation.
* If the animation is currently playing this has no effect.
* If the animation has stopped at its last frame, this will start it over
* at the beginning.
*
* @method play
*/
this.play = function() {
if (!this.animation) {
return;
}
// Normally this just sets the 'playing' flag without changing the animation
// frame, which will cause the animation to continue on the next update().
// If the animation is non-looping and is stopped at the last frame
// we also rewind the animation to the beginning.
if (!this.animation.looping && !this.animation.playing && this.animation.getFrame() === this.animation.images.length - 1) {
this.animation.rewind();
}
this.animation.play();
};
/**
* Wrapper to access animation.frameChanged
*
* @method frameDidChange
* @return {Boolean} true if the animation frame has changed
*/
this.frameDidChange = function() {
return this.animation ? this.animation.frameChanged : false;
};
/**
* Rotate the sprite towards a specific position
*
* @method setFrame
* @param {Number} x Horizontal coordinate to point to
* @param {Number} y Vertical coordinate to point to
*/
this.pointTo = function(x, y) {
var yDelta = y - this.position.y;
var xDelta = x - this.position.x;
if (!isNaN(xDelta) && !isNaN(yDelta) && (xDelta !== 0 || yDelta !== 0)) {
var radiansAngle = Math.atan2(yDelta, xDelta);
this.rotation = 360 * radiansAngle / (2 * Math.PI);
}
};
/**
* 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 or