diff --git a/physics.go b/physics.go new file mode 100644 index 0000000..3542ec6 --- /dev/null +++ b/physics.go @@ -0,0 +1,77 @@ +// Simple physics projectile motion. +// +// Example usage: +// +// // Run once to initialize. +// projectile := NewProjectile(FPS(60), Point{6.0, 100.0, 0.0}, Vector{2.0, 0.0, 0.0}, Vector{2.0, -9.81, 0.0}) +// +// // Update on every frame. +// someUpdateLoop(func() { +// pos := projectile.Update() +// }) +// +// For background on projectile motion see: +// https://en.wikipedia.org/wiki/Projectile_motion +package harmonica + +// Projectile is the representation of a projectile that has a position on a +// plane and an acceleration and velocity +type Projectile struct { + pos Point + vel Vector + acc Vector + deltaTime float64 +} + +// Point is a representation of a point which contains the X, Y, Z coordinates +// of the point on a plane. +type Point struct { + X, Y, Z float64 +} + +// Vector is a representation of a vector which carries a magnitude and a +// direction. We represent the vector as a point from the origin (0, 0) where +// the magnitude is the euclidean distance from the origin and the direction is +// the direction to the point from the origin. +type Vector struct { + X, Y, Z float64 +} + +// Gravity is a utility vector that represents gravity in 2D and 3D contexts, +// assuming that your coordinate plane looks like in 2D or 3D: +// +// -y -y ±z +// │ │ / +// │ │/ +// └───── ±x └───── ±x +// +// Note: Gravity usually is -9.81m/s however we use a positive value because we +// assume the origin is placed at the top-left corner and that downward is the +// positive y direction (only if using this utility variable). +// Otherwise, you can place the origin wherever you'd like. +var Gravity = Vector{0, 9.81, 0} + +// NewProjectile accepts a frame rate, and initial values for position, velocity, and acceleration and +// returns a new projectile. +func NewProjectile(deltaTime float64, initialPosition Point, initialVelocity, initalAcceleration Vector) Projectile { + return Projectile{ + pos: initialPosition, + vel: initialVelocity, + acc: initalAcceleration, + deltaTime: deltaTime, + } +} + +// Update updates the position and velocity values for the given projectile. +// Call this after calling NewProjectile to update values. +func (p *Projectile) Update() Point { + p.pos.X += (p.vel.X * p.deltaTime) + p.pos.Y += (p.vel.Y * p.deltaTime) + p.pos.Z += (p.vel.Z * p.deltaTime) + + p.vel.X += (p.acc.X * p.deltaTime) + p.vel.Y += (p.acc.Y * p.deltaTime) + p.vel.Z += (p.acc.Z * p.deltaTime) + + return p.pos +} diff --git a/physics_test.go b/physics_test.go new file mode 100644 index 0000000..8a8423f --- /dev/null +++ b/physics_test.go @@ -0,0 +1,91 @@ +package harmonica_test + +import ( + "math" + "testing" + + . "github.com/charmbracelet/harmonica" +) + +const fps = 60 + +func TestNew(t *testing.T) { + x := 8 + y := 20 + z := 0 + + projectile := NewProjectile(FPS(60), Point{float64(x), float64(y), float64(z)}, Vector{1, 1, 0}, Vector{0, 9.81, 0}) + pos := projectile.Update() + if x != int(pos.X) { + t.Logf("Want: %d, Got: %d", int(x), int(pos.X)) + t.Fatal("x coordinate unexpected") + } + + if y != int(pos.Y) { + t.Logf("Want: %d, Got: %d", int(y), int(pos.Y)) + t.Fatal("y coordinate unexpected") + } +} + +const equalityThreshold = 1e-2 + +// floating point comparison function that tests for an equality under the: +// equalityThreshold +func equal(a, b float64) bool { + return math.Abs(a-b) <= equalityThreshold +} + +func TestUpdate(t *testing.T) { + projectile := NewProjectile(FPS(fps), Point{0, 0, 0}, Vector{5, 5, 0}, Vector{0, 0, 0}) + coordinates := []Point{ + {5.0, 5.0, 0}, + {10.0, 10.0, 0}, + {15.0, 15.0, 0}, + {20.0, 20.0, 0}, + {25.0, 25.0, 0}, + {30.0, 30.0, 0}, + {35.0, 35.0, 0}, + } + + for _, c := range coordinates { + var pos Point + for i := 0; i < fps; i++ { + pos = projectile.Update() + } + + if !equal(pos.X, c.X) || !equal(pos.Y, c.Y) { + t.Logf("Want: (%.2f, %.2f)", c.X, c.Y) + t.Logf("Got: (%.2f, %.2f)", pos.X, pos.Y) + t.Fatal("coordinate unexpected") + } + } +} + +func TestUpdateGravity(t *testing.T) { + fps := 60 + projectile := NewProjectile(FPS(fps), Point{0, 0, 0}, Vector{5, 5, 0}, Vector{0, 9.81, 0}) + + coordinates := []Point{ + {5.0, 9.82, 0}, + {10.0, 29.46, 0}, + {15.0, 58.90, 0}, + {20.0, 98.15, 0}, + {25.0, 147.22, 0}, + {30.0, 206.09, 0}, + {35.0, 274.77, 0}, + } + + for _, c := range coordinates { + var pos Point + for f := 0; f < fps; f++ { + pos = projectile.Update() + } + + if !equal(pos.X, c.X) || !equal(pos.Y, c.Y) { + t.Log("coordinate unexpected") + t.Logf("Want: (%.2f, %.2f)", c.X, c.Y) + t.Logf("Got: (%.2f, %.2f)", pos.X, pos.Y) + t.Fail() + } + } +}