還原一個 Windows 10 Metro 佈局
Win10 Metro 相較於前一代完全扁平化風格的 Win8 Metro 在動畫效果與互動體驗上有了比較大的差異,那麼想要實現一個較為逼真的Win10 Metro需要哪些動畫效果呢?
真的是Windows 10 Metro嗎?
先來看一下這個Demo,看似複雜的互動實現起來其實並不難,下面我們就來拆解一下其中的動畫效果以及實現原理吧。
Metro動畫效果與實現
1. 3D旋轉
Win10 Metro的一個比較顯著的特點就是磁貼的3D旋轉。
原版

實現
我們將每一個磁貼展示為橫截面為正方形的長方體,然後通過css旋轉動畫來實現3D旋轉。
html
<div class="scene"> <div class="box-container"> <div class="front"> </div> <div class="back"> </div> <div class="top"> </div> <div class="bottom"> </div> <div class="right"> </div> <div class="left"> </div> </div> </div> 複製程式碼
在html結構中, scene
節點的主要作用是作為一個場景容器,有了 scene
我們便可以對場景中的屬性作出一些調整,例如通過調節 perspective
,即可增強或者減弱長方體在旋轉時的立體感。 box-container
是整個長方體的容器節點,其中包含了長方體的6個面節點 front
, back
, top
, bottom
, right
, left
。
css
.scene { position: relative; width: 300px; height: 150px; perspective: 700px; } .box-container { position: relative; width: 100%; height: 100%; transform-style: preserve-3d; transition: transform 0.5s; transform-origin: 50% 50% -75px; } .front { background-color: rgba(3,122,241,0.5); position: absolute; width: 300px; height: 150px; } .back { background-color: rgba(241,3,3,0.5); position: absolute; width: 300px; height: 150px; transform: translateZ(-150px) rotateZ(180deg) rotateY(180deg); } .top { background-color: rgba(3,241,122,0.5); position: absolute; width: 300px; height: 150px; transform: translate3d(0,-75px,-75px) rotateX(90deg); } .bottom { background-color: rgba(241,241,3,0.5); position: absolute; width: 300px; height: 150px; transform: translate3d(0,75px,-75px) rotateX(-90deg); } .left { background-color: rgba(270,97,48,0.5); position: absolute; width: 150px; height: 150px; transform: translate3d(-75px,0,-75px) rotateY(-90deg); } .right { background-color: rgba(30,97,48,0.5); position: absolute; width: 150px; height: 150px; transform: translate3d(225px,0,-75px) rotateY(90deg); } 複製程式碼
在css中,有以下幾點值得注意的地方
-
.box-container
中使用了transform-style: preserve-3d
來保留子節點在3D變換時的3D座標。並用了transform-origin: 50% 50% -75px
將.box-container
的旋轉原點放在了長方體的中心。 - 為了使長方體背面的內容在旋轉後不顛倒,在
.back
的定義中我們需要在transform
中額外新增rotateZ(180deg)
預先顛倒背面的內容。 - 為了保持每一面中內容不因為
Z軸
的座標而放大或者縮小,我們始終保持了當前正對觀察者面的Z座標
為0
。
Demo
2. 傾斜
在點選並按住磁貼的時,磁貼會出現一個向點選位置傾斜的動畫。
原版

