class Bump { constructor(renderingEngine = PIXI) { if (renderingEngine === undefined) throw new Error("Please assign a rendering engine in the constructor before using bump.js"); //Find out which rendering engine is being used (the default is Pixi) this.renderer = ""; //If the `renderingEngine` is Pixi, set up Pixi object aliases if (renderingEngine.ParticleContainer && renderingEngine.Sprite) { this.renderer = "pixi"; } } //`addCollisionProperties` adds extra properties to sprites to help //simplify the collision code. It won't add these properties if they //already exist on the sprite. After these properties have been //added, this methods adds a Boolean property to the sprite called `_bumpPropertiesAdded` //and sets it to `true` to flag that the sprite has these //new properties addCollisionProperties(sprite) { //Add properties to Pixi sprites if (this.renderer === "pixi") { //gx if (sprite.gx === undefined) { Object.defineProperty(sprite, "gx", { get(){return sprite.getGlobalPosition().x}, enumerable: true, configurable: true }); } //gy if (sprite.gy === undefined) { Object.defineProperty(sprite, "gy", { get(){return sprite.getGlobalPosition().y}, enumerable: true, configurable: true }); } //centerX if (sprite.centerX === undefined) { Object.defineProperty(sprite, "centerX", { get(){return sprite.x + sprite.width / 2}, enumerable: true, configurable: true }); } //centerY if (sprite.centerY === undefined) { Object.defineProperty(sprite, "centerY", { get(){return sprite.y + sprite.height / 2}, enumerable: true, configurable: true }); } //halfWidth if (sprite.halfWidth === undefined) { Object.defineProperty(sprite, "halfWidth", { get(){return sprite.width / 2}, enumerable: true, configurable: true }); } //halfHeight if (sprite.halfHeight === undefined) { Object.defineProperty(sprite, "halfHeight", { get(){return sprite.height / 2}, enumerable: true, configurable: true }); } //xAnchorOffset if (sprite.xAnchorOffset === undefined) { Object.defineProperty(sprite, "xAnchorOffset", { get(){ if (sprite.anchor !== undefined) { return sprite.height * sprite.anchor.x; } else { return 0; } }, enumerable: true, configurable: true }); } //yAnchorOffset if (sprite.yAnchorOffset === undefined) { Object.defineProperty(sprite, "yAnchorOffset", { get(){ if (sprite.anchor !== undefined) { return sprite.width * sprite.anchor.y; } else { return 0; } }, enumerable: true, configurable: true }); } if (sprite.circular && sprite.radius === undefined) { Object.defineProperty(sprite, "radius", { get(){return sprite.width / 2}, enumerable: true, configurable: true }); } //Earlier code - not needed now. /* Object.defineProperties(sprite, { "gx": { get(){return sprite.getGlobalPosition().x}, enumerable: true, configurable: true }, "gy": { get(){return sprite.getGlobalPosition().y}, enumerable: true, configurable: true }, "centerX": { get(){return sprite.x + sprite.width / 2}, enumerable: true, configurable: true }, "centerY": { get(){return sprite.y + sprite.height / 2}, enumerable: true, configurable: true }, "halfWidth": { get(){return sprite.width / 2}, enumerable: true, configurable: true }, "halfHeight": { get(){return sprite.height / 2}, enumerable: true, configurable: true }, "xAnchorOffset": { get(){ if (sprite.anchor !== undefined) { return sprite.height * sprite.anchor.x; } else { return 0; } }, enumerable: true, configurable: true }, "yAnchorOffset": { get(){ if (sprite.anchor !== undefined) { return sprite.width * sprite.anchor.y; } else { return 0; } }, enumerable: true, configurable: true } }); */ } //Add a Boolean `_bumpPropertiesAdded` property to the sprite to flag it //as having these new properties sprite._bumpPropertiesAdded = true; } /* hitTestPoint ------------ Use it to find out if a point is touching a circlular or rectangular sprite. Parameters: a. An object with `x` and `y` properties. b. A sprite object with `x`, `y`, `centerX` and `centerY` properties. If the sprite has a `radius` property, the function will interpret the shape as a circle. */ hitTestPoint(point, sprite) { //Add collision properties if (!sprite._bumpPropertiesAdded) this.addCollisionProperties(sprite); let shape, left, right, top, bottom, vx, vy, magnitude, hit; //Find out if the sprite is rectangular or circular depending //on whether it has a `radius` property if (sprite.radius) { shape = "circle"; } else { shape = "rectangle"; } //Rectangle if (shape === "rectangle") { //Get the position of the sprite's edges left = sprite.x - sprite.xAnchorOffset; right = sprite.x + sprite.width - sprite.xAnchorOffset; top = sprite.y - sprite.yAnchorOffset; bottom = sprite.y + sprite.height - sprite.yAnchorOffset; //Find out if the point is intersecting the rectangle hit = point.x > left && point.x < right && point.y > top && point.y < bottom; } //Circle if (shape === "circle") { //Find the distance between the point and the //center of the circle let vx = point.x - sprite.x - (sprite.width / 2) + sprite.xAnchorOffset, vy = point.y - sprite.y - (sprite.height / 2) + sprite.yAnchorOffset, magnitude = Math.sqrt(vx * vx + vy * vy); //The point is intersecting the circle if the magnitude //(distance) is less than the circle's radius hit = magnitude < sprite.radius; } //`hit` will be either `true` or `false` return hit; } /* hitTestCircle ------------- Use it to find out if two circular sprites are touching. Parameters: a. A sprite object with `centerX`, `centerY` and `radius` properties. b. A sprite object with `centerX`, `centerY` and `radius`. */ hitTestCircle(c1, c2, global = false) { //Add collision properties if (!c1._bumpPropertiesAdded) this.addCollisionProperties(c1); if (!c2._bumpPropertiesAdded) this.addCollisionProperties(c2); let vx, vy, magnitude, combinedRadii, hit; //Calculate the vector between the circles’ center points if (global) { //Use global coordinates vx = (c2.gx + (c2.width / 2) - c2.xAnchorOffset) - (c1.gx + (c1.width / 2) - c1.xAnchorOffset); vy = (c2.gy + (c2.width / 2) - c2.yAnchorOffset) - (c1.gy + (c1.width / 2) - c1.yAnchorOffset); } else { //Use local coordinates vx = (c2.x + (c2.width / 2) - c2.xAnchorOffset) - (c1.x + (c1.width / 2) - c1.xAnchorOffset); vy = (c2.y + (c2.width / 2) - c2.yAnchorOffset) - (c1.y + (c1.width / 2) - c1.yAnchorOffset); } //Find the distance between the circles by calculating //the vector's magnitude (how long the vector is) magnitude = Math.sqrt(vx * vx + vy * vy); //Add together the circles' total radii combinedRadii = c1.radius + c2.radius; //Set `hit` to `true` if the distance between the circles is //less than their `combinedRadii` hit = magnitude < combinedRadii; //`hit` will be either `true` or `false` return hit; } /* circleCollision --------------- Use it to prevent a moving circular sprite from overlapping and optionally bouncing off a non-moving circular sprite. Parameters: a. A sprite object with `x`, `y` `centerX`, `centerY` and `radius` properties. b. A sprite object with `x`, `y` `centerX`, `centerY` and `radius` properties. c. Optional: true or false to indicate whether or not the first sprite should bounce off the second sprite. The sprites can contain an optional mass property that should be greater than 1. */ circleCollision(c1, c2, bounce = false, global = false) { //Add collision properties if (!c1._bumpPropertiesAdded) this.addCollisionProperties(c1); if (!c2._bumpPropertiesAdded) this.addCollisionProperties(c2); let magnitude, combinedRadii, overlap, vx, vy, dx, dy, s = {}, hit = false; //Calculate the vector between the circles’ center points if (global) { //Use global coordinates vx = (c2.gx + (c2.width / 2) - c2.xAnchorOffset) - (c1.gx + (c1.width / 2) - c1.xAnchorOffset); vy = (c2.gy + (c2.width / 2) - c2.yAnchorOffset) - (c1.gy + (c1.width / 2) - c1.yAnchorOffset); } else { //Use local coordinates vx = (c2.x + (c2.width / 2) - c2.xAnchorOffset) - (c1.x + (c1.width / 2) - c1.xAnchorOffset); vy = (c2.y + (c2.width / 2) - c2.yAnchorOffset) - (c1.y + (c1.width / 2) - c1.yAnchorOffset); } //Find the distance between the circles by calculating //the vector's magnitude (how long the vector is) magnitude = Math.sqrt(vx * vx + vy * vy); //Add together the circles' combined half-widths combinedRadii = c1.radius + c2.radius; //Figure out if there's a collision if (magnitude < combinedRadii) { //Yes, a collision is happening hit = true; //Find the amount of overlap between the circles overlap = combinedRadii - magnitude; //Add some "quantum padding". This adds a tiny amount of space //between the circles to reduce their surface tension and make //them more slippery. "0.3" is a good place to start but you might //need to modify this slightly depending on the exact behaviour //you want. Too little and the balls will feel sticky, too much //and they could start to jitter if they're jammed together let quantumPadding = 0.3; overlap += quantumPadding; //Normalize the vector //These numbers tell us the direction of the collision dx = vx / magnitude; dy = vy / magnitude; //Move circle 1 out of the collision by multiplying //the overlap with the normalized vector and subtract it from //circle 1's position c1.x -= overlap * dx; c1.y -= overlap * dy; //Bounce if (bounce) { //Create a collision vector object, `s` to represent the bounce "surface". //Find the bounce surface's x and y properties //(This represents the normal of the distance vector between the circles) s.x = vy; s.y = -vx; //Bounce c1 off the surface this.bounceOffSurface(c1, s); } } return hit; } /* movingCircleCollision --------------------- Use it to make two moving circles bounce off each other. Parameters: a. A sprite object with `x`, `y` `centerX`, `centerY` and `radius` properties. b. A sprite object with `x`, `y` `centerX`, `centerY` and `radius` properties. The sprites can contain an optional mass property that should be greater than 1. */ movingCircleCollision(c1, c2, global = false) { //Add collision properties if (!c1._bumpPropertiesAdded) this.addCollisionProperties(c1); if (!c2._bumpPropertiesAdded) this.addCollisionProperties(c2); let combinedRadii, overlap, xSide, ySide, //`s` refers to the distance vector between the circles s = {}, p1A = {}, p1B = {}, p2A = {}, p2B = {}, hit = false; //Apply mass, if the circles have mass properties c1.mass = c1.mass || 1; c2.mass = c2.mass || 1; //Calculate the vector between the circles’ center points if (global) { //Use global coordinates s.vx = (c2.gx + c2.radius - c2.xAnchorOffset) - (c1.gx + c1.radius - c1.xAnchorOffset); s.vy = (c2.gy + c2.radius - c2.yAnchorOffset) - (c1.gy + c1.radius - c1.yAnchorOffset); } else { //Use local coordinates s.vx = (c2.x + c2.radius - c2.xAnchorOffset) - (c1.x + c1.radius - c1.xAnchorOffset); s.vy = (c2.y + c2.radius - c2.yAnchorOffset) - (c1.y + c1.radius - c1.yAnchorOffset); } //Find the distance between the circles by calculating //the vector's magnitude (how long the vector is) s.magnitude = Math.sqrt(s.vx * s.vx + s.vy * s.vy); //Add together the circles' combined half-widths combinedRadii = c1.radius + c2.radius; //Figure out if there's a collision if (s.magnitude < combinedRadii) { //Yes, a collision is happening hit = true; //Find the amount of overlap between the circles overlap = combinedRadii - s.magnitude; //Add some "quantum padding" to the overlap overlap += 0.3; //Normalize the vector. //These numbers tell us the direction of the collision s.dx = s.vx / s.magnitude; s.dy = s.vy / s.magnitude; //Find the collision vector. //Divide it in half to share between the circles, and make it absolute s.vxHalf = Math.abs(s.dx * overlap / 2); s.vyHalf = Math.abs(s.dy * overlap / 2); //Find the side that the collision is occurring on (c1.x > c2.x) ? xSide = 1 : xSide = -1; (c1.y > c2.y) ? ySide = 1 : ySide = -1; //Move c1 out of the collision by multiplying //the overlap with the normalized vector and adding it to //the circles' positions c1.x = c1.x + (s.vxHalf * xSide); c1.y = c1.y + (s.vyHalf * ySide); //Move c2 out of the collision c2.x = c2.x + (s.vxHalf * -xSide); c2.y = c2.y + (s.vyHalf * -ySide); //1. Calculate the collision surface's properties //Find the surface vector's left normal s.lx = s.vy; s.ly = -s.vx; //2. Bounce c1 off the surface (s) //Find the dot product between c1 and the surface let dp1 = c1.vx * s.dx + c1.vy * s.dy; //Project c1's velocity onto the collision surface p1A.x = dp1 * s.dx; p1A.y = dp1 * s.dy; //Find the dot product of c1 and the surface's left normal (s.lx and s.ly) let dp2 = c1.vx * (s.lx / s.magnitude) + c1.vy * (s.ly / s.magnitude); //Project the c1's velocity onto the surface's left normal p1B.x = dp2 * (s.lx / s.magnitude); p1B.y = dp2 * (s.ly / s.magnitude); //3. Bounce c2 off the surface (s) //Find the dot product between c2 and the surface let dp3 = c2.vx * s.dx + c2.vy * s.dy; //Project c2's velocity onto the collision surface p2A.x = dp3 * s.dx; p2A.y = dp3 * s.dy; //Find the dot product of c2 and the surface's left normal (s.lx and s.ly) let dp4 = c2.vx * (s.lx / s.magnitude) + c2.vy * (s.ly / s.magnitude); //Project c2's velocity onto the surface's left normal p2B.x = dp4 * (s.lx / s.magnitude); p2B.y = dp4 * (s.ly / s.magnitude); //4. Calculate the bounce vectors //Bounce c1 //using p1B and p2A c1.bounce = {}; c1.bounce.x = p1B.x + p2A.x; c1.bounce.y = p1B.y + p2A.y; //Bounce c2 //using p1A and p2B c2.bounce = {}; c2.bounce.x = p1A.x + p2B.x; c2.bounce.y = p1A.y + p2B.y; //Add the bounce vector to the circles' velocity //and add mass if the circle has a mass property c1.vx = c1.bounce.x / c1.mass; c1.vy = c1.bounce.y / c1.mass; c2.vx = c2.bounce.x / c2.mass; c2.vy = c2.bounce.y / c2.mass; } return hit; } /* multipleCircleCollision ----------------------- Checks all the circles in an array for a collision against all the other circles in an array, using `movingCircleCollision` (above) */ multipleCircleCollision(arrayOfCircles, global = false) { for (let i = 0; i < arrayOfCircles.length; i++) { //The first circle to use in the collision check var c1 = arrayOfCircles[i]; for (let j = i + 1; j < arrayOfCircles.length; j++) { //The second circle to use in the collision check let c2 = arrayOfCircles[j]; //Check for a collision and bounce the circles apart if //they collide. Use an optional `mass` property on the sprite //to affect the bounciness of each marble this.movingCircleCollision(c1, c2, global); } } } /* rectangleCollision ------------------ Use it to prevent two rectangular sprites from overlapping. Optionally, make the first rectangle bounce off the second rectangle. Parameters: a. A sprite object with `x`, `y` `centerX`, `centerY`, `halfWidth` and `halfHeight` properties. b. A sprite object with `x`, `y` `centerX`, `centerY`, `halfWidth` and `halfHeight` properties. c. Optional: true or false to indicate whether or not the first sprite should bounce off the second sprite. */ rectangleCollision( r1, r2, bounce = false, global = true ) { //Add collision properties if (!r1._bumpPropertiesAdded) this.addCollisionProperties(r1); if (!r2._bumpPropertiesAdded) this.addCollisionProperties(r2); let collision, combinedHalfWidths, combinedHalfHeights, overlapX, overlapY, vx, vy; //Calculate the distance vector if (global) { vx = (r1.gx + r1.halfWidth - r1.xAnchorOffset) - (r2.gx + r2.halfWidth - r2.xAnchorOffset); vy = (r1.gy + r1.halfHeight - r1.yAnchorOffset) - (r2.gy + r2.halfHeight - r2.yAnchorOffset); } else { //vx = r1.centerX - r2.centerX; //vy = r1.centerY - r2.centerY; vx = (r1.x + r1.halfWidth - r1.xAnchorOffset) - (r2.x + r2.halfWidth - r2.xAnchorOffset); vy = (r1.y + r1.halfHeight - r1.yAnchorOffset) - (r2.y + r2.halfHeight - r2.yAnchorOffset); } //Figure out the combined half-widths and half-heights combinedHalfWidths = r1.halfWidth + r2.halfWidth; combinedHalfHeights = r1.halfHeight + r2.halfHeight; //Check whether vx is less than the combined half widths if (Math.abs(vx) < combinedHalfWidths) { //A collision might be occurring! //Check whether vy is less than the combined half heights if (Math.abs(vy) < combinedHalfHeights) { //A collision has occurred! This is good! //Find out the size of the overlap on both the X and Y axes overlapX = combinedHalfWidths - Math.abs(vx); overlapY = combinedHalfHeights - Math.abs(vy); //The collision has occurred on the axis with the //*smallest* amount of overlap. Let's figure out which //axis that is if (overlapX >= overlapY) { //The collision is happening on the X axis //But on which side? vy can tell us if (vy > 0) { collision = "top"; //Move the rectangle out of the collision r1.y = r1.y + overlapY; } else { collision = "bottom"; //Move the rectangle out of the collision r1.y = r1.y - overlapY; } //Bounce if (bounce) { r1.vy *= -1; /*Alternative //Find the bounce surface's vx and vy properties var s = {}; s.vx = r2.x - r2.x + r2.width; s.vy = 0; //Bounce r1 off the surface //this.bounceOffSurface(r1, s); */ } } else { //The collision is happening on the Y axis //But on which side? vx can tell us if (vx > 0) { collision = "left"; //Move the rectangle out of the collision r1.x = r1.x + overlapX; } else { collision = "right"; //Move the rectangle out of the collision r1.x = r1.x - overlapX; } //Bounce if (bounce) { r1.vx *= -1; /*Alternative //Find the bounce surface's vx and vy properties var s = {}; s.vx = 0; s.vy = r2.y - r2.y + r2.height; //Bounce r1 off the surface this.bounceOffSurface(r1, s); */ } } } else { //No collision } } else { //No collision } //Return the collision string. it will be either "top", "right", //"bottom", or "left" depending on which side of r1 is touching r2. return collision; } /* hitTestRectangle ---------------- Use it to find out if two rectangular sprites are touching. Parameters: a. A sprite object with `centerX`, `centerY`, `halfWidth` and `halfHeight` properties. b. A sprite object with `centerX`, `centerY`, `halfWidth` and `halfHeight` properties. */ hitTestRectangle(r1, r2, global = false) { //Add collision properties if (!r1._bumpPropertiesAdded) this.addCollisionProperties(r1); if (!r2._bumpPropertiesAdded) this.addCollisionProperties(r2); let hit, combinedHalfWidths, combinedHalfHeights, vx, vy; //A variable to determine whether there's a collision hit = false; //Calculate the distance vector if (global) { vx = (r1.gx + r1.halfWidth - r1.xAnchorOffset) - (r2.gx + r2.halfWidth - r2.xAnchorOffset); vy = (r1.gy + r1.halfHeight - r1.yAnchorOffset) - (r2.gy + r2.halfHeight - r2.yAnchorOffset); } else { vx = (r1.x + r1.halfWidth - r1.xAnchorOffset) - (r2.x + r2.halfWidth - r2.xAnchorOffset); vy = (r1.y + r1.halfHeight - r1.yAnchorOffset) - (r2.y + r2.halfHeight - r2.yAnchorOffset); } //Figure out the combined half-widths and half-heights combinedHalfWidths = r1.halfWidth + r2.halfWidth; combinedHalfHeights = r1.halfHeight + r2.halfHeight; //Check for a collision on the x axis if (Math.abs(vx) < combinedHalfWidths) { //A collision might be occuring. Check for a collision on the y axis if (Math.abs(vy) < combinedHalfHeights) { //There's definitely a collision happening hit = true; } else { //There's no collision on the y axis hit = false; } } else { //There's no collision on the x axis hit = false; } //`hit` will be either `true` or `false` return hit; } /* hitTestCircleRectangle ---------------- Use it to find out if a circular shape is touching a rectangular shape Parameters: a. A sprite object with `centerX`, `centerY`, `halfWidth` and `halfHeight` properties. b. A sprite object with `centerX`, `centerY`, `halfWidth` and `halfHeight` properties. */ hitTestCircleRectangle(c1, r1, global = false) { //Add collision properties if (!r1._bumpPropertiesAdded) this.addCollisionProperties(r1); if (!c1._bumpPropertiesAdded) this.addCollisionProperties(c1); let region, collision, c1x, c1y, r1x, r1y; //Use either global or local coordinates if (global) { c1x = c1.gx; c1y = c1.gy r1x = r1.gx; r1y = r1.gy; } else { c1x = c1.x; c1y = c1.y; r1x = r1.x; r1y = r1.y; } //Is the circle above the rectangle's top edge? if (c1y - c1.yAnchorOffset < r1y - r1.halfHeight - r1.yAnchorOffset) { //If it is, we need to check whether it's in the //top left, top center or top right if (c1x - c1.xAnchorOffset < r1x - 1 - r1.halfWidth - r1.xAnchorOffset) { region = "topLeft"; } else if (c1x - c1.xAnchorOffset > r1x + 1 + r1.halfWidth - r1.xAnchorOffset) { region = "topRight"; } else { region = "topMiddle"; } } //The circle isn't above the top edge, so it might be //below the bottom edge else if (c1y - c1.yAnchorOffset > r1y + r1.halfHeight - r1.yAnchorOffset) { //If it is, we need to check whether it's in the bottom left, //bottom center, or bottom right if (c1x - c1.xAnchorOffset < r1x - 1 - r1.halfWidth - r1.xAnchorOffset) { region = "bottomLeft"; } else if (c1x - c1.xAnchorOffset > r1x + 1 + r1.halfWidth - r1.xAnchorOffset) { region = "bottomRight"; } else { region = "bottomMiddle"; } } //The circle isn't above the top edge or below the bottom edge, //so it must be on the left or right side else { if (c1x - c1.xAnchorOffset < r1x - r1.halfWidth - r1.xAnchorOffset) { region = "leftMiddle"; } else { region = "rightMiddle"; } } //Is this the circle touching the flat sides //of the rectangle? if (region === "topMiddle" || region === "bottomMiddle" || region === "leftMiddle" || region === "rightMiddle") { //Yes, it is, so do a standard rectangle vs. rectangle collision test collision = this.hitTestRectangle(c1, r1, global); } //The circle is touching one of the corners, so do a //circle vs. point collision test else { let point = {}; switch (region) { case "topLeft": point.x = r1x - r1.xAnchorOffset; point.y = r1y - r1.yAnchorOffset; break; case "topRight": point.x = r1x + r1.width - r1.xAnchorOffset; point.y = r1y - r1.yAnchorOffset; break; case "bottomLeft": point.x = r1x - r1.xAnchorOffset; point.y = r1y + r1.height - r1.yAnchorOffset; break; case "bottomRight": point.x = r1x + r1.width - r1.xAnchorOffset; point.y = r1y + r1.height - r1.yAnchorOffset; } //Check for a collision between the circle and the point collision = this.hitTestCirclePoint(c1, point, global); } //Return the result of the collision. //The return value will be `undefined` if there's no collision if (collision) { return region; } else { return collision; } } /* hitTestCirclePoint ------------------ Use it to find out if a circular shape is touching a point Parameters: a. A sprite object with `centerX`, `centerY`, and `radius` properties. b. A point object with `x` and `y` properties. */ hitTestCirclePoint(c1, point, global = false) { //Add collision properties if (!c1._bumpPropertiesAdded) this.addCollisionProperties(c1); //A point is just a circle with a diameter of //1 pixel, so we can cheat. All we need to do is an ordinary circle vs. circle //Collision test. Just supply the point with the properties //it needs point.diameter = 1; point.width = point.diameter; point.radius = 0.5; point.centerX = point.x; point.centerY = point.y; point.gx = point.x; point.gy = point.y; point.xAnchorOffset = 0; point.yAnchorOffset = 0; point._bumpPropertiesAdded = true; return this.hitTestCircle(c1, point, global); } /* circleRectangleCollision ------------------------ Use it to bounce a circular shape off a rectangular shape Parameters: a. A sprite object with `centerX`, `centerY`, `halfWidth` and `halfHeight` properties. b. A sprite object with `centerX`, `centerY`, `halfWidth` and `halfHeight` properties. */ circleRectangleCollision( c1, r1, bounce = false, global = false ) { //Add collision properties if (!r1._bumpPropertiesAdded) this.addCollisionProperties(r1); if (!c1._bumpPropertiesAdded) this.addCollisionProperties(c1); let region, collision, c1x, c1y, r1x, r1y; //Use either the global or local coordinates if (global) { c1x = c1.gx; c1y = c1.gy; r1x = r1.gx; r1y = r1.gy; } else { c1x = c1.x; c1y = c1.y; r1x = r1.x; r1y = r1.y; } //Is the circle above the rectangle's top edge? if (c1y - c1.yAnchorOffset < r1y - r1.halfHeight - r1.yAnchorOffset) { //If it is, we need to check whether it's in the //top left, top center or top right if (c1x - c1.xAnchorOffset < r1x - 1 - r1.halfWidth - r1.xAnchorOffset) { region = "topLeft"; } else if (c1x - c1.xAnchorOffset > r1x + 1 + r1.halfWidth - r1.xAnchorOffset) { region = "topRight"; } else { region = "topMiddle"; } } //The circle isn't above the top edge, so it might be //below the bottom edge else if (c1y - c1.yAnchorOffset > r1y + r1.halfHeight - r1.yAnchorOffset) { //If it is, we need to check whether it's in the bottom left, //bottom center, or bottom right if (c1x - c1.xAnchorOffset < r1x - 1 - r1.halfWidth - r1.xAnchorOffset) { region = "bottomLeft"; } else if (c1x - c1.xAnchorOffset > r1x + 1 + r1.halfWidth - r1.xAnchorOffset) { region = "bottomRight"; } else { region = "bottomMiddle"; } } //The circle isn't above the top edge or below the bottom edge, //so it must be on the left or right side else { if (c1x - c1.xAnchorOffset < r1x - r1.halfWidth - r1.xAnchorOffset) { region = "leftMiddle"; } else { region = "rightMiddle"; } } //Is this the circle touching the flat sides //of the rectangle? if (region === "topMiddle" || region === "bottomMiddle" || region === "leftMiddle" || region === "rightMiddle") { //Yes, it is, so do a standard rectangle vs. rectangle collision test collision = this.rectangleCollision(c1, r1, bounce, global); } //The circle is touching one of the corners, so do a //circle vs. point collision test else { let point = {}; switch (region) { case "topLeft": point.x = r1x - r1.xAnchorOffset; point.y = r1y - r1.yAnchorOffset; break; case "topRight": point.x = r1x + r1.width - r1.xAnchorOffset; point.y = r1y - r1.yAnchorOffset; break; case "bottomLeft": point.x = r1x - r1.xAnchorOffset; point.y = r1y + r1.height - r1.yAnchorOffset; break; case "bottomRight": point.x = r1x + r1.width - r1.xAnchorOffset; point.y = r1y + r1.height - r1.yAnchorOffset; } //Check for a collision between the circle and the point collision = this.circlePointCollision(c1, point, bounce, global); } if (collision) { return region; } else { return collision; } } /* circlePointCollision -------------------- Use it to boucnce a circle off a point. Parameters: a. A sprite object with `centerX`, `centerY`, and `radius` properties. b. A point object with `x` and `y` properties. */ circlePointCollision(c1, point, bounce = false, global = false) { //Add collision properties if (!c1._bumpPropertiesAdded) this.addCollisionProperties(c1); //A point is just a circle with a diameter of //1 pixel, so we can cheat. All we need to do is an ordinary circle vs. circle //Collision test. Just supply the point with the properties //it needs point.diameter = 1; point.width = point.diameter; point.radius = 0.5; point.centerX = point.x; point.centerY = point.y; point.gx = point.x; point.gy = point.y; point.xAnchorOffset = 0; point.yAnchorOffset = 0; point._bumpPropertiesAdded = true; return this.circleCollision(c1, point, bounce, global); } /* bounceOffSurface ---------------- Use this to bounce an object off another object. Parameters: a. An object with `v.x` and `v.y` properties. This represents the object that is colliding with a surface. b. An object with `x` and `y` properties. This represents the surface that the object is colliding into. The first object can optionally have a mass property that's greater than 1. The mass will be used to dampen the bounce effect. */ bounceOffSurface(o, s) { //Add collision properties if (!o._bumpPropertiesAdded) this.addCollisionProperties(o); let dp1, dp2, p1 = {}, p2 = {}, bounce = {}, mass = o.mass || 1; //1. Calculate the collision surface's properties //Find the surface vector's left normal s.lx = s.y; s.ly = -s.x; //Find its magnitude s.magnitude = Math.sqrt(s.x * s.x + s.y * s.y); //Find its normalized values s.dx = s.x / s.magnitude; s.dy = s.y / s.magnitude; //2. Bounce the object (o) off the surface (s) //Find the dot product between the object and the surface dp1 = o.vx * s.dx + o.vy * s.dy; //Project the object's velocity onto the collision surface p1.vx = dp1 * s.dx; p1.vy = dp1 * s.dy; //Find the dot product of the object and the surface's left normal (s.lx and s.ly) dp2 = o.vx * (s.lx / s.magnitude) + o.vy * (s.ly / s.magnitude); //Project the object's velocity onto the surface's left normal p2.vx = dp2 * (s.lx / s.magnitude); p2.vy = dp2 * (s.ly / s.magnitude); //Reverse the projection on the surface's left normal p2.vx *= -1; p2.vy *= -1; //Add up the projections to create a new bounce vector bounce.x = p1.vx + p2.vx; bounce.y = p1.vy + p2.vy; //Assign the bounce vector to the object's velocity //with optional mass to dampen the effect o.vx = bounce.x / mass; o.vy = bounce.y / mass; } /* contain ------- `contain` can be used to contain a sprite with `x` and `y` properties inside a rectangular area. The `contain` function takes four arguments: a sprite with `x` and `y` properties, an object literal with `x`, `y`, `width` and `height` properties. The third argument is a Boolean (true/false) value that determines if the sprite should bounce when it hits the edge of the container. The fourth argument is an extra user-defined callback function that you can call when the sprite hits the container ```js contain(anySprite, {x: 0, y: 0, width: 512, height: 512}, true, callbackFunction); ``` The code above will contain the sprite's position inside the 512 by 512 pixel area defined by the object. If the sprite hits the edges of the container, it will bounce. The `callBackFunction` will run if there's a collision. An additional feature of the `contain` method is that if the sprite has a `mass` property, it will be used to dampen the sprite's bounce in a natural looking way. If the sprite bumps into any of the containing object's boundaries, the `contain` function will return a value that tells you which side the sprite bumped into: “left”, “top”, “right” or “bottom”. Here's how you could keep the sprite contained and also find out which boundary it hit: ```js //Contain the sprite and find the collision value let collision = contain(anySprite, {x: 0, y: 0, width: 512, height: 512}); //If there's a collision, display the boundary that the collision happened on if(collision) { if collision.has("left") console.log("The sprite hit the left"); if collision.has("top") console.log("The sprite hit the top"); if collision.has("right") console.log("The sprite hit the right"); if collision.has("bottom") console.log("The sprite hit the bottom"); } ``` If the sprite doesn't hit a boundary, the value of `collision` will be `undefined`. */ /* contain(sprite, container, bounce = false, extra = undefined) { //Helper methods that compensate for any possible shift the the //sprites' anchor points let nudgeAnchor = (o, value, axis) => { if (o.anchor !== undefined) { if (o.anchor[axis] !== 0) { return value * ((1 - o.anchor[axis]) - o.anchor[axis]); } else { return value; } } else { return value; } }; let compensateForAnchor = (o, value, axis) => { if (o.anchor !== undefined) { if (o.anchor[axis] !== 0) { return value * o.anchor[axis]; } else { return 0; } } else { return 0; } }; let compensateForAnchors = (a, b, property1, property2) => { return compensateForAnchor(a, a[property1], property2) + compensateForAnchor(b, b[property1], property2) }; //Create a set called `collision` to keep track of the //boundaries with which the sprite is colliding let collision = new Set(); //Left if (sprite.x - compensateForAnchor(sprite, sprite.width, "x") < container.x - sprite.parent.gx - compensateForAnchor(container, container.width, "x")) { //Bounce the sprite if `bounce` is true if (bounce) sprite.vx *= -1; //If the sprite has `mass`, let the mass //affect the sprite's velocity if(sprite.mass) sprite.vx /= sprite.mass; //Keep the sprite inside the container sprite.x = container.x - sprite.parent.gx + compensateForAnchor(sprite, sprite.width, "x") - compensateForAnchor(container, container.width, "x"); //Add "left" to the collision set collision.add("left"); } //Top if (sprite.y - compensateForAnchor(sprite, sprite.height, "y") < container.y - sprite.parent.gy - compensateForAnchor(container, container.height, "y")) { if (bounce) sprite.vy *= -1; if(sprite.mass) sprite.vy /= sprite.mass; sprite.y = container.x - sprite.parent.gy + compensateForAnchor(sprite, sprite.height, "y") - compensateForAnchor(container, container.height, "y"); collision.add("top"); } //Right if (sprite.x - compensateForAnchor(sprite, sprite.width, "x") + sprite.width > container.width - compensateForAnchor(container, container.width, "x")) { if (bounce) sprite.vx *= -1; if(sprite.mass) sprite.vx /= sprite.mass; sprite.x = container.width - sprite.width + compensateForAnchor(sprite, sprite.width, "x") - compensateForAnchor(container, container.width, "x"); collision.add("right"); } //Bottom if (sprite.y - compensateForAnchor(sprite, sprite.height, "y") + sprite.height > container.height - compensateForAnchor(container, container.height, "y")) { if (bounce) sprite.vy *= -1; if(sprite.mass) sprite.vy /= sprite.mass; sprite.y = container.height - sprite.height + compensateForAnchor(sprite, sprite.height, "y") - compensateForAnchor(container, container.height, "y"); collision.add("bottom"); } //If there were no collisions, set `collision` to `undefined` if (collision.size === 0) collision = undefined; //The `extra` function runs if there was a collision //and `extra` has been defined if (collision && extra) extra(collision); //Return the `collision` value return collision; } */ contain(sprite, container, bounce = false, extra = undefined) { //Add collision properties if (!sprite._bumpPropertiesAdded) this.addCollisionProperties(sprite); //Give the container x and y anchor offset values, if it doesn't //have any if (container.xAnchorOffset === undefined) container.xAnchorOffset = 0; if (container.yAnchorOffset === undefined) container.yAnchorOffset = 0; if (sprite.parent.gx === undefined) sprite.parent.gx = 0; if (sprite.parent.gy === undefined) sprite.parent.gy = 0; //Create a Set called `collision` to keep track of the //boundaries with which the sprite is colliding let collision = new Set(); //Left if (sprite.x - sprite.xAnchorOffset < container.x - sprite.parent.gx - container.xAnchorOffset) { //Bounce the sprite if `bounce` is true if (bounce) sprite.vx *= -1; //If the sprite has `mass`, let the mass //affect the sprite's velocity if (sprite.mass) sprite.vx /= sprite.mass; //Reposition the sprite inside the container sprite.x = container.x - sprite.parent.gx - container.xAnchorOffset + sprite.xAnchorOffset; //Make a record of the side which the container hit collision.add("left"); } //Top if (sprite.y - sprite.yAnchorOffset < container.y - sprite.parent.gy - container.yAnchorOffset) { if (bounce) sprite.vy *= -1; if (sprite.mass) sprite.vy /= sprite.mass; sprite.y = container.y - sprite.parent.gy - container.yAnchorOffset + sprite.yAnchorOffset;; collision.add("top"); } //Right if (sprite.x - sprite.xAnchorOffset + sprite.width > container.width - container.xAnchorOffset) { if (bounce) sprite.vx *= -1; if (sprite.mass) sprite.vx /= sprite.mass; sprite.x = container.width - sprite.width - container.xAnchorOffset + sprite.xAnchorOffset; collision.add("right"); } //Bottom if (sprite.y - sprite.yAnchorOffset + sprite.height > container.height - container.yAnchorOffset) { if (bounce) sprite.vy *= -1; if (sprite.mass) sprite.vy /= sprite.mass; sprite.y = container.height - sprite.height - container.yAnchorOffset + sprite.yAnchorOffset; collision.add("bottom"); } //If there were no collisions, set `collision` to `undefined` if (collision.size === 0) collision = undefined; //The `extra` function runs if there was a collision //and `extra` has been defined if (collision && extra) extra(collision); //Return the `collision` value return collision; } //`outsideBounds` checks whether a sprite is outide the boundary of //another object. It returns an object called `collision`. `collision` will be `undefined` if there's no //collision. But if there is a collision, `collision` will be //returned as a Set containg strings that tell you which boundary //side was crossed: "left", "right", "top" or "bottom" outsideBounds(s, bounds, extra) { let x = bounds.x, y = bounds.y, width = bounds.width, height = bounds.height; //The `collision` object is used to store which //side of the containing rectangle the sprite hits let collision = new Set(); //Left if (s.x < x - s.width) { collision.add("left"); } //Top if (s.y < y - s.height) { collision.add("top"); } //Right if (s.x > width + s.width) { collision.add("right"); } //Bottom if (s.y > height + s.height) { collision.add("bottom"); } //If there were no collisions, set `collision` to `undefined` if (collision.size === 0) collision = undefined; //The `extra` function runs if there was a collision //and `extra` has been defined if (collision && extra) extra(collision); //Return the `collision` object return collision; } /* _getCenter ---------- A utility that finds the center point of the sprite. If it's anchor point is the sprite's top left corner, then the center is calculated from that point. If the anchor point has been shifted, then the anchor x/y point is used as the sprite's center */ _getCenter(o, dimension, axis) { if (o.anchor !== undefined) { if (o.anchor[axis] !== 0) { return 0; } else { //console.log(o.anchor[axis]) return dimension / 2; } } else { return dimension; } } /* hit --- A convenient universal collision function to test for collisions between rectangles, circles, and points. */ hit(a, b, react = false, bounce = false, global, extra = undefined) { //Local references to bump's collision methods let hitTestPoint = this.hitTestPoint.bind(this), hitTestRectangle = this.hitTestRectangle.bind(this), hitTestCircle = this.hitTestCircle.bind(this), movingCircleCollision = this.movingCircleCollision.bind(this), circleCollision = this.circleCollision.bind(this), hitTestCircleRectangle = this.hitTestCircleRectangle.bind(this), rectangleCollision = this.rectangleCollision.bind(this), circleRectangleCollision = this.circleRectangleCollision.bind(this); let collision, aIsASprite = a.parent !== undefined, bIsASprite = b.parent !== undefined; //Check to make sure one of the arguments isn't an array if (aIsASprite && b instanceof Array || bIsASprite && a instanceof Array) { //If it is, check for a collision between a sprite and an array spriteVsArray(); } else { //If one of the arguments isn't an array, find out what type of //collision check to run collision = findCollisionType(a, b); if (collision && extra) extra(collision); } //Return the result of the collision. //It will be `undefined` if there's no collision and `true` if //there is a collision. `rectangleCollision` sets `collsision` to //"top", "bottom", "left" or "right" depeneding on which side the //collision is occuring on return collision; function findCollisionType(a, b) { //Are `a` and `b` both sprites? //(We have to check again if this function was called from //`spriteVsArray`) let aIsASprite = a.parent !== undefined; let bIsASprite = b.parent !== undefined; if (aIsASprite && bIsASprite) { //Yes, but what kind of sprites? if (a.diameter && b.diameter) { //They're circles return circleVsCircle(a, b); } else if (a.diameter && !b.diameter) { //The first one is a circle and the second is a rectangle return circleVsRectangle(a, b); } else { //They're rectangles return rectangleVsRectangle(a, b); } } //They're not both sprites, so what are they? //Is `a` not a sprite and does it have x and y properties? else if (bIsASprite && !(a.x === undefined) && !(a.y === undefined)) { //Yes, so this is a point vs. sprite collision test return hitTestPoint(a, b); } else { //The user is trying to test some incompatible objects throw new Error(`I'm sorry, ${a} and ${b} cannot be use together in a collision test.'`); } } function spriteVsArray() { //If `a` happens to be the array, flip it around so that it becomes `b` if (a instanceof Array) { let [a, b] = [b, a]; } //Loop through the array in reverse for (let i = b.length - 1; i >= 0; i--) { let sprite = b[i]; collision = findCollisionType(a, sprite); if (collision && extra) extra(collision, sprite); } } function circleVsCircle(a, b) { //If the circles shouldn't react to the collision, //just test to see if they're touching if (!react) { return hitTestCircle(a, b); } //Yes, the circles should react to the collision else { //Are they both moving? if (a.vx + a.vy !== 0 && b.vx + b.vy !== 0) { //Yes, they are both moving //(moving circle collisions always bounce apart so there's //no need for the third, `bounce`, argument) return movingCircleCollision(a, b, global); } else { //No, they're not both moving return circleCollision(a, b, bounce, global); } } } function rectangleVsRectangle(a, b) { //If the rectangles shouldn't react to the collision, just //test to see if they're touching if (!react) { return hitTestRectangle(a, b, global); } else { return rectangleCollision(a, b, bounce, global); } } function circleVsRectangle(a, b) { //If the rectangles shouldn't react to the collision, just //test to see if they're touching if (!react) { return hitTestCircleRectangle(a, b, global); } else { return circleRectangleCollision(a, b, bounce, global); } } } }