import { BufferDrawing, BufferSize, CharBuffer, Color, PaletteMap, } from "./base.ts"; import { cmp } from "./deps.ts"; import { Display } from "./display.ts"; /** * Common abstraction for a textual UI */ export class TextUI { private screen = new CharBuffer({ w: 1, h: 1 }); private palettemap: PaletteMap = []; constructor(private display: Display) { } get drawing(): BufferDrawing { return new BufferDrawing(this.screen, this.palettemap); } /** * Initializes the display */ async init(palette: UIPalette): Promise { var size = await this.display.getSize(); this.screen = new CharBuffer(size); this.palettemap = await this.getPaletteMapping(palette); await this.display.clear(); } /** * Get the current display size */ getSize(): BufferSize { return this.screen.getSize(); } /** * Flush the internal buffer to the display */ async flush(): Promise { // TODO only dirty chars const { w, h } = this.screen.getSize(); for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { await this.display.setChar({ x, y }, this.screen.get({ x, y })); } } } /** * Start the event loop, waiting for input */ async loop(refresh = 1000): Promise { while (true) { await this.flush(); await new Promise((resolve) => setTimeout(resolve, refresh)); } } private async getPaletteMapping( palette: UIPalette, ): Promise { // get the colors supported by display const app_colors = palette.map((c): Color[] => Array.isArray(c) ? c : [c]); const all_colors = app_colors.reduce((acc, val) => acc.concat(val), []); const display_colors = await this.display.setupPalette(all_colors); // rank all supported colors by proximity to each app color let ranked: { color: Color; idx: number; penalty: number; matches: { color: Color; idx: number; distance: number }[]; }[] = []; app_colors.forEach((colors, idx) => { colors.forEach((color, alt) => { ranked.push({ color, idx, penalty: alt + 1, matches: display_colors.map((display_color, didx) => { return { color: display_color, idx: didx, distance: colorDistance(color, display_color), }; }).sort(cmp({ key: (info) => info.distance })), }); }); }); // TODO negatively score colors too much near previously chosen ones // find the best color mapping for each source color const result = palette.map(() => -1); while (ranked.length > 0) { ranked.sort( cmp({ key: (info) => info.matches[0].distance * info.penalty }), ); const best = ranked[0]; const app_idx = best.idx; const display_idx = best.matches[0].idx; result[app_idx] = display_idx; for (const color of ranked) { color.matches = color.matches.filter((match) => match.idx !== display_idx ); } ranked = ranked.filter((color) => color.idx !== app_idx && color.matches.length > 0 ); } return result; } } /** * Color palette requirements. * * The array represents the "ideal" colors desired by the application. * When drawing things, *bg* and *fg* color information should be an index * in this palette. * * For each palette index, a single color can be requested, or an * array of accepted alternatives, with decreasing priority. */ export type UIPalette = ReadonlyArray>; function colorDistance(e1: Color, e2: Color): number { /*return (e2.r - e1.r) * (e2.r - e1.r) + (e2.g - e1.g) * (e2.g - e1.g) + (e2.b - e1.b) * (e2.b - e1.b);*/ const c = (x: number) => Math.round(x * 255); const rmean = (c(e1.r) + c(e2.r)) / 2; const r = c(e1.r) - c(e2.r); const g = c(e1.g) - c(e2.g); const b = c(e1.b) - c(e2.b); return Math.sqrt( (((512 + rmean) * r * r) >> 8) + 4 * g * g + (((767 - rmean) * b * b) >> 8), ); }