Beberapa tahun yang lalu saat Olimpiade Tokyo 2020 saya membuat demo cincin Olimpiade animasi 3D. Saya menyukainya, tampak hebat, dan saya menyukai efek cincin yang saling bersilangan.
Namun kodenya sendiri sudah agak ketinggalan jaman. Saya menulisnya di SCSS, dan saya tidak melakukannya dengan baik. Saya tahu ini bisa lebih baik, setidaknya menurut standar modern.
-Advertisement-.
Jadi, saya memutuskan untuk membuat versi beta dari awal lagi untuk menghormati Olimpiade tahun ini. Saya menulis CSS dasar kali ini, memanfaatkan fitur modern seperti fungsi trigonometri untuk mengurangi jumlah angka ajaib dan aturan warna relatif untuk meningkatkan manajemen warna. Yang menarik, ternyata versi beta baru ini jauh lebih efisien dengan baris kode yang lebih sedikit dibandingkan versi SCSS lama yang saya tulis pada tahun 2020!
Lihatlah kembali tab CSS di demo pertama karena kita akan mendapatkan sesuatu yang benar-benar berbeda — dan lebih baik — dengan pendekatan yang akan kita gunakan bersama. Jadi, mari kita mulai!
Pengkodean
Kami akan menggunakan lapisan untuk membuat efek 3D. Lapisan-lapisan ini ditempatkan satu per satu (pada sumbu z) untuk mendapatkan kedalaman objek 3D, yang dalam kasus kita adalah sebuah cincin. Kombinasi bentuk, ukuran, dan warna setiap lapisan — serta perbedaannya dari satu lapisan ke lapisan lainnya — inilah yang menciptakan keseluruhan objek 3D.
Dalam hal ini, saya menggunakan 16 lapisan di mana setiap lapisan memiliki warna yang berbeda (dengan lapisan yang lebih gelap ditumpuk di belakang) untuk sedikit efek pencahayaan, dan menggunakan ukuran dan ketebalan setiap lapisan untuk membuat bentuk lingkaran bulat.
Untuk HTML, kita membutuhkan lima <div>
Elemen, satu untuk setiap episode, di mana masing-masing <div>
Ia memiliki 16 elemen yang bertindak sebagai lapisan, yang saya lapisi <i>
Tag. Kelima cincin itu akan kita masukkan ke dalam wadah utama untuk mengikat semuanya. Kami akan memberikan wadah utama .rings
kelas dan setiap episode, secara kreatif, .ring
musim.
Ini adalah versi singkat HTML yang menunjukkan cara mengompilasinya:
<div class="rings">
<div class="ring">
<i style="--i: 1;"></i>
<i style="--i: 2;"></i>
<i style="--i: 3;"></i>
<i style="--i: 4;"></i>
<i style="--i: 5;"></i>
<i style="--i: 6;"></i>
<i style="--i: 7;"></i>
<i style="--i: 8;"></i>
<i style="--i: 9;"></i>
<i style="--i: 10;"></i>
<i style="--i: 11;"></i>
<i style="--i: 12;"></i>
<i style="--i: 13;"></i>
<i style="--i: 14;"></i>
<i style="--i: 15;"></i>
<i style="--i: 16;"></i>
</div>
<!-- 4 more rings... -->
</div>
Melihat --i
Properti khusus yang Anda kunjungi style
Karakteristik masing-masing <i>
komponen:
<i style="--i: 1;"></i>
<i style="--i: 2;"></i>
<i style="--i: 3;"></i>
<!-- etc. -->
Kami akan menggunakan --i
Menghitung posisi, ukuran, dan warna setiap lapisan. Itu sebabnya saya menetapkan nilainya sebagai bilangan bulat dalam urutan menaik — ini akan menjadi kelipatan dari urutan dan desain setiap lapisan.
Kiat Pro: Anda dapat menghindari penulisan HTML secara manual untuk setiap lapisan jika Anda bekerja pada IDE yang mendukung Emmet. Namun jika belum, jangan khawatir, karena CodePen mendukungnya! Masukkan yang berikut ini ke dalam editor HTML Anda dan tekan Tab
Tekan tombol pada keyboard Anda untuk memperluasnya menjadi 16 lapisan: i*16[style="--i: $;"]
CSS (vanila)
Mari kita mulai dengan orang tua .rings
Untuk saat ini, container hanya akan mendapat posisi relatif. Tanpa menentukan posisi relatif, perulangan akan dihapus dari aliran dokumen dan berakhir di luar halaman di suatu tempat ketika posisi absolut ditetapkan pada perulangan tersebut.
.rings {
position: relative;
}
.ring {
position: absolute;
}
Mari kita lakukan hal yang sama dengan <i>
elemen, tetapi gunakan sarang CSS untuk menjaga kode tetap kompak. Kami akan menambahkan border-radius
Sedangkan bagian tepi persegi kita potong hingga membentuk lingkaran sempurna.
.rings {
position: relative;
}
.ring {
position: absolute;
i {
position: absolute;
border-radius: 50%;
}
}
Bagian terakhir dari desain dasar yang akan kita terapkan sebelum melanjutkan ke berikutnya adalah properti kustom untuk --ringColor
Ini akan membuat pewarnaan loop cukup mudah karena kita dapat menulisnya satu kali, dan kemudian mengulanginya lapis demi lapis. Kami mengumumkan --ringColor
pada border
Properti ini karena kita hanya ingin mewarnai tepi luar setiap lapisan, bukan mengisinya sepenuhnya background-color
:
.rings {
position: relative;
}
.ring {
position: absolute;
--ringColor: #0085c7;
i {
position: absolute;
inset: -100px;
border: 16px var(--ringColor) solid;
border-radius: 50%;
}
}
Apakah kamu memperhatikan aku menyelipkan sesuatu yang lain ke sana? Ini benar, inset
Properti juga ada dan disetel ke nilai negatif 100px
Ini mungkin terdengar agak aneh, jadi mari kita bicarakan hal itu terlebih dahulu selagi kita terus merancang karya kita.
Inklusi negatif
Tetapkan nilai negatif ke inset
Properti berarti posisi lapisan tersebut berada <>di luar> itu .ring
Barang. Jadi, kita mungkin menganggapnya sebagai “permulaan”. Dalam kasus kami, .ring
Itu tidak memiliki ukuran karena tidak ada konten atau properti CSS untuk memberikan dimensi. Artinya kelas itu inset
(atau lebih tepatnya “permulaan”) adalah 100px
Ke segala arah, menuju ke .ring
Ini adalah 200×200 piksel.

