Houdini'nin Animasyon İş Akışı

Web uygulamanızdaki animasyonları güçlendirin

Özet: Animation Worklet, cihazın yerel kare hızında çalışan zorunlu animasyonlar yazmanıza olanak tanır. Böylece, animasyonlarınız ana iş parçacığı takılmalarına karşı daha dayanıklı hale gelir ve zaman yerine kaydırmaya bağlanabilir. Animation Worklet, Chrome Canary'de ("Deneysel Web Platformu özellikleri" işaretinin arkasında) yer alıyor. Chrome 71 için kaynak denemesi planlıyoruz. Bu özelliği bugünden itibaren aşamalı iyileştirme olarak kullanmaya başlayabilirsiniz.

Başka bir Animation API mi?

Aslında hayır, bu özellik mevcut olanın bir uzantısıdır ve bunun iyi bir nedeni vardır. Baştan başlayalım. Web'de herhangi bir DOM öğesine animasyon eklemek istiyorsanız 2,5 seçeneğiniz vardır: Basit A'dan B'ye geçişler için CSS Geçişleri, potansiyel olarak döngüsel olan ve daha karmaşık zaman tabanlı animasyonlar için CSS Animasyonları ve neredeyse rastgele karmaşık animasyonlar için Web Animations API (WAAPI). WAAPI'nin destek matrisi oldukça kötü durumda olsa da iyileşme yolunda. Bu özellik kullanıma sunulana kadar polyfill kullanabilirsiniz.

Bu yöntemlerin ortak özelliği, durum bilgisiz ve zamana dayalı olmalarıdır. Ancak geliştiricilerin denediği bazı efektler ne zamana bağlıdır ne de durumsuz. Örneğin, kötü şöhretli paralaks kaydırma, adından da anlaşılacağı gibi kaydırmaya dayalıdır. Günümüzde web'de yüksek performanslı bir paralaks kaydırma aracı uygulamak şaşırtıcı derecede zordur.

Peki ya durumsuzluk? Örneğin, Android'deki Chrome adres çubuğunu düşünün. Aşağı kaydırdığınızda görünümden çıkar. Ancak sayfayı yukarı kaydırdığınız anda, sayfanın yarısına kadar aşağı inmiş olsanız bile geri gelir. Animasyon yalnızca kaydırma konumuna değil, aynı zamanda önceki kaydırma yönünüze de bağlıdır. Durum bilgili olmalıdır.

Bir diğer sorun ise kaydırma çubuklarını stilize etmektir. Bunlar, stil verilememesiyle veya en azından yeterince stil verilememesiyle bilinir. Nyan cat'i kaydırma çubuğum olarak kullanmak istersem ne yapmalıyım? Hangi tekniği seçerseniz seçin, özel bir kaydırma çubuğu oluşturmak ne performanslıdır ne de kolaydır.

Buradaki sorun, tüm bu şeylerin garip olması ve verimli bir şekilde uygulanmasının zor, hatta imkansız olmasıdır. Bunların çoğu etkinliklere ve/veya requestAnimationFrame bağlıdır. Bu durum, ekranınız 90 FPS, 120 FPS veya daha yüksek hızda çalışmaya uygun olsa bile sizi 60 FPS'de tutabilir ve değerli ana iş parçacığı çerçeve bütçenizin bir kısmını kullanabilir.

Animation Worklet, bu tür efektleri kolaylaştırmak için web'in animasyon yığını özelliklerini genişletir. Ayrıntılara geçmeden önce animasyonların temel bilgileri konusunda güncel olduğumuzdan emin olalım.

Animasyonlar ve zaman çizelgeleri hakkında temel bilgiler

WAAPI ve Animation Worklet, animasyonları ve efektleri istediğiniz şekilde düzenlemenize olanak tanımak için zaman çizelgelerinden yoğun şekilde yararlanır. Bu bölüm, zaman çizelgeleri ve bunların animasyonlarla işleyiş şekli hakkında hızlı bir hatırlatma veya giriş niteliğindedir.

