Compare commits

...

23 Commits

Author SHA1 Message Date
2d87b7bdfd fix: hero 字母不可见 (渐变不继承) + 升级 text.splitText API
- .hero-letter 加自己的渐变 + -webkit-background-clip:text
  原因: 父 h1 的 -webkit-text-fill-color:transparent 继承到 span 但
  background 不继承,span 变成 box 就切断 background-clip:text 的连贯,
  span 里文字就全透明看不见

- text.split(...) → text.splitText(...)
  修掉 v4.3 的 deprecation warning

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 01:18:57 +08:00
aa67005534 auto-save 2026-04-24 01:17 (~1) 2026-04-24 01:17:57 +08:00
d24821c287 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>
2026-04-24 01:16:46 +08:00
71ad32f946 auto-save 2026-04-24 01:12 (+1, ~2) 2026-04-24 01:12:29 +08:00
47c45e36dd chore: gitignore playwright QA 产物
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 01:10:11 +08:00
b75961e112 fix: grid minmax 260→220 避免 1200 viewport 下卡片溢出
1200 宽度下 main padding 缩到 2.5rem(media query),可用宽度 840px。
3 × minmax(260, 1fr) + 2 gap = 872 超出 32px。
改成 minmax(220, 1fr),3 × 220 + 32 = 692 < 840,留足余量。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 01:07:32 +08:00
e034d29e02 auto-save 2026-04-24 01:06 (~1) 2026-04-24 01:07:02 +08:00
738b0db877 auto-save 2026-04-24 01:01 (~1) 2026-04-24 01:01:33 +08:00
5929a64d99 auto-save 2026-04-24 00:55 (~1) 2026-04-24 00:55:55 +08:00
b2773fdfb7 auto-save 2026-04-24 00:50 (~1) 2026-04-24 00:50:25 +08:00
db4b89f8b7 auto-save 2026-04-24 00:44 (~1) 2026-04-24 00:44:58 +08:00
2ef5d48f76 auto-save 2026-04-24 00:39 (~1) 2026-04-24 00:39:30 +08:00
7bdfc38b8d auto-save 2026-04-24 00:33 (~1) 2026-04-24 00:34:03 +08:00
599a9935d6 auto-save 2026-04-24 00:28 (~1) 2026-04-24 00:28:32 +08:00
ce748599c3 auto-save 2026-04-24 00:22 (~1) 2026-04-24 00:22:53 +08:00
acfe0f4964 auto-save 2026-04-24 00:16 (~1) 2026-04-24 00:17:03 +08:00
aa631de718 auto-save 2026-04-24 00:11 (~1) 2026-04-24 00:11:13 +08:00
7d33dc7e1a auto-save 2026-04-24 00:05 (~1) 2026-04-24 00:05:23 +08:00
aee4fe9c78 auto-save 2026-04-23 23:59 (~1) 2026-04-23 23:59:56 +08:00
686add8dc3 auto-save 2026-04-23 23:54 (~1) 2026-04-23 23:54:28 +08:00
e130066c5f auto-save 2026-04-23 23:48 (~1) 2026-04-23 23:49:00 +08:00
9ca7d3f0e6 auto-save 2026-04-23 23:43 (~1) 2026-04-23 23:43:33 +08:00
bd7b463acd auto-save 2026-04-23 23:35 (~1) 2026-04-23 23:38:04 +08:00
5 changed files with 702 additions and 3 deletions

2
.gitignore vendored
View File

@@ -11,3 +11,5 @@ __pycache__/
.idea/
*.log
source/
.playwright-mcp/
qa-*.png

View File

