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.
14 KiB
14 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 fromfile://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"
│ │
├──────────────────────────────┤
│ ‹ Sunday, July 13 › │ day navigator: step back to fix a missed day
│ ┌──────────────────────┐ │
│ │ 💧 Drink water 🔥5[✓]│ │ today's habit checklist — each row shows
│ │ 🏃 Go for a run [ ] │ │ its own streak; big, thumb-friendly rows
│ │ 📖 Read 10 pages 🔥2[]│ │
│ └──────────────────────┘ │
├──────────────────────────────┤
│ total checks · images │ small stat chips
│ 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,
and Changelog (a dated list of released changes, one concise sentence each,
newest first). 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, 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.
- Backfilling past days: the Today card is a day navigator (‹ / › arrows plus a "back to today" link) so a user who forgot to check off on the day can step back and fix it. Any day from the active image's start through today is editable (checking or unchecking); you cannot edit days before the current picture existed (they can't paint pixels) or into the future. Because reveal is derived from the log with each habit's streak counted backwards from its own day, backfilling a missed day automatically re-bridges the streak and paints the pixels it earns — no separate reconciliation needed.
- 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.
- Per-habit streaks, 1 unit = 1 pixel: each habit has its own streak — the number of consecutive days (ending on, and including, a given day) that habit was checked. Checking a habit reveals pixels equal to that habit's own streak length on the day checked: 1 pixel the first day, 2 pixels the next consecutive day, 3 the day after, and so on. There is no cross-habit multiplier or bonus for doing several habits in one day — every habit scores purely on its own history. A habit's streak counts backwards from the day it's evaluated on, so checking or unchecking a different day (or a different habit) never changes it.
- Revealed pixels are derived from the log, not stored. Revealed pixel total =
min(opaqueCount, Σ Σ streakLen(habit, d))over daysd ≥ startedAtand habitshabitchecked on dayd. No separate per-image calibration constant — bigger images simply take longer to fill in, smaller ones finish sooner. Because reveal is a pure function of the log, same-day uncheck only shrinks that habit's contribution for today — 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
- 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. - 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 + opaque pixel count 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 onvisibilitychangeso a tab left open overnight rolls over correctly (checklist resets, floor for uncheck updates). - Streak: per-habit, not global — each habit's streak is the count of consecutive days (ending today or yesterday, so it stays "alive" until a day is fully missed) that habit itself was checked. Shown inline on that habit's row (e.g. 🔥3), not as a single app-wide number. Separately, show a "perfect day" sparkle ✨ on days where every currently-listed habit was checked — purely a flair indicator, unrelated to streak math.
- Stat chips: ✅ 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,
"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.parseof the loaded value in try/catch; on corruption, keep the corrupt string underbitbybit.v1.corruptand start fresh rather than crashing. - The reveal order is derived from
seedat 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
setItemin 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">(andapple-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"followsprefers-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,habitsis 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">orrole="checkbox"with labels. - Canvas has an
aria-labeldescribing 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)
- First run / empty state → onboarding.
- Editing any day (today or a backfilled past day within the active image's range) recomputes reveal from the whole log: checking a missed day re-bridges the affected habit's streak and paints the pixels it earns, unchecking removes exactly those pixels. Because each habit's streak counts backwards from the day it's evaluated on, editing one day never corrupts an unrelated day's contribution. Days before the image's start or in the future are not editable.
- Habit deleted after being checked today → today's log keeps the id; its streak/counts keep working off the log id even though it no longer appears in the habit list or UI.
- Day rollover with tab open → checklist resets without reload.
- Import of garbage / wrong-version / truncated JSON → friendly rejection.
- localStorage full or unavailable (private mode) → app still renders; banner warns that progress won't persist.
- Image with very few opaque pixels (e.g., 20) finishes early — fine; celebrate.
- A big escalating reward overshooting the last pixels just clamps to 100%.
- Legacy saves carrying an old
checksfield, or images started under the previous day-level/triangular reward, load fine — it is ignored and reveal is always recomputed from the log under the current per-habit-streak rule (revealed totals may shift from what an old version would have shown).