Mari kita periksa apa yang kita miliki sejauh ini:
Posisi untuk kedalaman
Kami menggunakan lapisan untuk menciptakan kesan mendalam. Kita melakukan ini dengan menempatkan masing-masing dari enam belas lapisan di sepanjang sumbu z, yang menumpuk elemen dari depan ke belakang. Kami akan memberi jarak antara setiap lapisan hanya 1 cm. 2px
Pisahkan – Ini adalah satu-satunya ruang yang kita perlukan untuk membuat sedikit pemisahan visual antara setiap lapisan, memberi kita kedalaman yang kita cari.
untuk diingat --i
Properti khusus yang kami gunakan dalam HTML?
<i style="--i: 1;"></i>
<i style="--i: 2;"></i>
<i style="--i: 3;"></i>
<!-- etc. -->
Sekali lagi, ini adalah pengganda yang membantu kita translate
Setiap lapisan berada di sepanjang sumbu z. Mari buat properti kustom baru yang mendefinisikan persamaan sehingga kita dapat menerapkannya ke setiap lapisan:
i {
--translateZ: calc(var(--i) * 2px);
}
Untuk apa kita menerapkan ini? Kita bisa menggunakan CSS transform
Milik. Dengan cara ini, kita dapat memutar layer secara vertikal (yaitu, rotateY()
) sambil menerjemahkannya sepanjang sumbu z:
i {
--translateZ: calc(var(--i) * 2px);
transform: rotateY(-45deg) translateZ(var(--translateZ));
}
Warna untuk penetasan
Untuk membuat bayangan warna, kita akan menggelapkan lapisan tergantung pada posisinya sehingga lapisan menjadi lebih gelap saat kita berpindah dari depan sumbu z ke belakang. Ada beberapa cara untuk melakukan ini. Salah satunya adalah dengan menghilangkan lapisan hitam lainnya sambil mengurangi opacity. Cara lain adalah dengan mengatur saluran “Kecerahan”. hsl()
Fungsi warna yang nilainya “lebih terang” di latar depan dan semakin gelap di belakang. Pilihan ketiga adalah bermain-main dengan opacity layer, tapi ini menjadi berantakan.
Meskipun kami menggunakan ketiga metode ini, menurut saya sintaks warna relatif CSS modern adalah cara terbaik. Kami telah menentukan mode warna default --ringColor
Properti khusus. Kita dapat memasukkannya melalui sintaks warna relatif untuk memanipulasinya dengan warna lain untuk setiap cincin <i>
lapisan.
Pertama, kita memerlukan properti khusus baru yang dapat kita gunakan untuk menghitung nilai “ringan”:
.ring {
--ringColor: #0085c7;
i {
--light: calc(var(--i) / 16);
border: 16px var(--ringColor) solid;
}
}
Kami akan menggunakan calc()
-Hasil yang diberikan di properti khusus lainnya menetapkan nilai default kami --ringColor
Melalui sintaks warna relatif dimana --light
Fitur custom membantu mengatur kecerahan warna yang dihasilkan.
.ring {
--ringColor: #0085c7;
i {
--light: calc(var(--i) / 16);
--layerColor: rgb(from var(--ringColor) calc(r * var(--light)) calc(g * var(--light)) calc(b * var(--light)));
border: 16px var(--ringColor) solid;
}
}
Itu persamaan yang sangat rumit! Namun tampaknya rumit karena rumus warna relatif memerlukan argumen untuk setiap saluran dalam warna (RGB) dan kami menghitung masing-masing saluran tersebut.
rgb(from origin-color channelR channelG channelB)
Untuk perhitungannya, kami mengalikan setiap saluran RGB dengan --light
Properti yang ditetapkan, yaitu angka di antaranya 0
Dan 1
divided
Tergantung pada jumlah lapisan, 16
.
Saatnya melakukan pemeriksaan lagi untuk melihat posisi kita:
Buat bentuknya
Untuk mendapatkan bentuk cincin melingkar, kita akan mengatur ukuran lapisan (yaitu ketebalan) menggunakan border
Di sinilah kita bisa mulai menggunakan trigonometri dalam pekerjaan kita!
Kami ingin ketebalan setiap cincin berada di antara nilai 0deg
ke 180deg
– Karena kita sebenarnya hanya membuat setengah lingkaran, – jadi kita akan membaginya 180deg
Tergantung pada jumlah lapisan, 16
yang keluar ke 11.25deg
. menggunakan sin()
Fungsi trigonometri (yang setara dengan <>balik> Dan <>Sisi miring sudut dalam> Sisi sudut kanan, kita mendapatkan ekspresi ini untuk layer --size
:
--size: calc(sin(var(--i) * 11.25deg) * 16px);
Jadi, terserah --i
Ditemukan dalam HTML, ini bertindak sebagai pengganda untuk penghitungan lapisan border
Ikan. Kami telah mendeklarasikan batas kelas sebagai berikut:
i {
border: 16px var(--ringColor) solid;
)
Sekarang kita dapat mengganti kode yang diprogram 16px
nilai dengan --size
akun:
i {
--size: calc(sin(var(--i) * 11.25deg) * 16px);
border: var(--size) var(--layerColor) solid;
)
Tetapi! Seperti yang Anda perhatikan, kami tidak mengubah ukuran layer saat kami mengubah ukurannya. border
tawaran itu. Akibatnya, bentuk lingkaran hanya muncul di sisi dalam lapisan saja. Kuncinya di sini adalah memahami penyetelan itu --size
dengan inset
Properti berarti tidak mempengaruhi elemen box-sizing
Hasilnya adalah cincin 3D yang jelas, namun sebagian besar bayangannya terkubur.
⚠️ Memutar media secara otomatis

Kita dapat menampilkan shader dengan perhitungan baru inset
Untuk setiap lapisan. Inilah yang saya lakukan di versi 2020, tetapi saya rasa saya menemukan cara yang lebih mudah: tambahkan outline
Dengan hal yang sama border
Nilai yang dibutuhkan untuk menyelesaikan busur ada di sisi luar ring.
i {
--size: calc(sin(var(--i) * 11.25deg) * 16px);
border: var(--size) var(--layerColor) solid;
outline: var(--size) var(--layerColor) solid;
}
Kami memiliki lingkaran yang tampak lebih alami setelah kami buat outline
:
Pindahkan cincinnya
Saya harus memindahkan cincin di demo terakhir untuk membandingkan bayangan cincin sebelum dan sesudahnya. Kami akan menggunakan animasi yang sama di demo final, jadi mari tunjukkan bagaimana Anda melakukannya sebelum menambahkan empat episode lainnya ke HTML
Saya tidak mencoba melakukan sesuatu yang mewah; Saya hanya mengatur rotasi pada sumbu y -45deg
ke 45deg
(itu translateZ
(Nilainya tetap konstan).
@keyframes ring {
from { transform: rotateY(-45deg) translateZ(var(--translateZ, 0)); }
to { transform: rotateY(45deg) translateZ(var(--translateZ, 0)); }
}
Adapun animation
Properti, saya telah memberikan namanya ring
dan durasi tertentu (setidaknya untuk saat ini) 3s
yang diulangi tanpa henti. Sesuaikan fungsi pengaturan waktu animasi menggunakan ease-in-out
Dan alternate
masing-masing, memberi kita gerakan maju mundur yang mulus.
i {
animation: ring 3s infinite ease-in-out alternate;
}
Beginilah cara kerja animasi!
Tambahkan lebih banyak episode
Sekarang kita dapat menambahkan empat loop tersisa ke HTML. Ingat kami memiliki total lima episode dan setiap episode memiliki 16 <i>
Lapisan. Ini mungkin terlihat sederhana seperti ini:
<div class="rings">
<div class="ring"> <!-- <i> layers --> </div>
<div class="ring"> <!-- <i> layers --> </div>
<div class="ring"> <!-- <i> layers --> </div>
<div class="ring"> <!-- <i> layers --> </div>
<div class="ring"> <!-- <i> layers --> </div>
</div>
Ada sesuatu yang elegan dalam kesederhanaan tanda ini. Dan kita bisa menggunakan CSS nth-child()
Pemilih semu untuk mengidentifikasi mereka satu per satu. Saya ingin lebih jelas dari itu dan saya akan mencobanya masing-masing .ring
Dan kelas tambahan yang bisa kita gunakan untuk mendefinisikan loop tertentu secara eksplisit.
<div class="rings">
<div class="ring ring__1"> <!-- layers --> </div>
<div class="ring ring__2"> <!-- layers --> </div>
<div class="ring ring__3"> <!-- layers --> </div>
<div class="ring ring__4"> <!-- layers --> </div>
<div class="ring ring__5"> <!-- layers --> </div>
</div>
Tugas kita sekarang adalah menyesuaikan setiap episode satu per satu. Saat ini, semuanya tampak seperti episode pertama yang kami buat bersama. Kita akan menggunakan kelas unik yang baru saja kita atur di HTML untuk memberikan warna, posisi, dan durasi animasinya sendiri.
Kabar baiknya adalah kami telah menggunakan fitur khusus selama ini! Yang harus kita lakukan adalah memperbarui nilai di kelas setiap loop unik.
.ring {
&.ring__1 { --ringColor: #0081c8; --duration: 3.2s; --translate: -240px, -40px; }
&.ring__2 { --ringColor: #fcb131; --duration: 2.6s; --translate: -120px, 40px; }
&.ring__3 { --ringColor: #444444; --duration: 3.0s; --translate: 0, -40px; }
&.ring__4 { --ringColor: #00a651; --duration: 3.4s; --translate: 120px, 40px; }
&.ring__5 { --ringColor: #ee334e; --duration: 2.8s; --translate: 240px, -40px; }
}
Jika Anda bertanya-tanya di mana itu --ringColor
Nilai-nilai tersebut berasal dari warna yang didokumentasikan oleh Komite Olimpiade Internasional. --duration
Mereka sedikit diimbangi satu sama lain untuk mengganggu pergerakan antara cincin dan cincin --translate
'Dr 120px
Pisahkan lalu bersarang secara vertikal dengan bergantian posisinya 40px
Dan -40px
.
Mari kita terapkan materi terjemahannya .ring
Elemen:
.ring {
transform: translate(var(--translate));
}
Sebelumnya, kami mengatur durasi animasi menjadi tiga detik yang telah diprogram sebelumnya:
i {
animation: ring 3s infinite ease-in-out alternate;
}
Ini saat yang tepat untuk menggantinya dengan fitur khusus yang menghitung durasi setiap episode satu per satu.
i {
animation: ring var(--duration) -10s infinite ease-in-out alternate;
}
Wah, wah! Ada apa? -10s
Berapakah nilai dari tindakan ini? Meskipun setiap lapisan loop diatur untuk dianimasikan dengan durasi berbeda, sudut awal animasinya sama. Menambahkan penundaan negatif tetap saat mengubah durasi akan memastikan bahwa setiap putaran animasi dimulai pada sudut yang berbeda.
Sekarang kita memiliki sesuatu yang ada <>hampir tidak> saya menyelesaikan:
Beberapa sentuhan akhir
Kami telah mencapai tahap akhir! Animasinya tampak bagus, tetapi saya ingin menambahkan beberapa hal lagi. Yang pertama adalah gambar kecil-10deg
“Miringkan” pada sumbu x elemen aslinya .rings
Wadah. Ini akan membuat kita seolah-olah melihat sesuatu dari sudut pandang yang lebih tinggi.
.rings {
rotate: x -10deg;
}
Sentuhan terakhir kedua menyangkut bayangan. Kami benar-benar dapat mendefinisikan kedalaman 3D pada pekerjaan kami dan yang diperlukan hanyalah sebuah definisi .ring
Elemen ::after
Elemen palsu dan desainnya <>Dia mencintai> Bayangan.
Pertama, kita akan mengatur lebar batas dummy dan outline ke nilai tetap (24px
) sambil mengatur warnanya menjadi hitam semi transparan (#0003
). Maka kita akan melakukannya translate
Jadi mereka tampak semakin jauh. Kami juga akan melakukannya inset
Kami mengaturnya agar cocok dengan episode sebenarnya. Pada dasarnya, kami memindahkan elemen semu relatif terhadap elemen sebenarnya.
.ring {
/* etc. */
&::after {
content: '';
position: absolute;
inset: -100px;
border: 24px #0003 solid;
outline: 24px #0003 solid;
translate: 0 -100px -400px;
}
}
Gambar palsu tersebut tampaknya tidak begitu misterius saat ini. Tapi nanti akan terlihat seperti itu blur()
mereka sedikit:
.ring {
/* etc. */
&::after {
content: '';
position: absolute;
inset: -100px;
border: 24px #0003 solid;
outline: 24px #0003 solid;
translate: 0 -100px -400px;
filter: blur(12px);
}
}
Bayangannya juga berbentuk agak persegi. Mari kita pastikan bentuknya bulat seperti cincin:
.ring {
/* etc. */
&::after {
content: '';
position: absolute;
inset: -100px;
border: 24px #0003 solid;
outline: 24px #0003 solid;
translate: 0 -100px -400px;
filter: blur(12px);
border-radius: 50%;
}
}
Oh, dan kita harus memasang animasi yang sama pada yang palsu sehingga bayangan bergerak bersamaan dengan cincin:
.ring {
/* etc. */
&::after {
content: '';
position: absolute;
inset: -100px;
border: 24px #0003 solid;
outline: 24px #0003 solid;
translate: 0 -100px -400px;
filter: blur(12px);
border-radius: 50%;
animation: ring var(--duration) -10s infinite ease-in-out alternate;
}
}
Demo terakhir
Mari kita berhenti dan mengagumi hasil karya kita:
Pada akhirnya, saya sangat senang dengan cincin Olimpiade versi 2024. Versi 2020 menyelesaikan pekerjaannya dan mungkin merupakan pendekatan yang tepat pada saat itu. Namun dengan semua fitur yang kita dapatkan di CSS modern saat ini, saya memiliki banyak peluang untuk meningkatkan kode sehingga tidak hanya lebih efisien, namun lebih dapat digunakan kembali – misalnya, ini dapat digunakan di proyek lain dan “disesuaikan” hanya dengan memperbarui --ringColor
Properti yang dialokasikan.
Pada akhirnya, latihan ini menunjukkan kepada saya kekuatan dan fleksibilitas CSS modern. Kami mengambil ide yang sudah ada dengan kompleksitas dan menciptakannya kembali dengan kesederhanaan dan keanggunan.
Cincin Olimpiade CSS awalnya diterbitkan di CSS-Tricks, bagian dari keluarga DigitalOcean. Anda harus mendapatkan buletin.