From 5404a95c15e176d25728bf1a319ddb9828b23625 Mon Sep 17 00:00:00 2001 From: Josh Kingsley Date: Sat, 25 Oct 2025 20:46:35 +0300 Subject: refactor(web): re-organize files --- web/src/components/app/index.css | 3 ++ web/src/components/app/index.ts | 14 ++++++ web/src/components/grid/index.css | 8 ++++ web/src/components/grid/index.ts | 24 ++++++++++ web/src/components/index.ts | 2 + web/src/favicon.ico | Bin 0 -> 15406 bytes web/src/global.d.ts | 9 ++++ web/src/html.ts | 26 ++++++++++ web/src/index.css | 10 ++++ web/src/index.html | 13 +++++ web/src/index.ts | 22 +++++++++ web/src/math/Coord.ts | 23 +++++++++ web/src/math/Ratio.ts | 61 ++++++++++++++++++++++++ web/src/math/Rect.ts | 97 ++++++++++++++++++++++++++++++++++++++ web/src/renderGrid.ts | 64 +++++++++++++++++++++++++ web/src/types.ts | 36 ++++++++++++++ 16 files changed, 412 insertions(+) create mode 100644 web/src/components/app/index.css create mode 100644 web/src/components/app/index.ts create mode 100644 web/src/components/grid/index.css create mode 100644 web/src/components/grid/index.ts create mode 100644 web/src/components/index.ts create mode 100644 web/src/favicon.ico create mode 100644 web/src/global.d.ts create mode 100644 web/src/html.ts create mode 100644 web/src/index.css create mode 100644 web/src/index.html create mode 100644 web/src/index.ts create mode 100644 web/src/math/Coord.ts create mode 100644 web/src/math/Ratio.ts create mode 100644 web/src/math/Rect.ts create mode 100644 web/src/renderGrid.ts create mode 100644 web/src/types.ts (limited to 'web/src') 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/src/components/app/index.ts b/web/src/components/app/index.ts new file mode 100644 index 0000000..5967a46 --- /dev/null +++ b/web/src/components/app/index.ts @@ -0,0 +1,14 @@ +import ntvGrid from "../grid"; +import "./index.css"; + +class NotiveAppElement extends HTMLElement { + connectedCallback() { + this.append( + ...window.notive.doc.grids.map((_grid) => { + return ntvGrid(); + }), + ); + } +} + +customElements.define("ntv-app", NotiveAppElement); 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; diff --git a/web/src/components/index.ts b/web/src/components/index.ts new file mode 100644 index 0000000..8bc14e7 --- /dev/null +++ b/web/src/components/index.ts @@ -0,0 +1,2 @@ +import "./app"; +import "./grid"; diff --git a/web/src/favicon.ico b/web/src/favicon.ico new file mode 100644 index 0000000..c10cfe9 Binary files /dev/null and b/web/src/favicon.ico differ diff --git a/web/src/global.d.ts b/web/src/global.d.ts new file mode 100644 index 0000000..3b6d980 --- /dev/null +++ b/web/src/global.d.ts @@ -0,0 +1,9 @@ +import type Notive from "./index"; + +declare global { + interface Window { + notive: Notive; + } +} + +export {}; diff --git a/web/src/html.ts b/web/src/html.ts new file mode 100644 index 0000000..5bfff21 --- /dev/null +++ b/web/src/html.ts @@ -0,0 +1,26 @@ +export type CreateElement = { + (...children: (Node | string)[]): T; + (attrs: Partial, ...children: (Node | string)[]): T; +}; + +type ElementCreator = { + [K in keyof HTMLElementTagNameMap]: CreateElement; +}; + +const h = new Proxy({} as ElementCreator, { + get: + (_, tag: string) => + (...args: any[]) => { + const el = document.createElement(tag); + + if (typeof args[0] === "object") { + Object.assign(el, args.shift()); + } + + el.append(...args.flat()); + + return el; + }, +}); + +export default h; diff --git a/web/src/index.css b/web/src/index.css new file mode 100644 index 0000000..f578562 --- /dev/null +++ b/web/src/index.css @@ -0,0 +1,10 @@ +@import "open-color"; + +body { + background: var(--oc-gray-9); + color: var(--oc-white); + font-weight: normal; + font-family: + Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans", source-sans-pro, + sans-serif; +} diff --git a/web/src/index.html b/web/src/index.html new file mode 100644 index 0000000..846f5be --- /dev/null +++ b/web/src/index.html @@ -0,0 +1,13 @@ + + + + + Notive + + + + + + + + diff --git a/web/src/index.ts b/web/src/index.ts new file mode 100644 index 0000000..a32aaf1 --- /dev/null +++ b/web/src/index.ts @@ -0,0 +1,22 @@ +import { Doc } from "./types"; + +function defaultDoc(): Doc { + const defaultCells = Array(16).map(() => ({ value: "1" })); + + return { + grids: [ + { + baseCellSize: 48, + parts: [{ rows: Array(4).map(() => ({ cells: [...defaultCells] })) }], + }, + ], + }; +} + +export default class Notive { + doc: Doc = defaultDoc(); +} + +window.notive = new Notive(); + +window.dispatchEvent(new CustomEvent("ntv:initialized")); 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; +} -- cgit v1.2.3