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 相关文章推荐
JavaScript 定义function的三种方式小结
Oct 16 Javascript
JQuery获取文本框中字符长度的代码
Sep 29 Javascript
让元素在网页中可拖动示例代码
Aug 13 Javascript
基于jquery的9行js轻松实现tab控件示例
Oct 12 Javascript
javascript实现禁止复制网页内容汇总
Dec 30 Javascript
Node.js服务器环境下使用Mock.js拦截AJAX请求的教程
May 23 Javascript
JS实现的自定义水平滚动字体插件完整实例
Jun 17 Javascript
jQuery中JSONP的两种实现方式详解
Sep 26 Javascript
json2.js 入门教程之使用方法与实例分析
Sep 14 Javascript
seaJs使用心得之exports与module.exports的区别实例分析
Oct 13 Javascript
少女风vue组件库的制作全过程
May 15 Javascript
关于layui的动态图标不显示的解决方法
Sep 04 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安全防范技巧分享
2011/11/03 PHP
php伪静态之APACHE篇
2014/06/02 PHP
php实现字符串首字母大写和单词首字母大写的方法
2015/03/14 PHP
PHP 双链表(SplDoublyLinkedList)简介和使用实例
2015/05/12 PHP
Alliance vs AM BO3 第二场2.13
2021/03/10 DOTA
让innerHTML的脚本也可以运行起来
2006/07/01 Javascript
Prototype使用指南之dom.js
2007/01/10 Javascript
jQuery.getScript加载同域JS的代码
2012/02/13 Javascript
JavaScript面向对象(极简主义法minimalist approach)
2012/07/17 Javascript
js自定义事件及事件交互原理概述(一)
2013/02/01 Javascript
javaScript面向对象继承方法经典实现
2013/08/20 Javascript
JS实现窗口加载时模拟鼠标移动的方法
2015/06/03 Javascript
javascript排序函数实现数字排序
2015/06/26 Javascript
js正则表达式中exec用法实例
2015/07/23 Javascript
JavaScript实现的伸展收缩型菜单代码
2015/10/14 Javascript
js内置对象处理_打印学生成绩单的简单实现
2016/09/24 Javascript
如何正确理解javascript的模块化
2017/03/02 Javascript
jQuery插件FusionCharts绘制ScrollColumn2D图效果示例【附demo源码下载】
2017/03/22 jQuery
node.js中cluster的使用教程
2017/06/09 Javascript
Vue.js devtool插件安装后无法使用的解决办法
2017/11/27 Javascript
JavaScript原型继承和原型链原理详解
2020/02/04 Javascript
详解在Python程序中自定义异常的方法
2015/10/16 Python
Python+matplotlib实现华丽的文本框演示代码
2018/01/22 Python
python方法生成txt标签文件的实例代码
2018/05/10 Python
python实现微信每日一句自动发送给喜欢的人
2019/04/29 Python
Jupyter notebook如何修改平台字体
2020/05/13 Python
解决python3.x安装numpy成功但import出错的问题
2020/11/17 Python
Scholastic父母商店:儿童书籍
2017/01/01 全球购物
Superdry极度干燥美国官网:英国制造的服装品牌
2018/11/13 全球购物
数控专业大学生的自我鉴定
2013/11/13 职场文书
工作自我评价分享
2013/12/01 职场文书
关于中国梦的演讲稿
2014/04/23 职场文书
材料成型及控制工程专业求职信
2014/06/19 职场文书
大学生毕业个人总结
2015/02/15 职场文书
详解redis分布式锁的这些坑
2021/05/19 Redis
pytorch 梯度NAN异常值的解决方案
2021/06/05 Python