IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    实现精准的流体排版原理

    Airen发表于 2017-04-28 14:36:50
    love 0

    流体排版这一词似乎看上去有点陌生,在英文中常把他称之为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()函数来进行计算,详细的使用方式可以点击这里。

    其实有关于这方面的介绍,在早期分享的文章中也或多或少的提到过:

    • 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的大小,甚至只要是带有长度单位的属性都可以通过这样方式,达到精准的值。

    扩展阅读

    • 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

    大漠

    常用昵称“大漠”,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.html

    CSS3
    typography
    排版


沪ICP备19023445号-2号
友情链接