Canvas шаг за шагом: основы

Если верить англо-русскому словарю, то можно узнать что canvas переводится как холст, а если верить википедии, то можно узнать что тег canvas, это элемент HTML 5, который предназначен для создания растрового изображения при помощи JavaScript. Тому как создать это растровое изображение и будет посвящен мой небольшой текст. Прежде чем начинать пробовать свои силы в этом не легком деле рекомендуется уже иметь базовые знания о том что такое HTML и с чем едят JavaScript.

Предварительная «настройка» нашего холста

У нашего подопытного тега есть всего два атрибута — height и width, высота и ширина соответственно, по умолчанию размер холста 150х300 пикселей.
Стоит отметить что canvas создает область фиксированного размера содержимым которого управляют контексты.
Элементарный пример:
<html>
<head>
  <title>canvasExample</title>
</head>
<body>
  <canvas height='320' width='480' id='example'>Обновите браузер</canvas>
  <script>
    var example = document.getElementById("example");
    var ctx = example.getContext('2d');
    ctx.fillRect(0, 0, example.width, example.height);
  </script>
</body>
</html>

Если сохранить эти несчастные 13 строк в файл и открыть его браузером, то можно будет увидеть область с чёрным прямоугольником, так вот это и есть тот самый холст, на котором нарисован прямоугольник размеры которого равны размерам canvas'а.

Прямоугольники

Самой элементарной фигурой которую можно рисовать является прямоугольник. Предусмотрено три функции для отрисовки прямоугольников.
strokeRect(x, y, ширина, высота) : Рисует прямоугольник
fillRect(x, y, ширина, высота) : Рисует закрашенный прямоугольник
clearRect(x, y, ширина, высота) : Очищает область на холсте размер с прямоугольник заданного размера
Пример иллюстрирующий работу этих функций:
<html>
<head>
  <title>rectExample</title>
</head>
<body>
  <canvas id='example'>Обновите браузер</canvas>
  <script>
  var example = document.getElementById("example");
  var ctx = example.getContext('2d');
  example.width  = 640;
  example.height = 480;
  ctx.strokeRect(15, 15, 266, 266);
  ctx.strokeRect(18, 18, 260, 260);
  ctx.fillRect(20, 20, 256, 256);
  for (i=0; i<8; i+=2)
    for (j=0; j<8; j+=2) {
      ctx.clearRect(20+i*32, 20+j*32, 32, 32);
      ctx.clearRect(20+(i+1)*32, 20+(j+1)*32, 32, 32);
    }
  </script>
</body>
</html>

А теперь краткий построчный разбор:
  • в строках 10 и 11 мы изменили размер холста — чтоб бы задуманное нами изображение полностью отобразилось,
  • в строках 12 и 13 мы нарисовали два не закрашенных прямоугольника которые будут символизировать своеобразную рамку нашей «шахматной доски»,
  • в строке 14 отрисовываем закрашенный прямоугольник размеры которого бы позволил вместить в себе 64 квадрата с шириной стороны 32 пикселя,
  • в строках с 15 по 19 у нас работает два цикла которые очищают на чёрном прямоугольнике квадратные области в таком порядке что бы в итоге полученное изображение было похоже на шахматную доску,

Линии и дуги

Рисование фигур составленных из линий выполняется последовательно в несколько шагов:
beginPath()
closePath()
stroke()
fill()
beginPath используется что бы «начать» серию действий описывающих отрисовку фигуры. Каждый новый вызов этого метода сбрасывает все действия предыдущего и начинает «рисовать» занова.
closePath является не обязательным действием и по сути оно пытается завершить рисование проведя линию от текущей позиции к позиции с которой начали рисовать.
Завершающий шаг это вызовом метода stroke или fill. Собственно первый обводит фигуру линиями, а второй заливает фигуру сплошным цветом.
Те кто когда-то на школьных 486х в былые годы рисовал в бейсике домик, забор и деревце по задумке учителя тот сразу поймёт часть ниже. Итак, существуют такие методы как,
moveTo(x, y) : перемещает "курсор" в позицию x, y и делает её текущей
lineTo(x, y) : ведёт линию из текущей позиции в указанную, и делает в последствии указанную текущей
arc(x, y, radius, startAngle, endAngle, anticlockwise) рисование дуги, где x и y центр окружности, далее начальный и конечный угол, последний параметр указывает направление
Пример ниже показывает действие всего описанного выше:
<html>
<head>
  <title>pathExample</title>
