邊做邊學jQuery系列11 - jQuery UI牛刀小試-刁十三支!

課程錄製時間: 2009年
jQuery UI牛刀小試-刁十三支!

歷經上集對jQuery UI的示範,相信大家對jQuery UI所提供的人性化操作介面肯定印象深刻吧! 只需要幾行程式就可以在網頁上加入Window Form般的拖拉、放置、排序效果,而Widget提供了日期選擇器、頁籤、摺疊選單等UI常用的小控制項,用網頁比擬Window Form效果的門檻頓時降低不少。對jQuery UI有了基本認識後,這回讓我們小試身手,利用jQuery UI的draggable、droppable、sorttable實作一個有趣的十三支刁牌介面。

【介面設計構想】

首先界定一下目標,我們並不打算做一套完整的十三支遊戲,重點只放在利用jQuery UI,在網頁實現拖拉操作,證明只需少許程式就可達成我們要的效果。我們都知道,"刁十三支"的精髓在於將十三張牌依最有利的方式分成三張、五張、五張共三墩的組合。"刁"的樂趣在於嘗試自由組合牌形,提高勝算,因此使用者需要將發到的牌分配到三墩之一,並有可能反覆嘗試移動牌所在墩數或交換位置,這一切操作我們都打算在網頁上透過拖拉方式完成。(像我一樣不懂十三支規則的人可以參考遊戲廠商所提供的資訊,例如這裡。)

如上圖,我們的界面將分成上下兩區,上方為發牌區,一開始會放入十三張牌;下方為刁牌區,由三個虛線框出三墩的位置。使用者可以將上方的牌拖入下方的三墩區域,或將牌拖回發牌區。進一步,牌要能在各墩間移動,同一墩內的牌也要能對調位置。

【顯示撲克牌】

撲克牌牌面來自於網路上找到的現成圖檔[來源],但這裡並不打算切割出52個小圖檔,而是借用上回製作拼圖時所用的裁圖技巧,以一張牌大小的<div>設定overflow:hidden,內嵌圖檔再以marging-left、margin-top設定負值的方式製造位移,達到顯示不同牌面的目的。

    <style type="text/css">
    body { padding: 0px; margin: 0px; }
    /* 撲克牌裁圖 */
    .clsPokerFrame  
    {
        overflow:hidden; padding:0px;
        width:71px; height:96px; 
    }
    </style>
    <script type="text/javascript">
        $(function() {
            //利用圖片座標位移顯示特定牌面
            function getCard(cardId) {
                var pos = cardId.split("-");
                return $(
                "<div class=clsPokerFrame>" +
                "<img src='poker.gif' style='margin-left:" +
                (parseInt(pos[1]) - 1) * -71 + "px;margin-top:" +
                (parseInt(pos[0]) - 1) * -96 + "px;' /></div>");
            }

            $("body")
            .append(getCard("1-13"))
            .append(getCard("2-13"))
            .append(getCard("3-13"))
            .append(getCard("4-13"))
            .find(".clsPokerFrame")
            .css({float: "left", margin: "5px"});
        });
    </script>

在此,我們將顯示特定牌面的功能寫成函數getCard,傳入1-13的字串,就可顯示花色1(黑桃)的第13張牌(老K),在上例中,我們亮出一手老K鐵支,再加上CSS float:left讓四個<div>可以並排顯示,並設定magin在牌的上下左右間保留一點空隙:

【洗牌與發牌】

可以任意顯示每一張牌面後,我們先建立一個1-1, 1-2, ..., 1-13, 2-1, ... , 4-13包含52張牌的陣列代表一副樸克牌。接著以亂數任意將其中兩張對調順序,連續做500次就可以模擬洗牌後的結果。最後,將13牌append()放入<div>中(用綠底比較有賭場牌桌的味道)。執行程式,我們可以在畫面上看到一副洗好的樸克牌。

    <style type="text/css">
    body { padding: 0px; margin: 0px; }
    /* 撲克牌裁圖 */
    .clsPokerFrame  
    {
        overflow:hidden; padding:0px;
        width:71px; height:96px; 
    }
    /* 發牌區 */
    .clsCardPool 
    {
    	width: 1024px; height: 110px;
    	border: solid 2px blue;
    	background-color: green;
    	margin: 2px;
    }
    .clsCardPool div 
    { 
    	float: left; margin: 2px;
    }
    </style>
    <script type="text/javascript">
        $(function() {

            //產生52張牌
            var cards = [], c = 0;
            for (var i = 1; i <= 4; i++) {
                for (var j = 1; j <= 13; j++) {
                    cards[c++] = i + "-" + j;
                }
            }
            //洗牌
            for (var i = 0; i < 500; i++) {
                var j = parseInt(Math.random() * 52);
                var k = parseInt(Math.random() * 52);
                var t = cards[j]; cards[j] = cards[k]; cards[k] = t;
            }

            //發13張
            var cardPool = $("#dvCardPool");
            $.each(cards, function(i, v) {
                if (i >= 13) return false;
                cardPool.append(getCard(v));
            });
            //利用圖片座標位移顯示特定牌面
            function getCard(cardId) {
                var pos = cardId.split("-");
                return $(
                "<a href='javascript:void(0);' id='C" + cardId + "'>" +
                "<div class=clsPokerFrame>" +
                "<img src='poker.gif' style='margin-left:" +
                (parseInt(pos[1]) - 1) * -71 + "px;margin-top:" +
                (parseInt(pos[0]) - 1) * -96 + "px;' /></div></a>");
            }

        });
    </script>

