Уверен, что никому не нужно объяснять почему сайты должны быть гибкими и адаптивными. Все используют проценты и медиа-запросы в своей верстке. Сейчас это уже стандарт.


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


Но потом появились они — vw, vh, vmin, vmax — единицы измерения, которые базируются на viewport. У нас появился шанс на отзывчивую типографику.



Коротко о viewport-заисимых единицах


100vw равно ширине (100vh — высоте) экрана в пикселях (или будет приведено к другим величинам если использовать в calc()). 100vmax — равно большему из пары высота/ширина, 100vmin — меньшему. Сразу же начали появляться рецепты использования этих чудо величин в типографике.


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


Задача


Хотелось иметь универсальный рецепт, который позволял бы задавать минимальное и максимальное значения для размера шрифта, да еще и привязать эти цифры к конкретным размерам viewport. Например, при ширине экрана в 480px иметь ровно 16px, а при 1280px24px.


Путь к решению


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



На оси X — ширина окна браузера, на оси Y — наш размер шрифта. Получаем уравнение с двумя неизвестными:


{x-x_1\over x_2-x_1} = {y-y_1\over y_2-y_1}


откуда


y = {x-x_1\over x_2-x_1}(y_2-y_1)+y_1


y — наш неизвестный размер шрифта, x — текущий размер экрана. x1 — минимальное значение шрифта, x2 — максимальное значение шрифта. y1 и y2 минимальное и максимальные значение ширины экрана соответственно.


А теперь самое интересное: имея CSS функцию calc() и текущее значение вьюпорта благодаря vw можно получить динамический размер шрифта:


font-size: calc( (100vw - Vmin)/(Vmax - Vmin) * (Fmax - Fmin) + Fmin);

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


font-size: calc( (100vw - 480px)/(1280 - 480) * (24 - 16) + 16px);

Единицы измерений можно использовать любые, главное, чтобы они совпадали. Тоже самое, только в rem:


font-size: calc( (100vw - 30rem)/(80 - 30) * (1.5 - 1) + 1rem);

Используем Sass/SCSS


Имея под рукой такой инструмент как Sass можно большую часть вычислений спрятать от браузера.
Для этого больше подойдет следующее уравнение прямой:


y=kx+b


Коеффициенты k и b можно получить из уравнения прямой по двум точкам:


k={y_2-y_1\over x_2-x_1}


b=y_1-{y_2-y_1\over x_2-x_1}x_1=y_1-kx_1


Получим функцию:


@function calcFluidFontSize($f-min, $f-max, $v-min, $v-max) {
  $k: ($f-max - $f-min)/($v-max - $v-min);
  $b: $f-min - $k * $v-min;

  $b: $b * 1px;

  @return calc( #{$k} * 100vw + #{$b} );
}
.fluid-font-size {
  font-size: calcFluidFontSize(16, 48, 480, 1280);
}

В случае, если мы хотим иметь возможность использовать не только px но и другие единицы измерений, можно передавать их одним из параметров:


@function calcFluidFontSize($f-min, $f-max, $v-min, $v-max, $units: px) {
  $k: ($f-max - $f-min)/($v-max - $v-min);
  $b: $f-min - $k * $v-min;

  $b: $b + $units;

  @return calc( #{$k} * 100vw + #{$b} );
}

.fluid-font-size {
  font-size: calcFluidFontSize(1, 3, 30, 80, rem);
}

Для защиты от неверно введенных данных, можно обрезать единицы измерений при получении данных. Для этого можно использовать функцию strip-unit или взять ее из библиотеки bourbon:


@function strip-unit($number) {
  @if type-of($number) == 'number' and not unitless($number) {
    @return $number / ($number * 0 + 1);
  }

  @return $number;
}

@function calcFluidFontSize($f-min, $f-max, $w-min, $w-max, $units: px) {
  $f-min: strip-unit($f-min);
  $f-max: strip-unit($f-max);
  $w-min: strip-unit($w-min);
  $w-max: strip-unit($w-max);

  $k: ($f-max - $f-min)/($w-max - $w-min);
  $b: $f-min - $k * $w-min;

  $b: $b + $units;

  @return calc( #{$k} * 100vw + #{$b} );
}

Расширяем возможности нашей функции


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


Например, мы хотим фолбек для старых браузеров:


@mixin fluidFontSize($f-min, $f-max, $w-min, $w-max, $fallback: false) {
  @if ($fallback) {
    font-size: $fallback;
  }
  font-size: calcFluidFontSize($f-min, $f-max, $w-min, $w-max, px);

}

.fluid-font-size {
  @include fluidFontSize(16px, 24px, 480px, 1280px, 18px);
}

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


Можно ограничить размеры шрифта нашим минимальным и/или максимальным значением:


@mixin fluidFontSize($f-min, $f-max, $w-min, $w-max, $fallback: false) {

  font-size: $f-min;

  @media (min-width: $w-min) {
    @if ($fallback) {
      font-size: $fallback;
    }
    font-size: calcFluidFontSize($f-min, $f-max, $w-min, $w-max, px);  
  }
  @media (min-width: $w-max) {
    font-size: $f-max;
  }
}

.fluid-font-size {
  @include fluidFontSize(16px, 24px, 480px, 1280px, 18px);
}

Теперь при ширине экрана меньше 480px шрифт всегда будет 16px, после 480 он станет резиновым, а после 1280px всегда будет 24px.


Результат


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


@function calcFluidFontSize($f-min, $f-max, $w-min, $w-max, $units: px) {
  $f-min: strip-unit($f-min);
  $f-max: strip-unit($f-max);
  $w-min: strip-unit($w-min);
  $w-max: strip-unit($w-max);

  $k: ($f-max - $f-min)/($w-max - $w-min);
  $b: $f-min - $k * $w-min;

  $b: $b + $units;

  @return calc( #{$k} * 100vw + #{$b} );
}

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


Почитать


» The Lengths of CSS
» REM vs EM – The Great Debate
» Viewport Sized Typography
» Truly Fluid Typography With vh And vw Units
» Viewport Unit Based Typography

Поделиться с друзьями
-->

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


  1. inoyakaigor
    16.09.2016 14:45

    font-size: calc( (100vw - 30rem)/(80 - 30) * (1.5 - 1) + 1rem);
    

    можно сократить до
    font-size: calc( (100vw - 30rem)/50 * 0.5 + 1rem);
    

    который в свою очередь есть
    font-size: calc( (100vw - 30rem)/25 + 1rem);
    


    И тут, кстати, мы получаем новое магическое число: 25


    1. Aingis
      16.09.2016 14:56
      +1

      Можно пойти дальше:

      font-size: calc(4vw - 1.2rem + 1rem);

      И получаем линейную функцию вида y = kx + b:

      font-size: calc(4vw - 0.2rem);

      что логично, это же линейная функция относительно размера экрана.


    1. alexxisr
      16.09.2016 15:07
      +2

      если следовать нормальным приоритетам операций умножения/деления, то получим таки:
      font-size: calc( (100vw — 30rem)/100 + 1rem);


      1. inoyakaigor
        16.09.2016 15:36

        Я в толк не возьму: как вы 100 получили?


        1. alexxisr
          16.09.2016 16:10

          a/50*0.5 = a*0.5/50 = a/(2*50) = a/100
          — там нет скобок вокруг умножения, так что умножается числитель, а не знаменатель


          1. inoyakaigor
            16.09.2016 16:47

            Посыпаю голову пеплом. Совсем школьную математику забыл


            1. alexxisr
              16.09.2016 17:09
              +1

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


        1. pudovMaxim
          16.09.2016 16:38

          x/50 * 0.5 => x/50 * 1/2 => (x * 1) / (50 * 2) => x/100
          Для проверки в своих выражения подставьте например 100 вместо (100vw — 30rem).
          100/50 * 0.5 = 1
          100/25 = 4


        1. yuratim
          16.09.2016 16:38

          Очень просто a / b * c интерпретируется, как поделить a на b, и результат умножить на c.


    1. ROBsoer
      16.09.2016 15:09

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


  1. ivan386
    16.09.2016 15:33

    Спасибо что напомнили о существовании vmax и vmin. Я при помощи скрипта vw на vh менял.


  1. dom1n1k
    16.09.2016 16:23

    Видимо, довольно много людей совершило это открытие независимо друг от друга.
    С тех пор как я начал использовать аналогичный подход (только у меня vmin и px, но это нюансы), вижу уже где-то третью заметку на эту тему.


    1. janitor
      16.09.2016 22:14

      Ага, Виталий Фридман неделю назад рассказывал точь-в-точь о такой же формулe на Nordic.js