DevGang
Авторизоваться

Создание градиентного отображения текста при прокрутке с помощью Tailwind CSS и JS

Для этого урока мы вдохновились красивой моделью ручки, созданной Джеем Томпкинсом, в которой при прокрутке из темноты появляется участок, состоящий из текста. Джей показал, как использовать свойство background-clip и анимацию, управляемую прокруткой, для создания эффекта градиентного раскрытия текста.

Однако на момент написания статьи эта техника работает только в Chrome и Edge, поскольку свойство animation-timeline: scroll() пока не поддерживается в других браузерах. По этой причине - а также для того, чтобы иметь больше возможностей для контроля над анимацией, - мы воссоздали подобный эффект с помощью JavaScript и Tailwind CSS.

Создание HTML

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

С учетом этого давайте определим структуру HTML, используя для стилизации классы Tailwind:

<section class="h-screen flex items-center">
    <div class="fixed w-full top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
        <div class="px-4 md:px-6">
            <div class="max-w-5xl mx-auto">
                <div class="font-extrabold text-3xl md:text-4xl text-white">
                    Life is shaped by the total of our actions, a multifaceted tapestry where each and every detail holds and contributes to our individual journey. The books you've read, the kilometers you've run, the moments of joy and connection during every night out, and even the simple act of rehydrating with a glass of water the morning after all intertwine and meld into the intricate fabric of existence. Everything, without exception, counts, and it is this collective mosaic of experiences that invariably molds and sculpts us into the unique individuals we become.
                </div>
                <div class="mt-8 md:flex items-center justify-between space-y-4 md:space-y-0">
                    <div class="flex items-center space-x-4">
                        <img class="rounded-full" src="./author.jpg" width="36" height="36" alt="Author">
                        <div class="text-slate-600"><a class="text-slate-500 font-medium hover:text-slate-400" href="#0">Beatrice Watson</a> · <span class="text-slate-500 italic">Jun 17</span></div>
                    </div>
                    <div class="flex items-center space-x-8">
                        <div class="flex items-center space-x-1 whitespace-nowrap">
                            <button class="text-slate-300 hover:text-slate-50 p-2" aria-label="Like">
                                <svg class="fill-current" xmlns="http://www.w3.org/2000/svg" width="18" height="17">
                                    <path d="M16.428 1.57a5.363 5.363 0 0 1 0 7.586l-7.43 7.429-.707-.707-6.72-6.722A5.363 5.363 0 0 1 8.999 1.42a5.364 5.364 0 0 1 7.429.15Zm-1.415 6.172a3.363 3.363 0 1 0-5.18-4.237l-.834 1.256-.833-1.256a3.363 3.363 0 1 0-5.18 4.237l6.013 6.014 6.014-6.014Z" />
                                </svg>
                            </button>
                            <div class="text-slate-400 font-medium">4.7K Reactions</div>
                        </div>
                        <div class="flex items-center space-x-1 whitespace-nowrap">
                            <button class="text-slate-300 hover:text-slate-50 p-2" aria-label="Comment">
                                <svg class="fill-current" xmlns="http://www.w3.org/2000/svg" width="18" height="18">
                                    <path d="m16.732 17.108-5.546-2.256A8.607 8.607 0 0 1 9 15.09c-4.952 0-9-3.31-9-7.546C0 3.31 4.048 0 9 0s9 3.31 9 7.57a6.972 6.972 0 0 1-1.842 4.556l.574 4.982Zm-2.674-5.73.368-.345A4.96 4.96 0 0 0 16 7.545C16 4.513 12.926 2 9 2S2 4.513 2 7.545c0 3.033 3.074 5.546 7.02 5.546a6.671 6.671 0 0 0 1.961-.253l.331-.094 3.047 1.24-.301-2.606Z" />
                                </svg>
                            </button>
                            <div class="text-slate-400 font-medium">112 Comments</div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</section>

У нас есть section - в дальнейшем мы будем называть ее "родительским" элементом, - которая занимает всю высоту области просмотра (h-screen). Эта секция содержит div - т.е. "дочерний" элемент, - который располагается в центре экрана.

