Acho que a maioria das pessoas se contenta em apenas usar jQuery para esses tipos de tarefas. Apesar disso, ainda acho que isso pode ser útil.
O que essa função faz é mudar ao element.scrollTop
longo de um período de tempo, de modo que, no final da duração desejada, ela chegue a um valor alvo desejado.
A única vantagem que isso pode ter sobre a animação do jQuery é que ela retorna uma promessa que é cumprida no final da animação ou rejeitada se a animação for interrompida.
Explicarei o pensamento por trás disso primeiro e depois mostrarei a implementação.
Explicação
Em primeiro lugar, para fazer a animação funcionar, precisamos saber para cada ponto no tempo, como devemos definir o valor de scrollTop.
Suponha que a animação comece na hora 3000 e termine na hora 3500 e, para essa duração, o scrollTop precise mudar de 140 para 250.
Agora estamos no tempo 3120. Qual é o valor de scrollTop?
Se fizéssemos uma interpolação linear simples, a matemática seria mais ou menos assim:
interpolated_time = 120/500; // how much time since we started, over how much overall time
element.scrollTop = 140 + (110 * interpolated_time); // 110 being the distance we want to cover
Dedique um minuto para ter certeza de que entendeu de onde vem a matemática.
Agora, na verdade, essa fórmula funcionaria principalmente, exceto que a animação não pareceria tão natural. Precisamos criar um efeito de atenuação, uma interpolação linear mais simples sozinha não vai funcionar.
Procurei um pouco e encontrei isto: http://en.wikipedia.org/wiki/Smoothstep
Esta é uma função de interpolação com suavização / atenuação:
var smooth_step = function(start, end, point) {
if(point <= start) { return 0; }
if(point >= end) { return 1; }
var x = (point - start) / (end - start); // interpolation
return x*x*(3 - 2*x);
}
Com isso, o cálculo do scrollTop em um determinado momento now
é feito assim:
var point = smooth_step(start_time, end_time, now);
var scrollTop = start_top + (distance * point);
Isso é normal, mas retornaria um número de ponto flutuante. No DOM, você não pode definir o valor scrollTop para um float; ele será arredondado para um número inteiro. Então, para evitar confusão, é melhor contornarmos também:
var scrollTop = Math.round(start_top + (distance * point));
Além disso, quero que a função retorne uma promessa. Esta promessa é cumprida quando terminamos a animação ou rejeitada se a animação for interrompida.
Como sabemos que fomos interrompidos?
Nós mantemos o controle de onde pensamos que deveríamos estar se nossa animação estiver rodando bem, e se não estivermos naquele lugar, então algo não está funcionando: provavelmente o uso rolou de forma diferente, ou outro pedaço de código está tentando nos anima de uma maneira diferente. Portanto, neste caso, apenas abortamos nossa animação e reconhecemos a interrupção.
Também é possível que a animação não esteja a correr da forma que queremos sem sermos interrompidos: se atingirmos o limite / limite e não conseguirmos avançar mais. Isso é fácil de detectar: após alterarmos scrollTop, verificamos imediatamente seu valor (durante o mesmo tick
): se não mudou para o valor desejado, provavelmente significa que não pode mais se mover! Na verdade, não tenho certeza se isso funciona em todos os navegadores, mas funciona no Chrome e no Firefox mais recentes, e isso é bom o suficiente para mim.
A implementação
/**
Smoothly scroll element to the given target (element.scrollTop)
for the given duration
Returns a promise that's fulfilled when done, or rejected if
interrupted
*/
var smooth_scroll_to = function(element, target, duration) {
target = Math.round(target);
duration = Math.round(duration);
if (duration < 0) {
return Promise.reject("bad duration");
}
if (duration === 0) {
element.scrollTop = target;
return Promise.resolve();
}
var start_time = Date.now();
var end_time = start_time + duration;
var start_top = element.scrollTop;
var distance = target - start_top;
// based on http://en.wikipedia.org/wiki/Smoothstep
var smooth_step = function(start, end, point) {
if(point <= start) { return 0; }
if(point >= end) { return 1; }
var x = (point - start) / (end - start); // interpolation
return x*x*(3 - 2*x);
}
return new Promise(function(resolve, reject) {
// This is to keep track of where the element's scrollTop is
// supposed to be, based on what we're doing
var previous_top = element.scrollTop;
// This is like a think function from a game loop
var scroll_frame = function() {
if(element.scrollTop != previous_top) {
reject("interrupted");
return;
}
// set the scrollTop for this frame
var now = Date.now();
var point = smooth_step(start_time, end_time, now);
var frameTop = Math.round(start_top + (distance * point));
element.scrollTop = frameTop;
// check if we're done!
if(now >= end_time) {
resolve();
return;
}
// If we were supposed to scroll but didn't, then we
// probably hit the limit, so consider it done; not
// interrupted.
if(element.scrollTop === previous_top
&& element.scrollTop !== frameTop) {
resolve();
return;
}
previous_top = element.scrollTop;
// schedule next frame for execution
setTimeout(scroll_frame, 0);
}
// boostrap the animation process
setTimeout(scroll_frame, 0);
});
}
Exemplos de uso
Encontre alguma página da web com um corpo longo, abra o console dev e cole a definição da função nele.
Em seguida, tente o seguinte:
smooth_scroll_to(document.body, 600, 2000);
No Chrome, isso deve rolar suavemente o corpo em 600 pixels por 2 segundos.
Observação: para Firefox, você deve usar .document.documentElement
Agora vamos tentar algo mais divertido: encadear várias animações de rolagem!
smooth_scroll_to(document.body, 1600, 2000).then(function() {
return smooth_scroll_to(document.body, 800, 2000);
}).then(function() {
return smooth_scroll_to(document.body, 3100, 2000);
}).catch(function(error) {
console.log("Sequence cancelled:", error)
})
Isso criaria uma sequência de animações para cima e para baixo e, quando qualquer uma delas for interrompida, a sequência inteira também será interrompida!
Assim é o poder das promessas.
Experimente: execute a sequência e role manualmente no meio dela para interrompê-la. A sequência será interrompida e você verá a seguinte mensagem no console:
Sequence cancelled: interrupted
Então aí está. Espero que isso seja útil para você, ou pelo menos, espero que você tenha aprendido algo novo hoje!