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,719 @@
|
||||
import re
|
||||
import math
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from .handler import SvgHandler, NameMode
|
||||
from ... import objects
|
||||
from ...nvector import NVector
|
||||
from ...utils import restructure
|
||||
from ...utils.transform import TransformMatrix
|
||||
try:
|
||||
from ...utils import font
|
||||
has_font = True
|
||||
except ImportError:
|
||||
has_font = False
|
||||
|
||||
|
||||
class PrecompTime:
|
||||
def __init__(self, pcl: objects.PreCompLayer):
|
||||
self.pcl = pcl
|
||||
|
||||
def get_time_offset(self, time, lot):
|
||||
remap = time
|
||||
if self.pcl.time_remapping:
|
||||
remapf = self.pcl.time_remapping.get_value(time)
|
||||
remap = lot.in_point * (1-remapf) + lot.out_point * remapf
|
||||
|
||||
return remap - self.pcl.start_time
|
||||
|
||||
|
||||
class SvgBuilder(SvgHandler, restructure.AbstractBuilder):
|
||||
merge_paths = True
|
||||
namestart = (
|
||||
r":_A-Za-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF" +
|
||||
r"\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF" +
|
||||
r"\uFDF0-\uFFFD\U00010000-\U000EFFFF"
|
||||
)
|
||||
namenostart = r"-.0-9\xB7\u0300-\u036F\u203F-\u2040"
|
||||
id_re = re.compile("^[%s][%s%s]*$" % (namestart, namenostart, namestart))
|
||||
|
||||
def __init__(self, time=0):
|
||||
super().__init__()
|
||||
self.svg = ElementTree.Element("svg")
|
||||
self.dom = ElementTree.ElementTree(self.svg)
|
||||
self.svg.attrib["xmlns"] = self.ns_map["svg"]
|
||||
self.ids = set()
|
||||
self.idc = 0
|
||||
self.name_mode = NameMode.Inkscape
|
||||
self.actual_time = time
|
||||
self.precomp_times = []
|
||||
self._precomps = {}
|
||||
self._assets = {}
|
||||
self._current_layer = []
|
||||
|
||||
@property
|
||||
def time(self):
|
||||
time = self.actual_time
|
||||
if self.precomp_times:
|
||||
for pct in self.precomp_times:
|
||||
time = pct.get_time_offset(time, self._current_layer[-1])
|
||||
return time
|
||||
|
||||
def gen_id(self, prefix="id"):
|
||||
while True:
|
||||
self.idc += 1
|
||||
id = "%s_%s" % (prefix, self.idc)
|
||||
if id not in self.ids:
|
||||
break
|
||||
self.ids.add(id)
|
||||
return id
|
||||
|
||||
def set_clean_id(self, dom, n):
|
||||
idn = n.replace(" ", "_")
|
||||
if self.id_re.match(idn) and idn not in self.ids:
|
||||
self.ids.add(idn)
|
||||
else:
|
||||
idn = self.gen_id(dom.tag)
|
||||
|
||||
dom.attrib["id"] = idn
|
||||
return idn
|
||||
|
||||
def set_id(self, dom, lottieobj, inkscape_qual=None, force=False):
|
||||
n = getattr(lottieobj, "name", None)
|
||||
if n is None or self.name_mode == NameMode.NoName:
|
||||
if force:
|
||||
id = self.gen_id(dom.tag)
|
||||
dom.attrib["id"] = id
|
||||
return id
|
||||
return None
|
||||
|
||||
idn = self.set_clean_id(dom, n)
|
||||
if inkscape_qual is None:
|
||||
inkscape_qual = self.qualified("inkscape", "label")
|
||||
if inkscape_qual:
|
||||
dom.attrib[inkscape_qual] = n
|
||||
return idn
|
||||
|
||||
def _on_animation(self, animation: objects.Animation):
|
||||
self.svg.attrib["width"] = str(animation.width)
|
||||
self.svg.attrib["height"] = str(animation.height)
|
||||
self.svg.attrib["viewBox"] = "0 0 %s %s" % (animation.width, animation.height)
|
||||
self.svg.attrib["version"] = "1.1"
|
||||
self.set_id(self.svg, animation, self.qualified("sodipodi", "docname"))
|
||||
self.defs = ElementTree.SubElement(self.svg, "defs")
|
||||
if self.name_mode == NameMode.Inkscape:
|
||||
self.svg.attrib[self.qualified("inkscape", "export-xdpi")] = "96"
|
||||
self.svg.attrib[self.qualified("inkscape", "export-ydpi")] = "96"
|
||||
namedview = ElementTree.SubElement(self.svg, self.qualified("sodipodi", "namedview"))
|
||||
namedview.attrib[self.qualified("inkscape", "pagecheckerboard")] = "true"
|
||||
namedview.attrib["borderlayer"] = "true"
|
||||
namedview.attrib["bordercolor"] = "#666666"
|
||||
namedview.attrib["pagecolor"] = "#ffffff"
|
||||
self.svg.attrib["style"] = "fill: none; stroke: none"
|
||||
|
||||
self._current_layer = [animation]
|
||||
return self.svg
|
||||
|
||||
def _mask_to_def(self, mask):
|
||||
svgmask = ElementTree.SubElement(self.defs, "mask")
|
||||
mask_id = self.gen_id()
|
||||
svgmask.attrib["id"] = mask_id
|
||||
svgmask.attrib["mask-type"] = "alpha"
|
||||
path = ElementTree.SubElement(svgmask, "path")
|
||||
path.attrib["d"] = self._bezier_to_d(mask.shape.get_value(self.time))
|
||||
path.attrib["fill"] = "#fff"
|
||||
path.attrib["fill-opacity"] = str(mask.opacity.get_value(self.time) / 100)
|
||||
return mask_id
|
||||
|
||||
def _matte_source_to_def(self, layer_builder):
|
||||
svgmask = ElementTree.SubElement(self.defs, "mask")
|
||||
if not layer_builder.matte_id:
|
||||
layer_builder.matte_id = self.gen_id()
|
||||
svgmask.attrib["id"] = layer_builder.matte_id
|
||||
matte_mode = layer_builder.matte_target.lottie.matte_mode
|
||||
|
||||
mask_type = "alpha"
|
||||
if matte_mode == objects.MatteMode.Luma:
|
||||
mask_type = "luminance"
|
||||
svgmask.attrib["mask-type"] = mask_type
|
||||
return svgmask
|
||||
|
||||
def _on_masks(self, masks):
|
||||
if len(masks) == 1:
|
||||
return self._mask_to_def(masks[0])
|
||||
mask_ids = list(map(self._mask_to_def, masks))
|
||||
mask_def = ElementTree.SubElement(self.defs, "mask")
|
||||
mask_id = self.gen_id()
|
||||
mask_def.attrib["id"] = mask_id
|
||||
g = mask_def
|
||||
for mid in mask_ids:
|
||||
g = ElementTree.SubElement(g, "g")
|
||||
g.attrib["mask"] = "url(#%s)" % mid
|
||||
full = ElementTree.SubElement(g, "rect")
|
||||
full.attrib["fill"] = "#fff"
|
||||
full.attrib["width"] = self.svg.attrib["width"]
|
||||
full.attrib["height"] = self.svg.attrib["height"]
|
||||
full.attrib["x"] = "0"
|
||||
full.attrib["y"] = "0"
|
||||
return mask_id
|
||||
|
||||
def _on_layer(self, layer_builder, dom_parent):
|
||||
lot = layer_builder.lottie
|
||||
self._current_layer.append(lot)
|
||||
|
||||
if not self.precomp_times and (lot.in_point > self.time or lot.out_point < self.time):
|
||||
self._current_layer.pop()
|
||||
return None
|
||||
|
||||
if layer_builder.matte_target:
|
||||
dom_parent = self._matte_source_to_def(layer_builder)
|
||||
|
||||
g = self.group_from_lottie(lot, dom_parent, True)
|
||||
|
||||
if lot.masks:
|
||||
g.attrib["mask"] = "url(#%s)" % self._on_masks(lot.masks)
|
||||
elif layer_builder.matte_source:
|
||||
matte_id = layer_builder.matte_source.matte_id
|
||||
if not matte_id:
|
||||
matte_id = layer_builder.matte_source.matte_id = self.gen_id()
|
||||
g.attrib["mask"] = "url(#%s)" % matte_id
|
||||
|
||||
if isinstance(lot, objects.PreCompLayer):
|
||||
self.precomp_times.append(PrecompTime(lot))
|
||||
|
||||
for layer in self._precomps.get(lot.reference_id, []):
|
||||
self.process_layer(layer, g)
|
||||
|
||||
self.precomp_times.pop()
|
||||
elif isinstance(lot, objects.NullLayer):
|
||||
g.attrib["opacity"] = "1"
|
||||
elif isinstance(lot, objects.ImageLayer):
|
||||
use = ElementTree.SubElement(g, "use")
|
||||
use.attrib[self.qualified("xlink", "href")] = "#" + self._assets[lot.image_id]
|
||||
elif isinstance(lot, objects.TextLayer):
|
||||
self._on_text_layer(g, lot)
|
||||
elif isinstance(lot, objects.SolidColorLayer):
|
||||
rect = ElementTree.SubElement(g, "rect")
|
||||
rect.attrib["width"] = str(lot.width)
|
||||
rect.attrib["height"] = str(lot.height)
|
||||
rect.attrib["fill"] = lot.color
|
||||
|
||||
if not lot.name:
|
||||
g.attrib[self.qualified("inkscape", "label")] = lot.__class__.__name__
|
||||
if layer_builder.shapegroup:
|
||||
g.attrib["style"] = self.group_to_style(layer_builder.shapegroup)
|
||||
self._split_stroke(layer_builder.shapegroup, g, dom_parent)
|
||||
#if lot.hidden:
|
||||
#g.attrib.setdefault("style", "")
|
||||
#g.attrib["style"] += "display: none;"
|
||||
|
||||
return g
|
||||
|
||||
def _on_text_layer(self, g, lot):
|
||||
text = ElementTree.SubElement(g, "text")
|
||||
doc = lot.data.get_value(self.time)
|
||||
if doc:
|
||||
text.attrib["font-family"] = doc.font_family
|
||||
text.attrib["font-size"] = str(doc.font_size)
|
||||
if doc.line_height:
|
||||
text.attrib["line-height"] = "%s%%" % doc.line_height
|
||||
if doc.justify == objects.text.TextJustify.Left:
|
||||
text.attrib["text-align"] = "start"
|
||||
elif doc.justify == objects.text.TextJustify.Center:
|
||||
text.attrib["text-align"] = "center"
|
||||
elif doc.justify == objects.text.TextJustify.Right:
|
||||
text.attrib["text-align"] = "end"
|
||||
|
||||
text.attrib["fill"] = color_to_css(doc.color)
|
||||
text.text = doc.text
|
||||
|
||||
def _on_layer_end(self, out_layer):
|
||||
self._current_layer.pop()
|
||||
|
||||
def _on_precomp(self, id, dom_parent, layers):
|
||||
self._precomps[id] = layers
|
||||
|
||||
def _on_asset(self, asset):
|
||||
if isinstance(asset, objects.assets.Image):
|
||||
img = ElementTree.SubElement(self.defs, "image")
|
||||
xmlid = self.set_clean_id(img, asset.id)
|
||||
self._assets[asset.id] = xmlid
|
||||
if asset.is_embedded:
|
||||
url = asset.image
|
||||
else:
|
||||
url = asset.image_path + asset.image
|
||||
img.attrib[self.qualified("xlink", "href")] = url
|
||||
img.attrib["width"] = str(asset.width)
|
||||
img.attrib["height"] = str(asset.height)
|
||||
|
||||
def _get_value(self, prop, default=NVector(0, 0)):
|
||||
if prop:
|
||||
v = prop.get_value(self.time)
|
||||
else:
|
||||
v = default
|
||||
|
||||
if v is None:
|
||||
return default
|
||||
if isinstance(v, NVector):
|
||||
return v.clone()
|
||||
return v
|
||||
|
||||
def set_transform(self, dom, transform, auto_orient=False):
|
||||
mat = transform.to_matrix(self.time, auto_orient)
|
||||
dom.attrib["transform"] = mat.to_css_2d()
|
||||
|
||||
if transform.opacity is not None:
|
||||
op = transform.opacity.get_value(self.time)
|
||||
if op != 100:
|
||||
dom.attrib["opacity"] = str(op/100)
|
||||
|
||||
def _get_group_stroke(self, group):
|
||||
style = {}
|
||||
if group.stroke:
|
||||
if isinstance(group.stroke, objects.GradientStroke):
|
||||
style["stroke"] = "url(#%s)" % self.process_gradient(group.stroke)
|
||||
else:
|
||||
style["stroke"] = color_to_css(group.stroke.color.get_value(self.time))
|
||||
|
||||
style["stroke-opacity"] = group.stroke.opacity.get_value(self.time) / 100
|
||||
style["stroke-width"] = group.stroke.width.get_value(self.time)
|
||||
if group.stroke.miter_limit is not None:
|
||||
style["stroke-miterlimit"] = group.stroke.miter_limit
|
||||
|
||||
if group.stroke.line_cap == objects.LineCap.Round:
|
||||
style["stroke-linecap"] = "round"
|
||||
elif group.stroke.line_cap == objects.LineCap.Butt:
|
||||
style["stroke-linecap"] = "butt"
|
||||
elif group.stroke.line_cap == objects.LineCap.Square:
|
||||
style["stroke-linecap"] = "square"
|
||||
|
||||
if group.stroke.line_join == objects.LineJoin.Round:
|
||||
style["stroke-linejoin"] = "round"
|
||||
elif group.stroke.line_join == objects.LineJoin.Bevel:
|
||||
style["stroke-linejoin"] = "bevel"
|
||||
elif group.stroke.line_join == objects.LineJoin.Miter:
|
||||
style["stroke-linejoin"] = "miter"
|
||||
|
||||
if group.stroke.dashes:
|
||||
dasharray = []
|
||||
last = 0
|
||||
last_mode = objects.StrokeDashType.Dash
|
||||
for dash in group.stroke.dashes:
|
||||
if last_mode == dash.type:
|
||||
last += dash.length.get_value(self.time)
|
||||
else:
|
||||
if last_mode != objects.StrokeDashType.Offset:
|
||||
dasharray.append(str(last))
|
||||
last = 0
|
||||
last_mode = dash.type
|
||||
style["stroke-dasharray"] = " ".join(dasharray)
|
||||
return style
|
||||
|
||||
def _style_to_css(self, style):
|
||||
return ";".join(map(
|
||||
lambda x: ":".join(map(str, x)),
|
||||
style.items()
|
||||
))
|
||||
|
||||
def _split_stroke(self, group, fill_layer, out_parent):
|
||||
if not group.stroke:# or group.stroke_above:
|
||||
return
|
||||
|
||||
style = self._get_group_stroke(group)
|
||||
if style.get("stroke-width", 0) <= 0 or style["stroke-opacity"] <= 0:
|
||||
return
|
||||
|
||||
if group.stroke_above:
|
||||
if fill_layer.attrib.get("style", ""):
|
||||
fill_layer.attrib["style"] += ";"
|
||||
else:
|
||||
fill_layer.attrib["style"] = ""
|
||||
fill_layer.attrib["style"] += self._style_to_css(style)
|
||||
return fill_layer
|
||||
|
||||
g = ElementTree.Element("g")
|
||||
self.set_clean_id(g, "stroke")
|
||||
use = ElementTree.Element("use")
|
||||
for i, e in enumerate(out_parent):
|
||||
if e is fill_layer:
|
||||
out_parent.insert(i, g)
|
||||
out_parent.remove(fill_layer)
|
||||
break
|
||||
else:
|
||||
return
|
||||
|
||||
g.append(use)
|
||||
g.append(fill_layer)
|
||||
|
||||
use.attrib[self.qualified("xlink", "href")] = "#" + fill_layer.attrib["id"]
|
||||
use.attrib["style"] = self._style_to_css(style)
|
||||
return g
|
||||
|
||||
def group_to_style(self, group):
|
||||
style = {}
|
||||
if group.fill:
|
||||
style["fill-opacity"] = group.fill.opacity.get_value(self.time) / 100
|
||||
if isinstance(group.fill, objects.GradientFill):
|
||||
style["fill"] = "url(#%s)" % self.process_gradient(group.fill)
|
||||
else:
|
||||
style["fill"] = color_to_css(group.fill.color.get_value(self.time))
|
||||
|
||||
if group.fill.fill_rule:
|
||||
style["fill-rule"] = "evenodd" if group.fill.fill_rule == objects.FillRule.EvenOdd else "nonzero"
|
||||
|
||||
if group.lottie.hidden:
|
||||
style["display"] = "none"
|
||||
#if group.stroke_above:
|
||||
#style.update(self._get_group_stroke(group))
|
||||
|
||||
return self._style_to_css(style)
|
||||
|
||||
def process_gradient(self, gradient):
|
||||
spos = gradient.start_point.get_value(self.time)
|
||||
epos = gradient.end_point.get_value(self.time)
|
||||
|
||||
if gradient.gradient_type == objects.GradientType.Linear:
|
||||
dom = ElementTree.SubElement(self.defs, "linearGradient")
|
||||
dom.attrib["x1"] = str(spos[0])
|
||||
dom.attrib["y1"] = str(spos[1])
|
||||
dom.attrib["x2"] = str(epos[0])
|
||||
dom.attrib["y2"] = str(epos[1])
|
||||
elif gradient.gradient_type == objects.GradientType.Radial:
|
||||
dom = ElementTree.SubElement(self.defs, "radialGradient")
|
||||
dom.attrib["cx"] = str(spos[0])
|
||||
dom.attrib["cy"] = str(spos[1])
|
||||
dom.attrib["r"] = str((epos-spos).length)
|
||||
a = gradient.highlight_angle.get_value(self.time) * math.pi / 180
|
||||
l = gradient.highlight_length.get_value(self.time)
|
||||
dom.attrib["fx"] = str(spos[0] + math.cos(a) * l)
|
||||
dom.attrib["fy"] = str(spos[1] + math.sin(a) * l)
|
||||
|
||||
id = self.set_id(dom, gradient, force=True)
|
||||
dom.attrib["gradientUnits"] = "userSpaceOnUse"
|
||||
|
||||
for off, color in gradient.colors.stops_at(self.time):
|
||||
stop = ElementTree.SubElement(dom, "stop")
|
||||
stop.attrib["offset"] = "%s%%" % (off * 100)
|
||||
stop.attrib["stop-color"] = color_to_css(color[:3])
|
||||
if len(color) > 3:
|
||||
stop.attrib["stop-opacity"] = str(color[3])
|
||||
|
||||
return id
|
||||
|
||||
def group_from_lottie(self, lottie, dom_parent, layer):
|
||||
g = ElementTree.SubElement(dom_parent, "g")
|
||||
if layer and self.name_mode == NameMode.Inkscape:
|
||||
g.attrib[self.qualified("inkscape", "groupmode")] = "layer"
|
||||
self.set_id(g, lottie, force=True)
|
||||
self.set_transform(g, lottie.transform, getattr(lottie, "auto_orient", False))
|
||||
return g
|
||||
|
||||
def _on_shapegroup(self, group, dom_parent):
|
||||
if group.empty():
|
||||
return
|
||||
|
||||
if len(group.children) == 1 and isinstance(group.children[0], restructure.RestructuredPathMerger):
|
||||
path = self.build_path(group.paths.paths, dom_parent)
|
||||
self.set_id(path, group.paths.paths[0], force=True)
|
||||
path.attrib["style"] = self.group_to_style(group)
|
||||
self.set_transform(path, group.lottie.transform)
|
||||
return self._split_stroke(group, path, dom_parent)
|
||||
|
||||
g = self.group_from_lottie(group.lottie, dom_parent, group.layer)
|
||||
g.attrib["style"] = self.group_to_style(group)
|
||||
self.shapegroup_process_children(group, g)
|
||||
return self._split_stroke(group, g, dom_parent)
|
||||
|
||||
def _on_merged_path(self, shape, shapegroup, out_parent):
|
||||
path = self.build_path(shape.paths, out_parent)
|
||||
self.set_id(path, shape.paths[0])
|
||||
path.attrib["style"] = self.group_to_style(shapegroup)
|
||||
#self._split_stroke(shapegroup, path, out_parent)
|
||||
return path
|
||||
|
||||
def _on_shape(self, shape, shapegroup, out_parent):
|
||||
if isinstance(shape, objects.Rect):
|
||||
svgshape = self.build_rect(shape, out_parent)
|
||||
elif isinstance(shape, objects.Ellipse):
|
||||
svgshape = self.build_ellipse(shape, out_parent)
|
||||
elif isinstance(shape, objects.Star):
|
||||
svgshape = self.build_path([shape.to_bezier()], out_parent)
|
||||
elif isinstance(shape, objects.Path):
|
||||
svgshape = self.build_path([shape], out_parent)
|
||||
elif has_font and isinstance(shape, font.FontShape):
|
||||
svgshape = self.build_text(shape, out_parent)
|
||||
else:
|
||||
return
|
||||
self.set_id(svgshape, shape, force=True)
|
||||
if "style" not in svgshape.attrib:
|
||||
svgshape.attrib["style"] = ""
|
||||
svgshape.attrib["style"] += self.group_to_style(shapegroup)
|
||||
#self._split_stroke(shapegroup, svgshape, out_parent)
|
||||
|
||||
if shape.hidden:
|
||||
svgshape.attrib["style"] += "display: none;"
|
||||
return svgshape
|
||||
|
||||
def build_rect(self, shape, parent):
|
||||
rect = ElementTree.SubElement(parent, "rect")
|
||||
size = shape.size.get_value(self.time)
|
||||
pos = shape.position.get_value(self.time)
|
||||
rect.attrib["width"] = str(size[0])
|
||||
rect.attrib["height"] = str(size[1])
|
||||
rect.attrib["x"] = str(pos[0] - size[0] / 2)
|
||||
rect.attrib["y"] = str(pos[1] - size[1] / 2)
|
||||
rect.attrib["rx"] = str(shape.rounded.get_value(self.time))
|
||||
return rect
|
||||
|
||||
def build_ellipse(self, shape, parent):
|
||||
ellipse = ElementTree.SubElement(parent, "ellipse")
|
||||
size = shape.size.get_value(self.time)
|
||||
pos = shape.position.get_value(self.time)
|
||||
ellipse.attrib["rx"] = str(size[0] / 2)
|
||||
ellipse.attrib["ry"] = str(size[1] / 2)
|
||||
ellipse.attrib["cx"] = str(pos[0])
|
||||
ellipse.attrib["cy"] = str(pos[1])
|
||||
return ellipse
|
||||
|
||||
def build_path(self, shapes, parent):
|
||||
path = ElementTree.SubElement(parent, "path")
|
||||
d = ""
|
||||
for shape in shapes:
|
||||
bez = shape.shape.get_value(self.time)
|
||||
if isinstance(bez, list):
|
||||
bez = bez[0]
|
||||
if not bez.vertices:
|
||||
continue
|
||||
if d:
|
||||
d += "\n"
|
||||
d += self._bezier_to_d(bez)
|
||||
|
||||
path.attrib["d"] = d
|
||||
return path
|
||||
|
||||
def _bezier_tangent(self, tangent):
|
||||
_tangent_threshold = 0.5
|
||||
if tangent.length < _tangent_threshold:
|
||||
return NVector(0, 0)
|
||||
return tangent
|
||||
|
||||
def _bezier_to_d(self, bez):
|
||||
d = "M %s,%s " % tuple(bez.vertices[0].components[:2])
|
||||
for i in range(1, len(bez.vertices)):
|
||||
qfrom = bez.vertices[i-1]
|
||||
h1 = self._bezier_tangent(bez.out_tangents[i-1]) + qfrom
|
||||
qto = bez.vertices[i]
|
||||
h2 = self._bezier_tangent(bez.in_tangents[i]) + qto
|
||||
|
||||
d += "C %s,%s %s,%s %s,%s " % (
|
||||
h1[0], h1[1],
|
||||
h2[0], h2[1],
|
||||
qto[0], qto[1],
|
||||
)
|
||||
if bez.closed:
|
||||
qfrom = bez.vertices[-1]
|
||||
h1 = self._bezier_tangent(bez.out_tangents[-1]) + qfrom
|
||||
qto = bez.vertices[0]
|
||||
h2 = self._bezier_tangent(bez.in_tangents[0]) + qto
|
||||
d += "C %s,%s %s,%s %s,%s Z" % (
|
||||
h1[0], h1[1],
|
||||
h2[0], h2[1],
|
||||
qto[0], qto[1],
|
||||
)
|
||||
|
||||
return d
|
||||
|
||||
def _on_shape_modifier(self, shape, shapegroup, out_parent):
|
||||
if isinstance(shape.lottie, objects.Repeater):
|
||||
svgshape = self.build_repeater(shape.lottie, shape.child, shapegroup, out_parent)
|
||||
elif isinstance(shape.lottie, objects.RoundedCorners):
|
||||
svgshape = self.build_rouded_corners(shape.lottie, shape.child, shapegroup, out_parent)
|
||||
elif isinstance(shape.lottie, objects.Trim):
|
||||
svgshape = self.build_trim_path(shape.lottie, shape.child, shapegroup, out_parent)
|
||||
else:
|
||||
return self.shapegroup_process_child(shape.child, shapegroup, out_parent)
|
||||
return svgshape
|
||||
|
||||
def build_repeater(self, shape, child, shapegroup, out_parent):
|
||||
original = self.shapegroup_process_child(child, shapegroup, out_parent)
|
||||
if not original:
|
||||
return
|
||||
|
||||
ncopies = int(round(shape.copies.get_value(self.time)))
|
||||
if ncopies == 1:
|
||||
return
|
||||
|
||||
out_parent.remove(original)
|
||||
|
||||
g = ElementTree.SubElement(out_parent, "g")
|
||||
self.set_clean_id(g, "repeater")
|
||||
|
||||
for copy in range(ncopies-1):
|
||||
use = ElementTree.SubElement(g, "use")
|
||||
use.attrib[self.qualified("xlink", "href")] = "#" + original.attrib["id"]
|
||||
|
||||
orig_wrapper = ElementTree.SubElement(g, "g")
|
||||
orig_wrapper.append(original)
|
||||
|
||||
transform = objects.Transform()
|
||||
so = shape.transform.start_opacity.get_value(self.time)
|
||||
eo = shape.transform.end_opacity.get_value(self.time)
|
||||
position = shape.transform.position.get_value(self.time)
|
||||
rotation = shape.transform.rotation.get_value(self.time)
|
||||
anchor_point = shape.transform.anchor_point.get_value(self.time)
|
||||
for i in range(ncopies-1, -1, -1):
|
||||
of = i / (ncopies-1)
|
||||
transform.opacity.value = so * of + eo * (1 - of)
|
||||
self.set_transform(g[i], transform)
|
||||
transform.position.value += position
|
||||
transform.rotation.value += rotation
|
||||
transform.anchor_point.value += anchor_point
|
||||
|
||||
return g
|
||||
|
||||
def build_rouded_corners(self, shape, child, shapegroup, out_parent):
|
||||
round_amount = shape.radius.get_value(self.time)
|
||||
return self._modifier_process(child, shapegroup, out_parent, self._build_rouded_corners_shape, round_amount)
|
||||
|
||||
def _build_rouded_corners_shape(self, shape, round_amount):
|
||||
if not isinstance(shape, objects.Shape):
|
||||
return [shape]
|
||||
path = shape.to_bezier()
|
||||
bezier = path.shape.get_value(self.time).rounded(round_amount)
|
||||
path.shape.clear_animation(bezier)
|
||||
return [path]
|
||||
|
||||
def build_trim_path(self, shape, child, shapegroup, out_parent):
|
||||
start = max(0, min(1, shape.start.get_value(self.time) / 100))
|
||||
end = max(0, min(1, shape.end.get_value(self.time) / 100))
|
||||
offset = shape.offset.get_value(self.time) / 360 % 1
|
||||
|
||||
multidata = {}
|
||||
length = 0
|
||||
|
||||
if shape.multiple == objects.TrimMultipleShapes.Individually:
|
||||
for visishape in reversed(list(self._modifier_foreach_shape(child))):
|
||||
bez = visishape.to_bezier().shape.get_value(self.time)
|
||||
local_length = bez.rough_length()
|
||||
multidata[visishape] = (bez, length, local_length)
|
||||
length += local_length
|
||||
|
||||
return self._modifier_process(
|
||||
child, shapegroup, out_parent, self._build_trim_path_shape,
|
||||
start+offset, end+offset, multidata, length
|
||||
)
|
||||
|
||||
def _modifier_foreach_shape(self, shape):
|
||||
if isinstance(shape, restructure.RestructuredShapeGroup):
|
||||
for child in shape.children:
|
||||
for chsh in self._modifier_foreach_shape(child):
|
||||
yield chsh
|
||||
elif isinstance(shape, restructure.RestructuredPathMerger):
|
||||
for p in shape.paths:
|
||||
yield p
|
||||
elif isinstance(shape, objects.Shape):
|
||||
yield shape
|
||||
|
||||
def _modifier_process(self, child, shapegroup, out_parent, callback, *args):
|
||||
children = self._modifier_process_child(child, shapegroup, out_parent, callback, *args)
|
||||
return [self.shapegroup_process_child(ch, shapegroup, out_parent) for ch in children]
|
||||
|
||||
def _trim_offlocal(self, t, local_start, local_length, total_length):
|
||||
gt = (t * total_length - local_start) / local_length
|
||||
return max(0, min(1, gt))
|
||||
|
||||
def _build_trim_path_shape(self, shape, start, end, multidata, total_length):
|
||||
if not isinstance(shape, objects.Shape):
|
||||
return [shape]
|
||||
|
||||
if multidata:
|
||||
bezier, local_start, local_length = multidata[shape]
|
||||
if end > 1:
|
||||
lstart = self._trim_offlocal(start, local_start, local_length, total_length)
|
||||
lend = self._trim_offlocal(end-1, local_start, local_length, total_length)
|
||||
out = []
|
||||
if lstart < 1:
|
||||
out.append(objects.Path(bezier.segment(lstart, 1)))
|
||||
if lend > 0:
|
||||
out.append(objects.Path(bezier.segment(0, lend)))
|
||||
return out
|
||||
|
||||
lstart = self._trim_offlocal(start, local_start, local_length, total_length)
|
||||
lend = self._trim_offlocal(end, local_start, local_length, total_length)
|
||||
if lend <= 0 or lstart >= 1:
|
||||
return []
|
||||
if lstart <= 0 and lend >= 1:
|
||||
return [objects.Path(bezier)]
|
||||
seg = bezier.segment(lstart, lend)
|
||||
return [objects.Path(seg)]
|
||||
|
||||
path = shape.to_bezier()
|
||||
bezier = path.shape.get_value(self.time)
|
||||
if end > 1:
|
||||
bez1 = bezier.segment(start, 1)
|
||||
bez2 = bezier.segment(0, end-1)
|
||||
return [objects.Path(bez1), objects.Path(bez2)]
|
||||
else:
|
||||
seg = bezier.segment(start, end)
|
||||
return [objects.Path(seg)]
|
||||
|
||||
def _modifier_process_children(self, shapegroup, out_parent, callback, *args):
|
||||
children = []
|
||||
for shape in shapegroup.children:
|
||||
children.extend(self._modifier_process_child(shape, shapegroup, out_parent, callback, *args))
|
||||
shapegroup.children = children
|
||||
|
||||
def _modifier_process_child(self, shape, shapegroup, out_parent, callback, *args):
|
||||
if isinstance(shape, restructure.RestructuredShapeGroup):
|
||||
self._modifier_process_children(shape, out_parent, callback, *args)
|
||||
return [shape]
|
||||
elif isinstance(shape, restructure.RestructuredPathMerger):
|
||||
paths = []
|
||||
for p in shape.paths:
|
||||
paths.extend(callback(p, *args))
|
||||
shape.paths = paths
|
||||
if paths:
|
||||
return [shape]
|
||||
return []
|
||||
else:
|
||||
return callback(shape, *args)
|
||||
|
||||
def _custom_object_supported(self, shape):
|
||||
if has_font and isinstance(shape, font.FontShape):
|
||||
return True
|
||||
return False
|
||||
|
||||
def build_text(self, shape, parent):
|
||||
text = ElementTree.SubElement(parent, "text")
|
||||
if "family" in shape.query:
|
||||
text.attrib["font-family"] = shape.query["family"]
|
||||
if "weight" in shape.query:
|
||||
text.attrib["font-weight"] = str(shape.query.weight_to_css())
|
||||
slant = int(shape.query.get("slant", 0))
|
||||
if slant > 0 and slant < 110:
|
||||
text.attrib["font-style"] = "italic"
|
||||
elif slant >= 110:
|
||||
text.attrib["font-style"] = "oblique"
|
||||
|
||||
text.attrib["font-size"] = str(shape.size)
|
||||
|
||||
text.attrib["white-space"] = "pre"
|
||||
|
||||
pos = shape.style.position
|
||||
text.attrib["x"] = str(pos.x)
|
||||
text.attrib["y"] = str(pos.y)
|
||||
text.text = shape.text
|
||||
|
||||
return text
|
||||
|
||||
|
||||
def color_to_css(color):
|
||||
#if len(color) == 4:
|
||||
#return ("rgba(%s, %s, %s" % tuple(map(lambda c: int(round(c*255)), color[:3]))) + ", %s)" % color[3]
|
||||
return "rgb(%s, %s, %s)" % tuple(map(lambda c: int(round(c*255)), color[:3]))
|
||||
|
||||
|
||||
def to_svg(animation, time):
|
||||
builder = SvgBuilder(time)
|
||||
builder.process(animation)
|
||||
return builder.dom
|
||||
Reference in New Issue
Block a user