CLI tool
charmbracelet/harmonica avatar
charmbracelet/harmonica

Harmonica is compact spring math with no module requirements

A simple, physics-based animation library 🎼

1,628 stars45 forksGoMIT

At a glance

What is it?
Harmonica is a Go port of a damped simple harmonic oscillator that gives you a two-value state and one Update call per frame. There is no scene graph and no easing curve catalogue, which is both its strength and its limit.
Who is it for?
Adopt Harmonica when you need motion that responds to retargeting mid-flight, since a spring carries velocity across updates where a tween restarts, and it adds nothing to your dependency tree because the module has no requirements. Do not adopt it for scripted or timeline-driven animation, for pixel-exact easing curves, or for anything needing interpolation control, because it computes one damped oscillator and nothing else.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 71 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The entire API is NewSpring and Update

Harmonica is described as a simple, efficient spring animation library for smooth, natural motion, and the surface area matches the description. You create a spring with `NewSpring`, then call `Update` on each frame. There is no timeline, no tween object and no framework to conform to.

The repository root includes `spring.go`, `harmonica.go`, and `projectile.go`, with `projectile_test.go` alongside them. The module declares no dependencies at all:

code
module github.com/charmbracelet/harmonica

go 1.16

An empty require block and an old language version are the practical facts here. You can vendor this into a project without worrying about a transitive tree, and the Go version floor means it compiles on toolchains well past what 1.16 originally targeted.

It is also framework-agnostic and works in 2D and 3D, which is accurate in a literal sense: the math operates on scalars, so the dimensionality is a property of how many springs you create rather than of the library.

Position in, position and velocity out

The API signature is the interesting design decision, and it explains why the library feels different from a tween. `Update` takes the current position and the current velocity and returns the new position and the new velocity. Velocity is caller state, not library state.

go
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)
}

One spring instance drives both axes here, because the settings are the same for x and y. Because velocity is returned rather than stored, you can persist it, interpolate it between two springs, or inspect it. And because the target is a parameter of `Update` rather than of `NewSpring`, changing the target mid-flight does not restart the animation. The motion continues from its current velocity toward the new destination, which is the behaviour that makes a spring feel different from a duration-based tween.

The loop is fixed at 60 frames per second in this example, which keeps the demonstration short. Real code should derive the delta from the engine.

Three constructor arguments, and the one you will get wrong

`NewSpring` takes three values, and the README describes each in terms of what it changes.

The time delta is the step the simulation operates on. Game engines usually hand you a delta; where one is not available, the library ships an `FPS(int)` helper. The warning attached to this parameter is the important part: the framerate you set has to match your actual framerate. A spring tuned for 60Hz run at 30Hz integrates the same physics in half the steps and arrives late, and the error is subtle enough that it usually gets blamed on the damping value instead.

Angular frequency translates roughly to speed, and higher values are faster. It is the knob you reach for when the motion feels sluggish.

The damping ratio is springiness, generally between 0 and 1, though it can go higher. Lower is springier. That third value does most of the work in deciding whether an animation reads as physical or as mechanical, and the README devotes three sections to it.

Under, critical and over damping are three different looks

The damping ratio changes the character of the motion in three distinct ways, and this is the part worth internalising before tuning anything else.

Under-damping is a ratio below 1. The spring reaches equilibrium the fastest of the three, but it overshoots and keeps oscillating while the amplitude decays. That overshoot is what makes UI feel physical: a panel that settles with a small bounce reads as having mass.

Critical damping is exactly 1. The spring reaches equilibrium as fast as possible without oscillating at all. This is the setting to use when you want speed without any visible bounce, such as a value that must land exactly.

Over-damping is above 1. The spring never oscillates and arrives more slowly than a critically damped one. A ratio that is too high produces motion that feels sluggish and dead, which is the usual symptom of someone raising damping to remove a bounce and overshooting.

The default in the README example is 0.5, comfortably under-damped. There is no per-spring preset or named mode in the API, so these three descriptions are the vocabulary you tune against.

A port with a known parent, and a quiet release history

Harmonica is stated to be a fairly straightforward port of Ryan Juckett's damped simple harmonic oscillator, originally written in C++ in 2008 and published in 2012, with a writeup linked from the README. Knowing the provenance is useful: the README links to the author's writeup about the original oscillator.

The repository structure is small and tells you where to look for working code. `examples/` has its own `go.mod` and `go.sum`, which keeps the examples out of the library module, and contains two directories, `spring/` and `particle/`. The README shows both an OpenGL demo and a terminal demo, the latter captioned with the observation that it works well on the command line. Terminal animation is the easier target for the library, since a TUI frame loop is a delta you control directly.

The licence is MIT. On maintenance, the last push was on 2026-08-13, but the newest tag is v0.2.0 from 2022-04-15, with v0.1.0 in July 2021. The gap between the last commit and the last release is a fair signal: this is a settled library receiving occasional work rather than one on a release cadence, so you should expect to consume it from the default branch or pin to v0.2.0 and accept that the two differ.

The comparison worth naming is a hand-written easing function. If your animation runs once from 0 to 1 over a fixed duration with a curve you chose, an easing function is shorter and gives you exact control of the shape. A spring earns its keep when the target changes mid-animation or when the visual needs to respond to interruption, because velocity is carried across updates and a tween cannot preserve that.

Editorial conclusion

Adopt Harmonica when you need motion that responds to retargeting mid-flight, since a spring carries velocity across updates where a tween restarts, and it adds nothing to your dependency tree because the module has no requirements. Do not adopt it for scripted or timeline-driven animation, for pixel-exact easing curves, or for anything needing interpolation control, because it computes one damped oscillator and nothing else. Verify first that your frame loop feeds a real delta, since the README is explicit that the framerate you pass to `FPS` must match your actual framerate or the motion timing will be wrong.

Frequently asked questions

How do I create a spring animation with Harmonica in Go?

Call `harmonica.NewSpring(harmonica.FPS(60), 6.0, 0.5)` with a time delta, an angular frequency and a damping ratio, then call `spring.Update(position, velocity, target)` each frame and store the two returned values.

What damping ratio should I use in Harmonica?

Below 1 is under-damped, which arrives fastest and overshoots. Exactly 1 is critical damping, which arrives as fast as possible without oscillating. Above 1 is over-damped, never oscillates but arrives more slowly.

Does Harmonica have any dependencies?

No. The module declares no requirements, and the examples are kept in a separate module under `examples/`.

What happens if my frame rate does not match the one I pass to FPS?

The spring integrates a time step that does not match the real one, so motion timing comes out wrong. The README states that the framerate you set must match your actual framerate, and game engines are the better source for a real delta when one is available.

Official sources

  1. charmbracelet/harmonica on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/charmbracelet-harmonica.svg)](https://hysenlabs.com/projects/charmbracelet-harmonica)

Was this useful?