其他
烧脑!JS+Canvas 带你体验「偶消奇不消」的智商挑战
(给前端大全加星标,提升前端技能)
作者:huangjianke
https://segmentfault.com/a/1190000020268623
启逻辑之高妙,因想象而自由
Talk is cheap,Show me the code.
js
+canvas
实现,没有使用任何游戏引擎,对于初学者来说,也比较容易入门。如何解决 Canvas 绘图模糊? 如何绘制任意多边形图形? 1 + 1 = 0,「偶消奇不消」的效果如何实现? 如何判断一个点是否在任意多边形内部 ? 如何判断游戏结果是否正确? 排行榜的展示 游戏性能优化
canvas 绘图时,会从两个物理像素的中间位置开始绘制并向两边扩散 0.5 个物理像素。 当设备像素比为 1 时,一个 1px 的线条实际上占据了两个物理像素(每个像素实际上只占一半),由于不存在 0.5 个像素,所以这两个像素本来不应该被绘制的部分也被绘制了,于是 1 物理像素的线条变成了 2 物理像素,视觉上就造成了模糊。
let ctx = canvas.getContext('2d')
canvas.width = screenWidth * ratio
canvas.height = screenHeight * ratio
ctx.fillStyle = 'black'
ctx.font = `${18 * ratio}px Arial`
ctx.fillText('我是清晰的文字', x * ratio, y * ratio)
ctx.fillStyle = 'red'
ctx.fillRect(x * ratio, y * ratio, width * ratio, height * ratio)
wx.getSystemInfoSync().pixelRatio
获取设备的像素比ratio
。Canvas
的宽度和高度按照所获取的像素比ratio
进行放大,在绘制文字、图片的时候,坐标点 x
、y
和所要绘制图形的 width
、height
均需要按照像素比 ratio
进行缩放。canvas
是由 weapp-adapter 预先调用 wx.createCanvas()
创建一个上屏 Canvas
,并暴露为一个全局变量 canvas。任意一个多边形图形,是由多个平面坐标点所组成的图形区域。
{x: 0, y: 0}
,一个多边形包含多个单位长度的平面坐标点。[{ x: 1, y: 3 }, { x: 5, y: 3 }, { x: 3, y: 5 }]
表示为一个三角形的区域,需要注意的是,x
、y
并不是真实的平面坐标值,而是通过屏幕宽度计算出来的单位长度。{x: x * itemWidth, y: y * itemWidth}
。* 绘制多边形
*/
export default class Block {
constructor() { }
init(points, itemWidth, ctx) {
this.points = []
this.itemWidth = itemWidth // 单位长度
this.ctx = ctx
for (let i = 0; i < points.length; i++) {
let point = points[i]
this.points.push({
x: point.x * this.itemWidth,
y: point.y * this.itemWidth
})
}
}
draw() {
this.ctx.globalCompositeOperation = 'xor'
this.ctx.fillStyle = 'black'
this.ctx.beginPath()
this.ctx.moveTo(this.points[0].x, this.points[0].y)
for (let i = 1; i < this.points.length; i++) {
let point = this.points[i]
this.ctx.lineTo(point.x, point.y)
}
this.ctx.closePath()
this.ctx.fill()
}
}
[{ x: 4, y: 5 }, { x: 8, y: 9 }, { x: 4, y: 9 }],
[{ x: 10, y: 8 }, { x: 10, y: 12 }, { x: 6, y: 12 }],
[{ x: 7, y: 4 }, { x: 11, y: 4 }, { x: 11, y: 8 }]
]
points.map((sub_points) => {
let block = new Block()
block.init(sub_points, this.itemWidth, this.ctx)
block.draw()
})
1 + 1 = 0,是层叠拼图Plus小游戏玩法的精髓所在。
1 + 1 = 0
刚好符合通过 异或运算
得出的结果。如何绘制任意多边形图形
这一章节内,有一句特殊的代码:this.ctx.globalCompositeOperation = 'xor'
,也正是通过设置 CanvasContext
的 globalCompositeOperation
属性值为 xor
便实现了「偶消奇不消」的神奇效果。globalCompositeOperation
是指 在绘制新形状时应用的合成操作的类型
当回转数为 0 时,点在闭合曲线外部。
Canvas
画布内绘制出偶消奇不消效果的层叠图形了,接下来我们来看下玩家如何移动选中的图形。x
,y
坐标在哪个多边形图形内部区域,从而判断出玩家选中的是哪一个多边形图形。射线法 面积判别法 叉乘判别法 回转数法 ...
回转数
法来判断玩家触摸点是否在多边形内部。回转数
是拓扑学中的一个基本概念,具有很重要的性质和用途。回转数
的概念并不在该文的讨论范围内,我们仅需了解一个概念:当回转数为 0 时,点在闭合曲线外部。2
。用线段分别连接点和多边形的全部顶点
计算所有点与相邻顶点连线的夹角
计算所有夹角和。注意每个夹角都是有方向的,所以有可能是负值
JavaScript
实现时,需要注意以下问题:JavaScript
的数只有64
位双精度浮点这一种。对于三角函数产生的无理数,浮点数计算不可避免会造成一些误差,因此在最后计算回转数需要做取整操作。通常情况下,平面直角坐标系内一个角的取值范围是 -π 到 π 这个区间,这也是 JavaScript
三角函数Math.atan2()
返回值的范围。但JavaScript
并不能直接计算任意两条线的夹角,我们只能先计算两条线与x
正轴夹角,再取两者差值。这个差值的结果就有可能超出-π
到π
这个区间,因此我们还需要处理差值超出取值区间的情况。
* 判断点是否在多边形内/边上
*/
isPointInPolygon(p, poly) {
let px = p.x,
py = p.y,
sum = 0
for (let i = 0, l = poly.length, j = l - 1; i < l; j = i, i++) {
let sx = poly[i].x,
sy = poly[i].y,
tx = poly[j].x,
ty = poly[j].y
// 点与多边形顶点重合或在多边形的边上
if ((sx - px) * (px - tx) >= 0 &&
(sy - py) * (py - ty) >= 0 &&
(px - sx) * (ty - sy) === (py - sy) * (tx - sx)) {
return true
}
// 点与相邻顶点连线的夹角
let angle = Math.atan2(sy - py, sx - px) - Math.atan2(ty - py, tx - px)
// 确保夹角不超出取值范围(-π 到 π)
if (angle >= Math.PI) {
angle = angle - Math.PI * 2
} else if (angle <= -Math.PI) {
angle = angle + Math.PI * 2
}
sum += angle
}
// 计算回转数并判断点和多边形的几何关系
return Math.round(sum / Math.PI) === 0 ? false : true
}
探索的过程固然精彩,而结果却更令我们期待
xor
结果与目标图形的 xor
结果。xor
的结果呢? polygon-clipping 正是为此而生的。xor
操作,还有其他的比如:union
, intersection
, difference
等操作。目标图形
[{ x: 6, y: 6 }, { x: 10, y: 6 }, { x: 10, y: 10 }, { x: 6, y: 10 }],
[{ x: 8, y: 6 }, { x: 10, y: 8 }, { x: 8, y: 10 }, { x: 6, y: 8 }]
]
// 找出左上角的点
let min_x = 100, min_y = 100
results.forEach(function (sub_results) {
sub_results.forEach(function (temps) {
temps.forEach(function (point) {
if (point[0] < min_x) min_x = point[0]
if (point[1] < min_y) min_y = point[1]
})
})
})
// 以左上角为参考点 多边形平移至 原点 {x: 0, y: 0}
results.forEach(function (sub_results) {
sub_results.forEach(function (temps) {
temps.forEach(function (point) {
point[0] -= min_x
point[1] -= min_y
})
})
})
}
xor
结果:[[[0, 0], [2, 0], [0, 2], [0, 0]]],
[[[0, 2], [2, 4], [0, 4], [0, 2]]],
[[[2, 0], [4, 0], [4, 2], [2, 0]]],
[[[2, 4], [4, 2], [4, 4], [2, 4]]]
]
xor
结果进行比对即可得出答案正确与否。xor
结果并不能直接拿来与目标图形xor
结果进行比较。xor
的结果以左上角为参考点将图形平移至原点内,然后再进行比较,如果结果一致,则代表玩家答案正确。有人的地方就有江湖,有江湖的地方就有排行
开放数据域
开放数据域
是一个封闭、独立的 JavaScript
作用域。game.json
中添加配置项 openDataContext
指定开放数据域的代码目录。{
"openDataContext": "src/myOpenDataContext"
}
在游戏内使用 wx.setUserCloudStorage(obj)
对玩家游戏数据进行托管。在开放数据域内使用 wx.getFriendCloudStorage(obj)
拉取当前用户所有同玩好友的托管数据展示关系链数据
API
获取到的用户数据,如绘制排行榜等业务场景,需要将排行榜绘制到 sharedCanvas
上,再在主域将 sharedCanvas
渲染上屏。let sharedCanvas = wx.getSharedCanvas()
function drawRankList (data) {
data.forEach((item, index) => {
// ...
})
}
wx.getFriendCloudStorage({
success: res => {
let data = res.data
drawRankList(data)
}
})
sharedCanvas
是主域和开放数据域都可以访问的一个离屏画布。在开放数据域调用 wx.getSharedCanvas()
将返回 sharedCanvas
。let sharedCanvas = wx.getSharedCanvas()
let context = sharedCanvas.getContext('2d')
context.fillStyle = 'red'
context.fillRect(0, 0, 100, 100)
sharedCanvas
,通过 drawImage()
方法可以将 sharedCanvas
绘制到上屏画布。let sharedCanvas = wx.getSharedCanvas()
let context = sharedCanvas.getContext('2d')
context.fillStyle = 'red'
context.fillRect(0, 0, 100, 100)
sharedCanvas
本质上也是一个离屏 Canvas
,而重设 Canvas
的宽高会清空 Canvas
上的内容。所以要通知开放数据域去重绘 sharedCanvas
。openDataContext.postMessage({
command: 'render'
})
// src/myOpenDataContext/index.js
openDataContext.onMessage(data => {
if (data.command === 'render') {
// 重绘 sharedCanvas
}
})
sharedCanvas
的宽高只能在主域设置,不能在开放数据域中设置。性能优化,简而言之,就是在不影响系统运行正确性的前提下,使之运行地更快,完成特定功能所需的时间更短。
Canvas
Canvas
进行绘制的,如首页网格背景、关卡列表、排名列表等。wx.createCanvas()
首次调用创建的是显示在屏幕上的画布,之后调用创建的都是离屏画布。Canvas
的图像即可。Canvas
绘制本身就是不断的更新帧从而达到动画的效果,通过使用离屏 Canvas
,就大大减少了一些静态内容在上屏Canvas
的绘制,从而提升了绘制性能。this.offScreenCanvas.width = this.width * ratio
this.offScreenCanvas.height = this.height * ratio
this.ctx.drawImage(this.offScreenCanvas, x * ratio, y * ratio, this.offScreenCanvas.width, this.offScreenCanvas.height)
Block
对象。对象池
的方法进行优化,对象池维护一个装着空闲对象的池子。new
,而是从对象池中取出,如果对象池中没有空闲对象,则新建一个空闲对象。demo
内已经实现的对象池
类,实现如下:this.offScreenCanvas.width = this.width * ratio
this.offScreenCanvas.height = this.height * ratio
this.ctx.drawImage(this.offScreenCanvas, x * ratio, y * ratio, this.offScreenCanvas.width, this.offScreenCanvas.height)
JavaScript
中的每一个 Canvas
或 Image
对象都会有一个客户端层的实际纹理储存,实际纹理储存中存放着 Canvas
、Image
的真实纹理,通常会占用相当一部分内存。JavaScript
中的 Canvas
、Image
对象回收。JavaScript
的 Canvas
、Image
对象被回收之前,客户端对应的实际纹理储存不会被回收。wx.triggerGC()
方法,可以加快触发 JavaScriptCore Garbage Collection
(垃圾回收),从而触发 JavaScript
中没有引用的 Canvas
、Image
回收,释放对应的实际纹理储存。GC
具体触发时机还要取决于 JavaScriptCore
自身机制,并不能保证调用 wx.triggerGC()
能马上触发回收。16ms
是极其宝贵的,如果有一些可以异步处理的任务,可以放置于 Worker
中运行,待运行结束后,再把结果返回到主线程。Worker
运行于一个单独的全局上下文与线程中,不能直接调用主线程的方法,Worker
也不具备渲染的能力。Worker
与主线程之间的数据传输,双方使用 Worker.postMessage()
来发送数据,Worker.onMessage()
来接收数据,传输的数据并不是直接共享,而是被复制的。this.offScreenCanvas.width = this.width * ratio
this.offScreenCanvas.height = this.height * ratio
this.ctx.drawImage(this.offScreenCanvas, x * ratio, y * ratio, this.offScreenCanvas.width, this.offScreenCanvas.height)
Worker
最大并发数量限制为 1
个,创建下一个前请用 Worker.terminate()
结束当前 Worker
推荐阅读
(点击标题可跳转阅读)
觉得本文对你有帮助?请分享给更多人
关注「前端大全」加星标,提升前端技能
好文章,我在看❤️