视口宽度咋转成整数,那些花里胡哨的过渡动画到底咋搞?

2,690字
11–17 分钟
in

搞过响应式布局的都晓得,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))
旋转角degcalc(var(--wideness) * 360deg)
百分比偏移%calc(var(--wideness) * 100%)
长度位移pxcalc(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 装了个传感器,视口宽度不再是一个死板的尺寸,而是一个可以参与运算的动态整数。从文字弧线移动到背景色渐变,从图片缩放比例到滚动视差强度,只要想得到,就能往里套。