前在這篇文章中 -- 《老生常談之 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小時內與您取得聯系。