diff --git a/canvas.go b/canvas.go
new file mode 100644
index 0000000000000000000000000000000000000000..be8c8d7708572805c7b3982042f0b2c9a3817108
--- /dev/null
+++ b/canvas.go
@@ -0,0 +1,222 @@
+package pixel
+
+import (
+	"image/color"
+
+	"github.com/faiface/mainthread"
+	"github.com/faiface/pixel/pixelgl"
+	"github.com/go-gl/mathgl/mgl32"
+	"github.com/pkg/errors"
+)
+
+// Canvas is basically a Picture that you can draw on.
+//
+// Canvas supports TrianglesPosition, TrianglesColor and TrianglesTexture.
+type Canvas struct {
+	f *pixelgl.Frame
+	s *pixelgl.Shader
+
+	pic *Picture
+	mat mgl32.Mat3
+	col mgl32.Vec4
+	bnd mgl32.Vec4
+}
+
+// NewCanvas creates a new fully transparent Canvas with specified dimensions in pixels.
+func NewCanvas(width, height float64, smooth bool) *Canvas {
+	c := &Canvas{}
+	mainthread.Call(func() {
+		var err error
+		c.f = pixelgl.NewFrame(int(width), int(height), smooth)
+		c.s, err = pixelgl.NewShader(
+			canvasVertexFormat,
+			canvasUniformFormat,
+			canvasVertexShader,
+			canvasFragmentShader,
+		)
+		if err != nil {
+			panic(errors.Wrap(err, "failed to create canvas"))
+		}
+	})
+	c.pic = nil
+	c.mat = mgl32.Ident3()
+	c.col = mgl32.Vec4{1, 1, 1, 1}
+	c.bnd = mgl32.Vec4{0, 0, 1, 1}
+	return c
+}
+
+// Size returns the width and the height of the Canvas in pixels.
+func (c *Canvas) Size() (width, height float64) {
+	return float64(c.f.Width()), float64(c.f.Height())
+}
+
+// Content returns a Picture that contains the content of this Canvas. The returned Picture changes
+// as you draw onto the Canvas, so there is no real need to call this method more than once.
+func (c *Canvas) Content() *Picture {
+	tex := c.f.Texture()
+	return &Picture{
+		texture: tex,
+		bounds:  R(0, 0, float64(tex.Width()), float64(tex.Height())),
+	}
+}
+
+// Clear fill the whole Canvas with on specified color.
+func (c *Canvas) Clear(col color.Color) {
+	mainthread.CallNonBlock(func() {
+		c.f.Begin()
+		col := NRGBAModel.Convert(col).(NRGBA)
+		pixelgl.Clear(float32(col.R), float32(col.G), float32(col.B), float32(col.A))
+		c.f.End()
+	})
+}
+
+// MakeTriangles returns Triangles that draw onto this Canvas.
+func (c *Canvas) MakeTriangles(t Triangles) Triangles {
+	tpcs := NewGLTriangles(c.s, t).(trianglesPositionColorTexture)
+	return &canvasTriangles{
+		c: c,
+		trianglesPositionColorTexture: tpcs,
+	}
+}
+
+// SetPicture sets a Picture that will be used in further draw operations.
+//
+// This does not set the Picture that this Canvas draws onto, don't confuse it.
+func (c *Canvas) SetPicture(p *Picture) {
+	if p != nil {
+		min := pictureBounds(p, V(0, 0))
+		max := pictureBounds(p, V(1, 1))
+		c.bnd = mgl32.Vec4{
+			float32(min.X()), float32(min.Y()),
+			float32(max.X()), float32(max.Y()),
+		}
+	}
+	c.pic = p
+}
+
+// SetTransform sets the transformations used in further draw operations.
+func (c *Canvas) SetTransform(t ...Transform) {
+	c.mat = transformToMat(t...)
+}
+
+// SetMaskColor sets the mask color used in further draw operations.
+func (c *Canvas) SetMaskColor(col color.Color) {
+	if col == nil {
+		col = NRGBA{1, 1, 1, 1}
+	}
+	nrgba := NRGBAModel.Convert(col).(NRGBA)
+	r := float32(nrgba.R)
+	g := float32(nrgba.G)
+	b := float32(nrgba.B)
+	a := float32(nrgba.A)
+	c.col = mgl32.Vec4{r, g, b, a}
+}
+
+type trianglesPositionColorTexture interface {
+	Triangles
+	Position(i int) Vec
+	Color(i int) NRGBA
+	Texture(i int) Vec
+}
+
+type canvasTriangles struct {
+	c *Canvas
+	trianglesPositionColorTexture
+}
+
+func (ct *canvasTriangles) Draw() {
+	// avoid possible race condition
+	pic := ct.c.pic
+	mat := ct.c.mat
+	col := ct.c.col
+	bnd := ct.c.bnd
+
+	mainthread.CallNonBlock(func() {
+		ct.c.f.Begin()
+		ct.c.s.Begin()
+
+		ct.c.s.SetUniformAttr(canvasTransformMat3, mat)
+		ct.c.s.SetUniformAttr(canvasMaskColorVec4, col)
+		ct.c.s.SetUniformAttr(canvasBoundsVec4, bnd)
+
+		if pic != nil {
+			pic.Texture().Begin()
+			ct.trianglesPositionColorTexture.Draw()
+			pic.Texture().End()
+		} else {
+			ct.trianglesPositionColorTexture.Draw()
+		}
+
+		ct.c.s.End()
+		ct.c.f.End()
+	})
+}
+
+const (
+	canvasPositionVec2 int = iota
+	canvasColorVec4
+	canvasTextureVec2
+)
+
+var canvasVertexFormat = pixelgl.AttrFormat{
+	canvasPositionVec2: {Name: "position", Type: pixelgl.Vec2},
+	canvasColorVec4:    {Name: "color", Type: pixelgl.Vec4},
+	canvasTextureVec2:  {Name: "texture", Type: pixelgl.Vec2},
+}
+
+const (
+	canvasMaskColorVec4 int = iota
+	canvasTransformMat3
+	canvasBoundsVec4
+)
+
+var canvasUniformFormat = pixelgl.AttrFormat{
+	{Name: "maskColor", Type: pixelgl.Vec4},
+	{Name: "transform", Type: pixelgl.Mat3},
+	{Name: "bounds", Type: pixelgl.Vec4},
+}
+
+var canvasVertexShader = `
+#version 330 core
+
+in vec2 position;
+in vec4 color;
+in vec2 texture;
+
+out vec4 Color;
+out vec2 Texture;
+
+uniform mat3 transform;
+
+void main() {
+	gl_Position = vec4((transform * vec3(position.x, position.y, 1.0)).xy, 0.0, 1.0);
+	Color = color;
+	Texture = texture;
+}
+`
+
+var canvasFragmentShader = `
+#version 330 core
+
+in vec4 Color;
+in vec2 Texture;
+
+out vec4 color;
+
+uniform vec4 maskColor;
+uniform vec4 bounds;
+uniform sampler2D tex;
+
+void main() {
+	vec2 boundsMin = bounds.xy;
+	vec2 boundsMax = bounds.zw;
+
+	if (Texture == vec2(-1, -1)) {
+		color = maskColor * Color;
+	} else {
+		float tx = boundsMin.x * (1 - Texture.x) + boundsMax.x * Texture.x;
+		float ty = boundsMin.y * (1 - Texture.y) + boundsMax.y * Texture.y;
+		color = maskColor * Color * texture(tex, vec2(tx, ty));
+	}
+}
+`
diff --git a/window.go b/window.go
index 3fa543166c0e1a9cb3d465926bdd458e8721fb78..75a5f03ca6383a68dd92c9d1acb5e004adcfc737 100644
--- a/window.go
+++ b/window.go
@@ -8,7 +8,6 @@ import (
 	"github.com/faiface/mainthread"
 	"github.com/faiface/pixel/pixelgl"
 	"github.com/go-gl/glfw/v3.2/glfw"
-	"github.com/go-gl/mathgl/mgl32"
 	"github.com/pkg/errors"
 )
 
@@ -60,16 +59,10 @@ type Window struct {
 	enabled bool
 	window  *glfw.Window
 	config  WindowConfig
-	shader  *pixelgl.Shader
 
-	// cache
-	width, height float64
-
-	// Target stuff, Picture, transformation matrix and color
-	pic *Picture
-	mat mgl32.Mat3
-	col mgl32.Vec4
-	bnd mgl32.Vec4
+	canvas   *Canvas
+	canvasVs *pixelgl.VertexSlice
+	shader   *pixelgl.Shader
 
 	// need to save these to correctly restore a fullscreen window
 	restore struct {
@@ -80,9 +73,6 @@ type Window struct {
 		buttons [KeyLast + 1]bool
 		scroll  Vec
 	}
-
-	//DEBUG
-	Frame *pixelgl.Frame
 }
 
 var currentWindow *Window
@@ -120,42 +110,52 @@ func NewWindow(config WindowConfig) (*Window, error) {
 		if currentWindow != nil {
 			share = currentWindow.window
 		}
-		w.window, err = glfw.CreateWindow(int(config.Width), int(config.Height), config.Title, nil, share)
+		w.window, err = glfw.CreateWindow(
+			int(config.Width),
+			int(config.Height),
+			config.Title,
+			nil,
+			share,
+		)
 		if err != nil {
 			return err
 		}
 
-		return nil
-	})
-	if err != nil {
-		return nil, errors.Wrap(err, "creating window failed")
-	}
-
-	mainthread.Call(func() {
+		// enter the OpenGL context
 		w.begin()
 		w.end()
 
 		w.shader, err = pixelgl.NewShader(
-			defaultVertexFormat,
-			defaultUniformFormat,
-			defaultVertexShader,
-			defaultFragmentShader,
+			windowVertexFormat,
+			windowUniformFormat,
+			windowVertexShader,
+			windowFragmentShader,
 		)
 		if err != nil {
-			panic(errors.Wrap(err, "NewWindow: failed to create shader"))
+			return err
 		}
+
+		w.canvasVs = pixelgl.MakeVertexSlice(w.shader, 6, 6)
+		w.canvasVs.Begin()
+		w.canvasVs.SetVertexData([]float32{
+			-1, -1, 0, 0,
+			1, -1, 1, 0,
+			1, 1, 1, 1,
+			-1, -1, 0, 0,
+			1, 1, 1, 1,
+			-1, 1, 0, 1,
+		})
+		w.canvasVs.End()
+
+		return nil
 	})
 	if err != nil {
-		w.Destroy()
 		return nil, errors.Wrap(err, "creating window failed")
 	}
 
 	w.initInput()
 	w.SetFullscreen(config.Fullscreen)
