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.
- 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.
Prerequisites: Node 20+ (developed on Node 24) and npm.
npm install
npm run devThen 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 PNGsYou need your own Supabase project (this repo cannot create one for you).
-
Create a project at https://supabase.com (free tier is fine).
-
Create the schema. In the dashboard open SQL Editor → New query, paste the contents of
supabase/schema.sql, and run it. This creates thelobbiesandplayerstables, enables Realtime on them, and creates thecolored-halvesandcoloring-pages-uploadsStorage 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-halvesandcoloring-pages-uploadsmanually under Storage, then re-run the script (theon conflict do nothingmakes it safe).
- The script creates both Storage buckets for you. If your project blocks
creating buckets from SQL, create public buckets named
-
Grab your keys. Go to Project Settings → API and copy the Project URL and the anon public key.
-
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
-
Restart
npm run dev. Create a room on one device and join it from another (or a second browser) to test the full flow.
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.
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.
-
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. -
Add an entry to
manifest.json:{ "pages": [ { "id": "my-page", "title": "My Page", "src": "/coloring-pages/my-page.png", "width": 1000, "height": 750 } ] }width/heightshould 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.
- Push this repo to GitHub and import it in Vercel.
- In the Vercel project, add the two environment variables (Project Settings →
Environment Variables), for all environments:
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEY
- Deploy. That's it — the app is a standard Next.js App Router build.
| 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.