Перейти к содержимому
Забрать бесплатно
Модификации выключены. Так эта страница выглядит на обычной Тильде.

Autoscale не ломает ваш transform — он работает через zoom

28 июля 2026 9 МИН КОМАНДА MODKOD

«Включил Autoscale — поехала анимация» звучит как готовый диагноз, и он неверный. В файле автомасштабирования 3734 байта и ни одного упоминания transform. Он ставит записи свойство zoom, и это меняет не ваши правила, а результат измерений — координаты элемента приезжают уже умноженными. Инлайновый transform на странице Тильды правда появляется, но пишет его совсем другой скрипт и по другому поводу.

✓ ПРОВЕРЕНО 28 июля 2026 живой tilda-autoscale-1.0.min.js (3734 байта, ни одного вхождения слова transform) совпадает байт в байт со снимком в репозитории; функция t396__processElementsTransform в живом tilda-zero-1.1.min.js; поведение zoom в Chromium — координаты, раскладка, position fixed и согласие двух путей в MK.inView — static.tildacdn.com, Chromium 1280×800, стенд с разметкой Autoscale

Жалоба звучит так: «поставил галочку 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 фиксированногоширина на экраневысота документа
нет01003000
zoom: 1.2501253750
transform: scale(1.25)−4001253750

Картинку оба свойства масштабируют одинаково. Различаются они ровно на том, что есть почти на каждой странице Тильды: закреплённое меню, липкая корзина, всплывающее окно. С 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: 11.066671.25
getBoundingClientRect().top200021332500
offsetTop200020002000
getBoundingClientRect().height200213250
offsetHeight200200200
window.innerHeight800800800
ширина записи по rect120012801280
offsetWidth записи120012001024
scrollHeight документа520055476500

Правило читается прямо из таблицы: getBoundingClientRect() живёт в координатах окна и масштаб уже учитывает, а offsetTop и offsetHeight — в координатах раскладки и о масштабе не знают. Смешивать их нельзя. Именно из этой смеси получается «анимация срабатывает слишком поздно»: offsetTop даёт 2000, а реально элемент показывается на 2500.

Проверил на этом же стенде и наше собственное появление по скроллу — оба его пути должны давать один результат. MK.inView работает через IntersectionObserver, а там, где его нет, — через геометрию: rect.top < innerHeight * 0.92 && rect.bottom > 0. Позиция прокрутки, на которой срабатывает появление:

--t-zoomчерез IntersectionObserverчерез геометрию
112701270
1.0666714001400
1.2517701770

Совпадение полное, и причина у него та же: 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 и с тем, что Тильда уже делает сама. Готовые моды из каталога все три вещи учитывают в ядре.

Вопросы по этой статье 4

Нет. В файле tilda-autoscale-1.0.min.js слова transform нет ни одного — это проверяется поиском по файлу за секунду. Он вставляет два элемента style и задаёт свойство zoom на записях с классом t-record_scaled. Ваше правило с transform остаётся ровно таким, каким вы его написали, — вычисленное значение под масштабированием не меняется.