Reputation: 1912
Its well know that we can pause svg animation with the method SVGElement.pauseAnimations() and we can also set the animation current time using the method SVGElement.setCurrentTime() -with fist argument the time in seconds.Everything work well but my question is, can you export the paused frame to a raster images -jpg, png .
Example (here we create a svg with animation and we pause the animation at time - 0.958s)
let svg = document.getElementById('testSvg');
svg.pauseAnimations();
svg.setCurrentTime(0.958); // keyframes times: 0s, 0.458s, 0.958s
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<svg id="testSvg" image-rendering="auto" baseProfile="basic" version="1.1" x="0px" y="0px" width="550" height="400" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Scene-1" overflow="visible" transform="translate(-56 -145.5)">
<g display="none" id="Layer3_0_FILL">
<path fill="#F00" stroke="none" d="M116.3 159.95L116.3 220.95 232.8 220.95 232.8 159.95 116.3 159.95Z" test="Scene 1"/>
<animate attributeName="display" repeatCount="indefinite" dur="1s" keyTimes="0;.958;1" values="none;inline;inline"/>
</g>
<g display="none" id="Layer2_0_FILL">
<path fill="#0F0" stroke="none" d="M116.3 159.95L116.3 220.95 232.8 220.95 232.8 159.95 116.3 159.95Z" test="Scene 1"/>
<animate attributeName="display" repeatCount="indefinite" dur="1s" keyTimes="0;.458;.958;1" values="none;inline;none;none"/>
</g>
<g id="Layer1_0_FILL">
<path fill="#0F0" stroke="none" d="M78.05 139.95L78.05 240.95 271 240.95 271 139.95 78.05 139.95Z" test="Scene 1"/>
<animate attributeName="display" repeatCount="indefinite" dur="1s" keyTimes="0;.458;1" values="inline;none;none"/>
</g>
</g>
</svg>
</body>
</html>
Now if we set the svg as a src to an image using XMLSerializer.
let image = document.createElement( "image" );
let xml = new XMLSerializer().serializeToString(svg);
let data = "data:image/svg+xml;base64," + btoa(xml);
image.src = data;
document.body.appendChild();
The image is set, but the animation is played as normal, and its now paused. So is there a way to pause the animation in an image tag. Or use the paused svg element and somehow export an raster image from it.
Upvotes: 3
Views: 1155
Reputation: 21811
The best I can think of is reading the animated attributes and setting them for a clone of the animated image.
This is not trivial. SMIL animation can target both XML and CSS attributes, and the syntax for both differs. In addition, you'll have to look carefully if the names of XML attributes match the property names. That may not always the case. Then, the basic process goes like this:
While it is stated nowhere that pausing the animation is done asynchronuously, I was suprised to find it seems to be. You have to delay the reading of the attributes, for example with a setTimeout
.
For CSS presentation attributes,
window.getComputedStyle(element)[attribute]
For XML attributes,
element[attribute].animVal.valueAsString || element[attribute].animVal
This has another complexity: SVG has a dedicated interface for handling units. When you fetch a value that, for example, is a length, you need to get the attribute string with animVal.valueAsString
. For number or string lists, or for transforms, it gets even more complicated, because these lists do not implement the Iterable interface.
Here is an example that works for the listed attributes. I've set attributeType
on the <animate>
elements for ease of identification, and an id on the targeted element, because you need to identify it both on the original (which is in paused animation state) and its clone (which is not). I've drawn to canvas, so you immediately have a raster image available.
const svg = document.getElementById('testSvg');
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext("2d");
svg.pauseAnimations();
svg.setCurrentTime(0.35);
setTimeout(() => {
// clone svg
const copy = svg.cloneNode(true);
// remove the animations from the clone
copy.querySelectorAll('animate').forEach(animate => animate.remove());
// query all animate elements on the original
svg.querySelectorAll('animate').forEach(animate => {
// target element in original
const target = animate.targetElement;
const attr = animate.getAttribute('attributeName');
const type = animate.getAttribute('attributeType');
// target element in copy
const copyTarget = copy.getElementById(target.id);
// differentiate attribute type
if (type === 'XML') {
const value = target[attr].animVal.valueAsString || target[attr].animVal;
copyTarget.setAttribute(attr, value);
} else if (type === 'CSS') {
const value = window.getComputedStyle(target)[attr];
copyTarget.style[attr] = value
}
});
svg.unpauseAnimations();
const xml = new XMLSerializer().serializeToString(copy);
const data = "data:image/svg+xml;base64," + btoa(xml);
const image = new Image();
// image load is asynchronuous
image.onload = () => ctx.drawImage(image, 0, 0);
image.src = data;
},0);
<svg id="testSvg" width="200" height="200">
<rect id="animationTarget" x="50" y="50" width="100" height="100"
rx="0" fill="red">
<animate attributeType="XML" attributeName="rx"
dur="1s" keyTimes="0;0.5;1" values="0;50;0" />
<animate attributeType="CSS" attributeName="fill"
dur="1s" keyTimes="0;0.25;0.75" values="red;green;red" calcMode="discrete" />
</rect>
</svg>
<canvas width="200" height="200"></canvas>
Upvotes: 4