Her doküman document.timeline içerir. Doküman oluşturulduğunda 0'dan başlar ve dokümanın var olmaya başladığı andan itibaren geçen milisaniyeleri sayar. Bir dokümandaki tüm animasyonlar bu zaman çizelgesine göre çalışır.

İşleri biraz daha somut hale getirmek için bu WAAPI snippet'ine göz atalım.

const animation = new Animation(
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)',
      },
      {
        transform: 'translateX(500px)',
      },
      {
        transform: 'translateY(500px)',
      },
    ],
    {
      delay: 3000,
      duration: 2000,
      iterations: 3,
    }
  ),
  document.timeline
);

animation.play();

animation.play() işlevini çağırdığımızda animasyon, başlangıç zamanı olarak zaman çizelgesinin currentTime özelliğini kullanır. Animasyonumuzda 3.000 ms'lik bir gecikme var. Bu, zaman çizelgesi `startTime` değerine ulaştığında animasyonun başlayacağı (veya "etkin" hale geleceği) anlamına geliyor.

  • 3000. After that time, the animation engine will animate the given element from the first keyframe (translateX(0)), through all intermediate keyframes (translateX(500px)) all the way to the last keyframe (translateY(500px)) in exactly 2000ms, as prescribed by thedurationoptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline'scurrentTimeisstartTime + 3000 + 1000and the last keyframe atstartTime + 3000 + 2000`. Buradaki nokta, zaman çizelgesinin animasyonumuzda nerede olduğumuzu kontrol etmesidir.

Animasyon son animasyon karesine ulaştığında ilk animasyon karesine geri döner ve animasyonun bir sonraki yinelemesini başlatır. iterations: 3 değerini belirlediğimizden beri bu işlem toplam 3 kez tekrarlanır. Animasyonun hiç durmamasını isteseydik iterations: Number.POSITIVE_INFINITY yazardık. Yukarıdaki kodun sonucu aşağıda verilmiştir.

WAAPI inanılmaz derecede güçlüdür ve bu API'de, bu makalenin kapsamını aşacak çok daha fazla özellik (ör. kolaylaştırma, başlangıç uzaklıkları, anahtar kare ağırlıkları ve doldurma davranışı) bulunur. Daha fazla bilgi edinmek isterseniz CSS Tricks'teki CSS Animasyonları hakkındaki bu makaleyi okumanızı öneririz.

Animasyon Worklet'i yazma

Zaman çizelgeleri kavramını anladığımıza göre artık Animation Worklet'e ve zaman çizelgeleriyle oynamanıza nasıl olanak tanıdığına bakabiliriz. Animation Worklet API yalnızca WAAPI'ye dayalı değildir. Aynı zamanda genişletilebilir web anlamında, WAAPI'nin nasıl çalıştığını açıklayan daha düşük düzeyli bir temeldir. Söz dizimi açısından son derece benzerdirler:

Animasyon iş akışı WAAPI
new WorkletAnimation(
  'passthrough',
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)'
      },
      {
        transform: 'translateX(500px)'
      }
    ],
    {
      duration: 2000,
      iterations: Number.POSITIVE_INFINITY
    }
  ),
  document.timeline
).play();
      
        new Animation(

        new KeyframeEffect(
        document.querySelector('#a'),
        [
        {
        transform: 'translateX(0)'
        },
        {
        transform: 'translateX(500px)'
        }
        ],
        {
        duration: 2000,
        iterations: Number.POSITIVE_INFINITY
        }
        ),
        document.timeline
        ).play();
        

Fark, bu animasyonu yönlendiren işlet'in adı olan ilk parametrededir.

Özellik algılama

Chrome, bu özelliği sunan ilk tarayıcıdır. Bu nedenle, kodunuzun yalnızca AnimationWorklet öğesinin orada olmasını beklemediğinden emin olmanız gerekir. Bu nedenle, iş parçacığını yüklemeden önce kullanıcının tarayıcısının AnimationWorklet desteği olup olmadığını basit bir kontrolle tespit etmemiz gerekir:

if ('animationWorklet' in CSS) {
  // AnimationWorklet is supported!
}

Bir işlev yükleme

