全方位讲解:鼠标框选图形功能的技术实现

引言

如果你正在考虑开发一款图形编辑软件,并希望能够支持常用的框选功能,本文将为你提供一些有用的信息。像下面动画所示那样快速选取单个或多个图形并非易事,尤其是还要考虑到像直线这样的图形有其特别之处。本文提出的解决方案主要是利用包围盒的概念,通过包围盒与包围盒重叠的检测判断图形是否被选中。此外,我们还会详细介绍针对直线的特殊情况该如何进行是否选中的检测,并给出一些关键算法的伪代码,帮助读者理解其实现原理。希望通过本文的讲解,你能够更好地理解框选功能的实现方法,并将其应用于你的应用程序中。

图片

包围盒重叠检测

如果你想让你的图形编辑软件具备框选功能,你可以考虑使用包围盒这一概念。所谓的包围盒是指覆盖所有图形的最小矩形区域。当用户选择某个图形时,程序会生成一个表示该图形包围盒的矩形区域。这种方法适用于大多数图形,但也有例外,例如直线。因为直线本身就没有宽度,所以使用包围盒的方法可能导致框选结果不够理想。在本文中,我们将详细解释如何更准确地框选直线。总的来说,包围盒是一种简单有效的框选方法,但对于直线这类特殊的图形,我们需要采取更为精细的方法来提高框选体验。

图片

对于实时框选功能,只需跟踪鼠标的按下和拖动事件,并计算出对应的包围盒即可,这样判断图形有没有被框选住就可以通过简单的判断两个包围盒有没有重叠就可以了。本文重点介绍框选检测部分的算法实现,不会涉及具体图形的绘制算法的讲解,其实只要掌握基本的图形(点、线、三角形、矩形等)绘制方法就可以了,被框选住的图形效果显示仍然还是基本图形的绘制,比如选中图形包围盒的顶点是由更小的矩形组合而成的,这里假定已经实现了基本图形的绘制。我们更多需要关注的是图形数据结构的表示、包围盒与包围盒重叠的检测、包围盒与直线重叠的检测。

判断两个包围盒A、B是否重叠比较简单,只要在x、y轴坐标上满足两个条件就说明是A和B是重叠的,即A.xmax >= B.xmin && B.xmax >= A.xmin,A.ymax >= B.ymin && B.ymax >= A.ymin。

图片

可以实现一个BoundingBox类,它提供isCollision方法用来检测两个包围盒是否重叠,另外提供一个merge方法来合并两个包围盒,在框选多个图形时需要处理多个包围盒的合并。下面是实现BoundingBox类的伪代码:

class BoundingBox {
    constructor(xmin, ymin, xmax, ymax) {
        this.xmin = xmin;
        this.ymin = ymin;
        this.xmax = xmax;
        this.ymax = ymax;
        this.width = xmax - xmin;
        this.height = ymax - ymin;
    }
	
