问题描述
假设我某个页面上使用了<svg>
,其中包括一个<circle>
。我希望实现的是:在circle上点击某个位置后,拖动,出现圆弧状阴影。实现效果为:
解决思路
要实现这个效果,需要考虑3个问题:
- 如何绘制圆弧阴影?
- 何时添加圆弧阴影?
- 在拖动时如何修改圆弧阴影的范围?
对于第1个问题,很明显我们需要使用<path>
,并且通过设置fill
、stroke
、stroke-width
和opacity
等属性定制圆弧阴影的外观,通过设置d
属性修改其位置和大小。
对于第2个问题,思路是这样的:首先,鼠标按下(mousedown
事件)时为<svg>
添加<path>
元素,同时设置外观参数,并将初始为false
的isDragging
属性设置为true
;当鼠标开始移动(mousemove
事件)时,如果isDragging == true
,那么说明是在拖动,此时计算d
属性的值(即第3个问题),并进行更新;当鼠标抬起(mouseup
事件)时,如果isDragging == true
,说明拖动已经结束,此时将isDragging
重新设置为false
,并做一些清理(如果需要的话)。
对于第3个问题,实际是绘制如图所示的图像(从(x0, y0)顺时针连线,r1r2是不同的半径):
此时<path>
的d
属性的值应该是这样的结构:
M x0 y0
L x1 y1
A r2 r2 a1 isLargeArc isOuterClockwise x2 y2
L x3 y3
A r1 r1 a2 isLargeArc isInnerClockwise x0 y0
Z
其中:
a 1 = θ 2 − θ 1 , a 2 = 360 − a 1 a1 = θ_2 - θ_1, a2 = 360 - a1 a1=θ2−θ1,a2=360−a1
isLargeArc表示是大弧(1)还是小弧(0),isOuterClockwise和isInnerClockwise表示外层圆弧和内侧圆弧是否为顺时针。
这里需要考虑的问题有:
3.1 如何获取θ1和当前θ2?如何通过θ计算圆弧上点的坐标?
这个在上一篇已经写过,此处不再赘述。javascript-svg-在圆环上加入闪烁光标-CSDN博客
3.2 如何判断圆弧阴影区域是大弧还是小弧?
由于绘制时可能出现跨越0点的情况,如下图所示:
所以考虑先判断绘制方向是顺时针还是逆时针。首先默认都是小弧,如果满足以下条件之一则为大弧:
a) θ1 > θ2、绘制方向为顺时针、(θ2 + 360 - θ1) > 180
b) θ1 < θ2、绘制方向为逆时针、(θ1 + 360 - θ2) > 180
c) 如不满足a和b,同时满足abs(θ2 - θ1) > 180
3.3 如何判断绘制方向是顺时针还是逆时针?
此时需要考虑最近两个θ的大小关系。假设有两个全局变量startAngle
和lastAngle
,以及一个标记方向的变量angleDirection
。在mousedown
事件中,先计算初始位置的θ,并赋值给startAngle
和lastAngle
;在mousemove
事件中,获得当前的θ(记作currentAngle
),并通过这个函数来判断方向:
function determineDirection(currentAngle) {
if (lastAngle != null) {
const diff = currentAngle - lastAngle;
if (diff > 0 && Math.abs(diff) < 180 || diff < -180) {
angleDirection = 1; // Clockwise
} else if (diff < 0 && Math.abs(diff) < 180 || diff > 180) {
angleDirection = 0; // Counterclockwise
}
}
lastAngle = currentAngle;
console.log(angleDirection > 0 ? "Clockwise" : "Counterclockwise");
}
最后在mouseup
事件中重置lastAngle
和angleDirection
。
3.4 如何计算isOuterClockwise和isInnerClockwise?
首先,由于绘制方向的问题,这俩一定是相反的。直接使用绘制方向是否为顺时针作为isOuterClockwise的值,isInnerClockwise只要与之相反即可。
代码结构
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<div>
<svg width="100%" height="1000" xmlns="http://www.w3.org/2000/svg">
<circle id="myCircle1" cx="600" cy="480" r="285" fill="#fff" stroke="#ccc" stroke-width="1" />
<!-- 先隐藏line -->
<line x1="1" y1="11" x2="2" y2="2" id="cursor" stroke="#1F2744" stroke-width="1" style="display:none;"></line>
</svg>
</div>
<script>
// 获取svg和circle
const svg = document.querySelector('svg');
const circle = document.getElementById('myCircle1');
let isDragging = false;
let arcPath = null;
let startAngle, lastAngle;
// 1 for clockwise, 0 for counterclockwise, -1 for none. 这是方便写svg
// 其实可以自定义
let angleDirection = 1;
// 添加监听
circle.addEventListener('mousedown', (e) => {
isDragging = true;
// 使用窗口中的圆心和点击点计算夹角,省略函数的实现
// 计算细节可看:https://blog.csdn.net/pxy7896/article/details/144256701
let angle = calculateCircleInfo(centerX, centerY, radius, clickX, clickY);
startAngle = angle;
lastAngle = startAngle;
// do something
// 添加path元素,并做一些设置
arcPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
arcPath.setAttribute("opacity", 0.3);
arcPath.setAttribute("fill", "blue");
arcPath.setAttribute("stroke", "blue");
arcPath.setAttribute("stroke-width", 1);
svg.appendChild(arcPath);
});
svg.addEventListener('mousemove', (e) => {
if (isDragging) {
// 使用窗口中的圆心和点击点计算夹角,省略函数的实现
// 判断绘制方向并计算d
arcPath.setAttribute("d", d);
}
});
svg.addEventListener('mouseup', (e) => {
if(isDragging) {
// 重置
isDragging = false;
lastAngle = null;
angleDirection = 1;
}
});
</script>
</body>
</html>