feat: 加 8 个 Live Demo + hero 文字 stagger 动画
- hero: 逐字浮现 + elastic - stagger grid 15x8 120 点: center/first/random/wave 循环 - animatable 鼠标跟随: 3 种 easing 对比 - additive blend: idle wobble + click shake 不打架 - SVG 三件套: morphTo(星↔心↔圆) + motionPath + drawable - text.split + stagger: 上移/波浪/故障态 - timeline 位置语法: < += label 三 box 演示 - draggable: 容器边界 + spring + snap Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
527
index.html
527
index.html
@@ -158,6 +158,125 @@
|
||||
}
|
||||
footer a{color:var(--accent);text-decoration:none}
|
||||
footer a:hover{text-decoration:underline}
|
||||
|
||||
/* ==== Live Demo 样式 ==== */
|
||||
.demo{
|
||||
background:linear-gradient(135deg,#121215,#181820);
|
||||
border:1px solid var(--border);border-radius:10px;
|
||||
margin:1.5rem 0;overflow:hidden;
|
||||
}
|
||||
.demo-head{
|
||||
display:flex;align-items:center;justify-content:space-between;
|
||||
padding:.7rem 1rem;border-bottom:1px solid var(--border);
|
||||
background:#0f0f13;
|
||||
}
|
||||
.demo-title{font-size:.82rem;color:var(--accent);font-weight:600;letter-spacing:.02em}
|
||||
.demo-title::before{content:"▶ ";color:var(--accent2)}
|
||||
.demo-btns{display:flex;gap:.4rem}
|
||||
.demo-btn{
|
||||
background:#26262d;color:var(--fg);border:1px solid var(--border);
|
||||
padding:.3rem .75rem;border-radius:5px;font-size:.78rem;
|
||||
font-family:var(--mono);cursor:pointer;transition:all .15s;
|
||||
}
|
||||
.demo-btn:hover{background:var(--accent);color:#1a1a1e;border-color:var(--accent)}
|
||||
.demo-btn.primary{background:var(--accent);color:#1a1a1e;border-color:var(--accent)}
|
||||
.demo-btn.primary:hover{background:var(--accent3);border-color:var(--accent3)}
|
||||
.demo-stage{
|
||||
padding:1.25rem;min-height:180px;display:flex;
|
||||
align-items:center;justify-content:center;
|
||||
position:relative;overflow:hidden;
|
||||
}
|
||||
.demo-stage.tall{min-height:260px}
|
||||
|
||||
/* stagger grid */
|
||||
.stagger-grid{display:grid;grid-template-columns:repeat(15,1fr);gap:6px;max-width:480px;width:100%}
|
||||
.stagger-grid > div{
|
||||
aspect-ratio:1;background:var(--accent);border-radius:50%;
|
||||
opacity:.3;will-change:transform,opacity;
|
||||
}
|
||||
|
||||
/* timeline demo */
|
||||
.tl-row{display:flex;gap:1rem;width:100%;max-width:520px;justify-content:space-around}
|
||||
.tl-row .box{
|
||||
width:60px;height:60px;border-radius:10px;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
font-family:var(--mono);font-size:.85rem;font-weight:700;color:#1a1a1e;
|
||||
}
|
||||
.tl-box-a{background:var(--accent)}
|
||||
.tl-box-b{background:var(--accent2)}
|
||||
.tl-box-c{background:var(--accent3)}
|
||||
|
||||
/* additive demo */
|
||||
.additive-stage{display:flex;justify-content:center;align-items:center;gap:2rem}
|
||||
.additive-btn{
|
||||
width:120px;height:120px;border-radius:20px;background:var(--accent3);
|
||||
border:0;cursor:pointer;color:#1a1a1e;font-weight:700;font-size:.9rem;
|
||||
will-change:transform;
|
||||
}
|
||||
.additive-hint{color:var(--muted);font-size:.82rem;font-family:var(--mono);line-height:1.5}
|
||||
|
||||
/* SVG demos */
|
||||
.svg-row{display:flex;gap:1rem;flex-wrap:wrap;justify-content:center;width:100%}
|
||||
.svg-card{
|
||||
flex:1;min-width:170px;max-width:240px;
|
||||
background:#0d0d10;border:1px solid var(--border);border-radius:8px;
|
||||
padding:1rem;text-align:center;
|
||||
}
|
||||
.svg-card svg{display:block;margin:0 auto}
|
||||
.svg-card-label{
|
||||
font-size:.72rem;color:var(--accent2);font-family:var(--mono);
|
||||
margin-top:.5rem;letter-spacing:.04em;
|
||||
}
|
||||
|
||||
/* text split */
|
||||
.text-demo-target{
|
||||
font-size:2rem;font-weight:700;line-height:1.3;
|
||||
background:linear-gradient(135deg,var(--accent),var(--accent3),var(--accent2));
|
||||
-webkit-background-clip:text;-webkit-text-fill-color:transparent;
|
||||
text-align:center;
|
||||
}
|
||||
.text-demo-target .letter{display:inline-block;will-change:transform,opacity}
|
||||
|
||||
/* draggable */
|
||||
.drag-stage{position:relative;height:220px;background:#0d0d10;border:1px dashed #333;border-radius:8px;width:100%}
|
||||
.drag-box{
|
||||
position:absolute;top:50%;left:50%;margin:-40px 0 0 -40px;
|
||||
width:80px;height:80px;background:var(--accent);border-radius:12px;
|
||||
cursor:grab;display:flex;align-items:center;justify-content:center;
|
||||
color:#1a1a1e;font-weight:700;user-select:none;
|
||||
}
|
||||
.drag-box:active{cursor:grabbing}
|
||||
|
||||
/* animatable mouse-follow */
|
||||
.follow-stage{position:relative;height:220px;background:#0d0d10;border:1px solid var(--border);border-radius:8px;width:100%;cursor:crosshair}
|
||||
.follow-dot{
|
||||
position:absolute;top:0;left:0;width:28px;height:28px;
|
||||
margin:-14px 0 0 -14px;background:var(--accent2);border-radius:50%;
|
||||
pointer-events:none;box-shadow:0 0 20px var(--accent2);
|
||||
}
|
||||
.follow-hint{
|
||||
position:absolute;inset:auto 1rem 1rem auto;font-size:.75rem;
|
||||
color:var(--muted);font-family:var(--mono);
|
||||
}
|
||||
|
||||
/* engine basic translate */
|
||||
.basic-row{display:flex;gap:.5rem;align-items:center;justify-content:center;width:100%}
|
||||
.basic-row .dot{
|
||||
width:50px;height:50px;border-radius:50%;background:var(--accent);
|
||||
will-change:transform;
|
||||
}
|
||||
|
||||
/* hero animated logo */
|
||||
h1 .hero-letter{display:inline-block;will-change:transform,opacity,color}
|
||||
|
||||
/* online badge */
|
||||
.live-badge{
|
||||
display:inline-block;background:var(--accent2);color:#0b0b0d;
|
||||
font-size:.7rem;font-weight:700;padding:.1rem .5rem;border-radius:999px;
|
||||
margin-left:.5rem;font-family:var(--mono);letter-spacing:.04em;
|
||||
animation:pulse 2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.6}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -206,8 +325,8 @@
|
||||
|
||||
<main>
|
||||
<header class="hero">
|
||||
<h1>anime.js v4.3.6 源码深度解析</h1>
|
||||
<p class="hero-sub">Julian Garnier 重写版动画引擎的 11,121 行内部拆解。覆盖 Engine / Tween 值系统 / Composition / Timeline / Draggable / Layout FLIP / Scroll 全栈。</p>
|
||||
<h1 id="heroTitle">anime.js v4.3.6 源码深度解析<span class="live-badge">LIVE DEMOS</span></h1>
|
||||
<p class="hero-sub">Julian Garnier 重写版动画引擎的 11,121 行内部拆解。覆盖 Engine / Tween 值系统 / Composition / Timeline / Draggable / Layout FLIP / Scroll 全栈。<strong style="color:var(--accent2)">每个核心章节嵌入 live demo —— 边看代码边看效果。</strong></p>
|
||||
<div class="badges">
|
||||
<span class="badge">上游 <strong>juliangarnier/anime</strong></span>
|
||||
<span class="badge">版本 <strong>v4.3.6</strong></span>
|
||||
@@ -655,6 +774,22 @@ lookupTween._fromNumber = toNumber;</code></pre>
|
||||
<h4>典型场景</h4>
|
||||
<p>鼠标 hover → scale up(+0.1)<br>持续 idle wobble → scale ±0.05<br>点击 → shake scale ±0.2<br>三个动画同时生效时,scale 自动累加,而非互相覆盖。这正是 Animatable 内部用 blend 的原因。</p>
|
||||
</div>
|
||||
|
||||
<div class="demo">
|
||||
<div class="demo-head">
|
||||
<div class="demo-title">blend composition — idle wobble + click shake 叠加不打架</div>
|
||||
<div class="demo-btns">
|
||||
<button class="demo-btn primary" data-demo="additive-shake">点击按钮触发 shake</button>
|
||||
<button class="demo-btn" data-demo="additive-toggle">开关持续抖动</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="demo-stage tall">
|
||||
<button class="additive-btn" id="additiveBtn">CLICK ME</button>
|
||||
<div class="additive-hint" style="margin-left:2rem">
|
||||
按钮同时在跑<br>· scale idle wobble<br>· rotate idle wobble<br>点击时 shake 叠加不干扰
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Timeline -->
|
||||
@@ -709,6 +844,24 @@ lookupTween._fromNumber = toNumber;</code></pre>
|
||||
}, position);
|
||||
}</code></pre>
|
||||
<p><a class="ref">timeline.js:L256-265</a>。用补间外部对象 <code>currentTime</code> 的方式,把 WAAPI Animation 驱动进 anime Timeline。非常机智的统一。</p>
|
||||
|
||||
<div class="demo">
|
||||
<div class="demo-head">
|
||||
<div class="demo-title">Timeline 位置语法 — 三个 box 用 < += label 衔接</div>
|
||||
<div class="demo-btns">
|
||||
<button class="demo-btn primary" data-demo="tl-play">▶ 播放</button>
|
||||
<button class="demo-btn" data-demo="tl-restart">从头重播</button>
|
||||
<button class="demo-btn" data-demo="tl-reverse">反向</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="demo-stage tall">
|
||||
<div class="tl-row">
|
||||
<div class="box tl-box-a" id="tlBoxA">A</div>
|
||||
<div class="box tl-box-b" id="tlBoxB">B</div>
|
||||
<div class="box tl-box-c" id="tlBoxC">C</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr>
|
||||
@@ -740,6 +893,21 @@ lookupTween._fromNumber = toNumber;</code></pre>
|
||||
<span class="k">if</span> (axis === <span class="s">'x'</span>) value = -distanceX; <span class="c">// 单轴模式</span>
|
||||
<span class="k">if</span> (axis === <span class="s">'y'</span>) value = -distanceY;</code></pre>
|
||||
<p><a class="ref">utils/stagger.js:L117-126</a>。values 数组懒初始化 + 缓存,第一次调用时算完整个距离 map。</p>
|
||||
|
||||
<div class="demo">
|
||||
<div class="demo-head">
|
||||
<div class="demo-title">stagger({ grid: [15,8], from: 'center' }) 波纹</div>
|
||||
<div class="demo-btns">
|
||||
<button class="demo-btn" data-demo="stagger-center">从中心</button>
|
||||
<button class="demo-btn" data-demo="stagger-first">从第一个</button>
|
||||
<button class="demo-btn" data-demo="stagger-random">随机</button>
|
||||
<button class="demo-btn primary" data-demo="stagger-wave">波纹循环</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="demo-stage tall">
|
||||
<div class="stagger-grid" id="staggerGrid"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Animatable -->
|
||||
@@ -759,6 +927,23 @@ document.<span class="fn">addEventListener</span>(<span class="s">'mousemove'</s
|
||||
</ul>
|
||||
|
||||
<p>配合 blend composition,可实现"多输入源推同一对象,平滑叠加"—— 典型如粒子系统有重力、鼠标吸引、风力三种力同时作用。</p>
|
||||
|
||||
<div class="demo">
|
||||
<div class="demo-head">
|
||||
<div class="demo-title">createAnimatable 鼠标跟随 — 不同 easing 对比</div>
|
||||
<div class="demo-btns">
|
||||
<button class="demo-btn" data-demo="follow-linear">linear</button>
|
||||
<button class="demo-btn primary" data-demo="follow-elastic">outElastic</button>
|
||||
<button class="demo-btn" data-demo="follow-quad">outQuad</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="demo-stage tall">
|
||||
<div class="follow-stage" id="followStage">
|
||||
<div class="follow-dot" id="followDot"></div>
|
||||
<div class="follow-hint">↑ 在区域内移动鼠标</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Scope -->
|
||||
@@ -863,6 +1048,41 @@ document.<span class="fn">addEventListener</span>(<span class="s">'mousemove'</s
|
||||
}
|
||||
});</code></pre>
|
||||
<p><a class="ref">svg/drawable.js:L54-95</a>。<strong>精妙之处</strong>:不引入新 CSS 属性,而是用 Proxy 拦截 <code>setAttribute('draw', '0 0.5')</code>,转译成 <code>stroke-dasharray</code> + <code>stroke-dashoffset</code>。且强行设置 <code>pathLength=1000</code>(L47, L96-97),让 [0, 1000] 成为规范化空间 —— 不管 path 真实长度多少,画线 API 一致。</p>
|
||||
|
||||
<div class="demo">
|
||||
<div class="demo-head">
|
||||
<div class="demo-title">SVG 三件套同台 — morphTo / motionPath / drawable</div>
|
||||
<div class="demo-btns">
|
||||
<button class="demo-btn primary" data-demo="svg-run">▶ 运行全部</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="demo-stage tall" style="padding:1.5rem 1rem">
|
||||
<div class="svg-row">
|
||||
<div class="svg-card">
|
||||
<svg width="180" height="180" viewBox="0 0 100 100">
|
||||
<path id="morphPath" d="M 50 10 L 70 40 L 90 50 L 70 60 L 50 90 L 30 60 L 10 50 L 30 40 Z"
|
||||
fill="none" stroke="#ff8a5b" stroke-width="2.5" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<div class="svg-card-label">morphTo ← 星/心/圆</div>
|
||||
</div>
|
||||
<div class="svg-card">
|
||||
<svg width="180" height="180" viewBox="0 0 100 100">
|
||||
<path id="motionTrack" d="M 15 50 Q 35 10, 50 50 T 85 50"
|
||||
fill="none" stroke="#2a2a30" stroke-width="1" stroke-dasharray="2 2"/>
|
||||
<circle id="motionBall" cx="0" cy="0" r="6" fill="#5ed1b7"/>
|
||||
</svg>
|
||||
<div class="svg-card-label">motionPath 沿曲线跑</div>
|
||||
</div>
|
||||
<div class="svg-card">
|
||||
<svg width="180" height="180" viewBox="0 0 100 100">
|
||||
<path id="drawablePath" d="M 20 80 Q 20 20 50 20 Q 80 20 80 50 Q 80 80 50 80 L 20 80"
|
||||
fill="none" stroke="#c792ea" stroke-width="3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<div class="svg-card-label">drawable 画线 0→1</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Text -->
|
||||
@@ -875,6 +1095,20 @@ document.<span class="fn">addEventListener</span>(<span class="s">'mousemove'</s
|
||||
<li><strong>Line 重排</strong>:换行逻辑用 <code>filterLineElements</code> 递归剔除不属于此行的元素 + 相邻空白 textNode(避免孤零零残留)。<a class="ref">split.js:L109-126</a></li>
|
||||
<li><strong>模板占位</strong>:<code>{value}</code> / <code>{i}</code> 支持用户自定义包装 HTML。<a class="ref">split.js:L42-43</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="demo">
|
||||
<div class="demo-head">
|
||||
<div class="demo-title">text.split + stagger — 文字逐字浮现</div>
|
||||
<div class="demo-btns">
|
||||
<button class="demo-btn" data-demo="text-up">上移进场</button>
|
||||
<button class="demo-btn" data-demo="text-wave">波浪</button>
|
||||
<button class="demo-btn primary" data-demo="text-glitch">故障态</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="demo-stage tall">
|
||||
<div class="text-demo-target" id="textTarget">anime.js · 每一个字都会动</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- WAAPI -->
|
||||
@@ -935,6 +1169,22 @@ velocity = <span class="fn">max</span>(...velocityStack);</code></pre>
|
||||
|
||||
<h3>临时清空祖先 transform 测量</h3>
|
||||
<p><a class="ref">draggable.js:L593</a>:<code>transforms.remove()</code> 临时清除父元素 transform 才能用 <code>getBoundingClientRect</code> 拿到准确边界,然后再 revert。<strong>这个技巧在 Layout(L386-L392)和 Scroll(L774-L781)里也重复使用</strong>。</p>
|
||||
|
||||
<div class="demo">
|
||||
<div class="demo-head">
|
||||
<div class="demo-title">createDraggable — 拖我、撞墙会回弹</div>
|
||||
<div class="demo-btns">
|
||||
<button class="demo-btn" data-demo="drag-reset">回中心</button>
|
||||
<button class="demo-btn primary" data-demo="drag-release-spring">spring 回弹</button>
|
||||
<button class="demo-btn" data-demo="drag-snap">开启 50px snap</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="demo-stage tall">
|
||||
<div class="drag-stage" id="dragStage">
|
||||
<div class="drag-box" id="dragBox">拖我</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Layout -->
|
||||
@@ -1162,5 +1412,278 @@ stickys.<span class="fn">forEach</span>(s => s.<span class="fn">revert</span>
|
||||
}, { rootMargin: '-20% 0px -70% 0px' });
|
||||
sections.forEach(s => io.observe(s));
|
||||
</script>
|
||||
|
||||
<!-- ========= anime.js 运行时 + Live Demos ========= -->
|
||||
<script src="./anime.umd.min.js"></script>
|
||||
<script>
|
||||
(() => {
|
||||
const A = window.anime;
|
||||
if (!A) { console.error('anime.js 未加载'); return; }
|
||||
const { animate, createTimeline, createDraggable, createAnimatable, stagger, svg, text, utils } = A;
|
||||
|
||||
// ---- 1. Hero 文字动画:每字逐个浮现 + 颜色流转 ----
|
||||
const heroTitle = document.getElementById('heroTitle');
|
||||
if (heroTitle) {
|
||||
const badge = heroTitle.querySelector('.live-badge');
|
||||
badge && badge.remove();
|
||||
const raw = heroTitle.textContent;
|
||||
heroTitle.textContent = '';
|
||||
raw.split('').forEach((ch, i) => {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'hero-letter';
|
||||
span.textContent = ch === ' ' ? ' ' : ch;
|
||||
heroTitle.appendChild(span);
|
||||
});
|
||||
badge && heroTitle.appendChild(badge);
|
||||
animate('.hero-letter', {
|
||||
translateY: [
|
||||
{ from: 40, to: -6, ease: 'outQuad' },
|
||||
{ to: 0, ease: 'outElastic(1, .5)' }
|
||||
],
|
||||
opacity: [{ from: 0, to: 1, ease: 'outQuad' }],
|
||||
duration: 900,
|
||||
delay: stagger(40),
|
||||
});
|
||||
}
|
||||
|
||||
// ---- 2. Stagger Grid 波纹 ----
|
||||
const gridEl = document.getElementById('staggerGrid');
|
||||
if (gridEl) {
|
||||
const cols = 15, rows = 8;
|
||||
for (let i = 0; i < cols * rows; i++) {
|
||||
gridEl.appendChild(document.createElement('div'));
|
||||
}
|
||||
const dots = gridEl.children;
|
||||
let waveLoop = null;
|
||||
function stopWave() { if (waveLoop) { waveLoop.cancel(); waveLoop = null; } }
|
||||
function staggerFrom(from) {
|
||||
stopWave();
|
||||
animate(dots, {
|
||||
scale: [{ from: 1.6, to: 1 }],
|
||||
opacity: [{ from: 1, to: 0.35 }],
|
||||
duration: 900,
|
||||
delay: stagger(45, { grid: [cols, rows], from }),
|
||||
ease: 'outQuad',
|
||||
});
|
||||
}
|
||||
function waveLoopStart() {
|
||||
stopWave();
|
||||
waveLoop = animate(dots, {
|
||||
scale: [{ from: 1, to: 1.6 }, { to: 1 }],
|
||||
opacity: [{ from: 0.3, to: 1 }, { to: 0.3 }],
|
||||
duration: 1400,
|
||||
delay: stagger(50, { grid: [cols, rows], from: 'center' }),
|
||||
loop: true,
|
||||
ease: 'inOutSine',
|
||||
});
|
||||
}
|
||||
// initial
|
||||
waveLoopStart();
|
||||
document.querySelectorAll('[data-demo^="stagger-"]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const act = btn.dataset.demo;
|
||||
if (act === 'stagger-center') staggerFrom('center');
|
||||
if (act === 'stagger-first') staggerFrom('first');
|
||||
if (act === 'stagger-random') staggerFrom('random');
|
||||
if (act === 'stagger-wave') waveLoopStart();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ---- 3. Animatable 鼠标跟随 ----
|
||||
const followStage = document.getElementById('followStage');
|
||||
const followDot = document.getElementById('followDot');
|
||||
if (followStage && followDot) {
|
||||
let dot = createAnimatable(followDot, { x: 200, y: 100, ease: 'outElastic(1, .5)', duration: 600 });
|
||||
let currentEase = 'outElastic(1, .5)';
|
||||
followStage.addEventListener('mousemove', e => {
|
||||
const r = followStage.getBoundingClientRect();
|
||||
dot.x(e.clientX - r.left).y(e.clientY - r.top);
|
||||
});
|
||||
document.querySelectorAll('[data-demo^="follow-"]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const ease = btn.dataset.demo === 'follow-linear' ? 'linear'
|
||||
: btn.dataset.demo === 'follow-elastic' ? 'outElastic(1, .5)'
|
||||
: 'outQuad';
|
||||
dot.revert();
|
||||
dot = createAnimatable(followDot, { x: 200, y: 100, ease, duration: ease === 'linear' ? 200 : 600 });
|
||||
currentEase = ease;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ---- 4. Additive button(blend composition)----
|
||||
const addBtn = document.getElementById('additiveBtn');
|
||||
if (addBtn) {
|
||||
// idle 持续抖动(blend)
|
||||
animate(addBtn, {
|
||||
scale: [{ from: 0.96, to: 1.04 }, { to: 0.96 }],
|
||||
duration: 1400, loop: true, ease: 'inOutSine', composition: 'blend',
|
||||
});
|
||||
animate(addBtn, {
|
||||
rotate: [{ from: -2, to: 2 }, { to: -2 }],
|
||||
duration: 1800, loop: true, ease: 'inOutSine', composition: 'blend',
|
||||
});
|
||||
addBtn.addEventListener('click', () => {
|
||||
// shake 叠加(blend)不会干扰 idle
|
||||
animate(addBtn, {
|
||||
translateX: [
|
||||
{ to: 10 }, { to: -10 }, { to: 6 }, { to: -6 }, { to: 0 }
|
||||
],
|
||||
duration: 400, ease: 'outQuad', composition: 'blend',
|
||||
});
|
||||
animate(addBtn, {
|
||||
scale: [{ to: 1.15 }, { to: 1 }],
|
||||
duration: 500, ease: 'outElastic(1, .4)', composition: 'blend',
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('[data-demo^="additive-"]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
if (btn.dataset.demo === 'additive-shake') addBtn.click();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ---- 5. SVG 三件套 ----
|
||||
const morphPath = document.getElementById('morphPath');
|
||||
const motionBall = document.getElementById('motionBall');
|
||||
const motionTrack = document.getElementById('motionTrack');
|
||||
const drawPath = document.getElementById('drawablePath');
|
||||
const shapes = {
|
||||
star: 'M 50 10 L 70 40 L 90 50 L 70 60 L 50 90 L 30 60 L 10 50 L 30 40 Z',
|
||||
heart: 'M 50 80 C 20 60 10 30 30 20 C 40 15 48 25 50 30 C 52 25 60 15 70 20 C 90 30 80 60 50 80 Z',
|
||||
circle: 'M 50 10 C 78 10 90 32 90 50 C 90 68 78 90 50 90 C 22 90 10 68 10 50 C 10 32 22 10 50 10 Z',
|
||||
};
|
||||
let drawEl = null;
|
||||
function runSvgDemo() {
|
||||
if (morphPath) {
|
||||
animate(morphPath, {
|
||||
d: [
|
||||
{ to: shapes.heart, duration: 1200 },
|
||||
{ to: shapes.circle, duration: 1200 },
|
||||
{ to: shapes.star, duration: 1200 },
|
||||
],
|
||||
ease: 'inOutQuad',
|
||||
loop: true,
|
||||
});
|
||||
}
|
||||
if (motionBall && motionTrack && svg && svg.createMotionPath) {
|
||||
const mp = svg.createMotionPath(motionTrack);
|
||||
animate(motionBall, {
|
||||
...mp,
|
||||
duration: 2500, ease: 'inOutSine', loop: true, alternate: true,
|
||||
});
|
||||
}
|
||||
if (drawPath && svg && svg.createDrawable) {
|
||||
if (!drawEl) drawEl = svg.createDrawable(drawPath)[0];
|
||||
animate(drawEl, {
|
||||
draw: ['0 0', '0 1', '1 1'],
|
||||
duration: 2600, ease: 'inOutQuad', loop: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
// 默认自动跑(首屏外也可以,因为不影响性能)
|
||||
runSvgDemo();
|
||||
document.querySelectorAll('[data-demo="svg-run"]').forEach(b => b.addEventListener('click', runSvgDemo));
|
||||
|
||||
// ---- 6. Text split 动画 ----
|
||||
const textTarget = document.getElementById('textTarget');
|
||||
let splitInstance = null;
|
||||
function ensureSplit() {
|
||||
if (splitInstance) splitInstance.revert && splitInstance.revert();
|
||||
if (text && text.split) {
|
||||
splitInstance = text.split(textTarget, { chars: { class: 'letter' } });
|
||||
}
|
||||
}
|
||||
function textDemo(kind) {
|
||||
ensureSplit();
|
||||
const letters = textTarget.querySelectorAll('.letter');
|
||||
if (!letters.length) return;
|
||||
if (kind === 'text-up') {
|
||||
animate(letters, {
|
||||
translateY: [{ from: '120%', to: 0 }],
|
||||
opacity: [{ from: 0, to: 1 }],
|
||||
duration: 900, ease: 'outExpo', delay: stagger(35),
|
||||
});
|
||||
} else if (kind === 'text-wave') {
|
||||
animate(letters, {
|
||||
translateY: [{ from: -12, to: 12 }, { to: 0 }],
|
||||
rotate: [{ from: -8, to: 8 }, { to: 0 }],
|
||||
duration: 1600, ease: 'inOutSine', loop: true,
|
||||
delay: stagger(60, { from: 'center' }),
|
||||
});
|
||||
} else if (kind === 'text-glitch') {
|
||||
animate(letters, {
|
||||
translateX: () => utils.random(-8, 8),
|
||||
translateY: () => utils.random(-8, 8),
|
||||
rotate: () => utils.random(-15, 15),
|
||||
opacity: [{ from: 0.3, to: 1 }, { to: 0.3 }, { to: 1 }],
|
||||
color: '#ffffff',
|
||||
duration: 1200, ease: 'outQuad', loop: true,
|
||||
delay: stagger(20, { from: 'random' }),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (textTarget) {
|
||||
textDemo('text-up');
|
||||
document.querySelectorAll('[data-demo^="text-"]').forEach(btn => {
|
||||
btn.addEventListener('click', () => textDemo(btn.dataset.demo));
|
||||
});
|
||||
}
|
||||
|
||||
// ---- 7. Timeline 位置语法 ----
|
||||
let tlInstance = null;
|
||||
function buildTl() {
|
||||
if (tlInstance) tlInstance.revert();
|
||||
tlInstance = createTimeline({ defaults: { duration: 600, ease: 'inOutQuad' }, autoplay: false });
|
||||
tlInstance
|
||||
.add('#tlBoxA', { translateX: [0, 120], translateY: [0, -20] })
|
||||
.add('#tlBoxB', { translateX: [0, 0], translateY: [0, -40], rotate: [0, 360] }, '<') // 与上一个同时开始
|
||||
.add('#tlBoxC', { translateX: [0, -120], scale: [1, 1.2] }, '+=100') // 延后 100ms
|
||||
.label('back')
|
||||
.add('#tlBoxA', { translateX: 0, translateY: 0 }, 'back')
|
||||
.add('#tlBoxB', { translateX: 0, translateY: 0, rotate: 0 }, 'back')
|
||||
.add('#tlBoxC', { translateX: 0, scale: 1 }, 'back');
|
||||
return tlInstance;
|
||||
}
|
||||
if (document.getElementById('tlBoxA')) {
|
||||
buildTl();
|
||||
document.querySelector('[data-demo="tl-play"]').addEventListener('click', () => buildTl().play());
|
||||
document.querySelector('[data-demo="tl-restart"]').addEventListener('click', () => buildTl().restart());
|
||||
document.querySelector('[data-demo="tl-reverse"]').addEventListener('click', () => {
|
||||
if (!tlInstance) buildTl();
|
||||
tlInstance.reverse();
|
||||
});
|
||||
}
|
||||
|
||||
// ---- 8. Draggable ----
|
||||
const dragBox = document.getElementById('dragBox');
|
||||
const dragStage = document.getElementById('dragStage');
|
||||
let draggable = null;
|
||||
function mountDraggable(params) {
|
||||
if (draggable) draggable.revert();
|
||||
draggable = createDraggable(dragBox, {
|
||||
container: dragStage,
|
||||
containerPadding: 6,
|
||||
releaseEase: 'outElastic(1, .5)',
|
||||
...params,
|
||||
});
|
||||
}
|
||||
if (dragBox) {
|
||||
mountDraggable({});
|
||||
document.querySelector('[data-demo="drag-reset"]').addEventListener('click', () => {
|
||||
draggable && draggable.animate.x(0).y(0);
|
||||
});
|
||||
document.querySelector('[data-demo="drag-release-spring"]').addEventListener('click', () => {
|
||||
mountDraggable({ releaseEase: A.createSpring ? A.createSpring({ stiffness: 80, damping: 10 }) : 'outElastic(1, .3)' });
|
||||
});
|
||||
document.querySelector('[data-demo="drag-snap"]').addEventListener('click', () => {
|
||||
mountDraggable({ snap: 50 });
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[demos] 已激活', Object.keys(A).filter(k => !k.startsWith('_')).length, '个 anime.js API');
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user