Javascript: Рисуем с помощью кривых Безье

    Доброго времени суток, хабражители!
    Мне очень нравятся барочные элементы. В очередной раз встретив подобный узор на одном из сайтов, представил, как бы он чудесно смотрелся в анимации, картинка бы ожила. Тем более с приходом html5 оживление должно стать гораздо проще в реализации. Но как по точкам нарисовать кривую? Тут как раз кстати вспомнилась кривая Безье!

    На уроках векторной графики я люто ненавидел кривые Безье. Принцип ее работы казался магическим, непостижимым и, как бы не сказать грубее, нелогичным. Складывалось ощущение, что сама кривая не знает как извернуться, и как наглый котяра растягивалась как ей удобнее, а не как мне надо.

    На самом деле, как всегда, надо было лишь уделить немного времени теории. Все оказалось просто и довольно интересно. В итоге получилась реализация кривых на яваскрипте с использованием canvas.
    Кому интересно как строится эта извилистая бестия добро пожаловать под кат!

    Немного теории


    Начнем с принципа построения. Кривая Безье строится по нескольким опорным точкам. Образно говоря: кривая начинается в нулевой опорной точке, начинает двигаться к первой, но вдруг замечает вторую — начинает тяготеть к ней, плавно меняет свой маршрут в ее сторону, а тут на горизонте появляется третья, еще более привлекательная… И так пройдя стороной мимо всех точек, кривая останавливает свой выбор на последней опорной точке, куда и приходит. Путь получается как у алкоголика идущего под утро домой.
    image

    От лирики перейдем к суровой математике.

    Теория нагло заимствована из Википедии:

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

    image, где

    n — количество опорных точек;
    i — номер опорной точки;
    t — шаг на котором мы считаем положение кривой. К примеру, при построении кривой по 100 точкам, шаг будет 0,01 (не опорным, а точкам на самой кривой);
    Р — в нашем случае координата опорной точки;
    b(t) — базисная функция кривой Безье. Этот коэффициент, определяет вес опорной точки. Является собственно полином Бернштейна:

    image

    , где image — число сочетаний из n по i, где n — степень полинома, i — порядковый номер опорной вершины.

    На первом и последнем шагах значение полинома Бернштейна равно 1, объяснение здесь. На середину кривой наибольшее влияние оказывают средние опорные точки, в первой трети — опорные точки первой трети и так далее. Полином Бернштейна принимает значения от 0 до 1.

    И так, чтобы посчитать координату кривой Безье нам надо:
    1. Посчитать вес опорной точки;
    2. Умножить вес на координату этой опорной точки;
    3. Повторить шаги 1-2 для всех опорных точек;
    4. Сложить получившиеся значения — это и будет координата кривой.


    С теорией вроде разобрались, переходим к практике.

    Ура! Практика.



    Считаем базисную функцию:

    // i - номер вершины, n - количество вершин, t - положение кривой (от 0 до 1)
    function getBezierBasis(i, n, t) {
    	// Факториал
    	function f(n) {
    		return (n <= 1) ? 1 : n * f(n - 1);
    	};
    	
    	// считаем i-й элемент полинома Берштейна
    	return (f(n)/(f(i)*f(n - i)))* Math.pow(t, i)*Math.pow(1 - t, n - i);
    }
    


    Далее получаем координаты кривой.
    Построить кривую Безье можно в трехмерном, четырехмерном пространстве и так далее, но мы остановимся на плоскости.

    // arr - массив опорных точек. Точка - двухэлементный массив, (x = arr[0], y = arr[1])
    // step - шаг при расчете кривой (0 < step < 1), по умолчанию 0.01
    function getBezierCurve(arr, step) {
    	if (step == undefined) {
    		step = 0.01;
    	}
    	
    	var res = new Array()
    	
    	for (var t = 0; t < 1 + step; t += step) {
    		if (t > 1) {
    			t = 1;
    		}
    		
    		var ind = res.length;
    		
    		res[ind] = new Array(0, 0);
    		
    		for (var i = 0; i < arr.length; i++) {
    			var b = getBezierBasis(i, arr.length - 1, t);
    			
    			res[ind][0] += arr[i][0] * b;
    			res[ind][1] += arr[i][1] * b;
    		}
    	}
    	
    	return res;
    }
    


    Рисуем кривую:

    // ctx - rendering context холста, arr - массив точек по которым строим кривую
    // delay - задержка перед отрисовкой следующей точки, pause - пауза перед началом  рисования,
    function drawLines(ctx, arr, delay, pause) {
    	if (delay == undefined) {
    		delay = 10;
    	}
    	
    	if (pause == undefined) {
    		pause = delay;
    	}
    	var i = 0;
    	
    	function delayDraw() {
    		if (i >= arr.length - 1) {
    			return;
    		}
    		
    		ctx.moveTo(arr[i][0],arr[i][1]);
    		ctx.lineTo(arr[i+1][0],arr[i+1][1]);
    		ctx.stroke();
    	
    		++i;
    		
    		setTimeout(delayDraw, delay);
    	}
    	
    	setTimeout(delayDraw, pause);
    }
    


    Пора пробовать:

    drawC = document.getElementById('bezier');
    drawC.width = document.width - 30;
    drawC.height = document.height - 30;
    		
    if (drawC && drawC.getContext) {
    	ctx = drawC.getContext('2d');
    	ctx.fillStyle="#33CC99";
    	ctx.lineWidth=0.1;
    			
    	var flow; // Массив координат кривой
    	var arr = new Array();
    
    	arr[0] = new Array(0, 100);
    	arr[1] = new Array(100, 80);
    	arr[2] = new Array(150, 150);
    	arr[3] = new Array(200, 155);
    	flow = getBezierCurve(new Array(arr[0], arr[1], arr[2], arr[3]), 0.01);
    	drawLines(ctx, flow, 10);
    }
    


    Пример на jsfiddle

    Ссылки по теме:
    Статья на javascript.ru

    upd: Стандарт HTML5 поддерживает методы quadraticCurveTo и bezierCurveTo для построения кривых Безье по трем и четырем точкам соответственно. Спасибо lany.
    Метки:
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 17
    • +3
      Пример выдаёт пустой экран. (FF, Chrome, Ubuntu). И как-то привычнее видеть исходники либо на jsfiddle, либо на github.
      • +2
        Пример нормально отображается, Chrome, Ubuntu
        • 0
          Windows 7, работает только в Chrome. В FF и IE9 пустой экран.
          • 0
            Каюсь, не проверил. Поправил.
            • 0
              Windows 7-64, Opera 12.12 — полёт нормальный.
      • 0
        Уже видел данный эффект вот в этом интерактивном клипе. Там в конце нужно рисовать какой-то текст или картинку, которые потом очень красиво расцветают =)
        • 0
          Как подсказывает инфографика, сайт был создан в августе 2011. Хотел узнать как это у них получилось. Теперь узнал, благодаря вашей статье =)
        • 0
          Спасибо, надо будет попробовать.
          Когда отделяешь дизайн как конструирование от живописи, это идет на пользу и первому и второму. Буду дизайнить программно.
          • НЛО прилетело и опубликовало эту надпись здесь
            • 0
              У кривых Безье целая куча вкусных особенностей. Например, чтобы получить аффинное преобразование кривой Безье, надо применить преобразование к контрольным точкам, а затем построить кривую Безье по полученным точкам. Скажем, с дугой эллипса (заданной центром, полуосями, начальным и конечным углом) вы так не повеселитесь.
              • 0
                C аффинными преобразованиями действительно просто. Даже алгоритм distort по четырем точкам нашел.
                Но алгоритма нелинейного искажения (по типу как сделан warp или envelope distort в Иллюстраторе) не могу найти.
                Может кто сталкивался?
            • +3
              Хм… А чем не устроила готовая функция Canvas context.bezierCurveTo? Или третьего порядка кажется мало? Обычно для практических целей хватает. Или просто захотелось всё руками сделать? По крайней мере, укажите в статье, что второй и третий порядок поддерживаются стандартом, а то ведь вас кто-нибудь почитает и будут использовать велосипеды.
              • +1
                Не знал. Спасибо, добавлю. Интересно было с самой кривой разобраться.
              • 0
                На уроках векторной графики я люто ненавидел кривые Безье. Принцип ее работы казался магическим, непостижимым и, как бы не сказать грубее, нелогичным.

                Позор таким преподавателям, которые не могут нормально объяснить тему и отбивают у студентов всякое желание учится! Или все же дело в студенте?
                • +1
                  Ну в итоге разобрался же. Даже статью вот написал.
                • 0
                  for (var t = 0; t < 1 + step; t += step) { if (t > 1) { t = 1; }

                  ->

                  for (var t = 0; t <= 1; t += step) {

                  ?
                  • 0
                    Нет, надо чтобы t заканчивалось именно единицей, не больше. Если t будет > 1, то полином Берштейна также будет > 1 и кривая уплывет в сторону.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.