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 the
durationoptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline's
currentTimeis
startTime + 3000 + 1000and the last keyframe at
startTime + 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.