Compare commits

...

52 Commits

Author SHA1 Message Date
Nathan Kunicki
d05736773b Audio looping, proper sizing for fullscreen on images 2017-02-24 23:30:57 -07:00
Nathan Kunicki
b5da68a56e Added Audio class 2017-02-24 16:49:17 -06:00
Nathan Kunicki
38744c399c Updated to latest Node 2017-02-23 21:02:08 +00:00
Nathan Kunicki
a903bd6aa3 Added the ability to disable rendering of an item internally 2016-04-17 15:02:54 +01:00
Nathan Kunicki
d8b1411a3e Added wasPressed as well as wasReleased 2016-04-16 15:56:08 +01:00
Nathan Kunicki
bf1f590d20 Fixed wasPressed 2016-04-16 15:17:41 +01:00
Nathan Kunicki
099317232e Added wasPressed function to KeyboardInput class 2016-04-16 15:14:13 +01:00
Nathan Kunicki
ec56fe7dea Added rotation support 2016-03-27 15:46:06 +01:00
Nathan Kunicki
f283c4fdc4 Added JSDoc for basic documentation 2016-03-25 00:47:41 +00:00
Nathan Kunicki
00f7a64b59 Added JSDoc for basic documentation 2016-03-25 00:11:59 +00:00
Nathan Kunicki
fa8ec9125e Fixed particles demo and made text fullscreen relative 2016-03-24 23:02:22 +00:00
Nathan Kunicki
d10e9b4aef Updated Babel 2016-03-24 22:34:20 +00:00
Nathan Kunicki
a743ec206b Updated Babel 2016-03-24 22:27:23 +00:00
Nathan Kunicki
1136aec709 Minor improvement to renderText function 2016-03-09 18:55:15 -06:00
Nathan Kunicki
bada9f03a3 Minor improvement to renderText function 2016-03-09 18:53:27 -06:00
Nathan Kunicki
26929f37d8 A little more fullscreen work for native res and aspect ratios - NOTE: Do not enable at this time 2016-03-09 16:26:40 -06:00
Nathan Kunicki
a7cce2157c Properish gamepad implementation and more work on fullscreen mode 2016-03-09 12:11:24 -06:00
Nathan Kunicki
483ea7cc54 Added fullscreen API| 2016-03-08 17:49:38 -06:00
Nathan Kunicki
68e205216f Extension to font rendering to allow letterSpacing 2016-03-08 17:26:09 -06:00
Nathan Kunicki
89c3595f94 Moved devDependencies to dependencies 2016-03-08 12:11:36 -06:00
Nathan Kunicki
2560e7656c Gamepad API works in Chrome 2016-03-08 12:06:55 -06:00
Nathan Kunicki
11c6cc9cb8 Gamepad API works in Chrome 2016-03-07 18:41:07 -06:00
Nathan Kunicki
5336f0d4bf Pong example uses sticks 2016-03-07 17:59:04 -06:00
Nathan Kunicki
3406c28a80 Start of gamepad support - connection and disconnection 2016-03-07 09:11:02 -05:00
Nathan Kunicki
3cd618e66c Version bump to 0.2.0 for text rendering 2016-03-06 22:38:21 -05:00
Nathan Kunicki
8c85ec17af Added Text entity with Font representation class for rendering text 2016-03-06 22:32:27 -05:00
Nathan Kunicki
dbff95c496 Converted Pong example over to ES6 and new format 2016-03-06 19:45:53 -05:00
Nathan Kunicki
2a032ca2e2 Added repo to package.json 2016-03-04 12:12:27 -05:00
Nathan Kunicki
1e23ef1e62 Changed license, updated deps 2016-03-04 12:06:58 -05:00
Nathan Kunicki
91c282b915 No longer commit builds, instead build on postinstall 2016-03-04 11:39:24 -05:00
Nathan Kunicki
1da427f168 Removed distributables 2016-03-04 11:38:22 -05:00
Nathan Kunicki
84a3c92ac1 Added camera positioning 2016-03-04 11:31:31 -05:00
Nathan Kunicki
73ee61cb00 Everything has an explicit width (Even if its 0), and renamed getters/setters for x/y to left/top 2016-03-03 15:05:09 -05:00
Nathan Kunicki
6936f97933 Moved relative calculation from preprocessing to on-demand via a getter 2016-03-02 13:09:27 -05:00
Nathan Kunicki
d0eec1412e Started fire example, added gulp tasks for all, moved all examples into build process 2016-03-02 11:31:29 -05:00
Nathan Kunicki
d409b2f232 Tidied up the particle demo 2016-02-14 18:40:23 +00:00
Nathan Kunicki
49cb0efe72 Optimised particle fields 2016-02-14 18:06:51 +00:00
Nathan Kunicki
7e8c4cdf12 Particle fields 2016-02-14 15:44:12 +00:00
Nathan Kunicki
db9e310e77 Merge pull request #1 from nathankunicki/feature/particle-system
Particle system
2016-02-14 01:58:15 +00:00
Nathan Kunicki
b098281a02 Fixed addition of velocity to take into account delta 2016-02-14 01:56:23 +00:00
Nathan Kunicki
73a517edc8 Particle system appears to be working 2016-02-14 01:40:00 +00:00
Nathan Kunicki
aa281e57b4 Particle system appears to be working 2016-02-14 01:19:07 +00:00
Nathan Kunicki
13508b7fd9 Particle system appears to be working 2016-02-14 01:16:11 +00:00
Nathan Kunicki
246453a25e Particles now emit at a specified rate and correctly obey parent positions 2016-02-13 23:22:21 +00:00
Nathan Kunicki
8735e06619 First pass at particle system 2016-02-13 16:32:26 +00:00
Nathan Kunicki
7dbeda0dc9 Delta is now passed as param to update methods rather than game property, tidied up sprite and entity 2015-12-24 18:29:00 +00:00
Nathan Kunicki
e2ad337f75 Seperated out stepper from start function, allows manual stepping of world 2015-12-23 17:32:44 +00:00
Nathan Kunicki
f9d0dba07a Fixed Safari Rect rendering issue 2015-12-23 16:41:46 +00:00
Nathan Kunicki
5a76bc6773 Added image classes, new example (snowflakes) 2015-12-22 18:08:33 +00:00
Nathan Kunicki
9d0f3db412 Tidied up method names 2015-12-21 16:56:24 +00:00
Nathan Kunicki
c8777e6556 Fixed bug in pong bounds handling 2015-12-21 16:48:55 +00:00
Nathan Kunicki
14f7f3e30e Committed dists 2015-12-21 16:40:40 +00:00
31 changed files with 2121 additions and 218 deletions

5
.gitignore vendored
View File

