@dothq/littlejsengine
Version:
LittleJS - Tiny and Fast HTML5 Game Engine
1,008 lines (1,006 loc) • 127 kB
JavaScript
/*
LittleJS - The Tiny JavaScript Game Engine That Can
MIT License - Copyright 2019 Frank Force
*/
/*
LittleJS Debug System
Debug Features
- debug overlay with mouse pick
- debug primitive rendering
- save screenshots
*/
'use strict';
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __spreadArray = (this && this.__spreadArray) || function (to, from) {
for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
to[j] = from[i];
return to;
};
var debug = 1;
var enableAsserts = 1;
var debugPointSize = .5;
var showWatermark = 1;
var godMode = 0;
var debugPrimitives = [];
var debugOverlay = 0;
var debugPhysics = 0;
var debugRaycast = 0;
var debugParticles = 0;
var debugGamepads = 0;
var debugMedals = 0;
var debugCanvas = -1;
var debugTakeScreenshot;
var downloadLink;
// debug helper functions
var ASSERT = enableAsserts ? function () {
var assert = [];
for (var _i = 0; _i < arguments.length; _i++) {
assert[_i] = arguments[_i];
}
return console.assert.apply(console, assert);
} : function () { };
var debugRect = function (pos, size, color, time, angle, fill) {
if (size === void 0) { size = vec2(0); }
if (color === void 0) { color = '#fff'; }
if (time === void 0) { time = 0; }
if (angle === void 0) { angle = 0; }
if (fill === void 0) { fill = 0; }
ASSERT(typeof color == 'string'); // pass in regular html strings as colors
debugPrimitives.push({ pos: pos, size: vec2(size), color: color, time: new Timer(time), angle: angle, fill: fill });
};
var debugCircle = function (pos, radius, color, time, fill) {
if (radius === void 0) { radius = 0; }
if (color === void 0) { color = '#fff'; }
if (time === void 0) { time = 0; }
if (fill === void 0) { fill = 0; }
ASSERT(typeof color == 'string'); // pass in regular html strings as colors
debugPrimitives.push({ pos: pos, size: radius, color: color, time: new Timer(time), angle: 0, fill: fill });
};
var debugPoint = function (pos, color, time, angle) { return debugRect(pos, 0, color, time, angle); };
var debugLine = function (posA, posB, color, thickness, time) {
if (thickness === void 0) { thickness = .1; }
var halfDelta = vec2((posB.x - posA.x) / 2, (posB.y - posA.y) / 2);
var size = vec2(thickness, halfDelta.length() * 2);
debugRect(posA.add(halfDelta), size, color, time, halfDelta.angle(), 1);
};
var debugAABB = function (pA, pB, sA, sB, color) {
var minPos = vec2(min(pA.x - sA.x / 2, pB.x - sB.x / 2), min(pA.y - sA.y / 2, pB.y - sB.y / 2));
var maxPos = vec2(max(pA.x + sA.x / 2, pB.x + sB.x / 2), max(pA.y + sA.y / 2, pB.y + sB.y / 2));
debugRect(minPos.lerp(maxPos, .5), maxPos.subtract(minPos), color);
};
var debugText = function (text, pos, size, color, time, angle, font) {
if (text === void 0) { text = ''; }
if (size === void 0) { size = 1; }
if (color === void 0) { color = '#fff'; }
if (time === void 0) { time = 0; }
if (angle === void 0) { angle = 0; }
if (font === void 0) { font = 'monospace'; }
ASSERT(typeof color == 'string'); // pass in regular html strings as colors
debugPrimitives.push({ text: text, pos: pos, size: size, color: color, time: new Timer(time), angle: angle, font: font });
};
var debugClear = function () { return debugPrimitives = []; };
// save a canvas to disk
var debugSaveCanvas = function (canvas, filename) {
if (filename === void 0) { filename = engineName + '.png'; }
downloadLink.download = 'screenshot.png';
downloadLink.href = canvas.toDataURL('image/png').replace('image/png', 'image/octet-stream');
downloadLink.click();
};
///////////////////////////////////////////////////////////////////////////////
// engine debug function (called automatically)
var debugInit = function () {
// create link for saving screenshots
document.body.appendChild(downloadLink = document.createElement('a'));
downloadLink.style.display = 'none';
};
var debugUpdate = function () {
if (!debug)
return;
if (keyWasPressed(192)) // ~
debugOverlay = !debugOverlay;
if (keyWasPressed(49)) // 1
debugPhysics = !debugPhysics, debugParticles = 0;
if (keyWasPressed(50)) // 2
debugParticles = !debugParticles, debugPhysics = 0;
if (keyWasPressed(51)) // 3
godMode = !godMode;
if (keyWasPressed(53)) // 5
debugTakeScreenshot = 1;
//if (keyWasPressed(54)) // 6
// debugToggleParticleEditor();
if (keyWasPressed(55)) // 7
debugGamepads = !debugGamepads;
//if (keyWasPressed(56)) // 8
//if (keyWasPressed(57)) // 9
if (keyWasPressed(48)) // 0
showWatermark = !showWatermark;
};
var debugRender = function () {
glCopyToContext(mainContext);
if (debugTakeScreenshot) {
// composite canvas
glCopyToContext(mainContext, 1);
mainContext.drawImage(overlayCanvas, 0, 0);
overlayCanvas.width |= 0;
debugSaveCanvas(mainCanvas);
debugTakeScreenshot = 0;
}
if (debugGamepads && gamepadsEnable && navigator.getGamepads) {
// poll gamepads
var gamepads = navigator.getGamepads();
for (var i = gamepads.length; i--;) {
var gamepad = gamepads[i];
if (gamepad) {
// gamepad debug display
var stickScale = 1;
var buttonScale = .2;
var centerPos = cameraPos;
var sticks = stickData[i];
for (var j = sticks.length; j--;) {
var drawPos = centerPos.add(vec2(j * stickScale * 2, i * stickScale * 3));
var stickPos = drawPos.add(sticks[j].scale(stickScale));
debugCircle(drawPos, stickScale, '#fff7', 0, 1);
debugLine(drawPos, stickPos, '#f00');
debugPoint(stickPos, '#f00');
}
for (var j = gamepad.buttons.length; j--;) {
var drawPos = centerPos.add(vec2(j * buttonScale * 2, i * stickScale * 3 - stickScale - buttonScale));
var pressed = gamepad.buttons[j].pressed;
debugCircle(drawPos, buttonScale, pressed ? '#f00' : '#fff7', 0, 1);
debugText(j, drawPos, .2);
}
}
}
}
if (debugOverlay) {
for (var _i = 0, engineObjects_1 = engineObjects; _i < engineObjects_1.length; _i++) {
var o = engineObjects_1[_i];
if (o.canvas)
continue; // skip tile layers
var size = o.size.copy();
size.x = max(size.x, .2);
size.y = max(size.y, .2);
var color = new Color(o.collideTiles ? 1 : 0, o.collideSolidObjects ? 1 : 0, o.isSolid ? 1 : 0, o.parent ? .2 : .5);
// show object info
drawRect(o.pos, size, color);
drawRect(o.pos, size.scale(.8), o.parent ? new Color(1, 1, 1, .5) : new Color(0, 0, 0, .8));
o.parent && drawLine(o.pos, o.parent.pos, .1, new Color(0, 0, 1, .5));
}
// mouse pick
var bestDistance = Infinity, bestObject = void 0;
for (var _a = 0, engineObjects_2 = engineObjects; _a < engineObjects_2.length; _a++) {
var o = engineObjects_2[_a];
var distance = mousePos.distanceSquared(o.pos);
if (distance < bestDistance) {
bestDistance = distance;
bestObject = o;
}
}
if (bestObject) {
var saveContext = mainContext;
mainContext = overlayContext;
var raycastHitPos = tileCollisionRaycast(bestObject.pos, mousePos);
raycastHitPos && drawRect(raycastHitPos.int().add(vec2(.5)), vec2(1), new Color(0, 1, 1, .3));
drawRect(mousePos.int().add(vec2(.5)), vec2(1), new Color(0, 0, 1, .5));
drawLine(mousePos, bestObject.pos, .1, !raycastHitPos ? new Color(0, 1, 0, .5) : new Color(1, 0, 0, .5));
var pos = mousePos.copy(), height = vec2(0, .5);
var printVec2 = function (v) { return '(' + (v.x > 0 ? ' ' : '') + (v.x).toFixed(2) + ',' + (v.y > 0 ? ' ' : '') + (v.y).toFixed(2) + ')'; };
var args = [.5, new Color, .05, undefined, undefined, 'monospace'];
drawText.apply(void 0, __spreadArray(['pos = ' + printVec2(bestObject.pos)
+ (bestObject.angle > 0 ? ' ' : ' ') + (bestObject.angle * 180 / PI).toFixed(1) + '°',
pos = pos.add(height)], args));
drawText.apply(void 0, __spreadArray(['vel = ' + printVec2(bestObject.velocity), pos = pos.add(height)], args));
drawText.apply(void 0, __spreadArray(['size = ' + printVec2(bestObject.size), pos = pos.add(height)], args));
drawText.apply(void 0, __spreadArray(['collision = ' + getTileCollisionData(mousePos), pos = mousePos.subtract(height)], args));
mainContext = saveContext;
}
glCopyToContext(mainContext);
}
{
// render debug rects
overlayContext.lineWidth = 1;
var pointSize_1 = debugPointSize * cameraScale;
debugPrimitives.forEach(function (p) {
// create canvas transform from world space to screen space
var pos = worldToScreen(p.pos);
overlayContext.save();
overlayContext.lineWidth = 2;
overlayContext.translate(pos.x | 0, pos.y | 0);
overlayContext.rotate(p.angle);
overlayContext.fillStyle = overlayContext.strokeStyle = p.color;
if (p.text != undefined) {
overlayContext.font = p.size * cameraScale + 'px ' + p.font;
overlayContext.textAlign = 'center';
overlayContext.textBaseline = 'middle';
overlayContext.fillText(p.text, 0, 0);
}
else if (p.size == 0 || p.size.x === 0 && p.size.y === 0) {
// point
overlayContext.fillRect(-pointSize_1 / 2, -1, pointSize_1, 3);
overlayContext.fillRect(-1, -pointSize_1 / 2, 3, pointSize_1);
}
else if (p.size.x != undefined) {
// rect
var w = p.size.x * cameraScale | 0, h = p.size.y * cameraScale | 0;
p.fill && overlayContext.fillRect(-w / 2 | 0, -h / 2 | 0, w, h);
overlayContext.strokeRect(-w / 2 | 0, -h / 2 | 0, w, h);
}
else {
// circle
overlayContext.beginPath();
overlayContext.arc(0, 0, p.size * cameraScale, 0, 9);
p.fill && overlayContext.fill();
overlayContext.stroke();
}
overlayContext.restore();
});
overlayContext.fillStyle = overlayContext.strokeStyle = '#fff';
}
{
var x = 9, y = -20, h = 30;
overlayContext.fillStyle = '#fff';
overlayContext.textAlign = 'left';
overlayContext.textBaseline = 'top';
overlayContext.font = '28px monospace';
overlayContext.shadowColor = '#000';
overlayContext.shadowBlur = 9;
if (debugOverlay) {
overlayContext.fillText(engineName, x, y += h);
overlayContext.fillText('Objects: ' + engineObjects.length, x, y += h);
overlayContext.fillText('Time: ' + formatTime(time), x, y += h);
overlayContext.fillText('---------', x, y += h);
overlayContext.fillStyle = '#f00';
overlayContext.fillText('~: Debug Overlay', x, y += h);
overlayContext.fillStyle = debugPhysics ? '#f00' : '#fff';
overlayContext.fillText('1: Debug Physics', x, y += h);
overlayContext.fillStyle = debugParticles ? '#f00' : '#fff';
overlayContext.fillText('2: Debug Particles', x, y += h);
overlayContext.fillStyle = godMode ? '#f00' : '#fff';
overlayContext.fillText('3: God Mode', x, y += h);
overlayContext.fillStyle = '#fff';
overlayContext.fillText('5: Save Screenshot', x, y += h);
//overlayContext.fillStyle = debugParticleEditor ? '#f00' : '#fff';
//overlayContext.fillText('6: Particle Editor', x, y += h);
overlayContext.fillStyle = debugGamepads ? '#f00' : '#fff';
overlayContext.fillText('7: Debug Gamepads', x, y += h);
}
else {
overlayContext.fillText(debugPhysics ? 'Debug Physics' : '', x, y += h);
overlayContext.fillText(debugParticles ? 'Debug Particles' : '', x, y += h);
overlayContext.fillText(godMode ? 'God Mode' : '', x, y += h);
overlayContext.fillText(debugGamepads ? 'Debug Gamepads' : '', x, y += h);
}
overlayContext.shadowBlur = 0;
}
debugPrimitives = debugPrimitives.filter(function (r) { return r.time.get() < 0; });
};
///////////////////////////////////////////////////////////////////////////////
// particle system editor
var debugParticleEditor = 0, debugParticleSystem, debugParticleSystemDiv, particleSystemCode;
var debugToggleParticleEditor = function () {
debugParticleEditor = !debugParticleEditor;
if (debugParticleEditor) {
if (!debugParticleSystem || debugParticleSystem.destroyed)
debugParticleSystem = new ParticleEmitter(cameraPos);
}
else if (debugParticleSystem && !debugParticleSystem.destroyed)
debugParticleSystem.destroy();
var colorToHex = function (color) {
var componentToHex = function (c) {
var hex = (c * 255 | 0).toString(16);
return hex.length == 1 ? '0' + hex : hex;
};
return '#' + componentToHex(color.r) + componentToHex(color.g) + componentToHex(color.b);
};
var hexToColor = function (hex) {
return new Color(parseInt(hex.substr(1, 2), 16) / 255, parseInt(hex.substr(3, 2), 16) / 255, parseInt(hex.substr(5, 2), 16) / 255);
};
if (!debugParticleSystemDiv) {
var div = debugParticleSystemDiv = document.createElement('div');
div.innerHTML = '<big><b>Particle Editor';
div.style = 'position:absolute;top:10;left:10;color:#fff';
document.body.appendChild(div);
var _loop_1 = function (setting) {
var input = setting[2] = document.createElement('input');
var name_1 = setting[0];
var type = setting[1];
if (type) {
if (type == 'color') {
input.type = type;
var color = debugParticleSystem[name_1];
input.value = colorToHex(color);
}
else if (type == 'alpha' && name_1 == 'colorStartAlpha')
input.value = debugParticleSystem.colorStartA.a;
else if (type == 'alpha' && name_1 == 'colorEndAlpha')
input.value = debugParticleSystem.colorEndA.a;
else if (name_1 == 'tileSizeX')
input.value = debugParticleSystem.tileSize.x;
else if (name_1 == 'tileSizeY')
input.value = debugParticleSystem.tileSize.y;
}
else
input.value = debugParticleSystem[name_1] || '0';
input.oninput = function (e) {
var inputFloat = parseFloat(input.value) || 0;
if (type) {
if (type == 'color') {
var color = hexToColor(input.value);
debugParticleSystem[name_1].r = color.r;
debugParticleSystem[name_1].g = color.g;
debugParticleSystem[name_1].b = color.b;
}
else if (type == 'alpha' && name_1 == 'colorStartAlpha') {
debugParticleSystem.colorStartA.a = clamp(inputFloat);
debugParticleSystem.colorStartB.a = clamp(inputFloat);
}
else if (type == 'alpha' && name_1 == 'colorEndAlpha') {
debugParticleSystem.colorEndA.a = clamp(inputFloat);
debugParticleSystem.colorEndB.a = clamp(inputFloat);
}
else if (name_1 == 'tileSizeX') {
debugParticleSystem.tileSize = vec2(parseInt(input.value), debugParticleSystem.tileSize.y);
}
else if (name_1 == 'tileSizeY') {
debugParticleSystem.tileSize.y = vec2(debugParticleSystem.tileSize.x, parseInt(input.value));
}
}
else
debugParticleSystem[name_1] = inputFloat;
updateCode_1();
};
div.appendChild(document.createElement('br'));
div.appendChild(input);
div.appendChild(document.createTextNode(' ' + name_1));
};
for (var _i = 0, debugParticleSettings_1 = debugParticleSettings; _i < debugParticleSettings_1.length; _i++) {
var setting = debugParticleSettings_1[_i];
_loop_1(setting);
}
div.appendChild(document.createElement('br'));
div.appendChild(document.createElement('br'));
div.appendChild(particleSystemCode = document.createElement('input'));
particleSystemCode.disabled = true;
div.appendChild(document.createTextNode(' code'));
div.appendChild(document.createElement('br'));
var button = document.createElement('button');
div.appendChild(button);
button.innerHTML = 'Copy To Clipboard';
button.onclick = function (e) { return navigator.clipboard.writeText(particleSystemCode.value); };
var updateCode_1 = function () {
var code = '';
var count = 0;
for (var _i = 0, debugParticleSettings_2 = debugParticleSettings; _i < debugParticleSettings_2.length; _i++) {
var setting = debugParticleSettings_2[_i];
var name_2 = setting[0];
var type = setting[1];
var value = void 0;
if (name_2 == 'tileSizeX' || type == 'alpha')
continue;
if (count++)
code += ', ';
if (name_2 == 'tileSizeY') {
value = "vec2(" + debugParticleSystem.tileSize.x + "," + debugParticleSystem.tileSize.y + ")";
}
else if (type == 'color') {
var c = debugParticleSystem[name_2];
value = "new Color(" + c.r + "," + c.g + "," + c.b + "," + c.a + ")";
}
else
value = debugParticleSystem[name_2];
code += value;
}
particleSystemCode.value = '...[' + code + ']';
};
updateCode_1();
}
debugParticleSystemDiv.style.display = debugParticleEditor ? '' : 'none';
};
var debugParticleSettings = [
['emitSize'],
['emitTime'],
['emitRate'],
['emitConeAngle'],
['tileIndex'],
['tileSizeX', 'tileSize'],
['tileSizeY', 'tileSize'],
['colorStartA', 'color'],
['colorStartB', 'color'],
['colorStartAlpha', 'alpha'],
['colorEndA', 'color'],
['colorEndB', 'color'],
['colorEndAlpha', 'alpha'],
['particleTime'],
['sizeStart'],
['sizeEnd'],
['speed'],
['angleSpeed'],
['damping'],
['angleDamping'],
['gravityScale'],
['particleConeAngle'],
['fadeRate'],
['randomness'],
['collideTiles'],
['additive'],
['randomColorComponents'],
['renderOrder'],
];
/*
LittleJS Utility Classes and Functions
- Vector2 - fast, simple, easy vector class
- Color - holds a rgba color with math functions
- Timer - tracks time automatically
- Small math lib
*/
'use strict';
///////////////////////////////////////////////////////////////////////////////
// helper functions
var PI = Math.PI;
var isChrome = window['chrome'];
var abs = function (a) { return a < 0 ? -a : a; };
var sign = function (a) { return a < 0 ? -1 : 1; };
var min = function (a, b) { return a < b ? a : b; };
var max = function (a, b) { return a > b ? a : b; };
var mod = function (a, b) { return ((a % b) + b) % b; };
var clamp = function (v, max, min) {
if (max === void 0) { max = 1; }
if (min === void 0) { min = 0; }
return (ASSERT(max > min), v < min ? min : v > max ? max : v);
};
var percent = function (v, max, min) {
if (max === void 0) { max = 1; }
if (min === void 0) { min = 0; }
return max - min ? clamp((v - min) / (max - min)) : 0;
};
var lerp = function (p, max, min) {
if (max === void 0) { max = 1; }
if (min === void 0) { min = 0; }
return min + clamp(p) * (max - min);
};
var formatTime = function (t) { return (t / 60 | 0) + ':' + (t % 60 < 10 ? '0' : '') + (t % 60 | 0); };
var isOverlapping = function (pA, sA, pB, sB) { return abs(pA.x - pB.x) * 2 < sA.x + sB.x & abs(pA.y - pB.y) * 2 < sA.y + sB.y; };
var nearestPowerOfTwo = function (v) { return Math.pow(2, Math.ceil(Math.log2(v))); };
var wave = function (f, a, t) {
if (f === void 0) { f = 1; }
if (a === void 0) { a = 1; }
if (t === void 0) { t = time; }
return a / 2 * (1 - Math.cos(t * f * 2 * PI));
};
var smoothStep = function (p) { return p * p * (3 - 2 * p); };
// random functions
var rand = function (a, b) {
if (a === void 0) { a = 1; }
if (b === void 0) { b = 0; }
return b + (a - b) * Math.random();
};
var randInt = function (a, b) {
if (a === void 0) { a = 1; }
if (b === void 0) { b = 0; }
return rand(a, b) | 0;
};
var randSign = function () { return (rand(2) | 0) * 2 - 1; };
var randInCircle = function (radius, minRadius) {
if (radius === void 0) { radius = 1; }
if (minRadius === void 0) { minRadius = 0; }
return radius > 0 ? randVector(radius * Math.pow(rand(minRadius / radius, 1), .5)) : new Vector2;
};
var randVector = function (length) {
if (length === void 0) { length = 1; }
return new Vector2().setAngle(rand(2 * PI), length);
};
var randColor = function (cA, cB, linear) {
if (cA === void 0) { cA = new Color; }
if (cB === void 0) { cB = new Color(0, 0, 0, 1); }
return linear ? cA.lerp(cB, rand()) : new Color(rand(cA.r, cB.r), rand(cA.g, cB.g), rand(cA.b, cB.b), rand(cA.a, cB.a));
};
// seeded random numbers using xorshift
var randSeed = 1;
var randSeeded = function (a, b) {
if (a === void 0) { a = 1; }
if (b === void 0) { b = 0; }
randSeed ^= randSeed << 13;
randSeed ^= randSeed >>> 17;
randSeed ^= randSeed << 5;
return b + (a - b) * abs(randSeed % 1e9) / 1e9;
};
// create a 2d vector, can take another Vector2 to copy, 2 scalars, or 1 scalar
var vec2 = function (x, y) {
if (x === void 0) { x = 0; }
return x.x == undefined ? new Vector2(x, y == undefined ? x : y) : new Vector2(x.x, x.y);
};
///////////////////////////////////////////////////////////////////////////////
var Vector2 = /** @class */ (function () {
function Vector2(x, y) {
if (x === void 0) { x = 0; }
if (y === void 0) { y = 0; }
this.x = x;
this.y = y;
}
// basic math operators, a vector or scaler can be passed in
Vector2.prototype.copy = function () { return new Vector2(this.x, this.y); };
Vector2.prototype.scale = function (s) { ASSERT(s.x == undefined); return new Vector2(this.x * s, this.y * s); };
Vector2.prototype.add = function (v) { ASSERT(v.x != undefined); return new Vector2(this.x + v.x, this.y + v.y); };
Vector2.prototype.subtract = function (v) { ASSERT(v.x != undefined); return new Vector2(this.x - v.x, this.y - v.y); };
Vector2.prototype.multiply = function (v) { ASSERT(v.x != undefined); return new Vector2(this.x * v.x, this.y * v.y); };
Vector2.prototype.divide = function (v) { ASSERT(v.x != undefined); return new Vector2(this.x / v.x, this.y / v.y); };
// vector math operators
Vector2.prototype.length = function () { return Math.pow(this.lengthSquared(), .5); };
Vector2.prototype.lengthSquared = function () { return Math.pow(this.x, 2) + Math.pow(this.y, 2); };
Vector2.prototype.distance = function (p) { return Math.pow(this.distanceSquared(p), .5); };
Vector2.prototype.distanceSquared = function (p) { return Math.pow((this.x - p.x), 2) + Math.pow((this.y - p.y), 2); };
Vector2.prototype.normalize = function (length) {
if (length === void 0) { length = 1; }
var l = this.length();
return l ? this.scale(length / l) : new Vector2(length);
};
Vector2.prototype.clampLength = function (length) {
if (length === void 0) { length = 1; }
var l = this.length();
return l > length ? this.scale(length / l) : this;
};
Vector2.prototype.dot = function (v) { ASSERT(v.x != undefined); return this.x * v.x + this.y * v.y; };
Vector2.prototype.cross = function (v) { ASSERT(v.x != undefined); return this.x * v.y - this.y * v.x; };
Vector2.prototype.angle = function () { return Math.atan2(this.x, this.y); };
Vector2.prototype.setAngle = function (a, length) {
if (length === void 0) { length = 1; }
this.x = length * Math.sin(a);
this.y = length * Math.cos(a);
return this;
};
Vector2.prototype.rotate = function (a) { var c = Math.cos(a), s = Math.sin(a); return new Vector2(this.x * c - this.y * s, this.x * s + this.y * c); };
Vector2.prototype.direction = function () { return abs(this.x) > abs(this.y) ? this.x < 0 ? 3 : 1 : this.y < 0 ? 2 : 0; };
Vector2.prototype.flip = function () { return new Vector2(this.y, this.x); };
Vector2.prototype.invert = function () { return new Vector2(this.y, -this.x); };
Vector2.prototype.round = function () { return new Vector2(Math.round(this.x), Math.round(this.y)); };
Vector2.prototype.floor = function () { return new Vector2(Math.floor(this.x), Math.floor(this.y)); };
Vector2.prototype.int = function () { return new Vector2(this.x | 0, this.y | 0); };
Vector2.prototype.lerp = function (v, p) { ASSERT(v.x != undefined); return this.add(v.subtract(this).scale(clamp(p))); };
Vector2.prototype.area = function () { return this.x * this.y; };
Vector2.prototype.arrayCheck = function (arraySize) { return this.x >= 0 && this.y >= 0 && this.x < arraySize.x && this.y < arraySize.y; };
return Vector2;
}());
///////////////////////////////////////////////////////////////////////////////
var Color = /** @class */ (function () {
function Color(r, g, b, a) {
if (r === void 0) { r = 1; }
if (g === void 0) { g = 1; }
if (b === void 0) { b = 1; }
if (a === void 0) { a = 1; }
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
Color.prototype.copy = function (c) { return new Color(this.r, this.g, this.b, this.a); };
Color.prototype.add = function (c) { return new Color(this.r + c.r, this.g + c.g, this.b + c.b, this.a + c.a); };
Color.prototype.subtract = function (c) { return new Color(this.r - c.r, this.g - c.g, this.b - c.b, this.a - c.a); };
Color.prototype.multiply = function (c) { return new Color(this.r * c.r, this.g * c.g, this.b * c.b, this.a * c.a); };
Color.prototype.scale = function (s, a) {
if (a === void 0) { a = s; }
return new Color(this.r * s, this.g * s, this.b * s, this.a * a);
};
Color.prototype.clamp = function () { return new Color(clamp(this.r), clamp(this.g), clamp(this.b), clamp(this.a)); };
Color.prototype.lerp = function (c, p) { return this.add(c.subtract(this).scale(clamp(p))); };
Color.prototype.mutate = function (amount, alphaAmount) {
if (amount === void 0) { amount = .05; }
if (alphaAmount === void 0) { alphaAmount = 0; }
return new Color(this.r + rand(amount, -amount), this.g + rand(amount, -amount), this.b + rand(amount, -amount), this.a + rand(alphaAmount, -alphaAmount)).clamp();
};
Color.prototype.rgba = function () {
ASSERT(this.r >= 0 && this.r <= 1 && this.g >= 0 && this.g <= 1 && this.b >= 0 && this.b <= 1 && this.a >= 0 && this.a <= 1);
return "rgb(" + (this.r * 255 | 0) + "," + (this.g * 255 | 0) + "," + (this.b * 255 | 0) + "," + this.a + ")";
};
Color.prototype.rgbaInt = function () {
ASSERT(this.r >= 0 && this.r <= 1 && this.g >= 0 && this.g <= 1 && this.b >= 0 && this.b <= 1 && this.a >= 0 && this.a <= 1);
return (this.r * 255 | 0) + (this.g * 255 << 8) + (this.b * 255 << 16) + (this.a * 255 << 24);
};
Color.prototype.setHSLA = function (h, s, l, a) {
if (h === void 0) { h = 0; }
if (s === void 0) { s = 0; }
if (l === void 0) { l = 1; }
if (a === void 0) { a = 1; }
var q = l < .5 ? l * (1 + s) : l + s - l * s, p = 2 * l - q, f = function (p, q, t) {
return (t = ((t % 1) + 1) % 1) < 1 / 6 ? p + (q - p) * 6 * t :
t < 1 / 2 ? q :
t < 2 / 3 ? p + (q - p) * (2 / 3 - t) * 6 : p;
};
this.r = f(p, q, h + 1 / 3);
this.g = f(p, q, h);
this.b = f(p, q, h - 1 / 3);
this.a = a;
return this;
};
return Color;
}());
///////////////////////////////////////////////////////////////////////////////
var Timer = /** @class */ (function () {
function Timer(timeLeft) {
this.time = timeLeft == undefined ? undefined : time + timeLeft;
this.setTime = timeLeft;
}
Timer.prototype.set = function (timeLeft) {
if (timeLeft === void 0) { timeLeft = 0; }
this.time = time + timeLeft;
this.setTime = timeLeft;
};
Timer.prototype.unset = function () { this.time = undefined; };
Timer.prototype.isSet = function () { return this.time != undefined; };
Timer.prototype.active = function () { return time <= this.time; };
Timer.prototype.elapsed = function () { return time > this.time; };
Timer.prototype.get = function () { return this.isSet() ? time - this.time : 0; };
Timer.prototype.getPercent = function () { return this.isSet() ? percent(this.time - time, 0, this.setTime) : 0; };
return Timer;
}());
/*
LittleJS Engine Configuration
*/
///////////////////////////////////////////////////////////////////////////////
// display settings
var maxWidth = 1920, maxHeight = 1200; // up to 1080p and 16:10
var defaultFont = 'arial'; // font used for text rendering
var fixedWidth = 0, fixedHeight = 0; // use native resolution
var fixedFitToWindow = 1; // stretch canvas to fit window
//const fixedWidth = 1280, fixedHeight = 720; // 720p
//const fixedWidth = 1920, fixedHeight = 1080; // 1080p
//const fixedWidth = 128, fixedHeight = 128; // PICO-8
//const fixedWidth = 240, fixedHeight = 136; // TIC-80
///////////////////////////////////////////////////////////////////////////////
// tile sheet settings
var defaultTileSize = vec2(16); // default size of tiles in pixels
var tileBleedShrinkFix = .3; // prevent tile bleeding from neighbors
var pixelated = 1; // use crisp pixels for pixel art
///////////////////////////////////////////////////////////////////////////////
// webgl config
var glEnable = 1; // can run without gl (texured coloring will be disabled)
var glOverlay = 0; // fix slow rendering in some browsers by not compositing the WebGL canvas
///////////////////////////////////////////////////////////////////////////////
// object config
var defaultObjectSize = vec2(.999); // size of objecs, tiny bit less then 1 to fit in holes
var defaultObjectMass = 1; // how heavy are objects for collison calcuations
var defaultObjectDamping = .99; // how much to slow velocity by each frame 0-1
var defaultObjectAngleDamping = .99; // how much to slow angular velocity each frame 0-1
var defaultObjectElasticity = 0; // how much to bounce 0-1
var defaultObjectFriction = .8; // how much to slow when touching 0-1
var maxObjectSpeed = 1; // camp max speed to avoid fast objects missing collisions
///////////////////////////////////////////////////////////////////////////////
// input config
var gamepadsEnable = 1; // should gamepads be allowed
var touchInputEnable = 1; // touch input is routed to mouse
var copyGamepadDirectionToStick = 1; // allow players to use dpad as analog stick
var copyWASDToDpad = 1; // allow players to use WASD as direction keys
///////////////////////////////////////////////////////////////////////////////
// audio config
var soundEnable = 1; // all audio can be disabled
var audioVolume = .5; // volume for sound, music and speech
var defaultSoundRange = 30; // range where sound no longer plays
var defaultSoundTaper = .7; // what range percent to start tapering off sound 0-1
///////////////////////////////////////////////////////////////////////////////
// medals config
var medalDisplayTime = 5; // how long to show medals
var medalDisplaySlideTime = .5; // how quick to slide on/off medals
var medalDisplayWidth = 640; // width of medal display
var medalDisplayHeight = 99; // height of medal display
var medalDisplayIconSize = 80; // size of icon in medal display
/*
LittleJS - The Tiny JavaScript Game Engine That Can
MIT License - Copyright 2019 Frank Force
Engine Features
- Engine and debug system are separate from game code
- Object oriented with base class engine object
- Engine handles core update loop
- Base class object handles update, physics, collision, rendering, etc
- Engine helper classes and functions like Vector2, Color, and Timer
- Super fast rendering system for tile sheets
- Sound effects audio with zzfx and music with zzfxm
- Input processing system with gamepad and touchscreen support
- Tile layer rendering and collision system
- Particle effect system
- Automatically calls gameInit(), gameUpdate(), gameUpdatePost(), gameRender(), gameRenderPost()
- Debug tools and debug rendering system
- Call engineInit() to start it up!
*/
'use strict';
var engineName = 'LittleJS';
var engineVersion = '1.0.16';
var FPS = 60, timeDelta = 1 / FPS; // engine uses a fixed time step
var tileImage = new Image(); // everything uses the same tile sheet
// core engine variables
var mainCanvas, mainContext, overlayCanvas, overlayContext, mainCanvasSize = vec2(), engineObjects = [], engineCollideObjects = [], cameraPos = vec2(), cameraScale = max(defaultTileSize.x, defaultTileSize.y), frame = 0, time = 0, realTime = 0, paused = 0, frameTimeLastMS = 0, frameTimeBufferMS = 0, debugFPS = 0, gravity = 0, tileImageSize, tileImageSizeInverse, shrinkTilesX, shrinkTilesY, drawCount;
// call this function to start the engine
function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, tileImageSource) {
// init engine when tiles load
tileImage.onload = function () {
// save tile image info
tileImageSizeInverse = vec2(1).divide(tileImageSize = vec2(tileImage.width, tileImage.height));
debug && (tileImage.onload = function () { return ASSERT(1); }); // tile sheet can not reloaded
shrinkTilesX = tileBleedShrinkFix / tileImageSize.x;
shrinkTilesY = tileBleedShrinkFix / tileImageSize.y;
// setup html
document.body.appendChild(mainCanvas = document.createElement('canvas'));
document.body.style = 'margin:0;overflow:hidden;background:#000';
mainCanvas.style = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)' +
(pixelated ? ';image-rendering:crisp-edges;image-rendering:pixelated' : ''); // pixelated rendering
mainContext = mainCanvas.getContext('2d');
// init stuff and start engine
debugInit();
glInit();
// create overlay canvas for hud to appear above gl canvas
document.body.appendChild(overlayCanvas = document.createElement('canvas'));
overlayCanvas.style = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)';
overlayContext = overlayCanvas.getContext('2d');
gameInit();
engineUpdate();
};
// main update loop
var engineUpdate = function (frameTimeMS) {
if (frameTimeMS === void 0) { frameTimeMS = 0; }
requestAnimationFrame(engineUpdate);
// update time keeping
var frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
frameTimeLastMS = frameTimeMS;
if (debug || showWatermark)
debugFPS = lerp(.05, 1e3 / (frameTimeDeltaMS || 1), debugFPS);
if (debug)
frameTimeDeltaMS *= keyIsDown(107) ? 5 : keyIsDown(109) ? .2 : 1; // +/- to speed/slow time
realTime += frameTimeDeltaMS / 1e3;
frameTimeBufferMS = min(frameTimeBufferMS + !paused * frameTimeDeltaMS, 50); // clamp incase of slow framerate
if (paused) {
// do post update even when paused
inputUpdate();
debugUpdate();
gameUpdatePost();
inputUpdatePost();
}
else {
// apply time delta smoothing, improves smoothness of framerate in some browsers
var deltaSmooth = 0;
if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9) {
// force an update each frame if time is close enough (not just a fast refresh rate)
deltaSmooth = frameTimeBufferMS;
frameTimeBufferMS = 0;
}
// update multiple frames if necessary in case of slow framerate
for (; frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / FPS) {
// update game and objects
inputUpdate();
gameUpdate();
engineUpdateObjects();
// do post update
debugUpdate();
gameUpdatePost();
inputUpdatePost();
}
// add the time smoothing back in
frameTimeBufferMS += deltaSmooth;
}
if (fixedWidth) {
// clear set fixed size
mainCanvas.width = fixedWidth;
mainCanvas.height = fixedHeight;
if (fixedFitToWindow) {
// fit to window by adding space on top or bottom if necessary
var aspect = innerWidth / innerHeight;
var fixedAspect = fixedWidth / fixedHeight;
mainCanvas.style.width = overlayCanvas.style.width = aspect < fixedAspect ? '100%' : '';
mainCanvas.style.height = overlayCanvas.style.height = aspect < fixedAspect ? '' : '100%';
if (glCanvas) {
glCanvas.style.width = mainCanvas.style.width;
glCanvas.style.height = mainCanvas.style.height;
}
}
}
else {
// clear and set size to same as window
mainCanvas.width = min(innerWidth, maxWidth);
mainCanvas.height = min(innerHeight, maxHeight);
}
// save canvas size and clear overlay canvas
mainCanvasSize = vec2(overlayCanvas.width = mainCanvas.width, overlayCanvas.height = mainCanvas.height);
mainContext.imageSmoothingEnabled = !pixelated; // disable smoothing for pixel art
// render sort then render while removing destroyed objects
glPreRender(mainCanvas.width, mainCanvas.height);
gameRender();
engineObjects.sort(function (a, b) { return a.renderOrder - b.renderOrder; });
for (var _i = 0, engineObjects_3 = engineObjects; _i < engineObjects_3.length; _i++) {
var o = engineObjects_3[_i];
o.destroyed || o.render();
}
gameRenderPost();
medalsRender();
debugRender();
glCopyToContext(mainContext);
if (showWatermark) {
// update fps
overlayContext.textAlign = 'right';
overlayContext.textBaseline = 'top';
overlayContext.font = '1em monospace';
overlayContext.fillStyle = '#000';
var text = engineName + ' ' + 'v' + engineVersion + ' / '
+ drawCount + ' / ' + engineObjects.length + ' / ' + debugFPS.toFixed(1);
overlayContext.fillText(text, mainCanvas.width - 3, 3);
overlayContext.fillStyle = '#fff';
overlayContext.fillText(text, mainCanvas.width - 2, 2);
drawCount = 0;
}
};
// set tile image source to load the image and start the engine
tileImageSource ? tileImage.src = tileImageSource : tileImage.onload();
}
function engineUpdateObjects() {
// recursive object update
var updateObject = function (o) {
if (!o.destroyed) {
o.update();
for (var _i = 0, _a = o.children; _i < _a.length; _i++) {
var child = _a[_i];
updateObject(child);
}
}
};
for (var _i = 0, engineObjects_4 = engineObjects; _i < engineObjects_4.length; _i++) {
var o = engineObjects_4[_i];
o.parent || updateObject(o);
}
// remove destroyed objects
engineObjects = engineObjects.filter(function (o) { return !o.destroyed; });
engineCollideObjects = engineCollideObjects.filter(function (o) { return !o.destroyed; });
// increment frame and update time
time = ++frame / FPS;
}
/*
LittleJS Object Base Class
- Base object class used by the engine
- Automatically adds self to object list
- Will be updated and rendered each frame
- Renders as a sprite from a tilesheet by default
- Can have color and addtive color applied
- 2d Physics and collision system
- Sorted by renderOrder
- Objects can have children attached
- Parents are updated before children, and set child transform
- Call destroy() to get rid of objects
*/
'use strict';
var EngineObject = /** @class */ (function () {
function EngineObject(pos, size, tileIndex, tileSize, angle, color) {
if (size === void 0) { size = defaultObjectSize; }
if (tileIndex === void 0) { tileIndex = -1; }
if (tileSize === void 0) { tileSize = defaultTileSize; }
if (angle === void 0) { angle = 0; }
// set passed in params
ASSERT(pos && pos.x != undefined && size.x != undefined); // ensure pos and size are vec2s
this.pos = pos.copy();
this.size = size;
this.tileIndex = tileIndex;
this.tileSize = tileSize;
this.angle = angle;
this.color = color;
// set physics defaults
this.mass = defaultObjectMass;
this.damping = defaultObjectDamping;
this.angleDamping = defaultObjectAngleDamping;
this.elasticity = defaultObjectElasticity;
this.friction = defaultObjectFriction;
// init other object stuff
this.spawnTime = time;
this.velocity = vec2(this.collideSolidObjects = this.renderOrder = this.angleVelocity = 0);
this.collideTiles = this.gravityScale = 1;
this.children = [];
// add to list of objects
engineObjects.push(this);
}
EngineObject.prototype.update = function () {
var parent = this.parent;
if (parent) {
// copy parent pos/angle
this.pos = this.localPos.multiply(vec2(parent.getMirrorSign(), 1)).rotate(-parent.angle).add(parent.pos);
this.angle = parent.getMirrorSign() * this.localAngle + parent.angle;
return;
}
// limit max speed to prevent missing collisions
this.velocity.x = clamp(this.velocity.x, maxObjectSpeed, -maxObjectSpeed);
this.velocity.y = clamp(this.velocity.y, maxObjectSpeed, -maxObjectSpeed);
// apply physics
var oldPos = this.pos.copy();
this.pos.x += this.velocity.x = this.damping * this.velocity.x;
this.pos.y += this.velocity.y = this.damping * this.velocity.y + gravity * this.gravityScale;
this.angle += this.angleVelocity *= this.angleDamping;
// physics sanity checks
ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
ASSERT(this.damping >= 0 && this.damping <= 1);
if (!this.mass) // do not update collision for fixed objects
return;
var wasMovingDown = this.velocity.y < 0;
if (this.groundObject) {
// apply friction in local space of ground object
var groundSpeed = this.groundObject.velocity ? this.groundObject.velocity.x : 0;
this.velocity.x = groundSpeed + (this.velocity.x - groundSpeed) * this.friction;
this.groundObject = 0;
//debugPhysics && debugPoint(this.pos.subtract(vec2(0,this.size.y/2)), '#0f0');
}
if (this.collideSolidObjects) {
// check collisions against solid objects
var epsilon = 1e-3; // necessary to push slightly outside of the collision
for (var _i = 0, engineCollideObjects_1 = engineCollideObjects; _i < engineCollideObjects_1.length; _i++) {
var o = engineCollideObjects_1[_i];
// non solid objects don't collide with eachother
if (!this.isSolid & !o.isSolid || o.destroyed || o.parent)
continue;
// check collision
if (!isOverlapping(this.pos, this.size, o.pos, o.size) || o == this)
continue;
// pass collision to objects
if (!this.collideWithObject(o) | !o.collideWithObject(this))
continue;
if (isOverlapping(oldPos, this.size, o.pos, o.size)) {
// if already was touching, try to push away
var deltaPos = oldPos.subtract(o.pos);
var length_1 = deltaPos.length();
var pushAwayAccel = .001; // push away if alread overlapping
var velocity = length_1 < .01 ? randVector(pushAwayAccel) : deltaPos.scale(pushAwayAccel / length_1);
this.velocity = this.velocity.add(velocity);
if (o.mass) // push away if not fixed
o.velocity = o.velocity.subtract(velocity);
debugPhysics && debugAABB(this.pos, o.pos, this.size, o.size, '#f00');
continue;
}
// check for collision
var sx = this.size.x + o.size.x;
var sy = this.size.y + o.size.y;
var smallStepUp = (oldPos.y - o.pos.y) * 2 > sy + gravity; // prefer to push up if small delta
var isBlockedX = abs(oldPos.y - o.pos.y) * 2 < sy;
var isBlockedY = abs(oldPos.x - o.pos.x) * 2 < sx;
if (smallStepUp || isBlockedY || !isBlockedX) // resolve y collision
{
// push outside object collision
this.pos.y = o.pos.y + (sy / 2 + epsilon) * sign(oldPos.y - o.pos.y);
if (o.groundObject && wasMovingDown || !o.mass) {
// set ground object if landed on something
if (wasMovingDown)
this.groundObject = o;
// bounce if other object is fixed or grounded
this.velocity.y *= -this.elasticity;
}
else if (o.mass) {
// inelastic collision
var inelastic = (this.mass * this.velocity.y + o.mass * o.velocity.y) / (this.mass + o.mass);
// elastic collision
var elastic0 = this.velocity.y * (this.mass - o.mass) / (this.mass + o.mass)
+ o.velocity.y * 2 * o.mass / (this.mass + o.mass);
var elastic1 = o.velocity.y * (o.mass - this.mass) / (this.mass + o.mass)
+ this.velocity.y * 2 * this.mass / (this.mass + o.mass);
// lerp betwen elastic or inelastic based on elasticity
var elasticity = max(this.elasticity, o.elasticity);
this.velocity.y = lerp(elasticity, elastic0, inelastic);
o.velocity.y = lerp(elasticity, elastic1, inelastic);
}
debugPhysics && smallStepUp && (abs(oldPos.x - o.pos.x) * 2 > sx) && console.log('stepUp', oldPos.y - o.pos.y);
}
if (!smallStepUp && (isBlockedX || !isBlockedY)) // resolve x collision
{
// push outside collision
this.pos.x = o.pos.x + (sx / 2 + epsilon) * sign(oldPos.x - o.pos.x);
if (o.mass) {
// inelastic collision
var inelastic = (this.mass * this.velocity.x + o.mass * o.velocity.x) / (this.mass + o.mass);
// elastic collision
var elastic0 = this.velocity.x * (this.mass - o.mass) / (this.mass + o.mass)
+ o.velocity.x * 2 * o.mass / (this.mass + o.mass);
var elastic1 = o.velocity.x * (o.mass - this.mass) / (this.mass + o.mass)
+ this.velocity.x * 2 * this.mass / (this.mass + o.mass);
// lerp betwen elasti