summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosh Kingsley <josh@joshkingsley.me>2025-10-25 20:46:35 +0300
committerJosh Kingsley <josh@joshkingsley.me>2025-10-25 22:09:48 +0300
commit5404a95c15e176d25728bf1a319ddb9828b23625 (patch)
tree639d175e15170618d36c0b22b3c8ad7764925175
parent2a4d7a7fc3b968ed8cdfd958a5e65fbe140042da (diff)
refactor(web): re-organize files
-rw-r--r--web/components/grid/index.ts14
-rw-r--r--web/package.json8
-rw-r--r--web/src/components/app/index.css3
-rw-r--r--web/src/components/app/index.ts (renamed from web/components/app/index.ts)3
-rw-r--r--web/src/components/grid/index.css8
-rw-r--r--web/src/components/grid/index.ts24
-rw-r--r--web/src/components/index.ts (renamed from web/components/index.ts)0
-rw-r--r--web/src/favicon.ico (renamed from web/favicon.ico)bin15406 -> 15406 bytes
-rw-r--r--web/src/global.d.ts (renamed from web/global.d.ts)0
-rw-r--r--web/src/html.ts (renamed from web/html.ts)0
-rw-r--r--web/src/index.css (renamed from web/index.css)0
-rw-r--r--web/src/index.html (renamed from web/index.html)0
-rw-r--r--web/src/index.ts (renamed from web/index.ts)25
-rw-r--r--web/src/math/Coord.ts23
-rw-r--r--web/src/math/Ratio.ts61
-rw-r--r--web/src/math/Rect.ts97
-rw-r--r--web/src/renderGrid.ts64
-rw-r--r--web/src/types.ts36
-rw-r--r--web/tsconfig.json6
-rw-r--r--web/vite.config.ts3
20 files changed, 333 insertions, 42 deletions
diff --git a/web/components/grid/index.ts b/web/components/grid/index.ts
deleted file mode 100644
index 7faf6b1..0000000
--- a/web/components/grid/index.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import h, { type CreateElement } from "../../html";
-
-class NotiveGridElement extends HTMLElement {
- foo: string;
-
- connectedCallback() {
- this.appendChild(h.p("OK: " + this.foo));
- }
-}
-
-customElements.define("ntv-grid", NotiveGridElement);
-
-export default ((...args: any[]): NotiveGridElement =>
- (h as any)["ntv-grid"](...args)) as CreateElement<NotiveGridElement>;
diff --git a/web/package.json b/web/package.json
index e0b674d..a84c562 100644
--- a/web/package.json
+++ b/web/package.json
@@ -2,12 +2,12 @@
"name": "@notive/web",
"private": true,
"scripts": {
- "dev": "vite --clearScreen false"
- },
- "devDependencies": {
- "vite": "^7.1.12"
+ "dev": "CI=true vite --clearScreen false"
},
"dependencies": {
"open-color": "^1.9.1"
+ },
+ "devDependencies": {
+ "vite": "^7.1.12"
}
}
diff --git a/web/src/components/app/index.css b/web/src/components/app/index.css
new file mode 100644
index 0000000..3eeaee9
--- /dev/null
+++ b/web/src/components/app/index.css
@@ -0,0 +1,3 @@
+ntv-app {
+ display: block;
+}
diff --git a/web/components/app/index.ts b/web/src/components/app/index.ts
index f4eaff1..5967a46 100644
--- a/web/components/app/index.ts
+++ b/web/src/components/app/index.ts
@@ -1,10 +1,11 @@
import ntvGrid from "../grid";
+import "./index.css";
class NotiveAppElement extends HTMLElement {
connectedCallback() {
this.append(
...window.notive.doc.grids.map((_grid) => {
- return ntvGrid({ foo: "1" });
+ return ntvGrid();
}),
);
}
diff --git a/web/src/components/grid/index.css b/web/src/components/grid/index.css
new file mode 100644
index 0000000..296c155
--- /dev/null
+++ b/web/src/components/grid/index.css
@@ -0,0 +1,8 @@
+ntv-grid {
+ display: block;
+}
+
+ntv-grid > canvas {
+ display: block;
+ width: 100%;
+}
diff --git a/web/src/components/grid/index.ts b/web/src/components/grid/index.ts
new file mode 100644
index 0000000..18bf75a
--- /dev/null
+++ b/web/src/components/grid/index.ts
@@ -0,0 +1,24 @@
+import h, { type CreateElement } from "../../html";
+import "./index.css";
+import colors from "open-color";
+
+class NotiveGridElement extends HTMLElement {
+ canvasEl: HTMLCanvasElement = h.canvas();
+
+ connectedCallback() {
+ this.append(this.canvasEl);
+ this.draw();
+ }
+
+ draw() {
+ const ctx = this.canvasEl.getContext("2d");
+ if (!ctx) throw new Error("Unable to get canvas context");
+ ctx.fillStyle = colors.gray[8];
+ ctx.fillRect(0, 0, this.canvasEl.width, this.canvasEl.height);
+ }
+}
+
+customElements.define("ntv-grid", NotiveGridElement);
+
+export default ((...args: any[]): NotiveGridElement =>
+ (h as any)["ntv-grid"](...args)) as CreateElement<NotiveGridElement>;
diff --git a/web/components/index.ts b/web/src/components/index.ts
index 8bc14e7..8bc14e7 100644
--- a/web/components/index.ts
+++ b/web/src/components/index.ts
diff --git a/web/favicon.ico b/web/src/favicon.ico
index c10cfe9..c10cfe9 100644
--- a/web/favicon.ico
+++ b/web/src/favicon.ico
Binary files differ
diff --git a/web/global.d.ts b/web/src/global.d.ts
index 3b6d980..3b6d980 100644
--- a/web/global.d.ts
+++ b/web/src/global.d.ts
diff --git a/web/html.ts b/web/src/html.ts
index 5bfff21..5bfff21 100644
--- a/web/html.ts
+++ b/web/src/html.ts
diff --git a/web/index.css b/web/src/index.css
index f578562..f578562 100644
--- a/web/index.css
+++ b/web/src/index.css
diff --git a/web/index.html b/web/src/index.html
index 846f5be..846f5be 100644
--- a/web/index.html
+++ b/web/src/index.html
diff --git a/web/index.ts b/web/src/index.ts
index 1524b04..a32aaf1 100644
--- a/web/index.ts
+++ b/web/src/index.ts
@@ -1,29 +1,14 @@
-interface Cell {
- value: string;
-}
-
-interface Row {
- cells: Cell[];
-}
-
-interface Part {
- rows: Row[];
-}
-
-interface Grid {
- parts: Part[];
-}
-
-interface Doc {
- grids: Grid[];
-}
+import { Doc } from "./types";
function defaultDoc(): Doc {
const defaultCells = Array(16).map(() => ({ value: "1" }));
return {
grids: [
- { parts: [{ rows: Array(4).map(() => ({ cells: [...defaultCells] })) }] },
+ {
+ baseCellSize: 48,
+ parts: [{ rows: Array(4).map(() => ({ cells: [...defaultCells] })) }],
+ },
],
};
}
diff --git a/web/src/math/Coord.ts b/web/src/math/Coord.ts
new file mode 100644
index 0000000..db7ee6d
--- /dev/null
+++ b/web/src/math/Coord.ts
@@ -0,0 +1,23 @@
+/** A coord on a grid whose origin is in the top left. */
+export default class Coord {
+ private readonly _x: number;
+ private readonly _y: number;
+
+ constructor(x: number, y: number) {
+ this._x = x;
+ this._y = y;
+ }
+
+ get x(): number {
+ return this._x;
+ }
+
+ get y(): number {
+ return this._y;
+ }
+
+ /** Get the squared distance of this point from the origin. */
+ squaredDistanceFromOrigin(): number {
+ return this._x * this._x + this._y * this._y;
+ }
+}
diff --git a/web/src/math/Ratio.ts b/web/src/math/Ratio.ts
new file mode 100644
index 0000000..4973ff4
--- /dev/null
+++ b/web/src/math/Ratio.ts
@@ -0,0 +1,61 @@
+/** Serializable representation of a ratio. */
+export type RatioData = [numerator: number, denominator: number];
+
+/** Representation of a ratio for performing fractional artithmetic. */
+export default class Ratio {
+ private readonly _numerator: number;
+ private readonly _denominator: number;
+
+ get numerator(): number {
+ return this._numerator;
+ }
+
+ get denominator(): number {
+ return this._denominator;
+ }
+
+ constructor(numerator: number, denominator: number) {
+ if (!Number.isInteger(numerator) || !Number.isInteger(denominator)) {
+ throw new TypeError(
+ `Ratio must have integer parts: ${numerator} / ${denominator}`,
+ );
+ }
+
+ if (denominator === 0) {
+ throw new RangeError("Ratio demnominator cannot be zero");
+ }
+
+ this._numerator = numerator;
+ this._denominator = denominator;
+ }
+
+ multiplyRatio(other: Ratio): Ratio {
+ return new Ratio(
+ this.numerator * other.numerator,
+ this.denominator * other.denominator,
+ );
+ }
+
+ divideRatio(other: Ratio): Ratio {
+ return new Ratio(
+ this.numerator * other.denominator,
+ this.denominator * other.numerator,
+ );
+ }
+
+ toNumber(): number {
+ return this.numerator / this.denominator;
+ }
+
+ static fromInteger(n: number): Ratio {
+ return new Ratio(n, 1);
+ }
+
+ toData(): RatioData {
+ return [this.numerator, this.denominator];
+ }
+
+ static fromData(ratio: RatioData): Ratio {
+ return new Ratio(ratio[0], ratio[1]);
+ }
+}
diff --git a/web/src/math/Rect.ts b/web/src/math/Rect.ts
new file mode 100644
index 0000000..e26fbae
--- /dev/null
+++ b/web/src/math/Rect.ts
@@ -0,0 +1,97 @@
+import Coord from "./Coord";
+
+/** A rectangle on a grid whose origin is in the top left. */
+export default class Rect {
+ private readonly _topLeft: Coord;
+ private readonly _width: number;
+ private readonly _height: number;
+
+ constructor(
+ topLeftX: number,
+ topLeftY: number,
+ width: number,
+ height: number,
+ ) {
+ this._topLeft = new Coord(topLeftX, topLeftY);
+ this._width = width;
+ this._height = height;
+ }
+
+ /** Width of this rectangle. */
+ get width(): number {
+ return this._width;
+ }
+
+ /** Height of this rectangle. */
+ get height(): number {
+ return this._height;
+ }
+
+ /** Coord of the top-left point of this rectangle. */
+ get topLeft(): Coord {
+ return this._topLeft;
+ }
+
+ /** Coord of the bottom-right point of this rectangle. */
+ get bottomRight(): Coord {
+ return new Coord(
+ this._topLeft.x + this._width,
+ this._topLeft.y + this._height,
+ );
+ }
+
+ /** Determine if this rectangle contains the point at `coord`. */
+ containsCoord(coord: Coord): boolean {
+ return (
+ this.topLeft.x <= coord.x &&
+ coord.x <= this.bottomRight.x &&
+ this.topLeft.y <= coord.y &&
+ coord.y <= this.bottomRight.y
+ );
+ }
+
+ verticallyContainsCoord(coord: Coord): boolean {
+ return this.topLeft.y <= coord.y && coord.y <= this.bottomRight.y;
+ }
+
+ horizontallyContainsCoord(coord: Coord): boolean {
+ return this.topLeft.x <= coord.x && coord.x <= this.bottomRight.x;
+ }
+
+ extend(other: Rect): Rect {
+ const topLeftX = Math.min(
+ this.topLeft.x,
+ this.bottomRight.x,
+ other.topLeft.x,
+ other.bottomRight.x,
+ );
+
+ const topLeftY = Math.min(
+ this.topLeft.y,
+ this.bottomRight.y,
+ other.topLeft.y,
+ other.bottomRight.y,
+ );
+
+ const bottomRightX = Math.max(
+ this.topLeft.x,
+ this.bottomRight.x,
+ other.topLeft.x,
+ other.bottomRight.x,
+ );
+
+ const bottomRightY = Math.max(
+ this.topLeft.y,
+ this.bottomRight.y,
+ other.topLeft.y,
+ other.bottomRight.y,
+ );
+
+ return new Rect(
+ topLeftX,
+ topLeftY,
+ bottomRightX - topLeftX,
+ bottomRightY - topLeftY,
+ );
+ }
+}
diff --git a/web/src/renderGrid.ts b/web/src/renderGrid.ts
new file mode 100644
index 0000000..476876b
--- /dev/null
+++ b/web/src/renderGrid.ts
@@ -0,0 +1,64 @@
+import Rect from "./math/Rect";
+import { Cell, CellRef, Grid, Row, RowRef } from "./types";
+
+export interface RenderedCell extends Cell {
+ cellRef: CellRef;
+ rect: Rect;
+}
+
+export interface RenderedRow {
+ rowRef: RowRef;
+ rect: Rect;
+ renderedCells: RenderedCell[];
+}
+
+export interface RenderedGrid extends Grid {
+ rect: Rect;
+ renderedRows: RenderedRow[];
+}
+
+function renderCell(grid: Grid, row: Row, cell: Cell): RenderedCell {}
+
+function renderRow(grid: Grid, row: Row): RenderedRow {
+ let topLeftX = 0;
+
+ const renderedCells = row.cells.map((cell, cellIndex) => {
+ const renderedCell = renderCell(grid, row, cell);
+ topLeftX = renderedCell.rect.bottomRight.y;
+ return renderedCell;
+ });
+}
+
+function renderRows(grid: Grid): RenderedRow[] {
+ const renderedRows: RenderedRow[] = [];
+
+ let partIndex = 0;
+ let rowIndex = 0;
+ let topLeftY = 0;
+ let renderedRowIndex = 0;
+
+ while (true) {
+ if (!grid.parts[partIndex]?.rows[rowIndex]) break;
+
+ const row = grid.parts[partIndex].rows[rowIndex];
+ const renderedRow = renderRow(grid, row);
+
+ topLeftY = renderedRow.rect.bottomRight.y;
+ renderedRows.push(renderedRow);
+
+ if (!grid.parts[++partIndex]) {
+ partIndex = 0;
+ rowIndex++;
+ }
+
+ renderedRowIndex++;
+ }
+
+ return renderedRows;
+}
+
+export default function renderGrid(grid: Grid) {
+ const renderedRows = renderRows(grid);
+ const rect = renderedRows[0].rect.extend(renderedRows.at(-1)!.rect);
+ return { ...grid, rect, renderedRows };
+}
diff --git a/web/src/types.ts b/web/src/types.ts
new file mode 100644
index 0000000..df421d7
--- /dev/null
+++ b/web/src/types.ts
@@ -0,0 +1,36 @@
+import Ratio from "./math/Ratio";
+
+export interface Cell {
+ value?: string;
+}
+
+export interface Row {
+ cells: [Cell, ...Cell[]];
+}
+
+export interface Part {
+ title?: string;
+ rows: [Row, ...Row[]];
+}
+
+export interface Grid {
+ id: string;
+ baseCellSize: number;
+ baseCellWidthRatio: Ratio;
+ parts: [Part, ...Part[]];
+}
+
+export interface Doc {
+ grids: Grid[];
+}
+
+export interface RowRef {
+ partIndex: number;
+ rowIndex: number;
+}
+
+export interface CellRef {
+ partIndex: number;
+ rowIndex: number;
+ cellIndex: number;
+}
diff --git a/web/tsconfig.json b/web/tsconfig.json
index 7ef66ad..91be5ef 100644
--- a/web/tsconfig.json
+++ b/web/tsconfig.json
@@ -1,12 +1,12 @@
{
"compilerOptions": {
- "target": "ES2020",
+ "target": "ESNext",
"module": "ESNext",
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true
},
- "include": ["*.ts", "**/*.ts"]
+ "include": ["src/*.ts", "src/**/*.ts"]
}
diff --git a/web/vite.config.ts b/web/vite.config.ts
new file mode 100644
index 0000000..d59c396
--- /dev/null
+++ b/web/vite.config.ts
@@ -0,0 +1,3 @@
+export default {
+ root: "src",
+};