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时开始吗?

您创建的<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()方法设置<svg>的当前时间。
要创建一个<animate>从它创建的时候开始,不管它是什么时候,我们都可以使用这也会影响所有已经在<svg>中的其他<animate>:

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>

(不是答案,只是提出问题,以便可能适应问题和/或OP在评论中建议的适当答案。

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

最新更新