Математика CSS-шлюзов

Перевод статьи The math of CSS locks с сайта fvsch.com для CSS-live.ru, автор — Флоран Вершельд

CSS-шлюз — приём в отзывчивом дизайне, позволяющий сделать плавный переход между двумя значениями, в зависимости от текущего размера окна браузера, вместо резкого «перескока» с одного значения на другое.

Эту идею, вместе с одной из реализаций, предложил Тим Браун в статье «Гибкая типографика с помощью CSS-шлюзов»

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

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

Оглавление

  1. Что такое CSS-шлюз?
  2. CSS-шлюзы с контрольными точками в пикселях
  3. CSS-шлюзы с контрольными точками в em-ах
  4. Заключение

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

Размеры относительно окна браузера

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

Ранние подходы для этого выглядели примерно так:

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

У этого было два недостатка:

  1. Текст становится совсем мелким на маленьких экранах (12.8 пикселей при 320px) и очень крупным на больших (64px при 1600px);
  2. Он не реагирует на пользовательские настройки для размера шрифта.

Техники CSS-шлюзов призваны исправить первый пункт. Отличные техники CSS-шлюзов стараются исправить и второй: учет пользовательских предпочтений.

Понятие CSS-шлюза

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

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

px-fontsize-complete

«Пусть у нас font-size будет 20px при 320px и меньше, 40px при 960px и выше, а в промежутке его значение меняется от 20px до 40px».

В CSS это может выглядеть так:

h1 { font-size: 1.25rem; }

@media (min-width: 320px) {
  h1 { font-size: /* магическое значение от 1.25rem  до 2.5rem */; }
}

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

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

h1 {
  font-size: calc(1.25rem + значение_относительно_окна);
}

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

Ограничения

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

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

(У Тима в статье есть ошибка, где он пишет, что выражение типа 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. Мы можем отметить наши две точки на графике и провести через них линию:

px-fontsize-linear

То, что здесь выделено красным — простая линейная функция. Можно записать ее в виде y = mx + b, где:

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

Наша задача — выяснить значения m и b соответственно. Они — неизменные части этого уравнения.

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

m = приращение_размера_шрифта / приращение_области_просмотра

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

Этот же расчет можно представить по-другому:

  • Полное приращение font-size равно 20px (40 — 20).
  • Полное приращение ширины окна равно 640px (960 — 320).
  • Если ширину окна увеличить всего на 1px, на какую величину увеличится font-size? На 20 / 640 = 0.03125px.

Теперь вычислим 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

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

Как бы то ни было, у нас получилась вот такая линейная функция:

y = 0.03125x + 10

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

Как нам преобразовать нашу функцию в синтаксис CSS? Мы знаем, что y — это font-size, и что для основных арифметических действий в CSS нам понадобится calc().

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

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

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

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

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

Разумеется, мы хотим применять этот стиль только для размеров окна от 320px до 960px. Так что добавим пару медиавыражений:

h1 { font-size: 20px; }

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

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

И теперь график у нас выглядит как тот, что показан во введении.

px-fontsize-complete-1

Это славно, но меня не очень радуют эти пиксельные значения для размера шрифта: нельзя ли сделать лучше?

Учитываем пользовательские предпочтения

Практически в любом браузере пользователь может выбрать более мелкий или более крупный текст по умолчанию. Типичное значение по умолчанию — 16px, но пользователи могут поменять его на что угодно (как правило, на более крупный шрифт).

Я хотел бы ввести это пользовательское предпочтение в нашу формулу, и собираюсь для этого воспользоваться значениями в rem. Обратите внимание, что те же принципы подходят и к значениям в em или процентах.

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

html {
  font-size: 10px;
}

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

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

Учитывая это, мы собираемся вообще не трогать font-size корневого элемента, так что по умолчанию он будет равен 16px. Посмотрим, что получится, если в нашем шлюзе для 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; }
}

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

