← Kursa Dön
📄 Text · 30 min

Canvas ve Animasyon

Piksel Piksel Çizmek

Web sayfaları genelde kutulardan oluşur: <div>, <p>, <span> — hepsi dikdörtgen kutular. Ama ya bir yıldız çizmek, bir grafik oluşturmak, bir oyun yapmak istersen? HTML elementleriyle bunu yapmak ya imkansız ya da aşırı verimsiz. İşte Canvas: tarayıcıda piksel seviyesinde çizim yapmanı sağlayan bir API.

Canvas'ı bir tuval olarak düşün — gerçek anlamda. Ressamın elindeki tuval gibi: boş bir yüzey, üzerine ne istersen çizersin. Çizgiler, daireler, dikdörtgenler, metinler, görseller, gradyanlar... Hepsi JavaScript ile, piksel piksel kontrol altında.

Bu derste Canvas 2D API'nin temellerini öğrenecek, ardından requestAnimationFrame ile akıcı animasyonlar oluşturacak ve dersin sonunda basit bir oyun mekaniği kuracağız.


Canvas'a Giriş

Temel Kurulum

<canvas id="game" width="800" height="600"></canvas>
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d"); // 2D çizim bağlamı

// Canvas boyutunu ayarla (piksel cinsinden)
canvas.width = 800;
canvas.height = 600;

// ⚠️ CSS ile boyutlandırma canvas'ı UZATIR, piksel sayısını DEĞİŞTİRMEZ
// canvas { width: 100%; } → Bulanık görüntü! Doğru yol:
function resizeCanvas() {
  canvas.width = canvas.clientWidth * window.devicePixelRatio;
  canvas.height = canvas.clientHeight * window.devicePixelRatio;
  ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
}

⚠️ Dikkat: Canvas'ın width/height attribute'ları çizim alanının piksel çözünürlüğünü belirler. CSS width/height ise görüntü boyutunu belirler. Bu ikisi farklıysa görüntü bulanıklaşır. Retina ekranlarda devicePixelRatio ile çarparak düzelt.

Koordinat Sistemi

Canvas'ın koordinat sistemi sol üst köşeden başlar:

  • x: Soldan sağa artar (0 → width)

  • y: Yukarıdan aşağı artar (0 → height)

(0,0) ────────────────────→ x (800)
  │
  │     Canvas Alanı
  │
  ↓
  y (600)

Temel Şekiller

Dikdörtgen

Canvas'ın doğrudan çizebileceği tek şekil dikdörtgendir. Diğer tüm şekiller path (yol) ile çizilir:

// Dolu dikdörtgen
ctx.fillStyle = "#3498db";
ctx.fillRect(50, 50, 200, 100); // x, y, genişlik, yükseklik

// Çerçeve dikdörtgen
ctx.strokeStyle = "#e74c3c";
ctx.lineWidth = 3;
ctx.strokeRect(300, 50, 200, 100);

// Dikdörtgen alanı temizle (silgi gibi)
ctx.clearRect(100, 70, 50, 50); // İçini temizler

Çizgi ve Path

// Basit çizgi
ctx.beginPath();
ctx.moveTo(50, 200);     // Kalemi buraya koy
ctx.lineTo(300, 200);    // Buraya çiz
ctx.lineTo(300, 350);    // Buraya devam et
ctx.strokeStyle = "#2ecc71";
ctx.lineWidth = 2;
ctx.stroke();            // Çizgiyi göster

// Kapalı şekil (üçgen)
ctx.beginPath();
ctx.moveTo(400, 350);
ctx.lineTo(500, 200);
ctx.lineTo(600, 350);
ctx.closePath();          // Başlangıca geri dön
ctx.fillStyle = "#f39c12";
ctx.fill();               // İçini doldur
ctx.stroke();             // Kenarları çiz

Daire ve Yay

// Tam daire
ctx.beginPath();
ctx.arc(
  150, 450,      // Merkez (x, y)
  60,             // Yarıçap
  0,              // Başlangıç açısı (radyan)
  Math.PI * 2     // Bitiş açısı (tam daire = 2π)
);
ctx.fillStyle = "#9b59b6";
ctx.fill();

// Yarım daire (pasta dilimi)
ctx.beginPath();
ctx.arc(350, 450, 60, 0, Math.PI); // 0 → π = yarım daire
ctx.closePath();
ctx.fillStyle = "#1abc9c";
ctx.fill();

