holodepth

HTML5 Canvas · Render döngüsü

Update vs render: mantık ve piksel sunumunu ayırmak

Sürekli çalışan bir canvas uygulamasında iki farklı iş vardır: birincisi dünya durumunu — konumlar, skor, animasyon fazı, seçim mantığı — güncellemek ( update); ikincisi o durumu piksel buffer’ına çizmek ( render). İkisini aynı fonksiyon içinde yazmak küçük demolar için işe yarar; fakat kare atlama, sabit adım simülasyon, ağır çizim veya girdi fırtınası olduğunda ayrımı bilinçli tutmak hata ayıklamayı ve performansı iyileştirir. Bu sayfa, bu iki aşamanın canvas üreticisi gözünden sınırını, sırasını ve tipik kısayollarını sabitler.

Zaman damgası ve adım süresi Delta time ile; kare planlama requestAnimationFrame ile; piksel sıfırlama Clear & redraw ile — burada yalnızca «önce neyi hesaplıyorum, sonra neyi boyuyorum?» sorusu ön plandadır.

Özet: iki aşamanın karşılaştırması

Aşama Tipik iş Canvas’ta dikkat
Güncelleme Durum, fizik adımı, kaydırılan içerik, sayaçlar Bağlam (ctx) okuması nadirdir; saf veri tercih edilir
Sunum clearRect, yol, dolgu, drawImage, metin Yoğun piksel işi; tek turda atomik «kare» tamamlanmalıdır
Birleşik döngü Küçük örneklerde step+draw tek geri çağrıda Büyüyünce modüllere bölmek maliyeti düşürür
Olay güdümlü sunum Güncelleme olayda, çizim birleştirilmiş rAF’ta Aynı karede onlarca draw yerine tek planlı çağrı

Güncelleme ve çizim: iki ayrı sorumluluk

Zihinsel model: güncelleme saf JavaScript nesneleri üzerinde çalışır — dizi, yapı, oyun durumu — ve mümkün olduğunda CanvasRenderingContext2D API’sine dokunmaz. Böylece aynı durumu başka bir yüzeye çizmek, ekran görüntüsü üretmek veya birim testinde doğrulamak kolaylaşır. Sunum ise yalnızca o anki durumu okuyup komut dizisi ile piksele döker; iş mantığı içinde gizli yan etkiler (ör. yanlışlıkla global fillStyle değiştirmek) üretmemelidir.

Pratikte küçük projede tek tick(dt) içinde önce alt rutinlerle durumu ilerletip sonra draw() çağırmak yeterlidir — önemli olan isimlerin ve dosya sınırlarının iki aşamayı ayırmasıdır. «Çizim içinde rastgele skor artırma» gibi desenler, mod büyüdüğünde yarış durumu ve telafisi zor bug’lar üretir.

Sunum katmanı, durumun tam kopyasını tutmak zorunda değildir; yeterince göstermek için türetilmiş alanlar (ekran uzayındaki önbellekli yol, son çizilen metin genişliği) kullanılabilir — fakat «tek doğruluk kaynağı» çoğu ekipte yine güncelleme çıktısıdır; çizim önbelleği ile oyun durumu karıştırılmamalıdır.

Kare döngüsünde çağrı sırası ve atomik sunum

Tipik requestAnimationFrame geri çağrısında sıra genelde şöyledir: ölçülen süreyi hesapla → güncellesun (temizlik + dünya + HUD). Kullanıcıya yansıyan bitmap, bu turun sonunda oluşan halidir; ara durumda ekranın yarım çizilmiş kalması üretim kalitesi açısından hatalıdır — istisna yalnızca kasıtlı hata ayıklama modlarıdır.

Bazı uygulamalar önce sunumu, sonra güncellemeyi dener; bu, bir kare gecikmesi yaratır ve girdinin ekranda hissedilir biçimde «bir adım geriden» gelmesine yol açar. Canvas odaklı etkileşimli sahnelerde varsayılan kural: bu karenin girdisi → bu karenin yeni durumu → bu karenin çizimi.

