holodepth

HTML5 Canvas · Canvas API & context

Koordinat sistemi: eksen, ölçek ve dönüşüm

Canvas’ta her fillRect, lineTo veya drawImage çağrısı bir (x, y) çiftine dayanır. Bu sayılar, bitmap’in sol üst köşesinden başlayan piksel ızgarasındaki konumdur — okulda gördüğünüz «yukarı pozitif» ekseninden bilinçli olarak farklıdır. Önceki sayfalarda yüzeyi ve çizim kalemini kurduk; burada «nereye çiziyorum, döndürünce ne oluyor, fare tıklaması neden uyuşmuyor?» sorularının canvas tarafındaki cevabını netleştiririz.

Hedef, matematik dersi değil; 2D bağlamın uzay modeli. Dönüşüm yığınının tam tablosu ve save/restore disiplini State sistemi sayfasında; pencere–bitmap ölçeklemesi Resize mantığı ile; fare konumunun canvas’a taşınması Event coordinates ile devam eder.

Özet: canvas uzayının ana hatları

Kavram Canvas’ta Bu sayfada
Orijin (0, 0) sol üst Eksen yönü, sınır kutusu
Birim Bitmap pikseli (width/height) Attribute boyutu = koordinat aralığı
translate Orijini kaydırır Yerel uzay, sahne parçalama
rotate / scale Mevcut orijin etrafında Sıra ve birleşim; save ile güvenli kullanım
Hizalama Yarım piksel kayması 1 px çizgide bulanıklık önleme

Canvas uzayı ≠ matematik grafiği

Analitik geometride genelde x sağa, y yukarı gider; canvas 2D bağlamında y aşağı artar. (0, 0) bitmap’in sol üst köşesidir; x = canvas.width sağ kenar, y = canvas.height alt kenardır. Negatif koordinatlar çoğu zaman görünür alanın dışına düşer — çizim kırpılmazsa yine de «var» sayılır, fakat ekranda görünmez.

CSS ile konumlandırılmış bir div de sol üst referanslıdır; benzerlik burada biter. CSS layout kutusu ve akış modeli üzerinde çalışır; canvas ise tek bir piksel dizisine komut gönderir. «Elemanı ortala» yerine «dikdörtgenin sol üst köşesini (cx - w/2, cy - h/2) yap» dersiniz — merkezleme formülü bu sayfanın pratik çıktılarından biridir.

Three.js ve WebGL tarafında clip space ve kamera dönüşümleri farklı katmanlardadır; 2D canvas düzleminde ise çoğu proje doğrudan bu piksel uzayında kalır veya translate/scale ile «dünya» taklit edilir.

Eksen, ölçek ve görünür sınır

Canvas oluşturma sayfasında attribute width ve height bitmap çözünürlüğünü belirler; koordinat ekseninin sayısal üst sınırı da budur. CSS ile canvas’ı büyütmek yalnızca ekrandaki kutuyu ölçekler — içerideki (100, 50) hâlâ aynı pikseldir. Bu ayrım, «koordinatım doğru ama çizgi kalın/bulanık» şikâyetlerinin yarısını resize ve DPR konusuna bağlar; piksel oranı ayrıntısı resize sayfasına bırakılır.

fillRect(x, y, w, h) konumu sol üst köşe olarak okur; dairenin merkezi değil. Path’te arc(x, y, r, …) merkez kullanır — aynı sayılar farklı geometrik anlama gelebilir; karışıklık genelde API farkından kaynaklanır, eksenin kendisinden değil.

const w = canvas.width;
const h = canvas.height;

ctx.fillStyle = '#ff6b6b';
ctx.fillRect(0, 0, 12, 12);           // orijin köşesi

ctx.fillStyle = '#2ee7f2';
ctx.fillRect(w - 40, 0, 40, 24);      // sağ üst bölge

ctx.fillStyle = '#b56cff';
ctx.fillRect(0, h - 24, 40, 24);      // sol alt bölge

Translate: Yerel uzay açmak

ctx.translate(dx, dy) mevcut koordinat sisteminin orijinini kaydırır; sonraki tüm çizim komutları bu yeni orijine göre yorumlanır. Piksel cinsinden düşünürseniz: dünya koordinatlarındaki bir noktayı çizmek yerine önce «kâğıdı» kaydırırsınız, sonra o noktayı (0, 0) etrafında tanımlarsınız. Bu, tekil bir dikdörtgen için fazladan satır gibi görünür; onlarca parçadan oluşan karakter, UI bileşeni veya harita parçası çizerken tekrarlayan
x + offset, y + offset yazımını keser.

