Заменим прямоугольник знакомым чёрным котом. Тело, уши, глаза и хвост будут собраны из команд Canvas и привязаны к одной точке. Тогда перенос героя будет менять весь рисунок, а не отдельную его часть.

Результат урока

Функция drawHero(x, y) рисует кота в указанном месте. Координаты хранятся в player, а render() строит сцену по этим данным. Это пока статичный персонаж: стрелки и касания его не перемещают.

Открыть результат отдельно

Какие файлы понадобятся

Нужен результат первого урока Canvas: ты умеешь получить контекст, нарисовать прямоугольник и круг, очистить картинку и вызвать render(). Функции с параметрами и объекты здесь уже знакомые инструменты.

start/ в архиве — точная копия предыдущего final/, без самостоятельных ворот. Открой start/index.html: должны появиться небо, солнце, земля и фиолетовый прямоугольник. Править будем game.js. HTML и CSS остаются прежними; три файла должны лежать рядом. Открытие напрямую в браузере и локальный сервер подходят одинаково.

Исходники урока

start/ — предыдущая сцена; step-01/ — тело; step-02/ — минимальный кот; step-03/ — лицо и усы; step-04/ и final/ — хвост и отделка. В solutions/ — самостоятельная версия с двумя котами. Инструкция по запуску доступна в просмотрщике и ZIP.

Скачать ZIP

Одна опорная точка для всех частей

Если записать глаз в точке (92,202), а тело в (96,205), картинка будет выглядеть правильно ровно в одном месте. Чтобы перенести её, пришлось бы искать все числа. Вместо этого зададим расстояния от общего верхнего левого угла.

const player = { x: 80, y: 180, width: 32, height: 40 };

Область героя имеет размер 32×40. x и y задают её верхний левый угол; это не центр тела. Центр круглого тела находится на 16 правее и на 25 ниже этой точки. Поэтому в любой позиции его координаты вычисляются как x + 16 и y + 25.

ЧастьПоложение относительно x,yРазмер
ТелоЦентр (16,25)Радиус 12
Вершины ушей(7,2) и (25,2)Треугольники
ГлазаЦентры (12,22) и (20,22)Радиус 3
НосЦентр (16,27)Радиус 1,8
ХвостЦентр дуги (25,28)Радиус 5, толщина 3

Эти небольшие числа описывают форму, а player.x и player.y — положение всей фигуры. Свойства width и height пока только описывают занимаемую область для будущих правил. Изменение player.width само по себе не масштабирует кота: команды ниже используют фиксированные смещения.

Шаг 1. Тело через функцию с параметрами

Оставь начало game.js, объект player и drawScene(). Перед render() добавь две функции:

function drawBody(x, y) {
  ctx.fillStyle = '#111111';
  ctx.beginPath();
  ctx.arc(x + 16, y + 25, 12, 0, Math.PI * 2);
  ctx.fill();
}

function drawHero(x, y) {
  drawBody(x, y);
}

drawBody получает координаты как параметры. Она не читает player напрямую, поэтому позже сможет нарисовать тело другого кота. drawHero собирает персонажа из частей: пока внутри только тело. Объявление функции ничего не рисует — нужна цепочка вызовов.

Теперь замени прежнюю функцию render() и её вызов в конце файла:

function render() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawScene();
  drawHero(player.x, player.y);
}

render();

Получается цепочка render → drawHero → drawBody. Значения player.x и player.y проходят по ней через параметры. Прямоугольник больше не рисуем.

Проверь: вместо прямоугольника появился чёрный круг. В отдельно открытой версии выполни в консоли player.x = 160; render();. Круг переедет вправо, а старый исчезнет. Для сравнения есть step-01/.

Шаг 2. Минимальный кот: уши и глаза

Ухо — треугольник. Круг мы описывали через arc(), теперь зададим контур отрезками. moveTo() выбирает начальную точку без линии; lineTo() добавляет линию до следующей; closePath() соединяет конец с началом. После этого fill() зальёт контур. Перед каждой отдельной фигурой нужен beginPath().

У кота два одинаковых уха по разные стороны от середины. Дополнительный параметр direction принимает −1 для левого и 1 для правого. Например, 16 + direction * 9 даёт 7 слева и 25 справа. Y не меняется: мы отражаем только горизонтальные смещения. Это обычное умножение, не специальная команда Canvas.

Добавь перед drawHero() функцию уха:

function drawEar(x, y, direction) {
  ctx.fillStyle = '#111111';
  ctx.beginPath();
  ctx.moveTo(x + 16 + direction * 11, y + 18);
  ctx.lineTo(x + 16 + direction * 9, y + 2);
  ctx.lineTo(x + 16 + direction * 1, y + 14);
  ctx.closePath();
  ctx.fill();
}

