Scaffolding
This commit is contained in:
parent
bb898ba0a8
commit
3e0e4fb8aa
23 changed files with 156 additions and 175 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
|||
deno.d.ts
|
||||
.vscode
|
||||
.local
|
||||
.output
|
||||
web/*.js
|
||||
|
|
7
cli.ts
Executable file
7
cli.ts
Executable file
|
@ -0,0 +1,7 @@
|
|||
#!./run
|
||||
|
||||
import { runUIDemo } from "./src/demo.ts";
|
||||
|
||||
if (import.meta.main) {
|
||||
await runUIDemo();
|
||||
}
|
34
demo.ts
34
demo.ts
|
@ -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 });
|
||||
}
|
|
@ -2,5 +2,5 @@ export {
|
|||
describe,
|
||||
expect,
|
||||
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";
|
||||
|
|
45
mod.ts
45
mod.ts
|
@ -1,42 +1,3 @@
|
|||
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 { 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;
|
||||
}
|
||||
export { TextUI } from "./src/ui.ts";
|
||||
export { createTextUI, UI_DISPLAY_TYPES } from "./src/main.ts";
|
||||
export { runUIDemo } from "./src/demo.ts";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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(): {
|
||||
stdout: Buffer;
|
|
@ -1,5 +1,5 @@
|
|||
import { BufferLocation, BufferSize, Char, Color } from "./base.ts";
|
||||
import { readKeypress } from "./deps.ts";
|
||||
import { readKeypress } from "../deps.ts";
|
||||
import { Display } from "./display.ts";
|
||||
|
||||
export enum AnsiColorMode {
|
|
@ -1,5 +1,5 @@
|
|||
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, () => {
|
||||
it("initializes empty, sets and gets characters", () => {
|
77
src/colors.ts
Normal file
77
src/colors.ts
Normal 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
2
src/controls.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export interface Control {
|
||||
}
|
18
web-demo/demo.js → src/demo.ts
Normal file → Executable file
18
web-demo/demo.js → src/demo.ts
Normal file → Executable 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) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
export async function runUIDemo(
|
||||
display_type: keyof typeof UI_DISPLAY_TYPES = "autodetect",
|
||||
): Promise<void> {
|
||||
let x = 0;
|
||||
const ui = await createTextUI({
|
||||
const config: Partial<UIConfig> = {
|
||||
palette: [
|
||||
{ r: 0, g: 0, b: 0 },
|
||||
{ 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 });
|
||||
x += text.length + 1;
|
||||
},
|
||||
}, display_type);
|
||||
};
|
||||
const ui = await createTextUI(config, display_type);
|
||||
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 });
|
||||
}
|
||||
await ui.loop();
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { Display } from "./display.ts";
|
||||
import { describe, expect, it } from "./deps.testing.ts";
|
||||
import { describe, expect, it } from "../deps.testing.ts";
|
||||
|
||||
describe(Display, () => {
|
||||
it("buffers unique events", async () => {
|
40
src/main.ts
Normal file
40
src/main.ts
Normal 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;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
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 { TextUI } from "./ui.ts";
|
||||
|
|
@ -1,12 +1,6 @@
|
|||
import {
|
||||
BufferDrawing,
|
||||
BufferSize,
|
||||
CharBuffer,
|
||||
Color,
|
||||
PaletteMap,
|
||||
} from "./base.ts";
|
||||
import { UI_CONFIG_DEFAULTS, UIConfig, UIPalette } from "./config.ts";
|
||||
import { cmp } from "./deps.ts";
|
||||
import { BufferDrawing, BufferSize, CharBuffer, PaletteMap } from "./base.ts";
|
||||
import { getPaletteMapping } from "./colors.ts";
|
||||
import { UI_CONFIG_DEFAULTS, UIConfig } from "./config.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),
|
||||
);
|
||||
}
|
1
web-demo/.gitignore
vendored
1
web-demo/.gitignore
vendored
|
@ -1 +0,0 @@
|
|||
textui.js
|
|
@ -4,8 +4,8 @@
|
|||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="demo.css">
|
||||
<script type="module">
|
||||
import { demo } from "./demo.js";
|
||||
demo("web_canvas");
|
||||
import { runUIDemo } from "./mod.js";
|
||||
runUIDemo("web_canvas");
|
||||
</script>
|
||||
</head>
|
||||
|
|
@ -4,8 +4,8 @@
|
|||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="demo.css">
|
||||
<script type="module">
|
||||
import { demo } from "./demo.js";
|
||||
demo("web_div");
|
||||
import { runUIDemo } from "./mod.js";
|
||||
runUIDemo("web_div");
|
||||
</script>
|
||||
</head>
|
||||
|
Loading…
Reference in a new issue