w3ctech

【译】 CSS calc() 初体验

原文地址:http://www.smashingmagazine.com/2015/12/getting-started-css-calc-techniques/#easier-to-understand-computed-values

四年之前,我在CSS3 Click Chart上第一次发现 calc() 函数,很高兴看到基础的数学计算方法(加、减、乘、除)被应用到CSS中。

很多人认为预处理器完全涵盖了逻辑和计算领域,但 calc() 函数可以做预处理器不能完成的工作:任意单位的混合计算。预处理器只能处理有既定关系的单位,例如角度、时间、频率、分辨率、长度。

1圈总是360度100梯度总是90度,且3.14弧度总是180度1s总是1000ms1khz总是1000Hz1英寸总是2.54cm25.4mm96px1dppx始终等于96dpi 。这就是为什么预处理器能转换它们并且在计算中混合它们。然而,预处理器不清楚 1em、1%、1vmin 和 1ch 在上下文中实际代表了多少。

看一些基础的例子:

div {
   font-size: calc(3em + 5px);
   padding: calc(1vmax + -1vmin);
   transform: rotate(calc(1turn - 32deg));
   background: hsl(180, calc(2*25%), 65%); 
   line-height: calc(8/3);
   width: calc(23vmin - 2*3rem);
}

某些情况下,你可能想要在 calc() 函数中使用变量。多数受欢迎的预处理器都可以这样。首先,在 Sass中,可以使用变量插值的方式,和其他原生的 CSS 方法一样:

