CSS中的动态计算
自从CSS的calc()
函数得到浏览器的支持起,在CSS中就可以做一些简单的数学运算。如果你阅读过 图解CSS系列 中的 《CSS函数》一文的话,你会发现现在或将来有更多的函数可以直接帮助我们在CSS做一些计算,比如颜色计算、三角函数的计算等。尤其是CSS的min()
、max()
、clamp()
以及CSS Grid布局模块中的minmax()
出现之后,网页布局带来了革命性的变化。除此之外,在一些特定的环境之下,还有一些其他的CSS函数可以帮助我们做一些动态计数和计算。今天在这篇文章中就来和大家聊聊这方面的话题。
回顾一下
CSS中的函数有很多种:
但我们今天主要和大家探讨其中的几个:
- 数学函数
calc()
:使用calc()
函数可以在CSS中进行数学运算,可以结合CSS的单位一起计算,比如calc(100vw - 300px)
- 比较函数
min()
、max()
和clamp()
:min()
和max()
函数可以接受两个或多个参数,并返回最小或最大的参数(值);clamp()
是min()
和max()
的超集(组合),它接受三个参数,即clamp(MIN, VAL, MAX)
,等同于min(max(MIN, VAL), MAX)
或max(MIN, min(VAL, MAX))
- 网格函数
minmax()
:它接受两个参数,即minmax(MIN, MAX)
,该函数是CSS Grid布局模块中独有的一个函数,通常和另外一个网格函数repeat()
运用在grid-template-columns
属性上 - 计数函数
counter()
:该函数用来设置插入计数器的值,常和CSS的counter-reset
、counter-increment
属性以及伪元素::before
、::after
或::marker
配合content
生成计数器内容,比如实现列表项序列号,大纲列表等
另外,在接下来的内容中会涉及到CSS自定义属性(变量)相关的内容,如果你从未接触过的话,建议你先花一点点时间阅读《图解CSS:CSS自定义属性》和《CSS自定义属性你知多少》,这样更利于你阅读接下来的内容。
更多有关于CSS自定义属性的介绍还可以点击这里阅读。
相对而言,其中min()
、max()
和clamp()
是较新的CSS特性,为此先花一点时间简单向大家介绍一下这三个函数。
min()
、max()
和clamp()
三个函数都常称为CSS的比较函数,其中min()
和max()
相对而言要比clamp()
函数易于理解。
min()
和max()
min()
和max()
都可以接受两个或两个以上的参数,参数之间用逗号分隔,而且参数还可以是一些表达式,比如说一些简单的计算5vw + 5px
,并且这些计算表达式不需要使用calc()
函数。当然,你还可以在一个CSS变量里面进行计算,然后使用这个变量即可。这两个函数可以运用于可接受<length>
,<frequency>
, <angle>
, <time>
, <percentage>
, <number>
和 <integer>
类型值的属性。
min()
和max()
之间的差异只是返回值的不同:
min()
函数会从多个参数(或表达式)中返回一个最小值作为CSS属性的值,即 使用min()
设置最大值,等同于max-width
max()
函数会从多个参数(或表达式)中返回一个最大值作为CSS属性的值,即 使用max()
设置最小值,等同于min-width
比如下面这个是min()
函数的示例:
.element {
width: min(50vw, 500px);
}
这个是max()
函数的示例:
.element {
width: max(50vw, 500px);
}
你可以尝试着在浏览器查看这两个示例,并且改变浏览器视窗的大小,你将看到.element
的变化。
clamp()
clamp()
和min()
以及max()
略有不同,它将返回一个区间值,即 在定义的最小值和最大值之间的数值范围内的一个中间值。该函数接受三个参数:
- 最小值(
MIN
), - 中间值(
VAL
),也称首选值 - 最大值(
MAX
)
clamp(MIN, VAL, MAX)
,这三个值之间的关系(或者说取值的方式):
- 如果
VAL
在MIN
和MAX
之间,则使用VAL
作为函数的返回值 - 如果
VAL
大于MAX
,则使用MAX
作为函数的返回值 - 如果
VAL
小于MIN
,则使用MIN
作为函数的返回值
比如下面这个示例:
.element {
/**
* MIN = 100px
* VAL = 50vw ➜ 根据视窗的宽度计算
* MAX = 500px
**/
width: clamp(100px, 50vw, 500px);
}
就这个示例而言,clamp()
函数的计算会经历以下几个步骤:
.element {
width: clamp(100px, 50vw, 500px);
/* 50vw相当于视窗宽度的一半,如果视窗宽度是760px的话,那么50vw相当等于380px*/
width: clamp(100px, 380px, 500px);
/* 用min()和max()描述*/
width: max(100px, min(380px, 500px))
/*min(380px, 500px)返回的值是380px*/
width: max(100px, 380px)
/*max(100px, 380px)返回的值是380px*/
width: 380px;
}
示例效果如下:
简单地说,clamp()
、min()
和max()
函数都可以随着浏览器视窗宽度的缩放对值进行调整,但它们的计算的值取决于上下文。
特别声明,有关于这三个参数更详细的介绍,可以阅读《聊聊
min()
,max()
和clamp()
函数》一文。
动态计算
前奏有点长,下面开始来聊CSS中的动态计算,我们先从calc()
函数开始。
calc()
大家对calc()
函数的第一印象就是使用它来做一些计算。在一些布局场景中,calc()
能帮助我们快速解决一些麻烦,甚至是实现一些令人感到头疼的问题。比如说,我们在构建一个APP应用的,时常会碰到NavBar均分的效果,如果NavBar的项目数是一个偶数项的话,计算非常简单,但对于一些奇数项来说,让我们无法均分完。拿三等分为例吧,将100%
均分成三份的话,将永远无法均分完:
你可能首先会想到Flexbox布局中的flex: 1
或CSS Grid布局中的grid-template-columns: repeat(3, 1fr)
来实现,正如《你可能不太熟知的布局技巧》文章中介绍的布局示例。这两种方案在某些情况之下是OK的,但当其带有文本,并且某一个文本的长度大于其容器的宽度时,将会打破其均分的状态:
这个时候我们使用calc()
就可以避免打破均分的效果:
.nav__item {
width: calc(100% / 3);
}
你可能也发现了,这样做也有一定的缺陷,那就是文本内容过长时会溢出。如果不希望溢出的话,则需要考虑使用CSS的text-oveflow:ellipsis
用三个点来表示被截取的内容。
我们还可以使用CSS的自定义属性来代表NavBar的项目的数量,这样就可以灵活的根据项目数做计算:
:root {
--i: 3;
}
.nav__item {
width: calc(100% / var(--i));
}
也可以稍微借助JavaScript,根据NavBar的项目数动态设置--i
的值:
const itemNums = document.querySelectorAll('.flex__item').length
document.documentElement.style.setProperty('--i', itemNums)
这里插点额外的东东。前面提到过,当在NavBar项目上内容超长时,即使显式设置flex:1
会也会被长内容打破均分状态,如果希望避免被打破的话,有一个小技巧,只需要在Flex项目上显示设置min-width: 0
即可。
.flex__item {
min-width: 0;
}
.flex__item span {
padding: 0 5px;
font-size: 1.2rem;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
min-width: 0;
width: 100%;
}
接下来,来看一个非常有意思的案例。在以往我们要动态调整元素的大小,很多时候会依赖于CSS的媒体条件,在不同的断点下调整元素的大小。其实,我们可以直接使用calc()
函数在脱离媒体查询也可以实现动态缩放HTML元素大小。这功能随着vw
视窗单位的出现就变得越来越受开发者的喜爱。而且这个功能有一个专业的术语,即 CSS锁(CSS Locks):
CSS锁是一种响应式的Web设计技术,他主要致力于解决响应式设计的文本排版,它允许你根据当前的视窗大小大两个值之间平稳地转换,而不是直接从一个值跳到另一个值。
CSS锁是@Mike Riethmuller在2015年的《Precise control over responsive typography》中首次展示的,但早在2012年的时候@Tim Brown在《Flexible typography with CSS locks》中就提出了该概。
@Tim Brown用了实际生活中的一个示例来描述CSS锁。在运河和河流中都会备有船闸用来控制河中水位,也可以在不同水位的水域之间升降船只:
不过大多时候该技术只是运用于font-size
上。其实该技术还可以运用于任何需要数值的CSS属性上。从font-size
,到width
,再到box-shadow
等。
注意,该技术除了使用calc()
函数之外还需要紧密的结合视窗单位vw
(或vh
),并且该技术有一个核心的计算公式:
calc([min size]px + ([max size] — [min size]) * ((100vw — [min viewport's width]px) / ([max viewport's width] — [min viewport's width])))
先来看calc()
函数的左侧部分,即, [min size]px
。它主要用来给元素设置一个最小的尺寸(即元素最小尺寸),其单位是px
。这样无论怎么缩放元素的最终尺寸都不会是0
。如果你希望元素最小尺寸是240px
时,那么就可以将[min size]
值设置为240
,即[min size]px = 240px
,并且将其放到calc()
公式中:
// [min size] = 240
calc(240px + ([max size] — [min size]) * ((100vw — [min viewport's width]px) / ([max viewport's width] — [min viewport's width])))
calc()
函数后半部分有点复杂,我们拆分出来看。先看([max size] — [min size])
这个部分。我们希望元素在一个尺寸范围内,比如最小[min size]
到最大[max size]
这个范围。而这两者的差值将会作为一个 倍数。假设我们希望元素尺寸范围是在240px ~ 480px
范围内,那么[min size] = 240
,[max size] = 480
,对应的([max size] - [min size])
即是:
// [min size] = 240
// [max size] = 480
([max size] - [min size]) = (480 - 240)
将其放到calc()
中:
// [min size] = 240
// [max size] = 480
calc(240px + (480 — 240) * ((100vw — [min viewport's width]px) / ([max viewport's width] — [min viewport's width])))
再来看乘号(*
)后面的这个部分((100vw — [min viewport's width]px) / ([max viewport's width] — [min viewport's width]))
,也是calc()
中最后面的一部分。这里我们可以给浏览器视窗大小设置一个范围,即 [min viewport's width] ~ [max viewport's width]
,其中[min viewport's width]
是视窗最小值,[max viewport's width]
是视窗最大值。假设你期望的视窗宽度是576px ~ 1400px
范围内,那么[min viewport's width] = 576
,[max viewport's width] = 1400
,即:
// [min viewport's width] = 576
// [max viewport's width] = 1400
((100vw — [min viewport's width]px) / ([max viewport's width] — [min viewport's width])) = ((100vw - 576px) / (1400 - 576))
这将根据浏览器视窗的宽度大小创建一个比例。任何超出576px ~ 1400px
范围的东西都会继续分别向上或向下以线性的速度进行缩放。在这个基础上做一些简化的事情,就是插入一些数值,看看计算出来的比例值。我们使用一些常见的媒体断点的值来替代100vw
,比如:
// »576px
((576px - 576px) / (1400 - 576)) = 0 / 824 = 0
// »768px
((768px - 576px) / (1400 - 576)) = 192 / 824 = 0.2330097
// »992px
((992px - 576px) / (1400 - 576)) = 416 / 824 = 0.5048543
// »1200px
((1200px - 576px) / (1400 - 576)) = 624 / 824 = 0.7572815
// »1400px
((1400px - 576px) / (1400 - 576)) = 824 / 824 = 1
如果我们将之前设置的元素尺寸(即,(480 - 240)
)乘以这个比例,就可以得到一个基于浏览器视口尺寸的元素尺寸的动态值:
// » 576px
(480 - 240) * ((576px - 576px) / (1400 - 576)) = 240 * 0 = 0px
// » 768px
(480 - 240) * ((768px - 576px) / (1400 - 576)) = 240 * 0.2330097 = 55.92px
// » 992px
(480 - 240) * ((992px - 576px) / (1400 - 576)) = 240 * 0.5048543 = 121.17px
// » 1200px
(480 - 240) * ((1200px - 576px) / (1400 - 576)) = 240 * 0.7572815 =181.75px
// » 1400px
(480 - 240) * ((1400px - 576px) / (1400 - 576)) = 240 * 1 = 240px
最后在这个基础上加上元素最小尺寸值,即[min size]px = 240px
:
// » 576px
240px + (480 - 240) * ((576px - 576px) / (1400 - 576)) = 240px + 240 * 0 = 240px + 0px = 240px
// » 768px
240px + (480 - 240) * ((768px - 576px) / (1400 - 576)) = 240px + 240 * 0.2330097 = 240px + 55.92px = 295.92px
// » 992px
240px + (480 - 240) * ((992px - 576px) / (1400 - 576)) = 240px + 240 * 0.5048543 = 240px + 121.17px = 361.17px
// » 1200px
240px + (480 - 240) * ((1200px - 576px) / (1400 - 576)) = 240px + 240 * 0.7572815 = 240px + 181.75px = 421.75px
// » 1400px
240px + (480 - 240) * ((1400px - 576px) / (1400 - 576)) = 240px + 240 * 1 = 240px + 240px = 480px
也就是说,如果我们希望元素的宽度会是:
- 浏览器视窗宽度为
576px
时,元素的宽度为240px
- 浏览器视窗宽度为
1400px
时,元素的宽度为480px
可以像下面这样使用:
.element {
width: calc(240px + (480 - 240) * ((100vw - 576px) / (1400 - 576)))
}
来看一个具体的示例:
如果你希望继续深入的了解这方面的知识,可以阅读前期整理的一篇文章《给CSS加把锁》。
要是你阅读过《CSS自定义属性的使用实例》一文,你会发现在文章中很多示例都有calc()
的身影,比如说使用它来动态计算颜色:
:root {
--h: 80;
--s: 20;
--l: 60;
--threshold: 65;
--border-threshold: 80;
}
.button {
--hue: var(--h);
--switch: calc((var(--l) - var(--threshold)) * -100%);
--border-light: calc(var(--l) * 0.7%);
--border-alpha:calc((var(--l) - var(--border-threshold)) * 10);
background: hsl(var(--hue), calc(var(--s) * 1%), calc(var(--l) * 1%));
color: hsl(0, 0%, var(--switch));
border:.2em solid hsla(var(--hue), calc(var(--s) * 1%), var(--border-light), var(--border-alpha));
}
.button--secondary {
--hue: calc(var(--h) + 60);
}
还要以使用它来实现aspect-ration
的效果(实现宽高比缩放):
.rect {
--ratio-w: 4;
--ratio-h: 3;
--aspect-ratio: calc(var(--ratio-w) / var(--ratio-h));
--width: 400;
width: calc(var(--width) * 1px);
height: calc(var(--width) * 1px / var(--aspect-ratio));
}
这个示例使用的是calc()
和CSS自定义属性来实现aspect-ratio
的特性,如果你使用的是现代浏览器的话就不必这么麻烦了,可以直接使用aspect-ratio
。如果你对aspect-ratio
特性感兴趣的话,可以阅读《使用CSS的aspect-ratio
实现宽高比缩放》一文。
min()
、max()
和clamp()
这里不会详细介绍min()
、max()
和clamp()
函数的具体使用,有关于这几个函数的具体分析可以阅读《聊聊min()
,max()
和clamp()
函数》一文。在这一节我们主要和大家探讨在实际开发中如何使用这几个函数,以及在使用的时候他们存在什么边缘,需要注意什么?通过这些简单的实例,你可以更清楚的知道,这几个函数能在实际业务开发中能帮助我们做些什么?
先从简单的min()
和max()
开始,因为clamp()
是他们的超集,相对复杂一点。
假设我们使用min()
函数给你元素的width
属性设置了一个值,比如:
.element {
width: min(1px, 50vw, 50%);
}
min()
函数会从列表值中返回最小值,因此,.element
的width
值是1px
。
在W3C的 CSS值和单位模块Level 4(CSS Values and Units Module Level 4) 有过这样的一段描述:
An occasional point of confusion when using
min()
/max()
is that you usemax()
to impose a minimum value on something (that is, properties likemin-width
effectively usemax()
), andmin()
to impose a maximum value on something; it’s easy to accidentally reach for the opposite function and try to usemin()
to add a minimum size. Usingclamp()
can make the code read more naturally, as the value is nestled between its minimum and maximum.
大致的意思是,在使用min()
或max()
函数时,偶尔会有一点混淆点,那就是你使用max()
给某个元素设置了一个最小值(应该像给元素设置了min-width
),而使用min()
会给某个元素设置一个最大值(类似于max-width
)。很容易让开发者使用min()
给元素设置一个最小值(min-width
)。使用clamp()
可以让代码在阅读和理解方面更自然,因为值被嵌套在其最小值和最大值之间。
来看一个简单的示例,示例中有三个div
元素,其中前两个使用min()
函数给其设置width
值,最后一个使用max()
函数给其设置width
值:
.box1 {
width: min(150px, 300px);
}
.box2 {
width: min(50px, 150px, 300px);
}
.box3 {
width: max(150px, 300px);
}
示例中三个div
的width
分别是150px
、50px
和300px
,最终效果如下:
上面的示例有一个明显的特征,非常明显的将带有单位的返回值作为属性的输入值,即 在任何需要单位的函数中使用它们。而在一些场景中是不需要带有单位的,比如说颜色值的描述。来看一个简单的示例,看如何使用min()
和max()
函数来给hsl
赋值,比如:
.element {
background-color: hsl(
min(50, 100, 150),
max(50%, 75%),
max(25%, 50%)
)
}
你可有在自己的脑海中立马会想到hsl()
的值是hsl(50, 75%, 50%)
:
要是将CSS自定义属性引入进入的话,我们还可以通过CSS的媒体查询可者JavaScript来改变CSS自定义属性的值,让我们在一些特定的条件之下动态的改变颜色,比如下面的示例:
:root {
--base-hue: 60;
--extreme-hue: 300;
--base-saturation: 60%;
--region-staturation: 40%;
--base-light: 70%;
--settled-light: 30%;
}
@media only screen and (min-width: 600px) {
:root {
--extreme-hue: 1;
--base-hue: 359;
}
}
body {
--start-color: hsl(
min(var(--base-hue), var(--extreme-hue)),
max(var(--base-saturation), var(--region-staturation)),
min(var(--base-light), var(--settled-light))
);
--stop-color: hsl(
max(var(--base-hue), var(--extreme-hue)),
min(var(--base-saturation), var(--region-staturation)),
max(var(--base-light), var(--settled-light))
);
background-image: linear-gradient(
to right,
var(--start-color),
var(--stop-color)
)
}
尝试手动拖动浏览器,改变视窗宽度将会看到下图这样的效果:
我们可以在上面的示例基础上去掉媒体查询那部分代码,使用JavaScript的.setProperty()
来动态改变CSS自定义属性的值:
const root = document.documentElement;
function setRandomVariables() {
const variablesArray = [
{
"--base-hue": 60,
"--extreme-hue": 300,
"--base-saturation": "60%",
"--region-saturation": "40%",
"--base-light": "70%",
"--settled-light": "30%"
},
{
"--base-hue": 75,
"--base-saturation": "20%",
"--region-saturation": "80%",
"--base-light": "30%"
},
{
"--base-hue": 50,
"--region-saturation": "5%",
"--settled-light": "10%"
},
{
"--extreme-hue": 5
}
];
const selectedVariables = Object.entries(
variablesArray[Math.floor(Math.random() * variablesArray.length)]
);
selectedVariables.forEach((cssvars) => {
root.style.setProperty(cssvars[0], cssvars[1]);
});
}
let intervalID = window.setInterval(setRandomVariables, 2000);
效果如下:
你或许会说,这有啥动态的。还不是依赖于CSS媒体查询或JavaScript动态的调整CSS自定义属性的值。嗯,是这样的,但也不全是。不知道你是否留意过,我们在《CSS自定义属性》一文中还提到了CSS自定义属性另一个特性,即 CSS自定义属性和calc()
结合可以实现 if ... else
的效果。比如,我们有一个自定义属性--i
,当:
--i
的值为1
时,表示真(即打开)--i
的值为0
时,表示假(即关闭)
来看一个小示例,我们有一个容器.box
,希望根据自定义属性--i
的取值为0
或1
做条件判断:
- 当
--i
的值为1
时,表示真,容器.box
旋转30deg
- 当
--i
的值为0
时,表示假,容器.box
不旋转
代码可能像下面这样:
:root {
--i: 0;
}
.box {
// 当 --i = 0 » calc(var(--i) * 30deg) = calc(0 * 30deg) = 0deg
// 当 --i = 1 » calc(var(--i) * 30deg) = calc(1 * 30deg) = 30deg
transform: rotate(calc(1 - var(--i)) * 30deg))
}
.box.rotate {
--i: 1;
}
或者
:root {
--i: 1;
}
.box {
// 当 --i = 0 » calc((1 - var(--i)) * 30deg) = calc((1 - 0) * 30deg) = calc(1 * 30deg) = 30deg
// 当 --i = 0 » calc((1 - var(--i)) * 30deg) = calc((1 - 1) * 30deg) = calc(0 * 30deg) = 0deg transform: rotate(calc((1 - var(--i)) * 30deg))
}
.box.rotate {
--i: 0;
}
整个效果如下图:
有关于这方面更详细的介绍,还可以阅读:
- 如何通过CSS自定义属性给CSS属性切换提供开关
- DRY Switching with CSS Variables: The Difference of One Declaration
- DRY State Switching With CSS Variables: Fallbacks and Invalid Values
- Logical Operations with CSS Variables
而在CSS中除了媒体查询之外,还可以借助CSS的伪类选择器让属性值做出相应的变化。还可以将CSS自定义属性的切换开关结合起来。
再来看clamp()
吧。相比较而言,clamp()
要比min()
和max()
更易于理解。clamp()
可以让我们在三个值(clamp()
中的MIN
、VAL
和MAX
)取出MIN ~ MAX
之间的一个范围值。比如:
下图中是首先值(VAL
)是50%
,最小值(MIN
)是300px
和最大值(MAX
)是800px
。根据前面的介绍,clamp(MIN, VAL, MAX)
的作用是:
- 如果
VAL
在MIN
和MAX
之间,则使用VAL
作为函数的返回值 - 如果
VAL
大于MAX
,则使用MAX
作为函数的返回值 - 如果
VAL
小于MIN
,则使用MIN
作为函数的返回值
在CSS中,单位取值为%
时,会根据上下文来做出相应的计算(有关于这方面详细的介绍可以阅读《CSS中百分比单位计算方式》一文)。为了让大家更易于理解,将上图中的clamp(300px, 50%, 800px)
运用于img
,而且它的上下文是:
<!-- HTML -->
<body>
<img src="photo.jpg" alt="image" />
</body>
/* CSS */
img {
width: clamp(300px, 50%, 800px);
height: auto;
}
假设:
- 视窗的宽度是
1100px
,那么50%
对应的是550px
,它在VAL
和MAX
之间,那么img
的宽度是550px
- 视窗的宽度是
1624px
,那么50%
对应的是812
,它大于MAX
,那么img
的宽度是800px
- 视窗的宽度是
560px
,那么50%
对应的是280px
,它小于MIN
,那么img
的宽度是300px
我们可以将这种技术用于响应式的布局中,比如说,我们要把一个页面容器的宽度设置在16rem ~ 70rem
之间,而且容器首选宽度是90vw
时,可以像下面这样应用:
.wrapper {
width: clamp(16rem, 90vw, 70rem)
}
可以得到像下图这样的效果:
上图来自于《[Use CSS Clamp to create a more flexible wrapper utility](https://piccalil.li/quick-tip/use-css-clamp-to-create-a-more-flexible-wrapper-utility》一文。
大家还记得前面介绍的?使用calc()
实现CSS锁的功能。有了clamp()
会让事情变得更为简单。比如说:
- 视窗小于或等于
360px
时font-size
(也可以是比的属性)值是1rem
- 视窗在
361px ~ 839px
之间时font-size
的值在1rem ~ 3.5rem
间自动变化 - 视窗大于或等于
840px
时font-size
值是3.5rem
使用clamp()
实现这样的效果可以像下面这样做。先设置:
- 最小字号,比如
1rem
- 最大字号,比如
3.5rem
- 最小视窗宽度,比如
360px
- 最大视窗宽度,比如
840px
如果把单位都转换为rem
的话,我们可以将视窗宽度都用rem
来表示。CSS中的rem
都是基于html
元素的font-size
来计算,一般情况之下,浏览器的html
的font-size
值是16px
,那么视窗宽度的360px
和840px
对应的值分别是22.5rem
和52.5rem
。
如果我们在一个坐标系统中来描述视窗宽度和字号之间的关系的话,可以用x
坐标来表示视窗宽度,y
的值表示font-size
:
可以用相应的数学公式来描述:
slope = (maxFontSize - minFontSize) / (maxWidth - minWidth)
yAxisIntersection = -minWidth * slope + minFontSize
根据上面计算公式,可以得到的斜率值为0.08333333333333333
,y
轴处的交点值为-0.875
:
slope = (3.5 - 1) / (52.5 - 22.5) = 2.5 / 30 = 0.08333333333333333
yAxisIntersection = -22.5 * 0.08333333333333333 + 1 = -0.875
根据斜率值和y
轴处的交点值,可以计算出clamp()
函数中的首先值:
VAL = yAxisIntersection[rem] + (slope * 100)[vw]
即:
VAL = -0.875rem + 8.333vw
这个时候clamp()
的值为:clamp(1rem, -0.875rem + 8.333vw, 3.5rem)
。即:
.element {
font-size: clamp(1rem, -0.875rem + 8.333vw, 3.5rem)
}
要是和CSS自定义属性结合起来的话,可以像下面这样:
:root {
--maxViewportWidth: 52.5; // 840px相对于16px计算出来的rem值
--minViewportWidth: 22.5; // 360px相对于16px计算出来的rem值
--minFontSize: 1; // 最小字号font-size的 rem值
--maxFontSize: 3.5; // 最大字号font-size的 rem值
--f-slope: ((var(--maxFontSize) - var(--minFontSize)) / (var(--maxViewportWidth) - var(--minViewportWidth)));
--yAxisIntersection: (-1 * var(--minViewportWidth) * var(--f-slope) + var(--minFontSize))
--clamp: clamp(
var(--minFontSize) * 1rem,
var(--yAxisIntersection) * 1rem + (var(--f-slope) * 100vw),
var(--maxFontSize) * 1rem
)
}
.element {
font-size: var(--clamp)
}
这种方式同样可以像calc()
函数一样,运用于任何接受<length>
,<frequency>
, <angle>
, <time>
, <percentage>
, <number>
和 <integer>
类型值的属性。
另外 @Mathias Hülsbusch 在 《The Raven Technique: One Step Closer to Container Queries》介绍了一种叫作“乌鸦技术”(Raven Technique)。简单地说,将CSS的calc()
、min()
、max()
、clamp()
函数与CSS自定义属性(条件判断和逻辑运算)结合在一起,实现一种优秀的布局效果,即 根据容器查询条件(Containeer Queries) 实现不同的布局效果,比如下面这样的效果:
在前面提到,CSS自定义属性可以让我们实现0
和1
之间的切换(也就是模拟JavaScript中的if ... else
的效果)。如果将这个功能和CSS的clamp()
函数结合在一起的话,可以让进度条在不同的百分值下有不同的颜色:
上图的示例来自于 @Yair Even Or 在《CSS Switch-Case Conditions》示例演示。
@Yair Even Or 在《CSS Switch-Case Conditions》在文章中介绍了,使用clamp()
和calc()
和CSS自定义属性实现的条件切换的效果。具体的代码可以查看作者在Codepen提供的示例:
在示例中,还使用到了一些 CSS Houdini 的技术,如果你从未接触过的话,建议你花点时间阅读下面两篇文章:
CSS Grid 中的 minmax()
在《你可能不太熟知的布局技巧》和《响应式网格布局》中都提到过,我们可以使用CSS Grid 布局模块中的minmax()
函数 和 CSS Grid的repeat()
以及 auto-fit
(或auto-fill
)等特性,在不依赖任何CSS媒体查询特性就可以实现响应式布局效果,特别是那种根据不同容器宽度实现不同的布局:
.grid__container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2vh;
}
效果如下:
W3C规范是这样描述minmax(MIN, MAX)
的,即minmax()
函数定义一个大于或等于MIN
,小于或等于MAX
范围的值。该函数一般只用于CSS Grid布局中的grid-template-columns
和grid-template-rows
属性上。比如下面这个示例:
.grid__container {
grid-template-columns: minmax(200px, 500px) 1fr 1fr;
}
注意,上图来自于 @shadeed9 的《A Deep Dive Into CSS Grid minmax()
》一文。
从上图中不难发现,grid-template-columns
中定义了一个三列网格,其中第一列的宽度是minmax(200px, 500px)
,其意思是列的最小宽度是200px
,最大宽度是500px
,即第一列的宽度是在200px ~ 500px
之间。第二列和第三列是1fr
,意味着这两列的宽度将会均分容器剩余下来的空间。
有关于CSS Grid中
fr
的介绍,可以阅读《CSS Grid带来的新单位:分数单位fr
》。
就该示例而言,如果视窗宽度小到一定程度的时候,就会出现滚动条,如下:
如果要达到示例中的效果,就需要使用repeat()
函数,不过repeat()
函数有一个特征,表示重复。网格列的宽度都是一样的,比如:
.grid__container {
grid-template-columns: minmax(200px, 1fr) minmax(200px, 1fr) minmax(200px, 1fr);
}
换成repeat()
可以写成:
.grid__container {
grid-template-columns: repeat(3, minmax(200px, 1fr));
}
仅此还不行,还需要使用auto-fill
或auto-fit
来替代repeat()
函数表示重复的数量(比如示例中的3
)。两者的区别是:auto-fit
会扩展网格项,以填充网格容器的可用空间,而auto-fill
不会扩展网格项,而将保留网格容器可用空间,也不会改变网格项的宽度。比如:
来看一个示例,@shadeed9 的《A Deep Dive Into CSS Grid minmax()
》文中提到的一个示例:
希望达到的效果是,随着视窗宽度变化卡片都有一个最佳的效果。实现这样的一个效果,首先会想到minmax()
:
.wrapper {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-gap: 1rem;
}
上面代码在很多场景之下都会有一个好的效果,但当视窗宽度小于250px
时就会出现水平滚动条。在CSS中要解决这个问题,方案有很多种,比如媒体查询:
.wrapper {
display: grid;
grid-template-columns: 1fr;
grid-gap: 1rem;
}
@media (min-width: 300px) {
.wrapper {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
}
除了媒体查询之外,还可以使用前面提介绍的比较函数,比如min()
:
.wrapper {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(100%, 250px), 1fr));
grid-gap: 1rem;
}
示例中minmax()
和min()
结合在一起使用,它们所起的作用是:
- 当视窗宽度小于
250px
,那么minmax()
函数的第一个值将是父宽度的100%
- 当视窗宽度大于
250px
,那么minmax()
函数的第一个值将是250px
具体效果如下:
不过在使用minmax()
函数时有两个细节要注意:
- 如果
minmax(MIN, MAX)
中MIN
大于MAX
,那么MAX
将会被忽略,minmax(MIN, MAX)
将会取MIN
值 - 如果
minmax(MIN, MAX)
中MIN
值是1fr
时,MIN
值将是无效的,整个声明将被忽略;但它对MAX
值有效
因此,在minmax(MIN, MAX)
中的MIN
使用%
或vw
值是需要特别注意,它会根据父容器或视窗的宽度做计算,计算出来的值很有可能会比MAX
值大,最终就有可能会取MIN
的值。这个时候你将看到auto-fit
和auto-fill
相同的效果。
如果想了深入了解minmax()
的话,可以阅读@shadeed9 的《A Deep Dive Into CSS Grid minmax()
》一文。
counter()
和 counters()
在《伪元素能帮助我们做些什么》和 《聊聊CSS的::marker
》提到过,使用CSS的counter()
函数和CSS的content
可以给HTML的任何元素提供类似ol
列表的有序列表计数效果:
比如下面这个示例:
<!-- HTML -->
<div>
<p>Lorem ipsum dolor ...</p>
<p>Lorem ipsum dolor ...</p>
<p>Lorem ipsum dolor ...</p>
<p>Lorem ipsum dolor ...</p>
<p>Lorem ipsum dolor ...</p>
</div>
/* CSS */
p {
counter-increment: section;
}
p:nth-child(even):before {
content: counter(section);
}
p:nth-child(odd):before {
content: counter(section);
}
效果如下:
上面的示例,我们使用counter()
和counter-increment
实现计数。除此之外,可以直接在::marker
伪元素的content
中使用counter(list-item)
或counters(list-item, '.')
。
但是非列表元素,哪怕是设置了display:list-item
,直接在::marker
的content
中使用counters(list-item, '.')
所起的效果和我们预期的有所不同。如果在非列表元素的::marker
的content
中使用counters()
达到我们想要的效果,需要使counter-reset
先声明计数器标识符,然后counter-increment
调用已声明的计数器标识符(回归到以前::before
的使用)。具本的可以看下面的示例代码:
::marker {
content: counter(list-item);
padding: 5px 30px 5px 12px;
background: linear-gradient(to right, #f36, #f09);
font-size: 2rem;
clip-path: polygon(0% 0%, 75% 0, 75% 51%, 100% 52%, 75% 65%, 75% 100%, 0 100%);
border-radius: 5px;
color: #fff;
text-shadow: 1px 1px 1px rgba(#09f, .5);
}
.box:nth-child(2n) ::marker {
content: counters(list-item, '.');
}
.box:nth-child(3) {
section {
counter-reset: item;
}
article {
counter-increment: item;
}
::marker {
content: counters(item, '.');
}
}
除此之外,使用CSS Houdini的自定义属性,还可以做一些动态计数,比如下面这个效果:
小结
上面大家所看到的就是CSS中现在已经提供的一些动态计算的能力。特别是CSS自定义属性的到来之后,这方面的能力变得更强,当然也让CSS变得更难于理解,带来了一定的复杂性。比如我们可以使用CSS自定义属性帮助我们做条件判断,做简单的逻辑运算,甚至还可以实现随机效果。在后面将和大家一起探讨在CSS中如何实现随机运算,如果你对这方面感兴趣的话,欢迎关注后续的相关更新。