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>
下面的代码为 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>