CSS 实现 3D 旋转立方体

最近对 CSS3 的一些新特性比较感兴趣,经常可以碰到自己没见过的 CSS 特性。今天就利用 CSS 的 Z 轴旋转和位移来做一个 3D 可视化立方体。

预览页面:

https://demo.chengww.com/css-cube/
预览视频:

源代码:
https://github.com/chengww5217/css-cube

放置立方体的 6 个面

定义 3D 元素的视距:

html {
-webkit-perspective: 800px;
perspective: 800px;
}

perspective 属性允许改变 3D 元素查看 3D 元素的视图。当为元素定义 perspective 属性时,其子元素会获得透视效果,而不是元素本身。

首先一个立方体是有 6 个面的,我们每个面放一张图片:

<div class="cube cube-wrapper" id="cube-rotate">
<div class="box-left">
<img src="1.png">
</div>
<div class="box-right">
<img src="2.png">
</div>
<div class="box-top">
<img src="3.png">
</div>
<div class="box-bottom">
<img src="4.png">
</div>
<div class="box-end">
<img src="6.png">
</div>
<div class="box-front">
<img src="5.png">
</div>
</div>

然后我们需要将这 6 个 div 叠在一起,不能让其直接上下排列。

设定 .cube-wrapper 内的子 div 绝对定位,让其脱离文档流:

.cube-wrapper > div {
position: absolute;
}

现在我们 6 个面就堆叠在一起了。

具体效果参考:
https://demo.chengww.com/css-cube/#step1

现在我们就开始将每一个面按照设定的方向进行移动。

移动

将每一个面都移动到它对应的地方,比如左面:

https://demo.chengww.com/css-cube/#step2

具体操作是:沿 Y 轴旋转 90°,然后沿着 Z 轴负方向移动立方体边长的一半

首先定义下参数:

:root {
--cube-width: 220px; /* 立方体边长 */
--transfrom-width: calc(var(--cube-width)/2); /* 沿 Z 轴正方向移动的位移 */
--transfrom-width-negative: calc(var(--cube-width)/-2); /* 沿 Z 轴负方向移动的位移 */
--cube-margin: 180px /* 设置立方体上下边距,避免立方体旋转而覆盖网页上下内容 */
}

其次定义左平移动画:

/* every side of the cube */
/* 为了兼容 webkit 内核,这里所有属性都加了 -webkit-xxx 如不需要兼容可以去掉*/
.box-left {
-webkit-transform: rotateY(90deg) translateZ(var(--transfrom-width-negative));
}

.box-right {
-webkit-transform: rotateY(90deg) translateZ(var(--transfrom-width));
}

.box-top {
-webkit-transform: rotateX(90deg) translateZ(var(--transfrom-width));
}

.box-bottom {
-webkit-transform: rotateX(90deg) translateZ(var(--transfrom-width-negative));
}

.box-front {
-webkit-transform: translateZ(var(--transfrom-width));
}

.box-end {
-webkit-transform: translateZ(var(--transfrom-width-negative));
}

说明下, 0 -25% 做了 90°旋转,从 25% - 50% 什么都没做,是为了模拟暂停效果。

因为总动画时长是 4s,每一个 25% 就是 1s,这里会暂停 1s。

50% - 75% 做了 Z 轴方向的位移,75% - 100% 同理,模拟暂停效果。

其他几个面重复移动操作

同理将其他几个面也旋转到指定的位置,最后将整个立方体添加旋转动画即可

https://demo.chengww.com/css-cube/#step3

/* cube animation */
#cube-rotate {
-webkit-animation: rotate 25s linear infinite;
-webkit-transform-style: preserve-3d;
}

@-webkit-keyframes rotate {
from {
-webkit-transform: rotateX(0) rotateZ(0);
}
to {
-webkit-transform: rotateX(1turn) rotateZ(1turn);
}
}

Dom 元素处于屏幕可见区时才播放动画

该页面中包含多个动画(下面有我写的步骤示例动画),如果每一个动画都直接播放的话,在移动端上性能会很差。

现在我们优化下,仅在可见区时才播放动画。

动画开关

首先我们通过为元素设定/移除各种预先定义好的动画 class 来控制动画的加载。

比如我们定义立方体的最终旋转动画为:

/* cube animation */
.cube-rotate {
-webkit-animation: rotate 25s linear infinite;
-webkit-transform-style: preserve-3d;
}

@-webkit-keyframes rotate {
from {
-webkit-transform: rotateX(0) rotateZ(0);
}
to {
-webkit-transform: rotateX(1turn) rotateZ(1turn);
}
}

没错,就是将上一段代码的 id 选择器换成类选择器。这样我们通过预先定义元素和类加载器同名 id,然后通过 id 获取元素,最后判断其是否可见来增删 class:

if (isElementInViewport(target)) {
target.classList.add(target.id)
} else {
target.classList.remove(target.id)
}

判断元素是否可见也非常简单,getBoundingClientRect 函数可以获取元素的大小及其相对于视口的位置。

然后判断其底部坐标是否小于 window.innerHeight || document.documentElement.clientHeight

function isElementInViewport (el) {
const rect = el.getBoundingClientRect();
return (
rect.top >= -100 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
);
}

事件监听

动画开关的边界条件确定了之后,最后只需要添加滚动事件监听即可:

autoAnimate('cube-rotate',
'transform-left-repeat', 'transform-right-repeat',
'transform-top-repeat', 'transform-bottom-repeat',
'transform-front-repeat', 'transform-end-repeat')
document.getElementById('cube-rotate').classList.add('cube-rotate')

function autoAnimate(...elementIds) {
let targets = []
elementIds.forEach(id => {
const target = document.getElementById(id)
if (target) targets.push(target)
})
let windowHeight = document.documentElement.clientHeight
window.onresize = () => windowHeight = document.documentElement.clientHeight
window.scroll(() => {
targets.forEach(target => target.dTop = document.scrollTop)
})
document.addEventListener('scroll', function () {
let scrollTop = document.documentElement.scrollTop
targets.forEach(target => {
if (isElementInViewport(target)) {
target.classList.add(target.id)
} else {
target.classList.remove(target.id)
}
})
})
}

function isElementInViewport (el) {
const rect = el.getBoundingClientRect();
return (
rect.top >= -100 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
文章作者: chengww
文章链接: https://chengww.com/archives/css-cube.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 chengww's blog