本文最后更新于 10 个月前,文中所描述的信息可能已发生改变。
css 3d
font
back
bottom
left
right
top
api
transform-style
声明 3D 空间,一般作用在容器盒子上
rotate[XYZ]
时,正值为顺时针,负值为逆时针;左手定则。
perspective
摄像机和屏幕的距离;声明具体数值后,有透视效果;默认为none
,无透视效果
如图:perspective
的值为 d
,translateZ
的值为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
实现轨迹动画要素:
- 椭圆/圆 轨迹
- 点在轨迹上的位置
- 透视
给定如下dom
结构:
- 3d 容器
- 轨迹
- 元素
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;
}
}
透视的实现:
- 在定位元素时,只通过
transform
api 是不足以实现该效果的 - 可以将轨迹 3d 变换的函数转换成对应的 matrix 矩阵,使用:https://meyerweb.com/eric/tools/matrix/
- 将圆上的点 xyz 和矩阵相乘得到变换后的坐标
这个过程实际上是规避了 transform
api 对面的变换,因为我们只需要点的位置,所以只需要计算点在圆上的位置,然后通过矩阵转换即可。