iis服务器助手广告广告
返回顶部
首页 > 资讯 > 前端开发 > JavaScript >canvas 中如何实现物体的框选
  • 151
分享到

canvas 中如何实现物体的框选

canvas物体框选canvas框选 2022-11-13 14:11:29 151人浏览 独家记忆
摘要

目录前言框选的实现Group 类的实现小结前言 虽然这两个月基金涨的还行,但是离回本还有一大大大段距离?。 今天呢,我们要实现的是 canvas 中物体的框选功能,大概就像下面这个样

前言

虽然这两个月基金涨的还行,但是离回本还有一大大大段距离?。

今天呢,我们要实现的是 canvas 中物体的框选功能,大概就像下面这个样子:

然后话不多说,直接开撸 ✍?

框选的实现

先来说下拖蓝选区(鼠标拖拽区域)的实现方式吧,仔细观察你会发现选区其实就是个普通矩形,这个区域由鼠标按下的点和拖动的终点组成,通过这两点我们就能够确认一个规规矩矩的矩形(边和 xy 轴平行),那在哪里绘制呢?还记得我们之前说过的么,所有的交互都是在上层画布进行的,所以它理所当然的应该绘制在上层画布,并且这样一来还可以避免重绘所有的物体。

  • 然后抬起鼠标的时候又要做些什么呢?

首先要做的就是把上层画布的拖蓝选区清除掉,再来就是不可避免的要遍历所有物体,找出和这个拖蓝选区有交集的所有物体。显然这又是一个数学问题,等价于判断两个矩形是否相交,相比之前判断点是否在矩形内部好像又麻烦了一丢丢,因为我们并没有直观的思路,并且还希望最好还可以推广到两个多边形,em...这里可以先思考几秒钟?。。。

  • 仔细想想两个矩形相交会有什么效果呢?

它们的边必相交,所以问题又可以转化为判断两个矩形的边是否相交。那如何判断两个矩形的边是否相交呢,稍微一想,最根本的就是判断两条边是否相交,这么一来,是不是稍微明朗了一点?。

具体一点就是:假设现在有物体 A 和物体 B,我们可以用 A 的第一条边去遍历 B 的每条边,如果能找到一个交点就说明两个物体相交;

否则继续用 A 的第二条边去遍历 B 的每条边,以此类推,如果遍历完了所有的还是没有交点,则说明物体 A、B 不相交。

当然这种方法还不够完全,少了一种特例,就是物体 A、B 还可能是包含与被包含的关系,比如物体被拖蓝选区完全包围,它们的边是没有交点的,所以我们也应该囊括这种情况,这种包含关系判断起来就比较简单了,就是比较下两个物体的最大最小 xy 值即可。

经过上面简单的推论不难得出,最基本的判断就是看两条线段是否相交,常规的解法就是:

  • 因为每条线段的端点是已知的,所以能求出两条线段所在的直线方程(注意直线和线段的措词,后面内容也是)
  • 如果两条直线斜率相同,那两条线段肯定不相交
  • 如果斜率不同,就需要联立方程组求解
  • 不过这个求解结果是直线的交点,最后还要简单校验下这个解是不是在两个线段的坐标范围内才行 这个就是最朴实无华的解法啦,我们先这么理解就行。其实在图形学中,类似这种运算都是用向量来计算的,比如用向量叉乘来判断线段是否相交,fabric.js 中也是用这样的思想,不过这个系列我并没有强调向量的概念,因为容易劝退,所以这些内容我会在这个系列的最后几个章节中单独写一篇来讲解,这里就简单贴下代码,可跳过??:

static intersectLineLine(a1: Point, a2: Point, b1: Point, b2: Point): Intersection {
    // 向量叉乘公式 `a✖️b = (x1, y1)✖️(x2, y2) = x1y2 - x2y1`
    let result,
        // b1->b2向量 与 a1->b1向量的向量叉乘
        ua_t = (b2.x - b1.x) * (a1.y - b1.y) - (b2.y - b1.y) * (a1.x - b1.x),
        // a1->a2向量 与 a1->b1向量的向量叉乘
        ub_t = (a2.x - a1.x) * (a1.y - b1.y) - (a2.y - a1.y) * (a1.x - b1.x),
        // a1->a2向量 与 b1->b2向量的向量叉乘
        u_b = (b2.y - b1.y) * (a2.x - a1.x) - (b2.x - b1.x) * (a2.y - a1.y);
    if (u_b !== 0) {
        let ua = ua_t / u_b,
            ub = ub_t / u_b;
        if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) {
            result = new Intersection('Intersection');
            result.points.push(new Point(a1.x + ua * (a2.x - a1.x), a1.y + ua * (a2.y - a1.y)));
        } else {
            result = new Intersection('No Intersection');
        }
    } else {
        // u_b == 0时,角度为0或者180 平行或者共线不属于相交
        if (ua_t === 0 || ub_t === 0) {
            result = new Intersection('Coincident');
        } else {
            result = new Intersection('Parallel');
        }
    }
    return result;
}
  • 现在假设我们通过上面的方法找到了所有与拖蓝选区相交的物体,那之后要做什么呢??

可以看到框选的最终效果就是用一个更大的包围盒把所有物体都框起来,最终生成的也只有外面的包围盒和控制点,被包裹的物体则只进行边框绘制,而没有控制点。

里面的物体好绘制,就是把物体设置成选中态即可,只是不绘制控制点(多加一个变量的事)。那外面的包围盒呢,怎么将这个大的包围盒和多个物体进行关联呢,这里又可以停下来想个几秒钟啦?。。。

Group 类的实现

一个大的包围盒和多个物体,能想到什么呢?

其实我们所有的物体是不是都在画布中,画布就可以看做是一个很大的包围盒,框住所有物体,所有物体也都依附于这个画布,这很形象,也顺便引出了接下来要介绍的组(Group)的概念。

Group 本身也继承于 FabricObject 类,它也是个物体,只不过这个物体下面还会有很多个小物体;

至于组的包围盒,和一个普通物体类似,找出所有子物体的最大最小 xy 值即可,这里我们直接看代码应该会更好理解(具体代码可以随便瞟一瞟,但是注释一定要看哦)??:


class Group extends FabricObject {
    public type: string = 'group';
    public objects: FabricObject[]; // 组中所有的物体
    constructor(objects: FabricObject[], options: any = {}) {
        super(options);
        this.objects = objects || [];
        this._calcBounds(); // 计算组的包围盒
        this._updateObjectsCoords(); // 更新组中的物体信息
    }
    
    _calcBounds() {
        // 就是求子物体中所有 objects 的最大最小 xy 值
    }
    
    _updateObjectsCoords() {
        let groupDeltaX = this.left,
            groupDeltaY = this.top;
        this.objects.forEach((object) => {
            let objectLeft = object.get('left'),
                objectTop = object.get('top');
            object.set('originalLeft', objectLeft);
            object.set('originalTop', objectTop);
            object.set('left', objectLeft - groupDeltaX);
            object.set('top', objectTop - groupDeltaY);
            object.setCoords();
            // 当有选中组的时候,不显示子物体的控制点
            object.orignHasControls = object.hasControls;
            object.hasControls = false;
        });
    }
    
    add(object: FabricObject) {
        this.objects.push(object);
        return this;
    }
    
    remove(object: FabricObject) {
        Util.removeFromArray(this.objects, object);
        return this;
    }
    
    addWithUpdate(object: FabricObject): Group {
        this._restoreObjectsState();
        this.objects.push(object);
        this._calcBounds();
        this._updateObjectsCoords();
        return this;
    }
    
    removeWithUpdate(object: FabricObject) {
        this._restoreObjectsState();
        Util.removeFromArray(this.objects, object);
        object.setActive(false);
        this._calcBounds();
        this._updateObjectsCoords();
        return this;
    }
    
