最近对 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 ); --transfrom-width-negative : calc (var(--cube-width)/-2 ); --cube-margin : 180px }
其次定义左平移动画:
.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-rotate { -webkit-animation : rotate 25s linear infinite; -webkit-transform-style : preserve-3 d; } @-webkit -keyframes rotate { from { -webkit-transform : rotateX (0) rotateZ (0); } to { -webkit-transform : rotateX (1turn) rotateZ (1turn); } }
Dom 元素处于屏幕可见区时才播放动画 该页面中包含多个动画(下面有我写的步骤示例动画),如果每一个动画都直接播放的话,在移动端上性能会很差。
现在我们优化下,仅在可见区时才播放动画。
动画开关 首先我们通过为元素设定/移除各种预先定义好的动画 class 来控制动画的加载。
比如我们定义立方体的最终旋转动画为:
.cube-rotate { -webkit-animation : rotate 25s linear infinite; -webkit-transform-style : preserve-3 d; } @-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) && rect.right <= (window .innerWidth || document .documentElement.clientWidth) ); }
事件监听 动画开关的边界条件确定了之后,最后只需要添加滚动事件监听即可:
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) ); }