Hello, Habr!I have long wanted to add Spring animations to any projects. But I did this only for React projects using react-spring, because I did not know anything else.But finally, I decided to figure out how it all works and write my implementation!If you also want to use Spring-animations everywhere, go under the cat. There you will find some theory, implementing Spring in pure JS, and implementing Spring animation in Vue using components and composition-api.TL; DR
Implementing Spring Animation in JS: https://playcode.io/590645/ .You need to add Spring animations to your project - take Spring from the Popmotion library .You need to add Spring animations to your Vue project - take Spring from the Popmotion library and write a wrapper over it.What are Spring Animations for?
Conventional css animations on the web use a duration and a function for calculating position from time to time.Spring animations use a different approach - spring physics.For animation, we set the physical properties of the system: the mass of the spring, the coefficient of elasticity of the spring, the coefficient of friction of the medium. Then we stretch the spring to the distance that we need to animate, and release it.In front-end projects, I see such advantages in Spring animations:1. You can set the initial animation speed.If the site processes swipes with your fingers or drag with the mouse, then the object animations will look more natural if you set them with the initial speed like swipe speed or mouse speed.
CSS animation that doesn't retain mouse speed
Spring-animation, in which the speed of the mouse is transmitted to the spring2. You can change the endpoint of the animationIf, for example, you want to animate an object to the mouse cursor, then you can’t help with CSS - when you update the cursor position, the animation is initialized again, which will cause jerking is visible.There is no such problem with Spring-animations - if you need to change the end point of the animation, we stretch or compress the spring to the desired indent.
An example of animating an element to a mouse cursor; top - CSS animation, bottom - Spring animationSpring physics
Take an ordinary spring with a load. She is still at rest.Pull her down a bit. An elastic force (), which seeks to return the spring to its original position. When we release the spring, it will begin to swing along the sinusoid:Add friction force. Let it be directly proportional to the speed of the spring () In this case, the oscillations will decay over time:We want our Spring-animations to be animated in the same way that a spring oscillates when exposed to elasticity and friction.We consider the position of the spring for animation
In the context of animations, the difference between the initial and final value of the animation is the initial tension of the spring .Before the start of the animation, we are given: spring mass, spring coefficient of elasticity , friction coefficient of the medium spring tension , starting speed .The animation position is calculated several tens of times per second. The time interval between animation calculations is called.During each new calculation, we have the spring speed and spring tension on the last animation interval. For the first calculation of animation, .We start to calculate the position of the animation for the interval!According to Newton’s second law, the sum of all applied forces is equal to the mass of the body multiplied by its acceleration:
Two forces act on a spring - elastic force and friction force:
We will write down the forces in more detail and find the acceleration:
After that we can find a new speed. It is equal to the sum of the speed in the previous animation interval and the acceleration multiplied by the time interval:
Knowing the speed, you can find the new position of the spring:
We got the right position, display it to the user!Once displayed, start a new animation interval. For a new interval, .The animation is worth stopping when and become very small - at this moment the spring oscillations are almost invisible.JS Spring Animation
We have the necessary formulas, it remains to write our implementation:class Spring {
constructor({ mass, tension, friction, initVelocity, from, to, onUpdate }) {
this.mass = mass;
this.tension = tension;
this.friction = friction;
this.initVelocity = initVelocity;
this.from = from;
this.to = to;
this.onUpdate = onUpdate;
}
startAnimation() {
const callDoAnimationTick = () => {
const isFinished = this.doAnimationTick();
if (isFinished) {
return;
}
this.nextTick = window.requestAnimationFrame(callDoAnimationTick);
};
callDoAnimationTick();
}
stopAnimation() {
const { nextTick } = this;
if (nextTick) {
window.cancelAnimationFrame(nextTick);
}
this.isFinished = true;
}
doAnimationTick() {
const {
mass, tension, friction, initVelocity, from, to,
previousTimestamp, prevVelocity, prevValue, onUpdate,
} = this;
const currentTimestamp = performance.now();
const deltaT = (currentTimestamp - (previousTimestamp || currentTimestamp))
/ 1000;
const normalizedDeltaT = Math.min(deltaT, 0.046);
let prevSafeVelocity = prevVelocity || initVelocity || 0;
let prevSafeValue = prevValue || from;
const springRestoringForce = -1 * tension * (prevSafeValue - to);
const dampingForce = -1 * prevSafeVelocity * friction;
const acceleration = (springRestoringForce + dampingForce) / mass;
const newVelocity = prevSafeVelocity + acceleration * normalizedDeltaT;
const newValue = prevSafeValue + newVelocity * normalizedDeltaT;
const precision = 0.001;
const isFinished = Math.abs(newVelocity) < precision
&& Math.abs(newValue - to) < precision;
onUpdate({ value: newValue, isFinished });
this.previousTimestamp = currentTimestamp;
this.prevValue = newValue;
this.prevVelocity = newVelocity;
this.isFinished = isFinished;
return isFinished;
}
}
Sandbox with Spring class code and an example of its use:https://playcode.io/590645Minor Spring Animation Improvement
There is a small problem with our Spring class - will be different each time with new starts of the animation.Open on an old cell phone -will be equal to 46 milliseconds, our maximum limit. Open on a powerful computer -will be 16-17 milliseconds.Changingmeans that the duration of the animation and changes in the animated value will be slightly different each time.To prevent this from happening, we can do this:Takeas a fixed value. With a new animation interval, we will have to calculate how much time has passed since the last interval and how many fixed values are in it. If time is not divisible by, then transfer the remainder to the next animation interval.Then we calculate and as many times as we got fixed values. Last valueshowing to the user.Example:let's take as 1 millisecond, 32 milliseconds passed between the last and the new animation interval.We have to calculate physics 32 times, and ; lastmust be shown to the user.This is what the doAnimationTick method will look like:class Spring {
doAnimationTick() {
const {
mass, tension, friction, initVelocity, from, to,
previousTimestamp, prevVelocity, prevValue, onUpdate,
} = this;
const currentTimestamp = performance.now();
const fractionalDiff = currentTimestamp - (previousTimestamp || currentTimestamp);
const naturalDiffPart = Math.floor(fractionalDiff);
const decimalDiffPart = fractionalDiff % 1;
const normalizedDiff = Math.min(naturalDiffPart, 46);
let safeVelocity = prevVelocity || initVelocity || 0;
let safeValue = prevValue || from;
for (let i = 0; i < normalizedDiff; i++) {
const springRestoringForce = -1 * tension * (safeValue - to);
const dampingForce = -1 * safeVelocity * friction;
const acceleration = (springRestoringForce + dampingForce) / mass;
safeVelocity = safeVelocity + acceleration / 1000;
safeValue = safeValue + safeVelocity / 1000;
}
const precision = 0.001;
const isFinished = Math.abs(safeVelocity) < precision
&& Math.abs(safeValue - to) < precision;
onUpdate({ value: safeValue, isFinished });
this.previousTimestamp = currentTimestamp - decimalDiffPart;
this.prevValue = safeValue;
this.prevVelocity = safeVelocity;
this.isFinished = isFinished;
return isFinished;
}
}
This method of calculating physics uses react-spring, in this article you can read more.About attenuation coefficient and intrinsic oscillation period
Looking at the properties of the system - the mass of the load, the coefficient of elasticity and the coefficient of friction of the medium - it is completely unclear how the spring behaves. When creating Spring-animations, these variables are selected randomly until the programmer is satisfied with the "springiness" of the animation.However, in physics there are a couple of variables for a damped spring, looking at them you can tell how the spring behaves.To find them, let's return to our equation of Newton’s second law:
We write it as a differential equation:
It can be rewritten as:
Where - the frequency of the oscillations of the spring without taking into account the friction force, - attenuation coefficient.If we solve this equation, then we get several functionsthat determine the position of the spring versus time. I’ll put the solution under the spoiler so as not to stretch the article too much, we will go straight to the result.Equation solution, :
https://www.youtube.com/watch?v=uI2xt8nTOlQ:
:
:
1.
.
:
— , :
:
.
, 0.
2.
.
:
— , :
:
, 0. ,
.
, , , 0 ,
. , ?
3.
.
:
— , :
:
, 0.
,
0, .. .
.
At we get an equation that describes the damped vibrations of a spring. Less, the lower the friction force and the more visible vibrations.Atwe get the equation with critical friction, i.e. the equation in which the system returns to the equilibrium position without hesitation, in the fastest way.Atwe get an equation in which the system returns to the equilibrium position without hesitation. This will be slower than with; with increase the rate of convergence to the equilibrium position decreases.Examples of animations for various . Animation speed reduced by 4 times.In addition to, there is another understandable characteristic of spring oscillations - the period of spring oscillations without taking into account friction:
We got two parameters:→, which determines how much the animation will fluctuate.Possible Values0 to 1.less, the more visible vibrations the spring will have. At 0, the system will not have friction, the spring will oscillate without attenuation; at 1, the spring will converge to the equilibrium position without hesitation.Values greater than 1 are not necessary for us - they work as 1, only in vain drag out our animation.→which will say how long the animation will last.It should be borne in mind the parametertalks about the duration of a single vibration of the spring without taking into account the friction force, and not about the total duration of the animation.Now we can derive the coefficient of elasticity and the coefficient of friction of the medium, knowing and and assuming that :
We implement this logic in the code:const getSpringValues = ({ dampingRatio, period }) => {
const mass = 1;
const tension = Math.pow(2 * Math.PI / period, 2);
const friction = 4 * Math.PI * dampingRatio / period;
return {
mass,
tension,
friction,
};
};
new Spring({
...getSpringValues({
dampingRatio: 0.3,
period: 0.6,
}),
from: 0,
to: 20,
onUpdate: () => { },
});
new Spring({
...getSpringValues({
dampingRatio: 1,
period: 0.2,
}),
from: 0,
to: 20,
onUpdate: () => { },
});
Now we have a getSpringValues method that accepts human-readable attenuation coefficient and period of oscillation and returns the mass of the spring, the coefficient of friction and the coefficient of elasticity.This method is in the sandbox at the link below, you can try to use it instead of the usual system properties:https://playcode.io/590645Vue Spring Animation
To conveniently use Spring animations in Vue, you can do two things: write a wrapper component or the composition-api method .Spring animation through component
Let's write a component that abstracts the use of the Spring class.Before writing it, imagine how we want to use it:<template>
<div>
<SpringWrapper
:mass="1"
:tension="170"
:friction="14"
:animationProps="{ x, y }"
:animate="true"
v-slot="{ x: newX, y: newY }"
>
<div :style="{ transform: `translate(${newX}px, ${newY}px)` }" />
</SpringWrapper>
</div>
</template>
<script>
import SpringWrapper from 'path/to/SpringWrapper';
export default {
components: {
SpringWrapper,
},
data() {
return {
x: 0,
y: 0,
};
},
mounted() {
setTimeout(() => {
this.x = 100;
this.y = 100;
}, 1000);
},
};
</script>
We would like to:- You could set the properties of the spring
- In the animationProps parameter, it was possible to specify the fields that we want to animate
- The animate parameter passed the value whether animationProps should be animated
- Whenever fields change in animationProps, they are changed using Spring animation and transferred to the scoped slot
For full functionality, the component must still be able to change the speed of the spring, but we will not do this in order not to complicate the code.We begin development:const SpringWrapper = {
props: {
friction: { type: Number, default: 10 },
tension: { type: Number, default: 270 },
mass: { type: Number, default: 1 },
animationProps: { type: Object, required: true },
animate: { type: Boolean, required: true },
},
data: () => ({
actualAnimationProps: null,
}),
created() {
this.actualAnimationProps = this.animationProps;
this._springs = {};
},
watch: {
animationProps: {
deep: true,
handler(animationProps) {
const {
friction, tension, mass,
animate, actualAnimationProps, _springs,
} = this;
if (!animate) {
this.actualAnimationProps = animationProps;
return;
}
Object.entries(animationProps).forEach((([key, value]) => {
const _spring = _springs[key];
const actualValue = actualAnimationProps[key];
if (_spring && !_spring.isFinished) {
_spring.to = value;
return;
}
const spring = new Spring({
friction,
tension,
mass,
initVelocity: 0,
from: actualValue,
to: value,
onUpdate: ({ value }) => {
actualAnimationProps[key] = value;
},
});
spring.startAnimation();
_springs[key] = spring;
}));
}
},
animate(val) {
const { _springs } = this;
if (!val) {
Object.values(_springs).forEach((spring) => {
spring.stopAnimation();
})
this.actualAnimationProps = this.animationProps;
}
},
},
render() {
const { $scopedSlots, actualAnimationProps } = this;
return $scopedSlots.default(actualAnimationProps)[0];
},
};
Sandbox with a component and an example of its use:https://playcode.io/591686/Spring animation via composition-api method
In order to use composition-api, which will appear in Vue 3.0, we need the composition-api package .Add it to ourselves:npm i @vue/composition-api
import Vue from 'vue'
import VueCompositionApi from '@vue/composition-api';
Vue.use(VueCompositionApi);
Now let's think about how we would like to see our animation method:<template>
<div>
<div :style="{ transform: `translate(${x}px, ${y}px)` }" />
</div>
</template>
<script>
import useSpring from 'path/to/useSpring';
export default {
setup() {
const { x, y, animate } = useSpring({
mass: 1,
tension: 170,
friction: 10,
x: 0,
y: 0,
});
setTimeout(() => {
x.value = 100;
y.value = 100;
}, 1000);
return {
x,
y,
animate,
};
},
};
</script>
We would like to:- You could set the properties of the spring
- The function took an object with the fields we want to animate as input.
- The function returned computed wrappers. Setters will accept the values we want to animate as input; getters will return an animated value
- The function returned animate variable, which will be responsible for whether we need to play the animation or not.
As in the case of the component, we will not add customization of the spring speed so as not to complicate the code.We begin to make a method:import { reactive, computed, ref, watch } from '@vue/composition-api';
import { Spring } from '@cag-animate/core';
const useSpring = (initialProps) => {
const {
mass, tension, friction,
animate: initialAnimate,
...restInitialProps
} = initialProps;
const actualProps = reactive(restInitialProps);
const springs = {};
const mirrorProps = {};
Object.keys(initialProps).forEach(key => {
mirrorProps[key] = computed({
get: () => actualProps[key],
set: val => {
const _spring = springs[key];
const actualValue = actualProps[key];
if (!mirrorProps.animate.value) {
actualProps[key] = val;
return
}
if (_spring && !_spring.isFinished) {
_spring.to = val;
return;
}
const spring = new Spring({
friction,
tension,
mass,
initVelocity: 0,
from: actualValue,
to: val,
onUpdate: ({ value }) => {
actualProps[key] = value;
},
});
spring.startAnimation();
springs[key] = spring;
},
});
});
mirrorProps.animate = ref(initialAnimate || true);
watch(() => mirrorProps.animate.value, (val) => {
if (!val) {
Object.entries(springs).forEach(([key, spring]) => {
spring.stopAnimation();
actualProps[key] = spring.to;
});
}
});
return mirrorProps;
};
export default useSpring;
Sandbox with composition-api method and an example of its use:https://playcode.io/591812/The last word
In the article, we examined the basic features of Spring animations and implemented them in Vue. But we still have room for improvements - in Vue-components you can add the ability to add new fields after initialization, add a change in the speed of the spring.True, writing your Spring in real projects is not necessary at all: the Popmotion library already has a ready-made Spring implementation . You can write a wrapper for Vue, as we did above.Thank you for reading to the end!Used materials