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). Kendi JavaScript'inizi oluşturmak için JavaScript kullanabilirsiniz, ancak bu pahalıdır, düşük kalitelidir ve takılabilir. Bu makalede, kaydırma sırasında JavaScript gerektirmeyen, yalnızca bazı kurulum kodları gerektiren özel bir kaydırıcı oluşturmak için alışılmadık bazı CSS matrislerinden yararlanacağız.

Özet

Küçük şeyler sizin için önemli değil mi? Nyan kedi demosuna bakıp kitaplığı edinmek mi istiyorsunuz? Demonun kodunu GitHub depomuzda bulabilirsiniz.

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

Bir süre önce bir paralaks kaydırıcısı geliştirdik (Bu makaleyi okudunuz mu? Gerçekten iyi, zaman ayırmaya değer!). CSS 3D dönüştürmeleri kullanarak öğeleri geri iten öğeler, gerçek kaydırma hızımızdan daha yavaş hareket etti.

Özet

Paralaks kaydırma çubuğunun çalışma şeklini özetleyerek başlayalım.

Animasyonda gösterildiği gibi, öğeleri 3D uzayda Z ekseni boyunca "geri" iterek paralaks efektini elde ettik. Bir dokümanı kaydırmak, Y ekseninde etkili bir çeviridir. Yani, örneğin 100 piksel aşağı kaydırırsak her öğe 100 piksel yukarı çevrilir. Bu durum, "daha uzak" olanlar dahil olmak üzere tüm öğeler için geçerlidir. Ancak kameradan daha uzak olduğu için gözlemlenen ekranda hareketleri 100 pikselden az olacak ve istenen paralaks efekti elde edilecektir.

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. Paralaks kaydırıcıyı geliştirirken matematiği tam olarak anladık, bu yüzden tüm ayrıntıları tekrarlamayacağım.

0. Adım: Ne yapmak istiyoruz?

Kaydırma çubukları. İşte bunu geliştireceğiz. Ama ne yaptıklarını hiç düşünmüş müydünüz? Kesinlikle yapmadım. Kaydırma çubukları, mevcut içeriğin ne kadarının görünür durumda olduğunu ve okuyucu olarak sizin ne kadar ilerleme kaydettiğinizin bir göstergesidir. Ekranı aşağı kaydırdığınızda, kaydırma çubuğu da sona doğru ilerlemekte olduğunuzu 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 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 1⁄3'üne kadar ölçeklendirir. Bu şekilde bir desen görebilirsiniz. Sitede daha hızlı gezinmek için, kaydırma yerine kaydırma çubuğunu tıklayıp sürükleyebilirsiniz. Göze çarpmayan bir öğe için bu şaşırtıcı bir davranıştır. Gelin, teker teker savaşalım.

1. Adım: Ters konulma

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ü de ters çevirebilir miyiz? Kare mükemmelliğinde, özel bir kaydırma çubuğu oluşturmak için elimizden geleni yapıyoruz. Bunun nasıl çalıştığını anlamak için önce birkaç CSS 3D ile ilgili temel bilgi vermemiz gerekiyor.

Matematiksel açıdan herhangi bir perspektif projeksiyonu elde etmek için büyük olasılıkla homojen koordinatlar kullanırsınız. Bunların ne olduğunu ve neden çalıştığını ayrıntılı olarak açıklamayacağım. Ancak bunları, w adlı ek bir dördüncü koordinat içeren 3D koordinatlar gibi düşünebilirsiniz. Perspektif bozulması 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. Dolayısıyla, tüm noktalar artık 4 boyutlu vektörlerden [x, y, z, w=1] alınır ve sonuç olarak 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ğu için) birbiri ardına sütun belirtir. Dolayısıyla, 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 ve içinde yeni oluşturulan 3D alanda dönüştürebileceğimiz bazı öğeler içeren bir container'a ihtiyacımız var. Örnek:

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

Perspektif kapsayıcısı içindeki öğeler, CSS motoru tarafından şu şekilde işlenir:

  • Bir öğenin her bir 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 matris olarak uygulayın.
  • Perspektif öğesi kaydırılabiliyorsa bir kaydırma matrisi uygulayın.
  • Perspektif matrisini uygulayın.

Kaydırma matrisi, y ekseni boyunca uzanan bir çeviridir. 400 piksel kaydırırsak tüm öğelerin 400 piksel yukarı taşınması gerekir. Perspektif matrisi, noktaları 3D uzayda oldukları kadar kaybolma noktasına “ç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". Yani bir öğe geri itilirse 400 piksellik bir çeviri, öğenin ekranda yalnızca 300 piksel hareket etmesine neden olur.

Tüm ayrıntıları öğrenmek istiyorsanız CSS'nin dönüşüm oluşturma modeliyle ilgili spec okumanız gerekir. Ancak bu makalede örnek olarak, yukarıdaki algoritmayı sadeleş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üşüm matrisi, dördüncü sıra üçüncü sütundaki eksi bire bir p üzerinden dörde dörde eşittir.

İlk matris, perspektif matrisi, ikinci matris ise kaydırma matrisi. Özetlemek gerekirse: Kaydırma matrisinin işi, aşağı kaydırdığımızda bir öğenin yukarı hareket etmesini sağlamaktır. Bu nedenle, eksi işareti vardır.

Ancak kaydırma çubuğumuz için tam tersini istiyoruz. Aşağı kaydırırken öğemizin aşağı hareket etmesini istiyoruz. Şöyle bir ipucu 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 yaparız? CSS motoru, kutumuzun köşelerini homojen koordinatlara dönüştürür ve w değerini 1'e ayarlar. matrix3d() ile dikkatleri üzerinize çekin!

.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. Dolayısıyla, CSS motoru her köşeyi [x,y,z,1] biçimli bir vektöre dönüştürdüğünde, matris bunu [x,y,z,-1] biçimine dönüştürür.

Dörtx dört özdeş matrisi, dördüncü satırdaki eksi bir eksi 1 dördüncü sütunda, dördüncüsü dört x dördüncü eksi üçüncü sütunda eksi dört x dördüncü sütundaki eksi n olmak üzere dörte eşit özdeşlik matrisi, dördüncü satırdaki dördüncü sütundaki eksi bir x bir x, y, z sütunu, 1 satırının dördü x y sütunu, ikinci satırda eksi 1 sütun için dört x z eksi satır eksi satır ve 1 özdeş sütunun dördü püs sütunuyla çarpılır eksi n x dördüncü sütundaki özdeşlik matrisi ile eşittir.

Öğe dönüşüm matrisinizin etkisini göstermek için bir ara adım listeledim. Matris matematiğiyle ilgili bilginiz yoksa sorun değil. Eureka'ya göre, son satırdaki kaydırma ofseti n değerini y koordinatımıza eklemek yerine y koordinatımıza ekleriz. Aşağı kaydırırsak öğe aşağı çevrilir.

Bununla birlikte, bu matrisi örneğimize yerleştirirseniz öğe gösterilmez. Bunun nedeni, CSS spesifikasyonunun w < 0 olan tüm köşe noktalarının öğenin oluşturulmasını engellemesidir. Ayrıca, z koordinatımız şu anda 0 ve p 1 olduğu için w, -1 olur.

Neyse ki z'nin değerini seçebiliriz. w=1'i elde ettiğimizden emin olmak için z = -2'yi girmemiz gerekir.

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

Bakın, kutumuz geri döndü!

2. Adım: Harekete geçirin

Kutumuz yerinde duruyor ve herhangi bir dönüşüm olmadığı gibi görünecek. Perspektif kapsayıcısı şu anda kaydırılamıyor. Bu nedenle onu göremiyoruz. Ancak öğemizin kaydırıldığında başka yöne gideceğini biliyoruz. Kapsayıcıyı kaydıralım, değil mi? 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 de kutuyu kaydırın! Kırmızı kutu aşağı hareket eder.

3. Adım: Bir beden belirleyin

Sayfa aşağı kaydırıldığında aşağı hareket eden bir öğemiz vardır. İşin zor kısmı da tam olarak bu. Şimdi kaydırma çubuğu gibi görünecek ve 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. Başparmak yüksekliği, içeriğin ne kadarının görünür olduğuyla doğru 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 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 yüksekliği ile kaydırma 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>

