Прочитай перед тем, как делать анимацию по скроллу
Я интегрировал видео анимацию, которая перематывалась в зависимости от положения скролла, для лендинга детского парка развлечений — wizardia.land.
Я думаю, я попробовал все неправильные способы, как можно это реализовать, и дальше расскажу про свой опыт.
Стек проекта: nuxt 3 (ts) / tailwindcss
Идея нашего руководства состояла в том, чтобы создать «вау» эффект для новых пользователей. Для этого оно обратились к 3д художнику, чтобы он намоделил нам видео с красивой переливающейся сферой посередине и последующим ее взрывом с разлетающимся конфетти и тематическими элементами. После того, как оказалось, что само по себе видео выглядит не так впечатляюще, они решили, что оно не должно воспроизводится сразу, а должно перематываться при скроллинге страницы — и тут все началось.
Содержание — вкратце по тупым ошибкам, которые я совершил\
Делал перемотку напрямую видоса mp4
Проблема с энергосбережением на IOS
Проблема фактической невозможности загрузить видео на некоторых устройствах
Проблема «мелькания» между слайдами при скроллинге
Проблема долгого кеширования кадров
Решил использовать GSAP — ScrollTrigger: проблема с «бликающими» кадра стала меньше
Решил поглубже изучить GSAP и наткнулся на Image Sequence on Scroll
Выводы
Референс, на который я должен был опираться — hang.com
Проблемы перемотки видео в веб разработке
Изначально видео — это довольно громоздкий объект, затрачивающий ресурсы устройства, поэтому стоит использовать его осторожно. В современных плеерах используется HLS streaming (e.x. .m3u8), который работает намного шустрее древнего mp4, позволяет быстро перематывать видео на любой момент, да и в целом, выглядит более стабильно и оптимизировано. Опустим, почему данная технология не была использована в данном проекте, но, как факт, я использовал .mp4 исходник.
Видимо, под давлением сжатых сроков, я не провел ресерч возможных подходов к этому вопросу и пошел на проблему в лобовую — сделал скроллящийся контейнер, пихнул туда видео, написал обработчик скролла и соответственную перемотку видео и получил результат. Мне даже сначала показалось, что все нормально, но, если не вдаваться в подробности, на не самых новых андроидах видео просто не запускалось, на большинстве остальных устройств все жутко лагало, а также дополнительная проблема — на видео был значок плей, если на айфоне включен режим энергосбережения.
Решение проблемы с энергосбережением на IOS
Тривиальные решения проблемы с энергосбережением на мое удивление работали только если поставить аттрибут autoplay у видео (а мое видео, как вы помните, не должно сразу воспроизводиться), но без аттрибута autoplay, опять же, на мое удивление, само видео просто не показывалась на части устройств. Я сначала подумал, что я могу поставить аттрибут и останавливать видео сразу при загрузке страницы, но, конечно, браузер не позволяет взаимодействовать с видео тегом без предварительного действия пользователя (клик, скроллинг — любое действие, которое можно обработать).
Самым простым решением оказалось выгрузить вручную первый кадр видео и показывать его при загрузке страницы, а потом, как только юзер прикоснется к экрану, убирать картинку и показывать видео
Вот целиком компонент vue, в котором целиком содержится анимация вместо с загрузочным экраном.
Загрузка...
Разбитие видео по кадрам
Некоторым образом моя проблема дошла до одного веб разработчика, и он за один вечер сильно освежил мою голову своим, принципиально новым для меня тогда, подходом к проблеме: он разделил видео на кадры и написал простейший скрипт -, а это сразу решает проблему с энергосбережением, и сильно уменьшает вес анимации, при этом сама анимация выглядит плавнее.
Вот этот скрипт:
const frameContainer = document.getElementById("frameContainer");
const totalFrames = 163; // Количество изображений
const isMobile = window.innerWidth <= 576;
const imagePath = (index) => isMobile ? `mobile/frame${index}.jpg` : `frames/frame${index}.jpg`;
const preloadedImages = [];
const preloadCount = 5; // Количество изображений для предзагрузки
let lastScrollY = 0;
let currentFrame = 0;
// Предзагрузка изображений
function preloadImages() {
for (let i = 1; i <= totalFrames; i++) {
const img = new Image();
img.src = imagePath(i);
preloadedImages.push(img);
}
}
preloadImages(); // Предзагрузка изображений
// Задержка обновления (мс)
const updateDelay = 50;
let timeoutId;
function updateFrame() {
const scrollPosition = window.scrollY;
if (lastScrollY !== scrollPosition) {
lastScrollY = scrollPosition;
const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
const scrollFraction = scrollPosition / maxScroll;
// Рассчитываем текущий кадр
const frameIndex = Math.min(totalFrames - 1, Math.floor(scrollFraction * totalFrames));
// Если кадр изменился, устанавливаем новое изображение
if (frameIndex !== currentFrame) {
currentFrame = frameIndex;
// Используем изображение по умолчанию, пока загружается новое
frameContainer.style.backgroundImage = `url(${imagePath(currentFrame + 1)})`;
// Предзагрузка следующих кадров
for (let i = 1; i <= preloadCount; i++) {
const nextFrameIndex = currentFrame + i + 1;
if (nextFrameIndex <= totalFrames) {
const img = new Image();
img.src = imagePath(nextFrameIndex);
}
}
}
}
timeoutId = setTimeout(updateFrame, updateDelay); // Задержка
}
updateFrame(); // Запуск анимации
Тут используется простейшая предзагрузка изображений, чтобы скроллинг не лагал, и просто подмена картинок при скроллинге. Вероятно, код полностью написан чат гпт, но это не важно, потому что именно из-за него я понял, насколько бесполезной ерундой страдал до этого.
Две проблемы, которые содержит в себе этот скрипт: предзагрузка 160 кадров — это довольно долгий процесс, а так же, хоть анимация и стала намного плавнее, при скроллинге иногда (даже очень часто) появлялись пропуски — выглядит, как будто кадры не успевают подгружаться, но кешированием кадров это не решилось (не хочу это подробно описывать, как факт — кеширование не решило проблему)
На самом деле до того, как начать исполбзовать gsap, я еще потерял некоторое время — пытался ограничивать FPS анимации, кешировать кадры, предзагружать их иначе и т.д., и т.п., но это было настолько бессмысленно, что лучше я расскажу, как можно сделать нормально.
Решение использовать GSAP
После того, как я увидел, что подход с кадрами работает намного лучше, чем простая перемотка видео, я решил обратиться к специализированным инструментам и начал гуглить библиотеки веб анимаций, где и нашел gsap— библиотеку, предоставляющую широкий спектор возможностей в отношении анимаций на странице
Интеграция GSAP в Nuxt 3 структуру
Раз я настолько детально все описываю, то тут же расскажу, как быстро начать использовать gsap, если пользуешься nuxt.js (v3)
Скачать пакет
npm:
npm install gsap
yarn:
yarn add gsap
В папке plugins нужно создать файл gsap.client.ts:
// plugins/gsap.client.ts
import gsap from 'gsap';
import ScrollTrigger from 'gsap/ScrollTrigger';
export default defineNuxtPlugin((nuxtApp) => {
if (process.client) {
gsap.registerPlugin(ScrollTrigger);
nuxtApp.provide('gsap', gsap);
}
});
Тут регистрируется ScrollTrigger плагин, который нужен для контроля скролл-анимации
nuxt.config.js: (подключить плагин в конфиге проекта)
plugins: ['~/plugins/gsap.client.ts'],
И потом я мог использовать модуль gsap`а таким образом:
onMounted(async () => {
const nuxtApp = useNuxtApp();
const {$gsap} = nuxtApp;
});
Имплементация анимации через gsap.ScrollTrigger
Я не буду особенно объяснять код, который я сейчас приложу, потому что он делает все то же самое, что и предыдущий, но теперь использует встроенные возможности библиотеки. Предзагружает и кеширует (для этого я положил кадры в папку public) кадры, а далее, используя стролл триггер, контролирует скроллинг.
Анимация вновь стала плавнее и стабильнее, но проблема с «мелькающими» кадрами осталась — она стала реже проявляться, но все же осталась
Компонент vue для скролл-анимации через gsap.ScrollTrigger:
Загрузка... {{ imagesLoaded }} / {{ frameCount }}
Финальное решение
Поняв, что нужно опять найти что-то посвежее, что я еще не пробовал, я решил просмотреть всю документацию GSAP и наткнулся на Image Sequence on Scroll— я полагаю, уже из названия кристаллически понятно, насколько это подходящий для моего кейса инструмент. Я не могу сказать, каким образом я не наткнулся на него в самом начале, но вместо демагогии просто приложу финальный рабочий компонент vue:
Я просто взял код из примера, заново написал свою негромоздкую логику в более приятном формате — для десктопа одни картинки, для мобилки другие, размеры канваса динамически подставляю в размер экрана (не динамически работать не будет), убираю экран загрузки в хуке onStart, а также подставляю проверенные мной раннее параметры для scrollTrigger из предыдущей версии кода
Выводы
Перед тем, как подходить к неизвестной задаче, нужно потратить довольно много времени на тщательный подбор инструментов / стека, просмотреть и проанализировать аналогичные работы, чтобы не тратить впоследствии очень много времени и сил на в корне бессмысленные вещи. Если бы на Хабре была подобная статья на видном месте, я бы сэкономил десятки часов времени, поэтому я решил все это написать.
Да, конечно, в этом проекте я буквально выбирал только плохие стратегии, но думаю, моя статья вполне может быть полезна юному зрителю.