Added Maren's Line Animator extension
This commit is contained in:
parent
55aceee55e
commit
d3af6c33c5
@ -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>
|
235
extensions/fablabchemnitz/ink_line_animator/line-animator.py
Normal file
235
extensions/fablabchemnitz/ink_line_animator/line-animator.py
Normal 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()
|
Reference in New Issue
Block a user