如何实现点选
我们如何控制鼠标在点击后将目标图元做高亮或其他处理呢?
嗯….,在web端如果是svg或者是dom,我们可以简单绑定click事件修改样式, 那本文差不多也结束了。
? 额,别急着划开,那如果我问你如何在canvas里面实现点选,在webgl又怎么点选呢?
好像也挺简单的,我们可以这样
canvas点选
边界方法
let canvas = document.querySelector('#myCanvas')
let ctx = canvas.getContext('2d')
let selectedShape = null // 被选中的图形
// 绘制矩形
function drawRect(x, y, w, h) {
ctx.beginPath()
ctx.rect(x, y, w, h)
ctx.fill()
ctx.closePath()
}
// 判断是否在矩形内
function isInRect(x, y, w, h, mouseX, mouseY) {
return (mouseX >= x && mouseX <= (x + w) && mouseY >= y && mouseY <= (y + h))
}
function handleMouseDown(e) {
let mouseX = e.clientX - canvas.offsetLeft
let mouseY = e.clientY - canvas.offsetTop
for (let i = 0; i < shapes.length; i++) {
let shape = shapes[i]
if (isInRect(shape.x, shape.y, shape.w, shape.h, mouseX, mouseY)) {
selectedShape = shape
break
}
}
}
// 绘制矩形和选中状态
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
for (let i = 0; i < shapes.length; i++) {
let shape = shapes[i]
drawRect(shape.x, shape.y, shape.w, shape.h)
if (shape === selectedShape) {
ctx.strokeStyle = 'red'
ctx.strokeRect(shape.x, shape.y, shape.w, shape.h)
}
}
}
// 矩形的数组
let shapes = [
{ x: 100, y: 100, w: 100, h: 50 },
{ x: 200, y: 200, w: 100, h: 50 },
{ x: 300, y: 300, w: 100, h: 50 }
]
canvas.addEventListener('mousedown', handleMouseDown, false)
setInterval(draw, 10)
但是如果我们是一个圆形,好像又变复杂了些?
不怕,我们用点简单的数学的技巧
let distance = Math.sqrt(Math.pow((mouseX - x), 2) + Math.pow((mouseY - y), 2))
这段代码是计算鼠标点击位置到圆心的距离,用于判断鼠标点击位置是否在圆形内。它的原理是通过勾股定理求两点之间的距离,即鼠标点击位置和圆心之间的距离。如果这个距离小于圆的半径,则鼠标点击位置在圆形内。
要是这种图呢,实现点选真的是噩梦。?
其实我们可以最简单的方式,用颜色区分,rbga的每个通道范围在0到255,所有组合可能一共有40亿以上个值。
颜色检测方法
let canvas = document.querySelector('#myCanvas')
let ctx = canvas.getContext('2d')
let selectedShape = null // 被选中的图形
// 绘制矩形
function drawRect(x, y, w, h, color) {
ctx.beginPath()
ctx.fillStyle = color
ctx.rect(x, y, w, h)
ctx.fill()
ctx.closePath()
}
// 获取像素颜色
function getPixelColor(imageData, x, y) {
let index = (x + y * imageData.width) * 4
let r = imageData.data[index]
let g = imageData.data[index + 1]
let b = imageData.data[index + 2]
let a = imageData.data[index + 3]
return `rgba(${r}, ${g}, ${b}, ${a})`
}
function handleMouseDown(e) {
let mouseX = e.clientX - canvas.offsetLeft
let mouseY = e.clientY - canvas.offsetTop
let imageData = ctx.getImageData(mouseX, mouseY, 1, 1)
let color = getPixelColor(imageData, 0, 0)
for (let i = 0; i < shapes.length; i++) {
let shape = shapes[i]
// 如果颜色相同,说明已经选中
if (shape.color === color) {
selectedShape = shape
break
}
}
}
// 绘制矩形和选中状态
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
for (let i = 0; i < shapes.length; i++) {
let shape = shapes[i]
drawRect(shape.x, shape.y, shape.w, shape.h, shape.color)
if (shape === selectedShape) {
ctx.strokeStyle = 'red'
ctx.strokeRect(shape.x, shape.y, shape.w, shape.h)
}
}
}
// 矩形的数组
let shapes = [
{ x: 100, y: 100, w: 100, h: 50, color: '#e6194b' },
{ x: 200, y: 200, w: 100, h: 50, color: '#3cb44b' },
{ x: 300, y: 300, w: 100, h: 50, color: '#ffe119' }
]
canvas.addEventListener('mousedown', handleMouseDown, false)
setInterval(draw, 10)
这时有聪明的小伙伴露出了质疑,如果我要点选的某个图元的集合呢?就像下面的复杂图形我需要点击某个图元就要选中这个图元的集合。
同时又有杠精要说了,我根据屏幕颜色,总有那么几个图元的颜色是相同的,点选肯定有问题!
其实很简单,我们在另外生成一个画布,每次canvas绘制图形的时候重新分析边缘然后填充一个随机颜色。我们在颜色检测的时候只需要检测这个画布即可。
如何分析复杂图形的边缘
那么如何分析复杂图形的边缘呢?
有的大佬要说了,既然颜色检测那么方便,我们通过背景颜色跟图形颜色做区分,获取所有图元内部的所有的点,然后canvas绘制填充颜色即可。注意这是绘制后、而且像素量经过大量的计算,性能是个问题。
所以我们可以在绘制前将坐标记录,我们可以创建多个路径对象var path = new Path2D();
最终合并成一个路径,然后填充即可。下面简单演示下具体的用法:
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
var path1 = new Path2D();
path1.moveTo(0, 0);
path1.lineTo(100, 0);
ctx.stroke(path1);
var path2 = new Path2D();
path2.moveTo(100, 0);
path2.lineTo(100, 100);
ctx.stroke(path2);
var mergedPath = new Path2D();
mergedPath.addPath(path1);
mergedPath.addPath(path2);
ctx.strokeStyle = "red";
ctx.stroke(mergedPath);
不过正经的修图软件都是有一个选择的矩形框,选择的时候不用识别边缘,主要是性能的考虑。那如果同一个矩形选择框内有多个不同组的图元怎么办呢,类似的展示效果如叠加、遮挡等,通用做法其实就是做图层。
当我们每次创建图元的时候会有一个对象,记录位置信息等和id。我们通过id查询就可点选,当鼠标在图元内和对象的信息做比对是否在范围内即可。
canvas的Path对象与isPointinPath方法
图形的边界可以通过Canvas的Path对象创建,这种方法就是在创建图形时给图形创建Path路径,路径对象可以通过isPointinPath方法判断当前鼠标是否在路径范围内。
那又有大佬要问了,webgl怎么点选呢?嗯……
webgl点选
3d拾取技巧——射线相交
其实简单理解你需要将屏幕坐标,也就是你在鼠标的坐标先转为透视坐标系再转为裁剪坐标系,然后在近裁剪面的中心和鼠标坐标点就是射线的向量。判断是否与最近的图元的坐标相交即可。
3D拾取技巧——深度缓冲及颜色检测在WebGL的实现
为每一个对象赋予一个数字id,我们可以在关闭光照和纹理的情况下将数字id当作颜色绘制所有对象。 随后我们将得到一帧图片,上面绘制了所有物体的剪影,而深度缓冲会自动帮我们排序。 我们可以读取鼠标坐标下的像素颜色为数字id,就能得到这个位置上渲染的对应物体。
// mouseX 和 mouseY 是CSS显示空间下画布中指针的相对位置
let mouseX = -1;
let mouseY = -1;
let oldPickNdx = -1;
let oldPickColor;
let frameCount = 0;
// 绘制场景
function drawScene(time) {
time *= 0.0005;
++frameCount;
// ------ 把物体绘制到纹理上 --------
...
// ------ 找到指针下的像素颜色并读取
const pixelX = mouseX * gl.canvas.width / gl.canvas.clientWidth;
const pixelY = gl.canvas.height - mouseY * gl.canvas.height / gl.canvas.clientHeight - 1;
const data = new Uint8Array(4);
gl.readPixels(
pixelX, // x
pixelY, // y
1, // width
1, // height
gl.RGBA, // format
gl.UNSIGNED_BYTE, // type
data); // typed array to hold result
const id = data[0] + (data[1] << 8) + (data[2] << 16) + (data[3] << 24);
// 恢复对象的颜色
if (oldPickNdx >= 0) {
const object = objects[oldPickNdx];
object.uniforms.u_colorMult = oldPickColor;
oldPickNdx = -1;
}
// 高亮指针下的颜色
if (id > 0) {
const pickNdx = id - 1;
oldPickNdx = pickNdx;
const object = objects[pickNdx];
oldPickColor = object.uniforms.u_colorMult;
object.uniforms.u_colorMult = (frameCount & 0x8) ? [1, 0, 0, 1] : [1, 1, 0, 1];
}
// ------ 绘制对象到画布上
总结
在一些图形领域做交互的时候,往往计算是否选中会有很复杂的算法,如果使用颜色检测的方法来实现点选那再适合不过了。所以当你在写游戏存在大量敌人点击交互的时候,不妨思考下有必要计算offsetLeft、offsetTop么?
文章会同步更新到我的公众号:谦宇的编程世界。有兴趣的可以关注关注,我们一起共同进步。