Scaffolding

This commit is contained in:
Michaël Lemaire 2021-09-07 00:35:15 +02:00
parent bb898ba0a8
commit 3e0e4fb8aa
23 changed files with 156 additions and 175 deletions

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
deno.d.ts deno.d.ts
.vscode .vscode
.local .local
.output
web/*.js

7
cli.ts Executable file
View file

@ -0,0 +1,7 @@
#!./run
import { runUIDemo } from "./src/demo.ts";
if (import.meta.main) {
await runUIDemo();
}

34
demo.ts
View file

@ -1,34 +0,0 @@
#!./run
import { AnsiTerminalDisplay } from "./ansi.ts";
import { UIConfig } from "./config.ts";
import { TextUI } from "./ui.ts";
const display = new AnsiTerminalDisplay();
let x = 0;
const config: Partial<UIConfig> = {
palette: [
{ r: 0, g: 0, b: 0 },
{ r: 1, g: 1, b: 1 },
{ r: 0, g: 1, b: 1 },
],
onResize: draw,
onKeyStroke: (key) => {
ui.drawing.color(1, 0).text(key, { x, y: 7 });
x += key.length + 1;
},
onMouseClick: (loc) => {
const text = `${loc.x}:${loc.y}`;
ui.drawing.color(1, 0).text(text, { x, y: 7 });
x += text.length + 1;
},
};
const ui = new TextUI(display, config);
await ui.init();
draw();
await ui.loop();
function draw() {
ui.drawing.color(2, 0).text("hello", { x: 10, y: 3 });
ui.drawing.color(0, 1).text("world", { x: 10, y: 5 });
}

View file

@ -2,5 +2,5 @@ export {
describe, describe,
expect, expect,
it, it,
} from "https://js.thunderk.net/devtools@1.3.0/testing.ts"; } from "https://js.thunderk.net/testing@1.0.0/mod.ts";
export { Buffer } from "https://deno.land/std@0.106.0/io/buffer.ts"; export { Buffer } from "https://deno.land/std@0.106.0/io/buffer.ts";

45
mod.ts
View file

@ -1,42 +1,3 @@
import { AnsiTerminalDisplay } from "./ansi.ts"; export { TextUI } from "./src/ui.ts";
import { UIConfig } from "./config.ts"; export { createTextUI, UI_DISPLAY_TYPES } from "./src/main.ts";
import { Display } from "./display.ts"; export { runUIDemo } from "./src/demo.ts";
import { TextUI } from "./ui.ts";
import { CanvasTerminalDisplay, DivTerminalDisplay } from "./web.ts";
export { TextUI };
export const UI_DISPLAY_TYPES = {
autodetect: undefined,
ansi: AnsiTerminalDisplay,
web_div: DivTerminalDisplay,
web_canvas: CanvasTerminalDisplay,
dummy: Display,
} as const;
export async function createTextUI(
config: Partial<UIConfig>,
display_type: keyof typeof UI_DISPLAY_TYPES = "autodetect",
): Promise<TextUI> {
if (display_type == "autodetect") {
if (typeof (window as any).document != "undefined") {
display_type = "web_canvas";
// TODO if canvas is not available, fall back to div
} else if (typeof (Deno as any) != "undefined") {
display_type = "ansi";
} else {
const message = "Cannot initialize display";
if (typeof alert == "function") {
alert(message);
}
throw new Error(message);
}
}
var display = new UI_DISPLAY_TYPES[display_type]();
var ui = new TextUI(display, config);
await ui.init();
return ui;
}

View file

@ -1,5 +1,5 @@
import { AnsiColorMode, AnsiTerminalDisplay } from "./ansi.ts"; import { AnsiColorMode, AnsiTerminalDisplay } from "./ansi.ts";
import { Buffer, describe, expect, it } from "./deps.testing.ts"; import { Buffer, describe, expect, it } from "../deps.testing.ts";
function createTestDisplay(): { function createTestDisplay(): {
stdout: Buffer; stdout: Buffer;

View file

@ -1,5 +1,5 @@
import { BufferLocation, BufferSize, Char, Color } from "./base.ts"; import { BufferLocation, BufferSize, Char, Color } from "./base.ts";
import { readKeypress } from "./deps.ts"; import { readKeypress } from "../deps.ts";
import { Display } from "./display.ts"; import { Display } from "./display.ts";
export enum AnsiColorMode { export enum AnsiColorMode {

View file

@ -1,5 +1,5 @@
import { BufferDrawing, BufferLocation, CharBuffer } from "./base.ts"; import { BufferDrawing, BufferLocation, CharBuffer } from "./base.ts";
import { describe, expect, it } from "./deps.testing.ts"; import { describe, expect, it } from "../deps.testing.ts";
describe(CharBuffer, () => { describe(CharBuffer, () => {
it("initializes empty, sets and gets characters", () => { it("initializes empty, sets and gets characters", () => {

77
src/colors.ts Normal file
View file

@ -0,0 +1,77 @@
import { Color, PaletteMap } from "./base.ts";
import { UIPalette } from "./config.ts";
import { cmp } from "../deps.ts";
import { Display } from "./display.ts";
export async function getPaletteMapping(
palette: UIPalette,
display: Display,
): 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 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;
}
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),
);
}

2
src/controls.ts Normal file
View file

@ -0,0 +1,2 @@
export interface Control {
}

18
web-demo/demo.js → src/demo.ts Normal file → Executable file
View file

@ -1,9 +1,11 @@
import { createTextUI } from "./textui.js"; import { UIConfig } from "./config.ts";
import { createTextUI, UI_DISPLAY_TYPES } from "./main.ts";
export async function demo(display_type) { export async function runUIDemo(
await new Promise((resolve) => setTimeout(resolve, 500)); display_type: keyof typeof UI_DISPLAY_TYPES = "autodetect",
): Promise<void> {
let x = 0; let x = 0;
const ui = await createTextUI({ const config: Partial<UIConfig> = {
palette: [ palette: [
{ r: 0, g: 0, b: 0 }, { r: 0, g: 0, b: 0 },
{ r: 1, g: 1, b: 1 }, { r: 1, g: 1, b: 1 },
@ -19,10 +21,14 @@ export async function demo(display_type) {
ui.drawing.color(1, 0).text(text, { x, y: 7 }); ui.drawing.color(1, 0).text(text, { x, y: 7 });
x += text.length + 1; x += text.length + 1;
}, },
}, display_type); };
const ui = await createTextUI(config, display_type);
await ui.init();
draw();
await ui.loop();
function draw() { function draw() {
ui.drawing.color(2, 0).text("hello", { x: 10, y: 3 }); ui.drawing.color(2, 0).text("hello", { x: 10, y: 3 });
ui.drawing.color(0, 1).text("world", { x: 10, y: 5 }); ui.drawing.color(0, 1).text("world", { x: 10, y: 5 });
} }
await ui.loop();
} }

View file

@ -1,5 +1,5 @@
import { Display } from "./display.ts"; import { Display } from "./display.ts";
import { describe, expect, it } from "./deps.testing.ts"; import { describe, expect, it } from "../deps.testing.ts";
describe(Display, () => { describe(Display, () => {
it("buffers unique events", async () => { it("buffers unique events", async () => {

40
src/main.ts Normal file
View file

@ -0,0 +1,40 @@
import { AnsiTerminalDisplay } from "./ansi.ts";
import { UIConfig } from "./config.ts";
import { Display } from "./display.ts";
import { TextUI } from "./ui.ts";
import { CanvasTerminalDisplay, DivTerminalDisplay } from "./web.ts";
export const UI_DISPLAY_TYPES = {
autodetect: undefined,
ansi: AnsiTerminalDisplay,
web_div: DivTerminalDisplay,
web_canvas: CanvasTerminalDisplay,
dummy: Display,
} as const;
export async function createTextUI(
config: Partial<UIConfig>,
display_type: keyof typeof UI_DISPLAY_TYPES = "autodetect",
): Promise<TextUI> {
if (display_type == "autodetect") {
if (typeof (window as any).document != "undefined") {
display_type = "web_canvas";
// TODO if canvas is not available, fall back to div
} else if (typeof (Deno as any) != "undefined") {
display_type = "ansi";
} else {
const message = "Cannot initialize display";
if (typeof alert == "function") {
alert(message);
}
throw new Error(message);
}
}
var display = new UI_DISPLAY_TYPES[display_type]();
var ui = new TextUI(display, config);
await ui.init();
return ui;
}

View file

@ -1,5 +1,5 @@
import { BufferLocation, Char, Color, SPACE } from "./base.ts"; import { BufferLocation, Char, Color, SPACE } from "./base.ts";
import { describe, expect, it } from "./deps.testing.ts"; import { describe, expect, it } from "../deps.testing.ts";
import { Display } from "./display.ts"; import { Display } from "./display.ts";
import { TextUI } from "./ui.ts"; import { TextUI } from "./ui.ts";

View file

@ -1,12 +1,6 @@
import { import { BufferDrawing, BufferSize, CharBuffer, PaletteMap } from "./base.ts";
BufferDrawing, import { getPaletteMapping } from "./colors.ts";
BufferSize, import { UI_CONFIG_DEFAULTS, UIConfig } from "./config.ts";
CharBuffer,
Color,
PaletteMap,
} from "./base.ts";
import { UI_CONFIG_DEFAULTS, UIConfig, UIPalette } from "./config.ts";
import { cmp } from "./deps.ts";
import { Display } from "./display.ts"; import { Display } from "./display.ts";
/** /**
@ -136,76 +130,3 @@ export class TextUI {
} }
} }
} }
async function getPaletteMapping(
palette: UIPalette,
display: Display,
): 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 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;
}
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),
);
}

View file

1
web-demo/.gitignore vendored
View file

@ -1 +0,0 @@
textui.js

View file

@ -4,8 +4,8 @@
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="stylesheet" href="demo.css"> <link rel="stylesheet" href="demo.css">
<script type="module"> <script type="module">
import { demo } from "./demo.js"; import { runUIDemo } from "./mod.js";
demo("web_canvas"); runUIDemo("web_canvas");
</script> </script>
</head> </head>

View file

@ -4,8 +4,8 @@
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="stylesheet" href="demo.css"> <link rel="stylesheet" href="demo.css">
<script type="module"> <script type="module">
import { demo } from "./demo.js"; import { runUIDemo } from "./mod.js";
demo("web_div"); runUIDemo("web_div");
</script> </script>
</head> </head>