CSS的计算属性,三角函数

CSS当前支持的计算属性

calc()、min()、 max()、 clamp()

函数计算适用的场景:

表示距离尺寸的数据类型

数据类型表示频率维度,例如语音的音高

角的大小,单位为度(degrees)、百分度(gradians)、弧度(radians)或圈数(turns)

百分比值

一个数字,可为整数或小数

一个整数,无论是正数还是负数

一、clac()

calc() 函数用一个表达式作为它的参数,用这个表达式的结果作为值。

适用的操作符的组合

+
-

* 乘数中至少有一个是number

/  除数(`/` 右面的数)必须是number

表达式中的运算对象可以使用任意 <length> 值。如果你愿意,你可以在一个表达式中混用这类值的不同单位。在需要时,你还可以使用小括号来建立计算顺序。

+- 运算符的两边必须要有空白字符

*/ 这两个运算符前后不需要空白字符,但如果考虑到统一性,仍然推荐加上空白符

用 0 作除数会使 HTML 解析器抛出异常。

涉及自动布局和固定布局的表格中的表列、表列组、表行、表行组和表单元格的宽度和高度百分比的数学表达式,auto 可视为已指定。

calc() 函数支持嵌套

二、min()

min()方法允许你从逗号分隔符表达式中选择一个最小值作为 CSS 的属性值

例:

系统会比较当前状态的 50vw 和 300px 的大小。哪一个值小,则哪一个值生效

width: min(50vw, 300px);

当然,我们可以增加更多的动态值,同样系统只会取最小的值

width: min(1vw, 4em, 80px);

三、max()

max()方法允许你从逗号分隔的表达式列表中选择最大(正方向)的值作为属性的值

与min相反

例:

系统会比较当前状态的 50vw 和 300px 的大小。哪一个值大,则哪一个值生效

width: max(50vw, 300px);

当然,我们可以增加更多的动态值,同样系统只会取最林的值

width: max(1vw, 4em, 80px);

四、clamp()

clamp()函数的作用是把一个值限制在一个上限和下限之间,当这个值超过最小值和最大值的范围时,在最小值和最大值之间选择一个值使用。它接收三个参数:最小值、首选值、最大值。

例:

当值小于30px的时候,则值为30px

当值大于300px的时候,则值为300px

其它情况下则按50vw动态计算

width: clamp(30px, 50vw, 300px);

现在让我们开始数学吧

三角函数:sin(),cos(),tan()

sin() 正弦 全称:sine

cos() 余弦 全称:cosine

tan() 正切 全称:tangent

sin() 为三角函数,返回某数的正弦值,此值介于 -11 之间。此函数含有单个计算式,此式须将参数结果按弧度数解析为 <number><angle>,即 sin(45deg)sin(0.125turn)sin(3.14159 / 4) 均表示同一值,约为 0.707

cos() 为三角函数,返回某数的余弦值,此值介于 -11 之间。此函数含有单个计算式,此式须将参数结果按弧度数解析为 <number><angle>,即 cos(45deg)cos(0.125turn)cos(3.14159 / 4) 均表示同一值,约为 0.707

tan() 为三角函数,返回某数的正切值,此值介于 −infinityinfinity 之间。此函数含有单个计算式,此式须将参数结果按弧度数解析为 <number><angle>

计算公式:

image

image

image

在直角三角形中,一个锐角的正切定义为它的对边与邻边的比值,也就是:

实现一个正方形或者长方形

<template>
  <div class="div-tans" style="--deg:60deg">60deg</div>
  <div class="div-tans" style="--deg:45deg">45deg</div>
  <div class="div-tans" style="--deg:30deg">30deg</div>
</template>

<style scoped>
.div-tans{
  --length: 100;
  width: calc(1px * var(--length));
  height: calc(1px * var(--length) * tan(var(--deg)));
  border: 1px solid #000;
}
</style>

image

这里面输入的是角度,当然支持角度的其它单位,比如:turn,grad,rad

各角度单位之间的关系:90deg = 100grad = 0.25turn ≈ 1.570796326794897rad

文档里面说明了还支持number类型

一个角度对应的数字换算关系

你说了这么多,那它有什么毛用呢?

Q:假设我们要处理一个类似心电图的正弦曲线/正弦波

image

理论:

X轴持续向前运行,Y轴根据函数算出波幅

<div class='g-single'></div>
// 重点
@property --angle {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}
@property --dis {
  syntax: '<length>';
  inherits: false;
  initial-value: 0px;
}
.g-single {
  width: 20px;
  height: 20px;
  background: #000;
  border-radius: 50%;
  animation: move 5s infinite ease-in-out alternate;
  transform: translate(
      calc(var(--dis) - 40vw),
      calc(5 * sin(var(--angle)) * 20px)
  );
}
@keyframes move {
  0% {
    --dis: 0px;
    --angle: 0deg;
  }
  100% {
    --dis: 80vw;
    --angle: 1080deg;
  }
}

Q:如果我有一个点做一个圆的运动呢?

<div class='b-single'></div>
// 这个还是重点
@property --angle {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}
.b-single {
  width: 20px;
  height: 20px;
  background: #000;
  border-radius: 50%;
  animation: move 5s infinite linear;
  transform: translate(
      calc(sin(var(--angle)) * 10vmin),
      calc(cos(var(--angle)) * 10vmin)
  );
}
@keyframes circle-move {
  0% {
    --angle: 0deg;
  }
  100% {
    --angle: 360deg;
  }
}

@property CSS at-rule是CSS Houdini API 的一部分,它允许开发者显式地定义他们的CSS 自定义属性, 允许进行属性类型检查、设定默认值以及定义该自定义属性是否可以被继承。

Calc 内不同单位的混合运算

calc() 支持不同单位的混合运算,对于长度,只要是属于长度相关的单位都可以进行混合运算,包含这些:

  • px
  • %
  • em
  • rem
  • in
  • mm
  • cm
  • pt
  • pc
  • ex
  • ch
  • vh
  • vw
  • vmin
  • vmax

这里有一个有意思的点,运算肯定是消耗性能的,早年间,有这样一段 CSS 代码,可以直接让 Chrome 浏览器崩溃 Crash:

<div></div>

CSS 样式如下:

div {
  --initial-level-0: calc(1vh + 1% + 1px + 1em + 1vw + 1cm);

  --level-1: calc(var(--initial-level-0) + var(--initial-level-0));
  --level-2: calc(var(--level-1) + var(--level-1));
  --level-3: calc(var(--level-2) + var(--level-2));
  --level-4: calc(var(--level-3) + var(--level-3));
  --level-5: calc(var(--level-4) + var(--level-4));
  --level-6: calc(var(--level-5) + var(--level-5));
  --level-7: calc(var(--level-6) + var(--level-6));
  --level-8: calc(var(--level-7) + var(--level-7));
  --level-9: calc(var(--level-8) + var(--level-8));
  --level-10: calc(var(--level-9) + var(--level-9));
  --level-11: calc(var(--level-10) + var(--level-10));
  --level-12: calc(var(--level-11) + var(--level-11));
  --level-13: calc(var(--level-12) + var(--level-12));
  --level-14: calc(var(--level-13) + var(--level-13));
  --level-15: calc(var(--level-14) + var(--level-14));
  --level-16: calc(var(--level-15) + var(--level-15));
  --level-17: calc(var(--level-16) + var(--level-16));
  --level-18: calc(var(--level-17) + var(--level-17));
  --level-19: calc(var(--level-18) + var(--level-18));
  --level-20: calc(var(--level-19) + var(--level-19));
  --level-21: calc(var(--level-20) + var(--level-20));
  --level-22: calc(var(--level-21) + var(--level-21));
  --level-23: calc(var(--level-22) + var(--level-22));
  --level-24: calc(var(--level-23) + var(--level-23));
  --level-25: calc(var(--level-24) + var(--level-24));
  --level-26: calc(var(--level-25) + var(--level-25));
  --level-27: calc(var(--level-26) + var(--level-26));
  --level-28: calc(var(--level-27) + var(--level-27));
  --level-29: calc(var(--level-28) + var(--level-28));
  --level-30: calc(var(--level-29) + var(--level-29));

  --level-final: calc(var(--level-30) + 1px);

    border-width: var(--level-final);                                 
    border-style: solid;
}

