Рисуем спрайты и фигуры

Сегодня ты впервые увидишь своего цыплёнка на экране: сначала из простых фигур, а потом — настоящей картинкой-спрайтом.
Спрайт — это картинка игрового объекта (героя, врага, монетки), которую мы рисуем на canvas. Любой герой любой игры начинается именно с этого.

Зачем вообще учиться рисовать фигуры

Представь, что ты открыл свою любимую игру — неважно, мобильную аркаду или что-то на компьютере. Всё, что ты там видишь: герой, монетки, кнопки, полоска здоровья, фон — это нарисованные пиксели. Игра тысячи раз в секунду перерисовывает картинку, и кажется, что персонаж живой. Но под капотом — простые команды вроде «нарисуй прямоугольник вот тут» и «выведи картинку героя вот сюда».

В прошлом уроке про canvas и контекст 2D ты уже подготовил холст и достал из него объект context — тот самый пульт, через который мы отдаём команды рисования. Сегодня мы наконец нажмём на этом пульте первые кнопки. К концу урока у тебя на экране появится площадка: зелёная земля, голубое небо, монетка-кружок и — главное — настоящий цыплёнок, твой герой на все следующие уроки курса.

Звучит как много? На самом деле это всего штук пять команд. Давай разберём их по одной, по принципу «меняем строчку — смотрим, что вышло».

И ещё одна хорошая новость: эти же пять команд работают в любой игре, которую ты потом захочешь сделать. Понг, Змейка, платформер — всё рисуется прямоугольниками, кругами и спрайтами. Ты не учишь что-то одноразовое «для урока»: прямо сейчас ты собираешь набор инструментов, которым будешь пользоваться до самого конца курса. Поэтому не торопись — лучше один раз понять, как ведёт себя каждая команда, чем потом гадать, почему монетка съехала на полэкрана или почему цыплёнок упорно не появляется.

Холст — это лист в клеточку

Самая удобная метафора для canvas — это лист бумаги в клеточку, только клеточки тут размером в один пиксель. Левый верхний угол — это точка с координатами (0, 0). Ось x растёт вправо, а ось yвниз, а не вверх, как на уроках математики. Это первое, обо что спотыкаются все новички: чем больше y, тем ниже объект на экране.

Запомни так: верх экрана — это y = 0, и чем ниже ты опускаешься, тем число больше. Цыплёнок, который «падает вниз», на самом деле увеличивает свой y. Это пригодится уже в следующих уроках, когда мы добавим гравитацию.

Все команды рисования мы отдаём через контекст. Договоримся, что он у нас уже получен из прошлого урока вот такой строчкой:

const canvas = document.getElementById('game');
const context = canvas.getContext('2d');

Результат: на странице есть пустой холст-canvas, и в переменной context лежит «пульт», через который дальше идут все команды рисования. Пока на экране ничего нет — белый прямоугольник.

Пример 1: рисуем прямоугольник — это будет земля

Самая частая фигура в играх — прямоугольник. Платформа, стена, кнопка, полоска здоровья, фон — всё это прямоугольники. Команда для заливки одна: context.fillRect(x, y, ширина, высота). Первые два числа — координаты левого верхнего угла фигуры, вторые два — её размеры.

Но перед тем как рисовать, надо выбрать цвет. За цвет заливки отвечает свойство context.fillStyle. Важная деталь: цвет — это «кисточка». Ты один раз макаешь кисточку в краску, и дальше всё, что рисуешь, идёт этим цветом — пока не поменяешь. Нарисуем небо и землю:

// небо — большой голубой прямоугольник на весь холст
context.fillStyle = 'skyblue';
context.fillRect(0, 0, 480, 360);

// земля — зелёная полоса внизу
context.fillStyle = 'forestgreen';
context.fillRect(0, 300, 480, 60);

Результат: весь холст залит голубым небом, а внизу появляется зелёная полоса земли высотой 60 пикселей. Получилась простая сцена-задник, как фон в платформере.

Разберём по шагам, что произошло:

  • Сначала fillStyle = 'skyblue' — макнули кисточку в голубой.
  • fillRect(0, 0, 480, 360) — нарисовали прямоугольник от левого верхнего угла на всю ширину и высоту. Это закрасило фон.
  • Потом сменили кисточку на 'forestgreen' и нарисовали полоску, начиная с y = 300 (то есть в нижней части), высотой 60.

