-
Notifications
You must be signed in to change notification settings - Fork 294
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
可视化拖拽组件库一些技术要点原理分析(三) #21
Comments
图裂了 |
1 similar comment
你这高中毕业学历实在是太屌了 |
Awsome works! |
mark 学习 |
您好,请问下我本地跑起来的项目,已经拖拽搭建好了一个表单页面,那我怎么拿到这个表单页面代码呢,望指教~ |
你得把组件数据保存起来,用一个渲染器项目渲染。可以参考一下预览部分的代码,那相当于一个渲染器。 |
感谢指教 |
楼主教程帮助很大,理清了许多的思路,希望后面有关于底层画布缩放的分享 |
如果我现在有一个自己的vue项目,怎么把你这个项目作为一个功能引入到已有项目中使用呢?谢谢! |
可以考虑一下 iframe |
请问没有出现旋转的那个图标是怎么回事呀,具体是在哪里操作的呢 |
用qiankun等微服务也可以 |
请教一下,为什么文字组件的删除快捷键不起作用呢?是在哪里有限制吗 |
你说的快捷键是哪个?目前可以全选然后按 delete 键删除 |
Vtext那个组件,选中之后,delete删除不起作用 |
是在 demo 里试的吗?什么系统、浏览器? |
也有在demo里面试过,Windows,然后用的edge浏览器 |
你用 chrome 试试看 |
|
你这组合不完美,要多个组件要完全在选框内才行,当有个组件的宽度或高度大于等于画布时,就永远不能框选中了 改一下这个函数就行
|
本文是可视化拖拽系列的第三篇,之前的两篇文章一共对 17 个功能点的技术原理进行了分析:
本文在此基础上,将对以下几个功能点的技术原理进行分析:
如果你对我之前的两篇文章不是很了解,建议先把这两篇文章看一遍,再来阅读此文:
虽然我这个可视化拖拽组件库只是一个 DEMO,但对比了一下市面上的一些现成产品(例如 processon、墨刀),就基础功能来说,我这个 DEMO 实现了绝大部分的功能。
如果你对于低代码平台有兴趣,但又不了解的话。强烈建议将我的三篇文章结合项目源码一起阅读,相信对你的收获绝对不小。另附上项目、在线 DEMO 地址:
18. 多个组件的组合和拆分
组合和拆分的技术点相对来说比较多,共有以下 4 个:
选中区域
在将多个组件组合之前,需要先选中它们。利用鼠标事件可以很方便的将选中区域展示出来:
mousedown
记录起点坐标mousemove
将当前坐标和起点坐标进行计算得出移动区域在
mouseup
事件触发时,需要对选中区域内的所有组件的位移大小信息进行计算,得出一个能包含区域内所有组件的最小区域。这个效果如下图所示:这个计算过程的代码:
简单描述一下这段代码的处理逻辑:
left
top
right
bottom
。Group
组合组件,则需要对它里面的子组件进行计算,而不是对组合组件进行计算。组合后的移动、旋转
为了方便将多个组件一起进行移动、旋转、放大缩小等操作,我新创建了一个
Group
组合组件:Group
组件的作用就是将区域内的组件放到它下面,成为子组件。并且在创建Group
组件时,获取每个子组件在Group
组件内的相对位移和相对大小:也就是将子组件的
left
top
width
height
等属性转成以%
结尾的相对数值。为什么不使用绝对数值?
如果使用绝对数值,那么在移动
Group
组件时,除了对Group
组件的属性进行计算外,还需要对它的每个子组件进行计算。并且Group
包含子组件太多的话,在进行移动、放大缩小时,计算量会非常大,有可能会造成页面卡顿。如果改成相对数值,则只需要在Group
创建时计算一次。然后在Group
组件进行移动、旋转时也不用管Group
的子组件,只对它自己计算即可。组合后的放大缩小
组合后的放大缩小是个大问题,主要是因为有旋转角度的存在。首先来看一下各个子组件没旋转时的放大缩小:
从动图可以看出,效果非常完美。各个子组件的大小是跟随
Group
组件的大小而改变的。现在试着给子组件加上旋转角度,再看一下效果:
为什么会出现这个问题?
主要是因为一个组件无论旋不旋转,它的
top
left
属性都是不变的。这样就会有一个问题,虽然实际上组件的top
left
width
height
属性没有变化。但在外观上却发生了变化。下面是两个同样的组件:一个没旋转,一个旋转了 45 度。可以看出来旋转后按钮的
top
left
width
height
属性和我们从外观上看到的是不一样的。接下来再看一个具体的示例:
上面是一个
Group
组件,它左边的子组件属性为:可以看到
width
的值为51.2267%
,但从外观上来看,这个子组件最多占Group
组件宽度的三分之一。所以这就是放大缩小不正常的问题所在。一个不可行的解决方案(不想看的可以跳过)
一开始我想的是,先算出它相对浏览器视口的
top
left
width
height
属性,再算出这几个属性在Group
组件上的相对数值。这可以通过getBoundingClientRect()
API 实现。只要维持外观上的各个属性占比不变,这样Group
组件在放大缩小时,再通过旋转角度,利用旋转矩阵的知识(这一点在第二篇有详细描述)获取它未旋转前的top
left
width
height
属性。这样就可以做到子组件动态调整了。但是这有个问题,通过
getBoundingClientRect()
API 只能获取组件外观上的top
left
right
bottom
width
height
属性。再加上一个角度,参数还是不够,所以无法计算出组件实际的top
left
width
height
属性。就像上面的这张图,只知道原点
O(x,y)
w
h
和旋转角度,无法算出按钮的宽高。一个可行的解决方案
这是无意中发现的,我在对
Group
组件进行放大缩小时,发现只要保持Group
组件的宽高比例,子组件就能做到根据比例放大缩小。那么现在问题就转变成了如何让Group
组件放大缩小时保持宽高比例。我在网上找到了这一篇文章,它详细描述了一个旋转组件如何保持宽高比来进行放大缩小,并配有源码示例。现在我尝试简单描述一下如何保持宽高比对一个旋转组件进行放大缩小(建议还是看看原文)。下面是一个已旋转一定角度的矩形,假设现在拖动它左上方的点进行拉伸。
第一步,算出组件宽高比,以及按下鼠标时通过组件的坐标(无论旋转多少度,组件的
top
left
属性不变)和大小算出组件中心点:第二步,用当前点击坐标和组件中心点算出当前点击坐标的对称点坐标:
第三步,摁住组件左上角进行拉伸时,通过当前鼠标实时坐标和对称点计算出新的组件中心点:
由于组件处于旋转状态,即使你知道了拉伸时移动的
xy
距离,也不能直接对组件进行计算。否则就会出现 BUG,移位或者放大缩小方向不正确。因此,我们需要在组件未旋转的情况下对其进行计算。第四步,根据已知的旋转角度、新的组件中心点、当前鼠标实时坐标可以算出当前鼠标实时坐标
currentPosition
在未旋转时的坐标newTopLeftPoint
。同时也能根据已知的旋转角度、新的组件中心点、对称点算出组件对称点sPoint
在未旋转时的坐标newBottomRightPoint
。对应的计算公式如下:
上面的公式涉及到线性代数中旋转矩阵的知识,对于一个没上过大学的人来说,实在太难了。还好我从知乎上的一个回答中找到了这一公式的推理过程,下面是回答的原文:
通过以上几个计算值,就可以得到组件新的位移值
top
left
以及新的组件大小。对应的完整代码如下:现在再来看一下旋转后的放大缩小:
第五步,由于我们现在需要的是锁定宽高比来进行放大缩小,所以需要重新计算拉伸后的图形的左上角坐标。
这里先确定好几个形状的命名:
在第四步中算出组件未旋转前的
newTopLeftPoint
newBottomRightPoint
newWidth
newHeight
后,需要根据宽高比proportion
来算出新的宽度或高度。上图就是一个需要改变高度的示例,计算过程如下:
由于现在求的未旋转前的坐标是以没按比例缩减宽高前的坐标来计算的,所以缩减宽高后,需要按照原来的中心点旋转回去,获得缩减宽高并旋转后对应的坐标。然后以这个坐标和对称点获得新的中心点,并重新计算未旋转前的坐标。
经过修改后的完整代码如下:
保持宽高比进行放大缩小的效果如下:
当
Group
组件有旋转的子组件时,才需要保持宽高比进行放大缩小。所以在创建Group
组件时可以判断一下子组件是否有旋转角度。如果没有,就不需要保持宽度比进行放大缩小。拆分后子组件样式的恢复
将多个组件组合在一起只是第一步,第二步是将
Group
组件进行拆分并恢复各个子组件的样式。保证拆分后的子组件在外观上的属性不变。计算代码如下:
这段代码的处理逻辑为:
Group
的子组件并恢复它们的样式getBoundingClientRect()
API 获取子组件相对于浏览器视口的left
top
width
height
属性。width
height
属性是相对于Group
组件的,所以将它们的百分比值和Group
相乘得出具体数值。center(x, y)
减去子组件宽高的一半得出它的left
top
属性。至此,组合和拆分就讲解完了。
19. 文本组件
文本组件
VText
之前就已经实现过了,但不完美。例如无法对文字进行选中。现在我对它进行了重写,让它支持选中功能。改造后的
VText
组件功能如下:20. 矩形组件
矩形组件其实就是一个内嵌
VText
文本组件的一个 DIV。VText
文本组件有的功能它都有,并且可以任意放大缩小。21. 锁定组件
锁定组件主要是看到
processon
和墨刀有这个功能,于是我顺便实现了。锁定组件的具体需求为:不能移动、放大缩小、旋转、复制、粘贴等,只能进行解锁操作。它的实现原理也不难:
isLock
属性,表示是否锁定组件。isLock
是否为true
来隐藏组件上的八个点和旋转图标。相关代码如下:
22. 快捷键
支持快捷键主要是为了提升开发效率,用鼠标点点点毕竟没有按键盘快。目前快捷键支持的功能如下:
实现原理主要是利用 window 全局监听按键事件,在符合条件的按键触发时执行对应的操作:
为了防止和浏览器默认快捷键冲突,所以需要加上
e.preventDefault()
。23. 网格线
网格线功能使用 SVG 来实现:
对 SVG 不太懂的,建议看一下 MDN 的教程。
24. 编辑器快照的另一种实现方式
在系列文章的第一篇中,我已经分析过快照的实现原理。
用一个数组来保存编辑器的快照数据。保存快照就是不停地执行
push()
操作,将当前的编辑器数据推入snapshotData
数组,并增加快照索引snapshotIndex
。由于每一次添加快照都是将当前编辑器的所有组件数据推入
snapshotData
,保存的快照数据越多占用的内存就越多。对此有两个解决方案:现在详细描述一下第二个解决方案。
假设依次往画布上添加 a b c d 四个组件,在原来的实现中,对应的
snapshotData
数据为:从上面的代码可以发现,每一相邻的快照中,只有一个数据是不同的。所以我们可以为每一步的快照添加一个类型字段,用来表示此次操作是添加还是删除。
那么上面添加四个组件的操作,所对应的
snapshotData
数据为:如果我们要删除 c 组件,那么
snapshotData
数据将变为:那如何使用现在的快照数据呢?
我们需要遍历一遍快照数据,来生成编辑器的组件数据
componentData
。假设在上面的数据基础上执行了undo
撤销操作:snapshotData[0]
类型为add
,将组件 a 添加到componentData
中,此时componentData
为[a]
[a, b]
[a, b, c]
[a, b, c, d]
如果这时执行
redo
重做操作,快照索引snapshotIndex
变为 4。对应的快照数据类型为type: 'remove'
, 移除组件 c。则数组数据为[a, b, d]
。这种方法其实就是时间换空间,虽然每一次保存的快照数据只有一项,但每次都得遍历一遍所有的快照数据。两种方法都不完美,要使用哪种取决于你,目前我仍在使用第一种方法。
总结
从造轮子的角度来看,这是我目前造的第四个比较满意的轮子,其他三个为:
造轮子是一个很好的提升自己技术水平的方法,但造轮子一定要造有意义、有难度的轮子,并且同类型的轮子只造一个。造完轮子后,还需要写总结,最好输出成文章分享出去。
参考资料
The text was updated successfully, but these errors were encountered: