Added Maren's Line Animator extension

This commit is contained in:
Mario Voigt 2021-05-18 16:30:45 +02:00
parent 55aceee55e
commit d3af6c33c5
2 changed files with 282 additions and 0 deletions

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Line Animator</name>
<id>fablabchemnitz.de.line_animator</id>
<param name="action" type="notebook">
<page name="add_anim" gui-text="Add Animation">
<param name="add_desc" type="description">Animate the selected objects as if they were drawn with a pencil.</param>
<param name="identifier" type="string" gui-description="Only letters A-Z and a-z, numbers and underscores." gui-text="Unique name:">animation_1</param>
<param name="duration" type="float" precision="3" min="0.001" max="1000.000" gui-description="Duration of the animation in seconds." gui-text="Duration (seconds):">10.000</param>
<param name="repeat" type="int" min="0" max="1000" gui-description="Number of times the animation should be played. 0 means infinite repetition." gui-text="Number of repetitions (0 = infinite):">1</param>
<param name="delay" type="float" precision="3" min="0.000" max="1000.000" gui-description="Time to wait until the animation starts. Can be used to chain animations with different speed, or to create parallel animations." gui-text="Delay (seconds):">0.000</param>
</page>
<page name="advanced" gui-text="Advanced options">
<param name="timing" type="optiongroup" gui-description="Select the timing profile of the animation" gui-text="Timing function">
<option value="ease">ease</option>
<option value="ease-in">ease-in</option>
<option value="ease-out">ease-out</option>
<option value="ease-in-out">ease-in-out</option>
<option value="linear">linear</option>
</param>
</page>
<page name="remove_anim" gui-text="Remove Animation">
<param name="remove_from" type="enum" gui-description="Remove all traces of animations from the file or only remove animations from selected objects." gui-text="Remove animations:">
<item value="selected">from selected objects</item>
<item value="all">from the whole document</item>
</param>
</page>
<page name="help" gui-text="Help">
<label>This extension allows you to convert your line drawings into CSS animations that will run on any modern web browser. The animations will look as if the paths are drawn by hand.</label>
<label>Set color and stroke width to your liking. Subpaths will be drawn simultaneously. Separate paths will be drawn in stacking order.</label>
<label>You can use the extension multiple times per document for (entirely) different sets of objects. This allows you to set different durations, and optionally adding a delay, so the separate animations will start at different times.</label>
<label>Extension development happens on GitLab at:</label>
<label appearance="url">https://gitlab.com/Moini/ink_line_animator</label>
</page>
</param>
<effect needs-live-preview="false">
<object-type>path</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Modify existing Path(s)"/>
</submenu>
</effects-menu>
</effect>
<script>
<command reldir="inx" interpreter="python">line-animator.py</command>
</script>
</inkscape-extension>

View File

@ -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 <marenhachmann@yahoo.com>
using path length measuring code from measure.py, written by:
Copyright (C) 2015 ~suv <suv-sf@users.sf.net>
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()