Рисование фигур с помощью canvas

Перевод не завершен. Пожалуйста, помогите перевести эту статью с английского.

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

Сетка

Перед тем как мы начнем рисовать, нам нужно поговорить о сетке canvas или координатной плоскости. Наш HTML каркас из предыдущей страницы включал в себя элемент canvas 150 пикселей в ширину и 150 пикселей в высоту. Справа можно увидеть этот canvas с сеткой, накладываемой по умолчанию. Обычно 1 единица на сетке соответствует 1 пикселю на canvas. Начало координат этой сетки расположено в верхнем левом углу в координате (0,0). Все элементы размещены относительно этого начала. Таким образом, положение верхнего левого угла синего квадрата составляет х пикселей слева и у пикселей сверху, на координате , у). Позже в этом уроке мы увидим, как можно перевести начало координат в другое место, вращать сетку и даже масштабировать ее, но сейчас мы будем придерживаться настроек сетки по умолчанию.

Рисование прямоугольников

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

Сначала рассмотрим прямоугольник. Ниже представлены три функции рисования прямоугольников в canvas:

fillRect(x, y, width, height)
Рисование заполненного прямоугольника.
strokeRect(x, y, width, height)
Рисование прямоугольного контура.
clearRect(x, y, width, height)
Очистка  прямоугольной области, делая содержимое совершенно прозрачным.

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

  • xy устанавливают положение верхнего левого угла прямоугольника в canvas (относительно начала координат);
  • width (ширина) и height (высота) определяют размеры прямоугольника.

Ниже приведена функция draw(), использующая эти три функции.

Пример создания прямоугольных фигур

function draw() {
  var canvas = document.getElementById('canvas');
  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);
  }
}

Этот пример изображен ниже.

ScreenshotLive sample

Функция fillRect() рисует большой чёрный квадрат со стороной 100 px. Функция clearRect() вырезает квадрат 60х60 из центра, а функция strokeRect() создает прямоугольный контур 50х50 пикселей внутри очищенного квадрата.

На следующей странице мы рассмотрим две альтернативы методу clearRect(), и также увидим, как можно изменять цвет и стиль контура отображаемых фигур.

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

Рисование контуров (path)

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

Создание фигур используя контуры происходит в несколько важных шагов:

  1. Сначала вы создаете контур.
  2. Затем, используя команды рисования, рисуете контур.
  3. Потом закрываете контур.
  4. Созданный контур вы можете обвести или залить для его отображения.

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

beginPath()
Создает новый контур. После создания используется в дальнейшем командами рисования при построении контуров.
Path методы
Методы для установки различных контуров объекта.
closePath()
Закрывает контур, так что будущие команды рисования вновь направлены контекст.
stroke()
Рисует фигуру с внешней обводкой.
fill()
Рисует фигуру с заливкой внутренней области.

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

Note:  если текущий контур пуст (например, как после вызова beginPath() или на вновь созданном canvas), первой командой построения контура всегда является функция  moveTo(). Поэтому мы всегда можем установить начальную позицию рисования контура после перезагрузки.

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

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

Note: Когда вы вызываете fill(), то каждая открытая фигура закрывается автоматически, так что вы можете не использовать closePath(). Это обстоятельство не имеет место в случае вызова stroke().

Рисование треугольника

Например, код для рисования треугольника будет выглядеть как-то так:

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

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

Результат выглядит так:

ScreenshotLive sample

Передвижение пера

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

moveTo(x, y)
Перемещает перо в точку с координатами x и y.

При инициализации canvas или при вызове beginPath(), вы захотите использовать функцию moveTo() для перемещения в точку начала рисования. Можно использовать moveTo() и для рисования несвязанного(незакрытого) контура. Посмотрите на смайлик ниже.

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

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

    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();
  }
}

Результат этого ниже:

ScreenshotLive sample

Если вы захотите увидеть соединные линии, то можете удалить вызов moveTo().

Note: Подробнее о функции arc(),посмотрите Arcs .

