canvas交互怎样实现的拖拽、旋转、缩放效果

Admin 2022-09-07 群英技术资讯 293 次浏览

今天这篇给大家分享的知识是“canvas交互怎样实现的拖拽、旋转、缩放效果”,小编觉得挺不错的,对大家学习或是工作可能会有所帮助,对此分享发大家做个参考,希望这篇“canvas交互怎样实现的拖拽、旋转、缩放效果”文章能帮助大家解决问题。


正文

到目前为止,我们已经能够对物体进行点选和框选的操作了,但是这还不够,因为并没有什么实际性的改变,并且画布看起来也有点呆板,所以这个章节的主要目的就是让画布中的物体活起来,其实就是增加一些常见的交互而已啦,比如拖拽、旋转和缩放。这是这个系列最重要的章节之一,希望能够对你有所帮助。

拖拽

先来说说拖拽平移的实现吧,因为它最为简单。我们知道每个物体都是有 top 和 left 值来表示物体位置的,所以平移的时候只需要简单的更新下物体的 top 和 left 值即可,然后每次移动都会触发 renderAll 方法进行重新渲染,于是就自然而然的在新的位置绘制物体了。

这个就是典型的数据与视图分离,这个章节包括接下来的章节我们一般都不需要去修改物体的 render 方法了,但凡画布上有物体在动(物体状态改变了),我们都只需要更新物体的数据就行,而不用去关心如何绘制,反正值改了会自然而然的反应到画布上,这点很重要。

然后简单看下平移的代码:

/** 平移当前选中物体 */
_translateObject(x: number, y: number) {
    const target = this._currentTransform.target;
    target.set('left', x - this._currentTransform.offsetX); // offsetX 是画布整体偏移
    target.set('top', y - this._currentTransform.offsetY); // offsetY 是画布整体偏移
}

是的,代码就那么点,也不难理解,因为物体的绘制方法是固定的,我们所做的任何变换操作都仅仅是单纯的修改数据而已。不过要提下上面代码中的 _currentTransform 是什么东西,它就是一开始我们按下鼠标时记录的一些初始信息,大概长下面这个样子,看看就行,有个印象即可:

em...,没错,拖拽平移的部分就那么短,毕竟确实简单。

旋转

再来说下旋转吧,旋转也比较简单。我们知道每个物体都是有一个 angle 变量来表示物体旋转角度的,当对物体进行旋转操作的时候,我们可以先计算出拖拽旋转的角度 deltaAngle,于是新的 angle = 旧的 angle + deltaAngle,然后重新赋值 angle 变量即可,同样的这个过程中也不会涉及修改物体的 _render 方法,只不过比平移稍微麻烦点的就是这个变换的角度该怎么计算呢?

其实旋转的过程本质就是鼠标点的旋转,也就是说我们只要计算出当前鼠标点和初始鼠标点之间的角度就行。就像下面这张图一样:

我们先来看看一个点的情况下,怎么算这个点的朝向,一般我们算的是该点与原点的连线和 x 轴正方向之间的逆时针方向的夹角,如下图所示:

通常我们会用 radian = Math.atan2(y, x) 来计算弧度,注意是弧度(radian)不是角度(angle),所以再提醒下,canvas 中用的都是弧度,但是角度方便我们理解,所以时不时需要转换;

另外要注意我们用的是 Math.atan2 而不是 Math.atan,虽然它们大同小异,但是我们不能根据 atan 的值来确定唯一的方向,比如点(1, 1)和点(-1, -1),它们的 atan 值都一样,但是方向确相反,所以有了 atan2,atan2 的取值范围在 [-Math.PI, Math.PI] 之间,并且四个象限的取值各不相同,所以一般都是用它来计算。

知道了这些计算就简单了,原点就是物体的中心点,鼠标按下的点可以与物体中心点相连形成一个起始角度,鼠标拖拽时的点也可以与物体中心点相连形成一个最终角度,用最终角度-起始角度就能得到要变换的角度了。

切记,通常情况下我们对什么物体进行旋转,原点就是物体的中心点。下面是核心的代码示例,代码不多也好消化:

/** 旋转当前选中物体 */
_rotateObject(x: number, y: number) {
    const t = this._currentTransform;
    const o = this._offset;
    // 鼠标按下的点与物体中心点连线和 x 轴正方向形成的弧度
    const lastRadian = Math.atan2(t.ey - o.top - t.top, t.ex - o.left - t.left);
    // 鼠标拖拽的终点与物体中心点连线和 x 轴正方向形成的弧度
    const curRadian = Math.atan2(y - o.top - t.top, x - o.left - t.left);
    const deltaRadian = curRadian - lastRadian;
    let angle = Util.radiansToDegrees(t.theta + deltaRadian); // 新的角度 = 原来的角度 + 变换的角度
    if (angle < 0) angle = 360 + angle;
    angle = angle % 360;
    t.target.angle = angle;
}

缩放

再来就是缩放啦,这个又比上面的旋转稍微麻烦些,这里我们以右边中间的缩放控制点为例子,其他控制点是一个意思(复制改改就行),先看看效果:

大家仔细看上图中右边中间红色的那个控制点,它的缩放结果其实是就沿着 x 轴拉伸,本能的想法是什么呢?就是计算出水平方向的拖拽距离 dx,然后去改变物体的宽度,就像这样 object.width += dx,但是如果 width 变成了负数怎么办,是不是也要处理一下,简单点的做法就是我们可以限制个最小值,如果是右边的控制点拉到最左边了,就不允许再拉了。

不过,不知道你还记得我们早前说过的一个知识点么?????就是我们一般不会去改变物体自身的大小,而是去修改物体的变换值,所以缩放的本质也仅仅是改变物体的 scaleX 和 scaleY 值。还是以拖拽右边中间控制点的拉伸为例子,这次我们算的是 scaleX,怎么算这个值会方便点呢?可以将拉伸的变换基点暂时变为左边中间的控制点,也就是左边的蓝点(这个很重要),这样计算当前宽度的时候就会比较方便了:

  • 当前宽度 = 鼠标位置的 x - 左边中间控制点的位置的 x
  • scaleX = 当前宽度 / 自身宽度 记住,我们自身 width 的值并没有改变,只是改变了 scaleX 值。同样的它也有反向拉伸的问题,但我们可以变通处理一下,临时变换下拉伸基点。什么意思呢?????就是一旦变成反向拉伸,我们就立马切换成按左边中间控制点拖拽的逻辑执行,也就是变成拖拽蓝点,而红点变成了参考基点,大家可以再好好看看上面那个动图体会下。
  • 当然这样还不够,拖拽缩放的时候还有个问题,就是 top 和 left 值也会随之改变,所以算完 scaleX 之后还需要对这两个值进行更新,大家注意看上面那个动图中的黑点就能体会到了。然后再提醒两个点:
  • 就是缩放的时候中心点并不是在物体的中心,所以我们可以简单的理解成单边缩放;当然其实也可以沿着中心点缩放,只不过我们讲解的是默认的形式;
  • 如果是竖直拉伸,只要把 x 换成 y,把宽度换成高度即可,如果是右下角那个控制点就把 xy 的代码都加上即可;

这里也简单贴下核心代码:

/**
 * 缩放当前选中物体
 * @param x 鼠标点 x
 * @param y 鼠标点 y
 * @param by 是否等比缩放,x | y | equally
 */
_scaleObject(x: number, y: number, by = 'equally') {
    let t = this._currentTransform, // 在鼠标按下的时候会记录物体的状态
        offset = this._offset, // 画布偏移
        target: FabricObject = t.target;
    // 缩放基点:比如拖拽右边中间的控制点,其实我们参考的变换基点是左边中间的控制点
    let constraintPosition = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY);
    // 以物体变换中心为原点的鼠标点坐标值
    let localMouse = target.toLocalPoint(new Point(x - offset.left, y - offset.top), t.originX, t.originY);
    if (t.originX === 'right') {
        localMouse.x *= -1;
    }
    // 计算新的缩放值,以变换中心为原点,根据本地鼠标坐标点/原始宽度进行计算,重新设定物体缩放值
    let newScaleX = target.scaleX;
    if (by === 'x') {
        newScaleX = localMouse.x / (target.width + target.padding);
        target.set('scaleX', newScaleX);
    }
    // 如果是反向拉伸 x
    if (newScaleX < 0) {
        if (t.originX === 'left') t.originX = 'right';
        else if (t.originX === 'right') t.originX = 'left';
    }
    // 缩放会改变物体位置,所以要重新设置
    target.setPositionByOrigin(constraintPosition, t.originX, t.originY);
}

这个变换看起来麻烦点,所以我单独写了个小 demo,有兴趣的可以点击这个链接单独查看。建议大家多动手试试,记住,最核心的要点就是:

我们不改变物体自身的宽高大小,也不改变物体的渲染方法,而只是改变三种变换的值。

可能有的同学还会问到上面的变换操作在鼠标移动时会不停的调用 renderAll 这个渲染函数,性能是不是一般啊,尤其是当物体一多就更不咋地了?

那肯定是这样的,在前端,不管啥东西,只要东西多了就会垮掉,比如数据多了就得分页,虚拟滚动;元素多了能不绘制就不绘制。

当然在 canvas 中也有它的解法,比如缓存、分层、上 webgl 等等,这个在后续的优化章节中会专门讲到,所以敬请期待吧。不过还是要说一下,性能这东西,我觉得吧,一个普通页面一般是很少会遇到的,所以等遇到了再去考虑解决和优化也不迟,不然就属于过度优化了(没必要),不过在 canvas 中性能是个比较普遍的问题,你很容易写出卡卡的 canvas,所以我们还是有必要讲一讲的????。

小结

本个章节我们主要讲的是物体的一些变换操作,本来感觉应该是件很难的事情,但是归功于我们之前做了很好的结构划分,也就是将数据和渲染层分离,所以这一趴其实我们最核心的就是只改变了数据,其它什么都没变,这种感觉就像什么。。。那是数据驱动视图的味道,哈哈。扯犊子了,这里就简单总结下三种基本的操作吧:

  • 拖拽,计算新的 top、left
  • 旋转,计算新的 angle
  • 缩放,计算新的 scaleX、scaleY

其实三种变换操作的本质就是依托于鼠标坐标点的计算,啪,没了。


到此这篇关于“canvas交互怎样实现的拖拽、旋转、缩放效果”的文章就介绍到这了,更多相关内容请搜索群英网络以前的文章或继续浏览下面的相关文章,希望大家以后多多支持群英网络!
群英智防CDN,智能加速解决方案
标签: canvas

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:mmqy2019@163.com进行举报,并提供相关证据,查实之后,将立刻删除涉嫌侵权内容。

猜你喜欢

成为群英会员,开启智能安全云计算之旅

立即注册
专业资深工程师驻守
7X24小时快速响应
一站式无忧技术支持
免费备案服务
免费拨打  400-678-4567
免费拨打  400-678-4567 免费拨打 400-678-4567 或 0668-2555555
在线客服
微信公众号
返回顶部
返回顶部 返回顶部
在线客服
在线客服