'use strict'; import * as util from '../util'; import EntityPosition from '../model/EntityPosition'; import MapData from '../model/MapData'; import Position from '../model/Position'; const tileSize = 32; const body = document.getElementsByTagName('body')[0]; function loadImage(url: string): Promise { return new Promise(function(resolve, reject) { var img = new Image(); img.addEventListener('load', () => { resolve(img); }); img.addEventListener('error', () => { reject(Error('Failed to load ' + url)); }); img.src = url; }); } function loadImages(imgs: {[key: string]: string}): Promise<{[key: string]: HTMLImageElement}> { return util.mapPromises(_.mapValues(imgs, loadImage)); } function loadTiles(tiles: {[key: string]: {file: string}}): Promise<{[key: string]: HTMLImageElement}> { return loadImages(_.mapValues(tiles, (t) => `resources/sprite/tile/${t.file}.png`)); } function loadEntities(entities: EntityPosition[]): Promise<{[key: string]: HTMLImageElement}> { var p: {[key: string]: Promise} = {}; entities.forEach(e => { p[e.entity.name] = loadImage(`resources/sprite/entity/${e.entity.name}.png`); }); return util.mapPromises(p); } function entityPosition(e: EntityPosition, time: number): Position { if (e.transition) { var t = e.transition; var d = (time - t.start) / (t.end-t.start); return new Position( (1-d) * t.orig.x + d * t.dest.x, (1-d) * t.orig.y + d * t.dest.y ); } else { return e.position; } } function tiles(n: number) { return Math.round(tileSize*n); } export default class MapView { private redrawPending: boolean = false; private canvas: HTMLCanvasElement; private ctx: CanvasRenderingContext2D; private tiles: {[key: string]: HTMLImageElement}; private entitySprites: {[key: string]: HTMLImageElement}; constructor(private map: MapData, private entities: {[key: string]: EntityPosition}, private getOrigin: () => EntityPosition, private updateState: (time: number) => void) { this.canvas = document.createElement('canvas'); this.canvas.style.position = 'absolute'; body.appendChild(this.canvas); this.ctx = this.canvas.getContext('2d'); window.addEventListener('resize', () => this.setSize()); this.setSize(); var tilesReady = loadTiles(map.tiles).then((tiles) => { this.tiles = tiles; }); var entitiesReady = loadEntities(this.getEntities()).then((entities) => { this.entitySprites = entities; }); Promise.all([tilesReady, entitiesReady]).then(() => { this.redraw(); }); } private getEntities(): EntityPosition[] { return _.values(this.entities); } private setSize() { var e = document.documentElement; var w = window.innerWidth || e.clientWidth || body.clientWidth; var h = window.innerHeight || e.clientHeight || body.clientHeight; this.canvas.width = w; this.canvas.height = h; this.redraw() } private drawTile(x: number, y: number, tile: HTMLImageElement) { if (!tile) return; this.ctx.drawImage(tile, x, y); } private drawEntity(e: EntityPosition, time: number): boolean { var sprite = this.entitySprites[e.entity.name]; if (!sprite) return false; var p = entityPosition(e, time); this.ctx.drawImage( sprite, tiles(e.direction), 0, tileSize, tileSize, tiles(p.x), tiles(p.y), tileSize, tileSize ); return !!e.transition; } private setOrigin(time: number) { var origin = entityPosition(this.getOrigin(), time); var w = this.canvas.width; var h = this.canvas.height; this.ctx.translate(Math.round(w/2), Math.round(h/2)); this.ctx.translate(-tiles(origin.x + 0.5), -tiles(origin.y + 0.5)); } private draw(time: number) { this.updateState(time); this.redrawPending = false; this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); if (!this.tiles || !this.entitySprites) return; this.ctx.save(); this.setOrigin(time); this.map.layers.forEach((layer) => { let y = 0; layer.forEach((row) => { let x = 0; for (let tile in row) { this.drawTile(x, y, this.tiles[row[tile]]); x += tileSize; } y += tileSize; }); }); var animate = false; this.getEntities().forEach(e => { if (this.drawEntity(e, time)) animate = true; }); this.ctx.restore(); if (animate) this.redraw(); } redraw() { if (this.redrawPending) return; this.redrawPending = true; window.requestAnimationFrame((time: number) => this.draw(time)); } }