翻譯:瘋狂的技術宅
原文: https://www.smashingmagazine....
本文首發微信公衆號:jingchengyideng
歡迎關注,天天都給你推送新鮮的前端技術文章javascript
摘要
在這篇文章中你將瞭解Awwwards網是怎樣實現動畫的。 本文介紹了HTML5 SVG中的circle 元素,它的stroke屬性,以及如何使用CSS變量以及用 Vanilla JavaScript 爲它們設置動畫。
SVG是一種基於XML的,用於定義縮放矢量圖形的標記語言。 它容許你經過在2D平面中肯定的一組點來繪製路徑、曲線和形狀。 此外你還能夠經過在這些路徑上添加動態屬性(例如筆觸,顏色,粗細,填充等)來生成動畫。css
從2017年4月起,CSS Level 3 填充和描邊模塊開始支持從外部樣式表設置SVG顏色和填充圖案,而不是在每一個元素上設置屬性。 在本教程中,咱們將會使用簡單的純十六進制顏色,不過填充和描邊屬性也支持圖案,漸變和圖像做爲值。html
注意:訪問 Awwwards網站時,你須要把瀏覽器寬度設置爲1024px或更高的才能更好的查看動畫顯示。
讓咱們從在終端中建立文件開始:前端
🌹 mkdir note-display 🌹 cd note-display 🌹 touch index.html styles.css scripts.js
HTML
這是鏈接css
和js
文件的初始模板:html5
<html lang="en"> <head> <meta charset="UTF-8"> <title>Note Display</title> <link rel="stylesheet" href="./styles.css"> </head> <body> <script src="./scripts.js"></script> </body> </html>
每一個note元素都包含一個列表項:li
用於保存circle
,note
值及其label
。java
圖:列出項元素及其直接子元素:.circle
, .percent
和 .label
css3
.circle_svg
是一個SVG元素,它包含兩個 <circle>元素。 第一個是要填充的路徑,第二個用來爲動畫做準備。git
圖:SVG元素:SVG包裝器和圓形標籤github
註釋分爲整數和小數,因此能夠把它們設定爲不一樣大小的字體。 label
是一個簡單的<span>
。 把全部得這些元素放在一塊兒看起來像這樣:編程
<li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Transparent</span> </li>
cx
和cy
屬性定義圓的x軸和y軸中心點。 r
屬性定義其半徑。
你可能已經注意到類名中的下劃線/破折號模式。 這是BEM(block element modifier),分別表明 block
, element
和 modifier
。 它是使元素命名更加結構化、有條理和語義化的一種方法。
推薦閱讀:什麼是BEM以及爲何須要它
爲了完成模板結構,讓咱們將四個列表項包裝在無序列表元素中:
圖:無序列表包裝器擁有四個li
子元素
<ul class="display-container"> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Transparent</span> </li> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Reasonable</span> </li> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Usable</span> </li> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Exemplary</span> </li> </ul>
你必須先問一下本身 Transparent
、 Reasonable
、 Usable
和 Exemplary
標籤都表明什麼意思。 隨着你對編程的不斷熟悉,就會發現寫代碼不只僅是爲了可以使程序正常運行,還須要要確保它可以被長期維護和擴展。 這些只有在你的代碼容易被修改時纔可以實現。
「縮略詞
TRUE
應該可以幫助你肯定本身編寫的代碼是否可以適應將來的變化。」
那麼,下次問問你本身:
透明:代碼更改後果是否明確?
合理:成本效益值得嗎?
可用:我是否可以在乎外狀況下重複使用它?
示例:它是否以高質量做爲將來代碼的示例?
Transparent(透明)
:代碼在修改後果是否明確?Reasonable(合理)
:成本效益值得嗎?Usable(可用)
:我是否可以在不一樣的場景下重複使用它?Exemplary(示例)
:將來它是否能夠做爲高質量做爲代碼範本?注:Sandi Metz在《面向對象設計實踐指南:Ruby語言描述》一書解釋了TRUE
和其餘原則,以及如何經過設計模式實現它們。 若是你尚未開始研究設計模式,請考慮將此書放到本身的案頭。
讓咱們導入字體並使其對全部內容生效:
@import url('https://fonts.googleapis.com/css?family=Nixie+One|Raleway:200'); * { padding: 0; margin: 0; box-sizing: border-box; }
box-sizing: border-box
屬性中包括填充與邊框值到元素的總寬度和高度,因此更容易計算圖形的範圍。
注意:有關
*box-sizing
*的說明,請閱讀
「使用CSS Box讓你更輕鬆」_。
body { height: 100vh; color: #fff; display: flex; background: #3E423A; font-family: 'Nixie One', cursive; } .display-container { margin: auto; display: flex; }
經過組合規則顯示:body
中的 flex
和 .display-container
中的 margin-auto
,能夠將子元素垂直水平居中。 .display-container
元素也將做爲一個 flex-container
; 這樣,它的子元素會沿主軸被放置在同一行。
.note-display
列表項也將是一個 flex-container
。 因爲有不少子項被居中,因此咱們能夠經過 justify-content
和 align-items
屬性來完成。 全部 flex-items
都將垂直水平居中。 若是你不肯定它們是什麼,請查看「CSS Flexbox 可視化指南」中的對齊部分。
.note-display { display: flex; flex-direction: column; align-items: center; margin: 0 25px; }
讓咱們經過設置`stroke-width
,stroke-opacity
和 stroke-linecap
將筆劃應用於圓,這些規則會使畫面動起來。 接下來,咱們爲每一個圓添加一種顏色:
.circle__progress { fill: none; stroke-width: 3; stroke-opacity: 0.3; stroke-linecap: round; } .note-display:nth-child(1) .circle__progress { stroke: #AAFF00; } .note-display:nth-child(2) .circle__progress { stroke: #FF00AA; } .note-display:nth-child(3) .circle__progress { stroke: #AA00FF; } .note-display:nth-child(4) .circle__progress { stroke: #00AAFF; }
爲了絕對定位百分比元素,必須徹底知道這些概念是什麼。 .circle
元素應該是引用,因此讓咱們爲其添加添加 position: relative
。
注意:對絕對定位更深刻、直觀的解釋,請閱讀「一勞永逸的理解 CSS Position」一文。
另外一種使元素居中的方法是把 top: 50%
, left: 50%
和 transform: translate(-50%, -50%);
組合在一塊兒, 將元素的中心定位在其父級中心。
.circle { position: relative; } .percent { width: 100%; top: 50%; left: 50%; position: absolute; font-weight: bold; text-align: center; line-height: 28px; transform: translate(-50%, -50%); } .percent__int { font-size: 28px; } .percent__dec { font-size: 12px; } .label { font-family: 'Raleway', serif; font-size: 14px; text-transform: uppercase; margin-top: 15px; }
到目前爲止,模板應如該是下面這個樣子:
圖:完成的模板元素和樣式
能夠在兩個圓形SVG屬性的幫助下建立圓形動畫:stroke-dasharray
和 stroke-dashoffset
。
「
stroke-dasharray
定義筆劃中的虛線間隙模式。」
它最多可能須要四個值:
當它被設置爲惟一的整數( stroke-dasharray:10
)時,破折號和間隙具備相同的大小;
對於兩個值( stroke-dasharray:10 5
),第一個應用於破折號,第二個應用於間隙;
第三種和第四種形式(stroke-dasharray:10 5 2
和 stroke-dasharray:10 5 2 3
)將產生各類樣式的虛線和間隙。
圖:stroke-dasharray
屬性值
左邊的圖像顯示屬性stroke-dasharray
設置爲 0 到圓周長度 238px。
第二個圖像表示 stroke-dashoffset
屬性,它抵消了dash數組的開頭。 它的取值範圍也是從0到圓周長度。
圖:stroke-dasharray
和 stroke-dashoffset
屬性
爲了產生填充效果,咱們將 stroke-dasharray
設置爲圓周長度,以便它全部長度都能充滿其衝刺範圍而不留間隙。 咱們也會用相同的值抵消它,這樣會使它可以被「隱藏」。 而後,stroke-dashoffset
將更新爲對應的說明文字,根據過渡持續時間填充其行程。
屬性更新將經過CSS Variables在腳本中完成。 下面讓咱們聲明變量並設置屬性:
.circle__progress--fill { --initialStroke: 0; --transitionDuration: 0; stroke-opacity: 1; stroke-dasharray: var(--initialStroke); stroke-dashoffset: var(--initialStroke); transition: stroke-dashoffset var(--transitionDuration) ease; }
爲了設置初始值並更新變量,讓咱們從使用 document.querySelectorAll
選擇全部.note-display
元素開始。 同時把 transitionDuration
設置爲900毫秒。
而後,咱們遍歷顯示數組,選擇它的 .circle__progress.circle__progress--fill
並提取HTML中的 r
屬性集來計算周長。 有了它,咱們能夠設置初始的 --dasharray
和 --dashoffset
值。
當 --dashoffset
變量被 setTimeout
更新時,將發生動畫:
const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { let progress = display.querySelector('.circle__progress--fill'); let radius = progress.r.baseVal.value; let circumference = 2 * Math.PI * radius; progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); progress.style.setProperty('--initialStroke', circumference); setTimeout(() => progress.style.strokeDashoffset = 50, 100); });
要從頂部開始過分,必須旋轉 .circle__svg
元素:
.circle__svg { transform: rotate(-90deg); }
圖:Stroke 屬性轉換
如今,讓咱們計算相對於 note 的dashoffset
值。 note 值將經過 data-*
屬性插入每一個li
項目。 *
能夠替換爲任何符合你需求的名稱,而後能夠經過元素的數據集在元數據集中檢索:element.dataset.*
。
注意:你能夠在MDN Web Docs上獲得有關 data-*
屬性的更多信息。
咱們的屬性將被命名爲 「data-note
」:
<ul class="display-container"> + <li class="note-display" data-note="7.50"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Transparent</span> </li> + <li class="note-display" data-note="9.27"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Reasonable</span> </li> + <li class="note-display" data-note="6.93"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Usable</span> </li> + <li class="note-display" data-note="8.72"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Exemplary</span> </li> </ul>
parseFloat
方法將display.dataset.note
返回的字符串轉換爲浮點數。 offset
表示達到最高值時缺失的百分比。 所以,對於 7.50
note,咱們將獲得 (10 - 7.50) / 10 = 0.25
,這意味着 circumference
長度應該偏移其值的25%:
let note = parseFloat(display.dataset.note); let offset = circumference * (10 - note) / 10;
更新scripts.js:
const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { let progress = display.querySelector('.circle__progress--fill'); let radius = progress.r.baseVal.value; let circumference = 2 * Math.PI * radius; + let note = parseFloat(display.dataset.note); + let offset = circumference * (10 - note) / 10; progress.style.setProperty('--initialStroke', circumference); progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); + setTimeout(() => progress.style.strokeDashoffset = offset, 100); });
sroke屬性轉換爲note值
在繼續以前,讓咱們將stoke轉換提取到它本身的方法中:
const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { - let progress = display.querySelector('.circle__progress--fill'); - let radius = progress.r.baseVal.value; - let circumference = 2 * Math.PI * radius; let note = parseFloat(display.dataset.note); - let offset = circumference * (10 - note) / 10; - progress.style.setProperty('--initialStroke', circumference); - progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); - setTimeout(() => progress.style.strokeDashoffset = offset, 100); + strokeTransition(display, note); }); + function strokeTransition(display, note) { + let progress = display.querySelector('.circle__progress--fill'); + let radius = progress.r.baseVal.value; + let circumference = 2 * Math.PI * radius; + let offset = circumference * (10 - note) / 10; + progress.style.setProperty('--initialStroke', circumference); + progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); + setTimeout(() => progress.style.strokeDashoffset = offset, 100); + }
還有一件事就是把 note 從0.00
轉換到要最終的 note 值。 首先要作的是分隔整數和小數值。 可使用字符串方法split()
。 以後它們將被轉換爲數字,並做爲參數傳遞給 increaseNumber()
函數,經過整數和小數的標誌正確顯示在對應元素上。
const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { let note = parseFloat(display.dataset.note); + let [int, dec] = display.dataset.note.split('.'); + [int, dec] = [Number(int), Number(dec)]; strokeTransition(display, note); + increaseNumber(display, int, 'int'); + increaseNumber(display, dec, 'dec'); });
在 increaseNumber()
函數中,咱們究竟選擇 .percent__int
仍是 .percent__dec
元素,取決於 className
,以及輸出是否應包含小數點。 接下來把transitionDuration
設置爲900毫秒。 如今,動畫表示從0到7的數字,持續時間必須除以note 900 / 7 = 128.57ms
。 結果表示每次增長迭代將花費多長時間。 這意味着 setInterval
將每隔 128.57ms
觸發一次。
設置好這些變量後,接着定義setInterval
。 counter
變量將做爲文本附加到元素,並在每次迭代時增長:
function increaseNumber(display, number, className) { let element = display.querySelector(`.percent__${className}`), decPoint = className === 'int' ? '.' : '', interval = transitionDuration / number, counter = 0; let increaseInterval = setInterval(() => { element.textContent = counter + decPoint; counter++; }, interval); }
圖:計數增加
太酷了! 確實增長了計數值,但它在無限循環播放。 當note達到咱們想要的值時,還須要清除setInterval
。 能夠經過clearInterval
函數完成:
function increaseNumber(display, number, className) { let element = display.querySelector(`.percent__${className}`), decPoint = className === 'int' ? '.' : '', interval = transitionDuration / number, counter = 0; let increaseInterval = setInterval(() => { + if (counter === number) { window.clearInterval(increaseInterval); } element.textContent = counter + decPoint; counter++; }, interval); }
圖:最終完成
如今,數字更新到note值,並使用clearInterval()
函數清除。
教程到此就結束了,但願你能喜歡它!
若是你想開發一些更具互動性的東西,請查看使用 Vanilla JavaScript 建立的Memory Game Tutorial 。 它涵蓋了基本的HTML5,CSS3和JavaScript概念,如定位、透視、轉換、Flexbox、事件處理、超時和三元組。
祝你快樂的編碼!🌹
歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章