Harmonica

repository·master·Indexed 23 days ago

https://github.com/charmbracelet/harmonica

A framework-agnostic spring animation and projectile simulation library for smooth, natural motion in 2D and 3D contexts, including command-line TUIs. It provides tools for simulating damped harmonic oscillators via NewSpring and projectile motion via NewProjectile, utilizing Point and Vector types for spatial coordinates.

Tokens
2.7K
Snippets
10
Records
17
Agent score
75%

What's inside harmonica

  1. Understand Damping Ratios and motion behavior

    master

    The damping ratio determines how the spring behaves as it approaches its target (equilibrium):

    • Under-Damping (Ratio < 1): Reaches equilibrium the fastest but overshoots the target and oscillates with decaying amplitude.
    • Critical Damping (Ratio == 1): Reaches equilibrium as fast as possible without any oscillation.
    • Over-Damping (Ratio > 1): Does not oscillate, but reaches equilibrium more slowly than a critically damped spring.
  2. How to use Harmonica for spring animations

    master

    Harmonica is a framework-agnostic library for smooth, natural motion using spring physics. To use it, initialize a spring with NewSpring and call Update on every frame of your animation loop.

    Update requires the current position and current velocity of the object being animated, and returns the new position and new velocity. This allows you to animate multiple dimensions (like X and Y) independently using the same spring instance.

    import "github.com/charmbracelet/harmonica"
    
    // A thing we want to animate.
    sprite := struct{
        x, xVelocity float64
        y, yVelocity float64
    }{}
    
    // Where we want to animate it.
    const targetX = 50.0
    const targetY = 100.0
    
    // Initialize a spring with framerate, angular frequency, and damping values.
    spring := harmonica.NewSpring(harmonica.FPS(60), 6.0, 0.5)
    
    // Animate!
    for {
        sprite.x, sprite.xVelocity = spring.Update(sprite.x, sprite.xVelocity, targetX)
        sprite.y, sprite.yVelocity = spring.Update(sprite.y, sprite.yVelocity, targetY)
        time.Sleep(time.Second/60)
    }
  3. Configure Spring damping ratios

    master

    The dampingRatio parameter in NewSpring determines how the spring behaves as it approaches its target position:

    Damping RatioTypeBehavior
    > 1.0Over-dampedThe spring will never oscillate; it reaches equilibrium slowly.
    = 1.0Critically-dampedThe spring reaches equilibrium as fast as possible without oscillating.
    < 1.0Under-dampedThe spring reaches equilibrium quickly but overshoots and oscillates with decaying amplitude.
  4. Initialize and use a Spring for animation

    master

    A Spring simulates a damped harmonic oscillator, useful for creating smooth, springy animations. To use it efficiently, you should initialize a Spring once with your desired physics parameters and then call Update on every frame of your animation loop.

    Workflow

    1. Initialize: Call NewSpring with your deltaTime (the time elapsed between frames), angularFrequency (speed of motion), and dampingRatio (oscillation behavior).
    2. Update: In your animation loop, call Update passing the current position, current velocity, and the targetPosition (equilibrium) you want the spring to reach.
    // Run once to initialize.
    spring := NewSpring(FPS(60), 6.0, 0.2)
    
    // Update on every frame.
    pos := 0.0
    velocity := 0.0
    targetPos := 100.0
    someUpdateLoop(func() {
        pos, velocity = spring.Update(pos, velocity, targetPos)
    })
  5. Initialize and update a projectile

    master

    To simulate projectile motion, use NewProjectile to initialize the state with a frame rate (delta time), starting position, initial velocity, and acceleration. On every frame of your simulation loop, call Update() to advance the physics state. Update() returns the new Point position of the projectile.

    // Run once to initialize.
    projectile := NewProjectile(
        FPS(60), // Note: Ensure your FPS helper matches the float64 deltaTime requirement
        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()
    })
  6. Configure spring settings with NewSpring

    master

    The NewSpring function initializes a spring simulation with three primary parameters:

    1. Time Delta: The time step for each operation. If your environment doesn't provide a delta time, use the harmonica.FPS(int) utility to set a fixed framerate. Ensure this matches your actual loop frequency.
    2. Angular Velocity: Controls the speed of the animation. Higher values result in faster motion.
    3. Damping Ratio: Controls the 'springiness' or oscillation of the motion. Generally a value between 0 and 1, but can be higher.
  7. Use NewProjectile to create a new projectile

    master

    The NewProjectile function initializes a Projectile instance. It requires a float64 representing the time step (delta time) between updates, an initial Point for position, and Vector types for velocity and acceleration.

    func NewProjectile(deltaTime float64, initialPosition Point, initialVelocity, initalAcceleration Vector) *Projectile
  8. Use projectile simulation with NewProjectile

    master

    To simulate projectiles or particles, initialize a simulator with NewProjectile. This requires the frame rate (via FPS), an initial starting Point, an initial velocity Vector, and a gravity Vector. On every frame, call Update() to retrieve the new position of the projectile.

    // 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()
    })
  9. NewSpring

    master

    Initializes a new Spring by computing cached coefficients for efficient updates. This allows you to use the same physics settings across multiple different springs in an update loop.

    Parameters:

    • deltaTime (float64): The time step to advance (e.g., the length of one animation frame).
    • angularFrequency (float64): The angular frequency of motion; higher values increase the speed of the spring.
    • dampingRatio (float64): Determines the oscillation behavior (see Damping Ratios).
  10. Calculate time delta with FPS()

    master

    The FPS(n int) function returns the time delta (in seconds) for a given number of frames per second. This is a convenient way to provide a deltaTime to NewSpring if you are targeting a specific framerate.

    Note: If your game engine or animation loop provides a real-time delta (the actual time elapsed since the last frame), use that instead of FPS() for more accurate physics.

  11. Get projectile state with Position, Velocity, and Acceleration

    master

    You can inspect the current state of a Projectile using the following methods:

    • Position(): Returns the current Point coordinates.
    • Velocity(): Returns the current Vector velocity.
    • Acceleration(): Returns the current Vector acceleration.
    func (p *Projectile) Position() Point
    func (p *Projectile) Velocity() Vector
    func (p *Projectile) Acceleration() Vector
  12. Define spatial coordinates with Point and Vector

    master

    Harmonica uses Point and Vector types to represent 3D coordinates and directions.

    • Point: Represents a specific location in 3D space using X, Y, and Z (all float64).
    • Vector: Represents a magnitude and direction using X, Y, and Z (all float64).
    type Point struct {
    	X, Y, Z float64
    }
    
    type Vector struct {
    	X, Y, Z float64
    }