Çizim içinde ağır hesap (ör. grafik veri setini her karede sıralamak) sunum bütçesini yer; mümkünse sonuçları güncelleme aşamasında önbelleğe alıp sunumda yalnız okuyun. Yoğun iş güncellemede bloklanırsa yine kare süresi uzar — fakat kod okunurluğu ve profil ayırımı için blokların etiketlenmesi değerlidir.

/**
 * world: saf veri nesnesi. update sadece world döndürür; render sadece okur.
 */
function tickFrame(ctx, canvas, world, dtSeconds, update, render) {
  const next = update(world, dtSeconds, canvas);
  render(ctx, canvas, next);
  return next;
}

Tek görünür karede birden fazla simülasyon adımı

Gerçek süre iki kare arasında büyük gelebilir; sabit küçük adımlarla ilerleyen simülasyonlarda tek görünür kare içinde birden fazla güncelleme çalıştırılıp yine de yalnızca bir kez çizim yapılır. Bu, görsel ara pozların ihmal edildiği «adım adım doğru, çizimde kare başına tek snapshot» modelidir; ara görüntüyü yumuşatmak için interpolasyon ( interpolation) ayrı karardır — birikimli artık ve sabit adım ayrıntısı Delta time · sabit adım ile sınırlı örtüşür, burada yalnızca «güncelleme sayısı ≠ çizim sayısı» ilkesi vurgulanır.

Çok adım + tek çizim, CPU maliyetini aynı görünür karede toplar; ekranda donma hissi yaratmamak için birikim tavanı ve maksimum adım sayısı ( cap) kullanılır — aksi halde uzun sekme donmasından sonra saniyelerce yakalama döngüsüne girilir.

Tam tersi desen — tek güncelleme ama ara çizim denemeleri — genelde yanlıştır; kullanıcı ara bitmap’i görmemelidir. Özel durum: uzun işi kesip ilerleme çubuğu çizmek gibi bilinçli ara sunum, ayrı ürün kararıdır.

Girdi olayları, mantık güncellemesi ve sunum

İmleç ve dokunma olayları ana iş parçacığında tetiklenir; her olayda draw çağırmak, aynı karede onlarca piksel geçişi üretebilir. Yerleşik desen: olayda yalnızca durumu güncelle veya bir «kirli» bayrak kaldır, asıl çizimi requestAnimationFrame ile birleştir — böylece tek kompozit yolunda toplanır.

Tuşa basılı tutma gibi sürekli girdi, ya rAF içinde «basılı mı?» bayrakları ile okunur ya da olay birleştirilir. Canvas tek başınaysa girdi genelde DOM üzerinden gelir; güncelleme fonksiyonuna parametre olarak iletilen ham olayları, mümkünse erken aşamaya indirgenmiş niceliklere (dünya x/y) çevirmek sunum kodunu sade tutar.

Girdi ile sunum sırasını karıştırmamak: önce yeni girdiyi duruma işle, sonra o duruma göre çiz — böylece kullanıcı gördüğü kare, en güncel girişle uyumludur (tek iş parçacığı varsayımıyla).

/**
 * Olaylarda sadece world güncellenir; çizim en fazla bir planlı rAF ile yapılır.
 */
function createCoalescedPainter(ctx, canvas, initialWorld, updateFromEvent, render) {
  let world = initialWorld;
  let rafId = 0;
  let scheduled = false;

  function frame() {
    scheduled = false;
    render(ctx, canvas, world);
  }

  function schedulePaint() {
    if (scheduled) return;
    scheduled = true;
    rafId = requestAnimationFrame(frame);
  }

  function onEvent(e) {
    world = updateFromEvent(world, e, canvas);
    schedulePaint();
  }

  function dispose() {
    cancelAnimationFrame(rafId);
    scheduled = false;
  }

  return { onEvent, schedulePaint, dispose, getWorld: () => world };
}

Kirli bayrak ve gerektiğinde çizim

