流体排版这一词似乎看上去有点陌生,在英文中常把他称之为Fluid Typography,当然也有很多朋友称之为流体字号(Fluid Size)。大概的意思就是Web排版中的font-size
会根据浏览器窗口的大小自动改为。比如下图所示的一个效果:
看到上图的效果,大家首先可能会想到的是CSS中的Viewport单位vw
或者vh
之类,当然也可能会认为是通过媒体查询来改变元素font-size
来实现的。事实上,他们都能实现类似的效果,但问题是我们想要精确的实现流体排版(根据视窗大小变化精确改变font-size
的值),那并不是件容易的事情。那问题来了,我们有没有方法可以实现所谓的精准流体排版呢?答案是肯定的,接下来我们就要来探讨这方面的实现思路、细节以及使用到的一些数学公式。
实现思路
精准流体排版最核心的就是浏览器视窗大小改变时,font-size
能够根据视窗的大小做到精准的变化。当用户收缩和拉大浏览器窗口时,其大小有一个变化,在CSS中,咱们通过把每一个大小点称为断点,断点也是媒体查询中一个重要的概念。除此之外,如果我们用Viewport单位来描述的话,视窗大小始终是100vw
。如果font-size
设置为2vw
,那么其大小就是浏览器窗口宽度的2%
,当窗口拉到1000px
时,这个时候font-size
对应的是20px
。
原理是不是很简单,而且其中还涉及到一些数学计算,CSS中动态计算的话,可以依赖calc()
函数来进行计算,详细的使用方式可以点击这里。
其实有关于这方面的介绍,在早期分享的文章中也或多或少的提到过:
为了方便大家使用,在Sassmagic仓库中,使用SCSS声明了一个混合宏:
/// Fluid vertical rhythm and Fluid Modular scale
/// @param {string} $properties - CSS属性
/// @param {string} $min-vw - 视窗最小宽度(viewport min-width)
/// @param {string} $max-vw - 视窗最大宽度(viewport max-width)
/// @param {string} $min-value - 最小值
/// @param {string} $max-value - 最大值
@mixin fluid-type($properties, $min-vw, $max-vw, $min-value, $max-value) {
& {
@each $property in $properties {
#{$property}: $min-value;
}
@media screen and (min-width: $min-vw) {
@each $property in $properties {
#{$property}: calc(#{$min-value} + #{strip-units($max-value - $min-value)} * ((100vw - #{$min-vw}) / #{strip-units($max-vw - $min-vw)}));
}
}
@media screen and (min-width: $max-vw) {
@each $property in $properties {
#{$property}: $max-value;
}
}
}
}
只需要这样调用:
$minScreen: 20rem; // $min-vw
$maxScreen: 50rem; // $max-vw
$minFont: .8rem; // $min-value
$maxFont: 2rem; // $max-value
:root {
@include fluid-type(font-size, $minScreen, $maxScreen, $minFont, $maxFont);
}
就可以编译出:
:root {
font-size: 0.8rem;
}
@media screen and (min-width: 20rem) {
:root {
font-size: calc(0.8rem + 1.2 * ((100vw - 20rem) / 30));
}
}
@media screen and (min-width: 50rem) {
:root {
font-size: 2rem;
}
}
既然前面都有多篇文章介绍过了,为何还需要花时间来整理这篇文章呢?正如文章开头所说,我们今天主要介绍一些细节和原理以及一些数学知识。在继续阅读下面的内容之前,需要特别感谢@Jake Wilson分享的博文:《CSS Poly Fluid Sizing using calc()
, vw
, breakpoints and linear equations》。这篇文章介绍了精准流体排版的一些细节以及用到的相关数学知识。接下来的文章中,将会直接使用@Jake Wilson文章中使用到的公式。
计算的演变过程与细节
假设页面中有一个h1
的标题元素,希望在不同的断点之下有不一样的font-size
,这样可以让我们阅读体验更友好。比如:
- 在小屏幕下(Small:
576px
)标题h1
的font-size
是22px
- 在中间屏幕下(Medium:
768px
)标题h1
的font-size
是24px
- 在大屏幕下(Large:
992px
)标题h1
的font-size
是34px
前面也提到过了,改变font-size
我们有多种方式。首先来看CSS媒体查询的实现方式:
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;
}
}
是不是很简单,大家感兴趣的话,可以把这段代码复制到你的项目中,你就能看到效果。如果为了效果看上去更佳,你可以在h1
中添加一个过渡效果:
h1 {
font-size: 22px;
transition: font-size .2s;
}
虽然你在不同的断点下,借助媒体查询的特性,能轻易的改变font-size
值,从而得到你所需要的效果。但如果你够仔细的话,他的改变都是一下跳到断点对应的值(特别是没有添加transition
属性的时候)。另外他只适应三个断点内,如果你需要更多的屏幕断点效果时,需要不断的添加媒体查询的条件以及对应的改变font-size
值。
是不是感觉有点蛋疼,而且难维护。此时可能很多同学会立马想到CSS新特性:Viewport单位(vw
)。那来看看vw
应用,比如给h1
标题设置font-size
的值为2vw
。根据vw
的一些原理,我们可以计算出对应的值(还是拿前面所说的三个断点为例吧):
576px
时2vw
对应的是576 * 2% = 11.52
,也就是说这个时候font-size
的值为11.52px
768px
时2vw
对应的是768 * 2% = 15.36
,也就是说这个时候font-size
的值为15.36px
992px
时2vw
对应的是992 * 2% = 19.84
,也就是说这个时候font-size
的值为19.84px
从计算的结果上来看,这几个值并不是设计师所需的22px
、24px
和34px
。这个时候我们可以进行反推,在不同的断点下它其实都是100vw
,那么每1vw
在不同断点下的值是:
576px
对应的是576 / 100
768px
对应的是768 / 100
992px
对应的是992 / 100
接着继续推算出不同断点下,22px
、24px
和34px
所对应的vw
值:
22px
对应567px
的值:22 / 576 * 100% = 3.82%
,也就是3.82vw
24px
对应768px
的值:24 / 768 * 100% = 3.13%
,也就是3.13vw
34px
对应992px
的值:34 / 992 * 100% = 3.43%
,也就是3.43vw
这个时候,它们的值是对应上了,但依旧是离不开媒体查询,font-size
在过渡的时候仍将跳跃。而且还会有一个奇怪的副作用:
断点
767px
时,3.82%
对应的视窗宽度是29.33759999px
。用户把浏览器宽度减小1px
,这个时候font-size
就立马跳到24px
。这将是让人感到非常的奇怪。
那么我们的核心问题就是这个了,如何解决这个现象?
如果我们把这些数据做一个简单的图表统计,不难发现,屏幕宽度越大,元素对应的font-size
值也就越大,如下图所示:
如果根据这个趋势图,h1
可以得到所有分辨率尺寸下最接近匹配设计师所需要的font-size
值。这里有一个数学公式,直线方程中的斜截式:
y = mx + b
其中参数对应的意义:
m
直线的斜率(Slope)b
是y
轴截距(y-intercept)x
是当前视窗宽度(Viewport Width)y
是font-size
的值
这里关键是怎么来决定斜率(Slope)和截距(y-intercept)。决定这两个参数有多种方法,最常见的方法是最小二乘法:
看到这里,不是觉得数学对于一位程序员是有多么的重要(虽然偶还不是一位真正的程序员,但在制作一些CSS的动画以及Canvas的运用中,体会到了数学公式是多么的重要)。继续我们今天要聊的话题,既然知道如何计算出自己所需参数,那么怎么将这些运用到我们的Web开发中来呢?对于CSS而言,要具备计算能力,目前也仅有calc()
函数可以帮助我们实现。
既然如此,我们就把y = mx + b
转到我们CSS代码当中来:
h1 {
font-size: calc( {slope} * 100vw + {y-intercept}px);
}
要得到想要的结果还是斜率(slope)和截距(y-intercept)。因为我们的视窗宽度是100vw
。而1vw
单位就是视窗宽度的1/100
。如果我们把斜率做多次的计算,比如100
次,那么每一次对应的也就是1vw
。
这只是人肉的计算。那么有没有什么方式可以帮我们自动计算呢?这里我们可以采用CSS处理器来完成,比如Sass:
/// leastSquaresFit
/// 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: leastSquaresFit((576: 24, 768: 24, 992: 34));
/// @author Jake Wilson <jake.e.wilson@gmail.com>
@function leastSquaresFit($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}px);
}
这样写,真的有效吗?打开这个DEMO,然后调整你的浏览器大小,你就可以看到变化了。而且字体大小非常接近最初的设计要求。
现在虽然font-size
能随着视窗变化非常接近设计师要的,但如果你追求完美的话,你可能还是不太会接受。这是因为一个线性趋势线是一个特定的font-size
与特定的视窗宽度接近。这是继承的线性回归。在你的结果中总是会有一些错误。这个时候就需要一个权衡,你要不要一个准确性。
这个时候你可能会追求更好。那我们可以做到?
前面采用的是直线趋势线,使用的是最小二乘法。接下来我们再一起来看看多项式最小二乘法。就像一个多项式回归趋势线,可能看起来像这样:
他对应也有一个数学公式,只是变得更为复杂:
简单点说:想要越精准的曲线,需要更复杂的方程。但是非常的不幸,在CSS中我们使用calc()
函数并不能完成这样复杂的方程式计算。具体来说,没有指数运算:
font-size: calc(3vw * 3vw); /* This doesn't work in CSS */
那又来了一个新问题,calc()
不支持这种类型的非线性数学计算,那我们能怎么做?
我们来考虑一下断点加多元线性方程的方式来弥补这方面的欠缺。如果我们只计算每一对断点之间一条线,趋势图看起来像这样:
在这个例子中我们将计算22px
和24px
之间的直线,然后另一个是24px
和34px
之间的直线。用Sass看起来像这样:
h1 {
@media (min-width:576px) {
font-size: calc(???);
}
@media (min-width:768px) {
font-size: calc(???);
}
}
还记得我们前面介绍的方程式?
y = mx + b
现在我们要说的是两个点,那我们的方程式就变成:
同样使用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});
}
在调用的时候可以这样使用:
h1 {
// Minimum font-size
font-size: 22px;
// Font-size between 576 - 768
@media (min-width:576px) {
$map: (576px: 22px, 768px: 24px);
font-size: linearInterpolation($map);
}
// Font-size between 768 - 992
@media (min-width:768px) {
$map: (768px: 24px, 992px: 34px);
font-size: linearInterpolation($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;
}
}
为了Sass能更好的高效工作,对前面的再进行封装一下,比如我们把其封装成一个poly-fluid-sizing()
函数:
/// 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)) {
#{$property}: linear-interpolation((nth($keys,$i): map-get($map, nth($keys,$i)), nth($keys,($i+1)): map-get($map, nth($keys,($i + 1)))));
}
}
// Maxmimum size
@media (min-width:nth($keys,$length)) {
#{$property}: map-get($map, nth($keys,$length));
}
}
poly-fluid-sizing()
函数中还依赖linear-interpolation()
、map-sort()
、list-sort()
和list-remove()
几个函数:
linear-interpolation()函数
/// 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});
}
map-sort()函数
/// map-sort
/// Sort map by keys
/// @param $map - A SASS map
/// @returns A SASS map sorted by keys
/// @requires function list-sort
/// @author Jake Wilson <jake.e.wilson@gmail.com>
@function map-sort($map) {
$keys: list-sort(map-keys($map));
$sortedMap: ();
@each $key in $keys {
$sortedMap: map-merge($sortedMap, ($key: map-get($map, $key)));
}
@return $sortedMap;
}
list-sort()函数
/// list-sort
/// Sort a SASS list
/// @param $list - A SASS list
/// @returns A sorted SASS list
/// @requires function list-remove
/// @author Jake Wilson <jake.e.wilson@gmail.com>
@function list-sort($list) {
$sortedlist: ();
@while length($list) > 0 {
$value: nth($list,1);
@each $item in $list {
@if $item < $value {
$value: $item;
}
}
$sortedlist: append($sortedlist, $value, 'space');
$list: list-remove($list, index($list, $value));
}
@return $sortedlist;
}
list-remove()函数
/// list-remove
/// Remove an item from a list
/// @param $list - A SASS list
/// @param $index - The list index to remove
/// @returns A SASS list
/// @author Jake Wilson <jake.e.wilson@gmail.com>
@function list-remove($list, $index) {
$newList: ();
@for $i from 1 through length($list) {
@if $i != $index {
$newList: append($newList, nth($list,$i), 'space');
}
}
@return $newList;
}
显然这种方法要强大的多,它不仅仅适用于font-size
,它适用于任何带有单位或长度属性,比如margin
、padding
等。在实际使用当中,你可以使用poly-fluid-sizing()
函数,当然你也可以使用前面最早提到的leastSquaresFit()
函数。这里有一个poly-fluid-sizing()
的使用示例。感兴趣的可以看看。
其他类似方案
@eduardoboucas提供的responsive-font()
混合宏:
/// Viewport sized typography with minimum and maximum values
///
/// @author Eduardo Boucas (@eduardoboucas)
///
/// @param {Number} $responsive - Viewport-based size
/// @param {Number} $min - Minimum font size (px)
/// @param {Number} $max - Maximum font size (px)
/// (optional)
/// @param {Number} $fallback - Fallback for viewport-
/// based units (optional)
///
/// @example scss - 5vw font size (with 50px fallback),
/// minumum of 35px and maximum of 150px
/// @include responsive-font(5vw, 35px, 150px, 50px);
///
@mixin responsive-font($responsive, $min, $max: false, $fallback: false) {
$responsive-unitless: $responsive / ($responsive - $responsive + 1);
$dimension: if(unit($responsive) == 'vh', 'height', 'width');
$min-breakpoint: $min / $responsive-unitless * 100;
@media (max-#{$dimension}: #{$min-breakpoint}) {
font-size: $min;
}
@if $max {
$max-breakpoint: $max / $responsive-unitless * 100;
@media (min-#{$dimension}: #{$max-breakpoint}) {
font-size: $max;
}
}
@if $fallback {
font-size: $fallback;
}
font-size: $responsive;
}
总结
文章介绍了如何实现精准的流式排版。其中原理非常的简单,通过CSS的Viewport单位和calc()
配合一些数学公式,较为精准的实现随着视窗改变,能较为精准的改变font-size
的大小,甚至只要是带有长度单位的属性都可以通过这样方式,达到精准的值。
扩展阅读
- Web排版的缩放
- 如何精确控制响应式排版
- 基于视窗单位的排版
- CSS Poly Fluid Sizing using
calc()
,vw
, breakpoints and linear equations - Truly Fluid Typography With vh And vw Units
- Flexible typography with CSS locks
- Fluid Typography
- Between the Lines
如需转载,烦请注明出处:http://www.w3cplus.com/css/css-polyfluidsizing-using-calc-vw-breakpoints-and-linear-equations.html