How to create a script for stroke reveal

Hello everybody,

I am looking for a solution to create a script that automates the process of creating the stroke reveal effect. See this tutorial:

Someone to help me? I never did a script …

Thanks! Mica

1 Like

I also had the idea to create a script which would ease the work of creating the stroke reveal effect.

Before we start to create some script, we have to think about some problems like where will you add the waypoints for it, or how to create the stroke reveal effect for selective advanced outline, it’s not always correct to assume that you have want the effect for all advanced outline. And if you have more than 2 width point how should you deal with it (maybe ignoring that layer would be good).

As you have not specified what you will use this script for, as you are suggesting the tutorial it seems you just want stroke reveal effect, even if you create any plugin you still would have to manually displace the waypoints yourself.

I don’t think a plugin would be helpful for this case. If you are going to create any plugin, you need to take into consideration of above problems and create some solution for it which can get a bit complicated.

Maybe you should create a custom syntax denoting which outlines to select, how to animate them, duration for animation (of the stroke effect), delay between layers, there is a lot of input that you can expect from user. For now a solution is to rename layers and take any kind input/information from the user. For now rather than creating such complicated programs lets just take some time and animate the outlines.

If Synfig provides a way to take input for plugins then that would be really useful for plugin developers, for now it is complicated for both developers and users to provide some information for the plugin.I have already made a request (and it is not easy for developers too, to create some UI for plugins to use). I think this is really important, because plugins are easy to develop and small scripts can save a lot of time.

2 Likes

I actually used veermetri05’s ‘Make advanced outline’ plugin (thank you for script and inspiration) and modified it to make my own ‘Make stroke animation’ plugin. Unfortunately, I cannot attach the zip, so here’s the script.

import xml.etree.ElementTree as ET
import random, string
import uuid
import sys
import copy
tree = ET.parse(sys.argv[1])
root = tree.getroot()
rootChildren = list(root.iter('layer'))

def get_id():
    return uuid.uuid4().hex

for child in rootChildren:
    if "outline" in str(child.attrib['type']) and "_xa" not in str(child.attrib['desc']) :

        if str(child.attrib['type']) == "outline" : 
            child.attrib['type'] = "advanced_outline"
            for index, item in enumerate(list(child)):
                    if(item.attrib["name"] == "sharp_cusps"):
                        item.remove(list(item)[0])
                        item.attrib["name"] = "cusp_type"
                        integer = ET.SubElement(item, "integer").set("value", "0")
                    if(item.attrib["name"] == "round_tip[0]"):
                        item.remove(list(item)[0])
                        integer = ET.SubElement(item, "integer").set("value", "1")
                        item.attrib["name"] = "start_tip"
                    if(item.attrib["name"] == "round_tip[1]"):
                        item.remove(list(item)[0])
                        ET.SubElement(item, "integer").set("value", "1")
                        item.attrib["name"] = "end_tip"
                    if(item.attrib["name"] == "homogeneous_width"):
                        item.attrib["name"] = "homogeneous"

        child.extend([ET.fromstring("""        <param name="wplist">
          <wplist type="width_point" loop="false">
            <entry>
              <composite guid=\"""" + str(get_id()) + """\" type="width_point">
                <position>
                  <real value="0.0000000000"/>
                </position>
                <width>
                  <real value="1.0000000000"/>
                </width>
                <side_before>
                  <integer value="0"/>
                </side_before>
                <side_after>
                  <integer value="0"/>
                </side_after>
                <lower_bound>
                  <real value="0.0000000000" static="true"/>
                </lower_bound>
                <upper_bound>
                  <real value="1.0000000000" static="true"/>
                </upper_bound>
              </composite>
            </entry>
            <entry>
              <composite guid=\"""" + str(get_id()) + """\" type="width_point">
                <position>
                  <animated type="real">
                    <waypoint time="1f" before="clamped" after="clamped">
                      <real guid=\"""" + str(get_id()) + """\" value="0.0000000000"/>
                    </waypoint>
                    <waypoint time="5s" before="clamped" after="clamped">
                      <real guid=\"""" + str(get_id()) + """\" value="1.0000000000"/>
                    </waypoint>
                  </animated>
                </position>
                <width>
                  <animated type="real">
                    <waypoint time="0" before="clamped" after="clamped">
                      <real guid=\"""" + str(get_id()) + """\" value="0.0000000000"/>
                    </waypoint>
                    <waypoint time="1f" before="clamped" after="clamped">
                      <real guid=\"""" + str(get_id()) + """\" value="1.0000000000"/>
                    </waypoint>
                  </animated>
                </width>
                <side_before>
                  <integer value="0"/>
                </side_before>
                <side_after>
                  <integer value="1"/>
                </side_after>
                <lower_bound>
                  <real value="0.0000000000" static="true"/>
                </lower_bound>
                <upper_bound>
                  <real value="1.0000000000" static="true"/>
                </upper_bound>
              </composite>
            </entry>
          </wplist>
        </param>"""), ET.fromstring("""        <param name="dash_enabled">
          <bool value="false"/>
        </param>"""), ET.fromstring("""        <param name="dilist">
          <dilist type="dash_item" loop="false">
            <entry>
              <composite guid=\"""" + str(get_id()) + """\" type="dash_item">
                <offset>
                  <real value="0.1000000000"/>
                </offset>
                <length>
                  <real value="0.1000000000"/>
                </length>
                <side_before>
                  <integer value="4"/>
                </side_before>
                <side_after>
                  <integer value="4"/>
                </side_after>
              </composite>
            </entry>
          </dilist>
        </param>"""), ET.fromstring("""        <param name="dash_offset">
          <real value="0.0000000000"/>
        </param>""")])

        for bline in child.iter("bline"):
            if bline.attrib["loop"] == "true":
                bline.set("loop", "false")
                entry = bline.find("entry")
                entry_rpt = copy.deepcopy(entry)
                composite = entry_rpt.find("composite")
                composite.set("guid", get_id())
                bline.extend([entry_rpt])

