|
|
# 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"
|
|
|
│ │
|
|
|
├──────────────────────────────┤
|
|
|
│ ‹ 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 days `d ≥ startedAt` and
|
|
|
habits `habit` checked on day `d`. 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
|
|
|
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 + 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 on `visibilitychange` so 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:
|
|
|
|
|
|
```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,
|
|
|
"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 `<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. 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.
|
|
|
3. 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.
|
|
|
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, 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).
|