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.

246 lines
13 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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:
```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,
"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).