diff --git a/js/lores-animator.js b/js/lores-animator.js new file mode 100644 index 0000000..18328ad --- /dev/null +++ b/js/lores-animator.js @@ -0,0 +1,119 @@ +// Lores.js - animator module +// (c) Ondrej Hruska 2013 +// www.ondrovo.com + +// MIT license + + +/* Animation manager */ +function LoresAnimator(opts) { + this.initAnim = opts.init || function(){}; + this.animFrame = opts.frame || function(){}; + this.fps = opts.fps || 40; + + this.started = false; + this.paused = false; + this.halted = false; +}; + + +LoresAnimator.prototype.start = function() { + this.initAnim(); + + this.started = true; + this.paused = false; + this.halted = false; + + this._requestFrame(); +}; + + +LoresAnimator.prototype.stop = function() { + var self = this; + + this.started = false; + this.halted = true; +}; + + +LoresAnimator.prototype.isPaused = function() { + return this.started && this.paused && !this.halted; +}; + + +LoresAnimator.prototype.isRunning = function() { + return this.started && !this.paused && !this.halted; +}; + + +LoresAnimator.prototype.toggle = function() { + var self = this; + + if(!this.started || this.halted) { + throw "Invalid state for toggle()"; + } + + if(this.paused) { + this.resume(); + } else { + this.pause(); + } +}; + + +LoresAnimator.prototype.pause = function() { + var self = this; + + if(!this.started || this.halted) { + throw "Invalid state for pause()"; + } + + this.paused = true; + this.halted = false; +}; + + +LoresAnimator.prototype.setFps = function(fps) { + + this.fps = fps; +}; + + +LoresAnimator.prototype.resume = function() { + var self = this; + + if(!this.started || !this.paused || this.halted) { + throw "Invalid state for resume()"; + } + + this.paused = false; + this.halted = false; + + this._requestFrame(); +}; + + +LoresAnimator.prototype.step = function(timestamp) { + var self = this; + + if(!this.started) { + throw "Invalid state for step()"; + } + + if(this.halted || this.paused) return; + + setTimeout(function() { + self._requestFrame(); + self.animFrame(timestamp); + }, 1000 / self.fps); +}; + + +LoresAnimator.prototype._requestFrame = function() { + + if(this.halted || this.paused) return; + + var self = this; + window.requestAnimationFrame(function(time){self.step(time)}); +}; + diff --git a/js/lores-base.js b/js/lores-base.js new file mode 100644 index 0000000..f9f0b94 --- /dev/null +++ b/js/lores-base.js @@ -0,0 +1,847 @@ +// Lores.js - base module +// (c) Ondrej Hruska 2013 +// www.ondrovo.com + +// MIT license + + +/* Display module with mouse input */ +function LoresDisplay(element, options) { + + var self = this; + + this.wrapper = $(element); + options = options || {}; + options.width = options.width || $(element).width(); + options.height = options.height || $(element).height(); + options.cols = options.cols || 16; + options.rows = options.rows || 16; + options.bg = options.bg || 0; + options.fg = options.fg || 1; + + options.colors = new LoresPalette(options.colors) || new LoresPalette(); + + options.filter = options.filter || new LoresColorFilter(); + + this.colors = options.colors; + this.filter = options.filter; + + this.width = options.width; + this.height = options.height; + + this.rows = options.rows; + this.cols = options.cols; + + this.pixelWidth = (this.width/this.cols); + this.pixelHeight = (this.height/this.rows); + + this.bg = options.bg; + this.fg = options.fg; + + + // build a canvas + //this.wrapper.empty(); + + this.canvas = $('') + .css({position: "absolute", left: 0, top: 0}) + .attr({"width": this.width, "height": this.height}) + .appendTo(this.wrapper); + + this.context = this.canvas[0].getContext('2d'); + + this.erase(true); + + + // mouse input + + // undefined if none + this.moveHandler = options.moveHandler; + this.rawMoveHandler = options.rawMoveHandler; + + this.clickHandler = options.clickHandler; + this.rawClickHandler = options.rawClickHandler; + + this.mouseDown = false; + + this.lastMousePos = {x:-1,y:-1,outside:true}; + this.lastMousePosRaw = {x:-1,y:-1,outside:true}; + + + // add click handler + $(this.canvas).on('click', function(evt) { + var pos = self._getMousePos(self.canvas, evt); + + if(self.rawClickHandler) { + var pixel = { + x: pos.x, + y: pos.y, + outside: false, + }; + + self.rawClickHandler(pixel, self); + } + + if(self.clickHandler) { + + var pixel = { + x: Math.floor(pos.x / self.pixelWidth), + y: Math.floor(pos.y / self.pixelHeight), + }; + + self.clickHandler(pixel, self); + } + }); + + + // add move handler + $(window).on('mousemove', function(evt) { + var pos = self._getMousePos(self.canvas, evt); + + + if(self.rawMoveHandler) { + var pixel = { + x: pos.x, + y: pos.y, + outside: false, + }; + + if(pixel.x < 0 || pixel.x >= self.width || pixel.y < 0 || pixel.y >= self.height) { + pixel.outside = true; + } + + self.rawMoveHandler(pixel, self); + self.lastMousePosRaw = pixel; + } + + if(self.moveHandler) { + var pixel = { + x: Math.floor(pos.x / self.pixelWidth), + y: Math.floor(pos.y / self.pixelHeight), + outside: false, + }; + + if(pixel.x < 0 || pixel.x >= self.cols || pixel.y < 0 || pixel.y >= self.rows) { + pixel.outside = true; + } + + if(self.lastMousePos.x != pixel.x || self.lastMousePos.y != pixel.y) { + self.moveHandler(pixel, self.lastMousePos, self); + self.lastMousePos = pixel; + } + } + }); + + + $(window).on('mousedown', function(evt) { + self.mouseDown = true; + }); + + + $(window).on('mouseup', function(evt) { + self.mouseDown = false; + }); +}; + + +LoresDisplay.prototype.erase = function(fillWithBg) { + + if(fillWithBg) { + this.fill(this.bg); + } else { + this.context.clearRect(0,0,this.width,this.height); + } +}; + + +LoresDisplay.prototype.fill = function(color) { + + color = this.resolveColor(color); + + if(color == -1) { + color = this.resolveColor(this.bg); + } + + if(color == -1) { + this.erase(false); + } else { + this.context.fillStyle = color; + this.context.fillRect(0,0,this.width,this.height); + } +}; + + +LoresDisplay.prototype.getCanvas = function() { + return this.canvas; +}; + + +LoresDisplay.prototype.getContext = function() { + return this.context; +}; + + +LoresDisplay.prototype.getPalette = function() { + return this.colors; +}; + + +LoresDisplay.prototype.setPalette = function(newPalette) { + this.colors = newPalette; +}; + + +LoresDisplay.prototype.getColorFilter = function() { + return this.filter; +}; + + +LoresDisplay.prototype.setColorFilter = function(newFilter) { + this.filter = newFilter; +}; + + +LoresDisplay.prototype.addFilter = function(from, to) { + this.filter.addFilter(from, to); +}; + + +LoresDisplay.prototype.removeFilter = function(color) { + this.filter.removeFilter(color); +}; + + +LoresDisplay.prototype.getPixelSize = function() { + return { + x: this.pixelWidth, + w: this.pixelWidth, + y: this.pixelHeight, + h: this.pixelHeight, + }; +}; + + +LoresDisplay.prototype.resolveColor = function(color) { + + if(color === undefined || color === null || color === "") { + throw "Null color"; + } else if(typeof color == "boolean") { + color = color ? this.fg : this.bg; + } else if(color == "transparent") { + color = -1; + } + + color = this.filter.process(color); + + + if(typeof color == "number") { + + if(color == -1) return -1; // alpha = bg + + var color = this.getColor(color); + + if(color === undefined) { + throw "Undefined color id '" + JSON.stringify(color) + "'"; + } + } + + + return color; +}; + + +// alias +LoresDisplay.prototype.setColor = function(index, color) { + this.colors.add(index, color); +}; + + +LoresDisplay.prototype.addColor = function(index, color) { + this.colors.add(index, color); +}; + + +LoresDisplay.prototype.removeColor = function(index) { + this.colors.remove(index, color); +}; + + +LoresDisplay.prototype.hasColor = function(index) { + return index == -1 || this.colors.has(index); +}; + + +LoresDisplay.prototype.getColor = function(index) { + return this.colors.get(index); +}; + + +LoresDisplay.prototype.setBg = function(color) { + this.bg = color; +}; + + +LoresDisplay.prototype.erasePixel = function(x, y) { + this.setPixel(x, y, -1); +}; + + +LoresDisplay.prototype.setPixel = function(x, y, color) { + + color = this.resolveColor(color); + + if(color == -1) { + color = this.resolveColor(this.bg); + } + + x = Math.floor(x); + y = Math.floor(y); + + var x1 = x * this.pixelWidth; + var y1 = y * this.pixelHeight; + + if(color == -1) { + this.context.clearRect(x1, y1, this.pixelWidth, this.pixelHeight); + } else { + this.context.fillStyle = color; + this.context.fillRect(x1, y1, this.pixelWidth, this.pixelHeight); + } +}; + + +/* moveHandler(display, pos, lastPos) */ +LoresDisplay.prototype.setMoveHandler = function(handler) { + this.moveHandler = handler; +}; + + +/* rawMoveHandler(display, pos, lastPos) */ +LoresDisplay.prototype.setRawMoveHandler = function(handler) { + this.rawMoveHandler = handler; +}; + + +/* clickHandler(display, pos) */ +LoresDisplay.prototype.setClickHandler = function(handler) { + this.clickHandler = handler; +}; + + +LoresDisplay.prototype.isMouseDown = function() { + return this.mouseDown; +}; + + +LoresDisplay.prototype.resetMouse = function() { + this.mouseDown = false; +}; + + +LoresDisplay.prototype._getMousePos = function(canvas, event) { + var rect = canvas[0].getBoundingClientRect(); + return { + x: event.clientX - rect.left, + y: event.clientY - rect.top + }; +}; + + +LoresDisplay.prototype.getWidth = function() { + return this.cols; +}; + + +LoresDisplay.prototype.getHeight = function() { + return this.rows; +}; + + +LoresDisplay.prototype.drawMap = function(map, x, y) { + + for(var yy = 0; yy= this.rows) break; + if(y+yy < 0) continue; + + for(var xx = 0; xx= this.cols) break; + if(x+xx < 0) continue; + + this.setPixel(x+xx, y+yy, color); + } + } + +}; + + +LoresDisplay.prototype.getMap = function() { + var map = new LoresPixmap(this.cols, this.rows, this.bg); + map.connectedDisplay = this; + return map; +}; + + + + +/* Color table */ +function LoresPalette(values) { + this.table = { + 0: "#000000", + 1: "#00ff00", + }; + + this.table = $.extend( this.table, values ); +}; + + +// alias +LoresPalette.prototype.set = function(index, color) { + this.add(index, color); +}; + + +LoresPalette.prototype.add = function(index, color) { + this.table[index] = color; +}; + + +LoresPalette.prototype.remove = function(index) { + delete this.table[index]; +}; + + +LoresPalette.prototype.has = function(index) { + return this.table[index] !== undefined; +}; + + +LoresPalette.prototype.get = function(index) { + return this.table[index]; +}; + + + + +/* Pixel map */ +function LoresPixmap(width, height, fill) { + + this.doAutoFlush = true; + this.connectedDisplay = null; + if(fill == undefined) fill = -1; + this.bg = fill; + + if(width === undefined || height === undefined) return; + + this.width = width; + this.height = height; + + this.map = []; + + for(var y=0; y= this.width || x < 0 || y >= this.height || y < 0 ) return; + + if(this.map[y][x] == color) return; // no need to overwrite it + + this.map[y][x] = color; + + if(color != -1 && this.doAutoFlush && this.connectedDisplay !== null) { + this.connectedDisplay.setPixel(x,y,color); + } +}; + + +LoresPixmap.prototype.getPixel = function(x,y) { + if(x >= this.width || x < 0 || y >= this.height || y < 0 ) return -1; + + return this.map[y][x]; +}; + + +LoresPixmap.prototype.getArray = function() { + return this.map; +}; + + +LoresPixmap.prototype.setArray = function(dataArray) { + this.width = dataArray[0].length; + this.height = dataArray.length; + + this.map = dataArray; +}; + + +LoresPixmap.prototype.getWidth = function() { + return this.width; +}; + + +LoresPixmap.prototype.getHeight = function() { + return this.height; +}; + + +LoresPixmap.prototype.drawMap = function(otherMap, x, y) { + + for(var yy = 0; yy < otherMap.getHeight(); yy++) { + for(var xx = 0; xx < otherMap.getWidth(); xx++) { + + var color = otherMap.getPixel(xx,yy); + if(color == -1) continue; // transparent = no draw + this.setPixel(x+xx, y+yy, color); + + } + } +}; + + +LoresPixmap.prototype.eraseMap = function(otherMap, x, y) { + + for(var yy = 0; yy < otherMap.getHeight(); yy++) { + for(var xx = 0; xx < otherMap.getWidth(); xx++) { + + var color = otherMap.getPixel(xx,yy); + if(color == -1) continue; // transparent = no draw + this.setPixel(x+xx, y+yy, this.bg); + + } + } +}; + + +LoresPixmap.prototype.eraseRect = function(x, y, w, h) { + + for(var yy = 0; yy < h; yy++) { + for(var xx = 0; xx < w; xx++) { + + this.setPixel(x+xx, y+yy, this.bg); + } + } +}; + + +LoresPixmap.prototype.fillRect = function(x, y, w, h, color) { + + for(var yy = 0; yy < h; yy++) { + for(var xx = 0; xx < w; xx++) { + + this.setPixel(x+xx, y+yy, color); + } + } +}; + + +LoresPixmap.prototype.flush = function() { + if(this.connectedDisplay == null) { + throw "Cannot flush map without a connected display."; + } + + this.connectedDisplay.drawMap(this, 0, 0); +}; + + +LoresPixmap.prototype.autoFlush = function(state) { + this.doAutoFlush = state; +}; + + + + +/* Color filter + * + * Used to translate color codes when + * writing from a pixmap to display + */ +function LoresColorFilter(translations) { + this.table = translations || {}; +} + + +LoresColorFilter.prototype.process = function(color) { + color = (this.table[color] !== undefined) ? this.table[color] : color; + return color; +}; + + +LoresColorFilter.prototype.addFilter = function(from, to) { + this.table[from] = to; +}; + + +LoresColorFilter.prototype.removeFilter = function(color) { + delete this.table[from]; +}; + + + + +/* Keyboard input handler */ +function LoresKeyboard() { + + this.states = {}; + + this.pressHandlers = {}; + this.downHandlers = {}; + this.upHandlers = {}; + + var self = this; + + $(window).on("keydown", function(event) { + self.states[event.which] = true; + self.downHandlers[event.which] && self.downHandlers[event.which](event.which); + }); + + $(window).on("keyup", function(event) { + self.states[event.which] = false; + self.upHandlers[event.which] && self.upHandlers[event.which](event.which); + }); + + $(window).on("keypress", function(event) { + self.pressHandlers[event.which] && self.pressHandlers[event.which](event.which); + }); + +}; + +LoresKeyboard.DELETE = 46; +LoresKeyboard.BACKSPACE = 8; +LoresKeyboard.SPACE = 32; +LoresKeyboard.ENTER = 13; +LoresKeyboard.ESC = 27; + +LoresKeyboard.LEFT = 37; +LoresKeyboard.RIGHT = 39; +LoresKeyboard.UP = 38; +LoresKeyboard.DOWN = 40; + +LoresKeyboard.CTRL = 17; +LoresKeyboard.SHIFT = 16; +LoresKeyboard.META = 91; +LoresKeyboard.INSERT = 45; +LoresKeyboard.PAGEUP = 33; +LoresKeyboard.PAGEDOWN = 34; +LoresKeyboard.HOME = 36; +LoresKeyboard.END = 35; + +LoresKeyboard.A = 65; +LoresKeyboard.B = 66; +LoresKeyboard.C = 67; +LoresKeyboard.D = 68; +LoresKeyboard.E = 69; +LoresKeyboard.F = 70; +LoresKeyboard.G = 71; +LoresKeyboard.H = 72; +LoresKeyboard.I = 73; +LoresKeyboard.J = 74; +LoresKeyboard.K = 75; +LoresKeyboard.L = 76; +LoresKeyboard.M = 77; +LoresKeyboard.N = 78; +LoresKeyboard.O = 79; +LoresKeyboard.P = 80; +LoresKeyboard.Q = 81; +LoresKeyboard.R = 82; +LoresKeyboard.S = 83; +LoresKeyboard.T = 84; +LoresKeyboard.U = 85; +LoresKeyboard.V = 86; +LoresKeyboard.W = 87; +LoresKeyboard.X = 88; +LoresKeyboard.Y = 89; +LoresKeyboard.Z = 90; + +LoresKeyboard.NUM_0 = 96; +LoresKeyboard.NUM_1 = 97; +LoresKeyboard.NUM_2 = 98; +LoresKeyboard.NUM_3 = 99; +LoresKeyboard.NUM_4 = 100; +LoresKeyboard.NUM_5 = 101; +LoresKeyboard.NUM_6 = 102; +LoresKeyboard.NUM_7 = 103; +LoresKeyboard.NUM_8 = 104; +LoresKeyboard.NUM_9 = 105; + +LoresKeyboard.LETTERS = [65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90]; +LoresKeyboard.ASDW = [65,83,68,87]; +LoresKeyboard.ARROWS = [37,38,39,40]; +LoresKeyboard.NUMBERS = [96,97,98,99,100,101,102,103,104,105]; + + + +LoresKeyboard.prototype.isDown = function(keycode) { + var val = this.states[keycode]; + if(val == undefined) val = false; + return val; +}; + + +LoresKeyboard.prototype.resetKeys = function() { + this.states = {}; +}; + + +LoresKeyboard.prototype.setPressHandler = function(keycode, handler) { + + if(keycode.constructor == Array) { + + for(var i=0; i= 0; i--) { + if (this.sprites[i] == sprite) this.sprites.splice(i, 1); + } +}; + + +LoresSpritePool.prototype.getSprites = function() { + return this.sprites; +}; + + +LoresSpritePool.prototype.getColliding = function(sprite, pixelPerfect) { + var results = []; + for(var i in this.sprites) { + var spr = this.sprites[i]; + if(spr === sprite) continue; + if(spr.collidesWith(sprite, pixelPerfect)) { + results.push(spr); + } + } + return results; +}; + + +LoresSpritePool.prototype.eraseMap = function() { + if(this.drawingMap == undefined) throw "LoresSpritePool has no map."; + + this.drawingMap.erase(); +}; + + +LoresSpritePool.prototype.drawOnMap = function() { + if(this.drawingMap == undefined) throw "LoresSpritePool has no map."; + + for(var i in this.sprites) { + this.sprites[i].drawOnMap(this.drawingMap); + } +}; + + +LoresSpritePool.prototype.eraseOnMap = function(perPixel) { + if(this.drawingMap == undefined) throw "LoresSpritePool has no map."; + + for(var i in this.sprites) { + this.sprites[i].eraseOnMap(this.drawingMap, perPixel); + } +}; + + +LoresSpritePool.prototype.sortSprites = function() { + this.sprites.sort(function(a,b) { + return (a.z > b.z) ? -1 : ((b.z > a.z) ? 1 : 0); + }); +}; + + +LoresSpritePool.prototype.moveSprites = function() { + for(var i in this.sprites) { + this.sprites[i].doMove(); + } +}; + + + + +function LoresSprite(opts) { + this.map = opts.map; + + this.x = opts.x; + this.y = opts.y; + + this.newx = opts.x; + this.newy = opts.y; + + this.z = (opts.z === undefined ? 0 : opts.z); // z-index +} + + +LoresSprite.prototype.setMap = function(map) { + this.map = map; +}; + + +LoresSprite.prototype.setPosition = function(xpos, ypos) { + this.newx = xpos; + this.newy = ypos; +}; + + +LoresSprite.prototype.scheduleMove = function(xoffset, yoffset) { + this.newx = this.x + xoffset; + this.newy = this.y + yoffset; +}; + + +LoresSprite.prototype.doMove = function() { + this.x = this.newx; + this.y = this.newy; +}; + + +LoresSprite.prototype.getWidth = function() { + return this.map.getWidth(); +}; + + +LoresSprite.prototype.getHeight = function() { + return this.map.getHeight(); +}; + + +LoresSprite.prototype.collidesWith = function(other, pixelPerfect) { + var x1L = this.x; + var x1R = this.x + this.getWidth() - 1; + var y1U = this.y; + var y1D = this.y + this.getHeight() - 1; + + var x2L = other.x; + var x2R = other.x + other.getWidth() - 1; + var y2U = other.y; + var y2D = other.y + other.getHeight() - 1; + + var horizontal = x1L >= x2L && x1L <= x2R; + horizontal |= x1R <= x2R && x1R >= x2L; + + var vertical = y1U >= y2U && y1U <= y2D; + vertical |= y1D <= y2D && y1D >= y2U; + + var rectCollides = (horizontal && vertical); + + if(!rectCollides) return false; // surely + + if(!pixelPerfect) { + return true; // rect collision suffices + + } else { + + for(var yy = Math.max(y1U, y2U); yy <= Math.min(y1D, y2D); yy++) { + for(var xx = Math.max(x1L, x2L); xx <= Math.min(x1R, x2R); xx++) { + + var c1 = this.map.getPixel( (xx - x1L), (yy - y1U) ); + var c2 = other.map.getPixel( (xx - x2L), (yy - y2U) ); + + if(c1 != -1 && c2 != -1) return true; // collision detected + } + } + + return false; // nope + } +}; + + +LoresSprite.prototype.drawOnMap = function(map) { + map.drawMap(this.map, this.x, this.y); +}; + + +LoresSprite.prototype.eraseOnMap = function(map, perPixel) { + if(perPixel) { + map.eraseMap(this.map, this.x, this.y); + } else { + map.eraseRect(this.x, this.y, this.map.getWidth(), this.map.getHeight()); + } +}; \ No newline at end of file