diff --git a/Makefile b/Makefile
index 999cd41..ec57571 100644
--- a/Makefile
+++ b/Makefile
@@ -50,6 +50,12 @@ tinygo-canvas: clean
cp ./canvas/index.html ./html/index.html
cp ./canvas/main.go ./html/main.go
+.PHONY: ebiten
+ebiten: clean
+ GO111MODULE=on GOOS=js GOARCH=wasm go build -o ./html/ebiten.wasm ./ebiten/main.go
+ cp ./ebiten/index.html ./html/index.html
+ cp $$(go env GOROOT)/misc/wasm/wasm_exec.js ./html/wasm_exec.js
+
test: clean
GOOS=js GOARCH=wasm go test -c -o ./html/test.wasm ./test/
diff --git a/README.md b/README.md
index e808502..200a82d 100644
--- a/README.md
+++ b/README.md
@@ -82,3 +82,7 @@ send a HTTP request, parse the result and write it to the DOM.
An updated version of the [repulsion](https://stdiopt.github.io/gowasm-experiments/repulsion)
demo by [Luis Figuerido](https://github.com/stdiopt) usin Go 1.12.
+### Ebiten
+
+A short demo of using the [Ebiten game engine](https://github.com/hajimehoshi/ebiten)
+to create a WebGL based flappy bird clone. Copied from the Ebiten examples.
diff --git a/ebiten/LICENSE b/ebiten/LICENSE
new file mode 100644
index 0000000..8bae162
--- /dev/null
+++ b/ebiten/LICENSE
@@ -0,0 +1,13 @@
+// Copyright 2018 The Ebiten Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
diff --git a/ebiten/index.html b/ebiten/index.html
new file mode 100644
index 0000000..d7c4a4a
--- /dev/null
+++ b/ebiten/index.html
@@ -0,0 +1,16 @@
+
+
+
diff --git a/ebiten/main.go b/ebiten/main.go
new file mode 100644
index 0000000..c402f1c
--- /dev/null
+++ b/ebiten/main.go
@@ -0,0 +1,389 @@
+// +build js,wasm
+
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "image"
+ "image/color"
+ _ "image/png"
+ "log"
+ "math"
+ "math/rand"
+ "runtime"
+ "time"
+
+ "github.com/golang/freetype/truetype"
+ "golang.org/x/image/font"
+
+ "github.com/hajimehoshi/ebiten"
+ "github.com/hajimehoshi/ebiten/audio"
+ "github.com/hajimehoshi/ebiten/audio/vorbis"
+ "github.com/hajimehoshi/ebiten/audio/wav"
+ "github.com/hajimehoshi/ebiten/ebitenutil"
+ raudio "github.com/hajimehoshi/ebiten/examples/resources/audio"
+ "github.com/hajimehoshi/ebiten/examples/resources/fonts"
+ resources "github.com/hajimehoshi/ebiten/examples/resources/images/flappy"
+ "github.com/hajimehoshi/ebiten/inpututil"
+ "github.com/hajimehoshi/ebiten/text"
+)
+
+func init() {
+ rand.Seed(time.Now().UnixNano())
+}
+
+func floorDiv(x, y int) int {
+ d := x / y
+ if d*y == x || x >= 0 {
+ return d
+ }
+ return d - 1
+}
+
+func floorMod(x, y int) int {
+ return x - floorDiv(x, y)*y
+}
+
+const (
+ screenWidth = 640
+ screenHeight = 480
+ tileSize = 32
+ fontSize = 32
+ smallFontSize = fontSize / 2
+ pipeWidth = tileSize * 2
+ pipeStartOffsetX = 8
+ pipeIntervalX = 8
+ pipeGapY = 5
+)
+
+var (
+ gopherImage *ebiten.Image
+ tilesImage *ebiten.Image
+ arcadeFont font.Face
+ smallArcadeFont font.Face
+)
+
+func init() {
+ img, _, err := image.Decode(bytes.NewReader(resources.Gopher_png))
+ if err != nil {
+ log.Fatal(err)
+ }
+ gopherImage, _ = ebiten.NewImageFromImage(img, ebiten.FilterDefault)
+
+ img, _, err = image.Decode(bytes.NewReader(resources.Tiles_png))
+ if err != nil {
+ log.Fatal(err)
+ }
+ tilesImage, _ = ebiten.NewImageFromImage(img, ebiten.FilterDefault)
+}
+
+func init() {
+ tt, err := truetype.Parse(fonts.ArcadeN_ttf)
+ if err != nil {
+ log.Fatal(err)
+ }
+ const dpi = 72
+ arcadeFont = truetype.NewFace(tt, &truetype.Options{
+ Size: fontSize,
+ DPI: dpi,
+ Hinting: font.HintingFull,
+ })
+ smallArcadeFont = truetype.NewFace(tt, &truetype.Options{
+ Size: smallFontSize,
+ DPI: dpi,
+ Hinting: font.HintingFull,
+ })
+}
+
+var (
+ audioContext *audio.Context
+ jumpPlayer *audio.Player
+ hitPlayer *audio.Player
+)
+
+func init() {
+ audioContext, _ = audio.NewContext(44100)
+
+ jumpD, err := vorbis.Decode(audioContext, audio.BytesReadSeekCloser(raudio.Jump_ogg))
+ if err != nil {
+ log.Fatal(err)
+ }
+ jumpPlayer, err = audio.NewPlayer(audioContext, jumpD)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ jabD, err := wav.Decode(audioContext, audio.BytesReadSeekCloser(raudio.Jab_wav))
+ if err != nil {
+ log.Fatal(err)
+ }
+ hitPlayer, err = audio.NewPlayer(audioContext, jabD)
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+type Mode int
+
+const (
+ ModeTitle Mode = iota
+ ModeGame
+ ModeGameOver
+)
+
+type Game struct {
+ mode Mode
+
+ // The gopher's position
+ x16 int
+ y16 int
+ vy16 int
+
+ // Camera
+ cameraX int
+ cameraY int
+
+ // Pipes
+ pipeTileYs []int
+
+ gameoverCount int
+}
+
+func NewGame() *Game {
+ g := &Game{}
+ g.init()
+ return g
+}
+
+func (g *Game) init() {
+ g.x16 = 0
+ g.y16 = 100 * 16
+ g.cameraX = -240
+ g.cameraY = 0
+ g.pipeTileYs = make([]int, 256)
+ for i := range g.pipeTileYs {
+ g.pipeTileYs[i] = rand.Intn(6) + 2
+ }
+}
+
+func jump() bool {
+ if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
+ return true
+ }
+ if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
+ return true
+ }
+ if len(inpututil.JustPressedTouchIDs()) > 0 {
+ return true
+ }
+ return false
+}
+
+func (g *Game) Update(screen *ebiten.Image) error {
+ switch g.mode {
+ case ModeTitle:
+ if jump() {
+ g.mode = ModeGame
+ }
+ case ModeGame:
+ g.x16 += 32
+ g.cameraX += 2
+ if jump() {
+ g.vy16 = -96
+ jumpPlayer.Rewind()
+ jumpPlayer.Play()
+ }
+ g.y16 += g.vy16
+
+ // Gravity
+ g.vy16 += 4
+ if g.vy16 > 96 {
+ g.vy16 = 96
+ }
+
+ if g.hit() {
+ hitPlayer.Rewind()
+ hitPlayer.Play()
+ g.mode = ModeGameOver
+ g.gameoverCount = 30
+ }
+ case ModeGameOver:
+ if g.gameoverCount > 0 {
+ g.gameoverCount--
+ }
+ if g.gameoverCount == 0 && jump() {
+ g.init()
+ g.mode = ModeTitle
+ }
+ }
+
+ if ebiten.IsDrawingSkipped() {
+ return nil
+ }
+
+ screen.Fill(color.RGBA{0x80, 0xa0, 0xc0, 0xff})
+ g.drawTiles(screen)
+ if g.mode != ModeTitle {
+ g.drawGopher(screen)
+ }
+ var texts []string
+ switch g.mode {
+ case ModeTitle:
+ texts = []string{"FLAPPY GOPHER", "", "", "", "", "PRESS SPACE KEY", "", "OR TOUCH SCREEN"}
+ case ModeGameOver:
+ texts = []string{"", "GAMEOVER!"}
+ }
+ for i, l := range texts {
+ x := (screenWidth - len(l)*fontSize) / 2
+ text.Draw(screen, l, arcadeFont, x, (i+4)*fontSize, color.White)
+ }
+
+ if g.mode == ModeTitle {
+ msg := []string{
+ "Go Gopher by Renee French is",
+ "licenced under CC BY 3.0.",
+ }
+ for i, l := range msg {
+ x := (screenWidth - len(l)*smallFontSize) / 2
+ text.Draw(screen, l, smallArcadeFont, x, screenHeight-4+(i-1)*smallFontSize, color.White)
+ }
+ }
+
+ scoreStr := fmt.Sprintf("%04d", g.score())
+ text.Draw(screen, scoreStr, arcadeFont, screenWidth-len(scoreStr)*fontSize, fontSize, color.White)
+ ebitenutil.DebugPrint(screen, fmt.Sprintf("TPS: %0.2f", ebiten.CurrentTPS()))
+ return nil
+}
+
+func (g *Game) pipeAt(tileX int) (tileY int, ok bool) {
+ if (tileX - pipeStartOffsetX) <= 0 {
+ return 0, false
+ }
+ if floorMod(tileX-pipeStartOffsetX, pipeIntervalX) != 0 {
+ return 0, false
+ }
+ idx := floorDiv(tileX-pipeStartOffsetX, pipeIntervalX)
+ return g.pipeTileYs[idx%len(g.pipeTileYs)], true
+}
+
+func (g *Game) score() int {
+ x := floorDiv(g.x16, 16) / tileSize
+ if (x - pipeStartOffsetX) <= 0 {
+ return 0
+ }
+ return floorDiv(x-pipeStartOffsetX, pipeIntervalX)
+}
+
+func (g *Game) hit() bool {
+ if g.mode != ModeGame {
+ return false
+ }
+ const (
+ gopherWidth = 30
+ gopherHeight = 60
+ )
+ w, h := gopherImage.Size()
+ x0 := floorDiv(g.x16, 16) + (w-gopherWidth)/2
+ y0 := floorDiv(g.y16, 16) + (h-gopherHeight)/2
+ x1 := x0 + gopherWidth
+ y1 := y0 + gopherHeight
+ if y0 < -tileSize*4 {
+ return true
+ }
+ if y1 >= screenHeight-tileSize {
+ return true
+ }
+ xMin := floorDiv(x0-pipeWidth, tileSize)
+ xMax := floorDiv(x0+gopherWidth, tileSize)
+ for x := xMin; x <= xMax; x++ {
+ y, ok := g.pipeAt(x)
+ if !ok {
+ continue
+ }
+ if x0 >= x*tileSize+pipeWidth {
+ continue
+ }
+ if x1 < x*tileSize {
+ continue
+ }
+ if y0 < y*tileSize {
+ return true
+ }
+ if y1 >= (y+pipeGapY)*tileSize {
+ return true
+ }
+ }
+ return false
+}
+
+func (g *Game) drawTiles(screen *ebiten.Image) {
+ const (
+ nx = screenWidth / tileSize
+ ny = screenHeight / tileSize
+ pipeTileSrcX = 128
+ pipeTileSrcY = 192
+ )
+
+ op := &ebiten.DrawImageOptions{}
+ for i := -2; i < nx+1; i++ {
+ // ground
+ op.GeoM.Reset()
+ op.GeoM.Translate(float64(i*tileSize-floorMod(g.cameraX, tileSize)),
+ float64((ny-1)*tileSize-floorMod(g.cameraY, tileSize)))
+ screen.DrawImage(tilesImage.SubImage(image.Rect(0, 0, tileSize, tileSize)).(*ebiten.Image), op)
+
+ // pipe
+ if tileY, ok := g.pipeAt(floorDiv(g.cameraX, tileSize) + i); ok {
+ for j := 0; j < tileY; j++ {
+ op.GeoM.Reset()
+ op.GeoM.Scale(1, -1)
+ op.GeoM.Translate(float64(i*tileSize-floorMod(g.cameraX, tileSize)),
+ float64(j*tileSize-floorMod(g.cameraY, tileSize)))
+ op.GeoM.Translate(0, tileSize)
+ var r image.Rectangle
+ if j == tileY-1 {
+ r = image.Rect(pipeTileSrcX, pipeTileSrcY, pipeTileSrcX+tileSize*2, pipeTileSrcY+tileSize)
+ } else {
+ r = image.Rect(pipeTileSrcX, pipeTileSrcY+tileSize, pipeTileSrcX+tileSize*2, pipeTileSrcY+tileSize*2)
+ }
+ screen.DrawImage(tilesImage.SubImage(r).(*ebiten.Image), op)
+ }
+ for j := tileY + pipeGapY; j < screenHeight/tileSize-1; j++ {
+ op.GeoM.Reset()
+ op.GeoM.Translate(float64(i*tileSize-floorMod(g.cameraX, tileSize)),
+ float64(j*tileSize-floorMod(g.cameraY, tileSize)))
+ var r image.Rectangle
+ if j == tileY+pipeGapY {
+ r = image.Rect(pipeTileSrcX, pipeTileSrcY, pipeTileSrcX+pipeWidth, pipeTileSrcY+tileSize)
+ } else {
+ r = image.Rect(pipeTileSrcX, pipeTileSrcY+tileSize, pipeTileSrcX+pipeWidth, pipeTileSrcY+tileSize+tileSize)
+ }
+ screen.DrawImage(tilesImage.SubImage(r).(*ebiten.Image), op)
+ }
+ }
+ }
+}
+
+func (g *Game) drawGopher(screen *ebiten.Image) {
+ op := &ebiten.DrawImageOptions{}
+ w, h := gopherImage.Size()
+ op.GeoM.Translate(-float64(w)/2.0, -float64(h)/2.0)
+ op.GeoM.Rotate(float64(g.vy16) / 96.0 * math.Pi / 6)
+ op.GeoM.Translate(float64(w)/2.0, float64(h)/2.0)
+ op.GeoM.Translate(float64(g.x16/16.0)-float64(g.cameraX), float64(g.y16/16.0)-float64(g.cameraY))
+ op.Filter = ebiten.FilterLinear
+ screen.DrawImage(gopherImage, op)
+}
+
+func main() {
+ g := NewGame()
+ // On browsers, let's use fullscreen so that this is playable on any browsers.
+ // It is planned to ignore the given 'scale' apply fullscreen automatically on browsers (#571).
+ if runtime.GOARCH == "js" || runtime.GOOS == "js" {
+ ebiten.SetFullscreen(true)
+ }
+ if err := ebiten.Run(g.Update, screenWidth, screenHeight, 1, "Flappy Gopher (Ebiten Demo)"); err != nil {
+ panic(err)
+ }
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..983774b
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,8 @@
+module github.com/johanbrandhorst/wasm-experiments
+
+go 1.12
+
+require (
+ github.com/hajimehoshi/ebiten v1.9.3 // indirect
+ golang.org/x/image v0.0.0-20190618124811-92942e4437e2 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..14d88ae
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,39 @@
+github.com/go-gl/glfw v0.0.0-20181213070059-819e8ce5125f/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/gofrs/flock v0.7.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
+github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gopherjs/gopherwasm v0.1.1/go.mod h1:kx4n9a+MzHH0BJJhvlsQ65hqLFXDO/m256AsaDPQ+/4=
+github.com/gopherjs/gopherwasm v1.0.0/go.mod h1:SkZ8z7CWBz5VXbhJel8TxCmAcsQqzgWGR/8nMhyhZSI=
+github.com/gopherjs/gopherwasm v1.1.0 h1:fA2uLoctU5+T3OhOn2vYP0DVT6pxc7xhTlBB1paATqQ=
+github.com/gopherjs/gopherwasm v1.1.0/go.mod h1:SkZ8z7CWBz5VXbhJel8TxCmAcsQqzgWGR/8nMhyhZSI=
+github.com/hajimehoshi/bitmapfont v1.1.1/go.mod h1:Hamfxgney7tDSmVOSDh2AWzoDH70OaC+P24zc02Gum4=
+github.com/hajimehoshi/ebiten v1.9.3 h1:YijWGMBwH2XA1ZytUQFy33UDHeCSS6d4JZKH1wq38O0=
+github.com/hajimehoshi/ebiten v1.9.3/go.mod h1:XxiJ4Eltvb1KmcD0i6F81eIB1asJhK47y5DC+FPkyso=
+github.com/hajimehoshi/go-mp3 v0.2.0/go.mod h1:4i+c5pDNKDrxl1iu9iG90/+fhP37lio6gNhjCx9WBJw=
+github.com/hajimehoshi/oto v0.1.1/go.mod h1:hUiLWeBQnbDu4pZsAhOnGqMI1ZGibS6e2qhQdfpwz04=
+github.com/hajimehoshi/oto v0.3.3 h1:Wi7VVtxe9sF2rbDBIJtVXnpFWhRfK57hw0JY7tR2qXM=
+github.com/hajimehoshi/oto v0.3.3/go.mod h1:e9eTLBB9iZto045HLbzfHJIc+jP3xaKrjZTghvb6fdM=
+github.com/jakecoffman/cp v0.1.0/go.mod h1:a3xPx9N8RyFAACD644t2dj/nK4SuLg1v+jL61m2yVo4=
+github.com/jfreymuth/oggvorbis v1.0.0 h1:aOpiihGrFLXpsh2osOlEvTcg5/aluzGQeC7m3uYWOZ0=
+github.com/jfreymuth/oggvorbis v1.0.0/go.mod h1:abe6F9QRjuU9l+2jek3gj46lu40N4qlYxh2grqkLEDM=
+github.com/jfreymuth/vorbis v1.0.0 h1:SmDf783s82lIjGZi8EGUUaS7YxPHgRj4ZXW/h7rUi7U=
+github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+golang.org/x/exp v0.0.0-20180710024300-14dda7b62fcd/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
+golang.org/x/image v0.0.0-20180926015637-991ec62608f3/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
+golang.org/x/image v0.0.0-20190118043309-183bebdce1b2/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
+golang.org/x/image v0.0.0-20190618124811-92942e4437e2 h1:fqF3kMQ0tlBEpnfxavzOrjqW5gokBwllwOABYxETOMA=
+golang.org/x/image v0.0.0-20190618124811-92942e4437e2/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/mobile v0.0.0-20180806140643-507816974b79/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190127143845-a42111704963/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190203050204-7ae0202eb74c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20190202235157-7414d4c1f71c/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=