可以看到,从  --level-1 到  --level-30,每次的运算量都是成倍的增长,最终到  --level-final 变量,展开将有 2^30 = 1073741824 个 --initial-level-0 表达式的内容。

并且,每个 --initial-level-0 表达式的内容 -- calc(1vh + 1% + 1px + 1em + 1vw + 1cm),在浏览器解析的时候,也已经足够复杂。

混合在一起,就导致了浏览器的 BOOM(Chrome 70之前的版本),为了能看到效果,我们将上述样式赋给某个元素被 hover 的时候,得到如下效果:

image

当然,这个 BUG 目前已经被修复了,我们也可以通过这个小 DEMO 了解到,一是 calc 是可以进行不同单位的混合运算的,另外一个就是注意具体使用的时候如果计算量巨大,可能会导致性能上较大的消耗。

当然,不要将长度单位和非长度单位混合使用,像是这样:

{
    animation-delay: calc(1s + 1px);
}
Calc 搭配 CSS 自定义变量使用

calc() 函数非常重要的一个特性就是能够搭配 CSS 自定义以及 CSS @Property 变量一起使用。

最简单的一个 DEMO:

:root {
    --width: 10px;
}
div {
    width: calc(var(--width));
}

当然,这样看上去,根本看不出这样的写法的作用,好像没有什么意义。实际应用场景中,会比上述的 DEMO 要稍微复杂一些。

假设我们要实现这样一个 loading 动画效果,一开始只有 3 个球:

image

可能的写法是这样,我们给 3 个球都添加同一个旋转动画,然后分别控制他们的 animation-delay

<div class="g-container">
    <div class="g-item"></div>
    <div class="g-item"></div>
    <div class="g-item"></div>
</div>
.item:nth-child(1) {
    animation: rotate 3s infinite linear;
}
.item:nth-child(2) {
    animation: rotate 3s infinite -1s linear;
}
.item:nth-child(3) {
    animation: rotate 3s infinite -2s linear;
}

如果有一天,这个动画需要扩展成 5 个球的话,像是这样:

image

我们就不得已,得去既添加 HTML,又修改 CSS。而如果借助 Calc 和 CSS 变量,这个场景就可以稍微简化一下。

假设只有 3 个球:

<div class="g-container">
    <div class="g-item" style="--delay: 0"></div>
    <div class="g-item" style="--delay: 1"></div>
    <div class="g-item" style="--delay: 2"></div>
</div>

我们通过 HTML 的 Style 标签,传入 --delay 变量,在 CSS 中直接使用它们:

.g-item {
    animation: rotate 3s infinite linear;
    animation-delay: calc(var(--delay) * -1s);
}
@keyframes rotate {
    to {
        transform: rotate(360deg);
    }
}

而当动画修改成 5 个球时,我们就不需要修改 CSS,直接修改 HTML 即可,像是这样:

<div class="g-container">
    <div class="g-item" style="--delay: 0"></div>
    <div class="g-item" style="--delay: 0.6"></div>
    <div class="g-item" style="--delay: 1.2"></div>
    <div class="g-item" style="--delay: 1.8"></div>
    <div class="g-item" style="--delay: 2.4"></div>
</div>

核心的 CSS 还是这一句,不需要做任何修改:

{
    animation-delay: calc(var(--delay) * -1s);
}

完整的 DEMO,你可以戳这里:CodePen Demo–Calc & CSS Variable Demo

calc 搭配自定义变量时候的默认值

还是上述的 Loading 动画效果,如果我的 HTML 标签中,有一个标签忘记填充 --delay 的值了,那会发生什么?

像是这样:

<div class="g-container">
    <div class="g-item" style="--delay: 0"></div>
    <div class="g-item" style="--delay: 0.6"></div>
    <div class="g-item"></div>
    <div class="g-item" style="--delay: 1.8"></div>
    <div class="g-item" style="--delay: 2.4"></div>
</div>
{
    animation-delay: calc(var(--delay) * -1s);
}

由于 HTML 标签没有传入 --delay 的值,并且在 CSS 中向上查找也没找到对应的值,此时,animation-delay: calc(var(--delay) * -1s) 这一句其实是无效的,相当于 animation-delay: 0,效果也就是少了个球的效果:

