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 çoğunlukla kaydırma çubuklarının web'de pek hoş olmayan kalan parçalardan biri olmasıdır (buna bakıyorum, tarih seçici). Kendi URL'nizi oluşturmak için JavaScript'i kullanabilirsiniz, ancak bu pahalıdır, düşük kalitelidir ve duraklamaya neden olabilir. Bu makalede, kaydırma sırasında JavaScript gerektirmeyen, yalnızca birkaç kurulum kodu gerektiren özel bir kaydırıcı oluşturmak için bazı alışılmadık CSS matrislerinden yararlanacağız.

Özet

Küçük şeyler umursamıyor mu? Sadece Nyan Cat demosuna bakıp kitaplığı edinmek mi istiyorsunuz? Demonun kodunu GitHub depomuzda bulabilirsiniz.

LAM;WRA (Uzun ve matematiksel; yine de okunacaktır)

Bir süre önce paralaks kaydırıcısı geliştirdik (Bu makaleyi okudunuz mu? Gerçekten çok iyi, ayırdığınız zamana değer.) Öğeler, CSS 3D dönüşümlerini kullanarak geri itildiğinde öğeler gerçek kaydırma hızımızdan daha yavaş hareket etti.

Özet

Paralaks kaydırıcının nasıl çalıştığıyla ilgili bir özetle başlayalım.

Animasyonda gösterildiği gibi, paralaks efektini 3D uzayda öğeleri Z ekseni boyunca "geri" iterek elde ettik. Bir dokümanı kaydırmak, aslında Y ekseni boyunca yapılan bir çeviridir. Dolayısıyla, 100 piksel gibi aşağı kaydırırsak her öğe 100 piksel yukarı çevrilir. Bu, "daha gerideki"ler dahil tüm öğeler için geçerlidir. Ancak, kameradan daha uzak oldukları için ekrandaki gözlemlenen hareketleri 100 pikselden az olur ve istenen paralaks efektini sağlar.

Elbette bir öğe uzayda geri taşındığında daha küçük görünür. Bu durumu, öğenin ölçeğini yeniden artırarak düzeltiyoruz. Paralaks kaydırıcısını oluştururken tam matematiği çözdük, bu yüzden tüm ayrıntıları tekrar etmeyeceğim.

0. Adım: Ne yapmak istiyoruz?

Kaydırma çubukları. Biz de bunu oluşturacağız. Peki onların ne yaptığını hiç düşündünüz mü? Ben yapmadım. Kaydırma çubukları, mevcut içeriğin ne kadar görünür olduğunun ve okuyucu olarak sizin ne kadar ilerleme kaydettiğinizin bir göstergesidir. Ekranı aşağı kaydırırsanız kaydırma çubuğu da sona doğru ilerlemekte olduğunuzu gösterir. Tüm içerikler görüntü alanına sığarsa kaydırma çubuğu genellikle gizlenir. İçerik, görüntü alanının 2 katı yüksekliğindeyse kaydırma çubuğu, görüntü alanı 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 1/3'üne ölçekler. Kalıbı görüyorsunuz. Sitede daha hızlı hareket etmek için, kaydırma yerine kaydırma çubuğunu tıklayıp sürükleyebilirsiniz. Böyle göze çarpmayan bir öğe için bu şaşırtıcı bir davranış miktarıdır. Birlikte savaşalım.

1. Adım: Adaptörü ters çevirme

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ü de ters çevirebilir miyiz? Mükemmel, özel bir kaydırma çubuğu oluşturmak için bunu yapabiliyoruz. Bunun nasıl çalıştığını anlamak için öncelikle CSS 3D ile ilgili birkaç temele değinmemiz gerekir.

Matematiksel olarak herhangi bir perspektif projeksiyonu elde etmek için muhtemelen homojen koordinatlar kullanırsınız. Bunların ne olduğunu ve neden çalıştıklarını ayrıntılı olarak açıklamayacağım, ancak bunları w adlı ek bir dördüncü koordinatla 3D koordinatlar gibi düşünebilirsiniz. Perspektif bozulmasını istemiyorsanız bu koordinat 1 olmalıdır. 1'den başka bir değer kullanmayacağımızdan w öğesinin ayrıntılarıyla ilgili endişe duymamıza gerek yoktur. Bu nedenle, tüm noktalar artık [x, y, z, w=1] 4 boyutlu vektörlere bağlı. Dolayısıyla matrislerin de 4x4 olması gerekiyor.