// Yay (sadece kenar, doldurma yok)
ctx.beginPath();
ctx.arc(550, 450, 60, 0, Math.PI * 1.5); // 270 derece yay
ctx.strokeStyle = "#e67e22";
ctx.lineWidth = 4;
ctx.stroke();

Metin

// Dolu metin
ctx.font = "bold 32px Arial";
ctx.fillStyle = "#2c3e50";
ctx.fillText("Merhaba Canvas!", 50, 560);

// Çerçeveli metin
ctx.font = "48px Georgia";
ctx.strokeStyle = "#3498db";
ctx.lineWidth = 2;
ctx.strokeText("Outline", 400, 560);

// Metin hizalama
ctx.textAlign = "center";       // "left", "center", "right"
ctx.textBaseline = "middle";    // "top", "middle", "bottom"
ctx.fillText("Ortalanmış", canvas.width / 2, 30);

Renk ve Stil

Gradyan

// Doğrusal gradyan (linear gradient)
const gradient = ctx.createLinearGradient(0, 0, 400, 0);
gradient.addColorStop(0, "#3498db");
gradient.addColorStop(0.5, "#2ecc71");
gradient.addColorStop(1, "#f39c12");

ctx.fillStyle = gradient;
ctx.fillRect(50, 50, 400, 100);

// Dairesel gradyan (radial gradient)
const radial = ctx.createRadialGradient(300, 300, 20, 300, 300, 100);
radial.addColorStop(0, "#fff");
radial.addColorStop(1, "#3498db");

ctx.fillStyle = radial;
ctx.beginPath();
ctx.arc(300, 300, 100, 0, Math.PI * 2);
ctx.fill();

Gölge

ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
ctx.shadowBlur = 10;
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;

ctx.fillStyle = "#e74c3c";
ctx.fillRect(100, 100, 200, 100);

// Gölgeyi kapat
ctx.shadowColor = "transparent";

Görsel Çizme

const img = new Image();
img.src = "character.png";

img.onload = () => {
  // Tam boyut
  ctx.drawImage(img, 50, 50);

  // Boyutlandırarak
  ctx.drawImage(img, 300, 50, 100, 100); // x, y, width, height

  // Kaynak bölgesi kesme (sprite sheet'ten)
  ctx.drawImage(
    img,
    0, 0, 64, 64,       // Kaynaktan kes: sx, sy, sWidth, sHeight
    400, 50, 128, 128    // Canvas'a çiz: dx, dy, dWidth, dHeight
  );
};

requestAnimationFrame: Akıcı Animasyon

Animasyon = sürekli değişen çizimler. Her kare (frame) ekranı temizleyip yeniden çizersin. Ama bunu nasıl zamanlarız?

// ❌ setInterval ile animasyon — kötü!
setInterval(() => {
  draw(); // 60fps için 16.67ms ama garanti değil
}, 1000 / 60);

// Neden kötü?
// 1. Ekran yenileme hızıyla senkron değil → yırtılma (tearing)
// 2. Sekme arka plandayken de çalışır → pil israfı
// 3. Zamanlama kesin değil → atlama ve tutarsızlık
// ✅ requestAnimationFrame — tarayıcı ile senkron
let lastTime = 0;

function gameLoop(currentTime) {
  const deltaTime = (currentTime - lastTime) / 1000; // Saniye cinsinden
  lastTime = currentTime;

  update(deltaTime);  // Oyun mantığı
  render();           // Çizim

  requestAnimationFrame(gameLoop); // Sonraki kareyi planla
}

// Animasyonu başlat
requestAnimationFrame(gameLoop);

requestAnimationFrame tarayıcının ekran yenileme döngüsüyle senkronize çalışır (genelde 60fps). Sekme arka plandayken durur — pil tasarrufu. Delta time ile hız hesaplanır — farklı bilgisayarlarda aynı hızda çalışır.

Delta Time Neden Önemli?

// ❌ Sabit hareket — hızlı bilgisayarda hızlı, yavaşta yavaş
function update() {
  player.x += 5; // Her karede 5 piksel
  // 60fps'de: saniyede 300px
  // 30fps'de: saniyede 150px — YANLIŞ!
}

