/* p5.play by Paolo Pedercini/molleindustria, 2015 http://molleindustria.org/ */ (function (root, factory) { if (typeof define === 'function' && define.amd) define('p5.play', ['p5'], function (p5) { (factory(p5)); }); else if (typeof exports === 'object') factory(require('../p5')); else factory(root.p5); }(this, function (p5) { /** * p5.play is a library for p5.js to facilitate the creation of games and gamelike * projects. * * It provides a flexible Sprite class to manage visual objects in 2D space * and features such as animation support, basic collision detection * and resolution, mouse and keyboard interactions, and a virtual camera. * * p5.play is not a box2D-derived physics engine, it doesn't use events, and it's * designed to be understood and possibly modified by intermediate programmers. * * See the examples folder for more info on how to use this library. * * @module p5.play * @submodule p5.play * @for p5.play * @main */ // ============================================================================= // initialization // ============================================================================= // This is the new way to initialize custom p5 properties for any p5 instance. // The goal is to migrate lazy P5 properties over to this method. // @see https://github.com/molleindustria/p5.play/issues/46 p5.prototype.registerMethod('init', function p5PlayInit() { /** * The sketch camera automatically created at the beginning of a sketch. * A camera facilitates scrolling and zooming for scenes extending beyond * the canvas. A camera has a position, a zoom factor, and the mouse * coordinates relative to the view. * * In p5.js terms the camera wraps the whole drawing cycle in a * transformation matrix but it can be disabled anytime during the draw * cycle, for example to draw interface elements in an absolute position. * * @property camera * @type {camera} */ this.camera = new Camera(this, 0, 0, 1); this.camera.init = false; }); // This provides a way for us to lazily define properties that // are global to p5 instances. // // Note that this isn't just an optimization: p5 currently provides no // way for add-ons to be notified when new p5 instances are created, so // lazily creating these properties is the *only* mechanism available // to us. For more information, see: // // https://github.com/processing/p5.js/issues/1263 function defineLazyP5Property(name, getter) { Object.defineProperty(p5.prototype, name, { configurable: true, enumerable: true, get: function () { var context = (this instanceof p5 && !this._isGlobal) ? this : window; if (typeof (context._p5PlayProperties) === 'undefined') { context._p5PlayProperties = {}; } if (!(name in context._p5PlayProperties)) { context._p5PlayProperties[name] = getter.call(context); } return context._p5PlayProperties[name]; } }); } // This returns a factory function, suitable for passing to // defineLazyP5Property, that returns a subclass of the given // constructor that is always bound to a particular p5 instance. function boundConstructorFactory(constructor) { if (typeof (constructor) !== 'function') throw new Error('constructor must be a function'); return function createBoundConstructor() { var pInst = this; function F() { var args = Array.prototype.slice.call(arguments); return constructor.apply(this, [pInst].concat(args)); } F.prototype = constructor.prototype; return F; }; } // This is a utility that makes it easy to define convenient aliases to // pre-bound p5 instance methods. // // For example: // // var pInstBind = createPInstBinder(pInst); // // var createVector = pInstBind('createVector'); // var loadImage = pInstBind('loadImage'); // // The above will create functions createVector and loadImage, which can be // used similar to p5 global mode--however, they're bound to specific p5 // instances, and can thus be used outside of global mode. function createPInstBinder(pInst) { return function pInstBind(methodName) { var method = pInst[methodName]; if (typeof (method) !== 'function') throw new Error('"' + methodName + '" is not a p5 method'); return method.bind(pInst); }; } // These are utility p5 functions that don't depend on p5 instance state in // order to work properly, so we'll go ahead and make them easy to // access without needing to bind them to a p5 instance. var abs = p5.prototype.abs; var radians = p5.prototype.radians; var dist = p5.prototype.dist; var degrees = p5.prototype.degrees; var pow = p5.prototype.pow; var round = p5.prototype.round; // ============================================================================= // p5 additions // ============================================================================= /** * A Group containing all the sprites in the sketch. * * @property allSprites * @type {Group} */ defineLazyP5Property('allSprites', function () { return new p5.prototype.Group(); }); p5.prototype.spriteUpdate = true; /** * A Sprite is the main building block of p5.play: * an element able to store images or animations with a set of * properties such as position and visibility. * A Sprite can have a collider that defines the active area to detect * collisions or overlappings with other sprites and mouse interactions. * * Sprites created using createSprite (the preferred way) are added to the * allSprites group and given a depth value that puts it in front of all * other sprites. * * @method createSprite * @param {Number} x Initial x coordinate * @param {Number} y Initial y coordinate * @param {Number} width Width of the placeholder rectangle and of the * collider until an image or new collider are set * @param {Number} height Height of the placeholder rectangle and of the * collider until an image or new collider are set * @return {Object} The new sprite instance */ p5.prototype.createSprite = function (x, y, width, height) { var s = new Sprite(this, x, y, width, height); s.depth = this.allSprites.maxDepth() + 1; this.allSprites.add(s); return s; }; /** * Removes a Sprite from the sketch. * The removed Sprite won't be drawn or updated anymore. * Equivalent to Sprite.remove() * * @method removeSprite * @param {Object} sprite Sprite to be removed */ p5.prototype.removeSprite = function (sprite) { sprite.remove(); }; /** * Updates all the sprites in the sketch (position, animation...) * it's called automatically at every draw(). * It can be paused by passing a parameter true or false; * Note: it does not render the sprites. * * @method updateSprites * @param {Boolean} updating false to pause the update, true to resume */ p5.prototype.updateSprites = function (upd) { if (upd === false) this.spriteUpdate = false; if (upd === true) this.spriteUpdate = true; if (this.spriteUpdate) for (var i = 0; i < this.allSprites.size(); i++) { this.allSprites.get(i).update(); } }; /** * Returns all the sprites in the sketch as an array * * @method getSprites * @return {Array} Array of Sprites */ p5.prototype.getSprites = function () { //draw everything if (arguments.length === 0) { return this.allSprites.toArray(); } else { var arr = []; //for every tag for (var j = 0; j < arguments.length; j++) { for (var i = 0; i < this.allSprites.size(); i++) { if (this.allSprites.get(i).isTagged(arguments[j])) arr.push(this.allSprites.get(i)); } } return arr; } }; /** * Displays a Group of sprites. * If no parameter is specified, draws all sprites in the * sketch. * The drawing order is determined by the Sprite property "depth" * * @method drawSprites * @param {Group} [group] Group of Sprites to be displayed */ p5.prototype.drawSprites = function (group) { // If no group is provided, draw the allSprites group. group = group || this.allSprites; if (typeof group.draw !== 'function') { throw ('Error: with drawSprites you can only draw all sprites or a group'); } group.draw(); }; /** * Displays a Sprite. * To be typically used in the main draw function. * * @method drawSprite * @param {Sprite} sprite Sprite to be displayed */ p5.prototype.drawSprite = function (sprite) { if (sprite) sprite.display(); }; /** * Loads an animation. * To be typically used in the preload() function of the sketch. * * @method loadAnimation * @param {Sprite} sprite Sprite to be displayed */ p5.prototype.loadAnimation = function () { return construct(this.Animation, arguments); }; /** * Loads a Sprite Sheet. * To be typically used in the preload() function of the sketch. * * @method loadSpriteSheet */ p5.prototype.loadSpriteSheet = function () { return construct(this.SpriteSheet, arguments); }; /** * Displays an animation. * * @method animation * @param {Animation} anim Animation to be displayed * @param {Number} x X coordinate * @param {Number} y Y coordinate * */ p5.prototype.animation = function (anim, x, y) { anim.draw(x, y); }; //variable to detect instant presses defineLazyP5Property('_p5play', function () { return { keyStates: {}, mouseStates: {} }; }); var KEY_IS_UP = 0; var KEY_WENT_DOWN = 1; var KEY_IS_DOWN = 2; var KEY_WENT_UP = 3; /** * Detects if a key was pressed during the last cycle. * It can be used to trigger events once, when a key is pressed or released. * Example: Super Mario jumping. * * @method keyWentDown * @param {Number|String} key Key code or character * @return {Boolean} True if the key was pressed */ p5.prototype.keyWentDown = function (key) { return this._isKeyInState(key, KEY_WENT_DOWN); }; /** * Detects if a key was released during the last cycle. * It can be used to trigger events once, when a key is pressed or released. * Example: Spaceship shooting. * * @method keyWentUp * @param {Number|String} key Key code or character * @return {Boolean} True if the key was released */ p5.prototype.keyWentUp = function (key) { return this._isKeyInState(key, KEY_WENT_UP); }; /** * Detects if a key is currently pressed * Like p5 keyIsDown but accepts strings and codes * * @method keyDown * @param {Number|String} key Key code or character * @return {Boolean} True if the key is down */ p5.prototype.keyDown = function (key) { return this._isKeyInState(key, KEY_IS_DOWN); }; /** * Detects if a key is in the given state during the last cycle. * Helper method encapsulating common key state logic; it may be preferable * to call keyDown or other methods directly. * * @private * @method _isKeyInState * @param {Number|String} key Key code or character * @param {Number} state Key state to check against * @return {Boolean} True if the key is in the given state */ p5.prototype._isKeyInState = function (key, state) { var keyCode; var keyStates = this._p5play.keyStates; if (typeof key === 'string') { keyCode = this._keyCodeFromAlias(key); } else { keyCode = key; } //if undefined start checking it if (keyStates[keyCode] === undefined) { if (this.keyIsDown(keyCode)) keyStates[keyCode] = KEY_IS_DOWN; else keyStates[keyCode] = KEY_IS_UP; } return (keyStates[keyCode] === state); }; /** * Detects if a mouse button is currently down * Combines mouseIsPressed and mouseButton of p5 * * @method mouseDown * @param {Number} [buttonCode] Mouse button constant LEFT, RIGHT or CENTER * @return {Boolean} True if the button is down */ p5.prototype.mouseDown = function (buttonCode) { return this._isMouseButtonInState(buttonCode, KEY_IS_DOWN); }; /** * Detects if a mouse button is currently up * Combines mouseIsPressed and mouseButton of p5 * * @method mouseUp * @param {Number} [buttonCode] Mouse button constant LEFT, RIGHT or CENTER * @return {Boolean} True if the button is up */ p5.prototype.mouseUp = function (buttonCode) { return this._isMouseButtonInState(buttonCode, KEY_IS_UP); }; /** * Detects if a mouse button was released during the last cycle. * It can be used to trigger events once, to be checked in the draw cycle * * @method mouseWentUp * @param {Number} [buttonCode] Mouse button constant LEFT, RIGHT or CENTER * @return {Boolean} True if the button was just released */ p5.prototype.mouseWentUp = function (buttonCode) { return this._isMouseButtonInState(buttonCode, KEY_WENT_UP); }; /** * Detects if a mouse button was pressed during the last cycle. * It can be used to trigger events once, to be checked in the draw cycle * * @method mouseWentDown * @param {Number} [buttonCode] Mouse button constant LEFT, RIGHT or CENTER * @return {Boolean} True if the button was just pressed */ p5.prototype.mouseWentDown = function (buttonCode) { return this._isMouseButtonInState(buttonCode, KEY_WENT_DOWN); }; /** * Detects if a mouse button is in the given state during the last cycle. * Helper method encapsulating common mouse button state logic; it may be * preferable to call mouseWentUp, etc, directly. * * @private * @method _isMouseButtonInState * @param {Number} [buttonCode] Mouse button constant LEFT, RIGHT or CENTER * @param {Number} state * @return {boolean} True if the button was in the given state */ p5.prototype._isMouseButtonInState = function (buttonCode, state) { var mouseStates = this._p5play.mouseStates; if (buttonCode === undefined) buttonCode = this.LEFT; //undefined = not tracked yet, start tracking if (mouseStates[buttonCode] === undefined) { if (this.mouseIsPressed && this.mouseButton === buttonCode) mouseStates[buttonCode] = KEY_IS_DOWN; else mouseStates[buttonCode] = KEY_IS_UP; } return (mouseStates[buttonCode] === state); }; /** * An object storing all useful keys for easy access * Key.tab = 9 * * @private * @property KEY * @type {Object} */ p5.prototype.KEY = { 'BACKSPACE': 8, 'TAB': 9, 'ENTER': 13, 'SHIFT': 16, 'CTRL': 17, 'ALT': 18, 'PAUSE': 19, 'CAPS_LOCK': 20, 'ESC': 27, 'SPACE': 32, ' ': 32, 'PAGE_UP': 33, 'PAGE_DOWN': 34, 'END': 35, 'HOME': 36, 'LEFT_ARROW': 37, 'LEFT': 37, 'UP_ARROW': 38, 'UP': 38, 'RIGHT_ARROW': 39, 'RIGHT': 39, 'DOWN_ARROW': 40, 'DOWN': 40, 'INSERT': 45, 'DELETE': 46, '0': 48, '1': 49, '2': 50, '3': 51, '4': 52, '5': 53, '6': 54, '7': 55, '8': 56, '9': 57, 'A': 65, 'B': 66, 'C': 67, 'D': 68, 'E': 69, 'F': 70, 'G': 71, 'H': 72, 'I': 73, 'J': 74, 'K': 75, 'L': 76, 'M': 77, 'N': 78, 'O': 79, 'P': 80, 'Q': 81, 'R': 82, 'S': 83, 'T': 84, 'U': 85, 'V': 86, 'W': 87, 'X': 88, 'Y': 89, 'Z': 90, '0NUMPAD': 96, '1NUMPAD': 97, '2NUMPAD': 98, '3NUMPAD': 99, '4NUMPAD': 100, '5NUMPAD': 101, '6NUMPAD': 102, '7NUMPAD': 103, '8NUMPAD': 104, '9NUMPAD': 105, 'MULTIPLY': 106, 'PLUS': 107, 'MINUS': 109, 'DOT': 110, 'SLASH1': 111, 'F1': 112, 'F2': 113, 'F3': 114, 'F4': 115, 'F5': 116, 'F6': 117, 'F7': 118, 'F8': 119, 'F9': 120, 'F10': 121, 'F11': 122, 'F12': 123, 'EQUAL': 187, 'COMMA': 188, 'SLASH': 191, 'BACKSLASH': 220 }; /** * An object storing deprecated key aliases, which we still support but * should be mapped to valid aliases and generate warnings. * * @private * @property KEY_DEPRECATIONS * @type {Object} */ p5.prototype.KEY_DEPRECATIONS = { 'MINUT': 'MINUS', 'COMA': 'COMMA' }; /** * Given a string key alias (as defined in the KEY property above), look up * and return the numeric JavaScript key code for that key. If a deprecated * alias is passed (as defined in the KEY_DEPRECATIONS property) it will be * mapped to a valid key code, but will also generate a warning about use * of the deprecated alias. * * @private * @method _keyCodeFromAlias * @param {!string} alias - a case-insensitive key alias * @return {number|undefined} a numeric JavaScript key code, or undefined * if no key code matching the given alias is found. */ p5.prototype._keyCodeFromAlias = function (alias) { alias = alias.toUpperCase(); if (this.KEY_DEPRECATIONS[alias]) { this._warn('Key literal "' + alias + '" is deprecated and may be removed ' + 'in a future version of p5.play. ' + 'Please use "' + this.KEY_DEPRECATIONS[alias] + '" instead.'); alias = this.KEY_DEPRECATIONS[alias]; } return this.KEY[alias]; }; //pre draw: detect keyStates p5.prototype.readPresses = function () { var keyStates = this._p5play.keyStates; var mouseStates = this._p5play.mouseStates; for (var key in keyStates) { if (this.keyIsDown(key)) //if is down { if (keyStates[key] === KEY_IS_UP)//and was up keyStates[key] = KEY_WENT_DOWN; else keyStates[key] = KEY_IS_DOWN; //now is simply down } else //if it's up { if (keyStates[key] === KEY_IS_DOWN)//and was up keyStates[key] = KEY_WENT_UP; else keyStates[key] = KEY_IS_UP; //now is simply down } } //mouse for (var btn in mouseStates) { if (this.mouseIsPressed && this.mouseButton === btn) //if is down { if (mouseStates[btn] === KEY_IS_UP)//and was up mouseStates[btn] = KEY_WENT_DOWN; else mouseStates[btn] = KEY_IS_DOWN; //now is simply down } else //if it's up { if (mouseStates[btn] === KEY_IS_DOWN)//and was up mouseStates[btn] = KEY_WENT_UP; else mouseStates[btn] = KEY_IS_UP; //now is simply down } } }; /** * Turns the quadTree on or off. * A quadtree is a data structure used to optimize collision detection. * It can improve performance when there is a large number of Sprites to be * checked continuously for overlapping. * * p5.play will create and update a quadtree automatically. * * @method useQuadTree * @param {Boolean} use Pass true to enable, false to disable */ p5.prototype.useQuadTree = function (use) { if (this.quadTree !== undefined) { if (use === undefined) return this.quadTree.active; else if (use) this.quadTree.active = true; else this.quadTree.active = false; } else return false; }; //the actual quadTree defineLazyP5Property('quadTree', function () { return new Quadtree({ x: 0, y: 0, width: 0, height: 0 }, 4); }); /* //framerate independent delta, doesn't really work p5.prototype.deltaTime = 1; var now = Date.now(); var then = Date.now(); var INTERVAL_60 = 0.0166666; //60 fps function updateDelta() { then = now; now = Date.now(); deltaTime = ((now - then) / 1000)/INTERVAL_60; // seconds since last frame } */ /** * A Sprite is the main building block of p5.play: * an element able to store images or animations with a set of * properties such as position and visibility. * A Sprite can have a collider that defines the active area to detect * collisions or overlappings with other sprites and mouse interactions. * * To create a Sprite, use * {{#crossLink "p5.play/createSprite:method"}}{{/crossLink}}. * * @class Sprite */ // For details on why these docs aren't in a YUIDoc comment block, see: // // https://github.com/molleindustria/p5.play/pull/67 // // @param {Number} x Initial x coordinate // @param {Number} y Initial y coordinate // @param {Number} width Width of the placeholder rectangle and of the // collider until an image or new collider are set // @param {Number} height Height of the placeholder rectangle and of the // collider until an image or new collider are set function Sprite(pInst, _x, _y, _w, _h) { var pInstBind = createPInstBinder(pInst); var createVector = pInstBind('createVector'); var color = pInstBind('color'); var random = pInstBind('random'); var print = pInstBind('print'); var push = pInstBind('push'); var pop = pInstBind('pop'); var colorMode = pInstBind('colorMode'); var noStroke = pInstBind('noStroke'); var rectMode = pInstBind('rectMode'); var ellipseMode = pInstBind('ellipseMode'); var imageMode = pInstBind('imageMode'); var translate = pInstBind('translate'); var scale = pInstBind('scale'); var rotate = pInstBind('rotate'); var stroke = pInstBind('stroke'); var strokeWeight = pInstBind('strokeWeight'); var line = pInstBind('line'); var noFill = pInstBind('noFill'); var fill = pInstBind('fill'); var textAlign = pInstBind('textAlign'); var textSize = pInstBind('textSize'); var text = pInstBind('text'); var rect = pInstBind('rect'); var cos = pInstBind('cos'); var sin = pInstBind('sin'); var atan2 = pInstBind('atan2'); var quadTree = pInst.quadTree; var camera = pInst.camera; // These are p5 constants that we'd like easy access to. var RGB = p5.prototype.RGB; var CENTER = p5.prototype.CENTER; var LEFT = p5.prototype.LEFT; var BOTTOM = p5.prototype.BOTTOM; /** * The sprite's position of the sprite as a vector (x,y). * @property position * @type {p5.Vector} */ this.position = createVector(_x, _y); /** * The sprite's position at the beginning of the last update as a vector (x,y). * @property previousPosition * @type {p5.Vector} */ this.previousPosition = createVector(_x, _y); /* The sprite's position at the end of the last update as a vector (x,y). Note: this will differ from position whenever the position is changed directly by assignment. */ this.newPosition = createVector(_x, _y); //Position displacement on the x coordinate since the last update this.deltaX = 0; this.deltaY = 0; /** * The sprite's velocity as a vector (x,y) * Velocity is speed broken down to its vertical and horizontal components. * * @property velocity * @type {p5.Vector} */ this.velocity = createVector(0, 0); /** * Set a limit to the sprite's scalar speed regardless of the direction. * The value can only be positive. If set to -1, there's no limit. * * @property maxSpeed * @type {Number} * @default -1 */ this.maxSpeed = -1; /** * Friction factor, reduces the sprite's velocity. * The friction should be close to 0 (eg. 0.01) * 0: no friction * 1: full friction * * @property friction * @type {Number} * @default 0 */ this.friction = 0; /** * The sprite's current collider. * It can either be an Axis Aligned Bounding Box (a non-rotated rectangle) * or a circular collider. * If the sprite is checked for collision, bounce, overlapping or mouse events the * collider is automatically created from the width and height * of the sprite or from the image dimension in case of animate sprites * * You can set a custom collider with Sprite.setCollider * * @property collider * @type {Object} */ this.collider = undefined; //internal use //"default" - no image or custom collider is specified, use the shape width / height //"custom" - specified with setCollider //"image" - no collider is set with setCollider and an image is added this.colliderType = 'none'; /** * Object containing information about the most recent collision/overlapping * To be typically used in combination with Sprite.overlap or Sprite.collide * functions. * The properties are touching.left, touching.right, touching.top, * touching.bottom and are either true or false depending on the side of the * collider. * * @property touching * @type {Object} */ this.touching = {}; this.touching.left = false; this.touching.right = false; this.touching.top = false; this.touching.bottom = false; /** * The mass determines the velocity transfer when sprites bounce * against each other. See Sprite.bounce * The higher the mass the least the sprite will be affected by collisions. * * @property mass * @type {Number} * @default 1 */ this.mass = 1; /** * If set to true the sprite won't bounce or be displaced by collisions * Simulates an infinite mass or an anchored object. * * @property immovable * @type {Boolean} * @default false */ this.immovable = false; //Coefficient of restitution - velocity lost in the bouncing //0 perfectly inelastic , 1 elastic, > 1 hyper elastic /** * Coefficient of restitution. The velocity lost after bouncing. * 1: perfectly elastic, no energy is lost * 0: perfectly inelastic, no bouncing * less than 1: inelastic, this is the most common in nature * greater than 1: hyper elastic, energy is increased like in a pinball bumper * * @property restitution * @type {Number} * @default 1 */ this.restitution = 1; /** * Rotation in degrees of the visual element (image or animation) * Note: this is not the movement's direction, see getDirection. * * @property rotation * @type {Number} * @default 0 */ Object.defineProperty(this, 'rotation', { enumerable: true, get: function () { return this._rotation; }, set: function (value) { this._rotation = value; if (this.rotateToDirection) { this.setSpeed(this.getSpeed(), value); } } }); /** * Internal rotation variable (expressed in degrees). * Note: external callers access this through the rotation property above. * * @private * @property _rotation * @type {Number} * @default 0 */ this._rotation = 0; /** * Rotation change in degrees per frame of thevisual element (image or animation) * Note: this is not the movement's direction, see getDirection. * * @property rotationSpeed * @type {Number} * @default 0 */ this.rotationSpeed = 0; /** * Automatically lock the rotation property of the visual element * (image or animation) to the sprite's movement direction and vice versa. * * @property rotateToDirection * @type {Boolean} * @default false */ this.rotateToDirection = false; /** * Determines the rendering order within a group: a sprite with * lower depth will appear below the ones with higher depth. * * Note: drawing a group before another with drawSprites will make * its members appear below the second one, like in normal p5 canvas * drawing. * * @property depth * @type {Number} * @default One more than the greatest existing sprite depth, when calling * createSprite(). When calling new Sprite() directly, depth will * initialize to 0 (not recommended). */ this.depth = 0; /** * Determines the sprite's scale. * Example: 2 will be twice the native size of the visuals, * 0.5 will be half. Scaling up may make images blurry. * * @property scale * @type {Number} * @default 1 */ this.scale = 1; var dirX = 1; var dirY = 1; /** * The sprite's visibility. * * @property visible * @type {Boolean} * @default true */ this.visible = true; /** * If set to true sprite will track its mouse state. * the properties mouseIsPressed and mouseIsOver will be updated. * Note: automatically set to true if the functions * onMouseReleased or onMousePressed are set. * * @property mouseActive * @type {Boolean} * @default false */ this.mouseActive = false; /** * True if mouse is on the sprite's collider. * Read only. * * @property mouseIsOver * @type {Boolean} */ this.mouseIsOver = false; /** * True if mouse is pressed on the sprite's collider. * Read only. * * @property mouseIsPressed * @type {Boolean} */ this.mouseIsPressed = false; /* * Width of the sprite's current image. * If no images or animations are set it's the width of the * placeholder rectangle. * Used internally to make calculations and draw the sprite. * * @private * @property _internalWidth * @type {Number} * @default 100 */ this._internalWidth = _w; /* * Height of the sprite's current image. * If no images or animations are set it's the height of the * placeholder rectangle. * Used internally to make calculations and draw the sprite. * * @private * @property _internalHeight * @type {Number} * @default 100 */ this._internalHeight = _h; /* * _internalWidth and _internalHeight are used for all p5.play * calculations, but width and height can be extended. For example, * you may want users to always get and set a scaled width: Object.defineProperty(this, 'width', { enumerable: true, configurable: true, get: function() { return this._internalWidth * this.scale; }, set: function(value) { this._internalWidth = value / this.scale; } }); */ /** * Width of the sprite's current image. * If no images or animations are set it's the width of the * placeholder rectangle. * * @property width * @type {Number} * @default 100 */ Object.defineProperty(this, 'width', { enumerable: true, configurable: true, get: function () { return this._internalWidth; }, set: function (value) { this._internalWidth = value; } }); if (_w === undefined) this.width = 100; else this.width = _w; /** * Height of the sprite's current image. * If no images or animations are set it's the height of the * placeholder rectangle. * * @property height * @type {Number} * @default 100 */ Object.defineProperty(this, 'height', { enumerable: true, configurable: true, get: function () { return this._internalHeight; }, set: function (value) { this._internalHeight = value; } }); if (_h === undefined) this.height = 100; else this.height = _h; /** * Unscaled width of the sprite * If no images or animations are set it's the width of the * placeholder rectangle. * * @property originalWidth * @type {Number} * @default 100 */ this.originalWidth = this._internalWidth; /** * Unscaled height of the sprite * If no images or animations are set it's the height of the * placeholder rectangle. * * @property originalHeight * @type {Number} * @default 100 */ this.originalHeight = this._internalHeight; /** * True if the sprite has been removed. * * @property removed * @type {Boolean} */ this.removed = false; /** * Cycles before self removal. * Set it to initiate a countdown, every draw cycle the property is * reduced by 1 unit. At 0 it will call a sprite.remove() * Disabled if set to -1. * * @property life * @type {Number} * @default -1 */ this.life = -1; /** * If set to true, draws an outline of the collider, the depth, and center. * * @property debug * @type {Boolean} * @default false */ this.debug = false; /** * If no image or animations are set this is color of the * placeholder rectangle * * @property shapeColor * @type {color} */ this.shapeColor = color(random(255), random(255), random(255)); /** * Groups the sprite belongs to, including allSprites * * @property groups * @type {Array} */ this.groups = []; var animations = {}; //The current animation's label. var currentAnimation = ''; /** * Reference to the current animation. * * @property animation * @type {Animation} */ this.animation = undefined; /* * @private * Keep animation properties in sync with how the animation changes. */ this._syncAnimationSizes = function () { //has an animation but the collider is still default //the animation wasn't loaded. if the animation is not a 1x1 image //it means it just finished loading if (this.colliderType === 'default' && animations[currentAnimation].getWidth() !== 1 && animations[currentAnimation].getHeight() !== 1) { this.collider = this.getBoundingBox(); this.colliderType = 'image'; this._internalWidth = animations[currentAnimation].getWidth() * abs(this._getScaleX()); this._internalHeight = animations[currentAnimation].getHeight() * abs(this._getScaleY()); //quadTree.insert(this); } //update size and collider if (animations[currentAnimation].frameChanged || this.width === undefined || this.height === undefined) { //this.collider = this.getBoundingBox(); this._internalWidth = animations[currentAnimation].getWidth() * abs(this._getScaleX()); this._internalHeight = animations[currentAnimation].getHeight() * abs(this._getScaleY()); } }; /** * Updates the sprite. * Called automatically at the beginning of the draw cycle. * * @method update */ this.update = function () { if (!this.removed) { //if there has been a change somewhere after the last update //the old position is the last position registered in the update if (this.newPosition !== this.position) this.previousPosition = createVector(this.newPosition.x, this.newPosition.y); else this.previousPosition = createVector(this.position.x, this.position.y); this.velocity.x *= 1 - this.friction; this.velocity.y *= 1 - this.friction; if (this.maxSpeed !== -1) this.limitSpeed(this.maxSpeed); if (this.rotateToDirection && this.velocity.mag() > 0) this._rotation = this.getDirection(); this.rotation += this.rotationSpeed; this.position.x += this.velocity.x; this.position.y += this.velocity.y; this.newPosition = createVector(this.position.x, this.position.y); this.deltaX = this.position.x - this.previousPosition.x; this.deltaY = this.position.y - this.previousPosition.y; //if there is an animation if (animations[currentAnimation]) { //update it animations[currentAnimation].update(); this._syncAnimationSizes(); } //a collider is created either manually with setCollider or //when I check this sprite for collisions or overlaps if (this.collider) { if (this.collider instanceof AABB) { //scale / rotate collider var t; if (pInst._angleMode === pInst.RADIANS) { t = radians(this.rotation); } else { t = this.rotation; } if (this.colliderType === 'custom') { this.collider.extents.x = this.collider.originalExtents.x * abs(this._getScaleX()) * abs(cos(t)) + this.collider.originalExtents.y * abs(this._getScaleY()) * abs(sin(t)); this.collider.extents.y = this.collider.originalExtents.x * abs(this._getScaleX()) * abs(sin(t)) + this.collider.originalExtents.y * abs(this._getScaleY()) * abs(cos(t)); } else if (this.colliderType === 'default') { this.collider.extents.x = this._internalWidth * abs(this._getScaleX()) * abs(cos(t)) + this._internalHeight * abs(this._getScaleY()) * abs(sin(t)); this.collider.extents.y = this._internalWidth * abs(this._getScaleX()) * abs(sin(t)) + this._internalHeight * abs(this._getScaleY()) * abs(cos(t)); } else if (this.colliderType === 'image') { this.collider.extents.x = this._internalWidth * abs(cos(t)) + this._internalHeight * abs(sin(t)); this.collider.extents.y = this._internalWidth * abs(sin(t)) + this._internalHeight * abs(cos(t)); } } if (this.collider instanceof CircleCollider) { //print(this.scale); this.collider.radius = this.collider.originalRadius * abs(this.scale); } }//end collider != null //mouse actions if (this.mouseActive) { //if no collider set it if (!this.collider) this.setDefaultCollider(); this.mouseUpdate(); } else { if (typeof (this.onMouseOver) === 'function' || typeof (this.onMouseOut) === 'function' || typeof (this.onMousePressed) === 'function' || typeof (this.onMouseReleased) === 'function') { //if a mouse function is set //it's implied we want to have it mouse active so //we do this automatically this.mouseActive = true; //if no collider set it if (!this.collider) this.setDefaultCollider(); this.mouseUpdate(); } } //self destruction countdown if (this.life > 0) this.life--; if (this.life === 0) this.remove(); } };//end update /** * Creates a default collider matching the size of the * placeholder rectangle or the bounding box of the image. * * @method setDefaultCollider */ this.setDefaultCollider = function () { //if has animation get the animation bounding box //working only for preloaded images if (animations[currentAnimation] && (animations[currentAnimation].getWidth() !== 1 && animations[currentAnimation].getHeight() !== 1)) { this.collider = this.getBoundingBox(); this._internalWidth = animations[currentAnimation].getWidth() * abs(this._getScaleX()); this._internalHeight = animations[currentAnimation].getHeight() * abs(this._getScaleY()); //quadTree.insert(this); this.colliderType = 'image'; //print("IMAGE COLLIDER ADDED"); } else if (animations[currentAnimation] && animations[currentAnimation].getWidth() === 1 && animations[currentAnimation].getHeight() === 1) { //animation is still loading //print("wait"); } else //get the with and height defined at the creation { this.collider = new AABB(pInst, this.position, createVector(this._internalWidth, this._internalHeight)); //quadTree.insert(this); this.colliderType = 'default'; } pInst.quadTree.insert(this); }; /** * Updates the sprite mouse states and triggers the mouse events: * onMouseOver, onMouseOut, onMousePressed, onMouseReleased * * @method mouseUpdate */ this.mouseUpdate = function () { var mouseWasOver = this.mouseIsOver; var mouseWasPressed = this.mouseIsPressed; this.mouseIsOver = false; this.mouseIsPressed = false; var mousePosition; if (camera.active) mousePosition = createVector(camera.mouseX, camera.mouseY); else mousePosition = createVector(pInst.mouseX, pInst.mouseY); //rollover if (this.collider) { if (this.collider instanceof CircleCollider) { if (dist(mousePosition.x, mousePosition.y, this.collider.center.x, this.collider.center.y) < this.collider.radius) this.mouseIsOver = true; } else if (this.collider instanceof AABB) { if (mousePosition.x > this.collider.left() && mousePosition.y > this.collider.top() && mousePosition.x < this.collider.right() && mousePosition.y < this.collider.bottom()) { this.mouseIsOver = true; } } //global p5 var if (this.mouseIsOver && pInst.mouseIsPressed) this.mouseIsPressed = true; //event change - call functions if (!mouseWasOver && this.mouseIsOver && this.onMouseOver !== undefined) if (typeof (this.onMouseOver) === 'function') this.onMouseOver.call(this, this); else print('Warning: onMouseOver should be a function'); if (mouseWasOver && !this.mouseIsOver && this.onMouseOut !== undefined) if (typeof (this.onMouseOut) === 'function') this.onMouseOut.call(this, this); else print('Warning: onMouseOut should be a function'); if (!mouseWasPressed && this.mouseIsPressed && this.onMousePressed !== undefined) if (typeof (this.onMousePressed) === 'function') this.onMousePressed.call(this, this); else print('Warning: onMousePressed should be a function'); if (mouseWasPressed && !pInst.mouseIsPressed && !this.mouseIsPressed && this.onMouseReleased !== undefined) if (typeof (this.onMouseReleased) === 'function') this.onMouseReleased.call(this, this); else print('Warning: onMouseReleased should be a function'); } }; /** * Sets a collider for the sprite. * * In p5.play a Collider is an invisible circle or rectangle * that can have any size or position relative to the sprite and which * will be used to detect collisions and overlapping with other sprites, * or the mouse cursor. * * If the sprite is checked for collision, bounce, overlapping or mouse events * a collider is automatically created from the width and height parameter * passed at the creation of the sprite or the from the image dimension in case * of animated sprites. * * Often the image bounding box is not appropriate as the active area for * collision detection so you can set a circular or rectangular sprite with * different dimensions and offset from the sprite's center. * * There are four ways to call this method: * * 1. setCollider("rectangle") * 2. setCollider("rectangle", offsetX, offsetY, width, height) * 3. setCollider("circle") * 4. setCollider("circle", offsetX, offsetY, radius) * * @method setCollider * @param {String} type Either "rectangle" or "circle" * @param {Number} offsetX Collider x position from the center of the sprite * @param {Number} offsetY Collider y position from the center of the sprite * @param {Number} width Collider width or radius * @param {Number} height Collider height * @throws {TypeError} if given invalid parameters. */ this.setCollider = function (type, offsetX, offsetY, width, height) { if (!(type === 'rectangle' || type === 'circle')) { throw new TypeError('setCollider expects the first argument to be either "circle" or "rectangle"'); } else if (type === 'circle' && arguments.length > 1 && arguments.length < 4) { throw new TypeError('Usage: setCollider("circle") or setCollider("circle", offsetX, offsetY, radius)'); } else if (type === 'circle' && arguments.length > 4) { pInst._warn('Extra parameters to setCollider were ignored. Usage: setCollider("circle") or setCollider("circle", offsetX, offsetY, radius)'); } else if (type === 'rectangle' && arguments.length > 1 && arguments.length < 5) { throw new TypeError('Usage: setCollider("rectangle") or setCollider("rectangle", offsetX, offsetY, width, height)'); } else if (type === 'rectangle' && arguments.length > 5) { pInst._warn('Extra parameters to setCollider were ignored. Usage: setCollider("rectangle") or setCollider("rectangle", offsetX, offsetY, width, height)'); } this.colliderType = 'custom'; var v = createVector(offsetX, offsetY); if (type === 'rectangle' && arguments.length === 1) { this.collider = new AABB(pInst, this.position, createVector(this.width, this.height)); } else if (type === 'rectangle' && arguments.length >= 5) { this.collider = new AABB(pInst, this.position, createVector(width, height), v); } else if (type === 'circle' && arguments.length === 1) { this.collider = new CircleCollider(pInst, this.position, Math.floor(Math.max(this.width, this.height) / 2)); } else if (type === 'circle' && arguments.length >= 4) { this.collider = new CircleCollider(pInst, this.position, width, v); } quadTree.insert(this); }; /** * Returns a the bounding box of the current image * @method getBoundingBox */ this.getBoundingBox = function () { var w = animations[currentAnimation].getWidth() * abs(this._getScaleX()); var h = animations[currentAnimation].getHeight() * abs(this._getScaleY()); //if the bounding box is 1x1 the image is not loaded //potential issue with actual 1x1 images if (w === 1 && h === 1) { //not loaded yet return new AABB(pInst, this.position, createVector(w, h)); } else { return new AABB(pInst, this.position, createVector(w, h)); } }; /** * Sets the sprite's horizontal mirroring. * If 1 the images displayed normally * If -1 the images are flipped horizontally * If no argument returns the current x mirroring * * @method mirrorX * @param {Number} dir Either 1 or -1 * @return {Number} Current mirroring if no parameter is specified */ this.mirrorX = function (dir) { if (dir === 1 || dir === -1) dirX = dir; else return dirX; }; /** * Sets the sprite's vertical mirroring. * If 1 the images displayed normally * If -1 the images are flipped vertically * If no argument returns the current y mirroring * * @method mirrorY * @param {Number} dir Either 1 or -1 * @return {Number} Current mirroring if no parameter is specified */ this.mirrorY = function (dir) { if (dir === 1 || dir === -1) dirY = dir; else return dirY; }; /* * Returns the value the sprite should be scaled in the X direction. * Used to calculate rendering and collisions. * @private */ this._getScaleX = function () { return this.scale; }; /* * Returns the value the sprite should be scaled in the Y direction. * Used to calculate rendering and collisions. * @private */ this._getScaleY = function () { return this.scale; }; /** * Manages the positioning, scale and rotation of the sprite * Called automatically, it should not be overridden * @private * @final * @method display */ this.display = function () { if (this.visible && !this.removed) { push(); colorMode(RGB); noStroke(); rectMode(CENTER); ellipseMode(CENTER); imageMode(CENTER); translate(this.position.x, this.position.y); scale(this._getScaleX() * dirX, this._getScaleY() * dirY); if (pInst._angleMode === pInst.RADIANS) { rotate(radians(this.rotation)); } else { rotate(this.rotation); } this.draw(); //draw debug info pop(); if (this.debug) { push(); //draw the anchor point stroke(0, 255, 0); strokeWeight(1); line(this.position.x - 10, this.position.y, this.position.x + 10, this.position.y); line(this.position.x, this.position.y - 10, this.position.x, this.position.y + 10); noFill(); //depth number noStroke(); fill(0, 255, 0); textAlign(LEFT, BOTTOM); textSize(16); text(this.depth + '', this.position.x + 4, this.position.y - 2); noFill(); stroke(0, 255, 0); //bounding box if (this.collider !== undefined) { this.collider.draw(); } pop(); } } }; /** * Manages the visuals of the sprite. * It can be overridden with a custom drawing function. * The 0,0 point will be the center of the sprite. * Example: * sprite.draw = function() { ellipse(0,0,10,10) } * Will display the sprite as circle. * * @method draw */ this.draw = function () { if (currentAnimation !== '' && animations) { if (animations[currentAnimation]) animations[currentAnimation].draw(0, 0, 0); } else { noStroke(); fill(this.shapeColor); rect(0, 0, this._internalWidth, this._internalHeight); } }; /** * Removes the Sprite from the sketch. * The removed Sprite won't be drawn or updated anymore. * * @method remove */ this.remove = function () { this.removed = true; quadTree.removeObject(this); //when removed from the "scene" also remove all the references in all the groups while (this.groups.length > 0) { this.groups[0].remove(this); } }; /** * Sets the velocity vector. * * @method setVelocity * @param {Number} x X component * @param {Number} y Y component */ this.setVelocity = function (x, y) { this.velocity.x = x; this.velocity.y = y; }; /** * Calculates the scalar speed. * * @method getSpeed * @return {Number} Scalar speed */ this.getSpeed = function () { return this.velocity.mag(); }; /** * Calculates the movement's direction in degrees. * * @method getDirection * @return {Number} Angle in degrees */ this.getDirection = function () { var direction = atan2(this.velocity.y, this.velocity.x); if (isNaN(direction)) direction = 0; // Unlike Math.atan2, the atan2 method above will return degrees if // the current p5 angleMode is DEGREES, and radians if the p5 angleMode is // RADIANS. This method should always return degrees (for now). // See https://github.com/molleindustria/p5.play/issues/94 if (pInst._angleMode === pInst.RADIANS) { direction = degrees(direction); } return direction; }; /** * Adds the sprite to an existing group * * @method addToGroup * @param {Object} group */ this.addToGroup = function (group) { if (group instanceof Array) group.add(this); else print('addToGroup error: ' + group + ' is not a group'); }; /** * Limits the scalar speed. * * @method limitSpeed * @param {Number} max Max speed: positive number */ this.limitSpeed = function (max) { //update linear speed var speed = this.getSpeed(); if (abs(speed) > max) { //find reduction factor var k = max / abs(speed); this.velocity.x *= k; this.velocity.y *= k; } }; /** * Set the speed and direction of the sprite. * The action overwrites the current velocity. * If direction is not supplied, the current direction is maintained. * If direction is not supplied and there is no current velocity, the current * rotation angle used for the direction. * * @method setSpeed * @param {Number} speed Scalar speed * @param {Number} [angle] Direction in degrees */ this.setSpeed = function (speed, angle) { var a; if (typeof angle === 'undefined') { if (this.velocity.x !== 0 || this.velocity.y !== 0) { a = pInst.atan2(this.velocity.y, this.velocity.x); } else { if (pInst._angleMode === pInst.RADIANS) { a = radians(this._rotation); } else { a = this._rotation; } } } else { if (pInst._angleMode === pInst.RADIANS) { a = radians(angle); } else { a = angle; } } this.velocity.x = cos(a) * speed; this.velocity.y = sin(a) * speed; }; /** * Pushes the sprite in a direction defined by an angle. * The force is added to the current velocity. * * @method addSpeed * @param {Number} speed Scalar speed to add * @param {Number} angle Direction in degrees */ this.addSpeed = function (speed, angle) { var a; if (pInst._angleMode === pInst.RADIANS) { a = radians(angle); } else { a = angle; } this.velocity.x += cos(a) * speed; this.velocity.y += sin(a) * speed; }; /** * Pushes the sprite toward a point. * The force is added to the current velocity. * * @method attractionPoint * @param {Number} magnitude Scalar speed to add * @param {Number} pointX Direction x coordinate * @param {Number} pointY Direction y coordinate */ this.attractionPoint = function (magnitude, pointX, pointY) { var angle = atan2(pointY - this.position.y, pointX - this.position.x); this.velocity.x += cos(angle) * magnitude; this.velocity.y += sin(angle) * magnitude; }; /** * Adds an image to the sprite. * An image will be considered a one-frame animation. * The image should be preloaded in the preload() function using p5 loadImage. * Animations require a identifying label (string) to change them. * The image is stored in the sprite but not necessarily displayed * until Sprite.changeAnimation(label) is called * * Usages: * - sprite.addImage(label, image); * - sprite.addImage(image); * * If only an image is passed no label is specified * * @method addImage * @param {String|p5.Image} label Label or image * @param {p5.Image} [img] Image */ this.addImage = function () { if (typeof arguments[0] === 'string' && arguments[1] instanceof p5.Image) this.addAnimation(arguments[0], arguments[1]); else if (arguments[0] instanceof p5.Image) this.addAnimation('normal', arguments[0]); else throw ('addImage error: allowed usages are or