SVG是向量圖形表示,它的一個強大之處在於path標籤可以表示任意的向量形狀,利用好這個path可以做出很多傳統html/css做不出來的效果。下面舉幾個例子。
1. 做路徑動畫
這個我在《SVG導航下劃線游標跟隨效果》文後補充介紹了這個實現,最後的效果是這樣的:
實現程式碼如下:
利用animateMotion結合path做的動畫,具體說明可見上文。
2. 實現不規則形狀的點選
如下圖所示,需要實現點到哪個洲就進入哪個洲的效果,例如點到非洲就進入非洲:
我們可以用div定一個框蓋在非洲的上面,但是用div的話只能是規則的四方形,沒辦法實現點到非洲大陸的時候才進入,但是大陸的輪廓又是不規則的,所以用傳統html是不能解決這個問題的。但是用SVG的path可以解決這個問題,方法1是監聽path的點選事件即可,如下圖所示:
因為這個輪廓可以跟UI要到,他們一般都是用AI/PS等向量軟體畫出來的,讓他們導一個SVG給你就好了。
方法2是可以調SVG的isPointInFill這個API判斷點選的點是否在Path的fill區域裡面,這個也可以實現,但是相對於方法1來說比較麻煩,因為還需要把滑鼠的位置轉換為svg檢視的位置。
3. 沿著路徑拖拽的互動
在第1點沿著路徑的動畫是自動的過程,有沒有辦法讓使用者自己拖拽過去呢,實現如下效果:
這種的場景有音量控制等需要有百分比控制的。可以先用一個SVG的線上工具畫出一個這樣的圖形:
就可以拿到SVG的程式碼:
<svg class="volumn-controller" width="580" height="400" xmlns="http://www.w3.org/2000/svg">
<path class="volumn-path" stroke="#000" d="m100,247c93,-128 284,-129 388,6" opacity="0.5" stroke-width="1" fill="#fff"/>
<circle class="drag-button" r="12" cy="247" cx="100" stroke-width="1" stroke="#000" fill="#fff"/>
</g>
</svg>複製程式碼
這裡比較關鍵的是path標籤裡的d屬性,d是data的縮寫,定義這個路徑的形狀,它裡面可以用很多屬性控制形狀的變化,如下圖所示:
為了實現這個互動,需要動態地改變circle的圓心位置(cx, cy)到路徑上相應的地方。SVG沒有直接提供相關的API,但是它提供了一個可以間接利用的API叫getPointAtLength,傳遞一個長度引數,如下程式碼所示:
let volumnPath = document.querySelector('.volumn-path');
// 輸出path在長度為100的位置的點座標
console.log(volumnPath.getPointAtLength(100));
// 輸出當前path的總長度
console.log(volumnPath.getTotalLength());複製程式碼
控制檯輸出:
把circle的cx/cy改成上面的x/y座標,圓圈就會跑到對應的位置去了:
這裡的問題在於這個API傳遞的length引數是相對於曲線長度的,但是滑鼠移動的位置是線性的,沒辦法直接知道當前滑鼠在曲線上距離起始位置多少。
所以需要算一下,在這個場景裡面我們可以取滑鼠的x座標在曲線上對應的位置就可以了,如下圖示意:
到這裡就有思路了,可以把這條路徑上每隔一個畫素長度就算一下它的座標在哪裡,然後存在一個陣列裡面。由於滑鼠移動的時候x座標是知道的,就可以查一下在這個陣列裡面相應x座標的y座標是多少,就能得到想要的圓心位置了。
所以先計算一下,儲存到一個陣列:
let $volumnController = document.querySelector('.volumn-controller'),
$volumnPath = $volumnController.querySelector('.volumn-path');
// 得到當前路徑的總長度
let pathTotalLength = $volumnPath.getTotalLength() >> 0;
let points = [];
// 起始位置為長度為0的位置
let startX = Math.round($volumnPath.getPointAtLength(0).x);
// 每隔一個畫素距離就儲存一下路徑上點的座標
for (let i = 0; i < pathTotalLength; i++) {
let p = $volumnPath.getPointAtLength(i);
// 儲存的座標用四捨五入,可以平衡誤差
points[Math.round(p.x) - startX] = Math.round(p.y);
}複製程式碼
這裡用一個p0ints陣列來儲存,它的索引index就為x座標,值為y座標。在這個例子裡面,總長度為451.5px,得到的points陣列長度為388. 你可以隔0.5px長度就儲存一個座標,不過在這個例子裡面1px就夠了。
然後監聽滑鼠事件,得到x座標,查詢y座標,動態地改變circle的圓心位置,如下程式碼所示:
let $dragButton = $volumnController.querySelector('.drag-button'),
// 得到起始位置相對當前視窗的位置,相當於jQuery.fn.offset
dragButtonPos = $dragButton.getBoundingClientRect();
function movePoint (event) {
// 當前滑鼠的位置減去圓心起始位置就得到移位偏差,12是半徑值,這裡先直接寫死
let diffX = event.clientX - Math.round(dragButtonPos.left + 12);
// 需要做個邊界判斷
diffX < 0 && (diffX = 0);
diffX >= points.length && (diffX = points.length - 1);
// startX是在上面的程式碼得到的長度為0的位置
$dragButton.setAttribute('cx', diffX + startX);
// 使用points陣列得到y座標
$dragButton.setAttribute('cy', points[diffX]);
}
$dragButton.addEventListener('mousedown', function (event) {
document.addEventListener('mousemove', movePoint);
});
document.addEventListener('mouseup', function () {
document.removeEventListener('mousemove', movePoint);
});複製程式碼
這個實現的程式碼也是比較簡單,需要注意的地方是起始位置的選取,這裡有兩個座標系,一個是相對頁面的視窗的,它的原點(0, 0)座標點是當前頁面可視區域(client)的左上角,第二個座標系是SVG的座標系,它的原點(0, 0)位置是SVG畫布的左上角,如下圖所示:
滑鼠的位置是相對於檢視client的,所以需要得到圓圈在client的位置,可以通過原生的getBoundingClient獲取,然後用滑鼠的clientX減掉圓圈的clientX就得到正確的位移偏差diff了,這個diff值加上圓圏的在svg座標的起始位置就能得到svg裡的x座標了,然後去查一下points陣列就能得到y座標,然後去設定circle的cx/cy值。
這個的實現已經算是十分簡單的,大概30行程式碼。需要注意的是如果svg縮放了,那麼座標也要相應比例地改一下。所以最好是不要縮放,1:1顯示就簡單多了。
如果要顯示具體的音量值呢?這個也好辦,只需要在第一步儲存點座標的時候把在路徑上的長度也儲存下來就好了,最後效果如下:
一個完整的demo:svg-path-drag.html
如果路徑比較複雜怎麼辦呢,一個x座標可能會對應兩個點,如下圖所示:
這個也是有辦法的,計算的方法類似,也是需要把路徑上所有每隔1px的點座標都取出來,然後計算一下滑鼠的位置距離哪個點的座標最接近,然後就取那個點就好了。當然在判斷哪個點最優時,演算法需要優化,不能直接一個for迴圈,具體可見這個codepen。
4. 路徑的變形動畫
路徑結合關鍵幀可以做出一些有趣的效果,如這個codepen的示例:
它的實現是hover的時候改變path的d值,然後做d的transition動畫,如下程式碼:
<svg viewBox="0 0 10 10" class="svg-1">
<path d="M2,2 L8,8" />
</svg>
<style>
.svg-1:hover path {
d: path("M8,2 L2,8");
}
path {
transition: d 0.5s linear;
}
</style>複製程式碼
這種變形過渡動畫是有條件的,就是它的路徑資料格式是要一致的,有多少個M/L/C屬性都要保持一致,否則無法做變形動畫。
5. 結合clip-path做遮罩效果
使用CSS通常只能用border-radius做一些圓角的遮罩,即用border-radius結合overflow: hidden實現,但是使用clip-path + svg的路徑能夠做出任意形狀遮罩,如下做一個心形的:
如下程式碼所示:
<div style="width:200px;height:200px">
<img src="photo.png" alt style="width:100%">
</div>
<style>
img {
clip-path: url("#heart");
}
</style>複製程式碼
style裡面的id: #heart是指向了一個SVG的的clipPath,如下圖所示:
<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0">
<clipPath id="heart" clipPathUnits="objectBoundingBox">
<path transform="scale(0.0081967, 0.0101010)" d="m61.18795,24.08746c24.91828,-57.29309 122.5489,0 0,73.66254c-122.5489,-73.66254 -24.91828,-130.95562 0,-73.66254z"/>
</clipPath>
</svg>複製程式碼
為了讓這個path剛好能撐起div容器寬度的100%,需要設定:
clipPathUnits="objectBoundingBox"複製程式碼
這樣會導致d屬性裡面的單位變成比例的0到1,所以需要把它縮小一下,原本的width是122,height是99,需要需要scale的值為(1 / 122, 1 / 99)。這樣就達到100%佔滿的目的,如果一開始d屬性座標比例就是0到1,就不用這麼搞了。
另外clip-path使用svg的path不支援變形動畫。
本篇介紹了使用svg路徑path做的幾種效果:做一個路徑動畫、不規則形狀的點選、沿著路徑拖拽、路徑的變形動畫以及和clip-path做一些遮罩效果。可以說svg的path效果還是很強大的,當你有些效果用html/css無法實現的時候,不妨往svg的方向思考。
【號外】《高效前端》已經第二次印刷了,聽說你還沒買