mirror of
https://github.com/OpenVGLab/OmniLottie.git
synced 2026-09-17 15:46:26 +00:00
720 lines
28 KiB
Python
720 lines
28 KiB
Python
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
|