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
+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