SVG 动画在加载时触发,而不是在 DOM 插入时触发

SVG animation triggering on load rather than on DOM insertion

下面的代码为 SVG 圆圈改变颜色设置动画,并按预期工作。

如果对 SVG.addAnimatedCircle(this.root) 的调用是从 callback 方法中进行的(而不是在下方,在 constructor 中),动画将在加载文档时开始— 因此不可见,除非单击 window — 而不是在触发事件时。

class SVG {

    constructor() {
        const root = document.createElementNS(
            'http://www.w3.org/2000/svg', 'svg');
        root.setAttribute('viewBox', '-50 -50 100 100');
        this.root = root;

        this.callback = this.callback.bind(this);
        window.addEventListener('click', this.callback);

        SVG.addAnimatedCircle(this.root);
    }

    callback() {
        // SVG.addAnimatedCircle(this.root);
    }

    static addAnimatedCircle(toElement) {
        const el = document.createElementNS(
            'http://www.w3.org/2000/svg', 'circle');
        el.setAttribute('cx', '0');
        el.setAttribute('cy', '0');
        el.setAttribute('r', '10');
        el.setAttribute('fill', 'red');

        toElement.appendChild(el);

        const anim = document.createElementNS(
            'http://www.w3.org/2000/svg', 'animate');
        anim.setAttribute('attributeName', 'fill');
        anim.setAttribute('from', 'blue');
        anim.setAttribute('to', 'red');
        anim.setAttribute('dur', '3s');

        el.appendChild(anim);  
    }   
    
}

const svg = new SVG();
document.body.appendChild(svg.root);

(上面当然不需要在class里面,我简化了一个比较复杂的class)。

这是为什么?动画不是应该在元素创建并添加到 DOM 时开始吗?

