Görmediğini durdur: döngüleri ekran dışında ve perde arkasında uyutmak
Sitede sürekli hareket eden çok şey var: hero alanı, halkalar, kayan kelime bandı, menüdeki kapak çizimleri. Bu notta bunların hangi kurallarla çalıştığını yazıyorum: yalnızca transform ve opacity, Vue'yu atlayan rAF döngüleri ve görünmeyen her şeyi durduran küçük mekanizmalar.
01Kural: yalnızca transform ve opacity
Animasyonlarda ilk kuralım şu: kare başına değişen şey transform ya da opacity olsun. Bu iki özellik genellikle layout ve paint tetiklemeden compositor tarafında çözülebiliyor. base.css'teki kaydırmaya bağlı reveal'lar (rv1, rv2, rv3) bu yüzden yalnızca translate, scale, rotate ve opacity kullanıyor. Bu animasyonlar animation-timeline: view() ile tamamen CSS'te çalışıyor; desteklemeyen tarayıcılar içeriği doğrudan gösteriyor.
Aynı kural küçük yerlerde de geçerli. Hero'daki crosshair çizgileri left ve top yerine translateX ve translateY ile hareket ediyor. Hero'nun boyutu bir kez ölçülüp önbelleğe alınıyor ve yalnızca resize ya da scroll olduğunda sıfırlanıyor; böylece hareket sırasında layout okunmuyor.
Kaydırma da aynı dikkati istiyor. usePointer içindeki scroll dinleyicisi her olayda iş yapmıyor; bir kare isteyip o karede çalışıyor. Kaydırma ilerlemesini bir :root değişkenine yazmak kolay olurdu, ama koddaki yorumun da dediği gibi bu bütün sayfanın stilini yeniden hesaplatırdı. Onun yerine değer yalnızca iki tüketiciye gidiyor: header'daki ilerleme çubuğu scaleX ile, hero halkaları translate ile hareket ediyor. Halkaların parallax'ı ilk 1200 pikselden sonra ve hareket azaltma açıkken hiç güncellenmiyor.
02Vue'yu döngünün dışında tutmak
Ana sayfadaki hero 14×8'lik bir artı alanı ve beş halkadan oluşuyor; imleci takip ediyorlar, tıklayınca alandan bir dalga geçiyor. Bunu reaktif state ile yazsaydım her pointermove, Vue'ya 112 hücrelik bir yeniden render yaptırırdı. Onun yerine alan bir kez render ediliyor. pointermove yalnızca iki sayıyı güncelliyor ve schedule çağırıyor; schedule da bekleyen bir kare yoksa requestAnimationFrame istiyor.
function onMove(e: PointerEvent) {
const p = local(e)
px = p.x
py = p.y
schedule()
}
function schedule() {
if (!raf) raf = requestAnimationFrame(paint)
}
// inside paint(), for each of the 112 cells
const still = k === 0 && w === 0
if (still && c.rest) continue // far from pointer and already drawn at rest: nothing to write
c.rest = still
// end of paint(): only a running pulse asks for another frame
if (pulse) schedule()paint içinde her hücre için imlece uzaklık ve varsa dalganın etkisi hesaplanıyor. İkisi de sıfırsa ve hücre zaten dinlenme halinde çizildiyse continue ile atlanıyor, DOM'a hiçbir şey yazılmıyor. Döngü kendini de durduruyor: yalnızca dalga sürerken bir sonraki kareyi istiyor. İmleç durduğunda kare istenmiyor, yani boşta neredeyse hiçbir şey çalışmıyor.
Playground'daki GenerativeField aynı deseni kullanıyor: 144 çubuk, açılar 3 derecelik adımlara yuvarlanıyor ve değer bir önceki kareyle aynıysa style'a dokunulmuyor. Diğer sayfalardaki pointer işleri için src/composables/useRaf.ts içinde useRafThrottle var; hızlı gelen olayları kare başına tek bir güncellemeye indiriyor.
03Ekran dışındakini durdurmak
CSS animasyonları görünmeseler bile çalışmaya devam ediyor. Bunun için v-pause-offscreen adında bir direktif yazdım (src/composables/useOffscreen.ts). Sayfa için tek bir paylaşılan IntersectionObserver var; gözlenen eleman görünür alandan çıkınca ona off sınıfını ekliyor. Gerisini base.css'teki tek bir kural hallediyor.
let io: IntersectionObserver | null = null
function observer() {
if (!io) {
io = new IntersectionObserver(
(entries) => {
for (const e of entries) (e.target as HTMLElement).classList.toggle('off', !e.isIntersecting)
},
{ rootMargin: '120px 0px' }
)
}
return io
}
/* base.css */
.off, .off * { animation-play-state: paused !important; }rootMargin 120 piksel olduğu için döngüler görünür alana girmeden biraz önce yeniden başlıyor; kaydırırken donmuş bir kare görmüyorsun. Direktif hero halkalarında, ProceduralArt'ın dönen çizimlerinde, OrbitRings ve ReactiveGrid deneylerinde kullanılıyor. JavaScript ile çalışan kayan kelime bandı MarqueeBand aynı fikri kendi IntersectionObserver'ı ile uyguluyor: bant görünmüyorsa loop bir sonraki kareyi istemiyor.
04Perde arkasındakini durdurmak
Menü ya da komut paleti açıkken sayfa arkada kalıyor ama döngüler çalışmaya devam edebiliyordu. ui store'unda overlay adında bir computed var: menü ya da palet açıksa true. usePointer bunu izleyip html elemanına ovl sınıfını ekliyor; shell.css'te .ovl altındaki dönen ve nabız atan sınıflar (spin, horb, orr, rpA, rpB) duraklatılıyor. MarqueeBand overlay'i doğrudan okuyor; true ise döngüsünü bırakıyor, false olunca watch üzerinden yeniden başlıyor.
Görünmeyen bir animasyonu çalıştırmanın kimseye faydası yok; durdurmak çoğu zaman tek bir sınıf.
05Hiç render etmemek
Bazı şeyleri duraklatmak yerine geç oluşturmak daha ucuz. Menüdeki beş kapak çizimi (koddaki yoruma göre yaklaşık 200 şekil) menü ilk kez açılana kadar DOM'a girmiyor; seen adında bir ref bunu v-if ile kontrol ediyor. Ana sayfanın alt bölümlerinde, footer'da ve uzun sayfaların sonundaki bölümlerde content-visibility: auto var; görünür alandan uzak bölümler layout ve paint'i atlıyor. contain-intrinsic-size: auto 900px ise bir kez render edilen bölümün gerçek yüksekliğini hatırlıyor, böylece kaydırma çubuğu zıplamıyor.
Fontlar da ilk boyamayı bekletmiyor. index.html'de Google Fonts stil dosyası önce preload ediliyor, sonra media="print" ile yüklenip onload anında all'a çevriliyor; display=swap metni hemen gösteriyor. JavaScript kapalıysa noscript içindeki normal link devreye giriyor.
06Hareket azaltma
prefers-reduced-motion açıkken bu döngülerin bir kısmı hiç başlamıyor. useReducedMotion tek bir matchMedia dinleyicisi tutuyor. MarqueeBand başlamadan önce isReduced kontrol ediyor, hero tıklamada dalga üretmiyor ve giriş animasyonunu atlıyor, responsive.css dönen animasyonları ve reveal'ları kapatıyor. Hiç çalışmayan bir döngü, durdurulan bir döngüden de ucuz.