Сейчас этот текст всегда виден на экране; позже мы рассмотрим, как реализовать эффект раскрытия с помощью JavaScript. Пока же сосредоточимся на замене белого цвета текста на более динамичный радиальный градиент.

Градиент текста с помощью Tailwind CSS

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

Итак, мы будем использовать следующие классы:

  • bg-clip-text и text-transparent для определения цвета текста с фоном.
  • bg-[radial-gradient(50%_50%_at_50%_50%,theme(colors.purple.50),theme(colors.purple.500)_20%,transparent_50%)] задает радиальный градиент, который начинается с "Purple 50", меняется на "Purple 500" при 20% и заканчивается прозрачностью при 50%.
  • bg-[50%_50%] выравнивает градиент по центру.
  • bg-[length:400%_800%] позволяет увеличить масштаб фона.

Таким образом, элемент div будет изменен следующим образом:

<div class="font-extrabold text-3xl md:text-4xl bg-clip-text text-transparent bg-[radial-gradient(50%_50%_at_50%_50%,theme(colors.purple.50),theme(colors.purple.500)_20%,transparent_50%)] bg-[length:400%_800%] bg-[50%_50%]">

Внедрение JavaScript

Теперь самая сложная часть: добавление JavaScript для эффекта раскрытия. Мы создадим класс TextReveal в файле с именем gradient-text-reveal.js, который затем включим в наш HTML.

После инстанцирования JS-класс будет принимать на вход элемент DOM:

class TextReveal {
  constructor(element) {
    this.element = element;
    this.child = this.element.firstElementChild;
    this.setupReveal = this.setupReveal.bind(this);
    this.init();
  }

  setupReveal() {
    // Code to reveal the text
  }

  init() {
    this.setupReveal();
    window.addEventListener('resize', this.setupReveal);
  }
}

// Init TextReveal
const els = document.querySelectorAll('[data-text-reveal]');
els.forEach((el) => {
  new TextReveal(el);
});

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

Теперь, чтобы запустить приведенный выше код, необходимо добавить атрибут data-text-reveal в родительскую секцию:

<section class="h-[200vh] flex items-center" data-text-reveal>

Проверка реализуемости анимации

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

Поэтому нам нужна функция, которая проверяет, выполнены ли условия для выполнения анимации:

class TextReveal {
  constructor(element) {
    this.element = element;
    this.child = this.element.firstElementChild;
    this.canReveal = this.canReveal.bind(this);
    this.getRects = this.getRects.bind(this);
    this.setupReveal = this.setupReveal.bind(this);
    this.init();
  }

  canReveal() {
    const { childRect } = this.getRects();
    // Returns false if the child container is taller than the viewport.
    return childRect.height <= window.innerHeight;
  }

  getRects() {
    return {
      rect: this.element.getBoundingClientRect(),
      childRect: this.child.getBoundingClientRect()
    };
  }  

  setupReveal() {
    if (this.canReveal()) {
      console.log('doable');
    } else {
      console.log('not doable');
    }
  }
  
  init() {
    this.setupReveal();
    window.addEventListener('resize', this.setupReveal);
  }
}

// Init TextReveal
const els = document.querySelectorAll('[data-text-reveal]');
els.forEach((el) => {
  new TextReveal(el);
});

Мы создали два метода:

  • Функция getRects() возвращает координаты родительской секции и фиксированного div относительно области просмотра.
  • Функция canReveal() возвращает true, если высота фиксированного div меньше или равна высоте области просмотра.

Теперь при изменении размеров окна браузера консоль сообщит нам, возможна ли анимация. Если анимация невозможна, то мы изменим позиционирование элемента-контейнера с fixed на relative.

Обновим метод setupReveal:

