CSS-шлюзом (CSS-lock) называется методика из адаптивного веб-дизайна, позволяющая не перепрыгивать от одного значения к другому, а переходить плавно, в зависимости от текущего размера области просмотра (viewport). Идею и одну из реализаций предложил Тим Браун в статье Flexible typography with CSS locks. Когда я пытался разобраться с его реализацией и создать свои варианты, мне с трудом удавалось понять, что именно происходит. Я выполнил много вычислений и подумал, что полезно будет объяснить другим всю эту математику.

В статье я опишу саму методику, её ограничения и лежащую в её основе математику. Не волнуйтесь: там в основном одни сложения и вычитания. К тому же я постарался всё разбить на этапы и украсил их графиками.

Что такое CSS-шлюз?


Зависимость от размера области просмотра


В моих последних проектах использовался баннер на всю ширину с заголовком и одни лишь «десктопные» шаблоны с крупными шрифтами. Я решил, что мне нужны маленькие шрифты для небольших экранов и что-то для экранов промежуточных размеров. Так почему бы не поставить размер шрифтов в зависимость от ширины области просмотра?

Раньше это делали примерно так:

h1 { font-size: 4vw; /* Бум! Готово. */ }

У этого подхода есть две проблемы:

  1. На очень маленьких экранах текст становится крохотным (12,8 пикселя в высоту при ширине экрана 320 пикселей), на больших — огромным (64 при 1600);
  2. Не учитываются пользовательские настройки размера шрифта.

CSS-шлюзы позволяют избавиться от первой проблемы. Замечательные CSS-шлюзы также постараются учитывать и пользовательские настройки.

Идея CSS-шлюза


CSS-шлюз — это особый вид вычисления CSS-значения, при котором:

  • есть минимальное и максимальное значение,
  • есть две контрольные точки (breakpoint) (обычно зависят от ширины области просмотра),
  • между этими точками значение меняется линейно от минимума до максимума.



«При ширине меньше 320 пикселей будем использовать шрифты 20px, свыше 960 пикселей — 40px, а между 320 и 960 — от 20px до 40px».

На стороне CSS это может выглядеть так:

h1 { font-size: 1.25rem; }

@media (min-width: 320px) {
  h1 { font-size: /* волшебное значение от 1.25 rem до 2.5 rem */; }
}

@media (min-width: 960px) {
  h1 { font-size: 2.5rem; }
}

Первая задача — реализовать волшебное значение. Немного подпорчу вам удовольствие и сразу скажу, что это выглядит так:

h1 {
  font-size: calc(1.25rem + viewport_relative_value);
}

Здесь viewport_relative_value может быть одиночным значением (например, 3vw) или представлять собой более сложное вычисление (на основе единицы измерения области просмотра vw или какой-то другой единицы).

Ограничения


Поскольку CSS-шлюзы завязаны на единицы измерения области просмотра, шлюзы имеют ряд важных ограничений. Они могут принимать только числовые значения, использовать calc() и принимать значения в пикселях.

Почему так? Потому что единицы измерения области просмотра (vw, vh, vmin и vmax) всегда определяются в пикселях. Например, если ширина области просмотра — 768 пикселей, то 1vw определяется в 7,68 пикселя.

(В статье Тима есть ошибка: он пишет, что вычисления вроде 100vw - 30em дают значение em. Это не так. Браузер считает 100vw в пикселях и вычитает из него значение 30em для этого элемента и свойства.)

Некоторые примеры того, что не работает:

  • CSS-шлюз для свойства opacity, потому что opacity: calc(.5+1px) является ошибкой;
  • CSS-шлюз для большинства функций transform (например, rotate: шлюз не может выполнять вращение на основании значения в пикселях).

Итак, у нас есть пиксельное ограничение. Может быть, найдутся храбрецы и вычислят все свойства и методики, которые могут использоваться в CSS-шлюзах.

Для начала возьмём свойства font-size и line-height и посмотрим, как можно создавать для них CSS-шлюзы с контрольными точками на основе пикселей или em.

CSS-шлюзы с пиксельными контрольными точками


Демки



Далее мы рассмотрим, как получить CSS-код для каждого из этих примеров.

Размер шрифта как линейная функция


Нам нужно, чтобы font-size пропорционально увеличивался с 20px при ширине области 320px до 40px при ширине 960px. Отразим это на графике:



Красная линия — это график линейной функции. Можно записать её как y = mx + b:

  • y — размер шрифта (вертикальная ось),
  • x — ширина области просмотра в пикселях (горизонтальная ось),
  • m — наклон (slope) функции (сколько пикселей мы добавляем к размеру шрифта при увеличении ширины области просмотра на 1 пиксель),
  • b — размер шрифта до того, как мы начинаем увеличивать размер области просмотра.

Нам нужно вычислить m и b. В уравнении они являются константами.

Сначала разберёмся с m. Для этого нужны только координаты (x,y). Это похоже на вычисление скорости (дистанция, пройденная за единицу времени), но в данном случае мы вычисляем размер шрифта в зависимости от ширины области просмотра:

m = font_size_increase / viewport_increase
m = (y2 - y1) / (x2 - x1)
m = (40 - 20) / (960 - 320)
m = 20 / 640
m = 0.03125

Другая форма:

Общее увеличение font-size — 20 пикселей (40 - 20).
Общее уменьшение области просмотра — 640 пикселей (960 - 320).
Если ширина области вырастет на 1 пиксель, насколько увеличится размер font-size?

20 / 640 = 0.03125 px.

Теперь вычислим b.

y = mx + b
b = y - mx
b = y - 0.03125x

Поскольку наша функция проверяется с помощью обеих этих точек, мы можем использовать координаты (x,y) любой из них. Возьмём первую:

b = y1 - 0.03125 ? x1
b = 20 - 0.03125 ? 320
b = 10

Кстати, вычислить эти 10 пикселей можно было, просто посмотрев на график. Но ведь он не всегда у нас есть :-)

Теперь наша функция выглядит так:

y = 0.03125x + 10

Преобразование в CSS


y — размер font-size, и, если мы хотим выполнить базовые операции в CSS, нам нужен calc().

font-size: calc( 0.03125x + 10px );

Конечно, это псевдоCSS, потому x не является валидным синтаксисом. Но в нашей линейной функции x представляет ширину области просмотра, которую в CSS можно выразить как 100vw.

font-size: calc( 0.03125 * 100vw + 10px );

Вот теперь получился работающий CSS. Если нужно выразить более кратко, выполним умножение. Поскольку 0.03125 ? 100 = 3.125, то:

font-size: calc( 3.125vw + 10px );

Теперь ограничим ширину области просмотра 320 и 960 пикселями. Добавим несколько media-запросов:

h1 { font-size: 20px; }

@media (min-width: 320px) {
  h1 { font-size: calc( 3.125vw + 10px ); }
}

@media (min-width: 960px) {
  h1 { font-size: 40px; }
}

Теперь наш график выглядит как нужно:



Красиво, но мне не очень нравятся значения в пикселях при объявлении font-size. Можно ли сделать лучше?

Применение пользовательских настроек


Практически каждый браузер позволяет пользователям задавать размер текста по умолчанию. Чаще всего оно изначально равно 16 пикселям, но иногда его изменяют (обычно увеличивают).
Я хочу вставить пользовательские настройки в нашу формулу и для этого обращу внимание на значения rem. В отношении em и процентных значений применяется тот же принцип.

Сначала проверим, что базовому (root) font-size не присвоено абсолютное значение. Например, если вы используете CSS из Bootstrap 3, там встречается немало такого:

html {
  font-size: 10px;
}

Никогда так не делайте! (К счастью, в Bootstrap 4 это исправили.) Если вам действительно нужно изменить значение базового em (1rem), используйте:

/*
 * Меняет значение rem с соблюдением пропорциональности.
 * При размере по умолчанию font-size 16 пикселей:
 * • 62.5% -> 1rem = 10px, .1rem  = 1px
 * • 125%  -> 1rem = 20px, .05rem = 1px
 */
html {
  font-size: 62.5%;
}

Тем не менее оставим в покое базовый font-size, пусть он будет по умолчанию равен 16 пикселям. Давайте посмотрим, что произойдёт, если в нашем font-size-шлюзе заменить пиксельные значения rem-значениями:

/*
 * С пользовательскими настройками по умолчанию:
 * • 0.625rem = 10px
 * • 1.25rem  = 20px
 * • 2.5rem   = 40px
 */
h1 { font-size: 1.25rem; }

@media (min-width: 320px) {
  h1 { font-size: calc( 3.125vw + .625rem ); }
}

@media (min-width: 960px) {
  h1 { font-size: 2.5rem; }
}

Если запустить код с браузерными настройками по умолчанию, то он будет вести себя, как предыдущий код, использовавший пиксели. Замечательно!

Но поскольку мы сделали это ради поддержки вносимых пользователями изменений, нужно проверить, как всё работает. Допустим, пользователь задал размер шрифта 24 пикселя вместо 16 (на 50 % больше). Как поведёт себя код?



Синяя линия: font-size по умолчанию равен 16 пикселям.
Красная линия: font-size по умолчанию равен 24 пикселям.

При увеличении области просмотр до 320 пикселей шрифт становится меньше (с 30 пикселей уменьшается до 25), а при достижении второй контрольной точки увеличивается скачкообразно (с 45 до 60 пикселей). Ой.

Исправить это поможет то же настраиваемое пользователем базовое значение (baseline) для всех трёх размеров. Например, выберем 1.25rem:

h1 { font-size: 1.25rem; }

@media (min-width: 320px) {
  h1 { font-size: calc( 1.25rem + 3.125vw - 10px ); }
}

@media (min-width: 960px) {
  h1 { font-size: calc( 1.25rem + 20px ); }
}

Обратите внимание на 3.125vw - 10px. Это наша старая линейная функция (в виде mx + b), но с другим значением b. Назовём его b?. В данном случае мы знаем, что базовое значение равно 20 пикселям, поэтому можем получить значение b? простым вычитанием:

b? = b - baseline_value
b? = 10 - 20
b? = 10

Другой способ — выбирать базовое значение с самого начала, а потом искать линейную функцию, описывающую увеличение font-size (назовём его y?, чтобы не путать со значением самого font-size y).

x1 = 320
x2 = 960

y?1 = 0
y?2 = 20

m = (y?2 - y?1) / (x2 - x1)
m = (20 - 0) / (960 - 320)
m = 20 / 640
m = 0.03125

b? = y? - mx
b? = y?1 - 0.03125 ? x1
b? = 0 - 0.03125 ? 320
b? = -10

Получили функцию y? = 0.03125x - 10, которая выглядит так:



С базовым значением в rem и дополнительными значениями в vw и/или px мы наконец-то можем создать полноценный работающий шлюз для font-size. Когда пользователь меняет размер шрифта по умолчанию, система подстраивается под него и не ломается.



Пурпурная линия: степень увеличения font-size.
Синяя линия: font-size по умолчанию равен 16 пикселям.
Красная линия: font-size по умолчанию равен 24 пикселям.

Конечно, это не совсем то, что просил пользователь: он хотел увеличить шрифт на 50%, а мы увеличили его на 50% в маленьких областях просмотра и на 25% — в больших. Но это хороший компромисс.

Создание шлюза для высоты строки


В данном случае у нас будет такой сценарий: «Я хочу параграфы с высотой строки в 140% при области просмотра шириной 320 пикселей и 180% — при 960».

Поскольку мы будем работать с базовым значением плюс динамически изменяемым значением, выраженным в пикселях, нам нужно знать, сколько пикселей составляют коэффициенты 1,4 и 1,8. То есть нужно вычислить font-size для наших параграфов. Допустим, базовый размер шрифта равен 16 пикселям. Получаем:

  • 16 * 1.4 = 22.4 пикселя при нижнем размере области просмотра (320 px)
  • 16 * 1.8 = 28.8 пикселя при верхнем размере области просмотра (960 px)

В качестве базового значения возьмём 140% = 22.4px. Получается, что общее увеличение шрифта составляет 6,4 пикселя. Воспользуемся нашей линейной формулой:

x1 = 320
x2 = 960

y?1 = 0
y?2 = 6.4

m = (y?2 - y?1) / (x2 - x1)
m = (6.4 - 0) / (960 - 320)
m = 6.4 / 640
m = 0.01

b? = y? - mx
b? = y?1 - 0.01 ? x1
b? = 0 - 0.01 ? 320
b? = 3.2

y? = 0.01x - 3.2

Преобразуем в CSS:

line-height: calc( 140% + 1vw - 3.2px );

Примечание: базовое значение нужно выражать как 140% или 1.4em; безразмерное 1.4 не будет работать внутри calc().

Затем добавляем media-запросы и проверяем, чтобы все объявления line-height использовали одно базовое значение (140%).

p { line-height: 140%; }

@media (min-width: 320px) {
  p { line-height: calc( 140% + 1vw - 3.2px ); }
}

@media (min-width: 960px) {
  p { line-height: calc( 140% + 6.4px ); }
}

Напоминаю: для большой области просмотра нельзя использовать просто 180%, нам нужна выраженная в пикселях часть, которая добавляется к базовому значению. Если взять 180%, то при базовом размере шрифта 16 пикселей всё будет нормально, пока пользователь его не поменяет.

Построим график и проверим работу кода при разных базовых значениях font-size.



Синяя линия: font-size по умолчанию равен 16 пикселям.
Красная линия: font-size по умолчанию равен 24 пикселям.

Теперь, когда наша формула line-height зависит от собственного размера font-size элемента, изменение размера шрифта приведёт к изменению формулы. Например, в этом демо показан параграф с увеличенным текстом, определённым как:

.big {
  font-size: 166%;
}

Это меняет наши контрольные точки:

  • 16 * 1.66 * 1.4 = 37.184 пикселя при нижнем размере области просмотра (320px)
  • 16 * 1.66 * 1.8 = 47.808 пикселя при верхнем размере области просмотра (960px)

Проведём вычисления и получим обновлённую формулу: y? = 0.0166x - 5.312. Затем объединим в CSS этот и предыдущий стили:

p { line-height: 140%; }
.big { font-size: 166%; }

@media (min-width: 320px) {
  p    { line-height: calc( 140% + 1vw - 3.2px ); }
  .big { line-height: calc( 140% + 1.66vw - 5.312px ); }
}

@media (min-width: 960px) {
  p    { line-height: calc( 140% + 6.4px ); }
  .big { line-height: calc( 140% + 10.624px ); }
}

Также можно возложить вычисления на CSS. Раз мы используем одни и те же контрольные точки и относительные размеры line-heights, как для стандартного параграфа, то нам нужно лишь добавить коэффициент 1,66:

p { line-height: 140%; }
.big { font-size: 166%; }

@media (min-width: 320px) {
  p    { line-height: calc( 140% + 1vw - 3.2px ); }
  .big { line-height: calc( 140% + (1vw - 3.2px) * 1.66 ); }
}
@media (min-width: 960px) {
  p    { line-height: calc( 140% + 6.4px ); }
  .big { line-height: calc( 140% + 6.4px * 1.66 ); }
}

Объединение шлюзов font-size и line-height


Попробуем теперь всё собрать воедино. Сценарий: есть адаптирующийся столбец текста (fluid column) с H1 и несколькими параграфами. Нам нужно изменить font-size и line-height, используя следующие значения:

Элемент и свойство Значение при 320px Значение при 960px
H1 font-size 24 пикселя 40 пикселей
H1 line-height 133,33% 120%
P font-size 15 пикселей 18 пикселей
P line-height 150% 166,67%

Вы увидите, что с высотой строки мы делаем две вещи. Есть общее правило: когда текст увеличивается, высоту строки нужно уменьшать, а когда столбец становится шире — увеличивать. Но в нашем сценарии одновременно происходят обе ситуации, противоречащие друг другу! Поэтому нужно выбрать приоритеты:

  • Для H1 увеличение font-size будет критичнее, чем увеличение ширины столбца.
  • Для параграфов увеличение ширины столбца будет критичнее, чем небольшое увеличение font-size.

Теперь выберем две контрольные точки — область просмотра шириной 320 и 960 пикселей. Начнём с написания шлюза для font-size:

h1 { font-size: 1.5rem; }
/* .9375rem = 15px с настройками по умолчанию */
p { font-size: .9375rem; }

@media (min-width: 320px) {
  h1 { font-size: calc( 1.5rem + 2.5vw - 8px ); }
  /* .46875vw - 1.5px равно от 0 до 3px */
  p { font-size: calc( .9375rem + .46875vw - 1.5px ); }
}
@media (min-width: 960px) {
  h1 { font-size: calc(1.5rem + 16px); }
  p { font-size: calc( .9375rem + 3px ); }
}

Здесь ничего нового, только значения поменялись.

Теперь рассчитываем шлюзы для line-height. Это будет куда сложнее, чем в прошлый раз.

Начнём с элемента H1. Для line-height воспользуемся относительным базовым значением — 120%. Поскольку размер шрифта элемента у нас изменяем, то эти 120% позволяют описать динамическое и линейное значение, определяемое двумя точками:

  • 24 ? 1.2 = 28.8px в нижней контрольной точке,
  • 40 ? 1.2 = 48px в верхней контрольной точке.

В нижней контрольной точке нам нужно иметь значение line-height, равное 133,33%, это около 32 пикселей.

Найдём линейную функцию, описывающую «то, что добавляется к базовому значению 120%». Если убрать эти 120%, получим два модифицированных значения:

  • 24 ? (1.3333 - 1.2) = 3.2px в нижней контрольной точке,
  • 40 ? (1.2 - 1.2) = 0px в верхней контрольной точке.

Должен получиться отрицательный наклон.

m = (y?2 - y?1) / (x2 - x1)
m = (0 - 3.2) / (960 - 320)
m = -3.2 / 640
m = -0.005

b? = y? - mx
b? = y?1 - (-0.005 ? x1)
b? = 3.2 + 0.005 ? 320
b? = 4.8

y? = -0.005x + 4.8

Преобразуем в CSS:

h1 {
  line-height: calc( 120% - .5vw + 4.8px );
}

Посмотрим на график:



Синяя линия: уменьшение line-height.
Красная линия: базовое значение line-height (120% font-size заголовка).
Пурпурная линия: финальная line-height.

На графике видно, что результирующая высота строки (пурпурная линия) равна базовому значению 120% плюс уменьшение высоты строки (синяя линия). Можете сами проверить вычисления на GraphSketch.com.

Для параграфов мы воспользуемся базовым значением 150%. Увеличение line-height:

(1.75 - 1.5) ? 18 = 4.5px.



Мой калькулятор говорит, что формула будет такая:

y? = 0.00703125x - 2.25

Чтобы увидеть полный CSS-код, взгляните на исходный код демки, в которой объединены font-size и line-height. Изменяя размер браузерного окна, вы убедитесь, что эффект есть, хоть и слабый.

Также рекомендую потестировать эту демку, изменив размер шрифта по умолчанию. Обратите внимание, что здесь соотношения line-height будут немного другими, но вполне допустимыми. Нет ничего плохого в том, что line-height становится меньше базового значения.

Автоматизация вычислений


Готовя этот раздел, я выполнял все вычисления вручную либо с помощью калькулятора Soulver. Но это довольно трудоёмко, и высока вероятность ошибок. Чтобы исключить человеческий фактор, хорошо бы внедрить автоматизацию.

Первый способ — перенести все вычисления в CSS. Это вариант формулы, использованной в примерах с font-size, когда подробно разбирались все значения:

@media (min-width: 320px) and (max-width: 959px) {
  h1 {
    font-size: calc(
      /* y1 */
      1.5rem
      /* + m ? x */
      + ((40 - 24) / (960 - 320)) * 100vw
      /* - m ? x1 */ 
      - ((40 - 24) / (960 - 320)) * 320px
    );
  }
}

Но получается слишком много букв, можно написать гораздо лаконичней:

@media (min-width: 320px) and (max-width: 959px) {
  h1 {
    font-size: calc( 1.5rem + 16 * (100vw - 320px) / (960 - 320) );
  }
}

Так совпало, что эту формулу использовал Тим Браун в статье Flexible typography with CSS locks, правда, с пикселями вместо em в значении переменной. Это работает и для объединённого варианта с font-size и line-height, но может быть не столь очевидно, особенно при отрицательном наклоне.

@media (min-width: 320px) and (max-width: 959px) {
  h1 {
    font-size: calc( 1.5rem + 16 * (100vw - 320px) / (960 - 320) );
    /* При отрицательном наклоне нужно инвертировать контрольные точки */
    line-height: calc( 120% + 3.2 * (100vw - 960px) / (320 - 960) );
  }
}

Второй способ — автоматизировать вычисления с помощью плагина Sass или PostCSS mixin.

CSS-шлюзы с em-контрольными точками


Новые демки


Я взял три первые демки и вместо пиксельных значений контрольных точек и инкрементирований вставил значения на основе rem.


В следующем разделе мы рассмотрим работу специфического синтаксиса в этих демках.

Синтаксис m ? 100vw для media-запросов на основе em — не лучшая идея


Выше мы задействовали синтаксис m ? 100vw (например, здесь calc(base + 2.5vw)). Его нельзя использовать с media-запросами на основе em.

Всё дело в контексте media-запросов. Единицы em и rem ссылаются на одно и то же: базовый размер шрифта в User Agent. А он, как мы уже несколько раз видели, обычно равен 16 пикселям, но значение может быть и другое. Почему?

  1. По воле браузера или ОС (в основном в специфических случаях вроде ТВ-браузеров и читалок).
  2. По воле пользователя.

Так что если у нас контрольные точки 20em и 60em, то они будут соответствовать реальной CSS-ширине:

  • 320 и 960 пикселей при базовом размере шрифта 16 пикселей,
  • 480 и 1440 — при 24 пикселях и т. д.

(Обратите внимание, что это CSS-пиксели, а не аппаратные пиксели. В статье мы не рассматриваем аппаратные пиксели, поскольку они не влияют на наши вычисления.)

Выше приводились примеры кода наподобие такого:

font-size: calc( 3.125vw + .625rem );

Если в этом синтаксисе заменить все контрольные точки с использованием em, приняв, что в media-запросе 1 em равен 16 пикселям, то получится:

h1 { font-size: 1.25rem; }

/* Не делайте так :((( */
@media (min-width: 20em) {
  h1 { font-size: calc( 1.25rem + 3.125vw - 10px ); }
}

/* Или так. */
@media (min-width: 60em) {
  h1 { font-size: calc( 1.25rem + 20px ); }
}

Это сработает, если ОС, браузер и пользователь никогда не меняют размер шрифта по умолчанию. А иначе будет плохо:



Синяя линия: font-size по умолчанию равен 16 пикселям.
Красная линия: font-size по умолчанию равен 24 пикселям.

Что здесь происходит? Когда мы меняем базовый font-size, контрольные точки на основе em смещаются на более высокие пиксельные значения. Единственно верным значением для конкретных точек будет 3.125vw - 10px!

  • При 320 пикселях 3.125vw - 10px равно 0 пикселям, как и должно быть.
  • При 480 пикселях 3.125vw - 10px равно 5 пикселям.

