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 } // parametric angle before any reflection t := a.mario.angle // compute candidate center position on the ellipse marioX := int(math.Round(a.mario.a*math.Cos(t))) + centerX marioY := int(math.Round(a.mario.b*math.Sin(t))) + centerY // detect collisions against allowed center ranges collidedX := marioX < minCenterX || marioX > maxCenterX collidedY := marioY < minCenterY || marioY > maxCenterY // If allowed range collapsed to the center, don't reflect on that axis // because reflection would keep flipping every frame. Treat that as // non-colliding for reflection purposes (we'll clamp position later). if minCenterX == maxCenterX { collidedX = false } if minCenterY == maxCenterY { collidedY = false } // Reflect the parametric angle correctly: // - For horizontal collision we want cos(t_new) = -cos(t) => t_new = Pi - t // - For vertical collision we want sin(t_new) = -sin(t) => t_new = -t if collidedX { t = math.Pi - t } if collidedY { t = -t } // normalize angle into [0, 2π) for t < 0 { t += 2 * math.Pi } for t >= 2*math.Pi { t -= 2 * math.Pi } a.mario.angle = t // recompute position from possibly-updated angle so sprite is inside bounds marioX = int(math.Round(a.mario.a*math.Cos(t))) + centerX marioY = int(math.Round(a.mario.b*math.Sin(t))) + centerY // clamp as a safety net if marioX < minCenterX { marioX = minCenterX } if marioX > maxCenterX { marioX = maxCenterX } if marioY < minCenterY { marioY = minCenterY } if marioY > maxCenterY { marioY = maxCenterY } a.mario.position.X = marioX a.mario.position.Y = marioY // advance angle for next frame a.mario.angle += 0.05 if a.mario.angle >= 2*math.Pi { a.mario.angle -= 2 * math.Pi } // Direction logic (based on param t used for the current frame) if math.Sin(t) > 0 { a.mario.dir.X = -1 // moving left } else { a.mario.dir.X = 1 // moving right } if math.Cos(t) > 0 { a.mario.updown = "marioDown" // moving downward } else { a.mario.updown = "marioUp" // moving upward } }