A lightweight JavaScript game engine
git clone git://soophie.de/game-engine
1
import { Shape } from './shape.js';3
export function collideRect(a, b) {4
return a.x < b.x + b.shape.width && a.x + a.shape.width > b.x5
&& a.y < b.y + b.shape.height && a.y + a.shape.height > b.y;6
}8
export function collideCircle(a, b) {9
const dx = Math.abs(a.x - b.x);10
const dy = Math.abs(a.y - b.y);11
return Math.sqrt(dx ** 2 + dy ** 2) < a.shape.radius + b.shape.radius;12
}14
export function collideRectAndCircle(a, b) {15
const [rect, circle] = a instanceof Shape.Rect ? [a, b] : [b, a];16
let x = circle.x;17
let y = circle.y;18
if(circle.x < rect.x) {19
x = rect.x;20
}21
else if(circle.x > rect.x + rect.shape.width) {22
x = rect.x + rect.shape.width;23
}24
if(circle.y < rect.y) {25
y = rect.y;26
}27
else if(circle.y > rect.y + rect.shape.height) {28
y = rect.y + rect.shape.height;29
}30
const distance = Math.sqrt((circle.x - x) ** 2 + (circle.y - y) ** 2);31
return distance <= circle.shape.radius;32
}34
export function collidePoint(object, point) {35
if(object.shape instanceof Shape.Rect) {36
const {x, y, shape: {width, height}} = object;37
return x < point.x && x + width > point.x && y < point.y && y + height > point.y;38
}39
if(object.shape instanceof Shape.Circle) {40
const {x, y, shape: {radius}} = object;41
return Math.sqrt((x - point.x) ** 2 + (y - point.y) ** 2) < radius;42
}43
}
1
import { scaleCanvas } from './util.js';1
import { scaleCanvas } from './util/canvas.js';3
export class Game {4
constructor(width, height, { fps } = {}) {
10
scaleCanvas(this.canvas, this.width, this.height);11
this.g = this.canvas.getContext('2d');12
this.events = [];13
this.sprites = [];14
}14
start() {15
async start() {16
for(const sprite of this.sprites) {17
await sprite.load();18
}19
requestAnimationFrame(this.loop.bind(this));20
this.update();21
this.render(this.g);
50
bindEvent(element, type) {51
element['on' + type] = event => this.events.push(event);52
}53
loadSprite(sprite) {54
this.sprites.push(sprite);55
}56
}
2
export * from './group.js';3
export * from './surface.js';4
export * from './shape.js';5
export * from './collision.js';6
export * from './util/canvas.js';7
export * from './util/collision.js';8
export * from './util/timer.js';9
export * from './util/animation.js';11
export * from './sprite/sprite.js';12
export * from './sprite/spritesheet.js';
1
const cache = new Map();3
export default class Sprite {4
constructor(path) {5
this.path = path;6
this.image = new Image();7
}8
load() {9
return new Promise(async (resolve, reject) => {10
this.image.onload = () => {11
if(!cache.has(this.path)) {12
cache.set(this.path, this.image.src);13
}14
resolve();15
};16
this.image.onerror = () => {17
reject(new Error(`Unable to load sprite '${this.path}'!`));18
}19
if(cache.has(this.path)) {20
this.image.src = cache.get(this.path);21
}22
else {23
if(this.path.startsWith('data:')) {24
this.image.src = this.path;25
}26
else {27
try {28
const response = await fetch(this.path);29
if(response.status !== 200) {30
reject(new Error(`Invalid response code ${response.status} for sprite '${this.path}'!`));31
}32
const blob = await response.blob();33
const url = URL.createObjectURL(blob);34
this.image.src = url;35
}36
catch(error) {37
reject(error);38
}39
}40
}41
});42
}43
}
1
import Sprite from './sprite.js';2
import { scaleCanvas } from 'https://git.typable.dev/std/js/canvas.js';4
export default class Spritesheet extends Sprite {5
constructor(path, size) {6
super(path);7
this.size = size;8
this.sprites = [];9
}10
async load() {11
await super.load();12
const {width, height} = this.image;13
if(width % this.size !== 0 || height % this.size !== 0) {14
throw new Error(`Invalid size ${this.size} for spritesheet '${this.path}'!`);15
}16
this.sprites = new Array(parseInt(width / this.size) * parseInt(height / this.size));17
const canvas = document.createElement('canvas');18
scaleCanvas(canvas, this.size, this.size);19
const g = canvas.getContext('2d');20
g.imageSmoothingEnabled = false;21
for(let i = 0; i < this.sprites.length; i++) {22
const y = parseInt(i / (width / this.size));23
const x = parseInt(i % (width / this.size));24
g.clearRect(0, 0, this.size, this.size);25
g.drawImage(this.image, -x * this.size, -y * this.size, width, height);26
const sprite = new Sprite(canvas.toDataURL('image/png'));27
await sprite.load();28
this.sprites[i] = sprite;29
}30
}31
all() {32
return this.sprites;33
}34
}
1
const DEVICE_PIXEL_RATIO = window.devicePixelRatio || 1;3
export function scaleCanvas(canvas, width, height) {4
const g = canvas.getContext('2d');5
const backingStoreRatio = g.webkitBackingStorePixelRatio || 1;6
const ratio = DEVICE_PIXEL_RATIO / backingStoreRatio;7
if(DEVICE_PIXEL_RATIO != backingStoreRatio) {8
canvas.width = width * ratio;9
canvas.height = height * ratio;10
canvas.style.width = width + 'px';11
canvas.style.height = height + 'px';12
}13
else {14
canvas.width = width;15
canvas.height = height;16
canvas.style.width = '';17
canvas.style.height = '';18
}19
g.scale(ratio, ratio);20
}
1
import Timer from './timer.js';3
export default class Animation {4
constructor(size, step) {5
this.size = size;6
this.timer = new Timer(step);7
this.frame = 0;8
this.offset = 0;9
}10
count() {11
if(this.timer.count()) {12
if(this.frame === this.size - 1) {13
this.frame = 0;14
}15
else {16
this.frame++;17
}18
}19
}20
reset() {21
this.timer.reset();22
this.frame = 0;23
}24
get(sprites) {25
return sprites[this.offset + this.frame];26
}27
}
1
const DEVICE_PIXEL_RATIO = window.devicePixelRatio || 1;3
export function scaleCanvas(canvas, width, height) {4
const g = canvas.getContext('2d');5
const backingStoreRatio = g.webkitBackingStorePixelRatio || 1;6
const ratio = DEVICE_PIXEL_RATIO / backingStoreRatio;7
if(DEVICE_PIXEL_RATIO != backingStoreRatio) {8
canvas.width = width * ratio;9
canvas.height = height * ratio;10
canvas.style.width = width + 'px';11
canvas.style.height = height + 'px';12
}13
else {14
canvas.width = width;15
canvas.height = height;16
canvas.style.width = '';17
canvas.style.height = '';18
}19
g.scale(ratio, ratio);20
}
1
import { Shape } from '../shape.js';3
export function collideRect(a, b) {4
return a.x < b.x + b.shape.width && a.x + a.shape.width > b.x5
&& a.y < b.y + b.shape.height && a.y + a.shape.height > b.y;6
}8
export function collideCircle(a, b) {9
const dx = Math.abs(a.x - b.x);10
const dy = Math.abs(a.y - b.y);11
return Math.sqrt(dx ** 2 + dy ** 2) < a.shape.radius + b.shape.radius;12
}14
export function collideRectAndCircle(a, b) {15
const [rect, circle] = a instanceof Shape.Rect ? [a, b] : [b, a];16
let x = circle.x;17
let y = circle.y;18
if(circle.x < rect.x) {19
x = rect.x;20
}21
else if(circle.x > rect.x + rect.shape.width) {22
x = rect.x + rect.shape.width;23
}24
if(circle.y < rect.y) {25
y = rect.y;26
}27
else if(circle.y > rect.y + rect.shape.height) {28
y = rect.y + rect.shape.height;29
}30
const distance = Math.sqrt((circle.x - x) ** 2 + (circle.y - y) ** 2);31
return distance <= circle.shape.radius;32
}34
export function collidePoint(object, point) {35
if(object.shape instanceof Shape.Rect) {36
const {x, y, shape: {width, height}} = object;37
return x < point.x && x + width > point.x && y < point.y && y + height > point.y;38
}39
if(object.shape instanceof Shape.Circle) {40
const {x, y, shape: {radius}} = object;41
return Math.sqrt((x - point.x) ** 2 + (y - point.y) ** 2) < radius;42
}43
}
1
export default class Timer {2
constructor(step) {3
this.time = 0;4
this.step = step || 1;5
}6
count() {7
if(this.time >= 1) {8
this.time = 0;9
return true;10
}11
this.time += 1 / this.step;12
return false;13
}14
reset() {15
this.time = 0;16
}17
}