// Lores.js // (c) Ondrej Hruska 2013 // www.ondrovo.com | @MightyPork | ondra@ondrovo.com // MIT license /* ====== CORE ====== */ Lores = { version: "0.1", // library version verbose: true, // enable debug output to console }; /* ====== DISPLAY ====== */ 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.colorTranslator = options.colorTranslator || new LoresColorTranslator(); this.colors = options.colors; this.colorTranslator = options.colorTranslator; 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.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 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.lastMousePosRaw, 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.eraseRect = function(x1,y1,w,h,fillWithBg) { if(fillWithBg) { this.fillRect(x1,y1,x2,y2,this.bg); } else { this.context.clearRect( x1 * this.pixelWidth, y1 * this.pixelHeight, w * this.pixelWidth, h * this.pixelHeight ); } }; LoresDisplay.prototype.fillRect = function(x1,y1,w,h,color) { color = this.resolveColor(color); if(color == -1) { color = this.resolveColor(this.bg); } if(color == -1) { this.eraseRect(x1,y1,w,h,false); } else { this.context.fillStyle = color; this.context.fillRect( x1 * this.pixelWidth, y1 * this.pixelHeight, w * this.pixelWidth, h * this.pixelHeight ); } }; LoresDisplay.prototype.getCanvas = function() { return this.canvas[0]; }; LoresDisplay.prototype.getContext = function() { return this.context; }; LoresDisplay.prototype.getPalette = function() { return this.colors; }; LoresDisplay.prototype.setPalette = function(newPalette) { this.colors = newPalette; }; LoresDisplay.prototype.getColorTranslator = function() { return this.colorTranslator; }; LoresDisplay.prototype.setColorTranslator = function(newTranslator) { this.colorTranslator = newTranslator; }; LoresDisplay.prototype.setColorReplacementTable = function(newTable) { this.colorTranslator.setRules(newTable); }; LoresDisplay.prototype.addColorRule = function(from, to) { this.colorTranslator.addRule(from, to); }; LoresDisplay.prototype.removeColorRule = function(color) { this.colorTranslator.removeRule(color); }; LoresDisplay.prototype.setColorFilter = function(filterFunction) { this.colorTranslator.setFilter(filterFunction); }; 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.colorTranslator.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) { if(x<0||x>=this.cols||y<0||y>=this.rows) return; // out of bounds color = this.resolveColor(color); if(color == -1 && this.bg != -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(fill) { if(fill === undefined) fill = this.bg; var map = new LoresPixmap(this.cols, this.rows, fill); map.connectedDisplay = this; return map; }; /* ====== PALETTE ====== */ 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]; }; /* ====== PIXMAP ====== */ function LoresPixmap(width, height, fill) { this.doAutoFlush = true; this.connectedDisplay = null; if(fill == undefined) fill = -1; this.bg = fill; this.ready = false; if(width === undefined || height === undefined) return; this.width = width; this.height = height; this.map = []; for(var y=0; y= 1 && validLineIndex <= cc) { // key c color var key = line.substring(0, cpp); var parts = line.substring(cpp).trim().split(/\s/g); if(parts[0]=='c') { var color = parts[1]; colors[key] = color; } else { throw "Invalid color definition: " + line; } valid = true; } else if(line.length == w * cpp) { // an actual line valid = true; var row = []; for(var pi = 0; pi < w; pi++) { var c = line.substring(pi*cpp, (pi+1)*cpp); var color = colors[c]; if(color === undefined) color = -1; row.push(color); } array.push(row); } else { throw "Invalid img data: " + line; } if(valid) validLineIndex++; } } return array; }; LoresPixmap.prototype.setReady = function() { this.ready = true; }; LoresPixmap.prototype.isReady = function() { return this.ready; }; LoresPixmap.prototype.setBg = function(color) { this.bg = color; }; LoresPixmap.prototype.erase = function() { this.fill(this.bg); }; LoresPixmap.prototype.fill = function(color) { 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.setReady(true); 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; }; LoresPixmap.prototype.checker = function(base, color) { for(var y = 0; y < this.height; y++) { for(var x = 0; x < this.width; x++) { this.setPixel(x, y, ((x+y)%2==0 ? color : base)); } } }; LoresPixmap.prototype.walk = function(walker) { for(var y = 0; y < this.height; y++) { for(var x = 0; x < this.width; x++) { walker(x, y, this); } } }; /* ====== COLOR TRANSLATOR ====== */ // Used to translate color codes when writing from a pixmap to display function LoresColorTranslator(rules) { this.table = rules || {}; this.filter = function(color) {return color}; } LoresColorTranslator.prototype.process = function(color) { color = (this.table[color] !== undefined) ? this.table[color] : color; return this.filter(color); }; LoresColorTranslator.prototype.addRule = function(from, to) { this.table[from] = to; }; LoresColorTranslator.prototype.removeRule = function(color) { delete this.table[from]; }; LoresColorTranslator.prototype.setRules = function(rules) { this.table = rules; }; LoresColorTranslator.prototype.setFilter = function(filterFunction) { this.filter = filterFunction; }; /* ====== KEYBOARD ====== */ 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); self.downHandlers[ -1 ] && self.downHandlers[ -1 ](event.which); }); $(window).on("keyup", function(event) { self.states[event.which] = false; self.upHandlers[event.which] && self.upHandlers[event.which](event.which); self.upHandlers[ -1 ] && self.upHandlers[ -1 ](event.which); }); $(window).on("keypress", function(event) { self.pressHandlers[event.which] && self.pressHandlers[event.which](event.which); self.pressHandlers[ -1 ] && self.pressHandlers[ -1 ](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]; // for the lazy bums LKey = LoresKeyboard; 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(); } }; /* ====== SPRITE ====== */ 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()); } }; /* ====== TILE SET ====== */ function LoresTileset(helperDisplay, tileWidth, tileHeight) { this.display = helperDisplay || null; this.tiles = {}; this.w = tileWidth; this.h = tileHeight; this.lastX = -this.w; this.lastY = 0; } LoresTileset.prototype.erase = function() { this.display.erase(true); }; LoresTileset.prototype.addTile = function(name, pixmap) { if(pixmap.getWidth() != this.w && pixmap.getHeight() != this.h) { throw "Tile size not compatible with tileset."; } var x = this.lastX + this.w; var y = this.lastY; var maxX = x + this.w; var maxY = y + this.h; if(maxX >= this.display.getWidth()) { x = 0; y += this.h; } if(maxY >= this.display.getHeight()) { throw "Tileset is full, can't add another tile."; } this.tiles[name] = {x:x,y:y}; this.lastX = x; this.lastY = y; Lores.verbose && console.log("new tile at "+x+","+y); this.display.drawMap(pixmap, x, y); }; LoresTileset.prototype.renderTile = function(dest, name, x, y) { var source = this.display.getCanvas(); var ctx = dest.getContext(); var tile = this.tiles[name]; if(tile===undefined) throw "Tile not found: " + name; var px = dest.getPixelSize(); var px2 = this.display.getPixelSize(); if(px.x != px2.x || px.y != px2.y) throw "Tileset's pixel size doesn't match target pixel size. Can't draw."; ctx.drawImage( source, tile.x*px.x, tile.y*px.y, this.w*px.x, this.h*px.y, x*this.w*px.x, y*this.w*px.y, this.w*px.x, this.h*px.y ); }; /* ====== UTILS ====== */ Lores.buildCheckerBackground = function(element, bgCols, bgRows, bg, fg) { var scr = new LoresDisplay( $(element), { cols: bgCols, rows: bgRows, colors: { 0: bg || "#000", 1: fg || "#111", }, bg: 0, } ); var map = scr.getMap(); map.autoFlush(false); map.checker(0,1); map.flush(); }; Lores.buildHelperCanvas = function(element, cols, rows, palette, bg) { var scr = new LoresDisplay( $(element), { cols: cols, rows: rows, colors: palette, bg: (bg===undefined ? -1 : bg), } ); $(scr.getCanvas()).css("display","none"); return scr; }; Lores.shuffleArray = function(array) { for (var i = array.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = array[i]; array[i] = array[j]; array[j] = temp; } return array; }; /* ====== THIRD PARTY ====== */ // Shim for requestAnimationFrame // http://paulirish.com/2011/requestanimationframe-for-smart-animating/ // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating // requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel // MIT license (function() { var lastTime = 0; var vendors = ['ms', 'moz', 'webkit', 'o']; for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; } if (!window.requestAnimationFrame) window.requestAnimationFrame = function(callback, element) { var currTime = new Date().getTime(); var timeToCall = Math.max(0, 16 - (currTime - lastTime)); var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); lastTime = currTime + timeToCall; return id; }; if (!window.cancelAnimationFrame) window.cancelAnimationFrame = function(id) { clearTimeout(id); }; }());