# SPDX-License-Identifier: GPL-2.0-or-later

#  Filename : shaders.py
#  Authors  : Fredo Durand, Stephane Grabli, Francois Sillion, Emmanuel Turquin
#  Date     : 11/08/2005
#  Purpose  : Stroke shaders to be used for creation of stylized strokes

"""
This module contains stroke shaders used for creation of stylized
strokes.  It is also intended to be a collection of examples for
shader definition in Python.

User-defined stroke shaders inherit the
:class:`freestyle.types.StrokeShader` class.
"""

__all__ = (
    "BackboneStretcherShader",
    "BezierCurveShader",
    "BlenderTextureShader",
    "CalligraphicShader",
    "ColorNoiseShader",
    "ConstantColorShader",
    "ConstantThicknessShader",
    "ConstrainedIncreasingThicknessShader",
    "GuidingLinesShader",
    "IncreasingColorShader",
    "IncreasingThicknessShader",
    "PolygonalizationShader",
    "RoundCapShader",
    "SamplingShader",
    "SmoothingShader",
    "SpatialNoiseShader",
    "SquareCapShader",
    "StrokeTextureStepShader",
    "ThicknessNoiseShader",
    "TipRemoverShader",
    "py2DCurvatureColorShader",
    "pyBackboneStretcherNoCuspShader",
    "pyBackboneStretcherShader",
    "pyBluePrintCirclesShader",
    "pyBluePrintDirectedSquaresShader",
    "pyBluePrintEllipsesShader",
    "pyBluePrintSquaresShader",
    "pyConstantColorShader",
    "pyConstantThicknessShader",
    "pyConstrainedIncreasingThicknessShader",
    "pyDecreasingThicknessShader",
    "pyDepthDiscontinuityThicknessShader",
    "pyDiffusion2Shader",
    "pyFXSVaryingThicknessWithDensityShader",
    "pyGuidingLineShader",
    "pyHLRShader",
    "pyImportance2DThicknessShader",
    "pyImportance3DThicknessShader",
    "pyIncreasingColorShader",
    "pyIncreasingThicknessShader",
    "pyInterpolateColorShader",
    "pyLengthDependingBackboneStretcherShader",
    "pyMaterialColorShader",
    "pyModulateAlphaShader",
    "pyNonLinearVaryingThicknessShader",
    "pyPerlinNoise1DShader",
    "pyPerlinNoise2DShader",
    "pyRandomColorShader",
    "pySLERPThicknessShader",
    "pySamplingShader",
    "pySinusDisplacementShader",
    "pyTVertexRemoverShader",
    "pyTVertexThickenerShader",
    "pyTimeColorShader",
    "pyTipRemoverShader",
    "pyZDependingThicknessShader",
)


# module members
from _freestyle import (
    BackboneStretcherShader,
    BezierCurveShader,
    BlenderTextureShader,
    CalligraphicShader,
    ColorNoiseShader,
    ConstantColorShader,
    ConstantThicknessShader,
    ConstrainedIncreasingThicknessShader,
    GuidingLinesShader,
    IncreasingColorShader,
    IncreasingThicknessShader,
    PolygonalizationShader,
    SamplingShader,
    SmoothingShader,
    SpatialNoiseShader,
    StrokeTextureStepShader,
    ThicknessNoiseShader,
    TipRemoverShader,
)

# constructs for shader definition in Python
from freestyle.types import (
    Interface0DIterator,
    Nature,
    Noise,
    StrokeAttribute,
    StrokeShader,
    StrokeVertexIterator,
    StrokeVertex,
)
from freestyle.functions import (
    Curvature2DAngleF0D,
    DensityF0D,
    GetProjectedZF0D,
    MaterialF0D,
    Normal2DF0D,
    Orientation2DF1D,
    ZDiscontinuityF0D,
)
from freestyle.predicates import (
    pyVertexNatureUP0D,
    pyUEqualsUP0D,
)

from freestyle.utils import (
    bound,
    BoundingBox,
    pairwise,
    phase_to_direction,
)

from freestyle.utils import ContextFunctions as CF

import bpy
import random

from math import atan, cos, pi, sin, sinh, sqrt
from mathutils import Vector
from random import randint


# -- Thickness Stroke Shaders -- #


class pyDepthDiscontinuityThicknessShader(StrokeShader):
    """
    Assigns a thickness to the stroke based on the stroke's distance
    to the camera (Z-value).
    """

    def __init__(self, min, max):
        StrokeShader.__init__(self)
        self.a = max - min
        self.b = min
        self.func = ZDiscontinuityF0D()

    def shade(self, stroke):
        it = Interface0DIterator(stroke)
        for svert in it:
            z = self.func(it)
            thickness = self.a * z + self.b
            svert.attribute.thickness = (thickness, thickness)


class pyConstantThicknessShader(StrokeShader):
    """
    Assigns a constant thickness along the stroke.
    """

    def __init__(self, thickness):
        StrokeShader.__init__(self)
        self._thickness = thickness / 2.0

    def shade(self, stroke):
        for svert in stroke:
            svert.attribute.thickness = (self._thickness, self._thickness)