    render(ctx: CanvasRenderinGContext2D) {
        ctx.save();
        this.transfORM(ctx); // 组有自身的变换,会影响所有子物体
        for (let i = 0, len = this.objects.length; i < len; i++) { // 遍历绘制组中所有物体
            let object = this.objects[i],
            object.render(ctx); // 回顾一下:每个物体的 render = 每个物体的 transform + 每个物体的 _render
        }
        if (this.active) { // 组是否被选中
            this.drawBorders(ctx);
            this.drawControls(ctx);
        }
        ctx.restore();
        this.setCoords();
    }
}

所以我们把 Group 当做一个普通的大物体就行,里面的子物体该怎么绘制还是怎么绘制,当 hover 和 click 的时候只要判断 Group 的包围盒即可,里面的子物体是不用去遍历的,因为它们是一个整体。

但是要注意的是上面代码中的 _updateObjectsCoords 方法,当我们把某些物体放进一个 Group 的时候,需要修改其 top 和 left 值,使其位置变为相对 Group 的位置,而不是相对于画布的位置,这点要尤其注意,类似这种嵌套关系,子物体的位置一般都是相对于其父元素来说的,而不是画布的位置?。

回过头来再说说框选,当鼠标抬起的时候,我们会找出与拖蓝选区相交的所有物体:

  • 如果只有一个物体与之相交的话,其实就变成了普通点选的情况,我们直接将该物体的置为选中态即可
  • 如果有多个物体相交,那就需要临时创建一个 Group 实例,叫 _activeGroup,将这些物体都添加进来,然后对这个临时组完成一些操作之后再销毁这个组即可 来看下核心代码??,也是很通俗易懂的:
class Canvas {
    
    _findSelectedObjects(e: MouseEvent) {
        let objects: FabricObject[] = [], // 存储最终框选的元素
            x1 = this._groupSelector.ex,
            y1 = this._groupSelector.ey,
            x2 = x1 + this._groupSelector.left,
            y2 = y1 + this._groupSelector.top,
            selectionX1Y1 = new Point(Math.min(x1, x2), Math.min(y1, y2)),
            selectionX2Y2 = new Point(Math.max(x1, x2), Math.max(y1, y2));
        for (let i = 0, len = this._objects.length; i < len; ++i) {
            let currentObject = this._objects[i];
            // 物体是否与拖蓝选区相交或者被选区包含,用到的就是前面说过的多边形相交算法,具体的算法会在文末附上
            if (currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2) || currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2)) {
                currentObject.setActive(true);
                objects.push(currentObject);
            }
        }
        if (objects.length === 1) { // 如果只有一个物体被选中
            this.setActiveObject(objects[0], e);
        } else if (objects.length > 1) { // 如果有多个物体被选中
            const newGroup = new Group(objects);
            this.setActiveGroup(newGroup);
        }
        this.renderAll();
    }
    setActiveGroup(group: Group): Canvas {
        this._activeGroup = group;
        if (group) {
            group.canvas = this;
            group.setActive(true);
        }
        return this;
    }
}

上面代码中要注意的就是我们还需要对 renderAll 这个绘制方法做一些修改,就是把所有激活的物体都放到最后绘制,就像下面这样??:

class Canvas {
    renderAll(): Canvas {
        ...
        // 先将物体排个序,这样才能体现出层级关系,简单来说就是先绘制未激活物体,再绘制激活物体
        const sortedObjects = this._chooseObjectsToRender();
        for (let i = 0, len = sortedObjects.length; i < len; ++i) {
            this._draw(canvasToDrawOn, sortedObjects[i]);
        }
        return this;
    }
    
