Книга: Выразительный JavaScript
Рисование
Рисование
Инкапсулировать код для рисования мы будем, введя объект display
, который выводит уровень на экран. Тип экрана, который мы определяем, зовётся DOMDisplay
, потому что он использует элементы DOM для показа уровня.
Мы используем таблицу стилей для задания цветов и других фиксированных свойств элементов, составляющих игру. Было бы возможно непосредственно назначать стиль элементу через свойство style
при его создании, но программа в этом случае стала бы излишне многословной.
Следующая вспомогательная функция даёт простой способ создания элемента с назначением класса.
function elt(name, className) {
var elt = document.createElement(name);
if (className) elt.className = className;
return elt;
}
Экран создаём, передавая ему родительский элемент, к которому необходимо подсоединиться, и объект уровня.
function DOMDisplay(parent, level) {
this.wrap = parent.appendChild(elt("div", "game"));
this.level = level;
this.wrap.appendChild(this.drawBackground());
this.actorLayer = null;
this.drawFrame();
}
Используя тот факт, что appendChild
возвращает добавленный элемент, мы создаём окружающий элемент wrapper
и сохраняем его в свойстве wrap
.
Неизменный фон уровня рисуется единожды. Актёры перерисовываются каждый раз при обновлении экрана. Свойство actorLayer
используется в drawFrame
для отслеживания элемента, содержащего актёра – чтобы их было легко удалять и заменять.
Координаты и размеры измеряются в единицах, относительных к размеру решётки так, что дистанция в единицу означает один элемент решётки. Когда мы задаём размеры в пикселях, нам нужно будет масштабировать координаты – игра была бы очень мелкой, если б один квадратик задавался одним пикселем. Переменная scale
даёт количество пикселей, которое занимает один элемент решётки.
var scale = 20;
DOMDisplay.prototype.drawBackground = function() {
var table = elt("table", "background");
table.style.width = this.level.width * scale + "px";
this.level.grid.forEach(function(row) {
var rowElt = table.appendChild(elt("tr"));
rowElt.style.height = scale + "px";
row.forEach(function(type) {
rowElt.appendChild(elt("td", type));
});
});
return table;
};
Как мы уже упоминали, фон рисуется через элемент <table>
. Это удобно соответствует тому факту, что уровень задан в виде решётки – каждый ряд решётки превращается в ряд таблицы (элемент <tr>
). Строки решётки используются как имена классов ячеек таблицы (<td>
). Следующий CSS приводит фон к необходимому нам внешнему виду:
.background { background: rgb(52, 166, 251);
table-layout: fixed;
border-spacing: 0; }
.background td { padding: 0; }
.lava { background: rgb(255, 100, 100); }
.wall { background: white; }
Некоторые из настроек (table-layout
, border-spacing
и padding
) используются для подавления нежелательного поведения по умолчанию. Не нужно, чтобы вид таблицы зависел от содержимого ячеек, и не нужны пробелы между ячейками или отступы внутри них.
Правило background
задаёт цвет фона. CSS разрешает задавать цвета словами (white
) и в формате rgb(R, G, B)
, где красная, зелёная и синяя компоненты разделены на три числа от 0 до 255. То есть, в записи rgb(52, 166, 251)
красный компонент равен 52, зелёный 166 и синий 251. Поскольку синий компонент самый большой, результирующий цвет будет синеватым. Вы можете видеть, что самый большой компонент в правиле .lava
– красный.
Каждый актёр рисуется созданием элемента DOM и заданием позиции и размера, основываясь на свойства актёра. Значения надо умножать на масштаб scale
, чтобы переходить от единиц игры к пикселям.
DOMDisplay.prototype.drawActors = function() {
var wrap = elt("div");
this.level.actors.forEach(function(actor) {
var rect = wrap.appendChild(elt("div",
"actor " + actor.type));
rect.style.width = actor.size.x * scale + "px";
rect.style.height = actor.size.y * scale + "px";
rect.style.left = actor.pos.x * scale + "px";
rect.style.top = actor.pos.y * scale + "px";
});
return wrap;
};
Чтобы задать элементу больше одного класса, мы разделяем их имена пробелами. В коде CSS класс actor
задаёт позицию absolute
. Имя типа используется в дополнительном классе для задания цвета. Нам не надо заново определять класс lava
, потому что мы повторно используем класс для лавы из решётки, который мы определили ранее.
.actor { position: absolute; }
.coin { background: rgb(241, 229, 89); }
.player { background: rgb(64, 64, 64); }
При обновлении экрана метод drawFrame
удаляет старое изображение актёра, если оно было, и затем перерисовывает его на новой позиции. Напрашивается использование элементов DOM в качестве актёров, но для этого нам потребовалось бы передавать слишком много дополнительной информации между кодом дисплея и кодом симуляции. Надо было бы связать актёров с элементами DOM, и код рисования должен был бы удалять элементы при исчезновении актёров. Так как обычно в игре актёров совсем немного, их перерисовка отнимает немного ресурсов.
DOMDisplay.prototype.drawFrame = function() {
if (this.actorLayer)
this.wrap.removeChild(this.actorLayer);
this.actorLayer = this.wrap.appendChild(this.drawActors());
this.wrap.className = "game " + (this.level.status || "");
this.scrollPlayerIntoView();
};
Добавив в обёртку wrapper
текущий статус уровня в виде класса, мы можем стилизовать персонажа по-разному в зависимости от того, выиграна игра или проиграна. Мы добавим правило CSS, которое работает, только когда у игрока есть потомок с заданным классом.
.lost .player {
background: rgb(160, 64, 64);
}
.won .player {
box-shadow: -4px -7px 8px white, 4px -7px 8px white;
}
После прикосновения к лаве цвета игрока становятся тёмно-красными, будто он сгорел. Когда последняя монетка собрана, мы используем размытые тени для создания эффекта сияния.
Нельзя предполагать, что уровни всегда вмещаются в окно просмотра. Поэтому нам нужен scrollPlayerIntoView
– он нужен для гарантии того, что если уровень не влезает в окно, он будет прокручен, чтобы игрок всегда был близко к центру. Следующий CSS задаёт обёртке максимальный размер, и гарантирует, что всё вылезающее за него не видно. Также мы задаём элементу позицию relative
, чтобы актёры внутри него располагались относительно его левого верхнего угла.
.game {
overflow: hidden;
max-width: 600px;
max-height: 450px;
position: relative;
}
В методе scrollPlayerIntoView
мы находим положение игрока и обновляем позицию прокрутки обёртывающего элемента. Мы меняем позицию, работая со свойствами scrollLeft
и scrollTop
, когда игрок подходит близко к краю.
DOMDisplay.prototype.scrollPlayerIntoView = function() {
var width = this.wrap.clientWidth;
var height = this.wrap.clientHeight;
var margin = width / 3;
// The viewport
var left = this.wrap.scrollLeft, right = left + width;
var top = this.wrap.scrollTop, bottom = top + height;
var player = this.level.player;
var center = player.pos.plus(player.size.times(0.5))
.times(scale);
if (center.x < left + margin)
this.wrap.scrollLeft = center.x - margin;
else if (center.x > right - margin)
this.wrap.scrollLeft = center.x + margin - width;
if (center.y < top + margin)
this.wrap.scrollTop = center.y - margin;
else if (center.y > bottom - margin)
this.wrap.scrollTop = center.y + margin - height;
};
Метод нахождения центра игрока показывает, как методы наших типов Vector
позволяют записывать расчёты, производимые с объектами, наглядно. Чтобы найти центр актёра, мы добавляем его позицию (его левый верхний угол) и половину высоты. Это центр в координатах уровня, но нам он нужен в координатах пикселей, поэтому мы умножаем результирующий вектор на наш масштаб.
Затем серия проверок подтверждает, что игрок не находится вне доступного пространства. Иногда в результате будут заданы неправильные координаты прокрутки, ниже нуля или больше, чем размер прокручиваемого элемента. Но это не страшно – DOM автоматически ограничит их допустимыми значениями. Если назначить scrollLeft
значение -10, он будет равен 0.
Было бы немного проще пробовать прокручивать позицию игрока в центр окна просмотра – но это создаёт неприятный дрожащий эффект. Во время прыжков вид будет постоянно двигаться вверх и вниз. Гораздо приятнее иметь «нейтральную» зону в середине экрана, где можно двигаться, не вызывая прокрутки.
Ещё нам необходимо очищать уровень, когда мы переходим на следующий или начинаем заново.
DOMDisplay.prototype.clear = function() {
this.wrap.parentNode.removeChild(this.wrap);
};
Теперь мы можем показать наш уровень.
<link rel="stylesheet" href="css/game.css">
<script>
var simpleLevel = new Level(simpleLevelPlan);
var display = new DOMDisplay(document.body, simpleLevel);
</script>
Тэг <link>
при использовании с rel="stylesheet"
позволяет загружать файл с CSS. Файл game.css
содержит необходимые для игры стили.
- Рисование фигур и линий
- Рисование линий и фигур
- Рисование таблицы
- Урок 5.7. Рисование в Microsoft Word
- Рисование фигур с помощью OnPaint
- Рисование текста
- Глава 7. Осваиваем рисование и работу с графическими объектами
- 21.4.5. Рисование и заливка кривых
- 16. Рисование на холсте
- 13.9. Использование возможностей панели Рисование
- Базовое геометрическое рисование
- Рисование непосредственно в форме