CSS也能做数学题,typed arithmetic到底给网页设计带来了什么新花样

2,836字
12–18 分钟
in

最近在捣鼓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单位,这是容器查询里用的内联尺寸单位,相当于是容器宽度的百分比。