-
-	w.SetPicture(nil)
-	w.SetTransform()
-	w.SetMaskColor(NRGBA{1, 1, 1, 1})
+	w.Update()
 
 	runtime.SetFinalizer(w, (*Window).Destroy)
 
@@ -171,18 +171,41 @@ func (w *Window) Destroy() {
 
 // Clear clears the window with a color.
 func (w *Window) Clear(c color.Color) {
-	mainthread.CallNonBlock(func() {
-		w.begin()
-		c := NRGBAModel.Convert(c).(NRGBA)
-		pixelgl.Clear(float32(c.R), float32(c.G), float32(c.B), float32(c.A))
-		w.end()
-	})
+	w.canvas.Clear(c)
 }
 
 // Update swaps buffers and polls events.
 func (w *Window) Update() {
+	width, height := w.Size()
+	if w.canvas == nil || V(w.canvas.Size()) != V(width, height) {
+		oldCanvas := w.canvas
+		w.canvas = NewCanvas(width, height, false)
+		if oldCanvas != nil {
+			td := TrianglesDrawer{Triangles: &TrianglesData{
+				{Position: V(-1, -1), Color: NRGBA{1, 1, 1, 1}, Texture: V(0, 0)},
+				{Position: V(1, -1), Color: NRGBA{1, 1, 1, 1}, Texture: V(1, 0)},
+				{Position: V(1, 1), Color: NRGBA{1, 1, 1, 1}, Texture: V(1, 1)},
+				{Position: V(-1, -1), Color: NRGBA{1, 1, 1, 1}, Texture: V(0, 0)},
+				{Position: V(1, 1), Color: NRGBA{1, 1, 1, 1}, Texture: V(1, 1)},
+				{Position: V(-1, 1), Color: NRGBA{1, 1, 1, 1}, Texture: V(0, 1)},
+			}}
+			w.canvas.SetPicture(oldCanvas.Content())
+			td.Draw(w.canvas)
+		}
+	}
+
 	mainthread.Call(func() {
 		w.begin()
+
+		pixelgl.Clear(0, 0, 0, 0)
+		w.shader.Begin()
+		w.canvas.f.Texture().Begin()
+		w.canvasVs.Begin()
+		w.canvasVs.Draw()
+		w.canvasVs.End()
+		w.canvas.f.Texture().End()
+		w.shader.End()
+
 		if w.config.VSync {
 			glfw.SwapInterval(1)
 		}
@@ -191,8 +214,6 @@ func (w *Window) Update() {
 	})
 
 	w.updateInput()
-
-	w.width, w.height = w.Size()
 }
 
 // SetClosed sets the closed flag of a window.
@@ -346,63 +367,11 @@ func (w *Window) begin() {
 		pixelgl.Init()
 		currentWindow = w
 	}
-	if w.shader != nil {
-		w.shader.Begin()
-	}
-	if w.Frame != nil {
-		w.Frame.Begin()
-		pixelgl.Viewport(0, 0, int32(w.Frame.Width()), int32(w.Frame.Height()))
-	} else {
-		pixelgl.Viewport(0, 0, int32(w.width), int32(w.height))
-	}
 }
 
 // Note: must be called inside the main thread.
 func (w *Window) end() {
-	if w.Frame != nil {
-		w.Frame.End()
-	}
-	if w.shader != nil {
-		w.shader.End()
-	}
-}
-
-type trianglesPositionColorTexture interface {
-	Triangles
-	Position(i int) Vec
-	Color(i int) NRGBA
-	Texture(i int) Vec
-}
-
-type windowTriangles struct {
-	w *Window
-	trianglesPositionColorTexture
-}
-
-func (wt *windowTriangles) Draw() {
-	// avoid possible race condition
-	pic := wt.w.pic
-	mat := wt.w.mat
-	col := wt.w.col
-	bnd := wt.w.bnd
-
-	mainthread.CallNonBlock(func() {
-		wt.w.begin()
-
-		wt.w.shader.SetUniformAttr(transformMat3, mat)
-		wt.w.shader.SetUniformAttr(maskColorVec4, col)
-		wt.w.shader.SetUniformAttr(boundsVec4, bnd)
-
-		if pic != nil {
-			pic.Texture().Begin()
-		}
-		wt.trianglesPositionColorTexture.Draw()
-		if pic != nil {
-			pic.Texture().End()
-		}
-
-		wt.w.end()
-	})
+	// nothing really
 }
 
 // MakeTriangles generates a specialized copy of the supplied triangles that will draw onto this
@@ -410,112 +379,62 @@ func (wt *windowTriangles) Draw() {
 //
 // Window supports TrianglesPosition, TrianglesColor and TrianglesTexture.
 func (w *Window) MakeTriangles(t Triangles) Triangles {
-	tpcs := NewGLTriangles(w.shader, t).(trianglesPositionColorTexture)
-	wt := &windowTriangles{
-		w: w,
-		trianglesPositionColorTexture: tpcs,
-	}
-	return wt
+	return w.canvas.MakeTriangles(t)
 }
 
 // SetPicture sets a Picture that will be used in subsequent drawings onto the window.
 func (w *Window) SetPicture(p *Picture) {
-	if p != nil {
-		min := pictureBounds(p, V(0, 0))
-		max := pictureBounds(p, V(1, 1))
-		w.bnd = mgl32.Vec4{
-			float32(min.X()), float32(min.Y()),
-			float32(max.X()), float32(max.Y()),
-		}
-	}
-	w.pic = p
+	w.canvas.SetPicture(p)
 }
 
 // SetTransform sets a global transformation matrix for the Window.
 //
 // Transforms are applied right-to-left.
 func (w *Window) SetTransform(t ...Transform) {
-	w.mat = transformToMat(t...)
+	w.canvas.SetTransform(t...)
 }
 
 // SetMaskColor sets a global mask color for the Window.
 func (w *Window) SetMaskColor(c color.Color) {
-	if c == nil {
-		c = NRGBA{1, 1, 1, 1}
-	}
-	nrgba := NRGBAModel.Convert(c).(NRGBA)
-	r := float32(nrgba.R)
-	g := float32(nrgba.G)
-	b := float32(nrgba.B)
-	a := float32(nrgba.A)
-	w.col = mgl32.Vec4{r, g, b, a}
+	w.canvas.SetMaskColor(c)
 }
 
 const (
-	positionVec2 int = iota
-	colorVec4
-	textureVec2
+	windowPositionVec2 = iota
+	windowTextureVec2
 )
 
-var defaultVertexFormat = pixelgl.AttrFormat{
-	positionVec2: {Name: "position", Type: pixelgl.Vec2},
-	colorVec4:    {Name: "color", Type: pixelgl.Vec4},
-	textureVec2:  {Name: "texture", Type: pixelgl.Vec2},
+var windowVertexFormat = pixelgl.AttrFormat{
+	windowPositionVec2: {Name: "position", Type: pixelgl.Vec2},
+	windowTextureVec2:  {Name: "texture", Type: pixelgl.Vec2},
 }
 
-const (
-	maskColorVec4 int = iota
-	transformMat3
-	boundsVec4
-)
-
-var defaultUniformFormat = pixelgl.AttrFormat{
-	{Name: "maskColor", Type: pixelgl.Vec4},
-	{Name: "transform", Type: pixelgl.Mat3},
-	{Name: "bounds", Type: pixelgl.Vec4},
-}
+var windowUniformFormat = pixelgl.AttrFormat{}
 
-var defaultVertexShader = `
+var windowVertexShader = `
 #version 330 core
 
 in vec2 position;
-in vec4 color;
 in vec2 texture;
 
-out vec4 Color;
 out vec2 Texture;
 
-uniform mat3 transform;
-
 void main() {
-	gl_Position = vec4((transform * vec3(position.x, position.y, 1.0)).xy, 0.0, 1.0);
-	Color = color;
+	gl_Position = vec4(position, 0.0, 1.0);
 	Texture = texture;
 }
 `
 
-var defaultFragmentShader = `
+var windowFragmentShader = `
 #version 330 core
 
-in vec4 Color;
 in vec2 Texture;
 
 out vec4 color;
 
-uniform vec4 maskColor;
-uniform vec4 bounds;
 uniform sampler2D tex;
 
 void main() {
-	vec2 boundsMin = bounds.xy;
-	vec2 boundsMax = bounds.zw;
-
-	if (Texture == vec2(-1, -1)) {
-		color = maskColor * Color;
-	} else {
-		float tx = boundsMin.x * (1 - Texture.x) + boundsMax.x * Texture.x;
-		float ty = boundsMin.y * (1 - Texture.y) + boundsMax.y * Texture.y;
-		color = maskColor * Color * texture(tex, vec2(tx, 1 - ty));
-	}
+	color = texture(tex, Texture);
 }
 `