$a: 4em
height: calc(#{$a} + 7px)

这是less的写法:

@a: 4em;
height: ~"calc(@{a} + 7px)";

以及 Stylus的写法

a = 4em
height: "calc(%s + 7px)" % a

我们还可以使用 CSS 原生变量, 但注意目前它只能在火狐31+的版本中使用,因为其他的浏览器还不支持 CSS 变量。

--a: 4em;
height: calc(var(--a) + 7px);

要正常使用 calc(),还要记住几件事。首先,除以0是明显不能用的。函数名与圆括号之间有空格是不允许的。并且加减运算符左右必须有空格。这意味着下面的写法是无效的:

calc(50% / 0)  //被0除
calc (1em + 7px)  //calc 与 ( 间有空隙
calc(2rem+2vmin)  //+ 两侧没有空格
calc(2vw-2vh)  //- 两侧没有空格

calc() 函数应该出现在会出现数字的地方,可以带单位也可以不带单位。然而,尽管基本支持还算好,但有的时候用错地方还是会给我们带来麻烦。让我们看一些例子,包括它们有什么支持的问题以及它们最终是否是最好的解决方法。

易于理解的计算值


我们想要一个彩虹渐变,用 CSS 实现是很简单的

background: linear-gradient(#f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00);

但这些十六进制的值并不好理解。尽管使用 hsl()calc() 会很冗长,但意思很明确。

background: linear-gradient(hsl(calc(0*60), 100%, 50%), 
                            hsl(calc(1*60), 100%, 50%), 
                            hsl(calc(2*60), 100%, 50%), 
                            hsl(calc(3*60), 100%, 50%), 
                            hsl(calc(4*60), 100%, 50%), 
                            hsl(calc(5*60), 100%, 50%), 
                            hsl(calc(6*60), 100%, 50%));

遗憾的是,目前在火狐或IE浏览器中 calc() 函数不能使用在 hsl()rgb()hsla()rgba() 方法中,只有在 Webkit 的浏览器中才能这样使用。所以,开发中可能预处理所有计算更可取。使用预处理器最大的好处是可以在循环中生成列表

$n: 6;
$l: ();

@for $i from 0 through $n {
   $l: append($l, hsl($i*360/$n, 100%, 50%), comma);
}

background: linear-gradient($l);

适合做弹性元素的渐变背景

假如我们想要背景的顶部和底部分别有固定1em宽的条纹。唯一的问题是我们不知道元素的高度。一种解决方案是采用两个渐变

background: 
   linear-gradient(#e53b2c 1em, transparent 1em),
   linear-gradient(0deg, #e53b2c 1em, #f9f9f9 1em);

但如果使用 calc() 就只需要一个渐变

background: 
   linear-gradient(#e53b2c 1em, #f9f9f9 1em, 
                   #f9f9f9 calc(100% - 1em), 
                   #e53b2c calc(100% - 1em));

在所有支持 calc() 和渐变的浏览器中可以运行得很好,因为它涉及混合单位,是预处理器不能胜任的。然而使用变量会使代码更易维护。

$s: 1em;
$c: #e53b2c;
$bg: #f9f9f9;

background: 
   linear-gradient($c $s, 
                   $bg $s, 
                   $bg calc(100% - #{$s}), 
               $c calc(100% - #{$s}));

注意:某些情况下,在 Chrome 和 Opera 浏览器中显示的条纹中会有一条比另一条稍显模糊和狭窄。

对角渐变条纹

假如我们想要渐变在元素对角线两侧分别向外延伸的效果,这里使用基于百分比的渐变

background: 
   linear-gradient(to right bottom, 
                   transparent 42%, #000 0, #000 58%, 
                   transparent 0);

这种情况下,条纹的宽度取决于元素的尺寸。有时这就是我们想要的。比如,在 css 中实现一面旗帜就是这样做的。 在一面旗帜上添加绿色、黄色、蓝色的渐变,精美巧克力的爱好者会发现这是坦桑尼亚国旗。

background: 
   linear-gradient(to right bottom, 
                   #1eb53a 38%, #fcd116 0, 
                   #fcd116 42%, #000 0, 
                   #000 58%, #fcd116 0, 
                   #fcd116 62%, #00a3dd 0);

坦桑尼亚国旗 坦桑尼亚国旗. (查看大图)

但如果我们想对象线条纹固定不依赖元素的尺寸该如何实现呢?那么,我们可以使用 calc() 并把渐变点设置在 50% 减去固定条纹宽度一半和 50% 加上固定条纹宽度一半的位置。如果我们想要条纹的宽度为4em ,可以这样做:

background: 
   linear-gradient(to right bottom, 
                   transparent calc(50% - 2em), 
                   #000 0, 
                   #000 calc(50% + 2em), 
                   transparent 0);

这个链接可以通过改变窗口大小来感受一下,元素的尺寸以视口单位表示,因此会随视口的变化而变化,但对角线的条纹总是保持同样的宽度。

在容器中定位已知尺寸的子元素

你可能看到过这个把元素绝对定位在父级元素中间的小技巧

position: absolute;
top: 50%; 
left: 50%;
margin: -2em -2.5em;
width: 5em; 
height: 4em;

使用 calc() 可以省掉margin计算

position: absolute;
top: calc(50% - 2em); 
left: calc(50% - 2.5em);
width: 5em; 
height: 4em;

并且我们可以使用宽高变量使其更易维护:

$w: 5em;
$h: 4em;

position: absolute;
top: calc(50% - #{.5*$h});
left: calc(50% - #{.5*$w});
width: $w; 
height: $h;

需要注意的是,虽然初始定位使用 offsets(top, left) 是安全的,但如果你以后想要动态改变元素的位置,那应该使用 transforms 。这是因为改变 transforms 只需要合成计算,而改变偏移量会触发布局计算和重绘,因此会有损性能。

中间起点的网格和坐标

自从知道 background-position四值简写开始,我很少使用 calc() 相对于元素的右侧或底部来定位背景。但 calc() 原本是相对于元素的中心定位背景中某一点的好方法。

几年前,我曾想创建一个背景,它是一个带网格的坐标系,原点会定在屏幕的正中间。

网格坐标系 网格坐标系. (查看大图)

网格坐标系是很容易实现的:

background-image: 
   linear-gradient(#e53b2c .5em, transparent .5em) /* 水平轴 */,
   linear-gradient(90deg, #e53b2c .5em, transparent .5em) /* 垂直轴 */, 
   linear-gradient(#333 .25em, transparent .25em) /* major horizontal gridline */, 
   linear-gradient(90deg, #333 .25em, transparent .25em) /* major vertical gridline */, 
   linear-gradient(#777 .125em, transparent .125em) /* minor horizontal gridline */, 
   linear-gradient(90deg, #777 .125em, transparent .125em) /* minor vertical gridline */;

background-size: 
   100vw 100vh, 100vw 100vh, 
   10em 10em, 10em 10em, 
   1em 1em, 1em 1em;

但如何实现背景的原点在中间而不是左上角呢?首先,background-position: 50% 50% 是达不到效果的,因为它会让渐变 50% 50% 的点与元素 50% 50% 的点相重合,但线分别在渐变的左侧和顶部。解决方法是使用 calc() 和定位渐变,使其左上角几乎在视口中间,即向左上偏移坐标轴或网格线宽度的一半:

background-position: 
    0 calc(50vh - .25em), calc(50vw - .25em), 
    0 calc(50vh - .125em), calc(50vw - .125em), 
    0 calc(50vh - .0625em), calc(50vw - .0625em);

同样,使用变量会使其更易维护: 可以在 CodePen 中查看 Ana Tudorsystem of coordinates + grid #2

覆盖 Viewport 大小并保持高宽比

我创建HTML幻灯片时一直想要实现一个效果,就是让每一页都能保持固定的高宽比,有一边始终与视口的一个维度等宽,另一边则在视口的另一个维度上居中对齐。

比例框动画

我们首先假设幻灯片的宽高比是 4:3并是在宽屏上演示。这就意味着幻灯片在垂直方向是完全覆盖左右仍有空隙。

比例框 比例框: case 1. (查看大图)

垂直覆盖视口意味着100hv。知道了高和宽高比,我们可以得到宽是 4/3*100vh。使要使其居中,我们需要将其向左移动视口宽度的一半(100vw/2)减去幻灯片宽度的一半(4/3*100vh/2)。这时候就需要 calc() 函数了,因为要混合单位。

.slide {
   position: absolute;
   left: calc(100vw/2 - 4/3*100vh/2);
   width: calc(4/3*100vh);
   height: 100vh;
}

然而,在视口宽高比小于 4:3的时候情况又不同了。这种情况下,幻灯片在水平方向是完全覆盖视口,而顶部和底部仍有空隙。

比例框 比例框: case 2. (View large version)

水平方向上全覆盖的话意味着100vw。知道了宽和宽高比,我们可以得到高是 3/4*100vw。最后,顶部偏移量是视口高度的一半减去幻灯片高度的一半,即 100vh/2 – 3/4*100vw/2

@media (max-aspect-ratio: 4/3) {
   .slide {
      top: calc(100vh/2 - 3/4*100vw/2);
      left: auto; /* Undo style set outside media query  */
      width: 100vw;
      height: calc(3/4*100vh);
   }
}

当然,为了更灵活,可以不设置宽高比而使用两个变量(一个是宽一个是高)。这是 Sass 版本的,你可以改变窗口的大小来动态测试

$a: 4;
$b: 3;

.slide {
   position: absolute;
   top: 0; 
   left: calc(50vw - #{$a/$b/2*100vh});
   width: $a/$b*100vh; 
   height: 100vh;

   @media (max-aspect-ratio: #{$a}/#{$b}) {
      top: calc(50vh - #{$b/$a/2*100vw}); 
      left: 0;
      width: 100vw; 
      height: $b/$a*100vw;
   }
}

甚至,我们可以将以上代码转成 mixin,这通常是比使用全局变量更好的方法:

@mixin proportional-box($a: 1, $b: $a) {
   position: absolute;
   top: 0; 
   left: calc(50vw - #{$a/$b/2*100vh});
   width: $a/$b*100vh; 
   height: 100vh;

   @media (max-aspect-ratio: #{$a}/#{$b}) {
      top: calc(50vh - #{$b/$a/2*100vw}); left: 0;
      width: 100vw; height: $b/$a*100vw;
   }
}

.slide {
   @include proportional-box(4, 3);
}

注意 $a$b 必须是整数才能在媒体查询中使用。

目前大多数的浏览器的所有版本都支持这种方式。然而, WebKit 浏览器直到近期才支持在 calc() 函数中使用视口单位。这个已经在Safari 8 、 Chrome 34 以及 Opera 的后续版本中修复。

幻灯片居中的短标题

幻灯片播放中我还想实现两个效果。

第一是希望幻灯片不要完全覆盖整个视口,因为边缘可能被截断。这是很容易解决的。我仅设置 box-sizingborder-box 并且为它们设置边框即可。 第二是我想突出每一部分的开始,就是在该部分的第一张幻灯片中间只显示一个简短但让人无法忘怀的标题。

预期的结果 预期的结果(查看大图)

我不想使用绝对定位,所以我想应该设置一个适当的 line-height。如果幻灯片的高度(包含边框高度)正好覆盖视口的高度,line-height 应该是 100vh 减去两个幻灯片的 border-width

$slide-border-width: 5vmin;

.slide {
   /* The other styles */
   box-sizing: border-box;
   border: solid $slide-border-width dimgrey;

   h1 {
      line-height: calc(100vh - #{2*$slide-border-width});
   }
}

如果幻灯片的宽度(包括边框)水平覆盖视口(垂直居中),其高应该是 $b/$a*100vw 。因此,标题的 line-height 是这个值再减去两倍的border-width

line-height: calc(#{$b/$a*100vw} - #{2*$slide-border-width});

这是我最初的想法,理论上应该没有问题。而且在 WebKit 浏览器和IE中确实如此。然而,我发现在Firefox中line-height(或其他一些属性)不能使用 calc() 计算的值。因此,calc() 并不是最好的解决方案。幸运的是,有很多其他的方法解决这个问题(flexbox布局、绝对定位等等)。

固定视角

我对 CSS 3D 比较感兴趣,特别是用 CSS 创建 3D 的几何形状。如果我仅仅创建一个形状,我通常会将其定位包含它的场景中间。场景是我设置了 perspective 元素,也是形状元素的父级元素。形状元素有它自己的后代元素,也就是形状的面,这里我们不会详细讲解。如果你想要学习如何定位这些元素,可以看看我受邀为 CSS-Tricks 写的文章。

在场景中设置 perspective 可以看到近处的东西要大些,远处的形状要小些。perspective 属性接收长度值,这些值越小,近大远小的对比就越大。 现在,假设场景中有一个非常简单的 3D 形状 — — 立方体。它看起来并不是很有 3D效果: 它太对称了,如果表面都是完全不透明的,那我们只能看到它前面的一个面。 立方体 立方体. (查看大图)

我们可以让它绕 y 轴旋转 30 ° (y轴穿过立方体的中间) 或者绕 x 轴也行。这看起来好多了,但也只能看到两个面。另外,立方体有明显旋转,但这不是想要的。 旋转的立方体 旋转的立方体. (查看大图)

除此之外,我们也可以改变视角。设置 perspective-origin 属性就行。它的初始值是 50% 50%,它们是相对于场景而言的,场景中50% 50%的点就是定位形状的中心点。现在,我们想要向上和向右移动。做到这一点的最简单方法是设置 perspective-origin: 100% 0。但这产生了一个问题: 改变场景大小会看见什么样的立方体 (你可以调整视口大小来动态测试) 。

改变视口的尺寸查看立方体

值为100% 0的perspective-origin意味着度量以右上角为起点,而立方体总是在场景的中间。正因为如此,更改场景的尺寸将改变 50% 50% 点(立方体的位置)和 100% 0点 (我们把perspective-origin设置在这里)之间的距离。 一个解决方案为在 perspective-origin中使用 calc() ,当然,是简单地在最初的 50%加上或减去一个固定的值:

perspective-origin: calc(50% + 15em) calc(50% - 10em);

解决方法

你可以调整大小视区来动态测试。 你呢? 你用过 calc() 吗? 如果用过,实现了什么?

w3ctech微信

扫码关注w3ctech微信公众号

共收到5条回复

  • 馬住,學習

    回复此楼
  • 原来css可以做这么多,受教了

    回复此楼
  • 原来css可以做这么多,受教了

    回复此楼
  • 原来css可以做这么多,受教了

    回复此楼
  • 原来css可以做这么多,受教了

    回复此楼