web games में text rendering
games में text हर जगह होता है, scores, dialogue, menus, damage numbers. यह tutorial अलग-अलग तरीके और हर एक को कब इस्तेमाल करना है, यह बताता है.
1) Canvas text की बुनियादी बातें
सबसे सरल तरीका:
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 करने की जरूरत नहीं:
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
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
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 पाने के लिए:
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 करें और दोबारा इस्तेमाल करें:
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
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
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
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
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 होते हैं:
// 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 अच्छे से काम करते हैं.
जुड़े हुए लेख
- Canvas 2D game loop
- Pixel art rendering
- WebGL fundamentals
- Responsive game canvas — canvas resolution और DPI के साथ text को scale करना
- मुफ्त Game Assets कहाँ मिलें — font और UI asset के स्रोत