Заменим прямоугольник знакомым чёрным котом. Тело, уши, глаза и хвост будут собраны из команд 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.
Одна опорная точка для всех частей
Если записать глаз в точке (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 с нуля: учимся программировать, создавая игрыПошаговый формат с маленькими практическими задачами: от базового синтаксиса до первых игровых механик без перегруза теорией.
Подробнее о книге