holodepth

HTML5 Canvas · Canvas API & context

Resize mantığı: görünen kutu, piksel ızgarası ve DPR

Canvas’ta «tam ekran yap» demek yalnızca CSS width: 100% yazmak değildir. Tarayıcı öğeyi ekranda büyütür; iç bitmap çoğu zaman eski piksel sayısında kalır — sonuç bulanık ölçeklenmiş çizimdir. Bu sayfa layout kutusu (CSS) ile iç çözünürlük (canvas.width / height) arasındaki köprüyü kurar: devicePixelRatio, yeniden boyut olayları ve bağlamın sıfırlanması birlikte düşünülür.

Koordinat ekseni ve dönüşüm Koordinat sistemi sayfasında; yüzeyin ilk kurulumu Canvas oluşturma ile sabitlendi. Burada pencere büyüdükçe bitmap’i nasıl yeniden ayarlayacağınız, olay koordinatı eşlemesinin neden buna bağlı olduğu ve durum yığını neden sıfırlandığı anlatılır — tam stack detayı State sistemi sayfasına köprülenir.

Özet: resize kararının ana hatları

Soru Yanıt Pratik
Neden bulanık? CSS büyüttü, bitmap küçük kaldı Backing store = css × DPR (yuvarlama ile)
Boyut kimde? Attribute = piksel; CSS = görünen kutu Ölç → attribute yaz
width değişince? Bağlam sıfırlanır Stil ve dönüşümü yeniden kur
Ne zaman dinle? Pencere, konteyner, tam ekran ResizeObserver + isteğe resize

İki katman: CSS kutusu ve bitmap

Canvas oluşturma sayfasında ayrılmıştı: <canvas width="600" height="400"> iç ızgarayı, style="width:100%" ise sayfadaki görünen dikdörtgeni belirler. İkisi senkron değilse tarayıcı bitmap’i filtre ile ölçekler (çoğu zaman bikübik veya benzeri) — vektör dünyasındaki «rasterize et» adımını kullanıcı her karede fark etmiş olur.

Resize mantığının özü: önce hedef mantıksal boyutu belirleyin (genelde CSS pikseli), sonra devicePixelRatio ile çarpıp fiziksel piksel sayısına yuvarlayın, canvas.width ve canvas.height atayın. Böylece bir çizim koordinatı hâlâ bir pikseldir; koordinat sistemi sayfasındaki ızgarayla uyum korunur.

devicePixelRatio ve keskin piksel

window.devicePixelRatio (DPR), bir CSS pikselinin kaç cihaz pikseline karşılık geldiğini söyler. Retina ekranlarda 2 veya 3 görülür; tarayıcı yakınlaştırınca değişebilir. Canvas backing store’u yalnızca CSS genişliğe eşitlerseniz çizgi halen yumuşak görünür; DPR ile çarpmak, bitmap’i fiziksel grid’e oturtur.

Önemli nüans: DPR çizim API’sinin bir parçası değildir; layout ve görüntüleme katmanının ölçek bilgisidir. Siz «bu canvas şu CSS kutusuna sığsın» dersiniz, sonra o kutunun cihaz pikseli karşılığını hesaplarsınız. Aynı DPR, medya sorguları veya matchMedia('(resolution: …)') ile birlikte düşünülebilir; canvas tarafında pratik kaynak hâlâ çoğu zaman window.devicePixelRatio okumasıdır.

Yuvarlama ve «neredeyse keskin»

Math.floor(cssPx * dpr) genelde güvenli alt sınır verir; Math.round bazen daha simetrik sonuç üretir. Çok ince farklarda yarım piksel kayması olabilir; kritik HUD grid’lerinde bir kez test edin. DPR tamsayı değilse (bazı Windows ölçekleri %125, %150) «tam keskin» her zaman mümkün olmayabilir; kabul toleransı üretim kararıdır. Koordinat sayfasındaki ince çizgi hizalaması, DPR sonrası bile scale ile bozulabilir — grid çizgilerini HUD katmanında minimal dönüşümle çizmek ayrı bir alışkanlıktır.

function backingDimensions(cssW, cssH, dpr = window.devicePixelRatio || 1) {
  return {
    width: Math.max(1, Math.floor(cssW * dpr)),
    height: Math.max(1, Math.floor(cssH * dpr)),
    dpr,
  };
}

Yakınlaştırma ve tam ekran

Kullanıcı sayfayı %110 yaptığında veya tam ekran geçtiğinde DPR veya CSS boyutu değişebilir; dinleyici (resize / ResizeObserver) tetiklenmezse eski bitmap ile kalmak mümkündür. Üretimde «DPR veya kutuyu etkileyen her değişiklikte fit çalışsın» disiplinini benimseyin; beşinci bölümdeki gözlemci bunun ana taşıyıcısıdır.

Canvas Resize & DPR Lab ile CSS kutusu, bitmap, DPR eşlemesi, CSS-only bulanıklık tuzakı ve girdi remap’i canlı deneyin.

