Inkscape.org
Creating New Extensions How to add an XML tag (<animate>) to an object using an extension?
  1. #1
    scribbleed scribbleed @scribbleed
    *

    Let's say I have selected 2 objects (circle and triangle) and their XML in the svg would be as follows:

        <circle
           style="fill:#00ffff;stroke:#000000;stroke-width:5;"
           id="path1"
           cx="100"
           cy="25"
           r="20" />
        <path
           style="fill:none;stroke:#888888;stroke-width:3;"
           d="M 10,10 V 40 H 50 Z"
           id="path2" />
    

    I would then want to modify and append it into this:

        <circle
           style="fill:#00ffff;stroke:#000000;stroke-width:5;stroke-dasharray:0, 126"
           id="path1"
           cx="100"
           cy="25"
           r="20">
          <animate
             attributeName="stroke-dasharray"
             from="0 126"
             to="126 0"
             begin="0s"
             dur="3s"
             repeatCount="indefinite" />
        </circle>
        <path
           style="fill:none;stroke:#888888;stroke-width:3;stroke-dasharray:0, 120"
           d="M 10,10 V 40 H 50 Z"
           id="path2">
          <animate
             attributeName="stroke-dasharray"
             from="0 120"
             to="120 0"
             begin="0s"
             dur="3s"
             repeatCount="indefinite" />
        </path>

    The stroke-dasharray would be equivalent to the length of the stroke, which I can calculate using the same function in the Measure Path extension. With the <animate> tag, I can see the animation of the stroke of the svg file in a browser with no extra coding of css or javascript, like down here (it's an SVG file, not GIF!). The tag can be typed in the XML Editor in Inkscape without affecting the object itself, but of course, the animation will not be visible in Inkscape.

    So how would I add the <animate> tag to the xml? I can't find how to reference the xml of the self.svg.selection. I think it's possible to get the xml of the whole document but that would be overkill as well as cumbersome to find just the objects I want (I guess iterating the ids). I also assume that using inkex's lxml would be enough without needing additional ElementTree package.

    Also, how would I detect and remove the tag if it exists, so I don't make duplicates?

     


  2. #2
    inklinea inklinea @inklinea⛰️

    The basic method to create a new element in inkex is:

    (at the top of the python code)
    from inkex import ELEMENT_NAME

    (in the code itself)
    new_element = ELEMENT_NAME()

    new_element.set('ATTRIBUTE_NAME', 'ATTRIBUTE_VALUE')

    You can set the attributes individually as above, or create a dictionary and loop through it setting each value.

    So for example to create a new path would be: 

    from inkex import PathElement

    new_path = PathElement()
    new_path.set('d', 'M 120 130 L 200 250')
    new_path.set('stroke-width', '1px')
    new_path.style['stroke', 'green')

    and to add that to a document ( in the current layer )

    self.svg.get_current_layer().append(new_path)

    or to add as a subelement of an existing element

    parent_element.append(new_path)

    Appending as a subelement only makes sense if supported by the svg code. For example adding to a group or adding a <tspan> to a <text> element etc.

    ------------------------------------------------

    However, what do you do if you want to use an element which is not supported by Inkscape or Inkex ? 

    (At the moment Inkscape does not support animation elements, it just ignores them)

    Any generic element can be added to the svg in python using a generic inkex (etree) statement

    *You should never use this for elements which are natively supported by Inkscape / Inkex*

    (at the top of the python code)
    from lxml import etree

    (in the code itself)
    new_element = etree.SubElement(parent_element, inkex.addNS('new_element_name', 'svg'))


    There actually might be a slightly better way to do this in 1.3, but this works and is simple.

    --------------------------------------------------------

    I've made a simple example below and attached a zipped working extension.

    I've used intertools.cycle() to loop your animate element examples infinitely so it supports any number of selected items.

    -------------------------------------------

    import inkex

    from lxml import etree

    from itertools import cycle

    def add_animate_element(self, parent, animate_dict):

        # Delete <animate> element if already exists
        for child in parent.getchildren():
            if child.TAG == 'animate':
                child.delete()

        # Generic statement to create any element in the svg namespace
        # Should only be used for elements which are not in the Inkscape api

        animate_element = etree.SubElement(parent, inkex.addNS('animate', 'svg'))

        for key, value in animate_dict.items():

            animate_element.set(key, value)


    class AddAnimateElement(inkex.EffectExtension):

        def add_arguments(self, pars):
            pass
        
        def effect(self):

            # Get the selection list and exit if nothing is selected
            selection_list = self.svg.selected
            if len(selection_list) < 1:
                inkex.errormsg('Please select at least one object')
                return

            # Print out selection list to a message box (optional)
            for selection in selection_list:
                inkex.errormsg(f'Elemlent Type: {selection} Element ID: {selection.get_id()}')

            # Make an attribute dictionary for a couple of animation elements
            # From the sample below

            # <animate
            # attributeName="stroke-dasharray"
            # from="0 126"
            # to="126 0"
            # begin="0s"
            # dur="3s"
            # repeatCount="indefinite" />

            animate_attrib_dict_1 = {'attributeName': 'stroke-dasharray', 'from': '0 126', 'to': '126 0', 'begin': '0s', 'dur': '3s', 'repeatCount': 'indefinite'}
            animate_attrib_dict_2 = {'attributeName': 'stroke-dasharray', 'from': '0 120', 'to': '120 0', 'begin': '0s', 'dur': '3s', 'repeatCount': 'indefinite'}

            # Lets make an infinite cycling list so we can account for any number of items in the selection.

            animate_list = [animate_attrib_dict_1, animate_attrib_dict_2]

            animate_list_cycle = cycle(animate_list)

            # Loop through the selection list adding animate elements
            for selected in selection_list:
                add_animate_element(self, selected, next(animate_list_cycle))

            
    if __name__ == '__main__':
        AddAnimateElement().run()

     

     


     

    Peek 2024 03 21 07 00
  3. #3
    scribbleed scribbleed @scribbleed

    Wow, thanks so much. That was way above and beyond response. Saves me many steps finding the other python functions to append an XML tag.

    Lots of good groundwork to start on a useful extension. For a start, I noticed that each primitive object must have its own animate tag. If it is on a group, then it might or might not work. But that can be addressed in the extension.. Also, reading about SVG animation, dash-offset is somewhat better than using dash-array for the stroke animation.

    Once again, thank you.

     

  4. #4
    inklinea inklinea @inklinea⛰️

    It's more a case of understanding how svg animation works  :)

    Also there was an error above in first part of the post:

    new_path.style['stroke', 'green') should be new_path.style['stroke'] = 'green'

    There are already a couple of Inkscape animation extensions if you search for them. 

    What is surprising is the different python coding styles that are used in them. 

    My python coding is very basic and straightforward - nothing fancy :)

     

     

     

  5. #5
    Tyler Durden Tyler Durden @TylerDurden

    Maybe this: https://inkscape.org/~Moini/%E2%98%85line-animator-draw-by-invisible-hand

  6. #6
    inklinea inklinea @inklinea⛰️

    Just an update. 

    After speaking to a Inkscape developer

    animate_element = etree.SubElement(parent, inkex.addNS('animate', 'svg'))

    Is out of date for Inkscape 1.1+

    At the top of the .py file (but below the import statements):

    Something like this:

    class Animate(inkex.BaseElement):
        tag_name = 'animate'

        
    Which creates a new element from the Inkscape base element

    Then instead of 

    animate_element = etree.SubElement(parent, inkex.addNS('animate', 'svg'))

    Use:

    animate_element = Animate()
    parent.append(animate_element)
    animate_element.set_random_id('ani_')

        
    This creates a new <animate> element with random number id, the id will be prefixed with ani_

    Thanks.

Inkscape Inkscape.org Inkscape Forum Creating New Extensions How to add an XML tag (<animate>) to an object using an extension?