</head>
<body>
  <canvas id='example'>Обновите браузер</canvas>
  <script>
    var example = document.getElementById("example");
    var ctx = example.getContext('2d');
    example.height = 480;
    example.width = 640;
    ctx.beginPath();
    ctx.arc(80, 100, 56, 3/4*Math.PI, 1/4*Math.PI, true);
    ctx.fill();
    ctx.moveTo(40, 140);
    ctx.lineTo(20, 40);
    ctx.lineTo(60, 100);
    ctx.lineTo(80, 20);
    ctx.lineTo(100, 100);
    ctx.lineTo(140, 40);
    ctx.lineTo(120, 140);
    ctx.stroke();
  </script>
</body>
</html>

В строке 14 заливается цветом дуга, в строке 22 обводится контур нашей короны.

Кривые Бернштейна-Безье

Что такое кривые Безье я думаю лучше объяснит Википедия.
Нам доступно две функции, для построения кубической кривой Бизье и квадратичной, соотвестствено:
quadraticCurveTo(Px, Py, x, y) 
bezierCurveTo(P1x, P1y, P2x, P2y, x, y)
x и y это точки в которые необходимо перейти, а координаты P(Px, Py) в квадратичной кривой это дополнительные точки которые используются для построения кривой. В кубическо кривой соответственно две дополнительные точки.
Пример двух кривых:
<html>
<head>
  <title>curveExample</title>
</head>
<body>
  <canvas id='example'>Обновите браузер</canvas>
  <script>
    var example = document.getElementById("example");
    var ctx = example.getContext('2d');
    example.height = 480;
    example.width = 640;
    ctx.beginPath();
    ctx.moveTo(10, 15);
    ctx.bezierCurveTo(75, 55, 175, 20, 250, 15);
    ctx.moveTo(10, 15);
    ctx.quadraticCurveTo(100, 100, 250, 15);
    ctx.stroke();
  </script>
</body>
</html>


Добавим цвета

Что бы наше изображение было не только двух цветов, а любого цвета предусмотрено, два свойства
fillStyle = color : определяет цвет заливки 
strokeStyle = color : цвет линий

цвет задается точно так же как и css, на примере все четыре способа задания цвета
// все четыре строки задают оранжевый цвет заливки
ctx.fillStyle = "orange";
ctx.fillStyle = "#FFA500";
ctx.fillStyle = "rgb(255,165,0)";
ctx.fillStyle = "rgba(255,165,0,1)"
аналогично задаётся и цвет для линий.
Возьмём пример с шахматной доской и добавим в него немного цвета:
<html>
<head>
  <title>rectExample</title>
</head>
<body>
  <canvas id='example'>Обновите браузер</canvas>
  <script>
  var example = document.getElementById("example");
  var ctx = example.getContext('2d');
  example.width  = 640;
  example.height = 480;
  ctx.strokeStyle = '#B70A02'; // меняем цвет рамки
  ctx.strokeRect(15, 15, 266, 266);
  ctx.strokeRect(18, 18, 260, 260);
  ctx.fillStyle = '#AF5200'; // меняем цвет клеток
  ctx.fillRect(20, 20, 256, 256);
  for (i=0; i<8; i+=2)
    for (j=0; j<8; j+=2) {
      ctx.clearRect(20+i*32, 20+j*32, 32, 32);
      ctx.clearRect(20+(i+1)*32, 20+(j+1)*32, 32, 32);
    }
  </script>
</body>
</html>


Задача

Что бы усвоить информацию и закрепить прочитанное на практике я всегда ставлю перед собой не большую цель которая бы одновременно охватывала всё прочитанное и одновременно процесс достижения которой было бы интересен мне самому. В данном случае я попытаюсь отрисовать уровень одной из моих самых любимых в детстве игр. Собственно за не имением времени добавлять жизни на него я не беде, а сделаю максимально понятный код охватывающий практически всё то что сегодня здесь описал.
<html>
<head>
  <title>colorExample</title>
