搞过响应式布局的都晓得,100vw 能直接拿到视口宽度,但这玩意儿是个长度单位,不是数字。想根据屏幕大小改透明度、旋转角度或者动画进度?直接拿 100vw 根本没法算。直到有人发现了个骚操作——用三角函数 tan() 和 atan2() 硬生生把长度转成整数。这招最早是2023年捣鼓出来的,当时就为了做个图片透明度随窗口变化的效果,结果发现能玩的花样太多了。
整活前的准备
先搞清楚核心逻辑:atan2(100vw, 1px) 会把长度转成弧度(一种角度单位),再用 tan() 把它变回数字。但浏览器对这套不太感冒,得套多层壳才能跨平台跑通。
下面这段代码看着像黑魔法,实际就是定义了一个自定义属性 --100vw 来存视口宽度,然后算出整数版的宽度 --int-width。
@property --100vw {
syntax: "<length>";
initial-value: 0px;
inherits: false;
}
:root {
--100vw: 100vw;
--int-width: calc(10000 * tan(atan2(var(--100vw), 10000px)));
}打个比方,屏幕宽 800px 时,--int-width 就变成了 800 这个整数。不过直接拿 800 去调透明度还是不方便,因为属性值的范围五花八门——透明度要 0~1,旋转要 0~360deg,位移要 0%~100%。所以得再套一层归一化,把 --int-width 压成 0 到 1 之间的数,这里叫它 --wideness。
设定两个边界值,比如窄屏 400px 时 --wideness 为 0,宽屏 1200px 时 --wideness 为 1。低于 400 或高于 1200 就钳住不动。
:root {
--lower-bound: 400;
--upper-bound: 1200;
--wideness: calc(
(clamp(var(--lower-bound), var(--int-width), var(--upper-bound)) - var(--lower-bound)) / (var(--upper-bound) - var(--lower-bound))
);
}clamp 把 --int-width 锁死在 400~1200 之间,减掉下限再除以上下限差值,就能得到一个顺滑的 0~1 过渡值。这下想改啥属性都方便了,比如透明度直接写成 opacity: var(--wideness);。
标题蹦迪的完整流程
拿一个标题做例子,HTML 分成两个 span,因为 CSS 没法单独选中句子里的某个词。
<h1><span>Resize</span> and <span>enjoy!</span></h1>先干掉默认换行,让标题绝对定位在中间。
h1 {
position: absolute;
white-space: nowrap;
}期望的效果是:屏幕变窄时,第一个 span 往右上角移动,第二个 span 往左下角移动。定义 --direction 变量,1 表示正向,-1 表示反向。
h1 span {
display: inline-block;
position: relative;
bottom: calc(1.2lh * var(--direction));
left: calc(50% * var(--direction));
transform: translate(calc(-50% * var(--direction)));
}
h1 span:nth-child(1) {
--direction: 1;
}
h1 span:nth-child(2) {
--direction: -1;
}这时候还没加 --wideness,所以位置是固定的。加上 --wideness 之后,直接乘上去会发现方向反了——屏幕越宽反而越靠近中间。原因是想要窄屏时完成移动,而 --wideness 是从 0 涨到 1,所以得反过来减。
h1 span {
bottom: calc((1.2lh - var(--wideness) * 1.2lh) * var(--direction));
left: calc((50% - var(--wideness) * 50%) * var(--direction));
transform: translate(calc((-50% - var(--wideness) * -50%) * var(--direction)));
}这样窄屏时 --wideness 接近 0,括号里算出来就是完整偏移量;宽屏时 --wideness 接近 1,偏移量趋近 0,文字回到原位。
但问题来了——文字走直线,中间会跟中央词块撞车。得让 X 轴移动速度比 Y 轴快一倍,走个弧线绕过去。用 min(var(--wideness) * 2, 1) 把速度翻倍但不超过上限。
h1 span {
left: calc((50% - min(var(--wideness) * 2, 1) * 50%) * var(--direction));
transform: translate(calc((-50% - min(var(--wideness) * 2, 1) * -50%) * var(--direction)));
}最终效果:屏幕从 400px 拉到 1200px,两个 span 沿着弧线优雅归位,中间不带一丝卡顿。
不同场景的转换套路
--wideness 不只是能调位置,任何数值属性都能套。下面这个表格整理了常见属性的转换公式:
| 属性类型 | 单位 | 转换公式 |
|---|---|---|
| 透明度 | 无 | calc(var(--wideness)) |
| 旋转角 | deg | calc(var(--wideness) * 360deg) |
| 百分比偏移 | % | calc(var(--wideness) * 100%) |
| 长度位移 | px | calc(var(--wideness) * 200px) |
| 灰度滤镜 | 无 | calc(var(--wideness)) |
拿旋转来说,想让一个方块从 0deg 转到 360deg,直接写 transform: rotate(calc(var(--wideness) * 360deg));。屏幕从 400px 拉到 1200px,方块就完整转一圈。
如果要反向过渡(比如窄屏时全转,宽屏时归零),用 1 - var(--wideness) 就行。透明度从 1 变到 0:opacity: calc(1 - var(--wideness));。
边界值也能自定义。比如想让过渡发生在 600px 到 1000px 之间,改 --lower-bound: 600; 和 --upper-bound: 1000;。低于 600 时 --wideness 恒为 0,高于 1000 时恒为 1。
这种玩法就像给 CSS 装了个传感器,视口宽度不再是一个死板的尺寸,而是一个可以参与运算的动态整数。从文字弧线移动到背景色渐变,从图片缩放比例到滚动视差强度,只要想得到,就能往里套。
