跟我一起玩转CSS变形

476 阅读8分钟

前言

前两天看到一篇文章做了一个“剑气”的加载动画,很有趣,自己顺道做了一下,发现变形属性已经不太记得了,想着写一篇文章复习一下。

四种常用变形

变形是CSS3新增的功能,变形是动画的基础,后面我们会聊聊关于变形的性能优势。变形一共分四种,移动、旋转、缩放、倾斜,这四种变形基本覆盖了我们日常使用的方方面面。

移动

移动就是将元素从一个地方移到另一个地方,类似于相对定位,咱们看看基础的API:

属性变形函数描述
transformtranslateX(20px)向x移动一段距离
transformtranslateY(20px)向y移动一段距离
transformtranslateZ(20px)向z移动一段距离
transformtranslate(20px, 20px)向x,y移动一段距离
transformtranslate3d(20px, 20px, 20px)向x,y,z移动一段距离

整体来说,变形函数还是比较好记忆的,非常整齐,我们只需要了解xyz三个坐标轴分别指向哪里就可以了,x指向右边,y指向下边,z指向屏幕外层(冲着你的眼睛去的)

旋转

旋转和移动的API差不多,都很整齐,我们列一下:

属性变形函数描述
transformrotateX(45deg)绕x轴转多少度
transformrotateY(45deg)绕y轴转多少度
transformrotateZ(45deg)绕z轴转多少度
transformrotate(45deg)绕z轴转多少度
transformrotate3d(45deg, 45deg, 45deg)绕x,y,z轴转多少度

这里要特别注意一点,xyz三个轴的指向随着元素变形而变化,始终贴着元素的表面,比如我们这儿的旋转,如果元素绕Y轴旋转90度,那x轴就指向向屏幕里面,y轴还是向下,z轴则指向右边。比如我们看个例子:


  <head>
    <title>Document</title>
    <style>
      div {
        width: 200px;
        height: 200px;
      }
      .stage {
        border: dashed 2px green;
        perspective: 1500px;
      }
      .performer {
        background-color: #ccc;
        transform: rotateY(85deg) translateZ(50px);
      }
    </style>
  </head>
  <body>
    <div class="stage">
      <div class="performer"></div>
    </div>
  </body>

image.png

在这个例子中,我们先是将元素绕Y轴旋转85deg,然后又沿着Z轴移动了50px,从图上看,z轴已经指向了右侧,因为z轴永远和元素表面垂直。

另一个我们要关注的是perspective属性,这个属性叫做景深,表示人眼离舞台的距离,舞台也就是父盒子,注意这个属性一定要设置在舞台上才生效,一般来说,我们离舞台越近,3d效果就越好,离舞台越远,效果就越弱。

当然元素本身也可以设置景深,通过transform: perspective(1500px) rotateY(85deg),这里要注意perspective() 函数必须作为transform的第一个属性值放在最前面。

另一个很有趣的属性是transform-origin,这个属性叫做变形原点,啥叫变形原点呢?就是坐标轴原点,我们所有的变形都是基于坐标轴的,默认原点是在元素的中心,基本用法是transform-origin: x, y, z,其中x、y可以取left,center,right,top,bottom,px,%等类型的值,表示平面上坐标原点距离元素左上角的偏移,z只能写px表示在z轴上的定位。我们用旋转来看看效果:

 <head>
    <style>
      div {
        width: 200px;
        height: 200px;
      }
      .stage {
        margin: 0px auto;
        border: dashed 2px green;
        perspective: 1500px;
      }
      .performer {
        background-color: #ccc;
        transform: rotateY(85deg);
        transform-origin: left top;
      }
    </style>
  </head>
  <body>
    <div class="stage">
      <div class="performer"></div>
    </div>
  </body>

image.png

我们把坐标轴原点设置在了左上角,这时候y轴就是元素左边缘,我们看到绕Y轴旋转就像是推开了一扇门那样。

transform-origin属性对旋转、缩放、倾斜都有影响。

缩放

缩放比较简单,常用的就是在2d平面上对元素进行放大缩小,基本API如下:

属性变形函数描述
transformscaleX(0.1)沿X轴缩放
transformscaleY(0.1)沿Y轴缩放
transformscale(0.1, 0.2)沿X、Y轴缩放

scale函数里的参数为一个无单位的值,默认是1,如果小于1则缩小,大于1则放大。我们看个例子:

  <head>
    <style>
      div {
        width: 200px;
        height: 200px;
      }
      .stage {
        margin: 200px auto;
        border: dashed 2px green;
      }
      .performer {
        background-color: #ccc;
        transform: scale(0.5, 2);
      }
    </style>
  </head>
  <body>
    <div class="stage">
      <div class="performer"></div>
    </div>
  </body>

image.png

倾斜

倾斜也常用于2d平面,指的是元素相对于X轴、Y轴有一个倾斜角度,常用API如下:

属性变形函数描述
transformskewX(45deg)相对X轴倾斜
transformskewY(45deg)相对Y轴倾斜
transformskew(45deg, 45deg)相对X轴、Y轴倾斜

我们看个例子:

  <head>
    <style>
      div {
        width: 200px;
        height: 200px;
      }
      .stage {
        margin: 200px auto;
        border: dashed 2px green;
      }
      .performer {
        background-color: #ccc;
        transform: skewX(45deg);
      }
    </style>
  </head>
  <body>
    <div class="stage">
      <div class="performer"></div>
    </div>
  </body>

image.png

在这个例子中,我们让元素沿X轴逆时针倾斜45deg,元素具体如何倾斜实际开发中试一下就能明白,不需要记忆。

做一个剑气加载动画

这个动画是从另一篇文章学到的,觉得很酷炫,所以想着自己也实现一遍并按照自己的想法讲一下。

制作一个动画可以按照三个步骤来进行:

  1. 制作静态样式
  2. 添加变形
  3. 添加帧动画

一个"剑气"加载🌪️ #掘金文章# juejin.cn/post/700177…

Sep-04-2021 14-00-52.gif

制作静态样式

我们首先制作静态样式,这个剑气本质就是圆形的边框,所以我们可以通过边框和圆角来实现,代码如下:

  <head>
    <style>
      /* 初始化 */
      * {
        padding: 0;
        margin: 0;
      }
      /* 黑色背景,添加景深 */
      .container {
        width: 100vw;
        height: 100vh;
        background-color: #000;
        perspective: 1500px;
      }
      /* 制作一个盒子,实现居中和边框 */
      .sword {
        position: absolute;
        left: 50%;
        top: 50%;
        margin-left: -100px;
        margin-top: -100px;
        width: 200px;
        height: 200px;
        border-bottom: solid 3px #fff;
        border-radius: 50%;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="sword sword-1"></div>
      <div class="sword sword-2"></div>
      <div class="sword sword-3"></div>
    </div>
  </body>

image.png

添加变形

上一步中,三道剑气重叠在了一起,我们接下来添加旋转变形将他们分开,代码如下:

  </head>
    <style>
      /* 添加旋转变形 */
      .sword-1 {
        transform: rotateX(127deg) rotateY(21deg) rotateZ(299deg);
      }

      .sword-2 {
        transform: rotateX(55deg) rotateY(50deg) rotateZ(90deg);
      }

      .sword-3 {
        transform: rotateX(135deg) rotateY(329deg) rotateZ(203deg);
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="sword sword-1"></div>
      <div class="sword sword-2"></div>
      <div class="sword sword-3"></div>
    </div>
  </body>

关于如何调试动画我给出自己的建议,我们可以取动画里的一帧进行调试,比如我随机截图一帧动画,然后通过浏览器的调试工具不断调整旋转角度使得自己的效果和截图一致,由于每个剑气的运动模式都是一致的(匀速旋转),所以只要其中一帧一致了,后面都会一致。

这是我从动画里截的图 WX20210904-143013@2x.png

这是我在用调试工具调整旋转角度 image.png

添加帧动画

变形调整完毕后,接下来就要让元素动起来,动画的本质就在多个状态之间切换,每个状态就是一帧,通常我们只需要确定关键帧,浏览器就会为我们自动补全中间的帧。在这个案例中,每个剑气的运动模式就是匀速旋转,所以起始帧就是我们上一步定义的变形状态,终点帧就是绕Z轴旋转360deg,我们看下代码:

 /* 添加帧动画 */
  <head>
     <style>
      .sword-1 {
        animation: sword-1 1s linear infinite;
      }

      .sword-2 {
        animation: sword-2 1s linear infinite;
      }

      .sword-3 {
        animation: sword-3 1s linear infinite;
      }

      @keyframes sword-1 {
        /* 将上一步的旋转变形作为起始帧 */
        from {
          transform: rotateX(127deg) rotateY(21deg) rotateZ(299deg);
        }
        /* 剑气的运动模式就是旋转一圈,所以将绕行360deg作为最后一帧 */
        to {
          transform: rotateX(127deg) rotateY(21deg) rotateZ(-61deg);
        }
      }

      @keyframes sword-2 {
        /* 将上一步的旋转变形作为起始帧 */
        from {
          transform: rotateX(55deg) rotateY(50deg) rotateZ(90deg);
        }
        /* 剑气的运动模式就是旋转一圈,所以将绕行360deg作为最后一帧 */
        to {
          transform: rotateX(55deg) rotateY(50deg) rotateZ(450deg);
        }
      }

      @keyframes sword-3 {
        /* 将上一步的旋转变形作为起始帧 */
        from {
          transform: rotateX(135deg) rotateY(329deg) rotateZ(203deg);
        }
        /* 剑气的运动模式就是旋转一圈,所以将绕行360deg作为最后一帧 */
        to {
          transform: rotateX(135deg) rotateY(329deg) rotateZ(-157deg);
        }
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="sword sword-1"></div>
      <div class="sword sword-2"></div>
      <div class="sword sword-3"></div>
    </div>
  </body>

当浏览器自动为我们补全中间帧的时候,元素就动起来了。这里面有一点要注意,结束帧我们说是绕Z轴旋转360deg,但究竟是增加360deg还是减少360deg则要看我们希望元素按顺时针还是逆时针旋转而定。

为什么变形的性能要更好?

最后我们再聊一聊变形的性能优势,我们以translate()举例,你可能觉得为什么要添加这么一个移动的变形,我用position不是也可以改变元素的位置吗?的确,用position是可以的,但如果位置要不断变化,尤其是在动画中,position的性能就不如translate()好了,我们解释一下原因。

浏览器在确定了DOM和CSS之后就会开始进入渲染流程,这个过程一般分为三步:布局、绘制、合成。

在布局中,浏览器要确定每个元素占据多大的位置,当元素的大小、位置发生变化就会触发重排。

绘制是在内存中进行像素填充,比如背景、边框等,默认情况下所有元素都在主图层里,但有些时候某些元素会被提取到独立的图层,主图层由CPU进行渲染,而独立图层会由GPU进行渲染,即所谓的硬件加速。transform性能高的原理就在这儿,当浏览器检测到元素使用了transform属性且值还在变化,就会将该元素提取到独立的图层进行渲染,当发生重绘的时候也不会影响到主图层。

最后一步就是合成,浏览器将多个图层合成为一个页面,合成的时候会按特定的顺序进行以保证某些图层在另一些图层上面。

我们来对比一下position和transform是否分了图层,首先是position:

  <head>
    <style>
      div {
        width: 200px;
        height: 200px;
        border: solid 1px #ccc;
        position: absolute;
        animation: hd 2s linear infinite;
      }

      @keyframes hd {
        from {
          left: 100px;
        }
        to {
          left: 300px;
        }
      }
    </style>
  </head>
  <body>
    <div class="d1"></div>
  </body>

image.png

再来是transform:

  <head>
    <style>
      div {
        width: 200px;
        height: 200px;
        border: solid 1px #ccc;
        position: absolute;
        animation: hd 2s linear infinite;
      }

      @keyframes hd {
        from {
          transform: translateX(100px);
        }
        to {
          transform: translateX(300px);
        }
      }
    </style>
  </head>
  <body>
    <div class="d1"></div>
  </body>

image.png

显然我们发现transform属性的变化让浏览器将div元素提取到了独立的图层,这样虽然会占用更多的内存,但带来了更高的效率,之前你可能听说CSS动画比JS动画性能高,原理也在这里,JS动画也是改变了元素的宽高或者位置,从而触发了重排和重绘,而CSS动画中使用transform变形则不会有这些问题。

总的来说就是,transform属性的变化不会引发:1.页面重排 2.主图层的重绘,所以性能更好。

尾声

又是一篇3000字的文章,技术世界实在是丰富和有趣,路漫漫其修远兮,希望终有一天我们可以在技术的世界里做到随心所欲,信手拈来