【问题标题】:SVG animation triggering on load rather than on DOM insertionSVG 动画在加载时触发,而不是在 DOM 插入时触发
【发布时间】:2021-11-20 20:32:06
【问题描述】:

下面的代码动画一个 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 时开始吗?

【问题讨论】:

  • 这取决于是否运行任何其他动画来移动时间线。
  • 不涉及其他动画。这就是为什么我将我的课程简化为一个最小的独立示例来演示这个问题。
  • 有趣的观察。我添加了带有静态 SVG 的简化测试用例,并在 SVG 节点“之后”立即从脚本触发警报:在我的 Firefox 中,它在模态下显示黑色非动画圆圈。 Chrome 会等到警报被解除以绘制(和动画)SVG 并且随后的警报暂停动画(与 Firefox 不同)。
  • 好吧,但这次不是问题,恰恰相反:我故意使用警报来推迟加载事件,并查看 SVG 在“加载前”/使用 main 时的外观和行为线程暂停。顺便说一句,运行测试用例几次,我得到了一次红色圆圈(没有动画的状态),所以这似乎是一个不错的竞争条件。 (我没有深入了解 JS 线程、HTML 渲染和 SVG SMIL 计时器如何/是否相互关联,只是想添加示例以供使用。)
  • 我没有逐渐加载的应用程序。问题比这简单得多。我的原始代码可以按原样复制和粘贴,并且可以清楚地说明问题,不需要额外的组件。然而,经过大量搜索,结果发现代码缺少一些关键的东西:anim.setAttribute('begin', 'indefinite') 延迟动画开始,document.querySelector('animate').beginElement()(在回调内部)。然后beginElement 方法在事件触发时触发动画。我应该把这个作为答案吗?

标签: javascript svg


【解决方案1】:

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

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

我们可以通过SVGSVGElement.setCurrentTime() 方法设置&lt;svg&gt; 的当前时间。
因此,要创建一个在创建时开始的&lt;animate&gt;,无论何时,我们都可以使用它,但是,这也会影响已经在@987654339 中的所有其他&lt;animate&gt; @

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>

因此,虽然它可能对某些用户有用,但在大多数情况下,最好只设置&lt;animate&gt;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 (&lt;animate&gt;)'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>

【讨论】:

  • 这确实是我在 cmets 中针对我的问题提到的解决方案,并建议将其写为答案。感谢您提供所有选项。
  • @Cirrocumulus 是的,我是在撰写此答案后才阅读您的评论,确实您找到了(我相信成为)最佳解决方案(beginElement())。
【解决方案2】:

(不是答案,只是提出问题以便可能适应问题和/或正确答案suggested by OP in their comment

<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 中已经过去的持续时间相对应的状态:它与第一个相匹配,因此它要么同步运行,要么已完成。目标是在圆圈出现时运行动画。

【讨论】:

    猜你喜欢
    • 2021-05-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-12-08
    • 1970-01-01
    • 2022-11-21
    相关资源
    最近更新 更多