Statik veya nadiren değişen sahnelerde her rAF’ta tam clear + yeniden çizmek pil ve CPU tüketir. Kirli bayrak deseni: yalnız durum değiştiğinde çizim planlayın; animasyon durduğunda döngüyü durdurma ( optional) ile birleştirilebilir — fakat yeni girdi geldiğinde yeniden başlatmayı unutmak yaygın hatadır.

Bayrağı «nesne değişti» düzeyinde tutmak yerine, bazen görsel etki alanı notasyonu ( invalidation) daha nettir: örneğin yalnız metin önbelleği kirliyse tam sahneyi değil metin bloğunu yeniden ölçüp çizersiniz. Özet tek bit bile yeterli olabilir; ekip içinde bayrağın anlamını (tam kare mi, alt düğüm mü) yazılı sözleşmeye bağlayın.

Kısmi kirli bölgeler ( Clear & redraw · kirli bölge ) ile birlikte düşünüldüğünde güncelleme hangi dikdörtgenleri etkilediyse sunumda yalnız o bölgeleri yenilemek mümkündür; maliyet hesabı sahne karmaşıklığına bağlıdır. Güncelleme tarafı bu dikdörtgenleri üretmezse sunum tarafı tahmin etmek zorunda kalır — kirli bölge sınırını mühendislik sızıntısı olmaması için ya duruma taşıyın ya da tek bir «görünür alan hesaplayıcı» modülde toplayın.

resize, cihaz piksel oranı veya yazı tipi yüklemesi gibi dış olaylar bazen durumu numerik olarak değiştirmeden sunumu geçersiz kılar; kirli bayrak yalnızca mantık deltasını izliyorsa ekran boş veya eski ölçekte kalabilir. Bu tür sınırlarda tam yeniden çizim zorunluluğunu ayrı bayrakla veya sabit «her boyut değişiminde tam repaint» kuralıyla kapatın ( Resize mantığı kısa köprü).

Sürekli akış (oyun döngüsü) ile «olay olduğunda çiz» modeli aynı kodda karışırsa çift çizim veya hiç çizilmeme oluşabilir; ekip içinde birincil motoru ( always-on rAF mu, olay güdümlü mü) seçin ve diğerini istisna olarak dokümante edin. İkisi birden aktifse biri schedulePaint, diğeri doğrudan render çağırıyor mu bir tabloyla sabitleyin — kod gözden geçirmede çelişki böyle görünür olur.

Katman düzeni: güncelleme modülleri ve çizim sırası

Büyük projede güncelleme bileşenleri (fizik, otomasyon — ör. kaydırma veya zamanlayıcı —, kullanıcı aracı) ayrı fonksiyon veya sınıflar olur; çağrı sırası oyun kuralına göre sabitlenir. Bu sıra çoğu zaman bağımlılık grafına benzer: önce girdi işlendi mi, sonra fizik mi, en sonda oyun kuralları mı — sunum tarafı bu sıranın aynasını izlemek zorunda değildir.

Sunum tarafında ise tipik sıra arka plan → dünya nesneleri → ön plan / HUD korunur — güncelleme sırası ile çizim sırası aynı olmak zorunda değildir; önemli olan «hangi veri hangi sprite’a karşılık geliyor?» eşlemesinin tutarlı olmasıdır. Canvas 2D’de derinlik tamamen çağrı sırasıdır; önce çizilen altta kalır — z-index yoktur, bu yüzden sunum sırası doğrudan okunabilirlik ve çarpışma maskesi ile ilgilidir, güncelleme çağrı zinciri ile değil.

Aynı kare anlık görüntüsü için sunum başında dünya durumunun bir okunur kopyası veya tutarlı referansı kullanılır; güncelleme hâlâ sürerken render ortasında başka modülün nesneyi taşıması «yarım kare» üretir. Pratik çözüm: tick içinde önce tüm güncellemeleri bitir, sonra tek render(snapshot) — ara içeride paylaşılan nesneyi mutasyona uğratmayın.

