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 相关文章推荐
IE6下出现JavaScript未结束的字符串常量错误的解决方法
Nov 21 Javascript
JQuery切换显示的效果实例代码
Feb 27 Javascript
JS网页播放声音实现代码兼容各种浏览器
Sep 22 Javascript
推荐 21 款优秀的高性能 Node.js 开发框架
Aug 18 Javascript
JS简单实现多级Select联动菜单效果代码
Sep 06 Javascript
对js eval()函数的一些见解
Aug 15 Javascript
浅析js的模块化编写 require.js
Dec 07 Javascript
实例详解Vue项目使用eslint + prettier规范代码风格
Aug 20 Javascript
vue-cli项目修改文件热重载失效的解决方法
Sep 19 Javascript
angular.js实现列表orderby排序的方法
Oct 02 Javascript
原生javascript制作贪吃蛇小游戏的方法分析
Feb 26 Javascript
vue-drag-chart 拖动/缩放图表组件的实例代码
Apr 10 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
yii实现创建验证码实例解析
2014/07/31 PHP
PHP实现XML与数据格式进行转换类实例
2015/07/29 PHP
Win7环境下Apache连接MySQL提示连接已重置的解决办法
2017/05/09 PHP
php魔法函数与魔法常量使用介绍
2017/07/23 PHP
浅谈PHP中new self()和new static()的区别
2017/08/11 PHP
javascript 数组排序函数
2009/08/20 Javascript
csdn 论坛技术区平均给分功能
2009/11/07 Javascript
用js做一个小游戏平台 (一)
2009/12/29 Javascript
当滚动条滚动到页面底部自动加载增加内容的js代码
2014/05/13 Javascript
Javascript封装DOMContentLoaded事件实例
2014/06/12 Javascript
jQuery实现列表的全选功能
2015/03/18 Javascript
JQuery.validate在ie8下不支持的快速解决方法
2016/05/18 Javascript
Node.JS更改Windows注册表Regedit的方法小结
2017/08/18 Javascript
20170918 前端开发周报之JS前端开发必看
2017/09/18 Javascript
Vue实现active点击切换方法
2018/03/16 Javascript
js验证身份证号码记录的方法
2019/04/26 Javascript
JS原型与继承操作示例
2019/05/09 Javascript
js实现漂亮的星空背景
2019/11/01 Javascript
vue点击标签切换选中及互相排斥操作
2020/07/17 Javascript
关于python的bottle框架跨域请求报错问题的处理方法
2017/03/19 Python
Python2和Python3的共存和切换使用
2019/04/12 Python
Python3.8对可迭代解包的改进及用法详解
2019/10/15 Python
在Python中等距取出一个数组其中n个数的实现方式
2019/11/27 Python
python3.8下载及安装步骤详解
2020/01/15 Python
python实现从尾到头打印单链表操作示例
2020/02/22 Python
python中numpy数组与list相互转换实例方法
2021/01/29 Python
详解css3中dispaly的Grid布局与Flex布局
2020/09/11 HTML / CSS
美国半成品食材配送服务商:Home Chef
2018/01/25 全球购物
Carrs Silver官网:英国著名的银器品牌
2020/08/29 全球购物
100%羊绒:NakedCashmere
2020/08/26 全球购物
播音主持女孩的自我评价分享
2013/11/20 职场文书
项目开发计划书
2014/01/09 职场文书
触摸春天教学反思
2014/02/03 职场文书
西湖英语导游词
2015/02/06 职场文书
在职证明书模板
2015/06/15 职场文书
详解Redis复制原理
2021/06/04 Redis