Нарисуйте анимацию кривой с холстом - поглощение глубины кривых Безье

JavaScript Canvas

Во фронтенд-разработке кривые Безье повсюду:

  • Его можно использовать для рисования кривых.В svg и canvas изначально предоставленные кривые рисуются с использованием кривых Безье.
  • Его также можно использовать для описания алгоритма смягчения, устанавливая csstransition-timing-functionАтрибуты, вы можете использовать кривые Безье для описания расчета плавности перехода
  • Почти все интерфейсные библиотеки 2D или 3D графики и диаграмм (echarts, d3, three.js) используют кривые Безье.

В этой статье я собираюсь начать с реализации очень простого эффекта анимации кривой, чтобы помочь вам полностью понять, что такое кривая Безье и каковы ее характеристики.В статье есть немного математических формул, но все они очень просты: ).

160935917f7f0d3f
Реализовать кривую анимацию

Нажмите здесь, чтобы просмотретьонлайн демо

Прежде чем писать код, давайте разберемся, что такое кривая Безье.


Кривая Безье

Кривая Безье является очень важным параметром кривой в компьютерной графике.Описывает кривую через уравнение.В соответствии с высшим порядком уравнения она делится на линейную кривую Безье и квадратичную кривую Безье.,кубическую кривую Безье и кривую более высокого порядка. Кривые Безье.

Ниже приводится подробное введение в более часто используемые квадратичные кривые Безье и кубические кривые Безье.

Квадратичная кривая Безье

Квадратичная кривая Безье состоит из трех точек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, можно получить квадратичную кривую Безье:

s

Рисунок: Процесс рисования квадратичной линии Безье

На холсте метод рисования квадратичной кривой Безье таков:

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).

Характеристики кривых Безье

За кубической кривой Безье стоят кривые Безье более высокого порядка, и процесс их рисования также более сложен.

кривая Безье четвертой степени

Рисунок: Квадратичная кривая Безье

пятая кривая Безье

Рисунок: Пятикратная кривая Безье

Мы можем заключить, что кривые Безье обладают несколькими важными характеристиками:

  1. Кривая Безье порядка n требует n+1 точек для определения
  2. Кривые Безье гладкие
  3. Начальная и конечная точки кривой Безье касаются линии, соединяющей соответствующие контрольные точки.

рисовать кривые Безье

Ознакомившись с основными понятиями, поговорим о том, как рисовать кривые Безье.

Для простоты,Мы решили использовать квадратичные кривые Безье.

Давайте не будем сначала рассматривать анимацию, давайте упростим задачу: для заданной начальной и конечной точек нам нужно реализовать функцию, которая может рисовать кривую.

То есть нам нужно реализовать функцию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Стереть часть? Это невозможно, потому что трудно определить степень стирания. Если ширина линии кривой относительно широкая, также необходимо обеспечить, чтобы граница стирания была перпендикулярна концу кривой, и проблема становится очень сложной.

Теперь снова посмотрите на картинку

s

Можем ли мы поставить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Как насчет рисования части полной кривой?

Вернемся к этой картинке снова

s

В какой-то момент посередине, скажем, 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 ]
    );

}

Замените ранее написанную страницу вышеуказанным кодом, и вы можете увидеть, что результат одинаково:

рисовать анимацию

Теперь, когда самая острая проблема решена, мы можем рисовать анимацию. Но эта часть не важна, код выкладывать не буду.

Полный код можно посмотретьздесь

160935917f7f0d3f

конец

Этот блог закончился, мы говорили о том, как холст рисует кривые и как рисовать эффекты перехода кривых. В следующем блоге я планирую написать о том, как смоделировать эффект свечения источников света на поверхности объектов в холсте.

Адрес моего блога:GitHub.com/Ху Цзюлун/Но…

Здесь я поделюсь своими знаниями и опытом, особенно технологиями canvas/WebGL/svg. Если вы заинтересованы в графическом рисовании переднего плана, вы можете подписаться на мой блог, добавить звезду в закладки и подписаться на просмотр.

Я только недавно перенесла блог на гитхаб, поэтому статей не так много, буду писать дальше!