Урок - Линии и дуги, кривые Бернштейна-Безье

Главная » Курсы » Курс HTML5 Canvas » Урок - Линии и дуги, кривые Бернштейна-Безье

Обучающий онлайн курс
HTML5 Canvas

Лицензия: Копирование запрещено.

Линии и дуги

В отличие от рисования прямоугольников рисование фигур составленных из линий выполняется последовательно в несколько шагов:

  1. Начало рисования.
  2. Рисование с помощью простых функций.
  3. Завершение рисования.

Для выполнения этих этапов используются следующие функции:

  1. beginPath() - используется что бы «начать» серию действий описывающих отрисовку фигуры. Каждый новый вызов этого метода сбрасывает все действия предыдущего и начинает «рисовать» заново.
  2. closePath() - является не обязательным действием и по сути оно пытается завершить рисование проведя линию от текущей позиции к позиции с которой начали рисовать.
  3. stroke() - завершает рисование, обводя фигуру линиями.
  4. fill() - завершает рисование, заливая фигуру сплошным цветом.

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

Для отрисовки используются следующие методы:

  1. moveTo(x, y) - перемещает "курсор" в позицию x, y и делает её текущей
  2. lineTo(x, y) - ведёт линию из текущей позиции "курсора" в указанную, и делает в последствии указанную точку текущей позицией "курсора".
  3. arc(x, y, radius, startAngle, endAngle, anticlockwise) - рисование дуги, где x и y центр окружности, далее начальный и конечный угол, последний параметр указывает направление.
    Углы в функции arc измеряются в радианах, а не в градусах.

    Для конвертации градусов в радианы вы можете использовать следующее выражение на JavaScript:

    var radians = (Math.PI/180)*degrees;

     

  4. quadraticCurveTo(Px, Py, x, y) - рисует квадратичную кривую
  5. bezierCurveTo(P1x, P1y, P2x, P2y, x, y) - рисует кривую Безье

    Пример ниже показывает действие всех функций:

    // при возникновении ошибки выведем сообщение.
    try{
    	// Получить контекст
    	var canvas = document.getElementById('Example1');
    	// Получить 2D контекст.
    	// ! Можете инициализировать только один контекст для каждого элемента.
    	var ctx = canvas.getContext('2d');
    	
    
    ctx.beginPath();
    ctx.arc(100, 100, 50, (Math.PI/180)*(160), (Math.PI/180)*(-160), true);
    ctx.arc(96, 100, 48, (Math.PI/180)*(160), (Math.PI/180)*(-160), true);
    ctx.fill();
    ctx.stroke();
    ctx.moveTo(60, 140);
    ctx.moveTo(140, 140);
    ctx.moveTo(140, 130);
    ctx.moveTo(60, 130);
    ctx.fill();
    ctx.moveTo(60, 140);
    ctx.moveTo(140, 140);
    ctx.moveTo(140, 130);
    ctx.moveTo(60, 130);
    ctx.fill();
    		
    
    }
    catch(err){
    	alert('Ошибка');
    }
    РезультатОбновите браузер. Должно выглядеть так

Рисовние Дуг в Canvas

Кривые Бернштейна-Безье

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

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

  • quadraticCurveTo(Px, Py, x, y)
  • bezierCurveTo(P1x, P1y, P2x, P2y, x, y)

кривые Безье

x и y это точки в которые необходимо перейти, а координаты P(Px, Py) в квадратичной кривой это дополнительные точки которые используются для построения кривой. В кубическо кривой соответственно две дополнительные точки.

Пример двух кривых:

<html>
<body>
	<canvas id='example'>Обновите браузер</canvas>
	<script>
		var example = document.getElementById("example");
		var ctx = example.getContext('2d');
		example.height = 480;
		example.width = 640;
		ctx.beginPath();
		ctx.moveTo(10, 15);
		ctx.bezierCurveTo(75, 55, 175, 20, 250, 15);
		ctx.moveTo(10, 15);
		ctx.quadraticCurveTo(100, 100, 250, 15);
		ctx.stroke();
	</script>
</body>
</html>

Рисование фигур

Источник: https://developer.mozilla.org/ru/%D0%9E%D0%B1%D1%83%D1%87%D0%B5%D0%BD%D0%B8%D0%B5_canvas

В отличие от SVG, canvas поддерживает только одну примитивную форму - прямоугольники. Все другие формы могут быть созданы комбинацией одного или нескольких путей. К счастью, у нас имеется коллекция функций рисования путей, которая делает возможным создание очень сложных форм.

