一个卡牌小游戏入门CSS3的3D变换

·  阅读 2878
一个卡牌小游戏入门CSS3的3D变换

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>
复制代码
分类:
前端
标签: