158 lines
3.7 KiB
TypeScript
158 lines
3.7 KiB
TypeScript
/**
|
|
* Color represented by RGB (0.0-1.0) components
|
|
*/
|
|
export type Color = {
|
|
r: number;
|
|
g: number;
|
|
b: number;
|
|
};
|
|
|
|
export type PaletteMap = ReadonlyArray<number>;
|
|
|
|
/**
|
|
* 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<Char>;
|
|
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<void>,
|
|
): Promise<void> {
|
|
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;
|
|
}
|
|
}
|