</head>
<body>
  <canvas id='example'>Обновите браузер</canvas>
  <script>
    example = document.getElementById("example");
    ctx = example.getContext('2d');
    // Размер одной ячейки на карте
    cellSize = 32;
    // Массив карты поля боя
    var map = [
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0],
      [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0],
      [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0],
      [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0],
      [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 2, 2, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0],
      [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 2, 2, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0],
      [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0],
      [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0],
      [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1],
      [2, 2, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 2, 2],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0],
      [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0],
      [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0],
      [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0],
      [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0],
      [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0],
      [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
      [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    ];
    example.width = 16*cellSize;
    example.height = 15*cellSize;
    ctx.fillStyle = '#ccc';
	ctx.fillRect(0, 0, example.width, example.height);
    ctx.fillStyle = '#000';
	ctx.fillRect(cellSize, cellSize, 13*cellSize, 13*cellSize)
    // Цикл обрабатывающий массив в котором содержатся значения элементов карты
    // если попадается 1 то рисуется кирпичный блок
    // если 2, то бетонная стена
    for (var j=0; j<26; j++) 
      for (var i=0; i<26; i++) {
        switch (map[j][i]) {
          case 1:
            DrawBrick(i*cellSize/2 + cellSize, j*cellSize/2 + cellSize);
            break;
          case 2:
            DrawHardBrick(i*cellSize/2 + cellSize, j*cellSize/2 + cellSize);
            break;
		}
	  }
  // Рисуем часть кирпичной стены
  function DrawBrick(x, y) {
    // Отрисовка основного цвета кирпича
    ctx.fillStyle = '#FFA500';
    ctx.fillRect(x, y, cellSize/2, cellSize/2); 
    // Отрисовка теней
    ctx.fillStyle = '#CD8500';
    ctx.fillRect(x, y, cellSize/2, cellSize/16);
    ctx.fillRect(x, y+cellSize/4, cellSize/2, cellSize/16);
    ctx.fillRect(x+cellSize/4, y, cellSize/16, cellSize/4); 
    ctx.fillRect(x+cellSize/16, y+cellSize/4, cellSize/16, cellSize/4); 
    // Отрисовка раствора между кирпичами
    ctx.fillStyle = '#D3D3D3';
    ctx.fillRect(x, y+cellSize/4-cellSize/16, cellSize/2, cellSize/16);
    ctx.fillRect(x, y+cellSize/2-cellSize/16, cellSize/2, cellSize/16);
    ctx.fillRect(x+cellSize/4-cellSize/16, y, cellSize/16, cellSize/4); 
    ctx.fillRect(x, y+cellSize/4-cellSize/16, cellSize/16, cellSize/4); 
  }
  // Рисуем часть бутонного блока
  function DrawHardBrick(x, y) {
    // Отрисовка основного фона
    ctx.fillStyle = '#cccccc';
    ctx.fillRect(x, y, cellSize/2, cellSize/2); 
    // Отрисовка Тени
    ctx.fillStyle = '#909090';
    ctx.beginPath();
    ctx.moveTo(x,y+cellSize/2);  
    ctx.lineTo(x+cellSize/2,y+cellSize/2);  
    ctx.lineTo(x+cellSize/2,y);  
    ctx.fill();  
    // Отрисовка белого прямоугольника сверху
    ctx.fillStyle = '#eeeeee';
    ctx.fillRect(x+cellSize/8, y+cellSize/8, cellSize/4, cellSize/4);
  }
  </script>
</body>
</html>

На последок комментарий по последнему примеру. В спецификациях картинки которую может выдавать Денди разрешение экрана должно быть 256?240 пикселей.
Поле боя в общеизвестнных Танчиках размером 13х13 больших блоков. Каждый из которых нарисован 4мя повторяющимися спрайтами (коих по общему подсчёту выходит на карте 26х26=676). Итак прикинем как было в оригинале по пикселам и как это правильно масштабировать. Если поделить 240 на 26 то выйдет что целая часть от деления будет 8. Получается что размерность текстуры была 8х8 пиксела т.е. размер поля боя 208х208, а большого блока 16х16. Ширина должна быть 256 пикселов. Сейчас вычислим размер правого столбца с дополнительной информацией и размер полей сверху/снизу. Справа если присмотреться ширина составляет размерность в два блока, итого 2*16=32. У нас уже 32+208=240 слева поле 16, а снизу и сверху соответственно так же по 16 пикселов. Собственно в моём примере размерность большого блока заключена в переменной cellSize, собственно все вычисления делаются иходя из её размеров. Можете по экспериментировать и поменять её значение, настоятельно рекомендую делать его кратным степеням двойки (16, 32, 64, 128...), если хотите чтоб всё выглядело так как на старом добром денди то устанавливайте её значение равным 16. Хотя и при любых других значениях всё выглядит нормально. Если то как я пишу понравится кому-то кроме меня, то напишу продолжение, а то что в нём будет пока утаю )

6 комментариев

avatar
Отлично пишешь, отрисовка кирпичей и раствора между ними порадовала :)
  • kog
  • 0
avatar
ну а кстать у меня на винте ещё лежит версия с патроном разрушающим стены и катающимся танком. Но код слишком кривой, да и в пределах тех границ статьи что я тут задал это было бы излишним. Потом перепишу код и выложу как время будет.
avatar
интересно, жду с нетерпением
avatar
Я б ещё кинулся б писать, но институт, экзамены, сессия©. Это-то писал с угрызениями совести
avatar
ух, не думал что так просто, спасибо за статью, мотивирует!
avatar
Ох, не успел на праздниках отдохнуть как тут уже мою инициативу перехватили. Статья хороша, подметил для себя штучки
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.