PageSourceSearch

https://www.datahash.com/js/dh-motion.js?v=3ed20b56ad

js datahash.com collected 2026-10-02 02:12:43 UTC 13,565 bytes, 329 lines download raw bytes

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.