class pyFXSVaryingThicknessWithDensityShader(StrokeShader):
    """
    Assigns thickness to a stroke based on the density of the diffuse map.
    """

    def __init__(self, wsize, threshold_min, threshold_max, thicknessMin, thicknessMax):
        StrokeShader.__init__(self)
        self._func = DensityF0D(wsize)
        self.threshold_min = threshold_min
        self.threshold_max = threshold_max
        self._thicknessMin = thicknessMin
        self._thicknessMax = thicknessMax

    def shade(self, stroke):
        it = Interface0DIterator(stroke)
        delta_threshold = self.threshold_max - self.threshold_min
        delta_thickness = self._thicknessMax - self._thicknessMin

        for svert in it:
            c = self._func(it)
            c = bound(self.threshold_min, c, self.threshold_max)
            t = (self.threshold_max - c) / delta_threshold * delta_thickness + self._thicknessMin
            svert.attribute.thickness = (t / 2.0, t / 2.0)


class pyIncreasingThicknessShader(StrokeShader):
    """
    Increasingly thickens the stroke.
    """

    def __init__(self, thicknessMin, thicknessMax):
        StrokeShader.__init__(self)
        self._thicknessMin = thicknessMin
        self._thicknessMax = thicknessMax

    def shade(self, stroke):
        n = len(stroke)
        for i, svert in enumerate(stroke):
            c = i / n
            if i < (n * 0.5):
                t = (1.0 - c) * self._thicknessMin + c * self._thicknessMax
            else:
                t = (1.0 - c) * self._thicknessMax + c * self._thicknessMin
            svert.attribute.thickness = (t / 2.0, t / 2.0)


class pyConstrainedIncreasingThicknessShader(StrokeShader):
    """
    Increasingly thickens the stroke, constrained by a ratio of the
    stroke's length.
    """

    def __init__(self, thicknessMin, thicknessMax, ratio):
        StrokeShader.__init__(self)
        self._thicknessMin = thicknessMin
        self._thicknessMax = thicknessMax
        self._ratio = ratio

    def shade(self, stroke):
        n = len(stroke)
        maxT = min(self._ratio * stroke.length_2d, self._thicknessMax)

        for i, svert in enumerate(stroke):
            c = i / n
            if i < (n * 0.5):
                t = (1.0 - c) * self._thicknessMin + c * maxT
            else:
                t = (1.0 - c) * maxT + c * self._thicknessMin

            if i == (n - 1):
                svert.attribute.thickness = (self._thicknessMin / 2.0, self._thicknessMin / 2.0)
            else:
                svert.attribute.thickness = (t / 2.0, t / 2.0)


class pyDecreasingThicknessShader(StrokeShader):
    """
    Inverse of pyIncreasingThicknessShader, decreasingly thickens the stroke.
    """

    def __init__(self, thicknessMin, thicknessMax):
        StrokeShader.__init__(self)
        self._thicknessMin = thicknessMin
        self._thicknessMax = thicknessMax

    def shade(self, stroke):
        l = stroke.length_2d
        n = len(stroke)
        tMax = min(self._thicknessMax, 0.33 * l)
        tMin = min(self._thicknessMin, 0.10 * l)

        for i, svert in enumerate(stroke):
            c = i / n
            t = (1.0 - c) * tMax + c * tMin
            svert.attribute.thickness = (t / 2.0, t / 2.0)


class pyNonLinearVaryingThicknessShader(StrokeShader):
    """
    Assigns thickness to a stroke based on an exponential function.
    """

    def __init__(self, thicknessExtremity, thicknessMiddle, exponent):
        self._thicknessMin = thicknessMiddle
        self._thicknessMax = thicknessExtremity
        self._exp = exponent
        StrokeShader.__init__(self)

    def shade(self, stroke):
        n = len(stroke)
        for i, svert in enumerate(stroke):
            c = (i / n) if (i < n / 2.0) else ((n - i) / n)
            c = pow(c, self._exp) * pow(2.0, self._exp)
            t = (1.0 - c) * self._thicknessMax + c * self._thicknessMin
            svert.attribute.thickness = (t / 2.0, t / 2.0)


class pySLERPThicknessShader(StrokeShader):
    """
    Assigns thickness to a stroke based on spherical linear interpolation.
    """

    def __init__(self, thicknessMin, thicknessMax, omega=1.2):
        StrokeShader.__init__(self)
        self._thicknessMin = thicknessMin
        self._thicknessMax = thicknessMax
        self.omega = omega

    def shade(self, stroke):
        n = len(stroke)
        maxT = min(self._thicknessMax, 0.33 * stroke.length_2d)
        omega = self.omega
        sinhyp = sinh(omega)
        for i, svert in enumerate(stroke):
            c = i / n
            if i < (n * 0.5):
                t = sin((1 - c) * omega) / sinhyp * self._thicknessMin + sin(c * omega) / sinhyp * maxT
            else:
                t = sin((1 - c) * omega) / sinhyp * maxT + sin(c * omega) / sinhyp * self._thicknessMin
            svert.attribute.thickness = (t / 2.0, t / 2.0)


