Skip to content

Repository files navigation

Pixel Animator

A browser-based pixel animation editor built with React + TypeScript + Vite.

See release notes for the v1.2.1 collaboration fixes.

Features

Drawing Tools

  • Brush and Eraser with two brush sizes (1x, 2x)
  • Fill (bucket) tool
  • Selection mask tool with floating selection/stamp workflow
  • Smooth line interpolation while drawing fast

Shape Assist (Brush)

  • Draw a near-straight stroke and hold still (~0.6s) to snap it to a line
  • Draw a round loop back to your starting point and hold still to snap it to a circle
  • Scribbles and sketch strokes never snap — the hold only offers a shape when the stroke already resembles one
  • A countdown ring in your draw color shows the hold progress; works with touch
  • Shape previews render before commit; drag to adjust, release to paint

Eyedropper Hold Mode

  • Long-press on canvas to activate eyedropper
  • Hold progress indicator with drain animation
  • Works with magnified color picker overlay (MagnifyingGlass)
  • Dropper messaging and timing are tuned for intentional use

Layer System (2 Layers Per Frame)

  • Every frame has:
    • Base layer (pixelData)
    • Top layer (overlayPixelData)
  • Layers can be viewed as:
    • Stacked (top over base)
    • Unstacked (side-by-side)
  • App starts in stacked mode
  • In stacked mode, drawing targets top layer

Layer-Aware Editing Behavior

  • Frame duplication is layer-aware:
    • Unstacked: duplicates selected layer only
    • Stacked: duplicates both layers together
  • Timeline preview is layer-aware:
    • Unstacked: active layer preview
    • Stacked: composited preview
  • During stacked timeline press-select, top-only preview is shown while holding

Playback + Onion Skin

  • Playback:
    • Stacked: composite playback
    • Unstacked: each canvas plays its own layer
  • Onion skin:
    • Stacked: composite onion
    • Unstacked: onion of current layer only

Selection, Stamp, and Smudge Workflow

  • Selection acts as a mask and creates a floating stamp
  • Enter stamps floating content into the active layer
  • Hold Enter + arrow keys for smudge-like nudge+stamp
  • Hold Enter also auto-stamps after rotate/flip transforms
  • Context messages explain inside-mask vs outside-mask drawing

Timeline

  • Horizontal filmstrip with drag-and-drop frame reordering
  • Multi-frame paint selection (long-press + drag)
  • Batch actions:
    • duplicate selected frames
    • delete selected frames
  • Number-key navigation (1..8, 9, 0)
  • Mouse wheel vertical scroll maps to horizontal timeline scroll
  • FPS controls for playback speed
  • Up to 64 frames per project

