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 İş Akışı, Chrome Canary'dedir ("Deneysel Web Platformu özellikleri" işaretinin arkasında) ve Chrome 71 için bir Kaynak Denemesi planlıyoruz. Bu özelliği bugün aşamalı iyileştirme olarak kullanmaya başlayabilirsiniz.
Başka bir Animation API var mı?
Aslına bakarsanız hayır, bu, sahip olduğumuz bilgilerin bir devamı ve bunun geçerli 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 bir polyfill kullanılabilir.
Bu yöntemlerin hepsinin ortak özelliği, durum bilgisi içermemesi ve zamana dayalı olmasıdır. Ancak geliştiricilerin denediği etkilerin bazıları ne zaman odaklı ne de durum bilgisizdir. Örneğin, kötü şöhretli paralaks kaydırma çubuğu, adından da anlaşılacağı gibi kaydırma ile ç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, Chrome'un Android'deki adres çubuğunu düşünün. Sayfayı aşağı kaydırırsanız kaybolur. Ancak sayfayı yukarı kaydırdığınızda, sayfanın yarısına kadar inmiş olsanız bile geri döner. Animasyon yalnızca kaydırma konumuna değil, aynı zamanda önceki kaydırma yönünüze de bağlıdır. Durum bilgisine sahip olmalıdır.
Diğer bir sorun da kaydırma çubuklarının stilini ayarlamayla ilgilidir. Bu tür cihazlar, stilize edilemez veya en azından yeterince stilize edilemez olarak bilinir. Kaydırma çubuğum olarak nyan kedi kullanmak istersem ne olur? Hangi tekniği seçerseniz seçin, özel bir kaydırma çubuğu oluşturmak etkili veya kolay değildir.
Buradaki nokta, tüm bunların garip olduğu ve verimli bir şekilde uygulanmasının zor olduğudur. 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 bilgileri edinelim.
Animasyonlar ve zaman çizelgeleri hakkında bir yardım
WAAPI ve Animation Worklet, animasyon ve efektleri istediğiniz şekilde düzenleyebilmeniz için zaman çizelgelerini yoğun şekilde kullanır. Bu bölüm, zaman çizelgeleri ve bunların animasyonlarla nasıl çalıştığına dair kısa bir bilgi tazeleme veya giriş niteliğindedir.
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". Önemli olan, zaman çizelgesinin animasyonumuzdaki yeri kontrol ettiğidir.
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 son derece güçlüdür ve bu API'de yumuşak geçiş, başlangıç ofsetleri, animasyon karesi ağırlıklandırmaları ve doldurma davranışı gibi bu makalenin kapsamını genişletecek daha pek çok özellik vardır. Daha fazla bilgi edinmek istiyorsanız CSS Püf Noktalarında CSS Animasyonları ile ilgili 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 dil birbirine çok benzer:
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
'ün mevcut olmasını beklemediğinden emin olmanız gerekir. Bu nedenle, iş uygulamasını yüklemeden önce basit bir kontrolle kullanıcının tarayıcısının AnimationWorklet
desteğini destekleyip desteklemediğini 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;
}
}
);
Neler oluyor? AnimasyonWorklet'in registerAnimator()
çağrısıyla bir sınıfı animatör olarak kaydediyoruz. Bu çağrıya "geçiş" adı veriliyor.
Bu, yukarıdaki WorkletAnimation()
kurucusunda kullandığımız adla aynıdır. Kayıt tamamlandıktan sonra addModule()
tarafından verilen söz yerine getirilir ve bu işleti kullanarak animasyonlar 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 etkiye aktardık. 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. Animasyon karelerini veya animasyon seçeneklerini değiştirmeden animasyon çok farklı görünür. Worklet kodu istediğiniz kadar 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 oluşturucusu, iş uygulamasına bir seçenek nesnesi aktarmanıza izin verir:
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 eyaletinizi söyleyin.
Daha önce de belirttiğim gibi, animasyon iş parçası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 hatta kaynak tasarrufu sağlamak 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. İş uygulaması yeniden oluşturulduğunda bu nesne oluşturucuya aktarılır. İ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 döneceği 50/50 şansa sahip olursunuz. 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 şimdiye kadar zaman çizelgemiz, zamanı takip eden document.timeline
oldu.
ScrollTimeline
, yeni olanaklar sunar ve zaman yerine kaydırmayı
kullanarak animasyonları yönlendirmenize olanak tanır. Bu demo için ilk "geçiş" iş programımızı tekrar 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
öğesini iletmek 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ırmayan bir öğeye sahip bir 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 teknoloji 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 pek çok tarayıcıda olduğu gibi), birleştirici adı verilen bir sürece sahibiz. İşi burada çok basitleştiriyorum; katmanları ve dokuları düzenlemek ve ardından ekranı olabildiğince düzenli olarak, ideal olarak ekranın güncellenebileceği hızda (tipik olarak 60 Hz) güncellemek için GPU'yu kullanı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ığında 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. Ne pahasına olursa olsun bu durumdan 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 öğrenmek ve neleri iyileştirebileceğimizi öğrenmek için sabırsızlanıyoruz. 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 havalı olmak isterseniz AnimationWorklet yanınızda!