diff --git a/_docs/gen.go b/_docs/gen.go index b982fa9d7..b86d1cfc6 100644 --- a/_docs/gen.go +++ b/_docs/gen.go @@ -171,13 +171,18 @@ func versions() string { var examples = []example{ {Name: "alphablending"}, + {Name: "audio"}, + {Name: "font"}, + {Name: "hsv"}, {Name: "hue"}, {Name: "gamepad"}, {Name: "keyboard"}, + {Name: "masking"}, {Name: "mosaic"}, {Name: "noise"}, {Name: "paint"}, {Name: "perspective"}, + {Name: "piano"}, {Name: "rotate"}, {Name: "sprites"}, {Name: "blocks"}, diff --git a/_docs/public/examples/audio.content.html b/_docs/public/examples/audio.content.html new file mode 100644 index 000000000..48a40c929 --- /dev/null +++ b/_docs/public/examples/audio.content.html @@ -0,0 +1,39 @@ + + + diff --git a/_docs/public/examples/audio.html b/_docs/public/examples/audio.html new file mode 100644 index 000000000..d84654a94 --- /dev/null +++ b/_docs/public/examples/audio.html @@ -0,0 +1,326 @@ + + + + +
package main
+
+import (
+ "bytes"
+ "fmt"
+ "image/color"
+ "log"
+ "time"
+
+ "github.com/hajimehoshi/ebiten"
+ "github.com/hajimehoshi/ebiten/ebitenutil"
+ "github.com/hajimehoshi/ebiten/exp/audio"
+ "github.com/hajimehoshi/ebiten/exp/audio/vorbis"
+ "github.com/hajimehoshi/ebiten/exp/audio/wav"
+)
+
+const (
+ screenWidth = 320
+ screenHeight = 240
+)
+
+var (
+ playerBarImage *ebiten.Image
+ playerCurrentImage *ebiten.Image
+)
+
+func init() {
+ var err error
+ playerBarImage, err = ebiten.NewImage(300, 4, ebiten.FilterNearest)
+ if err != nil {
+ log.Fatal(err)
+ }
+ playerBarImage.Fill(&color.RGBA{0x80, 0x80, 0x80, 0xff})
+
+ playerCurrentImage, err = ebiten.NewImage(4, 10, ebiten.FilterNearest)
+ if err != nil {
+ log.Fatal(err)
+ }
+ playerCurrentImage.Fill(&color.RGBA{0xff, 0xff, 0xff, 0xff})
+}
+
+type Player struct {
+ audioPlayer *audio.Player
+ total time.Duration
+}
+
+var (
+ audioContext *audio.Context
+ musicPlayer *Player
+ seStream *wav.Stream
+ sePlayer *audio.Player
+ musicCh = make(chan *Player)
+ seCh = make(chan *wav.Stream)
+ mouseButtonState = map[ebiten.MouseButton]int{}
+ keyState = map[ebiten.Key]int{}
+ volume128 = 128
+)
+
+func playerBarRect() (x, y, w, h int) {
+ w, h = playerBarImage.Size()
+ x = (screenWidth - w) / 2
+ y = screenHeight - h - 16
+ return
+}
+
+type SEStream struct {
+ *bytes.Reader
+}
+
+func (s *SEStream) Close() error {
+ return nil
+}
+
+func (p *Player) updateSE() error {
+ if seStream == nil {
+ return nil
+ }
+ if !ebiten.IsKeyPressed(ebiten.KeyP) {
+ keyState[ebiten.KeyP] = 0
+ return nil
+ }
+ keyState[ebiten.KeyP]++
+ if keyState[ebiten.KeyP] != 1 {
+ return nil
+ }
+ if sePlayer == nil {
+ var err error
+ sePlayer, err = audioContext.NewPlayer(seStream)
+ if err != nil {
+ return err
+ }
+ }
+ if sePlayer.IsPlaying() {
+ return nil
+ }
+ if err := sePlayer.Rewind(); err != nil {
+ return err
+ }
+ return sePlayer.Play()
+}
+
+func (p *Player) updateVolume() error {
+ if p.audioPlayer == nil {
+ return nil
+ }
+ if ebiten.IsKeyPressed(ebiten.KeyZ) {
+ volume128--
+ }
+ if ebiten.IsKeyPressed(ebiten.KeyX) {
+ volume128++
+ }
+ if volume128 < 0 {
+ volume128 = 0
+ }
+ if 128 < volume128 {
+ volume128 = 128
+ }
+ p.audioPlayer.SetVolume(float64(volume128) / 128)
+ return nil
+}
+
+func (p *Player) updatePlayPause() error {
+ if p.audioPlayer == nil {
+ return nil
+ }
+ if !ebiten.IsKeyPressed(ebiten.KeyS) {
+ keyState[ebiten.KeyS] = 0
+ return nil
+ }
+ keyState[ebiten.KeyS]++
+ if keyState[ebiten.KeyS] != 1 {
+ return nil
+ }
+ if p.audioPlayer.IsPlaying() {
+ return p.audioPlayer.Pause()
+ }
+ return p.audioPlayer.Play()
+}
+
+func (p *Player) updateBar() error {
+ if p.audioPlayer == nil {
+ return nil
+ }
+ if !ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
+ mouseButtonState[ebiten.MouseButtonLeft] = 0
+ return nil
+ }
+ mouseButtonState[ebiten.MouseButtonLeft]++
+ if mouseButtonState[ebiten.MouseButtonLeft] != 1 {
+ return nil
+ }
+ x, y := ebiten.CursorPosition()
+ bx, by, bw, bh := playerBarRect()
+ if y < by || by+bh <= y {
+ return nil
+ }
+ if x < bx || bx+bw <= x {
+ return nil
+ }
+ pos := time.Duration(x-bx) * p.total / time.Duration(bw)
+ return p.audioPlayer.Seek(pos)
+}
+
+func (p *Player) close() error {
+ return p.audioPlayer.Close()
+}
+
+func update(screen *ebiten.Image) error {
+ if err := audioContext.Update(); err != nil {
+ return err
+ }
+ if musicPlayer == nil {
+ select {
+ case musicPlayer = <-musicCh:
+ default:
+ }
+ }
+ if seStream == nil {
+ select {
+ case seStream = <-seCh:
+ default:
+ }
+ }
+ if musicPlayer != nil {
+ if err := musicPlayer.updateBar(); err != nil {
+ return err
+ }
+ if err := musicPlayer.updatePlayPause(); err != nil {
+ return err
+ }
+ if err := musicPlayer.updateSE(); err != nil {
+ return err
+ }
+ if err := musicPlayer.updateVolume(); err != nil {
+ return err
+ }
+ }
+
+ op := &ebiten.DrawImageOptions{}
+ x, y, w, h := playerBarRect()
+ op.GeoM.Translate(float64(x), float64(y))
+ screen.DrawImage(playerBarImage, op)
+ currentTimeStr := "00:00"
+ if musicPlayer != nil {
+ c := musicPlayer.audioPlayer.Current()
+
+ // Current Time
+ m := (c / time.Minute) % 100
+ s := (c / time.Second) % 60
+ currentTimeStr = fmt.Sprintf("%02d:%02d", m, s)
+
+ // Bar
+ cw, ch := playerCurrentImage.Size()
+ cx := int(time.Duration(w)*c/musicPlayer.total) + x - cw/2
+ cy := y - (ch-h)/2
+ op := &ebiten.DrawImageOptions{}
+ op.GeoM.Translate(float64(cx), float64(cy))
+ screen.DrawImage(playerCurrentImage, op)
+ }
+
+ msg := fmt.Sprintf(`FPS: %0.2f
+Press S to toggle Play/Pause
+Press P to play SE
+Press Z or X to change volume of the music
+%s`, ebiten.CurrentFPS(), currentTimeStr)
+ if musicPlayer == nil {
+ msg += "\nNow Loading..."
+ }
+ ebitenutil.DebugPrint(screen, msg)
+ return nil
+}
+
+func main() {
+ wavF, err := ebitenutil.OpenFile("_resources/audio/jab.wav")
+ if err != nil {
+ log.Fatal(err)
+ }
+ oggF, err := ebitenutil.OpenFile("_resources/audio/ragtime.ogg")
+ if err != nil {
+ log.Fatal(err)
+ }
+ const sampleRate = 22050
+ const bytesPerSample = 4 // TODO: This should be defined in audio package
+ audioContext, err = audio.NewContext(sampleRate)
+ if err != nil {
+ log.Fatal(err)
+ }
+ go func() {
+ s, err := wav.Decode(audioContext, wavF)
+ if err != nil {
+ log.Fatal(err)
+ return
+ }
+ seCh <- s
+ close(seCh)
+ }()
+ go func() {
+ s, err := vorbis.Decode(audioContext, oggF)
+ if err != nil {
+ log.Fatal(err)
+ return
+ }
+ p, err := audioContext.NewPlayer(s)
+ if err != nil {
+ log.Fatal(err)
+ return
+ }
+ musicCh <- &Player{
+ audioPlayer: p,
+ total: time.Second * time.Duration(s.Size()) / bytesPerSample / sampleRate,
+ }
+ close(musicCh)
+ // TODO: Is this goroutine-safe?
+ p.Play()
+ }()
+ if err := ebiten.Run(update, screenWidth, screenHeight, 2, "Audio (Ebiten Demo)"); err != nil {
+ log.Fatal(err)
+ }
+ if musicPlayer != nil {
+ if err := musicPlayer.close(); err != nil {
+ log.Fatal(err)
+ }
+ }
+}
+
+
+
+
diff --git a/_docs/public/examples/font.content.html b/_docs/public/examples/font.content.html
new file mode 100644
index 000000000..a62e2236c
--- /dev/null
+++ b/_docs/public/examples/font.content.html
@@ -0,0 +1,39 @@
+
+
+
diff --git a/_docs/public/examples/font.html b/_docs/public/examples/font.html
new file mode 100644
index 000000000..972506d60
--- /dev/null
+++ b/_docs/public/examples/font.html
@@ -0,0 +1,134 @@
+
+
+
+
+package main
+
+import (
+ "image"
+ "io/ioutil"
+ "log"
+
+ "github.com/golang/freetype/truetype"
+ "github.com/hajimehoshi/ebiten"
+ "github.com/hajimehoshi/ebiten/ebitenutil"
+ "golang.org/x/image/font"
+ "golang.org/x/image/math/fixed"
+)
+
+const (
+ screenWidth = 640
+ screenHeight = 480
+)
+
+var (
+ textImage *ebiten.Image
+)
+
+var text = []string{
+ "The quick brown fox jumps over the lazy dog.",
+ "",
+ // A head part of a Japanese novel 山月記 (Sangetsuki)
+ // See http://www.aozora.gr.jp/cards/000119/files/624_14544.html.
+ "隴西の李徴は博学才穎、天宝の末年、",
+ "若くして名を虎榜に連ね、",
+ "ついで江南尉に補せられたが、",
+ "性、狷介、自ら恃むところ頗厚く、",
+ "賤吏に甘んずるを潔しとしなかった。",
+}
+
+func parseFont() error {
+ f, err := ebitenutil.OpenFile("_resources/fonts/mplus-1p-regular.ttf")
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ b, err := ioutil.ReadAll(f)
+ if err != nil {
+ return err
+ }
+ tt, err := truetype.Parse(b)
+ if err != nil {
+ return err
+ }
+ w, h := textImage.Size()
+ dst := image.NewRGBA(image.Rect(0, 0, w, h))
+ const size = 24
+ const dpi = 72
+ d := &font.Drawer{
+ Dst: dst,
+ Src: image.White,
+ Face: truetype.NewFace(tt, &truetype.Options{
+ Size: size,
+ DPI: dpi,
+ Hinting: font.HintingFull,
+ }),
+ }
+ dy := size * dpi / 72
+ y := dy
+ for _, s := range text {
+ d.Dot = fixed.P(0, y)
+ d.DrawString(s)
+ y += dy
+ }
+ return textImage.ReplacePixels(dst.Pix)
+}
+
+func update(screen *ebiten.Image) error {
+ if err := screen.DrawImage(textImage, &ebiten.DrawImageOptions{}); err != nil {
+ return err
+ }
+ return nil
+}
+
+func main() {
+ var err error
+ textImage, err = ebiten.NewImage(screenWidth, screenHeight, ebiten.FilterNearest)
+ if err != nil {
+ log.Fatal(err)
+ }
+ if err := parseFont(); err != nil {
+ log.Fatal(err)
+ }
+ if err := ebiten.Run(update, screenWidth, screenHeight, 1, "Font (Ebiten Demo)"); err != nil {
+ log.Fatal(err)
+ }
+}
+
+
+
+
diff --git a/_docs/public/examples/hsv.content.html b/_docs/public/examples/hsv.content.html
new file mode 100644
index 000000000..31af5cbbd
--- /dev/null
+++ b/_docs/public/examples/hsv.content.html
@@ -0,0 +1,39 @@
+
+
+
diff --git a/_docs/public/examples/hsv.html b/_docs/public/examples/hsv.html
new file mode 100644
index 000000000..9635ab0a6
--- /dev/null
+++ b/_docs/public/examples/hsv.html
@@ -0,0 +1,132 @@
+
+
+
+
+package main
+
+import (
+ "fmt"
+ _ "image/jpeg"
+ "log"
+ "math"
+
+ "github.com/hajimehoshi/ebiten"
+ "github.com/hajimehoshi/ebiten/ebitenutil"
+)
+
+const (
+ screenWidth = 320
+ screenHeight = 240
+)
+
+var (
+ hueInt = 0
+ saturationInt = 128
+ valueInt = 128
+ gophersImage *ebiten.Image
+)
+
+func clamp(v, min, max int) int {
+ if min > max {
+ panic("min must <= max")
+ }
+ if v < min {
+ return min
+ }
+ if max < v {
+ return max
+ }
+ return v
+}
+
+func update(screen *ebiten.Image) error {
+ if ebiten.IsKeyPressed(ebiten.KeyQ) {
+ hueInt--
+ }
+ if ebiten.IsKeyPressed(ebiten.KeyW) {
+ hueInt++
+ }
+ if ebiten.IsKeyPressed(ebiten.KeyA) {
+ saturationInt--
+ }
+ if ebiten.IsKeyPressed(ebiten.KeyS) {
+ saturationInt++
+ }
+ if ebiten.IsKeyPressed(ebiten.KeyZ) {
+ valueInt--
+ }
+ if ebiten.IsKeyPressed(ebiten.KeyX) {
+ valueInt++
+ }
+ hueInt = clamp(hueInt, -256, 256)
+ saturationInt = clamp(saturationInt, 0, 256)
+ valueInt = clamp(valueInt, 0, 256)
+
+ w, h := gophersImage.Size()
+ op := &ebiten.DrawImageOptions{}
+ op.GeoM.Translate(float64(screenWidth-w)/2, float64(screenHeight-h)/2)
+ hue := float64(hueInt) * 2 * math.Pi / 128
+ saturation := float64(saturationInt) / 128
+ value := float64(valueInt) / 128
+ op.ColorM.ChangeHSV(hue, saturation, value)
+ if err := screen.DrawImage(gophersImage, op); err != nil {
+ return err
+ }
+
+ msg := fmt.Sprintf(`Hue: %0.2f [Q][W]
+Saturation: %0.2f [A][S]
+Value: %0.2f [Z][X]`, hue, saturation, value)
+ if err := ebitenutil.DebugPrint(screen, msg); err != nil {
+ return err
+ }
+ return nil
+}
+
+func main() {
+ var err error
+ gophersImage, _, err = ebitenutil.NewImageFromFile("_resources/images/gophers.jpg", ebiten.FilterNearest)
+ if err != nil {
+ log.Fatal(err)
+ }
+ if err := ebiten.Run(update, screenWidth, screenHeight, 2, "HSV (Ebiten Demo)"); err != nil {
+ log.Fatal(err)
+ }
+}
+
+
+
+
diff --git a/_docs/public/examples/masking.content.html b/_docs/public/examples/masking.content.html
new file mode 100644
index 000000000..542cc846a
--- /dev/null
+++ b/_docs/public/examples/masking.content.html
@@ -0,0 +1,39 @@
+
+
+
diff --git a/_docs/public/examples/masking.html b/_docs/public/examples/masking.html
new file mode 100644
index 000000000..b49d4644e
--- /dev/null
+++ b/_docs/public/examples/masking.html
@@ -0,0 +1,169 @@
+
+
+
+
+package main
+
+import (
+ "image"
+ "image/color"
+ _ "image/jpeg"
+ "log"
+ "math"
+
+ "github.com/hajimehoshi/ebiten"
+ "github.com/hajimehoshi/ebiten/ebitenutil"
+)
+
+const (
+ screenWidth = 320
+ screenHeight = 240
+)
+
+var (
+ gophersImage *ebiten.Image
+ fiveyearsImage *ebiten.Image
+ maskImage *ebiten.Image
+ spotLightImage *ebiten.Image
+ spotLightX = 0
+ spotLightY = 0
+ spotLightVX = 1
+ spotLightVY = 1
+)
+
+func update(screen *ebiten.Image) error {
+ spotLightX += spotLightVX
+ spotLightY += spotLightVY
+ if spotLightX < 0 {
+ spotLightX = -spotLightX
+ spotLightVX = -spotLightVX
+ }
+ if spotLightY < 0 {
+ spotLightY = -spotLightY
+ spotLightVY = -spotLightVY
+ }
+ w, h := spotLightImage.Size()
+ maxX, maxY := screenWidth-w, screenHeight-h
+ if maxX < spotLightX {
+ spotLightX = -spotLightX + 2*maxX
+ spotLightVX = -spotLightVX
+ }
+ if maxY < spotLightY {
+ spotLightY = -spotLightY + 2*maxY
+ spotLightVY = -spotLightVY
+ }
+
+ if err := maskImage.Clear(); err != nil {
+ return err
+ }
+
+ op := &ebiten.DrawImageOptions{}
+ op.GeoM.Translate(float64(spotLightX), float64(spotLightY))
+ if err := maskImage.DrawImage(spotLightImage, op); err != nil {
+ return err
+ }
+
+ op = &ebiten.DrawImageOptions{}
+ op.CompositeMode = ebiten.CompositeModeSourceOut
+ if err := maskImage.DrawImage(fiveyearsImage, op); err != nil {
+ return err
+ }
+
+ if err := screen.Fill(color.RGBA{0x00, 0x00, 0x80, 0xff}); err != nil {
+ return err
+ }
+ if err := screen.DrawImage(gophersImage, &ebiten.DrawImageOptions{}); err != nil {
+ return err
+ }
+ if err := screen.DrawImage(maskImage, &ebiten.DrawImageOptions{}); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func max(a, b int) int {
+ if a < b {
+ return b
+ }
+ return a
+}
+
+func min(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}
+
+func main() {
+ var err error
+ gophersImage, _, err = ebitenutil.NewImageFromFile("_resources/images/gophers.jpg", ebiten.FilterNearest)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fiveyearsImage, _, err = ebitenutil.NewImageFromFile("_resources/images/fiveyears.jpg", ebiten.FilterNearest)
+ if err != nil {
+ log.Fatal(err)
+ }
+ maskImage, err = ebiten.NewImage(screenWidth, screenHeight, ebiten.FilterNearest)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ as := image.Point{128, 128}
+ a := image.NewAlpha(image.Rectangle{image.ZP, as})
+ for j := 0; j < as.Y; j++ {
+ for i := 0; i < as.X; i++ {
+ r := as.X / 2
+ d := math.Sqrt(float64((i-r)*(i-r) + (j-r)*(j-r)))
+ b := uint8(max(0, min(0xff, 3*0xff-int(d*3*0xff)/r)))
+ a.SetAlpha(i, j, color.Alpha{b})
+ }
+ }
+ spotLightImage, err = ebiten.NewImageFromImage(a, ebiten.FilterNearest)
+ if err != nil {
+ log.Fatal(err)
+ }
+ if err := ebiten.Run(update, screenWidth, screenHeight, 2, "Masking (Ebiten Demo)"); err != nil {
+ log.Fatal(err)
+ }
+}
+
+
+
+
diff --git a/_docs/public/examples/piano.content.html b/_docs/public/examples/piano.content.html
new file mode 100644
index 000000000..1e7d154d8
--- /dev/null
+++ b/_docs/public/examples/piano.content.html
@@ -0,0 +1,39 @@
+
+
+
diff --git a/_docs/public/examples/piano.html b/_docs/public/examples/piano.html
new file mode 100644
index 000000000..7790cfaec
--- /dev/null
+++ b/_docs/public/examples/piano.html
@@ -0,0 +1,261 @@
+
+
+
+
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "image/color"
+ "log"
+ "math"
+
+ "github.com/hajimehoshi/ebiten"
+ "github.com/hajimehoshi/ebiten/ebitenutil"
+ "github.com/hajimehoshi/ebiten/examples/common"
+ "github.com/hajimehoshi/ebiten/exp/audio"
+)
+
+const (
+ screenWidth = 320
+ screenHeight = 240
+ sampleRate = 44100
+)
+
+var audioContext *audio.Context
+
+func init() {
+ var err error
+ audioContext, err = audio.NewContext(sampleRate)
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+var pcm = make([]float64, 4*sampleRate)
+
+const baseFreq = 220
+
+func init() {
+ s := float64(sampleRate)
+ amp := []float64{1.0, 0.8, 0.6, 0.4, 0.2}
+ x := []float64{4.0, 2.0, 1.0, 0.5, 0.25}
+ for i := 0; i < len(pcm); i++ {
+ v := 0.0
+ twoPiF := 2.0 * math.Pi * baseFreq
+ for j := 0; j < len(amp); j++ {
+ a := amp[j] * math.Exp(-5*float64(i)/(x[j]*s))
+ v += a * math.Sin(float64(i)*twoPiF*float64(j+1)/s)
+ }
+ pcm[i] = v / 5.0
+ }
+}
+
+var (
+ noteCache = map[int][]byte{}
+)
+
+func toBytes(l, r []int16) []byte {
+ if len(l) != len(r) {
+ panic("len(l) must equal to len(r)")
+ }
+ b := make([]byte, len(l)*4)
+ for i, _ := range l {
+ b[4*i] = byte(l[i])
+ b[4*i+1] = byte(l[i] >> 8)
+ b[4*i+2] = byte(r[i])
+ b[4*i+3] = byte(r[i] >> 8)
+ }
+ return b
+}
+
+type stream struct {
+ *bytes.Reader
+}
+
+func (s *stream) Close() error {
+ s.Reader = nil
+ return nil
+}
+
+func addNote(freq float64, vol float64) error {
+ f := int(freq)
+ if n, ok := noteCache[f]; ok {
+ p, err := audioContext.NewPlayer(&stream{bytes.NewReader(n)})
+ if err != nil {
+ return err
+ }
+ p.Play()
+ return nil
+ }
+ length := len(pcm) * baseFreq / f
+ l := make([]int16, length)
+ r := make([]int16, length)
+ j := 0
+ jj := 0
+ for i := 0; i < len(l); i++ {
+ p := pcm[j]
+ l[i] = int16(p * vol * math.MaxInt16)
+ r[i] = l[i]
+ jj += f
+ j = jj / baseFreq
+ }
+ n := toBytes(l, r)
+ noteCache[f] = n
+ p, err := audioContext.NewPlayer(&stream{bytes.NewReader(n)})
+ if err != nil {
+ return err
+ }
+ p.Play()
+ return nil
+}
+
+var keys = []ebiten.Key{
+ ebiten.KeyQ,
+ ebiten.KeyA,
+ ebiten.KeyW,
+ ebiten.KeyS,
+ ebiten.KeyD,
+ ebiten.KeyR,
+ ebiten.KeyF,
+ ebiten.KeyT,
+ ebiten.KeyG,
+ ebiten.KeyH,
+ ebiten.KeyU,
+ ebiten.KeyJ,
+ ebiten.KeyI,
+ ebiten.KeyK,
+ ebiten.KeyO,
+ ebiten.KeyL,
+}
+
+var keyStates = map[ebiten.Key]int{}
+
+func init() {
+ for _, key := range keys {
+ keyStates[key] = 0
+ }
+}
+
+func updateInput() {
+ for _, key := range keys {
+ if !ebiten.IsKeyPressed(key) {
+ keyStates[key] = 0
+ continue
+ }
+ keyStates[key]++
+ }
+}
+
+var (
+ imagePiano *ebiten.Image
+)
+
+func init() {
+ var err error
+ imageEmpty, err := ebiten.NewImage(16, 16, ebiten.FilterNearest)
+ if err != nil {
+ panic(err)
+ }
+ imageEmpty.Fill(color.White)
+ imagePiano, err = ebiten.NewImage(screenWidth, screenHeight, ebiten.FilterNearest)
+ if err != nil {
+ panic(err)
+ }
+ whiteKeys := []string{"A", "S", "D", "F", "G", "H", "J", "K", "L"}
+ width := 24
+ y := 48
+ for i, k := range whiteKeys {
+ x := i*width + 36
+ height := 112
+ op := &ebiten.DrawImageOptions{}
+ w, h := imageEmpty.Size()
+ op.GeoM.Scale(float64(width-1)/float64(w), float64(height)/float64(h))
+ op.GeoM.Translate(float64(x), float64(y))
+ op.ColorM.Scale(1, 1, 1, 1)
+ imagePiano.DrawImage(imageEmpty, op)
+ common.ArcadeFont.DrawText(imagePiano, k, x+8, y+height-16, 1, color.Black)
+ }
+
+ blackKeys := []string{"Q", "W", "", "R", "T", "", "U", "I", "O"}
+ for i, k := range blackKeys {
+ if k == "" {
+ continue
+ }
+ x := i*width + 24
+ height := 64
+ op := &ebiten.DrawImageOptions{}
+ w, h := imageEmpty.Size()
+ op.GeoM.Scale(float64(width-1)/float64(w), float64(height)/float64(h))
+ op.GeoM.Translate(float64(x), float64(y))
+ op.ColorM.Scale(0, 0, 0, 1)
+ imagePiano.DrawImage(imageEmpty, op)
+ common.ArcadeFont.DrawText(imagePiano, k, x+8, y+height-16, 1, color.White)
+ }
+}
+
+func update(screen *ebiten.Image) error {
+ if err := audioContext.Update(); err != nil {
+ return err
+ }
+ updateInput()
+ for i, key := range keys {
+ if keyStates[key] != 1 {
+ continue
+ }
+ if err := addNote(220*math.Exp2(float64(i-1)/12.0), 1.0); err != nil {
+ return err
+ }
+ }
+
+ screen.Fill(color.RGBA{0x80, 0x80, 0xc0, 0xff})
+ screen.DrawImage(imagePiano, nil)
+
+ ebitenutil.DebugPrint(screen, fmt.Sprintf("FPS: %0.2f", ebiten.CurrentFPS()))
+ return nil
+}
+
+func main() {
+ if err := ebiten.Run(update, screenWidth, screenHeight, 2, "Piano (Ebiten Demo)"); err != nil {
+ log.Fatal(err)
+ }
+}
+
+
+
+
diff --git a/_docs/public/index.html b/_docs/public/index.html
index b7d95fefd..c01df519a 100644
--- a/_docs/public/index.html
+++ b/_docs/public/index.html
@@ -63,7 +63,7 @@ pre {