setupReveal() {
  if (this.canReveal()) {
    this.handleScroll();
    window.addEventListener('scroll', this.handleScroll);
    console.log('doable');
    // Remove the inline styles if previously set
    this.child.style.position = '';
    this.child.style.top = '';
    this.child.style.left = '';
    this.child.style.transform = '';
    this.element.style.height = '';      
  } else {
    window.removeEventListener('scroll', this.handleScroll);
    console.log('not doable');
    // Set some inline styles if the effect isn't doable
    this.child.style.position = 'relative';
    this.child.style.top = '0';
    this.child.style.left = '0';
    this.child.style.transform = 'translate3d(0,0,0)';
    this.element.style.height = 'auto';      
  }
}

Вычисление "значения раскрытия"

Следующий шаг - написание JavaScript-функции, которая в ответ на прокрутку вычисляет "значение раскрытия". Это числовое значение будет использоваться для установки CSS-переменной, определяющей позиционирование градиентного фона.

Создадим этот метод:

calculateRevealValue() {
  const { rect, childRect } = this.getRects(); 

  if (!this.canReveal()) return 1;

  // Calculate the intersection value based on the provided conditions
  if (rect.top <= childRect.top && rect.bottom >= childRect.top) {
    const totalHeightDifference = rect.height - childRect.height;
    const currentHeightDifference = childRect.top - rect.top;
    this.revealValue = currentHeightDifference / totalHeightDifference;
  } else if (rect.bottom < childRect.top || Math.abs(childRect.top - rect.bottom) < 0.01) {
    this.revealValue = 1;
  } else {
    this.revealValue = 0;
  }

  // Clamp the value between 0 and 1
  this.revealValue = Math.max(0, Math.min(1, this.revealValue));

  return this.revealValue;
}

Эта функция оценивает положение как фиксированного элемента, так и его родительского контейнера относительно области просмотра. В результате выполнения ряда условий она возвращает значение revealValue, которое представляет собой число, колеблющееся между 0 и 1.

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

handleScroll() {
  this.revealValue = this.calculateRevealValue();
  console.log(this.revealValue);
}

Сборка всего, что мы сделали на данный момент:

class TextReveal {
  constructor(element) {
    this.element = element;
    this.child = this.element.firstElementChild;
    this.revealValue = 0;
    this.opacityValue = 0;
    this.canReveal = this.canReveal.bind(this);
    this.getRects = this.getRects.bind(this);
    this.calculateRevealValue = this.calculateRevealValue.bind(this);
    this.handleScroll = this.handleScroll.bind(this);
    this.setupReveal = this.setupReveal.bind(this);
    this.init();
  }

  canReveal() {
    const { childRect } = this.getRects();
    // Returns false if the child container is taller than the viewport.
    return childRect.height <= window.innerHeight;
  }

  getRects() {
    return {
      rect: this.element.getBoundingClientRect(),
      childRect: this.child.getBoundingClientRect()
    };
  }

  calculateRevealValue() {
    const { rect, childRect } = this.getRects();

    if (!this.canReveal()) return 1;

    // Calculate the intersection value based on the provided conditions
    if (rect.top <= childRect.top && rect.bottom >= childRect.top) {
      const totalHeightDifference = rect.height - childRect.height;
      const currentHeightDifference = childRect.top - rect.top;
      this.revealValue = currentHeightDifference / totalHeightDifference;
    } else if (rect.bottom < childRect.top || Math.abs(childRect.top - rect.bottom) < 0.01) {
      this.revealValue = 1;
    } else {
      this.revealValue = 0;
    }

    // Clamp the value between 0 and 1
    this.revealValue = Math.max(0, Math.min(1, this.revealValue));

    return this.revealValue;
  }

  handleScroll() {
    this.revealValue = this.calculateRevealValue();
    console.log(this.revealValue);
  }

  setupReveal() {
    if (this.canReveal()) {
      this.handleScroll();
      window.addEventListener('scroll', this.handleScroll);
      console.log('doable');
      // Remove the inline styles if previously set
      this.child.style.position = '';
      this.child.style.top = '';
      this.child.style.left = '';
      this.child.style.transform = '';
      this.element.style.height = '';      
    } else {
      window.removeEventListener('scroll', this.handleScroll);
      console.log('not doable');
      // Set some inline styles if the effect isn't doable
      this.child.style.position = 'relative';
      this.child.style.top = '0';
      this.child.style.left = '0';
      this.child.style.transform = 'translate3d(0,0,0)';
      this.element.style.height = 'auto';      
    }
  }

  init() {
    this.setupReveal();
    window.addEventListener('resize', this.setupReveal);
  }
}

// Init TextReveal
const els = document.querySelectorAll('[data-text-reveal]');
els.forEach((el) => {
  new TextReveal(el);
});

Проверьте это! Если прокрутить браузер, наблюдая за работой консоли, то можно увидеть, как значение revealValue постепенно изменяется от 0 до 1. Оно начинается с 0 при загрузке страницы. По мере взаимодействия фиксированного элемента со своим контейнером значение увеличивается, пока не достигнет максимума - 1, когда дочерний элемент пересекает нижнюю границу родительского элемента.

Создание анимации на основе CSS

Для изменения положения градиентного фона при прокрутке мы зададим CSS-переменную --reveal-value, связанную с revealValue:

handleScroll() {
  this.revealValue = this.calculateRevealValue();
  this.element.style.setProperty('--reveal-value', this.revealValue);
}

setupReveal() {
  if (this.canReveal()) {
    this.handleScroll();
    window.addEventListener('scroll', this.handleScroll);
    // Remove the inline styles if previously set
    this.child.style.position = '';
    this.child.style.top = '';
    this.child.style.left = '';
    this.child.style.transform = '';
    this.element.style.height = '';      
  } else {
    window.removeEventListener('scroll', this.handleScroll);
    this.element.style.setProperty('--reveal-value', 0.5);
    // Set some inline styles if the effect isn't doable
    this.child.style.position = 'relative';
    this.child.style.top = '0';
    this.child.style.left = '0';
    this.child.style.transform = 'translate3d(0,0,0)';
    this.element.style.height = 'auto';      
  }
}

По умолчанию, когда анимация недостижима, значение --reveal-value будет равно 0,5. В противном случае будет использоваться значение revealValue.

Теперь, чтобы анимировать градиент, нам придется подправить наш HTML. Заменим класс, определяющий позицию фона, с bg-[50%_50%] на bg-[50%_calc(100%*var(--reveal-value))]. Таким образом, значение параметра --reveal-value будет управлять размещением фона.

В результате:

  • Если значение --reveal-value равно 0, то background-position становится 50% 0%, сдвигая центр фона вниз.
  • Если параметр --reveal-value равен 1, то background-position составит 50% 100%, смещая центр фона вверх.
  • Если значение --reveal-value равно 0,5, то background-position принимает значение 50% 50%, идеально центрируя фон относительно текстового элемента.

И вуаля! Теперь у вас есть эффект анимированного фона.

И последнее замечание: чтобы сохранить текст в фиксированном положении при более длительной прокрутке, просто удвойте высоту элемента-контейнера, заменив h-screen на h-[200vh].

Анимация элементов нижнего колонтитула

Чтобы сделать общий эффект более интересным, мы также будем анимировать непрозрачность элементов нижнего колонтитула. Для этого введем новую переменную --opacity-value и внесем необходимые изменения в методы setupReveal и handleScroll:

handleScroll() {
  this.revealValue = this.calculateRevealValue();
  this.element.style.setProperty('--reveal-value', this.revealValue);

  if (this.revealValue >= 0.3 && this.revealValue <= 0.7) {
    this.opacityValue = 1;
  } else if (this.revealValue >= 0.2 && this.revealValue < 0.3) {
    this.opacityValue = 10 * (this.revealValue - 0.2);
  } else if (this.revealValue > 0.7 && this.revealValue <= 0.8) {
    this.opacityValue = 1 - 10 * (this.revealValue - 0.7);
  } else {
    this.opacityValue = 0;
  }
  this.element.style.setProperty('--opacity-value', this.opacityValue);
}