(不是答案,只是将问题呈现给问题 and/or 正确答案

<style>svg {outline: #0FF6 solid; outline-offset: -2px;}</style>

<table role="presentation" border><tr><td>

1. Static SVG with animated circle:

<td>

<svg viewBox="-50 -50 100 100" width="30" height="30">
 <circle r="40" fill="black">
  <animate begin="0.5s" fill="freeze" attributeName="fill" from="blue" to="red" dur="5s"></animate>
 </circle>
</svg> 

<tr><td>

2. Empty SVG, 

<button onclick='
 emptySVG.innerHTML = document.querySelector("circle").outerHTML;
'>Put similar animated circle into it</button>:

<td>

<svg id="emptySVG" viewBox="-50 -50 100 100" width="30" height="30">
 <!-- empty -->
</svg>

<tr><td>

3. <button onclick='
 sampleCell.innerHTML = document.querySelector("svg").outerHTML
'>Create new SVG with animated circle</button>:

<td id="sampleCell">
(here.)

</table>

<p>
<button onclick="location.reload()">Reload page</button>
<button onclick="
[...document.querySelectorAll('animate')].forEach(a=>{
  //a.setAttribute('begin','indefinite'); // does not seem to be necessary
  a.beginElement();
 })
">Reset all animations</button>

将动画圆放到第二个 SVG 中会产生与现有空 SVG 中已经过时的持续时间相对应的状态:它与第一个匹配,因此它要么同步 运行 秒,要么完成。目标是 运行 圆圈出现时的动画。

您创建的 <animate> 元素的 begin 属性将计算为 0s(因为未设置)。
这个 0s 值是相对于“文档开始时间”的,它本身在这个 HTML 文档中对应于根 <svg> 的当前时间。

这意味着如果您确实在其根 <svg> 元素位于 DOM 之后创建了这样一个 <animate> 元素,其动画状态将取决于根 <svg> 的持续时间。 =26=] 元素已经在 DOM:

const root = document.querySelector("svg");
const circles = document.querySelectorAll("circle");
const duration = 3;
// will fully animate
circles[0].append(makeAnimate());

// will produce only half of the animation
setTimeout(() => {
  circles[1].append(makeAnimate());
}, duration * 500);

// will not animate
setTimeout(() => {
  circles[2].append(makeAnimate());
}, duration * 1000);

function makeAnimate() {
  const anim = document.createElementNS("http://www.w3.org/2000/svg", "animate");
  anim.setAttribute("attributeName", "fill");
  anim.setAttribute("from", "blue");
  anim.setAttribute("to", "red");
  anim.setAttribute("fill", "freeze");
  anim.setAttribute("dur", duration + "s");
  return anim;
}
circle { fill: blue }
<svg height="60">
  <circle cx="30" cy="30" r="25"/>
  <circle cx="90" cy="30" r="25"/>
  <circle cx="150" cy="30" r="25"/>
</svg>
<p>left circle starts immediately, and fully animates</p>
<p>middle circle starts after <code>duration / 2</code> and matches the same position as left circle</p>
<p>right circle starts after <code>duration</code>, the animation is already completed by then, nothing "animates"</p>

我们可以通过 SVGSVGElement.setCurrentTime() 方法设置 <svg> 的当前时间。
因此,要创建一个 <animate> 在创建时启动,无论何时,我们都可以使用它,但是, 这也会影响所有其他 <animate>已经在 <svg>:

const root = document.querySelector("svg");
const circles = document.querySelectorAll("circle");
const duration = 3;

circles[0].append(makeAnimate());
root.setCurrentTime(0); // reset <animate> time

setTimeout(() => {
  circles[1].append(makeAnimate());
  root.setCurrentTime(0); // reset <animate> time
}, duration * 500);

setTimeout(() => {
  circles[2].append(makeAnimate());
  root.setCurrentTime(0); // reset <animate> time
}, duration * 1000);

function makeAnimate() {
  const anim = document.createElementNS("http://www.w3.org/2000/svg", "animate");
  anim.setAttribute("attributeName", "fill");
  anim.setAttribute("from", "blue");
  anim.setAttribute("to", "red");
  anim.setAttribute("fill", "freeze");
  anim.setAttribute("dur", duration + "s");
  return anim;
}
circle { fill: blue }
<svg height="60">
  <circle cx="30" cy="30" r="25"/>
  <circle cx="90" cy="30" r="25"/>
  <circle cx="150" cy="30" r="25"/>
</svg>

因此,虽然它可能对某些用户有用,但在大多数情况下,最好只设置 <animate>begin 属性。
幸运的是,我们还可以使用 SVGSVGElement.getCurrentTime() 方法获取当前时间。

const root = document.querySelector("svg");
const circles = document.querySelectorAll("circle");
const duration = 3;

circles[0].append(makeAnimate());

setTimeout(() => {
  circles[1].append(makeAnimate());
}, duration * 500);

setTimeout(() => {
  circles[2].append(makeAnimate());
}, duration * 1000);

function makeAnimate() {
  const anim = document.createElementNS("http://www.w3.org/2000/svg", "animate");
  anim.setAttribute("attributeName", "fill");
  anim.setAttribute("from", "blue");
  anim.setAttribute("to", "red");
  anim.setAttribute("fill", "freeze");
  anim.setAttribute("dur", duration + "s");
  // set the `begin` to "now"
  anim.setAttribute("begin", root.getCurrentTime() + "s");
  return anim;
}
circle { fill: blue }
<svg height="60">
  <circle cx="30" cy="30" r="25"/>
  <circle cx="90" cy="30" r="25"/>
  <circle cx="150" cy="30" r="25"/>
</svg>

但我们通常的做法是充分利用API,通过JS来控制,因为你已经开始使用JS了
为此,我们将 begin 属性设置为“不确定”,这样它就不会自动启动,然后我们调用 SVGAnimateElement (<animate>)'s beginElement() 方法,它会在我们需要时手动启动动画:

const root = document.querySelector("svg");
const circles = document.querySelectorAll("circle");
const duration = 3;

{
  const animate = makeAnimate();
  circles[0].appendChild(animate);
  animate.beginElement();
}

setTimeout(() => {
  const animate = makeAnimate();
  circles[1].appendChild(animate);
  animate.beginElement();
}, duration * 500);

setTimeout(() => {
  const animate = makeAnimate();
  circles[2].appendChild(animate);
  animate.beginElement();
}, duration * 1000);

function makeAnimate() {
  const anim = document.createElementNS("http://www.w3.org/2000/svg", "animate");
  anim.setAttribute("attributeName", "fill");
  anim.setAttribute("from", "blue");
  anim.setAttribute("to", "red");
  anim.setAttribute("fill", "freeze");
  anim.setAttribute("dur", duration + "s");
  // set the `begin` to "manual"
  anim.setAttribute("begin", "indefinite");
  return anim;
}
circle { fill: blue }
<svg height="60">
  <circle cx="30" cy="30" r="25"/>
  <circle cx="90" cy="30" r="25"/>
  <circle cx="150" cy="30" r="25"/>
</svg>