Глаз состоит из белого круга и меньшего чёрного зрачка. Локальные переменные eyeX и eyeY дают обоим кругам один центр. В JavaScript дробная часть записывается через точку: 1.2. Добавь следующую функцию рядом:

function drawEye(x, y, direction) {
  const eyeX = x + 16 + direction * 4;
  const eyeY = y + 22;

  ctx.fillStyle = '#ffffff';
  ctx.beginPath();
  ctx.arc(eyeX, eyeY, 3, 0, Math.PI * 2);
  ctx.fill();

  ctx.fillStyle = '#111111';
  ctx.beginPath();
  ctx.arc(eyeX, eyeY, 1.2, 0, Math.PI * 2);
  ctx.fill();
}

Замени drawHero() на сборку минимального кота. Сначала тело, потом уши и глаза: поздние фигуры рисуются поверх предыдущих.

function drawHero(x, y) {
  drawBody(x, y);
  drawEar(x, y, -1);
  drawEar(x, y, 1);
  drawEye(x, y, -1);
  drawEye(x, y, 1);
}

Проверь: теперь есть узнаваемый кот с двумя ушами и глазами. Поставь player.x = 160; player.y = 100; render();. Тело, оба уха и оба глаза должны переехать вместе. Если часть осталась на месте, в её координатах потерялись x + или y +. Это версия step-02/.

Открыть результат отдельно

Минимальная фигура уже закончена

Для переноса и будущего движения этого кота достаточно. Дальше идут декоративные части того же рисунка. Можно сначала выполнить самостоятельную задачу на этой версии, а отделку разобрать позже. В следующих занятиях используется комплект final/ с уже собранными деталями: принцип передачи x,y там тот же.

Шаг 3. Нос, рот и усы

Нос рисуется знакомым кругом. Рот состоит из двух дуг. В отличие от заливки, stroke() рисует линию вдоль пути; её цвет задаёт strokeStyle, толщину — lineWidth. Дуга от 0 до Math.PI — нижняя половина окружности. У каждой половины рта свой вызов функции и свой новый путь, поэтому между ними не возникает случайной соединительной линии.

Добавь эти функции перед drawHero():

function drawNose(x, y) {
  ctx.fillStyle = '#ffffff';
  ctx.beginPath();
  ctx.arc(x + 16, y + 27, 1.8, 0, Math.PI * 2);
  ctx.fill();
}

function drawMouth(x, y, direction) {
  ctx.strokeStyle = '#ffffff';
  ctx.lineWidth = 1;
  ctx.beginPath();
  ctx.arc(x + 16 + direction * 2, y + 29, 2, 0, Math.PI);
  ctx.stroke();
}

Усы — три отдельных отрезка с каждой стороны. После каждого lineTo() следующий moveTo() начинает новый отрезок, а не соединяет конец прошлого с началом следующего. Один stroke() в конце рисует все три. Так получается функция:

function drawWhiskers(x, y, direction) {
  ctx.strokeStyle = '#ffffff';
  ctx.lineWidth = 1;
  ctx.beginPath();
  ctx.moveTo(x + 16 + direction * 4, y + 27);
  ctx.lineTo(x + 16 + direction * 11, y + 24);
  ctx.moveTo(x + 16 + direction * 4, y + 29);
  ctx.lineTo(x + 16 + direction * 12, y + 29);
  ctx.moveTo(x + 16 + direction * 4, y + 31);
  ctx.lineTo(x + 16 + direction * 10, y + 34);
  ctx.stroke();
}

В drawHero() после двух вызовов drawEye добавь:

drawNose(x, y);
drawMouth(x, y, -1);
drawMouth(x, y, 1);
drawWhiskers(x, y, -1);
drawWhiskers(x, y, 1);

Проверь: виден белый нос, две половинки рта и по три уса. Перенеси кота в обеих координатах. Усы не должны соединиться в зигзаг или остаться на старом месте. Сравни с step-03/. Цвет и толщину каждая функция задаёт сама, чтобы не зависеть от того, чем рисовали предыдущую деталь.

Шаг 4. Хвост, детали ушей и обводка

Хвост — толстая незамкнутая дуга. Начальный угол -Math.PI / 2 находится сверху окружности; конечный Math.PI / 1.5 — 120°. По умолчанию дуга идёт по часовой стрелке. Так получается закруглённый хвост справа от тела. Добавь функцию перед drawHero():

function drawTail(x, y) {
  ctx.strokeStyle = '#111111';
  ctx.lineWidth = 3;
  ctx.beginPath();
  ctx.arc(x + 25, y + 28, 5, -Math.PI / 2, Math.PI / 1.5);
  ctx.stroke();
}

Вызов drawTail(x, y); поставь первым внутри drawHero(), перед телом. Тело закроет начало хвоста. Если рисовать хвост последним, толстая линия пересечёт лицо.

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

ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.moveTo(x + 16 + direction * 9, y + 16);
ctx.lineTo(x + 16 + direction * 8, y + 7);
ctx.lineTo(x + 16 + direction * 4, y + 14);
ctx.closePath();
ctx.fill();

