一个不可行的解决方案(不想看的可以跳过)
一开始我想的是,先算出它相对浏览器视口的 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
属性不变)和大小算出组件中心点:
// 组件宽高比 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, } }
上面的公式涉及到线性代数中旋转矩阵的知识,对于一个没上过大学的人来说,实在太难了。还好我从知乎上的一个回答中找到了这一公式的推理过程,下面是回答的原文:
通过以上几个计算值,就可以得到组件新的位移值 top
left
以及新的组件大小。对应的完整代码如下:
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) } }
现在再来看一下旋转后的放大缩小:
第五步,由于我们现在需要的是锁定宽高比来进行放大缩小,所以需要重新计算拉伸后的图形的左上角坐标。
这里先确定好几个形状的命名:
- 原图形: 红色部分
- 新图形: 蓝色部分
- 修正图形: 绿色部分,即加上宽高比锁定规则的修正图形
在第四步中算出组件未旋转前的 newTopLeftPoint
newBottomRightPoint
newWidth
newHeight
后,需要根据宽高比 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 }