HTML5 SVG使用CSS3和Vanilla JavaScript填充動畫
翻譯:第一秩序
摘要 在這篇文章中你將瞭解Awwwards網是怎樣實現動畫的。 本文介紹了HTML5 SVG中的circle 元素,它的stroke屬性,以及如何使用CSS變數以及用 Vanilla JavaScript 為它們設定動畫。
SVG是一種基於XML的,用於定義縮放向量圖形的標記語言。 它允許你通過在2D平面中確定的一組點來繪製路徑、曲線和形狀。 此外你還可以通過在這些路徑上新增動態屬性(例如筆觸,顏色,粗細,填充等)來生成動畫。
從2017年4月起,CSS Level 3 填充和描邊模組開始支援從外部樣式表設定SVG顏色和填充圖案,而不是在每個元素上設定屬性。 在本教程中,我們將會使用簡單的純十六進位制顏色,不過填充和描邊屬性也支援圖案,漸變和影象作為值。
注意:訪問Awwwards網站時,你需要把瀏覽器寬度設定為1024px或更高的才能更好的檢視動畫顯示。
檔案結構
讓我們從在終端中建立檔案開始:
? mkdir note-display
? cd note-display
? touch index.html styles.css scripts.js
複製程式碼
HTML
這是連線css
和js
檔案的初始模板:
<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
。
圖:列出項元素及其直接子元素:.circle
, .percent
和 .label
.circle_svg
是一個SVG元素,它包含兩個
圖:SVG元素:SVG包裝器和圓形標籤
註釋分為整數和小數,所以可以把它們設定為不同大小的字型。 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
和其他原則,以及如何通過設計模式實現它們。 如果你還沒有開始研究設計模式,請考慮將此書放到自己的案頭。
CSS
讓我們匯入字型並使其對所有內容生效:
@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、事件處理、超時和三元組。