tr2ee = ET.ElementTree(root)
with open(sys.argv[1], "wb") as fh:
    tr2ee.write(fh)

What it does:

  1. As in the original plugin, all outlines are converted to advanced outlines (required for stroke reveal) EXCEPT in the new plugin, any layers that have ‘_xa’ (for ‘no animation’) in their names will not be converted.

  2. All advanced outlines, including previous advanced outlines without ‘_xa’ in their names, will have a stroke reveal of 5 seconds, starting from 0 seconds. As Synfig doesn’t have a way to interact with plugins, that’s the default unless you edit the python file (you can see the ‘5s’ in the script). However, you can easily drag the waypoints in the Time Track Panel after the plugin runs.

  3. Closed outlines will be converted to open outlines with a copy of the first point added to the end so that the animation will complete the loop. Otherwise it will stop at the second last point.

Since this plugin might mess up your current animation or change stroke widths, etc., do save a copy of your drawing before you run it.

Here is an example: an animated stroke reveal of the borders on the world map (svg file from Wikimedia Commons). Just imagine trying to manually change every stroke !!!

For info, changes that I made to veermetri05’s script:

  • Detection of ‘_xa’ in the layer’s name.
  • Changing of the cusps, tip, etc. is moved before adding the advanced outlines.
  • Added the animated tags between the position and width tags.
  • Convert any closed loop to open loops and copy the first point to the end.

Do try it out. Hope that it helps with your animation. If you can make better adjustments, please do.

10 Likes

Hello,

Thank you both for your answers!
Why am I trying to automate the line revelation effect? Originally it was to make animated explanatory videos of the whiteboard-animation type.
At present I have only found paid and proprietary software to do this.
In addition, as my cartoons contain a lot of lines, it is very laborious to do it by hand.
So thank you very much to you Scribbleed! This is exactly what I was thinking! But for now I can’t get it to work …

I get the message: Traceback (most recent call last):
File"home/michael/snap/synfigstudio/11/.config/synfig/plugins/-Animation de ligne/animer-la-ligne.py", line 121, in if bline.attrib[“loop”]==“true”:
KeyError:‘loop’

For info, being French I called the script animer-la-ligne.py and the Animation de ligne folder.
To create the script I copied and renamed the Make advanced outline folder. Then I copied the script you give there without modifying anything.

Do you have any idea why this isn’t working with me?

Thank you so much!

Be sure the attribute loop exists in your file.
It must be true or false, but it should be present.

Thanks for pointing it out, should have done some error checking. Just change the ‘blines’ lines to

    for bline in child.iter("bline"):
        if "loop" in bline.attrib:
            if bline.attrib["loop"] == "true":
                bline.set("loop", "false")
                entry = bline.find("entry")
                entry_rpt = copy.deepcopy(entry)
                composite = entry_rpt.find("composite")
                composite.set("guid", get_id())
                bline.extend([entry_rpt])

