最近在学校闲着也是闲着,打算复习一下react,想写点什么东西,最后决定写一个跳棋打发闲暇的时光。最后按照本身设想的写完了,因为是基于create-react-app的架子,不能放在codepen上有一点遗憾,不过本文最后给了线上地址和github地址,你们感兴趣能够看看,欢迎批评指正。css
咱们把跳棋这个项目先拆分为如下步骤前端
咱们仔细观察棋盘, 首先棋盘是由6个等边三角形(棋子)和中间一个正六边形(空闲的棋盘)组成。这里就教你们怎么画出这6个等边三角形吧, 先给个示意图吧。react
在画这些棋子以前咱们先作出以下思考,首先这6个三角形是对称的,便可以经过绕某一点旋转获得,其次任意两个棋子的距离是相同的。jquery
即须要画出 AEI 和 CMG 这两个等边三角形。git
这一步能够用border实现,这也是比较常规的方法,而后CMG就是AEI旋转180deg获得的图形。这里要注意一下,旋转的中心点是O点,你们要设置好transform-origin.github
固然最最重要的一点,棋盘是要适配的,即它的宽度不能写死,咱们把它写成一个变量最好了,为了你们看的清楚,我截取一段scss给你们看看。算法
$width: 250px;
$height: $width * sqrt(3);
$rotateY: round(($width * 2 * 2 / 3 ) * sqrt(3) / 2);
$containerX: 2 * $width;
$containerY: 2 * $rotateY;
$radius: getGap($width, 0.4) / 2; //0.4 是gap 和 直径的比
$gap: 2 * 0.4 * $radius;
复制代码
这里width和rotateY分别指示意图中加粗黑框宽的1/2和,高的1/2。 黑框的宽高分别为上述的containerX,containerY。radius指小球的半径,gap指棋子之间的间距。这里全部的属性只依赖于变量width,方便棋盘的放大和缩小,咱们能够写下以下式子。redux
咱们首先画出角BAN上的10个棋子,咱们从上往下画,一共四层,每一层为当前层数个棋子。咱们把AE上的棋子作为每一层的起始点。数组
width 黑色容器的宽 也为三角形边长 = A E
而三角形的每条边上平均放置了12个棋子,即棋子间距为 width / 12
第一层 chess-0-0 起始点(width/2, 0)
第二层 chess-0-0 起始点(width/2 - 棋子间距/2, 棋子间距 * Math.sqrt(3)/2)
chess-0-1 (chess-0-0.x + gap, chess-0-0.y)
...
@for $i from 0 to 4{
@for $j from 0 to ($i+1){
left: $width - $width * 2 / (4 * 3 * 2) * $i + $j * $width * 2 / (4 * 3);
top: $i * $gap + 2 * $radius * $i) * sqrt(3) / 2
}
}
复制代码
这时候棋子单边的棋子就出来了,但是咱们须要6边的棋子呀,难道咱们要一边一边画吗? 答案确定是No NO No啊!浏览器
好,咱们如今按照咱们以前的思路把角依次BAN旋转60deg。首先咱们有几个注意点:
咱们在绘制棋子的时候left为棋子的左上角,这个左上角并非棋盘的顶点,咱们须要经过css(transform: translate(-50% -50%))将球的左上角的点移至棋盘上。
咱们棋子的父标签是那个黑色的container,而咱们旋转的中心点是上图中的O点。
咱们来推导一些公式 (点的旋转公式)
A 点坐标 (x1,y1) 与 x 轴夹角为 b
B 点坐标 (x2, y2) 与 AO 夹角为 c
这里换算成极坐标
则 x1 = rcosb y1 = rsinb
x2 = rcos(b+c) = rcosbcosc - rsinbsinc = x1cosc - y1sinc
y2 = rsin(b+c) = rsinbcosc + rcoscsinb = x1sinc + y1cosc
复制代码
可是咱们的中心点默认是容器的左上角,不是容器的中心点呀。容易,咱们坐标平移一下就行了。
x2 = (x - w)cosc - (y - h)sinc
y2 = (x - w)sinc + (y - h)cisc
这时候的x2,y2 是相对于O中心点旋转后的坐标, 咱们再返到以前的坐标系中。
x2 = (x - w)cosc - (y - h)sinc + w
y2 = (x - w)sinc + (y - h)cisc + h
复制代码
没错,就是这样,咱们如今对BAN旋转吧,贴上scss的代码(话说三层循环真是有一点麻烦呢!)
@for $k from 0 to 6{
@for $i from 0 to 4{
@for $j from 0 to ($i+1){
.chess-#{$k}-#{$i}-#{$j}{
left: cos(60deg * $k) * ($width - $width * 2 / (4 * 3 * 2) * $i + $j * $width * 2 / (4 * 3) - $width) - sin(60deg * $k) * (($i * $gap + 2 * $radius * $i) * sqrt(3) / 2 - $rotateY) + $width;
top: sin(60deg * $k) * ($width - $width * 2 / (4 * 3 * 2) * $i + $j * $width * 2 / (4 * 3) - $width) + cos(60deg * $k) * (($i * $gap + 2 * $radius * $i) * sqrt(3) / 2 - $rotateY) + $rotateY;
}
}
}
}
复制代码
最后棋盘就是下面这样了(掘金不支持iframe 你们戳开连接看codepen吧)!!! 是否是颇有趣呢 :)
See the Pen chessBoard by shadowwalkerzero ( @shadowwalkerzero) on CodePen.咱们如今须要画出棋盘上的点,即棋子能够放的点。拆分一下棋盘,棋盘是由中心的正六边形和那6个角组成,正6边形按照咱们以前的方法绘制是否是很简单呢? 就是把三角形上的点绘出来,而后旋转6次就行了。这里就不赘述了。
由于棋子都是绝对定位的,咱们要计算下一跳的点,必然要计算出它的精确坐标呀。但是我该怎么表示这些点呢?拿二维坐标吗?固然能够了,毕竟是2d,可是这样就太笨了,太笨了!
咱们须要观察一下棋盘,其实棋子能够跳的点最终能够表现为6边形,画个示意图吧。
因此咱们须要把跳棋上的点表示成3元组。例如正六边形斜上方的点就该表示成chess-1-2-2 单位是当前轴上两个点的距离。
这里干脆也把给棋子编号的方法也告诉你们吧。其实也很简单,就是利用点到直线间距离公式( d = Math.abs(AX + BY + C) / Math.sqrt(A^2+B^2); )
咱们对一个点分别向3条轴计算三次距离,距离同样的就在一条线上。
看一下编号结束后的棋盘吧。
这里咱们须要明确一下跳棋的规则,跳棋是既能够向周围滚一步,也能够隔着棋子跳的。 为了标示棋盘该点已被占用,咱们须要引入一个属性isOccupy来标示。这里给出棋盘上的点的数据结构。
{
key: `,
isChess: ,
locate: '',
style: {
background: ,
left: ,
top:,
zIndex: 2,
transform:
}
复制代码
这里解释一下各个属性 isChess 用来区分棋盘上的点和棋子,locate表示棋子或棋盘上的点的编号。style标示棋子或棋盘上的点坐标,还有一些辅助属性,好比当前要走的棋子会显得大一点。既然咱们已经获取到了关于棋子和棋盘上的全部信息,下一步就是要让棋子跳起来了。
咱们再画一个简单的示意图
X 0 (0) X 0
咱们以 0 表示棋子, X表示棋盘上的空点。(0) 表示正要跳的棋子。
显然流程异常的简单:
1. 从当前(0) 位置分别向左,右搜寻,直至找到左边和右边的距离最近0(注意咱们是三条轴,分别向三条轴搜寻)。
2. 以刚找到的点为基点,当正要跳的棋子和找到的点距离为长度,找出对称的点,即棋子的 落点。
3. 将上一步的落点作为当前点。
回到第一步
复制代码
稍微分析一下 会发现是很简单的递归,发现从当前点向左右搜寻找点,真是和二叉树如出一辙,问题就转变为二叉树的遍历上了。
当让遍历方法很是多,深度优先算法和广度优先算法均可以,可是做者这里推荐广度优先算法,由于广度优先算法调试更方便,层数浅,我也是基于广度优先算法实现的。
咱们这里简单缕一下广度优先算法的思路,写一下伪码。
思路是 一个队列 path: []
压入左右搜寻的点 path.push[A.left, A.right]
压出left path: [A.right]
压入A.left 左右搜寻的点 path: [A.right, A.left.left, A.left.right]
//itemMove 指当前要跳的棋子
//position 放置了棋盘上的点和棋子
// 广度优先队列
//passNode 收集棋子的落点
calculatePath = (itemMove, position, allPath, passNode) => {
let path = getValidPoint(itemMove) //获取三条轴上的落点
allPath.push(...path);
if(allPath.length > 1){
let nextJump = allPath[0];
allPath.splice(0, 1);
passNode.push(nextJump); //这就是下一跳了
}
return nextJump ? calculatePath(nextJump, position, allPath, passNode) : passNode;
}
复制代码
固然这里有一个小问题,即成环的问题,你跳过去,下一跳又给你跳回来,就会死循环。这个问题解决的方法也不少,把走过的路径节点都标示一下,参照上面的伪码,全部的路径节点都在pressNode下,只要这个节点走过了,就不容许再走一遍。
为了更好的交互,让跳棋跳起来是必须的!咱们先捋捋咱们现有的数据
而后咱们的问题: 就是当用户点击任意一个落点时,要让跳棋一级一级的跳过去。
为了把路径肯定出来,咱们必须把这些过渡点链接起来,当用户点击任意一个落点时候,咱们须要计算从起跳点和落点的距离。
1.来把这些落点链接起来吧
在肯定跳棋的落点的时候,咱们检索出了一个棋子的全部落点。为了避免让这些数据丢失, 咱们能够用记录一下。
nextJump.parent = startJump
复制代码
nextJump 就是startJump 的全部落点,咱们用parent来保存它们的联系。如今咱们就要把它们串起来了,先从简单的例子出发吧。在设置为parent后,咱们大概获得了一组相似这样的数据。
let points = [{
name: 'A',
parents: ['C']
},{
name: 'C',
parents: ['D']
}, {
name: 'D',
parents: ['E']
}, {
name: 'E',
parents: ['L', 'F']
}, {
name: 'F',
parents: ['C']
},
{
name: 'L',
parents: []
}]
复制代码
上面的数据对应的示意图以下,大体为一个联通图。
假设咱们从起点A出发要到终点L,求出A - L 的路径。常规方法就是深度优先了。咱们简单描述一下流程(主要注意成环的问题)。
1.路径队列 [L] 当前节点 L
2.得到 L 的 parent [E]
3.E进栈 [L,E]
4.E. 作为下一个节点,要是 E 没有 parent 或者成环 E 出栈
重复1 直至找到A
附上代码
let flag = false;
function scanPath(start, end, path) {
let nextLists = getParents(start); //获取节点的parent
let nextJump = false;
for (let i = 0; i < nextLists.length; i++) {
nextJump = nextLists[i];
if (path.indexOf(nextJump) < 0) {
!flag && path.push(nextJump);
if (nextJump === end) {
flag = true;
}
!flag && scanPath(nextJump, end, path);
}
}
!flag && path.pop();
return path;
}
复制代码
这里咱们就把起始点和落点的路径找出来了,如今就要让棋子作动画了。
2.棋子跳吧(做者没有很好的解决)
咱们描述一下咱们上一步得到的路径,大体为 ['11-2-4', '6-8-13', '14-8-9', '9-3-8']。这里的元素对应上述咱们对棋盘编号的三元组。 表示 棋子要从 11-2-4 -> 6-8-13 -> 14-8-9 -> 9-3-8 一路跳过去。
彷佛实现也不难,在咱们刚学前端的时候,不借助react也能够作到,对dom作tiansition动画,而后监听onTransitionEnd事件,在这里面继续作下一步动画,本身也试着用这种最土的方法作。只是在react中一切都是state了。
好比当前节点要跳 4跳,咱们拿到路径数组 ['11-2-4', '6-8-13', '14-8-9', '9-3-8'] 起跳点 11-2-4 咱们 找到11-2-4的棋子 把它的style 设置成 数组的[index] 就行了,我这里的解决方案是。
styles.map((item, index) => {
setTimeout(() => {
this.setState({
nowStyle: styles[index]
})
}, 600 * (index + 1));
});
复制代码
styles 就是路径数组里路径节点的style,主要是left,top。nowStyle 就是起跳的棋子要不断应用的style。放一张本身的测试图,时间为600ms的缘由是由于transition的时间是 500ms,总要先让动画作完把。
可是这里我并不认为这个方案可行,react的diff render时间 还有不一样浏览器性能的时间都不可控,settimeout真是下下策。
中间也求助过一些很优秀的react动画库,好比react-motion。发现它能作一组动画的只有StaggeredMotion,可是在文档中,做者写明了:
(No onRest for StaggeredMotion because we haven't found a good semantics for it yet. Voice your support in the issues section.)
就是对组动画不提供回调,也就是说咱们无法监听这组动画里的某一个动画,真是遗憾。
因为做者并不以为这个解决方案很好,因此没有放在应用在项目的线上中,可是放在github目录下,感兴趣的同窗能够提供本身的解决方案。
好比怎么判断输赢
这个问题咱们能够在初始化棋盘就解决掉,好比假设如今执棋方是绿色,那么它的目的地是粉色,一开始的时候就把各个执棋方的目的地的位置计算好,每走一步,就check一下。
好比怎么作到棋手轮流下
这个咱们须要一个状态位控制,表示当前棋手,下完一步,加1对全部选手取余就行了。
如下为本人我的观点,不保证正确。
react作这种须要必定计算的网页,最让我担忧的是性能,每走一步就涉及到多个状态,好比isOccupy 占位,下一跳的坐标。要是setstate({}) 确定不行,由于这是异步的,会批量处理。因此只能setstate((prevState, prevProps) => {}),这样大量的diff,对性能确定是个挑战。这里做者是没有实时更新数据的,计算完一次更新,可是这样就不方便state 调试,并且redux写多了,数据一旦不更新,内心就很慌。
react 因为数据驱动,确实代码更加简洁,可是相比以前写的原生动画,状态太多,全部的状态都挤在state里,逻辑会很的很混乱(也有多是本身水平有限)
我以为react并不适应动画场景,咱们知道jquery 的animate自己也是基于setInterval实现的,而react 自己框架极其复杂,咱们很难把控时间(也是本身水平有限)。