Houdini'nin Animasyon İş Akışı

Web uygulamanızın animasyonlarına güç katın

Özet: Animasyon işleyici, cihazın doğal kare hızında çalışan ve pürüzsüz bir deneyim sunan zorunlu animasyonlar yazmanıza olanak tanır. Bu animasyonlar, ana iş parçacığı takılmalarına karşı daha dirençli olur ve zaman yerine kaydırmayla bağlantı kurulabilir. Animasyon Worklet, Chrome Canary'da ("Deneysel Web Platformu özellikleri" işaretinin arkasında) mevcuttur ve Chrome 71 için bir Kaynak Deneme planlıyoruz. Bu özelliği bugün aşamalı iyileştirme olarak kullanmaya başlayabilirsiniz.

Başka bir Animation API var mı?

Aslında hayır, mevcut olanın bir uzantısı. Bunun da iyi bir nedeni var. Baştan başlayalım. Günümüzde web'de herhangi bir DOM öğesini animasyonlu hale getirmek için 2, 5 seçeneğiniz vardır: Basit A'dan B'ye geçişler için CSS Geçişleri, döngüsel olabilecek, zamana dayalı daha karmaşık animasyonlar için CSS Animasyonları ve neredeyse keyfi olarak karmaşık animasyonlar için Web Animasyonları API'si (WAAPI). WAAPI'nin destek matrisi oldukça kötü görünüyor ancak iyileşme yolunda. O zamana kadar polyfill kullanılabilir.

Bu yöntemlerin ortak özelliği, durum bilgisi içermemesi ve zamana dayalı olmasıdır. Ancak geliştiricilerin denediği efektlerden bazıları zamana dayalı veya durumsuz değildir. Örneğin, kötü şöhretli paralaks kaydırma çubuğu, adından da anlaşılacağı gibi kaydırmayla çalışır. Günümüzde web'de yüksek performanslı bir paralaks kaydırma çubuğu uygulamak şaşırtıcı derecede zor.

Peki devletsizliğe ne dersiniz? Örneğin, Android'de Chrome'un adres çubuğunu düşünün. Sayfayı aşağı kaydırırsanız kaybolur. Ancak sayfanın yarısına kadar gelmiş olsanız bile yukarı kaydırdığınızda geri gelir. Animasyon yalnızca kaydırma konumuna değil, önceki kaydırma yönünüze de bağlıdır. Durum bilgisine sahiptir.

Bir diğer sorun da kaydırma çubuklarının stilidir. Bu tür cihazlar, stil açısından pek uygun değildir veya en azından yeterince uygun değildir. Kaydırma çubuğum olarak Nyan Cat kullanmak istersem ne olur? Hangi tekniği seçerseniz seçin, özel bir kaydırma çubuğu oluşturmak ne performanslı ne de kolay bir işlemdir.

Buradaki nokta, tüm bunların garip olması ve verimli bir şekilde uygulanmasının zor ya da imkansız olmasıdır. Bunların çoğu, ekranınız 90 fps, 120 fps veya daha yüksek hızlarda çalışabilse bile sizi 60 fps'de tutabilir ve değerli ana iş parçacığı çerçeve bütçenizin bir kısmını kullanabilir.requestAnimationFrame

Animasyon iş parçası, bu tür efektleri kolaylaştırmak için web'in animasyon yığınının özelliklerini genişletir. Başlamadan önce, animasyonlarla ilgili temel bilgilere göz atalım.

Animasyonlar ve zaman çizelgeleri hakkında temel bilgiler

WAAPI ve Animation Worklet, animasyon ve efektleri istediğiniz şekilde düzenleyebilmeniz için zaman çizelgelerini yoğun şekilde kullanır. Bu bölümde, zaman çizelgeleri ve animasyonların işleyiş şekliyle ilgili kısa bir hatırlatma veya giriş sunulmaktadır.

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

Konuyu biraz daha somutlaştırmak 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() çağrıldığında animasyon, başlangıç zamanı olarak zaman çizelgesinin currentTime değerini kullanır. Animasyonumuz 3000 ms gecikmeye sahiptir. Yani animasyon, zaman çizelgesi "startTime

  • 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 iterasyonunu başlatır. Bu işlem, iterations: 3 ayarlandığından toplam 3 kez tekrarlanır. Animasyonun hiç durmasını istemiyorsanız iterations: Number.POSITIVE_INFINITY yazarız. 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şabilecek akıcılık, başlangıç ofsetleri, anahtar kare ağırlıkları ve doldurma davranışı gibi daha birçok özellik vardır. Daha fazla bilgi edinmek isterseniz CSS Tricks'teki CSS Animasyonları hakkındaki bu makaleyi okumanızı öneririz.

Animasyon çalışma sayfası yazma

Zaman çizelgesi kavramını anladığımıza göre, animasyon iş parçasına ve zaman çizelgeleriyle nasıl oynamanıza olanak tanıdığına bakmaya başlayabiliriz. Animation Worklet API, yalnızca WAAPI'ye dayalı değildir. Genişletilebilir web bağlamında, WAAPI'nin işleyişini açıklayan daha düşük düzeyli bir primitiftir. Söz dizimi açısından bu iki işlev son derece benzerdir:

Animasyon İş 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 çalıştıran worklet'in adı olan ilk parametrededir.

Özellik algılama

Chrome bu özelliği kullanıma sunan ilk tarayıcı olduğundan, kodunuzun AnimationWorklet'nin mevcut olmasını beklemediğinden emin olmanız gerekir. Bu nedenle, iş parçacığı yüklenmeden önce kullanıcının tarayıcısında AnimationWorklet desteği olup olmadığını basit bir kontrolle tespit etmemiz gerekir:

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

Bir çalışma modülü yükleme

Worklet'ler, Houdini görev gücü tarafından yeni API'lerin çoğunun oluşturulmasını ve ölçeklendirilmesini kolaylaştırmak için sunulan yeni bir kavramdır. Worklet'lerin ayrıntılarını daha sonra biraz daha ele alacağız ancak basitlik açısından şimdilik bunları ucuz ve hafif iş parçacıkları (işçiler gibi) olarak düşünebilirsiniz.

Animasyonu beyan etmeden önce "passthrough" adlı bir iş parçası 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;
    }
  }
);

Burada neler oluyor? AnimationWorklet'in registerAnimator() çağrısını kullanarak bir sınıfı "passthrough" adını vererek animatör olarak kaydediyoruz. Bu, yukarıdaki WorkletAnimation() kurucusunda kullandığımız adla aynıdır. Kayıt tamamlandığında addModule() tarafından döndürülen söz çözülür ve bu iş parçacığı kullanılarak animasyon oluşturmaya başlayabiliriz.

Örneğimizin animate() yöntemi, tarayıcının oluşturmak istediği her kare için çağrılır. Bu yöntem, animasyon zaman çizelgesinin currentTime değerini ve şu anda işlenen efekti iletir. Yalnızca bir efektimiz (KeyframeEffect) var ve efektin localTime değerini ayarlamak için currentTime kullanıyoruz. Bu nedenle bu animatör "geçiş" olarak adlandırılır. Worklet için bu kodla, yukarıdaki WAAPI ve AnimationWorklet tam olarak aynı şekilde çalışır. Bunu demoda görebilirsiniz.

Saat

animate() yöntemimizin currentTime parametresi, WorkletAnimation() kurucusuna ilettiğimiz zaman çizelgesinin currentTime değeridir. Önceki örnekte, bu süreyi efekte ilettik. Ancak bu bir JavaScript kodu olduğu için zamanı bozabiliriz 💫

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)
      );
    }
  }
);

currentTime değerinin Math.sin() değerini alıp bu değeri, etkimizin tanımlandığı zaman aralığı olan [0; 2000] aralığına yeniden eşliyoruz. Anahtar kareleri veya animasyonun seçeneklerini değiştirmeden animasyon çok farklı görünüyor. Worklet kodu isteğe bağlı olarak karmaşık olabilir ve hangi efektlerin hangi sırayla ve ne ölçüde oynatıldığını programatik olarak tanımlamanıza olanak tanır.

Seçenekler üzerinde seçenekler

Bir çalışma aletini yeniden kullanmak ve sayılarını değiştirmek isteyebilirsiniz. Bu nedenle WorkletAnimation kurucusu, 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ır.

Yerel durumunuzu söyleyin.

Daha önce de belirttiğim gibi, animasyon iş parçacığının çözmeyi amaçladığı önemli sorunlardan biri durum bilgisine sahip animasyonlardır. Animasyon iş parçacıklarının durum bilgisi tutmasına izin verilir. Ancak iş parçacıklarının temel özelliklerinden biri, farklı bir iş parçacığı dizisine taşınabilmesi veya kaynakları korumak için silinebilmesidir. Bu durumda, iş parçacıklarının durumu da silinir. Durum kaybını önlemek için animasyon iş parçası, bir iş parçasının yok edilmesinden önce adlı bir kanca sunar. Bu kanca, bir durum nesnesini döndürmek için kullanılabilir. Bu nesne, iş parçası yeniden oluşturulduğunda kurucuya iletilir. İlk oluşturulduğunda 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/50'dir. Tarayıcı, iş parçacığını kaldırıp farklı bir iş parçacığına taşırsa oluşturma sırasında başka bir Math.random() çağrısı yapılır. Bu da ani bir yön değişikliğine neden olabilir. Bunun olmaması için animasyonların rastgele seçilen yönünü durum olarak döndürür ve sağlanırsa kurucuda kullanırız.

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

Önceki bölümde gösterildiği gibi AnimationWorklet, zaman çizelgesinin ilerlemesinin animasyon efektlerini nasıl etkilediğini programatik olarak tanımlamamıza olanak tanır. Ancak zaman çizelgemiz şimdiye kadar her zaman zamanı takip eden document.timeline şeklindeydi.