// ✅ Delta time ile — her yerde aynı hız
function update(dt) {
  player.x += 300 * dt; // Saniyede 300 piksel
  // 60fps'de: 300 * 0.0167 = 5px per frame
  // 30fps'de: 300 * 0.0333 = 10px per frame
  // İkisinde de saniyede 300px — DOĞRU!
}

Basit Animasyon: Zıplayan Top

const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");

const ball = {
  x: 100,
  y: 100,
  radius: 20,
  vx: 200,   // Yatay hız (piksel/saniye)
  vy: 150,   // Dikey hız
  color: "#e74c3c",
};

let lastTime = 0;

function update(dt) {
  // Pozisyon güncelle
  ball.x += ball.vx * dt;
  ball.y += ball.vy * dt;

  // Duvarlardan sekme
  if (ball.x - ball.radius < 0 || ball.x + ball.radius > canvas.width) {
    ball.vx *= -1; // Yönü ters çevir
    // Sınır içinde tut
    ball.x = Math.max(ball.radius, Math.min(canvas.width - ball.radius, ball.x));
  }

  if (ball.y - ball.radius < 0 || ball.y + ball.radius > canvas.height) {
    ball.vy *= -1;
    ball.y = Math.max(ball.radius, Math.min(canvas.height - ball.radius, ball.y));
  }
}

function render() {
  // Ekranı temizle
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // Arka plan
  ctx.fillStyle = "#ecf0f1";
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // Top çiz
  ctx.beginPath();
  ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
  ctx.fillStyle = ball.color;
  ctx.fill();

  // Gölge efekti
  ctx.shadowColor = "rgba(0, 0, 0, 0.2)";
  ctx.shadowBlur = 10;
  ctx.shadowOffsetY = 5;
  ctx.fill();
  ctx.shadowColor = "transparent";
}

function gameLoop(currentTime) {
  const dt = Math.min((currentTime - lastTime) / 1000, 0.1); // Max 100ms
  lastTime = currentTime;

  update(dt);
  render();

  requestAnimationFrame(gameLoop);
}

requestAnimationFrame(gameLoop);

💡 İpucu: dt'yi Math.min(dt, 0.1) ile sınırla. Sekme arka plana alınıp geri geldiğinde dt çok büyük olabilir — nesneler duvarların dışına fırlar!


Basit Oyun Mekaniği

Tüm öğrendiklerimizi birleştirerek basit bir "Paddle Ball" oyunu yapalım — topun düşmesini engelleyen bir platform:

const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
canvas.width = 600;
canvas.height = 400;

// Oyun durumu (state)
const state = {
  score: 0,
  lives: 3,
  running: true,

  ball: {
    x: 300, y: 200,
    radius: 10,
    vx: 250, vy: -200,
    color: "#e74c3c",
  },

  paddle: {
    x: 250, y: 370,
    width: 100, height: 12,
    speed: 400,
    color: "#3498db",
  },

  bricks: [],
};

// Tuğlaları oluştur
function createBricks() {
  const rows = 4, cols = 8;
  const brickWidth = 65, brickHeight = 20, padding = 5;
  const offsetX = 15, offsetY = 30;
  const colors = ["#e74c3c", "#f39c12", "#2ecc71", "#3498db"];

  state.bricks = [];
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      state.bricks.push({
        x: offsetX + c * (brickWidth + padding),
        y: offsetY + r * (brickHeight + padding),
        width: brickWidth,
        height: brickHeight,
        color: colors[r],
        alive: true,
      });
    }
  }
}

// Klavye kontrolü
const keys = {};
window.addEventListener("keydown", (e) => { keys[e.key] = true; });
window.addEventListener("keyup", (e) => { keys[e.key] = false; });

// Fare kontrolü
canvas.addEventListener("mousemove", (e) => {
  const rect = canvas.getBoundingClientRect();
  const mouseX = e.clientX - rect.left;
  state.paddle.x = mouseX - state.paddle.width / 2;
});

// Çarpışma kontrolü — daire ve dikdörtgen
function circleRectCollision(circle, rect) {
  const closestX = Math.max(rect.x, Math.min(circle.x, rect.x + rect.width));
  const closestY = Math.max(rect.y, Math.min(circle.y, rect.y + rect.height));

  const dx = circle.x - closestX;
  const dy = circle.y - closestY;

  return (dx * dx + dy * dy) < (circle.radius * circle.radius);
}

