Skip to content

ericchen682/duos

Repository files navigation

Duos 💞

Cozy little games for two people. No accounts, no fuss — one person starts a room, the other joins with a short code or a shareable link.

The first (and currently only) game is Split Coloring: a line-art page is split into two regions, each player colors their own half in secret, and when both are done the two halves combine into one finished drawing that you can download together.

Mobile/iPad-first. Works great with touch and stylus, and also with a mouse.


Tech stack

  • Next.js (App Router) + TypeScript
  • Tailwind CSS v4
  • Supabase — Postgres (lobby/player state), Realtime (live sync), Storage (colored halves)
  • Deploy target: Vercel

The coloring engine is a plain HTML5 Canvas raster engine:

  • Tap-to-fill — scanline flood fill with color tolerance, bounded by the dark line-art pixels (lib/floodFill.ts).
  • Freeform brush (4 sizes) + eraser, a color palette, and undo/redo (canvas snapshots).
  • All painting is clipped to the current player's region mask (lib/mask.ts), so you can only color your own half.

Run it locally

Prerequisites: Node 20+ (developed on Node 24) and npm.

npm install
npm run dev

Then open http://localhost:3000. Without Supabase env vars the home page loads, but creating/joining a room shows a friendly "connect Supabase" message. Follow the setup below to enable multiplayer.

Other scripts:

npm run build   # production build + typecheck
npm run lint    # eslint
node scripts/generate-pages.mjs   # regenerate the placeholder line-art PNGs

Supabase setup (required for multiplayer)

You need your own Supabase project (this repo cannot create one for you).

  1. Create a project at https://supabase.com (free tier is fine).

  2. Create the schema. In the dashboard open SQL Editor → New query, paste the contents of supabase/schema.sql, and run it. This creates the lobbies and players tables, enables Realtime on them, and creates the colored-halves and coloring-pages-uploads Storage buckets, adds the (anon-friendly) RLS policies.

    • The script creates both Storage buckets for you. If your project blocks creating buckets from SQL, create public buckets named colored-halves and coloring-pages-uploads manually under Storage, then re-run the script (the on conflict do nothing makes it safe).
  3. Grab your keys. Go to Project Settings → API and copy the Project URL and the anon public key.

  4. Configure the app. Copy the example env file and fill it in:

    cp .env.local.example .env.local
    NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
    NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-public-key
  5. Restart npm run dev. Create a room on one device and join it from another (or a second browser) to test the full flow.

A note on security (MVP)

There is no login. Players are identified only by a random client_id stored in localStorage, and the RLS policies allow anonymous read/write to lobbies, players, and the halves bucket. This is fine for casual, unlisted, short-lived games where the lobby code is effectively the only secret. Before anything serious you should tighten the policies (scope updates to the caller, add a cleanup job for old lobbies, consider Supabase anonymous auth). See the comments in supabase/schema.sql.


Adding your own coloring pages

In the app (recommended): During lobby setup, the creator can tap Upload yours to upload a PNG or JPG (max 5 MB). The image is stored in the coloring-pages-uploads bucket and referenced on the lobby's page_image field so both players load the same file.

Via manifest (bundled pages): Line-art pages also live in public/coloring-pages/ and are listed in public/coloring-pages/manifest.json.

  1. Drop a PNG or JPG into public/coloring-pages/. Best results come from black line art on a white (or transparent) background — the flood fill treats dark pixels as walls.

  2. Add an entry to manifest.json:

    {
      "pages": [
        {
          "id": "my-page",
          "title": "My Page",
          "src": "/coloring-pages/my-page.png",
          "width": 1000,
          "height": 750
        }
      ]
    }

    width/height should match the image's intrinsic pixel size (used as the canvas resolution).

The three bundled pages (house, flower, fish) are generated by scripts/generate-pages.mjs — a small pure-JS generator you can delete once you have real art.


Deploy to Vercel

  1. Push this repo to GitHub and import it in Vercel.
  2. In the Vercel project, add the two environment variables (Project Settings → Environment Variables), for all environments:
    • NEXT_PUBLIC_SUPABASE_URL
    • NEXT_PUBLIC_SUPABASE_ANON_KEY
  3. Deploy. That's it — the app is a standard Next.js App Router build.

How it works (a quick tour)

Path What it does
app/page.tsx Home — responsive grid of games.
app/games/split-coloring/page.tsx Create a room or join by code.
app/lobby/[code]/page.tsx Lobby state machine: setup → waiting → playing → revealed.
app/dev/canvas/page.tsx Dev harness for testing the canvas + color picker without Supabase.
components/lobby/ Split editor (incl. upload), share panel, reveal view.
components/coloring/ Canvas engine, toolbar, full color picker, play view.
components/ui/ Shared buttons, cards, panels, page headers.
lib/lobby/ Supabase lobby API, realtime hook, page upload.
lib/coloring/ Flood fill, masks, splits, palette, color utils, page resolution.
lib/games/registry.ts Game catalog for the home page (extensible for future games).

Player identity is a random client_id in localStorage, so a refresh reconnects you to the same role in the same room.

About

split coloring app

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors