可视化拖拽组件库一些技术要点原理分析(三)(二)

简介: 可视化拖拽组件库一些技术要点原理分析(三)(二)

一个不可行的解决方案(不想看的可以跳过)

一开始我想的是,先算出它相对浏览器视口的 topleftwidthheight 属性,再算出这几个属性在 Group 组件上的相对数值。这可以通过 getBoundingClientRect() API 实现。只要维持外观上的各个属性占比不变,这样 Group 组件在放大缩小时,再通过旋转角度,利用旋转矩阵的知识(这一点在第二篇有详细描述)获取它未旋转前的 topleftwidthheight 属性。这样就可以做到子组件动态调整了。

但是这有个问题,通过 getBoundingClientRect() API 只能获取组件外观上的 topleftrightbottomwidthheight 属性。再加上一个角度,参数还是不够,所以无法计算出组件实际的 topleftwidthheight 属性。

就像上面的这张图,只知道原点 O(x,y)wh 和旋转角度,无法算出按钮的宽高。

一个可行的解决方案

这是无意中发现的,我在对 Group 组件进行放大缩小时,发现只要保持 Group 组件的宽高比例,子组件就能做到根据比例放大缩小。那么现在问题就转变成了如何让 Group 组件放大缩小时保持宽高比例。我在网上找到了这一篇文章,它详细描述了一个旋转组件如何保持宽高比来进行放大缩小,并配有源码示例。

现在我尝试简单描述一下如何保持宽高比对一个旋转组件进行放大缩小(建议还是看看原文)。下面是一个已旋转一定角度的矩形,假设现在拖动它左上方的点进行拉伸。

第一步,算出组件宽高比,以及按下鼠标时通过组件的坐标(无论旋转多少度,组件的 topleft 属性不变)和大小算出组件中心点:

// 组件宽高比
const proportion = style.width / style.height
const center = {
    x: style.left + style.width / 2,
    y: style.top + style.height / 2,
}

第二步,用当前点击坐标和组件中心点算出当前点击坐标的对称点坐标:

// 获取画布位移信息
const editorRectInfo = document.querySelector('#editor').getBoundingClientRect()
// 当前点击坐标
const curPoint = {
    x: e.clientX - editorRectInfo.left,
    y: e.clientY - editorRectInfo.top,
}
// 获取对称点的坐标
const symmetricPoint = {
    x: center.x - (curPoint.x - center.x),
    y: center.y - (curPoint.y - center.y),
}

第三步,摁住组件左上角进行拉伸时,通过当前鼠标实时坐标和对称点计算出新的组件中心点:

const curPositon = {
    x: moveEvent.clientX - editorRectInfo.left,
    y: moveEvent.clientY - editorRectInfo.top,
}
const newCenterPoint = getCenterPoint(curPositon, symmetricPoint)
// 求两点之间的中点坐标
function getCenterPoint(p1, p2) {
    return {
        x: p1.x + ((p2.x - p1.x) / 2),
        y: p1.y + ((p2.y - p1.y) / 2),
    }
}

由于组件处于旋转状态,即使你知道了拉伸时移动的 xy 距离,也不能直接对组件进行计算。否则就会出现 BUG,移位或者放大缩小方向不正确。因此,我们需要在组件未旋转的情况下对其进行计算。

第四步,根据已知的旋转角度、新的组件中心点、当前鼠标实时坐标可以算出当前鼠标实时坐标currentPosition 在未旋转时的坐标 newTopLeftPoint。同时也能根据已知的旋转角度、新的组件中心点、对称点算出组件对称点sPoint 在未旋转时的坐标 newBottomRightPoint

对应的计算公式如下:

/**
 * 计算根据圆心旋转后的点的坐标
 * @param   {Object}  point  旋转前的点坐标
 * @param   {Object}  center 旋转中心
 * @param   {Number}  rotate 旋转的角度
 * @return  {Object}         旋转后的坐标
 * https://www.zhihu.com/question/67425734/answer/252724399 旋转矩阵公式
 */
