Compare commits
13 commits
063dd636bb
...
924d8ccf48
Author | SHA1 | Date | |
---|---|---|---|
924d8ccf48 | |||
763bfbc8cf | |||
af07866ac0 | |||
6c6c9386be | |||
63c41f07cb | |||
dc19a5ed9e | |||
525c6fa954 | |||
2f9bb73107 | |||
e90727a06c | |||
a2df9a4142 | |||
497731f62b | |||
b04ba4e622 | |||
b05cdab771 |
13 changed files with 500 additions and 34 deletions
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
}
|
17
.vscode/tasks.json
vendored
Normal file
17
.vscode/tasks.json
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "Serve Esbuild",
|
||||||
|
"detail": "Run a local server serving html files & transpiling TS -> JS",
|
||||||
|
"command": "npm",
|
||||||
|
"type": "process",
|
||||||
|
"args": ["run", "serve"],
|
||||||
|
"problemMatcher": ["$esbuild-watch"],
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "dedicated"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -6,6 +6,7 @@
|
||||||
<script src="js/debug.js"></script>
|
<script src="js/debug.js"></script>
|
||||||
<script>
|
<script>
|
||||||
document.body.append(...TickDebug());
|
document.body.append(...TickDebug());
|
||||||
|
document.body.append(...IdvDebug());
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
3
debug.ts
3
debug.ts
|
@ -1,3 +1,4 @@
|
||||||
import { TickDebug } from "./debug/tick";
|
import { TickDebug } from "./debug/tick";
|
||||||
|
import { IdvDebug } from "./debug/idv";
|
||||||
|
|
||||||
Object.assign(globalThis, { TickDebug });
|
Object.assign(globalThis, { TickDebug, IdvDebug });
|
||||||
|
|
58
debug/idv.ts
Normal file
58
debug/idv.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import { h } from "../lib/html";
|
||||||
|
import { Idv, StringProperty } from "../lib/idv";
|
||||||
|
|
||||||
|
export function IdvDebug() {
|
||||||
|
const textarea = h(
|
||||||
|
"textarea",
|
||||||
|
{
|
||||||
|
cols: 80,
|
||||||
|
rows: 40,
|
||||||
|
oninput(ev) {
|
||||||
|
parse();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`# Idv Testing Ground
|
||||||
|
User: tangent128
|
||||||
|
Uid: 10000
|
||||||
|
Shell: tclsh
|
||||||
|
Group: users
|
||||||
|
Group: sudo
|
||||||
|
Banner:
|
||||||
|
+------------------+
|
||||||
|
|Welcome to Debian!|
|
||||||
|
+------------------+
|
||||||
|
|
||||||
|
User: tirga
|
||||||
|
Uid: 10101
|
||||||
|
Shell: bash
|
||||||
|
Group: users
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
const pre = h("pre");
|
||||||
|
|
||||||
|
function parse() {
|
||||||
|
try {
|
||||||
|
const idv = Idv.parse(textarea.value);
|
||||||
|
|
||||||
|
pre.textContent = JSON.stringify(
|
||||||
|
idv.getMergedMap("User", UserFromDocument),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
pre.textContent = String(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parse();
|
||||||
|
|
||||||
|
return [textarea, pre];
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserFromDocument = (idv: Idv) => {
|
||||||
|
return {
|
||||||
|
shell: idv.getProperty("Shell", StringProperty),
|
||||||
|
groups: idv.getList("Group", StringProperty),
|
||||||
|
banner: idv.getProperty("Banner", StringProperty),
|
||||||
|
};
|
||||||
|
};
|
168
idv.md
Normal file
168
idv.md
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
# The Indented Document Values Format
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Indented Document Values (IDV) format is a meta-syntax for machine-readable textual data.
|
||||||
|
|
||||||
|
IDV is designed to prioritize human readability and writability by minimizing visual noise- there are no sigils, quotes, or brackets, only colons, indentation, and (when necessary) backslash escapes.
|
||||||
|
|
||||||
|
As a tradeoff, IDV is not a self-describing data format- while it can be used for defining a serialization or configuration format, systems using it need to layer their own semantics on top of it.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```
|
||||||
|
Person: Alice
|
||||||
|
Uid: 1000
|
||||||
|
Phone: 555-1234
|
||||||
|
Group: users
|
||||||
|
Group: sudo
|
||||||
|
Banner:
|
||||||
|
============================
|
||||||
|
This is my ASCII art login message
|
||||||
|
============================
|
||||||
|
|
||||||
|
Person: Bob
|
||||||
|
Uid: 1001
|
||||||
|
Phone: 555-5656
|
||||||
|
Group: users
|
||||||
|
```
|
||||||
|
|
||||||
|
## Syntax
|
||||||
|
|
||||||
|
IDV is a line-oriented format. Before any other parsing is done, the input is split into lines, and any trailing whitespace on a line (including line separators) is ignored.
|
||||||
|
|
||||||
|
### Comments
|
||||||
|
|
||||||
|
A **Comment** is any line whose first character is a `#` character. Comment lines are for human use and are ignored by the parser.
|
||||||
|
|
||||||
|
```
|
||||||
|
# This line is ignored
|
||||||
|
```
|
||||||
|
|
||||||
|
### Blank Lines
|
||||||
|
|
||||||
|
A **Blank Line** is any line that only contains whitespace. Because trailing whitespace is always trimmed, all Blank Lines are indistinguishable from each other.
|
||||||
|
|
||||||
|
Blank Lines are ignored unless they are part of a Document. (see below)
|
||||||
|
|
||||||
|
### Entries
|
||||||
|
|
||||||
|
An **Entry** is composed of one or more lines:
|
||||||
|
|
||||||
|
#### Tags
|
||||||
|
|
||||||
|
Each entry begins with a **Tag**, terminated by a colon (`:`). A Tag can contain any characters except leading or trailing whitespace, newlines, and colons:
|
||||||
|
|
||||||
|
```
|
||||||
|
Tag:
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Distinguishers
|
||||||
|
|
||||||
|
Optionally, a Distinguisher can follow the Tag on the same line. A Distinguisher can contain any characters except leading or trailing whitespace, and newlines:
|
||||||
|
|
||||||
|
```
|
||||||
|
Tag: distinguisher
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Escapes
|
||||||
|
|
||||||
|
Within Tags and Distinguishers, backslash escapes may be used to represent non-permitted or inconvenient characters:
|
||||||
|
|
||||||
|
```
|
||||||
|
Tag With \: And Spaces:
|
||||||
|
|
||||||
|
Tag: \ distinguisher with leading whitespace and\nA newline
|
||||||
|
```
|
||||||
|
|
||||||
|
| Escape sequence | Replacement |
|
||||||
|
| --------------- | ----------------- |
|
||||||
|
| \\_\<space>_ | A literal space |
|
||||||
|
| \\n | A newline |
|
||||||
|
| \\: | A colon (`:`) |
|
||||||
|
| \\\\ | A backslash (`\`) |
|
||||||
|
|
||||||
|
> TODO: additional escapes? ie, hex or unicode?
|
||||||
|
|
||||||
|
#### Documents
|
||||||
|
|
||||||
|
After the first line of an entry, any indented lines make up the **Document** portion of the entry:
|
||||||
|
|
||||||
|
```
|
||||||
|
Tag: distinguisher
|
||||||
|
First Line
|
||||||
|
Second Line
|
||||||
|
Third Line
|
||||||
|
```
|
||||||
|
|
||||||
|
The first line of a Document defines the Document's indentation- subsequent lines can be indented deeper, but no line may be indented _less_ than the first line. This indentation is removed from the beginning of each line when determining the Document's value.
|
||||||
|
|
||||||
|
Blank Lines can not carry indentation information. To resolve this ambiguity, Documents may not begin or end with Blank Lines- such lines are ignored. Blank Lines that occur _between_ indented lines _are_ considered part of the Document.
|
||||||
|
|
||||||
|
```
|
||||||
|
Tag:
|
||||||
|
|
||||||
|
The above blank line is ignored.
|
||||||
|
The below blank line is part of the Document.
|
||||||
|
|
||||||
|
The below blank line is ignored.
|
||||||
|
|
||||||
|
Tag:
|
||||||
|
Other stuff
|
||||||
|
```
|
||||||
|
|
||||||
|
Backslash escapes are _not_ processed within a Document. However, backslashes may be processed later, by higher-layered semantics.
|
||||||
|
|
||||||
|
In many cases the Document will contain recursive IDV data, and the rules above are designed to play nicely with this case- but it is up to the concrete format to decide how to parse the Document. It could just as easily contain free text, XML, or a base64 blob.
|
||||||
|
|
||||||
|
#### Disambiguations:
|
||||||
|
|
||||||
|
1. The Tag and Distinguisher are both trimmed of surrounding whitespace before being interpreted, but internal whitespace is left intact.
|
||||||
|
1. The Distinguisher may contain literal colons; these are treated as regular characters and carry no special meaning.
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
|
||||||
|
Applying minimal interpretation, IDV data can be represented as a list of Entries.
|
||||||
|
|
||||||
|
An Entry can be represented as a 3-tuple of:
|
||||||
|
|
||||||
|
1. a string (the Tag)
|
||||||
|
2. a string (the optional Distinguisher)
|
||||||
|
3. a list of strings (the lines of the Document)
|
||||||
|
|
||||||
|
How Entries are interpreted by the appication is not specified, but see below for some suggested patterns that should line up with things people usually want to do.
|
||||||
|
|
||||||
|
## Patterns
|
||||||
|
|
||||||
|
### Primitive Property
|
||||||
|
|
||||||
|
> TODO: one of distinguisher | document non-empty, parsing based on expected type
|
||||||
|
|
||||||
|
### Object Property
|
||||||
|
|
||||||
|
> TODO: distinguisher ignored, document is IDV
|
||||||
|
|
||||||
|
### Union Property
|
||||||
|
|
||||||
|
> TODO: distinguisher determines how the document is parsed
|
||||||
|
|
||||||
|
### List
|
||||||
|
|
||||||
|
> TODO: property specified multiple times
|
||||||
|
|
||||||
|
### Map
|
||||||
|
|
||||||
|
> TODO: distinguisher defines key, document parsed for value
|
||||||
|
|
||||||
|
### Property Map
|
||||||
|
|
||||||
|
> TODO: Category defines key, parsed as property for value
|
||||||
|
|
||||||
|
### Merged Map
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
> TODO:
|
||||||
|
>
|
||||||
|
> - yaml
|
||||||
|
> - dpkg control files
|
|
@ -1,27 +0,0 @@
|
||||||
{
|
|
||||||
"folders": [
|
|
||||||
{
|
|
||||||
"path": "."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"editor.formatOnSave": true
|
|
||||||
},
|
|
||||||
"tasks": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"label": "Serve Esbuild",
|
|
||||||
"detail": "Run a local server serving html files & transpiling TS -> JS",
|
|
||||||
"command": "npm",
|
|
||||||
"type": "process",
|
|
||||||
"args": ["run", "serve"],
|
|
||||||
"problemMatcher": ["$esbuild-watch"],
|
|
||||||
"presentation": {
|
|
||||||
"reveal": "always",
|
|
||||||
"panel": "dedicated"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
125
lib/idv.ts
Normal file
125
lib/idv.ts
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
export type DistinguisherParser<T> = (distinguisher: string) => T;
|
||||||
|
export type DocumentParser<T> = (document: string[]) => T;
|
||||||
|
export type UnionParser<T> = (distinguisher: string, document: string[]) => T;
|
||||||
|
export type IdvConverter<T> = (idv: Idv) => T;
|
||||||
|
|
||||||
|
const LEADING_WHITESPACE = /^([ \t]+)/;
|
||||||
|
const ENTRY = /^(.+?):\s*(.*)/;
|
||||||
|
|
||||||
|
export class Idv {
|
||||||
|
collections: Map<string, undefined | [string, string[]][]> = new Map();
|
||||||
|
|
||||||
|
public static parse(input: string): Idv {
|
||||||
|
const idv = new Idv();
|
||||||
|
return idv.import(input);
|
||||||
|
}
|
||||||
|
public static parseLines(input: string[]): Idv {
|
||||||
|
const idv = new Idv();
|
||||||
|
return idv.importLines(input);
|
||||||
|
}
|
||||||
|
public import(input: string): Idv {
|
||||||
|
const lines = input.split("\n").map((line) => line.trimEnd());
|
||||||
|
return this.importLines(lines);
|
||||||
|
}
|
||||||
|
public importLines(input: string[]): Idv {
|
||||||
|
let currentDocument: string[] | undefined = undefined;
|
||||||
|
let currentIndent: string | undefined = undefined;
|
||||||
|
let bufferedBlankLines: string[] = [];
|
||||||
|
|
||||||
|
input.forEach((line) => {
|
||||||
|
const indent = LEADING_WHITESPACE.exec(line)?.[1];
|
||||||
|
if (indent) {
|
||||||
|
if (currentDocument == undefined) {
|
||||||
|
throw new Error("Indented document found before an entry");
|
||||||
|
}
|
||||||
|
if (currentIndent == undefined) {
|
||||||
|
currentIndent = indent;
|
||||||
|
}
|
||||||
|
if (line.startsWith(currentIndent)) {
|
||||||
|
currentDocument.push(...bufferedBlankLines);
|
||||||
|
bufferedBlankLines = [];
|
||||||
|
currentDocument.push(line.substring(currentIndent.length));
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"Inconsistent indentation- line indented less than the first line of its document"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (line == "") {
|
||||||
|
bufferedBlankLines.push("");
|
||||||
|
} else if (line[0] == "#") {
|
||||||
|
// skip
|
||||||
|
} else {
|
||||||
|
const matches = ENTRY.exec(line);
|
||||||
|
if (matches) {
|
||||||
|
const [, collection, distinguisher] = matches;
|
||||||
|
|
||||||
|
if (!this.collections.has(collection)) {
|
||||||
|
this.collections.set(collection, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDocument = [];
|
||||||
|
currentIndent = undefined;
|
||||||
|
// TODO: implement backslash escaping in the distinguisher
|
||||||
|
this.collections
|
||||||
|
.get(collection)!
|
||||||
|
.push([distinguisher, currentDocument]);
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to parse a property");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getProperty<T>(name: string, parse: UnionParser<T>): T | undefined {
|
||||||
|
const firstEntry = this.collections.get(name)?.[0];
|
||||||
|
return firstEntry && parse(firstEntry[0], firstEntry[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getList<T>(name: string, parse: UnionParser<T>): T[] {
|
||||||
|
return (this.collections.get(name) ?? []).map(([distinguisher, document]) =>
|
||||||
|
parse(distinguisher, document)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMap<T>(
|
||||||
|
name: string,
|
||||||
|
parseDocument: DocumentParser<T>
|
||||||
|
): Record<string, T> {
|
||||||
|
return Object.fromEntries(
|
||||||
|
(this.collections.get(name) ?? []).map(([distinguisher, document]) => [
|
||||||
|
distinguisher,
|
||||||
|
parseDocument(document),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMergedMap<T>(
|
||||||
|
name: string,
|
||||||
|
convertIdv: IdvConverter<T>
|
||||||
|
): Record<string, T> {
|
||||||
|
const idvMap: Map<string, Idv> = new Map();
|
||||||
|
|
||||||
|
(this.collections.get(name) ?? []).forEach(([distinguisher, document]) => {
|
||||||
|
let idv = idvMap.get(distinguisher);
|
||||||
|
if (idv == undefined) {
|
||||||
|
idvMap.set(distinguisher, (idv = new Idv()));
|
||||||
|
}
|
||||||
|
|
||||||
|
idv.importLines(document);
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: Record<string, T> = {};
|
||||||
|
|
||||||
|
for (const [distinguisher, idv] of idvMap.entries()) {
|
||||||
|
result[distinguisher] = convertIdv(idv);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement backslash-escaping in the document?
|
||||||
|
export const StringProperty = (distinguisher: string, lines: string[]) =>
|
||||||
|
lines.length == 0 ? distinguisher : lines.join("\n");
|
67
lib/keys.ts
Normal file
67
lib/keys.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { Source } from "./source";
|
||||||
|
|
||||||
|
export type KeyName = "up" | "down" | "left" | "right" | "a" | "b" | "menu";
|
||||||
|
|
||||||
|
const KEY_NAMES: Record<string, KeyName> = {
|
||||||
|
// compact keys (WASD+ZXC)
|
||||||
|
KeyZ: "a",
|
||||||
|
KeyX: "b",
|
||||||
|
KeyC: "menu",
|
||||||
|
KeyW: "up",
|
||||||
|
KeyS: "down",
|
||||||
|
KeyA: "left",
|
||||||
|
KeyD: "right",
|
||||||
|
// full-board keys (arrows+space/shift/enter)
|
||||||
|
Space: "a",
|
||||||
|
ShiftLeft: "b",
|
||||||
|
ShiftRight: "b",
|
||||||
|
Enter: "menu",
|
||||||
|
ArrowUp: "up",
|
||||||
|
ArrowDown: "down",
|
||||||
|
ArrowLeft: "left",
|
||||||
|
ArrowRight: "right",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** A keypress/release event for an abstract button, or else ["focus", "release"] if for some reason future release events might not be registered. */
|
||||||
|
export type KeyEvent = [KeyName | "focus", "press" | "release"];
|
||||||
|
|
||||||
|
export function keyControl(source: HTMLElement): Source<KeyEvent> {
|
||||||
|
const tabIndex = source.getAttribute("tabIndex");
|
||||||
|
source.setAttribute(
|
||||||
|
"tabindex",
|
||||||
|
tabIndex == "" || tabIndex == null ? "-1" : tabIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
return ((callback?: (keyEvent: KeyEvent) => void) => {
|
||||||
|
if (callback) {
|
||||||
|
const handle = (evt: KeyboardEvent, action: "press" | "release") => {
|
||||||
|
const keyName = KEY_NAMES[evt.code];
|
||||||
|
if (keyName != undefined) {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
|
||||||
|
callback([keyName, action]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const keyUp = (evt: KeyboardEvent) => handle(evt, "release");
|
||||||
|
const keyDown = (evt: KeyboardEvent) => handle(evt, "press");
|
||||||
|
const focus = () => callback(["focus", "press"]);
|
||||||
|
const blur = () => callback(["focus", "release"]);
|
||||||
|
|
||||||
|
source.addEventListener("keyup", keyUp, false);
|
||||||
|
source.addEventListener("keydown", keyDown, false);
|
||||||
|
source.addEventListener("focus", focus, false);
|
||||||
|
source.addEventListener("blur", blur, false);
|
||||||
|
source.focus({ focusVisible: true } as FocusOptions);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
source.removeEventListener("keyup", keyUp, false);
|
||||||
|
source.removeEventListener("keydown", keyDown, false);
|
||||||
|
source.removeEventListener("focus", focus, false);
|
||||||
|
source.removeEventListener("blur", blur, false);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return ["blur", "release"];
|
||||||
|
}
|
||||||
|
}) as Source<KeyEvent>;
|
||||||
|
}
|
|
@ -1,13 +1,9 @@
|
||||||
import { Cancel, Source } from "./source";
|
import { Source } from "./source";
|
||||||
|
|
||||||
export type PhysicsTick = ["physics"];
|
export type PhysicsTick = ["physics"];
|
||||||
export type RenderTick = ["render", number];
|
export type RenderTick = ["render", number];
|
||||||
|
|
||||||
export function tick(fps: number): Source<PhysicsTick | RenderTick> {
|
export function tick(fps: number): Source<PhysicsTick | RenderTick> {
|
||||||
function tickSource(): PhysicsTick;
|
|
||||||
function tickSource(
|
|
||||||
callback: (tick: PhysicsTick | RenderTick) => void
|
|
||||||
): Cancel;
|
|
||||||
function tickSource(callback?: (tick: PhysicsTick | RenderTick) => void) {
|
function tickSource(callback?: (tick: PhysicsTick | RenderTick) => void) {
|
||||||
if (callback) {
|
if (callback) {
|
||||||
let lastPhysicsTick: number = new Date().getTime();
|
let lastPhysicsTick: number = new Date().getTime();
|
||||||
|
@ -32,5 +28,5 @@ export function tick(fps: number): Source<PhysicsTick | RenderTick> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tickSource;
|
return tickSource as Source<PhysicsTick | RenderTick>;
|
||||||
}
|
}
|
||||||
|
|
11
pixelflood.html
Normal file
11
pixelflood.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Pixelflood Art</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="js/pixelflood.js"></script>
|
||||||
|
<script>
|
||||||
|
document.body.append(...PixelfloodApplet());
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
45
pixelflood.ts
Normal file
45
pixelflood.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
// TODO choose generator
|
||||||
|
// TODO get/put image data in render/tick loop
|
||||||
|
// TODO rediscover RGB/HSV pixelflood techniques
|
||||||
|
|
||||||
|
import { canvas2d, h } from "./lib/html";
|
||||||
|
|
||||||
|
interface Controls {
|
||||||
|
seed: HTMLInputElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PixelfloodApplet() {
|
||||||
|
const [canvas, cx] = canvas2d({});
|
||||||
|
const [controls, controlUi] = ControlUi();
|
||||||
|
|
||||||
|
return [canvas, ...controlUi];
|
||||||
|
}
|
||||||
|
|
||||||
|
function numInput(init: number) {
|
||||||
|
return h("input", { type: "number", valueAsNumber: init });
|
||||||
|
}
|
||||||
|
|
||||||
|
function ControlUi(): [Controls, HTMLElement[]] {
|
||||||
|
let seed, width, height;
|
||||||
|
|
||||||
|
const html = [
|
||||||
|
h("h2", {}, "Controls"),
|
||||||
|
h(
|
||||||
|
"div",
|
||||||
|
{},
|
||||||
|
h(
|
||||||
|
"label",
|
||||||
|
{},
|
||||||
|
"Width: ",
|
||||||
|
(width = numInput(128)),
|
||||||
|
"Height: ",
|
||||||
|
(height = numInput(128))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
h("div", {}, h("label", {}, "Random Seed: ", (seed = numInput(128)))),
|
||||||
|
];
|
||||||
|
|
||||||
|
return [{ seed }, html];
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(globalThis, { PixelfloodApplet });
|
|
@ -3,6 +3,7 @@
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"lib": ["ES2019", "DOM"],
|
"lib": ["ES2019", "DOM"],
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"target": "ES2020",
|
||||||
"noEmit": true
|
"noEmit": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue