邊做邊學 jQuery 系列 11- jQuery UI牛刀小試-刁十三支!
邊做邊學 jQuery 系列 11- jQuery UI牛刀小試-刁十三支! 教學影片 > [!VIDEO https://www.microsoft.com/zh-tw/videoplayer/embed/21873f64-7d1a-406c-8a54-fc642e62f940] |
歷經上集對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比對將發牌區該張牌再顯示出來即可。
大功告成,大家來享受一下在網頁上刁牌的樂趣吧!
【範例檔案下載】