Parmağın boyutu iyi görünüyor ancak çok hızlı hareket ediyor. Burada tekniğimizi paralaks kaydırıcıdan alabiliriz. Öğeyi daha geriye taşırsak kaydırma sırasında daha yavaş hareket eder. Bu boyutu büyüterek düzeltebiliriz. Peki tam olarak ne kadar geri çekmeliyiz? Haydi tahmin edelim, matematik problemi çözelim. Bu son sefer, yemin ederim.

En önemli bilgi de, ekranı tamamen aşağı kaydırdığında başparmağın alt kenarının, kaydırılabilir öğenin alt kenarıyla aynı hizada olmasını istememizdir. Diğer bir deyişle: Ekranı scroller.scrollHeight - scroller.height piksel kaydırdığımızda, baş parmağımızın scroller.height - thumb.height diline ç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 eşittir; kaydırma çubuğu nokta yüksekliği eksi kaydırma çubuğu üzerinde nokta yüksekliği
  nokta kaydırma yüksekliği eksi kaydırma çubuğu nokta yüksekliği.

Bu, bizim ölçeklendirme faktörümüz. Şimdi, paralaks kaydırma makalesinde yaptığımız gibi ölçeklendirme faktörünü z ekseni üzerinde 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ğınızı z ekseni boyunca ne kadar çevirmemiz gerektiğini anlamak için bu z denklemini çözebiliriz. Ancak w koordinatı kurcalamalarımız nedeniyle z ile birlikte bir -2px daha ç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>

Bir kaydırma çubuğumuz var! Bu yalnızca bir DOM öğesi. İstediğimiz gibi şekillendirebiliriz. Pek çok kullanıcı kaydırma çubuğuyla bu şekilde etkileşimde bulunmaya alışkın. Bu nedenle, erişilebilirlik açısından yapılması gereken önemli şeylerden biri, başparmağının tıkla ve sürükleme işlevine yanıt vermesidir. Bu blog yayınını daha da uzatmamak adına, bu bölümün ayrıntılarını açıklamayacağım. Nasıl yapıldığını görmek için ayrıntılar için kitaplık koduna göz atın.

Peki ya iOS?

Ah, eski arkadaşım 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. Hafızanızı yenilemek için paralaks oluşturma makalesine göz atın.

Tarayıcının kaydırma çubuğuna ne olacak?

Bazı sistemlerde kalıcı, yerel bir kaydırma çubuğuyla uğraşmak zorundayız. Önceden kaydırma çubuğu gizlenemez (standart olmayan sözde seçici hariç). Dolayısıyla, bunu gizlemek için (matematiksiz) birtakım bilgisayar korsanlarına başvurmamız gerekiyor. Kaydırma öğemizi, overflow-x: hidden ile bir kapsayıcıya sarar ve kaydırma öğesini kapsayıcıdan daha geniş hale getiririz. Tarayıcının yerel kaydırma çubuğu artık görüntü dışında.

Palet

Tüm bunları bir araya getirerek, Nyan kedi demomuzdaki gibi, mükemmel kare kalitesinde bir özel kaydırma çubuğu oluşturabiliriz.

Nyan kedisini göremiyorsanız bu demoyu oluştururken bulduğumuz ve bildirdiğimiz bir hatayı yaşıyorsunuz demektir (Nyan kedisinin görünmesi için baş parmağını tıklayın). Chrome, ekran dışındaki nesneleri boyamak veya canlandırmak gibi gereksiz işlerden kaçınmada çok iyidir. Kötü haber, matris kurnazlıklarımız yüzünden Chrome'un Nyan kedi gif'inin aslında ekran dışı olduğunu düşünmesine yol açıyor. Bu sorunun kısa süre içinde düzeltileceğini umuyoruz.

İşte oldu. Çok emek harcamıştım. Tüm makaleyi okuduğunuz için sizi tebrik ederim. Bu yöntem, bunun çalışmasını sağlayacak son derece karmaşık bir uygulamadır ve özelleştirilmiş kaydırma çubuğunun deneyimin önemli bir parçası olması dışında çabaya muhtemelen değmez. Ama bunun mümkün olduğunu bilmek güzel, değil mi? Özel bir kaydırma çubuğu yapmanın bu kadar zor olması, CSS tarafında yapılması gereken işler olduğunu gösterir. Endişelenmeyin! Gelecekte Houdini’nin AnimationWorklet, bunun gibi kare mükemmel kaydırma bağlantılı efektleri çok daha kolay hale getirecek.