前在这篇文章中 -- 《老生常谈之 CSS 实现三角形》,介绍了 6 种使用 CSS 实现三角形的方式。
但是其中漏掉了一个非常重要的场景,如何使用纯 CSS 实现带圆角的三角形呢?,像是这样:
本文将介绍几种实现带圆角的三角形的实现方式。
想要生成一个带圆角的三角形,代码量最少、最好的方式是使用 SVG 生成。
使用 SVG 的 多边形标签 <polygon> 生成一个三边形,使用 SVG 的 stroke-linejoin="round" 生成连接处的圆角。
代码量非常少,核心代码如下:
<svg width="250" height="250" viewBox="-50 -50 300 300">
<polygon class="triangle" stroke-linejoin="round" points="100,0 0,200 200,200"/>
</svg>
.triangle {
fill: #0f0;
stroke: #0f0;
stroke-width: 10;
}
实际图形如下:
这里,其实是借助了 SVG 多边形的 stroke-linejoin: round 属性生成的圆角,stroke-linejoin 是什么?它用来控制两条描边线段之间,有三个可选值:
我们实际是通过一个带边框,且边框连接类型为 stroke-linejoin: round 的多边形生成圆角三角形的。
如果,我们把底色和边框色区分开,实际是这样的:
.triangle {
fill: #0f0;
stroke: #000;
stroke-width: 10;
}
那么如何控制圆角大小呢?也非常简单,通过控制 stroke-width 的大小,可以改变圆角的大小。
当然,要保持三角形大小一致,在增大/缩小 stroke-width 的同时,需要缩小/增大图形的 width/height:
完整的 DEMO 你可以戳这里:CodePen Demo -- 使用 SVG 实现带圆角的三角形
不过,上文提到了,使用纯 CSS 实现带圆角的三角形,但是上述第一个方法其实是借助了 SVG。那么仅仅使用 CSS,有没有办法呢?
当然,发散思维,CSS 有意思的地方正在于此处,用一个图形,能够有非常多种巧妙的解决方案!
我们看看,一个圆角三角形,它其实可以被拆分成几个部分:
所以,其实我们只需要能够画出一个这样的带圆角的菱形,通过 3 个进行旋转叠加,就能得到圆角三角形:
那么,接下来我们的目标就变成了绘制一个带圆角的菱形,方法有很多,本文给出其中一种方式:
<div></div>
div {
width: 10em;
height: 10em;
transform: rotate(-60deg) skewX(-30deg) scale(1, 0.866);
}
div {
width: 10em;
height: 10em;
transform: rotate(-60deg) skewX(-30deg) scale(1, 0.866);
+ border-top-right-radius: 30%;
}
至此,我们就顺利地得到一个带圆角的菱形了!
接下来就很简单了,我们只需要利用元素的另外两个伪元素,再生成 2 个带圆角的菱形,将一共 3 个图形旋转位移拼接起来即可!
完整的代码如下:
<div></div>
div{
position: relative;
background-color: orange;
}
div:before,
div:after {
content: '';
position: absolute;
background-color: inherit;
}
div,
div:before,
div:after {
width: 10em;
height: 10em;
border-top-right-radius: 30%;
}
div {
transform: rotate(-60deg) skewX(-30deg) scale(1,.866);
}
div:before {
transform: rotate(-135deg) skewX(-45deg) scale(1.414, .707) translate(0,-50%);
}
div:after {
transform: rotate(135deg) skewY(-45deg) scale(.707, 1.414) translate(50%);
}
就可以得到一个圆角三角形了!效果如下:
完整的代码你可以戳这里:CodePen Demo -- A triangle with rounded
完了吗?没有!
上述方案,虽然不算太复杂,但是有一点还不算太完美的。就是无法支持渐变色的圆角三角形。像是这样:
如果需要实现渐变色圆角三角形,还是有点复杂的。但真就还有人鼓捣出来了,下述方法参考至 -- How to make 3-corner-rounded triangle in CSS。
同样也是利用了多块进行拼接,但是这次我们的基础图形,会非常的复杂。
首先,我们需要实现这样一个容器外框,和上述的方法比较类似,可以理解为是一个圆角菱形(画出 border 方便理解):
<div></div>
div {
width: 200px;
height: 200px;
transform: rotate(30deg) skewY(30deg) scaleX(0.866);
border: 1px solid #000;
border-radius: 20%;
}
接着,我们同样使用两个伪元素,实现两个稍显怪异的图形进行拼接,算是对 transform 的各种用法的合集:
div::before,
div::after {
content: "";
position: absolute;
width: 200px;
height: 200px;
}
div::before {
border-radius: 20% 20% 20% 55%;
transform: scaleX(1.155) skewY(-30deg) rotate(-30deg) translateY(-42.3%) skewX(30deg) scaleY(0.866) translateX(-24%);
background: red;
}
div::after {
border-radius: 20% 20% 55% 20%;
background: blue;
transform: scaleX(1.155) skewY(-30deg) rotate(-30deg) translateY(-42.3%) skewX(-30deg) scaleY(0.866) translateX(24%);
}
为了方便理解,制作了一个简单的变换动画:
本质就是实现了这样一个图形:
最后,给父元素添加一个 overflow: hidden 并且去掉父元素的 border 即可得到一个圆角三角形:
由于这两个元素重叠空间的特殊结构,此时,给两个伪元素添加同一个渐变色,会完美的叠加在一起:
div::before,
div::after, {
background: linear-gradient(#0f0, #03a9f4);
}
最终得到一个渐变圆角三角形:
上述各个图形的完整代码,你可以戳这里:CodePen Demo -- A triangle with rounded and gradient background
本文介绍了几种在 CSS 中实现带圆角三角形的方式,虽然部分有些繁琐,但是也体现了 CSS ”有趣且折磨人“ 的一面,具体应用的时候,还是要思考一下,对是否使用上述方式进行取舍,有的时候,切图也许是更好的方案。
边框的设定在web设计中使用率非常的高,border:1px solid #00f;属于标准的边线写法,也可以实现单方向边线border-left:1px solid red;
1px red solid边线
(单边线)左边线
在CSS标准盒模型中,边线border是计算在容器总宽度和高度之中的,
总宽高是102*102
浏览器中呈现的总宽度和总高度102*102
但随着web布局要求越来越高,自适应布局应用逐渐广泛,横向排布四个div 各占据四分之一的宽度,但如果某一个要有边线border修饰,因为border是占据宽度的,最终会导致最后一个元素掉下来,因为实际宽度大于了总宽度。
各占据四分之一的宽度
第四个元素掉了下来
outline可以实现和border相同的效果,标准语法也基本相同(outline:1px solid red),也支持outline-style,outline-width,outline-color等分散属性。
但是outline不占位,不会增加元素的宽高。
outline使用,总宽高不变还是100*100
outline标准写法
outline缺点:
①不支持圆角 outline-radius:3px;
②不支持单方向outline。
不支持部分属性
默认的文本框input[type="text"]获取光标时会有边线高亮。
文本框高亮获取光标(新版本之前是蓝色边线)
实际上高亮的部分为outline在起作用
.text:focus{outline: 3px solid #00f;}
使用outline:none,可以去除默认文本框获取光标时出现的边线
outline:none
.outline{
/*标准写法*/
outline:1px solid red;
/*单方向边线*/
outline-left:4px solid #000;
}
.outline:focus{
/*去除默认边线*/
outline: none;
}
outline作为一个特殊的属性存在,在特殊的场景中会产生很棒的效果,灵活使用才能发挥出最大作用。
渐 大淘宝技术 2024-02-26 16:21 浙江
最近的需求中有一个tab切换的场景,设计师提出了自己期望的效果,核心关注点在蓝色边框上,本文围绕如何实现这样的边框效果展开讨论。
背景
设计师期望的效果如下,核心关注点在蓝色边框上。
,时长00:08
实现这样的边框,核心问题有几个:
CSS
我决定先用CSS试试,border + border-radius,应该轻松搞定。
这倒不难,我们需要:
,时长00:04
这时候缺点已经来了,我们通过加背景色遮盖边框实现边框相连,不可避免地遮盖了页面内容,如果页面背景比较复杂,我们会很难处理。这个方案并不足够通用,但好在我们的场景页面背景纯白,先忍了。
也还行,我们需要:
,时长00:09
其实和问题一一样,我们又使用了背景色对边框进行遮盖,但先忍了,实现要紧。
这个问题用css就比较难实现了,它可以被拆解成两个子问题:
如果世界上已经没有其他方式能实现这样的边框,我想硬着头皮写一堆恶心逻辑也是能实现效果的,但我觉得这样的实现比较丑陋,不太优雅,因此 CSS 的尝试到这里就结束了,我决定换个方案。
SVG
其实使用SVG来实现一些CSS不好处理的场景在社区中已经有很多实践了。比如用于新人引导的开源库 driver.js。
driver.js地址:https://driverjs.com/
【新人引导】指的是这样的场景:
,时长00:13
这个场景下,【蒙层内区域高亮】是技术核心,driver.js 在几个月前刚进行了一次重构,将蒙层改用SVG实现,支持了高亮区的圆角。这给了我启发,哥们也用 SVG 画个边框吧。
svg嘛,用起来就是更麻烦,先从简单的开始吧:
这个容易,使用 <line /> 标签,提供两个点坐标(x1, y1)、(x2, y2),在描述一下边框的样式就可以了。
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<line x1="0" y1="80" x2="100" y2="20" stroke="black" />
</svg>
使用 line 标签的方式固然可以,但为了方便后续代码逻辑,我们还有更好的方式:<path />标签,我们可以通过命令式的方式,完成 SVG 各种型状的绘制,比如一条直线:
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<path d="M 0 80 L 100 20" stroke="black" fill="none" />
</svg>
<path />文档地址:https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial/Paths
其中核心字段位 d="M 0 80 L 100 20",这一段命令中有两个指令 M、L:
关于 path 的其他指令不再赘述,总的来说,想使用 path 绘制边框,我们首先要获取到边框上各个结点坐标,之后再用命令将他们链接起来。
我们首先获取 tab元素 和 内容区 的四个节点,我们通过getBoundingClientRect方法获取 top、left、right、bottom 四个值来构造这些点坐标。
但我们不能直接给他两点相连起来,那就成这样了:
我们需要做做个调整,需要将(right1, top1)、(right1, bottom1)两个点的 x 坐标做偏移,让这两点的 x 和元素2的 left 一致,得到(left2, top1)、(left2, bottom1)
我们再给这些点加上编号,按照 ABCDEFGH 的顺序,将这些点通过直线相连,path的命令就会如下:
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<path
d="
M left1 top1
L left2 top1
L left2 top2
L right2 top2
L right2 bottom2
L left2 bottom2
L left2 bottom1
L left1 bottom1
Z
"
stroke="black"
fill="none"
/>
<!-- Z 命令为 path 结束指令-->
</svg>
这样实现的边框,不会有 CSS 背景色遮挡的问题。
问题又变得复杂起来了,同样,我们还是先从简单的开始吧:
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:弧线终点坐标
下边是一些示例:
<svg width="325" height="325" xmlns="http://www.w3.org/2000/svg">
<path
d="M 80 80
A 45 45, 0, 0, 0, 125 125
L 125 80 Z"
fill="green"
/>
<path
d="M 230 80
A 45 45, 0, 1, 0, 275 125
L 275 80 Z"
fill="red"
/>
<path
d="M 80 230
A 45 45, 0, 0, 1, 125 275
L 125 230 Z"
fill="purple"
/>
<path
d="M 230 230
A 45 45, 0, 1, 1, 275 275
L 275 230 Z"
fill="blue"
/>
</svg>
效果如下(有颜色区域是最终形状,其他线条是辅助线):
上文中,我们已经拿到了 ABCDEFGH 8个点,每一个点其实都会有一个对应的圆弧,因此在绘制边框的时候,我是这样管理 圆弧 和 直线 的,下边是一个点的数据结构:
const A={
x: 100,
y: 100,
arc: 'A xxxxxx', // 经过该点的圆弧
line: 'L xxxxxx' // 圆弧的结束点到下一个圆弧起点的直线
}
根据这个结构,我再按 ABCDEFGH 的顺序,将每个点的 svg 指令拼接起来,先拼接 圆弧(arc) 再拼接 直线(line)
那么圆弧的指令如何生成呢,我们以一个点来分析:
有了这些信息,其实一个圆弧的指令就呼之欲出了,我们通过一段代码快速生成(两个为 0 的值上文介绍A指令时有提到,不赘述原因):
enum ESweepFlag {
cw=1, // 顺时针
ccw=0, // 逆时针
}
/**
* 生成圆弧svg路径
* @param endX: 圆弧终点x坐标
* @param endY: 圆弧终点y坐标
* @param radius: 圆弧半径
* @param sweepFlag: 顺时针还是逆时针: 1 顺时针、0 逆时针
*/
const generatorArc=(endX: number, endY: number, radius: number, sweepFlag: ESweepFlag=ESweepFlag.cw)=> {
return `A${radius} ${radius} 0 0 ${sweepFlag} ${endX} ${endY}`;
}
到这里,我们将 圆弧 和 直线 指令,按 ABCDEFGH 点顺序,先圆弧后直线挨个拼接起来,边框也就画成了。
我们先看看理想效果:
,时长00:05
在上文中我们提到,这个问题其实可以拆解为两个子问题:
在元素已经有一部分离屏的时候,我们需要对点进行修正:
同理,元素往底部离屏的时候,我们强制更新 H 点,丢弃 G、F 点即可。
其实有6个圆角(A、B、C、F、G、H点对应的圆角)需要过渡到直线。我们以 B、C 两点为例:
如何通过SVG表达这种过渡曲线呢?我们可以使用圆弧命令 A 的能力,因为它支持椭圆,不过我们还有另一种方式:二次贝塞尔曲线,一个二次贝塞尔曲线由 起点、终点 和一个 控制点 组成,每个圆弧我们其实正好能拿到3与之对应的点。
二次贝塞尔曲线在 SVG path 中通过 Q 指令绘制Q x1 y1, x y,在SVG中,起点为画笔位置,因此Q指令指定 控制点 和 终点:
其他问题
到这,核心卡点问题我们都已经解决了,实际上最终也实现了一版,达到了设计师想要的效果,但还存在一些遗留问题:
由于要随滚动不断计算并渲染SVG边框,因此性能开销比较大。后续需要在算法上进行优化,才能真正达到高体验的标准。
我们的算法基本是为水平布局定制的,如果布局切换到垂直布局,很多地方需要改动,因此当前方案的通用性并不佳。
使用绘制边框SVG的源码附上,drawSVGBorder方法为入口:
/**
* 用于绘制边框的svg的id
*/
export const Svg_Id='____TAB_CONTAINER_BORDER_SVG_ID_MAKABAKA____';
/**
* tab容器的id
*/
export const Container_Id='__MKT_TAB_CONTAINER_ID__';
export interface IBorderStyle {
/**
* 边框颜色
*/
color?: string;
/**
* 边框宽度
*/
width?: number;
/**
* 边框圆角
*/
radius?: number;
}
/**
* 为了绘制svg边框,需要将两个dom元素的四个顶点定义出来
* 为了方便svg最终路径生成,因此每个点还会存储两个信息:
* 1. 经过这个点的圆弧的svg路径
* 2. 这个点到下一个圆弧起点的svg路径
*/
interface IPoint {
x: number;
y: number;
arc?: string; // 圆弧svg路径
line?: string; // 连线svg路径
}
interface IRect {
left: number;
top: number;
right: number;
bottom: number;
}
export enum EDirection {
column='column',
row='row',
}
enum ESweepFlag {
cw=1, // 顺时针
ccw=0, // 逆时针
}
/**
* 生成圆弧svg路径
* @param endX: 圆弧终点x坐标
* @param endY: 圆弧终点y坐标
* @param radius: 圆弧半径
* @param sweepFlag: 顺时针还是逆时针: 1 顺时针、0 逆时针
*/
const generatorArc=(endX: number, endY: number, radius: number, sweepFlag: ESweepFlag=ESweepFlag.cw)=> {
return `A${radius} ${radius} 0 0 ${sweepFlag} ${endX} ${endY}`;
}
/**
* 生成险段svg路径
* @param endX 线段终点x坐标
* @param endY 线段终点y坐标
* @returns
*/
const generatorLine=(endX: number, endY: number)=> {
return `L${endX} ${endY}`;
}
/**
* 生成二阶贝塞尔曲线
* @param controlPoint 贝塞尔曲线控制点
* @param endPoint 贝塞尔曲线结束点
* @returns
*/
const generatorSecondOrderBezierCurve=(controlPoint: IPoint, endPoint: IPoint)=> {
return `Q${controlPoint.x} ${controlPoint.y} ${endPoint.x} ${endPoint.y}`;
}
/**
* 判断两点是否相同
*/
const isSamePoint=(point1: IPoint, point2: IPoint)=> {
return point1.x===point2.x && point1.y===point2.y
}
/**
* 获取元素相对于容器的DomRect
*/
const getBoundingClientRect=(id: string)=> {
const containerNode=document.getElementById(Container_Id);
const node=document.getElementById(id);
const containerRect=containerNode?.getBoundingClientRect();
const rect=node?.getBoundingClientRect();
if (!containerRect || !rect) return;
// 获取相对于容器的 left 和 top
const left=rect.left - containerRect.left;
const top=rect.top - containerRect.top;
return {
left,
top,
right: left + rect.width,
bottom: top + rect.height
}
}
/**
* 绘制一个圆角矩形,该函数使用场景为:
* 1. 仅获取到一个元素时,给这个元素绘制边框
*/
function drawRectWithBorderRadius(rect: IRect, radius: number, borderStyle: IBorderStyle) {
const svgDom=document.getElementById(Svg_Id);
if (!svgDom) return;
const pathdom=document.createElementNS("http://www.w3.org/2000/svg", 'rect');
svgDom.appendChild(pathdom);
const { left, top, right, bottom }=rect;
const { color, width }=borderStyle || {};
pathdom.setAttribute("x", String(left));
pathdom.setAttribute("y", String(top));
pathdom.setAttribute("rx", String(radius));
pathdom.setAttribute("ry", String(radius));
pathdom.setAttribute("width", String(right - left));
pathdom.setAttribute("height", String(bottom - top));
pathdom.setAttribute("fill", "none");
pathdom.setAttribute("stroke", color || 'black');
pathdom.setAttribute("stroke-width", `${width || 1}px`);
}
/**
* 绘制svg路径,radius为矩形圆角半径,类似 border-radius
*/
function drawSvgPath(rect1: IRect, rect2: IRect, radius: number, borderStyle: IBorderStyle) {
let { left: left1, top: top1, right: right1, bottom: bottom1 }=rect1;
let { left: left2, top: top2, right: right2, bottom: bottom2 }=rect2;
// tab标题元素顶点
const dotMap1: Record<string, IPoint>={
leftTop: { x: left1, y: top1, },
leftBottom: { x: left1, y: bottom1 },
rightTop: { x: right1, y: top1 },
rightBottom: { x: right1, y: bottom1 },
}
// 内容区元素顶点
const dotMap2: Record<string, IPoint>={
leftTop: { x: left2, y: top2 },
leftBottom: { x: left2, y: bottom2 },
rightTop: { x: right2, y: top2 },
rightBottom: { x: right2, y: bottom2 },
}
// 当前tab顶边是否和内容区对齐,若对齐,tab标题的右上角顶点 和 tab内容的左上角顶点,在绘制path时,可以不考虑其svg路径
const isTopTab=isSamePoint(dotMap1.rightTop, dotMap2.leftTop);
// 当前tab底边是否和内容区对齐,若对齐,tab标题的右下角顶点 和 tab内容的左下角顶点,在绘制path时,可以不考虑其svg路径
const isBottomTab=isSamePoint(dotMap1.rightBottom, dotMap2.leftBottom);
// 当前tab标题右下角的圆弧和tab内容区左下角的圆弧,相交了
const isBottomRadiusConnect=(bottom2 - bottom1) < (radius * 2);
// 当前tab标题右上角的圆弧和tab内容区左上角的圆弧,相交了
const isTopRadiusConnect=(top1 - top2) < (radius * 2);
// 当前tab标题的边框高度,已经无法容纳两个圆弧了
const isTabTitleShort=(bottom1 - top1) < (radius * 2);
dotMap1.leftTop={
...dotMap1.leftTop,
arc: isTabTitleShort
? generatorSecondOrderBezierCurve(dotMap1.leftTop, { x: left1 + radius, y: top1 })
: generatorArc(left1 + radius, top1, radius),
line: isTopTab ? generatorLine(right2 - radius, top2) : generatorLine(right1 - radius, top1),
}
dotMap1.rightTop={
...dotMap1.rightTop,
arc: isTopTab
? ''
: isTopRadiusConnect
? generatorSecondOrderBezierCurve(dotMap1.rightTop, { x: right1, y: top1 - ((top1 - top2) / 2) })
: generatorArc(right1, top1 - radius, radius, ESweepFlag.ccw),
line: (isTopTab || isTopRadiusConnect) ? '' : generatorLine(left2, top2 + radius)
}
dotMap2.leftTop={
...dotMap2.leftTop,
arc: isTopTab
? ''
: isTopRadiusConnect
? generatorSecondOrderBezierCurve(dotMap2.leftTop, { x: left2 + radius, y: top2 })
: generatorArc(left2 + radius, top2, radius),
line: isTopTab ? '' : generatorLine(right2 - radius, top2),
}
dotMap2.rightTop={
...dotMap1.rightTop,
arc: generatorArc(right2, top2 + radius, radius),
line: generatorLine(right2, bottom2 - radius),
}
dotMap2.rightBottom={
...dotMap2.rightBottom,
arc: generatorArc(right2 - radius, bottom2, radius),
line: isBottomTab ? generatorLine(left1 + radius, bottom2) : generatorLine(left2 + radius, bottom2),
}
dotMap2.leftBottom={
...dotMap2.leftBottom,
arc: isBottomTab
? ''
: isBottomRadiusConnect
? generatorSecondOrderBezierCurve(dotMap2.leftBottom, { x: left2, y: bottom2 - ((bottom2 - bottom1) / 2) })
: generatorArc(left2, bottom2 - radius, radius),
line: (isBottomTab || isBottomRadiusConnect) ? '' : generatorLine(right1, bottom1 + radius)
}
dotMap1.rightBottom={
...dotMap1.rightBottom,
arc: isBottomTab
? ''
: isBottomRadiusConnect
? generatorSecondOrderBezierCurve(dotMap1.rightBottom, { x: right1 - radius, y: bottom1 })
: generatorArc(right1 - radius, bottom1, radius, ESweepFlag.ccw),
line: isBottomTab ? '' : generatorLine(left1 + radius, bottom1)
}
dotMap1.leftBottom={
...dotMap1.leftBottom,
arc: isTabTitleShort
? generatorSecondOrderBezierCurve(dotMap1.leftBottom, { x: left1, y: bottom1 - ((bottom1 - top1) / 2) })
: generatorArc(left1, bottom1 - radius, radius),
line: 'Z' // 该点是绘制的结束点
}
// 按path数组点的顺序,依次绘制path
const path=[
dotMap1.leftTop,
dotMap1.rightTop,
dotMap2.leftTop,
dotMap2.rightTop,
dotMap2.rightBottom,
dotMap2.leftBottom,
dotMap1.rightBottom,
dotMap1.leftBottom
];
const pathString=path.map((item)=> `${item.arc} ${item.line}`)
// SVG 路径的绘制起点
const startPoint={
x: isTabTitleShort ? left1 : path[0].x,
y: isTabTitleShort ? top1 + ((bottom1 - top1) / 2) : (path[0].y + radius)
}
/**
* 绘制的起点为:
* {
* x: dotMap1.leftTop.x,
* y: dotMap1.leftTop.y + radius
* }
*/
const svgPath=`M${startPoint.x} ${startPoint.y} ${pathString.join(' ')}`;
const svgDom=document.getElementById(Svg_Id);
if (!svgDom) return;
const pathDom=document.createElementNS("http://www.w3.org/2000/svg", 'path');
svgDom.appendChild(pathDom);
const { color, width }=borderStyle || {};
pathDom.setAttribute("d", svgPath);
pathDom.setAttribute("fill", "none");
pathDom.setAttribute("stroke", color || 'black');
pathDom.setAttribute("stroke-width", `${width || 1}px`);
}
function mergeRectSideAndGetNewRect(rect1: IRect, rect2: IRect, direction: EDirection, radius: number) {
let newRect1: IRect={ top: rect1.top, left: rect1.left, bottom: rect1.bottom, right: rect1.right };
let newRect2: IRect={ top: rect2.top, left: rect2.left, bottom: rect2.bottom, right: rect2.right };
let isOversize=false; // 两元素是否水平/垂直平移不相交(垂直布局中,水平平移;水平布局中,垂直平移)
if (direction===EDirection.column) {
/**
* 水平布局,固定tab在左边,后续的代码逻辑中,我们将 rect1 视为左边标题区,rect2 视为右边内容区
* 如果发现实际位置是相反的,那么需要对变量进行交换,确保 rect1 在左,rect2 在右
*/
if (newRect1.left > newRect2.left) {
const tempRect=newRect1;
newRect1=newRect2;
newRect2=tempRect;
}
newRect1.right=newRect2.left;
if (newRect1.top < newRect2.top) newRect1.top=newRect2.top;
if (newRect1.bottom > newRect2.bottom) newRect1.bottom=newRect2.bottom;
if (
// 如果 tab标题 已经无法通过水平平移,和内容区相交了,那也不用给tab标题加border了
newRect1.bottom < newRect2.top ||
newRect1.top > newRect2.bottom
// 如果tab标题的border框高度,已经不足以容纳两倍的圆角,那也不用给tab标题加border了
// (newRect2.bottom - newRect1.top) <=(radius * 2 ) ||
// (newRect1.bottom - newRect2.top) <=(radius * 2)
) {
isOversize=true;
};
} else if (direction===EDirection.row) {
// TODO: 后续增加水平布局
}
return {
rect1: newRect1,
rect2: newRect2,
isOversize
}
}
function updateSvgBorder() {
const svgDom=document.getElementById(Svg_Id);
if (!svgDom.children[0]) return;
svgDom.removeChild(svgDom.children[0]);
}
/**
* 使用SVG绘制边框
* @param elementId1 tab元素ID
* @param elementId2 内容区元素ID
* @param direction tab布局(水平或垂直)
* @param borderStyle 边框样式
*/
export default function drawSVGBorder(
elementId1: string='',
elementId2: string='',
direction=EDirection.column,
borderStyle: IBorderStyle
) {
updateSvgBorder();
if (!elementId1 || !elementId2) return; // 传入的元素id为空时,什么都不做
const radius=borderStyle.radius || 6;
// let rect1=document.getElementById(elementId1)?.getBoundingClientRect?.();
// let rect2=document.getElementById(elementId2)?.getBoundingClientRect?.();
let rect1=getBoundingClientRect(elementId1);
let rect2=getBoundingClientRect(elementId2);
if (!rect1 && !rect2) return; // 两个元素都没拿到时,什么都不做
/**
* 只能获取到一个元素时,这个场景有两种:
* 1. 获取不到的元素是tab标题,标题列表滚动后,这个元素已经不在视口内,由于虚拟滚动,元素不会渲染,因此获取不到
* 2. 元素tab标题能获取到,但是获取不到内容区
*/
if (
(!rect1 && rect2) ||
(!rect2 && rect1)
) {
// 给仅剩的dom元素画边框,一个圆角矩形
drawRectWithBorderRadius(rect1 || rect2, radius, borderStyle);
return;
}
const { rect1: newRect1, rect2: newRect2, isOversize }=mergeRectSideAndGetNewRect(rect1, rect2, direction, radius);
if (isOversize) {
drawRectWithBorderRadius(newRect2, radius, borderStyle); // 两元素平移不相交,则仅对内容区画边框
} else {
drawSvgPath(newRect1, newRect2, radius, borderStyle);
}
}
团队介绍
我们是淘天集团-营销中后台前端团队,负责核心的淘宝&天猫营销业务,搭建千级运营小二、百万级商家和亿级消费者三方之间的连接通道,在这里将有机会参与到618、双十一等大型营销活动,与上下游伙伴协同作战,参与百万级流量后台场景的前端基础能力建设,通过标准化、归一化、模块化、低代码化的架构设计,保障商家与运营的经营体验和效率;参与面向亿级消费者的万级活动页面快速生产背后的架构设计、交付手段和协作方式。
*请认真填写需求信息,我们会在24小时内与您取得联系。