SVG導航下劃線游標跟隨效果

人人網FED發表於2018-03-31

之前看到一篇博文,介紹導航下劃線游標跟隨的效果,是用的CSS的hover結合CSS3的選擇器做的,總感覺效果不太自然,所以我就在想能不能用SVG來做這個效果,試了一下,還是可以的,不過要藉助一點JS。先來看下一下按正常思路用JS應當怎麼實現。

1. 正常思路的實現

我的思路是這樣的,在導航的下樣畫一條線,然後當滑鼠hover的時候看它是在哪個item上面,然後改變下劃線的位置,並給這個位置加一個transition的動畫。html結構:

<nav>
    <ul>
        <li id="item1">首頁</li>
        <li id="item-2">產品</li>
        <li id="item-3">關於我們</li>
        <li id="item-4">幫助</li>
    </ul>
    <div class="underline"></div>
</nav>複製程式碼

動畫的CSS:

.underline {
    transition: transform 0.2s linear,
                width 0.2s linear;
}複製程式碼

因為不同導航寬度不一樣,所以除了transform之外再加個width的動畫。

然後監聽導航的mouseover事件,在裡面改變下劃線的transform和width屬性,激發transition動畫:

let $line = document.querySelector(".underline");
document.querySelector("nav").onmouseover = function(event) {
    let node = event.target;
    // 使用mouseover注意事件冒泡目標元素判斷
    if (node.nodeName === "LI") {
        // 計算postion,參考jQuery的postion函式的實現
        // 它的實現比這個複雜很多,考慮的情況要多一些
        let left = node.getBoundingClientRect().left;
        if (node.offsetParent) {
            // BFC
            left -= node.offsetParent.getBoundingClientRect().left;
        }
        $line.style.transform = `translateX(${left}px)`;
        $line.style.width = node.clientWidth + "px";
    }
};複製程式碼

效果如下圖所示(錄屏的效果稍微差了一點,實際上是挺好的):

一個demo:underline-move-js.html.(手機可通過點選觸發mouseover事件)

感覺這個的效果已經很好了,而且程式碼也不復雜,主要是postition計算那裡稍微複雜一點,如果沒有用jq的話。

用SVG的動畫可以怎麼實現呢?

2. SVG動畫效果

SVG有一個animate標籤,可以指定需要做動畫的屬性,還可以通過begin屬性指定動畫開始的時機,例如某個元素hover的時候才開始動畫。如果用CSS的hover配合選擇器話,它有一個缺點就是隻能選相鄰或者子元素,這個限制就導致我們只能把下劃線放在導航欄元素裡面,如做為它的border-bottom,或者緊跟在所有元素的後面。而svg沒有這個限制,svg動畫的觸發可以是頁面任意svg元素的任意事件

所以我們藉助這個特性來做動畫,先用svg的rect畫一條線,這條線具有寬度和位置的屬性,導航欄元素li觸發mouseenter的時候就相應地改變這個rect的x座標(注意mouseenter和mouseover的區別,mouseenter是不會冒泡的,這裡要用mouseenter,避免li的子元素冒泡上去)。由於觸發只能是svg的元素才有效,所以需要在每個li裡面寫一個svg給蓋住:

<li>首頁
    <svg>
        <rect id="svgitem1" x="0" y="0" width="100%" height="100%" fill="transparent"></rect>
    </svg>
</li>
<li>產品
    <svg>
        <rect id="svgitem2" x="0" y="0" width="100%" height="100%" fill="transparent"></rect>
    </svg>
</li>複製程式碼

這裡面每個svg元素都是用的一個透明的矩形rect鋪滿,每個rect都有一個id:svgitem1、svgitem2等。

然後再用一個獨立的svg的rect畫下劃線:

<nav>
    <ul><li>首頁...</li></ul>
    <svg id="svg-underline" width="100%" height="1">
        <rect id="nav-underline" x="0" y="0" width="80" height="2" stroke="black" stroke-width="2"/>
    </svg>
</nav>複製程式碼

把這個svg絕對定位到導航欄nav第一個元素的下面:

#svg-underline {
    position: absolute;
    left: 0;
    bottom: 0;
}複製程式碼

然後給這個svg新增動畫元素:

<svg id="svg-underline" width="100%" height="1">
    <rect id="nav-underline" x="0" y="0" width="80" height="2" stroke="black" stroke-width="2"/>
    <animate xlink:href="#nav-underline" attributeName="x" to="0" dur=".2s" begin="svgitem1.mouseenter" fill="freeze"></animate>
    <animate xlink:href="#nav-underline" attributeName="x" to="80" dur=".2s" begin="svgitem2.mouseenter" fill="freeze"></animate>
    <animate xlink:href="#nav-underline" attributeName="x" to="160" dur=".2s" begin="svgitem3.mouseenter" fill="freeze"></animate>
    <animate xlink:href="#nav-underline" attributeName="x" to="240" dur=".2s" begin="svgitem4.mouseenter" fill="freeze"></animate>
</svg>複製程式碼

其中attributeName指定要做動畫的屬性,這裡為x座標,還有from/to屬性,表示從哪個值變到哪個值,這裡沒有from,就會使用當前的值,這裡先指定固定的to,我們先假定導航是等寬的,每個為80px,所以to的值遞增。dur表示動畫的時間,這裡為0.2s。fill="freeze"表示動畫結束後停留在最後一幀。begin為動畫的觸發時機,每個animate元素的begin對應每個li裡面svg元素,當li的svg觸發mouseenter的時候就會開始相應的animate動畫。而動畫的效果是改變to屬性指定的值,這樣就實現了x座標隨著游標移動的動畫。