Note: after you run the plugin, the converted outlines will ‘disappear’ because in the first frame, it should not appear. Click the play button to see the stroke reveal.

Have fun!

Really cool, thanks for your contribution.

As you are ignoring layers with _xa you can just extend it. And also take input for timing.

Example,

_10s_nameOfLayer
_15f_nameOfLayer

You can use regex to parse names of layers and get the inputs.
Maybe even add a support to displace by renaming the group.

“_displace_20f_nameOfGroup”
---->"_10f_layer1"
---->"_20f_layer2"
---->"…"

So in this example, layer1’s animation would be of 10f then there would be a constant displacement of 20f and then layer2’s animation for 20f and so on

f=frames, s=seconds

I don’t think this is really necessary, but if would ease the work a lot.

1 Like

You’re welcome. You also have a good idea there to extend the layer names, but I’ll leave it to better python programmers if they want to modify the script above. Have to balance it too, whether it saves more time to change layer names or just drag the waypoints after the script runs. In my case, I wanted a script to modify a lot of layers, so adding ‘_xa’ was for a few ones I didn’t want was easier. Some other users might just want to change one or two layers instead, so maybe can use another script that does the opposite.

Ideally, I can imagine a Synfig feature where you can select layers and run a plugin just for those layers, maybe also with a GUI. Similarly, one could make scripts to apply a default animation such as zoom, flash, rotate. But again, it depends whether if it’s faster using the plugin and adjusting the animation after, or directly making the animation yourself, with the least keyboard input.

Hello, thank you for this nice plugin but I get this error if I do it with a vectorized image:

Hi, everyone! I took the recommendation of @veermetri05, so adding a few lines to the collective work of @Scribbleed and @veermetri05, I ended up with the following script, which takes input from layers’ names and sets the start and end of the animation from them. Thus, a layer labeled 1-4-outline would reveal its stroke during three seconds starting in second 1, while all the remaining labels lacking the “_” character (instead of “_xa”) would be animated by default from second 1 to 3.

Hope someone will find this, my very first post, useful :nerd_face: Thanks a lot to the above mentioned members for their kind contributions.

import xml.etree.ElementTree as ET
import random, string
import uuid
import sys
import copy
import re
tree = ET.parse(sys.argv[1])
root = tree.getroot()
rootChildren = list(root.iter('layer'))

def get_id():
    return uuid.uuid4().hex

