diff --git a/src/app.js b/src/app.js index db1ffd7..6971e7e 100644 --- a/src/app.js +++ b/src/app.js @@ -3177,10 +3177,10 @@ snackbarTimer = setTimeout(hideSnackbar, 5000); } - function quickLog(type, originEl) { + function quickLog(type) { const ev = addEvent(type, "", Date.now()); showSnackbar(`${EVENT_LABELS[type]} logged`, ev); - if (type === "pee" || type === "poo") pottyConfetti(type, originEl); + if (type === "pee" || type === "poo") pottyConfetti(type); } // A little burst of ๐Ÿ’ง/๐Ÿ’ฉ from the tapped button when a pee/poo is logged. @@ -3203,28 +3203,28 @@ } return confettiLayerEl; } - function pottyConfetti(type, originEl) { + function pottyConfetti(type) { if (!confettiEnabled()) return; if (window.matchMedia && matchMedia("(prefers-reduced-motion: reduce)").matches) return; const emoji = type === "poo" ? "๐Ÿ’ฉ" : "๐Ÿ’ง"; const layer = confettiLayer(); - const r = originEl && originEl.getBoundingClientRect - ? originEl.getBoundingClientRect() - : { left: innerWidth / 2, top: innerHeight / 2, width: 0, height: 0 }; - const ox = r.left + r.width / 2, oy = r.top + r.height / 2; - for (let i = 0; i < 16; i++) { + const W = window.innerWidth, H = window.innerHeight; + for (let i = 0; i < 30; i++) { const piece = document.createElement("span"); piece.className = "confetti-piece"; piece.textContent = emoji; - const ang = Math.random() * Math.PI * 2; - const dist = 60 + Math.random() * 130; - piece.style.left = `${ox}px`; - piece.style.top = `${oy}px`; - piece.style.setProperty("--dx", `${Math.round(Math.cos(ang) * dist)}px`); - piece.style.setProperty("--dy", `${Math.round(Math.sin(ang) * dist - 50)}px`); // bias upward + // Launch from a point across the bottom, shoot up to a random peak, drift + // sideways, then arc back down (endY is below the peak) while fading. + const peakY = -Math.round(H * (0.45 + Math.random() * 0.45)); + piece.style.left = `${Math.round(W * (0.1 + Math.random() * 0.8))}px`; + piece.style.top = `${H + 20}px`; + piece.style.setProperty("--peakY", `${peakY}px`); + piece.style.setProperty("--dx", `${Math.round((Math.random() * 2 - 1) * W * 0.22)}px`); + piece.style.setProperty("--endY", `${peakY + Math.round(H * 0.35)}px`); piece.style.setProperty("--rot", `${Math.round(Math.random() * 720 - 360)}deg`); - piece.style.fontSize = `${Math.round(14 + Math.random() * 16)}px`; - piece.style.animationDuration = `${Math.round(900 + Math.random() * 600)}ms`; + piece.style.fontSize = `${Math.round(16 + Math.random() * 18)}px`; + piece.style.animationDuration = `${Math.round(1100 + Math.random() * 700)}ms`; + piece.style.animationDelay = `${Math.round(Math.random() * 280)}ms`; piece.addEventListener("animationend", () => piece.remove()); layer.appendChild(piece); } @@ -3289,7 +3289,7 @@ // Weigh-ins need a typed value and meals ask for grams, so those two // keep the full dialog. if (type === "weight" || type === "eat") { openNoteDialog(type); return; } - quickLog(type, btn); + quickLog(type); }); }); diff --git a/src/changelog.json b/src/changelog.json index 2ca2318..5aa63b9 100644 --- a/src/changelog.json +++ b/src/changelog.json @@ -1,5 +1,5 @@ [ - { "date": "2026-08-01", "text": "Logging a pee or poo now sets off a little burst of ๐Ÿ’ง/๐Ÿ’ฉ confetti from the button โ€” a tiny celebration you can switch off in Settings (and it honours a reduced-motion preference)" }, + { "date": "2026-08-01", "text": "Logging a pee or poo now sets off ๐Ÿ’ง/๐Ÿ’ฉ fireworks that shoot up from the bottom of the screen โ€” a little celebration you can switch off in Settings (and it honours a reduced-motion preference)" }, { "date": "2026-08-01", "text": "Tidied the header on long names and ages โ€” the name now truncates instead of shoving the buttons, and the age reads as a compact \"16 wk ยท 3 mo 3 wk\"; weight-log rows are a single line again (\"Aug 1 ยท 16 wk\")" }, { "date": "2026-07-26", "text": "Added a fan-chart view of the pedigree (toggle it in the header): your dog at the centre with each generation fanning outward as a ring, so many generations fit at once without the tree sprawling sideways โ€” tap a wedge for that dog, and repeated ancestors keep their colour" }, { "date": "2026-07-26", "text": "Added a Collapse all / Expand all toggle to the pedigree, to fold the whole tree down to your dog or open every branch at once" }, diff --git a/src/style.css b/src/style.css index b41bfcc..bcb139a 100644 --- a/src/style.css +++ b/src/style.css @@ -1361,18 +1361,19 @@ section.collapsed > :not(h2) { display: none; } position: absolute; line-height: 1; will-change: transform, opacity; - animation-name: potty-burst; - animation-timing-function: cubic-bezier(0.2, 0.6, 0.35, 1); - animation-fill-mode: forwards; + animation-name: potty-firework; + animation-timing-function: ease-out; + /* `both` so the 0% state (invisible, at the bottom) also applies during the + per-piece launch delay โ€” no flash before it takes off. */ + animation-fill-mode: both; } -@keyframes potty-burst { - 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.4) rotate(0deg); } - 12% { opacity: 1; } - 100% { - opacity: 0; - /* fly out to (dx, dy) then keep falling (gravity) */ - transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy) + 150px)) scale(1) rotate(var(--rot)); - } +/* Launch up from the bottom, rise to a peak (spreading sideways), then arc back + down while fading โ€” a firework fountain. Distances come from JS custom props. */ +@keyframes potty-firework { + 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.5) rotate(0deg); } + 8% { opacity: 1; } + 60% { opacity: 1; transform: translate(calc(-50% + var(--dx) * 0.7), calc(-50% + var(--peakY))) scale(1) rotate(calc(var(--rot) * 0.7)); } + 100% { opacity: 0; transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--endY))) scale(0.9) rotate(var(--rot)); } } @media (prefers-reduced-motion: reduce) { .confetti-piece { display: none; }