Bonjour, Habr!Je souhaite depuis longtemps ajouter des animations Spring à tous les projets. Mais je ne l'ai fait que pour les projets React utilisant react-spring, car je ne savais rien d'autre.Mais finalement, j'ai décidé de comprendre comment tout cela fonctionne et d'écrire mon implémentation!Si vous souhaitez également utiliser des animations Spring partout, passez sous le chat. Vous y trouverez une théorie, l'implémentation de Spring en JS pur et l'implémentation de l'animation Spring dans Vue en utilisant les composants et la composition-api.TL; DR
Implémentation de Spring Animation dans JS: https://playcode.io/590645/ .Vous devez ajouter des animations Spring à votre projet - prenez Spring dans la bibliothèque Popmotion .Vous devez ajouter des animations Spring à votre projet Vue - prenez Spring dans la bibliothèque Popmotion et écrivez un wrapper dessus.À quoi servent les animations de printemps?
Les animations CSS classiques sur le Web utilisent une durée et une fonction pour calculer la position de temps en temps.Les animations de printemps utilisent une approche différente - la physique du printemps.Pour l'animation, nous définissons les propriétés physiques du système: la masse du ressort, le coefficient d'élasticité du ressort et le coefficient de frottement du milieu. Ensuite, nous étendons le ressort à la distance dont nous avons besoin pour l'animer et le relâchons.Dans les projets front-end, je vois de tels avantages dans les animations Spring:1. Vous pouvez définir la vitesse d'animation initiale.Si le site traite les balayages avec vos doigts ou faites glisser avec la souris, les animations d'objet seront plus naturelles si vous les définissez avec la vitesse initiale comme la vitesse de balayage ou la vitesse de la souris.
Animation CSS qui ne conserve pas la vitesse de la souris
Spring-animation, dans laquelle la vitesse de la souris est transmise au printemps2. Vous pouvez changer le point final de l'animationSi, par exemple, vous souhaitez animer un objet avec le curseur de la souris, vous ne pouvez pas aider avec CSS - lorsque vous mettez à jour la position du curseur, l'animation est à nouveau initialisée, ce qui entraînera des secousses sont visibles.Il n'y a pas un tel problème avec Spring-animations - si vous devez changer le point final de l'animation, nous étirons ou compressons le ressort au retrait souhaité.
Un exemple d'animation d'un élément à un curseur de souris; haut - animation CSS, bas - animation SpringPhysique du printemps
Prenez un ressort ordinaire avec une charge. Elle est toujours au repos.Tirez-la un peu. Une force élastique (), qui cherche à remettre le ressort dans sa position d'origine. Lorsque nous relâchons le ressort, il commencera à se balancer le long de la sinusoïde:Ajoutez une force de friction. Soit directement proportionnelle à la vitesse du ressort () Dans ce cas, les oscillations se désintègrent avec le temps:Nous voulons que nos animations de ressort soient animées de la même manière qu'un ressort oscille lorsqu'il est exposé à l'élasticité et au frottement.Nous considérons la position du ressort pour l'animation
Dans le cadre des animations, la différence entre la valeur initiale et finale de l'animation est la tension initiale du ressort .Avant le début de l'animation, on nous donne: masse de ressort, coefficient d'élasticité du ressort , coefficient de frottement du fluide la tension du ressort , vitesse de démarrage .La position d'animation est calculée plusieurs dizaines de fois par seconde. L'intervalle de temps entre les calculs d'animation est appelé.Lors de chaque nouveau calcul, nous avons la vitesse du ressort et la tension du ressort sur le dernier intervalle d'animation. Pour le premier calcul d'animation, .Nous commençons à calculer la position de l'animation pour l'intervalle!Selon la deuxième loi de Newton, la somme de toutes les forces appliquées est égale à la masse du corps multipliée par son accélération:
Deux forces agissent sur un ressort - force élastique et force de friction:
Nous noterons les forces plus en détail et trouverons l'accélération:
Après cela, nous pouvons trouver une nouvelle vitesse. Elle est égale à la somme de la vitesse dans l'intervalle d'animation précédent et de l'accélération multipliée par l'intervalle de temps:
Connaissant la vitesse, vous pouvez trouver la nouvelle position du ressort:
Nous avons obtenu la bonne position, affichez-la à l'utilisateur!Une fois affiché, démarrez un nouvel intervalle d'animation. Pour un nouvel intervalle, .L'animation mérite d'être arrêtée lorsque et deviennent très petites - en ce moment les oscillations du ressort sont presque invisibles.JS Spring Animation
Nous avons les formules nécessaires, il reste à écrire notre implémentation: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 avec le code de classe Spring et un exemple de son utilisation:https://playcode.io/590645Amélioration mineure de l'animation printanière
Il y a un petit problème avec notre classe de printemps - sera différent à chaque fois avec de nouveaux départs de l'animation.Ouvert sur un vieux téléphone portable -sera égal à 46 millisecondes, notre limite maximale. Ouvert sur un ordinateur puissant -sera de 16 à 17 millisecondes.En changeantsignifie que la durée de l'animation et les modifications de la valeur animée seront légèrement différentes à chaque fois.Pour éviter que cela se produise, nous pouvons le faire:Prenezcomme valeur fixe. Avec un nouvel intervalle d'animation, nous devrons calculer combien de temps s'est écoulé depuis le dernier intervalle et combien de valeurs fixes s'y trouvent. Si le temps n'est pas divisible par, puis transférez le reste à l'intervalle d'animation suivant.Ensuite, nous calculons et autant de fois que nous avons obtenu des valeurs fixes. Dernière valeurmontrer à l'utilisateur.Exemple:prenons comme 1 milliseconde, 32 millisecondes passées entre le dernier et le nouvel intervalle d'animation.Nous devons calculer la physique 32 fois, et ; dernierdoit être montré à l'utilisateur.Voici à quoi ressemblera la méthode doAnimationTick: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;
}
}
Cette méthode de calcul de la physique utilise le ressort de réaction, dans cet article, vous pouvez en savoir plus.A propos du coefficient d'atténuation et de la période d'oscillation intrinsèque
En regardant les propriétés du système - la masse de la charge, le coefficient d'élasticité et le coefficient de frottement du milieu - on ne sait pas très bien comment se comporte le ressort. Lors de la création d'animations Spring, ces variables sont sélectionnées de manière aléatoire jusqu'à ce que le programmeur soit satisfait de la "réactivité" de l'animation.Cependant, en physique, il existe quelques variables pour un ressort amorti, en les regardant, vous pouvez dire comment le ressort se comporte.Pour les trouver, revenons à notre équation de la deuxième loi de Newton:
Nous l'écrivons comme une équation différentielle:
Il peut être réécrit comme:
Où - la fréquence des oscillations du ressort sans tenir compte de la force de frottement, - coefficient d'atténuation.Si nous résolvons cette équation, alors nous obtenons plusieurs fonctionsqui déterminent la position du ressort en fonction du temps. Je vais mettre la solution sous le spoiler pour ne pas trop étirer l'article, on ira directement au résultat.Solution d'équation, :
https://www.youtube.com/watch?v=uI2xt8nTOlQ:
:
:
1.
.
:
— , :
:
.
, 0.
2.
.
:
— , :
:
, 0. ,
.
, , , 0 ,
. , ?
3.
.
:
— , :
:
, 0.
,
0, .. .
.
À nous obtenons une équation qui décrit les vibrations amorties d'un ressort. Moins, plus la force de friction est faible et plus les vibrations sont visibles.Ànous obtenons l'équation avec le frottement critique, c'est-à-dire l'équation dans laquelle le système revient à la position d'équilibre sans hésitation, de la manière la plus rapide.Àon obtient une équation dans laquelle le système revient sans hésitation à la position d'équilibre. Ce sera plus lent qu'avec; avec augmentation le taux de convergence vers la position d'équilibre diminue.Exemples d'animations pour divers . Vitesse d'animation réduite de 4 fois.En plus de, il existe une autre caractéristique compréhensible des oscillations du ressort - la période des oscillations du ressort sans tenir compte du frottement:
Nous avons obtenu deux paramètres:→, qui détermine la variation de l'animation.Valeurs possibles0 à 1.moins, plus les vibrations du ressort seront visibles. A 0, le système n'aura pas de frottement, le ressort oscillera sans atténuation; à 1, le ressort convergera sans hésitation vers la position d'équilibre.Les valeurs supérieures à 1 ne sont pas nécessaires pour nous - elles fonctionnent comme 1, seulement en vain faites glisser notre animation.→qui indiquera la durée de l'animation.Il convient de garder à l'esprit le paramètreparle de la durée d'une seule vibration du ressort sans tenir compte de la force de frottement, et non de la durée totale de l'animation.Maintenant, nous pouvons dériver le coefficient d'élasticité et le coefficient de frottement du milieu, sachant et et en supposant que :
Nous implémentons cette logique dans le 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: () => { },
});
Nous avons maintenant une méthode getSpringValues qui accepte un coefficient d'atténuation lisible par l'homme et période d'oscillation et renvoie la masse du ressort, le coefficient de frottement et le coefficient d'élasticité.Cette méthode est dans le bac à sable sur le lien ci-dessous, vous pouvez essayer de l'utiliser à la place des propriétés système habituelles:https://playcode.io/590645Vue Spring Animation
Pour utiliser facilement les animations Spring dans Vue, vous pouvez faire deux choses: écrire un composant wrapper ou la méthode composition-api .Animation de printemps via le composant
Écrivons un composant qui résume l'utilisation de la classe Spring.Avant de l'écrire, imaginez comment nous voulons l'utiliser:<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>
Nous voudrions:- Vous pouvez définir les propriétés du ressort
- Dans le paramètre animationProps, il était possible de spécifier les champs que nous voulons animer
- Le paramètre animate a transmis la valeur si animationProps doit être animée
- Chaque fois que les champs changent dans animationProps, ils sont modifiés à l'aide de l'animation Spring et transférés vers l'emplacement délimité
Pour une fonctionnalité complète, le composant doit encore pouvoir changer la vitesse du ressort, mais nous ne le ferons pas afin de ne pas compliquer le code.Nous commençons le développement: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 avec un composant et un exemple de son utilisation:https://playcode.io/591686/Animation de printemps via la méthode de composition-api
Pour utiliser composition-api, qui apparaîtra dans Vue 3.0, nous avons besoin du package composition-api .Ajoutez-le à nous:npm i @vue/composition-api
import Vue from 'vue'
import VueCompositionApi from '@vue/composition-api';
Vue.use(VueCompositionApi);
Voyons maintenant comment nous aimerions voir notre méthode d'animation:<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>
Nous voudrions:- Vous pouvez définir les propriétés du ressort
- La fonction a pris un objet avec les champs que nous voulons animer en entrée.
- La fonction a renvoyé des wrappers calculés. Les setters accepteront les valeurs que nous voulons animer en entrée; les getters retourneront une valeur animée
- La fonction a renvoyé la variable animate, qui sera responsable de savoir si nous devons jouer l'animation ou non.
Comme dans le cas du composant, nous n'ajouterons pas de personnalisation de la vitesse du ressort afin de ne pas compliquer le code.Nous commençons à faire une méthode: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 avec la méthode de composition-api et un exemple de son utilisation:https://playcode.io/591812/Le dernier mot
Dans l'article, nous avons examiné les fonctionnalités de base des animations Spring et les avons implémentées dans Vue. Mais nous avons encore de la place pour des améliorations - dans les composants Vue, vous pouvez ajouter la possibilité d'ajouter de nouveaux champs après l'initialisation, d'ajouter un changement dans la vitesse du ressort.Certes, écrire votre Spring dans de vrais projets n'est pas du tout nécessaire: la bibliothèque Popmotion a déjà une implémentation Spring prête à l'emploi . Vous pouvez écrire un wrapper pour Vue, comme nous l'avons fait ci-dessus.Merci d'avoir lu jusqu'au bout!Matériaux utilisés