HTML5 Canvas ve WebGL ile Performanslı Çizim Rehberi

# Rehber: HTML5 Canvas ve WebGL ile Performanslı Çizimler Tarayıcıda yoğun grafik işleri yapıyorsanız iki temel API vardır: 2D bağlamı sunan Canvas A...

HTML5 Canvas ve WebGL ile Performanslı Çizim Rehberi

Rehber: HTML5 Canvas ve WebGL ile Performanslı Çizimler

Tarayıcıda yoğun grafik işleri yapıyorsanız iki temel API vardır: 2D bağlamı sunan Canvas API ve GPU'ya doğrudan erişim sağlayan WebGL. Bu rehber, orta seviye bir geliştiricinin her iki API'yi de bilinçli biçimde seçip kullanabilmesi için pratik kararlara odaklanır.

Önkoşullar ve Kapsam

  • HTML, CSS ve JavaScript (ES2015+) bilgisi.
  • Tarayıcı DOM API'lerine aşinalık.
  • WebGL bölümü için temel matris/vektör kavramları (model, view, projection matrisleri) faydalıdır; burada matematik kütüphanesi kullanmadan ham WebGL API'sine odaklanacağız.
  • Kapsam: Canvas 2D performans pratikleri ve WebGL'in temel çalışma modeli. Kütüphaneler (Three.js, PixiJS vb.) bu rehberin dışındadır; amaç API'nin kendisini anlamaktır.
  • Sürüm notu: WebGL 1.0 ve WebGL 2.0 tarayıcı desteği yaygındır. WebGL 2.0, WebGL 1.0'ın üzerine OpenGL ES 3.0 tabanlı özellikler ekler. Hedef kitleniz eski cihazları içeriyorsa WebGL 1.0 ile başlayıp özellik algılaması yapmanız güvenli olur.

Canvas 2D: Nerede Güçlü, Nerede Zayıf

Canvas 2D, çizim komutlarını sıralı biçimde işleyen bir API'dir. UI katmanları, grafikler, oyun HUD'ları ve 2D görselleştirmeler için yeterlidir. Ancak her fillRect, drawImage veya stroke çağrısı bir maliyet üretir; binlerce nesne çizildiğinde CPU tarafı darboğaza girer.

1. Bağlamı Bir Kez Alın

getContext('2d') çağrısı her seferinde yeni bir bağlam üretmez, aynı bağlamı döndürür; yine de döngü içinde çağırmak gereksizdir. Bağlamı bir kez alıp referansı saklayın.

const canvas = document.getElementById('scene');
const ctx = canvas.getContext('2d');

2. Çizim Döngüsünü requestAnimationFrame ile Yönetin

setInterval yerine requestAnimationFrame (rAF) kullanın; tarayıcı sekme görünür değilken rAF çağrıları durur ve kare hızı ekran tazeleme hızına göre hizalanır.

let last = 0;

function frame(t) {
  const dt = t - last;
  last = t;
  update(dt);
  render();
  requestAnimationFrame(frame);
}

requestAnimationFrame(frame);

Beklenen davranış: t parametresi yüksek çözünürlüklü zaman damgasıdır; dt ile hareketi zamana bağlı hesaplarsanız farklı kare hızlarında tutarlı hız elde edersiniz.

3. Yüksek DPI Ekranlarda Ölçekleme

Canvas'ın CSS boyutu ile çizim yüzeyi boyutu farklıdır. Retina benzeri ekranlarda bulanıklığı önlemek için çizim yüzeyini devicePixelRatio ile ölçekleyin.

function resize(canvas, ctx) {
  const dpr = window.devicePixelRatio || 1;
  const rect = canvas.getBoundingClientRect();
  canvas.width = Math.round(rect.width * dpr);
  canvas.height = Math.round(rect.height * dpr);
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}

setTransform ile ölçek uyguladıktan sonra çizim koordinatlarını CSS pikseli cinsinden verebilirsiniz. Pencere yeniden boyutlandığında bu fonksiyonu tekrar çağırın; aksi hâlde çizim gerilir veya bulanıklaşır.

4. Katmanlama ve Offscreen Canvas

Statik arka planı her karede yeniden çiziyorsanız gereksiz iş yapıyorsunuz. Arka planı ayrı bir OffscreenCanvas veya gizli <canvas> üzerine bir kez çizip ana sahneye drawImage ile aktarın.

const bg = document.createElement('canvas');
bg.width = canvas.width;
bg.height = canvas.height;
const bgCtx = bg.getContext('2d');
drawBackground(bgCtx);

// her karede:
ctx.drawImage(bg, 0, 0, canvas.width, canvas.height);

5. Toplu Çizim ve Durum Değişikliklerini Azaltma

ctx.fillStyle, ctx.strokeStyle, ctx.font gibi atamalar GPU/CPU tarafında durum değişikliği tetikler. Aynı stile sahip nesneleri gruplayarak çizin. beginPath()/fill() çağrılarını nesne başına değil, uygun gruplar hâlinde yapın.

ctx.fillStyle = '#3b82f6';
ctx.beginPath();
for (const p of points) {
  ctx.moveTo(p.x + 2, p.y);
  ctx.arc(p.x, p.y, 2, 0, Math.PI * 2);
}
ctx.fill();

6. Piksel Tabanlı Erişimde ImageData

getImageData/putImageData çağrıları yavaştır çünkü GPU ile CPU arasında veri transferi yapılır. Piksel bazlı efektler için tam kare üzerinde değil, gereken bölge üzerinde çalışın. putImageData çağrısı dönüşüm matrisini yok sayar; konumlandırmayı kendiniz yönetmelisiniz.

WebGL: GPU'ya Geçiş

WebGL, GPU üzerinde çalışan shader programları üzerinden çizim yapar. Yüzlerce/binlerce nesne, parçacık sistemi, 3D sahne veya yoğun görüntü işleme söz konusu olduğunda CPU tabanlı Canvas 2D'ye göre belirgin biçimde avantajlıdır. Buna karşılık API düşük seviyelidir; kaynak yönetimi, hata ayıklama ve durum takibi size aittir.

1. Bağlamı Alın ve Hata Durumunu Kontrol Edin

const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
if (!gl) {
  // WebGL desteklenmiyor: kullanıcıya mesaj gösterin veya Canvas 2D'ye düşün.
}

WebGL 2.0 mevcutsa onu tercih edin; değilse WebGL 1.0'a düşün. Shader kodunuzda sürüme bağlı sözdizimi farkları olduğunu unutmayın (#version 300 es yalnızca WebGL 2.0'da geçerlidir).

2. Shader Programını Derleyin ve Bağlantılayın

Shader derleme ve program bağlama işlemi pahalıdır; bunu kare döngüsünde değil, başlangıçta bir kez yapın.

function compile(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    const log = gl.getShaderInfoLog(shader);
    gl.deleteShader(shader);
    throw new Error('Shader derleme hatası: ' + log);
  }
  return shader;
}

function link(gl, vsSource, fsSource) {
  const vs = compile(gl, gl.VERTEX_SHADER, vsSource);
  const fs = compile(gl, gl.FRAGMENT_SHADER, fsSource);
  const program = gl.createProgram();
  gl.attachShader(program, vs);
  gl.attachShader(program, fs);
  gl.linkProgram(program);
  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    const log = gl.getProgramInfoLog(program);
    gl.deleteProgram(program);
    throw new Error('Program bağlama hatası: ' + log);
  }
  gl.deleteShader(vs);
  gl.deleteShader(fs);
  return program;
}

Beklenen davranış: Derleme/bağlama hataları sessizce yutulmaz; getShaderInfoLog ve getProgramInfoLog ile tanı mesajı alırsınız. Shader'ları program bağlandıktan sonra silmek güvenlidir; GPU tarafındaki derlenmiş kopya korunur.

3. Tamponları ve Attribute'ları Kurun

Vertex verilerini bir kez yükleyip drawArrays/drawElements ile çizin. Statik veriler için gl.STATIC_DRAW, sık güncellenen veriler için gl.DYNAMIC_DRAW kullanın.

const positions = new Float32Array([
  -0.5, -0.5,
   0.5, -0.5,
   0.0,  0.5,
]);

const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);

const loc = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);

4. Viewport ve Temizleme

gl.viewport çağrısı canvas boyutuna göre ayarlanmalıdır. Canvas yeniden boyutlandığında viewport'u da güncelleyin; aksi hâlde çizim yanlış ölçeklenir.

gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);

5. Kare Döngüsü ve Durum Değişiklikleri

WebGL'de her useProgram, bindBuffer, uniform* çağrısı bir durum değişikliğidir. Aynı programı kullanan nesneleri gruplayın; uniform değerlerini kare başına bir kez güncelleyin.

function frame(t) {
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.useProgram(program);
  // uniform güncellemeleri
  gl.drawArrays(gl.TRIANGLES, 0, 3);
  requestAnimationFrame(frame);
}

Canvas 2D mi, WebGL mi?

Karar vermek için şu soruları sorun:

  • Nesne sayısı birkaç yüzün altında ve stil/şekil çeşitliliği yüksek mi? Canvas 2D genellikle yeterlidir.
  • Aynı türden binlerce nesne veya parçacık mı var? WebGL belirgin kazanç sağlar.
  • Piksel bazlı görüntü işleme mi yapıyorsunuz? WebGL fragment shader ile paralelleştirmek CPU tabanlı ImageData döngüsünden çok daha hızlıdır.
  • Ekip deneyimi ve bakım maliyeti önemli mi? WebGL karmaşıklığı daha yüksektir; gerekmiyorsa 2D ile başlamak mantıklıdır.

Yaygın Hatalar

  • Kare döngüsünde nesne oluşturmak. Her karede yeni Path2D, Float32Array veya buffer üretmek çöp toplayıcı baskısı yaratır. Yeniden kullanın.
  • requestAnimationFrame yerine setInterval kullanmak. Sekme arka plandayken boşa çalışır ve ekran tazeleme hızıyla senkronize olmaz.
  • DPI ölçeklemesini atlamak. Retina ekranlarda çıktı bulanık görünür.
  • WebGL'de hata kontrolü yapmamak. Derleme/bağlama hatalarını sessizce geçmek hata ayıklamayı imkânsız kılar.
  • Shader'ları kare başına yeniden derlemek. Derleme maliyeti yüksektir; başlangıçta bir kez yapın.
  • gl.getError() çağrısını döngü içinde yapmak. Bu çağrı senkronizasyon noktası oluşturur ve performansı düşürür; yalnızca geliştirme sırasında, kontrollü biçimde kullanın.
  • Canvas'ı CSS ile ölçekleyip width/height attribute'unu güncellememek. Çizim yüzeyi ile görüntülenen boyut uyuşmaz.

Güvenlik Etkileri

  • Shader kaynakları kullanıcı girdisinden üretilmemeli. Kullanıcıdan gelen metni doğrudan shader kaynağına gömmek, enjeksiyon benzeri sorunlara ve beklenmedik GPU yüküne yol açabilir.
  • getImageData ile çekilen pikseller CORS kurallarına tabidir. Başka bir kaynaktan yüklenen görüntüyü canvas'a çizip getImageData çağırırsanız, kaynak crossOrigin ile izin vermedikçe tarayıcı güvenlik hatası verir. Kullanıcı tarafından yüklenen görsellerde bu davranışı hesaba katın.
  • GPU kaynaklarını serbest bırakın. deleteBuffer, deleteProgram, deleteTexture çağrılarını yapmamak, özellikle tek sayfa uygulamalarında bellek sızıntısına yol açar.
  • preserveDrawingBuffer seçeneğini bilinçli kullanın. Varsayılan olarak false'tur ve performans için tercih edilir; ancak canvas.toDataURL gibi işlemler için içeriğin korunması gerekebilir.

Sonuç

Canvas 2D ve WebGL birbirinin rakibi değil, farklı ölçeklerin araçlarıdır. Canvas 2D'de kazanç; bağlamı bir kez almak, DPI ölçeklemesini doğru yapmak, katmanlamak ve durum değişikliklerini gruplamaktan gelir. WebGL'de kazanç; shader ve buffer'ları başlangıçta kurmak, kare döngüsünü yalın tutmak ve hata kontrolünü ihmal etmemekten gelir. Doğru kararı vermek için önce ölçün: nesne sayısı, kare süresi ve GPU/CPU kullanımı hangi API'nin gerektiğini size söyler.

İLGİLİ İÇERİKLER

Bunları da Okumak İsteyebilirsiniz