@@ -62,6 +62,167 @@
"message": "chore: gitignore source/(避免 submodule准备部署",
"hash": "e11c0d5",
"files_changed": 3
},
{
"ts": "2026-04-23T23:30:25+08:00",
"type": "commit",
"message": "auto-save 2026-04-23 23:30 (~1)",
"hash": "65cf418",
"files_changed": 1
},
{
"ts": "2026-04-23T23:31:10+08:00",
"type": "commit",
"message": "feat: Dockerfile (nginx:alpine) 用于 Coolify 部署",
"hash": "8183cae",
"files_changed": 1
},
{
"ts": "2026-04-23T23:38:04+08:00",
"type": "commit",
"message": "auto-save 2026-04-23 23:35 (~1)",
"hash": "bd7b463",
"files_changed": 1
},
{
"ts": "2026-04-23T23:43:33+08:00",
"type": "commit",
"message": "auto-save 2026-04-23 23:43 (~1)",
"hash": "9ca7d3f",
"files_changed": 1
},
{
"ts": "2026-04-23T23:49:00+08:00",
"type": "commit",
"message": "auto-save 2026-04-23 23:48 (~1)",
"hash": "e130066",
"files_changed": 1
},
{
"ts": "2026-04-23T23:54:28+08:00",
"type": "commit",
"message": "auto-save 2026-04-23 23:54 (~1)",
"hash": "686add8",
"files_changed": 1
},
{
"ts": "2026-04-23T23:59:56+08:00",
"type": "commit",
"message": "auto-save 2026-04-23 23:59 (~1)",
"hash": "aee4fe9",
"files_changed": 1
},
{
"ts": "2026-04-24T00:05:23+08:00",
"type": "commit",
"message": "auto-save 2026-04-24 00:05 (~1)",
"hash": "7d33dc7",
"files_changed": 1
},
{
"ts": "2026-04-24T00:11:13+08:00",
"type": "commit",
"message": "auto-save 2026-04-24 00:11 (~1)",
"hash": "aa631de",
"files_changed": 1
},
{
"ts": "2026-04-24T00:17:03+08:00",
"type": "commit",
"message": "auto-save 2026-04-24 00:16 (~1)",
"hash": "acfe0f4",
"files_changed": 1
},
{
"ts": "2026-04-24T00:22:53+08:00",
"type": "commit",
"message": "auto-save 2026-04-24 00:22 (~1)",
"hash": "ce74859",
"files_changed": 1
},
{
"ts": "2026-04-24T00:28:32+08:00",
"type": "commit",
"message": "auto-save 2026-04-24 00:28 (~1)",
"hash": "599a993",
"files_changed": 1
},
{
"ts": "2026-04-24T00:34:03+08:00",
"type": "commit",
"message": "auto-save 2026-04-24 00:33 (~1)",
"hash": "7bdfc38",
"files_changed": 1
},
{
"ts": "2026-04-24T00:39:30+08:00",
"type": "commit",
"message": "auto-save 2026-04-24 00:39 (~1)",
"hash": "2ef5d48",
"files_changed": 1
},
{
"ts": "2026-04-24T00:44:58+08:00",
"type": "commit",
"message": "auto-save 2026-04-24 00:44 (~1)",
"hash": "db4b89f",
"files_changed": 1
},
{
"ts": "2026-04-24T00:50:25+08:00",
"type": "commit",
"message": "auto-save 2026-04-24 00:50 (~1)",
"hash": "b2773fd",
"files_changed": 1
},
{
"ts": "2026-04-24T00:55:55+08:00",
"type": "commit",
"message": "auto-save 2026-04-24 00:55 (~1)",
"hash": "5929a64",
"files_changed": 1
},
{
"ts": "2026-04-24T01:01:33+08:00",
"type": "commit",
"message": "auto-save 2026-04-24 01:01 (~1)",
"hash": "738b0db",
"files_changed": 1
},
{
"ts": "2026-04-24T01:07:02+08:00",
"type": "commit",
"message": "auto-save 2026-04-24 01:06 (~1)",
"hash": "e034d29",
"files_changed": 1
},
{
"ts": "2026-04-24T01:07:32+08:00",
"type": "commit",
"message": "fix: grid minmax 260→220 避免 1200 viewport 下卡片溢出",
"hash": "b75961e",
"files_changed": 1
},
{
"ts": "2026-04-24T01:10:11+08:00",
"type": "commit",
"message": "chore: gitignore playwright QA 产物",
"hash": "47c45e3",
"files_changed": 1
},
{
"ts": "2026-04-24T01:12:29+08:00",
"type": "commit",
"message": "auto-save 2026-04-24 01:12 (+1, ~2)",
"hash": "71ad32f",
"files_changed": 3
},
{
"ts": "2026-04-24T01:16:46+08:00",
"type": "commit",
"message": "feat: 加 8 个 Live Demo + hero 文字 stagger 动画",
"hash": "d24821c",
"files_changed": 1
}
]
}

View File

@@ -1,6 +1,7 @@
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/index.html
COPY anime.umd.min.js /usr/share/nginx/html/anime.umd.min.js
RUN printf 'server {\n\
listen 80;\n\

7
anime.umd.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -133,7 +133,7 @@
.card.warn{border-left:3px solid var(--yellow)}
.card h4{margin-top:0}
.grid{display:grid;gap:1rem;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));margin:1rem 0}
.grid{display:grid;gap:1rem;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));margin:1rem 0}
.tldr{
background:linear-gradient(135deg,#2a1f1a,#1a2a26);
@@ -158,6 +158,130 @@
}
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 —— 每个字母自带渐变剪影(父级 -webkit-text-fill-color:transparent 不继承背景,这里补回) */
h1 .hero-letter{
display:inline-block;will-change:transform,opacity,color;
background:linear-gradient(135deg,var(--accent) 0%,var(--accent3) 55%,var(--accent2) 100%);
-webkit-background-clip:text;background-clip:text;
-webkit-text-fill-color:transparent;
}
/* 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 +330,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 +779,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 +849,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 用 &lt; += 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 +898,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 +932,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 +1053,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 +1100,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 +1174,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>这个技巧在 LayoutL386-L392和 ScrollL774-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 +1417,278 @@ stickys.<span class="fn">forEach</span>(s =&gt; 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 buttonblend 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.splitText) {
splitInstance = text.splitText(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>