148 lines
4 KiB
TypeScript
148 lines
4 KiB
TypeScript
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<void> {
|
|
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<void> {
|
|
// 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<void> {
|
|
while (true) {
|
|
await this.flush();
|
|
await new Promise((resolve) => setTimeout(resolve, refresh));
|
|
}
|
|
}
|
|
|
|
private async getPaletteMapping(
|
|
palette: UIPalette,
|
|
): Promise<PaletteMap> {
|
|
// 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<Color | ReadonlyArray<Color>>;
|
|
|
|
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),
|
|
);
|
|
}
|