Знакомство с техникой Drag-and-Drop и механизмом ее реализации с помощью JavaScript. Данный обзор не претендует на полноту покрытия материала. Задача статьи - познакомиться с созданием Drag-and-Drop на JavaScript. Понять сам принцип механизма и научиться применять основные инструменты для его реализации.

Определение Drag-and-Drop

Сам механизм Drag-and-Drop интуитивно понятен - “схватил-перетащил-бросил”. Преимущество внедрения Drag-and-Drop в интерфейсы заключается в упрощении реализации задач; в уменьшении количества пунктов меню типа “Copy-Paste”.

События Drag-and-Drop

Механизм Drag-and-Drop имеет в своем составе целую группу событий, с помощью которых можно контролировать процесс перетаскивания:

  • dragstart - пользователь начинает перетаскивание элемента
  • dragenter - перетаскиваемый элемент входит в область целевого объекта
  • dragover - перетаскиваемый элемент перемещается в области целевого объекта
  • dragleave - перетаскиваемый элемент покидает область целевого объекта
  • drag - момент начала процесса перетаскивания объекта
  • drop - момент, когда отпускается зажатая клавиша мыши (перетаскиваемый объект “роняется”)
  • dragend - момент завершения процесса перетаскивания объекта

Объект dataTransfer

Механизм Drag-and-Drop также имеет в своем составе объект dataTransfer, который служит для вспомогательных целей. В этом объекте хранится необходимая информация о событии перетаскивания. Помимо этого, в объект dataTransfer можно добавлять данные; а также считывать из него данные.

Свойства (наиболее важные) объекта dataTransfer:

  • dataTransfer.effectAllowed - задаем тип перетаскивания, которое пользователь может выполнять с элементом
  • dataTransfer.dropEffect - задаем внешний вид курсора мыши в соответствии с заданным типом перетаскивания

Методы (наиболее важные) объекта dataTransfer:

  • setData() - добавляет данные в нужном формате
  • clearData() - удаляет данные
  • setDragImage() - устанавливает изображение для перетаскивания с координатами курсора (0, 0 — левый верхний угол)
  • getData() - возвращает данные

Ниже будет рассматриваться практический пример реализации Drag-and-Drop на JavaScript.

HTML разметка

Базовая HTML-разметка будет простой:

<h2 id="dropStatus">application status</h2>
<h1 id="dropTitle">drop zone</h1>
<div id="dropZone"></div>
<div id="objectsZone">
    <div id="object1" class="objects">object 1</div>
    <div id="object2" class="objects">object 2</div>
    <div id="object3" class="objects">object 3</div>
</div>
<hr/>
<button type="button" id="readDropZone">get object data</button>

Что для чего служит в этой разметке?

Заголовок id="dropStatus" будет отображать текущее состояние процесса Drag-and-Drop. В него мы будет отправлять информацию о текущем состоянии Drag-and-Drop при помощи событий, о который говорилось выше.

Заголовок id="dropTitle" служит просто для декоративных целей.

Блок id="dropZone" является целевой областью - в нее мы будет перетаскивать объекты.

Объекты id="object1", id="object2", id="object3" - это перетаскиваемые объекты; их мы будем перемещать в область блока id="dropZone".

Кнопка id="readDropZone" будет выводить информацию об перемещенных объектах.

В итоге разметка совместно со стилями будут выглядеть таким образом - JavaScript - Drag’n’Drop - Part 1.

JavaScript - разбираемся с событиями

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

Итак, начнем с перетаскиваемых элементов id="object1", id="object2", id="object3". На каждый из них мы повесим два события:

  • dragstart - событие начала процесса перетаскивания элемента
  • dragend - событие окончания процесса перетаскивания элемента

Для каждого из элементов, при возникновении на нем события, мы будем запускать соответствующую функцию dragStart или dragEnd:

var objects = document.querySelectorAll('#objectsZone > .objects');
...
if ( objects ) {
    [].forEach.call(objects, function (el) {
        el.setAttribute('draggable', 'true');
        el.addEventListener('dragstart', dragStart, false);
        el.addEventListener('dragend', dragEnd, false);
    });
}

Обратим внимание на строку el.setAttribute('draggable', 'true'); - здесь мы динамически добавляем для всех элементов с классом .objects атрибут draggable="true", тем самым делая (благодаря HTML5) эти элементы доступными для перетаскивания.

На элемент id="dropZone" мы “повесим” гораздо больше событий:

  • dragenter - перетаскиваемый объект (например, id="object1") входит в область целевого объекта (id="dropZone")
  • dragleave - перетаскиваемый объект (например, id="object1") выходит из области целевого объекта (id="dropZone")
  • dragover - перетаскиваемый объект (например, id="object1") перемещается внутри области целевого объекта (id="dropZone")
  • drop - перетаскиваемый объект (например, id="object1") помещается внутри целевого объекта (id="dropZone")

И конечно же, для каждого события будет своя функция. JavaScript-код в итоге будет выглядеть таким образом:

