	var m_canvas;
	var m_ctx;
	var m_imgBackground;
	var m_intervalId = -1;
	var m_fpsId      = -1;
	var m_view = { x: 0, y: 0 }; // world coordinates
	var m_tiles;                 // array of images
	var m_keys = 0;
	var m_isScrolling = false;
	var m_scroll = 0;
	var m_lastTime;     // time at which we last calculated FPS
	var m_curFps = '';  // frames-per-second text for last calculated FPS rate
	var m_cFrames = 0;
	const KEY_LEFT  = 0x01;
	const KEY_UP    = 0x02;
	const KEY_RIGHT = 0x04;
	const KEY_DOWN  = 0x08;
	const TILE_SIZE = 24 >> 0;
	var m_cxScreen;
	var m_cyScreen;
	var m_cxScreenTiles;
	var m_cyScreenTiles;
	var m_xViewMax;
	var m_yViewMax;
	const DESIRED_FPS = 100.0;
	const NUM_TILES = 8;
	const m_map = [
		1,2,2,1,4,0,0,0,5,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
		1,2,2,1,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
		1,2,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,
		1,2,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,
		1,2,2,1,4,0,0,0,0,0,0,0,0,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,
		1,2,2,1,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,
		1,2,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,2,2,2,0,0,1,1,
		1,2,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,
		1,2,2,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,
		1,2,2,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,
		1,2,2,1,1,1,0,0,3,3,3,3,0,0,2,2,2,2,2,2,2,3,2,2,2,0,0,0,0,0,0,0,0,0,0,1,1,
		1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,
		1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,1,1,
		1,2,2,6,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,1,1,
		1,2,2,0,0,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,1,1,
		1,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,1,1,
		1,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,
		1,2,2,0,0,0,0,0,0,0,0,0,0,3,3,3,3,3,3,3,3,3,3,0,0,0,0,0,0,0,0,0,0,0,0,1,1,
		1,2,2,0,0,0,0,0,0,5,0,0,0,0,5,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,
		1,2,2,1,1,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,
		1,2,2,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,
		1,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,
		1,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,0,0,0,0,0,0,0,0,3,2,
		1,2,2,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
	];
	const m_cxMap = 37;
	const m_cyMap = (m_map.length / m_cxMap) >> 0;
	const m_imgPrefix = 'http://bikko.org/practice/canvas/d/';

	function clamp(x,min,max) {
		if (x < min) {
			return min;
		}
		if (x > max) {
			return max;
		}
		return x;
	}

	function doScroll() {
		m_view.x = m_cxMap * TILE_SIZE / 4 + Math.cos(m_scroll) * (m_cxScreenTiles*TILE_SIZE/3);
		m_view.y = m_cyMap * TILE_SIZE / 4 - Math.sin(m_scroll) * (m_cyScreenTiles*TILE_SIZE/3.7);
		m_scroll += 3.0 * (Math.PI / 180.0);
	}

	function gameFrame() {
		const k = m_keys;
		const speed = 4;

		if (m_isScrolling) {
			doScroll();
		}
		if (k & KEY_LEFT)  { m_view.x -= speed; }
		if (k & KEY_UP)    { m_view.y -= speed; }
		if (k & KEY_RIGHT) { m_view.x += speed; }
		if (k & KEY_DOWN)  { m_view.y += speed; }
		m_view.x = clamp(m_view.x, 0, m_xViewMax) >> 0;
		m_view.y = clamp(m_view.y, 0, m_yViewMax) >> 0;
		drawMap();
		drawFPS();

		m_cFrames++;
	}

	function drawFPS() {
		m_ctx.fillStyle = 'White';
		m_ctx.fillText(m_curFps, 8, 20);
	}

	
	function drawMap() {
		const extra = 7;
		const xBase = ((-m_view.x % TILE_SIZE) - TILE_SIZE * extra) >> 0;
		const yBase = -(m_view.y % TILE_SIZE) >> 0;
		const txBase = (m_view.x / TILE_SIZE) >> 0; // coerce to int
		const tyBase = (m_view.y / TILE_SIZE) >> 0;
		const pitchDiff = m_cxMap - (m_cxScreenTiles + extra);
		var tx, ty, iTile, index;
		var yScreen;

		index = ((tyBase - extra) * m_cxMap + (txBase - extra)) >> 0;
		yScreen = yBase - TILE_SIZE * extra;
		m_ctx.drawImage(m_imgBackground, 0, 0);
		for (ty = extra; ty > 0; ty--) {
			xScreen = xBase;
			for (tx = m_cxScreenTiles + extra; tx > 0; tx--) {
				if (index >= 0 && index < m_map.length) {
					iTile = m_map[index];
					if (iTile >= 4)
						m_ctx.drawImage(m_tiles[iTile - 1], xScreen, yScreen);
				}
				xScreen += TILE_SIZE;
				index++;
			}
			yScreen += TILE_SIZE;
			index += pitchDiff;
		}

		yScreen = yBase;
		for (ty = m_cyScreenTiles; ty > 0; ty--) {
			// For each row, first draw any BIG tiles which are off the view to the left
			xScreen = xBase;
			for (tx = -extra; tx < 0; tx++) {
				if (index >= 0 && index < m_map.length) {
					iTile = m_map[index];
					if (iTile >= 4)
						m_ctx.drawImage(m_tiles[iTile - 1], xScreen, yScreen);
				}
				xScreen += TILE_SIZE;
				index++;
			}

			for (tx = m_cxScreenTiles; tx > 0; tx--) {
				iTile = m_map[index];
				if (iTile != 0) {
					m_ctx.drawImage(m_tiles[iTile - 1], xScreen, yScreen);
				}
				xScreen += TILE_SIZE;
				index++;
			}
			yScreen += TILE_SIZE;
			index += pitchDiff;
		}
	}

	function onKeyDown(e) {
		switch (e.which) {
			case 37: m_keys |= KEY_LEFT; break;
			case 38: m_keys |= KEY_UP; break;
			case 39: m_keys |= KEY_RIGHT; break;
			case 40: m_keys |= KEY_DOWN; break;
			default: break;
		}
		
	}

	function onKeyUp(e) {
		switch (e.which) {
			case 37: m_keys &= ~KEY_LEFT; break;
			case 38: m_keys &= ~KEY_UP; break;
			case 39: m_keys &= ~KEY_RIGHT; break;
			case 40: m_keys &= ~KEY_DOWN; break;
			default: break;
		}
	}

	function init() {
		m_canvas = document.getElementById('my_canvas');
		m_tiles = new Array();
		for (i = 0; i < NUM_TILES; ++i) {
			m_tiles[i] = new Image();
			m_tiles[i].src = m_imgPrefix + 'tile' + (i+1) + '.png';
		}
		m_imgBackground = new Image();
		m_imgBackground.src = m_imgPrefix + 'background.png';

		document.addEventListener("keyup", onKeyUp, false);
		document.addEventListener("keydown", onKeyDown, false);

		if (m_canvas.getContext) {
			viewLarge();
			m_lastTime = currentTime();
			m_ctx = m_canvas.getContext('2d');
			m_ctx.font = 'bold 20px gentilis';
			m_imgBackground.onload = start;
		}
	}

	function currentTime() {
		return (new Date()).getTime();
	}

	function toggleScroll() {
		m_isScrolling = !m_isScrolling;
	}

	function calculateFps() {
		var now = currentTime();
		var diff = now - m_lastTime;
		if (diff > 0) {
			curFps = m_cFrames * 1000.0 / diff;
			m_lastTime = now;
			m_cFrames = 0;
			m_curFps = 'FPS: ' + Math.ceil(curFps * 100) / 100;
		}
	}

	function setView(cx, cy) {
		m_cxScreen = cx;
		m_cyScreen = cy;
		m_canvas.width = m_cxScreen;
		m_canvas.height = m_cyScreen;
		m_cxScreenTiles = (m_cxScreen / TILE_SIZE + 1) >> 0;
		m_cyScreenTiles = (m_cyScreen / TILE_SIZE + 1) >> 0;
		m_xViewMax = (m_cxMap - m_cxScreenTiles) * TILE_SIZE;
		m_yViewMax = (m_cyMap - m_cyScreenTiles) * TILE_SIZE;
	}

	function viewSmall()  { setView(240, 168); }
	function viewMedium() { setView(312, 240); }
	function viewLarge()  { setView(480, 312); }
	function viewHuge()   { setView(648, 480); }

	function start() {
		stop();
		m_intervalId = setInterval('gameFrame()', 1000.0/DESIRED_FPS);
		m_fpsId = setInterval('calculateFps()', 1000);
	}

	function stop() {
		if (m_intervalId != -1) {
			clearInterval(m_intervalId);
			m_intervalId = -1;
		}
		if (m_fpsId != -1) {
			clearInterval(m_fpsId);
			m_fpsId = -1;
		}
	}
