d3.js 制作简单的俄罗斯方块
d3.js是一个不错的可视化框架,同时对于操作dom也是十分方便的。今天我们使用d3.js配合es6的类来制作一个童年小游戏–俄罗斯方块。话不多说先上图片。
1. js tetris类
由于方法拆分的比较细所以加上了一些备注(这不是我的风格!)
const graphMap = [ { name: '倒梯形', position: [[0,4],[1,3],[1,4],[1,5]], rotate: [[[0,0],[-2,0],[-1,-1],[0,-2]],[[1,0],[1,2],[0,1],[-1,0]],[[-1,0],[1,0],[0,1],[-1,2]],[[0,0],[0,-2],[1,-1],[2,0]]], color: '#D7DF01' }, { name: '一字型', position: [[0,3],[0,4],[0,5],[0,6]], rotate: [[[-1,1],[0,0],[1,-1],[2,-2]],[[1,2],[0,1],[-1,0],[-2,-1]],[[2,-2],[1,-1],[0,0],[-1,1]],[[-2,-1],[-1,0],[0,1],[1,2]]], color: '#0000FF' }, { name: '正方形', position: [[0,4],[0,5],[1,4],[1,5]], rotate: [[[0,0],[0,0],[0,0],[0,0]],[[0,0],[0,0],[0,0],[0,0]],[[0,0],[0,0],[0,0],[0,0]],[[0,0],[0,0],[0,0],[0,0]]], color: '#FF0000' }, { name: 'Z字型', position: [[0,3],[0,4],[1,4],[1,5]], rotate: [[[0,1],[1,0],[0,-1],[1,-2]],[[1,1],[0,0],[-1,1],[-2,0]],[[1,-2],[0,-1],[1,0],[0,1]],[[-2,0],[-1,1],[0,0],[1,1]]], color: '#800080' }, { name: '反Z字型', position: [[1,3],[1,4],[0,4],[0,5]], rotate: [[[-1,1],[0,0],[1,1],[2,0]],[[0,1],[-1,0],[0,-1],[-1,-2]],[[2,0],[1,1],[0,0],[-1,1]],[[-1,-2],[0,-1],[-1,0],[0,1]]], color: '#FFA500' }, { name: 'L字型', position: [[1,3],[0,3],[0,4],[0,5]], rotate: [[[-1,1],[0,2],[1,1],[2,0]],[[0,1],[1,0],[0,-1],[-1,-2]],[[1,-1],[0,-2],[-1,-1],[-2,0]],[[0,-1],[-1,0],[0,1],[1,2]]], color: '#90EE90' }, { name: '反L字型', position: [[0,3],[0,4],[0,5],[1,5]], rotate: [[[-1,2],[0,1],[1,0],[0,-1]],[[2,0],[1,-1],[0,-2],[-1,-1]],[[1,-2],[0,-1],[-1,0],[0,1]],[[-2,0],[-1,1],[0,2],[1,1]]], color: '#AEEBFF' } ] class Tetris { constructor() { this._grid = []; this._rows = 18; this._cols = 10; this._div = 33; this._nextDiv = 15; this._duration = 1000; this._width = this._div * this._cols; this._height = this._div * this._rows; this._svg = null; this._nextSvg = null; this._timeout = null; this._time = null; this._showGrid = false; this._haveArray = []; this._curtArray = []; this._colors = ''; this._rotateIndex = 0; this._rotateArray = []; this._fixedColor = '#666'; this._nextNumber = 0; this._graphMap = graphMap; this._level = 1; this._levelLimit = [0,20,50,90,140,200,270,350,440,540,650,770,900,1040,1190,1350,1520]; this._score = 0; this._time = 0; this.initSvg(); this.initNextSvg(); this.addKeyListener(); } initSvg() { this._svg = d3.select('.svg-container') .append('svg') .attr('width', this._width) .attr('height', this._height) .attr('transform', 'translate(0, 4)') } initNextSvg() { this._nextSvg = d3.select('.next') .append('svg') .attr('width', 100) .attr('height', 60) } toggleGrid() { if(this._showGrid) { this._showGrid = false; d3.select('g.grid').remove(); } else { this._showGrid = true; this._grid = this._svg.append('g') .attr('class', 'grid') this._grid.selectAll('line.row') .data(d3.range(this._rows)) .enter() .append('line') .attr('class', 'row') .attr('x1', 0) .attr('y1', d => d * this._div) .attr('x2', this._width) .attr('y2', d => d * this._div) this._grid.selectAll('line.col') .data(d3.range(this._cols)) .enter() .append('line') .attr('class', 'col') .attr('x1', d => d * this._div) .attr('y1', 0) .attr('x2', d => d * this._div) .attr('y2', this._height) } } addKeyListener() { d3.select('body').on('keydown', () => { switch (d3.event.keyCode) { case 37: this.goLeft(); break; case 38: this.rotate(); break; case 39: this.goRight(); break; case 40: this.goDown(); break; case 32: console.log('空格'); break; case 80: console.log('暂停'); break; default: break; } }) } //设置运动图形 如果仍有掉落空间则继续掉落 反之调用setHaveArray initGraph() { this.renderGraph(); this._timeout = setTimeout(() => { if(this.canDown()) { this.downArray(); this.initGraph(); } else { this.setHaveArray(); if(!this.gameOver()) { this.randomData(); this.nextGraphNumber(); this.initGraph(); } else { clearTimeout(this._time); d3.select('#modal').style('top', '0px') } } }, this._duration * (1 - ((this._level - 1) / this._levelLimit.length) / 2)) } //渲染图形 renderGraph() { this._svg.selectAll('rect.active').remove(); this._svg.selectAll('rect.active') .data(this._curtArray) .enter() .append('rect') .attr('class', 'active') .attr('x', d => this._div * d[1] + 1) .attr('y', d => this._div * d[0] + 1) .attr('width', this._div - 3) .attr('height', this._div - 3) .attr('stroke', this._color) .attr('stroke-width', 2) .attr('fill', this._color) .attr('fill-opacity', 0.5) } //设置掉落后的数组,并清除运动的图形 重置状态 setHaveArray() { this._curtArray.forEach(d => this._haveArray.push(d)); this._svg.selectAll('rect.active').attr('class', 'fixed').attr('fill', this._fixedColor).attr('fill-opacity', 0.5).attr('stroke', this._fixedColor); this._rotateIndex = 0; this.clearLines(); } //检测有满列 然后消除 clearLines() { let clearLinesArr = []; let allRowsObj = {}; let temp = []; let arr = this._haveArray.map(d => d[0]); arr.forEach(d => { if(allRowsObj.hasOwnProperty(d)) { allRowsObj[d] ++ } else { allRowsObj[d] = 1; } }) for(var i in allRowsObj) { if(allRowsObj[i] == this._cols) { clearLinesArr.push(i) } } if(clearLinesArr.length != 0) { this.setScoreAndLevel(clearLinesArr.length); this._haveArray = this._haveArray.filter(a => !clearLinesArr.some(b => b == a[0])); this._haveArray = this._haveArray.map(d => [this.downSome(d[0],clearLinesArr), d[1]]) this._svg.selectAll('rect.fixed').remove(); this._svg.selectAll('rect.fixed').data(this._haveArray) .enter() .append('rect') .attr('class', 'fixed') .attr('x', d => this._div * d[1] + 1) .attr('y', d => this._div * d[0] + 1) .attr('width', this._div - 3) .attr('height', this._div - 3) .attr('stroke', this._fixedColor) .attr('stroke-width', 2) .attr('fill', this._fixedColor) .attr('fill-opacity', 0.5) } } //消除时 判断下落层数 downSome(c, arr) { let num = 0; arr.forEach(d => { if(c < d) { num ++; } }) return num + c; } //设置等级和分数 setScoreAndLevel(num) { switch(num) { case 1: this._score = this._score + 1; break; case 2: this._score = this._score + 3; break; case 3: this._score = this._score + 6; break; case 4: this._score = this._score + 10; default: break; } for(var i=0; i<this._levelLimit.length; i++) { if(this._score <= this._levelLimit[i]) { this._level = i + 1; break; } } d3.select('#score').html(this._score); d3.select('#level').html(this._level); } //左移动 goLeft() { if(this.canLeft()) { this.leftArray(); this.renderGraph(); } } //右移动 goRight() { if(this.canRight()) { this.rightArray(); this.renderGraph(); } } //旋转 rotate() { if(this.canRotate()) { this.rotateArray(); this.renderGraph(); } } //下移动 goDown() { if(this.canDown()) { this.downArray(); this.renderGraph(); } } //下落更新数组 downArray() { this._curtArray = this._curtArray.map(d => { return [d[0] + 1, d[1]] }) } //左移更新数组 leftArray() { this._curtArray = this._curtArray.map(d => { return [d[0], d[1] - 1] }) } //右移更新数组 rightArray() { this._curtArray = this._curtArray.map(d => { return [d[0], d[1] + 1] }) } //旋转更新数组 rotateArray() { let arr = this._rotateArray[this._rotateIndex]; this._curtArray = this._curtArray.map((d,i) => { return [d[0] + arr[i][0], d[1] + arr[i][1]] }) this._rotateIndex = (this._rotateIndex + 1) % 4; } //判断是否可以下落 canDown() { let max = 0; let status = true; let nextArr = this._curtArray.map(d => { if(d[0] + 1 > max) { max = d[0]; } return [d[0] + 1, d[1]] }); nextArr.forEach(d => { this._haveArray.forEach(item => { if(item[0] == d[0] && item[1] == d[1]) { status = false; } }) }) if(!status || max > 16) { return false; } else { return true; } } //判断是否可以左移 canLeft() { let min = this._cols; let status = true; let nextArr = this._curtArray.map(d => { if(d[1] - 1 < min) { min = d[1]; } return [d[0], d[1] - 1] }) nextArr.forEach(d => { this._haveArray.forEach(item => { if(item[0] == d[0] && item[1] == d[1]) { status = false; } }) }) if(!status || min <= 0) { return false; } else { return true; } } //判断是否可以右移 canRight() { let max = 0; let status = true; let nextArr = this._curtArray.map(d => { if(d[1] + 1 > max) { max = d[1]; } return [d[0], d[1] + 1] }) nextArr.forEach(d => { this._haveArray.forEach(item => { if(item[0] == d[0] && item[1] == d[1]) { status = false; } }) }) if(!status || max > this._cols - 2) { return false; } else { return true; } } //判断可以变形 canRotate() { let max = 0; let min = this._cols; let status = true; let arr = this._rotateArray[this._rotateIndex]; let nextArr = this._curtArray.map((d,i) => { if(d[1] + 1 > max) { max = d[1]; } if(d[1] - 1 < min) { min = d[1]; } return [d[0] + arr[i][0], d[1] + arr[i][1]] }) if(!status || max > this._cols - 1 || min < 0) { return false; } else { return true; } } //判断游戏结束 gameOver() { let status = false; this._haveArray.forEach(d => { if((d[0] == 0 && d[1] == 3) || (d[0] == 0 && d[1] == 4) || (d[0] == 0 && d[1] == 5) || (d[0] == 0 && d[1] == 6)) { status = true; } }) return status; } //随机生成图形块 randomData() { this._curtArray = this._graphMap[this._nextNumber].position; this._color = this._graphMap[this._nextNumber].color; this._rotateArray = this._graphMap[this._nextNumber].rotate; } //预设下一个图形展示 nextGraphNumber() { let rand = [0,0,1,1,2,2,3,4,5,6]; this._nextNumber = rand[Math.floor(Math.random() * 10000) % 10]; this._nextSvg.selectAll('rect.ne').remove(); this._nextSvg.selectAll('rect.ne') .data(this._graphMap[this._nextNumber].position) .enter() .append('rect') .attr('class', 'ne') .attr('x', d => this._nextDiv * (d[1] - 1) + 1) .attr('y', d => this._nextDiv * (d[0] + 1) + 1) .attr('width', this._nextDiv - 3) .attr('height', this._nextDiv - 3) .attr('stroke', this._graphMap[this._nextNumber].color) .attr('stroke-width', 2) .attr('fill', this._graphMap[this._nextNumber].color) .attr('fill-opacity', 0.5) } //初始化数据 initData() { this._haveArray = []; this._level = 1; this._score = 0; this._time = 0; this._svg.selectAll('rect').remove(); d3.select('#score').html(0); d3.select('#level').html(1); d3.select('#time').html(0); } //开始时间 initTime() { this._time = setInterval(() => { this._time ++; d3.select('#time').html(this._time); },1000) } //开始游戏 startGame() { this.initData(); this.randomData(); this.nextGraphNumber(); this.initGraph(); this.initTime(); } }
2. css 代码
* { padding: 0; margin: 0; } body { width: 480px; margin: 30px auto; } .svg-container { overflow: hidden; border: 5px solid rgba(0,0,0,0.2); width: 330px; position: relative; float: left; } #modal { position: absolute; top: 0px; background-color: white; border-bottom: 5px solid rgb(202,202,202); padding: 20px; width: 310px; text-align: center; z-index: 100; transition: 200ms linear; } #newGame { text-decoration: none; color: gray; font-size: 25px; cursor: pointer; } aside { position: relative; float: right; } aside .next { width: 100px; height: 60px; padding: 10px; border: 5px solid rgba(0,0,0,0.2); border-radius: 2px; margin-bottom: 10px; } aside .score { width: 100px; padding: 10px; border: 5px solid rgba(0,0,0,0.2); border-radius: 2px; color: gray; } aside .pause { color: gray; font-size: 12px; font-style: italic; padding-left: 3px; margin-top: 15px; } .row { stroke: lightgray; } .col { stroke: lightgray; }
3. html代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>$Title$</title> <link rel="stylesheet" type="text/css" href="css/base.css"/> <script type="text/javascript" src="js/d3.v4.js"></script> <script type="text/javascript" src="js/base.js"></script> </head> <body> <div class="container"> <div class="svg-container"> <div id="modal" class="active"> <span id="newGame" onclick="newGame()">New Game</span> </div> </div> <aside> <div class="next"></div> <div class="score"> <table> <tr> <td>Level:</td> <td id="level"></td> </tr> <tr> <td>Score:</td> <td id="score"></td> </tr> <tr> <td>Time:</td> <td id="time"></td> </tr> </table> </div> <div class="pause"> <input type="checkbox" onclick="toggleGrid()"/> 网格 </div> </aside> </div> <script> var tetris = new Tetris(); function toggleGrid() { tetris.toggleGrid() } function newGame() { document.getElementById('modal').style.top = '-100px'; tetris.startGame() } </script> </body> </html>
想预览或者下载demo的人请移步至原文