var dropZone = document.querySelector('#dropZone');
...
if ( dropZone ) {
    dropZone.addEventListener('dragenter', dragEnter, false);
    dropZone.addEventListener('dragleave', dragLeave, false);
    dropZone.addEventListener('dragover', dragOver, false);
    dropZone.addEventListener('drop', dragDrop, false);
}

Ну и на кнопку id="readDropZone" мы “повесим” обычный код с функцией readZone:

var dropButton = document.querySelector('#readDropZone');
...
if ( dropButton ) {
    dropButton.addEventListener('click', readZone, false);
}

Если суммировать все вышесказанное, то общий вид handler’ов в нашем случае будет выглядеть таким образом:

// LISTENERS

if ( objects ) {
    [].forEach.call(objects, function (el) {
        el.setAttribute('draggable', 'true');
        el.addEventListener('dragstart', dragStart, false);
        el.addEventListener('dragend', dragEnd, false);
    });
}

if ( dropZone ) {
    dropZone.addEventListener('dragenter', dragEnter, false);
    dropZone.addEventListener('dragleave', dragLeave, false);
    dropZone.addEventListener('dragover', dragOver, false);
    dropZone.addEventListener('drop', dragDrop, false);
}

if ( dropButton ) {
    dropButton.addEventListener('click', readZone, false);
}

Далее будет детально останавливаться на каждой из функций - что она делает и для чего.

Функция dragStart (event)

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

В данном случае мы будем обрабатывать событие dragstart, которое возникает на перетаскиваемом элементе (id="object1", id="object2" или id="object3" - не важно).

Событие dragstart в момент своего возникновения автоматически генерирует объект dataTransfer, который (как мне кажется) можно в общих чертах сравнить с событийным объектом Event; последний также хранит в себе множество данных о произошедшем событии. Некоторыми методами и свойствами объекта Event мы воспользуемся в нашем примере:

var dropStatus = document.querySelector('#dropStatus');
...
function dragStart (event) {
    dropStatus.innerHTML = 'Dragging the ' + event.target.getAttribute('id');
    event.dataTransfer.dropEffect = 'move';
    event.dataTransfer.setData('text', event.target.getAttribute('id'));
}

Функция dragStart при возникновении события “берет” элемент dropStatus и методом innerHTML “пихает” внутрь него строку, часть которой представляет из себя значение атрибута id элемента, на котором произошло событие (event.target).

Для объекта dataTransfer задается значение его свойства dropEffect - move.

В третьей строке для объекта dataTransfer с помощью метода setData() задается имя переменной text и значение для этой переменной - ID текущего элемента.

Функции dragEnter(), dragLeave(), dragOver()

Три функции, каждая из которых отслеживает событие, возникающее на элементе dropZone:

function dragEnter (event) {
    dropStatus.innerHTML = 'You are dragging over ' + event.target.getAttribute('id');
    this.classList.add('over');
}

function dragLeave (event) {
    dropStatus.innerHTML = 'You left the ' + event.target.getAttribute('id');
    this.classList.remove('over');
    this.removeAttribute('class');
}

function dragOver (event) {
    event.preventDefault();
}

Первые две функции - dragEnter (event) и dragLeave (event) очень похожи между собой. Каждая из них манипулирует содержимым заголовка dropStatus, сигнализируя о происходящем событии.

Третья функция dragOver (event) может показаться странной. Все ее назначение - это отмена действия по-умолчанию. Что это за действие по-умолчанию? Дело в том, что у браузеров имеется свой собственный (помимо HTML5) механизм реализации события перетаскивания Drag-and-Drop. И если его не отключить, то он не даст срабатывать нашему механизму.

Функция dragDrop (event)

Самая большая и самая важная функция в нашем коде. Она также срабатывает на событие, возникающее на элементе dropZone:

var droppedIN = false;
...
function dragDrop (event) {
    event.preventDefault();
    var elementID = event.dataTransfer.getData('text');
    var element = document.getElementById(elementID);
    event.target.appendChild(element);
    element.removeAttribute('draggable');
    element.classList.add('dragged');
    element.style.cursor = 'default';
    droppedIN = true;
    dropStatus.innerHTML = 'Element ' + elementID + ' dropped into the ' + event.target.getAttribute('id');
}

В строке event.preventDefault(); мы снова отменяем действие по-умолчанию. На этот раз это касается самого перетаскиваемого элемента - ведь он может быть ссылкой и браузер выполнит переход по ней (действие по-умолчанию), что нам совсем не нужно.

В строке:

var elementID = event.dataTransfer.getData('text');

… мы из объекта dataTransfer получаем ID перетаскиваемого элемента. Вы же помните, что в функции dragStart (event) с помощью строки:

event.dataTransfer.setData('text', event.target.getAttribute('id'));

…мы его как раз получали?

Далее находим перетаскиваемый элемент по его ID:

var element = document.getElementById(elementID);

И помещаем его внутрь текущего активного элемента:

event.target.appendChild(element);

Далее убираем у перетаскиваемого элемента атрибут draggable - он больше не перетаскиваемый. Визуально сигнализируем об этом, изменив вид курсора мыши:

element.style.cursor = 'default';

И сообщаем об изменившемся статусе в заголовке:

dropStatus.innerHTML = 'Element ' + elementID + ' dropped into the ' + event.target.getAttribute('id');

Отдельного упоминания стоит строка droppedIN = true;. Это флаг, с помощью которого мы определяем, произошло ли событие drop или нет.

Может случиться так, что объект мы перетащили в область элемента dropZone, но передумали его помещать туда. И “отпустили” перетаскиваемый элемент за областью элемента dropZone. В результате событие dragend произошло, но событие drop не выполнилось.

Такую ситуацию обрабатывает функция dragEnd():

function dragEnd() {
    if ( droppedIN === false ) {
        dropStatus.innerHTML = 'You let the ' + event.target.getAttribute('id') + ' to go!';
    }
    droppedIN = false;
}

Функция readZone ()

Последняя функция из нашего примера - это функция-счетчик. Ее задача - просто посчитать, сколько элементов на данный момент мы “бросили” в область dropZone:

function readZone () {
    var dropZoneChild = dropZone.children;
    for ( var i = 0; i < dropZoneChild.length; i++ ) {
        alert('Object ' + dropZoneChild[i].getAttribute('id') + ' is in ' + dropZone.getAttribute('id'));
    }
}

Нажимаем кнопку dropButton и alert’ом последовательно выводим все элементы, помещенные внутрь объекта dropZone.

Вот, в принципе, и все, что можно вкратце сказать. Осталось только взглянуть на готовый пример работы кода - JavaScript - Drag’n’Drop - Part 2.

На этом все. Здоровая критика и полезные замечания только приветствуются.


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

Есть более детальный обзор и более интересный пример задачи на JavaScript Drag-and-Drop размещен здесь:

Данная статья планируется как пошаговый обзор создания простой 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-дерево документа (событие 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 - setInterval(). А также немного воображения.

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

Чтобы мячик двигался достаточно быстро, изменять значения координат (ballX += ballStepX и ballY += ballStepY) мячика по оси X и Y будем с определенным шагом (ballStepX и 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;
}
...

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

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

Двигаем paddle

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

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

Обратите внимание на последнюю строку функции - paddleX = mouseX - paddleWidth/2;. Переменная 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;
}
...

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

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

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

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

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

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

Одна из таких функций уже была создана ранее - это функция 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 является последнее - 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()

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

Метод getImageData

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

Создадим простой пример для наглядного отображения работы метода 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]);

  }
});

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

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

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

Результат метода getImageData помещается в переменную rawPixel. Так как эта переменная не что иное, как ссылка на конкретный экземпляр объекта ImageData, то мы можем воспользоваться свойством 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);

  }

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

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

Для пущей наглядности с помощью свойства textContent в блок colorBox передается текущее значение цвета.

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

Метод putImageData

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

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

Отредактировванные данные возвращаются обратно в canvas с помощью метода 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);

В приведенном выше коде динамически (с помощью конструктора) создается экземпляр изображения и задается значение для его атрибута 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);

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

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

Функция imageInvert() инвертирует цвета - “пробегается” по массиву 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];

Функция grayScaleImage() также “пробегается” по массиву 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()

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

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

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

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

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

Для внесения большей ясности давайте рассмотрим еще один интересный пример, в котором будет показана работа метода 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’е при помощи мыши мы можем рисовать - за это отвечает функция drawCanvas().

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

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

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

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

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

Обратите внимание на редкий HTML5-атрибут download, в котором задается имя скачиваемого изображения.

Если для ссылки указан атрибут download, то при клике по этой ссылке перехода никуда не происходит, а выполняется скачивание изображения с именем по-умолчанию (заданном в атрибуте 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 (если используется OSX).

В моем случае используется 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 и кириллица в OSX

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

Устанавливать Midnight Commander в OSX проще всего с помощью 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 я привык пользоваться супер-удобными менеджерами пакетов, такими как apt-get или 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: sudo mc; если вы вдруг не знаете, что такое Midnight Commander - то в самое время узнать о нем, так как это программа из разряда must have под системой Linux

  • Шаг пятый - в Midnight Commander копируем распакованный архив WebStorm по пути ~/opt/; в итоге в ~/opt/ должна появиться папка примерно такого вида - “WebStorm-141.456” (~/opt/WebStorm-141.456)

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

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

Create Launcher

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

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

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

Create Launcher Ready

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

Как я уже упоминал ранее, установка двух других IDE - Aptana Studio и Visual Studio Code ничем не отличается от установки WebStorm. Единственный момент - для Visual Studio Code нужно покопаться с поисках фирменной иконки, которая расположена по пути: ~/opt/VSCode-linux-x64/resources/app/resources/linux/code.png, а исполняемый файл приложения - по пути: ~/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 - выдает какую-то ошибку, с которой мне нет желания разбираться.


На этом все.