Обрати внимание на порядок: сначала рисуется небо, потом земля поверх него. На canvas всё работает как со слоями краски — то, что нарисовано позже, ложится сверху. Поэтому фон рисуют первым, а героя — последним.

Цвета можно задавать тремя способами

Цвет в fillStyle можно указывать как тебе удобнее:

  • Словом — 'red', 'skyblue', 'gold' (браузер знает кучу названий).
  • Шестнадцатеричным кодом — '#ffcc00' (как в графических редакторах и CSS).
  • Через rgb'rgb(255, 204, 0)', где три числа от 0 до 255 — это доля красного, зелёного и синего.

Для игр удобнее всего hex-коды: их легко скопировать из палитры в фотошопе или из любого инструмента подбора цветов, и оттенок будет ровно тот, что ты задумал. Но на старте слова читаются понятнее, поэтому в примерах я часто буду писать словами — так код проще читать вслух и держать в голове. Когда дойдёшь до настоящей игры, наверняка перейдёшь на hex: с ним легко собрать целую палитру уровня и быстро её менять.

Пример 2: рисуем круг — это будет монетка

С кругами чуть хитрее: у canvas нет команды «нарисуй круг». Вместо этого есть универсальный инструмент context.arc(), который рисует дугу, а круг — это просто полная дуга на 360 градусов. Углы тут задаются не в градусах, а в радианах, и полный круг — это Math.PI * 2. Запомни эту формулу как заклинание: 0, Math.PI * 2 — значит «полный круг».

Рисование круга — это всегда три шага: начать путь, описать дугу, залить. Сделаем золотую монетку:

context.fillStyle = 'gold';
context.beginPath();
context.arc(240, 180, 20, 0, Math.PI * 2);
context.fill();

Результат: в центре холста появляется золотой кружок-монетка радиусом 20 пикселей. На фоне неба и земли из прошлого примера это выглядит как монетка, висящая в воздухе.

Разберём команды по порядку:

  • beginPath() — говорим «начинаю новый рисунок-контур». Это как поднять ручку и приставить её к бумаге в новом месте.
  • arc(240, 180, 20, 0, Math.PI * 2) — описываем дугу. Числа: центр по x (240), центр по y (180), радиус (20), начальный угол (0) и конечный угол (полный круг).
  • fill() — заливаем получившийся контур текущим цветом (золотым).

Заметь разницу с прямоугольником: у fillRect координаты — это угол фигуры, а у arc — это центр круга. Из-за этого новички часто рисуют круг не там, где ожидали. Держи это в голове.

Обводка вместо заливки

Иногда нужна не заливка, а контур — например, обвести монетку рамкой, чтобы она лучше читалась на фоне. За цвет обводки отвечает strokeStyle, за толщину линии — lineWidth, а рисует обводку команда stroke() вместо fill():

context.fillStyle = 'gold';
context.strokeStyle = '#a07c00';
context.lineWidth = 3;

context.beginPath();
context.arc(240, 180, 20, 0, Math.PI * 2);
context.fill();    // сначала залили золотым
context.stroke();  // потом обвели тёмной рамкой

Результат: та же золотая монетка, но теперь у неё аккуратная тёмно-жёлтая обводка толщиной 3 пикселя. Монетка выглядит объёмнее и заметнее на голубом небе.

Можно вызывать fill() и stroke() для одного контура подряд — сначала зальётся внутренность, потом нарисуется рамка по краю. Точно так же работают fillRect и парный ему strokeRect для прямоугольников.

Пример 3: главное — выводим спрайт цыплёнка

Фигуры — это здорово, но герой из квадратиков выглядит грустно. Настоящие игры рисуют объекты картинками — спрайтами. Команда для этого — context.drawImage(). Но есть нюанс, на котором спотыкаются вообще все: картинку нельзя нарисовать сразу. Сначала браузер должен её загрузить с диска или из сети, а это занимает время.

