Commit 7862b208 authored by Martin Owens's avatar Martin Owens 🕘
Browse files

Merge \!183 new feature: path interpolation with gradients by Matthias Meschede

parent 17dec7b4
Loading
Loading
Loading
Loading
Loading
+9 −1
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ Basic color controls
"""

from .utils import PY3
from .tween import interpcoord

# All the names that get added to the inkex API itself.
__all__ = ('Color', 'ColorError', 'ColorIdError')
@@ -297,7 +298,7 @@ class Color(list):
            return 'rgb', None

        if color.startswith('url('):
            raise ColorIdError("Gradient other referenced element id.")
            raise ColorIdError("Color references other element id, e.g. a gradient")

        # Next handle short colors (css: #abc -> #aabbcc)
        if color.startswith('#'):
@@ -409,6 +410,13 @@ class Color(list):
            return Color()
        return Color(COLOR_SVG.get(str(self), str(self)))

    def interpolate(self, other, fraction):
        """Iterpolate two colours by the given fraction"""
        return Color(
            [interpcoord(c1, c2, fraction)
             for (c1, c2) in zip(self.to_floats(), other.to_floats())]
            )


def rgb_to_hsl(red, green, blue):
    """RGB to HSL colour conversion"""
+3 −3
Original line number Diff line number Diff line
@@ -14,5 +14,5 @@ from ._text import FlowRegion, FlowRoot, FlowPara, FlowDiv, FlowSpan, TextElemen
from ._use import Symbol, Use
from ._meta import Defs, StyleElement, Script, Desc, Title, NamedView, Guide, \
                   Metadata, ForeignObject, Switch, Grid
from ._filters import Filter, Pattern, Gradient, LinearGradient, RadialGradient, PathEffect
from ._filters import Filter, Pattern, Gradient, LinearGradient, RadialGradient, PathEffect, Stop
from ._image import Image
+6 −0
Original line number Diff line number Diff line
@@ -333,6 +333,12 @@ class BaseElement(etree.ElementBase):
        if self.getparent() is not None:
            self.getparent().remove(self)

    def remove_all(self, *types):
        """Remove all children or child types"""
        for child in self:
            if not types or isinstance(child, types):
                self.remove(child)

    def replace_with(self, elem):
        """Replace this element with the given element"""
        self.addnext(elem)
+101 −1
Original line number Diff line number Diff line
@@ -24,10 +24,13 @@ Element interface for patterns, filters, gradients and path effects.
"""

from lxml import etree
from copy import deepcopy

from ..utils import addNS
from ..transforms import Transform
from ..tween import interpcoord, interp

from ..styles import Style
from ._base import BaseElement

class Filter(BaseElement):
@@ -92,6 +95,24 @@ class Filter(BaseElement):
        tag_name = 'feTurbulence'


class Stop(BaseElement):
    tag_name = 'stop'

    @property
    def offset(self):
        return self.get('offset')

    @offset.setter
    def offset(self, number):
        self.set('offset', number)

    def interpolate(self, other, fraction):
        newstop = Stop()
        newstop.style = self.style.interpolate(other.style, fraction)
        newstop.offset = interpcoord(float(self.offset), float(other.offset), fraction)
        return newstop


class Pattern(BaseElement):
    """Pattern element which is used in the def to control repeating fills"""
    tag_name = 'pattern'
@@ -102,14 +123,93 @@ class Gradient(BaseElement):
    """A gradient instruction usually in the defs"""
    WRAPPED_ATTRS = BaseElement.WRAPPED_ATTRS + (('gradientTransform', Transform),)

    orientation_attributes = ()

    @property
    def stops(self): # type: () -> List[Stop]
        """Return an ordered list of own or linked stop nodes"""
        gradcolor = self.href if isinstance(self.href, LinearGradient) else self
        return sorted(gradcolor, key=lambda x: float(x.offset))

    @property
    def stop_offsets(self): # type: () -> List[float]
        """Return a list of own or linked stop offsets"""
        return [child.offset for child in self.stops]

    @property
    def stop_styles(self): # type: () -> List[Style]
        """Return a list of own or linked offset styles"""
        return [child.style for child in self.stops]

    def remove_orientation(self):
        """Remove all orientation attributes from this element"""
        for attr in self.orientation_attributes:
            self.pop(attr)

    def interpolate(self, other, fraction): # type: (LinearGradient, float) -> LinearGradient
        """Interpolate with another gradient."""
        if self.tag_name != other.tag_name:
            return self
        newgrad = self.copy()

        # interpolate transforms
        newtransform = self.gradientTransform.interpolate(other.gradientTransform, fraction)
        newgrad.gradientTransform = newtransform

        # interpolate orientation
        for attr in self.orientation_attributes:
            newattr = interpcoord(float(self.get(attr)), float(other.get(attr)), fraction)
            newgrad.set(attr, newattr)

        # interpolate stops
        if self.href is not None and self.href is other.href:
            # both gradients link to the same stops
            pass
        else:
            # gradients might have different stops
            newoffsets = sorted(self.stop_offsets + other.stop_offsets[1:-1])
            sstops = interp(self.stop_offsets, self.stops, newoffsets)
            ostops = interp(other.stop_offsets, other.stops, newoffsets)
            newstops = [s1.interpolate(s2, fraction) for s1, s2 in zip(sstops, ostops)]
            newgrad.remove_all(Stop)
            newgrad.add(*newstops)
        return newgrad

    def stops_and_orientation(self):
        """Return a copy of all the stops in this gradient"""
        stops = self.copy()
        stops.remove_orientation()
        orientation = self.copy()
        orientation.remove_all(Stop)
        return stops, orientation


class LinearGradient(Gradient):
    tag_name = 'linearGradient'
    orientation_attributes = ('x1', 'y1', 'x2', 'y2')

    def apply_transform(self): # type: () -> None
       """Apply transform to orientation points and set it to identity."""
       trans = self.pop('gradientTransform')
       p1 = (float(self.get('x1')), float(self.get('y1')))
       p2 = (float(self.get('x2')), float(self.get('y2')))
       p1t = trans.apply_to_point(p1)
       p2t = trans.apply_to_point(p2)
       self.update(x1=p1t[0], y1=p1t[1], x2=p2t[0], y2=p2t[1])


class RadialGradient(Gradient):
    tag_name = 'radialGradient'

    orientation_attributes = ('cx', 'cy', 'fx', 'fy', 'r')

    def apply_transform(self): # type: () -> None
       """Apply transform to orientation points and set it to identity."""
       trans = self.pop('gradientTransform')
       p1 = (float(self.get('cx')), float(self.get('cy')))
       p2 = (float(self.get('fx')), float(self.get('fy')))
       p1t = trans.apply_to_point(p1)
       p2t = trans.apply_to_point(p2)
       self.update(cx=p1t[0], cy=p1t[1], fx=p2t[0], fy=p2t[1])

class PathEffect(BaseElement):
    """Inkscape LPE element"""
+36 −2
Original line number Diff line number Diff line
@@ -25,7 +25,8 @@ import re
from collections import OrderedDict

from .utils import PY3
from .colors import Color
from .colors import Color, ColorIdError
from .tween import interpcoord, interpunit

if PY3:
    unicode = str  # pylint: disable=redefined-builtin,invalid-name
@@ -72,7 +73,8 @@ class Classes(list):
class Style(OrderedDict):
    """A list of style directives"""
    color_props = ('stroke', 'fill', 'stop-color', 'flood-color', 'lighting-color')
    opacity_props = ('stroke-opacity', 'fill-opacity', 'opacity')
    opacity_props = ('stroke-opacity', 'fill-opacity', 'opacity', 'stop-opacity')
    unit_props = ('stroke-width')

    def __init__(self, style=None, callback=None, **kw):
        # This callback is set twice because this is 'pre-initial' data (no callback)
@@ -170,6 +172,38 @@ class Style(OrderedDict):
            if value == 'url(#{})'.format(old_id):
                self[name] = 'url(#{})'.format(new_id)

    def interpolate_prop(self, other, fraction, prop, svg=None):
        """Interpolate specific property."""
        a1 = self[prop]
        a2 = other.get(prop, None)
        if a2 is None:
            val = a1
        else:
            if prop in self.color_props:
                if isinstance(a1, Color):
                    val = a1.interpolate(Color(a2), fraction)
                elif a1.startswith('url(') or a2.startswith('url('):
                    # gradient requires changes to the whole svg
                    # and needs to be handled externally
                    val = a1
                else:
                    val = Color(a1).interpolate(Color(a2), fraction)
            elif prop in self.opacity_props:
                val = interpcoord(float(a1), float(a2), fraction)
            elif prop in self.unit_props:
                val = interpunit(a1, a2, fraction)
            else:
                val = a1
        return val

    def interpolate(self, other, fraction):  # type: (Style, float, Optional[str], Optional[str]) -> Style
        """Interpolate all properties."""
        style = Style()
        for prop, value in self.items():
            style[prop] = self.interpolate_prop(other, fraction, prop)
        return style


class AttrFallbackStyle(object):
    """
    A container for a style and an element that may have competing styles
Loading