export function calculateRotatedPointCoordinate(point, center, rotate) {
    /**
     * 旋转公式:
     *  点a(x, y)
     *  旋转中心c(x, y)
     *  旋转后点n(x, y)
     *  旋转角度θ                tan ??
     * nx = cosθ * (ax - cx) - sinθ * (ay - cy) + cx
     * ny = sinθ * (ax - cx) + cosθ * (ay - cy) + cy
     */
    return {
        x: (point.x - center.x) * Math.cos(angleToRadian(rotate)) - (point.y - center.y) * Math.sin(angleToRadian(rotate)) + center.x,
        y: (point.x - center.x) * Math.sin(angleToRadian(rotate)) + (point.y - center.y) * Math.cos(angleToRadian(rotate)) + center.y,
    }
}

上面的公式涉及到线性代数中旋转矩阵的知识,对于一个没上过大学的人来说,实在太难了。还好我从知乎上的一个回答中找到了这一公式的推理过程,下面是回答的原文:

通过以上几个计算值,就可以得到组件新的位移值 topleft 以及新的组件大小。对应的完整代码如下:

function calculateLeftTop(style, curPositon, pointInfo) {
    const { symmetricPoint } = pointInfo
    const newCenterPoint = getCenterPoint(curPositon, symmetricPoint)
    const newTopLeftPoint = calculateRotatedPointCoordinate(curPositon, newCenterPoint, -style.rotate)
    const newBottomRightPoint = calculateRotatedPointCoordinate(symmetricPoint, newCenterPoint, -style.rotate)
    const newWidth = newBottomRightPoint.x - newTopLeftPoint.x
    const newHeight = newBottomRightPoint.y - newTopLeftPoint.y
    if (newWidth > 0 && newHeight > 0) {
        style.width = Math.round(newWidth)
        style.height = Math.round(newHeight)
        style.left = Math.round(newTopLeftPoint.x)
        style.top = Math.round(newTopLeftPoint.y)
    }
}

现在再来看一下旋转后的放大缩小:

第五步,由于我们现在需要的是锁定宽高比来进行放大缩小,所以需要重新计算拉伸后的图形的左上角坐标。

这里先确定好几个形状的命名:

  • 原图形:  红色部分
  • 新图形:  蓝色部分
  • 修正图形: 绿色部分,即加上宽高比锁定规则的修正图形

在第四步中算出组件未旋转前的 newTopLeftPointnewBottomRightPointnewWidthnewHeight 后,需要根据宽高比 proportion 来算出新的宽度或高度。

上图就是一个需要改变高度的示例,计算过程如下:

if (newWidth / newHeight > proportion) {
    newTopLeftPoint.x += Math.abs(newWidth - newHeight * proportion)
    newWidth = newHeight * proportion
} else {
    newTopLeftPoint.y += Math.abs(newHeight - newWidth / proportion)
    newHeight = newWidth / proportion
}

由于现在求的未旋转前的坐标是以没按比例缩减宽高前的坐标来计算的,所以缩减宽高后,需要按照原来的中心点旋转回去,获得缩减宽高并旋转后对应的坐标。然后以这个坐标和对称点获得新的中心点,并重新计算未旋转前的坐标。

经过修改后的完整代码如下:

function calculateLeftTop(style, curPositon, proportion, needLockProportion, pointInfo) {
    const { symmetricPoint } = pointInfo
    let newCenterPoint = getCenterPoint(curPositon, symmetricPoint)
    let newTopLeftPoint = calculateRotatedPointCoordinate(curPositon, newCenterPoint, -style.rotate)
    let newBottomRightPoint = calculateRotatedPointCoordinate(symmetricPoint, newCenterPoint, -style.rotate)
    let newWidth = newBottomRightPoint.x - newTopLeftPoint.x
    let newHeight = newBottomRightPoint.y - newTopLeftPoint.y
    if (needLockProportion) {
        if (newWidth / newHeight > proportion) {
            newTopLeftPoint.x += Math.abs(newWidth - newHeight * proportion)
            newWidth = newHeight * proportion
        } else {
            newTopLeftPoint.y += Math.abs(newHeight - newWidth / proportion)
            newHeight = newWidth / proportion
        }
        // 由于现在求的未旋转前的坐标是以没按比例缩减宽高前的坐标来计算的
        // 所以缩减宽高后,需要按照原来的中心点旋转回去,获得缩减宽高并旋转后对应的坐标
        // 然后以这个坐标和对称点获得新的中心点,并重新计算未旋转前的坐标
        const rotatedTopLeftPoint = calculateRotatedPointCoordinate(newTopLeftPoint, newCenterPoint, style.rotate)
        newCenterPoint = getCenterPoint(rotatedTopLeftPoint, symmetricPoint)
        newTopLeftPoint = calculateRotatedPointCoordinate(rotatedTopLeftPoint, newCenterPoint, -style.rotate)
        newBottomRightPoint = calculateRotatedPointCoordinate(symmetricPoint, newCenterPoint, -style.rotate)
        newWidth = newBottomRightPoint.x - newTopLeftPoint.x
        newHeight = newBottomRightPoint.y - newTopLeftPoint.y
    }
    if (newWidth > 0 && newHeight > 0) {
        style.width = Math.round(newWidth)
        style.height = Math.round(newHeight)
        style.left = Math.round(newTopLeftPoint.x)
        style.top = Math.round(newTopLeftPoint.y)
    }
}

