diff --git a/extensions/fablabchemnitz/ink_line_animator/line-animator.inx b/extensions/fablabchemnitz/ink_line_animator/line-animator.inx
new file mode 100644
index 00000000..2c85e86f
--- /dev/null
+++ b/extensions/fablabchemnitz/ink_line_animator/line-animator.inx
@@ -0,0 +1,47 @@
+
+
+ Line Animator
+ fablabchemnitz.de.line_animator
+
+
+ Animate the selected objects as if they were drawn with a pencil.
+ animation_1
+ 10.000
+ 1
+ 0.000
+
+
+
+
+
+
+
+
+
+
+
+
+ from selected objects
+ from the whole document
+
+
+
+
+
+
+
+
+
+
+
+ path
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/fablabchemnitz/ink_line_animator/line-animator.py b/extensions/fablabchemnitz/ink_line_animator/line-animator.py
new file mode 100644
index 00000000..2cabbb51
--- /dev/null
+++ b/extensions/fablabchemnitz/ink_line_animator/line-animator.py
@@ -0,0 +1,235 @@
+#!/usr/bin/env python
+"""
+line animator - create CSS3 animations that look as if someone is drawing them by hand
+
+Copyright (C) 2018-2021, Maren Hachmann
+
+using path length measuring code from measure.py, written by:
+ Copyright (C) 2015 ~suv
+ Copyright (C) 2010 Alvin Penner
+ Copyright (C) 2006 Georg Wiora
+ Copyright (C) 2006 Nathan Hurst
+ Copyright (C) 2005 Aaron Spike, aaron@ekips.org
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+"""
+
+__version__ = 1.0
+
+import re
+import sys
+from lxml import etree
+
+# local libraries
+import inkex
+from inkex.bezier import csplength
+
+# TODO:
+# - do not add class style tag with anything but animation name list (needs to be parsed! animation-name: animation_1, animation_2;), but repeat all the other data for each path. This is the only way to add two different animations to a single path.
+# - fix delay
+# - implement removal of animations
+
+
+class Line_Animator(inkex.Effect):
+ def __init__(self):
+ inkex.Effect.__init__(self)
+ self.arg_parser.add_argument("--duration",
+ type=float,
+ default=10.0,
+ help="Duration in seconds")
+ self.arg_parser.add_argument("--repeat",
+ type=int,
+ default=1,
+ help="Number of repetitions for looping, 0 means infinite")
+ self.arg_parser.add_argument("--delay",
+ type=float,
+ default=0.0,
+ help="Delay of animation start in seconds")
+ self.arg_parser.add_argument("--identifier",
+ default="animation_1",
+ help="Unique identifier for the animation (only A-Z, a-z, 0-1, _)")
+ self.arg_parser.add_argument("--remove_from",
+ default="selected",
+ help="Remove animations from selected items")
+ self.arg_parser.add_argument("--action",
+ # other options: remove_anim, advanced, help
+ default="add_anim",
+ help="The active tab when Apply is pressed.")
+ self.arg_parser.add_argument("--timing",
+ # other options: ease-in, ease-out, ease-in-out, linear
+ default="ease")
+
+ def effect(self):
+
+ self.root = self.document.getroot()
+
+ id_regex = re.compile('^[a-zA-Z0-9_]+$')
+ if not id_regex.match(self.options.identifier):
+ inkex.errormsg(_("Please make sure that the animation's name does not contain any other characters than uppercase or lowercase letters from A to Z, numbers from 0 to 9, or underscores."))
+
+ if self.options.repeat == 0:
+ self.options.repeat = "infinite"
+
+ if self.options.action == 'remove_anim':
+ if self.options.remove_from == 'selected':
+ self.remove_selected()
+ else:
+ self.remove_all()
+ elif len(self.svg.selected):
+ self.add_animation(self.options.identifier,
+ self.options.duration,
+ self.options.delay,
+ self.options.repeat,
+ self.options.timing)
+ if self.options.delay > 0:
+ self.add_animation("delay_{0}".format(self.options.identifier),
+ self.options.delay,
+ 0,
+ 1,
+ "linear",
+ True)
+ else:
+ inkex.errormsg(_('Please select one or more paths to animate.'))
+
+ def add_animation(self, id, duration, delay, repetitions, timing, is_delay_anim=False):
+
+ animation_style_id = "anim_{0}".format(id)
+ # inkex.utils.debug('animation_style_id: '+animation_style_id)
+
+ to_animate = []
+ for thing in self.svg.selection.paint_order():
+ to_animate.append(thing)
+
+ lengths = [] # relevant lengths of all subsequently animated paths
+ for element in to_animate:
+ if element.tag == inkex.addNS('path','svg'):
+ lengths.append(self.get_animatable_length(element))
+ else:
+ inkex.errormsg(_('At least one of the selected objects is not a path: {}\nPlease convert all objects to paths before running this extension.\n').format(element.get('id')))
+ total_length = sum(lengths)
+ #inkex.utils.debug(total_length)
+ #inkex.utils.debug(lengths)
+
+ end_percent = 0
+
+ for index, length in enumerate(lengths):
+
+ # if we're creating an animation just to
+ # hide a path during delay time
+ if is_delay_anim == True:
+ # TODO: fix delay!
+ inkex.errormsg(_("Sorry, delay isn't working currently. Please set back to zero."))
+ sys.exit()
+ self.animate_path(to_animate[index], 0, 100, length, length, length, animation_style_id, True)
+ else:
+ # start where we ended before
+ start_percent = end_percent
+ # compute new end
+ end_percent += round(length/total_length*100, 3)
+ if end_percent > 100:
+ end_percent = 100
+
+ self.animate_path(to_animate[index], start_percent, end_percent, length, length, 0, animation_style_id, is_delay_anim)
+
+ animation_style_content = """
+ .{id} {{
+ animation-duration: {duration}s;
+ animation-timing-function: {timing};
+ animation-delay: {delay}s;
+ animation-iteration-count: {repetitions};
+ animation-fill-mode: forwards;
+ }}\n""".format(id=animation_style_id, duration=duration, delay=delay, repetitions=repetitions, timing=timing)
+
+ # create general style tag for animation that applies to all objects
+ self.add_or_replace_style_tag(animation_style_id, animation_style_content)
+
+
+ def animate_path(self, element, start_percent, end_percent, length, start_length, end_length, animation_identifier, is_delay_anim=False):
+
+ path_identifier = element.get('id')
+ animation_name = path_identifier
+ if is_delay_anim:
+ animation_name = 'delay_'+path_identifier
+ path_style_content = """
+ #{id} {{
+ animation-name: {animation_name};
+ stroke-dasharray: {length} !important;
+ }}
+
+ @keyframes {animation_name} {{
+ 0%, {start_percent}% {{stroke-dashoffset: {start_length};}}
+ {end_percent}%, 100% {{stroke-dashoffset: {end_length};}}
+ }}\n""".format(id=path_identifier,
+ animation_name=animation_name,
+ length=length,
+ start_percent=start_percent,
+ start_length=start_length,
+ end_percent=end_percent,
+ end_length=end_length)
+ # inkex.utils.debug(path_style_content)
+
+ # inkex.utils.debug('self.add_or_replace_style_tag('+'pathanim_' + animation_name + ','+path_style_content+')')
+ self.add_or_replace_style_tag('pathanim_' + animation_name, path_style_content)
+ # only change the element's class when we add the real animation
+ if is_delay_anim == False:
+ element.set("class", animation_identifier)
+
+ def get_animatable_length(self, elem):
+ # csp = elem.path.transform(elem.composed_transform()).to_superpath()
+ csp = elem.path.to_superpath()
+ subpath_lengths, path_length = csplength(csp)
+
+ # if there are subpaths, we do not want to extend the animation
+ # for longer than necessary (subpaths are animated in parallel)
+ if len(subpath_lengths) > 1:
+ path_length = max([sum(subpath_segments) for subpath_segments in subpath_lengths])
+
+ return path_length
+
+ def add_or_replace_style_tag(self, tag_id, content):
+
+ # inkex.utils.debug(tag_id)
+ old_tag = self.get_style_tag(tag_id)
+ # inkex.utils.debug('old_tag: '+str(old_tag))
+ if old_tag != False:
+ (self.root.remove(old_tag))
+ style_tag = etree.SubElement(self.root, 'style', {'id': tag_id})
+ style_tag.text = content
+
+ def get_style_tags(self):
+ style_tags = []
+ for element in self.root.getchildren():
+ if element.tag == inkex.addNS('style', 'svg'):
+ style_tags.append(element)
+ return(style_tags)
+
+ def get_style_tag(self, id):
+ for tag in self.get_style_tags():
+ if tag.get('id') == id:
+ return tag
+ return False
+
+
+ def remove_all(self):
+ inkex.utils.debug('Removing all not implemented yet.')
+
+ def remove_selected(self):
+
+ if len(self.svg.selected) == 0:
+ inkex.errormsg(_("Please select items to remove the animation from."))
+ inkex.utils.debug('Removing selected not implemented yet.')
+
+if __name__ == '__main__':
+ Line_Animator().run()