diff --git a/examples/typewriter/README.md b/examples/typewriter/README.md new file mode 100644 index 0000000..0813bb0 --- /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 0000000..6b4bb95 --- /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 0000000..d353e43 Binary files /dev/null and b/examples/typewriter/screenshot.png differ