class pyTVertexThickenerShader(StrokeShader):
    """
    Thickens TVertices (visual intersections between two edges).
    """

    def __init__(self, a=1.5, n=3):
        StrokeShader.__init__(self)
        self._a = a
        self._n = n

    def shade(self, stroke):
        n = self._n
        a = self._a

        term = (a - 1.0) / (n - 1.0)

        if (stroke[0].nature & Nature.T_VERTEX):
            for count, svert in zip(range(n), stroke):
                r = term * (n / (count + 1.0) - 1.0) + 1.0
                (tr, tl) = svert.attribute.thickness
                svert.attribute.thickness = (r * tr, r * tl)

        if (stroke[-1].nature & Nature.T_VERTEX):
            for count, svert in zip(range(n), reversed(stroke)):
                r = term * (n / (count + 1.0) - 1.0) + 1.0
                (tr, tl) = svert.attribute.thickness
                svert.attribute.thickness = (r * tr, r * tl)


class pyImportance2DThicknessShader(StrokeShader):
    """
    Assigns thickness based on distance to a given point in 2D space.
    the thickness is inverted, so the vertices closest to the
    specified point have the lowest thickness.
    """

    def __init__(self, x, y, w, kmin, kmax):
        StrokeShader.__init__(self)
        self._origin = Vector((x, y))
        self._w = w
        self._kmin, self._kmax = kmin, kmax

    def shade(self, stroke):
        for svert in stroke:
            d = (svert.point_2d - self._origin).length
            k = (self._kmin if (d > self._w) else
                 (self._kmax * (self._w - d) + self._kmin * d) / self._w)

            (tr, tl) = svert.attribute.thickness
            svert.attribute.thickness = (k * tr / 2.0, k * tl / 2.0)


class pyImportance3DThicknessShader(StrokeShader):
    """
    Assigns thickness based on distance to a given point in 3D space.
    """

    def __init__(self, x, y, z, w, kmin, kmax):
        StrokeShader.__init__(self)
        self._origin = Vector((x, y, z))
        self._w = w
        self._kmin, self._kmax = kmin, kmax

    def shade(self, stroke):
        for svert in stroke:
            d = (svert.point_3d - self._origin).length
            k = (self._kmin if (d > self._w) else
                 (self._kmax * (self._w - d) + self._kmin * d) / self._w)

            (tr, tl) = svert.attribute.thickness
            svert.attribute.thickness = (k * tr / 2.0, k * tl / 2.0)


class pyZDependingThicknessShader(StrokeShader):
    """
    Assigns thickness based on an object's local Z depth (point
    closest to camera is 1, point furthest from camera is zero).
    """

    def __init__(self, min, max):
        StrokeShader.__init__(self)
        self.__min = min
        self.__max = max
        self.func = GetProjectedZF0D()

    def shade(self, stroke):
        it = Interface0DIterator(stroke)
        z_indices = tuple(self.func(it) for _ in it)
        z_min, z_max = min(1, *z_indices), max(0, *z_indices)
        z_diff = 1 / (z_max - z_min)

        for svert, z_index in zip(stroke, z_indices):
            z = (z_index - z_min) * z_diff
            thickness = (1 - z) * self.__max + z * self.__min
            svert.attribute.thickness = (thickness, thickness)


# -- Color & Alpha Stroke Shaders -- #


class pyConstantColorShader(StrokeShader):
    """
    Assigns a constant color to the stroke.
    """

    def __init__(self, r, g, b, a=1):
        StrokeShader.__init__(self)
        self._color = (r, g, b)
        self._a = a

    def shade(self, stroke):
        for svert in stroke:
            svert.attribute.color = self._color
            svert.attribute.alpha = self._a


class pyIncreasingColorShader(StrokeShader):
    """
    Fades from one color to another along the stroke.
    """

    def __init__(self, r1, g1, b1, a1, r2, g2, b2, a2):
        StrokeShader.__init__(self)
        # use 4d vector to simplify math
        self._c1 = Vector((r1, g1, b1, a1))
        self._c2 = Vector((r2, g2, b2, a2))

    def shade(self, stroke):
        n = len(stroke) - 1

        for i, svert in enumerate(stroke):
            c = i / n
            color = (1 - c) * self._c1 + c * self._c2
            svert.attribute.color = color[:3]
            svert.attribute.alpha = color[3]


class pyInterpolateColorShader(StrokeShader):
    """
    Fades from one color to another and back.
    """

    def __init__(self, r1, g1, b1, a1, r2, g2, b2, a2):
        StrokeShader.__init__(self)
        # use 4d vector to simplify math
        self._c1 = Vector((r1, g1, b1, a1))
        self._c2 = Vector((r2, g2, b2, a2))

    def shade(self, stroke):
        n = len(stroke) - 1
        for i, svert in enumerate(stroke):
            c = 1.0 - 2.0 * abs((i / n) - 0.5)
            color = (1.0 - c) * self._c1 + c * self._c2
            svert.attribute.color = color[:3]
            svert.attribute.alpha = color[3]


class pyModulateAlphaShader(StrokeShader):
    """
    Limits the stroke's alpha between a min and max value.
    """

    def __init__(self, min=0, max=1):
        StrokeShader.__init__(self)
        self.__min = min
        self.__max = max

    def shade(self, stroke):
        for svert in stroke:
            alpha = svert.attribute.alpha
            alpha = bound(self.__min, alpha * svert.point.y * 0.0025, self.__max)
            svert.attribute.alpha = alpha


