diff --git a/README.md b/README.md
index ac347e9c32a6e515f0de7dd0f00b47cc0496ffd0..e1553de5ec75ddec9f2ba873d8e5ab0e9cb0a809 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,7 @@ covering several topics of Pixel. Here's the content of the tutorial parts so fa
 - [Pressing keys and clicking mouse](https://github.com/faiface/pixel/wiki/Pressing-keys-and-clicking-mouse)
 - [Drawing efficiently with Batch](https://github.com/faiface/pixel/wiki/Drawing-efficiently-with-Batch)
 - [Drawing shapes with IMDraw](https://github.com/faiface/pixel/wiki/Drawing-shapes-with-IMDraw)
+- [Typing text on the screen](https://github.com/faiface/pixel/wiki/Typing-text-on-the-screen)
 
 ## Examples
 
diff --git a/data.go b/data.go
index 4e6c52869f686bea717bfc1eee2874af4eeb9379..c9412413f58d32e4d946c4890b3148e9deb86992 100644
--- a/data.go
+++ b/data.go
@@ -45,7 +45,7 @@ func (td *TrianglesData) SetLen(len int) {
 				Color     RGBA
 				Picture   Vec
 				Intensity float64
-			}{ZV, Alpha(1), ZV, 0})
+			}{Color: RGBA{1, 1, 1, 1}})
 		}
 	}
 	if len < td.Len() {
diff --git a/examples/community/maze/LICENSE b/examples/community/maze/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..1326e36485d665086ee65d91473e3586d5b72661
--- /dev/null
+++ b/examples/community/maze/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 stephen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/examples/community/maze/README.md b/examples/community/maze/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..27b344af065d5f536e2a12fff254d92c2dc38e59
--- /dev/null
+++ b/examples/community/maze/README.md
@@ -0,0 +1,19 @@
+# Maze generator in Go
+
+Created by [Stephen Chavez](https://github.com/redragonx)
+
+This uses the game engine: Pixel. Install it here: https://github.com/faiface/pixel
+
+I made this to improve my understanding of Go and some game concepts with some basic maze generating algorithms.
+
+Controls: Press 'R' to restart the maze.
+
+Optional command-line arguments: `go run ./maze-generator.go`
+  - `-w` sets the maze's width in pixels.
+  -	`-h` sets the maze's height in pixels.
+  -	`-c` sets the maze cell's size in pixels.
+
+Code based on the Recursive backtracker algorithm. 
+- https://en.wikipedia.org/wiki/Maze_generation_algorithm#Recursive_backtracker
+
+![Screenshot](screenshot.png)
\ No newline at end of file
diff --git a/examples/community/maze/maze-generator.go b/examples/community/maze/maze-generator.go
new file mode 100644
index 0000000000000000000000000000000000000000..a8b09f84d78883bacda4dfe632a205810750cc00
--- /dev/null
+++ b/examples/community/maze/maze-generator.go
@@ -0,0 +1,317 @@
+package main
+
+// Code based on the Recursive backtracker algorithm.
+// https://en.wikipedia.org/wiki/Maze_generation_algorithm#Recursive_backtracker
+// See https://youtu.be/HyK_Q5rrcr4 as an example
+// YouTube example ported to Go for the Pixel library.
+
+// Created by Stephen Chavez
+
+import (
+	"crypto/rand"
+	"errors"
+	"flag"
+	"fmt"
+	"math/big"
+	"time"
+
+	"github.com/faiface/pixel"
+	"github.com/faiface/pixel/examples/community/maze/stack"
+	"github.com/faiface/pixel/imdraw"
+	"github.com/faiface/pixel/pixelgl"
+
+	"github.com/pkg/profile"
+	"golang.org/x/image/colornames"
+)
+
+var visitedColor = pixel.RGB(0.5, 0, 1).Mul(pixel.Alpha(0.35))
+var hightlightColor = pixel.RGB(0.3, 0, 0).Mul(pixel.Alpha(0.45))
+var debug = false
+
+type cell struct {
+	walls [4]bool // Wall order: top, right, bottom, left
+
+	row     int
+	col     int
+	visited bool
+}
+
+func (c *cell) Draw(imd *imdraw.IMDraw, wallSize int) {
+	drawCol := c.col * wallSize // x
+	drawRow := c.row * wallSize // y
+
+	imd.Color = colornames.White
+	if c.walls[0] {
+		// top line
+		imd.Push(pixel.V(float64(drawCol), float64(drawRow)), pixel.V(float64(drawCol+wallSize), float64(drawRow)))
+		imd.Line(3)
+	}
+	if c.walls[1] {
+		// right Line
+		imd.Push(pixel.V(float64(drawCol+wallSize), float64(drawRow)), pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize)))
+		imd.Line(3)
+	}
+	if c.walls[2] {
+		// bottom line
+		imd.Push(pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize)), pixel.V(float64(drawCol), float64(drawRow+wallSize)))
+		imd.Line(3)
+	}
+	if c.walls[3] {
+		// left line
+		imd.Push(pixel.V(float64(drawCol), float64(drawRow+wallSize)), pixel.V(float64(drawCol), float64(drawRow)))
+		imd.Line(3)
+	}
+	imd.EndShape = imdraw.SharpEndShape
+
+	if c.visited {
+		imd.Color = visitedColor
+		imd.Push(pixel.V(float64(drawCol), (float64(drawRow))), pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize)))
+		imd.Rectangle(0)
+	}
+}
+
+func (c *cell) GetNeighbors(grid []*cell, cols int, rows int) ([]*cell, error) {
+	neighbors := []*cell{}
+	j := c.row
+	i := c.col
+
+	top, _ := getCellAt(i, j-1, cols, rows, grid)
+	right, _ := getCellAt(i+1, j, cols, rows, grid)
+	bottom, _ := getCellAt(i, j+1, cols, rows, grid)
+	left, _ := getCellAt(i-1, j, cols, rows, grid)
+
+	if top != nil && !top.visited {
+		neighbors = append(neighbors, top)
+	}
+	if right != nil && !right.visited {
+		neighbors = append(neighbors, right)
+	}
+	if bottom != nil && !bottom.visited {
+		neighbors = append(neighbors, bottom)
+	}
+	if left != nil && !left.visited {
+		neighbors = append(neighbors, left)
+	}
+
+	if len(neighbors) == 0 {
+		return nil, errors.New("We checked all cells...")
+	}
+	return neighbors, nil
+}
+
+func (c *cell) GetRandomNeighbor(grid []*cell, cols int, rows int) (*cell, error) {
+	neighbors, err := c.GetNeighbors(grid, cols, rows)
+	if neighbors == nil {
+		return nil, err
+	}
+	nBig, err := rand.Int(rand.Reader, big.NewInt(int64(len(neighbors))))
+	if err != nil {
+		panic(err)
+	}
+	randomIndex := nBig.Int64()
+	return neighbors[randomIndex], nil
+}
+
+func (c *cell) hightlight(imd *imdraw.IMDraw, wallSize int) {
+	x := c.col * wallSize
+	y := c.row * wallSize
+
+	imd.Color = hightlightColor
+	imd.Push(pixel.V(float64(x), float64(y)), pixel.V(float64(x+wallSize), float64(y+wallSize)))
+	imd.Rectangle(0)
+}
+
+func newCell(col int, row int) *cell {
+	newCell := new(cell)
+	newCell.row = row
+	newCell.col = col
+
+	for i := range newCell.walls {
+		newCell.walls[i] = true
+	}
+	return newCell
+}
+
+// Creates the inital maze slice for use.
+func initGrid(cols, rows int) []*cell {
+	grid := []*cell{}
+	for j := 0; j < rows; j++ {
+		for i := 0; i < cols; i++ {
+			newCell := newCell(i, j)
+			grid = append(grid, newCell)
+		}
+	}
+	return grid
+}
+
+func setupMaze(cols, rows int) ([]*cell, *stack.Stack, *cell) {
+	// Make an empty grid
+	grid := initGrid(cols, rows)
+	backTrackStack := stack.NewStack(len(grid))
+	currentCell := grid[0]
+
+	return grid, backTrackStack, currentCell
+}
+
+func cellIndex(i, j, cols, rows int) int {
+	if i < 0 || j < 0 || i > cols-1 || j > rows-1 {
+		return -1
+	}
+	return i + j*cols
+}
+
+func getCellAt(i int, j int, cols int, rows int, grid []*cell) (*cell, error) {
+	possibleIndex := cellIndex(i, j, cols, rows)
+
+	if possibleIndex == -1 {
+		return nil, fmt.Errorf("cellIndex: CellIndex is a negative number %d", possibleIndex)
+	}
+	return grid[possibleIndex], nil
+}
+
+func removeWalls(a *cell, b *cell) {
+	x := a.col - b.col
+
+	if x == 1 {
+		a.walls[3] = false
+		b.walls[1] = false
+	} else if x == -1 {
+		a.walls[1] = false
+		b.walls[3] = false
+	}
+
+	y := a.row - b.row
+
+	if y == 1 {
+		a.walls[0] = false
+		b.walls[2] = false
+	} else if y == -1 {
+		a.walls[2] = false
+		b.walls[0] = false
+	}
+}
+
+func run() {
+	// unsiged integers, because easier parsing error checks.
+	// We must convert these to intergers, as done below...
+	uScreenWidth, uScreenHeight, uWallSize := parseArgs()
+
+	var (
+		// In pixels
+		// Defualt is 800x800x40 = 20x20 wallgrid
+		screenWidth  = int(uScreenWidth)
+		screenHeight = int(uScreenHeight)
+		wallSize     = int(uWallSize)
+
+		frames = 0
+		second = time.Tick(time.Second)
+
+		grid           = []*cell{}
+		cols           = screenWidth / wallSize
+		rows           = screenHeight / wallSize
+		currentCell    = new(cell)
+		backTrackStack = stack.NewStack(1)
+	)
+
+	// Set game FPS manually
+	fps := time.Tick(time.Second / 60)
+
+	cfg := pixelgl.WindowConfig{
+		Title:  "Pixel Rocks! - Maze example",
+		Bounds: pixel.R(0, 0, float64(screenHeight), float64(screenWidth)),
+	}
+
+	win, err := pixelgl.NewWindow(cfg)
+	if err != nil {
+		panic(err)
+	}
+
+	grid, backTrackStack, currentCell = setupMaze(cols, rows)
+
+	gridIMDraw := imdraw.New(nil)
+
+	for !win.Closed() {
+		if win.JustReleased(pixelgl.KeyR) {
+			fmt.Println("R pressed")
+			grid, backTrackStack, currentCell = setupMaze(cols, rows)
+		}
+
+		win.Clear(colornames.Gray)
+		gridIMDraw.Clear()
+
+		for i := range grid {
+			grid[i].Draw(gridIMDraw, wallSize)
+		}
+
+		// step 1
+		// Make the initial cell the current cell and mark it as visited
+		currentCell.visited = true
+		currentCell.hightlight(gridIMDraw, wallSize)
+
+		// step 2.1
+		// If the current cell has any neighbours which have not been visited
+		// Choose a random unvisited cell
+		nextCell, _ := currentCell.GetRandomNeighbor(grid, cols, rows)
+		if nextCell != nil && !nextCell.visited {
+			// step 2.2
+			// Push the current cell to the stack
+			backTrackStack.Push(currentCell)
+
+			// step 2.3
+			// Remove the wall between the current cell and the chosen cell
+
+			removeWalls(currentCell, nextCell)
+
+			// step 2.4
+			// Make the chosen cell the current cell and mark it as visited
+			nextCell.visited = true
+			currentCell = nextCell
+		} else if backTrackStack.Len() > 0 {
+			currentCell = backTrackStack.Pop().(*cell)
+		}
+
+		gridIMDraw.Draw(win)
+		win.Update()
+		<-fps
+		updateFPSDisplay(win, &cfg, &frames, grid, second)
+	}
+}
+
+// Parses the maze arguments, all of them are optional.
+// Uses uint as implicit error checking :)
+func parseArgs() (uint, uint, uint) {
+	var mazeWidthPtr = flag.Uint("w", 800, "w sets the maze's width in pixels.")
+	var mazeHeightPtr = flag.Uint("h", 800, "h sets the maze's height in pixels.")
+	var wallSizePtr = flag.Uint("c", 40, "c sets the maze cell's size in pixels.")
+
+	flag.Parse()
+
+	// If these aren't default values AND if they're not the same values.
+	// We should warn the user that the maze will look funny.
+	if *mazeWidthPtr != 800 || *mazeHeightPtr != 800 {
+		if *mazeWidthPtr != *mazeHeightPtr {
+			fmt.Printf("WARNING: maze width: %d and maze height: %d don't match. \n", *mazeWidthPtr, *mazeHeightPtr)
+			fmt.Println("Maze will look funny because the maze size is bond to the window size!")
+		}
+	}
+
+	return *mazeWidthPtr, *mazeHeightPtr, *wallSizePtr
+}
+
+func updateFPSDisplay(win *pixelgl.Window, cfg *pixelgl.WindowConfig, frames *int, grid []*cell, second <-chan time.Time) {
+	*frames++
+	select {
+	case <-second:
+		win.SetTitle(fmt.Sprintf("%s | FPS: %d with %d Cells", cfg.Title, *frames, len(grid)))
+		*frames = 0
+	default:
+	}
+
+}
+
+func main() {
+	if debug {
+		defer profile.Start().Stop()
+	}
+	pixelgl.Run(run)
+}
diff --git a/examples/community/maze/screenshot.png b/examples/community/maze/screenshot.png
new file mode 100644
index 0000000000000000000000000000000000000000..ce5bfd2f89336e571b304e1dc224af9f2e231013
Binary files /dev/null and b/examples/community/maze/screenshot.png differ
diff --git a/examples/community/maze/stack/stack.go b/examples/community/maze/stack/stack.go
new file mode 100644
index 0000000000000000000000000000000000000000..50a7a46fd89cd5f93a7490b9122298b6a9e44fa2
--- /dev/null
+++ b/examples/community/maze/stack/stack.go
@@ -0,0 +1,86 @@
+package stack
+
+type Stack struct {
+	top  *Element
+	size int
+	max  int
+}
+
+type Element struct {
+	value interface{}
+	next  *Element
+}
+
+func NewStack(max int) *Stack {
+	return &Stack{max: max}
+}
+
+// Return the stack's length
+func (s *Stack) Len() int {
+	return s.size
+}
+
+// Return the stack's max
+func (s *Stack) Max() int {
+	return s.max
+}
+
+// Push a new element onto the stack
+func (s *Stack) Push(value interface{}) {
+	if s.size+1 > s.max {
+		if last := s.PopLast(); last == nil {
+			panic("Unexpected nil in stack")
+		}
+	}
+	s.top = &Element{value, s.top}
+	s.size++
+}
+
+// Remove the top element from the stack and return it's value
+// If the stack is empty, return nil
+func (s *Stack) Pop() (value interface{}) {
+	if s.size > 0 {
+		value, s.top = s.top.value, s.top.next
+		s.size--
+		return
+	}
+	return nil
+}
+
+func (s *Stack) PopLast() (value interface{}) {
+	if lastElem := s.popLast(s.top); lastElem != nil {
+		return lastElem.value
+	}
+	return nil
+}
+
+//Peek returns a top without removing it from list
+func (s *Stack) Peek() (value interface{}, exists bool) {
+	exists = false
+	if s.size > 0 {
+		value = s.top.value
+		exists = true
+	}
+
+	return
+}
+
+func (s *Stack) popLast(elem *Element) *Element {
+	if elem == nil {
+		return nil
+	}
+	// not last because it has next and a grandchild
+	if elem.next != nil && elem.next.next != nil {
+		return s.popLast(elem.next)
+	}
+
+	// current elem is second from bottom, as next elem has no child
+	if elem.next != nil && elem.next.next == nil {
+		last := elem.next
+		// make current elem bottom of stack by removing its next element
+		elem.next = nil
+		s.size--
+		return last
+	}
+	return nil
+}
diff --git a/examples/guide/07_typing_text_on_the_screen/intuitive.ttf b/examples/guide/07_typing_text_on_the_screen/intuitive.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..9039d7b3e5e9bd7830b19102b1ba5d1c37599d0b
Binary files /dev/null and b/examples/guide/07_typing_text_on_the_screen/intuitive.ttf differ
diff --git a/examples/guide/07_typing_text_on_the_screen/main.go b/examples/guide/07_typing_text_on_the_screen/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..caaa0da683a497ceb3f9924a03542f685aae8036
--- /dev/null
+++ b/examples/guide/07_typing_text_on_the_screen/main.go
@@ -0,0 +1,78 @@
+package main
+
+import (
+	"io/ioutil"
+	"os"
+	"time"
+
+	"github.com/faiface/pixel"
+	"github.com/faiface/pixel/pixelgl"
+	"github.com/faiface/pixel/text"
+	"github.com/golang/freetype/truetype"
+	"golang.org/x/image/colornames"
+	"golang.org/x/image/font"
+)
+
+func loadTTF(path string, size float64) (font.Face, error) {
+	file, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer file.Close()
+
+	bytes, err := ioutil.ReadAll(file)
+	if err != nil {
+		return nil, err
+	}
+
+	font, err := truetype.Parse(bytes)
+	if err != nil {
+		return nil, err
+	}
+
+	return truetype.NewFace(font, &truetype.Options{
+		Size:              size,
+		GlyphCacheEntries: 1,
+	}), nil
+}
+
+func run() {
+	cfg := pixelgl.WindowConfig{
+		Title:  "Pixel Rocks!",
+		Bounds: pixel.R(0, 0, 1024, 768),
+	}
+	win, err := pixelgl.NewWindow(cfg)
+	if err != nil {
+		panic(err)
+	}
+	win.SetSmooth(true)
+
+	face, err := loadTTF("intuitive.ttf", 80)
+	if err != nil {
+		panic(err)
+	}
+
+	atlas := text.NewAtlas(face, text.ASCII)
+	txt := text.New(pixel.V(50, 500), atlas)
+
+	txt.Color = colornames.Lightgrey
+
+	fps := time.Tick(time.Second / 120)
+
+	for !win.Closed() {
+		txt.WriteString(win.Typed())
+		if win.JustPressed(pixelgl.KeyEnter) || win.Repeated(pixelgl.KeyEnter) {
+			txt.WriteRune('\n')
+		}
+
+		win.Clear(colornames.Darkcyan)
+		txt.Draw(win, pixel.IM.Moved(win.Bounds().Center().Sub(txt.Bounds().Center())))
+		win.Update()
+
+		<-fps
+	}
+}
+
+func main() {
+	pixelgl.Run(run)
+}
diff --git a/examples/typewriter/main.go b/examples/typewriter/main.go
index d7691ff716de82704f1a4ac43aed475a25183e01..24ee972b22dda31ed97bb0eb5ead4436d1b12d11 100644
--- a/examples/typewriter/main.go
+++ b/examples/typewriter/main.go
@@ -227,10 +227,7 @@ func (dl *dotlight) Draw(t pixel.Target, m pixel.Matrix) {
 	dl.imd.Color = pixel.Alpha(0)
 	for i := 0.0; i <= 32; i++ {
 		angle := i * 2 * math.Pi / 32
-		dl.imd.Push(dl.pos.Add(pixel.V(
-			math.Cos(angle)*dl.radius,
-			math.Sin(angle)*dl.radius,
-		)))
+		dl.imd.Push(dl.pos.Add(pixel.V(dl.radius, 0).Rotated(angle)))
 	}
 	dl.imd.Polygon(0)
 	dl.imd.Draw(t)
diff --git a/geometry.go b/geometry.go
index f9348398a5e580db38b180683cea2fb708829623..13a574d42f7df8b8fe6f9a0a5c1e1383f5cf7640 100644
--- a/geometry.go
+++ b/geometry.go
@@ -3,8 +3,6 @@ package pixel
 import (
 	"fmt"
 	"math"
-
-	"github.com/go-gl/mathgl/mgl64"
 )
 
 // Vec is a 2D vector type with X and Y coordinates.
@@ -251,7 +249,7 @@ func (r Rect) Union(s Rect) Rect {
 	)
 }
 
-// Matrix is a 3x3 transformation matrix that can be used for all kinds of spacial transforms, such
+// Matrix is a 3x2 affine matrix that can be used for all kinds of spatial transforms, such
 // as movement, scaling and rotations.
 //
 // Matrix has a handful of useful methods, each of which adds a transformation to the matrix. For
@@ -261,38 +259,41 @@ func (r Rect) Union(s Rect) Rect {
 //
 // This code creates a Matrix that first moves everything by 100 units horizontally and 200 units
 // vertically and then rotates everything by 90 degrees around the origin.
-type Matrix [9]float64
+//
+// Layout is:
+// [0] [2] [4]
+// [1] [3] [5]
+//  0   0   1  (implicit row)
+type Matrix [6]float64
 
 // IM stands for identity matrix. Does nothing, no transformation.
-var IM = Matrix(mgl64.Ident3())
+var IM = Matrix{1, 0, 0, 1, 0, 0}
 
 // String returns a string representation of the Matrix.
 //
 //   m := pixel.IM
-//   fmt.Println(m) // Matrix(1 0 0 | 0 1 0 | 0 0 1)
+//   fmt.Println(m) // Matrix(1 0 0 | 0 1 0)
 func (m Matrix) String() string {
 	return fmt.Sprintf(
-		"Matrix(%v %v %v | %v %v %v | %v %v %v)",
-		m[0], m[3], m[6],
-		m[1], m[4], m[7],
-		m[2], m[5], m[8],
+		"Matrix(%v %v %v | %v %v %v)",
+		m[0], m[2], m[4],
+		m[1], m[3], m[5],
 	)
 }
 
 // Moved moves everything by the delta vector.
 func (m Matrix) Moved(delta Vec) Matrix {
-	m3 := mgl64.Mat3(m)
-	m3 = mgl64.Translate2D(delta.XY()).Mul3(m3)
-	return Matrix(m3)
+	m[4], m[5] = m[4]+delta.X, m[5]+delta.Y
+	return m
 }
 
 // ScaledXY scales everything around a given point by the scale factor in each axis respectively.
 func (m Matrix) ScaledXY(around Vec, scale Vec) Matrix {
-	m3 := mgl64.Mat3(m)
-	m3 = mgl64.Translate2D(around.Scaled(-1).XY()).Mul3(m3)
-	m3 = mgl64.Scale2D(scale.XY()).Mul3(m3)
-	m3 = mgl64.Translate2D(around.XY()).Mul3(m3)
-	return Matrix(m3)
+	m[4], m[5] = m[4]-around.X, m[5]-around.Y
+	m[0], m[2], m[4] = m[0]*scale.X, m[2]*scale.X, m[4]*scale.X
+	m[1], m[3], m[5] = m[1]*scale.Y, m[3]*scale.Y, m[5]*scale.Y
+	m[4], m[5] = m[4]+around.X, m[5]+around.Y
+	return m
 }
 
 // Scaled scales everything around a given point by the scale factor.
@@ -302,36 +303,42 @@ func (m Matrix) Scaled(around Vec, scale float64) Matrix {
 
 // Rotated rotates everything around a given point by the given angle in radians.
 func (m Matrix) Rotated(around Vec, angle float64) Matrix {
-	m3 := mgl64.Mat3(m)
-	m3 = mgl64.Translate2D(around.Scaled(-1).XY()).Mul3(m3)
-	m3 = mgl64.Rotate3DZ(angle).Mul3(m3)
-	m3 = mgl64.Translate2D(around.XY()).Mul3(m3)
-	return Matrix(m3)
+	sint, cost := math.Sincos(angle)
+	m[4], m[5] = m[4]-around.X, m[5]-around.Y
+	m = m.Chained(Matrix{cost, sint, -sint, cost, 0, 0})
+	m[4], m[5] = m[4]+around.X, m[5]+around.Y
+	return m
 }
 
 // Chained adds another Matrix to this one. All tranformations by the next Matrix will be applied
 // after the transformations of this Matrix.
 func (m Matrix) Chained(next Matrix) Matrix {
-	m3 := mgl64.Mat3(m)
-	m3 = mgl64.Mat3(next).Mul3(m3)
-	return Matrix(m3)
+	return Matrix{
+		m[0]*next[0] + m[2]*next[1],
+		m[1]*next[0] + m[3]*next[1],
+		m[0]*next[2] + m[2]*next[3],
+		m[1]*next[2] + m[3]*next[3],
+		m[0]*next[4] + m[2]*next[5] + m[4],
+		m[1]*next[4] + m[3]*next[5] + m[5],
+	}
 }
 
 // Project applies all transformations added to the Matrix to a vector u and returns the result.
 //
 // Time complexity is O(1).
 func (m Matrix) Project(u Vec) Vec {
-	m3 := mgl64.Mat3(m)
-	proj := m3.Mul3x1(mgl64.Vec3{u.X, u.Y, 1})
-	return V(proj.X(), proj.Y())
+	return Vec{X: m[0]*u.X + m[2]*u.Y + m[4], Y: m[1]*u.X + m[3]*u.Y + m[5]}
 }
 
 // Unproject does the inverse operation to Project.
 //
+// It turns out that multiplying a vector by the inverse matrix of m can be nearly-accomplished by
+// subtracting the translate part of the matrix and multplying by the inverse of the top-left 2x2
+// matrix, and the inverse of a 2x2 matrix is simple enough to just be inlined in the computation.
+//
 // Time complexity is O(1).
 func (m Matrix) Unproject(u Vec) Vec {
-	m3 := mgl64.Mat3(m)
-	inv := m3.Inv()
-	unproj := inv.Mul3x1(mgl64.Vec3{u.X, u.Y, 1})
-	return V(unproj.X(), unproj.Y())
+	d := (m[0] * m[3]) - (m[1] * m[2])
+	u.X, u.Y = (u.X-m[4])/d, (u.Y-m[5])/d
+	return Vec{u.X*m[3] - u.Y*m[1], u.Y*m[0] - u.X*m[2]}
 }
diff --git a/imdraw/imdraw.go b/imdraw/imdraw.go
index 626cb2f640b305a95cb40d25bb82cba202a4a86c..f7ce7692de1f0d4969b07e8ad9d7013e4b0e3cbc 100644
--- a/imdraw/imdraw.go
+++ b/imdraw/imdraw.go
@@ -52,6 +52,7 @@ type IMDraw struct {
 	EndShape  EndShape
 
 	points []point
+	pool   [][]point
 	matrix pixel.Matrix
 	mask   pixel.RGBA
 
@@ -109,7 +110,7 @@ func (imd *IMDraw) Clear() {
 //
 // This does not affect matrix and color mask set by SetMatrix and SetColorMask.
 func (imd *IMDraw) Reset() {
-	imd.points = nil
+	imd.points = imd.points[:0]
 	imd.Color = pixel.Alpha(1)
 	imd.Picture = pixel.ZV
 	imd.Intensity = 0
@@ -256,10 +257,22 @@ func (imd *IMDraw) EllipseArc(radius pixel.Vec, low, high, thickness float64) {
 
 func (imd *IMDraw) getAndClearPoints() []point {
 	points := imd.points
-	imd.points = nil
+	// use one of the existing pools so we don't reallocate as often
+	if len(imd.pool) > 0 {
+		pos := len(imd.pool) - 1
+		imd.points = imd.pool[pos][:0]
+		imd.pool = imd.pool[:pos]
+	} else {
+		imd.points = nil
+	}
 	return points
 }
 
+func (imd *IMDraw) restorePoints(points []point) {
+	imd.pool = append(imd.pool, imd.points)
+	imd.points = points[:0]
+}
+
 func (imd *IMDraw) applyMatrixAndMask(off int) {
 	for i := range (*imd.tri)[off:] {
 		(*imd.tri)[off+i].Position = imd.matrix.Project((*imd.tri)[off+i].Position)
@@ -271,6 +284,7 @@ func (imd *IMDraw) fillRectangle() {
 	points := imd.getAndClearPoints()
 
 	if len(points) < 2 {
+		imd.restorePoints(points)
 		return
 	}
 
@@ -292,7 +306,7 @@ func (imd *IMDraw) fillRectangle() {
 			in:  (a.in + b.in) / 2,
 		}
 
-		for k, p := range []point{a, b, c, a, b, d} {
+		for k, p := range [...]point{a, b, c, a, b, d} {
 			(*imd.tri)[j+k].Position = p.pos
 			(*imd.tri)[j+k].Color = p.col
 			(*imd.tri)[j+k].Picture = p.pic
@@ -302,12 +316,15 @@ func (imd *IMDraw) fillRectangle() {
 
 	imd.applyMatrixAndMask(off)
 	imd.batch.Dirty()
+
+	imd.restorePoints(points)
 }
 
 func (imd *IMDraw) outlineRectangle(thickness float64) {
 	points := imd.getAndClearPoints()
 
 	if len(points) < 2 {
+		imd.restorePoints(points)
 		return
 	}
 
@@ -323,12 +340,15 @@ func (imd *IMDraw) outlineRectangle(thickness float64) {
 		imd.pushPt(pixel.V(b.pos.X, a.pos.Y), mid)
 		imd.polyline(thickness, true)
 	}
+
+	imd.restorePoints(points)
 }
 
 func (imd *IMDraw) fillPolygon() {
 	points := imd.getAndClearPoints()
 
 	if len(points) < 3 {
+		imd.restorePoints(points)
 		return
 	}
 
@@ -336,16 +356,19 @@ func (imd *IMDraw) fillPolygon() {
 	imd.tri.SetLen(imd.tri.Len() + 3*(len(points)-2))
 
 	for i, j := 1, off; i+1 < len(points); i, j = i+1, j+3 {
-		for k, p := range []point{points[0], points[i], points[i+1]} {
-			(*imd.tri)[j+k].Position = p.pos
-			(*imd.tri)[j+k].Color = p.col
-			(*imd.tri)[j+k].Picture = p.pic
-			(*imd.tri)[j+k].Intensity = p.in
+		for k, p := range [...]int{0, i, i + 1} {
+			tri := &(*imd.tri)[j+k]
+			tri.Position = points[p].pos
+			tri.Color = points[p].col
+			tri.Picture = points[p].pic
+			tri.Intensity = points[p].in
 		}
 	}
 
 	imd.applyMatrixAndMask(off)
 	imd.batch.Dirty()
+
+	imd.restorePoints(points)
 }
 
 func (imd *IMDraw) fillEllipseArc(radius pixel.Vec, low, high float64) {
@@ -387,6 +410,8 @@ func (imd *IMDraw) fillEllipseArc(radius pixel.Vec, low, high float64) {
 		imd.applyMatrixAndMask(off)
 		imd.batch.Dirty()
 	}
+
+	imd.restorePoints(points)
 }
 
 func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness float64, doEndShape bool) {
@@ -485,12 +510,15 @@ func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness floa
 			}
 		}
 	}
+
+	imd.restorePoints(points)
 }
 
 func (imd *IMDraw) polyline(thickness float64, closed bool) {
 	points := imd.getAndClearPoints()
 
 	if len(points) == 0 {
+		imd.restorePoints(points)
 		return
 	}
 	if len(points) == 1 {
@@ -521,6 +549,8 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
 	imd.pushPt(points[j].pos.Sub(normal), points[j])
 
 	// middle points
+	// compute "previous" normal:
+	ijNormal := points[1].pos.Sub(points[0].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2)
 	for i := 0; i < len(points); i++ {
 		j, k := i+1, i+2
 
@@ -536,7 +566,6 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
 			k %= len(points)
 		}
 
-		ijNormal := points[j].pos.Sub(points[i].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2)
 		jkNormal := points[k].pos.Sub(points[j].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2)
 
 		orientation := 1.0
@@ -567,6 +596,8 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
 			imd.pushPt(points[j].pos.Add(jkNormal), points[j])
 			imd.pushPt(points[j].pos.Sub(jkNormal), points[j])
 		}
+		// "next" normal becomes previous normal
+		ijNormal = jkNormal
 	}
 
 	// last point
@@ -591,4 +622,6 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) {
 			imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), normal.Angle(), normal.Angle()-math.Pi)
 		}
 	}
+
+	imd.restorePoints(points)
 }
diff --git a/pixelgl/canvas.go b/pixelgl/canvas.go
index 7a6400ec5a5eb87aeef1370880c57a9d5112a7eb..9e1efeb342fad91f040eef22e1a8fc4e31dbb483 100644
--- a/pixelgl/canvas.go
+++ b/pixelgl/canvas.go
@@ -91,8 +91,13 @@ func (c *Canvas) MakePicture(p pixel.Picture) pixel.TargetPicture {
 
 // SetMatrix sets a Matrix that every point will be projected by.
 func (c *Canvas) SetMatrix(m pixel.Matrix) {
-	for i := range m {
-		c.mat[i] = float32(m[i])
+	// pixel.Matrix is 3x2 with an implicit 0, 0, 1 row after it. So
+	// [0] [2] [4]    [0] [3] [6]
+	// [1] [3] [5] => [1] [4] [7]
+	//  0   0   1      0   0   1
+	// since all matrix ops are affine, the last row never changes, and we don't need to copy it
+	for i, j := range [...]int{0, 1, 3, 4, 6, 7} {
+		c.mat[j] = float32(m[i])
 	}
 }
 
@@ -218,6 +223,11 @@ func (c *Canvas) Texture() *glhf.Texture {
 	return c.gf.Texture()
 }
 
+// Frame returns the underlying OpenGL Frame of this Canvas.
+func (c *Canvas) Frame() *glhf.Frame {
+	return c.gf.frame
+}
+
 // SetPixels replaces the content of the Canvas with the provided pixels. The provided slice must be
 // an alpha-premultiplied RGBA sequence of correct length (4 * width * height).
 func (c *Canvas) SetPixels(pixels []uint8) {
diff --git a/pixelgl/gltriangles.go b/pixelgl/gltriangles.go
index 64b7355d060845052c506ae524c53e7e23b6eb7b..bf7a8956c34b9656d795c4e2bd2556c16b3f8920 100644
--- a/pixelgl/gltriangles.go
+++ b/pixelgl/gltriangles.go
@@ -103,15 +103,17 @@ func (gt *GLTriangles) updateData(t pixel.Triangles) {
 				tx, ty = (*t)[i].Picture.XY()
 				in     = (*t)[i].Intensity
 			)
-			gt.data[i*gt.vs.Stride()+0] = float32(px)
-			gt.data[i*gt.vs.Stride()+1] = float32(py)
-			gt.data[i*gt.vs.Stride()+2] = float32(col.R)
-			gt.data[i*gt.vs.Stride()+3] = float32(col.G)
-			gt.data[i*gt.vs.Stride()+4] = float32(col.B)
-			gt.data[i*gt.vs.Stride()+5] = float32(col.A)
-			gt.data[i*gt.vs.Stride()+6] = float32(tx)
-			gt.data[i*gt.vs.Stride()+7] = float32(ty)
-			gt.data[i*gt.vs.Stride()+8] = float32(in)
+			s := gt.vs.Stride()
+			d := gt.data[i*s : i*s+9]
+			d[0] = float32(px)
+			d[1] = float32(py)
+			d[2] = float32(col.R)
+			d[3] = float32(col.G)
+			d[4] = float32(col.B)
+			d[5] = float32(col.A)
+			d[6] = float32(tx)
+			d[7] = float32(ty)
+			d[8] = float32(in)
 		}
 		return
 	}