實現
如果仔細觀察原版會發現,面的傾斜角會隨著按壓位置的不同而產生不同的變化。當按壓位置靠近磁貼邊緣時,傾斜角會變大一些;而當按壓位置靠近磁貼中心時,傾斜角會隨之減小。總結以上的規律我們可以得出:
以磁貼的中心作為座標系原點(0, 0),當點選位置為(x, y)時,X軸上的傾斜角Ɵx ∝ |y|,而Y軸上的傾斜角 Ɵy ∝ |x|。
html
<div class="container"> <div class="tile"> <span>Hello World</span> </div> </div> 複製程式碼
css
.container { width: 200px; height: 200px; perspective: 700px; } .tile { background-color: #2d89ef; width: 100%; height: 100%; transition: transform 0.5s; text-align: center; color: white; } 複製程式碼
js
const maxTiltAngle = 30; // 設定最大傾斜角度 const container = document.getElementsByClassName('container')[0]; const tile = document.getElementsByClassName('tile')[0]; const boundingRect = container.getBoundingClientRect(); const tilt = event => { // 計算滑鼠相對於容器的位置 const relativeX = event.pageX - (boundingRect.left + window.scrollX); const relativeY = event.pageY - (boundingRect.top + window.scrollY); // 將原點從容器左上角移至容器中心 const normalizedX = relativeX - boundingRect.width / 2; const normalizedY = -(relativeY - boundingRect.height / 2); // 計算傾斜角 const tiltX = normalizedY / (boundingRect.height / 2) * maxTiltAngle; const tiltY = normalizedX / (boundingRect.width / 2) * maxTiltAngle; // 傾斜 tile.style.transform = `rotateX(${tiltX}deg) rotateY(${tiltY}deg)`; } const recover = () => { // 恢復傾斜 tile.style.transform = ''; } container.addEventListener('mousedown', tilt); container.addEventListener('mouseup', recover); container.addEventListener('mouseleave', recover); 複製程式碼
在傾斜的實現上,有以下幾點需要注意的地方
- 以長為L寬為W的磁貼中心作為座標系原點(0, 0),當點選位置為(x, y)時,實現中使用的公式為:X軸上的傾斜角Ɵx = y / (W / 2) * Ɵmax,而Y軸上的傾斜角 Ɵy = x / (L / 2) * Ɵmax。
- 僅在mouseup事件中恢復傾斜是不夠的,在mouseleave的時候也需要恢復傾斜。
Demo
3. 懸停光暈
當滑鼠懸停在磁鐵上時,磁貼上會有一個跟隨滑鼠移動的光圈。
原版

實現
光暈從中心至外圍顏色漸漸淡化,光暈中心的位置會隨著滑鼠的移動而移動。
html
<div class="container"> <div class="hoverLayer"> </div> <div class="hoverGlare"> </div> </div> 複製程式碼
css
.container { position: relative; background-color: #000; width: 200px; height: 200px; overflow: hidden; } .hoverLayer { position: absolute; z-index: 1; width: 100%; height: 100%; } .hoverGlare { position: absolute; background-image: radial-gradient(circle at center, rgba(255,255,255, 0.7) 0%, rgba(255,255,255,0.1) 100%); transform: translate(-100px, -100px); width: 400px; height: 400px; opacity: 0.4; } 複製程式碼
js
const boundingRect = document.getElementsByClassName('container')[0].getBoundingClientRect(); const hoverGlare = document.getElementsByClassName('hoverGlare')[0]; const glare = event => { // 計算滑鼠相對於容器的位置 const relativeX = event.pageX - (boundingRect.left + window.scrollX); const relativeY = event.pageY - (boundingRect.top + window.scrollY); // 將原點從容器左上角移至容器中心 const normalizedX = relativeX - boundingRect.width / 2; const normalizedY = relativeY - boundingRect.height / 2; // 調整光暈透明度及位置 hoverGlare.style.opacity = 0.4; hoverGlare.style.transform = `translate(${normalizedX}px, ${normalizedY}px) translate(-${boundingRect.width / 2}px, -${boundingRect.height / 2}px)`; } const resetGlare = () => { // 隱藏光暈 hoverGlare.style.opacity = 0; } const hoverLayer = document.getElementsByClassName('hoverLayer')[0]; hoverLayer.addEventListener('mousemove', glare); hoverLayer.addEventListener('mouseleave', resetGlare); 複製程式碼
在光暈的實現上,有以下幾點需要注意的地方
- 我們使用了
z-index
為1
的.hoverLayer
來當作滑鼠事件節點,避免因為子節點覆蓋父節點而產生滑鼠定位不準確的問題。 - 我們建立了一個2倍於容器寬高的光暈懸浮層,並通過
translate
移動這個懸浮層來實現高效率的光暈位置變換。
Demo
4. 點選波紋
當滑鼠點選磁貼時,在點選位置會形成圓形向外擴散的波紋動畫
原版

實現
我們可以看到圓形的波紋由點選位置開始向外漸漸擴散直至消散。
html
<div class="tile"> <div class="clickGlare"> </div> </div> 複製程式碼
css
.tile { position: relative; width: 200px; height: 200px; background-color: #000; overflow: hidden; } .clickGlare { position: absolute; width: 90px; height: 90px; border-radius: 50%; opacity: 0; filter: blur(5px); background-image: radial-gradient(rgba(255, 255, 255, 0.7) 0%, rgba(255, 255, 255, 0) 100%); } .ripple { animation-name: ripple; animation-duration: 1.3s; animation-timing-function: ease-in; } @keyframes ripple { 0% { opacity: 0.5; } 100% { transform: scale(5); opacity: 0; } } 複製程式碼
js
const tile = document.getElementsByClassName('tile')[0]; const boundingRect = tile.getBoundingClientRect(); const clickGlare = document.getElementsByClassName('clickGlare')[0]; const ripple = event => { // 僅當節點的class中不含ripple時執行 if (clickGlare.classList.contains('ripple')) return; // 計算滑鼠相對於容器的位置 const relativeX = event.pageX - (boundingRect.left + window.scrollX); const relativeY = event.pageY - (boundingRect.top + window.scrollY); // 根據滑鼠位置調整波紋的中心位置 clickGlare.style.top = `${relativeY - 45}px`; clickGlare.style.left =`${relativeX - 45}px`; // 新增波紋動畫 clickGlare.classList.add('ripple'); } const resetRipple = () => { // 移除波紋動畫 clickGlare.classList.remove('ripple'); } tile.addEventListener('mousedown', ripple); clickGlare.addEventListener('animationend', resetRipple); 複製程式碼
在點選波紋的實現上,有以下幾點需要注意的地方
- 為了使得波紋動畫更加近似於原版,我們使用了
filter: blur
這一條css,這條css可能會引起老版本瀏覽器或者IE/Edge中的相容性問題。
Demo
小結
如果將以上的動畫結合起來,我們就可以實現一個比較逼真的Windows 10 Metro 佈局了。看上去複雜的Windows 10 Metro,是不是其實挺簡單的呢?
Windows 10磁貼中包含的動畫拆解開來都是一些比較常見的、能提升使用者體驗的動畫。大家也可以在平時的專案或者工作中嘗試去模仿一下這些簡易的動畫,來使得互動與設計更加得友好。
最後,我也使用Vue做了一個 ofollow,noindex">小元件 ,方便大家在Vue中實現Win 10 Metro佈局,歡迎各位的交流與討論~