Адаптивная типографика с помощью математики

в 15:06, , рубрики: accessibility, css, html, usability, адаптивная типографика, веб-дизайн, интерфейсы, типографика

Адаптивный (резиновый) дизайн является нормой фронтэнд-разработки уже давно. Однако идея гибкой адаптивной типографики является относительно новой, которую еще предстоит изучить. Вплоть до недавнего времени реализация гибкой типографики сводилась к простому использованию Viewport, возможно, с учетом минимальных и максимальных значений.

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

image

Самое приятное то, что вы можете автоматизировать все это с помощью Sass.

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

  • H1 для маленького размера равен 22 пикселя
  • H1 для среднего размера равен 24 пикселя
  • H1 для маленького размера равен 34 пикселя

Для реализации используются media queries:

h1 {
  font-size: 22px;
}
@media (min-width:576px) {
  h1 {
    font-size: 22px;
  }
}
@media (min-width:768px) {
  h1 {
    font-size: 24px;
  }
}
@media (min-width:992px) {
  h1 {
    font-size: 34px;
  }
}

Это неплохое решение, но недостатком является то, что при изменении размера окна, размер шрифта меняется резко.

image

Хорошо, если бы была возможность сделать этот переход плавным. Можно использовать transition:

h1 {
  font-size: 22px;
  transition: font-size 0.2s;
}

image

Переход не так бросается в глаза, но все еще отчетливо виден. Что еще можно с этим сделать?

Viewport

Viewport позволяет плавно изменять размер шрифта. К тому же этот способ хорошо поддерживается браузерами.

image

Но жизнеспособность Viewport слишком зависит от изначального дизайна страницы. Было бы отлично просто назначать font-size, используя vw:

h1 {
  font-size: 2vw;
}

image

Но это работает, только если артборды от дизайнера учитывают это. Выбрал ли дизайнер размер текста, который составляет ровно 2% ширины каждой из его артбордов? Конечно нет.

Давайте вычислим, какое значение vw необходимо для каждого случая:

  • Размер 22px @ ширина 576px = 22/576*100 = 3.82vw
  • Размер 24px @ ширина 768px = 24/768*100 = 3.13vw
  • Размер 34px @ ширина 992px = 34/992*100 = 3.43vw

Они близки, но не одинаковы. Таким образом, все равно придется использовать media queries для смены размеров текста, и все равно будут переходы. И нельзя забывать о странном побочном эффекте:

image

Так как же решить проблему?

Статистическая линейная регрессия

Что? Да, это статья о CSS, но использование основ математики поможет нам найти элегантное решение. Во-первых, давайте построим графики для соответствующих размеров текста:

image

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

Если бы вы могли установить свой размер шрифта в соответствии с этой линией тренда, у вас был бы заголовок H1, который плавно масштабировался бы при любом размере экрана. д

Давайте посмотрим на математику. Прямая определяется простым уравнением:

image

Существуют несколько методов определения m и b. Распространенным методом является подбор по методу наименьших квадратов:

image

Как использовать все это в CSS?

Ответом будет calc()! Это довольно новая CSS технология, но уже хорошо поддерживаемая браузерами:

image

Уравнение выглядит так:

h1 {
  font-size: calc({slope}*100vw + {y-intercept}px);
}

Можно ли это автоматизировать?

Метод подгонки наименьших квадратов можно перевести в простую в функцию Sass:

