├── README.md ├── TODO.md ├── assets └── images │ └── ocelot.png ├── lib ├── assets.js ├── canvas.js ├── components │ ├── light.js │ ├── shape.js │ ├── sprite.js │ ├── text.js │ ├── tilemap.js │ └── transform.js ├── entities.js ├── game.js ├── stage.js ├── time.js ├── tween.js ├── utils.js └── utils │ ├── color.js │ ├── easing.js │ ├── effects.js │ ├── math.js │ └── random.js └── package.json /README.md: -------------------------------------------------------------------------------- 1 | # Ocelot 2 | 3 | A minimalist HTML5 2D game engine. Created specifically for the [Js13kGames][js13k] competition, Ocelot aims for simplicity and small fizesize. 4 | 5 | ![Ocelot](https://raw.githubusercontent.com/geoffb/ocelot/master/assets/images/ocelot.png) 6 | 7 | ## Features 8 | 9 | * Entity/component 10 | * Tweening 11 | * Asset loading 12 | 13 | ## License 14 | 15 | The MIT License (MIT) 16 | 17 | Copyright (c) 2015 Geoff Blair 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy 20 | of this software and associated documentation files (the "Software"), to deal 21 | in the Software without restriction, including without limitation the rights 22 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 23 | copies of the Software, and to permit persons to whom the Software is 24 | furnished to do so, subject to the following conditions: 25 | 26 | The above copyright notice and this permission notice shall be included in all 27 | copies or substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | SOFTWARE. 36 | 37 | [js13k]: http://js13kgames.com/ 38 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | * Audio - Use WebAudioAPI to play a sequence of notes 4 | * Mouse/touch input 5 | * "Camera" - Render entities only within the camera (take into account optimized tilemap rendering) 6 | * More easings 7 | * Active/inactive flag for entities 8 | -------------------------------------------------------------------------------- /assets/images/ocelot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoffb/ocelot/70b8ab73f3ad035819d948c50e3cae1c20443dfc/assets/images/ocelot.png -------------------------------------------------------------------------------- /lib/assets.js: -------------------------------------------------------------------------------- 1 | var assets = {}; 2 | 3 | exports.load = function (assetPaths, callback) { 4 | var loaded = 0; 5 | for (var i = 0; i < assetPaths.length; ++i) { 6 | var path = assetPaths[i]; 7 | var img = new Image(); 8 | img.src = path; 9 | img.onload = function () { 10 | loaded++; 11 | if (loaded >= assetPaths.length) { 12 | callback && callback(); 13 | } 14 | }; 15 | assets[path] = img; 16 | } 17 | }; 18 | 19 | exports.get = function (assetPath) { 20 | return assets[assetPath]; 21 | }; 22 | -------------------------------------------------------------------------------- /lib/canvas.js: -------------------------------------------------------------------------------- 1 | exports.create = function (width, height) { 2 | var canvas = document.createElement("canvas"); 3 | canvas.width = width; 4 | canvas.height = height; 5 | return canvas; 6 | }; 7 | -------------------------------------------------------------------------------- /lib/components/light.js: -------------------------------------------------------------------------------- 1 | var canvas = require("../canvas"); 2 | var color = require("../utils/color"); 3 | 4 | var buffer = canvas.create(1, 1); 5 | var bctx = buffer.getContext("2d"); 6 | 7 | exports.render = function (ctx) { 8 | var light = this.light; 9 | var r = light.radius; 10 | var size = r * 2; 11 | buffer.width = size; 12 | buffer.height = size; 13 | var gradient = bctx.createRadialGradient(r, r, 0, r, r, r); 14 | gradient.addColorStop(0, color.rgba(light.color, light.intensity)); 15 | gradient.addColorStop(1, color.rgba(light.color, 0)); 16 | bctx.fillStyle = gradient; 17 | bctx.fillRect(0, 0, size, size); 18 | ctx.save(); 19 | ctx.globalCompositeOperation = "lighter"; 20 | ctx.drawImage(buffer, -r, -r); 21 | ctx.restore(); 22 | }; 23 | -------------------------------------------------------------------------------- /lib/components/shape.js: -------------------------------------------------------------------------------- 1 | var renderers = { 2 | rect: function (ctx, shape) { 3 | var x = -Math.round(shape.width / 2); 4 | var y = -Math.round(shape.height / 2); 5 | ctx.fillRect(x, y, shape.width, shape.height); 6 | ctx.strokeRect(x, y, shape.width, shape.height); 7 | }, 8 | arc: function (ctx, shape) { 9 | ctx.beginPath(); 10 | ctx.arc(0, 0, shape.radius, 0, Math.PI * 2); 11 | ctx.fill(); 12 | ctx.stroke(); 13 | } 14 | }; 15 | 16 | exports.render = function (ctx) { 17 | var shape = this.shape; 18 | ctx.fillStyle = shape.fill; 19 | ctx.strokeStyle = shape.stroke; 20 | ctx.lineWidth = shape.lineWidth || 1; 21 | renderers[shape.type](ctx, shape); 22 | }; 23 | -------------------------------------------------------------------------------- /lib/components/sprite.js: -------------------------------------------------------------------------------- 1 | var assets = require("../assets"); 2 | 3 | exports.render = function (ctx) { 4 | var sprite = this.sprite; 5 | var size = sprite.size; 6 | var image = assets.get(sprite.image); 7 | var width = Math.floor(image.width / size); 8 | var sx = sprite.index % width; 9 | var sy = Math.floor(sprite.index / width); 10 | ctx.drawImage( 11 | image, 12 | sx * size, sy * size, size, size, 13 | -size / 2, -size / 2, size, size 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /lib/components/text.js: -------------------------------------------------------------------------------- 1 | exports.render = function (ctx) { 2 | var text = this.text; 3 | ctx.font = text.size + "px " + text.font; 4 | ctx.fillStyle = text.fill; 5 | ctx.textAlign = "center"; 6 | ctx.textBaseline = "middle"; 7 | ctx.fillText(text.text, 0, 0); 8 | }; 9 | -------------------------------------------------------------------------------- /lib/components/tilemap.js: -------------------------------------------------------------------------------- 1 | var assets = require("../assets"); 2 | 3 | exports.render = function (ctx, camera) { 4 | // Tilemap rendering is very optimized and 5 | // requires that transform.space === "camera" 6 | var tilemap = this.tilemap; 7 | var image = assets.get(tilemap.image); 8 | var map = tilemap.map; 9 | var size = tilemap.size; 10 | var width = Math.floor(image.width / size); 11 | 12 | var originX = Math.floor(camera.x / size); 13 | var originY = Math.floor(camera.y / size); 14 | var terminusX = Math.ceil((camera.x + camera.width) / size); 15 | var terminusY = Math.ceil((camera.y + camera.height) / size); 16 | var offsetX = -camera.x % size; 17 | var offsetY = -camera.y % size; 18 | 19 | for (var y = originY; y <= terminusY; ++y) { 20 | if (y < 0 || y >= map.length) { continue; } 21 | var row = map[y]; 22 | for (var x = originX; x <= terminusX; ++x) { 23 | if (x < 0 || x >= row.length) { continue; } 24 | var index = row[x]; 25 | var sx = index % width; 26 | var sy = Math.floor(index / width); 27 | var dx = (x - originX) * size + offsetX; 28 | var dy = (y - originY) * size + offsetY; 29 | ctx.drawImage( 30 | image, 31 | sx * size, sy * size, size, size, 32 | dx, dy, size, size 33 | ); 34 | } 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /lib/components/transform.js: -------------------------------------------------------------------------------- 1 | var entities = require("../entities"); 2 | 3 | // exports.add = function () { 4 | // var transform = this.transform; 5 | // var children = transform.children; 6 | // if (children && children.length) { 7 | // for (var i = 0; i < children.length; ++i) { 8 | // var child = children[i]; 9 | // child.transform.parent = this; 10 | // entities.add(child); 11 | // } 12 | // } 13 | // }; 14 | 15 | exports.render = function (ctx, camera) { 16 | var transform = this.transform; 17 | var cameraSpace = transform.space === "camera"; 18 | var offsetX = cameraSpace ? 0 : -camera.x; 19 | var offsetY = cameraSpace ? 0 : -camera.y; 20 | var x = Math.round(transform.x + offsetX); 21 | var y = Math.round(transform.y + offsetY); 22 | ctx.translate(x, y); 23 | ctx.rotate(transform.r); 24 | ctx.scale(transform.sx, transform.sy); 25 | }; 26 | -------------------------------------------------------------------------------- /lib/entities.js: -------------------------------------------------------------------------------- 1 | var utils = require("./utils"); 2 | 3 | var components = {}; 4 | var prefabs = {}; 5 | 6 | var entities = []; 7 | 8 | exports.init = function (componentData, prefabData) { 9 | components = componentData; 10 | prefabs = prefabData; 11 | }; 12 | 13 | exports.get = function () { 14 | return entities; 15 | }; 16 | 17 | exports.trigger = function (entity, method, args) { 18 | for (var key in entity) { 19 | var component = components[key]; 20 | if (!component || !component[method]) { continue; } 21 | component[method].apply(entity, args); 22 | } 23 | }; 24 | 25 | exports.triggerAll = function (method, args) { 26 | for (var i = 0; i < entities.length; ++i) { 27 | exports.trigger(entities[i], method, args); 28 | } 29 | }; 30 | 31 | exports.add = function (entity) { 32 | entities.push(entity); 33 | // exports.trigger(entity, "add"); 34 | return entity; 35 | }; 36 | 37 | exports.spawn = function (key) { 38 | // TODO: Handle missing prefab key 39 | var entity = utils.clone(prefabs[key]); 40 | return exports.add(entity); 41 | }; 42 | -------------------------------------------------------------------------------- /lib/game.js: -------------------------------------------------------------------------------- 1 | var stage = require("./stage"); 2 | var time = require("./time"); 3 | var entities = require("./entities"); 4 | var tween = require("./tween"); 5 | 6 | var keys = {}; 7 | 8 | var scenes = {}; 9 | var activeScene = null; 10 | 11 | var camera = { 12 | x: 0, y: 0, width: 0, height: 0 13 | }; 14 | 15 | var followTarget = null; 16 | 17 | var update = function (dt) { 18 | activeScene.update(dt, keys); 19 | entities.triggerAll("update", [dt, keys]); 20 | tween.update(dt); 21 | 22 | if (followTarget) { 23 | camera.x = Math.round(followTarget.transform.x - camera.width / 2); 24 | camera.y = Math.round(followTarget.transform.y - camera.height / 2); 25 | } 26 | 27 | stage.render(camera); 28 | }; 29 | 30 | var keydown = function (e) { 31 | keys[e.keyCode] = true; 32 | }; 33 | 34 | var keyup = function (e) { 35 | keys[e.keyCode] = false; 36 | }; 37 | 38 | exports.init = function (config) { 39 | camera.width = config.width; 40 | camera.height = config.height; 41 | stage.init(camera.width, camera.height); 42 | scenes = config.scenes; 43 | entities.init(config.components, config.prefabs); 44 | 45 | window.addEventListener("keydown", keydown, false); 46 | window.addEventListener("keyup", keyup, false); 47 | }; 48 | 49 | exports.loadScene = function (key) { 50 | activeScene = scenes[key]; 51 | activeScene.start(); 52 | }; 53 | 54 | exports.start = function () { 55 | time.start(update); 56 | }; 57 | 58 | exports.setCameraFollowTarget = function (entity) { 59 | followTarget = entity; 60 | }; 61 | -------------------------------------------------------------------------------- /lib/stage.js: -------------------------------------------------------------------------------- 1 | var canvas = require("./canvas"); 2 | var entities = require("./entities"); 3 | 4 | var stage; 5 | var ctx; 6 | 7 | var resize = function () { 8 | var clientWidth = window.innerWidth; 9 | var clientHeight = window.innerHeight; 10 | var ratioX = clientWidth / stage.width; 11 | var ratioY = clientHeight / stage.height; 12 | var scale = Math.min(ratioX, ratioY); 13 | 14 | var style = stage.style; 15 | style.position = "absolute"; 16 | style.transformOrigin = "0 0"; 17 | style.transform = "scale(" + scale + "," + scale + ")"; 18 | style.left = Math.round(clientWidth / 2 - (stage.width * scale) / 2) + "px"; 19 | style.top = Math.round(clientHeight / 2 - (stage.height * scale) / 2) + "px"; 20 | }; 21 | 22 | exports.init = function (width, height) { 23 | stage = canvas.create(width, height); 24 | document.body.appendChild(stage); 25 | ctx = stage.getContext("2d"); 26 | ctx.imageSmoothingEnabled = false; 27 | resize(); 28 | window.addEventListener("resize", resize, false); 29 | }; 30 | 31 | exports.clear = function (fill) { 32 | ctx.fillStyle = fill; 33 | ctx.fillRect(0, 0, stage.width, stage.height); 34 | }; 35 | 36 | exports.render = function (camera) { 37 | this.clear("dodgerblue"); 38 | 39 | var list = entities.get(); 40 | for (var i = 0; i < list.length; ++i) { 41 | var entity = list[i]; 42 | ctx.save(); 43 | entities.trigger(entity, "render", [ctx, camera]); 44 | ctx.restore(); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /lib/time.js: -------------------------------------------------------------------------------- 1 | var threshold = 100; 2 | var last = 0; 3 | var onUpdate; 4 | 5 | var update = function (time) { 6 | delta = time - last; 7 | last = time; 8 | if (delta <= threshold && onUpdate) { 9 | onUpdate(delta); 10 | } 11 | requestAnimationFrame(update); 12 | }; 13 | 14 | exports.start = function (callback) { 15 | onUpdate = callback; 16 | update(0); 17 | }; 18 | -------------------------------------------------------------------------------- /lib/tween.js: -------------------------------------------------------------------------------- 1 | var easing = require("./utils/easing"); 2 | var math = require("./utils/math"); 3 | 4 | var tweens = []; 5 | 6 | exports.create = function (target, to, duration, delay, ease) { 7 | var from = {}; 8 | for (var key in to) { 9 | from[key] = target[key]; 10 | } 11 | tweens.push({ 12 | target: target, 13 | from: from, 14 | to: to, 15 | duration: duration, 16 | delay: delay || 0, 17 | ease: ease || "linear", 18 | elapsed: 0 19 | }); 20 | }; 21 | 22 | exports.update = function (dt) { 23 | for (var i = tweens.length - 1; i >= 0; --i) { 24 | var tween = tweens[i]; 25 | if (tween.delay > 0) { 26 | tween.delay -= dt; 27 | } else { 28 | tween.elapsed = Math.min(tween.elapsed + dt, tween.duration); 29 | var normal = tween.elapsed / tween.duration; 30 | for (var key in tween.to) { 31 | tween.target[key] = math.lerp( 32 | tween.from[key], 33 | tween.to[key], 34 | easing[tween.ease](normal) 35 | ); 36 | } 37 | if (tween.elapsed >= tween.duration) { 38 | tweens.splice(i, 1); 39 | } 40 | } 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | exports.clone = function (obj) { 2 | return JSON.parse(JSON.stringify(obj)); 3 | }; 4 | -------------------------------------------------------------------------------- /lib/utils/color.js: -------------------------------------------------------------------------------- 1 | var colors = {}; 2 | 3 | exports.define = function (data) { 4 | colors = data; 5 | }; 6 | 7 | exports.rgba = function (key, a) { 8 | var c = colors[key]; 9 | return "rgba(" + c.join(",") + "," + a + ")"; 10 | }; 11 | -------------------------------------------------------------------------------- /lib/utils/easing.js: -------------------------------------------------------------------------------- 1 | exports.linear = function (k) { 2 | return k; 3 | }; 4 | 5 | exports.quadIn = function (k) { 6 | return k * k; 7 | }; 8 | 9 | exports.quadOut = function (k) { 10 | return k * (2 - k); 11 | }; 12 | 13 | exports.quadInOut = function (k) { 14 | return k < 0.5 ? exports.quadIn(k) : exports.quadOut(k); 15 | }; 16 | 17 | exports.sineIn = function (k) { 18 | if (k === 1) { return 1; } 19 | return 1 - Math.cos(k * Math.PI / 2); 20 | }; 21 | 22 | exports.sineOut = function (k) { 23 | return Math.sin(k * Math.PI / 2); 24 | }; 25 | 26 | exports.sineInOut = function (k) { 27 | return 0.5 * (1 - Math.cos(Math.PI * k)); 28 | }; 29 | -------------------------------------------------------------------------------- /lib/utils/effects.js: -------------------------------------------------------------------------------- 1 | var tween = require("../tween"); 2 | 3 | exports.pulse = function (entity) { 4 | var transform = entity.transform; 5 | tween.create(transform, { 6 | sx: 1.25, 7 | sy: 1.25 8 | }, 125); 9 | tween.create(transform, { 10 | sx: 1, 11 | sy: 1 12 | }, 125, 125); 13 | }; 14 | 15 | exports.negate = function (entity) { 16 | // TODO: Param for distance, duration, number of shakes 17 | var transform = entity.transform; 18 | tween.create(transform, { 19 | x: transform.x - 1 20 | }, 50); 21 | tween.create(transform, { 22 | x: transform.x + 1 23 | }, 100, 50); 24 | tween.create(transform, { 25 | x: transform.x 26 | }, 50, 150); 27 | }; 28 | -------------------------------------------------------------------------------- /lib/utils/math.js: -------------------------------------------------------------------------------- 1 | exports.tau = Math.PI * 2; 2 | 3 | exports.lerp = function (a, b, t) { 4 | return a + ((b - a) * t); 5 | }; 6 | -------------------------------------------------------------------------------- /lib/utils/random.js: -------------------------------------------------------------------------------- 1 | exports.chance = function (chance) { 2 | return Math.random() <= chance; 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ocelot", 3 | "version": "0.0.0", 4 | "decription": "Minimalist HTML5 2D game engine", 5 | "keywords": ["game", "engine", "2d", "canvas", "html5", "browser"], 6 | "author": { 7 | "name": "Geoff Blair", 8 | "email": "geoff@lostdecadegames.com" 9 | }, 10 | "repository": "geoffb/ocelot", 11 | "bugs": "https://github.com/geoffb/ocelot/issues", 12 | "license": "MIT", 13 | "main": "lib/game", 14 | "files": [ 15 | "lib" 16 | ] 17 | } 18 | --------------------------------------------------------------------------------