@ -1,4 +1,7 @@
.idea/
.DS_Store
dist/
node_modules/
dist/
docs/
examples/*/dist/
browse.VC.db

103
examples/fire/fire.js Normal file
View File

@ -0,0 +1,103 @@
"use strict";
import MomentumEngine from "../../src/es6";
let KeyConsts = MomentumEngine.Consts.Input.Keys;
let black = new MomentumEngine.Classes.Color(0, 0, 0),
fireParticleWidth = 150,
fireParticleHeight = 150;
let startColour = new MomentumEngine.Classes.Color(250, 218, 68, 1),
startColourRandom = new MomentumEngine.Classes.Color(62, 60, 60, 0),
finishColour = new MomentumEngine.Classes.Color(245, 35, 0, 0),
finishColourRandom = new MomentumEngine.Classes.Color(60, 60, 60, 0);
class RandomColor extends MomentumEngine.Classes.Color {
constructor (initialColor, deltaColor) {
let r = initialColor.r + (deltaColor.r * RandomColor._rand()),
g = initialColor.g + (deltaColor.g * RandomColor._rand()),
b = initialColor.b + (deltaColor.b * RandomColor._rand()),
a = initialColor.a + (deltaColor.a * RandomColor._rand());
super(~~r, ~~g, ~~b, ~~a);
}
static _rand () {
return (Math.random() * 2 - 1);
}
}
class FireParticle extends MomentumEngine.Classes.Entity {
constructor (x, y) {
super(x, y);
this.timeToLive = 10000;
this.startColor = new RandomColor(startColour, startColourRandom);
this.finishColor = new RandomColor(finishColour, startColour);
this.deltaColor = startColour.clone().subtract(finishColour);
}
update (delta) {
this.startColor.a = this.startColor.a - (delta * 0.0001);
}
render () {
var gradient = this._game.context.createRadialGradient(
this.relativeLeft + fireParticleWidth / 2,
this.relativeTop + fireParticleHeight / 2,
fireParticleWidth / 10,
this.relativeLeft + fireParticleWidth / 2,
this.relativeTop + fireParticleHeight / 2,
fireParticleWidth / 2
);
gradient.addColorStop(0, this.startColor.toString());
gradient.addColorStop(1, "rgba(0, 0, 0, 0)");
this._game.context.fillStyle = gradient;
this._game.context.fillRect(this.relativeLeft, this.relativeTop, fireParticleWidth, fireParticleHeight);
return true;
}
}
window.onload = function () {
let width = 640,
height = 360,
baseSize = width / 64;
let fireDemo = new MomentumEngine.Classes.Game({
canvas: document.getElementById("canvas"),
width: width,
height: height,
fixRatio: true,
desiredFps: 60,
inputs: {
keyboard: true
}
});
let mainScene = new MomentumEngine.Classes.Rect(0, 0, width, height, black);
fireDemo.addChildEntity(mainScene);
let fireEmitter = new MomentumEngine.Classes.Emitter(width / 2 - (fireParticleWidth / 2), height / 2 - (fireParticleHeight / 2), 250, new MomentumEngine.Classes.Vector2D(0.02, 0.02), FireParticle);
mainScene.addChildEntity(fireEmitter);
fireEmitter.setParticleParent(mainScene);
fireEmitter.emitting = true;
fireDemo.start();
};

10
examples/fire/index.html Normal file
View File

@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<title>Fire - MomentumEngine</title>
<script type="application/javascript" src="./dist/fire.js"></script>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
</html>

View File

@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<title>Particles - MomentumEngine</title>
<script type="application/javascript" src="./dist/particles.js"></script>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
</html>

View File

@ -0,0 +1,90 @@
"use strict";
import MomentumEngine from "../../src/es6";
let KeyConsts = MomentumEngine.Consts.Input.Keys;
class BlueParticle extends MomentumEngine.Classes.Rect {
constructor (x, y) {
super(x, y, 1, 1, new MomentumEngine.Classes.Color(0, 255, 0));
this.timeToLive = 25500;
}
update (delta) {
this.color.a = this.color.a - (delta * 0.00004);
}
}
window.onload = function () {
let width = 640,
height = 360,
baseSize = width / 64;
let particleDemo = new MomentumEngine.Classes.Game({
canvas: document.getElementById("canvas"),
width: width,
height: height,
fixRatio: true,
desiredFps: 60,
inputs: {
keyboard: true
}
});
let black = new MomentumEngine.Classes.Color(0, 0, 0),
red = new MomentumEngine.Classes.Color(255, 0, 0),
blue = new MomentumEngine.Classes.Color(0, 0, 255);
let mainScene = new MomentumEngine.Classes.Rect(0, 0, width, height, black);
particleDemo.addChildEntity(mainScene);
let emitterRect = new MomentumEngine.Classes.Rect(width / 8 - baseSize, height / 2 - baseSize, baseSize * 2, baseSize * 2, red),
emitter = new MomentumEngine.Classes.Emitter(baseSize, baseSize, 4, new MomentumEngine.Classes.Vector2D(0, 0.05), BlueParticle);
let bottomFieldRect = new MomentumEngine.Classes.Rect(width - (baseSize * 33), height - (baseSize * 11), baseSize * 2, baseSize * 2, blue),
bottomField = new MomentumEngine.Classes.Field(baseSize, baseSize, 0.1);
let topFieldRect = new MomentumEngine.Classes.Rect(width - (baseSize * 33), baseSize * 9, baseSize * 2, baseSize * 2, blue),
topField = new MomentumEngine.Classes.Field(baseSize, baseSize, 0.1);
mainScene.addChildEntity(emitterRect);
emitterRect.addChildEntity(emitter);
mainScene.addChildEntity(bottomFieldRect);
bottomFieldRect.addChildEntity(bottomField);
mainScene.addChildEntity(topFieldRect);
topFieldRect.addChildEntity(topField);
emitterRect.update = function (delta) {
if (particleDemo.inputs.keyboard.isPressed(KeyConsts.UP)) {
emitterRect.pos.y -= (0.2 * delta);
}
if (particleDemo.inputs.keyboard.isPressed(KeyConsts.DOWN)) {
emitterRect.pos.y += (0.2 * delta);
}
if (particleDemo.inputs.keyboard.isPressed(KeyConsts.LEFT)) {
emitterRect.pos.x -= (0.2 * delta);
}
if (particleDemo.inputs.keyboard.isPressed(KeyConsts.RIGHT)) {
emitterRect.pos.x += (0.2 * delta);
}
};
emitter.setParticleParent(mainScene);
emitter.particleFields.push(bottomField, topField);
emitter.spread = Math.PI / 8;
emitter.emitting = true;
particleDemo.start();
};

View File

@ -2,10 +2,9 @@
<html>
<head>
<title>Pong - MomentumEngine</title>
<script type="application/javascript" src="../../dist/es5.js"></script>
<script type="application/javascript" src="./pong.js"></script>
<script type="application/javascript" src="./dist/pong.js"></script>
</head>
<body>
<canvas id="game"></canvas>
<canvas id="canvas"></canvas>
</body>
</html>

View File

@ -1,119 +1,332 @@
"use strict";
window.onload = function () {
import MomentumEngine from "../../src/es6";
var KeyConsts = MomentumEngine.Consts.Input.Keys;
let KeyConsts = MomentumEngine.Consts.Input.Keys;
var width = 640,
let width = 640,
height = 360,
baseSize = width / 64;
let white = new MomentumEngine.Classes.Color(255, 255, 255),
black = new MomentumEngine.Classes.Color(0, 0, 0),
red = new MomentumEngine.Classes.Color(255, 0, 0);
var pong = new MomentumEngine.Classes.Game({
canvas: document.getElementById("game"),
let font = new MomentumEngine.Classes.Font("Arial", 32, white, red);
class Ball extends MomentumEngine.Classes.Rect {
constructor (startingLeft, startingTop) {
super(startingLeft, startingTop, baseSize, baseSize, white);
this.startingLeft = startingLeft;
this.startingTop = startingTop;
this.speed = new MomentumEngine.Classes.Vector2D(0.1, 0.05); // Starting ball speed
}
update (delta) {
this.pos.add(this.speed.clone().multiply(delta));
if (this.left + baseSize > width && this.speed.x > 0) {
this.left = this.startingLeft;
this.top = this.startingTop;
this.game.leftScoreboard.increment();
} else if (this.left < 0 && this.speed.x < 0) {
this.left = this.startingLeft;
this.top = this.startingTop;
this.game.rightScoreboard.increment();
}
if ((this.top + baseSize > height && this.speed.y > 0) || (this.top < 0 && this.speed.y < 0)) {
this.speed.y = -this.speed.y;
}
}
}
class Paddle extends MomentumEngine.Classes.Rect {
constructor (posLeft, keys) {
super(posLeft, baseSize, baseSize, baseSize * 7, white);
this.keyUp = keys.up;
this.keyDown = keys.down;
this.scoreboard = null;
}
update (delta) {
if (this.keyUp(this, delta)) {
this.top -= (0.5 * delta);
} else if (this.keyDown(this, delta)) {
this.top += (0.5 * delta);
}
if (this.top > height - (baseSize * 8)) {
this.top = height - (baseSize * 8);
} else if (this.top < baseSize) {
this.top = baseSize;
}
this.balls.forEach((ball) => {
if (this.isCollidingWith(ball)) {
ball.speed.x = -ball.speed.x;
}
});
}
}
class Scoreboard extends MomentumEngine.Classes.Text {
constructor (posLeft) {
super(posLeft, 35, font);
this.score = 0;
this.text = "Score: 0";
}
increment () {
this.score++;
this.text = `Score: ${this.score}`;
}
}
class Pong extends MomentumEngine.Classes.Game {
constructor (canvas, width, height) {
super({
canvas: canvas,
width: width,
height: height,
fixRatio: true,
desiredFps: 60,
//fixFrameRate: true,
fullscreen: {
nativeResolution: true,
maintainAspectRatio: true
},
inputs: {
keyboard: true
keyboard: true,
gamepad: true
}
});
let background = new MomentumEngine.Classes.Rect(0, 0, width, height, black);
this.addChildEntity(background);
// Colors
var white = new MomentumEngine.Classes.Color(255, 255, 255),
black = new MomentumEngine.Classes.Color(0, 0, 0);
this.balls = [];
this.paddles = [];
}
// All of these are instances of MomentumEngine.Entity;
var mainScene = new MomentumEngine.Classes.Rect(0, 0, width, height, black),
ball = new MomentumEngine.Classes.Rect((width / 2) - (baseSize / 2), (height / 2) - (baseSize / 2), baseSize, baseSize, white),
leftPaddle = new MomentumEngine.Classes.Rect(baseSize, baseSize, baseSize, baseSize * 7, white),
rightPaddle = new MomentumEngine.Classes.Rect(width - (baseSize * 2), baseSize, baseSize, baseSize * 7, white);
setLeftScoreboard (scoreboard) {
this.leftScoreboard = scoreboard;
this.addChildEntity(scoreboard);
}
setRightScoreboard (scoreboard) {
this.rightScoreboard = scoreboard;
this.addChildEntity(scoreboard);
}
addBall (ball) {
ball.game = this;
this.balls.push(ball);
this.addChildEntity(ball);
}
addPaddle (paddle) {
this.paddles.push(paddle);
this.addChildEntity(paddle);
paddle.balls = this.balls;
}
}
var leftPaddleUpCondition = function (paddle, delta) {
let gamepadInput = paddle._game.inputs.gamepad;
let gamepadConnected = (gamepadInput.numGamepads >= 1),
axisMoved = false;
if (gamepadConnected) {
let upDownAxes = gamepadInput.getGamepadById(0).getAxis(1);
if (upDownAxes < -0.1) {
this.top += (0.5 * delta) * upDownAxes;
axisMoved = true;
}
}
if (!axisMoved) {
if (paddle._game.inputs.keyboard.isPressed(KeyConsts.CHAR_Q)) {
this.top -= 0.5 * delta;
}
}
};
var leftPaddleDownCondition = function (paddle, delta) {
let gamepadInput = paddle._game.inputs.gamepad;
let gamepadConnected = (gamepadInput.numGamepads >= 1),
axisMoved = false;
if (gamepadConnected) {
let upDownAxes = gamepadInput.getGamepadById(0).getAxis(1);
if (upDownAxes > 0.1) {
this.top += (0.5 * delta) * upDownAxes;
axisMoved = true;
}
}
if (!axisMoved) {
if (paddle._game.inputs.keyboard.isPressed(KeyConsts.CHAR_A)) {
this.top += 0.5 * delta;
}
}
};
var rightPaddleUpCondition = function (paddle, delta) {
let gamepadInput = paddle._game.inputs.gamepad;
let gamepadConnected = (gamepadInput.numGamepads >= 1 && gamepadInput.getGamepadById(0).numAxis >= 6),
axisMoved = false;
if (gamepadConnected) {
let upDownAxes = gamepadInput.getGamepadById(0).getAxis(4);
if (upDownAxes < -0.1) {
this.top += (0.5 * delta) * upDownAxes;
axisMoved = true;
}
}
if (!axisMoved) {
if (paddle._game.inputs.keyboard.isPressed(KeyConsts.CHAR_O)) {
this.top -= 0.5 * delta;
}
}
};
var rightPaddleDownCondition = function (paddle, delta) {
let gamepadInput = paddle._game.inputs.gamepad;
let gamepadConnected = (gamepadInput.numGamepads >= 1),
axisMoved = false;
if (gamepadConnected) {
let upDownAxes = gamepadInput.getGamepadById(0).getAxis(4);
if (upDownAxes > 0.1) {
this.top += (0.5 * delta) * upDownAxes;
axisMoved = true;
}
}
if (!axisMoved) {
if (paddle._game.inputs.keyboard.isPressed(KeyConsts.CHAR_L)) {
this.top += 0.5 * delta;
}
}
};
window.onload = function () {
var pong = new Pong(document.getElementById("canvas"), width, height);
var ball = new Ball((width / 2) - (baseSize / 2), (height / 2) - (baseSize / 2));
var leftPaddle = new Paddle(baseSize, {
up: leftPaddleUpCondition,
down: leftPaddleDownCondition
});
var rightPaddle = new Paddle(width - (baseSize * 2), {
up: rightPaddleUpCondition,
down: rightPaddleDownCondition
});
var leftScoreboard = new Scoreboard(baseSize),
rightScoreboard = new Scoreboard(width - baseSize);
rightScoreboard.textAlign = "right"; // Right align the text of the right scoreboard
// Create scene graph
pong.addChildEntity(mainScene);
mainScene.addChildEntity(ball);
mainScene.addChildEntity(leftPaddle);
mainScene.addChildEntity(rightPaddle);
// Update and render the ball
ball.state.speed = new MomentumEngine.Classes.Vector2D(0.1, 0.05); // Current ball speed
ball.update = function () {
this.pos.add(this.state.speed.clone().multiply(pong.lastFrameDelta));
if ((this.pos.x + baseSize > width) || (this.pos.x < 0)) {
this.state.speed.x = -this.state.speed.x;
}
if ((this.pos.y + baseSize > height) || (this.pos.y < 0)) {
this.state.speed.y = -this.state.speed.y;
}
};
// Update and render the left paddle
leftPaddle.update = function () {
if (pong.inputs.keyboard.isPressed(KeyConsts.CHAR_Q) || pong.inputs.keyboard.isPressed(KeyConsts.UP)) {
leftPaddle.pos.y -= (0.5 * pong.lastFrameDelta);
}
if (pong.inputs.keyboard.isPressed(KeyConsts.CHAR_A) || pong.inputs.keyboard.isPressed(KeyConsts.DOWN)) {
leftPaddle.pos.y += (0.5 * pong.lastFrameDelta);
}
if (leftPaddle.pos.y > height - (baseSize * 8)) {
leftPaddle.pos.y = height - (baseSize * 8);
}
if (leftPaddle.pos.y < baseSize) {
leftPaddle.pos.y = baseSize;
}
if (leftPaddle.isColliding(ball) && ball.state.speed.x < 0) {
ball.state.speed.x = -ball.state.speed.x;
console.log(ball.state.speed.length());
}
};
// Render the right paddle
rightPaddle.update = function () {
if (pong.inputs.keyboard.isPressed(KeyConsts.CHAR_O)) {
rightPaddle.pos.y -= (0.5 * pong.lastFrameDelta);
}
if (pong.inputs.keyboard.isPressed(KeyConsts.CHAR_L)) {
rightPaddle.pos.y += (0.5 * pong.lastFrameDelta);
}
if (rightPaddle.pos.y > height - (baseSize * 8)) {
rightPaddle.pos.y = height - (baseSize * 8);
}
if (rightPaddle.pos.y < baseSize) {
rightPaddle.pos.y = baseSize;
}
if (rightPaddle.isColliding(ball) && ball.state.speed.x > 0) {
ball.state.speed.x = -ball.state.speed.x;
}
};
pong.addBall(ball);
pong.addPaddle(leftPaddle);
pong.addPaddle(rightPaddle);
pong.setLeftScoreboard(leftScoreboard);
pong.setRightScoreboard(rightScoreboard);
pong.start();
document.addEventListener("keydown", function(e) {
if (e.keyCode == 13) {
pong.toggleFullScreen();
}
}, false);
};

View File

@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<title>Snowflakes - MomentumEngine</title>
<script type="application/javascript" src="./dist/snowflakes.js"></script>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

View File

@ -0,0 +1,70 @@
"use strict";
import MomentumEngine from "../../src/es6";
let KeyConsts = MomentumEngine.Consts.Input.Keys;
window.onload = function () {
var width = 640,
height = 360;
var snowflakes = new MomentumEngine.Classes.Game({
canvas: document.getElementById("canvas"),
width: width,
height: height,
fixRatio: true,
desiredFps: 60
});
// Colors
var blue = new MomentumEngine.Classes.Color(204, 255, 255);
// All of these are instances of MomentumEngine.Classes.Entity
var mainScene = new MomentumEngine.Classes.Rect(0, 0, width, height, blue);
// Load images
var snowflakeImg = new MomentumEngine.Classes.ImageLoader("./snowflake.png");
// Create scene graph
snowflakes.addChildEntity(mainScene);
mainScene.update = function (delta) {
if ((snowflakes.frameCounter % 120) == 0) { // Every two seconds or so, add a new snowflake
var startPos = (Math.random() * width) - 50;
var newSnowflake = new MomentumEngine.Classes.Sprite(startPos, -100, 100, 100, snowflakeImg);
newSnowflake.update = function () {
this.top = this.top + (delta * 0.06);
};
mainScene.addChildEntity(newSnowflake);
mainScene.children.forEach(function (oldSnowflake) {
if (oldSnowflake.top > height) {
// Clean up snowflakes that are no longer visible
mainScene.detachChildEntity(oldSnowflake);
}
});
}
};
snowflakes.start();
};

View File

@ -3,7 +3,33 @@
var gulp = require("gulp"),
path = require("path"),
gutil = require("gulp-util"),
webpack = require("webpack");
webpack = require("webpack"),
jsdoc = require("gulp-jsdoc3");
let minify = true,
watch = false,
examples = [
"fire",
"particles",
"pong",
"snowflakes"
];
process.argv.forEach((arg) => {
if (arg == "--dev" || arg == "-d") {
minify = false;
gutil.log("[Momentum Engine] dev flag passed, enabled");
}
if (arg == "--watch" || arg == "-w") {
watch = true;
gutil.log("[Momentum Engine] watch flag passed, enabled");
}
});
var build = function (options, callback) {
@ -25,15 +51,13 @@ var build = function (options, callback) {
}
webpack({
entry: {
"es5": path.join(__dirname, "src", "es5.js")
},
entry: options.entry,
bail: !options.watch,
watch: options.watch,
devtool: "source-map",
plugins: plugins,
output: {
path: path.join(__dirname, "dist"),
path: options.path,
filename: "[name].js"
},
module: {
@ -41,7 +65,8 @@ var build = function (options, callback) {
loader: "babel-loader",
test: /\.js$/,
include: [
path.join(__dirname, "src")
path.join(__dirname, "src"),
path.join(__dirname, "examples")
],
query: {
plugins: ["transform-runtime"],
@ -76,25 +101,51 @@ var build = function (options, callback) {
};
gulp.task("build-dev", (callback) => {
examples.forEach((example) => {
let entry = {};
entry[example] = path.join(__dirname, "examples", example, `${example}.js`);
gulp.task(`${example}-example`, (callback) => {
build({
watch: false,
minify: false
entry: entry,
path: path.join(__dirname, "examples", example, "dist"),
watch: watch,
minify: minify
}, callback);
});
gulp.task("build", (callback) => {
build({
watch: false,
minify: true
}, callback);
});
gulp.task("watch", () => {
build({
watch: true,
minify: false
});
});
gulp.task("examples", examples.map((example) => { return `${example}-example`; }));
gulp.task("engine", (callback) => {
build({
entry: {
"es5": path.join(__dirname, "src", "es5.js")
},
path: path.join(__dirname, "dist"),
watch: watch,
minify: minify
}, callback);
});
gulp.task("docs", (callback) => {
gulp.src([
"src/classes/*.js"
], {
read: false
}).pipe(jsdoc({
opts: {
destination: "docs"
}
}, callback));
})
gulp.task("build", ["engine", "docs", "examples"]);
gulp.task("default", ["build"]);

View File

@ -1,26 +1,31 @@
{
"name": "momentumengine",
"version": "0.0.1",
"version": "0.10.0",
"description": "An ES6 game and animation engine.",
"main": "src/es6.js",
"repository": {
"type": "git",
"url": "https://github.com/nathankunicki/momentumengine.git"
},
"scripts": {
"postinstall": "gulp --silent",
"build": "gulp",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Nathan Kunicki <me@nathankunicki.com>",
"license": "ISC",
"license": "MIT",
"engines": {
"node": "^4.2.3"
"node": "^7.6.0"
},
"devDependencies": {
"babel-core": "6.3.17",
"babel-loader": "6.2.0",
"babel-plugin-transform-runtime": "6.3.13",
"babel-preset-es2015": "6.3.13",
"babel-preset-stage-0": "6.3.13",
"babel-runtime": "6.3.19",
"gulp": "3.9.0",
"gulp-util": "3.0.7",
"webpack": "1.12.9"
},
"dependencies": {}
"dependencies": {
"babel-core": "6.23.1",
"babel-loader": "6.3.2",
"babel-plugin-transform-runtime": "6.23.0",
"babel-preset-es2015": "6.22.0",
"babel-preset-stage-0": "6.22.0",
"gulp": "3.9.1",
"gulp-jsdoc3": "1.0.1",
"gulp-util": "3.0.8",
"webpack": "2.2.1"
}
}

87
src/classes/audio.js Normal file
View File

@ -0,0 +1,87 @@
"use strict";
class AudioTrack {
constructor (src) {
this._loaded = false; // Default is true, set it to false until the audio has loaded
this._error = false; // If the audio fails to load, this will contain the reason
this._loop = false;
this._audioObj = new Audio();
this._audioObj.addEventListener("loadeddata", () => {
this._loaded = true;
this._error = false;
this._audioObj.addEventListener("ended", () => {
if (this._loop) {
this._audioObj.currentTime = 0;
this._audioObj.play();
}
});
});
this._audioObj.addEventListener("error", (err) => {
this._loaded = false;
this._error = err;
});
this._audioObj.src = src;
}
get loop () {
return this._loop;
}
set loop (shouldLoop) {
return this._loop = shouldLoop;
}
play () {
if (this._loaded) {
return this._audioObj.play();
} else {
return false;
}
}
pause () {
if (this._loaded) {
return this._audioObj.pause();
} else {
return false;
}
}
seek (seconds) {
if (this._loaded) {
return this._audioObj.currentTime = seconds;
} else {
return false;
}
}
isLoaded () {
return this._loaded;
}
isError () {
return this._error;
}
}
export default AudioTrack;

View File

@ -1,26 +1,128 @@
"use strict";
/**
* Class representing a color
*/
class Color {
constructor (r, g, b, a) {
/**
* Create a color
* @param {number} red - Red value (0-255)
* @param {number} green - Green value (0-255)
* @param {number} blue - Blue value (0-255)
* @param {number} alpha - Alpha value (0-1)
*/
constructor (red = 0, green = 0, blue = 0, alpha = 1) {
this.r = r || 0;
this.g = g || 0;
this.b = b || 0;
this.a = a || 1;
this.red = red;
this.green = green;
this.blue = blue;
this.alpha = alpha;
}
/**
* Returns the rgba (rgba()) string representation of the color
* @returns {string}
*/
toString () {
return `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a})`;
return `rgba(${this.red}, ${this.green}, ${this.blue}, ${this.alpha})`;
}
/**
* Returns the hex (#) representation of the color
* @returns {string}
*/
toHex () {
return `#${((r << 16) | (g << 8) | b).toString(16)}`;
}
/**
* Clones the color and returns a new color
* @returns {Color}
*/
clone () {
return new Color(this.red, this.green, this.blue, this.alpha);
}
/**
* Add a color to this color
* @param {Color} color - Color to add
* @returns {Color}
*/
add (color) {
if (color instanceof Color) {
this.red += color.r; this.green += color.g; this.blue += color.b; this.alpha += color.a;
} else {
this.red += color; this.green += color; this.blue += color; this.alpha += color;
}
return this;
}
/**
* Subtract a color from this color
* @param {Color} color - Color to subtract
* @returns {Color}
*/
subtract (color) {
if (color instanceof Color) {
this.red -= color.r; this.green -= color.g; this.blue -= color.b; this.alpha -= color.a;
} else {
this.red -= color; this.green -= color; this.blue -= color; this.alpha -= color;
}
return this;
}
/**
* Multiply this color with another color
* @param {Color} color - Color to multiply with
* @returns {Color}
*/
multiply (color) {
if (color instanceof Color) {
this.red *= color.r; this.green *= color.g; this.blue *= color.b; this.alpha *= color.a;
} else {
this.red *= color; this.green *= color; this.blue *= color; this.alpha *= color;
}
return this;
}
/**
* Divide this color with another color
* @param {Color} color - Color to divide by
* @returns {Color}
*/
divide (color) {
if (color instanceof Color) {
this.red /= color.r; this.green /= color.g; this.blue /= color.b; this.alpha /= color.a;
} else {
this.red /= color; this.green /= color; this.blue /= color; this.alpha /= color;
}
return this;
}
}

95
src/classes/emitter.js Normal file
View File

@ -0,0 +1,95 @@
"use strict";
import Entity from "./entity.js";
import Vector2D from "./vector2d.js";
import Utils from "../libs/utils";
class Emitter extends Entity {
constructor (x, y, rate, velocity, particle) {
super(x, y);
this.particleVelocity = velocity;
this.particleClass = particle;
this.particleFields = [];
this.rate = rate;
this.emitting = false;
this.spread = Math.PI;
this._lastEmitTime = this._creationTime;
this._wasEmitting = false;
this._particles = [];
}
setParticleParent (entity) {
this._particleParent = entity;
}
_emit () {
let ParticleClass = this.particleClass,
parent = this._particleParent || this._parent;
let angle = this.particleVelocity.angle() + this.spread - (Math.random() * this.spread * 2),
magnitude = this.particleVelocity.length(),
velocity = Vector2D.fromAngle(angle, magnitude);
// NK: This might cause a bug where child renders have an incorrect position because preprocess should normally be called after the update function but before the render, here it is before update. We'll see.
let particle = new ParticleClass(this.relativeLeft, this.relativeTop);
particle.velocity = velocity;
Utils.mergeIntoArray(particle.fields, this.particleFields);
//this._particles.push(particle);
parent.addChildEntity(particle);
}
_triggerEmissions () {
if (this.emitting) {
let currentTime = Date.now();
if (!this._wasEmitting) {
this._wasEmitting = true;
this._lastEmitTime = currentTime;
}
let emitDelta = currentTime - this._lastEmitTime;
if (emitDelta > this.rate) {
let emissions = ~~(emitDelta / this.rate);
this._lastEmitTime = currentTime + (emitDelta - (this.rate * emissions));
for (let i = 0; i < emissions; i++) {
this._emit();
}
}
} else {
this._wasEmitting = false;
}
}
update () {
this._triggerEmissions();
}
}
export default Emitter;

View File

@ -2,24 +2,162 @@
import Vector2D from "./vector2d.js";
/**
* Class representing an entity in a scene
*/
class Entity {
constructor (x, y) {
/**
* Create an entity
* @param {number} x - x (Left) position of the entity
* @param {number} y - y (Top) position of the entity
*/
constructor (x = 0, y = 0) {
this.pos = new Vector2D(x || 0, y || 0);
this.pos = new Vector2D(x, y);
this.velocity = new Vector2D(0, 0);
this.acceleration = new Vector2D(0, 0);
this.size = new Vector2D(0, 0);
this.rotation = 0;
this.display = true;
this.fields = [];
this.state = {};
this.children = [];
this._calculatedPos = this.pos.clone();
this._lastCalculated = 0;
this._relativePos = this.pos.clone();
this._lastRelativePosCalculated = 0;
this._lastRelativeSizeCalculated = 0;
this._game = null;
this._parent = null;
this._creationTime = +(new Date());
}
/**
* x (Left) position of the entity
*/
get left () {
return this.pos.x;
}
set left (val) {
let res = (this.pos.x = val);
if (this._parent) {
this._relativePos.x = this.pos.x + this._parent.relativeLeft;
} else {
this._relativePos.x = this.pos.x;
}
return res;
}
/**
* y (Top) position of the entity
*/
get top () {
return this.pos.y;
}
set top (val) {
let res = (this.pos.y = val);
if (this._parent) {
this._relativePos.y = this.pos.y + this._parent.relativeTop;
} else {
this._relativePos.y = this.pos.y;
}
return res;
}
/**
* Returns the absolute x (Left) position relative to the entities parent tree
* @returns {number} x - x (Left) position relative to the entities parent tree
*/
get relativeLeft () {
return this._calculateRelativePos().x;
}
/**
* Returns the absolute y (Top) position relative to the entities parent tree
* @returns {number} y - y (Top) position relative to the entities parent tree
*/
get relativeTop () {
return this._calculateRelativePos().y;
}
/**
* Width of the entity
*/
get width () {
return this.size.x;
}
set width (width) {
return this.size.x = width;
}
/**
* Height of the entity
*/
get height () {
return this.size.y;
}
set height (height) {
return this.size.y = height;
}
/**
* Set the velocity of the entity
* @param {Number} x - x (Left) velocity
* @param {Number} y - y (Top) velocity
*/
setVelocity (x, y) {
if (x instanceof Vector2D) {
this.velocity = x;
} else {
this.velocity.x = x;
this.velocity.y = y;
}
}
/**
* Set the acceleration of the entity
* @param x {Number} x - x (Left) acceleration
* @param y {Number} y - y (Top) acceleration
*/
setAcceleration (x, y) {
if (x instanceof Vector2D) {
this.acceleration = x;
} else {
this.acceleration.x = x;
this.acceleration.y = y;
}
}
/**
* Creates a new child entity.
* Note: This creates an instance of Entity, the base class. Under most circumstances you should use addChildEntity with an entity you have created.
* @returns {Entity}
*/
createChildEntity () {
let child = new Entity();
@ -33,6 +171,11 @@ class Entity {
}
/**
* Add an entity as a child
* @param {Entity} child - The child entity
* @returns {Entity}
*/
addChildEntity (child) {
child._updateGame(this._game);
@ -44,35 +187,52 @@ class Entity {
}
detachChildEntity (entity) {
// Not implemented
/**
* Removes entity from children
* @param {Entity} child - The child entity
* @returns {boolean} Indicates successful removal
*/
detachChildEntity (child) {
for (let i = 0; i < this.children.length; i++) {
if (this.children[i] == child) {
this.children.splice(i, 1);
return true;
}
}
return false;
}
_recalculatePos () {
_calculateRelativePos () {
// NK: This should be called within "render", not "update". The purpose of this function is to calculate the true position of the entity relative to all its parents. It does this recursively, calling the _recalculatePos method all the way back up the tree and continuously adding the results together.
// NK: The purpose of this function is to calculate the true position of the entity relative to all its parents. It does this recursively, calling the _calculateRelativePos method all the way back up the tree and continuously adding the results together.
// Note there is a limiter, where the last calculated frame is stored, so that if the position has already been calculated for that node in this particular frame, the cached result is used rather than recalculating.
if (this._game && this._lastCalculated < this._game.frameCounter) {
// When rendering, the draw calls should use this._relativePos rather than this.pos in order for the position to be correct.
if (this._game && this._lastRelativePosCalculated < this._game.frameCounter) {
if (this._parent) {
let parentPos = this._parent._recalculatePos();
this._relativePos.x = this.pos.x + this._parent.relativeLeft;
this._relativePos.y = this.pos.y + this._parent.relativeTop;
this._calculatedPos.x = this.pos.x + parentPos.x;
this._calculatedPos.y = this.pos.y + parentPos.y;
} else {
this._calculatedPos.x = this.pos.x;
this._calculatedPos.y = this.pos.y;
this._relativePos.x = this.pos.x;
this._relativePos.y = this.pos.y;
}
this._lastCalculated = this._game.frameCounter;
this._lastRelativePosCalculated = this._game.frameCounter;
}
return this._calculatedPos;
return this._relativePos;
}
@ -88,12 +248,109 @@ class Entity {
}
_updateEntity () {
_scaleForLeft (val) {
if ((this.update && this.update()) || (typeof this.update === "undefined")) {
let game = this._game;
if (!game.isFullScreen) {
return val;
} else {
return (game._fullScreenXPos + (val * game._fullScreenXScaling));
}
}
_scaleForWidth (val) {
let game = this._game;
if (!game.isFullScreen) {
return val;
} else {
return (val * game._fullScreenXScaling);
}
}
_scaleForTop (val) {
let game = this._game;
if (!game.isFullScreen) {
return val;
} else {
return (game._fullScreenYPos + (val * game._fullScreenYScaling));
}
}
_scaleForHeight (val) {
let game = this._game;
if (!game.isFullScreen) {
return val;
} else {
return (val * game._fullScreenYScaling);
}
}
_calculateFields (delta) {
let acceleration = new Vector2D(0, 0);
for (let i = 0; i < this.fields.length; i++) {
let field = this.fields[i];
// NK: These call _relativePos, I don't like using this outside of the render method...
let vector = new Vector2D(
field.relativeLeft - this.relativeLeft,
field.relativeTop - this.relativeTop
);
let force = field.mass / Math.pow(vector.dot(vector), 1.5);
acceleration.add(vector.multiply(force).multiply(delta));
}
return this.acceleration.clone().add(acceleration);
}
_updateEntity (delta) {
if (this.timeToLive) {
if (Date.now() - this._creationTime > this.timeToLive) {
this._parent.detachChildEntity(this);
}
}
// Calculate new position based on velocity and acceleration if there's one set
if (this.velocity && (this.velocity.x !== 0 || this.velocity.y !== 0)) {
this.velocity.add(this._calculateFields(delta));
this.pos.x += (this.velocity.x * delta);
this.pos.y += (this.velocity.y * delta);
}
// If there's an update method, call it
let updated = this.update && this.update(delta);
if (updated || (typeof updated == "undefined") || (typeof this.update === "undefined")) {
this.children.forEach((child) => {
child._updateEntity();
child._updateEntity(delta);
});
}
@ -103,7 +360,13 @@ class Entity {
_renderEntity () {
if ((this.render && this.render()) || (typeof this.render === "undefined")) {
let rendered = this.display && this.render && this.render();
if (rendered) {
this._game._lastFrameTotalRenders++;
}
if (rendered || (typeof rendered == "undefined") || (typeof this.render === "undefined")) {
this.children.forEach((child) => {
child._renderEntity();

21
src/classes/field.js Normal file
View File

@ -0,0 +1,21 @@
"use strict";
import Entity from "./entity.js";
import Vector2D from "./vector2d.js";
class Field extends Entity {
constructor (x, y, mass) {
super(x, y);
this.mass = mass;
}
}
export default Field;

22
src/classes/font.js Normal file
View File

@ -0,0 +1,22 @@
"use strict";
import Color from "./color.js";
class Font {
constructor(family, size, fill = null, stroke = null) {
this.family = family;
this.size = `${size}px`;
this.fill = fill;
this.stroke = stroke;
}
}
export default Font;

View File

@ -1,7 +1,10 @@
"use strict";
import Entity from "./entity.js";
import Vector2D from "./vector2d.js";
import KeyboardInput from "./keyboardinput.js";
import GamepadInput from "./gamepadinput.js";
class Game extends Entity {
@ -10,6 +13,7 @@ class Game extends Entity {
super(); // Call entity constructor
config = config || {};
config.fullscreen = config.fullscreen || {};
config.inputs = config.inputs || {};
@ -17,24 +21,28 @@ class Game extends Entity {
if (config.canvas) {
this.canvas = config.canvas;
} else {
throw new Error("MomentumEngine.Game must be constructed with a canvas");
throw new Error("MomentumEngine.Classes.Game must be constructed with a canvas");
}
if (config.width) {
this.width = config.width;
} else {
throw new Error("MomentumEngine.Game must be constructed with canvas width");
throw new Error("MomentumEngine.Classes.Game must be constructed with canvas width");
}
if (config.height) {
this.height = config.height;
} else {
throw new Error("MomentumEngine.Game must be constructed with canvas height");
throw new Error("MomentumEngine.Classes.Game must be constructed with canvas height");
}
this.scale = 1;
// Optional params
this.desiredFps = config.desiredFps || 60;
this.fixFrameRate = !!config.fixFrameRate;
this.fullScreenNativeResolution = !!config.fullscreen.nativeResolution;
if (config.fixRatio) {
@ -57,19 +65,45 @@ class Game extends Entity {
}
this.scale = deviceRatio / backingStoreRatio;
this._deviceRatio = deviceRatio;
this.canvas.width = this.width * this.scale;
this.canvas.height = this.height * this.scale;
this.canvas.style.width = this.width + "px";
this.canvas.style.height = this.height + "px";
this.canvas.style.width = `${this.width}px`;
this.canvas.style.height = `${this.height}px`;
// Calculate fullscreen settings
if (config.fullscreen.nativeResolution) {
this._fullScreenXScaling = screen.width / this.width;
this._fullScreenYScaling = screen.height / this.height;
} else {
this._fullScreenXScaling = 1;
this._fullScreenYScaling = 1;
}
this._fullScreenXPos = 0;
this._fullScreenYPos = 0;
if (config.fullscreen.maintainAspectRatio) {
if (this._fullScreenXScaling > this._fullScreenYScaling) {
this._fullScreenXScaling = this._fullScreenYScaling;
this._fullScreenXPos = (screen.width - (this.width * this._fullScreenXScaling)) / 2;
} else {
this._fullScreenYScaling = this._fullScreenXScaling;
this._fullScreenYPos = (screen.height - (this.height * this._fullScreenYScaling)) / 2;
}
}
// Call getContext last for Ejecta only.
if (typeof ejecta !== "undefined") {
this.context = this.canvas.getContext("2d");
}
this.context.scale(deviceRatio, deviceRatio);
this.context.scale(this._deviceRatio, this._deviceRatio);
} else {
@ -86,17 +120,108 @@ class Game extends Entity {
// Initialize defaults
this.lastFrameDelta = 0;
this.frameCounter = 0;
// Initialize input methods
this.inputs = {};
if (config.inputs.keyboard) {
this.inputs.keyboard = new KeyboardInput(this);
}
if (config.inputs.gamepad) {
this.inputs.gamepad = new GamepadInput(this);
}
this._game = this;
this._lastFrameTimestamp = 0;
this._lastFrameTotalRenders = 0;
this._wantPause = true;
this._fullScreenLastFrame = false;
}
setCamera (x, y) {
if (x instanceof Vector2D) {
let pos = x.clone();
pos.x = -pos.x;
pos.y = -pos.y;
this.pos = pos;
} else {
this.pos.x = -x;
this.pos.y = -y;
}
}
step (delta) {
this.frameCounter++;
this._preStep();
this._updateEntity(delta);
this._renderEntity();
this._updateInputs(); // NK: This happens at the end for reasons
}
_updateInputs () {
for (let input in this.inputs) {
if (this.inputs[input].update) {
this.inputs[input].update();
}
}
}
_preStep () {
if (this.isFullScreen) {
if (this._fullScreenLastFrame == false) {
this.canvas.style.width = `${screen.width}px`;
this.canvas.style.height = `${screen.height}px`;
if (this.fullScreenNativeResolution) {
this.canvas.width = screen.width * this.scale;
this.canvas.height = screen.height * this.scale;
this.context.scale(this._deviceRatio, this._deviceRatio);
}
}
this._fullScreenLastFrame = true;
} else {
if (this._fullScreenLastFrame == true) {
this.canvas.style.width = `${this.width}px`;
this.canvas.style.height = `${this.height}px`;
this.canvas.width = this.width * this.scale;
this.canvas.height = this.height * this.scale;
this.context.scale(this._deviceRatio, this._deviceRatio);
}
this._fullScreenLastFrame = false;
}
}
@ -108,13 +233,13 @@ class Game extends Entity {
if (self._wantPause) {
self._wantPause = false;
} else {
console.log("MomentumEngine.Game.start called, game instance is already started");
console.log("MomentumEngine.Classes.Game.start called, game instance is already started");
return false; // Game is already running
}
self._wantPause = false;
var requestFrame = (() => {
let requestFrame = (() => {
return (window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
@ -126,27 +251,26 @@ class Game extends Entity {
})();
self._lastFrameTimestamp = +(new Date());
self.startTime = self._lastFrameTimestamp;
var loop = function () {
self.frameCounter++;
let currentTimestamp = +(new Date());
self.lastFrameDelta = currentTimestamp - self._lastFrameTimestamp;
self._lastFrameTimestamp = currentTimestamp;
self.lastFrameDelta = Math.min(self.lastFrameDelta, 1000 / self.desiredFps);
if (self._wantPause) {
return;
}
self._updateEntity.bind(self);
self._updateEntity();
let currentTimestamp = +(new Date()),
delta = currentTimestamp - self._lastFrameTimestamp;
self._renderEntity.bind(self);
self._renderEntity();
if (self.fixFrameRate) {
delta = 1000 / self.desiredFps;
}
//delta = Math.min(delta, 1000 / self.desiredFps);
self._lastFrameTimestamp = currentTimestamp;
self._lastFrameTotalRenders = 0;
self.step(delta);
requestFrame(loop);
@ -164,12 +288,40 @@ class Game extends Entity {
this._wantPause = true;
return true;
} else {
console.log("MomentumEngine.Game.pause called, game instance is already paused");
console.log("MomentumEngine.Classes.Game.pause called, game instance is already paused");
return false;
}
}
toggleFullScreen () {
if (!document.mozFullScreen && !document.webkitFullScreen) {
if (this.canvas.mozRequestFullScreen) {
this.canvas.mozRequestFullScreen();
} else {
this.canvas.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
}
} else {
if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else {
document.webkitCancelFullScreen();
}
}
}
get isFullScreen () {
return document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen;
}
}

155
src/classes/gamepadinput.js Normal file
View File

@ -0,0 +1,155 @@
"use strict";
class Gamepad {
constructor (gamepadObj) {
this._gamepadObj = gamepadObj;
}
get numButtons () {
return this._gamepadObj.buttons.length;
}
get numAxis () {
return this._gamepadObj.axes.length;
}
isPressed (buttonId) {
if (this._gamepadObj.buttons[buttonId]) {
return !!this._gamepadObj.buttons[buttonId].pressed;
} else {
throw new Error(`Button ${buttonId} not found on gamepad`);
}
}
getAxis (axisId) {
if (typeof this._gamepadObj.axes[axisId] !== "undefined") {
return this._gamepadObj.axes[axisId];
} else {
throw new Error(`Axis ${axisId} not found on gamepad`);
}
}
}
class GamepadInput {
constructor () {
var self = this;
self._gamepadState = {};
self.gamepadIds = [];
if ('ongamepadconnected' in window) {
window.addEventListener("gamepadconnected", (event) => {
self._gamepadState[event.gamepad.index] = new Gamepad(event.gamepad);
self.gamepadIds.push(event.gamepad.index);
console.log(`Gamepad ${event.gamepad.index} connected`);
});
window.addEventListener("gamepaddisconnected", (event) => {
delete self._gamepadState[event.gamepad.index];
self.gamepadIds.splice(self.gamepadIds.indexOf(event.gamepad.index));
console.log(`Gamepad ${event.gamepad.index} disconnected`);
});
}
}
update () {
if (!("ongamepadconnected" in window)) {
let gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads : []);
// If there are more gamepads registered than we know about, make ourselves aware of the new ones
if (gamepads.length != this.gamepadIds.length) {
for (let i = 0; i < gamepads.length; i++) {
let gamepad = gamepads[i];
if (gamepad) {
if (this.gamepadIds.indexOf(gamepad.index) < 0) {
this._gamepadState[gamepad.index] = new Gamepad(gamepad);
this.gamepadIds.push(gamepad.index);
console.log(`Gamepad ${gamepad.index} connected`);
}
}
}
// If there is still a mismatch, then we assume some gamepads have been disconnected, so we need to remove them
if (gamepads.length != this.gamepadIds.length) {
for (let i = 0; i < this.gamepadIds.length; i++) {
let found = false;
for (let j = 0; j < gamepads.length; j++) {
let gamepad = gamepads[i];
if (gamepad && gamepad.index == this.gamepadIds[i]) {
found = true;
}
}
if (!found) {
console.log(`Gamepad ${this.gamepadIds[i]} disconnected`);
delete this._gamepadState[this.gamepadIds[i]];
this.gamepadIds.splice(this.gamepadIds.indexOf(this.gamepadIds[i]));
i--;
}
}
}
}
}
}
get numGamepads () {
return this.gamepadIds.length;
}
getGamepadById (gamepadId) {
if (this._gamepadState[gamepadId]) {
return this._gamepadState[gamepadId];
} else {
throw new Error(`Gamepad ${buttonId} is not connected`);
}
}
}
export default GamepadInput;

View File

@ -0,0 +1,52 @@
"use strict";
class ImageLoader {
constructor (src) {
this._loaded = false; // Default is true, set it to false until the image has loaded
this._error = false; // If the image fails to load, this will contain the reason
this._imageObj = new Image();
this._imageObj.addEventListener("load", () => {
this._loaded = true;
this._error = false;
});
this._imageObj.addEventListener("error", (err) => {
this._loaded = false;
this._error = err;
});
this._imageObj.src = src;
}
getImageObj () {
if (this._loaded) {
return this._imageObj;
} else {
return false;
}
}
isLoaded () {
return this._loaded;
}
isError () {
return this._error;
}
}
export default ImageLoader;

View File

@ -99,6 +99,10 @@ const KeyConsts = {
};
let wasReleased = {},
wasPressed = {};
class KeyboardInput {
@ -119,16 +123,52 @@ class KeyboardInput {
isPressed (keyCode) {
console.log("[MomentumEngine] WARNING: MomentumEngine.Classes.KeyboardInput.isPressed is deprecated. Use isDown instead.")
return !!this._keyState[keyCode];
}
isDown (keyCode) {
return !!this._keyState[keyCode];
}
wasPressed (keyCode) {
let pressed = !!wasPressed[keyCode];
if (pressed) {
wasPressed[keyCode] = false;
}
return pressed;
}
wasReleased (keyCode) {
let pressed = !!wasReleased[keyCode];
if (pressed) {
wasReleased[keyCode] = false;
}
return pressed;
}
_keyDownHandler (event) {
wasReleased[event.keyCode] = false;
wasPressed[event.keyCode] = true;
this._keyState[event.keyCode] = true;
}
_keyUpHandler (event) {
wasReleased[event.keyCode] = true;
wasPressed[event.keyCode] = false;
this._keyState[event.keyCode] = false;
}

57
src/classes/path.js Normal file
View File

@ -0,0 +1,57 @@
"use strict";
import Entity from "./entity.js";
import Vector2D from "./vector2d.js";
import CollisionMethods from "../libs/collisionmethods.js";
class Path extends Entity {
constructor (x, y, width, height, color) {
super(x, y);
if (width instanceof Array) {
color = height;
this.coords = width;
} else {
this.coords = [new Vector2D(width, height)];
}
this.color = color;
}
render () {
if (this._game) {
let ctx = this._game.context;
ctx.strokeStyle = this.color.toString();
ctx.beginPath();
ctx.moveTo(this._scaleForLeft(this.relativeLeft), this._scaleForTop(this.relativeTop));
for (let coord in this.coords) {
ctx.lineTo(this._scaleForWidth(this.relativeLeft + this.coords[coord].x), this._scaleForHeight(this.relativeTop + this.coords[coord].y));
}
//ctx.closePath();
ctx.stroke();
return true;
} else {
return false;
}
}
}
export default Path;

View File

@ -1,25 +1,42 @@
"use strict";
import Entity from "./entity.js";
import Vector2D from "./vector2d.js";
import CollisionMethods from "../libs/collisionmethods.js";
/**
* Class representing a rectangle in a scene
* @extends Entity
*/
class Rect extends Entity {
/**
* Create a rectangle
* @param {number} x - x (Left) position of the rectangle
* @param {number} y - y (Top) position of the rectangle
* @param {number} width - Width of the rectangle
* @param {number} height - Height of the rectangle
* @param {Color} color - Color of the rectangle
*/
constructor (x, y, width, height, color) {
super(x, y);
this.size = new Vector2D(width, height);
this.size.x = width || 0;
this.size.y = height || 0;
this.color = color;
}
isColliding (entity) {
/**
* Detects if the rectangle is colliding with another entity
* @param {Entity} entity - Entity to check against
* @returns {boolean} Indicates whether the entities are colliding
*/
isCollidingWith (entity) {
if (entity instanceof Rect) {
return CollisionMethods.AABB(this, entity);
@ -30,12 +47,33 @@ class Rect extends Entity {
render () {
this._recalculatePos();
if (this._game) {
this._game.context.fillStyle = this.color;
this._game.context.fillRect(this._calculatedPos.x, this._calculatedPos.y, this.size.x, this.size.y);
let left = this._scaleForLeft(this.relativeLeft),
top = this._scaleForTop(this.relativeTop),
width = this._scaleForWidth(this.width),
height = this._scaleForHeight(this.height),
ctx = this._game.context;
let xRot = left + (width / 2),
yRot = top + (height / 2);
if (this.rotation > 0) {
// Rotate the canvas based on the central point of this entity
ctx.translate(xRot, yRot);
ctx.rotate(this.rotation * Math.PI / 180);
ctx.translate(-xRot, -yRot);
}
ctx.fillStyle = this.color.toString();
ctx.fillRect(left, top, width, height);
if (this.rotation > 0) {
// Rotate back after drawing
ctx.translate(xRot, yRot);
ctx.rotate(-(this.rotation * Math.PI / 180));
ctx.translate(-xRot, -yRot);
}
return true;

79
src/classes/sprite.js Normal file
View File

@ -0,0 +1,79 @@
"use strict";
import Entity from "./entity.js";
import Vector2D from "./vector2d.js";
import ImageLoader from "./imageloader.js";
import CollisionMethods from "../libs/collisionmethods.js";
class Sprite extends Entity {
constructor (x, y, width, height, image) {
if (!image instanceof ImageLoader) {
throw new Error("MomentumEngine.Classes.Sprite must be instantiated with an ImageLoader instance");
}
super(x, y);
this.size.x = width || 0;
this.size.y = height || 0;
this._image = image;
this._imagePos = new Vector2D(0, 0);
this._imageSize = new Vector2D(0, 0);
}
setImageCoords (x, y, width, height) {
this._imagePos.x = x;
this._imagePos.y = y;
this._imageSize.x = width || 0;
this._imageSize.y = height || 0;
}
isReady () {
return (this._image.isLoaded() && !this._image.isError());
}
render () {
if (this.isReady() && this._game) {
let imageObj = this._image.getImageObj();
let subWidth = imageObj.width - this._imagePos.x,
subHeight = imageObj.height - this._imagePos.y;
this._game.context.drawImage(
imageObj,
this._imagePos.x,
this._imagePos.y,
this._imageSize.x || subWidth,
this._imageSize.y || subHeight,
this._scaleForLeft(this.relativeLeft),
this._scaleForTop(this.relativeTop),
this._scaleForWidth(this.width || subWidth),
this._scaleForHeight(this.height || subHeight)
);
return true;
} else {
return false;
}
}
}
export default Sprite;

113
src/classes/text.js Normal file
View File

@ -0,0 +1,113 @@
"use strict";
import Entity from "./entity.js";
import Vector2D from "./vector2d.js";
class Text extends Entity {
constructor (x, y, font, text) {
super(x, y);
this.font = font;
this.text = text;
this.textAlign = "start";
this.textBaseline = "alphabetic";
this.direction = "inherit";
this.letterSpacing = 0;
}
isCollidingWith (entity) {
if (entity instanceof Rect) {
return CollisionMethods.AABB(this, entity);
}
}
_renderText (text, x, y, letterSpacing, renderFunc) {
// Code modified from original by David Hong (sozonov): https://jsfiddle.net/sozonov/mg1jkz3q/
if (!text || typeof text !== "string" || text.length === 0) {
return false;
}
if (letterSpacing == 0) {
renderFunc(text, x, y);
return;
}
let characters = text.split(""),
index = 0,
current,
currentPosition = x,
align = 1;
if (this.textAlign === "right") {
characters = characters.reverse();
align = -1;
} else if (this.textAlign === "center") {
let totalWidth = 0;
for (let i = 0; i < characters.length; i++) {
// NK: We want to cache the results of measureText instead of recalculating every character every frame
totalWidth += (this._game.context.measureText(characters[i]).width + letterSpacing);
}
currentPosition = x - (totalWidth / 2);
}
while (index < text.length) {
current = characters[index++];
renderFunc(current, currentPosition, y);
// NK: We want to cache the results of measureText instead of recalculating every character every frame
currentPosition += (align * (this._game.context.measureText(current).width + letterSpacing));
}
}
render () {
if (this._game) {
this._game.context.font = `${this.font.size} ${this.font.family}`;
this._game.context.textAlign = this.textAlign;
this._game.context.textBaseline = this.textBaseline;
this._game.context.direction = this.direction;
if (this.font.fill) {
this._game.context.fillStyle = this.font.fill;
this._renderText(this.text, this._scaleForLeft(this.relativeLeft), this._scaleForTop(this.relativeTop), this.letterSpacing, this._game.context.fillText.bind(this._game.context));
}
if (this.font.stroke) {
this._game.context.strokeStyle = this.font.stroke;
this._renderText(this.text, this._scaleForLeft(this.relativeLeft), this._scaleForTop(this.relativeTop), this.letterSpacing, this._game.context.strokeText.bind(this._game.context));
}
return true;
} else {
return false;
}
}
}
export default Text;

View File

@ -101,13 +101,18 @@ class Vector2D {
}
angle () {
return Math.atan2(this.x, this.y);
}
toArray () {
return [this.x, this.y];
}
toString () {
return `[${this.x}},${this.y}}]`;
return `[${this.x},${this.y}]`;
}
@ -116,6 +121,11 @@ class Vector2D {
}
static fromAngle (angle, length) {
return new Vector2D(length * Math.cos(angle), length * Math.sin(angle));
}
}

View File

@ -1,20 +1,36 @@
"use strict";
import Game from "./classes/game.js";
import Emitter from "./classes/emitter.js";
import Field from "./classes/field.js";
import Entity from "./classes/entity.js";
import Vector2D from "./classes/vector2d.js";
import Sprite from "./classes/sprite.js";
import Rect from "./classes/rect.js";
import Path from "./classes/path.js";
import Color from "./classes/color.js";
import Text from "./classes/text.js";
import Font from "./classes/font.js";
import AudioTrack from "./classes/audio.js";
import ImageLoader from "./classes/imageloader.js";
import {KeyConsts} from "./classes/keyboardinput.js";
const Classes = {
Game: Game,
Entity: Entity,
Rect: Rect,
Vector2D: Vector2D,
Color: Color
Game,
Emitter,
Field,
Entity,
Sprite,
Rect,
Path,
Vector2D,
Color,
Text,
Font,
AudioTrack,
ImageLoader
};

View File

@ -1,20 +1,36 @@
"use strict";
import Game from "./classes/game.js";
import Emitter from "./classes/emitter.js";
import Field from "./classes/field.js";
import Entity from "./classes/entity.js";
import Vector2D from "./classes/vector2d.js";
import Sprite from "./classes/sprite.js";
import Rect from "./classes/rect.js";
import Path from "./classes/path.js";
import Color from "./classes/color.js";
import Text from "./classes/text.js";
import Font from "./classes/font.js";
import AudioTrack from "./classes/audio.js";
import ImageLoader from "./classes/imageloader.js";
import {KeyConsts} from "./classes/keyboardinput.js";
const Classes = {
Game: Game,
Entity: Entity,
Rect: Rect,
Vector2D: Vector2D,
Color: Color
Game,
Emitter,
Field,
Entity,
Sprite,
Rect,
Path,
Vector2D,
Color,
Text,
Font,
AudioTrack,
ImageLoader
};
@ -25,7 +41,7 @@ const Consts = {
};
export {
export default {
Classes,
Consts
};

View File

@ -1,5 +1,4 @@
import Rect from "../classes/rect.js";
import Vector2D from "../classes/vector2d.js";
class CollisionMethods {
@ -10,10 +9,12 @@ class CollisionMethods {
throw new Error("AABB collisions can only be checked on these entity types: Rect");
}
return (entity1.pos.x < entity2.pos.x + entity2.size.x &&
entity1.pos.x + entity1.size.x > entity2.pos.x &&
entity1.pos.y < entity2.pos.y + entity2.size.y &&
entity1.size.y + entity1.pos.y > entity2.pos.y);
let colliding = (entity1.left < entity2.left + entity2.width &&
entity1.left + entity1.width > entity2.left &&
entity1.top < entity2.top + entity2.height &&
entity1.height + entity1.top > entity2.top);
return colliding;
}

20
src/libs/utils.js Normal file
View File

@ -0,0 +1,20 @@
class Utils {
static mergeIntoArray (dest, source) {
source.forEach((item) => {
if (dest.indexOf(item) < 0) {
dest.push(item);
}
});
return dest;
}
}
export default Utils;