class pyMaterialColorShader(StrokeShader):
    """
    Assigns the color of the underlying material to the stroke.
    """

    def __init__(self, threshold=50):
        StrokeShader.__init__(self)
        self._threshold = threshold
        self._func = MaterialF0D()

    def shade(self, stroke):
        xn = 0.312713
        yn = 0.329016
        Yn = 1.0
        un = 4.0 * xn / (-2.0 * xn + 12.0 * yn + 3.0)
        vn = 9.0 * yn / (-2.0 * xn + 12.0 * yn + 3.0)

        it = Interface0DIterator(stroke)
        for svert in it:
            mat = self._func(it)

            r, g, b, *_ = mat.diffuse

            X = 0.412453 * r + 0.35758 * g + 0.180423 * b
            Y = 0.212671 * r + 0.71516 * g + 0.072169 * b
            Z = 0.019334 * r + 0.11919 * g + 0.950227 * b

            if not any((X, Y, Z)):
                X = Y = Z = 0.01

            u = 4.0 * X / (X + 15.0 * Y + 3.0 * Z)
            v = 9.0 * Y / (X + 15.0 * Y + 3.0 * Z)

            L = 116. * pow((Y / Yn), (1. / 3.)) - 16
            U = 13. * L * (u - un)
            V = 13. * L * (v - vn)

            if L > self._threshold:
                L /= 1.3
                U += 10.
            else:
                L = L + 2.5 * (100 - L) * 0.2
                U /= 3.0
                V /= 3.0

            u = U / (13.0 * L) + un
            v = V / (13.0 * L) + vn

            Y = Yn * pow(((L + 16.) / 116.), 3.)
            X = -9. * Y * u / ((u - 4.) * v - u * v)
            Z = (9. * Y - 15 * v * Y - v * X) / (3. * v)

            r = 3.240479 * X - 1.53715 * Y - 0.498535 * Z
            g = -0.969256 * X + 1.875991 * Y + 0.041556 * Z
            b = 0.055648 * X - 0.204043 * Y + 1.057311 * Z

            r = max(0, r)
            g = max(0, g)
            b = max(0, b)

            svert.attribute.color = (r, g, b)


class pyRandomColorShader(StrokeShader):
    """
    Assigns a color to the stroke based on given seed.
    """

    def __init__(self, s=1):
        StrokeShader.__init__(self)
        random.seed = s

    def shade(self, stroke):
        c = (random.uniform(15, 75) * 0.01,
             random.uniform(15, 75) * 0.01,
             random.uniform(15, 75) * 0.01)
        for svert in stroke:
            svert.attribute.color = c


class py2DCurvatureColorShader(StrokeShader):
    """
    Assigns a color (grayscale) to the stroke based on the curvature.
    A higher curvature will yield a brighter color.
    """

    def shade(self, stroke):
        func = Curvature2DAngleF0D()
        it = Interface0DIterator(stroke)
        for svert in it:
            c = func(it)
            if c < 0 and bpy.app.debug_freestyle:
                print("py2DCurvatureColorShader: negative 2D curvature")
            color = 10.0 * c / pi
            svert.attribute.color = (color, color, color)


class pyTimeColorShader(StrokeShader):
    """
    Assigns a grayscale value that increases for every vertex.
    The brightness will increase along the stroke.
    """

    def __init__(self, step=0.01):
        StrokeShader.__init__(self)
        self._step = step

    def shade(self, stroke):
        for i, svert in enumerate(stroke):
            c = i * self._step
            svert.attribute.color = (c, c, c)


# -- Geometry Stroke Shaders -- #


class pySamplingShader(StrokeShader):
    """
    Resamples the stroke, which gives the stroke the amount of
    vertices specified.
    """

    def __init__(self, sampling):
        StrokeShader.__init__(self)
        self._sampling = sampling

    def shade(self, stroke):
        stroke.resample(float(self._sampling))
        stroke.update_length()


class pyBackboneStretcherShader(StrokeShader):
    """
    Stretches the stroke's backbone by a given length (in pixels).
    """

    def __init__(self, l):
        StrokeShader.__init__(self)
        self._l = l

    def shade(self, stroke):
        # get start and end points
        v0, vn = stroke[0], stroke[-1]
        p0, pn = v0.point, vn.point
        # get the direction
        d1 = (p0 - stroke[1].point).normalized()
        dn = (pn - stroke[-2].point).normalized()
        v0.point += d1 * self._l
        vn.point += dn * self._l
        stroke.update_length()


class pyLengthDependingBackboneStretcherShader(StrokeShader):
    """
    Stretches the stroke's backbone proportional to the stroke's length
    NOTE: you'll probably want an l somewhere between (0.5 - 0). A value that
    is too high may yield unexpected results.
    """

    def __init__(self, l):
        StrokeShader.__init__(self)
        self._l = l

    def shade(self, stroke):
        # get start and end points
        v0, vn = stroke[0], stroke[-1]
        p0, pn = v0.point, vn.point
        # get the direction
        d1 = (p0 - stroke[1].point).normalized()
        dn = (pn - stroke[-2].point).normalized()
        v0.point += d1 * self._l * stroke.length_2d
        vn.point += dn * self._l * stroke.length_2d
        stroke.update_length()


