mirror of
https://github.com/OpenVGLab/OmniLottie.git
synced 2026-09-17 15:46:26 +00:00
Initial Commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
from . import builder, importer
|
||||
from .importer import parse_sif_file
|
||||
from .builder import to_sif
|
||||
|
||||
__all__ = ["builder", "importer", "parse_sif_file", "to_sif"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
from .sif.nodes import *
|
||||
from . import ast
|
||||
from .ast_impl.base import SifKeyframe, Interpolation
|
||||
@@ -0,0 +1,2 @@
|
||||
from .ast_impl.nodes import *
|
||||
from .ast_impl.base import *
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,120 @@
|
||||
from xml.dom import minidom
|
||||
import enum
|
||||
from lottie.parsers.sif.sif.core import TypeDescriptor, ObjectRegistry, SifNodeMeta, FrameTime
|
||||
from lottie.parsers.sif.xml.utils import xml_child_elements, xml_first_element_child
|
||||
|
||||
|
||||
class SifAstNode:
|
||||
_subclasses = None
|
||||
_tag = None
|
||||
|
||||
@staticmethod
|
||||
def from_dom(xml: minidom.Element, param: TypeDescriptor, registry: ObjectRegistry):
|
||||
if xml.tagName == param.typename:
|
||||
return SifValue.from_dom(xml, param, registry)
|
||||
|
||||
xmltype = xml.getAttribute("type")
|
||||
if xmltype != param.typename and xmltype != "weighted_" + param.typename:
|
||||
raise ValueError("Invalid type %s (should be %s)" % (xmltype, param.typename))
|
||||
|
||||
return SifAstNode.ast_node_types()[xml.tagName].from_dom(xml, param, registry)
|
||||
|
||||
@staticmethod
|
||||
def ast_node_types():
|
||||
if SifAstNode._subclasses is None:
|
||||
from . import nodes
|
||||
SifAstNode._subclasses = {}
|
||||
SifAstNode._gather_ast_types(SifAstNode)
|
||||
return SifAstNode._subclasses
|
||||
|
||||
@staticmethod
|
||||
def _gather_ast_types(cls):
|
||||
for subcls in cls.__subclasses__():
|
||||
if subcls._tag:
|
||||
SifAstNode._subclasses[subcls._tag] = subcls
|
||||
SifAstNode._gather_ast_types(subcls)
|
||||
|
||||
def _prepare_to_dom(self, dom: minidom.Document, param: TypeDescriptor):
|
||||
element = dom.createElement(self._tag)
|
||||
element.setAttribute("type", param.typename)
|
||||
return element
|
||||
|
||||
|
||||
class SifValue(SifAstNode):
|
||||
def __init__(self, value=None):
|
||||
self.value = value
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s %r>" % (self.__class__.__name__, self.value)
|
||||
|
||||
@classmethod
|
||||
def from_dom(cls, xml: minidom.Element, param: TypeDescriptor, registry: ObjectRegistry):
|
||||
return SifValue(param.value_from_xml_element(xml, registry))
|
||||
|
||||
def to_dom(self, dom: minidom.Document, param: TypeDescriptor):
|
||||
return param.value_to_xml_element(self.value, dom)
|
||||
|
||||
|
||||
class Interpolation(enum.Enum):
|
||||
Auto = "auto"
|
||||
Linear = "linear"
|
||||
Clamped = "clamped"
|
||||
Ease = "halt"
|
||||
Constant = "constant"
|
||||
|
||||
|
||||
class SifKeyframe:
|
||||
def __init__(self, value, time: FrameTime, before=Interpolation.Clamped, after=Interpolation.Clamped):
|
||||
self.value = value
|
||||
self.time = time
|
||||
self.before = before
|
||||
self.after = after
|
||||
|
||||
@classmethod
|
||||
def from_dom(cls, xml: minidom.Element, param: TypeDescriptor, registry: ObjectRegistry):
|
||||
return cls(
|
||||
param.value_from_xml_element(xml_first_element_child(xml), registry),
|
||||
FrameTime.parse_string(xml.getAttribute("time"), registry),
|
||||
Interpolation(xml.getAttribute("before")),
|
||||
Interpolation(xml.getAttribute("after"))
|
||||
)
|
||||
|
||||
def to_dom(self, dom: minidom.Document, param: TypeDescriptor):
|
||||
element = dom.createElement("waypoint")
|
||||
element.setAttribute("time", str(self.time))
|
||||
element.setAttribute("before", self.before.value)
|
||||
element.setAttribute("after", self.after.value)
|
||||
element.appendChild(param.value_to_xml_element(self.value, dom))
|
||||
return element
|
||||
|
||||
def __repr__(self):
|
||||
return "<SifKeyframe %s %s>" % (self.time, self.value)
|
||||
|
||||
|
||||
class SifAnimated(SifAstNode):
|
||||
_tag = "animated"
|
||||
|
||||
def __init__(self):
|
||||
self.keyframes = []
|
||||
|
||||
@classmethod
|
||||
def from_dom(cls, xml: minidom.Element, param: TypeDescriptor, registry: ObjectRegistry):
|
||||
obj = SifAnimated()
|
||||
for waypoint in xml_child_elements(xml, "waypoint"):
|
||||
obj.keyframes.append(SifKeyframe.from_dom(waypoint, param, registry))
|
||||
return obj
|
||||
|
||||
def to_dom(self, dom: minidom.Document, param: TypeDescriptor):
|
||||
element = self._prepare_to_dom(dom, param)
|
||||
for kf in self.keyframes:
|
||||
element.appendChild(kf.to_dom(dom, param))
|
||||
return element
|
||||
|
||||
def add_keyframe(self, *args, **kwargs):
|
||||
if not kwargs and len(args) == 1 and isinstance(args[0], SifKeyframe):
|
||||
keyframe = args[0]
|
||||
else:
|
||||
keyframe = SifKeyframe(*args, **kwargs)
|
||||
|
||||
self.keyframes.append(keyframe)
|
||||
return keyframe
|
||||
@@ -0,0 +1,321 @@
|
||||
from xml.dom import minidom
|
||||
import enum
|
||||
|
||||
from lottie.nvector import NVector
|
||||
from lottie.parsers.sif.ast_impl.base import SifAstNode, TypeDescriptor, ObjectRegistry
|
||||
from lottie.parsers.sif.xml.animatable import XmlAnimatable
|
||||
from lottie.parsers.sif.xml.wrappers import XmlBoneReference, XmlSifElement, XmlList
|
||||
from lottie.parsers.sif.sif.nodes import Segment, WeightedVector, Bline
|
||||
from lottie.parsers.sif.sif.enums import Smooth
|
||||
from lottie.parsers.sif.sif.core import SifNodeMeta, FrameTime
|
||||
|
||||
|
||||
class SifAstComplex(SifAstNode, metaclass=SifNodeMeta):
|
||||
_nodes = []
|
||||
|
||||
def __init__(self, **kw):
|
||||
for node in self._nodes:
|
||||
node.initialize_object(kw, self)
|
||||
|
||||
@classmethod
|
||||
def from_dom(cls, xml: minidom.Element, param: TypeDescriptor, registry: ObjectRegistry):
|
||||
outcls = cls.get_class_from_dom(xml, param, registry)
|
||||
instance = outcls()
|
||||
for node in outcls._nodes:
|
||||
node.from_xml(instance, xml, registry)
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def get_class_from_dom(cls, xml: minidom.Element, param: TypeDescriptor, registry: ObjectRegistry):
|
||||
return cls
|
||||
|
||||
def to_dom(self, dom: minidom.Document, param: TypeDescriptor):
|
||||
element = self._prepare_to_dom(dom, param)
|
||||
for node in self._nodes:
|
||||
node.to_xml(self, element, dom, param)
|
||||
return element
|
||||
|
||||
|
||||
class SifAstBoneLink(SifAstComplex):
|
||||
_tag = "bone_link"
|
||||
|
||||
_nodes = [
|
||||
XmlBoneReference("bone"),
|
||||
XmlAnimatable("base_value", "vector", NVector(0, 0)),
|
||||
XmlAnimatable("translate", "bool", True),
|
||||
XmlAnimatable("rotate", "bool", True),
|
||||
XmlAnimatable("skew", "bool", True),
|
||||
XmlAnimatable("scale_x", "bool", True),
|
||||
XmlAnimatable("scale_y", "bool", True),
|
||||
]
|
||||
|
||||
|
||||
class SifAstBoneInfluence(SifAstComplex):
|
||||
_tag = "boneinfluence"
|
||||
|
||||
_nodes = [
|
||||
# TODO bone_weight_list
|
||||
XmlAnimatable("link", "vector", NVector(0, 0)),
|
||||
]
|
||||
|
||||
|
||||
class SifSegCalcTangent(SifAstComplex):
|
||||
_tag = "segcalctangent"
|
||||
|
||||
_nodes = [
|
||||
XmlSifElement("segment", Segment),
|
||||
XmlAnimatable("amount", "real", .5),
|
||||
]
|
||||
|
||||
|
||||
class SifSegCalcVertex(SifAstComplex):
|
||||
_tag = "segcalcvertex"
|
||||
|
||||
_nodes = [
|
||||
XmlSifElement("segment", Segment),
|
||||
XmlAnimatable("amount", "real", .5),
|
||||
]
|
||||
|
||||
|
||||
class WeightedAverage(SifAstComplex):
|
||||
_tag = "weighted_average"
|
||||
|
||||
_nodes = [
|
||||
XmlList(WeightedVector, "vectors", "entry"),
|
||||
]
|
||||
|
||||
def _prepare_to_dom(self, dom: minidom.Document, param: TypeDescriptor):
|
||||
element = dom.createElement(self._tag)
|
||||
element.setAttribute("type", "weighted_vector")
|
||||
return element
|
||||
|
||||
|
||||
class SifAdd(SifAstComplex):
|
||||
_tag = "add"
|
||||
|
||||
_nodes = [
|
||||
XmlAnimatable("lhs", "_recurse"),
|
||||
XmlAnimatable("rhs", "_recurse"),
|
||||
XmlAnimatable("scalar", "real", 1.),
|
||||
]
|
||||
|
||||
|
||||
class SifAnimatedFile(SifAstComplex):
|
||||
_tag = "animated_file"
|
||||
|
||||
_nodes = [
|
||||
XmlAnimatable("filename", "string"),
|
||||
]
|
||||
|
||||
|
||||
class Accuracy(enum.Enum):
|
||||
Rough = 0
|
||||
Normal = 1
|
||||
Fine = 2
|
||||
Extreme = 3
|
||||
|
||||
|
||||
class DerivativeOrder:
|
||||
FirstDerivative = 0
|
||||
SecondDerivative = 1
|
||||
|
||||
|
||||
class SifDerivative(SifAstComplex):
|
||||
_tag = "derivative"
|
||||
|
||||
_nodes = [
|
||||
XmlAnimatable("link", "_recurse"),
|
||||
XmlAnimatable("interval", "real", 0.01),
|
||||
XmlAnimatable("accuracy", "integer", Accuracy.Normal, Accuracy),
|
||||
XmlAnimatable("order", "integer", DerivativeOrder.FirstDerivative, DerivativeOrder),
|
||||
]
|
||||
|
||||
|
||||
class SifDynamic(SifAstComplex):
|
||||
_tag = "dynamic"
|
||||
|
||||
_nodes = [
|
||||
XmlAnimatable("tip_static", "vector", NVector(0, 0)),
|
||||
XmlAnimatable("origin", "vector", NVector(0, 0)),
|
||||
XmlAnimatable("force", "vector", NVector(0, 0)),
|
||||
XmlAnimatable("torque", "real", 0.),
|
||||
XmlAnimatable("damping", "real", 0.4),
|
||||
XmlAnimatable("friction", "real", 0.4),
|
||||
XmlAnimatable("spring", "real", 30.),
|
||||
XmlAnimatable("torsion", "real", 30.),
|
||||
XmlAnimatable("mass", "real", 0.3),
|
||||
XmlAnimatable("inertia", "real", 0.3),
|
||||
XmlAnimatable("spring_rigid", "bool", False),
|
||||
XmlAnimatable("torsion_rigid", "bool", False),
|
||||
XmlAnimatable("origin_drags_tip", "bool", True),
|
||||
]
|
||||
|
||||
|
||||
class SifGreyed(SifAstComplex):
|
||||
_tag = "greyed"
|
||||
|
||||
_nodes = [
|
||||
XmlAnimatable("link", "_recurse"),
|
||||
]
|
||||
|
||||
|
||||
class SifLinear(SifAstComplex):
|
||||
_tag = "linear"
|
||||
|
||||
_nodes = [
|
||||
XmlAnimatable("slope", "vector", NVector(0, 0)),
|
||||
XmlAnimatable("offset", "vector", NVector(0, 0)),
|
||||
]
|
||||
|
||||
|
||||
class SifRadialComposite(SifAstComplex):
|
||||
_tag = "radial_composite"
|
||||
|
||||
_nodes = [
|
||||
XmlAnimatable("radius", "real", 0.),
|
||||
XmlAnimatable("theta", "angle", 0.),
|
||||
]
|
||||
|
||||
|
||||
class SifComposite(SifAstComplex):
|
||||
_tag = "composite"
|
||||
|
||||
@classmethod
|
||||
def get_class_from_dom(cls, xml: minidom.Element, param: TypeDescriptor, registry: ObjectRegistry):
|
||||
type = xml.getAttribute("type")
|
||||
if type == "vector":
|
||||
return SifVectorComposite
|
||||
return None
|
||||
|
||||
def _prepare_to_dom(self, dom: minidom.Document, param: TypeDescriptor):
|
||||
element = dom.createElement("composite")
|
||||
element.setAttribute("type", param.typename)
|
||||
return element
|
||||
|
||||
|
||||
class SifVectorComposite(SifComposite):
|
||||
_type = "vector"
|
||||
|
||||
_nodes = [
|
||||
XmlAnimatable("x", "real", 0.),
|
||||
XmlAnimatable("y", "real", 0.),
|
||||
]
|
||||
|
||||
|
||||
class SifRandom(SifAstComplex):
|
||||
_tag = "random"
|
||||
|
||||
_nodes = [
|
||||
XmlAnimatable("link", "_recurse"),
|
||||
XmlAnimatable("radius", "real", 0.),
|
||||
XmlAnimatable("seed", "integer", 0),
|
||||
XmlAnimatable("speed", "real", 1.),
|
||||
XmlAnimatable("smooth", "integer", Smooth.Cubic, Smooth),
|
||||
XmlAnimatable("loop", "real", 0.),
|
||||
]
|
||||
|
||||
|
||||
class SifReference(SifAstComplex):
|
||||
_tag = "link"
|
||||
|
||||
_nodes = [
|
||||
XmlAnimatable("reference", "_recurse"),
|
||||
]
|
||||
|
||||
|
||||
class SifScale(SifAstComplex):
|
||||
_tag = "scale"
|
||||
|
||||
_nodes = [
|
||||
XmlAnimatable("link", "_recurse"),
|
||||
XmlAnimatable("scalar", "real", 1.),
|
||||
]
|
||||
|
||||
|
||||
class SifStep(SifAstComplex):
|
||||
_tag = "step"
|
||||
|
||||
_nodes = [
|
||||
XmlAnimatable("link", "_recurse"),
|
||||
XmlAnimatable("duration", "time", FrameTime(1, FrameTime.Unit.Seconds)),
|
||||
XmlAnimatable("start_time", "time", FrameTime(0, FrameTime.Unit.Seconds)),
|
||||
XmlAnimatable("intersection", "real", 0.5),
|
||||
]
|
||||
|
||||
|
||||
class SifSubtract(SifAstComplex):
|
||||
_tag = "subtract"
|
||||
|
||||
_nodes = [
|
||||
XmlAnimatable("lhs", "_recurse"),
|
||||
XmlAnimatable("rhs", "_recurse"),
|
||||
XmlAnimatable("scalar", "real", 1.),
|
||||
]
|
||||
|
||||
|
||||
class SifSwitch(SifAstComplex):
|
||||
_tag = "switch"
|
||||
|
||||
_nodes = [
|
||||
XmlAnimatable("link_off", "_recurse"),
|
||||
XmlAnimatable("link_on", "_recurse"),
|
||||
XmlAnimatable("switch", "bool", False),
|
||||
]
|
||||
|
||||
|
||||
class SifTimedSwap(SifAstComplex):
|
||||
_tag = "timed_swap"
|
||||
|
||||
_nodes = [
|
||||
XmlAnimatable("before", "_recurse"),
|
||||
XmlAnimatable("after", "_recurse"),
|
||||
XmlAnimatable("time", "time", FrameTime(0, FrameTime.Unit.Seconds)),
|
||||
XmlAnimatable("length", "time", FrameTime(0, FrameTime.Unit.Seconds)),
|
||||
]
|
||||
|
||||
|
||||
class SifTimeLoop(SifAstComplex):
|
||||
_tag = "timeloop"
|
||||
|
||||
_nodes = [
|
||||
XmlAnimatable("link", "_recurse"),
|
||||
XmlAnimatable("link_time", "time", FrameTime(0, FrameTime.Unit.Seconds)),
|
||||
XmlAnimatable("local_time", "time", FrameTime(0, FrameTime.Unit.Seconds)),
|
||||
XmlAnimatable("duration", "time", FrameTime(0, FrameTime.Unit.Seconds)),
|
||||
]
|
||||
|
||||
|
||||
class SifPower(SifAstComplex):
|
||||
_tag = "power"
|
||||
|
||||
_nodes = [
|
||||
XmlAnimatable("base", "real", 1.),
|
||||
XmlAnimatable("power", "real", 1.),
|
||||
XmlAnimatable("epsilon", "real", 0.000001),
|
||||
XmlAnimatable("infinite", "real", 999999.),
|
||||
]
|
||||
|
||||
|
||||
class SifBlineCalcTangent(SifAstComplex):
|
||||
_tag = "blinecalctangent"
|
||||
|
||||
_nodes = [
|
||||
XmlSifElement("bline", Bline),
|
||||
XmlAnimatable("loop", "bool", False),
|
||||
XmlAnimatable("amount", "real", 0.5),
|
||||
XmlAnimatable("offset", "angle", 0.),
|
||||
XmlAnimatable("scale", "real", 1.),
|
||||
XmlAnimatable("fixed_length", "bool", False),
|
||||
XmlAnimatable("homogeneous", "bool", False),
|
||||
]
|
||||
|
||||
|
||||
class SifBlineCalcVertex(SifAstComplex):
|
||||
_tag = "blinecalcvertex"
|
||||
|
||||
_nodes = [
|
||||
XmlSifElement("bline", Bline),
|
||||
XmlAnimatable("loop", "bool", False),
|
||||
XmlAnimatable("amount", "real", 0.5),
|
||||
XmlAnimatable("homogeneous", "bool", False),
|
||||
]
|
||||
@@ -0,0 +1,411 @@
|
||||
import math
|
||||
from xml.dom import minidom
|
||||
|
||||
from ... import objects
|
||||
from ...nvector import NVector
|
||||
from ...utils import restructure
|
||||
from . import api, ast
|
||||
|
||||
|
||||
blend_modes = {
|
||||
objects.BlendMode.Normal: api.BlendMethod.Composite,
|
||||
objects.BlendMode.Multiply: api.BlendMethod.Multiply,
|
||||
objects.BlendMode.Screen: api.BlendMethod.Screen,
|
||||
objects.BlendMode.Overlay: api.BlendMethod.Overlay,
|
||||
objects.BlendMode.Darken: api.BlendMethod.Darken,
|
||||
objects.BlendMode.Lighten: api.BlendMethod.Lighten,
|
||||
objects.BlendMode.HardLight: api.BlendMethod.HardLight,
|
||||
objects.BlendMode.Difference: api.BlendMethod.Difference,
|
||||
objects.BlendMode.Hue: api.BlendMethod.Hue,
|
||||
objects.BlendMode.Saturation: api.BlendMethod.Saturation,
|
||||
objects.BlendMode.Color: api.BlendMethod.Color,
|
||||
objects.BlendMode.Luminosity: api.BlendMethod.Luminosity,
|
||||
objects.BlendMode.Exclusion: api.BlendMethod.Difference,
|
||||
objects.BlendMode.SoftLight: api.BlendMethod.Multiply,
|
||||
objects.BlendMode.ColorDodge: api.BlendMethod.Composite,
|
||||
objects.BlendMode.ColorBurn: api.BlendMethod.Composite,
|
||||
}
|
||||
|
||||
|
||||
class SifBuilder(restructure.AbstractBuilder):
|
||||
def __init__(self, gamma=1.0):
|
||||
"""
|
||||
@todo Add gamma option to lottie_convert.py
|
||||
"""
|
||||
super().__init__()
|
||||
self.canvas = api.Canvas()
|
||||
self.canvas.version = "1.2"
|
||||
self.canvas.gamma_r = self.canvas.gamma_g = self.canvas.gamma_b = gamma
|
||||
self.autoid = objects.base.Index()
|
||||
|
||||
def _on_animation(self, animation: objects.Animation):
|
||||
if animation.name:
|
||||
self.canvas.name = animation.name
|
||||
self.canvas.width = animation.width
|
||||
self.canvas.height = animation.height
|
||||
self.canvas.xres = animation.width
|
||||
self.canvas.yres = animation.height
|
||||
self.canvas.view_box = NVector(0, 0, animation.width, animation.height)
|
||||
self.canvas.fps = animation.frame_rate
|
||||
self.canvas.begin_time = api.FrameTime.frame(animation.in_point)
|
||||
self.canvas.end_time = api.FrameTime.frame(animation.out_point)
|
||||
self.canvas.antialias = True
|
||||
return self.canvas
|
||||
|
||||
def _on_precomp(self, id, dom_parent, layers):
|
||||
g = dom_parent.add_layer(api.GroupLayer())
|
||||
g.desc = id
|
||||
for layer_builder in layers:
|
||||
self.process_layer(layer_builder, g)
|
||||
|
||||
def _on_layer(self, layer_builder, dom_parent):
|
||||
layer = self.layer_from_lottie(api.GroupLayer, layer_builder.lottie, dom_parent)
|
||||
if not layer_builder.lottie.name:
|
||||
layer.desc = layer_builder.lottie.__class__.__name__
|
||||
|
||||
bm = getattr(layer_builder.lottie, "blend_mode", None)
|
||||
if bm is None:
|
||||
bm = objects.BlendMode.Normal
|
||||
layer.blend_method = blend_modes[bm]
|
||||
|
||||
layer.time_drilation = getattr(layer_builder.lottie, "stretch", 1) or 1
|
||||
|
||||
in_point = getattr(layer_builder.lottie, "in_point", 0)
|
||||
layer.time_offset.value = api.FrameTime.frame(in_point)
|
||||
|
||||
#layer.canvas.end_time = api.FrameTime.frame(out_point)
|
||||
return layer
|
||||
|
||||
def layer_from_lottie(self, type, lottie, dom_parent):
|
||||
g = dom_parent.add_layer(type())
|
||||
if lottie.name:
|
||||
g.desc = lottie.name
|
||||
g.active = not lottie.hidden
|
||||
transf = getattr(lottie, "transform", None)
|
||||
if transf:
|
||||
self.set_transform(g, transf)
|
||||
|
||||
if isinstance(lottie, objects.NullLayer):
|
||||
g.amount.value = 1
|
||||
|
||||
return g
|
||||
|
||||
def _get_scale(self, transform):
|
||||
def func(keyframe):
|
||||
t = keyframe.time if keyframe else 0
|
||||
scale_x, scale_y = transform.scale.get_value(t)[:2]
|
||||
scale_x /= 100
|
||||
scale_y /= 100
|
||||
skew = transform.skew.get_value(t) if transform.skew else 0
|
||||
c = math.cos(skew * math.pi / 180)
|
||||
if c != 0:
|
||||
scale_y *= 1 / c
|
||||
return NVector(scale_x, scale_y)
|
||||
return func
|
||||
|
||||
def set_transform(self, group, transform):
|
||||
composite = group.transformation
|
||||
|
||||
if transform.position:
|
||||
composite.offset = self.process_vector(transform.position)
|
||||
|
||||
if transform.scale:
|
||||
keyframes = self._merge_keyframes([transform.scale, transform.skew])
|
||||
composite.scale = self.process_vector_ext(keyframes, self._get_scale(transform))
|
||||
|
||||
composite.skew_angle = self.process_scalar(transform.skew or objects.Value(0))
|
||||
|
||||
if transform.rotation:
|
||||
composite.angle = self.process_scalar(transform.rotation)
|
||||
|
||||
if transform.opacity:
|
||||
group.amount = self.process_scalar(transform.opacity, 1/100)
|
||||
|
||||
if transform.anchor_point:
|
||||
group.origin = self.process_vector(transform.anchor_point)
|
||||
|
||||
# TODO get z_depth from position
|
||||
composite.z_depth = 0
|
||||
|
||||
def process_vector(self, multidim):
|
||||
def getter(keyframe):
|
||||
if keyframe is None:
|
||||
v = multidim.value
|
||||
else:
|
||||
v = keyframe.start
|
||||
return NVector(v[0], v[1])
|
||||
|
||||
return self.process_vector_ext(multidim.keyframes, getter)
|
||||
|
||||
def process_vector_ext(self, kframes, getter):
|
||||
if kframes is not None:
|
||||
wrap = ast.SifAnimated()
|
||||
for i in range(len(kframes)):
|
||||
keyframe = kframes[i]
|
||||
waypoint = wrap.add_keyframe(getter(keyframe), api.FrameTime.frame(keyframe.time))
|
||||
|
||||
if i > 0:
|
||||
prev = kframes[i-1]
|
||||
if prev.jump:
|
||||
waypoint.before = api.Interpolation.Constant
|
||||
elif prev.in_value and prev.in_value.x < 1:
|
||||
waypoint.before = api.Interpolation.Ease
|
||||
else:
|
||||
waypoint.before = api.Interpolation.Linear
|
||||
else:
|
||||
waypoint.before = api.Interpolation.Linear
|
||||
|
||||
if keyframe.jump:
|
||||
waypoint.after = api.Interpolation.Constant
|
||||
elif keyframe.out_value and keyframe.out_value.x > 0:
|
||||
waypoint.after = api.Interpolation.Ease
|
||||
else:
|
||||
waypoint.after = api.Interpolation.Linear
|
||||
else:
|
||||
wrap = api.SifValue(getter(None))
|
||||
|
||||
return wrap
|
||||
|
||||
def process_scalar(self, value, mult=None):
|
||||
def getter(keyframe):
|
||||
if keyframe is None:
|
||||
v = value.value
|
||||
else:
|
||||
v = keyframe.start[0]
|
||||
if mult is not None:
|
||||
v *= mult
|
||||
return v
|
||||
return self.process_vector_ext(value.keyframes, getter)
|
||||
|
||||
def _on_shape(self, shape, group, dom_parent):
|
||||
layers = []
|
||||
if not hasattr(shape, "to_bezier"):
|
||||
return []
|
||||
|
||||
if group.stroke:
|
||||
sif_shape = self.build_path(api.OutlineLayer, shape.to_bezier(), dom_parent, shape)
|
||||
self.apply_group_stroke(sif_shape, group.stroke)
|
||||
layers.append(sif_shape)
|
||||
|
||||
if group.fill:
|
||||
sif_shape = self.build_path(api.RegionLayer, shape.to_bezier(), dom_parent, shape)
|
||||
layers.append(sif_shape)
|
||||
self.apply_group_fill(sif_shape, group.fill)
|
||||
|
||||
return layers
|
||||
|
||||
def _merge_keyframes(self, props):
|
||||
keyframes = {}
|
||||
for prop in props:
|
||||
if prop is not None and prop.animated:
|
||||
keyframes.update({kf.time: kf for kf in prop.keyframes})
|
||||
return list(sorted(keyframes.values(), key=lambda kf: kf.time)) or None
|
||||
|
||||
def apply_origin(self, sif_shape, lottie_shape):
|
||||
if hasattr(lottie_shape, "position"):
|
||||
sif_shape.origin.value = lottie_shape.position.get_value()
|
||||
else:
|
||||
sif_shape.origin.value = lottie_shape.bounding_box().center()
|
||||
|
||||
def apply_group_fill(self, sif_shape, fill):
|
||||
## @todo gradients?
|
||||
if hasattr(fill, "colors"):
|
||||
return
|
||||
|
||||
def getter(keyframe):
|
||||
if keyframe is None:
|
||||
v = fill.color.value
|
||||
else:
|
||||
v = keyframe.start
|
||||
return self.canvas.make_color(*v)
|
||||
|
||||
sif_shape.color = self.process_vector_ext(fill.color.keyframes, getter)
|
||||
|
||||
def get_op(keyframe):
|
||||
if keyframe is None:
|
||||
v = fill.opacity.value
|
||||
else:
|
||||
v = keyframe.start[0]
|
||||
v /= 100
|
||||
return v
|
||||
|
||||
sif_shape.amount = self.process_vector_ext(fill.opacity.keyframes, get_op)
|
||||
|
||||
def apply_group_stroke(self, sif_shape, stroke):
|
||||
self.apply_group_fill(sif_shape, stroke)
|
||||
sif_shape.sharp_cusps.value = stroke.line_join == objects.LineJoin.Miter
|
||||
round_cap = stroke.line_cap == objects.LineCap.Round
|
||||
sif_shape.round_tip_0.value = round_cap
|
||||
sif_shape.round_tip_1.value = round_cap
|
||||
sif_shape.width = self.process_scalar(stroke.width, 0.5)
|
||||
|
||||
def build_path(self, type, path, dom_parent, lottie_shape):
|
||||
layer = self.layer_from_lottie(type, lottie_shape, dom_parent)
|
||||
self.apply_origin(layer, lottie_shape)
|
||||
startbez = path.shape.get_value()
|
||||
layer.bline.loop = startbez.closed
|
||||
nverts = len(startbez.vertices)
|
||||
for point in range(nverts):
|
||||
self.bezier_point(path, point, layer.bline, layer.origin.value)
|
||||
return layer
|
||||
|
||||
def bezier_point(self, lottie_path, point_index, sif_parent, offset):
|
||||
composite = api.BlinePoint()
|
||||
|
||||
def get_point(keyframe):
|
||||
if keyframe is None:
|
||||
bezier = lottie_path.shape.value
|
||||
else:
|
||||
bezier = keyframe.start
|
||||
if not bezier:
|
||||
#elem.parentNode.parentNode.removeChild(elem.parentNode)
|
||||
return
|
||||
vert = bezier.vertices[point_index]
|
||||
return NVector(vert[0], vert[1]) - offset
|
||||
|
||||
composite.point = self.process_vector_ext(lottie_path.shape.keyframes, get_point)
|
||||
composite.split.value = True
|
||||
composite.split_radius.value = True
|
||||
composite.split_angle.value = True
|
||||
|
||||
def get_tangent(keyframe):
|
||||
if keyframe is None:
|
||||
bezier = lottie_path.shape.value
|
||||
else:
|
||||
bezier = keyframe.start
|
||||
if not bezier:
|
||||
#elem.parentNode.parentNode.removeChild(elem.parentNode)
|
||||
return
|
||||
|
||||
inp = getattr(bezier, which_point)[point_index]
|
||||
return NVector(inp.x, inp.y) * 3 * mult
|
||||
|
||||
mult = -1
|
||||
which_point = "in_tangents"
|
||||
composite.t1 = self.process_vector_ext(lottie_path.shape.keyframes, get_tangent)
|
||||
|
||||
mult = 1
|
||||
which_point = "out_tangents"
|
||||
composite.t2 = self.process_vector_ext(lottie_path.shape.keyframes, get_tangent)
|
||||
sif_parent.points.append(composite)
|
||||
|
||||
def _on_shapegroup(self, shape_group, dom_parent):
|
||||
if shape_group.empty():
|
||||
return
|
||||
|
||||
layer = self.layer_from_lottie(api.GroupLayer, shape_group.lottie, dom_parent)
|
||||
|
||||
self.shapegroup_process_children(shape_group, layer)
|
||||
|
||||
def _modifier_inner_group(self, modifier, shapegroup, dom_parent):
|
||||
layer = dom_parent.add_layer(api.GroupLayer())
|
||||
self.shapegroup_process_child(modifier.child, shapegroup, layer)
|
||||
return layer
|
||||
|
||||
def _on_shape_modifier(self, modifier, shapegroup, dom_parent):
|
||||
layer = dom_parent.add_layer(api.GroupLayer())
|
||||
if modifier.lottie.name:
|
||||
layer.desc = modifier.lottie.name
|
||||
|
||||
inner = self._modifier_inner_group(modifier, shapegroup, layer)
|
||||
if isinstance(modifier.lottie, objects.Repeater):
|
||||
self.build_repeater(modifier.lottie, inner, layer)
|
||||
|
||||
def _build_repeater_defs(self, shape, name_id):
|
||||
dup = api.Duplicate()
|
||||
dup.id = name_id
|
||||
self.canvas.defs.append(dup)
|
||||
self.canvas.register_as(dup, name_id)
|
||||
|
||||
def getter(keyframe):
|
||||
if keyframe is None:
|
||||
v = shape.copies.value
|
||||
else:
|
||||
v = keyframe.start[0]
|
||||
|
||||
return v - 1
|
||||
|
||||
setattr(dup, "from", self.process_vector_ext(shape.copies.keyframes, getter))
|
||||
dup.to.value = 0
|
||||
dup.step.value = -1
|
||||
return dup
|
||||
|
||||
def _build_repeater_transform_scale_component(self, shape, name_id, comp, scalecomposite):
|
||||
power = ast.SifPower()
|
||||
setattr(scalecomposite, "xy"[comp], power)
|
||||
|
||||
def getter(keyframe):
|
||||
if keyframe is None:
|
||||
v = shape.transform.scale.value
|
||||
else:
|
||||
v = keyframe.start
|
||||
v = v[comp] / 100
|
||||
return v
|
||||
|
||||
power.base = self.process_vector_ext(shape.transform.scale.keyframes, getter)
|
||||
|
||||
# HACK work around an issue in Synfig
|
||||
power.power = ast.SifAdd()
|
||||
power.power.lhs.value = api.ValueReference(name_id)
|
||||
power.power.rhs.value = 0.000001
|
||||
|
||||
def _build_repeater_transform(self, shape, inner, name_id):
|
||||
offset_id = name_id + "_origin"
|
||||
origin = api.ExportedValue(offset_id, self.process_vector(shape.transform.anchor_point), "vector")
|
||||
self.canvas.defs.append(origin)
|
||||
self.canvas.register_as(origin, offset_id)
|
||||
inner.origin = origin
|
||||
|
||||
composite = inner.transformation
|
||||
|
||||
composite.offset = ast.SifAdd()
|
||||
composite.offset.rhs.value = api.ValueReference(offset_id)
|
||||
composite.offset.lhs = ast.SifScale()
|
||||
composite.offset.lhs.scalar.value = api.ValueReference(name_id)
|
||||
composite.offset.lhs.link = self.process_vector(shape.transform.position)
|
||||
|
||||
composite.angle = ast.SifScale()
|
||||
composite.angle.scalar.value = api.ValueReference(name_id)
|
||||
composite.angle.link = self.process_scalar(shape.transform.rotation)
|
||||
|
||||
composite.scale = ast.SifVectorComposite()
|
||||
self._build_repeater_transform_scale_component(shape, name_id, 0, composite.scale)
|
||||
self._build_repeater_transform_scale_component(shape, name_id, 1, composite.scale)
|
||||
|
||||
def _build_repeater_amount(self, shape, inner, name_id):
|
||||
inner.amount = ast.SifSubtract()
|
||||
inner.amount.lhs = self.process_scalar(shape.transform.start_opacity, 0.01)
|
||||
|
||||
inner.amount.rhs = ast.SifScale()
|
||||
inner.amount.rhs.scalar.value = api.ValueReference(name_id)
|
||||
|
||||
def getter(keyframe):
|
||||
if keyframe is None:
|
||||
t = 0
|
||||
end = shape.transform.end_opacity.value
|
||||
else:
|
||||
t = keyframe.time
|
||||
end = keyframe.start[0]
|
||||
start = shape.transform.start_opacity.get_value(t)
|
||||
n = shape.copies.get_value(t)
|
||||
v = (start - end) / (n - 1) / 100 if n > 0 else 0
|
||||
return v
|
||||
inner.amount.rhs.link = self.process_vector_ext(shape.transform.end_opacity.keyframes, getter)
|
||||
|
||||
def build_repeater(self, shape, inner, dom_parent):
|
||||
name_id = "duplicate_%s" % next(self.autoid)
|
||||
dup = self._build_repeater_defs(shape, name_id)
|
||||
self._build_repeater_transform(shape, inner, name_id)
|
||||
self._build_repeater_amount(shape, inner, name_id)
|
||||
inner.desc = "Transformation for " + (dom_parent.desc or "duplicate")
|
||||
|
||||
# duplicate layer
|
||||
duplicate = dom_parent.add_layer(api.DuplicateLayer())
|
||||
duplicate.index = dup
|
||||
duplicate.desc = shape.name
|
||||
|
||||
|
||||
def to_sif(animation):
|
||||
builder = SifBuilder()
|
||||
builder.process(animation)
|
||||
return builder.canvas
|
||||
@@ -0,0 +1,528 @@
|
||||
import math
|
||||
from ... import objects
|
||||
from ...objects import easing
|
||||
from . import api, ast
|
||||
from ... import NVector, PolarVector
|
||||
|
||||
try:
|
||||
from ...utils import font
|
||||
has_font = True
|
||||
except ImportError:
|
||||
has_font = False
|
||||
|
||||
|
||||
def convert(canvas: api.Canvas):
|
||||
return Converter().convert(canvas)
|
||||
|
||||
|
||||
class Converter:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def _animated(self, sifval):
|
||||
return isinstance(sifval, ast.SifAnimated)
|
||||
|
||||
def convert(self, canvas: api.Canvas):
|
||||
self.canvas = canvas
|
||||
self.animation = objects.Animation(
|
||||
self._time(canvas.end_time),
|
||||
canvas.fps
|
||||
)
|
||||
self.animation.in_point = self._time(canvas.begin_time)
|
||||
self.animation.width = canvas.width
|
||||
self.animation.height = canvas.height
|
||||
self.view_p1 = NVector(canvas.view_box[0], canvas.view_box[1])
|
||||
self.view_p2 = NVector(canvas.view_box[2], canvas.view_box[3])
|
||||
self.target_size = NVector(canvas.width, canvas.height)
|
||||
self.shape_layer = self.animation.add_layer(objects.ShapeLayer())
|
||||
self.gamma = NVector(canvas.gamma_r, canvas.gamma_g, canvas.gamma_b)
|
||||
self._process_layers(canvas.layers, self.shape_layer)
|
||||
return self.animation
|
||||
|
||||
def _time(self, t: api.FrameTime):
|
||||
return self.canvas.time_to_frames(t)
|
||||
|
||||
def _process_layers(self, layers, parent):
|
||||
old_gamma = self.gamma
|
||||
|
||||
for layer in reversed(layers):
|
||||
if not layer.active:
|
||||
continue
|
||||
elif isinstance(layer, api.GroupLayerBase):
|
||||
parent.add_shape(self._convert_group(layer))
|
||||
elif isinstance(layer, api.RectangleLayer):
|
||||
parent.add_shape(self._convert_fill(layer, self._convert_rect))
|
||||
elif isinstance(layer, api.CircleLayer):
|
||||
parent.add_shape(self._convert_fill(layer, self._convert_circle))
|
||||
elif isinstance(layer, api.StarLayer):
|
||||
parent.add_shape(self._convert_fill(layer, self._convert_star))
|
||||
elif isinstance(layer, api.PolygonLayer):
|
||||
parent.add_shape(self._convert_fill(layer, self._convert_polygon))
|
||||
elif isinstance(layer, api.RegionLayer):
|
||||
parent.add_shape(self._convert_fill(layer, self._convert_bline))
|
||||
elif isinstance(layer, api.AbstractOutline):
|
||||
parent.add_shape(self._convert_outline(layer, self._convert_bline))
|
||||
elif isinstance(layer, api.GradientLayer):
|
||||
parent.add_shape(self._convert_gradient(layer, parent))
|
||||
elif isinstance(layer, api.TransformDown):
|
||||
shape = self._convert_transform_down(layer)
|
||||
parent.add_shape(shape)
|
||||
parent = shape
|
||||
elif isinstance(layer, api.TextLayer):
|
||||
if has_font:
|
||||
parent.add_shape(self._convert_fill(layer, self._convert_text))
|
||||
elif isinstance(layer, api.ColorCorrectLayer):
|
||||
self.gamma = self.gamma * NVector(layer.gamma.value, layer.gamma.value, layer.gamma.value)
|
||||
|
||||
self.gamma = old_gamma
|
||||
|
||||
def _convert_group(self, layer: api.GroupLayer):
|
||||
shape = objects.Group()
|
||||
self._set_name(shape, layer)
|
||||
shape.transform.anchor_point = self._adjust_coords(self._convert_vector(layer.origin))
|
||||
self._convert_transform(layer.transformation, shape.transform)
|
||||
self._process_layers(layer.layers, shape)
|
||||
shape.transform.opacity = self._adjust_animated(
|
||||
self._convert_scalar(layer.amount),
|
||||
lambda x: x*100
|
||||
)
|
||||
return shape
|
||||
|
||||
def _convert_transform(self, sif_transform: api.AbstractTransform, lottie_transform: objects.Transform):
|
||||
if isinstance(sif_transform, api.BoneLinkTransform):
|
||||
base_transform = sif_transform.base_value
|
||||
else:
|
||||
base_transform = sif_transform
|
||||
|
||||
position = self._adjust_coords(self._convert_vector(base_transform.offset))
|
||||
rotation = self._adjust_angle(self._convert_scalar(base_transform.angle))
|
||||
scale = self._adjust_animated(
|
||||
self._convert_vector(base_transform.scale),
|
||||
lambda x: x * 100
|
||||
)
|
||||
|
||||
lottie_transform.skew_axis = self._adjust_angle(self._convert_scalar(base_transform.skew_angle))
|
||||
|
||||
if isinstance(sif_transform, api.BoneLinkTransform):
|
||||
lottie_transform.position = position
|
||||
lottie_transform.rotation = rotation
|
||||
lottie_transform.scale = scale
|
||||
#bone = sif_transform.bone
|
||||
#b_pos = self._adjust_coords(self._convert_vector(bone.origin))
|
||||
#old_anchor = lottie_transform.anchor_point
|
||||
|
||||
#if sif_transform.translate:
|
||||
#self._mix_animations_into(
|
||||
#[position, b_pos, old_anchor],
|
||||
#lottie_transform.position,
|
||||
#lambda base_p, bone_p, anchor: (anchor-self.target_size/2)/2+self.target_size/2
|
||||
#)
|
||||
#else:
|
||||
#lottie_transform.position = position
|
||||
|
||||
#lottie_transform.anchor_point = b_pos
|
||||
#lottie_transform.anchor_point.value += NVector(100,0)
|
||||
|
||||
#if sif_transform.rotate:
|
||||
#b_rot = self._convert_scalar(bone.angle)
|
||||
#self._mix_animations_into([rotation, b_rot], lottie_transform.rotation, lambda a, b: a-b)
|
||||
#else:
|
||||
#lottie_transform.rotation = rotation
|
||||
|
||||
#if sif_transform.scale_y:
|
||||
#b_scale = self._convert_scalar(bone.scalelx)
|
||||
#self._mix_animations_into(
|
||||
#scale, b_scale, lottie_transform.scale,
|
||||
#lambda a, b: NVector(a.x, a.y * b)
|
||||
#)
|
||||
#else:
|
||||
#lottie_transform.scale = scale
|
||||
else:
|
||||
lottie_transform.position = position
|
||||
lottie_transform.rotation = rotation
|
||||
lottie_transform.scale = scale
|
||||
|
||||
def _mix_animations_into(self, animations, output, mix):
|
||||
if not any(x.animated for x in animations):
|
||||
output.value = mix(*(x.value for x in animations))
|
||||
else:
|
||||
for vals in self._mix_animations(*animations):
|
||||
time = vals.pop(0)
|
||||
output.add_keyframe(time, mix(*vals))
|
||||
|
||||
def _convert_fill(self, layer, converter):
|
||||
shape = objects.Group()
|
||||
self._set_name(shape, layer)
|
||||
shape.add_shape(converter(layer))
|
||||
if layer.invert.value:
|
||||
shape.add_shape(objects.Rect(self.target_size/2, self.target_size))
|
||||
|
||||
fill = objects.Fill()
|
||||
fill.color = self._convert_color(layer.color)
|
||||
fill.opacity = self._adjust_animated(
|
||||
self._convert_scalar(layer.amount),
|
||||
lambda x: x * 100
|
||||
)
|
||||
shape.add_shape(fill)
|
||||
return shape
|
||||
|
||||
def _convert_linecap(self, lc: api.LineCap):
|
||||
if lc == api.LineCap.Rounded:
|
||||
return objects.LineCap.Round
|
||||
if lc == api.LineCap.Squared:
|
||||
return objects.LineCap.Square
|
||||
return objects.LineCap.Butt
|
||||
|
||||
def _convert_cusp(self, lc: api.CuspStyle):
|
||||
if lc == api.CuspStyle.Miter:
|
||||
return objects.LineJoin.Miter
|
||||
if lc == api.CuspStyle.Bevel:
|
||||
return objects.LineJoin.Bevel
|
||||
return objects.LineJoin.Round
|
||||
|
||||
def _convert_outline(self, layer: api.AbstractOutline, converter):
|
||||
shape = objects.Group()
|
||||
self._set_name(shape, layer)
|
||||
shape.add_shape(converter(layer))
|
||||
stroke = objects.Stroke()
|
||||
stroke.color = self._convert_color(layer.color)
|
||||
stroke.line_cap = self._convert_linecap(layer.start_tip)
|
||||
stroke.line_join = self._convert_cusp(layer.cusp_type)
|
||||
stroke.width = self._adjust_scalar(self._convert_scalar(layer.width))
|
||||
shape.add_shape(stroke)
|
||||
return shape
|
||||
|
||||
def _convert_rect(self, layer: api.RectangleLayer):
|
||||
rect = objects.Rect()
|
||||
p1 = self._adjust_coords(self._convert_vector(layer.point1))
|
||||
p2 = self._adjust_coords(self._convert_vector(layer.point2))
|
||||
if p1.animated or p2.animated:
|
||||
for time, p1v, p2v in self._mix_animations(p1, p2):
|
||||
rect.position.add_keyframe(time, (p1v + p2v) / 2)
|
||||
rect.size.add_keyframe(time, abs(p2v - p1v))
|
||||
pass
|
||||
else:
|
||||
rect.position.value = (p1.value + p2.value) / 2
|
||||
rect.size.value = abs(p2.value - p1.value)
|
||||
rect.rounded = self._adjust_scalar(self._convert_scalar(layer.bevel))
|
||||
return rect
|
||||
|
||||
def _convert_circle(self, layer: api.CircleLayer):
|
||||
shape = objects.Ellipse()
|
||||
shape.position = self._adjust_coords(self._convert_vector(layer.origin))
|
||||
radius = self._adjust_scalar(self._convert_scalar(layer.radius))
|
||||
shape.size = self._adjust_add_dimension(radius, lambda x: NVector(x, x) * 2)
|
||||
return shape
|
||||
|
||||
def _convert_star(self, layer: api.StarLayer):
|
||||
shape = objects.Star()
|
||||
shape.position = self._adjust_coords(self._convert_vector(layer.origin))
|
||||
shape.inner_radius = self._adjust_scalar(self._convert_scalar(layer.radius2))
|
||||
shape.outer_radius = self._adjust_scalar(self._convert_scalar(layer.radius1))
|
||||
shape.rotation = self._adjust_animated(
|
||||
self._convert_scalar(layer.angle),
|
||||
lambda x: 90-x
|
||||
)
|
||||
shape.points = self._convert_scalar(layer.points)
|
||||
if layer.regular_polygon.value:
|
||||
shape.star_type = objects.StarType.Polygon
|
||||
return shape
|
||||
|
||||
def _mix_animations(self, *animatable):
|
||||
times = set()
|
||||
for v in animatable:
|
||||
self._force_animated(v)
|
||||
for kf in v.keyframes:
|
||||
times.add(kf.time)
|
||||
|
||||
for time in sorted(times):
|
||||
yield [time] + [v.get_value(time) for v in animatable]
|
||||
|
||||
def _force_animated(self, lottieval):
|
||||
if not lottieval.animated:
|
||||
v = lottieval.value
|
||||
lottieval.add_keyframe(0, v)
|
||||
lottieval.add_keyframe(self.animation.out_point, v)
|
||||
|
||||
def _convert_easing_part(self, interp: api.Interpolation):
|
||||
if interp == api.Interpolation.Linear:
|
||||
return easing.Linear()
|
||||
return easing.Sigmoid()
|
||||
|
||||
def _convert_easing(self, start: api.Interpolation, end: api.Interpolation):
|
||||
if api.Interpolation.Constant in (start, end):
|
||||
return easing.Jump()
|
||||
if start == end:
|
||||
return self._convert_easing_part(start)
|
||||
return easing.Split(self._convert_easing_part(start), self._convert_easing_part(end))
|
||||
|
||||
def _convert_animatable(self, v: ast.SifAstNode, lot: objects.properties.AnimatableMixin):
|
||||
if self._animated(v):
|
||||
if len(v.keyframes) == 1:
|
||||
lot.value = self._convert_ast_value(v.keyframes[0].value)
|
||||
else:
|
||||
for i, kf in enumerate(v.keyframes):
|
||||
if i+1 < len(v.keyframes):
|
||||
start = kf.after
|
||||
end = v.keyframes[i+1].before
|
||||
ease = self._convert_easing(start, end)
|
||||
else:
|
||||
ease = easing.Linear()
|
||||
|
||||
lot.add_keyframe(self._time(kf.time), self._convert_ast_value(kf.value), ease)
|
||||
else:
|
||||
lot.value = self._convert_ast_value(v)
|
||||
return lot
|
||||
|
||||
def _convert_ast_value(self, v):
|
||||
if isinstance(v, ast.SifRadialComposite):
|
||||
return self._polar(v.radius.value, v.theta.value, 1)
|
||||
elif isinstance(v, ast.SifValue):
|
||||
return v.value
|
||||
elif isinstance(v, ast.SifVectorComposite):
|
||||
return NVector(v.x.value, v.y.value)
|
||||
else:
|
||||
return v
|
||||
|
||||
def _converted_vector_values(self, v):
|
||||
if isinstance(v, ast.SifRadialComposite):
|
||||
return [self._convert_scalar(v.radius), self._convert_scalar(v.theta)]
|
||||
return self._convert_vector(v)
|
||||
|
||||
def _convert_color(self, v: ast.SifAstNode):
|
||||
return self._adjust_animated(
|
||||
self._convert_animatable(v, objects.ColorValue()),
|
||||
self._color_gamma
|
||||
)
|
||||
|
||||
def _convert_vector(self, v: ast.SifAstNode):
|
||||
return self._convert_animatable(v, objects.MultiDimensional())
|
||||
|
||||
def _convert_scalar(self, v: ast.SifAstNode):
|
||||
return self._convert_animatable(v, objects.Value())
|
||||
|
||||
def _color_gamma(self, color):
|
||||
color = color.clone()
|
||||
for i in range(3):
|
||||
color[i] = color[i] ** (1/self.gamma[i])
|
||||
return color
|
||||
|
||||
def _adjust_animated(self, lottieval, transform):
|
||||
if lottieval.animated:
|
||||
for kf in lottieval.keyframes:
|
||||
if kf.start is not None:
|
||||
kf.start = transform(kf.start)
|
||||
if kf.end is not None:
|
||||
kf.end = transform(kf.end)
|
||||
else:
|
||||
lottieval.value = transform(lottieval.value)
|
||||
return lottieval
|
||||
|
||||
def _adjust_scalar(self, lottieval: objects.Value):
|
||||
return self._adjust_animated(lottieval, self._scalar_mult)
|
||||
|
||||
def _adjust_angle(self, lottieval: objects.Value):
|
||||
return self._adjust_animated(lottieval, lambda x: -x)
|
||||
|
||||
def _adjust_add_dimension(self, lottieval, transform):
|
||||
to_val = objects.MultiDimensional()
|
||||
to_val.animated = lottieval.animated
|
||||
if lottieval.animated:
|
||||
to_val.keyframes = []
|
||||
for kf in lottieval.keyframes:
|
||||
if kf.start is not None:
|
||||
kf.start = transform(kf.start[0])
|
||||
if kf.end is not None:
|
||||
kf.end = transform(kf.end[0])
|
||||
to_val.keyframes.append(kf)
|
||||
else:
|
||||
to_val.value = transform(lottieval.value)
|
||||
return to_val
|
||||
|
||||
def _scalar_mult(self, x):
|
||||
return x * 60
|
||||
|
||||
def _adjust_coords(self, lottieval: objects.MultiDimensional):
|
||||
return self._adjust_animated(lottieval, self._coord)
|
||||
|
||||
def _coord(self, val: NVector):
|
||||
return NVector(
|
||||
self.target_size.x * (val.x / (self.view_p2.x - self.view_p1.x) + 0.5),
|
||||
self.target_size.y * (val.y / (self.view_p2.y - self.view_p1.y) + 0.5),
|
||||
)
|
||||
|
||||
def _convert_polygon(self, layer: api.PolygonLayer):
|
||||
lot = objects.Path()
|
||||
animatables = [self._convert_vector(layer.origin)] + [
|
||||
self._convert_vector(p)
|
||||
for p in layer.points
|
||||
]
|
||||
animated = any(x.animated for x in animatables)
|
||||
if not animated:
|
||||
lot.shape.value = self._polygon([x.value for x in animatables[1:]], animatables[0].value)
|
||||
else:
|
||||
for values in self._mix_animations(*animatables):
|
||||
time = values[0]
|
||||
origin = values[1]
|
||||
points = values[2:]
|
||||
lot.shape.add_keyframe(time, self._polygon(points, origin))
|
||||
return lot
|
||||
|
||||
def _polygon(self, points, origin):
|
||||
bezier = objects.Bezier()
|
||||
bezier.closed = True
|
||||
for point in points:
|
||||
bezier.add_point(self._coord(point+origin))
|
||||
return bezier
|
||||
|
||||
def _convert_bline(self, layer: api.AbstractOutline):
|
||||
lot = objects.Path()
|
||||
closed = layer.bline.loop
|
||||
animatables = [
|
||||
self._convert_vector(layer.origin)
|
||||
]
|
||||
for p in layer.bline.points:
|
||||
animatables += [
|
||||
self._convert_vector(p.point),
|
||||
self._convert_scalar(p.t1.radius) if hasattr(p.t1, "radius") else objects.Value(0),
|
||||
self._convert_scalar(p.t1.theta) if hasattr(p.t1, "radius") else objects.Value(0),
|
||||
self._convert_scalar(p.t2.radius) if hasattr(p.t2, "radius") else objects.Value(0),
|
||||
self._convert_scalar(p.t2.theta) if hasattr(p.t2, "radius") else objects.Value(0)
|
||||
]
|
||||
animated = any(x.animated for x in animatables)
|
||||
if not animated:
|
||||
lot.shape.value = self._bezier(
|
||||
closed, [x.value for x in animatables[1:]], animatables[0].value, layer.bline.points
|
||||
)
|
||||
else:
|
||||
for values in self._mix_animations(*animatables):
|
||||
time = values[0]
|
||||
origin = values[1]
|
||||
values = values[2:]
|
||||
lot.shape.add_keyframe(time, self._bezier(closed, values, origin, layer.bline.points))
|
||||
return lot
|
||||
|
||||
def _bezier(self, closed, values, origin, points):
|
||||
chunk_size = 5
|
||||
bezier = objects.Bezier()
|
||||
bezier.closed = closed
|
||||
for i in range(0, len(values), chunk_size):
|
||||
point, r1, a1, r2, a2 = values[i:i+chunk_size]
|
||||
sifvert = point+origin
|
||||
vert = self._coord(sifvert)
|
||||
if not points[i//chunk_size].split_radius.value:
|
||||
r2 = r1
|
||||
if not points[i//chunk_size].split_angle.value:
|
||||
a2 = a1
|
||||
t1 = self._coord(sifvert + self._polar(r1, a1, 1)) - vert
|
||||
t2 = self._coord(sifvert + self._polar(r2, a2, 2)) - vert
|
||||
bezier.add_point(vert, t1, t2)
|
||||
return bezier
|
||||
|
||||
def _polar(self, radius, angle, dir):
|
||||
offset_angle = 0
|
||||
if dir == 1:
|
||||
offset_angle += 180
|
||||
return PolarVector(radius/3, (angle+offset_angle) * math.pi / 180)
|
||||
|
||||
def _convert_transform_down(self, tl: api.TransformDown):
|
||||
group = objects.Group()
|
||||
self._set_name(group, tl)
|
||||
|
||||
if isinstance(tl, api.TranslateLayer):
|
||||
group.transform.anchor_point.value = self.target_size / 2
|
||||
group.transform.position = self._adjust_coords(self._convert_vector(tl.origin))
|
||||
elif isinstance(tl, api.RotateLayer):
|
||||
group.transform.anchor_point = self._adjust_coords(self._convert_vector(tl.origin))
|
||||
group.transform.position = group.transform.anchor_point.clone()
|
||||
group.transform.rotation = self._adjust_angle(self._convert_scalar(tl.amount))
|
||||
elif isinstance(tl, api.ScaleLayer):
|
||||
group.transform.anchor_point = self._adjust_coords(self._convert_vector(tl.center))
|
||||
group.transform.position = group.transform.anchor_point.clone()
|
||||
group.transform.scale = self._adjust_add_dimension(
|
||||
self._convert_scalar(tl.amount),
|
||||
self._zoom_to_scale
|
||||
)
|
||||
|
||||
return group
|
||||
|
||||
def _zoom_to_scale(self, value):
|
||||
zoom = math.e ** value * 100
|
||||
return NVector(zoom, zoom)
|
||||
|
||||
def _set_name(self, lottie, sif):
|
||||
lottie.name = sif.desc if sif.desc is not None else sif.__class__.__name__
|
||||
|
||||
def _convert_gradient(self, layer: api.GradientLayer, parent):
|
||||
group = objects.Group()
|
||||
|
||||
parent_shapes = parent.shapes
|
||||
parent.shapes = []
|
||||
if isinstance(parent, objects.Group):
|
||||
parent.shapes.append(parent_shapes[-1])
|
||||
|
||||
self._gradient_gather_shapes(parent_shapes, group)
|
||||
|
||||
gradient = objects.GradientFill()
|
||||
self._set_name(gradient, layer)
|
||||
group.add_shape(gradient)
|
||||
gradient.colors = self._convert_gradient_stops(layer.gradient)
|
||||
gradient.opacity = self._adjust_animated(
|
||||
self._convert_scalar(layer.amount),
|
||||
lambda x: x * 100
|
||||
)
|
||||
|
||||
if isinstance(layer, api.LinearGradient):
|
||||
gradient.start_point = self._adjust_coords(self._convert_vector(layer.p1))
|
||||
gradient.end_point = self._adjust_coords(self._convert_vector(layer.p2))
|
||||
gradient.gradient_type = objects.GradientType.Linear
|
||||
elif isinstance(layer, api.RadialGradient):
|
||||
gradient.gradient_type = objects.GradientType.Radial
|
||||
gradient.start_point = self._adjust_coords(self._convert_vector(layer.center))
|
||||
radius = self._adjust_animated(self._convert_scalar(layer.radius), lambda x: x*45)
|
||||
if not radius.animated and not gradient.start_point.animated:
|
||||
gradient.end_point.value = gradient.start_point.value + NVector(radius.value, radius.value)
|
||||
else:
|
||||
for time, c, r in self._mix_animations(gradient.start_point.clone(), radius):
|
||||
gradient.end_point.add_keyframe(time, c + NVector(r + r))
|
||||
|
||||
return group
|
||||
|
||||
def _gradient_gather_shapes(self, shapes, output: objects.Group):
|
||||
for shape in shapes:
|
||||
if isinstance(shape, objects.Shape):
|
||||
output.add_shape(shape)
|
||||
elif isinstance(shape, objects.Group):
|
||||
self._gradient_gather_shapes(shape.shapes, output)
|
||||
|
||||
def _convert_gradient_stops(self, sif_gradient):
|
||||
stops = objects.GradientColors()
|
||||
if not self._animated(sif_gradient):
|
||||
stops.set_stops(self._flatten_gradient_colors(sif_gradient.value))
|
||||
stops.count = len(sif_gradient.value)
|
||||
else:
|
||||
# TODO easing
|
||||
for kf in sif_gradient.keyframes:
|
||||
stops.add_keyframe(self._time(kf.time), self._flatten_gradient_colors(kf.value))
|
||||
stops.count = len(kf.value)
|
||||
|
||||
return stops
|
||||
|
||||
def _flatten_gradient_colors(self, stops):
|
||||
return [
|
||||
(stop.pos, self._color_gamma(stop.color))
|
||||
for stop in stops
|
||||
]
|
||||
|
||||
def _convert_text(self, layer: api.TextLayer):
|
||||
shape = font.FontShape(layer.text.value, font.FontStyle(layer.family.value, 110, font.TextJustify.Center))
|
||||
shape.refresh()
|
||||
trans = shape.wrapped.transform
|
||||
trans.anchor_point.value = shape.wrapped.bounding_box().center()
|
||||
trans.anchor_point.value.x /= 2
|
||||
trans.position = self._adjust_coords(self._convert_vector(layer.origin))
|
||||
trans.scale = self._adjust_animated(
|
||||
self._convert_vector(layer.size),
|
||||
lambda v: v * 100
|
||||
)
|
||||
return shape
|
||||
@@ -0,0 +1,7 @@
|
||||
from ..tgs import open_maybe_gzipped
|
||||
|
||||
|
||||
def parse_sif_file(file):
|
||||
from .converter import convert
|
||||
from . import api
|
||||
return convert(open_maybe_gzipped(file, api.Canvas.from_xml_file))
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,165 @@
|
||||
from xml.dom import minidom
|
||||
import enum
|
||||
from uuid import uuid4
|
||||
|
||||
from lottie.nvector import NVector
|
||||
from lottie.parsers.sif.xml.utils import xml_text, str_to_bool
|
||||
from lottie.parsers.sif.xml.utils import xml_child_elements, value_from_xml_string, xml_make_text, value_to_xml_string
|
||||
from lottie.parsers.sif.sif.frame_time import FrameTime
|
||||
|
||||
|
||||
class ObjectRegistry:
|
||||
def __init__(self):
|
||||
self.registry = {}
|
||||
|
||||
def register_as(self, object, key):
|
||||
self.registry[key] = object
|
||||
|
||||
def register(self, object):
|
||||
guid = getattr(object, "guid", None)
|
||||
if guid is None:
|
||||
guid = self.guid()
|
||||
object.guid = guid
|
||||
self.registry[guid] = object
|
||||
|
||||
@classmethod
|
||||
def guid(cls):
|
||||
return str(uuid4()).replace("-", "").upper()
|
||||
|
||||
def get_object(self, guid):
|
||||
return self.registry[guid]
|
||||
|
||||
|
||||
def noop(x):
|
||||
return x
|
||||
|
||||
|
||||
class TypeDescriptor:
|
||||
_type_tag_names = {
|
||||
"bone_object": "bone"
|
||||
}
|
||||
|
||||
def __init__(self, typename, default=None, type_wrapper=noop):
|
||||
self.typename = typename
|
||||
self.type_wrapper = type_wrapper
|
||||
self.default_value = default
|
||||
|
||||
def value_to_xml_element(self, value, dom: minidom.Document):
|
||||
element = dom.createElement(self.tag_name)
|
||||
|
||||
if self.typename == "vector":
|
||||
element.appendChild(xml_make_text(dom, "x", str(value.x)))
|
||||
element.appendChild(xml_make_text(dom, "y", str(value.y)))
|
||||
if hasattr(value, "guid"):
|
||||
element.setAttribute("guid", value.guid)
|
||||
elif self.typename == "color":
|
||||
element.appendChild(xml_make_text(dom, "r", str(value[0])))
|
||||
element.appendChild(xml_make_text(dom, "g", str(value[1])))
|
||||
element.appendChild(xml_make_text(dom, "b", str(value[2])))
|
||||
element.appendChild(xml_make_text(dom, "a", str(value[3])))
|
||||
if hasattr(value, "guid"):
|
||||
element.setAttribute("guid", value.guid)
|
||||
elif self.typename == "gradient":
|
||||
for point in value:
|
||||
element.appendChild(point.to_dom(dom))
|
||||
elif self.typename == "bool":
|
||||
element.setAttribute("value", "true" if value else "false")
|
||||
elif self.typename == "bone_object":
|
||||
element.setAttribute("guid", value.guid)
|
||||
element.setAttribute("type", self.typename)
|
||||
elif self.typename == "string":
|
||||
element.appendChild(dom.createTextNode(value))
|
||||
else:
|
||||
if isinstance(value, enum.Enum):
|
||||
value = value.value
|
||||
element.setAttribute("value", str(value))
|
||||
|
||||
return element
|
||||
|
||||
@property
|
||||
def tag_name(self):
|
||||
return self._type_tag_names.get(self.typename, self.typename)
|
||||
|
||||
def value_from_xml_element(self, xml: minidom.Element, registry: ObjectRegistry):
|
||||
if xml.tagName != self.tag_name:
|
||||
raise ValueError("Wrong value type (%s instead of %s)" % (xml.tagName, self.tag_name))
|
||||
|
||||
guid = xml.getAttribute("guid")
|
||||
if guid and guid in registry.registry:
|
||||
value = registry.registry[guid]
|
||||
elif self.typename == "vector":
|
||||
value = NVector(
|
||||
float(xml_text(xml.getElementsByTagName("x")[0])),
|
||||
float(xml_text(xml.getElementsByTagName("y")[0]))
|
||||
)
|
||||
if xml.getAttribute("guid"):
|
||||
value.guid = xml.getAttribute("guid")
|
||||
registry.register(value)
|
||||
elif self.typename == "color":
|
||||
value = NVector(
|
||||
float(xml_text(xml.getElementsByTagName("r")[0])),
|
||||
float(xml_text(xml.getElementsByTagName("g")[0])),
|
||||
float(xml_text(xml.getElementsByTagName("b")[0])),
|
||||
float(xml_text(xml.getElementsByTagName("a")[0]))
|
||||
)
|
||||
elif self.typename == "gradient":
|
||||
value = [
|
||||
GradientPoint.from_dom(sub, registry)
|
||||
for sub in xml_child_elements(xml, GradientPoint.type.typename)
|
||||
]
|
||||
elif self.typename == "real" or self.typename == "angle":
|
||||
value = float(xml.getAttribute("value"))
|
||||
elif self.typename == "integer":
|
||||
value = int(xml.getAttribute("value"))
|
||||
elif self.typename == "time":
|
||||
value = FrameTime.parse_string(xml.getAttribute("value"), registry)
|
||||
elif self.typename == "bool":
|
||||
value = str_to_bool(xml.getAttribute("value"))
|
||||
elif self.typename == "string":
|
||||
return xml_text(xml)
|
||||
elif self.typename == "bone_object":
|
||||
# Already done above but this forces the guid to be present
|
||||
return registry.get_object(xml.getAttribute("guid"))
|
||||
else:
|
||||
raise ValueError("Unsupported type %s" % self.typename)
|
||||
|
||||
return self.type_wrapper(value)
|
||||
|
||||
|
||||
class GradientPoint:
|
||||
type = TypeDescriptor("color")
|
||||
|
||||
def __init__(self, pos: float, color: NVector):
|
||||
self.pos = pos
|
||||
self.color = color
|
||||
|
||||
def to_dom(self, dom: minidom.Document):
|
||||
element = self.type.value_to_xml_element(self.color, dom)
|
||||
element.setAttribute("pos", value_to_xml_string(self.pos, float))
|
||||
return element
|
||||
|
||||
@classmethod
|
||||
def from_dom(cls, xml: minidom.Element, registry: ObjectRegistry):
|
||||
return GradientPoint(
|
||||
value_from_xml_string(xml.getAttribute("pos"), float, registry),
|
||||
cls.type.value_from_xml_element(xml, registry)
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "<GradientPoint %s %s>" % (self.pos, self.color)
|
||||
|
||||
|
||||
class SifNodeMeta(type):
|
||||
def __new__(cls, name, bases, attr):
|
||||
props = []
|
||||
for base in bases:
|
||||
if type(base) == cls:
|
||||
props += base._nodes
|
||||
attr["_nodes"] = props + attr.get("_nodes", [])
|
||||
if "_tag" not in attr:
|
||||
attr["_tag"] = name.lower()
|
||||
attr["_nodemap"] = {
|
||||
node.att_name: node
|
||||
for node in attr["_nodes"]
|
||||
}
|
||||
return super().__new__(cls, name, bases, attr)
|
||||
@@ -0,0 +1,9 @@
|
||||
import enum
|
||||
|
||||
|
||||
class Smooth(enum.Enum):
|
||||
NearestNeighbour = 0
|
||||
Linear = 1
|
||||
Cosine = 2
|
||||
Spline = 3
|
||||
Cubic = 4
|
||||
@@ -0,0 +1,46 @@
|
||||
import enum
|
||||
|
||||
|
||||
class FrameTime:
|
||||
class Unit(enum.Enum):
|
||||
Frame = "f"
|
||||
Seconds = "s"
|
||||
|
||||
def __init__(self, value, unit):
|
||||
self.value = value
|
||||
self.unit = unit
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.value == other.value and self.unit == other.unit
|
||||
|
||||
def __ne__(self, other):
|
||||
return self.value == other.value and self.unit == other.unit
|
||||
|
||||
def __str__(self):
|
||||
return "%s%s" % (self.value, self.unit.value)
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s %s>" % (self.__class__.__name__, self)
|
||||
|
||||
@classmethod
|
||||
def frame(cls, amount):
|
||||
return cls(amount, cls.Unit.Frame)
|
||||
|
||||
@classmethod
|
||||
def seconds(cls, amount):
|
||||
return cls(amount, cls.Unit.Seconds)
|
||||
|
||||
@classmethod
|
||||
def parse_string(cls, value_str, canvas):
|
||||
if " " in value_str:
|
||||
value = 0
|
||||
unit = cls.Unit.Frame
|
||||
for sub in value_str.split():
|
||||
sv = float(sub[:-1])
|
||||
if sub[-1] == "s":
|
||||
sv *= canvas.fps
|
||||
value += sv
|
||||
else:
|
||||
value = float(value_str[:-1])
|
||||
unit = cls.Unit(value_str[-1])
|
||||
return FrameTime(value, unit)
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,160 @@
|
||||
from xml.dom import minidom
|
||||
import copy
|
||||
import enum
|
||||
|
||||
from .core_nodes import XmlDescriptor, XmlSimpleElement, ValueReference
|
||||
from .utils import *
|
||||
from lottie.nvector import NVector
|
||||
from lottie.parsers.sif.ast_impl.base import SifAstNode, SifValue
|
||||
from lottie.parsers.sif.sif.core import ObjectRegistry, TypeDescriptor, noop
|
||||
|
||||
|
||||
class XmlAnimatable(XmlDescriptor):
|
||||
def __init__(self, name, typename, default=None, type_wrapper=noop):
|
||||
super().__init__(name)
|
||||
self.type = TypeDescriptor(typename, default, type_wrapper)
|
||||
|
||||
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry, param: TypeDescriptor = None):
|
||||
cn = xml_first_element_child(parent, self.name)
|
||||
if cn:
|
||||
value = SifAstNode.from_dom(xml_first_element_child(cn), self.type_for(param), registry)
|
||||
else:
|
||||
value = self.default()
|
||||
|
||||
setattr(obj, self.att_name, value)
|
||||
|
||||
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document, type: TypeDescriptor = None):
|
||||
value = getattr(obj, self.att_name)
|
||||
if isinstance(value, SifValue) and isinstance(value.value, ValueReference):
|
||||
parent.setAttribute(self.name, ":" + value.value.id)
|
||||
return
|
||||
param = parent.appendChild(dom.createElement(self.name))
|
||||
param.appendChild(value.to_dom(dom, self.type_for(type)))
|
||||
return param
|
||||
|
||||
def from_python(self, value):
|
||||
if not isinstance(value, SifAstNode):
|
||||
raise ValueError("%s isn't a valid value for %s" % (value, self.name))
|
||||
return value
|
||||
|
||||
def default(self):
|
||||
return SifValue(copy.deepcopy(self.type.default_value))
|
||||
|
||||
def type_for(self, param: TypeDescriptor):
|
||||
if param is not None and self.type.typename == "_recurse":
|
||||
return param
|
||||
return self.type
|
||||
|
||||
|
||||
class XmlParam(XmlDescriptor):
|
||||
def __init__(self, name, typename, default=None, type_wrapper=noop, static=False):
|
||||
super().__init__(name)
|
||||
self.type = TypeDescriptor(typename, default, type_wrapper)
|
||||
self.static = static
|
||||
|
||||
def _def(self):
|
||||
from ..api import Def
|
||||
return Def
|
||||
|
||||
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
|
||||
for cn in xml_child_elements(parent, "param"):
|
||||
if cn.getAttribute("name") == self.name:
|
||||
use = cn.getAttribute("use")
|
||||
if use:
|
||||
value = registry.get_object(use)
|
||||
else:
|
||||
value_node = xml_first_element_child(cn)
|
||||
if self.static:
|
||||
value = self.type.value_from_xml_element(value_node, registry)
|
||||
else:
|
||||
value = SifAstNode.from_dom(value_node, self.type, registry)
|
||||
break
|
||||
else:
|
||||
value = self.default()
|
||||
|
||||
setattr(obj, self.att_name, value)
|
||||
|
||||
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
|
||||
param = parent.appendChild(dom.createElement("param"))
|
||||
param.setAttribute("name", self.name)
|
||||
value = getattr(obj, self.att_name)
|
||||
if isinstance(value, self._def()):
|
||||
param.setAttribute("use", ":" + value.id)
|
||||
else:
|
||||
if self.static:
|
||||
elem = self.type.value_to_xml_element(value, dom)
|
||||
else:
|
||||
elem = value.to_dom(dom, self.type)
|
||||
param.appendChild(elem)
|
||||
return param
|
||||
|
||||
def from_python(self, value):
|
||||
if self.static:
|
||||
return self.type.type_wrapper(value)
|
||||
if not isinstance(value, (SifAstNode, self._def())):
|
||||
raise ValueError("%s isn't a valid value for %s" % (value, self.name))
|
||||
return value
|
||||
|
||||
def default(self):
|
||||
if self.static:
|
||||
return copy.deepcopy(self.type.default_value)
|
||||
return SifValue(copy.deepcopy(self.type.default_value))
|
||||
|
||||
|
||||
class XmlDynamicListParam(XmlDescriptor):
|
||||
_tag = "dynamic_list"
|
||||
|
||||
def __init__(self, name, typename, att_name=None):
|
||||
super().__init__(name)
|
||||
self.type = TypeDescriptor(typename)
|
||||
if att_name is not None:
|
||||
self.att_name = att_name
|
||||
|
||||
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
|
||||
param = parent.appendChild(dom.createElement("param"))
|
||||
param.setAttribute("name", self.name)
|
||||
dyl = param.appendChild(dom.createElement(self._tag))
|
||||
dyl.setAttribute("type", self.type.typename)
|
||||
values = getattr(obj, self.att_name)
|
||||
for val in values:
|
||||
entry = dyl.appendChild(dom.createElement("entry"))
|
||||
entry.appendChild(self._value_to_dom(val, dom))
|
||||
|
||||
def _value_to_dom(self, val, dom: minidom.Document):
|
||||
return val.to_dom(dom, self.type)
|
||||
|
||||
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
|
||||
values = []
|
||||
|
||||
for cn in xml_child_elements(parent, "param"):
|
||||
if cn.getAttribute("name") == self.name:
|
||||
list = xml_first_element_child(cn)
|
||||
if list.getAttribute("type") != self.type.typename:
|
||||
raise ValueError(
|
||||
"Wrong type for %s: got %s instead of %s" %
|
||||
(self.name, self.type.typename, list.getAttribute("type"))
|
||||
)
|
||||
for entry in xml_child_elements(list, "entry"):
|
||||
values.append(self._value_from_dom(xml_first_element_child(entry), registry))
|
||||
break
|
||||
|
||||
setattr(obj, self.att_name, values)
|
||||
|
||||
def _value_from_dom(self, element, registry):
|
||||
return SifAstNode.from_dom(element, self.type, registry)
|
||||
|
||||
def from_python(self, value):
|
||||
return value
|
||||
|
||||
def default(self):
|
||||
return []
|
||||
|
||||
|
||||
class XmlStaticListParam(XmlDynamicListParam):
|
||||
_tag = "static_list"
|
||||
|
||||
def _value_to_dom(self, val, dom: minidom.Document):
|
||||
return self.type.value_to_xml_element(val, dom)
|
||||
|
||||
def _value_from_dom(self, element: minidom.Element, registry: ObjectRegistry):
|
||||
return self.type.value_from_xml_element(element, registry)
|
||||
@@ -0,0 +1,142 @@
|
||||
from xml.dom import minidom
|
||||
import copy
|
||||
from uuid import uuid4
|
||||
|
||||
from .utils import *
|
||||
from lottie.parsers.sif.sif.core import ObjectRegistry
|
||||
|
||||
|
||||
class XmlDescriptor:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.att_name = name.replace("-", "_").replace("[", "_").replace("]", "").replace(".", "_")
|
||||
|
||||
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
|
||||
raise NotImplementedError
|
||||
|
||||
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
|
||||
raise NotImplementedError
|
||||
|
||||
def from_python(self, value):
|
||||
raise NotImplementedError
|
||||
|
||||
def initialize_object(self, dict, obj):
|
||||
if self.att_name in dict:
|
||||
setattr(obj, self.att_name, self.from_python(dict[self.att_name]))
|
||||
else:
|
||||
setattr(obj, self.att_name, self.default())
|
||||
|
||||
def clean(self, value):
|
||||
return self.from_python(value)
|
||||
|
||||
def default(self):
|
||||
return None
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%r)" % (self.__class__.__name__, self.name)
|
||||
|
||||
|
||||
class TypedXmlDescriptor(XmlDescriptor):
|
||||
def __init__(self, name, type=str, default_value=None, att_name=None):
|
||||
super().__init__(name)
|
||||
self.type = type
|
||||
self.default_value = default_value
|
||||
if att_name is not None:
|
||||
self.att_name = att_name
|
||||
|
||||
def from_python(self, value):
|
||||
if value is None and self.default_value is None:
|
||||
return None
|
||||
if not value_isinstance(value, self.type):
|
||||
return self.type(value)
|
||||
return value
|
||||
|
||||
def default(self):
|
||||
return copy.deepcopy(self.default_value)
|
||||
|
||||
|
||||
class ValueReference:
|
||||
def __init__(self, id, value=None):
|
||||
self.value = value
|
||||
self.id = id
|
||||
|
||||
@classmethod
|
||||
def from_registry(cls, id, registry: ObjectRegistry):
|
||||
return cls(id, registry.get_object(id))
|
||||
|
||||
|
||||
class XmlAttribute(TypedXmlDescriptor):
|
||||
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
|
||||
xml_str = parent.getAttribute(self.name)
|
||||
if xml_str:
|
||||
if xml_str.startswith(":") and xml_str[1:] in registry.registry:
|
||||
value = ValueReference.from_registry(xml_str[1:], registry)
|
||||
else:
|
||||
value = value_from_xml_string(xml_str, self.type, registry)
|
||||
setattr(obj, self.att_name, value)
|
||||
|
||||
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
|
||||
value = getattr(obj, self.att_name)
|
||||
if value is not None:
|
||||
if isinstance(value, ValueReference):
|
||||
xml_str = ":" + value.id
|
||||
else:
|
||||
xml_str = value_to_xml_string(value, self.type)
|
||||
parent.setAttribute(self.name, xml_str)
|
||||
|
||||
|
||||
class XmlFixedAttribute(XmlDescriptor):
|
||||
def __init__(self, name, value, type=str):
|
||||
super().__init__(name)
|
||||
self.value = value
|
||||
self.type = type
|
||||
|
||||
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
|
||||
parent.setAttribute(self.name, value_to_xml_string(self.value, self.type))
|
||||
|
||||
def from_python(self, value):
|
||||
if value != self.value:
|
||||
raise ValueError("Value of %s should be %s, got %s" % (self.name, self.value, value))
|
||||
return value
|
||||
|
||||
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
|
||||
xml_str = parent.getAttribute(self.name)
|
||||
setattr(obj, self.att_name, value_from_xml_string(xml_str, self.type, registry))
|
||||
|
||||
def default(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class XmlSimpleElement(TypedXmlDescriptor):
|
||||
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
|
||||
cn = xml_first_element_child(parent, self.name, allow_none=True)
|
||||
if cn:
|
||||
value = value_from_xml_string(xml_text(cn), self.type, registry)
|
||||
else:
|
||||
value = self.default_value
|
||||
|
||||
setattr(obj, self.att_name, value)
|
||||
|
||||
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
|
||||
value = getattr(obj, self.att_name)
|
||||
if value is not None:
|
||||
parent.appendChild(xml_make_text(dom, self.name, value_to_xml_string(value, self.type)))
|
||||
|
||||
|
||||
class XmlMeta(TypedXmlDescriptor):
|
||||
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
|
||||
for cn in xml_child_elements(parent, "meta"):
|
||||
if cn.getAttribute("name") == self.name:
|
||||
value = value_from_xml_string(cn.getAttribute("content"), self.type, registry)
|
||||
break
|
||||
else:
|
||||
value = self.default_value
|
||||
|
||||
setattr(obj, self.att_name, value)
|
||||
|
||||
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
|
||||
value = getattr(obj, self.att_name)
|
||||
if value is not None:
|
||||
meta = parent.appendChild(dom.createElement("meta"))
|
||||
meta.setAttribute("name", self.name)
|
||||
meta.setAttribute("content", value_to_xml_string(value, self.type))
|
||||
@@ -0,0 +1,85 @@
|
||||
from xml.dom import minidom
|
||||
from distutils.util import strtobool
|
||||
|
||||
from lottie.nvector import NVector
|
||||
from lottie.parsers.sif.sif.frame_time import FrameTime
|
||||
|
||||
|
||||
class _tag:
|
||||
def __init__(self, type):
|
||||
self.type = type
|
||||
|
||||
def __call__(self, v):
|
||||
return self.type(v)
|
||||
|
||||
|
||||
bool_str = _tag(bool)
|
||||
|
||||
|
||||
def str_to_bool(strval):
|
||||
return bool(strtobool(strval))
|
||||
|
||||
|
||||
def value_from_xml_string(xml_str, type, registry):
|
||||
if type in (bool_str, bool):
|
||||
return str_to_bool(xml_str)
|
||||
elif type is NVector:
|
||||
return NVector(*map(float, xml_str.split()))
|
||||
if type is FrameTime:
|
||||
return FrameTime.parse_string(xml_str, registry)
|
||||
return type(xml_str)
|
||||
|
||||
|
||||
def value_to_xml_string(value, type):
|
||||
if type is bool:
|
||||
return "1" if value else "0"
|
||||
if type is bool_str:
|
||||
return "true" if value else "false"
|
||||
elif type is NVector:
|
||||
return " ".join(map(str, value))
|
||||
return str(value)
|
||||
|
||||
|
||||
def value_isinstance(value, type):
|
||||
if isinstance(type, _tag):
|
||||
type = type.type
|
||||
return isinstance(value, type)
|
||||
|
||||
|
||||
def xml_text(node):
|
||||
return "".join(
|
||||
x.nodeValue
|
||||
for x in node.childNodes
|
||||
if x.nodeType in {minidom.Node.TEXT_NODE, minidom.Node.CDATA_SECTION_NODE}
|
||||
)
|
||||
|
||||
|
||||
def xml_make_text(dom: minidom.Document, tag_name, text):
|
||||
e = dom.createElement(tag_name)
|
||||
e.appendChild(dom.createTextNode(text))
|
||||
return e
|
||||
|
||||
|
||||
def xml_element_matches(ch: minidom.Node, tagname=None):
|
||||
if ch.nodeType != minidom.Node.ELEMENT_NODE:
|
||||
return False
|
||||
|
||||
if tagname is not None and ch.tagName != tagname:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def xml_child_elements(xml: minidom.Node, tagname=None):
|
||||
for ch in xml.childNodes:
|
||||
if xml_element_matches(ch, tagname):
|
||||
yield ch
|
||||
|
||||
|
||||
def xml_first_element_child(xml: minidom.Node, tagname=None, allow_none=False):
|
||||
for ch in xml_child_elements(xml, tagname):
|
||||
return ch
|
||||
|
||||
if allow_none:
|
||||
return None
|
||||
raise ValueError("No %s in %s" % (tagname or "child element", getattr(xml, "tagName", "node")))
|
||||
@@ -0,0 +1,217 @@
|
||||
from .utils import *
|
||||
from .core_nodes import XmlDescriptor, ObjectRegistry
|
||||
|
||||
|
||||
class XmlParamSif(XmlDescriptor):
|
||||
def __init__(self, name, child_node, default_ctor=None):
|
||||
super().__init__(name)
|
||||
self.child_node = child_node
|
||||
self.default_ctor = default_ctor or child_node
|
||||
|
||||
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
|
||||
for cn in xml_child_elements(parent, "param"):
|
||||
if cn.getAttribute("name") == self.name:
|
||||
value = self.child_node.from_dom(xml_first_element_child(cn), registry)
|
||||
break
|
||||
else:
|
||||
value = self.default()
|
||||
|
||||
setattr(obj, self.att_name, value)
|
||||
|
||||
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
|
||||
param = parent.appendChild(dom.createElement("param"))
|
||||
param.setAttribute("name", self.name)
|
||||
param.appendChild(getattr(obj, self.att_name).to_dom(dom))
|
||||
return param
|
||||
|
||||
def clean(self, value):
|
||||
if not isinstance(value, self.child_node):
|
||||
raise ValueError("%s isn't a valid value for %s" % (value, self.name))
|
||||
return value
|
||||
|
||||
def default(self):
|
||||
return self.default_ctor()
|
||||
|
||||
|
||||
class SifNodeList:
|
||||
def __init__(self, type):
|
||||
self._items = []
|
||||
self._type = type
|
||||
|
||||
def __len__(self):
|
||||
return len(self._items)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._items)
|
||||
|
||||
def __getitem__(self, name):
|
||||
return self._items[name]
|
||||
|
||||
def __getslice__(self, i, j):
|
||||
return self._items[i:j]
|
||||
|
||||
def __setitem__(self, key, value: "Layer"):
|
||||
self.validate(value)
|
||||
self._items[key] = value
|
||||
|
||||
def append(self, value: "Layer"):
|
||||
self.validate(value)
|
||||
self._items.append(value)
|
||||
|
||||
def __str__(self):
|
||||
return str(self._items)
|
||||
|
||||
def __repr__(self):
|
||||
return "<SifNodeList %s>" % self._items
|
||||
|
||||
def validate(self, value):
|
||||
if not isinstance(value, self._type):
|
||||
raise ValueError("Not a valid object: %s" % value)
|
||||
|
||||
|
||||
class XmlSifElement(XmlDescriptor):
|
||||
def __init__(self, name, child_node, nested=True):
|
||||
super().__init__(name)
|
||||
self.child_node = child_node
|
||||
self.nested = nested
|
||||
|
||||
def default(self):
|
||||
return self.child_node()
|
||||
|
||||
def from_python(self, value):
|
||||
if not isinstance(value, self.child_node):
|
||||
raise ValueError("Invalid value for %s: %s" % (self.name, value))
|
||||
return value
|
||||
|
||||
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
|
||||
cn = xml_first_element_child(parent, self.name, allow_none=True)
|
||||
if cn:
|
||||
if self.nested:
|
||||
element = xml_first_element_child(cn)
|
||||
else:
|
||||
element = cn
|
||||
value = self.child_node.from_dom(element, registry)
|
||||
else:
|
||||
value = self.default()
|
||||
|
||||
setattr(obj, self.att_name, value)
|
||||
|
||||
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
|
||||
value = getattr(obj, self.att_name)
|
||||
if self.nested:
|
||||
node = dom.createElement(self.name)
|
||||
parent.appendChild(node)
|
||||
else:
|
||||
node = parent
|
||||
node.appendChild(value.to_dom(dom))
|
||||
|
||||
|
||||
class XmlList(XmlDescriptor):
|
||||
def __init__(self, child_node, name=None, wrapper_tag=None, tags=None):
|
||||
super().__init__(wrapper_tag or child_node._tag)
|
||||
self.child_node = child_node
|
||||
self.att_name = self.att_name + "s" if name is None else name
|
||||
self.wrapper_tag = wrapper_tag
|
||||
if tags is None:
|
||||
self.tags = {self.name}
|
||||
else:
|
||||
self.tags = tags
|
||||
|
||||
def default(self):
|
||||
return SifNodeList(self.child_node)
|
||||
|
||||
def clean(self, value):
|
||||
return value
|
||||
|
||||
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
|
||||
values = self.default()
|
||||
for cn in xml_child_elements(parent):
|
||||
if cn.tagName in self.tags:
|
||||
value_node = cn
|
||||
if self.wrapper_tag:
|
||||
value_node = xml_first_element_child(cn)
|
||||
values.append(self.child_node.from_dom(value_node, registry))
|
||||
|
||||
setattr(obj, self.att_name, values)
|
||||
|
||||
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
|
||||
for value in getattr(obj, self.att_name):
|
||||
value_node = value.to_dom(dom)
|
||||
if self.wrapper_tag:
|
||||
wrapper = dom.createElement(self.wrapper_tag)
|
||||
wrapper.appendChild(value_node)
|
||||
value_node = wrapper
|
||||
parent.appendChild(value_node)
|
||||
|
||||
|
||||
class XmlWrapper(XmlDescriptor):
|
||||
def __init__(self, name, wrapped: XmlDescriptor):
|
||||
super().__init__(name)
|
||||
self.wrapped = wrapped
|
||||
|
||||
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
|
||||
wrapper = parent.appendChild(dom.createElement(self.name))
|
||||
self.wrapped.to_xml(obj, wrapper, dom)
|
||||
|
||||
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
|
||||
wrapper = xml_first_element_child(parent, self.name, True)
|
||||
if wrapper:
|
||||
return self.wrapped.from_xml(obj, wrapper, registry)
|
||||
return self.default()
|
||||
|
||||
def from_python(self, value):
|
||||
return self.wrapped.from_python(value)
|
||||
|
||||
def initialize_object(self, dict, obj):
|
||||
return self.wrapped.initialize_object(dict, obj)
|
||||
|
||||
def clean(self, value):
|
||||
return self.wrapped.clean(value)
|
||||
|
||||
def default(self):
|
||||
return self.wrapped.default()
|
||||
|
||||
|
||||
class XmlWrapperParam(XmlWrapper):
|
||||
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
|
||||
wrapper = parent.appendChild(dom.createElement("param"))
|
||||
wrapper.setAttribute("name", self.name)
|
||||
self.wrapped.to_xml(obj, wrapper, dom)
|
||||
|
||||
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
|
||||
for wrapper in xml_child_elements(parent, "param"):
|
||||
if wrapper.getAttribute("name") == self.name:
|
||||
return self.wrapped.from_xml(obj, wrapper, registry)
|
||||
return self.default()
|
||||
|
||||
|
||||
class XmlBoneReference(XmlDescriptor):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
|
||||
def to_xml(self, obj, parent: minidom.Element, dom: minidom.Document):
|
||||
value = getattr(obj, self.name, None)
|
||||
if not value:
|
||||
return
|
||||
|
||||
node = parent.appendChild(dom.createElement(self.name))
|
||||
value_node = node.appendChild(dom.createElement("bone_valuenode"))
|
||||
value_node.setAttribute("type", value.type)
|
||||
value_node.setAttribute("guid", value.guid)
|
||||
|
||||
return node
|
||||
|
||||
def from_xml(self, obj, parent: minidom.Element, registry: ObjectRegistry):
|
||||
node = xml_first_element_child(parent, self.name, True)
|
||||
value = None
|
||||
if node:
|
||||
value_node = xml_first_element_child(node, "bone_valuenode", True)
|
||||
if value_node:
|
||||
value = registry.get_object(value_node.getAttribute("guid"))
|
||||
if value.type != value_node.getAttribute("type"):
|
||||
raise ValueError("Bone type %s is not %s" % (value.type, value_node.getAttribute("type")))
|
||||
|
||||
setattr(obj, self.att_name, value)
|
||||
|
||||
def from_python(self, value):
|
||||
return value
|
||||
Reference in New Issue
Block a user