Во фронтенд-разработке кривые Безье повсюду:
- Его можно использовать для рисования кривых.В svg и canvas изначально предоставленные кривые рисуются с использованием кривых Безье.
- Его также можно использовать для описания алгоритма смягчения, устанавливая css
transition-timing-function
Атрибуты, вы можете использовать кривые Безье для описания расчета плавности перехода - Почти все интерфейсные библиотеки 2D или 3D графики и диаграмм (echarts, d3, three.js) используют кривые Безье.
В этой статье я собираюсь начать с реализации очень простого эффекта анимации кривой, чтобы помочь вам полностью понять, что такое кривая Безье и каковы ее характеристики.В статье есть немного математических формул, но все они очень просты: ).
Нажмите здесь, чтобы просмотретьонлайн демо
Прежде чем писать код, давайте разберемся, что такое кривая Безье.
Кривая Безье
Кривая Безье является очень важным параметром кривой в компьютерной графике.Описывает кривую через уравнение.В соответствии с высшим порядком уравнения она делится на линейную кривую Безье и квадратичную кривую Безье.,кубическую кривую Безье и кривую более высокого порядка. Кривые Безье.
Ниже приводится подробное введение в более часто используемые квадратичные кривые Безье и кубические кривые Безье.
Квадратичная кривая Безье
Квадратичная кривая Безье состоит из трех точекP0
,P1
,P2
Для определения эти точки также называют контрольными точками. Уравнение кривой:
Это уравнение на самом деле имеет геометрический смысл, а это означает, что кривую можно нарисовать, выполнив следующие шаги:
- Выбери один
0-1
изt
стоимость - пройти через
P0
а такжеP1
Рассчитать точкуQ0
,Q0
существуетP0
P1
по прямой иlength( P0, Q0 ) = length( P0, P1 ) * t
- Аналогично, по
P1
а такжеP2
РассчитатьQ1
, так чтоlength( P1, Q1 ) = length( P1, P2 ) * t
- Повторите этот шаг еще раз,
Q1
а такжеQ2
РассчитатьB
, так чтоlength( Q0, Q1 ) = length( Q0, B ) * t
.B
точка на текущей кривой
Примечание: вышеlength
Указывает длину между двумя точками
С уравнением кривой мы напрямую подставляем конкретноеt
стоимость может быть рассчитанаB
.
еслиt
значение от0
Переход к1
, непрерывно подсчитывая баллыB
, можно получить квадратичную кривую Безье:
На холсте метод рисования квадратичной кривой Безье таков:
ctx.quadraticCurveTo( p1x, p1y, p2x, p2y )
вp1x, p1y, p2x, p2y
для двух последних контрольных точек (P1
а такжеP2
), который по умолчанию использует начальную точку текущего пути в качестве контрольной точки (P0
).
кубическая кривая Безье
Кубическая кривая Безье требует четырех точекP0
,P1
,P2
,P3
Для определения уравнение кривой имеет вид
По аналогии,t
значение от0
Переход к1
, можно нарисовать кубическую кривую Безье:
На холсте метод рисования кубической кривой Безье таков:
ctx.bezierCurveTo( p1x, p1y, p2x, p2y, p3x, p3y )
вp1x, p1y, p2x, p2y, p3x, p3y
для последних трех контрольных точек (P1
,P2
а такжеP3
), который по умолчанию использует начальную точку текущего пути в качестве контрольной точки (P0
).
Характеристики кривых Безье
За кубической кривой Безье стоят кривые Безье более высокого порядка, и процесс их рисования также более сложен.
кривая Безье четвертой степени
Рисунок: Квадратичная кривая Безьепятая кривая Безье
Рисунок: Пятикратная кривая БезьеМы можем заключить, что кривые Безье обладают несколькими важными характеристиками:
- Кривая Безье порядка n требует n+1 точек для определения
- Кривые Безье гладкие
- Начальная и конечная точки кривой Безье касаются линии, соединяющей соответствующие контрольные точки.
рисовать кривые Безье
Ознакомившись с основными понятиями, поговорим о том, как рисовать кривые Безье.
Для простоты,Мы решили использовать квадратичные кривые Безье.
Давайте не будем сначала рассматривать анимацию, давайте упростим задачу: для заданной начальной и конечной точек нам нужно реализовать функцию, которая может рисовать кривую.
То есть нам нужно реализовать функциюdrawCurvePath
, В дополнение к контексту рендеринга CTX (студенты, которые не знают, что такое CTX, может ознакомиться с основными концепциями холста), он принимает три параметра, которые являются тремя контрольными точками кривой квадратичной Bezier. Мы перемещаем элемент управления в стиле за пределами функции,drawCurvePath
Используется только для рисования путей.
/**
* 绘制二次贝赛尔曲线路径
* @param {Object} ctx
* @param {Array<number>} p0
* @param {Array<number>} p1
* @param {Array<number>} p2
*/
function drawCurvePath( ctx, p0, p1, p2 ) {
// ...
}
Как упоминалось ранее, в холсте метод рисования квадратичной кривой Безье таков:quadraticCurveTo
, поэтому этот метод можно выполнить всего в две строки.
/**
* 绘制二次贝赛尔曲线路径
* @param {CanvasRenderingContext2D} ctx
* @param {Array<number>} p0
* @param {Array<number>} p1
* @param {Array<number>} p2
*/
function drawCurvePath( ctx, p0, p1, p2 ) {
ctx.moveTo( p0[ 0 ], p0[ 1 ] );
ctx.quadraticCurveTo(
p1[ 0 ], p1[ 1 ],
p2[ 0 ], p2[ 1 ]
);
}
Это завершает основной метод рисования квадратичных кривых Безье.
Но есть небольшая проблема с дизайном такой функции
Если мы делаем графическую библиотеку, мы хотим предоставить пользователям возможность рисовать кривые.
Для пользователя он просто хочет нарисовать кривую между заданной начальной точкой и конечной точкой.Кривая, которую он хочет получить как можно красивее, но он не хочет заботиться о конкретных деталях реализации.Если ему нужно дайте третью точку, используйте. Для пользователя будет определенная стоимость обучения (по крайней мере, нужно выяснить, что такое кривая Безье).
Увидев это, вы можете запутаться.Даже квадратичная кривая Безье требует трех контрольных точек.Как нарисовать кривую только с начальной и конечной точками.
Мы можем выбрать точку на вертикальной биссектрисе начальной и конечной точек в качестве третьей контрольной точки, и мы можем предоставить пользователю параметр для управления степенью кривизны кривой, Теперь функция становится такой
/**
* 绘制一条曲线路径
* @param {CanvasRenderingContext2D} ctx
* @param {Array<number>} start 起点
* @param {Array<number>} end 终点
* @param {number} curveness 曲度(0-1)
*/
function drawCurvePath( ctx, start, end, curveness ) {
// ...
}
мы используемcurveness
указать степень кривизны кривой, то есть степень отклонения третьей контрольной точки. Это облегчает вычисление промежуточной точки.
Полная функция теперь выглядит так:
/**
* 绘制一条曲线路径
* @param {Object} ctx canvas渲染上下文
* @param {Array<number>} start 起点
* @param {Array<number>} end 终点
* @param {number} curveness 曲度(0-1)
*/
function drawCurvePath( ctx, start, end, curveness ) {
// 计算中间控制点
var cp = [
( start[ 0 ] + end[ 0 ] ) / 2 - ( start[ 1 ] - end[ 1 ] ) * curveness,
( start[ 1 ] + end[ 1 ] ) / 2 - ( end[ 0 ] - start[ 0 ] ) * curveness
];
ctx.moveTo( start[ 0 ], start[ 1 ] );
ctx.quadraticCurveTo(
cp[ 0 ], cp[ 1 ],
end[ 0 ], end[ 1 ]
);
}
Да, это то, что несколько строк, то мы можем нарисовать кривую через него, код выглядит следующим образом
<!DOCTYPE html>
<html lang="en">
<head>
<title>draw curve</title>
</head>
<body>
<canvas id="canvas" width="800" height="800"></canvas>
<script>
var canvas = document.getElementById( 'canvas' );
var ctx = canvas.getContext( '2d' );
ctx.lineWidth = 2;
ctx.strokeStyle = '#ff0000';
ctx.beginPath();
drawCurvePath(
ctx,
[ 100, 100 ],
[ 200, 300 ],
0.2
);
ctx.stroke();
function drawCurvePath( ctx, start, end, curveness ) {
// ...
}
</script>
</body>
</html>
Рисуем анимацию кривой Безье
Наконец-то мы подошли к основной части статьи, наша цель не в том, чтобы нарисовать статическую кривую, мы хотим нарисовать кривую с эффектом перехода.
Упрощение проблемы заключается в том, что функция, которую мы хотим нарисовать, также принимает другой параметр, который представляет собой процент отрисовываемой кривой. Мы регулярно вызываем эту функцию, увеличиваем параметр в процентах и можем рисовать анимацию.
Мы добавляем параметрpercent
для представления процента функция теперь выглядит так:
/**
* 绘制一条曲线路径
* @param {Object} ctx canvas渲染上下文
* @param {Array<number>} start 起点
* @param {Array<number>} end 终点
* @param {number} curveness 曲度(0-1)
* @param {number} percent 绘制百分比(0-100)
*/
function drawCurvePath( ctx, start, end, curveness, percent ) {
// ...
}
Но холст обеспечиваетquadraticCurveTo
Методы могут нарисовать только полную вторичную кривую Байеса, нет возможности контролировать только ее часть.
использовать после покраскиclearRect
Стереть часть? Это невозможно, потому что трудно определить степень стирания. Если ширина линии кривой относительно широкая, также необходимо обеспечить, чтобы граница стирания была перпендикулярна концу кривой, и проблема становится очень сложной.
Теперь снова посмотрите на картинку
Можем ли мы поставитьpercent
Этот параметр понимается какt
значение, а затем использовать уравнение кривой Безье, чтобы вычислить все точки в середине и соединить их прямыми линиями, чтобы имитировать рисование части кривой Безье?
метод первый
Мы больше не используем предоставленный холстquadraticCurveTo
Чтобы нарисовать кривую, ряд точек вычисляется с помощью уравнения кривой Безье, и кривая моделируется прямой линией с несколькими сегментами.
Преимущество этого в том, что мы можем легко контролировать объем чертежа.
Тогда реализация функции становится такой:
/**
* 绘制一条曲线路径
* @param {Object} ctx canvas渲染上下文
* @param {Array<number>} start 起点
* @param {Array<number>} end 终点
* @param {number} curveness 曲度(0-1)
* @param {number} percent 绘制百分比(0-100)
*/
function drawCurvePath( ctx, start, end, curveness, percent ) {
var cp = [
( start[ 0 ] + end[ 0 ] ) / 2 - ( start[ 1 ] - end[ 1 ] ) * curveness,
( start[ 1 ] + end[ 1 ] ) / 2 - ( end[ 0 ] - start[ 0 ] ) * curveness
];
ctx.moveTo( start[ 0 ], start[ 1 ] );
for ( var t = 0; t <= percent / 100; t += 0.01 ) {
var x = quadraticBezier( start[ 0 ], cp[ 0 ], end[ 0 ], t );
var y = quadraticBezier( start[ 1 ], cp[ 1 ], end[ 1 ], t );
ctx.lineTo( x, y );
}
}
function quadraticBezier( p0, p1, p2, t ) {
var k = 1 - t;
return k * k * p0 + 2 * ( 1 - t ) * t * p1 + t * t * p2; // 这个方程就是二次贝赛尔曲线方程
}
Затем вы можете установить таймер, время от времени вызывать этот метод и увеличивать процентное значение.
Для более плавной анимации мы используемrequestAnimationFrame
вместо таймера
<!DOCTYPE html>
<html lang="en">
<head>
<title>draw curve</title>
</head>
<body>
<canvas id="canvas" width="800" height="800"></canvas>
<script>
var canvas = document.getElementById( 'canvas' );
var ctx = canvas.getContext( '2d' );
ctx.lineWidth = 2;
ctx.strokeStyle = '#000';
var percent = 0;
function animate() {
ctx.clearRect( 0, 0, 800, 800 );
ctx.beginPath();
drawCurvePath(
ctx,
[ 100, 100 ],
[ 200, 300 ],
0.2,
percent
);
ctx.stroke();
percent = ( percent + 1 ) % 100;
requestAnimationFrame( animate );
}
animate();
function drawCurvePath( ctx, start, end, curveness, percent ) {
// ...
}
</script>
</body>
</html>
Полученные результаты:
Это в основном удовлетворяет наши потребности, но у него есть проблема:
Тест найден, сделайте его один разlineTo
раз и однаждыquadraticCurveTo
примерно в то же время, ноquadraticCurveTo
Вам нужно нарисовать кривую только один раз и использоватьlineTo
Это занимает десятки раз.
Другими словами, рисование кривой таким образом снижает производительность в десятки раз по сравнению с нашей предыдущей реализацией. Вы можете не почувствовать разницы при рисовании одной кривой, но если вам нужно рисовать тысячи кривых одновременно, производительность сильно пострадает.
Способ второй
Есть ли способ сделать этоquadraticCurveTo
Как насчет рисования части полной кривой?
Вернемся к этой картинке снова
В какой-то момент посередине, скажем, t=0,25, это выглядит так:
Заметим, что криваяP0-B
Этот участок также выглядит как кривая Безье, и его контрольные точки становятсяP0,Q0,B
.
Теперь задача решена, осталось только каждый раз считатьQ0,B
, можно получить контрольные точки одной из малых кривых Безье, а затем пройтиquadraticCurveTo
нарисовать его.
код показывает, как показано ниже:
/**
* 绘制一条曲线路径
* @param {Object} ctx canvas渲染上下文
* @param {Array<number>} start 起点
* @param {Array<number>} end 终点
* @param {number} curveness 曲度(0-1)
* @param {number} percent 绘制百分比(0-100)
*/
function drawCurvePath( ctx, start, end, curveness, percent ) {
var cp = [
( start[ 0 ] + end[ 0 ] ) / 2 - ( start[ 1 ] - end[ 1 ] ) * curveness,
( start[ 1 ] + end[ 1 ] ) / 2 - ( end[ 0 ] - start[ 0 ] ) * curveness
];
var t = percent / 100;
var p0 = start;
var p1 = cp;
var p2 = end;
var v01 = [ p1[ 0 ] - p0[ 0 ], p1[ 1 ] - p0[ 1 ] ]; // 向量<p0, p1>
var v12 = [ p2[ 0 ] - p1[ 0 ], p2[ 1 ] - p1[ 1 ] ]; // 向量<p1, p2>
var q0 = [ p0[ 0 ] + v01[ 0 ] * t, p0[ 1 ] + v01[ 1 ] * t ];
var q1 = [ p1[ 0 ] + v12[ 0 ] * t, p1[ 1 ] + v12[ 1 ] * t ];
var v = [ q1[ 0 ] - q0[ 0 ], q1[ 1 ] - q0[ 1 ] ]; // 向量<q0, q1>
var b = [ q0[ 0 ] + v[ 0 ] * t, q0[ 1 ] + v[ 1 ] * t ];
ctx.moveTo( p0[ 0 ], p0[ 1 ] );
ctx.quadraticCurveTo(
q0[ 0 ], q0[ 1 ],
b[ 0 ], b[ 1 ]
);
}
Замените ранее написанную страницу вышеуказанным кодом, и вы можете увидеть, что результат одинаково:
рисовать анимацию
Теперь, когда самая острая проблема решена, мы можем рисовать анимацию. Но эта часть не важна, код выкладывать не буду.
Полный код можно посмотретьздесь
конец
Этот блог закончился, мы говорили о том, как холст рисует кривые и как рисовать эффекты перехода кривых. В следующем блоге я планирую написать о том, как смоделировать эффект свечения источников света на поверхности объектов в холсте.
Адрес моего блога:GitHub.com/Ху Цзюлун/Но…
Здесь я поделюсь своими знаниями и опытом, особенно технологиями canvas/WebGL/svg. Если вы заинтересованы в графическом рисовании переднего плана, вы можете подписаться на мой блог, добавить звезду в закладки и подписаться на просмотр.
Я только недавно перенесла блог на гитхаб, поэтому статей не так много, буду писать дальше!