Ders 16 / 23
Animasyon Başarımı
Hareketin çizim hattındaki karşılığı; hangi özellik değişiminin yerleşim, boyama ve bileşim aşamalarını yeniden çalıştırdığı, bileşik katmanın ne olduğu, zorlanmış yerleşim hesabı ve kare bütçesi.
İçindekiler
Önceki üç ders hareketi tanımladı: dönüşüm geometriyi verdi, geçiş ve anahtar kareler zamanı verdi. Bedelinden hiç söz edilmedi.
Bir kartı sağa kaydırmanın iki yolu var: left değerini artırmak ya da transform
listesine öteleme yazmak. Ekranda ikisi aynı görünür. Tarayıcı için ikisi aynı iş değildir.
Bu ders farkın nereden geldiğini kural olarak kurar.
Çizim Hattı
Bir sayfanın ekrana gelmesi sıralı aşamalardan geçer. Hareket açısından dördü önemlidir:
- Stil hesabı. Hangi bildirimin hangi ögeye uygulandığı çözülür; basamaklama ve kalıtım burada işler. Her değişim bu aşamayı çalıştırır.
- Yerleşim. Her kutunun ölçüsü ve yeri hesaplanır. Bir kutunun genişliği değişirse kardeşleri, ana ögesi ve sonraki akış etkilenebilir; hesap yerel kalmayabilir.
- Boyama. Kutuların içeriği — renk, kenarlık, gölge, metin — piksellere çevrilir.
- Bileşim. Boyanmış yüzeyler sıralanır, dönüştürülür ve ekrana yerleştirilir.
Aşamalar sıralıdır: yerleşim yeniden çalışırsa boyama ve bileşim de çalışır. Ama tersi geçerli değildir; bileşim tek başına çalışabilir.
Hangi Özellik Hangi Aşamayı Tetikler
// basarim.mjs — ozellik degisiminin cizim hattinda tetikledigi asamalar ve zorlanmis duzen sayimi const HAT = ["stil", "duzen", "boyama", "bilesim"]; // Kural: bir ozellik hangi asamadan itibaren yeniden calisir? const BASLANGIC = { width: "duzen", height: "duzen", "margin-block-start": "duzen", padding: "duzen", "border-width": "duzen", "font-size": "duzen", "line-height": "duzen", top: "duzen", left: "duzen", display: "duzen", "flex-basis": "duzen", color: "boyama", "background-color": "boyama", "background-image": "boyama", "border-radius": "boyama", "box-shadow": "boyama", outline: "boyama", visibility: "boyama", "text-decoration": "boyama", transform: "bilesim", opacity: "bilesim", }; console.log("--- ozellik degisimi hangi asamadan itibaren yeniden calisir ---"); console.log("ozellik".padEnd(20) + HAT.map((a) => a.padStart(10)).join("") + " asama sayisi"); for (const [ozellik, bas] of Object.entries(BASLANGIC)) { const i = HAT.indexOf(bas); const isaret = HAT.map((_, k) => (k === 0 || k >= i ? "x" : "-").padStart(10)).join(""); console.log(ozellik.padEnd(20) + isaret + String(HAT.length - i + (i > 0 ? 1 : 0)).padStart(15)); } console.log("\n--- ayni gorsel sonucu veren iki yazim ---"); for (const [a, b] of [["left", "transform"], ["width", "transform"], ["visibility", "opacity"]]) { const ia = HAT.indexOf(BASLANGIC[a]), ib = HAT.indexOf(BASLANGIC[b]); console.log( `${a.padEnd(12)} -> ${HAT.slice(ia).join(" > ").padEnd(26)}` + `${b.padEnd(12)} -> ${HAT.slice(ib).join(" > ")}`, ); } // --- zorlanmis duzen (forced synchronous layout) sayimi --- // Model: yazma islemi duzeni gecersiz kilar; olcu okuma islemi gecersiz duzeni // okumak icin duzeni yeniden hesaplatir. Kare sonunda bir hesap zaten yapilir. function duzenSayimi(islemler) { let kirli = false, zorlanmis = 0; const iz = []; for (const [tip, ad] of islemler) { if (tip === "yaz") { kirli = true; iz.push(`yaz ${ad}`); } else { if (kirli) { zorlanmis++; kirli = false; iz.push(`oku ${ad} -> DUZEN HESABI`); } else iz.push(`oku ${ad}`); } } return { zorlanmis, kareSonu: kirli ? 1 : 0, iz }; } const KARISIK = [ ["oku", "genislik(kart 1)"], ["yaz", "width(kart 1)"], ["oku", "genislik(kart 2)"], ["yaz", "width(kart 2)"], ["oku", "genislik(kart 3)"], ["yaz", "width(kart 3)"], ]; const AYRIK = [ ["oku", "genislik(kart 1)"], ["oku", "genislik(kart 2)"], ["oku", "genislik(kart 3)"], ["yaz", "width(kart 1)"], ["yaz", "width(kart 2)"], ["yaz", "width(kart 3)"], ]; for (const [ad, islemler] of [["okuma ve yazma ic ice", KARISIK], ["once okuma, sonra yazma", AYRIK]]) { const s = duzenSayimi(islemler); console.log(`\n--- ${ad} ---`); for (const satir of s.iz) console.log(" " + satir); console.log(` zorlanmis duzen hesabi: ${s.zorlanmis}, kare sonundaki hesap: ${s.kareSonu}`); } console.log("\n--- kare butcesi (yenileme hizindan aritmetik) ---"); for (const hz of [60, 90, 120, 144]) { console.log(`${String(hz).padStart(3)} kare/s -> kare basina ${(1000 / hz).toFixed(2)} ms`); }
--- ozellik degisimi hangi asamadan itibaren yeniden calisir --- ozellik stil duzen boyama bilesim asama sayisi width x x x x 4 height x x x x 4 margin-block-start x x x x 4 padding x x x x 4 border-width x x x x 4 font-size x x x x 4 line-height x x x x 4 top x x x x 4 left x x x x 4 display x x x x 4 flex-basis x x x x 4 color x - x x 3 background-color x - x x 3 background-image x - x x 3 border-radius x - x x 3 box-shadow x - x x 3 outline x - x x 3 visibility x - x x 3 text-decoration x - x x 3 transform x - - x 2 opacity x - - x 2 --- ayni gorsel sonucu veren iki yazim --- left -> duzen > boyama > bilesim transform -> bilesim width -> duzen > boyama > bilesim transform -> bilesim visibility -> boyama > bilesim opacity -> bilesim --- okuma ve yazma ic ice --- oku genislik(kart 1) yaz width(kart 1) oku genislik(kart 2) -> DUZEN HESABI yaz width(kart 2) oku genislik(kart 3) -> DUZEN HESABI yaz width(kart 3) zorlanmis duzen hesabi: 2, kare sonundaki hesap: 1 --- once okuma, sonra yazma --- oku genislik(kart 1) oku genislik(kart 2) oku genislik(kart 3) yaz width(kart 1) yaz width(kart 2) yaz width(kart 3) zorlanmis duzen hesabi: 0, kare sonundaki hesap: 1 --- kare butcesi (yenileme hizindan aritmetik) --- 60 kare/s -> kare basina 16.67 ms 90 kare/s -> kare basina 11.11 ms 120 kare/s -> kare basina 8.33 ms 144 kare/s -> kare basina 6.94 ms
Tablo bir ölçüm değil, bir kural kümesidir: özellikler tanımlarına göre üç gruba ayrılır. Geometriye dokunanlar yerleşimden itibaren, yalnız görünüşe dokunanlar boyamadan itibaren, kutunun ne ölçüsünü ne içeriğini değiştirenler ise yalnız bileşimden itibaren çalışır.
Sınıflandırmanın kenarında birkaç özellik vardır — süzgeç ve karışım kipleri gibi — ve bunların hangi aşamadan başladığı çizim gerçekleştirimine göre değişir. Kesin bilgi tarayıcının geliştirici araçlarındaki çizim kaydından okunur; kural olarak ezberlenmemelidir.
Aynı Görünüm, Farklı Maliyet
Çıktının ikinci bloğu üç çifti karşılaştırıyor. left ile ötelemek üç aşama çalıştırır,
transform ile ötelemek bir aşama. visibility ile gizlemek boyamayı yeniden çalıştırır,
opacity ile soldurmak çalıştırmaz.
Buradan hareket yazımının en pratik kuralı çıkar: animasyonlar dönüşüm ve saydamlık
üzerinden yazılır. Bir kutunun büyümesi gerekiyorsa width yerine scale, yer
değiştirmesi gerekiyorsa left yerine translate kullanılır.
Kuralın bir bedeli vardır. scale kutunun içeriğini de ölçekler; metin, kutu büyürken
büyür. width değiştirmek ise metni yeniden satırlara böler. İkisi aynı görsel sonucu
vermez ve seçim, istenen sonucun hangisi olduğuna bağlıdır.
Bileşik Katman
Dönüşümün ucuz olmasının nedeni, uygulandığı yerdir. Boyanmış bir yüzey bir kez üretilir; dönüşüm ve saydamlık o yüzeye bileşim aşamasında uygulanır. Yüzey yeniden boyanmaz, yalnızca farklı yerleştirilir.
Bunun için ögenin kendi yüzeyine, yani bileşik katmana (compositing layer) ayrılmış
olması gerekir. Katman ayrımını tarayıcı kendi karar vererek yapar; will-change bildirimi
bu kararı önceden bildirmenin yoludur:
.olcum-kartlari > .kart:hover { will-change: transform; }
will-change bir garanti değil, bir ipucudur. Üç kuralı vardır. Katman ayırmak bellek
harcar; her ögeye yazmak yararı tersine çevirir. Bildirim, hareket başlamadan kısa süre
önce açılıp bittikten sonra kaldırılmalıdır — kalıcı yazılan bir ipucu kalıcı bir maliyettir.
Ve will-change de yığılma bağlamı kurar; ilk dersteki uyarı burada da geçerlidir.
Zorlanmış Yerleşim Hesabı
Çıktının üçüncü ve dördüncü blokları, betikle sürülen bir hareketin sık düştüğü tuzağı bir model üzerinde sayıyor. Model şudur: bir yazma işlemi yerleşimi geçersiz kılar; sonraki bir ölçü okuma işlemi güncel değeri vermek zorunda olduğu için yerleşimi hemen yeniden hesaplatır. Buna zorlanmış yerleşim hesabı (forced synchronous layout) denir.
Okuma ve yazma iç içe yapıldığında model iki ek hesap sayıyor. Aynı işlemler önce bütün okumalar, sonra bütün yazmalar biçiminde sıralandığında ek hesap kalmıyor; kare sonunda zaten yapılacak olan tek hesap yeterli oluyor.
Kural yazımdan bağımsızdır: ölçü okuyan işlemler bir öbekte, ölçü değiştiren işlemler ayrı bir öbekte toplanır. Öbek sayısı arttıkça hesap sayısı da artar.
Kare Bütçesi
Son blok aritmetik bir dönüşümdür: saniyede 60 kare üreten bir ekranda kare başına düşen süre milisaniyedir. Bu süre, bir karenin bütün aşamalarının — betik, stil, yerleşim, boyama, bileşim — paylaştığı bütçedir.
Bütçe aşıldığında kare atlanır ve hareket takılır. Bu yüzden hareket yazarken sorulacak soru “ne kadar sürüyor” değil, “kare başına hangi aşamalar çalışıyor”dur. Aşama sayısını düşürmek, süreyi tahmin etmeye çalışmaktan daha güvenilir bir yoldur.
Yenileme hızı ekrana göre değişir; aynı animasyon farklı bütçelerle çalışır. Kare sayısına bağlı yazılmış bir hareket hızlı ekranda hızlanır. Süre üzerinden yazılan bildirimler bu sorunu taşımaz.
Boyama Alanını Sınırlamak
Bir ögenin yeniden boyanması komşularını da kapsayabilir; boyama alanı ögeden büyük olabilir.
contain bildirimi bu yayılmayı sınırlar: contain: layout bir ögenin yerleşim hesabının dışarı
taşmayacağını, contain: paint boyamanın kutunun dışına çıkmayacağını bildirir.
Bu bildirim bir söz verir; sözü tutmayan bir içerik bozulur. paint değeri yazılan bir
kutudan taşan gölge ya da açılır menü kırpılır. Bildirim, kutunun gerçekten kendi kendine
yeter olduğu yerlerde yazılır.
İstasyon Sayfasında Ölçüm
/* hareket.css — 4. adım: hareketin çizim maliyetini sınırlamak */ .olcum-kartlari > .kart { transform-origin: center bottom; transition: transform 240ms ease-out, box-shadow 240ms ease-out; contain: layout paint; } .olcum-kartlari > .kart:hover, .olcum-kartlari > .kart:focus-within { transform: translateY(-8px) scale(1.04); transition-duration: 160ms; } .istasyon-durumu[data-durum="bekliyor"] .gosterge { animation: nabiz 1400ms 200ms infinite alternate; will-change: transform, opacity; }
Kart hareketi yalnız dönüşüm üzerinden yazıldı; contain bildirimi kartın yerleşim ve boyama
etkisini kendi kutusuyla sınırlıyor. Gösterge sürekli hareket ettiği için will-change
bildirimi orada kalıcı yazıldı — kalıcı hareket, kalıcı katmanı haklı çıkaran ender
durumlardan biridir.
Gölge geçişi bir ödünleşimdir: box-shadow boyamayı yeniden çalıştırır. Aynı görünüm,
kartın arkasına konan ayrı bir sözde elemanın saydamlığıyla da elde edilebilir ve o yazım
yalnız bileşim çalıştırır.
Özet
- Çizim hattı sıralıdır: yerleşim çalışırsa boyama ve bileşim de çalışır, ama bileşim tek başına çalışabilir.
- Geometriye dokunan özellikler yerleşimden, yalnız görünüşe dokunanlar boyamadan, dönüşüm ve saydamlık ise bileşimden itibaren çalışır.
- Hareket dönüşüm ve saydamlık üzerinden yazıldığında aşama sayısı düşer; ölçü değiştiren yazımlar aynı görünümü daha çok işle üretir.
- Bileşik katman dönüşümün ucuz olmasının nedenidir;
will-changebu ayrımı bildiren bir ipucudur ve bellek harcadığı için geçici kullanılır. - Ölçü okuma ile ölçü yazma işlemleri iç içe yapıldığında zorlanmış yerleşim hesapları oluşur; okumaları ve yazmaları öbeklemek bunu ortadan kaldırır.
Sonraki Adım
Buraya kadar hareketin nasıl yazıldığı ve neye mal olduğu kuruldu. Bir soru dışarıda kaldı: hareket her kullanıcı için aynı şey midir? Bazı kullanıcılar için ekrandaki hareket rahatsızlık, baş dönmesi ya da bulantı üretir ve işletim sistemi bu tercihi bildirir. Sonraki ders, hareket azaltma tercihinin ne anlama geldiğini ve hangi hareketin kaldırılıp hangisinin korunacağını ele alır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.