juliangarnier / anime

JavaScript animation engine
https://animejs.com
MIT License
49.65k stars 3.66k forks source link

querySelectorAll() in external SVG breaks [NodeList] for attribute 'targets:' #548

Open hirschferkel opened 5 years ago

hirschferkel commented 5 years ago

I'm trying to animate all within an SVG with anime.js.

As long as the SVG is inline, I can call a querySelectorAll() or getElementsByTagName() and take this nodelist as the attribute for targets: and all selected elements will be animated.

elements = document.querySelectorAll("rect");

inside anime code

targets: elements,

BUT: When I link the same SVG code to an external SVG (embeded with an element), anime.js stopps working (only FireFox will work,still) when I set


elements2 = document.getElementById("SVG").contentDocument.querySelectorAll("rect");

targets: elements2,

When I get single elements for the targets: attribute like

targets: elements2[0|, anime.js will animate the single element.

A function will not work, too.

targets: function(i){return elements2[i];},

How can I get the elements as targets in an external SVG in Edge/IE/Safari/Chrome to be animated with anime.js?

https://stackoverflow.com/questions/54970635/anime-js-queryselectorall-in-external-svg-breaks-nodelist-for-attribute-tar/54971795#54971795

hirschferkel commented 5 years ago

So the only workaround for anime.js and an external SVG seems to be, to collect all elements in a new array and use this as targets: attribute.

var elements3 = new Array();

elements2 = document.getElementById("SVG").contentDocument.querySelectorAll("rect");

for (i = 0; i<elements2.length; i++) {
    elements3.push(elements2[i]);
}

targets: elements3,

But I do not understand why this is necessary...

elliottregan commented 5 years ago

@hirschferkel You can do this with ES6 shorthand like this:

const elements2 = document.getElementById("SVG").contentDocument.querySelectorAll("rect");
const elements3 = [...elements2]
hirschferkel commented 5 years ago

Oh your'e right and it's a kind of coincidence as I really learned that kind of shorthand half an hour ago...

Thanks

@hirschferkel You can do this with ES6 shorthand like this:

const elements2 = document.getElementById("SVG").contentDocument.querySelectorAll("rect");
const elements3 = [...elements2]
rafaelramalho19 commented 4 years ago

Any progress on this? Spreading the NodeList doesn't seem to work anymore. You actually have to do something like

const elements = [];
nodeListElements.forEach(nodeListElement => elements.push(nodeListElement));

EDIT: I can actually do Array.from(letters)

Is the problem related with iterables? 🤔

hirschferkel commented 4 years ago

You could try d3, it's more focused on animating selections.

mbforbes commented 3 years ago

I ran into this issue as well, and devoted a chunk of this Sunday to figuring it out. I think I know where the problem lies, and why the workaround works.

Setup

Your webpage with an external SVG:

<object id="my-diagram" type="image/svg+xml" data="picture.svg"></object>

Just for completeness, say that picture.svg has a group with two <rect>s in it. (Doesn't matter, just the example I tested with.) So it looks roughly like:

<svg xmlns="http://www.w3.org/2000/svg">
    <g id="fun-group">
        <rect ...>
        <rect ...>
   </g>
</svg>

Observed Problem

The surface-level problem is that anime.js cannot query from an external SVG, because the SVG gets put inside its own document within the <object> element. This is reasonable. So, to work around that, as beautifully illustrated by @hirschferkel, we get the SVG's document and query within it:

// Wait until SVG has loaded. Otherwise, the document will be empty.
document.getElementById('my-diagram').addEventListener("load", function() {
    const innerDoc = document.getElementById('my-diagram').contentDocument;
    const targets = innerDoc.querySelectorAll("#fun-group *");
    // Now, we can send these targets to anime.js
};

If we inspect targets, we see they are of type NodeList, a type that's supported by anime.js.

image

However, we now encounter the deeper problem: passing in these targets does not work. No animation happens.

Root Issue

I think that the root of the issue is in the toArray() function, where our targets fail the check o instanceof NodeList:

https://github.com/juliangarnier/anime/blob/3ebfd913a04f7dc59cc3d52e38275272a5a12ae6/src/index.js#L303-L308

You can verify this yourself in a page's javascript REPL. If we check our targets above, targets instanceof NodeList evaluates to false.

image

I have not dug into why this happens. A wild guess would be that Javascript has namespaces at the document-level.

Effects

Here I'll explain why I think the above is the root issue.

Since toArray() doesn't recognize our targets (there o) as a NodeList, it wraps them in an array, returning [o]. This puts anime.js in an unexpected state: now the targets are a nested iterable.

toArray() was called from parseTargets(), which was called from getAnimatables(). So now, in the createNewInstance() function, we have set our animatables to [NodeList] (i.e., an array of length 1, containing the NodeList).

Where this fails is when we try to get our animations. Plugging along in createNewInstance(), we call getAnimations(), which tries to call createAnimation() using each individual thing that can be animated by looping over the animatables that we passed in:

https://github.com/juliangarnier/anime/blob/3ebfd913a04f7dc59cc3d52e38275272a5a12ae6/src/index.js#L800-L806

But remember that animatables is an array containing a NodeList. So what gets passed into createAnimation() is a NodeList, when it's expecting a single element. createAnimation() calls getAnimationType(), which checks for every kind of el it can think of:

https://github.com/juliangarnier/anime/blob/3ebfd913a04f7dc59cc3d52e38275272a5a12ae6/src/index.js#L433-L438

Our poor NodeList fails all of these checks, and we reach the end of the function without a return value. So undefined is returned.

The createAnimation() function checks the result, and it fails the if (animType) check, so it also returns undefined. Etc. Pop back up to createNewInstance(), and our animations are empty. So nothing happens.

Why unpacking into our own array works

If we instead unpack the NodeList we got into our own array, this mitigates the root issue because the toArray() function correctly recognizes what we sent in as an array. All of the functions afterward are able to iterate over the selection properly.

Recommendation

I am new to this project, so I'm not really in the position to make a recommendation! But I struggled with this issue for days before digging in to solve it. I thought there was no way that I was the only one animating an external SVG. (After all, they are often huge---much cleaner to keep in a separate file!)

As such, I would humbly suggest that the documentation gives an example of how to work with an external SVG. This ends up being significantly more complicated than an inline SVG.

Here's a minimal example, first with inline SVG:

// inline svg
anime({
    targets: "#fun-group *",
    ...
});

To do the equivalent thing with an external SVG:

// external svg
// Wait for the <object> element to load.
document.addEventListener('DOMContentLoaded', function () {
    // Wait for the contents of the SVG document to load.
    document.getElementById('my-diagram').addEventListener("load", function() {
        // Grab the inner document and manually run our query.
        const innerDoc = document.getElementById('my-diagram').contentDocument;
        const targets = innerDoc.querySelectorAll("#fun-group *");
        anime({
            // Bug workaround: repackage targets as a vanilla array.
            targets: [...targets],
            ...
        });
    });
});

What do you think, @juliangarnier ? :-)

mbforbes commented 3 months ago

Hey @juliangarnier, I saw Anime v4 is in early access, huge congrats and great to hear! If you have a minute to spare, I'd love it if you might consider ergonomics of external SVG. Extensive writeup of rough edge in v3 in previous comment. Thank you for your hard work and the great library!