Keskin
canvas üzerine gelin
Canvas Resize & DPR Lab: Sürgüler ResizeObserver + fit kalıbını simüle eder — bitmap değişince bağlam sıfırlanır. İmleci canvas üzerinde gezdirerek clientbitmap eşlemesini görün. Oluşturma sayfasındaki Resolution Lab attribute/CSS ayrımına odaklanır; bu demo DPR + resize döngüsüdür.

width / height ataması bağlamı sıfırlar

Özellik ataması: canvas.width = w veya canvas.height = h (veya ikisi) yapıldığında tarayıcı bitmap’i yeniden ayırır ve çizim bağlamının tüm durumunu sıfırlar: dönüşüm matrisi, klip, stil özellikleri, yol, shadow… (spesifikasyon gereği). Bu yüzden resize işleminden sonra getContext('2d') hâlâ aynı nesneyi döndürse bile görünür çizim ayarlarınız kaybolmuş olur.

Özellikle sıfırlananlar arasında globalAlpha, globalCompositeOperation, çizgi ve metin stilleri, imageSmoothing*, mevcut path ve setTransform ile kurulmuş matris sayılır — yani koordinat ve 2D context sayfalarında öğrendiğiniz her «kalıcı ctx ayarı» fiilen yok olur. Bu, hatanın kaynağını bulmayı zorlaştırır: resize sonrası ilk karede «neden hep siyah doldurma var?» sorusunun cevabı çoğu zaman varsayılan bağlamdır.

Üretim kalıbı: kur → çiz

Üretim kalıbı: boyutu güncelle → bağlam varsayılanına döndüğü için temel stil ve gerekirse transform kur → sahneyi yeniden çiz. State sistemi sayfasındaki save/restore yığını, resize anında otomatik geri gelmez; saklanmış snapshot’lar da attribute değişince geçersizleşmiş sayılır. Çözüm: tek bir applyCanvasDefaults(ctx) (ve gerekiyorsa configureWorldTransform(ctx)) fonksiyonunu resize ve ilk başlatmada aynen çağırmak.

function applyCanvasDefaults(ctx) {
  ctx.setTransform(1, 0, 0, 1, 0, 0);
  ctx.globalAlpha = 1;
  ctx.globalCompositeOperation = 'source-over';
  ctx.imageSmoothingEnabled = true;
  ctx.fillStyle = '#0b0f14';
  ctx.strokeStyle = '#2ee7f2';
  ctx.lineWidth = 1;
  // font, textAlign vb. ihtiyaca göre
}

function onCanvasResized(canvas, ctx) {
  applyCanvasDefaults(ctx);
  drawScene(ctx); // Tam sahne yeniden çizimi
}

save/restore burada yetmez

Resize öncesi yapılmış save() stack’i, bitmap yeniden ayrıldığında eski durumu geri yüklemez — yığın mantığı State sistemi içinde anlatılır; bu bölümün özü: boyut değiştiyse kurulum rutinini tekrar çalıştır.

fitCanvasToContainer: tipik kalıp

Aşağıdaki kalıp, mantıksal boyutu (CSS pikseli) alır, DPR ile çarpar ve attribute’lara yazar. Ölçüm kaynağı olarak konteynerın clientWidth / clientHeight, getBoundingClientRect() veya ResizeObserver’ın contentRect değerleri kullanılabilir — önemli olan, okuduğunuz sayının layout ile uyumlu tek bir kurala bağlı olmasıdır. Farklı kaynaklar alt piksel veya scrollbar dahil/hariç tutarlılığı yaratabilir; beşinci bölümde gözlemci ile sabitlemek yaygındır.

CSS ile eşitleme

Backing store büyüdüyse görünen kutunun da aynı oranda büyüdüğünden emin olun: canvas.style.width = cssW + 'px' ve height genelde contentRect ile aynı sayılara ayarlanır. Aksi halde tarayıcı yine bitmap’i kutuya sığdırmak için ölçekler — bu sayfanın birinci bölümündeki bulanıklığa dönersiniz.

function fitCanvasToCssPixels(canvas, cssW, cssH) {
  const dpr = window.devicePixelRatio || 1;
  const bw = Math.max(1, Math.floor(cssW * dpr));
  const bh = Math.max(1, Math.floor(cssH * dpr));

  if (canvas.width === bw && canvas.height === bh) {
    return false; // attribute dokunulmadı → bağlam sıfırlanmadı
  }

  canvas.width = bw;
  canvas.height = bh;
  canvas.style.width = `${cssW}px`;
  canvas.style.height = `${cssH}px`;
  return true; // üçüncü bölüm: bağlam sıfır → applyCanvasDefaults çağır
}

fit’in true dönmesi, çizim ayarlarınızı sıfırladığınız anlamına gelir — bu yüzden çağıran tarafta «boyut değiştiyse defaults + tam redraw» zinciri kurulur. Yan etkisiz tekrar boyut atamalarından kaçınmak için yukarıdaki erken return false faydalıdır.

ResizeObserver ve pencere resize

