/** * Color represented by RGB (0.0-1.0) components */ export type Color = { r: number; g: number; b: number; }; export type PaletteMap = ReadonlyArray; /** * Displayable character, with background and foreground color taken from the palette */ export type Char = Readonly<{ ch: string; bg: number; fg: number; }>; export type BufferSize = Readonly<{ w: number; h: number }>; export type BufferLocation = Readonly<{ x: number; y: number }>; export const SPACE: Char = { ch: " ", bg: 0, fg: 0 } as const; /** * Rectangular buffer of displayable characters * * All methods in this class do not properly check for * out-of-bounds coordinates, use BufferDrawing for this */ export class CharBuffer { private chars: Array; private dirty: boolean; private dirty_lines: Uint8Array; private dirty_seg_length: number; constructor(private size: BufferSize) { this.chars = new Array(size.w * size.h).fill(SPACE); this.dirty = true; this.dirty_lines = new Uint8Array(size.h).fill(0xFF); this.dirty_seg_length = Math.ceil(size.w / 8); } /** * Get the character buffered at a given location */ get(at: BufferLocation): Char { const i = at.y * this.size.w + at.x; if (i >= 0 && i < this.chars.length) { return this.chars[i]; } else { return SPACE; } } /** * Change the character buffered at a given location */ set(at: BufferLocation, char: Char): void { const i = at.y * this.size.w + at.x; if (i >= 0 && i < this.chars.length) { // TODO only if changed? this.chars[i] = char; this.dirty = true; this.dirty_lines[at.y] |= 1 << Math.floor(at.x / this.dirty_seg_length); } } /** * Call a method for each dirty character, then clear the dirty flags */ async forEachDirty( op: (at: BufferLocation, char: Char) => Promise, ): Promise { if (this.dirty) { const { w, h } = this.getSize(); for (let y = 0; y < h; y++) { const line = this.dirty_lines[y]; if (line) { for (let sx = 0; sx < 8; sx++) { if (line & 1 << sx) { const limit = Math.min((sx + 1) * this.dirty_seg_length, w); for (let x = sx * this.dirty_seg_length; x < limit; x++) { await op({ x, y }, this.get({ x, y })); } } } } } this.setDirty(false); } } /** * Set the dirty state for all characters */ setDirty(dirty: boolean): void { this.dirty = dirty; this.dirty_lines.fill(dirty ? 0xFF : 0x00); } /** * Check if a given location is potentially dirty * * This ignores the global dirty flag, only checks dirty line segments */ isDirty(at: BufferLocation): boolean { const seg = Math.floor(at.x / this.dirty_seg_length); return (this.dirty_lines[at.y] & (1 << seg)) != 0; } getSize(): BufferSize { return this.size; } toString(): string { return this.chars.map((c) => c.ch).join(""); } } /** * Tools for drawing inside a display buffer */ export class BufferDrawing { private bg = 0; private fg = 0; constructor( private readonly buffer: CharBuffer, private readonly palettemap: PaletteMap, ) { } color(fg: number, bg: number): BufferDrawing { this.fg = this.palettemap[fg]; this.bg = this.palettemap[bg]; return this; } /** * Draw a piece of text of the same color */ text(content: string, from: BufferLocation): BufferDrawing { const { bg, fg, buffer } = this; const { w, h } = buffer.getSize(); let { x, y } = from; if (y >= 0 && y < h) { for (let ch of content) { if (x >= 0 && x < w) { buffer.set({ x, y }, { ch, bg, fg }); } x++; } } return this; } }