最近在捣鼓CSS动画的时候,发现了一个叫typed arithmetic的新特性,这东西听着挺唬人,实际上就是让CSS里的数学运算变得更聪明了。以前想在CSS里做点复杂计算,动不动就得搬出JavaScript,现在好了,光靠CSS自己就能搞定单位之间的转换和计算。这篇文章就聊聊这个新玩意儿怎么用,以及它能整出哪些有意思的效果。
数据类型,CSS里的数字也有身份证
什么是CSS数据类型
写CSS的时候,其实一直在跟不同类型的数据打交道。比如给一个元素设置宽度,写width: 200px,这里的px就是一种长度单位,属于<length>这个数据类型。设置透明度的时候写opacity: 0.5,这里的0.5就是纯粹的<number>数字类型。还有旋转角度用的deg,动画时长用的s或者ms,这些都属于不同的数据类型。
可以把这些数据类型想象成不同种类的工具,每种工具都有自己擅长的活儿。<length>负责尺寸,<angle>负责角度,<time>负责时间,<number>就是个纯数字。不同的CSS属性只认特定的数据类型,比如rotate()函数只接收<angle>类型的值,给个px进去肯定报错。
能算和不能算的类型
不过不是所有数据类型都能参与数学运算。像<color>、<string>这种,本身就是没法做加减乘除的。"red"乘以"blue"这种表达式,听着就像天方夜谭。所以玩typed arithmetic的时候,主要盯住<length>、<angle>、<number>、<time>这些本身就带数值属性的类型就行。
加减乘除,CSS数学运算的规矩
加减法不能跨类型
做加法或者减法的时候,有个硬性规定:参与运算的值必须是同一种数据类型。calc(3em + 45deg)这种写法,就像把苹果和橘子加在一起,结果是没法用的。但是同一种类型内部的不同单位可以混着来,比如calc(4em + 20px)就没问题,浏览器会自动换算。
乘法只能乘纯数字
乘法运算有个限制,只能乘以<number>类型的纯数字。写calc(3px * 7)没问题,结果是21px。但如果写成calc(10px * 10px),这就尴尬了。按照常规数学,10px乘以10px应该等于100平方像素,可惜CSS里根本没有平方像素这个概念,所以这种写法是不允许的。
除法玩法变多了
除法这块是这次typed arithmetic最出彩的地方。以前写calc(70px / 10px)是不行的,但现在浏览器开始支持这种写法了,结果会返回一个纯数字7。这看起来就是个小小的改变,但带来的可能性可大了去了。
把角度变成距离,螺旋排列这么玩
i {
--angle: calc(sibling-index() * 10deg);
--distance: calc(var(--angle) / 360deg * 180px);
}这段代码演示了怎么把角度转换成距离。每个<i>元素按照索引值获得一个角度,第一个转10度,第二个转20度,以此类推。然后通过var(--angle) / 360deg,把角度除以360度,得到一个不带单位的纯数字,这个数字代表了当前角度占一圈的比例。最后再乘以180px,就得到了对应的距离值。
这样一来,每个元素的旋转角度和离中心的距离就形成了一个固定的比例关系,排列出来的螺旋线会特别规整。就算改动每个元素的角度,它们依然会乖乖地待在螺旋线上。
除法的单位有讲究
用除法得到纯数字的时候,除数的单位很关键。同样是算屏幕宽度,calc(100vw / 1px)得到的是屏幕宽度的像素数量,假设屏幕宽1080px,结果就是1080。而calc(100vw / 1em)得到的是屏幕宽度能容纳多少个em单位,假设1em等于16px,结果就是67.5。两个结果都是纯数字,但代表的含义完全不同。
不用媒体查询,也能做出响应式旋转
p {
rotate: calc(((clamp(300px, 100vw, 700px) - 700px) / 400px) * -90deg);
}这段代码只用了一行CSS,就实现了根据屏幕宽度自动调整旋转角度。先通过clamp(300px, 100vw, 700px)把屏幕宽度限制在300px到700px之间,然后减去700px,得到一个从-400px到0px的范围。再除以400px,得到一个从-1到0的纯数字。最后乘以-90deg,把这个范围映射到0deg到90deg之间。屏幕越宽,旋转角度就越大,整个过程完全没有用到媒体查询。
字体大小决定透明度,这事能办
p {
font-size: var(--font-size, 1rem);
opacity: calc(1 - (var(--font-size, 1rem) - 0.8rem) / 2.4rem);
}
p::after {
counter-reset: px calc(var(--font-size, 1rem) / 1px);
content: counter(px) 'px';
}想让小字更清晰、大字更淡雅,这段代码就做到了。通过自定义属性--font-size控制字体大小,范围从0.8rem到2rem。先减去0.8rem,得到0到1.2rem的范围,除以2.4rem(两倍的范围),得到一个0到0.5的纯数字,再用1减去这个值,透明度就落在0.5到1之间。字体越大,透明度越低,颜色越淡。
::after伪元素里还做了个好玩的事,把rem单位的字体大小除以1px,得到了以像素为单位的纯数字,再用counter显示出来,这样就能直观地看到当前字体到底有多大。
宽度变颜色,拖拽就能看效果
p {
--hue: calc(100cqi / 1px);
background-color: hsl(var(--hue, 0) 75% 25%);
}这个例子更直接,把元素的宽度(用cqi单位)除以1px,得到一个纯数字,直接用这个数字作为HSL色相的值。拖拽改变元素宽度的时候,背景颜色会跟着实时变化。因为元素默认宽度是屏幕宽度的50%,在不同设备上打开,初始颜色可能完全不同,但这一切都发生在CSS内部,没碰一行JavaScript。
宽度控制弹跳轨迹,抛物线的玩法
.ball {
--translateY: calc(sin(calc(100cqi / 300px) * 180deg) * -200px) ;
translate: -50% var(--translateY, 0);
}这段代码实现了一个弹跳小球,它的运动轨迹完全由下面那条线的宽度决定。先把元素的宽度(100cqi)除以300px,得到一个0到1之间的纯数字,乘以180deg,得到0deg到180deg的角度。用sin()函数把这个角度转换成-1到1之间的纯数字,但注意sin函数在0到180度之间先增后减,形成的数值分布是抛物线的形状。最后乘以-200px,就得到了小球的垂直位移。
不管下面那条线有多宽,小球都会沿着同样的抛物线轨迹运动。这里还用了cqi单位,这是容器查询里用的内联尺寸单位,相当于是容器宽度的百分比。
