Make the pee/poo confetti a bottom-up fireworks fountain

Rework the effect: instead of a small burst from the tapped button, 30 emoji
launch from across the bottom of the screen, rise to random peaks while
drifting sideways, then arc back down and fade — staggered launches and varied
speed/size/rotation for a fuller, firework feel. Still pointer-events:none,
self-removing, and gated by the Settings toggle and reduced-motion.
This commit is contained in:
Alexander Heldt
2026-08-01 18:24:33 +00:00
parent 52c50c97b1
commit e2f99590f1
3 changed files with 30 additions and 29 deletions
+17 -17
View File
@@ -3177,10 +3177,10 @@
snackbarTimer = setTimeout(hideSnackbar, 5000); snackbarTimer = setTimeout(hideSnackbar, 5000);
} }
function quickLog(type, originEl) { function quickLog(type) {
const ev = addEvent(type, "", Date.now()); const ev = addEvent(type, "", Date.now());
showSnackbar(`${EVENT_LABELS[type]} logged`, ev); 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. // A little burst of 💧/💩 from the tapped button when a pee/poo is logged.
@@ -3203,28 +3203,28 @@
} }
return confettiLayerEl; return confettiLayerEl;
} }
function pottyConfetti(type, originEl) { function pottyConfetti(type) {
if (!confettiEnabled()) return; if (!confettiEnabled()) return;
if (window.matchMedia && matchMedia("(prefers-reduced-motion: reduce)").matches) return; if (window.matchMedia && matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const emoji = type === "poo" ? "💩" : "💧"; const emoji = type === "poo" ? "💩" : "💧";
const layer = confettiLayer(); const layer = confettiLayer();
const r = originEl && originEl.getBoundingClientRect const W = window.innerWidth, H = window.innerHeight;
? originEl.getBoundingClientRect() for (let i = 0; i < 30; i++) {
: { 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 piece = document.createElement("span"); const piece = document.createElement("span");
piece.className = "confetti-piece"; piece.className = "confetti-piece";
piece.textContent = emoji; piece.textContent = emoji;
const ang = Math.random() * Math.PI * 2; // Launch from a point across the bottom, shoot up to a random peak, drift
const dist = 60 + Math.random() * 130; // sideways, then arc back down (endY is below the peak) while fading.
piece.style.left = `${ox}px`; const peakY = -Math.round(H * (0.45 + Math.random() * 0.45));
piece.style.top = `${oy}px`; piece.style.left = `${Math.round(W * (0.1 + Math.random() * 0.8))}px`;
piece.style.setProperty("--dx", `${Math.round(Math.cos(ang) * dist)}px`); piece.style.top = `${H + 20}px`;
piece.style.setProperty("--dy", `${Math.round(Math.sin(ang) * dist - 50)}px`); // bias upward 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.setProperty("--rot", `${Math.round(Math.random() * 720 - 360)}deg`);
piece.style.fontSize = `${Math.round(14 + Math.random() * 16)}px`; piece.style.fontSize = `${Math.round(16 + Math.random() * 18)}px`;
piece.style.animationDuration = `${Math.round(900 + Math.random() * 600)}ms`; piece.style.animationDuration = `${Math.round(1100 + Math.random() * 700)}ms`;
piece.style.animationDelay = `${Math.round(Math.random() * 280)}ms`;
piece.addEventListener("animationend", () => piece.remove()); piece.addEventListener("animationend", () => piece.remove());
layer.appendChild(piece); layer.appendChild(piece);
} }
@@ -3289,7 +3289,7 @@
// Weigh-ins need a typed value and meals ask for grams, so those two // Weigh-ins need a typed value and meals ask for grams, so those two
// keep the full dialog. // keep the full dialog.
if (type === "weight" || type === "eat") { openNoteDialog(type); return; } if (type === "weight" || type === "eat") { openNoteDialog(type); return; }
quickLog(type, btn); quickLog(type);
}); });
}); });
+1 -1
View File
@@ -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-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 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" }, { "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" },
+12 -11
View File
@@ -1361,18 +1361,19 @@ section.collapsed > :not(h2) { display: none; }
position: absolute; position: absolute;
line-height: 1; line-height: 1;
will-change: transform, opacity; will-change: transform, opacity;
animation-name: potty-burst; animation-name: potty-firework;
animation-timing-function: cubic-bezier(0.2, 0.6, 0.35, 1); animation-timing-function: ease-out;
animation-fill-mode: forwards; /* `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 { /* Launch up from the bottom, rise to a peak (spreading sideways), then arc back
0% { opacity: 0; transform: translate(-50%, -50%) scale(0.4) rotate(0deg); } down while fading a firework fountain. Distances come from JS custom props. */
12% { opacity: 1; } @keyframes potty-firework {
100% { 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.5) rotate(0deg); }
opacity: 0; 8% { opacity: 1; }
/* fly out to (dx, dy) then keep falling (gravity) */ 60% { opacity: 1; transform: translate(calc(-50% + var(--dx) * 0.7), calc(-50% + var(--peakY))) scale(1) rotate(calc(var(--rot) * 0.7)); }
transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy) + 150px)) scale(1) rotate(var(--rot)); 100% { opacity: 0; transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--endY))) scale(0.9) rotate(var(--rot)); }
}
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.confetti-piece { display: none; } .confetti-piece { display: none; }