Прямоугольники

Сначала давайте рассмотрим прямоугольник. Есть три функции, рисующие прямоугольники на холсте:
fillRect(x,y,width,height): Рисует заполненный прямоугольник
strokeRect(x,y,width,height): Рисует границы прямоугольника
clearRect(x,y,width,height):Очищает заданную область и делает её полностью прозрачной

Каждая из этих трёх функций принимает одинаковые параметры. x и y описывают позицию на холсте (относительно начала координат) верхнего левого угла прямоугольника. Ну а width и height довольно очевидны. Давайте увидим эти функции в действии.

function draw(){
var canvas = document.getElementById('tutorial');
if (canvas.getContext){
var ctx = canvas.getContext('2d');

ctx.fillRect(25,25,100,100);
ctx.clearRect(45,45,60,60);
ctx.strokeRect(50,50,50,50);
}
}

прямоугольник
Результат должен быть похож на изображение. Функция fillRect рисует большой чёрный квадрат 100x100 пикселей. Функция clearRect убирает квадрат 60x60 пикселей из центра, а в конце strokeRect рисует рамку прямоугольника 50x50 внутри очищенного квадрата. На следующих страницах мы увидим два альтернативных метода функции clearRect, а также рассмотрим то, как менять цвет и штрих рисуемых фигур.

В отличие от функций путей, которые мы увидим в следующей секции, все эти три функции рисуют на холсте немедленно.

Рисование путей

Для рисования фигур с использованием путей нам надо произвести несколько дополнительных действий.

beginPath()
closePath()
stroke()
fill()


Первый шаг в создании пути - вызов метода beginPath. Внутри системы пути хранятся в виде списка суб-путей (линии, дуги и т.п.), вместе образующих фигуру. Каждый раз при вызове этого метода список очищается, и мы можем начинать рисовать новые фигуры.

Вторым шагом является вызов методов, непосредственно создающих рисуемые пути. Вскоре мы их рассмотрим.

Третим и необязательным шагом будет вызов метода closePath. Этот метод пытается закрыть фигуру, рисуя прямую линию от текущей точки до начальной. Если фигура уже была закрыта или нарисована только одна точка, эта функия ничего не делает.

Ну и заключительным шагом будет вызов методов stroke и/или fill. Вызов любого из них прорисовывает фигуру на холсте. stroke используется для рисования незаполненной фигуры (только рамку), в то время как fill рисует залитую фигуру.

Примечание: При вызове метода fill любая открытая фигура будет автоматически закрыта, поэтому необходимости дополнительно вызывать метод closePath нет.

Код, рисующий простую фигуру(треугольник) может выглядеть примерно так:

ctx.beginPath();
ctx.moveTo(75,50);
ctx.lineTo(100,75);
ctx.lineTo(100,25);
ctx.fill();

moveTo

Одна из очень полезных функций, которая фактически ничего не рисует, но является частью списка путей, описанного выше - это функция moveTo. Возможно, вам будет удобнее воспринимать её, как перенесение карандаша с одного участка бумаги на другой.

moveTo(x, y)

Функция moveTo принимает два аргумента: x и y - координаты начала новой линии.

Когда холст (canvas) только инициализован, либо после вызова метода beginPath, начальная точка установлена в позиции (0,0). В большинстве случаев мы используем метод moveTo, чтобы поместить стартовую точку в другое место. Ещё метод moveTo нам пригодится для рисования несвязанных путей (линий). Взгляните на улыбающуюся рожицу справа. Я пометил красными линиями места, где использовался метод moveTo.
Смайл

Вы можете сами проверить, как это работает. Просто вставьте следующий кусочек кода в функцию draw, которая у нас уже есть.

Пример использования moveTo

ctx.beginPath();
ctx.arc(75,75,50,0,Math.PI*2,true); // Внешний круг
ctx.moveTo(110,75);
ctx.arc(75,75,35,0,Math.PI,false); // Рот
ctx.moveTo(65,65);
ctx.arc(60,65,5,0,Math.PI*2,true); // Левый глаз
ctx.moveTo(95,65);
ctx.arc(90,65,5,0,Math.PI*2,true); // Правый глаз
ctx.stroke();


Примечание: удалите метод moveTo, чтобы увидеть соединённые линии.

Примечание: Описание функции arc и её параметров смотрите ниже.

Линии

Для рисования прямых линий мы используем метод lineTo.

lineTo(x, y)

