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 ve bunun nedeni büyük ölçüde kaydırma çubuklarının web'de kolayca şekillendirilemeyen bitlerden biri olmasıdır (Sana bakıyorum, tarih seçici). Kendinizinkiyle oluşturmak 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? Demonun 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 çevirme işlemidir. Dolayısıyla, örneğin 100 piksel aşağı kaydırırsak her öğe 100 piksel yukarı kaydırılır. Bu, "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 tekrar uzaya taşımak, öğenin daha küçük görünmesine de neden olur. Bu sorunu, öğeyi tekrar ölçeklendirerek düzeltiriz. Paralel kaydırma özelliğini geliştirirken tüm matematiksel hesaplamaları yaptığımız için 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ığarsa kaydırma çubuğu genellikle gizlenir. İçerik, görüntü alanının yüksekliğinin 2 katıysa kaydırma çubuğu, görüntü alanının yüksekliğinin 1⁄2'sini 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 açıdan herhangi bir perspektif projeksiyonunu elde etmek için büyük olasılıkla homojen koordinatlar kullanırsınız. 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 kullanamayacağımız için w'nin ayrıntılarıyla ilgili endişelenmemize 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ı, matrix3d() işlevini kullanarak bir dönüştürme özelliğinde kendi 4x4 matrislerinizi tanımladığınızda görebilirsiniz. matrix3d, 16 bağımsız değişken alır (matris 4x4 olduğundan) ve sütunları tek tek belirtir. Böylece, rotasyonları, çevirileri vb. manuel olarak belirtmek için bu işlevi kullanabiliriz. Ancak aynı zamanda bu w koordinatını karıştırmamıza da olanak tanır.

matrix3d() özelliğini kullanabilmek için 3D bir bağlama ihtiyacımız var. Çünkü 3D bağlam olmadan perspektif bozulması 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), perspektif kapsayıcısına göre [x,y,z,w] homojen koordinatlara dönüştürün.
  • Öğenin tüm dönüşümlerini sağdan sola matrisler olarak uygulayın.
  • Perspektif öğesi kaydırılabiliyorsa bir 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. Böylece, nesneler daha geride olduğunda daha küçük görünürler. Aynı zamanda, çeviri sırasında "daha yavaş hareket ederler". 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ısının içinde bulunuyor ve 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 n bölü eksi bir 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 uygulanır. 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 parlama zamanı geldi.

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

Bu matrisin amacı w'yu eksiltmektir. 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) y koordinatımıza çıkarmak yerine eklememizdir. Aşağı kaydırırsak öğe aşağı çevrilir.

Bununla birlikte, bu matrisi örneğimize koyarsak öğ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 uygulanmamış 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. Yalnızca yer kaplayan bir boşluk öğesi ekleyebiliriz:

<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ğunda genellikle bir "parmak" ve bir "parça" bulunur ancak parça her zaman görünür değildir. 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 oranıdır. Başparmağın kapladığı dikey alanın oranı, görünen içeriğin oranına eşit olmalıdır:

baş parmak nokta stili nokta yüksekliği, kaydırma çubuğu üzerinde nokta kaydırma yüksekliğine eşittir. Bunun için yalnızca başparmak nokta stili nokta yüksekliği, kaydırma çubuğu yüksekliği ile kaydırma çubuğu yüksekliği üzerinde kaydırma çubuğu yüksekliğine eşitse kaydırma çubuğu yüksekliğine eşittir.
<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. Son kez yapıyorum, söz veriyorum.

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ırıcının her pikseli için parmağımızın bir pikselin bir kısmını hareket ettirmesini istiyoruz:

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ğınızı z ekseni boyunca ne kadar çevirmemiz gerektiğini anlamak için bu z denklemini çözebiliriz. Ancak w koordinatı hilelerimiz nedeniyle z boyunca ek bir -2px çevirmemiz gerektiğini unutmayın. Ayrıca, bir öğe dönüşümlerinin sağdan sola uygulandığını unutmayın. Yani, özel matrisinizden önceki tüm çeviriler tersine çevrilmez. Ancak özel matrismizden sonraki tüm çeviriler uygulanır. Bunu bir koda dönüştürelim.

<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ğu var. 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. Nasıl yapıldığını görmek için ayrıntılar 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ırma yaptığımız için -webkit-overflow-scrolling: touch öğesini belirtmemiz gerekir ancak bu durumda 3D düzleme uygulanır ve kaydırma efektimizin tamamı çalışmayı durdurur. Paralaks kaydırma aracındaki bu sorunu iOS Safari'yi tespit ederek ve geçici çözüm olarak position: sticky'ı kullanarak çözdük. Burada da tam olarak aynı şeyi 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. Önceden kaydırma çubuğu gizlenemez (standart olmayan sözde seçici hariç). 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üntü dışında.

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