Spring-driven motion - spr

:comet:spr

Docs | Download | Home

Ljt38piFyS

Springs are a powerful way of describing physically based motion.
spr is an accessible library for creating beautiful UI animations from springs.

Fluid animations can be created in a single line of code with a simple syntax. Blending between animations is handled automatically in a physically-accurate way.

Driving Simulator uses spr extensively to handle over 500 unique animations.

Gallery



Features


A small API

  • spr is easy to learn for non-programmers.
  • You can animate anything by giving spr a target value and a set of animation parameters.
  • You don’t have to memorize new datatypes or more than a few functions.

Easy-to-tune motion

  • You should be able to know how an animation will look without running the game.
  • Motion is defind by frequency and damping ratio, which are easy to understand and visualize.

An analytical spring solver

  • The spr spring solver is extremely robust and provides protections from accidentally passing bad motion parameters.
  • If spr is given a nonconverging set of motion parameters, it will throw a clear error describing what is wrong and how to fix it.

Tight integration with Roblox datatypes

  • spr directly animates Roblox properties without additional layers of indirection.
  • spr performs runtime type checking, providing stronger typing than Roblox instance property setters.
  • spr knows how to animate in the ideal space for each datatype.

Spring fundamentals


Damping ratio and frequency are the two properties defining a spring’s motion.

Damping ratio

  • Damping ratio < 1 overshoots and converges on the target. This is called underdamping.
  • Damping ratio = 1 converges on the target without overshooting. This is called critical damping.
  • Damping ratio > 1 converges on the target without overshooting, but slower. This is called overdamping.

Critical damping is recommended as the most visually neutral option.
Underdamping is recommended for animations that need to “pop.”

Damping ratio and frequency can be visualized here.

Examples


-- damping ratio 1 (critically damped), frequency 0.5 (slow)
-- frame slowly moves to the middle of the screen without overshooting

spr.target(frame, 1, 0.5, {
    Position = UDim2.fromScale(0.5, 0.5)
})
-- damping ratio 0.6 (underdamped), frequency 4 (fast)
-- frame quickly moves to the middle, overshoots, and wobbles before converging

spr.target(frame, 0.6, 4, {
    Position = UDim2.fromScale(0.5, 0.5)
})
-- animate Position and Size
spr.target(frame, 0.6, 1, {
    Position = UDim2.fromScale(1, 1),
    Size = UDim2.fromScale(0.5, 0.5)
})

wait(1)

-- stop all animations on Frame
spr.stop(frame)

How to get


  1. Paste the source of spr.lua into a new ModuleScript.
  2. Require the ModuleScript with local spr = require(<path to spr goes here>)
  3. Follow the documentation to get started with the API.

Documentation on how to use ModuleScripts can be found here.
roblox-ts bindings for spr can be installed here.

Issues and bugs can be filed under GitHub issues.

238 Likes

This is exactly what I was looking for. Now I can make cool animated GUIs

5 Likes

Is there a way for an instance’s property to target another instance property?

For example, spr.target(frame,damp,frequency,{
Position = Part2.Position
})

This way Part1 can follow Part2’s position. This should look similar to connecting a spring constraint to both parts.

Please let me know how I can achieve this with your library(If it is possible).

spr.target to the other object’s position every frame.

RunService.Stepped:Connect(function()
   spr.target(Part1, d, f, {
      Position = Part2.Position
   })
end)

Useful module, this looks great. I have three questions though:

#1. Do you scale by delta so that the spring movement is the same on every frame rate? I’m assuming you are already but I cba to read the source rn

#2. Do you have a rough estimate of frequency to seconds? Like 1 frequency = x seconds

#3. Is there any way for us to detect when the spring stops moving, like a .Completed event? This would be very useful

13 Likes

This is such a cool module. I will definitely be using this in my game! :slight_smile:

1 Like

Been using this module for a while and I love it! But one thing that would be useful is a .Completed event to check when the Spring has been completed.

3 Likes

This is awesome, keep up the good work

spr 2.1 has been released. Download

Changes since the last post here:

  • Added support for CFrames
  • Added support for booleans
  • Added support for ColorSequences
  • Model pivot can be animated via Pivot pseudo-property
  • Model scale can be animated via Scale pseudo-property
  • Added Luau strict mode for better auto-complete
  • Added completed callback to track when spr is finished animating an object

Example - Completion

-- Destroy when fully transparent
spr.target(part, damp, freq, {Transparency = 1})
spr.completed(part, function() part:Destroy() end)

Example - Pivot & Scale

spr.target(model, damp, freq, {
    Pivot = CFrame.new(),
    Scale = 2,
})

16 Likes

I’ve seen your post earlier and coming back to it as I’m testing it out.

The project is fairly near overall and well put together. Makes animating much smoother and appealing to the eye. Great project.

Dope! Using this in my game too!

Question: Do unsupported type like NumberSequence could be supported on the next update?

Would be really cool if it was compatible with plugins. Been using this module for a long time! I love it.

1 Like

Is it not? (I haven’t used this before)

Here’s an alternative, if you want it. The module does not step springs automatically
You can create a spring with module.create()

--!strict
--!native

--Unlike Spring_Raw, this module is more of an object-based approach, to increase simplicity

--Based on: https://gist.github.com/chadcable/92bc3958af5b171e593e36be57ca36ce

export type Spring = {
	frequency:number,
	damping:number,

	position:number,
	velocity:number,
	target:number,
}

local epsilon = 0.0001

