Browse Source

更多svg动画

robert 2 ngày trước cách đây
mục cha
commit
fc59971115

+ 12 - 0
doc/svg/list.html

@@ -114,6 +114,18 @@
         <div class="card-footer">SVG 属性动画(cx / r / …)</div>
       </div>
 
+      <div class="card" data-content="svg/test_mask_reveal.html" onclick="selectItem('test_mask_reveal.html', this)">
+        <div class="card-preview">
+          <div class="mini" aria-hidden="true">
+            <svg viewBox="0 0 120 60">
+               <rect width="100%" height="100%" fill="#333" />
+               <circle cx="60" cy="30" r="15" fill="#000" />
+            </svg>
+          </div>
+        </div>
+        <div class="card-footer">SVG Mask 遮罩动画</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">

+ 5 - 36
doc/svg/test_draw_path.html

@@ -3,7 +3,7 @@
 <head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
-  <title>SVG draw() - Animal.js</title>
+  <title>SVG Draw Path - Animal.js</title>
   <link rel="stylesheet" href="../demo.css">
   <style>
     .demo-visual {
@@ -54,13 +54,6 @@
       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>
@@ -72,10 +65,6 @@
 
     <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">
@@ -116,12 +105,6 @@
 
       <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>
@@ -145,26 +128,14 @@
       </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 class="doc-nav">
+      <a href="#" onclick="goPrev(); return false;">← Previous</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>
+      <a href="#" onclick="goNext(); return false;">Next →</a>
     </div>
   </div>
 
-  <div id="toast" class="toast" role="status" aria-live="polite"></div>
+  <div id="toast" class="toast"></div>
 
   <script src="../page_utils.js"></script>
   <script src="../../xjs.js"></script>
@@ -172,7 +143,6 @@
     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 = '';
@@ -201,4 +171,3 @@
   </script>
 </body>
 </html>
-

+ 104 - 0
doc/svg/test_mask_reveal.html

@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>SVG Mask Reveal - Animal.js</title>
+  <link rel="stylesheet" href="../demo.css">
+  <style>
+    .demo-visual {
+      padding: 26px 30px;
+      background: #151515;
+      display: grid;
+      grid-template-columns: 1fr 280px;
+      gap: 16px;
+    }
+    .stage {
+      background: #000;
+      border: 1px solid #262626;
+      border-radius: 12px;
+      padding: 0;
+      overflow: hidden;
+      min-height: 240px;
+      display: grid;
+      place-items: center;
+      position: relative;
+    }
+    .panel {
+      background: rgba(0,0,0,0.18);
+      border: 1px solid #262626;
+      border-radius: 12px;
+      padding: 14px;
+      display: flex;
+      flex-direction: column;
+      gap: 12px;
+    }
+    svg { width: 100%; height: 100%; position: absolute; top:0; left:0; }
+    
+    .bg-image {
+      width: 100%; height: 100%;
+      object-fit: cover;
+      opacity: 0.5;
+    }
+    .reveal-text {
+      fill: #fff;
+      font-size: 40px;
+      font-weight: 900;
+      text-anchor: middle;
+      letter-spacing: 4px;
+    }
+  </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 Mask Reveal</h1>
+    <p class="description">使用 SVG <code class="inline">&lt;mask&gt;</code> 制作遮罩动画。可以控制遮罩内的图形形状、位置、大小来揭示内容。</p>
+
+    <div class="box-container">
+      <div class="demo-visual">
+        <div class="stage">
+          <img src="https://images.unsplash.com/photo-1492144534655-ae79c964c9d7?auto=format&fit=crop&w=800&q=80" class="bg-image" alt="Background">
+          
+          <svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice">
+            <defs>
+              <mask id="myMask">
+                <rect width="100%" height="100%" fill="white" />
+                <circle id="maskCircle" cx="200" cy="150" r="0" fill="black" />
+              </mask>
+            </defs>
+            <rect width="100%" height="100%" fill="black" mask="url(#myMask)" opacity="0.8" />
+            <text x="200" y="165" class="reveal-text" mask="url(#myMask)">REVEAL</text>
+          </svg>
+        </div>
+        <div class="panel">
+           <button class="play-btn" onclick="runDemo()">REPLAY</button>
+        </div>
+      </div>
+    </div>
+
+    <script src="../page_utils.js"></script>
+    <script src="../../xjs.js"></script>
+    <script>
+      function runDemo() {
+        // Reset
+        xjs('#maskCircle').animate({ r: 0, duration: 0 });
+        
+        // Animate radius to reveal
+        xjs('#maskCircle').animate({
+          r: [0, 300],
+          duration: 2000,
+          easing: 'ease-in-out',
+          direction: 'alternate',
+          loop: true
+        });
+      }
+      setTimeout(runDemo, 320);
+    </script>
+  </div>
+</body>
+</html>

+ 3 - 14
doc/svg/test_morph_path.html

@@ -126,8 +126,7 @@
       <div class="since">SINCE 1.0.0</div>
     </div>
 
-    <h1>变形动画:path 的 <code class="inline">d</code> (DEBUG MODE)</h1>
-    <div style="background:#330; padding:10px; border-radius:4px; font-family:monospace; margin-bottom:10px;" id="debug-panel">Waiting for debug info...</div>
+    <h1>变形动画:path 的 <code class="inline">d</code></h1>
     <p class="description">
       这就是你想要的“形状变形”。在本库里,只要两段 <code class="inline">d</code> 字符串<strong>结构一致</strong>(命令序列相同,数字数量相同),
       就能直接插值,做出平滑 morph。
@@ -244,20 +243,10 @@
   <div id="toast" class="toast" role="status" aria-live="polite"></div>
 
   <script src="../page_utils.js?v=1"></script>
-  <script src="../../xjs.js?v=debug_fix_2"></script>
+  <script src="../../xjs.js?v=2"></script>
   <script>
     // Debug helper
-    window.addEventListener('load', () => {
-        const m = document.getElementById('m');
-        const isSVGInstance = typeof SVGElement !== 'undefined' && m instanceof SVGElement;
-        const ns = m.namespaceURI;
-        const info = `[Debug] isSVGElement:${isSVGInstance}, ns:${ns}`;
-        console.log(info);
-        const pText = document.getElementById('pText');
-        const debugPanel = document.getElementById('debug-panel');
-        if (debugPanel) debugPanel.textContent = info;
-        if (pText) pText.setAttribute('title', info); 
-    });
+    /* window.addEventListener('load', () => { ... }) */
   </script>
   <script>
     const CURRENT = 'svg/test_morph_path.html';

+ 23 - 226
doc/svg/test_motion_path.html

@@ -6,20 +6,12 @@
   <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);