    _chooseObjectsToRender() {
        // 当前有没有激活的物体
        let activeObject = this.getActiveObject();
        // 当前有没有激活的组(也就是多个物体)
        let activeGroup = this.getActiveGroup();
        // 最终要渲染的物体顺序,也就是把激活的物体放在后面绘制
        let objsToRender = [];
        if (activeGroup) { // 如果选中多个物体
            const activeGroupObjects = [];
            for (let i = 0, length = this._objects.length; i < length; i++) {
                let object = this._objects[i];
                if (activeGroup.contains(object)) {
                    activeGroupObjects.push(object);
                } else {
                    objsToRender.push(object);
                }
            }
            objsToRender.push(activeGroup);
        } else if (activeObject) { // 如果只选中一个物体
            let index = this._objects.indexOf(activeObject);
            objsToRender = this._objects.slice();
            if (index > -1) {
                objsToRender.splice(index, 1);
                objsToRender.push(activeObject);
            }
        } else { // 所有物体都没被选中
            objsToRender = this._objects;
        }
        return objsToRender;
    }

当然如果是框选或点击到空白处,只要把所有物体的 active 属性都设置 false 就行了。但有同学肯定又会有疑问了,上面这样的排序绘制好像并不能精确控制每个物体的层级关系,如果我们需要做个上移一层、下移一层的功能该怎么搞呢?

这个也很简单,在 html 中也已经给了我们答案,就是用 z-index,我们给每个物体多加一个 zIndex 属性就行了,之后直接用 zIndex 排序就行。

其实在 canvas 上绘制东西和浏览器展示页面内容这个过程很像很像,很多思想都是共通的,比如盒模型、元素的继承、transform、zIndex、top、left 等常见的 CSS 属性,以及后续会提到的事件监听,只不过我们习惯了用 html 和 css 去描绘这个页面,而 canvas 需要我们用 js 去描述,canvas 库则是提供了这个桥梁,极大方便了我们开发

小结

这个章节我们主要讲的是 canvas 中框选和 Group 类的实现,最重要的有以下几点:

