Commit Graph
31 Commits
Author SHA1 Message Date
tiennm99 4a5b6ef903 feat(importer): phase 4 — more dithering algorithms
Replace the single Floyd-Steinberg toggle with a method dropdown offering
none, floyd, atkinson, jarvis, burkes, sierra, sierra-lite (error diffusion)
and bayer-2 / bayer-4 / bayer-8 (ordered). Kernels live in
src/lib/dither-kernels.js as plain data; rgbaToPalette dispatches via a
shared runErrorDiffusion runner and a runOrderedDither for Bayer.

The legacy `dither: true` option keeps working (maps to `method: 'floyd'`),
so existing CLI invocations and tests stay green. CLI gains
`--dither-method <name>` which takes precedence over `--dither`.

13 unit tests cover exact-palette hits, transparent pixels, all-method
smoke, kernel weight sums (Atkinson=0.75, others=1.0), and verify Bayer
actually scatters on mid-grey where plain nearest would produce one color.
2026-04-17 11:53:52 +07:00
tiennm99 fc0121fd48 feat(importer): phase 3 — flip / rotate transforms
Pure-function transformRgba in src/lib/image-transform.js supports flip H,
flip V, and rotation in 90° steps (fixed application order: rotate → flipH
→ flipV). Wired into the importer pipeline before resize with buttons for
each op; ±90° rotations auto-swap the resize dims so output aspect tracks
the rotation. CLI gains --flip-h, --flip-v, --rotate flags using the same
shared module.

8 new unit tests cover identity, each flip, 90/180/270 rotation, the
flipH+flipV ≡ 180° identity, and invalid rotation rejection.
2026-04-17 11:49:54 +07:00
tiennm99 75c1023554 feat(importer): phase 2 — overlay preview on main canvas
CanvasRenderer.setOverlay draws the palette-converted import preview on top
of committed + pending layers at (x, y) with a configurable alpha. Image
importer gains an "Overlay" toggle and opacity slider; the overlay
reactively follows resize/dither/origin changes and clears on panel close,
toggle-off, or image swap. Lets the user align the upload before spending
credits.
2026-04-17 11:45:16 +07:00
tiennm99 f94f1dc5f5 feat(importer): phase 1 — resize controls with multiple methods
Add W/H inputs, aspect-ratio lock, resampling method dropdown (nearest /
bilinear / box), "Fit to canvas" (respects current origin) and "1:1" reset
buttons to the import panel. Pipeline now runs resize before palette
quantization so dither/skip-matching operate on the final dimensions.

Shared src/lib/image-resize.js is reused by scripts/image-to-colors.js,
which gains --width, --height, --method flags with aspect-preserve when
only one dimension is provided.
2026-04-17 11:43:11 +07:00
tiennm99 9254fec8c9 docs(plans): add image importer enhancements plan
Six phases spanning resize, on-canvas overlay preview, transforms, more
dithering algorithms, color correction, and skip-white/paint-transparent
toggles. Based on WPlace-AutoBOT's image-processor.js feature set, scoped
to what fits our 32-color canvas.
2026-04-17 11:43:04 +07:00
tiennm99 4a149c38eb feat(client): add image importer panel with dither toggle
New floating "Import Image" panel decodes PNG/JPG/WebP in the browser, shows
a palette-matched preview, and uploads pixels at a user-chosen origin with
pause/resume/cancel and live progress. Options:

- Skip-matching: compare against CanvasRenderer.getCommittedColor so pixels
  already correct don't waste credits.
- Dither (Floyd-Steinberg): toggle between nearest-color and error-diffusion
  quantization; reactively re-quantizes the cached source on change.

Uploader (src/lib/image-uploader.js) tracks credits locally with regen and
backs off on 429 responses so it can run alongside manual drawing.
2026-04-17 11:25:16 +07:00
tiennm99 1565776f02 feat(scripts): add image-to-colors + upload-colors CLI scripts
Convert PNG/JPG/WebP to rplace palette JSON (nearest-color or Floyd-Steinberg
dither) and upload at a target (x, y) with client-side credit throttling and
429 backoff. Shared palette conversion lives in src/lib/image-to-palette.js so
the browser importer can reuse it.
2026-04-17 11:25:06 +07:00
tiennm99 a0c32b18ee fix: serve /api/canvas raw, drop manual gzip + add error envelope
Manual CompressionStream + Content-Encoding: gzip caused the browser
to receive un-decoded gzipped bytes through the wrangler-dev + vite
proxy path, surfacing as "Canvas buffer truncated: got 3598 bytes"
in the new decoder bounds check.

Cloudflare's edge auto-compresses compressible content already, so the
manual gzip layer was redundant in prod and broken in dev.

Also wraps getFullCanvas in try/catch and returns a JSON error envelope
on failure instead of letting Hono's default handler return a 500 with
text body the client decoder would mis-parse.
2026-04-17 10:31:05 +07:00
tiennm99 ad8d2a6f71 docs: add ultrareview reports and fix-sweep plan
Three review reports (backend, frontend, security/scale) and a
follow-up re-review after pull. Plan documents which findings were
addressed in this sweep and which were intentionally deferred.
2026-04-17 10:14:45 +07:00
tiennm99 a823f8527d fix: address ultrareview findings across backend and frontend
Backend:
- rate-limiter: retryAfter now in seconds; ms-precision lu preserves
  fractional regen residue across calls (C1, C2)
- redis-client: throw on Upstash 200-with-error envelope; redisRaw
  returns body.result (NH1)
- constants: MAX_BATCH_SIZE = MAX_CREDITS = 256 (was 512 vs 256)
- worker: content-length cap, gzip + s-maxage=10 on /api/canvas,
  broadcast via executionCtx.waitUntil with r.ok check (NC2, H4, H5)
- canvas-storage: warn on truncated Upstash read instead of silent
  zero-pad (NH2)
- get-user-id: SHA-256 (16 hex chars) replaces 32-bit string hash;
  missing cf-connecting-ip routes to anon:dev with warn (H1, H2);
  function is now async
- canvas-room: log unclean WS closes and errors; defensive close on
  unexpected client message (NH4, N5)

Frontend:
- pixel buffer capped at MAX_BATCH_SIZE with toast (NC2)
- Submit error UX: toast for 429/413/400/5xx/network; honor
  retryAfter (NC1)
- committedColors allocated upfront so WS updates during initial
  fetch no longer null-deref (NC3)
- handleWheel always renders even when zoom is clamped (C1, C2)
- canvas-decoder throws on truncated input instead of reading past
  end with || 0 (C3)
- WS reconnect refetches canvas to recover missed pixels (C4)
- pixel-buffer Map cache for O(1) getColorAt/pixelCount (NH1)
- cancel in-progress stroke on mode switch (NH3)
- canvas load error overlay with Retry button (NH5)
- DPR-aware canvas sizing (H1)
- onMount cleanup is now sync (no leaked resize listener) (H2)

Tests:
- update for async getUserId, decoder bounds check, redis error
  format; add Upstash error-envelope coverage
2026-04-17 10:14:45 +07:00
tiennm99 8e1f8c4049 test: add Redis integration tests with Testcontainers
Docker-based tests with real Redis verifying:
- BITFIELD write → GETRANGE read round-trip for all 32 colors
- Pixel placement at canvas boundaries and various positions
- Pixel overwrite correctness
- 5-bit boundary isolation (adjacent pixels don't corrupt)
- Batch atomicity (100 pixels in single BITFIELD)
- Rate limiter Lua script: credit grant, deduction, regen, cap, rejection

Test commands: npm test (unit), npm run test:integration (Docker),
npm run test:all (both). 82 total tests.
2026-04-16 22:52:36 +07:00
tiennm99 fcddb1f9c5 test: add unit tests for Durable Object and clean up test setup
- Add CanvasRoom broadcast/close/error tests with mock WebSockets
- Remove @cloudflare/vitest-pool-workers (incompatible with Vitest 4)
- Clean up vitest config (single config, no integration workspace)
- 71 tests across 7 test files, all passing
2026-04-16 22:45:16 +07:00
tiennm99 33cfd3d7b3 test: add comprehensive unit tests with Vitest
65 tests covering canvas decoder, pixel buffer, user ID hashing,
canvas storage (with binary encoding regression test), Redis client
API shape, and worker endpoint validation.
2026-04-16 22:37:12 +07:00
tiennm99 b35769cc73 fix: binary-safe canvas read via Upstash base64 encoding
Use path-based REST API with Upstash-Encoding: base64 header for
GETRANGE to prevent binary data corruption through JSON text encoding.
Verified all 32 colors round-trip correctly.
2026-04-16 22:19:26 +07:00
tiennm99 e3eb34c6de fix: BITFIELD via raw REST API and improve error handling
- Use Upstash REST API directly for BITFIELD (SDK builder broken in v1.37)
- Add redisRaw() helper for raw command execution
- Wrap setPixels in try-catch, return JSON errors instead of 500 text
- Client handles non-JSON server responses gracefully
- Toolbar: larger 44px touch targets, separators, better contrast
2026-04-16 22:00:58 +07:00
tiennm99 e0cf802ec2 feat: batch drawing with paint/draw modes, undo/redo, and submit
Replace per-pixel immediate placement with local buffer system:
- Paint mode (click) and Draw mode (drag) for pixel placement
- Undo/Redo strokes (Ctrl+Z/Y) before submitting
- Submit button sends batch to server for storage + broadcast
- Right-click drag to pan in draw mode
- Increase MAX_BATCH_SIZE to 512 for larger batches
2026-04-16 21:22:19 +07:00
tiennm99 eef6879ff2 fix: use Hibernation API for reliable WebSocket broadcast
Replace manual session tracking with Cloudflare's Hibernation API
so WebSocket connections survive Durable Object eviction.
2026-04-16 20:51:47 +07:00
tiennm99 50f4365034 feat: enable observability logs and traces 2026-04-16 20:47:14 +07:00
tiennm99 c357a3f265 fix: use new_sqlite_classes for free-plan Durable Objects and gitignore .wrangler 2026-04-16 20:46:52 +07:00
tiennm99 f97ca4d34d refactor: add Redis key prefix for namespace isolation
Introduce REDIS_KEY_PREFIX constant to namespace all Redis keys under
'rplace:', preventing collisions in shared Redis instances.
2026-04-16 20:39:12 +07:00
tiennm99 fc49de154a add project documentation and detailed README
- README: features, architecture diagram, setup guide, API reference,
  configuration table, project structure
- docs/system-architecture.md: data flow, storage design, rate limiting
- docs/code-standards.md: conventions, project layout, API format
- docs/deployment-guide.md: step-by-step CF Workers + Upstash deploy
2026-04-16 17:05:29 +07:00
tiennm99 078ccaa70e fix: final review polish before documentation
- Handle base64 encoding from Upstash GETRANGE (atob fallback)
- Add optimistic credit deduction on pixel placement
- Handle non-ok API responses in placePixel
- Enable WebSocket proxy in Vite dev config
- Remove dead CANVAS_WIDTH/HEIGHT vars from wrangler.json
- Suppress favicon 404 with empty data URI
2026-04-16 17:03:38 +07:00
tiennm99 75441d650a feat: add touch support, cooldown bar, and cap batch size
- Cap MAX_BATCH_SIZE to 32 (was 256) to limit burst grief damage
- Add touch handlers: pinch-zoom, drag-pan, long-press to place pixel
- Add credit bar visualization with fill animation and color states
  (yellow while regenerating, green when full)
2026-04-16 16:52:00 +07:00
tiennm99 7793163e0b fix: critical Upstash API and UX issues from second review
- Fix setPixels() to use Upstash bitfield builder (.set().exec())
  instead of flat array (would throw at runtime)
- Fix getFullCanvas() to use GETRANGE for reliable binary retrieval
  instead of GET which may mangle BITFIELD data
- Fix getUserId hash overflow: use >>> 0 instead of Math.abs()
- Add client-side credit regeneration timer (1/sec)
- Add exponential backoff to WebSocket reconnection (1s → 30s cap)
2026-04-16 16:37:07 +07:00
tiennm99 b3b4916263 fix: address code review findings
- Fix WebSocket reconnection logic in App.svelte (was a no-op)
- Remove spoofable x-forwarded-for fallback, use cf-connecting-ip only
- Wrap Durable Object broadcast in try/catch to prevent place failures
- Cache OffscreenCanvas to avoid re-allocation every render frame
2026-04-16 16:27:35 +07:00
tiennm99 2ccf1c9779 refactor: migrate from Next.js/Vercel to Svelte/Hono/Cloudflare Workers
Replace Next.js + SSE with Svelte frontend + Hono API + Durable Objects
WebSocket on Cloudflare Workers. Single worker serves static assets and
API routes. Native WebSocket replaces SSE polling for real-time updates.
17kb gzipped client bundle vs 90kb+ React runtime.
2026-04-16 16:23:27 +07:00
tiennm99 3beaa9d43f feat: implement core pixel canvas with real-time updates
Next.js App Router + Upstash Redis BITFIELD for 2048x2048 canvas.
- 32-color palette (5-bit encoding) from rplace.live
- Batch pixel placement API (up to 256 pixels per request)
- Stackable credit rate limiting via Redis Lua script
- SSE real-time broadcasting via Redis sorted set
- HTML5 Canvas frontend with zoom/pan and color picker
2026-04-16 15:56:20 +07:00
tiennm99 1c6b2edcff add implementation plan for rplace pixel canvas
7-phase plan: setup, canvas backend (Redis BITFIELD), rate limiting
(stackable credits), SSE real-time updates, frontend canvas, auth, deploy.
2026-04-16 15:24:55 +07:00
tiennm99 62188d26d2 update references and add credits to README
Remove unfetchable URLs (redditinc, reddit.com, tejash.dev, rplace.live).
Add credits section to README attributing all referenced projects.
2026-04-16 15:24:50 +07:00
tiennm99 9bdde946a0 add r/place reference links and resources
Curated collection of official post-mortems, technical walkthroughs,
open-source implementations, and live instances for r/place research.
2026-04-16 14:43:23 +07:00
tiennm99 8ca80531cc Initial commit 2026-04-16 14:36:24 +07:00