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