Осталась тонкая обводка тела. В конце drawBody(), сразу после ctx.fill();, добавь:

ctx.strokeStyle = '#352b48';
ctx.lineWidth = 1;
ctx.stroke();

Здесь stroke() обводит тот же круг, который только что залили: между fill() и stroke() нет beginPath(). Обводка толщиной 1 выступает на половину единицы с каждой стороны контура. Мы оставили запас вокруг частей, чтобы рисунок вместе с хвостом и обводкой помещался в область 32×40.

Итоговая сборка частей выглядит так:

function drawHero(x, y) {
  drawTail(x, y);
  drawBody(x, y);
  drawEar(x, y, -1);
  drawEar(x, y, 1);
  drawEye(x, y, -1);
  drawEye(x, y, 1);
  drawNose(x, y);
  drawMouth(x, y, -1);
  drawMouth(x, y, 1);
  drawWhiskers(x, y, -1);
  drawWhiskers(x, y, 1);
}

Проверь: в (0,0) уши не обрезаются, а при player.x = 448; player.y = 280; render(); весь кот помещается в правом нижнем углу. Числа получились из 480 − 32 и 320 − 40. Верни начальные (80,180). Финал совпадает с step-04/; первое встроенное демо показывает именно его.

Самостоятельно: два кота из одной функции

Сделай второго кота в (300,180), не копируя функции тела, ушей и лица. Координаты второго храни в отдельном объекте. Чтобы экземпляры различались, добавь функцию drawScarf(x, y, color) — шарф с цветом, переданным через параметр.

Шарф состоит из двух прямоугольников относительно опорной точки: полоса (7,33,18,3) и свисающий конец (20,36,3,3). У первого кота шарф жёлтый, у второго — другого цвета. Рисуй шарф поверх тела. Изменение координат любого кота должно переносить его целиком вместе с шарфом; второй при этом остаётся на месте. Размеры обоих рисунков сохраняются внутри 32×40.

Сначала напиши решение сам. Проверяй не только картинку в начальном положении: перенеси первого в (160,100), второго в (350,180), затем вызови render() два раза. Старых копий и отдельных «забытых» шарфов быть не должно.

Подсказка: не привязывай функцию к одному объекту

Функция рисования шарфа должна получать x,y,color и использовать только их для своей детали. Объекты player и второго кота выбираются в render(), где вызываются функции.

Подсказка с одним прямоугольником

Полоса начинается в x + 7, y + 33, её размеры 18×3. Не используй внутри функции фиксированные 80/180 или имя player. Цвет присвой ctx.fillStyle перед рисованием.

Разбор после попытки

В solutions/ добавлен объект friend с координатами и scarfColor. Одна и та же drawHero вызывается дважды. После каждого кота рисуется шарф с теми же x,y. Две функции не меняют объекты — только изображение.

function drawScarf(x, y, color) {
  ctx.fillStyle = color;
  ctx.fillRect(x + 7, y + 33, 18, 3);
  ctx.fillRect(x + 20, y + 36, 3, 3);
}

Проверка friend.x = 350; render(); должна перенести только второго кота. Так мы используем параметры для повторного рисунка, а не создаём вторую независимую копию длинного кода.

Если кот рассыпался или изменил цвет

ОшибкаПричина и проверка
Ухо осталось на местеПроверь каждую точку moveTo/lineTo: к горизонтальному смещению добавляется x, к вертикальному — y.
Оба глаза с одной стороныНужны два вызова с direction −1 и 1. Умножается смещение от середины, а не вся координата x.
Лицо стало чёрнымПроверь порядок вызовов: тело перед глазами и носом. Каждая часть задаёт свой fillStyle.
Появились лишние линииДля новой фигуры вызывай beginPath, для отдельного уса — moveTo. clearRect стирает пиксели, а не текущий путь.
Обводка оказалась на другой частиstroke для тела должен идти сразу после его fill, до создания следующего пути.
После width=64 кот не увеличилсяwidth описывает область, но не участвует в смещениях. Этот урок меняет положение, а не масштаб.
После переноса остался следВызывай render целиком: очистка, фон, все части героя. Один drawHero рисует поверх прежних пикселей.

Проверка готовности

Ты можешь объяснить, где находится опорная точка и почему центр тела записан как x + 16, y + 25. Весь кот переносится двумя числами, помещается у краёв холста, повторный render() не меняет состояние. Второй экземпляр рисуется теми же функциями и отличается параметром шарфа. Попробуй без готового образца восстановить вызовы двух глаз и назвать, что изменится, если поменять местами рисование тела и хвоста.

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

Книга по JavaScript для начинающих

JavaScript с нуля: учимся программировать, создавая игры

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

Подробнее о книге