需求背景
在canvas中绘制矩形时,当激活一个矩形时,需要将被覆盖区域的边框用虚线绘制,绘制效果图如下:
算法实现:
type Rectangle = { x: number, y: number, width: number, height: number }
type Edge = { x1: number, y1: number, x2: number, y2: number }
/**
* 获取目标矩形的未被覆盖的边框和被覆盖的边框
* @param targetRect - 目标矩形
* @param rectangles - 矩形数组,表示遮挡矩形
* @returns 一个对象,包含未被覆盖的边框和被覆盖的边框
*/
function getEdgeRanges(targetRect: Rectangle, rectangles: Rectangle[]): { uncoveredEdges: Edge[], coveredEdges: Edge[] } {
// 计算未被覆盖的边框
const uncoveredEdges = calculateEdges(targetRect, rectangles, false)
// 计算被覆盖的边框
const coveredEdges = calculateEdges(targetRect, rectangles, true)
return {
uncoveredEdges,
coveredEdges
}
}
/**
* 计算目标矩形的边框坐标
* @param targetRect - 目标矩形
* @param rectangles - 矩形数组,表示遮挡矩形
* @param isCovered - 是否计算被覆盖的边框(true)还是未被覆盖的边框(false)
* @returns 边框坐标数组
*/
function calculateEdges(targetRect: Rectangle, rectangles: Rectangle[], isCovered: boolean): Edge[] {
// 对每条边进行计算,合并所有边的结果
const edges: Edge[] = [
...calculateEdge(targetRect, rectangles, 'top', isCovered),
...calculateEdge(targetRect, rectangles, 'right', isCovered),
...calculateEdge(targetRect, rectangles, 'bottom', isCovered),
...calculateEdge(targetRect, rectangles, 'left', isCovered)
]
return edges
}
/**
* 计算单条边框的坐标
* @param targetRect - 目标矩形
* @param rectangles - 矩形数组,表示遮挡矩形
* @param edge - 边框类型(top, right, bottom, left)
* @param isCovered - 是否计算被覆盖的边框(true)还是未被覆盖的边框(false)
* @returns 边框坐标数组
*/
function calculateEdge(targetRect: Rectangle, rectangles: Rectangle[], edge: 'top' | 'right' | 'bottom' | 'left', isCovered: boolean): Edge[] {
// 根据边框类型确定完整范围
const fullRange: [number, number] = edge === 'top' || edge === 'bottom'
? [targetRect.x, targetRect.x + targetRect.width]
: [targetRect.y, targetRect.y + targetRect.height]
// 存储被遮挡的范围
const coveredRanges: [number, number][] = []
// 遍历所有遮挡矩形
for (const rect of rectangles) {
// 检查遮挡矩形是否与当前边框相交
if (rectIntersectsEdge(targetRect, rect, edge)) {
// 计算遮挡矩形与边框的交集范围
const [rectStart, rectEnd] = getIntersectionRange(targetRect, rect, edge)
// 只有在交集范围有效时才进行处理
if (rectStart < rectEnd) {
coveredRanges.push([rectStart, rectEnd])
}
}
}
// 计算目标矩形边框的被覆盖范围或未被覆盖范围
const targetRanges: [number, number][] = isCovered
? mergeRanges(coveredRanges)
: subtractRanges(fullRange, mergeRanges(coveredRanges))
// 将计算出的范围转换为边框坐标对象
return targetRanges.map(range => createEdge(edge, range[0], range[1], targetRect))
}
/**
* 合并重叠的范围
* @param ranges - 范围数组
* @returns 合并后的范围数组
*/
function mergeRanges(ranges: [number, number][]): [number, number][] {
if (ranges.length === 0) return []
// 按开始点排序
ranges.sort((a, b) => a[0] - b[0])
const merged: [number, number][] = []
let currentStart: number = ranges[0][0]
let currentEnd: number = ranges[0][1]
for (let i = 1; i < ranges.length; i++) {
const [start, end] = ranges[i]
if (start <= currentEnd) {
// 合并重叠的范围
currentEnd = Math.max(currentEnd, end)
} else {
// 记录当前合并的范围,并开始新的范围
merged.push([currentStart, currentEnd])
currentStart = start
currentEnd = end
}
}
// 添加最后一个合并的范围
merged.push([currentStart, currentEnd])
return merged
}
/**
* 从完整范围中减去被覆盖的范围
* @param fullRange - 完整范围
* @param coveredRanges - 被覆盖的范围
* @returns 未被覆盖的范围数组
*/
function subtractRanges(fullRange: [number, number], coveredRanges: [number, number][]): [number, number][] {
const uncoveredRanges: [number, number][] = []
let currentStart: number = fullRange[0]
for (const [start, end] of coveredRanges) {
if (start > currentStart) {
// 记录未被覆盖的范围
uncoveredRanges.push([currentStart, start])
}
currentStart = Math.max(currentStart, end)
}
if (currentStart < fullRange[1]) {
// 记录最后的未被覆盖的范围
uncoveredRanges.push([currentStart, fullRange[1]])
}
return uncoveredRanges
}
/**
* 根据边框类型和范围创建边框坐标
* @param edge - 边框类型(top, right, bottom, left)
* @param start - 范围开始点
* @param end - 范围结束点
* @param targetRect - 目标矩形
* @returns 边框坐标对象
*/
function createEdge(edge: 'top' | 'right' | 'bottom' | 'left', start: number, end: number, targetRect: Rectangle): Edge {
switch (edge) {
case 'top':
// 上边框的坐标
return { x1: start, y1: targetRect.y, x2: end, y2: targetRect.y }
case 'right':
// 右边框的坐标
return { x1: targetRect.x + targetRect.width, y1: start, x2: targetRect.x + targetRect.width, y2: end }
case 'bottom':
// 下边框的坐标
return { x1: start, y1: targetRect.y + targetRect.height, x2: end, y2: targetRect.y + targetRect.height }
case 'left':
// 左边框的坐标
return { x1: targetRect.x, y1: start, x2: targetRect.x, y2: end }
}
}
/**
* 判断矩形是否与边框相交
* @param targetRect - 目标矩形
* @param rect - 遮挡矩形
* @param edge - 边框类型(top, right, bottom, left)
* @returns 是否相交
*/
function rectIntersectsEdge(targetRect: Rectangle, rect: Rectangle, edge: 'top' | 'right' | 'bottom' | 'left'): boolean {
switch (edge) {
case 'top':
// 检查遮挡矩形的顶部是否与目标矩形的顶部相交
return rect.y <= targetRect.y && rect.y + rect.height >= targetRect.y
case 'right':
// 检查遮挡矩形的右侧是否与目标矩形的右侧相交
return rect.x <= targetRect.x + targetRect.width && rect.x + rect.width >= targetRect.x + targetRect.width
case 'bottom':
// 检查遮挡矩形的底部是否与目标矩形的底部相交
return rect.y + rect.height >= targetRect.y + targetRect.height && rect.y <= targetRect.y + targetRect.height
case 'left':
// 检查遮挡矩形的左侧是否与目标矩形的左侧相交
return rect.x <= targetRect.x && rect.x + rect.width >= targetRect.x
}
}
/**
* 获取矩形与边框的交集范围
* @param targetRect - 目标矩形
* @param rect - 遮挡矩形
* @param edge - 边框类型(top, right, bottom, left)
* @returns 交集范围
*/
function getIntersectionRange(targetRect: Rectangle, rect: Rectangle, edge: 'top' | 'right' | 'bottom' | 'left'): [number, number] {
switch (edge) {
case 'top':
// 计算上边框的交集范围
return [Math.max(rect.x, targetRect.x), Math.min(rect.x + rect.width, targetRect.x + targetRect.width)]
case 'right':
// 计算右边框的交集范围
return [Math.max(rect.y, targetRect.y), Math.min(rect.y + rect.height, targetRect.y + targetRect.height)]
case 'bottom':
// 计算下边框的交集范围
return [Math.max(rect.x, targetRect.x), Math.min(rect.x + rect.width, targetRect.x + targetRect.width)]
case 'left':
// 计算左边框的交集范围
return [Math.max(rect.y, targetRect.y), Math.min(rect.y + rect.height, targetRect.y + targetRect.height)]
}
}
// 示例数据
const targetRect: Rectangle = { x: 0, y: 0, width: 100, height: 10 }
const rectArray: Rectangle[] = [
{ x: 50, y: -5, width: 50, height: 20 }, // 覆盖上边框
{ x: 90, y: 5, width: 10, height: 10 }, // 覆盖右边框
{ x: 0, y: 10, width: 100, height: 10 }, // 覆盖下边框
{ x: -10, y: 0, width: 10, height: 10 } // 覆盖左边框
]
const edgeRanges = getEdgeRanges(targetRect, rectArray)
console.log('Uncovered Edges:', edgeRanges.uncoveredEdges)
console.log('Covered Edges:', edgeRanges.coveredEdges)