Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sprite): allow sprite to be a circle #404

Merged
merged 6 commits into from
Jun 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/pages/reducing-file-size.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ __Note:__ Some of the more advance classes – such as Button or Grid – requir
| `anchor` | `anchor` |
| `group` | `children`, `parent`, `addChild`, `removeChild` |
| `opacity` | `opacity` |
| `radius` | `radius`, collision detection with circle |
| `rotation` | `rotation` |
| `scale` | `scaleX`, `scaleY`, `setScale()` |
| `ttl` | `ttl`, `isAlive` |
Expand All @@ -103,7 +104,7 @@ __Note:__ Some of the more advance classes – such as Button or Grid – requir

| Name | Functionality Enabled |
| ------------- | ------------- |
| `autoNewline` | Setting a fixed with that automatically adds new lines to the text |
| `autoNewline` | Setting a fixed width that automatically adds new lines to the text |
| `newline` | Support for new line characters (`\n`) in the text |
| `rtl` | Support for RTL languages |
| `align` | `textAlign` |
Expand Down
67 changes: 60 additions & 7 deletions src/gameObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { noop, removeFromArray } from './utils.js';
* @param {Number} [properties.y] - Y coordinate of the position vector.
* @param {Number} [properties.width] - Width of the game object.
* @param {Number} [properties.height] - Height of the game object.
* @param {Number} [properties.radius] - Radius of the game object. **Note:** radius is mutually exclusive with `width` and `height` as the GameObject will always use `radius` over `width` and `height` for any logic.
*
* @param {CanvasRenderingContext2D} [properties.context] - The context the game object should draw to. Defaults to [core.getContext()](api/core#getContext).
*
Expand Down Expand Up @@ -83,6 +84,12 @@ class GameObject extends Updatable {
// optionals
// --------------------------------------------------

/**
* The radius of the game object. Represents the local radius of the object as opposed to the [world](api/gameObject#world) radius.
* @memberof GameObject
* @property {Number} radius
*/

// @ifdef GAMEOBJECT_GROUP
/**
* The game objects parent object.
Expand Down Expand Up @@ -319,8 +326,17 @@ class GameObject extends Updatable {
// @ifdef GAMEOBJECT_ANCHOR
// 5) translate to the anchor so (0,0) is the top left corner
// for the render function
let anchorX = -this.width * this.anchor.x;
let anchorY = -this.height * this.anchor.y;
let width = this.width;
let height = this.height;

// @ifdef GAMEOBJECT_RADIUS
if (this.radius) {
width = height = this.radius * 2;
}
// @endif

let anchorX = -width * this.anchor.x;
let anchorY = -height * this.anchor.y;

if (anchorX || anchorY) {
context.translate(anchorX, anchorY);
Expand Down Expand Up @@ -465,7 +481,7 @@ class GameObject extends Updatable {
// @endif

// @ifdef GAMEOBJECT_ROTATION
_wr = 0,
_wrot = 0,
// @endif

// @ifdef GAMEOBJECT_SCALE
Expand All @@ -483,6 +499,14 @@ class GameObject extends Updatable {
this._ww = this.width;
this._wh = this.height;

// @ifdef GAMEOBJECT_RADIUS
// wrx = world radius x, wry = world radius y
if (this.radius) {
this._wrx = this.radius;
this._wry = this.radius;
}
// @endif

// @ifdef GAMEOBJECT_OPACITY
// wo = world opacity
this._wo = _wo * this.opacity;
Expand All @@ -497,13 +521,20 @@ class GameObject extends Updatable {
this._wy = this._wy * _wsy;
this._ww = this.width * this._wsx;
this._wh = this.height * this._wsy;

// @ifdef GAMEOBJECT_RADIUS
if (this.radius) {
this._wrx = this.radius * this._wsx;
this._wry = this.radius * this._wsy;
}
// @endif
// @endif

// @ifdef GAMEOBJECT_ROTATION
// wr = world rotation
this._wr = _wr + this.rotation;
// wrot = world rotation
this._wrot = _wrot + this.rotation;

let { x, y } = rotatePoint({ x: this._wx, y: this._wy }, _wr);
let { x, y } = rotatePoint({ x: this._wx, y: this._wy }, _wrot);
this._wx = x;
this._wy = y;
// @endif
Expand All @@ -528,12 +559,18 @@ class GameObject extends Updatable {
width: this._ww,
height: this._wh,

// @ifdef GAMEOBJECT_RADIUS
radius: this.radius
? { x: this._wrx, y: this._wry }
: undefined,
// @endif

// @ifdef GAMEOBJECT_OPACITY
opacity: this._wo,
// @endif

// @ifdef GAMEOBJECT_ROTATION
rotation: this._wr,
rotation: this._wrot,
// @endif

// @ifdef GAMEOBJECT_SCALE
Expand Down Expand Up @@ -626,6 +663,22 @@ class GameObject extends Updatable {
}
// @endif

// --------------------------------------------------
// radius
// --------------------------------------------------

// @ifdef GAMEOBJECT_RADIUS
get radius() {
// r = radius
return this._r;
}

set radius(value) {
this._r = value;
this._pc();
}
// @endif

// --------------------------------------------------
// opacity
// --------------------------------------------------
Expand Down
59 changes: 53 additions & 6 deletions src/helpers.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { circleRectCollision } from './utils.js';

/**
* A group of helpful functions that are commonly used for game development. Includes things such as converting between radians and degrees and getting random integers.
*
Expand Down Expand Up @@ -235,13 +237,50 @@ export function getStoreItem(key) {
* @returns {Boolean} `true` if the objects collide, `false` otherwise.
*/
export function collides(obj1, obj2) {
[obj1, obj2] = [obj1, obj2].map(obj => getWorldRect(obj));
let rect1 = getWorldRect(obj1);
let rect2 = getWorldRect(obj2);

// @ifdef GAMEOBJECT_RADIUS
// don't work with ellipses (i.e. scaling has made
// the width and height not the same which means it's
// an ellipse and not a circle)
if (
(obj1.radius && rect1.width != rect1.height) ||
(obj2.radius && rect2.width != rect2.height)
) {
return false;
}

[rect1, rect2] = [rect1, rect2].map(rect => {
if ((rect == rect1 ? obj1 : obj2).radius) {
rect.radius = rect.width / 2;
rect.x += rect.radius;
rect.y += rect.radius;
}

return rect;
});

if (obj1.radius && obj2.radius) {
return (
Math.hypot(rect1.x - rect2.x, rect1.y - rect2.y) <
rect1.radius + rect2.radius
);
}

if (obj1.radius || obj2.radius) {
return circleRectCollision(
obj1.radius ? rect1 : rect2, // circle
obj1.radius ? obj2 : obj1 // rect
);
}
// @endif

return (
obj1.x < obj2.x + obj2.width &&
obj1.x + obj1.width > obj2.x &&
obj1.y < obj2.y + obj2.height &&
obj1.y + obj1.height > obj2.y
rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y
);
}

Expand All @@ -254,14 +293,22 @@ export function collides(obj1, obj2) {
* @returns {{x: Number, y: Number, width: Number, height: Number}} The world `x`, `y`, `width`, and `height` of the object.
*/
export function getWorldRect(obj) {
let { x = 0, y = 0, width, height } = obj.world || obj;
let { x = 0, y = 0, width, height, radius } = obj.world || obj;

// take into account tileEngine
if (obj.mapwidth) {
width = obj.mapwidth;
height = obj.mapheight;
}

// @ifdef GAMEOBJECT_RADIUS
// account for circle
if (radius) {
width = radius.x * 2;
height = radius.y * 2;
}
// @endif

// @ifdef GAMEOBJECT_ANCHOR
// account for anchor
if (obj.anchor) {
Expand Down
25 changes: 2 additions & 23 deletions src/pointer.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { getCanvas } from './core.js';
import { on, emit } from './events.js';
import { getWorldRect } from './helpers.js';
import { removeFromArray } from './utils.js';
import { removeFromArray, circleRectCollision } from './utils.js';

/**
* A simple pointer API. You can use it move the main sprite or respond to a pointer event. Works with both mouse and touch events.
Expand Down Expand Up @@ -116,26 +115,6 @@ export function getPointer(canvas = getCanvas()) {
return pointers.get(canvas);
}

/**
* Detection collision between a rectangle and a circle.
* @see https://yal.cc/rectangle-circle-intersection-test/
*
* @param {Object} object - Object to check collision against.
*/
function circleRectCollision(object, pointer) {
let { x, y, width, height } = getWorldRect(object);

// account for camera
do {
x -= object.sx || 0;
y -= object.sy || 0;
} while ((object = object.parent));

let dx = pointer.x - Math.max(x, Math.min(pointer.x, x + width));
let dy = pointer.y - Math.max(y, Math.min(pointer.y, y + height));
return dx * dx + dy * dy < pointer.radius * pointer.radius;
}

/**
* Get the first on top object that the pointer collides with.
*
Expand All @@ -154,7 +133,7 @@ function getCurrentObject(pointer) {
let object = renderedObjects[i];
let collides = object.collidesWithPointer
? object.collidesWithPointer(pointer)
: circleRectCollision(object, pointer);
: circleRectCollision(pointer, object);

if (collides) {
return object;
Expand Down
16 changes: 16 additions & 0 deletions src/sprite.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,22 @@ class Sprite extends GameObjectClass {

if (this.color) {
this.context.fillStyle = this.color;

// @ifdef GAMEOBJECT_RADIUS
if (this.radius) {
this.context.beginPath();
this.context.arc(
this.radius,
this.radius,
this.radius,
0,
Math.PI * 2
);
this.context.fill();
return;
}
// @endif

this.context.fillRect(0, 0, this.width, this.height);
}
}
Expand Down
25 changes: 25 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { getWorldRect } from './helpers.js';

export let noop = () => {};

// style used for DOM nodes needed for screen readers
Expand Down Expand Up @@ -42,3 +44,26 @@ export function removeFromArray(array, item) {
return true;
}
}

/**
* Detection collision between a rectangle and a circle.
* @see https://yal.cc/rectangle-circle-intersection-test/
*
* @param {Object} rect - Rectangular object to check collision against.
* @param {Object} circle - Circular object to check collision against.
*
* @returns {Boolean} True if objects collide.
*/
export function circleRectCollision(circle, rect) {
let { x, y, width, height } = getWorldRect(rect);

// account for camera
do {
x -= rect.sx || 0;
y -= rect.sy || 0;
} while ((rect = rect.parent));

let dx = circle.x - Math.max(x, Math.min(circle.x, x + width));
let dy = circle.y - Math.max(y, Math.min(circle.y, y + height));
return dx * dx + dy * dy < circle.radius * circle.radius;
}
3 changes: 2 additions & 1 deletion test/permutations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ let options = {
'GAMEOBJECT_ANCHOR',
'GAMEOBJECT_GROUP',
'GAMEOBJECT_OPACITY',
'GAMEOBJECT_RADIUS',
'GAMEOBJECT_ROTATION',
'GAMEOBJECT_SCALE'
],
sprite: ['SPRITE_IMAGE', 'SPRITE_ANIMATION'],
sprite: ['SPRITE_IMAGE', 'SPRITE_ANIMATION', 'GAMEOBJECT_RADIUS'],
text: [
'TEXT_AUTONEWLINE',
'TEXT_NEWLINE',
Expand Down
Loading
Loading