@furious_whale/flyline
Version:
用 canvas 实现 HTML 元素之间相互连接飞线 支持vue2/vue3/原生
354 lines (350 loc) • 11.6 kB
JavaScript
import Flyline from 'e-flyline'
export default class FlyLine {
constructor({ responseData, arrowSize, lineMax, lineMin }) {
this.container = null;
this.elementPosition = [];
this.attrName = '';
this.lineData = [];
this.responseData = (responseData && Array.isArray(responseData)) ? responseData : [];
this.flylinePlugin = null;
this.arrowSize = (arrowSize || arrowSize === 0) ? arrowSize : 25;
this.lineWMax = Number(lineMax || 5);
this.lineWMin = Number(lineMin || 1)
this.lineWD = Math.abs(this.lineWMax - this.lineWMin);
}
init(container, attrName) {
if (!container || !attrName) return;
this.container = container;
this.attrName = attrName;
const labelList = this.container.querySelectorAll(`[${attrName}]`);
labelList.forEach((el) => {
const captureValue = el.getAttribute(attrName)
if (captureValue) {
const pos = this.getElToBoxPosition(el)
this.elementPosition.push({
labelid: captureValue,
left: pos.left,
top: pos.top,
bottom: pos.top + el.offsetHeight,
right: pos.left + el.offsetWidth,
centerTop: pos.top + el.offsetHeight / 2,
centerLeft: pos.left + el.offsetWidth / 2,
height: el.offsetHeight,
width: el.offsetWidth,
prentWidth: container.offsetWidth,
prentHeight: container.offsetHeight,
})
}
})
this.flylinePlugin = new Flyline(this.container, {
arrow: {
size: this.arrowSize,
},
loop: (data) => {
if (this.loop) {
this.loop(data)
}
},
click: (data) => {
if (this.click) {
this.click(data)
}
},
move: (data) => {
if (this.move) {
this.move(data)
}
}
}, this.responseData)
}
destroy() {
if (this.flylinePlugin) {
this.flylinePlugin.destroy()
}
}
draw(list) {
this.lineData = []
let maxWindth = 0;
let minWidth = Number.MAX_SAFE_INTEGER;
if (list && list.length) {
list.forEach(item => {
let width = item.width || 0;
if (width > maxWindth) maxWindth = width;
if (width < minWidth) minWidth = width;
})
let whidthD = maxWindth - minWidth;
list.forEach(item => {
const from = this.elementPosition.find(el => el.labelid === item.from)
const to = this.elementPosition.find(el => el.labelid === item.to)
if (from && to) {
const startPosition = [];
const endPosition = [];
switch (item.start) {
case "left":
startPosition.push(from.left)
startPosition.push(from.centerTop)
break;
case "right":
startPosition.push(from.right)
startPosition.push(from.centerTop)
break;
case "top":
startPosition.push(from.centerLeft)
startPosition.push(from.top)
break;
case "bottom":
startPosition.push(from.centerLeft)
startPosition.push(from.bottom)
break;
case "center":
startPosition.push(from.centerLeft)
startPosition.push(from.centerTop)
break;
case "leftTop":
startPosition.push(from.left)
startPosition.push(from.centerTop - from.height / 2)
break;
case "leftBottom":
startPosition.push(from.left)
startPosition.push(from.centerTop + from.height / 2)
break;
case "topLeft":
startPosition.push(from.centerLeft - from.width / 2)
startPosition.push(from.top)
break;
case "topRight":
startPosition.push(from.centerLeft + from.width / 2)
startPosition.push(from.top)
break;
case "rightTop":
startPosition.push(from.right)
startPosition.push(from.centerTop - from.height / 2)
break;
case "rightBottom":
startPosition.push(from.right)
startPosition.push(from.centerTop + from.height / 2)
break;
case "bottomLeft":
startPosition.push(from.centerLeft - from.width / 2)
startPosition.push(from.bottom)
break;
case "bottomRight":
startPosition.push(from.centerLeft + from.width / 2)
startPosition.push(from.bottom)
break;
default:
startPosition.push(from.right)
startPosition.push(from.centerTop)
break;
}
switch (item.end) {
case "left":
endPosition.push(to.left)
endPosition.push(to.centerTop)
break;
case "right":
endPosition.push(to.right)
endPosition.push(to.centerTop)
break;
case "top":
endPosition.push(to.centerLeft)
endPosition.push(to.top)
break;
case "bottom":
endPosition.push(to.centerLeft)
endPosition.push(to.bottom)
break;
case "center":
endPosition.push(to.centerLeft)
endPosition.push(to.centerTop)
break;
case "leftTop":
endPosition.push(to.left)
endPosition.push(to.centerTop - from.height / 2)
break;
case "leftBottom":
endPosition.push(to.left)
endPosition.push(to.centerTop + from.height / 2)
break;
case "topLeft":
endPosition.push(to.centerLeft - from.width / 2)
endPosition.push(to.top)
break;
case "topRight":
endPosition.push(to.centerLeft + from.width / 2)
endPosition.push(to.top)
break;
case "rightTop":
endPosition.push(to.right)
endPosition.push(to.centerTop - from.height / 2)
break;
case "rightBottom":
endPosition.push(to.right)
endPosition.push(to.centerTop + from.height / 2)
break;
case "bottomLeft":
endPosition.push(to.centerLeft - from.width / 2)
endPosition.push(to.bottom)
break;
case "bottomRight":
endPosition.push(to.centerLeft + from.width / 2)
endPosition.push(to.bottom)
break;
default:
endPosition.push(to.left)
endPosition.push(to.centerTop)
break;
}
let points = [startPosition];
if (item.type && item.type == "angle" && item.path && item.path.length) {
points = points.concat(item.path.filter(e => e[0] && e[1]).map(e => this.processPoint(e, points, { startPosition, endPosition })));
}
if (item.type && item.type == "curve") {
points = points.concat([this.delCurvePath(item, startPosition, endPosition)]);
}
points.push(endPosition)
this.lineData.push({
startPosition,
endPosition,
points,
id: item.id || (item.from + "_" + item.to),
...item,
width: list.length == 1 ? (this.lineWMin + (this.lineWD / 2)) : (this.lineWMin + (((item.width || 0) - minWidth) / whidthD) * this.lineWD),
curve: item.type == "curve",
})
}
})
}
this.flylinePlugin.draw(this.lineData)
}
delCurvePath(config, start, end) {
let qX = start[0];
let qY = start[1];
const endF = end[0] - start[0];
const endT = end[1] - start[1];
let pF = Math.abs(endF / 3);
let pT = Math.abs(endT / 3);
if (endF > endT) {
pF = Math.max(pF, 60);
} else if (endF < endT) {
pT = Math.max(pT, 60);
}
const directionMap = {
'left': () => qX -= pF,
'right': () => qX += pF,
'top': () => qY -= pT,
'bottom': () => qY += pT
};
if (config.start) {
let s = 'right';
if (config.start.startsWith('left')) {
s = 'left';
}
if (config.start.startsWith('top')) {
s = 'top';
}
if (config.start.startsWith('bottom')) {
s = 'bottom';
}
if (directionMap[s]) {
directionMap[s]();
}
}
return [qX, qY];
}
processPoint(item, pathList, config) {
let p = this.pointPx(item, config);
if (String(item[0]).startsWith('i')) {
p[0] = pathList[pathList.length - 1][0] + this.delInheritX(item[0], config);
}
if (String(item[1]).startsWith('i')) {
p[1] = pathList[pathList.length - 1][1] + this.delInheritY(item[1], config);
}
if (String(item[0]).startsWith('e')) {
p[0] = config.endPosition[0] + this.delInheritX(item[0], config);
}
if (String(item[1]).startsWith('e')) {
p[1] = config.endPosition[1] + this.delInheritY(item[1], config);
}
return p;
}
delInheritX(txt, config) {
if (!txt) return 0;
if (txt.includes('-')) {
const [, value] = txt.split('-');
const p = this.pointPx([value, 0], config);
return -p[0];
}
if (txt.includes('+')) {
const [, value] = txt.split('+');
const p = this.pointPx([value, 0], config);
return p[0];
}
return 0;
}
delInheritY(txt, config) {
if (!txt) return 0;
if (txt.includes('-')) {
const [, value] = txt.split('-');
const p = this.pointPx([0, value], config);
return -p[1];
}
if (txt.includes('+')) {
const [, value] = txt.split('+');
const p = this.pointPx([0, value], config);
return p[1];
}
return 0;
}
convertToPx(value, dimension) {
const isPercentage = String(value).includes('%');
const parsedValue = parseFloat(value);
return isPercentage ? Number((parsedValue / 100 * dimension).toFixed(2)) : parsedValue;
}
pointPx([x, y], config) {
const vW = config.endPosition[0] - config.startPosition[0]
const vH = config.endPosition[1] - config.startPosition[1]
return [
Number(this.convertToPx(x, vW)),
Number(this.convertToPx(y, vH))
];
}
getElToBoxPosition(el) {
let itemTop = el.offsetTop;
let itemLeft = el.offsetLeft;
let xTransform = 0
let yTransform = 0
const computedStyles = window.getComputedStyle(el)
if (computedStyles.transform && computedStyles.transform !== 'none') {
const list = computedStyles.transform.split('(')[1].split(')')[0].split(',')
if (list[4]) xTransform = Number(list[4].trim())
if (list[5]) yTransform = Number(list[5].trim())
}
itemLeft += xTransform
itemTop += yTransform
const parent = this.getPositionParent(el)
if (parent && parent !== this.container) {
const pos = this.getElToBoxPosition(parent)
itemLeft += pos.left
itemTop += pos.top
}
return {
left: itemLeft,
top: itemTop,
}
}
getPositionParent(el) {
if (!el.parentNode) return null
const computedStyles = window.getComputedStyle(el.parentNode)
if (
computedStyles.position &&
(computedStyles.position === 'absolute' || computedStyles.position === 'relative')
) {
return el.parentNode
} else if (el.parentNode) {
return this.getPositionParent(el.parentNode)
} else {
return null
}
}
}