Данная статья планируется как пошаговый обзор создания простой JavaScript-игры класса “Ball and Paddle” на Canvas. Примерами такой игры могут послужить старые DOS-е игры наподобие таких - Ball and Paddle.

Пример кода из этой статьи взят из видео-курса достаточно известного Интернет-ресурса, посвященного фронтенд-разработке - Udemy.

Почему Canvas и почему игра? Лично для меня процесс познания JavaScript сильно облегчается благодаря Canvas - так интереснее. А создание игры на Canvas - это еще интереснее!

Итак, с чего начнем? Дальше в меру своих сил буду стараться детально пошагово рассказывать, что делает тот или иной кусок кода. И начнем с базового набора - создания Canvas.

Базовый Canvas

HTML-разметка страницы будет предельно простой:

<body>
  <canvas id="canvas"></canvas>
  <script src="script.js"></script>
</body>

В JavaScript’е создадим две глобальные переменные - одну для элемента Canvas, вторую - для 2d-контекста Canvas. Когда parser браузера построит DOM-дерево документа (событие

1
DOMContentLoaded
), инициализируем обе переменные, выполним проверку удачного получения 2d-контекста Canvas и если проверка будет пройдена успешно, то динамически зададим размеры Canvas:

var canvas = null;
var ctx = null;

window.addEventListener('DOMContentLoaded', function () {
  canvas = document.querySelector('#canvas');
  ctx = canvas.getContext('2d');
  if ( ctx ) {
    canvas.width = 800;
    canvas.height = 500;
  }
}, false);

Базовые элементы игры

Основа Canvas была создана в предыдущем шаге. В этом шаге создадим три фигуры, которые будут учавствовать в игре. Таковыми фигурами будут:

  • фон игры
  • мячик (ball)
  • площадка (paddle)

Ниже я приведу JavaScript-код создания всех трех элементов, но сам код комментировать не буду, так как он очень простой и относится к основам Canvas:

var canvas = null;
var ctx = null;

window.addEventListener('DOMContentLoaded', function () {

  canvas = document.querySelector('#canvas');
  ctx = canvas.getContext('2d');

  if ( ctx ) {

    canvas.width = 800;
    canvas.height = 500;

    ctx.fillStyle = '#000';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    ctx.fillStyle = 'firebrick';
    ctx.beginPath();
    ctx.arc(50, 50, 10, 0, 360*Math.PI/180, true);
    ctx.fill();
    ctx.closePath();

    ctx.fillStyle = '#fff';
    ctx.fillRect(100, canvas.height-40, 100, 10);

  }
}, false);

Живой результат вышеприведенного кода можно посмотреть на этой странице - Lesson1-1. Это то, что должно получиться и что послужит заготовкой для игры.

Анимация мячика

В этом шаге предстоит сделать более интересные вещи. Во-первых, мы сделаем так, чтобы мячик начал двигаться как по-горизонтали, так и по-вертикали. А во-вторых, сделаем так, чтобы он вел себя как настоящий резиновый мячик - при ударе о стену отскакивал от нее и мчался в противоположном направлении.

Сделать это достаточно просто. Для этого нам понадобится одна из так называемых тайминговых функций JavaScript -

1
setInterval()
. А также немного воображения.

Анимация мячика будем делать по-простому принципу, по которому делается любой мультфильм или кино - мячик будет отрисовываться с заданной частотой (

1
1000/frames
), но каждый раз в новой позиции. В результате будет создаваться иллюзия его движения. Каждая новая позиция мячика - это его координата по оси X или Y с новым значением соответственно.

Чтобы мячик двигался достаточно быстро, изменять значения координат (

1
ballX += ballStepX
и
1
ballY += ballStepY
) мячика по оси X и Y будем с определенным шагом (
1
ballStepX
и
1
ballStepY
) - допустим, со значениями 5 или 6:

var canvas = null;
var ctx = null;

var frames = 24;

var ballX = 50;
var ballY = 50;
var ballStepX = 5;
var ballStepY = 6;
var ballRadius = 10;

window.addEventListener('DOMContentLoaded', function () {

  canvas = document.querySelector('#canvas');
  ctx = canvas.getContext('2d');

  if ( ctx ) {

    canvas.width = 800;
    canvas.height = 400;

    setInterval( function () {

      ballX += ballStepX;
      ballY += ballStepY;

      if ( ballX < 0 || ballX > canvas.width) {
        ballStepX *= -1;
      }
      if ( ballY < 0 || ballY > canvas.height ) {
        ballStepY *= -1;
      }

      ctx.fillStyle = '#000';
      ctx.fillRect(0, 0, canvas.width, canvas.height);

      ctx.fillStyle = 'firebrick';
      ctx.beginPath();
      ctx.arc(ballX, ballY, ballRadius, 0, 360*Math.PI/180,true);
      ctx.fill();
      ctx.closePath();

      ctx.fillStyle = '#fff';
      ctx.fillRect(100, canvas.height-40, 100, 10);

    }, 1000/frames);

  }
}, false);

Эффект отскакивания от стенок (как резиновый мячик) обеспечивает проверка условий в участке кода:

...
if ( ballX < 0 || ballX > canvas.width) {
  ballStepX *= -1;
}
if ( ballY < 0 || ballY > canvas.height ) {
  ballStepY *= -1;
}
...

Здесь все просто - при выполнении условия знак переменной

1
ballStepX
или
1
ballStepY
будет меняться на противоположный. В результате значение переменной
1
ballX
или
1
ballY
будет возрастать или уменьшаться. Как следствие, мячик будет двигаться в одну или в другую сторону.

Живой пример приведенного выше кода можно посмотреть и изучить на этой странице - Lesson1-2.

Двигаем paddle

В этом шаге нужно заставить двигаться paddle при помощи мыши. Для этого по событию

1
mousemove
внутри элемента Canvas будем получать значение X-координаты курсора мыши. И передавать это значение элементу paddle, его X-координате левого верхнего угла. Тем самым мы заставим paddle двигаться. За все эти действия будет отвечать функция
1
mouseCoords()
:

...
function mouseCoords (event) {
  var canvasOffset = canvas.getBoundingClientRect();
  var htmlElement = document.documentElement;
  mouseX = event.clientX - canvasOffset.left - htmlElement.scrollLeft;
  paddleX = mouseX - paddleWidth/2;
}
...

Обратите внимание на последнюю строку функции -

1
paddleX = mouseX - paddleWidth/2;
. Переменная
1
paddleX
необходима для того, чтобы при выходе за границы Canvas элемент paddle скрывался ровно на половину своей ширины.

Также не забудем создать переменные для paddle и передать их в код для отрисовки фигуры:

...
var paddleX = null;
var paddleWidth = 100;
var paddleHeight = 10;
var paddleOffset = 40;
...
ctx.fillStyle = '#fff';
ctx.fillRect(paddleX, canvas.height - paddleOffset, paddleWidth, paddleHeight);
...

Живой пример приведенного выше кода можно посмотреть и изучить на этой странице - Lesson1-3. Подвигайте курсором мыши право-влево, чтобы увидеть эффект.

Мячик отскакивает от paddle

На этом этапе нужно сделать так, чтобы мячик отскакивал от paddle, когда последний оказывается на его пути. Выполнить эту задачу просто - ведь мячик уже отскакивает от “стен” Canvas. Следовательно, нужно научить мячик “видеть” еще и paddle.

Для этого сначала нужно опеределить внешние границы paddle - все его четыре стороны:

...
var paddleLeftEdge = paddleX;
var paddleRightEdge = paddleLeftEdge + paddleWidth;
var paddleTopEdge = canvas.height - paddleOffset;
var paddleBottomEdge = paddleTopEdge + paddleHeight;
...

Когда значения всех сторон будут определены, то можно будет подставить эти значения в условие - и дело сделано:

...
if ( ballX > paddleLeftEdge && ballX < paddleRightEdge && ballY > paddleTopEdge && ballY < paddleBottomEdge ) {
  ballStepY *= -1;
}
...

Живой пример приведенного выше кода можно посмотреть и изучить на этой странице - Lesson1-4. Подвигайте курсором мыши право-влево и постарайтесь поймать мячик с помощью paddle, чтобы увидеть эффект.

Угол отскока мячика

В этом шаге сделаем так, чтобы наша игра смотрелась более правильной с точки зрения физики и обычной природы. То есть, при разном угле попадания на paddle мячик должен отскакивать от него с разной скоростью. Чем острее угол падения, тем с большей скоростью отскакивает от paddle мячик.

Решается эта задача несколькими строками кода:

...
if ( ballX > paddleLeftEdge && ballX < paddleRightEdge && ballY > paddleTopEdge && ballY < paddleBottomEdge ) {
  ballStepY *= -1;
  var paddleCenter = paddleLeftEdge + paddleWidth/2;
  var ballDistance = ballX - paddleCenter;
  ballStepX = ballDistance * 0.35;
}
...

В первой строке

1
var paddleCenter = paddleLeftEdge + paddleWidth/2;
находится X-координата середины paddle. В строке
1
var ballDistance = ballX - paddleCenter;
определяется расстояние, на котором мячик соприкоснулся с paddle относительно его середины. В строке
1
ballStepX = ballDistance * 0.35;
полученная дистанция присваивается шагу приращения по оси Х мячика -
1
ballStepX
.

Логично предположить, что чем больше величина дистанции точки соприкосновения мячика относительно середины paddle, тем выше новая скорость движения мячика по-горизонтали. Чтобы эта скорость не была слишком высокой, ее необходимо уменьшить, умножив на 0.35, к примеру.

Живой пример приведенного выше кода можно посмотреть и изучить на этой странице - Lesson1-5.

Оптимизация кода

На данный момент наша задача по построению игры практически решена. Но остался один организационный момент.

Заключается он в том, что код необходимо реорганизовать в отдельные функции. Такой код будет читаться и поддерживаться значительно лучше.

Одна из таких функций уже была создана ранее - это функция

1
mouseCoords()
. Давайте преобразуемся и весь оставшийся код подобным образом:

...
function drawRect (leftX, leftY, boxWidth, boxHeight, boxFillColor) {
  ctx.fillStyle = boxFillColor;
  ctx.fillRect(leftX, leftY, boxWidth, boxHeight);
}
...
function drawBall(centerX, centerY, radius, fillColor) {
  ctx.fillStyle = fillColor;
  ctx.beginPath();
  ctx.arc(centerX, centerY, radius, 0, 360*Math.PI/180, true);
  ctx.fill();
  ctx.closePath();
}
...
function drawAll() {
  drawRect(0, 0, canvas.width, canvas.height, '#000');
  drawBall(ballX, ballY, ballRadius, 'firebrick');
  drawRect(paddleX, canvas.height - paddleOffset, paddleWidth, paddleHeight, '#fff');
}
...

Готовый пример преобразованного в функции кода можно посмотреть на этой странице - Lesson1-6.


На этом все.

Попытка разобраться с интересной возможностью canvas, которая называется “манипуляция с пикселями” (raw pixel). Основная суть этой возможности заключается в том, что можно получить информацию о цвете и альфа-канале любого пикселя, расположенного в произвольном месте canvas.

Образно выражаясь, можно сделать снимок (снять цветовой отпечаток) с любого участка canvas. Причем, этот отпечаток может быть любого размера (20х20 пикселей, 100х100 пикселей, 1х1 пиксель) - какой потребуется.

Техника Raw Pixel возможна благодаря объекту ImageData, у которого есть три свойства:

  • ImageData.width - ширина объекта в пикселях
  • ImageData.height - высота объекта в пикселях
  • ImageData.data - массив данных

Первые два свойства примитивно просты - это геометрические размеры объекта ImageData.

Самым интересным свойством объекта ImageData является последнее -

1
ImageData.data
.

