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)
This commit is contained in:
tiennm99 authored and GitHub committed 2026-04-18 13:47:01 +07:00
1 parent 0bf51a410e
commit cfbac2a586
12 files changed
+371 -214

No files matched your search

+11 -10
View File
@@ -4,7 +4,7 @@ A collaborative pixel art canvas inspired by [Reddit's r/place](https://www.redd
## Features
- **2048x2048 canvas** with 32-color palette (from [rplace.live](https://rplace.live/))
- **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)
@@ -26,7 +26,7 @@ A collaborative pixel art canvas inspired by [Reddit's r/place](https://www.redd
```
Browser (Svelte SPA + WebSocket)
| GET /api/canvas → full canvas binary (2.5MB raw)
| GET /api/canvas → full canvas binary (16MB raw, ~5MB gzip)
| POST /api/place → batch pixel placement
| WS /api/ws → Durable Object broadcast room
v
@@ -36,7 +36,7 @@ Cloudflare Worker (Hono)
└── Durable Object (WebSocket broadcast to all clients)
↕
Upstash Redis
├── BITFIELD "canvas" (5-bit per pixel, 2048x2048 = 2.62MB)
├── STRING "canvas:v2" (1 byte per pixel, 4096×4096 = 16 MB)
└── STRING "cooldown:{userId}" (1s TTL, blocks repeat requests)
```
@@ -93,7 +93,7 @@ src/
│ ├── constants.js # Config, palette, limits (shared)
│ ├── redis-client.js # Upstash Redis factory
│ ├── canvas-storage.js # BITFIELD read/write
│ ├── canvas-decoder.js # 5-bit → RGBA (client-side)
│ ├── 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
@@ -103,7 +103,7 @@ src/
│ ├── app.css # Global styles
│ └── components/
│ ├── CanvasRenderer.svelte # Canvas + zoom/pan + touch
│ ├── ColorPicker.svelte # 32-color palette grid
│ ├── 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
@@ -114,7 +114,7 @@ src/
### `GET /api/canvas`
Returns the full canvas as raw binary (5-bit packed, ~2.5MB).
Returns the full canvas as raw binary (1 byte per pixel, 16 MB — Cloudflare gzips it on the edge).
### `POST /api/place`
@@ -149,9 +149,10 @@ Key constants in `src/lib/constants.js`:
| Constant | Default | Description |
|---|---|---|
| `CANVAS_WIDTH` | 2048 | Canvas width in pixels |
| `CANVAS_HEIGHT` | 2048 | Canvas height in pixels |
| `MAX_COLORS` | 32 | Number of colors in palette |
| `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 |
@@ -167,7 +168,7 @@ Key constants in `src/lib/constants.js`:
- [redis-place by mehdiamrane](https://github.com/mehdiamrane/redis-place)
- [redis-challenge by alfredosalzillo](https://github.com/alfredosalzillo/redis-challenge)
- [place by dynastic](https://github.com/dynastic/place)
- [rplace.live](https://rplace.live/) — color palette reference
- [rplace.live](https://rplace.live/) — original 32-color palette reference (since superseded by our 256-color HSL wheel)
## License