HTML5 SVG Isi Animasi Dengan CSS3 Dan Vanilla JavaScript

Diterbitkan: 2022-03-10
Ringkasan cepat Dalam artikel ini, Anda dapat mempelajari cara membuat tampilan catatan animasi dari situs web Awwwards. Ini membahas elemen lingkaran HTML5 SVG, properti goresannya, dan cara menganimasikannya dengan variabel CSS dan JavaScript Vanilla.

SVG 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
Demo hasil akhir (Pratinjau besar)
  • Demo: Proyek Tampilan Catatan
  • Repo: Catatan Tampilan Repo
Lebih banyak setelah melompat! Lanjutkan membaca di bawah ini

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 .

Elemen daftar item dan anak-anak langsung
Elemen item daftar dan turunan langsungnya: .circle , .percent dan .label . (Pratinjau besar)

.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.

elemen SVG
elemen SVG. Pembungkus SVG dan tag lingkaran. (Pratinjau besar)

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:

Pembungkus daftar tidak berurutan
Pembungkus daftar tidak berurutan menampung empat anak li (Pratinjau besar)
 <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:

Template awal yang sudah jadi
Elemen dan gaya template yang telah selesai (Pratinjau besar)

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 dan stroke-dasharray: 10 5 2 3 ) akan menghasilkan garis putus-putus dan celah dalam berbagai ukuran.
Nilai properti stroke dasharray
nilai properti stroke-dasharray (Pratinjau besar)

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.

Stroke dasharray dan properti dashoffset
stroke-dasharray dan stroke-dashoffset properti (Pratinjau besar)

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); } 
Transisi properti stroke
Transisi properti goresan (Pratinjau besar)

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); }); 
Transisi properti stroke hingga nilai nada
Transisi properti goresan hingga nilai nada (Pratinjau besar)

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); } 
Peningkatan penghitung tak terbatas
Peningkatan penghitung tak terbatas (Pratinjau besar)

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); } 
Proyek tampilan catatan selesai
Proyek selesai (Pratinjau besar)

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!