/** Class representing an animation based on a spritesheet and json. */
class Animation {
#spritesheet = undefined;
#frames = undefined;
#animationLoops = {};
#currentAnimationLoop = "";
#currentAnimationFrame = 0;
/**
* Create a new Animation.
* @param {string} spritesheetImagePath - The path to an image representing a spritesheet.
* @param {string} framesJSONPath - The path to a JSON file holding all frames data for the spritesheet.
*/
constructor(spritesheetImagePath, framesJSONPath, speed = 20) {
this.#spritesheet = loadImage(spritesheetImagePath);
this.#frames = loadJSON(framesJSONPath);
setInterval(() => {
if (this.#currentAnimationLoop in this.#animationLoops) {
this.#currentAnimationFrame = (this.#currentAnimationFrame + 1) % this.#animationLoops[this.#currentAnimationLoop].length;
}
}, speed);
}
/**
* Add an animationloop to the animation. An animation loop consists of several frames that are defined in the framesJSON file for this animation.
* Framenumbers are 0-based.
* Frames can be reused in different animationloops.
* @param {string} loopName - The name for this animationloop. This name must be unique!
* @param {number} frameNumbers - the framenumbers for this animationloop, as defined (in order) in the framesJSON file for this animation.
*/
AddAnimationLoop(loopName, ...frameNumbers) {
this.#animationLoops[loopName] = frameNumbers;
}
/**
* Set the current animation loop to a previously-defined animation-loop.
* @param {string} value - the name of the animation-loop that should run from now on.
*/
set CurrentAnimationLoop(value) {
if (this.#currentAnimationLoop == value) {
return;
}
if (value in this.#animationLoops) {
this.#currentAnimationLoop = value;
this.#currentAnimationFrame = 0;
}
}
/**
* Draws the current animation-frame for the currently selected animation-loop.
* @param {number} w - the draw-width for this animation
* @param {number} h - the draw-height for this animation
*/
Draw(w, h) {
if (this.#currentAnimationLoop in this.#animationLoops &&
this.#frames[this.#animationLoops[this.#currentAnimationLoop][this.#currentAnimationFrame]]) {
image(this.#spritesheet, 0, 0, w, h,
this.#frames[this.#animationLoops[this.#currentAnimationLoop][this.#currentAnimationFrame]].frame.x,
this.#frames[this.#animationLoops[this.#currentAnimationLoop][this.#currentAnimationFrame]].frame.y,
this.#frames[this.#animationLoops[this.#currentAnimationLoop][this.#currentAnimationFrame]].frame.width,
this.#frames[this.#animationLoops[this.#currentAnimationLoop][this.#currentAnimationFrame]].frame.height);
}
}
}
class Collider {
#center;
#offset;
#extents;
#originalExtents;
#parent;
#hit = {
left: false,
right: false,
top: false,
bottom: false
};
constructor(_parent, _center, _extents, _offset) {
this.#parent = _parent;
this.#center = _center;
this.#extents = _extents;
this.#originalExtents = _extents.copy();
if (_offset === undefined)
this.#offset = createVector(0, 0);
else
this.#offset = _offset;
}
get HitLocation() {
return this.#hit;
}
get Center() {
return this.#center;
}
get Extents() {
return this.#extents;
}
get Offset() {
return this.#offset;
}
get Min() {
return createVector(this.Center.x + this.#offset.x - this.#extents.x, this.Center.y + this.#offset.y - this.#extents.y);
}
get Max() {
return createVector(this.Center.x + this.#offset.x + this.#extents.x, this.Center.y + this.#offset.y + this.#extents.y);
}
get Right() {
return this.Center.x + this.#offset.x + this.#extents.x / 2;
}
get Left() {
return this.Center.x + this.#offset.x - this.#extents.x / 2;
}
get Top() {
return this.Center.y + this.#offset.y - this.#extents.y / 2;
}
get Bottom() {
return this.Center.y + this.#offset.y + this.#extents.y / 2;
}
get Size() {
return createVector(this.#extents.x * 2, this.#extents.y * 2);
}
Hit(overlap, ...otherColliders) {
this.#hit.left = false;
this.#hit.right = false;
this.#hit.top = false;
this.#hit.bottom = false;
let collidersHit = [];
if (overlap === true) {
otherColliders.forEach(otherCollider => {
if (this !== otherCollider && !this.#parent.Removed) {
if (this.Overlap(otherCollider) === true) {
collidersHit.push(otherCollider);
}
}
});
}
else {
otherColliders.forEach(otherCollider => {
if (this !== otherCollider && !this.#parent.Removed) {
let displacement = this.Collide(otherCollider);
if (displacement.x !== 0 || displacement.y !== 0) {
if (!this.#parent.Immovable) {
this.#parent.Displace(displacement);
}
collidersHit.push(otherCollider);
if (displacement.x > 0)
this.#hit.left = true;
if (displacement.x < 0)
this.#hit.right = true;
if (displacement.y < 0)
this.#hit.bottom = true;
if (displacement.y > 0)
this.#hit.top = true;
}
}
});
}
return collidersHit.map(collider => collider.#parent);
}
UpdateCollider() {
this.#extents.x = this.#parent.Width;
this.#extents.y = this.#parent.Height;
}
Overlap(other) {
//box vs box
if (other instanceof Collider && !other.#parent.Removed && !this.#parent.Removed) {
let difference = other.MinkowskiDifference(this);
if (difference.Min.x <= 0 &&
difference.Max.x >= 0 &&
difference.Min.y <= 0 &&
difference.Max.y >= 0) {
return true;
}
else
return false;
}
return false;
// //box vs circle
// else if (other instanceof CircleCollider) {
// //find closest point to the circle on the box
// let pt = createVector(other.Center.x, other.Center.y);
// //I don't know what's going o try to trace a line from centers to see
// if (other.Center.x < this.left())
// pt.x = this.left();
// else if (other.Center.x > this.right())
// pt.x = this.right();
// if (other.Center.y < this.top())
// pt.y = this.top();
// else if (other.Center.y > this.bottom())
// pt.y = this.bottom();
// let distance = pt.dist(other.Center);
// return distance < other.radius;
// }
}
Collide(other) {
if (other instanceof Collider) {
let md = other.MinkowskiDifference(this);
if (md.Min.x <= 0 &&
md.Max.x >= 0 &&
md.Min.y <= 0 &&
md.Max.y >= 0) {
let boundsPoint = md.ClosestPointOnBoundsToPoint(createVector(0, 0));
return boundsPoint;
}
else
return createVector(0, 0);
}
}
MinkowskiDifference(other) {
let topLeft = this.Min.sub(other.Max);
let fullSize = this.Size.add(other.Size);
let difference = new Collider(null, topLeft.add(fullSize.div(2)), fullSize.div(2));
return difference;
}
ClosestPointOnBoundsToPoint(point) {
// test x first
let minDist = abs(point.x - this.Min.x);
let boundsPoint = createVector(this.Min.x, point.y);
if (abs(this.Max.x - point.x) < minDist) {
minDist = abs(this.Max.x - point.x);
boundsPoint = createVector(this.Max.x, point.y);
}
if (abs(this.Max.y - point.y) < minDist) {
minDist = abs(this.Max.y - point.y);
boundsPoint = createVector(point.x, this.Max.y);
}
if (abs(this.Min.y - point.y) < minDist) {
minDist = abs(this.Min.y - point.y);
boundsPoint = createVector(point.x, this.Min.y);
}
return boundsPoint;
}
}
/**
* Class that represents a Game. Extend this class to create a baseclass for your Game.
*/
class Game {
/**
* Creates a new Game instance.
*/
constructor() {
GameManager.GetInstance().GameInstance = this;
}
}
/** Singleton class that manages the game and handles various p5 functionalities. */
class GameManager {
static #INSTANCE;
/**
* Function to retrieve the current instance of the GameManager. Always use this function instead of the constructor!
* When no instance is available, one is created.
* @returns The singleton instance of the GameManager.
*/
static GetInstance() {
if (GameManager.#INSTANCE == undefined) {
GameManager.#INSTANCE = new GameManager();
GameManager.#INSTANCE.Init();
}
return GameManager.#INSTANCE;
}
#allGameObjects = [];
#collisionLayers = {};
#gameInstance = undefined;
#debugUpdateTimer = 0;
#debugText = "";
static keysPressed = [];
/**
* The instance of the game that is playing.
* @param {Game} value - An instance of a game that the gameManager should manage.
*/
set GameInstance(value) {
this.#gameInstance = value;
}
/**
* Initializes the canvas and game values.
*/
Init() {
createCanvas(Settings.GameWidth, Settings.GameHeight);
if (Settings.Debug === true || Settings.ShowStats === true) {
this.#UpdateDebugText();
setInterval(() => { this.#UpdateDebugText() }, 500);
}
}
/**
* Removes a GameObject from the scene.
* @param {GameObject} gameObject - The GameObject to be removed from the scene.
*/
RemoveGameObject(gameObject) {
this.#allGameObjects = this.#allGameObjects.filter(go => go != gameObject);
for (const layerId in this.#collisionLayers) {
let index = this.#collisionLayers[layerId].indexOf(gameObject);
if (index >= 0) {
this.#collisionLayers[layerId].splice(index, 1);
}
}
}
/**
* Adds a GameObject to the current game.
* @param {GameObject} gameObject - The GameObject to be added to the scene.
*/
AddGameObject(gameObject) {
gameObject.Index = this.#allGameObjects.length;
this.#allGameObjects.push(gameObject);
}
/**
* Puts the given GameObject on the given Collision Layer. Collision Layers are defined in Settings/Settings.js
* @param {GameObject} gameObject - The GameObject to be added to the collision layer
* @param {Settings.collisionLayer} collisionLayer - Collision layer defined in the Settings file
*/
AddGameObjectToCollisionLayer(gameObject, collisionLayer) {
for (const layerId in this.#collisionLayers) {
let index = this.#collisionLayers[layerId].indexOf(gameObject);
if (index >= 0) {
if (layerId === collisionLayer) {
return;
}
else {
this.#collisionLayers[layerId].splice(index, 1);
}
}
}
if (collisionLayer in this.#collisionLayers === false) {
this.#collisionLayers[collisionLayer] = [];
}
this.#collisionLayers[collisionLayer].push(gameObject);
}
/**
* Sets up a Game instance, defined by the type of Game in Settings/Settings.js
*/
static Setup() {
GameManager.GetInstance().#gameInstance = new Settings.GameClass();
}
/**
* Static function that functions as a replacement for p5.draw. This calls Update from the GameManager Instance.
*/
static Draw() {
GameManager.GetInstance().Update();
}
/**
* Static function that functions as a replacement for p5.kyePressed. This calls KeyPressed from the current Game instance.
*/
static KeyPressed() {
GameManager.keysPressed.push(keyCode);
this.#gameInstance.KeyPressed();
}
/**
* Update draws all GameObjects in the correct order and draws debug information in case Settings/Settings.js/Debug was set to true.
*/
Update() {
push();
background(Settings.BackgroundColor);
if (this.#gameInstance) {
this.#gameInstance.Update();
}
this.#allGameObjects.sort((a, b) => a.Depth == b.Depth ? 0 : a.Depth > b.Depth ? 1 : -1);
this.#allGameObjects.forEach(go => go.Display());
for (const layerA in Settings.LayerInteractions) {
let value = Settings.LayerInteractions[layerA];
value.forEach(layerB => {
if (this.#collisionLayers[layerA] && this.#collisionLayers[layerB]) {
this.#collisionLayers[layerA].forEach(go => go.Collide(...this.#collisionLayers[layerB]));
}
});
}
this.#allGameObjects.forEach(go => {
if (go.Collider) {
let gameObjectsWithCollider = this.#allGameObjects.filter(f => f.Collider && f != go)
go.Overlap(...gameObjectsWithCollider);
}
});
if (Settings.ShowGrid) {
this.#DrawGridOverlay();
}
pop();
if (Settings.Debug === true || Settings.ShowStats === true) {
push();
textAlign(RIGHT, TOP);
text(this.#debugText, width - 10, 0);
pop();
}
if (GameManager.keysPressed.length > 0) {
GameManager.keysPressed = [];
}
}
/**
* Updates the debug text that is shown in the upper right corner.
*/
#UpdateDebugText() {
this.#debugText = `
${"debugMode On"}
${Math.round(frameRate())} fps
`;
}
/**
* Draws a Grid overlay, where each square is sized according to Settings/Settings.js/GridSize
*/
#DrawGridOverlay() {
push();
stroke('rgba(0,255,0, 0.25)');
for (let i = 0; i < 100; ++i) {
line(0, i * Settings.GridSize, 100 * Settings.GridSize, i * Settings.GridSize);
}
for (let i = 0; i < 100; ++i) {
line(i * Settings.GridSize, 0, i * Settings.GridSize, 100 * Settings.GridSize);
}
pop();
}
}
// window.preload = GameManager.Setup;
window.setup = GameManager.Setup;
window.draw = GameManager.Draw;
window.keyPressed = GameManager.KeyPressed;
/**
* Global function to check if a key (by keycode) was pressed in the previous frame.
* @param {number} key - keyCode to check if the key went down in the previous frame
* @returns true if the key went down in the previous frame.
*/
p5.prototype.keyWentDown = function (key) {
return GameManager.keysPressed.includes(key);
};
/**
* Copies a source image as a pattern to a target image.
* @param {p5.Image} src The source image on which the fill image is based.
* @param {number} dw Width of the target image
* @param {number} dh Height of the target image
* @param {number} sw Width of the source image
* @param {number} sh Height of the source image
* @returns An image that contains the source image as a pattern
*/
p5.prototype.createFillImage = function (src, dw, dh, sw, sh) {
let img = createImage(dw, dh);
src.resize(sw, sh);
for (let i = 0; i < dw; i += src.width) {
for (let j = 0; j < dh; j += src.height) {
img.copy(src, 0, 0, src.width, src.height, i, j, src.width, src.height);
}
}
return img;
}
/** Class representing an object in your game. */
class GameObject {
#width = 100;
#height = 100;
#position;
#previousPosition;
#newPosition;
#deltaPosition;
#index = 0;
#depth = 0;
#life = -1;
#visible = true;
#debug = false;
#removed = false;
#velocity;
#maxSpeed = -1;
#immovable = false;
#collider = undefined;
#collisionLayer = undefined;
/**
* Create a GameObject
* @param {number} x - X position
* @param {number} y - Y position
* @param {number} width - The width of the GameObject
* @param {number} height - The height of the GameObject
*/
constructor(x, y, width, height) {
this.#position = createVector(x, y);
this.#previousPosition = createVector(x, y);
this.#newPosition = createVector(x, y);
this.#deltaPosition = createVector(0, 0);
this.Width = width;
this.Height = height;
this.#width = width;
this.#height = height;
this.#velocity = createVector(0, 0);
GameManager.GetInstance().AddGameObject(this);
}
/**
* Property to check if the GameObject was removed from the scene
* @return {boolean} true if the GameObject was removed from the scene
*/
get Removed() {
return this.#removed;
}
/**
* The collider that is currently used on this GameObject
* @return {Collider} The collider for this GameObject
*/
get Collider() {
return this.#collider;
}
/**
* The location where the collider for this GameObject was hit. (left, right, top, bottom)
* @return {Collider} The side on which the current collider was hit.
*/
get Hit() {
return this.#collider.HitLocation;
}
/**
* Changes the collision layer for this GameObject. The collision layer determines with which other GameObject this GameObject can interact.
* collision layers are set in the Settings.js
* @param {string} value - The side on which the current collider was hit.
*/
set CollisionLayer(value) {
GameManager.GetInstance().AddGameObjectToCollisionLayer(this, value);
this.#collisionLayer = value;
}
/**
* Determines how many frames this GameObject will remain alive (before being removed automatically).
* @param {number} value - How many frames this GameObject will remain alive. -1 will keep this GameObject alive indefinite (default).
*/
set Life(value) {
this.#life = value;
}
/**
* An immovable object cannot be displaced by another GameObject.
* @return {Boolean} Returns true if this GameObject is immovable.
*/
get Immovable() {
return this.#immovable;
}
/**
* An immovable object cannot be displaced by another GameObject.
* @param {Boolean} value - set it to true to make this GameObject immovable. False allows it to be displaced on collision with other GameObjects (default).
*/
set Immovable(value) {
this.#immovable = value;
}
/**
* When debug is set to true, debug values are shown on screen. (e.g. render-depth, collisionbox, ...)
* @param {Boolean} value - True to show debug values on screen.
*/
set Debug(value) {
this.#debug = value;
}
/**
* The depth value determines the rendering order of each GameObject. GameObjects with identical depth values have no predetermined rendering order.
* @param {number} value - integer value that represents the rendering order for this GameObject. A higher depth value renders a GameObject on top.
*/
set Depth(value) {
this.#depth = value;
}
/**
* The depth value determines the rendering order of each GameObject. GameObjects with identical depth values have no predetermined rendering order.
* @returns {number} integer value that represents the rendering order for this GameObject. A higher depth value renders a GameObject on top.
*/
get Depth() {
return this.#depth;
}
/**
* The index shows in what order a GameObject was added to the game. The first GameObject that was added gets index 0, the next 1, ...
* @param {number} value - integer value that represents the order in which this GameObject was added to the GameObject.
*/
set Index(value) {
this.#index = value;
}
/**
* The width of the GameObject on screen in pixels.
* @returns {number} The width of this GameObject in pixels.
*/
get Width() {
return this.#width;
}
/**
* The width of the GameObject on screen in pixels.
* @param {number} value - The width of this GameObject in pixels.
*/
set Width(value) {
this.#width = value;
}
/**
* The height of the GameObject on screen in pixels.
* @returns {number} The height of this GameObject in pixels.
*/
get Height() {
return this.#height;
}
/**
* The height of the GameObject on screen in pixels.
* @param {number} value - The height of this GameObject in pixels.
*/
set Height(value) {
this.#height = value;
}
/**
* A p5.Vector (x, y) representing the velocity of the GameObject on the X and Y axis.
* @returns {p5.Vector} returns the current velocity of this GameObject.
*/
get Velocity() {
return this.#velocity;
}
/**
* A p5.Vector (x, y) representing the velocity of the GameObject on the X and Y axis.
* @param {p5.Vector} value - the new current velocity of this GameObject.
*/
set Velocity(value) {
this.#velocity.x = value.x;
this.#velocity.y = value.y;
}
/**
* The speed of this GameObject, the same as the magnitude of the Velocity Vector.
* @returns {number} The speed of the current GameObject.
*/
get Speed() {
return this.#velocity.mag();
}
/**
* A p5.Vector (x, y) that represents the position of the current GameObject on the X and Y axis in pixels.
* @param {p5.Vector} value - The new position for the GameObject to be rendered at.
*/
set Position(value) {
this.#position.x = value.x;
this.#position.y = value.y;
}
/**
* A p5.Vector (x, y) that represents the position of the current GameObject on the X and Y axis in pixels.
* @returns {p5.Vector} The position of the current GameObject.
*/
get Position() {
return this.#position;
}
/**
* The angle of direction of the velocity vector in radians.
* @returns {number} The angle of direction of the velocity vector in radians.
*/
get Direction() {
let direction = atan2(this.#velocity.y, this.#velocity.x);
if (isNaN(direction))
direction = 0;
return direction;
}
/**
* Moves the GameObject according to a given displacement.
* @param {p5.Vector} displacement - The displacement vector that determines where and how much the current GameObject should be moved.
*/
Displace(displacement) {
this.#position.add(displacement);
this.#previousPosition = createVector(this.#position.x, this.#position.y);
this.#newPosition = createVector(this.#position.x, this.#position.y);
}
/**
* Sets a maximum size for the velocity vector. The GameObject cannot move faster than this speed.
* @param {number} max - The maximum speed at which the GameObject can move when using the Velocity.
*/
LimitSpeed(max) {
//update linear speed
let speed = this.Speed;
if (abs(speed) > max) {
//find reduction factor
let k = max / abs(speed);
this.#velocity.x *= k;
this.#velocity.y *= k;
}
}
/**
* Sets the speed and direction for the current GameObject.
* @param {number} speed - The speed at which the GameObject should start moving.
* @param {number} angle - The angle of direction where the GameObject should move to.
*/
SetSpeed(speed, angle) {
let a = radians(angle);
this.#velocity.x = cos(a) * speed;
this.#velocity.y = sin(a) * speed;
}
/**
* Adds some speed in a certain direction.
* @param {number} speed The speed that should be added to the current speed (determined by the Velocity).
* @param {number} angle The angle of direction where the GameObject should move to.
*/
AddSpeed(speed, angle) {
let a = radians(angle);
this.#velocity.x += cos(a) * speed;
this.#velocity.y += sin(a) * speed;
};
/**
* Creates a default collider based on the GameObject's width and height, and assigns it to the current GameObject.
*/
SetDefaultCollider() {
this.#collider = new Collider(this, this.#position, createVector(this.Width, this.Height));
}
/**
* Removes the currently set collider from the GameObject. If no collider was set before, this does nothing.
*/
RemoveCollider() {
this.#collider = undefined;
}
/**
* Check if the current GameObject overlaps with any of a list of GameObjects. When it overlaps, OnOverlap gets called.
* @param {...any} otherSprites - List of GameObjects. The functions checks which of these GameObjects overlaps with the current GameObject (ignoring itself).
* @returns true if an overlap was found, false if not
*/
Overlap(...otherSprites) {
let spritesHit = this.#collider.Hit(true, ...otherSprites.map(sprite => sprite.#collider)); // the collider hit function needs an array of colliders, not sprites
if (spritesHit.length > 0)
this.OnOverlap(spritesHit);
return spritesHit.length > 0;
}
/**
* Function that gets called when an overlap with this GameObject is detected. Override this function to check for overlaps.
* @param {GameObject[]} spritesHit - Array of GameObjects that overlap with this GameObject
*/
OnOverlap(spritesHit) { }
/**
* Check if the current GameObject collides with any of a list of GameObjects. When it collides, OnCollide gets called.
* @param {...any} otherSprites - List of GameObjects. The functions checks which of these GameObjects collides with the current GameObject (ignoring itself).
* @returns true if a collision was found, false if not
*/
Collide(...otherSprites) {
let spritesHit = this.#collider.Hit(false, ...otherSprites.map(sprite => sprite.#collider)); // the collider hit function needs an array of colliders, not sprites
if (spritesHit.length > 0)
this.OnCollide(spritesHit);
return spritesHit.length > 0;
}
/**
* Function that gets called when a collision with this GameObject is detected. Override this function to check for collisions.
* @param {GameObject[]} spritesHit - Array of GameObjects that overlap with this GameObject
*/
OnCollide(spritesHit) { }
/**
* Removes the current GameObject from the scene. The GameObject may not be removed from memory instantly, but the Removed property is set to true.
*/
Remove() {
this.RemoveCollider();
this.#removed = true;
GameManager.GetInstance().RemoveGameObject(this);
delete this;
}
/**
* Update gets called every frame. Override this function to implement your own visualisations and/or frame-actions (input, movement, ...).
*/
Update() {
noStroke();
fill("magenta");
rect(0, 0, this.#width, this.#height);
}
/**
* Display gets called every frame, and prepares the GameObject to be drawn correctly. Do not call this directly, or override this!
*/
Display() {
if (this.#visible && !this.#removed) {
if (this.Collider) {
this.Collider.UpdateCollider();
}
//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);
if (this.#maxSpeed !== -1)
this.LimitSpeed(this.#maxSpeed);
this.#position.x += this.#velocity.x;
this.#position.y += this.#velocity.y;
this.#newPosition = createVector(this.#position.x, this.#position.y);
this.#deltaPosition.x = this.#position.x - this.#previousPosition.x;
this.#deltaPosition.y = this.#position.y - this.#previousPosition.y;
//self destruction countdown
if (this.#life > 0)
this.#life--;
if (this.#life === 0)
this.Remove();
push();
colorMode(RGB);
noStroke();
rectMode(CENTER);
ellipseMode(CENTER);
imageMode(CENTER);
translate(this.#position.x, this.#position.y);
this.Update();
pop();
//draw debug info
if (Settings.Debug || this.#debug) {
this.#DrawDebugInfo();
}
}
}
/**
* Draws debug information for this GameObject.
*/
#DrawDebugInfo() {
let ctx = document.querySelector("canvas").getContext("2d");
ctx.save();
let pos = this.Collider ? this.Collider.Center : this.Position;
let w = this.Collider ? this.Collider.Size.x / 2 : 0;
let h = this.Collider ? this.Collider.Size.y / 2 : 0;
ctx.lineWidth = 2;
ctx.strokeStyle = "#00FF00";
ctx.strokeRect(pos.x - w / 2, pos.y - h / 2, w, h);
ctx.beginPath();
ctx.moveTo(pos.x - 10, pos.y);
ctx.lineTo(pos.x + 10, pos.y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(pos.x, pos.y - 10);
ctx.lineTo(pos.x, pos.y + 10);
ctx.stroke();
ctx.fillStyle = "#00FF00";
ctx.font = '16px sans-serif';
ctx.fillText(this.#index + '', pos.x + 4, pos.y - 2);
ctx.fillText(this.#depth + '', pos.x + 4, pos.y + 15);
ctx.restore();
}
}
Source