# Paint by Habits — Design Specification **Author:** Claude Fable 5 (design) · **Implementer:** Claude Sonnet · **Reviewer:** Claude Opus ## 1. Product Concept Paint by Habits is a daily habit tracker where consistency paints a picture — literally. The user defines daily habits and picks a pixel-art image. Every habit they check off reveals a few pixels of the hidden artwork. Over days and weeks the image emerges, and finishing it is the reward. Then they pick the next one. **Tone:** warm, playful, encouraging. Think "cozy pixel garden," not "productivity dashboard." Copy should be friendly and lightly whimsical ("3 pixels bloomed!"), never guilt-trippy. Missing a day never destroys progress — pixels, once revealed, stay revealed. **Hard constraints:** - One self-contained `index.html`. No external requests of any kind: no CDNs, no fonts, no analytics, no fetch. Must work from `file://` and any static host. - All state in `localStorage`. No server. - Vanilla JS + CSS. No frameworks, no build step. - Dark/light theme, auto-detected with manual override. - Full-fidelity export/import so users can move devices. ## 2. Layout (single screen, mobile-first) ``` ┌──────────────────────────────┐ │ ✿ Paint by Habits 🌓 ⚙️ │ header: title, theme toggle, settings ├──────────────────────────────┤ │ │ │ [ ARTWORK CANVAS ] │ the star of the show, centered, │ progress: ▓▓▓░░ 42% │ progress bar + "142 / 340 pixels" │ │ ├──────────────────────────────┤ │ Today · Sunday, July 13 │ │ 🔥 5-day streak │ │ ┌──────────────────────┐ │ │ │ 💧 Drink water [✓] │ │ today's habit checklist — │ │ 🏃 Go for a run [ ] │ │ big, thumb-friendly rows │ │ 📖 Read 10 pages [ ] │ │ │ └──────────────────────┘ │ ├──────────────────────────────┤ │ streak · total checks · │ small stat chips │ images finished │ └──────────────────────────────┘ ``` - Max content width ~28rem, centered; scales up gracefully on desktop. - Settings (⚙️) opens a panel/modal with sections: **Habits**, **Artwork**, **Backup** (export/import), **About**. Use `` or an equivalent overlay. - First visit (no saved state): a friendly 2-step onboarding — (1) add habits, (2) pick an artwork — then straight into the main screen. ## 3. Core Mechanics ### 3.1 Habits - User-defined list, 1–12 habits. Each habit: name (≤40 chars) and an optional emoji (offer a small emoji picker of ~24 curated options; default assigned cyclically if skipped). - Habits reset daily (local timezone). Checking a habit today logs it for today. - Unchecking is allowed **same-day only** (undo mistaken taps). Past days are immutable. - Habits can be added/renamed/deleted in settings at any time. Deleting a habit keeps its historical log entries (they still count toward revealed pixels). ### 3.2 Pixel reveal - The active image is a grid of pixels; each cell is either a color or transparent. Only **opaque** pixels count. - On image start, generate a random permutation of opaque pixel indices using a **seeded PRNG (mulberry32)** with a stored seed. This is the reveal order — stable across sessions and re-renders. - `pixelsPerCheck` is computed **once, at image start**: `max(1, round(opaqueCount / (habitCount * 30)))` — so a typical image finishes in about a month of consistency. Locked for the life of the image even if habits change (prevents gaming/confusion). - `checks` counter on the image: +1 per habit check, −1 on same-day uncheck (clamped ≥ 0 and never below checks from previous days — derive today's floor from the log). Revealed pixel count = `min(opaqueCount, checks * pixelsPerCheck)`. - Unrevealed cells render as a subtle dim checkerboard ("static"). Revealed cells show their color and **pop in with a tiny scale/settle animation** when newly revealed (animate only the delta, not on every render). - When revealed == opaqueCount: **celebration** (see 5), image is archived to the gallery, and the user is prompted to pick the next artwork. ### 3.3 Artwork sources 1. **Presets:** 8 built-in pixel arts, each 16×16 to 24×24, defined compactly in JS as `{name, palette, rows}` where rows are strings of palette keys (`.` = transparent). Suggested set (implementer designs the actual art, and should make it genuinely charming): mushroom, cat face, heart, rainbow, rocket, sunflower, friendly ghost, coffee mug. 2. **Upload:** file input (image/*) → draw to offscreen canvas at a chosen grid size (16 / 24 / 32 / 48) → sample each cell's center region; alpha < 128 → transparent. Show a live preview + "~N days to finish" estimate before confirming. Reject nothing — any image works; it just gets pixelated. - Switching artwork mid-image warns that current progress will be archived as unfinished (kept in gallery, marked incomplete) — or user cancels. ### 3.4 Dates, streaks, stats - "Today" = local date `YYYY-MM-DD` (from local time, not UTC). Recompute on an interval (~30s) and on `visibilitychange` so a tab left open overnight rolls over correctly (checklist resets, floor for uncheck updates). - **Streak:** consecutive days (ending today or yesterday) where **all habits then existing** were checked… simplify: a day counts if **every habit checked that day ≥ 1** is too fuzzy — use: day counts if the user checked **all currently-listed habits** is wrong for history. **Rule:** a day counts toward the streak if **at least one habit was checked** that day; show a separate "perfect day" sparkle ✨ on days where all habits were done. Streak of any-activity is kinder and simpler; perfect days are the bonus flair. - Stat chips: 🔥 current streak, ✅ total checks all-time, 🖼️ images finished. ## 4. Data Model Single localStorage key: `bitbybit.v1`. Entire state is one JSON document: ```jsonc { "version": 1, "theme": "auto", // "auto" | "light" | "dark" "habits": [ { "id": "h_x7f2", "name": "Drink water", "emoji": "💧" } ], "image": { // active artwork, null if none chosen "name": "Cheeky Mushroom", "source": "preset", // "preset" | "upload" "w": 16, "h": 16, "pixels": [null, "#e84d4d", ...], // row-major, length w*h, null = transparent "seed": 123456789, "pixelsPerCheck": 2, "checks": 37, "startedAt": "2026-07-01" }, "log": { // immutable history of check-offs "2026-07-13": ["h_x7f2", "h_a3b9"] }, "gallery": [ // finished/archived artworks (cap 30, FIFO) { "name": "Heart", "w": 16, "h": 16, "pixels": [...], "finishedAt": "2026-06-20", "complete": true } ] } ``` - Save on every mutation (debounced is fine). Wrap `JSON.parse` of the loaded value in try/catch; on corruption, keep the corrupt string under `bitbybit.v1.corrupt` and start fresh rather than crashing. - The reveal **order** is derived from `seed` at load time (don't store the full permutation — keeps exports small; mulberry32 + Fisher–Yates is deterministic). - Storage safety: 48×48 upload = 2304 pixels ≈ 20KB JSON worst case; gallery capped at 30 entries. Well within quota. Still wrap `setItem` in try/catch and surface a friendly error if it fails. ## 5. Whimsy & Delight (required, not optional) - **Pixel pop:** newly revealed pixels scale/bounce in with slight random stagger. - **Check-off feedback:** the habit row does a happy little squish; a toast like "+2 pixels ✨" floats up near the canvas. - **Completion:** self-contained canvas confetti burst (no library), the finished image does a gentle celebratory wiggle, and a congratulations card appears ("You painted *Cheeky Mushroom* — 24 days of showing up!") with a button to choose the next artwork. - **Copy:** playful microcopy throughout. Empty states are friendly ("No habits yet — plant your first one 🌱"). - **prefers-reduced-motion:** all animation (pop, confetti, wiggle) degrades to simple fades or nothing. - **Live favicon:** the browser-tab icon is a 64×64 miniature of the current artwork in its current state — revealed pixels solid, unrevealed ones a two-tone checker, transparent ones the canvas background — so the tab itself fills in as habits are checked off. Drawn into an offscreen canvas and set as a data-URL PNG on `` (and `apple-touch-icon`); no external file, no network request. Redrawn on reveal and on theme change, coalesced to one repaint per frame, and only reassigned when the image actually changes. ## 6. Theming - CSS custom properties on `:root`; `data-theme="light"|"dark"` attribute set by JS. `"auto"` follows `prefers-color-scheme` (and listens for changes). - Toggle button cycles auto → light → dark (icon reflects state). - Palette direction: soft, warm, slightly candy-colored. Dark mode is a cozy near-black with muted accents, not pure #000. Both themes must keep the artwork canvas neutral so the art's own colors read true. - System font stack; headers get a chunky "pixel-ish" treatment via letter-spacing/weight (no embedded fonts — keep the file lean). ## 7. Export / Import - **Export:** button in Backup settings → downloads `bit-by-bit-backup-YYYY-MM-DD.json` (pretty-printed full state) via Blob + object URL. Also a "Copy to clipboard" fallback. - **Import:** file picker (accept `.json,application/json`) → parse → validate (`version === 1`, `habits` is array, sanity-check shapes) → show a summary ("3 habits, 142 checks, 1 artwork in progress — replace current data?") → on confirm, overwrite state and re-render. Invalid files get a kind error message, never a crash. ## 8. Accessibility & Quality Bar - Fully keyboard operable; visible focus states; habit checkboxes are real `` or `role="checkbox"` with labels. - Canvas has an `aria-label` describing progress; progress also exists as text. - Color contrast ≥ 4.5:1 for text in both themes. - Touch targets ≥ 44px. No horizontal page scroll at 320px width. - No console errors. No globals leaking beyond one app namespace. - Code organized in clear sections (state, rendering, mechanics, UI wiring) with brief section comments — one file, but readable. ## 9. Edge Cases (must handle) 1. First run / empty state → onboarding. 2. Same-day uncheck cannot reduce `checks` below the floor established by previous days' log entries. 3. Habit deleted after being checked today → today's log keeps the id; counts still work. 4. Day rollover with tab open → checklist resets without reload. 5. Import of garbage / wrong-version / truncated JSON → friendly rejection. 6. localStorage full or unavailable (private mode) → app still renders; banner warns that progress won't persist. 7. Image with very few opaque pixels (e.g., 20) finishes early — fine; celebrate. 8. `pixelsPerCheck` overshoot on final check just clamps to 100%.