Skip to content

Web oyunlarında piksel sanatı işleme

Piksel sanatı doğru işlendiğinde harika, bulanık olduğundaysa berbat görünür. Bu eğitimde net ve özgün piksel grafikler oluşturma tekniklerini ele alacağız.

1) Bulanıklık sorunu

Tarayıcılar, varsayılan olarak ölçeklendirme sırasında görselleri yumuşatır. Bu, piksel sanatı için yanlıştır:

css
/* CSS solution */
canvas {
  image-rendering: pixelated;
  image-rendering: crisp-edges; /* Firefox fallback */
}

2) Canvas bağlamı ayarları

js
const canvas = document.getElementById('game')
const ctx = canvas.getContext('2d')

// Disable smoothing for crisp pixels
ctx.imageSmoothingEnabled = false

Bu ayar, canvas her yeniden boyutlandırıldığında veya bağlam her değiştiğinde yeniden yapılmalıdır.

3) Tam sayı ölçeklendirme

Piksel sanatı en iyi tam sayı ölçeklerinde (1x, 2x, 3x, 4x) görünür:

js
const GAME_WIDTH = 160
const GAME_HEIGHT = 144 // Game Boy resolution

function resize() {
  const maxScale = Math.floor(Math.min(
    window.innerWidth / GAME_WIDTH,
    window.innerHeight / GAME_HEIGHT
  ))
  const scale = Math.max(1, maxScale)
  
  canvas.width = GAME_WIDTH
  canvas.height = GAME_HEIGHT
  canvas.style.width = GAME_WIDTH * scale + 'px'
  canvas.style.height = GAME_HEIGHT * scale + 'px'
  
  ctx.imageSmoothingEnabled = false
}

4) Sprite'ları tam sayı konumlarında çizme

Alt piksel konumları bulanıklığa neden olur:

js
// Bad: causes blur
ctx.drawImage(sprite, 10.5, 20.3)

// Good: crisp pixels
ctx.drawImage(sprite, Math.floor(10.5), Math.floor(20.3))

// Or round to nearest
ctx.drawImage(sprite, Math.round(player.x), Math.round(player.y))

5) Sprite sayfası animasyonu

js
class PixelSprite {
  constructor(image, frameWidth, frameHeight) {
    this.image = image
    this.frameWidth = frameWidth
    this.frameHeight = frameHeight
    this.animations = {}
    this.currentAnim = null
    this.frame = 0
    this.timer = 0
  }
  
  addAnimation(name, frames, fps = 8) {
    this.animations[name] = { frames, duration: 1 / fps }
  }
  
  play(name) {
    if (this.currentAnim !== name) {
      this.currentAnim = name
      this.frame = 0
      this.timer = 0
    }
  }
  
  update(dt) {
    if (!this.currentAnim) return
    
    const anim = this.animations[this.currentAnim]
    this.timer += dt
    
    if (this.timer >= anim.duration) {
      this.timer = 0
      this.frame = (this.frame + 1) % anim.frames.length
    }
  }
  
  draw(ctx, x, y, flipX = false) {
    if (!this.currentAnim) return
    
    const anim = this.animations[this.currentAnim]
    const frameIndex = anim.frames[this.frame]
    const sx = (frameIndex % 16) * this.frameWidth
    const sy = Math.floor(frameIndex / 16) * this.frameHeight
    
    ctx.save()
    if (flipX) {
      ctx.scale(-1, 1)
      x = -x - this.frameWidth
    }
    
    ctx.drawImage(
      this.image,
      sx, sy, this.frameWidth, this.frameHeight,
      Math.round(x), Math.round(y), this.frameWidth, this.frameHeight
    )
    ctx.restore()
  }
}

// Usage
const player = new PixelSprite(spriteSheet, 16, 16)
player.addAnimation('idle', [0, 1], 2)
player.addAnimation('walk', [2, 3, 4, 5], 8)
player.addAnimation('jump', [6], 1)

6) Palet değiştirme

Sprite'ları yeniden renklendirmek için kullanılan klasik bir teknik:

js
function createPaletteSwap(image, oldColors, newColors) {
  const canvas = document.createElement('canvas')
  canvas.width = image.width
  canvas.height = image.height
  const ctx = canvas.getContext('2d')
  
  ctx.drawImage(image, 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 r = data[i], g = data[i + 1], b = data[i + 2]
    
    for (let j = 0; j < oldColors.length; j++) {
      const old = oldColors[j]
      if (r === old.r && g === old.g && b === old.b) {
        const newC = newColors[j]
        data[i] = newC.r
        data[i + 1] = newC.g
        data[i + 2] = newC.b
        break
      }
    }
  }
  
  ctx.putImageData(imageData, 0, 0)
  return canvas
}