На высоких контрольных точках ещё хуже:

  • При 960 пикселях 3.125vw - 10px равно 20 пикселям, как и должно быть.
  • При 1440 пикселях 3.125vw - 10px равно 35 пикселям (на 15 больше).

Если вы хотите использовать контрольные на основе em, то нужно делать иначе.

Снова выполняем расчёты


Эта методика продемонстрирована в статье Тима Брауна. Она подразумевает, что большинство вычислений делается в CSS с использованием двух переменных частей:

  • 100vw — ширина области просмотра;
  • нижняя контрольная точка, выраженная в rem.

Воспользуемся формулой:

y = m ? (x - x1) / (x2 - x1)

Почему именно ней? Давайте разберём по шагам. Выше мы увидели, что font-size и line-height можно описать линейной функцией:

y = mx + b


В CSS можно работать с x (это 100vw). Но нельзя задать m и b точные значения в пикселях или vw, потому что это константы, выраженные в пикселях, и их можно спутать с нашими контрольными точками, выраженными в em, если пользователь изменит размер шрифта по умолчанию.

Попробуем заменить m и b другими известными значениями, а именно (x1,y1) и (x2,y2).

Находим b с помощью первой пары координат:

b = y - mx
b = y1 - m ? x1

Собираем всё вместе:

y = mx + b
y = mx + y1 - m ? x1

Мы исключили b из формулы!

Также выше мы видели, что на самом деле нам нужно было не полное значение font-size или line-height, а только динамическая часть, которую мы добавляем к базовому значению. Мы назвали её y? и можем выразить так:

y  = y1 + y?
y? = y - y1

Заменим y с помощью выведенного равенства:

y? = mx + y1 - m ? x1 - y1
y? = mx + y1 - m ? x1 - y1

Мы можем избавиться от кусков + y1 и - y1!

y? = m ? x - m ? x1
y? = m ? (x - x1)

Теперь можем заменить m уже известными значениями:

m = (y2 - y1) / (x2 - x1)

Тогда:

y? = (y2 - y1) / (x2 - x1) ? (x - x1)

Также это можно записать так:

y? = max_value_increase ? (x - x1) / (x2 - x1)

Преобразование в CSS


Это значение мы можем использовать в CSS. Вернёмся опять к нашему примеру «от 20 до 40 пикселей»:

@media (min-width: 20em) and (max-width: 60em) {
  h1 {
    /* ВНИМАНИЕ: это пока не работает! */
    font-size: calc(
      1.25rem /* базовое значение */
      + 20px /* разница между максимальным и базовым значениями */
      * (100vw - 20rem) /* x - x1 */
      / (60rem - 20rem) /* x2 - x1 */
    );
  }
}

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

Начнём с фрагмента 100vw - 20rem. Эта часть работает как есть и возвращает значение в пикселях.

Например, если font-size по умолчанию — 16 пикселей, а ширина области просмотра — 600 пикселей, то результат равен 280 пикселям (600 - 20 ? 15). Если font-size по умолчанию — 24 пикселя, а ширина области просмотра — 600 пикселей, то результат равен 120 пикселям (600 - 20 ? 24).



Обратите внимание, что для выражения наших контрольных точек мы используем единицу rem. Почему не em? Потому что в CSS-значении em ссылается не на базовый font-size, а на собственный font-size элемента (в общем) либо на его родительский font-size (когда используется свойство font-size).

В идеале нам нужна CSS — единица измерения, ссылающаяся на браузерный размер шрифта по умолчанию. Но такой единицы не существует. Самое близкое — это rem, которая ссылается на базовый font-size, только если он абсолютно не менялся.

То есть в нашем CSS ни в коем случае не должно быть подобного кода:

/* Плохо */
html { font-size: 10px; }

/* Достаточно плохо */
:root { font-size: 16px; }

/* Удовлетворительно, но придётся прописать все
   ключевые точки, например 20rem/1.25,
   40em/1.25 и т. д. */
:root { font-size: 125%; }

Безразмерные знаменатели и множители calc


Хотелось бы привести 60rem - 20rem к ширине в пикселях. Это означало бы, что дробь (x - x1) / (x2 - x1) давала бы значение в диапазоне от 0 до 1. Назовём его n.

При размере шрифта по умолчанию в 16 пикселей и ширине области просмотра в 600 пикселей мы получим:

n = (x - x1) / (x2 - x1)
n = (600 - 320) / (960 - 320)
n = 280 / 640
n = 0.475

К сожалению, не совсем то.

Проблема в том, что при делении в calc() мы не можем в качестве знаменателя использовать пиксели или какую-либо CSS-единицу. Величина должна быть безразмерной. Так что нам делать?

А что если просто убрать единицы измерения в знаменателе? Каков будет результат вычисления calc((100vw - 20rem)/(60 - 20))?

Размер шрифта по умолчанию — 16 пикселей
Область просмотра Деление в CSS Результат
20em (320px) (320px – 16px ? 20) / (60 – 20) = 0px
40em (640px) (640px – 16px ? 20) / (60 – 20) = 8px
60em (960px) (960px – 16px ? 20) / (60 – 20) = 16px
Размер шрифта по умолчанию — 24 пикселя
Область просмотра Деление в CSS Результат
20em (480px) (480px – 24px ? 20) / (60 – 20) = 0px
40em (960px) (960px – 24px ? 20) / (60 – 20) = 12px
60em (1440px) (1440px – 24px ? 20) / (60 – 20) = 24px

Как видите, в диапазоне между контрольными точками (от 20em до 60em) мы получаем линейное изменение от 0rem до 1rem. Годится!

При первой попытке заставить работать наш CSS мы использовали множитель 20px. Надо его вычеркнуть.

Код первой попытки:

font-size: calc( 1.25rem + 20px * n );

Здесь n принимала значение от 0 до 1. Но из-за ограничений синтаксиса деления в calc() мы не могли получать нужный нам результат от 0 до 1.

