PageSourceSearch

https://zhiyuanryanchen.github.io/javascripts/motion.js

js zhiyuanryanchen.github.io collected 2026-10-03 10:14:49 UTC 5,320 bytes, 147 lines download raw bytes

1/* motion.js — 界面动效统一脚本
2   场景 A:二级按钮内容切换入场(委托监听,不改各页内联逻辑)
3   场景 B:Abstract 手风琴 grid 折叠展开(接管 research 页旧逻辑)
4   场景 D:滚动入场(IntersectionObserver,首屏元素跳过) */
5(function () {
6  'use strict';
7
8  var reduceMotion = window.matchMedia &&
9    window.matchMedia('(prefers-reduced-motion: reduce)').matches;
10
11  /* ---------- 场景 A:切换内容入场 ---------- */
12  function animateSection(targetId) {
13    var el = document.getElementById(targetId);
14    if (!el) return;
15    el.classList.remove('section-enter');
16    void el.offsetWidth; /* 强制 reflow 以重启动画 */
17    el.classList.add('section-enter');
18    el.addEventListener('animationend', function handler() {
19      el.classList.remove('section-enter');
20      el.removeEventListener('animationend', handler);
21    });
22  }
23
24  /* ---------- 场景 B:手风琴 ---------- */
25  /* 初始化:把每个 panel 的内容包进 .acc-wrap > .acc-inner,
26     grid-template-rows 0fr -> 1fr 实现高度自适应的折叠动画 */
27  function prepareAccordions() {
28    var panels = document.querySelectorAll('div.panel');
29    Array.prototype.forEach.call(panels, function (panel) {
30      if (panel.querySelector(':scope > .acc-wrap')) return;
31      var wrap = document.createElement('div');
32      wrap.className = 'acc-wrap';
33      var inner = document.createElement('div');
34      inner.className = 'acc-inner';
35      while (panel.firstChild) inner.appendChild(panel.firstChild);
36      wrap.appendChild(inner);
37      panel.appendChild(wrap);
38      if (panel.classList.contains('show')) wrap.classList.add('open');
39    });
40  }
41
42  function toggleAccordion(btn) {
43    var host = btn.parentElement;
44    if (!host) return;
45    var panel = host.nextElementSibling;
46    if (!panel || !panel.classList.contains('panel')) return;
47    var wrap = panel.querySelector(':scope > .acc-wrap');
48
49    btn.classList.toggle('active');
50    var willShow = !panel.classList.contains('show');
51
52    if (willShow) {
53      panel.classList.add('show');
54      if (wrap) {
55        /* 双 rAF 确保先完成 display 切换,再播放展开过渡 */
56        requestAnimationFrame(function () {
57          requestAnimationFrame(function () { wrap.classList.add('open'); });
58        });
59      }
60    } else if (wrap) {
61      wrap.classList.remove('open');
62      var settled = false;
63      var finish = function () {
64        if (settled) return;
65        settled = true;
66        wrap.removeEventListener('transitionend', onEnd);
67        if (!wrap.classList.contains('open')) panel.classList.remove('show');
68      };
69      var onEnd = function (e) {
70        if (e.target === wrap && e.propertyName !== 'grid-template-rows') return;
71        finish();
72      };
73      wrap.addEventListener('transitionend', onEnd);
74      setTimeout(finish, 320); /* 兜底:transitionend 未触发时收起 */
75    } else {
76      panel.classList.remove('show');
77    }
78  }
79
80  /* ---------- 场景 D:滚动入场 ---------- */
81  var REVEAL_SELECTOR = '.paper-item, .handout-card, .scheduled-updates, ' +
82    '.updates-list li, .lecture-card, .resource-list li, .event-list li, ' +
83    '.course-item, #ug > li, #grad > li';
84
85  function setupReveal() {
86    if (reduceMotion || !('IntersectionObserver' in window)) return;
87    var vh = window.innerHeight || document.documentElement.clientHeight;
88    var candidates = document.querySelectorAll(REVEAL_SELECTOR);
89    var targets = [];
90
91    Array.prototype.forEach.call(candidates, function (el) {
92      var rect = el.getBoundingClientRect();
93      var isHidden = rect.width === 0 && rect.height === 0;
94      /* 首屏可见元素不做滚动入场(页面入场动画已覆盖) */
95      if (!isHidden && rect.top < vh && rect.bottom > 0) return;
96      el.classList.add('reveal');
97      targets.push(el);
98    });
99
100    if (!targets.length) return;
101    var observer = new IntersectionObserver(function (entries) {
102      var batch = 0;
103      entries.forEach(function (entry) {
104        if (!entry.isIntersecting) return;
105        var el = entry.target;
106        observer.unobserve(el);
107        /* 同批元素 50ms 递进,300ms 封顶 */
108        el.style.setProperty('--reveal-delay',
109          Math.min(batch * 50, 300) + 'ms');
110        el.classList.add('is-visible');
111        batch += 1;
112      });
113    }, { threshold: 0.08, rootMargin: '0px 0px -5% 0px' });
114
115    targets.forEach(function (el) { observer.observe(el); });
116  }
117
118  /* ---------- 事件委托 ---------- */
119  function onDocClick(e) {
120    var target = e.target;
121    if (!target || !target.closest) return;
122
123    var accBtn = target.closest('button.accordion');
124    if (accBtn) {
125      toggleAccordion(accBtn);
126      return;
127    }
128
129    var toggle = target.closest('.pub-btn, .sub-link, .pub-subbtn');
130    if (toggle && toggle.getAttribute('data-target') && !reduceMotion) {
131      /* 各页内联脚本先行切换 display,此处为新区块补入场动画 */
132      animateSection(toggle.getAttribute('data-target'));
133    }
134  }
135
136  function init() {
137    prepareAccordions();
138    setupReveal();
139    document.addEventListener('click', onDocClick);
140  }
141
142  if (document.readyState === 'loading') {
143    document.addEventListener('DOMContentLoaded', init);
144  } else {
145    init();
146  }
147})();

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.