Имеется такая задача - на странице расположен блок. Страница скроллится, то есть наличествует событие scroll.

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

Вариантов решения этой задачи может быть несколько. В этой статье будут показаны два из них. Все они будут выполнены на JavaScript + jQuery.

Вариант первый - метод off()

Этот способ основан на методе jQuery - .off(). Как говорится в описании, метод

1
.off()
удаляет событие, вызванное с помощью метода
1
.on()
. То есть, выполняет обратную операцию.

Рассмотрим пример ниже:

var blockScrolled = $('.scrolled');
var spins = $('.spin');

spins.text('0');

$(window).on('scroll', function () {
    if ( $(window).scrollTop() > blockScrolled.offset().top - $(window).height() / 2 ) {
        spins.each( function () {
            var current = $(this);
            $( { Spin: 0 }).animate( { Spin: current.attr('data-stop') }, {
                duration: 2000,
                easing: 'swing',
                step: function ( now ) {
                    current.text( Math.ceil( now ) );
                }
            });
        });
        $(window).off('scroll');
    }
});

Что мы имеем в этом коде? Видим, что на окно

1
window
повешен обработчик ( метод
1
.on()
`) события scroll.

При возникновении этого события выполняется проверка условия. Если условие true, то для каждого из элементов коллекции spins выполнить функцию.

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

1
$(window).off('scroll')
. Эта строка “говорит” - на объекте
1
window
остановить событие scroll.

Мы получили нужный результат - при событии scroll и выполнении условия функция выполнится только один раз.

Один важный момент - после выполнения строки

1
$(window).off('scroll')
событие scroll будет “прибито” и дальнейшие попытки повесить handler на него не увенчаются успехом.

Другими словами, если дальше на странице есть еще элементы, на которых нужно обработать событие scroll, то ничего не получится.

Вариант второй - маркер

Второй способ не использует метод

1
.off()
, но применяет специальный маркер. Код простой, но эффективный. И в этом случае событие scroll не “убивается”, а функция исполняется один раз:

var marker = true;

function count() {
    // function code here ...
    marker = false;
}

$( window ).on('scroll', function () {
    if ( $( window ).scrollTop() > overview.offset().top - $( window ).height() * 0.5 ) {
        if ( marker ) {
            count();
        }
    }
});

Что есть в этом коде? Есть глобальная переменная

1
marker
, у которой изначально значение
1
true
.

При выполнении условия функция

1
count()
запускается и по ходу дела меняет значение
1
marker
на противоположный. Таким образом функция
1
count()
сработает только один раз.

Но событие scroll останется “невредимым”.

Заключение

За бортом остался еще один jQuery-метод - .one(). Его задача - также запустить на исполнение функцию только один раз.

Например, в коде ниже клик мыши на каждом из элементов коллекции likeLinks сработает только один раз:

var likeLinks = $('.overlay__link-heart');
likeLinks.one('click', function () {

// увеличить значение счетчика на единицу при клике
// -----------------------------------------------------------
var currentCount = $(this).find('.overlay__link-count');
currentCount.html( currentCount.html() * 1 + 1 );

Однако, применить метод

1
.one()
для обработки события scroll и вызова на исполнение функции только один раз у меня не получилось. Событие scroll вообще не срабатывало.

На этом все. Похоже на то, что все верно написал и описал. Но если будут здравые комментарии и замечания, то буду только рад )


P.S. Сразу на ум пришло одно у самого себя - заменить

1
$( window )
на переменную. Но уже лень менять. )

Создание игры Dice Game (игра в кости) на JavaScript. Вот нашлось время и силы познакомиться с игрой такого типа. Сразу оговорюсь, что материал и пример не мой, а взят из зарубежного ресурса для изучения.

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

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

HTML разметка и стили

Разметка очень простая и состоит из четырех элементов:

<div class="wrapper">
    <div class="column">
        <div id="dice-side-1" class="dice">0</div>
        <div id="dice-side-2" class="dice">0</div>
    </div>
    <div class="column">
        <button type="button" class="dice-roll">roll dice</button>
        <h2 id="status"></h2>
    </div>
</div>

Элементы

1
id="dice-side-1"
и
1
id="dice-side-2"
- это иммитация двух игральных костей; как если бы эти кости были обращены лицевой стороной к зрителю.

Кнопка

1
class="dice-roll"
служит для управления; с помощью нее мы будем “бросать” игральные кости.

Заголовок

1
id="status"
носит информативный характер - в нем будет выводиться текущая информация.

CSS-стили в комментариях не нуждаются, ибо они небольшого размера и “прозрачные”:

body {
    position: relative;
}

.wrapper {
    width: 400px;
    margin: 0 auto;
    display: flex;
    justify-content: space-between;
}

.column {
    display: flex;
    align-items: center;
}

#status {
    position: absolute;
    top: 1rem;
    right: 1rem;
    min-width: 400px;
    margin: 0;
}

.dice {
    width: 32px;
    float: left;
    background-color: lightcoral;
    border: 1px solid black;
    padding: 10px;
    font-size: 24px;
    text-align: center;
    margin: 5px;
    border-radius: 5px;
}

.dice-roll {
    cursor: pointer;
    text-transform: capitalize;
}

JavaScript - оживляем игральные кости

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

Для этого “выберем” из HTML кнопку и “повесим” на нее функцию rollDice для обработки клика по кнопке:

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

    var buttonRollDice = document.querySelector('.dice-roll');
    buttonRollDice.addEventListener('click', rollDice, false);

}, false);

Затем начнем описывать функцию rollDice. Создадим три переменные, в которые поместим обе игральные кости и информативный заголовок:

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

    function rollDice () {

        var diceSide1 = document.getElementById('dice-side-1');
        var diceSide2 = document.getElementById('dice-side-2');
        var status = document.getElementById('status');

    }

}, false);

Сгенерируем два случайных числа из диапазона от 1 до 6. Игральная кость имеет шесть сторон - поэтому такой диапазон. Эти числа будут иммитацией одной из шести сторон каждой игральной кости.

Другими словами. На всех сторонах игрального кубика “выбито” точками число - от 1 до 6. Поэтому можно сказать иначе - диапазон от 1 до 6 - это диапазон возможных значений, которые выпадают на каждой из игральных костей:

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

    function rollDice () {

        var diceSide1 = document.getElementById('dice-side-1');
        var diceSide2 = document.getElementById('dice-side-2');
        var status = document.getElementById('status');

        var side1 = Math.floor( Math.random() * 6 ) + 1;
        var side2 = Math.floor( Math.random() * 6 ) + 1;
        var diceTotal = side1 + side2;

    }

}, false);

Переменная

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

Осталось поместить случайно сгенерированную пару чисел в HTML-код. Помимо этого вывести информационное сообщение, сколько очков в сумме выпало. И предупредить, если на обеих костях выпавшее число одинаковое - тогда предоставить игроку еще один ход:

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

    function rollDice () {

        var diceSide1 = document.getElementById('dice-side-1');
        var diceSide2 = document.getElementById('dice-side-2');
        var status = document.getElementById('status');

        var side1 = Math.floor( Math.random() * 6 ) + 1;
        var side2 = Math.floor( Math.random() * 6 ) + 1;
        var diceTotal = side1 + side2;

        diceSide1.innerHTML = side1;
        diceSide2.innerHTML = side2;

        status.innerHTML = 'You rolled ' + diceTotal + '.';

        if ( side1 === side2 ) {
            status.innerHTML += ' Doubles! You get a free turn!';
        }
    }

}, false);

Наш JavaScript-код готов и выглядит таким несложным способом:

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

    function rollDice () {

        var diceSide1 = document.getElementById('dice-side-1');
        var diceSide2 = document.getElementById('dice-side-2');
        var status = document.getElementById('status');

        var side1 = Math.floor( Math.random() * 6 ) + 1;
        var side2 = Math.floor( Math.random() * 6 ) + 1;
        var diceTotal = side1 + side2;

        diceSide1.innerHTML = side1;
        diceSide2.innerHTML = side2;

        status.innerHTML = 'You rolled ' + diceTotal + '.';

        if ( side1 === side2 ) {
            status.innerHTML += ' Doubles! You get a free turn!';
        }
    }

    var buttonRollDice = document.querySelector('.dice-roll');
    buttonRollDice.addEventListener('click', rollDice, false);

}, false);

Готовый пример Dice Game можно потестить по этой ссылке - JavaScript Dice Game.

Заключение

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

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


На этом все.

Знакомство с техникой 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 имеет в своем составе целую группу событий, с помощью которых можно контролировать процесс перетаскивания:

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

Объект dataTransfer

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

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

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

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

  • 1
    
    setData()
    
    - добавляет данные в нужном формате
  • 1
    
    clearData()
    
    - удаляет данные
  • 1
    
    setDragImage()
    
    - устанавливает изображение для перетаскивания с координатами курсора (0, 0 — левый верхний угол)
  • 1
    
    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>

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

Заголовок

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

Заголовок

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

Блок

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

Объекты

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

Кнопка

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

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

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

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

Итак, начнем с перетаскиваемых элементов

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

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

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

1
dragStart
или
1
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);
    });
}

Обратим внимание на строку

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

На элемент

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

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

Ну и на кнопку

1
id="readDropZone"
мы “повесим” обычный код с функцией
1
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)

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

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

В данном случае мы будем обрабатывать событие

1
dragstart
, которое возникает на перетаскиваемом элементе (
1
id="object1"
,
1
id="object2"
или
1
id="object3"
- не важно).

Событие

1
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'));
}

Функция

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

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

1
dropEffect
-
1
move
.

В третьей строке для объекта dataTransfer с помощью метода

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

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

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

1
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();
}

Первые две функции -

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

Третья функция

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

Функция dragDrop (event)

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

1
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');
}

В строке

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

В строке:

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

… мы из объекта dataTransfer получаем ID перетаскиваемого элемента. Вы же помните, что в функции

1
dragStart (event)
с помощью строки:

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

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

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

var element = document.getElementById(elementID);

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

event.target.appendChild(element);

Далее убираем у перетаскиваемого элемента атрибут

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

element.style.cursor = 'default';

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

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

Отдельного упоминания стоит строка

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

Может случиться так, что объект мы перетащили в область элемента

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

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

1
dragEnd()
:

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

Функция readZone ()

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

1
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'));
    }
}

Нажимаем кнопку

1
dropButton
и alert’ом последовательно выводим все элементы, помещенные внутрь объекта
1
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-дерево документа (событие

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.

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


На этом все.