Тогда мы получили пиксельный эквивалент для диапазона 0rem — 1rem; назовём это значение r.

Другое ограничение calc() относится к умножению. Если записать calc(a * b), то a или b должно быть безразмерным числом.

Поскольку у r есть размерность (это пиксели), то безразмерным должен быть второй множитель.

Мы хотим увеличить на 20 пикселей в верхней контрольной точке. 20 пикселей — это 1.25rem, так что множитель будет 1.25:

font-size: calc( 1.25rem + 1.25 * r );

Должно сработать. Но имейте в виду, что значение r будет меняться в зависимости от размера шрифта по умолчанию:

  • 16 пикселей: 1.25 * r равно от 0 до 20 пикселей.
  • 24 пикселя: 1.25 * r равно от 0 до 30 пикселей.

Давайте теперь напишем весь CSS-шлюз целиком, с media-запросами, верхним и нижним значениями:

h1 {
  font-size: 1.25rem;
}

@media (min-width: 20em) {
  /* Результат (100vw - 20rem) / (60 - 20) в диапазоне 0-1rem, 
     в зависимости от ширины области просмотра (от 20em до 60em). */
  h1 {
    font-size: calc( 1.25rem + 1.25 * (100vw - 20rem) / (60 - 20) );
  }
}

@media (min-width: 60em) {
  /* Правая часть дополнения ДОЛЖНА быть rem. 
     В нашем примере мы МОЖЕМ заменить всё объявление
     на font-size: 2.5rem, но если наше базовое значение
     выражалось не в rem, то придётся использовать calc. */
  h1 {
    font-size: calc( 1.25rem + 1.25 * 1rem );
  }
}

В этом случае, в отличие от шлюза font-size, использующего пиксели, когда пользователь увеличивает размер шрифта по умолчанию на 50%, всё остальное тоже увеличивается на 50%: базовое значение, переменная и контрольные точки. Мы получаем диапазон 30—60 пикселей вместо необходимого 20—40.



Синяя линия: font-size по умолчанию равен 16 пикселям.
Красная линия: font-size по умолчанию равен 24 пикселям.

Можете проверить это самостоятельно в первой демке, использующей em.

Шлюзы line-height c em/rem


В нашей второй демке мы изменим line-height параграфа со 140% до 180%. Будем использовать 140% в качестве базового значения, а в роли переменной части выступит та же формула, что и в примере с font-size.

p {
  line-height: 140%;
}
@media (min-width: 20em) {
  p {
    line-height: calc( 140% + .4 * (100vw - 20rem) / (60 - 20) );
  }
}
@media (min-width: 60em) {
  p {
    line-height: calc( 140% + .4 * 1rem );
  }
}

Для переменной части line-height нам нужно rem-значение, потому что (100vw - 20rem) / (60 - 20) даёт результат в пикселях в диапазоне от 0rem до 1rem.

Поскольку font-size нашего параграфа остаётся равен 1rem, увеличение высоты строки ещё на 40% равно .4rem. Это значение мы и будем использовать в двух calc()-выражениях.

Теперь возьмём из третьей демки пример с line-height. Нам нужно уменьшить line-height H1 со 133,33% до 120%. И мы знаем, что при этом изменится font-size.

Для того же примера мы уже определяли, что уменьшение высоты строки можно выразить с помощью двух контрольных точек:

  • 24 ? (1.3333 - 1.2) = 3.2px при нижнем размере области видимости,
  • 40 ? (1.2 - 1.2) = 0px при верхнем размере области видимости.

В качестве базового значения возьмём 120%, а переменная часть будет от 3,2 до 0 пикселей. Если размер шрифта по умолчанию равен 16 пикселям, то 3,2 пикселя = 0.2rem, так что множитель равен .2.

Наконец, поскольку переменная часть должна быть равна нулю в верхней точке, нужно инвертировать в формуле контрольные точки:

h1 {
  line-height: calc( 120% + 0.2 * 1rem );
}
@media (min-width: 20em) {
  h1 {
    line-height: calc( 120% + 0.2 * (100vw - 60rem) / (20 - 60) );
  }
}
@media (min-width: 60em) {
  h1 {
    line-height: 120%;
  }
}

Два замечания:

  1. Значение .2rem единственно верное, если также имеется шлюз font-size в диапазоне от 24 до 40 пикселей. Здесь это не показано, но есть в исходном коде демки.
  2. Поскольку мы инвертируем значения контрольных точек, обе части дроби (100vw - 60rem) / (20 - 60) будут отрицательными для ширин области видимости меньше 60em и больше 20em (включительно). Например, в нижней контрольной точке при размере шрифта по умолчанию 16 пикселей получим -640px / -40. А если в дроби знаменатель и числитель отрицательные, то результат будет положительным, так что нам не нужно менять знак перед множителем 0.2.

Заключение


Краткие итоги. Мы рассмотрели две формы CSS-шлюзов:

  • для свойств, которые могут использовать размерности,
  • с примерами для font-size и line-height,
  • для контрольных точек, измеряемых в пикселях и em.

Определяющий фактор — тип контрольной точки. В большинстве проектов вы будете использовать одинаковые точки, скажем, для шлюза font-size и для изменений шаблонов. В зависимости от проекта или вашей привычки ключевые точки могут измеряться в пикселях или em. Лично я предпочитаю пиксели, но оба варианта имеют свои преимущества.

Напомню: если вы имеете дело с media-запросами в em, то избегайте размерностей в пикселях при определении размеров контейнеров. Также нельзя игнорировать базовый font-size элемента и можно использовать единственную форму CSS-шлюза:

@media (min-width: 20em) and (max-width: 60em) {
  selector {
    property: calc(
      baseline_value +
      multiplier *
      (100vw - 20rem) / (60 - 20)
    );
  }
}

Здесь multiplier — ожидаемое общее увеличение значения, выраженное в rem, но без размерности. Например: 0.75 для максимального увеличения на 0.75rem.

