Files
painter/README.md
Gabriel Kaszewski f652785acb
All checks were successful
CI / ci (push) Successful in 7m16s
v1.0.0 — Hexagonal architecture rewrite
Restructure the monolithic 252-line main.rs into a 10-crate workspace
with clean hexagonal architecture, swappable adapters, and a production-
ready deployment pipeline.

Backend architecture:
- domain: Canvas, Color/Position/PixelUpdate value objects, port traits
  (CanvasStore, CanvasPersistence, EventBroadcaster), BroadcastEvent
- application: use cases (place_pixel, get_state, save/restore snapshot,
  connect/disconnect), AppState with Arc snapshot cache
- config: AppConfig structs + ConfigSource trait
- api-types: shared DTOs, event name constants
- adapters: config-env, canvas-file, http-axum (rust-embed), socketio,
  websocket — all behind port traits, swappable via feature flags
- server: composition root with graceful shutdown (SIGTERM/SIGINT)

Frontend:
- Transport abstraction: Socket.IO and native WebSocket via VITE_TRANSPORT
- Canvas zoom/pan with mouse wheel, pinch-to-zoom, and +/- buttons
- ImageData rendering (~50x faster than fillRect loop)
- Touch support, responsive CSS scaling, mobile-friendly layout
- OG/Twitter Card meta tags for rich link previews

Production:
- Docker: musl static build on scratch — 2.73MB image
- CI workflows for Gitea and GitHub Actions (fmt, clippy, test, Docker push)
- deploy.sh for private registry
- 39 unit tests across domain and application
- Zero unwraps, zero unsafe, graceful error handling with tracing
- Periodic canvas snapshots with rotation, restored on startup
- All config via environment variables with typed defaults
2026-08-18 01:41:40 +02:00

122 lines
3.4 KiB
Markdown

# Painter
A collaborative pixel canvas inspired by r/place. Users connect in real-time and place colored pixels on a shared 500x500 canvas.
## Quick Start
```bash
# Install frontend dependencies and build
cd painter-js && bun install && bun run build && cd ..
# Run the server (serves embedded frontend)
cargo run --release
```
Open `http://localhost:3000` in your browser.
## Configuration
All configuration is via environment variables (or `.env` file):
| Variable | Default | Description |
|---|---|---|
| `ADDRESS` | `0.0.0.0` | Bind address |
| `PORT` | `3000` | Bind port |
| `ENABLE_CORS` | `true` | Enable CORS headers |
| `CANVAS_WIDTH` | `500` | Canvas width in pixels |
| `CANVAS_HEIGHT` | `500` | Canvas height in pixels |
| `COOLDOWN_SECS` | `10` | Seconds between pixel placements per user |
| `RATE_LIMIT_BURST` | `10` | HTTP rate limit burst size |
| `RATE_LIMIT_PER_SECOND` | `10` | HTTP rate limit per second |
| `BROADCAST_CAPACITY` | `1024` | Broadcast channel buffer size |
| `SNAPSHOT_ENABLED` | `true` | Enable periodic canvas snapshots |
| `SNAPSHOT_INTERVAL_SECS` | `300` | Seconds between snapshots |
| `SNAPSHOT_MAX` | `5` | Maximum snapshot files to keep |
| `SNAPSHOT_DIR` | `snapshots/` | Snapshot storage directory |
## Transport Adapters
The server supports two real-time transport protocols, selected at compile time:
```bash
# Socket.IO (default)
cargo run --release
# Native WebSocket
cargo run --release --no-default-features --features websocket
```
The frontend auto-detects via `VITE_TRANSPORT` environment variable (`socketio` or `websocket`).
## Docker
```bash
# Build
docker build -t painter .
# Run
docker run -p 3000:3000 painter
# With custom config
docker run -p 3000:3000 \
-e CANVAS_WIDTH=1000 \
-e CANVAS_HEIGHT=1000 \
-e COOLDOWN_SECS=30 \
-v ./snapshots:/app/snapshots \
painter
```
The Docker image is a single statically-linked binary on `scratch`**~3MB** total, no OS, no runtime dependencies. The frontend is embedded at compile time.
The server shuts down gracefully on SIGTERM/SIGINT (Ctrl+C) — in-flight connections are drained and a final canvas snapshot is saved before exit.
## Development
```bash
# Backend (watches for changes)
RUST_LOG=debug cargo run
# Frontend (Vite dev server with HMR)
cd painter-js
VITE_IS_DEBUG=true bun run dev
# Run all checks (fmt + clippy + tests)
make check
# Format
make fmt
cd painter-js && bun run fmt
```
## Architecture
Hexagonal architecture with clean dependency boundaries. See [architecture.mmd](architecture.mmd) for the full diagram.
```
crates/
config/ Config structs, ConfigSource trait
domain/ Canvas, value objects, port traits, events
application/ Use cases, AppState, InProcessBroadcaster
api-types/ Shared DTOs, event constants
adapters/
config-env/ Environment variable config loader
canvas-file/ File-based snapshot persistence
http-axum/ HTTP routes, rate limiting, embedded static files
socketio/ Socket.IO transport adapter
websocket/ Native WebSocket transport adapter
server/ Composition root
```
All infrastructure is behind port traits:
| Port | Adapter | Swappable to |
|---|---|---|
| `CanvasStore` | In-memory | Redis, mmap'd file |
| `CanvasPersistence` | File system | S3, SQLite |
| `EventBroadcaster` | tokio broadcast | NATS, Redis pub/sub |
| `ConfigSource` | Env vars | TOML, JSON, remote API |
## License
[MIT](LICENSE)