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:
1 parent
a593cf4b73
commit
e09f931308
6 files changed
+337
-69
No files matched your search
Vendored
+8
@@ -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
@@ -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>
|
||||
Reference in new issue
Block a user