setupReveal() {
  if (this.canReveal()) {
    this.handleScroll();
    window.addEventListener('scroll', this.handleScroll);
    // Remove the inline styles if previously set
    this.child.style.position = '';
    this.child.style.top = '';
    this.child.style.left = '';
    this.child.style.transform = '';
    this.element.style.height = '';
  } else {
    window.removeEventListener('scroll', this.handleScroll);
    this.element.style.setProperty('--reveal-value', 0.5);
    this.element.style.setProperty('--opacity-value', 1);
    // Set some inline styles if the effect isn't doable
    this.child.style.position = 'relative';
    this.child.style.top = '0';
    this.child.style.left = '0';
    this.child.style.transform = 'translate3d(0,0,0)';
    this.element.style.height = 'auto';
  }
}

Мы использовали текущую переменную revealValue для определения значения параметра --opacity-value, добавив условия для придания динамичности общему эффекту.

Наконец, обновите HTML, добавив класс opacity-[var(--opacity-value)] в div, содержащий элементы нижнего колонтитула.

На этом мы закончили. Вот окончательный HTML-код:

<section class="h-[200vh] flex items-center" data-text-reveal>
    <div class="fixed w-full top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
        <div class="px-4 md:px-6">
            <div class="max-w-5xl mx-auto">
                <div class="font-extrabold text-3xl md:text-4xl bg-clip-text text-transparent bg-[radial-gradient(50%_50%_at_50%_50%,theme(colors.purple.50),theme(colors.purple.500)_20%,transparent_50%)] bg-[length:400%_800%] bg-[50%_calc(100%*var(--reveal-value))]">
                    Life is shaped by the total of our actions, a multifaceted tapestry where each and every detail holds and contributes to our individual journey. The books you've read, the kilometers you've run, the moments of joy and connection during every night out, and even the simple act of rehydrating with a glass of water the morning after all intertwine and meld into the intricate fabric of existence. Everything, without exception, counts, and it is this collective mosaic of experiences that invariably molds and sculpts us into the unique individuals we become.
                </div>
                <div class="mt-8 md:flex items-center justify-between space-y-4 md:space-y-0 opacity-[var(--opacity-value)]">
                    <div class="flex items-center space-x-4">
                        <img class="rounded-full" src="./author.jpg" width="36" height="36" alt="Author">
                        <div class="text-slate-600"><a class="text-slate-500 font-medium hover:text-slate-400" href="#0">Beatrice Watson</a> · <span class="text-slate-500 italic">Jun 17</span></div>
                    </div>
                    <div class="flex items-center space-x-8">
                        <div class="flex items-center space-x-1 whitespace-nowrap">
                            <button class="text-slate-300 hover:text-slate-50 p-2" aria-label="Like">
                                <svg class="fill-current" xmlns="http://www.w3.org/2000/svg" width="18" height="17">
                                    <path d="M16.428 1.57a5.363 5.363 0 0 1 0 7.586l-7.43 7.429-.707-.707-6.72-6.722A5.363 5.363 0 0 1 8.999 1.42a5.364 5.364 0 0 1 7.429.15Zm-1.415 6.172a3.363 3.363 0 1 0-5.18-4.237l-.834 1.256-.833-1.256a3.363 3.363 0 1 0-5.18 4.237l6.013 6.014 6.014-6.014Z" />
                                </svg>
                            </button>
                            <div class="text-slate-400 font-medium">4.7K Reactions</div>
                        </div>
                        <div class="flex items-center space-x-1 whitespace-nowrap">
                            <button class="text-slate-300 hover:text-slate-50 p-2" aria-label="Comment">
                                <svg class="fill-current" xmlns="http://www.w3.org/2000/svg" width="18" height="18">
                                    <path d="m16.732 17.108-5.546-2.256A8.607 8.607 0 0 1 9 15.09c-4.952 0-9-3.31-9-7.546C0 3.31 4.048 0 9 0s9 3.31 9 7.57a6.972 6.972 0 0 1-1.842 4.556l.574 4.982Zm-2.674-5.73.368-.345A4.96 4.96 0 0 0 16 7.545C16 4.513 12.926 2 9 2S2 4.513 2 7.545c0 3.033 3.074 5.546 7.02 5.546a6.671 6.671 0 0 0 1.961-.253l.331-.094 3.047 1.24-.301-2.606Z" />
                                </svg>
                            </button>
                            <div class="text-slate-400 font-medium">112 Comments</div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</section>