Этот метод принимает два аргумента - x and y, - координаты конечной точки линии. Начальная точка зависит от предыдущих нарисованных путей, причём конечная точка предыдущего пути является начальной точкой следующего. Начальная точка также может быть изменена методом moveTo.

Пример использования lineTo

В следующем примере нарисованы два треугольника - один залитый, другой - очерченный. (Результат виден на изображении справа). Чтобы начать рисовать путь новой фигуры, сначала вызван метод beginPath. Потом мы используем метод moveTo для смещения стартовой точки в желаемую позицию. Потом рисуются две линии, которые образуют две стороны треугольника.
треугольники
Также заметьте разницу между рисованием залитого и очерченного треугольников. Это связано с тем, что залитые фигуры закрываются автоматически. Если бы мы так же поступили с очерченным треугольником, то были бы нарисованы только две линии, а не весь треугольник.

// Залитый треугольник
ctx.beginPath();
ctx.moveTo(25,25);
ctx.lineTo(105,25);
ctx.lineTo(25,105);
ctx.fill();

// Очерченный треугольник
ctx.beginPath();
ctx.moveTo(125,125);
ctx.lineTo(125,45);
ctx.lineTo(45,125);
ctx.closePath();
ctx.stroke();

Дуги

Для рисования дуг или кругов мы используем метод arc. Спецификация также описывает метод arcTo, поддерживаемый Safari, но он реализован в современных Gecko-браузерах.

arc(x, y, radius, startAngle, endAngle, anticlockwise)

Этот метод принимает пять параметров: x и y это координаты центра круга. radius говорит сам за себя. Параметры startAngle и endAngle определяют точки начала и конца арки в радианах. Начальный и конечный углы отмеряются от оси x. Параметр anticlockwise булевый (логический). Если он равняется true, то дуга рисуется против часовой стрелки. Иначе - по часовой.

Внимание: В бета-билдах Firefox последний параметр такой: clockwise. Финальный же релиз будет поддерживать функцию в том виде, в котором она описана выше. Поэтому все скрипты, использующие этот метод в старом виде, при выходе последней версии необходимо будет переделать.

Примечание: Углы в функции arc измеряются в радианах, а не в градусах. Для конвертации градусов в радианы вы можете использовать следующее выражение на JavaScript: var radians = (Math.PI/180)*degrees.

Пример использования arc

Следующий пример чуть более сложный, чем те, которые были до этого. Я нарисовал 12 разных дуг, используя разные углы и заполнения. Если бы я взялся нарисовать таким образом смайлики из предыдущего примера, то, во-первых, получился бы слишком длинный список выражений, а во-вторых, рисуя дуги, я бы нуждался в каждой конкретной стартовой позиции. Для дуг с углами 90, 180 или 270 градусов, как здесь, это не было бы большой проблемой, но для более сложных это была бы слишком сложная задача.
дуги
Два цикла for пробегают по рядам и колонкам рисуемых дуг. Для каждой дуги я начинаю новый путь, используя beginPath. Далее я описал все параметры переменными - так легче понимать, что происходит. Обычно то же самое умещается в одну строку. Координаты x and y должны быть достаточно понятны. radius and startAngle фиксированы. endAngle начинается с 180 градусов (первая колонка) и с каждым циклом увеличивается на 90 градусов, в итоге формируя круг (последняя колонка). Выражение с параметром clockwise приводит к тому, что в первом и третьем ряду дуги нарисованы по часовой стрелке, а во втором и четвёртом - против. Наконец, инструкция if делает в верхней половине очерченные дуги, а в нижней - заполненные.

for (i=0;i<4;i++){
for(j=0;j<3;j++){
ctx.beginPath();
var x = 25+j*50; // x координата
var y = 25+i*50; // y кордината
var radius = 20; // Радиус дуги
var startAngle = 0; // Начальная точка дуги
var endAngle = Math.PI+(Math.PI*j)/2; // Конечная точка дуги
var anticlockwise = i%2==0 ? false : true; // По часовой или против

ctx.arc(x,y,radius,startAngle,endAngle, anticlockwise);

if (i>1){
ctx.fill();
} else {
ctx.stroke();
}
}
}

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

 


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

quadraticCurveTo(cp1x, cp1y, x, y) // НЕ РАБОТАЕТ в Firefox 1.5 (смотри обходной путь ниже)
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)

кривые
Разницу между этими способами легче объяснить, используя картинку. Квардатичная кривая Безье имеет начальную и конечную точки (синие точки на рисунке) и всего одну точку контроля (красная точка), в то время как кубическая кривая Безье использует две точки контроля.

