/*
p5.play
by Paolo Pedercini/molleindustria, 2015
http://molleindustria.org/
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
(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
*/
// =============================================================================
// p5 additions
// =============================================================================
/**
* A Group containing all the sprites in the sketch.
*
* @property allSprites
* @type {Group}
*/
p5.prototype.allSprites = new Group();
/**
* 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.
*
* @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(x, y, width, height);
s.depth = allSprites.maxDepth()+1;
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)
spriteUpdate = false;
if(upd==true)
spriteUpdate = true;
if(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
* <1: inelastic, this is the most common in nature
* >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
*/
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 set the rotation of the visual element
* (image or animation) to the sprite's movement direction.
*
* @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 0
*/
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.
*
* @property width
* @type {Number}
* @default 100
*/
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
*/
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.width;
/**
* 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.height;
/**
* False 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 removed
* @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 = new Array();
var animations = {};
//The current animation's label.
var currentAnimation = "";
/**
* Reference to the current animation.
*
* @property animation
* @type {Animation}
*/
this.animation;
/**
* 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 *= this.friction;
this.velocity.y *= this.friction;
if(this.maxSpeed != -1)
this.limitSpeed(this.maxSpeed);
if(this.rotateToDirection)
this.rotation = this.getDirection();
else
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] != null)
{
//update it
animations[currentAnimation].update();
//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.width = animations[currentAnimation].getWidth()*abs(this.scale);
this.height = animations[currentAnimation].getHeight()*abs(this.scale);
//quadTree.insert(this);
}
//update size and collider
if(animations[currentAnimation].frameChanged || this.width == undefined || this.height == undefined)
{
//this.collider = this.getBoundingBox();
this.width = animations[currentAnimation].getWidth()*abs(this.scale);
this.height = animations[currentAnimation].getHeight()*abs(this.scale);
}
}
//a collider is created either manually with setCollider or
//when I check this sprite for collisions or overlaps
if(this.collider != null)
{
if(this.collider instanceof AABB)
{
//scale / rotate collider
var t = radians(this.rotation);
if(this.colliderType == "custom")
{
this.collider.extents.x = this.collider.originalExtents.x * abs(this.scale) * abs(cos(t)) +
this.collider.originalExtents.y * abs(this.scale) * abs(sin(t))
this.collider.extents.y = this.collider.originalExtents.x * abs(this.scale) * abs(sin(t)) +
this.collider.originalExtents.y * abs(this.scale) * abs(cos(t));
}
else if(this.colliderType == "default")
{
this.collider.extents.x = this.originalWidth * abs(this.scale) * abs(cos(t)) +
this.originalHeight * abs(this.scale) * abs(sin(t))
this.collider.extents.y = this.originalWidth * abs(this.scale) * abs(sin(t)) +
this.originalHeight * abs(this.scale) * abs(cos(t));
}
else if(this.colliderType == "image")
{
this.collider.extents.x = this.width * abs(cos(t)) +
this.height * abs(sin(t))
this.collider.extents.y = this.width * abs(sin(t)) +
this.height * 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==null)
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==null)
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.
*/
this.setDefaultCollider = function() {
//if has animation get the animation bounding box
//working only for preloaded images
if(animations[currentAnimation] != null && (animations[currentAnimation].getWidth() != 1 && animations[currentAnimation].getHeight()!=1))
{
this.collider = this.getBoundingBox();
this.width = animations[currentAnimation].getWidth()*abs(this.scale);
this.height = animations[currentAnimation].getHeight()*abs(this.scale);
//quadTree.insert(this);
this.colliderType = "image";
//print("IMAGE COLLIDER ADDED");
}
else if(animations[currentAnimation] != null && 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(this.position, createVector(this.width, this.height));
//quadTree.insert(this);
this.colliderType = "default";
}
quadTree.insert(this);
};
/**
* Updates the sprite mouse states and triggers the mouse events:
* onMouseOver, onMouseOut, onMousePressed, onMouseReleased
*/
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(mouseX, mouseY)
//rollover
if(this.collider != null)
{
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 && 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 && !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 animate sprites.
*
* Often the image bounding box is not appropriate as active area for
* a collision detection so you can set a circular or rectangular sprite with different
* dimensions and offset from the sprite's center.
*
* setCollider
* @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
*
*/
this.setCollider = function(type, offsetX, offsetY, width, height) {
this.colliderType = "custom";
if(type=="rectangle" && arguments.length==5)
this.collider = new AABB(this.position, createVector(arguments[3], arguments[4]), createVector(arguments[1],arguments[2]) );
else if(type=="circle")
{
var v = createVector(arguments[1], arguments[2])
if(arguments.length!=4)
print("Warning: usage setCollider(\"circle\", offsetX, offsetY, radius)");
this.collider = new CircleCollider(this.position, arguments[3], createVector(arguments[1], arguments[2]));
}
quadTree.insert(this);
}
/**
* Returns a the bounding box of the current image
*/
this.getBoundingBox = function() {
var w = animations[currentAnimation].getWidth()*abs(this.scale);
var h = animations[currentAnimation].getHeight()*abs(this.scale);
//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(this.position, createVector(w, h));
}
else {
return new AABB(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
* @returns {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
* @returns {Number} Current mirroring if no parameter is specified
*/
this.mirrorY = function(dir) {
if(dir == 1 || dir == -1)
dirY = dir;
else
return dirY;
}
/**
* Manages the positioning, scale and rotation of the sprite
* Called automatically, it should not be overridden
*/
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.scale*dirX, this.scale*dirY);
rotate(radians(this.rotation));
this.draw();
//draw debug info
pop();
if(this.debug)
{
//draw the anchor point
stroke(0,255,0);
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();
}
}
}
}
/**
* 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 != null)
{
if(animations[currentAnimation] != null)
animations[currentAnimation].draw(0,0,0);
}
else
{
noStroke();
fill(this.shapeColor);
rect(0, 0, this.width, this.height);
}
}
/**
* 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
for(var i=0; imax)
{
//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.
*
* @method setSpeed
* @param {Number} speed Scalar speed to add
* @param {Number} angle Direction in degrees
*/
this.setSpeed = function(speed, angle) {
var a = radians(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 = radians(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