canvas拾取方案汇总
使用canvas绘制图形的时候,也是经常会有交互的。 再canvas中是没有
js 操作 dom
的那么方便的事件系统。也有一些拾取方案...
简介
当前流行的图表库的底层渲染层都是使用 Canvas 或者 SVG 。从功能上来说 SVG 的功能更全面一些,提供的接口更丰富,使用更简单,但是所有的渲染和拾取(点击获取图形)都是浏览器内置的行为,其性能跟浏览器相关;而 Canvas 是使用一种直接绘制的方式,其渲染性能和拾取性能跟用户的实现的方式密切相关,性能的优化空间非常大。本文关注 Canvas 的拾取的方案以及各种方案的优缺点,提供给用户在合适的场景下选择恰当的拾取方式。
拾取方案
由于 Canvas 不会保存绘制图形的信息,一旦绘制完成用户在浏览器中得到的是一张图片,用户在图片上点击时时不能获取对应的图形信息,所以需要缓存图形的信息,根据用户点击的位置进行判断击中了那些图形。常见的拾取方案有以下几种:
- 使用缓存离屏渲染 Canvas 通过颜色拾取图形
- 使用 Canvas 内置的 API 拾取图形
- 使用几何运算拾取图形
- 混杂上面的几种方式来拾取图形
第一种(几何坐标计算)
利用canvas的
event
事件中的x、y
来判断, 基础的图形(正方形、圆形,直线)自然好判断,如果是多边形、不规则图形就有一些计算的成本。 效果图:
案例1(圆形):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<canvas width="600" height="600" style="border: 1px solid;"></canvas>
</body>
</html>
<script>
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const radius = 50;
const circleX = 50;
const circleY = 220;
ctx.beginPath();
ctx.arc(circleX, circleY, radius, 0, Math.PI * 2 , true);
ctx.closePath();
ctx.fill();
ctx.stroke();
canvas.onclick = function(event) {
const { x, y } = event;
//勾股定理计算 c2 = a2 + b2
const py = Math.sqrt(Math.pow(x - circleX, 2) + Math.pow(y - circleY, 2))
if(py <= radius) {
console.log('点击到了圆形')
}
}
</script>
案例2(矩形)
效果图:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<canvas width="600" height="600" style="border: 1px solid;"></canvas>
</body>
</html>
<script>
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const rectX = 50;
const rectY = 50;
const rectW = 50;
const rectH = 50;
ctx.beginPath();
ctx.fillRect(rectX, rectY, rectW, rectH)
ctx.stroke();
canvas.onclick = function(event) {
const { x, y } = event;
const x1 = rectX;
const y1 = rectY;
const x2 = rectX + rectW;
const y2 = rectY + rectH;
if(x >= x1 && x <= x2 && y >= y1 && y <= y2) {
console.log('点击到了矩形');
}
console.log(x, y, 'canvas')
}
</script>
阅读d3-lasso
源码 他实现套索框选是用了多边形的库, 需要必须涉及计算的话可以参考:(github.com/mikolalysen…]
第二种(自带Api isPointInPath)
isPointInPath 如果指定的点位于当前路径中,则返回 true,否则返回 false。 ⚠不支持strokeRect、fillRect。
示例一(基础用法):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas的事件监听</title>
</head>
<body>
<canvas width="600" height="600" style="border: 1px solid;"></canvas>
</body>
</html>
<script>
//isPointInPath 如果指定的点位于当前路径中,则返回 true,否则返回 false
//不支持 strokeRect
//不支持 fillRect
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext('2d')
ctx.beginPath()
ctx.rect(10, 5, 50, 50)
ctx.fill()
ctx.closePath()
ctx.beginPath()
ctx.rect(10, 60, 50, 50)
ctx.stroke()
ctx.closePath()
canvas.onclick = function (e) {
const canvasInfo = canvas.getBoundingClientRect()
const { x, y, clientX, clientY } = e
const some = ctx.isPointInPath(e.clientX - canvasInfo.left, e.clientY - canvasInfo.top)
console.log(some,'some')
}
</script>
总结: 正常编写canvas程序的这个写法,再用api isPointInPath
判断是否点击的时候,就只能判断最后一个绘制的图形。
示例二(给基本用法封装下):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas的事件监听</title>
</head>
<body>
<canvas width="600" height="600" style="border: 1px solid;"></canvas>
</body>
</html>
<script>
//isPointInPath
//不支持 strokeRect
//不支持 fillRect
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext('2d')
function draw1() {
ctx.beginPath()
ctx.rect(10, 5, 50, 50)
ctx.fill()
ctx.closePath()
}
function draw2() {
ctx.beginPath()
ctx.rect(10, 60, 50, 50)
ctx.stroke()
ctx.closePath()
}
const drawArray = [draw1,draw2]
drawArray.forEach(fn => fn())
canvas.onclick = function (e) {
ctx.clearRect(0,0,600,600)
const canvasInfo = canvas.getBoundingClientRect()
const { x, y, clientX, clientY } = e
const someArray = drawArray.map(item => {
item()
const some = ctx.isPointInPath(e.clientX - canvasInfo.left, e.clientY - canvasInfo.top)
return some
})
console.log(someArray,'someArray')
}
</script>
总结: 这个样确实能确定点击到了那个元素,但是每次都要重新绘制,还是不太友好的。
示例3(new Path2D结合isPointInPath)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas的事件监听</title>
</head>
<body>
<canvas width="600" height="600" style="border: 1px solid;"></canvas>
</body>
</html>
<script>
//isPointInPath
//不支持 strokeRect
//不支持 fillRect
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext('2d')
const path1 = new Path2D()
path1.rect(10, 5, 50, 50)
ctx.fill(path1)
const path2 = new Path2D()
path2.rect(10, 60, 50, 50)
ctx.stroke(path2)
const paths = [path1, path2];
canvas.onclick = function (e) {
const canvasInfo = canvas.getBoundingClientRect()
const { x, y, clientX, clientY } = e
const pathSomeArray = paths.map((item) => {
return ctx.isPointInPath(item,e.clientX - canvasInfo.left, e.clientY - canvasInfo.top)
})
console.log(pathSomeArray,'path1123')
}
</script>
总结: path2D 和 isPointInPath使用起来是比前面两种示例写法友好点。
离屏渲染(通过唯一颜色拾取图形)
OffscreenCanvas提供了一个可以脱离屏幕渲染的canvas对象。它在窗口环境和web worker环境均有效。OffscreenCanvas构造函数。创建一个新的OffscreenCanvas对象。
效果图:
用法示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
</head>
<body>
<canvas width="600" height="600" style="border: 1px solid;"></canvas>
</body>
</html>
<script>
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
//MDN: OffscreenCanvas提供了一个可以脱离屏幕渲染的canvas对象
const osCanvas = new OffscreenCanvas(canvas.width, canvas.height);
const osCtx = osCanvas.getContext('2d');
ctx.beginPath()
ctx.rect(10, 5, 50, 50)
ctx.fill()
ctx.closePath()
ctx.save();
ctx.beginPath();
ctx.rect(10, 60, 50, 50);
ctx.stroke();
ctx.closePath();
ctx.restore();
osCtx.save();
osCtx.beginPath();
osCtx.fillStyle = `rgba(${1}, ${1}, ${1}, ${255})`;
osCtx.strokeStyle = `rgba(${1}, ${1}, ${1}, ${255})`;
osCtx.rect(10, 5, 50, 50)
osCtx.fill()
osCtx.closePath()
osCtx.restore();
osCtx.save();
osCtx.beginPath()
osCtx.fillStyle = `rgba(${2}, ${2}, ${2}, ${255})`;
osCtx.strokeStyle = `rgba(${2}, ${2}, ${2}, ${255})`;
osCtx.rect(10, 60, 50, 50)
osCtx.stroke()
osCtx.fill()
osCtx.closePath()
osCtx.restore();
canvas.onclick = function (e) {
const x = event.offsetX;
const y = event.offsetY;
const rgba = osCtx.getImageData((x), (y), 1, 1).data;
console.log(rgba, 'rgba>>>>>>>>>................')
}
</script>
总结: 如果不考虑性能来说的话,离屏渲染应该是最好的拾取方案。 性能来说的话他毕竟重复绘制了两次。
提供一个离屏渲染方便操作rgba的对象:
const helperCanvasId = {
idPool: {},
createOnceId() {
return Array(3)
.fill(0)
.map(() => Math.ceil(Math.random() * 255))
.concat(255)
.join("-");
},
idToRgba(id) {
return id.split("-");
},
rgbaToId(rgba) {
return rgba.join("-");
},
createId() {
let id = this.createOnceId();
while (this.idPool[id]) {
id = this.createOnceId(e);
}
return id;
}
};
调用
helperCanvasId.createId()
即可生成一个不会重复的rgba
值可用来绑定离屏渲染的ctx
。
结束语
以上就是canvas中的拾取方案,您更倾向于那个那,可以留言一块学习交流。
- 大家好 我是三原,多谢您的观看,我会更加努力(๑•̀ㅂ•́)و✧多多总结。
- 每个方法都是敲出来验证过通过的,有需要可以放心复制。
- 如果您给帮点个赞👍就更好了 谢谢您~~~~~
- 期待您的关注