// Güncelleme
function update(dt) {
  if (!state.running) return;
  const { ball, paddle, bricks } = state;

  // Paddle hareketi (klavye)
  if (keys["ArrowLeft"] || keys["a"]) {
    paddle.x -= paddle.speed * dt;
  }
  if (keys["ArrowRight"] || keys["d"]) {
    paddle.x += paddle.speed * dt;
  }
  paddle.x = Math.max(0, Math.min(canvas.width - paddle.width, paddle.x));

  // Top hareketi
  ball.x += ball.vx * dt;
  ball.y += ball.vy * dt;

  // Duvar çarpışması (sol, sağ, üst)
  if (ball.x - ball.radius < 0 || ball.x + ball.radius > canvas.width) {
    ball.vx *= -1;
  }
  if (ball.y - ball.radius < 0) {
    ball.vy *= -1;
  }

  // Alt sınır — can kaybı
  if (ball.y + ball.radius > canvas.height) {
    state.lives--;
    if (state.lives <= 0) {
      state.running = false;
      return;
    }
    // Topu sıfırla
    ball.x = canvas.width / 2;
    ball.y = canvas.height / 2;
    ball.vy = -200;
  }

  // Paddle çarpışması
  if (circleRectCollision(ball, paddle) && ball.vy > 0) {
    ball.vy *= -1;
    // Çarpma noktasına göre yatay açı ver
    const hitPoint = (ball.x - paddle.x) / paddle.width; // 0-1 arası
    ball.vx = (hitPoint - 0.5) * 500; // Merkez = düz, kenarlar = açılı
  }

  // Tuğla çarpışması
  for (const brick of bricks) {
    if (!brick.alive) continue;

    if (circleRectCollision(ball, brick)) {
      brick.alive = false;
      ball.vy *= -1;
      state.score += 10;

      // Tüm tuğlalar kırıldı mı?
      if (bricks.every(b => !b.alive)) {
        state.running = false; // Kazandın!
      }
      break; // Bir karede tek tuğla kır
    }
  }
}

// Çizim
function render() {
  // Temizle
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // Arka plan
  ctx.fillStyle = "#1a1a2e";
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  const { ball, paddle, bricks } = state;

  // Tuğlaları çiz
  for (const brick of bricks) {
    if (!brick.alive) continue;

    ctx.fillStyle = brick.color;
    ctx.fillRect(brick.x, brick.y, brick.width, brick.height);

    // Tuğla kenarı
    ctx.strokeStyle = "rgba(255, 255, 255, 0.3)";
    ctx.lineWidth = 1;
    ctx.strokeRect(brick.x, brick.y, brick.width, brick.height);
  }

  // Paddle çiz
  ctx.fillStyle = paddle.color;
  ctx.beginPath();
  ctx.roundRect(paddle.x, paddle.y, paddle.width, paddle.height, 6);
  ctx.fill();

  // Top çiz
  ctx.beginPath();
  ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
  ctx.fillStyle = ball.color;
  ctx.fill();

  // Skor ve can
  ctx.font = "16px monospace";
  ctx.fillStyle = "#ecf0f1";
  ctx.textAlign = "left";
  ctx.fillText(`Skor: ${state.score}`, 10, 20);
  ctx.textAlign = "right";
  ctx.fillText(`❤️ ${state.lives}`, canvas.width - 10, 20);

  // Oyun bitti
  if (!state.running) {
    ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    ctx.font = "bold 36px Arial";
    ctx.fillStyle = "#fff";
    ctx.textAlign = "center";

    if (state.lives <= 0) {
      ctx.fillText("GAME OVER", canvas.width / 2, canvas.height / 2);
    } else {
      ctx.fillText("TEBRİKLER!", canvas.width / 2, canvas.height / 2);
    }

    ctx.font = "18px Arial";
    ctx.fillText(
      `Skor: ${state.score} — Yeniden başlamak için tıklayın`,
      canvas.width / 2,
      canvas.height / 2 + 40
    );
  }
}

// Yeniden başlat
canvas.addEventListener("click", () => {
  if (!state.running) {
    state.score = 0;
    state.lives = 3;
    state.running = true;
    state.ball.x = 300;
    state.ball.y = 200;
    state.ball.vx = 250;
    state.ball.vy = -200;
    createBricks();
  }
});

