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;