13.12.2018

Capturing SVG frames with advanced effects(update of capturing frames in complex SVG animations)

(2.2020) Update: method and article updated to support CSS animations. (And old SMIL animations are unsupported)

In the approach in the older article its shows up, it has some limitations. The old procedure works only for some specific SVG's SMIL animations.

New approach rely on CSS SVG animations with window.getComputedStyle.

The principle is, just copy the computed style from paused SVG animation to snapshotted SVG, like this:


/* pause for snapshot */
var sf, els = this.svg_root.getElementsByTagName('path');

for (sf = 0; sf < els.length; sf++) {
    els[sf].style.animationPlayState = "paused";
}

/* capture snapped svg xml string */
var xml_data = this.svg_root.outerHTML;

/* remove style elements (it may damage snapshotted svg and will be rewritten imidietly by inline styles) */
var sf, styleEls = svg_xml.getElementsByTagName('style');

for (sf = styleEls.length - 1; sf >= 0; sf--) {
    styleEls[sf].parentNode.removeChild(styleEls[sf]);
}

/* parse snapped svg frame as object DOM structure */
var svg_xml = (new DOMParser()).parseFromString(xml_data, "text/xml");

/* capture the processed style */
/* paths in original svg */
var pathsOrig = this.svg_root.getElementsByTagName('path');
/* paths in snapped svg */
var pathsSnap = svg_xml.getElementsByTagName('path');

for (var sf = 0; sf < pathsOrig.length; sf++) {
    var cStyle = getComputedStyle(pathsOrig[sf]);

    /* enforce computed styles state */
    ['d', 'fill', 'stroke', 'opacity'].forEach(function(cStyle) {
        pathsSnap[sf].style[stKey] = cStyle[stKey];
    });
}

/* save as blob with a help of XMLserializer (using object DOM structure) */
var svg_data = new Blob([(new XMLSerializer()).serializeToString(svg_xml)], {type: 'image/svg+xml'});

                

Here is the full updated code:
(and example below - working in Chrome)


/* uniformly named URL object */
var DOMURL = window.URL || window.webkitURL || window;

