CSS3的3D基础
坐标系
对于3D变换,最基础的肯定是坐标系。在3D中有两种完全不同的坐标系:左手坐标系和右手坐标系。
- 右手坐标系是X轴向右,Y轴向上,Z轴是指向“自己”的。
伸出右手,让拇指和食指成"L"形,大拇指向右,食指向上。其余的手指指向自己,这样就建立了一个右手坐标系。
其中,拇指、食指和其余手指分别代表x,y,z轴的正方向
- 左手坐标系的Z轴正好相反,是背向“自己”的,在计算机中通常使用的是左手坐标系,而数学中则通常使用右手坐标系。
伸出左手,让拇指和食指成"L"形,大拇指向右,食指向上。其余的手指指向前方,这样就建立了一个左手坐标系。
其中,拇指、食指和其余手指分别代表x,y,z轴的正方向
CSS3中的3D坐标系和上述的左手坐标系还有点不一样,相当于其绕着X轴旋转了180度,试着把左手绕拇指方向旋转,直到食指指向正下方,此时拇指、食指和其余手指分别代表x,y,z轴的正方向。
x轴正方向朝右,y轴的正方向向下,z轴正方向指向自己,如下图(格子平面相当于屏幕)。
transform
CSS3的3D变换的核心就是transform属性。transform属性应用于元素的2D或3D转换,它允许你将元素旋转,缩放,移动,倾斜等。语法:transform: none|transform-functions;
3D相关的transform-functions。
translate3d(x,y,z):平移。
translateX(x)
translateY(y)
translateZ(z)
scale3d(x,y,z):缩放。
scaleX(x)
scaleY(y)
scaleZ(z)
rotate3d(x,y,z,angle):旋转。
rotateX(angle)
rotateY(angle)
rotateZ(angle)
xyz三个方向的变换值可以一起设定,也可以分别设定。平移和缩放很好理解,这里我们主要看旋转。
rotate3d(x,y,z,angle)平时不常用,更多是用rotateX(angle) rotateY(angle) rotateZ(angle)这种各方向分量的方式。
- rotateX(): 对应的是绕着X轴的旋转,传入参数如: rotateX(180deg),表示的是绕X轴旋转180度。具体是怎么旋转,就要对照上面坐标系,再一次展开左手,拇指指向x轴正方向,其余手指的弯曲方向就是旋转的正方向,y轴和z轴旋转方向的判断同理。所以
rotateX(>0deg)是向里翻转,如果是rotateX(<0deg)就是向外翻转。下图是rotateX(180deg)。 - rotateY(): 对应的是绕着Y轴的旋转,下图是
rotateY(180deg)。 - rotateZ(): 对应的是绕着Z轴的旋转,下图是
rotateZ(180deg)。
transform-style
然后,我们就可以写一个简单的卡牌翻转效果。
卡牌有正反两个面,分别用两个div表示,通过position:absolute把这两个div"叠"在一起,然后把其中一个面旋转到背后,就有正反两面了。
<div class="card">
<!-- 卡牌正面 -->
<div class="card-face card-front">正面</div>
<!-- 卡牌反面 -->
<div class="card-face card-back">反面</div>
</div>
.card{
width: 200px;
height: 200px;
position: relative;
margin: 30px auto;
transition: transform 2s;
}
.card-face {
width: 100%;
height: 100%;
position: absolute;
}
.card-front {
background: cornflowerblue;
}
.card-back {
background: rgb(248, 167, 140);
transform: rotateY( 180deg );
}
.card:hover {
transform: rotateY( 180deg );
}
打开浏览器,反面倒是翻转了,但是它并没有翻到背后,而是在当面平面,而且hover不hover怎么翻转也看不到正面。在2d平面
position:absolute后面的div把前面的div盖住了,在3d空间旋转,两个div应该都有露面的机会,但是没有。
这就涉及到transform-style属性了。transform-style属性是3D空间一个重要属性,指定嵌套元素是怎样在三维空间中呈现。它有两个属性值:flat和preserve-3d。其中flat值为默认值,表示所有子元素在2D平面呈现。preserve-3d表示所有子元素在3D空间中呈现。我们把transform-style属性加上:
.card{
width: 200px;
height: 200px;
position: relative;
margin: 30px auto;
transform-style: preserve-3d;
transition: transform 2s;
}
于是,就出现了卡牌翻转效果~~
transform-style需要注意的点:
这个属性不能继承,因此只要有子元素需要设置成3D呈现效果,就得在父元素添加这个属性。在下面的翻书例子中,class="card"的元素必须设置transform-style,不能只设置在class="demo"的元素让class="card"的元素来继承,那样行不通。
设置了transform-style值为preserve-3d的元素,就不能设置overflow值为hidden,否则和transform-style: flat一个效果。
transform-origin
transform-origin让你更改一个元素变形的原点,默认值是50% 50% 0,也就是元素的旋转、移位、缩放等操作都是以元素自身的在x、y方向的中心位置(z轴是在0,也就是屏幕所在平面)进行变形的。在没设置transform-origin的情况下,上面的翻牌效果就是绕着卡牌垂直的中线进行翻转的,我们可以通过设置transform-origin,改变变换中心,实现更多酷炫的效果。
transform-origin可能的值,参考MDN。
基于上面的卡牌翻转,来实现一个翻书的效果。
//基本结构
<div class="demo">
<div class="card">
<div class="card-face card-front">This is page4!</div>
<div class="card-face card-back cover-back"></div>
</div>
<div class="card">
<div class="card-face card-front">This is page2!</div>
<div class="card-face card-back">This is page3!</div>
</div>
<div class="card">
<div class="card-face card-front cover-front">Welcome to CSS3</div>
<div class="card-face card-back">This is page1!</div>
</div>
</div>
//css
<style>
.demo {
width: 400px;
height: 300px;
perspective: 1200px; /*为了让翻书有景深效果,可以改变perspective对比一下*/
position: relative;
margin: 30px auto;
}
.card{
width: 50%;
height: 100%;
position: absolute;
left: 50%;
transform-style: preserve-3d;
transform-origin: left; /*将元素变形的原点设置到左边缘*/
display: inline-block;
border-left: 1px solid #eee;
box-shadow: 4px 4px 2px #eee;
transition: transform 2s;
}
.card .card-face{
width: 100%;
height: 100%;
box-sizing: border-box;
position: absolute;
margin: 0;
padding-top: 40%;
background-color: beige;
text-align: center;
color: #666;
}
.card-back {
transform: rotateY( -180deg );
}
.cover-back{
background: url('./images/pic6.jpg') no-repeat 50% 0; /*书的封面,这里用的是本地图片,可以换成别的图片*/
background-size: cover;
}
.cover-front{
background: url('./images/pic6.jpg') no-repeat 0 0;
background-size: cover;
}
/* 翻牌动作 */
.card.flipped {
transform: rotateY( -180deg );
}
</style>
没有景深的效果:
//js
<script src="https://lib.sinaapp.com/js/jquery/2.0.2/jquery-2.0.2.min.js"></script>
<script>
let zIndex = 1
$('.card').on('click',function(e){
//设置z-index,让刚翻转的书页处于最上面,模拟实际翻书
$(e.currentTarget).toggleClass('flipped').css('z-index', zIndex++)
})
</script>
perspective
翻书例子中我们了perspective让翻书效果更立体。
perspective 指定了观察者与 z=0 平面的距离,单位是px。使具有三维位置变换的元素产生透视效果。
举个栗子,perspective: 1200px表示人眼在距离屏幕1200px的位置观察物体,物体此时应该呈现的效果,就是在1200px这个距离在人眼中的效果。
透视原理可以参考这个up主,透视原理
当为元素定义 perspective 属性时,其子元素会获得透视效果,而不是元素本身。所以要上面例子中perspective属性要设置在class="demo"的元素上。
调整perspective不同取值的效果:
demo代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.demo {
width: 300px;
height: 300px;
perspective: 1000px;
position: relative;
margin: 30px auto;
}
.cube {
width: 100%;
height: 100%;
position: absolute;
transform-style: preserve-3d;
animation: rotating ease 18s infinite alternate;
}
.cube .cube-face {
border: 2px solid #000;
width: 100%;
height: 100%;
position: absolute;
overflow: hidden;
opacity: 0.5;
/* backface-visibility: hidden; */
}
.cube-face.is-front {
transform: translateZ( 150px );
}
.cube-face.is-back {
transform: rotateX( -180deg ) translateZ( 150px );
}
.cube-face.is-right {
transform: rotateY( 90deg ) translateZ( 150px );
}
.cube-face.is-left {
transform: rotateY( -90deg ) translateZ( 150px );
}
.cube-face.is-top {
transform: rotateX( 90deg ) translateZ( 150px );
}
.cube-face.is-bottom {
transform: rotateX( -90deg ) translateZ( 150px );
}
.cube-img {
display: block;
width: 100%;
height: 100%;
}
img{
height: 100%;
width: 100%;
}
@keyframes rotating {
/* show-front */
0%, 100% {
transform: translateZ( -150px );
}
/* show-back */
16.5% {
transform: translateZ( -150px ) rotateX( -180deg );
}
/* show-left */
33% {
transform: translateZ( -150px ) rotateY( 90deg );
}
/* show-right */
49.5% {
transform: translateZ( -150px ) rotateY( -90deg );
}
/* show-top */
66% {
transform: translateZ( -150px ) rotateX( -90deg );
}
/* show-bottom */
82.5% {
transform: translateZ( -150px ) rotateX( 90deg );
}
}
</style>
</head>
<body>
<div class="demo">
<div class="cube">
<div class="cube-face is-front"><img src="./images/pic1.jpg" /></div>
<div class="cube-face is-back"><img src="./images/pic2.jpg" /></div>
<div class="cube-face is-right"><img src="./images/pic3.jpg" /></div>
<div class="cube-face is-left"><img src="./images/pic4.jpg" /></div>
<div class="cube-face is-top"><img src="./images/pic5.jpg" /></div>
<div class="cube-face is-bottom"><img src="./images/pic6.jpg" /></div>
</div>
</div>
</body>
</html>
完成一个卡牌小游戏
为了练习CSS3的3D变换,我们来写一个小游戏。记忆翻翻卡。效果就是最上面的封面大图那样。点击卡片进行翻转,如果翻到的图案和上一次相同,就算匹配成功,整个面板的图片都翻转过来了,就算游戏完成。我们用vue来写这个游戏。
洗牌思路
卡牌定位是用translate属性,所以洗牌就是改变元素的translate属性。
initImage(){ //初始化卡牌数组
const NUM = 8
let firstAdd = true
let i = 0
while(i < NUM){
this.icons.push({
url: `./img/pic${i}.png`,
value: i, //通过value来判断两张卡牌图案是否相同
id: `pic${i}${firstAdd}`,
flip: false,
loc: null
})
if(!firstAdd) i++ //同一个图案,向数组添加两次
firstAdd = !firstAdd
}
},
initLocation(){ //生成一个数组,初始化translate属性需要设置的x,y
const LATTICE_NUM = 4, delta = 150, gap = 10
for(let i=0; i<LATTICE_NUM; i++){
for(let j=0; j<LATTICE_NUM; j++){
this.locs.push({
x: j * ( gap + delta ),
y: i * ( gap + delta )
})
}
}
},
shuffle(){
this.locs.sort( () => { //将位置数组打乱,再赋给元素,这样,元素的translate属性就是随机的,就实现了洗牌功能
return .5 - Math.random();
});
this.icons.forEach((item,index) => {
item.flip = false
item.loc = this.locs[index]
})
},
翻牌思路
翻牌思路就是基于上面的卡牌翻转demo。
<div class="board">
<div v-for="item in icons"
:key="item.id"
@click="check(item)"
class="card"
:style="{ transform: `translate(${item.loc.x}px, ${item.loc.y}px) rotateY( ${item.flip?180:0}deg )` }">
<!-- 卡牌正面 -->
<div class="card-face card-front">
<img :src="item.url"/>
</div>
<!-- 卡牌反面 -->
<div class="card-face card-back">
<img src="./img/back.png"/>
</div>
</div>
</div>
.card-front {
transform: rotateY( 180deg ); /*因为是猜牌游戏,所以这里是把正面翻到背后*/
}
check(item){
item.flip = !item.flip //翻转卡片
}
游戏的完整代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Memory Game</title>
<style>
.game{
width: 800px;
margin: 0 auto;
padding-bottom: 50px;
border: 1px solid green;
text-align: center;
background: #243B55;
color: #ddd;
}
.reset{
text-align: right;
margin: 30px 120px 50px;
}
.reset span{
background-color: #FD746C;
padding: 8px 30px 10px;
cursor: pointer;
border-radius: 4px;
font-size: 20px;
}
.board{
width: 600px;
height: 600px;
position: relative;
margin: 30px auto;
}
.card{
background-color: #fff;
width: 120px;
height: 120px;
position: absolute;
transform-style: preserve-3d;
transition: transform 1s;
}
.card img{
width: 70%;
margin-top: 10%;
}
.card-face{
width: 100%;
height: 100%;
position: absolute;
overflow: hidden;
backface-visibility: hidden;
}
.card-front {
transform: rotateY( 180deg );
}
</style>
</head>
<body>
<div class="game">
<h2>
Welcome to our memory game!
</h2>
<div id="root">
<div class="reset">
<span @click="initStatus">reset</span>
</div>
<div class="board">
<div v-for="item in icons"
:key="item.id"
@click="check(item)"
class="card"
:style="{ transform: `translate(${item.loc.x}px, ${item.loc.y}px) rotateY( ${item.flip?180:0}deg )` }"
>
<!-- 卡牌正面 -->
<div class="card-face card-front">
<img :src="item.url"/>
</div>
<!-- 卡牌反面 -->
<div class="card-face card-back">
<img src="./img/back.png"/>
</div>
</div>
</div>
</div>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
new Vue({
el: "#root",
data: {
icons: [],
locs: [],
current: null
},
methods: {
init(){
this.initImage()
this.initLocation()
this.initStatus()
},
shuffle(){
this.locs.sort( () => {
return .5 - Math.random();
});
this.icons.forEach((item,index) => {
item.flip = false
item.loc = this.locs[index]
})
},
initImage(){
const NUM = 8
let firstAdd = true
let i = 0
while(i < NUM){
this.icons.push({
url: `./img/pic${i}.png`,
value: i,
id: `pic${i}${firstAdd}`,
flip: false,
loc: null
})
if(!firstAdd) i++
firstAdd = !firstAdd
}
},
initLocation(){
const LATTICE_NUM = 4, delta = 150, gap = 10
for(let i=0; i<LATTICE_NUM; i++){
for(let j=0; j<LATTICE_NUM; j++){
this.locs.push({
x: j * ( gap + delta ),
y: i * ( gap + delta )
})
}
}
},
calcTime(){
if(this.startTime === -1) this.startTime = new Date().getTime() //记录游戏开始时间
if(!this.icons.some((icon) => {
return icon.flip === false
})){
let endTime = new Date().getTime()
let time = (endTime-this.startTime)/1000
this.timer = setTimeout(() => { //动画完成时间1s,在1s后再通知结果
alert(`您用了${time.toFixed(0)}秒完成游戏!`)
clearTimeout(this.timer)
},1000)
}
},
initStatus(){
this.timer = null
this.animation = false
this.startTime = -1
this.shuffle()
},
check(item){
if(this.animation || item.flip) return
item.flip = !item.flip //翻转卡片
this.calcTime() //检查游戏是否完成
if(this.current === null){ //判断是否有待匹配卡片
this.current = item
return
}
if(this.current.value === item.value){ //有待匹配卡片的话,就判断两张卡片是否匹配
this.current = null
}else{
this.animation = true
this.timer = setTimeout(() => {
item.flip = !item.flip
this.current.flip = !this.current.flip
this.current = null
this.animation = false
clearTimeout(this.timer)
},600)
}
}
},
created(){
this.init()
}
})
</script>
</html>