15 мая в 18:06

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

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

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

Но возможно и нет… Линейное масштабирование может быть лучше.
Автор оригинала: Jake Wilson
Кирилл @grokru
карма
413,0
рейтинг 0,0
Дизайнер
Похожие публикации
Самое читаемое Дизайн

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

  • +6
    Зачем менять размер шрифта плавно, если для этого нет задач? :)
    • 0

      Ни всегда… Есть задача, когда ты делаешь что-то типа своего оконного менагера (а такое у каких-нибудь полубухгалтерских приложеньках нужно). Ну конечно задача очень узкая, но тогда ты уже начинаешь либо использовать @media и iframe, что, вообще-то, как-то не современно. Либо начинаешь делать одно приложение, которое внутри себя делает вот такие вот окошки и делаешь свой велосипед для @media на js...

  • +1
    font-size: 2vw;
    Если вы уменьшаете размеры ячейки для меньшего экрана шрифт надо увеличивать, а не уменьшать.
  • +1
    Мы в своих проектах используем это решение для плавного изменения размеров: Математика CSS-шлюзов
  • +2
    Какая разница насколько плавно меняется шрифт, если при повороте экрана планшета видимая надпись всё равно уедет за его пределы?
  • +1
    в топик по машинному обучению, однозначно!
  • +1
    встречал viewport только в tex`е при редактировании изображений
  • 0
    H1 для маленького размера равен 34 пикселя

    тут не очепятка? Может все-таки для большого?

    Спасибо
  • 0
    Но для прохождения трех точек нужна квадратичная, а не кубическая функция.
    y == a + b * x + c * x * x
    
  • +2
    Может кому пригодится, написал себе такой сниппет, уже как более полугода использую (как прочитал эту статью http://fvsch.com/code/css-locks/ ).

    /*=========================АДАПТИВНЫЙ ШРИФТ=========================*/
    /*миксин возвращает адаптивный шрифт между указанными ширинами экранов, 
    за пределами этих ширин шрифт оставляет неизменными свои указанные 
    крайние значения, т.е. миксин на вход получает следующие параметры:
    - $bigFont - шрифт которой мы хотим получить при ширине экрана $bigView,
    - $smallFont - шрифт которой мы хотим получить при ширине экрана $smallView,
    соответственно в промежутке между указанными ширинами экранов - шрифт 
    будет изменятся линейно*/
    @mixin responsiveFont($bigFont, $smallFont, $bigView, $smallView){
    	//http://fvsch.com/code/css-locks/
    	// y = m * x + b - уравнение прямой, где:
    	// y - вертикальная ось с нужными нам шрифтами, 
    	// x - горизонтальная ось с шириной экрана (вьюпорта),
    	// b - промежуток между пересечением графика (в данном случае это 
    	// функция прямой линии) с вертикальной осью x,
    	// m - наклон прямой (степерь прироста координаты y при 
    	// каждом увеличении вьюпорта на 1px);
    
    	$m: ($bigFont - $smallFont) / ($bigView - $smallView);
    	$b: $smallFont - ($m * $smallView);
    	
    	@media only screen and (min-width: $smallView){
    		font-size: calc(#{$m} * 100vw + #{$b});	
    	}
    
    	@media only screen and (min-width: $bigView){
    		font-size: $bigFont;
    	}
    }
    


    статью просмотрел по диагонали, подозреваю речь о том же..))

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