Данное свойство в свою очередь является объектом, а если быть точнее - одномерным массивом. В этом массиве на каждый пиксель из “отпечатка” отводится 4 байта:

  • imageData.data[0] — значение красного цвета (число от 0 до 255);
  • imageData.data[1] — значение зеленого цвета (число от 0 до 255);
  • imageData.data[2] — значение синего цвета (число от 0 до 255);
  • imageData.data[3] — значение прозрачности (число от 0 до 255);

В результате получается значение цвета в формате RGBA.

У Canvas есть несколько методов для работы с объектом ImageData:

  • getImageData()
  • putImageData()
  • toDataURL()
  • createIamgeData()

Наиболее интересные и полезные два первых метода -

1
getImageData
и
1
putImageData
.

Метод getImageData

Метод

1
getImageData
позволяет создать экземпляр объекта ImageData на основе существующего canvas. Другими словами, этот метод “делает снимок” существующего canvas и преобразует этот “снимок” в объект ImageData.

Создадим простой пример для наглядного отображения работы метода

1
getImageData
:

window.addEventListener('DOMContentLoaded', function () {

  var ctx = document.querySelector('#canvas').getContext('2d');

  if ( ctx ) {

    var rawPixel;

    ctx.canvas.width = 400;
    ctx.canvas.height = 400;

    ctx.fillStyle = '#00f';
    ctx.fillRect(0, 0, 100, 100);

    ctx.fillStyle = 'rgba(0, 255, 0, .5)';
    ctx.fillRect(30,30,100,100);

    rawPixel = ctx.getImageData(40, 40, 1, 1);
    console.log(rawPixel.data[0], rawPixel.data[1], rawPixel.data[2], rawPixel.data[3]);

    rawPixel = ctx.getImageData(20, 20, 1, 1);
    console.log(rawPixel.data[0], rawPixel.data[1], rawPixel.data[2], rawPixel.data[3]);

  }
});

Что происходит в выше приведенном коде? Все просто - создаются два блока с синим и зеленым цветом, причем блок с зеленым цветом намеренно накладывается на блок с синим цветом.

А затем с помощью метода

1
getImageData
делаем снимок (снимаем отпечаток - если хотите) размером 1х1 пиксель с уже готового рисунка в canvas.

В первом случае левый верхний угол “отпечатка” будет находиться в точке (40, 40) координатной сетки canvas; во-втором случае левый верхний угол “отпечатка” будет находиться в точке (20, 20). В обоих случая ширина и высота “отпечатка” (снимка) равна 1x1 пиксель - то есть, будет делаться “снимок” размером (площадью) в 1 пиксель.

Результат метода

1
getImageData
помещается в переменную
1
rawPixel
. Так как эта переменная не что иное, как ссылка на конкретный экземпляр объекта ImageData, то мы можем воспользоваться свойством
1
data
этого объекта, а точнее - массивом данных. Обращаясь по индексу к каждому из элементов массива, в итоге мы получаем значение цвета данного пикселя в формате RGBA.

Площадь “снимка” можно произвольно увеличить и тогда массив данных также увеличиться. К примеру, такой код:

rawPixel = ctx.getImageData(20, 20, 2, 2);

… создаст массив вида:

Canvas getImageData

Как видим, к любому элементу этого массива можно обратиться по его индексу, чтобы получить значение цвета конкретного пикселя.

Сделаем приведенный выше пример более интересным (и наглядным для понимания) - добавим в него динамики. Преобразуем его так, чтобы в любой момент времени в отдельном информационном блоке выводился цвет (в формате RGBA) того участка canvas, над которым в данный момент находится курсор мыши. Будем считать, что курсор мыши имеет размер 1х1 пиксель:

  var picker = document.querySelector('#picker').getContext('2d');
  var colorBox = document.querySelector('#colorBox');

  var image = new Image();
  image.src = 'images/rhino.jpg';

  function getColor(event) {
    var cx = event.clientX - picker.canvas.offsetLeft;
    var cy = event.clientY - picker.canvas.offsetTop;
    var currentColor = picker.getImageData(cx, cy, 1, 1);
    colorBox.style.background = 'rgba(' + currentColor.data[0] + ',' + currentColor.data[1] + ',' + currentColor.data[2] + ',' + currentColor.data[3] + ')';
    colorBox.textContent = 'rgba(' + currentColor.data[0] + ',' + currentColor.data[1] + ',' + currentColor.data[2] + ',' + currentColor.data[3] + ')';
  }

  if ( picker ) {

    picker.canvas.width = 400;
    picker.canvas.height = 300;

    image.addEventListener('load', function () {
      picker.drawImage(image, 0, 0, picker.canvas.width, picker.canvas.height);
      image.crossOrigin = "Anonymous";
    });

    picker.canvas.addEventListener('mousemove', getColor);

  }

В этом коде функция

1
getColor
при движении курсора мыши (событие
1
mousemove
) над областью canvas (
1
picker.canvas
) считывает координаты этого курсора в переменные
1
cx
и
1
cy
. Значения этих переменных передаются в качестве параметров методу
1
getImageData
, результат работы которого помещается в переменную
1
currentColor
.

Из переменной

1
currentColor
с помощью свойства
1
data
достаются значения (как элементы массива, по индексу) для каждого из RGBA-каналов. Все четыре значения конкатенируются и передаются в виде строкового значения - как фоновый RGBA-цвет для блока
1
colorBox
.

Для пущей наглядности с помощью свойства

1
textContent
в блок
1
colorBox
передается текущее значение цвета.

Представленный выше функционал - не что иное, как обычный Color Picker в любом графическом редакторе. Просто в данном примере достаточно изменить событие

1
mousemove
на событие
1
click
, чтобы все заработало как надо.

Метод putImageData

Возможности метода

1
putImageData
значительно шире, так как этот метод позволяет редактировать canvas. Другими словами, с помощью метода
1
getImageData
получается конкретный экземпляр объекта ImageData из текущего canvas.

Затем при помощи произвольной функции производится преобразование данных этого объекта.