@@ -27,7 +19,6 @@
       border-radius: 12px;
       padding: 14px;
       overflow: hidden;
-      position: relative;
       min-height: 220px;
       display: grid;
       place-items: center;
@@ -40,40 +31,7 @@
       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;
@@ -82,18 +40,10 @@
       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>
@@ -105,197 +55,44 @@
 
     <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">
+          <svg viewBox="0 0 320 180">
             <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>
+          <button class="play-btn" onclick="runDemo()">REPLAY</button>
         </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>
+    <script src="../page_utils.js"></script>
+    <script src="../../xjs.js"></script>
+    <script>
+      const path = document.getElementById('track');
+      const runner = document.getElementById('runner');
+      
+      function runDemo() {
+        const len = path.getTotalLength();
+        xjs({ p: 0 }).animate({
+          p: [0, 1],
+          duration: 1400,
+          easing: 'ease-in-out',
+          update: ({ progress }) => {
+            const pt = path.getPointAtLength(len * progress);
+            runner.setAttribute('transform', `translate(${pt.x} ${pt.y})`);
+          }
+        });
+      }
+      
+      setTimeout(runDemo, 320);
+    </script>
   </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>
-

+ 46 - 179
doc/svg/test_svg_attributes.html

@@ -6,20 +6,12 @@
   <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);
@@ -27,7 +19,6 @@
       border-radius: 12px;
       padding: 14px;
       overflow: hidden;
-      position: relative;
       min-height: 240px;
       display: grid;
       place-items: center;
@@ -40,28 +31,17 @@
       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;
+    .circle {
+      fill: rgba(255,159,67,0.2);
+      stroke: var(--accent-color);
+      stroke-width: 2;
+    }
+    .circle.blue {
+      fill: rgba(48,133,214,0.2);
+      stroke: #3085d6;
     }
-    .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>
@@ -71,178 +51,65 @@
       <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>
+    <h1>SVG 属性动画 (Attributes)</h1>
+    <p class="description">直接通过属性名动画 SVG 特有属性,如 <code class="inline">r</code>, <code class="inline">cx</code>, <code class="inline">cy</code>, <code class="inline">width</code>, <code class="inline">rx</code> 等。</p>
 
     <div class="box-container">
       <div class="box-header">
-        <div class="box-title">SVG attributes code example</div>
+        <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>
+             <!-- Code copy button omitted for brevity in this generator -->
         </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>
+  r<span class="punc">:</span> <span class="punc">[</span><span class="num">20</span><span class="punc">,</span> <span class="num">50</span><span class="punc">]</span><span class="punc">,</span>
+  cx<span class="punc">:</span> <span class="punc">[</span><span class="num">100</span><span class="punc">,</span> <span class="num">150</span><span class="punc">]</span><span class="punc">,</span>
+  duration<span class="punc">:</span> <span class="num">1000</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>
+  loop<span class="punc">:</span> <span class="kwd">true</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 viewBox="0 0 320 200">
+            <circle id="c1" class="circle" cx="100" cy="100" r="20" />
+            <circle id="c2" class="circle blue" cx="220" cy="100" r="30" />
           </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>
+           <button class="play-btn" onclick="runDemo()">REPLAY</button>
         </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>
+    <script src="../page_utils.js"></script>
+    <script src="../../xjs.js"></script>
+    <script>
+      function runDemo() {
+        xjs('#c1').animate({
+          r: [20, 50],
+          cx: [100, 160],
+          duration: 1200,
+          direction: 'alternate',
+          loop: true,
+          easing: 'ease-in-out'
+        });
+        
+        xjs('#c2').animate({
+          r: [30, 10],
+          cy: [100, 150],
+          duration: 1200,
+          direction: 'alternate',
+          loop: true,
+          easing: 'ease-in-out',
+          delay: 100
+        });
+      }
+      setTimeout(runDemo, 320);
+    </script>
   </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>
-

+ 0 - 19
xjs.js

@@ -1006,22 +1006,6 @@
     
     _writeValue(k, v) {
       const t = this.target;
-      
-      // Debug for 'd'
-      if (k === 'd') {
-          const isSvg = isSVG(t);
-          const isElType = isEl(t);
-          if (Math.random() < 0.05) {
-             console.log('[Animal.js] DEBUG: _writeValue d', { 
-                 isSvg, 
-                 isEl: isElType, 
-                 attr: toKebab(k), 
-                 valLen: v.length,
-                 hasSetAttribute: typeof t.setAttribute === 'function'
-             });
-          }
-      }
-
       // JS object
       if (!isEl(t) && !isSVG(t)) {
         t[k] = v;
@@ -1104,7 +1088,6 @@
         const a = parseNumberTemplate(fromStr);
         const b = parseNumberTemplate(toStr);
         if (a.nums.length >= 2 && a.nums.length === b.nums.length && a.parts.length === b.parts.length) {
-          if (k === 'd') console.log('[Animal.js] DEBUG: Building d tween. Num count:', a.nums.length);
           const aStrs = extractNumberStrings(fromStr);
           const bStrs = extractNumberStrings(toStr);
           const decs = a.nums.map((_, i) => Math.max(countDecimals(aStrs[i]), countDecimals(bStrs[i])));
@@ -1116,7 +1099,6 @@
            );
         }
 
-        if (k === 'd') console.log('[Animal.js] DEBUG: Fallback to discrete for d (mismatch or too short). a.nums:', a.nums.length, 'b.nums:', b.nums.length);
         // Non-numeric: fall back to "switch" (still useful for seek endpoints)
         tween[k] = { type: 'discrete', from: fromStr, to: toStr };
       });
@@ -1145,7 +1127,6 @@
             out += parts[i + 1] || '';
           }
           this._writeValue(k, out);
-          if (k === 'd' && Math.random() < 0.05) console.log('[Animal.js] DEBUG: writing d. Len:', out.length, 'Sample:', out.slice(0, 30));
         } else {
           const val = this._progress >= 1 ? t.to : t.from;
           this._writeValue(k, val);