canvas绘制拖拽矩形那些事2
引子
前面写过一篇拖拽矩形的文章,里面详细写了矩形绘制的实现逻辑,对于拖拽变形只是在最后代码里表现了下,并没有具体说明,因为当时实现是非常简单的,拖拽一边变化d,另外一边也相应的变化d,中心点不变所以不管是在旋转后还是未旋转的状态,都不会有问题,但后来更改需求上下拖拽实现单边变化……
拖拽距离 Distance 到底是多少?
双边变动情况
/**
* originalPoint:为了方便计算旋转后的矩形伸缩偏移量,需要先计算旋转前的point坐标;
* lineIntersection: 经过centerPoint和point的水平线/垂线的交点;
* distance:centerPoint 和 lineIntersection 的距离;
* **/
const originalPoint = calculatePointBeforeRotate(centerPoint, point, rectInfo.rotate)
const lineIntersection = isMouseAngleDown.value === "topLine" || isMouseAngleDown.value === "bottomLine" ?
[centerPoint[0], originalPoint[1]] as Point :
[originalPoint[0], centerPoint[1]] as Point
const distance = calculateDistance(centerPoint, lineIntersection )
if (isMouseAngleDown.value === "topLine") {
// 限制最小高度为10,最大高度为image的高度,矩形左上角点位不能移出image范围
if (distance * 2 <= bound.height && distance * 2 >= 10 && (originalPoint[1] >= bound.top && originalPoint[1] <= centerPoint[1])) {
rectInfo.top = originalPoint[1]
rectInfo.height = distance * 2
}
}
之前的想法很简单,就是把旋转后的矩形反推回未旋转的状态,根据 isMouseAngleDown.value
的值,决定计算的线段交点是水平线(topLine
或 bottomLine
)还是垂直线。
如果是水平线,交点的 x 坐标与 centerPoint
的 x 坐标相同,而 y 坐标与 originalPoint
的 y 坐标相同;
如果是垂直线,交点的 y 坐标与 centerPoint
的 y 坐标相同,而 x 坐标与 originalPoint
的 x 坐标相同。
当前线段 distance 根据欧几里得距离公式计算出来,在 “topline”情况下,distance 就是新矩形的高度一半,因为整个矩形的绘制是根据左上角点和宽高绘制的,中心点不会变动,所以只需要改左上角点的y和高度即可。
单边拉动
如果是拖动一边,对边保持不变的话,那就是左上角的 y
还是等于 originalPoint[1]
,但是rectInfo.height = distance + react.height / 2
,然后重新推算出中心点:
rectInfo.top = originalPoint[1]
rectInfo.height = distance + rectInfo.height / 2
rectInfo.centerPoint = [
rectInfo.width / 2 + rectInfo.left,
rectInfo.height / 2 + rectInfo.top
]
如果rotate
为0情况下,上述写法是没什么问题的,但只要是旋转后,再次拖动就会发生偏移。
朋友说我 d 算的有问题,预期不对认怂,那就换种方式求。
几何求法
假设两条平行直线 L1和 L2 的倾斜角度为 α,经过点 A(x1,y1) 和 B(x2,y2,并且两条直线的方程分别为:
- L1:y=kx+b1
- L2:y=kx+b2
找到直线 L1 和 L2 的截距
直线 L1和 L2 的截距分别为:
b1=y1−kx1
b2=y2−kx2
计算两条直线的截距差
Δb=∣b2−b1∣
利用几何方法计算垂直距离
两条平行直线的垂直距离可以通过截距差与直线倾斜角度的余弦值相乘来计算:
d=Δb⋅cos(α)
/**
* 计算两条平行直线之间的垂直距离
* @param {number} x1 - 点A的x坐标
* @param {number} y1 - 点A的y坐标
* @param {number} x2 - 点B的x坐标
* @param {number} y2 - 点B的y坐标
* @param {number} alpha - 倾斜角度(度数)
* @returns {number} - 两条直线之间的垂直距离
*/
function calculateParallelLinesDistanceGeometrically(x1, y1, x2, y2, alpha) {
const alphaRadians = (alpha * Math.PI) / 180;
const k = Math.tan(alphaRadians);
const b1 = y1 - k * x1;
const b2 = y2 - k * x2;
const deltaB = Math.abs(b2 - b1);
const distance = deltaB * Math.cos(alphaRadians);
return distance;
}
根据拖拽距离反推新的左上角点
const offsetX = Math.sin(rectInfo.rotate * Math.PI / 180) * d
const offsetY = Math.cos(rectInfo.rotate * Math.PI / 180) * d
// 推算新的左上角点
reactInfo.left = reactInfo.left - offsetX
reactInfo.top = reactInfo.top + offsetX
reactInfo.height = reactInfo.height - d
// 求中心点
rectInfo.centerPoint = [
rectInfo.width / 2 + rectInfo.left,
rectInfo.height / 2 + rectInfo.top
]
最后一通折腾,表现出来的预期跟我一开始的方案一毛一样,啥也不是。
最后解决方案
折腾了好久发现预期达不到,开始从头分析到底是哪里出现了问题,我先固定了倾斜角度,写死了每拉动一下移动距离,一步步的来找可能出现的问题。
推算出来的中心点坐标不对
我通过旋转45度来定位的问题,按道理左上角的点(x,y)的位移都是一样的只不过是正负的问题,求出来的中心点跟之前比也是二分之一的xy的偏移量,但是如果先计算left、top,就是先求左上角的xy的话,left+offsetX,top+offsetY,然后在反求中心点
rectInfo.centerPoint = [
rectInfo.width / 2 + rectInfo.left,
rectInfo.height / 2 + rectInfo.top
]
// 原始中心点[256, 256],因为是顺时针旋转了45度,向下移动30,预期应该是[245?,266?]左右,但实际推出来的[234.78679656440357,262.2132034355964],这就有点离谱了
{
"width": 384,
"height": 226,
"left": 42.78679656440357,
"top": 149.21320343559643,
"rotate": 45,
"imageSize": 30,
"lockType": "sliceThickNess",
"interval": 8.533333333333333,
"centerPoint": [
234.78679656440357,
262.2132034355964
],
"pixcelSpacing": 6
}
如果先求中心点centerPoint然后再反推左上角点left、top,算出来的centerPoint符合预期,left、top就很诡异了,但在canvas绘制的矩形符合预期。
rectInfo.centerPoint = [
rectInfo.centerPoint[0] - offsetX / 2,
rectInfo.centerPoint[1] + offsetY / 2
]
rectInfo.height = rectInfo.height - d
rectInfo.left = rectInfo.centerPoint[0] - rectInfo.width / 2
rectInfo.top = rectInfo.centerPoint[1] - rectInfo.height / 2