首页 > 解决方案 > SVG 动画在加载时触发,而不是在 DOM 插入时触发

问题描述

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

如果调用SVG.addAnimatedCircle(this.root)是从callback方法内部(而不是它在下面的位置,在 内部constructor)进行的,则动画在文档加载时开始 - 因此除非单击窗口,否则它是不可见的 - 而不是在触发事件时。

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,我正在简化一个更复杂的类)。

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

标签: javascriptsvg

解决方案


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

这意味着如果你<animate>在它的根<svg>元素已经在 DOM 中之后创建这样的元素,它的动画状态将取决于根<svg>元素在 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>

我们可以通过<svg>它的方法设置 的当前时间SVGSVGElement.setCurrentTime()
因此,要创建一个<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属性设置为“indefinite”,这样它就不会自动启动,然后我们调用SVGAnimateElement ( <animate>) 的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>


推荐阅读