Files
rplace/README.md
T
tiennm99 cfbac2a586 feat(canvas): 4096^2 canvas, 256-color palette (u8 byte-aligned), custom picker (#5)
Canvas:
- CANVAS_W/H = 4096, total 16,777,216 pixels
- BITS_PER_PIXEL = 8 (byte-aligned) — raw Redis bytes are palette indices
- Canvas-decoder becomes an identity wrap/copy
- Storage BITFIELD uses u8; offset = y*W + x
- Redis key versioned to rplace:canvas:v2 so old 32-color/2048^2 data is
  orphaned (operators can DEL the old key to reclaim memory)

Palette:
- 256 entries, generated deterministically:
  - 0..15  = 16-step grayscale ramp (pure black -> pure white)
  - 16..255 = 240 HSL wheel (4 lightness rings x 60 hues @ 82% saturation)
- nearestPaletteIndex(r,g,b) helper for custom-color snapping

UI:
- ColorPicker: 16-swatch favorites strip (grays + 8 accents) + current-color
  swatch + expand toggle for the full 16x16 grid + "Custom..." button that
  opens the native <input type="color"> and snaps to nearest palette entry
- Default selected color bumped to index 0 (black)

Tests + docs:
- canvas-decoder tests rewritten for identity u8 decode
- canvas-storage tests updated for u8 offsets
- image-to-palette tests anchored to PALETTE_BLACK=0 / PALETTE_WHITE=15 and
  COLORS_RGBA[i] probes (no more hardcoded old 32-color indices)
- integration test uses u8 BITFIELD and canvas-aware bounds
- README, system-architecture, deployment-guide updated (storage math,
  migration note for orphaned old key)
2026-04-18 13:47:01 +07:00

6.1 KiB
Raw Blame History

rplace

A collaborative pixel art canvas inspired by Reddit's r/place. Place pixels, create art together in real-time.

Features

  • 4096×4096 canvas with a 256-color palette (16-step grayscale + 240-hue HSL wheel)
  • Real-time updates via WebSocket (Cloudflare Durable Objects)
  • Batch pixel placement up to 2048 pixels per request
  • Rate limit — 1 request per second per user (batch size independent)
  • Zoom/pan with mouse wheel + drag (desktop) and pinch-zoom + drag (mobile)
  • Long-press to place on touch devices
  • Image importer — upload, dither, and auto-paint images onto the canvas

Tech Stack

Layer Technology
Frontend Svelte 5 (runes) + HTML5 Canvas
Backend Hono on Cloudflare Workers
Real-time WebSocket via Cloudflare Durable Objects
Storage Upstash Redis (BITFIELD for canvas, SET NX EX for rate limiting)
Build Vite

Architecture

Browser (Svelte SPA + WebSocket)
  |  GET  /api/canvas  → full canvas binary (16MB raw, ~5MB gzip)
  |  POST /api/place   → batch pixel placement
  |  WS   /api/ws      → Durable Object broadcast room
  v
Cloudflare Worker (Hono)
  ├── Canvas API (read/write pixels via Redis BITFIELD)
  ├── Rate Limiter (SET NX EX — atomic per-user cooldown)
  └── Durable Object (WebSocket broadcast to all clients)
        ↕
Upstash Redis
  ├── STRING "canvas:v2" (1 byte per pixel, 4096×4096 = 16 MB)
  └── STRING "cooldown:{userId}" (1s TTL, blocks repeat requests)

Getting Started

Prerequisites

Setup

# Clone and install
git clone <repo-url>
cd rplace
npm install

# Configure environment
cp .env.example .env
# Edit .env with your Upstash Redis credentials

# For wrangler (Cloudflare Workers CLI)
npx wrangler secret put UPSTASH_REDIS_REST_URL
npx wrangler secret put UPSTASH_REDIS_REST_TOKEN

Development

# Run worker locally (serves both API and frontend)
npm run dev

# Or run frontend and worker separately
npm run dev:client   # Vite dev server on :5173 (proxies /api to :8787)
npm run dev          # Wrangler dev server on :8787

Deploy

npm run deploy   # Builds frontend + deploys worker to Cloudflare

Project Structure

src/
├── worker.js                          # Hono API entry point
├── durable-objects/
│   └── canvas-room.js                 # WebSocket broadcast room
├── lib/
│   ├── constants.js                   # Config, palette, limits (shared)
│   ├── redis-client.js                # Upstash Redis factory
│   ├── canvas-storage.js              # BITFIELD read/write
│   ├── canvas-decoder.js              # Raw bytes → RGBA (client-side; u8 = identity indices)
│   ├── rate-limiter.js                # SET NX EX cooldown
│   ├── image-uploader.js              # Browser-side batched uploader
│   └── get-user-id.js                 # IP-based identity
├── client/
│   ├── main.js                        # Svelte mount
│   ├── App.svelte                     # Root + WebSocket
│   ├── app.css                        # Global styles
│   └── components/
│       ├── CanvasRenderer.svelte      # Canvas + zoom/pan + touch
│       ├── ColorPicker.svelte         # Favorites strip + 256-color grid + custom picker
│       ├── CanvasControls.svelte      # Zoom buttons + coordinates
│       ├── DrawToolbar.svelte         # Paint / submit / undo / redo
│       └── ImageImporter.svelte       # Image-to-canvas uploader
└── index.html                         # Vite entry

API

GET /api/canvas

Returns the full canvas as raw binary (1 byte per pixel, 16 MB — Cloudflare gzips it on the edge).

POST /api/place

Place pixels on the canvas.

{
  "pixels": [
    { "x": 100, "y": 200, "color": 27 }
  ]
}

Response: { "ok": true }

Errors:

  • 400 — invalid pixel data or batch > 2048
  • 413 — request body too large
  • 429 — rate limited (includes retryAfter seconds)

WS /api/ws

WebSocket for real-time pixel updates. Messages are JSON:

{ "type": "pixels", "pixels": [{ "x": 100, "y": 200, "color": 27 }] }

Configuration

Key constants in src/lib/constants.js:

Constant Default Description
CANVAS_WIDTH 4096 Canvas width in pixels
CANVAS_HEIGHT 4096 Canvas height in pixels
MAX_COLORS 256 Number of palette entries
BITS_PER_PIXEL 8 Byte-aligned (storage = W × H bytes)
MAX_BATCH_SIZE 2048 Max pixels per placement request
REQUEST_COOLDOWN_SEC 1 Minimum seconds between requests per user

Credits & References

License

MIT