До этого мы меняли кнопки, надписи и карточки на странице. Теперь нарисуем сцену внутри одного элемента Canvas: небо, круглое солнце, землю и заготовку героя. Разберём, откуда берутся координаты и почему изменение данных само по себе не передвигает рисунок.
На холсте появится сцена 480×320. Ты сможешь перенести прямоугольник, изменив две координаты, стереть картинку и восстановить её из данных. Движения по времени и управления здесь пока нет.
Что понадобится
Пригодятся функции с параметрами, обращение к свойствам объекта и поиск HTML-элемента. Если обращение к свойству player.x пока непонятно, вернись к уроку о состоянии игры. Сохранение из предыдущего проекта здесь не требуется: мы начинаем отдельную сцену.
Распакуй архив и открой start/index.html. Рядом должны остаться style.css и game.js. В этом проекте можно открыть HTML прямо в браузере или воспользоваться знакомым локальным сервером. После правки сохраняй файл и обновляй страницу. Пока будет виден белый пустой холст — это ожидаемый старт.
В start/ лежит пустой холст; step-01/ — прямоугольник; step-02/ — сцена с кругом; step-03/ и final/ — перерисовка из данных. solutions/ содержит ответ на самостоятельную задачу. Инструкция по запуску доступна в просмотрщике и ZIP.
Холст и его два размера
Canvas хранит итоговую картинку — сетку пикселей, или bitmap. Нарисованный прямоугольник не становится отдельным DOM-элементом, как кнопка или карточка. Браузер не запоминает, что это «герой»: данные героя будем хранить сами, а Canvas получит команды рисования.
В index.html уже есть такой элемент. Атрибуты width и height задают внутреннее поле координат. Здесь по горизонтали 480 единиц, по вертикали 320.
<canvas
id="game"
width="480"
height="320"
aria-label="Учебная сцена: небо, солнце, земля и герой"
>
Твоему браузеру нужна поддержка Canvas, чтобы показать рисунок.
</canvas>
Текст внутри тега нужен как запасное содержимое для браузера без Canvas. aria-label кратко описывает статичный рисунок для вспомогательных технологий. Это ещё не делает будущую игру полностью доступной: интерактивным правилам и результату позже понадобятся свои понятные элементы интерфейса.
CSS отвечает за размер картинки на странице. В готовом style.css у родителя main стоит max-width: 480px, а у холста — width: 100%; height: auto. На узком экране он уменьшается целиком и сохраняет пропорции. Внутренние координаты остаются прежними. Если дать CSS ширину 480px и высоту 100px, круг станет сплющенным, хотя команда рисования не изменится. Эту разницу описывает введение MDN в Canvas.
Не меняй canvas.width при каждом рисовании: присваивание внутреннего размера очищает bitmap и сбрасывает настройки контекста. В этом проекте размеры постоянны; адаптивность делает CSS.
Шаг 1. Прямоугольник по координатам
Замени комментарий в game.js этим кодом. Скрипт подключён с defer, поэтому к его запуску браузер уже прочитает HTML и найдёт холст по id="game".
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#6c42d8';
ctx.fillRect(80, 180, 32, 40);
getContext('2d') возвращает объект с командами двумерного рисования. Мы сохраняем его в ctx; имя можно было выбрать другим. fillStyle задаёт цвет для последующей заливки. Уже нарисованные пиксели от присваивания цвета не меняются.
Верхний левый угол холста — точка (0, 0). X растёт вправо, Y — вниз. У fillRect(x, y, width, height) первые два аргумента задают верхний левый угол прямоугольника, следующие два — ширину и высоту. В нашем примере правая граница получается 80 + 32 = 112, нижняя — 180 + 40 = 220. Граница — край фигуры, а не дополнительный закрашенный столбец.
Проверь: виден небольшой фиолетовый прямоугольник. Поменяй только 80 на 160: он должен уйти вправо, сохранив размер. Верни 80. Если менять третье число, меняется ширина, а не положение. Результат этого этапа лежит в step-01/.
Шаг 2. Цвет, круг и порядок слоёв
Построим фон в отдельной функции drawScene(). Так его можно будет вызывать повторно. Замени содержимое game.js целиком: здесь сохранены поиск холста и прямоугольник, а между ними появилась сцена.
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
function drawScene() {
ctx.fillStyle = '#e8e2ff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#ffc857';
ctx.beginPath();
ctx.arc(380, 70, 24, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#655a86';
ctx.fillRect(0, 220, canvas.width, 100);
}
drawScene();
ctx.fillStyle = '#6c42d8';
ctx.fillRect(80, 180, 32, 40);
Небо заполняет весь холст. Земля начинается на Y=220 и занимает оставшиеся 100 единиц. Солнце устроено иначе, чем прямоугольник: мы сначала задаём путь, то есть описание контура, затем заливаем его.
beginPath()начинает новый путь. Это не очистка картинки.arc(380, 70, 24, 0, Math.PI * 2)описывает окружность: центр (380,70), радиус 24. Последние два числа — начальный и конечный углы.Math.PI— число π, примерно 3,14. Canvas измеряет углы в радианах:Math.PIсоответствует половине оборота,Math.PI * 2— целому. Здесь нужен полный круг.fill()закрашивает текущий контур цветомfillStyle.
В отличие от fillRect(), метод arc() сам не закрашивает пиксели. И его X/Y задают центр, а не угол. У нашего солнца левый край находится на 356, правый — на 404. Параметры arc в MDN пригодятся, если захочешь нарисовать дугу.
Команды выполняются сверху вниз. Если вызвать заливку неба в самом конце, она закроет и солнце, и героя. Поэтому порядок такой: небо → солнце → земля → герой. Никакого CSS z-index у отдельных фигур здесь нет.
Проверь: прямоугольник стоит нижней гранью на земле, а солнце остаётся круглым при сужении окна. На минуту перенеси drawScene(); после ctx.fillRect(80, 180, 32, 40);. Герой исчезнет под фоном. Верни вызов на место. Это законченная версия step-02/.
Шаг 3. Данные, очистка и повторное рисование
Сейчас координаты спрятаны внутри команды. Вынесем их в знакомый объект player, а рисование целой картинки назовём render(). Эта функция будет читать данные, а не менять их. Полностью замени game.js следующей версией:
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const player = { x: 80, y: 180, width: 32, height: 40 };
function drawScene() {
ctx.fillStyle = '#e8e2ff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#ffc857';
ctx.beginPath();
ctx.arc(380, 70, 24, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#655a86';
ctx.fillRect(0, 220, canvas.width, 100);
}
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawScene();
ctx.fillStyle = '#6c42d8';
ctx.fillRect(player.x, player.y, player.width, player.height);
}
render();
clearRect(0, 0, canvas.width, canvas.height) делает всю область холста прозрачной. Белый цвет после очистки — это фон из CSS, а не белая заливка Canvas. Сама очистка не меняет ни player.x, ни fillStyle, ни текущий путь. Для нового круга поэтому по-прежнему нужен beginPath(). Разница между стиранием пикселей и новым путём показана в справочнике clearRect.
Наше небо и так закрывает весь холст. Явную очистку оставляем, чтобы порядок был виден и продолжал работать, если позже фон станет частичным. Затем восстанавливаем фон и рисуем героя в текущих координатах.
Проверь в консоли разработчика у отдельно открытой страницы final/index.html: выполни команды по очереди. После первой команды координата изменится, но изображение останется прежним. Только вызов render() перестроит картинку.
player.x = 160;
render();
Теперь выполни очистку отдельно:
ctx.clearRect(0, 0, canvas.width, canvas.height);
console.log(player.x);
Рисунок исчез, но в консоли осталось 160. Вызови render(); — сцена вернётся с героем в новой позиции. Если повторить render(), герой не поедет ещё дальше: функция рисует уже имеющееся состояние. Обновление страницы восстановит начальное x: 80 из файла.
Самостоятельно: поставь героя в проход между стойками
На базе final/ построй ворота. Координаты заданы намеренно: задача — прочитать их и определить место для героя, а не подобрать красивую картинку на глаз. Автоматическое движение не требуется.
| Часть | X | Y | Ширина | Высота |
|---|---|---|---|---|
| Левая стойка | 180 | 160 | 12 | 60 |
| Правая стойка | 232 | 160 | 12 | 60 |
| Перекладина | 180 | 148 | 64 | 12 |
Добавь их рисование после фона. Размер героя оставь 32×40, Y=180. Выбери X так, чтобы он целиком поместился между стойками и не касался их. Затем перенеси героя, вызови render() и убедись, что старой копии нет. Объясни, почему X=228 не подходит, даже если левый край героя ещё находится в проходе.
Подсказка: какие границы сравнивать
Проход начинается после ширины левой стойки, а заканчивается у левого края правой. У героя нужно проверить обе стороны: player.x и player.x + player.width.
Более конкретная подсказка
Проход занимает X от 192 до 232. Его ширина 40, у героя — 32. Останется 8 единиц свободного места; можно распределить их поровну по краям.
Разбор после своей попытки
X=196 оставляет по четыре единицы с каждой стороны: правая граница героя будет 228. При X=228 правая граница станет 260 и зайдёт за стойку. Один рабочий вариант есть в solutions/ общего комплекта. В нём ворота рисуются после drawScene(), затем герой. Эти проверки пока делаешь ты; столкновений в программе ещё нет.
Если результат не совпал
| Что видно | Что проверить |
|---|---|
| Ошибка про null или getContext | Одинаковый ли id у холста и в getElementById? Есть ли defer у подключения game.js? |
| Белый холст, ошибок нет | Вызвана ли render()? Не нарисован ли фон после героя? Не осталась ли отдельная clearRect в конце файла? |
| Круг пропал или соединён с чужой фигурой | Перед arc нужен beginPath, после — fill. Проверь положительный радиус. |
| Цвет не изменился | fillStyle должен стоять до нужной заливки. После правки данных надо перерисовать картинку. |
| Круг вытянулся | Внутренний размер 480×320 и пропорции CSS должны совпадать. Для адаптивной высоты оставь auto. |
| После переноса видны две копии | Не рисуй только героя поверх прежних пикселей: используй полную render с очисткой и фоном. |
Что должно получиться
Ты можешь назвать верхний левый угол и размер прямоугольника, отличить их от центра и радиуса круга, предсказать перекрытие фигур по порядку команд. Очистка стирает изображение, но сохраняет данные; render() восстанавливает сцену. В самостоятельной версии герой помещается в проход целиком, а при сужении страницы координаты и пропорции сохраняются.
Попробуй без подсматривания восстановить две строки, которые задают цвет и рисуют прямоугольник из player. Затем объясни, почему присваивание player.x = 160 не равнозначно перемещению DOM-элемента. Если эти проверки получились, обязательная часть урока закончена.
Справочное продолжение: другие команды Canvas
Здесь можно попробовать другие команды Canvas. К этим примерам удобно вернуться позже. Выполняй их по отдельности: не вставляй все подряд в рабочую render(). Для них нужны уже созданные canvas и ctx.
Контур прямоугольника, линии и треугольник
strokeStyle задаёт цвет обводки, lineWidth — её толщину. strokeRect() рисует только контур прямоугольника. Линия проходит по границе фигуры и выступает на половину своей толщины в обе стороны.
ctx.strokeStyle = '#6c42d8';
ctx.lineWidth = 3;
ctx.strokeRect(40, 40, 100, 60);
У произвольного контура сначала задают путь. moveTo() переводит начало в точку без соединительной линии; lineTo() добавляет отрезок. stroke() обводит созданный путь. Новый moveTo() внутри пути позволяет начать отдельный отрезок, например второй ус кота.
ctx.beginPath();
ctx.moveTo(40, 140);
ctx.lineTo(100, 100);
ctx.lineTo(160, 140);
ctx.stroke();
Чтобы замкнуть путь до начальной точки, вызови closePath(). Для заполненного треугольника используй fill(); при необходимости после заливки можно обвести тот же путь. Закрытие пути не стирает его и не рисует само по себе.
ctx.fillStyle = '#ffc857';
ctx.beginPath();
ctx.moveTo(200, 140);
ctx.lineTo(240, 80);
ctx.lineTo(280, 140);
ctx.closePath();
ctx.fill();
ctx.stroke();
Полукруг и текст
Дуга от 0 до Math.PI идёт по нижней половине окружности: в Canvas Y растёт вниз. Здесь stroke() оставит только дугу, а fill() даст заполненный полукруг.
ctx.beginPath();
ctx.arc(350, 100, 30, 0, Math.PI);
ctx.stroke();
font задаёт размер и семейство шрифта, fillText(text, x, y) выводит строку. Y по умолчанию обозначает базовую линию текста, а не его верхний край. textAlign = 'center' ставит горизонтальный центр надписи на выбранный X. Текст становится пикселями: изменение строки в переменной потребует перерисовки.
ctx.fillStyle = '#211939';
ctx.font = '24px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Сцена готова', canvas.width / 2, 60);
Изображение: сначала загрузка, затем drawImage
Фигуры можно заменить файлом изображения. new Image() создаёт объект изображения, src задаёт путь. Положи свою картинку player.png рядом с HTML. В отличие от прямоугольника, она не доступна мгновенно: рисуем после события загрузки. Пример показывает только один ресурс и не заменяет систему загрузки целой игры.
const playerImage = new Image();
playerImage.onload = function () {
ctx.drawImage(playerImage, 100, 100, 64, 64);
};
playerImage.onerror = function () {
console.log('Не удалось загрузить player.png');
ctx.fillStyle = '#6c42d8';
ctx.fillRect(100, 100, 64, 64);
};
playerImage.src = 'player.png';
onload и onerror получают функции, которые браузер вызовет при успехе или ошибке. Обработчики стоят перед присваиванием пути. У drawImage(image, x, y, width, height) последние два аргумента задают размер рисунка на холсте. Можно вызвать вариант с тремя аргументами и использовать исходный размер файла. Неверный путь проверь намеренно: в этом опыте должен появиться запасной прямоугольник. Подробно ожидание нескольких ресурсов рассматривается отдельно, после того как появится сама игра.
Как работает анимация
Чтобы получать меняющуюся картинку, нужно обновлять данные и повторять рисование. requestAnimationFrame(callback) просит браузер вызвать функцию перед очередной отрисовкой страницы. Это запрос одного кадра; для продолжения нужен следующий запрос. Он не обещает ровно 60 кадров в секунду.
Что выбирать: Canvas, HTML или SVG
HTML подходит для кнопок, форм и текстового интерфейса: элементы остаются доступны через DOM. SVG описывает векторные фигуры отдельными элементами. Canvas хранит итоговый bitmap и удобен там, где ты сам регулярно перестраиваешь сцену из данных. Эти подходы можно сочетать: Canvas для мира, HTML для управления и результата. 2d — двумерный контекст; трёхмерные сцены требуют других средств, например WebGL. Учить все методы до первой игры не нужно.
Справочник для дальнейших опытов: фигуры и пути в MDN, текст, изображения.