Ниже 1200 пикселей Тильда снимает анимацию сама: четыре выключателя
Порог один во всех четырёх файлах анимации — 1200 пикселей, — а выключателей за ним четыре, и работают они по-разному: один снимает класс `t-animate`, второй возвращает `null` вместо настроек, третий глушит переходы, четвёртый показывает элемент сразу. Замерено: у элемента Zero Block анимация ниже порога включается атрибутом, у обычного блока — никогда, а одна группа в записи гасит анимацию всей записи вместе с соседями.
Симптом описывают почти всегда одинаково. На компьютере всё появляется как задумано; открываешь тот же адрес с телефона — блоки просто стоят на месте. Ничего не мигает, в консоли пусто, разметка та же. Так и должно быть: ниже 1200 пикселей рантайм Тильды выключает анимацию сам, и делает это в четырёх местах независимо друг от друга.
Короткий ответ
Порог — 1200 пикселей ширины окна. Ниже него Тильда снимает у элементов класс
t-animate, а покадровой анимации не строит вовсе. Отменяется это единственным
атрибутом — data-animate-mobile="y" на обёртке элемента Zero Block. У обычного
блока такой обёртки нет, поэтому там анимация ниже порога выключена
безусловно.
Один порог, четыре выключателя
Файлов анимации в рантайме четыре, и в каждом порог свой, отдельно написанный. Общее у них только число.
| файл | функция | что делает ниже 1200 |
|---|---|---|
tilda-animation-2.0 | t_animate__checkMobile | снимает класс t-animate |
tilda-animation-2.0 | t_animate__getAttrByResBase | возвращает null вместо любой настройки, ставит transition: none |
tilda-animation-ext | t_animationExt__getAttrByRes | то же для параллакса и «прилипания» |
tilda-animation-sbs | t_animationSBS__getAnimOptions | не строит шаги и показывает элемент сразу |
Разница между первым и вторым важна на практике. Первый убирает саму анимацию.
Второй оставляет её включённой, но обесточивает: style, distance, scale,
delay, duration, trigger-offset — все шесть настроек приходят пустыми,
и переходы вдобавок глушатся прямо в inline-стиле.
Что именно снимается
Точка сбора — t_animate__startAnimation. Она набирает три списка:
var t = document.querySelectorAll('.r'),
e = Array.prototype.filter.call(t, function (t) {
return t.querySelector('.t-animate[data-animate-group=yes]');
}),
a = Array.prototype.filter.call(t, function (t) {
return t.querySelector('.t-animate[data-animate-chain="yes"]');
}),
n = document.querySelectorAll('.t-animate');
n — все анимированные элементы страницы, без разбора блоков: селектор
.t-animate берётся от document. e и a — не элементы, а записи (.r,
обёртка блока Тильды), внутри которых нашлась групповая или цепочечная анимация.
Это различие и даёт самое неожиданное следствие, до него дойдём.
Дальше — сам порог, одной строкой:
window.innerWidth < 1200 &&
(e = t_animate__checkMobile(e), a = t_animate__checkMobile(a), n = t_animate__checkMobile(n));
И фильтр, который решает судьбу каждого:
function t_animate__checkMobile(t) {
return t.filter(function (t) {
var e = t.closest('.t396__elem, .t396__group');
return (
!(!e || 'y' !== e.getAttribute('data-animate-mobile')) ||
(t.classList.contains('r')
? Array.prototype.forEach.call(t.querySelectorAll('.t-animate'), function (t) {
t_animate__removeAnimationClass(t, '');
})
: t_animate__removeAnimationClass(t, ''),
false)
);
});
}
Читается так: элемент остаётся в списке, только если у него есть предок
.t396__elem или .t396__group и у этого предка data-animate-mobile равен
y. Иначе вызывается t_animate__removeAnimationClass, а он делает ровно одно
— classList.remove('t-animate').
Отсюда первый твёрдый вывод. Предки .t396__elem и .t396__group существуют
только внутри Zero Block. У элемента обычного блока — T142, T004, любого —
такого предка нет, closest возвращает null, первое условие ложно, класс
снимается. Никакая настройка это не меняет, потому что атрибут читается не с
элемента, а с обёртки, которой в обычном блоке не бывает.
Замер: восемь видов разметки
Взят настоящий tilda-animation-2.0.min.js из рантайма, без правок, и восемь
вариантов разметки. Вызов t_animate__init(), пауза 1800 мс (сама Тильда
стартует анимацию через 1500), затем проверка класса.
| разметка | при 1280 | при 1000 |
|---|---|---|
| обычный блок, одиночный элемент | есть | снят |
| обычный блок, группа | есть | снят |
| обычный блок, цепочка | есть | снят |
Zero Block без data-animate-mobile | есть | снят |
Zero Block с data-animate-mobile="y" | есть | есть |
Zero Block, группа с data-animate-mobile="y" | есть | снят |
| та же запись: группа с атрибутом | есть | снят |
| та же запись: одиночный с атрибутом | есть | снят |
Пятая строка — единственная, где анимация ниже порога выживает. Всё остальное гаснет.
Границу проверили отдельно, по пикселю: при ширине 1198 и 1199 класса нет и
<span class="t-animate__number"> не создан, при 1200 и 1201 есть и то, и
другое. Сравнения в коде строгие — < 1200 для класса и >= 1200 для чисел, —
так что ровно на 1200 работает всё.
Правило, которого в коде не видно
Шестая, седьмая и восьмая строки таблицы — про одно и то же, и это самая дорогая находка замера.
Группа в Zero Block с выставленным атрибутом всё равно гаснет. Причина в том, что
в список e попала не сама группа, а запись, в которой она лежит. У записи
.r предка .t396__elem нет по определению — она сама верхний уровень блока.
Значит первое условие фильтра ложно, а поскольку у неё есть класс r, работает
не одиночное снятие, а обход: t.querySelectorAll('.t-animate') и снятие класса
у каждого найденного элемента внутри записи.
Последняя строка таблицы это подтверждает прямо. В одной записи стоят два
элемента: группа с атрибутом и одиночный элемент, у которого атрибут выставлен на
своём собственном .t396__elem. Тот же самый одиночный элемент в отдельной
записи ниже порога выживает (строка пять). Здесь — гибнет, потому что запись
обошли целиком.
Практический вид этого правила: одна групповая или цепочечная анимация в записи выключает на мобильных анимацию всей записи, вместе с соседями, у которых всё выставлено верно. Лечится только разнесением по разным блокам — тем же приёмом, которым разносят два мода на одной странице, и по той же причине: Тильда считает не по элементам, а по записям.
Числа: свой порог, своя причина
У счётчика чисел порог отдельный и написан в другой функции — t_animate__init:
document.documentElement.clientWidth >= 1200 && t_animate__parseNumberText();
t_animate__parseNumberText — единственное место во всём файле, где создаётся
<span class="t-animate__number">. Не вызвали её — span не появился, а таймер
t_animate__animateNumbers пишет результат так:
f && (f.textContent = h);
f — тот самый span, найденный через querySelector. Его нет, значит f равен
null, условие ложно, и таймер честно отсчитывает свои 108 шагов по 12 мс в
никуда. Число на странице просто стоит.
Обратите внимание на две разные мерки в одном файле: класс снимается по
window.innerWidth, а числа разбираются по document.documentElement.clientWidth.
Первое включает полосу прокрутки, второе — нет, так что при полосе шириной 15
пикселей пороги расходятся. В нашем замере они совпали ровно: у Chromium в
headless полоса накладная и ширины не занимает, innerWidth и clientWidth
равны на всех семи проверенных ширинах. То есть расхождение существует только
там, где полоса отнимает место у страницы, и его величина — это её ширина.
Скорость счётчика зашита теми же 108 шагами по 12 мс, то есть 1296 мс, и не настраивается. У мода «Анимация чисел» длительность — параметр, а порога ширины в нём нет ни одного; ниже 1200 он считает так же, как выше.
Ещё два порога, о которых не предупреждают
Ниже 1200 выключается не только появление.
Мышиный параллакс (t_animateParallax__moveEl в tilda-animation-ext) не
инициализируется совсем, если выполняются оба условия — 'ontouchend' in document и clientWidth < 1200. Тело функции целиком лежит внутри проверки, так
что на планшете в портретной ориентации мышиного параллакса нет по устройству, а
не из-за отсутствия мыши.
Порог 980 — отдельный, и он не про выключение, а про побочный эффект.
t_animate__preventHorizontalScroll собирает элементы с
data-animate-style="fadeinleft", у которых правый край уже выходит за
window.innerWidth, и вешает их записи класс t-animate__overflow-x-hidden —
то есть чинит горизонтальную полосу прокрутки, которую сам же эффект и создаёт.
Ниже 980 не вешает: !e.length || window.innerWidth < 980 || e.forEach(…).
Полоса остаётся — та же, о которой речь в разборе горизонтального
скролла.
В этом же отборе стоит исключение, которое стоит знать отдельно от порога:
return !t.closest('.t396__artboard, .t-cover') && t.getBoundingClientRect().right > window.innerWidth;
Элементы внутри артборда Zero Block и внутри обложки в отбор не попадают вообще — значит защиты от горизонтальной полосы у них нет ни на какой ширине, а не только ниже 980.
Там же ниже 980 работает t_animate__removeInlineAnimStyles: на каждое изменение
размера окна он сбрасывает inline transform, transitionDuration и
transitionDelay у .t396__elem.t-animate:not(.t-animate_wait).
«Мобильный» — три разных определения
Слово одно, проверок три, и они не совпадают:
// tilda-animation-ext
window.t_animationExt__isMobile =
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
'ontouchend' in document;
// tilda-zero-1.1
window.t396__isMobile =
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
navigator.userAgent.indexOf('Instagram') > -1;
Списки устройств одинаковые, хвосты — нет. У ext в «мобильные» попадает всё, где
есть касания. У Zero Block касания не учитываются вовсе, зато учитывается
встроенный браузер Instagram. Ноутбук с сенсорным экраном для одного файла
мобильный, для другого — нет, и это не опечатка в статье, а два разных условия в
двух файлах, которые работают на одной странице.
Третье определение живёт в покадровой анимации: animationSBS__isIOS считает
своим ещё и Macintosh с касаниями — то есть iPad, который представляется
компьютером. У него же триггер hover подменяется на click: наведения на
касании нет, и вместо него подписывается нажатие.
Что включается, а что нет
Коротко и без обхода:
- Элемент Zero Block — включается атрибутом
data-animate-mobile="y"на.t396__elem. Замерено, работает. - Группа или цепочка в Zero Block — не включается. Проверка идёт по записи, а у записи атрибута быть не может.
- Любой элемент в записи, где есть группа или цепочка — не включается, даже со своим атрибутом. Гибнет вся запись.
- Обычный блок — не включается никогда. Атрибут ищут на обёртке Zero Block, которой там нет.
- Счётчик чисел — не включается ничем: порог стоит до создания span, и атрибут в этой ветке не участвует.
- Мышиный параллакс на касании — не включается: условие не про настройку.
Что остаётся в случаях 2–6 — своя анимация в блоке с кодом. Она не проходит ни
через один из четырёх выключателей, потому что не пользуется классом t-animate
и не спрашивает data-animate-mobile: браузер применяет её на любой ширине.
Именно так сделаны проявление текста по скроллу и
анимация чисел — ни в одном из пяти собранных модов нет ни
одного обращения к innerWidth или clientWidth.
Как проверить у себя
Три строки в консоли на опубликованной странице, с телефонной шириной окна:
// 1. Сколько анимированных элементов осталось живыми
document.querySelectorAll('.t-animate').length;
// 2. У каких обёрток выставлен атрибут
[...document.querySelectorAll('[data-animate-mobile]')].map((el) => el.className);
// 3. Есть ли в записи групповая анимация — из-за неё гаснет вся запись
[...document.querySelectorAll('.r')].filter((r) =>
r.querySelector('.t-animate[data-animate-group=yes], .t-animate[data-animate-chain="yes"]')
).length;
Первый ноль при живой разметке означает, что сработал checkMobile. Если во
втором пусто — атрибут до страницы не доехал. Если в третьем не ноль, а во втором
что-то есть, вы попали в правило про запись целиком.
Отдельная проверка, которая экономит время: анимация могла не сработать и вовсе
не по ширине. Так бывает, когда своё правило проигрывает по специфичности
собственному правилу Zero Block — этот случай разобран в
анимации в зеро блоке, и по
симптому он неотличим от порога. Разводит их вторая строка: при пороге класс
t-animate снят, при специфичности — на месте.
Чего в этих файлах нет вообще
Системную настройку «уменьшить движение» ни один из четырёх файлов не читает:
prefers-reduced-motion и matchMedia в них не встречаются ни разу. Порог по
ширине есть, порога по просьбе пользователя — нет. Что это значит для
доступности и как это закрывают своим кодом — в разборе
доступности анимации.