Skip to content

web games में text rendering

games में text हर जगह होता है, scores, dialogue, menus, damage numbers. यह tutorial अलग-अलग तरीके और हर एक को कब इस्तेमाल करना है, यह बताता है.

1) Canvas text की बुनियादी बातें

सबसे सरल तरीका:

js
ctx.font = '24px Arial'
ctx.fillStyle = '#ffffff'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText('Score: 1000', canvas.width / 2, 50)

अगर आपको headings और HUD labels के लिए ज्यादा कसी हुई या ज्यादा खुली spacing चाहिए, तो canvas context अब letterSpacing और wordSpacing को सीधे support करता है. दोनों CSS length strings लेते हैं और 2025 में cross-browser Baseline तक पहुँच गए, इसलिए kerning को नकली बनाने के लिए अब आपको एक-एक करके glyphs draw करने की जरूरत नहीं:

js
ctx.font = '24px sans-serif'
ctx.letterSpacing = '2px'
ctx.wordSpacing = '6px'
ctx.fillText('GAME OVER', 100, 100)

काम पूरा होने पर इन्हें '0px' पर reset कर दें, क्योंकि ये font और fillStyle की तरह context पर बने रहते हैं.

2) outline वाला text

js
function drawTextWithOutline(ctx, text, x, y, fillColor, outlineColor, outlineWidth = 2) {
  ctx.font = '24px Arial'
  ctx.textAlign = 'center'
  ctx.textBaseline = 'middle'
  
  ctx.strokeStyle = outlineColor
  ctx.lineWidth = outlineWidth
  ctx.strokeText(text, x, y)
  
  ctx.fillStyle = fillColor
  ctx.fillText(text, x, y)
}

drawTextWithOutline(ctx, 'GAME OVER', 400, 300, '#fff', '#000', 3)

3) shadow वाला text

js
function drawTextWithShadow(ctx, text, x, y, color, shadowOffset = 2) {
  ctx.font = '32px sans-serif'
  ctx.textAlign = 'left'
  ctx.textBaseline = 'top'
  
  // Shadow
  ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'
  ctx.fillText(text, x + shadowOffset, y + shadowOffset)
  
  // Main text
  ctx.fillStyle = color
  ctx.fillText(text, x, y)
}

4) bitmap font rendering

pixel art games के लिए या एक जैसा cross-platform look पाने के लिए:

js
class BitmapFont {
  constructor(image, charWidth, charHeight, chars) {
    this.image = image
    this.charWidth = charWidth
    this.charHeight = charHeight
    this.chars = chars
  }
  
  draw(ctx, text, x, y, scale = 1) {
    const w = this.charWidth * scale
    const h = this.charHeight * scale
    
    for (let i = 0; i < text.length; i++) {
      const charIndex = this.chars.indexOf(text[i].toUpperCase())
      if (charIndex === -1) continue
      
      const sx = (charIndex % 16) * this.charWidth
      const sy = Math.floor(charIndex / 16) * this.charHeight
      
      ctx.drawImage(
        this.image,
        sx, sy, this.charWidth, this.charHeight,
        Math.round(x + i * w), Math.round(y), w, h
      )
    }
  }
  
  measureText(text, scale = 1) {
    return text.length * this.charWidth * scale
  }
}

// Usage
const font = new BitmapFont(fontImage, 8, 8, 
  'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,!?-+:; '
)
font.draw(ctx, 'SCORE: 1000', 10, 10, 2)

5) performance के लिए pre-rendered text

static text के लिए, एक बार render करें और दोबारा इस्तेमाल करें:

js
function createTextImage(text, font, color) {
  const tempCanvas = document.createElement('canvas')
  const tempCtx = tempCanvas.getContext('2d')
  
  tempCtx.font = font
  const metrics = tempCtx.measureText(text)
  
  tempCanvas.width = Math.ceil(metrics.width)
  tempCanvas.height = parseInt(font) * 1.5
  
  tempCtx.font = font
  tempCtx.fillStyle = color
  tempCtx.textBaseline = 'top'
  tempCtx.fillText(text, 0, 0)
  
  return tempCanvas
}

// Create once
const titleImage = createTextImage('MY GAME', '48px Impact', '#fff')

// Draw many times (fast)
ctx.drawImage(titleImage, x, y)

6) typewriter effect

js
class TypewriterText {
  constructor(text, x, y, charsPerSecond = 20) {
    this.fullText = text
    this.x = x
    this.y = y
    this.speed = 1 / charsPerSecond
    this.timer = 0
    this.charIndex = 0
  }
  
  update(dt) {
    this.timer += dt
    while (this.timer >= this.speed && this.charIndex < this.fullText.length) {
      this.charIndex++
      this.timer -= this.speed
    }
  }
  
  draw(ctx) {
    ctx.font = '18px monospace'
    ctx.fillStyle = '#fff'
    ctx.fillText(this.fullText.slice(0, this.charIndex), this.x, this.y)
  }
  
  isComplete() {
    return this.charIndex >= this.fullText.length
  }
  
  skip() {
    this.charIndex = this.fullText.length
  }
}

7) word-wrapped text

