863 lines
33 KiB
Python
863 lines
33 KiB
Python
# coding=utf-8
|
|
#
|
|
# Copyright (C) 2005 Aaron Spike, aaron@ekips.org
|
|
# 2020 Jonathan Neuhauser, jonathan.neuhauser@outlook.com
|
|
#
|
|
# 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.
|
|
#
|
|
"""Module for interpolating attributes and styles
|
|
|
|
.. versionchanged:: 1.2
|
|
Rewritten in inkex 1.2 in an object-oriented structure to support more attributes.
|
|
"""
|
|
|
|
from bisect import bisect_left
|
|
import abc
|
|
import copy
|
|
|
|
from .styles import Style
|
|
from .elements._filters import LinearGradient, RadialGradient, Stop
|
|
from .transforms import Transform
|
|
from .colors import Color
|
|
from .units import convert_unit, parse_unit, render_unit
|
|
from .bezier import bezlenapprx, cspbezsplit, cspbezsplitatlength, csplength
|
|
from .paths import Path, CubicSuperPath
|
|
from .elements import SvgDocumentElement
|
|
from .utils import FragmentError
|
|
|
|
|
|
try:
|
|
from typing import Tuple, TypeVar
|
|
|
|
Value = TypeVar("Value")
|
|
Number = TypeVar("Number", int, float)
|
|
except ImportError:
|
|
pass
|
|
|
|
|
|
def interpcoord(coord_a: Number, coord_b: Number, time: float):
|
|
"""Interpolate single coordinate by the amount of time"""
|
|
return ValueInterpolator(coord_a, coord_b).interpolate(time)
|
|
|
|
|
|
def interppoints(point1, point2, time):
|
|
# type: (Tuple[float, float], Tuple[float, float], float) -> Tuple[float, float]
|
|
"""Interpolate coordinate points by amount of time"""
|
|
return ArrayInterpolator(point1, point2).interpolate(time)
|
|
|
|
|
|
class AttributeInterpolator(abc.ABC):
|
|
"""Interpolate between attributes"""
|
|
|
|
def __init__(self, start_value, end_value):
|
|
self.start_value = start_value
|
|
self.end_value = end_value
|
|
|
|
@staticmethod
|
|
def best_style(node):
|
|
"""Gets the best possible approximation to a node's style. For nodes inside the
|
|
element tree of an SVG file, stylesheets defined in the defs of that file can be
|
|
taken into account. This should be the case for input elements, but is not
|
|
required - in that case, only the local inline style is used.
|
|
|
|
During the interpolation process, some nodes are created temporarily, such as
|
|
plain gradients of a single color to allow solid<->gradient interpolation. These
|
|
are not attached to the document tree and therefore have no root. Since the only
|
|
style relevant for them is the inline style, it is acceptable to fallback to it.
|
|
|
|
Args:
|
|
node (BaseElement): The node to get the best approximated style of
|
|
|
|
Returns:
|
|
Style: If the node is rooted, the CSS specified style. Else, the inline
|
|
style."""
|
|
try:
|
|
return node.specified_style()
|
|
except FragmentError:
|
|
return node.style
|
|
|
|
@staticmethod
|
|
def create_from_attribute(snode, enode, attribute, method=None):
|
|
"""Creates an interpolator for an attribute. Currently, only path, transform and
|
|
style attributes are supported
|
|
|
|
Args:
|
|
snode (BaseElement): start element
|
|
enode (BaseElement): end element
|
|
attribute (str): attribute name (for styles, starting with "style/")
|
|
method (AttributeInterpolator, optional): (currently only used for paths).
|
|
Specifies a method used to interpolate the attribute. Defaults to None.
|
|
|
|
Raises:
|
|
ValueError: if an attribute is passed that is not a style, path or transform
|
|
attribute
|
|
|
|
Returns:
|
|
AttributeInterpolator: an interpolator whose type depends on attribute.
|
|
"""
|
|
if attribute in Style.color_props:
|
|
return StyleInterpolator.create_from_fill_stroke(snode, enode, attribute)
|
|
if attribute == "d":
|
|
if method is None:
|
|
method = FirstNodesInterpolator
|
|
return method(snode.path, enode.path)
|
|
if attribute == "style":
|
|
return StyleInterpolator(snode, enode)
|
|
if attribute.startswith("style/"):
|
|
return StyleInterpolator.create(snode, enode, attribute[6:])
|
|
if attribute == "transform":
|
|
return TransformInterpolator(snode.transform, enode.transform)
|
|
if method is not None:
|
|
return method(snode.get(attribute), enode.get(attribute))
|
|
raise ValueError("only path and style attributes are supported")
|
|
|
|
@abc.abstractmethod
|
|
def interpolate(self, time=0):
|
|
"""Interpolation method, needs to be implemented by subclasses"""
|
|
return
|
|
|
|
|
|
class StyleInterpolator(AttributeInterpolator):
|
|
"""Class to interpolate styles"""
|
|
|
|
def __init__(self, start_value, end_value):
|
|
super().__init__(start_value, end_value)
|
|
self.interpolators = {}
|
|
# some keys are always processed in a certain order, these provide alternative
|
|
# interpolation routes if e.g. Color<->none is interpolated
|
|
all_keys = list(
|
|
dict.fromkeys(
|
|
["fill", "stroke", "fill-opacity", "stroke-opacity", "stroke-width"]
|
|
+ list(self.best_style(start_value).keys())
|
|
+ list(self.best_style(end_value).keys())
|
|
)
|
|
)
|
|
for attr in all_keys:
|
|
sstyle = self.best_style(start_value)
|
|
estyle = self.best_style(end_value)
|
|
if attr not in sstyle and attr not in estyle:
|
|
continue
|
|
try:
|
|
interp = StyleInterpolator.create(
|
|
self.start_value, self.end_value, attr
|
|
)
|
|
self.interpolators[attr] = interp
|
|
except ValueError:
|
|
# no interpolation method known for this attribute
|
|
pass
|
|
|
|
@staticmethod
|
|
def create(snode, enode, attribute):
|
|
"""Creates an Interpolator for a given style attribute, depending on its type:
|
|
|
|
- Color properties (such as fill, stroke) -> :class:`ColorInterpolator`,
|
|
:class:`GradientInterpolator` ect.
|
|
- Unit properties -> :class:`UnitValueInterpolator`
|
|
- other properties -> :class:`ValueInterpolator`
|
|
|
|
Args:
|
|
snode (BaseElement): start element
|
|
enode (BaseElement): end element
|
|
attribute (str): attribute to interpolate
|
|
|
|
Raises:
|
|
ValueError: if the attribute is not in any of the lists
|
|
|
|
Returns:
|
|
AttributeInterpolator: an interpolator object whose type depends on the
|
|
attribute.
|
|
"""
|
|
if attribute in Style.color_props:
|
|
return StyleInterpolator.create_from_fill_stroke(snode, enode, attribute)
|
|
|
|
if attribute in Style.unit_props:
|
|
return UnitValueInterpolator(
|
|
AttributeInterpolator.best_style(snode)(attribute),
|
|
AttributeInterpolator.best_style(enode)(attribute),
|
|
)
|
|
|
|
if attribute in Style.opacity_props:
|
|
return ValueInterpolator(
|
|
AttributeInterpolator.best_style(snode)(attribute),
|
|
AttributeInterpolator.best_style(enode)(attribute),
|
|
)
|
|
|
|
raise ValueError("Unknown attribute")
|
|
|
|
@staticmethod
|
|
def create_from_fill_stroke(snode, enode, attribute):
|
|
"""Creates an Interpolator for a given color-like attribute
|
|
|
|
Args:
|
|
snode (BaseElement): start element
|
|
enode (BaseElement): end element
|
|
attribute (str): attribute to interpolate
|
|
|
|
Raises:
|
|
ValueError: if the attribute is not color-like
|
|
ValueError: if the attribute is unset on both start and end style
|
|
|
|
Returns:
|
|
AttributeInterpolator: an interpolator object whose type depends on the
|
|
attribute.
|
|
"""
|
|
if attribute not in Style.color_props:
|
|
raise ValueError("attribute must be a color property")
|
|
|
|
sstyle = AttributeInterpolator.best_style(snode)
|
|
estyle = AttributeInterpolator.best_style(enode)
|
|
|
|
styles = [[snode, sstyle], [enode, estyle]]
|
|
for cur, curstyle in styles:
|
|
if curstyle(attribute) is None:
|
|
cur.style[attribute + "-opacity"] = 0.0
|
|
if attribute == "stroke":
|
|
cur.style["stroke-width"] = 0.0
|
|
|
|
# check if style is none, unset or a color
|
|
if isinstance(
|
|
sstyle(attribute), (LinearGradient, RadialGradient)
|
|
) or isinstance(estyle(attribute), (LinearGradient, RadialGradient)):
|
|
# if one of the two styles is a gradient, use gradient interpolation.
|
|
try:
|
|
return GradientInterpolator.create(snode, enode, attribute)
|
|
except ValueError:
|
|
# different gradient types, just duplicate the first
|
|
return TrivialInterpolator(sstyle(attribute))
|
|
if sstyle(attribute) is None and estyle(attribute) is None:
|
|
return TrivialInterpolator("none")
|
|
return ColorInterpolator.create(sstyle, estyle, attribute)
|
|
|
|
def interpolate(self, time=0):
|
|
"""Interpolates a style using the interpolators set in self.interpolators
|
|
|
|
Args:
|
|
time (int, optional): Interpolation position. If 0, start_value is returned,
|
|
if 1, end_value is returned. Defaults to 0.
|
|
|
|
Returns:
|
|
inkex.Style: interpolated style
|
|
"""
|
|
style = Style()
|
|
for prop, interp in self.interpolators.items():
|
|
style[prop] = interp.interpolate(time)
|
|
return style
|
|
|
|
|
|
class TrivialInterpolator(AttributeInterpolator):
|
|
"""Trivial interpolator, returns value for every time"""
|
|
|
|
def __init__(self, value):
|
|
super().__init__(value, value)
|
|
|
|
def interpolate(self, time=0):
|
|
return self.start_value
|
|
|
|
|
|
class ValueInterpolator(AttributeInterpolator):
|
|
"""Class for interpolation of a single value"""
|
|
|
|
def __init__(self, start_value=0, end_value=0):
|
|
super().__init__(float(start_value), float(end_value))
|
|
|
|
def interpolate(self, time=0):
|
|
"""(Linearly) interpolates a value
|
|
|
|
Args:
|
|
time (int, optional): Interpolation position. If 0, start_value is returned,
|
|
if 1, end_value is returned. Defaults to 0.
|
|
|
|
Returns:
|
|
int: interpolated value
|
|
"""
|
|
return self.start_value + ((self.end_value - self.start_value) * time)
|
|
|
|
|
|
class UnitValueInterpolator(ValueInterpolator):
|
|
"""Class for interpolation of a value with unit"""
|
|
|
|
def __init__(self, start_value=0, end_value=0):
|
|
start_val, start_unit = parse_unit(start_value)
|
|
end_val = convert_unit(end_value, start_unit)
|
|
super().__init__(start_val, end_val)
|
|
self.unit = start_unit
|
|
|
|
def interpolate(self, time=0):
|
|
return render_unit(super().interpolate(time), self.unit)
|
|
|
|
|
|
class ArrayInterpolator(AttributeInterpolator):
|
|
"""Interpolates array-like objects element-wise, e.g. color, transform,
|
|
coordinate"""
|
|
|
|
def __init__(self, start_value, end_value):
|
|
super().__init__(start_value, end_value)
|
|
self.interpolators = [
|
|
ValueInterpolator(cur, other)
|
|
for (cur, other) in zip(start_value, end_value)
|
|
]
|
|
|
|
def interpolate(self, time=0):
|
|
"""Interpolates an array element-wise
|
|
|
|
Args:
|
|
time (int, optional): [description]. Defaults to 0.
|
|
|
|
Returns:
|
|
List: interpolated array
|
|
"""
|
|
return [interp.interpolate(time) for interp in self.interpolators]
|
|
|
|
|
|
class TransformInterpolator(ArrayInterpolator):
|
|
"""Class for interpolation of transforms"""
|
|
|
|
def __init__(self, start_value=Transform(), end_value=Transform()):
|
|
"""Creates a transform interpolator.
|
|
|
|
Args:
|
|
start_value (inkex.Transform, optional): start transform. Defaults to
|
|
inkex.Transform().
|
|
end_value (inkex.Transform, optional): end transform. Defaults to
|
|
inkex.Transform().
|
|
"""
|
|
super().__init__(start_value.to_hexad(), end_value.to_hexad())
|
|
|
|
def interpolate(self, time=0):
|
|
"""Interpolates a transform by interpolating each item in the transform hexad
|
|
separately.
|
|
|
|
Args:
|
|
time (int, optional): Interpolation position. If 0, start_value is returned,
|
|
if 1, end_value is returned. Defaults to 0.
|
|
|
|
Returns:
|
|
Transform: interpolated transform
|
|
"""
|
|
return Transform(super().interpolate(time))
|
|
|
|
|
|
class ColorInterpolator(ArrayInterpolator):
|
|
"""Class for color interpolation"""
|
|
|
|
@staticmethod
|
|
def create(sst, est, attribute):
|
|
"""Creates a ColorInterpolator for either Fill or stroke, depending on the
|
|
attribute.
|
|
|
|
Args:
|
|
sst (Style): Start style
|
|
est (Style): End style
|
|
attribute (string): either fill or stroke
|
|
|
|
Raises:
|
|
ValueError: if none of the start or end style is a color.
|
|
|
|
Returns:
|
|
ColorInterpolator: A ColorInterpolator object
|
|
"""
|
|
styles = [sst, est]
|
|
for cur, other in zip(styles, reversed(styles)):
|
|
if not isinstance(cur(attribute), Color) or cur(attribute) is None:
|
|
cur[attribute] = other(attribute)
|
|
this = ColorInterpolator(
|
|
Color(styles[0](attribute)), Color(styles[1](attribute))
|
|
)
|
|
if this is None:
|
|
raise ValueError("One of the two attribute needs to be a plain color")
|
|
return this
|
|
|
|
def __init__(self, start_value=Color("#000000"), end_value=Color("#000000")):
|
|
# Remember what type the color was, handle none types as a special case
|
|
# so we can tween from "none" to some color effectively.
|
|
self.output_type = type(start_value)
|
|
if self.output_type.name == "none":
|
|
self.output_type = type(end_value)
|
|
|
|
# We tween alpha is there is any in either value
|
|
tween_alpha = (
|
|
start_value.effective_alpha != end_value.effective_alpha
|
|
or start_value.alpha is not None
|
|
or end_value.alpha is not None
|
|
)
|
|
super().__init__(
|
|
start_value.get_values(tween_alpha), end_value.get_values(tween_alpha)
|
|
)
|
|
|
|
def interpolate(self, time=0):
|
|
"""Interpolates a color by interpolating its channels separately.
|
|
|
|
Args:
|
|
time (int, optional): Interpolation position. If 0, start_value is returned,
|
|
if 1, end_value is returned. Defaults to 0.
|
|
|
|
Returns:
|
|
Color: interpolated color
|
|
"""
|
|
return self.output_type(list(map(float, super().interpolate(time))))
|
|
|
|
|
|
class GradientInterpolator(AttributeInterpolator):
|
|
"""Base class for Gradient Interpolation"""
|
|
|
|
def __init__(self, start_value, end_value, svg=None):
|
|
super().__init__(start_value, end_value)
|
|
self.svg = svg
|
|
# If one of the styles is empty, set it to the gradient of the other
|
|
if start_value is None:
|
|
self.start_value = end_value
|
|
if end_value is None:
|
|
self.end_value = start_value
|
|
self.transform_interpolator = TransformInterpolator(
|
|
self.start_value.gradientTransform, self.end_value.gradientTransform
|
|
)
|
|
self.orientation_interpolator = {
|
|
attr: UnitValueInterpolator(
|
|
self.start_value.get(attr), self.end_value.get(attr)
|
|
)
|
|
for attr in self.start_value.orientation_attributes
|
|
if self.start_value.get(attr) is not None
|
|
and self.end_value.get(attr) is not None
|
|
}
|
|
if not (
|
|
self.start_value.href is not None
|
|
and self.start_value.href is self.end_value.href
|
|
):
|
|
# the gradient link to different stops, interpolate between them
|
|
# add both start and end offsets, then take distict
|
|
newoffsets = sorted(
|
|
list(set(self.start_value.stop_offsets + self.end_value.stop_offsets))
|
|
)
|
|
|
|
def func(start, end, time):
|
|
return StopInterpolator(start, end).interpolate(time)
|
|
|
|
sstops = GradientInterpolator.interpolate_linear_list(
|
|
self.start_value.stop_offsets,
|
|
list(self.start_value.stops),
|
|
newoffsets,
|
|
func,
|
|
)
|
|
ostops = GradientInterpolator.interpolate_linear_list(
|
|
self.end_value.stop_offsets,
|
|
list(self.end_value.stops),
|
|
newoffsets,
|
|
func,
|
|
)
|
|
self.newstop_interpolator = [
|
|
StopInterpolator(s1, s2) for s1, s2 in zip(sstops, ostops)
|
|
]
|
|
else:
|
|
self.newstop_interpolator = None
|
|
|
|
@staticmethod
|
|
def create(snode, enode, attribute):
|
|
"""Creates a `GradientInterpolator` for either fill or stroke, depending on
|
|
attribute.
|
|
|
|
Cases: (A, B) -> Interpolator
|
|
|
|
- Linear Gradient, Linear Gradient -> LinearGradientInterpolator
|
|
- Color or None, Linear Gradient -> LinearGradientInterpolator
|
|
- Radial Gradient, Radial Gradient -> RadialGradientInterpolator
|
|
- Color or None, Radial Gradient -> RadialGradientInterpolator
|
|
- Radial Gradient, Linear Gradient -> ValueError
|
|
- Color or None, Color or None -> ValueError
|
|
|
|
Args:
|
|
snode (BaseElement): start element
|
|
enode (BaseElement): end element
|
|
attribute (string): either fill or stroke
|
|
|
|
Raises:
|
|
ValueError: if none of the styles are a gradient or if they are gradients
|
|
of different types
|
|
|
|
Returns:
|
|
GradientInterpolator: an Interpolator object
|
|
"""
|
|
interpolator = None
|
|
gradienttype = None
|
|
# first find out which type of interpolator we need
|
|
sstyle = AttributeInterpolator.best_style(snode)
|
|
estyle = AttributeInterpolator.best_style(enode)
|
|
for cur in [sstyle, estyle]:
|
|
curgrad = None
|
|
if isinstance(cur(attribute), (LinearGradient, RadialGradient)):
|
|
curgrad = cur(attribute)
|
|
for gradtype, interp in [
|
|
[LinearGradient, LinearGradientInterpolator],
|
|
[RadialGradient, RadialGradientInterpolator],
|
|
]:
|
|
if curgrad is not None and isinstance(curgrad, gradtype):
|
|
if interpolator is None:
|
|
interpolator = interp
|
|
gradienttype = gradtype
|
|
if not (interp == interpolator):
|
|
raise ValueError("Gradient types don't match")
|
|
# If one of the styles is empty, set it to the gradient of the other, but with
|
|
# zero opacity (and stroke-width for strokes)
|
|
# If one of the styles is a plain color, replace it by a gradient with a single
|
|
# stop
|
|
iterator = [[snode, gradienttype(), enode], [enode, gradienttype(), snode]]
|
|
for index in [0, 1]:
|
|
curstyle = AttributeInterpolator.best_style(iterator[index][0])
|
|
value = curstyle(attribute)
|
|
if value is None:
|
|
# if the attribute of one of the two ends is unset, set the opacity to
|
|
# zero.
|
|
iterator[index][0].style[attribute + "-opacity"] = 0.0
|
|
if attribute == "stroke":
|
|
iterator[index][0].style["stroke-width"] = 0.0
|
|
if isinstance(value, Color):
|
|
# if the attribute of one of the two ends is a color, convert it to a
|
|
# one-stop gradient. Type depends on the type of the other gradient.
|
|
interpolator.initialize_position(
|
|
iterator[index][1], iterator[index][0].bounding_box()
|
|
)
|
|
stop = Stop()
|
|
stop.style = Style()
|
|
stop.style["stop-color"] = value
|
|
stop.offset = 0
|
|
iterator[index][1].add(stop)
|
|
stop = Stop()
|
|
stop.style = Style()
|
|
stop.style["stop-color"] = value
|
|
stop.offset = 1
|
|
iterator[index][1].add(stop)
|
|
else:
|
|
iterator[index][1] = value # is a gradient
|
|
if interpolator is None:
|
|
raise ValueError("None of the two styles is a gradient")
|
|
if interpolator in [LinearGradientInterpolator, RadialGradientInterpolator]:
|
|
return interpolator(iterator[0][1], iterator[1][1], snode)
|
|
return interpolator(iterator[0][1], iterator[1][1])
|
|
|
|
@staticmethod
|
|
def interpolate_linear_list(positions, values, newpositions, func):
|
|
"""Interpolates a list of values given at n positions to the best approximation
|
|
at m newpositions.
|
|
|
|
>>>
|
|
|
|
|
| x
|
|
| x
|
|
_________________
|
|
pq q p q
|
|
(x denotes function values, p: positions, q: newpositions)
|
|
A function may be given to interpolate between given values.
|
|
|
|
Args:
|
|
positions (list[number-like]): position of current function values
|
|
values (list[Type]): list of arbitrary type,
|
|
``len(values) == len(positions)``
|
|
newpositions (list[number-like]): position of interpolated values
|
|
func (Callable[[Type, Type, float], Type]): Function to interpolate between
|
|
values
|
|
|
|
Returns:
|
|
list[Type]: interpolated function values at positions
|
|
"""
|
|
newvalues = []
|
|
positions = list(map(float, positions))
|
|
newpositions = list(map(float, newpositions))
|
|
for pos in newpositions:
|
|
if len(positions) == 1:
|
|
newvalues.append(values[0])
|
|
else:
|
|
# current run:
|
|
# idxl pos idxr
|
|
# p p | p
|
|
# q q
|
|
idxl = max(0, bisect_left(positions, pos) - 1)
|
|
idxr = min(len(positions) - 1, idxl + 1)
|
|
fraction = (pos - positions[idxl]) / (positions[idxr] - positions[idxl])
|
|
vall = values[idxl]
|
|
valr = values[idxr]
|
|
newval = func(vall, valr, fraction)
|
|
newvalues.append(newval)
|
|
return newvalues
|
|
|
|
@staticmethod
|
|
def append_to_doc(element, gradient):
|
|
"""Splits a gradient into stops and orientation, appends it to the document's
|
|
defs and returns the href to the orientation gradient.
|
|
|
|
Args:
|
|
element (BaseElement): an element inside the SVG that the gradient should be
|
|
added to
|
|
gradient (Gradient): the gradient to append to the document
|
|
|
|
Returns:
|
|
Gradient: the orientation gradient, or the gradient object if
|
|
element has no root or is None
|
|
"""
|
|
stops, orientation = gradient.stops_and_orientation()
|
|
if element is None or (
|
|
element.getparent() is None and not isinstance(element, SvgDocumentElement)
|
|
):
|
|
return gradient
|
|
element.root.defs.add(orientation)
|
|
if len(stops) > 0:
|
|
element.root.defs.add(stops, orientation)
|
|
orientation.href = stops.get_id()
|
|
return orientation
|
|
|
|
def interpolate(self, time=0):
|
|
"""Interpolate with another gradient."""
|
|
newgrad = self.start_value.copy()
|
|
# interpolate transforms
|
|
newgrad.gradientTransform = self.transform_interpolator.interpolate(time)
|
|
|
|
# interpolate orientation
|
|
for attr in self.orientation_interpolator.keys():
|
|
newgrad.set(attr, self.orientation_interpolator[attr].interpolate(time))
|
|
|
|
# interpolate stops
|
|
if self.newstop_interpolator is not None:
|
|
newgrad.remove_all(Stop)
|
|
newgrad.add(
|
|
*[interp.interpolate(time) for interp in self.newstop_interpolator]
|
|
)
|
|
if self.svg is None:
|
|
return newgrad
|
|
return GradientInterpolator.append_to_doc(self.svg, newgrad)
|
|
|
|
|
|
class LinearGradientInterpolator(GradientInterpolator):
|
|
"""Class for interpolation of linear gradients"""
|
|
|
|
def __init__(
|
|
self, start_value=LinearGradient(), end_value=LinearGradient(), svg=None
|
|
):
|
|
super().__init__(start_value, end_value, svg)
|
|
|
|
@staticmethod
|
|
def initialize_position(grad, bbox):
|
|
"""Initializes a linear gradient's position"""
|
|
grad.set("x1", bbox.left)
|
|
grad.set("x2", bbox.right)
|
|
grad.set("y1", bbox.center.y)
|
|
grad.set("y2", bbox.center.y)
|
|
|
|
|
|
class RadialGradientInterpolator(GradientInterpolator):
|
|
"""Class to interpolate radial gradients"""
|
|
|
|
def __init__(
|
|
self, start_value=RadialGradient(), end_value=RadialGradient(), svg=None
|
|
):
|
|
super().__init__(start_value, end_value, svg)
|
|
|
|
@staticmethod
|
|
def initialize_position(grad, bbox):
|
|
"""Initializes a radial gradient's position"""
|
|
x, y = bbox.center
|
|
grad.set("cx", x)
|
|
grad.set("cy", y)
|
|
grad.set("fx", x)
|
|
grad.set("fy", y)
|
|
grad.set("r", bbox.right - bbox.center.x)
|
|
|
|
|
|
class StopInterpolator(AttributeInterpolator):
|
|
"""Class to interpolate gradient stops"""
|
|
|
|
def __init__(self, start_value, end_value):
|
|
super().__init__(start_value, end_value)
|
|
self.style_interpolator = StyleInterpolator(start_value, end_value)
|
|
self.position_interpolator = ValueInterpolator(
|
|
float(start_value.offset), float(end_value.offset)
|
|
)
|
|
|
|
def interpolate(self, time=0):
|
|
"""Interpolates a gradient stop by interpolating style and offset separately
|
|
|
|
Args:
|
|
time (int, optional): Interpolation position. If 0, start_value is returned,
|
|
if 1, end_value is returned. Defaults to 0.
|
|
|
|
Returns:
|
|
Stop: interpolated gradient stop
|
|
"""
|
|
newstop = Stop()
|
|
newstop.style = self.style_interpolator.interpolate(time)
|
|
newstop.offset = self.position_interpolator.interpolate(time)
|
|
return newstop
|
|
|
|
|
|
class PathInterpolator(AttributeInterpolator):
|
|
"""Base class for Path interpolation"""
|
|
|
|
def __init__(self, start_value=Path(), end_value=Path()):
|
|
super().__init__(start_value.to_superpath(), end_value.to_superpath())
|
|
self.processed_end_path = None
|
|
self.processed_start_path = None
|
|
|
|
def truncate_subpaths(self):
|
|
"""Truncates the longer path so that all subpaths in both paths have an equal
|
|
number of bezier commands"""
|
|
s = [[]]
|
|
e = [[]]
|
|
# loop through all subpaths as long as there are remaining ones
|
|
while self.start_value and self.end_value:
|
|
# if both subpaths contain a bezier command, append it to s and e
|
|
if self.start_value[0] and self.end_value[0]:
|
|
s[-1].append(self.start_value[0].pop(0))
|
|
e[-1].append(self.end_value[0].pop(0))
|
|
# if the subpath of start_value is empty, add the remaining empty list as
|
|
# new subpath of s and one more item of end_value as new subpath of e.
|
|
# Afterwards, the loop terminates
|
|
elif self.end_value[0]:
|
|
s.append(self.start_value.pop(0))
|
|
e[-1].append(self.end_value[0][0])
|
|
e.append([self.end_value[0].pop(0)])
|
|
elif self.start_value[0]:
|
|
e.append(self.end_value.pop(0))
|
|
s[-1].append(self.start_value[0][0])
|
|
s.append([self.start_value[0].pop(0)])
|
|
# if there are no commands left in both start_value or end_value, add empty
|
|
# list to both start_value and end_value
|
|
else:
|
|
s.append(self.start_value.pop(0))
|
|
e.append(self.end_value.pop(0))
|
|
self.processed_start_path = s
|
|
self.processed_end_path = e
|
|
|
|
def interpolate(self, time=0):
|
|
# create an interpolated path for each interval
|
|
interp = []
|
|
# process subpaths
|
|
for ssubpath, esubpath in zip(
|
|
self.processed_start_path, self.processed_end_path
|
|
):
|
|
if not (ssubpath or esubpath):
|
|
break
|
|
# add a new subpath to the interpolated path
|
|
interp.append([])
|
|
# process each bezier command in the subpaths (which now have equal length)
|
|
for sbezier, ebezier in zip(ssubpath, esubpath):
|
|
if not (sbezier or ebezier):
|
|
break
|
|
# add a new bezier command to the last subpath
|
|
interp[-1].append([])
|
|
# process points
|
|
for point1, point2 in zip(sbezier, ebezier):
|
|
if not (point1 or point2):
|
|
break
|
|
# add a new point to the last bezier command
|
|
interp[-1][-1].append(
|
|
ArrayInterpolator(point1, point2).interpolate(time)
|
|
)
|
|
# remove final subpath if empty.
|
|
if not interp[-1]:
|
|
del interp[-1]
|
|
return CubicSuperPath(interp)
|
|
|
|
|
|
class EqualSubsegmentsInterpolator(PathInterpolator):
|
|
"""Interpolates the path by rediscretizing the subpaths first."""
|
|
|
|
@staticmethod
|
|
def get_subpath_lenghts(path):
|
|
"""prepare lengths for interpolation"""
|
|
sp_lenghts, total = csplength(path)
|
|
t = 0
|
|
lenghts = []
|
|
for sp in sp_lenghts:
|
|
for l in sp:
|
|
t += l / total
|
|
lenghts.append(t)
|
|
lenghts.sort()
|
|
return sp_lenghts, total, lenghts
|
|
|
|
@staticmethod
|
|
def process_path(path, other):
|
|
"""Rediscretize path so that all subpaths have an equal number of segments,
|
|
so that there is a node at the path "times" where path or other have a node
|
|
|
|
Args:
|
|
path (Path): the first path
|
|
other (Path): the second path
|
|
|
|
Returns:
|
|
Array: the prepared path description for the intermediate path"""
|
|
sp_lenghts, total, _ = EqualSubsegmentsInterpolator.get_subpath_lenghts(path)
|
|
_, _, lenghts = EqualSubsegmentsInterpolator.get_subpath_lenghts(other)
|
|
t = 0
|
|
s = [[]]
|
|
for sp in sp_lenghts:
|
|
if not path[0]:
|
|
s.append(path.pop(0))
|
|
s[-1].append(path[0].pop(0))
|
|
for l in sp:
|
|
pt = t
|
|
t += l / total
|
|
if lenghts and t > lenghts[0]:
|
|
while lenghts and lenghts[0] < t:
|
|
nt = (lenghts[0] - pt) / (t - pt)
|
|
bezes = cspbezsplitatlength(s[-1][-1][:], path[0][0][:], nt)
|
|
s[-1][-1:] = bezes[:2]
|
|
path[0][0] = bezes[2]
|
|
pt = lenghts.pop(0)
|
|
s[-1].append(path[0].pop(0))
|
|
return s
|
|
|
|
def __init__(self, start_path=Path(), end_path=Path()):
|
|
super().__init__(start_path, end_path)
|
|
# rediscretisize both paths
|
|
start_copy = copy.deepcopy(self.start_value)
|
|
# TODO find out why self.start_value.copy() doesn't work
|
|
self.start_value = EqualSubsegmentsInterpolator.process_path(
|
|
self.start_value, self.end_value
|
|
)
|
|
self.end_value = EqualSubsegmentsInterpolator.process_path(
|
|
self.end_value, start_copy
|
|
)
|
|
|
|
self.truncate_subpaths()
|
|
|
|
|
|
class FirstNodesInterpolator(PathInterpolator):
|
|
"""Interpolates a path by discarding the trailing nodes of the longer subpath"""
|
|
|
|
def __init__(self, start_path=Path(), end_path=Path()):
|
|
super().__init__(start_path, end_path)
|
|
# which path has fewer segments?
|
|
lengthdiff = len(self.start_value) - len(self.end_value)
|
|
# swap shortest first
|
|
if lengthdiff > 0:
|
|
self.start_value, self.end_value = self.end_value, self.start_value
|
|
# subdivide the shorter path
|
|
for _ in range(abs(lengthdiff)):
|
|
maxlen = 0
|
|
subpath = 0
|
|
segment = 0
|
|
for y, _ in enumerate(self.start_value):
|
|
for z in range(1, len(self.start_value[y])):
|
|
leng = bezlenapprx(
|
|
self.start_value[y][z - 1], self.start_value[y][z]
|
|
)
|
|
if leng > maxlen:
|
|
maxlen = leng
|
|
subpath = y
|
|
segment = z
|
|
sp1, sp2 = self.start_value[subpath][segment - 1 : segment + 1]
|
|
self.start_value[subpath][segment - 1 : segment + 1] = cspbezsplit(sp1, sp2)
|
|
# if swapped, swap them back
|
|
if lengthdiff > 0:
|
|
self.start_value, self.end_value = self.end_value, self.start_value
|
|
self.truncate_subpaths()
|