HTML5 templates, bodymovin - Changing the json data using pre-compositions

Hello all,

I’m trying to make the switch from Flash templates to HTML5 templates.
After reading different topics on the forum, i have started with bodymovin to create the graphics.

At this moment, i succeeded in creating a flat template from bodymovin, and changing the values in the bodymovin .json file that i get from caspar using the function updateDocumentData.

But the only elements i can change are the elements that are in the main composition of After Effects. Now i would like to try and make a more difficult template where i work with pre-compositions in After Effects. How can i use updateDocumentData (or another function?) to change text values in deeper compositions.

       function functionUpdate() {
            anim.playSegments([450, 460], true);
            const animElement = anim.renderer.elements[1];
            console.log(anim.renderer.elements[1])
            animElement.updateDocumentData({t: 'Test Text'});
        }

        const animData = 'js/data_test.json';
        const animContainer = document.getElementById('lottie-container');
        const anim = lottie.loadAnimation({
            container: animContainer, // The dom element that will contain the animation
            renderer: 'svg',
            loop: false,
            autoplay: false,
            path: animData // Path to the animation json
        });

I know this is nearly 2 years old but I’m stuck on the same thing. Has anyone got a solution to this?

According to the BodyMovin / Lottie documentation you can not change data in pre-compositions. That is a limitation of this workflow. But I guess most of the stuff done with pre-compositions can also be done in a “flat” composition.

OK good (if a bit annoying) to know. Thanks.

Hi Neil not quite sure if I misunderstood your question but I normally make several pre comp in my templates since makes it easier also to deal with the template as “groups” and I have not issues on updating data on them. Important thing is the naming of the text instance and the “.” or whatever to help it to identify and iterate on the entire lottie export (JSON).
I repeat maybe I misunderstood the issue but using precomps as I do (Nested or not) I cannot see any problem on doing it.

Hi fdeigiudici that’s exactly what I’m getting stuck on. I’ve named the text instance with “.” but whatever I try I just can’t seem to get it to update. Is there a specific bit of code you’re using to update the field to access within the precomp?

'use strict';

