css 3d轨迹动画

本文最后更新于 10 个月前,文中所描述的信息可能已发生改变。

css 3d

font
back
bottom
left
right
top

api

transform-style

声明 3D 空间,一般作用在容器盒子上

rotate[XYZ]时,正值为顺时针,负值为逆时针;左手定则。

perspective

摄像机和屏幕的距离;声明具体数值后,有透视效果;默认为none,无透视效果

如图:perspective的值为 dtranslateZ的值为z

则透视的效果(缩放倍数):d/(d-z)

下图则为放大 1/2 和缩小 1/3 的效果

transform

  • translate 平移 xyz
  • rotate 旋转 xyz
  • skew 拉伸 xy
  • scale 缩放 xyz

matrix

变换矩阵

demo

立方盒子

js
export default function Box3d() {
  const $box = useRef<HTMLDivElement>(null)
  const speed = 1

  let onMouseDown = false
  let startX = 0
  let startY = 0

  let rotateX = -33.5
  let rotateY = 45

  const rotateCube = (x: number, y: number) => {
    rotateY += x
    rotateX -= y
  }

  useEffect(() => {
    $box.current?.addEventListener('mousedown', e => {
      startX = e.clientX / speed
      startY = e.clientY / speed
      onMouseDown = true
    })

    window.addEventListener('mouseup', () => (onMouseDown = false))
    window.addEventListener('mousemove', e => {
      if (onMouseDown) {
        const curX = e.clientX / speed
        const curY = e.clientY / speed
        const dX = curX - startX
        const dY = curY - startY
        startX = curX
        startY = curY
        rotateCube(dX, dY)
        $box.current!.style.transform = `rotateX(${rotateX}deg) rotateY(${rotateY}deg)`
      }
    })
  }, [])

  return (
    <div ref={$box} className="box" style={{ transform: 'rotateX(-33.5deg) rotateY(45deg)' }}>
      <div className="font">font</div>
      <div className="back">back</div>
      <div className="bottom">bottom</div>
      <div className="left">left</div>
      <div className="right">right</div>
      <div className="top">top</div>
    </div>
  )
}
css
.box {
  --w: 200px;
  width: var(--w);
  height: var(--w);
  transform-style: preserve-3d;
  transform-origin: center;
  margin: 300px auto;
  position: relative;
  transform: rotateX(-33.5deg) rotateY(45deg);
  // animation: A 2s infinite ease-in-out;
}
.box > div {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background-color: #04ed65a4;
  text-align: center;
  line-height: var(--w);
  font-weight: bold;
  border: 1px solid white;
  backface-visibility: hidden;
  user-select: none;
  cursor: pointer;
}
.font {
  transform: translateZ(calc(var(--w) / 2));
}
.back {
  transform: rotateX(180deg) translateZ(calc(var(--w) / 2));
}
.bottom {
  transform: rotateX(-90deg) translateZ(calc(var(--w) / 2));
}
.left {
  transform: rotateY(-90deg) translateZ(calc(var(--w) / 2));
}
.right {
  transform: rotateY(90deg) translateZ(calc(var(--w) / 2));
}
.top {
  transform: rotateX(90deg) translateZ(calc(var(--w) / 2));
}

@keyframes A {
  0% {
    transform: rotateX(-33.5deg) rotateY(45deg);
  }
  40%,
  to {
    transform: rotateX(-33.5deg) rotateY(315deg);
  }
}

翻转面时,需要保证 z 轴始终向外,否则backface-visibility: hidden;(遮蔽背面)会失效

利用 matrix 实现轨迹动画

ball-0
ball-1
ball-2
ball-3
ball-4

实现轨迹动画要素:

  1. 椭圆/圆 轨迹
  2. 点在轨迹上的位置
  3. 透视

给定如下dom结构:

  1. 3d 容器
  2. 轨迹
  3. 元素
jsx
import React, { useRef, useState } from 'react'
import './css/index.scss'
export default function Orbit3d() {
  const DISTANCE_X = 300
  const DISTANCE_H = 300
  // js 辅助定位,计算初始元素在轨迹上的位置(角度)
  const [items, setItems] = useState(
    Array.from({ length: 5 }, (_, index) => ({ name: `ball-${index}`, rotate: (index * 360) / 5 }))
  )

  const update = () => {
    setItems(items.map(item => ({ ...item, rotate: item.rotate + 0.5 })))
  }
// 计算圆上坐标、
// 角度转弧度
// x: r * cos(θ)
// y: r * sin(θ)
  const getPosition = (rotate: number) => {
    rotate = (rotate * Math.PI) / 180
    const x = DISTANCE_X * Math.cos(rotate)
    const y = DISTANCE_H * Math.sin(rotate)
    return { x, y }
  }

  const rafRef = useRef<number | null>(null)

  const start = () => {
    rafRef.current = requestAnimationFrame(update)
  }

  const stop = () => {
    cancelAnimationFrame(rafRef.current!)
  }
  start()
  return (
    <div className="orbit-container">
      <div className="circle"></div>
      {items.map(item => {
        const { x, y } = getPosition(item.rotate)
        return (
          <div
            className="item"
            key={item.name}
            onMouseEnter={stop}
            onMouseLeave={start}
           style={{ transform: `translate3d(${x}px, ${0.34202 * y}px,${0.939693 * y}px)` }}
          >
            {item.name}
          </div>
        )
      })}
    </div>
  )
}
css
.orbit-container {
  width: 100%;
  height: 100%;
  position: relative;
  transform-style: preserve-3d;
  perspective: 479px;
  //轨迹,椭圆
  .circle {
    position: absolute;
    width: 600px;
    height: 600px;
    border: 2px solid #fafafa;
    left: 50%;
    top: 50%;
    transform: translate3d(-50%, -50%, 0px) rotateX(40deg);
    border-radius: 50%;
  }

  .item {
    position: absolute;
    left: calc(50% - 25px);
    top: calc(50% - 25px);
    background-color: #04b4ff;
    width: 50px;
    height: 50px;
    border-radius: 50%;
    text-align: center;
    line-height: 50px;
    cursor: pointer;
  }
}

透视的实现:

  1. 在定位元素时,只通过transformapi 是不足以实现该效果的
  2. 可以将轨迹 3d 变换的函数转换成对应的 matrix 矩阵,使用:https://meyerweb.com/eric/tools/matrix/
  3. 将圆上的点 xyz 和矩阵相乘得到变换后的坐标

这个过程实际上是规避了 transform api 对面的变换,因为我们只需要点的位置,所以只需要计算点在圆上的位置,然后通过矩阵转换即可。

pnpm&monorepo
TinyMCE薄封装