Статьи

Холст с нуля: трансформации и градиенты

В этой статье я расскажу вам о преобразованиях в канве, а также о тенях и градиентах. Преобразования — это чрезвычайно ценный набор методов, которые позволяют вам начать творчески подходить к рисованию объектов на холсте. Давайте начнем после прыжка!


Вы собираетесь использовать тот же шаблон HTML из предыдущих статей, поэтому откройте ваш любимый редактор и вставьте следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
 
<html>
    <head>
        <title>Canvas from scratch</title>
        <meta charset=»utf-8″>
 
        <script src=»https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js»></script>
 
        <script>
            $(document).ready(function() {
                var canvas = document.getElementById(«myCanvas»);
                var ctx = canvas.getContext(«2d»);
            });
        </script>
    </head>
 
    <body>
        <canvas id=»myCanvas» width=»500″ height=»500″>
            <!— Insert fallback content here —>
        </canvas>
    </body>
</html>

Здесь у нас нет ничего, кроме простой HTML-страницы с элементом canvas и некоторого JavaScript, который запускается после загрузки DOM. Ничего сумасшедшего.


Перевод по существу сдвигает всю систему координат.

Одним из самых простых преобразований в canvas является translate . Это позволяет вам перемещать начальную точку контекста рендеринга 2d; (0, 0) позиция на холсте. Позвольте мне показать вам, что это значит.

Сначала разместите квадрат на холсте в позиции (0, 0):

1
ctx.fillRect(0, 0, 100, 100);

Он нарисует себя на верхнем левом краю холста. Тем не менее — ничего необычного здесь.

Простой квадрат

Теперь попробуйте перевести 2-й контекст рендеринга и нарисовать еще один квадрат в той же позиции:

1
2
3
4
5
ctx.save();
ctx.translate(100, 100);
ctx.fillStyle = «rgb(0, 0, 255)»;
ctx.fillRect(0, 0, 100, 100);
ctx.restore();

Как ты думаешь, что произойдет? Получите золотую звезду, если догадались, что новый квадрат будет нарисован в позиции (100, 100). Нет времени игры для тех, кто догадался. Сожалею!

Перевод квадрата

Так что же здесь произошло? Что касается кода для рисования второго квадрата, вы нарисовали его там же, где и первый квадрат. Причина этого в том, что вы в основном сместили всю систему координат холста так, чтобы его (0, 0) положение теперь было на месте (100, 100).

Как работает перевод

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

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

Давайте посмотрим на следующее преобразование в списке.


Как вы, наверное, догадались, преобразование scale используется для изменения размера. Более конкретно, преобразование масштаба используется для масштабирования контекста 2d-рендеринга.

Удалите код, над которым вы работали с примером translate , и добавьте следующий код:

1
ctx.fillRect(100, 100, 100, 100);

Это нарисует стандартный квадрат в позиции (100, 100), шириной и высотой 100 пикселей. Так как мы масштабируем это?

Простой квадрат

Свойства в масштабе являются множителями для размеров x и y.

Преобразование scale используется аналогичным образом для translate , в том смысле, что оно вызывается перед тем, как вы рисуете объекты, к которым хотите применить его. Важно отметить, что свойства в scale являются множителями для размеров x и y . Это означает, что scale (1, 1) умножит размер контекста рендеринга 2d на единицу, оставив тот же размер, что был раньше. scale (5, 5) умножит размер 2-го контекста рендеринга на пять, сделав его в пять раз больше, чем был ранее. Просто.

В вашем случае вы хотите удвоить размер квадрата, поэтому вы применяете scale (2, 2):

1
2
3
4
ctx.save();
ctx.scale(2, 2);
ctx.fillRect(100, 100, 100, 100);
ctx.restore();

В результате получается квадрат, в два раза превышающий размер:

Масштабирование квадрата

Однако обратите внимание, как квадрат теперь рисуется в другом положении, чем он был нарисован до применения scale . Причина этого в том, что scale умножает размер всего в контексте рендеринга 2d, включая координаты. В вашем случае позиция (100, 100) теперь становится (200, 200); координаты в два раза больше, чем они были бы без масштабирования.

Чтобы обойти это, мы можем выполнить translate который перемещает начало контекста 2d рендеринга в положение, в котором вы хотите нарисовать квадрат. Если затем применить scale и нарисовать квадрат в позиции (0, 0), его позиция не будет смещена:

1
2
3
4
5
ctx.save();
ctx.translate(100, 100);
ctx.scale(2, 2);
ctx.fillRect(0, 0, 100, 100);
ctx.restore();

В результате получается квадрат, который в два раза больше оригинала, но рисуется в той же позиции, что и оригинал:

Масштабирование и перевод квадрата

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


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

Я уверен, что rotate нуждается в представлении, поэтому давайте прыгнем прямо и повернем квадрат на 45 градусов (помните, что градусы должны быть в радианах):

1
2
3
4
ctx.save();
ctx.rotate(Math.PI/4);
ctx.fillRect(100, 100, 100, 100);
ctx.restore();

Который позиционирует квадрат в (100, 100) и вращается .. вау, держись! Это не выглядит правильно:

Вращая квадрат

Видишь что случилось? Кажется, что квадрат пытается вырваться из окна браузера, а не вращаться на месте в позиции (100, 100). Это потому, что rotate , как и все преобразования, влияет на весь контекст рендеринга 2d, а не на объекты по отдельности.

Вот иллюстрация того, что происходит с системой координат, когда вы выполняете rotate на 45 градусов:

Как вращение работает

Обратите внимание, как вся система координат повернулась на 45 градусов от исходной точки (0, 0)? Это то, что заставляло квадрат выглядеть так, как будто он выходил из окна браузера, просто потому, что позиция (100, 100) была повернута ударом по краю браузера.

Простой способ обойти эту проблему — объединить rotate с translate , вот так:

1
2
3
4
5
ctx.save();
ctx.translate(150, 150);
ctx.rotate(Math.PI/4);
ctx.fillRect(-50, -50, 100, 100);
ctx.restore();

Выполнение translate перемещает исходную точку 2-го контекста рендеринга (0, 0) в то, что должно быть центральной точкой квадрата (150, 150). Это означает, что любое вращение теперь будет основываться вокруг позиции (150, 150). Если вы затем нарисуете квадрат с отрицательными координатами x и y , равными половине ширины и высоты квадрата, вы в конечном итоге нарисуете квадрат, который выглядит так, как будто он повернут вокруг своей центральной точки:

Вращение и перевод квадрата

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

Давайте перейдем к чему-то более визуально впечатляющему.


Добавление теней к объектам восхитительно просто.

Canvas поставляется с несколькими свойствами для управления внешним видом объектов, нарисованных на нем, и один набор этих свойств позволяет добавлять тени.

Добавление теней к объектам восхитительно просто. Для этого просто необходимо, чтобы свойство shadowColor было установлено в контексте рендеринга 2d на цвет, который не является прозрачным черным, а для shadowBlur , shadowOffsetX или shadowOffsetY должно быть установлено значение, отличное от 0.

Попробуйте следующий код:

1
2
3
4
5
ctx.save();
ctx.shadowBlur = 15;
ctx.shadowColor = «rgb(0, 0, 0)»;
ctx.fillRect(100, 100, 100, 100);
ctx.restore();

Это даст пятне пятнадцать пикселей размытия и установит цвет на сплошной черный:

Добавление размытой тени

Довольно стандартные вещи до сих пор.

Если установить для shadowBlur значение 0, измените shadowColor на светло-серый и задайте положительные значения shadowOffsetX и shadowOffsetY :

1
2
3
4
5
6
7
ctx.save();
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 6;
ctx.shadowOffsetY = 6;
ctx.shadowColor = «rgba(125, 125, 125, 0.5)»;
ctx.fillRect(300, 100, 100, 100);
ctx.restore();

В итоге вы получите сплошную тень, которая появляется немного правее и ниже нарисованного объекта:

Добавление сплошной тени

Какими бы крутыми ни были тени, они могут быть чем-то вроде борова с ресурсами.

Важно помнить, что тени влияют на все, что рисуется после их определения, поэтому полезно использовать методы save и restore чтобы избавить вас от необходимости сбрасывать свойства теней после их использования.

Помните, что производительность может снизиться, если вы накладываете тень на множество объектов одновременно. В некоторых случаях, возможно, стоит использовать изображение PNG с тенью вместо рисования объекта вручную и применения динамической тени с использованием кода. О том, как использовать изображения с canvas, мы расскажем в следующей части этой серии.


Вы можете создать два типа градиентов на холсте — линейный и радиальный.

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

