mightyscape-1.2/extensions/fablabchemnitz/line_animator/line_animator.py

224 lines
9.5 KiB
Python
Raw Normal View History

2022-11-04 22:26:10 +01:00
#!/usr/bin/env python3
"""
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 LineAnimator(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument("--duration", type=float, default=10.0, help="Duration in seconds")
pars.add_argument("--repeat", type=int, default=1, help="Number of repetitions for looping, 0 means infinite")
pars.add_argument("--delay", type=float, default=0.0, help="Delay of animation start in seconds")
pars.add_argument("--identifier", default="animation_1", help="Unique identifier for the animation (only A-Z, a-z, 0-1, _)")
pars.add_argument("--remove_from", default="selected", help="Remove animations from selected items")
pars.add_argument("--action", default="add_anim", help="The active tab when Apply is pressed.")
pars.add_argument("--timing", default="ease")
def effect(self):
self.root = self.document.getroot()
'''
some workaround creating a dummy style tag, because applying this extension causes issue:
the first style tag, which contains keyframes, is always incomplete. But when creating a dummy first, all following style tags are fine (CDATA).
tested with Inkscape 1.2.1 on Fedora 37 with lxml 4.9.1 at Python 3.10
'''
dummy_style_tag = etree.SubElement(self.root, 'style', {'id': "dummy"})
dummy_style_tag.text = "This is just a dummy style tag which can be deleted. It is getting creating due to a bug which causes that the first path_anim style is incomplete in it's formatting. This seems to be an lxml etree issue."
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.rendering_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};}}
}}""".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__':
LineAnimator().run()