Skip to content

वेब गेम्स में पिक्सेल आर्ट रेंडरिंग

पिक्सेल आर्ट सही तरीके से रेंडर होने पर शानदार दिखती है, और धुंधली होने पर बेहद खराब। यह ट्यूटोरियल क्रिस्प, ऑथेंटिक पिक्सेल ग्राफिक्स की तकनीकें बताता है।

1) धुंधलेपन की समस्या

डिफ़ॉल्ट रूप से, ब्राउज़र स्केल करते समय इमेज को स्मूद कर देते हैं। पिक्सेल आर्ट के लिए यह गलत है:

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

2) Canvas context सेटिंग्स

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

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

इसे हर canvas resize या context बदलाव के बाद सेट करना ज़रूरी है।

3) इंटीजर स्केलिंग

पिक्सेल आर्ट इंटीजर स्केल (1x, 2x, 3x, 4x) पर सबसे अच्छी दिखती है:

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) स्प्राइट को इंटीजर पोज़िशन पर ड्रॉ करना

सब-पिक्सेल पोज़िशन धुंधलापन पैदा करती हैं:

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) स्प्राइट शीट एनिमेशन

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) पैलेट स्वैपिंग

स्प्राइट को दोबारा रंगने की क्लासिक तकनीक:

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) टाइलमैप रेंडरिंग

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) आउटलाइन इफेक्ट

बेहतर दिखने के लिए स्प्राइट में आउटलाइन जोड़ें:

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) स्क्रीन शेक

क्लासिक गेम-फील इफेक्ट:

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) कलर पैलेट की सीमाएं

ऑथेंटिक पिक्सेल आर्ट सीमित पैलेट का इस्तेमाल करती है:

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
}

संबंधित