class pyGuidingLineShader(StrokeShader):
    def shade(self, stroke):
        # get the tangent direction
        t = stroke[-1].point - stroke[0].point
        # look for the stroke middle vertex
        itmiddle = iter(stroke)
        while itmiddle.object.u < 0.5:
            itmiddle.increment()
        center_vertex = itmiddle.object
        # position all the vertices along the tangent for the right part
        it = StrokeVertexIterator(itmiddle)
        for svert in it:
            svert.point = center_vertex.point + t * (svert.u - center_vertex.u)
        # position all the vertices along the tangent for the left part
        it = StrokeVertexIterator(itmiddle).reversed()
        for svert in it:
            svert.point = center_vertex.point - t * (center_vertex.u - svert.u)
        stroke.update_length()


class pyBackboneStretcherNoCuspShader(StrokeShader):
    """
    Stretches the stroke's backbone, excluding cusp vertices (end junctions).
    """

    def __init__(self, l):
        StrokeShader.__init__(self)
        self._l = l

    def shade(self, stroke):

        v0, v1 = stroke[0], stroke[1]
        vn, vn_1 = stroke[-1], stroke[-2]

        if not (v0.nature & v1.nature & Nature.CUSP):
            d1 = (v0.point - v1.point).normalized()
            v0.point += d1 * self._l

        if not (vn.nature & vn_1.nature & Nature.CUSP):
            dn = (vn.point - vn_1.point).normalized()
            vn.point += dn * self._l

        stroke.update_length()


class pyDiffusion2Shader(StrokeShader):
    """
    Iteratively adds an offset to the position of each stroke vertex
    in the direction perpendicular to the stroke direction at the
    point. The offset is scaled by the 2D curvature (i.e. how quickly
    the stroke curve is) at the point.
    """

    def __init__(self, lambda1, nbIter):
        StrokeShader.__init__(self)
        self._lambda = lambda1
        self._nbIter = nbIter
        self._normalInfo = Normal2DF0D()
        self._curvatureInfo = Curvature2DAngleF0D()

    def shade(self, stroke):
        for _i in range(1, self._nbIter):
            it = Interface0DIterator(stroke)
            for svert in it:
                svert.point += self._normalInfo(it) * self._lambda * self._curvatureInfo(it)
        stroke.update_length()


class pyTipRemoverShader(StrokeShader):
    """
    Removes the tips of the stroke.
    """

    def __init__(self, l):
        StrokeShader.__init__(self)
        self._l = l

    @staticmethod
    def check_vertex(v, length):
        # Returns True if the given strokevertex is less than self._l away
        # from the stroke's tip and therefore should be removed.
        return (v.curvilinear_abscissa < length or v.stroke_length - v.curvilinear_abscissa < length)

    def shade(self, stroke):
        n = len(stroke)
        if n < 4:
            return

        verticesToRemove = tuple(svert for svert in stroke if self.check_vertex(svert, self._l))
        # explicit conversion to StrokeAttribute is needed
        oldAttributes = (StrokeAttribute(svert.attribute) for svert in stroke)

        if n - len(verticesToRemove) < 2:
            return

        for sv in verticesToRemove:
            stroke.remove_vertex(sv)

        stroke.update_length()
        stroke.resample(n)
        if len(stroke) != n and bpy.app.debug_freestyle:
            print("pyTipRemover: Warning: resampling problem")

        for svert, a in zip(stroke, oldAttributes):
            svert.attribute = a
        stroke.update_length()


class pyTVertexRemoverShader(StrokeShader):
    """
    Removes t-vertices from the stroke.
    """

    def shade(self, stroke):
        if len(stroke) < 4:
            return

        v0, vn = stroke[0], stroke[-1]
        if (v0.nature & Nature.T_VERTEX):
            stroke.remove_vertex(v0)
        if (vn.nature & Nature.T_VERTEX):
            stroke.remove_vertex(vn)
        stroke.update_length()


class pyHLRShader(StrokeShader):
    """
    Controls visibility based upon the quantitative invisibility (QI)
    based on hidden line removal (HLR).
    """

    def shade(self, stroke):
        if len(stroke) < 4:
            return

        it = iter(stroke)
        for v1, v2 in zip(it, it.incremented()):
            if (v1.nature & Nature.VIEW_VERTEX):
                visible = (v1.get_fedge(v2).viewedge.qi != 0)
            v1.attribute.visible = not visible


class pySinusDisplacementShader(StrokeShader):
    """
    Displaces the stroke in the shape of a sine wave.
    """

    def __init__(self, f, a):
        StrokeShader.__init__(self)
        self._f = f
        self._a = a
        self._getNormal = Normal2DF0D()

    def shade(self, stroke):
        it = Interface0DIterator(stroke)
        for svert in it:
            normal = self._getNormal(it)
            a = self._a * (1 - 2 * (abs(svert.u - 0.5)))
            n = normal * a * cos(self._f * svert.u * 6.28)
            svert.point += n
        stroke.update_length()


