1/*! 2 * DH Motion v1.1 â Datahash site-wide animation engine 3 * v1.1 (site build): adds data-dh-count (number counter on reveal, per the 4 * web brand guideline §5) and manual mode for autotabs (data-dh-interval="0"). 5 * Everything else is the reconstructed v1.0 contract, unchanged. 6 * ---------------------------------------------------- 7 * ONE script, pasted ONCE into Site Settings â Custom Code â Before </body>. 8 * Every component opts in via data attributes. No external libraries. 9 * 10 * Behaviors: 11 * 1. data-dh-type Typewriter: type â hold â backspace â next word 12 * words via data-dh-words="First phrase|Second phrase" 13 * optional data-dh-type-speed (ms/char, default 55) 14 * optional data-dh-hold (ms, default 1800) 15 * 2. data-dh-fill Scroll-fill headline: outline text fills word-by-word 16 * in reading order as the element moves through viewport 17 * 3. data-dh-autotabs Auto-navigating tabs with progress-bar feedback 18 * tabs: [data-dh-tab] (engine injects fill bar) 19 * panels: [data-dh-panel] (matched by DOM order) 20 * optional data-dh-interval (ms, default 5000) 21 * 4. data-dh-anim="rise" Enter animation on scroll (rise+fade) 22 * data-dh-anim="fade" Fade only 23 * Parent with data-dh-stagger="80" staggers child [data-dh-anim] items 24 * 25 * Color semantics (Shared Style Lock): blue owns flow/progress â tab progress 26 * bars are Datahash blue. Caret is brand red (brand accent, not status). 27 * Motion explains causality, never decorates. prefers-reduced-motion renders 28 * everything in final state with cycling disabled. 29 */ 30(function () { 31 'use strict'; 32 if (window.DHMotion) return; 33 34 var REDUCED = window.matchMedia && 35 window.matchMedia('(prefers-reduced-motion: reduce)').matches; 36 37 /* ---------- runtime stylesheet (engine-owned classes only) ---------- */ 38 var css = [ 39 ':root{--dh-red:#E72939;--dh-slate:#283E51;--dh-blue:#0375CC;', 40 '--dh-green:#00C3AF;--dh-orange:#FE9400;--dh-grey:#91A6A7;', 41 '--dh-ease:cubic-bezier(.22,.61,.36,1);}', 42 43 /* typewriter */ 44 '.dh-caret{display:inline-block;width:.08em;min-width:2px;height:1em;', 45 'margin-left:.06em;background:var(--dh-red);vertical-align:-0.08em;', 46 'animation:dhCaret 1.05s steps(1) infinite;}', 47 '@keyframes dhCaret{0%,55%{opacity:1}56%,100%{opacity:0}}', 48 49 /* scroll-fill: stroke/fill follow the surface heading token so the 50 headline stays legible on every archetype in both themes */ 51 '[data-dh-fill] .dh-w{-webkit-text-stroke:1px var(--heading,var(--dh-slate));', 52 'color:transparent;transition:color .45s var(--dh-ease);}', 53 '[data-dh-fill] .dh-w.is-filled{color:var(--heading,var(--dh-slate));}', 54 '[data-dh-fill].dh-on-dark .dh-w{-webkit-text-stroke:1px rgba(255,255,255,.85);}', 55 '[data-dh-fill].dh-on-dark .dh-w.is-filled{color:#fff;}', 56 57 /* auto-tabs */ 58 '[data-dh-tab]{position:relative;cursor:pointer;}', 59 '.dh-tabfill{position:absolute;left:0;bottom:0;height:2px;width:0%;', 60 'background:var(--dh-blue);pointer-events:none;}', 61 '[data-dh-tab].is-active .dh-tabfill{width:100%;', 62 'transition:width var(--dh-tabdur,5000ms) linear;}', 63 '[data-dh-panel]{display:none;}', 64 '[data-dh-panel].is-active{display:block;animation:dhPanel .5s var(--dh-ease);}', 65 '@keyframes dhPanel{from{opacity:0;transform:translateY(10px)}', 66 'to{opacity:1;transform:none}}', 67 68 /* enter animations */ 69 '[data-dh-anim]{opacity:0;}', 70 '[data-dh-anim="rise"]{transform:translateY(28px);}', 71 '[data-dh-anim].dh-in{opacity:1;transform:none;', 72 'transition:opacity .7s var(--dh-ease),transform .7s var(--dh-ease);', 73 'transition-delay:var(--dh-d,0ms);}' 74 ].join(''); 75 76 if (REDUCED) { 77 css += '[data-dh-anim]{opacity:1!important;transform:none!important;}' + 78 '.dh-caret{animation:none;}' + 79 '[data-dh-fill] .dh-w{color:var(--heading,var(--dh-slate));-webkit-text-stroke:0;}' + 80 '[data-dh-fill].dh-on-dark .dh-w{color:#fff;}'; 81 } 82 var tag = document.createElement('style');
83 tag.id = 'dh-motion-css'; 84 tag.textContent = css; 85 document.head.appendChild(tag); 86 87 /* ------------------------------ helpers ------------------------------ */ 88 function $$(sel, root) { 89 return Array.prototype.slice.call((root || document).querySelectorAll(sel)); 90 } 91 var io = ('IntersectionObserver' in window) ? new IntersectionObserver(onIO, { 92 threshold: 0.18 93 }) : null; 94 var ioHandlers = new Map(); 95 function onIO(entries) { 96 entries.forEach(function (e) { 97 var h = ioHandlers.get(e.target); 98 if (h) h(e.isIntersecting, e); 99 }); 100 } 101 function observe(el, fn) { 102 if (!io) { fn(true); return; } 103 ioHandlers.set(el, fn); 104 io.observe(el); 105 } 106 107 /* --------------------------- 1. typewriter --------------------------- */ 108 function initType(el) { 109 if (el.__dh) return; el.__dh = 1; 110 var words = (el.getAttribute('data-dh-words') || el.textContent) 111 .split('|').map(function (w) { return w.trim(); }).filter(Boolean); 112 if (!words.length) return; 113 114 var speed = +el.getAttribute('data-dh-type-speed') || 55; 115 var hold = +el.getAttribute('data-dh-hold') || 1800; 116 117 el.textContent = ''; 118 var span = document.createElement('span'); 119 var caret = document.createElement('span'); 120 caret.className = 'dh-caret'; 121 caret.setAttribute('aria-hidden', 'true'); 122 el.appendChild(span); el.appendChild(caret); 123 el.setAttribute('aria-label', words.join(', ')); 124 125 if (REDUCED) { span.textContent = words[0]; return; } 126 127 var wi = 0, ci = 0, deleting = false, running = false, timer = null; 128 129 function tick() { 130 var word = words[wi]; 131 if (!deleting) { 132 ci++; 133 span.textContent = word.slice(0, ci); 134 if (ci === word.length) { deleting = true; timer = setTimeout(tick, hold); return; } 135 timer = setTimeout(tick, speed + Math.random() * 28); 136 } else { 137 ci--; 138 span.textContent = word.slice(0, ci); 139 if (ci === 0) { 140 deleting = false; wi = (wi + 1) % words.length; 141 timer = setTimeout(tick, 320); return; 142 } 143 timer = setTimeout(tick, Math.max(24, speed * 0.45)); 144 } 145 } 146 observe(el, function (inView) { 147 if (inView && !running) { running = true; tick(); } 148 else if (!inView && running) { running = false; clearTimeout(timer); } 149 }); 150 } 151 152 /* -------------------------- 2. scroll-fill --------------------------- */ 153 var fillEls = []; 154 function initFill(el) { 155 if (el.__dh) return; el.__dh = 1; 156 var text = el.textContent; 157 el.setAttribute('aria-label', text); 158 el.textContent = ''; 159 text.split(/\s+/).filter(Boolean).forEach(function (w, i, arr) { 160 var s = document.createElement('span'); 161 s.className = 'dh-w'; 162 s.setAttribute('aria-hidden', 'true'); 163 s.textContent = w; 164 el.appendChild(s); 165 if (i < arr.length - 1) el.appendChild(document.createTextNode(' ')); 166 }); 167 if (REDUCED) return; 168 el.__words = $$('.dh-w', el); 169 fillEls.push(el); 170 } 171 var fillTicking = false; 172 function fillFrame() { 173 fillTicking = false; 174 var vh = window.innerHeight; 175 fillEls.forEach(function (el) { 176 var r = el.getBoundingClientRect(); 177 if (r.bottom < 0 || r.top > vh) return; 178 /* progress: 0 when element top hits 88% of vp, 1 when it reaches 38% */ 179 var p = (vh * 0.88 - r.top) / (vh * 0.5); 180 p = Math.max(0, Math.min(1, p)); 181 var n = Math.round(p * el.__words.length); 182 el.__words.forEach(function (w, i) { 183 w.classList.toggle('is-filled', i < n); 184 }); 185 }); 186 } 187 function onScroll() { 188 if (!fillTicking) { fillTicking = true; requestAnimationFrame(fillFrame); } 189 } 190 window.addEventListener('scroll', onScroll, { passive: true }); 191 window.addEventListener('resize', onScroll, { passive: true }); 192 193 /* --------------------------- 3. auto-tabs ---------------------------- */ 194 function initAutoTabs(root) { 195 if (root.__dh) return; root.__dh = 1; 196 var tabs = $$('[data-dh-tab]', root); 197 var panels = $$('[data-dh-panel]', root); 198 if (!tabs.length || tabs.length !== panels.length) return; 199 200 var rawInterval = root.getAttribute('data-dh-interval'); 201 var interval = rawInterval === null ? 5000 : +rawInterval; 202 var manual = !(interval > 0); /* v1.1: 0 = manual tabs */ 203 if (manual) root.classList.add('dh-tabs-manual'); 204 root.style.setProperty('--dh-tabdur', (manual ? 0 : interval) + 'ms'); 205 var idx = 0, timer = null, inView = false, hovered = false;
206 207 tabs.forEach(function (t, i) { 208 var fill = document.createElement('span'); 209 fill.className = 'dh-tabfill'; 210 t.appendChild(fill); 211 t.setAttribute('role', 'tab'); 212 t.setAttribute('tabindex', '0'); 213 function pick() { activate(i); restart(); } 214 t.addEventListener('click', pick); 215 t.addEventListener('keydown', function (e) { 216 if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pick(); } 217 }); 218 }); 219 panels.forEach(function (p) { p.setAttribute('role', 'tabpanel'); }); 220 221 function activate(i) { 222 idx = i; 223 tabs.forEach(function (t, k) { 224 /* force progress-bar restart: drop class, reflow, re-add */ 225 t.classList.remove('is-active'); 226 if (k === i) { void t.offsetWidth; t.classList.add('is-active'); } 227 t.setAttribute('aria-selected', k === i ? 'true' : 'false'); 228 }); 229 panels.forEach(function (p, k) { 230 p.classList.toggle('is-active', k === i); 231 }); 232 } 233 function next() { activate((idx + 1) % tabs.length); } 234 function restart() { 235 clearInterval(timer); timer = null; 236 if (manual || REDUCED || !inView || hovered) return; 237 timer = setInterval(next, interval); 238 } 239 root.addEventListener('mouseenter', function () { hovered = true; restart(); }); 240 root.addEventListener('mouseleave', function () { hovered = false; restart(); }); 241 document.addEventListener('visibilitychange', restart); 242 observe(root, function (vis) { inView = vis; restart(); }); 243 activate(0); 244 } 245 246 /* ----------------------- 3b. counters (v1.1) ------------------------- */ 247 function initCount(el) { 248 if (el.__dh) return; el.__dh = 1; 249 var raw = el.getAttribute('data-dh-count') || el.textContent; 250 var target = parseFloat(raw); 251 if (isNaN(target)) return; 252 var decimals = (raw.split('.')[1] || '').length; 253 var dur = +el.getAttribute('data-dh-count-dur') || 1100; 254 el.textContent = (0).toFixed(decimals); 255 if (REDUCED) { el.textContent = target.toFixed(decimals); return; } 256 observe(el, function (inView) { 257 if (!inView || el.__dhDone) return; 258 el.__dhDone = 1; 259 io && io.unobserve(el); ioHandlers.delete(el); 260 var t0 = null; 261 function frame(ts) { 262 if (!t0) t0 = ts; 263 var p = Math.min(1, (ts - t0) / dur); 264 /* ease-out cubic: the value decelerates as it lands */ 265 var v = target * (1 - Math.pow(1 - p, 3)); 266 el.textContent = v.toFixed(decimals); 267 if (p < 1) requestAnimationFrame(frame); 268 else el.textContent = target.toFixed(decimals); 269 } 270 requestAnimationFrame(frame); 271 }); 272 } 273 274 /* --------------------- 3c. rotator (crossfade) ----------------------- */ 275 function initRotate(el) { 276 if (el.__dh) return; el.__dh = 1; 277 var items = $$(':scope > *', el); 278 if (items.length < 2) { if (items[0]) items[0].classList.add('is-active'); return; } 279 var i = 0; 280 items.forEach(function (it, idx) { it.classList.toggle('is-active', idx === 0); }); 281 if (REDUCED) return; 282 var period = +el.getAttribute('data-dh-rotate-interval') || 3800; 283 var timer = null; 284 function step() { 285 items[i].classList.remove('is-active'); 286 i = (i + 1) % items.length; 287 items[i].classList.add('is-active'); 288 } 289 /* only cycle while the rotator is on screen */ 290 observe(el, function (inView) { 291 if (inView && !timer) { timer = setInterval(step, period); } 292 else if (!inView && timer) { clearInterval(timer); timer = null; } 293 }); 294 } 295 296 /* ----------------------- 4. enter animations ------------------------- */ 297 function initRise(el) { 298 if (el.__dh) return; el.__dh = 1; 299 if (REDUCED) { el.classList.add('dh-in'); return; } 300 observe(el, function (inView) { 301 if (inView) { el.classList.add('dh-in'); io && io.unobserve(el); ioHandlers.delete(el); } 302 }); 303 } 304 function initStagger(parent) { 305 if (parent.__dhS) return; parent.__dhS = 1; 306 var gap = +parent.getAttribute('data-dh-stagger') || 80; 307 $$('[data-dh-anim]', parent).forEach(function (el, i) { 308 el.style.setProperty('--dh-d', (i * gap) + 'ms'); 309 }); 310 } 311 312 /* ------------------------------ boot --------------------------------- */ 313 function scan(root) { 314 $$('[data-dh-stagger]', root).forEach(initStagger); 315 $$('[data-dh-anim]', root).forEach(initRise); 316 $$('[data-dh-type]', root).forEach(initType); 317 $$('[data-dh-fill]', root).forEach(initFill); 318 $$('[data-dh-autotabs]', root).forEach(initAutoTabs); 319 $$('[data-dh-count]', root).forEach(initCount); 320 $$('[data-dh-rotate]', root).forEach(initRotate); 321 onScroll(); 322 } 323 function boot() { scan(document); } 324 if (document.readyState === 'loading') { 325 document.addEventListener('DOMContentLoaded', boot); 326 } else { boot(); } 327 328 window.DHMotion = { refresh: function (root) { scan(root || document); } }; 329})();
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.