Параметры x и y в обоих методах - координаты конечной точки. cp1x и cp1y - это координаты первой точки контроля, а cp2x и cp2y - координаты второй точки контроля.

Использование квадратичных и кубических кривых Безье может быть довольно сложным, потому что, в отличие от рисования в векторных редакторах, наподобие Adobe Illustrator'а, мы не имеем прямого визуального контроля над тем, что мы делаем. Это усложняет рисование сложных фигур. В следующем примере мы будем рисовать простые округлые фигуры, но при наличии времени и, что самое главное, терпения, можно создать гораздо более сложные формы.

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

// Пример использования квадратичных кривых
ctx.beginPath();
ctx.moveTo(75,25);
ctx.quadraticCurveTo(25,25,25,62.5);
ctx.quadraticCurveTo(25,100,50,100);
ctx.quadraticCurveTo(50,120,30,125);
ctx.quadraticCurveTo(60,120,65,100);
ctx.quadraticCurveTo(125,100,125,62.5);
ctx.quadraticCurveTo(125,25,75,25);
ctx.stroke();

кривая
Можно конвертировать квадратичную кривую Безье в кубическую, вычислив обе точки контроля кубической кривой Безье из единственной точки контроля квадратичной кривой, хотя обратное вычисление НЕ всегда возможно. Точное преобразование кубической кривой в квадратичную возможно только если кубическое выражение(cubic term) равно нулю. Чаще используется метод разбиения (subdivision) для получения приблизительной кубической кривой Безье с использованием множества квадратичных кривых Безье.

Пример использования bezierCurveTo

// пример использования кривых Безье
ctx.beginPath();
ctx.moveTo(75,40);
ctx.bezierCurveTo(75,37,70,25,50,25);
ctx.bezierCurveTo(20,25,20,62.5,20,62.5);
ctx.bezierCurveTo(20,80,40,102,75,120);
ctx.bezierCurveTo(110,102,130,80,130,62.5);
ctx.bezierCurveTo(130,62.5,130,25,100,25);
ctx.bezierCurveTo(85,25,75,37,75,40);
ctx.fill();

кривая

Обход бага в quadraticCurveTo() в Firefox 1.5

В Firefox 1.5 в реализации quadatricCurveTo() имеется баг (ошибка). Функция НЕ рисует квадратичную кривую, а просто вызывает функцию создания кубической кривой bezierCurveTo(), и дважды использует координату единственной квадратичной точки контроля. В этой связи quadatricCurveTo() даёт некорректные результаты. Если вам необходимо использовать quadatricCurveTo(), вы должны самостоятельно преобразовать вашу квадратичную кривую в кубическую, что позволит вам использовать работающий метод bezierCurveTo().

var currentX, currentY; // set to last x,y sent to lineTo/moveTo/bezierCurveTo or quadraticCurveToFixed()

function quadraticCurveToFixed( cpx, cpy, x, y ) {
/*
For the equations below the following variable name prefixes are used:
qp0 is the quadratic curve starting point (you must keep this from your last point sent to moveTo(), lineTo(), or bezierCurveTo() ).
qp1 is the quadatric curve control point (this is the cpx,cpy you would have sent to quadraticCurveTo() ).
qp2 is the quadratic curve ending point (this is the x,y arguments you would have sent to quadraticCurveTo() ).
We will convert these points to compute the two needed cubic control points (the starting/ending points are the same for both
the quadratic and cubic curves.

The equations for the two cubic control points are:
cp0=qp0 and cp3=qp2
cp1 = qp0 + 2/3 *(qp1-qp0)
cp2 = cp1 + 1/3 *(qp2-qp0)

In the code below, we must compute both the x and y terms for each point separately.

cp1x = qp0x + 2.0/3.0*(qp1x - qp0x);
cp1y = qp0y + 2.0/3.0*(qp1y - qp0y);
cp2x = cp1x + (qp2x - qp0x)/3.0;
cp2y = cp1y + (qp2y - qp0y)/3.0;

We will now
a) replace the qp0x and qp0y variables with currentX and currentY (which *you* must store for each moveTo/lineTo/bezierCurveTo)
b) replace the qp1x and qp1y variables with cpx and cpy (which we would have passed to quadraticCurveTo)
c) replace the qp2x and qp2y variables with x and y.
which leaves us with:
*/
var cp1x = currentX + 2.0/3.0*(cpx - currentX);
var cp1y = currentY + 2.0/3.0*(cpy - currentY);
var cp2x = cp1x + (x - currentX)/3.0;
var cp2y = cp1y + (y - currentY)/3.0;

// and now call cubic Bezier curve to function
bezierCurveTo( cp1x, cp1y, cp2x, cp2y, x, y );

currentX = x;
currentY = y;
}