// Create enemy variant
const blueEnemy = createPaletteSwap(
  enemySprite,
  [{ r: 255, g: 0, b: 0 }],    // Red
  [{ r: 0, g: 100, b: 255 }]   // Blue
)

7) Döşeme haritası işleme

js
class Tilemap {
  constructor(tileSize, tilesetImage) {
    this.tileSize = tileSize
    this.tileset = tilesetImage
    this.tilesPerRow = Math.floor(tilesetImage.width / tileSize)
    this.data = []
  }
  
  setData(width, height, tiles) {
    this.width = width
    this.height = height
    this.data = tiles
  }
  
  draw(ctx, cameraX = 0, cameraY = 0) {
    const startCol = Math.floor(cameraX / this.tileSize)
    const startRow = Math.floor(cameraY / this.tileSize)
    const endCol = startCol + Math.ceil(ctx.canvas.width / this.tileSize) + 1
    const endRow = startRow + Math.ceil(ctx.canvas.height / this.tileSize) + 1
    
    for (let row = startRow; row < endRow; row++) {
      for (let col = startCol; col < endCol; col++) {
        if (row < 0 || row >= this.height || col < 0 || col >= this.width) continue
        
        const tileIndex = this.data[row * this.width + col]
        if (tileIndex < 0) continue
        
        const sx = (tileIndex % this.tilesPerRow) * this.tileSize
        const sy = Math.floor(tileIndex / this.tilesPerRow) * this.tileSize
        const dx = Math.round(col * this.tileSize - cameraX)
        const dy = Math.round(row * this.tileSize - cameraY)
        
        ctx.drawImage(
          this.tileset,
          sx, sy, this.tileSize, this.tileSize,
          dx, dy, this.tileSize, this.tileSize
        )
      }
    }
  }
}

8) Dış çizgi efektleri

Görünürlüğü artırmak için sprite'lara dış çizgiler ekleyin:

js
function drawWithOutline(ctx, image, x, y, outlineColor = '#000') {
  const offsets = [[-1, 0], [1, 0], [0, -1], [0, 1]]
  
  // Draw outline
  ctx.globalCompositeOperation = 'source-over'
  for (const [ox, oy] of offsets) {
    ctx.drawImage(image, Math.round(x + ox), Math.round(y + oy))
  }
  
  // Apply color to outline
  ctx.globalCompositeOperation = 'source-in'
  ctx.fillStyle = outlineColor
  ctx.fillRect(x - 1, y - 1, image.width + 2, image.height + 2)
  
  // Draw sprite on top
  ctx.globalCompositeOperation = 'source-over'
  ctx.drawImage(image, Math.round(x), Math.round(y))
}

9) Ekran sarsıntısı

Klasik bir oyun hissi efekti:

js
const camera = {
  x: 0,
  y: 0,
  shakeIntensity: 0,
  shakeDuration: 0,
  
  shake(intensity, duration) {
    this.shakeIntensity = intensity
    this.shakeDuration = duration
  },
  
  update(dt) {
    if (this.shakeDuration > 0) {
      this.shakeDuration -= dt
    }
  },
  
  getOffset() {
    if (this.shakeDuration <= 0) return { x: 0, y: 0 }
    
    return {
      x: Math.round((Math.random() - 0.5) * 2 * this.shakeIntensity),
      y: Math.round((Math.random() - 0.5) * 2 * this.shakeIntensity),
    }
  }
}

// In render:
function render() {
  const shake = camera.getOffset()
  ctx.save()
  ctx.translate(shake.x, shake.y)
  // Draw everything...
  ctx.restore()
}

// On hit:
camera.shake(4, 0.2) // 4 pixel intensity, 0.2 seconds

10) Renk paleti kısıtlamaları

Özgün piksel sanatında sınırlı paletler kullanılır:

js
const PALETTES = {
  gameboy: ['#0f380f', '#306230', '#8bac0f', '#9bbc0f'],
  nes: ['#000000', '#fcfcfc', '#f8f8f8', '#bcbcbc', /* ... */],
  pico8: [
    '#000000', '#1d2b53', '#7e2553', '#008751',
    '#ab5236', '#5f574f', '#c2c3c7', '#fff1e8',
    '#ff004d', '#ffa300', '#ffec27', '#00e436',
    '#29adff', '#83769c', '#ff77a8', '#ffccaa'
  ],
}

function quantizeColor(r, g, b, palette) {
  let minDist = Infinity
  let closest = palette[0]
  
  for (const color of palette) {
    const pr = parseInt(color.slice(1, 3), 16)
    const pg = parseInt(color.slice(3, 5), 16)
    const pb = parseInt(color.slice(5, 7), 16)
    
    const dist = (r - pr) ** 2 + (g - pg) ** 2 + (b - pb) ** 2
    if (dist < minDist) {
      minDist = dist
      closest = color
    }
  }
  
  return closest
}

İlgili içerikler