[Перевод] Делаем собственные курсоры для сайтов

Существует множество способов обогащения визуальных впечатлений, которые вызывает у пользователей креативный веб-сайт, множество способов перевода существующего статического дизайна на новый уровень. В частности, речь идёт об оснащении сайтов интерактивным функционалом. А здесь мы исследуем один из способов реализации такого функционала через разработку собственного курсора (указателя мыши) для сайта, представляющего собой независимый элемент пользовательского интерфейса. Это — элемент интерфейса, который будет не только интерактивным и визуально привлекательным, но и полезным с практической точки зрения. Мы уделим особое внимание примерам кода, которые дадут всем желающим возможность расширить те базовые вещи, которые мы рассмотрим, и сделать собственный качественный курсор для сайта.
1*mBoP8lJWpJvsLLGOqdfgGA.gif

Пользователь взаимодействует со страницей сайта 14islands.com с использованием собственного курсора этого сайта

Зачем нужен собственный курсор?


С точки зрения UX применение собственного курсора — это, если не сказать больше, весьма мутная тема. Для начала — такой курсор вторгается на территорию стандартных механизмов поведения браузера и операционной системы. Добавление дополнительного визуального элемента поверх существующего значка курсора или полная замена стандартного значка приводят к изменению стандартного интерфейса сайта, к которому привыкли посетители. В результате появление особого курсора на сайте мгновенно вызывает у всех удивление. Однако если речь идёт о ресурсе, дизайн которого допускает достаточно креативный подход к элементам фронтенда, то устоявшиеся, отчасти строгие, представления о дизайне можно подвергнуть сомнению, ориентируясь на ту пользу, которую что-то новое может принести конкретному проекту.

Собственный курсор может играть несколько ролей. Например, он может быть ярлыком, сообщающим посетителю о том, что указатель мыши находится над неким особым элементом. Или он может представлять собой индикатор прокрутки страницы. То есть — показывать пользователю то, насколько далеко он прокрутил страницу. Ещё одна интересная идея заключается в том, чтобы сделать курсор неотъемлемой частью сайта, сделать его чем-то вроде смеси индикатора загрузки и элемента, участвующего в процессе смены одних страниц другими. Можно даже показывать некий контент, вроде картинок или видеоклипов, следующий за курсором, меняющийся при взаимодействии пользователя с разными частями сайта.

Одно из мест, где можно найти реализацию некоторых из вышеозвученных идей — это данная коллекция проекта Awwwards.

1*7ApMqWlGMMHtcxpf7fPQLQ.gif

Примеры особых курсоров

Интересно… А можно сделать такой курсор самому? И как его сделать?


Есть много подходов к разработке собственного указателя мыши. Выбор конкретного подхода зависит от того, с какими именно технологиями лучше всего знаком разработчик. Можно, в плане рендеринга, положиться на WebGL (именно это решение используется на сайте 14islands.com). Или, в зависимости от ситуации, можно пойти более простым путём, прибегнув к возможностям DOM. А именно — сымитировать курсор с помощью DOM-элемента и оформить его так, как нужно.

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

Структура кода


Начнём с нескольких строк JavaScript-кода. Для начала мы создадим простой ES6-класс и дадим ему имя Cursor. Конструктор этого класса содержит команды по настройке некоторых конфигурационных свойств объекта, касающихся скорости и позиционирования курсора. Тут же вызывается и метод init.
class Cursor {
  constructor() {
    this.target = { x: 0.5, y: 0.5 }; //координаты указателя мыши
    this.cursor = { x: 0.5, y: 0.5 }; //координаты нового курсора
    this.speed = 0.3; //скорость курсора
    this.init();
  }
}

Метод init отвечает за создание собственной области видимости для логики курсора. В этом методе мы решаем несколько задач. Так, мы сразу же вызываем вспомогательный метод для привязки к экземпляру класса двух основных методов, которыми пользуемся — onMouseMove и render. Затем мы подключаем прослушиватель события mousemove, который поставляет нам необходимые данные о перемещениях указателя мыши. И, наконец, мы регистрируем requestAnimationFrame.
init() {
  this.bindAll();
  window.addEventListener("mousemove", this.onMouseMove);
  this.raf = requestAnimationFrame(this.render);
}

Примечание о requestAnimationFrame


Логика нашего курсора основана на цикле requestAnimationFrame (rAF). requestAnimationFrame — это часть API window. Этот метод принимает коллбэк, который вызывается тогда, когда у системы имеется достаточно ресурсов для перерисовки экрана. При таком подходе мы отправляем браузеру запрос, уведомляющий его о том, что мы собираемся произвести анимирование чего-либо. Делается это путём вызова указанного коллбэка до следующей операции перерисовки экрана, до следующего кадра.

Обычно коллбэк requestAnimationFrame должен, после выполнения некоего внутреннего кода (например — обсчёта очередного шага анимации), позаботиться о новом собственном вызове. Это позволит данной функции постоянно вызываться, то есть — будет выполняться последовательный вызов этой функции и будет организован цикл рендеринга анимации. Тут важен ещё один момент, который заключается в том, что rAF возвращает ID, который позволяет остановить этот цикл. То есть — у нас нет необходимости абсолютно всегда выполнять вышеописанный цикл. Например, остановить его можно тогда, когда пользователь не перемещает мышь. Этот ID мы будем хранить в свойстве rAF экземпляра класса Cursor, что позволит нам останавливать цикл в том случае, если мышь неподвижна.

Перемещение мыши