Линии

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

lineTo(x, y)
Рисует линию с текущей позиции до позиции, определенной x и y.

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

Пример ниже рисует два треугольника, один закрашенный и другой обведен контуром.

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

    // Filled triangle
    ctx.beginPath();
    ctx.moveTo(25,25);
    ctx.lineTo(105,25);
    ctx.lineTo(25,105);
    ctx.fill();

    // Stroked triangle
    ctx.beginPath();
    ctx.moveTo(125,125);
    ctx.lineTo(125,45);
    ctx.lineTo(45,125);
    ctx.closePath();
    ctx.stroke();
  }
}

Отрисовка начинается с вызова beginPath(), чтобы начать рисовать путь новой фигуры. Затем мы используем метод moveTo(), чтобы переместить начальную точку в нужное положение. Ниже рисуются две линии, которые образуют две стороны треугольника.

ScreenshotLive sample

Вы заметите разницу между закрашенным и обведенным контуром треугольниками. Это, как упоминалось выше, из-за того, что фигуры автоматически закрываются, когда путь заполнен (т. е. закрашен), но не тогда, когда он очерчен (т. е. обведен контуром). Если бы мы не учли closePath() для очерченного треугольника, тогда только две линии были бы нарисованы, а не весь треугольник.

Дуги

Для рисования дуг и окружностей, используем методы arc() и arcTo().

arc(x, y, radius, startAngle, endAngle, anticlockwise)
Рисуем дугу с центром в точке (x,y) радиусом r, начиная с угла startAngle и заканчивая в endAngle в направлении против часовой стрелки anticlockwise (по умолчанию по ходу движения часовой стрелки).
arcTo(x1, y1, x2, y2, radius)
Рисуем дугу с заданными контрольными точками и радиусом, соединяя эти точки прямой линией.

Давайте более детальнее рассмотрим метод arc(), который имеет пять параметров: x и y — это координаты центра окружности, в которой должна быть нарисована дуга. radius — не требует пояснений. Углы startAngle and endAngle определяют начальную и конечную точки дуги в радианах вдоль кривой окружности. Отсчет происходит от оси x. Параметр anticlockwise — логическое значение, которое, если true, то рисование дуги совершается против хода часовой стрелки; иначе рисование происходит по ходу часовой стрелки.

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

Следующий пример немного сложнее, чем мы рассматривали ранее. Здесь нарисованы 12 различных дуг с разными углами и заливками.

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

Координаты x и y  должны быть достаточны ясны. radius and startAngle — фиксированы. endAngle начинается со 180 градусов (полуокружность) в первой колонке и, увеличиваясь с шагом 90 градусов, достигает кульминации полноценной окружностью в последнем столбце.

Установка параметра clockwise определяет результат, в первой и третьей строках рисование дуг происходит по часовой стрелке, а во второй и четвертой против часовой стрелки. Благодаря if-условию верхняя половина дуг образуется с контуром (обводкой), а нижняя половина дуг с заливкой.

Note: Этот пример требует немного большего холста (canvas), чем другие на этой странице: 150 x 200 pixels.

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

    for(var i=0;i<4;i++){
      for(var j=0;j<3;j++){
        ctx.beginPath();
        var x = 25+j*50; // x coordinate
        var y = 25+i*50; // y coordinate
        var radius = 20; // Arc radius
        var startAngle = 0; // Starting point on circle
        var endAngle = Math.PI+(Math.PI*j)/2; // End point on circle
        var anticlockwise = i%2==0 ? false : true; // clockwise or anticlockwise

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

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

ScreenshotLive sample

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

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

quadraticCurveTo(cp1x, cp1y, x, y)
Рисуется квадратичная кривая Безье с текущей позиции пера в конечную точку с координатами x и y, используя контрольную точку с координатами cp1x и cp1y.
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
Рисуется кубическая кривая Безье с текущей позиции пера в конечную точку с координатами x и y, используя две контрольные точки с координатами (cp1x, cp1y) и (cp2x, cp2y).

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

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

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

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

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

В этом примере многократно используются квадратичные кривые Безье для рисования речевой выноски.

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

    // Quadratric curves example
    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();
  }
}

ScreenshotLive sample

Кубические кривые Безье

В этом примере нарисовано сердце с использованием кубических кривых Безье.

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

    // Quadratric curves example
    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();
  }
}

ScreenshotLive sample

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

In addition to the three methods we saw in Drawing rectangles, which draw rectangular shapes directly to the canvas, there's also the rect() method, which adds a rectangular path to a currently open path.

rect(x, y, width, height)
Draws a rectangle whose top-left corner is specified by (x, y) with the specified width and height.

When this method is executed, the moveTo() method is automatically called with the parameters (0,0). In other words, the current pen position is automatically reset to the default coordinates.

Создание комбинаций

So far, each example on this page has used only one type of path function per shape. However, there's no limitation to the number or types of paths you can use to create a shape. So in this final example, let's combine all of the path functions to make a set of very famous game characters.

function draw() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext){
    var ctx = 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,false);
    ctx.lineTo(31,37);
    ctx.fill();

    for(var 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();
  }
}

// A utility function to draw a rectangle with rounded corners.

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();
}

The resulting image looks like this:

ScreenshotLive sample

We won't go over this in detail, since it's actually surprisingly simple. The most important things to note are the use of the fillStyle property on the drawing context, and the use of a utility function (in this case roundedRect()). Using utility functions for bits of drawing you do often can be very helpful and reduce the amount of code you need, as well as its complexity.

We'll take another look at fillStyle, in more detail, later in this tutorial. Here, all we're doing is using it to change the fill color for paths from the default color of black to white, and then back again.

Path2D объекты

As we have seen in the last example, there can be a series of paths and drawing commands to draw objects onto your canvas. To simplify the code and to improve performance, the Path2D object, available in recent versions of browsers, lets you cache or record these drawing commands. You are able to play back your paths quickly.
Lets see how we can construct a Path2D object:

Path2D()
The Path2D() constructor returns a newly instantiated Path2D object, optionally with another path as an argument (creates a copy), or optionally with a string consisting of SVG path data.
new Path2D();     // empty path object
new Path2D(path); // copy from another path
new Path2D(d);    // path from SVG path data

All path methods like moveTo, rect, arc or quadraticCurveTo, etc., which we got to know above, are available on Path2D objects.

The Path2D API also adds a way to combine paths using the addPath method. This can be useful when you want to built objects from several components, for example.

Path2D.addPath(path [, transform])
Adds a path to the current path with an optional transformation matrix.

Path2D пример

In this example, we are creating a rectangle and a circle. Both are stored as a Path2D object, so that they are available for later usage. With the new Path2D API, several methods got updated to optionally accept a Path2D object to use instead of the current path. Here, stroke and fill are used with a path argument to draw both objects onto the canvas, for example.

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

    var rectangle = new Path2D();
    rectangle.rect(10, 10, 50, 50);

    var circle = new Path2D();
    circle.moveTo(125, 35);
    circle.arc(100, 35, 25, 0, 2 * Math.PI);

    ctx.stroke(rectangle);
    ctx.fill(circle);
  }
}

ScreenshotLive sample

Использование SVG путей

Another powerful feature of the new canvas Path2D API is using SVG path data to initialize paths on your canvas. This might allow you to pass around path data and re-use them in both, SVG and canvas.

The path will move to point (M10 10) and then move horizontally 80 points to the right (h 80), then 80 points down (v 80), then 80 points to the left (h -80), and then back to the start (z). You can see this example on the Path2D constructor page.

var p = new Path2D("M10 10 h 80 v 80 h -80 Z");

Метки документа и участники

 Внесли вклад в эту страницу: hraboviyvadim, jynweythek, dyachenco, fil7, ifeature, gearmobile, teoli, fscholz, Mirsait, Neir
 Обновлялась последний раз: hraboviyvadim,