Прямоугольники

Помимо тех трёх методов, которые мы рассматривали выше, рисующих прямоугольник непосредственно на холст, у нас также есть метод rect, который добавляет прямоугольный путь в список путей.

rect(x, y, width, height)

Этот метод принимает четыре аргумента. Параметры x и y задают координаты верхнего левого угла нового прямоугольного пути. width и height задают ширину и высоту прямоугольника.

После выполнения этого метода автоматически вызывается метод moveTo с параметрами (0,0) (т.е. стартовая точка сбрасывается в значение по умолчанию).

Смешанное использование

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

Я не буду подробно разбирать весь скрипт, но прошу обратить внимание на такую важную вещь, как функция roundedRect, а также на использование свойства fillStyle. Определение собственных функций очень полезно и экономит время. Без roundedRect этот скрипт был бы в два раза более длинным, чем он есть сейчас.
Свойство fillStyle. позже мы рассмотрим более подробно. Здесь я использую его для смены цвета заливки с чёрного по умолчанию на белый, и обратно.

function draw() {
var ctx = document.getElementById('canvas').getContext('2d');
roundedRect(ctx,12,12,150,150,15);
roundedRect(ctx,19,19,150,150,9);
roundedRect(ctx,53,53,49,33,10);
roundedRect(ctx,53,119,49,16,6);
roundedRect(ctx,135,53,49,33,10);
roundedRect(ctx,135,119,25,49,10);

ctx.beginPath();
ctx.arc(37,37,13,Math.PI/7,-Math.PI/7,true);
ctx.lineTo(31,37);
ctx.fill();
for(i=0;i<8;i++){
ctx.fillRect(51+i*16,35,4,4);
}
for(i=0;i<6;i++){
ctx.fillRect(115,51+i*16,4,4);
}
for(i=0;i<8;i++){
ctx.fillRect(51+i*16,99,4,4);
}
ctx.beginPath();
ctx.moveTo(83,116);
ctx.lineTo(83,102);
ctx.bezierCurveTo(83,94,89,88,97,88);
ctx.bezierCurveTo(105,88,111,94,111,102);
ctx.lineTo(111,116);
ctx.lineTo(106.333,111.333);
ctx.lineTo(101.666,116);
ctx.lineTo(97,111.333);
ctx.lineTo(92.333,116);
ctx.lineTo(87.666,111.333);
ctx.lineTo(83,116);
ctx.fill();
ctx.fillStyle = "white";
ctx.beginPath();
ctx.moveTo(91,96);
ctx.bezierCurveTo(88,96,87,99,87,101);
ctx.bezierCurveTo(87,103,88,106,91,106);
ctx.bezierCurveTo(94,106,95,103,95,101);
ctx.bezierCurveTo(95,99,94,96,91,96);
ctx.moveTo(103,96);
ctx.bezierCurveTo(100,96,99,99,99,101);
ctx.bezierCurveTo(99,103,100,106,103,106);
ctx.bezierCurveTo(106,106,107,103,107,101);
ctx.bezierCurveTo(107,99,106,96,103,96);
ctx.fill();
ctx.fillStyle = "black";
ctx.beginPath();
ctx.arc(101,102,2,0,Math.PI*2,true);
ctx.fill();
ctx.beginPath();
ctx.arc(89,102,2,0,Math.PI*2,true);
ctx.fill();
}
function roundedRect(ctx,x,y,width,height,radius){
ctx.beginPath();
ctx.moveTo(x,y+radius);
ctx.lineTo(x,y+height-radius);
ctx.quadraticCurveTo(x,y+height,x+radius,y+height);
ctx.lineTo(x+width-radius,y+height);
ctx.quadraticCurveTo(x+width,y+height,x+width,y+height-radius);
ctx.lineTo(x+width,y+radius);
ctx.quadraticCurveTo(x+width,y,x+width-radius,y);
ctx.lineTo(x+radius,y);
ctx.quadraticCurveTo(x,y,x,y+radius);
ctx.stroke();
}

Задание для самостоятельной работы

Нарисовать "розу ветров" по следующим данным:

НаправлениеКол-во дней
С 4
С-В 7
В 2
Ю-В 3
Ю 3
Ю-З 6
З 5
С-З 1