diff --git a/examples/typewriter/README.md b/examples/typewriter/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..0813bb0b823a52bf5bdef30176f945dbd3adacc4
--- /dev/null
+++ b/examples/typewriter/README.md
@@ -0,0 +1,7 @@
+# Typewriter
+
+This example demonstrates text drawing and text input facilities by implementing a fancy typewriter
+with a red laser cursor. Screen shakes a bit when typing and some letters turn bold or italic
+randomly.
+
+![Screenshot](screenshot.png)
\ No newline at end of file
diff --git a/examples/typewriter/main.go b/examples/typewriter/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..6b4bb952245b4b1b598381ab60237126698c474c
--- /dev/null
+++ b/examples/typewriter/main.go
@@ -0,0 +1,314 @@
+package main
+
+import (
+	"image/color"
+	"math"
+	"math/rand"
+	"sync"
+	"time"
+	"unicode"
+
+	"github.com/faiface/pixel"
+	"github.com/faiface/pixel/imdraw"
+	"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"
+	"golang.org/x/image/font/gofont/gobold"
+	"golang.org/x/image/font/gofont/goitalic"
+	"golang.org/x/image/font/gofont/goregular"
+)
+
+func ttfFromBytesMust(b []byte, opts *truetype.Options) font.Face {
+	ttf, err := truetype.Parse(b)
+	if err != nil {
+		panic(err)
+	}
+	return truetype.NewFace(ttf, opts)
+}
+
+type typewriter struct {
+	mu sync.Mutex
+
+	regular *text.Text
+	bold    *text.Text
+	italic  *text.Text
+
+	offset   pixel.Vec
+	position pixel.Vec
+	move     pixel.Vec
+}
+
+func newTypewriter(c color.Color, regular, bold, italic *text.Atlas) *typewriter {
+	tw := &typewriter{
+		regular: text.New(pixel.ZV, regular),
+		bold:    text.New(pixel.ZV, bold),
+		italic:  text.New(pixel.ZV, italic),
+	}
+	tw.regular.Color = c
+	tw.bold.Color = c
+	tw.italic.Color = c
+	return tw
+}
+
+func (tw *typewriter) Ribbon(r rune) {
+	tw.mu.Lock()
+	defer tw.mu.Unlock()
+
+	dice := rand.Intn(21)
+	switch {
+	case 0 <= dice && dice <= 18:
+		tw.regular.WriteRune(r)
+	case dice == 19:
+		tw.bold.Dot = tw.regular.Dot
+		tw.bold.WriteRune(r)
+		tw.regular.Dot = tw.bold.Dot
+	case dice == 20:
+		tw.italic.Dot = tw.regular.Dot
+		tw.italic.WriteRune(r)
+		tw.regular.Dot = tw.italic.Dot
+	}
+}
+
+func (tw *typewriter) Back() {
+	tw.mu.Lock()
+	defer tw.mu.Unlock()
+	tw.regular.Dot = tw.regular.Dot.Sub(pixel.V(tw.regular.Atlas().Glyph(' ').Advance, 0))
+}
+
+func (tw *typewriter) Offset(off pixel.Vec) {
+	tw.mu.Lock()
+	defer tw.mu.Unlock()
+	tw.offset = tw.offset.Add(off)
+}
+
+func (tw *typewriter) Position() pixel.Vec {
+	tw.mu.Lock()
+	defer tw.mu.Unlock()
+	return tw.position
+}
+
+func (tw *typewriter) Move(vel pixel.Vec) {
+	tw.mu.Lock()
+	defer tw.mu.Unlock()
+	tw.move = vel
+}
+
+func (tw *typewriter) Dot() pixel.Vec {
+	tw.mu.Lock()
+	defer tw.mu.Unlock()
+	return tw.regular.Dot
+}
+
+func (tw *typewriter) Update(dt float64) {
+	tw.mu.Lock()
+	defer tw.mu.Unlock()
+	tw.position = tw.position.Add(tw.move.Scaled(dt))
+}
+
+func (tw *typewriter) Draw(t pixel.Target, m pixel.Matrix) {
+	tw.mu.Lock()
+	defer tw.mu.Unlock()
+
+	m = pixel.IM.Moved(tw.position.Add(tw.offset)).Chained(m)
+	tw.regular.Draw(t, m)
+	tw.bold.Draw(t, m)
+	tw.italic.Draw(t, m)
+}
+
+func typeRune(tw *typewriter, r rune) {
+	tw.Ribbon(r)
+	if !unicode.IsSpace(r) {
+		go shake(tw, 3, 30)
+	}
+}
+
+func back(tw *typewriter) {
+	tw.Back()
+}
+
+func shake(tw *typewriter, intensity, friction float64) {
+	const (
+		freq = 24
+		dt   = 1.0 / freq
+	)
+	ticker := time.NewTicker(time.Second / freq)
+	defer ticker.Stop()
+
+	off := pixel.ZV
+
+	for range ticker.C {
+		tw.Offset(off.Scaled(-1))
+
+		if intensity < 0.01*dt {
+			break
+		}
+
+		off = pixel.V((rand.Float64()-0.5)*intensity*2, (rand.Float64()-0.5)*intensity*2)
+		intensity -= friction * dt
+
+		tw.Offset(off)
+	}
+}
+
+func scroll(tw *typewriter, intensity, speedUp float64) {
+	const (
+		freq = 120
+		dt   = 1.0 / freq
+	)
+	ticker := time.NewTicker(time.Second / freq)
+	defer ticker.Stop()
+
+	speed := 0.0
+
+	for range ticker.C {
+		if math.Abs(tw.Dot().Y+tw.Position().Y) < 0.01 {
+			break
+		}
+
+		targetSpeed := -(tw.Dot().Y + tw.Position().Y) * intensity
+		if speed < targetSpeed {
+			speed += speedUp * dt
+		} else {
+			speed = targetSpeed
+		}
+
+		tw.Move(pixel.V(0, speed))
+	}
+}
+
+type dotlight struct {
+	tw           *typewriter
+	color        color.Color
+	radius       float64
+	intensity    float64
+	acceleration float64
+	maxSpeed     float64
+
+	pos pixel.Vec
+	vel pixel.Vec
+
+	imd *imdraw.IMDraw
+}
+
+func newDotlight(tw *typewriter, c color.Color, radius, intensity, acceleration, maxSpeed float64) *dotlight {
+	return &dotlight{
+		tw:           tw,
+		color:        c,
+		radius:       radius,
+		intensity:    intensity,
+		acceleration: acceleration,
+		maxSpeed:     maxSpeed,
+		pos:          tw.Dot(),
+		vel:          pixel.ZV,
+		imd:          imdraw.New(nil),
+	}
+}
+
+func (dl *dotlight) Update(dt float64) {
+	targetVel := dl.tw.Dot().Add(dl.tw.Position()).Sub(dl.pos).Scaled(dl.intensity)
+	acc := targetVel.Sub(dl.vel).Scaled(dl.acceleration)
+	dl.vel = dl.vel.Add(acc.Scaled(dt))
+	if dl.vel.Len() > dl.maxSpeed {
+		dl.vel = dl.vel.Unit().Scaled(dl.maxSpeed)
+	}
+	dl.pos = dl.pos.Add(dl.vel.Scaled(dt))
+}
+
+func (dl *dotlight) Draw(t pixel.Target, m pixel.Matrix) {
+	dl.imd.Clear()
+	dl.imd.SetMatrix(m)
+	dl.imd.Color = dl.color
+	dl.imd.Push(dl.pos)
+	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.Polygon(0)
+	dl.imd.Draw(t)
+}
+
+func run() {
+	rand.Seed(time.Now().UnixNano())
+
+	cfg := pixelgl.WindowConfig{
+		Title:     "Typewriter",
+		Bounds:    pixel.R(0, 0, 1024, 768),
+		Resizable: true,
+	}
+	win, err := pixelgl.NewWindow(cfg)
+	if err != nil {
+		panic(err)
+	}
+	win.SetSmooth(true)
+
+	var (
+		regular = text.NewAtlas(ttfFromBytesMust(goregular.TTF, &truetype.Options{
+			Size: 42,
+		}), text.ASCII, text.RangeTable(unicode.Latin))
+		bold = text.NewAtlas(ttfFromBytesMust(gobold.TTF, &truetype.Options{
+			Size: 42,
+		}), text.ASCII, text.RangeTable(unicode.Latin))
+		italic = text.NewAtlas(ttfFromBytesMust(goitalic.TTF, &truetype.Options{
+			Size: 42,
+		}), text.ASCII, text.RangeTable(unicode.Latin))
+
+		bgColor = color.RGBA{
+			R: 241,
+			G: 241,
+			B: 212,
+			A: 255,
+		}
+		fgColor = color.RGBA{
+			R: 0,
+			G: 15,
+			B: 85,
+			A: 255,
+		}
+
+		tw = newTypewriter(pixel.ToRGBA(fgColor).Scaled(0.9), regular, bold, italic)
+		dl = newDotlight(tw, colornames.Red, 6, 30, 20, 1600)
+	)
+
+	fps := time.Tick(time.Second / 120)
+	last := time.Now()
+	for !win.Closed() {
+		for _, r := range win.Typed() {
+			go typeRune(tw, r)
+		}
+		if win.JustPressed(pixelgl.KeyTab) || win.Repeated(pixelgl.KeyTab) {
+			go typeRune(tw, '\t')
+		}
+		if win.JustPressed(pixelgl.KeyEnter) || win.Repeated(pixelgl.KeyEnter) {
+			go typeRune(tw, '\n')
+			go scroll(tw, 20, 6400)
+		}
+		if win.JustPressed(pixelgl.KeyBackspace) || win.Repeated(pixelgl.KeyBackspace) {
+			go back(tw)
+		}
+
+		dt := time.Since(last).Seconds()
+		last = time.Now()
+
+		tw.Update(dt)
+		dl.Update(dt)
+
+		win.Clear(bgColor)
+
+		m := pixel.IM.Moved(pixel.V(32, 32))
+		tw.Draw(win, m)
+		dl.Draw(win, m)
+
+		win.Update()
+		<-fps
+	}
+}
+
+func main() {
+	pixelgl.Run(run)
+}
diff --git a/examples/typewriter/screenshot.png b/examples/typewriter/screenshot.png
new file mode 100644
index 0000000000000000000000000000000000000000..d353e4311cf54f61b6d086112552f45ae64b9202
Binary files /dev/null and b/examples/typewriter/screenshot.png differ