• 如何用SVG画一个特定边框


    fd445007f617c8089bb504cc94f8c0d8.gif

    最近的需求中有一个tab切换的场景,设计师提出了自己期望的效果,核心关注点在蓝色边框上,本文围绕如何实现这样的边框效果展开讨论。

    9c620aa19878e296a41fc0e2296f4d67.png

    背景

    设计师期望的效果如下,核心关注点在蓝色边框上。

    实现这样的边框,核心问题有几个:

    1. 如何将两个元素的边框相连

    2. 内凹的圆角如何实现

    3. tab元素滚动离屏,边框如何过渡

    16e7556fd5bf64803ede7dcd361e3b9e.png

    CSS

    我决定先用CSS试试,border + border-radius,应该轻松搞定。

      问题一:CSS 如何实现边框相连

    这倒不难,我们需要:

    1. 给 tab元素 设置border-right: none,同时border-top-right-radius: 0border-bottom-right-radius: 0

    2. 再给 tab元素 一个向右的偏移,偏移量 = 边框宽度

    3. 最后让 tab元素 z-Index 高于内容区,并给 tab元素 加上背景色(背景色需要和页面背景色一致)

    • 缺陷

    这时候缺点已经来了,我们通过加背景色遮盖边框实现边框相连,不可避免地遮盖了页面内容,如果页面背景比较复杂,我们会很难处理。这个方案并不足够通用,但好在我们的场景页面背景纯白,先忍了。

      问题二:CSS 如何实现内凹圆角

    也还行,我们需要:

    1. 新增两个元素,宽高和border-radius值相等

    2. 元素1 设置背景色,先覆盖在边框相连处

    3. 元素2 设置border-bottom-right-radius: 50%,同时border和tab元素保持一致

    • 缺陷

    其实和问题一一样,我们又使用了背景色对边框进行遮盖,但先忍了,实现要紧。

      问题三:CSS 如何实现滚动离屏过渡

    这个问题用css就比较难实现了,它可以被拆解成两个子问题:

    1. tab元素的顶边在离屏过程中需要固定,border 框选区域高度不断变小

    2. 圆角如何平滑过渡到直线

    c040eb2a7be1f7482ab2f6df11907857.png

    如果世界上已经没有其他方式能实现这样的边框,我想硬着头皮写一堆恶心逻辑也是能实现效果的,但我觉得这样的实现比较丑陋,不太优雅,因此 CSS 的尝试到这里就结束了,我决定换个方案。

    3dfef78122ffd64cdbe6cc500ba9484b.png

    SVG

    其实使用SVG来实现一些CSS不好处理的场景在社区中已经有很多实践了。比如用于新人引导的开源库 driver.js。

    driver.js地址:https://driverjs.com/

    【新人引导】指的是这样的场景:

    这个场景下,【蒙层内区域高亮】是技术核心,driver.js 在几个月前刚进行了一次重构,将蒙层改用SVG实现,支持了高亮区的圆角。这给了我启发,哥们也用 SVG 画个边框吧。

      问题一:SVG 如何实现边框相连

    svg嘛,用起来就是更麻烦,先从简单的开始吧:

    • 怎么用 SVG 画一条线

    这个容易,使用 标签,提供两个点坐标(x1, y1)、(x2, y2),在描述一下边框的样式就可以了。

    1. <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
    2. <line x1="0" y1="80" x2="100" y2="20" stroke="black" />
    3. svg>

    使用 line 标签的方式固然可以,但为了方便后续代码逻辑,我们还有更好的方式:标签,我们可以通过命令式的方式,完成 SVG 各种型状的绘制,比如一条直线:

    1. <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
    2. <path d="M 0 80 L 100 20" stroke="black" fill="none" />
    3. svg>

    文档地址:https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial/Paths

    其中核心字段位 d="M 0 80 L 100 20",这一段命令中有两个指令 ML

    1. M全称“Move to”,可以理解为将SVG画笔挪到某个点作为路径起点,因此该命令后边跟两个数字,分别对应起点的 x、y

    2. L全称“Line”,可以理解为从当前画笔位置为起点,绘制一条直线到另一个点(终点),并且绘制后,画笔位置也会挪到终点,(path 中大多数指令都是指定终点即可,起点就是当前画笔位置),因此该命令后边跟两个数字,分别对应终点的 x、y

    关于 path 的其他指令不再赘述,总的来说,想使用 path 绘制边框,我们首先要获取到边框上各个结点坐标,之后再用命令将他们链接起来。

    • 获取点坐标来画线

    我们首先获取 tab元素 和 内容区 的四个节点,我们通过getBoundingClientRect方法获取 topleftrightbottom 四个值来构造这些点坐标。

    db19f0a101c07122fec3ac9ff96f6513.png

    但我们不能直接给他两点相连起来,那就成这样了:

    128503eaf7c89d17d420654005daa884.png

    我们需要做做个调整,需要将(right1, top1)(right1, bottom1)两个点的 x 坐标做偏移,让这两点的 x 和元素2的 left 一致,得到(left2, top1)(left2, bottom1)

    7dc9db6298d3fd9802c5d4f0743ad976.png

    我们再给这些点加上编号,按照 ABCDEFGH 的顺序,将这些点通过直线相连,path的命令就会如下:

    1. <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
    2. <path
    3. d="
    4. M left1 top1
    5. L left2 top1
    6. L left2 top2
    7. L right2 top2
    8. L right2 bottom2
    9. L left2 bottom2
    10. L left2 bottom1
    11. L left1 bottom1
    12. Z
    13. "
    14. stroke="black"
    15. fill="none"
    16. />
    17. svg>

    这样实现的边框,不会有 CSS 背景色遮挡的问题。

      问题二:SVG 如何实现内凹圆角

    问题又变得复杂起来了,同样,我们还是先从简单的开始吧:

    • 怎么用 SVG 画一个圆弧

    path 中有一个弧形指令A,这个指令能绘制椭圆,正圆自然也不在话下,他的参数有很多:

    A rx ry x-axis-rotation large-arc-flag sweep-flag x y

    rx ry:为 X 轴和 Y 轴的半径,对于正圆来说,rx = ry,在我们的场景里,他的值和 border-radius 是等效的

    x-axis-rotation:用于控制这个弧线沿 X 轴旋转的角度,对于正圆来说,怎么转都一样,所以这个值我们使用时始终为 0 即可

    large-arc-flag:决定弧线是大于还是小于 180 度,0 表示小角度弧,1 表示大角度弧,由于border-radius 其实都是 90 度角,因此我们使用时始终为 0 即可

    sweep-flag:表示弧线的方向,0 表示从起点到终点沿逆时针画弧,1 表示从起点到终点沿顺时针画弧

    x y:弧线终点坐标

    下边是一些示例:

    1. <svg width="325" height="325" xmlns="http://www.w3.org/2000/svg">
    2. <path
    3. d="M 80 80
    4. A 45 45, 0, 0, 0, 125 125
    5. L 125 80 Z"
    6. fill="green"
    7. />
    8. <path
    9. d="M 230 80
    10. A 45 45, 0, 1, 0, 275 125
    11. L 275 80 Z"
    12. fill="red"
    13. />
    14. <path
    15. d="M 80 230
    16. A 45 45, 0, 0, 1, 125 275
    17. L 125 230 Z"
    18. fill="purple"
    19. />
    20. <path
    21. d="M 230 230
    22. A 45 45, 0, 1, 1, 275 275
    23. L 275 230 Z"
    24. fill="blue"
    25. />
    26. svg>

    效果如下(有颜色区域是最终形状,其他线条是辅助线):

    e3333b020ab3e6f53dec7fee7f14a157.png

    • 给边框加上圆角

    上文中,我们已经拿到了 ABCDEFGH 8个点,每一个点其实都会有一个对应的圆弧,因此在绘制边框的时候,我是这样管理 圆弧 和 直线 的,下边是一个点的数据结构:

    1. const A = {
    2. x: 100,
    3. y: 100,
    4. arc: 'A xxxxxx', // 经过该点的圆弧
    5. line: 'L xxxxxx' // 圆弧的结束点到下一个圆弧起点的直线
    6. }

    根据这个结构,我再按 ABCDEFGH 的顺序,将每个点的 svg 指令拼接起来,先拼接 圆弧(arc) 再拼接 直线(line)

    那么圆弧的指令如何生成呢,我们以一个点来分析:

    7d9eaf76a8ccd1a0a2bcd8ff7b3431f5.png

    1. 圆弧的起点坐标为(x, y-radius)

    2. 终点坐标为(x+radius, y)

    3. 半径就是 border-radius 的值

    4. 弧线方向会有区别,两个内凹圆角是逆时针,其他圆角都是顺时针

    有了这些信息,其实一个圆弧的指令就呼之欲出了,我们通过一段代码快速生成(两个为 0 的值上文介绍A指令时有提到,不赘述原因):

    1. enum ESweepFlag {
    2. cw = 1, // 顺时针
    3. ccw = 0, // 逆时针
    4. }
    5. /**
    6. * 生成圆弧svg路径
    7. * @param endX: 圆弧终点x坐标
    8. * @param endY: 圆弧终点y坐标
    9. * @param radius: 圆弧半径
    10. * @param sweepFlag: 顺时针还是逆时针: 1 顺时针、0 逆时针
    11. */
    12. const generatorArc = (endX: number, endY: number, radius: number, sweepFlag: ESweepFlag = ESweepFlag.cw) => {
    13. return `A${radius} ${radius} 0 0 ${sweepFlag} ${endX} ${endY}`;
    14. }

    到这里,我们将 圆弧 和 直线 指令,按 ABCDEFGH 点顺序,先圆弧后直线挨个拼接起来,边框也就画成了。

      问题三:SVG 如何实现滚动离屏过渡

    我们先看看理想效果:

    在上文中我们提到,这个问题其实可以拆解为两个子问题:

    1. tab元素的顶边在离屏过程中需要固定,border 框选区域高度不断变小

    2. 圆角如何平滑过渡到直线

    • 如何固定顶边

    在元素已经有一部分离屏的时候,我们需要对点进行修正:

    1. A 点的坐标将会被强制更新,我们用 A' 点表示新坐标,y 值永远被固定为 top2

    2. B、C 点可以直接被丢弃,即数据结构中的 arc、line 值都可以为空,因为 A' 点可以直接一条线连到 D 点

    ea116ac9ca773b64d2a5832fcdcbf94a.png

    同理,元素往底部离屏的时候,我们强制更新 H 点,丢弃 G、F 点即可。

    • 圆角如何平滑过渡到直线

    其实有6个圆角(A、B、C、F、G、H点对应的圆角)需要过渡到直线。我们以 B、C 两点为例:

    1. 两点想距较远(> 2 * radius),弧是一个正圆弧,两半径长度都为 radius

    2. 两点想距较近(< 2 * radius),弧是一个椭圆弧,X轴半径不变,Y轴半径变为 (y1 - y2) / 2

    25c57dafee26f60a1384df1e4b4da7ec.png

    如何通过SVG表达这种过渡曲线呢?我们可以使用圆弧命令 A 的能力,因为它支持椭圆,不过我们还有另一种方式:二次贝塞尔曲线,一个二次贝塞尔曲线由 起点、终点 和一个 控制点 组成,每个圆弧我们其实正好能拿到3与之对应的点。

    2793bd42161bea16fb4a87976975e31c.png

    二次贝塞尔曲线在 SVG path 中通过 Q 指令绘制Q x1 y1, x y,在SVG中,起点为画笔位置,因此Q指令指定 控制点 和 终点:

    1. x1 y1:控制点坐标

    2. x y:终点坐标

    7e99dd49c8fc8a9268e8834066ba6a3a.png

    其他问题

    到这,核心卡点问题我们都已经解决了,实际上最终也实现了一版,达到了设计师想要的效果,但还存在一些遗留问题:

      性能问题

    由于要随滚动不断计算并渲染SVG边框,因此性能开销比较大。后续需要在算法上进行优化,才能真正达到高体验的标准。

      拓展性

    我们的算法基本是为水平布局定制的,如果布局切换到垂直布局,很多地方需要改动,因此当前方案的通用性并不佳。

      源码

    使用绘制边框SVG的源码附上,drawSVGBorder方法为入口:

    1. /**
    2. * 用于绘制边框的svg的id
    3. */
    4. export const Svg_Id = '____TAB_CONTAINER_BORDER_SVG_ID_MAKABAKA____';
    5. /**
    6. * tab容器的id
    7. */
    8. export const Container_Id = '__MKT_TAB_CONTAINER_ID__';
    9. export interface IBorderStyle {
    10. /**
    11. * 边框颜色
    12. */
    13. color?: string;
    14. /**
    15. * 边框宽度
    16. */
    17. width?: number;
    18. /**
    19. * 边框圆角
    20. */
    21. radius?: number;
    22. }
    23. /**
    24. * 为了绘制svg边框,需要将两个dom元素的四个顶点定义出来
    25. * 为了方便svg最终路径生成,因此每个点还会存储两个信息:
    26. * 1. 经过这个点的圆弧的svg路径
    27. * 2. 这个点到下一个圆弧起点的svg路径
    28. */
    29. interface IPoint {
    30. x: number;
    31. y: number;
    32. arc?: string; // 圆弧svg路径
    33. line?: string; // 连线svg路径
    34. }
    35. interface IRect {
    36. left: number;
    37. top: number;
    38. right: number;
    39. bottom: number;
    40. }
    41. export enum EDirection {
    42. column = 'column',
    43. row = 'row',
    44. }
    45. enum ESweepFlag {
    46. cw = 1, // 顺时针
    47. ccw = 0, // 逆时针
    48. }
    49. /**
    50. * 生成圆弧svg路径
    51. * @param endX: 圆弧终点x坐标
    52. * @param endY: 圆弧终点y坐标
    53. * @param radius: 圆弧半径
    54. * @param sweepFlag: 顺时针还是逆时针: 1 顺时针、0 逆时针
    55. */
    56. const generatorArc = (endX: number, endY: number, radius: number, sweepFlag: ESweepFlag = ESweepFlag.cw) => {
    57. return `A${radius} ${radius} 0 0 ${sweepFlag} ${endX} ${endY}`;
    58. }
    59. /**
    60. * 生成险段svg路径
    61. * @param endX 线段终点x坐标
    62. * @param endY 线段终点y坐标
    63. * @returns
    64. */
    65. const generatorLine = (endX: number, endY: number) => {
    66. return `L${endX} ${endY}`;
    67. }
    68. /**
    69. * 生成二阶贝塞尔曲线
    70. * @param controlPoint 贝塞尔曲线控制点
    71. * @param endPoint 贝塞尔曲线结束点
    72. * @returns
    73. */
    74. const generatorSecondOrderBezierCurve = (controlPoint: IPoint, endPoint: IPoint) => {
    75. return `Q${controlPoint.x} ${controlPoint.y} ${endPoint.x} ${endPoint.y}`;
    76. }
    77. /**
    78. * 判断两点是否相同
    79. */
    80. const isSamePoint = (point1: IPoint, point2: IPoint) => {
    81. return point1.x === point2.x && point1.y === point2.y
    82. }
    83. /**
    84. * 获取元素相对于容器的DomRect
    85. */
    86. const getBoundingClientRect = (id: string) => {
    87. const containerNode = document.getElementById(Container_Id);
    88. const node = document.getElementById(id);
    89. const containerRect = containerNode?.getBoundingClientRect();
    90. const rect = node?.getBoundingClientRect();
    91. if (!containerRect || !rect) return;
    92. // 获取相对于容器的 left 和 top
    93. const left = rect.left - containerRect.left;
    94. const top = rect.top - containerRect.top;
    95. return {
    96. left,
    97. top,
    98. right: left + rect.width,
    99. bottom: top + rect.height
    100. }
    101. }
    102. /**
    103. * 绘制一个圆角矩形,该函数使用场景为:
    104. * 1. 仅获取到一个元素时,给这个元素绘制边框
    105. */
    106. function drawRectWithBorderRadius(rect: IRect, radius: number, borderStyle: IBorderStyle) {
    107. const svgDom = document.getElementById(Svg_Id);
    108. if (!svgDom) return;
    109. const pathdom = document.createElementNS("http://www.w3.org/2000/svg", 'rect');
    110. svgDom.appendChild(pathdom);
    111. const { left, top, right, bottom } = rect;
    112. const { color, width } = borderStyle || {};
    113. pathdom.setAttribute("x", String(left));
    114. pathdom.setAttribute("y", String(top));
    115. pathdom.setAttribute("rx", String(radius));
    116. pathdom.setAttribute("ry", String(radius));
    117. pathdom.setAttribute("width", String(right - left));
    118. pathdom.setAttribute("height", String(bottom - top));
    119. pathdom.setAttribute("fill", "none");
    120. pathdom.setAttribute("stroke", color || 'black');
    121. pathdom.setAttribute("stroke-width", `${width || 1}px`);
    122. }
    123. /**
    124. * 绘制svg路径,radius为矩形圆角半径,类似 border-radius
    125. */
    126. function drawSvgPath(rect1: IRect, rect2: IRect, radius: number, borderStyle: IBorderStyle) {
    127. let { left: left1, top: top1, right: right1, bottom: bottom1 } = rect1;
    128. let { left: left2, top: top2, right: right2, bottom: bottom2 } = rect2;
    129. // tab标题元素顶点
    130. const dotMap1: RecordIPoint> = {
    131. leftTop: { x: left1, y: top1, },
    132. leftBottom: { x: left1, y: bottom1 },
    133. rightTop: { x: right1, y: top1 },
    134. rightBottom: { x: right1, y: bottom1 },
    135. }
    136. // 内容区元素顶点
    137. const dotMap2: RecordIPoint> = {
    138. leftTop: { x: left2, y: top2 },
    139. leftBottom: { x: left2, y: bottom2 },
    140. rightTop: { x: right2, y: top2 },
    141. rightBottom: { x: right2, y: bottom2 },
    142. }
    143. // 当前tab顶边是否和内容区对齐,若对齐,tab标题的右上角顶点 和 tab内容的左上角顶点,在绘制path时,可以不考虑其svg路径
    144. const isTopTab = isSamePoint(dotMap1.rightTop, dotMap2.leftTop);
    145. // 当前tab底边是否和内容区对齐,若对齐,tab标题的右下角顶点 和 tab内容的左下角顶点,在绘制path时,可以不考虑其svg路径
    146. const isBottomTab = isSamePoint(dotMap1.rightBottom, dotMap2.leftBottom);
    147. // 当前tab标题右下角的圆弧和tab内容区左下角的圆弧,相交了
    148. const isBottomRadiusConnect = (bottom2 - bottom1) < (radius * 2);
    149. // 当前tab标题右上角的圆弧和tab内容区左上角的圆弧,相交了
    150. const isTopRadiusConnect = (top1 - top2) < (radius * 2);
    151. // 当前tab标题的边框高度,已经无法容纳两个圆弧了
    152. const isTabTitleShort = (bottom1 - top1) < (radius * 2);
    153. dotMap1.leftTop = {
    154. ...dotMap1.leftTop,
    155. arc: isTabTitleShort
    156. ? generatorSecondOrderBezierCurve(dotMap1.leftTop, { x: left1 + radius, y: top1 })
    157. : generatorArc(left1 + radius, top1, radius),
    158. line: isTopTab ? generatorLine(right2 - radius, top2) : generatorLine(right1 - radius, top1),
    159. }
    160. dotMap1.rightTop = {
    161. ...dotMap1.rightTop,
    162. arc: isTopTab
    163. ? ''
    164. : isTopRadiusConnect
    165. ? generatorSecondOrderBezierCurve(dotMap1.rightTop, { x: right1, y: top1 - ((top1 - top2) / 2) })
    166. : generatorArc(right1, top1 - radius, radius, ESweepFlag.ccw),
    167. line: (isTopTab || isTopRadiusConnect) ? '' : generatorLine(left2, top2 + radius)
    168. }
    169. dotMap2.leftTop = {
    170. ...dotMap2.leftTop,
    171. arc: isTopTab
    172. ? ''
    173. : isTopRadiusConnect
    174. ? generatorSecondOrderBezierCurve(dotMap2.leftTop, { x: left2 + radius, y: top2 })
    175. : generatorArc(left2 + radius, top2, radius),
    176. line: isTopTab ? '' : generatorLine(right2 - radius, top2),
    177. }
    178. dotMap2.rightTop = {
    179. ...dotMap1.rightTop,
    180. arc: generatorArc(right2, top2 + radius, radius),
    181. line: generatorLine(right2, bottom2 - radius),
    182. }
    183. dotMap2.rightBottom = {
    184. ...dotMap2.rightBottom,
    185. arc: generatorArc(right2 - radius, bottom2, radius),
    186. line: isBottomTab ? generatorLine(left1 + radius, bottom2) : generatorLine(left2 + radius, bottom2),
    187. }
    188. dotMap2.leftBottom = {
    189. ...dotMap2.leftBottom,
    190. arc: isBottomTab
    191. ? ''
    192. : isBottomRadiusConnect
    193. ? generatorSecondOrderBezierCurve(dotMap2.leftBottom, { x: left2, y: bottom2 - ((bottom2 - bottom1) / 2) })
    194. : generatorArc(left2, bottom2 - radius, radius),
    195. line: (isBottomTab || isBottomRadiusConnect) ? '' : generatorLine(right1, bottom1 + radius)
    196. }
    197. dotMap1.rightBottom = {
    198. ...dotMap1.rightBottom,
    199. arc: isBottomTab
    200. ? ''
    201. : isBottomRadiusConnect
    202. ? generatorSecondOrderBezierCurve(dotMap1.rightBottom, { x: right1 - radius, y: bottom1 })
    203. : generatorArc(right1 - radius, bottom1, radius, ESweepFlag.ccw),
    204. line: isBottomTab ? '' : generatorLine(left1 + radius, bottom1)
    205. }
    206. dotMap1.leftBottom = {
    207. ...dotMap1.leftBottom,
    208. arc: isTabTitleShort
    209. ? generatorSecondOrderBezierCurve(dotMap1.leftBottom, { x: left1, y: bottom1 - ((bottom1 - top1) / 2) })
    210. : generatorArc(left1, bottom1 - radius, radius),
    211. line: 'Z' // 该点是绘制的结束点
    212. }
    213. // 按path数组点的顺序,依次绘制path
    214. const path = [
    215. dotMap1.leftTop,
    216. dotMap1.rightTop,
    217. dotMap2.leftTop,
    218. dotMap2.rightTop,
    219. dotMap2.rightBottom,
    220. dotMap2.leftBottom,
    221. dotMap1.rightBottom,
    222. dotMap1.leftBottom
    223. ];
    224. const pathString = path.map((item) => `${item.arc} ${item.line}`)
    225. // SVG 路径的绘制起点
    226. const startPoint = {
    227. x: isTabTitleShort ? left1 : path[0].x,
    228. y: isTabTitleShort ? top1 + ((bottom1 - top1) / 2) : (path[0].y + radius)
    229. }
    230. /**
    231. * 绘制的起点为:
    232. * {
    233. * x: dotMap1.leftTop.x,
    234. * y: dotMap1.leftTop.y + radius
    235. * }
    236. */
    237. const svgPath = `M${startPoint.x} ${startPoint.y} ${pathString.join(' ')}`;
    238. const svgDom = document.getElementById(Svg_Id);
    239. if (!svgDom) return;
    240. const pathDom = document.createElementNS("http://www.w3.org/2000/svg", 'path');
    241. svgDom.appendChild(pathDom);
    242. const { color, width } = borderStyle || {};
    243. pathDom.setAttribute("d", svgPath);
    244. pathDom.setAttribute("fill", "none");
    245. pathDom.setAttribute("stroke", color || 'black');
    246. pathDom.setAttribute("stroke-width", `${width || 1}px`);
    247. }
    248. function mergeRectSideAndGetNewRect(rect1: IRect, rect2: IRect, direction: EDirection, radius: number) {
    249. let newRect1: IRect = { top: rect1.top, left: rect1.left, bottom: rect1.bottom, right: rect1.right };
    250. let newRect2: IRect = { top: rect2.top, left: rect2.left, bottom: rect2.bottom, right: rect2.right };
    251. let isOversize = false; // 两元素是否水平/垂直平移不相交(垂直布局中,水平平移;水平布局中,垂直平移)
    252. if (direction === EDirection.column) {
    253. /**
    254. * 水平布局,固定tab在左边,后续的代码逻辑中,我们将 rect1 视为左边标题区,rect2 视为右边内容区
    255. * 如果发现实际位置是相反的,那么需要对变量进行交换,确保 rect1 在左,rect2 在右
    256. */
    257. if (newRect1.left > newRect2.left) {
    258. const tempRect = newRect1;
    259. newRect1 = newRect2;
    260. newRect2 = tempRect;
    261. }
    262. newRect1.right = newRect2.left;
    263. if (newRect1.top < newRect2.top) newRect1.top = newRect2.top;
    264. if (newRect1.bottom > newRect2.bottom) newRect1.bottom = newRect2.bottom;
    265. if (
    266. // 如果 tab标题 已经无法通过水平平移,和内容区相交了,那也不用给tab标题加border了
    267. newRect1.bottom < newRect2.top ||
    268. newRect1.top > newRect2.bottom
    269. // 如果tab标题的border框高度,已经不足以容纳两倍的圆角,那也不用给tab标题加border了
    270. // (newRect2.bottom - newRect1.top) <= (radius * 2 ) ||
    271. // (newRect1.bottom - newRect2.top) <= (radius * 2)
    272. ) {
    273. isOversize = true;
    274. };
    275. } else if (direction === EDirection.row) {
    276. // TODO: 后续增加水平布局
    277. }
    278. return {
    279. rect1: newRect1,
    280. rect2: newRect2,
    281. isOversize
    282. }
    283. }
    284. function updateSvgBorder() {
    285. const svgDom = document.getElementById(Svg_Id);
    286. if (!svgDom.children[0]) return;
    287. svgDom.removeChild(svgDom.children[0]);
    288. }
    289. /**
    290. * 使用SVG绘制边框
    291. * @param elementId1 tab元素ID
    292. * @param elementId2 内容区元素ID
    293. * @param direction tab布局(水平或垂直)
    294. * @param borderStyle 边框样式
    295. */
    296. export default function drawSVGBorder(
    297. elementId1: string = '',
    298. elementId2: string = '',
    299. direction = EDirection.column,
    300. borderStyle: IBorderStyle
    301. ) {
    302. updateSvgBorder();
    303. if (!elementId1 || !elementId2) return; // 传入的元素id为空时,什么都不做
    304. const radius = borderStyle.radius || 6;
    305. // let rect1 = document.getElementById(elementId1)?.getBoundingClientRect?.();
    306. // let rect2 = document.getElementById(elementId2)?.getBoundingClientRect?.();
    307. let rect1 = getBoundingClientRect(elementId1);
    308. let rect2 = getBoundingClientRect(elementId2);
    309. if (!rect1 && !rect2) return; // 两个元素都没拿到时,什么都不做
    310. /**
    311. * 只能获取到一个元素时,这个场景有两种:
    312. * 1. 获取不到的元素是tab标题,标题列表滚动后,这个元素已经不在视口内,由于虚拟滚动,元素不会渲染,因此获取不到
    313. * 2. 元素tab标题能获取到,但是获取不到内容区
    314. */
    315. if (
    316. (!rect1 && rect2) ||
    317. (!rect2 && rect1)
    318. ) {
    319. // 给仅剩的dom元素画边框,一个圆角矩形
    320. drawRectWithBorderRadius(rect1 || rect2, radius, borderStyle);
    321. return;
    322. }
    323. const { rect1: newRect1, rect2: newRect2, isOversize } = mergeRectSideAndGetNewRect(rect1, rect2, direction, radius);
    324. if (isOversize) {
    325. drawRectWithBorderRadius(newRect2, radius, borderStyle); // 两元素平移不相交,则仅对内容区画边框
    326. } else {
    327. drawSvgPath(newRect1, newRect2, radius, borderStyle);
    328. }
    329. }

    dac71fcba768c35ad2a4da4468ef7db4.png

    团队介绍

    我们是淘天集团-营销中后台前端团队,负责核心的淘宝&天猫营销业务,搭建千级运营小二、百万级商家和亿级消费者三方之间的连接通道,在这里将有机会参与到618、双十一等大型营销活动,与上下游伙伴协同作战,参与百万级流量后台场景的前端基础能力建设,通过标准化、归一化、模块化、低代码化的架构设计,保障商家与运营的经营体验和效率;参与面向亿级消费者的万级活动页面快速生产背后的架构设计、交付手段和协作方式。

    ¤ 拓展阅读 ¤

    3DXR技术 | 终端技术 | 音视频技术

    服务端技术 | 技术质量 | 数据算法

  • 相关阅读:
    Spring中Bean的作用域
    MySQL索引、事务与存储引擎
    Electron实战之环境搭建
    redis应用——实现访问量案例(redis+定时任务+分布式锁)
    HDFS 联邦
    JavaScript实现选择排序
    SSRF 漏洞笔记
    电脑如何查看代理服务器IP?
    Windows 0x80190001错误登录失败
    IT行业的革新力量:技术进步与未来展望
  • 原文地址:https://blog.csdn.net/Taobaojishu/article/details/136311730