保持宽高比进行放大缩小的效果如下:

Group 组件有旋转的子组件时,才需要保持宽高比进行放大缩小。所以在创建 Group 组件时可以判断一下子组件是否有旋转角度。如果没有,就不需要保持宽度比进行放大缩小。

isNeedLockProportion() {
    if (this.element.component != 'Group') return false
    const ratates = [0, 90, 180, 360]
    for (const component of this.element.propValue) {
        if (!ratates.includes(mod360(parseInt(component.style.rotate)))) {
            return true
        }
    }
    return false
}
目录
相关文章
|
6月前
|
前端开发 算法 数据可视化
可拖拽流程图组件开发
可拖拽流程图组件开发
117 0
|
2月前
|
JavaScript
从零开始写一套广告组件【一】搭建基础框架并配置UI组件库
其实这个从零有点歧义,因为本质上是要基于`tdesign-vue-next`来进行二次封装为一套广告UI组件库,现在让我们在一起快乐的搭建自己的广告UI库之前,先对以下内容做出共识:
80 0
从零开始写一套广告组件【一】搭建基础框架并配置UI组件库
|
3月前
|
C# 开发者 数据处理
WPF开发者必备秘籍:深度解析数据网格最佳实践,轻松玩转数据展示与编辑大揭秘!
【8月更文挑战第31天】数据网格控件是WPF应用程序中展示和编辑数据的关键组件,提供排序、筛选等功能,显著提升用户体验。本文探讨WPF中数据网格的最佳实践,通过DevExpress DataGrid示例介绍其集成方法,包括添加引用、定义数据模型及XAML配置。通过遵循数据绑定、性能优化、自定义列等最佳实践,可大幅提升数据处理效率和用户体验。
61 0
|
4月前
|
小程序 前端开发
【微信小程序-原生开发】实用教程22 - 绘制图表(引入 echarts,含图表的懒加载-获取到数据后再渲染图表,多图表加载等技巧)
【微信小程序-原生开发】实用教程22 - 绘制图表(引入 echarts,含图表的懒加载-获取到数据后再渲染图表,多图表加载等技巧)
246 0
|
数据可视化 JavaScript
可视化拖拽组件库一些技术要点原理分析(二)(上)
可视化拖拽组件库一些技术要点原理分析(二)
100 1
|
数据可视化 索引
可视化拖拽组件库一些技术要点原理分析(二)
可视化拖拽组件库一些技术要点原理分析(二)
148 0
|
6月前
|
API
【鸿蒙软件开发】ArkTS基础组件之Gauge(环形图表)、LoadingProgress(动态加载)
【鸿蒙软件开发】ArkTS基础组件之Gauge(环形图表)、LoadingProgress(动态加载)
376 0
|
数据可视化 JavaScript
可视化拖拽组件库一些技术要点原理分析(四)(上)
可视化拖拽组件库一些技术要点原理分析(四)
94 0
可视化拖拽组件库一些技术要点原理分析(四)(上)
|
数据可视化 API
可视化拖拽组件库一些技术要点原理分析(三)(一)
可视化拖拽组件库一些技术要点原理分析(三)
97 0
|
数据可视化 前端开发 JavaScript
可视化拖拽组件库一些技术要点原理分析(四)(下)
可视化拖拽组件库一些技术要点原理分析(四)(下)
74 0