From d3af6c33c58f416b2bfa3492599aa6c3edad8fb6 Mon Sep 17 00:00:00 2001 From: Mario Voigt Date: Tue, 18 May 2021 16:30:45 +0200 Subject: [PATCH] Added Maren's Line Animator extension --- .../ink_line_animator/line-animator.inx | 47 ++++ .../ink_line_animator/line-animator.py | 235 ++++++++++++++++++ 2 files changed, 282 insertions(+) create mode 100644 extensions/fablabchemnitz/ink_line_animator/line-animator.inx create mode 100644 extensions/fablabchemnitz/ink_line_animator/line-animator.py 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()