AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Charm Harmonica

skill-alxxpersonal-forge-charm-harmonica · by alxxpersonal

Physics-based animation for Go TUIs - damped spring oscillator and projectile motion. Use when adding spring animations, physics-based motion, or smooth transitions to Go terminal apps. No Ease function exists in this library.

No reviews yet
0 installs
19 views
0.0% view→install

Install

$ agentstack add skill-alxxpersonal-forge-charm-harmonica

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-alxxpersonal-forge-charm-harmonica)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
3mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Charm Harmonica? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

charm-harmonica

Physics-based animation primitives: spring (damped harmonic oscillator) and projectile motion.

Quick Start

import "github.com/charmbracelet/harmonica"

// Create spring once - expensive coefficients computed here
spring := harmonica.NewSpring(harmonica.FPS(60), 6.0, 0.5)

// Per-frame state - YOU own these
var pos, vel float64

// Each frame:
pos, vel = spring.Update(pos, vel, targetPos)

Core API

FPS(n int) float64

Converts a frame rate to a delta time in seconds. Pass directly to NewSpring or NewProjectile.

dt := harmonica.FPS(60) // 0.01666...

Use the engine's actual delta time instead when available (e.g. real elapsed time between frames).

NewSpring(deltaTime, angularFrequency, dampingRatio float64) Spring

Pre-computes spring coefficients. Call once, reuse across frames.

| Parameter | Effect | |-----------|--------| | deltaTime | Frame duration in seconds - use FPS(n) | | angularFrequency | Speed of motion. Typical range: 1-20 | | dampingRatio | Oscillation behavior |

Damping ratio guide:

  • 1.0 - over-damped, slow and sluggish

Practical starting points: frequency 6.0, damping 0.5 (smooth). For bouncy: frequency 8.0, damping 0.15. For snappy: frequency 12.0, damping 1.0.

Spring.Update(pos, vel, target float64) (newPos, newVel float64)

Advances one frame. Returns new position and velocity - always capture both.

// One spring can drive multiple independent axes
x, xVel = spring.Update(x, xVel, targetX)
y, yVel = spring.Update(y, yVel, targetY)
radius, radiusVel = spring.Update(radius, radiusVel, targetRadius)

NewProjectile(deltaTime float64, pos Point, vel, acc Vector) *Projectile

Kinematic projectile - no spring, just position + velocity + acceleration per frame.

p := harmonica.NewProjectile(
    harmonica.FPS(60),
    harmonica.Point{X: 0, Y: 0, Z: 0},
    harmonica.Vector{X: 5, Y: 0, Z: 0},   // initial velocity
    harmonica.TerminalGravity,              // acceleration: {0, 9.81, 0}
)

// Each frame:
pos := p.Update() // returns harmonica.Point

Gravity constants:

  • harmonica.Gravity - {0, -9.81, 0} - origin bottom-left
  • harmonica.TerminalGravity - {0, 9.81, 0} - origin top-left (standard TUI)

Projectile accessors: p.Position(), p.Velocity(), p.Acceleration()

No Ease Function

harmonica does not have an Ease function. It has Spring and Projectile only.

bubbletea Integration Pattern

The canonical pattern uses a frameMsg sentinel type and tea.Tick to drive the loop.

const fps = 60

type frameMsg time.Time

// Schedules the next frame tick
func animate() tea.Cmd {
    return tea.Tick(time.Second/fps, func(t time.Time) tea.Msg {
        return frameMsg(t)
    })
}

type model struct {
    x      float64
    xVel   float64
    spring harmonica.Spring
}

func (m model) Init() tea.Cmd {
    return animate() // kick off the loop
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg.(type) {
    case frameMsg:
        const target = 60.0
        m.x, m.xVel = m.spring.Update(m.x, m.xVel, target)

        // Stop ticking when close enough
        if math.Abs(m.x-target) < 0.01 {
            return m, nil
        }

        return m, animate() // schedule next frame
    }
    return m, nil
}

func main() {
    m := model{
        spring: harmonica.NewSpring(harmonica.FPS(fps), 7.0, 0.15),
    }
    tea.NewProgram(m).Run()
}

Changing spring parameters mid-animation

When frequency or damping changes, call NewSpring again with the same deltaTime. The current pos and vel carry over - do not reset them.

// User changed settings - recompute spring, keep state
spring = harmonica.NewSpring(harmonica.FPS(fps), newFreq, newDamp)
// x, xVel unchanged - animation continues smoothly from current state

Stopping the animation loop

Return nil (no command) instead of animate() to stop. Resume by returning animate() again on the next relevant message.

Common Mistakes

Forgetting to capture velocity. spring.Update returns two values. Discarding the velocity breaks the simulation on the next frame.

// wrong
pos, _ = spring.Update(pos, vel, target)

// right
pos, vel = spring.Update(pos, vel, target)

Creating Spring inside the update loop. NewSpring is expensive - it computes trig/exp coefficients. Create it once in main or Init, store it on the model.

Using Gravity instead of TerminalGravity in TUIs. TUI coordinate systems have Y increasing downward. Use TerminalGravity ({0, 9.81, 0}) so things fall down the screen, not up.

Not passing real delta time in non-fixed-FPS contexts. In game loops with variable frame time, pass the actual time.Since(last).Seconds() to NewSpring each frame instead of FPS(60). Recreating the spring with the real dt each frame is correct and expected.

Calling animate() unconditionally. Always check if the animation has converged before scheduling the next frame, or it runs forever at 60fps.

Checklist

  • [ ] NewSpring called once, stored on model struct
  • [ ] Both return values from Update captured (pos, vel = ...)
  • [ ] animate() returns nil when animation is done (convergence check)
  • [ ] Using TerminalGravity for TUI projectiles (Y-down coordinate space)
  • [ ] frameMsg type defined as type frameMsg time.Time
  • [ ] Spring recomputed (not state reset) when parameters change at runtime

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.