前端新手项目练习之拼图游戏

前端新手记录自己在网络上找到的前端练习项目。趁热打铁,再练习一下鼠标拖动相关的事件,这个小游戏是一个不错的选择,玩着有趣,看着也养眼。

项目简介

三个选项代表三幅图片,可以选择任意一个开始游戏,拼图游戏跟我们平时玩的差不多,全部拼正确后会提示完成游戏并显示花费的时间。


拼图游戏.gif

Html部分

分成3个部分,上面是3个图片作为选项,中间是开始游戏的按钮,下面是游戏正文部分,因为较为复杂需要通过JavaScript添加。



    
        
        拼图小游戏
        
    
    
        

    CSS部分

    body, ul, li {
        margin: 0;
        padding: 0;
    }
    
    body {
        font: 30px/1.5 Tahoma;
        background: url(img/bg.png);
        text-align: center;
    }
    
    #box {
        position: relative;
        width:410px;
        height:570px;
        margin: 10px auto;
    }
    
    #box li {
        float: left;
        width: 82px;
        height: 190px;
        overflow:hidden;
    }
    
    #box li img {
        width: 82px;
        height: 190px;
    }
    
    #box li.hig {
        width: 78px;
        height: 186px;
        overflow: hidden;
        border: 2px dashed yellow;
    }
    
    #box li.hig img {
        width: 78px;
        height: 186px;
        opacity: 0.5;
        filter: alpha(opacity=50);
    }
    
    #mask {
        position: absolute;
        top: 0;
        left: 0;
        width: 410px;
        height: 570px;
        background: red;
        opacity: 0;
        filter: alpha(opacity=0);
    }
    
    /*设置margin和display来居中,需要父元素text-align:center */
    #center {
        margin: 0 auto;
        display: inline-block;
    }
    
    input {
        cursor: pointer;
    }
    
    #photo {
        text-align: center;
        margin: 10px 0;
    }
    
    #photo img {
        width: 100px;
        height: 100px;
        border-radius: 5px;
        margin: 0 5px;
        opacity:0.5;
        filter: alpha(opacity=50);
        cursor: pointer;
    }
    
    #photo img.hover {
        opacity: 1;
        filter: alpha(opacity=100);
    }
    
    #photo img.selected {
        border: 2px solid yellow;
        width: 96px;
        height: 96px;
        opacity: 1;
        filter: alpha(opacity=100);
    }
    

    一个值得学习的知识点时display的值inline-block。这里简单对比一下inline、block和inline-block。

    • display:block

      1. block元素会独占一行,多个block元素会各自新起一行。默认情况下,block元素宽度自动填满其父元素宽度。
      2. block元素可以设置width,height属性。块级元素即使设置了宽度,仍然是独占一行。
      3. block元素可以设置margin和padding属性。
    • display:inline

      1. inline元素不会独占一行,多个相邻的行内元素会排列在同一行里,直到一行排列不下,才会新换一行,其宽度随元素的内容而变化。
      2. inline元素设置width,height属性无效。
      3. inline元素的margin和padding属性,水平方向的padding-left, padding-right, margin-left, margin-right都产生边距效果;但竖直方向的padding-top, padding-bottom, margin-top, margin-bottom不会产生边距效果。
    • display:inline-block

      1. 简单来说就是将对象呈现为inline对象,但是对象的内容作为block对象呈现。之后的内联对象会被排列在同一行内。比如我们可以给一个link(a元素)inline-block属性值,使其既具有block的宽度高度特性又具有inline的同行特性。

    JavaScript部分

    简单的说一下拼图游戏的逻辑:图片顺序随机打乱,打乱方法是对图片随机排序,然后根据排序后的顺序放到各个位置上去。玩家可以通过拖动图片来交换两个元素的位置,当所有元素的位置都和初始一样的时候就会判定游戏结束。

    var zIndex = 1;
    window.onload = function() {
        //获取元素
        var oPhoto = this.document.getElementById("photo");
        var aThumb = oPhoto.getElementsByTagName("img");
        var oBox = document.getElementById("box");
        var aLi = oBox.getElementsByTagName("li");
        var oInput = this.document.getElementsByTagName("input")[0];
    
        var i = 0;
        var imgPath = 0;//第几个文件夹中的图片
        var oDateStart = null;
        var aPos = []; //位置
        var aData = []; 
    
        //0-15数组
        for(i = 0; i < 15; i++) {
            aData.push(i + 1);
        }
    
        //缩略图
        for(i = 0; i < aThumb.length; i++) {
            aThumb[i].index = i;
            aThumb[i].onmouseover = function() {
                this.className += " hover";
            };
            aThumb[i].onmouseout = function() {
                this.className = this.className.replace(/\shover/, "");
            };
            aThumb[i].onclick = function() {
                for(i = 0; i < aThumb.length; i++) {
                    aThumb[i].className = "";
                }
                this.className = "selected";
                imgPath = this.index;
                oBox.innerHTML = "";
                oInput.value = "开始游戏";
                createMask();
                aData.sort(function(a, b) {return a - b});//按从小到大排序
                GAME(false);
            }
        }
    
        //创建遮罩层
        function createMask() {
            var oMask = document.createElement("div");
            oMask.id = "mask";
            oMask.style.zIndex = zIndex;
            oBox.appendChild(oMask);
        }
        createMask();
    
        function GAME(ran) {
            //随机排列数组
            ran && aData.sort(function(a, b) {return Math.random() > 0.5 ? -1: 1});
    
            //插入结构,用来将一块块拼图插入
            var oFragment = document.createDocumentFragment();
            for(i = 0; i < aData.length; i++) {
                var oLi = document.createElement("li");
                var oImg = document.createElement("img");
                oImg.src = "img/girl" + imgPath + "/" + aData[i] + ".png";
                oLi.appendChild(oImg);
                oFragment.appendChild(oLi);
            }
            oBox.appendChild(oFragment);
            oBox.style.background = "url(img/girl" + imgPath + "/bg.png) no-repeat";
    
            for(i = 0; i < aLi.length; i++) {
                aLi[i].index = i;//添加索引
                aLi[i].style.top = aLi[i].offsetTop + "px";
                aLi[i].style.left = aLi[i].offsetLeft + "px";
                aPos.push({"left":aLi[i].offsetLeft, "top":aLi[i].offsetTop});
            }
            for(i = 0; i < aLi.length; i++) {
                aLi[i].style.position = "absolute";
                aLi[i].style.margin = "0";
                drag(aLi[i]);
            }
    
            /**
             * 添加拖动的函数
             * @param {Element} obj 被拖动的对象
             * @param {Element} handle 拖动对象时需要点击的元素
             */
            function drag(obj ,handle) {
                var handle = handle || obj;
                handle.style.cursor = "move";
                handle.onmousedown = function(event) {
                    var event = event || window.event;
                    var disX = event.clientX - this.offsetLeft;
                    var disY = event.clientY - this.offsetTop;
                    var oNear = null;
                    obj.style.zIndex = zIndex++;
                    document.onmousemove = function(event) {
                        var event = event || window.event;
                        var iL = event.clientX - disX;
                        var iT = event.clientY - disY;
                        var maxL = obj.parentNode.clientWidth - obj.offsetWidth;
                        var maxT = obj.parentNode.clientHeight - obj.offsetHeight;
    
                        iL < 0 && (iL = 0);
                        iT < 0 && (iT < 0);
                        iL > maxL && (iL = maxL);
                        iT > maxT && (iT = maxT);
                        obj.style.left = iL + "px";
                        obj.style.top = iT + "px";
    
                        for(i = 0; i < aLi.length; i++) {
                            aLi[i].className = "";
                        }
    
                        oNear = findNearest(obj);
                        oNear && (oNear.className = "hig");
                        return false;
                    };
    
                    document.onmouseup = function() {
                        document.onmousemove = null;
                        document.onmouseup = null;
                        if(oNear){ //存在最近的碰撞元素
                            var tIndex = obj.index;
                            obj.index = oNear.index;
                            oNear.index = tIndex;
                            //交换位置
                            startMove(obj, aPos[obj.index]); 
                            startMove(oNear, aPos[oNear.index], function() {
                                if(finish()) {
                                    var iHour = iMin = iSec = 0;
                                    var oDateNow = new Date();
                                    var iRemain = parseInt((oDateNow.getTime() - oDateStart.getTime()) /1000);
    
                                    iHour = parseInt(iRemain / 3600);
                                    iRemain %= 3600;
                                    iMin = parseInt(iRemain/ 60);
                                    iRemain %= 60;
                                    iSec = iRemain;
    
                                    alert("\u606d\u559c\u60a8\uff0c\u62fc\u56fe\u5b8c\u6210\uff01\n\n\u7528\u65f6\uff1a" 
                                        + iHour  + "\u5c0f\u65f6" + iMin + "\u5206" + iSec + "\u79d2");
                                    createMask();
                                }
                            });
                            oNear.className = "";
                        }
                        else {
                            startMove(obj, aPos[obj.index]);
                        }
                        handle.releaseCapture && handle.releaseCapture();
                    };
                    this.setCapture && this.setCapture();
                    return false;
                };
            }
    
            /**
             * 找出最近的碰撞元素
             * @param {Element} obj 
             */
            function findNearest(obj) {
                var filterLi = [];//存放碰撞的元素
                var aDistance = [];//存放和碰撞元素的距离
        
                for(i = 0; i < aLi.length; i++) {
                    aLi[i] != obj && (isButt(obj, aLi[i]) && (aDistance.push(getDistance(obj, aLi[i])),
                        filterLi.push(aLi[i])));
                }
                var minNum = Number.MAX_VALUE;
                var minLi = null;
        
                for(i = 0; i < aDistance.length; i++) {
                    aDistance[i] < minNum && (minNum = aDistance[i], minLi = filterLi[i]);
                }
        
                return minLi;
        
            }
        } 
        GAME();
    
        //开始游戏
        oInput.onclick = function() {
            oDateStart = new Date();
            oBox.innerHTML = "";
            this.value = "\u91cd\u65b0\u5f00\u59cb"
            GAME(true);
        };
    
        /**
         * 拼图是否完成
         */
        function finish() {
            var aTemp = [];
            var success = true;
            aTemp.length = 0;
    
            for(i = 0; i < aLi.length; i++) {
                for(var j = 0; j < aLi.length; j++) {
                    i == aLi[j]["index"] && aTemp.push(aLi[j].getElementsByTagName("img")[0].src.match(/(\d+)\./)[1]);
                }           
            }
            for(i = 1; i <= aTemp.length; i++) {
                if(i != aTemp[i - 1]) {
                    success = false;
                    break;
                }
            }
            return success;
        }
    };
    
    
    /**
     * 求2个元素中心之间的距离
     * @param {Element} obj1 
     * @param {Element} obj2 
     */
    function getDistance(obj1, obj2) {
        var a = (obj1.offsetLeft + obj2.offsetWidth / 2) - (obj2.offsetLeft + obj2.offsetWidth / 2);
        var b = (obj1.offsetTop + obj1.offsetHeight / 2) - (obj2.offsetTop + obj2.offsetHeight / 2);
        return Math.sqrt(a * a + b * b);
    }
    
    /**
     * 碰撞检测
     * @param {Element} obj1 
     * @param {Element} obj2 
     */
    function isButt(obj1, obj2) {
        var l1 = obj1.offsetLeft;
        var t1 = obj1.offsetTop;
        var r1 = obj1.offsetLeft + obj1.offsetWidth;
        var b1 = obj1.offsetTop + obj1.offsetHeight;
    
        var l2 = obj2.offsetLeft;
        var t2 = obj2.offsetTop;
        var r2 = obj2.offsetLeft + obj2.offsetWidth;
        var b2 = obj2.offsetTop + obj2.offsetHeight;
    
        return !(r1 < l2 || b1 < t2 || r2 < l1 || b2 < t1);
    }
    
    /**
     * 获取最终样式
     * @param {Element} obj 元素
     * @param {string} attr 属性
     */
    function getStyle(obj, attr) {
        return parseFloat(obj.currentStyle ? obj.currentStyle[attr] : getComputedStyle(obj, null)[attr]);
    }
    
    //运动框架
    function startMove(obj, pos, onEnd) {
        clearInterval(obj.timer);
        obj.timer = setInterval(function() {
            doMove(obj, pos, onEnd);
        }, 30);
    }
    
    /**
     * 移动元素
     * @param {Element} obj 要移动的元素
     * @param {Object} pos 最终位置 {"left":"", "top": ""}
     * @param {function} onEnd 移动结束后要执行的函数,可选
     */
    function doMove(obj, pos, onEnd) {
        var iCurL = getStyle(obj, "left");
        var iCurT = getStyle(obj, "top");
        var iSpeedL = (pos.left - iCurL) / 5;
        var iSpeedT = (pos.top - iCurT) / 5;
        iSpeedL = iSpeedL > 0 ? Math.ceil(iSpeedL) : Math.floor(iSpeedL);
        iSpeedT = iSpeedT > 0 ? Math.ceil(iSpeedT) : Math.floor(iSpeedT);
    
        if(pos.left == iCurL && pos.top == iCurT) { //如果到达最终位置
            clearInterval(obj.timer);
            onEnd && onEnd();
        }
        else { //没到达则继续
            obj.style.left = iCurL + iSpeedL + "px";
            obj.style.top = iCurT + iSpeedT + "px";
        }
    }
    

    需要学习以下的知识点。

    createDocumentFragment方法

    用来创建一个虚拟的节点对象,或者说,是用来创建文档碎片节点。它可以包含各种类型的节点,在创建之初是空的。

    DocumentFragment节点不属于文档树,继承的parentNode属性总是null。它有一个很实用的特点,当请求把一个DocumentFragment节点插入文档树时,插入的不是DocumentFragment自身,而是它的所有子孙节点。这个特性使得DocumentFragment成了占位符,暂时存放那些一次插入文档的节点。它还有利于实现文档的剪切、复制和粘贴操作。
    另外,当需要添加多个dom元素时,如果先将这些元素添加到DocumentFragment中,再统一将DocumentFragment添加到页面,会减少页面渲染dom的次数,效率会明显提升。

    getComputedStyle方法

    getComputedStyle()方法。这个方法接受两个参数:要取得计算样式的元素和一个伪元素字符串(例如“:after”)。如果不需要伪元素信息,第二个参数可以是null。getComputerStyle()方法返回一个CSSStyleDeclaration对象,其中包含当前元素的所有计算的样式。

    IE中使用的是obj.currentStyle方法, 文中的使用方式可以保证兼容。

    不使用obj.style的原因是这个方法只能获取写在style属性中的值(style="…"),而无法获取定义在