You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

13 KiB

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, Data (export/import plus a destructive "wipe all data" action), About. Use <dialog> or an equivalent overlay.
  • A small page footer discloses authorship ("Written with Claude Code · Sonnet 5, Opus 4.8 & Fable 5"), plain text with no external link.
  • First visit (no saved state): a friendly 2-step onboarding — (1) add habits, (2) pick an artwork (a preset or an uploaded image, same upload pipeline as settings) — then straight into the main screen.

3. Core Mechanics

3.1 Habits

  • User-defined list, 112 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.
  • Escalating daily reward: the Nth habit checked on a given day is worth N base units (1 + 2 + 3 + … ), so a day of k checks earns triangular(k) = k·(k+1)/2 base units. Doing more in one day is rewarded super-linearly; the toast and pop for a later check reveal noticeably more pixels than an earlier one.
  • Streak multiplier: a day's units are multiplied by that day's streak length — the number of consecutive days with ≥1 check ending on (and including) that day. On a 3-day streak the 1st check of the day is worth 3 units and the 2nd 6; on a 5-day streak, 5 and 10. A day's streak length counts backwards from itself, so checking or unchecking a later day never changes an earlier day's multiplier.
  • pixelsPerCheck is computed once, at image start: max(1, round(opaqueCount / (triangular(30) · triangular(habitCount)))) — calibrated so a "perfect month" (all habits every day for 30 days, whose streak-weighted units total triangular(30) · triangular(habitCount)) just fills the image. Locked for the life of the image even if habits change (prevents gaming/confusion). Clamped to ≥1 so the very first check always reveals at least one pixel; small images simply fill in sooner than a month.
  • Revealed pixels are derived from the log, not stored. Revealed unit total = Σ streakLen(d) · triangular(len(log[d])) over days d ≥ startedAt; revealed pixel count = min(opaqueCount, floor(units · pixelsPerCheck)). Because reveal is a pure function of the log, same-day uncheck only shrinks today's contribution — past days can never be un-revealed, and reloads/undo stay perfectly consistent with no separate counter to keep in sync.
  • 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:

{
  "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,
    "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 + FisherYates 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 <link rel="icon"> (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 / Wipe

  • Wipe all data: a clearly-marked destructive button in the Data settings section (styled as danger). Double-confirms, then removes the localStorage keys, resets in-memory state to a clean slate, and returns to first-run onboarding.
  • Export: button in Data 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 <input type="checkbox"> 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 only shrinks today's (streak-multiplied) contribution; previous days' revealed pixels are never lost (reveal is derived from the log, and each day's streak multiplier counts backwards from itself).
  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. A big escalating reward overshooting the last pixels just clamps to 100%.
  9. Legacy saves carrying an old checks field load fine — it is ignored and reveal is recomputed from the log (older images may reveal more than before, since the reward is now triangular per day and multiplied by streak length).