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 相关文章推荐
js实现GridView单选效果自动设置交替行、选中行、鼠标移动行背景色
May 27 Javascript
jquery模拟按下回车实现代码
Sep 20 Javascript
jQuery链使用指南
Jan 20 Javascript
JS获取下拉框显示值和判断单选按钮的方法
Jul 09 Javascript
jquery实现简单的瀑布流布局
Dec 11 Javascript
Bootstrap3.3.7导航栏下拉菜单鼠标滑过展开效果
Oct 31 Javascript
使用Angular Cli如何创建Angular私有库详解
Jan 30 Javascript
微信小程序聊天功能的示例代码
Jan 13 Javascript
js判断鼠标移入移出方向的方法
Jun 24 Javascript
Vue-CLI 3 scp2自动部署项目至服务器的方法
Jul 24 Javascript
vue使用screenfull插件实现全屏功能
Sep 17 Javascript
二维码条形码生成的JavaScript脚本库
Jul 07 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
一个程序下载的管理程序(四)
2006/10/09 PHP
刚才在简化php的库,结果发现很多东西
2006/12/31 PHP
php输出echo、print、print_r、printf、sprintf、var_dump的区别比较
2013/06/21 PHP
Codeigniter框架的更新事务(transaction)BUG及解决方法
2014/07/25 PHP
分享一段PHP制作的中文拼音首字母工具类
2014/12/11 PHP
php中实现可以返回多个值的函数实例
2015/03/21 PHP
php之static静态属性与静态方法实例分析
2015/07/30 PHP
PHP判断上传文件类型的解决办法
2015/10/20 PHP
jQuery数组处理代码详解(含实例演示)
2012/02/03 Javascript
JavaScript代码复用模式实例分析
2012/12/02 Javascript
javascript生成不重复的随机数
2015/07/17 Javascript
Javascript中作用域的详细介绍
2016/10/06 Javascript
React/Redux应用使用Async/Await的方法
2017/11/16 Javascript
jQuery解析json格式数据示例
2018/09/01 jQuery
JavaScript判断浏览器运行环境的详细方法
2019/06/30 Javascript
Vue.js 无限滚动列表性能优化方案
2019/12/02 Javascript
Python 闭包的使用方法
2017/09/07 Python
python爬虫获取百度首页内容教学
2018/12/23 Python
使用wxpy实现自动发送微信消息功能
2020/02/28 Python
Django框架配置mysql数据库实现过程
2020/04/22 Python
python中如何进行连乘计算
2020/05/28 Python
Android Q之气泡弹窗的实现示例
2020/06/23 Python
读取nii或nii.gz文件中的信息即输出图像操作
2020/07/01 Python
HTML5实现的图片无限加载的瀑布流效果另带边框圆角阴影
2014/03/07 HTML / CSS
State Cashmere官网:半零售价可持续蒙古羊绒
2020/02/26 全球购物
应届生程序员求职信
2013/11/05 职场文书
工作失误检讨书范文大全
2014/01/13 职场文书
审计主管岗位职责
2014/01/31 职场文书
幼儿园大班开学教师寄语
2014/04/03 职场文书
经典的毕业生自荐信范文
2014/04/14 职场文书
农村党员对照检查材料
2014/09/24 职场文书
2014年卫生保健工作总结
2014/12/08 职场文书
导游词300字
2015/02/13 职场文书
总结Python变量的相关知识
2021/06/28 Python
Redis主从配置和底层实现原理解析(实战记录)
2021/06/30 Redis
分享提高 Python 代码的可读性的技巧
2022/03/03 Python