HTML5 SVG Isi Animasi Dengan CSS3 Dan Vanilla JavaScript
Diterbitkan: 2022-03-10SVG adalah singkatan dari S calable V ector G raphics dan merupakan bahasa markup standar berbasis XML untuk grafik vektor. Ini memungkinkan Anda menggambar jalur, kurva, dan bentuk dengan menentukan sekumpulan titik di bidang 2D. Selain itu, Anda dapat menambahkan properti twitch pada jalur tersebut (seperti goresan, warna, ketebalan, isian, dan lainnya) untuk menghasilkan animasi.
Sejak April 2017, Modul Isi dan Goresan Level 3 CSS memungkinkan warna SVG dan pola isian disetel dari lembar gaya eksternal, alih-alih menyetel atribut pada setiap elemen. Dalam tutorial ini, kita akan menggunakan warna hex polos sederhana, tetapi properti fill dan stroke juga menerima pola, gradien, dan gambar sebagai nilai.
Catatan : Saat mengunjungi situs web Awwwards, tampilan catatan animasi hanya dapat dilihat dengan lebar browser yang disetel ke 1024px atau lebih.
- Demo: Proyek Tampilan Catatan
- Repo: Catatan Tampilan Repo
Struktur File
Mari kita mulai dengan membuat file di terminal:
mkdir note-display cd note-display touch index.html styles.css scripts.js
HTML
Berikut adalah template awal yang menautkan file css
dan 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>
Setiap elemen not terdiri dari item daftar: li
yang menampung circle
, nilai note
, dan label
.
.circle_svg
adalah elemen SVG, yang membungkus dua elemen <circle>. Yang pertama adalah path yang akan diisi sedangkan yang kedua adalah isian yang akan dianimasikan.
note
dipisahkan menjadi bilangan bulat dan desimal sehingga ukuran font yang berbeda dapat diterapkan padanya. label
adalah <span>
sederhana. Jadi, menggabungkan semua ini terlihat seperti ini:
<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>
Atribut cx
dan cy
mendefinisikan titik pusat sumbu x dan sumbu y lingkaran. Atribut r
mendefinisikan radiusnya.
Anda mungkin telah memperhatikan pola garis bawah/garis putus-putus dalam nama kelas. Itu BEM, yang merupakan singkatan dari block
, element
dan modifier
. Ini adalah metodologi yang membuat penamaan elemen Anda lebih terstruktur, terorganisir, dan semantik.
Bacaan yang direkomendasikan : Penjelasan BEM Dan Mengapa Anda Membutuhkannya
Untuk menyelesaikan struktur template, mari kita bungkus empat item daftar dalam elemen daftar yang tidak berurutan:
<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>
Anda pasti bertanya pada diri sendiri apa arti label Transparent
, Reasonable
, Usable
, dan Exemplary
. Semakin Anda mengenal pemrograman, Anda akan menyadari bahwa menulis kode tidak hanya tentang membuat aplikasi berfungsi, tetapi juga memastikan bahwa itu akan dapat dipelihara dan diskalakan dalam jangka panjang. Itu hanya tercapai jika kode Anda mudah diubah.
“Akronim TRUE
akan membantu memutuskan apakah kode yang Anda tulis akan dapat mengakomodasi perubahan di masa mendatang atau tidak.”
Jadi, lain kali, tanyakan pada diri sendiri:
-
Transparent
: Apakah konsekuensi perubahan kode jelas? -
Reasonable
: Apakah manfaat biaya sepadan? - Dapat
Usable
: Apakah saya dapat menggunakannya kembali dalam skenario yang tidak terduga? -
Exemplary
: Apakah ini menyajikan kualitas tinggi sebagai contoh untuk kode masa depan?
Catatan : “Desain Berorientasi Objek Praktis di Ruby” oleh Sandi Metz menjelaskan TRUE
bersama dengan prinsip-prinsip lain dan bagaimana mencapainya melalui pola desain. Jika Anda belum meluangkan waktu untuk mempelajari pola desain, pertimbangkan untuk menambahkan buku ini ke bacaan sebelum tidur Anda.
CSS
Mari impor font dan terapkan reset ke semua item:
@import url('https://fonts.googleapis.com/css?family=Nixie+One|Raleway:200'); * { padding: 0; margin: 0; box-sizing: border-box; }
Properti box-sizing: border-box
menyertakan nilai padding dan border ke dalam total lebar dan tinggi elemen, sehingga lebih mudah untuk menghitung dimensinya.
Catatan : Untuk penjelasan visual tentang box-sizing
, silakan baca “Jadikan Hidup Anda Lebih Mudah Dengan CSS Box Sizing.”
body { height: 100vh; color: #fff; display: flex; background: #3E423A; font-family: 'Nixie One', cursive; } .display-container { margin: auto; display: flex; }
Dengan menggabungkan aturan display: flex
di body
dan margin-auto
di .display-container
, elemen turunan dapat dipusatkan secara vertikal dan horizontal. Elemen .display-container
juga akan menjadi flex-container
; dengan begitu, anak-anaknya akan ditempatkan pada baris yang sama di sepanjang sumbu utama.
.note-display
list juga akan menjadi flex-container
. Karena ada banyak anak untuk pemusatan, mari kita lakukan melalui properti justify-content
dan align-items
. Semua flex-items
akan dipusatkan di sepanjang cross
dan sumbu main
. Jika Anda tidak yakin apa itu, lihat bagian penyelarasan di "Panduan Visual Dasar-dasar CSS Flexbox."
.note-display { display: flex; flex-direction: column; align-items: center; margin: 0 25px; }
Mari kita terapkan goresan pada lingkaran dengan mengatur aturan stroke-width
, stroke-opacity
dan stroke-linecap
yang secara bersamaan menata gaya stroke secara langsung. Selanjutnya, mari tambahkan warna ke setiap lingkaran:
.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; }
Untuk memposisikan elemen percent
secara mutlak, perlu diketahui secara mutlak untuk apa. Elemen .circle
harus menjadi referensi, jadi mari tambahkan position: relative
terhadapnya.
Catatan : Untuk penjelasan visual yang lebih mendalam tentang pemosisian absolut, silakan baca “Cara Memahami Posisi Absolut CSS Sekali Dan Untuk Semua.”
Cara lain untuk memusatkan elemen adalah dengan menggabungkan top: 50%
, left: 50%
dan transform: translate(-50%, -50%);
yang memposisikan pusat elemen di pusat induknya.
.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; }
Sekarang, template akan terlihat seperti ini:
Isi Transisi
Animasi lingkaran dapat dibuat dengan bantuan dua properti SVG lingkaran: stroke-dasharray
dan stroke-dashoffset
.
“ stroke-dasharray
mendefinisikan pola dash-gap dalam sebuah stroke.”
Ini dapat mengambil hingga empat nilai:
- Saat disetel ke satu-satunya bilangan bulat (
stroke-dasharray: 10
), tanda hubung dan celah memiliki ukuran yang sama; - Untuk dua nilai (
stroke-dasharray: 10 5
), yang pertama diterapkan ke tanda hubung, kedua untuk celah; - Bentuk ketiga dan seterusnya (
stroke-dasharray: 10 5 2
danstroke-dasharray: 10 5 2 3
) akan menghasilkan garis putus-putus dan celah dalam berbagai ukuran.
Gambar di sebelah kiri menunjukkan properti stroke-dasharray
yang disetel dari 0 hingga 238px, yang merupakan panjang keliling lingkaran.
Gambar kedua mewakili properti stroke-dashoffset
yang mengimbangi awal larik tanda hubung. Itu juga diatur dari 0 ke panjang keliling lingkaran.
Untuk menghasilkan efek filling, kita akan mengatur stroke-dasharray
ke panjang keliling, sehingga semua panjangnya diisi dengan tanda hubung besar dan tidak ada celah. Kami juga akan mengimbanginya dengan nilai yang sama, sehingga menjadi "tersembunyi". Kemudian stroke-dashoffset
akan diperbarui ke nilai nada yang sesuai, mengisi stroke sesuai dengan durasi transisi.
Pembaruan properti akan dilakukan dalam skrip melalui Variabel CSS. Mari kita mendeklarasikan variabel dan mengatur properti:
.circle__progress--fill { --initialStroke: 0; --transitionDuration: 0; stroke-opacity: 1; stroke-dasharray: var(--initialStroke); stroke-dashoffset: var(--initialStroke); transition: stroke-dashoffset var(--transitionDuration) ease; }
Untuk menetapkan nilai awal dan memperbarui variabel, mari kita mulai dengan memilih semua elemen .note-display
dengan document.querySelectorAll
. transitionDuration
akan diatur ke 900
milidetik.
Kemudian, kita mengulangi melalui array tampilan, pilih .circle__progress.circle__progress--fill
dan ekstrak atribut r
yang ditetapkan dalam HTML untuk menghitung panjang keliling. Dengan itu, kita dapat mengatur nilai --dasharray
dan --dashoffset
awal.
Animasi akan muncul ketika variabel --dashoffset
diperbarui dengan setTimeout 100 md:
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); });
Untuk memulai transisi dari atas, elemen .circle__svg
harus diputar:
.circle__svg { transform: rotate(-90deg); }
Sekarang, mari kita hitung nilai dashoffset
— relatif terhadap catatan. Nilai catatan akan dimasukkan ke setiap item li
melalui atribut data-*. *
dapat diganti untuk nama apa pun yang sesuai dengan kebutuhan Anda dan kemudian dapat diambil dalam JavaScript melalui kumpulan data elemen: element.dataset.*
.
Catatan : Anda dapat membaca lebih lanjut tentang atribut data-* di MDN Web Docs.
Atribut kami akan disebut " 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>
Metode parseFloat
akan mengubah string yang dikembalikan oleh display.dataset.note
menjadi angka floating point. offset
mewakili persentase yang hilang untuk mencapai skor maksimum. Jadi, untuk not 7.50
, kita akan memiliki (10 - 7.50) / 10 = 0.25
, yang berarti panjang circumference
harus diimbangi dengan 25%
dari nilainya:
let note = parseFloat(display.dataset.note); let offset = circumference * (10 - note) / 10;
Memperbarui 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); });
Sebelum kita melanjutkan, mari kita ekstrak transisi stoke ke metodenya sendiri:
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); + }
Catatan Nilai Peningkatan
Masih ada transisi not dari 0.00
ke nilai not yang akan dibangun. Hal pertama yang harus dilakukan adalah memisahkan nilai integer dan desimal. Kami akan menggunakan metode string split()
(dibutuhkan argumen yang menentukan di mana string akan diputus dan mengembalikan array yang berisi kedua string yang rusak). Itu akan dikonversi ke angka dan diteruskan sebagai argumen ke fungsi increaseNumber()
, bersama dengan elemen display
dan tanda yang menunjukkan apakah itu bilangan bulat atau desimal.
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'); });
Dalam fungsi increaseNumber()
, kita memilih elemen .percent__int
atau .percent__dec
, bergantung pada className
, dan juga jika output harus berisi titik desimal atau tidak. Kami telah menyetel durasi transitionDuration
kami ke 900ms
. Nah, untuk menganimasikan angka dari 0 hingga 7, misalnya, durasinya harus dibagi dengan not 900 / 7 = 128.57ms
. Hasilnya menunjukkan berapa lama setiap peningkatan iterasi akan berlangsung. Ini berarti setInterval
kami akan diaktifkan setiap 128.57ms
.
Dengan set variabel tersebut, mari kita definisikan setInterval
. Variabel counter
akan ditambahkan ke elemen sebagai teks dan ditingkatkan pada setiap iterasi:
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); }
Dingin! Itu memang meningkatkan nilai, tetapi itu seperti itu selamanya. Kita perlu menghapus setInterval
ketika catatan mencapai nilai yang kita inginkan. Itu dilakukan dengan fungsi 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); }
Sekarang nomor tersebut diperbarui hingga nilai catatan dan dihapus dengan fungsi clearInterval()
.
Itu cukup banyak untuk tutorial ini. Saya harap Anda menikmatinya!
Jika Anda ingin membangun sesuatu yang sedikit lebih interaktif, lihat Tutorial Permainan Memori saya yang dibuat dengan JavaScript Vanilla. Ini mencakup konsep dasar HTML5, CSS3 dan JavaScript seperti penentuan posisi, perspektif, transisi, Flexbox, penanganan acara, batas waktu, dan ternaries.
Selamat mengkode!