Physics and velocity
A core feature of Popmotion is the ability for some animations (decay, spring and physics) to take a velocity
parameter.
velocity
changes each of these animations in a way that feels natural and visceral.
By passing the velocity of an existing animation to another, we can create natural-feeling transitions. And if that existing animation is pointer
, it allows the user to directly affect animations with their own force leading to natural and playful interactions.
Inspect velocity
But how do we get velocity
?
Popmotion provides a special type of reaction called value
.
import { value } from 'popmotion';
value
sits between your action and another reaction (for instance a style setter), and can be queried with get
and getVelocity
:
const myValue = value(0, console.log);
tween().start(myValue);
setTimeout(() => myValue.getVelocity(), 100)
The returned velocity
is measured in units per second. Why? Although 60fps is the current common framerate, VR devices support 90+ fps and the iPad Pro delivers a silky 120 frames per second!
To future-proof our code, we decouple velocity from the device framerate, otherwise our animations would run at 1.5x or even 2x the speed on these faster displays.
Using value
value
is provided two arguments: A value, and a function to call when this value updates:
const foo = value(0, console.log);
As value
is a reaction, it has an update
method. We can call it to update the value:
foo.update(5);
Usually though, we provide the value
directly to an action:
tween({ to: 5 }).start(foo);
Like our animations, value
can accept objects and arrays:
const xy = value({ x: 0, y: 0 }, console.log);
const foo = tween({
to: { x: 100, y: 200 }
}).start(xy);
setTimeout(() => xy.getVelocity(), 100); // Returns as object
Now we know enough about value
to get the velocity of our user’s pointer.
Get pointer
velocity
Using the example from the previous tutorial, let’s first make a value
that updates ballStyler
‘s x
and y
properties:
const ballXY = value({ x: 0, y: 0 }, ballStyler.set);
Now we can replace our startTracking
function with this:
const startTracking = () => {
pointer(ballXY.get()).start(ballXY);
};
As an added benefit of using value
, a value
can’t be subscribed to more than one action at a time.
This means that we can stop using pointerTracker
to maintain a reference to our active pointer
.
Instead, we can either use ballXY.stop()
, which will stop the action it’s currently subscribed to. Or, we simply provide it to a different action, which is what we’ll do in the following examples.
For now, amend stopTracking
so it queries ballXY
‘s current velocity:
const stopTracking = () => {
const velocity = ballXY.getVelocity();
};
Using velocity
Three Popmotion animations accept a velocity
property: decay
, physics
and spring
.
Let’s modify stopTracking
three times, once for each, to see what they each do with velocity
.
decay
decay
exponentially decreases a velocity over time. It’s a form of the algorithm used in smartphone momentum scrolling, making it a natural-feeling way of slowing something down.
Based on the initial properties and velocity
, it’ll automatically calculate a target
to animate to.
Using it is as easy as passing our newly-calculated velocity
to decay
:
decay({ velocity }).start(ballXY);
decay
also accepts a modifyTarget
function, which is provided the calculated target and returns a new one.
This can be used, for instance, to snap the target to a grid:
decay({
from: ballX.get(),
velocity,
modifyTarget: (target) => Math.ceil(target / 100) * 100
})
spring
spring
is a spring simulation using mass
, velocity
, stiffness
and damping
. It can be used to simulate a wide variety of spring-feels.
Springs are great for interaction designers because they’re expressive. For instance, you could design a spring that has a stiffness
and damping
property, but the mass
property is based on the relative size of the element moving.
spring
has defaults for all properties but you’ll likely want to adjust at least stiffness
and damping
:
spring({
from: ballXY.get(),
velocity,
stiffness: 300,
damping: 10
}).start(ballXY);
physics
The physics
animation is the swiss army knife of velocity-based animations.
It offers friction
, to
and springStrength
properties, so it can theoretically be used to create motion similar to decay
and spring
.
However, decay
and spring
animations are differential equations that resolve for a given elapsedTime
. In practical terms, this means that if you want to change the animation they’re creating, you need create a new animation with the new properties.
These equations are incredibly accurate, offering the smoothest motion and in the near future, will allow these animations to be scrubbable the same way tween
is.
Instead, physics
is an integrated simulation. This means that, once the simulation has started, its properties can be modified because physics
uses its current state to calculate its next
For instance, we can tether a physics
spring between the ball and the pointer:
const springTo = physics({
velocity: ballXY.getVelocity(),
friction: 0.6,
springStrength: 400,
to: ballXY.get(),
restSpeed: false
}).start(ballXY);
pointer(ballXY.get())
.start((v) => springTo.setSpringTarget(v));
Conclusion
velocity
is a key part of creating natural interactions with Popmotion.
Be sure to check out the full docs of value, decay, spring and physics, as there’s much more to each than we’ve been able to cover in this introductory tutorial.