class pyPerlinNoise1DShader(StrokeShader):
    """
    Displaces the stroke using the curvilinear abscissa.  This means
    that lines with the same length and sampling interval will be
    identically distorded.
    """

    def __init__(self, freq=10, amp=10, oct=4, seed=-1):
        StrokeShader.__init__(self)
        self.__noise = Noise(seed)
        self.__freq = freq
        self.__amp = amp
        self.__oct = oct

    def shade(self, stroke):
        for svert in stroke:
            s = svert.projected_x + svert.projected_y
            nres = self.__noise.turbulence1(s, self.__freq, self.__amp, self.__oct)
            svert.point = (svert.projected_x + nres, svert.projected_y + nres)
        stroke.update_length()


class pyPerlinNoise2DShader(StrokeShader):
    """
    Displaces the stroke using the strokes coordinates.  This means
    that in a scene no strokes will be distorded identically.

    More information on the noise shaders can be found at:
    freestyleintegration.wordpress.com/2011/09/25/development-updates-on-september-25/
    """

    def __init__(self, freq=10, amp=10, oct=4, seed=-1):
        StrokeShader.__init__(self)
        self.__noise = Noise(seed)
        self.__freq = freq
        self.__amp = amp
        self.__oct = oct

    def shade(self, stroke):
        for svert in stroke:
            nres = self.__noise.turbulence2(svert.point_2d, self.__freq, self.__amp, self.__oct)
            svert.point = (svert.projected_x + nres, svert.projected_y + nres)
        stroke.update_length()


class pyBluePrintCirclesShader(StrokeShader):
    """
    Draws the silhouette of the object as a circle.
    """

    def __init__(self, turns=1, random_radius=3, random_center=5):
        StrokeShader.__init__(self)
        self.__turns = turns
        self.__random_center = random_center
        self.__random_radius = random_radius

    def shade(self, stroke):
        # get minimum and maximum coordinates
        p_min, p_max = BoundingBox.from_sequence(svert.point for svert in stroke).corners

        stroke.resample(32 * self.__turns)
        sv_nb = len(stroke) // self.__turns
        center = (p_min + p_max) / 2
        radius = (center.x - p_min.x + center.y - p_min.y) / 2
        R = self.__random_radius
        C = self.__random_center

        # The directions (and phases) are calculated using a separate
        # function decorated with an lru-cache. This guarantees that
        # the directions (involving sin and cos) are calculated as few
        # times as possible.
        #
        # This works because the phases and directions are only
        # dependent on the stroke length, and the chance that
        # stroke.resample() above produces strokes of the same length
        # is quite high.
        #
        # In tests, the amount of calls to sin() and cos() went from
        # over 21000 to just 32 times, yielding a speedup of over 100%
        directions = phase_to_direction(sv_nb)

        it = iter(stroke)

        for _j in range(self.__turns):
            prev_radius = radius
            prev_center = center
            radius += randint(-R, R)
            center += Vector((randint(-C, C), randint(-C, C)))

            for (phase, direction), svert in zip(directions, it):
                r = prev_radius + (radius - prev_radius) * phase
                c = prev_center + (center - prev_center) * phase
                svert.point = c + r * direction

        if not it.is_end:
            it.increment()
            for sv in tuple(it):
                stroke.remove_vertex(sv)

        stroke.update_length()


class pyBluePrintEllipsesShader(StrokeShader):
    def __init__(self, turns=1, random_radius=3, random_center=5):
        StrokeShader.__init__(self)
        self.__turns = turns
        self.__random_center = random_center
        self.__random_radius = random_radius

    def shade(self, stroke):
        p_min, p_max = BoundingBox.from_sequence(svert.point for svert in stroke).corners

        stroke.resample(32 * self.__turns)
        sv_nb = len(stroke) // self.__turns

        center = (p_min + p_max) / 2
        radius = center - p_min

        R = self.__random_radius
        C = self.__random_center

        # for description of the line below, see pyBluePrintCirclesShader
        directions = phase_to_direction(sv_nb)
        it = iter(stroke)
        for _j in range(self.__turns):
            prev_radius = radius
            prev_center = center
            radius = radius + Vector((randint(-R, R), randint(-R, R)))
            center = center + Vector((randint(-C, C), randint(-C, C)))

            for (phase, direction), svert in zip(directions, it):
                r = prev_radius + (radius - prev_radius) * phase
                c = prev_center + (center - prev_center) * phase
                svert.point = (c.x + r.x * direction.x, c.y + r.y * direction.y)

        # remove excess vertices
        if not it.is_end:
            it.increment()
            for sv in tuple(it):
                stroke.remove_vertex(sv)

        stroke.update_length()


