整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          CSS 巧妙地實現帶圓角的三角形

          CSS 巧妙地實現帶圓角的三角形

          前在這篇文章中 -- 《老生常談之 CSS 實現三角形》,介紹了 6 種使用 CSS 實現三角形的方式。

          但是其中漏掉了一個非常重要的場景,如何使用純 CSS 實現帶圓角的三角形呢?,像是這樣:

          本文將介紹幾種實現帶圓角的三角形的實現方式。

          法一. 全兼容的 SVG 大法

          想要生成一個帶圓角的三角形,代碼量最少、最好的方式是使用 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 是什么?它用來控制兩條描邊線段之間,有三個可選值:

          • miter 是默認值,表示用方形畫筆在連接處形成尖角
          • round 表示用圓角連接,實現平滑效果
          • bevel 連接處會形成一個斜接

          我們實際是通過一個帶邊框,且邊框連接類型為 stroke-linejoin: round 的多邊形生成圓角三角形的

          如果,我們把底色和邊框色區分開,實際是這樣的:

          .triangle {
              fill: #0f0;
              stroke: #000;
              stroke-width: 10;
          }

          通過 stroke-width 控制圓角大小

          那么如何控制圓角大小呢?也非常簡單,通過控制 stroke-width 的大小,可以改變圓角的大小。

          當然,要保持三角形大小一致,在增大/縮小 stroke-width 的同時,需要縮小/增大圖形的 width/height

          完整的 DEMO 你可以戳這里:CodePen Demo -- 使用 SVG 實現帶圓角的三角形

          法二. 圖形拼接

          不過,上文提到了,使用純 CSS 實現帶圓角的三角形,但是上述第一個方法其實是借助了 SVG。那么僅僅使用 CSS,有沒有辦法呢?

          當然,發散思維,CSS 有意思的地方正在于此處,用一個圖形,能夠有非常多種巧妙的解決方案!

          我們看看,一個圓角三角形,它其實可以被拆分成幾個部分:

          所以,其實我們只需要能夠畫出一個這樣的帶圓角的菱形,通過 3 個進行旋轉疊加,就能得到圓角三角形:

          繪制帶圓角的菱形

          那么,接下來我們的目標就變成了繪制一個帶圓角的菱形,方法有很多,本文給出其中一種方式:

          1. 首先將一個正方形變成一個菱形,利用 transform 有一個固定的公式:
          <div></div>
          div {
              width:  10em;
              height: 10em;
              transform: rotate(-60deg) skewX(-30deg) scale(1, 0.866);
          }
          1. 將其中一個角變成圓角:
          div {
              width:  10em;
              height: 10em;
              transform: rotate(-60deg) skewX(-30deg) scale(1, 0.866);
            + border-top-right-radius: 30%;
          }

          至此,我們就順利地得到一個帶圓角的菱形了!

          拼接 3 個帶圓角的菱形

          接下來就很簡單了,我們只需要利用元素的另外兩個偽元素,再生成 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 ”有趣且折磨人“ 的一面,具體應用的時候,還是要思考一下,對是否使用上述方式進行取舍,有的時候,切圖也許是更好的方案。

          utline與border的異同:

          邊框的設定在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-styleoutline-widthoutline-color等分散屬性。

          但是outline不占位,不會增加元素的寬高。

          outline使用,總寬高不變還是100*100

          outline標準寫法

          outline缺點:

          不支持圓角 outline-radius:3px;

          ②不支持單方向outline

          不支持部分屬性

          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


          實現這樣的邊框,核心問題有幾個:

          1. 如何將兩個元素的邊框相連
          2. 內凹的圓角如何實現
          3. tab元素滾動離屏,邊框如何過渡


          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元素 加上背景色(背景色需要和頁面背景色一致)



          ,時長00:04


          • 缺陷


          這時候缺點已經來了,我們通過加背景色遮蓋邊框實現邊框相連,不可避免地遮蓋了頁面內容,如果頁面背景比較復雜,我們會很難處理。這個方案并不足夠通用,但好在我們的場景頁面背景純白,先忍了。


          ?問題二:CSS 如何實現內凹圓角


          也還行,我們需要:

          1. 新增兩個元素,寬高和border-radius值相等
          2. 元素1 設置背景色,先覆蓋在邊框相連處
          3. 元素2 設置border-bottom-right-radius: 50%,同時border和tab元素保持一致



          ,時長00:09


          • 缺陷


          其實和問題一一樣,我們又使用了背景色對邊框進行遮蓋,但先忍了,實現要緊。


          ?問題三:CSS 如何實現滾動離屏過渡


          這個問題用css就比較難實現了,它可以被拆解成兩個子問題:

          1. tab元素的頂邊在離屏過程中需要固定,border 框選區域高度不斷變小
          2. 圓角如何平滑過渡到直線



          如果世界上已經沒有其他方式能實現這樣的邊框,我想硬著頭皮寫一堆惡心邏輯也是能實現效果的,但我覺得這樣的實現比較丑陋,不太優雅,因此 CSS 的嘗試到這里就結束了,我決定換個方案。


          SVG


          其實使用SVG來實現一些CSS不好處理的場景在社區中已經有很多實踐了。比如用于新人引導的開源庫 driver.js

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


          【新人引導】指的是這樣的場景:


          ,時長00:13


          這個場景下,【蒙層內區域高亮】是技術核心,driver.js 在幾個月前剛進行了一次重構,將蒙層改用SVG實現,支持了高亮區的圓角。這給了我啟發,哥們也用 SVG 畫個邊框吧。


          ?問題一: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",這一段命令中有兩個指令 ML

          1. M全稱“Move to”,可以理解為將SVG畫筆挪到某個點作為路徑起點,因此該命令后邊跟兩個數字,分別對應起點的 x、y
          2. L全稱“Line”,可以理解為從當前畫筆位置為起點,繪制一條直線到另一個點(終點),并且繪制后,畫筆位置也會挪到終點,(path 中大多數指令都是指定終點即可,起點就是當前畫筆位置),因此該命令后邊跟兩個數字,分別對應終點的 x、y


          關于 path 的其他指令不再贅述,總的來說,想使用 path 繪制邊框,我們首先要獲取到邊框上各個結點坐標,之后再用命令將他們鏈接起來。


          • 獲取點坐標來畫線


          我們首先獲取 tab元素 和 內容區 的四個節點,我們通過getBoundingClientRect方法獲取 topleftrightbottom 四個值來構造這些點坐標。



          但我們不能直接給他兩點相連起來,那就成這樣了:


          我們需要做做個調整,需要將(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 背景色遮擋的問題。


          ?問題二: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:弧線終點坐標


          下邊是一些示例:

          <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)


          那么圓弧的指令如何生成呢,我們以一個點來分析:


          1. 圓弧的起點坐標為(x, y-radius)
          2. 終點坐標為(x+radius, y)
          3. 半徑就是 border-radius 的值
          4. 弧線方向會有區別,兩個內凹圓角是逆時針其他圓角都是順時針


          有了這些信息,其實一個圓弧的指令就呼之欲出了,我們通過一段代碼快速生成(兩個為 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 點順序,先圓弧后直線挨個拼接起來,邊框也就畫成了。


          ?問題三:SVG 如何實現滾動離屏過渡


          我們先看看理想效果:


          ,時長00:05


          在上文中我們提到,這個問題其實可以拆解為兩個子問題:

          1. tab元素的頂邊在離屏過程中需要固定,border 框選區域高度不斷變小
          2. 圓角如何平滑過渡到直線


          • 如何固定頂邊


          在元素已經有一部分離屏的時候,我們需要對點進行修正:

          1. A 點的坐標將會被強制更新,我們用 A' 點表示新坐標,y 值永遠被固定為 top2
          2. B、C 點可以直接被丟棄,即數據結構中的 arc、line 值都可以為空,因為 A' 點可以直接一條線連到 D 點



          同理,元素往底部離屏的時候,我們強制更新 H 點,丟棄 G、F 點即可。


          • 圓角如何平滑過渡到直線


          其實有6個圓角(A、B、C、F、G、H點對應的圓角)需要過渡到直線。我們以 B、C 兩點為例:

          1. 兩點想距較遠(> 2 * radius),弧是一個正圓弧,兩半徑長度都為 radius
          2. 兩點想距較近(< 2 * radius),弧是一個橢圓弧,X軸半徑不變,Y軸半徑變為 (y1 - y2) / 2



          如何通過SVG表達這種過渡曲線呢?我們可以使用圓弧命令 A 的能力,因為它支持橢圓,不過我們還有另一種方式:二次貝塞爾曲線,一個二次貝塞爾曲線由 起點、終點 和一個 控制點 組成,每個圓弧我們其實正好能拿到3與之對應的點。


          二次貝塞爾曲線在 SVG path 中通過 Q 指令繪制Q x1 y1, x y,在SVG中,起點為畫筆位置,因此Q指令指定 控制點 和 終點:

          1. x1 y1:控制點坐標
          2. x y:終點坐標


          其他問題


          到這,核心卡點問題我們都已經解決了,實際上最終也實現了一版,達到了設計師想要的效果,但還存在一些遺留問題:


          ?性能問題


          由于要隨滾動不斷計算并渲染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、雙十一等大型營銷活動,與上下游伙伴協同作戰,參與百萬級流量后臺場景的前端基礎能力建設,通過標準化、歸一化、模塊化、低代碼化的架構設計,保障商家與運營的經營體驗和效率;參與面向億級消費者的萬級活動頁面快速生產背后的架構設計、交付手段和協作方式。


          主站蜘蛛池模板: 中文字幕在线观看一区二区三区| 一色一伦一区二区三区| 日本欧洲视频一区| 精品亚洲一区二区三区在线播放| 日韩在线一区视频| 国产乱码精品一区二区三区中| 国产日本亚洲一区二区三区| 中文字幕乱码亚洲精品一区| 国产一区二区在线观看| 性色av闺蜜一区二区三区| 丰满爆乳无码一区二区三区| 国产欧美色一区二区三区| 国产主播福利精品一区二区| 国产精品自在拍一区二区不卡| 中文字幕久久亚洲一区| 亚洲综合无码一区二区| 精品国产一区二区三区久久久狼| 亚洲AV无码一区二区大桥未久| 国产一区中文字幕| 91国在线啪精品一区| 熟女精品视频一区二区三区| 中文字幕AV一区二区三区人妻少妇| 免费一本色道久久一区| 影院无码人妻精品一区二区| 国产激情з∠视频一区二区| 无码人妻一区二区三区一| 日本一区二区三区在线看| 国产精品亚洲综合一区| 精品日韩亚洲AV无码一区二区三区| 亚洲一区欧洲一区| 波多野结衣中文字幕一区二区三区 | 中文字幕精品一区二区2021年 | 国产福利一区视频| 韩日午夜在线资源一区二区| 97精品一区二区视频在线观看 | 偷拍激情视频一区二区三区| 精品91一区二区三区| 日本高清不卡一区| 国产第一区二区三区在线观看| 日韩AV无码一区二区三区不卡毛片 | 亚洲成av人片一区二区三区|