Отредактировванные данные возвращаются обратно в canvas с помощью метода

1
putImageData
.

Давайте на конкретном примере рассмотрим описанный выше пример:

window.addEventListener('DOMContentLoaded', function () {

  var ctx = document.querySelector('#canvas').getContext('2d');

  if ( ctx ) {

    ctx.canvas.width = 400;
    ctx.canvas.height = 300;

    var image = new Image();
    image.src = 'images/rhino.jpg';

    image.addEventListener('load', function () {
      imageDraw(this);
    });

    function imageDraw(img) {

      ctx.drawImage(img,0,0,ctx.canvas.width,ctx.canvas.height);
      var imageData = ctx.getImageData(0,0,ctx.canvas.width,ctx.canvas.height);

      function imageInvert() {
        for ( var i = 0; i < imageData.data.length; i += 4 ) {
          imageData.data[i] = 255 - imageData.data[i];
          imageData.data[i + 1] = 255 - imageData.data[i + 1];
          imageData.data[i + 2] = 255 - imageData.data[i + 2];
        }
        ctx.putImageData(imageData,0,0);
      }

      function grayScaleImage() {
        for ( var i = 0; i < imageData.data.length; i += 4 ) {
          var averageColor = ( imageData.data[i] + imageData.data[i+1] + imageData.data[i+2] ) / 3;
          imageData.data[i] = imageData.data[i+1] = imageData.data[i+2] = averageColor;
        }
        ctx.putImageData(imageData,0,0);
      }

      document.querySelector('#graScaleColor').addEventListener('click', grayScaleImage);
      document.querySelector('#invertColor').addEventListener('click', imageInvert);

    }

  }

}, false);

В приведенном выше коде динамически (с помощью конструктора) создается экземпляр изображения и задается значение для его атрибута

1
src
. Затем на это изображение “вешается” функция, задача которой - отрисовать это изображение в canvas.

Изображение отрисовывается в canvas:

ctx.drawImage(img,0,0,ctx.canvas.width,ctx.canvas.height);

… и тут же с него снимается отпечаток - создается объект ImageData:

var imageData = ctx.getImageData(0,0,ctx.canvas.width,ctx.canvas.height);

Полученный объект

1
imageData
обрабатывается двумя произвольными функциями -
1
imageInvert()
и
1
grayScaleImage()
при событии
1
click
на кнопках:

document.querySelector('#graScaleColor').addEventListener('click', grayScaleImage);
document.querySelector('#invertColor').addEventListener('click', imageInvert);

Функция

1
imageInvert()
инвертирует цвета - “пробегается” по массиву
1
imageData.data
и производит простое вычитание текущего значения цвета из 255:

imageData.data[i] = 255 - imageData.data[i];
imageData.data[i + 1] = 255 - imageData.data[i + 1];
imageData.data[i + 2] = 255 - imageData.data[i + 2];

Функция

1
grayScaleImage()
также “пробегается” по массиву
1
imageData.data
, но при этом производит усреднение значения цвета для каждого из пикселов:

var averageColor = ( imageData.data[i] + imageData.data[i+1] + imageData.data[i+2] ) / 3;
imageData.data[i] = imageData.data[i+1] = imageData.data[i+2] = averageColor;

Приведем еще один пример инвертации цвета. В произвольной функции будет производится перемена цвета местами - значение красного канала будет помешаться в зеленый канал; значение зеленого цвета будет помещаться в красный канал:

window.addEventListener('DOMContentLoaded', function () {

  var ctxCanvas = document.querySelector('#replaceImage').getContext('2d');
  var originImage = document.querySelector('#originImage');

  if ( ctxCanvas ) {

    ctxCanvas.canvas.width = originImage.naturalWidth;
    ctxCanvas.canvas.height = originImage.naturalHeight;
    ctxCanvas.drawImage(originImage, 0, 0);
    ctxCanvas.canvas.style.display = 'none';

    function shiftColors () {
      var imageData = ctxCanvas.getImageData(0, 0, ctxCanvas.canvas.width, ctxCanvas.canvas.height);
      for ( var i = 0; i < imageData.data.length; i += 4 ) {
        var dump = imageData.data[i];
        imageData.data[i+1] = imageData.data[i];
        imageData.data[i] = dump;
      }
      ctxCanvas.putImageData(imageData, 0, 0);
      ctxCanvas.canvas.style.display = 'inline';
    }

    document.querySelector('#btnReplace').addEventListener('click', shiftColors, false);

  }

}, false);

В этом коде canvas задаются размеры оригинального изображения:

ctxCanvas.canvas.width = originImage.naturalWidth;
ctxCanvas.canvas.height = originImage.naturalHeight;

Затем в цикле производится взаимозамещение красного и зеленого каналов:

var dump = imageData.data[i];
imageData.data[i+1] = imageData.data[i];
imageData.data[i] = dump;

Метод toDataURL()

Еще одним интересным методом при работе с замещением пикселей является метод

1
toDataURL()
. Суть его проста - также как и метод
1
getImageData()
, этот метод получается “снимок” текущего canvas и сохраняет результат в виде изображения в двух форматах на выбор -
1
jpg
или
1
png
.

Синтаксис этого метода таков:

var imageJPG = canvas.toDataURL('image/png');
var imagePNG = canvas.toDataURL('image/jpg',1);

Стоит обратить внимание на явное указание (с помощью MIME) формата, в котором производится сохранение изображения. Помимо этого, при сохранении в формате

1
jpg
возможно указание второго параметра, который служит для задания качества сохраняемого изображения (от 0 до 1).

Кроме этого, стоит обратить внимание, что изображение кодируется в base64 формате и именно в этом виде может быть использовано; но никак не в форматах

1
jpg
или
1
png
.

Для внесения большей ясности давайте рассмотрим еще один интересный пример, в котором будет показана работа метода

1
toDataURL()
:

window.addEventListener('DOMContentLoaded', function () {

  var mouseDown = false;
  var drawCtx = document.querySelector('#draw').getContext('2d');

  var link = document.createElement('a');
  link.innerHTML = 'download image';
  link.href = '#';
  link.download = 'result.png';
  document.body.insertBefore(link, drawCtx.canvas);

  if ( drawCtx ) {

    drawCtx.canvas.width = 400;
    drawCtx.canvas.height = 400;
    drawCtx.fillStyle = '#f00';

    function drawCanvas (event) {
      if ( mouseDown ) {
        var xCoor = event.clientX - drawCtx.canvas.offsetLeft;
        var yCoor = event.clientY - drawCtx.canvas.offsetTop;
        drawCtx.beginPath();
        drawCtx.arc(xCoor, yCoor, 2, 0, 360*Math.PI/180);
        drawCtx.fill();
      }
    }

    drawCtx.canvas.addEventListener('mousemove', drawCanvas, false);

    drawCtx.canvas.addEventListener('mousedown', function () {
      mouseDown = true;
    }, false);

    drawCtx.canvas.addEventListener('mouseup', function () {
      link.href = drawCtx.canvas.toDataURL('image/png');
      mouseDown = false;
    }, false);

  }

}, false);

Что мы имеем в приведенном выше коде? Ну, во-первых, это конечно же canvas. На этом canvas’е при помощи мыши мы можем рисовать - за это отвечает функция

1
drawCanvas()
.

У этой функции работа проста, но есть одна фишка - это флаг

1
mouseDown
. Когда курсор мыши попадает в область canvas и начинает двигаться в пределах области этого canvas (событие
1
mousemove
), то запускается функция
1
drawCanvas (event)
.

Но результата работы этой функции нет, так условие внутри этой функции не срабатывает из-за значения флага

1
mouseDown == false
.

С помощью событий

1
mousedown
и
1
mouseup
в коде производится переключение состояний флага
1
mouseDown
из
1
false
в
1
true
и обратно.

Также при событии

1
mouseup
производится “снимок” текущего canvas и помещение его в атрибут
1
href
ссылки
1
a
:

link.href = drawCtx.canvas.toDataURL('image/png');

Обратите внимание на редкий HTML5-атрибут

1
download
, в котором задается имя скачиваемого изображения.

Если для ссылки указан атрибут

1
download
, то при клике по этой ссылке перехода никуда не происходит, а выполняется скачивание изображения с именем по-умолчанию (заданном в атрибуте
1
download
).

Заключение

Вот в принципе и все о манипуляцих с пикселями (raw pixel) в canvas. На самом деле это конечно же не все, что можно рассказать и сделать с помощью этой техники.

Здесь я просто сам познакомился с нею и вкратце описал моменты, которые мне показались наиболее интересными.

Этой статьи бы не было, если бы не существовали источники, которые и помогли ей родиться:

P.S.

“Живых” примеров кода из этого обзора я не привожу, ибо лень ) Адекватная критика и замечания приветствуются )


На этом все.

Небольшая заметка, посвященная вопросу настройки тем оформления (skins) в популярном и очень полезном консольном файловом менеджере Midnight Commander.

И попутно затрагивается вопрос с настройкой отображения кириллицы в Midnight Commander под управлением OSX.

Пару хвалебных слов

Midnight Commander - это консольный файловый менеджер. Консольный - потому что он работает в консоли, из эмулятора терминала. Внешне он очень похож на аналогичный Far Manager под операционной системой Windows.

Midnight Commander - очень легкий, потому что для своей работы он использует псевдографику.

Midnight Commander - обладает большими возможностями, больше половины которых обычный пользователь даже не применяет на практике.

Устанавливается Midnight Commander из пакетного менеджера, так как эта утилита имеется в репозиториях любого дистрибутива Linux. В Debian \ Ubuntu \ Mint установка производится такой командой:

sudo apt-get install mc

Оформление Midnight Commander

После установки Midnight Commander и его первоначального запуска внешний вид программы будет примерно таким:

Midnight Commander Default Skin

Прямо скажем, зрелище не очень привлекательное, особенно - зеленый шрифт на синем фоне. Это тема оформления (skin) по умолчанию для Midnight Commander и называется она также - default.

Но оформление Midnight Commander можно (и нужно) поменять и сделать это просто, так как эта программа идет с предустановленным набором тем оформления.

Готовые темы оформления (skins) после установки Midnight Commander располагаются по пути:

~|⇒ ll /usr/share/mc/skins

Туда можно заглянуть и выбрать, что понравиться:

~|⇒ ll /usr/share/mc/skins
total 212K
-rw-r--r-- 1 root root 3,0K Dec  5  2013 darkfar.ini
-rw-r--r-- 1 root root 3,0K Dec  5  2013 dark.ini
-rw-r--r-- 1 root root 2,7K Dec  5  2013 default.ini
-rw-r--r-- 1 root root 2,7K Dec  5  2013 double-lines.ini
-rw-r--r-- 1 root root 3,1K Dec  5  2013 featured.ini
-rw-r--r-- 1 root root 2,2K Dec  5  2013 gotar.ini
-rw-r--r-- 1 root root 2,3K Dec  5  2013 mc46.ini
-rw-r--r-- 1 root root 4,0K Dec  5  2013 modarcon16-defbg.ini
-rw-r--r-- 1 root root 4,0K Sep 24  2012 modarcon16-defbg-thin.ini
-rw-r--r-- 1 root root 4,0K Dec  5  2013 modarcon16.ini
-rw-r--r-- 1 root root 4,0K Dec  5  2013 modarcon16root-defbg.ini
-rw-r--r-- 1 root root 4,0K Sep 24  2012 modarcon16root-defbg-thin.ini
-rw-r--r-- 1 root root 4,0K Dec  5  2013 modarcon16root.ini
-rw-r--r-- 1 root root 4,0K Sep 24  2012 modarcon16root-thin.ini
-rw-r--r-- 1 root root 4,0K Sep 24  2012 modarcon16-thin.ini
-rw-r--r-- 1 root root 4,1K Dec  5  2013 modarin256-defbg.ini
-rw-r--r-- 1 root root 4,1K Sep 24  2012 modarin256-defbg-thin.ini
-rw-r--r-- 1 root root 4,1K Dec  5  2013 modarin256.ini
-rw-r--r-- 1 root root 4,1K Dec  5  2013 modarin256root-defbg.ini
-rw-r--r-- 1 root root 4,1K Sep 24  2012 modarin256root-defbg-thin.ini
-rw-r--r-- 1 root root 4,1K Dec  5  2013 modarin256root.ini
-rw-r--r-- 1 root root 4,1K Sep 24  2012 modarin256root-thin.ini
-rw-r--r-- 1 root root 4,1K Sep 24  2012 modarin256-thin.ini
-rw-r--r-- 1 root root 2,9K Dec  5  2013 nicedark.ini
-rw-r--r-- 1 root root 5,4K Dec  5  2013 sand256.ini
-rw-r--r-- 1 root root 3,9K Dec  5  2013 xoria256.ini

