Renderer API ​
The Renderer class is the main interface for drawing to the screen. It provides a chainable API with double-buffering and support for multiple rendering primitives.
Overview ​
The Renderer class:
- Provides drawing primitives (setChar, drawText, fill)
- Uses double-buffering for flicker-free rendering
- Supports method chaining for better DX
- Offers both safe mode (throws on errors) and clip mode (ignores out-of-bounds)
- Works with any RenderTarget implementation
Constructor ​
new Renderer(target: RenderTarget, options?: RendererCreateOptions)Parameters ​
- target: Any object implementing the
RenderTargetinterface - options: Optional configuration
RendererCreateOptions ​
interface RendererCreateOptions {
defaultFg?: Color; // Default foreground color
defaultBg?: Color; // Default background color
autoClear?: boolean; // Auto-clear render target each frame
clearColor?: Color; // Optional clear color when autoClear is enabled
}Auto-Clear Example ​
import { Renderer } from "@shaisrc/tty";
const canvas = document.getElementById("game") as HTMLCanvasElement;
const renderer = Renderer.fromCanvas(
canvas,
{ width: 80, height: 24 },
{
defaultFg: "white",
defaultBg: "black",
autoClear: true,
clearColor: "black",
},
);
function renderFrame() {
renderer.clear().drawText(0, 0, "AUTO-CLEAR", { fg: "yellow" }).render();
}By default, autoClear is false, so you control background clearing manually with .fill(...) or custom draw logic. Enable autoClear to have the renderer clear the target before each frame.
Factory Methods (Recommended) ​
For convenience, Renderer provides static factory methods.
Renderer.forCanvas(canvas, options) (Recommended) ​
Create a renderer with improved, semantic API grouping. This is the recommended method for new code.
const renderer = Renderer.forCanvas(canvas, {
grid: { width: 80, height: 24 },
cell: { width: 12, height: 20 },
font: { family: "monospace", size: 16 },
colors: { fg: "white", bg: "black" },
});CanvasRendererOptions ​
interface CanvasRendererOptions {
// Grid dimensions (required)
grid: {
width: number; // Grid width in characters
height: number; // Grid height in characters
};
// Cell/character sizing (optional)
cell?: {
width?: number; // Character width in pixels (default: 8)
height?: number; // Character height in pixels (default: 16)
};
// Font configuration (optional)
font?: {
family?: string; // Font family (default: 'monospace')
size?: number; // Font size in pixels (default: 14)
};
// Color defaults (optional)
colors?: {
fg?: Color; // Default foreground color
bg?: Color; // Default background color
};
// Auto-clear behavior (optional)
autoClear?: boolean; // Auto-clear render target each frame
clearColor?: Color; // Clear color when autoClear is enabled
}Renderer.fromCanvas(canvas, canvasOptions, rendererOptions?) ​
Alternative factory method with separate canvas and renderer options.
const renderer = Renderer.fromCanvas(
canvas,
{
width: 80,
height: 24,
charWidth: 12,
charHeight: 20,
},
{
defaultFg: "white",
defaultBg: "black",
},
);Comparison ​
// Manual approach (most control, most verbose)
import { Renderer, CanvasTarget } from "@shaisrc/tty";
const target = new CanvasTarget(canvas, { width: 80, height: 24 });
const renderer = new Renderer(target, { defaultFg: "white" });
// fromCanvas (simpler, flat options)
const renderer = Renderer.fromCanvas(
canvas,
{ width: 80, height: 24 },
{ defaultFg: "white" },
);
// forCanvas (recommended, semantic grouping)
const renderer = Renderer.forCanvas(canvas, {
grid: { width: 80, height: 24 },
colors: { fg: "white" },
});Properties ​
width: number (readonly) ​
Width of the render target in characters.
console.log(renderer.width); // 80height: number (readonly) ​
Height of the render target in characters.
console.log(renderer.height); // 24Drawing Methods ​
setChar(x, y, char, fg?, bg?): this ​
Set a single character at the specified position.
renderer.setChar(10, 5, "@", "yellow", "black");Parameters:
x: X coordinate (0-indexed)y: Y coordinate (0-indexed)char: Character to displayfg: Foreground color (optional, uses default if omitted)bg: Background color (optional, uses default if omitted)
Returns: The renderer instance for chaining
drawText(x, y, text, options?): this ​
Draw a string of text horizontally.
renderer.drawText(5, 10, "Hello, World!", { fg: "green" });Parameters:
x: Starting X coordinatey: Y coordinatetext: Text to drawoptions: Text styling options
TextOptions:
interface TextOptions {
fg?: Color; // Foreground color
bg?: Color; // Background color
wrap?: boolean; // Enable word wrapping (not yet implemented)
align?: TextAlign; // Text alignment (not yet implemented)
}Returns: The renderer instance for chaining
fill(x, y, width, height, char, fg?, bg?): this ​
Fill a rectangular area with a character.
renderer.fill(10, 5, 20, 10, " ", null, "blue");Parameters:
x: Starting X coordinatey: Starting Y coordinatewidth: Width of the rectangleheight: Height of the rectanglechar: Character to fill withfg: Foreground color (optional)bg: Background color (optional)
Returns: The renderer instance for chaining
Buffer Management ​
clear(): this ​
Clear the render buffer.
renderer.clear();Note: This clears the internal buffer, not the screen. Call render() to update the display.
Returns: The renderer instance for chaining
render(): this ​
Flush the buffer to the render target.
renderer.render();This method:
- Sends all buffered cells to the render target
- Calls
flush()on the target - Leaves the buffer intact for the next frame
Returns: The renderer instance for chaining
getCell(x, y): Cell | undefined ​
Get the current cell at a position from the buffer.
const cell = renderer.getCell(10, 5);
if (cell) {
console.log(`Character: ${cell.char}, Color: ${cell.fg}`);
}Returns: The cell at the position, or undefined if not set
Mode Configuration ​
setSafeMode(enabled): this ​
Enable or disable safe mode.
In safe mode, out-of-bounds operations throw errors. This is useful during development to catch bugs.
renderer.setSafeMode(true);
renderer.setChar(-1, 0, "X"); // Throws errorDefault: false
Returns: The renderer instance for chaining
setClipMode(enabled): this ​
Enable or disable clip mode.
In clip mode, out-of-bounds operations are silently ignored. This is useful for drawing that might extend beyond screen boundaries.
renderer.setClipMode(true);
renderer.setChar(-1, 0, "X"); // Silently ignored
renderer.drawText(78, 0, "Hello"); // Only 'He' appearsDefault: false
Returns: The renderer instance for chaining
validate (object) ​
Validation helpers for bounds checking operations before drawing.
The validate object provides methods to check if drawing operations would fit within the renderer's bounds. This is useful for conditional rendering logic and preventing out-of-bounds errors.
validate.cell(x, y): boolean ​
Check if a single cell position is within bounds.
if (renderer.validate.cell(x, y)) {
renderer.setChar(x, y, "@");
} else {
console.warn("Position out of bounds");
}validate.box(x, y, width, height): boolean ​
Check if a box would fit entirely within bounds.
if (renderer.validate.box(x, y, 20, 10)) {
renderer.box(x, y, 20, 10);
} else {
console.warn("Box would extend outside screen");
}validate.text(x, y, text): boolean ​
Check if text would fit within bounds.
const message = "This is a long message";
if (renderer.validate.text(x, y, message)) {
renderer.drawText(x, y, message);
} else {
// Truncate to fit
const maxLen = renderer.width - x;
renderer.drawText(x, y, message.substring(0, maxLen));
}validate.line(x1, y1, x2, y2): boolean ​
Check if both line endpoints are within bounds.
if (renderer.validate.line(x1, y1, x2, y2)) {
renderer.drawLine(x1, y1, x2, y2, "-");
}Usage Examples:
// Safe dynamic menu positioning
const menuWidth = 30;
const menuHeight = 10;
const x = playerX - menuWidth / 2;
const y = playerY - menuHeight - 2;
if (renderer.validate.box(x, y, menuWidth, menuHeight)) {
renderer.menu(x, y, menuItems);
} else {
// Fallback to center screen
const centerX = (renderer.width - menuWidth) / 2;
const centerY = (renderer.height - menuHeight) / 2;
renderer.menu(centerX, centerY, menuItems);
}Usage Examples ​
Basic Usage ​
import { Renderer, CanvasTarget } from "@shaisrc/tty";
const canvas = document.getElementById("game") as HTMLCanvasElement;
const target = new CanvasTarget(canvas, { width: 80, height: 24 });
const renderer = new Renderer(target);
// Draw something
renderer
.clear()
.setChar(10, 5, "@", "yellow")
.drawText(0, 0, "Hello, World!", { fg: "green" })
.render();Game Loop ​
function gameLoop() {
// Clear previous frame
renderer.clear();
// Draw game state
drawMap(renderer);
drawEntities(renderer);
drawUI(renderer);
// Render to screen
renderer.render();
requestAnimationFrame(gameLoop);
}
gameLoop();With Method Chaining ​
renderer
.clear()
.fill(0, 0, 80, 24, " ", null, "black")
.drawText(35, 10, "GAME OVER", { fg: "red" })
.drawText(30, 12, "Press SPACE to retry", { fg: "white" })
.render();Clip Mode for Scrolling ​
renderer.setClipMode(true);
// Draw a large map, only visible portion appears
for (let y = 0; y < map.height; y++) {
for (let x = 0; x < map.width; x++) {
const screenX = x - camera.x;
const screenY = y - camera.y;
renderer.setChar(screenX, screenY, map.getTile(x, y).char);
}
}
renderer.render();Safe Mode for Development ​
if (process.env.NODE_ENV === "development") {
renderer.setSafeMode(true);
}
// This will throw in development if coordinates are invalid
function drawPlayer(x: number, y: number) {
renderer.setChar(x, y, "@", "yellow");
}Double Buffering Pattern ​
// Frame 1
renderer.clear();
renderer.setChar(10, 10, "@");
renderer.render(); // Player at (10, 10)
// Frame 2
renderer.clear();
renderer.setChar(11, 10, "@");
renderer.render(); // Player at (11, 10)
// No flicker! Buffer is swapped cleanlyEfficient Partial Updates ​
// Only clear and redraw what changed
if (playerMoved) {
// Clear old position
renderer.setChar(player.oldX, player.oldY, " ");
// Draw new position
renderer.setChar(player.x, player.y, "@", "yellow");
renderer.render();
}Performance Considerations ​
Buffer Management ​
The renderer uses a Map-based buffer:
- Only stores cells that have been explicitly set
- No allocation for empty cells
- Fast lookups and updates
Rendering Strategy ​
// Good: Only render what's needed
renderer.clear();
drawVisibleArea();
renderer.render();
// Bad: Rendering unnecessary cells
for (let y = 0; y < 1000; y++) {
for (let x = 0; x < 1000; x++) {
renderer.setChar(x, y, " "); // Most won't be visible!
}
}Chaining Optimization ​
Method chaining returns this, which has zero overhead:
// These are equivalent in performance
renderer.clear();
renderer.setChar(0, 0, "A");
renderer.render();
renderer.clear().setChar(0, 0, "A").render();Coordinate System ​
The renderer uses a character-based coordinate system:
- Origin (0, 0) is at the top-left
- X increases to the right
- Y increases downward
- All coordinates are 0-indexed
(0,0) ────> X
│
│
â–¼
YError Handling ​
Safe Mode Errors ​
renderer.setSafeMode(true);
try {
renderer.setChar(-1, 0, "X");
} catch (error) {
console.error(error.message);
// "Position (-1, 0) is out of bounds (80x24)"
}Graceful Degradation ​
// In normal mode (not safe, not clip), out-of-bounds is ignored
renderer.setChar(1000, 1000, "X"); // No error, just ignoredIntegration with RenderTargets ​
The Renderer works with any RenderTarget:
// Browser Canvas
const canvasTarget = new CanvasTarget(canvas, { width: 80, height: 24 });
const canvasRenderer = new Renderer(canvasTarget);
// Future: DOM
const domTarget = new DOMTarget(element, 80, 24);
const domRenderer = new Renderer(domTarget);Complete Example ​
import { Renderer, CanvasTarget } from "@shaisrc/tty";
// Setup
const canvas = document.getElementById("game") as HTMLCanvasElement;
const target = new CanvasTarget(canvas, { width: 80, height: 24 });
const renderer = new Renderer(target, {
defaultFg: "white",
defaultBg: "black",
autoClear: true,
clearColor: "black",
});
// Enable clip mode for easier drawing
renderer.setClipMode(true);
// Game state
const player = { x: 40, y: 12, char: "@" };
// Render function
function render() {
renderer
.clear()
.drawText(0, 0, "SCORE: 1000", { fg: "yellow" })
.setChar(player.x, player.y, player.char, "green")
.render();
}
// Input
window.addEventListener("keydown", (e) => {
if (e.key === "ArrowRight") player.x++;
if (e.key === "ArrowLeft") player.x--;
if (e.key === "ArrowDown") player.y++;
if (e.key === "ArrowUp") player.y--;
render();
});
// Initial render
render();Next Steps ​
- See Box Drawing for drawing boxes and borders
- See Layer System for multi-layer rendering
- See Alignment Helpers for text alignment utilities