  • 判断两个多边形相交的方法:判断各个边是否相交 && 整体包含关系
  • 框选的时候我们会临时生成一个组,之后销毁即可
  • 组的变换会影响到其子元素的变换
  • 渲染的时候需要将所有激活的物体放在后面绘制,有需要的话可以加上 zIndex 属性进行精确控制
  • 另外补充一点:Group 也让我们整个物体链变成了树形结构

然后这里是简版 fabric.js 的代码链接,有兴趣的可以看看。下个章节我们会讲解怎么对一个物体进行各种变换操作(拖拽、缩放、旋转),也是本系列最重要的章节之一?。

canvas 中如何实现物体的点选(五)?

canvas 中物体边框和控制点的实现(四)?

实现一个轻量 fabric.js 系列三(物体基类)?

实现一个轻量 fabric.js 系列二(画布初始化)?

实现一个轻量 fabric.js 系列一(摸透 canvas)?

以上就是canvas 中如何实现物体的框选的详细内容,更多关于canvas物体框选的资料请关注编程网其它相关文章!

--结束END--

本文标题: canvas 中如何实现物体的框选

本文链接: https://www.lsjlt.com/news/171574.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • canvas 中如何实现物体的框选
    目录前言框选的实现Group 类的实现小结前言 虽然这两个月基金涨的还行,但是离回本还有一大大大段距离。 今天呢,我们要实现的是 canvas 中物体的框选功能,大概就像下面这个样子...
    99+
    2022-11-13
    canvas物体框选 canvas框选
  • 前端canvas中物体边框和控制点的实现示例
    目录前言关于边框关于控制点本章小结前言 在上一章中我们已经搞定了下层画布,也就是能够对物体进行绘制了,现在就可以开始搞搞上层交互了。 不过在和画布产生交互之前,我们还要做一件事情,就...
    99+
    2022-11-13
    前端canvas物体边框控制点 canvas物体边框控制点
  • JS前端使用canvas实现物体的点选示例
    目录前言hover 的实现click 的实现矩形的坐标哪来的点在多边形内的其他判断方法穿透本章小结前言 上个章节中我们已经给物体加上了被选中的效果,现在可以上点交互了,这个章节主要实...
    99+
    2022-11-13
    JS前端canvas物体点选 canvas物体点选
  • HTML5中Canvas如何实现弹出框效果
    这篇文章将为大家详细讲解有关HTML5中Canvas如何实现弹出框效果,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。用户鼠标移入时,有弹出框出现,这样的需求很常见。这在处理HTML元素实现时简单,但是如果...
    99+
    2023-06-09
  • AmazeUI如何实现单选框和多选框
    这篇文章将为大家详细讲解有关AmazeUI如何实现单选框和多选框,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。AmazeUI 单选框和多选框的实现示例,具体如下:<!doctype htm...
    99+
    2023-06-09
  • python pygame如何实现控制物体移动
    本篇内容主要讲解“python pygame如何实现控制物体移动”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“python pygame如何实现控制物体移动”吧!import...
    99+
    2023-06-22
  • jQuery如何实现复选框的全选和反选
    这篇文章主要为大家展示了“jQuery如何实现复选框的全选和反选”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“jQuery如何实现复选框的全选和反选”这篇文章吧...
    99+
    2022-10-19
  • Angular如何实现多选复选框的弹出框指令
    这篇文章将为大家详细讲解有关Angular如何实现多选复选框的弹出框指令,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。要实现一个包含多个复选框的下拉框该如何做呢?先上个效...
    99+
    2022-10-19
  • C#模拟试验中如何实现的弹性物体碰撞
    这篇文章将为大家详细讲解有关C#模拟试验中如何实现的弹性物体碰撞,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。无聊之际用C#写了一个弹性物体碰撞模拟玩玩。这个想法源自与前几天上机课有人想我在...
    99+
    2023-06-17
  • HTML+CSS如何实现单选框、复选框美观的样式
    小编给大家分享一下HTML+CSS如何实现单选框、复选框美观的样式,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!1.背景图html<div cla...
    99+
    2023-06-08
  • 如何实现超炫checkbox复选框和radio单选框
    这篇文章主要讲解了“如何实现超炫checkbox复选框和radio单选框”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“如何实现超炫checkbox复选框和r...
    99+
    2022-10-19
  • BootStrap中如何实现Table复选框默认选中功能
    这篇文章主要介绍BootStrap中如何实现Table复选框默认选中功能,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!废话不多说,直接给大家贴代码了,具...
    99+
    2022-10-19
  • PyQt5如何实现字体对话框
    这篇文章主要介绍“PyQt5如何实现字体对话框”,在日常操作中,相信很多人在PyQt5如何实现字体对话框问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”PyQt5如何实现字体对话框”的疑惑有所帮助!接下来,请跟...
    99+
    2023-07-05
  • Vue如何实现淘宝购物车三级选中功能
    本文小编为大家详细介绍“Vue如何实现淘宝购物车三级选中功能”,内容详细,步骤清晰,细节处理妥当,希望这篇“Vue如何实现淘宝购物车三级选中功能”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。实现:全选时所有商品+...
    99+
    2023-06-26
  • html页面中的单选框功能如何实现
    本文小编为大家详细介绍“html页面中的单选框功能如何实现”,内容详细,步骤清晰,细节处理妥当,希望这篇“html页面中的单选框功能如何实现”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新...
    99+
    2022-10-19
  • AngularJS如何实现单选框及多选框的双向动态绑定
    这篇文章给大家分享的是有关AngularJS如何实现单选框及多选框的双向动态绑定的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。AngularJS 在 <input type...
    99+
    2022-10-19
  • angular框架如何实现全选与单选chekbox
    这篇文章将为大家详细讲解有关angular框架如何实现全选与单选chekbox,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。1)页面内容(静态页)<ul ...
    99+
    2022-10-19
  • 如何实现jQuery多选框功能
    这篇文章主要介绍如何实现jQuery多选框功能,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!Jquery多选框的基本操作:支持全选、反选、取消全选的功能HTML正文:<inpu...
    99+
    2022-10-19
  • vue中radio单选框如何实现取消选中状态问题
    目录vue radio单选框如何取消选中状态客户需求如何获取radio的选中值 、选中状态方法1方法2总结vue radio单选框如何取消选中状态 客户需求 单选radio选中后,再...
    99+
    2023-05-20
    vue中radio单选框 radio单选框取消选中状态 radio单选框选中状态
  • css如何实现边框字体同色
    小编给大家分享一下css如何实现边框字体同色,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!边框字体同色 .wrap&n...
    99+
    2022-10-19
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作