feat: integrate Motion One animation system (default-on)

- Add assets/motion.min.js (local fallback for offline demos)
- template.html: embed loader with local + CDN fallback chain,
  5 recipes (cascade / hero / quote / directional / pipeline)
- layouts.md: add data-anim markers to all 10 layouts,
  data-animate on pipeline / quote / directional layouts,
  Pre-flight section E documenting recipe decision tree
- components.md: add Motion 动效系统 chapter with loader,
  data attributes, recipes table, decision tree, FAQ
- checklist.md: add P1 entries 9b/9c and a 动效 self-check block
- SKILL.md: document animations in features list,
  resource tree, and loading order hints

Pipeline pages use manual advance: → / space / wheel-down light
up steps one by one; final → advances to next slide.
This commit is contained in:
郭浩 committed 2026-04-26 11:47:24 +08:00
1 parent a593cf4b73
commit e09f931308
6 files changed
+337 -69

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
/**
* Bundled by jsDelivr using Rollup v2.79.2 and Terser v5.39.0.
* Original file: /npm/motion@11.11.17/dist/es/motion/lib/index.mjs
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
function t(t,e){const n=t.indexOf(e);n>-1&&t.splice(n,1)}class e{constructor(){this.subscriptions=[]}add(e){var n,s;return n=this.subscriptions,s=e,-1===n.indexOf(s)&&n.push(s),()=>t(this.subscriptions,e)}notify(t,e,n){const s=this.subscriptions.length;if(s)if(1===s)this.subscriptions[0](t,e,n);else for(let i=0;i<s;i++){const s=this.subscriptions[i];s&&s(t,e,n)}}getSize(){return this.subscriptions.length}clear(){this.subscriptions.length=0}}function n(t,e){return e?t*(1e3/e):0}const s=!1,i=t=>t;const r=["read","resolveKeyframes","update","preRender","render","postRender"];const{schedule:o,cancel:a,state:l,steps:u}=function(t,e){let n=!1,s=!0;const i={delta:0,timestamp:0,isProcessing:!1},o=()=>n=!0,a=r.reduce(((t,e)=>(t[e]=function(t){let e=new Set,n=new Set,s=!1,i=!1;const r=new WeakSet;let o={delta:0,timestamp:0,isProcessing:!1};function a(e){r.has(e)&&(l.schedule(e),t()),e(o)}const l={schedule:(t,i=!1,o=!1)=>{const a=o&&s?e:n;return i&&r.add(t),a.has(t)||a.add(t),t},cancel:t=>{n.delete(t),r.delete(t)},process:t=>{o=t,s?i=!0:(s=!0,[e,n]=[n,e],n.clear(),e.forEach(a),s=!1,i&&(i=!1,l.process(t)))}};return l}(o),t)),{}),{read:l,resolveKeyframes:u,update:c,preRender:h,render:d,postRender:p}=a,f=()=>{const r=performance.now();n=!1,i.delta=s?1e3/60:Math.max(Math.min(r-i.timestamp,40),1),i.timestamp=r,i.isProcessing=!0,l.process(i),u.process(i),c.process(i),h.process(i),d.process(i),p.process(i),i.isProcessing=!1,n&&e&&(s=!1,t(f))};return{schedule:r.reduce(((e,r)=>{const o=a[r];return e[r]=(e,r=!1,a=!1)=>(n||(n=!0,s=!0,i.isProcessing||t(f)),o.schedule(e,r,a)),e}),{}),cancel:t=>{for(let e=0;e<r.length;e++)a[r[e]].cancel(t)},state:i,steps:a}}("undefined"!=typeof requestAnimationFrame?requestAnimationFrame:i,!0);let c;function h(){c=void 0}const d={now:()=>(void 0===c&&d.set(l.isProcessing||s?l.timestamp:performance.now()),c),set:t=>{c=t,queueMicrotask(h)}};class p{constructor(t,e={}){this.version="11.11.17",this.canTrackVelocity=null,this.events={},this.updateAndNotify=(t,e=!0)=>{const n=d.now();this.updatedAt!==n&&this.setPrevFrameValue(),this.prev=this.current,this.setCurrent(t),this.current!==this.prev&&this.events.change&&this.events.change.notify(this.current),e&&this.events.renderRequest&&this.events.renderRequest.notify(this.current)},this.hasAnimated=!1,this.setCurrent(t),this.owner=e.owner}setCurrent(t){var e;this.current=t,this.updatedAt=d.now(),null===this.canTrackVelocity&&void 0!==t&&(this.canTrackVelocity=(e=this.current,!isNaN(parseFloat(e))))}setPrevFrameValue(t=this.current){this.prevFrameValue=t,this.prevUpdatedAt=this.updatedAt}onChange(t){return this.on("change",t)}on(t,n){this.events[t]||(this.events[t]=new e);const s=this.events[t].add(n);return"change"===t?()=>{s(),o.read((()=>{this.events.change.getSize()||this.stop()}))}:s}clearListeners(){for(const t in this.events)this.events[t].clear()}attach(t,e){this.passiveEffect=t,this.stopPassiveEffect=e}set(t,e=!0){e&&this.passiveEffect?this.passiveEffect(t,this.updateAndNotify):this.updateAndNotify(t,e)}setWithVelocity(t,e,n){this.set(e),this.prev=void 0,this.prevFrameValue=t,this.prevUpdatedAt=this.updatedAt-n}jump(t,e=!0){this.updateAndNotify(t),this.prev=t,this.prevUpdatedAt=this.prevFrameValue=void 0,e&&this.stop(),this.stopPassiveEffect&&this.stopPassiveEffect()}get(){return this.current}getPrevious(){return this.prev}getVelocity(){const t=d.now();if(!this.canTrackVelocity||void 0===this.prevFrameValue||t-this.updatedAt>30)return 0;const e=Math.min(this.updatedAt-this.prevUpdatedAt,30);return n(parseFloat(this.current)-parseFloat(this.prevFrameValue),e)}start(t){return this.stop(),new Promise((e=>{this.hasAnimated=!0,this.animation=t(e),this.events.animationStart&&this.events.animationStart.notify()})).then((()=>{this.events.animationComplete&&this.events.animationComplete.notify(),this.clearAnimation()}))}stop(){this.animation&&(this.animation.stop(),this.events.animationCancel&&this.events.animationCancel.notify()),this.clearAnimation()}isAnimating(){return!!this.animation}clearAnimation(){delete this.animation}destroy(){this.clearListeners(),this.stop(),this.stopPassiveEffect&&this.stopPassiveEffect()}}function f(t,e){return new p(t,e)}function m(t){let e;return()=>(void 0===e&&(e=t()),e)}const g=m((()=>void 0!==window.ScrollTimeline));class y{constructor(t){this.stop=()=>this.runAll("stop"),this.animations=t.filter(Boolean)}then(t,e){return Promise.all(this.animations).then(t).catch(e)}getAll(t){return this.animations[0][t]}setAll(t,e){for(let n=0;n<this.animations.length;n++)this.animations[n][t]=e}attachTimeline(t,e){const n=this.animations.map((n=>g()&&n.attachTimeline?n.attachTimeline(t):e(n)));return()=>{n.forEach(((t,e)=>{t&&t(),this.animations[e].stop()}))}}get time(){return this.getAll("time")}set time(t){this.setAll("time",t)}get speed(){return this.getAll("speed")}set speed(t){this.setAll("speed",t)}get startTime(){return this.getAll("startTime")}get duration(){let t=0;for(let e=0;e<this.animations.length;e++)t=Math.max(t,this.aLine truncated
//# sourceMappingURL=/sm/d5d4bfa5a3f292edc74ce2b856e25f03ebbfa4c4c22aa37e3f75ca1dabe374b0.map
+137 -3
View File
@@ -396,6 +396,21 @@
.chrome{font-family:var(--mono);font-size:max(11px,.78vw);letter-spacing:.2em;text-transform:uppercase;opacity:.62}
.foot{font-family:var(--mono);font-size:max(11px,.78vw);letter-spacing:.18em;text-transform:uppercase;opacity:.5}
/* ============ 动效系统(Motion One 驱动) ============
所有 [data-anim] 元素默认隐藏,进入页面时由 JS 逐个揭示。
- cascade(默认):按 DOM 顺序 stagger 120ms 淡入 + y:16→0
- hero(hero 页自动):慢一点的 stagger 180ms
- quote(含 [data-anim="line"]):逐行 600ms 淡入
- directional(含 [data-anim="left|right"]):左→分隔线→右
- pipeline(data-animate="pipeline"):Space/→ 手动推进
Motion One 没加载成功时(CDN 断 + 本地丢),所有 [data-anim] 会兜底显示。
*/
[data-anim]{opacity:0}
[data-anim="left"]{transform:translateX(-24px)}
[data-anim="right"]{transform:translateX(24px)}
[data-anim="line"]{opacity:0;transform:translateY(10px)}
[data-animate="pipeline"] [data-anim]{opacity:.15}
/* ---------- 响应式降级 ---------- */
@media (max-width:900px){
.display{font-size:16vw}
@@ -570,6 +585,8 @@ function go(n){
const el=slides[idx];
const th=el.dataset.theme || (el.classList.contains('light')?'light':(el.classList.contains('dark')?'dark':'dark'));
document.body.classList.toggle('light-bg',th==='light');
/* 动效:翻页过渡中段触发当前页的入场动画(由 motion-boot 注册) */
if(window.__playSlide) setTimeout(()=>window.__playSlide(idx), 450);
lock=true;setTimeout(()=>lock=false,700);
}
@@ -614,7 +631,11 @@ function toggleOverview(){
addEventListener('keydown',e=>{
if(e.key==='Escape'){e.preventDefault();toggleOverview();return;}
if(overviewOn)return;
if(e.key==='ArrowRight'||e.key==='PageDown'||e.key===' '||e.key==='ArrowDown')go(idx+1);
if(e.key==='ArrowRight'||e.key==='PageDown'||e.key===' '||e.key==='ArrowDown'){
if(window.__pipeAdvance && window.__pipeAdvance()) return;
go(idx+1);
return;
}
if(e.key==='ArrowLeft'||e.key==='PageUp'||e.key==='ArrowUp')go(idx-1);
if(e.key==='Home')go(0);
if(e.key==='End')go(total-1);
@@ -623,7 +644,13 @@ addEventListener('keydown',e=>{
let wheelTO=null,wheelAcc=0;
addEventListener('wheel',e=>{
wheelAcc+=e.deltaY+e.deltaX;
if(Math.abs(wheelAcc)>50){go(idx+(wheelAcc>0?1:-1));wheelAcc=0;}
if(Math.abs(wheelAcc)>50){
if(wheelAcc>0 && window.__pipeAdvance && window.__pipeAdvance()){
wheelAcc=0;
}else{
go(idx+(wheelAcc>0?1:-1));wheelAcc=0;
}
}
clearTimeout(wheelTO);wheelTO=setTimeout(()=>wheelAcc=0,150);
},{passive:true});
@@ -632,12 +659,119 @@ addEventListener('touchstart',e=>{tx=e.touches[0].clientX;ty=e.touches[0].client
addEventListener('touchend',e=>{
const dx=(e.changedTouches[0].clientX-tx);
const dy=(e.changedTouches[0].clientY-ty);
if(Math.abs(dx)>50&&Math.abs(dx)>Math.abs(dy))go(idx+(dx<0?1:-1));
if(Math.abs(dx)>50&&Math.abs(dx)>Math.abs(dy)){
if(dx<0 && window.__pipeAdvance && window.__pipeAdvance()) return;
go(idx+(dx<0?1:-1));
}
},{passive:true});
go(0);
</script>
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
<script>lucide.createIcons();</script>
<!-- ============ Motion One 动效引擎(本地优先,CDN 兜底) ============
加载策略:先 ./assets/motion.min.js(本地,离线可用),失败 fallback 到 jsDelivr CDN
加载完成后挂 window.__playSlide / window.__pipeAdvance,
导航脚本会在翻页中段调用 __playSlide(idx),键盘/滚轮/触屏会在翻页前调用 __pipeAdvance()
双双失败时会兜底把所有 [data-anim] 设为可见,不让动效破坏阅读
-->
<script type="module">
let motion;
try {
motion = await import('./assets/motion.min.js');
} catch(e1) {
try {
motion = await import('https://cdn.jsdelivr.net/npm/motion@11.11.17/+esm');
} catch(e2) {
console.warn('[motion] local + CDN both failed, disabling animations', e1, e2);
document.querySelectorAll('[data-anim]').forEach(el=>{el.style.opacity='1';el.style.transform='none'});
document.querySelectorAll('[data-animate="pipeline"] [data-anim]').forEach(el=>el.style.opacity='1');
}
}
if(motion){
const { animate, stagger } = motion;
const EASE = [.22, 1, .36, 1];
const slides = [...document.querySelectorAll('.slide')];
let pipeStep = -1;
let lastIdx = -1;
function resetAnims(slide){
slide.querySelectorAll('[data-anim]').forEach(el=>{
el.style.opacity='';
el.style.transform='';
});
}
function playSlide(i){
const slide = slides[i];
if(!slide) return;
lastIdx = i;
const recipe = slide.dataset.animate || (slide.classList.contains('hero') ? 'hero' : 'cascade');
if(recipe === 'pipeline'){
pipeStep = -1;
slide.querySelectorAll('[data-anim]').forEach(el=>{
el.style.opacity='0.15';
el.style.transform='none';
});
return;
}
resetAnims(slide);
const all = [...slide.querySelectorAll('[data-anim]')];
if(!all.length) return;
if(recipe === 'directional'){
const lefts = all.filter(el=>el.dataset.anim==='left');
const divs = all.filter(el=>el.dataset.anim==='divider');
const rights = all.filter(el=>el.dataset.anim==='right');
const others = all.filter(el=>!['left','right','divider'].includes(el.dataset.anim));
if(others.length) animate(others, {opacity:[0,1], y:[12,0]}, {duration:.6, delay:stagger(.1, {start:.15}), easing:EASE});
if(lefts.length) animate(lefts, {opacity:[0,1], x:[-24,0]}, {duration:.8, delay:.35, easing:EASE});
if(divs.length) animate(divs, {opacity:[0,.25]}, {duration:.5, delay:.9});
if(rights.length) animate(rights, {opacity:[0,1], x:[24,0]}, {duration:.8, delay:1.0, easing:EASE});
return;
}
if(recipe === 'quote'){
const lines = all.filter(el=>el.dataset.anim==='line');
const others = all.filter(el=>el.dataset.anim!=='line');
if(others.length) animate(others, {opacity:[0,1], y:[8,0]}, {duration:.6, delay:stagger(.12, {start:.2}), easing:EASE});
if(lines.length) animate(lines, {opacity:[.35,1], y:[10,0]}, {duration:.8, delay:stagger(.55, {start:.5}), easing:EASE});
return;
}
if(recipe === 'hero'){
animate(all, {opacity:[0,1], y:[14,0]}, {duration:.9, delay:stagger(.16, {start:.2}), easing:EASE});
return;
}
// default: cascade
animate(all, {opacity:[0,1], y:[16,0]}, {duration:.75, delay:stagger(.1, {start:.15}), easing:EASE});
}
function pipeAdvance(){
const slide = slides[lastIdx];
if(!slide || slide.dataset.animate !== 'pipeline') return false;
const steps = [...slide.querySelectorAll('[data-anim="step"]')];
const arrows = [...slide.querySelectorAll('[data-anim="arrow"]')];
if(pipeStep >= steps.length - 1) return false;
pipeStep++;
animate(steps[pipeStep], {opacity:[0.15,1], y:[8,0]}, {duration:.5, easing:EASE});
if(pipeStep > 0 && arrows[pipeStep-1]){
animate(arrows[pipeStep-1], {opacity:[0.15,.7]}, {duration:.3, delay:.15});
}
return true;
}
window.__playSlide = playSlide;
window.__pipeAdvance = pipeAdvance;
// 首屏:go(0) 已跑过(同步),没来得及触发动效 → 这里补一下
playSlide(0);
}
</script>
</body>
</html>