CSS'nin arka planda homojen koordinatlar kullandığını görebileceğiniz durumlardan biri, matrix3d() işlevini kullanarak kendi 4x4 matrislerinizi bir dönüşüm özelliğinde tanımladığınız zamanlardır. matrix3d, art arda bir sütun belirterek 16 bağımsız değişken alır (çünkü matris 4x4). Böylece, döndürmeleri, çevirileri vb. manuel olarak belirtmek için bu işlevi kullanabiliriz. Ancak, bu w koordinatını karıştırmamıza da olanak tanır.

matrix3d() bileşenini kullanabilmek için 3D bağlama ihtiyacımız vardır. Çünkü, 3D bağlam olmadan perspektif bozulması ve homojen koordinatlara ihtiyaç duyulmaz. 3D bağlam oluşturmak için perspective içeren bir kapsayıcıya ve yeni oluşturulan 3D alanda dönüştürebileceğimiz bazı öğelere ihtiyacımız vardır. Örnek:

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

Perspektif kapsayıcısının içindeki öğeler, CSS motoru tarafından aşağıdaki gibi işlenir:

  • Bir öğenin her bir köşesini (köşe) perspektif kapsayıcısına göre homojen koordinatlara [x,y,z,w] dönüştürün.
  • Öğenin tüm dönüşümlerini sağdan sola matris olarak uygulayın.
  • Perspektif öğesi kaydırılabiliyorsa kaydırma matrisi uygulayın.
  • Perspektif matrisini uygulayın.

Kaydırma matrisi, y ekseni üzerindeki bir çeviridir. 400 piksel aşağı kaydırırsak tüm öğelerin 400 piksel yukarı taşınması gerekir. Perspektif matrisi, noktaları 3D uzayda daha da geriye, kayma noktasına yaklaştıran bir matristir. Bu şekilde, öğeler uzaklaşıldığında küçülür ve çeviri sırasında öğeler "daha yavaş hareket eder" hale gelir. Dolayısıyla, bir öğe geri itilirse 400 piksellik bir çeviri, öğenin ekranda yalnızca 300 piksel hareket etmesine neden olur.

Tüm ayrıntıları bilmek istiyorsanız CSS'nin dönüşüm oluşturma modeliyle ilgili spec okumanız gerekir. Ancak bu makale adına, yukarıdaki algoritmayı basitleştirdik.

Kutumuz, perspective özelliği için p değerine sahip bir perspektif kapsayıcısının içindedir 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üşüm matrisi, dördüncü sıra üçüncü sütunda eksi bir bölü p olan özdeşlik matrisi dörde eşit

Birinci matris perspektif matrisi, ikinci matris ise kaydırma matrisidir. Özetlemek gerekirse, kaydırma matrisinin işi, aşağı kaydırdığımızda bir öğenin yukarı hareket etmesini sağlamaktır. Bu yüzden eksi işareti.

Ancak kaydırma çubuğumuz için bunun tersini istiyoruz. Aşağı kaydırırken öğemizin aşağı hareket etmesini istiyoruz. Bu yöntemi şu şekilde kullanabiliriz: Kutumuzun köşelerinin w koordinatını tersine çevirmek. 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() için öne çıkma zamanı.

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

Bu matrisin w değerini reddetmekten başka işlevi yoktur. 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çimine dönüştürür.

dördüncü satır dördüncü satır dördüncü sütunda eksi bir, dördüncü satır x, y, z, 1 dördüncü satır x, y, z, 1, dördüncü satır, dördüncü satır, p x p'nin çıkarılmasıyla elde edilen özdeşlik matrisi

Öğe dönüşüm matrisimizin etkisini göstermek için aradaki bir adımı listeledim. Matris matematiğiyle ilgili bilginiz yoksa sorun değil. Son satırda, kaydırma ofseti n sayısını y koordinatımıza çıkarmak yerine y koordinatına ekleriz. Aşağı kaydırırsak öğe aşağı çevrilir.

Bununla birlikte, bu matrisi sadece örneğimize yerleştirirsek öğe görüntülenmez. Bunun nedeni, CSS spesifikasyonunun, w < 0 olan herhangi bir köşe noktasının öğenin oluşturulmasını engellemesini gerektirmesidir. Z koordinatımız şu anda 0 olduğundan ve p 1 olduğundan w, -1 olacaktır.

Neyse ki z değerini seçebiliriz. w=1 sonucunun elde edildiğinden emin olmak 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);
}

Bakın, kutusumuz geri döndü!

2. Adım: Hareket etmesini sağlayın

Şimdi kutumuz buradadır ve herhangi bir dönüşüm olmadan olacağı şekilde görünür. Şu anda perspektif kapsayıcısı kaydırılamadığı için görülmüyor, ancak kaydırıldığında öğemizin başka yöne gideceğini biliyoruz. Şimdi kapsayıcıyı kaydıralım, olur mu? Sadece 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 de kutuyu kaydırın! Kırmızı kutu aşağı hareket eder.

3. Adım: Bir beden verin

Sayfa aşağı kaydırıldığında aşağı hareket eden bir öğemiz vardır. Gerçekten de işin zor kısmı bu. Şimdi, kaydırma çubuğu gibi görünmesini sağlamamız ve biraz daha etkileşimli hale getirmemiz gerekiyor.

Kaydırma çubuğu genellikle bir "parmak" ve bir "iz"den oluşur, ancak parça her zaman görünür değildir. Başparmağın yüksekliği, içeriğin ne kadarının görünür olduğuyla doğrudan orantı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 olan kısmıdır. Baş parmak kapağının dikey alanın oranı, görünen içeriğin oranına eşit olmalıdır:

kaydırmalı nokta stili nokta yüksekliği, kaydırma öğesi yüksekliği üzerinde kaydırma öğesi yüksekliğine eşittir. Bunu sağlamak için yalnızca baş parmak nokta stili nokta yüksekliğinin, kaydırma çubuğu yüksekliği çarpı kaydırma öğesi yüksekliğinin kaydırma çubuğu nokta kaydırma yüksekliğine eşit olması gerekir.
<script>
    // …
    thumb.style.height =
    scrollerHeight * scrollerHeight / scroller.scrollHeight + 'px';
    // Accommodate for native scrollbars
    thumb.style.right =
    (scroller.clientWidth - scroller.getBoundingClientRect().width) + 'px';
</script>

Başparmağın boyutu iyi görünüyor, ancak çok hızlı hareket ediyor. Burada tekniğimizi paralaks kaydırıcısından alabiliriz. Öğeyi uzaklaştırırsak kaydırma sırasında daha yavaş hareket eder. Boyutu büyüterek düzeltebiliriz. Peki bunu tam olarak ne kadar düşürmeliyiz? Haydi, tahmin edeceğiniz gibi, matematik yapalım. Sanırım bu son defa.

En önemli bilgi de, başparmağın tamamen aşağı kaydırıldığında kaydırılabilir öğenin alt kenarıyla aynı hizada olmasını istediğimizdir. Başka bir deyişle: scroller.scrollHeight - scroller.height piksel kaydırdıysak başparmağımızın scroller.height - thumb.height tarafından çevrilmesini isteriz. Her kaydırıcı pikseli için, başparmağımızın bir pikselin bir kısmını hareket ettirmesini istiyoruz:

Kaydırma noktası yüksekliği (kaydırıcı nokta yüksekliği) eksi kaydırıcı nokta kaydırma yüksekliği (kaydırıcı nokta yüksekliği) üzerinden başparmak nokta yüksekliğine eşittir.

