Создание анимации скремблирования текста с помощью JavaScript
Эффект шифрования текста — это классная анимация, которая быстро раскрывает текст путем случайной смены символов — точно так же, как те сцены в фильмах, где хакеры расшифровывают строки текста! Вдохновленные блогом Evervault, мы создадим навигационное меню с таким эффектом, когда вы нажимаете на ссылки. Кроме того, мы предоставим вам как светлую, так и темную версии меню, так что вы сможете интегрировать этот пример в любой из наших шаблонов Tailwind.
Начало работы с HTML
Для начала мы можем определить структуру нашего навигационного меню в HTML. Мы создадим элемент <nav>
, содержащий список ссылок. А для стилизации мы воспользуемся Tailwind CSS:
<nav class="text-sm flex flex-col space-y-2" role="navigation">
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5">General</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5">Work Preferences</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5">Integrations</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5">Billing</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5">Subscription</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5">Security</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5">Data Sources</a>
</nav>
Создание JS-класса для анимации
Мы создадим класс JavaScript, как мы это делали в наших руководствах. Таким образом, мы можем легко применить анимацию к различным элементам страницы. Итак, создайте файл text-scramble.js
и включите его в HTML-документ непосредственно перед закрывающим тегом </body>
:
<script src="./text-scramble.js"></script>
Теперь в файле JS мы создадим класс TextScramble
. Внутри этого класса будет метод init
, который инициализирует анимацию:
class TextScramble {
constructor(element) {
this.element = element;
this.links = Array.from(this.element.querySelectorAll('a'));
this.init();
}
init = () => {
this.links.forEach(link => {
link.addEventListener('click', (event) => {
event.preventDefault();
// Run the scramble animation
});
});
}
}
// Init TextScramble
const scrambleElements = document.querySelectorAll('[data-text-scramble]');
scrambleElements.forEach((element) => {
new TextScramble(element);
});
Чтобы использовать класс TextScramble
, просто добавьте атрибут data-text-scramble
в меню навигации:
<nav class="text-sm flex flex-col space-y-2" role="navigation" data-text-scramble>
Затем класс выбирает все ссылки внутри <nav>
и запускает анимацию при нажатии каждой ссылки.
Управление активной ссылкой
Теперь в нашем HTML отметьте первую ссылку атрибутом aria-current="page"
. Этот атрибут сообщает браузеру, какая ссылка в данный момент «активна», и позволяет применять определенные стили, чтобы отличать ее от других ссылок.
<nav class="text-sm flex flex-col space-y-2" role="navigation">
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5 aria-[current]:text-indigo-500 before:absolute before:top-0 before:-left-5 before:h-full before:w-0.5 aria-[current]:before:bg-indigo-400" aria-current="page">General</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5 aria-[current]:text-indigo-500 before:absolute before:top-0 before:-left-5 before:h-full before:w-0.5 aria-[current]:before:bg-indigo-400">Work Preferences</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5 aria-[current]:text-indigo-500 before:absolute before:top-0 before:-left-5 before:h-full before:w-0.5 aria-[current]:before:bg-indigo-400">Integrations</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5 aria-[current]:text-indigo-500 before:absolute before:top-0 before:-left-5 before:h-full before:w-0.5 aria-[current]:before:bg-indigo-400">Billing</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5 aria-[current]:text-indigo-500 before:absolute before:top-0 before:-left-5 before:h-full before:w-0.5 aria-[current]:before:bg-indigo-400">Subscription</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5 aria-[current]:text-indigo-500 before:absolute before:top-0 before:-left-5 before:h-full before:w-0.5 aria-[current]:before:bg-indigo-400">Security</a>
<a href="#0" class="relative font-medium text-slate-500 hover:text-slate-600 py-0.5 aria-[current]:text-indigo-500 before:absolute before:top-0 before:-left-5 before:h-full before:w-0.5 aria-[current]:before:bg-indigo-400">Data Sources</a>
</nav>
Чтобы сделать активную ссылку более заметной, мы использовали префикс aria-[current]:
, чтобы придать ей другой цвет (aria-[current]:text-indigo-500
). Мы также добавили вертикальную линию слева от каждой ссылки, используя псевдоэлемент: :before
. По умолчанию эта строка прозрачна, но она становится синего цвета, когда ссылка активна (aria-[current]:before:bg-indigo-400
).
Прекрасно! Теперь давайте позаботимся об активной ссылке во время навигации.
class TextScramble {
constructor(element) {
this.element = element;
this.links = Array.from(this.element.querySelectorAll('a'));
this.activeLink = this.links.find(link => link.getAttribute('aria-current') === 'page');
this.init();
}
handleActiveLink = (link) => {
if (this.activeLink) {
this.activeLink.removeAttribute('aria-current');
}
this.activeLink = link;
this.activeLink.setAttribute('aria-current', 'page');
}
init = () => {
this.links.forEach(link => {
link.addEventListener('click', (event) => {
event.preventDefault();
if (link === this.activeLink) return;
this.handleActiveLink(link);
});
});
}
}
// Init TextScramble
const scrambleElements = document.querySelectorAll('[data-text-scramble]');
scrambleElements.forEach((element) => {
new TextScramble(element);
});
Ключевые шаги, которые мы предприняли, заключаются в следующем:
- Мы добавили в класс свойство
activeLink
, которое находит активную ссылку в массиве ссылок.
- Затем мы создали метод
handleActiveLink
, задачей которого является обновление активной ссылки при каждом нажатии ссылки меню. Он удаляет атрибутaria-current
из активной ссылки, устанавливает новую активную ссылку и присваивает ей атрибутaria-current="page"
.
- Наконец, мы добавили условие внутри метода init, чтобы проверить, активна ли уже выбранная ссылка —
if (link === this.activeLink)
. Если это так, выполнение методаinit
останавливается с возвратом. В противном случае мы вызываем методhandleActiveLink
для обновления активной ссылки.
Обработка эффекта скремблирования
Теперь начинается более сложная часть. Мы представим пару новых свойств и методов для обработки эффекта перемешивания.
class TextScramble {
constructor(element) {
this.element = element;
this.links = Array.from(this.element.querySelectorAll('a'));
this.activeLink = this.links.find(link => link.getAttribute('aria-current') === 'page');
this.isScrambling = false;
this.intervalId = null;
this.init();
}
handleActiveLink = (link) => {
if (this.activeLink) {
this.activeLink.removeAttribute('aria-current');
}
this.activeLink = link;
this.activeLink.setAttribute('aria-current', 'page');
}
startScramble = (link) => {
if (this.isScrambling) {
this.stopScramble(this.activeLink);
}
this.isScrambling = true;
this.intervalId = setInterval(() => {
// Do stuff...
console.log('Scrambling...');
}, 50);
}
stopScramble = (link) => {
this.isScrambling = false;
clearInterval(this.intervalId);
}
init = () => {
this.links.forEach(link => {
link.addEventListener('click', (event) => {
event.preventDefault();
if (link === this.activeLink) return;
this.startScramble(link);
this.handleActiveLink(link);
});
});
}
}
// Init TextScramble
const scrambleElements = document.querySelectorAll('[data-text-scramble]');
scrambleElements.forEach((element) => {
new TextScramble(element);
});
Начнем с недавно добавленных свойств:
- Логическое свойство
isScrambling
указывает, выполняется ли в данный момент анимация. По умолчанию для него установлено значениеfalse
, и оно меняется наtrue
при запуске анимации. Это свойство полезно для остановки текущей анимации при нажатии другой ссылки.
- Переменная
intervalId
используется для хранения идентификатора интервала, используемого для анимации. Сохранив идентификатор интервала, мы можем позже очистить интервал, используяclearInterval(this.intervalId)
, когда анимация будет завершена или прервана.
Теперь новые методы:
- Метод
startScramble
отвечает за инициацию анимации. Он вызывается при нажатии ссылки меню. Этот метод сначала проверяет, выполняется ли анимация, и при необходимости останавливает ее. Затем он устанавливает для свойстваisScrambling
значение true и запускает временной интервал, который выполняет функцию каждые 50 миллисекунд — в настоящее время эта функция пуста, но мы вскоре ее заполним.
- Метод
stopScramble
используется для остановки анимации. Он вызывается, когда анимация завершается или прерывается. Этот метод устанавливает для свойстваisScrambling
значениеfalse
и останавливает временной интервал с помощью методаcleInterval(this.intervalId)
.
Обработка префикса и суффикса
Сначала мы создадим свойство под названием this.input;
он будет хранить текст ссылки, которая будет анимирована.
Далее мы добавим еще два свойства: this.prefix
и this.suffix
. Эти свойства будут использоваться для замены текста ссылки во время анимации.
Давайте рассмотрим практический пример, чтобы понять, как это работает. Предположим, вы нажимаете на ссылку с текстом clearInterval(this.intervalId)
.
Анимация пройдет следующие итерации:
«»(this.prefix) + «spfjert» (this.suffix)
«G» (this.prefix) + «pfjfqr» (this.suffix)
«Ge» (this.prefix) + «jdqll» (this.suffix)
«Gen» (this.prefix) + «rmnb» (this.suffix)
«Gene» (this.prefix) + «swt» (this.suffix)
«Gener» (this.prefix) + «oe» (this.suffix)
«Genera» (this.prefix) + «z» (this.suffix)
«General» (this.prefix) + «» (this.suffix)
Вначале префикс представляет собой пустую строку, а суффикс состоит из случайных символов. На каждой итерации префикс будет таким же, как первая буква this.input
, а суффикс будет содержать все последующие символы. Этот шаблон продолжается до тех пор, пока префикс не будет соответствовать this.input
, а суффикс не станет пустой строкой.
Теперь давайте посмотрим, как мы можем интегрировать эту логику в наш класс.
class TextScramble {
constructor(element) {
this.element = element;
this.links = Array.from(this.element.querySelectorAll('a'));
this.activeLink = this.links.find(link => link.getAttribute('aria-current') === 'page');
this.input = null;
this.prefix = '';
this.suffix = '';
this.isScrambling = false;
this.intervalId = null;
this.init();
}
handleActiveLink = (link) => {
if (this.activeLink) {
this.activeLink.removeAttribute('aria-current');
}
this.activeLink = link;
this.activeLink.setAttribute('aria-current', 'page');
}
startScramble = (link) => {
if (this.isScrambling) {
this.stopScramble(this.activeLink);
}
this.isScrambling = true;
this.input = link.textContent;
this.prefix = '';
this.suffix = 'xxx';
this.intervalId = setInterval(() => this.scrambleIteration(link), 50);
}
scrambleIteration = (link) => {
let nextChar = this.input.charAt(this.prefix.length);
if (nextChar === '') {
this.stopScramble(link);
} else {
this.prefix += nextChar;
this.suffix = 'xxx';
link.textContent = this.prefix;
link.setAttribute('data-scramble-suffix', this.suffix);
}
}
stopScramble = (link) => {
link.textContent = this.input;
link.setAttribute('data-scramble-suffix', '');
this.isScrambling = false;
clearInterval(this.intervalId);
}
init = () => {
this.links.forEach(link => {
link.addEventListener('click', (event) => {
event.preventDefault();
if (link === this.activeLink) return;
this.startScramble(link);
this.handleActiveLink(link);
});
});
}
}
// Init TextScramble
const scrambleElements = document.querySelectorAll('[data-text-scramble]');
scrambleElements.forEach((element) => {
new TextScramble(element);
});
Помимо добавления this.input
, this.prefix
и this.suffix
, мы также интегрировали методы startScramble
и stopScramble
для обновления значений this.prefix
и this.suffix
на каждой итерации.
Более того, во время скремблирования метод scrambleIteration
добавляет к ссылке атрибут data-scramble-suffix
, который содержит значение свойства this.suffix
. Этот атрибут будет использоваться для отображения суффикса с использованием псевдоэлемента ::after
ссылки. В Tailwind все, что вам нужно сделать, это добавить класс after:content-[attr(data-scramble-suffix)]
ко всем ссылкам меню.
Генерация случайных символов
Последний шаг — сгенерировать случайные символы соответствующей длины для суффикса. Для этого мы создадим метод под названием randomChars
, возвращающий строку случайных букв:
randomChars = (length) => {
let result = '';
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * this.charsetLength);
result += `${this.charset[randomIndex]}`;
}
return result;
}
Теперь нам просто нужно использовать этот метод в методах startScramble
и scrambleIteration
. Итак, окончательный код класса будет выглядеть так:
class TextScramble {
constructor(element) {
this.element = element;
this.links = Array.from(this.element.querySelectorAll('a'));
this.activeLink = this.links.find(link => link.getAttribute('aria-current') === 'page');
this.input = null;
this.prefix = '';
this.suffix = '';
this.isScrambling = false;
this.intervalId = null;
this.charset = 'abcdefghijklmnopqrstuvwxyz';
this.charsetLength = this.charset.length;
this.scrambleIteration = this.scrambleIteration.bind(this);
this.init();
}
handleActiveLink = (link) => {
if (this.activeLink) {
this.activeLink.removeAttribute('aria-current');
}
this.activeLink = link;
this.activeLink.setAttribute('aria-current', 'page');
}
startScramble = (link) => {
if (this.isScrambling) {
this.stopScramble(this.activeLink);
}
this.isScrambling = true;
this.input = link.textContent;
this.prefix = '';
this.suffix = this.randomChars(this.input.length);
this.intervalId = setInterval(() => this.scrambleIteration(link), 50);
}
scrambleIteration = (link) => {
let nextChar = this.input.charAt(this.prefix.length);
if (nextChar === '') {
this.stopScramble(link);
} else {
this.prefix += nextChar;
this.suffix = this.randomChars(this.input.length - this.prefix.length);
link.textContent = this.prefix;
link.setAttribute('data-scramble-suffix', this.suffix);
}
}
stopScramble = (link) => {
link.textContent = this.input;
link.setAttribute('data-scramble-suffix', '');
this.isScrambling = false;
clearInterval(this.intervalId);
}
randomChars = (length) => {
let result = '';
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * this.charsetLength);
result += `${this.charset[randomIndex]}`;
}
return result;
}
init = () => {
this.links.forEach(link => {
link.addEventListener('click', (event) => {
event.preventDefault();
if (link === this.activeLink) return;
this.startScramble(link);
this.handleActiveLink(link);
});
});
}
}
// Init TextScramble
const scrambleElements = document.querySelectorAll('[data-text-scramble]');
scrambleElements.forEach((element) => {
new TextScramble(element);
});
https://cruip-tutorials.vercel.app/text-scramble/
Заключение
Создание привлекательных эффектов и анимации, которые не ставят под угрозу удобство использования веб-сайта, может оказаться сложной задачей. В этом уроке мы постарались добавить немного веселья во взаимодействие с меню, не ухудшив при этом общее впечатление. Достигли ли мы своей цели? Свяжитесь с нами и дайте нам знать, что вы думаете!