Для того чтобы использовать собственное изображение для курсора, нам надо воспользоваться стандартным событием mousemove и получить позицию курсора на экране. Это позволит нам применить полученные данные при настройке собственного курсора (то есть — элемента
). Поговорим о том, что происходит в onMouseMove.

Если мы будем рассматривать экран в виде области, в которой расположен курсор, мы сможем описать границы координатной системы, в которой происходят перемещения. Пусть координаты верхнего левого угла этого пространства будут {x: 0, y: 0}, а координаты нижнего правого угла — {x: 1, y: 1}.

676970682c46e07fa8edac0765b1cffd.png

Координатная система экрана

Прослушивая событие mousemove, мы получаем сведения о позиции мыши. Сведения о горизонтальной координате мы читаем из свойства e.clientX, а сведения о вертикальной координате — из свойства e.clientY. Эти значения выражены в пикселях, поэтому их, чтобы нормализовать, приведя к диапазону (0, 1), представляющему границы нашей системы координат, нужно поделить на длину соответствующей оси координат (то есть — на ширину или высоту области просмотра содержимого в пикселях). Затем эти два значения сохраняются в виде свойств x и y целевого объектного литерала в классе Cursor. Теперь это — целевые координаты, текущая позиция мыши, приведённая к используемой нами системе координат.

И, наконец, мы проверяем, активен ли вышеупомянутый цикл рендеринга (то есть — узнаём, был ли до этого курсор в неподвижном состоянии) и, если надо, перезапускаем этот цикл, поступая так в том случае, если было зарегистрировано новое событие mousemove, что указывает на необходимость перемещения нашего курсора, следом за указателем мыши, в новое место.

onMouseMove(e) {
  this.target.x = e.clientX / window.innerWidth;
  this.target.y = e.clientY / window.innerHeight;
  if (!this.raf) this.raf = requestAnimationFrame(this.render);
}

Цикл рендеринга


Теперь нам надо применить вычисленные значения к чему-то, что видно на экране. При реализации нашего подхода используются CSS-переменные, поэтому управлять мы можем практически всем, чем угодно. Обсудим это.

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

Задержка перемещения курсора задаётся третьим аргументом функции lerp, который мы изначально, в конструкторе класса, задали как свойство speed. Это свойство принимает значение между 0 и 1. Чем ближе его значение к 0 — тем больше и задержка. А если оно равняется 1 — наш курсор перемещается в новую позицию моментально.

this.cursor.x = lerp(this.cursor.x, this.target.x, this.speed);
this.cursor.y = lerp(this.cursor.y, this.target.y, this.speed);

Далее, мы назначаем интерполированные значения CSS-переменным. Тут мы задаём переменные корневого элемента (тега ), поэтому обратиться к ним можно из любого CSS-селектора, который имеется в дереве DOM.
document.documentElement.style.setProperty("--cursor-x", this.cursor.x);
document.documentElement.style.setProperty("--cursor-y", this.cursor.y);

Обратите внимание на то, что lerp — это наша внутренняя вспомогательная функция следующего вида:
const lerp = (a, b, n) => (1 - n) * a + n * b;

И, наконец, мы проверяем, совпадают ли, с учётом небольшого отклонения, позиции указателя мыши и нашего курсора. Если это так — это значит, что мышь остановилась и никуда не двигается. Поэтому мы останавливаем анимацию вместо того, чтобы её продолжать, и производим жёсткий возврат из метода для того чтобы не допустить ненужных операций рендеринга. Если же мышь продолжает двигаться, то соответствующее условие не выполняется. Это значит, что цикл рендеринга продолжит работать, анимация продолжится, а мы будем ждать следующего кадра.
render() {
  this.cursor.x = lerp(this.cursor.x, this.target.x, this.speed);
  this.cursor.y = lerp(this.cursor.y, this.target.y, this.speed);
  document.documentElement.style.setProperty("--cursor-x", this.cursor.x);
  document.documentElement.style.setProperty("--cursor-y", this.cursor.y);
  const delta = Math.sqrt(
      Math.pow(this.target.x - this.cursor.x, 2) +
      Math.pow(this.target.y - this.cursor.y, 2)
  );
  if (delta < 0.001) {
    cancelAnimationFrame(this.raf);
    this.raf = null;
    return;
  }
    
  this.raf = requestAnimationFrame(this.render);
}

Советы


  • При использовании этого подхода нет абсолютной необходимости в применении CSS-переменных. В его рамках с HTML-элементами можно взаимодействовать напрямую, используя встроенные стили.
  • requestAnimationFrame в методе init используется для того чтобы поместить курсор, по умолчанию, в середину экрана.
  • Значение speed надо настраивать с умом, так как, если оно близко к 0, это может вызвать серьёзные задержки в перемещении курсора.

Итоги


Вышеописанная реализация собственного курсора не ограничена возможностью перемещения обычной точки, как это сделано здесь. Подобная конструкция способна не только порадовать кошку, которая любит сидеть у компьютера. Она ещё и позволяет, например, анимировать фоновый цвет с помощью CSS, настраивать непрозрачность элементов или наборов элементов, сдвигать элементы, «подтягивать» элементы поближе к курсору по мере его перемещения на странице, показывать картинки и видеоклипы при перемещении курсора над определёнными словами. С её помощью можно достичь и многого другого.

То, о чём мы рассказали, имеет смысл воспринимать как начальную точку разработки собственного курсора для очередного креативного веб-проекта.

Делали ли вы когда-нибудь собственные курсоры для веб-сайтов?

oug5kh6sjydt9llengsiebnp40w.png

© Habrahabr.ru