基於lufylegend和cropper自定義圖片的拼圖華容道小遊戲
前幾天公司要做一個拼圖的小遊戲,我看到了ofollow,noindex">《速度挑戰 - 2小時完成HTML5拼圖小遊戲》 ,照著寫完了一個小遊戲以後,這幾天使用cropper 和lufylegend 遊戲引擎製作了一款簡單的可以自定義圖片的拼圖華容道遊戲,該遊戲除了實現基本的遊戲功能以外,還支援遊戲圖片上傳,剪下,以及圖片過大可以進行壓縮的功能。
準備
俗話說“兵馬未動糧草先行”,在直接開始擼遊戲之前,需要先做一些準備:
(1)cropper
cropper 是一款使用簡單且功能強大的圖片剪裁jQuery外掛,我選擇使用該外掛來實現圖片裁剪的功能,在使用之前需要引入cropper:
<link href="https://cdn.bootcss.com/cropperjs/1.3.6/cropper.min.css" rel="stylesheet"> <script src="https://cdn.bootcss.com/cropperjs/1.3.6/cropper.min.js"></script>
(2)jQuery
jQuery就不多說了,cropper就是jQuery的外掛,自然需要引入,需要注意的是jQuery需要在cropper之前引入
(3)lufylegend
lufylegend 是一個HTML5開源引擎,使用之前需要引入:
<script src="js/lib/lufylegend-1.10.1.simple.min.js"></script>
(4)WeUI
WeUI
是一套同微信原生視覺體驗一致的基礎樣式庫,由微信官方設計團隊為微信內網頁和微信小程式量身設計,令使用者的使用感知更加統一。使用這個ui庫主要是使用了gallery
用來展示上傳的圖片用於下一步的裁剪,要使用的話也需要引入:
<link rel="stylesheet" href="https://res.wx.qq.com/open/libs/weui/1.1.2/weui.min.css"> <script type="text/javascript" src="https://res.wx.qq.com/open/libs/weuijs/1.1.3/weui.min.js"></script>
開始
在做好之前的準備以後,就可以開始擼遊戲了
1.佈局
遊戲本身只需要一個div
,整個頁面分為兩塊,圖片上傳模組和遊戲模組,整體還是比較簡單,之後可以繼續增加其他的內容:
<input type="file" id="file" multiple="multiple" accept="image/*" style="display: none"> <!--圖片上傳按鈕--> <div class="weui-uploader__input-box getImg"> <input id="getImg" class="weui-uploader__input"> </div> <!--圖片上傳--> <div id="uploadPhotoBox" class="page gallery js_show img" style="display: none"> <div class="weui-gallery" style="display: block"> <div id="photoBox" class="photo weui-gallery__img"> <img id="photo" style="max-width: 100%" src=""> </div> <div class="tool"> <a> <i id="crop">確定</i> <i id="cancel">取消</i> </a> </div> </div> </div> <!--遊戲--> <div class="game" style="display: none"> <div class="time">時間: <span id="time">99</span></div> <div class="steps">步數: <span id="steps">0</span></div> <div id="myGame"></div> </div>
還有一點點樣式:
.time, .steps { position: absolute; margin-left: 20%; margin-top: 5%; } .steps { margin-top: 10%; } .tool { position: absolute; z-index: 2; background-color: #111111; width: 100%; } .tool i { font-size: 1rem; color: white; right: 0; float: right; padding-left: 2rem; padding-right: 2rem; } #cancel { float: left; }
2.定義全域性變數
首先需要定義一些變數,並且新增一些簡單的邏輯事件,例如滑鼠點選:
var imgResult;//裁剪出來圖片的base64值 var imgResultImg = new Image();//裁剪出來的圖片物件 var flObj = document.getElementById("file"); $('#getImg').click(function () { $('#file').bind('change', function () {//檔案上傳控制元件繫結監聽事件 var file = $(this).val(); if (file.length > 0) {//檔案不為空時自動提交圖片 uploadImg();//圖片提交 $('#photoBox').css('display', 'block'); $('.getImg').css('display', 'none'); } }); $('#file').click(); $('#uploadPhotoBox').css('display', 'block'); }); $('#cancel').click(function () {//取消圖片提交 window.location.reload(); });
上面的uploadImg()
函式會在之後有定義
3.圖片處理部分
需要完成圖片的上傳,壓縮,裁剪功能
3.1 圖片上傳
在使用者選擇檔案以後要先判斷是否是圖片,然後再判斷圖片大小確定是否要壓縮,然後才能開始裁剪
function uploadImg() {//圖片上傳 $('#photoBox').empty(); $('#photoBox').html('<img id="photo" src="">'); var file = flObj.files[0];//因為每次只上傳了一張圖片,所以獲取到flObj.files[0]; var fReader = new FileReader(); var isImage = checkFile(file);//檢查檔案是否為影象型別 if (!isImage) { alert("請確保檔案為影象型別"); } else { fReader.onload = function (e) { var imageSize = e.total; //圖片大小 var image = new Image(); image.src = e.target.result; image.onload = function () { //判斷是否需要壓縮圖片 image = judgeCompress(image, imageSize); document.getElementById("photo").src = image.src; cropper(document.getElementById("photo"), options); }; } } fReader.readAsDataURL(isImage); };
檢查檔案是否為影象型別:
function checkFile(file) {//檢查檔案是否為影象型別 console.log(file); //使用正則表示式匹配判斷 if (!/image\/\w+/.test(file.type)) { return false; } return file; }
3.2 圖片壓縮
在圖片壓縮之前,需要先檢查圖片大小,圖片過大才需要壓縮:
function judgeCompress(image, imageSize) {//判斷圖片大小 //判斷圖片是否大於300000 bit var threshold = 300000; //閾值,可根據實際情況調整 if (imageSize > threshold) { var imageData = compress(image);//圖片壓縮 var newImage = new Image(); newImage.src = imageData; return newImage; } else { return image; } }
圖片壓縮是使用canvas
實現,先將圖片繪製出來,然後再講繪製出來的圖片儲存為圖片物件以完成壓縮,程式碼實現如下:
function compress(image) {//圖片壓縮 var canvas = document.createElement("canvas"); var ctx = canvas.getContext("2d"); var originWidth = image.width; var originHeight = image.height; var maxWidth = 800, maxHeight = 800; // 目標尺寸 var targetWidth = originWidth, targetHeight = originHeight; // 圖片尺寸超過800x800的限制 if (originWidth > maxWidth || originHeight > maxHeight) { if (originWidth / originHeight > maxWidth / maxHeight) { // 更寬,按照寬度限定尺寸 targetWidth = maxWidth; targetHeight = Math.round(maxWidth * (originHeight / originWidth)); } else { targetHeight = maxHeight; targetWidth = Math.round(maxHeight * (originWidth / originHeight)); } } canvas.height = targetHeight; canvas.width = targetWidth; ctx.drawImage(image, 0, 0, targetWidth, targetHeight); //壓縮操作 var quality = 0.8; //圖片質量範圍:0<quality<=1 根據實際需求調正 var imageData = canvas.toDataURL("image/jpeg", quality); return imageData; }
3.3 圖片裁剪
得到了處理完成的圖片以後,就可以進行圖片裁剪了,首先先按照官網進行外掛的配置,我把它們寫在了一個物件中:
var options = { aspectRatio: 1,//寬高比 preview: '.preview',//預覽視窗 guides: true,//裁剪框的虛線 autoCropArea: 0.5,//0-1之間的數值,定義自動剪裁區域的大小,預設0.8 dragCrop: true,//是否允許移除當前的剪裁框,並通過拖動來新建一個剪裁框區域 movable: true,//是否允許移動剪裁框 resizable: true,//是否允許改變裁剪框的大小 zoomable: false,//是否允許縮放圖片大小 mouseWheelZoom: false,//是否允許通過滑鼠滾輪來縮放圖片 touchDragZoom: false,//是否允許通過觸控移動來縮放圖片 rotatable: false,//是否允許旋轉圖片 minContainerWidth: 200,//容器的最小寬度 minContainerHeight: 200,//容器的最小高度 minCanvasWidth: 0,//canvas 的最小寬度(image wrapper) minCanvasHeight: 0,//canvas 的最小高度(image wrapper) strict: true, };
然後就是圖片這部分最核心的裁剪部分了,但是使用cropper裁剪出來的圖片是canvas
,解決的方法是將canvas
使用toDataURL()
方法轉化為base64
之後再轉為img
物件,用於後面的操作:
function cropper(photo, options) {//圖片裁剪 var cropper = new Cropper(photo, options); $('#crop').on('click', function () { imgResult = cropper.getCroppedCanvas().toDataURL();//裁剪出來的base64 imgResultImg.src = imgResult;//裁剪出來的圖片物件 $('.img').css('display', 'none'); LInit(60, "myGame", gameWidth, gameHeight, main);//遊戲初始化 $('.game').css('display', 'block'); }) }
這樣子圖片處理部分就完成了,在圖片裁剪完成之後就開始初始化遊戲了。
4.遊戲部分
接下來就開始遊戲部分的製作,遊戲中大致思路模仿了《速度挑戰 - 2小時完成HTML5拼圖小遊戲》 ,在其基礎上做了修改
4.1 定義變數
首先還是要定義遊戲中需要用到的變數:
/** 初始化遊戲 */ var gameWidth = 390; var gameHeight = 390; /** 遊戲層 */ var stageLayer, gameLayer, overLayer; /** 拼圖塊列表 */ var blockList; /** 是否遊戲結束 */ var isGameOver, isTimeOver; /** 用時 */ var startTime, time, countTime; /** 步數 */ var steps; /** 圖片 */ var imgBmpd, startNewGame, fail, startBitmap, Again, failBitmap, againBitmap, succeed, succeedBitmap; var _blockList = [];//拼圖序列 var datalist = [];//存放圖片
4.2 遊戲初始化
遊戲初始化,包括素材的載入,以及遊戲介面的顯示:
function main() {//遊戲資源初始化 /** 全屏設定 */ if (LGlobal.mobile) { LGlobal.width = gameWidth; LGlobal.height = gameHeight; LGlobal.stageScale = LStageScaleMode.SHOW_ALL; } LGlobal.screen(LGlobal.FULL_SCREEN); LGlobal.preventDefault = false; /** 新增載入提示 */ var loadingHint = new LTextField(); loadingHint.text = "資源載入中……"; loadingHint.size = 20; loadingHint.x = (LGlobal.width - loadingHint.getWidth()) / 2; loadingHint.y = (LGlobal.height - loadingHint.getHeight()) / 2; addChild(loadingHint); /** 載入圖片 檔案*/ LLoadManage.load( [ {path: "./js/Block.js"}, {name: "startGame", path: "./images/start.png"}, {name: "fail", path: "./images/fail.png"}, {name: "Again", path: "./images/challengeAgain.png"}, {name: "succeed", path: "./images/succeed.png"}, ], null, function (result) { /** 移除載入提示 */ loadingHint.remove(); /** 儲存點陣圖資料,方便後續使用 */ imgBmpd = new LBitmapData(imgResultImg); gameInit(result); } ); } function gameInit(e) {//遊戲內容初始化 datalist = e; var bitmapData = new LBitmapData(imgResultImg); var bitmap = new LBitmap(bitmapData); bitmap.scaleX = LGlobal.width / bitmap.width; bitmap.scaleY = LGlobal.height / bitmap.height; bitmap.width = LGlobal.width; bitmap.height = LGlobal.height; bitmap.x = 0; bitmap.y = 0; addChild(bitmap); startNewGame = new LBitmapData(datalist['startGame']); fail = new LBitmapData(datalist["fail"]); Again = new LBitmapData(datalist["Again"]); succeed = new LBitmapData(datalist["succeed"]); startBitmap = new LBitmap(startNewGame); failBitmap = new LBitmap(fail); againBitmap = new LBitmap(Again); succeedBitmap = new LBitmap(succeed); /** 初始化舞臺層 */ stageLayer = new LSprite(); stageLayer.graphics.drawRect(0, "", [0, 0, LGlobal.width, LGlobal.height], true, "transparent"); addChild(stageLayer); /** 初始化遊戲層 */ gameLayer = new LSprite(); stageLayer.addChild(gameLayer); /** 初始化最上層 */ overLayer = new LSprite(); stageLayer.addChild(overLayer); /** 新增開始介面 */ addBeginningUI(); } function addBeginningUI() {//遊戲開始介面 var beginningLayer = new LSprite(); beginningLayer.graphics.drawRect(0, "", [0, 0, LGlobal.width, LGlobal.height], true, "transparent"); stageLayer.addChild(beginningLayer); /** 遊戲標題 */ var title = new LTextField(); title.text = "拼圖華容道"; title.size = 48; title.weight = "bold"; title.x = (LGlobal.width - title.getWidth()) / 2; title.y = 70; title.color = "#f8fbb5"; title.lineWidth = 5; title.lineColor = "#000000"; title.stroke = true; beginningLayer.addChild(title); /** 開始遊戲提示 */ startBitmap.scaleX = 0.7; startBitmap.scaleY = 0.7; startBitmap.x = (LGlobal.width - startBitmap.getWidth()) / 2; startBitmap.y = 250 + 0; beginningLayer.addChild(startBitmap); /** 初始化拼圖塊列表 */ initBlockList(); /** 打亂拼圖 */ getRandomBlockList(); /** 開始遊戲 */ beginningLayer.addEventListener(LMouseEvent.MOUSE_UP, function () { beginningLayer.remove(); startGame(); }); }
4.3 遊戲主體
接下來來到了整個遊戲最重要的部分,遊戲主體的實現,開始以後將拼圖打亂,然後進行遊戲:
(1)開始遊戲
點選介面以後將遊戲的各種值初始化,然後開始遊戲
function startGame() {//開始遊戲 isGameOver = false; isTimeOver = false; /** 初始化時間和步數 */ startTime = (new Date()).getTime(); countTime = 0; time = 0; steps = 0; /** 顯示拼圖 */ showBlock(); /** 計時 */ updateTimeTxt(countTime); /** 顯示步數 */ updateStepsTxt(); stageLayer.addEventListener(LEvent.ENTER_FRAME, onFrame); }
(2)顯示拼圖
顯示拼圖顯然不是一張圖,那麼就需要拼圖塊,後面會定義一個類來表示這些拼圖塊
function showBlock() {//顯示拼圖 for (var i = 0, l = blockList.length; i < l; i++) { var b = blockList[i]; /** 根據序號計算拼圖塊位置 */ var y = (i / 3) >>> 0, x = i % 3; b.setLocation(x, y); gameLayer.addChild(b); } }
(3)初始化拼圖列表
根據序號計算拼圖塊圖片顯示位置,將拼圖塊存放到列表中
function initBlockList() {//初始化拼圖列表 blockList = new Array(); for (var i = 0; i < 9; i++) { /** 根據序號計算拼圖塊圖片顯示位置 */ var y = (i / 3) >>> 0, x = i % 3; blockList.push(new Block(i, x, y)); } }
(4)打亂拼圖
這裡使用的是隨機打亂拼圖,然後計算序列的倒序和,如果倒序和是奇數,拼圖無法完成,需要重新打亂,直到倒序和為偶數。
function getRandomBlockList() {//隨機打亂拼圖 /** 隨機打亂拼圖 */ blockList.sort(function () { return 0.5 - Math.random(); }); /** 計算逆序和 */ var reverseAmount = 0; for (var i = 0, l = blockList.length; i < l; i++) { var currentBlock = blockList[i]; for (var j = i + 1; j < l; j++) { var comparedBlock = blockList[j]; if (comparedBlock.index < currentBlock.index) { reverseAmount++; } } } /** 檢測打亂後是否可還原 */ if (reverseAmount % 2 != 0) { /** 不合格,重新打亂 */ getRandomBlockList(); } else { var str = ""; for (var i = 0; i < blockList.length; i++) { str = str + blockList[i].index; } console.log(str); if (str.substr(0, 3) == "012") { getRandomBlockList(); } else { _blockList.push(str); } } }
4.4 拼圖塊
在拼圖過程中,引入了一個新的Block
類,這個類用來表示並且操作拼圖塊:
function Block(index, x, y) { LExtends(this, LSprite, []); var bmpd = imgBmpd.clone(); if (index != 8) { bmpd.setProperties(x * bmpd.width / 3, y * bmpd.width / 3, bmpd.width / 3, bmpd.width / 3); this.bmp = new LBitmap(bmpd); this.bmp.scaleX = 130 / this.bmp.width; this.bmp.scaleY = 130 / this.bmp.height; this.addChild(this.bmp); } else { var shape = new LShape(); shape.graphics.drawRect(2, "#ffffff", [0, 0, 130, 130], true, "#ffffff"); this.addChild(shape); } // 格子邊框 var border = new LShape(); border.graphics.drawRect(3, "#ffffff", [0, 0, 130, 130]); border.graphics.drawRoundRect(3, "#ffffff", [0, 0, 130, 130, 10]); this.addChild(border); this.index = index; this.addEventListener(LMouseEvent.MOUSE_UP, this.onClick); } Block.getBlock = function (x, y) { return blockList[y * 3 + x]; }; Block.isGameOver = function () { var reductionAmount = 0, l = blockList.length; /** 計算還原度 */ for (var i = 0; i < l; i++) { var b = blockList[i]; if (b.index == i) { reductionAmount++; } } /** 計算是否完全還原 */ if (reductionAmount == l) { /** 遊戲結束 */ gameOver(); } }; Block.exchangePosition = function (b1, b2) { var b1x = b1.locationX, b1y = b1.locationY, b2x = b2.locationX, b2y = b2.locationY, b1Index = b1y * 3 + b1x, b2Index = b2y * 3 + b2x; /** 在地圖塊陣列中交換兩者位置 */ blockList.splice(b1Index, 1, b2); blockList.splice(b2Index, 1, b1); /** 交換兩者顯示位置 */ b1.setLocation(b2x, b2y); b2.setLocation(b1x, b1y); /** 判斷遊戲是否結束 */ Block.isGameOver(); }; Block.prototype.setLocation = function (x, y) {//方塊位置 this.locationX = x; this.locationY = y; this.x = x * 130; this.y = y * 130 + 0; }; Block.prototype.onClick = function (e) {//方塊的點選事件 var self = e.currentTarget; if (isGameOver) { return; } var checkList = new Array(); /** 判斷右側是否有方塊 */ if (self.locationX > 0) { checkList.push(Block.getBlock(self.locationX - 1, self.locationY)); } /** 判斷左側是否有方塊 */ if (self.locationX < 2) { checkList.push(Block.getBlock(self.locationX + 1, self.locationY)); } /** 判斷上方是否有方塊 */ if (self.locationY > 0) { checkList.push(Block.getBlock(self.locationX, self.locationY - 1)); } /** 判斷下方是否有方塊 */ if (self.locationY < 2) { checkList.push(Block.getBlock(self.locationX, self.locationY + 1)); } for (var i = 0, l = checkList.length; i < l; i++) { var checkO = checkList[i]; /** 判斷是否是空白拼圖塊 */ if (checkO.index == 8) { steps++; updateStepsTxt(); Block.exchangePosition(self, checkO); var str = ""; for (var i = 0; i < blockList.length; i++) { str = str + blockList[i].index; } _blockList.push(str); break; } } };
4.5 遊戲結束
新增遊戲結束功能,根據拼圖完成或者計時結束判斷遊戲成功或是失敗
(1)遊戲計時計步
遊戲未結束之前更新遊戲的時間和步數:
function onFrame() {//計時 if (isGameOver) { return; } if (isTimeOver) { return; } /** 獲取當前時間 */ var currentTime = (new Date()).getTime(); /** 計算使用的時間並更新時間顯示 */ time = currentTime - startTime; if (countTime > 0) {// 倒計時 updateTimeTxt(); } else { timeOver(); } }
(2)更新時間和步數
function updateTimeTxt() {//更新時間 $('#time').html(getTimeTxt()); } function getTimeTxt() { var d = new Date(time); countTime = 99 - Math.floor(d / 1000); return countTime; } function updateStepsTxt() {//更新步數 $('#steps').html(steps); }
(3)遊戲失敗
遊戲時間結束後視為遊戲失敗,彈出遊戲失敗介面,點選重新開始
function timeOver() {// 判斷時間是否結束 失敗 isTimeOver = true; var resultLayer = new LSprite(); resultLayer.filters = [new LDropShadowFilter()]; resultLayer.graphics.drawRoundRect(3, "#BBBBBB", [0, 0, 390, 450, 5], true, "rgba(0,0,0,.6)"); resultLayer.x = (LGlobal.width - resultLayer.getWidth()) / 2; resultLayer.y = LGlobal.height / 2; resultLayer.alpha = 0; overLayer.addChild(resultLayer); failBitmap.scaleX = 0.6; failBitmap.scaleY = 0.6; failBitmap.x = (LGlobal.width - failBitmap.getWidth()) / 2; failBitmap.y = 70 + 0; resultLayer.addChild(failBitmap); againBitmap.scaleX = 0.6; againBitmap.scaleY = 0.6; againBitmap.x = (LGlobal.width - againBitmap.getWidth()) / 2; againBitmap.y = 250 + 0; resultLayer.addChild(againBitmap); LTweenLite.to(resultLayer, 0.5, { alpha: 0.7, y: (LGlobal.height - resultLayer.getHeight()) / 2 - 15, onComplete: function () { /** 點選介面重新開始遊戲 */ stageLayer.addEventListener(LMouseEvent.MOUSE_UP, function () { gameLayer.removeAllChild(); overLayer.removeAllChild(); stageLayer.removeAllEventListener(); main(); }); } }); }
(4)遊戲成功
拼圖完成時遊戲成功,彈出遊戲成功介面,輸出遊戲用時,步數,分數以及遊戲情況的序列,點選重新開始
function gameOver() {// 遊戲成功 let score = 99 - getTimeTxt(time); let blockList = _blockList.join("-"); let step = steps; isGameOver = true; console.log('用時:' + getTimeTxt(time) + '步數:' + step + '分數:' + score + '序列:' + blockList); var resultLayer = new LSprite(); resultLayer.filters = [new LDropShadowFilter()]; resultLayer.graphics.drawRoundRect(3, "#BBBBBB", [0, 0, 390, 450, 5], true, "rgba(0,0,0,.6)"); resultLayer.x = (LGlobal.width - resultLayer.getWidth()) / 2; resultLayer.y = LGlobal.height / 2; resultLayer.alpha = 0; overLayer.addChild(resultLayer); succeedBitmap.scaleX = 0.6; succeedBitmap.scaleY = 0.6; succeedBitmap.x = (LGlobal.width - succeedBitmap.getWidth()) / 2; succeedBitmap.y = 70 + 0; resultLayer.addChild(succeedBitmap); againBitmap.scaleX = 0.6; againBitmap.scaleY = 0.6; againBitmap.x = (LGlobal.width - againBitmap.getWidth()) / 2; againBitmap.y = 250 + 0; resultLayer.addChild(againBitmap); LTweenLite.to(resultLayer, 0.5, { alpha: 0.7, y: (LGlobal.height - resultLayer.getHeight()) / 2 - 15, onComplete: function () { /** 點選介面重新開始遊戲 */ stageLayer.addEventListener(LMouseEvent.MOUSE_UP, function () { gameLayer.removeAllChild(); overLayer.removeAllChild(); stageLayer.removeAllEventListener(); main(); }); } }); }
反思和總結
感受
終於擼完了這個簡單的小遊戲,雖然寫的毛毛糙糙,很多地方寫得也不夠好,但是收穫肯定是有的,不光是看了文件,學習了大佬們的思路,同時也增長了一些動手能力,雖然程式碼比較簡單,但是隻要堅持,日積月累肯定會有收穫。
可優化項
在寫這篇部落格的時候,回顧程式碼就已經滿滿的槽點了,把可優化項記錄下來,以後有空可以嘗試去優化:
- 1.使用jQuery和原生js兩種方式獲取dom元素,編碼習慣不好,使程式碼更加混亂
- 2.遊戲白底介面過於簡單
- 3.隨機打亂拼圖,使遊戲無法設定多種難度,缺少遊戲樂趣
- 4.圖片裁剪部分佈局粗糙使用WeUI來完成
- 5.無法保留遊戲記錄