автор:@ЧеннингХил 🙌, эта статья разрешает исключительное использование общедоступной учетной записи сообщества разработчиков Nuggets, включая, помимо прочего, редактирование, пометку оригинальности и другие права.
предисловие
Давно не виделись, меня зовут Ченнинг
Извините, я не писал новую статью в течение двух месяцев, потому что в последнее время произошло много событий.
В последние несколько дней у меня наконец-то появилась возможность поделиться с вами этой статьей из-за некоторого вдохновения на работе.
Надеюсь, это вдохновит вас или поможет 🙏
иметь сильнуюCanvas, мы можем использовать JavaScript, чтобы управлять им, чтобы легко рисовать различную желаемую графику, и мы также можем использовать JavaScript, чтобы тесно связать взаимодействие пользователя с рисованием холста, и даже использовать наш мозг, чтобы делать различные всевозможные небесные вещи.
Хотя холст помогает нам легко рисовать все виды графики, сам холст неэффект перехода, он не будет меняться при каждом рисовании, так как же заставить двигаться "статический" холст?
Во-первых, давайте разбиратьсяанимацияСмысл сам по себе, обратитесь к Википедии:
анимация(англ. Animation) — серия стационарных твердотельныхизображение(Рамка) непрерывно изменяющиеся с определенной частотой и движущиеся (воспроизводящиеся) со скоростью (например, 16 кадров в секунду), вызывающие невооруженным глазомвизуальный остаточный образпроизведеноиллюзия- ошибочно принимается за картинки или предметы (картинки)Мероприятияпроизведения и их видеотехника.
Как было сказано выше, анимация — это статичная картинка, которая переключается с определенной скоростью, а каждая картинка — это кадр.Пока частота кадров (скорость переключения) достаточно высока, эти статичные изображения невооруженным глазом выглядят так, как будто они движутся. .
Итак, насколько быстро это достаточно быстро?
человеческий глазвизуальный осадокОсобенности: это явление, при котором зрение, создаваемое светом на сетчатке, сохраняется в течение определенного периода времени после того, как свет перестает действовать, что вызвано скоростью реакции зрительных нейронов. Его временная стоимостьдвадцать четвертых секунды.
Итак, чтобы обмануть наши глаза, теоретически, двадцать четвертая секунды - это24 кадраПереключение картинок на этой скорости позволяет добиться эффекта анимации, конечно, чем выше скорость, тем анимация будет более тонкой и плавной.
Обладая этими базовыми знаниями, идея реализации анимации холста становится очень ясной: возьмите результат каждого рисования холста в качестве кадра и выполняйте многократное рисование холста достаточно часто, чтобы добиться эффекта анимации.
В этой статье в качестве примера используется анимация пути, чтобы увидеть, как рисовать некоторую графику на холсте.Анимация пути.
Как - как управлять кадрами анимации
Чтобы реализовать анимационный эффект Canvas, сначала нам нужен метод синхронизации для периодического выполнения рисования Canvas, то есть нам нужно регулярно рисовать каждый кадр нашей анимации.
Когда дело доходит до выбора времени, сначала мы подумаем о двух методах:window.setTimeoutиwindow.setInterval.
Эти два метода конечно могут добиться нужной нам анимации, но я их использовать не собираюсь, еще и потому, что есть более подходящие методы:окно.requestAnimationFrame.
Если вы достаточно знаете JavaScript, вы будете знать, что из-за его однопоточной связи реализация таймера заключается в выполнении обратного вызова таймера после завершения текущей очереди задач, то есть, если время выполнения текущей очереди больше, чем время, установленное таймером, то это время таймера не так надежно.
Возьмите каштан:
Когда время выполнения очереди задач короткое:
Когда время выполнения очереди велико:
Поскольку время таймера — это только ожидаемое время рендеринга, установленное нами, но этот момент времени на самом деле не является моментом времени перерисовки браузера, при отклонении между этими двумя моментами времени может произойти потеря кадров и другие явления.
Это длинное описание.Студенты, у которых есть дух обучения, могут прочитать эту статью, написанную Чжан Синьсюй:Анимация CSS3 настолько сильна, что requestAnimationFrame все еще используется для шерсти?
requestAnimationFrameОбеспечивает более плавный и эффективный способ выполнения анимации, вызывая метод отрисовки кадра анимации только тогда, когда система готова к условию перерисовки.
Другими словами, вызвать метод requestAnimationFrame и отправить запрос в браузер о том, что я хочу выполнить отрисовку кадра анимации, когда браузер захочет перерисовать
Функция обратного вызова, которую мы передали в requestAnimationFrame, будет вызываться, а в нашей функции обратного вызова, пока нужно отрисовывать кадр анимации, будет снова вызываться requestAnimationFrame, а сама функция обратного вызова будет передаваться до тех пор, пока отрисовка анимации не будет завершена.
Использование requestAnimationFrame может показаться немного запутанным, но это не имеет значения, вы должны понять это после прочтения следующего примера.
Выбрав метод синхронизации для управления перерисовкой, давайте начнем анимировать наш путь.
Начните с прямой
Первый — это основной метод рисования прямой линии:
<body>
<div id="executeButton" onclick="handleExecute()">执行</div>
<canvas id="myCanvas" width="800" height="800"></canvas>
<script>
function handleExecute() {
// 获取canvas元素
const canvas = document.querySelector('#myCanvas')
// 获取canvas渲染上下文
const ctx = canvas.getContext('2d')
// 设置线条样式
ctx.strokeStyle = 'rgba(81, 160, 255,1)'
ctx.lineWidth = 3
// 创建路径
ctx.beginPath()
// 移动笔触到(100,100)坐标处
ctx.moveTo(100,100)
// 把线连接到(700,700)这个位置
ctx.lineTo(700,700)
// 把刚刚的路径绘制出来
ctx.stroke()
}
</script>
</body>
портал кода:код spray.IO/product Ninghan…
Это полная прямая линия, так как же сделать анимацию пути?
Есть две идеи:
- Используя несколько путей, представьте, что линия состоит из нескольких сегментов линии, каждый раз, когда сегмент рисуется упорядоченным образом, и каждый рисунок представляет собой кадр анимации.
- Используя один и тот же путь, длина линии, нарисованной в каждом кадре анимации, постепенно увеличивается, пока не достигнет желаемой длины.
Способы реализации двух идей схожи, но есть некоторые отличия, которые нельзя игнорировать: второй рисунок может нуждаться в очистке области холста от предыдущего кадра каждый раз при его перерисовке, иначе непрерывное покрытие приведет к некоторым неожиданные результаты, такие как Когда цвет линии имеет прозрачность, цвет линии будет продолжать углубляться из-за непрерывного покрытия, которое не может обеспечить желаемую прозрачность.
Кроме того, если вы очистите холст, это окажет определенное влияние.Когда вы интенсивно рисуете несколько изображений, очистка холста повлияет на другие изображения, например, другие изображения будут удалены частично или даже полностью.
Чтобы создавать более сложные сцены без учета влияния очистки холста, я решил использовать первую идею для рисования анимации пути в следующих примерах.
Но чтобы более интуитивно увидеть реализацию или разницу между двумя идеями, в этом примере я сначала реализую обе идеи один раз:
Идея 1: Используйте несколько путей
используется в каждом кадреbeginPathМетод создает новый путь (он очистит предыдущий путь в памяти, но будьте уверены, что он не очищает результат рисования), вычисление начальной и конечной точек на пути требует некоторых простых математических и геометрических знаний, и выполняется в конце каждого кадраstrokeметод рисования пути.
прогресс анимацииprogressПредставляет время от начала каждого кадра, когда выполняется рисунок, деленное на установленную нами продолжительность анимации.durationполучить.
прогресс1выполнен. Продолжать звонить, когда прогресс меньше 1requestAnimationFrameчтобы запросить выполнение нашего метода рисования кадров анимации в браузере.
<script>
function handleExecute() {
// 获取canvas元素
const canvas = document.querySelector('#myCanvas')
// 获取canvas渲染上下文
const ctx = canvas.getContext('2d')
// 设置线条样式
ctx.strokeStyle = 'rgba(81, 160, 255,1)'
ctx.lineWidth = 4
ctx.lineJoin = 'round'
// 定义起点和终点的坐标
const startX = 100
const startY = 100
const endX = 700
const endY = 700
let prevX = startX
let prevY = startY
let nextX
let nextY
// 第一帧执行的时间
let startTime;
// 期望动画持续的时间
const duration = 1000
/*
* 动画帧绘制方法.
* currentTime是requestAnimation执行回调方法step时会传入的一个执行时的时间(由performance.now()获得).
* */
const step = (currentTime) => {
// 第一帧绘制时记录下开始的时间
!startTime && (startTime = currentTime)
// 已经过去的时间(ms)
const timeElapsed = currentTime - startTime
// 动画执行的进度 {0,1}
const progress = Math.min(timeElapsed / duration, 1)
// 绘制方法
const draw = () => {
// 创建新的路径
ctx.beginPath()
// 创建子路径,并将起点移动到上一帧绘制到达的坐标点
ctx.moveTo(prevX, prevY)
// 计算这一帧中线段应该到达的坐标点,并且将prevX/Y更新为此值给下一帧使用.
prevX = nextX = startX + (endX - startX) * progress
prevY = nextY = startY + (endY - startY) * progress
// 用直线将刚刚moveTo中的点连接到(nextX,nextY)上
ctx.lineTo(nextX, nextY)
ctx.strokeStyle = `rgba(${81}, ${160}, ${255},${0.25})`
// 把这一帧的路径绘制出来
ctx.stroke()
}
draw()
if (progress < 1) {
requestAnimationFrame(step)
} else {
console.log('动画执行完毕')
}
}
requestAnimationFrame(step)
}
</script>
Портал codePen:код spray.IO/product Ninghan…
Изображение эффекта:
Второй: использовать тот же путь
<script>
function handleExecute() {
// 获取canvas元素
const canvas = document.querySelector('#myCanvas')
// 获取canvas渲染上下文
const ctx = canvas.getContext('2d')
// 定义起点和终点的坐标
const startX = 100
const startY = 100
const endX = 700
const endY = 700
let nextX
let nextY
// 第一帧执行的时间
let startTime;
// 期望动画持续的时间
const duration = 1000
// 创建路径
ctx.beginPath()
// 创建一条子路径,把新的子路径起始点移动到(prevX,prevY)坐标.
ctx.moveTo(startX, startY)
// 设置线条样式
ctx.strokeStyle = `rgba(${81}, ${160}, ${255},${0.25})`
ctx.lineWidth = 4
/*
* 动画帧绘制方法.
* currentTime是requestAnimation执行回调方法step时会传入的一个执行时的时间(由performance.now()获得).
* */
const step = (currentTime) => {
// ctx.clearRect(startX - 4, startY - 4, Math.abs(endX - startY) + 8, Math.abs(endY - startY) + 8)
// 第一帧绘制时记录下开始的时间
!startTime && (startTime = currentTime)
// 已经过去的时间(ms)
const timeElapsed = currentTime - startTime
// 动画执行的进度 {0,1}
const progress = Math.min(timeElapsed / duration, 1)
// 绘制方法
const draw = () => {
// 计算这一帧中线段应该到达的坐标点
nextX = startX + (endX - startX) * progress
nextY = startY + (endY - startY) * progress
// 用直线连接子路径的最后的点到(nextX,nextY)坐标
ctx.lineTo(nextX, nextY)
// 绘制路径(所有子路径都会被绘制一次)
ctx.stroke()
}
draw()
if (progress < 1) {
requestAnimationFrame(step)
} else {
console.log('动画执行完毕')
}
}
requestAnimationFrame(step)
}
</script>
портал кода:код spray.IO/product Ninghan…
Изображение эффекта:
Добавьте функцию смягчения
Поскольку мы хотим написать анимацию и сделать наши анимационные эффекты более реалистичными и богатыми, мы должны применить их кФункция ослабления(easing function), что на самом деле очень часто встречается, когда мы пишем css (его также можно вызвать в csstiming function),Например
transition: all 600ms ease-in-out;
вышеease-in-outЭто нужно, чтобы указать функцию плавности для анимации, и в CSS есть определенные ограничения, которые не поддерживают все функции плавности, или указать кривую Безье (кубическая кривая Безье) для достижения другой функции плавности.
Затем мы также можем добавить функции замедления к нашей линейной анимации пути, и эти функции замедления можно найти в разных местах. Здесь я используюtween.jsФункция смягчения в:GitHub.com/tween is/сумма вопроса…
Существует также множество функций плавности, и часто используемые функции плавности хорошо понятны.amountЭто соответствует нашему прогрессу анимацииprogress, такие как функция квадратичного смягчения Quadratic.In, которая является квадратом прогресса возврата, и на основе простого математического здравого смысла изображение, которое мы видим на интервале [0, 1] x до y=x^2, длинное вот так:
Как вы понимаете, анимация начинается относительно медленно, а затем становится все быстрее и быстрее.
Quadratic.Out почти наоборот, сначала быстро, а потом медленно.
А Quadratic.InOut сначала медленный, потом быстрый и потом медленный.
Мы можем понять это, когда поймем название функции облегчения памяти,easeЭто означает медленно, медленно, а за легкостью следуетinпросто медленно входи,outмедленно уходит,in-outОн входит и выходит медленно.
Тогда в нашем примере прямой линии использование функции плавности тоже очень просто, прогресс — это параметр количества в функции плавности, а результат, полученный функцией плавности, понимается каквычисляемое свойство, чтобы заменить наш исходный прямой прогресс:
let progress = Math.min(timeElapsed / duration, 1)
progress = Easing.Quadratic.In(progress)
портал кода:код spray.IO/product Ninghan…
Изображение эффекта:
Наблюдайте за анимацией кадра в пунктирной линии
Вообще говоря, браузер будет выполнять функцию обратного вызова 60 раз в секунду, что60 кадров(60 кадров в секунду), но браузер будет поддерживать максимально стабильную частоту кадров, то есть она может быть снижена до других значений частоты кадров. Например, при низкой производительности страницы браузер может выбрать понижение до30fps, значение которого снижается, когда контекст браузера не виден4fpsили даже ниже.
Чтобы увидеть вызов requestAniamtionFrame более наглядно и интуитивно, на основе первого метода, используйтеcountПеременная записывает количество вызовов, а countнечетное числобез рисунка, образуя таким образомпунктирная линия:
<script>
function handleExecute() {
// ......
// 期望动画持续的时间
const duration = 1000
let count = 0
const step = (currentTime) => {
!startTime && (startTime = currentTime)
const timeElapsed = currentTime - startTime
const progress = Math.min(timeElapsed / duration, 1)
// 绘制方法
const draw = () => {
ctx.beginPath()
ctx.moveTo(prevX, prevY)
// 计算该次线段绘制的终点,并将prevX/Y更新为此值,给下一次绘制的时候使用
prevX = nextX = startX + (endX - startX) * progress
prevY = nextY = startY + (endY - startY) * progress
if (count % 2 === 0) {
// 设置线条样式
ctx.lineWidth = 20 * progress
ctx.strokeStyle = `rgba(${171 * (1 - progress) + 81}, ${160 * progress}, ${255},1)`
ctx.lineTo(nextX, nextY)
ctx.stroke()
}
}
draw()
if (progress < 1) {
count++
requestAnimationFrame(step)
} else {
console.log('动画执行完毕')
console.log(`${duration}ms内回调执行次数:${count}次`)
}
}
// 向浏览器发送动画执行请求,当浏览器要进行重绘时,会调用我们传入的step方法
requestAnimationFrame(step)
}
</script>
портал кода:код spray.IO/product Ninghan…
Изображение эффекта:
Как видите, когда я установил продолжительность анимации на1Секунды, обратный вызов анимации пошагового кадра в requestAnimationFrame выполняется60раз, то есть достигнув 60 кадров, и можно даже посчитать цветные отрезки на графике.30сегмент, соответствующий интервал пунктирной линии также30абзац, просто60сегмент (не верьте, что вы считаете).
Полилиния
После освоения анимации траектории прямой линии анимация траектории полилинии будет решаться легко.поворотный моментразделить на частипрямая линия.
Способов разделения может быть много, вот одна из моих реализаций.
портал кода:код spray.IO/product Ninghan…
Изображение эффекта:
function handleExecute() {
const canvas = document.querySelector('#myCanvas')
const ctx = canvas.getContext('2d')
// 设置线条样式
ctx.lineWidth = 7
ctx.lineJoin = 'round'
ctx.lineCap = 'round'
// 顺序定义折线上各个转折点的坐标
const keyPoints = [
{x: 250, y: 200},
{x: 550, y: 200},
{x: 250, y: 500},
{x: 550, y: 500},
{x: 250, y: 200}
]
let prevX = keyPoints[0].x
let prevY = keyPoints[0].y
let nextX
let nextY
// 第一帧执行的时间
let startTime;
// 期望动画持续的时间
const duration = 900
// 动画被切分成若干段,每一段所占总进度的比例
const partProportion = 1 / (keyPoints.length - 1)
// 缓存绘制第n段线段的n值,为了在进行下一段绘制前把这一段线段的末尾补齐
let lineIndexCache = 1
/*
* 动画帧绘制方法.
* currentTime是requestAnimation执行回调方法step时会传入的一个执行时的时间(由performance.now()获得).
* */
const step = (currentTime) => {
// 第一帧绘制时记录下开始的时间
!startTime && (startTime = currentTime)
// 已经过去的时间(ms)
const timeElapsed = currentTime - startTime
// 动画执行的进度 {0,1}
let progress = Math.min(timeElapsed / duration, 1)
// 加入二次方缓动函数
progress = Easing.Quadratic.In(progress)
// 描述当前所绘制的是第几段线段
const lineIndex = Math.min(Math.floor(progress / partProportion) + 1, keyPoints.length - 1)
// 当前线段的进度 {0,1}
const partProgress = (progress - (lineIndex - 1) * partProportion) / partProportion
// 绘制方法
const draw = () => {
ctx.strokeStyle = `rgba(${81 + 175 * Math.abs(1 - progress * 2)}, ${160 - 160 * Math.abs(progress * 2 - 1)}, ${255},${1})`
ctx.beginPath()
ctx.moveTo(prevX, prevY)
// 当绘制下一段线段前,把上一段末尾缺失的部分补齐
if (lineIndex !== lineIndexCache) {
ctx.lineTo(keyPoints[lineIndex - 1].x, keyPoints[lineIndex - 1].y)
lineIndexCache = lineIndex
}
prevX = nextX = ~~(keyPoints[lineIndex - 1].x + ((keyPoints[lineIndex]).x - keyPoints[lineIndex - 1].x) * partProgress)
prevY = nextY = ~~(keyPoints[lineIndex - 1].y + ((keyPoints[lineIndex]).y - keyPoints[lineIndex - 1].y) * partProgress)
ctx.lineTo(nextX, nextY)
ctx.stroke()
}
draw()
if (progress < 1) {
requestAnimationFrame(step)
} else {
console.log('动画执行完毕')
}
}
requestAnimationFrame(step)
}
в основном на основеповоротный моментРазделите несколько линейных сегментов, затем разделите общий прогресс на несколько линейных сегментов и пропорцию прогресса каждого линейного сегмента.partProportion(Например, если он разделен на четыре сегмента, то каждый сегмент равен 0,25), а затем вычислить ход текущего сегмента строкиpartProgress, в остальном почти такой же, как метод обработки прямой линии.
Стоит отметить, что метод расчета прогресса текущего сегмента линии:
const partProgress = (progress - (lineIndex - 1) * partProportion) / partProportion
Из-за общего прогрессаprogressОн не является линейным и непрерывным, поэтому отрезок линии прогрессируетpartProgressВысокая вероятность не будет точно равна 1, что означает, что небольшой отрезок в конце каждого отрезка линии не будет отрисован, или начальная точка следующего отрезка линии не является точкой поворота, которую мы определили, когда продолжительность анимации короткий или общий путь относительно короткий. В течение длительного времени это приведет к тому, что прерывистая линия будет идти "ярлыкявление (представьте, что вы играете в гоночную игру, перед шпилькой есть короткий вход, который ведет прямо на другую сторону шпильки).
Для решения этой проблемы я использовалlineIndexCacheпеременная для кэшированияlineIndex, когда нарисован отрезокlineIndexне равноlineIndexCache, то есть при подготовке к рисованию следующего отрезка линии сначала провести небольшой отрезок от конца предыдущего рисунка до соответствующей точки поворота, и обновитьlineIndexCache, а затем начните рисовать следующий сегмент линии.
Таким образом, любое/любое количество поворотных координатных точек может быть отображено в непрерывной анимации траектории.
Конечно, с этой идеей легко сделать даже прерывистую анимацию пути, вы можете попробовать это сами.
круглый
Видя это, анимация пути круга на самом деле очень проста.Конечно, вы можете использовать достаточно фрагментов треугольника, чтобы сложить круг, как рисование круга в webgl, и использовать достаточное количество сегментов линии, чтобы нарисовать «круг», но это немного хлопотно, у холста есть упакованный специальный API для рисования кругов:arc(x, y, radius, startAngle, endAngle, anticlockwise)
, (на самом деле естьarcToметод, но официально он не рекомендуется, так как реализация этого метода немного "ненадежна").
Идея круговой анимации пути состоит в том, чтобы рисовать один из них для каждого кадра анимации.Диапазон угловиздуга.
портал кода:код spray.IO/product Ninghan…
Изображение эффекта:
Основной код:
function handleExecute() {
// 获取canvas元素
const canvas = document.querySelector('#myCanvas')
// 获取canvas渲染上下文
const ctx = canvas.getContext('2d')
// 设置线条样式
ctx.lineWidth = 7
ctx.lineJoin = 'round'
ctx.lineCap = 'round'
// 定义圆心的坐标点
// const center = {x: ctx.canvas.width / 2, y: ctx.canvas.height / 2}
const center = {x: 400, y: 400}
// 定义圆的半径大小
const radius = 200
// 定义起点和终点的角度
const startAngle = 0
const endAngle = 2 * Math.PI
let prevAngle = startAngle
let nextAngle
// 第一帧执行的时间
let startTime;
// 期望动画持续的时间
const duration = 900
/*
* 动画帧绘制方法.
* currentTime是requestAnimation执行回调方法step时会传入的一个执行时的时间(由performance.now()获得).
* */
const step = (currentTime) => {
// 第一帧绘制时记录下开始的时间
!startTime && (startTime = currentTime)
// 已经过去的时间(ms)
const timeElapsed = currentTime - startTime
// 动画执行的进度 {0,1}
let progress = Math.min(timeElapsed / duration, 1)
progress = Easing.Cubic.In(progress)
// 绘制方法
const draw = () => {
// 创建新的路径
ctx.beginPath()
// 计算这一帧中圆弧应该到达的角度
nextAngle = startAngle + (endAngle - startAngle) * progress
// 创建一段圆弧
ctx.arc(center.x, center.y, radius, prevAngle, nextAngle, false)
// 设置渐变的颜色
ctx.strokeStyle = `rgba(${81 + 171 * Math.abs(1 - progress * 2)}, ${160 - 160 * Math.abs(1 - progress * 2)}, ${255},1)`
// 把这一帧的圆弧绘制出来
ctx.stroke()
//将prevAngle更新为这一帧中的nextAngle给下一帧使用
prevAngle = nextAngle
}
draw()
if (progress < 1) {
requestAnimationFrame(step)
} else {
console.log('动画执行完毕')
}
}
requestAnimationFrame(step)
}
Ключом на самом деле является только эта строка:
// 计算这一帧中圆弧应该到达的角度
nextAngle = startAngle + (endAngle - startAngle) * progress
Вы увидите, что это работает так же, как и при анимации прямых путей.
Когда дело доходит до кругов, трудно не думать об дугах, и тогда вы можете подумать ознаменитость....
GIF кадр падает
портал кода:код spray.IO/product Ninghan…
Кривая Безье
холст имеетвторичныйиКубическая **** кривая Безье, им сложно пользоваться, но при достаточном терпении вы сможете рисовать сложную и регулярную графику.
Давайте сначала посмотрим на Canvas API, который их рисует:
Постройте квадратичную кривую Безье:
quadraticCurveTo(cp1x, cp1y, x, y)
cp1x,cp1y
является контрольной точкой,x,y为
конечная точкаПостройте кубическую кривую Безье:
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
cp1x,cp1y
Для контрольной точки один,cp2x,cp2y
Для контрольной точки два,x,y
является конечной точкой.
вы можете понять это какlineToметод, за исключением того, что он должен пройти еще один или дваконтрольная точкаВводятся параметры координат, и основная операция также усложняется.
Если вы не разбираетесь в кривых Безье и интересуетесь ими, можете порекомендовать очень хорошую статью:
Нарисуйте кривую анимацию с помощью холста — глубокое понимание кривых Безье
Не будет ли сложнее анимировать пути с кривыми Безье? Canvas предназначен для рисования полной кривой Безье, и у нас, похоже, нет возможности нарисовать ее часть напрямую.
Поэтому я могу найти только другой способ:
- Метод 1. Рассчитайте точки на кривой Безье и используйте достаточное количество точек для использованияпрямая линиясоедините их.
- Метод 2: наблюдая и анализируя характеристики кривой Безье,Частичная кривая БезьеНайдите контрольные точки в и соответственно нарисуйте эти частичные кривые Безье.
Прежде всего, будь то метод 1 или метод 2, необходимолюбая точка кривой Безьенайти координаты.
Очень трудно заставить себя вывести координаты точки на кривой Безье, но мы можем легко найти ееУравнение кривой, Baidu/google делается за один раз.
P0этокоординаты начальной точки(По умолчанию в холсте это начальная точка текущего пути),P2(квадратный Безье) илиP3(три Безье)Координаты конечной точки, остальные посередине управляют кривой БезьеКоординаты контрольной точки.
По формуле,точка Бэто определенная точка на кривой Безье, которую мы хотим, зная, что это на самом деле достаточно для нас, чтобы использовать метод 1 для рисования их анимации пути, но этот метод уродлив, потому что мы уже знаем, как получить произвольные координаты точек прогресса, будет достаточно очков на пути спрямая линияПросто подключитесь.
Поскольку первый метод не является «элегантным» для реализации, я все еще хочу бросить и бросить и попытаться использоватьВторой методЧто нужно сделать.
Второй метод также требует условия:Контрольные точки на некоторых кривых Безье, я дам ему имя сейчасSC(субконтрольная точка), что означаетдетский контрольный пункт.
тогда этодетский контрольный пунктКак получить его?
Квадратичная кривая Безье
Во-первых, давайте посмотрим наКвадратичная кривая БезьеАнимированное изображение:
Тогда нужно знать, когда вы будете смотреть эту анимацию несколько раз, у вас будет такой видинтуиция:
Детская контрольная точкаP0-P1на этой линии, и его положение соответствует нашему продвижениюprogressСвязанные, теперь смело предполагаем:
sc.x = p0.x + p1.x - p0.x
sc.y = p0.y + p1.y - p0.y
Затем тщательно проверьте его и используйте специальный код для его реализации, чтобы увидеть:
function handleExecute() {
// 计算出子控制点的坐标
function calSC(t) {
SC.x = p0.x + (p1.x - p0.x) * t
SC.y = p0.y + (p1.y - p0.y) * t
}
// 计算出子贝塞尔曲线的终点
function calB(t) {
B.x = Math.pow(1 - t, 2) * p0.x + 2 * t * (1 - t) * p1.x + Math.pow(t, 2) * p2.x
B.y = Math.pow(1 - t, 2) * p0.y + 2 * t * (1 - t) * p1.y + Math.pow(t, 2) * p2.y
}
// 获取canvas元素
const canvas = document.querySelector('#myCanvas')
// 获取canvas渲染上下文
const ctx = canvas.getContext('2d')
// 设置线条样式
ctx.strokeStyle = 'rgba(81, 160, 255,1)'
ctx.lineWidth = 4
ctx.lineJoin = 'round'
// 第一帧执行的时间
let startTime;
// 期望动画持续的时间
const duration = 1000
// 起点
const p0 = {x: 100, y: 500}
// 控制点
const p1 = {x: 200, y: 100}
// 终点
const p2 = {x: 700, y: 500}
// 子控制点(这里初始化的坐标不重要,先设置成p0的值)
const SC = {...p0}
// 子贝塞尔曲线上的终点(这里初始化的坐标不重要,先设置成p0的值)
let B = {...p0}
// 先画一条完整的贝塞尔曲线以验证贝塞尔曲线动画的准确性
ctx.beginPath()
ctx.moveTo(p0.x, p0.y)
ctx.strokeStyle = '#e3e3e3'
ctx.quadraticCurveTo(p1.x, p1.y, p2.x, p2.y)
ctx.stroke()
// 随便画个眼睛(不重要)
function drawEye(color) {
ctx.beginPath()
ctx.strokeStyle = color
ctx.arc(p0.x + 100, p0.y - 50, 50, 0, 2 * Math.PI, false)
ctx.stroke()
ctx.moveTo(p0.x + 300, p0.y - 50)
ctx.arc(p0.x + 250, p0.y - 50, 50, 0, 2 * Math.PI, false)
ctx.stroke()
}
drawEye('rgb(227, 227, 227)')
/*
* 动画帧绘制方法.
* currentTime是requestAnimation执行回调方法step时会传入的一个执行时的时间(由performance.now()获得).
* */
const step = (currentTime) => {
// 第一帧绘制时记录下开始的时间
!startTime && (startTime = currentTime)
// 已经过去的时间(ms)
const timeElapsed = currentTime - startTime
// 动画执行的进度 {0,1}
let progress = Math.min(timeElapsed / duration, 1)
progress = Easing.Quadratic.In(progress)
// 绘制方法
const draw = () => {
ctx.beginPath()
ctx.moveTo(p0.x, p0.y)
// 计算并更新B和SC的坐标
calB(progress)
calSC(progress)
// 用直线将刚刚moveTo中的点连接到(nextX,nextY)上
ctx.quadraticCurveTo(SC.x, SC.y, B.x, B.y)
ctx.strokeStyle = `rgba(${171 * (1 - progress) + 81}, ${160 * progress}, ${255},1)`
ctx.stroke()
// 眼睛渐变色
drawEye(`rgba(${227 - (227 - 81) * progress}, ${227 - (227 - 160) * progress}, ${255},1)`)
}
draw()
if (progress < 1) {
requestAnimationFrame(step)
} else {
console.log('动画执行完毕')
}
}
setTimeout(() => {
requestAnimationFrame(step)
}, 1000)
}
Ядро состоит в том, чтобы извлечь два метода расчета calcB и calSC для вычисления и обновления конечной точки и вспомогательной контрольной точки вспомогательной кривой Безье, а затем использовать API рисования кривой Безье:
ctx.quadraticCurveTo(SC.x, SC.y, B.x, B.y)
портал кода:код spray.IO/product Ninghan…
Перед выполнением анимации я сначала рисую полную кривую Безье серой линией, а затем запускаю анимацию, чтобы увидеть, можно ли ее точно покрыть, чтобы убедиться, что наш метод анимации верен:
благослови бог🙏
кубическая кривая Безье
Для кубической кривой Безье старый способ заключается в том, чтобы сначала посмотреть на ее анимацию:
Я нашел это чувство в квадратичной кривой Безье, поэтому нетрудно почувствовать, где находятся две подконтрольные точки, и сделать смелое предположение:
тогда нам просто нужно добавитьSC1,SC2,SC3и изменитьBДостаточно метода расчета координат, где SC1 и SC2детский контрольный пункт, SC3 используется для вычисления координат SC2.
Эти баллы рассчитываются как:
// 计算出子控制点1的坐标
function calSC1(t) {
SC1.x = p0.x + (p1.x - p0.x) * t
SC1.y = p0.y + (p1.y - p0.y) * t
}
// 计算用于计算子控制点2的坐标的点坐标
function calSC3(t) {
SC3.x = p1.x + (p2.x - p1.x) * t
SC3.y = p1.y + (p2.y - p1.y) * t
}
// 计算出子控制点2的坐标
function calSC2(t) {
SC2.x = SC1.x + (SC3.x - SC1.x) * t
SC2.y = SC1.y + (SC3.y - SC1.y) * t
}
// 计算出子贝塞尔曲线的终点
function calB(t) {
B.x = Math.pow(1 - t, 3) * p0.x + 3 * t * Math.pow(1 - t, 2) * p1.x + 3 * p2.x * Math.pow(t, 2) * (1 - t) + Math.pow(t, 3) * p3.x
B.y = Math.pow(1 - t, 3) * p0.y + 3 * t * Math.pow(1 - t, 2) * p1.y + 3 * p2.y * Math.pow(t, 2) * (1 - t) + Math.pow(t, 3) * p3.y
}
Основной метод покадровой анимации:
ctx.beginPath()
ctx.moveTo(p0.x, p0.y)
// 计算并更新B和SC1、SC2、SC3的坐标
calB(progress)
calSC1(progress)
calSC3(progress)
calSC2(progress)
// 用三次贝塞尔曲线将刚刚moveTo中的点连接到B上
ctx.bezierCurveTo(SC1.x, SC1.y, SC2.x, SC2.y, B.x, B.y)
ctx.strokeStyle = `rgba(${171 * (1 - progress) + 81}, ${160 * progress}, ${255},1)`
ctx.stroke()
Остальное похоже на анимацию пути квадратичной кривой Безье.
портал кода:код spray.IO/product Ninghan…
Давайте снова проверим анимацию пути кубической кривой Безье:
благослови бог еще раз🙏
На самом деле у меня не все сразу получилось, при неправильном выборе подконтрольных точек будут появляться некоторые интересные явления:
Наконец
До сих пор методы анимации пути различной графики холста в основном были общими.На самом деле, это не обязательно используется в повседневной работе.В то же время есть и другие способы завершения анимации пути, такие как css, svg и т. д. .
Причина, по которой я трачу так много времени на написание подобных вещей, в основном состоит в том, что я нахожу это интересным, а во-вторых, в процессе я понимаю как холст, так и некоторые принципы, лежащие в основе анимации.
Спасибо за ваше терпение, чтобы прочитать это далеко.
Конечно, в тексте могут быть неточности и ошибки, и вы можетеКомментарийобщаться со мной.
Все использовано в текстеDemoбыли размещены в:Портал GitHub.
Наконец, я надеюсь, что мои друзья смогутСтавьте лайки, подписывайтесьСанлиан, это все источники мотивации, которыми я могу поделиться🙏