Загрузочный экран в стиле Skyrim

Загрузочный экран в стиле Skyrim

Статьи

Как-то после рабочего зимнего дня стоял я с чашкой чая у окна, смотрел, как в тёплом свете фонарей медленно падают огромные хлопья снега. Смотрел бы я на эту красоту и смотрел бесконечно долго, как вдруг понял — нужно сделать загрузочный экран в стиле игры The Elder Scrolls Skyrim. На вопрос «зачем нам нужен такой загрузочный экран?» думаю нет отвечать смысла, поскольку это стиль.

Подготовительные работы

Для начала работ нужно определиться, что мы в итоге будем технически реализовывать и как. Поскольку чащё всего фронт я делаю на React, поэтому с базой уже определись =).

Относительно самого загрузочного экрана. Если, дорогой читатель, ты ни разу не видел и не знаешь, как это выглядит — вот он:

Конструктивно он состоит из 4 элементов:

  1. блока с игровым прогрессом (прокачка персонажа)
  2. теста, где разработчики оставили нечто, что должно ещё более погружать в игровой процесс
  3. 3d-объекта (статичного), который относительно двух первых пунктов находится на уровень ниже, выполняет медленное линейное движение, а ещё он при зажатой левой кнопке мышки может крутиться туда-сюда
  4. дым, который то появляется, то нет и отчего это зависит, я так и не понял

И сразу определимся, что мы реализовывать не будем:

  • дымовую завесу (хотя в принципе можно заморочится, но для начала давайте без него)
  • настоящий 3d-объект с взаимодействием с мышкой

Причина, почему мы эти штуки не потащим, на самом деле простая — для того, чтобы это получилось правильно, красиво и вообще труЪ, нужно подтягивать библиотеки для того, чтобы все визуальные эффекты реализовать (дым и 3d).

А нам всё ещё очень важно, чтобы для отображения загрузочного экрана не нужно было загружать сам загрузочный экран.

Но общее настроение попытаемся повторить.

Пишем базу

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

И начинаем добавлять новый компонент. Стандартный загрузочный экран в игре работает следующим образом:

  1. начинается загрузка контента и резко появляется тёмный загрузочный экран
  2. как всё загружено, загрузочный экран плавно растворяется

Кроме того, мне подумалось, что неплохо иметь гибкий загрузочный экран, чтобы он был не только во весь экран, а мог бы и при желании занять определённую область (внесём такой дополнительный кастом в конструктив).

В самом простом виде у нас получается вот такой компонент:

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, так и просто чему-либо в тематике фэнтези. И для пасхалок, конечно же.

Вот здесь можно посмотреть живое демо, а тут — посмотреть весь код.

Благодарю за внимание и да хранит вас Талос.

Анатолий Куликов

Анатолий Куликов

Автор блога, веб-разработчик
  • at sign
  • vk logo
Комментариев нет

Добавить комментарий

Для отправки комментария вам необходимо авторизоваться.