A real-time collaborative drawing app where two people can draw together on a shared canvas. Built with Expo and Convex.
Live demo: candle-take-home.vercel.app
- Real-time collaboration — Draw together on the same canvas with live updates
- Shareable sessions — Each canvas gets a unique link; share it to invite someone
- Responsive canvas — Drawings scale proportionally across any screen size or orientation
- Drawing tools — Pen, eraser, and eyedropper
- Color picker — Popup spectrum with tap and slide support
- Brush size slider — Adjustable stroke width (1–20px)
- Undo & clear — Undo your last stroke or clear the entire canvas
- Pinch-to-zoom — Two-finger zoom and pan for drawing fine details, double-tap to reset
- Live cursors — See your collaborator's cursor and initials in real time
- Connection indicator — Shows connection status and collaborator count
- Smooth strokes — Pressure-simulated freehand drawing via perfect-freehand
- GPU-accelerated rendering — Skia-powered canvas for smooth performance
| Layer | Technology |
|---|---|
| Framework | Expo (React Native) |
| Routing | Expo Router (file-based) |
| Drawing | @shopify/react-native-skia |
| Stroke smoothing | perfect-freehand |
| Gestures | react-native-gesture-handler |
| Backend & real-time sync | Convex |
| Identity persistence | AsyncStorage |
All stroke coordinates are normalized to a 0–1 range before being stored. This means drawings look correct on any screen size — a stroke drawn on a phone renders proportionally on a tablet or desktop browser.
Strokes are sent to Convex when a gesture ends (not per-point), with optimistic updates so the drawer sees their stroke immediately without waiting for a server round-trip. Remote collaborators receive updates through Convex's real-time subscriptions.
Cursor positions are broadcast via a separate presence system, throttled to one update per 80ms, with a 30-second TTL to automatically clean up stale cursors.
- Node.js 18+
- A Convex account (convex.dev)
# Clone the repo
git clone https://github.com/anikrish05/CandleTakeHome.git
cd CandleTakeHome/couples-canvas
# Install dependencies
npm install
# Set up Convex
npx convex dev
# This will prompt you to create a project and set CONVEX_URL in .env.local
# Start the app
npx expo startPress w to open in a web browser, or scan the QR code with Expo Go for mobile.
| Variable | Description |
|---|---|
EXPO_PUBLIC_CONVEX_URL |
Your Convex deployment URL (set automatically by npx convex dev) |
app/
_layout.tsx # Root layout with Convex provider
index.tsx # Landing page (create/join canvas)
canvas/[sessionId].tsx # Canvas session page
components/
CanvasRoom.tsx # Main orchestrator (state, mutations, layout)
DrawingCanvas.tsx # Skia canvas with gesture handling
Toolbar.tsx # Bottom toolbar (tools, brush size, colors)
ColorSpectrum.tsx # Popup color picker with tap and slide
CursorOverlay.tsx # Remote cursor display (zoom-aware)
ShareButton.tsx # Share link generation
ConnectionIndicator.tsx # Connection status pill
hooks/
useIdentity.ts # Persistent random user identity
usePresence.ts # Cursor broadcasting & subscription
useCanvasSize.ts # Canvas dimension tracking
useCanvasZoom.ts # Pinch-to-zoom state & gesture handling
lib/
strokeUtils.ts # Stroke path generation & eyedropper hit testing
drawingUtils.ts # Coordinate normalization
identity.ts # Identity generation & persistence
constants.ts # Colors and defaults
types.ts # TypeScript interfaces
convex/
schema.ts # Database schema (canvases, strokes, presence)
canvases.ts # Canvas CRUD
strokes.ts # Stroke mutations & queries
presence.ts # Cursor presence system
- Normalized coordinates — Storing points as 0–1 values rather than pixels means drawings are resolution-independent. A stroke drawn on a 400px-wide canvas renders identically on a 1200px-wide one.
- Optimistic updates — Every mutation (add stroke, undo, clear) updates the local Convex cache immediately, so the UI never feels laggy even with network latency.
- Stroke-on-end, not per-point — Points accumulate locally during a gesture and are sent as a single mutation when the gesture ends. This reduces server load significantly compared to per-point mutations.
- Eraser as colored stroke — The eraser draws strokes in the canvas background color rather than performing true deletion. This keeps the rendering pipeline simple and consistent.
- Convex for real-time — Convex's reactive queries eliminate the need to manage WebSocket connections, reconnection logic, or manual cache invalidation.
- Platform-aware zoom — On native, RNGH pinch/pan gestures handle zoom reliably. On web, native
touchstart/touchmoveevents bypass RNGH's weaker multi-touch support. A sharedisPinchingflag cancels any in-progress draw stroke the instant a second finger lands, preventing draw/zoom conflicts.
I prioritized real-time collaboration quality and cross-device consistency. The normalized coordinate system, optimistic updates, and throttled presence broadcasting all serve the goal of making two people drawing together feel seamless and responsive. I also focused on matching Candle's aesthetic — warm colors, soft shadows, rounded corners — to show design sensibility alongside technical depth.