И сопутствующий JavaScript:

class TextReveal {
  constructor(element) {
    this.element = element;
    this.child = this.element.firstElementChild;
    this.revealValue = 0;
    this.opacityValue = 0;
    this.canReveal = this.canReveal.bind(this);
    this.getRects = this.getRects.bind(this);
    this.calculateRevealValue = this.calculateRevealValue.bind(this);
    this.handleScroll = this.handleScroll.bind(this);
    this.setupReveal = this.setupReveal.bind(this);
    this.init();
  }

  canReveal() {
    const { childRect } = this.getRects();
    // Returns false if the child container is taller than the viewport.
    return childRect.height <= window.innerHeight;
  }

  getRects() {
    return {
      rect: this.element.getBoundingClientRect(),
      childRect: this.child.getBoundingClientRect()
    };
  }

  calculateRevealValue() {
    const { rect, childRect } = this.getRects();

    if (!this.canReveal()) return 1;

    // Calculate the intersection value based on the provided conditions
    if (rect.top <= childRect.top && rect.bottom >= childRect.top) {
      const totalHeightDifference = rect.height - childRect.height;
      const currentHeightDifference = childRect.top - rect.top;
      this.revealValue = currentHeightDifference / totalHeightDifference;
    } else if (rect.bottom < childRect.top || Math.abs(childRect.top - rect.bottom) < 0.01) {
      this.revealValue = 1;
    } else {
      this.revealValue = 0;
    }

    // Clamp the value between 0 and 1
    this.revealValue = Math.max(0, Math.min(1, this.revealValue));

    return this.revealValue;
  }

  handleScroll() {
    this.revealValue = this.calculateRevealValue();
    this.element.style.setProperty('--reveal-value', this.revealValue);

    if (this.revealValue >= 0.3 && this.revealValue <= 0.7) {
      this.opacityValue = 1;
    } else if (this.revealValue >= 0.2 && this.revealValue < 0.3) {
      this.opacityValue = 10 * (this.revealValue - 0.2);
    } else if (this.revealValue > 0.7 && this.revealValue <= 0.8) {
      this.opacityValue = 1 - 10 * (this.revealValue - 0.7);
    } else {
      this.opacityValue = 0;
    }
    this.element.style.setProperty('--opacity-value', this.opacityValue);
  }

  setupReveal() {
    if (this.canReveal()) {
      this.handleScroll();
      window.addEventListener('scroll', this.handleScroll);
      // Remove the inline styles if previously set
      this.child.style.position = '';
      this.child.style.top = '';
      this.child.style.left = '';
      this.child.style.transform = '';
      this.element.style.height = '';
    } else {
      window.removeEventListener('scroll', this.handleScroll);
      this.element.style.setProperty('--reveal-value', 0.5);
      this.element.style.setProperty('--opacity-value', 1);
      // Set some inline styles if the effect isn't doable
      this.child.style.position = 'relative';
      this.child.style.top = '0';
      this.child.style.left = '0';
      this.child.style.transform = 'translate3d(0,0,0)';
      this.element.style.height = 'auto';
    }
  }

  init() {
    this.setupReveal();
    window.addEventListener('resize', this.setupReveal);
  }
}

// Init TextReveal
const els = document.querySelectorAll('[data-text-reveal]');
els.forEach((el) => {
  new TextReveal(el);
});

Заключение

Если вы хотите, чтобы ваш сайт выделялся на фоне других, вам необходимо создавать интересные и увлекательные впечатления. Мы надеемся, что вам понравился этот свежий взгляд!

Спасибо за прочтение!

Источник:

#JavaScript #CSS #HTML
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу