robert 2 zile în urmă
părinte
comite
60f90b4d03

+ 108 - 66
doc/index.html

@@ -129,6 +129,18 @@
             background: var(--active-color);
         }
 
+        /* Flat menu section title */
+        .nav-section-title {
+            margin-top: 18px;
+            padding: 10px 0 6px 0;
+            color: #7a7a7a;
+            font-size: 11px;
+            font-weight: 800;
+            letter-spacing: 1px;
+            text-transform: uppercase;
+            opacity: 0.9;
+        }
+
         /* Search Box in sidebar */
         .sidebar-search {
             margin: 0 20px 20px 20px;
@@ -217,62 +229,21 @@
             Search
         </div>
         
-        <div class="nav-tree">
-            <!-- Group: Getting Started -->
-            <div class="tree-group">
-                <div class="tree-header" onclick="toggleTree(this)">Getting started</div>
-                <div class="tree-children">
-                    <div class="nav-item" onclick="selectCategory('search.html', 'test_css_selector.html', this)">Installation</div>
-                    <div class="nav-item" onclick="selectCategory('search.html', 'test_css_selector.html', this)">Module imports</div>
-                </div>
-            </div>
-
-            <!-- Group: Timer -->
-            <div class="tree-group">
-                <div class="tree-header" onclick="toggleTree(this)">Timer</div>
-                <div class="tree-children">
-                    <div class="nav-item">Settings</div>
-                </div>
-            </div>
-
-            <!-- Group: Animation -->
-            <div class="tree-group">
-                <div class="tree-header active-header" onclick="toggleTree(this)">Animation</div>
-                <div class="tree-children expanded">
-                    <div class="nav-item active" onclick="selectCategory('targets/list.html', 'targets/overview.html', this)">Targets</div>
-                    <div class="nav-item" onclick="selectCategory('animatable_properties/list.html', 'animatable_properties/test_css_props.html', this)">Animatable properties</div>
-                    <div class="nav-item" onclick="selectCategory('tween_value_types/list.html', 'tween_value_types/test_numerical.html', this)">Tween value types</div>
-            <div class="nav-item">Tween parameters</div>
-                    <div class="nav-item">Keyframes</div>
-                    <div class="nav-item">Playback settings</div>
-                    
-                    <!-- Callbacks Nested? Or separate? Reference shows Callbacks under Animation -->
-                    <div class="nav-item" onclick="selectCategory('list_callbacks.html', 'test_on_update.html', this)">Callbacks</div>
-                    
-                    <!-- Examples -->
-                    <div class="nav-item" onclick="selectCategory('examples/list.html', 'examples/controls.html', this)">Examples</div>
-                    
-                    <div class="nav-item">Methods</div>
-                    <div class="nav-item">Properties</div>
-                </div>
-            </div>
-            
-            <!-- Group: Timeline -->
-            <div class="tree-group">
-                <div class="tree-header" onclick="toggleTree(this)">Timeline</div>
-                <div class="tree-children">
-                    <div class="nav-item">Basics</div>
-                </div>
-            </div>
-            
-            <!-- Group: Animatable -->
-            <div class="tree-group">
-                <div class="tree-header" onclick="toggleTree(this)">Animatable</div>
-                <div class="tree-children">
-                    <div class="nav-item">Properties</div>
-                </div>
-            </div>
-
+        <div class="nav-tree" aria-label="主菜单(扁平化)">
+            <div class="nav-section-title">Animal.js 动画</div>
+            <div class="nav-item active" onclick="selectCategory('targets/list.html', 'targets/overview.html', this)">目标 Targets</div>
+            <div class="nav-item" onclick="selectCategory('animatable_properties/list.html', 'animatable_properties/test_transforms.html', this)">可动画属性 Properties</div>
+            <div class="nav-item" onclick="selectCategory('tween_value_types/list.html', 'tween_value_types/test_numerical.html', this)">补间值类型 Tween 值</div>
+            <div class="nav-item" onclick="selectCategory('tween_parameters/list.html', 'tween_parameters/overview.html', this)">补间参数 Tween 参数</div>
+            <div class="nav-item" onclick="selectCategory('svg/list.html', 'svg/overview.html', this)">SVG 动画</div>
+            <div class="nav-item" onclick="selectCategory('list_callbacks.html', 'test_on_update.html', this)">回调 Callbacks</div>
+            <div class="nav-item" onclick="selectCategory('examples/list.html', 'examples/controls.html', this)">示例 Examples</div>
+
+            <div class="nav-section-title">Layer 弹窗</div>
+            <div class="nav-item" onclick="selectCategory('examples/list.html', 'examples/layer.html', this)">交互示例(Layer + 动画)</div>
+
+            <div class="nav-section-title">开发者</div>
+            <div class="nav-item" onclick="selectCategory('search.html', 'module.html', this)">模块开发与测试指南</div>
         </div>
     </div>
 
@@ -292,16 +263,6 @@
     </svg>
 
     <script>
-        function toggleTree(header) {
-            const children = header.nextElementSibling;
-            if (children) {
-                children.classList.toggle('expanded');
-                
-                // Optional: Highlight header when expanded?
-                // header.classList.toggle('active-header');
-            }
-        }
-
         function selectCategory(listUrl, defaultContentUrl, el) {
             // 1. Highlight Nav Item
             document.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active'));
@@ -345,6 +306,87 @@
             scheduleConnectionLineUpdate();
         }
 
+        // =========================================================
+        // Global Prev/Next(组内顺序 + 跨组跳转,由路由表统一驱动)
+        // =========================================================
+        const DOC_PAGES = [
+            // Targets
+            { group: 'Targets', title: 'Targets 概览', url: 'targets/overview.html' },
+            { group: 'Targets', title: 'CSS Selector', url: 'targets/test_css_selector.html' },
+            { group: 'Targets', title: 'DOM Elements', url: 'targets/test_dom_elements.html' },
+            { group: 'Targets', title: 'JavaScript Objects', url: 'targets/test_js_objects.html' },
+            { group: 'Targets', title: 'Array of targets', url: 'targets/test_array_targets.html' },
+
+            // Animatable properties
+            { group: 'Properties', title: 'Transforms', url: 'animatable_properties/test_transforms.html' },
+            { group: 'Properties', title: 'CSS Properties', url: 'animatable_properties/test_css_props.html' },
+            { group: 'Properties', title: 'CSS Variables', url: 'animatable_properties/test_css_vars.html' },
+            { group: 'Properties', title: 'JS Properties', url: 'animatable_properties/test_js_props.html' },
+
+            // Tween value types
+            { group: 'Tween 值', title: 'Numerical', url: 'tween_value_types/test_numerical.html' },
+            { group: 'Tween 值', title: 'Unit', url: 'tween_value_types/test_unit.html' },
+            { group: 'Tween 值', title: 'Relative', url: 'tween_value_types/test_relative.html' },
+            { group: 'Tween 值', title: 'Colors', url: 'tween_value_types/test_colors.html' },
+
+            // Tween parameters
+            { group: 'Tween 参数', title: 'Tween parameters 概览', url: 'tween_parameters/overview.html' },
+            { group: 'Tween 参数', title: 'duration / delay', url: 'tween_parameters/test_duration_delay.html' },
+
+            // SVG
+            { group: 'SVG', title: 'SVG 概览', url: 'svg/overview.html' },
+            { group: 'SVG', title: 'draw() 路径描边', url: 'svg/test_draw_path.html' },
+            { group: 'SVG', title: 'Motion Path', url: 'svg/test_motion_path.html' },
+            { group: 'SVG', title: 'SVG 属性动画', url: 'svg/test_svg_attributes.html' },
+            { group: 'SVG', title: 'SVG transform', url: 'svg/test_svg_transforms.html' },
+
+            // Callbacks
+            { group: 'Callbacks', title: 'onUpdate', url: 'test_on_update.html' },
+
+            // Examples
+            { group: 'Examples', title: 'Controls', url: 'examples/controls.html' },
+            { group: 'Examples', title: 'Layer', url: 'examples/layer.html' },
+
+            // Developer
+            { group: 'Developer', title: '模块开发与测试指南', url: 'module.html' }
+        ];
+
+        function normalizeDocUrl(url) {
+            return String(url || '')
+                .replace(/^[#/]+/, '')
+                .replace(/^\.\//, '')
+                .replace(/^\/+/, '');
+        }
+
+        function docFindIndex(currentUrl) {
+            const u = normalizeDocUrl(currentUrl);
+            if (!u) return -1;
+            return DOC_PAGES.findIndex(p => u === p.url || u.endsWith(p.url));
+        }
+
+        function docGetPrevNext(currentUrl) {
+            const idx = docFindIndex(currentUrl);
+            if (idx < 0) return { idx: -1, prev: null, next: null, current: null };
+            const prev = (idx > 0) ? DOC_PAGES[idx - 1] : null;
+            const next = (idx < DOC_PAGES.length - 1) ? DOC_PAGES[idx + 1] : null;
+            return { idx, prev, next, current: DOC_PAGES[idx] };
+        }
+
+        function docNavigatePrev(currentUrl) {
+            const { prev } = docGetPrevNext(currentUrl);
+            if (prev) loadContent(prev.url);
+        }
+
+        function docNavigateNext(currentUrl) {
+            const { next } = docGetPrevNext(currentUrl);
+            if (next) loadContent(next.url);
+        }
+
+        // Expose to iframes (same origin)
+        window.docGetPrevNext = docGetPrevNext;
+        window.docNavigatePrev = docNavigatePrev;
+        window.docNavigateNext = docNavigateNext;
+
         // =============================
         // Middle ↔ Right connection line
         // =============================

+ 177 - 0
doc/module.html

@@ -0,0 +1,177 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>模块开发与测试指南 - Animal.js</title>
+  <link rel="stylesheet" href="demo.css">
+  <style>
+    .md-wrap {
+      background: #0e0e0e;
+      border: 1px solid #1e1e1e;
+      border-radius: 10px;
+      overflow: hidden;
+      box-shadow: 0 10px 40px rgba(0,0,0,0.25);
+    }
+    .md-head {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      gap: 12px;
+      padding: 12px 16px;
+      border-bottom: 1px solid #222;
+      background: rgba(255,255,255,0.02);
+    }
+    .md-title {
+      color: var(--orange);
+      font-weight: 700;
+      font-size: 13px;
+    }
+    .md-actions {
+      display: inline-flex;
+      gap: 10px;
+      align-items: center;
+    }
+    .md-actions button {
+      appearance: none;
+      background: rgba(255,255,255,0.02);
+      border: 1px solid #2a2a2a;
+      color: #cfcfcf;
+      padding: 6px 10px;
+      border-radius: 999px;
+      font-size: 12px;
+      font-weight: 700;
+      cursor: pointer;
+    }
+    .md-actions button:hover {
+      border-color: var(--orange);
+      color: var(--orange);
+    }
+    pre.md {
+      margin: 0;
+      padding: 18px 18px 22px;
+      white-space: pre-wrap;
+      word-break: break-word;
+      line-height: 1.65;
+      color: #cfcfcf;
+      font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+      font-size: 13px;
+      background: #0b0b0b;
+      max-height: calc(100vh - 210px);
+      overflow: auto;
+    }
+    .hint {
+      color: #8a8a8a;
+      font-size: 12px;
+      margin: 0;
+      padding: 0 18px 16px;
+    }
+  </style>
+</head>
+<body>
+  <div class="container">
+    <div class="page-top">
+      <div class="crumb">DEVELOPER › MODULE</div>
+      <div class="since">WORKFLOW</div>
+    </div>
+
+    <h1>模块开发与测试指南</h1>
+    <p class="description">
+      这个页面会直接读取 <code class="inline">doc/module.md</code> 的内容(原样显示)。<br>
+      你可以在编辑器里继续完善它,然后回来刷新这里查看最新版本。
+    </p>
+
+    <div class="md-wrap">
+      <div class="md-head">
+        <div class="md-title">doc/module.md</div>
+        <div class="md-actions">
+          <button type="button" onclick="reloadMd()">刷新</button>
+          <button type="button" onclick="copyMd()">复制全文</button>
+        </div>
+      </div>
+      <pre id="md" class="md">Loading…</pre>
+      <p class="hint">提示:这是一份“开发用”总指导文档;具体示例页仍在各个模块里。</p>
+    </div>
+
+    <div class="doc-nav" aria-label="Previous and next navigation">
+      <a href="#" onclick="goPrev(); return false;">
+        <span>
+          <span class="nav-label">Previous</span><br>
+          <span id="prev-title" class="nav-title">—</span>
+        </span>
+        <span aria-hidden="true">←</span>
+      </a>
+      <div id="nav-center" class="nav-center">Developer</div>
+      <a href="#" onclick="goNext(); return false;">
+        <span>
+          <span class="nav-label">Next</span><br>
+          <span id="next-title" class="nav-title">—</span>
+        </span>
+        <span aria-hidden="true">→</span>
+      </a>
+    </div>
+  </div>
+
+  <div id="toast" class="toast" role="status" aria-live="polite"></div>
+
+  <script>
+    const CURRENT = 'module.html';
+
+    function showToast(msg) {
+      const el = document.getElementById('toast');
+      if (!el) return;
+      el.textContent = msg;
+      el.classList.add('show');
+      clearTimeout(el._t);
+      el._t = setTimeout(() => el.classList.remove('show'), 900);
+    }
+
+    async function reloadMd() {
+      const el = document.getElementById('md');
+      if (!el) return;
+      el.textContent = 'Loading…';
+      try {
+        const res = await fetch('module.md', { cache: 'no-store' });
+        const txt = await res.text();
+        el.textContent = txt || '(empty)';
+      } catch (e) {
+        el.textContent = 'Failed to load doc/module.md\n\n' + (e && e.message ? e.message : String(e));
+      }
+      syncPrevNext();
+    }
+
+    async function copyMd() {
+      const el = document.getElementById('md');
+      const text = el ? el.textContent : '';
+      if (!text) return;
+      try {
+        await navigator.clipboard.writeText(text);
+        showToast('Copied');
+      } catch (e) {
+        showToast('Copy failed');
+      }
+    }
+
+    function syncPrevNext() {
+      try {
+        const pn = window.parent?.docGetPrevNext?.(CURRENT);
+        const prevEl = document.getElementById('prev-title');
+        const nextEl = document.getElementById('next-title');
+        const centerEl = document.getElementById('nav-center');
+        if (pn?.current && centerEl) centerEl.textContent = pn.current.group || 'Developer';
+        if (prevEl) prevEl.textContent = pn?.prev?.title || '—';
+        if (nextEl) nextEl.textContent = pn?.next?.title || '—';
+      } catch {}
+    }
+
+    function goPrev() { window.parent?.docNavigatePrev?.(CURRENT); }
+    function goNext() { window.parent?.docNavigateNext?.(CURRENT); }
+
+    // Sync middle list selection when loaded inside doc/index.html
+    try { window.parent?.setMiddleActive?.(CURRENT); } catch {}
+
+    reloadMd();
+  </script>
+</body>
+</html>
+

+ 267 - 0
doc/module.md

@@ -0,0 +1,267 @@
+## XJS / Animal.js 文档:模块化开发 & 测试总指南(工作流)
+
+> 目标:把文档拆成可独立开发/测试的“小模块”,每个模块都有:
+>
+> - **左侧(第一列)**:扁平化总菜单(未来会加入 Layer 等 UI 库)
+> - **中间(第二列)**:该模块/分组的「缩略图卡片」导航(像 anime.js 文档那样好玩)
+> - **右侧(第三列)**:每个卡片对应的“完整示例页”(中文说明 + Tabs + Live Demo + Prev/Next)
+
+---
+
+## 目录(简体中文)
+
+- 1. 三列架构是怎么工作的?
+- 2. 文件/目录命名规范(强烈建议照做)
+- 3. 左侧菜单(扁平化)规范:怎么加一个模块?
+- 4. 第二列 list 页面规范:缩略图卡片 + 高亮同步
+- 5. 第三列内容页面规范:Tabs + Demo + 组内/跨组 Prev/Next
+- 6. Prev/Next 规则与路由表维护方式
+- 7. `animal.js`(`xjs`)例子与测试规划(分组清单)
+- 8. 已实现 / 进行中 / 待实现(进度表)
+
+---
+
+## 1) 三列架构是怎么工作的?
+
+文档入口是 `doc/index.html`,它由 3 个区域组成:
+
+- **左侧菜单(第一列)**
+  - 点击某个菜单项会调用 `selectCategory(listUrl, defaultContentUrl)`
+  - 它会同时更新:
+    - **第二列 iframe**:加载该模块的 list 页面(卡片列表)
+    - **第三列 iframe**:加载该模块默认的内容页
+
+- **第二列(中列)**
+  - 本质是一个 list 页(例如 `doc/targets/list.html`)
+  - list 页点击卡片会调用 `window.parent.loadContent('xxx.html')` 来切换第三列内容页
+  - list 页还实现了 `window.setActiveByContentUrl(contentUrl)`,用于**跟随第三列自动高亮**
+
+- **第三列(右侧)**
+  - 每个示例是一个独立 HTML 页面(例如 `doc/targets/test_css_selector.html`)
+  - 内容页在加载时会调用 `window.parent.setMiddleActive('xxx.html')`,让第二列自动高亮对应卡片
+
+---
+
+## 2) 文件/目录命名规范(强烈建议照做)
+
+每个“分组”建议放一个文件夹,结构如下:
+
+```
+doc/
+  <group_name>/
+    list.html               # 第二列:卡片列表(缩略图导航)
+    overview.html           # 第三列:可选的分组概览页(建议)
+    test_<topic>.html       # 第三列:具体示例页(一个功能点一个页面)
+```
+
+命名建议:
+
+- **分组目录名**:英文小写 + 下划线,例如 `targets/`、`tween_value_types/`
+- **示例页**:统一前缀 `test_`,例如 `test_css_selector.html`
+- **概览页**:统一 `overview.html`
+
+---
+
+## 3) 左侧菜单(扁平化)规范:怎么加一个模块?
+
+> 原则:**一个总菜单**,用“分区标题”做视觉分组,但不要折叠树(便于未来加入 Layer/其他 UI 库)。
+
+左侧菜单当前在 `doc/index.html` 内直接维护。
+
+### 左侧菜单建议结构(示例)
+
+- **Animal.js 动画**
+  - Targets(目标)
+  - Properties(可动画属性)
+  - Tween 值(补间值类型)
+  - Callbacks(回调)
+  - Examples(示例)
+- **Layer 弹窗**
+  - Layer + 动画联动示例
+  - (未来)Layer API / 主题 / 组件化
+- **开发者**
+  - 模块开发与测试指南(本文件)
+
+### 加菜单项时必须同时准备
+
+- **第二列 list 页面**:`<group>/list.html`
+- **第三列默认页**:`<group>/overview.html` 或 `test_xxx.html`
+
+否则点击会白屏(找不到页面)。
+
+---
+
+## 4) 第二列 list 页面规范:缩略图卡片 + 高亮同步
+
+第二列 list 页的核心要求:
+
+- 卡片元素带 `data-content="group/test_xxx.html"`,用于与第三列 URL 做匹配
+- 暴露函数 `window.setActiveByContentUrl(contentUrl)`,供父窗口调用进行高亮同步
+- 点击卡片时调用:`window.parent.loadContent('group/test_xxx.html')`
+
+参考现成实现:
+
+- `doc/targets/list.html`
+
+---
+
+## 5) 第三列内容页面规范:Tabs + Demo + Prev/Next
+
+第三列内容页建议统一采用“CSS Selector”同款结构:
+
+- 顶部 breadcrumb + since
+- 标题 + 简体中文说明
+- **Tabs**(JavaScript / HTML / CSS)
+- **Live Demo**(尽可能直观、活泼、有趣)
+- **Prev / Next**(统一走父窗口的全局路由表)
+
+内容页必须做两件事(保证三列联动):
+
+1. **同步第二列高亮**
+
+```
+try { window.parent?.setMiddleActive?.('group/test_xxx.html'); } catch {}
+```
+
+2. **Prev / Next 走父窗口统一逻辑**
+
+```
+const CURRENT = 'group/test_xxx.html';
+function goPrev() { window.parent?.docNavigatePrev?.(CURRENT); }
+function goNext() { window.parent?.docNavigateNext?.(CURRENT); }
+```
+
+> 好处:不用每个页面手写上一个/下一个链接;组内/跨组顺序统一由父窗口维护。
+
+---
+
+## 6) Prev/Next 规则与路由表维护方式
+
+Prev/Next 规则:
+
+- **组内跳转**:同一 group 内按页面顺序移动
+- **跨组跳转**:到上一组的最后一个 / 下一组的第一个(整体顺序不间断)
+
+路由表位置:
+
+- 当前维护在 `doc/index.html` 里的 `DOC_PAGES` 数组
+
+维护原则:
+
+- 每新增一个 `test_xxx.html`,**就把它加进 `DOC_PAGES`**
+- `url` 使用相对 `doc/` 的路径,例如 `targets/test_css_selector.html`
+- `group` 用于 Nav 中间的“当前分组”显示
+
+---
+
+## 7) `animal.js`(`xjs`)例子与测试规划(分组清单)
+
+说明:
+
+- ✅ = 已存在页面(可能需要补充中文说明/统一 PrevNext)
+- ⬜ = 待新增(本工作流会按顺序逐步补齐)
+
+### A. Targets(目标选择)
+
+- ✅ `targets/overview.html`:概览
+- ✅ `targets/test_css_selector.html`:CSS Selector(字符串)
+- ✅ `targets/test_dom_elements.html`:DOM Element / NodeList
+- ✅ `targets/test_js_objects.html`:JS 对象作为目标(对象 tween)
+- ✅ `targets/test_array_targets.html`:数组 targets(混合目标)
+
+### B. Animatable properties(可动画属性 / 引擎分流)
+
+- ✅ `animatable_properties/test_transforms.html`:Transform(x/y/rotate/scale)
+- ✅ `animatable_properties/test_css_props.html`:CSS 属性
+- ✅ `animatable_properties/test_css_vars.html`:CSS 变量 `--xxx`
+- ✅ `animatable_properties/test_js_props.html`:JS 属性(非 style)
+
+⬜(建议新增)
+
+- ⬜ `animatable_properties/overview.html`:本组概览 + “WAAPI vs JS fallback” 解释 + `controls.engine` 展示
+
+### C. Tween value types(补间值类型)
+
+- ✅ `tween_value_types/test_numerical.html`:数字
+- ✅ `tween_value_types/test_unit.html`:单位(px/deg/%/rem…)
+- ✅ `tween_value_types/test_relative.html`:相对值(如果支持)
+- ✅ `tween_value_types/test_colors.html`:颜色(如果支持)
+
+⬜(建议新增)
+
+- ⬜ `tween_value_types/overview.html`:本组概览 + “哪些值会走 JS 插值/哪些会离散切换” 说明
+
+### D. Tween parameters(补间参数 / 播放参数)
+
+> `animal.js` 实际支持的参数(来自源码):`duration / delay / easing / direction / fill / loop / endDelay / autoplay / springFrames / begin / update / complete`。
+
+- ⬜ `tween_parameters/list.html`
+- ⬜ `tween_parameters/overview.html`
+- ⬜ `tween_parameters/test_duration_delay.html`
+- ⬜ `tween_parameters/test_endDelay_fill.html`
+- ⬜ `tween_parameters/test_direction_loop.html`
+- ⬜ `tween_parameters/test_easing_named_and_bezier.html`
+- ⬜ `tween_parameters/test_easing_spring.html`(展示 spring 曲线 + springFrames)
+
+### E. Controls(控制器 API)
+
+> `animate()` 返回 Controls:`play/pause/cancel/finish/seek`,并且是 thenable(可 `await`)。
+
+- ⬜ `controls/list.html`
+- ⬜ `controls/overview.html`
+- ⬜ `controls/test_play_pause_resume.html`
+- ⬜ `controls/test_seek_slider.html`(进度条拖动)
+- ⬜ `controls/test_thenable.html`(`await xjs(...).animate(...)`)
+- ⬜ `controls/test_engine_info.html`(展示 `controls.engine.waapiKeys/jsKeys`)
+
+### F. Timeline(时间轴)
+
+- ⬜ `timeline/list.html`
+- ⬜ `timeline/overview.html`
+- ⬜ `timeline/test_add_offsets.html`(`+=`、`-=`、number offset)
+- ⬜ `timeline/test_defaults.html`(timeline defaults)
+
+### G. SVG(draw)
+
+- ⬜ `svg/list.html`
+- ⬜ `svg/overview.html`
+- ⬜ `svg/test_draw_path.html`(`xjs('path').draw()`)
+- ⬜ `svg/test_svg_attr_js_fallback.html`(SVG attribute 动画:哪些走 JS)
+
+### H. InView / Scroll(视口触发 / 滚动联动)
+
+- ⬜ `scroll/list.html`
+- ⬜ `scroll/overview.html`
+- ⬜ `scroll/test_inview_once.html`(inViewAnimate once:true/false)
+- ⬜ `scroll/test_scroll_linked_element.html`(scroll(target))
+- ⬜ `scroll/test_scroll_container.html`(scroll container)
+
+### I. Layer 集成(联动展示库特性)
+
+> `Selection.layer()` / `Selection.unlayer()` + `xjs.fire()` / `xjs.layer()`
+
+- ✅ `examples/layer.html`(已有示例,但后续会补充“更像产品”的玩法)
+- ⬜ `layer/overview.html`(独立分组:弹窗 API + data-* 配置)
+- ⬜ `layer/test_selection_layer_dataset.html`
+- ⬜ `layer/test_static_fire.html`
+
+---
+
+## 8) 已实现 / 进行中 / 待实现(进度表)
+
+### 已实现 ✅
+
+- ✅ 左侧菜单扁平化(`doc/index.html`)
+- ✅ 全局 Prev/Next 路由表与 API(`doc/index.html`:`DOC_PAGES` + `docGetPrevNext/docNavigatePrev/docNavigateNext`)
+- ✅ “模块开发与测试指南”查看页(`doc/module.html`,读取 `doc/module.md`)
+
+### 进行中 🛠️
+
+- 🛠️ 补齐 `animal.js` 的缺失分组:Tween parameters / Controls / Timeline / SVG / Scroll / Layer(独立分组)
+
+### 待实现 ⬜
+
+- ⬜ 为新增分组创建第二列 list 页面(缩略图卡片导航)
+- ⬜ 为新增分组创建第三列内容页(中文说明 + Tabs + Demo + Prev/Next)
+- ⬜ 统一现有页面的 Prev/Next(从“硬编码跳转”迁移到全局路由表)
+

+ 109 - 0
doc/page_utils.js

@@ -0,0 +1,109 @@
+// doc/page_utils.js
+// Small helpers shared by documentation demo pages (3rd column).
+
+(function () {
+  function qs(sel) { return document.querySelector(sel); }
+  function qsa(sel) { return Array.from(document.querySelectorAll(sel)); }
+
+  function showToast(msg) {
+    const el = document.getElementById('toast');
+    if (!el) return;
+    el.textContent = msg;
+    el.classList.add('show');
+    clearTimeout(el._t);
+    el._t = setTimeout(() => el.classList.remove('show'), 900);
+  }
+
+  async function copyActiveCode() {
+    const active = qs('.code-view.active, .html-view.active, .css-view.active');
+    const text = active ? active.innerText.trimEnd() : '';
+    if (!text) return;
+    try {
+      await navigator.clipboard.writeText(text);
+      showToast('Copied');
+    } catch (e) {
+      // Fallback
+      const ta = document.createElement('textarea');
+      ta.value = text;
+      ta.style.position = 'fixed';
+      ta.style.opacity = '0';
+      document.body.appendChild(ta);
+      ta.select();
+      document.execCommand('copy');
+      ta.remove();
+      showToast('Copied');
+    }
+  }
+
+  function switchTab(tab) {
+    const tabs = qsa('.tabs .tab');
+    tabs.forEach(t => t.classList.remove('active'));
+    qsa('.code-view, .html-view, .css-view').forEach(v => v.classList.remove('active'));
+
+    const t = String(tab || 'js').toLowerCase();
+    const idx = (t === 'js') ? 0 : (t === 'html') ? 1 : 2;
+    const tabEls = [tabs[0], tabs[1], tabs[2]];
+    const viewEls = [
+      document.getElementById('js-code'),
+      document.getElementById('html-code'),
+      document.getElementById('css-code')
+    ];
+
+    tabEls.forEach((el, i) => {
+      if (!el) return;
+      const active = i === idx;
+      el.classList.toggle('active', active);
+      el.setAttribute('aria-selected', active ? 'true' : 'false');
+      el.tabIndex = active ? 0 : -1;
+    });
+    const view = viewEls[idx];
+    if (view) view.classList.add('active');
+  }
+
+  function enableTabKeyboardNav() {
+    document.addEventListener('keydown', (e) => {
+      const focused = document.activeElement;
+      if (!focused || !focused.classList.contains('tab')) return;
+      if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
+      e.preventDefault();
+
+      const tabs = qsa('.tabs .tab');
+      const idx = Math.max(0, tabs.indexOf(focused));
+      const nextIdx = (e.key === 'ArrowRight')
+        ? (idx + 1) % tabs.length
+        : (idx - 1 + tabs.length) % tabs.length;
+      const next = tabs[nextIdx];
+      const label = (next?.textContent || '').toLowerCase();
+      const lang = label.includes('html') ? 'html' : label.includes('css') ? 'css' : 'js';
+      switchTab(lang);
+      qs('.tab.active')?.focus();
+    });
+  }
+
+  function syncPrevNext(currentUrl) {
+    try {
+      const pn = window.parent?.docGetPrevNext?.(currentUrl);
+      const prevEl = document.getElementById('prev-title');
+      const nextEl = document.getElementById('next-title');
+      const centerEl = document.getElementById('nav-center');
+      if (centerEl) centerEl.textContent = pn?.current?.group || centerEl.textContent || '';
+      if (prevEl) prevEl.textContent = pn?.prev?.title || '—';
+      if (nextEl) nextEl.textContent = pn?.next?.title || '—';
+    } catch {}
+  }
+
+  function syncMiddleActive(currentUrl) {
+    try { window.parent?.setMiddleActive?.(currentUrl); } catch {}
+  }
+
+  // Expose
+  window.DocPage = {
+    showToast,
+    copyActiveCode,
+    switchTab,
+    enableTabKeyboardNav,
+    syncPrevNext,
+    syncMiddleActive
+  };
+}());
+

+ 162 - 0
doc/svg/list.html

@@ -0,0 +1,162 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>SVG - List</title>
+  <link rel="stylesheet" href="../list.css">
+  <style>
+    .card { min-height: 132px; padding: 18px 18px 16px 18px; }
+    .card-preview {
+      height: 78px;
+      border-radius: 10px;
+      background: rgba(255,255,255,0.02);
+      border: 1px solid rgba(255,255,255,0.04);
+      position: relative;
+      overflow: hidden;
+      display: grid;
+      place-items: center;
+      margin-bottom: 14px;
+    }
+
+    .mini {
+      width: 100%;
+      height: 100%;
+      display: grid;
+      place-items: center;
+    }
+
+    .mini svg {
+      width: 120px;
+      height: 60px;
+      overflow: visible;
+    }
+
+    .stroke {
+      fill: none;
+      stroke: rgba(255,255,255,0.25);
+      stroke-width: 2.2;
+      stroke-linecap: round;
+      stroke-linejoin: round;
+    }
+    .stroke.orange { stroke: #ff9f43; opacity: 0.9; }
+    .stroke.red { stroke: #ff4b4b; opacity: 0.9; }
+
+    .mini-dot {
+      fill: #ff9f43;
+      opacity: 0.9;
+    }
+
+    .mini-text {
+      fill: rgba(255,255,255,0.22);
+      font-weight: 800;
+      font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+      font-size: 14px;
+      letter-spacing: 1px;
+    }
+
+    /* Hover: subtle “alive” motion */
+    .card:hover .stroke.orange { stroke-dasharray: 6 6; animation: dash 0.8s linear infinite; }
+    .card:hover .mini-dot { animation: bob 0.8s ease-in-out infinite alternate; }
+    @keyframes dash { to { stroke-dashoffset: -12; } }
+    @keyframes bob { to { transform: translateY(-3px); } }
+  </style>
+</head>
+<body>
+  <div class="search-header">
+    <div class="search-title">SVG</div>
+    <div class="search-input-container">
+      <div class="search-icon"></div>
+      <input type="text" class="search-input" placeholder="Filter SVG demos...">
+    </div>
+  </div>
+
+  <div class="grid-section">
+    <div class="header-card active" onclick="selectItem('overview.html', this)">
+      <div class="header-dots"></div>
+      <div class="header-title">SVG</div>
+    </div>
+
+    <div class="card-grid" style="margin-top: 40px;">
+      <div class="card" data-content="svg/test_draw_path.html" onclick="selectItem('test_draw_path.html', this)">
+        <div class="card-preview">
+          <div class="mini" aria-hidden="true">
+            <svg viewBox="0 0 120 60">
+              <path class="stroke" d="M10,42 C25,10 48,10 60,30 C72,50 92,50 110,18" />
+              <path class="stroke orange" d="M10,42 C25,10 48,10 60,30 C72,50 92,50 110,18" />
+            </svg>
+          </div>
+        </div>
+        <div class="card-footer">draw() 路径描边</div>
+      </div>
+
+      <div class="card" data-content="svg/test_motion_path.html" onclick="selectItem('test_motion_path.html', this)">
+        <div class="card-preview">
+          <div class="mini" aria-hidden="true">
+            <svg viewBox="0 0 120 60">
+              <path class="stroke" d="M8,44 C26,8 56,6 74,30 C90,52 102,50 112,16" />
+              <circle class="mini-dot" cx="20" cy="32" r="4" />
+            </svg>
+          </div>
+        </div>
+        <div class="card-footer">沿路径运动(Motion Path)</div>
+      </div>
+
+      <div class="card" data-content="svg/test_svg_attributes.html" onclick="selectItem('test_svg_attributes.html', this)">
+        <div class="card-preview">
+          <div class="mini" aria-hidden="true">
+            <svg viewBox="0 0 120 60">
+              <circle class="stroke red" cx="38" cy="30" r="10" />
+              <circle class="stroke orange" cx="78" cy="30" r="16" />
+            </svg>
+          </div>
+        </div>
+        <div class="card-footer">SVG 属性动画(cx / r / …)</div>
+      </div>
+
+      <div class="card" data-content="svg/test_svg_transforms.html" onclick="selectItem('test_svg_transforms.html', this)">
+        <div class="card-preview">
+          <div class="mini" aria-hidden="true">
+            <svg viewBox="0 0 120 60">
+              <rect class="stroke orange" x="18" y="18" width="18" height="18" rx="4" />
+              <rect class="stroke" x="52" y="18" width="18" height="18" rx="4" />
+              <rect class="stroke red" x="86" y="18" width="18" height="18" rx="4" />
+              <text class="mini-text" x="60" y="54" text-anchor="middle">TRANSFORM</text>
+            </svg>
+          </div>
+        </div>
+        <div class="card-footer">SVG transform(x/y/rotate/scale)</div>
+      </div>
+    </div>
+  </div>
+
+  <script>
+    function selectItem(url, el) {
+      document.querySelectorAll('.card').forEach(c => c.classList.remove('active'));
+      document.querySelectorAll('.header-card').forEach(c => c.classList.remove('active'));
+      el.classList.add('active');
+      window.parent?.loadContent?.('svg/' + url);
+    }
+
+    function setActiveByContentUrl(contentUrl) {
+      const normalized = String(contentUrl || '').replace(/^\.\//, '');
+      const header = document.querySelector('.header-card');
+      const cards = Array.from(document.querySelectorAll('.card[data-content]'));
+
+      document.querySelectorAll('.card').forEach(c => c.classList.remove('active'));
+      document.querySelectorAll('.header-card').forEach(c => c.classList.remove('active'));
+      if (!normalized) return;
+
+      if (normalized.includes('svg/overview.html')) {
+        header?.classList.add('active');
+        return;
+      }
+      const match = cards.find(c => normalized.endsWith(c.dataset.content) || normalized.includes(c.dataset.content));
+      if (match) match.classList.add('active');
+    }
+
+    window.setActiveByContentUrl = setActiveByContentUrl;
+  </script>
+</body>
+</html>
+

+ 145 - 0
doc/svg/overview.html

@@ -0,0 +1,145 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>SVG - Overview</title>
+  <link rel="stylesheet" href="../demo.css">
+  <style>
+    .grid {
+      display: grid;
+      grid-template-columns: repeat(3, minmax(0, 1fr));
+      gap: 14px;
+    }
+    .tile {
+      background: rgba(255,255,255,0.02);
+      border: 1px solid #242424;
+      border-radius: 12px;
+      padding: 14px;
+      min-height: 120px;
+      overflow: hidden;
+      position: relative;
+    }
+    .tile h3 {
+      margin: 0 0 8px;
+      font-size: 13px;
+      color: #fff;
+      font-weight: 800;
+    }
+    .tile p {
+      margin: 0;
+      font-size: 12px;
+      color: #a8a8a8;
+      line-height: 1.6;
+    }
+    .badge {
+      position: absolute;
+      right: 12px;
+      top: 12px;
+      font-size: 10px;
+      font-weight: 900;
+      letter-spacing: 0.8px;
+      color: #6f6f6f;
+      text-transform: uppercase;
+      background: rgba(255,255,255,0.02);
+      border: 1px solid #2a2a2a;
+      padding: 4px 8px;
+      border-radius: 999px;
+    }
+    .tile svg { width: 100%; height: 64px; margin-top: 10px; }
+    .stroke { fill: none; stroke: rgba(255,255,255,0.2); stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
+    .stroke.orange { stroke: #ff9f43; opacity: 0.9; }
+    .stroke.red { stroke: #ff4b4b; opacity: 0.9; }
+    .dot { fill: #ff9f43; opacity: 0.95; }
+  </style>
+</head>
+<body>
+  <div class="container">
+    <div class="page-top">
+      <div class="crumb">ANIMATION › SVG</div>
+      <div class="since">SINCE 1.0.0</div>
+    </div>
+
+    <h1>SVG</h1>
+    <p class="description">
+      SVG 动画最容易“看起来像魔法”。这组示例会用 <code class="inline">xjs</code> 展示几种常见玩法:<br>
+      <strong>路径描边</strong>、<strong>沿路径运动</strong>、<strong>属性动画</strong>、以及 <strong>transform 动画</strong>。
+    </p>
+
+    <h2>你会看到什么?</h2>
+    <div class="grid">
+      <div class="tile">
+        <div class="badge">draw</div>
+        <h3>路径描边(draw)</h3>
+        <p>把路径变成“正在被画出来”的效果。适合 logo / loading / 强调线条。</p>
+        <svg viewBox="0 0 120 60" aria-hidden="true">
+          <path class="stroke" d="M10,42 C25,10 48,10 60,30 C72,50 92,50 110,18" />
+          <path class="stroke orange" d="M10,42 C25,10 48,10 60,30 C72,50 92,50 110,18" />
+        </svg>
+      </div>
+      <div class="tile">
+        <div class="badge">motion</div>
+        <h3>沿路径运动(Motion Path)</h3>
+        <p>一个点沿着 path 跑,像小飞机/小精灵巡航。用 update 回调把进度映射到路径长度。</p>
+        <svg viewBox="0 0 120 60" aria-hidden="true">
+          <path class="stroke" d="M8,44 C26,8 56,6 74,30 C90,52 102,50 112,16" />
+          <circle class="dot" cx="24" cy="32" r="4" />
+        </svg>
+      </div>
+      <div class="tile">
+        <div class="badge">attr</div>
+        <h3>SVG 属性动画(cx / r / …)</h3>
+        <p>不是所有 SVG 属性都走 WAAPI;有些会走 JS fallback,但写法一样简洁。</p>
+        <svg viewBox="0 0 120 60" aria-hidden="true">
+          <circle class="stroke red" cx="38" cy="30" r="10" />
+          <circle class="stroke orange" cx="78" cy="30" r="16" />
+        </svg>
+      </div>
+    </div>
+
+    <div class="box-container" style="margin-top: 18px;">
+      <div class="box-header">
+        <div class="box-title">一句话总结</div>
+      </div>
+      <div class="feature-desc">
+        <strong>核心心法:</strong>
+        <br>- SVG 动画的“视觉冲击力”来自<strong>路径</strong>与<strong>形状变化</strong>。
+        <br>- 你可以把“进度(0..1)”当作万能遥控器:它能驱动位置、旋转、描边、透明度……几乎一切。
+      </div>
+      <div class="action-bar">
+        <button class="play-btn" onclick="goNext()">开始看第一个示例 →</button>
+      </div>
+    </div>
+
+    <div class="doc-nav" aria-label="Previous and next navigation">
+      <a href="#" onclick="goPrev(); return false;">
+        <span>
+          <span class="nav-label">Previous</span><br>
+          <span id="prev-title" class="nav-title">—</span>
+        </span>
+        <span aria-hidden="true">←</span>
+      </a>
+      <div id="nav-center" class="nav-center">SVG</div>
+      <a href="#" onclick="goNext(); return false;">
+        <span>
+          <span class="nav-label">Next</span><br>
+          <span id="next-title" class="nav-title">—</span>
+        </span>
+        <span aria-hidden="true">→</span>
+      </a>
+    </div>
+  </div>
+
+  <div id="toast" class="toast" role="status" aria-live="polite"></div>
+
+  <script src="../page_utils.js"></script>
+  <script>
+    const CURRENT = 'svg/overview.html';
+    function goPrev() { window.parent?.docNavigatePrev?.(CURRENT); }
+    function goNext() { window.parent?.docNavigateNext?.(CURRENT); }
+    DocPage.syncMiddleActive(CURRENT);
+    DocPage.syncPrevNext(CURRENT);
+  </script>
+</body>
+</html>
+

+ 204 - 0
doc/svg/test_draw_path.html

@@ -0,0 +1,204 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>SVG draw() - Animal.js</title>
+  <link rel="stylesheet" href="../demo.css">
+  <style>
+    .demo-visual {
+      padding: 26px 30px;
+      background: #151515;
+      display: grid;
+      grid-template-columns: 1fr 1fr;
+      gap: 18px;
+      align-items: center;
+    }
+    .card {
+      background: rgba(0,0,0,0.25);
+      border: 1px solid #262626;
+      border-radius: 12px;
+      padding: 14px;
+      overflow: hidden;
+      min-height: 160px;
+      position: relative;
+    }
+    .tag {
+      position: absolute;
+      left: 12px;
+      top: 12px;
+      font-size: 10px;
+      font-weight: 900;
+      letter-spacing: 0.9px;
+      text-transform: uppercase;
+      color: #6f6f6f;
+      background: rgba(255,255,255,0.02);
+      border: 1px solid #2a2a2a;
+      padding: 4px 8px;
+      border-radius: 999px;
+    }
+    svg { width: 100%; height: 100%; }
+    .stroke-base {
+      fill: none;
+      stroke: rgba(255,255,255,0.10);
+      stroke-width: 3;
+      stroke-linecap: round;
+      stroke-linejoin: round;
+    }
+    .stroke-draw {
+      fill: none;
+      stroke: var(--accent-color);
+      stroke-width: 3;
+      stroke-linecap: round;
+      stroke-linejoin: round;
+      filter: drop-shadow(0 0 6px rgba(255,159,67,0.18));
+    }
+    .stroke-red { stroke: var(--highlight-color); }
+    .hint {
+      color: #9a9a9a;
+      font-size: 12px;
+      line-height: 1.65;
+      margin-top: -8px;
+      margin-bottom: 16px;
+    }
+  </style>
+</head>
+<body>
+  <div class="container">
+    <div class="page-top">
+      <div class="crumb">ANIMATION › SVG</div>
+      <div class="since">SINCE 1.0.0</div>
+    </div>
+
+    <h1>draw() 路径描边</h1>
+    <p class="description">把 SVG path/line/circle 变成“正在被画出来”的效果,做 Logo、Loading、强调线条都很香。</p>
+    <p class="hint">
+      <strong>它怎么做到的?</strong>核心是 <code class="inline">stroke-dasharray</code> 和 <code class="inline">stroke-dashoffset</code>:
+      先把“虚线间隔”设成路径总长度,再把 dashoffset 从总长度动画到 0。
+    </p>
+
+    <div class="box-container">
+      <div class="box-header">
+        <div class="box-title">SVG draw() code example</div>
+        <div class="box-right">
+          <button class="icon-btn" type="button" title="Copy" onclick="DocPage.copyActiveCode()" aria-label="Copy code">
+            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
+              <rect x="9" y="9" width="13" height="13" rx="2"></rect>
+              <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
+            </svg>
+          </button>
+          <div class="tabs" role="tablist" aria-label="Code tabs">
+            <div class="tab active" role="tab" aria-selected="true" tabindex="0" onclick="DocPage.switchTab('js')">JavaScript</div>
+            <div class="tab" role="tab" aria-selected="false" tabindex="-1" onclick="DocPage.switchTab('html')">HTML</div>
+            <div class="tab" role="tab" aria-selected="false" tabindex="-1" onclick="DocPage.switchTab('css')">CSS</div>
+          </div>
+        </div>
+      </div>
+
+      <div id="js-code" class="code-view active">
+<span class="fun">xjs</span><span class="punc">(</span><span class="str">'#p1'</span><span class="punc">)</span>.<span class="fun">draw</span><span class="punc">({</span>
+  duration<span class="punc">:</span> <span class="num">900</span><span class="punc">,</span>
+  easing<span class="punc">:</span> <span class="str">'ease-out'</span>
+<span class="punc">}</span><span class="punc">)</span><span class="punc">;</span>
+
+<span class="fun">xjs</span><span class="punc">(</span><span class="str">'#p2'</span><span class="punc">)</span>.<span class="fun">draw</span><span class="punc">({</span>
+  duration<span class="punc">:</span> <span class="num">1200</span><span class="punc">,</span>
+  easing<span class="punc">:</span> <span class="punc">[</span><span class="num">0.2</span><span class="punc">,</span> <span class="num">0.8</span><span class="punc">,</span> <span class="num">0.2</span><span class="punc">,</span> <span class="num">1</span><span class="punc">]</span>
+<span class="punc">}</span><span class="punc">)</span><span class="punc">;</span>
+      </div>
+
+      <div id="html-code" class="html-view">
+<span class="tag">&lt;svg</span> <span class="attr">viewBox</span>=<span class="val">"0 0 240 120"</span><span class="tag">&gt;</span>
+  <span class="tag">&lt;path</span> <span class="attr">id</span>=<span class="val">"p1"</span> <span class="attr">d</span>=<span class="val">"M..."</span> <span class="tag">/&gt;</span>
+  <span class="tag">&lt;path</span> <span class="attr">id</span>=<span class="val">"p2"</span> <span class="attr">d</span>=<span class="val">"M..."</span> <span class="tag">/&gt;</span>
+<span class="tag">&lt;/svg&gt;</span>
+      </div>
+
+      <pre id="css-code" class="css-view">.stroke-draw { fill: none; stroke: var(--accent-color); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }</pre>
+
+      <div class="feature-desc">
+        <strong>功能说明:</strong>
+        <br>- <code class="inline">xjs(svgPath).draw()</code> 会自动读取 <code class="inline">getTotalLength()</code> 并设置 dasharray/dashoffset。
+        <br>- 你可以把多个路径一起 draw:例如 logo 的多段笔画同时或分批画出来。
+      </div>
+
+      <div class="demo-visual">
+        <div class="card">
+          <div class="tag">SQUIGGLE</div>
+          <svg viewBox="0 0 240 120">
+            <path class="stroke-base" d="M16,84 C48,18 94,18 120,60 C146,102 194,102 224,36" />
+            <path id="p1" class="stroke-draw" d="M16,84 C48,18 94,18 120,60 C146,102 194,102 224,36" />
+          </svg>
+        </div>
+        <div class="card">
+          <div class="tag">STAR</div>
+          <svg viewBox="0 0 240 120">
+            <path class="stroke-base" d="M120 14 L144 54 L188 60 L154 88 L164 110 L120 94 L76 110 L86 88 L52 60 L96 54 Z" />
+            <path id="p2" class="stroke-draw stroke-red" d="M120 14 L144 54 L188 60 L154 88 L164 110 L120 94 L76 110 L86 88 L52 60 L96 54 Z" />
+          </svg>
+        </div>
+      </div>
+
+      <div class="action-bar">
+        <button class="play-btn" onclick="runDemo()">REPLAY</button>
+        <button class="play-btn secondary" onclick="runStagger()">STAGGER</button>
+      </div>
+    </div>
+
+    <div class="doc-nav" aria-label="Previous and next navigation">
+      <a href="#" onclick="goPrev(); return false;">
+        <span>
+          <span class="nav-label">Previous</span><br>
+          <span id="prev-title" class="nav-title">—</span>
+        </span>
+        <span aria-hidden="true">←</span>
+      </a>
+      <div id="nav-center" class="nav-center">SVG</div>
+      <a href="#" onclick="goNext(); return false;">
+        <span>
+          <span class="nav-label">Next</span><br>
+          <span id="next-title" class="nav-title">—</span>
+        </span>
+        <span aria-hidden="true">→</span>
+      </a>
+    </div>
+  </div>
+
+  <div id="toast" class="toast" role="status" aria-live="polite"></div>
+
+  <script src="../page_utils.js"></script>
+  <script src="../../xjs.js"></script>
+  <script>
+    const CURRENT = 'svg/test_draw_path.html';
+
+    function reset() {
+      // draw() will re-init dash values, but we also clear offsets to avoid flashes
+      document.querySelectorAll('#p1, #p2').forEach(p => {
+        p.style.strokeDasharray = '';
+        p.style.strokeDashoffset = '';
+      });
+    }
+
+    function runDemo() {
+      reset();
+      xjs('#p1').draw({ duration: 900, easing: 'ease-out' });
+      xjs('#p2').draw({ duration: 1200, easing: [0.2, 0.8, 0.2, 1] });
+    }
+
+    function runStagger() {
+      reset();
+      xjs('#p1').draw({ duration: 800, easing: 'ease-out' });
+      setTimeout(() => xjs('#p2').draw({ duration: 1100, easing: 'ease-in-out' }), 220);
+    }
+
+    function goPrev() { window.parent?.docNavigatePrev?.(CURRENT); }
+    function goNext() { window.parent?.docNavigateNext?.(CURRENT); }
+
+    DocPage.enableTabKeyboardNav();
+    DocPage.syncMiddleActive(CURRENT);
+    DocPage.syncPrevNext(CURRENT);
+    setTimeout(runDemo, 320);
+  </script>
+</body>
+</html>
+

+ 301 - 0
doc/svg/test_motion_path.html

@@ -0,0 +1,301 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>SVG Motion Path - Animal.js</title>
+  <link rel="stylesheet" href="../demo.css">
+  <style>
+    .hint {
+      color: #9a9a9a;
+      font-size: 12px;
+      line-height: 1.65;
+      margin-top: -8px;
+      margin-bottom: 16px;
+    }
+    .demo-visual {
+      padding: 26px 30px;
+      background: #151515;
+      display: grid;
+      grid-template-columns: 1fr 280px;
+      gap: 16px;
+      align-items: stretch;
+    }
+    .stage {
+      background: rgba(0,0,0,0.25);
+      border: 1px solid #262626;
+      border-radius: 12px;
+      padding: 14px;
+      overflow: hidden;
+      position: relative;
+      min-height: 220px;
+      display: grid;
+      place-items: center;
+    }
+    .panel {
+      background: rgba(0,0,0,0.18);
+      border: 1px solid #262626;
+      border-radius: 12px;
+      padding: 14px;
+      display: flex;
+      flex-direction: column;
+      gap: 12px;
+      justify-content: space-between;
+    }
+    .control {
+      display: grid;
+      grid-template-columns: 90px 1fr 64px;
+      gap: 10px;
+      align-items: center;
+    }
+    .control label { color: #bdbdbd; font-size: 12px; font-weight: 800; }
+    .control output { color: #e6e6e6; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12px; }
+    input[type="range"] { width: 100%; }
+    .chip {
+      display: inline-flex;
+      gap: 10px;
+      align-items: center;
+      padding: 8px 10px;
+      border-radius: 12px;
+      border: 1px solid #2a2a2a;
+      background: rgba(255,255,255,0.02);
+      color: #cfcfcf;
+      font-size: 12px;
+      font-weight: 700;
+    }
+    .lamp {
+      width: 10px;
+      height: 10px;
+      border-radius: 50%;
+      background: #444;
+    }
+    .lamp.on {
+      background: var(--accent-color);
+      box-shadow: 0 0 0 5px rgba(255,159,67,0.14);
+    }
+
+    svg { width: 100%; height: 100%; max-height: 260px; }
+    .path {
+      fill: none;
+      stroke: rgba(255,255,255,0.12);
+      stroke-width: 2.2;
+      stroke-linecap: round;
+      stroke-linejoin: round;
+    }
+    .path.hint {
+      stroke-dasharray: 6 6;
+      stroke: rgba(255,255,255,0.12);
+    }
+    .runner {
+      fill: var(--accent-color);
+      filter: drop-shadow(0 0 8px rgba(255,159,67,0.25));
+    }
+    .runner-eye {
+      fill: rgba(0,0,0,0.55);
+      opacity: 0.8;
+    }
+  </style>
+</head>
+<body>
+  <div class="container">
+    <div class="page-top">
+      <div class="crumb">ANIMATION › SVG</div>
+      <div class="since">SINCE 1.0.0</div>
+    </div>
+
+    <h1>沿路径运动(Motion Path)</h1>
+    <p class="description">让一个小点沿着 path 跑:像小飞机巡航、像精灵带路、像 UI 的“引导线”。</p>
+    <p class="hint">
+      <strong>实现思路:</strong>把动画进度(0..1)映射到 path 的长度,然后用 <code class="inline">getPointAtLength()</code>
+      取出坐标,再把坐标写到目标的 transform 上。
+    </p>
+
+    <div class="box-container">
+      <div class="box-header">
+        <div class="box-title">Motion path code example</div>
+        <div class="box-right">
+          <button class="icon-btn" type="button" title="Copy" onclick="DocPage.copyActiveCode()" aria-label="Copy code">
+            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
+              <rect x="9" y="9" width="13" height="13" rx="2"></rect>
+              <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
+            </svg>
+          </button>
+          <div class="tabs" role="tablist" aria-label="Code tabs">
+            <div class="tab active" role="tab" aria-selected="true" tabindex="0" onclick="DocPage.switchTab('js')">JavaScript</div>
+            <div class="tab" role="tab" aria-selected="false" tabindex="-1" onclick="DocPage.switchTab('html')">HTML</div>
+            <div class="tab" role="tab" aria-selected="false" tabindex="-1" onclick="DocPage.switchTab('css')">CSS</div>
+          </div>
+        </div>
+      </div>
+
+      <div id="js-code" class="code-view active">
+<span class="kwd">const</span> path <span class="punc">=</span> document.<span class="fun">querySelector</span>(<span class="str">'#track'</span>)<span class="punc">;</span>
+<span class="kwd">const</span> runner <span class="punc">=</span> document.<span class="fun">querySelector</span>(<span class="str">'#runner'</span>)<span class="punc">;</span>
+<span class="kwd">const</span> len <span class="punc">=</span> path.<span class="fun">getTotalLength</span>()<span class="punc">;</span>
+
+<span class="fun">xjs</span><span class="punc">({</span>p<span class="punc">:</span> <span class="num">0</span><span class="punc">}</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span>
+  p<span class="punc">:</span> <span class="punc">[</span><span class="num">0</span><span class="punc">,</span> <span class="num">1</span><span class="punc">]</span><span class="punc">,</span>
+  duration<span class="punc">:</span> <span class="num">1400</span><span class="punc">,</span>
+  easing<span class="punc">:</span> <span class="str">'ease-in-out'</span><span class="punc">,</span>
+  update<span class="punc">:</span> <span class="punc">({</span>progress<span class="punc">})</span> <span class="punc">=&gt;</span> <span class="punc">{</span>
+    <span class="kwd">const</span> pt <span class="punc">=</span> path.<span class="fun">getPointAtLength</span>(len <span class="punc">*</span> progress)<span class="punc">;</span>
+    runner.<span class="fun">setAttribute</span>(<span class="str">'transform'</span><span class="punc">,</span> <span class="str">`translate(${pt.x} ${pt.y})`</span>)<span class="punc">;</span>
+  <span class="punc">}</span>
+<span class="punc">}</span><span class="punc">)</span><span class="punc">;</span>
+      </div>
+
+      <div id="html-code" class="html-view">
+<span class="tag">&lt;path</span> <span class="attr">id</span>=<span class="val">"track"</span> <span class="attr">d</span>=<span class="val">"M..."</span> <span class="tag">/&gt;</span>
+<span class="tag">&lt;g</span> <span class="attr">id</span>=<span class="val">"runner"</span><span class="tag">&gt;</span>...<span class="tag">&lt;/g&gt;</span>
+      </div>
+
+      <pre id="css-code" class="css-view">.runner { fill: var(--accent-color); filter: drop-shadow(0 0 8px rgba(255,159,67,0.25)); }</pre>
+
+      <div class="feature-desc">
+        <strong>功能说明:</strong>
+        <br>- 这里用 <code class="inline">xjs(plainObject)</code> 做“进度发生器”,在 <code class="inline">update</code> 里把进度映射到 path。
+        <br>- 好处是:你可以把任何复杂逻辑(路径、碰撞、转向、跟随摄像机)都写在 update 里。
+      </div>
+
+      <div class="demo-visual">
+        <div class="stage">
+          <svg viewBox="0 0 320 180" aria-label="Motion path demo">
+            <path id="track" class="path" d="M18,142 C62,14 142,12 176,90 C204,154 262,166 300,42" />
+            <path class="path hint" d="M18,142 C62,14 142,12 176,90 C204,154 262,166 300,42" />
+            <g id="runner" transform="translate(18 142)">
+              <circle class="runner" cx="0" cy="0" r="10"></circle>
+              <circle class="runner-eye" cx="3" cy="-2" r="2.2"></circle>
+            </g>
+          </svg>
+        </div>
+        <div class="panel">
+          <div class="chip"><span id="lamp" class="lamp"></span><span id="stateText">READY</span></div>
+          <div>
+            <div class="control">
+              <label for="dur">duration</label>
+              <input id="dur" type="range" min="300" max="3600" step="50" value="1400" />
+              <output id="durOut">1400ms</output>
+            </div>
+            <div class="control">
+              <label for="turn">spin</label>
+              <input id="turn" type="range" min="0" max="3" step="0.25" value="0.5" />
+              <output id="turnOut">0.5turn</output>
+            </div>
+          </div>
+          <div style="display:flex; gap:10px; justify-content:flex-end;">
+            <button class="play-btn secondary" onclick="pauseDemo()">PAUSE</button>
+            <button class="play-btn secondary" onclick="resumeDemo()">RESUME</button>
+            <button class="play-btn" onclick="runDemo()">REPLAY</button>
+          </div>
+        </div>
+      </div>
+
+      <div class="action-bar" style="justify-content:flex-end;">
+        <button class="play-btn secondary" onclick="randomize()">SURPRISE</button>
+      </div>
+    </div>
+
+    <div class="doc-nav" aria-label="Previous and next navigation">
+      <a href="#" onclick="goPrev(); return false;">
+        <span>
+          <span class="nav-label">Previous</span><br>
+          <span id="prev-title" class="nav-title">—</span>
+        </span>
+        <span aria-hidden="true">←</span>
+      </a>
+      <div id="nav-center" class="nav-center">SVG</div>
+      <a href="#" onclick="goNext(); return false;">
+        <span>
+          <span class="nav-label">Next</span><br>
+          <span id="next-title" class="nav-title">—</span>
+        </span>
+        <span aria-hidden="true">→</span>
+      </a>
+    </div>
+  </div>
+
+  <div id="toast" class="toast" role="status" aria-live="polite"></div>
+
+  <script src="../page_utils.js"></script>
+  <script src="../../xjs.js"></script>
+  <script>
+    const CURRENT = 'svg/test_motion_path.html';
+    let __ctl = null;
+
+    const path = document.getElementById('track');
+    const runner = document.getElementById('runner');
+    const dur = document.getElementById('dur');
+    const turn = document.getElementById('turn');
+    const durOut = document.getElementById('durOut');
+    const turnOut = document.getElementById('turnOut');
+    const lamp = document.getElementById('lamp');
+    const stateText = document.getElementById('stateText');
+
+    function setState(t, on) {
+      stateText.textContent = t;
+      lamp.classList.toggle('on', !!on);
+    }
+
+    function read() {
+      const duration = Number(dur.value);
+      const spin = Number(turn.value);
+      durOut.textContent = duration + 'ms';
+      turnOut.textContent = spin + 'turn';
+      return { duration, spin };
+    }
+
+    function reset() {
+      try { __ctl?.cancel?.(); } catch {}
+      __ctl = null;
+      runner.setAttribute('transform', 'translate(18 142) rotate(0)');
+      setState('READY', false);
+    }
+
+    function runDemo() {
+      reset();
+      const { duration, spin } = read();
+      const len = path.getTotalLength();
+
+      setState('RUNNING', true);
+      __ctl = xjs({ p: 0 }).animate({
+        p: [0, 1],
+        duration,
+        easing: 'ease-in-out',
+        update: ({ progress }) => {
+          const pt = path.getPointAtLength(len * progress);
+          const r = spin * 360 * progress;
+          runner.setAttribute('transform', `translate(${pt.x} ${pt.y}) rotate(${r})`);
+        },
+        complete: () => setState('DONE', false)
+      });
+    }
+
+    function pauseDemo() {
+      try { __ctl?.pause?.(); setState('PAUSED', true); } catch {}
+    }
+    function resumeDemo() {
+      try { __ctl?.play?.(); setState('RUNNING', true); } catch {}
+    }
+
+    function randomize() {
+      dur.value = String(400 + Math.floor(Math.random() * 55) * 50);
+      turn.value = String((Math.floor(Math.random() * 13) / 4).toFixed(2));
+      read();
+      runDemo();
+    }
+
+    dur.addEventListener('input', read);
+    turn.addEventListener('input', read);
+
+    function goPrev() { window.parent?.docNavigatePrev?.(CURRENT); }
+    function goNext() { window.parent?.docNavigateNext?.(CURRENT); }
+
+    DocPage.enableTabKeyboardNav();
+    DocPage.syncMiddleActive(CURRENT);
+    DocPage.syncPrevNext(CURRENT);
+    read();
+    setTimeout(runDemo, 320);
+  </script>
+</body>
+</html>
+

+ 248 - 0
doc/svg/test_svg_attributes.html

@@ -0,0 +1,248 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>SVG Attributes - Animal.js</title>
+  <link rel="stylesheet" href="../demo.css">
+  <style>
+    .hint {
+      color: #9a9a9a;
+      font-size: 12px;
+      line-height: 1.65;
+      margin-top: -8px;
+      margin-bottom: 16px;
+    }
+    .demo-visual {
+      padding: 26px 30px;
+      background: #151515;
+      display: grid;
+      grid-template-columns: 1fr 280px;
+      gap: 16px;
+      align-items: stretch;
+    }
+    .stage {
+      background: rgba(0,0,0,0.25);
+      border: 1px solid #262626;
+      border-radius: 12px;
+      padding: 14px;
+      overflow: hidden;
+      position: relative;
+      min-height: 240px;
+      display: grid;
+      place-items: center;
+    }
+    .panel {
+      background: rgba(0,0,0,0.18);
+      border: 1px solid #262626;
+      border-radius: 12px;
+      padding: 14px;
+      display: flex;
+      flex-direction: column;
+      gap: 12px;
+      justify-content: space-between;
+    }
+    .control {
+      display: grid;
+      grid-template-columns: 90px 1fr 64px;
+      gap: 10px;
+      align-items: center;
+    }
+    .control label { color: #bdbdbd; font-size: 12px; font-weight: 800; }
+    .control output { color: #e6e6e6; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12px; }
+    input[type="range"] { width: 100%; }
+
+    svg { width: 100%; height: 100%; max-height: 280px; }
+    .bg {
+      fill: rgba(255,255,255,0.02);
+      stroke: rgba(255,255,255,0.06);
+      stroke-width: 1;
+    }
+    .ring { fill: none; stroke: rgba(255,255,255,0.14); stroke-width: 8; }
+    .ring.orange { stroke: var(--accent-color); filter: drop-shadow(0 0 10px rgba(255,159,67,0.18)); }
+    .ring.red { stroke: var(--highlight-color); filter: drop-shadow(0 0 10px rgba(255,75,75,0.16)); }
+    .dot { fill: rgba(255,255,255,0.18); }
+  </style>
+</head>
+<body>
+  <div class="container">
+    <div class="page-top">
+      <div class="crumb">ANIMATION › SVG</div>
+      <div class="since">SINCE 1.0.0</div>
+    </div>
+
+    <h1>SVG 属性动画(cx / r / …)</h1>
+    <p class="description">
+      SVG 很多“形状”都由属性控制(比如 <code class="inline">cx</code>、<code class="inline">r</code>、<code class="inline">strokeDashoffset</code>)。
+      这类属性不一定走 WAAPI,但 <code class="inline">xjs().animate()</code> 仍然能动画它们(会走 JS fallback)。
+    </p>
+    <p class="hint">
+      <strong>直觉理解:</strong>“CSS 属性”是写到 style 的;而 SVG 的很多东西是写在 attribute 上的。<br>
+      在本库里:同一套写法,内部会决定用 WAAPI 还是 JS 去更新。
+    </p>
+
+    <div class="box-container">
+      <div class="box-header">
+        <div class="box-title">SVG attributes code example</div>
+        <div class="box-right">
+          <button class="icon-btn" type="button" title="Copy" onclick="DocPage.copyActiveCode()" aria-label="Copy code">
+            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
+              <rect x="9" y="9" width="13" height="13" rx="2"></rect>
+              <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
+            </svg>
+          </button>
+          <div class="tabs" role="tablist" aria-label="Code tabs">
+            <div class="tab active" role="tab" aria-selected="true" tabindex="0" onclick="DocPage.switchTab('js')">JavaScript</div>
+            <div class="tab" role="tab" aria-selected="false" tabindex="-1" onclick="DocPage.switchTab('html')">HTML</div>
+            <div class="tab" role="tab" aria-selected="false" tabindex="-1" onclick="DocPage.switchTab('css')">CSS</div>
+          </div>
+        </div>
+      </div>
+
+      <div id="js-code" class="code-view active">
+<span class="fun">xjs</span><span class="punc">(</span><span class="str">'#c1'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span>
+  cx<span class="punc">:</span> <span class="punc">[</span><span class="num">96</span><span class="punc">,</span> <span class="num">224</span><span class="punc">]</span><span class="punc">,</span>
+  r<span class="punc">:</span> <span class="punc">[</span><span class="num">14</span><span class="punc">,</span> <span class="num">34</span><span class="punc">]</span><span class="punc">,</span>
+  duration<span class="punc">:</span> <span class="num">900</span><span class="punc">,</span>
+  direction<span class="punc">:</span> <span class="str">'alternate'</span><span class="punc">,</span>
+  loop<span class="punc">:</span> <span class="num">2</span>
+<span class="punc">}</span><span class="punc">)</span><span class="punc">;</span>
+      </div>
+
+      <div id="html-code" class="html-view">
+<span class="tag">&lt;circle</span> <span class="attr">id</span>=<span class="val">"c1"</span> <span class="attr">cx</span>=<span class="val">"96"</span> <span class="attr">cy</span>=<span class="val">"90"</span> <span class="attr">r</span>=<span class="val">"14"</span> <span class="tag">/&gt;</span>
+      </div>
+
+      <pre id="css-code" class="css-view">.ring.orange { stroke: var(--accent-color); }
+.ring.red { stroke: var(--highlight-color); }</pre>
+
+      <div class="feature-desc">
+        <strong>功能说明:</strong>
+        <br>- 对 SVG 元素来说,“不是 transform / opacity / filter 的属性”,通常会走 JS 更新属性值。
+        <br>- 这让你能直接动画 <code class="inline">cx</code>、<code class="inline">r</code>、<code class="inline">strokeWidth</code> 等等。
+      </div>
+
+      <div class="demo-visual">
+        <div class="stage">
+          <svg viewBox="0 0 320 220" aria-label="SVG attributes demo">
+            <rect class="bg" x="16" y="16" width="288" height="188" rx="14"></rect>
+            <circle class="ring" cx="160" cy="110" r="70"></circle>
+            <circle id="c1" class="ring orange" cx="96" cy="90" r="14"></circle>
+            <circle id="c2" class="ring red" cx="224" cy="130" r="22"></circle>
+            <circle class="dot" cx="160" cy="110" r="2"></circle>
+          </svg>
+        </div>
+        <div class="panel">
+          <div>
+            <div class="control">
+              <label for="amp">amplitude</label>
+              <input id="amp" type="range" min="20" max="120" step="5" value="70" />
+              <output id="ampOut">70</output>
+            </div>
+            <div class="control">
+              <label for="dur">duration</label>
+              <input id="dur" type="range" min="200" max="2400" step="50" value="900" />
+              <output id="durOut">900ms</output>
+            </div>
+          </div>
+          <div style="display:flex; gap:10px; justify-content:flex-end;">
+            <button class="play-btn secondary" onclick="pauseDemo()">PAUSE</button>
+            <button class="play-btn secondary" onclick="resumeDemo()">RESUME</button>
+            <button class="play-btn" onclick="runDemo()">REPLAY</button>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="doc-nav" aria-label="Previous and next navigation">
+      <a href="#" onclick="goPrev(); return false;">
+        <span>
+          <span class="nav-label">Previous</span><br>
+          <span id="prev-title" class="nav-title">—</span>
+        </span>
+        <span aria-hidden="true">←</span>
+      </a>
+      <div id="nav-center" class="nav-center">SVG</div>
+      <a href="#" onclick="goNext(); return false;">
+        <span>
+          <span class="nav-label">Next</span><br>
+          <span id="next-title" class="nav-title">—</span>
+        </span>
+        <span aria-hidden="true">→</span>
+      </a>
+    </div>
+  </div>
+
+  <div id="toast" class="toast" role="status" aria-live="polite"></div>
+
+  <script src="../page_utils.js"></script>
+  <script src="../../xjs.js"></script>
+  <script>
+    const CURRENT = 'svg/test_svg_attributes.html';
+    let __ctls = [];
+
+    const c1 = document.getElementById('c1');
+    const c2 = document.getElementById('c2');
+    const amp = document.getElementById('amp');
+    const dur = document.getElementById('dur');
+    const ampOut = document.getElementById('ampOut');
+    const durOut = document.getElementById('durOut');
+
+    function read() {
+      const a = Number(amp.value);
+      const d = Number(dur.value);
+      ampOut.textContent = String(a);
+      durOut.textContent = d + 'ms';
+      return { a, d };
+    }
+
+    function reset() {
+      const prev = __ctls || [];
+      prev.forEach(c => { try { c?.cancel?.(); } catch {} });
+      __ctls = [];
+      c1.setAttribute('cx', '96'); c1.setAttribute('cy', '90'); c1.setAttribute('r', '14');
+      c2.setAttribute('cx', '224'); c2.setAttribute('cy', '130'); c2.setAttribute('r', '22');
+    }
+
+    function runDemo() {
+      reset();
+      const { a, d } = read();
+      // Two circles cross & breathe (separate controls; library doesn't support per-target function values here)
+      __ctls.push(xjs(c1).animate({
+        cx: [96, 160 + a],
+        cy: [90, 110 - a * 0.2],
+        r: [14, 24],
+        duration: d,
+        easing: 'ease-in-out',
+        direction: 'alternate',
+        loop: 2
+      }));
+      __ctls.push(xjs(c2).animate({
+        cx: [224, 160 - a],
+        cy: [130, 110 + a * 0.2],
+        r: [22, 14],
+        duration: d,
+        easing: 'ease-in-out',
+        direction: 'alternate',
+        loop: 2
+      }));
+    }
+
+    function pauseDemo() { (__ctls || []).forEach(c => { try { c?.pause?.(); } catch {} }); }
+    function resumeDemo() { (__ctls || []).forEach(c => { try { c?.play?.(); } catch {} }); }
+
+    amp.addEventListener('input', read);
+    dur.addEventListener('input', read);
+
+    function goPrev() { window.parent?.docNavigatePrev?.(CURRENT); }
+    function goNext() { window.parent?.docNavigateNext?.(CURRENT); }
+
+    DocPage.enableTabKeyboardNav();
+    DocPage.syncMiddleActive(CURRENT);
+    DocPage.syncPrevNext(CURRENT);
+    read();
+    setTimeout(runDemo, 320);
+  </script>
+</body>
+</html>
+

+ 256 - 0
doc/svg/test_svg_transforms.html

@@ -0,0 +1,256 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>SVG Transforms - Animal.js</title>
+  <link rel="stylesheet" href="../demo.css">
+  <style>
+    .hint {
+      color: #9a9a9a;
+      font-size: 12px;
+      line-height: 1.65;
+      margin-top: -8px;
+      margin-bottom: 16px;
+    }
+    .demo-visual {
+      padding: 26px 30px;
+      background: #151515;
+      display: grid;
+      grid-template-columns: 1fr 280px;
+      gap: 16px;
+      align-items: stretch;
+    }
+    .stage {
+      background: rgba(0,0,0,0.25);
+      border: 1px solid #262626;
+      border-radius: 12px;
+      padding: 14px;
+      overflow: hidden;
+      position: relative;
+      min-height: 260px;
+      display: grid;
+      place-items: center;
+    }
+    .panel {
+      background: rgba(0,0,0,0.18);
+      border: 1px solid #262626;
+      border-radius: 12px;
+      padding: 14px;
+      display: flex;
+      flex-direction: column;
+      gap: 12px;
+      justify-content: space-between;
+    }
+    .control {
+      display: grid;
+      grid-template-columns: 90px 1fr 64px;
+      gap: 10px;
+      align-items: center;
+    }
+    .control label { color: #bdbdbd; font-size: 12px; font-weight: 800; }
+    .control output { color: #e6e6e6; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12px; }
+    input[type="range"] { width: 100%; }
+
+    svg { width: 100%; height: 100%; max-height: 320px; overflow: visible; }
+    .grid-bg {
+      stroke: rgba(255,255,255,0.06);
+      stroke-width: 1;
+    }
+    .box {
+      fill: rgba(255,159,67,0.15);
+      stroke: rgba(255,159,67,0.45);
+      stroke-width: 2;
+      rx: 10;
+      ry: 10;
+      transform-box: fill-box;
+      transform-origin: center;
+    }
+    .box.red {
+      fill: rgba(255,75,75,0.12);
+      stroke: rgba(255,75,75,0.45);
+    }
+    .box.gray {
+      fill: rgba(255,255,255,0.06);
+      stroke: rgba(255,255,255,0.16);
+    }
+    .label {
+      fill: rgba(255,255,255,0.22);
+      font-weight: 800;
+      font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+      font-size: 12px;
+      letter-spacing: 0.8px;
+    }
+  </style>
+</head>
+<body>
+  <div class="container">
+    <div class="page-top">
+      <div class="crumb">ANIMATION › SVG</div>
+      <div class="since">SINCE 1.0.0</div>
+    </div>
+
+    <h1>SVG transform(x/y/rotate/scale)</h1>
+    <p class="description">
+      SVG 元素也能像普通 DOM 一样做 transform 动画:<code class="inline">x</code>/<code class="inline">y</code>、<code class="inline">rotate</code>、<code class="inline">scale</code>。
+      这类通常会走 WAAPI,性能也更稳。
+    </p>
+    <p class="hint">
+      <strong>建议:</strong>如果你要做“动起来的图标/插画”,优先用 transform(位移/旋转/缩放),再配合 opacity / filter。
+    </p>
+
+    <div class="box-container">
+      <div class="box-header">
+        <div class="box-title">SVG transforms code example</div>
+        <div class="box-right">
+          <button class="icon-btn" type="button" title="Copy" onclick="DocPage.copyActiveCode()" aria-label="Copy code">
+            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
+              <rect x="9" y="9" width="13" height="13" rx="2"></rect>
+              <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
+            </svg>
+          </button>
+          <div class="tabs" role="tablist" aria-label="Code tabs">
+            <div class="tab active" role="tab" aria-selected="true" tabindex="0" onclick="DocPage.switchTab('js')">JavaScript</div>
+            <div class="tab" role="tab" aria-selected="false" tabindex="-1" onclick="DocPage.switchTab('html')">HTML</div>
+            <div class="tab" role="tab" aria-selected="false" tabindex="-1" onclick="DocPage.switchTab('css')">CSS</div>
+          </div>
+        </div>
+      </div>
+
+      <div id="js-code" class="code-view active">
+<span class="fun">xjs</span><span class="punc">(</span><span class="str">'#b1'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span> x<span class="punc">:</span> <span class="str">'6rem'</span><span class="punc">,</span> rotate<span class="punc">:</span> <span class="str">'1turn'</span><span class="punc"> }</span><span class="punc">)</span><span class="punc">;</span>
+<span class="fun">xjs</span><span class="punc">(</span><span class="str">'#b2'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span> y<span class="punc">:</span> <span class="str">'-3rem'</span><span class="punc">,</span> scale<span class="punc">:</span> <span class="punc">[</span><span class="num">1</span><span class="punc">,</span> <span class="num">1.35</span><span class="punc">]</span><span class="punc"> }</span><span class="punc">)</span><span class="punc">;</span>
+      </div>
+
+      <div id="html-code" class="html-view">
+<span class="tag">&lt;rect</span> <span class="attr">id</span>=<span class="val">"b1"</span> <span class="attr">x</span>=<span class="val">"78"</span> <span class="attr">y</span>=<span class="val">"92"</span> <span class="attr">width</span>=<span class="val">"44"</span> <span class="attr">height</span>=<span class="val">"44"</span> <span class="tag">/&gt;</span>
+      </div>
+
+      <pre id="css-code" class="css-view">.box { transform-box: fill-box; transform-origin: center; }</pre>
+
+      <div class="feature-desc">
+        <strong>功能说明:</strong>
+        <br>- 对 SVG 做 transform 动画时,建议设置 <code class="inline">transform-box</code> 和 <code class="inline">transform-origin</code>,旋转更符合直觉。
+        <br>- 你可以把多个图形组合成一个小“角色”,用一套参数让它动起来。
+      </div>
+
+      <div class="demo-visual">
+        <div class="stage">
+          <svg viewBox="0 0 320 240" aria-label="SVG transforms demo">
+            <!-- background grid -->
+            <g opacity="1">
+              <path class="grid-bg" d="M16 40 H304"></path>
+              <path class="grid-bg" d="M16 80 H304"></path>
+              <path class="grid-bg" d="M16 120 H304"></path>
+              <path class="grid-bg" d="M16 160 H304"></path>
+              <path class="grid-bg" d="M16 200 H304"></path>
+              <path class="grid-bg" d="M64 24 V216"></path>
+              <path class="grid-bg" d="M112 24 V216"></path>
+              <path class="grid-bg" d="M160 24 V216"></path>
+              <path class="grid-bg" d="M208 24 V216"></path>
+              <path class="grid-bg" d="M256 24 V216"></path>
+            </g>
+
+            <rect id="b1" class="box" x="78" y="92" width="44" height="44" rx="10" ry="10"></rect>
+            <rect id="b2" class="box gray" x="138" y="92" width="44" height="44" rx="10" ry="10"></rect>
+            <rect id="b3" class="box red" x="198" y="92" width="44" height="44" rx="10" ry="10"></rect>
+            <text class="label" x="160" y="230" text-anchor="middle">x / y / rotate / scale</text>
+          </svg>
+        </div>
+        <div class="panel">
+          <div>
+            <div class="control">
+              <label for="dist">distance</label>
+              <input id="dist" type="range" min="20" max="140" step="5" value="96" />
+              <output id="distOut">96px</output>
+            </div>
+            <div class="control">
+              <label for="dur">duration</label>
+              <input id="dur" type="range" min="200" max="2400" step="50" value="900" />
+              <output id="durOut">900ms</output>
+            </div>
+          </div>
+          <div style="display:flex; gap:10px; justify-content:flex-end;">
+            <button class="play-btn secondary" onclick="pauseDemo()">PAUSE</button>
+            <button class="play-btn secondary" onclick="resumeDemo()">RESUME</button>
+            <button class="play-btn" onclick="runDemo()">REPLAY</button>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="doc-nav" aria-label="Previous and next navigation">
+      <a href="#" onclick="goPrev(); return false;">
+        <span>
+          <span class="nav-label">Previous</span><br>
+          <span id="prev-title" class="nav-title">—</span>
+        </span>
+        <span aria-hidden="true">←</span>
+      </a>
+      <div id="nav-center" class="nav-center">SVG</div>
+      <a href="#" onclick="goNext(); return false;">
+        <span>
+          <span class="nav-label">Next</span><br>
+          <span id="next-title" class="nav-title">—</span>
+        </span>
+        <span aria-hidden="true">→</span>
+      </a>
+    </div>
+  </div>
+
+  <div id="toast" class="toast" role="status" aria-live="polite"></div>
+
+  <script src="../page_utils.js"></script>
+  <script src="../../xjs.js"></script>
+  <script>
+    const CURRENT = 'svg/test_svg_transforms.html';
+    let __ctls = [];
+
+    const b1 = document.getElementById('b1');
+    const b2 = document.getElementById('b2');
+    const b3 = document.getElementById('b3');
+    const dist = document.getElementById('dist');
+    const dur = document.getElementById('dur');
+    const distOut = document.getElementById('distOut');
+    const durOut = document.getElementById('durOut');
+
+    function read() {
+      const d = Number(dist.value);
+      const t = Number(dur.value);
+      distOut.textContent = d + 'px';
+      durOut.textContent = t + 'ms';
+      return { d, t };
+    }
+
+    function reset() {
+      (__ctls || []).forEach(c => { try { c?.cancel?.(); } catch {} });
+      __ctls = [];
+      [b1, b2, b3].forEach(el => { if (el) el.style.transform = 'none'; });
+    }
+
+    function runDemo() {
+      reset();
+      const { d, t } = read();
+      __ctls.push(xjs(b1).animate({ x: d, rotate: '1turn', duration: t, easing: 'ease-out' }));
+      __ctls.push(xjs(b2).animate({ y: -Math.round(d * 0.55), scale: [1, 1.35], duration: t, easing: 'ease-in-out' }));
+      __ctls.push(xjs(b3).animate({ x: -d, rotate: '-1turn', duration: t, easing: 'ease-out' }));
+    }
+
+    function pauseDemo() { (__ctls || []).forEach(c => { try { c?.pause?.(); } catch {} }); }
+    function resumeDemo() { (__ctls || []).forEach(c => { try { c?.play?.(); } catch {} }); }
+
+    dist.addEventListener('input', read);
+    dur.addEventListener('input', read);
+
+    function goPrev() { window.parent?.docNavigatePrev?.(CURRENT); }
+    function goNext() { window.parent?.docNavigateNext?.(CURRENT); }
+
+    DocPage.enableTabKeyboardNav();
+    DocPage.syncMiddleActive(CURRENT);
+    DocPage.syncPrevNext(CURRENT);
+    read();
+    setTimeout(runDemo, 320);
+  </script>
+</body>
+</html>
+

+ 129 - 0
doc/tween_parameters/list.html

@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Tween Parameters - List</title>
+  <link rel="stylesheet" href="../list.css">
+  <style>
+    .card { min-height: 120px; padding: 18px 18px 16px 18px; }
+    .card-preview {
+      height: 62px;
+      border-radius: 10px;
+      background: rgba(255,255,255,0.02);
+      border: 1px solid rgba(255,255,255,0.04);
+      position: relative;
+      overflow: hidden;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      margin-bottom: 14px;
+    }
+    .pill {
+      font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+      color: #b0b0b0;
+      background: rgba(255,255,255,0.04);
+      border: 1px solid rgba(255,255,255,0.06);
+      padding: 10px 14px;
+      border-radius: 10px;
+      font-size: 12px;
+      letter-spacing: 0.2px;
+      display: inline-flex;
+      gap: 10px;
+      align-items: center;
+    }
+    .dot {
+      width: 10px;
+      height: 10px;
+      border-radius: 50%;
+      background: #3a3a3a;
+      box-shadow: 0 0 0 1px rgba(0,0,0,0.25) inset;
+    }
+    .dot.orange { background: #ff9f43; }
+    .dot.red { background: #ff4b4b; }
+    .card:hover .dot.orange { transform: translateX(6px); transition: transform 0.25s ease; }
+  </style>
+</head>
+<body>
+  <div class="search-header">
+    <div class="search-title">Tween parameters</div>
+    <div class="search-input-container">
+      <div class="search-icon"></div>
+      <input type="text" class="search-input" placeholder="Filter tween parameters...">
+    </div>
+  </div>
+
+  <div class="grid-section">
+    <div class="header-card active" onclick="selectItem('overview.html', this)">
+      <div class="header-dots"></div>
+      <div class="header-title">Tween parameters</div>
+    </div>
+
+    <div class="card-grid" style="margin-top: 40px;">
+      <div class="card" data-content="tween_parameters/test_duration_delay.html" onclick="selectItem('test_duration_delay.html', this)">
+        <div class="card-preview">
+          <div class="pill"><span class="dot orange"></span><span>duration + delay</span></div>
+        </div>
+        <div class="card-footer">duration / delay</div>
+      </div>
+
+      <div class="card" data-content="tween_parameters/test_endDelay_fill.html" onclick="selectItem('test_endDelay_fill.html', this)">
+        <div class="card-preview">
+          <div class="pill"><span class="dot"></span><span class="dot red"></span><span>endDelay + fill</span></div>
+        </div>
+        <div class="card-footer">endDelay / fill</div>
+      </div>
+
+      <div class="card" data-content="tween_parameters/test_direction_loop.html" onclick="selectItem('test_direction_loop.html', this)">
+        <div class="card-preview">
+          <div class="pill"><span class="dot orange"></span><span class="dot orange"></span><span>direction + loop</span></div>
+        </div>
+        <div class="card-footer">direction / loop</div>
+      </div>
+
+      <div class="card" data-content="tween_parameters/test_easing_named_and_bezier.html" onclick="selectItem('test_easing_named_and_bezier.html', this)">
+        <div class="card-preview">
+          <div class="pill"><span class="dot"></span><span>easing: named / cubic-bezier</span></div>
+        </div>
+        <div class="card-footer">easing(常用 + 贝塞尔)</div>
+      </div>
+
+      <div class="card" data-content="tween_parameters/test_easing_spring.html" onclick="selectItem('test_easing_spring.html', this)">
+        <div class="card-preview">
+          <div class="pill"><span class="dot orange"></span><span>easing: spring 🪀</span></div>
+        </div>
+        <div class="card-footer">easing(弹簧 spring)</div>
+      </div>
+    </div>
+  </div>
+
+  <script>
+    function selectItem(url, el) {
+      document.querySelectorAll('.card').forEach(c => c.classList.remove('active'));
+      document.querySelectorAll('.header-card').forEach(c => c.classList.remove('active'));
+      el.classList.add('active');
+      window.parent?.loadContent?.('tween_parameters/' + url);
+    }
+
+    function setActiveByContentUrl(contentUrl) {
+      const normalized = String(contentUrl || '').replace(/^\.\//, '');
+      const header = document.querySelector('.header-card');
+      const cards = Array.from(document.querySelectorAll('.card[data-content]'));
+
+      document.querySelectorAll('.card').forEach(c => c.classList.remove('active'));
+      document.querySelectorAll('.header-card').forEach(c => c.classList.remove('active'));
+      if (!normalized) return;
+
+      if (normalized.includes('tween_parameters/overview.html')) {
+        header?.classList.add('active');
+        return;
+      }
+      const match = cards.find(c => normalized.endsWith(c.dataset.content) || normalized.includes(c.dataset.content));
+      if (match) match.classList.add('active');
+    }
+
+    window.setActiveByContentUrl = setActiveByContentUrl;
+  </script>
+</body>
+</html>
+

+ 183 - 0
doc/tween_parameters/overview.html

@@ -0,0 +1,183 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Tween parameters - Overview</title>
+  <link rel="stylesheet" href="../demo.css">
+  <style>
+    .track {
+      position: relative;
+      height: 1px;
+      background: #262626;
+      margin: 18px 0 8px;
+    }
+    .lane {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      gap: 14px;
+      padding: 14px 0;
+      border-bottom: 1px solid #202020;
+    }
+    .lane:last-child { border-bottom: none; }
+    .dot {
+      width: 16px;
+      height: 16px;
+      border-radius: 4px;
+      background: var(--highlight-color);
+      will-change: transform;
+    }
+    .dot.orange { background: var(--accent-color); }
+    .dot.gray { background: #666; }
+    .meta {
+      width: 240px;
+      min-width: 240px;
+      color: #bdbdbd;
+      font-size: 13px;
+      line-height: 1.35;
+    }
+    .meta strong { color: #fff; font-weight: 700; }
+    .meta code { font-size: 12px; }
+  </style>
+</head>
+<body>
+  <div class="container">
+    <div class="page-top">
+      <div class="crumb">ANIMATION › TWEEN PARAMETERS</div>
+      <div class="since">SINCE 1.0.0</div>
+    </div>
+
+    <h1>Tween parameters</h1>
+    <p class="description">
+      这组参数决定动画<strong>怎么播放</strong>:多久开始、跑多快、怎么缓动、循环几次、结束时停在哪…<br>
+      在 <code class="inline">xjs(target).animate({...})</code> 里,它们跟“要动的属性”写在同一个对象里。
+    </p>
+
+    <h2>核心参数一览</h2>
+    <p>
+      支持的常用项:<code class="inline">duration</code>、<code class="inline">delay</code>、<code class="inline">endDelay</code>、
+      <code class="inline">easing</code>、<code class="inline">direction</code>、<code class="inline">loop</code>、
+      <code class="inline">autoplay</code>、<code class="inline">fill</code>。
+      其中 <code class="inline">easing</code> 还支持 <strong>spring</strong>(对象形式)。
+    </p>
+
+    <div class="box-container">
+      <div class="box-header">
+        <div class="box-title">Quick overview</div>
+        <div class="box-right">
+          <button class="icon-btn" type="button" title="Copy" onclick="DocPage.copyActiveCode()" aria-label="Copy code">
+            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
+              <rect x="9" y="9" width="13" height="13" rx="2"></rect>
+              <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
+            </svg>
+          </button>
+          <div class="tabs" role="tablist" aria-label="Code tabs">
+            <div class="tab active" role="tab" aria-selected="true" tabindex="0" onclick="DocPage.switchTab('js')">JavaScript</div>
+            <div class="tab" role="tab" aria-selected="false" tabindex="-1" onclick="DocPage.switchTab('html')">HTML</div>
+            <div class="tab" role="tab" aria-selected="false" tabindex="-1" onclick="DocPage.switchTab('css')">CSS</div>
+          </div>
+        </div>
+      </div>
+
+      <div id="js-code" class="code-view active">
+<span class="fun">xjs</span><span class="punc">(</span><span class="str">'.dot'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span>
+  x<span class="punc">:</span> <span class="str">'18rem'</span><span class="punc">,</span>
+  duration<span class="punc">:</span> <span class="num">900</span><span class="punc">,</span>
+  delay<span class="punc">:</span> <span class="num">120</span><span class="punc">,</span>
+  easing<span class="punc">:</span> <span class="str">'ease-out'</span><span class="punc">,</span>
+  direction<span class="punc">:</span> <span class="str">'alternate'</span><span class="punc">,</span>
+  loop<span class="punc">:</span> <span class="num">2</span><span class="punc">,</span>
+  endDelay<span class="punc">:</span> <span class="num">200</span><span class="punc">,</span>
+  fill<span class="punc">:</span> <span class="str">'forwards'</span>
+<span class="punc">}</span><span class="punc">)</span><span class="punc">;</span>
+      </div>
+
+      <div id="html-code" class="html-view">
+<span class="tag">&lt;div</span> <span class="attr">class</span>=<span class="val">"lane"</span><span class="tag">&gt;</span>
+  <span class="tag">&lt;div</span> <span class="attr">class</span>=<span class="val">"meta"</span><span class="tag">&gt;</span>...<span class="tag">&lt;/div&gt;</span>
+  <span class="tag">&lt;div</span> <span class="attr">class</span>=<span class="val">"dot"</span><span class="tag">&gt;&lt;/div&gt;</span>
+<span class="tag">&lt;/div&gt;</span>
+      </div>
+
+      <pre id="css-code" class="css-view">.dot { width: 16px; height: 16px; border-radius: 4px; background: var(--highlight-color); }
+.dot.orange { background: var(--accent-color); }
+.dot.gray { background: #666; }</pre>
+
+      <div class="feature-desc">
+        <strong>这组最想让你记住的点:</strong>
+        <br>1) 参数和属性写在同一个对象里(上面代码里 <code class="inline">x</code> 是属性,其余是播放参数)。
+        <br>2) 不会写也没关系:先只写 <code class="inline">duration</code> + <code class="inline">easing</code>,就已经很好玩了。
+        <br>3) 想要“弹簧手感”?把 <code class="inline">easing</code> 写成对象即可(后面有 spring 专门页面)。
+      </div>
+
+      <div class="demo-visual">
+        <div class="lane">
+          <div class="meta"><strong>默认</strong><br><code class="inline">duration: 900</code> <code class="inline">ease-out</code></div>
+          <div class="dot"></div>
+        </div>
+        <div class="lane">
+          <div class="meta"><strong>更慢</strong><br><code class="inline">duration: 1600</code> <code class="inline">ease-in-out</code></div>
+          <div class="dot orange"></div>
+        </div>
+        <div class="lane">
+          <div class="meta"><strong>更“复读机”</strong><br><code class="inline">loop: 3</code> <code class="inline">alternate</code></div>
+          <div class="dot gray"></div>
+        </div>
+        <div class="track" aria-hidden="true"></div>
+      </div>
+
+      <div class="action-bar">
+        <button class="play-btn" onclick="runDemo()">REPLAY</button>
+      </div>
+    </div>
+
+    <div class="doc-nav" aria-label="Previous and next navigation">
+      <a href="#" onclick="goPrev(); return false;">
+        <span>
+          <span class="nav-label">Previous</span><br>
+          <span id="prev-title" class="nav-title">—</span>
+        </span>
+        <span aria-hidden="true">←</span>
+      </a>
+      <div id="nav-center" class="nav-center">Tween parameters</div>
+      <a href="#" onclick="goNext(); return false;">
+        <span>
+          <span class="nav-label">Next</span><br>
+          <span id="next-title" class="nav-title">—</span>
+        </span>
+        <span aria-hidden="true">→</span>
+      </a>
+    </div>
+  </div>
+
+  <div id="toast" class="toast" role="status" aria-live="polite"></div>
+
+  <script src="../page_utils.js"></script>
+  <script src="../../xjs.js"></script>
+  <script>
+    const CURRENT = 'tween_parameters/overview.html';
+
+    function resetDots() {
+      document.querySelectorAll('.dot').forEach(el => el.style.transform = 'none');
+    }
+
+    function runDemo() {
+      resetDots();
+      const dots = document.querySelectorAll('.dot');
+      xjs(dots[0]).animate({ x: '18rem', duration: 900, easing: 'ease-out' });
+      xjs(dots[1]).animate({ x: '18rem', duration: 1600, easing: 'ease-in-out' });
+      xjs(dots[2]).animate({ x: '18rem', duration: 700, easing: 'linear', loop: 3, direction: 'alternate' });
+    }
+
+    function goPrev() { window.parent?.docNavigatePrev?.(CURRENT); }
+    function goNext() { window.parent?.docNavigateNext?.(CURRENT); }
+
+    DocPage.enableTabKeyboardNav();
+    DocPage.syncMiddleActive(CURRENT);
+    DocPage.syncPrevNext(CURRENT);
+    setTimeout(runDemo, 300);
+  </script>
+</body>
+</html>
+

+ 249 - 0
doc/tween_parameters/test_duration_delay.html

@@ -0,0 +1,249 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>duration / delay - Animal.js</title>
+  <link rel="stylesheet" href="../demo.css">
+  <style>
+    .row {
+      display: grid;
+      grid-template-columns: 1fr 280px;
+      gap: 18px;
+      align-items: center;
+      padding: 18px 0;
+      border-bottom: 1px solid #222;
+    }
+    .row:last-child { border-bottom: none; }
+    .hint {
+      color: #9a9a9a;
+      font-size: 12px;
+      line-height: 1.65;
+      margin-top: -8px;
+      margin-bottom: 16px;
+    }
+    .dot {
+      width: 16px;
+      height: 16px;
+      border-radius: 4px;
+      background: var(--accent-color);
+      will-change: transform;
+    }
+    .track {
+      position: relative;
+      height: 1px;
+      background: #262626;
+      margin: 18px 0 8px;
+    }
+    .panel {
+      background: rgba(255,255,255,0.02);
+      border: 1px solid #262626;
+      border-radius: 10px;
+      padding: 14px 14px 10px;
+    }
+    .control {
+      display: grid;
+      grid-template-columns: 90px 1fr 64px;
+      gap: 10px;
+      align-items: center;
+      margin-bottom: 10px;
+    }
+    .control label { color: #bdbdbd; font-size: 12px; font-weight: 700; }
+    .control output { color: #e6e6e6; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12px; }
+    input[type="range"] { width: 100%; }
+    .badge {
+      display: inline-flex;
+      gap: 8px;
+      align-items: center;
+      padding: 4px 10px;
+      border-radius: 999px;
+      border: 1px solid #2a2a2a;
+      background: rgba(255,255,255,0.02);
+      color: #cfcfcf;
+      font-size: 12px;
+      font-weight: 700;
+    }
+    .blink {
+      width: 8px;
+      height: 8px;
+      border-radius: 50%;
+      background: #444;
+    }
+    .blink.on { background: var(--highlight-color); box-shadow: 0 0 0 4px rgba(255,75,75,0.12); }
+  </style>
+</head>
+<body>
+  <div class="container">
+    <div class="page-top">
+      <div class="crumb">ANIMATION › TWEEN PARAMETERS</div>
+      <div class="since">SINCE 1.0.0</div>
+    </div>
+
+    <h1>duration / delay</h1>
+    <p class="description">让动画<strong>快一点/慢一点</strong>,以及<strong>晚一点才开始</strong>。</p>
+    <p class="hint">
+      <strong>小窍门:</strong><code class="inline">delay</code> 只影响“开始时间”,不会改变动画跑完所需的 <code class="inline">duration</code>。
+      你可以把 delay 当成“起跑前的倒计时”。
+    </p>
+
+    <div class="box-container">
+      <div class="box-header">
+        <div class="box-title">duration / delay code example</div>
+        <div class="box-right">
+          <button class="icon-btn" type="button" title="Copy" onclick="DocPage.copyActiveCode()" aria-label="Copy code">
+            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
+              <rect x="9" y="9" width="13" height="13" rx="2"></rect>
+              <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
+            </svg>
+          </button>
+          <div class="tabs" role="tablist" aria-label="Code tabs">
+            <div class="tab active" role="tab" aria-selected="true" tabindex="0" onclick="DocPage.switchTab('js')">JavaScript</div>
+            <div class="tab" role="tab" aria-selected="false" tabindex="-1" onclick="DocPage.switchTab('html')">HTML</div>
+            <div class="tab" role="tab" aria-selected="false" tabindex="-1" onclick="DocPage.switchTab('css')">CSS</div>
+          </div>
+        </div>
+      </div>
+
+      <div id="js-code" class="code-view active">
+<span class="kwd">const</span> duration <span class="punc">=</span> <span class="num">900</span><span class="punc">;</span>
+<span class="kwd">const</span> delay <span class="punc">=</span> <span class="num">200</span><span class="punc">;</span>
+
+<span class="fun">xjs</span><span class="punc">(</span><span class="str">'.dot'</span><span class="punc">)</span>.<span class="fun">animate</span><span class="punc">({</span>
+  x<span class="punc">:</span> <span class="str">'18rem'</span><span class="punc">,</span>
+  duration<span class="punc">,</span>
+  delay<span class="punc">,</span>
+  easing<span class="punc">:</span> <span class="str">'ease-out'</span>
+<span class="punc">}</span><span class="punc">)</span><span class="punc">;</span>
+      </div>
+
+      <div id="html-code" class="html-view">
+<span class="tag">&lt;div</span> <span class="attr">class</span>=<span class="val">"dot"</span><span class="tag">&gt;&lt;/div&gt;</span>
+      </div>
+
+      <pre id="css-code" class="css-view">.dot { width: 16px; height: 16px; border-radius: 4px; background: var(--accent-color); }</pre>
+
+      <div class="feature-desc">
+        <strong>功能说明:</strong>
+        <br>- <code class="inline">duration</code>:动画跑完一次需要的时间(毫秒)。
+        <br>- <code class="inline">delay</code>:动画开始前等待的时间(毫秒)。
+        <br>你可以用 delay 做“队列编排”:比如多个元素按 0ms/80ms/160ms 依次启动。
+      </div>
+
+      <div class="demo-visual">
+        <div class="row">
+          <div>
+            <div class="badge"><span id="blink" class="blink"></span><span id="statusText">READY</span></div>
+            <div class="track" aria-hidden="true"></div>
+            <div class="dot" id="dot"></div>
+          </div>
+          <div class="panel">
+            <div class="control">
+              <label for="dur">duration</label>
+              <input id="dur" type="range" min="200" max="2400" step="50" value="900" />
+              <output id="durOut">900ms</output>
+            </div>
+            <div class="control">
+              <label for="del">delay</label>
+              <input id="del" type="range" min="0" max="1200" step="50" value="200" />
+              <output id="delOut">200ms</output>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="action-bar">
+        <button class="play-btn" onclick="runDemo()">REPLAY</button>
+        <button class="play-btn secondary" onclick="randomize()">SURPRISE</button>
+      </div>
+    </div>
+
+    <div class="doc-nav" aria-label="Previous and next navigation">
+      <a href="#" onclick="goPrev(); return false;">
+        <span>
+          <span class="nav-label">Previous</span><br>
+          <span id="prev-title" class="nav-title">—</span>
+        </span>
+        <span aria-hidden="true">←</span>
+      </a>
+      <div id="nav-center" class="nav-center">Tween parameters</div>
+      <a href="#" onclick="goNext(); return false;">
+        <span>
+          <span class="nav-label">Next</span><br>
+          <span id="next-title" class="nav-title">—</span>
+        </span>
+        <span aria-hidden="true">→</span>
+      </a>
+    </div>
+  </div>
+
+  <div id="toast" class="toast" role="status" aria-live="polite"></div>
+
+  <script src="../page_utils.js"></script>
+  <script src="../../xjs.js"></script>
+  <script>
+    const CURRENT = 'tween_parameters/test_duration_delay.html';
+    let __ctl = null;
+
+    const elDot = document.getElementById('dot');
+    const elDur = document.getElementById('dur');
+    const elDel = document.getElementById('del');
+    const elDurOut = document.getElementById('durOut');
+    const elDelOut = document.getElementById('delOut');
+    const elBlink = document.getElementById('blink');
+    const elStatus = document.getElementById('statusText');
+
+    function read() {
+      const duration = Number(elDur.value);
+      const delay = Number(elDel.value);
+      elDurOut.textContent = duration + 'ms';
+      elDelOut.textContent = delay + 'ms';
+      return { duration, delay };
+    }
+
+    function setStatus(s, on) {
+      elStatus.textContent = s;
+      elBlink.classList.toggle('on', !!on);
+    }
+
+    function reset() {
+      if (elDot) elDot.style.transform = 'none';
+      try { __ctl?.cancel?.(); } catch {}
+      __ctl = null;
+      setStatus('READY', false);
+    }
+
+    function runDemo() {
+      reset();
+      const { duration, delay } = read();
+      setStatus(delay ? ('WAIT ' + delay + 'ms…') : 'GO!', true);
+      __ctl = xjs(elDot).animate({
+        x: '18rem',
+        duration,
+        delay,
+        easing: 'ease-out',
+        begin: () => setStatus('RUNNING', true),
+        complete: () => setStatus('DONE', false)
+      });
+    }
+
+    function randomize() {
+      elDur.value = String(200 + Math.floor(Math.random() * 45) * 50);
+      elDel.value = String(Math.floor(Math.random() * 25) * 50);
+      read();
+      runDemo();
+    }
+
+    elDur.addEventListener('input', () => { read(); });
+    elDel.addEventListener('input', () => { read(); });
+
+    function goPrev() { window.parent?.docNavigatePrev?.(CURRENT); }
+    function goNext() { window.parent?.docNavigateNext?.(CURRENT); }
+
+    DocPage.enableTabKeyboardNav();
+    DocPage.syncMiddleActive(CURRENT);
+    DocPage.syncPrevNext(CURRENT);
+    setTimeout(runDemo, 300);
+  </script>
+</body>
+</html>
+