JS前端canvas交互实现拖拽旋转及缩放示例


Posted in Javascript onAugust 05, 2022

正文

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

拖拽

先来说说拖拽平移的实现吧,因为它最为简单?。我们知道每个物体都是有 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 是什么东西,它就是一开始我们按下鼠标时记录的一些初始信息,大概长下面这个样子,看看就行,有个印象即可??:

JS前端canvas交互实现拖拽旋转及缩放示例

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

旋转

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

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

JS前端canvas交互实现拖拽旋转及缩放示例

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

JS前端canvas交互实现拖拽旋转及缩放示例

通常我们会用 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;
}

缩放

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

JS前端canvas交互实现拖拽旋转及缩放示例

大家仔细看上图中右边中间红色的那个控制点,它的缩放结果其实是就沿着 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

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

然后这里是简版 fabric.js 的代码链接,有兴趣的可以看看。好啦,本次分享就到这里

canvas 中如何实现物体的框选(六)?

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

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

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

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

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

更多关于JS前端canvas交互的资料请关注三水点靠木其它相关文章!

Javascript 相关文章推荐
幻宇的层模拟窗口效果-提供演示和下载
Jan 20 Javascript
监控 url fragment变化的js代码
Apr 19 Javascript
jquery解析JSON数据示例代码
Mar 17 Javascript
JavaScript加入收藏夹功能(兼容IE、firefox、chrome)
May 05 Javascript
浅谈使用MVC模式进行JavaScript程序开发
Nov 10 Javascript
复杂的javascript窗口分帧解析
Feb 19 Javascript
从零开始学习Node.js系列教程六:EventEmitter发送和接收事件的方法示例
Apr 13 Javascript
详解如何使用webpack打包Vue工程
May 27 Javascript
js排序与重组的实例讲解
Aug 28 Javascript
基于axios封装fetch方法及调用实例
Feb 05 Javascript
35个最好用的Vue开源库(史上最全)
Jan 03 Javascript
JavaScript进制转换实现方法解析
Jan 18 Javascript
canvas 中如何实现物体的框选
Aug 05 #Javascript
JS前端使用canvas实现物体的点选示例
Aug 05 #Javascript
前端canvas中物体边框和控制点的实现示例
Aug 05 #Javascript
JS前端轻量fabric.js系列物体基类
Aug 05 #Javascript
JS前端轻量fabric.js系列之画布初始化
Aug 05 #Javascript
vue3 自定义图片放大器效果的示例代码
Jul 23 #Vue.js
JavaScript parseInt0.0000005打印5原理解析
Jul 23 #Javascript
You might like
PHP也可以?成Shell Script
2006/10/09 PHP
PHP遍历二维数组的代码
2011/04/22 PHP
探寻PHP脚本不报错的原因
2014/06/12 PHP
php实现的树形结构数据存取类实例
2014/11/29 PHP
php中请求url的五种方法总结
2017/07/13 PHP
12个非常有创意的JavaScript小游戏
2010/03/18 Javascript
让你的CSS像Jquery一样做筛选的实现方法
2011/07/10 Javascript
页面加载完后自动执行一个方法的js代码
2014/09/06 Javascript
javascript中mouseover、mouseout使用详解
2015/07/19 Javascript
jQuery UI设置固定日期选择特效代码分享
2015/08/27 Javascript
微信小程序movable view移动图片和双指缩放实例代码
2017/08/08 Javascript
JS数组Object.keys()方法的使用示例
2019/06/05 Javascript
vue中element 的upload组件发送请求给后端操作
2020/09/07 Javascript
[01:18]DOTA2超级联赛专访hanci ForLove淘汰感言曝光
2013/06/04 DOTA
python寻找list中最大值、最小值并返回其所在位置的方法
2018/06/27 Python
Python二叉搜索树与双向链表转换算法示例
2019/03/02 Python
pandas DataFrame的修改方法(值、列、索引)
2019/08/02 Python
python时间与Unix时间戳相互转换方法详解
2020/02/13 Python
python实现门限回归方式
2020/02/29 Python
Python greenlet和gevent使用代码示例解析
2020/04/01 Python
python实现简单的五子棋游戏
2020/09/01 Python
网络工程与软件技术毕业生自荐信
2013/09/24 职场文书
建筑设计师岗位职责
2013/11/18 职场文书
质量月活动策划方案
2014/03/10 职场文书
学生手册评语
2014/05/05 职场文书
党的群众路线教育实践活动心得体会(乡镇)
2014/11/03 职场文书
2014幼儿园家长工作总结
2014/11/10 职场文书
检讨书格式
2015/01/23 职场文书
2015年护士节活动策划方案
2015/05/04 职场文书
2015年幼儿园国庆节活动总结
2015/07/30 职场文书
2019XX公司员工考核管理制度!
2019/08/07 职场文书
Django如何创作一个简单的最小程序
2021/05/12 Python
pytorch 梯度NAN异常值的解决方案
2021/06/05 Python
用Python创建简易网站图文教程
2021/06/11 Python
Java 多线程协作作业之信号同步
2022/05/11 Java/Android
Redis高并发缓存架构性能优化
2022/05/15 Redis