    isCollision(box) {
        if ((this.xmax >= box.xmin && box.xmax > this.xmin
            && (this.ymax >= box.ymin && box.ymax > this.ymin) {
            return true;
        }
    }

    merge(box) {
        this.xmin = Math.min(this.xmin, box.xmin);
        this.ymin = Math.min(this.ymin, box.ymin);
        this.xmax = Math.max(this.xmax, box.xmax);
        this.ymax = Math.max(this.ymax, box.ymax);
        this.width = Math.abs(this.xmax - this.xmin;
        this.height = Math.abs(this.ymax - this.ymin;
        return this;
    }
}

框选单个图形

首先,给出如下会使用到的对象及其功能:

  • 场景scene表示画布场景对象,scene.children是存放了所有图形对象的列表,scene.render方法负责绘制场景中的图形到canvas画布;

  • selector表示鼠标框选的区域对象,selector.start(x, y)用于鼠标首次点击跟踪鼠标位置,selector.moveTo(x, y)跟踪鼠标拖动的实时位置,selector.isSelected方法检查鼠标框选区域是否选中了图形;

  • 所有的图形对象都包含顶点信息,且都拥有一个getBoundingBox方法,用于生成图形的包围盒,生成包围盒的方法就是遍历其顶点找到(xmin, ymin)、(xmax, ymax);

1. 框选除直线外的其他图形检测方法
除直线外,判断某个图形是否被框选住的条件就是判断鼠标框选区域的包围盒和图形的包围盒是否重叠,这里直接给出伪代码:

class Selector {
    // 模拟鼠标按下开始启动选择
    start(x, y) {
        this._start = new Vector2(x, y);
    }

    // 鼠标最新位置
    moveTo(x, y) {
        this._end = new Vector2(x, y);
        // 更新鼠标选取的包围盒
        this.boundingBox = new BoundingBox(this._start, this._end);
    }

    // 鼠标选取是否选中图形
    isSelected(object) {
	    // 判断鼠标框选区域包围盒是否和图形的包围盒相交
		return this.boundingBox.isCollision(object.getBoundingBox());
	}
}
    
// 遍历场景中的图形
for (let i = 0; i < scene.children.length; i++) {
	let shape = scene.children[i];
	// 检查图形的包围盒是否和鼠标框选区域重叠
	if (selector.isSelected(shape)) {
		// 鼠标框选住图形
		shape.select(); // 设置shape.isSelected为true
	} else {
		shape.unselect(); // 设置shape.isSelected为false
	}
}

被框选中的图形将调用select方法将图形对象的isSelected状态置为true,否则置为false,scene.render方法中会检查每个图形的选中状态并进行选中效果的绘制。

2. 框选直线
接下来分析直线被选中的条件,根据下面的示意图可以得出直线被框选住必须同时满足以下三个条件:

  • 框选矩形的四个顶点不能同时在直线的同一侧,也就是说至少一个点在直线上或直线的另一侧,可以通过把矩形的四个顶点代入直线方程通过正负号进行判断是否分布在两侧;

  • 矩形的xmin必须小于等于直线的xmax,且矩形的xmax必须大于等于直线的xmin;

  • 矩形的ymin必须小于等于直线的ymax,且矩形的ymax必须大于等于直线的ymin;

图片

不难测试出以上条件针对任意被框选住的情况都是适用的,而不仅仅上面示意图中的情形。加入了框选直线之后,selector.isSelected方法的代码需要稍作改动以适配框选图形是直线的情况。

class Selector {
    // 模拟鼠标按下开始启动选择
    start(x, y) {
        this._start = new Vector2(x, y);
    }

    // 鼠标最新位置
    moveTo(x, y) {
        this._end = new Vector2(x, y);
        // 更新鼠标选取的包围盒
        this.boundingBox = new BoundingBox(this._start, this._end);
    }

    // 鼠标选取是否选中图形
    isSelected(object) {
		// 直线不使用包围盒判断
		if (object.type == "line") {
			// 获取直线方程,它是一个方法,输入参数为坐标(x, y),实现代码在下面给出
			let func_line = object.lineEquation();
			// 返回矩形的四个顶点
			let box_points = this.boundingBox.getBoxPoints();
			let result;
			// 遍历矩形的四个顶点,判断是否分布在直线两侧
			for (let i = 0; i < box_points.length; i++) {
				// 只存储直线方程求值的正负号
				let res = func_line(box_points[i].x, box_points[i].y) >= 0;
				if (i == 0) {
					result = res;
				} else if (res != result) {
					// res和result不同表示已经有一个点在直线另一侧
					let xmin = this.boundingBox.xmin;
					let xmax = this.boundingBox.xmax;
					let ymin = this.boundingBox.ymin;
					let ymax = this.boundingBox.ymax;
					let line_xmin = Math.min(object.v0.x, object.v1.x);
					let line_xmax = Math.max(object.v0.x, object.v1.x);
					let line_ymin = Math.min(object.v0.y, object.v1.y);
					let line_ymax = Math.max(object.v0.y, object.v1.y);
					if (((xmin <= line_xmax) && (xmax >= line_xmin)) 
						&& ((ymin <= line_ymax) && (ymax >= line_ymin)) ) {
						// 直线被选中
						return true;
					}
				}
			}
			return false;
		} else {
			// 判断鼠标框选区域包围盒是否和图形的包围盒相交
			return this.boundingBox.isCollision(object.getBoundingBox());
		}
	}
}

// 获取直线方程部分的代码
class Line extends Geometry {
	...	
	// 根据直线的端点得到直线方程
	lineEquation() {
		// 求直线方程a*x + b*y + c = 0
		let v0 = this.vstart;
		let v1 = this.vend;
		let a = v1.y - v0.y;
		let b = v0.x - v1.x;
		let c = v1.x * v0.y - v1.y * v0.x;
		// 返回值为直线方程表示的函数
		// 求ax+by+c的值, 值为0表示(x,y)在直线上,
		// 大于0表示在直线的上方,小于0表示在直线的下方
		return function(x, y) {
			return a * x + b * y + c;
		}
	}
}

框选多个图形

当框选多于一个图形时,一律使用包围盒显示选中的效果,之前的代码已经做到了场景中所有被选中的图形对象isSelected状态为true。在绘制到canvas画布之前,需要检查场景中的图形对象的选中状态,并做下面的处理:

  • 如果只有一个图形被选中且选中的图形为直线,则绘制直线被选中的效果;

  • 如果只有一个图形被选中且选中的图形不是直线,则直接绘制选中的矩形包围盒效果;

  • 如果选中了多个图形,则逐个合并每个图形的包围盒,最终绘制合并后的矩形包围盒的效果。


    伪代码如下:

class Scene {
	... 
	render() {
	    let selected_shapes = [];
		for (let i = 0; i < this.children.length; i++) {
		    if (this.children[i].isSelected) {
			    selected_shapes.push(this.children[i]);
		    }
		    // 绘制图形自身
			this.children[i].draw();
		}
		// 绘制选中效果
		if (selected_shapes.length > 0) {
			if (selected_shapes.length == 1 && selected_shapes[0].type == "line") {
				// 只选中了一个图形且为直线
				// 绘制直线选中效果,假定已经实现
				selected_shapes[0].drawLineSeleted();			
			} else {
				// 选中了多个图形或一个非直线的图形,逐个合并每个图形的包围盒
				let merged_box = new BoundingBox();
				for (let i = 0; i < selected_shapes.length; i++) {
					merged_box.merge(selected_shapes[i].getBoundingBox());
				}
				// 绘制合并后的包围盒效果,假定已经实现
				drawBoundingBox(merged_box);
			}
		}
	}
}

总结

我们已经分析了实现鼠标框选图形的方法,以及如何检测一个图形是否被框选的方法。特别是对于直线对象,我们提供了更具直观的方法来判断是否被框选住,而不需要依赖于包围盒。而对于非直线对象,可以通过对比两个包围盒的大小来判断是否重叠。最后,在框选多个图形时,我们可以合并它们对应的包围盒,以便实现框选多个图形的效果。虽然文章中并未涵盖图形绘制的内容,但它是一个基本技能,很多图形学方面的书籍都会有详细的介绍,并且在网络上也有丰富的资料可供查阅。如果你想了解更多的相关信息,建议你参考《WebGL编程指南》,这是一本非常适合初学者阅读的书籍,里面有许多实用的代码实例可供参考。

添加微信即可免费领取下面的自动化测试资料和一份超全的软件测试面试宝典!!!

图片

图片

图片

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值