【顯示與互動強化】

接著,由於13張牌平面攤開太寬,我們做個小調整,使其部分相疊,縮短佔用的寬度。這裡使用的方法是將CSS position設為absolute,稍候我們會用程式逐一設定每張牌的left值。

由於使用者會從13張牌中挑選一張進行操作,我們再做點小手腳讓它生動一點。我們希望當滑鼠移到牌面上方時,該牌可以向下突出、並加上邊框,呈現出聚焦的效果。雖然jQuery裡有hover可以讓我們撰寫函數達到此一目的,此處則決定借用CSS a:hover的滑鼠感知功能,不必撰寫程式只靠設定樣式達到同樣效果。

    .clsCardPool a
    {
	    position: absolute;
	    top: 10px;
    }
    .clsCardPool a:hover 
    {
    	top: 15px;
    	border: solid 2px yellow;
    }

為配合演出,顯示牌面時,要在<div>外再加上一個<a>,getCard()做了小小修改,最後再補上逐一設定left值的邏輯。

            //利用圖片座標位移顯示特定牌面
            function getCard(cardId) {
                var pos = cardId.split("-");
                return $(//外包link以適用a:hover
                "<a href='javascript:void(0);' id='C" + cardId + "'>" +
                "<div class=clsPokerFrame>" +
                "<img src='poker.gif' style='margin-left:" +
                (parseInt(pos[1]) - 1) * -71 + "px;margin-top:" +
                (parseInt(pos[0]) - 1) * -96 + "px;' /></div></a>");
            }
            //排列整齊
            $("#dvCardPool a").each(function(i) {
                $(this).css({
                    "left": (i * 35 + 15) + "px"
                });
            });
            

修改後執行結果以下圖,牌面變緊湊了,同時隨著滑鼠移動,焦點所在的牌會突出並加黃框顯示。

【加入拖拉效果】

看到滑鼠所在的牌面會自動突出,大家是否會有不自覺有衝動想要把牌拖拉出來呢? 不急,有jQuery UI在,這只是小Case。

我們先下載取得jquery.ui.js,並在網頁中引用: <script src="jquery-ui.js" type="text/javascript"></script>

接著只要加入以下程式: (opacity:0.5設定可以讓牌面在拖拉過程呈現半透明)
$("#dvCardPool a").draggable({ opacity: 0.50 });
接著執行程式跑看看,神奇的事發生了,我們只加了一行程式,牌面立刻就多了拖拉效果,試著操作一下,我們可以將牌移到網頁上的任意位置。

【放置邏輯】

接著我們來佈置牌桌。由於十三支要將13張牌分成3張、5張、5張三墩,所以我們先放一個大的<div>,裡面再擺三個<div>分成三墩,這一切透過HTML+CSS就可以搞定:

<div id="dvCardPool" class="clsCardPool"></div>
<div id="dvGambleTable">
<div id="dvPack1" style="width: 250px;" class="clsCardPack" maxcards="3"></div>
<div id="dvPack2" style="width: 410px;" class="clsCardPack" maxcards="5"></div>
<div id="dvPack3" style="width: 410px;" class="clsCardPack" maxcards="5"></div>
</div>

在CSS中,我們調整背景色,以及顯示配置、尺寸等細節。

    /* 牌桌 */
    #dvGambleTable {
        border:solid 3px brown;
        background-color:Green;
        width: 500px;
        height: 400px;
        margin-top: 50px;
    }
    /* 墩 */
    .clsCardPack 
    {
    	margin-top: 10px;
    	margin-left: 10px;
    	border: dotted 1px white; 
    	height: 110px;
    }
    .clsCardPack a
    {
    	float: left;
    	margin: 5px;
    }	
	
	