1
ctx.createLinearGradient(startX, startY, endX, endY);

Первый набор из двух аргументов — это координаты x и y начала градиента, а второй набор — это координаты x и y конца градиента. Также важно отметить, что градиент на холсте на самом деле является типом значения цвета, поэтому вы применяете их к свойствам fillStyle и strokeStyle .

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

1
2
3
4
5
6
7
8
var gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, «rgb(255, 255, 255)»);
gradient.addColorStop(1, «rgb(0, 0, 0)»);
 
ctx.save();
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();

Обратите внимание, как вы присваиваете градиент переменной, а затем используете эту переменную для вызова метода addColorStop . Этот метод позволяет установить цвет в определенных точках вдоль градиента. Например, позиция 0 будет представлять начало градиента (первая позиция x и y ), а 1 будет представлять конец градиента (вторая позиция x и y ). Вы также можете использовать десятичные точки от 0 до 1, чтобы назначить цвет в другой точке вдоль градиента, например, 0,5 будет на полпути.

Применяя переменную градиента к свойству fillStyle , вы получите красивый градиент от белого (в позиции 0 вверху холста) до черного (в позиции 1 внизу холста):

Создание линейного градиента

Но вы не всегда должны использовать линейные градиенты; Вы также можете создавать радиальные градиенты!

Радиальные градиенты создаются с createRadialGradient метода createRadialGradient , который выглядит следующим образом в псевдокоде:

1
ctx.createRadialGradient(startX, startY, startRadius, endX, endY, endRadius);

Первый набор из трех аргументов — это координаты x и y, а также радиус окружности в начале градиента, а последние три аргумента представляют позицию x и y, а также радиус окружности в конце градиент

Звучит запутанно, правда? Это немного, поэтому давайте прыгнем и создадим радиальный градиент, чтобы увидеть, что происходит:

1
2
3
4
5
6
7
8
var gradient = ctx.createRadialGradient(350, 350, 0, 50, 50, 100);
gradient.addColorStop(0, «rgb(0, 0, 0)»);
gradient.addColorStop(1, «rgb(125, 125, 125)»);
 
ctx.save();
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();

Вы создали радиальный градиент с начальной точкой (350, 350) с радиусом 0 и конечной точкой (50, 50) с радиусом 100. Можете ли вы догадаться, как это будет выглядеть? 20 баллов, если вы догадались, это будет выглядеть так:

Создание радиального градиента

Если вы похожи на меня, я не ожидал увидеть это. Я раньше использовал радиальные градиенты в приложениях, таких как Adobe Photoshop, и они ничего подобного не выглядят! Так почему же это выглядит так? Ну, вот как это должно выглядеть, странно.

Посмотрите на эту диаграмму, которая показывает, как именно работает радиальный градиент на холсте:

Как работают радиальные градиенты

Интересно, не правда ли? Это в основном позволяет вам создавать форму конуса, но что, если вы хотите создать правильный радиальный градиент, как в Photoshop? К счастью, все просто.

Создание правильного радиального градиента просто требует, чтобы вы поместили два круга градиента в одно и то же положение x и y , убедившись, что один из кругов градиента больше другого:

01
02
03
04
05
06
07
08
09
10
11
var canvasCentreX = canvas.width/2;
var canvasCentreY = canvas.height/2;
 
var gradient = ctx.createRadialGradient(canvasCentreX, canvasCentreY, 250, canvasCentreX, canvasCentreY, 0);
gradient.addColorStop(0, «rgb(0, 0, 0)»);
gradient.addColorStop(1, «rgb(125, 125, 125)»);
 
ctx.save();
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();

Приведенный выше код создает радиальный градиент, который находится в центре холста. Один из кругов в градиенте имеет радиус 0, а другой имеет радиус 250. В результате получается традиционный радиальный градиент, который перемещается от центра холста наружу, например, так:

Создание радиального градиента

Это выглядит лучше! Я был искренне удивлен, когда увидел, как в холсте были реализованы радиальные градиенты. Могу поспорить, это сбило с толку многих людей, когда они видят эту форму конуса. Ну хорошо, по крайней мере, вы знаете, как создавать правильные сейчас.

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


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

В следующей записи «Холст с нуля» мы отойдем от рисования объектов и посмотрим, как управлять изображениями и видео на холсте. Здесь вещи начинают становиться действительно интересными! Будьте на связи!