现代 CSS

实现精准的流体排版原理

特别声明:如果您喜欢小站的内容,可以点击申请会员进行全站阅读。如果您对付费阅读有任何建议或想法,欢迎发送邮件至: airenliao@gmail.com!或添加QQ:874472854(^_^)

流体排版这一词似乎看上去有点陌生,在英文中常把他称之为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)标题h1font-size22px
  • 在中间屏幕下(Medium:768px)标题h1font-size24px
  • 在大屏幕下(Large:992px)标题h1font-size34px

前面也提到过了,改变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的一些原理,我们可以计算出对应的值(还是拿前面所说的三个断点为例吧):

  • 576px2vw对应的是 576 * 2% = 11.52,也就是说这个时候font-size的值为11.52px
  • 768px2vw对应的是 768 * 2% = 15.36,也就是说这个时候font-size的值为15.36px
  • 992px2vw对应的是 992 * 2% = 19.84,也就是说这个时候font-size的值为19.84px

从计算的结果上来看,这几个值并不是设计师所需的22px24px34px。这个时候我们可以进行反推,在不同的断点下它其实都是100vw,那么每1vw在不同断点下的值是:

  • 576px 对应的是576 / 100
  • 768px 对应的是768 / 100
  • 992px 对应的是992 / 100

接着继续推算出不同断点下,22px24px34px所对应的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)
  • by轴截距(y-intercept)
  • x是当前视窗宽度(Viewport Width)
  • yfont-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()不支持这种类型的非线性数学计算,那我们能怎么做?

我们来考虑一下断点加多元线性方程的方式来弥补这方面的欠缺。如果我们只计算每一对断点之间一条线,趋势图看起来像这样:

在这个例子中我们将计算22px24px之间的直线,然后另一个是24px34px之间的直线。用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,它适用于任何带有单位或长度属性,比如marginpadding等。在实际使用当中,你可以使用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的大小,甚至只要是带有长度单位的属性都可以通过这样方式,达到精准的值。

扩展阅读

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:http://www.w3cplus.com/css/css-polyfluidsizing-using-calc-vw-breakpoints-and-linear-equations.htmlFILA Luminous Pack

返回顶部