Aynı nesne için güncelleme iki yerde yapılıyorsa ( double update) hareket iki kat hızlanır veya dalgalanır. Bunu önlemek için tek sahiplik kuralı: her simülasyon niceliği için tek yazıcı modül seçin. İnce ayar: bileşenler arası ileti olayları ( message bus) kullanıyorsanız aynı olayı iki dinleyicinin iki kez tüketmediğinden emin olun.

Özel birleştirme geçişleri ve gölgeler sunum aşamasında kısa dallara bölünür; bu dallar güncelleme mantığına karışmamalıdır — aksi halde bir sonraki karede stil sızıntısı üretir ( State sistemi ). Gölge veya filter gibi pahalı nitelikler yalnız HUD öğesine uygulanacaksa, dünya çiziminden sonra dar bir dalda açılıp kapatılması okunabilirliği korur.

Anti-kalıplar: iç içe geçmiş aşamalar

Sunum içinde gizli güncelleme: Çizim fonksiyonu yan etkiyle skor veya konumu değiştirirse hata ayıklama ve test imkânsızlaşır — sunum salt okunur kalsın. Özellikle fillText ile metin ölçümü yaparken metin kutusunu büyütmek için yanlışlıkla durum alanını güncellemek, «neden sayaç iki kez arttı?» sorununu gizler.

Güncellemede doğrudan ctx komutu: Durum ile bağlam birbirine yapışır; farklı yüzeye, baskı yoluna veya sunucu tarafı önizlemeye geçiş zorlaşır. İstisna: gerçekten ölçüm amaçlı ( measureText ) çağrıları — bunları da mümkünse sunum öncesi önbelleğe alın.

Olay başına tam çizim: Performans ve titreşim artar; birleştirilmiş rAF tercih edin. İstisna: düşük frekanslı (ör. yılda bir kez açılan modal) tamamen statik arayüzler — yine de iki olayın aynı karede üst üste binmesi göz önünde bulundurulmalıdır.

Güncelleme sonra sunum önce: Bir kare gecikmesi ve hantal his — sırayı düzeltin. Pratik gösterge: girdi olayında durum anında değiştiği halde ekranda bir önceki kare görünüyorsa, tur içi sırayı veya kirli bayrak birleşimini gözden geçirin.

Asenkron güncelleme ile çakışma: Ağ veya worker cevabı geldiğinde dünya düşünülmeden mutasyonlanıyorsa, o sırada çalışan render yarı tutarlı veri okuyabilir. Çözüm: atomik değiş tokuş ( swap snapshot ), kilit veya tek iş parçacığı kuralı — konunun tamamı bu sayfanın dışındadır, fakat «güncelle ve sun ayrımı» varken yarışlar yine görülür.

Test ve snapshot: Saf update(state, action) → state yazımı, birim testinde kolaydır; render ise görsel regresyon veya ekran görüntüsü ile doğrulanır. İkisi birbirine yapışık olunca test piramidi çöker — ayrımın faydası burada da kendini gösterir.

Bu sayfanın sınırı

Çok iş parçacıklı motorlar, Web Worker ile veri paylaşımı ve kilitlenmesiz yapılar bu sayfada işlenmez; varsayım tek ana iş parçacığı ve klasik 2D bağlamdır.

Entity–component mimarisi veya büyük ölçekli saha grafı ayrı tasarım konularıdır; ilke düzeyi «güncelle / sun» ayrımı burada sabittir.

  • Güncelleme fonksiyonu ctx yazıyor mu (istenmeyen yan etki)?
  • Aynı turda girdi → güncelle → sun sırası korunuyor mu?
  • Olay fırtınasında çizim birleştirildi mi?
  • Çift güncelleme veya çift çizim riski var mı?
  • Kirli bayrak yalnız mantık mı izliyor; resize / font sonrası tam boyama tetiklendi mi?
  • Asenkron cevap dünyayı mutasyonlarken render yarışta mı?