js
function wrapText(ctx, text, maxWidth) {
  const words = text.split(' ')
  const lines = []
  let currentLine = ''
  
  for (const word of words) {
    const testLine = currentLine ? currentLine + ' ' + word : word
    const metrics = ctx.measureText(testLine)
    
    if (metrics.width > maxWidth && currentLine) {
      lines.push(currentLine)
      currentLine = word
    } else {
      currentLine = testLine
    }
  }
  
  if (currentLine) lines.push(currentLine)
  return lines
}

function drawWrappedText(ctx, text, x, y, maxWidth, lineHeight) {
  const lines = wrapText(ctx, text, maxWidth)
  for (let i = 0; i < lines.length; i++) {
    ctx.fillText(lines[i], x, y + i * lineHeight)
  }
}

8) dialogue box system

js
class DialogueBox {
  constructor(x, y, width, height) {
    this.x = x
    this.y = y
    this.width = width
    this.height = height
    this.padding = 16
    this.lineHeight = 24
    this.text = ''
    this.displayedChars = 0
    this.charsPerSecond = 30
    this.timer = 0
  }
  
  show(text) {
    this.text = text
    this.displayedChars = 0
    this.timer = 0
  }
  
  update(dt) {
    if (this.displayedChars < this.text.length) {
      this.timer += dt
      const charsToAdd = Math.floor(this.timer * this.charsPerSecond)
      this.displayedChars = Math.min(this.displayedChars + charsToAdd, this.text.length)
      this.timer = this.timer % (1 / this.charsPerSecond)
    }
  }
  
  skip() {
    this.displayedChars = this.text.length
  }
  
  isComplete() {
    return this.displayedChars >= this.text.length
  }
  
  draw(ctx) {
    // Box background
    ctx.fillStyle = 'rgba(0, 0, 0, 0.8)'
    ctx.fillRect(this.x, this.y, this.width, this.height)
    
    // Border
    ctx.strokeStyle = '#fff'
    ctx.lineWidth = 2
    ctx.strokeRect(this.x, this.y, this.width, this.height)
    
    // Text
    ctx.font = '18px sans-serif'
    ctx.fillStyle = '#fff'
    ctx.textBaseline = 'top'
    
    const displayText = this.text.slice(0, this.displayedChars)
    const maxWidth = this.width - this.padding * 2
    const lines = wrapText(ctx, displayText, maxWidth)
    
    for (let i = 0; i < lines.length; i++) {
      ctx.fillText(
        lines[i],
        this.x + this.padding,
        this.y + this.padding + i * this.lineHeight
      )
    }
    
    // Continue indicator
    if (this.isComplete()) {
      ctx.fillText('▼', this.x + this.width - 24, this.y + this.height - 24)
    }
  }
}

9) उड़ते हुए damage numbers

js
class FloatingText {
  constructor(text, x, y, color = '#fff') {
    this.text = text
    this.x = x
    this.y = y
    this.color = color
    this.vy = -60
    this.lifetime = 1
    this.age = 0
  }
  
  update(dt) {
    this.y += this.vy * dt
    this.vy *= 0.95
    this.age += dt
  }
  
  draw(ctx) {
    const alpha = 1 - (this.age / this.lifetime)
    ctx.font = 'bold 20px sans-serif'
    ctx.textAlign = 'center'
    ctx.fillStyle = this.color
    ctx.globalAlpha = alpha
    ctx.fillText(this.text, Math.round(this.x), Math.round(this.y))
    ctx.globalAlpha = 1
  }
  
  isExpired() {
    return this.age >= this.lifetime
  }
}

// Manager
const floatingTexts = []

function spawnDamageNumber(damage, x, y) {
  const color = damage > 50 ? '#ff4444' : '#ffff44'
  floatingTexts.push(new FloatingText(damage.toString(), x, y, color))
}

function updateFloatingTexts(dt) {
  for (let i = floatingTexts.length - 1; i >= 0; i--) {
    floatingTexts[i].update(dt)
    if (floatingTexts[i].isExpired()) {
      floatingTexts.splice(i, 1)
    }
  }
}

10) WebGL text (SDF fonts)

WebGL games के लिए, Signed Distance Field fonts साफ-सुथरे scale होते हैं:

js
// SDF shader fragment
const sdfFragmentShader = `
  precision mediump float;
  
  uniform sampler2D u_texture;
  uniform vec4 u_color;
  varying vec2 v_texCoord;
  
  void main() {
    float distance = texture2D(u_texture, v_texCoord).a;
    float alpha = smoothstep(0.4, 0.6, distance);
    gl_FragColor = vec4(u_color.rgb, u_color.a * alpha);
  }
`

// Libraries like msdf-bmfont-xml can generate SDF font atlases

खास तौर पर three.js scenes के लिए, troika-three-text एक अच्छा विकल्प है. atlas को पहले से bake करने के बजाय, यह .ttf/.otf/.woff files को parse करता है और SDF glyphs को एक web worker में तुरंत generate करता है, और यह kerning, ligatures, और right-to-left scripts आपके लिए संभाल लेता है.

ज्यादातर 2D Canvas games के लिए, bitmap fonts या pre-rendered Canvas text अच्छे से काम करते हैं.

जुड़े हुए लेख