缺點是需要寫很多animate元素,但是一般我們用模板渲染,所以這個情況應該會好些。效果如下圖所示:

一個完整的demo:underline-move-svg.html.

上面我們假定了導航是等寬的,但實際上導航往往是不等寬的,那怎麼辦呢?不等寬的情況基本只能藉助JS計算元素寬度,然後動記地改變animate元素裡面to的值,同時新增一個下劃線寬度width的動畫,如下示例:

<animate xlink:href="#nav-underline" attributeName="x" to="0" dur=".2s" begin="svgitem1.mouseenter" fill="freeze"></animate>
<animate xlink:href="#nav-underline" attributeName="width" to="0" dur=".2s" begin="svgitem1.mouseenter" fill="freeze"></animate>複製程式碼

然後寫一點JS改變一個這兩個to的值:

let $lis = document.querySelectorAll("nav li"),
    $animates = document.querySelectorAll("#svg-underline animate");
let widthSum = 0;
for (let i = 0; i < $lis.length; i++) {
    let width = $lis[i].clientWidth;
    $animates[i * 2].setAttribute("to", widthSum);
    $animates[i * 2 + 1].setAttribute("to", width);
    widthSum += width;
}複製程式碼

如果使用Vue/React等框架,可以在mounted的時候,動態地改變to繫結的值,這個程式碼也是挺簡單的。

效果如下:

一個完整的Demo:underline-move-svg-2.html.

這個效果也很好,但是相對來說還是第1點正常思路的實現比較簡單一點。有個問題就是重複進入同一個元素會重複動畫,它的from還是上一次的值。

但是不管怎麼樣,當你發現用html/css不太好做動畫時,可以往SVG動畫的方向思考,SVG做動畫還是很靈活的。上面那個例子其實不是很典型,再介紹另一個使用SVG做動畫的例子。

3. SVG路徑動畫

SVG還可以做路徑的動畫,可以用鋼筆工具勾勒一條路徑,然後讓目標元素沿著這個路徑運動。如下效果示例:

程式碼如下所示:

<svg viewBox="0 0 2706 2048" width="120" height="66" class="svg-hand">
    <g id="hand">
        <!--手上的圓圏-->
        <circle cx="370" cy="90" r="210" fill="yellow"></circle>
        <!--手的形狀-->
        <path d="..."></path>
    </g>
    <!--動畫路徑-->
    <path id="animation-hand-arc" d="M-514,665c0,0,1378.463-1138.762,2891,0" stroke="transparent" fill="transparent"/>
    <!--動畫設定-->
    <animateMotion id="arcmove" xlink:href="#hand" dur="1s" begin="0" fill="freeze" repeatCount="1">
        <mpath xlink:href="#animation-hand-arc" />
    </animateMotion>
</svg>複製程式碼

這裡主要使用了animateMotion標籤,通過它的mpath指定一個動畫的路徑,就可以了。

這個路徑可以用PS的鋼筆工具畫一個形狀,然後匯出成SVG(PS CC版本支援)或者是用線上的一些svg編輯工具也是可以。而運動的目標元素(如上面的手)可以使用圖示字型裡的SVG,這裡比較麻煩的地方是自己勾勒的路徑需要根據圖示字型SVG的viewbox做調整,比例才對得上。

例如如果圖示字型的viewbox是1024 * 1024的,那麼你的PS畫布大小也得是1024 * 1024的,如下筆者畫的孤形路徑:

(據說現在的前端很多不會PS,因為公司切圖基本用zeplin).

匯出來的SVG檔案是這樣的:

<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
     width="1024px" height="1024px" viewBox="0 0 1024 1024" style="enable-background:new 0 0 1024 1024;" xml:space="preserve">
<path d="M-514,665c0,0,1378.463-1138.762,2891,0"/>
</svg>複製程式碼

複製裡面的path路徑:

M-514,665c0,0,1378.463-1138.762,2891,0

作為上面動畫的路徑。


我們可以進以進一步完善這個動畫,如果現在要求動畫不是立馬開始,如重新整理頁面後隔個0.5s再開始,並且要求那隻手一開始的時候沒有出現,動畫開始才出現,主要是為了給個時間先處理頁面其它元素。

可以把動畫的begin改成0.5s,hand元素一開始設定成不可見:

<svg viewBox="0 0 2706 2048" width="120" height="66" class="svg-hand">
    <!--一開始是不可見的-->
    <g id="hand" visibility="hidden">
        <!--手上的圓圏-->
        <circle cx="370" cy="90" r="210" fill="yellow"></circle>
        <!--手的形狀-->
        <path d="..."></path>
        <!--動畫開始後設定成可見-->
        <set attributeName="visibility" from="hidden" to="visible" begin="arcmove.begin"/>
    </g>
    <!--動畫路徑-->
    <path id="animation-hand-arc" d="M-514,665c0,0,1378.463-1138.762,2891,0" stroke="transparent" fill="transparent"/>
    <!--動畫設定-->
    <animateMotion id="arcmove" xlink:href="#hand" dur="1s" begin="0.5s" fill="freeze" repeatCount="1">
        <mpath xlink:href="#animation-hand-arc" />
    </animateMotion>
</svg>複製程式碼

上面程式碼主要通過在g元素裡面新增一個set標籤:

<set attributeName="visibility" from="hidden" to="visible" begin="arcmove.begin"/>複製程式碼

也就是說動畫的begin可以是另外一個動畫的begin或者end。如果要在某個動畫結束後開始,則可以把begin改成arcmove.end即可。


相關閱讀:

  1. 怎樣做一個圓環放大的動畫

【號外】《高效前端》已經上市 ,京東和亞馬遜、早讀課等均有售


相關文章