web based infinite canvas
1import { BehaviorSubject, type Subscription } from "rxjs";
2import { Vec2 } from "./math";
3
4/**
5 * Cursor position + timing in world/screen space.
6 *
7 * CursorState is intentionally separate from EditorState so it can be updated
8 * with high frequency (e.g., on pointer move) without touching history or
9 * triggering document persistence.
10 */
11export type CursorState = { cursorWorld: Vec2; cursorScreen?: Vec2; lastMoveAt: number };
12
13export const CursorState = {
14 /**
15 * Create a cursor state positioned at origin with no screen point.
16 */
17 create(world?: Vec2, screen?: Vec2, timestamp = Date.now()): CursorState {
18 return {
19 cursorWorld: Vec2.clone(world ?? { x: 0, y: 0 }),
20 cursorScreen: screen ? Vec2.clone(screen) : undefined,
21 lastMoveAt: timestamp,
22 };
23 },
24};
25
26export type CursorListener = (state: CursorState) => void;
27
28/**
29 * Store that tracks cursor movement separately from the undoable editor state.
30 */
31export class CursorStore {
32 private readonly state$: BehaviorSubject<CursorState>;
33
34 constructor(initialState?: CursorState) {
35 this.state$ = new BehaviorSubject(initialState ?? CursorState.create());
36 }
37
38 /**
39 * Read the latest cursor snapshot.
40 */
41 getState(): CursorState {
42 return this.state$.value;
43 }
44
45 /**
46 * Subscribe to cursor updates.
47 */
48 subscribe(listener: CursorListener): () => void {
49 const subscription: Subscription = this.state$.subscribe(listener);
50 return () => subscription.unsubscribe();
51 }
52
53 /**
54 * Update the cursor position without touching editor history/persistence.
55 */
56 updateCursor(world: Vec2, screen?: Vec2, timestamp = Date.now()): void {
57 this.state$.next({
58 cursorWorld: Vec2.clone(world),
59 cursorScreen: screen ? Vec2.clone(screen) : undefined,
60 lastMoveAt: timestamp,
61 });
62 }
63}