Show:
/**
 * p5.play v3
 * Upgraded and maintained by Quinton Ashley @quinton-ashley, 2022
 * https://quintos.org
 *
 * p5.play was founded by Paolo Pedercini @molleindustria, 2015
 * https://molleindustria.org/
 */
p5.prototype.registerMethod('init', function p5PlayInit() {
	const log = console.log; // shortcut

	// store a reference to the p5 instance that p5play is being added to
	let pInst = this;
	this.angleMode(p5.prototype.DEGREES);

	const pl = planck;
	pl.Settings.velocityThreshold = 0.19;

	let world;
	let plScale = 60;
	this._p5play = {};
	this._p5play.autoDrawSprites = true;
	this._p5play.autoUpdateSprites = true;

	const scaleTo = ({ x, y }) => new pl.Vec2((x * world.tileSize) / plScale, (y * world.tileSize) / plScale);
	const scaleFrom = ({ x, y }) => new pl.Vec2((x / world.tileSize) * plScale, (y / world.tileSize) * plScale);
	const fixRound = (val) => (Math.abs(val - Math.round(val)) <= pl.Settings.linearSlop ? Math.round(val) : val);

	let spriteCount = 0;
	let groupCount = 0;

	let contacts = [];

	/**
	 * What is a sprite? Sprites are ghosts!
	 *
	 * Video game developers use the word sprite to refer to
	 * characters, items, enemies, or any other objects that
	 * move above a background.
	 *
	 * In p5.play a sprite can be anything. Sprites have
	 * properties such as: position, width, height, and speed.
	 * They can be displayed using a simple shape, image, or animation.
	 *
	 * By default sprites have a dynamic physics collider.
	 * Colliders are used to resolve overlaps or collisions
	 * with other sprites.
	 *
	 * Every sprite you create is added to the allSprites
	 * group and put on the top layer, in front of all other
	 * previously created sprites.
	 *
	 * Look at all the examples to learn how to create many different
	 * kinds of sprites.
	 *
	 * @example
	 *
	 *   let rectangle = new Sprite(x, y, width, height);
	 *
	 *   let circle = new Sprite(x, y, diameter);
	 *
	 *   let line = new Sprite(x, y, [length, angle]);
	 *
	 *   let chain = new Sprite(x, y, [length0, angle0, length1, angle1]);
	 *
	 * @class Sprite
	 * @constructor
	 * @param {String|SpriteAnimation|p5.Image} [aniName|ani|image]
	 * @param {Number} x Horizontal position of the sprite
	 * @param {Number} y Vertical position of the sprite
	 * @param {Number} [width|diameter] Width of the placeholder rectangle and of
	 * the collider until an image or new collider are set. *OR* If height is not
	 * set then this parameter becomes the diameter of the placeholder circle.
	 * @param {Number} [height] Height of the placeholder rectangle and of the collider
	 * until an image or new collider are set
	 * @param {String} [physics] collider type is 'dynamic' by default, can be
	 * 'static', 'kinematic', or 'none'
	 */
	class Sprite {
		constructor(x, y, w, h, collider) {
			this.p = pInst;
			let args = [...arguments];

			if (!world) world = new World();

			/**
			 * Groups the sprite belongs to, including allSprites
			 *
			 * @property groups
			 * @type {Array}
			 */
			this.groups = [];
			this.p.allSprites.add(this);

			/**
			 * Keys are the animation label, values are SpriteAnimation objects.
			 *
			 * @property animations
			 * @type {Object}
			 */
			this.animations = {};

			/**
			 * Reference to the sprite's current animation.
			 *
			 * @property animation
			 * @type {SpriteAnimation}
			 */
			this.animation = undefined;

			/**
			 * If false, animations that are stopped before they are completed,
			 * typically by a call to sprite.changeAnimation, will start at the frame
			 * they were stopped at. If true, animations will always start playing from
			 * frame 0 unless specified by the user in a separate anim.changeFrame
			 * call.
			 *
			 * @property autoResetAnimations
			 * @type {SpriteAnimation}
			 * @default false
			 */
			this.autoResetAnimations = false;

			/**
			 * True if the sprite was removed from the world
			 *
			 * @property removed
			 * @type {Boolean}
			 * @default false
			 */
			this.removed = false;

			/**
			 * The kind of shape: 'box', 'circle', 'polygon', or 'chain'
			 *
			 * @property shape
			 * @type {String}
			 * @default box
			 */
			this.shape;

			let ani, group;

			if (args[0] !== undefined && args[0] instanceof Group) {
				group = args[0];
				args = args.slice(1);
				this.addToGroup(group);
				for (let _ani in group.animations) {
					ani = _ani;
					break;
				}
			}
			if (args[0] !== undefined && typeof args[0] != 'number') {
				// ani instanceof p5.Image
				// shift
				ani = args[0];
				args = args.slice(1);
			}

			if (arguments.length != args.length) {
				x = args[0];
				y = args[1];
				w = args[2];
				h = args[3];
				collider = args[4];
			}

			if (Array.isArray(w) || (!isNaN(w) && typeof h == 'string')) {
				collider = h;
				h = undefined;
			} else if (isNaN(w)) {
				collider = w;
				w = undefined;
			}

			if (group) {
				collider ??= group.collider;
				this.shape = group.shape;
			}

			/**
			 * Cycles before self removal.
			 * Set it to initiate a countdown, every draw cycle the property is
			 * reduced by 1 unit. If less than or equal to 0, this sprite will be removed.
			 *
			 * @property life
			 * @type {Number}
			 * @default 10000000
			 */
			this.life = 10000000;

			/**
			 * The sprite's visibility.
			 *
			 * @property visible
			 * @type {Boolean}
			 * @default true
			 */
			this.visible = true;

			/**
			 * If no image or animations are set this is color of the
			 * placeholder rectangle
			 *
			 * @property shapeColor
			 * @type {color}
			 * @default a randomly generated color
			 */
			this.shapeColor = this.p.color(this.p.random(255), this.p.random(255), this.p.random(255));

			/**
			 * Contains all the collision callback functions for this sprite
			 * when it comes in contact with other sprites or groups.
			 */
			this.collides = {};

			/**
			 * Contains all the overlap callback functions for this sprite
			 * when it comes in contact with other sprites or groups.
			 */
			this.overlaps = {};

			let _this = this;

			/**
			 * Set position using .x and .y instead.
			 *
			 * @deprecated
			 */
			this.position = {
				get x() {
					return _this.x;
				},
				set x(val) {
					_this.x = val;
				},
				get y() {
					return _this.y;
				},
				set y(val) {
					_this.y = val;
				}
			};

			this._pos = {
				x: 0,
				y: 0
			};

			this._vel = {
				x: 0,
				y: 0
			};

			/**
			 * A vector representing how fast the sprite is moving horizontally
			 * and vertically.
			 *
			 * @property vel
			 */
			this.vel = {
				get x() {
					if (!_this.body) return _this._vel.x;
					return _this.body.getLinearVelocity().x;
				},
				set x(val) {
					if (_this.body) {
						_this.body.setLinearVelocity(new pl.Vec2(val, _this.body.getLinearVelocity().y));
					} else {
						_this._vel.x = val;
					}
				},
				get y() {
					if (!_this.body) return _this._vel.y;
					return _this.body.getLinearVelocity().y;
				},
				set y(val) {
					if (_this.body) {
						_this.body.setLinearVelocity(new pl.Vec2(_this.body.getLinearVelocity().x, val));
					} else {
						_this._vel.y = val;
					}
				}
			};

			/**
			 * Verbose/legacy version of sprite.vel
			 *
			 * @property velocity
			 */
			this.velocity = {
				get x() {
					return _this.vel.x;
				},
				set x(val) {
					_this.vel.x = val;
				},
				get y() {
					return _this.vel.y;
				},
				set y(val) {
					_this.vel.y = val;
				}
			};

			/**
			 * Object containing information about the most recent collision/overlapping
			 * To be typically used in combination with Sprite.overlap or Sprite.collide
			 * functions.
			 * The properties are touching.left, touching.right, touching.top,
			 * touching.bottom and are either true or false depending on the side of the
			 * collider.
			 *
			 * @property touching
			 * @type {Object}
			 */
			this.touching = {};
			this.touching.left = null;
			this.touching.right = null;
			this.touching.top = null;
			this.touching.bottom = null;

			if (ani) {
				if (ani instanceof p5.Image) {
					w ??= ani.width;
					if (this.shape != 'circle') h ??= ani.height;
					this.addImage(ani);
				} else {
					if (typeof ani == 'string') this.ani(ani);
					else this.animation = ani;
					w ??= this.animation.width;
					if (this.shape != 'circle') h ??= this.animation.height;
				}
			}

			if (!group) {
				this.layer = this.p.allSprites.maxDepth() + 1;
			} else {
				this.layer = group.layer || 0;
				collider ??= group.collider;
			}

			if (!collider || typeof collider != 'string') {
				collider = 'dynamic';
			}

			x ??= 0;
			y ??= 0;
			if (collider != 'none' && collider != 'n') {
				this.body = world.createBody({
					position: scaleTo({ x, y }),
					type: collider
				});
				this.body.sprite = this;

				this.addCollider(0, 0, w, h);
			} else {
				this.x = x;
				this.y = y;
				this.w = w;
				this.h = h;
			}

			this.previousPosition = { x, y };
			this.dest = { x, y };
			this.vel.x = 0;
			this.vel.y = 0;
			this.scale = 1;
			this._mirrorX = 1;
			this._mirrorY = 1;
			this.debug = false;
			this._shift = {};
			this.idNum = spriteCount;
			spriteCount++;
		}

		/**
		 * Similar to createSprite and the Sprite constructor except
		 * offset is the distance the collider is from the center of the
		 * sprite.
		 *
		 * @param {Number} offsetX distance from the center of the sprite
		 * @param {Number} offsetY distance from the center of the sprite
		 */
		addCollider(offsetX, offsetY, w, h) {
			let path, shape;

			offsetX ??= 0;
			offsetY ??= 0;
			w ??= this.w;
			h ??= this.h;

			if (Array.isArray(w)) {
				path = w;
			} else {
				if (w !== undefined && h === undefined) shape ??= 'circle';
				shape ??= 'box';
			}

			if (shape == 'box' || shape == 'circle') {
				w ??= world.tileSize > 1 ? 1 : 100;
				this.width = w;
				h ??= w;
				this.height = h;
				if (shape == 'circle') this.diameter = w;
			}

			let props = {};

			let dimensions;

			// the actual dimensions of the collider for a box or circle are a
			// little bit smaller so that they can slid past each other
			// when in a tile grid
			if (shape == 'box' || shape == 'circle') {
				dimensions = scaleTo({ x: w - 0.08, y: h - 0.08 });
			}

			let s;
			if (shape == 'box') {
				s = pl.Box(dimensions.x / 2, dimensions.y / 2, scaleTo(offsetX, offsetY), 0);
			} else if (shape == 'circle') {
				s = pl.Circle(dimensions.x / 2);
				s.m_p.x = 0;
				s.m_p.y = 0;
			} else if (path) {
				let vecs = [{ x: 0, y: 0 }];
				let vert = { x: 0, y: 0 };
				let min = { x: 0, y: 0 };
				let max = { x: 0, y: 0 };

				let rep = 1;
				if (path.length % 2) rep = path[path.length - 1];
				let mod = rep > 0 ? 1 : -1;
				rep = Math.abs(rep);
				let ang = 0;
				for (let i = 0; i < rep; i++) {
					for (let j = 0; j < path.length - 1; j += 2) {
						let len = path[j];
						ang += path[j + 1];
						vert.x += len * this.p.cos(ang);
						vert.y += len * this.p.sin(ang);
						vecs.push({ x: vert.x, y: vert.y });
						if (vert.x < min.x) min.x = vert.x;
						if (vert.y < min.y) min.y = vert.y;
						if (vert.x > max.x) max.x = vert.x;
						if (vert.y > max.y) max.y = vert.y;
					}
					ang *= mod;
				}
				if (
					Math.round(vert.x * 1e6) / 1e6 == 0 &&
					Math.round(vert.y * 1e6) / 1e6 == 0 &&
					vecs.length - 1 <= pl.Settings.maxPolygonVertices
				) {
					shape = 'polygon';
				} else {
					shape = 'chain';
				}
				this.w = max.x - min.x;
				this.h = max.y - min.y;
				for (let i = 0; i < vecs.length; i++) {
					let vec = vecs[i];
					vecs[i] = new pl.Vec2(
						((vec.x - this._hw - min.x) * world.tileSize) / plScale,
						((vec.y - this._hh - min.y) * world.tileSize) / plScale
					);
				}
				if (shape == 'polygon') {
					if (this._isConvexPoly(vecs.slice(0, -1))) {
						s = pl.Polygon(vecs);
					} else shape = 'chain';
				}
				if (shape == 'chain') {
					s = pl.Chain(vecs, false);
					props.density = 0;
					props.restitution = 0;
				}
			}
			props.shape = s;
			props.density ??= this.density || 5;
			props.friction ??= this.friction || 0.5;
			props.restitution ??= this.bounciness || 0.2;
			this.body.createFixture(props); /*RPC051521*/ /*RPC052521*/
			if (!this.shape) {
				this.shape = shape;
			} else {
				this.shape = 'combo';
			}
		}

		/**
		 * In p5.play v3 there is no need for users to use this method.
		 *
		 * @deprecated
		 */
		setDefaultCollider() {
			this.addCollider();
		}

		/**
		 * Use sprite.addCollider instead.
		 *
		 * @deprecated
		 */
		setCollider(shape, x, y, w, h) {
			this.addCollider(x, y, w, h);
		}

		/**
		 * Removes the physics body collider from the sprite.
		 *
		 * Avoid using this method. It is more efficient to set the physics
		 * body type of the sprite to 'none'.
		 *
		 * @method removeCollider
		 */
		removeCollider() {
			world.destroyBody(this.body);
		}

		/**
		 * The Angle Aligned Bounding Box of the sprite's physics body.
		 *
		 * @private
		 * @type {Object}
		 */
		get aabb() {
			return getAABB(this);
		}

		// set advance(val) {
		// 	this.body.advance(val);
		// }
		// set angularImpulse(val) {
		// 	this.body.applyAngularImpulse(val, true);
		// }

		/**
		 * The bounciness of the sprite's physics body.
		 *
		 * @property bounciness
		 * @type {Number}
		 */
		get bounciness() {
			if (!this.fixture) return;
			return this.fixture.getRestitution();
		}
		set bounciness(val) {
			for (let fxt = this.fixtureList; fxt; fxt = fxt.getNext()) {
				fxt.setRestitution(val);
			}
		}

		/**
		 * The center of mass of the sprite's physics body.
		 *
		 * @property centerOfMass
		 * @type {Number}
		 */
		get centerOfMass() {
			return scaleFrom(this.body.getWorldCenter());
		}
		/**
		 * Use sprite.animation.name instead.
		 *
		 * @deprecated
		 * @type {String}
		 */
		get currentAnimation() {
			return this.animation.name;
		}

		/**
		 * The density of the sprite's physics body.
		 *
		 * @property density
		 * @type {Number}
		 */
		get density() {
			if (!this.fixture) return;
			return this.fixture.getDensity();
		}
		set density(val) {
			for (let fxt = this.fixtureList; fxt; fxt = fxt.getNext()) {
				fxt.setDensity(val);
			}
		}

		/**
		 * Use .layer instead.
		 *
		 * @deprecated
		 * @property depth
		 */
		get depth() {
			return this.layer;
		}
		set depth(val) {
			this.layer = val;
		}

		/**
		 * The angle of the sprite's movement or it's rotation angle if the
		 * sprite is not moving.
		 *
		 * @property drag
		 * @type {Number}
		 */
		get direction() {
			if (this._direction) return this._direction;
			if (this.vel.x !== 0 || this.vel.y !== 0) {
				return this.p.atan2(this.vel.y, this.vel.x);
			}
			return this.rotation;
		}
		set direction(val) {
			this._direction = val;
		}

		/**
		 * The amount of resistance a sprite has to being moved.
		 *
		 * @property drag
		 * @type {Number}
		 */
		get drag() {
			return this.body.getLinearDamping();
		}
		set drag(val) {
			this.body.setLinearDamping(val);
		}

		/**
		 * True if the sprite's physics body is dynamic.
		 *
		 * @property dynamic
		 * @type {Boolean}
		 */
		get dynamic() {
			return this.body.isDynamic();
		}
		set dynamic(val) {
			if (val) this.body.setDynamic();
		}

		/**
		 * If true the sprite can not rotate.
		 *
		 * @property rotationLocked
		 * @type {Boolean}
		 */
		get rotationLocked() {
			return this.body.isFixedRotation();
		}
		set rotationLocked(val) {
			this.body.setFixedRotation(val);
		}

		get fixture() {
			return this.fixtureList;
		}
		get fixtureList() {
			return this.body.getFixtureList();
		}

		// set force(val) {
		// 	this.body.applyForceToCenter(val, true);
		// }

		/**
		 * The amount the sprite's physics body resists moving
		 * when rubbing against another physics body.
		 *
		 * @property friction
		 * @type {Number}
		 */
		get friction() {
			if (!this.fixture) return;
			return this.fixture.getFriction();
		}
		set friction(val) {
			for (let fxt = this.fixtureList; fxt; fxt = fxt.getNext()) {
				fxt.setFriction(val);
			}
		}

		/**
		 * Use .static instead.
		 *
		 * @deprecated
		 * @property immovable
		 */
		get immovable() {
			return this.body.isStatic();
		}
		set immovable(val) {
			if (val) this.body.setStatic();
		}
		// set impulse(val) {
		// 	this.body.applyLinearImpulse(val, this.body.getWorldCenter(), true);
		// }
		// get inertia() {
		// 	return this.body.getInertia();
		// }

		/**
		 * Set this to true if the sprite goes really fast to prevent
		 * inaccurate physics simulation.
		 *
		 * @property isSuperFast
		 * @type {Boolean}
		 */
		get isSuperFast() {
			return this.body.isBullet();
		}
		set isSuperFast(val) {
			this.body.setBullet(val);
		}

		// get joint() {
		// 	return this.body.getJointList().joint;
		// }
		// get jointList() {
		// 	return this.body.getJointList();
		// }

		/**
		 * True if the sprite's physics body is kinematic.
		 *
		 * @property kinematic
		 * @type {Boolean}
		 */
		get kinematic() {
			return this.body.isKinematic();
		}
		set kinematic(val) {
			if (val) this.body.setKinematic();
		}
		/**
		 * The mass of the sprite's physics body.
		 *
		 * @property mass
		 * @type {Number}
		 */
		get mass() {
			return this.body.getMass();
		}
		set mass(val) {
			let t = this.massData;
			t.mass = val;
			this.body.setMassData(t);
		}
		/**
		 * @private
		 */
		get massData() {
			const t = { I: 0, center: new pl.Vec2(0, 0), mass: 0 };
			this.body.getMassData(t);
			t.center = scaleFrom(t.center);
			return t;
		}
		// set massData(val) {
		// 	val.center = scaleTo(val.center);
		// 	this.body.setMassData(val);
		// }

		/**
		 * Setting mirrorX to true will mirror the sprite horizontally.
		 *
		 * @property mirrorX
		 * @type {Boolean}
		 */
		get mirrorX() {
			return this._mirrorX;
		}
		set mirrorX(val) {
			this._mirrorX = val ? -1 : 1;
		}
		/**
		 * Setting mirrorY to true will mirror the sprite vertically.
		 *
		 * @property mirrorY
		 * @type {Boolean}
		 */
		get mirrorY() {
			return this._mirrorY;
		}
		set mirrorY(val) {
			this._mirrorY = val ? -1 : 1;
		}

		// get next() {
		// 	return this.body.getNext();
		// }

		/**
		 * The angle of the sprite's rotation, not the direction it is moving.
		 *
		 * @property rotation
		 * @type {Number}
		 */
		get rotation() {
			if (!this.body) return this._angle || 0;
			if (this.p._angleMode === p5.prototype.DEGREES) {
				return p5.prototype.degrees(this.body.getAngle());
			}
			return this.body.getAngle();
		}
		set rotation(val) {
			if (this.body) {
				if (this.p._angleMode === p5.prototype.DEGREES) {
					this.body.setAngle(p5.prototype.radians(val));
				} else {
					this.body.setAngle(val);
				}
			} else {
				this._angle = val;
			}
		}
		/**
		 * The amount of the sprite resists rotating.
		 *
		 * @property rotationDrag
		 * @type {Number}
		 */
		get rotationDrag() {
			return this.body.getAngularDamping();
		}
		set rotationDrag(val) {
			this.body.setAngularDamping(val);
		}
		/**
		 * The speed of the sprite's rotation.
		 *
		 * @property rotationSpeed
		 * @type {Number}
		 */
		get rotationSpeed() {
			if (this.body) return this.body.getAngularVelocity();
			return this._rotationSpeed || 0;
		}
		set rotationSpeed(val) {
			if (this.body) this.body.setAngularVelocity(val);
			else this._rotationSpeed = val;
		}
		// get physicsBody() {
		// 	return this._physicsBody;
		// }
		// set physicsBody(val) {
		// 	if (val == 'none') {
		// 		this.removeCollider();
		// 	} else if (val == 'static') {
		// 		this.static = true;
		// 	} else if (val == 'kinematic') {
		// 		this.kinematic = true;
		// 	} else if (val == 'dynamic') {
		// 		this.dynamic = true;
		// 	}
		// }
		// get sensor() {
		// 	return this.body.m_fixtureList.isSensor();
		// }
		// set sensor(val) {
		// 	for (let fxt = this.fixtureList; fxt; fxt = fxt.getNext()) {
		// 		fxt.setSensor(val);
		// 	}
		// }
		/**
		 * The sprite's speed.
		 *
		 * @property speed
		 * @type {Number}
		 */
		get speed() {
			return this.p.createVector(this.vel.x, this.vel.y).mag();
		}

		/**
		 * @deprecated
		 */
		getSpeed() {
			return this.speed;
		}
		/**
		 * The sprite's speed.
		 *
		 * @property speed
		 * @type {Number}
		 * @param {Number} speed that the sprite will move at in the direction of its current rotation
		 */
		set speed(val) {
			let angle = this._direction;
			angle ??= this.direction;

			this.vel.x = this.p.cos(angle) * val;
			this.vel.y = this.p.sin(angle) * val;
			this._direction = null;
		}

		/**
		 * Is the sprite's physics collider static?
		 *
		 * @property static
		 * @type {Boolean}
		 */
		get static() {
			return this.body.isStatic();
		}
		set static(val) {
			if (val) this.body.setStatic();
		}
		// set torque(val) {
		// 	this.body.applyTorque(val, true);
		// }
		// get transform() {
		// 	const t = this.body.getTransform();
		// 	return { position: scaleFrom(t.p), angle: asin(t.q.s) };
		// }
		// set transform({ position, angle }) {
		// 	this.body.setTransform(scaleTo(position), angle);
		// }
		// get world() {
		// 	return this.body.getWorld();
		// }

		/**
		 * The horizontal position of the sprite.
		 * @property x
		 * @type {Number}
		 */
		get x() {
			if (!this.body) return this._pos.x;
			let x = (this.body.getPosition().x / world.tileSize) * plScale;
			return fixRound(x);
		}
		set x(val) {
			if (this.body) {
				let pos = new pl.Vec2((val * world.tileSize) / plScale, this.body.getPosition().y);
				this.body.setPosition(pos);
			} else {
				this._pos.x = val;
			}
		}
		/**
		 * The vertical position of the sprite.
		 * @property y
		 * @type {Number}
		 */
		get y() {
			if (!this.body) return this._pos.y;
			let y = (this.body.getPosition().y / world.tileSize) * plScale;
			return fixRound(y);
		}
		set y(val) {
			if (this.body) {
				let pos = new pl.Vec2(this.body.getPosition().x, (val * world.tileSize) / plScale);
				this.body.setPosition(pos);
			} else {
				this._pos.y = val;
			}
		}
		/**
		 * Set the position vector {x, y}
		 *
		 * @property pos
		 * @type {Object}
		 */
		set pos(val) {
			let pos = new pl.Vec2((val.x * world.tileSize) / plScale, (val.y * world.tileSize) / plScale);
			_this.body.setPosition(pos);
		}
		/**
		 * The width of the sprite.
		 * @property w
		 * @type {Number}
		 */
		get w() {
			return this._w;
		}
		set w(val) {
			this._w = val;
			this._hw = val * 0.5;
		}
		/**
		 * The width of the sprite.
		 * @property width
		 * @type {Number}
		 */
		get width() {
			return this.w;
		}
		set width(val) {
			this.w = val;
		}
		/**
		 * The height of the sprite.
		 * @property h
		 * @type {Number}
		 */
		get h() {
			return this._h;
		}
		set h(val) {
			this._h = val;
			this._hh = val * 0.5;
		}
		/**
		 * The height of the sprite.
		 * @property height
		 * @type {Number}
		 */
		get height() {
			return this.h;
		}
		set height(val) {
			this.h = val;
		}
		/**
		 * The diameter of a circular sprite.
		 * @property d
		 * @type {Number}
		 */
		get d() {
			this._diameter ??= this.w;
			return this._diameter;
		}
		set d(val) {
			this._diameter = val;
			this.w = val;
			this.h = val;
		}
		/**
		 * The diameter of a circular sprite.
		 * @property diameter
		 * @type {Number}
		 */
		get diameter() {
			return this.d;
		}
		set diameter(val) {
			this.d = val;
		}

		/**
		 * Validate convexity. This is a very time consuming operation.
		 *
		 * @private
		 * @param vecs {Array} an array of planck.Vec2 vertices
		 * @returns true if valid
		 */
		_isConvexPoly(vecs) {
			for (let i = 0; i < vecs.length; ++i) {
				const i1 = i;
				const i2 = i < vecs.length - 1 ? i1 + 1 : 0;
				const p = vecs[i1];
				const e = pl.Vec2.sub(vecs[i2], p);

				for (let j = 0; j < vecs.length; ++j) {
					if (j == i1 || j == i2) {
						continue;
					}

					const v = pl.Vec2.sub(vecs[j], p);
					const c = pl.Vec2.cross(e, v);
					if (c < 0.0) {
						return false;
					}
				}
			}

			return true;
		}

		/**
		 * Updates the sprite. Called automatically at the end of the draw
		 * cycle.
		 *
		 * @private
		 */
		update() {
			// if (this._shift.x || this._shift.y) {
			// 	this._shift.x ??= this.x;
			// 	this._shift.y ??= this.y;
			// 	let pos = new pl.Vec2((this._shift.x * world.tileSize) / plScale, (this._shift.y * world.tileSize) / plScale);
			// 	this.body.move(pos.x, pos.y);
			// 	this._shift = {};
			// }
			if (this.animation) this.animation.update();
			// this._syncAnimationSizes();
			//patch for un-preloaded single image sprites
			// if (this.width == 1 && this.height == 1) {
			// 	this.width = this.animation.getWidth();
			// 	this.height = this.animation.getHeight();
			// }
			if (this._rotationSpeed) this.rotation += this._rotationSpeed;
			if (this._vel.x) this.x += this._vel.x;
			if (this._vel.y) this.y += this._vel.y;
		}

		/**
		 * Displays the Sprite with rotation and scaling applied before
		 * the sprite is drawn.
		 *
		 * @private
		 */
		display() {
			let x = this.p.width * 0.5 - world.origin.x + this.x * world.tileSize;
			let y = this.p.height * 0.5 - world.origin.y + this.y * world.tileSize;

			// skip drawing for out-of-view bodies, but
			// edges can be very long, so they still should be drawn
			if (this.shape != 'chain' && (x < -100 || x > this.p.width + 100 || y < -100 || y > this.p.height + 100)) return;
			x = fixRound(x) - (this.w % 2) * 0.5;
			y = fixRound(y) - (this.h % 2) * 0.5;

			x += world.tileSize * 0.015;
			y += world.tileSize * 0.015;

			this.p.push();

			this.p.translate(x, y);
			if (this.rotation) this.p.rotate(this.rotation);
			this.p.scale(this.scale * this.mirrorX, this.scale * this.mirrorY);

			this.draw();

			this.p.pop();
		}

		/**
		 * Manages the visuals of the sprite. It can be overridden with a
		 * custom drawing function. The center of the sprite is (0, 0).
		 *
		 * @example
		 * sprite.draw = function() {
		 *   // an oval
		 *   ellipse(0,0,20,10);
		 * }
		 *
		 * @method draw
		 */
		draw() {
			if (this.animation && !this.debug) this.animation.draw(0, 0, 0);
			else {
				for (let fxt = this.fixtureList; fxt; fxt = fxt.getNext()) {
					this.drawFixture(fxt);
				}
			}
		}

		/**
		 *
		 * @private
		 */
		drawFixture({ m_shape }) {
			this.p.fill(this.shapeColor);
			const s = m_shape;
			if (s.m_type == 'polygon' || s.m_type == 'chain') {
				if (s.m_type == 'chain') {
					this.p.push();
					this.p.noFill();
				}
				let v = s.m_vertices;
				this.p.beginShape();
				for (let i = 0; i < v.length; i++) {
					this.p.vertex(v[i].x * plScale, v[i].y * plScale);
				}
				if (s.m_type != 'chain') this.p.endShape(p5.prototype.CLOSE);
				else {
					this.p.endShape();
					this.p.pop();
				}
			} else if (s.m_type == 'circle') {
				const d = s.m_radius * plScale * 2;
				this.p.ellipse(s.m_p.x * plScale, s.m_p.y * plScale, d, d);
			} else if (s.m_type == 'edge') {
				this.p.line(s.m_vertex1.x * plScale, s.m_vertex1.y * plScale, s.m_vertex2.x * plScale, s.m_vertex2.y * plScale);
			}
		}

		/**
		 * 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
		 */
		addImage() {
			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 if (typeof arguments[0] == 'string') {
				this.addAnimation('normal', loadImage(arguments[0]));
			} else {
				throw new TypeError('only accepts a p5.image, file path, or an image label string followed by a p5.image)');
			}
		}

		/**
		 * Adds an animation to the sprite. Use this function in the preload p5.js
		 * function. You don't need to name the animation if the sprite will only
		 * use one animation. See SpriteAnimation for more information.
		 *
		 * Uses:
		 * - sprite.addAnimation(label, animation);
		 * - sprite.addAnimation(label, firstFrame, lastFrame);
		 * - sprite.addAnimation(label, frame1, frame2, frame3...);
		 *
		 * @method addAnimation
		 * @param {String} label SpriteAnimation identifier
		 * @param {SpriteAnimation} animation The preloaded animation
		 */
		addAnimation() {
			let args = [...arguments];
			let name, anim;
			if (args[0] instanceof SpriteAnimation) {
				anim = args[0].clone();
				name = anim.name || 'default';
				anim.name = name;
			} else if (args[1] instanceof SpriteAnimation) {
				name = args[0];
				anim = args[1].clone();
				anim.name = name;
			} else {
				anim = new SpriteAnimation(...args);
				name = anim.name;
			}
			anim.isSpriteAnimation = true;
			this.animations[name] = anim;
			if (!this.animation) {
				this.animation = anim;
				this.w ??= anim.width;
				this.h ??= anim.height;
			}
			return anim;
		}

		/**
		 * Changes the displayed image/animation.
		 * Equivalent to changeAnimation
		 *
		 * @method changeImage
		 * @param {String} label Image/SpriteAnimation identifier
		 */
		changeImage(label) {
			this.changeAnimation(label);
		}

		/**
		 * Changes the displayed animation. The animation must be added first
		 * using the sprite.addAnimation method. The animation could also be
		 * added using the group.addAnimation method to a group the sprite
		 * has been added to.
		 *
		 * See SpriteAnimation for more control over the sequence.
		 *
		 * @method changeAnimation
		 * @param {String} label SpriteAnimation identifier
		 */
		_changeAnimation(label) {
			let anim = this.animations[label];
			if (!anim) {
				for (let g of this.groups) {
					anim = g.animations[label];
					if (anim) break;
				}
			}
			if (!anim) {
				this.p.print('changeAnimation error: no animation labeled ' + label);
				return;
			}
			this.animation = anim;
			this.animation.name = label;
			// reset to frame 0 of that animation
			if (this.autoResetAnimations) this.animation.frame = 0;
		}

		changeAnimation() {
			return this.ani(...arguments);
		}

		/**
		 * Set the velocity vector.
		 *
		 * @deprecated
		 * @param {Number} x horizontal velocity
		 * @param {Number} y vertical velocity
		 */
		setVelocity(x, y) {
			this.body.setLinearVelocity(new pl.Vec2(x, y));
		}

		/**
		 * Deprecated: set direction and speed separately
		 *
		 * Set the speed 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
		 * @deprecated
		 * @param {Number} speed Scalar speed
		 * @param {Number} [direction] angle
		 */
		setSpeed(speed, direction) {
			if (direction) this.direction = direction;
			this.speed = speed;
		}

		/**
		 * Add to the speed of the sprite.
		 * 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 addSpeed
		 * @param {Number} speed Scalar speed
		 * @param {Number} [angle] Direction in degrees
		 */
		addSpeed(speed, angle) {
			angle ??= this.direction;

			this.vel.x += this.p.cos(angle) * speed;
			this.vel.y += this.p.sin(angle) * speed;
		}

		getDirection() {
			return this.direction;
		}

		/**
		 * Move a sprite towards a position
		 *
		 * @method moveTowards
		 * @param {Number} destX destination x
		 * @param {Number} destY destination y
		 * @param {Number} tracking 1 represents 1:1 tracking, the mouse moves to the destination immediately, 0 represents no tracking
		 */
		moveTowards(destX, destY, tracking) {
			if (!destX && !destY) return;
			tracking ??= 0.1;
			this.vel.x = (destX - this.x) * tracking * world.tileSize;
			this.vel.y = (destY - this.y) * tracking * world.tileSize;
		}

		/**
		 * Move the sprite to a destination position
		 *
		 * @method move
		 * @param {Number} destX destination x
		 * @param {Number} destY destination y
		 * @param {Number} speed scalar
		 * @param {Function} cb callback, called when movement is complete
		 * @returns {Promise} resolves when the movement is complete
		 */
		move(destX, destY, speed, cb) {
			if (typeof destX == 'undefined') {
				console.error('sprite.move ERROR: movement direction or destination not defined');
				return;
			}
			// if the sprite is moving stop it from moving in the direction it used to be moving in
			// if (this.isMoving) {
			// 	this.velocity.x = 0;
			// 	this.velocity.y = 0;
			// }
			let direction = true;
			// if destY is actually the direction (up, down, left, or right)
			if (typeof destX == 'string') {
				// shift input parameters over by one
				cb = arguments[2];
				speed = arguments[1];
				direction = arguments[0];
				destX = this.dest.x;
				destY = this.dest.y;
				if (direction == 'up') destY--;
				if (direction == 'down') destY++;
				if (direction == 'left') destX--;
				if (direction == 'right') destX++;
				if (/(up|down)/.test(direction)) {
					this.dest.y = destY;
				}
				if (/(left|right)/.test(direction)) {
					this.dest.x = destX;
				}
				this.direction = direction;
			} else {
				this.dest.x = destX;
				this.dest.y = destY;
			}

			if (world.tileSize > 1) speed ??= 0.1;
			speed ??= 1;
			if (speed <= 0) {
				console.warn('sprite.move: speed should be a positive number');
				speed = Math.abs(speed);
			}
			this.isMoving = true;

			let dist = Math.max(Math.abs(this.x - destX), Math.abs(this.y - destY));

			let percent = speed / dist;

			this.vel.x = (destX - this.x) * percent * world.tileSize;
			this.vel.y = (destY - this.y) * percent * world.tileSize;

			let totalSpeed = Math.sqrt(this.vel.x ** 2 + this.vel.y ** 2);

			// estimate how many frames it will take for the sprite
			// to reach its destination
			let frames = Math.floor(dist / totalSpeed) - 5;

			// margin of error
			let margin = totalSpeed - 0.001;

			return (async () => {
				let distX = margin + margin;
				let distY = margin + margin;
				do {
					await p5.prototype.delay();

					// skip calculations if not close enough to destination yet
					if (frames > 0) {
						frames--;
						continue;
					}
					// check if the sprite has reached its destination
					distX = Math.abs(this.x - this.dest.x);
					distY = Math.abs(this.y - this.dest.y);
				} while (this.isMoving && (distX > margin || distY > margin));
				// stop moving the sprite
				this.x = this.dest.x;
				this.y = this.dest.y;
				this.vel.x = 0;
				this.vel.y = 0;
				this.isMoving = false;
				// if a callback was given, call it
				if (typeof cb == 'function') cb();
			})();
		}

		/**
		 * Same as sprite.move
		 *
		 * @method moveTo
		 */
		moveTo(destX, destY, speed, cb) {
			return this.move(destX, destY, speed, cb);
		}

		/**
		 * Pushes the sprite toward a point.
		 * The force is added to the current velocity.
		 *
		 * Legacy method, use move or moveTowards instead.
		 *
		 * @deprecated
		 * @param {Number}  magnitude Scalar speed to add
		 * @param {Number}  x Direction x coordinate
		 * @param {Number}  y Direction y coordinate
		 */
		attractionPoint(magnitude, x, y) {
			let angle = this.p.atan2(y - this.y, x - this.x);
			this.vel.x += this.p.cos(angle) * magnitude;
			this.vel.y += this.p.sin(angle) * magnitude;
		}

		/**
		 * Rotates the sprite towards a position or angle.
		 *
		 * @method rotateTowards
		 * @param {*} angle
		 * @param {*} tracking
		 */
		rotateTowards(x, y, tracking) {
			// tracking ??= 0.1;
			// console.log(angle, this.rotation, ang);
			// this.angularVelocity = (angle - this.rotation) * tracking;
		}

		/**
		 * Rotates the sprite to an angle with a specified speed.
		 *
		 * @method rotate
		 * @param {Number} angle
		 * @param {Number} speed
		 */
		rotate(angle, speed) {
			if (!angle) {
				throw new Error('angle must be a number greater or less than zero');
			}
			let ang = this.rotation + angle;
			let mod = ang - this.rotation > 0 ? 1 : -1;
			this.rotationSpeed = speed * mod;

			return (async () => {
				let cw = ang > this.rotation;
				while ((cw && this.rotation < ang) || (!cw && this.rotation > ang)) {
					await p5.prototype.delay();
				}
				this.rotationSpeed = 0;
				this.rotation = ang;
			})();
		}

		/**
		 * Changes the sprite's animation. Use `addAni` to define the
		 * animation(s) first.
		 *
		 * @method ani
		 * @param {...String} anis the names of one or many animations to be played in
		 * sequence
		 * @returns A promise that fulfills when the animation or sequence of animations
		 * completes
		 */
		async ani(...anis) {
			let count = ++this._aniChanged;

			for (let i = 0; i < anis.length; i++) {
				if (typeof anis[i] == 'string') anis[i] = { name: anis[i] };
				let ani = anis[i];
				if (ani.name[0] == '!') {
					ani.name = ani.name.slice(1);
					ani.start = -1;
					ani.end = 0;
				}
			}

			let _ani = (name, start, end) => {
				return new Promise((resolve) => {
					this._changeAnimation(name);
					if (start < 0) {
						start = this.animation.images.length + start;
					}
					if (start) this.animation.frame = start;
					if (end != undefined) this.animation.goToFrame(end);
					this.animation.onComplete = () => {
						resolve();
					};
				});
			};

			for (let i = 0; i < anis.length; i++) {
				let ani = anis[i];
				if (ani.name == '*') {
					if (count == this._aniChanged) i = 0;
					continue;
				}
				let { name, start, end } = ani;
				await _ani(name, start, end);
			}
		}

		/**
		 * Change the sprite's image
		 *
		 * @param {String} img The name of the image
		 * @returns A promise that returns when the sequence is complete
		 */
		img(img) {
			return this.ani(img);
		}

		/**
		 * Add an animation.
		 *
		 * @param {String} name
		 * @param {Object} atlas
		 */
		addAni(name, atlas) {
			if (typeof name != 'string') {
				atlas = name;
				name = 'default';
			}
			if (Array.isArray(atlas)) {
				atlas = { pos: atlas };
			}
			let { size, pos, line, frames, frameDelay } = atlas;
			size ??= [this.w / this.scale, this.h / this.scale];
			pos ??= line || 0;
			let spriteSheetImg = this.spriteSheet || world.spriteSheet;
			this.addAnimation(name, createAni(spriteSheetImg, size, pos, frames, frameDelay));
		}

		addImg(name, atlas) {
			this.addAni(name, atlas);
		}

		/**
		 * Add multiple animations
		 *
		 * @method addAnis
		 * @param {Object} atlases
		 */
		addAnis(atlases) {
			for (let name in atlases) {
				let atlas = atlases[name];
				this.addAni(name, atlas);
			}
		}

		/**
		 * Removes the Sprite from the sketch.
		 * The removed Sprite will not be drawn or updated anymore.
		 *
		 * @method remove
		 */
		remove() {
			if (this.body) world.destroyBody(this.body);
			this.removed = true;

			//when removed from the "scene" also remove all the references in all the groups
			while (this.groups.length > 0) {
				this.groups[0].remove(this);
			}
		}

		/**
		 * Adds the sprite to an existing group
		 *
		 * @method addToGroup
		 * @param {Group} group
		 */
		addToGroup(group) {
			if (group instanceof Array) {
				// if the sprite has not been already to the group
				if (group.add(this)) this.groups.push(group);
			} else this.p.print('addToGroup error: ' + group + ' is not a group');
		}

		/**
		 * Returns the sprite's unique identifier
		 *
		 * @method toString
		 * @returns the sprite's id
		 */
		toString() {
			return 's' + this.idNum;
		}

		createJoint(type, { body }, props, anchor) {
			let j;
			const bodyA = this;
			j = {
				bodyA: this.body,
				bodyB: body,
				length: props.length != undefined ? props.length / plScale : null,
				frequencyHz: props.frequencyHz,
				dampingRatio: props.dampingRatio,
				collideConnected: props.collideConnected,
				maxLength: props.maxLength != undefined ? props.maxLength / plScale : null,
				userData: props.userData,
				lengthA: props.lengthA != undefined ? props.lengthA / plScale : null,
				lengthB: props.lengthB != undefined ? props.lengthB / plScale : null,
				ratio: props.ratio,
				groundAnchorA: props.groundAnchorA ? scaleTo(props.groundAnchorA) : new pl.Vec2(0, 0),
				groundAnchorB: props.groundAnchorB ? scaleTo(props.groundAnchorB) : new pl.Vec2(0, 0),
				enableLimit: props.enableLimit,
				enableMotor: props.enableMotor,
				lowerAngle: props.lowerAngle,
				maxMotorTorque: props.maxMotorTorque,
				maxMotorForce: props.maxMotorForce,
				motorSpeed: props.motorSpeed,
				referenceAngle: props.referenceAngle,
				upperAngle: props.upperAngle,
				maxForce: props.maxForce,
				maxTorque: props.maxTorque,
				localAxisA: props.localAxisA,
				upperTranslation: props.upperTranslation ? props.upperTranslation / plScale : 1,
				lowerTranslation: props.lowerTranslation ? props.lowerTranslation / plScale : 1,
				angularOffset: props.angularOffset,
				joint1: props.joint1,
				joint2: props.joint2,
				correctionFactor: props.correctionFactor,
				linearOffset: props.linearOffset ? scaleTo(props.linearOffset) : new pl.Vec2(0, 0)
			};
			if (anchor) {
				j.localAnchorA = bodyA.body.getLocalPoint(scaleTo(anchor));
				j.localAnchorB = body.getLocalPoint(scaleTo(anchor));
			} else {
				j.localAnchorA = props.localAnchorA ? scaleTo(props.localAnchorA) : new pl.Vec2(0, 0);
				j.localAnchorB = props.localAnchorB ? scaleTo(props.localAnchorB) : new pl.Vec2(0, 0);
			}
			if (type == 'distance') {
				j = pl.DistanceJoint(j);
			} else if (type == 'pulley') {
				j = pl.PulleyJoint(j);
			} else if (type == 'wheel') {
				j = pl.WheelJoint(j);
			} else if (type == 'rope') {
				j = pl.RopeJoint(j);
			} else if (type == 'weld') {
				j = pl.WeldJoint(j);
			} else if (type == 'revolute') {
				j = pl.RevoluteJoint(j);
			} else if (type == 'gear') {
				j = pl.GearJoint(j);
			} else if (type == 'friction') {
				j = pl.FrictionJoint(j);
			} else if (type == 'motor') {
				j = pl.MotorJoint(j);
			} else if (type == 'prismatic') {
				j = pl.PrismaticJoint(j);
			} else if (type == 'mouse') {
				/*j = new box2d.b2MouseJointDef();
            j.bodyA = bodyA!=null?bodyA.body:b2world.CreateBody(new box2d.b2BodyDef());
            j.bodyB = bodyB.body;
            j.target = b2scaleTo(props.xy);
            j.collideConnected = true;
            j.maxForce = props.maxForce||(1000.0 * bodyB.body.GetMass());
            j.frequencyHz = props.frequency||5;  // Try a value less than 5 (0 for no elasticity)
            j.dampingRatio = props.damping||0.9; // Ranges between 0 and 1 (1 for no springiness)
            bodyB.body.SetAwake(true);
            bodyA=bodyB;*/
			}
			return world.createJoint(j);
		}

		/**
		 * Checks if the the sprite is colliding with another sprite or a group.
		 * The check is performed using the sprite's physics body (colliders).
		 *
		 * A callback function can be specified to perform additional operations
		 * when contact occurs. If the target is a group the function will be called
		 * for each single sprite colliding. The parameter of the function are
		 * respectively the current sprite and the colliding sprite.
		 *
		 * @example
		 *     sprite.collide(otherSprite, explosion);
		 *
		 *     function explosion(spriteA, spriteB) {
		 *       spriteA.remove();
		 *       spriteB.score++;
		 *     }
		 *
		 * @method collide
		 * @param {Sprite|Group} target Sprite or group to check against the current one
		 * @param {Function} [callback] The function to be called if overlap is positive
		 */
		collide(target, callback) {
			this.collides[target] = callback || true;
		}

		/**
		 * Deprecated, use sprite.collide instead.
		 *
		 * @deprecated
		 * @method bounce
		 * @param {Sprite|Group} target
		 * @param {Function} callback
		 */
		bounce(target, callback) {
			this.collide(target, callback);
		}

		/**
		 * Deprecated, use sprite.collide instead.
		 *
		 * @deprecated
		 * @method displace
		 * @param {Sprite|Group} target
		 * @param {Function} callback
		 */
		displace(target, callback) {
			this.collide(target, callback);
		}

		/**
		 * Checks if this sprite is overlapping with another sprite or a group.
		 * The check is performed using the sprite's physics body (colliders).
		 *
		 * A callback function can be specified to perform additional operations
		 * when contact occurs. If the target is a group the function will be called
		 * for each single sprite overlapping. The parameters of the callback function
		 * are the current sprite and the overlapping sprite.
		 *
		 * Since v3, this function only needs to be called once, it doesn't need to be
		 * used in the p5.js draw loop.
		 *
		 * @example
		 *   sprite.overlap(otherSprite, pickup);
		 *
		 *   function pickup(spriteA, spriteB) {
		 *     spriteA.remove();
		 *     spriteB.itemCount++;
		 *   }
		 *
		 * @method overlap
		 * @param {Sprite|Group} target Sprite or group to check against the current one
		 * @param {Function} [callback] The function to be called if an overlap occurs
		 */
		overlap(target, callback) {
			if (!this.sensor) {
				let shape;
				for (let fxt = this.fixtureList; fxt; fxt = fxt.getNext()) {
					shape = fxt.m_shape;
					break;
				}

				this.sensor = this.body.createFixture({
					shape: shape,
					isSensor: true
				});
			}

			this.overlaps[target] = callback || true;
			return this.touching[target];
		}

		/**
		 * Use sprite.animation.name instead.
		 *
		 * @deprecated
		 * @returns the name of the sprite's current animation
		 */
		getAnimationLabel() {
			return this.animation.name;
		}
	}

	/**
	 * A SpriteAnimation object contains a series of images (p5.Image objects)
	 * that can be displayed sequentially.
	 *
	 * A sprite can have multiple labeled animations, see Sprite.addAnimation
	 * and Sprite.changeAnimation, but you can also create animations that
	 * can be used without being added to a sprite first.
	 *
	 * An animation can be created either from a list of images or sequentially
	 * numbered images. p5.play will try to detect the sequence pattern.
	 *
	 * For example if the image file path is "image1.png" and the last frame
	 * index is 3 then "image2.png" and "image3.png" will be loaded as well.
	 *
	 * @example
	 *
	 * let shapeShifter = new SpriteAnimation("dog.png", "cat.png", "snake.png");
	 * let walking = new SpriteAnimation("walking0001.png", 5);
	 *
	 * @class SpriteAnimation
	 * @constructor
	 */
	class SpriteAnimation {
		constructor() {
			this.p = pInst;
			let args = [...arguments];

			/**
			 * The name of the animation
			 *
			 * @property name
			 * @type {String}
			 */
			this.name = 'default';

			if (typeof args[0] == 'string' && !args[0].includes('.')) {
				this.name = args[0];
				args = args.slice(1);
			}

			/**
			 * Array of frames (p5.Image)
			 *
			 * @property images
			 * @type {Array}
			 */
			this.images = [];

			/**
			 * The index of the current frame that the animation is on.
			 *
			 * @property frame
			 * @type {Number}
			 */
			this.frame = 0;

			this.cycles = 0;

			this.targetFrame = -1;

			/**
			 * The offset is how far the animation should be placed from
			 * the location it is played at.
			 *
			 * @property offset
			 * @type {Object} x and y keys
			 *
			 * @example
			 * offset.x = 16;
			 */
			this.offset = { x: 0, y: 0 };

			/**
			 * Delay between frames in number of draw cycles.
			 * If set to 4 the framerate of the animation would be the
			 * sketch framerate divided by 4 (60fps = 15fps)
			 *
			 * @property frameDelay
			 * @type {Number}
			 * @default 4
			 */
			this.frameDelay = 4;

			/**
			 * True if the animation is currently playing.
			 *
			 * @property playing
			 * @type {Boolean}
			 * @default true
			 */
			this.playing = true;

			/**
			 * Animation visibility.
			 *
			 * @property visible
			 * @type {Boolean}
			 * @default true
			 */
			this.visible = true;

			/**
			 * If set to false the animation will stop after reaching the last frame
			 *
			 * @property looping
			 * @type {Boolean}
			 * @default true
			 */
			this.looping = true;

			/**
			 * True if frame changed during the last draw cycle
			 *
			 * @property frameChanged
			 * @type {Boolean}
			 */
			this.frameChanged = false;

			//sequence mode
			if (
				args.length == 2 &&
				typeof args[0] == 'string' &&
				(typeof args[1] == 'string' || typeof args[1] == 'number')
			) {
				let from = args[0];
				let to, num2;
				if (!isNaN(args[1])) num2 = Number(args[1]);
				else to = args[1];

				// print("sequence mode "+from+" -> "+to);

				// make sure the extensions are fine
				if (from.slice(-4) != '.png' || (to && to.slice(-4) != '.png')) {
					throw new Error('SpriteAnimation error: you need to use .png files (filename ' + from + ')');
				}

				let digits1 = 0;
				let digits2 = 0;

				// skip extension work backwards to find the numbers
				for (let i = from.length - 5; i >= 0; i--) {
					if (!isNaN(from.charAt(i))) digits1++;
					else break;
				}

				if (to) {
					for (let i = to.length - 5; i >= 0; i--) {
						if (!isNaN(to.charAt(i))) digits2++;
						else break;
					}
				}

				let prefix1 = from.slice(0, -4 - digits1);
				let prefix2;
				if (to) prefix2 = to.slice(0, -4 - digits2);

				// images don't belong to the same sequence
				// they are just two separate images with numbers
				if (to && prefix1 != prefix2) {
					this.images.push(this.p.loadImage(from));
					this.images.push(this.p.loadImage(to));
				} else {
					// Our numbers likely have leading zeroes, which means that some
					// browsers (e.g., PhantomJS) will interpret them as base 8 (octal)
					// instead of decimal. To fix this, we'll explicity tell parseInt to
					// use a base of 10 (decimal). For more details on this issue, see
					// http://stackoverflow.com/a/8763427/2422398.
					let num1 = parseInt(from.slice(-4 - digits1, -4), 10);
					num2 ??= parseInt(to.slice(-4 - digits2, -4), 10);

					// swap if inverted
					if (num2 < num1) {
						let t = num2;
						num2 = num1;
						num1 = t;
					}

					let fileName;
					if (!to || digits1 == digits2) {
						// load all images
						for (let i = num1; i <= num2; i++) {
							// Use nf() to number format 'i' into the amount of digits
							// ex: 14 with 4 digits is 0014
							fileName = prefix1 + this.p.nf(i, digits1) + '.png';
							this.images.push(this.p.loadImage(fileName));
						}
					} // case: case img1, img2
					else {
						for (let i = num1; i <= num2; i++) {
							// Use nf() to number format 'i' into four digits
							fileName = prefix1 + i + '.png';
							this.images.push(this.p.loadImage(fileName));
						}
					}
				}
			} // end sequence mode
			// Sprite sheet mode
			else if (args.length == 1 && args[0] instanceof SpriteSheet) {
				this.spriteSheet = args[0];
				this.images = this.spriteSheet.frames;
			} else if (args.length != 0) {
				// arbitrary list of images
				for (let i = 0; i < args.length; i++) {
					if (args[i] instanceof p5.Image) this.images.push(args[i]);
					else this.images.push(this.p.loadImage(args[i]));
				}
			}
		}

		/**
		 * Use offset.x
		 *
		 * @deprecated
		 */
		get offX() {
			return this.offset.x;
		}
		set offX(val) {
			this.offset.x = val;
		}
		/**
		 * Use offset.y
		 *
		 * @deprecated
		 */
		get offY() {
			return this.offset.y;
		}
		set offY(val) {
			this.offset.y = val;
		}

		/**
		 * Objects are passed by reference so to have different sprites
		 * using the same animation you need to clone it.
		 *
		 * @private
		 * @return {SpriteAnimation} A clone of the current animation
		 */
		clone() {
			var myClone = new SpriteAnimation(); //empty
			myClone.images = [];

			if (this.spriteSheet) {
				myClone.spriteSheet = this.spriteSheet.clone();
			}
			myClone.images = this.images.slice();

			myClone.offset.x = this.offset.x;
			myClone.offset.y = this.offset.y;
			myClone.frameDelay = this.frameDelay;
			myClone.playing = this.playing;
			myClone.looping = this.looping;

			return myClone;
		}

		/**
		 * Draws the animation at coordinate x and y.
		 * Updates the frames automatically.
		 *
		 * @method draw
		 * @param {Number} x x coordinate
		 * @param {Number} y y coordinate
		 * @param {Number} [r=0] rotation
		 */
		draw(x, y, r) {
			this.x = x;
			this.y = y;
			this.rotation = r || 0;

			if (!this.visible) return;
			//only connection with the sprite class
			//if animation is used independently draw and update are the sam
			if (!this.isSpriteAnimation) this.update();

			this.p.push();
			this.p.imageMode(p5.prototype.CENTER);
			this.p.translate(this.x, this.y);
			this.p.rotate(this.rotation);
			if (this.images[this.frame] !== undefined) {
				if (this.spriteSheet) {
					let frame_info = this.images[this.frame].frame;

					this.p.image(
						this.spriteSheet.image,
						this.offset.x,
						this.offset.y,
						frame_info.width,
						frame_info.height,
						frame_info.x,
						frame_info.y,
						frame_info.width,
						frame_info.height
					);
				} else {
					this.p.image(this.images[this.frame], this.offset.x, this.offset.y);
				}
			} else {
				this.p.print('Warning undefined frame ' + this.frame);
				//this.isActive = false;
			}

			this.p.pop();
		}

		/**
		 * @private
		 */
		update() {
			this.cycles++;
			var previousFrame = this.frame;
			this.frameChanged = false;

			//go to frame
			if (this.images.length === 1) {
				this.playing = false;
				this.frame = 0;
			}

			if (this.playing && this.cycles % this.frameDelay === 0) {
				//going to target frame up
				if (this.targetFrame > this.frame && this.targetFrame !== -1) {
					this.frame++;
				}
				//going to target frame down
				else if (this.targetFrame < this.frame && this.targetFrame !== -1) {
					this.frame--;
				} else if (this.targetFrame === this.frame && this.targetFrame !== -1) {
					this.playing = false;
				} else if (this.looping) {
					//advance frame
					//if next frame is too high
					if (this.frame >= this.lastFrame) this.frame = 0;
					else this.frame++;
				} else {
					//if next frame is too high
					if (this.frame < this.lastFrame) this.frame++;
				}
			}
			if (
				this.onComplete &&
				((this.targetFrame == -1 && this.frame == this.lastFrame) || this.frame == this.targetFrame)
			) {
				if (this.looping) this.targetFrame = -1;
				this.onComplete(); //fire when on last frame
			}

			if (previousFrame !== this.frame) this.frameChanged = true;
		} //end update

		/**
		 * Plays the animation.
		 *
		 * @method play
		 */
		play() {
			this.playing = true;
			this.targetFrame = -1;
		}

		/**
		 * Stops the animation.
		 *
		 * @method stop
		 */
		stop() {
			this.playing = false;
		}

		/**
		 * Plays the animation backwards.
		 * Equivalent to ani.goToFrame(0)
		 *
		 * @method rewind
		 */
		rewind() {
			this.goToFrame(0);
		}

		/**
		 * fire when animation ends
		 *
		 * @method onComplete
		 * @return {SpriteAnimation}
		 */
		onComplete() {
			return undefined;
		}

		/**
		 * Deprecated, change the frame property directly.
		 *
		 * Changes the current frame.
		 *
		 * @deprecated
		 * @param {Number} frame Frame number (starts from 0).
		 */
		changeFrame(f) {
			if (f < this.images.length) this.frame = f;
			else this.frame = this.images.length - 1;

			this.targetFrame = -1;
			//this.playing = false;
		}

		/**
		 * Goes to the next frame and stops.
		 *
		 * @method nextFrame
		 */
		nextFrame() {
			if (this.frame < this.images.length - 1) this.frame = this.frame + 1;
			else if (this.looping) this.frame = 0;

			this.targetFrame = -1;
			this.playing = false;
		}

		/**
		 * Goes to the previous frame and stops.
		 *
		 * @method previousFrame
		 */
		previousFrame() {
			if (this.frame > 0) this.frame = this.frame - 1;
			else if (this.looping) this.frame = this.images.length - 1;

			this.targetFrame = -1;
			this.playing = false;
		}

		/**
		 * Plays the animation forward or backward toward a target frame.
		 *
		 * @method goToFrame
		 * @param {Number} toFrame Frame number destination (starts from 0)
		 */
		goToFrame(toFrame) {
			if (toFrame < 0 || toFrame >= this.images.length) {
				return;
			}

			// targetFrame gets used by the update() method to decide what frame to
			// select next.  When it's not being used it gets set to -1.
			this.targetFrame = toFrame;

			if (this.targetFrame !== this.frame) {
				this.playing = true;
			}
		}

		/**
		 * Use .frame instead.
		 *
		 * Returns the current frame number.
		 *
		 * @deprecated
		 * @return {Number} Current frame (starts from 0)
		 */
		getFrame() {
			return this.frame;
		}

		/**
		 * Use .lastFrame instead.
		 *
		 * Returns the last frame number.
		 *
		 * @deprecated
		 * @return {Number} Last frame number (starts from 0)
		 */
		getLastFrame() {
			return this.lastFrame;
		}

		get lastFrame() {
			return this.images.length - 1;
		}

		/**
		 * Returns the current frame image as p5.Image.
		 *
		 * @method getFrameImage
		 * @return {p5.Image} Current frame image
		 */
		getFrameImage() {
			return this.images[this.frame];
		}

		/**
		 * Returns the frame image at the specified frame number.
		 *
		 * @method getImageAt
		 * @param {Number} frame Frame number
		 * @return {p5.Image} Frame image
		 */
		getImageAt(f) {
			return this.images[f];
		}

		/**
		 * Use .w or .width instead.
		 *
		 * Returns the current frame width in pixels.
		 * If there is no image loaded, returns 1.
		 *
		 * @deprecated
		 * @method getWidth
		 * @return {Number} Frame width
		 */
		getWidth() {
			return this.width;
		}

		get w() {
			return this.width;
		}

		get width() {
			if (this.images[this.frame] instanceof p5.Image) {
				return this.images[this.frame].width;
			} else if (this.images[this.frame]) {
				// Special case: Animation-from-spritesheet treats its images array differently.
				return this.images[this.frame].frame.width;
			}
			return 1;
		}

		/**
		 * Use .h or .height instead.
		 *
		 * Returns the current frame height in pixels.
		 * If there is no image loaded, returns 1.
		 *
		 * @deprecated
		 * @return {Number} Frame height
		 */
		getHeight() {
			return this.height;
		}

		get h() {
			return this.height;
		}

		get height() {
			if (this.images[this.frame] instanceof p5.Image) {
				return this.images[this.frame].height;
			} else if (this.images[this.frame]) {
				// Special case: Animation-from-spritesheet treats its images array differently.
				return this.images[this.frame].frame.height;
			}
			return 1;
		}
	}

	/**
	 * Represents a sprite sheet and all it's frames.  To be used with SpriteAnimation,
	 * or static drawing single frames.
	 *
	 *  There are two different ways to load a SpriteSheet
	 *
	 * 1. Given width, height that will be used for every frame and the
	 *    number of frames to cycle through. The sprite sheet must have a
	 *    uniform grid with consistent rows and columns.
	 *
	 * 2. Given an array of frame objects that define the position and
	 *    dimensions of each frame.  This is Flexible because you can use
	 *    sprite sheets that don't have uniform rows and columns.
	 *
	 * @example
	 *     // Method 1 - Using width, height for each frame and number of frames
	 *     explode_sprite_sheet = loadSpriteSheet('assets/explode_sprite_sheet.png', 171, 158, 11);
	 *
	 *     // Method 2 - Using an array of objects that define each frame
	 *     var player_frames = loadJSON('assets/tiles.json');
	 *     player_sprite_sheet = loadSpriteSheet('assets/player_spritesheet.png', player_frames);
	 *
	 * @class SpriteSheet
	 * @constructor
	 * @param image String image path or p5.Image object
	 */
	class SpriteSheet {
		constructor() {
			this.p = pInst;
			var spriteSheetArgs = arguments;

			this.image = null;
			this.frames = [];
			this.frame_width = 0;
			this.frame_height = 0;
			this.num_frames = 0;

			if (spriteSheetArgs.length === 2 && Array.isArray(spriteSheetArgs[1])) {
				this.frames = spriteSheetArgs[1];
				this.num_frames = this.frames.length;
			} else if (
				spriteSheetArgs.length === 4 &&
				typeof spriteSheetArgs[1] === 'number' &&
				typeof spriteSheetArgs[2] === 'number' &&
				typeof spriteSheetArgs[3] === 'number'
			) {
				this.frame_width = spriteSheetArgs[1];
				this.frame_height = spriteSheetArgs[2];
				this.num_frames = spriteSheetArgs[3];
			}

			if (spriteSheetArgs[0] instanceof p5.Image) {
				this.image = spriteSheetArgs[0];
				if (spriteSheetArgs.length === 4) {
					this._generateSheetFrames();
				}
			} else {
				if (spriteSheetArgs.length === 2) {
					this.image = this.p.loadImage(spriteSheetArgs[0]);
				} else if (spriteSheetArgs.length === 4) {
					this.image = this.p.loadImage(spriteSheetArgs[0], this._generateSheetFrames.bind(this));
				}
			}
		}

		/**
		 * Generate the frames data for this sprite sheet based on user params
		 *
		 * @private
		 */
		_generateSheetFrames() {
			var sX = 0,
				sY = 0;
			for (var i = 0; i < this.num_frames; i++) {
				this.frames.push({
					name: i,
					frame: {
						x: sX,
						y: sY,
						width: this.frame_width,
						height: this.frame_height
					}
				});
				sX += this.frame_width;
				if (sX >= this.image.width) {
					sX = 0;
					sY += this.frame_height;
					if (sY >= this.image.height) {
						sY = 0;
					}
				}
			}
		}

		/**
		 * Draws a specific frame to the canvas.
		 *
		 * @method drawFrame
		 * @param frame_name  Can either be a string name, or a numeric index.
		 * @param x   x position to draw the frame at
		 * @param y   y position to draw the frame at
		 * @param [width]   optional width to draw the frame
		 * @param [height]  optional height to draw the frame
		 */
		drawFrame(frame_name, x, y, width, height) {
			var frameToDraw;
			if (typeof frame_name === 'number') {
				frameToDraw = this.frames[frame_name].frame;
			} else {
				for (var i = 0; i < this.frames.length; i++) {
					if (this.frames[i].name === frame_name) {
						frameToDraw = this.frames[i].frame;
						break;
					}
				}
			}
			var dWidth = width || frameToDraw.width;
			var dHeight = height || frameToDraw.height;

			this.p.image(
				this.image,
				x,
				y,
				dWidth,
				dHeight,
				frameToDraw.x,
				frameToDraw.y,
				frameToDraw.width,
				frameToDraw.height
			);
		}

		/**
		 * Objects are passed by reference so to have different sprites
		 * using the same animation you need to clone it.
		 *
		 * @return {SpriteSheet} A clone of the current SpriteSheet
		 */
		clone() {
			var myClone = new SpriteSheet(); //empty

			// Deep clone the frames by value not reference
			for (var i = 0; i < this.frames.length; i++) {
				var frame = this.frames[i].frame;
				var cloneFrame = {
					name: frame.name,
					frame: {
						x: frame.x,
						y: frame.y,
						width: frame.width,
						height: frame.height
					}
				};
				myClone.frames.push(cloneFrame);
			}

			// clone other fields
			myClone.image = this.image;
			myClone.frame_width = this.frame_width;
			myClone.frame_height = this.frame_height;
			myClone.num_frames = this.num_frames;

			return myClone;
		}
	}

	/**
	 * In p5.play groups are collections of sprites with similar behavior.
	 * For example a group may contain all the coin sprites that the
	 * player can collect.
	 *
	 * Group extends Array. You can use them in for loops just like arrays
	 * since they inherit all the properties of standard arrays such as
	 * group.length
	 *
	 * Since groups contain only references, a sprite can be in multiple
	 * groups and deleting a group doesn't affect the sprites themselves.
	 *
	 * sprite.remove() removes the sprite from all the groups
	 * it belongs to.
	 *
	 * @class Group
	 * @constructor
	 */
	class Group extends Array {
		constructor(...args) {
			super(...args);
			this.p = pInst;

			/**
			 * Keys are the animation label, values are SpriteAnimation objects.
			 *
			 * @property animations
			 * @type {Object}
			 */
			this.animations = {};

			/**
			 * Contains all the collision callback functions for this sprite
			 * when it comes in contact with other sprites or groups.
			 * @property collides
			 */
			this.collides = {};

			/**
			 * Contains all the overlap callback functions for this sprite
			 * when it comes in contact with other sprites or groups.
			 * @property overlaps
			 */
			this.overlaps = {};

			// mainly for internal use
			// shouldCull as a property of allSprites only refers to the default allSprites cull
			// in the post draw function, if the user calls cull on allSprites it should work
			// for any other group made by users shouldCull affects whether cull removes sprites or not
			// by default for allSprites it is set to true, for all other groups it is undefined
			this.shouldCull;

			this.shape;

			this.idNum = groupCount++;

			if (world) world.groups.push(this);
		}

		get debug() {
			return this._debug;
		}

		set debug(val) {
			for (let s of this) {
				s.debug = val;
			}
			this._debug = val;
		}

		/**
		 * The default layer for sprites in the group.
		 *
		 * @property layer
		 */
		get layer() {
			return this._layer;
		}

		set layer(val) {
			for (let s of this) {
				s.layer = val;
			}
			this._layer = val;
		}

		group() {
			return this.subGroup();
		}

		subGroup() {
			let g = new Group();
			let traits = Object.assign({}, this);
			let deletes = ['groupID', 'p', 'length', 'collides', 'overlaps'];
			for (let d of deletes) {
				delete traits[d];
			}
			for (let prop in traits) {
				if (!isNaN(prop)) continue;
				if (typeof traits[prop] == 'object') {
					g[prop] = Object.assign({}, traits[prop]);
				} else {
					g[prop] = traits[prop];
				}
			}
			return g;
		}

		sprite() {
			let s = new Sprite(this, ...arguments);
			let traits = Object.assign({}, this);

			let deletes = ['groupID', 'p', 'length', 'collides', 'overlaps', 'animation', 'animations'];
			for (let d of deletes) {
				delete traits[d];
			}
			for (let prop in traits) {
				if (!isNaN(prop)) continue;
				if (typeof traits[prop] == 'object') {
					s[prop] = Object.assign({}, traits[prop]);
				} else {
					s[prop] = traits[prop];
				}
			}
			return s;
		}

		createSprite() {
			return this.sprite(...arguments);
		}

		addAni(name, atlas) {
			// if (typeof name != 'string') {
			// 	atlas = name;
			// 	name = 'default' + this.animations.length;
			// }
			if (Array.isArray(atlas)) {
				atlas = { pos: atlas };
			}
			let { size, pos, line, frames, frameDelay } = atlas;
			size ??= this.tileSize || world.tileSize;
			pos ??= line || 0;
			let sheet = this.spriteSheet || world.spriteSheet;
			this.addAnimation(name, createAni(sheet, size, pos, frames, frameDelay));
		}

		addImg(name, atlas) {
			this.addAni(name, atlas);
		}

		addAnis(atlases) {
			for (let name in atlases) {
				let atlas = atlases[name];
				this.addAni(name, atlas);
			}
		}

		// group.snap = function (o, dist) {
		// 		if (o.isMoving) return;
		// 		dist ??= 1;
		// 		for (let i = 0; i < this.length; i++) {
		// 			let sprite = this[i];
		// 			let row = (sprite.y - _this.y) / sprite.w;
		// 			let col = (sprite.x - _this.x) / sprite.h;
		// 			if (Math.abs(row) % 1 >= dist || Math.abs(col) % 1 >= dist) continue;
		// 			row = Math.round(row);
		// 			col = Math.round(col);
		// 			sprite._row = row;
		// 			sprite._col = col;
		// 			sprite.velocity.x = 0;
		// 			sprite.velocity.y = 0;
		// 			sprite.y = _this.y + row * sprite.w;
		// 			sprite.x = _this.x + col * sprite.h;
		// 		}
		// 	};

		/**
		 * Checks if a sprite in the group is colliding with another sprite or a group.
		 * The check is performed using the sprite's physics body (colliders).
		 *
		 * A callback function can be specified to perform additional operations
		 * when contact occurs. If the target is a group the function will be called
		 * for each single sprite colliding. The parameter of the function are
		 * respectively the current sprite and the colliding sprite.
		 *
		 * Since v3, this function only needs to be called once, it doesn't need to be
		 * used in the p5.js draw loop.
		 *
		 * @example
		 *     group.collide(otherSprite, explosion);
		 *
		 *     function explosion(spriteA, spriteB) {
		 *       spriteA.remove();
		 *       spriteB.score++;
		 *     }
		 *
		 * @method collide
		 * @param {Sprite|Group} target Sprite or group to check against the current one
		 * @param {Function} [callback] The function to be called when a collision occurs
		 */
		collide(target, callback) {
			this.collides[target] = callback || true;
		}

		/**
		 * Deprecated, use group.collide instead.
		 *
		 * @deprecated
		 * @method bounce
		 * @param {Sprite|Group} target
		 * @param {Function} callback
		 */
		bounce(target, callback) {
			this.collide(target, callback);
		}

		/**
		 * Deprecated, use group.collide instead.
		 *
		 * @deprecated
		 * @method displace
		 * @param {Sprite|Group} target
		 * @param {Function} callback
		 */
		displace(target, callback) {
			this.collide(target, callback);
		}

		/**
		 * Checks if a sprite in the group is overlapping with another sprite or a group.
		 * The check is performed using the sprite's physics body (colliders).
		 *
		 * A callback function can be specified to perform additional operations
		 * when contact occurs. If the target is a group the function will be called
		 * for each single sprite overlapping. The parameter of the function are
		 * respectively the current sprite and the overlapping sprite.
		 *
		 * Since v3, this function only needs to be called once, it doesn't need to be
		 * used in the p5.js draw loop.
		 *
		 * @example
		 *     group.overlap(otherSprite, pickup);
		 *
		 *     function pickup(spriteA, spriteB) {
		 *       spriteA.remove();
		 *       spriteB.itemCount++;
		 *     }
		 *
		 * @method overlap
		 * @param {Sprite|Group} target Sprite or group to check against the current one
		 * @param {Function} [callback] The function to be called if overlap is positive
		 */
		overlap(target, callback) {
			this.overlaps[target] = callback || true;
		}

		/**
		 * Gets the member at index i.
		 *
		 * @deprecated
		 * @method get
		 * @param {Number} i The index of the object to retrieve
		 */
		get(i) {
			return this[i];
		}

		/**
		 * Checks if the group contains a sprite.
		 *
		 * @method contains
		 * @param {Sprite} sprite The sprite to search
		 * @return {Number} Index or -1 if not found
		 */
		contains(sprite) {
			return this.indexOf(sprite) > -1;
		}

		/**
		 * Adds a sprite to the group. Returns true if the sprite was added
		 * because it was not already in the group.
		 *
		 * @method push
		 * @param {Sprite} s The sprite to be added
		 */
		push(s) {
			if (!(s instanceof Sprite)) {
				throw new Error('you can only add sprites to a group');
			}

			if (-1 === this.indexOf(s)) {
				super.push(s);
				s.groups.push(this);
				return true;
			}
		}

		/**
		 * Adds a sprite to the group. Returns true if the sprite was added
		 * because it was not already in the group.
		 *
		 * @method add
		 * @param {Sprite} s The sprite to be added
		 */
		add(s) {
			this.push(s);
		}

		/**
		 * Same as group.length
		 *
		 * @method size
		 */
		size() {
			return this.length;
		}

		/**
		 * Returns the group's unique identifier.
		 *
		 * @returns {String} groupID
		 */
		toString() {
			return 'g' + this.idNum;
		}

		/**
		 * Remove sprites that go outside the culling boundary
		 *
		 * @method cull
		 * @param {Number} top|size The distance that sprites can move below the p5.js canvas before they are removed. *OR* The distance sprites can travel outside the screen on all sides before they get removed.
		 * @param {Number} bottom|cb The distance that sprites can move below the p5.js canvas before they are removed.
		 * @param {Number} [left] The distance that sprites can move beyond the left side of the p5.js canvas before they are removed.
		 * @param {Number} [right] The distance that sprites can move beyond the right side of the p5.js canvas before they are removed.
		 * @param {Function} [cb(sprite)] The callback is given the sprite that
		 * passed the cull boundary, if no callback is given the sprite is
		 * removed by default
		 */
		cull(top, bottom, left, right, cb) {
			if (this.shouldCull === false && !this._isAllSpritesGroup) return;

			if (left === undefined) {
				let size = top;
				cb = bottom;
				top = bottom = left = right = size;
			}
			if (isNaN(top) || isNaN(bottom) || isNaN(left) || isNaN(right)) {
				throw new TypeError('The culling boundary must be defined with numbers');
			}
			if (cb && typeof cb != 'function') {
				throw new TypeError('The callback to group.cull must be a function');
			}

			let minX = -left;
			let minY = -top;
			let maxX = this.p.width + right;
			let maxY = this.p.height + bottom;

			for (let s of this) {
				if (s.x < minX || s.y < minY || s.x > maxX || s.y > maxY) {
					if (cb) cb(s);
					else s.remove();
				}
			}

			// no need to cull allSprites again post draw
			// if the user used cull on allSprites to redefine the cull boundary
			if (this._isAllSpritesGroup) this.shouldCull = false;
		}

		/**
		 * Removes all the sprites in the group
		 * from the scene.
		 *
		 * @method removeSprites
		 */
		removeSprites() {
			this.removeAll();
		}

		/**
		 * Removes all the sprites in the group
		 * from the scene.
		 *
		 * @method removeAll
		 */
		removeAll() {
			while (this.length > 0) {
				this[0].remove();
			}
		}

		/**
		 * Removes a sprite from the group.
		 * Does not remove the actual sprite, only the reference.
		 *
		 * @method remove
		 * @param {Sprite} item The sprite to be removed
		 * @return {Boolean} true if sprite was found and removed
		 */
		remove(item) {
			if (!(item instanceof Sprite)) {
				throw new TypeError('you can only remove sprites from a group');
			}

			var i,
				removed = false;
			for (i = this.length - 1; i >= 0; i--) {
				if (this[i] === item) {
					this.splice(i, 1);
					removed = true;
				}
			}

			if (removed) {
				for (i = item.groups.length - 1; i >= 0; i--) {
					if (item.groups[i] === this) {
						item.groups.splice(i, 1);
					}
				}
			}

			return removed;
		}

		/**
		 * Returns the highest depth in a group
		 *
		 * @method maxDepth
		 * @return {Number} The depth of the sprite drawn on the top
		 */
		maxDepth() {
			if (this.length == 0) return 0;
			if (this.length == 1 && this[0].depth === undefined) return 0;

			return this.reduce(function (maxDepth, sprite) {
				return Math.max(maxDepth, sprite.depth);
			}, -Infinity);
		}

		/**
		 * Returns the lowest depth in a group
		 *
		 * @method minDepth
		 * @return {Number} The depth of the sprite drawn on the bottom
		 */
		minDepth() {
			if (this.length === 0) {
				return 99999;
			}

			return this.reduce(function (minDepth, sprite) {
				return Math.min(minDepth, sprite.depth);
			}, Infinity);
		}

		/**
		 * Adds an image to the Group.
		 * 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 Group but not necessarily displayed
		 * until Sprite.changeAnimation(label) is called
		 *
		 * Usages:
		 * - group.addImage(label, image);
		 * - group.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
		 */
		addImage() {
			let args = arguments;
			if (typeof args[0] === 'string' && args[1] instanceof p5.Image) {
				this.addAnimation(args[0], args[1]);
			} else if (args[0] instanceof p5.Image) {
				this.addAnimation('default', args[0]);
			} else {
				throw new TypeError('only accepts a p5.image or an image label string followed by a p5.image)');
			}
		}

		/**
		 * Adds an animation to the group. This function should be used in
		 * the preload p5.js function. You don't need to name the animation if
		 * the sprites in the group will only use one animation. See SpriteAnimation
		 * for more information.
		 *
		 * Uses:
		 * - group.addAnimation(label, animation);
		 * - group.addAnimation(label, firstFrame, lastFrame);
		 * - group.addAnimation(label, frame1, frame2, frame3...);
		 *
		 * @method addAnimation
		 * @param {String} label SpriteAnimation identifier
		 * @param {SpriteAnimation} animation The preloaded animation
		 */
		addAnimation() {
			let args = [...arguments];
			let name, anim;
			if (args[0] instanceof SpriteAnimation) {
				anim = args[0].clone();
				name = anim.name || 'default';
				anim.name = name;
			} else if (args[1] instanceof SpriteAnimation) {
				name = args[0];
				anim = args[1].clone();
				anim.name = name;
			} else {
				anim = new SpriteAnimation(...args);
				name = anim.name;
			}
			anim.isSpriteAnimation = true;
			this.animations[name] = anim;
			if (!this.animation) {
				this.animation = anim;
			}
			return anim;
		}

		draw() {
			this.p.push();
			this.p.imageMode(p5.prototype.CENTER);
			this.p.rectMode(p5.prototype.CENTER);
			this.p.ellipseMode(p5.prototype.CENTER);
			let g = [...this];
			g.sort((a, b) => a.layer - b.layer);
			for (let i = 0; i < g.length; i++) {
				let sprite = g[i];
				if (sprite.life-- < 0) {
					sprite.remove();
					g.splice(i, 1);
					i--;
					continue;
				}
				if (sprite.visible) sprite.display();
			}
			this.p.pop();
		}
	}

	/**
	 * World
	 */
	class World extends pl.World {
		constructor(gravityX, gravityY, tileSize) {
			super(new pl.Vec2(gravityX || 0, gravityY || 0), true);
			this.p = pInst;
			this.width = this.p.width;
			this.height = this.p.height;
			this.tileSize = tileSize || 1;
			this._offset = { x: 0, y: 0 };
			let _this = this;
			this.offset = {
				get x() {
					return -_this._offset.x;
				},
				/**
				 * @property offset.x
				 */
				set x(val) {
					_this._offset.x -= val;
					_this.origin.x -= val;
				},
				get y() {
					return -_this._offset.y;
				},
				/**
				 * @property offset.y
				 */
				set y(val) {
					_this._offset.y -= val;
					_this.origin.y -= val;
				}
			};
			this.resize();
			this.spriteSheet;
			this.groups = [this.p.allSprites];
			this.on('begin-contact', this._beginContact);
			this.on('end-contact', this._endContact);
			world = this;
		}

		resize() {
			this.origin = {
				x: this.p.width * 0.5,
				y: this.p.height * 0.5
			};
			if (this.tileSize != 1) {
				this.origin.x -= this.tileSize * 0.5 + this.offset.x;
				this.origin.y -= this.tileSize * 0.5 + this.offset.y;
			}
		}

		_beginContact(contact) {
			// Get both fixtures
			let a = contact.m_fixtureA;
			let b = contact.m_fixtureB;

			let contactType = 'collides';
			if (a.isSensor() || b.isSensor()) contactType = 'overlaps';

			a = a.m_body.sprite;
			b = b.m_body.sprite;

			a.touching[b] = true;
			b.touching[a] = true;

			// log(a, b);
			let cb = _findContactCB(contactType, a, b);
			if (cb) {
				contacts.push([cb, a, b]);
				return;
			}
			cb = _findContactCB(contactType, b, a);
			if (cb) contacts.push([cb, b, a]);
		}

		_endContact(contact) {
			let a = contact.m_fixtureA.m_body.sprite;
			let b = contact.m_fixtureB.m_body.sprite;
			a.touching[b] = false;
			b.touching[a] = false;
		}

		get gravity() {
			return this.m_gravity;
		}
		/**
		 * Gravity vector
		 * @property gravity
		 */
		set gravity(val) {
			this.setGravity(val);
		}

		createTiles(tiles, x, y, w, h) {
			if (typeof tiles == 'string') tiles = tiles.split('\n');

			x ??= 0;
			y ??= 0;

			for (let row = 0; row < tiles.length; row++) {
				for (let col = 0; col < tiles[row].length; col++) {
					let t = tiles[row][col];
					if (t == ' ') continue;
					let anim, g;
					for (g of this.groups) {
						anim = g.animations[t];
						if (anim) break;
					}
					if (!anim) throw new Error("Couldn't find tile: " + t);
					g.createSprite(anim, x + col, y + row, w, h);
				}
			}
		}
	}

	/**
	 *
	 * @private
	 * @param {String} type "collides" or "overlaps"
	 * @param {Sprite} s0
	 * @param {Sprite} s1
	 * @returns contact cb if one can be found between the two sprites
	 */
	function _findContactCB(type, s0, s1) {
		let cb = s0[type][s1];
		if (cb) return cb;

		for (let g1 of s1.groups) {
			cb = s0[type][g1];
			if (cb) return cb;
		}

		for (let g0 of s0.groups) {
			cb = g0[type][s1];
			if (cb) return cb;
			for (let g1 of s1.groups) {
				cb = g0[type][g1];
				if (cb) return cb;
			}
		}
		return false;
	}

	pl.Fixture.prototype.shouldCollide = function (that) {
		// should this and that collide?
		let a = this;
		let b = that;

		if (a.isSensor() || b.isSensor()) return true;

		a = a.m_body.sprite;
		b = b.m_body.sprite;

		let cb = _findContactCB('overlaps', a, b);
		if (!cb) cb = _findContactCB('overlaps', b, a);
		if (cb) return false;
		return true;
	};

	/**
	 * @method createTiles
	 * @param {String|Array} tiles String or array of strings
	 */
	p5.prototype.createTiles = function (tiles) {
		world.createTiles(tiles);
	};

	/**
	 * This function is automatically called at the end of the p5.js draw
	 * loop, unless it was already called in the draw loop.
	 *
	 * @method updateSprites
	 * @param {Number} timeStep
	 * @param {Number} velocityIterations
	 * @param {Number} positionIterations
	 */
	p5.prototype.updateSprites = function (timeStep, velocityIterations, positionIterations) {
		if (!world) world = new World();

		for (let s of this.allSprites) {
			s.previousPosition.x = s.x;
			s.previousPosition.y = s.y;
		}

		// 2nd and 3rd arguments are velocity and position iterations
		world.step(timeStep || 1 / 60, velocityIterations || 8, positionIterations || 3);

		for (let c of contacts) {
			if (typeof c[0] == 'function') {
				c[0](c[1], c[2]);
			}
		}
		contacts = [];

		for (let s of this.allSprites) {
			s.update();
		}

		this._p5play.autoUpdateSprites = false;
	};

	/**
	 * Returns the sprite at
	 *
	 * @method getSpriteAt
	 * @param {Number} x
	 * @param {Number} y
	 * @returns
	 */
	p5.prototype.getSpriteAt = function (x, y) {
		const convertedPoint = new pl.Vec2(x / plScale, y / plScale);
		const aabb = new pl.AABB();
		aabb.lowerBound = new pl.Vec2(convertedPoint.x - 0.001, convertedPoint.y - 0.001);
		aabb.upperBound = new pl.Vec2(convertedPoint.x + 0.001, convertedPoint.y + 0.001);

		// Query the world for overlapping shapes.
		let selectedFxt = null;
		world.queryAABB(aabb, (fxt) => {
			if (!fxt.getBody().isStatic()) {
				if (fxt.getShape().testPoint(fxt.getBody().getTransform(), convertedPoint)) {
					selectedFxt = fxt;
					return false;
				}
			}
			return true;
		});
		if (selectedFxt) {
			for (let s of allSprites) {
				if (selectedFxt == s.body.m_fixtureList) {
					return s;
				}
			}
		}
		return null;
	};

	// const debugDraw = (canvas, scale, world) => {
	// 	const context = canvas.getContext('2d');
	// 	//context.fillStyle = '#DDD';
	// 	//context.fillRect(0, 0, canvas.width, canvas.height);

	// 	// Draw joints
	// 	for (let j = world.m_jointList; j; j = j.m_next) {
	// 		context.lineWidth = 0.25;
	// 		context.strokeStyle = '#00F';
	// 		drawJoint(context, scale, world, j);
	// 	}
	// };

	// const drawJoint = (context, scale, world, joint) => {
	// 	context.save();
	// 	context.scale(scale, scale);
	// 	context.lineWidth /= scale;

	// 	const b1 = joint.m_bodyA;
	// 	const b2 = joint.m_bodyB;
	// 	const x1 = b1.getPosition();
	// 	const x2 = b2.getPosition();
	// 	let p1;
	// 	let p2;
	// 	context.beginPath();
	// 	switch (joint.m_type) {
	// 		case 'distance-joint':
	// 		case 'rope-joint':
	// 			context.moveTo(x1.x, x1.y);
	// 			context.lineTo(x2.x, x2.y);
	// 			break;
	// 		case 'wheel-joint':
	// 		case 'revolute-joint':
	// 			p1 = joint.m_localAnchorA;
	// 			p2 = joint.m_localAnchorB;
	// 			const a = b2.getAngle();
	// 			const v = new pl.Vec2(cos(a), sin(a));
	// 			context.moveTo(x2.x, x2.y);
	// 			context.lineTo(x2.x + v.x, x2.y + v.y);
	// 			break;
	// 		case 'mouse-joint':
	// 		case 'weld-joint':
	// 			p1 = joint.getAnchorA();
	// 			p2 = joint.getAnchorB();
	// 			context.moveTo(p1.x, p1.y);
	// 			context.lineTo(p2.x, p2.y);
	// 			break;
	// 		case 'pulley-joint':
	// 			p1 = joint.m_groundAnchorA;
	// 			p2 = joint.m_groundAnchorB;
	// 			context.moveTo(p1.x, p1.y);
	// 			context.lineTo(x1.x, x1.y);
	// 			context.moveTo(p2.x, p2.y);
	// 			context.lineTo(x2.x, x2.y);
	// 			context.moveTo(p1.x, p1.y);
	// 			context.lineTo(p2.x, p2.y);
	// 			break;
	// 		default:
	// 			break;
	// 	}
	// 	context.closePath();
	// 	context.stroke();
	// 	context.restore();
	// };

	function getAABB(body) {
		const aabb = new pl.AABB();
		const t = new pl.Transform();
		t.setIdentity();
		const shapeAABB = new pl.AABB();
		aabb.lowerBound = new pl.Vec2(1000000, 1000000);
		aabb.upperBound = new pl.Vec2(-1000000, -1000000);
		let fixture = body.body.getFixtureList();
		while (fixture) {
			const shape = fixture.getShape();
			const childCount = shape.getChildCount(); //only for chains
			for (let child = 0; child < childCount; ++child) {
				shape.computeAABB(shapeAABB, body.body.m_xf, child);
				unionTo(aabb, shapeAABB);
			}
			fixture = fixture.getNext();
		}
		aabb.lowerBound.mul(plScale); //upper left, offset from center
		aabb.upperBound.mul(plScale); //lower right
		return aabb;
	}

	function unionTo(a, b) {
		a.lowerBound.x = min(a.lowerBound.x, b.lowerBound.x);
		a.lowerBound.y = min(a.lowerBound.y, b.lowerBound.y);
		a.upperBound.x = max(a.upperBound.x, b.upperBound.x);
		a.upperBound.y = max(a.upperBound.y, b.upperBound.y);
	}

	// The ray cast collects multiple hits along the ray in ALL mode.
	// The fixtures are not necessary reported in order.
	// We might not capture the closest fixture in ANY.
	const rayCast = (() => {
		let def = {
			ANY: 0,
			NEAREST: 2,
			ALL: 1
		};

		const reset = (mode, ignore) => {
			def.points = [];
			def.normals = [];
			def.fixtures = [];
			def.fractions = [];
			def.ignore = ignore || [];
			def.mode = mode == undefined ? def.NEAREST : mode;
		};

		def.rayCast = (point1, point2, mode, ignore) => {
			reset(mode, ignore);
			world.rayCast(scaleTo(point1), scaleTo(point2), def.callback);
		};

		def.callback = (fixture, point, normal, fraction) => {
			if (def.ignore.length > 0) for (let i = 0; i < def.ignore.length; i++) if (def.ignore[i] === fixture) return -1;
			if (def.mode == def.NEAREST && def.points.length == 1) {
				def.fixtures[0] = fixture;
				def.points[0] = scaleFrom(point);
				def.normals[0] = normal;
				def.fractions[0] = fraction;
			} else {
				def.fixtures.push(fixture);
				def.points.push(scaleFrom(point));
				def.normals.push(normal);
				def.fractions.push(fraction);
			}
			// -1 to ignore a fixture and continue
			//  0 to terminate on first hit, or for searching
			//  fraction seems to return nearest fixture as last entry in array
			// +1 returns multiple but mix of low high or high low
			return def.mode == def.NEAREST ? fraction : def.mode;
		};

		return def;
	})();

	const queryAABB = (() => {
		let def = {};
		function reset(search) {
			def.fixtures = [];
			def.search = search || [];
		}

		def.query = ({ lowerBound, upperBound }, search) => {
			reset(search);
			aabbc = new pl.AABB(lowerBound.mul(1 / plScale), upperBound.mul(1 / plScale));
			world.queryAABB(aabbc, def.callback);
		};

		def.callback = (fixture) => {
			def.fixtures.push(fixture);
			return true;
		};

		return def;
	})();

	/**
	 * Gets a color from a color palette
	 *
	 * @method colorPal
	 * @param {String} c A single character, a key found in the color palette object.
	 * @param {Number|Object} palette Can be a palette object or number index
	 * in the system's palettes array.
	 * @returns a hex color string for use by p5.js functions
	 */
	p5.prototype.colorPal = (c, palette) => {
		if (typeof palette == 'number') {
			palette = world.palettes[palette];
		}
		palette ??= world.palettes[0];
		c = palette[c];
		if (!c) return color(0, 0, 0, 0);
		return color(c);
	};

	/**
	 * Create pixel art images from a string. Each character in the
	 * input string represents a color value defined in the palette
	 * object.
	 *
	 * @method spriteArt
	 * @param {String} txt Each character represents a pixel color value
	 * @param {Number} scale The scale of the image
	 * @param {Number|Object} palette Color palette
	 * @returns A p5.js Image
	 *
	 * @example
	 * let str = `
	 * ...yyyy
	 * .yybyybyy
	 * yyyyyyyyyy
	 * yybyyyybyy
	 * .yybbbbyy
	 * ...yyyy`;
	 *
	 * let img = spriteArt(str);
	 */
	p5.prototype.spriteArt = (txt, scale, palette) => {
		scale ??= 1;
		if (typeof palette == 'number') {
			palette = world.palettes[palette];
		}
		palette ??= world.palettes[0];
		let lines = txt; // accepts 2D arrays of characters
		if (typeof txt == 'string') {
			txt = txt.replace(/^[\n\t]+|\s+$/g, ''); // trim newlines
			lines = txt.split('\n');
		}
		let w = 0;
		for (let line of lines) {
			if (line.length > w) w = line.length;
		}
		let h = lines.length;
		let img = createImage(w * scale, h * scale);
		img.loadPixels();

		for (let i = 0; i < lines.length; i++) {
			for (let j = 0; j < lines[i].length; j++) {
				for (let sX = 0; sX < scale; sX++) {
					for (let sY = 0; sY < scale; sY++) {
						let c = colorPal(lines[i][j], palette);
						img.set(j * scale + sX, i * scale + sY, c);
					}
				}
			}
		}
		img.updatePixels();
		return img; // return the p5 graphics object
	};

	/**
	 * This function is called automatically at the end of the p5.js draw
	 * loop unless it was already called in the draw loop.
	 *
	 * @method drawSprites
	 * @param {Group} group of sprites, allSprites by default
	 */
	p5.prototype.drawSprites = function (group) {
		group ??= this.allSprites;
		group.draw();
		this._p5play.autoDrawSprites = false;
	};

	/**
	 * Creates a new sprite.
	 *
	 * @returns {Sprite}
	 */
	p5.prototype.createSprite = function () {
		return new Sprite(...arguments);
	};

	/**
	 * Creates a new group of sprites.
	 *
	 * @returns {Group}
	 */
	p5.prototype.createGroup = function () {
		return new Group(...arguments);
	};

	/**
	 * Loads a Sprite Sheet.
	 * Use this in the preload p5.js function.
	 *
	 * @method loadSpriteSheet
	 * @returns {SpriteSheet}
	 */
	p5.prototype.loadSpriteSheet = function () {
		return new SpriteSheet(...arguments);
	};

	/**
	 * Loads an animation.
	 * Use this in the preload p5.js function.
	 *
	 * @method loadAnimation
	 * @returns {SpriteAnimation}
	 */
	p5.prototype.loadAnimation = function () {
		return new SpriteAnimation(...arguments);
	};

	/**
	 * TODO fix this function
	 *
	 * @param {p5.Image} spriteSheetImg
	 * @param {Number|Array} size
	 * @param {Array} pos
	 * @param {Number} frameCount
	 * @param {Number} frameDelay
	 * @returns a SpriteAnimation
	 */
	p5.prototype.createAni = function (spriteSheetImg, size, pos, frameCount, frameDelay) {
		let w, h;
		if (typeof size == 'number') {
			w = size;
			h = size;
		} else {
			w = size[0];
			h = size[1];
		}

		// add all the frames in the animation to the frames array
		let frames = [];
		frameCount ??= 1; // set frameCount to 1 by default
		for (let i = 0; i < frameCount; i++) {
			let x, y;
			// if pos is a number, that means it's just a line number
			// and the animation's first frame is at x = 0
			// the line number is the location of the animation line
			// given as a distance in tiles from the top of the image
			if (typeof pos == 'number') {
				x = w * i;
				y = h * pos;
			} else {
				// pos is the location of the animation line
				// given as a [row,column] coordinate pair of distances in tiles
				// from the top left corner of the image
				x = w * (i + pos[1]); // column
				y = h * pos[0]; // row
			}

			frames.push({
				frame: { x: x, y: y, width: w, height: h }
			});
		}
		let ani = loadAnimation(loadSpriteSheet(spriteSheetImg, frames));
		if (typeof frameDelay != 'undefined') ani.frameDelay = frameDelay;
		return ani;
	};

	/**
	 * Displays an animation. Similar to the p5.js image function.
	 *
	 * @method animation
	 * @param {SpriteAnimation} anim Animation to be displayed
	 * @param {Number} x X coordinate
	 * @param {Number} y Y coordinate
	 *
	 */
	p5.prototype.animation = function (anim, x, y) {
		anim.draw(x, y);
	};

	/**
	 * Delay
	 *
	 * @param {Number} millisecond
	 * @returns {Promise} A Promise that fulfills after the specified time.
	 *
	 * @example
	 * async function startGame() {
	 *   await delay(3000);
	 * }
	 */
	p5.prototype.delay = (millisecond) => {
		// if no input arg given, delay waits for the next possible animation frame
		if (!millisecond) {
			return new Promise(requestAnimationFrame);
		} else {
			// else it wraps setTimeout in a Promise
			return new Promise((resolve) => {
				setTimeout(resolve, millisecond);
			});
		}
	};

	/**
	 * Delay
	 *
	 * @param {Number} millisecond
	 * @returns {Promise} A Promise that fulfills after the specified time.
	 *
	 * @example
	 * async function startGame() {
	 *   await sleep(3000);
	 * }
	 */
	p5.prototype.sleep = (millisecond) => {
		return this.delay(millisecond);
	};

	/**
	 * Awaitable function for playing sounds.
	 *
	 * @method play
	 * @param {p5.Sound} sound
	 * @returns {Promise}
	 */
	p5.prototype.play = (sound) => {
		// TODO reject if sound not found
		return new Promise((resolve, reject) => {
			sound.play();
			sound.onended(() => {
				resolve();
			});
		});
	};

	let keyCodes = {
		_: 189,
		'-': 189,
		',': 188,
		';': 188,
		':': 190,
		'!': 49,
		'?': 219,
		'.': 190,
		'"': 50,
		'(': 56,
		')': 57,
		'§': 51,
		'*': 187,
		'/': 55,
		'&': 54,
		'#': 191,
		'%': 53,
		'°': 220,
		'+': 187,
		'=': 48,
		"'": 191,
		$: 52,
		Alt: 18,
		ArrowUp: 38,
		ArrowDown: 40,
		ArrowLeft: 37,
		ArrowRight: 39,
		CapsLock: 20,
		Clear: 12,
		Control: 17,
		Delete: 46,
		Escape: 27,
		Insert: 45,
		PageDown: 34,
		PageUp: 33,
		Shift: 16,
		Tab: 9
	};

	/**
	 * Get the keyCode of a key
	 *
	 * @method getKeyCode
	 * @param {string} keyName
	 * @returns {number} keyCode
	 */
	function getKeyCode(keyName) {
		if (typeof keyName != 'string') return keyName;
		let code = keyCodes[keyName];
		if (code) return code;
		return keyName.toUpperCase().charCodeAt(0);
	}

	let userDisabledP5Errors = p5.disableFriendlyErrors;
	p5.disableFriendlyErrors = true;

	// keyIsDown is a p5.js function
	let _keyIsDown = this.keyIsDown;

	/**
	 * Check if key is down.
	 *
	 * @method keyIsDown
	 * @param {string} keyName
	 * @returns {boolean} true if key is down
	 */
	this.keyIsDown = function (keyName) {
		return _keyIsDown.call(pInst, getKeyCode(keyName));
	};

	/**
	 * Use keyIsDown instead.
	 *
	 * @deprecated
	 */
	this.isKeyDown = function (keyName) {
		return pInst.keyIsDown(keyName);
	};

	/**
	 * Use keyIsDown instead.
	 *
	 * @deprecated
	 */
	this.keyDown = function (keyName) {
		return pInst.keyIsDown(keyName);
	};

	const _createCanvas = this.createCanvas;

	this.createCanvas = function () {
		let args = [...arguments];
		if (args.length < 2) {
			args[0] = 100;
			args[1] = 100;
		}
		if (args.length < 3) {
			args[2] = 'p2d';
		}
		_createCanvas.call(pInst, ...args);
		if (world) world.resize();
		if (!userDisabledP5Errors) p5.disableFriendlyErrors = false;
	};

	const _background = this.background;

	/**
	 * Just like the p5.js background function except it also accepts
	 * a color pallette code.
	 *
	 * @method background
	 */
	this.background = function () {
		let args = arguments;

		if (typeof args[0] == 'string' && args[0].length == 1) {
			_background.call(this, colorPal(args[0]));
		} else {
			_background.call(this, ...args);
		}
	};

	const _fill = this.fill;

	/**
	 * Just like the p5.js fill function except it also accepts
	 * a color pallette code.
	 *
	 * @method fill
	 */
	this.fill = function () {
		let args = arguments;

		if (typeof args[0] == 'string' && args[0].length == 1) {
			_fill.call(this, colorPal(args[0]));
		} else {
			_fill.call(this, ...args);
		}
	};

	const _stroke = this.stroke;

	/**
	 * Just like the p5.js stroke function except it also accepts
	 * a color pallette code.
	 *
	 * @method stroke
	 */
	this.stroke = function () {
		let args = arguments;

		if (typeof args[0] == 'string' && args[0].length == 1) {
			_stroke.call(this, colorPal(args[0]));
		} else {
			_stroke.call(this, ...args);
		}
	};

	this.Sprite = Sprite;
	this.SpriteAnimation = SpriteAnimation;
	this.SpriteSheet = SpriteSheet;
	this.Group = Group;
	this.World = World;

	/**
	 * A group of all the sprites.
	 *
	 * @property allSprites
	 */
	this.allSprites = new Group();
	this.allSprites._isAllSpritesGroup = true;
	this.allSprites.shouldCull = true;
});

p5.prototype.registerMethod('post', function p5playPostDraw() {
	this.centerX ??= this.width * 0.5;
	this.centerY ??= this.height * 0.5;

	if (!this.allSprites.length) return;

	if (this._p5play.autoDrawSprites) {
		this.drawSprites();
		this._p5play.autoDrawSprites = true;
	}

	if (this._p5play.autoUpdateSprites) {
		this.updateSprites();
		this._p5play.autoUpdateSprites = true;
	}
});