diff options
| author | Josh Kingsley <josh@joshkingsley.me> | 2025-10-25 20:46:35 +0300 |
|---|---|---|
| committer | Josh Kingsley <josh@joshkingsley.me> | 2025-10-25 22:09:48 +0300 |
| commit | 5404a95c15e176d25728bf1a319ddb9828b23625 (patch) | |
| tree | 639d175e15170618d36c0b22b3c8ad7764925175 /web | |
| parent | 2a4d7a7fc3b968ed8cdfd958a5e65fbe140042da (diff) | |
refactor(web): re-organize files
Diffstat (limited to 'web')
| -rw-r--r-- | web/components/grid/index.ts | 14 | ||||
| -rw-r--r-- | web/package.json | 8 | ||||
| -rw-r--r-- | web/src/components/app/index.css | 3 | ||||
| -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.css | 8 | ||||
| -rw-r--r-- | web/src/components/grid/index.ts | 24 | ||||
| -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) | bin | 15406 -> 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.ts | 23 | ||||
| -rw-r--r-- | web/src/math/Ratio.ts | 61 | ||||
| -rw-r--r-- | web/src/math/Rect.ts | 97 | ||||
| -rw-r--r-- | web/src/renderGrid.ts | 64 | ||||
| -rw-r--r-- | web/src/types.ts | 36 | ||||
| -rw-r--r-- | web/tsconfig.json | 6 | ||||
| -rw-r--r-- | web/vite.config.ts | 3 |
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 Binary files differindex c10cfe9..c10cfe9 100644 --- a/web/favicon.ico +++ b/web/src/favicon.ico 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", +}; |
