點贊 + 收藏 + 關注=學會了
這次會使用css畫出一個格子背景。并且一步步分析如何實現~
直接給答案:通過2個相等的直角三角形拼接,形成一個正方形。
三角形可以使用 background-image 的漸變來實現。
html,
body {
margin: 0;
width: 100%;
height: 100%;
}
body {
background-image: linear-gradient(45deg, #000 25%, transparent 0);
}
此時出來的效果如上圖所示。
做一個45度的線性漸變,第一個顏色是#000(黑色),占整個背景貼片的25%,其余部分都是紅色。
在上面的基礎上,用 background-size 來控制背景貼片的大小。
body {
background-image: linear-gradient(45deg, #000 25%, transparent 0);
background-size: 200px 200px;
}
開始有點想法了嗎?
此時如果我們再畫多一個反過來的黑色的直角三角形,拼在一起不就成了正方形了嗎?
反過來的三角形怎么畫呢?我嘗試將黑色從 25% 改成 75%,會得到以下效果
body {
background-image: linear-gradient(45deg, #000 75%, transparent 0);
background-size: 200px 200px;
}
可以看到紅色的三角形就是原本黑色三角形反過來的樣子。
把上圖的“白色三角形”變成黑色,原本的黑色三角形(25%)繼續保留。
于是我又加多層漸變~
body {
background-image:
linear-gradient(45deg, #000 25%, transparent 0),
linear-gradient(45deg, transparent 75%, #000 0);
background-size: 200px 200px;
}
簡化一下代碼:
body {
background-image: linear-gradient(45deg, #000 25%, transparent 0, transparent 75%, #000 0);
background-size: 200px 200px;
}
最后再做多一層上面的效果,然后移動一下其中一層的位置,就可以合并成一個黑色正方形。
body {
background-image:
linear-gradient(45deg, #000 25%, transparent 0, transparent 75%, #000 0),
linear-gradient(45deg, #000 25%, transparent 0, transparent 75%, #000 0);
background-position: 0 0, 100px 100px;
background-size: 200px 200px;
}
大功告成。
最后需要提醒的是,在本例中 background-position 第二個漸變的位移是 background-size 的一半,這樣就能實現這種格子背景了~
<style>
html,
body {
margin: 0;
width: 100%;
height: 100%;
}
body {
background-image:
linear-gradient(45deg, #000 25%, transparent 0, transparent 75%, #000 0),
linear-gradient(45deg, #000 25%, transparent 0, transparent 75%, #000 0);
background-position: 0 0, 100px 100px;
background-size: 200px 200px;
}
</style>
這是做成背景的完整代碼。
想實現2048游戲書寫代碼時可以分為三個步驟
先書寫HTML把游戲結構搭建出來
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="index.css">
</head>
<body>
<!--最外部的大框-->
<div class="outermost"> //包裹游戲全局的大盒子
<!--title-->
<span class="top"><b>SCORE:<span id="score01"></span></b></span>//頂部實時顯示的游戲分數
<!--游戲大框框-->
<div class="big">//2048游戲為四行四列因此需要16個div盒子
<div class="cell" id="c00"></div>
<div class="cell" id="c01"></div>
<div class="cell" id="c02"></div>
<div class="cell" id="c03"></div>
<div class="cell" id="c10"></div>
<div class="cell" id="c11"></div>
<div class="cell" id="c12"></div>
<div class="cell" id="c13"></div>
<div class="cell" id="c20"></div>
<div class="cell" id="c21"></div>
<div class="cell" id="c22"></div>
<div class="cell" id="c23"></div>
<div class="cell" id="c30"></div>
<div class="cell" id="c31"></div>
<div class="cell" id="c32"></div>
<div class="cell" id="c33"></div>
//游戲結束時會彈出的提示框
</div>
<!--提示框-->
<div class="tips" id="gameover">
<p>GAME OVER!!! <br>
SCORE: <span id="score02">0</span><br>
<button class="startbtn">重新開始</button>
</p>
</div>
<!--重玩一遍-->
<div class="foot">
<button class="replay"><a>重玩一遍</a></button>
</div>
</div>
<script type="text/javascript" src="index.js"></script>
</body>
</html>
經過了第一步的搭建游戲框架,第二部就是給游戲添加樣式,使它能顯示出來
*{
padding: 0px;
margin: 0px auto;
font-family: Arial;
}
/*最外部的大框*/
.outermost{
width: 480px;
height: 600px;
font-size: 40px;
margin-top: 120px;
}
/*title*/
<!--頂部顯示分數的樣式-->
.top{
margin: auto;
}
.top span{
color: red;
}
/*游戲大框框*/
.big{
width: 480px;
height: 480px;
background:pink;
border-radius: 8px;
}
<!--給每一個盒子包裹的小框子添加樣式-->
.cell{
list-style: none;
float: left;
display: inline-block;
width: 100px;
height: 100px;
line-height: 100px;
text-align: center;
background-color: #fbf8cd;
margin-left: 16px;
margin-top: 16px;
border-radius: 6px;
}
<!--提前把出現的數2、4、8、16等的數所在的格子給添加好樣式增加游戲體驗感-->
.n2{background-color:#f65e3b;color:#776e65}
.n4{background-color:#33b5e5;color:#776e65}
.n8{background-color:#f2b179;color:#776e65}
.n16{background-color:#f59563;color:#776e65}
.n32{background-color:#f67c5f;color:#776e65}
.n64{background-color:#f65e3b;color:#776e65}
.n128{background-color:#edcf72;color:#776e65}
.n256{background-color:#edcc61;color:#776e65}
.n512{background-color:#9c0;color:#776e65}
.n1024{background-color:#33b5e5;color:#776e65;font-size:40px}
.n2048{background-color:#09c;color:#776e65;font-size:40px}
/*提示框樣式*/
.tips{
border: 1px solid #cccccc;
background: #FFFFFF;
width: 400px;
height: 200px;
border-radius: 10px;
color: #ff4456;
text-align: center;
line-height: 60px;
position: absolute;
top: 50%;
left: 50%;
margin-left: -200px;
margin-top: -100px;
display: none;
}
.tips .startbtn{
height: 50px;
width: 200px;
color: #FFFFFF;
font-size: 20px;
line-height: 50px;
border-radius: 10px;
background: cornflowerblue;
border: none;
}
/*重玩一遍*/
.foot{
width: 200px;
height: 50px;
}
.foot>.replay{
width: 200px;
height: 50px;
background: aquamarine;
margin-top: 60px;
color: lightpink;
border:0;
font-size: 24px;
font-weight: bold;
border-radius: 6px;
}
書寫好了HTML+CSS部分游戲的模樣也就出來了,如下圖所示:
下面就到了最后也是最關鍵的一步----添加行為,也就是JS部分的書寫,給其添加效果
//創建一個對象,里面存儲所有的游戲數據及游戲方法
var game={
data : [], //定義一個數組,用來存所有的游戲的數據
score : 0, //定義一個分數的屬性
gamerunning : 1, //定義一個游戲運行的狀態,將其設置為1與其他狀態區分開
gameover : 0, //定義一個游戲結束的狀態
status : 0, //這個是目前游戲的狀態,時刻的跟上面兩個狀態做比較,確定游戲處于運行或者結束
start : function(){ //游戲開始時候的方法
// 游戲開始的時候肯定是要把游戲的狀態設置成游戲運行的狀態
// this==game
this.status=this.gamerunning;
// 游戲開始的時候分數清空
this.score=0;
// 數組中的所有元素全部設置成0
this.data=[
[0,0,0,0],
[0,0,0,0],
[0,0,0,0],
[0,0,0,0]
];
this.randomNum();//調用下面自定義的隨機函數,可以在移動和開始的時候隨機出來一個數
this.randomNum();//調用兩次是因為這是游戲開始時的方法,開局隨機出現兩個數和位置,因此需要調用兩次
this.dataView();//調用下面所寫的更新視圖的方法
},
// 隨機數的函數,開始的時候隨機生成,移動的時候隨機生成
randomNum: function(){
while(true){
// 隨機生成行和列 0 - 3隨機整數
var r=Math.floor( Math.random() * 4 ); //隨機生成一個行
var c=Math.floor( Math.random() * 4 ); //隨機生成一個列
if(this.data[r][c]==0){
var num=Math.random() > 0.5 ? 2 : 4;//隨機出現2或4
this.data[r][c]=num;
break;
}
}
},
// 更新試圖的方法
dataView: function(){
// 大的循環,然后把所有的元素全部遍歷一遍
for(var r=0; r < 4; r++){
for(var c=0; c < 4; c++){
// 找到對應的div
var div=document.getElementById("c" + r + c); //字符串拼接
if(this.data[r][c] !=0){
// 數組中對應的內容放到格子上面去
div.innerHTML=this.data[r][c];
// 樣式也寫成對應的
div.className="cell n" + this.data[r][c];
}else{
div.innerHTML="";
div.className="cell"
}
}
}
// 更新分數
document.getElementById("score01").innerHTML=this.score;
//游戲沒有結束的時候 彈出層時刻都是隱藏的
if(this.status==this.gamerunning){
document.getElementById("gameover").style.display="none";
}else{
document.getElementById("gameover").style.display="block";
document.getElementById("score02").innerHTML=this.score;
}
},
// 判斷游戲是否結束的方法
isgameover: function(){
for(var r=0; r < 4; r++){
for(var c=0; c < 4; c++){
if(this.data[r][c]==0){ //里面有空格子的時候,游戲還是可以運行
return false; //表示游戲還沒有結束
}
if(c < 3){//判斷左右是否有相同的
if(this.data[r][c]==this.data[r][c+1]){
return false;
}
}
if(r < 3){
if(this.data[r][c]==this.data[r+1][c]){
return false;
}
}
}
}
return true;
},
//移動的方法,左 右 上 下四個部分
// 左 右 上 下
// 左移的方法
moveLeft: function(){
var before=String(this.data); //之前做一次轉換
// 具體的移動需要處理的邏輯,直接處理好每一行即可
for(var r=0;r < 4;r ++){
this.moveLeftInRow(r);
}
var after=String(this.data); //移動之后再做一次轉換
// 如果說移動之前不等于移動之后,肯定是發生了移動
if(before !=after){
this.randomNum(); //生成隨機數
// 生成的隨機數可能會造成游戲的gameover
if(this.isgameover()){
// 改變游戲的狀態
this.status=this.gameover
}
// 更新視圖
this.dataView();
}
},
moveLeftInRow: function(r){ //只去做處理每一行的邏輯
for(var c=0; c < 3; c++){
var nextc=this.getNextinRow(r,c);
if(nextc !=-1){
if(this.data[r][c]==0){
// 如果等于0,直接替換
this.data[r][c]=this.data[r][nextc];
this.data[r][nextc]=0; //位置恢復成0
c --; //要讓位置恢復到原地
}else if(this.data[r][c]==this.data[r][nextc]){
this.data[r][c] *=2; //位置直接翻一倍
this.data[r][nextc]=0;
this.score +=this.data[r][c]; //更新分數
}
}else{ //沒有找到
break; //直接退出循環
}
}
},
getNextinRow: function(r,c){
for(var i=c + 1; i < 4; i++){
if(this.data[r][i] !=0){
return i; //表示已經找到位置,并且把位置返回出來
}
}
return -1; //返回一個標識符
},
// 右移的方法
moveRight: function(){
var before=String(this.data);
for(var r=0; r < 4; r++){
this.moveRightInRow(r);
}
var after=String(this.data);
if(before !=after){
this.randomNum();
if(this.isgameover()){
this.status=this.gameover;
}
this.dataView();
}
},
moveRightInRow: function(r){
for(var c=4; c > 0; c--){
var prevc=this.getPrevInRow(r,c);
if(prevc !=-1){
if(this.data[r][c]==0){
this.data[r][c]=this.data[r][prevc];
this.data[r][prevc]=0;
c ++
}else if(this.data[r][c]==this.data[r][prevc]){
this.data[r][c] *=2;
this.data[r][prevc]=0;
this.score +=this.data[r][c];
}
}else{
break;
}
}
},
getPrevInRow: function(r,c){
for(var i=c - 1; i >=0; i--){
if(this.data[r][i] !=0){
return i;
}
}
return -1;
},
// 上移
moveUp: function(){
var before=String(this.data);
for(var c=0; c < 4; c++){
this.moveUpInCol(c);
}
var after=String(this.data);
if(before !=after){
this.randomNum();
if(this.isgameover()){
this.status=this.gameover;
}
this.dataView();
}
},
moveUpInCol: function(c){
for(var r=0;r < 4; r++){
var nextr=this.getNextInCol(r,c);
if(nextr !=-1){
if(this.data[r][c]==0){
this.data[r][c]=this.data[nextr][c];
this.data[nextr][c]=0;
r -- ;
}else if(this.data[r][c]==this.data[nextr][c]){
this.data[r][c] *=2;
this.data[nextr][c]=0;
this.score +=this.data[r][c];
}
}else{
break;
}
}
},
getNextInCol: function(r,c){
for(var i=r + 1; i < 4; i++){
if(this.data[i][c] !=0){
return i;
}
}
return -1;
},
// 下移的方法
moveDown: function(){
var before=String(this.data);
for(var c=0;c < 4; c++){
this.moveDownInCol(c);
}
var after=String(this.data);
if(before !=after){
this.randomNum();
if(this.isgameover()){
this.status=this.gameover;
}
this.dataView();
}
},
moveDownInCol: function(c){
for(var r=3; r > 0; r--){
var prev=this.getPrevIncol(r,c);
if(prev !=-1){
if(this.data[r][c]==0){
this.data[r][c]=this.data[prev][c];
this.data[prev][c]=0;
r -- ;
}else if(this.data[r][c]==this.data[prev][c]){
this.data[r][c] *=2;
this.data[prev][c]=0;
this.score +=this.data[r][c];
}
}else{
break;
}
}
},
getPrevIncol: function(r,c){
for(var i=r - 1; i >=0; i--){
if(this.data[i][c] !=0){
return i;
}
}
return -1;
},
}
game.start();
console.log(game.data)
console.log(game.status);
console.log(game.score);
//鍵盤事件
document.onkeydown=function(){
if(event.keyCode==37){
//console.log("左")
game.moveLeft();
}else if(event.keyCode==38){
//console.log("上")
game.moveUp()
}else if(event.keyCode==39){
//console.log("右")
game.moveRight()
}else if(event.keyCode==40){
//console.log("下")
game.moveDown()
}
}
//touch事件
//手指按下
var startX;//設定開始起始位置的x坐標
var startY;//設定開始起始位置的y坐標
var endX;//設定結束滑動位置的x坐標
var endY;//設定結束滑動位置的y坐標
document.addEventListener('touchstart',function(){
// console.log("手指按下了屏幕")
console.log(event);
startX=event.touches[0].pageX;
startY=event.touches[0].pageY;
})
//手指移動
//document.addEventListener('touchmove',function(){
// console.log("手指的移動")
//})
//手指松開
document.addEventListener("touchend",function(){
// console.log("手指松開")
console.log(event);
endX=event.changedTouches[0].pageX;//如何獲取結束時的位置x
endY=event.changedTouches[0].pageY;
var X=endX - startX;
var Y=endY - startY
var absX=Math.abs(X) > Math.abs(Y);
var absY=Math.abs(Y) > Math.abs(X);
if(X > 0 && absX){
console.log("右滑動")
game.moveRight()
}else if(X < 0 && absX){
console.log("左滑動")
game.moveLeft()
}if(Y > 0 && absY){
console.log("下滑動")
game.moveDown()
}if(Y < 0 && absY){
console.log("上滑動")
game.moveUp()
}
})
就這樣一個簡單的2048游戲就完成啦~
非常感謝您能看到這里~
關注我~帶給你更多驚喜~
在介紹 Permission 指令之前,我們先來簡單了解一下自定義指令是什么。
Vue的自定義指令通過Vue.directive方法來創建。
除了上述的鉤子函數外,還可以在指令對象中定義其他屬性和方法:
下面是一個簡單的自定義指令示例:
Vue.directive('my-directive', {
bind(el, binding, vnode) {
// 初始化設置
},
inserted(el, binding, vnode) {
// 元素插入父元素時調用
},
update(el, binding, vnode) {
// 組件更新時調用
},
componentUpdated(el, binding, vnode) {
// 組件及子組件更新后調用
},
unbind(el, binding, vnode) {
// 解綁時調用
}
});
在模板中使用自定義指令:
<div v-my-directive="value"></div>
常用的內置指令有 v-if、v-show、v-bind、v-for等等,大家都用過,也就不在過多贅述了。
先問一個問題
Q: Vue 自定義指令在什么時機執行?
A: 是在運行時解析并執行的
從內置到自定義指令中間到底發生了什么?
可能是由于之前沒有深入了解,一直認為內置指令和自定義指令都在編譯時解析。
從下圖中可以編譯時內置指令在到 Render 時已經執行完成,然而到了運行時之后才開始自定義指令的解析工作。
常常碰到的面試題,“v-if 和 v-show有什么區別”,區別可不是簡簡單單的一個前者是直接刪除DOM,后者是改變display屬性。
v-if 在生成 VNode 前就已經在模板編譯階段進行了判斷,而 v-show 也是在編譯時解析,只不過在 v-if 在編譯時就確定了渲染元素,而v-show在運行時根據條件進行顯示和隱藏。
如下摘抄 《深入淺出Vue.js》中 15.1.1 中 v-if 指令的原理概述 的部分代碼
v-if
<!-- 模板 -->
<li v-if="has">if</li>
<li v-else>else</li>
// 編譯后
(has)
? _c('li',[_v("if")])
: _c('li',[_v("else")])
當 has 為 true,就會將第一個 li 元素創建 VNode。
v-show
我們再看看 v-show, 很明顯是一個指令,與我們在render 函數中寫指令是一樣的,但是這樣來看 v-if 肯定是一個語法糖,因為它并不是真正意義上的指令。
也就是說自定義指令時機不可能在內置指令之前解析(當然,如果你要是通過 vue-template-compiler 直接修改編譯時或者其他騷操作,就當俺沒說)。
感興趣的朋友可以去 vue-template-explorer.netlify.app/ 上試試
在真正了解了內置指令的執行時機,之后我們接下來寫一個自定義的指令
在實現 Permission 指令之前,我們需要先了解一下常見的權限顆粒度。下面是一些常見的方案。
先寫一個簡單的顯示隱藏控制,可以通過響應式數據控制。
// 全局自定義指令
Vue.directive("permission", {
// 在元素被插入到 DOM 中時觸發
inserted(el, binding) {
// 如果綁定值為 false,則從父節點中移除元素
if (!binding.value) {
el.parentNode.removeChild(el); // 移除元素
}
},
// 在元素更新時觸發
update(el, binding) {
// 如果綁定值為 true
if (binding.value) {
// 如果元素沒有父節點(即之前被移除了)
if (!el.parentNode) {
// 將元素插入到原來的位置
el.__v_originalParent.insertBefore(el, el.__v_anchor || null);
}
} else {
// 如果元素有父節點
if (el.parentNode) {
// 創建一個注釋節點作為替換元素的占位符
el.__v_anchor=document.createComment("");
el.__v_originalParent=el.parentNode;
// 用注釋節點替換原來的元素
el.parentNode.replaceChild(el.__v_anchor, el);
}
}
},
});
在 inserted 鉤子中,當元素被插入到 DOM 中時,根據綁定值的狀態,如果為 false,則從父節點中移除該元素,實現隱藏的效果。
在 update 鉤子中,當元素更新時,根據綁定值的狀態,如果為 true,并且元素之前被移除了(沒有父節點),則將元素插入到原來的位置,實現顯示的效果。如果綁定值為 false,并且元素當前有父節點,則創建一個注釋節點作為替換元素的占位符,并將注釋節點替換掉原來的元素,實現隱藏的效果。
使用方式很簡單,在指令中傳入需要的值即可。
<template>
<div id="app">
isVisible: {{ isVisible }}
<button v-permission="isVisible">自定義指令</button>
<button @click="isVisible=!isVisible">Toggle</button>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
isVisible: true,
};
},
};
</script>
可以看到效果基本上與 v-if 一致,當然這個是運行時的操作,在原理上來說還是有區別的。
但是如果只是一個簡單的變量的話,我們的這個 v-permission 沒有太大意義。真的可以直接通過 v-if 控制即可,因為我們需要異步請求,并且實現統一管理。接下來會加強一下這個指令。
在常見中后臺中表格頁出現4個以上的按鈕,如下圖,有新增、刪除、編輯、查詢。從業務中來,又從業務中走出去。
一般需要在某個模塊、功能又或者某個路由下的add|delete|update|query進行權限控制,又或者其他的一些操作。
所以我們一個按鈕權限應該是 頁面::新增,接下來就需要一個checkPermission 方法來提供支撐。
假設現在在某個API中能夠拿到所有按鈕級別的權限,并且與服務端同學約定的格式一致。
{
"orders": [
"add",
"update",
"delete",
"query",
"detail",
"enable",
"disable"
]
}
如上JSON,orders 作為訂單頁面,而對應能夠操作 Actions 放在數組內,若不存在則認定為沒有此權限。
消費方傳入orders::update 則代表需要獲取到訂單的編輯權限
<button v-permission="'orders::update'">Update</button>
在指令區域,我們需要將指令的鉤子(這里的示例只在inserted中調整了)改為異步等待,剛進入 inserted鉤子時需要將 display 設置為 none,或者在 bind 鉤子里面去做這個事情,不然會出現一閃而過的效果。
然后在這里面去調用checkPermission傳入 binding.value 也就是 orders::update 然后在這里等待 checkPermission 的返回值然后判斷是否需要刪除此元素。
Vue.directive("permission", {
async inserted(el, binding) {
// 默認先改為 none,等數據回來之后再進行操作,你通過class來控制
el.style.display="none";
const hasPermission=await checkPermission(binding.value);
el.style.display="";
if (!hasPermission) {
el.parentNode?.removeChild(el); // 移除元素
}
},
...
});
接收 orders::update,通過splitPermissionString分割成orders和update。
然后再去發送請求,但是如果一個頁面有多處同時調用v-permission,可能會存在重復請求的問題。
所以通過 controller.hasRequested 和 controller.task 來控制請求的重復性。這個是有必要的,目前JavaScript中沒發現類似與Java中的wait/notify(鎖機制)。所以只是一個仿造Java的鎖機制
const controller={
// 是否發過請求
hasRequested: false,
// 權限集合
permissionList: [],
// 真正的請求任務
task: null,
};
const checkPermission=async (value=null)=> {
// 截取對應的模塊和操作
const [module=null, operate=null]=splitPermissionString(value) ?? [];
// 判斷模塊和操作是否存在
if (!module || !operate) return false;
// 判斷是否發送過請求
if (!controller.hasRequested) {
controller.hasRequested=true;
controller.task=getPermissionsApi();
}
// 獲取權限數據,進行賦值
controller.permissionList=await controller.task ?? [];
// 判斷是否有權限
return controller.permissionList[module]?.includes(operate) ?? false;
};
可以看到在發送請求前默認先隱藏,而后等待接口數據回來,重新判定哪些沒有權限,然后控制顯示或者刪除元素節點。
// 模擬請求
const getPermissionsApi=async ()=>
fetch(`/api.json?t=${new Date().getTime()}`).then((res)=> res.json());
// 分割字符串
const splitPermissionString=(str)=> {
try {
if (typeof str==="string" && str.includes("::")) {
const [firstPart, secondPart]=str.split("::");
return [firstPart.trim(), secondPart.trim()];
} else {
throw new Error("Invalid permission string or delimiter not found");
}
} catch (error) {
console.error(error.message);
return [];
}
};
作者:嚴老濕
鏈接:https://juejin.cn/post/7259356504758779965
*請認真填寫需求信息,我們會在24小時內與您取得聯系。