diff --git a/examples/platformer/README.md b/examples/platformer/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..c6b4e65f6bc41e764fc8c44f0f63c078de89ab46
--- /dev/null
+++ b/examples/platformer/README.md
@@ -0,0 +1,11 @@
+# Platformer
+
+This example demostrates a way to put things together and create a simple platformer game with a
+Gopher!
+
+The pixel art feel is, other than from the pixel art spritesheet, achieved by using a 160x120px
+large off-screen canvas, drawing everything to it and then stretching it to fit the window.
+
+The Gopher spritesheet comes from excellent [Egon Elbre](https://github.com/egonelbre/gophers).
+
+[Screenshot](screenshot.png)
\ No newline at end of file
diff --git a/examples/platformer/main.go b/examples/platformer/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..fb3b31c4a68e7676eb54bf61e8a300cdbeea5cee
--- /dev/null
+++ b/examples/platformer/main.go
@@ -0,0 +1,395 @@
+package main
+
+import (
+	"encoding/csv"
+	"image"
+	"image/color"
+	"io"
+	"math"
+	"math/rand"
+	"os"
+	"strconv"
+	"time"
+
+	_ "image/png"
+
+	"github.com/faiface/pixel"
+	"github.com/faiface/pixel/imdraw"
+	"github.com/faiface/pixel/pixelgl"
+	"github.com/pkg/errors"
+	"golang.org/x/image/colornames"
+)
+
+func loadAnimationSheet(sheetPath, descPath string, frameWidth float64) (sheet pixel.Picture, anims map[string][]pixel.Rect, err error) {
+	// total hack, nicely format the error at the end, so I don't have to type it every time
+	defer func() {
+		if err != nil {
+			err = errors.Wrap(err, "error loading animation sheet")
+		}
+	}()
+
+	// open and load the spritesheet
+	sheetFile, err := os.Open(sheetPath)
+	if err != nil {
+		return nil, nil, err
+	}
+	defer sheetFile.Close()
+	sheetImg, _, err := image.Decode(sheetFile)
+	if err != nil {
+		return nil, nil, err
+	}
+	sheet = pixel.PictureDataFromImage(sheetImg)
+
+	// create a slice of frames inside the spritesheet
+	var frames []pixel.Rect
+	for x := 0.0; x+frameWidth <= sheet.Bounds().Max.X(); x += frameWidth {
+		frames = append(frames, pixel.R(
+			x,
+			0,
+			x+frameWidth,
+			sheet.Bounds().H(),
+		))
+	}
+
+	descFile, err := os.Open(descPath)
+	if err != nil {
+		return nil, nil, err
+	}
+	defer descFile.Close()
+
+	anims = make(map[string][]pixel.Rect)
+
+	// load the animation information, name and interval inside the spritesheet
+	desc := csv.NewReader(descFile)
+	for {
+		anim, err := desc.Read()
+		if err == io.EOF {
+			break
+		}
+		if err != nil {
+			return nil, nil, err
+		}
+
+		name := anim[0]
+		start, _ := strconv.Atoi(anim[1])
+		end, _ := strconv.Atoi(anim[2])
+
+		anims[name] = frames[start : end+1]
+	}
+
+	return sheet, anims, nil
+}
+
+type platform struct {
+	rect  pixel.Rect
+	color color.Color
+}
+
+func (p *platform) draw(imd *imdraw.IMDraw) {
+	imd.Color(p.color)
+	imd.Push(p.rect.Min, p.rect.Max)
+	imd.Rectangle(0)
+}
+
+type gopherPhys struct {
+	gravity   float64
+	runSpeed  float64
+	jumpSpeed float64
+
+	rect   pixel.Rect
+	vel    pixel.Vec
+	ground bool
+}
+
+func (gp *gopherPhys) update(dt float64, ctrl pixel.Vec, platforms []platform) {
+	// apply controls
+	switch {
+	case ctrl.X() < 0:
+		gp.vel = gp.vel.WithX(-gp.runSpeed)
+	case ctrl.X() > 0:
+		gp.vel = gp.vel.WithX(+gp.runSpeed)
+	default:
+		gp.vel = gp.vel.WithX(0)
+	}
+
+	// apply gravity and velocity
+	gp.vel += pixel.Y(gp.gravity).Scaled(dt)
+	gp.rect = gp.rect.Moved(gp.vel.Scaled(dt))
+
+	// check collisions agains each platform
+	gp.ground = false
+	if gp.vel.Y() <= 0 {
+		for _, p := range platforms {
+			if gp.rect.Max.X() <= p.rect.Min.X() || gp.rect.Min.X() >= p.rect.Max.X() {
+				continue
+			}
+			if gp.rect.Min.Y() > p.rect.Max.Y() || gp.rect.Min.Y() < p.rect.Max.Y()+gp.vel.Y()*dt {
+				continue
+			}
+			gp.vel = gp.vel.WithY(0)
+			gp.rect = gp.rect.Moved(pixel.Y(p.rect.Max.Y() - gp.rect.Min.Y()))
+			gp.ground = true
+		}
+	}
+
+	// jump if on the ground and the player wants to jump
+	if gp.ground && ctrl.Y() > 0 {
+		gp.vel = gp.vel.WithY(gp.jumpSpeed)
+	}
+}
+
+type animState int
+
+const (
+	idle animState = iota
+	running
+	jumping
+)
+
+type gopherAnim struct {
+	sheet pixel.Picture
+	anims map[string][]pixel.Rect
+	rate  float64
+
+	state   animState
+	counter float64
+	dir     float64
+
+	frame pixel.Rect
+
+	sprite *pixel.Sprite
+}
+
+func (ga *gopherAnim) update(dt float64, phys *gopherPhys) {
+	ga.counter += dt
+
+	// determine the new animation state
+	var newState animState
+	switch {
+	case !phys.ground:
+		newState = jumping
+	case phys.vel.Len() == 0:
+		newState = idle
+	case phys.vel.Len() > 0:
+		newState = running
+	}
+
+	// reset the time counter if the state changed
+	if ga.state != newState {
+		ga.state = newState
+		ga.counter = 0
+	}
+
+	// determine the correct animation frame
+	switch ga.state {
+	case idle:
+		ga.frame = ga.anims["Front"][0]
+	case running:
+		i := int(math.Floor(ga.counter / ga.rate))
+		ga.frame = ga.anims["Run"][i%len(ga.anims["Run"])]
+	case jumping:
+		speed := phys.vel.Y()
+		i := int((-speed/phys.jumpSpeed + 1) / 2 * float64(len(ga.anims["Jump"])))
+		if i < 0 {
+			i = 0
+		}
+		if i >= len(ga.anims["Jump"]) {
+			i = len(ga.anims["Jump"]) - 1
+		}
+		ga.frame = ga.anims["Jump"][i]
+	}
+
+	// set the facing direction of the gopher
+	if phys.vel.X() != 0 {
+		if phys.vel.X() > 0 {
+			ga.dir = +1
+		} else {
+			ga.dir = -1
+		}
+	}
+}
+
+func (ga *gopherAnim) draw(t pixel.Target, phys *gopherPhys) {
+	if ga.sprite == nil {
+		ga.sprite = pixel.NewSprite(nil, pixel.Rect{})
+	}
+	// draw the correct frame with the correct positon and direction
+	ga.sprite.Set(ga.sheet, ga.frame)
+	ga.sprite.SetMatrix(pixel.IM.
+		ScaledXY(0, pixel.V(
+			phys.rect.W()/ga.sprite.Frame().W(),
+			phys.rect.H()/ga.sprite.Frame().H(),
+		)).
+		ScaledXY(0, pixel.V(-ga.dir, 1)).
+		Moved(phys.rect.Center()),
+	)
+	ga.sprite.Draw(t)
+}
+
+type goal struct {
+	pos    pixel.Vec
+	radius float64
+	step   float64
+
+	counter float64
+	cols    [5]pixel.RGBA
+}
+
+func (g *goal) update(dt float64) {
+	g.counter += dt
+	for g.counter > g.step {
+		g.counter -= g.step
+		for i := len(g.cols) - 2; i >= 0; i-- {
+			g.cols[i+1] = g.cols[i]
+		}
+		g.cols[0] = randomNiceColor()
+	}
+}
+
+func (g *goal) draw(imd *imdraw.IMDraw) {
+	for i := len(g.cols) - 1; i >= 0; i-- {
+		imd.Color(g.cols[i])
+		imd.Push(g.pos)
+		imd.Circle(float64(i+1)*g.radius/float64(len(g.cols)), 0)
+	}
+}
+
+func randomNiceColor() pixel.RGBA {
+again:
+	r := rand.Float64()
+	g := rand.Float64()
+	b := rand.Float64()
+	len := math.Sqrt(r*r + g*g + b*b)
+	if len == 0 {
+		goto again
+	}
+	return pixel.RGB(r/len, g/len, b/len)
+}
+
+func run() {
+	rand.Seed(time.Now().UnixNano())
+
+	sheet, anims, err := loadAnimationSheet("sheet.png", "sheet.csv", 12)
+	if err != nil {
+		panic(err)
+	}
+
+	cfg := pixelgl.WindowConfig{
+		Title:  "Platformer",
+		Bounds: pixel.R(0, 0, 1024, 768),
+		VSync:  true,
+	}
+	win, err := pixelgl.NewWindow(cfg)
+	if err != nil {
+		panic(err)
+	}
+
+	phys := &gopherPhys{
+		gravity:   -512,
+		runSpeed:  64,
+		jumpSpeed: 192,
+		rect:      pixel.R(-6, -7, 6, 7),
+	}
+
+	anim := &gopherAnim{
+		sheet: sheet,
+		anims: anims,
+		rate:  1.0 / 10,
+		dir:   +1,
+	}
+
+	// hardcoded level
+	platforms := []platform{
+		{rect: pixel.R(-50, -34, 50, -32)},
+		{rect: pixel.R(20, 0, 70, 2)},
+		{rect: pixel.R(-100, 10, -50, 12)},
+		{rect: pixel.R(120, -22, 140, -20)},
+		{rect: pixel.R(120, -72, 140, -70)},
+		{rect: pixel.R(120, -122, 140, -120)},
+		{rect: pixel.R(-100, -152, 100, -150)},
+		{rect: pixel.R(-150, -127, -140, -125)},
+		{rect: pixel.R(-180, -97, -170, -95)},
+		{rect: pixel.R(-150, -67, -140, -65)},
+		{rect: pixel.R(-180, -37, -170, -35)},
+		{rect: pixel.R(-150, -7, -140, -5)},
+	}
+	for i := range platforms {
+		platforms[i].color = randomNiceColor()
+	}
+
+	gol := &goal{
+		pos:    pixel.V(-75, 40),
+		radius: 18,
+		step:   1.0 / 7,
+	}
+
+	canvas := pixelgl.NewCanvas(pixel.R(-160/2, -120/2, 160/2, 120/2))
+	imd := imdraw.New(sheet)
+	imd.Precision(32)
+
+	camPos := pixel.V(0, 0)
+
+	last := time.Now()
+	for !win.Closed() {
+		dt := time.Since(last).Seconds()
+		last = time.Now()
+
+		// lerp the camera position towards the gopher
+		camPos = pixel.Lerp(camPos, phys.rect.Center(), 1-math.Pow(1.0/64, dt))
+		cam := pixel.IM.Moved(-camPos)
+		canvas.SetMatrix(cam)
+
+		// slow motion with tab
+		if win.Pressed(pixelgl.KeyTab) {
+			dt /= 8
+		}
+
+		// restart the level on pressing enter
+		if win.JustPressed(pixelgl.KeyEnter) {
+			phys.rect = phys.rect.Moved(-phys.rect.Center())
+			phys.vel = 0
+		}
+
+		// control the gopher with keys
+		ctrl := pixel.V(0, 0)
+		if win.Pressed(pixelgl.KeyLeft) {
+			ctrl -= pixel.X(1)
+		}
+		if win.Pressed(pixelgl.KeyRight) {
+			ctrl += pixel.X(1)
+		}
+		if win.JustPressed(pixelgl.KeyUp) {
+			ctrl = ctrl.WithY(1)
+		}
+
+		// update the physics and animation
+		phys.update(dt, ctrl, platforms)
+		gol.update(dt)
+		anim.update(dt, phys)
+
+		// draw the scene to the canvas using IMDraw
+		canvas.Clear(colornames.Black)
+		imd.Clear()
+		for _, p := range platforms {
+			p.draw(imd)
+		}
+		gol.draw(imd)
+		anim.draw(imd, phys)
+		imd.Draw(canvas)
+
+		// stretch the canvas to the window
+		win.Clear(colornames.White)
+		win.SetMatrix(pixel.IM.Scaled(0,
+			math.Min(
+				win.Bounds().W()/canvas.Bounds().W(),
+				win.Bounds().H()/canvas.Bounds().H(),
+			),
+		).Moved(win.Bounds().Center()))
+		canvas.Draw(win)
+		win.Update()
+	}
+}
+
+func main() {
+	pixelgl.Run(run)
+}
diff --git a/examples/platformer/screenshot.png b/examples/platformer/screenshot.png
new file mode 100644
index 0000000000000000000000000000000000000000..4b8b54be64b0ad7a2a72d3e3fe2e5c947504f4a9
Binary files /dev/null and b/examples/platformer/screenshot.png differ
diff --git a/examples/platformer/sheet.csv b/examples/platformer/sheet.csv
new file mode 100644
index 0000000000000000000000000000000000000000..159846dc8a60912e90461305d99be4cec412d850
--- /dev/null
+++ b/examples/platformer/sheet.csv
@@ -0,0 +1,9 @@
+Front,0,0
+FrontBlink,1,1
+LookUp,2,2
+Left,3,7
+LeftRight,4,6
+LeftBlink,7,7
+Walk,8,15
+Run,16,23
+Jump,24,26
\ No newline at end of file
diff --git a/examples/platformer/sheet.png b/examples/platformer/sheet.png
new file mode 100644
index 0000000000000000000000000000000000000000..8be1b97b1cffdcfbcb783840c5f8c2fe092a7e03
Binary files /dev/null and b/examples/platformer/sheet.png differ