create-audora-next 0.1.7 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -5
- package/assets/audora-blog.png +0 -0
- package/assets/audora-next.webp +0 -0
- package/index.ts +9 -4
- package/package.json +8 -2
- package/templates/blog/README.md +164 -0
- package/templates/blog/bun.lock +1341 -0
- package/templates/blog/env.example.template +5 -0
- package/templates/blog/eslint.config.mjs +18 -0
- package/templates/blog/gitignore.template +41 -0
- package/templates/blog/husky.template/pre-commit +16 -0
- package/templates/blog/lint-staged.config.mjs +17 -0
- package/templates/blog/next.config.ts +38 -0
- package/templates/blog/package.json +59 -0
- package/templates/blog/postcss.config.mjs +7 -0
- package/templates/blog/public/favicon/apple-touch-icon.png +0 -0
- package/templates/blog/public/favicon/favicon-96x96.png +0 -0
- package/templates/blog/public/favicon/favicon.ico +0 -0
- package/templates/blog/public/favicon/favicon.svg +1 -0
- package/templates/blog/public/favicon/site.webmanifest +21 -0
- package/templates/blog/public/favicon/web-app-manifest-192x192.png +0 -0
- package/templates/blog/public/favicon/web-app-manifest-512x512.png +0 -0
- package/templates/blog/public/images/screenshot-desktop-dark.webp +0 -0
- package/templates/blog/public/images/screenshot-desktop-light.webp +0 -0
- package/templates/blog/public/images/screenshot-mobile-dark.webp +0 -0
- package/templates/blog/public/images/screenshot-mobile-light.webp +0 -0
- package/templates/blog/src/app/blogs/[slug]/page.tsx +171 -0
- package/templates/blog/src/app/blogs/page.tsx +108 -0
- package/templates/blog/src/app/layout.tsx +60 -0
- package/templates/blog/src/app/llms-full.txt/route.ts +97 -0
- package/templates/blog/src/app/llms.txt/route.ts +40 -0
- package/templates/blog/src/app/manifest.ts +61 -0
- package/templates/blog/src/app/page.tsx +57 -0
- package/templates/blog/src/app/robots.ts +16 -0
- package/templates/blog/src/app/sitemap.ts +52 -0
- package/templates/blog/src/blogs/components/animated-blog-list.tsx +33 -0
- package/templates/blog/src/blogs/components/blog-post-card.tsx +46 -0
- package/templates/blog/src/blogs/components/blog-section.tsx +34 -0
- package/templates/blog/src/blogs/components/blog-table-of-contents.tsx +369 -0
- package/templates/blog/src/blogs/components/copy-button.tsx +46 -0
- package/templates/blog/src/blogs/components/mdx.tsx +225 -0
- package/templates/blog/src/blogs/content/cosketch/cosketch-canvas-engine.mdx +186 -0
- package/templates/blog/src/blogs/content/cosketch/cosketch-docker-architecture.mdx +175 -0
- package/templates/blog/src/blogs/content/cosketch/cosketch-eraser-and-selection.mdx +207 -0
- package/templates/blog/src/blogs/content/hello-world.mdx +66 -0
- package/templates/blog/src/blogs/data/mdx.ts +68 -0
- package/templates/blog/src/blogs/utils/extract-headings.ts +38 -0
- package/templates/blog/src/components/copyable-code.tsx +41 -0
- package/templates/blog/src/components/footer.tsx +25 -0
- package/templates/blog/src/components/header.tsx +27 -0
- package/templates/blog/src/components/icons.tsx +84 -0
- package/templates/blog/src/components/section-heading.tsx +11 -0
- package/templates/blog/src/components/theme-provider.tsx +11 -0
- package/templates/blog/src/components/theme-toggle.tsx +20 -0
- package/templates/blog/src/components/view-all-link.tsx +56 -0
- package/templates/blog/src/config/site.ts +19 -0
- package/templates/blog/src/data/llms.ts +112 -0
- package/templates/blog/src/data/site.ts +52 -0
- package/templates/blog/src/lib/seo.ts +190 -0
- package/templates/blog/src/lib/utils.ts +83 -0
- package/templates/blog/src/styles/globals.css +99 -0
- package/templates/blog/src/utils/cn.ts +7 -0
- package/templates/blog/tsconfig.json +34 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "How CoSketch's Canvas Engine Works"
|
|
3
|
+
publishedAt: "2025-03-15"
|
|
4
|
+
summary: "A conceptual walkthrough of the CoSketch canvas engine, from pointer events to state stores and React rendering."
|
|
5
|
+
description: "A conceptual walkthrough of the CoSketch canvas engine, from pointer events to state stores and React rendering."
|
|
6
|
+
date: "2025-03-15"
|
|
7
|
+
tags:
|
|
8
|
+
- canvas
|
|
9
|
+
- architecture
|
|
10
|
+
- frontend
|
|
11
|
+
- state-management
|
|
12
|
+
- cosketch
|
|
13
|
+
repoUrl: "https://github.com/NarsiBhati-Dev/cosketch"
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Why a dedicated canvas engine?
|
|
17
|
+
|
|
18
|
+
CoSketch is a real-time collaborative whiteboard where multiple people draw and edit shapes on a shared canvas. It needs to support real-time collaboration, multiple shapes and tools, smooth interactions, and synchronization over WebSockets. To manage that complexity, the project centralizes all drawing logic in a **canvas engine** instead of scattering the rules across React components.
|
|
19
|
+
|
|
20
|
+
The engine pipeline in `apps/cosketch-frontend/src/canvas_engine/` defines how pointer events become shapes on the screen. React components in `apps/cosketch-frontend/src/components/canvas/` focus on UI and layout, while state stores in `apps/cosketch-frontend/src/stores/` keep the shared canvas state consistent and serializable.
|
|
21
|
+
|
|
22
|
+
> In short: pointer events go to the engine, the engine updates stores, and React components render from those stores.
|
|
23
|
+
|
|
24
|
+
## The core engine modules
|
|
25
|
+
|
|
26
|
+
The canvas engine is implemented as a set of focused modules under `apps/cosketch-frontend/src/canvas_engine/`:
|
|
27
|
+
|
|
28
|
+
- [`CanvasEngine.ts`](https://github.com/narsibhati-dev/cosketch/blob/master/apps/cosketch-frontend/src/canvas_engine/CanvasEngine.ts): orchestrates tools, shapes, and event handling.
|
|
29
|
+
- [`SelectionManager.ts`](https://github.com/narsibhati-dev/cosketch/blob/master/apps/cosketch-frontend/src/canvas_engine/SelectionManager.ts): encapsulates logic for tracking and manipulating selected shapes.
|
|
30
|
+
- [`eraser.ts`](https://github.com/narsibhati-dev/cosketch/blob/master/apps/cosketch-frontend/src/canvas_engine/eraser.ts): provides erasing behavior and hit-testing helpers.
|
|
31
|
+
|
|
32
|
+
From a conceptual standpoint, `CanvasEngine` is the **central coordinator**:
|
|
33
|
+
|
|
34
|
+
- It receives low-level pointer events from React components (`pointerdown`, `pointermove`, `pointerup`).
|
|
35
|
+
- It consults the **current tool** and canvas state to decide what to do (draw, move, resize, erase, select).
|
|
36
|
+
- It updates one or more shared stores to reflect the new canvas state.
|
|
37
|
+
|
|
38
|
+
`SelectionManager` and `eraser` are specialized collaborators that plug into this flow, but they do not render UI themselves.
|
|
39
|
+
|
|
40
|
+
## State stores: the source of truth
|
|
41
|
+
|
|
42
|
+
The canvas engine heavily relies on a set of dedicated stores under `apps/cosketch-frontend/src/stores/`:
|
|
43
|
+
|
|
44
|
+
- [`tool.store.ts`](https://github.com/narsibhati-dev/cosketch/blob/master/apps/cosketch-frontend/src/stores/tool.store.ts)
|
|
45
|
+
- [`canvas.store.ts`](https://github.com/narsibhati-dev/cosketch/blob/master/apps/cosketch-frontend/src/stores/canvas.store.ts)
|
|
46
|
+
- [`canvas_style.store.ts`](https://github.com/narsibhati-dev/cosketch/blob/master/apps/cosketch-frontend/src/stores/canvas_style.store.ts)
|
|
47
|
+
- [`shape_selected.store.ts`](https://github.com/narsibhati-dev/cosketch/blob/master/apps/cosketch-frontend/src/stores/shape_selected.store.ts)
|
|
48
|
+
|
|
49
|
+
At a high level, each store has a distinct responsibility:
|
|
50
|
+
|
|
51
|
+
- **Tool store (`tool.store.ts`)**: which tool is active (e.g., rectangle, ellipse, diamond, arrow, line, freehand, text, eraser, move).
|
|
52
|
+
- **Canvas store (`canvas.store.ts`)**: the actual shapes, their geometry, and any serialized representation of the canvas.
|
|
53
|
+
- **Canvas style store (`canvas_style.store.ts`)**: stroke color, fill color, line width, and other styling options.
|
|
54
|
+
- **Selection store (`shape_selected.store.ts`)**: which shapes are currently selected and how they are being manipulated.
|
|
55
|
+
|
|
56
|
+
React components subscribe to these stores to stay in sync, while the canvas engine writes updates to them in response to user actions and incoming collaboration events.
|
|
57
|
+
|
|
58
|
+
> Data flows from the tool and canvas-style stores into the canvas store when shapes are created or updated; the selection store tracks what is selected.
|
|
59
|
+
|
|
60
|
+
## From pointer events to shapes
|
|
61
|
+
|
|
62
|
+
The pipeline from a user’s pointer event to a rendered shape is conceptually straightforward but implemented with many moving parts.
|
|
63
|
+
|
|
64
|
+
For instance, when you draw a rectangle: the UI fires pointerdown at the start point, pointermove as you drag, and pointerup when you release. The engine creates a rectangle primitive, updates it on each pointermove, and writes the final shape to the canvas store. React subscribes to the store and re-renders, so the new rectangle appears on screen and can be synced to other users.
|
|
65
|
+
|
|
66
|
+
### 1. UI captures the event
|
|
67
|
+
|
|
68
|
+
React components in `apps/cosketch-frontend/src/components/canvas/` (such as `canvas.tsx`, toolbar, and sidebar) are responsible for attaching event listeners to the drawing surface. When a user interacts with the canvas:
|
|
69
|
+
|
|
70
|
+
- `pointerdown` marks the beginning of a gesture.
|
|
71
|
+
- `pointermove` tracks the gesture over time (for drawing or dragging).
|
|
72
|
+
- `pointerup` finalizes the gesture and commits changes.
|
|
73
|
+
|
|
74
|
+
These components do not interpret the event in terms of shapes. Instead, they call into `CanvasEngine` with a normalized event object.
|
|
75
|
+
|
|
76
|
+
For example, a simplified handler might look like:
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
canvasEngine.handlePointerDown({
|
|
80
|
+
x: event.clientX,
|
|
81
|
+
y: event.clientY,
|
|
82
|
+
pointerId: event.pointerId,
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 2. Engine consults the current tool
|
|
87
|
+
|
|
88
|
+
Inside `CanvasEngine`, the next step is to determine what the user intends to do by checking the active tool from `tool.store.ts`. The logic is conceptually:
|
|
89
|
+
|
|
90
|
+
The real implementation may use a map of tool handlers; this switch illustrates the idea:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const currentTool = toolStore.getState().currentTool;
|
|
94
|
+
|
|
95
|
+
switch (currentTool) {
|
|
96
|
+
case "rectangle":
|
|
97
|
+
// start a new rectangle
|
|
98
|
+
break;
|
|
99
|
+
case "arrow":
|
|
100
|
+
// start a new arrow
|
|
101
|
+
break;
|
|
102
|
+
case "text":
|
|
103
|
+
// place or edit text
|
|
104
|
+
break;
|
|
105
|
+
case "eraser":
|
|
106
|
+
// delegate to eraser behavior
|
|
107
|
+
break;
|
|
108
|
+
case "selection":
|
|
109
|
+
// delegate to SelectionManager
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Each tool has its own conventions:
|
|
115
|
+
|
|
116
|
+
- **Rectangle / ellipse / diamond**: usually start from an origin point and expand as the pointer moves.
|
|
117
|
+
- **Arrow / line**: defined by a start and end point.
|
|
118
|
+
- **Freehand**: a polyline or spline constructed from many pointer samples.
|
|
119
|
+
- **Text**: placing a text box and then managing text editing separately.
|
|
120
|
+
|
|
121
|
+
### 3. Updating the stores
|
|
122
|
+
|
|
123
|
+
As the engine interprets pointer events, it updates the relevant stores:
|
|
124
|
+
|
|
125
|
+
- New shapes are added to `canvas.store.ts`.
|
|
126
|
+
- Style information is pulled from `canvas_style.store.ts` and attached to shapes.
|
|
127
|
+
- Selection state may be updated in `shape_selected.store.ts` if the tool implies selection changes.
|
|
128
|
+
|
|
129
|
+
Because stores are the source of truth, React components re-render automatically as the data changes, and the WebSocket layer can serialize store changes for collaboration.
|
|
130
|
+
|
|
131
|
+
## UI components around the engine
|
|
132
|
+
|
|
133
|
+
The engine and stores do not render anything by themselves—the user sees the canvas and controls through React components. The React components in `apps/cosketch-frontend/src/components/canvas/` provide the user-facing controls that drive the engine:
|
|
134
|
+
|
|
135
|
+
- Toolbar and buttons in `components/canvas/toolbar/` let users choose tools and actions.
|
|
136
|
+
- Sidebar components in `components/canvas/sidebar/` expose shape and color options.
|
|
137
|
+
- Footer components in `components/canvas/footer/` manage zoom, status indicators, and encryption display.
|
|
138
|
+
|
|
139
|
+
These components:
|
|
140
|
+
|
|
141
|
+
- Read from stores (e.g., which tool is active, current zoom level).
|
|
142
|
+
- Dispatch actions to change store values (e.g., selecting a new tool or color).
|
|
143
|
+
- Call engine methods when significant pointer or keyboard events occur.
|
|
144
|
+
|
|
145
|
+
The key design decision is that the **React layer is thin**: it owns DOM and layout, while the engine and stores own business logic and state.
|
|
146
|
+
|
|
147
|
+
> Toolbar, sidebar, and footer wrap the canvas and talk to the engine and stores; they do not contain drawing logic.
|
|
148
|
+
|
|
149
|
+
## Shapes and internal primitives
|
|
150
|
+
|
|
151
|
+
While the exact shape models live in the code, conceptually each visual object on the canvas is represented as a **shape primitive** with:
|
|
152
|
+
|
|
153
|
+
- A type (rectangle, ellipse, diamond, arrow, line, freehand, text).
|
|
154
|
+
- Geometry (position, size, path data).
|
|
155
|
+
- Style (stroke color, fill color, line width, opacity).
|
|
156
|
+
- Metadata (e.g., ID, timestamps, ownership or author).
|
|
157
|
+
|
|
158
|
+
The engine adds and mutates these primitives within the canvas store as tools are used. For example:
|
|
159
|
+
|
|
160
|
+
- Drawing a rectangle creates a new rectangle primitive and updates it during `pointermove`.
|
|
161
|
+
- Moving a group of shapes updates their positions while preserving IDs so other clients can reconcile changes.
|
|
162
|
+
- Text primitives may contain additional fields for the text content and font properties.
|
|
163
|
+
|
|
164
|
+
Because shapes are plain data objects, they are easy to:
|
|
165
|
+
|
|
166
|
+
- Serialize over WebSockets.
|
|
167
|
+
- Persist via the backend API.
|
|
168
|
+
- Re-apply when clients reconnect.
|
|
169
|
+
|
|
170
|
+
## Lessons learned & design trade-offs
|
|
171
|
+
|
|
172
|
+
A few takeaways from building the engine:
|
|
173
|
+
|
|
174
|
+
- **Separation of concerns**: Keeping canvas logic in `canvas_engine` and state in stores helps avoid React components becoming overly complex and difficult to test.
|
|
175
|
+
- **Data-first design**: Representing shapes as serializable data structures makes collaboration and persistence natural but requires more discipline in versioning the shape schema.
|
|
176
|
+
- **Tool-centric interaction**: Routing all input through the current tool simplifies reasoning about behavior, but it adds a layer of indirection that developers must learn.
|
|
177
|
+
|
|
178
|
+
## What’s next
|
|
179
|
+
|
|
180
|
+
For how eraser and selection fit into this pipeline, see [Eraser & Selection Mechanics](/blogs/cosketch-eraser-and-selection).
|
|
181
|
+
|
|
182
|
+
Future evolution of the canvas engine might include:
|
|
183
|
+
|
|
184
|
+
- More sophisticated snapping and alignment features, implemented as separate modules that hook into `CanvasEngine`.
|
|
185
|
+
- A richer plugin system for tools so new shapes and behaviors can be added without changing core engine code.
|
|
186
|
+
- Optimizations for very large canvases, including offscreen rendering and more granular updates to minimize re-renders.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Inside CoSketch's Docker & Docker Compose Architecture"
|
|
3
|
+
publishedAt: "2025-04-10"
|
|
4
|
+
summary: "A conceptual tour of how CoSketch uses Docker, Docker Compose, Bun, and Turborepo to run the frontend, backend API, WebSocket server, and PostgreSQL database."
|
|
5
|
+
description: "A conceptual tour of how CoSketch uses Docker, Docker Compose, Bun, and Turborepo to run the frontend, backend API, WebSocket server, and PostgreSQL database."
|
|
6
|
+
date: "2025-04-10"
|
|
7
|
+
tags:
|
|
8
|
+
- docker
|
|
9
|
+
- docker-compose
|
|
10
|
+
- infrastructure
|
|
11
|
+
- architecture
|
|
12
|
+
- cosketch
|
|
13
|
+
repoUrl: "https://github.com/NarsiBhati-Dev/cosketch"
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Why CoSketch cares about containers
|
|
17
|
+
|
|
18
|
+
CoSketch is a real-time collaborative whiteboard with a rich frontend, an HTTP backend, a WebSocket server, and a PostgreSQL database. Running all of these pieces consistently across machines is hard if you rely only on local tooling, language runtimes, and environment configuration.
|
|
19
|
+
|
|
20
|
+
Containers give CoSketch a **repeatable, declarative runtime** for each service. Docker images built from the production Dockerfiles in `docker/` can be launched locally, in CI, or in production with predictable behavior. Docker Compose then layers orchestration on top, wiring the services together and defining how they start and talk to each other.
|
|
21
|
+
|
|
22
|
+
> In short: the frontend, backend, WebSocket server, and PostgreSQL database run as separate containers; Docker Compose wires them together over a shared network.
|
|
23
|
+
|
|
24
|
+
## The containerized services in CoSketch
|
|
25
|
+
|
|
26
|
+
At a conceptual level, CoSketch runs four main services in containers:
|
|
27
|
+
|
|
28
|
+
- **Frontend (Next.js + React)**: the user-facing app in `apps/cosketch-frontend/`.
|
|
29
|
+
- **Backend API (REST)**: the HTTP API implemented in `apps/cosketch-backend/`.
|
|
30
|
+
- **WebSocket server**: a dedicated real-time collaboration service in `apps/cosketch-websocket/`.
|
|
31
|
+
- **PostgreSQL database**: the persistent data store managed via the `packages/database` package and Prisma migrations.
|
|
32
|
+
|
|
33
|
+
Each of the application services is built using a dedicated production Dockerfile in the `docker/` directory:
|
|
34
|
+
|
|
35
|
+
- `docker/backend.prod.Dockerfile`
|
|
36
|
+
- `docker/frontend.prod.Dockerfile`
|
|
37
|
+
- `docker/websocket.prod.Dockerfile`
|
|
38
|
+
|
|
39
|
+
These Dockerfiles encapsulate how code is copied, dependencies are installed, and the runtime entrypoint is defined for each service.
|
|
40
|
+
|
|
41
|
+
> Each Dockerfile produces an image; the frontend, backend, and WebSocket services connect to the database and to each other as defined in the Compose file.
|
|
42
|
+
|
|
43
|
+
## Reading the production Dockerfiles
|
|
44
|
+
|
|
45
|
+
Even without looking at every line, it helps to build a mental model of what the Dockerfiles in `docker/` are doing.
|
|
46
|
+
|
|
47
|
+
### Backend image (`backend.prod.Dockerfile`)
|
|
48
|
+
|
|
49
|
+
The backend service at `apps/cosketch-backend/` exposes REST endpoints for authentication, rooms, and canvas persistence. Conceptually, the backend Dockerfile:
|
|
50
|
+
|
|
51
|
+
- **Selects a base image** with Node/Bun that matches the backend’s runtime.
|
|
52
|
+
- **Copies the monorepo or relevant app subtree** into the image.
|
|
53
|
+
- **Installs dependencies** for the backend and any shared packages (such as `packages/database` and `packages/backend-common`).
|
|
54
|
+
- **Runs build scripts** (driven by Turborepo at the root `package.json`) to emit compiled JavaScript.
|
|
55
|
+
- **Defines an entrypoint** that starts the backend server (often `bun run start` or similar) and binds to the container’s HTTP port.
|
|
56
|
+
|
|
57
|
+
From the codebase perspective, this image is responsible for serving logic found under:
|
|
58
|
+
|
|
59
|
+
- `apps/cosketch-backend/src/server.ts`
|
|
60
|
+
- `apps/cosketch-backend/src/routes/*`
|
|
61
|
+
- `apps/cosketch-backend/src/controllers/*`
|
|
62
|
+
|
|
63
|
+
### Frontend image (`frontend.prod.Dockerfile`)
|
|
64
|
+
|
|
65
|
+
The frontend service at `apps/cosketch-frontend/` is a Next.js app. Its Dockerfile typically:
|
|
66
|
+
|
|
67
|
+
- Builds the Next.js app using the monorepo build pipeline (Turborepo).
|
|
68
|
+
- Produces optimized static assets and server-side bundles.
|
|
69
|
+
- Uses a runtime image with only the pieces needed to serve the built app.
|
|
70
|
+
|
|
71
|
+
Conceptually, that means the Dockerfile:
|
|
72
|
+
|
|
73
|
+
- Installs dependencies for the frontend app and shared packages.
|
|
74
|
+
- Runs a build command such as `bun run build` or a Turborepo target that triggers the Next.js production build.
|
|
75
|
+
- Sets the entrypoint to run the Next.js server (or a minimal Node/Bun process that serves the built output).
|
|
76
|
+
|
|
77
|
+
This image serves application code from:
|
|
78
|
+
|
|
79
|
+
- `apps/cosketch-frontend/src/app/**`
|
|
80
|
+
- Canvas UI components in `apps/cosketch-frontend/src/components/canvas/**`
|
|
81
|
+
- Hooks, stores, and other frontend helpers under `apps/cosketch-frontend/src/**`.
|
|
82
|
+
|
|
83
|
+
### WebSocket image (`websocket.prod.Dockerfile`)
|
|
84
|
+
|
|
85
|
+
Real-time collaboration in CoSketch is handled by the WebSocket server in `apps/cosketch-websocket/`. This Dockerfile:
|
|
86
|
+
|
|
87
|
+
- Focuses on the WebSocket process and its dependencies.
|
|
88
|
+
- Copies the relevant source (`apps/cosketch-websocket/src/**`) into the image.
|
|
89
|
+
- Installs only what is needed to run the WebSocket server.
|
|
90
|
+
- Exposes the WebSocket port and defines an entrypoint to start `server.ts`.
|
|
91
|
+
|
|
92
|
+
From the code, the WebSocket image is the runtime home for:
|
|
93
|
+
|
|
94
|
+
- `apps/cosketch-websocket/src/server.ts`
|
|
95
|
+
- Handlers in `apps/cosketch-websocket/src/handlers/**`
|
|
96
|
+
- Auth and token utilities in `apps/cosketch-websocket/src/services/**`
|
|
97
|
+
|
|
98
|
+
> Clients hit the frontend, upgrade to WebSocket for real-time collaboration, and the WebSocket server relays canvas and presence messages between users.
|
|
99
|
+
|
|
100
|
+
## Docker Compose: composing the stack
|
|
101
|
+
|
|
102
|
+
While Dockerfiles describe **how to build images**, Docker Compose describes **how to run them together**. CoSketch uses:
|
|
103
|
+
|
|
104
|
+
- A root `docker-compose.yml` for orchestrating the main stack.
|
|
105
|
+
- `docker/db.docker-compose.yml` (and related files) for database-focused or infrastructure-only scenarios.
|
|
106
|
+
|
|
107
|
+
Conceptually, `docker-compose.yml` defines:
|
|
108
|
+
|
|
109
|
+
- **Services**: frontend, backend, websocket, and database containers.
|
|
110
|
+
- **Networks**: how containers discover each other (service names become hostnames).
|
|
111
|
+
- **Volumes**: persistence for PostgreSQL data.
|
|
112
|
+
- **Environment variables**: configuration such as database URLs, JWT secrets, and frontend API endpoints.
|
|
113
|
+
|
|
114
|
+
The application services referenced in the Compose file are built from the images whose definitions live in:
|
|
115
|
+
|
|
116
|
+
- `docker/frontend.prod.Dockerfile`
|
|
117
|
+
- `docker/backend.prod.Dockerfile`
|
|
118
|
+
- `docker/websocket.prod.Dockerfile`
|
|
119
|
+
|
|
120
|
+
> The Compose file defines the four services and a shared bridge network so they can reach each other by service name.
|
|
121
|
+
|
|
122
|
+
## Bun and Turborepo inside containers
|
|
123
|
+
|
|
124
|
+
CoSketch uses **Bun** and **Turborepo** at the monorepo root to manage builds and scripts. The root `package.json` contains scripts that orchestrate infra and dev flows, such as:
|
|
125
|
+
|
|
126
|
+
- `infra:up`
|
|
127
|
+
- `infra:down`
|
|
128
|
+
- `db:up`
|
|
129
|
+
- `db:down`
|
|
130
|
+
|
|
131
|
+
In a Dockerized environment, Bun and Turborepo serve two main purposes:
|
|
132
|
+
|
|
133
|
+
- **Build orchestration**: A single Turborepo target can build multiple apps (`cosketch-backend`, `cosketch-frontend`, `cosketch-websocket`) and shared packages (`packages/database`, `packages/types`, etc.) before images are finalized.
|
|
134
|
+
- **Unified commands**: The same scripts can be used locally and in CI to ensure that images are always built in a consistent way.
|
|
135
|
+
|
|
136
|
+
From a conceptual standpoint, the Dockerfiles often:
|
|
137
|
+
|
|
138
|
+
1. Install Bun and dependencies once at the root.
|
|
139
|
+
2. Run Turborepo build commands to compile all relevant apps.
|
|
140
|
+
3. Copy only the compiled output and necessary runtime files into the final image layers.
|
|
141
|
+
|
|
142
|
+
This keeps runtime images smaller and builds deterministic across environments.
|
|
143
|
+
|
|
144
|
+
## Local vs. production container flows
|
|
145
|
+
|
|
146
|
+
Even if you run CoSketch differently in local development and in production, the mental model is similar:
|
|
147
|
+
|
|
148
|
+
- **Local dev**:
|
|
149
|
+
- You may run some services directly with `bun dev` or similar commands.
|
|
150
|
+
- Compose is often used for the database and sometimes for the full stack when you want environment parity.
|
|
151
|
+
- **Production**:
|
|
152
|
+
- All application services run as containers built from the production Dockerfiles.
|
|
153
|
+
- Docker Compose (or an orchestrator like Kubernetes) manages replicas, restarts, and shared networks.
|
|
154
|
+
|
|
155
|
+
The root `docker-compose.yml` is effectively a **declarative recipe** for a full CoSketch stack, which can anchor both environments.
|
|
156
|
+
|
|
157
|
+
> The same images can run on a developer machine or in a production cluster; Compose (or another orchestrator) defines how they are started and connected.
|
|
158
|
+
|
|
159
|
+
## Lessons learned & design trade-offs
|
|
160
|
+
|
|
161
|
+
A few takeaways from building the stack:
|
|
162
|
+
|
|
163
|
+
- **Image boundaries matter**: Separating frontend, backend, and WebSocket into distinct images mirrors their responsibilities in the codebase and allows independent scaling.
|
|
164
|
+
- **Monorepo + Docker**: Using Turborepo and shared packages (`packages/database`, `packages/types`) means Docker builds need to be aware of the workspace structure, but the payoff is consistent types and logic across services.
|
|
165
|
+
- **Compose as documentation**: The `docker-compose.yml` file is not just an orchestration tool—it doubles as executable documentation for how CoSketch’s services relate.
|
|
166
|
+
|
|
167
|
+
## What’s next
|
|
168
|
+
|
|
169
|
+
For how the frontend canvas engine works inside the app, see [How CoSketch's Canvas Engine Works](/blogs/cosketch-canvas-engine).
|
|
170
|
+
|
|
171
|
+
Future improvements to CoSketch’s container story might include:
|
|
172
|
+
|
|
173
|
+
- Adding health checks and better observability into the images (e.g., `/healthz` endpoints and metrics scraping).
|
|
174
|
+
- Splitting build and runtime stages further to minimize image size.
|
|
175
|
+
- Introducing more sophisticated orchestration (Kubernetes, Nomad, etc.) using the existing images as a foundation.
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Eraser & Selection Mechanics in CoSketch"
|
|
3
|
+
publishedAt: "2025-05-01"
|
|
4
|
+
summary: "A deep dive into how CoSketch handles erasing and selecting shapes in its canvas engine, and how those changes propagate to collaborators."
|
|
5
|
+
description: "A deep dive into how CoSketch handles erasing and selecting shapes in its canvas engine, and how those changes propagate to collaborators."
|
|
6
|
+
date: "2025-05-01"
|
|
7
|
+
tags:
|
|
8
|
+
- canvas
|
|
9
|
+
- eraser
|
|
10
|
+
- selection
|
|
11
|
+
- collaboration
|
|
12
|
+
- cosketch
|
|
13
|
+
repoUrl: "https://github.com/NarsiBhati-Dev/cosketch"
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Why erasing and selection are hard problems
|
|
17
|
+
|
|
18
|
+
CoSketch is a real-time collaborative whiteboard where multiple people draw and edit shapes on a shared canvas. On a simple drawing app, an eraser might just paint white pixels and selection might be little more than a single highlighted object. CoSketch, however, needs to support:
|
|
19
|
+
|
|
20
|
+
- **Multiple shape types** (rectangles, ellipses, diamonds, arrows, lines, freehand paths, text).
|
|
21
|
+
- **Real-time collaboration**, where changes must be synchronized over WebSockets.
|
|
22
|
+
- **Consistent state** across the frontend stores, backend persistence, and other clients.
|
|
23
|
+
|
|
24
|
+
To achieve this, CoSketch centralizes eraser and selection behavior in the canvas engine modules:
|
|
25
|
+
|
|
26
|
+
- [`eraser.ts`](https://github.com/NarsiBhati-Dev/cosketch/blob/master/apps/cosketch-frontend/src/canvas_engine/eraser.ts)
|
|
27
|
+
- [`SelectionManager.ts`](https://github.com/NarsiBhati-Dev/cosketch/blob/master/apps/cosketch-frontend/src/canvas_engine/SelectionManager.ts)
|
|
28
|
+
|
|
29
|
+
These modules work with shared stores and the WebSocket layer to transform user gestures into deterministic, shareable canvas updates.
|
|
30
|
+
|
|
31
|
+
> In short: the eraser defines a hitbox along the pointer path; any shape that intersects that region is removed or trimmed, and the canvas store is updated so all clients stay in sync.
|
|
32
|
+
|
|
33
|
+
## Eraser behavior in `eraser.ts`
|
|
34
|
+
|
|
35
|
+
The `eraser.ts` module is responsible for turning a user’s erasing gesture into updates on the underlying shapes. Conceptually, the eraser tool:
|
|
36
|
+
|
|
37
|
+
- Tracks the pointer path while the eraser is active.
|
|
38
|
+
- Builds a **hitbox region** around the pointer path (often a circle or rectangle sized by eraser thickness).
|
|
39
|
+
- Performs hit-testing against shapes and freehand paths intersecting the hitbox.
|
|
40
|
+
- Updates the canvas store to remove or modify affected shapes.
|
|
41
|
+
|
|
42
|
+
The eraser works hand-in-hand with the active tool logic in `CanvasEngine.ts`:
|
|
43
|
+
|
|
44
|
+
- When the current tool is `"eraser"`, pointer events are routed to eraser-specific handlers.
|
|
45
|
+
- The eraser module receives information about the pointer’s position and the current canvas state.
|
|
46
|
+
- It computes which shapes are impacted and returns a set of changes for the engine to apply to stores.
|
|
47
|
+
|
|
48
|
+
At a high level, the eraser flow looks like this:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// Pseudocode for eraser integration
|
|
52
|
+
canvasEngine.handlePointerMove(event, () => {
|
|
53
|
+
if (toolStore.getState().currentTool === "eraser") {
|
|
54
|
+
const changes = eraser.computeEraseChanges({
|
|
55
|
+
pointerPath,
|
|
56
|
+
shapes: canvasStore.getState().shapes,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
canvasStore.getState().applyChanges(changes);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The actual implementation may use more efficient data structures and batch updates to minimize re-renders and network chatter, but the conceptual pipeline remains the same.
|
|
65
|
+
|
|
66
|
+
## Hit-testing shapes and freehand paths
|
|
67
|
+
|
|
68
|
+
Hit-testing is the process of determining which shapes the eraser intersects. CoSketch must support:
|
|
69
|
+
|
|
70
|
+
- **Geometric shapes** (rectangles, ellipses, diamonds).
|
|
71
|
+
- **Linear shapes** (arrows, lines).
|
|
72
|
+
- **Freehand paths**, typically represented as polylines.
|
|
73
|
+
|
|
74
|
+
For each shape type, `eraser.ts` or related utilities can use:
|
|
75
|
+
|
|
76
|
+
- **Bounding box checks** as a cheap first filter.
|
|
77
|
+
- **More precise geometry checks** (e.g., distance from a line segment, point-in-ellipse tests, or polyline distance sampling).
|
|
78
|
+
|
|
79
|
+
For freehand paths, the common pattern is:
|
|
80
|
+
|
|
81
|
+
- Sample the path as a sequence of points.
|
|
82
|
+
- For each eraser sample point or segment, check if the distance to any path segment is below an eraser threshold.
|
|
83
|
+
- If the path is partially erased, either:
|
|
84
|
+
- Split it into multiple smaller paths, or
|
|
85
|
+
- Remove it entirely if the majority is hit (depending on UX decisions).
|
|
86
|
+
|
|
87
|
+
The result of hit-testing is a list of shapes (and sometimes sub-paths) that need to be updated or removed from the canvas store.
|
|
88
|
+
|
|
89
|
+
> The eraser samples the pointer path and checks distance to each path segment; segments within the eraser threshold are removed or split, producing updated primitives for the canvas store.
|
|
90
|
+
|
|
91
|
+
## Updating shared canvas state on erase
|
|
92
|
+
|
|
93
|
+
Once `eraser.ts` identifies the impacted shapes, it transforms that information into state updates:
|
|
94
|
+
|
|
95
|
+
- **Shape deletion**: shapes fully covered by the eraser hitbox are removed from `canvas.store.ts`.
|
|
96
|
+
- **Shape modification**: paths or complex shapes may be trimmed or split, resulting in a set of new primitives replacing the old one.
|
|
97
|
+
- **Selection cleanup**: any shapes currently selected that are erased must also be removed from the selection state in `shape_selected.store.ts`.
|
|
98
|
+
|
|
99
|
+
Because the stores are the single source of truth, all downstream systems see the same result:
|
|
100
|
+
|
|
101
|
+
- React components re-render without the erased shapes.
|
|
102
|
+
- The WebSocket synchronization layer serializes the changes and sends them to other clients.
|
|
103
|
+
- The backend can persist the updated canvas representation.
|
|
104
|
+
|
|
105
|
+
From the engine’s perspective, erasing is just another state transition on the same shape model used for drawing and moving.
|
|
106
|
+
|
|
107
|
+
## Selection mechanics via `SelectionManager.ts`
|
|
108
|
+
|
|
109
|
+
Selection is managed by the `SelectionManager` module in `apps/cosketch-frontend/src/canvas_engine/SelectionManager.ts`. This module encapsulates:
|
|
110
|
+
|
|
111
|
+
- How the app decides what is "currently selected".
|
|
112
|
+
- How bounding boxes and drag handles are computed.
|
|
113
|
+
- How multi-select and group operations work.
|
|
114
|
+
|
|
115
|
+
The typical selection lifecycle goes through several conceptual states:
|
|
116
|
+
|
|
117
|
+
1. **Idle**: no active selection, or selection is present but not being modified.
|
|
118
|
+
2. **Hover**: the pointer is over a shape or a selection handle, highlighting potential actions.
|
|
119
|
+
3. **Selected**: one or more shapes are selected; a bounding box and handles may be shown.
|
|
120
|
+
4. **Dragging / Resizing**: the user drags the selection or a handle to move or resize shapes.
|
|
121
|
+
|
|
122
|
+
`SelectionManager` works closely with:
|
|
123
|
+
|
|
124
|
+
- `shape_selected.store.ts` for storing selected IDs and transient selection metadata.
|
|
125
|
+
- `canvas.store.ts` for reading the latest positions and sizes of shapes.
|
|
126
|
+
- `tool.store.ts` to interpret intent (e.g., whether the user is using a selection tool or a direct manipulation tool).
|
|
127
|
+
|
|
128
|
+
> Selection moves through idle, hover, selected, and dragging or resizing; the selection store and canvas store stay in sync so handles and bounding boxes reflect the current state.
|
|
129
|
+
|
|
130
|
+
## Bounding boxes, handles, and multi-select
|
|
131
|
+
|
|
132
|
+
To provide a good UX, CoSketch uses bounding boxes and drag handles around selections:
|
|
133
|
+
|
|
134
|
+
- For a single shape, the bounding box closely wraps that shape.
|
|
135
|
+
- For multiple shapes, the box encompasses all selected shapes.
|
|
136
|
+
|
|
137
|
+
`SelectionManager` computes this envelope by:
|
|
138
|
+
|
|
139
|
+
- Reading the geometry from `canvas.store.ts` for all selected shapes.
|
|
140
|
+
- Computing the min/max of x and y coordinates to form a bounding rectangle.
|
|
141
|
+
- Deriving handle positions (corners, edges, possibly rotation handles).
|
|
142
|
+
|
|
143
|
+
Multi-select can be initiated by:
|
|
144
|
+
|
|
145
|
+
- Clicking while holding a modifier key (e.g., Shift).
|
|
146
|
+
- Dragging a marquee (selection rectangle) that intersects multiple shapes.
|
|
147
|
+
|
|
148
|
+
The resulting selection is stored in `shape_selected.store.ts`, which might include:
|
|
149
|
+
|
|
150
|
+
- A list of selected shape IDs.
|
|
151
|
+
- The current bounding box.
|
|
152
|
+
- Flags for whether the user is currently dragging, resizing, or rotating.
|
|
153
|
+
|
|
154
|
+
The engine then applies transformations (translation, scaling, rotation) to the underlying shapes when the user drags the selection or a handle, updating `canvas.store.ts` accordingly.
|
|
155
|
+
|
|
156
|
+
## Interactions between selection and tools
|
|
157
|
+
|
|
158
|
+
Selection doesn’t exist in isolation; it interacts with other tools:
|
|
159
|
+
|
|
160
|
+
- **Move / selection tool**: directly manipulates selected shapes.
|
|
161
|
+
- **Style tools**: changes color, stroke width, or other properties of the selection via `canvas_style.store.ts`.
|
|
162
|
+
- **Delete / eraser tools**:
|
|
163
|
+
- A delete action may remove all selected shapes at once.
|
|
164
|
+
- The eraser may implicitly change the current selection if it modifies or removes shapes under the cursor.
|
|
165
|
+
|
|
166
|
+
`SelectionManager` and `eraser.ts` must coordinate to keep the selection store consistent when shapes are modified or removed. For example:
|
|
167
|
+
|
|
168
|
+
- If a selected shape is erased, `shape_selected.store.ts` must remove that shape ID from the selection.
|
|
169
|
+
- If a shape is split into multiple shapes, the selection state may choose to select the new shapes or clear the selection depending on UX rules.
|
|
170
|
+
|
|
171
|
+
This coordination ensures that users never see stale handles or bounding boxes attached to shapes that no longer exist.
|
|
172
|
+
|
|
173
|
+
## WebSocket synchronization of erasing and selection
|
|
174
|
+
|
|
175
|
+
CoSketch uses the WebSocket server in `apps/cosketch-websocket/` to keep multiple clients in sync. While exact message formats are defined in the code, conceptually:
|
|
176
|
+
|
|
177
|
+
- The frontend sends **canvas update messages** when:
|
|
178
|
+
- Shapes are added, moved, resized, or deleted.
|
|
179
|
+
- Eraser operations result in shape modifications.
|
|
180
|
+
- Other clients apply these updates to their own stores, reproducing the same erasing and selection state transitions.
|
|
181
|
+
|
|
182
|
+
Selection state itself may or may not be fully synchronized (depending on design), but the **underlying shape updates definitely are**. For example:
|
|
183
|
+
|
|
184
|
+
- If you erase part of a freehand stroke, your client computes the resulting shapes and sends a message describing those changes.
|
|
185
|
+
- Other clients remove or adjust the same shapes in their canvas store, even if their local selection differs.
|
|
186
|
+
|
|
187
|
+
This design keeps the core canvas data model consistent across all participants, while allowing some local freedom (such as independent selection state).
|
|
188
|
+
|
|
189
|
+
> One client applies an erase; the WebSocket server relays the canvas update; other clients apply the same changes to their stores so everyone sees the same canvas.
|
|
190
|
+
|
|
191
|
+
## Lessons learned & design trade-offs
|
|
192
|
+
|
|
193
|
+
A few takeaways from building eraser and selection:
|
|
194
|
+
|
|
195
|
+
- **Geometry vs. UX**: Accurate hit-testing and shape splitting can be complex; CoSketch balances precision with performance and a reasonable mental model for users.
|
|
196
|
+
- **State consistency**: Centralizing shape and selection state in stores simplifies reasoning, but requires careful coordination between eraser, selection, and other tools.
|
|
197
|
+
- **Collaboration granularity**: Sending fine-grained canvas updates over WebSockets enables smooth collaboration but must be batched or compressed to avoid overwhelming the network.
|
|
198
|
+
|
|
199
|
+
## What’s next
|
|
200
|
+
|
|
201
|
+
For the broader canvas engine pipeline (pointer events, stores, and tools), see [How CoSketch's Canvas Engine Works](/blogs/cosketch-canvas-engine).
|
|
202
|
+
|
|
203
|
+
Potential future improvements to eraser and selection mechanics in CoSketch include:
|
|
204
|
+
|
|
205
|
+
- More advanced partial-erasing behavior for freehand strokes with visual previews.
|
|
206
|
+
- Improved visual feedback for multi-select (e.g., per-shape outlines plus a group box).
|
|
207
|
+
- Conflict resolution strategies when multiple collaborators erase or modify the same shapes simultaneously.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Hello World in Rust with Axum"
|
|
3
|
+
publishedAt: "2026-01-03"
|
|
4
|
+
summary: "A minimal Hello World web server using Rust and the Axum framework."
|
|
5
|
+
description: "A minimal Hello World web server using Rust and the Axum framework."
|
|
6
|
+
date: "2026-01-03"
|
|
7
|
+
tags:
|
|
8
|
+
- rust
|
|
9
|
+
- axum
|
|
10
|
+
- web
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
**Axum** is a fast, ergonomic web framework for Rust built on [Tokio](https://tokio.rs/) (async runtime) and [Tower](https://github.com/tower-rs/tower) (middleware). In this post we’ll build a minimal “Hello, World!” server you can run in a few minutes.
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
Create a new project and add the dependencies:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
cargo new hello-axum
|
|
21
|
+
cd hello-axum
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
In `Cargo.toml`:
|
|
25
|
+
|
|
26
|
+
```toml
|
|
27
|
+
[dependencies]
|
|
28
|
+
axum = "0.7"
|
|
29
|
+
tokio = { version = "1", features = ["full"] }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## The server
|
|
33
|
+
|
|
34
|
+
In `src/main.rs`:
|
|
35
|
+
|
|
36
|
+
```rust
|
|
37
|
+
use axum::{routing::get, Router};
|
|
38
|
+
|
|
39
|
+
#[tokio::main]
|
|
40
|
+
async fn main() {
|
|
41
|
+
let app = Router::new().route("/", get(handler));
|
|
42
|
+
|
|
43
|
+
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
|
44
|
+
axum::serve(listener, app).await.unwrap();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async fn handler() -> &'static str {
|
|
48
|
+
"Hello, World!"
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Run it
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
cargo run
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Open [http://localhost:3000](http://localhost:3000) and you should see **Hello, World!**.
|
|
59
|
+
|
|
60
|
+
## What’s going on
|
|
61
|
+
|
|
62
|
+
- **Router** – Defines routes; here we attach a single `GET /` to `handler`.
|
|
63
|
+
- **handler** – An async function that returns a string; Axum sends it as `text/plain` with status 200.
|
|
64
|
+
- **tokio::main** – Runs the async runtime so `main` can `await`.
|
|
65
|
+
|
|
66
|
+
From here you can add more routes, JSON with `axum::Json`, and middleware from the Tower ecosystem.
|