class pyBluePrintSquaresShader(StrokeShader):
    def __init__(self, turns=1, bb_len=10, bb_rand=0):
        StrokeShader.__init__(self)
        self.__turns = turns  # does not have any effect atm
        self.__bb_len = bb_len
        self.__bb_rand = bb_rand

    def shade(self, stroke):
        # this condition will lead to errors later, end now
        if len(stroke) < 1:
            return

        # get minimum and maximum coordinates
        p_min, p_max = BoundingBox.from_sequence(svert.point for svert in stroke).corners

        stroke.resample(32 * self.__turns)
        num_segments = len(stroke) // self.__turns
        f = num_segments // 4
        # indices of the vertices that will form corners
        first, second, third, fourth = (f, f * 2, f * 3, num_segments)

        # construct points of the backbone
        bb_len = self.__bb_len
        points = (
            Vector((p_min.x - bb_len, p_min.y)),
            Vector((p_max.x + bb_len, p_min.y)),
            Vector((p_max.x, p_min.y - bb_len)),
            Vector((p_max.x, p_max.y + bb_len)),
            Vector((p_max.x + bb_len, p_max.y)),
            Vector((p_min.x - bb_len, p_max.y)),
            Vector((p_min.x, p_max.y + bb_len)),
            Vector((p_min.x, p_min.y - bb_len)),
        )

        # add randomization to the points (if needed)
        if self.__bb_rand:
            R, r = self.__bb_rand, self.__bb_rand // 2

            randomization_mat = (
                Vector((randint(-R, R), randint(-r, r))),
                Vector((randint(-R, R), randint(-r, r))),
                Vector((randint(-r, r), randint(-R, R))),
                Vector((randint(-r, r), randint(-R, R))),
                Vector((randint(-R, R), randint(-r, r))),
                Vector((randint(-R, R), randint(-r, r))),
                Vector((randint(-r, r), randint(-R, R))),
                Vector((randint(-r, r), randint(-R, R))),
            )

            # combine both tuples
            points = tuple(p + rand for (p, rand) in zip(points, randomization_mat))

        # subtract even from uneven; result is length four tuple of vectors
        it = iter(points)
        old_vecs = tuple(next(it) - current for current in it)

        it = iter(stroke)
        verticesToRemove = list()
        for _j in range(self.__turns):
            for i, svert in zip(range(num_segments), it):
                if i < first:
                    svert.point = points[0] + old_vecs[0] * i / (first - 1)
                    svert.attribute.visible = (i != first - 1)
                elif i < second:
                    svert.point = points[2] + old_vecs[1] * (i - first) / (second - first - 1)
                    svert.attribute.visible = (i != second - 1)
                elif i < third:
                    svert.point = points[4] + old_vecs[2] * (i - second) / (third - second - 1)
                    svert.attribute.visible = (i != third - 1)
                elif i < fourth:
                    svert.point = points[6] + old_vecs[3] * (i - third) / (fourth - third - 1)
                    svert.attribute.visible = (i != fourth - 1)
                else:
                    # special case; remove these vertices
                    verticesToRemove.append(svert)

        # remove excess vertices (if any)
        if not it.is_end:
            it.increment()
            verticesToRemove += [svert for svert in it]
            for sv in verticesToRemove:
                stroke.remove_vertex(sv)
        stroke.update_length()


class pyBluePrintDirectedSquaresShader(StrokeShader):
    """
    Replaces the stroke with a directed square.
    """

    def __init__(self, turns=1, bb_len=10, mult=1):
        StrokeShader.__init__(self)
        self.__mult = mult
        self.__turns = turns
        self.__bb_len = 1 + bb_len * 0.01

    def shade(self, stroke):
        stroke.resample(32 * self.__turns)
        n = len(stroke)

        p_mean = (1 / n) * sum((svert.point for svert in stroke), Vector((0.0, 0.0)))
        p_var = Vector((0, 0))
        p_var_xy = 0.0
        for d in (svert.point - p_mean for svert in stroke):
            p_var += Vector((d.x ** 2, d.y ** 2))
            p_var_xy += d.x * d.y

        # divide by number of vertices
        p_var /= n
        p_var_xy /= n
        trace = p_var.x + p_var.y
        det = p_var.x * p_var.y - pow(p_var_xy, 2)

        sqrt_coeff = sqrt(trace * trace - 4 * det)
        lambda1, lambda2 = (trace + sqrt_coeff) / 2, (trace - sqrt_coeff) / 2
        # make sure those numbers aren't to small, if they are, rooting them will yield complex numbers
        lambda1, lambda2 = max(1e-12, lambda1), max(1e-12, lambda2)
        theta = atan(2 * p_var_xy / (p_var.x - p_var.y)) / 2

        # Keep alignment for readability.
        # autopep8: off
        if p_var.y > p_var.x:
            e1 = Vector((cos(theta + pi / 2), sin(theta + pi / 2))) * sqrt(lambda1) * self.__mult
            e2 = Vector((cos(theta + pi    ), sin(theta + pi    ))) * sqrt(lambda2) * self.__mult
        else:
            e1 = Vector((cos(theta), sin(theta)))                   * sqrt(lambda1) * self.__mult
            e2 = Vector((cos(theta + pi / 2), sin(theta + pi / 2))) * sqrt(lambda2) * self.__mult
        # autopep8: on

        # partition the stroke
        num_segments = len(stroke) // self.__turns
        f = num_segments // 4
        # indices of the vertices that will form corners
        first, second, third, fourth = (f, f * 2, f * 3, num_segments)

        bb_len1 = self.__bb_len
        bb_len2 = 1 + (bb_len1 - 1) * sqrt(lambda1 / lambda2)
        points = (
            p_mean - e1 - e2 * bb_len2,
            p_mean - e1 * bb_len1 + e2,
            p_mean + e1 + e2 * bb_len2,
            p_mean + e1 * bb_len1 - e2,
        )

        old_vecs = (
            e2 * bb_len2 * 2,
            e1 * bb_len1 * 2,
            -e2 * bb_len2 * 2,
            -e1 * bb_len1 * 2,
        )

        it = iter(stroke)
        verticesToRemove = list()
        for _j in range(self.__turns):
            for i, svert in zip(range(num_segments), it):
                if i < first:
                    svert.point = points[0] + old_vecs[0] * i / (first - 1)
                    svert.attribute.visible = (i != first - 1)
                elif i < second:
                    svert.point = points[1] + old_vecs[1] * (i - first) / (second - first - 1)
                    svert.attribute.visible = (i != second - 1)
                elif i < third:
                    svert.point = points[2] + old_vecs[2] * (i - second) / (third - second - 1)
                    svert.attribute.visible = (i != third - 1)
                elif i < fourth:
                    svert.point = points[3] + old_vecs[3] * (i - third) / (fourth - third - 1)
                    svert.attribute.visible = (i != fourth - 1)
                else:
                    # special case; remove these vertices
                    verticesToRemove.append(svert)

        # remove excess vertices
        if not it.is_end:
            it.increment()
            verticesToRemove += [svert for svert in it]
            for sv in verticesToRemove:
                stroke.remove_vertex(sv)
        stroke.update_length()


