CSS Ayrıntılı İnceleme - mükemmel kareler için özel kaydırma çubuğu için matrix3d()

Özel kaydırma çubukları son derece nadirdir. Bunun başlıca nedeni, kaydırma çubuklarının web'de neredeyse stilize edilemeyecek kalan öğelerden biri olmasıdır (tarih seçiciyi kastediyoruz). Kendinizinkiyle değiştirmek için JavaScript'i kullanabilirsiniz ancak bu yöntem pahalı, düşük kaliteli ve gecikmeli olabilir. Bu makalede, kaydırma sırasında JavaScript'in yanı sıra yalnızca bazı kurulum kodları gerektiren özel bir kaydırma çubuğu oluşturmak için bazı alışılmışın dışında CSS matrislerinden yararlanacağız.

Özet

Küçük şeyleri umursamıyor musunuz? Yalnızca Nyan cat demosuna göz atmak ve kitaplığı almak mı istiyorsunuz? Demo'nun kodunu GitHub depomuzda bulabilirsiniz.

LAM;WRA (Uzun ve matematiksel; yine de okunur)

Bir süre önce paralaks kaydırma oluşturduk (Bu makaleyi okudunuz mu? Gerçekten çok iyi, zaman ayırmaya değer. CSS 3D dönüşümleri kullanılarak geri itilen öğeler, gerçek kaydırma hızımızdan daha yavaş hareket ediyordu.

Özet

Paralaks kaydırma çubuğunun işleyiş şekliyle ilgili bir özetle başlayalım.

Animasyonda gösterildiği gibi, öğeleri 3D uzayda Z ekseni boyunca "geri" iterek paralaks etkisine ulaştık. Dokümanda kaydırma işlemi, Y ekseni boyunca bir çeviri işlemidir. Dolayısıyla, örneğin 100 piksel aşağı kaydırırsak her öğe 100 piksel yukarı kaydırılır. Bu durum, "daha uzakta" olanlar da dahil olmak üzere tüm öğeler için geçerlidir. Ancak kameradan daha uzakta oldukları için ekrandaki gözlenen hareketleri 100 pikselden az olur ve istenen paralaks etkisini oluşturur.

Elbette, bir öğeyi uzayda geriye doğru hareket ettirdiğinizde öğe daha küçük görünür. Bu durumu, öğeyi tekrar ölçeklendirerek düzeltiriz. Paralel kaydırma özelliğini geliştirirken tüm matematiksel hesaplamaları yaptık. Bu nedenle, tüm ayrıntıları tekrarlamayacağım.

0. Adım: Ne yapmak istiyoruz?

Kaydırma çubukları. Bu şekilde bir uygulama oluşturacağız. Peki, bu kişilerin ne yaptığını hiç düşündünüz mü? Kesinlikle hayır. Kaydırma çubukları, mevcut içeriğin ne kadarının şu anda görünür olduğunu ve okuyucu olarak ne kadar ilerlediğinizi gösterir. Kaydırma çubuğunu aşağı kaydırdığınızda, sona doğru ilerlediğinizi gösterir. Tüm içerik görüntü alanına sığıyorsa kaydırma çubuğu genellikle gizlenir. İçerik, görüntü alanının 2 katı yükseklikteyse kaydırma çubuğu, görüntü alanının yüksekliğinin yarısını doldurur. Görüntü alanının 3 katı yüksekliğindeki içerik, kaydırma çubuğunu görüntü alanının ⅓'üne ölçeklendirir. Bu kalıbı anlamışsınızdır. Sitede daha hızlı gezinmek için kaydırma çubuğunu tıklayıp sürükleyebilirsiniz. Bu, bu tür göze çarpmayan bir öğe için şaşırtıcı bir davranış miktarı. Bir seferde bir sorunla ilgilenelim.

1. Adım: Geri vitese alma

Tamam, paralaks kaydırma makalesinde belirtildiği gibi CSS 3D dönüşümleriyle öğelerin kaydırma hızından daha yavaş hareket etmesini sağlayabiliriz. Yönü tersine çevirebilir miyiz? Bu yöntemi kullanarak, çerçeveye mükemmel şekilde uyan özel bir kaydırma çubuğu oluşturduk. Bunun işleyiş şeklini anlamak için öncelikle CSS 3D ile ilgili temel bilgilerden bazılarını ele almamız gerekir.

Matematiksel anlamda herhangi bir perspektif projeksiyonu elde etmek için büyük olasılıkla tek biçimli koordinatlar kullanmanız gerekir. Ne olduklarını ve neden işe yaradıklarını ayrıntılı olarak açıklamayacağız ancak bunları w adlı dördüncü bir koordinat içeren 3D koordinatlar olarak düşünebilirsiniz. Perspektif bozulmasını istiyorsanız bu koordinat 1 olmalıdır. 1'den başka bir değer kullanmayacağımızdan w ile ilgili ayrıntılar hakkında endişelenmenize gerek yoktur. Bu nedenle, tüm noktalar artık 4 boyutlu vektörlerdir [x, y, z, w=1] ve dolayısıyla matrislerin de 4x4 olması gerekir.

CSS'nin arka planda homojen koordinatlar kullandığını görebileceğiniz durumlardan biri, matrix3d() işlevini kullanarak bir dönüştürme mülkünde kendi 4x4 matrislerinizi tanımladığınız zamandır. matrix3d, 16 bağımsız değişken alır (matris 4x4 olduğundan) ve sütunları tek tek belirtir. Bu işlevi kullanarak dönme, kaydırma vb. işlemleri manuel olarak belirtebiliriz. Ancak bu işlev, w koordinatıyla da oynamamıza olanak tanır.

matrix3d() değerini kullanabilmek için 3D bir bağlama ihtiyacımız vardır. 3D bağlam olmadan perspektif bozulması olmaz ve homojen koordinatlara ihtiyaç duyulmaz. 3D bağlam oluşturmak için perspective içeren ve içinde yeni oluşturulan 3D alanda dönüştürebileceğimiz bazı öğeler bulunan bir kapsayıcıya ihtiyacımız vardır. Örneğin:

CSS'nin perspektif özelliğini kullanarak bir div'i bozan bir CSS kodu parçası.

Perspektif kapsayıcı içindeki öğeler CSS motoru tarafından aşağıdaki şekilde işlenir:

  • Bir öğenin her köşesini (köşe noktası), perspektif kapsayıcıya göre tekdüze koordinatlara [x,y,z,w] dönüştürün.
  • Öğenin tüm dönüştürmelerini sağdan sola matrisler olarak uygulayın.
  • Perspektif öğesi kaydırılabilirse kaydırma matrisi uygulayın.
  • Perspektif matrisini uygulayın.

Kaydırma matrisi, y ekseni boyunca bir çeviridir. 400 piksel aşağı kaydırırsak tüm öğelerin 400 piksel yukarı taşınması gerekir. Perspektif matrisi, 3D uzayda ne kadar geride olurlarsa olsunlar noktaları kaybolan noktaya daha yakın bir yere "çeken" bir matristir. Bu sayede, nesneler daha uzaktayken daha küçük görünür ve çevrilirken de "daha yavaş hareket eder". Bu nedenle, bir öğe geri itilirse 400 piksellik bir kaydırma, öğenin ekranda yalnızca 300 piksel hareket etmesine neden olur.

Tüm ayrıntıları öğrenmek istiyorsanız CSS'nin dönüştürme oluşturma modeliyle ilgili özelliği okumanız gerekir. Ancak bu makale için yukarıdaki algoritmayı basitleştirdik.

Kutumuz, perspective özelliği için p değerine sahip bir perspektif kapsayıcı içindedir. Kapsayıcının kaydırılabilir olduğunu ve n piksel aşağı kaydırıldığını varsayalım.

Perspektif matrisi çarpı kaydırma matrisi çarpı öğe dönüştürme matrisi, dördüncü satırın üçüncü sütununda p bölü eksi bir olan dört dörtlük kimlik matrisi çarpı ikinci satırın dördüncü sütununda eksi n olan dört dörtlük kimlik matrisi çarpı öğe dönüştürme matrisine eşittir.

İlk matris perspektif matrisi, ikinci matris ise kaydırma matrisidir. Özetlemek gerekirse: Kaydırma matrisinin görevi, aşağı kaydırdığımızda bir öğenin yukarı taşınmasını sağlamaktır. Bu nedenle negatif işaret kullanılır.

Ancak kaydırma çubuğunuzda tam tersini istiyoruz. Yani aşağı kaydırdığımızda öğemizin aşağı hareket etmesini istiyoruz. Burada bir hile kullanabiliriz: Kutu köşelerimizin w koordinatını tersine çeviririz. w koordinatı -1 ise tüm çeviriler ters yönde gerçekleşir. Peki bunu nasıl yapacağız? CSS motoru, kutumuzun köşelerini homojen koordinatlara dönüştürür ve w değerini 1 olarak ayarlar. matrix3d()'nin zamanı geldi.

.box {
  transform:
    matrix3d(
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, -1
    );
}

Bu matris, w değerini inkar etmekten başka bir şey yapmaz. Bu nedenle, CSS motoru her köşeyi [x,y,z,1] biçiminde bir vektöre dönüştürdüğünde matris bunu [x,y,z,-1] biçiminde dönüştürür.

Dördüncü satır üçüncü sütununda p bölü eksi bir olan dört dörtlük kimlik matrisi çarpı ikinci satır dördüncü sütununda eksi n olan dört dörtlük kimlik matrisi çarpı dördüncü satır dördüncü sütununda eksi bir olan dört dörtlük kimlik matrisi çarpı dört boyutlu vektör x, y, z, 1 eşittir dördüncü satır üçüncü sütununda p bölü eksi bir olan dört dörtlük kimlik matrisi çarpı ikinci satır dördüncü sütununda eksi n olan dört dörtlük kimlik matrisi çarpı dördüncü satır dördüncü sütununda eksi bir olan dört dörtlük kimlik matrisi eşittir dört boyutlu vektör x, y artı n, z, p eksi 1 bölü eksi z.

Öğe dönüştürme matrisimizin etkisini göstermek için bir ara adım listelendi. Matris matematik konusunda kendinizi rahat hissetmiyorsanız sorun değil. En önemli nokta, son satırda kaydırma ofsetini (n) çıkarmak yerine y koordinatımıza eklememizdir. Aşağı kaydırırsak öğe aşağı çevrilir.

Ancak bu matrisi örneğimize eklersek öğe gösterilmez. Bunun nedeni, CSS spesifikasyonunun w < 0 olan tüm köşelerin öğenin oluşturulmasını engellemesini gerektirmesidir. z koordinatımız şu anda 0 ve p 1 olduğundan w -1 olur.

Neyse ki z değerini seçebiliriz. Sonucun w=1 olmasını sağlamak için z = -2 değerini ayarlamamız gerekir.

.box {
  transform:
    matrix3d(
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, -1
    )
    translateZ(-2px);
}

Kutusu geri geldi.

2. Adım: Hareket ettirin

Kutumuz artık orada ve herhangi bir dönüştürme işlemi yapılmamış gibi görünüyor. Şu anda perspektif kapsayıcısı kaydırılabilir olmadığı için göremiyoruz ancak kaydırıldığında öğemizin diğer yöne gideceğini biliyoruz. Kapsayıcıyı kaydıralım. Alan kaplayan bir boşluk öğesi eklememiz yeterlidir:

<div class="container">
    <div class="box"></div>
    <span class="spacer"></span>
</div>

<style>
/* … all the styles from the previous example … */
.container {
    overflow: scroll;
}
.spacer {
    display: block;
    height: 500px;
}
</style>

Şimdi kutuyu kaydırın. Kırmızı kutu aşağı hareket eder.

3. Adım: Boyut belirleyin

Sayfa aşağı kaydırılırken aşağı hareket eden bir öğemiz var. En zor kısmı atlattık. Şimdi de kaydırma çubuğu gibi görünecek şekilde biçimlendirmemiz ve biraz daha etkileşimli hale getirmemiz gerekiyor.

Kaydırma çubuğu genellikle bir "ok" ve bir "ray"dan oluşur. Ray her zaman görünmez. Küçük resmin yüksekliği, içeriğin ne kadarının görünür olduğuna bağlıdır.

<script>
    const scroller = document.querySelector('.container');
    const thumb = document.querySelector('.box');
    const scrollerHeight = scroller.getBoundingClientRect().height;
    thumb.style.height = /* ??? */;
</script>

scrollerHeight, kaydırılabilir öğenin yüksekliği, scroller.scrollHeight ise kaydırılabilir içeriğin toplam yüksekliğidir. scrollerHeight/scroller.scrollHeight, içeriğin görünür kısmının kesridir. Küçük resmin kapladığı dikey alan oranı, görünen içeriğin oranına eşit olmalıdır:

Başparmak noktası stili nokta yüksekliği, kaydırma çubuğu yüksekliği bölü kaydırma noktası kaydırma yüksekliğine eşitse ve yalnızca bu durumda kaydırma çubuğu yüksekliği bölü kaydırma noktası kaydırma yüksekliği çarpı kaydırma çubuğu yüksekliği, başparmak noktası stili nokta yüksekliğine eşit olur.
<script>
    // …
    thumb.style.height =
    scrollerHeight * scrollerHeight / scroller.scrollHeight + 'px';
    // Accommodate for native scrollbars
    thumb.style.right =
    (scroller.clientWidth - scroller.getBoundingClientRect().width) + 'px';
</script>

Başparmak boyutu iyi görünüyor ancak çok hızlı hareket ediyor. Burada, paralaks kaydırma çubuğundan tekniğimizi alabilir. Öğeyi daha geriye taşırsak kaydırma sırasında daha yavaş hareket eder. Boyutu büyüterek düzeltebiliriz. Ancak tam olarak ne kadar geri itmeliyiz? Hemen tahmin ettiğiniz gibi matematik yapalım. Söz veriyorum, son kez.

Buradaki önemli nokta, kaydırma çubuğu en alta kaydırıldığında alt kenarının kaydırılabilir öğenin alt kenarıyla hizalanmasıdır. Diğer bir deyişle: scroller.scrollHeight - scroller.height piksel kaydırmışsak başparmağımızın scroller.height - thumb.height tarafından çevrilmesini isteriz. Kaydırma çubuğunun her pikseli için başparmağımızın bir piksel kesri kadar hareket etmesini isteriz:

Faktör, kaydırma çubuğu noktası yüksekliği eksi kaydırma çubuğu izi yüksekliği bölü kaydırma çubuğu izi kaydırma yüksekliği eksi kaydırma çubuğu noktası yüksekliğine eşittir.

Bu bizim ölçeklendirme faktörümüzdür. Şimdi ölçeklendirme faktörünü z ekseni boyunca bir kaydırmaya dönüştürmemiz gerekiyor. Bunu paralaks kaydırma makalesinde zaten yapmıştık. Spesifikasyondaki ilgili bölüme göre: Ölçeklendirme faktörü p/(p − z) değerine eşittir. Başparmağımızı z ekseni boyunca ne kadar kaydırmamız gerektiğini bulmak için bu denklemi z için çözebiliriz. Ancak w koordinatı hilelerimiz nedeniyle z boyunca ek bir -2px çevirmemiz gerektiğini unutmayın. Ayrıca, bir öğenin dönüşümlerinin sağdan sola uygulandığını unutmayın. Yani özel matrisimizden önceki tüm çeviriler tersine çevrilmez, ancak özel matrisimizden sonraki tüm çeviriler tersine çevrilir. Bunu kodlayalım.

<script>
    // ... code from above...
    const factor =
    (scrollerHeight - thumbHeight)/(scroller.scrollHeight - scrollerHeight);
    thumb.style.transform = `
    matrix3d(
        1, 0, 0, 0,
        0, 1, 0, 0,
        0, 0, 1, 0,
        0, 0, 0, -1
    )
    scale(${1/factor})
    translateZ(${1 - 1/factor}px)
    translateZ(-2px)
    `;
</script>

Kaydırma çubuğunu kullanabilirsiniz. Bu, istediğimiz gibi stil verebildiğimiz bir DOM öğesidir. Erişilebilirlik açısından yapılması gereken önemli bir işlem, kullanıcıların kaydırma çubuklarıyla bu şekilde etkileşime alışkın olması nedeniyle başparmağın tıklayıp sürükleme işlemine yanıt vermesini sağlamaktır. Bu blog yayınını daha da uzatmamak için bu bölümle ilgili ayrıntıları açıklamayacağım. Bunun nasıl yapıldığını öğrenmek için kitaplık koduna göz atın.

iOS'te durum nedir?

Ah, eski dostum iOS Safari. Paralaks kaydırmada olduğu gibi burada da bir sorunla karşılaşıyoruz. Bir öğe üzerinde kaydırıyoruz, bu nedenle -webkit-overflow-scrolling: touch değerini belirtmemiz gerekiyor ancak bu, 3D düzleştirmeye neden oluyor ve kaydırma efektimizin tamamı çalışmayı durduruyor. Paralaks kaydırma çubuğunda bu sorunu, iOS Safari'yi algılayarak ve geçici çözüm olarak position: sticky'e başvurarak çözdük. Burada da tam olarak aynısını yapacağız. Hatırlamak için paralellik makalesine göz atın.

Tarayıcı kaydırma çubuğu ne olacak?

Bazı sistemlerde kalıcı, yerel bir kaydırma çubuğuyla uğraşmamız gerekir. Geçmişte kaydırma çubuğu gizlenememiştir (standart olmayan sözde seçici dışında). Bu nedenle, bunu gizlemek için bazı (matematik içermeyen) bilgisayar korsanlığı yöntemlerine başvurmamız gerekiyor. Kaydırma öğemizi overflow-x: hidden ile bir kapsayıcıya sarmalıyoruz ve kaydırma öğesini kapsayıcıdan daha geniş yapıyoruz. Tarayıcının yerel kaydırma çubuğu artık görünmüyor.

Fin

Tüm bunları bir araya getirerek artık Nyan cat demo'daki gibi kare mükemmelliğinde özel bir kaydırma çubuğu oluşturabiliriz.

Nyan cat'i göremiyorsanız bu demoyu oluştururken bulduğumuz ve kaydettiğimiz bir hatayla karşılaşıyorsunuzdur (Nyan cat'i göstermek için küçük resmi tıklayın). Chrome, ekran dışındaki öğeleri boyama veya animasyonlu hale getirme gibi gereksiz işlerden kaçınmada çok başarılıdır. Kötü haberimiz ise matrisle ilgili şakalarımız nedeniyle Chrome'un, Nyan Cat gif'inin aslında ekranda olmadığını düşünmesi. Bu sorunun kısa süre içinde çözüleceğini umuyoruz.

İşte bu kadar. Çok çalıştık. Tümünü okuduğunuz için sizi tebrik ediyorum. Bu işlemin çalışması için bazı hilelere başvurmanız gerekir. Özelleştirilmiş kaydırma çubuğunun deneyimin önemli bir parçası olmadığı sürece bu işleme değmez. Ancak bunun mümkün olduğunu bilmek de iyi bir şey, değil mi? Özel kaydırma çubuğunun bu kadar zor olması, CSS tarafında yapılması gereken çalışmalar olduğunu gösteriyor. Ancak endişelenmeyin. Gelecekte Houdini'nin AnimationWorklet aracı, kaydırmayla bağlantılı bu tür efektleri kare cinsinden mükemmel şekilde oluşturmayı çok daha kolay hale getirecek.