Color and Palette

  • Up to 255 preset colors; colors used by art are encoded in a wider 65,535-color space
  • Palette selector with bundled Lospec templates (palettes/*.hex) and live previews of your art in each candidate palette
  • Apply a template two ways: convert the art to the nearest colors (perceptual OKLab matching) or keep the art's exact colors
  • Edit any palette entry's hex directly — pixels drawn with it recolor instantly
  • Import .hex files as custom templates (persisted locally) and download the current palette as a Lospec-compatible .hex file
  • Recent colors strip
  • Pixel data is stored palette-indexed (Uint16Array) so merged peer palettes do not corrupt colors

Peer-to-Peer Collaboration

  • Share an encrypted live-room link with one Guest; no application backend owns the project
  • Per-pixel Yjs merging, independent frame navigation, colored canvas cursors, and timeline presence dots
  • Full local IndexedDB cache on each live peer, plus normal JSON saves as the durable portable copy
  • Collaborative undo affects only your own pixel actions on the active frame
  • Palette conversion and project replacement require the other peer's awareness-based approval
  • Either peer leaving ends the live room immediately; both keep the latest merged drawing as local projects
  • One-shot copy links use a separate immutable snapshot room, expire after 30 minutes, and detach after receipt
  • Public signaling is used by default; set VITE_COLLAB_SIGNALING to a comma-separated ws:///wss:// list to override it
  • Join and copy attempts time out with a retryable error; an absent host never causes a guest to become Host
  • Invite links also work when opened in a tab where the app is already running

Save, Load, and Export

  • Save/load the full project as JSON (frames, layers, palette, FPS, project name)
  • Export options with per-layer modes (merged, base, top):
    • Current frame or selected frames as PNG
    • Sprite sheet PNG
    • Animated GIF (via gifenc)
    • Frames as JSON (re-importable)
  • Import multiple JSON frames; they splice in after the active frame

Mobile Support

  • Responsive layout with a dedicated mobile top bar and toolbar/timeline toggles
  • Touch-first timeline gestures: long-press paint-select, direction-gated drag reordering, and edge auto-scroll

Navigation and View Controls

  • Drag empty workspace area to pan
  • Mouse wheel and trackpad/pinch zoom support in workspace
  • Scrollbars are visually hidden while scrolling remains functional

Keyboard Shortcuts

  • Tools: B, E, F/G, S/M
  • Brush size: [ / ]
  • Playback: Space
  • Stamp: Enter
  • Smudge: hold Enter + arrow keys
  • Rotate/Flip selection: R, Shift+R, Shift+H, Shift+V
  • Undo/Redo: Cmd/Ctrl+Z, Shift+Z, Cmd/Ctrl+Y
  • Frame operations: Shift+N, Shift+Delete

UI/UX

  • Context-sensitive top status label (tool hints, mask hints, dropper hints)
  • Optional shortcuts panel (hidden by default on startup)
  • Pixel-focused cursor/preview behavior for precision editing

Tech Stack

  • React 19
  • TypeScript
  • Vite
  • zustand (editor state store)
  • dnd-kit (timeline drag/sort)
  • gifenc (GIF export)
  • mousetrap (keyboard shortcuts)
  • lucide-react (icons)
  • Yjs, y-webrtc, and y-indexeddb (collaboration and peer persistence)
  • fractional-indexing and nanoid (convergent frame order and collision-safe ids)

Deployment

Vercel with public signaling (default)

The frontend can be deployed to Vercel without hosting your own backend. y-webrtc is MIT-licensed and provides public signaling services that introduce browsers to each other. Drawing updates then travel over WebRTC. This setup uses external infrastructure, but does not require you to operate a signaling server or deploy the optional Docker container.

  1. Push the updated app code to the Git branch connected to your Vercel project. Redeploying an older commit will not include local fixes.

  2. In the project's Environment Variables, use these defaults for Production and Preview:

    Variable Default setup
    VITE_COLLAB_SIGNALING Omit it to use wss://y-webrtc-eu.fly.dev automatically. Delete empty or localhost overrides.
    VITE_COLLAB_ICE_SERVERS Omit it to keep the library's default STUN servers. Do not copy the test value [] into deployment settings.
  3. Build and deploy the updated commit. The Vite frontend uses npm run build and the dist output directory. Environment values are included at build time, so changes require a new build. Leaving a variable unset means removing it, not saving an empty value.

  4. Open the deployed site, select Share → Invite a collaborator, and send the generated link to the Guest. Keep the Host's tab open while the Guest joins. If clipboard access fails, select and copy the visible link manually.

The public endpoint is a dependency whose availability must be checked when a connection fails. Selecting it explicitly does not repair an outage or a network restriction. A custom service is an option, not a prerequisite for deployment.

Verify collaboration and diagnose connection failures

Use separate browsers or devices. Confirm that the Guest joins as Guest, both browsers show a connected collaborator, and a stroke drawn in either browser appears in the other. Same-browser tabs can communicate through BroadcastChannel without working signaling or WebRTC, so they are not sufficient to verify a public deployment.

There are two separate connection stages:

  • Signaling: both browsers must reach a common signaling endpoint. Inspect WebSocket failures if the app cannot connect to the collaboration service.
  • WebRTC: after exchanging connection details, the browsers must establish a peer connection. A working signaling service does not guarantee this stage; network restrictions can require a TURN relay.

Join and copy attempts time out after 45 seconds of waiting for a peer. Live joins also allow up to 15 seconds to load their local cache first. Errors allow retry or cancellation without installing an unreceived project. An absent Host never causes a Guest to become Host. A copy offer requires its sender to stay online until receipt; a live invite is not a hosted saved project.

Optional custom signaling

To use another compatible y-webrtc signaling service, set VITE_COLLAB_SIGNALING to its wss:// URL in the Vercel environment settings and rebuild. Multiple URLs can be comma-separated. Both peers must share at least one reachable endpoint. HTTPS pages require wss://; ws://localhost is only for local development.

For optional self-hosting, the installed y-webrtc signaling server runs with npm ci --omit=dev followed by npm run collab:signaling. It listens on PORT (default 4444). Expose it through TLS with WebSocket upgrades enabled and keep one instance: its in-memory topic subscriptions are not shared across replicas. The HTTP / endpoint returns okay for health checks.

Dockerfile.signaling packages that optional persistent Node service:

docker build -f Dockerfile.signaling -t pixel-signaling .
docker run --rm -p 4444:4444 pixel-signaling

Configure TLS on the container host's ingress. This container is not deployed by the Vercel frontend build. A serverless signaling implementation would need a separate adapter; one is not included in this release.

Optional TURN relay

Some mobile, corporate, or restrictive NAT networks need a TURN relay. VITE_COLLAB_ICE_SERVERS accepts a JSON array of browser RTCIceServer entries and replaces the default STUN list. All VITE_* values are visible in the browser: use only credentials intended for browser clients, never account API keys or a TURN shared authentication secret. Static build-time credentials cannot refresh themselves; services requiring short-lived TURN credentials need a server-side credential endpoint, which this app does not currently provide.

Development

Public signaling works with local development too; no environment file is required. To use a local signaling server instead, copy .env.example to .env.local, uncomment the localhost signaling override, run npm run collab:signaling, and start Vite in another terminal. Restart Vite after changing .env.local.

Install

npm install

Run Dev Server

npm run dev

Build

npm run build

Type Check

npx tsc --noEmit

Tests

npm test
npm run test:e2e
npm run lint

To run the collaboration tests against the built frontend:

COLLAB_E2E_PREVIEW=1 npm run test:e2e

The WebRTC integration cases require local UDP connectivity between browser processes. If signaling connects but these cases time out, inspect the browser's ICE diagnostics and local firewall/network permissions. Test configuration uses host-only ICE (VITE_COLLAB_ICE_SERVERS=[]) so it does not depend on public STUN servers; production should use the default STUN list or configured STUN/TURN.

About

Try this if you like Pixel Art

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages