到目前为止,我们已经能够对物体进行点选和框选的操作了,但是这还不够,因为并没有什么实际性的改变,并且画布看起来也有点呆板,所以这个章节的主要目的就是让画布中的物体活起来,其实就是增加一些常见的交互而已啦,比如拖拽、旋转和缩放。这是这个系列最重要的章节之一,希望能够对你有所帮助。
先来说说拖拽平移的实现吧,因为它最为简单。我们知道每个物体都是有 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,怎么算这个值会方便点呢?可以将拉伸的变换基点暂时变为左边中间的控制点,也就是左边的蓝点(这个很重要),这样计算当前宽度的时候就会比较方便了:
这里也简单贴下核心代码:
/**
* 缩放当前选中物体
* @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,所以我们还是有必要讲一讲的????。
本个章节我们主要讲的是物体的一些变换操作,本来感觉应该是件很难的事情,但是归功于我们之前做了很好的结构划分,也就是将数据和渲染层分离,所以这一趴其实我们最核心的就是只改变了数据,其它什么都没变,这种感觉就像什么。。。那是数据驱动视图的味道,哈哈。扯犊子了,这里就简单总结下三种基本的操作吧:
其实三种变换操作的本质就是依托于鼠标坐标点的计算,啪,没了。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:mmqy2019@163.com进行举报,并提供相关证据,查实之后,将立刻删除涉嫌侵权内容。
长按识别二维码并关注微信
更方便到期提醒、手机管理