for child in rootChildren:
    if "outline" in str(child.attrib['type']) and ("_" not in str(child.attrib['desc']) or len(re.findall("-", str(child.attrib['desc']))) == 2) :
        
        if len(re.findall("-", str(child.attrib['desc']))) == 2:
            anim_times = re.split("-", str(child.attrib['desc']))
            ini = str(anim_times[0])
            end = str(anim_times[1])
        else:
            ini = str(1)
            end = str(3)            
            
        
        if str(child.attrib['type']) == "outline" : 
            child.attrib['type'] = "advanced_outline"
            for index, item in enumerate(list(child)):
                    if(item.attrib["name"] == "sharp_cusps"):
                        item.remove(list(item)[0])
                        item.attrib["name"] = "cusp_type"
                        integer = ET.SubElement(item, "integer").set("value", "0")
                    if(item.attrib["name"] == "round_tip[0]"):
                        item.remove(list(item)[0])
                        integer = ET.SubElement(item, "integer").set("value", "1")
                        item.attrib["name"] = "start_tip"
                    if(item.attrib["name"] == "round_tip[1]"):
                        item.remove(list(item)[0])
                        ET.SubElement(item, "integer").set("value", "1")
                        item.attrib["name"] = "end_tip"
                    if(item.attrib["name"] == "homogeneous_width"):
                        item.attrib["name"] = "homogeneous"

        child.extend([ET.fromstring("""        <param name="wplist">
          <wplist type="width_point" loop="false">
            <entry>
              <composite guid=\"""" + str(get_id()) + """\" type="width_point">
                <position>
                  <real value="0.0000000000"/>
                </position>
                <width>
                  <real value="1.0000000000"/>
                </width>
                <side_before>
                  <integer value="0"/>
                </side_before>
                <side_after>
                  <integer value="0"/>
                </side_after>
                <lower_bound>
                  <real value="0.0000000000" static="true"/>
                </lower_bound>
                <upper_bound>
                  <real value="1.0000000000" static="true"/>
                </upper_bound>
              </composite>
            </entry>
            <entry>
              <composite guid=\"""" + str(get_id()) + """\" type="width_point">
                <position>
                  <animated type="real">
                    <waypoint time=\"""" + ini + str("s") + """\" before="clamped" after="clamped">
                      <real guid=\"""" + str(get_id()) + """\" value="0.0000000000"/>
                    </waypoint>
                    <waypoint time=\"""" + end + str("s") + """\" before="clamped" after="clamped">
                      <real guid=\"""" + str(get_id()) + """\" value="1.0000000000"/>
                    </waypoint>
                  </animated>
                </position>
                <width>
                  <animated type="real">
                    <waypoint time="0" before="clamped" after="clamped">
                      <real guid=\"""" + str(get_id()) + """\" value="0.0000000000"/>
                    </waypoint>
                    <waypoint time="1f" before="clamped" after="clamped">
                      <real guid=\"""" + str(get_id()) + """\" value="1.0000000000"/>
                    </waypoint>
                  </animated>
                </width>
                <side_before>
                  <integer value="0"/>
                </side_before>
                <side_after>
                  <integer value="1"/>
                </side_after>
                <lower_bound>
                  <real value="0.0000000000" static="true"/>
                </lower_bound>
                <upper_bound>
                  <real value="1.0000000000" static="true"/>
                </upper_bound>
              </composite>
            </entry>
          </wplist>
        </param>"""), ET.fromstring("""        <param name="dash_enabled">
          <bool value="false"/>
        </param>"""), ET.fromstring("""        <param name="dilist">
          <dilist type="dash_item" loop="false">
            <entry>
              <composite guid=\"""" + str(get_id()) + """\" type="dash_item">
                <offset>
                  <real value="0.1000000000"/>
                </offset>
                <length>
                  <real value="0.1000000000"/>
                </length>
                <side_before>
                  <integer value="4"/>
                </side_before>
                <side_after>
                  <integer value="4"/>
                </side_after>
              </composite>
            </entry>
          </dilist>
        </param>"""), ET.fromstring("""        <param name="dash_offset">
          <real value="0.0000000000"/>
        </param>""")])

        for bline in child.iter("bline"):
            if "loop" in bline.attrib:
                if bline.attrib["loop"] == "true":
                    bline.set("loop", "false")
                    entry = bline.find("entry")
                    entry_rpt = copy.deepcopy(entry)
                    composite = entry_rpt.find("composite")
                    composite.set("guid", get_id())
                    bline.extend([entry_rpt])

tr2ee = ET.ElementTree(root)
with open(sys.argv[1], "wb") as fh:
    tr2ee.write(fh)

Another nice little plugin for the set. Cheers @Thor and the collective.
I’ve also gone in and made a few tweaks to this script as I noticed a couple of things to do with ‘side type’ and animation offset start, that being when you tell it start the reveal from 1 second, you would get an outlines width dot become visible from frame 1 and would sit there till the reveal actually started.

plugin now looks at the sif canvas FPS to help place the offset widthpoint scale start where it needs to be.
switched the initial waypoints being created to constant rather than clamped too.
packaged for plugin use.

stroke-reveal-plugin.zip (1.8 KB)

stroke_tst

1 Like

I became member of this forum wanting to share and learn, and that’s what I’m doing from the very begining!!

You’re right, @KEgg, now the result looks much better. Thank you for improving and packing the code!

:beers:

1 Like

Thank you @Thor … Very Welcome.
Had a little time this morning to add some new functionality to the script.
You can now control start and end interpolation. (ease, linear, clamped).

0-4-outline (like before… default interpolation clamped)
2-6-linear-outline (single value sets the outline start and end interpolation)
3-6-linear-ease-outline (different start and end interpolation)

check this example for use.
stroke_pv2.sifz (3.6 KB)

Updated stroke-reveal plugin
stroke-reveal-plugin.zip (2.0 KB)

stroke_tst_v2

2 Likes

Sweet!! I honestly didn’t know the different types of interpolation, as I’m pretty novice to Synfig, but thanks your generosity, @KEgg, I know a bit more now; surely more people will find your contribution very enriching too. Right now I’m working on an infographic video with many animations and this plugin is helping me to make everything easier (and better looking). Cheers!

1 Like