function createSprite(name,spritegrid, colors, padding, flippy) {
	if (colors === undefined) {
		colors = [state.bgcolor, state.fgcolor];
	}
    if( flippy == undefined )
    {
        flippy = false;
    }

	var sprite = makeSpriteCanvas(name);
	var spritectx = sprite.getContext('2d');

	spritectx.clearRect(0, 0, cellwidth, cellheight);

	var w = spritegrid[0].length;
	var h = spritegrid.length;
	var cw = ~~(cellwidth / (w + (padding|0)));
	var ch = ~~(cellheight / (h + (padding|0)));
	var pixh=ch;
	if ("scanline" in state.metadata) {
		pixh=Math.ceil(ch/2);
	}
	spritectx.fillStyle = state.fgcolor;
	for (var j = 0; j < w; j++) {
		for (var k = 0; k < h; k++) {
            var val = flippy ? spritegrid[k][j] : spritegrid[j][k];
            
			if (val >= 0) {
				var cy = (j * cw)|0;
				var cx = (k * ch)|0;
				spritectx.fillStyle = colors[val];
				spritectx.fillRect(cx, cy, cw, pixh);
			}
		}
	}

	return sprite;
}

function createSpriteBuffer(name,spritegrid, colors, renderType)
{
	// build converted colours for use on verts
	if (colors === undefined)
	{
		colors = [state.bgcolor, state.fgcolor];
	}
	rgbColors = []
	for( var i = 0; i < colors.length; ++i )
	{
		rgbColors.push( [ parseInt( colors[i].substring( 1, 3 ), 16 ) / 255.0, parseInt( colors[i].substring( 3, 5 ), 16 ) / 255.0, parseInt( colors[i].substring( 5, 7 ), 16 ) / 255.0 ] );
	}
	
    // build verts from sprite grid based on render type
	var vertices = [];
	var vertColors = [];
	switch( renderType )
	{
		case "floor":
			addHorizontalPlaneVerts( spritegrid[0], rgbColors, vertices, vertColors, 0 );
            break;
        case "ceiling":
			addHorizontalPlaneVerts( spritegrid[0], rgbColors, vertices, vertColors, 1 );
            break;
        case "sandwich":
			addHorizontalPlaneVerts( spritegrid[0], rgbColors, vertices, vertColors, 1 );
            if( spritegrid.length > 1 )
            {
                // read floor from second plane
                addHorizontalPlaneVerts( spritegrid[1], rgbColors, vertices, vertColors, 0 );
            }
            else
            {
                // nope, floor is first plane still
       			addHorizontalPlaneVerts( spritegrid[0], rgbColors, vertices, vertColors, 0 );
            }
            break;
        case "sprite":
        case "billboard":
		case "hud":
			addVerticalPlaneVerts( spritegrid[0], rgbColors, vertices, vertColors );
            break;        
        case "wall":
			addWallVerts( spritegrid[0], rgbColors, vertices, vertColors );
            break;
        case "voxel":
        case "motionvoxel":
        case "facingvoxel":
            addVoxelVerts( spritegrid, rgbColors, vertices, vertColors );
            break;
    }
	
	var verticesBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, verticesBuffer);  
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
	var colorBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertColors), gl.STATIC_DRAW);

	return [verticesBuffer, colorBuffer, vertices.length / 3];
}

function addHorizontalPlaneVerts( spritegrid, rgbColors, vertices, vertColors, height )
{
	var w = spritegrid[0].length;
	var h = spritegrid.length;
	var vertexInsert = vertices.length;
	var colorInsert = vertColors.length;
	var dxa;
	var dxb;
	var dya;
	var dyb;
	var r;
	var g;
	var b;
	for (var j = 0; j < w; j++)
	{
		for (var k = 0; k < h; k++)
		{
			var val = spritegrid[k][j];
			if (val >= 0)
			{
				dxa = j / w - 0.5;
                dxb = ( j + 1 ) / w - 0.5;
				dya = k / h - 0.5;
				dyb = ( k + 1 ) / h - 0.5;

				vertices.splice( vertexInsert, 0, dxb, height, dyb );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, dxa, height, dyb );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, dxb, height, dya );
				vertexInsert += 3;

				vertices.splice( vertexInsert, 0, dxa, height, dyb );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, dxb, height, dya );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, dxa, height, dya );
				vertexInsert += 3;

				r = rgbColors[val][0];
				g = rgbColors[val][1];
				b = rgbColors[val][2];
				vertColors.splice( colorInsert, 0, r, g, b, 1.0 );
				colorInsert += 4;
				vertColors.splice( colorInsert, 0, r, g, b, 1.0 );
				colorInsert += 4;
				vertColors.splice( colorInsert, 0, r, g, b, 1.0 );
				colorInsert += 4;
				
				vertColors.splice( colorInsert, 0, r, g, b, 1.0 );
				colorInsert += 4;
				vertColors.splice( colorInsert, 0, r, g, b, 1.0 );
				colorInsert += 4;
				vertColors.splice( colorInsert, 0, r, g, b, 1.0 );
				colorInsert += 4;
			}
		}
	}
}

function addVerticalPlaneVerts( spritegrid, rgbColors, vertices, vertColors )
{
	var w = spritegrid[0].length;
	var h = spritegrid.length;
	var vertexInsert = vertices.length;
	var colorInsert = vertColors.length;
	var dxa;
	var dxb;
	var dya;
	var dyb;
	var r;
	var g;
	var b;
	for (var j = 0; j < w; j++)
	{
		for (var k = 0; k < h; k++)
		{
			var val = spritegrid[k][j];
			if (val >= 0)
			{
				dxa = j / w - 0.5;
                dxb = ( j + 1 ) / w - 0.5;
				dya = 1 - k / h;
				dyb = 1 - ( k + 1 ) / h;

				vertices.splice( vertexInsert, 0, dxb, dyb, 0.0 );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, dxa, dyb, 0.0 );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, dxb, dya, 0.0 );
				vertexInsert += 3;

				vertices.splice( vertexInsert, 0, dxa, dyb, 0.0 );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, dxb, dya, 0.0 );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, dxa, dya, 0.0 );
				vertexInsert += 3;

				r = rgbColors[val][0];
				g = rgbColors[val][1];
				b = rgbColors[val][2];
				vertColors.splice( colorInsert, 0, r, g, b, 1.0 );
				colorInsert += 4;
				vertColors.splice( colorInsert, 0, r, g, b, 1.0 );
				colorInsert += 4;
				vertColors.splice( colorInsert, 0, r, g, b, 1.0 );
				colorInsert += 4;
				
				vertColors.splice( colorInsert, 0, r, g, b, 1.0 );
				colorInsert += 4;
				vertColors.splice( colorInsert, 0, r, g, b, 1.0 );
				colorInsert += 4;
				vertColors.splice( colorInsert, 0, r, g, b, 1.0 );
				colorInsert += 4;
			}
		}
	}
}

function addWallVerts( spritegrid, rgbColors, vertices, vertColors )
{
	var w = spritegrid[0].length;
	var h = spritegrid.length;
	var vertexInsert = vertices.length;
	var colorInsert = vertColors.length;
	var dxa;
	var dxb;
	var dya;
	var dyb;
	var r;
	var g;
	var b;
	for (var j = 0; j < w; j++)
	{
		for (var k = 0; k < h; k++)
		{
			var val = spritegrid[k][j];
			if (val >= 0)
			{
				dxa = j / w - 0.5;
                dxb = ( j + 1 ) / w - 0.5;
				dya = 1 - k / h;
				dyb = 1 - ( k + 1 ) / h;

				vertices.splice( vertexInsert, 0, dxb, dyb, 0.5 );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, dxa, dyb, 0.5 );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, dxb, dya, 0.5 );
				vertexInsert += 3;

				vertices.splice( vertexInsert, 0, dxa, dyb, 0.5 );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, dxb, dya, 0.5 );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, dxa, dya, 0.5 );
				vertexInsert += 3;

				vertices.splice( vertexInsert, 0, -0.5, dyb, dxb );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, -0.5, dyb, dxa );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, -0.5, dya, dxb );
				vertexInsert += 3;

				vertices.splice( vertexInsert, 0, -0.5, dyb, dxa );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, -0.5, dya, dxb );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, -0.5, dya, dxa );
				vertexInsert += 3;

				vertices.splice( vertexInsert, 0, -dxb, dyb, -0.5 );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, -dxa, dyb, -0.5 );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, -dxb, dya, -0.5 );
				vertexInsert += 3;

				vertices.splice( vertexInsert, 0, -dxa, dyb, -0.5 );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, -dxb, dya, -0.5 );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, -dxa, dya, -0.5 );
				vertexInsert += 3;

				vertices.splice( vertexInsert, 0, 0.5, dyb, -dxb );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, 0.5, dyb, -dxa );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, 0.5, dya, -dxb );
				vertexInsert += 3;

				vertices.splice( vertexInsert, 0, 0.5, dyb, -dxa );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, 0.5, dya, -dxb );
				vertexInsert += 3;
				vertices.splice( vertexInsert, 0, 0.5, dya, -dxa );
				vertexInsert += 3;

				r = rgbColors[val][0];
				g = rgbColors[val][1];
				b = rgbColors[val][2];
                for( var c = 0; c < 24; ++c )
                {
                    vertColors.splice( colorInsert, 0, r, g, b, 1.0 );
                    colorInsert += 4;
                }
			}
		}
	}
}

function addVoxelVerts( spritegrid, rgbColors, vertices, vertColors )
{
	var w = spritegrid.width;
	var h = spritegrid.height;
    var d = spritegrid.depth;
	var vertexInsert = vertices.length;
	var colorInsert = vertColors.length;
	var dxa;
	var dxb;
	var dya;
	var dyb;
    var dza;
    var dzb;
	var r;
	var g;
	var b;
    var coloursToInsert = 0;
	for (var x = 0; x < w; x++)
	{
		for (var y = 0; y < h; y++)
		{
            for( var z = 0; z < d; ++z )
            {
                var val = spritegrid[z][y][x];
                if (val >= 0)
                {
                    dxa = x / w - 0.5;
                    dxb = ( x + 1 ) / w - 0.5;
                    dya = y / h - 0.5;
                    dyb = ( y + 1 ) / h - 0.5;
                    dza = 1 - z / d;
                    dzb = 1 - ( z + 1 ) / d;
                    
                    coloursToInsert = 0;

                    // bottom
                    if( z == d - 1 || spritegrid[z+1][y][x] == -1 )
                    {
                        vertices.splice( vertexInsert, 0, dxb, dzb, dyb );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxa, dzb, dyb );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxb, dzb, dya );
                        vertexInsert += 3;

                        vertices.splice( vertexInsert, 0, dxa, dzb, dyb );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxb, dzb, dya );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxa, dzb, dya );
                        vertexInsert += 3;
                        
                        coloursToInsert += 6;
                    }
                    
                    // top
                    if( z == 0 || spritegrid[z-1][y][x] == -1 )
                    {
                        vertices.splice( vertexInsert, 0, dxb, dza, dyb );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxa, dza, dyb );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxb, dza, dya );
                        vertexInsert += 3;

                        vertices.splice( vertexInsert, 0, dxa, dza, dyb );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxb, dza, dya );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxa, dza, dya );
                        vertexInsert += 3;
                        
                        coloursToInsert += 6;
                    }
                    
                    // left
                    if( x == 0 || spritegrid[z][y][x-1] == -1 )
                    {
                        vertices.splice( vertexInsert, 0, dxa, dzb, dyb );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxa, dza, dyb );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxa, dzb, dya );
                        vertexInsert += 3;

                        vertices.splice( vertexInsert, 0, dxa, dza, dyb );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxa, dzb, dya );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxa, dza, dya );
                        vertexInsert += 3;
                        
                        coloursToInsert += 6;
                    }
                    
                    // right
                    if( x == w - 1 || spritegrid[z][y][x+1] == -1 )
                    {
                        vertices.splice( vertexInsert, 0, dxb, dzb, dyb );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxb, dza, dyb );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxb, dzb, dya );
                        vertexInsert += 3;

                        vertices.splice( vertexInsert, 0, dxb, dza, dyb );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxb, dzb, dya );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxb, dza, dya );
                        vertexInsert += 3;
                        
                        coloursToInsert += 6;
                    }
                    
                    // front
                    if( y == h - 1 || spritegrid[z][y+1][x] == -1 )
                    {
                        vertices.splice( vertexInsert, 0, dxb, dzb, dyb );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxb, dza, dyb );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxa, dzb, dyb );
                        vertexInsert += 3;

                        vertices.splice( vertexInsert, 0, dxb, dza, dyb );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxa, dzb, dyb );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxa, dza, dyb );
                        vertexInsert += 3;
                        
                        coloursToInsert += 6;
                    }
                    
                    // back
                    if( y == 0 || spritegrid[z][y-1][x] == -1 )
                    {
                        vertices.splice( vertexInsert, 0, dxb, dzb, dya );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxb, dza, dya );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxa, dzb, dya );
                        vertexInsert += 3;

                        vertices.splice( vertexInsert, 0, dxb, dza, dya );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxa, dzb, dya );
                        vertexInsert += 3;
                        vertices.splice( vertexInsert, 0, dxa, dza, dya );
                        vertexInsert += 3;
                        
                        coloursToInsert += 6;
                    }
                    
                    // colours
                    if( coloursToInsert > 0 )
                    {
                        r = rgbColors[val][0];
                        g = rgbColors[val][1];
                        b = rgbColors[val][2];
                        
                        while( coloursToInsert > 0 )
                        {
                            vertColors.splice( colorInsert, 0, r, g, b, 1.0 );
                            colorInsert += 4;
                            coloursToInsert -= 1;
                        }
                    }
                }
			}
		}
	}
}


function regenText(spritecanvas,spritectx) {
	textImages={};

	for (var n in font) {
		if (font.hasOwnProperty(n)) {
			textImages[n] = createSprite('char'+n,font[n], undefined, 1);
		}
	}
}
var spriteimages;
var spriteBuffers = [];
function regenSpriteImages() {
	if (textMode) {
		regenText();
		return;
	} else if (levelEditorOpened) {
		textImages['s'] = createSprite('chars',font['s'],undefined);
	}
	
	if (state.levels.length===0) {
		return;
	}
	spriteimages = [];

	for (var i = 0; i < sprites.length; i++) {
		if (sprites[i] == undefined) {
			continue;
		}
		spriteimages[i] = createSprite(i.toString(),sprites[i].dat[0], sprites[i].colors, 0, true);
	}
	
	// clear existing sprite buffers
	for( var i = 0; i < spriteBuffers.length; ++i )
	{
		if( spriteBuffers[i] == undefined )
		{
			continue;
		}
		gl.deleteBuffer( spriteBuffers[i][0] );
		gl.deleteBuffer( spriteBuffers[i][1] );
	}
	spriteBuffers = [];
	
	// create new sprite buffers
	for( var i = 0; i < sprites.length; ++i )
	{
		if( sprites[i] == undefined )
		{
			continue;
		}
		spriteBuffers[i] = createSpriteBuffer(i.toString(),sprites[i].dat, sprites[i].colors, sprites[i].renderType);
	}

	if (canOpenEditor) {
		generateGlyphImages();
	}
}

var glyphImagesCorrespondance;
var glyphImages;
var glyphHighlight;
var glyphHighlightResize;
var glyphPrintButton;
var glyphMouseOver;
var glyphSelectedIndex=0;
var editorRowCount=1;

var canvasdict={};
function makeSpriteCanvas(name) {
	var canvas;
	if (name in canvasdict) {
		canvas = canvasdict[name];
	} else {
		canvas = document.createElement('canvas');
		canvasdict[name]=canvas;
	}
	canvas.width = cellwidth;
	canvas.height = cellheight;
	return canvas;
}


function generateGlyphImages() {
	if (cellwidth===0||cellheight===0) {
		return;
	}
	glyphImagesCorrespondance=[];
	glyphImages=[];
	
	for (var n in state.glyphDict) {
		if (n.length==1 && state.glyphDict.hasOwnProperty(n)) {
			var g=state.glyphDict[n];
			var sprite = makeSpriteCanvas("C"+n)
			var spritectx = sprite.getContext('2d');
			glyphImagesCorrespondance.push(n);
			for (var i=0;i<g.length;i++){
				var id = g[i];
				if (id===-1) {
					continue;
				}
				spritectx.drawImage(spriteimages[id], 0, 0);
			}
			glyphImages.push(sprite);
		}
	}

	{
		//make highlight thingy
		glyphHighlight = makeSpriteCanvas("highlight");
		var spritectx = glyphHighlight.getContext('2d');
		spritectx.fillStyle = '#FFFFFF';

		spritectx.fillRect(0,0,cellwidth,1);
		spritectx.fillRect(0,0,1,cellheight);
		
		spritectx.fillRect(0,cellheight-1,cellwidth,1);
		spritectx.fillRect(cellwidth-1,0,1,cellheight);
	}

	{
		glyphPrintButton = textImages['s'];
	}
	{
		//make highlight thingy
		glyphHighlightResize = makeSpriteCanvas("highlightresize");
		var spritectx = glyphHighlightResize.getContext('2d');
		spritectx.fillStyle = '#FFFFFF';
		
		var minx=((cellwidth/2)-1)|0;
		var xsize=cellwidth-minx-1-minx;
		var miny=((cellheight/2)-1)|0;
		var ysize=cellheight-miny-1-minx;

		spritectx.fillRect(minx,0,xsize,cellheight);
		spritectx.fillRect(0,miny,cellwidth,ysize);
	}

	{
		//make highlight thingy
		glyphMouseOver = makeSpriteCanvas();
		var spritectx = glyphMouseOver.getContext('2d');
		spritectx.fillStyle = 'yellow';
		
		spritectx.fillRect(0,0,cellwidth,2);
		spritectx.fillRect(0,0,2,cellheight);
		
		spritectx.fillRect(0,cellheight-2,cellwidth,2);
		spritectx.fillRect(cellwidth-2,0,2,cellheight);
	}
}

var canvas;
var ctx;


var x;
var y;
var cellwidth;
var cellheight;
var magnification;
var xoffset;
var yoffset;

window.addEventListener('resize', canvasResize, false);
canvas = document.getElementById('gameCanvas');
ctx = canvas.getContext('2d');
canvas3d = document.getElementById('gameCanvas3d');
gl = initWebGL(canvas3d);
initMatrices();
initShaders();
initUniforms();
x = 0;
y = 0;

function glyphCount(){
	var count=0;
	for (var n in state.glyphDict) {
		if (n.length==1 && state.glyphDict.hasOwnProperty(n)) {
			count++;
		}
	}    
	return count;
}

function initWebGL(canvas)
{
  gl = null;
  
  // Try to grab the standard context. If it fails, fallback to experimental.
  gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
  
  // If we don't have a GL context, give up now
  if (!gl) {
	alert('Unable to initialize WebGL. Your browser may not support it.');
  }
  
  return gl;
}

function initMatrices()
{
	vMatrix = mat4.create();
	mMatrix = mat4.create();
	pMatrix = mat4.create();
}

function initShaders()
{
	// make vert shader
	var vertShader = gl.createShader( gl.VERTEX_SHADER );
	gl.shaderSource(vertShader, "\
		attribute vec3 aVertexPosition;\
		attribute vec4 aVertexColor;\
		\
		uniform mat4 uMMatrix;\
		uniform mat4 uVMatrix;\
		uniform mat4 uPMatrix;\
		uniform lowp float uScaleHeight;\
		\
		varying lowp vec4 vColor;\
		varying highp vec3 vPos;\
        varying highp vec3 wPos;\
		\
		void main(void)\
		{\
			gl_Position = uPMatrix * uVMatrix * uMMatrix * vec4(aVertexPosition * vec3(1.0,uScaleHeight,1.0), 1.0);\
			vColor = aVertexColor;\
			vPos = ( uVMatrix * uMMatrix * vec4( aVertexPosition, 1.0 ) ).xyz;\
            wPos = ( uMMatrix * vec4( aVertexPosition, 1.0 ) ).xyz;\
		}\
	   ");
	gl.compileShader( vertShader );
	if (!gl.getShaderParameter(vertShader, gl.COMPILE_STATUS)) { console.log('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(vertShader));  }
	
	// make frag shader
	var fragShader = gl.createShader( gl.FRAGMENT_SHADER );
	gl.shaderSource(fragShader, "\
		precision mediump float;\
		\
		varying lowp vec4 vColor;\
		varying highp vec3 vPos;\
		varying highp vec3 wPos;\
		\
		uniform lowp float uViewDistance;\
		uniform lowp vec4 uFogColor;\
		uniform lowp float uFogPower;\
        \
		uniform lowp vec4 uGradientBottom;\
		uniform lowp vec4 uGradientMiddle;\
		uniform lowp vec4 uGradientTop;\
		\
		void main(void)\
		{\
			float fog = max( 1.0 - sqrt( dot( vPos, vPos ) ) / uViewDistance, 0.0 );\
			fog = uFogPower > 0.0 ? pow( fog, uFogPower ) : ( fog <= 0.0 ? 0.0 : 1.0 );\
            float aoPos = wPos.y;\
            vec4 ao = mix( mix( uGradientBottom, uGradientMiddle, aoPos ), mix( uGradientMiddle, uGradientTop, aoPos ), aoPos );\
			gl_FragColor = vColor * fog * ao + uFogColor * ( 1.0 - fog );\
		}\
	");
	gl.compileShader( fragShader );
	if (!gl.getShaderParameter(fragShader, gl.COMPILE_STATUS)) { console.log('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(fragShader));  }
	
	// make shader program
	shaderProgram = gl.createProgram();
	gl.attachShader(shaderProgram, vertShader);
	gl.attachShader(shaderProgram, fragShader);
	gl.linkProgram(shaderProgram);
	gl.useProgram(shaderProgram);
	
	// set up vert attributes
	vertexPositionAttribute = gl.getAttribLocation(shaderProgram, 'aVertexPosition');
	gl.enableVertexAttribArray(vertexPositionAttribute);
	vertexColorAttribute = gl.getAttribLocation(shaderProgram, 'aVertexColor');
	gl.enableVertexAttribArray(vertexColorAttribute);
}

function initUniforms()
{
	mUniform = gl.getUniformLocation(shaderProgram, "uMMatrix");
	vUniform = gl.getUniformLocation(shaderProgram, "uVMatrix");
	pUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
	scaleHeightUniform = gl.getUniformLocation(shaderProgram, "uScaleHeight");
	viewDistanceUniform = gl.getUniformLocation(shaderProgram, "uViewDistance");
	fogColorUniform = gl.getUniformLocation(shaderProgram, "uFogColor");
	fogPowerUniform = gl.getUniformLocation(shaderProgram, "uFogPower");
	gradientBottomUniform = gl.getUniformLocation(shaderProgram, "uGradientBottom");
	gradientMiddleUniform = gl.getUniformLocation(shaderProgram, "uGradientMiddle");
	gradientTopUniform = gl.getUniformLocation(shaderProgram, "uGradientTop");
}

function redraw() {
	if (cellwidth===0||cellheight===0) {
		return;
	}
	if (spriteimages===undefined) {
		regenSpriteImages();
	}
	
	var drawLevel = level;
	if( backups != undefined && displayIndex < backups.length )
	{
		drawLevel = backups[displayIndex];
	}

	if (textMode) {
		canvas.style.visibility = "visible";
		ctx.fillStyle = state.bgcolor;
		ctx.fillRect(0, 0, canvas.width, canvas.height);

		for (var i = 0; i < titleWidth; i++) {
			for (var j = 0; j < titleHeight; j++) {
				var ch = titleImage[j].charAt(i);
				if (ch in textImages) {
					var sprite = textImages[ch];
					ctx.drawImage(sprite, xoffset + i * cellwidth, yoffset + j * cellheight);                   
				}
			}
		}
		return;
	} else {
		//ctx.fillStyle = state.bgcolor;
		//ctx.fillRect(0, 0, canvas.width, canvas.height);

		var mini=0;
		var maxi=screenwidth;
		var minj=0;
		var maxj=screenheight;

		if (levelEditorOpened) {
			var glyphcount = glyphCount();
			editorRowCount = Math.ceil(glyphcount/(screenwidth-1));
			maxi-=2;
			maxj-=2+editorRowCount;
		} else if (flickscreen) {
			var playerPositions = drawLevel.getPlayerPositions();
			if (playerPositions.length>0) {
				var playerPosition=playerPositions[0];
				var px = (playerPosition/(drawLevel.height))|0;
				var py = (playerPosition%drawLevel.height)|0;

				var screenx = (px/screenwidth)|0;
				var screeny = (py/screenheight)|0;
				mini=screenx*screenwidth;
				minj=screeny*screenheight;
				maxi=Math.min(mini+screenwidth,drawLevel.width);
				maxj=Math.min(minj+screenheight,drawLevel.height);

				oldflickscreendat=[mini,minj,maxi,maxj];
			} else if (oldflickscreendat.length>0){
				mini=oldflickscreendat[0];
				minj=oldflickscreendat[1];
				maxi=oldflickscreendat[2];
				maxj=oldflickscreendat[3];
			}
		} else if (zoomscreen) {
			var playerPositions = getPlayerPositions();
			if (playerPositions.length>0) {
				var playerPosition=playerPositions[0];
				var px = (playerPosition/(drawLevel.height))|0;
				var py = (playerPosition%drawLevel.height)|0;
				mini=Math.max(Math.min(px-((screenwidth/2)|0),drawLevel.width-screenwidth),0);
				minj=Math.max(Math.min(py-((screenheight/2)|0),drawLevel.height-screenheight),0);
				maxi=Math.min(mini+screenwidth,drawLevel.width);
				maxj=Math.min(minj+screenheight,drawLevel.height);
				oldflickscreendat=[mini,minj,maxi,maxj];
			}  else if (oldflickscreendat.length>0){
				mini=oldflickscreendat[0];
				minj=oldflickscreendat[1];
				maxi=oldflickscreendat[2];
				maxj=oldflickscreendat[3];
			}         
		}
		

		/*for (var i = mini; i < maxi; i++) {
			for (var j = minj; j < maxj; j++) {
				var posIndex = j + i * level.height;
				var posMask = level.getCellInto(posIndex,_o12);                
				for (var k = 0; k < state.objectCount; k++) {
					if (posMask.get(k) != 0) {                  
						var sprite = spriteimages[k];
						ctx.drawImage(sprite, xoffset + (i-mini) * cellwidth, yoffset + (j-minj) * cellheight);
					}
				}
			}
		}*/

		if (levelEditorOpened) {
			drawEditorIcons();
		}
	}
	
	// prep for interpolation
//	var start = interpolating * interpolating * 0.65 + interpolating * 0.35;
	var start = interpolating;
	var end = 1 - start;
	var previousLevel = drawLevel;
	if( backups != undefined && backups.length > 0 && displayIndex > 0 && displayIndex <= backups.length )
	{
		previousLevel = backups[ displayIndex - 1 ];
	}
	var i;
	var j;
	var k;
	
	// get background color in rgba
	var bgR = parseInt( state.bgcolor.substring( 1, 3 ), 16 ) / 255.0;
	var bgG = parseInt( state.bgcolor.substring( 3, 5 ), 16 ) / 255.0;
	var bgB = parseInt( state.bgcolor.substring( 5, 7 ), 16 ) / 255.0;
	var bgA = 1.0;
	
    // get gradient colours in rgba
	var gbR = parseInt( gradientBottom.substring( 1, 3 ), 16 ) / 255.0;
	var gbG = parseInt( gradientBottom.substring( 3, 5 ), 16 ) / 255.0;
	var gbB = parseInt( gradientBottom.substring( 5, 7 ), 16 ) / 255.0;
	var gbA = 1.0;
	
	var gmR = parseInt( gradientMiddle.substring( 1, 3 ), 16 ) / 255.0;
	var gmG = parseInt( gradientMiddle.substring( 3, 5 ), 16 ) / 255.0;
	var gmB = parseInt( gradientMiddle.substring( 5, 7 ), 16 ) / 255.0;
	var gmA = 1.0;
	
	var gtR = parseInt( gradientTop.substring( 1, 3 ), 16 ) / 255.0;
	var gtG = parseInt( gradientTop.substring( 3, 5 ), 16 ) / 255.0;
	var gtB = parseInt( gradientTop.substring( 5, 7 ), 16 ) / 255.0;
	var gtA = 1.0;
    
    // convert middle to be curve location rather than curve anchor
    // anchor = 2 * mid - 0.5 * top - 0.5 * bottom
    gmR = 2 * gmR - 0.5 * gtR - 0.5 * gbR;
    gmG = 2 * gmG - 0.5 * gtG - 0.5 * gbG;
    gmB = 2 * gmB - 0.5 * gtB - 0.5 * gbB;
    
	// draw in GL
	canvas.style.visibility = "hidden";
	gl.clearColor(bgR, bgG, bgB, bgA);
	gl.enable(gl.DEPTH_TEST);
	gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
	mat4.perspective(pMatrix, fieldOfView / 180.0 * Math.PI, canvas3d.width/canvas3d.height, 0.01, viewDistance);
	
	// camera
	mat4.fromXRotation( vMatrix, -viewPitch );
	var startYaw = facingToYaw( previousLevel.facing );
	var endYaw = facingToYaw( drawLevel.facing );
	if( Math.abs( endYaw - startYaw ) > Math.PI )
	{
		if( startYaw > endYaw )
		{
			endYaw += Math.PI * 2;
		}
		else
		{
			startYaw += Math.PI * 2;
		}
	}
	var yaw = startYaw * start + endYaw * end;
	mat4.rotateY( vMatrix, vMatrix, yaw );
	
	// reset start/end if just turning
	if( previousLevel.facing[0] != drawLevel.facing[0] || previousLevel.facing[1] != drawLevel.facing[1] )
	{
		start = 0;
		end = 1;
	}
	
	var halfi = ( mini + maxi ) * 0.5;
	var halfj = ( minj + maxj ) * 0.5;

	var px = 0;
	var py = 0;
	var playerPos = drawLevel.getPlayerPositions();
	var playerTween = "linear";
	var playerTweenV = 0;
	if( playerPos.length > 0 )
	{
		playerPos = playerPos[0];
		pxe = (playerPos/(drawLevel.height))|0;
		pye = (playerPos%drawLevel.height)|0;
		
		var playerMotions = drawLevel.getMotions( playerPos );
		var layers = getLayersOfMask(state.playerMask);
		var dir = dirMasksDelta[dirMasks.no];
		for (var i=0;i<layers.length;i++)
		{
			var layerMotion = playerMotions.getshiftor(0x1f, 5*layers[i]);
			if( ( layerMotion & dirMasks.moving ) != 0 && ( layerMotion & dirMasks.action ) != 0 )
			{
				// found a movement!
				// that'll do
				dir = dirMasksDelta[layerMotion & dirMasks.moving];
				break;
			}
		}
		pxs = pxe - dir[0];
		pys = pye - dir[1];
		
		// if moved, tween there
		// otherwise sit tight
		if( pxs != pxe || pys != pye )
		{
			drawLevel.getCellInto(pye + pxe * drawLevel.height,_o11);
			_o11.iand(state.playerMask);
			for (var i=0;i<state.objectCount;i++)
			{
				if (_o11.get(i))
				{
					var n = state.idDict[i];
					playerTween = state.objects[n].tween;
				}
			}
			
			px = tweenH( playerTween, pxs, pxe, start, end );
			py = tweenH( playerTween, pys, pye, start, end );
			playerTweenV = tweenV( playerTween, start, end );
		}
		else
		{
			px = pxs;
			py = pys;
		}
	}
	else
	{
		px = 0;
		py = 0;
	}
    mat4.translate( vMatrix, vMatrix, [ -px, -viewHeight - playerTweenV * 0.25, -py ] );
	
	// fixed uniforms
	gl.uniformMatrix4fv(pUniform, false, pMatrix);
	gl.uniformMatrix4fv(vUniform, false, vMatrix);
	gl.uniform1f(scaleHeightUniform,scaleHeight);
	gl.uniform1f(viewDistanceUniform,viewDistance);
	gl.uniform4f(fogColorUniform,bgR,bgG,bgB,bgA);
	gl.uniform1f(fogPowerUniform,fogPower);
	gl.uniform4f(gradientBottomUniform,gbR, gbG, gbB, gbA);
	gl.uniform4f(gradientMiddleUniform,gmR, gmG, gmB, gmA);
	gl.uniform4f(gradientTopUniform,gtR, gtG, gtB, gtA);
	
	// clamp draw around player
	mini = Math.max( mini, Math.floor( px - viewDistance - 1 ) );
	maxi = Math.min( maxi, Math.ceil( px + viewDistance + 1 ) );
	minj = Math.max( minj, Math.floor( py - viewDistance - 1 ) );
	maxj = Math.min( maxj, Math.ceil( py + viewDistance + 1 ) );
	
	// draw floors and ceilings first (no depth culling so they can overlap), then voxels and walls and sprites etc
	var tileCount = 0;
	for( var pass = 0; pass < 3; pass++ )
	{
		gl.depthFunc( pass != 1 ? gl.ALWAYS : gl.LEQUAL );
		
		for (var o = 0; o < state.objectCount; o++)
		{
			var buffer = spriteBuffers[o];
			if( buffer == undefined || buffer[2] == 0 )
			{
				// skip empty buffers
				continue;
			}
			
			// skip inappropriate render types
			var renderType = sprites[o].renderType;
			var tween = sprites[o].tween;
			if( pass != 0 && ( renderType == "floor" || renderType == "ceiling" || renderType == "sandwich" ) )
			{
				continue
			}
			if( pass != 1 && ( renderType == "sprite" || renderType == "billboard" || renderType == "wall" || renderType == "voxel" || renderType == "motionvoxel" || renderType == "facingvoxel" ) )
			{
				continue;
			}
			if( pass != 2 && renderType == "hud" )
			{
				continue;
			}
			
			// set buffers
			gl.bindBuffer(gl.ARRAY_BUFFER, buffer[0]);
			gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
			gl.bindBuffer(gl.ARRAY_BUFFER, buffer[1]);
			gl.vertexAttribPointer(vertexColorAttribute, 4, gl.FLOAT, false, 0, 0);
			
			// get layer info for motion reads
			var layer = state.objects[state.idDict[o]].layer;
			
			// clear transforms and gradient for hud mode
			if( pass == 2 )
			{
				mat4.identity(pMatrix);
				mat4.identity(vMatrix);
				gl.uniformMatrix4fv(pUniform, false, pMatrix);
				gl.uniformMatrix4fv(vUniform, false, vMatrix);
				gl.uniform4f(gradientBottomUniform,1, 1, 1, 1);
				gl.uniform4f(gradientMiddleUniform,1, 1, 1, 1);
				gl.uniform4f(gradientTopUniform,1, 1, 1, 1);
			}
			
			for (var ie = mini; ie < maxi; ie++)
			{
				for (var je = minj; je < maxj; je++)
				{
					var posIndex = je + ie * drawLevel.height;
					var posMask = drawLevel.getCellInto(posIndex,_o12);
					
					if (posMask.get(o) != 0)
					{
						// calc motion
						var layerMotions = drawLevel.getMotions( posIndex );
						var dir = dirMasksDelta[dirMasks.no];
						var layerMotion = layerMotions.getshiftor(0x1f, 5*layer);
						var layerFacing = layerMotions.getshiftor(0xf, 5*layer);
						if( ( layerFacing & dirMasks.moving ) != 0 )
						{
							// found a direction!
							dir = dirMasksDelta[layerFacing];
						}

						is = ie - dir[0];
						js = je - dir[1];
		
						// only tween if in motion
						if( ( layerMotion & dirMasks.action ) != 0 && ( dir[0] != 0 || dir[1] != 0 ) )
						{
							i = tweenH( tween, is, ie, start, end );
							j = tweenH( tween, js, je, start, end );
							k = tweenV( tween, start, end );
						}
						else
						{
							i = ie;
							j = je;
							k = 0;
						}

						tileCount += 1;
						if( renderType == "floor" || renderType == "ceiling" || renderType == "sandwich" || renderType == "wall" || renderType == "voxel" )
						{
							mat4.identity( mMatrix );
							mat4.translate( mMatrix, mMatrix, [ i, k, j] );
							setMUniform();
							gl.drawArrays(gl.TRIANGLES, 0, buffer[2]);
						}
						else if( renderType == "sprite" || renderType == "facingvoxel" )
						{
							mat4.identity( mMatrix );
							mat4.translate(mMatrix, mMatrix, [ i, k, j] );
							if( i != px || j != py )
							{
								mat4.rotateY(mMatrix, mMatrix, -Math.PI * 0.5 - Math.atan2( j - py, i - px ) );
							}
							setMUniform();
							gl.drawArrays(gl.TRIANGLES, 0, buffer[2]);
						}
						else if( renderType == "billboard" )
						{
							mat4.identity( mMatrix );
							mat4.translate(mMatrix, mMatrix, [ i, k, j] );
							mat4.rotateY(mMatrix, mMatrix, -yaw );
							setMUniform();
							gl.drawArrays(gl.TRIANGLES, 0, buffer[2]);
						}
						else if( renderType == "motionvoxel" )
						{
							mat4.identity( mMatrix );
							mat4.translate(mMatrix, mMatrix, [ i, k , j] );
							if( dir[0] != 0 || dir[1] != 0 )
							{
								mat4.rotateY(mMatrix, mMatrix, Math.atan2( dir[0], dir[1] ) );
							}
							//mat4.rotateX(mMatrix, mMatrix, Math.sin( end * Math.PI * 2 ) * 0.125 );
							var pitch = tweenP( tween, start, end );	// get pitch in case we want a pitch tween
							if( tweenP != 0 )
							{
								mat4.translate(mMatrix,mMatrix,[0,0.5,0]);
								mat4.rotateX(mMatrix, mMatrix, pitch );
								mat4.translate(mMatrix,mMatrix,[0,-0.5,0]);
							}
							setMUniform();
							gl.drawArrays(gl.TRIANGLES, 0, buffer[2]);
						}						
						else if( renderType == "hud" )
						{
							mat4.identity( mMatrix );
							mat4.translate( mMatrix, mMatrix, [ 0, -1, 0 ] );
							mat4.scale( mMatrix, mMatrix, [ 2, 2, 2 ] );
							setMUniform();
							gl.drawArrays(gl.TRIANGLES, 0, buffer[2]);
						}
					}
				}
			}
		}
	}
//	console.log( tileCount );
}

function facingToYaw( facing )
{
	if( facing[0] == -1 )
	{
		return Math.PI * 1.5;
	}
	else if( facing[0] == 1 )
	{
		return Math.PI * 0.5;
	}
	else if( facing[1] == 1 )
	{
		return Math.PI;
	}
	return 0;
}

function tweenH( tween, startPos, endPos, start, end )
{
	switch( tween )
	{
		case "easein":
			var a = end * end;
			return startPos * ( 1 - a ) + endPos * a;
		case "easeout":
			var a = 1 - ( 1 - end ) * ( 1 - end );
			return startPos * ( 1 - a ) + endPos * a;
		case "smoothstep":
			var a = end * end * ( 3 - 2 * end );
			return startPos * ( 1 - a ) + endPos * a;
		case "throw":
			var t1 = Math.min( end / 0.66666, 1 );
			var t2 = Math.max( ( end - 0.66666 ) / ( 1 - 0.66666 ), 0 );
			var d1 = 1 - ( 1 - 0.66666 ) * 0.5;
			var a = t1 * d1 + t2 * ( 1 - d1 ); 
			return startPos * ( 1 - a ) + endPos * a;
			break;
		case "linear":
		default:
			return start * startPos + end * endPos;
			break;
	}
}

function tweenV( tween, start, end )
{
	switch( tween )
	{
		case "step":
		case "bound":
			return ( 1 - Math.pow( end * 2 - 1, 2 ) ) * 0.25;
			break;
		case "doublestep":
			return ( 1 - Math.pow( ( ( end * 2 ) % 1 )  * 2 - 1, 2 ) ) * 0.125;
			break;
		case "throw":
			var t1 = Math.min( end / 0.66666, 1 );
			var t2 = Math.max( ( end - 0.66666 ) / ( 1 - 0.66666 ), 0 );
			if( end == 1 )
			{
				t1 = 1;
				t2 = 1;
			}
			return tweenV( "bound", t1, t1 ) + tweenV( "bound", t2, t2 ) * 0.125;
			break;
		default:
			return 0;
			break;
	}
}

function tweenP( tween, start, end )
{
	switch( tween )
	{
		case "bound":
			return Math.sin( end * Math.PI * 2 ) * -0.125;
			break;
		case "roll":
			return end * Math.PI * 2;
			break;
		default:
			return 0;
			break;
	}
}

function drawEditorIcons() {
	var glyphCount = glyphImages.length;
	var glyphStartIndex=0;
	var glyphEndIndex = glyphImages.length;/*Math.min(
							glyphStartIndex+10,
							screenwidth-2,
							glyphStartIndex+Math.max(glyphCount-glyphStartIndex,0)
							);*/
	var glyphsToDisplay = glyphEndIndex-glyphStartIndex;

	ctx.drawImage(glyphPrintButton,xoffset-cellwidth,yoffset-cellheight*(1+editorRowCount));
	if (mouseCoordY===(-1-editorRowCount)&&mouseCoordX===-1) {
			ctx.drawImage(glyphMouseOver,xoffset-cellwidth,yoffset-cellheight*(1+editorRowCount));								
	}

	var ypos = editorRowCount-(-mouseCoordY-2)-1;
	var mouseIndex=mouseCoordX+(screenwidth-1)*ypos;

	for (var i=0;i<glyphsToDisplay;i++) {
		var glyphIndex = glyphStartIndex+i;
		var sprite = glyphImages[glyphIndex];
		var xpos=i%(screenwidth-1);
		var ypos=(i/(screenwidth-1))|0;
		ctx.drawImage(sprite,xoffset+(xpos)*cellwidth,yoffset+ypos*cellheight-cellheight*(1+editorRowCount));
		if (mouseCoordX>=0&&mouseCoordX<(screenwidth-1)&&mouseIndex===i) {
			ctx.drawImage(glyphMouseOver,xoffset+xpos*cellwidth,yoffset+ypos*cellheight-cellheight*(1+editorRowCount));						
		}
		if (i===glyphSelectedIndex) {
			ctx.drawImage(glyphHighlight,xoffset+xpos*cellwidth,yoffset+ypos*cellheight-cellheight*(1+editorRowCount));
		} 		
	}
	if (mouseCoordX>=-1&&mouseCoordY>=-1&&mouseCoordX<screenwidth-1&&mouseCoordY<screenheight-1-editorRowCount) {
		if (mouseCoordX==-1||mouseCoordY==-1||mouseCoordX==screenwidth-2||mouseCoordY===screenheight-2-editorRowCount) {
			ctx.drawImage(glyphHighlightResize,
				xoffset+mouseCoordX*cellwidth,
				yoffset+mouseCoordY*cellheight
				);								
		} else {
			ctx.drawImage(glyphHighlight,
				xoffset+mouseCoordX*cellwidth,
				yoffset+mouseCoordY*cellheight
				);				
		}
	}
}

var lastDownTarget;

var oldcellwidth=0;
var oldcellheight=0;
var oldtextmode=-1;
var oldfgcolor=-1;
var forceRegenImages=false;
function canvasResize() {
	canvas3d.width = canvas.width = canvas.parentNode.clientWidth;
	canvas3d.height = canvas.height = canvas.parentNode.clientHeight;
	gl.viewport(0, 0, canvas3d.width, canvas3d.height);
	
	screenwidth=level.width;
	screenheight=level.height;
	if (state!==undefined){
		flickscreen=state.metadata.flickscreen!==undefined;
		zoomscreen=state.metadata.zoomscreen!==undefined;
		if (levelEditorOpened) {
			screenwidth+=2;
			var glyphcount = glyphCount();
			editorRowCount = Math.ceil(glyphcount/(screenwidth-1));
			screenheight+=2+editorRowCount;
		} else if (flickscreen) {
			screenwidth=state.metadata.flickscreen[0];
			screenheight=state.metadata.flickscreen[1];
		} else if (zoomscreen) {
			screenwidth=state.metadata.zoomscreen[0];
			screenheight=state.metadata.zoomscreen[1];
		}
	}

	if (textMode) {
		screenwidth=titleWidth;
		screenheight=titleHeight;
	}
	
	cellwidth = canvas.width / screenwidth;
	cellheight = canvas.height / screenheight;

	var w = 5;//sprites[1].dat.length;
	var h = 5;//sprites[1].dat[0].length;


	if (textMode) {
		w=6;
		h=6;
	}

	cellwidth = w * ~~(cellwidth / w);
	cellheight = h * ~~(cellheight / h);

	xoffset = 0;
	yoffset = 0;

	if (cellwidth > cellheight) {
		cellwidth = cellheight;
		xoffset = (canvas.width - cellwidth * screenwidth) / 2;
		yoffset = (canvas.height - cellheight * screenheight) / 2;
	}
	else { //if (cellheight > cellwidth) {
		cellheight = cellwidth;
		yoffset = (canvas.height - cellheight * screenheight) / 2;
		xoffset = (canvas.width - cellwidth * screenwidth) / 2;
	}
	magnification = ((cellwidth/w)*5)|0;

	if (levelEditorOpened && !textMode) {
		xoffset+=cellwidth;
		yoffset+=cellheight*(1+editorRowCount);
	}

	cellwidth = cellwidth|0;
	cellheight = cellheight|0;
	xoffset = xoffset|0;
	yoffset = yoffset|0;

	if (oldcellwidth!=cellwidth||oldcellheight!=cellheight||oldtextmode!=textMode||oldfgcolor!=state.fgcolor||forceRegenImages){
		forceRegenImages=false;
		regenSpriteImages();
	}

	oldcellheight=cellheight;
	oldcellwidth=cellwidth;
	oldtextmode=textMode;
	oldfgcolor=state.fgcolor;

	redraw();
}

function setMUniform()
{
	gl.uniformMatrix4fv(mUniform, false, mMatrix);
}
