pixel-examples/community/amidakuji/path.go

282 lines
8.7 KiB
Go

package main
import (
"math"
"sync"
gg "github.com/faiface/pixel-examples/community/amidakuji/glossary"
"github.com/faiface/pixel"
"github.com/faiface/pixel/imdraw"
)
// Path is for animating a path to the prize in a ladder.
type Path struct {
imd *imdraw.IMDraw // shared variable
mutex sync.Mutex // synchronize
//
roads []pixel.Vec // A list of vectors - each vector for a position where a road starts.
prize *int
tip *pixel.Vec
tipDir pixel.Vec
iroad int
//
watchAnim gg.DtWatch // When it started to animate.
timeLimitAnimInSec float64
animateInTime bool
isAnimating bool
// -----------------------------------------------------------
// exported callbacks(listeners) regarding animation
// callback on reaching the prize level of a ladder.
OnFinishedAnimation func()
// callback when the animating 'tip' passes a point of a road.
// pt: a point(road) just passed.
// dir: ...
// dir is a normalized vector. (pixel.ZV) is passed if the direction can't be found.
// dir can be different depending on how fast this Path is updated.
OnPassedEachPoint func(pt pixel.Vec, dir pixel.Vec)
}
// NewPath is a contructor.
func NewPath(_roads []pixel.Vec, _prize *int) *Path {
newTip := func() *pixel.Vec {
if _roads != nil {
if len(_roads) > 0 {
v := _roads[0]
return &v
}
}
return nil
}
return &Path{
roads: _roads,
prize: _prize,
tip: newTip(),
tipDir: pixel.ZV,
}
}
// NewPathEmpty is a contructor.
func NewPathEmpty() *Path {
return &Path{}
}
// -------------------------------------------------------------------------
// Important methods
// Draw guarantees the thread safety, though it's not a necessary condition.
// It is quite dangerous to access this struct's member (imdraw) directly from outside these methods.
func (path *Path) Draw(t pixel.Target) {
path.mutex.Lock()
defer path.mutex.Unlock()
if path.imd == nil { // isInvisible set to true.
return // An empty image is drawn.
}
path.imd.Draw(t)
}
// Update animates a path. A path is drawn on an imdraw.
func (path *Path) Update(color pixel.RGBA) {
var (
iroad = len(path.roads) - 1
from = path.roads[len(path.roads)-1]
to = path.roads[len(path.roads)-1]
dir = pixel.ZV
)
if path.isAnimating {
// get where it is abstract // get a scalar
dt := path.watchAnim.DtSinceStart()
const distPerSec = 500
scalarProgress := dt * distPerSec
// log.Println(dt, path.Len(), scalarProgress) //
if path.animateInTime { // overwrite scalarProgress
fromLot := pixel.V(0, 0)
toPrize := pixel.V(path.Len(), 0)
percentagePointPerSec := 1 / path.timeLimitAnimInSec
scalarProgress = pixel.Lerp(fromLot, toPrize, dt*percentagePointPerSec).X
// log.Println(dt, path.Len(), scalarProgress, dt*percentagePointPerSec) //
}
// get where it is concrete // turn a scalar into a set of vectors
iroad, from, to, dir = path.FindRoadByDist(scalarProgress)
if iroad > path.iroad {
if path.OnPassedEachPoint != nil {
go path.OnPassedEachPoint(from, dir)
}
}
path.iroad = iroad
// log.Println(iroad, len(path.roads), iroad == len(path.roads)-1, path.isAnimating) //
if iroad >= len(path.roads)-1 { // the end
path.isAnimating = false
// log.Println(path.Len(), dt) //
if path.OnFinishedAnimation != nil {
go path.OnFinishedAnimation()
} // callback
}
}
// lock before imdraw update
path.mutex.Lock()
defer path.mutex.Unlock()
// imdraw (a state machine)
if path.imd == nil { // lazy creation
path.imd = imdraw.New(nil)
}
imd := path.imd
imd.Clear()
// draw path
imd.Color = color
imd.EndShape = imdraw.RoundEndShape
for i := 0; i < iroad; i++ {
imd.Push(path.roads[i], path.roads[i+1])
imd.Line(9)
}
imd.Push(from, to)
imd.Line(9)
// save where the tip is
path.tip = &to
}
// -------------------------------------------------------------------------
// Read only methods
// IsAnimating determines whether this Path is about to be updated or not.
// Pass lock by value warning from (path Path) should be ignored,
// because a Path here is just passed as a read only argument.
func (path Path) IsAnimating() bool {
return path.isAnimating
}
// GetPrize is just an average getter.
// It returns -1 if the receiver is not initialized with that member(prize).
// Pass lock by value warning from (path Path) should be ignored,
// because a Path here is just passed as a read only argument.
func (path Path) GetPrize() int {
if path.prize == nil {
return -1
}
return *path.prize
}
// PosTip returns a vector that tells you how far the animating path currently has reached.
// A non-ptr Path as a read only argument passes lock by value within itself but that seems totally fine.
func (path Path) PosTip() (v pixel.Vec) {
return *path.tip
}
// Len returns the total length of all roads.
// A non-ptr Path as a read only argument passes lock by value within itself but that seems totally fine.
func (path Path) Len() (sum float64) {
for i := 0; i < len(path.roads)-1; i++ {
sum += math.Abs(path.roads[i].Sub(path.roads[i+1]).Len())
}
return
}
// FindRoadByDist converts a scalar into a set of vectors.
// A non-ptr Path as a read only argument passes lock by value within itself but that seems totally fine.
//
// Returns
// iroad: The index of a road found.
// road: The vector representation of a road found. A road is a line from pt A to B, and that vector points to where pt A is.
// pos: A position(point) found which is in the middle of that found road(line).
// dirVecNormalized: A direction as a normalized vector. This vector always has a length of 1.
func (path Path) FindRoadByDist(distProgress float64) (iroad int, road pixel.Vec, pos pixel.Vec, dirVecNormalized pixel.Vec) {
lengthOfTraveledRoads := float64(0.0)
for iroad = 0; iroad < len(path.roads)-1; iroad++ {
var lengthOfThisRoad float64
iroadNext := iroad + 1
lengthOfThisRoad = math.Abs(path.roads[iroad].Sub(path.roads[iroadNext]).Len())
lengthOfTraveledRoads += lengthOfThisRoad
// For loop breaker: distProgress is somewhere between the total length of a path.
if lengthOfTraveledRoads > distProgress {
scalar := lengthOfThisRoad - (lengthOfTraveledRoads - distProgress)
if path.roads[iroad].Y == path.roads[iroadNext].Y &&
path.roads[iroad].X < path.roads[iroadNext].X { // to the bottom (east)
pos = path.roads[iroad]
pos.X += scalar
dirVecNormalized = pixel.V(1, 0)
} else if path.roads[iroad].X == path.roads[iroadNext].X &&
path.roads[iroad].Y > path.roads[iroadNext].Y { // to the left (south)
pos = path.roads[iroad]
pos.Y -= scalar
dirVecNormalized = pixel.V(0, -1)
} else if path.roads[iroad].X == path.roads[iroadNext].X &&
path.roads[iroad].Y < path.roads[iroadNext].Y { // to the right (north)
pos = path.roads[iroad]
pos.Y += scalar
dirVecNormalized = pixel.V(0, 1)
} else if path.roads[iroad].Y == path.roads[iroadNext].Y &&
path.roads[iroad].X > path.roads[iroadNext].X { // to the top (west)
// Placed at the end of an elif statement,
// since this case is of no possibility unless the path finding is going reverse.
pos = path.roads[iroad]
pos.X -= scalar
dirVecNormalized = pixel.V(-1, 0)
} else {
panic("unhandled exception: it may be a diagonal bridge")
} // elif
return iroad, path.roads[iroad], pos, dirVecNormalized
} // if - for loop breaker
} // for
// coming down to here means that the case is (road == pos)
from := iroad - 1
to := iroad
if iroad == 0 {
from = iroad
to = iroad + 1
}
if from < 0 || to >= len(path.roads) {
dirVecNormalized = pixel.ZV
} else {
dirVecNormalized = gg.Direction(path.roads[from], path.roads[to])
}
return iroad, path.roads[iroad], path.roads[iroad], dirVecNormalized
}
// -------------------------------------------------------------------------
// Methods that write to itself
// Animate a path.
func (path *Path) Animate() {
path.watchAnim.Start()
path.animateInTime = false
path.isAnimating = true
}
// AnimateInTime animates a path in given time.
func (path *Path) AnimateInTime(sec float64) {
path.watchAnim.Start()
path.timeLimitAnimInSec = sec
path.animateInTime = true
path.isAnimating = true
}
// Pause a path's clock.
func (path *Path) Pause() {
if path.watchAnim.IsStarted() {
path.watchAnim.Dt()
}
}
// Resume after pause.
func (path *Path) Resume() {
if path.watchAnim.IsStarted() {
started := path.watchAnim.GetTimeStarted()
dtPause := path.watchAnim.DtNano()
path.watchAnim.SetTimeStarted(started.Add(dtPause))
// log.Println(dtPause, started, path.watchAnim.GetTimeStarted()) //
}
}