diff --git a/PROPOSAL_20243197.md b/PROPOSAL_20243197.md index 9d55692..d2a7f7a 100644 --- a/PROPOSAL_20243197.md +++ b/PROPOSAL_20243197.md @@ -7,23 +7,7 @@ URL: http://git.prototyping.id/20243197/20243197 ## Game : BABA is YOU Game Link: [link(steam)](https://store.steampowered.com/app/736260/Baba_Is_You/) Genra: Word Puzzle Game -### Elements -- Objects (Sprite) - - Baba![(character)](assets/Baba.webp) - - Rock![rock](assets/Rock.webp) - - Flag![Flag](assets/Flag.webp) -- Terrain (Blocking) - - Tile(floor) ![tile](assets/Tile.webp) - - Wall(initailly blocking)![wall](assets/Wall.webp) - - lava ![(lava)](assets/Lava.webp) - - Water ![(water)](assets/Water.webp) - - etc.. -- Properties (Action) - - You ![you](assets/You.webp) - - Move ![Move](assets/Move.webp) - - Stop ![Stop](assets/Stop.webp) - - Win ![win](assets/Win.webp) - - etc +User Contol: ASDW ### What does the players have to do? ---> Win! ![Play](assets/BabaPlayEx.gif) @@ -33,8 +17,56 @@ Genra: Word Puzzle Game This is a *word puzzle* based computer game. You make the rules by combining the objects and words (properties). And according to the rules you play the game(*move around and touch the 'win'.*) + + move baba and push the words to make rules. Then within the rules you made, reach the 'win' object and win the stage. + +### Elements +| Objects | Image | Instructions | +| :------ | :---: | :----------- | +| Baba | | initial main caracter. Moves with ASDW | +| Rock | | | +| Flag | | | + +### Terrain (Blocking) +| Objects | Image | Instructions | +| :------ | :---: | :----------- | +| Tile(floor) | | | +| Wall(initailly blocking) | | | +| lava | | | +| Water | | | +| etc.. | + +### Properties (Indicator) +| Objects | Image | Instructions | +| :------ | :---: | :----------: | +| BABA | | | +| Rock | | | +| Flag | | | +| Wall | | | +| Lava | | | +| Water | | | +| etc... + +### Properties (Operator) +| Objects | Image | Instructions | +| :------ | :---: | :----------- | +| IS | | Operator | + +### Properties (Action) +| Objects | Image | Instructions | +| :------ | :---: | :----------- | +| You | | | +| Move | | | +| Win | | | +| Stop | | | +| Hot | | | +| Sink | | if something goes to this tile, it sinks and the tile becomes 'tile'. | +| etc... + ### How will the game look like? + Environment: tile based. + To build the scene described above, I plan to develop the game using modular rules, where each interaction and game logic is handled through well-structured functions. `+`...If I can AI generate the stages of 'baba is you', that will be my final goal of this project. @@ -42,4 +74,28 @@ Genra: Word Puzzle Game ### Main Challenges 1. Make the rules work. 2. Make the elements to be interchangeable. -3. Make the World. (can I AI generate this?) \ No newline at end of file +3. Make the World. (can I AI generate this?) + +### Documentations +- Tile based + +### LOG +| DATE | Updates | +| :--: | :-------------- | +| 2025.04.14 | Updated the Markdown, initial creation of the project | +| 2025.04.15 | Create the folders and js files. Create the HTML, CSS for the projects. | +| 2025.04.19 | Put sprites, make initial stage. | +| 2025.04.20 | Done with initial stage, but no move rock. | + + +### TODO +- Add Move Script +- Add Sprite Combination Script +- Make Rules Modul-able +- Make sprites as animation +- Put in Sound Effects +- Add 'Start Game', 'Pause Game' +- Make 'Game Over', 'Congrats' good looking + +### Reference +https://youtu.be/M5St-vvohzs diff --git a/README.md b/README.md index 4a08bb4..2101beb 100644 --- a/README.md +++ b/README.md @@ -59,4 +59,4 @@ This homework will be evaluated following two criteria: - Submissions after the deadline (even a few minutes) will receive a penalty of 20%. Submissions submitted after 24 hours from the deadline will be ignored (score will be 0). - Keep a screenshot that proves your completed submission. - Writing style might be considered in grading -- Other subjective metrics by prof may apply +- Other subjective metrics by prof may apply \ No newline at end of file diff --git a/assets/Baba Is You OST - Baba Is You Theme.mp3 b/assets/Baba Is You OST - Baba Is You Theme.mp3 new file mode 100644 index 0000000..8136370 Binary files /dev/null and b/assets/Baba Is You OST - Baba Is You Theme.mp3 differ diff --git a/assets/Hot.webp b/assets/Hot.webp new file mode 100644 index 0000000..7947b68 Binary files /dev/null and b/assets/Hot.webp differ diff --git a/assets/Sink.webp b/assets/Sink.webp new file mode 100644 index 0000000..d3528e2 Binary files /dev/null and b/assets/Sink.webp differ diff --git a/assets/Text_BABA.webp b/assets/Text_BABA.webp new file mode 100644 index 0000000..d01bd90 Binary files /dev/null and b/assets/Text_BABA.webp differ diff --git a/assets/Text_FLAG.webp b/assets/Text_FLAG.webp new file mode 100644 index 0000000..5251b5a Binary files /dev/null and b/assets/Text_FLAG.webp differ diff --git a/assets/Text_IS.webp b/assets/Text_IS.webp new file mode 100644 index 0000000..d310516 Binary files /dev/null and b/assets/Text_IS.webp differ diff --git a/assets/Text_LAVA.webp b/assets/Text_LAVA.webp new file mode 100644 index 0000000..9c567d6 Binary files /dev/null and b/assets/Text_LAVA.webp differ diff --git a/assets/Text_ROCK.webp b/assets/Text_ROCK.webp new file mode 100644 index 0000000..f042dd4 Binary files /dev/null and b/assets/Text_ROCK.webp differ diff --git a/assets/Text_WALL.webp b/assets/Text_WALL.webp new file mode 100644 index 0000000..b9bcd7c Binary files /dev/null and b/assets/Text_WALL.webp differ diff --git a/assets/Text_WATER.webp b/assets/Text_WATER.webp new file mode 100644 index 0000000..c9e1c2f Binary files /dev/null and b/assets/Text_WATER.webp differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..e4a93f4 --- /dev/null +++ b/index.html @@ -0,0 +1,26 @@ + + + + + + Document + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/p5.play.js b/lib/p5.play.js new file mode 100644 index 0000000..2a05d2b --- /dev/null +++ b/lib/p5.play.js @@ -0,0 +1,6764 @@ +/* +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