Worklet'ler, Houdini görev gücü tarafından birçok yeni API'nin oluşturulmasını ve ölçeklendirilmesini kolaylaştırmak için sunulan yeni bir kavramdır. İşletlerin ayrıntılarına daha sonra biraz daha değineceğiz ancak şimdilik bunları basitçe ucuz ve hafif iş parçacıkları (çalışanlar gibi) olarak düşünebilirsiniz.

Animasyonu bildirmeden önce "passthrough" adlı bir işleti yüklediğimizden emin olmamız gerekir:

// index.html
await CSS.animationWorklet.addModule('passthrough-aw.js');
// ... WorkletAnimation initialization from above ...

// passthrough-aw.js
registerAnimator(
  'passthrough',
  class {
    animate(currentTime, effect) {
      effect.localTime = currentTime;
    }
  }
);

Neler oluyor? AnimationWorklet'in registerAnimator() çağrısını kullanarak bir sınıfı animatör olarak kaydediyoruz ve sınıfa "passthrough" adını veriyoruz. Bu, yukarıdaki WorkletAnimation() oluşturucusunda kullandığımız adla aynıdır. Kaydolma işlemi tamamlandığında addModule() tarafından döndürülen söz çözülür ve bu iş birimini kullanarak animasyon oluşturmaya başlayabiliriz.

Tarayıcının oluşturmak istediği her kare için örneğimizin animate() yöntemi çağrılır. Bu yönteme, animasyonun zaman çizelgesinin currentTime'si ve şu anda işlenmekte olan efekt iletilir. Yalnızca bir efektimiz var: KeyframeEffect. Efektin localTime değerini ayarlamak için currentTime kullanıyoruz. Bu nedenle bu animatöre "geçiş" adı veriliyor. İşlet için bu kodla, yukarıdaki WAAPI ve AnimationWorklet tam olarak aynı şekilde davranır. Bunu demoda görebilirsiniz.

Saat

animate() yöntemimizin currentTime parametresi, WorkletAnimation() oluşturucusuna ilettiğimiz zaman çizelgesinin currentTime değeridir. Önceki örnekte, bu süreyi efektin tamamına uyguladık. Ancak bu bir JavaScript kodu olduğundan ve zamanı çarpıtabildiğimizden 💫

function remap(minIn, maxIn, minOut, maxOut, v) {
  return ((v - minIn) / (maxIn - minIn)) * (maxOut - minOut) + minOut;
}
registerAnimator(
  'sin',
  class {
    animate(currentTime, effect) {
      effect.localTime = remap(
        -1,
        1,
        0,
        2000,
        Math.sin((currentTime * 2 * Math.PI) / 2000)
      );
    }
  }
);

Math.sin() değerini currentTime olarak alıp bu değeri, efektimizin tanımlandığı zaman aralığı olan [0; 2000] aralığına yeniden eşliyoruz. Artık animasyon çok farklı görünüyor. Anahtar kareler veya animasyonun seçenekleri değiştirilmedi. İşlet kodu, rastgele karmaşık olabilir ve hangi efektlerin hangi sırayla ve ne ölçüde oynatılacağını programatik olarak tanımlamanıza olanak tanır.

Seçenekler Üzerinde Seçenekler

Bir işlev öğesini yeniden kullanmak ve içindeki sayıları değiştirmek isteyebilirsiniz. Bu nedenle, WorkletAnimation oluşturucusu, worklete bir seçenekler nesnesi iletmenize olanak tanır:

registerAnimator(
  'factor',
  class {
    constructor(options = {}) {
      this.factor = options.factor || 1;
    }
    animate(currentTime, effect) {
      effect.localTime = currentTime * this.factor;
    }
  }
);

new WorkletAnimation(
  'factor',
  new KeyframeEffect(
    document.querySelector('#b'),
    [
      /* ... same keyframes as before ... */
    ],
    {
      duration: 2000,
      iterations: Number.POSITIVE_INFINITY,
    }
  ),
  document.timeline,
  {factor: 0.5}
).play();

Bu örnekte, her iki animasyon da aynı kodla ancak farklı seçeneklerle çalıştırılıyor.

Gimme your local state! (Bana yerel eyaletini söyle!)

