UNPKG

@furious_whale/flyline

Version:

用 canvas 实现 HTML 元素之间相互连接飞线 支持vue2/vue3/原生

354 lines (350 loc) 11.6 kB
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 } } }