Ölçeklendirme faktörümüz budur. Şimdi ölçeklendirme faktörünü, paralaks kaydırma makalesinde yaptığımız gibi z ekseni boyunca bir çeviriye dönüştürmemiz gerekiyor. 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 çevirmemiz gerektiğini bulmak için bu z denklemini çözebiliriz. Ancak, w koordinatı kurnazlarımızdan dolayı, z boyunca ek bir -2px çevirmemiz gerektiğini unutmayın. Ayrıca, bir öğenin dönüşümlerinin sağdan sola doğru uygulandığını unutmayın. Diğer bir deyişle, özel matrisimizden önceki tüm çeviriler ters çevirmez, ancak özel matrisimizden sonraki tüm çeviriler ters çevrilmez! Şimdi 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>

Bir kaydırma çubuğumuz var! Ayrıca, yalnızca istediğimiz gibi biçimlendirebileceğimiz bir DOM öğesi. Birçok kullanıcı kaydırma çubuğuyla bu şekilde etkileşimde bulunmaya alışkın olduğundan, erişilebilirlik açısından yapılması gereken önemli bir şey, başparmağın tıkla ve sürükle işlemine yanıt vermesini sağlamaktır. Bu blog yayınını daha da uzatmamak adına, ilgili kısımla ilgili ayrıntıları açıklayamayacağım. Nasıl yapıldığını görmek için ayrıntılı bilgi için kitaplık koduna göz atın.

Peki ya iOS?

Eski arkadaşım iOS Safari. Paralaks kaydırmada olduğu gibi burada da bir sorunla karşı karşıyayız. Bir öğeyi kaydırdığımız için -webkit-overflow-scrolling: touch değerini belirtmemiz gerekir. Ancak bu, 3D düzleştirmeye neden olur ve kaydırma efektimizin tamamı çalışmayı durdurur. Paralaks kaydırma aracındaki bu sorunu, iOS Safari'yi algılayıp geçici çözüm olarak position: sticky kullanarak çözdük ve burada da tam olarak aynı şeyi yapacağız. Hafızanızı tazelemek için paralaks makalesine göz atın.

Peki ya tarayıcı kaydırma çubuğu?

Bazı sistemlerde kalıcı, yerel bir kaydırma çubuğuyla uğraşmak zorunda kalırız. Geçmişte, kaydırma çubuğu gizlenemez (standart olmayan sözde seçici hariç). Bunu gizlemek için bilgisayar korsanlığına (matematik içermeyen) çözüm bulmamız gerekiyor. Kaydırma öğemizi overflow-x: hidden ile bir kapsayıcı içine sarar ve kaydırma öğesini container'dan daha geniş hale getiririz. Tarayıcının yerel kaydırma çubuğu artık görünmüyor.

Son

Hepsini bir araya getirdiğimizde artık Nyan kedi demosundaki gibi mükemmel bir kare çubuğu oluşturabiliyoruz.

Nyan kedisini göremiyorsanız bu demoyu oluştururken bulduğumuz ve bildirdiğimiz bir hatayla karşılaşıyorsunuz demektir (Nyan kedisinin görünmesini sağlamak için başparmağı tıklayın). Chrome, ekran dışındaki şeyleri boyamak veya canlandırmak gibi gereksiz işlerden kaçınmada gerçekten başarılıdır. Kötü haber ise matris şakalarımız Chrome'un Nyan kedisi GIF'inin aslında ekran dışı olduğunu düşünmesini sağlıyor. Bu durumun yakında düzeltileceğini umuyoruz.

İşte oldu. Çok çalıştık. Tüm bunları okuduğunuz için sizi tebrik ediyorum. Bunu başarmak, gerçekten zorlu bir işlemdir. Özelleştirilmiş kaydırma çubuğunun bu deneyimin önemli bir parçası olmadığı durumlar haricinde, büyük olasılıkla çabanıza değmez. Ancak bunun mümkün olduğunu bilmek güzel. Özel bir kaydırma çubuğu yapmanın bu kadar zor olması, CSS tarafında yapılması gereken işler olduğunu gösterir. Ama korkmayın! Gelecekte Houdini'nin AnimationWorklet hizmeti, kaydırmayla kusursuz bir şekilde bağlantılı efektleri çok daha kolay hale getirecek.