ScrollTimeline yeni olanaklar sunar ve animasyonlarınızı zaman yerine kaydırmayla kontrol etmenize olanak tanır. Bu demo için ilk "geçiş" iş parçacımızı 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, çalışma sayfasında currentTime değerini ayarlamak için zamanı değil scrollSource'un kaydırma konumunu kullanır. Ekran en üste (veya sola) kaydırıldığında currentTime = 0, en alta (veya sağa) kaydırıldığında ise currentTime timeRange olarak ayarlanır. Bu demo'da kutuyu kaydırarak kırmızı kutunun konumunu kontrol edebilirsiniz.

Kaydırma yapmayan bir öğeyle ScrollTimeline oluşturursanız zaman çizelgesinin currentTime değeri NaN olur. Bu nedenle, özellikle duyarlı tasarımı göz önünde bulundurarak currentTime olarak her zaman NaN'e hazır olmalısınız. Varsayılan olarak 0 değerini kullanmak genellikle mantıklıdır.

Animasyonları kaydırma konumuyla bağlama fikri uzun zamandır aranıyordu ancak bu düzeyde bir doğrulukla hiçbir zaman elde edilemedi (CSS3D ile yapılan hileli geçici çözümler dışında). Animasyon iş parçası, bu efektlerin yüksek performanslı bir şekilde basit bir şekilde uygulanmasını sağlar. Örneğin: Bu demo'daki gibi bir paralaks kaydırma efekti, kaydırma odaklı bir animasyonu tanımlamak için artık yalnızca birkaç satır gerektiğini gösteriyor.

Perde arkası

Worklet'ler

Worklet'ler, izole bir kapsama ve çok küçük bir API yüzeyine sahip JavaScript bağlamlarıdır. Küçük API yüzeyi, özellikle düşük özellikli cihazlarda tarayıcıdan daha agresif optimizasyon yapılmasına olanak tanır. Ayrıca, iş parçacıkları belirli bir etkinlik döngüsüne bağlı değildir ancak gerektiğinde iş parçacıkları arasında taşınabilir. Bu, özellikle AnimationWorklet için önemlidir.

Birleştirici NSync

Bazı CSS özelliklerinin animasyon oluşturmak için hızlı, bazılarının ise yavaş olduğunu biliyor olabilirsiniz. Bazı mülkleri animasyonlu hale getirmek için GPU'da yalnızca biraz çalışma yapılması gerekirken bazıları, tarayıcıyı belgenin tamamını 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, katmanları ve dokuları düzenlemek ve ardından ekranı mümkün olduğunca düzenli olarak (ideal olarak ekranın güncelleyebileceği en hızlı şekilde, genellikle 60 Hz) güncellemek için GPU'yu kullanmaktır. Hangi CSS özelliklerinin animasyonlu hale getirildiğine bağlı olarak, tarayıcının tek yapması gereken işleyicinin işini yapması olabilir. Diğer özelliklerin ise düzeni çalıştırması gerekir. Bu işlem yalnızca ana iş parçacığı tarafından yapılabilir. Hangi özellikleri animasyonlu hale getirmeyi planladığınıza bağlı olarak animasyon iş parçacığınız ana iş parçacığına bağlanır veya işleyiciyle senkronize olarak ayrı bir iş parçacığında çalışır.

Cüzdan

GPU, çok fazla talep gören bir kaynak olduğundan genellikle birden fazla sekme arasında paylaşılabilen tek bir kompozisyon işlemi vardır. Bir şekilde engellenen kompozitör, tarayıcının tamamını durdurur ve kullanıcı girişine yanıt vermez. Bu durumdan her ne pahasına olursa olsun kaçınılmalıdır. Peki, iş parçacığınız karenin oluşturulması için gerekli verileri zamanında sağlayamazsa ne olur?

Bu durumda, spesifikasyona göre iş parçasının "kaymasına" izin verilir. Oluşturucu geride kalır ve kare hızını yüksek tutmak için oluşturucunun son karenin verilerini yeniden kullanmasına izin verilir. Bu durum görsel olarak takılma gibi görünse de büyük fark, tarayıcı kullanıcı girişlerine hâlâ yanıt vermesidir.

Sonuç

AnimationWorklet'in ve web'e sunduğu avantajların birçok yönü vardır. Bu özelliğin en belirgin avantajları, animasyonlar üzerinde daha fazla kontrol sahibi olmak ve web'e yeni bir görsel doğruluk düzeyi getirmek için animasyonlar oluşturmanın yeni yollarıdır. Ancak API'lerin tasarımı, aynı zamanda tüm yeni özelliklere erişirken uygulamanızı takılmalara karşı daha dayanıklı hale getirmenize de olanak tanır.

Animasyon Worklet Canary'dadır ve Chrome 71 ile bir Kaynak Deneme sürümü sunmayı hedefliyoruz. Yeni web deneyimlerinizi öğrenmeyi ve neleri iyileştirebileceğimizi öğrenmeyi heyecanla bekliyoruz. Aynı API'yi sunan ancak performans izolasyonu sağlamayan 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 animasyona ihtiyacınız varsa AnimationWorklet'i kullanabilirsiniz.