Выбрать тему оформления для Midnight Commander можно командой:

~|⇒ mc -S darkfar

Здесь ключ -S указывает, что при запуске Midnight Commander необходимо использовать тему оформления. Имя темы оформления (skin) указывается после ключа. Результат приведенной выше команды будет следующим:

Midnight Commander Darkfar Skin

Уже значительно лучше, не правда ли? Таким образом можно перебрать все имеющиеся в комплекте темы и выбрать понравившуюся.

Когда тема оформления выбрана, нужно прописать ее в конфигурационном файле Midnight Commander, чтобы последний запускался каждый раз именно с этим skin’ом.

Файл настроек Midnight Commander располагается по пути:

~|⇒ ll ~/.config/mc/ini

и запускается на редактирование таким образом:

~|⇒ nano ~/.config/mc/ini

В этом файле нужно найти строчку skin и изменить значение параметра на название файла темы (из /usr/share/mc/skins):

...
editor_filesize_threshold=64M
mcview_eof=
ignore_ftp_chattr_errors=true
skin=modarin256

[Layout]
message_visible=1
keybar_visible=1
...

Обратите внимание на название skin’а в данном случае - modarin256. Здесь 256 - это количество цветов отображения, которые используются в этой теме.

По умолчанию в Linux Mint консоль не поддерживает отображение такого количества цветов. Если запустить Midnight Commander с темой modarin256 (к примеру), то появится ошибка и предложении использовать тему по-умолчанию (default).

Включить поддержку отображения 256 цветов в консоли можно, добавив строку export TERM=xterm-256color в файле .bash_profile (если используется BASH), в файле .zshrc (если используется ZSH), в файле .profile (если используется macOS).

В моем случае используется ZSH и файл .zshrc будет выглядеть таким образом:

# User configuration
...
export TERM=xterm-256color
...

Если все сделано без ошибок, то запуск Midnight Commander выдаст такой результат (используется тема оформления modarin256):

Midnight Commander Modarin Skin

Можно попробовать тему xoria256 - хорошо проработанная тема, с которой также приятно работать. Об этой теме была статья на Хабрахабр - Цветовая схема Xoria256 для Midnight Commander:

Midnight Commander Xoria Skin

Midnight Commander и кириллица в macOS

Установка и настройка Midnight Commander в операционной системе OSX мало отличается от аналогичных действий в Linux.

Устанавливать Midnight Commander в macOS проще всего с помощью Homebrew:

$ brew update
$ brew install mc

Не забываем включить поддержку 256 цветов в консоли OSX, если хотим использовать богатые цветом темы оформления Midnight Commander, такие как modarin256 или xoria256.

Для этого редактируем файл .bash_profile или файл .zshrc (если используется ZSH):

...
export TERM=xterm-256color
...

Дополнительным шагом будет добавление в файл .bash_profile (или .zshrc) двух строчек:

...
export LC_CTYPE=en_US.UTF-8
export LC_ALL=en_US.UTF-8
...

… для того, чтобы в Midnight Commander правильно отображались русскоязычные имена файлов и директорий. Иначе вместо вразумительных имен файлов будут одни вопросительные знаки.

Вариант с добавлением строки:

export LANG=ru_RU.UTF-8

… в файле .bash_profile у меня не сработал.


На этом все.

С недавних пор на практике оценил преимущества использования профессиональных IDE для задач кодинга. К таким IDE я отношу WebStorm, Visual Studio Code, Aptana Studio.

До недавнего времени я пользовался отличным Sublime Text (к поклонникам Atom я себя не отношу) и все меня устраивало. Но в последнее время я все больше и больше начинаю заниматься с JavaScript (надо сказать - не без удовольствия, особенно впечатлил Canvas).

И вот тут произошло так, что в один прекрасный день я просто попробовал поработать в WebStorm с JavaScript. И все! Я уже не мог вернуться на Sublime Text!

Описать конкретно, что именно мне понравилось в WebStorm vs Sublime Text, я так вот в двух словах и не могу. Но скажу только одно - работа в WebStorm действительно удобная; в этом IDE есть много продуманных и отшлифованных вещей, которые сильно облегчают жизнь кодера.

После того, как я оценил работу в WebStorm на Mac OS X, мне захотелось иметь этот IDE и на ноутбуке с Linux Mint 17.2 Cinnamon. Не могу сказать точно, почему так, но мне работать под Linux как-то комфортнее, чем под Mac OS X. Наверное, просто сказывается сила привычки - я linuxoid со стажем.

Но вот незадача - под Linux я привык пользоваться супер-удобными менеджерами пакетов, такими как

1
apt-get
или
1
pacman
.

А вот что касается Visual Studio Code, Aptana Studio или WebStorm - то официальных портов этих IDE в Debian \ Ubuntu-репозиториях нет (поправьте меня, если я ошибаюсь).

На официальных сайтах этих IDE описывается только процесс установки под операционную систему Linux вручную. Не сказать, что там плохо описан этот процесс, но мне он не помог совсем.

