1/** 2 * Noah motion: scroll reveals, hero entrances and magnetic CTAs for the main pages (home + condition pages, JP and EN). 3 * Loaded only on the pages listed in src/lib/motion.mjs; hover states live in public/css/of-motion.css. 4 * 5 * Same motion language as ordinaryfolk.health and noah-sg (their of-motion.js): one easing curve, Web Animations API, 6 * and the individual `translate` / `scale` properties, which compose with the transforms Webflow, Swiper and GSAP set 7 * instead of overwriting them. 8 * 9 * - Content is never hidden without JS: everything below the fold is hidden by a paused animation created here, 10 * and the hero is pre-hidden by CSS only while html.ofm-js is set and html.ofm-ready isn't (with a failsafe). 11 * - Sections the pages' own GSAP / Webflow interactions drive are skipped (SKIP, and anything Webflow has hidden 12 * with an inline opacity:0 for its own entrance). 13 * - prefers-reduced-motion: nothing moves. 14 * - To animate a new block, add a rule to RULES (selectors that aren't on a page are ignored), or to LATE_RULES if 15 * another script adds the block on DOMContentLoaded. 16 */ 17(function () { 18 'use strict'; 19 var root = document.documentElement; 20 var ready = function () { root.classList.add('ofm-ready'); }; 21 if (!Element.prototype.animate || (window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches)) return ready(); 22 23 // Slow load: when this script runs after the CSS failsafe has already shown the hero (3s, see of-motion.css), the page 24 // has been on screen as it is. Don't hide what people can already see: whatever is in view now (or the failsafe 25 // revealed) gets no entrance; content further down still reveals on scroll. 26 var shown = new Set(), slow = false; 27 if (document.getAnimations) document.getAnimations().forEach(function (a) { 28 if (a.animationName === 'ofm-failsafe' && a.playState === 'finished') { slow = true; shown.add(a.effect.target); } 29 }); 30 function seen(el) { 31 if (!slow) return false; 32 if (shown.has(el)) return true; 33 var r = el.getBoundingClientRect(); 34 return !!(r.width || r.height) && r.bottom > 0 && r.top < innerHeight; 35 } 36 37 var EASE = 'cubic-bezier(.2,.8,.2,1)'; 38 var POP = 'cubic-bezier(.34,1.56,.64,1)'; 39 // wipe/band clip with a margin (G) so glyphs that overflow the line box are never cut off mid-reveal: Noto Sans JP 40 // at line-height 1 (the home h1) draws Latin descenders ~.15em below the box. 41 var G = '-.25em'; 42 var V = { 43 up: { k: [{ opacity: 0, translate: '0 28px' }, { opacity: 1, translate: '0 0' }], d: 800 }, 44 rise: { k: [{ opacity: 0, translate: '0 56px', scale: '0.97' }, { opacity: 1, translate: '0 0', scale: '1' }], d: 1000 }, 45 left: { k: [{ opacity: 0, translate: '-40px 0' }, { opacity: 1, translate: '0 0' }], d: 800 }, 46 pop: { k: [{ opacity: 0, scale: '0.6' }, { opacity: 1, scale: '1' }], d: 650, e: POP }, 47 fade: { k: [{ opacity: 0 }, { opacity: 1 }], d: 700 }, 48 wipe: { k: [{ clipPath: 'inset(' + G + ' ' + G + ' calc(100% + .25em) ' + G + ')', translate: '0 6%' }, { clipPath: 'inset(' + G + ')', translate: '0 0' }], d: 1100 }, 49 band: { k: [{ clipPath: 'inset(' + G + ' calc(100% + .25em) ' + G + ' ' + G + ')' }, { clipPath: 'inset(' + G + ')' }], d: 900 }, 50 settle: { k: [{ scale: '1.08' }, { scale: '1' }], d: 1800 } 51 }; 52 53 // Left alone: the GSAP scroll stories and pinned sections (inline scripts on the ED / hair loss / weight loss pages), 54 // headers and nav, sticky CTAs, Webflow marquees (.marquee, .moving-carousel), popups, tabs, footer. 55 var SKIP = '.ed-animated-section, .why-noah-how-it-works, #tired-section, #plan-section, #doctor-section, ' + 56 '#tired-sticky, #plan-sticky, #doctor-sticky, #why-noah-sticky, .pin-spacer, .navbar-holder, header, nav, ' + 57 '.ed-cta-wrapper, .section_sticky-cta, .condition_cta, .marquee, .moving-carousel, [class*="popup"], ' + 58 '[class*="pop-up"], [class*="pop_up"], .w-tab-content, footer'; 59 var IX_HIDDEN = '[style*="opacity:0"], [style*="opacity: 0"]'; 60 61 // [selector, variant, options]. stagger: animate the element's children one after another (kids filters them); 62 // delay / step in ms. Order matters only in that an element is animated by the first rule that matches it. 63 var RULES = [
64 // Heroes (on screen at load) 65 ['.home-page-hero-sec :is(.hero-text-wrapper, .hero-text-wrapper-home-ja) > h1', 'wipe', { delay: 50 }], 66 ['.home-page-hero-sec :is(.hero-text-wrapper, .hero-text-wrapper-home-ja)', 'up', { stagger: true, kids: ':not(h1)', delay: 300 }], 67 ['.home-page-hero-sec .category-sliders', 'rise', { stagger: true, delay: 400, step: 110 }], 68 ['.ed-hero-container .slide:not(.small)', 'wipe', { delay: 150 }], 69 ['.ed-hero-container .slide.small:only-child', 'up', { stagger: true, delay: 150, step: 100 }], // JP hair loss: no headline 70 ['.ed-hero-container .slide.small', 'up', { stagger: true, delay: 500, step: 100 }], 71 ['.wl-badge-absolute', 'pop', { delay: 800 }], 72 ['.hero-bg, .hl-hero-bg, .wl-hero-bg', 'settle'], 73 ['[class^="ed-hero-col"]', 'up', { stagger: true, delay: 100, step: 90 }], 74 ['.pe-card_component.is-pe', 'rise', { delay: 50 }], 75 ['.pe-card_component .ed-card_subtitle', 'up', { delay: 200 }], 76 ['.pe-card_component .ed-card_content > .max-width-custom', 'wipe', { delay: 260 }], 77 ['.pe-card_component .card_dots', 'band', { delay: 500 }], 78 ['.pe-card_component .banner_feature_list', 'up', { stagger: true, delay: 520, step: 90 }], 79 ['.pe-card_component > .z-index-2', 'up', { delay: 720 }], 80 81 // Home 82 ['.category-info-wrapper, .category-info-wrapper-black-text', 'up', { stagger: true, step: 110 }], 83 ['.category-med-slider-wrapper > :first-child', 'up', { delay: 150 }], 84 ['.condition-cards-wrapper', 'rise', { stagger: true, delay: 200, step: 100 }], 85 ['.custom-container-new.simplified > .hero-text-wrapper', 'up', { stagger: true, step: 120 }], 86 ['.simplified-card', 'rise'], 87 ['.simplified-text-wrapper', 'up', { stagger: true, delay: 250, step: 110 }], 88 89 // Condition pages (ED, hair loss, weight loss; same template in JP and EN) 90 ['.desc .offset-110px', 'up', { stagger: true, step: 120 }], 91 ['.medication-section .med-card-new', 'up', { delay: 100 }], 92 ['.hard-enough-con .left-align-container', 'up', { stagger: true, kids: ':not(.ctas-wrapper)', step: 110 }], 93 ['.ctas-wrapper', 'left', { stagger: true, delay: 300, step: 80 }], 94 ['.hl-content-wrapper > :first-child', 'up'], 95 ['.hl-content-wrapper .hl-bullets', 'up', { stagger: true, delay: 150, step: 80 }], 96 ['.plan-sec .t-left-col', 'up', { stagger: true, step: 110 }], 97 ['.t-right-col-plan > .swiper-wrapper', 'rise', { stagger: true, delay: 200, step: 110 }], 98 ['.mj-lp-sec > a', 'rise'], 99 ['.personalized .flex-box-28px-gap', 'up', { stagger: true, step: 120 }], 100 ['.personalized .bullet-wrapper', 'up', { stagger: true, delay: 250, step: 80 }], 101 ['.expect-tt-holder', 'up', { stagger: true, step: 120 }], 102 ['.expect-flex', 'rise', { stagger: true, delay: 150, step: 120 }], 103 ['[class^="bmi-calculator-block"]', 'up', { stagger: true, step: 100 }], 104 ['[class^="bmi_calculator-graph-wrapper"]', 'rise', { delay: 200 }], 105 ['.faq-title-flexbox', 'up'], 106 ['.new-faq-ed_list', 'up', { stagger: true, step: 70 }], 107 ['.extra-question', 'up', { delay: 200 }], 108 ['.ceo-speech .offset-100px', 'up', { stagger: true, step: 160 }], 109 110 // English premature ejaculation (older template, same as noah-sg's) 111 ['.sing-men-stat', 'pop'], 112 ['.stopwatch', 'rise', { delay: 150 }], 113 ['.hair-loss-stats_text', 'up', { stagger: true, step: 110 }], 114 ['.never-treatment-wrapper', 'up', { stagger: true, step: 120 }], 115 ['.orange-stats-wrapper', 'rise', { stagger: true, step: 110 }], 116 ['.pill-tab', 'up'], 117 ['.here-expect-width-50', 'up'], 118 ['.icons-grid', 'pop', { stagger: true, kids: ':not([class*="dotted-line"])', delay: 150, step: 130 }], 119 ['.icons-grid > [class*="dotted-line"]', 'band', { delay: 100 }], 120 ['[class^="_3-expect-grid_right"]', 'up', { stagger: true, delay: 250, step: 100 }], 121 ['.faq-two-col > .col-50:first-child', 'up', { stagger: true, step: 120 }], 122 ['.faq-two-col > .col-50.padding', 'up', { stagger: true, step: 70 }], 123 124 // Japanese premature ejaculation (its own template) 125 ['.logo-moving-section .container-large', 'up'], 126 ['.pe_treat_benef_content', 'up', { stagger: true, step: 120 }], 127 ['.pe_treat_benef_img', 'rise', { delay: 150 }], 128 ['.teal_dotted_line', 'band', { delay: 200 }], 129 ['.pe_meds-component .info_content-item > :not(.w-dyn-list)', 'up'], 130 ['.pe_meds-component ._4-col-med-list', 'rise', { stagger: true, delay: 150, step: 120 }], 131 ['.examination_top > :is(.text-align-center, .flex-center-align)', 'up'], 132 ['.examination_top .med_exam_step_wrapper', 'rise', { delay: 100 }], 133 ['.examination_top > .button-wrapper', 'up', { stagger: true, step: 120 }] 134 ]; 135 136 // Same, for blocks other scripts add on DOMContentLoaded (after this script has run): the JP "å®å ¨ã«ä½¿ãããã«"
137 // section that jp23ed/hl/wl-sideeffects.js add to the ED, hair loss and weight loss pages, and the fee note 138 // (of-jp-fee-note.js; on ED / hair loss it sits inside .med-card-new, which already moves). 139 var LATE_RULES = [ 140 ['.jp23ed-safely-head', 'up', { stagger: true, step: 120 }], 141 ['.jp23ed-safely-grid', 'rise', { stagger: true, delay: 150, step: 120 }], 142 ['.plan-sec > .of-fee-note', 'up'] 143 ]; 144 145 var claimed = new WeakSet(); 146 var triggers = new Map(); // trigger element -> animations to play when it scrolls into view 147 148 function usable(el) { 149 return !claimed.has(el) && !el.closest(SKIP) && !el.closest(IX_HIDDEN) && !el.querySelector(IX_HIDDEN); 150 } 151 152 function prep(el, variant, delay, trigger) { 153 if (!usable(el) || seen(el)) return; 154 claimed.add(el); 155 var v = V[variant]; 156 var a = el.animate(v.k, { duration: v.d, easing: v.e || EASE, delay: delay || 0, fill: 'both' }); 157 a.pause(); 158 // Hand the element back to its own CSS (hover transitions, Webflow interactions) once it's in place. 159 a.onfinish = function () { a.cancel(); }; 160 if (!triggers.has(trigger)) triggers.set(trigger, []); 161 triggers.get(trigger).push(a); 162 } 163 164 function apply(rules) { 165 try {
166 rules.forEach(function (rule) { 167 var o = rule[2] || {}; 168 var els; 169 try { els = document.querySelectorAll(rule[0]); } catch (e) { return; } // selector this browser can't parse 170 els.forEach(function (el) { 171 if (!o.stagger) return prep(el, rule[1], o.delay, el); 172 if (el.closest(SKIP)) return; 173 var kids = Array.prototype.filter.call(el.children, function (c) { return !o.kids || c.matches(o.kids); }); 174 kids.forEach(function (c, i) { prep(c, rule[1], (o.delay || 0) + Math.min(i, 8) * (o.step || 90), el); }); 175 }); 176 }); 177 } catch (e) { 178 // Never leave anything hidden: show whatever was already prepared. 179 triggers.forEach(function (anims) { anims.forEach(function (a) { a.cancel(); }); }); 180 triggers.clear(); 181 } 182 } 183 184 try { apply(RULES); } finally { ready(); } 185 186 // Scroll reveals: rect-based rather than IntersectionObserver, so swiper slides that start off to the side 187 // still reveal with their row, and clip-path doesn't hide a target from its own trigger. 188 var raf = 0, visible = false, listening = false; 189 function check() { 190 raf = 0; 191 var line = window.innerHeight * 0.88; 192 triggers.forEach(function (anims, t) { 193 var r = t.getBoundingClientRect(); 194 if (r.top < line || (!r.width && !r.height)) { 195 anims.forEach(function (a) { a.play(); }); 196 triggers.delete(t); 197 } 198 }); 199 if (!triggers.size && listening) { listening = false; removeEventListener('scroll', onScroll); removeEventListener('resize', onScroll); } 200 } 201 function onScroll() { if (!raf) raf = requestAnimationFrame(check); } 202 function start() { 203 if (!listening && triggers.size) { listening = true; addEventListener('scroll', onScroll, { passive: true }); addEventListener('resize', onScroll); } 204 check(); 205 } 206 207 // VWO hides the whole body until its script arrives (up to 2s); start once the page can actually be seen. VWO first 208 // empties the hide rule and removes the <style> a moment later, so check the body itself. 209 function hiddenByVwo() { 210 return !!document.getElementById('_vis_opt_path_hides') && +getComputedStyle(document.body).opacity < 0.05; 211 } 212 function whenVisible(fn) { 213 if (!hiddenByVwo()) return fn(); 214 var done = false; 215 var go = function () { if (!done) { done = true; mo.disconnect(); fn(); } }; 216 var mo = new MutationObserver(function () { if (!hiddenByVwo()) go(); }); 217 mo.observe(document.head, { childList: true, subtree: true, characterData: true }); 218 setTimeout(go, 2500); 219 } 220 whenVisible(function () { visible = true; start(); }); 221 222 // Their DOMContentLoaded handlers were registered before this one, so the blocks exist by the time it runs. (A defer 223 // script runs while readyState is already "interactive", before DOMContentLoaded; `load` is the fallback.) 224 var lateDone = false; 225 function late() { if (lateDone) return; lateDone = true; apply(LATE_RULES); if (visible) start(); } 226 if (document.readyState === 'complete') late(); 227 else { document.addEventListener('DOMContentLoaded', late); addEventListener('load', late); } 228 229 // Magnetic primary CTAs (mouse / trackpad only). 230 if (window.matchMedia && matchMedia('(hover: hover) and (pointer: fine)').matches) { 231 document.querySelectorAll('.category-cta, .ed-cta-button, [class^="button-orange"], [class*=" button-orange"], ' + 232 ':is(.section_hero-image, .section_med_cards, .section_examination) .button').forEach(function (el) { 233 el.style.transition = (el.style.transition ? el.style.transition + ', ' : '') + 'translate 350ms ' + EASE; 234 el.addEventListener('pointermove', function (e) { 235 var r = el.getBoundingClientRect(); 236 el.style.translate = ((e.clientX - r.left - r.width / 2) * 0.22).toFixed(1) + 'px ' + ((e.clientY - r.top - r.height / 2) * 0.35).toFixed(1) + 'px'; 237 }); 238 el.addEventListener('pointerleave', function () { el.style.translate = ''; }); 239 }); 240 } 241})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.