QML

Параллакс-эффекты и многослойная анимация фона

Введение

Параллакс-эффект — визуальная техника, при которой разные слои фона движутся с разной скоростью относительно друг друга при скролле или другом взаимодействии, создавая иллюзию глубины и трёхмерности на плоском экране. В статье разберём реализацию многослойного параллакс-эффекта для фонового изображения экрана и для прокручиваемого списка.

Концепция

Базовая идея параллакса — несколько визуальных слоёв (обычно изображений), расположенных друг над другом, каждый со своим собственным коэффициентом скорости движения относительно общего управляющего параметра (позиция скролла, наклон устройства, позиция курсора) — слои, представляющие «далёкие» объекты, движутся медленнее (создавая иллюзию большого расстояния), а «близкие» — быстрее, что соответствует тому, как воспринимается реальная глубина при движении наблюдателя в физическом мире.

Пример кода: Параллакс-эффекты и многослойная

// ParallaxBackground.qml — многослойный фон с разной скоростью движения слоёв
import QtQuick

Item {
    id: root
    property real scrollOffset: 0 // управляющий параметр — например, позиция скролла основного контента

    // Самый дальний слой — движется медленнее всего (коэффициент 0.2)
    Image {
        source: "qrc:/backgrounds/sky.png"
        width: parent.width * 1.5
        x: -root.scrollOffset * 0.2
        fillMode: Image.PreserveAspectCrop
        anchors.verticalCenter: parent.verticalCenter
    }

    // Средний слой — облака, движутся быстрее неба, но медленнее переднего плана
    Image {
        source: "qrc:/backgrounds/clouds.png"
        width: parent.width * 1.8
        x: -root.scrollOffset * 0.5
        fillMode: Image.PreserveAspectCrop
        anchors.verticalCenter: parent.verticalCenter
    }

    // Ближний слой — горы/деревья на переднем плане, движутся быстрее всего
    Image {
        source: "qrc:/backgrounds/foreground.png"
        width: parent.width * 2.2
        x: -root.scrollOffset * 0.9
        fillMode: Image.PreserveAspectCrop
        anchors.bottom: parent.bottom
    }
}
// Привязка параллакса к реальной прокрутке списка контента
import QtQuick

Item {
    ParallaxBackground {
        id: background
        anchors.fill: parent
        scrollOffset: contentListView.contentX // синхронизация с горизонтальной прокруткой списка
    }

    ListView {
        id: contentListView
        anchors.fill: parent
        orientation: ListView.Horizontal
        model: contentModel
        delegate: ContentCard { }
    }
}
// Параллакс, управляемый наклоном устройства через QtSensors (для мобильных платформ)
import QtQuick
import QtSensors

Item {
    Accelerometer {
        id: accelerometer
        active: true
        onReadingChanged: {
            // Наклон устройства транслируется в смещение параллакс-фона
            background.scrollOffset = -reading.x * 50;
        }
    }

    ParallaxBackground {
        id: background
        anchors.fill: parent
    }
}

Пояснения к коду

ParallaxBackground определяет три слоя изображений с разными коэффициентами умножения на общий управляющий параметр scrollOffset (0.2 для самого дальнего слоя, 0.9 для самого близкого) — поскольку все три слоя получают одно и то же значение scrollOffset, но умножают его на разные коэффициенты, их визуальное смещение оказывается пропорционально разным, что и создаёт ощущение глубины. Каждый слой шире самого видимого экрана (width: parent.width * 1.5 и больше) — это необходимый запас, чтобы при смещении слоя в любую сторону на экране не появлялись пустые области без изображения по краям.

Второй пример показывает практическое применение — привязка scrollOffset к реальной позиции прокрутки ListView (contentX) синхронизирует параллакс-эффект с тем, как пользователь физически взаимодействует со списком контента, создавая ощущение, что фон является частью одной согласованной «глубокой» сцены, а не отдельным, не связанным с прокруткой декоративным элементом. Третий пример показывает альтернативный источник управляющего параметра — акселерометр устройства через модуль QtSensors, создающий параллакс-эффект, реагирующий на физический наклон устройства, типичный для некоторых мобильных игр и интерактивных заставок.

Подводные камни

  • Недостаточная ширина слоёв фона относительно максимально возможного диапазона смещения приводит к появлению пустых, незаполненных областей по краям экрана при достижении предельных значений scrollOffset — нужный запас ширины каждого слоя зависит от максимального ожидаемого диапазона значений управляющего параметра и соответствующего коэффициента слоя, и это нужно явно рассчитывать (или щедро завышать с запасом) для каждого конкретного сценария использования, а не выбирать произвольные значения ширины.
  • Параллакс-эффект, синхронизированный с прокруткой основного контента, может конфликтовать с инерционной прокруткой (flick) ListView, если обновление позиции фона выполняется недостаточно часто или с заметной задержкой относительно реальной позиции списка — для плавного, без визуального «отставания» параллакса важно, чтобы привязка scrollOffset к contentX была прямой, реактивной привязкой (как в примере), а не опосредованной через промежуточные таймеры или асинхронные обновления, вносящие дополнительную задержку.
  • Несколько крупных, полупрозрачных или сложных изображений, рендерящихся одновременно как слои параллакса, особенно при больших размерах (с запасом по ширине) создают значительную нагрузку на память и рендеринг — для embedded или менее мощных мобильных устройств (основной цикл статей про embedded) стоит явно ограничивать количество и разрешение слоёв параллакса, возможно, заменяя часть слоёв на более простые, программно генерируемые градиенты вместо полноценных растровых изображений.
  • Параллакс-эффект, управляемый акселерометром, без фильтрации «шума» необработанных показаний датчика может создавать дрожащее, нервное визуальное движение фона вместо плавного, приятного эффекта — необработанные показания акселерометра обычно содержат высокочастотный шум от вибрации устройства и движения руки пользователя, и для приятного визуального результата практически всегда нужна программная фильтрация (простое экспоненциальное усреднение или более сложный фильтр) перед использованием значения датчика напрямую для управления визуальным эффектом.