UNPKG

gjoint-line

Version:

join two elements with bezier line, arc line or square line

645 lines (566 loc) 19.7 kB
/** * get element * @param selectorOrEle * @returns {{tagName}|*|Element} */ const getElement = (selectorOrEle) => { console.log("getElement", selectorOrEle); if ('object' === typeof selectorOrEle && selectorOrEle.tagName) { return selectorOrEle; } if ('string' === typeof selectorOrEle) { return document.querySelector(selectorOrEle); } console.error("cannot get element", selectorOrEle); throw new Error("cannot get element"); }; const round = (num) => parseInt(`${Math.round(num)}`); /** * get the relative rect with its fields rounded to integer * @param rect * @param baseRect * @returns {any} */ const getRelativeRect = (rect, baseRect) => { const newRect = JSON.parse(JSON.stringify(rect)); newRect.left = round(newRect.left) - round(baseRect.left); newRect.right = round(newRect.right) - round(baseRect.left); newRect.x = round(newRect.x) - round(baseRect.left); newRect.top = round(newRect.top) - round(baseRect.top); newRect.bottom = round(newRect.bottom) - round(baseRect.top); newRect.y = round(newRect.y) - round(baseRect.top); return newRect; }; const hideEle = (ele) => { if (ele) { setStyleProperties(ele, {display: 'none',}); } }; const setStyleProperties = (ele, props) => { Object.keys(props).forEach(k => { ele.style.setProperty(k, null === props[k] || '' === props[k] ? '' : props[k]); }); } const setAttributes = (ele, props) => { Object.keys(props).forEach(k => { ele.setAttribute(k, null === props[k] || '' === props[k] ? '' : props[k]) }); }; const ShapeEnum = { bezier: "bezier", arc: "arc", square: "square", }; const PositionEnum = { center: "center", edge: "edge", }; const createRectBaseOn = (rect, newX, newY) => { const newRect = { x: rect.x, y: rect.y, left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom, width: rect.width, height: rect.height, }; adjustRect(newRect, newX, newY); return newRect; }; const adjustRect = (rect, newX, newY) => { if (['number', 'string'].includes(typeof newX)) { newX = parseInt(newX); rect.x = newX; rect.left = newX; rect.right = newX + rect.width; } if (['number', 'string'].includes(typeof newY)) { newY = parseInt(newY); rect.y = newY; rect.top = newY; rect.bottom = newY + rect.height; } }; const isRect = (rect) => { return ( 'object' === typeof (rect) && 'left' in rect && 'top' in rect && 'right' in rect && 'bottom' in rect && 'width' in rect && 'height' in rect ); }; /** * * @param from selector or element or rect * @param to selector or element or rect * @param line selector or element * @param orientation h/v * @param strokeWidth * @param shape bezier/arc/square, always square when orientation=v * @param fromPosition center/edge, always center when shape=arc or orientation=v * @param toPosition center/edge, always center when shape=arc or orientation=v * @param color * @param redeemingPx true,false,0,1,2... */ const putLine = (from, to, line, { orientation, strokeWidth, shape, fromPosition, toPosition, color, redeemingPx, } = {}) => { orientation = (orientation ?? 'h'); strokeWidth = (strokeWidth ?? 1); fromPosition = (fromPosition ?? PositionEnum.edge); toPosition = (toPosition ?? PositionEnum.edge); shape = (shape ?? ShapeEnum.bezier); color = (color ?? 'lightgrey'); if ('boolean' === typeof redeemingPx) { redeemingPx = redeemingPx ? 1 : 0; } else if (['number', 'string'].includes(typeof redeemingPx)) { redeemingPx = parseInt(redeemingPx); } else { redeemingPx = 0; } const lineWrapperEle = getElement(line); const svgNS = "http://www.w3.org/2000/svg"; let lineSvgEle = lineWrapperEle.querySelector("svg") || lineWrapperEle.appendChild(document.createElementNS(svgNS, "svg")); const linePathEle = lineSvgEle.querySelector("path") || lineSvgEle.appendChild(document.createElementNS(svgNS, "path")); const lineEllipseEle = lineSvgEle.querySelector("ellipse") || lineSvgEle.appendChild(document.createElementNS(svgNS, "ellipse")); const lineRedeeming1 = lineSvgEle.querySelectorAll("line")[0] || lineSvgEle.appendChild(document.createElementNS(svgNS, "line")); const lineRedeeming2 = lineSvgEle.querySelectorAll("line")[1] || lineSvgEle.appendChild(document.createElementNS(svgNS, "line")); let fromRect; if (isRect(from)) { fromRect = from; } else { const fromEle = getElement(from); const relativeRect = fromEle.parentNode.getBoundingClientRect(); fromRect = getRelativeRect(fromEle.getBoundingClientRect(), relativeRect); } let toRect; if (isRect(to)) { toRect = to; } else { const toEle = getElement(to); const relativeRect = toEle.parentNode.getBoundingClientRect(); toRect = getRelativeRect(toEle.getBoundingClientRect(), relativeRect); } const baseContext = { orientation, strokeWidth, fromPosition, toPosition, shape, color, // fromEle, // toEle, lineWrapperEle, lineSvgEle, linePathEle, lineEllipseEle, lineRedeeming1, lineRedeeming2, fromRect, toRect, }; if ('h' === orientation) { const getJointPointTop = (rect, position) => { if (PositionEnum.center === position) { return round(rect.top + rect.height / 2 - strokeWidth / 2); } if (PositionEnum.edge === position) { return round(rect.bottom - strokeWidth); } console.error(`unsupported joint point position: ${position}`); throw new Error("unsupported joint point position"); }; const context = {...baseContext, getJointPointTop, redeemingPx}; if (ShapeEnum.bezier === shape) { bezierHLine(context); return; } if (ShapeEnum.arc === shape) { arcHLine(context); return; } if (ShapeEnum.square === shape) { squareHLine(context); return; } return; } if ('v' === orientation) { const getJointPointLeft = (rect, position) => { if (PositionEnum.center === position) { return round(rect.left + rect.width / 2 - strokeWidth / 2); } console.error(`unsupported joint point position: ${position}`); throw new Error("unsupported joint point position"); }; const context = {...baseContext, getJointPointLeft, redeemingPx,}; squareVLine(context); } }; const bezierHLine = ({ strokeWidth, fromPosition, toPosition, color, lineWrapperEle, lineSvgEle, linePathEle, lineEllipseEle, lineRedeeming1, lineRedeeming2, fromRect, toRect, redeemingPx, getJointPointTop, }) => { hideEle(lineEllipseEle) const padding = 10; const leftToRight = (fromRect.left < toRect.left); let left = (leftToRight ? fromRect.right : toRect.right); let right = (leftToRight ? toRect.left : fromRect.left); // control point: min(xDist*3/4, 75) const controlLevel = Math.min(round(Math.abs(right - left) * 0.75), 75); const fromTop = getJointPointTop(fromRect, fromPosition); const toTop = getJointPointTop(toRect, toPosition); const top = Math.min(fromTop, toTop); const bottom = Math.max(fromTop, toTop) + strokeWidth; // look from left to right, whether the line is up to down const lineLeftRightTopDown = ((leftToRight && fromTop < toTop) || (!leftToRight && fromTop > toTop)); let x1 = padding; let y1 = padding + strokeWidth / 2; let x2 = padding + (right - left); let y2 = (padding + bottom - top - strokeWidth / 2); let c1x = x1 + controlLevel; let c1y; let c2x = x2 - controlLevel; let c2y; if (!lineLeftRightTopDown) { [y1, y2] = [y2, y1]; } c1y = y1; c2y = y2; setStyleProperties(lineWrapperEle, { left: `${left - padding}px`, width: `${right - left + 2 * padding}px`, top: `${top - padding}px`, height: `${bottom - top + 2 * padding}px`, "background-color": "transparent", position: "absolute", overflow: null, }); setStyleProperties(lineSvgEle, { width: `${right - left + 2 * padding}px`, height: `${bottom - top + 2 * padding}px`, "background-color": "transparent", position: null, bottom: null, right: null, }); linePathEle.setAttribute("d", `M ${x1} ${y1} C ${c1x} ${c1y} ${c2x} ${c2y} ${x2} ${y2}`); setStyleProperties(linePathEle, { stroke: color, "stroke-width": strokeWidth, fill: "none", display: "block", }); if (0 === redeemingPx) { hideEle(lineRedeeming1); hideEle(lineRedeeming2); } else { setAttributes(lineRedeeming1, { "x1": x1 - redeemingPx, "y1": y1, "x2": x1, "y2": y1, }); setStyleProperties(lineRedeeming1, { stroke: color, "stroke-width": strokeWidth, display: "block", }); setAttributes(lineRedeeming2, { "x1": x2, "y1": y2, "x2": x2 + redeemingPx, "y2": y2 }); setStyleProperties(lineRedeeming2, { stroke: color, "stroke-width": strokeWidth, display: "block", }); } }; const arcHLine = ({ strokeWidth, color, lineWrapperEle, lineSvgEle, linePathEle, lineEllipseEle, lineRedeeming1, lineRedeeming2, fromRect, toRect, getJointPointTop, redeemingPx, }) => { hideEle(linePathEle); hideEle(lineRedeeming1); hideEle(lineRedeeming2); const leftToRight = (fromRect.left < toRect.left); let left = round(leftToRight ? fromRect.right - fromRect.width / 2 : toRect.right); let right = round(leftToRight ? toRect.left : fromRect.right - fromRect.width / 2); if (redeemingPx > 0) { if (leftToRight) { right += redeemingPx; } else { left -= redeemingPx; } } const fromTop = getJointPointTop(fromRect, PositionEnum.center); const toTop = getJointPointTop(toRect, PositionEnum.center); const topToDown = (fromTop < toTop); const top = Math.min(fromTop, toTop); const bottom = Math.max(fromTop, toTop) + strokeWidth; setStyleProperties(lineWrapperEle, { left: `${left}px`, width: `${right - left}px`, top: `${top}px`, height: `${bottom - top}px`, position: "absolute", "background-color": "transparent", overflow: "hidden", }); const svgStyle = { width: `${(right - left) * 2}px`, height: `${(bottom - top) * 2}px`, "background-color": "transparent", position: "absolute", bottom: null, right: null, }; if (leftToRight && topToDown) { svgStyle.bottom = `0`; } if (leftToRight && !topToDown) { // nothing to do } if (!leftToRight && topToDown) { svgStyle.bottom = `0`; svgStyle.right = `0`; } if (!leftToRight && !topToDown) { svgStyle.right = `0`; } setStyleProperties(lineSvgEle, svgStyle); lineEllipseEle.setAttribute("cx", `${right - left}`); lineEllipseEle.setAttribute("cy", `${bottom - top}`); lineEllipseEle.setAttribute("rx", `${right - left - strokeWidth / 2}`); lineEllipseEle.setAttribute("ry", `${bottom - top - strokeWidth / 2}`); setStyleProperties(lineEllipseEle, { stroke: color, "stroke-width": strokeWidth, fill: "none", display: "block", }); }; const squareHLine = ({ strokeWidth, fromPosition, toPosition, color, lineWrapperEle, lineSvgEle, linePathEle, lineEllipseEle, lineRedeeming1, lineRedeeming2, fromRect, toRect, getJointPointTop, redeemingPx, }) => { hideEle(lineEllipseEle); const padding = 10; const controlRate = 0.4; const leftToRight = (fromRect.left < toRect.left); let left = (leftToRight ? fromRect.right : toRect.right); let right = (leftToRight ? toRect.left : fromRect.left); const fromTop = getJointPointTop(fromRect, fromPosition); const toTop = getJointPointTop(toRect, toPosition); const top = Math.min(fromTop, toTop); const bottom = Math.max(fromTop, toTop) + strokeWidth; // look from left to right, whether the line is up to down const lineLeftRightTopDown = ((leftToRight && fromTop < toTop) || (!leftToRight && fromTop > toTop)); let step = round((right - left) * controlRate); let x1 = padding; let y1 = padding + strokeWidth / 2; let x2 = padding + (right - left); let y2 = (padding + bottom - top - strokeWidth / 2); if (!lineLeftRightTopDown) { [y1, y2] = [y2, y1]; } let cx = (leftToRight ? x1 + step : x2 - step); setStyleProperties(lineWrapperEle, { left: `${left - padding}px`, width: `${right - left + 2 * padding}px`, top: `${top - padding}px`, height: `${bottom - top + 2 * padding}px`, "background-color": "transparent", position: "absolute", overflow: null, }); setStyleProperties(lineSvgEle, { width: `${right - left + 2 * padding}px`, height: `${bottom - top + 2 * padding}px`, "background-color": "transparent", position: null, bottom: null, right: null, }); linePathEle.setAttribute("d", `M ${x1} ${y1} L ${cx} ${y1} L ${cx} ${y2} L ${x2} ${y2}`); setStyleProperties(linePathEle, { stroke: color, "stroke-width": strokeWidth, fill: "none", display: "block", }); if (0 === redeemingPx) { hideEle(lineRedeeming1); hideEle(lineRedeeming2); } else { setAttributes(lineRedeeming1, { "x1": x1 - redeemingPx, "y1": y1, "x2": x1, "y2": y1, }); setStyleProperties(lineRedeeming1, { stroke: color, "stroke-width": strokeWidth, display: "block", }); setAttributes(lineRedeeming2, { "x1": x2, "y1": y2, "x2": x2 + redeemingPx, "y2": y2 }); setStyleProperties(lineRedeeming2, { stroke: color, "stroke-width": strokeWidth, display: "block", }); } }; const squareVLine = ({ strokeWidth, fromPosition, toPosition, color, lineWrapperEle, lineSvgEle, linePathEle, lineEllipseEle, lineRedeeming1, lineRedeeming2, fromRect, toRect, getJointPointLeft, redeemingPx, }) => { hideEle(lineEllipseEle); const padding = 10; const controlRate = 0.4; const topToBottom = (fromRect.top < toRect.top); let top = (topToBottom ? fromRect.bottom : toRect.bottom); let bottom = (topToBottom ? toRect.top : fromRect.top); const fromLeft = getJointPointLeft(fromRect, fromPosition); const toLeft = getJointPointLeft(toRect, toPosition); const left = Math.min(fromLeft, toLeft); const right = Math.max(fromLeft, toLeft) + strokeWidth; // look from top to bottom, whether the line is left to right const lineTopDownLeftRight = ((topToBottom && fromLeft < toLeft) || (!topToBottom && fromLeft > toLeft)); let step = round((bottom - top) * controlRate); let x1 = padding + strokeWidth / 2; let y1 = padding; let x2 = (padding + right - left - strokeWidth / 2); let y2 = padding + (bottom - top); if (!lineTopDownLeftRight) { [x1, x2] = [x2, x1]; } let cy = (topToBottom ? y1 + step : y2 - step); setStyleProperties(lineWrapperEle, { left: `${left - padding}px`, width: `${right - left + 2 * padding}px`, top: `${top - padding}px`, height: `${bottom - top + 2 * padding}px`, "background-color": "transparent", position: "absolute", overflow: null, }); setStyleProperties(lineSvgEle, { width: `${right - left + 2 * padding}px`, height: `${bottom - top + 2 * padding}px`, "background-color": "transparent", position: null, bottom: null, right: null, }); linePathEle.setAttribute("d", `M ${x1} ${y1} L ${x1} ${cy} L ${x2} ${cy} L ${x2} ${y2}`); setStyleProperties(linePathEle, { stroke: color, "stroke-width": strokeWidth, fill: "none", display: "block", }); if (0 === redeemingPx) { hideEle(lineRedeeming1); hideEle(lineRedeeming2); } else { setAttributes(lineRedeeming1, { "x1": x1, "y1": y1 - redeemingPx, "x2": x1, "y2": y1, }); setStyleProperties(lineRedeeming1, { stroke: color, "stroke-width": strokeWidth, display: "block", }); setAttributes(lineRedeeming2, { "x1": x2, "y1": y2, "x2": x2, "y2": y2 + redeemingPx, }); setStyleProperties(lineRedeeming2, { stroke: color, "stroke-width": strokeWidth, display: "block", }); } }; export { putLine, createRectBaseOn, isRect, };