Метафора простая: ты заказал распечатку фото в фотосалоне. Пока фото не готово — клеить его в альбом бесполезно, на странице будет пусто. Поэтому мы создаём объект картинки, говорим ему адрес файла и ждём события onload — «фото готово». И только внутри этого обработчика рисуем спрайт. Заведём нашего сквозного героя:

const chickenSprite = new Image();
chickenSprite.src = '/sprites/chicken.png';

chickenSprite.onload = function () {
  // картинка загрузилась — теперь её можно рисовать
  context.drawImage(chickenSprite, 200, 250, 48, 48);
};

Результат: как только файл спрайта подгрузится, на земле (примерно в координатах 200, 250) появляется цыплёнок размером 48×48 пикселей. Это и есть наш герой, который будет жить во всех следующих уроках курса.

Разберём по шагам:

  • new Image() — создаём пустой объект-картинку и кладём его в переменную chickenSprite. Это имя мы переиспользуем во всех уроках, так что запомни его.
  • chickenSprite.src = '...' — задаём адрес файла. В этот момент браузер начинает загрузку в фоне.
  • chickenSprite.onload = function () { ... } — говорим: «когда загрузится, выполни вот это». Внутри и вызываем drawImage.
  • drawImage(картинка, x, y, ширина, высота) — рисуем спрайт: его левый верхний угол в точке (200, 250), а сам он растянут до 48 на 48 пикселей.

У drawImage есть короткая форма без размеров — drawImage(chickenSprite, 200, 250) — тогда картинка рисуется в свою натуральную величину. Но в играх почти всегда задают размер явно, чтобы герой не оказался гигантским или микроскопическим.

Пример 4: собираем всю сцену вместе

Теперь склеим всё в одну картинку: небо, землю, монетку с обводкой и цыплёнка. Порядок рисования = порядок слоёв, поэтому фон идёт первым, герой — последним. А поскольку спрайт грузится с задержкой, фон и фигуры рисуем сразу, а героя — внутри onload:

const canvas = document.getElementById('game');
const context = canvas.getContext('2d');

// 1. небо
context.fillStyle = 'skyblue';
context.fillRect(0, 0, 480, 360);

// 2. земля
context.fillStyle = 'forestgreen';
context.fillRect(0, 300, 480, 60);

// 3. монетка с обводкой
context.fillStyle = 'gold';
context.strokeStyle = '#a07c00';
context.lineWidth = 3;
context.beginPath();
context.arc(360, 180, 16, 0, Math.PI * 2);
context.fill();
context.stroke();

// 4. цыплёнок-спрайт (когда загрузится)
const chickenSprite = new Image();
chickenSprite.src = '/sprites/chicken.png';
chickenSprite.onload = function () {
  context.drawImage(chickenSprite, 80, 256, 48, 48);
};

Результат: на холсте готовая сцена платформера — голубое небо, зелёная земля внизу, золотая монетка с тёмной рамкой справа в воздухе, и цыплёнок, стоящий на земле слева. Это первый кадр твоей будущей игры.

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

Заметь, как естественно код делится на части по комментариям: «небо», «земля», «монетка», «цыплёнок». Это не случайно — так пишут игры по-настоящему. Сначала ты рисуешь дальний фон, потом то, что ближе, потом героя поверх всего. Когда позже мы добавим движение, каждый кадр будет перерисовывать ровно эти же четыре блока заново, просто с новыми координатами. Так что присмотрись к структуре: ты смотришь на скелет будущей игры, а не на разовый набросок.

Частые ошибки и подводные камни

Вот на чём чаще всего спотыкаются, когда впервые рисуют на canvas. Пробеги глазами заранее — сэкономишь себе полчаса нервов.

  • Рисуешь картинку сразу, без onload. Самая частая ошибка. Если вызвать drawImage сразу после src, картинка ещё не успела загрузиться и на экране ничего не появится — ошибки в консоли тоже не будет, просто пусто. Всегда рисуй спрайт внутри обработчика onload.
  • Забыл beginPath() перед кругом. Если рисовать несколько дуг без beginPath(), они склеиваются в один контур, и при заливке между ними появляются странные линии. Каждую новую фигуру с дугой начинай с beginPath().
  • Путаешь координаты прямоугольника и круга. У fillRect точка (x, y) — это левый верхний угол, а у arcцентр круга. Если монетка оказалась не там, где ждал, дело почти всегда в этом.
  • Ждёшь, что ось y идёт вверх. На canvas y растёт вниз. Чтобы поставить объект ниже, увеличивай y, а не уменьшай. Земля у нас на y = 300 именно потому, что это низ экрана.
  • Цвет «не меняется». fillStyle — это кисточка: она держит последний выбранный цвет, пока ты её не перемакнёшь. Если две фигуры вышли одного цвета — ты просто забыл сменить fillStyle перед второй.
  • Неправильный путь к спрайту. Если в src опечатка или файла нет, картинка не загрузится и onload не сработает. Проверь адрес и срабатывание onerror, если цыплёнок упорно не появляется.

Мини-практика: построй свою площадку

Теперь твоя очередь. Возьми код из четвёртого примера за основу и доработай сцену так, чтобы она стала «живее»:

  1. Добавь солнце — жёлтый круг в правом верхнем углу неба (подсказка: центр где-то в районе x = 420, y = 50, радиус 30).
  2. Нарисуй вторую платформу — узкий коричневый прямоугольник, парящий над землёй, на который цыплёнок потом сможет запрыгнуть.
  3. Поставь три монетки в ряд над платформой. Совет: чтобы не копировать код трижды, нарисуй их в цикле for, меняя только координату x.
  4. Передвинь цыплёнка так, чтобы он стоял на краю первой платформы, а не на земле.

Если всё получилось — у тебя на экране полноценный кадр уровня, собранный своими руками. Сохрани этот код: имена chickenSprite и context мы понесём дальше без изменений.

Итоги и что дальше

Сегодня ты освоил базовый набор команд рисования, на котором держится любая игровая графика:

  • fillRect(x, y, w, h) — прямоугольник по углу;
  • beginPath() + arc(x, y, r, 0, Math.PI * 2) + fill() — круг по центру;
  • fillStyle и strokeStyle + lineWidth + stroke() — заливка и обводка;
  • new Image() + onload + drawImage() — вывод спрайта цыплёнка.

И запомнил три вещи, об которые спотыкаются все: ось y идёт вниз, спрайт рисуют только после onload, а порядок команд = порядок слоёв.

Но пока наш цыплёнок стоит как вкопанный — это всего один кадр. В следующем уроке мы заставим картинку обновляться много раз в секунду: познакомимся с игровым циклом и функцией requestAnimationFrame, которая задаёт ритм игры, как сердцебиение. Тогда цыплёнок впервые задвигается по экрану. Увидимся там.

Проверьте себя
1. Где находится точка с координатами (0, 0) на canvas и куда растёт ось y?
AВ центре холста, y растёт вверх
BВ левом верхнем углу, y растёт вниз
CВ левом нижнем углу, y растёт вверх
DВ правом верхнем углу, y растёт вниз
2. Почему спрайт цыплёнка нужно рисовать внутри обработчика chickenSprite.onload?
AТак быстрее работает drawImage
BИначе картинка нарисуется в неправильном цвете
CКартинка грузится не мгновенно, и до загрузки рисовать нечего
Donload автоматически центрирует спрайт на холсте
3. Какая команда задаёт цвет заливки фигуры?
Acontext.fillStyle
Bcontext.strokeStyle
Ccontext.lineWidth
Dcontext.fillColor
4. Чем отличается смысл координат (x, y) у fillRect и у arc?
AУ обоих это центр фигуры
BУ обоих это левый верхний угол
CУ fillRect это левый верхний угол, у arc — центр круга
DУ fillRect это центр, у arc — левый верхний угол
5. Что означает аргумент Math.PI * 2 в вызове context.arc(x, y, r, 0, Math.PI * 2)?
AРадиус круга в пикселях
BКонечный угол дуги, равный полному кругу
CТолщину обводки
DСкорость рисования
6. Почему фон (небо, землю) рисуют раньше, чем цыплёнка?
AИначе фон не зальётся цветом
BКоманды рисования ложатся слоями: нарисованное позже оказывается сверху
CdrawImage работает только после fillRect
DТак требует функция beginPath