// Oyun döngüsü
let lastTime = 0;
function gameLoop(time) {
  const dt = Math.min((time - lastTime) / 1000, 0.05);
  lastTime = time;

  update(dt);
  render();

  requestAnimationFrame(gameLoop);
}

createBricks();
requestAnimationFrame(gameLoop);

Bu oyunda kullanılan kavramlar:

  • Canvas çizim: fillRect, arc, fillText, roundRect

  • requestAnimationFrame: Akıcı 60fps animasyon

  • Delta time: Tüm hareketler zamana bağlı

  • Çarpışma tespiti: Daire-dikdörtgen kesişim kontrolü

  • Game state: Merkezi durum yönetimi

  • Input handling: Hem klavye hem fare kontrolü


CSS Animasyonları ile JavaScript

Her şeyi Canvas'ta yapmak zorunda değilsin. Basit UI animasyonları için CSS daha verimli — tarayıcı GPU hızlandırmasından faydalanır:

// JavaScript ile CSS animasyonu kontrol et
const element = document.getElementById("box");

// Class toggle ile animasyon
element.classList.add("animate-bounce");

// Web Animations API — JavaScript'te animasyon tanımla
element.animate(
  [
    { transform: "translateY(0)", opacity: 1 },
    { transform: "translateY(-50px)", opacity: 0.5 },
    { transform: "translateY(0)", opacity: 1 },
  ],
  {
    duration: 1000,
    iterations: Infinity,
    easing: "ease-in-out",
  }
);

// Animasyon kontrolü
const animation = element.animate(/* ... */);
animation.pause();
animation.play();
animation.reverse();
animation.cancel();

// Animasyon bittiğinde
animation.finished.then(() => {
  console.log("Animasyon tamamlandı");
});

💡 İpucu: Kural: UI animasyonları → CSS/Web Animations API. Oyun/grafik/özel çizim → Canvas. İkisini karıştırma — her birinin güçlü olduğu alan farklı.


Yaygın Hatalar

1. Canvas Bulanıklığı

// ❌ CSS ile boyut verip canvas resolution'ı güncellememek
// <canvas style="width: 400px; height: 300px"></canvas>

// ✅ Piksel yoğunluğunu dikkate al
const dpr = window.devicePixelRatio || 1;
canvas.width = 400 * dpr;
canvas.height = 300 * dpr;
canvas.style.width = "400px";
canvas.style.height = "300px";
ctx.scale(dpr, dpr);

2. clearRect Unutmak

// ❌ Temizlemeden çizmek — önceki kareler birikir
function render() {
  ctx.arc(ball.x, ball.y, 10, 0, Math.PI * 2); // Üst üste biner!
}

// ✅ Her karede temizle
function render() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  // Yeni kareyi çiz
}

3. beginPath Unutmak

// ❌ beginPath olmadan çizgiler birbirine bağlanır
ctx.moveTo(0, 0);
ctx.lineTo(100, 100);
ctx.stroke(); // Çizgi 1

ctx.moveTo(200, 0);
ctx.lineTo(300, 100);
ctx.stroke(); // Çizgi 1 + Çizgi 2 birlikte çizilir!

// ✅ Her şekil için beginPath
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(100, 100);
ctx.stroke();

ctx.beginPath(); // Yeni yol başlat
ctx.moveTo(200, 0);
ctx.lineTo(300, 100);
ctx.stroke();

Özet

  • Canvas, HTML'de piksel seviyesinde çizim alanı sağlar. getContext("2d") ile 2D çizim bağlamı elde edilir.

  • Temel şekiller: fillRect, strokeRect (dikdörtgen); arc (daire/yay); moveTo/lineTo (çizgi/path); fillText (metin).

  • requestAnimationFrame, ekran yenileme hızıyla senkronize çalışan animasyon mekanizması. setInterval'den üstün — performans, pil, senkronizasyon.

  • Delta time (dt), farklı fps'lerde tutarlı hareket sağlar. Tüm hız/hareket hesaplarını dt ile çarp.

  • Oyun döngüsü: update(dt) → mantık, render() → çizim. Her kare temizle → güncelle → çiz.

  • UI animasyonları → CSS/Web Animations API, oyun/grafik → Canvas. Doğru aracı seç.