File indexing completed on 2025-01-19 03:59:52

0001 import math
0002 from functools import reduce
0003 from .base import LottieObject, LottieProp, PseudoList, PseudoBool
0004 from . import easing
0005 from ..nvector import NVector
0006 from .bezier import Bezier
0007 from ..utils.color import Color
0008 
0009 
0010 class KeyframeBezier:
0011     NEWTON_ITERATIONS = 4
0012     NEWTON_MIN_SLOPE = 0.001
0013     SUBDIVISION_PRECISION = 0.0000001
0014     SUBDIVISION_MAX_ITERATIONS = 10
0015     SPLINE_TABLE_SIZE = 11
0016     SAMPLE_STEP_SIZE = 1.0 / (SPLINE_TABLE_SIZE - 1.0)
0017 
0018     def __init__(self, h1, h2):
0019         self.h1 = h1
0020         self.h2 = h2
0021         self._sample_values = None
0022 
0023     @classmethod
0024     def from_keyframe(cls, keyframe):
0025         return cls(keyframe.out_value, keyframe.in_value)
0026 
0027     def bezier(self):
0028         bez = Bezier()
0029         bez.add_point(NVector(0, 0), outp=NVector(self.h1.x, self.h1.y))
0030         bez.add_point(NVector(1, 1), inp=NVector(self.h2.x-1, self.h2.y-1))
0031         return bez
0032 
0033     def _a(self, c1, c2):
0034         return 1 - 3 * c2 + 3 * c1
0035 
0036     def _b(self, c1, c2):
0037         return 3 * c2 - 6 * c1
0038 
0039     def _c(self, c1):
0040         return 3 * c1
0041 
0042     def _bezier_component(self, t, c1, c2):
0043         return ((self._a(c1, c2) * t + self._b(c1, c2)) * t + self._c(c1)) * t
0044 
0045     def point_at(self, t):
0046         return NVector(
0047             self._bezier_component(t, self.h1.x, self.h2.x),
0048             self._bezier_component(t, self.h1.y, self.h2.y)
0049         )
0050 
0051     def _slope_component(self, t, c1, c2):
0052         return 3 * self._a(c1, c2) * t * t + 2 * self._b(c1, c2) * t + self._c(c1)
0053 
0054     def slope_at(self, t):
0055         return NVector(
0056             self._slope_component(t, self.h1.x, self.h2.x),
0057             self._slope_component(t, self.h1.y, self.h2.y)
0058         )
0059 
0060     def _binary_subdivide(self, x, interval_start, interval_end):
0061         current_x = None
0062         t = None
0063         i = 0
0064         for i in range(self.SUBDIVISION_MAX_ITERATIONS):
0065             if current_x is not None and abs(current_x) < self.SUBDIVISION_PRECISION:
0066                 break
0067             t = interval_start + (interval_end - interval_start) / 2.0
0068             current_x = self._bezier_component(t, self.h1.x, self.h2.x) - x
0069             if current_x > 0.0:
0070                 interval_end = t
0071             else:
0072                 interval_start = t
0073         return t
0074 
0075     def _newton_raphson(self, x, t_guess):
0076         for i in range(self.NEWTON_ITERATIONS):
0077             slope = self._slope_component(t_guess, self.h1.x, self.h2.x)
0078             if slope == 0:
0079                 return t_guess
0080             current_x = self._bezier_component(t_guess, self.h1.x, self.h2.x) - x
0081             t_guess -= current_x / slope
0082         return t_guess
0083 
0084     def _get_sample_values(self):
0085         if self._sample_values is None:
0086             self._sample_values = [
0087                 self._bezier_component(i * self.SAMPLE_STEP_SIZE, self.h1.x, self.h2.x)
0088                 for i in range(self.SPLINE_TABLE_SIZE)
0089             ]
0090         return self._sample_values
0091 
0092     def t_for_x(self, x):
0093         sample_values = self._get_sample_values()
0094         interval_start = 0
0095         current_sample = 1
0096         last_sample = self.SPLINE_TABLE_SIZE - 1
0097         while current_sample != last_sample and sample_values[current_sample] <= x:
0098             interval_start += self.SAMPLE_STEP_SIZE
0099             current_sample += 1
0100         current_sample -= 1
0101 
0102         dist = (x - sample_values[current_sample]) / (sample_values[current_sample+1] - sample_values[current_sample])
0103         t_guess = interval_start + dist * self.SAMPLE_STEP_SIZE
0104         initial_slope = self._slope_component(t_guess, self.h1.x, self.h2.x)
0105         if initial_slope >= self.NEWTON_MIN_SLOPE:
0106             return self._newton_raphson(x, t_guess)
0107         if initial_slope == 0:
0108             return t_guess
0109         return self._binary_subdivide(x, interval_start, interval_start + self.SAMPLE_STEP_SIZE)
0110 
0111     def y_at_x(self, x):
0112         t = self.t_for_x(x)
0113         return self._bezier_component(t, self.h1.y, self.h2.y)
0114 
0115 
0116 ## @ingroup Lottie
0117 class Keyframe(LottieObject):
0118     _props = [
0119         LottieProp("time", "t", float, False),
0120         LottieProp("in_value", "i", easing.KeyframeBezierHandle, False),
0121         LottieProp("out_value", "o", easing.KeyframeBezierHandle, False),
0122         LottieProp("jump", "h", PseudoBool),
0123     ]
0124 
0125     def __init__(self, time=0, easing_function=None):
0126         """!
0127         @param time             Start time of keyframe segment
0128         @param easing_function  Callable that performs the easing
0129         """
0130         ## Start time of keyframe segment.
0131         self.time = time
0132         ## Bezier curve easing in value.
0133         self.in_value = None
0134         ## Bezier curve easing out value.
0135         self.out_value = None
0136         ## Jump to the end value
0137         self.jump = None
0138 
0139         if easing_function:
0140             easing_function(self)
0141 
0142     def bezier(self):
0143         if self.jump:
0144             bez = Bezier()
0145             bez.add_point(NVector(0, 0))
0146             bez.add_point(NVector(1, 0))
0147             bez.add_point(NVector(1, 1))
0148             return bez
0149         else:
0150             return KeyframeBezier.from_keyframe(self).bezier()
0151 
0152     def lerp_factor(self, ratio):
0153         return KeyframeBezier.from_keyframe(self).y_at_x(ratio)
0154 
0155     def __str__(self):
0156         return "%s %s" % (self.time, self.start)
0157 
0158 
0159 ## @ingroup Lottie
0160 class OffsetKeyframe(Keyframe):
0161     """!
0162     Keyframe for MultiDimensional values
0163 
0164     @par Bezier easing
0165     @parblock
0166     Imagine a quadratic bezier, with starting point at (0, 0) and end point at (1, 1).
0167 
0168     @p out_value and @p in_value are the other two handles for a quadratic bezier,
0169     expressed as absoulte values in this 0-1 space.
0170 
0171     See also https://cubic-bezier.com/
0172     @endparblock
0173     """
0174     _props = [
0175         LottieProp("start", "s", NVector, False),
0176         LottieProp("end", "e", NVector, False),
0177         LottieProp("in_tan", "ti", NVector, False),
0178         LottieProp("out_tan", "to", NVector, False),
0179     ]
0180 
0181     def __init__(self, time=0, start=None, end=None, easing_function=None, in_tan=None, out_tan=None):
0182         Keyframe.__init__(self, time, easing_function)
0183         ## Start value of keyframe segment.
0184         self.start = start
0185         ## End value of keyframe segment.
0186         self.end = end
0187         ## In Spatial Tangent. Only for spatial properties. (for bezier smoothing on position)
0188         self.in_tan = in_tan
0189         ## Out Spatial Tangent. Only for spatial properties. (for bezier smoothing on position)
0190         self.out_tan = out_tan
0191 
0192     def interpolated_value(self, ratio, next_start=None):
0193         end = next_start if self.end is None else self.end
0194         if end is None:
0195             return self.start
0196         if not self.in_value or not self.out_value:
0197             return self.start
0198         if ratio == 1:
0199             return end
0200         if ratio == 0:
0201             return self.start
0202         if self.in_tan and self.out_tan:
0203             bezier = Bezier()
0204             bezier.add_point(self.start, NVector(0, 0), self.out_tan)
0205             bezier.add_point(end, self.in_tan, NVector(0, 0))
0206             return bezier.point_at(ratio)
0207 
0208         lerpv = self.lerp_factor(ratio)
0209         return self.start.lerp(end, lerpv)
0210 
0211     def interpolated_tangent_angle(self, ratio, next_start=None):
0212         end = next_start if self.end is None else self.end
0213         if end is None or not self.in_tan or not self.out_tan:
0214             return 0
0215 
0216         bezier = Bezier()
0217         bezier.add_point(self.start, NVector(0, 0), self.out_tan)
0218         bezier.add_point(end, self.in_tan, NVector(0, 0))
0219         return bezier.tangent_angle_at(ratio)
0220 
0221     def __repr__(self):
0222         return "<%s.%s %s %s%s>" % (
0223             type(self).__module__,
0224             type(self).__name__,
0225             self.time,
0226             self.start,
0227             (" -> %s" % self.end) if self.end is not None else ""
0228         )
0229 
0230 
0231 class AnimatableMixin:
0232     keyframe_type = Keyframe
0233 
0234     def __init__(self, value=None):
0235         ## Non-animated value
0236         self.value = value
0237         ## Property index
0238         self.property_index = None
0239         ## Whether it's animated
0240         self.animated = False
0241         ## Keyframe list
0242         self.keyframes = None
0243 
0244     def clear_animation(self, value):
0245         """!
0246         Sets a fixed value, removing animated keyframes
0247         """
0248         self.value = value
0249         self.animated = False
0250         self.keyframes = None
0251 
0252     def add_keyframe(self, time, value, interp=easing.Linear(), *args, **kwargs):
0253         """!
0254         @param time     The time this keyframe appears in
0255         @param value    The value the property should have at @p time
0256         @param interp   The easing callable used to update the tangents of the previous keyframe
0257         @param args     Extra arguments to pass the keyframe constructor
0258         @param kwargs   Extra arguments to pass the keyframe constructor
0259         @note Always call add_keyframe with increasing @p time value
0260         """
0261         if not self.animated:
0262             self.value = None
0263             self.keyframes = []
0264             self.animated = True
0265         else:
0266             if self.keyframes[-1].time == time:
0267                 if value != self.keyframes[-1].start:
0268                     self.keyframes[-1].start = value
0269                 return
0270             else:
0271                 self.keyframes[-1].end = value.clone()
0272 
0273         self.keyframes.append(self.keyframe_type(
0274             time,
0275             value,
0276             None,
0277             interp,
0278             *args,
0279             **kwargs
0280         ))
0281 
0282     def get_value(self, time=0):
0283         """!
0284         @brief Returns the value of the property at the given frame/time
0285         """
0286         if not self.animated:
0287             return self.value
0288 
0289         if not self.keyframes:
0290             return None
0291 
0292         return self._get_value_helper(time)[0]
0293 
0294     def _get_value_helper(self, time):
0295         val = self.keyframes[0].start
0296         for i in range(len(self.keyframes)):
0297             k = self.keyframes[i]
0298             if time - k.time <= 0:
0299                 if k.start is not None:
0300                     val = k.start
0301 
0302                 kp = self.keyframes[i-1] if i > 0 else None
0303                 if kp:
0304                     t = (time - kp.time) / (k.time - kp.time)
0305                     end = kp.end
0306                     if end is None:
0307                         end = val
0308                     if end is not None:
0309                         val = kp.interpolated_value(t, end)
0310                     return val, end, kp, t
0311                 return val, None, None, None
0312             if k.end is not None:
0313                 val = k.end
0314         return val, None, None, None
0315 
0316     def to_dict(self):
0317         d = super().to_dict()
0318         if self.animated:
0319             last = d["k"][-1]
0320             last.pop("i", None)
0321             last.pop("o", None)
0322         return d
0323 
0324     def __repr__(self):
0325         if self.keyframes and len(self.keyframes) > 1:
0326             val = "%s -> %s" % (self.keyframes[0].start, self.keyframes[-2].end)
0327         else:
0328             val = self.value
0329         return "<%s.%s %s>" % (type(self).__module__, type(self).__name__, val)
0330 
0331     def __str__(self):
0332         if self.animated:
0333             return "animated"
0334         return str(self.value)
0335 
0336     @classmethod
0337     def merge_keyframes(cls, items, conversion):
0338         """
0339         @todo Remove similar functionality from SVG/sif parsers
0340         """
0341         keyframes = []
0342         for animatable in items:
0343             if animatable.animated:
0344                 keyframes.extend(animatable.keyframes)
0345 
0346         # TODO properly interpolate tangents
0347         new_kframes = []
0348         for keyframe in sorted(keyframes, key=lambda kf: kf.time):
0349             if new_kframes and new_kframes[-1].time == keyframe.time:
0350                 continue
0351             kfcopy = keyframe.clone()
0352             kfcopy.start = conversion(*(i.get_value(keyframe.time) for i in items))
0353             new_kframes.append(kfcopy)
0354 
0355         for i in range(0, len(new_kframes) - 1):
0356             new_kframes[i].end = new_kframes[i+1].start
0357 
0358         return new_kframes
0359 
0360     @classmethod
0361     def load(cls, lottiedict):
0362         obj = super().load(lottiedict)
0363         if "a" not in lottiedict:
0364             obj.animated = prop_animated(lottiedict)
0365         return obj
0366 
0367 
0368 def prop_animated(l):
0369     if "a" in l:
0370         return l["a"]
0371     if "k" not in l:
0372         return False
0373     if isinstance(l["k"], list) and l["k"] and isinstance(l["k"][0], dict):
0374         return True
0375     return False
0376 
0377 
0378 def prop_not_animated(l):
0379     return not prop_animated(l)
0380 
0381 
0382 ## @ingroup Lottie
0383 class MultiDimensional(AnimatableMixin, LottieObject):
0384     """!
0385     An animatable property that holds a NVector
0386     """
0387     keyframe_type = OffsetKeyframe
0388     _props = [
0389         LottieProp("value", "k", NVector, False, prop_not_animated),
0390         LottieProp("property_index", "ix", int, False),
0391         LottieProp("animated", "a", PseudoBool, False),
0392         LottieProp("keyframes", "k", OffsetKeyframe, True, prop_animated),
0393     ]
0394 
0395     def get_tangent_angle(self, time=0):
0396         """!
0397         @brief Returns the value tangent angle of the property at the given frame/time
0398         """
0399         if not self.keyframes or len(self.keyframes) < 2:
0400             return 0
0401 
0402         val, end, kp, t = self._get_value_helper(time)
0403         if kp:
0404             return kp.interpolated_tangent_angle(t, end)
0405 
0406         if self.keyframes[0].time >= time:
0407             end = self.keyframes[0].end if self.keyframes[0].end is not None else self.keyframes[1].start
0408             return self.keyframes[0].interpolated_tangent_angle(0, end)
0409 
0410         return 0
0411 
0412 
0413 class PositionValue(MultiDimensional):
0414     _props = [
0415         LottieProp("value", "k", NVector, False, prop_not_animated),
0416         LottieProp("property_index", "ix", int, False),
0417         LottieProp("animated", "a", PseudoBool, False),
0418         LottieProp("keyframes", "k", OffsetKeyframe, True, prop_animated),
0419     ]
0420 
0421     @classmethod
0422     def load(cls, lottiedict):
0423         obj = super().load(lottiedict)
0424         if lottiedict.get("s", False):
0425             cls._load_split(lottiedict, obj)
0426 
0427         return obj
0428 
0429     @classmethod
0430     def _load_split(cls, lottiedict, obj):
0431         components = [
0432             Value.load(lottiedict.get("x", {})),
0433             Value.load(lottiedict.get("y", {})),
0434         ]
0435         if "z" in lottiedict:
0436             components.append(Value.load(lottiedict.get("z", {})))
0437 
0438         has_anim = any(x for x in components if x.animated)
0439         if not has_anim:
0440             obj.value = NVector(*(a.value for a in components))
0441             obj.animated = False
0442             obj.keyframes = None
0443             return
0444 
0445         obj.animated = True
0446         obj.value = None
0447         obj.keyframes = cls.merge_keyframes(components, NVector)
0448 
0449 
0450 class ColorValue(AnimatableMixin, LottieObject):
0451     """!
0452     An animatable property that holds a Color
0453     """
0454     keyframe_type = OffsetKeyframe
0455     _props = [
0456         LottieProp("value", "k", Color, False, prop_not_animated),
0457         LottieProp("property_index", "ix", int, False),
0458         LottieProp("animated", "a", PseudoBool, False),
0459         LottieProp("keyframes", "k", OffsetKeyframe, True, prop_animated),
0460     ]
0461 
0462 
0463 ## @ingroup Lottie
0464 class GradientColors(LottieObject):
0465     """!
0466     Represents colors and offsets in a gradient
0467 
0468     Colors are represented as a flat list interleaving offsets and color components in weird ways
0469     There are two possible layouts:
0470 
0471     Without alpha, the colors are a sequence of offset, r, g, b
0472 
0473     With alpha, same as above but at the end of the list there is a sequence of offset, alpha
0474 
0475     Examples:
0476 
0477     For the gradient [0, red], [0.5, yellow], [1, green]
0478     The list would be [0, 1, 0, 0, 0.5, 1, 1, 0, 1, 0, 1, 0]
0479 
0480     For the gradient [0, red at 80% opacity], [0.5, yellow at 70% opacity], [1, green at 60% opacity]
0481     The list would be [0, 1, 0, 0, 0.5, 1, 1, 0, 1, 0, 1, 0, 0, 0.8, 0.5, 0.7, 1, 0.6]
0482     """
0483     _props = [
0484         LottieProp("colors", "k", MultiDimensional),
0485         LottieProp("count", "p", int),
0486     ]
0487 
0488     def __init__(self, stops=[]):
0489         ## Animatable colors, as a vector containing [offset, r, g, b] values as a flat array
0490         self.colors = MultiDimensional(NVector())
0491         ## Number of colors
0492         self.count = 0
0493         if stops:
0494             self.set_stops(stops)
0495 
0496     @staticmethod
0497     def color_to_stops(self, colors):
0498         """
0499         Converts a list of colors (Color) to tuples (offset, color)
0500         """
0501         return [
0502             (i / (len(colors)-1), color)
0503             for i, color in enumerate(colors)
0504         ]
0505 
0506     def set_stops(self, stops, keyframe=None):
0507         """!
0508         @param stops iterable of (offset, Color) tuples
0509         @param keyframe keyframe index (or None if not animated)
0510         """
0511         flat = self._flatten_stops(stops)
0512         if self.colors.animated and keyframe is not None:
0513             if keyframe > 1:
0514                 self.colors.keyframes[keyframe-1].end = flat
0515             self.colors.keyframes[keyframe].start = flat
0516         else:
0517             self.colors.clear_animation(flat)
0518         self.count = len(stops)
0519 
0520     def _flatten_stops(self, stops):
0521         flattened_colors = NVector(*reduce(
0522             lambda a, b: a + b,
0523             (
0524                 [off] + color.components[:3]
0525                 for off, color in stops
0526             )
0527         ))
0528 
0529         if any(len(c) > 3 for o, c in stops):
0530             flattened_colors.components += reduce(
0531                 lambda a, b: a + b,
0532                 (
0533                     [off] + [self._get_alpha(color)]
0534                     for off, color in stops
0535                 )
0536             )
0537         return flattened_colors
0538 
0539     def _get_alpha(self, color):
0540         if len(color) > 3:
0541             return color[3]
0542         return 1
0543 
0544     def _add_to_flattened(self, offset, color, flattened):
0545         flat = [offset] + list(color[:3])
0546         rgb_size = 4 * self.count
0547 
0548         if len(flattened) == rgb_size:
0549             # No alpha
0550             flattened.extend(flat)
0551             if self.count == 0 and len(color) > 3:
0552                 flattened.append(offset)
0553                 flattened.append(color[3])
0554         else:
0555             flattened[rgb_size:rgb_size] = flat
0556             flattened.append(offset)
0557             flattened.append(self._get_alpha(color))
0558 
0559     def add_color(self, offset, color, keyframe=None):
0560         if self.colors.animated:
0561             if keyframe is None:
0562                 for kf in self.colors.keyframes:
0563                     if kf.start:
0564                         self._add_to_flattened(offset, color, kf.start.components)
0565                     if kf.end:
0566                         self._add_to_flattened(offset, color, kf.end.components)
0567             else:
0568                 if keyframe > 1:
0569                     self._add_to_flattened(offset, color, self.colors.keyframes[keyframe-1].end.components)
0570                 self._add_to_flattened(offset, color, self.colors.keyframes[keyframe].start.components)
0571         else:
0572             self._add_to_flattened(offset, color, self.colors.value.components)
0573         self.count += 1
0574 
0575     def add_keyframe(self, time, stops, ease=easing.Linear()):
0576         """!
0577         @param time   Frame time
0578         @param stops  Iterable of (offset, Color) tuples
0579         @param ease   Easing function
0580         """
0581         self.colors.add_keyframe(time, self._flatten_stops(stops), ease)
0582 
0583     def get_stops(self, keyframe=None):
0584         if keyframe is not None:
0585             colors = self.colors.keyframes[keyframe].start
0586         else:
0587             colors = self.colors.value
0588         return self._stops_from_flat(colors)
0589 
0590     def _stops_from_flat(self, colors):
0591         if len(colors) == 4 * self.count:
0592             for i in range(self.count):
0593                 off = i * 4
0594                 yield colors[off], Color(*colors[off+1:off+4])
0595         else:
0596             for i in range(self.count):
0597                 off = i * 4
0598                 aoff = self.count * 4 + i * 2 + 1
0599                 yield colors[off], Color(colors[off+1], colors[off+2], colors[off+3], colors[aoff])
0600 
0601     def stops_at(self, time):
0602         return self._stops_from_flat(self.colors.get_value(time))
0603 
0604 
0605 ## @ingroup Lottie
0606 class Value(AnimatableMixin, LottieObject):
0607     """!
0608     An animatable property that holds a float
0609     """
0610     keyframe_type = OffsetKeyframe
0611     _props = [
0612         LottieProp("value", "k", float, False, prop_not_animated),
0613         LottieProp("property_index", "ix", int, False),
0614         LottieProp("animated", "a", PseudoBool, False),
0615         LottieProp("keyframes", "k", keyframe_type, True, prop_animated),
0616     ]
0617 
0618     def __init__(self, value=0):
0619         super().__init__(value)
0620 
0621     def add_keyframe(self, time, value, ease=easing.Linear()):
0622         super().add_keyframe(time, NVector(value), ease)
0623 
0624     def get_value(self, time=0):
0625         v = super().get_value(time)
0626         if self.animated and self.keyframes:
0627             return v[0]
0628         return v
0629 
0630 
0631 ## @ingroup Lottie
0632 class ShapePropKeyframe(Keyframe):
0633     """!
0634     Keyframe holding Bezier objects
0635     """
0636     _props = [
0637         LottieProp("start", "s", Bezier, PseudoList),
0638         LottieProp("end", "e", Bezier, PseudoList),
0639     ]
0640 
0641     def __init__(self, time=0, start=None, end=None, easing_function=None):
0642         Keyframe.__init__(self, time, easing_function)
0643         ## Start value of keyframe segment.
0644         self.start = start
0645         ## End value of keyframe segment.
0646         self.end = end
0647 
0648     def interpolated_value(self, ratio, next_start=None):
0649         end = next_start if self.end is None else self.end
0650         if end is None:
0651             return self.start
0652         if not self.in_value or not self.out_value:
0653             return self.start
0654         if ratio == 1:
0655             return end
0656         if ratio == 0 or len(self.start.vertices) != len(end.vertices):
0657             return self.start
0658 
0659         lerpv = self.lerp_factor(ratio)
0660         bez = Bezier()
0661         bez.closed = self.start.closed
0662         for i in range(len(self.start.vertices)):
0663             bez.vertices.append(self.start.vertices[i].lerp(end.vertices[i], lerpv))
0664             bez.in_tangents.append(self.start.in_tangents[i].lerp(end.in_tangents[i], lerpv))
0665             bez.out_tangents.append(self.start.out_tangents[i].lerp(end.out_tangents[i], lerpv))
0666         return bez
0667 
0668 
0669 ## @ingroup Lottie
0670 class ShapeProperty(AnimatableMixin, LottieObject):
0671     """!
0672     An animatable property that holds a Bezier
0673     """
0674     keyframe_type = ShapePropKeyframe
0675     _props = [
0676         LottieProp("value", "k", Bezier, False, prop_not_animated),
0677         #LottieProp("expression", "x", str, False),
0678         LottieProp("property_index", "ix", float, False),
0679         LottieProp("animated", "a", PseudoBool, False),
0680         LottieProp("keyframes", "k", keyframe_type, True, prop_animated),
0681     ]
0682 
0683     def __init__(self, bezier=None):
0684         super().__init__(bezier or Bezier())