Как результат, я решил вкратце описать процесс ручной установки IDE WebStorm под Linux Mint 17.2 Cinnamon \ Xfce. Две другие IDE - Aptana Studio и Visual Studio Code устанавливаются абсолютно аналогично.

  • Шаг первый - с официального сайта скачивается пакет под Linux (32 или 64 бита - на выбор)

  • Шаг второй - делается копия скачанного архива с IDE WebStorm и помещается в любое удобное место (например, пусть это будет Desktop)

  • Шаг третий - распаковывается архив с IDE WebStorm (который в Desktop)

  • Шаг четвертый - в терминале запускается Midnight Commander с правами root:

    1
    
    sudo mc
    ; если вы вдруг не знаете, что такое Midnight Commander - то в самое время узнать о нем, так как это программа из разряда must have под системой Linux

  • Шаг пятый - в Midnight Commander копируем распакованный архив WebStorm по пути

    1
    
    ~/opt/
    ; в итоге в
    1
    
    ~/opt/
    должна появиться папка примерно такого вида - “WebStorm-141.456” (
    1
    
    ~/opt/WebStorm-141.456
    )

  • Шаг шестой - на любом пустом месте Desktop делаем правый клик мыши (ПКМ), чтобы вызвать контекстное меню

  • Шаг седьмой - в контекстном меню находим строку “Create Launcher”; текст строки может отличаться в зависимости от того, что именно используется на конкретном Linux - Cinnamon или Xfce; тут главное - увидеть знакомое слово “Launcher”; в результате должно открыться примерно такое окно (в данном случае это Xfce):

Create Launcher

  • Шаг восьмой - вводим значения в поля этого окна; во все поля вводить данные необязательно; нужно ввести только имя приложения в поле “Name” - WebStorm; в поле “Command” вручную вводить путь к исполняемому файлу приложения нет необходимости - достаточно нажать на значок рядом с полем и откроется диалоговое окно “Select an Application”; дальше можно легко и удобно найти IDE WebStorm по пути:

    1
    
    ~/opt/WebStorm-141.456/bin/webstorm.sh

  • Шаг девятый - в поле “Icon” добавляем фирменную иконку приложения WebStorm (чтобы легко углядеть WebStorm на Desktop); снова жмем на значок (уже в поле “Icon”); откроется диалоговое окно “Select an Icon”; в этом окне в выпадающем списке поля “Select icon from” выбираем самую нижнюю строку - “Image Files”; снова идем по пути

    1
    
    ~/opt/WebStorm-141.456/bin/webide.png

Если все шаги выполнены правильно, то в результате должно получиться примерно такое окно с заполненными полями:

Create Launcher Ready

Это минимальная конфигурация, достаточная для нормального запуска приложения из Desktop. При первом запуске WebStorm-приложения Linux-система задаст вопрос - сделать ли запускаемый файл исполняемым. Естественно, соглашаемся - ведь нам нужно запустить и работать в WebStorm-приложении.

Как я уже упоминал ранее, установка двух других IDE - Aptana Studio и Visual Studio Code ничем не отличается от установки WebStorm. Единственный момент - для Visual Studio Code нужно покопаться с поисках фирменной иконки, которая расположена по пути:

1
~/opt/VSCode-linux-x64/resources/app/resources/linux/code.png
, а исполняемый файл приложения - по пути:
1
~/opt/VSCode-linux-x64/code
.

К слову сказать, лично я был приятно удивлен Visual Studio Code и разочарован Aptana Studio. WebStorm - вне конкуренции!

WebStorm темы

Хочу немного отклониться в сторону выбора темы оформления под IDE WebStorm. В Sublime Text это была однозначно - Material Theme.

Под WebStorm есть порт этой темы - Material Theme JetBrains.

Документация хорошо расписана и автор даже постарался создать возможность легкой и “кошерной” установки темы - через репозиторий JetBrains, из самого WebStorm.

Но, как мне кажется, эта тема заметно уступает своему “оригиналу” из-под Sublime Text (автор сам об этом упоминает).

Хорошая коллекция тем под WebStorm расположена по этим адресам:

В дополнение можно еще установить модный шрифт Hack (на любителя). Или покопаться здесь - Top 11 Programming Fonts, чтобы выбрать что-то подходящее.

К примеру, автор блога WesBos долго пользовался OpenSource-шрифтом Inconsolata, а потом взял и купил шрифт Operator Mono за $200.

P.S.

Еще в тему установки программных пакетов для разработки под системой Linux стоит сказать, что под Ubuntu существует удобный пакет Ubuntu Make (он же Ubuntu Developer Tools Center в прошлом).

Задача пакета Ubuntu Make - быстрая и легкая установка общих потребностей разработчика в Ubuntu. Ubuntu Make может устанавливать:

На Хабрахабр есть небольшая обзорная статья об этом пакете - Ubuntu Make — разработчику в помощь.

Лично от себя могу сказать, что первый раз установка WebStorm при помощи Ubuntu Make на моем ноутбуке с Linux Mint 17.2 прошла “на ура”.

А вот во-второй раз что-то не заладилось и Ubuntu Make “не хочет” ставить WebStorm - выдает какую-то ошибку, с которой мне нет желания разбираться.


На этом все.

Попытка разобраться, в чем различие между

1
exports
и
1
module.exports
, основанная на статье Understanding module.exports and exports in Node.js.

Что такое
1
module.exports
и
1
exports
в Node.js

Как web-разработчики, мы часто сталкиваемся с ситуацией, когда необходимо иметь дело с малознакомым кодом. И тогда сам собою возникает логичный вопрос - сколько времени мне потребуется для того, чтобы разобраться с чужим кодом и понять принцип его работы?

Типичный ответ на этот вопрос - ровно столько, чтобы начать самому писать код; а затем вернуться к изучению этой темы позже, когда позволит на это время. Ну что-же, как мне кажется, пришло время получше разобраться с такими понятиями, как

1
module.exports
и
1
exports
в Node.js. Спешу поделиться с вами тем, что я узнал по этому вопросу.