Если вы используете media-запросы в пикселях, то вы можете игнорировать базовый font-size элемента. Но тогда рекомендую процентные значения. Также можно применять две разные формы CSS-шлюзов. Первая аналогична em/rem-шлюзу, только со значениями в пикселях:

@media (min-width: 320px) and (max-width: 960px) {
  selector {
    property: calc(
      baseline_value +
      multiplier *
      (100vw - 320px) / (960 - 320)
    );
  }
}

Здесь multiplier — ожидаемое общее увеличение значения, выраженное в пикселях, но без размерности. Например: 12 для максимального увеличения на 12px.

Вторая форма шлюза при вычислении не настолько зависит от браузера. Всё, что можно, мы вычисляем самостоятельно, прежде чем передать эти значения браузеру:

@media (min-width: 320px) and (max-width: 960px) {
  selector {
    property: calc(
      baseline_value + 0.25vw - 10px;
    );
  }
}

Здесь значения 0.25vw и -10px вычислены заранее, вероятно, с помощью Sass или PostCSS.

Последняя форма может быть сложнее в реализации (если не использовать mixin), но благодаря более очевидным значениям способна облегчить проверку стилей и отладку.
Поделиться с друзьями
-->

Комментарии (35)


  1. ImKremen
    16.11.2016 14:44
    +2

    А почему при масштабировании шрифта идёт завязка на размер области просмотра (min-width), а не размер устройства (min-device-width)? Ведь размер шрифта должен зависеть от расстояния глаз пользователя до экрана, а не от того как сильно он сжал окно своего браузера.


    1. Turba
      16.11.2016 16:18
      +2

      Потому что это вопрос доступности. Если человек хочет смотреть в пол-экрана, значит на то есть причины.


      1. ImKremen
        16.11.2016 17:10
        +1

        Я и не спорю с тем что что пользователь может хотеть сделать ширину окна браузера меньше 100% ширины монитора (например 60% занимает браузер, а 40% скайп), но зачем заставлять такого пользователя напрягать зрение читая мелкий шрифт?


        1. Turba
          16.11.2016 17:41

          Так для того и сделаны «рамки» внутри которых этот шрифт пляшет.
          Никто не заставляет вас выставлять не комфортный размер шрифта в минимальном разрешении.
          Пользуйтесь с умом. Например, иногда бывает нужно, наоборот, увеличить шрифт в меньших разрешениях.


          1. ImKremen
            16.11.2016 19:20
            +1

            Ещё раз. Я ничего не имею против рамок, но зачем привязыватся к ширине вьюпорта, а не к ширине экрана?


            1. SelenIT3
              17.11.2016 02:16

              Т.е. вы предлагаете для случая «скукоженное окно на конском мониторе» использовать мобильную раскладку блоков (одна колонка, всего по минимуму и т.п.), но громадный шрифт «для чтения издали»? Что-то вроде мобильного вида при 200-процентном (или типа того) масштабе? Пожалуй, какое-то рациональное зерно в этой идее есть, но… не боитесь, что в этом окне только пара-тройка слов и поместится, и читать такое будет куда труднее, чем маленький текст в маленьком окне (придвинувшись чуть поближе)?


              1. ImKremen
                17.11.2016 11:50

                Тут хорошим примером могут быть приложения Windows 10 (Skype Preview, News, Mail). Да, в некоторых случаях от уменьшения размера заголовков никуда не деться, но как минимум к размеру базового шрифта это не относится.


                1. ImKremen
                  17.11.2016 11:57

                  К слову Interaction Media Features из Media Queries Level 4 уже на пороге, и на метод ввода (coarse/fine) вполне можно будет завязываться.


                  1. SelenIT3
                    17.11.2016 13:25

                    По поводу метода ввода раньше часто пугали граничными случаями, типа ноутов с сенсорным экраном и телевизоров с джойстиками а-ля трекпойнт/трекбол (и опциональным подключением к тому и к другому беспроводной мышки). Сейчас посмотрел в спеке, что это вроде бы разрулили разделением типа ввода на «primary» и «rare». Но всё равно как-то пока нет уверенности, что это покрывает все варианты.

                    Но сама идея свежая и дельная! Каких-то компромиссов, на мой взгляд, не избежать (как в примере с line-height заголовка из статьи), но сам этот сценарий как-то редко рассматривали отдельно, а и вправду ведь надо бы. Не возьметесь за статью?:)


                    1. ImKremen
                      17.11.2016 14:02

                      Подумываю о статье, но хочу для начала пару кейсов проработать с хорошими примерами.
                      Вот ещё наглядный пример из классической типографики. Ширина колонки не определяет размер шрифта основного текста.


    1. Finesse
      17.11.2016 03:34

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


      1. ImKremen
        17.11.2016 11:52

        Тут соглашусь, к размеру заголовка в экстремально узком режиме окна это вполне применимо, но не к размеру базового шрифта.


  1. dimka11
    16.11.2016 16:42
    -1

    calc, вроде бы не поддерживается на Safari для ios?! Или я не прав?


    1. ImKremen
      16.11.2016 17:25

      1. dimka11
        16.11.2016 18:10

        Далеко не у всех ios9


        1. ImKremen
          16.11.2016 19:28
          +1

          Если нажмете «Show all» вы уведите что Safari iOS поддерживает его ещё с версии 7.1 (6.1 c вендорным префиксом).


          1. dimka11
            17.11.2016 17:20

            Спасибо! Буду чаще пользоваться caniuse.



  1. Snorqq
    16.11.2016 19:33
    -1

    CSS-шлюзы... Неужели вживую встретил надмозга, занимающегося переводом интерфейса? CSS-lock можно понять, что это про ограничения (от «запирания» в рамках). Но шлюзы… это каким боком?


    1. AloneCoder
      16.11.2016 19:41
      +1

      Шлюз — это одно из значений слова «lock», и аналогия с этими шлюзами напрашивается, если вы прочитали статью

      In canal and river navigation, a lock is a device used for raising and lowering vessels between stretches of water that are at different levels. That’s exactly what our formula accomplishes. Our formula is a CSS calc “lock”.


      1. Snorqq
        16.11.2016 21:57

        и все равно остаюсь при своем мнении, что «шлюзы» — слишком громкое и торжественное название для такой мелочи. Я такие «шлюзы» каждые полчаса сочиняю, но никогда даже проблеска ассоциации с Панамским каналом не возникало)
        CSS-ограничители — и понятно, и без пафоса, и без странных взываний к навыкам игры в ЧтоГдеКогда.


        1. SelenIT3
          17.11.2016 02:37
          +1

          Простите, но, по-моему, притягивать за уши первое попавшееся в словаре значение слова к понравившейся лично вам трактовке, игнорируя контекст, историю и суть вопроса и мнение автора термина — как раз это и попахивает надмозгом. Автор термина говорит вовсе не про «ограничители», а про плавный переход между ними. Ему это напомнило изменение уровня воды в речном шлюзе, который он и выбрал в качестве метафоры, это его право. Метафора многим понравилась. И причем тут пафос?


          1. Snorqq
            21.11.2016 17:08

            шлюз — не конвертер. Шлюз что-то в себя принимает и без изменений выпускает с другой стороны.


            1. SelenIT3
              22.11.2016 00:19

              Но принимает на одном уровне, а выпускает на другом.


          1. DistortNeo
            22.11.2016 01:45

            Всё гораздо проще. CSS — это каскады, а там, где есть каскады, есть и шлюзы.


            1. SelenIT3
              22.11.2016 06:55

              По ассоциации возможно, но по смыслу нет. В CSS-каскаде как раз все переходы дискретные: либо одно значение, либо другое. Только в раннем черновике было что-то вроде плавного перехода.


  1. duhar
    16.11.2016 23:12

    html {
      font-size: 10px;
    }
    

    Никогда так не делайте!

    Поясните, пожалуйста, почему нельзя так делать?


    1. DistortNeo
      17.11.2016 10:03
      +1

      Поясните, пожалуйста, почему нельзя так делать?

      Присоединяюсь к вопросу.

      Возможно, это было дурным тоном лет 5-10 назад, когда браузеры имели возможность масштабировать шрифты, но не страницу целиком. Задание фиксированного размера шрифта не давало возможности читать мелкий текст. Но с современными браузерами, когда страница масштабируется целиком, нет никаких проблем использовать «жёсткую» вёрстку, привязанную к виртуальным пикселям.


    1. zgmnkv
      17.11.2016 12:35

      В браузерах есть возможность поменять базовый размер шрифта в настройках, но если шрифт на сайте задан жестко в px, то эта настройка ничего не поменяет


      1. DistortNeo
        17.11.2016 14:08

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


      1. ImKremen
        17.11.2016 14:35

        Меня всегда интересовал вопрос насколько это критично в реальной жизни. В Chrome для изменения размера шрифта нужно в расширенные настройки идти, а подавляющее большинство сайтов игнорируют эти самые настройки.
        Если уж и следовать такой практике, то стоит озаботится о корректном отображении страницы и тестировать дизайн для разных размеров шрифта, а иначе толку нет усложнять себе жизнь.

        По факту скоро мы получим достаточную поддержку CSS custom properties (CSS переменные) и сможем делать так:

        :root {
          --sbg: 4px; /*square baseline grid*/
        }
        html {
          font-size: calc(var(--sbg) * 4);
        }
        

        Затем можно устанавливать размер базового грида JS-ом ну и хранить настройки в localstorage.


  1. zgmnkv
    16.11.2016 23:47

    Мне всегда казалось что верным подходом является задание размеров шрифтов в px (ну или в rem если хотим позволить пользователю менять базовый размер), так как именно эта единица отражает угловой размер элемента в поле зрения пользователя. Т.е. в идеале шрифт будет одинаково смотреться как на большом экране так и на маленьком, как далеко от глаз пользователя, так и близко, как при большой так и при низкой плотности физических пикселей.
    Однако чтобы это работало, устройства должны корректно проставлять свойство devicePixelRatio с учетом плотности физических пикселей и среднего расстояния до глаз при пользовании устройством. Но к сожалению мало кто из производителей устройств заботится об этом.


    1. ImKremen
      17.11.2016 14:51

      devicePixelRatio не коим образом не влияет на размеры шрифта, а для определения расстояния до глаз достаточно завязки на ширину устройства, а не вьюпорта (мой комментарий выше). Ну ещё можно метод ввода добавить (touch/mouse).
      Ну а не следование рекомендаций относительно размера CSS пикселя остаётся на совести производителей (передадим привет Apple с их iPad mini).


      1. zgmnkv
        18.11.2016 23:32

        Как это не влияет? devicePixelRatio как раз определяет размер css пикселя, чем больше devicePixelRatio, тем больше css пиксель, тем больше размер шрифта если он задан в px.
        Это все косвенные методы определения расстояния до глаз, десктопный монитор может быть развернут вертикально и иметь ширину как у планшета, и с поддержкой touch. На самом деле простым разработчикам и не нужно этого делать, производители девайса, будь то монитор, телевизор, ноутбук, планшет, телефон, в кооперации с разработчиками браузеров, должны подумать про это за нас и проставить правильный devicePixelRatio. Или даже давать пользователю возможность поменять его для разных вариантов использования одного и того же устройства.
        А от разработчика просто требуется указать размер в px безо всяких ухищрений.

        Надеюсь картинка с w3c продемонстрирует наглядно что я имею ввиду.
        image


  1. scoff
    23.11.2016 09:16

    Демка Combined font-size and line-height lock как-то спотыкается в Safari на mac OS — она работает, но странно. При медленном уменьшении ширины окна значения залипают где-то на уровне 35-38px, при быстром проскакивают до меньших значений. Если перегрузить страницу — работает (пересчитывает). В Chrome все нормально вроде бы.