Canvas'a resim çizmek için drawImage(), piksel okumak için getImageData(), değiştirilmiş pikselleri geri yazmak için putImageData(), sonucu dosyaya çevirmek için toBlob() veya toDataURL() kullanırsınız. Bu zincirin kırılgan halkası güvenliktir: başka bir kökenden CORS izni olmadan yüklenmiş bir resim canvas'ı "kirletir" (tainted) ve ondan sonra pikselleri okumaya ya da dışa aktarmaya çalışmak SecurityError ile sonuçlanır.

Bu yazıda resim kaynaklarını ve yüklenmeyi beklemeyi, drawImage() imzalarını, ImageData dizisinin yapısını, gri tonlama ve renk ters çevirme filtrelerini, canvas'ı PNG veya JPEG dosyasına dönüştürmeyi, kirli canvas kuralını ve ağır işi ana iş parçacığından almak için OffscreenCanvas kullanımını göreceksiniz. Temel çizim komutları Canvas çizim rehberinde anlatılıyor.

Hangi kaynaklar çizilebilir?

MDN, canvas'ın resim kaynağı olarak kabul ettiği türleri şöyle sıralar: HTMLImageElement (Image() yapıcısıyla oluşturulanlar ve <img> elemanları), SVGImageElement, HTMLVideoElement (videonun o anki karesi kullanılır), başka bir HTMLCanvasElement, ImageBitmap, OffscreenCanvas ve VideoFrame. SVG resimlerinde kök <svg> elemanında genişlik ve yükseklik belirtilmiş olmalıdır.

Resmin yüklenmesini beklemek

Resim henüz yüklenmeden drawImage() çağırırsanız hiçbir şey çizilmez; MDN eski tarayıcıların istisna bile fırlatabileceğini belirtir. Bu yüzden çizimi load olayına bağlayın:

javascript
const ctx = document.getElementById("canvas").getContext("2d");
const img = new Image();

img.addEventListener("load", () => {
  ctx.drawImage(img, 0, 0);
});

img.src = "/img/urun.png"; // dinleyiciden sonra adres verilir

Birden fazla resim kullanıyorsanız MDN, hepsinin yüklenmesini Promise.all ile beklemeyi gösterir; her resmin load olayı bir söze (promise) çevrilir. Sözlerin kendisi async/await yazısında anlatılıyor. Küçük resimler için data: adresi de kullanılabilir; MDN bunun ek bir sunucu isteği gerektirmediğini ve CSS, JavaScript, HTML ile resimleri tek bir dosyada toplayarak taşınabilirliği artırabileceğini, ama resmin önbelleğe alınmadığını ve büyük resimlerde adresin çok uzadığını belirtir.

drawImage() imzaları

drawImage() imzaları: drawImage(img, dx, dy), drawImage(img, dx, dy, dW, dH), drawImage(img, sx, sy, sW, sH, dx, dy, dW, dH)
İmzaNe yapar
drawImage(img, dx, dy)Resmi olduğu boyutta (dx, dy) noktasına çizer
drawImage(img, dx, dy, dW, dH)Resmi dW x dH boyutuna ölçekleyerek çizer
drawImage(img, sx, sy, sW, sH, dx, dy, dW, dH)Kaynaktan (sx, sy, sW, sH) dilimini kesip hedefte (dx, dy, dW, dH) alanına çizer

Dokuz parametreli sürümde ilk dört sayı kaynak resim üzerindeki dilimi, son dört sayı hedef canvas üzerindeki dikdörtgeni tanımlar. MDN bunun bütün parçaları tek bir resim dosyasında tutup (sprite) gerekeni kesip çizmek için kullanışlı olduğunu, her resmi ayrı yüklemek zorunda kalmadığınız için yükleme performansına da yardım edebileceğini belirtir:

javascript
// Gergedan resminden başını kesip çerçeveye yerleştir (MDN örneği)
ctx.drawImage(document.getElementById("source"), 33, 71, 104, 124, 21, 20, 87, 104);
ctx.drawImage(document.getElementById("frame"), 0, 0);

drawImage() resimde görüntü verisi yoksa veya canvas ya da kaynak dikdörtgeninin genişliği ya da yüksekliği sıfırsa InvalidStateError, null veya undefined bir resim verilirse TypeMismatchError fırlatır. Ölçekleme yaparken metin içeren resimler bulanıklaşabilir. Ölçeklemede kullanılan yumuşatmayı imageSmoothingEnabled özelliğiyle kapatabilirsiniz; varsayılan değeri true olduğu için resimler ölçeklenirken yumuşatılır. Piksel sanatı gibi keskin kenarlı görüntülerde false yapmak blok görünümü korur.

MDN'nin optimizasyon önerileri de burada geçerlidir: resimleri her karede drawImage() içinde ölçeklemek yerine farklı boyutlarını yükleme sırasında ekran dışı bir canvas'a önbelleğe alın ve ondalıklı koordinatlardan kaçının. drawImage(myImage, 0.3, 0.5) gibi bir çağrı alt piksel çizimine yol açar ve tarayıcıyı kenar yumuşatma için ek hesap yapmaya zorlar; koordinatları Math.floor() ile yuvarlamak bunu önler.

ImageData: piksellerin dizisi

ImageData nesnesi canvas'ın bir alanının ham piksel verisini temsil eder. data özelliği bir Uint8ClampedArray (istenirse Float16Array) döndürür. Her piksel sırasıyla kırmızı, yeşil, mavi ve alfa olmak üzere dört bayttan oluşur (RGBA). Dizi sol üst pikselin kırmızı değeriyle başlar, satır satır soldan sağa ve yukarıdan aşağıya ilerler; toplam uzunluk yükseklik x genişlik x 4 olur. (x, y) pikselinin kırmızı değerinin indeksi y * (genişlik * 4) + x * 4 formülüyle bulunur.

Aşağıdaki kod bu hesabı ve Uint8ClampedArray davranışını gösterir. Canvas gerektirmediği için Node.js ile çalıştırıldı; çıktılar gerçek sonuçlardır:

javascript
const genislik = 4;
const yukseklik = 2;
const data = new Uint8ClampedArray(genislik * yukseklik * 4);
console.log(data.length); // 32

function indeks(x, y, w) {
  return y * (w * 4) + x * 4;
}
console.log(indeks(0, 0, genislik), indeks(3, 0, genislik), indeks(1, 1, genislik)); // 0 12 20

const i = indeks(1, 1, genislik);
data[i] = 300;       // R: 255'e kırpılır
data[i + 1] = -20;   // G: 0'a kırpılır
data[i + 2] = 127.5; // B: yuvarlanır
data[i + 3] = 255;   // A
console.log(JSON.stringify(Array.from(data.slice(i, i + 4)))); // [255,0,128,255]

// Ters çevirme: her renk kanalını 255'ten çıkar, alfaya dokunma
for (let k = 0; k < data.length; k += 4) {
  data[k] = 255 - data[k];
  data[k + 1] = 255 - data[k + 1];
  data[k + 2] = 255 - data[k + 2];
}
console.log(JSON.stringify(Array.from(data.slice(i, i + 4)))); // [0,255,127,255]
console.log(JSON.stringify(Array.from(data.slice(0, 4))));     // [255,255,255,0]

Çıktıdaki iki ayrıntı önemlidir. Birincisi, Uint8ClampedArray 0 ile 255 dışındaki değerleri kırpar; bu yüzden filtre hesaplarında taşma için ayrıca kontrol yazmanız gerekmez. İkincisi, boş bir dizide bütün pikseller saydam siyahtır (0,0,0,0); ters çevirme renk kanallarını 255 yapsa da alfa 0 kaldığı için piksel hâlâ görünmez. MDN'nin ters çevirme örneği de alfa kanalına dokunmaz.

ImageData oluşturmanın yolları

  • ctx.createImageData(genislik, yukseklik): bütün pikselleri saydam olan yeni ve boş bir nesne.
  • ctx.createImageData(baskaImageData): aynı boyutta boş bir nesne. MDN özellikle uyarır: bu çağrı veriyi kopyalamaz.
  • ctx.getImageData(sx, sy, sw, sh): canvas'taki alanın piksellerinin kopyası.
  • new ImageData(genislik, yukseklik) veya new ImageData(dizi, genislik): yapıcıyla oluşturma; MDN bunu worker içinde tercih edilen yol olarak anar, çünkü orada createImageData() yoktur.

Yapıcıya dizi verirseniz uzunluğu piksel başına bayt sayısı ile genişlik ve yüksekliğin çarpımına eşit olmalıdır; değilse IndexSizeError fırlatılır. Yüksekliği vermezseniz dizi uzunluğundan hesaplanır: MDN'nin örneğinde 40.000 elemanlı bir dizi ve 200 genişlik, 50 yükseklik verir.

getImageData ve putImageData ile filtre

getImageData ve putImageData ile filtre

getImageData(sx, sy, sw, sh) belirtilen dikdörtgenin piksellerini döndürür. Canvas dönüşüm matrisinden etkilenmez; dikdörtgen canvas dışına taşarsa dışarıdaki pikseller saydam siyah olarak gelir. sw veya sh sıfırsa IndexSizeError fırlatılır. putImageData(imageData, dx, dy) ise veriyi canvas'a yazar ve o da dönüşüm matrisinden etkilenmez. İsteğe bağlı "kirli dikdörtgen" parametreleri (dirtyX, dirtyY, dirtyWidth, dirtyHeight) ile verinin yalnızca bir bölümünü yazabilirsiniz. MDN'ye göre bu yöntem argümanlardan biri sonsuzsa NotSupportedError, ImageData nesnesinin verisi ayrılmışsa (detached) InvalidStateError fırlatır.

javascript
function griTonla(ctx, canvas, img) {
  ctx.drawImage(img, 0, 0);
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const data = imageData.data;
  for (let i = 0; i < data.length; i += 4) {
    const ort = (data[i] + data[i + 1] + data[i + 2]) / 3;
    data[i] = ort;     // kırmızı
    data[i + 1] = ort; // yeşil
    data[i + 2] = ort; // mavi
  }
  ctx.putImageData(imageData, 0, 0);
}

Bu fonksiyon üç kanalın ortalamasını alır. MDN, ağırlıklı ortalama için 0.299r + 0.587g + 0.114b formülünü de örnek verir; aynı eğitimde sepya filtresi için katsayılı bir hesap ve Math.min(..., 255) sınırlaması da gösterilir. Fare altındaki rengi okumak için tek piksellik bir alan yeterlidir: ctx.getImageData(x, y, 1, 1).data dört değerlik bir dizi döndürür ve MDN'nin renk seçici örneği bunu rgb() metnine çevirir.

Kirli canvas ve CORS

Başka bir kökenden gelen resimler varsayılan olarak canvas'ı kirletir. Kirli bir canvas üzerinde getImageData(), toBlob() ve toDataURL() çağrıları SecurityError fırlatır; MDN bunu canvas bit eşleminin "origin-clean" olmaması diye tanımlar. Çözüm resmi CORS ile istemektir: <img> elemanının crossorigin özniteliği ya da onu yansıtan HTMLImageElement.crossOrigin özelliği ayarlanır. Resmi barındıran sunucu çapraz kökenli erişime izin veriyorsa resim canvas'ı kirletmeden kullanılabilir. MDN'nin piksel örnekleri tam da bu yüzden img.crossOrigin = "anonymous" satırıyla başlar:

javascript
const img = new Image();
img.crossOrigin = "anonymous"; // MDN örneklerinde src'den önce gelir
img.src = "https://cdn.example.com/foto.jpg";
img.addEventListener("load", () => {
  ctx.drawImage(img, 0, 0);
  const piksel = ctx.getImageData(0, 0, 1, 1).data; // sunucu izin verdiyse çalışır
});

crossOrigin değeri anonymous veya use-credentials olabilir. Ayarın işe yaraması için sunucunun uygun Access-Control-Allow-Origin yanıtını vermesi gerekir; bu başlıkların ayrıntısı CORS nedir yazısında, tarayıcıda görülen hata iletileri CORS hataları yazısında anlatılıyor. Video kareleri için de aynı kural geçerlidir; MDN'nin video işleme örneğindeki <video> elemanı crossorigin="anonymous" özniteliği taşır.

Canvas'ı dosyaya çevirmek: toBlob ve toDataURL

toDataURL(type, quality) görüntüyü bir data: adresi olarak döndürür; toBlob(callback, type, quality) ise geri çağırmaya bir Blob verir. Tür verilmezse veya desteklenmiyorsa ikisi de image/png üretir. Tarayıcıların PNG'yi desteklemesi zorunludur; çoğu image/jpeg ve image/webp türlerini de destekler. quality, kayıplı biçimlerde 0 ile 1 arası bir sayıdır. Oluşan görüntü, çözünürlük bilgisi taşıyabilen biçimlerde 96 dpi olur.

javascript
const canvas = document.getElementById("canvas");

canvas.toBlob(
  (blob) => {
    if (!blob) return; // görüntü oluşturulamadı
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = "cizim.jpg";
    a.click();
  },
  "image/jpeg",
  0.9,
);

MDN, toDataURL() için açık bir uyarı yapar: bütün görüntüyü bellekte tek bir metne kodlar, büyük görüntülerde performansı etkiler ve img.src değerine atandığında tarayıcının adres uzunluğu sınırını aşabilir. Bu yüzden genellikle toBlob() ile URL.createObjectURL() birlikte tercih edilmelidir. Canvas'ın genişliği veya yüksekliği 0 ise ya da azami boyutu aşıyorsa toDataURL() "data:," metnini döndürür. toBlob() geri çağırmasına ise görüntü herhangi bir sebeple oluşturulamazsa null gelebilir. Ayrıca toDataURL("image/webp") sonucu data:image/png ile başlıyorsa istediğiniz biçim desteklenmiyor demektir.

MDN'nin toBlob() örneği nesne adresini resim yüklendikten hemen sonra iptal etmez, çünkü bu, kullanıcının resmi sağ tıklayıp kaydetmesi gibi etkileşimleri bozar; uzun yaşayan uygulamalarda adresleri artık gerekmediğinde URL.revokeObjectURL() ile serbest bırakmanızı önerir. İndirme bağlantısı için oluşturduğunuz adresi de iş bittiğinde aynı yöntemle bırakın. Blob ve nesne adresleri File API ve Blob yazısında ayrıntılı anlatılıyor.

OffscreenCanvas: çizimi ana iş parçacığından almak

OffscreenCanvas: çizimi ana iş parçacığından almak: Senkron, Asenkron

Canvas çizimi, animasyonu ve kullanıcı etkileşimi normalde ana iş parçacığında olur; ağır çizim sayfanın tepkiselliğini düşürebilir. OffscreenCanvas ekranda olmayan bir canvas sağlar, DOM ile Canvas API arasındaki bağı gevşetir ve worker içinde de çalışır. Aktarılabilir (transferable) bir nesnedir. İki kullanım şekli vardır:

  • Senkron: new OffscreenCanvas(256, 256) ile oluşturup çizersiniz, transferToImageBitmap() ile kareyi bir ImageBitmap'e çevirir ve görünür bir canvas'ın "bitmaprenderer" bağlamında transferFromImageBitmap() ile gösterirsiniz.
  • Asenkron: sayfadaki canvas için transferControlToOffscreen() çağırıp dönen nesneyi worker'a aktarırsınız; worker'ın çizdikleri doğrudan sayfadaki canvas'ta görünür.
javascript
// main.js
const htmlCanvas = document.getElementById("canvas");
const offscreen = htmlCanvas.transferControlToOffscreen();
const worker = new Worker("cizici.js");
worker.postMessage({ canvas: offscreen }, [offscreen]);

// cizici.js
onmessage = (evt) => {
  const ctx = evt.data.canvas.getContext("2d");
  function render(time) {
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    // çizim komutları
    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);
};

transferControlToOffscreen(), canvas için daha önce getContext() çağrıldıysa ya da denetim zaten devredildiyse InvalidStateError fırlatır; yani aktarımı bağlam almadan önce yapmalısınız. postMessage çağrısındaki ikinci parametre nesneyi kopyalamak yerine aktarır; aktarılabilir nesneler Web Workers yazısında anlatılıyor. OffscreenCanvas görüntüyü convertToBlob() ile söz döndüren bir biçimde dosyaya çevirebilir. Worker içindeki kare döngüsü için requestAnimationFrame yazısına bakın. Tarayıcı desteği için MDN uyumluluk tablosuna bakın.

Video karelerini işlemek

Bir <video> elemanı da drawImage() kaynağıdır ve o anki kare çizilir. MDN'nin "yeşil perde" (chroma key) örneği her karede videoyu ilk canvas'a çizer, getImageData() ile pikselleri alır, belirli renk aralığındaki piksellerin alfa değerini 0 yapar ve sonucu putImageData() ile ikinci canvas'a yazar. İkinci canvas'ın arka plan resmi böylece saydam kalan alanlarda görünür. Kamera görüntüsünden fotoğraf almak da aynı fikre dayanır; konu getUserMedia yazısında anlatılıyor.

Sık yapılan hatalar

  • Resim yüklenmeden çizmek. load olayını bekleyin; önce dinleyiciyi, sonra src değerini verin.
  • Çapraz kökenli resimde crossOrigin ayarlamamak. Canvas kirlenir ve okuma SecurityError verir; MDN örneklerinde bu satır src atamasından önce gelir.
  • createImageData(digeri) ile kopya beklemek. Yalnızca aynı boyutta boş bir nesne döner.
  • Alfa kanalını unutmak. Saydam piksellerin renklerini değiştirmek görünür sonuç vermez.
  • Büyük görüntülerde toDataURL() kullanmak. toBlob() ve nesne adresi tercih edin.
  • Aktarımdan önce getContext() çağırmak. transferControlToOffscreen() bu durumda hata fırlatır.

Sık Sorulan Sorular

Kirli canvas'ı temizleyebilir miyim?

Kaynaklarda bir "temizleme" yöntemi tanımlanmaz. Çapraz kökenli resmi baştan CORS ile, yani crossOrigin ayarlayarak ve sunucunun izniyle yüklemek gerekir. Kendi sunucunuzdaki resimler aynı kökenden geldiği için bu sorunu yaşatmaz.

Canvas'tan aldığım Blob'u sunucuya nasıl gönderirim?

toBlob() ile aldığınız Blob nesnesini bir FormData içine ekleyip fetch ile gönderebilirsiniz. Form verisi FormData yazısında, istek gönderme Fetch API yazısında anlatılıyor.

JPEG kalitesini nasıl ayarlarım?

toBlob(callback, "image/jpeg", 0.95) veya toDataURL("image/jpeg", 0.5) şeklinde üçüncü ya da ikinci parametreye 0 ile 1 arası bir sayı verin. Değer aralık dışındaysa veya verilmezse tarayıcı kendi varsayılan kalitesini kullanır.

Pikselleri okumak neden yavaş olabilir?

getImageData() her çağrıda pikselleri kopyalar. Sık okuma gerekiyorsa bağlamı willReadFrequently: true ile oluşturmak ve okunan alanı olabildiğince küçük tutmak iki pratik önlemdir.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Web API öğrenme rehberi · Canvas API çizim rehberi · File API ve Blob · CORS nedir · Web Workers · getUserMedia ile kamera