# -- various (used in the parameter editor) -- #


def iter_stroke_vertices(stroke, epsilon=1e-6):
    yield stroke[0]
    for prev, svert in pairwise(stroke):
        if (prev.point - svert.point).length > epsilon:
            yield svert


class RoundCapShader(StrokeShader):
    def round_cap_thickness(self, x):
        x = max(0.0, min(x, 1.0))
        return pow(1.0 - (x ** 2.0), 0.5)

    def shade(self, stroke):
        # save the location and attribute of stroke vertices
        buffer = tuple((Vector(sv.point), StrokeAttribute(sv.attribute))
                       for sv in iter_stroke_vertices(stroke))
        nverts = len(buffer)
        if nverts < 2:
            return
        # calculate the number of additional vertices to form caps
        thickness_beg = sum(stroke[0].attribute.thickness)
        nverts_beg = max(5, int(thickness_beg))

        thickness_end = sum(stroke[-1].attribute.thickness)
        nverts_end = max(5, int(thickness_end))

        # adjust the total number of stroke vertices
        stroke.resample(nverts + nverts_beg + nverts_end)
        # restore the location and attribute of the original vertices
        for i, (p, attr) in enumerate(buffer):
            stroke[nverts_beg + i].point = p
            stroke[nverts_beg + i].attribute = attr
        # reshape the cap at the beginning of the stroke
        q, attr = buffer[1]
        p, attr = buffer[0]
        direction = (p - q).normalized() * thickness_beg
        n = 1.0 / nverts_beg
        R, L = attr.thickness
        for t, svert in zip(range(nverts_beg, 0, -1), stroke):
            r = self.round_cap_thickness((t + 1) * n)
            svert.point = p + direction * t * n
            svert.attribute = attr
            svert.attribute.thickness = (R * r, L * r)
        # reshape the cap at the end of the stroke
        q, attr = buffer[-2]
        p, attr = buffer[-1]
        direction = (p - q).normalized() * thickness_end
        n = 1.0 / nverts_end
        R, L = attr.thickness
        for t, svert in zip(range(nverts_end, 0, -1), reversed(stroke)):
            r = self.round_cap_thickness((t + 1) * n)
            svert.point = p + direction * t * n
            svert.attribute = attr
            svert.attribute.thickness = (R * r, L * r)
        # update the curvilinear 2D length of each vertex
        stroke.update_length()


class SquareCapShader(StrokeShader):
    def shade(self, stroke):
        # save the location and attribute of stroke vertices
        buffer = tuple((Vector(sv.point), StrokeAttribute(sv.attribute))
                       for sv in iter_stroke_vertices(stroke))
        nverts = len(buffer)
        if nverts < 2:
            return
        # calculate the number of additional vertices to form caps
        caplen_beg = sum(stroke[0].attribute.thickness) / 2.0
        nverts_beg = 1

        caplen_end = sum(stroke[-1].attribute.thickness) / 2.0
        nverts_end = 1
        # adjust the total number of stroke vertices
        stroke.resample(nverts + nverts_beg + nverts_end)
        # restore the location and attribute of the original vertices
        for i, (p, attr) in zip(range(nverts), buffer):
            stroke[nverts_beg + i].point = p
            stroke[nverts_beg + i].attribute = attr
        # reshape the cap at the beginning of the stroke
        q, attr = buffer[1]
        p, attr = buffer[0]
        stroke[0].point += (p - q).normalized() * caplen_beg
        stroke[0].attribute = attr
        # reshape the cap at the end of the stroke
        q, attr = buffer[-2]
        p, attr = buffer[-1]
        stroke[-1].point += (p - q).normalized() * caplen_end
        stroke[-1].attribute = attr
        # update the curvilinear 2D length of each vertex
        stroke.update_length()
