Недавно я преподаю студентам, первое занятие в прошлую субботу было посвящено JavaScript-анимации, включая несколько простых анимаций, таких как равномерные или равномерные движения ускорения/замедления, а также сложные комбинированные анимации. И основные принципы анимации, по моемупредыдущая статьяУже есть подробные вступления. Здесь я хочу поговорить о том, как мы можем разработать более простые API для современных браузеров для реализации анимационных последовательностей.
Библиотека анимации на основе обещаний
Так называемая последовательность анимаций означает, что следующая анимация может воспроизводиться после завершения предыдущей анимации, поэтому удобно использовать несколько анимаций для достижения различных сложных эффектов. И нам нетрудно подумать, что для достижения этой цели очень хорошим решением будет реализовать интерфейс анимации в виде промиса:
let animator = new Animator(2000, function(p){
let tx = -100 * Math.sin(2 * Math.PI * p),
ty = -100 * Math.cos(2 * Math.PI * p);
block.style.transform = 'translate('
+ tx + 'px,' + ty + 'px)';
});
block.addEventListener('click', async function(evt){
let i = 0;
//noprotect
while(1){
await animator.animate()
block.style.background = ['red','green','blue'][i++%3];
}
});
Приведенный выше пример выглядит очень лаконично и элегантно в современных браузерах, поддерживающих async/await. Если вы хотите быть совместимым со старыми браузерами, это не сложно, просто нужноПолифилл для обещания es6или представитьсторонняя библиотекаВот и все. Давайте посмотрим на другой пример:
var a1 = new Animator(1000, function(p){
var tx = 100 * p;
block.style.transform = 'translateX('
+ tx + 'px)';
});
var a2 = new Animator(1000, function(p){
var ty = 100 * p;
block.style.transform = 'translate(100px,'
+ ty + 'px)';
});
var a3 = new Animator(1000, function(p){
var tx = 100 * (1-p);
block.style.transform = 'translate('
+ tx + 'px, 100px)';
});
var a4 = new Animator(1000, function(p){
var ty = 100 * (1-p);
block.style.transform = 'translateY('
+ ty + 'px)';
});
block.addEventListener('click', async function(){
await a1.animate();
await a2.animate();
await a3.animate();
await a4.animate();
});
С промисами такие последовательные движения очень просты. Так как же реализовать эту библиотеку анимации?
Реализация
На самом деле всю библиотеку реализовать несложно, просто инкапсулируйте базовую анимацию в виде промиса.
Но здесь, чтобы быть совместимым со старыми версиями браузеров, мы сначала инкапсулируем некоторые основные функции:
function nowtime(){
if(typeof performance !== 'undefined' && performance.now){
return performance.now();
}
return Date.now ? Date.now() : (new Date()).getTime();
}
мы говоримАнимация — это функция времени., поэтому нам нужна простая функция получения времени. в новомСпецификация requestAnimationFrame, временная метка параметра обратного вызова кадраDOMHighResTimeStampobject, что является более точным, чем Date (может быть точным до наносекунд). Поэтому сначала мы используем performance.now() для получения времени.Если браузер не поддерживает performance.now(), мы понизим версию до использования Date.now().
Далее мы полифилим requestAnimationFrame:
if(typeof global.requestAnimationFrame === 'undefined'){
global.requestAnimationFrame = function(callback){
return setTimeout(function(){ //polyfill
callback.call(this, nowtime());
}, 1000/60);
}
global.cancelAnimationFrame = function(qId){
return clearTimeout(qId);
}
}
Затем конкретная реализация Animator:
function Animator(duration, update, easing){
this.duration = duration;
this.update = update;
this.easing = easing;
}
Animator.prototype = {
animate: function(){
var startTime = 0,
duration = this.duration,
update = this.update,
easing = this.easing,
self = this;
return new Promise(function(resolve, reject){
var qId = 0;
function step(timestamp){
startTime = startTime || timestamp;
var p = Math.min(1.0, (timestamp - startTime) / duration);
update.call(self, easing ? easing(p) : p, p);
if(p < 1.0){
qId = requestAnimationFrame(step);
}else{
resolve(self);
}
}
self.cancel = function(){
cancelAnimationFrame(qId);
update.call(self, 0, 0);
reject('User canceled!');
}
qId = requestAnimationFrame(step);
});
},
ease: function(easing){
return new Animator(this.duration, this.update, easing);
}
};
module.exports = Animator;
При построении Animator можно передать три параметра: первый — это общая продолжительность анимации, а второй — событие обновления каждого кадра анимации, где атрибуты элемента могут быть изменены для достижения анимации, а третий параметрeasing. Второй параметр, обратный вызов события обновления, предоставляет два параметра: один — ep, который представляет собой процесс анимации после смягчения, а другой — p, который представляет собой процесс анимации без смягчения, Значения ep и p начинаются с 0 и перейти к 1. конец. (Зачем использовать ep и p, впредыдущий урок анимацииуже объяснил. )
В Animator есть метод анимации объекта, который возвращает обещание.Когда анимация завершена, ее обещание разрешается, и пользователь также может вызвать метод отмены до того, как обещание будет разрешено, так что его обещание будет отклонено.
Итак, очень просто мыОбернув аниматор как метод интерфейсом, возвращающим Promise, который реализует последовательность анимации. Хотя его реализация проста, его функция очень мощная, а код анимации, реализованный с его помощью, также очень элегантен:
var a1 = new Animator(1414, function(p){
var ty = 200 * p * p;
block.style.transform = 'translateY('
+ ty + 'px)';
});
var a2 = new Animator(1414, function(p){
var ty = 200 - 200 * p * (2-p);
block.style.transform = 'translateY('
+ ty + 'px)';
});
block.addEventListener('click', async function(){
//noprotect
while(1){
await a1.animate();
await a2.animate();
}
});
Мы также предоставляем метод замедления (версия 0.2.0+), который может передавать новое замедление и возвращать новый объект Animator, чтобы мы могли расширять наши анимационные эффекты на основе исходной анимации:
var easeInOutBack = BezierEasing(0.68, -0.55, 0.265, 1.55);
//easeInOutBack
var a1 = new Animator(2000, function(ep,p){
var x = 200 * ep;
block.style.transform = 'translateX(' + x + 'px)';
}, easeInOutBack);
var a2 = a1.ease(p => easeInOutBack(1 - p)); //reverse a1
block.addEventListener('click', async function(){
await a1.animate();
await a2.animate();
});
Как насчет CSS3?
Действительно, многие анимации можно реализовать с помощью CSS3. Однако анимация JavaScript и анимация CSS3 имеют разные характеристики и сценарии использования. В общем, анимация CSS3 подходит для любой простой анимации, предназначенной исключительно для представления. Хотя он также может предоставить базовый метод композиции анимации (с временем animationEnd, но стандартизация запаздывает), он по-прежнему неудобен в работе, и для управления им также требуется JavaScript. Некоторые библиотеки анимации используют метод понижения версии. Те, которые могут использовать анимацию CSS3, используют анимацию CSS3, а те, которые не могут быть автоматически понижены до анимации JavaScript. Это хороший метод, но он также имеет свои преимущества и недостатки. Поскольку анимации CSS3 связаны с управлением свойствами элементов, JavaScript более гибок. Подобно нашей инкапсулированной анимационной библиотеке, она на самом деле предоставляет низкоуровневый API, и операция выполняется тольковремяа такжерасписание, не связывает какие-либо элементы, атрибуты или другие элементы представления, поэтому его можно использовать для управления DOM, Canvas, SVG, аудио/видеопотоками и даже другими асинхронными действиями. Кроме того, если вам нужна какая-то другая точная обработка движения во время анимации, вы все равно должны использовать анимацию JavaScript вместо анимации CSS3.
Суммировать
Простая библиотека анимации, реализованная с помощью Promise, может хорошо выполнять комбинированные последовательные анимации, она лаконична и элегантна с кодом async/await, обладает очень хорошей расширяемостью и может сочетать очень мощные анимационные эффекты. Я считаю, что это будет основная реализация анимации JavaScript в браузерах в будущем.
Наконец, можно получить доступGitHub repoПолучите последний код.