◆

Windi CSS transform 工具类:旋转、缩放、位移

2023-02-02 01:370

CSS 的 transform 是个复合属性,旋转、缩放、位移、倾斜全挤在一个属性里,多个函数用空格连接。这带来一个实际麻烦:后写的 transform 会整体覆盖先写的,想单独调整其中一项就得把整串重写。Windi CSS 的做法是把每种变换拆成独立的 CSS 变量(--tw-rotate、--tw-scale-x 之类),transform 属性里引用这些变量,于是各个工具类互不干扰,可以自由叠加。

正因为这个实现方式,用变换类时必须额外加一个 transform 类来声明那条引用变量的 transform 属性。只写 rotate-45 而不写 transform,变量设置了但没人用,页面上看不出任何效果——这是最常见的踩坑点。下面示例里 :class 数组带着 'transform ...' 就是为了这个。

另外要留意变换的执行顺序。transform 里函数从左到右依次应用,但视觉上的效果是从右往左叠加的,rotate 之后再 translate,位移方向会跟着旋转后的坐标系走。Windi 生成的 transform 属性里函数顺序是固定的(translate → rotate → skew → scale),所以你写类名的先后不影响结果,这点和手写 CSS 不一样。

下面是各类变换的工具类与对应 CSS 属性的对照表:

简写对应属性
origin-[left right top bottom lt lb rt rb]transform-origin: center;
rotate-45 rotate-y-45 rotate-x-45 rotate-z-45transform-rotate: 45deg;
scale-45 scale-y-45 scale-x-45 scale-z-45transform-scale: 0.45;
skew-x-45 skew-y-45transform-skew: 45deg;
translate-x-45px translate-y-10pxtranslate: 10px;

逐条说几个容易混淆的地方。

origin-* 控制变换的基准点,也就是旋转围绕哪个点转、缩放朝哪个方向缩。默认是 center,改成 origin-top-left 之后 rotate-45 就变成绕左上角旋转,视觉位置差别很大。表格里的 lt、lb、rt、rb 是左上、左下、右上、右下的缩写形式。

rotate-* 的数值单位是度。带轴向的 rotate-x-45、rotate-y-45 是绕 X、Y 轴的三维旋转,光加这个类效果会很弱甚至看不出来——三维旋转需要透视才有立体感,得给父元素加 perspective 相关的类,否则等于正交投影下的压缩。不带轴向的 rotate-45 等价于 rotate-z-45,是平面内旋转,用得最多。

scale-* 的数值是百分比而非倍数,scale-45 对应 scale(0.45),scale-150 才是放大到 1.5 倍。这点和 rotate 的直接对应关系不同,很容易记错写成 scale-1.5。

translate-* 支持写具体单位,translate-x-45px 就是 45 像素。也可以用百分比 translate-x-1/2,百分比是相对元素自身尺寸计算的——所以经典的居中技巧 left-1/2 -translate-x-1/2 才成立:left 的 50% 相对父容器,translate 的 50% 相对自身。数值前加 - 表示反方向。

示例

下面这个 Vue 组件用嵌套循环把各组变换类批量渲染出来,每个方块显示自己应用的类名,方便直观对比效果差异:

<template>
  <div class="flex justify-around align-middle flex-wrap bg-black">
    <div
      v-for="(value, index) in borderCfg"
      :key="index"
      class="flex align-middle justify-between w-screen mt-10px"
    >
      <div
        v-for="(item, subIndex) in value"
        :key="subIndex"
        :style="{ width: 100 / value.length + '%' }"
      >
        <div
          :class="[baseClass, item, 'transform bg-contain bg-no-repeat']"
          style="background-image: url(https://6d69-miofly-k1xjk-1303051262.tcb.qcloud.la/images/glnz/1.jpg)"
        >
          {{ item }}
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const baseClass = ref(
  'w-150px h-150px leading-150px bg-red-500 m-auto text-white whitespace-nowrap',
);

const borderCfg = ref({
  origin: [
    'origin-center rotate-45',
    'origin-top rotate-45',
    'origin-top-right rotate-45',
    'origin-right rotate-45',
    'origin-bottom-right rotate-45',
    'origin-bottom rotate-45',
    'origin-bottom-left rotate-45',
    'origin-left rotate-45',
    'origin-top-left rotate-45',
  ],
  rotate: ['rotate-0', 'rotate-45', 'rotate-90', 'rotate-135', 'rotate-180'],
  rotateX: [
    'rotate-x-0',
    'rotate-x-45',
    'rotate-x-90',
    'rotate-x-135',
    'rotate-x-180',
  ],
  rotateY: [
    'rotate-y-0',
    'rotate-y-45',
    'rotate-y-90',
    'rotate-y-135',
    'rotate-y-180',
  ],
  rotateZ: [
    'rotate-z-0',
    'rotate-z-45',
    'rotate-z-90',
    'rotate-z-135',
    'rotate-z-180',
  ],
  scale: ['scale-90', 'scale-50', 'scale-x-50', 'scale-y-50', 'scale-z-50'],
  skew: ['skew-x-45', 'skew-x-10', 'skew-y-50', 'skew-y-10'],
  translate: [
    'translate-x-45px',
    'translate-x-10px',
    'translate-y-50px',
    'translate-y-10px',
  ],
});
</script>

这段代码里有个值得注意的细节:所有类名都是以完整字符串的形式写在 borderCfg 数组里的,没有用 `rotate-${deg}` 这种拼接。原子化 CSS 靠静态扫描源文件来决定生成哪些规则,拼接出来的类名扫不到,样式不会被生成。写成完整字符串放数组里是安全的,因为字符串确实出现在文件中了。

whitespace-nowrap 加在 baseClass 里是为了让方块中的类名文本不折行,否则长类名会把方块撑变形,影响对比。leading-150px 让行高等于容器高度,文字垂直居中,这是单行文字居中最省事的写法。

实际项目里我很少这样堆一屏做对比,更常见的用法是配合 hover: 前缀做交互反馈,比如 hover:scale-105 transition-transform 做卡片悬浮微放大——比写 keyframes 动画轻量得多,而且 transform 属性走 GPU 合成,不触发重排。

💬留言板0

还没有留言,来发表第一条吧~

PAGE LOAD / LIVEWindi CSS transform 工具类:旋转、缩放、位移加载中