local module = {}

local function calculate_params(delta:number, freq:number, damping_ratio:number):(number, number, number, number)
	--If there is no angular frequency, the spring will not move.
	--Just return with default params
	if freq < epsilon then
		local m_posPosCoef = 1
		local m_posVelCoef = 0
		local m_velPosCoef = 0
		local m_velVelCoef = 1
		
		return
			m_posPosCoef,
			m_posVelCoef,
			m_velPosCoef,
			m_velVelCoef
	end

	if damping_ratio > 1 + epsilon then
		--Over-damped

		local za = -freq * damping_ratio
		local zb = freq * math.sqrt(damping_ratio*damping_ratio - 1)
		local z1 = za - zb
		local z2 = za + zb

		local e1 = math.exp(z1 * delta)
		local e2 = math.exp(z2 * delta)

		local invTwoZb = 1/(2 * zb)

		local e1_Over_TwoZb = e1*invTwoZb
		local e2_Over_TwoZb = e2*invTwoZb

		local z1e1_Over_TwoZb = z1*e1_Over_TwoZb
		local z2e2_Over_TwoZb = z2*e2_Over_TwoZb

		--output.m_posPosCoef =  e1_Over_TwoZb*z2 - z2e2_Over_TwoZb + e2
		--output.m_posVelCoef = -e1_Over_TwoZb 	+ e2_Over_TwoZb

		--output.m_velPosCoef = (z1e1_Over_TwoZb - z2e2_Over_TwoZb + e2)*z2
		--output.m_velVelCoef = -z1e1_Over_TwoZb + z2e2_Over_TwoZb
		
		local m_posPosCoef =  e1_Over_TwoZb*z2 - z2e2_Over_TwoZb + e2
		local m_posVelCoef = -e1_Over_TwoZb + e2_Over_TwoZb
		local m_velPosCoef = (z1e1_Over_TwoZb - z2e2_Over_TwoZb + e2)*z2
		local m_velVelCoef = -z1e1_Over_TwoZb + z2e2_Over_TwoZb
		
		return
			m_posPosCoef,
			m_posVelCoef,
			m_velPosCoef,
			m_velVelCoef
	elseif damping_ratio < 1 - epsilon then
		--Under-damped

		local omegaZeta = freq * damping_ratio
		local alpha = freq * math.sqrt(1 - damping_ratio*damping_ratio)

		local expTerm = math.exp(-omegaZeta * delta)
		local cosTerm = math.cos(alpha * delta)
		local sinTerm = math.sin(alpha * delta)

		local invAlpha = 1 / alpha

		local expSin = expTerm * sinTerm
		local expCos = expTerm * cosTerm
		local expOmegaZetaSin_Over_Alpha = expTerm * omegaZeta * sinTerm * invAlpha

		--output.m_posPosCoef = expCos + expOmegaZetaSin_Over_Alpha
		--output.m_posVelCoef = expSin * invAlpha

		--output.m_velPosCoef = -expSin*alpha - omegaZeta*expOmegaZetaSin_Over_Alpha
		--output.m_velVelCoef =  expCos - expOmegaZetaSin_Over_Alpha
		
		local m_posPosCoef = expCos + expOmegaZetaSin_Over_Alpha
		local m_posVelCoef = expSin * invAlpha
		local m_velPosCoef = -expSin*alpha - omegaZeta*expOmegaZetaSin_Over_Alpha
		local m_velVelCoef =  expCos - expOmegaZetaSin_Over_Alpha
		
		return
			m_posPosCoef,
			m_posVelCoef,
			m_velPosCoef,
			m_velVelCoef
	else
		--Critically damped

		local expTerm = math.exp(-freq * delta)
		local timeExp = delta * expTerm
		local timeExpFreq = timeExp * freq

		--output.m_posPosCoef = timeExpFreq + expTerm
		--output.m_posVelCoef = timeExp

		--output.m_velPosCoef = -freq*timeExpFreq
		--output.m_velVelCoef = -timeExpFreq + expTerm
		
		local m_posPosCoef = timeExpFreq + expTerm
		local m_posVelCoef = timeExp
		
		local m_velPosCoef = -freq*timeExpFreq
		local m_velVelCoef = -timeExpFreq + expTerm
		
		return
			m_posPosCoef,
			m_posVelCoef,
			m_velPosCoef,
			m_velVelCoef
	end
end
module._calculate_params = calculate_params

function module.create(frequency:number, damping:number, position:number?, velocity:number?):Spring
	return {
		frequency = frequency,
		damping = damping,

		position = position or 0,
		velocity = velocity or 0,
		target = position or 0,
	}
end

function module.step(spring:Spring, delta:number):(number, number)
	local target:number = spring.target
	
	local m_posPosCoef, m_posVelCoef, m_velPosCoef, m_velVelCoef = calculate_params(delta, spring.frequency, spring.damping)
	
	local oldPos = spring.position - target --Update in equilibrium (target) relative space
	local oldVel = spring.velocity

	local out_pos = oldPos*m_posPosCoef + oldVel*m_posVelCoef + target
	local out_vel = oldPos*m_velPosCoef + oldVel*m_velVelCoef
	
	spring.position = out_pos
	spring.velocity = out_vel
	
	return out_pos, out_vel
end

return module

When I die can you bury me with a USB stick with spr on it please I love this module

1 Like

just wondering, is spr considered the best spring library on the market atm?

I pretty much use it for everything, been using it for a long time and can say it’s a good module.