From 8c9bf8967668a2c2a5a8f19f78c43486b8be3d14 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 00:24:38 +0000 Subject: [PATCH] Add pregnancy timeline motion: growing slider fill, size pop, week-button wave Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01LSDgjk1Agjy9Z2UH2n8bc3 --- index.html | 3 ++- timeline.js | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 timeline.js diff --git a/index.html b/index.html index 35caffa..19c7a6e 100644 --- a/index.html +++ b/index.html @@ -266,7 +266,7 @@ button{font-family:inherit;} text-transform:uppercase;letter-spacing:0.05em;margin-bottom:8px;font-weight:600;} input[type="range"].week-slider{ width:100%;-webkit-appearance:none;appearance:none;height:8px;border-radius:999px; - background:linear-gradient(to right, var(--pregnancy-soft) 0%, var(--pregnancy-soft) 100%); + background:linear-gradient(to right, var(--pregnancy) 0, var(--pregnancy) calc(11px + (100% - 22px) * var(--f,0)), var(--pregnancy-soft) calc(11px + (100% - 22px) * var(--f,0)), var(--pregnancy-soft) 100%); outline:none;cursor:pointer; } input[type="range"].week-slider::-webkit-slider-thumb{ @@ -1074,3 +1074,4 @@ footer.note{ + diff --git a/timeline.js b/timeline.js new file mode 100644 index 0000000..59cb6d0 --- /dev/null +++ b/timeline.js @@ -0,0 +1,68 @@ +/* CycleWise AU - pregnancy timeline motion (Step 3). + - The week slider's track fills with colour up to the current week. + It grows the first time you scroll to it, then follows every week change. + - The baby-size picture pops a little when the week changes. + - The 40 week buttons wave in the first time they appear. + Cosmetic only: without GSAP the slider simply shows its normal look. */ +(function(){ + "use strict"; + var slider = document.getElementById("week-slider"); + var badge = document.getElementById("wk-badge"); + if (!slider || !badge) return; + + var hasGsap = !!window.gsap; + var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + var animate = hasGsap && !reduce; + + var fill = { f: 0 }; + var target = 0; + var introDone = false; + + function paint(){ slider.style.setProperty("--f", fill.f.toFixed(4)); } + function currentWeek(){ + var n = parseInt((badge.textContent || "").replace(/\D/g, ""), 10); + return isNaN(n) ? (parseInt(slider.value, 10) || 1) : Math.max(1, Math.min(40, n)); + } + function goTo(f){ + target = f; + if (!introDone) return; // wait until the slider is on screen + if (!animate) { fill.f = f; paint(); return; } + gsap.to(fill, { f: f, duration: 0.6, ease: "power2.out", overwrite: true, onUpdate: paint }); + } + + // Follow every week change, whether from the slider, a week button or the calendar. + var lastWeek = currentWeek(); + target = (lastWeek - 1) / 39; + new MutationObserver(function(){ + var w = currentWeek(); + if (w === lastWeek) return; + lastWeek = w; + goTo((w - 1) / 39); + var blob = document.querySelector("#wk-blob svg"); + if (animate && blob) { + gsap.fromTo(blob, { scale: 0.8, transformOrigin: "50% 50%" }, + { scale: 1, duration: 0.5, ease: "back.out(2)", clearProps: "transform" }); + } + }).observe(badge, { childList: true, characterData: true, subtree: true }); + + // First time the slider is visible: grow the fill from empty up to this week. + new IntersectionObserver(function(entries, obs){ + if (!entries[0].isIntersecting) return; + obs.disconnect(); + introDone = true; + if (!animate) { fill.f = target; paint(); return; } + gsap.to(fill, { f: target, duration: 1.2, ease: "power2.out", onUpdate: paint }); + }).observe(slider); + + // First time the week buttons are visible: wave them in. + var strip = document.getElementById("week-filmstrip"); + if (strip && animate) { + new IntersectionObserver(function(entries, obs){ + if (!entries[0].isIntersecting) return; + obs.disconnect(); + gsap.from(strip.querySelectorAll(".film-item"), { + opacity: 0, scale: 0.8, duration: 0.4, stagger: 0.02, ease: "power2.out", clearProps: "all" + }); + }).observe(strip); + } +})();