Olá Habr!Há muito tempo, queria adicionar animações do Spring a qualquer projeto. Mas fiz isso apenas para projetos React usando react-spring, porque não sabia de mais nada.Mas, finalmente, decidi descobrir como tudo funciona e escrever minha implementação!Se você também quiser usar as animações da Spring em todos os lugares, vá abaixo do gato. Lá você encontrará alguma teoria, implementando o Spring em JS puro e implementando a animação do Spring no Vue usando components e composition-api.TL; DR
Implementando o Spring Animation em JS: https://playcode.io/590645/ .Você precisa adicionar animações do Spring ao seu projeto - pegue o Spring da biblioteca Popmotion .Você precisa adicionar animações do Spring ao seu projeto Vue - pegue o Spring da biblioteca Popmotion e escreva um wrapper sobre ele.Para que servem as animações de primavera?
As animações css convencionais na web usam uma duração e uma função para calcular a posição periodicamente.As animações de primavera usam uma abordagem diferente - física da primavera.Para animação, definimos as propriedades físicas do sistema: a massa da mola, o coeficiente de elasticidade da mola, o coeficiente de atrito do meio. Depois, esticamos a mola para a distância que precisamos animar e a liberamos.Nos projetos de front-end, vejo essas vantagens nas animações do Spring:1. Você pode definir a velocidade inicial da animação.Se o site processar furtos com os dedos ou arrastar com o mouse, as animações de objetos parecerão mais naturais se você as definir com a velocidade inicial, como velocidade de furto ou velocidade do mouse.
Animação CSS que não retém a velocidade do mouse
Animação de mola, na qual a velocidade do mouse é transmitida para a primavera2. Você pode alterar o ponto final da animaçãoSe, por exemplo, você deseja animar um objeto para o cursor do mouse, não pode ajudar com CSS - quando você atualiza a posição do cursor, a animação é inicializada novamente, o que causará empurrões é visível.Não existe esse problema com as animações de mola - se você precisar alterar o ponto final da animação, esticamos ou compactamos a mola no recuo desejado.
Um exemplo de animação de um elemento para um cursor do mouse; topo - animação CSS, parte inferior - animação de primaveraFísica da primavera
Pegue uma mola comum com uma carga. Ela ainda está em repouso.Puxe-a um pouco para baixo. Uma força elástica (), que busca retornar a mola à sua posição original. Quando liberamos a primavera, ela começará a balançar ao longo do sinusóide:Adicione força de atrito. Seja diretamente proporcional à velocidade da mola () Nesse caso, as oscilações decairão com o tempo:Queremos que nossas animações de primavera sejam animadas da mesma maneira que uma mola oscila quando exposta à elasticidade e ao atrito.Consideramos a posição da primavera para animação
No contexto das animações, a diferença entre o valor inicial e o final da animação é a tensão inicial da primavera. .Antes do início da animação, recebemos: massa da primavera, coeficiente de elasticidade da mola , coeficiente de atrito do meio tensão da mola velocidade inicial .A posição da animação é calculada várias dezenas de vezes por segundo. O intervalo de tempo entre os cálculos da animação é chamado.Durante cada novo cálculo, temos a velocidade da mola e tensão da mola no último intervalo de animação. Para o primeiro cálculo da animação, .Começamos a calcular a posição da animação para o intervalo!De acordo com a segunda lei de Newton, a soma de todas as forças aplicadas é igual à massa do corpo multiplicada por sua aceleração:
Duas forças atuam em uma mola - força elástica e força de atrito:
Vamos anotar as forças com mais detalhes e encontrar a aceleração:
Depois disso, podemos encontrar uma nova velocidade. É igual à soma da velocidade no intervalo de animação anterior e a aceleração multiplicada pelo intervalo de tempo:
Conhecendo a velocidade, você pode encontrar a nova posição da mola:
Temos a posição correta, mostre-a ao usuário!Uma vez exibido, inicie um novo intervalo de animação. Para um novo intervalo, .Vale a pena parar a animação quando e tornam-se muito pequenas - neste momento as oscilações da primavera são quase invisíveis.JS Spring Animation
Temos as fórmulas necessárias, resta escrever nossa implementação: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 com código de classe Spring e um exemplo de uso:https://playcode.io/590645Melhoria menor na animação de primavera
Há um pequeno problema com a nossa classe Spring - será diferente a cada vez com novos começos da animação.Aberto em um telefone celular antigo -será igual a 46 milissegundos, nosso limite máximo. Aberto em um computador poderoso -será de 16 a 17 milissegundos.Mudandosignifica que a duração da animação e as alterações no valor animado serão ligeiramente diferentes a cada vez.Para impedir que isto aconteça, nós podemos fazer isso:Façacomo um valor fixo. Com um novo intervalo de animação, teremos que calcular quanto tempo se passou desde o último intervalo e quantos valores fixos estão nele. Se o tempo não é divisível por, transfira o restante para o próximo intervalo de animação.Então calculamos e quantas vezes obtivemos valores fixos. Último valormostrando ao usuário.Exemplo:tomemos como 1 milissegundo, 32 milissegundos passados entre o último e o novo intervalo de animação.Temos que calcular a física 32 vezes, e ; últimodeve ser mostrado ao usuário.É assim que o método doAnimationTick se parecerá: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;
}
}
Este método de cálculo da física usa a mola de reação, neste artigo você pode ler mais.Sobre o coeficiente de atenuação e o período de oscilação intrínseca
Observando as propriedades do sistema - a massa da carga, o coeficiente de elasticidade e o coeficiente de atrito do meio - não está totalmente claro como a mola se comporta. Ao criar animações Spring, essas variáveis são selecionadas aleatoriamente até que o programador esteja satisfeito com a "elasticidade" da animação.No entanto, na física existem algumas variáveis para uma mola amortecida; olhando para elas, você pode dizer como a mola se comporta.Para encontrá-los, voltemos à nossa equação da segunda lei de Newton:
Nós a escrevemos como uma equação diferencial:
Pode ser reescrito como:
Onde - a frequência das oscilações da mola sem ter em conta a força de atrito, - coeficiente de atenuação.Se resolvermos esta equação, obteremos várias funçõesque determinam a posição da mola versus o tempo. Colocarei a solução embaixo do spoiler para não esticar muito o artigo, vamos direto ao resultado.Solução de equação, :
https://www.youtube.com/watch?v=uI2xt8nTOlQ:
:
:
1.
.
:
— , :
:
.
, 0.
2.
.
:
— , :
:
, 0. ,
.
, , , 0 ,
. , ?
3.
.
:
— , :
:
, 0.
,
0, .. .
.
At obtemos uma equação que descreve as vibrações amortecidas de uma mola. Menos, menor a força de atrito e as vibrações mais visíveis.Atobtemos a equação com atrito crítico, ou seja, a equação na qual o sistema retorna à posição de equilíbrio sem hesitação, da maneira mais rápida.Atobtemos uma equação na qual o sistema retorna à posição de equilíbrio sem hesitação. Isso será mais lento do que com; com aumento a taxa de convergência para a posição de equilíbrio diminui.Exemplos de animações para vários . Velocidade de animação reduzida por 4 vezes.Para além, há outra característica compreensível das oscilações da mola - o período das oscilações da mola sem levar em consideração o atrito:
Temos dois parâmetros:→, que determina quanto a animação flutuará.Valores possíveis0 a 1.menos, as vibrações mais visíveis que a mola terá. Em 0, o sistema não terá atrito, a mola irá oscilar sem atenuação; em 1, a mola convergirá para a posição de equilíbrio sem hesitação.Valores maiores que 1 não são necessários para nós - eles funcionam como 1, apenas em vão arrastam nossa animação.→que dirá quanto tempo a animação vai durar.Deve-se ter em mente o parâmetrofala sobre a duração de uma única vibração da mola sem levar em conta a força de atrito, e não sobre a duração total da animação.Agora podemos derivar o coeficiente de elasticidade e o coeficiente de atrito do meio, sabendo e e assumindo que :
Implementamos essa lógica no código: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: () => { },
});
Agora, temos um método getSpringValues que aceita o coeficiente de atenuação legível por humanos e período de oscilação e retorna a massa da mola, o coeficiente de atrito e o coeficiente de elasticidade.Este método está na sandbox no link abaixo, você pode tentar usá-lo em vez das propriedades usuais do sistema:https://playcode.io/590645Vue Spring Animation
Para usar convenientemente as animações do Spring no Vue, você pode fazer duas coisas: escrever um componente de wrapper ou o método de composição-API .Animação de primavera através do componente
Vamos escrever um componente que abstraia o uso da classe Spring.Antes de escrever, imagine como queremos usá-lo:<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>
Nós gostaríamos de:- Você pode definir as propriedades da primavera
- No parâmetro animationProps, foi possível especificar os campos que queremos animar
- O parâmetro animate passou o valor se animationProps deve ser animado
- Sempre que os campos mudam em animationProps, eles são alterados usando a animação Spring e transferidos para o slot com escopo definido
Para funcionalidade completa, o componente ainda deve poder alterar a velocidade da mola, mas não faremos isso para não complicar o código.Começamos o desenvolvimento: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 com um componente e um exemplo de uso:https://playcode.io/591686/Animação de primavera pelo método de composição-API
Para usar o composition-api, que aparecerá no Vue 3.0, precisamos do pacote composition-api .Adicione a nós mesmos:npm i @vue/composition-api
import Vue from 'vue'
import VueCompositionApi from '@vue/composition-api';
Vue.use(VueCompositionApi);
Agora vamos pensar em como gostaríamos de ver nosso método de animação:<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>
Nós gostaríamos de:- Você pode definir as propriedades da primavera
- A função pegou um objeto com os campos que queremos animar como entrada.
- A função retornou invólucros computados. Os setters aceitarão os valores que queremos animar como entrada; getters retornará um valor animado
- A função retornou a variável animada, que será responsável por executar ou não a animação.
Como no caso do componente, não adicionaremos personalização da velocidade da mola para não complicar o código.Começamos a fazer um método: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 com o método composition-api e um exemplo de uso:https://playcode.io/591812/A última palavra
No artigo, examinamos os recursos básicos das animações do Spring e os implementamos no Vue. Mas ainda temos espaço para melhorias - nos componentes do Vue, você pode adicionar a capacidade de adicionar novos campos após a inicialização, adicionar uma alteração na velocidade da primavera.É verdade que escrever seu Spring em projetos reais não é necessário: a biblioteca Popmotion já possui uma implementação pronta do Spring . Você pode escrever um invólucro para o Vue, como fizemos acima.Obrigado por ler até o fim!Materiais usados