İki üretim kalıbı sık çıkar: nesne merkezli çizim (aşağıdaki drawShip) ve kamera kaydırması (dünyayı ters yönde
translate(-scrollX, -scrollY) ile kaydırıp dünya içeriğini dünya koordinatında bırakmak). İlkinde oyuncu ekranda sabit kalır dünya kayar; ikincisinde dünya sabit kalır kamera ilerler — matematik aynı yer değiştirme, oyun tasarımında karşılığı farklı.

Yerel orijin ve merkez

Bir gemiyi (x, y) dünya konumunda çizerken, gövdeyi dünya koordinatında hesaplamak yerine önce translate(x, y) yapıp gövdeyi
(-w/2, -h/2) civarında tanımlamak yaygındır. Böylece dönüşüm (rotate) sonraki bölümde aynı yerel orijin etrafında kolay uygulanır. Dikkat: fillRect sol üst köşe referanslıdır; merkezlemek için genişlik ve yüksekliği yerel uzayda eksi yarı genişlikle başlatırsınız.

function drawShip(ctx, worldX, worldY) {
  ctx.save();
  ctx.translate(worldX, worldY); // dünya konumu = yeni orijin
  ctx.fillStyle = '#2ee7f2';
  // Yerel (0,0) gemi merkezi; sol üst köşe (-20,-12)
  ctx.fillRect(-20, -12, 40, 24);
  ctx.restore();                 // matris ve stil önceki kareye dönmez
}

save mevcut dönüşüm + stil yığınının bir kopyasını alır; restore bir üst katmana döner — böylece bu fonksiyon çağrıldıktan sonra bağlamın «kirlenmediği» garanti edilir. translate(-worldX, -worldY) ile elle geri almak hataya açıktır (özellikle araya başka translate girdiğinde).

Kamera ofseti (scroll)

Harita çizerken tüm fayans veya nesne koordinatlarını ekran uzayına çevirmek yerine, kare başında bir kez translate(-camera.x, -camera.y) dersiniz; böylece dünya içeriği «kendi koordinatlarında» kalır. Negatif kaydırma, dünyayı sağa/aşağı itilmiş gibi gösterir — kamera sol üstten ilerliyormuş izlenimi verir.

function drawWorldLayer(ctx, camera) {
  ctx.save();
  ctx.translate(-camera.x, -camera.y);
  // Burada drawTile(worldX, worldY) doğrudan dünya koordinatında çağrılır
  drawTileMap(ctx);
  ctx.restore();
}

translate bağlamın dönüşüm matrisini değiştirir; fillStyle gibi bir stil özelliği değildir. Biriken dönüşümler State sistemi sayfasındaki save/restore veya beşinci bölümdeki resetTransform ile kontrol edilir — hangi yöntemin ne zaman uygun olduğu orada tablolaştırılır.

Bu sayfanın sınırı

İç içe save/ restore dengesi, kırpma (clip) ve unutulan geri alma hataları state sayfasının konusudur. Burada yalnızca şunu sabitleyin: her «geçici yerel uzay» açışında karşılığı restore veya açıkça matris sıfırlaması planlanmalıdır.

rotate ve scale: açı ile ölçek

rotate(açı) radyan bekler; derece kullanıyorsanız angle * Math.PI / 180 dönüşümünü üretim kodunda tek yerde toplayın. Dönüş, o andaki orijin etrafında uygulanır — bu yüzden nesneyi dünyada döndürmek için tipik sıra: dünya konumuna translate, sonra rotate, sonra yerel geometri (ör. ok gövdesi), en sonda restore. Orijini atlarsanız dönüş canvas’ın (0,0) sol üst köşesine göre yapılır; «neden uçtan dönüyor?» sorusu çoğu zaman budur.

scale(sx, sy) her iki ekseni çarpar; sx = -1 yatay ayna, sy = -1 dikey ayna üretir. Ölçek, hem şekli hem stroked çizgilerin kalınlığını, fillText ile yazılan metnin piksel boyunu da etkiler — bu davranış path geometry’si ile ayrı; ince çizgili, sabit kalınlık UI için ölçeklemeyi path’ten sonra veya ayrı katmanda düşünmek gerekir (bu sayfada sadece farkı netleştiriyoruz).

Çağrı sırası: translate önce mi, rotate önce mi?

translate → rotate → çiz ile rotate → translate → çiz farklı sonuç verir: ikinci durumda önce dönen eksen üzerinde kaydırma yapıldığı için nesne yörünge çizer. Zihinsel kural: matris çarpımı sağdan sola okunur; bağlama eklenen son çağrı, nokta vektörü üzerinde «ilk uygulananmış» gibi düşünülebilir. Emin değilseniz küçük bir kare çizip iki sırayı yan yana deneyin; göz kararı doğrulama canvas geliştirmede yasal yöntemdir.

Karmaşık sahnelerde her kare için resetTransform ve ardından tek setTransform ile hedef matrisi kurmak, onlarca translate birikmesini önler — detay beşinci bölümde.

Coordinate Space Visualizer ile iki sırayı aynı translate / rotate değerleriyle canlı karşılaştırın; yerel eksenler (kırmızı X, yeşil Y) döndükçe uzayın nasıl döndüğünü görün.

const a = Math.PI / 6;

// A: önce konum, sonra dönüş — tipik sprite / ok (orijin etrafında döner)
function drawAtA(ctx, x, y) {
  ctx.save();
  ctx.translate(x, y);
  ctx.rotate(a);
  ctx.fillRect(-10, -4, 20, 8);
  ctx.restore();
}

// B: önce dönüş, sonra öteleme — dönük eksen boyunca kayar
function drawAtB(ctx, x, y) {
  ctx.save();
  ctx.rotate(a);
  ctx.translate(x, y);
  ctx.fillRect(-10, -4, 20, 8);
  ctx.restore();
}
translate → rotate → scale
Coordinate Space Visualizer: Sol üst (0,0) world origin; kırmızı/yeşil world eksenleri sabittir. Nesne üzerindeki parlak eksenler local spacerotate sonrası döner. Sıra düğmeleri translate → rotate ile rotate → translate yörüngesini ayırır. 2D Context Playground çizim API’sine odaklanır; bu demo uzay matematiğine.

Örnek: merkezde dönen ok

Aşağıdaki drawArrow, hedef (x, y) noktasını yeni orijin yapıp açıyı uyguladıktan sonra ok uçunu +x yönünde çizer. Bu, «oyuncu yönü = açı» eşlemesinin en kısa yoludur.

function drawArrow(ctx, x, y, angle) {
  ctx.save();
  ctx.translate(x, y);
  ctx.rotate(angle);
  ctx.fillStyle = '#e8f4ff';
  ctx.beginPath();
  ctx.moveTo(0, 0);
  ctx.lineTo(28, 0);
  ctx.lineTo(20, -6);
  ctx.lineTo(20, 6);
  ctx.closePath();
  ctx.fill();
  ctx.restore();
}

Ölçek ve HUD ipucu

scale(2, 2) ile yaklaşım «zoom» hissi verir; fakat çizgi ve metin de ikiye çıkar. Oyun içi HUD’u dünya ölçeğinden ayırmak için çoğu proje dünya için translate/scale, ardından resetTransform ve ekran sabit UI için ikinci bir çizim bloğu kullanır — tam ayrım State sistemi ve beşinci bölümle birlikte netleşir.

transform, setTransform ve birleşim

2D bağlamda her çizim noktası önce afine dönüşüm matrisi ile çarpılır, sonra bitmap’e yazılır. translate, rotate, scale bu matrisi çarparak günceller; transform(a, b, c, d, e, f) ise doğrudan 2×3 biçiminde ( [a c e] birinci satır, [b d f] ikinci satır, son sütun öteleme) mevcut matrisin üzerine yeni bir dönüşüm ekler.

setTransform(a, b, c, d, e, f) kimlik matrisinden başlar; yani öncekileri yok sayıp verilen matrisi tek başına kurar. resetTransform() da eşdeğer bir «sıfırla»dır: (1,0,0,1,0,0) + cihaz piksel oranı (DPR) uyumunu tarayıcı yönetir. Animasyon döngüsünde her kare başında yalnızca dünya çizimini tekrar kurmak istiyorsanız resetTransform sonra dünya translate’i güvenli temiz başlangıç verir — aksi halde önceki karenin translate birikimi sessizce taşınır.

Kare başı sıfırlama

Aşağıdaki örnek, drawScene çağrılmadan önce matrisin bilinçli olarak temiz olduğunu varsayar. Oyun döngüsünde «her şeyi clearRect sonra çiz» ile birlikte düşünün; yalnızca clear etmek dönüşümü sıfırlamaz.

function drawScene(ctx, camera) {
  ctx.resetTransform();          // önceki karenin matrisi kalmaz
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  ctx.translate(-camera.x, -camera.y);
  drawWorld(ctx);
}

setTransform ile tek seferde kurmak

Bilinen bir ölçek ve kaydırma için üç ayrı scale/rotate/translate yerine tek setTransform hem okunabilir hem hata riskini düşürür. Örnek: sadece yatay aynalama + kaydırma (sprite sheet’te sol bakışı sağa çevirmek).

function drawSpriteFlipped(ctx, image, destX, destY, w, h) {
  ctx.save();
  // sx = -1 → x aynalanır; hedefe hizalamak için öteleme
  ctx.setTransform(-1, 0, 0, 1, destX + w, destY);
  ctx.drawImage(image, 0, 0, w, h, 0, 0, w, h);
  ctx.restore();
}

ctx.getTransform(), desteklenen ortamlarda DOMMatrix döndürür; tıklanan ekran koordinatını dünya koordinatına çevirmek (ters matris) veya isabet testi için ileri seviyede kullanılır — formül ve güvenlik Event coordinates ile bir arada düşünülür.

Özet: dönüşüm fillStyle gibi kalıcı bağlam durumudur. Geçici deneyim için save/restore, kare başı temiz başlangıç için resetTransform / setTransform — ikisinin rolleri State sistemi sayfasında birbirine bağlanır.

Resize ile birlikte düşünün

canvas.width değişince bağlam durumu (dönüşüm dahil) sıfırlanır; piksel oranı ve CSS kutusu ile uyumlu yeniden boyut için Resize mantığı sayfasına geçin. Bu sayfa koordinat sözleşmesini sabitler; cihazlar arası keskinlik orada ele alınır.

Piksel hizalama: keskin 1 px çizgiler

lineWidth = 1 ile tam sayı koordinatta (10, 20) çizilen yatay veya dikey çizgi, piksel ızgarasının iki satırına yayılır ve bulanık görünür. Çözüm: eksene dik çizgide koordinatı n + 0.5 yapmak (ör. ctx.moveTo(0, 40.5)). Bu, canvas’ın ayrık piksel dünyasına özgü bir ayrıntıdır; vektör grafik editörlerindeki «snap» alışkanlığının kod karşılığıdır.

2D context sayfasında ince kontur uyarısı bu yüzden koordinat sayfasına bağlanmıştı. Dönüşüm uygulandıktan sonra (özellikle scale) yarım piksel kuralı bozulabilir; HUD ve grid çizgilerinde dönüşüm öncesi/sonrası katman ayırmak işe yarar.

ctx.strokeStyle = '#5ec8ff';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, 40.5);
ctx.lineTo(canvas.width, 40.5);
ctx.stroke();

Olay koordinatı: pencere ≠ canvas

Fare olaylarındaki clientX / clientY (veya offsetX) viewport uzayındadır; canvas bitmap uzayı değil. CSS ile ölçeklenmiş canvas’ta tıklama konumunu çizim koordinatına çevirmek için öğenin ekrandaki dikdörtgeni ile iç width/height oranı kullanılır — formül ve kenar durumları Event coordinates sayfasının konusudur.

Bu sayfada hatırlatma: çizim mantığınızı canvas.width ızgarasında yazın; girdi katmanı geldiğinde aynı ızgaraya map edin. İki uzayı karıştırmak «sprite tıklanmıyor» hatasının en yaygın nedenlerinden biridir.

  • Çizim: attribute width/height piksel uzayı.
  • Dönüşüm: translate → rotate → draw kalıbı; geri almak için restore.
  • İnce çizgi: eksene dik n + 0.5 hizalama.
  • Fare: viewport → canvas dönüşümü ayrı başlık.

Three.js ile çakışmadan okumak

2D canvas vs 3D sahne ekseni

Three.js’te nesne position, rotation, scale ile sahne grafiğinde taşınır; canvas 2D’de aynı fikir translate/rotate/scale ile bağlam matrisinde yaşar — fakat tek düzlem ve sol üst orijin sabittir. 2D HUD overlay’i ayrı canvas’ta tutmak, WebGL NDC uzayı ile piksel uzayını karıştırmamayı sağlar. ByteOmi Koordinat sistemleri 3D katmanını anlatır; bu sayfa 2D bitmap eksenidir.

Sıradaki: resize ve piksel oranı

Eksen ve dönüşüm netleştikten sonra, canvas’ın fiziksel piksel boyutunu pencere ve devicePixelRatio ile eşlemek Resize mantığı sayfasına geçer. Önceki adımlar: 2D context, Canvas oluşturma.