Математика CSS-шлюзов
Перевод статьи The math of CSS locks с сайта fvsch.com для CSS-live.ru, автор — Флоран Вершельд
CSS-шлюз — приём в отзывчивом дизайне, позволяющий сделать плавный переход между двумя значениями, в зависимости от текущего размера окна браузера, вместо резкого «перескока» с одного значения на другое.
Эту идею, вместе с одной из реализаций, предложил Тим Браун в статье «Гибкая типографика с помощью CSS-шлюзов»
Когда я вникал в реализацию Тима и пробовал свои варианты, мне было нелегко разобраться, что именно там происходит. По ходу дела я набросал кучу формул, и решил, что будет полезно поделиться пояснением математической части.
Я опишу сам приём, его ограничения, и математику, на которой он строится. Но не пугайтесь насчет математики: по сути это сложение и умножение, к тому же я расписал всё по шагам подробнее некуда, а еще будут симпатичные графики.
Оглавление
- Что такое CSS-шлюз?
- CSS-шлюзы с контрольными точками в пикселях
- CSS-шлюзы с контрольными точками в em-ах
- Заключение
Что такое CSS-шлюз?
Размеры относительно окна браузера
В последнем проекте у меня был баннер во всю ширину с заголовком сверху, причем макет был только для настольных ПК, с крупным шрифтом. Я решил, что мне нужен шрифт помельче для маленьких экранов и что-то промежуточное для средних. Почему бы не сделать шрифт зависимым от ширины окна?
Ранние подходы для этого выглядели примерно так:
h1 { font-size: 4vw; /* Бац! Готово. */ }
У этого было два недостатка:
- Текст становится совсем мелким на маленьких экранах (12.8 пикселей при 320px) и очень крупным на больших (64px при 1600px);
- Он не реагирует на пользовательские настройки для размера шрифта.
Техники CSS-шлюзов призваны исправить первый пункт. Отличные техники CSS-шлюзов стараются исправить и второй: учет пользовательских предпочтений.
Понятие CSS-шлюза
CSS-шлюз — такой способ вычисления CSS-значения, при котором:
- есть минимальное и максимальное значения,
- есть две контрольные точки (обычно отталкивающиеся от ширины окна)
- и между этими контрольными точками фактическое значение линейно меняется от минимума до максимума.
«Пусть у нас 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-шлюз с calc для font-size (rem+px, медиавыражение в px)
- CSS-шлюз с calc для line-height (%+px, медиавыражение в px)
- Объединенный шлюз для font-size и line-height (на основе px)
В ближайших подразделах мы покажем, как мы получили CSS-код для каждого из примеров.
Размер шрифта как линейная функция
Нам нужно, чтобы font-size
пропорционально увеличивался между двумя точками: 20px при 320px и 40px при 960px. Мы можем отметить наши две точки на графике и провести через них линию:
То, что здесь выделено красным — простая линейная функция. Можно записать ее в виде 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; } }
И теперь график у нас выглядит как тот, что показан во введении.
Это славно, но меня не очень радуют эти пиксельные значения для размера шрифта: нельзя ли сделать лучше?
Учитываем пользовательские предпочтения
Практически в любом браузере пользователь может выбрать более мелкий или более крупный текст по умолчанию. Типичное значение по умолчанию — 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%): как отреагирует вышеприведенный код? Давайте построим график:
Пунктирная синяя линия: с базовым 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
, и выглядит это так:
С нашим базовым значением в rem
и дополнительными размерами в vw
и/или px
мы в итоге получили полностью работающий шлюз для font-size
. Когда пользователь меняет свой базовый font-size
, вся конструкция поднимается или опускается и при этом не ломается. Получилось!
Штриховая пурпурная линия: только увеличение 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.
Пунктирная синяя линия: с базовым 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.
Пунктирная синяя линия: уменьшение 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
.
Мой калькулятор выдает мне следующую формулу: 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
-ы.
- CSS-шлюз с calc для font-size (rem+rem, медиавыражение в em)
- CSS-шлюз с calc для line-height (%+rem, медиавыражение в px)
- Комбинированный шлюз font-size и line-height (основанный на em/rem)
В следующих подразделах мы опишем, как именно работает синтаксис, использованный для этих примеров.
Не используйте медиавыражения в em
-ах при m × 100vw
Помните синтаксис m × 100vw
, который мы использовали во втором разделе (к примеру, в коде наподобие calc(base + 2.5vw))
? Это нельзя использовать с медиавыражениями, основанными на em.
Дело в том, что в случае медиавыражений обе единицы em и rem обозначают одно и то же: базовый размер шрифта браузера. Который — как мы уже много раз отмечали — обычно равен 16px, но может быть меньше или больше в зависимости от двух вещей:
- Выбора браузера или ОС (в основном для особых случаев вроде браузеров в телевизорах и некоторых электронных книг).
- Предпочтения пользователя.
Это значит, что, если у нас есть две контрольные точки в 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 ); } }
Это, конечно, работало бы, если бы операционная система, браузер и пользователь никогда не меняли базовый размер шрифта. Но едва он по какой-либо причине окажется другим, как тут же начнется хаос.
Пунктирная синяя линия: результат при базовом размере шрифта 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)
.
Заметьте, что мы выражаем наши контрольные точки в единицах 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))
?
Ширина окна | Деление в 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 |
Ширина окна | Деление в 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 по умолчанию.
Пунктирная синяя линия: результат при базовом размере шрифта 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%; } }
Два момента, которые тут надо отметить:
- Значение
.2rem
верно только в том случае, если у нас есть еще и шлюз для font-size, меняющий значение от 24px до 40px (это не показано здесь, но видно в исходнике примера). - Поскольку мы меняем местами значения контрольных точек, при любой ширине окна в пределах от
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. Это тоже может быть интересно:
Очень познавательная статья. Все подробно расписано. Спасибо за перевод!
ЗЫ: еще я понял зачем нужно было учить математику в школе )
Вот у Вас в анналах, чего только не нароешь! Интересная идея! Благодарю за перевод :)
Вот у Вас в анналах, чего только не нароешь! Интересная идея! Благодарю за перевод :)
Автор, ты предлагаешь над каждым проектом выводить эти графики-нафики?!
Переводчик вряд ли сможет ответить за автора (оригинала), лучше спросить у него самого (в начале статьи есть ссылка на его твиттер). Но вообще графики тут служат только иллюстрациями к формулам, чтобы было понятнее, что в них происходит. Когда уже ясно, что куда подставлять (и почему), графики больше не нужны:)
Решение в реализации может быть и проще? если использовать любую программу которая может построить график на основе таблицы(Excel и т.п.) с данными(размер viewport соответствующее значение к примеру шрифта) и вывести линейную линию тренда(это к слову о сложностях расчета). В результате мы увидим какое то выражение, к примеру y = 0,0045x + 12,571. Осталось только перевести его в CSS. Y — в данном примере шрифт(данные из реального проекта), значение на viewport от 768 до 320px. X — значение 100vw(ширина viewport текущая ). Первое действие 0,0045x сразу вычисляем(благо умножить на 100 не трудно) — 0.45vw(не забыть единицы измерения). В итоге у нас получается вот такой результат: «font-size: calc(0.45vw + 12.571px)». Подставим значения, viewport — 320px: 1vw=(320px/100) = 3.2px, а у нас 0,42vw то есть 3.2 * 0.45 = 1.44px добавим коэффициент из формулы 1.44 + 12.571 = 14.011px(сотки после запятой результаты округлений). Данный формула линейно меняла шрифт от 14 на 320px до 16 — 768px.
Удобный сервис для получения готового значения в CSS
https://modern-fluid-typography.vercel.app/