藉助 :has 實現3d輪播圖

XboxYan發表於2022-11-24
歡迎關注我的公眾號:前端偵探

前不久在這篇文章:CSS 有了:has偽類可以做些什麼 中介紹了:has偽類的一些使用場景,可以說大大顛覆了 CSS 選擇器的認知,讓很多繁瑣的 js邏輯透過靈活的CSS輕易實現了。這次帶來一個比較常見的案例,3d 輪播圖,就像這樣的

Kapture 2022-09-12 at 14.46.55.gif

這個輪播圖有幾個需要實現的點:

  1. 3d 視覺,也就是中間大,兩邊小
  2. 自動輪播,滑鼠放上自動暫停
  3. 點選任意卡片會立即跳轉到該卡片

這次藉助:has來實現這樣的功能,相信可以帶來不一樣的思路,一起看看吧

⚠️溫馨提醒:相容性要求需要 Chrome 101+,並且開始實驗特性(105+正式支援),Safari 15.4+,Firefox 官方說開啟實驗特性可以支援,但是實測並不支援

一、3d 視覺樣式

這一部分才是重點。

首先我們來簡單佈局一下,假設HTML是這樣的:

<div class="view" id="view">
  <div class="item">1</div>
  <div class="item">2</div>
  <div class="item">3</div>
  <div class="item">4</div>
  <div class="item current">5</div>
  <div class="item">6</div>
  <div class="item">7</div>
  <div class="item">8</div>
  <div class="item">9</div>
  <div class="item">10</div>
</div>

為了儘可能減少複雜度,我們可以將所有的變化都集中在一個類名上。

比如.current表示當前選中項,也就是中間的那項。這裡採用絕對定位的方式,將所有卡片項都堆疊在一起,然後設定3d檢視,將卡片的Z軸往後移動一段距離,讓.current在所有卡片的最前面,實現近大遠小的效果,具體實現如下

.view {
  position: relative;
  width: 400px;
  height: 250px;
  transform-style: preserve-3d;
    perspective: 500px;
}
.item {
  position: absolute;
  width: 100%;
  height: 100%;
  border-radius: 8px;
  display: grid;
  place-content: center;
  box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);
  transform: translate3d(0, 0, -100px);
}
.item.current {
  transform: translate3d(0, 0, 0);
}

效果如下

image-20220912151224848

其實層級是這樣的,可以在 Chrome 圖層中看到

image-20220912151329501

現在,我們需要讓相鄰左右兩邊的都漏出來,右邊的比較容易,用相鄰兄弟選擇器+就可以了

.item.current + .item{
  transform: translate3d(30%, 0, -100px);
}

那相鄰左邊的呢?以前是無解的,只能透過 JS 分別設定不同的類名,非常麻煩,但現在有了:has偽類,也可以輕鬆實現了,如下

.item:has(+.item.current){
  transform: translate3d(-30%, 0, -100px);
}

效果如下

image-20220912151819533

不過還有一些臨界情況,比如在第一個卡片時,由於前面沒有兄弟節點了,所以就成了這樣

image-20220912152013476

所以需要再在第一個元素時,把最後一個元素放在它的左邊,第一個元素是:first-child,最後一個元素是:last-child,所以實現是這樣的(最後一個元素處理同理)

.item.current:first-child ~ .item:last-child {
  transform: translate3d(-30%, 0, -100px);
  opacity: 1;
}
.item:first-child:has(~ .item.current:last-child) {
  transform: translate3d(30%, 0, -100px);
  opacity: 1;
}

這樣就處理好了邊界情況

image-20220912152554132

進一步,還可以向兩側露出兩個卡片,實現也是類似的,完整實現如下

/*當前項*/
.item.current {
  transform: translate3d(0, 0, 0);
}
/*當前項右1*/
.item.current + .item,
.item:first-child:has(~ .item.current:last-child) {
  transform: translate3d(30%, 0, -100px);
}
/*當前項左1*/
.item:has(+ .item.current),
.item.current:first-child ~ .item:last-child {
  transform: translate3d(-30%, 0, -100px);
}
/*當前項右2*/
.item.current + .item + .item,
.item:first-child:has(~ .item.current:nth-last-child(2)), 
.item:nth-child(2):has(~ .item.current:last-child) {
  transform: translate3d(50%, 0, -150px);
}
/*當前項左2*/
.item:has(+.item + .item.current),
.item.current:first-child ~ .item:nth-last-child(2),
.item.current:nth-child(2) ~ .item:last-child{
  transform: translate3d(-50%, 0, -150px);
}

這樣就實現了關於.current的全部樣式處理了,只用一個變數就控制了所有變化

image-20220912152858792

二、自動輪播和暫停

有了上面的處理,接下來的邏輯就非常簡單了,只需要透過js動態控制.current的變化就行了。

一般情況下,我們可能會想到用定時器setInterval,但這裡,我們也可以不使用定時器,藉助 CSS 動畫的力量,可以更輕鬆地完整這樣的互動。

有興趣的可以參考之前這篇文章:還在用定時器嗎?藉助 CSS 來監聽事件,相信可以給你帶來一些靈感

要做的事情很簡單,給容器新增一個無關緊要的 CSS 動畫

.view {
  /**/
  animation: scroll 3s infinite;
}
@keyframes scroll {
  to {
    transform: translateZ(.1px); /*無關緊要的動畫樣式*/
  }
}

這樣就得到了一個時長為3s,無限迴圈的動畫了。

然後,監聽animationiteration事件,這個事件在動畫每次執行一次就會回撥一次,在這裡也就是每3s執行一次,就像setInterval的功能一樣

GlobalEventHandlers.onanimationiteration - Web API 介面參考 | MDN (mozilla.org)

animationiteration回撥中處理.current邏輯,很簡單,移除當前的.current,給下一個新增.current,注意一下邊界就行了,具體實現如下

view.addEventListener("animationiteration", () => {
  const current = view.querySelector(".current") || view.firstElementChild;
  current.classList.remove("current");
  if (current.nextElementSibling) {
    current.nextElementSibling.classList.add("current");
  } else {
    view.firstElementChild.classList.add("current");
  }
});

使用animationiteration的最大好處是,可以直接透過 CSS 進行動畫的控制,再也無需監聽滑鼠移入移出事件了

.view:hover{
  animation-play-state: paused;
}

效果如下(方便演示,速度調快了)

Kapture 2022-09-12 at 15.43.28

三、點選快速切換

點選切換,我其實最先想到的是透過:checked,也就是類似單選,比如

<div class="view" id="view">
  <label class="item"><input type="radio" checked name="item"></label>
  <label class="item"><input type="radio" name="item"></label>
  <label class="item"><input type="radio" name="item"></label>
  <label class="item"><input type="radio" name="item"></label>
  <label class="item"><input type="radio" name="item"></label>
  <label class="item"><input type="radio" name="item"></label>
</div>

但是目前來看:has偽類貌似不支援多層巢狀,比如下面這條語句

.item:has(+.item:has(:checked)){
  /*不生效*/
}

.item:has(:checked)選中的是子元素被選中的父級,然後.item:has(+.item:has(:checked))表示選中它的前面一個兄弟節點,這樣就能實現選中功能了,可惜現在並不支援?(以後可能支援)

沒辦法,只能透過傳統方式來實現,直接繫結監聽click事件

view.addEventListener("click", (ev) => {
  const current = view.querySelector(".current") || view.firstElementChild;
  current.classList.remove("current");
  ev.target.closest('.item').classList.add("current");
});

效果如下

Kapture 2022-09-12 at 16.01.23.gif

完整程式碼可以檢視線上 demo:CSS 3dscroll(runjs.work)

四、總結一下

以上就是藉助:has偽類來實現一個3d輪播圖的全部細節了,所有的視覺變化全部在 CSS 中完成,JS 只需要處理切換邏輯就行了,相比以前而言,實現上更加簡潔和優雅,下面總結一下

  1. 3d 視覺樣式可以透過transform-style: preserve-3d;實現近大遠小的效果
  2. 透過.item:has(+.item.current)可以設定當前項前面的兄弟節點
  3. 還需要考慮第一個和最後一個這兩種臨界情況
  4. 輪播圖自動輪播和暫停可以藉助animationiteration回撥,這種方式的優勢是可以透過:hover控制
  5. :has偽類貌似不支援多層巢狀,希望以後可以支援吧~

:has非常強大,目前唯一的缺陷在於相容性。好在瀏覽器對於這一新特性跟進的都比較積極,明年這個時候差不多可以在內部專案用起來了。最後,如果覺得還不錯,對你有幫助的話,歡迎點贊、收藏、轉發❤❤❤

歡迎關注我的公眾號:前端偵探

相關文章