Saturday, October 29, 2011

Простая игра на HTML5 Canvas – часть 2


Персонаж

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

Давайте создадим объект, представляющий главного героя со всеми необходимыми атрибутами и методами. Назовем объект ‘player’ (англ. – игрок). Далее мной выбран не лучший способ описания объекта в JS, потому что все атрибуты являются доступными, а закрытые не применяются вообще. Но это простейшее решение, позволяющее уложиться в запланированные 10КБ, а главное что это работает. Если вам захочется узнать о закрытых атрибутах и методах, наследовании и т.п. прочитайте об этом в учебнике по JavaScript (на англ.языке можно прочитать тут - http://jibbering.com/faq/notes/closures/ ). Также важно помнить что если вы соберетесь уменьшить код с помощью онлайн инструментов, таких как ‘Closure Compiler’ (http://closure-compiler.appspot.com/home) что имена аргументов объектов он не сокращает. Именно поэтому я использую 2-х буквенные сокращения для описания объекта, например, ‘player.im’ вместо ‘player.image’. Итак, вот код, который добавляется в наш game.js:
var player = new (function(){
//создаем новый объект основанный на функции
//и присваиваем его переменной ‘player’

    var that = this;
//'that' – псевдоним этого объекта

//attributes
    that.image = new Image();
    that.image.src = "angel.png";
//создаем новый Image элемент и
//'angel.png' закачиваем в него

    that.width = 65;
//ширина одного кадра
    that.height = 95;
//высота одного кадра (рисунок в 2 раза выше, ведь в нем 2 кадра)

    that.X = 0;
    that.Y = 0;
//X&Y позиция объекта на холсте

//далее описываем методы
    that.setPosition = function(x, y){
    that.X = x;
    that.Y = y;
}

    that.draw = function(){
        try {
            ctx.drawImage(that.image, 0, 0, that.width, that.height, that.X, that.Y, that.width, that.height);
//отрисовываем наш персонаж функцией drawImage(объект-рисунок, X источника,
//Y источника, ширина источника, высота источника, X назначения (X позиция),
//Y назначения (Y позиция), ширина назначения, высота назначения)
        } catch (e) {
//Иногда, если изображение персонажа слишком велико и еще не успело
//загрузиться до вывода первого кадра, JavaScript выдаст ошибку и прекратит
//выполнение всего. Чтобы этого избежать, мы перехватываем ошибку и повторим
//попытку рисования в следующем кадре. Это незаметно для пользователей ведь
//частота кадров 50 в секунду.
        }
    }
})();
//мы сразу же применим функции описанные выше
//и создадим переменную ‘player’
//как новый объект

player.setPosition(~~((width-player.width)/2),  ~~((height - player.height)/2));
//наш персонаж готов, давайте переместим его
//в центр экрана,
//'~~' возвращает округленное вниз целое значение
//переменной с плавающей точкой, как это делает Math.floor()

Итак, теперь  необходимо перерисовывать нашего ангела на каждом кадре в игровом цикле с помощью  функции ‘player.draw’:
var GameLoop = function(){
    clear();
    MoveCircles(5);
    DrawCircles();
    player.draw();
    gLoop = setTimeout(GameLoop, 1000 / 50);
}

Ну а как насчет анимации? Спрайт (файл с изображением ангела) имеет 2 кадра, но только один из них перерисовывается на каждом кадре. Для того, чтобы анимировать наш персонаж потребуются дополнительные атрибуты и небольшие изменения метода ‘draw’:
var player = new (function(){
(...)
    that.frames = 1;
//нумерация кадров начинается с нуля
    that.actualFrame = 0;
//текущий кадр
    that.interval = 0;
//нам не прийдется переключать кадры
//в игровом цикле, ‘interval’ позаботится об этом

    that.draw = function(){
        try {
            ctx.drawImage(that.image, 0, that.height * that.actualFrame, that.width, that.height, that.X, that.Y, that.width, that.height);
//третий аргумент умножается на номер текущего кадра, чтобы показать нужную
//часть исходного изображения
        } catch (e) {};

        if (that.interval == 4 ) {
            if (that.actualFrame == that.frames) {
                that.actualFrame = 0;
            } else {
                that.actualFrame++;
            }
            that.interval = 0;
        }
    that.interval++;
//выше показана простая логика переключающая кадры каждые 4 итерации

    }
})();

Спасибо за внимание! Результат работы проделанной в данной части урока, как обычно, можно посмотреть по адресу: http://jsbin.com/orohe4/, а исходники скачать тут: http://github.com/michalbe/Simple-game-with-HTML5-Canvas/tree/master/part2/

КОПИРАЙТЫ.

Автор: Михал Будзинский https://twitter.com/#!/@michalbe
Автор перевода: Андрей Семенов https://twitter.com/#!/a_semenov79

Thursday, October 27, 2011

Простая игра на HTML5 Canvas - часть 1


Предисловие от автора перевода.

Итак, я наконец-то подготовил перевод статьи Михала Будзинского открывающей нам новый web-стандарт HTML5. Этот стандарт еще не имеет утвержденной спецификации и представлен на сайте w3.org только как рабочий черновик, но уже активно продвигается производителями браузеров. Новшества, вносимые новой версией HTML (язык разметки гипертекстов) сближают его с языками программирования. Благодаря активному развитию социальных сетей, «облачных» сервисов и т.п. все яснее прослеживается необходимость улучшения интеграции мультимедийного контента и новый тэг <canvas> стал одним из решений данного вопроса представленный консорциумом.
Движок игры написан на JavaScript и те, кто еще не знаком с этим языком программирования найдут здесь также замечательные примеры для его начального освоения.

Введение.

StH - очень простой клон Doodle Jump, но чтобы быть до конца честным, я был вдохновлен Icy Tower. Ну да ладно.
Цель игры состоит в том, чтобы управлять маленьким ангелом и скакать на двух видах платформ - оранжевые (обычные) и зеленые (трамплины). Игра заканчивается, когда ангел падает за нижний край экрана.
Я создал эту игру за 8 часов и позже, когда довольно долго играл, я обнаружил немного ошибок,  так что в этом уроке, я хочу исправить их все. Давайте приступим!

Фон.

Поскольку вся игра, включая изображения и сценарии (скрипты), не должна быть больше 10КБ, я не захотел использовать растровое изображение на фоне. Гораздо экономичнее рисовать используя функции рисования <canvas>.
Прежде всего, мы нуждаемся в небольшом HTML, ничего особенного, только один <canvas> элемент с некоторым уникальным id, немного CSS и несуществующий, пока еще, game.js:
<html>
  <head>
    <title> Простая игра с HTML5 Canvas </ title>
  <style>
  body {margin:0px; padding:0px; text-align: center}
  canvas {outline:0; border:1px solid # 000; margin: 0 auto}
  </ style>
  </ head>
  <body>
    <canvas id='c'></ canvas>
    <script src="game.js"></ script>
  </ body>
</ html>
Это и есть весь HTML, который нам понадобится в течение этого урока. Хорошо, теперь давайте создавать Javascript. Прежде всего мы должны создать немного глобальных (хотя, я знаю что это Глобальное зло =) переменных и изменить атрибуты (свойства) <canvas>. Этого будет достаточно:
var width = 320, //ширина canvas
    height = 500, //высота canvas

  c = document.getElementById('c'), //сам canvas

  ctx = c.getContext('2d');
//двумерный контектс canvas (сейчас только он поддерживается всеми
//браузерами)

c.width = width;
c.height = height;
//устанавливаем размеры canvas

Первое, что важно понять про <canvas>, это то, что невозможно просто перемещать объекты на его поверхности. Нужно, обязательно, на каждом кадре его очищать, целиком или частично. Поэтому давайте создадим функцию очистки – clear():
var clear = function(){
  ctx.fillStyle = '#d0e7f9';
//выбираем цвет заливки (чудесный голубой)
//Использование clearRect() вызывало ошибку, в 2-х строках ниже показан
//старый вариант
//ctx.clearRect(0, 0, width, height);
//очистка экрана
  ctx.beginPath();
//запускаем рисование
  ctx.rect(0, 0, width, height);
//рисуем прямоугольник из точки (0, 0) до (widthheight) заполняя весь наш
//холст <canvas>
  ctx.closePath();
//заканчиваем рисование
  ctx.fill();
//заполняем прямоугольник цветом выбранным ранее
}

Одноцветный фон скучен как ад, так что давайте нарисуем несколько облаков на нем. Скорее не правильные облака, а простые, полупрозрачные круги, подражающие облакам. Круги мы будем рисовать в случайных местах холста, каждый с различным размером и прозрачностью. Мы будем держать всю информацию о кругах в 2-ом массиве (в JS их нет, но лучший путь к решению этой проблемы помещение одного массива в другой).

var howManyCircles = 10, circles = [];

for (var i = 0; i < howManyCircles; i++)
  circles.push([Math.random() * width, Math.random() * height, Math.random() * 100, Math.random() / 2]);
//добавляем информацию о кругах
//в массив 'circles'. Это x и y позиция,
//радиус в диапазоне 0-100 и прозрачность
//в диапазоне 0-0.5 (0 это абсолютно прозрачно, 1 непрозрачно)

var DrawCircles = function(){
  for (var i = 0; i < howManyCircles; i++) {
    ctx.fillStyle = 'rgba(255, 255, 255, ' + circles[i][3] + ')';
//белый цвет с прозрачностью в rgba
    ctx.beginPath();
    ctx.arc(circles[i][0], circles[i][1], circles[i][2], 0, Math.PI * 2, true);
//arc(x, y, radius, startAngle, endAngle, anticlockwise)
    ctx.closePath();
    ctx.fill();
  }
};

Хорошо, но скучновато. Почему облака стоят на месте? Давайте сделаем маленькую функцию с одним аргументом, которая сдвигает облака вниз на заданное число пикселей, и, когда определенный круг скрывается за пределами холста, он перемещается  наверх с изменением координаты X, радиуса и прозрачности:
var MoveCircles = function(deltaY){
  for (var i = 0; i < howManyCircles; i++) {
    if (circles[i][1] - circles[i][2] > height) {
//круг достигший нижнего края
//меняет параметры
      circles[i][0] = Math.random() * width;
      circles[i][2] = Math.random() * 100;
      circles[i][1] = 0 - circles[i][2];
      circles[i][3] = Math.random() / 2;
    } else {
//сдвигаем круг на deltaY пикселей вниз
      circles[i][1] += deltaY;
    }
  }
};

Теперь, не в последнюю очередь, давайте создадим основной цикл игры и подключим в нем все, что уже создали. Каждый кадр будет очищать экран, перемещать круги на 5px вниз, рисовать их и после 1/50 секунды вызывать следующий кадр. Я использую два setTimeouts вместо одного setInterval, хотя незнаю почему:). Вроде бы раньше были некоторые проблемы с производительностью в IE, что ли. И не забудьте добавить gLoopв глобальные переменныеобъявленные вначале.
var width = 320, 
//ширина холста <canvas>
  height = 500, 
//высота холста <canvas>
  gLoop,
(...) //остальная часть кода находится здесь

var GameLoop = function(){
  clear();
  MoveCircles(5);
  DrawCircles();
  gLoop = setTimeout(GameLoop, 1000 / 50);
}
GameLoop();

Благодарю Луиса Гирибоуна за комментарии к уроку. Он помог разобраться в чем отличие setTimeOut от setInterval. SetInterval запускает функцию переданную ему в аргументе не дожидаясь окончания предыдущей итерации, а setTimeOut ждет ее завершения даже если вышло время переданное в качестве второго аргумента.
Так же хочу поблагодарить пользователя под ником Ped7g за выявленные ошибки.
Результат этой части урока можно посмотреть по адресу: http://jsbin.com/odoho3, а скачать исходники тут: http://github.com/michalbe/Simple-game-with-HTML5-Canvas

Копирайты.

Автор: Михал Будзинский https://twitter.com/#!/@michalbe
Автор перевода: Андрей Семенов https://twitter.com/#!/a_semenov79

Tuesday, October 25, 2011

SVG. Масштабируемая векторная графика. Часть 4


Градиенты и текстуры.
А как же градиенты, спросите вы, но я отвечу - это тоже просто. В спецификации определено два типа градиента: линейный и радиальный.
Градиенты положено помещать внутрь тэга контейнера <defs> (от англ. definitions - определения), это говорит о том, что содержимое предназначено для многократного применения. Линейный градиент определен парным тэгом <linearGradient>, (а что же должно быть внутри?) который включает в себя тэги <stop />. В спецификации об этом сказано: "Буйство красок, чтобы использовать в градиенте определено элементами 'stop', которые являются дочерними элементами ‘linearGradient’ элемента или ‘radialGradient’ элемента." Разберем вышесказаное на примере градиента для нашего первого элемента (прямоугольника с фоном):
<defs>
        <linearGradient id="sky_n_water">
                <stop offset="0%" stop-color="#09f" />
                <stop offset="35%" stop-color="#9ff" />
                <stop offset="36%" stop-color="#fff" />
                <stop offset="100%" stop-color="#09f" />
        </linearGradient>
</defs>
Вот они наши запланированые переходы в верхней части от темно-синего к светло-синему, а ниже от белого к темно-синему изображающие небо и воду.
Рекомендую, для проектирования градиентов использовать Фотошоп и для этого несколько причин. Во-первых в "Редакторе градиентов" мы видим удобную шкалу на которой размещаем наши контрольные точки (тэги <stop />) и offset атрибут мы видим в поле "Позиция", что можно считать вторым плюсом, а в третьих на "Палитре цветов" поставив галочку в "Только Web-цвета" мы в поле "#" видим код RGB 0099ff, который можно смело сокращать до трех цифр 09f и писать их в stop-color атрибут.

Вот так легко и просто, но на практике мы получим прямоугольник с градиентом идущим слева-направо, вместо желанного сверху-вниз. Градиент надо повернуть и нам поможет атрибут gradientTransform = "rotate(90)", который необходимо указать в тэге <linearGradient>.
Применим теперь наш первый градиент на практике:
<defs>
        <linearGradient id="sky_n_water"  gradientTransform="rotate(90)">
          <stop offset="0%" stop-color="#09f" />
          <stop offset="35%" stop-color="#9ff" />
          <stop offset="36%" stop-color="#fff" />
          <stop offset="100%" stop-color="#09f" />  
        </linearGradient>
        </defs>
        <style type="text/css"><![CDATA[
          #bg {fill:url(#sky_n_water) #09f; stroke="none"}
        ]]></style>
<rect id="bg" width="100%" height="100%"/>
А вот и новые свойства для нашей «кочки»:
<linearGradient id="grass">
    <stop offset="0" stop-color="#6c0" />
    <stop offset="1" stop-color="#9f3" />
</linearGradient>

.grass {fill:url(#grass) #6c0; stroke:#090; stroke-width:1}
Обратите внимание, на этот раз для параметра offset я использовал не проценты (0 и 1 соответствуют 0% и 100%, а если бы нам понадобилась середина мы написали бы 0.5 вместо 50%).
Радиальный градиент для цветка сделать тоже не сложно. Отличается только тэг обрамляющий наши точки остановки цвета, вместо linearGradient используется radialGradient
<radialGradient id="flower">
   <stop stop-color="#f00" offset="0"/>
   <stop stop-color="#f00" offset="0.3"/>
   <stop stop-color="#fff" offset="0.3"/>
   <stop stop-color="#fff" offset="1"/>
</radialGradient>
.flower {stroke:#ccc; stroke-width:0.01; fill:url(#flower) #f00}
Текстуры в нашем проекте не используются, хотя их успешно можно было бы применить для раскрашивания наших «кочек». Дело в том, что в SVG для текстурирования используются не только растровые текстуры, но и векторные (что важнее). И наши цветочки можно было бы вынести из основного рисунка и поместить в определения <defs> заключив их в тэге <pattern>. Таким образом присвоив текстуре (паттерну) идентификатор мы сможем использовать ее для заливки. Обязательно попробуйте эту методику в ваших проектах — не пожалеете.

Трансформации

Спецификацией предусмотренно несколько видов трансформации - это перемещение (translate), масштабирование (scale), поворот (rotate), наклон по оси Х (skewX), наклон по оси Y (skewY) и matrix, которая в своих шести параметрах может включать сумму нескольких трансформаций. Но поскольку тема матричного представления двумерного пространства выходит за рамки данной статьи примем как факт, что те из вас кто изучал "Высшую математику" разберутся с матричными трансформациями сами, а те кто не изучал либо будут использовать последовательность простых трансформаций, либо найдут учебник по вышке и изучат матрицы самостоятельно. (заранее огорчу - к Морфиусу, Тринити и Нэо наши матрицы отношения не имеют, а их изучение скучнее просмотра одноименного фильма).
Вот матричные трансформации для цветков. Каждая, из них, увеличивает цветок в десять раз и переносит на новое место.
transform="matrix(10,0,0,10,45,75)"
 transform="matrix(10,0,0,10,125,60)"
 transform="matrix(10,0,0,10,160,85)"
Заменить ее обычными можно так:
transform = “scale(10) translate(45,75)”
Важно помнить, что последовательность трансформаций имеет важное значение. Поясню почему... Ось вращения для объекта всегда совпадает с 0,0 и если сначала его переместить, а затем повернуть, то результат будет сильно отличаться от того, который получиться при повороте с последующим перемещением. Тоже касается и масштабирования, так как при увеличении уже перемещенного объекта увеличится и расстояние его перемещения.

В проекте мы не применяем трансформацию «skewX» или «skewY» предназначенные для наклона объекта по горизонтали или вертикали, но они возможно вам пригодятся для проектов реализующих псевдо3D.Например, из трех прямоугольников можно создать куб, как показано на рисунке.
Для «кочек» я выбрал следующие значения [(3,0,0,3,1000,500), (4,0,0,4,480,410), (6,0,0,6,-150,260)] и получил очередной файл SVG. Да, эта картина грубовата и не сглажена фильтрами и спецэффектами Фотошоп, но я нахожу другой факт более важным - наш SVG весит 7,5 кБ, а этот JPEG с размером 400*250 и качеством 60% уже 18 кБ (а если пытаться сохранить в полном кач-ве и с полным разрешением, то вышло бы - 300 кБ). Еще один плюс - SVG сам масштабируется под разрешение экрана и размер рабочей области браузера, а под растр нам пришлось бы придумывать ухищрения.
И у нас в SVG есть простор для совершенствования, постаравшись мы можем еще уменьшить размер файла, добавив несколько атрибутов изменить стиль линий. Да и визуальные фильтры разработаны W3C, но пока, правда, реализованы не во всех браузерах. Мой любимый Google Chrome в числе передовиков и полностью поддерживает все спецификации консорциума W3C.

Визуальные фильтры.

Не углубляясь в подробности сразу показываю код визуального фильтра:
<filter id="BlurFar"><feGaussianBlur stdDeviation="1.5" /></filter>
<filter id="BlurNear"><feGaussianBlur stdDeviation="0.5" /></filter>
<filter id="BlurSmall"><feGaussianBlur stdDeviation="0.1" /></filter>

  <g transform="matrix(3,0,0,3,1000,500)" filter="url(#BlurFar)">
  <g transform="matrix(4,0,0,4,480,410)" filter="url(#BlurNear)">
   <path d="m0,100 q100,-100 200,0z" class="grass" filter="url(#BlurSmall)" />  
Вот такие, незначительные, изменения привносят в наш пейзаж немного реалистичности, за счет DOF (от англ. depth of field - на русский переведу, как глубина резкости). Парный тэг <filter> как и градиенты располагают внутрь контейнера <defs>, и он (в свою очередь) содержит один и более тэгов описывающих фильтры. Я применил тэг <feGaussianBlur> для размытия по Гауссу, а другие фильтры вы без труда отыщите в спецификации. Применять определенный в "defs" фильтр можно и к группам и к отдельным объектам указав им атрибут "filter".
Быстро и просто мы разобрали на практике большую часть современного SVG. Пока что за бортом остались связанные и интегрированные растры, текстовые объекты, шрифты, анимация и использование сценариев. Все это будет рассмотрено в следующих частях статьи.