package main import ( "image" "image/color" "log" "os" "math" "github.com/disintegration/imaging" ) type Mario struct { position image.Point dir image.Point images map[string]image.Image updown string a float64 b float64 angle float64 } func loadMario(file string) image.Image { reader, err := os.Open(file) if err != nil { log.Fatal(err) } rawMario, _, err := image.Decode(reader) if err != nil { log.Fatal(err) } mario := imaging.Resize(rawMario, 16, 16, imaging.Lanczos) return mario } func initialMap() map[string]image.Image { imageMap := make(map[string]image.Image) imageMap["marioUp"] = loadMario("marioUp.png") imageMap["marioDown"] = loadMario("marioDown.png") return imageMap } // initializes the struct for the an play animation function, this could all be dumped into function that's wrapping go routine if I wanted // this assumes mario context is up func (a *Animation) animateMario() { defer a.updateMarioPosition() a.ctx.SetColor(color.Black) a.ctx.Clear() if a.mario.dir.X == 1 { a.ctx.DrawImageAnchored(a.mario.images[a.mario.updown], a.mario.position.X, a.mario.position.Y, 0.5, 0.5) } else { a.ctx.DrawImageAnchored(imaging.FlipH(a.mario.images[a.mario.updown]), a.mario.position.X, a.mario.position.Y, 0.5, 0.5) } } func (a *Animation) updateMarioPosition() { centerX := a.ctx.Width() / 2 centerY := a.ctx.Height() / 2 // Determine sprite size (use current updown image if available) var sprite image.Image if img, ok := a.mario.images[a.mario.updown]; ok && img != nil { sprite = img } else { for _, im := range a.mario.images { sprite = im break } } // default half sizes if sprite missing halfW, halfH := 8, 8 if sprite != nil { halfW = sprite.Bounds().Dx() / 2 halfH = sprite.Bounds().Dy() / 2 } // allowable center range so the sprite stays fully on the panel minCenterX := halfW maxCenterX := a.ctx.Width() - 1 - halfW minCenterY := halfH maxCenterY := a.ctx.Height() - 1 - halfH // If the sprite is larger than the panel in a dimension, collapse // the allowed center range to the panel center so we don't get // immediate collisions every frame which makes Mario flash. if maxCenterX < minCenterX { minCenterX = centerX maxCenterX = centerX } if maxCenterY < minCenterY { minCenterY = centerY maxCenterY = centerY } // advance angle for this frame delta := 0.05 t := a.mario.angle tNext := t + delta if tNext >= 2*math.Pi { tNext -= 2 * math.Pi } // compute next candidate center from the next parametric position marioX := int(math.Round(a.mario.a*math.Cos(tNext))) + centerX marioY := int(math.Round(a.mario.b*math.Sin(tNext))) + centerY // clamp into allowed center ranges so sprite stays fully on panel if marioX < minCenterX { marioX = minCenterX } if marioX > maxCenterX { marioX = maxCenterX } if marioY < minCenterY { marioY = minCenterY } if marioY > maxCenterY { marioY = maxCenterY } // commit position and advance angle a.mario.position.X = marioX a.mario.position.Y = marioY a.mario.angle = tNext // Direction logic (based on the motion between t and tNext) // approximate direction by comparing positions (preferable to trig sign when clamped) prevX := int(math.Round(a.mario.a*math.Cos(t))) + centerX if prevX == marioX { // if we didn't move horizontally, keep previous direction // (this prevents flicker when clamped) } else if marioX < prevX { a.mario.dir.X = -1 } else { a.mario.dir.X = 1 } // up/down based on vertical movement prevY := int(math.Round(a.mario.b*math.Sin(t))) + centerY if prevY == marioY { // keep previous up/down state } else if marioY < prevY { a.mario.updown = "marioUp" } else { a.mario.updown = "marioDown" } }