Но раз мы делаем это ради поддержки пользовательских изменений, надо проверить в действии и это. Скажем, наш пользователь настроил свой браузер так, что font-size по умолчанию у него не 16px, а 24px (увеличен на 50%): как отреагирует вышеприведенный код? Давайте построим график:

px-fontsize-bigger-buggy

Пунктирная синяя линия: с базовым  font-size в 16px.

Сплошная красная линия: с базовым  font-size в 24px.

В контрольной точке 320px размер шрифта оказывается меньше (перескакивая с 30px на 25px), и есть большой перескок в верхней контрольной точке (с 45px на 60px). Ой.

Чтобы это исправить, можно использовать одно и то же базовое значение, настраиваемое пользователем, для всех 3 размеров. Например, можем взять базовое значение 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′. В нашем случае, раз мы знаем, что наше базовое значение эквивалентно 20px, можно получить значение b′ простым вычитанием:

b′ = b - baseline_value
b′ = 10 - 20
b′ = 10

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

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, и выглядит это так:

px-fontsize-increase

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

px-fontsize-bigger-fixed

Штриховая пурпурная линия: только увеличение  font-size.

Пунктирная синяя линия: с базовым font-size в 16px.

Сплошная красная линия: с базовым font-size в 24px.

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

Делаем шлюз для line-height

В этой части мы будем рассматривать такой сценарий: «нам нужно, чтобы line-height у абзацев был равен 140% при 320px и 180% при 960px».

Поскольку мы будем работать с базовым значением с динамическим значением в пикселях в придачу, нам нужно знать, скольким пикселям соответствуют эти дроби 1.4 и 1.8. А значит, нам понадобится знать font-size наших абзацев. Допустим, у наших абзацев font-size по умолчанию, т.е., скорее всего, 16px. Опорные точки у нас такие:

  • 16 * 1.4 = 22.4 пикселя в нижней контрольной точке (320px)
  • 16 * 1.8 = 28.8 пикселя в верхней контрольной точке (960px)

Также мы возьмем 140% = 22.4px в качестве базового значения. Так что нужная нам отсюда информация — приращение на 6.4px. Можно построить линейную формулу, как раньше:

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().

Затем мы добавим медиавыражения и убедимся, что все объявления для значения 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%, результат будет правильным для базового размера шрифта в 16px, но испортится, если пользователь его изменит.

Можно построить график нашей функции и проверить, что она работает с разными базовыми font-size.

px-lineheight-complete

Пунктирная синяя линия: с базовым font-size в 16px.

Сплошная красная линия: с базовым font-size в 24px.

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

.big {
  font-size: 166%;
}

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

  • 16 * 1.66 * 1.4 = 37.184 пикселя в нижней контрольной точке (320px)
  • 16 * 1.66 * 1.8 = 47.808 пикселя в верхней контрольной точке (60px)

Можно провести вычисления и получить вот такую доработанную формулу: 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-height-ы, что и для стандартных абзацев, нам просто нужно добавить множитель 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

Хорошо, давайте попробуем собрать это всё вместе. Вот наш сценарий: у нас есть резиновая колонка текста с H1 и несколькими абзацами, и мы собираемся изменить font-size и line-height, используя следующие значения:

Элемент и свойство Значение при 320px Значение при 960px
H1 font-size 24px 40px
H1 line-height 133.33% 120%
P font-size 15px 18px
P line-height 150% 166.67%

Вы наверняка заметите, что мы производим два разных действия над высотой строк. Как правило, когда шрифт увеличивается, line-height следует уплотнять, а когда колонка расширяется, line-height принято увеличивать, чтобы строки стали пореже. Но в нашем случае это происходит одновременно, и эти два принципа противоречат друг другу! Поэтому нам придется выбирать, какой из аспектов важнее:

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

Теперь давайте выберем две контрольные точки. Я снова возьму 320px и 960px, ура. Начнем с того, что запишем шлюзы для 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 to 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%, что можно округлить до 32px.

