From e1e213cc10d6125e1a2271a16083d7d6b0df6afa Mon Sep 17 00:00:00 2001 From: Huw Griffiths Date: Thu, 25 Mar 2021 00:27:12 +1100 Subject: [PATCH] Add a new Touch example (#1547) --- examples/touch/main.go | 255 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 examples/touch/main.go diff --git a/examples/touch/main.go b/examples/touch/main.go new file mode 100644 index 000000000..ac7428a26 --- /dev/null +++ b/examples/touch/main.go @@ -0,0 +1,255 @@ +// Copyright 2021 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. + +// +build example + +package main + +import ( + "bytes" + "image" + _ "image/jpeg" + "log" + "math" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/ebitenutil" + "github.com/hajimehoshi/ebiten/v2/examples/resources/images" + "github.com/hajimehoshi/ebiten/v2/inpututil" +) + +// distance between points a and b. +func distance(xa, ya, xb, yb int) float64 { + x := math.Abs(float64(xa - xb)) + y := math.Abs(float64(ya - yb)) + return math.Sqrt(x*x + y*y) +} + +const ( + screenWidth = 640 + screenHeight = 480 +) + +var ( + gophersImage *ebiten.Image +) + +type touch struct { + originX, originY int + currX, currY int + duration int + wasPinch, isPan bool +} + +type pinch struct { + id1, id2 ebiten.TouchID + originH float64 + prevH float64 +} + +type pan struct { + id ebiten.TouchID + + prevX, prevY int + originX, originY int +} + +type tap struct { + X, Y int +} + +type Game struct { + x, y float64 + zoom float64 + + touches map[ebiten.TouchID]*touch + pinch *pinch + pan *pan + taps []tap +} + +func (g *Game) Update() error { + // Clear the previous frame's taps. + g.taps = g.taps[:0] + + // What touches have just ended? + for id, t := range g.touches { + if inpututil.IsTouchJustReleased(id) { + if g.pinch != nil && (id == g.pinch.id1 || id == g.pinch.id2) { + g.pinch = nil + } + if g.pan != nil && id == g.pan.id { + g.pan = nil + } + + // If this one has not been touched long (30 frames can be assumed + // to be 500ms), or moved far, then it's a tap. + diff := distance(t.originX, t.originY, t.currX, t.currY) + if !t.wasPinch && !t.isPan && (t.duration <= 30 || diff < 2) { + g.taps = append(g.taps, tap{ + X: t.currX, + Y: t.currY, + }) + } + + delete(g.touches, id) + } + } + + // What touches are new in this frame? + for _, id := range inpututil.JustPressedTouchIDs() { + x, y := ebiten.TouchPosition(id) + g.touches[ebiten.TouchID(id)] = &touch{ + originX: x, originY: y, + currX: x, currY: y, + } + } + + // Update the current position and durations of any touches that have + // neither begun nor ended in this frame. + for _, id := range ebiten.TouchIDs() { + t := g.touches[id] + t.duration = inpututil.TouchPressDuration(id) + t.currX, t.currY = ebiten.TouchPosition(id) + } + + // Interpret the raw touch data that's been collected into g.touches into + // gestures like two-finger pinch or single-finger pan. + switch len(g.touches) { + case 2: + // Potentially the user is making a pinch gesture with two fingers. + // If the diff between their origins is different to the diff between + // their currents and if these two are not already a pinch, then this is + // a new pinch! + id1, id2 := ebiten.TouchIDs()[0], ebiten.TouchIDs()[1] + t1, t2 := g.touches[id1], g.touches[id2] + originDiff := distance(t1.originX, t1.originY, t2.originX, t2.originY) + currDiff := distance(t1.currX, t1.currY, t2.currX, t2.currY) + if g.pinch == nil && g.pan == nil && math.Abs(originDiff-currDiff) > 3 { + t1.wasPinch = true + t2.wasPinch = true + g.pinch = &pinch{ + id1: id1, + id2: id2, + originH: originDiff, + prevH: originDiff, + } + } + case 1: + // Potentially this is a new pan. + id := ebiten.TouchIDs()[0] + t := g.touches[id] + if !t.wasPinch && g.pan == nil && g.pinch == nil { + diff := math.Abs(distance(t.originX, t.originY, t.currX, t.currY)) + if diff > 1 { + t.isPan = true + g.pan = &pan{ + id: id, + originX: t.originX, + originY: t.originY, + prevX: t.originX, + prevY: t.originY, + } + } + } + } + + // Copy any active pinch gesture's movement to the Game's zoom. + if g.pinch != nil { + x1, y1 := ebiten.TouchPosition(g.pinch.id1) + x2, y2 := ebiten.TouchPosition(g.pinch.id2) + curr := distance(x1, y1, x2, y2) + delta := curr - g.pinch.prevH + g.pinch.prevH = curr + + g.zoom += (delta / 100) * g.zoom + if g.zoom < 0.25 { + g.zoom = 0.25 + } else if g.zoom > 10 { + g.zoom = 10 + } + } + + // Copy any active pan gesture's movement to the Game's x and y pan values. + if g.pan != nil { + currX, currY := ebiten.TouchPosition(g.pan.id) + deltaX, deltaY := currX-g.pan.prevX, currY-g.pan.prevY + + g.pan.prevX, g.pan.prevY = currX, currY + + g.x += float64(deltaX) + g.y += float64(deltaY) + } + + // If the user has tapped, then reset the Game's pan and zoom. + if len(g.taps) > 0 { + g.x = screenWidth / 2 + g.y = screenHeight / 2 + g.zoom = 1.0 + } + return nil +} + +func (g *Game) Draw(screen *ebiten.Image) { + op := &ebiten.DrawImageOptions{} + + // Apply zoom. + op.GeoM.Scale(g.zoom, g.zoom) + + // Apply pan. + op.GeoM.Translate(g.x, g.y) + + // Center the image (corrected by the current zoom). + w, h := gophersImage.Size() + op.GeoM.Translate(float64(-w)/2*g.zoom, float64(-h)/2*g.zoom) + + screen.DrawImage(gophersImage, op) + + ebitenutil.DebugPrint(screen, "Use a two finger pinch to zoom, swipe with one finger to pan, or tap to reset the view") +} + +func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { + return screenWidth, screenHeight +} + +func main() { + // Decode image from a byte slice instead of a file so that + // this example works in any working directory. + // If you want to use a file, there are some options: + // 1) Use os.Open and pass the file to the image decoder. + // This is a very regular way, but doesn't work on browsers. + // 2) Use ebitenutil.OpenFile and pass the file to the image decoder. + // This works even on browsers. + // 3) Use ebitenutil.NewImageFromFile to create an ebiten.Image directly from a file. + // This also works on browsers. + img, _, err := image.Decode(bytes.NewReader(images.Gophers_jpg)) + if err != nil { + log.Fatal(err) + } + gophersImage = ebiten.NewImageFromImage(img) + + g := &Game{ + x: screenWidth / 2, + y: screenHeight / 2, + zoom: 1.0, + + touches: map[ebiten.TouchID]*touch{}, + } + + ebiten.SetWindowSize(screenWidth, screenHeight) + ebiten.SetWindowTitle("Touch (Ebiten Demo)") + if err := ebiten.RunGame(g); err != nil { + log.Fatal(err) + } +}