/// least-squares-fit
/// Calculate the least square fit linear regression of provided values
/// @param {map} $map - A Sass map of viewport width and size value combinations
/// @return Linear equation as a calc() function
/// @example
///   font-size: least-squares-fit((576px: 24px, 768px: 24px, 992px: 34px));
/// @author Jake Wilson <jake.e.wilson@gmail.com>
@function least-squares-fit($map) {
  
  // Get the number of provided breakpoints
  $length: length(map-keys($map));
  
  // Error if the number of breakpoints is < 2
  @if ($length < 2) {
    @error "leastSquaresFit() $map must be at least 2 values"
  }
    
  // Calculate the Means
  $resTotal: 0;
  $valueTotal: 0;
  @each $res, $value in $map {
    $resTotal: $resTotal + $res;
    $valueTotal: $valueTotal + $value;
  }
  $resMean: $resTotal/$length;
  $valueMean: $valueTotal/$length;

  // Calculate some other stuff
  $multipliedDiff: 0;
  $squaredDiff: 0;
  @each $res, $value in $map {
    
    // Differences from means
    $resDiff: $res - $resMean;
    $valueDiff: $value - $valueMean;
    
    // Sum of multiplied differences
    $multipliedDiff: $multipliedDiff + ($resDiff * $valueDiff);
    
    // Sum of squared resolution differences
    $squaredDiff: $squaredDiff + ($resDiff * $resDiff);
  }

  // Calculate the Slope
  $m: $multipliedDiff / $squaredDiff;

  // Calculate the Y-Intercept
  $b: $valueMean - ($m * $resMean);

  // Return the CSS calc equation
  @return calc(#{$m*100}vw + #{$b});

}

Это действительно работает? Откройте пример на CodePen и измените размер окна браузера. Работает! Размеры шрифта довольно близки к тому, что требовалось для первоначального дизайна, и они плавно масштабируются вместе с макетом.

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

Полиномиальная регрессия

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

image

Теперь это больше похоже на то, что нам нужно! Гораздо точнее нашей прямой линии.
Уравнение основной полиномиальной регрессии выглядит так:

image

Чем точнее ваша кривая, тем сложнее получается уравнение. К сожалению, вы не можете сделать это в CSS. Calc() просто не может делать подобные вычисления. В частности, вы не можете рассчитать показатели:

font-size: calc(3vw * 3vw);

Итак, до тех пор, пока calc () не поддержит этот тип нелинейной математики, мы будем зависеть только от линейных уравнений. Можем ли мы что-нибудь еще улучшить?

Брейкпойнты и множественные линейные уравнения

Что, если бы мы используем прямые линии между каждой парой брейкопойнтов? Что-то вроде этого:

image

В этом случае мы вычислили прямую линию между 22px и 24px, а затем еще между 24px и 34px.
Sass будет выглядеть так:

h1 {
  @media (min-width:576px) {
    font-size: calc(???);
  }
  @media (min-width:768px) {
    font-size: calc(???);
  }
}

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

Помните уравнение для прямой линии?

image

Поскольку сейчас речь идет всего лишь о двух точках: m и b, то решение более простое:

image

Sass для этого:

/// linear-interpolation
/// Calculate the definition of a line between two points
/// @param $map - A Sass map of viewport widths and size value pairs
/// @returns A linear equation as a calc() function
/// @example
///   font-size: linear-interpolation((320px: 18px, 768px: 26px));
/// @author Jake Wilson <jake.e.wilson@gmail.com>
@function linear-interpolation($map) {
  $keys: map-keys($map);
  @if (length($keys) != 2) {
    @error "linear-interpolation() $map must be exactly 2 values";
  }
  // The slope
  $m: (map-get($map, nth($keys, 2)) - map-get($map, nth($keys, 1)))/(nth($keys, 2) - nth($keys,1));
  
  // The y-intercept
  $b: map-get($map, nth($keys, 1)) - $m * nth($keys, 1);
  
  // Determine if the sign should be positive or negative
  $sign: "+";
  @if ($b < 0) {
    $sign: "-";
    $b: abs($b);
  }
  
  @return calc(#{$m*100}vw #{$sign} #{$b});
}
Now, just use the linear interpolation function on multiple breakpoints in your Sass. Also, lets throw in some min and max font-sizes:
// SCSS
h1 {
  // Minimum font-size
  font-size: 22px;
  // Font-size between 576 - 768
  @media (min-width:576px) {
    $map: (576px: 22px, 768px: 24px);
    font-size: linear-interpolation($map);
  }
  // Font-size between 768 - 992
  @media (min-width:768px) {
    $map: (768px: 24px, 992px: 34px);
    font-size: linear-interpolation($map);
  }
  // Maximum font-size
  @media (min-width:992px) {
    font-size: 34px;
  }
}

Генерируемый CSS:

h1 {
  font-size: 22px;
}
@media (min-width: 576px) {
  h1 {
    font-size: calc(1.04166667vw + 16px);
  }
}
@media (min-width: 768px) {
  h1 {
    font-size: calc(4.46428571vw - 10.28571429px);
  }
}
@media (min-width: 992px) {
  h1 {
    font-size: 34px;
  }
}

image

Финальное решение

В итоге получается Sass mixin:

/// poly-fluid-sizing
/// Generate linear interpolated size values through multiple break points
/// @param $property - A string CSS property name
/// @param $map - A Sass map of viewport unit and size value pairs
/// @requires function linear-interpolation
/// @requires function map-sort
/// @example
///   @include poly-fluid-sizing('font-size', (576px: 22px, 768px: 24px, 992px: 34px));
/// @author Jake Wilson <jake.e.wilson@gmail.com>
@mixin poly-fluid-sizing($property, $map) {
  // Get the number of provided breakpoints
  $length: length(map-keys($map));
  
  // Error if the number of breakpoints is < 2
  @if ($length < 2) {
    @error "poly-fluid-sizing() $map requires at least values"
  }

  // Sort the map by viewport width (key)
  $map: map-sort($map);
  $keys: map-keys($map);

  // Minimum size
  #{$property}: map-get($map, nth($keys,1));
  
  // Interpolated size through breakpoints
  @for $i from 1 through ($length - 1) {
    @media (min-width:nth($keys,$i)) {
      $value1: map-get($map, nth($keys,$i));
      $value2: map-get($map, nth($keys,($i + 1)));
      // If values are not equal, perform linear interpolation
      @if ($value1 != $value2) {
        #{$property}: linear-interpolation((nth($keys,$i): $value1, nth($keys,($i+1)): $value2));
      } @else {
        #{$property}: $value1;
      }
    }
  }
  
  // Maxmimum size
  @media (min-width:nth($keys,$length)) {
    #{$property}: map-get($map, nth($keys,$length));
  }
}

Для реализации потребуется несколько Sass функций:

Poly-fluid-sizing () будет выполнять линейную интерполяцию на каждой паре ширины экрана и устанавливать минимальный и максимальный размер. Вы можете импортировать это в любой проект Sass и легко использовать его, не применяя никаких знаний математики.

Вот окончательный пример на CodePen, который использует описанный метод.

Заключение

Это лучшее, что мы можем сделать для гибкой адаптивной типографики? Возможно. CSS в настоящее время поддерживает функции нелинейной анимации и transition-timing, поэтому, возможно, когда-нибудь calc () также будет это поддерживать.

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

Но возможно и нет… Линейное масштабирование может быть лучше.

Автор: Кирилл

Источник


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js