window.addEventListener('resize', …) yalnızca viewport değişiminde ateşlenir; yan panele göre büyüyen bir flex içindeki canvas için konteyner boyutu değişse bile tetiklenmeyebilir. Modern çözüm: canvas’ı saran elemana ResizeObserver bağlamak — boyut değişimi DOM ölçümünden gelir.

Fırtınalı resize’da (sürüklerken sürekli olay) doğrudan ağır çizim yapmayın; bir bayrak veya requestAnimationFrame ile tek karede birleştirmek requestAnimationFrame disipliniyle örtüşür. Detaylı yeniden çizim maliyeti Redraw cost sayfasına bırakılır.

function observeCanvasHost(hostEl, canvas, onSized) {
  const ro = new ResizeObserver((entries) => {
    for (const entry of entries) {
      const cr = entry.contentRect;
      onSized(cr.width, cr.height);
    }
  });
  ro.observe(hostEl);
  return () => ro.disconnect();
}

Girdi koordinatı resize ile birlikte

Çizim koordinatı = bitmap pikseli; fare olayı ekran veya öğe uzayındadır. canvas.width ve görünen kutu değiştikçe ölçek oranı da değişir. Harita tıklaması, drag-select veya UI isabet testi yapıyorsanız olaydan gelen offsetX / offsetY veya client çiftini, o andaki iç boyuta projekte etmeniz gerekir. Resize & DPR Lab üzerinde imleci gezdirerek clientbitmap eşlemesini canlı görün.

Oran: CSS kutusu ↔ bitmap

Basit çarpan: scaleX = canvas.width / rect.width, scaleY = canvas.height / rect.height — burada rect canvas öğesinin getBoundingClientRect() çıktısıdır (CSS pikseli). Ardından örneğin bitmapX = (clientX - rect.left) * scaleX ile iç uzaya geçilir; yüksekliğe dik düzenlemeler, tam ekran ve retina için Event coordinates sayfasında tablolaştırılır.

Resize sonrası «tıklama kaydı» çoğu zaman şu iki sebepten biridir: (1) eski canvas.width ile oran hesaplamak, (2) CSS kutusu ile Bitmap’i senkronlamamış olmak. Fit fonksiyonu hem görüntüyü hem girdi oranını tutarlı tutar.

function eventToBitmap(canvas, clientX, clientY) {
  const rect = canvas.getBoundingClientRect();
  const scaleX = canvas.width / rect.width;
  const scaleY = canvas.height / rect.height;
  return {
    x: (clientX - rect.left) * scaleX,
    y: (clientY - rect.top) * scaleY,
  };
}

OffscreenCanvas ve işçi — bu sayfanın sınırı

Ana iş parçacığında resize ile büyük bitmap yeniden ayırma, tam yüzey clearRect ve ağır yeniden çizim aynı karede üst üste gelirse giriş gecikmesi hissedilir. Tarayıcı ana döngüyü bloke etmemek için: yükü azaltma (daha az çizim), kare başına bütçe ( Redraw cost) veya ağır işi arka plana taşıma seçenekleri vardır.

Offscreen canvas ve Web Worker ile boyut güncelleme ve raster işi ayrılabilir; bu sayfa yalnızca ana thread’deki boyut sözleşmesini sabitler. İşçiye bitmap taşımanın serileştirme / transfer maliyeti ayrı başlıktır.

  • Resize olayı geldi ≠ hemen en ağır sahneyi çiz; gerekirse bir sonraki frame bütçesine ertele.
  • Çok büyük canvas’ta çözünürlüğü kademeli artırma (placeholder → detay) üretim kararıdır.
  • Ağır iş taşıma: Offscreen + worker kataloğu ayrı sayfadadır.

Three.js / WebGL ile paralel okuma

Aynı problem, farklı API

Three.js WebGLRenderer.setSize(width, height, false) çağrısı görünen boyutu; üçüncü parametre ile updateStyle kontrol edilir. setPixelRatio(Math.min(window.devicePixelRatio, limit)) ise DPR tavanı koyarak hem keskinliği hem maliyeti yönetir — 2D tarafta sizin floor(css * dpr) ile yaptığınıza benzer bir soyutlama.

2D canvas’ta attribute ve CSS senkronunu elle yazarsınız; WebGL tarafında renderer çoğu temiz başlatmayı üstlenir. Önemli fark: Three sahnesinde «nesne koordinatları» kamera ve projeksiyon ile çarpılır; 2D bitmap ise doğrudan piksel ızgarasıdır. Bu yüzden HUD için ayrı 2D canvas katmanı kullanmak sık kalıptır. Karşılaştırma: Canvas vs WebGL.

Sıradaki: durum yığını

Boyut ayarlandıktan sonra stil ve dönüşümü güvenle yönetmek için State sistemi sayfasına geçin — orada save/restore yığını, clip ile etkileşim ve iç içe dönüşüm senaryoları derinleşir. Resize bu sayfada bağlamı «boşaltır»; state sayfası aynı bağlamı kasıtlı olarak nasıl katmanlayacağınızı anlatır.

Önceki adımlar: Koordinat sistemi, 2D context.