Autoscale не ломает ваш transform — он работает через zoom
«Включил Autoscale — поехала анимация» звучит как готовый диагноз, и он неверный. В файле автомасштабирования 3734 байта и ни одного упоминания transform. Он ставит записи свойство zoom, и это меняет не ваши правила, а результат измерений — координаты элемента приезжают уже умноженными. Инлайновый transform на странице Тильды правда появляется, но пишет его совсем другой скрипт и по другому поводу.
Жалоба звучит так: «поставил галочку Autoscale — своя анимация перестала
работать». Виновник назначен по совпадению во времени, а проверяется за секунду:
поиск слова transform по файлу автомасштабирования даёт ноль совпадений.
Проверено 28.07.26: живой tilda-autoscale-1.0.min.js со static.tildacdn.com —
3734 байта, дата изменения 17.07.26, совпадает байт в байт со снимком в
репозитории. Файл не менялся, значит сказанное ниже верно и для страниц сейчас.
Короткий ответ
Autoscale не трогает ваш transform. Он вставляет в <head> два <style> и
задаёт записям с классом t-record_scaled свойство zoom; под масштабированием
вычисленное значение transform то же самое, что без него, — измерено ниже.
Меняется другое: getBoundingClientRect() возвращает координаты уже с учётом
масштаба, а window.innerHeight — нет: окно не масштабируется. Скрипт,
сравнивающий offsetTop с высотой окна, под zoom сработает не там.
Что Autoscale вставляет на страницу: два <style> и одна переменная
Первый элемент, id="tilda-autoscale-main-style", пишется один раз и больше не
меняется. В нём по одному правилу на каждый брейкпоинт и последнее — про масштаб:
@media (min-width: 320px) { .t-record_scaled { max-width: 320px; } }
@media (min-width: 480px) { .t-record_scaled { max-width: 480px; } }
@media (min-width: 640px) { .t-record_scaled { max-width: 640px; } }
@media (min-width: 960px) { .t-record_scaled { max-width: 960px; } }
@media (min-width: 1200px) { .t-record_scaled { max-width: 1200px; } }
@media (min-width: 320px) { .t-record_scaled { zoom: var(--t-zoom, 1); } }
Второй, id="tilda-autoscale-style", содержит одну строку и перезаписывается при
каждом изменении размера окна:
.t-record_scaled { --t-zoom: 1.06667; }
Коэффициент считается так: берётся ближайший меньший брейкпоинт из списка
[1200, 960, 640, 480, 320] и доступная ширина делится на него, с округлением до
пяти знаков. Без полосы прокрутки результат ещё и прижимается к единице снизу
через Math.max. То есть Autoscale увеличивает, а не уменьшает: раскладка
считается по брейкпоинту, а потом растягивается до ширины окна. На 1280 пикселях
это 1280 / 1200 = 1.06667, на 1400 — 1.16667, а на 1199 брейкпоинт
переключается на 960 и коэффициент прыгает до 1.24896. Пересчёт вешается на
ResizeObserver по корневому элементу.
Включается это в настройках артборда Zero Block — параметр Scale Grid Container
со значением Autoscale to Window Width. Формулу подтверждает сама справка
Тильды: «Grid контейнер будет занимать не фиксированную ширину 1200
пикселей, а растягиваться на всю ширину экрана», и «все добавленные элементы будут
пропорционально увеличены». Пропорционально увеличены — в том числе и вашим
transform.
Особые случаи Тильда чинит сама, тем же коэффициентом наоборот: карте
(.t-map) ставит инлайном zoom: calc(1 / var(--t-zoom, 1)), а высотам,
заданным в vh, — calc(<N>vh / var(--t-zoom, 1)). Оба места — признание, что
масштабирование задевает не всё одинаково.
Почему zoom, а не transform, и чем это отличается для вашего CSS
Выбор не стилистический. Предок с трансформом становится блоком-контейнером для
всего, что внутри стоит position: fixed, — закреплённая шапка внутри такой
записи начинает уезжать вместе со страницей. Замер в Chromium, прокрутка на 400
пикселей, элемент с position: fixed; top: 0:
| Масштаб записи | rect.top фиксированного | ширина на экране | высота документа |
|---|---|---|---|
| нет | 0 | 100 | 3000 |
zoom: 1.25 | 0 | 125 | 3750 |
transform: scale(1.25) | −400 | 125 | 3750 |
Картинку оба свойства масштабируют одинаково. Различаются они ровно на том, что
есть почти на каждой странице Тильды: закреплённое меню, липкая корзина,
всплывающее окно. С transform пришлось бы чинить каждое такое место, с zoom
— ни одного.
Второе отличие: zoom меняет раскладку, а не только картинку. Внутри
масштабированной записи ширина считается от окно / масштаб — при --t-zoom: 1.25
в окне 1280 запись раскладывается по 1024 пикселям и уже потом увеличивается.
Поэтому текст переносится не там, где вы ждали по ширине окна, а медиазапросы
внутри записи по-прежнему смотрят на окно, а не на эти 1024.
Кто здесь действительно пишет инлайновый transform (и это не Autoscale)
Инлайновый transform на странице Тильды появляется, и пишет его Zero Block.
Функция называется t396__processElementsTransform, живёт в
tilda-zero-1.1.min.js и вызывается из t396__applyFixesForAllElements — по всем
.t396__elem страницы. Условия срабатывания узкие:
- у элемента
data-elem-type—text,image,shape,buttonилиvector; - у элемента непустой
backdrop-filter(или-webkit-backdrop-filter); - у элемента нет атрибута
data-animate-style, то есть своей анимации Zero Block на нём не настроено.
Тогда читается вычисленный transform у внутреннего .tn-atom, единичная
матрица считается за none, а всё остальное переносится:
t.atom.style.transform = 'none';
t.element.style.transform = t.atomTransform;
Смысл в размытии: элемент с собственным трансформом создаёт свой контекст, и
backdrop-filter начинает размывать не тот фон. Тильда уносит трансформ на
обёртку, чтобы размытие смотрело куда надо. Для вашего мода это значит одно:
инлайновое transform: none на атоме перебьёт любое ваше правило из <style> —
специфичности инлайна без !important не противопоставить ничего.
Опасен не первый проход, а повторный
t396__applyFixesForAllElements вызывается не только после отрисовки блоков
(t396__globalInit), но и из t396_doResize: при смене ширины окна
(t396__onResize, задержка 500 мс и только если ширина действительно изменилась —
t396__revalidateDimensions, высота не считается) и при повороте экрана
(t396__onOrientationChange, 600 мс).
Разница принципиальная. При первом проходе у атома стоит трансформ дизайнера, и
Тильда переносит его — вашего кода там ещё нет. При втором вычисленный трансформ
атома — это уже трансформ вашего мода. Тильда прочитает его как
дизайнерский, выпишет атому none и приколотит текущее значение мода к обёртке
инлайном. Элемент останется там, где его застал поворот телефона, и уже насовсем:
следующий кадр анимации до атома не дойдёт.
Поэтому в ядре ModKod есть canTransform(): если backdrop-filter есть на
элементе, на его обёртке или на обёртке обёртки — трансформ не используется,
появление делается прозрачностью. Инлайновую opacity Zero Block не пишет никогда.
Три узла — не запас, а точный расчёт: мод передаёт .tn-atom, а тот лежит либо
прямым ребёнком .t396__elem, либо под одной обёрткой
(.tn-atom__sbs-anim-wrapper, -prx-wrapper, -sticky-wrapper). Атом → обёртка →
элемент, и ни одного лишнего шага.
Живой tilda-zero-1.1.min.js от снимка в репозитории отличается — 45294
байта против 44395, поэтому функция перепроверена по живому файлу.
Что zoom делает с измерениями: getBoundingClientRect против innerHeight
Стенд: окно 1280×800, запись с max-width: 1200px, цель на 2000 пикселях от
начала записи, высота цели 200.
| Что мерим | --t-zoom: 1 | 1.06667 | 1.25 |
|---|---|---|---|
getBoundingClientRect().top | 2000 | 2133 | 2500 |
offsetTop | 2000 | 2000 | 2000 |
getBoundingClientRect().height | 200 | 213 | 250 |
offsetHeight | 200 | 200 | 200 |
window.innerHeight | 800 | 800 | 800 |
ширина записи по rect | 1200 | 1280 | 1280 |
offsetWidth записи | 1200 | 1200 | 1024 |
scrollHeight документа | 5200 | 5547 | 6500 |
Правило читается прямо из таблицы: getBoundingClientRect() живёт в координатах
окна и масштаб уже учитывает, а offsetTop и offsetHeight — в координатах
раскладки и о масштабе не знают. Смешивать их нельзя. Именно из этой смеси
получается «анимация срабатывает слишком поздно»: offsetTop даёт 2000, а
реально элемент показывается на 2500.
Проверил на этом же стенде и наше собственное появление по скроллу — оба его пути
должны давать один результат. MK.inView работает через IntersectionObserver, а
там, где его нет, — через геометрию: rect.top < innerHeight * 0.92 && rect.bottom > 0.
Позиция прокрутки, на которой срабатывает появление:
--t-zoom | через IntersectionObserver | через геометрию |
|---|---|---|
| 1 | 1270 | 1270 |
| 1.06667 | 1400 | 1400 |
| 1.25 | 1770 | 1770 |
Совпадение полное, и причина у него та же: rect.top уже с масштабом, а
innerHeight — про окно, которое не масштабируется. Обе величины из одной
системы координат, поэтому смесь корректна.
Заодно измерено главное утверждение статьи. Правило .mk-shift { transform: translateY(-40px) } внутри масштабированной записи: вычисленное значение и при
zoom: 1, и при zoom: 1.25 — одна и та же matrix(1, 0, 0, 1, 0, -40),
инлайнового transform не появляется, а сдвиг на экране составляет 40 и 50
пикселей. Трансформ масштабируется вместе с записью, пропорционально.
Когда Autoscale правда виноват, а когда виновата специфичность
Настоящие последствия масштабирования такие:
- дробный масштаб — границы в один пиксель и тонкие линии попадают между физическими пикселями и выглядят неровно;
- высота документа растёт (5200 → 6500 в таблице выше), и всё, что считает
проценты прокрутки по
scrollHeight, начинает считать по другому числу; vhвнутри записи умножается на масштаб; для своего атрибутаdata-height-in-vhТильда делит обратно, для ваших правил — нет;- Safari — часть пересчёта высот в Autoscale для него пропускается, проверять надо отдельно.
Чаще всего причина другая. Если правило не применилось — виновата специфичность,
и это видно в DevTools за десять секунд: выбрать элемент, открыть «Styles»,
посмотреть, не зачёркнуто ли оно.
Инлайновый стиль перебивает любое правило из <style>; правило Тильды с той же
специфичностью, но объявленное позже, — тоже. Autoscale к этому не причастен: он
не пишет инлайновых стилей никому, кроме карты.
Как проверить у себя за одну минуту
В консоли на своей странице:
// Включён ли Autoscale и какой сейчас коэффициент.
getComputedStyle(document.querySelector('.t-record_scaled')).zoom;
// Что именно вставил Autoscale — обе таблицы стилей целиком.
document.getElementById('tilda-autoscale-main-style').textContent;
document.getElementById('tilda-autoscale-style').textContent;
// Кто на странице получил инлайновый transform. Autoscale — никто.
[...document.querySelectorAll('[style*="transform"]')].map((el) => el.className);
Если третья строка вернула ваши элементы — смотрите на backdrop-filter у них
или у их предков: это Zero Block, а не масштабирование. Если вернула пустой
список, а анимация всё равно не идёт, — причина в специфичности или в том, что
элемент уже занят одной из четырёх систем анимации Тильды.
Разбор конкретной страницы, где эффект не запускается, а причина не находится глазами, — это платная работа: пишем мод, который считается с масштабированием, с Zero Block и с тем, что Тильда уже делает сама. Готовые моды из каталога все три вещи учитывают в ядре.