在剛才加入draggable()後,牌可以被拖到網頁上的任意位置,但我們希望做到的是牌只能被放入到各墩所在範圍內,這要透過droppable()完成。而這裡的"放置",其實並不是直接將元素由上方的dvCardPool移為.clsCardPack的子元素,背後動作其實是將dvCardPool來源的牌隱藏(不能刪除,否則會影響後續流程),並在.clsCardPack中,加入ui事件參數draggable物件的複製分身。在加入時要留意各墩的牌數上限,我們透過上述的maxcards自訂屬性加以管控。

另外,我們不允許牌被拖到墩以外的範圍,也就是若沒有觸發.clsCardPack的drop事件,牌不會被複製到墩區,整個拖拉操作視為無效。draggable有個屬性revert: true,可以讓拖拉動作結束時讓牌面乖乖退回原來的位置。

            //允許拖拉
            $("#dvCardPool a").draggable({
                revert: true, //拖完返回原始位置
                opacity: 0.50 //拖拉過程半透明
            });

            //接受放牌
            $(".clsCardPack").droppable({
                drop: function(evt, ui) {
                    var cardPack = $(this);
                    if (cardPack.find("a").length < parseInt(cardPack.attr("maxcards"))) {
                        //複製到"墩"
                        ui.draggable.clone().appendTo(cardPack)
                        .css({ position: "", top: "", left: "", opacity: "" });
                        //原牌隱藏,直接刪除會影響draggable的結束事件
                        ui.draggable.hide();
                    }
                }
            });

如此,刁牌的基本操作就完成了。

【加入排序操作】

簡單地"刁"一下,就會發現有個明顯不足,牌一旦被放入墩中,就不能再更動位置了,而我們常要把牌的前後位置挪動一下看起來比較順眼。(例如順子的牌習慣要由小到大、Two Pair的單張放在中間)

jQuery裡的sortable()可以輕鬆讓我們做到這一點。沒錯,只要加一行程式就可以了: $(".clsCardPack").sortable(); (或者直接串接在前述droppable()的後方)

試操作一下,我們可以藉著拖拉任意改變牌的順序。

等一下,有個更驚人的發現,加入sortable()後,我們還可以將牌拖到其他墩去! 原來,使用了sortable也等同於在宣告了draggable,因此droppable的邏輯會套用來自發牌區或其他墩移來的元素上。但是有個小缺點,由於我們在drop事件複製ui.draggable元素時,只是將來源元素隱藏,因此由第一墩移第二墩時,會形成第一墩有一張隱形牌,第二墩存在一張分身。隱形牌會打亂每墩牌數的計算,到後面會形成明明還有空位,卻再也不能拖牌加進去的窘境。

因為不能直接刪除ui.draggable,我們在.clsCardPack加上over事件清除隱藏牌來克服這個問題:

            //接受放牌
            $(".clsCardPack").droppable({
                over: function(evt, ui) {
                    //將隱藏的牌去除,以免影響計數
                    $(this).find("a:hidden").remove();
                },
                drop: function(evt, ui) {
                    var cardPack = $(this);
                    if (cardPack.find("a").length < parseInt(cardPack.attr("maxcards"))) {
                        //複製到"墩"
                        ui.draggable.clone().appendTo(cardPack)
                        .css({ position: "", top: "", left: "", opacity: "" });
                        //原牌隱藏,直接刪除會影響draggable的結束事件
                        ui.draggable.hide();
                    }
                }
            });

【退回發牌區】

到這裡,我們的程式已完成得差不多,只少了一味。有時我們會對原先的排法不滿意,想重新調過。此時很自然地會想將牌由墩區移回發牌區,再試試其他組合。但目前發牌區並不接受擺放,丟到發牌區的牌會被退回來。要補上這點功能不難,我們只要加入以下的程式就搞定了:

            //接受將牌拖回發牌區
            $(".clsCardPool").droppable({
                drop: function(evt, ui) {
                    //如果該牌是要回籠
                    var cId = ui.draggable.attr("id");
                    var hdnCard = $(this).find("#" + cId + ":hidden");
                    if (hdnCard.length > 0) {
                        ui.draggable.hide();
                        hdnCard.css("top", "").show();
                    }
                }
            });

在發牌區裡,被移出的牌都還存在,只是被隱藏,因此在drop事件毋需複製,只要將ui.draggable隱藏,並透過id比對將發牌區該張牌再顯示出來即可。

大功告成,大家來享受一下在網頁上刁牌的樂趣吧!

【範例檔案下載】

歡迎推文分享:



 
RSS
【工商服務】
OrcsWeb: Windows Server Hosting
twMVC

關於作者

一個醉心技術又酷愛分享的Coding魔人,十年的IT職場生涯,寫過系統、管過專案, 也帶過團隊,最後還是無怨無悔地選擇了技術鑽研這條路,近年來則以做一個"有為的中年人"自許。