/**************************************************************
const VERSION = "2020.12.21.0855";

//set ins and out of animation
var animSettings = {
    "play": {
        "in": 0,
        "out": 40
    },
    "update": {
        "in": 0,
        "out": 40
    },
    "stop": {
        "in": 40,
        "out": 0
    }
}

// lotti helpers
var animData = 'data';
var animContainer = document.getElementById('bm');
var anim = lottie.loadAnimation({
    container: animContainer, // The dom element that will contain the animation
    renderer: 'svg',
    loop: false,
    autoplay: false,
    path: 'data.json'
});
// flag for knowing if lotti has initialized
var DOMLoaded = false;
// flag for knowing it is first load, we will then not play the update animation!
var firstUpdate = true;

anim.addEventListener('DOMLoaded', function (e) {
    console.log('DOM loaded, ');
    DOMLoaded = true;
});

// the object getting the AMCP commands
const _graphic = (function () {

    (function () {
        // Runs after the rest of the parent function parses
        window['update'] = (raw) => update(raw);
        window['play'] = play;
        window['next'] = next;
        window['stop'] = stop;
        window['remove'] = remove;
        window['tick'] = tick;
        console.log("setup runnin on " + navigator.userAgent + " running version " + VERSION);
        window.document.title = "EQCT - " + VERSION;

        // start ticking
        setInterval(tick, 100);
    })();

    // last data received
    var loadedData = {};

    // function for updating, dont change this, template specific should be done in handleData or tick
    async function update(raw) {
        console.log("update", raw);
        let p = new Promise((resolve, reject) => {
            if (!DOMLoaded) {
                console.log("DOM NOT loaded, waiting 100ms");
                setTimeout(() => {
                    update(raw).then(resolve);
                }, 100);
            } else {
                console.log("DOM loaded, running update with " + raw);

                loadedData = JSON.parse(raw);

                applyData(loadedData);

                handleData(loadedData);

                if (!firstUpdate) {
                    anim.playSegments([animSettings.update.in, animSettings.update.out], true);
                } else {
                    firstUpdate = false;
                }

                resolve();
            }
        });
        await p;
        return p;
    }

    // function for playing, this will just do animation
    async function play() {
        console.log("play");
        let p = new Promise((resolve, reject) => {
            if (!DOMLoaded) {
                console.log("DOM NOT loaded, waiting 100ms");
                setTimeout(() => {
                    play().then(resolve);
                }, 100);
            } else {
                console.log("DOM loaded, running play");
                anim.playSegments([animSettings.play.in, animSettings.play.out], true);
                resolve();
            }
        });
        await p;
        return p;
    }

    // function for next, this will just do animation NB!!! this is not implemented
    async function next() {
        console.log("next");
        anim.playSegments([animSettings.next.in, animSettings.next.out], true);
    }

    // function for next, this will just do animation
    async function stop() {
        console.log("stop");
        anim.playSegments([animSettings.stop.in, animSettings.stop.out], true);
    }

    // ????
    async function remove() {
        console.log("remove");
    }

    function handleError(e) {
        console.error(e)
    }

    function handleWarning(w) {
        console.log(w)
    }


    // function to bind data to fields. It will use the ones named #examplename in aftereffect and if data has examplename text will change
    // if force is true it will fill even if animation is not running
    function applyData(data, forceupdate) {
        applyDataDeep(data, anim.renderer.elements);
        if (forceupdate) {
            for (let i in data) {
                let elm = document.getElementById(i);
                if (elm != null) {
                    elm.children[0].innerHTML = data[i];
                }
            }
        }
    }

    function applyDataDeep(data, list) {
        var animElementsLength = list.length;
        //console.log("Found " + animElementsLength + " items to check");
        for (let i = 0; i < animElementsLength; i++) {
            var animElement = list[i];
            //console.log("working on the one with data" + JSON.stringify(animElement.data));
            // Check the animation element has an id name and that the WebCG data has a key with the same id name
            //console.log("animElement.hasOwnProperty('data') " + (animElement.hasOwnProperty('data') ? "ja" : "nei"));
            //console.log("animElement.data.hasOwnProperty('ln')  " + (animElement.data.hasOwnProperty('ln') ? "ja" : "nei"));
            //console.log("data " + (data ? "ja" : "nei"));
            //console.log("data.hasOwnProperty(animElement.data.ln) " + (data.hasOwnProperty(animElement.data.ln) ? "ja" : "nei"));
            if (
                animElement.hasOwnProperty('data') && animElement.data.hasOwnProperty('ln') &&
                data && data.hasOwnProperty(animElement.data.ln)
            ) {
                var ln = animElement.data.ln;
                try {
                    //console.log("ln " + ln);
                    //console.log("has canResizeFont:" + (animElement.canResizeFont != undefined ? "ja" : "nei"));
                    if (animElement.canResizeFont) {
                        animElement.canResizeFont(true); // Let lottie resize text to fit the text box
                    }
                    //console.log("has updateDocumentData:" + (animElement.updateDocumentData != undefined ? "ja" : "nei"));
                    if (animElement.updateDocumentData) {
                        animElement.updateDocumentData({
                            t: data[ln] ? data[ln].text || data[ln] : ''
                        }, 0); // Update the text
                    }
                } catch (err) {
                    console.error(err);
                }
            }
            if (animElement.hasOwnProperty("elements")) {
                applyDataDeep(data, animElement.elements);
            }
        }
    }

    // function for specific code for template, you get the data from update/add in here
    function handleData(data) {
        // hide example

        if (!data.hasOwnProperty("athtext1") || data["athtext1"] == "") {
            document.getElementById("lineBG1").style.visibility = "hidden";
            document.getElementById("athtext1").style.visibility = "hidden";
        }

        console.log("CHECK MAINLINE")

        if (!data.hasOwnProperty("subathtext1") || data["subathtext1"] == "") {
            document.getElementById("sublineBG").style.visibility = "hidden";
            document.getElementById("subathtext1").style.visibility = "hidden";
        }
        console.log("CHECK SUBLINE")
    }

        }
    }

    return {}

})();

@Hylkewolf Hylkewolf

Check this code I posted, there could be some extra { at the bottom I just cut from a template quickly you can see the function deep data. I am using “#” instead than the “.” but that should not make any difference :slight_smile:

Let me know if was of an help.
It is of course important that you name the # with you aftereffect shapes and text layers otherwise nothing will work and set the correct frames to play at the top.

Aha, you manipulate the json before you load it into the lottie player. That is also a way to do it and probably allow it to manipulate text in sub-comps. The “official” way is to use updateDocumentData. That can be done after the animation has been loaded and can also update texts in an already showing animation, but can not change texts in sub-comps. So I guess both ways have their advantages.

Hello Didi! Yep!
To update data while template is playing I normally simply replace the data by ID and use the function UPDATE!

Thanks all. Making progress with this and I can get the animation to play out (not with dynamic text though) but I’m getting the following error in the console when I play the graphic out. Unfortunately my learning hasn’t got to promises yet so I’m not really sure what I’m missing. The json doesn’t have ‘<’ in it as far as I can tell. Tried with a couple of different Bodymovin exports and get the same thing. Any ideas appreciated!

Uncaught (in promise) SyntaxError: Unexpected token < in JSON at position 0

EDIT: It seems to be trying to parse using this line but can’t parse the templateData string with < in it. Should I stringify intestead?
loadedData = JSON.parse(raw);

Hi, good to hear is going better.
If you do not see dynamic text, try to check if you removed in the export the gliphs otherwise your text instances will be exported as “images”.
The exception is the one to prevent in any case to update your data, so the previous info was just to double check it :slight_smile:
For the JSON part you can use a JSON validator online and see what is missing.
In any case the data received by Caspar on the ServerLog should look something like the following:

"{\"athtext1\": \"HereTextMainLine\", \"subathtext1\": \"HereTextSubLine\", \"channel\": \"1\", \"layer\": \"10\"}"\r\n

Thanks. The Bodymovin export is fine and I can blank down the data or manually update so all good there, I’m not using glyphs. The JSON part it defeating me at the moment. The raw data being picked up and parsed is straight from Caspar Client in this format which is obviously not going to work as is. I’m sure there’s a simple but important part I’ve missed.

<templateData><componentData id="f2"><data id="text" value="TEST"/>

Hi again, that is not Json but XML. If you take a look to the string I posted above you will see the correct output Caspar is expecting. I suggest you to move this matter to another post since not related anymore in pre composition, so also other people can give an input probably even better than mine :slight_smile:

facepalm

I didn’t realise there was a ‘Send as JSON’ checkbox in Client. Gah! Thanks for your help, it’s all working now and you’re right, I’ll start a new topic if I have any more questions.

1 Like