Загрузочный экран в стиле Skyrim
СтатьиКак-то после рабочего зимнего дня стоял я с чашкой чая у окна, смотрел, как в тёплом свете фонарей медленно падают огромные хлопья снега. Смотрел бы я на эту красоту и смотрел бесконечно долго, как вдруг понял — нужно сделать загрузочный экран в стиле игры The Elder Scrolls Skyrim. На вопрос «зачем нам нужен такой загрузочный экран?» думаю нет отвечать смысла, поскольку это стиль.
Подготовительные работы
Для начала работ нужно определиться, что мы в итоге будем технически реализовывать и как. Поскольку чащё всего фронт я делаю на React, поэтому с базой уже определись =).
Относительно самого загрузочного экрана. Если, дорогой читатель, ты ни разу не видел и не знаешь, как это выглядит — вот он:
Конструктивно он состоит из 4 элементов:
- блока с игровым прогрессом (прокачка персонажа)
- теста, где разработчики оставили нечто, что должно ещё более погружать в игровой процесс
- 3d-объекта (статичного), который относительно двух первых пунктов находится на уровень ниже, выполняет медленное линейное движение, а ещё он при зажатой левой кнопке мышки может крутиться туда-сюда
- дым, который то появляется, то нет и отчего это зависит, я так и не понял
И сразу определимся, что мы реализовывать не будем:
- дымовую завесу (хотя в принципе можно заморочится, но для начала давайте без него)
- настоящий 3d-объект с взаимодействием с мышкой
Причина, почему мы эти штуки не потащим, на самом деле простая — для того, чтобы это получилось правильно, красиво и вообще труЪ, нужно подтягивать библиотеки для того, чтобы все визуальные эффекты реализовать (дым и 3d).
А нам всё ещё очень важно, чтобы для отображения загрузочного экрана не нужно было загружать сам загрузочный экран.
Но общее настроение попытаемся повторить.
Пишем базу
Представим, что у нас есть некое приложение для загрузки контента. Мы его писать не будем, просто эмулируем общий вид.
И начинаем добавлять новый компонент. Стандартный загрузочный экран в игре работает следующим образом:
- начинается загрузка контента и резко появляется тёмный загрузочный экран
- как всё загружено, загрузочный экран плавно растворяется
Кроме того, мне подумалось, что неплохо иметь гибкий загрузочный экран, чтобы он был не только во весь экран, а мог бы и при желании занять определённую область (внесём такой дополнительный кастом в конструктив).
В самом простом виде у нас получается вот такой компонент:
import React, {useEffect, useMemo} from 'react'
import { createPortal } from 'react-dom';
import './SkyrimLikeLoaderStyles.scss'
export const SkyrimLikeLoader = ({
willUnmount,
canDelete,
fullScreen = true
}) => {
useEffect(() => {
if(!willUnmount) return
setTimeout(() => {
canDelete()
}, 2100)
}, [willUnmount])
const classNames = ['skyrimLikeLoader'];
if(!fullScreen) classNames.push('skl-included')
if(fullScreen) classNames.push('skl-fullscreen')
if(willUnmount) classNames.push('skl-close')
if(fullScreen) return createPortal(
<div className={classNames.join(' ')}></div>,
document.body
)
return <div className={classNames.join(' ')}></div>
}
Код достаточно простой — у нас пока есть 3 параметра, которых нам пока
- willUnmount — bool-параметр, который отвечает за то, что нам нужно будет убрать загрузочный экран; если true, то мы добавляем к корневому элементу ещё один css-класс, в котором реализована анимация «растворения»
- canDelete — функция, которая вызывается через две с хвостиком секунды после того, как отработает анимация «растворения»
- fullScreen — определяем, будет ли наш лоадер занимать весь экран или вписываться в конкретную область, и кроме того, что он определяет стили, применяемые к элементу, так ещё и определяет, каким образом наш лоадер будет добавляться в структуру html-документа
Кроме того, вокруг нашего лоадера нужно организовать небольшую инфрастуктуру, которая нам позволит эмулировать правильную работу лоадера. Что я понимаю под правильной — это значит, что после того, как данные загрузятся, наш экран пропадёт не сразу же, а лишь после того, как анимация «растворения» завершится.
В компоненте, где мы расположим наш лоадер, будет примерно как-то так:
import { SkyrimLikeLoader } from './SkyrimLikeLoader/SkyrimLikeLoader';
const AnotherComponent = () => {
const [loading, setLoading] = useState(false)
const [enableLoader, setEnableLoader] = useState(false)
// Эмуляция загрузки
useEffect(() => {
if(!loading) return
setEnableLoader(true)
setTimeout(() => {
setLoading(false)
}, 4000)
}, [loading])
return <>
{enableLoader && (
<SkyrimLikeLoader
willUnmount={!loading && enableLoader}
canDelete={() => setEnableLoader(false)} />
)}
<div>Приложение</div>
</>
}
Конечно, вариантов того, как можно сделать удаление элемента, после того, как всё загрузилось, можно придумать разных, но мне понравился именно такой вариант — когда начинается загрузка, то просто сразу отображаем элемент, а вот после того, как загрузка будет закончена, мы для начала сообщим это лоадеру и он запустит анимацию, после которой canDelete окончательно удаляет его.
База у нас готова, теперь время добавить текста и картинок.
Добавляем красоты
Для начала сделаем самое простое — это текст. Тут даже большая сложность в написании стилей (т.к. там строчек больше).
А вот теперь нам нужно добавить картинку. Причем не просто картинку, а которая будет ещё плавно двигаться (и там будет несколько направлений движений), а кроме того, нам нужно не забывать, что изображение может занимать как половину, так и весь экран.
import React, {useEffect, useMemo} from 'react'
import { createPortal } from 'react-dom';
import './SkyrimLikeLoaderStyles.scss'
export const SkyrimLikeLoader = ({
willUnmount,
canDelete,
text = '',
image = null,
imageStyle = 'img-half',
fullScreen = true
}) => {
const randomNumber = (max) => {
return Math.floor(Math.random() * (max - 1 + 1) + 1)
}
const animationVersion = useMemo(() => {
return `skl-animation-${randomNumber(4)}`;
}, [])
useEffect(() => {
if(!willUnmount) return
setTimeout(() => {
canDelete()
}, 2100)
}, [willUnmount])
const classNames = ['skyrimLikeLoader'];
if(!fullScreen) classNames.push('skl-included')
if(fullScreen) classNames.push('skl-fullscreen')
if(willUnmount) classNames.push('skl-close')
if(fullScreen) return createPortal(
<div className={classNames.join(' ')}>
{image && <div className={`skyrimLikeLoader_image ${imageStyle} ${animationVersion}`}>{image}</div>}
<p>{text}</p>
</div>,
document.body
)
return <div className={classNames.join(' ')}>
{image && <div className={`skyrimLikeLoader_image ${imageStyle} ${animationVersion}`}>{image}</div>}
<p>{text}</p>
</div>
}
Попутно мы ещё добавили функцию animationVersion, которая нам будет случайным образом генерировать версию анимации, а чтобы при изменении входящих пропсов она резко не менялась — запоминаем её значение. И randomNumber нам ещё чуть позже понадобится.
В качестве изображения мы можем передать в компонент не только какую-то картинку типа img/svg, но и какой-то другой компонент, который может в себе нечто такое содержать. Вдруг однажды появится необходимость таки вставить 3d, то под него уже будет сделана подготовка.
В проекте нашего лоадера осталось сделать только шкалу прогресса и будет всё готово.
Шкала прогресса и последние штрихи
Сразу оговорюсь, что делать шкалу загрузки точь-в-точь не буду, поскольку там прям сложный красивый элемент. По хорошему его можно оттрасировать, сохранить как svg и вставить в лоадер. Мы же сделаем чуть проще — нечто, что будет в целом повторять внешний вид. В конце-концов всегда можно доработать и сделать ещё круче.
Наше определение компонента стало ещё больше:
export const SkyrimLikeLoader = ({
willUnmount,
canDelete,
text = '',
image = null,
imageStyle = 'img-half',
useLoadPercent = false,
percentValue = null,
fullScreen = true
})
Здесь мы можем не только определить, будем ли мы показывать эту шкалу, но также передадим количество процентов.
if(useLoadPercent && !percentValue) {
percentValue = useMemo(() => {
return randomNumber(100)
}, [])
}
if(percentValue && percentValue > 100) percentValue = 100;
Ещё добавим пару обработчиков:
- если мы решили отобразить шкалу прогресса, но забыли / решили не указывать проценты, то сгенерируем просто случайное число
- если количество процентов вдруг стало больше 100, что для шкалы прогресса недопустимо, добавим ограничитель
И конечно же, добавляем саму разметку шкалы:
if(fullScreen) return createPortal(
<div className={classNames.join(' ')}>
{useLoadPercent &&
(<div className={'skyrimLikeLoader_percentLoader'}>
<div className={'skyrimLikeLoader_percentLoader_body'}><div style={loaderStyles}></div></div>
</div>)
}
{image && <div className={`skyrimLikeLoader_image ${imageStyle} ${animationVersion}`}>{image}</div>}
<p>{text}</p>
</div>,
document.body
)
return <div className={classNames.join(' ')}>
{useLoadPercent &&
(<div className={'skyrimLikeLoader_percentLoader'}>
<div className={'skyrimLikeLoader_percentLoader_body'}><div style={loaderStyles}></div></div>
</div>)
}
{image && <div className={`skyrimLikeLoader_image ${imageStyle} ${animationVersion}`}>{image}</div>}
<p>{text}</p>
</div>
Наш лоадер готов — осталось только добавить текст и картинку, после чего наслаждаться его работой.
Такой лоадер классно зайдёт на сайтах, посвящённых как самому TES Skyrim, так и просто чему-либо в тематике фэнтези. И для пасхалок, конечно же.
Вот здесь можно посмотреть живое демо, а тут — посмотреть весь код.
Благодарю за внимание и да хранит вас Талос.