diff --git a/pixelgl/canvas.go b/pixelgl/canvas.go
index b8e40578c43d49fd9d01a008581459f405b566fc..8a5be0bf9179529a020532d1fff807f77bd30d18 100644
--- a/pixelgl/canvas.go
+++ b/pixelgl/canvas.go
@@ -17,7 +17,7 @@ import (
 // It supports TrianglesPosition, TrianglesColor, TrianglesPicture and PictureColor.
 type Canvas struct {
 	gf     *GLFrame
-	shader *glhf.Shader
+	shader *glShader
 
 	cmp    pixel.ComposeMethod
 	mat    mgl32.Mat3
@@ -37,24 +37,24 @@ func NewCanvas(bounds pixel.Rect) *Canvas {
 		col: mgl32.Vec4{1, 1, 1, 1},
 	}
 
+	baseShader(c)
 	c.SetBounds(bounds)
+	c.shader.update()
+	return c
+}
 
-	var shader *glhf.Shader
-	mainthread.Call(func() {
-		var err error
-		shader, err = glhf.NewShader(
-			canvasVertexFormat,
-			canvasUniformFormat,
-			canvasVertexShader,
-			canvasFragmentShader,
-		)
-		if err != nil {
-			panic(errors.Wrap(err, "failed to create Canvas, there's a bug in the shader"))
-		}
-	})
-	c.shader = shader
+// SetUniform will update the named uniform with the value of any supported underlying
+// attribute variable. If the uniform already exists, including defaults, they will be reassigned
+// to the new value. The value can be a pointer.
+func (c *Canvas) SetUniform(Name string, Value interface{}) {
+	c.shader.setUniform(Name, Value)
+}
 
-	return c
+// SetFragmentShader allows you to set a new fragment shader on the underlying
+// framebuffer. Argument "src" is the GLSL source, not a filename.
+func (c *Canvas) SetFragmentShader(src string) {
+	c.shader.fs = src
+	c.shader.update()
 }
 
 // MakeTriangles creates a specialized copy of the supplied Triangles that draws onto this Canvas.
@@ -62,7 +62,7 @@ func NewCanvas(bounds pixel.Rect) *Canvas {
 // TrianglesPosition, TrianglesColor and TrianglesPicture are supported.
 func (c *Canvas) MakeTriangles(t pixel.Triangles) pixel.TargetTriangles {
 	return &canvasTriangles{
-		GLTriangles: NewGLTriangles(c.shader, t),
+		GLTriangles: NewGLTriangles(c.shader.s, t),
 		dst:         c,
 	}
 }
@@ -279,29 +279,41 @@ func (ct *canvasTriangles) draw(tex *glhf.Texture, bounds pixel.Rect) {
 
 	// save the current state vars to avoid race condition
 	cmp := ct.dst.cmp
+	smt := ct.dst.smooth
 	mat := ct.dst.mat
 	col := ct.dst.col
-	smt := ct.dst.smooth
 
 	mainthread.CallNonBlock(func() {
 		ct.dst.setGlhfBounds()
 		setBlendFunc(cmp)
 
 		frame := ct.dst.gf.Frame()
-		shader := ct.dst.shader
+		shader := ct.dst.shader.s
 
 		frame.Begin()
 		shader.Begin()
 
+		ct.dst.shader.uniformDefaults.transform = mat
+		ct.dst.shader.uniformDefaults.colormask = col
 		dstBounds := ct.dst.Bounds()
-		shader.SetUniformAttr(canvasBounds, mgl32.Vec4{
+		ct.dst.shader.uniformDefaults.bounds = mgl32.Vec4{
 			float32(dstBounds.Min.X),
 			float32(dstBounds.Min.Y),
 			float32(dstBounds.W()),
 			float32(dstBounds.H()),
-		})
-		shader.SetUniformAttr(canvasTransform, mat)
-		shader.SetUniformAttr(canvasColorMask, col)
+		}
+
+		bx, by, bw, bh := intBounds(bounds)
+		ct.dst.shader.uniformDefaults.texbounds = mgl32.Vec4{
+			float32(bx),
+			float32(by),
+			float32(bw),
+			float32(bh),
+		}
+
+		for loc, u := range ct.dst.shader.uniforms {
+			ct.dst.shader.s.SetUniformAttr(loc, u.Value())
+		}
 
 		if tex == nil {
 			ct.vs.Begin()
@@ -310,14 +322,6 @@ func (ct *canvasTriangles) draw(tex *glhf.Texture, bounds pixel.Rect) {
 		} else {
 			tex.Begin()
 
-			bx, by, bw, bh := intBounds(bounds)
-			shader.SetUniformAttr(canvasTexBounds, mgl32.Vec4{
-				float32(bx),
-				float32(by),
-				float32(bw),
-				float32(bh),
-			})
-
 			if tex.Smooth() != smt {
 				tex.SetSmooth(smt)
 			}
@@ -358,74 +362,9 @@ const (
 	canvasIntensity
 )
 
-var canvasVertexFormat = glhf.AttrFormat{
+var defaultCanvasVertexFormat = glhf.AttrFormat{
 	canvasPosition:  {Name: "position", Type: glhf.Vec2},
 	canvasColor:     {Name: "color", Type: glhf.Vec4},
 	canvasTexCoords: {Name: "texCoords", Type: glhf.Vec2},
 	canvasIntensity: {Name: "intensity", Type: glhf.Float},
 }
-
-const (
-	canvasTransform int = iota
-	canvasColorMask
-	canvasBounds
-	canvasTexBounds
-)
-
-var canvasUniformFormat = glhf.AttrFormat{
-	canvasTransform: {Name: "transform", Type: glhf.Mat3},
-	canvasColorMask: {Name: "colorMask", Type: glhf.Vec4},
-	canvasBounds:    {Name: "bounds", Type: glhf.Vec4},
-	canvasTexBounds: {Name: "texBounds", Type: glhf.Vec4},
-}
-
-var canvasVertexShader = `
-#version 330 core
-
-in vec2 position;
-in vec4 color;
-in vec2 texCoords;
-in float intensity;
-
-out vec4 Color;
-out vec2 TexCoords;
-out float Intensity;
-
-uniform mat3 transform;
-uniform vec4 bounds;
-
-void main() {
-	vec2 transPos = (transform * vec3(position, 1.0)).xy;
-	vec2 normPos = (transPos - bounds.xy) / bounds.zw * 2 - vec2(1, 1);
-	gl_Position = vec4(normPos, 0.0, 1.0);
-	Color = color;
-	TexCoords = texCoords;
-	Intensity = intensity;
-}
-`
-
-var canvasFragmentShader = `
-#version 330 core
-
-in vec4 Color;
-in vec2 TexCoords;
-in float Intensity;
-
-out vec4 color;
-
-uniform vec4 colorMask;
-uniform vec4 texBounds;
-uniform sampler2D tex;
-
-void main() {
-	if (Intensity == 0) {
-		color = colorMask * Color;
-	} else {
-		color = vec4(0, 0, 0, 0);
-		color += (1 - Intensity) * Color;
-		vec2 t = (TexCoords - texBounds.xy) / texBounds.zw;
-		color += Intensity * Color * texture(tex, t);
-		color *= colorMask;
-	}
-}
-`
diff --git a/pixelgl/glshader.go b/pixelgl/glshader.go
new file mode 100644
index 0000000000000000000000000000000000000000..d1eb3a3de9221148e7a419ba99c2a4c27059e2f3
--- /dev/null
+++ b/pixelgl/glshader.go
@@ -0,0 +1,268 @@
+package pixelgl
+
+import (
+	"github.com/faiface/glhf"
+	"github.com/faiface/mainthread"
+	"github.com/go-gl/mathgl/mgl32"
+	"github.com/pkg/errors"
+)
+
+// glShader is a type to assist with managing a canvas's underlying
+// shader configuration. This allows for customization of shaders on
+// a per canvas basis.
+type glShader struct {
+	s      *glhf.Shader
+	vf, uf glhf.AttrFormat
+	vs, fs string
+
+	uniforms []gsUniformAttr
+
+	uniformDefaults struct {
+		transform mgl32.Mat3
+		colormask mgl32.Vec4
+		bounds    mgl32.Vec4
+		texbounds mgl32.Vec4
+	}
+}
+
+type gsUniformAttr struct {
+	Name      string
+	Type      glhf.AttrType
+	value     interface{}
+	ispointer bool
+}
+
+// reinitialize GLShader data and recompile the underlying gl shader object
+func (gs *glShader) update() {
+	gs.uf = nil
+	for _, u := range gs.uniforms {
+		gs.uf = append(gs.uf, glhf.Attr{
+			Name: u.Name,
+			Type: u.Type,
+		})
+	}
+	var shader *glhf.Shader
+	mainthread.Call(func() {
+		var err error
+		shader, err = glhf.NewShader(
+			gs.vf,
+			gs.uf,
+			gs.vs,
+			gs.fs,
+		)
+		if err != nil {
+			panic(errors.Wrap(err, "failed to create Canvas, there's a bug in the shader"))
+		}
+	})
+
+	gs.s = shader
+}
+
+// gets the uniform index from GLShader
+func (gs *glShader) getUniform(Name string) int {
+	for i, u := range gs.uniforms {
+		if u.Name == Name {
+			return i
+		}
+	}
+	return -1
+}
+
+// SetUniform appends a custom uniform name and value to the shader.
+// if the uniform already exists, it will simply be overwritten.
+//
+// example:
+//
+//   utime := float32(time.Since(starttime)).Seconds())
+//   mycanvas.shader.AddUniform("u_time", &utime)
+func (gs *glShader) setUniform(name string, value interface{}) {
+	t, p := getAttrType(value)
+	if loc := gs.getUniform(name); loc > -1 {
+		gs.uniforms[loc].Name = name
+		gs.uniforms[loc].Type = t
+		gs.uniforms[loc].ispointer = p
+		gs.uniforms[loc].value = value
+		return
+	}
+	gs.uniforms = append(gs.uniforms, gsUniformAttr{
+		Name:      name,
+		Type:      t,
+		ispointer: p,
+		value:     value,
+	})
+}
+
+// Sets up a base shader with everything needed for a Pixel
+// canvas to render correctly. The defaults can be overridden
+// by simply using the SetUniform function.
+func baseShader(c *Canvas) {
+	gs := &glShader{
+		vf: defaultCanvasVertexFormat,
+		vs: defaultCanvasVertexShader,
+		fs: baseCanvasFragmentShader,
+	}
+
+	gs.setUniform("u_transform", &gs.uniformDefaults.transform)
+	gs.setUniform("u_colormask", &gs.uniformDefaults.colormask)
+	gs.setUniform("u_bounds", &gs.uniformDefaults.bounds)
+	gs.setUniform("u_texbounds", &gs.uniformDefaults.texbounds)
+
+	c.shader = gs
+}
+
+// Value returns the attribute's concrete value. If the stored value
+// is a pointer, we return the dereferenced value.
+func (gu *gsUniformAttr) Value() interface{} {
+	if !gu.ispointer {
+		return gu.value
+	}
+	switch gu.Type {
+	case glhf.Vec2:
+		return *gu.value.(*mgl32.Vec2)
+	case glhf.Vec3:
+		return *gu.value.(*mgl32.Vec3)
+	case glhf.Vec4:
+		return *gu.value.(*mgl32.Vec4)
+	case glhf.Mat2:
+		return *gu.value.(*mgl32.Mat2)
+	case glhf.Mat23:
+		return *gu.value.(*mgl32.Mat2x3)
+	case glhf.Mat24:
+		return *gu.value.(*mgl32.Mat2x4)
+	case glhf.Mat3:
+		return *gu.value.(*mgl32.Mat3)
+	case glhf.Mat32:
+		return *gu.value.(*mgl32.Mat3x2)
+	case glhf.Mat34:
+		return *gu.value.(*mgl32.Mat3x4)
+	case glhf.Mat4:
+		return *gu.value.(*mgl32.Mat4)
+	case glhf.Mat42:
+		return *gu.value.(*mgl32.Mat4x2)
+	case glhf.Mat43:
+		return *gu.value.(*mgl32.Mat4x3)
+	case glhf.Int:
+		return *gu.value.(*int32)
+	case glhf.Float:
+		return *gu.value.(*float32)
+	default:
+		panic("invalid attrtype")
+	}
+}
+
+// Returns the type identifier for any (supported) attribute variable type
+// and whether or not it is a pointer of that type.
+func getAttrType(v interface{}) (glhf.AttrType, bool) {
+	switch v.(type) {
+	case int32:
+		return glhf.Int, false
+	case float32:
+		return glhf.Float, false
+	case mgl32.Vec2:
+		return glhf.Vec2, false
+	case mgl32.Vec3:
+		return glhf.Vec3, false
+	case mgl32.Vec4:
+		return glhf.Vec4, false
+	case mgl32.Mat2:
+		return glhf.Mat2, false
+	case mgl32.Mat2x3:
+		return glhf.Mat23, false
+	case mgl32.Mat2x4:
+		return glhf.Mat24, false
+	case mgl32.Mat3:
+		return glhf.Mat3, false
+	case mgl32.Mat3x2:
+		return glhf.Mat32, false
+	case mgl32.Mat3x4:
+		return glhf.Mat34, false
+	case mgl32.Mat4:
+		return glhf.Mat4, false
+	case mgl32.Mat4x2:
+		return glhf.Mat42, false
+	case mgl32.Mat4x3:
+		return glhf.Mat43, false
+	case *mgl32.Vec2:
+		return glhf.Vec2, true
+	case *mgl32.Vec3:
+		return glhf.Vec3, true
+	case *mgl32.Vec4:
+		return glhf.Vec4, true
+	case *mgl32.Mat2:
+		return glhf.Mat2, true
+	case *mgl32.Mat2x3:
+		return glhf.Mat23, true
+	case *mgl32.Mat2x4:
+		return glhf.Mat24, true
+	case *mgl32.Mat3:
+		return glhf.Mat3, true
+	case *mgl32.Mat3x2:
+		return glhf.Mat32, true
+	case *mgl32.Mat3x4:
+		return glhf.Mat34, true
+	case *mgl32.Mat4:
+		return glhf.Mat4, true
+	case *mgl32.Mat4x2:
+		return glhf.Mat42, true
+	case *mgl32.Mat4x3:
+		return glhf.Mat43, true
+	case *int32:
+		return glhf.Int, true
+	case *float32:
+		return glhf.Float, true
+	default:
+		panic("invalid AttrType")
+	}
+}
+
+var defaultCanvasVertexShader = `
+#version 330 core
+
+in vec2 position;
+in vec4 color;
+in vec2 texCoords;
+in float intensity;
+out vec4 Color;
+out vec2 texcoords;
+out vec2 glpos;
+out float Intensity;
+
+uniform mat3 u_transform;
+uniform vec4 u_bounds;
+
+void main() {
+	vec2 transPos = (u_transform * vec3(position, 1.0)).xy;
+	vec2 normPos = (transPos - u_bounds.xy) / u_bounds.zw * 2 - vec2(1, 1);
+	gl_Position = vec4(normPos, 0.0, 1.0);
+	Color = color;
+	texcoords = texCoords;
+	Intensity = intensity;
+	glpos = transPos;
+}
+`
+
+var baseCanvasFragmentShader = `
+#version 330 core
+
+in vec4 Color;
+in vec2 texcoords;
+in float Intensity;
+
+out vec4 fragColor;
+
+uniform vec4 u_colormask;
+uniform vec4 u_texbounds;
+uniform sampler2D u_texture;
+
+void main() {
+	if (Intensity == 0) {
+		fragColor = u_colormask * Color;
+	} else {
+		fragColor = vec4(0, 0, 0, 0);
+		fragColor += (1 - Intensity) * Color;
+		vec2 t = (texcoords - u_texbounds.xy) / u_texbounds.zw;
+		fragColor += Intensity * Color * texture(u_texture, t);
+		fragColor *= u_colormask;
+	}
+}
+`
diff --git a/pixelgl/window.go b/pixelgl/window.go
index ddc5426c93055cb878a5ac6ba8d38a968873a3c5..2d646c5331eb0880f969222950bdffeed0655617 100644
--- a/pixelgl/window.go
+++ b/pixelgl/window.go
@@ -424,3 +424,8 @@ func (w *Window) Clear(c color.Color) {
 func (w *Window) Color(at pixel.Vec) pixel.RGBA {
 	return w.canvas.Color(at)
 }
+
+// Canvas returns the window's underlying Canvas
+func (w *Window) Canvas() *Canvas {
+	return w.canvas
+}