summaryrefslogtreecommitdiff
path: root/web/src
diff options
context:
space:
mode:
Diffstat (limited to 'web/src')
-rw-r--r--web/src/components/app/index.ts1
-rw-r--r--web/src/components/grid/drawGrid.ts188
-rw-r--r--web/src/components/grid/index.css11
-rw-r--r--web/src/components/grid/index.ts69
-rw-r--r--web/src/index.ts67
-rw-r--r--web/src/selection.ts43
-rw-r--r--web/src/types.ts8
7 files changed, 308 insertions, 79 deletions
diff --git a/web/src/components/app/index.ts b/web/src/components/app/index.ts
index 910aa52..ec40754 100644
--- a/web/src/components/app/index.ts
+++ b/web/src/components/app/index.ts
@@ -1,4 +1,3 @@
-import h from "../../html";
import ntvGrid from "../grid";
import ntvToolbar from "../toolbar";
import "./index.css";
diff --git a/web/src/components/grid/drawGrid.ts b/web/src/components/grid/drawGrid.ts
index 8e64479..5ea17b6 100644
--- a/web/src/components/grid/drawGrid.ts
+++ b/web/src/components/grid/drawGrid.ts
@@ -1,21 +1,61 @@
-import colors from "tailwindcss/colors";
-import { PendingSelection, Selection } from "../../selection";
+import { RangeSelection, Selection } from "../../selection";
import { CellRef } from "../../types";
import { RenderedCell, RenderedGrid } from "./renderGrid";
-function fillBackground(ctx: CanvasRenderingContext2D, grid: RenderedGrid) {
+interface GridColors {
+ bgFill: string;
+ borderStroke: string;
+ cellStroke: string;
+ selelectionRangeFill: string;
+ selectionRangeStroke: string;
+ activeCellStroke: string;
+}
+
+export function getGridColors(el: HTMLElement): GridColors {
+ const style = window.getComputedStyle(el);
+ const color = (k: string) => style.getPropertyValue(k);
+
+ return {
+ bgFill: color("--grid-bg-fill"),
+ borderStroke: color("--grid-border-stroke"),
+ cellStroke: color("--grid-cell-stroke"),
+ selelectionRangeFill: color("--grid-selection-range-fill"),
+ selectionRangeStroke: color("--grid-selection-range-stroke"),
+ activeCellStroke: color("--grid-active-cell-stroke"),
+ };
+}
+
+function excursion(ctx: CanvasRenderingContext2D, f: () => void) {
+ ctx.save();
+ f();
+ ctx.restore();
+}
+
+function fillBackground(
+ ctx: CanvasRenderingContext2D,
+ colors: GridColors,
+ grid: RenderedGrid,
+) {
ctx.clearRect(0, 0, grid.rect.width, grid.rect.height);
- ctx.fillStyle = colors.neutral[900];
+ ctx.fillStyle = colors.bgFill;
ctx.fillRect(0, 0, grid.rect.width, grid.rect.height);
}
-function strokeGrid(ctx: CanvasRenderingContext2D, grid: RenderedGrid) {
- ctx.strokeStyle = colors.neutral[700];
+function strokeGrid(
+ ctx: CanvasRenderingContext2D,
+ colors: GridColors,
+ grid: RenderedGrid,
+) {
+ ctx.strokeStyle = colors.borderStroke;
ctx.strokeRect(0.5, 0.5, grid.rect.width - 1, grid.rect.height - 1);
}
-function strokeGridLines(ctx: CanvasRenderingContext2D, grid: RenderedGrid) {
- ctx.strokeStyle = colors.neutral[800];
+function strokeGridLines(
+ ctx: CanvasRenderingContext2D,
+ colors: GridColors,
+ grid: RenderedGrid,
+) {
+ ctx.strokeStyle = colors.cellStroke;
grid.renderedRows.forEach((row, renderedRowIndex) => {
const isLastRow = renderedRowIndex === grid.renderedRows.length - 1;
@@ -43,65 +83,127 @@ function getRenderedCell(
return grid.renderedRows[renderedRowIndex]?.renderedCells[cellRef.cellIndex];
}
-function drawPendingSelection(
+function strokeActiveCell(
ctx: CanvasRenderingContext2D,
+ colors: GridColors,
grid: RenderedGrid,
- selection: PendingSelection,
-) {}
+ cell: RenderedCell,
+) {
+ const isLastCell = cell.rect.bottomRight.x === grid.rect.bottomRight.x;
+ const isLastRow = cell.rect.bottomRight.y === grid.rect.bottomRight.y;
-function drawSelection(
+ ctx.strokeStyle = colors.activeCellStroke;
+ ctx.lineWidth = 2;
+
+ ctx.strokeRect(
+ cell.rect.topLeft.x + 1,
+ cell.rect.topLeft.y + 1,
+ isLastCell ? cell.rect.width - 2 : cell.rect.width - 1,
+ isLastRow ? cell.rect.height - 2 : cell.rect.height - 1,
+ );
+}
+
+function drawCellRange(
ctx: CanvasRenderingContext2D,
+ colors: GridColors,
grid: RenderedGrid,
- selection: Selection,
+ start: CellRef,
+ end: CellRef,
+ { stroke }: { stroke: boolean },
) {
- if (selection.gridId !== grid.id) return;
+ const startCell = getRenderedCell(grid, start);
+ const endCell = getRenderedCell(grid, end);
- const cell = getRenderedCell(grid, selection.activeCellRef);
+ if (!startCell || !endCell) return;
- if (!cell) return;
+ const rect = startCell.rect.extend(endCell.rect);
- const isLastCell = cell.rect.bottomRight.x === grid.rect.bottomRight.x;
- const isLastRow = cell.rect.bottomRight.y === grid.rect.bottomRight.y;
+ const isRightEdge = rect.bottomRight.x === grid.rect.bottomRight.x;
+ const isBottomEdge = rect.bottomRight.y === grid.rect.bottomRight.y;
+
+ ctx.fillStyle = colors.selelectionRangeFill;
- // ctx.fillStyle = colors.green[4] + "30";
+ ctx.fillRect(
+ rect.topLeft.x + 1,
+ rect.topLeft.y + 1,
+ isRightEdge ? rect.width - 2 : rect.width - 1,
+ isBottomEdge ? rect.height - 2 : rect.height - 1,
+ );
- // ctx.fillRect(
- // cell.rect.topLeft.x + 1,
- // cell.rect.topLeft.y + 1,
- // cell.rect.width - 1,
- // cell.rect.height - 1,
- // );
+ if (!stroke) return;
- ctx.strokeStyle = colors.green[400];
- ctx.lineWidth = 2;
+ ctx.strokeStyle = colors.selectionRangeStroke;
ctx.strokeRect(
- cell.rect.topLeft.x + 1,
- cell.rect.topLeft.y + 1,
- isLastCell ? cell.rect.width - 2 : cell.rect.width - 1,
- isLastRow ? cell.rect.height - 2 : cell.rect.height - 1,
+ rect.topLeft.x + 0.5,
+ rect.topLeft.y + 0.5,
+ isRightEdge ? rect.width - 1 : rect.width,
+ isBottomEdge ? rect.height - 1 : rect.height,
);
}
+function drawPendingSelection(
+ ctx: CanvasRenderingContext2D,
+ colors: GridColors,
+ grid: RenderedGrid,
+ selection: Selection,
+) {
+ if (selection.gridId !== grid.id) return;
+
+ const activeCell = getRenderedCell(grid, selection.activeCellRef);
+
+ if (!activeCell) return;
+
+ if (selection instanceof RangeSelection) {
+ excursion(ctx, () => {
+ drawCellRange(ctx, colors, grid, selection.range[0], selection.range[1], {
+ stroke: false,
+ });
+ });
+ }
+
+ excursion(ctx, () => strokeActiveCell(ctx, colors, grid, activeCell));
+}
+
+function drawSelection(
+ ctx: CanvasRenderingContext2D,
+ colors: GridColors,
+ grid: RenderedGrid,
+ selection: Selection,
+) {
+ if (selection.gridId !== grid.id) return;
+
+ const activeCell = getRenderedCell(grid, selection.activeCellRef);
+
+ if (!activeCell) return;
+
+ if (selection instanceof RangeSelection) {
+ excursion(ctx, () => {
+ drawCellRange(ctx, colors, grid, selection.range[0], selection.range[1], {
+ stroke: true,
+ });
+ });
+ }
+
+ excursion(ctx, () => strokeActiveCell(ctx, colors, grid, activeCell));
+}
+
export default function drawGrid(
ctx: CanvasRenderingContext2D,
+ colors: GridColors,
grid: RenderedGrid,
selection?: Selection,
- pendingSelection?: PendingSelection,
+ pendingSelection?: Selection,
) {
- const excursion = (f: () => void) => {
- ctx.save();
- f();
- ctx.restore();
- };
-
- excursion(() => fillBackground(ctx, grid));
- excursion(() => strokeGridLines(ctx, grid));
- excursion(() => strokeGrid(ctx, grid));
+ excursion(ctx, () => fillBackground(ctx, colors, grid));
+ excursion(ctx, () => strokeGridLines(ctx, colors, grid));
+ excursion(ctx, () => strokeGrid(ctx, colors, grid));
if (pendingSelection) {
- excursion(() => drawPendingSelection(ctx, grid, pendingSelection));
+ excursion(ctx, () =>
+ drawPendingSelection(ctx, colors, grid, pendingSelection),
+ );
} else if (selection) {
- excursion(() => drawSelection(ctx, grid, selection));
+ excursion(ctx, () => drawSelection(ctx, colors, grid, selection));
}
}
diff --git a/web/src/components/grid/index.css b/web/src/components/grid/index.css
index a733015..825a74a 100644
--- a/web/src/components/grid/index.css
+++ b/web/src/components/grid/index.css
@@ -1,5 +1,16 @@
ntv-grid {
display: block;
+
+ --grid-bg-fill: var(--color-neutral-900);
+ --grid-border-stroke: var(--color-neutral-700);
+ --grid-cell-stroke: var(--color-neutral-800);
+ --grid-active-cell-stroke: var(--color-green-400);
+ --grid-selection-range-fill: color-mix(
+ in oklab,
+ var(--color-green-400) 10%,
+ transparent
+ );
+ --grid-selection-range-stroke: var(--color-green-400);
}
ntv-grid > canvas {
diff --git a/web/src/components/grid/index.ts b/web/src/components/grid/index.ts
index 0acace4..fbb9c60 100644
--- a/web/src/components/grid/index.ts
+++ b/web/src/components/grid/index.ts
@@ -1,6 +1,7 @@
import h, { type CreateElement } from "../../html";
+import { CellRef } from "../../types";
import cellAtCoord from "./cellAtCoord";
-import drawGrid from "./drawGrid";
+import drawGrid, { getGridColors } from "./drawGrid";
import "./index.css";
class NotiveGridElement extends HTMLElement {
@@ -26,18 +27,12 @@ class NotiveGridElement extends HTMLElement {
throw new Error("ntv-grid requries gridId attribute");
}
- this.canvasEl.addEventListener("mousedown", (event) => {
- const clientRect = this.canvasEl.getBoundingClientRect();
- const x = event.x - clientRect.x;
- const y = event.y - clientRect.y;
- const cellRef = cellAtCoord(this.renderedGrid, x, y);
- if (!cellRef) return;
- window.notive.selectCell(this.#gridId, cellRef);
- });
+ window.addEventListener("ntv:selection-changed", () => this.draw());
- window.addEventListener("ntv:selection-changed", () => {
- this.draw();
- });
+ this.canvasEl.addEventListener(
+ "mousedown",
+ this.#canvasMouseDownCallback.bind(this),
+ );
this.append(this.canvasEl);
this.draw();
@@ -55,13 +50,63 @@ class NotiveGridElement extends HTMLElement {
this.canvasEl.setAttribute("width", grid.rect.width + "px");
this.canvasEl.setAttribute("height", grid.rect.height + "px");
+ const colors = getGridColors(this);
+
drawGrid(
ctx,
+ colors,
grid,
window.notive.selection,
window.notive.pendingSelection,
);
}
+
+ #mouseEventCellRef(
+ this: NotiveGridElement,
+ event: MouseEvent,
+ ): CellRef | undefined {
+ const clientRect = this.canvasEl.getBoundingClientRect();
+ const x = event.x - clientRect.x;
+ const y = event.y - clientRect.y;
+ return cellAtCoord(this.renderedGrid, x, y);
+ }
+
+ #canvasMouseDownCallback(this: NotiveGridElement, event: MouseEvent) {
+ const cellRef = this.#mouseEventCellRef(event);
+
+ if (!cellRef) return;
+
+ window.notive.startSelecting(this.gridId, cellRef);
+
+ this.#selectionAbortController = new AbortController();
+ const { signal } = this.#selectionAbortController;
+
+ window.addEventListener(
+ "mousemove",
+ this.#selectionMouseMoveCallback.bind(this),
+ { signal },
+ );
+
+ window.addEventListener(
+ "mouseup",
+ this.#selectionMouseUpCallback.bind(this),
+ { signal },
+ );
+ }
+
+ #selectionAbortController?: AbortController;
+
+ #selectionMouseMoveCallback(this: NotiveGridElement, event: MouseEvent) {
+ const cellRef = this.#mouseEventCellRef(event);
+ if (!cellRef) return;
+ window.notive.extendSelection(cellRef);
+ }
+
+ #selectionMouseUpCallback(this: NotiveGridElement, event: MouseEvent) {
+ this.#selectionAbortController?.abort();
+ this.#selectionAbortController = undefined;
+ window.notive.finishSelecting();
+ }
}
customElements.define("ntv-grid", NotiveGridElement);
diff --git a/web/src/index.ts b/web/src/index.ts
index ac4870c..6ab61c9 100644
--- a/web/src/index.ts
+++ b/web/src/index.ts
@@ -1,7 +1,8 @@
import Ratio from "./math/Ratio";
import { Cell, CellRef, Doc, Grid } from "./types";
-import { ActiveCellSelection, PendingSelection, Selection } from "./selection";
+import { ActiveCellSelection, Selection } from "./selection";
import renderGrid, { RenderedGrid } from "./components/grid/renderGrid";
+import cellAtCoord from "./components/grid/cellAtCoord";
function defaultDoc(): Doc {
const defaultCells: Cell[] = Array.from({ length: 16 }, () => ({
@@ -12,7 +13,7 @@ function defaultDoc(): Doc {
grids: [
{
id: window.crypto.randomUUID(),
- baseCellSize: 48,
+ baseCellSize: 42,
baseCellWidthRatio: new Ratio(1, 16),
parts: [
{
@@ -24,7 +25,7 @@ function defaultDoc(): Doc {
},
{
id: window.crypto.randomUUID(),
- baseCellSize: 48,
+ baseCellSize: 42,
baseCellWidthRatio: new Ratio(1, 16),
parts: [
{
@@ -39,29 +40,59 @@ function defaultDoc(): Doc {
}
export default class Notive {
- doc: Doc = defaultDoc();
+ #doc: Doc = defaultDoc();
- gridsById = Object.fromEntries(
- this.doc.grids.map((grid) => [grid.id, renderGrid(grid)]),
+ get doc() {
+ return this.#doc;
+ }
+
+ #gridsById = Object.fromEntries(
+ this.#doc.grids.map((grid) => [grid.id, renderGrid(grid)]),
);
- selection?: Selection;
+ getGrid(id: string): RenderedGrid | undefined {
+ return this.#gridsById[id];
+ }
- pendingSelection?: Selection;
+ #selection?: Selection;
- getGrid(id: string): RenderedGrid | undefined {
- return this.gridsById[id];
+ get selection() {
+ return this.#selection;
+ }
+
+ #pendingSelection?: Selection;
+
+ get pendingSelection() {
+ return this.#pendingSelection;
}
selectCell(gridId: string, cellRef: CellRef) {
- const previousSelection = this.selection;
- this.selection = new ActiveCellSelection(gridId, cellRef);
-
- window.dispatchEvent(
- new CustomEvent("ntv:selection-changed", {
- detail: { selection: this.selection, previousSelection },
- }),
- );
+ this.#selection = new ActiveCellSelection(gridId, cellRef);
+ this.#dispatchSelectionChanged();
+ }
+
+ startSelecting(gridId: string, cellRef: CellRef) {
+ this.#pendingSelection = new ActiveCellSelection(gridId, cellRef);
+ this.#dispatchSelectionChanged();
+ }
+
+ extendSelection(cellRef: CellRef) {
+ const newSelection = this.pendingSelection?.extend(cellRef);
+
+ if (newSelection !== this.pendingSelection) {
+ this.#pendingSelection = newSelection;
+ this.#dispatchSelectionChanged();
+ }
+ }
+
+ finishSelecting() {
+ this.#selection = this.pendingSelection;
+ this.#pendingSelection = undefined;
+ this.#dispatchSelectionChanged();
+ }
+
+ #dispatchSelectionChanged() {
+ window.dispatchEvent(new CustomEvent("ntv:selection-changed"));
}
}
diff --git a/web/src/selection.ts b/web/src/selection.ts
index 88d394b..3d18417 100644
--- a/web/src/selection.ts
+++ b/web/src/selection.ts
@@ -1,4 +1,4 @@
-import { CellRef } from "./types";
+import { CellRef, cellRefEquals } from "./types";
export abstract class Selection {
readonly gridId: string;
@@ -8,12 +8,45 @@ export abstract class Selection {
this.gridId = gridId;
this.activeCellRef = activeCellRef;
}
+
+ abstract extend(cellRef: CellRef): Selection;
+}
+
+export class ActiveCellSelection extends Selection {
+ extend(cellRef: CellRef): Selection {
+ if (cellRefEquals(cellRef, this.activeCellRef)) {
+ return this;
+ }
+
+ return new RangeSelection(this.gridId, this.activeCellRef, [
+ this.activeCellRef,
+ cellRef,
+ ]);
+ }
}
-export class ActiveCellSelection extends Selection {}
+export type CellRange = [CellRef, CellRef];
-export class RangeSelection extends Selection {}
+export class RangeSelection extends Selection {
+ #range: CellRange;
-export class AllSelection extends Selection {}
+ get range() {
+ return this.#range;
+ }
-export class PendingSelection extends Selection {}
+ constructor(gridId: string, activeCellRef: CellRef, range: CellRange) {
+ super(gridId, activeCellRef);
+ this.#range = range;
+ }
+
+ extend(cellRef: CellRef): Selection {
+ if (cellRefEquals(cellRef, this.activeCellRef)) {
+ return new ActiveCellSelection(this.gridId, cellRef);
+ }
+
+ return new RangeSelection(this.gridId, this.activeCellRef, [
+ this.#range[0],
+ cellRef,
+ ]);
+ }
+}
diff --git a/web/src/types.ts b/web/src/types.ts
index 008d1ee..9b7a51a 100644
--- a/web/src/types.ts
+++ b/web/src/types.ts
@@ -35,3 +35,11 @@ export interface CellRef {
rowIndex: number;
cellIndex: number;
}
+
+export function cellRefEquals(a: CellRef, b: CellRef): boolean {
+ return (
+ a.partIndex === b.partIndex &&
+ a.rowIndex === b.rowIndex &&
+ a.cellIndex === b.cellIndex
+ );
+}