Я полагаю, что многие студенты пишут
JSВ анимации голова больше, и Сяо Чжан одна из них.Прочитала недавно.animateplusИсходный код, я чувствую, что идеи в нем все еще очень умны, поэтому я сымитировал колесо, оригинальный текст был опубликован вмой блог
JSКаков основной метод создания анимации, т.е.requestAnimationFrame, так что давайте сначала представим его
requestAnimationFrame
window.requestAnimationFrame()Сообщите браузеру, что вы хотите выполнить анимацию, и попросите браузер вызвать указанную функцию обратного вызова, чтобы обновить анимацию перед следующей перерисовкой. Этот метод должен передать функцию обратного вызова в качестве параметра, функция обратного вызова будет выполнена до следующей перерисовки браузера.
Примечание. Если вы хотите продолжить обновление следующего кадра анимации перед следующей перерисовкой браузера, сама функция обратного вызова должна быть вызвана снова.
window.requestAnimationFrame()
Говоря простым языком, это можно понять как анимационную версию.setTimeout, то почему бы и нетsetTimeoutНу, потому что в браузереsetTimeoutмогут быть неточными по разным причинам, иrequestAnimationFrameОдной из особенностей является то, что он будет пытаться выполнять функцию обратного вызова 60 раз в секунду (60 кадров в секунду), за исключением карты компьютера.
Как использовать
let timer;
function foo(now) {
if (now > 1000) {
// 取消下一次动画帧
cancelAnimationFrame(timer);
return;
}
timer = requestAnimationFrame(foo);
}
requestAnimationFrame(foo);
Функция обратного вызова примет параметр, значением которого является текущее время, иperformance.now()такой же
Я вижу это и чувствуюsetTimeoutСпособ использования в основном похоже, в дополнение к времени вызывающего времени - это управление браузером, а такжеsetTimeoutуправление параметром времени
иди, иди, иди
Первый взгляд на эффект, который должен быть достигнут
new Animate({
element: document.querySelector(".box"),
duration: 2000
})
.then({
transform: ["translate(0px)", "translate(500px)"],
background: ["rgb(100, 149, 237)", "rgb(119, 237, 137)"]
})
.then({
duration: 5000,
transform: ["translate(500px,0px)", "translate(0,500px)"],
opacity: [1, 0]
})
.run();
нарисуй 7
функция входа
Так как естьclass, затем используйтеclassнаписать
Во-первых, он может принятьoptionsОбъект как параметр, этот объект имеет 2 значения,elementЗначение представляет элемент, который необходимо анимировать,durationУказывает продолжительность анимации
class Animate {
constructor(options) {
const { element, duration } = options;
// 需要进行动画的元素
this.element = element;
// 动画持续时间
this.duration = duration;
// 用来储存动画的队列,有了它才能做到一个动画结束,另一个动画开始
this.queue = [];
}
}
Добавить анимацию в очередь анимации
Пример, а.thenметод добавляет анимацию в очередь, поэтому вам нужно добавитьthenметод
class Animate {
// ...constructor
then = options => {
// duration 每一个动画可以有自己的持续时间,如果没传这个参数就用默认时间
// keyframes 取其余的参数
const { duration = this.duration, ...keyframes } = options;
// 把传入的参数转换为我们执行动画时需要的形式
const animationObject = createKeyframes(keyframes, { duration });
// 添加进队列
this.queue.push(animationObject);
// 链式调用需要return this
return this;
};
}
есть оченьфокусфункция, то естьcreateKeyframes, что напрямую связано сrequestAnimationFrameКак разбирать нашу анимацию
.then({
transform: ["translate(0px,0px)", "translate(500px,200px)"]
})
// ⬇️ 转换为
[{
// 属性名
propertyName: "transform",
// 属性值的字符串去值拆分
propertyValueStrings: ["translate(", "px)"],
// 属性值的数字
propertyValue: [[0,0], [500,200]]
}]
Принцип состоит в том, чтобы разделить числа и строки, а затемrequestAnimationFrameВычислите число в , а затем объедините его со строкой, чтобы назначить его элементуstyle, так что теперь давайте поговорим о базовой реализации
Реализация createKeyframes
Сначала поговорим о методе
Object.entries
Object.entriesПреобразование объектов в массивы
Object.entries({ transform: ["translate(0px)", "translate(100px)"] })
// ⬇️ 变为
[ [ 'transform', [ 'translate(0px)', 'translate(100px)' ] ] ]
разделить и сопоставить
splitМетод может разделить строку в соответствии с обычным,matchможет соответствовать обычным элементам
"a1b2c3d".split(/\d/g);
// ⬇️ 拆分为
[ 'a', 'b', 'c', 'd' ]
"a1b2c3d".match(/\d/g);
// ⬇️ 匹配
[ '1', '2', '3' ]
Таким образом, принцип заключается в том, чтобы использовать сначалаObject.entriesПройдите объект и используйте значение в немsplitа такжеmatchМетод преобразован, посмотрим конкретную реализацию метода
// 用来拆分和匹配数字的正则
const regExp = /-?\d*\.?\d+/g;
const createKeyframes = (keyframe, options) =>
// 遍历对象,解构出propertyName和values,values是值的数组,即[from,to]的形式
Object.entries(keyframe).map(([propertyName, values]) => {
// 属性值的字符串在from和to的值里都一样,所以直接去第一个来拆分
// 比如像opacity的值是[1,0],这个需要转成字符串
const propertyValueStrings = String(values[0]).split(regExp);
//如["translate(0px,0px)", "translate(500px,200px)"]变成[[0,0], [500,200]]
const propertyValue = values.map(value =>
String(value)
.match(regExp)
.map(Number)
);
// 返回转换的新对象,options是其他的值,比如duration
return {
propertyName,
propertyValueStrings,
propertyValue,
...options
};
});
// 转换试试
createKeyframes({
transform: ["translate(500px,0px)", "translate(0,500px)"],
opacity: [1, 0]
})
// ⬇️ 转换为
[
{
propertyName: 'transform',
propertyValueStrings: [ 'translate(', 'px,', 'px)' ],
propertyValue: [ [500,0], [0,500] ]
},
{
propertyName: 'opacity',
propertyValueStrings: [ '', '' ],
propertyValue: [ [1], [0] ]
}
]
Теперь вы успешно преобразовали параметры в формат анимации, а затем запустили анимацию.
Реализация функции rAF
rAFто естьrequestAnimationFrameАббревиатура , в этой функции нам нужно рассчитать значение текущей анимации в зависимости от продолжительности
const timeTicker = (current, now) => {
if (!current.startTime) current.startTime = now;
current.stopTime = now - current.startTime;
};
class Animate {
// ...constructor
// ...then
rAF = now => {
// 取出队列中的第一个动画
const [animationObject] = this.queue;
// 这里使用some方法的原因是,如果同时执行transform和opacity动画,当一个结束了,另一个也应该结束了,这时候就需要把当前的动画对象从动画队列中推出,所以用some判断动画是否结束
const finished = animationObject.some(current => {
// 标记当前的时间和应该结束的时间
// 因为是值引用,所以改了引用对象原对象也会改
timeTicker(current, now);
// 从当前执行的动画取出持续时间和
const { duration, stopTime } = current;
// 计算动画执行的进度,这个值是一个比例
const progress = duration > 0 ? Math.min(stopTime / duration, 1) : 1;
// 核心方法,将动画值计算后还原为style格式
const assignStyle = resumeStyles(current, progress);
Object.assign(this.element.style, assignStyle);
return progress === 1;
});
// 如果结束了,把当前的动画对象从动画队列中推出
if (finished) this.queue.shift();
// 如果队列中还有动画,继续执行
if (this.queue.length) requestAnimationFrame(this.rAF);
};
}
возобновитьStyles - метод для восстановления значений в формате стиля
resumeStylesФункция также очень важна во всем процессе, без этой функции нельзя присвоить значение элементу
const resumeStyles = (
// 解构出属性名,用于拼接属性值的string数组,值再解构出from和to
// 具体值的格式见上方的createKeyframes函数实现
{ propertyName, propertyValueStrings, propertyValue: [from, to] },
// 进度,用来计算当前值
progress
) => {
// 对propertyValueStrings做reduce操作,计算出style格式
const propertyValue = propertyValueStrings.reduce(
(styles, current, index) => {
// 计算当前动画的值,也就是开始位置 +(结束位置 - 开始位置)* 进度
const getCurrentValue = (a, b) => a + (b - a) * progress;
// 这里需要-1是关键,比如字符串为["translate(", "px,", "px)"],但值为[0,500]
// 所以当string的下标是1的时候,应该对应value的下标0
const valueIndex = index - 1;
const value = getCurrentValue(from[valueIndex], to[valueIndex]);
// 拼接字符串
return styles + value + current;
}
);
return { [propertyName]: propertyValue };
};
Окончательный код, когда закончите
// 正则
const regExp = /-?\d*\.?\d+/g;
// 创建animationObject
const createKeyframes = (keyframe, options) =>
Object.entries(keyframe).map(([propertyName, values]) => {
const propertyValueStrings = String(values[0]).split(regExp);
const propertyValue = values.map(value =>
String(value)
.match(regExp)
.map(Number)
);
return {
propertyName,
propertyValueStrings,
propertyValue,
...options
};
});
// 将值拼接还原style格式
const resumeStyles = (
{ propertyName, propertyValueStrings, propertyValue: [from, to] },
progress
) => {
const propertyValue = propertyValueStrings.reduce(
(styles, current, index) => {
const getCurrentValue = (a, b) => a + (b - a) * progress;
const valueIndex = index - 1;
const value = getCurrentValue(from[valueIndex], to[valueIndex]);
return styles + value + current;
}
);
return { [propertyName]: propertyValue };
};
// 标记时间
const timeTicker = (current, now) => {
if (!current.startTime) current.startTime = now;
current.stopTime = now - current.startTime;
};
class Animate {
constructor(options) {
const { element, duration } = options;
this.element = element;
this.duration = duration;
this.queue = [];
}
// 动画执行函数
rAF = now => {
const [animationObject] = this.queue;
const finished = animationObject.some(current => {
timeTicker(current, now);
const { duration, stopTime } = current;
const progress = duration > 0 ? Math.min(stopTime / duration, 1) : 1;
const assignStyle = resumeStyles(current, progress);
Object.assign(this.element.style, assignStyle);
return progress === 1;
});
if (finished) this.queue.shift();
if (this.queue.length) requestAnimationFrame(this.rAF);
};
// 添加动画
then = options => {
const { duration = this.duration, ...keyframes } = options;
const animationObject = createKeyframes(keyframes, { duration });
this.queue.push(animationObject);
return this;
};
// 开始执行
run = () => requestAnimationFrame(this.rAF);
}
Количество кода не велико, а логика не сложна, но следует отметить, что я попробовал только в Chrome80, и Safari не работает, потому что функции стрелки в классах не поддерживаются. Если вы хотите быть универсальным, тебе все еще нужноbindилиbabel
Главное в том, чтоcreateKeyframesа такжеresumeStylesЭти две функции, если вы понимаете, как работают эти две функции, поздравляю, у вас также есть собственное колесо анимации.
почти все
Все знают, что вCSSАнимация в , будетease-out,ease-in-outС такой опцией наша анимация теперь чисто линейная, и выглядит это странно
На самом деле добавить это к нашему колесу очень просто, нужно только найти формулу
const easings = {
linear: t => t,
easeInQuad: t => t * t,
easeOutQuad: t => t * (2 - t),
easeInOutQuad: t => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
easeInCubic: t => t * t * t,
easeOutCubic: t => --t * t * t + 1,
easeInOutCubic: t =>
t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
easeInQuart: t => t * t * t * t,
easeOutQuart: t => 1 - --t * t * t * t,
easeInOutQuart: t => (t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t),
easeInQuint: t => t * t * t * t * t,
easeOutQuint: t => 1 + --t * t * t * t * t,
easeInOutQuint: t =>
t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t
};
Формула принимает числовой параметр и вычисляет соответствующее значение, с ним анимация перестает быть линейной.
добавь его в наше колесо
class Animate {
constructor(options) {
// 新增一个easing参数
const { element, duration, easing = "linear" } = options;
this.element = element;
this.duration = duration;
this.easing = easing;
// ..
}
rAF = now => {
// ...
const { duration, stopTime, easing } = current;
// ...
// 解构出来并套入easings函数计算
const assignStyle = resumeStyles(current, easings[easing](progress));
// ...
};
then = options => {
const {
duration = this.duration,
easing = this.easing,
...keyframes
} = options;
// 添加到animationObject
const animationObject = createKeyframes(keyframes, { duration, easing });
// ...
};
// .. run
}
при входящемeasingПосле установки параметров производительность анимации будет другой, заинтересованные студенты могут попробовать
Суммировать
Основной метод анимации внутри, я изanimateplusОбобщается в этой библиотеке, но реализация этого колеса отличается от нее, а у нас может быть только одноэлементная анимация, она поддерживает больше функций и эффектов, если интересно, можете перейти к исходникам, только 400 строк
Я надеюсь, что каждый сможет изучить основные методы здесь, а затем упаковать их в свои собственные колеса Желаю всем гладкой работы.
Недавно я также создал публичный аккаунт для записи своих учебных записей.Я надеюсь, что кто-то обратит на это внимание... Я тоже не большой парень, не торопитесь, и я стану большим парнем.