Initial Commit

This commit is contained in:
OmniLottie
2026-03-01 21:36:54 +08:00
commit a386c803e1
199 changed files with 42253 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
"""!
Package with all the Lottie Python bindings
"""
from . import (
animation, base, effects, enums, helpers, layers, shapes, assets, easing,
text, bezier, composition
)
from .animation import Animation
from .layers import *
from .shapes import *
from .assets import Precomp
from .bezier import Bezier
from .composition import Composition
__all__ = [
"animation", "base", "effects", "enums", "helpers", "layers", "shapes", "assets",
"easing", "text", "bezier",
"Animation",
"NullLayer", "TextLayer", "ShapeLayer", "ImageLayer", "PreCompLayer", "SolidColorLayer",
"Rect", "Fill", "Trim", "Repeater", "GradientFill", "Stroke", "RoundedCorners", "Path",
"TransformShape", "Group", "Star", "Ellipse", "Merge", "GradientStroke",
"Bezier", "Precomp", "Composition",
]
+123
View File
@@ -0,0 +1,123 @@
from .base import LottieObject, LottieProp, PseudoBool, Index
from .layers import Layer
from .assets import Asset, Chars, Precomp
from .text import FontList
from .composition import Composition
##\defgroup Lottie Lottie
#
# Objects of the lottie file structure.
## \defgroup LottieCheck Lottie (to check)
#
# Lottie objects that have not been tested
## @ingroup Lottie
class Animation(Composition):
"""!
Top level object, describing the animation
@see http://docs.aenhancers.com/items/compitem/
"""
_props = [
LottieProp("version", "v", str, False),
LottieProp("frame_rate", "fr", float, False),
LottieProp("in_point", "ip", float, False),
LottieProp("out_point", "op", float, False),
LottieProp("width", "w", int, False),
LottieProp("height", "h", int, False),
LottieProp("name", "nm", str, False),
LottieProp("threedimensional", "ddd", PseudoBool, False),
LottieProp("assets", "assets", Asset, True),
#LottieProp("comps", "comps", Animation, True),
LottieProp("fonts", "fonts", FontList),
LottieProp("chars", "chars", Chars, True),
#LottieProp("markers", "markers", Marker, True),
#LottieProp("motion_blur", "mb", MotionBlur, False),
]
_version = "5.5.2"
def __init__(self, n_frames=60, framerate=60):
super().__init__()
## The time when the composition work area begins, in frames.
self.in_point = 0
## The time when the composition work area ends.
## Sets the final Frame of the animation
self.out_point = n_frames
## Frames per second
self.frame_rate = framerate
## Composition Width
self.width = 512
## Composition has 3-D layers
self.threedimensional = False
## Composition Height
self.height = 512
## Bodymovin Version
self.version = self._version
## Composition name
self.name = None
## source items that can be used in multiple places. Comps and Images for now.
self.assets = [] # Image, Precomp
## source chars for text layers
self.chars = None
## Available fonts
self.fonts = None
def precomp(self, name):
for ass in self.assets:
if isinstance(ass, Precomp) and ass.id == name:
return ass
return None
def _on_prepare_layer(self, layer):
if layer.in_point is None:
layer.in_point = self.in_point
if layer.out_point is None:
layer.out_point = self.out_point
def tgs_sanitize(self):
"""!
Cleans up some things to ensure it works as a telegram sticker
"""
if self.width != 512 or self.height != 512:
scale = min(512/self.width, 512/self.height)
self.width = self.height = 512
for layer in self.layers:
if layer.parent_index:
continue
if layer.transform.scale.animated:
for kf in layer.transform.scale.keyframes:
if kf.start is not None:
kf.start *= scale
if kf.end is not None:
kf.end *= scale
else:
layer.transform.scale.value *= scale
if layer.transform.position.animated:
for kf in layer.transform.position.keyframes:
if kf.start is not None:
kf.start *= scale
if kf.end is not None:
kf.end *= scale
else:
layer.transform.position.value *= scale
if self.frame_rate < 45:
self.frame_rate = 30
else:
self.frame_rate = 60
def _fixup(self):
super()._fixup()
if self.assets:
for ass in self.assets:
if isinstance(ass, Precomp):
ass.animation = self
ass._fixup()
def __str__(self):
return self.name or super().__str__()
+209
View File
@@ -0,0 +1,209 @@
import os
import re
import base64
import mimetypes
from io import BytesIO
from .base import LottieObject, LottieProp, PseudoBool, Index
from .layers import Layer
from .shapes import ShapeElement
from .composition import Composition
## @ingroup Lottie
class Asset(LottieObject):
@classmethod
def _load_get_class(cls, lottiedict):
if "p" in lottiedict or "u" in lottiedict:
return Image
if "layers" in lottiedict:
return Precomp
## @ingroup Lottie
class Image(Asset):
"""!
External image
@see http://docs.aenhancers.com/sources/filesource/
"""
_props = [
LottieProp("height", "h", float, False),
LottieProp("width", "w", float, False),
LottieProp("id", "id", str, False),
LottieProp("image", "p", str, False),
LottieProp("image_path", "u", str, False),
LottieProp("is_embedded", "e", PseudoBool, False),
]
@staticmethod
def guess_mime(file):
if isinstance(file, str):
filename = file
elif hasattr(file, "name"):
filename = file.name
else:
return "application/octet-stream"
return mimetypes.guess_type(filename)
def __init__(self, id=""):
## Image Height
self.height = 0
## Image Width
self.width = 0
## Image ID
self.id = id
## Image name
self.image = ""
## Image path
self.image_path = ""
## Image data is stored as a data: url
self.is_embedded = False
def load(self, file, format=None):
"""!
@param file Filename, file object, or PIL.Image.Image to load
@param format Format to store the image data as
"""
from PIL import Image
if not isinstance(file, Image.Image):
image = Image.open(file)
else:
image = file
self._id_from_file(file)
self.image_path = ""
if format is None:
format = (image.format or "png").lower()
self.width, self.height = image.size
output = BytesIO()
image.save(output, format=format)
self.image = "data:image/%s;base64,%s" % (
format,
base64.b64encode(output.getvalue()).decode("ascii")
)
self.is_embedded = True
return self
def _id_from_file(self, file):
if not self.id:
if isinstance(file, str):
self.id = os.path.basename(file)
elif hasattr(file, "name"):
self.id = os.path.basename(file.name)
elif hasattr(file, "filename"):
self.id = os.path.basename(file.filename)
else:
self.id = "image_%s" % id(self)
@classmethod
def embedded(cls, image, format=None):
"""!
Create an object from an image file
"""
lottie_image = cls()
return lottie_image.load(image, format)
@classmethod
def linked(cls, filename):
from PIL import Image
image = Image.open(filename)
lottie_image = cls()
lottie_image._id_from_file(filename)
lottie_image.image_path, lottie_image.image = os.path.split(filename)
lottie_image.image_path += "/"
lottie_image.width = image.width
lottie_image.height = image.height
return lottie_image
def image_data(self):
"""
Returns a tuple (format, data) with the contents of the image
`format` is a string like "png", and `data` is just raw binary data.
If it's impossible to fetch this info, returns (None, None)
"""
if self.is_embedded:
m = re.match("data:[^/]+/([^;,]+);base64,(.*)", self.image)
if m:
return m.group(1), base64.b64decode(m.group(2))
return None, None
path = self.image_path + self.image
if os.path.isfile(path):
with open(path, "rb") as imgfile:
return os.path.splitext(path)[1][1:], imgfile.read()
return None, None
## @ingroup Lottie
class CharacterData(LottieObject):
"""!
Character shapes
"""
_props = [
LottieProp("shapes", "shapes", ShapeElement, True),
]
def __init__(self):
self.shapes = []
## @ingroup Lottie
class Chars(LottieObject):
"""!
Defines character shapes to avoid loading system fonts
"""
_props = [
LottieProp("character", "ch", str, False),
LottieProp("font_family", "fFamily", str, False),
LottieProp("font_size", "size", float, False),
LottieProp("font_style", "style", str, False),
LottieProp("width", "w", float, False),
LottieProp("data", "data", CharacterData, False),
]
def __init__(self):
## Character Value
self.character = ""
## Character Font Family
self.font_family = ""
## Character Font Size
self.font_size = 0
## Character Font Style
self.font_style = "" # Regular
## Character Width
self.width = 0
## Character Data
self.data = CharacterData()
@property
def shapes(self):
return self.data.shapes
## @ingroup Lottie
class Precomp(Asset, Composition):
_props = [
LottieProp("id", "id", str, False),
]
def __init__(self, id="", animation=None):
super().__init__()
## Precomp ID
self.id = id
self.animation = animation
if animation:
self.animation.assets.append(self)
def _on_prepare_layer(self, layer):
if self.animation:
self.animation.prepare_layer(layer)
def set_timing(self, outpoint, inpoint=0, override=True):
for layer in self.layers:
if override or layer.in_point is None:
layer.in_point = inpoint
if override or layer.out_point is None:
layer.out_point = outpoint
+378
View File
@@ -0,0 +1,378 @@
import enum
import inspect
import importlib
from .nvector import NVector
from .color import Color
class LottieBase:
"""!
Base class for Lottie JSON objects bindings
"""
def to_dict(self):
"""!
Serializes into a JSON object fit for the Lottie format
"""
raise NotImplementedError
@classmethod
def load(cls, lottiedict):
"""!
Loads from a JSON object
@returns An instance of the class
"""
raise NotImplementedError
def clone(self):
"""!
Returns a copy of the object
"""
raise NotImplementedError
class EnumMeta(enum.EnumMeta):
"""!
Hack to counter-hack the hack in enum meta
"""
def __new__(cls, name, bases, classdict):
classdict["__reduce_ex__"] = lambda *a, **kw: None # pragma: no cover
return super().__new__(cls, name, bases, classdict)
class LottieEnum(LottieBase, enum.Enum, metaclass=EnumMeta):
"""!
Base class for enum-like types in the Lottie JSON structure
"""
def to_dict(self):
return self.value
@classmethod
def load(cls, lottieint):
return cls(lottieint)
def clone(self):
return self
class PseudoList:
"""!
List tag for some weird values in the Lottie JSON
"""
pass
class LottieValueConverter:
"""!
Factory for property types that require special conversions
"""
def __init__(self, py, lottie, name=None):
self.py = py
self.lottie = lottie
self.name = name or "%s but displayed as %s" % (self.py.__name__, self.lottie.__name__)
def py_to_lottie(self, val):
return self.lottie(val)
def lottie_to_py(self, val):
return self.py(val)
@property
def __name__(self):
return self.name
## For values in Lottie that are bools but ints in the JSON
PseudoBool = LottieValueConverter(bool, int, "0-1 int")
class LottieProp:
"""!
Lottie <-> Python property mapper
"""
def __init__(self, name, lottie, type=float, list=False, cond=None):
## Name of the Python property
self.name = name
## Name of the Lottie JSON property
self.lottie = lottie
## Type of the property
## @see LottieValueConverter, PseudoBool
self.type = type
## Whether the property is a list of self.type
## @see PseudoList
self.list = list
## Condition on when the property is loaded from the Lottie JSON
self.cond = cond
def get(self, obj):
"""!
Returns the value of the property from a Python object
"""
return getattr(obj, self.name)
def set(self, obj, value):
"""!
Sets the value of the property from a Python object
"""
if isinstance(getattr(obj.__class__, self.name, None), property):
return
return setattr(obj, self.name, value)
def load_from_parent(self, lottiedict):
"""!
Returns the value for this property from a JSON dict representing the parent object
@returns The loaded value or @c None if the property is not in @p lottiedict
"""
if self.lottie in lottiedict:
return self.load(lottiedict[self.lottie])
return None
def load_into(self, lottiedict, obj):
"""!
Loads from a Lottie dict into an object
"""
if self.cond and not self.cond(lottiedict):
return
self.set(obj, self.load_from_parent(lottiedict))
def load(self, lottieval):
"""!
Loads the property from a JSON value
@returns the Python equivalent of the JSON value
"""
if self.list is PseudoList and isinstance(lottieval, list):
return self._load_scalar(lottieval[0])
#return [
#self._load_scalar(it)
#for it in lottieval
#]
elif self.list is True:
return list(filter(lambda x: x is not None, (
self._load_scalar(it)
for it in lottieval
)))
return self._load_scalar(lottieval)
def _load_scalar(self, lottieval):
if lottieval is None:
return None
if inspect.isclass(self.type) and issubclass(self.type, LottieBase):
return self.type.load(lottieval)
elif isinstance(self.type, type) and isinstance(lottieval, self.type):
return lottieval
elif isinstance(self.type, LottieValueConverter):
return self.type.lottie_to_py(lottieval)
elif self.type is NVector:
return NVector(*lottieval)
elif self.type is Color:
return Color(*lottieval)
if isinstance(lottieval, list) and lottieval:
lottieval = lottieval[0]
return self.type(lottieval)
def to_dict(self, obj):
"""!
Converts the value of the property as from @p obj into a JSON value
@param obj LottieObject with this property
"""
val = self._basic_to_dict(self.get(obj))
if self.list is PseudoList:
if not isinstance(obj, list):
return [val]
elif isinstance(self.type, LottieValueConverter):
val = self._basic_to_dict(self.type.py_to_lottie(val))
return val
def _basic_to_dict(self, v):
if isinstance(v, LottieBase):
return v.to_dict()
elif isinstance(v, NVector):
return list(map(self._basic_to_dict, v.components))
elif isinstance(v, list):
return list(map(self._basic_to_dict, v))
elif isinstance(v, (int, str, bool)):
return v
elif isinstance(v, float):
if v % 1 == 0:
return int(v)
return v #round(v, 3)
else:
raise Exception("Unknown value %r" % v)
def __repr__(self):
return "<LottieProp %s:%s>" % (self.name, self.lottie)
def clone_value(self, value):
if isinstance(value, list):
return [self.clone_value(v) for v in value]
if isinstance(value, (LottieBase, NVector)):
return value.clone()
if isinstance(value, (int, float, bool, str)) or value is None:
return value
raise Exception("Could not convert %r" % value)
class LottieObjectMeta(type):
def __new__(cls, name, bases, attr):
props = []
for base in bases:
if type(base) == cls:
props += base._props
attr["_props"] = props + attr.get("_props", [])
return super().__new__(cls, name, bases, attr)
class LottieObject(LottieBase, metaclass=LottieObjectMeta):
"""!
@brief Base class for mapping Python classes into Lottie JSON objects
"""
def to_dict(self):
return {
prop.lottie: prop.to_dict(self)
for prop in self._props
if prop.get(self) is not None
}
@classmethod
def load(cls, lottiedict):
if "__pyclass" in lottiedict:
return CustomObject.load(lottiedict)
if not lottiedict:
return None
cls = cls._load_get_class(lottiedict)
obj = cls()
for prop in cls._props:
prop.load_into(lottiedict, obj)
return obj
@classmethod
def _load_get_class(cls, lottiedict):
return cls
def find(self, search, propname="name"):
"""!
@param search The value of the property to search
@param propname The name of the property used to search
@brief Recursively searches for child objects with a matching property
"""
if getattr(self, propname, None) == search:
return self
for prop in self._props:
v = prop.get(self)
if isinstance(v, LottieObject):
found = v.find(search, propname)
if found:
return found
elif isinstance(v, list) and v and isinstance(v[0], LottieObject):
for obj in v:
found = obj.find(search, propname)
if found:
return found
return None
def find_all(self, type, predicate=None, include_self=True):
"""!
Find all child objects that match a predicate
@param type Type (or tuple of types) of the objects to match
@param predicate Function that returns true on the objects to find
@param include_self Whether should counsider `self` for a potential match
"""
if isinstance(self, type) and include_self:
if not predicate or predicate(self):
yield self
for prop in self._props:
v = prop.get(self)
if isinstance(v, LottieObject):
for found in v.find_all(type, predicate, True):
yield found
elif isinstance(v, list) and v and isinstance(v[0], LottieObject):
for child in v:
for found in child.find_all(type, predicate, True):
yield found
def clone(self):
obj = self.__class__()
for prop in self._props:
v = prop.get(self)
prop.set(obj, prop.clone_value(v))
return obj
def __str__(self):
return type(self).__name__
class Index:
"""!
@brief Simple iterator to generate increasing integers
"""
def __init__(self):
self._i = -1
def __next__(self):
self._i += 1
return self._i
class CustomObject(LottieObject):
"""!
Allows extending the Lottie shapes with custom Python classes
"""
wrapped_lottie = LottieObject
def __init__(self):
self.wrapped = self.wrapped_lottie()
@classmethod
def load(cls, lottiedict):
ld = lottiedict.copy()
classname = ld.pop("__pyclass")
modn, clsn = classname.rsplit(".", 1)
subcls = getattr(importlib.import_module(modn), clsn)
obj = subcls()
for prop in subcls._props:
prop.load_into(lottiedict, obj)
obj.wrapped = subcls.wrapped_lottie.load(ld)
return obj
def clone(self):
obj = self.__class__(**self.to_pyctor())
obj.wrapped = self.wrapped.clone()
return obj
def to_dict(self):
dict = self.wrapped.to_dict()
dict["__pyclass"] = "{0.__module__}.{0.__name__}".format(self.__class__)
dict.update(LottieObject.to_dict(self))
return dict
def _build_wrapped(self):
return self.wrapped_lottie()
def refresh(self):
self.wrapped = self._build_wrapped()
class ObjectVisitor:
DONT_RECURSE = object()
def __call__(self, lottie_object):
self._process(lottie_object)
def _process(self, lottie_object):
self.visit(lottie_object)
for p in lottie_object._props:
pval = p.get(lottie_object)
if self.visit_property(lottie_object, p, pval) is not self.DONT_RECURSE:
if isinstance(pval, LottieObject):
self._process(pval)
elif isinstance(pval, list) and pval and isinstance(pval[0], LottieObject):
for c in pval:
self._process(c)
def visit(self, object):
pass
def visit_property(self, object, property, value):
pass
+485
View File
@@ -0,0 +1,485 @@
import math
from .base import LottieObject, LottieProp
from .nvector import NVector
class BezierPoint:
def __init__(self, vertex, in_tangent=None, out_tangent=None):
self.vertex = vertex
self.in_tangent = in_tangent or NVector(0, 0)
self.out_tangent = out_tangent or NVector(0, 0)
def relative(self):
return self
@classmethod
def smooth(cls, point, in_tangent):
return cls(point, in_tangent, -in_tangent)
@classmethod
def from_absolute(cls, point, in_tangent=None, out_tangent=None):
if not in_tangent:
in_tangent = point.clone()
if not out_tangent:
out_tangent = point.clone()
return BezierPoint(point, in_tangent, out_tangent)
class BezierPointView:
"""
View for bezier point
"""
def __init__(self, bezier, index):
self.bezier = bezier
self.index = index
@property
def vertex(self):
return self.bezier.vertices[self.index]
@vertex.setter
def vertex(self, point):
self.bezier.vertices[self.index] = point
@property
def in_tangent(self):
return self.bezier.in_tangents[self.index]
@in_tangent.setter
def in_tangent(self, point):
self.bezier.in_tangents[self.index] = point
@property
def out_tangent(self):
return self.bezier.out_tangents[self.index]
@out_tangent.setter
def out_tangent(self, point):
self.bezier.out_tangents[self.index] = point
def relative(self):
return self
class AbsoluteBezierPointView(BezierPointView):
@property
def in_tangent(self):
return self.bezier.in_tangents[self.index] + self.vertex
@in_tangent.setter
def in_tangent(self, point):
self.bezier.in_tangents[self.index] = point - self.vertex
@property
def out_tangent(self):
return self.bezier.out_tangents[self.index] + self.vertex
@out_tangent.setter
def out_tangent(self, point):
self.bezier.out_tangents[self.index] = point - self.vertex
def relative(self):
return BezierPointView(self.bezier, self.index)
class BezierView:
def __init__(self, bezier, absolute=False):
self.bezier = bezier
self.is_absolute = absolute
def point(self, index):
if self.is_absolute:
return AbsoluteBezierPointView(self.bezier, index)
return BezierPointView(self.bezier, index)
def __len__(self):
return len(self.bezier.vertices)
def __getitem__(self, key):
if isinstance(key, slice):
return [
self.point(i)
for i in key
]
return self.point(key)
def __iter__(self):
for i in range(len(self)):
yield self.point(i)
def append(self, point):
if isinstance(point, NVector):
self.bezier.add_point(point.clone())
else:
bpt = point.relative()
self.bezier.add_point(bpt.vertex.clone(), bpt.in_tangent.clone(), bpt.out_tangent.clone())
@property
def absolute(self):
return BezierView(self.bezier, True)
## @ingroup Lottie
class Bezier(LottieObject):
"""!
Single bezier curve
"""
_props = [
LottieProp("closed", "c", bool, False),
LottieProp("in_tangents", "i", NVector, True),
LottieProp("out_tangents", "o", NVector, True),
LottieProp("vertices", "v", NVector, True),
]
def __init__(self):
## Closed property of shape
self.closed = False
## Cubic bezier handles for the segments before each vertex
self.in_tangents = []
## Cubic bezier handles for the segments after each vertex
self.out_tangents = []
## Bezier curve vertices.
self.vertices = []
#self.rel_tangents = rel_tangents
## More convent way to access points
self.points = BezierView(self)
def clone(self):
clone = Bezier()
clone.closed = self.closed
clone.in_tangents = [p.clone() for p in self.in_tangents]
clone.out_tangents = [p.clone() for p in self.out_tangents]
clone.vertices = [p.clone() for p in self.vertices]
#clone.rel_tangents = self.rel_tangents
return clone
def insert_point(self, index, pos, inp=NVector(0, 0), outp=NVector(0, 0)):
"""!
Inserts a point at the given index
@param index Index to insert the point at
@param pos Point to add
@param inp Tangent entering the point, as a vector relative to @p pos
@param outp Tangent exiting the point, as a vector relative to @p pos
@returns @c self, for easy chaining
"""
self.vertices.insert(index, pos)
self.in_tangents.insert(index, inp.clone())
self.out_tangents.insert(index, outp.clone())
#if not self.rel_tangents:
#self.in_tangents[-1] += pos
#self.out_tangents[-1] += pos
return self
def add_point(self, pos, inp=NVector(0, 0), outp=NVector(0, 0)):
"""!
Appends a point to the curve
@see insert_point
"""
self.insert_point(len(self.vertices), pos, inp, outp)
return self
def add_smooth_point(self, pos, inp):
"""!
Appends a point with symmetrical tangents
@see insert_point
"""
self.add_point(pos, inp, -inp)
return self
def close(self, closed=True):
"""!
Updates self.closed
@returns @c self, for easy chaining
"""
self.closed = closed
return self
def point_at(self, t):
"""!
@param t A value between 0 and 1, percentage along the length of the curve
@returns The point at @p t in the curve
"""
i, t = self._index_t(t)
points = self._bezier_points(i, True)
return self._solve_bezier(t, points)
def tangent_angle_at(self, t):
i, t = self._index_t(t)
points = self._bezier_points(i, True)
n = len(points) - 1
if n > 0:
delta = sum((
(points[i+1] - points[i]) * n * self._solve_bezier_coeff(i, n - 1, t)
for i in range(n)
), NVector(0, 0))
return math.atan2(delta.y, delta.x)
return 0
def _split(self, t):
i, t = self._index_t(t)
cub = self._bezier_points(i, True)
split1, split2 = self._split_segment(t, cub)
return i, split1, split2
def _split_segment(self, t, cub):
if len(cub) == 2:
k = self._solve_bezier_step(t, cub)[0]
split1 = [cub[0], NVector(0, 0), NVector(0, 0), k]
split2 = [k, NVector(0, 0), NVector(0, 0), cub[-1]]
return split1, split2
if len(cub) == 3:
quad = cub
else:
quad = self._solve_bezier_step(t, cub)
lin = self._solve_bezier_step(t, quad)
k = self._solve_bezier_step(t, lin)[0]
split1 = [cub[0], quad[0]-cub[0], lin[0]-k, k]
split2 = [k, lin[-1]-k, quad[-1]-cub[-1], cub[-1]]
return split1, split2
def split_at(self, t):
"""!
Get two pieces out of a Bezier curve
@param t A value between 0 and 1, percentage along the length of the curve
@returns Two Bezier objects that correspond to self, but split at @p t
"""
i, split1, split2 = self._split(t)
seg1 = Bezier()
seg2 = Bezier()
for j in range(i):
seg1.add_point(self.vertices[j].clone(), self.in_tangents[j].clone(), self.out_tangents[j].clone())
for j in range(i+2, len(self.vertices)):
seg2.add_point(self.vertices[j].clone(), self.in_tangents[j].clone(), self.out_tangents[j].clone())
seg1.add_point(split1[0], self.in_tangents[i].clone(), split1[1])
seg1.add_point(split1[3], split1[2], split2[1])
seg2.insert_point(0, split2[0], split1[2], split2[1])
seg2.insert_point(1, split2[3], split2[2], self.out_tangents[i+1].clone())
return seg1, seg2
def segment(self, t1, t2):
"""!
Splits a Bezier in two points and returns the segment between the
@param t1 A value between 0 and 1, percentage along the length of the curve
@param t2 A value between 0 and 1, percentage along the length of the curve
@returns Bezier object that correspond to the segment between @p t1 and @p t2
"""
if self.closed and self.vertices and self.vertices[-1] != self.vertices[0]:
copy = self.clone()
copy.add_point(self.vertices[0])
copy.closed = False
return copy.segment(t1, t2)
if t1 > 1:
t1 = 1
if t2 > 1:
t2 = 1
if t1 > t2:
t1, t2 = t2, t1
elif t1 == t2:
seg = Bezier()
p = self.point_at(t1)
seg.add_point(p)
seg.add_point(p)
return seg
seg1, seg2 = self.split_at(t1)
t2p = (t2-t1) / (1-t1)
seg3, seg4 = seg2.split_at(t2p)
return seg3
def split_self_multi(self, positions):
"""!
Adds more points to the Bezier
@param positions list of percentages along the curve
"""
if not len(positions):
return
t1 = positions[0]
seg1, seg2 = self.split_at(t1)
self.vertices = []
self.in_tangents = []
self.out_tangents = []
self.vertices = seg1.vertices[:-1]
self.in_tangents = seg1.in_tangents[:-1]
self.out_tangents = seg1.out_tangents[:-1]
for t2 in positions[1:]:
t = (t2-t1) / (1-t1)
seg1, seg2 = seg2.split_at(t)
t1 = t
self.vertices += seg1.vertices[:-1]
self.in_tangents += seg1.in_tangents[:-1]
self.out_tangents += seg1.out_tangents[:-1]
self.vertices += seg2.vertices
self.in_tangents += seg2.in_tangents
self.out_tangents += seg2.out_tangents
def split_each_segment(self):
"""!
Adds a point in the middle of the segment between every pair of points in the Bezier
"""
vertices = self.vertices
in_tangents = self.in_tangents
out_tangents = self.out_tangents
self.vertices = []
self.in_tangents = []
self.out_tangents = []
for i in range(len(vertices)-1):
tocut = [vertices[i], out_tangents[i]+vertices[i], in_tangents[i+1]+vertices[i+1], vertices[i+1]]
split1, split2 = self._split_segment(0.5, tocut)
if i:
self.out_tangents[-1] = split1[1]
else:
self.add_point(vertices[0], in_tangents[0], split1[1])
self.add_point(split1[3], split1[2], split2[1])
self.add_point(vertices[i+1], split2[2], NVector(0, 0))
def split_self_chunks(self, n_chunks):
"""!
Adds points the Bezier, splitting it into @p n_chunks additional chunks.
"""
splits = [i/n_chunks for i in range(1, n_chunks)]
return self.split_self_multi(splits)
def _bezier_points(self, i, optimize):
v1 = self.vertices[i].clone()
v2 = self.vertices[i+1].clone()
points = [v1]
t1 = self.out_tangents[i].clone()
if not optimize or t1.length != 0:
points.append(t1+v1)
t2 = self.in_tangents[i+1].clone()
if not optimize or t1.length != 0:
points.append(t2+v2)
points.append(v2)
return points
def _solve_bezier_step(self, t, points):
next = []
p1 = points[0]
for p2 in points[1:]:
next.append(p1 * (1-t) + p2 * t)
p1 = p2
return next
def _solve_bezier_coeff(self, i, n, t):
return (
math.factorial(n) / (math.factorial(i) * math.factorial(n - i)) # (n choose i)
* (t ** i) * ((1 - t) ** (n-i))
)
def _solve_bezier(self, t, points):
n = len(points) - 1
if n > 0:
return sum((
points[i] * self._solve_bezier_coeff(i, n, t)
for i in range(n+1)
), NVector(0, 0))
#while len(points) > 1:
#points = self._solve_bezier_step(t, points)
return points[0]
def _index_t(self, t):
if t <= 0:
return 0, 0
if t >= 1:
return len(self.vertices)-2, 1
n = len(self.vertices)-1
for i in range(n):
if (i+1) / n > t:
break
return i, (t - (i/n)) * n
def reverse(self):
"""!
Reverses the Bezier curve
"""
self.vertices = list(reversed(self.vertices))
out_tangents = list(reversed(self.in_tangents))
in_tangents = list(reversed(self.out_tangents))
self.in_tangents = in_tangents
self.out_tangents = out_tangents
"""def to_absolute(self):
if self.rel_tangents:
self.rel_tangents = False
for i in range(len(self.vertices)):
p = self.vertices[i]
self.in_tangents[i] += p
self.out_tangents[i] += p
return self"""
def rounded(self, round_distance):
cloned = Bezier()
cloned.closed = self.closed
round_corner = 0.5519
def _get_vt(closest_index):
closer_v = self.vertices[closest_index]
distance = (current - closer_v).length
new_pos_perc = min(distance/2, round_distance) / distance if distance else 0
vert = current + (closer_v - current) * new_pos_perc
tan = - (vert - current) * round_corner
return vert, tan
for i, current in enumerate(self.vertices):
if not self.closed and (i == 0 or i == len(self.points) - 1):
cloned.points.append(self.points[i])
else:
vert1, out_t = _get_vt(i - 1)
cloned.add_point(vert1, NVector(0, 0), out_t)
vert2, in_t = _get_vt((i+1) % len(self.points))
cloned.add_point(vert2, in_t, NVector(0, 0))
return cloned
def scale(self, amount):
for vl in (self.vertices, self.in_tangents, self.out_tangents):
for v in vl:
v *= amount
def lerp(self, other, t):
if len(other.vertices) != len(self.vertices):
if t < 1:
return self.clone()
return other.clone()
bez = Bezier()
bez.closed = self.closed
for vlist_name in ["vertices", "in_tangents", "out_tangents"]:
vlist = getattr(self, vlist_name)
olist = getattr(other, vlist_name)
out = getattr(bez, vlist_name)
for v, o in zip(vlist, olist):
out.append(v.lerp(o, t))
return bez
def rough_length(self):
if len(self.vertices) < 2:
return 0
last = self.vertices[0]
length = 0
for v in self.vertices[1:]:
length += (v-last).length
last = v
if self.closed:
length += (last-self.vertices[0]).length
return length
+447
View File
@@ -0,0 +1,447 @@
import enum
import math
import colorsys
from .nvector import NVector
def from_uint8(r, g, b, a=255):
return Color(r, g, b, a) / 255
class ColorMode(enum.Enum):
## sRGB, Components in [0, 1]
RGB = enum.auto()
## HSV, components in [0, 1]
HSV = enum.auto()
## HSL, components in [0, 1]
HSL = enum.auto()
## CIE XYZ with Illuminant D65. Components in [0, 1]
XYZ = enum.auto()
## CIE L*u*v*
LUV = enum.auto()
## CIE Lch(uv), polar version of LUV where C is the radius and H an angle in radians
LCH_uv = enum.auto()
## CIE L*a*b*
LAB = enum.auto()
## CIE LCh(ab), polar version of LAB where C is the radius and H an angle in radians
#LCH_ab = enum.auto()
def _clamp(x):
return max(0, min(1, x))
class Conversion:
_conv_paths = {
(ColorMode.RGB, ColorMode.RGB): [],
(ColorMode.RGB, ColorMode.HSV): [],
(ColorMode.RGB, ColorMode.HSL): [],
(ColorMode.RGB, ColorMode.XYZ): [],
(ColorMode.RGB, ColorMode.LUV): [ColorMode.XYZ],
(ColorMode.RGB, ColorMode.LAB): [ColorMode.XYZ],
(ColorMode.RGB, ColorMode.LCH_uv): [ColorMode.XYZ, ColorMode.LUV],
#(ColorMode.RGB, ColorMode.LCH_ab): [ColorMode.XYZ, ColorMode.LAB],
(ColorMode.HSV, ColorMode.RGB): [],
(ColorMode.HSV, ColorMode.HSV): [],
(ColorMode.HSV, ColorMode.HSL): [],
(ColorMode.HSV, ColorMode.XYZ): [ColorMode.RGB],
(ColorMode.HSV, ColorMode.LUV): [ColorMode.RGB, ColorMode.XYZ],
(ColorMode.HSV, ColorMode.LAB): [ColorMode.RGB, ColorMode.XYZ],
(ColorMode.HSV, ColorMode.LCH_uv): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LUV],
#(ColorMode.HSV, ColorMode.LCH_ab): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LAB],
(ColorMode.HSL, ColorMode.RGB): [],
(ColorMode.HSL, ColorMode.HSV): [],
(ColorMode.HSL, ColorMode.HSL): [],
(ColorMode.HSL, ColorMode.XYZ): [ColorMode.RGB],
(ColorMode.HSL, ColorMode.LUV): [ColorMode.RGB, ColorMode.XYZ],
(ColorMode.HSL, ColorMode.LAB): [ColorMode.RGB, ColorMode.XYZ],
(ColorMode.HSL, ColorMode.LCH_uv): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LUV],
#(ColorMode.HSL, ColorMode.LCH_ab): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LAB],
(ColorMode.XYZ, ColorMode.RGB): [],
(ColorMode.XYZ, ColorMode.HSV): [ColorMode.RGB],
(ColorMode.XYZ, ColorMode.HSL): [ColorMode.RGB],
(ColorMode.XYZ, ColorMode.XYZ): [],
(ColorMode.XYZ, ColorMode.LUV): [],
(ColorMode.XYZ, ColorMode.LAB): [],
(ColorMode.XYZ, ColorMode.LCH_uv): [ColorMode.LUV],
#(ColorMode.XYZ, ColorMode.LCH_ab): [ColorMode.LAB],
(ColorMode.LCH_uv, ColorMode.RGB): [ColorMode.LUV, ColorMode.XYZ],
(ColorMode.LCH_uv, ColorMode.HSV): [ColorMode.LUV, ColorMode.XYZ, ColorMode.RGB],
(ColorMode.LCH_uv, ColorMode.HSL): [ColorMode.LUV, ColorMode.XYZ, ColorMode.RGB],
(ColorMode.LCH_uv, ColorMode.XYZ): [ColorMode.LUV],
(ColorMode.LCH_uv, ColorMode.LUV): [],
(ColorMode.LCH_uv, ColorMode.LAB): [ColorMode.LUV, ColorMode.XYZ],
(ColorMode.LCH_uv, ColorMode.LCH_uv): [],
#(ColorMode.LCH_uv, ColorMode.LCH_ab): [ColorMode.LUV, ColorMode.XYZ, ColorMode.LAB],
(ColorMode.LUV, ColorMode.RGB): [ColorMode.XYZ],
(ColorMode.LUV, ColorMode.HSV): [ColorMode.XYZ, ColorMode.RGB],
(ColorMode.LUV, ColorMode.HSL): [ColorMode.XYZ, ColorMode.RGB],
(ColorMode.LUV, ColorMode.XYZ): [],
(ColorMode.LUV, ColorMode.LUV): [],
(ColorMode.LUV, ColorMode.LAB): [ColorMode.XYZ],
(ColorMode.LUV, ColorMode.LCH_uv): [],
#(ColorMode.LUV, ColorMode.LCH_ab): [ColorMode.XYZ, ColorMode.LAB],
(ColorMode.LAB, ColorMode.RGB): [ColorMode.XYZ],
(ColorMode.LAB, ColorMode.HSV): [ColorMode.XYZ, ColorMode.RGB],
(ColorMode.LAB, ColorMode.HSL): [ColorMode.XYZ, ColorMode.RGB],
(ColorMode.LAB, ColorMode.XYZ): [],
(ColorMode.LAB, ColorMode.LUV): [ColorMode.XYZ],
(ColorMode.LAB, ColorMode.LAB): [],
(ColorMode.LAB, ColorMode.LCH_uv): [ColorMode.XYZ, ColorMode.LUV],
#(ColorMode.LAB, ColorMode.LCH_ab): [],
#(ColorMode.LCH_ab, ColorMode.RGB): [ColorMode.LAB, ColorMode.XYZ],
#(ColorMode.LCH_ab, ColorMode.HSV): [ColorMode.LAB, ColorMode.XYZ, ColorMode.RGB],
#(ColorMode.LCH_ab, ColorMode.HSL): [ColorMode.LAB, ColorMode.XYZ, ColorMode.RGB],
#(ColorMode.LCH_ab, ColorMode.XYZ): [ColorMode.LAB],
#(ColorMode.LCH_ab, ColorMode.LUV): [ColorMode.LAB, ColorMode.XYZ],
#(ColorMode.LCH_ab, ColorMode.LAB): [],
#(ColorMode.LCH_ab, ColorMode.LCH_uv): [ColorMode.LAB, ColorMode.XYZ, ColorMode.LUV],
#(ColorMode.LCH_ab, ColorMode.LCH_ab): [],
}
@staticmethod
def rgb_to_hsv(r, g, b):
return colorsys.rgb_to_hsv(r, g, b)
@staticmethod
def hsv_to_rgb(r, g, b):
return colorsys.hsv_to_rgb(r, g, b)
@staticmethod
def hsl_to_hsv(h, s_hsl, l):
v = l + s_hsl * min(l, 1 - l)
s_hsv = 0 if v == 0 else 2 - 2 * l / v
return (h, s_hsv, v)
@staticmethod
def hsv_to_hsl(h, s_hsv, v):
l = v - v * s_hsv / 2
s_hsl = 0 if l in (0, 1) else (v - l) / min(l, 1 - l)
return (h, s_hsl, l)
@staticmethod
def rgb_to_hsl(r, g, b):
h, l, s = colorsys.rgb_to_hls(r, g, b)
return (h, s, l)
@staticmethod
def hsl_to_rgb(h, s, l):
return colorsys.hls_to_rgb(h, l, s)
# http://w3.uqo.ca/missaoui/Publications/TRColorSpace.zip
#@staticmethod
#def rgb_to_hcl(r, g, b, gamma=3, y0=100):
#maxc = max(r, g, b)
#minc = min(r, g, b)
#if maxc > 0:
#alpha = 1/y0 * minc / maxc
#else:
#alpha = 0
#q = math.e ** (alpha * gamma)
#h = math.atan2(g - b, r - g)
#if h < 0:
#h += 2*math.pi
#h /= 2*math.pi
#c = q / 3 * (abs(r-g) + abs(g-b) + abs(b-r))
#l = (q * maxc + (q-1) * minc) / 2
#return (h, c, l)
#@staticmethod
#def hcl_to_rgb(h, c, l, gamma=3, y0=100):
#h *= 2*math.pi
#q = math.e ** ((1 - 2*c / 4*l) * gamma / y0)
#minc = (4*l - 3*c) / (4*q - 2)
#maxc = minc + 3*c / 2*q
#if h <= math.pi * 1 / 3:
#tan = math.tan(3/2*h)
#r = maxc
#b = minc
#g = (r * tan + b) / (1 + tan)
#elif h <= math.pi * 2 / 3:
#tan = math.tan(3/4*(h-math.pi))
#g = maxc
#b = minc
#r = (g * (1+tan) - b) / tan
#elif h <= math.pi * 3 / 3:
#tan = math.tan(3/4*(h-math.pi))
#g = maxc
#r = minc
#b = g * (1+tan) - r * tan
#elif h <= math.pi * 4 / 3:
#tan = math.tan(3/2*(h+math.pi))
#b = maxc
#r = minc
#g = (r * tan + b) / (1 + tan)
#elif h <= math.pi * 5 / 3:
#tan = math.tan(3/4*h)
#b = maxc
#g = minc
#r = (g * (1+tan) - b) / tan
#else:
#tan = math.tan(3/4*h)
#r = maxc
#g = minc
#b = g * (1+tan) - r * tan
#return _clamp(r), _clamp(g), _clamp(b)
@staticmethod
def rgb_to_xyz(r, g, b):
def _gamma(v):
return v / 12.92 if v <= 0.04045 else ((v + 0.055) / 1.055) ** 2.4
rgb = (_gamma(r), _gamma(g), _gamma(b))
matrix = [
[0.4124564, 0.3575761, 0.1804375],
[0.2126729, 0.7151522, 0.0721750],
[0.0193339, 0.1191920, 0.9503041],
]
return tuple(
sum(rgb[i] * c for i, c in enumerate(row))
for row in matrix
)
@staticmethod
def xyz_to_rgb(x, y, z):
def _gamma1(v):
return _clamp(v * 12.92 if v <= 0.0031308 else v ** (1/2.4) * 1.055 - 0.055)
matrix = [
[+3.2404542, -1.5371385, -0.4985314],
[-0.9692660, +1.8760108, +0.0415560],
[+0.0556434, -0.2040259, +1.0572252],
]
xyz = (x, y, z)
return tuple(map(_gamma1, (
sum(xyz[i] * c for i, c in enumerate(row))
for row in matrix
)))
@staticmethod
def xyz_to_luv(x, y, z):
u1r = 0.2009
v1r = 0.4610
yr = 100
kap = (29/3)**3
eps = (6/29)**3
try:
u1 = 4*x / (x + 15*y + 3*z)
v1 = 9*y / (x + 15*y + 3*z)
except ZeroDivisionError:
return 0, 0, 0
y_r = y/yr
l = 166 * y_r ** (1/3) - 16 if y_r > eps else kap * y_r
u = 13 * l * (u1 - u1r)
v = 13 * l * (v1 - v1r)
return l, u, v
@staticmethod
def luv_to_xyz(l, u, v):
u1r = 0.2009
v1r = 0.4610
yr = 100
kap = (29/3)**3
if l == 0:
u1 = u1r
v1 = v1r
else:
u1 = u / (13 * l) + u1r
v1 = v / (13 * l) + v1r
y = yr * l / kap if l <= 8 else yr * ((l + 16) / 116) ** 3
x = y * 9*u1 / (4*v1)
z = y * (12 - 3*u1 - 20*v1) / (4*v1)
return x, y, z
@staticmethod
def luv_to_lch_uv(l, u, v):
c = math.hypot(u, v)
h = math.atan2(v, u)
if h < 0:
h += math.tau
return l, c, h
@staticmethod
def lch_uv_to_luv(l, c, h):
u = math.cos(h) * c
v = math.sin(h) * c
return l, u, v
@staticmethod
def xyz_to_lab(x, y, z):
# D65 Illuminant aka sRGB(1,1,1)
xn = 0.950489
yn = 1
zn = 108.8840
delta = 6 / 29
def f(t):
return t ** (1/3) if t > delta ** 3 else t / (3*delta**2) + 4/29
fy = f(y/yn)
l = 116 * fy - 16
a = 500 * (f(x/xn) - fy)
b = 200 * (fy - f(z/zn))
return l, a, b
@staticmethod
def lab_to_xyz(l, a, b):
# D65 Illuminant aka sRGB(1,1,1)
xn = 0.950489
yn = 1
zn = 108.8840
delta = 6 / 29
def f1(t):
return t**3 if t > delta else 3*delta**2*(t-4/29)
l1 = (l+16) / 116
x = xn * f1(l1+a/500)
y = yn * f1(l1)
z = zn * f1(l1-b/200)
return x, y, z
#@staticmethod
#def lab_to_lch_ab(l, a, b):
#c = math.hypot(a, b)
#h = math.atan2(b, a)
#if h < 0:
#h += math.tau
#return l, c, h
#@staticmethod
#def lch_ab_to_lab(l, c, h):
#a = math.cos(h) * c
#b = math.sin(h) * c
#return l, a, b
@staticmethod
def conv_func(mode_from, mode_to):
return getattr(Conversion, "%s_to_%s" % (mode_from.name.lower(), mode_to.name.lower()), None)
@staticmethod
def convert(tuple, mode_from, mode_to):
if mode_from == mode_to:
return tuple
if len(tuple) == 4:
alpha = tuple[3]
tuple = tuple[:3]
else:
alpha = None
func = Conversion.conv_func(mode_from, mode_to)
if func:
return func(*tuple)
if (mode_from, mode_to) in Conversion._conv_paths:
steps = Conversion._conv_paths[(mode_from, mode_to)] + [mode_to]
for step in steps:
func = Conversion.conv_func(mode_from, step)
if not func:
raise ValueError("Missing definition for conversion from %s to %s" % (mode_from, step))
tuple = func(*tuple)
mode_from = step
if alpha is not None:
tuple += (alpha,)
return tuple
raise ValueError("No conversion path from %s to %s" % (mode_from, mode_to))
class Color(NVector):
Mode = ColorMode
def __init__(self, c1=0, c2=0, c3=0, a=1, *, mode=ColorMode.RGB):
if isinstance(a, ColorMode):
raise TypeError("Please update the Color constructor")
super().__init__(c1, c2, c3, a)
self._mode = mode
@property
def mode(self):
return self._mode
def convert(self, v):
if v == self._mode:
return self
self.components = list(Conversion.convert(self.components, self._mode, v))
self._mode = v
return self
def clone(self):
return Color(*self.components, mode=self._mode)
def converted(self, mode):
return self.clone().convert(mode)
def to_rgb(self):
return self.converted(ColorMode.RGB)
def __repr__(self):
return "<%s %s [%.3f, %.3f, %.3f, %.3f]>" % (
(self.__class__.__name__, self.mode.name) + tuple(self.components)
)
def component_names(self):
comps = None
if self._mode == ColorMode.RGB:
comps = ({"r", "red"}, {"g", "green"}, {"b", "blue"})
elif self._mode == ColorMode.HSV:
comps = ({"h", "hue"}, {"s", "saturation"}, {"v", "value"})
elif self._mode == ColorMode.HSL:
comps = ({"h", "hue"}, {"s", "saturation"}, {"l", "lightness"})
elif self._mode == ColorMode.LCH_uv: # in (ColorMode.LCH_uv, ColorMode.LCH_ab):
comps = ({"l", "luma", "luminance"}, {"c", "choma"}, {"h", "hue"})
elif self._mode == ColorMode.XYZ:
comps = "xyz"
elif self._mode == ColorMode.LUV:
comps = "luv"
elif self._mode == ColorMode.LAB:
comps = "lab"
return comps
def _attrindex(self, name):
comps = self.component_names()
if comps:
for i, vals in enumerate(comps):
if name in vals:
return i
return None
def __getattr__(self, name):
if name not in vars(self) and name not in {"_mode", "components"}:
i = self._attrindex(name)
if i is not None:
return self.components[i]
raise AttributeError(name)
def __setattr__(self, name, value):
if name not in vars(self) and name not in {"_mode", "components"}:
i = self._attrindex(name)
if i is not None:
self.components[i] = value
return
return super().__setattr__(name, value)
+80
View File
@@ -0,0 +1,80 @@
from .base import LottieObject, Index, LottieProp
from .layers import Layer
## @ingroup Lottie
class Composition(LottieObject):
"""!
Base class for layer holders
"""
_props = [
LottieProp("layers", "layers", Layer, True),
]
def __init__(self):
## List of Composition Layers
self.layers = [] # ShapeLayer, SolidLayer, CompLayer, ImageLayer, NullLayer, TextLayer
self._index_gen = Index()
def layer(self, index):
for layer in self.layers:
if layer.index == index:
return layer
raise IndexError("No layer %s" % index)
def add_layer(self, layer: Layer):
"""!
@brief Appends a layer to the composition
@see insert_layer
"""
return self.insert_layer(len(self.layers), layer)
@classmethod
def load(cls, lottiedict):
obj = super().load(lottiedict)
obj._fixup()
return obj
def _fixup(self):
for layer in self.layers:
layer.composition = self
def insert_layer(self, index, layer: Layer):
"""!
@brief Inserts a layer to the composition
@note Layers added first will be rendered on top of later layers
"""
self.layers.insert(index, layer)
self.prepare_layer(layer)
return layer
def prepare_layer(self, layer: Layer):
layer.composition = self
if layer.index is None:
layer.index = next(self._index_gen)
self._on_prepare_layer(layer)
def _on_prepare_layer(self, layer):
raise NotImplementedError
def clone(self):
c = super().clone()
c._index_gen._i = self._index_gen._i
return c
def remove_layer(self, layer: Layer):
"""!
@brief Removes a layer (and all of its children) from this composition
@param layer Layer to be removed
"""
if layer.composition is not self:
return
children = list(layer.children)
layer.composition = None
self.layers.remove(layer)
for c in children:
self.remove_layer(c)
File diff suppressed because it is too large Load Diff
+114
View File
@@ -0,0 +1,114 @@
import math
from .base import LottieObject, LottieProp, PseudoList, PseudoBool
## @ingroup Lottie
class KeyframeBezierHandle(LottieObject):
"""!
Bezier handle for keyframe interpolation
"""
_props = [
LottieProp("x", "x", list=PseudoList),
LottieProp("y", "y", list=PseudoList),
]
def __init__(self, x=0, y=0):
## x position of the handle.
## This represents the change in time of the keyframe
self.x = x
## y position of the handle.
## This represents the change in value of the keyframe
self.y = y
class Linear:
"""!
Linear easing, the value will change from start to end in a straight line
"""
def __call__(self, keyframe):
keyframe.out_value = KeyframeBezierHandle(
0,
0
)
keyframe.in_value = KeyframeBezierHandle(
1,
1
)
class EaseIn:
"""!
The value lingers near the start before accelerating towards the end
"""
def __init__(self, delay=1/3):
self.delay = delay
def __call__(self, keyframe):
keyframe.out_value = KeyframeBezierHandle(
self.delay,
0
)
keyframe.in_value = KeyframeBezierHandle(
1,
1
)
class EaseOut:
"""!
The value starts fast before decelerating towards the end
"""
def __init__(self, delay=1/3):
self.delay = delay
def __call__(self, keyframe):
keyframe.out_value = KeyframeBezierHandle(
0,
0
)
keyframe.in_value = KeyframeBezierHandle(
1-self.delay,
1
)
class Jump:
"""!
Jumps to the end value at the end of the keyframe
"""
def __call__(self, keyframe):
keyframe.jump = True
class Sigmoid:
"""!
Combines the effects of EaseIn and EaseOut
"""
def __init__(self, delay=1/3):
self.delay = delay
def __call__(self, keyframe):
keyframe.out_value = KeyframeBezierHandle(
self.delay,
0
)
keyframe.in_value = KeyframeBezierHandle(
1 - self.delay,
1
)
class Split:
"""
Uses different easing methods for in/out
"""
def __init__(self, out_ease, in_ease):
self.out_ease = out_ease
self.in_ease = in_ease
def __call__(self, keyframe):
self.out_ease(keyframe)
t = keyframe.out_value
self.in_ease(keyframe)
keyframe.out_value = t
+441
View File
@@ -0,0 +1,441 @@
from .base import LottieObject, LottieProp, PseudoBool
from .properties import Value, MultiDimensional, ColorValue
from .nvector import NVector
from .color import Color
#5: EffectsManager,
#11: MaskEffect,
class EffectValue(LottieObject):
"""!
Value for an effect
"""
## %Effect value type.
type = None
_classses = {}
_props = [
LottieProp("effect_index", "ix", int, False),
#LottieProp("match_name", "mn", str, False),
LottieProp("name", "nm", str, False),
LottieProp("type", "ty", int, False),
]
def __init__(self):
## Effect Index. Used for expressions.
self.effect_index = None
## After Effect's Name. Used for expressions.
self.name = None
"""
## After Effect's Match Name. Used for expressions.
self.match_name = ""
"""
@classmethod
def _load_get_class(cls, lottiedict):
if not EffectValue._classses:
EffectValue._classses = {
sc.type: sc
for sc in EffectValue.__subclasses__()
}
return EffectValue._classses[lottiedict["ty"]]
def __str__(self):
return self.name or super().__str__()
## @ingroup Lottie
class Effect(LottieObject):
"""!
Layer effect
"""
## %Effect type.
type = None
_classses = {}
_props = [
LottieProp("effect_index", "ix", int, False),
#LottieProp("match_name", "mn", str, False),
LottieProp("name", "nm", str, False),
LottieProp("type", "ty", int, False),
LottieProp("effects", "ef", EffectValue, True),
]
_effects = []
def __init__(self, *args, **kwargs):
## Effect Index. Used for expressions.
self.effect_index = None
## After Effect's Name. Used for expressions.
self.name = None
## Effect parameters
self.effects = self._load_values(*args, **kwargs)
"""
## After Effect's Match Name. Used for expressions.
self.match_name = ""
"""
@classmethod
def _load_get_class(cls, lottiedict):
if not Effect._classses:
Effect._classses = {
sc.type: sc
for sc in Effect.__subclasses__()
}
type = lottiedict["ty"]
if type in Effect._classses:
return Effect._classses[type]
else:
return Effect
def _load_values(self, *args, **kwargs):
values = []
for i, (name, type) in enumerate(self._effects):
val = []
if len(args) > i:
val = [args[i]]
if name in kwargs:
val = [kwargs[name]]
values.append(type(*val))
return values
def __getattr__(self, key):
for i, (name, type) in enumerate(self._effects):
if name == key:
return self.effects[i].value
return super().__getattr__(key)
def __str__(self):
return self.name or super().__str__()
## @ingroup Lottie
## @ingroup LottieCheck
class EffectNoValue(EffectValue):
_props = []
## @ingroup Lottie
class EffectValueSlider(EffectValue):
_props = [
LottieProp("value", "v", Value, False),
]
## %Effect type.
type = 0
def __init__(self, value=0):
EffectValue.__init__(self)
## Effect value.
self.value = Value(value)
## @ingroup Lottie
class EffectValueAngle(EffectValue):
_props = [
LottieProp("value", "v", Value, False),
]
## %Effect type.
type = 1
def __init__(self, angle=0):
EffectValue.__init__(self)
## Effect value.
self.value = Value(angle)
## @ingroup Lottie
class EffectValueColor(EffectValue):
_props = [
LottieProp("value", "v", ColorValue, False),
]
## %Effect type.
type = 2
def __init__(self, value=Color(0, 0, 0)):
EffectValue.__init__(self)
## Effect value.
self.value = ColorValue(value)
## @ingroup Lottie
class EffectValuePoint(EffectValue):
_props = [
LottieProp("value", "v", MultiDimensional, False),
]
## %Effect type.
type = 3
def __init__(self, value=NVector(0, 0)):
EffectValue.__init__(self)
## Effect value.
self.value = MultiDimensional(value)
## @ingroup Lottie
class EffectValueCheckbox(EffectValue):
_props = [
LottieProp("value", "v", Value, False),
]
## %Effect type.
type = 4
def __init__(self, value=0):
EffectValue.__init__(self)
## Effect value.
self.value = Value(value)
## @ingroup Lottie
## @ingroup LottieCheck
## Lottie-web ignores these
class IgnoredValue(EffectValue):
_props = [
LottieProp("value", "v", float, False),
]
## %Effect type.
type = 6
def __init__(self, value=0):
EffectValue.__init__(self)
## Effect value.
self.value = value
## @ingroup Lottie
## @ingroup LottieCheck
class EffectValueDropDown(EffectValue):
_props = [
LottieProp("value", "v", Value, False),
]
## %Effect type.
type = 7
def __init__(self, value=0):
EffectValue.__init__(self)
## Effect value.
self.value = Value(value)
## @ingroup Lottie
## @ingroup LottieCheck
class EffectValueLayer(EffectValue):
_props = [
LottieProp("value", "v", Value, False),
]
## %Effect type.
type = 10
def __init__(self):
EffectValue.__init__(self)
## Effect value.
self.value = Value()
## @ingroup Lottie
class FillEffect(Effect):
"""!
Replaces the whole layer with the given color
@note Opacity is in [0, 1]
"""
_effects = [
("00", EffectValuePoint),
("01", EffectValueDropDown),
("color", EffectValueColor),
("03", EffectValueDropDown),
("04", EffectValueSlider),
("05", EffectValueSlider),
("opacity", EffectValueSlider),
]
## %Effect type.
type = 21
## @ingroup Lottie
class StrokeEffect(Effect):
_effects = [
("00", EffectValueColor),
("01", EffectValueCheckbox),
("02", EffectValueCheckbox),
("color", EffectValueColor),
("04", EffectValueSlider),
("05", EffectValueSlider),
("06", EffectValueSlider),
("07", EffectValueSlider),
("08", EffectValueSlider),
("09", EffectValueDropDown),
("type", EffectValueDropDown),
]
## %Effect type.
type = 22
## @ingroup Lottie
class TritoneEffect(Effect):
"""!
Maps layers colors based on bright/mid/dark colors
"""
_effects = [
("bright", EffectValueColor),
("mid", EffectValueColor),
("dark", EffectValueColor),
]
## %Effect type.
type = 23
"""
## @ingroup Lottie
## @ingroup LottieCheck
class GroupEffect(Effect):
_props = [
LottieProp("enabled", "en", PseudoBool, False),
]
def __init__(self):
Effect.__init__(self)
## Enabled AE property value
self.enabled = True
"""
## @ingroup Lottie
## @ingroup LottieCheck
class ProLevelsEffect(Effect):
_effects = [
("00", EffectValueDropDown),
("01", EffectNoValue),
("02", EffectNoValue),
("comp_inblack", EffectValueSlider),
("comp_inwhite", EffectValueSlider),
("comp_gamma", EffectValueSlider),
("comp_outblack", EffectValueSlider),
("comp_outwhite", EffectNoValue),
("08", EffectNoValue),
("09", EffectValueSlider),
("r_inblack", EffectValueSlider),
("r_inwhite", EffectValueSlider),
("r_gamma", EffectValueSlider),
("r_outblack", EffectValueSlider),
("r_outwhite", EffectNoValue),
("15", EffectValueSlider),
("16", EffectValueSlider),
("g_inblack", EffectValueSlider),
("g_inwhite", EffectValueSlider),
("g_gamma", EffectValueSlider),
("g_outblack", EffectValueSlider),
("g_outwhite", EffectNoValue),
("22", EffectValueSlider),
("b3", EffectValueSlider),
("b_inblack", EffectValueSlider),
("b_inwhite", EffectValueSlider),
("b_gamma", EffectValueSlider),
("b_outblack", EffectValueSlider),
("b_outwhite", EffectNoValue),
("29", EffectValueSlider),
("a_inblack", EffectValueSlider),
("a_inwhite", EffectValueSlider),
("a_gamma", EffectValueSlider),
("a_outblack", EffectValueSlider),
("a_outwhite", EffectNoValue),
]
## %Effect type.
type = 24
## @ingroup Lottie
class TintEffect(Effect):
"""!
Colorizes the layer
@note Opacity is in [0, 100]
"""
_effects = [
("color_black", EffectValueColor),
("color_white", EffectValueColor),
("opacity", EffectValueSlider),
]
## %Effect type.
type = 20
## @ingroup Lottie
class DropShadowEffect(Effect):
"""!
Adds a shadow to the layer
@note Opacity is in [0, 255]
"""
_effects = [
("color", EffectValueColor),
("opacity", EffectValueSlider),
("angle", EffectValueAngle),
("distance", EffectValueSlider),
("blur", EffectValueSlider),
]
## %Effect type.
type = 25
## @ingroup Lottie
## @ingroup LottieCheck
class Matte3Effect(Effect):
_effects = [
("index", EffectValueSlider),
]
## %Effect type.
type = 28
## @ingroup Lottie
class GaussianBlurEffect(Effect):
"""!
Gaussian blur
"""
_effects = [
("sigma", EffectValueSlider),
("dimensions", EffectValueSlider),
("wrap", EffectValueCheckbox),
]
## %Effect type.
type = 29
#class ChangeColorEffect(Effect):
#"""!
#Gaussian blur
#"""
#_effects = [
#("view", EffectValueDropDown),
#("hue", EffectValueSlider),
#("lightness", EffectValueSlider),
#("saturation", EffectValueSlider),
#("color_to_change", EffectValueColor),
#("tolerance", EffectValueSlider),
#("softness", EffectValueSlider),
#("match", EffectValueDropDown),
#("invert_mask", EffectValueDropDown),
#]
### %Effect type.
#type = 29
## @ingroup Lottie
class ChangeToColorEffect(Effect):
"""!
Change to color
"""
_effects = [
("from_color", EffectValueColor),
("to_color", EffectValueColor),
("change", EffectValueDropDown),
("change_by", EffectValueDropDown),
("tolerance", IgnoredValue),
("hue", EffectValueSlider),
("lightness", EffectValueSlider),
("saturation", EffectValueSlider),
("saturation_", IgnoredValue),
("softness", EffectValueSlider),
("view_correction", EffectValueDropDown),
]
## %Effect type.
type = 5
+39
View File
@@ -0,0 +1,39 @@
from .base import LottieEnum
## @ingroup Lottie
class TestBased(LottieEnum):
Characters = 1
CharacterExcludingSpaces = 2
Words = 3
Lines = 4
@classmethod
def default(cls):
return cls.Characters
## @ingroup Lottie
class TextShape(LottieEnum):
Square = 1
RampUp = 2
RampDown = 3
Triangle = 4
Round = 5
Smooth = 6
@classmethod
def default(cls):
return cls.Square
## @ingroup Lottie
class TextGrouping(LottieEnum):
Characters = 1
Word = 2
Line = 3
All = 4
@classmethod
def default(cls):
return cls.Characters
+125
View File
@@ -0,0 +1,125 @@
import math
from .base import LottieObject, LottieProp, LottieEnum
from .properties import MultiDimensional, Value, NVector, ShapeProperty, PositionValue
## @ingroup Lottie
class Transform(LottieObject):
"""!
Layer transform
"""
_props = [
LottieProp("anchor_point", "a", MultiDimensional, False),
LottieProp("position", "p", PositionValue, False),
LottieProp("scale", "s", MultiDimensional, False),
LottieProp("rotation", "r", Value, False),
LottieProp("opacity", "o", Value, False),
#LottieProp("position_x", "px", Value, False),
#LottieProp("position_y", "py", Value, False),
#LottieProp("position_z", "pz", Value, False),
LottieProp("skew", "sk", Value, False),
LottieProp("skew_axis", "sa", Value, False),
]
def __init__(self):
## Transform Anchor Point
self.anchor_point = MultiDimensional(NVector(0, 0))
## Transform Position
self.position = PositionValue(NVector(0, 0))
## Transform Scale
self.scale = MultiDimensional(NVector(100, 100))
## Transform Rotation
self.rotation = Value(0)
## Transform Opacity
self.opacity = Value(100)
"""
# Transform Position X
#self.position_x = Value()
## Transform Position Y
#self.position_y = Value()
## Transform Position Z
#self.position_z = Value()
"""
## Transform Skew
self.skew = Value(0)
## Transform Skew Axis.
## An angle, if 0 skews on the X axis, if 90 skews on the Y axis
self.skew_axis = Value(0)
def to_matrix(self, time, auto_orient=False):
from ..utils.transform import TransformMatrix
mat = TransformMatrix()
anchor = self.anchor_point.get_value(time) if self.anchor_point else NVector(0, 0)
mat.translate(-anchor.x, -anchor.y)
scale = self.scale.get_value(time) if self.scale else NVector(100, 100)
mat.scale(scale.x / 100, scale.y / 100)
skew = (self.skew.get_value(time) * math.pi / 180) if self.skew else 0
if skew != 0:
axis = (self.skew_axis.get_value(time) * math.pi / 180) if self.skew_axis else 0
mat.skew_from_axis(-skew, axis)
rot = (self.rotation.get_value(time) * math.pi / 180) if self.rotation else 0
if rot:
mat.rotate(-rot)
if auto_orient:
if self.position and self.position.animated:
ao_angle = self.position.get_tangent_angle(time)
mat.rotate(-ao_angle)
pos = self.position.get_value(time) if self.position else NVector(0, 0)
mat.translate(pos.x, pos.y)
return mat
## @ingroup Lottie
class MaskMode(LottieEnum):
"""!
How masks interact with each other
@see https://helpx.adobe.com/after-effects/using/alpha-channels-masks-mattes.html
"""
No = "n"
Add = "a"
Subtract = "s"
Intersect = "i"
## @note Not in lottie web
Lightent = "l"
## @note Not in lottie web
Darken = "d"
## @note Not in lottie web
Difference = "f"
## @ingroup Lottie
## @todo Implement SVG/SIF I/O
class Mask(LottieObject):
_props = [
LottieProp("inverted", "inv", bool, False),
LottieProp("name", "nm", str, False),
LottieProp("shape", "pt", ShapeProperty, False),
LottieProp("opacity", "o", Value, False),
LottieProp("mode", "mode", MaskMode, False),
LottieProp("dilate", "x", Value, False),
]
def __init__(self, bezier=None):
## Inverted Mask flag
self.inverted = False
## Mask name. Used for expressions and effects.
self.name = None
## Mask vertices
self.shape = ShapeProperty(bezier)
## Mask opacity.
self.opacity = Value(100)
## Mask mode. Not all mask types are supported.
self.mode = MaskMode.Intersect
self.dilate = Value(0)
def __str__(self):
return self.name or super().__str__()
+294
View File
@@ -0,0 +1,294 @@
import warnings
from .base import LottieObject, LottieProp, PseudoBool, LottieEnum
from .effects import Effect
from .helpers import Transform, Mask
from .shapes import ShapeElement
from .text import TextAnimatorData
from .properties import Value
## @ingroup Lottie
class BlendMode(LottieEnum):
Normal = 0
Multiply = 1
Screen = 2
Overlay = 3
Darken = 4
Lighten = 5
ColorDodge = 6
ColorBurn = 7
HardLight = 8
SoftLight = 9
Difference = 10
Exclusion = 11
Hue = 12
Saturation = 13
Color = 14
Luminosity = 15
## @ingroup Lottie
## @todo SVG masks
class MatteMode(LottieEnum):
Normal = 0
Alpha = 1
InvertedAlpha = 2
Luma = 3
InvertedLuma = 4
## @ingroup Lottie
class Layer(LottieObject):
_props = [
LottieProp("threedimensional", "ddd", PseudoBool, False),
LottieProp("hidden", "hd", bool, False),
LottieProp("type", "ty", int, False),
LottieProp("name", "nm", str, False),
LottieProp("parent_index", "parent", int, False),
LottieProp("stretch", "sr", float, False),
LottieProp("transform", "ks", Transform, False),
LottieProp("auto_orient", "ao", PseudoBool, False),
LottieProp("in_point", "ip", float, False),
LottieProp("out_point", "op", float, False),
LottieProp("start_time", "st", float, False),
LottieProp("blend_mode", "bm", BlendMode, False),
LottieProp("matte_mode", "tt", MatteMode, False),
LottieProp("index", "ind", int, False),
#LottieProp("css_class", "cl", str, False),
LottieProp("layer_html_id", "ln", str, False),
LottieProp("has_masks", "hasMask", bool, False),
LottieProp("masks", "masksProperties", Mask, True),
LottieProp("effects", "ef", Effect, True),
LottieProp("matte_target", "td", int, False),
]
## %Layer type.
## @see https://github.com/bodymovin/bodymovin-extension/blob/master/bundle/jsx/enums/layerTypes.jsx
type = None
_classses = {}
@property
def has_masks(self):
"""!
Whether the layer has some masks applied
"""
return bool(self.masks) if getattr(self, "masks") is not None else None
def __init__(self):
## Transform properties
self.transform = Transform()
## Auto-Orient along path AE property.
self.auto_orient = False
## 3d layer flag
self.threedimensional = False
## Hidden layer
self.hidden = None
## Layer index in AE. Used for parenting and expressions.
self.index = None
"""
# Parsed layer name used as html class on SVG/HTML renderer
#self.css_class = ""
# Parsed layer name used as html id on SVG/HTML renderer
#self.layer_html_id = ""
"""
## In Point of layer. Sets the initial frame of the layer.
self.in_point = None
## Out Point of layer. Sets the final frame of the layer.
self.out_point = None
## Start Time of layer. Sets the start time of the layer.
self.start_time = 0
## After Effects Layer Name. Used for expressions.
self.name = None
## List of Effects
self.effects = None
## Layer Time Stretching
self.stretch = 1
## Layer Parent. Uses ind of parent.
self.parent_index = None
## List of Masks
self.masks = None
## Blend Mode
self.blend_mode = BlendMode.Normal
## Matte mode, the layer will inherit the transparency from the layer above
self.matte_mode = None
self.matte_target = None
## Composition owning the layer, set by add_layer
self.composition = None
def add_child(self, layer):
if not self.composition or self.index is None:
raise Exception("Must set composition / index first")
self._child_inout_auto(layer)
self.composition.add_layer(layer)
layer.parent_index = self.index
return layer
def _child_inout_auto(self, layer):
if layer.in_point is None:
layer.in_point = self.in_point
if layer.out_point is None:
layer.out_point = self.out_point
@property
def parent(self):
if self.parent_index is None:
return None
return self.composition.layer(self.parent_index)
@parent.setter
def parent(self, layer):
if layer is None:
self.parent_index = None
else:
self.parent_index = layer.index
layer._child_inout_auto(self)
@property
def children(self):
for layer in self.composition.layers:
if layer.parent_index == self.index:
yield layer
@classmethod
def _load_get_class(cls, lottiedict):
if not Layer._classses:
Layer._classses = {
sc.type: sc
for sc in Layer.__subclasses__()
}
type_id = lottiedict["ty"]
if type_id not in Layer._classses:
warnings.warn("Unknown layer type: %s" % type_id)
return Layer
return Layer._classses[type_id]
def __repr__(self):
return "<%s %s %s>" % (type(self).__name__, self.index, self.name)
def __str__(self):
return "%s %s" % (
self.name or super().__str__(),
self.index if self.index is not None else ""
)
def remove(self):
"""!
@brief Removes this layer from the componsitin
"""
self.composition.remove_layer(self)
## @ingroup Lottie
class NullLayer(Layer):
"""!
Layer with no data, useful to group layers together
"""
## %Layer type.
type = 3
def __init__(self):
Layer.__init__(self)
## @ingroup Lottie
class TextLayer(Layer):
_props = [
LottieProp("data", "t", TextAnimatorData, False),
]
## %Layer type.
type = 5
def __init__(self):
Layer.__init__(self)
## Text Data
self.data = TextAnimatorData()
## @ingroup Lottie
class ShapeLayer(Layer):
"""!
Layer containing ShapeElement objects
"""
_props = [
LottieProp("shapes", "shapes", ShapeElement, True),
]
## %Layer type.
type = 4
def __init__(self):
Layer.__init__(self)
## Shape list of items
self.shapes = [] # ShapeElement
def add_shape(self, shape):
self.shapes.append(shape)
return shape
def insert_shape(self, index, shape):
self.shapes.insert(index, shape)
return shape
## @ingroup Lottie
## @todo SIF I/O
class ImageLayer(Layer):
_props = [
LottieProp("image_id", "refId", str, False),
]
## %Layer type.
type = 2
def __init__(self, image_id=""):
Layer.__init__(self)
## id pointing to the source image defined on 'assets' object
self.image_id = image_id
## @ingroup Lottie
class PreCompLayer(Layer):
_props = [
LottieProp("reference_id", "refId", str, False),
LottieProp("time_remapping", "tm", Value, False),
LottieProp("width", "w", int, False),
LottieProp("height", "h", int, False),
]
## %Layer type.
type = 0
def __init__(self, reference_id=""):
Layer.__init__(self)
## id pointing to the source composition defined on 'assets' object
self.reference_id = reference_id
## Comp's Time remapping
self.time_remapping = None
## Width
self.width = 512
## Height
self.height = 512
## @ingroup Lottie
class SolidColorLayer(Layer):
"""!
Layer with a solid color rectangle
"""
_props = [
LottieProp("color", "sc", str, False),
LottieProp("height", "sh", float, False),
LottieProp("width", "sw", float, False),
]
## %Layer type.
type = 1
def __init__(self, color="", width=512, height=512):
Layer.__init__(self)
## Color of the layer as a @c \#rrggbb hex
# @todo Convert NVector to string
self.color = color
## Height of the layer.
self.height = height
## Width of the layer.
self.width = width
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+148
View File
@@ -0,0 +1,148 @@
import operator
import math
def vop(op, a, b):
return list(map(op, a, b))
class NVector():
def __init__(self, *components):
self.components = list(components)
def __str__(self):
return str(self.components)
def __repr__(self):
return "<NVector %s>" % self
def __len__(self):
return len(self.components)
def to_list(self):
return list(self.components)
def __add__(self, other):
return type(self)(*vop(operator.add, self.components, other.components))
def __sub__(self, other):
return type(self)(*vop(operator.sub, self.components, other.components))
def __mul__(self, scalar):
if isinstance(scalar, NVector):
return type(self)(*vop(operator.mul, self.components, scalar.components))
return type(self)(*(c * scalar for c in self.components))
def __truediv__(self, scalar):
return type(self)(*(c / scalar for c in self.components))
def __iadd__(self, other):
self.components = vop(operator.add, self.components, other.components)
return self
def __isub__(self, other):
self.components = vop(operator.sub, self.components, other.components)
return self
def __imul__(self, scalar):
if isinstance(scalar, NVector):
self.components = vop(operator.mul, self.components, scalar.components)
else:
self.components = [c * scalar for c in self.components]
return self
def __itruediv__(self, scalar):
self.components = [c / scalar for c in self.components]
return self
def __neg__(self):
return type(self)(*(-c for c in self.components))
def __getitem__(self, key):
if isinstance(key, slice):
return NVector(*self.components[key])
return self.components[key]
def __setitem__(self, key, value):
self.components[key] = value
def __eq__(self, other):
return self.components == other.components
def __abs__(self):
return type(self)(*(abs(c) for c in self.components))
@property
def length(self):
return math.sqrt(sum(map(lambda x: x**2, self.components)))
def dot(self, other):
return sum(map(operator.mul, self.components, other.components))
def clone(self):
return NVector(*self.components)
def lerp(self, other, t):
return self * (1-t) + other * t
@property
def x(self):
return self.components[0]
@x.setter
def x(self, v):
self.components[0] = v
@property
def y(self):
return self.components[1]
@y.setter
def y(self, v):
self.components[1] = v
@property
def z(self):
return self.components[2]
@z.setter
def z(self, v):
self.components[2] = v
def element_scaled(self, other):
return type(self)(*vop(operator.mul, self.components, other.components))
def cross(self, other):
"""
@pre len(self) == len(other) == 3
"""
a = self
b = other
return type(self)(
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
)
@property
def polar_angle(self):
"""
@pre len(self) == 2
"""
return math.atan2(self.y, self.x)
def Point(x, y):
return NVector(x, y)
def Size(x, y):
return NVector(x, y)
def Point3D(x, y, z):
return NVector(x, y, z)
def PolarVector(length, theta):
return NVector(length * math.cos(theta), length * math.sin(theta))
+686
View File
@@ -0,0 +1,686 @@
import math
from functools import reduce
from .base import LottieObject, LottieProp, PseudoList, PseudoBool
from .easing import KeyframeBezierHandle, Linear
from .nvector import NVector
from .bezier import Bezier
from .color import Color
class KeyframeBezier:
NEWTON_ITERATIONS = 4
NEWTON_MIN_SLOPE = 0.001
SUBDIVISION_PRECISION = 0.0000001
SUBDIVISION_MAX_ITERATIONS = 10
SPLINE_TABLE_SIZE = 11
SAMPLE_STEP_SIZE = 1.0 / (SPLINE_TABLE_SIZE - 1.0)
def __init__(self, h1, h2):
self.h1 = h1
self.h2 = h2
self._sample_values = None
@classmethod
def from_keyframe(cls, keyframe):
return cls(keyframe.out_value, keyframe.in_value)
def bezier(self):
bez = Bezier()
bez.add_point(NVector(0, 0), outp=NVector(self.h1.x, self.h1.y))
bez.add_point(NVector(1, 1), inp=NVector(self.h2.x-1, self.h2.y-1))
return bez
def _a(self, c1, c2):
return 1 - 3 * c2 + 3 * c1
def _b(self, c1, c2):
return 3 * c2 - 6 * c1
def _c(self, c1):
return 3 * c1
def _bezier_component(self, t, c1, c2):
return ((self._a(c1, c2) * t + self._b(c1, c2)) * t + self._c(c1)) * t
def point_at(self, t):
return NVector(
self._bezier_component(t, self.h1.x, self.h2.x),
self._bezier_component(t, self.h1.y, self.h2.y)
)
def _slope_component(self, t, c1, c2):
return 3 * self._a(c1, c2) * t * t + 2 * self._b(c1, c2) * t + self._c(c1)
def slope_at(self, t):
return NVector(
self._slope_component(t, self.h1.x, self.h2.x),
self._slope_component(t, self.h1.y, self.h2.y)
)
def _binary_subdivide(self, x, interval_start, interval_end):
current_x = None
t = None
i = 0
for i in range(self.SUBDIVISION_MAX_ITERATIONS):
if current_x is not None and abs(current_x) < self.SUBDIVISION_PRECISION:
break
t = interval_start + (interval_end - interval_start) / 2.0
current_x = self._bezier_component(t, self.h1.x, self.h2.x) - x
if current_x > 0.0:
interval_end = t
else:
interval_start = t
return t
def _newton_raphson(self, x, t_guess):
for i in range(self.NEWTON_ITERATIONS):
slope = self._slope_component(t_guess, self.h1.x, self.h2.x)
if slope == 0:
return t_guess
current_x = self._bezier_component(t_guess, self.h1.x, self.h2.x) - x
t_guess -= current_x / slope
return t_guess
def _get_sample_values(self):
if self._sample_values is None:
self._sample_values = [
self._bezier_component(i * self.SAMPLE_STEP_SIZE, self.h1.x, self.h2.x)
for i in range(self.SPLINE_TABLE_SIZE)
]
return self._sample_values
def t_for_x(self, x):
sample_values = self._get_sample_values()
interval_start = 0
current_sample = 1
last_sample = self.SPLINE_TABLE_SIZE - 1
while current_sample != last_sample and sample_values[current_sample] <= x:
interval_start += self.SAMPLE_STEP_SIZE
current_sample += 1
current_sample -= 1
dist = (x - sample_values[current_sample]) / (sample_values[current_sample+1] - sample_values[current_sample])
t_guess = interval_start + dist * self.SAMPLE_STEP_SIZE
initial_slope = self._slope_component(t_guess, self.h1.x, self.h2.x)
if initial_slope >= self.NEWTON_MIN_SLOPE:
return self._newton_raphson(x, t_guess)
if initial_slope == 0:
return t_guess
return self._binary_subdivide(x, interval_start, interval_start + self.SAMPLE_STEP_SIZE)
def y_at_x(self, x):
t = self.t_for_x(x)
return self._bezier_component(t, self.h1.y, self.h2.y)
## @ingroup Lottie
class Keyframe(LottieObject):
_props = [
LottieProp("time", "t", float, False),
LottieProp("in_value", "i", KeyframeBezierHandle, False),
LottieProp("out_value", "o", KeyframeBezierHandle, False),
LottieProp("jump", "h", PseudoBool),
]
def __init__(self, time=0, easing_function=None):
"""!
@param time Start time of keyframe segment
@param easing_function Callable that performs the easing
"""
## Start time of keyframe segment.
self.time = time
## Bezier curve easing in value.
self.in_value = None
## Bezier curve easing out value.
self.out_value = None
## Jump to the end value
self.jump = None
if easing_function:
easing_function(self)
def bezier(self):
if self.jump:
bez = Bezier()
bez.add_point(NVector(0, 0))
bez.add_point(NVector(1, 0))
bez.add_point(NVector(1, 1))
return bez
else:
return KeyframeBezier.from_keyframe(self).bezier()
def lerp_factor(self, ratio):
return KeyframeBezier.from_keyframe(self).y_at_x(ratio)
def __str__(self):
return "%s %s" % (self.time, self.start)
## @ingroup Lottie
class OffsetKeyframe(Keyframe):
"""!
Keyframe for MultiDimensional values
@par Bezier easing
@parblock
Imagine a quadratic bezier, with starting point at (0, 0) and end point at (1, 1).
@p out_value and @p in_value are the other two handles for a quadratic bezier,
expressed as absoulte values in this 0-1 space.
See also https://cubic-bezier.com/
@endparblock
"""
_props = [
LottieProp("start", "s", NVector, False),
LottieProp("end", "e", NVector, False),
LottieProp("in_tan", "ti", NVector, False),
LottieProp("out_tan", "to", NVector, False),
]
def __init__(self, time=0, start=None, end=None, easing_function=None, in_tan=None, out_tan=None):
Keyframe.__init__(self, time, easing_function)
## Start value of keyframe segment.
self.start = start
## End value of keyframe segment.
self.end = end
## In Spatial Tangent. Only for spatial properties. (for bezier smoothing on position)
self.in_tan = in_tan
## Out Spatial Tangent. Only for spatial properties. (for bezier smoothing on position)
self.out_tan = out_tan
def interpolated_value(self, ratio, next_start=None):
end = next_start if self.end is None else self.end
if end is None:
return self.start
if not self.in_value or not self.out_value:
return self.start
if ratio == 1:
return end
if ratio == 0:
return self.start
if self.in_tan and self.out_tan:
bezier = Bezier()
bezier.add_point(self.start, NVector(0, 0), self.out_tan)
bezier.add_point(end, self.in_tan, NVector(0, 0))
return bezier.point_at(ratio)
lerpv = self.lerp_factor(ratio)
return self.start.lerp(end, lerpv)
def interpolated_tangent_angle(self, ratio, next_start=None):
end = next_start if self.end is None else self.end
if end is None or not self.in_tan or not self.out_tan:
return 0
bezier = Bezier()
bezier.add_point(self.start, NVector(0, 0), self.out_tan)
bezier.add_point(end, self.in_tan, NVector(0, 0))
return bezier.tangent_angle_at(ratio)
def __repr__(self):
return "<%s.%s %s %s%s>" % (
type(self).__module__,
type(self).__name__,
self.time,
self.start,
(" -> %s" % self.end) if self.end is not None else ""
)
class AnimatableMixin:
keyframe_type = Keyframe
def __init__(self, value=None):
## Non-animated value
self.value = value
## Property index
self.property_index = None
## Whether it's animated
self.animated = False
## Keyframe list
self.keyframes = None
def clear_animation(self, value):
"""!
Sets a fixed value, removing animated keyframes
"""
self.value = value
self.animated = False
self.keyframes = None
def add_keyframe(self, time, value, interp=Linear(), *args, **kwargs):
"""!
@param time The time this keyframe appears in
@param value The value the property should have at @p time
@param interp The easing callable used to update the tangents of the previous keyframe
@param args Extra arguments to pass the keyframe constructor
@param kwargs Extra arguments to pass the keyframe constructor
@note Always call add_keyframe with increasing @p time value
"""
if not self.animated:
self.value = None
self.keyframes = []
self.animated = True
else:
if self.keyframes[-1].time == time:
if value != self.keyframes[-1].start:
self.keyframes[-1].start = value
return
else:
self.keyframes[-1].end = value.clone()
self.keyframes.append(self.keyframe_type(
time,
value,
None,
interp,
*args,
**kwargs
))
def get_value(self, time=0):
"""!
@brief Returns the value of the property at the given frame/time
"""
if not self.animated:
return self.value
if not self.keyframes:
return None
return self._get_value_helper(time)[0]
def _get_value_helper(self, time):
val = self.keyframes[0].start
for i in range(len(self.keyframes)):
k = self.keyframes[i]
if time - k.time <= 0:
if k.start is not None:
val = k.start
kp = self.keyframes[i-1] if i > 0 else None
if kp:
t = (time - kp.time) / (k.time - kp.time)
end = kp.end
if end is None:
end = val
if end is not None:
val = kp.interpolated_value(t, end)
return val, end, kp, t
return val, None, None, None
if k.end is not None:
val = k.end
return val, None, None, None
def to_dict(self):
d = super().to_dict()
if self.animated:
if "k" not in d:
return d
last = d["k"][-1]
last.pop("i", None)
last.pop("o", None)
return d
def __repr__(self):
if self.keyframes and len(self.keyframes) > 1:
val = "%s -> %s" % (self.keyframes[0].start, self.keyframes[-2].end)
else:
val = self.value
return "<%s.%s %s>" % (type(self).__module__, type(self).__name__, val)
def __str__(self):
if self.animated:
return "animated"
return str(self.value)
@classmethod
def merge_keyframes(cls, items, conversion):
"""
@todo Remove similar functionality from SVG/sif parsers
"""
keyframes = []
for animatable in items:
if animatable.animated:
keyframes.extend(animatable.keyframes)
# TODO properly interpolate tangents
new_kframes = []
for keyframe in sorted(keyframes, key=lambda kf: kf.time):
if new_kframes and new_kframes[-1].time == keyframe.time:
continue
kfcopy = keyframe.clone()
kfcopy.start = conversion(*(i.get_value(keyframe.time) for i in items))
new_kframes.append(kfcopy)
for i in range(0, len(new_kframes) - 1):
new_kframes[i].end = new_kframes[i+1].start
return new_kframes
@classmethod
def load(cls, lottiedict):
obj = super().load(lottiedict)
if "a" not in lottiedict:
obj.animated = prop_animated(lottiedict)
return obj
def prop_animated(l):
if "a" in l:
return l["a"]
if "k" not in l:
return False
if isinstance(l["k"], list) and l["k"] and isinstance(l["k"][0], dict):
return True
return False
def prop_not_animated(l):
return not prop_animated(l)
## @ingroup Lottie
class MultiDimensional(AnimatableMixin, LottieObject):
"""!
An animatable property that holds a NVector
"""
keyframe_type = OffsetKeyframe
_props = [
LottieProp("value", "k", NVector, False, prop_not_animated),
LottieProp("property_index", "ix", int, False),
LottieProp("animated", "a", PseudoBool, False),
LottieProp("keyframes", "k", OffsetKeyframe, True, prop_animated),
]
def get_tangent_angle(self, time=0):
"""!
@brief Returns the value tangent angle of the property at the given frame/time
"""
if not self.keyframes or len(self.keyframes) < 2:
return 0
val, end, kp, t = self._get_value_helper(time)
if kp:
return kp.interpolated_tangent_angle(t, end)
if self.keyframes[0].time >= time:
end = self.keyframes[0].end if self.keyframes[0].end is not None else self.keyframes[1].start
return self.keyframes[0].interpolated_tangent_angle(0, end)
return 0
class PositionValue(MultiDimensional):
_props = [
LottieProp("value", "k", NVector, False, prop_not_animated),
LottieProp("property_index", "ix", int, False),
LottieProp("animated", "a", PseudoBool, False),
LottieProp("keyframes", "k", OffsetKeyframe, True, prop_animated),
]
@classmethod
def load(cls, lottiedict):
obj = super().load(lottiedict)
if lottiedict.get("s", False):
cls._load_split(lottiedict, obj)
return obj
@classmethod
def _load_split(cls, lottiedict, obj):
components = [
Value.load(lottiedict.get("x", {})),
Value.load(lottiedict.get("y", {})),
]
if "z" in lottiedict:
components.append(Value.load(lottiedict.get("z", {})))
has_anim = any(x for x in components if x.animated)
if not has_anim:
obj.value = NVector(*(a.value for a in components))
obj.animated = False
obj.keyframes = None
return
obj.animated = True
obj.value = None
obj.keyframes = cls.merge_keyframes(components, NVector)
class ColorValue(AnimatableMixin, LottieObject):
"""!
An animatable property that holds a Color
"""
keyframe_type = OffsetKeyframe
_props = [
LottieProp("value", "k", Color, False, prop_not_animated),
LottieProp("property_index", "ix", int, False),
LottieProp("animated", "a", PseudoBool, False),
LottieProp("keyframes", "k", OffsetKeyframe, True, prop_animated),
]
## @ingroup Lottie
class GradientColors(LottieObject):
"""!
Represents colors and offsets in a gradient
Colors are represented as a flat list interleaving offsets and color components in weird ways
There are two possible layouts:
Without alpha, the colors are a sequence of offset, r, g, b
With alpha, same as above but at the end of the list there is a sequence of offset, alpha
Examples:
For the gradient [0, red], [0.5, yellow], [1, green]
The list would be [0, 1, 0, 0, 0.5, 1, 1, 0, 1, 0, 1, 0]
For the gradient [0, red at 80% opacity], [0.5, yellow at 70% opacity], [1, green at 60% opacity]
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]
"""
_props = [
LottieProp("colors", "k", MultiDimensional),
LottieProp("count", "p", int),
]
def __init__(self, stops=[]):
## Animatable colors, as a vector containing [offset, r, g, b] values as a flat array
self.colors = MultiDimensional(NVector())
## Number of colors
self.count = 0
if stops:
self.set_stops(stops)
@staticmethod
def color_to_stops(self, colors):
"""
Converts a list of colors (Color) to tuples (offset, color)
"""
return [
(i / (len(colors)-1), color)
for i, color in enumerate(colors)
]
def set_stops(self, stops, keyframe=None):
"""!
@param stops iterable of (offset, Color) tuples
@param keyframe keyframe index (or None if not animated)
"""
flat = self._flatten_stops(stops)
if self.colors.animated and keyframe is not None:
if keyframe > 1:
self.colors.keyframes[keyframe-1].end = flat
self.colors.keyframes[keyframe].start = flat
else:
self.colors.clear_animation(flat)
self.count = len(stops)
def _flatten_stops(self, stops):
flattened_colors = NVector(*reduce(
lambda a, b: a + b,
(
[off] + color.components[:3]
for off, color in stops
)
))
if any(len(c) > 3 for o, c in stops):
flattened_colors.components += reduce(
lambda a, b: a + b,
(
[off] + [self._get_alpha(color)]
for off, color in stops
)
)
return flattened_colors
def _get_alpha(self, color):
if len(color) > 3:
return color[3]
return 1
def _add_to_flattened(self, offset, color, flattened):
flat = [offset] + list(color[:3])
rgb_size = 4 * self.count
if len(flattened) == rgb_size:
# No alpha
flattened.extend(flat)
if self.count == 0 and len(color) > 3:
flattened.append(offset)
flattened.append(color[3])
else:
flattened[rgb_size:rgb_size] = flat
flattened.append(offset)
flattened.append(self._get_alpha(color))
def add_color(self, offset, color, keyframe=None):
if self.colors.animated:
if keyframe is None:
for kf in self.colors.keyframes:
if kf.start:
self._add_to_flattened(offset, color, kf.start.components)
if kf.end:
self._add_to_flattened(offset, color, kf.end.components)
else:
if keyframe > 1:
self._add_to_flattened(offset, color, self.colors.keyframes[keyframe-1].end.components)
self._add_to_flattened(offset, color, self.colors.keyframes[keyframe].start.components)
else:
self._add_to_flattened(offset, color, self.colors.value.components)
self.count += 1
def add_keyframe(self, time, stops, ease=Linear()):
"""!
@param time Frame time
@param stops Iterable of (offset, Color) tuples
@param ease Easing function
"""
self.colors.add_keyframe(time, self._flatten_stops(stops), ease)
def get_stops(self, keyframe=None):
if keyframe is not None:
colors = self.colors.keyframes[keyframe].start
else:
colors = self.colors.value
return self._stops_from_flat(colors)
def _stops_from_flat(self, colors):
if len(colors) == 4 * self.count:
for i in range(self.count):
off = i * 4
yield colors[off], Color(*colors[off+1:off+4])
else:
for i in range(self.count):
off = i * 4
aoff = self.count * 4 + i * 2 + 1
yield colors[off], Color(colors[off+1], colors[off+2], colors[off+3], colors[aoff])
def stops_at(self, time):
return self._stops_from_flat(self.colors.get_value(time))
## @ingroup Lottie
class Value(AnimatableMixin, LottieObject):
"""!
An animatable property that holds a float
"""
keyframe_type = OffsetKeyframe
_props = [
LottieProp("value", "k", float, False, prop_not_animated),
LottieProp("property_index", "ix", int, False),
LottieProp("animated", "a", PseudoBool, False),
LottieProp("keyframes", "k", keyframe_type, True, prop_animated),
]
def __init__(self, value=0):
super().__init__(value)
def add_keyframe(self, time, value, ease=Linear()):
super().add_keyframe(time, NVector(value), ease)
def get_value(self, time=0):
v = super().get_value(time)
if self.animated and self.keyframes:
return v[0]
return v
## @ingroup Lottie
class ShapePropKeyframe(Keyframe):
"""!
Keyframe holding Bezier objects
"""
_props = [
LottieProp("start", "s", Bezier, PseudoList),
LottieProp("end", "e", Bezier, PseudoList),
]
def __init__(self, time=0, start=None, end=None, easing_function=None):
Keyframe.__init__(self, time, easing_function)
## Start value of keyframe segment.
self.start = start
## End value of keyframe segment.
self.end = end
def interpolated_value(self, ratio, next_start=None):
end = next_start if self.end is None else self.end
if end is None:
return self.start
if not self.in_value or not self.out_value:
return self.start
if ratio == 1:
return end
if ratio == 0 or len(self.start.vertices) != len(end.vertices):
return self.start
lerpv = self.lerp_factor(ratio)
bez = Bezier()
bez.closed = self.start.closed
for i in range(len(self.start.vertices)):
bez.vertices.append(self.start.vertices[i].lerp(end.vertices[i], lerpv))
bez.in_tangents.append(self.start.in_tangents[i].lerp(end.in_tangents[i], lerpv))
bez.out_tangents.append(self.start.out_tangents[i].lerp(end.out_tangents[i], lerpv))
return bez
## @ingroup Lottie
class ShapeProperty(AnimatableMixin, LottieObject):
"""!
An animatable property that holds a Bezier
"""
keyframe_type = ShapePropKeyframe
_props = [
LottieProp("value", "k", Bezier, False, prop_not_animated),
#LottieProp("expression", "x", str, False),
LottieProp("property_index", "ix", float, False),
LottieProp("animated", "a", PseudoBool, False),
LottieProp("keyframes", "k", keyframe_type, True, prop_animated),
]
def __init__(self, bezier=None):
super().__init__(bezier or Bezier())
+847
View File
@@ -0,0 +1,847 @@
import math
from .base import LottieObject, LottieProp, LottieEnum, NVector
from .properties import Value, MultiDimensional, GradientColors, ShapeProperty, Bezier, ColorValue
from .color import Color
from .helpers import Transform
class BoundingBox:
"""!
Shape bounding box
"""
def __init__(self, x1=None, y1=None, x2=None, y2=None):
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
def include(self, x, y):
"""!
Expands the box to include the point at x, y
"""
if x is not None:
if self.x1 is None or self.x1 > x:
self.x1 = x
if self.x2 is None or self.x2 < x:
self.x2 = x
if y is not None:
if self.y1 is None or self.y1 > y:
self.y1 = y
if self.y2 is None or self.y2 < y:
self.y2 = y
def expand(self, other):
"""!
Expands the bounding box to include another bounding box
"""
self.include(other.x1, other.y1)
self.include(other.x2, other.y2)
def center(self):
"""!
Center point of the bounding box
"""
return NVector((self.x1 + self.x2) / 2, (self.y1 + self.y2) / 2)
def isnull(self):
"""!
Whether the box is default-initialized
"""
return self.x1 is None or self.y2 is None
def __repr__(self):
return "<BoundingBox [%s, %s] - [%s, %s]>" % (self.x1, self.y1, self.x2, self.y2)
@property
def width(self):
if self.isnull():
return 0
return self.x2 - self.x1
@property
def height(self):
if self.isnull():
return 0
return self.y2 - self.y1
def size(self):
return NVector(self.width, self.height)
## @ingroup Lottie
class ShapeElement(LottieObject):
"""!
Base class for all elements of ShapeLayer and Group
"""
_props = [
#LottieProp("match_name", "mn", str, False),
LottieProp("hidden", "hd", bool, False),
LottieProp("name", "nm", str, False),
LottieProp("type", "ty", str, False),
LottieProp("property_index", "cix", int, False),
LottieProp("bm", "bm", int, False),
]
## %Shape type.
type = None
_shape_classses = None
def __init__(self):
# After Effect's Match Name. Used for expressions.
#self.match_name = ""
## After Effect's Name. Used for expressions.
self.name = None
## Property index
self.property_index = None
## Hide element
self.hidden = None
## @todo figure out?
self.bm = None
def bounding_box(self, time=0):
"""!
Bounding box of the shape element at the given time
"""
return BoundingBox()
@classmethod
def _load_get_class(cls, lottiedict):
if not ShapeElement._shape_classses:
ShapeElement._shape_classses = {}
ShapeElement._load_sub(ShapeElement._shape_classses)
return ShapeElement._shape_classses[lottiedict["ty"]]
@classmethod
def _load_sub(cls, dict):
for sc in cls.__subclasses__():
if sc.type:
dict[sc.type] = sc
sc._load_sub(dict)
def __str__(self):
return self.name or super().__str__()
## @ingroup Lottie
class Shape(ShapeElement):
"""!
Drawable shape
"""
_props = [
LottieProp("direction", "d", float, False),
]
def __init__(self):
ShapeElement.__init__(self)
## After Effect's Direction. Direction how the shape is drawn. Used for trim path for example.
self.direction = 1
def to_bezier(self):
"""!
Returns a Path corresponding to this Shape
"""
raise NotImplementedError()
## @ingroup Lottie
class Rect(Shape):
"""!
A simple rectangle shape
"""
_props = [
LottieProp("position", "p", MultiDimensional, False),
LottieProp("size", "s", MultiDimensional, False),
LottieProp("rounded", "r", Value, False),
]
## %Shape type.
type = "rc"
def __init__(self, pos=None, size=None, rounded=0):
Shape.__init__(self)
## Rect's position
self.position = MultiDimensional(pos or NVector(0, 0))
## Rect's size
self.size = MultiDimensional(size or NVector(0, 0))
## Rect's rounded corners
self.rounded = Value(rounded)
def bounding_box(self, time=0):
pos = self.position.get_value(time)
sz = self.size.get_value(time)
return BoundingBox(
pos[0] - sz[0]/2,
pos[1] - sz[1]/2,
pos[0] + sz[0]/2,
pos[1] + sz[1]/2,
)
def to_bezier(self):
"""!
Returns a Shape corresponding to this rect
"""
shape = Path()
kft = set()
if self.position.animated:
kft |= set(kf.time for kf in self.position.keyframes)
if self.size.animated:
kft |= set(kf.time for kf in self.size.keyframes)
if self.rounded.animated:
kft |= set(kf.time for kf in self.rounded.keyframes)
if not kft:
shape.shape.value = self._bezier_t(0)
else:
for time in sorted(kft):
shape.shape.add_keyframe(time, self._bezier_t(time))
return shape
def _bezier_t(self, time):
bezier = Bezier()
bb = self.bounding_box(time)
rounded = self.rounded.get_value(time)
tl = NVector(bb.x1, bb.y1)
tr = NVector(bb.x2, bb.y1)
br = NVector(bb.x2, bb.y2)
bl = NVector(bb.x1, bb.y2)
if not self.rounded.animated and rounded == 0:
bezier.add_point(tl)
bezier.add_point(tr)
bezier.add_point(br)
bezier.add_point(bl)
else:
hh = NVector(rounded/2, 0)
vh = NVector(0, rounded/2)
hd = NVector(rounded, 0)
vd = NVector(0, rounded)
bezier.add_point(tl+vd, outp=-vh)
bezier.add_point(tl+hd, -hh)
bezier.add_point(tr-hd, outp=hh)
bezier.add_point(tr+vd, -vh)
bezier.add_point(br-vd, outp=vh)
bezier.add_point(br-hd, hh)
bezier.add_point(bl+hd, outp=-hh)
bezier.add_point(bl-vd, vh)
bezier.close()
return bezier
## @ingroup Lottie
class StarType(LottieEnum):
Star = 1
Polygon = 2
## @ingroup Lottie
class Star(Shape):
"""!
Star shape
"""
_props = [
LottieProp("position", "p", MultiDimensional, False),
LottieProp("inner_radius", "ir", Value, False),
LottieProp("inner_roundness", "is", Value, False),
LottieProp("outer_radius", "or", Value, False),
LottieProp("outer_roundness", "os", Value, False),
LottieProp("rotation", "r", Value, False),
LottieProp("points", "pt", Value, False),
LottieProp("star_type", "sy", StarType, False),
]
## %Shape type.
type = "sr"
def __init__(self):
Shape.__init__(self)
## Star's position
self.position = MultiDimensional(NVector(0, 0))
## Star's inner radius. (Star only)
self.inner_radius = Value()
## Star's inner roundness. (Star only)
self.inner_roundness = Value()
## Star's outer radius.
self.outer_radius = Value()
## Star's outer roundness.
self.outer_roundness = Value()
## Star's rotation.
self.rotation = Value()
## Star's number of points.
self.points = Value(5)
## Star's type. Polygon or Star.
self.star_type = StarType.Star
def bounding_box(self, time=0):
pos = self.position.get_value(time)
r = self.outer_radius.get_value(time)
return BoundingBox(
pos[0] - r,
pos[1] - r,
pos[0] + r,
pos[1] + r,
)
def to_bezier(self):
"""!
Returns a Shape corresponding to this star
"""
shape = Path()
kft = set()
if self.position.animated:
kft |= set(kf.time for kf in self.position.keyframes)
if self.inner_radius.animated:
kft |= set(kf.time for kf in self.inner_radius.keyframes)
if self.inner_roundness.animated:
kft |= set(kf.time for kf in self.inner_roundness.keyframes)
if self.points.animated:
kft |= set(kf.time for kf in self.points.keyframes)
if self.rotation.animated:
kft |= set(kf.time for kf in self.rotation.keyframes)
# TODO inner_roundness / outer_roundness
if not kft:
shape.shape.value = self._bezier_t(0)
else:
for time in sorted(kft):
shape.shape.add_keyframe(time, self._bezier_t(time))
return shape
def _bezier_t(self, time):
bezier = Bezier()
pos = self.position.get_value(time)
r1 = self.inner_radius.get_value(time)
r2 = self.outer_radius.get_value(time)
rot = -(self.rotation.get_value(time)) * math.pi / 180 + math.pi
p = self.points.get_value(time)
halfd = -math.pi / p
for i in range(int(p)):
main_angle = rot + i * halfd * 2
dx = r2 * math.sin(main_angle)
dy = r2 * math.cos(main_angle)
bezier.add_point(NVector(pos.x + dx, pos.y + dy))
if self.star_type == StarType.Star:
dx = r1 * math.sin(main_angle+halfd)
dy = r1 * math.cos(main_angle+halfd)
bezier.add_point(NVector(pos.x + dx, pos.y + dy))
bezier.close()
return bezier
## @ingroup Lottie
class Ellipse(Shape):
"""!
Ellipse shape
"""
_props = [
LottieProp("position", "p", MultiDimensional, False),
LottieProp("size", "s", MultiDimensional, False),
]
## %Shape type.
type = "el"
def __init__(self, position=None, size=None):
Shape.__init__(self)
## Ellipse's position
self.position = MultiDimensional(position or NVector(0, 0))
## Ellipse's size
self.size = MultiDimensional(size or NVector(0, 0))
def bounding_box(self, time=0):
pos = self.position.get_value(time)
sz = self.size.get_value(time)
return BoundingBox(
pos[0] - sz[0]/2,
pos[1] - sz[1]/2,
pos[0] + sz[0]/2,
pos[1] + sz[1]/2,
)
def to_bezier(self):
"""!
Returns a Shape corresponding to this ellipse
"""
shape = Path()
kft = set()
if self.position.animated:
kft |= set(kf.time for kf in self.position.keyframes)
if self.size.animated:
kft |= set(kf.time for kf in self.size.keyframes)
if not kft:
shape.shape.value = self._bezier_t(0)
else:
for time in sorted(kft):
shape.shape.add_keyframe(time, self._bezier_t(time))
return shape
def _bezier_t(self, time):
from ..utils.ellipse import Ellipse as EllipseConverter
bezier = Bezier()
position = self.position.get_value(time)
radii = self.size.get_value(time) / 2
el = EllipseConverter(position, radii, 0)
points = el.to_bezier(0, math.pi*2)
for point in points[1:]:
bezier.add_point(point.vertex, point.in_tangent, point.out_tangent)
bezier.close()
return bezier
## @ingroup Lottie
class Path(Shape):
"""!
Animatable Bezier curve
"""
_props = [
LottieProp("shape", "ks", ShapeProperty, False),
LottieProp("index", "ind", int, False),
]
## %Shape type.
type = "sh"
def __init__(self, bezier=None):
Shape.__init__(self)
## Shape's vertices
self.shape = ShapeProperty(bezier or Bezier())
## @todo Index?
self.index = None
def bounding_box(self, time=0):
pos = self.shape.get_value(time)
bb = BoundingBox()
for v in pos.vertices:
bb.include(*v)
return bb
def to_bezier(self):
return self.clone()
## @ingroup Lottie
class Group(ShapeElement):
"""!
ShapeElement that can contain other shapes
@note Shapes inside the same group will create "holes" in other shapes
"""
_props = [
LottieProp("number_of_properties", "np", float, False),
LottieProp("shapes", "it", ShapeElement, True),
]
## %Shape type.
type = "gr"
def __init__(self):
ShapeElement.__init__(self)
## Group number of properties. Used for expressions.
self.number_of_properties = None
## Group list of items
self.shapes = [TransformShape()]
@property
def transform(self):
return self.shapes[-1]
def bounding_box(self, time=0):
bb = BoundingBox()
for v in self.shapes:
bb.expand(v.bounding_box(time))
if not bb.isnull():
mat = self.transform.to_matrix(time)
points = [
mat.apply(NVector(bb.x1, bb.y1)),
mat.apply(NVector(bb.x1, bb.y2)),
mat.apply(NVector(bb.x2, bb.y2)),
mat.apply(NVector(bb.x2, bb.y1)),
]
x1 = min(p.x for p in points)
x2 = max(p.x for p in points)
y1 = min(p.y for p in points)
y2 = max(p.y for p in points)
return BoundingBox(x1, y1, x2, y2)
return bb
def add_shape(self, shape):
self.shapes.insert(-1, shape)
return shape
def insert_shape(self, index, shape):
self.shapes.insert(index, shape)
return shape
@classmethod
def load(cls, lottiedict):
object = ShapeElement.load(lottiedict)
shapes = []
transform = None
for obj in object.shapes:
if isinstance(obj, TransformShape):
if not transform:
transform = obj
else:
shapes.append(obj)
object.shapes = shapes
object.shapes.append(transform)
return object
## @ingroup Lottie
class FillRule(LottieEnum):
NonZero = 1
EvenOdd = 2
## @ingroup Lottie
class Fill(ShapeElement):
"""!
Solid fill color
"""
_props = [
LottieProp("opacity", "o", Value, False),
LottieProp("color", "c", ColorValue, False),
LottieProp("fill_rule", "r", FillRule, False),
]
## %Shape type.
type = "fl"
def __init__(self, color=None):
ShapeElement.__init__(self)
## Fill Opacity
self.opacity = Value(100)
## Fill Color
self.color = ColorValue(color or Color(1, 1, 1))
## Fill rule
self.fill_rule = None
## @ingroup Lottie
class GradientType(LottieEnum):
Linear = 1
Radial = 2
## @ingroup Lottie
class Gradient(LottieObject):
_props = [
LottieProp("start_point", "s", MultiDimensional, False),
LottieProp("end_point", "e", MultiDimensional, False),
LottieProp("gradient_type", "t", GradientType, False),
LottieProp("highlight_length", "h", Value, False),
LottieProp("highlight_angle", "a", Value, False),
LottieProp("colors", "g", GradientColors, False),
]
def __init__(self, colors=[]):
## Fill Opacity
self.opacity = Value(100)
## Gradient Start Point
self.start_point = MultiDimensional(NVector(0, 0))
## Gradient End Point
self.end_point = MultiDimensional(NVector(0, 0))
## Gradient Type
self.gradient_type = GradientType.Linear
## Gradient Highlight Length. Only if type is Radial
self.highlight_length = Value()
## Highlight Angle. Only if type is Radial
self.highlight_angle = Value()
## Gradient Colors
self.colors = GradientColors(colors)
## @ingroup Lottie
class GradientFill(ShapeElement, Gradient):
"""!
Gradient fill
"""
_props = [
LottieProp("opacity", "o", Value, False),
LottieProp("fill_rule", "r", FillRule, False),
]
## %Shape type.
type = "gf"
def __init__(self, colors=[]):
ShapeElement.__init__(self)
Gradient.__init__(self, colors)
## Fill Opacity
self.opacity = Value(100)
## Fill rule
self.fill_rule = None
## @ingroup Lottie
class LineJoin(LottieEnum):
Miter = 1
Round = 2
Bevel = 3
## @ingroup Lottie
class LineCap(LottieEnum):
Butt = 1
Round = 2
Square = 3
## @ingroup Lottie
class StrokeDashType(LottieEnum):
Dash = "d"
Gap = "g"
Offset = "o"
## @ingroup Lottie
class StrokeDash(LottieObject):
_props = [
LottieProp("name", "nm", str, False),
LottieProp("type", "n", StrokeDashType, False),
LottieProp("length", "v", Value, False),
]
def __init__(self, length=0, type=StrokeDashType.Dash):
self.name = type.name.lower()
self.type = type
self.length = Value(length)
def __str__(self):
return self.name or super().__str__()
## @ingroup Lottie
class BaseStroke(LottieObject):
_props = [
LottieProp("line_cap", "lc", LineCap, False),
LottieProp("line_join", "lj", LineJoin, False),
LottieProp("miter_limit", "ml", float, False),
LottieProp("opacity", "o", Value, False),
LottieProp("width", "w", Value, False),
LottieProp("dashes", "d", StrokeDash, True),
]
def __init__(self, width=1):
## Stroke Line Cap
self.line_cap = LineCap.Round
## Stroke Line Join
self.line_join = LineJoin.Round
## Stroke Miter Limit. Only if Line Join is set to Miter.
self.miter_limit = 0
## Stroke Opacity
self.opacity = Value(100)
## Stroke Width
self.width = Value(width)
## Dashes
self.dashes = None
## @ingroup Lottie
class Stroke(ShapeElement, BaseStroke):
"""!
Solid stroke
"""
_props = [
LottieProp("color", "c", MultiDimensional, False),
]
## %Shape type.
type = "st"
def __init__(self, color=None, width=1):
ShapeElement.__init__(self)
BaseStroke.__init__(self, width)
## Stroke Color
self.color = ColorValue(color or Color(0, 0, 0))
## @ingroup Lottie
class GradientStroke(ShapeElement, BaseStroke, Gradient):
"""!
Gradient stroke
"""
## %Shape type.
type = "gs"
def __init__(self, stroke_width=1):
ShapeElement.__init__(self)
BaseStroke.__init__(self, stroke_width)
Gradient.__init__(self)
def bounding_box(self, time=0):
return BoundingBox()
## @ingroup Lottie
class TransformShape(ShapeElement, Transform):
"""!
Group transform
"""
## %Shape type.
type = "tr"
def __init__(self):
ShapeElement.__init__(self)
Transform.__init__(self)
self.anchor_point = MultiDimensional(NVector(0, 0))
## @ingroup Lottie
class Composite(LottieEnum):
Above = 1
Below = 2
## @ingroup Lottie
class RepeaterTransform(Transform):
_props = [
LottieProp("start_opacity", "so", Value, False),
LottieProp("end_opacity", "eo", Value, False),
]
def __init__(self):
Transform.__init__(self)
self.start_opacity = Value(100)
self.end_opacity = Value(100)
## @ingroup Lottie
class Modifier(ShapeElement):
pass
## @ingroup Lottie
class TrimMultipleShapes(LottieEnum):
Simultaneously = 1
Individually = 2
## @ingroup Lottie
## @todo Implement SIF Export
class Trim(Modifier):
"""
Trims shapes into a segment
"""
_props = [
LottieProp("start", "s", Value, False),
LottieProp("end", "e", Value, False),
LottieProp("offset", "o", Value, False),
LottieProp("multiple", "m", TrimMultipleShapes, False),
]
## %Shape type.
type = "tm"
def __init__(self):
ShapeElement.__init__(self)
## Start of the segment, as a percentage
self.start = Value(0)
## End of the segment, as a percentage
self.end = Value(100)
## start/end offset, as an angle (0, 360)
self.offset = Value(0)
## @todo?
self.multiple = None
## @ingroup Lottie
class Repeater(Modifier):
"""
Duplicates previous shapes in a group
"""
_props = [
LottieProp("copies", "c", Value, False),
LottieProp("offset", "o", Value, False),
LottieProp("composite", "m", Composite, False),
LottieProp("transform", "tr", RepeaterTransform, False),
]
## %Shape type.
type = "rp"
def __init__(self, copies=1):
Modifier.__init__(self)
## Number of Copies
self.copies = Value(copies)
## Offset of Copies
self.offset = Value()
## Composite of copies
self.composite = Composite.Above
## Transform values for each repeater copy
self.transform = RepeaterTransform()
## @ingroup Lottie
## @todo Implement SIF Export
class RoundedCorners(Modifier):
"""
Rounds corners of other shapes
"""
_props = [
LottieProp("radius", "r", Value, False),
]
## %Shape type.
type = "rd"
def __init__(self):
Modifier.__init__(self)
## Rounded Corner Radius
self.radius = Value()
## @ingroup Lottie
## @ingroup LottieCheck
## @note marked as unsupported by lottie
class Merge(ShapeElement):
_props = [
LottieProp("merge_mode", "mm", float, False),
]
## %Shape type.
type = "mm"
def __init__(self):
ShapeElement.__init__(self)
## Merge Mode
self.merge_mode = 1
## @ingroup Lottie
## @note marked as unsupported by lottie
class Twist(ShapeElement):
_props = [
LottieProp("angle", "a", Value, False),
LottieProp("center", "c", MultiDimensional, False),
]
## %Shape type.
type = "tw"
def __init__(self):
ShapeElement.__init__(self)
self.angle = Value(0)
self.center = MultiDimensional(NVector(0, 0))
class ZigZag(ShapeElement):
"""
Zig Zag shape modifier
"""
_props = [
LottieProp("frequency", "r", Value, False),
LottieProp("amplitude", "s", Value, False),
LottieProp("point_type", "pt", Value, False),
]
## %Shape type.
type = "zz"
def __init__(self):
ShapeElement.__init__(self)
## Number of ridges per segment
self.frequency = Value(5)
## Distance between peaks and troughs
self.amplitude = Value(10)
## Point type (1 = corner, 2 = smooth)
self.point_type = Value(1)
+211
View File
@@ -0,0 +1,211 @@
from .base import LottieObject, LottieProp, LottieEnum
from .properties import Value, MultiDimensional
from .nvector import NVector
from .helpers import Transform
## @ingroup Lottie
## @ingroup LottieCheck
class MaskedPath(LottieObject):
_props = [
LottieProp("mask", "m", float),
LottieProp("f", "f", Value),
LottieProp("l", "l", Value),
LottieProp("r", "r", float),
]
def __init__(self):
## Type?
self.mask = None
## First?
self.f = None
## Last?
self.l = None
## ??
self.r = None
## @ingroup Lottie
## @ingroup LottieCheck
class TextAnimatorDataProperty(Transform):
_props = [
LottieProp("rx", "rx", Value),
LottieProp("ry", "ry", Value),
LottieProp("stroke_width", "sw", Value),
LottieProp("stroke_color", "sc", MultiDimensional),
LottieProp("fill_color", "fc", MultiDimensional),
LottieProp("fh", "fh", Value),
LottieProp("fs", "fs", Value),
LottieProp("fb", "fb", Value),
LottieProp("tracking", "t", Value),
LottieProp("scale", "s", MultiDimensional),
]
def __init__(self):
super().__init__()
## Angle?
self.rx = Value()
## Angle?
self.ry = Value()
## Stroke width
self.stroke_width = Value()
## Stroke color
self.stroke_color = MultiDimensional()
## Fill color
self.fill_color = MultiDimensional()
self.fh = Value()
## 0-100?
self.fs = Value()
## 0-100?
self.fb = Value()
## Tracking
self.tracking = Value()
## @ingroup Lottie
## @ingroup LottieCheck
class TextMoreOptions(LottieObject):
_props = [
LottieProp("alignment", "a", MultiDimensional),
LottieProp("g", "g", float),
]
def __init__(self):
self.alignment = MultiDimensional(NVector(0, 0))
self.g = None
## @ingroup Lottie
class TextJustify(LottieEnum):
Left = 0
Right = 1
Center = 2
## @ingroup Lottie
class TextDocument(LottieObject):
"""!
@see http://docs.aenhancers.com/other/textdocument/
Note that for multi-line text, lines are separated by \\r
"""
_props = [
LottieProp("font_family", "f", str),
LottieProp("color", "fc", NVector),
LottieProp("font_size", "s", float),
LottieProp("line_height", "lh", float),
LottieProp("wrap_size", "sz", NVector),
LottieProp("text", "t", str),
LottieProp("justify", "j", TextJustify),
# ls?
]
def __init__(self, text="", font_size=10, color=None, font_family=""):
self.font_family = font_family
## Text color
self.color = color or NVector(0, 0, 0)
## Line height when wrapping
self.line_height = None
## Text alignment
self.justify = TextJustify.Left
## Size of the box containing the text
self.wrap_size = None
## Text
self.text = text
## Font Size
self.font_size = font_size
## @ingroup Lottie
class TextDataKeyframe(LottieObject):
_props = [
LottieProp("start", "s", TextDocument),
LottieProp("time", "t", float),
]
def __init__(self, time=0, start=None):
## Start value of keyframe segment.
self.start = start
## Start time of keyframe segment.
self.time = time
## @ingroup Lottie
class TextData(LottieObject):
_props = [
LottieProp("keyframes", "k", TextDataKeyframe, True),
]
def __init__(self):
self.keyframes = []
def get_value(self, time):
for kf in self.keyframes:
if kf.time >= time:
return kf.start
return None
## @ingroup Lottie
class TextAnimatorData(LottieObject):
_props = [
LottieProp("properties", "a", TextAnimatorDataProperty, True),
LottieProp("data", "d", TextData, False),
LottieProp("more_options", "m", TextMoreOptions, False),
LottieProp("masked_path", "p", MaskedPath),
]
def __init__(self):
self.properties = []
self.data = TextData()
self.more_options = TextMoreOptions()
self.masked_path = MaskedPath()
def add_keyframe(self, time, item):
self.data.keyframes.append(TextDataKeyframe(time, item))
def get_value(self, time):
return self.data.get_value(time)
## @ingroup Lottie
class FontPathOrigin(LottieEnum):
Unknown = 0
CssUrl = 1
ScriptUrl = 2
FontUrl = 3
## @ingroup Lottie
class Font(LottieObject):
_props = [
LottieProp("ascent", "ascent", float),
LottieProp("font_family", "fFamily", str),
LottieProp("name", "fName", str),
LottieProp("font_style", "fStyle", str),
LottieProp("path", "fPath", str),
LottieProp("weight", "fWeight", str),
LottieProp("origin", "origin", FontPathOrigin),
]
def __init__(self, font_family="sans", font_style="Regular", name=None):
self.ascent = None
self.font_family = font_family
self.font_style = font_style
self.name = name or "%s-%s" % (font_family, font_style)
self.path = None
self.weight = None
self.origin = None
## @ingroup Lottie
class FontList(LottieObject):
_props = [
LottieProp("list", "list", Font, True),
]
def __init__(self):
self.list = []
def append(self, font):
self.list.append(font)