Daha önce de belirttiğim gibi, Animation Worklet'in çözmeyi amaçladığı temel sorunlardan biri durum bilgisi olan animasyonlardır. Animasyon iş parçacıklarının durum tutmasına izin verilir. Ancak işlevletlerin temel özelliklerinden biri, farklı bir iş parçacığına taşınabilmeleri veya kaynak tasarrufu için yok edilebilmeleridir. Bu durumda durumları da yok edilir. Durum kaybını önlemek için animasyon işleti, bir durum nesnesi döndürmek üzere kullanabileceğiniz, bir işleti yok edilmeden önce çağrılan bir kanca sunar. Bu nesne, işleti yeniden oluşturulduğunda oluşturucuya iletilir. İlk oluşturma sırasında bu parametre undefined olur.

registerAnimator(
  'randomspin',
  class {
    constructor(options = {}, state = {}) {
      this.direction = state.direction || (Math.random() > 0.5 ? 1 : -1);
    }
    animate(currentTime, effect) {
      // Some math to make sure that `localTime` is always > 0.
      effect.localTime = 2000 + this.direction * (currentTime % 2000);
    }
    destroy() {
      return {
        direction: this.direction,
      };
    }
  }
);

Bu demoyu her yenilediğinizde karenin hangi yönde döneceği %50 olasılıkla belirlenir. Tarayıcı, işleti parçalayıp farklı bir iş parçacığına geçirirse oluşturma sırasında başka bir Math.random() çağrısı yapılır. Bu da yönün aniden değişmesine neden olabilir. Bunun olmaması için animasyonları rastgele seçilen yönde durum olarak döndürüyoruz ve sağlanırsa oluşturucuda kullanıyoruz.

Uzay-zaman sürekliliğine bağlanma: ScrollTimeline

Önceki bölümde gösterildiği gibi, AnimationWorklet, zaman çizelgesinin ilerletilmesinin animasyonun efektlerini nasıl etkileyeceğini programatik olarak tanımlamamıza olanak tanır. Ancak şu ana kadar zaman çizelgemiz her zaman document.timeline oldu.

ScrollTimeline, yeni olanaklar sunar ve animasyonları zaman yerine kaydırma ile kontrol etmenize olanak tanır. Bu demo için ilk "geçiş" işlevimizi yeniden kullanacağız:

new WorkletAnimation(
  'passthrough',
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)',
      },
      {
        transform: 'translateX(500px)',
      },
    ],
    {
      duration: 2000,
      fill: 'both',
    }
  ),
  new ScrollTimeline({
    scrollSource: document.querySelector('main'),
    orientation: 'vertical', // "horizontal" or "vertical".
    timeRange: 2000,
  })
).play();

document.timeline yerine yeni bir ScrollTimeline oluşturuyoruz. Tahmin edebileceğiniz gibi ScrollTimeline, işlevlet içindeki currentTime değerini ayarlamak için zamanı değil scrollSource öğesinin kaydırma konumunu kullanır. En üste (veya sola) kaydırıldığında currentTime = 0, en alta (veya sağa) kaydırıldığında ise currentTime değeri timeRange olarak ayarlanır. Bu demodaki kutuyu kaydırarak kırmızı kutunun konumunu kontrol edebilirsiniz.

Kaydırılmayan bir öğe içeren ScrollTimeline oluşturursanız zaman çizelgesinin currentTime değeri NaN olur. Bu nedenle, özellikle duyarlı tasarım söz konusu olduğunda, NaN için her zaman NaN olarak hazırlanmalısınız.currentTime Genellikle varsayılan olarak 0 değerini kullanmak mantıklıdır.

Animasyonları kaydırma konumuyla bağlamak uzun süredir istenen bir özellik olsa da CSS3D ile yapılan geçici çözümler dışında bu doğruluk düzeyinde hiçbir zaman gerçekten elde edilemedi. Animation Worklet, bu efektlerin yüksek performanslı bir şekilde basitçe uygulanmasına olanak tanır. Örneğin: Şuna benzer bir paralaks kaydırma efekti demo, kaydırmaya dayalı bir animasyonu tanımlamak için artık yalnızca birkaç satır gerektiğini gösteriyor.