Что такое модуль (module)

Основная концепция модуля заключается в том, что он инкапсулирует логически связанный между собой код в единый блок. Можно сказать иначе - создание модуля заключается в помещении всех взаимосвязанных между собой функций в один единый файл.

Для понимания вышесказанного лучше всего создать пример приложения под Node.js. Давайте создадим файл с именем

1
greetings.js
, внутри которого размещены две функции:

sayHelloInEnglish = function () {
  return 'Hello';
}

sayHelloInSpanish = function () {
  return 'Hola';
}

Экспорт модуля

Польза от файла (модуля)

1
greetings.js
(и функций, которые находятся в этом файле) появляется в том случае, когда файл
1
greetings.js
можно использовать внутри других файлов (модулей).

Для достижения этого необходимо слегка изменить исходный код файла

1
greetings.js
. Чтобы понять, что происходит на самом деле, в данном случае будем выполнять пошаговый процесс:

  • представьте себе, что эта строка существует в качестве первой линии кода в
    1
    
    greetings.js
    :
var exports = module.exports = {}
  • видоизменим обе функции в файле
    1
    
    greetings.js
    с помощью выражения
    1
    
    exports
    таким образом, чтобы они были доступны для внешних файлов (модулей):
exports.sayHelloInEnglish = function () {
  return 'Hello';
}

exports.sayHelloInSpanish = function () {
  return 'Hola';
}

В приведенном выше коде можно было бы заменить выражение

1
exports
на
1
exports.module
и получить точно такой же результат.

Если этот момент кажется вам непонятным, то помните, что выражение

1
exports
и выражение
1
exports.module
ссылаются на один и тот же объект.

  • это текущее значение выражения
    1
    
    module.exports
    :
module.exports = {

  sayHelloInEnglish = function () {
    return 'Hello';
  }

  sayHelloInSpanish = function () {
    return 'Hola';
  }
}

Импортирование модуля

Давайте сделаем методы модуля

1
greetings.js
общедоступными для другого файла (модуля) с именем
1
main.js
. Этот процесс также разобьем пошагово для более лучшего понимания:

  • в Node.js используется команда
    1
    
    require
    для импортирования одного модуля в другой модуль:
var require = function(path) {

  // ...

  return module.exports;
};
  • давайте подключим модуль
    1
    
    greetings.js
    в модуль
    1
    
    main.js
    :
// main.js
var greeting = require('./greetings.js');

Приведенная выше строка кода равнозначна нижеследующему коду:

// main.js
var greeting = {

sayHelloInEnglish = function () {
  return 'Hello';
}

sayHelloInSpanish = function () {
  return 'Hola';
}

}
  • теперь можно использовать функции модуля
    1
    
    greetings.js
    внутри модуля
    1
    
    main.js
    как методы объекта
    1
    
    greeting
    :
// main.js
var greeting = require('./greetings.js');

// Hello
greeting.sayHelloInEnglish('Hello');

// Hola
greeting.sayHelloInSpanish('Hola');

Отличительные моменты

Команда

1
require
возвращает объект, свойства и методы которого доступны другим внешним модулям при помощи команды
1
module.exports
.

Нижеприведенный пример поможет разобраться в данном вопросе:

// greetings.js

// var exports = module.exports = {};

exports.sayHelloInEnglish = function () {
  return 'Hello';
}
exports.sayHelloInSpanish = function () {
  return 'Hola';
}

// Эта строка кода выполняет повторное переопределение,

module.exports = 'Bonjour';

Теперь сделаем подключение модуля

1
greetings.js
в модуль
1
main.js
:

// main.js
var greetings = require('./greetings.js')

На данный момент в нашем примере ничего не поменялось. В переменную

1
greetings
помещается код, доступный из модуля
1
greetings.js
. Не более того.

Однако, если мы попытаемся воспользоваться каким-либо из методов модуля

1
greetings.js
-
1
sayHelloInEnglish
или
1
sayHelloInSpanish
, то мы получим ошибку. Это произошло в следствие того, что было произведено переопределение экспортируемой структуры модуля при помощи команды
1
module.exports
.

Другими словами, последней командой

1
module.exports
экспортируется совсем другой модуль -
1
Bonjour
, у которого другие свойства и методы. Происходит переопределение экспортируемого модуля и вызов метода
1
sayHelloInEnglish
или
1
sayHelloInSpanish
вызовет ошибку:

// main.js
// var greetings = require("./greetings.js");

/*
 * TypeError: object Bonjour has no
 * method 'sayHelloInEnglish'
 */
greetings.sayHelloInEnglish();

/*
 * TypeError: object Bonjour has no
 * method 'sayHelloInSpanish'
 */
greetings.sayHelloInSpanish();

Чтобы отследить ошибки при использовании модуля

1
greetings
, можно вывести их в консоль:

// "Bonjour"
console.log(greetings);

Заключение

Импорт и экспорт модулей в Node.js является ежедневной задачей. Я надеюсь, что благодаря этой статье стала ясна разница между командой

1
exports
и командой
1
module.exports
. Более того, если у вас когда-либо произойдет ошибка при доступе к общедоступным методам модуля в будущем, то я надеюсь, что у вас есть лучшее понимание того, почему может возникнуть эти ошибки.

Заключение автора перевода

Если честно - прочитал статью и даже потрудился перевести ее, а вот разницы между

1
exports
и
1
module.exports
не заметил. Я хочу сказать, что автор этой статьи (как мне кажется) так и не показал разницы между ними. Могу ошибаться, конечно и буду рад комментариям.

UPD. Вопрос снят, так как решен. В принципе, существование этой и предыдущей статьи уже не надобно, так как есть отличный скринкаст от Ильи Кантора по Node.js, где раскрываются все вопросы - Скринкаст NODE.JS.

В свете этого скринкаста в обеих статьях обнаруживаются достаточно существенные ошибки. Прямо и не знаю - может убрать обе эти статьи о греха подальше? ))