/* our snapshotting class */
function svg_snapshot(svg_ref, fps, seconds, correction) {

    var self = this;

    /* DOM object element */
    this.svg_ref = svg_ref;

    /* svg xml root */
    this.svg_root = svg_ref.contentDocument.documentElement;

    /* frames per second */
    this.fps = fps;

    /* total animation duration in seconds */
    this.seconds = seconds;

    /* frame msec correction (to fix getComputedStyle bug which shifts a little animation) */
    this.correction = correction;

    /* list of possible elements (it probably not complete) */
    this.elements = ['path', 'circle', 'g'];

    /* list of possible styles (it probably not complete) */
    this.styleKeys = ['d', 'fill', 'fill-opacity', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-misterlimit', 'stroke-opacity', 'stroke-width', 'opacity', 'transform'];

    this.svg_element_list = {};

    /* load specific element lists from svg */
    this.elements.forEach(function(elementName) {

        var sf, els = self.svg_root.getElementsByTagName(elementName);

        self.svg_element_list[elementName] = [];

        for (sf = 0; sf < els.length; sf++) {
            self.svg_element_list[elementName].push({
                "ref": els.item(sf),
                "style": null
            });
        }
    });

    this.toggle_pause = function(flag) {
        var self = this;

        /* pause and save computed style when paused */
        this.elements.forEach(function(elementName) {
            self.svg_element_list[elementName].forEach(function(c) {
                c["ref"].style.animationPlayState = flag ? "paused" : "";
                c["style"] = flag ? getComputedStyle(c["ref"]) : null;
            });
        });
    };

    this.toggle_pause(true);

    this.make_step = function(step) {

        /* pause for snapshot */
        this.toggle_pause(true);

        if (step > this.fps * this.seconds) {
            // animation ended
            return false;
        }

        /* save object reference */
        var self = this;

        /* capture snapped svg xml string */
        var xml_data = this.svg_root.outerHTML;

        /* parse snapped svg frame as object DOM structure */
        var svg_xml = (new DOMParser()).parseFromString(xml_data, "text/xml");

        /* remove style elements (it may damage snapshotted svg and will be rewritten imidietly by inline styles) */
        var sf, styleEls = svg_xml.getElementsByTagName('style');

        for (sf = styleEls.length - 1; sf >= 0; sf--) {
            styleEls[sf].parentNode.removeChild(styleEls[sf]);
        }

        /* loop trough possible elements and copy */
        this.elements.forEach(function(elementName) {

            /* capture the processed style */
            /* paths in snapped svg */
            var elsSnap = svg_xml.getElementsByTagName(elementName);

            for (var sf = 0; sf < self.svg_element_list[elementName].length; sf++) {
                var cStyle = self.svg_element_list[elementName][sf]["style"];

                /* enforce computed styles state */
                self.styleKeys.forEach(function(stKey) {
                    elsSnap[sf].style[stKey] = cStyle.getPropertyValue(stKey);
                });
            }
        });

        /* save as blob with a help of XMLserializer (using object DOM structure) */
        var svg_data = new Blob([(new XMLSerializer()).serializeToString(svg_xml)], {type: 'image/svg+xml'});

        /* create data url (creates browsers interal blob: data link) */
        var data_url = DOMURL.createObjectURL(svg_data);

        /* create bitmap */
        var img = new Image();

        /* mount load process */
        img.onload = function() {

            self.make_step_next(step, this);

        };

        /* set image url */
        img.src = data_url;

    };

    this.make_step_next = function(step, img) {
        var self = this;

        /* create canvas */
        var canvas = document.createElement("canvas");

        canvas.setAttribute("width", this.svg_ref.clientWidth);
        canvas.setAttribute("height", this.svg_ref.clientHeight);

        canvas.style.border = "1px solid black";

        /* get canvas 2d contextr */
        var ctx = canvas.getContext('2d');

        /* drav loaded image onto it */
        ctx.drawImage(img, 0, 0);

        /* here we can get dataURL (base64 encoded url with image content) */
        var dataURL = canvas.toDataURL('image/png');

        /* and here you can do whatever you want - send image by ajax (that base64 encoded url which you can decode on serverside) or draw somewhere on page */
        var finalImg = document.createElement("IMG");

        finalImg.style.border = "1px solid black";

        /* add frame number to image alt */
        /* it might represent file name etc. */
        var frameId = "000" + step;
        finalImg.alt = "frame" + frameId.substr(frameId.length - 3);

        finalImg.onload = function() {
            document.getElementById("image_stack").appendChild(finalImg);

            self.make_step_go(step + 1);
        };

        finalImg.src = dataURL;
    };

    this.make_step_go = function(step) {
        var self = this;
        var interval = 1000 / this.fps - this.correction; // one frame interval

        setTimeout(function() {
            self.make_step(step);
        }, interval);

        /* let animation continue - before image is loaded, the animation is paused - by this is achieved perfect timing in this serial process */
        this.toggle_pause(false);
    };
}

/* usage - parameters: SVG DOM ref, frames per second, duration in seconds */
var item_ref = new svg_snapshot(document.getElementById('id_of_some_svg'), 30, 1, 3);

/* start snapshotting */
item_ref.make_step(0, 0);

                

Note: the window.getComputedStyle method has a bug or unexpected behavior which somehow moves animation forward with each read of property value. I did not find any other way how to prevent this other than put some correction to the time interval - which will be specific for each different SVG.

The code might not work for all SVG animations - but some cases may be solved easily by updating this.styleKeys and this.elements properties.

And here some demonstration (see the snapped flashes):



© 2021 Dzejkob games | info@dzejkobgames.eu