image

所以,基于这种情况,可以利用 CSS 自定义变量 var() 的 fallback 机制:

{
    // (--delay, 1) 中的 1 是个容错机制
    animation-delay: calc(var(--delay, 1) * -1s);
}

此时,如果没有读取到任何 --delay 值,就会使用默认的 1 与 -1s 进行运算。

Calc 字符串拼接

很多人在使用 CSS 的时候,会尝试字符串的拼接,像是这样:

<div style="--url: 'bsBD1I.png'"></div>
:root {
    --urlA: 'url(https://s1.ax1x.com/2022/03/07/';
    --urlB: ')';
}
div {
    width: 400px;
    height: 400px;
    background-image: calc(var(--urlA) + var(--url) + var(--urlB));
}

这里想利用 calc(var(--urlA) + var(--url) + var(--urlB)) 拼出完整的在 background-image 中可使用的 URL url(https://s1.ax1x.com/2022/03/07/bsBD1I.png)

然而,这是不被允许的(无法实现的)。calc 的没有字符串拼接的能力

唯一可能完成字符串拼接的是在元素的伪元素的 content 属性中。但是也不是利用 calc。

来看这样一个例子,这是错误的:

:root {
    --stringA: '123';
    --stringB: '456';
    --stringC: '789';
}

div::before {
    content: calc(var(--stringA) + var(--stringB) + var(--stringC));
}

此时,不需要 calc,直接使用自定义变量相加即可。

因此,正确的写法:

:root {
    --stringA: '123';
    --stringB: '456';
    --stringC: '789';
}
div::before {
    content: var(--stringA) + var(--stringB) + var(--stringC);
}

content: var(--stringA) + var(--stringB) + var(--stringC) 中的加号可以省略。

此时,内容可以正常展示:

image

再强调一下,calc 的没有字符串拼接的能力,如下的使用方式都是无法被识别的错误语法:

.el::before {
  // 不支持字符串拼接
  content: calc("My " + "counter");
}
.el::before {
  // 更不支持字符串乘法
  content: calc("String Repeat 3 times" * 3);
}

min()、max()、clamp()

min()、max()、clamp() 适合放在一起讲。它们的作用彼此之间有所关联。

  • max():从一个逗号分隔的表达式列表中选择最大(正方向)的值作为属性的值
  • min():从一个逗号分隔的表达式列表中选择最小的值作为属性的值
  • clamp():把一个值限制在一个上限和下限之间,当这个值超过最小值和最大值的范围时,在最小值和最大值之间选择一个值使用

由于在现实中,有非常多元素的的属性不是一成不变的,而是会根据上下文、环境的变化而变化。

譬如这样一个布局:

<div class="container"></div>
.container {
    height: 100px;
    background: #000;
}

效果如下,.container 块它会随着屏幕的增大而增大,始终占据整个屏幕:

image

对于一个响应式的项目,我们肯定不希望它的宽度会一直变大,而是当达到一定的阈值时,宽度从相对单位变成了绝对单位,这种情况就适用于 min(),简单改造下代码:

.container {
    width: min(100%, 500px);
    height: 100px;
    background: #000;
}

容器的宽度值会在 width: 100% 与 width: 500px 之间做选择,选取相对小的那个。

在屏幕宽度不足 500px 时候,也就表现为 width: 100%,反之,则表现为 width: 500px

image

同理,在类似的场景,我们也可以使用 max() 从多个值中,选取相对更大的值。

min()、max() 支持多个值的列表

min()、max() 支持多个值的列表,譬如 width: max(1px, 2px, 3px, 50px)

当然,对于上述表达:

width: max(1px, 2px, 3px, 50px) 其实等于 width: 50px。因此,对于 min()、max() 的具体使用而言,最多应该只包含一个具体的绝对单位。否则,这样的像上述这种代码,虽然语法支持,但是任何情况下,计算值都是确定的,其实没有意义。

配合 calc

min()、max()、clamp() 都可以配合 calc 一起使用。

譬如:

div {
    width: max(50vw, calc(300px + 10%));
}

在这种情况下,calc 和相应包裹的括号可以省略,因此,上述代码又可以写成:

div {
    width: max(50vw, 300px + 10%);
}
基于 max、min 模拟 clamp

现在,有这样一种场景,如果,我们又需要限制最大值,也需要限制最小值,怎么办呢?

像是这样一个场景,**字体的大小,最小是 12px,随着屏幕的变大,逐渐变大,但是为了避免老人机现象(随着屏幕变大,无限制变大),我们还需要限制一个最大值 20px。

我们可以利用 vw 来实现给字体赋动态值,假设在移动端,设备宽度的 CSS 像素为 320px 时,页面的字体宽度最小为 12px,换算成 vw 即是 320 / 100 = 3.2,也就是 1vw 在 屏幕宽度为 320px 时候,表现为 3.2px,12px 约等于 3.75 vw。

同时,我们需要限制最大字体值为 20px,对应的 CSS 如下:

p {
    font-size: max(12px, min(3.75vw, 20px));
}

看看效果:

image

通过 max()min() 的配合使用,以及搭配一个相对单位 vw,我们成功的给字体设置了上下限,而在这个上下限之间实现了动态变化。

当然,上面核心的这一段 max(12px, min(3.75vw, 20px)) 看上去有点绕,因此,CSS 推出了 clamp() 简化这个语法,下面两个写法是等价的:

p {
    font-size: max(12px, min(3.75vw, 20px));
    // 等价于
    font-size: clamp(12px, 3.75vw, 20px);
}
clamp()

clamp() 函数的作用是把一个值限制在一个上限和下限之间,当这个值超过最小值和最大值的范围时,在最小值和最大值之间选择一个值使用。它接收三个参数:最小值、首选值、最大值。

有意思的是,clamp(MIN, VAL, MAX) 其实就是表示 max(MIN, min(VAL, MAX))

使用 vw 配合 clamp 实现响应式布局

我们继续上面的话题。

在不久的过去,移动端的适配方面,使用更多的 rem 适配方案,可能会借助一些现成的库,类似于 flexible.js、hotcss.js 等库。rem 方案比较大的一个问题在于需要一段 JavaScript 响应视口变化,重设根元素的 font-size,并且,使用 rem 多少有点 hack 的感觉。

在现在,在移动端适配,我们更为推崇的是 vw 纯 CSS 方案,与 rem 方案类似,它的本质也是页面的等比例缩放。它的一个问题在于,如果仅仅使用 vw,随着屏幕的不断变大或者缩小,内容元素将会一直变大变小下去,这也导致了在大屏幕下,许多元素看着实在太大了!

因此,我们需要一种能够控制最大、最小阈值的方式,像是这样:

image

此时,clamp 就能非常好的派上用场,还是我们上述的例子,这一段代码 font-size: clamp(12px, 3.75vw, 20px),就能将字体限制在 12px - 20px 的范围内。

因此,对于移动端页面而言,所有涉及长度的单位,我们都可以使用 vw 进行设置。而诸如字体、内外边距、宽度等不应该完全等比例缩放的,采用 clamp() 控制最大最小阈值

在 Modern Fluid Typography Using CSS Clamp 一文中,对使用 clamp() 进行流式响应式布局还有更为深入的探讨,感兴趣的可以深入阅读。

总结一下,对于移动端页面,我们可以以 vw 配合 clamp() 的方式,​完成整个移动端布局的适配。它的优势在于:

  • 没有额外 JavaScript 代码的引入,纯 CSS 解决方案
  • 能够很好地控制边界阈值,合理的进行缩放展示
反向响应式变化

还有一个技巧,利用 clamp() 配合负值,我们也可以反向操作,得到一种屏幕越大,字体越小的反向响应式效果:

p {
    font-size: clamp(20px, -5vw + 96px, 60px);
}

看看效果:

image

这个技巧挺有意思的,由于 -5vw + 96px 的计算值会随着屏幕的变小而增大,实现了一种反向的字体响应式变化。

总结

总结一下,合理运用 min()、max()、clamp(),是构建现代响应式布局的重点,我们可以告别传统的需要 JavaScript 辅助的一些方案,基于 CSS 这些数学函数即可完成所有的诉求。