diff --git a/bit-by-bit/docs/ARCHITECTURE.md b/bit-by-bit/docs/ARCHITECTURE.md index a19ed51..44dbfde 100644 --- a/bit-by-bit/docs/ARCHITECTURE.md +++ b/bit-by-bit/docs/ARCHITECTURE.md @@ -5,7 +5,7 @@ Everything lives in one file, `index.html`, organized into sections: | Component | Responsibility | |---|---| | **State** | Single JSON document under localStorage key `bitbybit.v1`; load/save with corruption guards; versioned for future migration. | -| **Mechanics** | Habit check/uncheck logging, daily rollover, streak math, seeded (mulberry32) reveal-order permutation, `pixelsPerCheck` calibration, and log-derived reveal: each day earns `triangular(checks)` base units (the Nth check is worth N), multiplied by that day's streak length. | +| **Mechanics** | Habit check/uncheck logging, daily rollover, streak math, seeded (mulberry32) reveal-order permutation, and log-derived reveal: each day reveals `triangular(checks)` pixels (the Nth check reveals N), multiplied by that day's streak length. | | **Artwork** | Preset pixel-art definitions (palette + row strings); upload pipeline (offscreen canvas downsample → pixel grid); gallery of finished pieces. | | **Rendering** | Canvas renderer for the artwork (revealed pixels, checkerboard for hidden, pop-in animation for deltas); DOM renderer for checklist, stats, settings. | | **UI wiring** | Onboarding, settings dialog (habits / artwork / data / about), theme cycling (auto→light→dark via `data-theme` + CSS custom properties), export/import/wipe, confetti celebration, and a page footer disclosing authorship. | diff --git a/bit-by-bit/docs/DESIGN.md b/bit-by-bit/docs/DESIGN.md index 24042be..4c2a76c 100644 --- a/bit-by-bit/docs/DESIGN.md +++ b/bit-by-bit/docs/DESIGN.md @@ -74,27 +74,21 @@ stay revealed. - 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 +- **Escalating daily reward, 1 unit = 1 pixel:** the _Nth_ habit checked on a + given day reveals `N` pixels (1 + 2 + 3 + … ), so a day of `k` checks reveals + `triangular(k) = k·(k+1)/2` pixels before the streak multiplier. 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 pixels 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 + including) that day. On a 3-day streak the 1st check of the day reveals `3` + pixels 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 +- **Revealed pixels are derived from the log, not stored.** Revealed pixel total = + `min(opaqueCount, Σ streakLen(d) · triangular(len(log[d])))` over days + `d ≥ startedAt`. 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 today's contribution — past days can never be un-revealed, and reloads/undo stay perfectly consistent with no separate counter to keep in sync. @@ -112,8 +106,8 @@ stay revealed. 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. + 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. @@ -147,7 +141,6 @@ Single localStorage key: `bitbybit.v1`. Entire state is one JSON document: "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 diff --git a/bit-by-bit/index.html b/bit-by-bit/index.html index f09c1a2..8e5bc7c 100644 --- a/bit-by-bit/index.html +++ b/bit-by-bit/index.html @@ -432,13 +432,12 @@
Paint by Habits is a cozy pixel-art habit tracker. Check off your daily habits and watch a hidden picture reveal itself, one tiny square at a time. Missing a day never erases progress — pixels stay revealed forever.
-How you earn pixels: every habit you check off reveals more of the picture — so - the more you do, the more appears. Each check within a day is worth more than the last: your 1st - check paints a little, your 2nd paints more, your 3rd more still. Checking off your whole - list in a day is the sweet spot.
+How you earn pixels: each habit you check off today reveals pixels — your 1st + check reveals 1 pixel, your 2nd reveals 2, your 3rd reveals 3, and so on. Checking off your + whole list in a day is the sweet spot.
Streaks multiply everything. Every consecutive day you show up bumps the multiplier: - on a 3-day streak your 1st check of the day is worth 3× and your 2nd is worth 6×; on a 5-day streak - they're worth 5× and 10×. Come back tomorrow and finish the day's list, and the picture floods in.
+ on a 3-day streak your 1st check of the day reveals 3 pixels and your 2nd reveals 6; on a 5-day streak + they reveal 5 and 10. Come back tomorrow and finish the day's list, and the picture floods in.Everything lives only in this browser's local storage, on this device. Nothing is ever sent anywhere.
@@ -494,7 +493,6 @@ w: img.w | 0, h: img.h | 0, pixels: img.pixels.slice(), seed: (img.seed == null ? 1 : (img.seed >>> 0)), - pixelsPerCheck: Math.max(1, img.pixelsPerCheck | 0 || 1), startedAt: /^\d{4}-\d{2}-\d{2}$/.test(img.startedAt) ? img.startedAt : todayStr() }; } @@ -636,12 +634,12 @@ return order; } - // Escalating daily reward with a streak multiplier: - // • Within a day, the Nth habit checked is worth N "units" (1 + 2 + 3 + … = - // a triangular number), so k checks in a day earn triangular(k) base units. - // • The whole day's units are then multiplied by that day's streak length - // (consecutive active days ending on it): on a 3-day streak the 1st check - // is worth 3 units, the 2nd 6, and so on. + // Escalating daily reward with a streak multiplier, 1 unit = 1 pixel: + // • Within a day, the Nth habit checked reveals N pixels (1st = 1, 2nd = 2, + // ...), so k checks in a day reveal triangular(k) = k·(k+1)/2 pixels. + // • Each of those pixels is then multiplied by that day's streak length + // (consecutive active days ending on it): on a 3-day streak the 1st + // check reveals 3 pixels, the 2nd 6; on a 5-day streak, 5 and 10. // Revealed pixels are re-derived from the log every time rather than stored, // so undo/redo and reloads stay perfectly consistent and past days can never // be un-revealed by unchecking today (a day's streak length counts backwards @@ -673,23 +671,11 @@ function getRevealInfo(image){ var order = getRevealOrder(image); var opaque = order.length; - var revealedCount = Math.min(opaque, Math.floor(revealedUnits(image) * image.pixelsPerCheck)); + var revealedCount = Math.min(opaque, revealedUnits(image)); var set = new Set(order.slice(0, revealedCount)); return { order: order, opaque: opaque, revealedCount: revealedCount, set: set }; } - // Pixels revealed per unit, fixed at image start. Calibrated so a "perfect - // month" — checking all habits every day for 30 days — just fills the image. - // With the streak multiplier a perfect month earns - // Σ_{day=1..30} day × triangular(habitCount) = triangular(30) × triangular(habitCount) - // units, so pixelsPerCheck ≈ opaqueCount / (triangular(30) × triangular(habitCount)). - // Clamped to ≥1 so the very first check always reveals at least one pixel; - // small images just fill in comfortably sooner than a month. - function pixelsPerCheckFor(opaqueCount, habitCount){ - var perfectMonthUnits = triangular(30) * triangular(Math.max(1, habitCount)); - return Math.max(1, Math.round(opaqueCount / perfectMonthUnits)); - } - function randomSeed(){ // Never return 0: normalizeImage treats a missing seed as 1, and a 0 seed // would reshuffle after a save/load round-trip. @@ -712,13 +698,10 @@ } function buildImageFromDef(def){ - var opaque = def.pixels.filter(function(p){ return p != null; }).length; - var habitCount = Math.max(1, state.habits.length); return { name: def.name, source: def.source, w: def.w, h: def.h, pixels: def.pixels.slice(), seed: randomSeed(), - pixelsPerCheck: pixelsPerCheckFor(opaque, habitCount), startedAt: todayStr() }; } @@ -1469,12 +1452,7 @@ // Selecting an upload supersedes any highlighted preset tile. Array.prototype.forEach.call(q('obArtGrid').children, function(c){ c.classList.remove('selected'); }); obSelectedDef = { name: '', source: 'upload', w: result.w, h: result.h, pixels: result.pixels }; - var habitCount = Math.max(1, state.habits.length); - var ppc = pixelsPerCheckFor(opaque, habitCount); - var targetTri = opaque / (ppc * triangular(habitCount)); - var estDays = Math.max(1, Math.ceil((Math.sqrt(1 + 8 * targetTri) - 1) / 2)); - q('obUploadEstimate').textContent = '~' + estDays + ' day' + (estDays === 1 ? '' : 's') + - ' to finish (' + opaque + ' opaque pixels).'; + q('obUploadEstimate').textContent = opaque + ' opaque pixel' + (opaque === 1 ? '' : 's') + ' to reveal.'; q('obStart').disabled = false; }).catch(function(){ obUploadResult = null; @@ -1655,16 +1633,7 @@ return; } lastUploadResult = result; - var habitCount = Math.max(1, state.habits.length); - var ppc = pixelsPerCheckFor(opaque, habitCount); - // With the streak multiplier, D perfect days in a row reveal - // ppc × triangular(habitCount) × triangular(D) pixels - // (each day's units scaled by its streak length 1..D). Solve - // triangular(D) ≥ opaque / (ppc × triangular(habitCount)) for D. - var targetTri = opaque / (ppc * triangular(habitCount)); - var estDays = Math.max(1, Math.ceil((Math.sqrt(1 + 8 * targetTri) - 1) / 2)); - q('uploadEstimate').textContent = '~' + estDays + ' day' + (estDays === 1 ? '' : 's') + ' to finish, ' + - 'checking in daily (' + opaque + ' opaque pixels).'; + q('uploadEstimate').textContent = opaque + ' opaque pixel' + (opaque === 1 ? '' : 's') + ' to reveal.'; q('useUploadBtn').disabled = false; }).catch(function(){ lastUploadResult = null;