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/heightattribute'ları çizim alanının piksel çözünürlüğünü belirler. CSSwidth/heightise görüntü boyutunu belirler. Bu ikisi farklıysa görüntü bulanıklaşır. Retina ekranlardadevicePixelRatioile ç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ı çizDaire 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'yiMath.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,roundRectrequestAnimationFrame: 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ıdtile ç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ç.
AI Asistan
Sorularını yanıtlamaya hazır