Perde arkası

Worklet'ler

Worklet'ler, kapsamı yalıtılmış ve API yüzeyi çok küçük olan JavaScript bağlamlarıdır. Küçük API yüzeyi, özellikle düşük özellikli cihazlarda tarayıcının daha agresif optimizasyon yapmasına olanak tanır. Ayrıca, işlevletler belirli bir etkinlik döngüsüne bağlı değildir ancak gerektiğinde iş parçacıkları arasında taşınabilir. Bu durum, özellikle AnimationWorklet için önemlidir.

Compositor NSync

Bazı CSS özelliklerinin hızlı bir şekilde canlandırıldığını, bazılarının ise canlandırılmadığını biliyor olabilirsiniz. Bazı özelliklerin animasyonlu hale getirilmesi için yalnızca GPU'da biraz çalışma yapılması gerekirken diğerleri tarayıcıyı tüm belgeyi yeniden düzenlemeye zorlar.

Chrome'da (diğer birçok tarayıcıda olduğu gibi) birleştirici adı verilen bir işlem vardır. Bu işlemin görevi (burada çok basitleştirerek anlatıyorum) katmanları ve dokuları düzenlemek, ardından ekranı mümkün olduğunca düzenli olarak (ideal olarak ekranın güncellenebileceği kadar hızlı, genellikle 60 Hz) güncellemek için GPU'yu kullanmaktır. Hangi CSS özelliklerinin animasyon haline getirildiğine bağlı olarak, tarayıcının yalnızca birleştiricinin işini yapması gerekebilir. Diğer özelliklerin ise yalnızca ana iş parçacığının yapabileceği bir işlem olan düzeni çalıştırması gerekir. Animasyon haline getirmeyi planladığınız özelliklere bağlı olarak, animasyon işleti ya ana iş parçacığına bağlanır ya da birleştiriciyle senkronize olarak ayrı bir iş parçacığında çalışır.

Slap on the wrist

GPU, çok çekişmeli bir kaynak olduğundan genellikle birden fazla sekme arasında paylaşılan tek bir birleştirme işlemi vardır. Bir şekilde engellenirse tüm tarayıcı durur ve kullanıcı girişine yanıt vermez. Bu durumdan her ne pahasına olursa olsun kaçınılmalıdır. Peki, iş biriminiz, kare oluşturulmadan önce birleştiricinin ihtiyaç duyduğu verileri zamanında sağlayamazsa ne olur?

Bu durumda, spesifikasyona göre işlete "kaymasına" izin verilir. Birleştiricinin gerisinde kalır ve birleştiricinin, kare hızını yüksek tutmak için son karenin verilerini yeniden kullanmasına izin verilir. Bu durum görsel olarak sarsıntıya benzese de tarayıcı, kullanıcı girişine yanıt vermeye devam eder.

Sonuç

AnimationWorklet'in ve web'e sağladığı avantajların birçok yönü vardır. Bu API'nin en belirgin avantajları, animasyonlar üzerinde daha fazla kontrol sağlaması ve animasyonları yönlendirmek için yeni yöntemler sunarak web'e yeni bir görsel doğruluk düzeyi getirmesidir. Ancak API'lerin tasarımı, aynı zamanda tüm yeni özelliklere erişirken uygulamanızı takılmaya karşı daha dirençli hale getirmenize de olanak tanır.

Animation Worklet, Canary'de yer alıyor ve Chrome 71 ile bir kaynak denemesi yapmayı hedefliyoruz. Yeni web deneyimlerinizi ve iyileştirebileceğimiz noktalarla ilgili geri bildirimlerinizi merakla bekliyoruz. Aynı API'yi sağlayan ancak performans izolasyonu sunmayan bir polyfill de vardır.

CSS geçişlerinin ve CSS animasyonlarının hâlâ geçerli seçenekler olduğunu ve temel animasyonlar için çok daha basit olabileceğini unutmayın. Ancak daha gelişmiş bir animasyon kullanmanız gerekiyorsa AnimationWorklet'i kullanabilirsiniz.