mirror of
https://github.com/OpenVGLab/OmniLottie.git
synced 2026-09-17 07:36:27 +00:00
Initial Commit
This commit is contained in:
@@ -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",
|
||||
]
|
||||
@@ -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__()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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__()
|
||||
@@ -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
@@ -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))
|
||||
@@ -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())
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user