流体排版这一词似乎看上去有点陌生,在英文中常把他称之为Fluid Typography,当然也有很多朋友称之为流体字号(Fluid Size)。大概的意思就是 Web 排版中的font-size
会根据浏览器窗口的大小自动改为。比如下图所示的一个效果:
更多精彩内容请看 web 前端中文站
http://www.lisa33xiaoq.net 可按 Ctrl + D 进行收藏
看到上图的效果,大家首先可能会想到的是 CSS 中的 Viewport 单位vw
或者vh
之类,当然也可能会认为是通过媒体查询来改变元素font-size
来实现的。事实上,他们都能实现类似的效果,但问题是我们想要精确的实现流体排版(根据视窗大小变化精确改变font-size
的值),那并不是件容易的事情。那问题来了,我们有没有方法可以实现所谓的精准流体排版呢?答案是肯定的,接下来我们就要来探讨这方面的实现思路、细节以及使用到的一些数学公式。
实现思路
精准流体排版最核心的就是浏览器视窗大小改变时,font-size
能够根据视窗的大小做到精准的变化。当用户收缩和拉大浏览器窗口时,其大小有一个变化,在 CSS 中,咱们通过把每一个大小点称为断点,断点也是媒体查询中一个重要的概念。除此之外,如果我们用 Viewport 单位来描述的话,视窗大小始终是100vw
。如果font-size
设置为2vw
,那么其大小就是浏览器窗口宽度的2%
,当窗口拉到1000px
时,这个时候font-size
对应的是20px
。
原理是不是很简单,而且其中还涉及到一些数学计算,CSS 中动态计算的话,可以依赖calc()
函数来进行计算,详细的使用方式可以点击这里。
其实有关于这方面的介绍,在早期分享的文章中也或多或少的提到过:
- Web 排版的缩放
- 如何精确控制响应式排版
- 基于视窗单位的排版
为了方便大家使用,在 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
的大小,甚至只要是带有长度单位的属性都可以通过这样方式,达到精准的值。
【注:本文源自网络文章资源,由站长整理发布】