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
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 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"
│ │
├──────────────────────────────┤
│ Today · Sunday, July 13 │
│ ┌──────────────────────┐ │
│ │ 💧 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.
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.
- 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.
- 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.
- Same-day uncheck only shrinks that habit's contribution for today; previous days' revealed pixels are never lost (reveal is derived from the log, and each habit's streak counts backwards from the day it's evaluated on).
- 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).