/* 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 *
* * var imgEl; * function preload() { * imgEl = loadImageElement("assets/laDefense.jpg"); * } * function setup() { * imageElement(imgEl, 0, 0); * imageElement(imgEl, 0, 0, 100, 100); * imageElement(imgEl, 0, 0, 100, 100, 0, 0, 100, 100); * } * *
*
* * 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