Нам нужна линейная функция, описывающая «то, что мы добавим к базовому значению 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 );
}

Давайте взглянем на нашу функцию на графике, и посмотрим, как она соотносится с соответствующей функцией font-size.

px-combined-h1

Пунктирная синяя линия: уменьшение line-height

Штриховая красная линия: наше базовое значение line-height (120% от font-size заголовка).

Сплошная пурпурная линия: итоговый line-height.

На этом графике мы видим, что итоговый line-height (пурпурная линия) равен 120% базового значения (красные штрихи) плюс увеличение line-height (синий пунктир). Вы можете посмотреть на эти уравнения на GraphSketch.com и проверить сами.

Для абзацев мы будем использовать 150% в качестве нашего базового значения. Увеличение line-height, которое нам нужно, вот такое: (1.75 - 1.5) × 18 = 4.5px.

px-combined-soulver

Мой калькулятор выдает мне следующую формулу: 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) );
  }
}

Занятное совпадение, что такую же формулу использовал Тим Браун в своей «Гибкой типографике с помощью CSS-шлюзов», только у нас в переменной части пиксели, а не 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. К сожалению, сейчас у меня нет готового примера, но если вы сами готовы взяться за это, дайте мне знать, я добавлю ваш пример в статью.

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

Обновленные примеры

Я взял три наших первых примера и переделал их так, что вместо пикселей для контрольных точек и приращений значения теперь для контрольных точек используются em-ы, а для приращений значения — rem-ы.

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

Не используйте медиавыражения в em-ах при m × 100vw

Помните синтаксис m × 100vw, который мы использовали во втором разделе (к примеру, в коде наподобие calc(base + 2.5vw))? Это нельзя использовать с медиавыражениями, основанными на em.

Дело в том, что в случае медиавыражений обе единицы em и rem обозначают одно и то же: базовый размер шрифта браузера. Который — как мы уже много раз отмечали — обычно равен 16px, но может быть меньше или больше в зависимости от двух вещей:

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

Это значит, что, если у нас есть две контрольные точки в 20em и 60em, фактически они будут соответствовать следующим ширинам в CSS:

  • 320px и 960px для базового размера шрифта 16px.
  • 480px и1440px для базового размера шрифта 24px.
  • и т.д.

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

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

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

Если мы возьмём этот синтаксис и переделаем все контрольные точки на em-ы, предполагая, что 1em в медиавыражениях равен 16px, то у нас получится код вроде этого:

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 ); }
}

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

em-fontsize-px

Пунктирная синяя линия: результат при базовом размере шрифта 16px.

Сплошная красная линия: результат при базовом размере шрифта 24px

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

  • При 320px, 3.125vw — 10px равно 0px, как запланировано.
  • Но при 480px, 3.125vw — 10px равно 5px.

В верхней контрольной точке всё ещё хуже:

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

Если мы хотим использовать контрольные точки в em-ах, нам понадобится другой подход.

Еще раз посчитаем

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

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

Формула, которую мы будем использовать:

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

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

y = mx + b

В CSS мы можем работать с x (это 100vw). Но мы не можем вычислить m и b как точные значения в px или 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′ = максимальное_приращение_значения × (x - x1) / (x2 - x1)

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

Теперь мы можем использовать это значение в CSS. Возвращаясь к нашему примеру «от 20px до 40px», мы можем записать его как:

@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 равен 16px, а ширина области просмотра — 600px, то результат будет 280px (600 - 20 × 16). Если базовый font-size равен 24px а ширина области просмотра — 600px, то результат будет 120px (600 - 20 × 24).

em-moving-breakpoint

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

В идеале, нам нужна была бы единица CSS, которая ссылается на базовый font-size браузера, но такой единицы не существует. Самое близкое к этому, что у нас есть — это 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.

Например, при базовом font-size в 16px и ширине окна 600px мы получили бы:

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

К сожалению, это так не работает.

Главная причина в том, что нельзя использовать пиксели, и вообще единицы измерения CSS, в делителе при делении в calc(). (Делитель — это то, что справа. Не беда, если вы этого не помните из школьных знаний, мне самому вот пришлось заглянуть в справочник.) Делить можно только на безразмерную величину. Какие же у нас тут варианты?

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

При базовом font-size в 16px
Ширина окна Деление в 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
При базовом font-size в 24px
Ширина окна Деление в 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. Этим можно воспользоваться!

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

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

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

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

Нам удалось получить лишь пиксельное значение, эквивалентное 0rem и 1rem; давайте назовем это значение r.

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

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

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

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

Это должно работать как надо, но заметьте, что значение r будет меняться в зависимости от базового font-size.

  • При базовом значении 16px, 1.25 * r будет значением между 0px и 20px.
  • При базовом значении 24px, 1.25 * r будет значением между 0px и 30px.

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

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 на основе px, в этот раз, когда пользователь увеличивает базовый font-size на 50%, всё увеличивается на 50%: базовое значение, переменная часть и контрольные точки. Мы получим диапазон 30px–60px вместо диапазона 20px–40px по умолчанию.

em-fontsize-bigger

Пунктирная синяя линия: результат при базовом размере шрифта 16px.

Сплошная красная линия: результат при базовом размере  шрифта 24px.

Можете проверить это поведение в нашем первом примере с em-ами.

Шлюзы для line-height с em/rem

Во втором примере мы хотим менять line-height абзаца между 140% и 180%. Мы используем 140% в качестве базового значения, а для переменной части используем ту же формулу, что в примере с the 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, увеличение line-height на 40%, которое мы ищем, соответствует .4rem. Это и будет тем значением, которое мы используем в наших двух выражениях calc().

Теперь посмотрим на пример с line-height из нашего третьего демо. Мы хотим, чтобы значение line-height у H1 уменьшалось от 133.33% до 120%. Мы также знаем, что одновременно будет меняться его font-size.

Для этого самого примера во втором разделе мы выяснили, что это уменьшение line-height можно выразить через две опорные точки:

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

Поэтому используем базовое значение 120% и переменную часть от 3.2px до 0px. При базовом размере шрифта 16px 3.2px равно 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, меняющий значение от 24px до 40px (это не показано здесь, но видно в исходнике примера).
  2. Поскольку мы меняем местами значения контрольных точек, при любой ширине окна в пределах от 20em включительно до 60em и делимое, и делитель в выражении (100vw - 60rem) / (20 - 60) будут отрицательными. Например, в нижней контрольной точке и при базовом размере шрифта 16px оно равно 640px / -40. И так как минус, деленный на минус, дает плюс, нам не нужно менять знак перед множителем 0.2.

Заключение

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

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

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

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

@media (min-width: 20em) and (max-width: 60em) {
  selector {
    property: calc(
      базовое_значение+
      множитель *
      (100vw - 20rem) / (60 - 20)
    );
  }
}

Где множитель — это ожидаемое полное увеличение значения в rem, без единицы измерения (например: 0.75 для максимального увеличения 0.75rem).

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

@media (min-width: 320px) and (max-width: 960px) {
  selector {
    property: calc(
      базовое_значение+
      множитель *
      (100vw - 320px) / (960 - 320)
    );
  }
}

Где множитель — это ожидаемое полное увеличение значения в px, без единиц измерения (например: 12 для максимального увеличения 12px).

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

@media (min-width: 320px) and (max-width: 960px) {
  selector {
    property: calc(
      базовое_значение + 0.25vw - 10px;
    );
  }
}

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

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

P.S. Это тоже может быть интересно:

1 Комментарий

  1. Алексей

    Очень познавательная статья. Все подробно расписано. Спасибо за перевод!
    ЗЫ: еще я понял зачем нужно было учить математику в школе )

Оставить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Получать новые комментарии по электронной почте. Вы можете подписаться без комментирования.